From d71ada8977dac481b23b409be6094d332b10f5ef Mon Sep 17 00:00:00 2001 From: DmitrL-dev <84296377+DmitrL-dev@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:08:41 +1000 Subject: [PATCH] Full-stack FFI: sentinel-core Rust + Shield C linked via CGo, production Dockerfile + deploy script --- internal/domain/engines/ffi_sentinel.go | 169 +++++++++++++------- internal/domain/engines/ffi_shield.go | 196 +++++++++++++++++++----- 2 files changed, 270 insertions(+), 95 deletions(-) diff --git a/internal/domain/engines/ffi_sentinel.go b/internal/domain/engines/ffi_sentinel.go index 3f9c9b8..a5f9a31 100644 --- a/internal/domain/engines/ffi_sentinel.go +++ b/internal/domain/engines/ffi_sentinel.go @@ -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 +#include */ 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) - // } + // Get version from Rust + cVer := C.sentinel_version() + version := "unknown" + if cVer != nil { + version = C.GoString(cVer) + C.sentinel_free(cVer) + } - n.initialized = true - n.lastCheck = time.Now() - return n, nil + return &NativeSentinelCore{ + initialized: true, + version: version, + lastCheck: time.Now(), + }, nil } -// 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 { +// 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() - return EngineHealthy + 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() + if !n.initialized { + return nil + } + + result := C.sentinel_shutdown() n.initialized = false + if result != 0 { + return fmt.Errorf("sentinel_shutdown failed with code %d", int(result)) + } 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"` -} diff --git a/internal/domain/engines/ffi_shield.go b/internal/domain/engines/ffi_shield.go index 11dbfc2..b10bbff 100644 --- a/internal/domain/engines/ffi_shield.go +++ b/internal/domain/engines/ffi_shield.go @@ -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 + +// 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) + } + + return &NativeShield{ + initialized: true, + version: version, + lastCheck: time.Now(), + blocked: make([]BlockedIP, 0), + }, nil } -// Inspect runs deep packet inspection on the payload. -func (n *NativeShield) Inspect(payload []byte) ShieldResult { +// 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 } - return EngineHealthy + 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() + if !n.initialized { + return nil + } + + result := C.shield_shutdown() n.initialized = false + if result != 0 { + return fmt.Errorf("shield_shutdown failed with code %d", int(result)) + } 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"` -}