gomcp/cmd/soc/main.go

257 lines
8.3 KiB
Go

// Package main provides the standalone SOC API server entry point.
//
// Usage:
//
// go run ./cmd/soc/
// SOC_DB_PATH=/data/soc.db SOC_PORT=9100 go run ./cmd/soc/
// SOC_DB_DRIVER=postgres SOC_DB_DSN=postgres://sentinel:pass@localhost:5432/soc go run ./cmd/soc/
//
// SEC-003 Memory Safety: set GOMEMLIMIT, SOC_GOMAXPROCS for runtime hardening.
package main
import (
"context"
"database/sql"
"fmt"
"log/slog"
"os"
"os/signal"
"runtime"
"runtime/debug"
"strconv"
"syscall"
"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/auth"
"github.com/syntrex/gomcp/internal/infrastructure/audit"
"github.com/syntrex/gomcp/internal/infrastructure/email"
"github.com/syntrex/gomcp/internal/infrastructure/logging"
"github.com/syntrex/gomcp/internal/infrastructure/postgres"
"github.com/syntrex/gomcp/internal/infrastructure/sqlite"
"github.com/syntrex/gomcp/internal/infrastructure/tracing"
sochttp "github.com/syntrex/gomcp/internal/transport/http"
)
func main() {
// SEC-003: Top-level panic recovery — log stack trace before crash.
defer func() {
if r := recover(); r != nil {
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
fmt.Fprintf(os.Stderr, "SENTINEL SOC FATAL PANIC: %v\n%s\n", r, buf[:n])
os.Exit(2)
}
}()
// Structured logger: JSON for production, text for dev.
logFormat := env("SOC_LOG_FORMAT", "text")
logLevel := env("SOC_LOG_LEVEL", "info")
logger := logging.New(logFormat, logLevel)
slog.SetDefault(logger)
// SEC-003: Go runtime memory safety hardening.
configureMemorySafety(logger)
portStr := env("SOC_PORT", "9100")
dbPath := env("SOC_DB_PATH", "soc.db")
auditDir := env("SOC_AUDIT_DIR", ".")
port, err := strconv.Atoi(portStr)
if err != nil {
logger.Error("invalid port", "port", portStr, "error", err)
os.Exit(1)
}
logger.Info("starting SENTINEL SOC API",
"port", port,
"db", dbPath,
"log_format", logFormat,
"log_level", logLevel,
)
// Infrastructure — database driver selection.
dbDriver := env("SOC_DB_DRIVER", "sqlite")
dbDSN := env("SOC_DB_DSN", "")
var socRepo socdomain.SOCRepository
var dbCloser func() error
var sqlDB interface{} // raw DB reference for auth user store
switch dbDriver {
case "postgres":
if dbDSN == "" {
logger.Error("SOC_DB_DSN required for postgres driver")
os.Exit(1)
}
pgDB, err := postgres.Open(dbDSN, logger)
if err != nil {
logger.Error("PostgreSQL open failed", "error", err)
os.Exit(1)
}
dbCloser = pgDB.Close
socRepo = postgres.NewSOCRepo(pgDB)
sqlDB = pgDB.Pool() // pass PG pool to auth user/tenant stores
logger.Info("using PostgreSQL backend")
default: // "sqlite"
db, err := sqlite.Open(dbPath)
if err != nil {
logger.Error("database open failed", "path", dbPath, "error", err)
os.Exit(1)
}
dbCloser = db.Close
sqlDB = db // save for auth
repo, err := sqlite.NewSOCRepo(db)
if err != nil {
logger.Error("SOC repo init failed", "error", err)
os.Exit(1)
}
socRepo = repo
logger.Info("using SQLite backend", "path", dbPath)
}
defer dbCloser()
decisionLogger, err := audit.NewDecisionLogger(auditDir)
if err != nil {
logger.Warn("decision logger unavailable (continuing without audit trail)", "error", err)
decisionLogger = nil
}
// Service + HTTP
socSvc := soc.NewService(socRepo, decisionLogger)
srv := sochttp.New(socSvc, port)
// Threat Intelligence Store — always initialized for IOC enrichment (§6)
threatIntelStore := soc.NewThreatIntelStore()
threatIntelStore.AddDefaultFeeds()
socSvc.SetThreatIntel(threatIntelStore)
srv.SetThreatIntel(threatIntelStore)
// JWT Authentication (optional — set SOC_JWT_SECRET to enable)
if jwtSecret := env("SOC_JWT_SECRET", ""); jwtSecret != "" {
if db, ok := sqlDB.(*sql.DB); ok {
srv.SetJWTAuth([]byte(jwtSecret), db)
} else {
srv.SetJWTAuth([]byte(jwtSecret))
}
logger.Info("JWT authentication configured")
}
// Usage/quota tracking — metered free tier (1000 scans/month)
if db, ok := sqlDB.(*sql.DB); ok {
srv.SetUsageTracker(auth.NewUsageTracker(db))
logger.Info("usage tracker initialized (free tier: 1000 scans/month)")
}
// Email service — Resend (set RESEND_API_KEY to enable real email delivery)
if resendKey := env("RESEND_API_KEY", ""); resendKey != "" {
fromAddr := env("EMAIL_FROM", "SYNTREX <noreply@syntrex.pro>")
resendSender := email.NewResendSender(resendKey, fromAddr)
emailSvc := email.NewService(resendSender, "SYNTREX", fromAddr)
srv.SetEmailService(emailSvc)
logger.Info("email service configured", "provider", "Resend", "from", fromAddr)
} else {
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())
}
// Shield — C-native payload inspection engine (§4)
shieldEngine, shieldErr := engines.NewNativeShield()
if shieldErr != nil {
logger.Warn("shield: C engine not available, using stub", "error", shieldErr)
srv.SetShieldEngine(engines.NewStubShield())
} else {
srv.SetShieldEngine(shieldEngine)
logger.Info("shield: C engine initialized", "version", shieldEngine.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)
if otelErr != nil {
logger.Error("tracing init failed", "error", otelErr)
}
// Graceful shutdown via context
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
defer tracing.Shutdown(ctx, tp)
// STIX/TAXII Feed Sync (§P4A) — auto-enabled when OTX key is set
if otxKey := env("SOC_OTX_API_KEY", ""); otxKey != "" {
otxFeed := soc.DefaultOTXFeed(otxKey)
feedSync := soc.NewFeedSync(threatIntelStore, []soc.STIXFeedConfig{otxFeed})
feedSync.Start(ctx.Done())
logger.Info("STIX feed sync started", "feeds", 1, "feed", otxFeed.Name)
}
// Start background retention scheduler (§19)
socSvc.StartRetentionScheduler(ctx, 0) // 0 = default 1 hour
// pprof profiling (§P4C) — enabled by SOC_PPROF=true
if env("SOC_PPROF", "") == "true" {
srv.EnablePprof()
}
logger.Info("server ready", "endpoints", 49, "dashboard_pages", 20)
if err := srv.Start(ctx); err != nil {
logger.Error("server failed", "error", err)
os.Exit(1)
}
logger.Info("server stopped")
}
func env(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
// configureMemorySafety applies SEC-003 runtime hardening:
// - GOMEMLIMIT: soft memory limit (default 450MiB) to avoid OOM kills
// - SOC_GOMAXPROCS: restrict CPU parallelism
// - Logs runtime memory stats at startup for diagnostics.
func configureMemorySafety(logger *slog.Logger) {
// GOMEMLIMIT: set soft memory limit via env var.
// Format: integer bytes, or use Go's debug.SetMemoryLimit default parsing.
if limitStr := os.Getenv("GOMEMLIMIT"); limitStr == "" {
// Default: 450 MiB (90% of typical 512Mi container limit).
const defaultLimit = 450 * 1024 * 1024
debug.SetMemoryLimit(defaultLimit)
logger.Info("SEC-003: GOMEMLIMIT set", "limit_mib", 450, "source", "default")
} else {
// When GOMEMLIMIT env var is set, Go runtime handles it automatically.
logger.Info("SEC-003: GOMEMLIMIT from env", "value", limitStr)
}
// SOC_GOMAXPROCS: optional CPU limit (useful in containers).
if maxProcs := os.Getenv("SOC_GOMAXPROCS"); maxProcs != "" {
if n, err := strconv.Atoi(maxProcs); err == nil && n > 0 {
prev := runtime.GOMAXPROCS(n)
logger.Info("SEC-003: GOMAXPROCS set", "new", n, "previous", prev)
}
}
// Log runtime info for diagnostics.
var m runtime.MemStats
runtime.ReadMemStats(&m)
logger.Info("SEC-003: runtime memory stats",
"go_version", runtime.Version(),
"num_cpu", runtime.NumCPU(),
"gomaxprocs", runtime.GOMAXPROCS(0),
"heap_alloc_mib", m.HeapAlloc/1024/1024,
"sys_mib", m.Sys/1024/1024,
)
}