gomcp/internal/transport/http/server.go

390 lines
18 KiB
Go
Raw Normal View History

// Package httpserver provides an HTTP API transport for GoMCP SOC dashboard.
//
// Zero CGO: Uses ONLY Go stdlib net/http (supports HTTP/2 natively).
// Backward compatible: disabled by default (--http-port 0).
package httpserver
import (
"context"
"crypto/tls"
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"time"
shadowai "github.com/syntrex/gomcp/internal/application/shadow_ai"
appsoc "github.com/syntrex/gomcp/internal/application/soc"
"github.com/syntrex/gomcp/internal/domain/engines"
"github.com/syntrex/gomcp/internal/infrastructure/auth"
"github.com/syntrex/gomcp/internal/infrastructure/email"
"github.com/syntrex/gomcp/internal/infrastructure/tracing"
)
// Server provides HTTP API endpoints for SOC monitoring.
type Server struct {
socSvc *appsoc.Service
threatIntel *appsoc.ThreatIntelStore
shadowAI *shadowai.ShadowAIController
rbac *RBACMiddleware
rateLimiter *RateLimiter
metrics *Metrics
logger *RequestLogger
sentinelCore engines.SentinelCore
shieldEngine engines.Shield
jwtAuth *auth.JWTMiddleware
userStore *auth.UserStore
tenantStore *auth.TenantStore
emailService *email.Service
jwtSecret []byte
wsHub *WSHub
usageTracker *auth.UsageTracker
sovereignEnabled bool
sovereignMode string
pprofEnabled bool
port int
srv *http.Server
tlsCert string
tlsKey string
}
// New creates an HTTP server bound to the given port.
func New(socSvc *appsoc.Service, port int) *Server {
return &Server{
socSvc: socSvc,
port: port,
rbac: NewRBACMiddleware(RBACConfig{Enabled: false}),
rateLimiter: NewRateLimiter(context.Background(), 100, time.Minute),
metrics: NewMetrics(),
logger: NewRequestLogger(true),
wsHub: NewWSHub(),
}
}
// SetThreatIntel sets the threat intel store for API access.
func (s *Server) SetThreatIntel(store *appsoc.ThreatIntelStore) {
s.threatIntel = store
}
// SetShadowAI sets the Shadow AI Controller for API access.
func (s *Server) SetShadowAI(controller *shadowai.ShadowAIController) {
s.shadowAI = controller
}
// SetEmailService sets the email service for sending verification codes and alerts.
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
}
// SetShieldEngine sets the C-native Shield engine for payload inspection.
func (s *Server) SetShieldEngine(shield engines.Shield) {
s.shieldEngine = shield
}
// 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.
func (s *Server) SetJWTAuth(secret []byte, db ...*sql.DB) {
if len(secret) < 32 {
slog.Warn("JWT auth disabled: secret too short or not set")
return
}
s.jwtSecret = secret
s.jwtAuth = auth.NewJWTMiddleware(secret)
if len(db) > 0 && db[0] != nil {
s.userStore = auth.NewUserStore(db[0])
s.tenantStore = auth.NewTenantStore(db[0])
} else {
s.userStore = auth.NewUserStore()
}
slog.Info("JWT authentication enabled")
}
// SetUsageTracker sets the usage/quota tracker for scan metering.
func (s *Server) SetUsageTracker(tracker *auth.UsageTracker) {
s.usageTracker = tracker
}
// SetRBAC configures RBAC middleware with API key authentication (§17).
func (s *Server) SetRBAC(rbac *RBACMiddleware) {
s.rbac = rbac
}
// SetTLS enables TLS with the given certificate and key files.
// Cipher suites are hardened to AEAD-only (§P2 TLS hardening).
func (s *Server) SetTLS(certFile, keyFile string) {
s.tlsCert = certFile
s.tlsKey = keyFile
}
// StartEventBridge subscribes to the SOC EventBus and forwards events
// to the WSHub for real-time SSE/WebSocket dashboard streaming (§P1).
// Should be called once after server creation. Runs as a background goroutine.
func (s *Server) StartEventBridge(ctx context.Context) {
bus := s.socSvc.EventBus()
if bus == nil {
slog.Warn("event bridge: no EventBus available")
return
}
ch := bus.Subscribe("ws-hub-bridge")
go func() {
for {
select {
case <-ctx.Done():
bus.Unsubscribe("ws-hub-bridge")
return
case evt, ok := <-ch:
if !ok {
return
}
s.wsHub.Broadcast("soc_event", map[string]any{
"id": evt.ID,
"source": string(evt.Source),
"severity": string(evt.Severity),
"category": evt.Category,
"description": evt.Description,
"session_id": evt.SessionID,
})
}
}
}()
slog.Info("event bridge started: EventBus → WSHub")
}
// Start begins listening on the configured port. Blocks until ctx is cancelled.
func (s *Server) Start(ctx context.Context) error {
mux := http.NewServeMux()
// SOC API routes — read (requires Viewer role when RBAC enabled)
mux.HandleFunc("GET /api/soc/dashboard", s.rbac.Require(RoleViewer, s.handleDashboard))
mux.HandleFunc("GET /api/soc/events", s.rbac.Require(RoleViewer, s.handleEvents))
mux.HandleFunc("GET /api/soc/incidents", s.rbac.Require(RoleViewer, s.handleIncidents))
// Sprint 2: Advanced incident management (must be before generic {id})
mux.HandleFunc("GET /api/soc/incidents/advanced", s.rbac.Require(RoleViewer, s.handleIncidentsAdvanced))
mux.HandleFunc("POST /api/soc/incidents/bulk", s.rbac.Require(RoleAnalyst, s.handleIncidentsBulk))
mux.HandleFunc("GET /api/soc/incidents/export", s.rbac.Require(RoleViewer, s.handleIncidentsExport))
mux.HandleFunc("GET /api/soc/sla-config", s.rbac.Require(RoleViewer, s.handleSLAConfig))
mux.HandleFunc("GET /api/soc/incidents/{id}", s.rbac.Require(RoleViewer, s.handleIncidentDetail))
mux.HandleFunc("GET /api/soc/incidents/{id}/sla", s.rbac.Require(RoleViewer, s.handleIncidentSLA))
mux.HandleFunc("GET /api/soc/sensors", s.rbac.Require(RoleViewer, s.handleSensors))
mux.HandleFunc("GET /api/soc/clusters", s.rbac.Require(RoleViewer, s.handleClusters))
mux.HandleFunc("GET /api/soc/rules", s.rbac.Require(RoleViewer, s.handleRules))
mux.HandleFunc("GET /api/soc/killchain/{id}", s.rbac.Require(RoleViewer, s.handleKillChain))
mux.HandleFunc("GET /api/soc/stream", s.rbac.Require(RoleViewer, s.handleSSEStream))
mux.HandleFunc("GET /api/soc/threat-intel", s.rbac.Require(RoleAnalyst, s.handleThreatIntel))
mux.HandleFunc("GET /api/soc/webhook-stats", s.rbac.Require(RoleAnalyst, s.handleWebhookStats))
mux.HandleFunc("GET /api/soc/analytics", s.rbac.Require(RoleViewer, s.handleAnalytics))
// SOC API routes — write (requires Analyst/Sensor role when RBAC enabled)
mux.HandleFunc("POST /api/v1/soc/events", s.rbac.Require(RoleSensor, s.handleIngestEvent))
mux.HandleFunc("POST /api/v1/soc/events/batch", s.rbac.Require(RoleSensor, s.handleBatchIngest))
mux.HandleFunc("POST /api/soc/sensors/heartbeat", s.rbac.Require(RoleSensor, s.handleSensorHeartbeat))
mux.HandleFunc("POST /api/soc/incidents/{id}/verdict", s.rbac.Require(RoleAnalyst, s.handleVerdict))
// Case Management (SOAR §P3)
mux.HandleFunc("POST /api/soc/incidents/{id}/assign", s.rbac.Require(RoleAnalyst, s.handleIncidentAssign))
mux.HandleFunc("POST /api/soc/incidents/{id}/status", s.rbac.Require(RoleAnalyst, s.handleIncidentStatus))
mux.HandleFunc("GET /api/soc/incidents/{id}/notes", s.rbac.Require(RoleViewer, s.handleIncidentNotes))
mux.HandleFunc("POST /api/soc/incidents/{id}/notes", s.rbac.Require(RoleAnalyst, s.handleIncidentNotes))
mux.HandleFunc("GET /api/soc/incidents/{id}/timeline", s.rbac.Require(RoleViewer, s.handleIncidentTimeline))
mux.HandleFunc("GET /api/soc/incidents/{id}/detail", s.rbac.Require(RoleViewer, s.handleIncidentFullDetail))
// Webhook Management (SOAR §15)
mux.HandleFunc("GET /api/soc/webhooks", s.rbac.Require(RoleAnalyst, s.handleWebhooksGet))
mux.HandleFunc("POST /api/soc/webhooks", s.rbac.Require(RoleAdmin, s.handleWebhooksSet))
mux.HandleFunc("POST /api/soc/webhooks/test", s.rbac.Require(RoleAdmin, s.handleWebhooksTest))
mux.HandleFunc("POST /api/soc/sensors/register", s.rbac.Require(RoleAdmin, s.handleSensorRegister))
mux.HandleFunc("DELETE /api/soc/sensors/{id}", s.rbac.Require(RoleAdmin, s.handleSensorDelete))
// Admin routes (§9, §17)
mux.HandleFunc("GET /api/soc/audit", s.rbac.Require(RoleAdmin, s.handleAuditTrail))
mux.HandleFunc("GET /api/soc/keys", s.rbac.Require(RoleAdmin, s.handleListKeys))
// Zero-G Mode routes (§13.4)
mux.HandleFunc("GET /api/soc/zerog", s.rbac.Require(RoleAnalyst, s.handleZeroGStatus))
mux.HandleFunc("POST /api/soc/zerog/toggle", s.rbac.Require(RoleAdmin, s.handleZeroGToggle))
mux.HandleFunc("POST /api/soc/zerog/resolve", s.rbac.Require(RoleAnalyst, s.handleZeroGResolve))
// P2P SOC Sync routes (§14)
mux.HandleFunc("GET /api/soc/p2p/peers", s.rbac.Require(RoleAnalyst, s.handleP2PPeers))
mux.HandleFunc("POST /api/soc/p2p/peers", s.rbac.Require(RoleAdmin, s.handleP2PAddPeer))
mux.HandleFunc("DELETE /api/soc/p2p/peers/{id}", s.rbac.Require(RoleAdmin, s.handleP2PRemovePeer))
// Engine & Sovereign routes (§3, §4, §21)
mux.HandleFunc("GET /api/soc/engines", s.rbac.Require(RoleViewer, s.handleEngineStatus))
mux.HandleFunc("GET /api/soc/sovereign", s.rbac.Require(RoleAdmin, s.handleSovereignConfig))
// Anomaly detection (§5) + Playbook engine (§10)
mux.HandleFunc("GET /api/soc/anomaly/alerts", s.rbac.Require(RoleAnalyst, s.handleAnomalyAlerts))
mux.HandleFunc("GET /api/soc/anomaly/baselines", s.rbac.Require(RoleAnalyst, s.handleAnomalyBaselines))
mux.HandleFunc("GET /api/soc/playbooks", s.rbac.Require(RoleViewer, s.handlePlaybooks))
// Live updates — WebSocket-style SSE push (§20)
mux.HandleFunc("GET /api/soc/ws", s.rbac.Require(RoleViewer, s.wsHub.HandleSSEStream))
// Deep health, compliance, audit, explainability (§12, §15)
mux.HandleFunc("GET /api/soc/health/deep", s.rbac.Require(RoleViewer, s.handleDeepHealth))
mux.HandleFunc("GET /api/soc/compliance", s.rbac.Require(RoleAdmin, s.handleComplianceReport))
mux.HandleFunc("GET /api/soc/audit/trail", s.rbac.Require(RoleAnalyst, s.handleAuditTrailPage))
mux.HandleFunc("GET /api/soc/incidents/{id}/explain", s.rbac.Require(RoleAnalyst, s.handleIncidentExplain))
// Threat intel matching (§6) + Data retention (§19)
mux.HandleFunc("POST /api/soc/threat-intel/match", s.rbac.Require(RoleAnalyst, s.handleThreatIntelMatch))
mux.HandleFunc("GET /api/soc/retention", s.rbac.Require(RoleAdmin, s.handleRetentionPolicies))
// Shadow AI Control Module routes (§Shadow AI ТЗ)
mux.HandleFunc("GET /api/v1/shadow-ai/stats", s.rbac.Require(RoleViewer, s.handleShadowAIStats))
mux.HandleFunc("GET /api/v1/shadow-ai/events", s.rbac.Require(RoleViewer, s.handleShadowAIEvents))
mux.HandleFunc("GET /api/v1/shadow-ai/events/{id}", s.rbac.Require(RoleViewer, s.handleShadowAIEventDetail))
mux.HandleFunc("POST /api/v1/shadow-ai/block", s.rbac.Require(RoleAnalyst, s.handleShadowAIBlock))
mux.HandleFunc("POST /api/v1/shadow-ai/unblock", s.rbac.Require(RoleAnalyst, s.handleShadowAIUnblock))
mux.HandleFunc("POST /api/v1/shadow-ai/scan", s.rbac.Require(RoleAnalyst, s.handleShadowAIScan))
mux.HandleFunc("GET /api/v1/shadow-ai/integrations", s.rbac.Require(RoleViewer, s.handleShadowAIIntegrations))
mux.HandleFunc("GET /api/v1/shadow-ai/integrations/{vendor}/health", s.rbac.Require(RoleViewer, s.handleShadowAIVendorHealth))
mux.HandleFunc("GET /api/v1/shadow-ai/compliance", s.rbac.Require(RoleAdmin, s.handleShadowAICompliance))
mux.HandleFunc("POST /api/v1/shadow-ai/doc-review", s.rbac.Require(RoleAnalyst, s.handleShadowAIDocReview))
mux.HandleFunc("GET /api/v1/shadow-ai/doc-review/{id}", s.rbac.Require(RoleViewer, s.handleShadowAIDocReviewStatus))
mux.HandleFunc("GET /api/v1/shadow-ai/approvals", s.rbac.Require(RoleAnalyst, s.handleShadowAIPendingApprovals))
mux.HandleFunc("GET /api/v1/shadow-ai/approvals/tiers", s.rbac.Require(RoleViewer, s.handleShadowAIApprovalTiers))
mux.HandleFunc("POST /api/v1/shadow-ai/approvals/{id}/verdict", s.rbac.Require(RoleAnalyst, s.handleShadowAIApprovalVerdict))
// Observability — always public (unauthenticated, K8s probes)
mux.HandleFunc("GET /health", s.handleHealth)
mux.HandleFunc("GET /healthz", s.handleHealthz)
mux.HandleFunc("GET /readyz", s.handleReadyz)
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)
// Usage endpoint — returns scan quota for caller
mux.HandleFunc("GET /api/v1/usage", s.handleUsage)
// pprof debug endpoints (§P4C) — gated behind EnablePprof()
if s.pprofEnabled {
mux.HandleFunc("GET /debug/pprof/", s.handlePprof)
mux.HandleFunc("GET /debug/pprof/profile", s.handlePprofProfile)
mux.HandleFunc("GET /debug/pprof/heap", s.handlePprofHeap)
mux.HandleFunc("GET /debug/pprof/goroutine", s.handlePprofGoroutine)
slog.Info("pprof endpoints enabled", "path", "/debug/pprof/")
}
// Auth routes — login/refresh are public (JWT middleware exempts these)
if s.jwtAuth != nil {
loginLimiter := auth.NewRateLimiter(5, time.Minute)
mux.HandleFunc("POST /api/auth/login", auth.RateLimitMiddleware(loginLimiter, auth.HandleLogin(s.userStore, s.jwtSecret)))
mux.HandleFunc("POST /api/auth/refresh", auth.HandleRefresh(s.jwtSecret))
// Auth routes — require authentication
mux.HandleFunc("GET /api/auth/me", auth.HandleMe(s.userStore))
// User management (admin only)
mux.HandleFunc("GET /api/auth/users", auth.HandleListUsers(s.userStore))
mux.HandleFunc("POST /api/auth/users", auth.HandleCreateUser(s.userStore))
mux.HandleFunc("PUT /api/auth/users/{id}", auth.HandleUpdateUser(s.userStore))
mux.HandleFunc("DELETE /api/auth/users/{id}", auth.HandleDeleteUser(s.userStore))
// API key management
mux.HandleFunc("GET /api/auth/keys", auth.HandleListAPIKeys(s.userStore))
mux.HandleFunc("POST /api/auth/keys", auth.HandleCreateAPIKey(s.userStore))
mux.HandleFunc("DELETE /api/auth/keys/{id}", auth.HandleDeleteAPIKey(s.userStore))
// Tenant management (§SaaS multi-tenancy)
if s.tenantStore != nil {
registrationLimiter := auth.NewRateLimiter(3, time.Minute)
var emailFn auth.EmailSendFunc
if s.emailService != nil {
emailFn = s.emailService.SendVerificationCode
}
mux.HandleFunc("POST /api/auth/register", auth.RateLimitMiddleware(registrationLimiter, auth.HandleRegister(s.userStore, s.tenantStore, s.jwtSecret, emailFn)))
mux.HandleFunc("POST /api/auth/verify", auth.RateLimitMiddleware(registrationLimiter, auth.HandleVerifyEmail(s.userStore, s.tenantStore, s.jwtSecret)))
mux.HandleFunc("GET /api/auth/plans", auth.HandleListPlans())
mux.HandleFunc("GET /api/auth/tenant", auth.HandleGetTenant(s.tenantStore))
mux.HandleFunc("POST /api/auth/tenant/plan", auth.HandleUpdateTenantPlan(s.tenantStore))
mux.HandleFunc("GET /api/auth/billing", auth.HandleBillingStatus(s.tenantStore))
mux.HandleFunc("POST /api/billing/webhook", auth.HandleStripeWebhook(s.tenantStore))
}
}
// Build middleware chain: Tracing → Logger → Metrics → Rate Limiter → Security → CORS → [JWT] → mux
var handler http.Handler = mux
if s.jwtAuth != nil {
handler = s.jwtAuth.Middleware(handler)
}
handler = corsMiddleware(handler)
handler = securityHeadersMiddleware(handler)
handler = s.rateLimiter.Middleware(handler)
handler = s.metrics.Middleware(handler)
handler = s.logger.Middleware(handler)
handler = tracing.HTTPMiddleware(handler)
s.srv = &http.Server{
Addr: fmt.Sprintf(":%d", s.port),
Handler: handler,
ReadHeaderTimeout: 10 * time.Second,
// NOTE: WriteTimeout is intentionally 0 (disabled) to support SSE/WebSocket
// long-lived connections. ReadHeaderTimeout protects against slowloris.
// SSE keepalive (15s) ensures dead connections are detected.
IdleTimeout: 120 * time.Second,
}
// Graceful shutdown on context cancellation (applies to both TLS and plain HTTP).
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.srv.Shutdown(shutdownCtx); err != nil {
slog.Error("HTTP server shutdown error", "error", err)
}
}()
// Apply TLS if configured.
if s.tlsCert != "" && s.tlsKey != "" {
s.srv.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}
slog.Info("HTTPS API listening", "port", s.port, "cert", s.tlsCert, "min_version", "TLS1.2")
if err := s.srv.ListenAndServeTLS(s.tlsCert, s.tlsKey); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("https server: %w", err)
}
return nil
}
slog.Info("HTTP API listening", "port", s.port)
if err := s.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("http server: %w", err)
}
return nil
}
// Stop gracefully shuts down the HTTP server.
func (s *Server) Stop(ctx context.Context) error {
if s.srv == nil {
return nil
}
return s.srv.Shutdown(ctx)
}
// writeJSON writes a JSON response with the given status code.
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(v); err != nil {
slog.Error("failed to encode response", "error", err)
}
}
// writeError writes a JSON error response.
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}