mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-05-06 01:32:36 +02:00
187 lines
4.5 KiB
Go
187 lines
4.5 KiB
Go
//go:build sentinel_native
|
|
|
|
package engines
|
|
|
|
/*
|
|
#cgo LDFLAGS: -L${SRCDIR}/../../../../sentinel-core/target/release -lsentinel_core -ldl -lm -lpthread
|
|
#cgo CFLAGS: -I${SRCDIR}/../../../../sentinel-core/include
|
|
|
|
#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
|
|
type NativeSentinelCore struct {
|
|
mu sync.RWMutex
|
|
initialized bool
|
|
version string
|
|
lastCheck time.Time
|
|
}
|
|
|
|
// NewNativeSentinelCore creates the FFI bridge and initializes the Rust engine.
|
|
func NewNativeSentinelCore() (*NativeSentinelCore, error) {
|
|
result := C.sentinel_init()
|
|
if result != 0 {
|
|
return nil, fmt.Errorf("sentinel_init failed with code %d", int(result))
|
|
}
|
|
|
|
// Get version from Rust
|
|
cVer := C.sentinel_version()
|
|
version := "unknown"
|
|
if cVer != nil {
|
|
version = C.GoString(cVer)
|
|
C.sentinel_free(cVer)
|
|
}
|
|
|
|
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"`
|
|
EnginesTriggered int `json:"engines_triggered"`
|
|
ProcessingTimeUs uint64 `json:"processing_time_us"`
|
|
Indicators []string `json:"indicators"`
|
|
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 sentinelAnalyzeResult{Error: "engine not initialized"}
|
|
}
|
|
|
|
cText := C.CString(text)
|
|
defer C.free(unsafe.Pointer(cText))
|
|
|
|
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("engines=%d categories=%v", res.EnginesTriggered, res.Categories),
|
|
Indicators: res.Indicators,
|
|
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.
|
|
func (n *NativeSentinelCore) Status() EngineStatus {
|
|
n.mu.RLock()
|
|
defer n.mu.RUnlock()
|
|
|
|
if !n.initialized {
|
|
return EngineOffline
|
|
}
|
|
|
|
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.
|
|
func (n *NativeSentinelCore) Name() string {
|
|
return "sentinel-core"
|
|
}
|
|
|
|
// Version returns the native library version.
|
|
func (n *NativeSentinelCore) Version() string {
|
|
return n.version
|
|
}
|
|
|
|
// Shutdown gracefully closes the FFI bridge.
|
|
func (n *NativeSentinelCore) Shutdown() error {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
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
|
|
}
|