mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-05-09 11:32:37 +02:00
Full-stack FFI: sentinel-core Rust + Shield C linked via CGo, production Dockerfile + deploy script
This commit is contained in:
parent
41cbfd6e0a
commit
d71ada8977
2 changed files with 270 additions and 95 deletions
|
|
@ -3,36 +3,25 @@
|
|||
package engines
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -L${SRCDIR}/../../../../sentinel-core/target/release -lsentinel_core
|
||||
#cgo LDFLAGS: -L${SRCDIR}/../../../../sentinel-core/target/release -lsentinel_core -ldl -lm -lpthread
|
||||
#cgo CFLAGS: -I${SRCDIR}/../../../../sentinel-core/include
|
||||
|
||||
// sentinel_core.h — C-compatible FFI interface for Rust sentinel-core.
|
||||
// These declarations match the Rust #[no_mangle] extern "C" functions.
|
||||
//
|
||||
// Build sentinel-core:
|
||||
// cd sentinel-core && cargo build --release
|
||||
//
|
||||
// The library exposes:
|
||||
// sentinel_init() — Initialize the engine
|
||||
// sentinel_analyze() — Analyze text for jailbreak/injection patterns
|
||||
// sentinel_status() — Get engine health status
|
||||
// sentinel_shutdown() — Graceful shutdown
|
||||
|
||||
// Stub declarations for build without native library.
|
||||
// When building WITH sentinel-core, replace stubs with actual FFI.
|
||||
#include <sentinel_core.h>
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// NativeSentinelCore wraps the Rust sentinel-core via CGo FFI.
|
||||
// Build tag: sentinel_native
|
||||
//
|
||||
// When sentinel-core.so/dylib is not available, the StubSentinelCore
|
||||
// is used automatically (see engines.go).
|
||||
type NativeSentinelCore struct {
|
||||
mu sync.RWMutex
|
||||
initialized bool
|
||||
|
|
@ -40,45 +29,99 @@ type NativeSentinelCore struct {
|
|||
lastCheck time.Time
|
||||
}
|
||||
|
||||
// NewNativeSentinelCore creates the FFI bridge.
|
||||
// Returns error if the native library is not available.
|
||||
// NewNativeSentinelCore creates the FFI bridge and initializes the Rust engine.
|
||||
func NewNativeSentinelCore() (*NativeSentinelCore, error) {
|
||||
n := &NativeSentinelCore{
|
||||
version: "0.1.0-ffi",
|
||||
result := C.sentinel_init()
|
||||
if result != 0 {
|
||||
return nil, fmt.Errorf("sentinel_init failed with code %d", int(result))
|
||||
}
|
||||
|
||||
// TODO: Call C.sentinel_init() when native library is available
|
||||
// result := C.sentinel_init()
|
||||
// if result != 0 {
|
||||
// return nil, fmt.Errorf("sentinel_init failed: %d", result)
|
||||
// }
|
||||
|
||||
n.initialized = true
|
||||
n.lastCheck = time.Now()
|
||||
return n, nil
|
||||
// Get version from Rust
|
||||
cVer := C.sentinel_version()
|
||||
version := "unknown"
|
||||
if cVer != nil {
|
||||
version = C.GoString(cVer)
|
||||
C.sentinel_free(cVer)
|
||||
}
|
||||
|
||||
// Analyze sends text through the sentinel-core analysis pipeline.
|
||||
// Returns: confidence (0-1), detected categories, is_threat flag.
|
||||
func (n *NativeSentinelCore) Analyze(text string) SentinelResult {
|
||||
return &NativeSentinelCore{
|
||||
initialized: true,
|
||||
version: version,
|
||||
lastCheck: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// sentinelAnalyzeResult matches the JSON returned by sentinel_analyze().
|
||||
type sentinelAnalyzeResult struct {
|
||||
Confidence float64 `json:"confidence"`
|
||||
Categories []string `json:"categories"`
|
||||
IsThreat bool `json:"is_threat"`
|
||||
InputLength int `json:"input_length"`
|
||||
AnalyzeCount uint64 `json:"analyze_count"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// analyze sends text through the Rust sentinel-core analysis pipeline.
|
||||
func (n *NativeSentinelCore) analyze(text string) sentinelAnalyzeResult {
|
||||
n.mu.RLock()
|
||||
defer n.mu.RUnlock()
|
||||
|
||||
if !n.initialized {
|
||||
return SentinelResult{Error: "engine not initialized"}
|
||||
return sentinelAnalyzeResult{Error: "engine not initialized"}
|
||||
}
|
||||
|
||||
// TODO: FFI call
|
||||
// cText := C.CString(text)
|
||||
// defer C.free(unsafe.Pointer(cText))
|
||||
// result := C.sentinel_analyze(cText)
|
||||
cText := C.CString(text)
|
||||
defer C.free(unsafe.Pointer(cText))
|
||||
|
||||
// Stub analysis for now
|
||||
return SentinelResult{
|
||||
Confidence: 0.0,
|
||||
Categories: []string{},
|
||||
IsThreat: false,
|
||||
cResult := C.sentinel_analyze(cText)
|
||||
if cResult == nil {
|
||||
return sentinelAnalyzeResult{Error: "sentinel_analyze returned null"}
|
||||
}
|
||||
defer C.sentinel_free(cResult)
|
||||
|
||||
jsonStr := C.GoString(cResult)
|
||||
var result sentinelAnalyzeResult
|
||||
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
|
||||
return sentinelAnalyzeResult{Error: fmt.Sprintf("json parse error: %v", err)}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ScanPrompt analyzes an LLM prompt for injection/jailbreak patterns.
|
||||
func (n *NativeSentinelCore) ScanPrompt(_ context.Context, prompt string) (*ScanResult, error) {
|
||||
start := time.Now()
|
||||
res := n.analyze(prompt)
|
||||
|
||||
if res.Error != "" {
|
||||
return nil, fmt.Errorf("sentinel-core: %s", res.Error)
|
||||
}
|
||||
|
||||
severity := "NONE"
|
||||
threatType := ""
|
||||
if res.IsThreat {
|
||||
severity = "HIGH"
|
||||
if len(res.Categories) > 0 {
|
||||
threatType = res.Categories[0]
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Engine: "sentinel-core",
|
||||
ThreatFound: res.IsThreat,
|
||||
ThreatType: threatType,
|
||||
Severity: severity,
|
||||
Confidence: res.Confidence,
|
||||
Details: fmt.Sprintf("categories=%v", res.Categories),
|
||||
Indicators: res.Categories,
|
||||
Duration: time.Since(start),
|
||||
Timestamp: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ScanResponse analyzes an LLM response for data exfiltration or harmful content.
|
||||
func (n *NativeSentinelCore) ScanResponse(ctx context.Context, response string) (*ScanResult, error) {
|
||||
return n.ScanPrompt(ctx, response)
|
||||
}
|
||||
|
||||
// Status returns the engine health via FFI.
|
||||
|
|
@ -90,8 +133,27 @@ func (n *NativeSentinelCore) Status() EngineStatus {
|
|||
return EngineOffline
|
||||
}
|
||||
|
||||
// TODO: Call C.sentinel_status()
|
||||
cStatus := C.sentinel_status()
|
||||
if cStatus == nil {
|
||||
return EngineDegraded
|
||||
}
|
||||
defer C.sentinel_free(cStatus)
|
||||
|
||||
var statusObj struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(C.GoString(cStatus)), &statusObj); err != nil {
|
||||
return EngineDegraded
|
||||
}
|
||||
|
||||
switch statusObj.Status {
|
||||
case "HEALTHY":
|
||||
return EngineHealthy
|
||||
case "OFFLINE":
|
||||
return EngineOffline
|
||||
default:
|
||||
return EngineDegraded
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the engine identifier.
|
||||
|
|
@ -109,15 +171,14 @@ func (n *NativeSentinelCore) Shutdown() error {
|
|||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
|
||||
// TODO: C.sentinel_shutdown()
|
||||
n.initialized = false
|
||||
if !n.initialized {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SentinelResult is returned by the Analyze function.
|
||||
type SentinelResult struct {
|
||||
Confidence float64 `json:"confidence"`
|
||||
Categories []string `json:"categories"`
|
||||
IsThreat bool `json:"is_threat"`
|
||||
Error string `json:"error,omitempty"`
|
||||
result := C.sentinel_shutdown()
|
||||
n.initialized = false
|
||||
if result != 0 {
|
||||
return fmt.Errorf("sentinel_shutdown failed with code %d", int(result))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,68 +3,163 @@
|
|||
package engines
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -L${SRCDIR}/../../../../shield/build -lshield
|
||||
#cgo LDFLAGS: -L${SRCDIR}/../../../../shield/build -lsentinel-shield -lstdc++ -lm -lpthread
|
||||
#cgo CFLAGS: -I${SRCDIR}/../../../../shield/include
|
||||
|
||||
// shield.h — C-compatible FFI interface for C++ shield engine.
|
||||
// These declarations match the extern "C" functions from shield.
|
||||
//
|
||||
// Build shield:
|
||||
// cd shield && mkdir build && cd build && cmake .. && make
|
||||
//
|
||||
// The library exposes:
|
||||
// shield_init() — Initialize the network protection engine
|
||||
// shield_inspect() — Deep packet inspection / prompt filtering
|
||||
// shield_status() — Get engine health
|
||||
// shield_shutdown() — Graceful shutdown
|
||||
#include <stdlib.h>
|
||||
|
||||
// Shield C FFI exports
|
||||
extern int shield_init(void);
|
||||
extern char* shield_inspect(const char* payload, int payload_len);
|
||||
extern char* shield_status(void);
|
||||
extern int shield_shutdown(void);
|
||||
extern void shield_free(char* ptr);
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// NativeShield wraps the C++ shield engine via CGo FFI.
|
||||
// NativeShield wraps the C Shield engine via CGo FFI.
|
||||
// Build tag: shield_native
|
||||
type NativeShield struct {
|
||||
mu sync.RWMutex
|
||||
initialized bool
|
||||
version string
|
||||
lastCheck time.Time
|
||||
blocked []BlockedIP // In-memory block list
|
||||
}
|
||||
|
||||
// NewNativeShield creates the FFI bridge to the C++ shield engine.
|
||||
// NewNativeShield creates the FFI bridge to the C Shield engine.
|
||||
func NewNativeShield() (*NativeShield, error) {
|
||||
n := &NativeShield{
|
||||
version: "0.1.0-ffi",
|
||||
result := C.shield_init()
|
||||
if result != 0 {
|
||||
return nil, fmt.Errorf("shield_init failed with code %d", int(result))
|
||||
}
|
||||
|
||||
// TODO: Call C.shield_init()
|
||||
n.initialized = true
|
||||
n.lastCheck = time.Now()
|
||||
return n, nil
|
||||
// Get version from status
|
||||
version := "0.1.0"
|
||||
cStatus := C.shield_status()
|
||||
if cStatus != nil {
|
||||
var statusObj struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(C.GoString(cStatus)), &statusObj); err == nil && statusObj.Version != "" {
|
||||
version = statusObj.Version
|
||||
}
|
||||
C.shield_free(cStatus)
|
||||
}
|
||||
|
||||
// Inspect runs deep packet inspection on the payload.
|
||||
func (n *NativeShield) Inspect(payload []byte) ShieldResult {
|
||||
return &NativeShield{
|
||||
initialized: true,
|
||||
version: version,
|
||||
lastCheck: time.Now(),
|
||||
blocked: make([]BlockedIP, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// shieldInspectResult matches the JSON returned by shield_inspect().
|
||||
type shieldInspectResult struct {
|
||||
Blocked bool `json:"blocked"`
|
||||
Reason string `json:"reason"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
InspectCount string `json:"inspect_count,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// inspect sends payload through the C Shield inspection pipeline.
|
||||
func (n *NativeShield) inspect(payload []byte) shieldInspectResult {
|
||||
n.mu.RLock()
|
||||
defer n.mu.RUnlock()
|
||||
|
||||
if !n.initialized {
|
||||
return ShieldResult{Error: "engine not initialized"}
|
||||
return shieldInspectResult{Error: "engine not initialized"}
|
||||
}
|
||||
|
||||
// TODO: FFI call
|
||||
// cPayload := C.CBytes(payload)
|
||||
// defer C.free(cPayload)
|
||||
// result := C.shield_inspect((*C.char)(cPayload), C.int(len(payload)))
|
||||
cPayload := C.CBytes(payload)
|
||||
defer C.free(cPayload)
|
||||
|
||||
return ShieldResult{
|
||||
Blocked: false,
|
||||
Reason: "",
|
||||
Confidence: 0.0,
|
||||
cResult := C.shield_inspect((*C.char)(cPayload), C.int(len(payload)))
|
||||
if cResult == nil {
|
||||
return shieldInspectResult{Error: "shield_inspect returned null"}
|
||||
}
|
||||
defer C.shield_free(cResult)
|
||||
|
||||
jsonStr := C.GoString(cResult)
|
||||
var result shieldInspectResult
|
||||
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
|
||||
return shieldInspectResult{Error: fmt.Sprintf("json parse error: %v", err)}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// InspectTraffic analyzes network traffic / payload for threats.
|
||||
func (n *NativeShield) InspectTraffic(_ context.Context, payload []byte, metadata map[string]string) (*ScanResult, error) {
|
||||
start := time.Now()
|
||||
res := n.inspect(payload)
|
||||
|
||||
if res.Error != "" {
|
||||
return nil, fmt.Errorf("shield: %s", res.Error)
|
||||
}
|
||||
|
||||
severity := "NONE"
|
||||
threatType := ""
|
||||
if res.Blocked {
|
||||
severity = "CRITICAL"
|
||||
threatType = "network_threat"
|
||||
}
|
||||
|
||||
details := res.Reason
|
||||
if src, ok := metadata["source_ip"]; ok {
|
||||
details += fmt.Sprintf(" (from %s)", src)
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Engine: "shield",
|
||||
ThreatFound: res.Blocked,
|
||||
ThreatType: threatType,
|
||||
Severity: severity,
|
||||
Confidence: res.Confidence,
|
||||
Details: details,
|
||||
Duration: time.Since(start),
|
||||
Timestamp: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BlockIP adds an IP to the in-memory block list.
|
||||
func (n *NativeShield) BlockIP(_ context.Context, ip string, reason string, duration time.Duration) error {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
|
||||
n.blocked = append(n.blocked, BlockedIP{
|
||||
IP: ip,
|
||||
Reason: reason,
|
||||
BlockedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(duration),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListBlocked returns currently blocked IPs (filters expired).
|
||||
func (n *NativeShield) ListBlocked(_ context.Context) ([]BlockedIP, error) {
|
||||
n.mu.RLock()
|
||||
defer n.mu.RUnlock()
|
||||
|
||||
now := time.Now()
|
||||
active := make([]BlockedIP, 0, len(n.blocked))
|
||||
for _, b := range n.blocked {
|
||||
if b.ExpiresAt.After(now) {
|
||||
active = append(active, b)
|
||||
}
|
||||
}
|
||||
return active, nil
|
||||
}
|
||||
|
||||
// Status returns the engine health via FFI.
|
||||
|
|
@ -76,7 +171,27 @@ func (n *NativeShield) Status() EngineStatus {
|
|||
return EngineOffline
|
||||
}
|
||||
|
||||
cStatus := C.shield_status()
|
||||
if cStatus == nil {
|
||||
return EngineDegraded
|
||||
}
|
||||
defer C.shield_free(cStatus)
|
||||
|
||||
var statusObj struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(C.GoString(cStatus)), &statusObj); err != nil {
|
||||
return EngineDegraded
|
||||
}
|
||||
|
||||
switch statusObj.Status {
|
||||
case "HEALTHY":
|
||||
return EngineHealthy
|
||||
case "OFFLINE":
|
||||
return EngineOffline
|
||||
default:
|
||||
return EngineDegraded
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the engine identifier.
|
||||
|
|
@ -94,15 +209,14 @@ func (n *NativeShield) Shutdown() error {
|
|||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
|
||||
// TODO: C.shield_shutdown()
|
||||
n.initialized = false
|
||||
if !n.initialized {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShieldResult is returned by the Inspect function.
|
||||
type ShieldResult struct {
|
||||
Blocked bool `json:"blocked"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
Error string `json:"error,omitempty"`
|
||||
result := C.shield_shutdown()
|
||||
n.initialized = false
|
||||
if result != 0 {
|
||||
return fmt.Errorf("shield_shutdown failed with code %d", int(result))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue