mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-24 20:06:21 +02:00
280 lines
9.1 KiB
Go
280 lines
9.1 KiB
Go
// Copyright 2026 Syntrex Lab. All rights reserved.
|
|
// Use of this source code is governed by an Apache-2.0 license
|
|
// that can be found in the LICENSE file.
|
|
|
|
// Package main provides the standalone SOC API server entry point.
|
|
//
|
|
// @title SYNTREX Sentinel SOC API
|
|
// @version 1.0
|
|
// @description Enterprise API for AI Firewall and Security Operations Center. Auto-generated via Doc-as-Code.
|
|
// @host localhost:9100
|
|
// @BasePath /
|
|
//
|
|
// 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"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/syntrex-lab/gomcp/internal/application/soc"
|
|
"github.com/syntrex-lab/gomcp/internal/domain/engines"
|
|
socdomain "github.com/syntrex-lab/gomcp/internal/domain/soc"
|
|
"github.com/syntrex-lab/gomcp/internal/infrastructure/audit"
|
|
"github.com/syntrex-lab/gomcp/internal/infrastructure/auth"
|
|
"github.com/syntrex-lab/gomcp/internal/infrastructure/email"
|
|
"github.com/syntrex-lab/gomcp/internal/infrastructure/logging"
|
|
"github.com/syntrex-lab/gomcp/internal/infrastructure/postgres"
|
|
"github.com/syntrex-lab/gomcp/internal/infrastructure/sqlite"
|
|
"github.com/syntrex-lab/gomcp/internal/infrastructure/tracing"
|
|
sochttp "github.com/syntrex-lab/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)
|
|
|
|
// Configure CORS
|
|
if corsEnv := env("SOC_CORS_ORIGIN", ""); corsEnv != "" {
|
|
parts := strings.Split(corsEnv, ",")
|
|
for i := range parts {
|
|
parts[i] = strings.TrimSpace(parts[i])
|
|
}
|
|
srv.SetCORSOrigins(parts)
|
|
slog.Info("CORS strict origins configured", "origins", len(parts))
|
|
}
|
|
|
|
// 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
|
|
|
|
// Start WebSocket Event Bridge for live real-time streams
|
|
srv.StartEventBridge(ctx)
|
|
|
|
// 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,
|
|
)
|
|
}
|