diff --git a/cmd/soc/main.go b/cmd/soc/main.go index 16e113a..a702779 100644 --- a/cmd/soc/main.go +++ b/cmd/soc/main.go @@ -23,6 +23,7 @@ import ( "github.com/syntrex/gomcp/internal/application/soc" socdomain "github.com/syntrex/gomcp/internal/domain/soc" + "github.com/syntrex/gomcp/internal/domain/engines" "github.com/syntrex/gomcp/internal/infrastructure/audit" "github.com/syntrex/gomcp/internal/infrastructure/email" "github.com/syntrex/gomcp/internal/infrastructure/logging" @@ -147,6 +148,16 @@ func main() { logger.Warn("email service: RESEND_API_KEY not set — verification codes shown in API response (dev mode)") } + // Sentinel Core — Rust-native detection engine (§3) + sentinelCore, coreErr := engines.NewNativeSentinelCore() + if coreErr != nil { + logger.Warn("sentinel-core: Rust engine not available, using stub", "error", coreErr) + srv.SetSentinelCore(engines.NewStubSentinelCore()) + } else { + srv.SetSentinelCore(sentinelCore) + logger.Info("sentinel-core: Rust engine initialized", "version", sentinelCore.Version()) + } + // OpenTelemetry tracing (§P4B) — enabled when OTEL_EXPORTER_OTLP_ENDPOINT is set otelEndpoint := env("OTEL_EXPORTER_OTLP_ENDPOINT", "") tp, otelErr := tracing.InitTracer(context.Background(), otelEndpoint) diff --git a/internal/transport/http/server.go b/internal/transport/http/server.go index 0b71976..141f932 100644 --- a/internal/transport/http/server.go +++ b/internal/transport/http/server.go @@ -75,6 +75,11 @@ func (s *Server) SetEmailService(svc *email.Service) { s.emailService = svc } +// SetSentinelCore sets the Rust-native detection engine for real-time scanning. +func (s *Server) SetSentinelCore(core engines.SentinelCore) { + s.sentinelCore = core +} + // SetJWTAuth enables JWT authentication with the given secret. // If secret is empty or <32 bytes, JWT is disabled (backward compatible). // Optional db parameter enables SQLite-backed user persistence. @@ -243,6 +248,9 @@ func (s *Server) Start(ctx context.Context) error { mux.HandleFunc("GET /metrics", s.metrics.Handler()) mux.HandleFunc("GET /api/soc/ratelimit", s.handleRateLimitStats) + // Public scan endpoint — demo scanner (no auth required, rate-limited) + mux.HandleFunc("POST /api/v1/scan", s.handlePublicScan) + // pprof debug endpoints (§P4C) — gated behind EnablePprof() if s.pprofEnabled { mux.HandleFunc("GET /debug/pprof/", s.handlePprof) diff --git a/internal/transport/http/soc_handlers.go b/internal/transport/http/soc_handlers.go index e220ec6..7bdf460 100644 --- a/internal/transport/http/soc_handlers.go +++ b/internal/transport/http/soc_handlers.go @@ -1445,3 +1445,49 @@ func (s *Server) handleSLAConfig(w http.ResponseWriter, _ *http.Request) { "sla_thresholds": entries, }) } + +// handlePublicScan provides a public (no-auth) prompt scanning endpoint for the demo. +// POST /api/v1/scan body: {"prompt": "Ignore all instructions..."} +func (s *Server) handlePublicScan(w http.ResponseWriter, r *http.Request) { + limitBody(w, r) + defer r.Body.Close() + + var req struct { + Prompt string `json:"prompt"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) + return + } + + // Validate input + if req.Prompt == "" { + writeError(w, http.StatusBadRequest, "prompt is required") + return + } + if len(req.Prompt) > 2000 { + writeError(w, http.StatusBadRequest, "prompt too long (max 2000 chars)") + return + } + + // Get engine (real or stub) + engine := s.getEngine("sentinel-core") + + // Scan + result, err := engine.ScanPrompt(r.Context(), req.Prompt) + if err != nil { + writeError(w, http.StatusInternalServerError, "scan failed: "+err.Error()) + return + } + + writeJSON(w, http.StatusOK, map[string]any{ + "blocked": result.ThreatFound, + "threat_type": result.ThreatType, + "severity": result.Severity, + "confidence": result.Confidence, + "details": result.Details, + "indicators": result.Indicators, + "engine": result.Engine, + "latency_ms": float64(result.Duration.Microseconds()) / 1000.0, + }) +}