161ef94b4f
Check engine pin consistency / Dockerfile / CI pin consistency (push) Successful in 8s
Sirius CI/CD Pipeline / Detect Changes (push) Successful in 23s
Validate Docker Configuration / Validate Docker Compose Configuration (push) Successful in 47s
Sirius CI/CD Pipeline / Build API (${{ matrix.platform }}) (push) Has been cancelled
Sirius CI/CD Pipeline / Build UI (${{ matrix.platform }}) (push) Has been cancelled
Sirius CI/CD Pipeline / Merge Engine Manifest (push) Has been cancelled
Sirius CI/CD Pipeline / Merge API Manifest (push) Has been cancelled
Sirius CI/CD Pipeline / Merge UI Manifest (push) Has been cancelled
Sirius CI/CD Pipeline / Build Engine (${{ matrix.platform }}) (push) Has been cancelled
Sirius CI/CD Pipeline / Build Infra (${{ matrix.service }}, ${{ matrix.platform }}) (push) Has been cancelled
Sirius CI/CD Pipeline / Merge Infra Manifest (sirius-postgres) (push) Has been cancelled
Sirius CI/CD Pipeline / Merge Infra Manifest (sirius-rabbitmq) (push) Has been cancelled
Sirius CI/CD Pipeline / Merge Infra Manifest (sirius-valkey) (push) Has been cancelled
Sirius CI/CD Pipeline / Integration Test (push) Has been cancelled
Sirius CI/CD Pipeline / Public Stack Contract (push) Has been cancelled
Sirius CI/CD Pipeline / Dispatch Demo Deployment (sirius-demo branch) (push) Has been cancelled
Sirius CI/CD Pipeline / Dispatch Demo Canary (main branch) (push) Has been cancelled
Sirius CI/CD Pipeline / Guard Registry Namespace (push) Has been cancelled
267 lines
7.7 KiB
Go
Executable File
267 lines
7.7 KiB
Go
Executable File
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/SiriusScan/go-api/sirius/logging"
|
|
"github.com/SiriusScan/go-api/sirius/slogger"
|
|
"github.com/SiriusScan/go-api/sirius/store"
|
|
"github.com/SiriusScan/sirius-api/internal/infraauth"
|
|
"github.com/SiriusScan/sirius-api/middleware"
|
|
"github.com/SiriusScan/sirius-api/routes"
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
"github.com/gofiber/fiber/v2/middleware/requestid"
|
|
)
|
|
|
|
// waitForDatabase waits for PostgreSQL to be available before running migrations
|
|
func waitForDatabase() error {
|
|
dbHost := os.Getenv("POSTGRES_HOST")
|
|
dbPort := os.Getenv("POSTGRES_PORT")
|
|
if dbHost == "" {
|
|
dbHost = "sirius-postgres"
|
|
}
|
|
if dbPort == "" {
|
|
dbPort = "5432"
|
|
}
|
|
|
|
address := net.JoinHostPort(dbHost, dbPort)
|
|
slog.Info("Waiting for database", "address", address)
|
|
|
|
for attempts := 0; attempts < 30; attempts++ {
|
|
conn, err := net.DialTimeout("tcp", address, 3*time.Second)
|
|
if err == nil {
|
|
conn.Close()
|
|
slog.Info("Database is available", "address", address)
|
|
return nil
|
|
}
|
|
slog.Debug("Database not ready, retrying", "attempt", attempts+1, "max", 30)
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
|
|
return fmt.Errorf("database not available after 30 attempts")
|
|
}
|
|
|
|
// runMigrations executes database migrations before starting the API
|
|
func runMigrations() error {
|
|
slog.Info("Running database migrations")
|
|
|
|
// Wait for database to be available
|
|
if err := waitForDatabase(); err != nil {
|
|
return fmt.Errorf("database connectivity check failed: %w", err)
|
|
}
|
|
|
|
// Check if we're in development mode with volume mount
|
|
var goApiPath string
|
|
if _, err := os.Stat("/go-api"); err == nil {
|
|
goApiPath = "/go-api"
|
|
} else if _, err := os.Stat("../go-api"); err == nil {
|
|
goApiPath = "../go-api"
|
|
} else {
|
|
slog.Warn("go-api not found, skipping migrations")
|
|
return nil
|
|
}
|
|
|
|
migrationsPath := filepath.Join(goApiPath, "migrations")
|
|
|
|
// Run migration 002_source_attribution (creates scan_history_entries table)
|
|
migration002Path := filepath.Join(migrationsPath, "002_source_attribution", "main.go")
|
|
if _, err := os.Stat(migration002Path); err == nil {
|
|
slog.Info("Running migration", "name", "002_source_attribution")
|
|
cmd := exec.Command("go", "run", migration002Path)
|
|
cmd.Dir = goApiPath
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
slog.Warn("Migration failed (may already be applied)", "name", "002_source_attribution", "error", err)
|
|
} else {
|
|
slog.Info("Migration completed", "name", "002_source_attribution")
|
|
}
|
|
}
|
|
|
|
// Run migration 004_add_sbom_schema if it exists
|
|
migration004Path := filepath.Join(migrationsPath, "004_add_sbom_schema", "main.go")
|
|
if _, err := os.Stat(migration004Path); err == nil {
|
|
slog.Info("Running migration", "name", "004_add_sbom_schema")
|
|
cmd := exec.Command("go", "run", migration004Path)
|
|
cmd.Dir = goApiPath
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
slog.Warn("Migration failed (may already be applied)", "name", "004_add_sbom_schema", "error", err)
|
|
} else {
|
|
slog.Info("Migration completed", "name", "004_add_sbom_schema")
|
|
}
|
|
}
|
|
|
|
slog.Info("Database migrations completed")
|
|
return nil
|
|
}
|
|
|
|
|
|
func main() {
|
|
// Initialize structured logging (reads LOG_LEVEL env var)
|
|
slogger.Init()
|
|
|
|
// Root service key is validated statelessly in middleware (file or env).
|
|
// Valkey remains authoritative only for user-generated API keys.
|
|
serviceAPIKey, err := infraauth.LoadSiriusAPIKey()
|
|
if err != nil || strings.TrimSpace(serviceAPIKey) == "" {
|
|
slog.Error("internal API key is required for sirius-api startup (SIRIUS_API_KEY_FILE or SIRIUS_API_KEY)", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Initialize the logging SDK
|
|
logging.Init()
|
|
defer logging.Close()
|
|
|
|
// Run database migrations before starting the API
|
|
if err := runMigrations(); err != nil {
|
|
slog.Error("Failed to run migrations", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Initialize Valkey store for dynamic API key management.
|
|
kvStore, err := store.NewValkeyStore()
|
|
if err != nil {
|
|
slog.Error("Failed to connect to Valkey", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
defer kvStore.Close()
|
|
|
|
app := fiber.New()
|
|
|
|
// Add CORS middleware
|
|
allowedOrigins := os.Getenv("CORS_ALLOWED_ORIGINS")
|
|
if allowedOrigins == "" {
|
|
allowedOrigins = "*" // Default to allow all origins
|
|
}
|
|
app.Use(cors.New(cors.Config{
|
|
AllowOrigins: allowedOrigins,
|
|
AllowMethods: "GET,POST,HEAD,PUT,DELETE,PATCH",
|
|
}))
|
|
|
|
// Add request ID middleware
|
|
app.Use(requestid.New())
|
|
|
|
// Add level-aware request logging middleware
|
|
app.Use(requestLoggerMiddleware())
|
|
|
|
// Add API key authentication middleware
|
|
app.Use(middleware.APIKeyMiddleware(kvStore, serviceAPIKey))
|
|
|
|
// Add SDK-based logging middlewares
|
|
app.Use(middleware.SDKLoggingMiddleware())
|
|
app.Use(middleware.SDKErrorLoggingMiddleware())
|
|
app.Use(middleware.SDKPerformanceMetricsMiddleware())
|
|
|
|
vulnerabilityRouteSetter := &routes.VulnerabilityRouteSetter{}
|
|
templateRouteSetter := &routes.TemplateRouteSetter{}
|
|
scriptRouteSetter := &routes.ScriptRouteSetter{}
|
|
agentTemplateRouteSetter := &routes.AgentTemplateRouteSetter{}
|
|
agentTemplateRepositoryRouteSetter := &routes.AgentTemplateRepositoryRouteSetter{}
|
|
eventRouteSetter := &routes.EventRouteSetter{}
|
|
snapshotRouteSetter := &routes.SnapshotRouteSetter{}
|
|
statisticsRouteSetter := &routes.StatisticsRoutes{}
|
|
scanRouteSetter := &routes.ScanRouteSetter{}
|
|
apiKeyRouteSetter := &routes.APIKeyRouteSetter{Store: kvStore}
|
|
routes.SetupRoutes(
|
|
app,
|
|
&routes.HostRouteSetter{},
|
|
&routes.AppRouteSetter{},
|
|
vulnerabilityRouteSetter,
|
|
templateRouteSetter,
|
|
scriptRouteSetter,
|
|
agentTemplateRepositoryRouteSetter, // Must be before agentTemplateRouteSetter to avoid :id matching
|
|
agentTemplateRouteSetter,
|
|
eventRouteSetter, // Event routes for scan events
|
|
snapshotRouteSetter, // Snapshot and vulnerability trend routes
|
|
statisticsRouteSetter, // Statistics routes
|
|
scanRouteSetter, // Scan control routes (cancel, status)
|
|
apiKeyRouteSetter, // API key management routes
|
|
)
|
|
|
|
slog.Info("Sirius API starting", "port", 9001, "log_level", os.Getenv("LOG_LEVEL"), "auth_required", true)
|
|
app.Listen(":9001")
|
|
}
|
|
|
|
// requestLoggerMiddleware returns a Fiber middleware that logs HTTP requests
|
|
// at a level appropriate to the configured LOG_LEVEL:
|
|
//
|
|
// - debug: logs every request
|
|
// - info: skips /health and routine polling endpoints (GET /host/, GET /host/statistics/*)
|
|
// - warn: only logs slow requests (>1s) or 4xx+ status codes
|
|
// - error: only logs 5xx status codes
|
|
func requestLoggerMiddleware() fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
start := time.Now()
|
|
|
|
err := c.Next()
|
|
|
|
latency := time.Since(start)
|
|
status := c.Response().StatusCode()
|
|
method := c.Method()
|
|
path := c.Path()
|
|
|
|
attrs := []any{
|
|
"method", method,
|
|
"path", path,
|
|
"status", status,
|
|
"latency", latency.String(),
|
|
}
|
|
|
|
// error level: only 5xx
|
|
if status >= 500 {
|
|
slog.Error("request", attrs...)
|
|
return err
|
|
}
|
|
|
|
// warn level: 4xx or slow requests (>1s)
|
|
if status >= 400 {
|
|
slog.Warn("request", attrs...)
|
|
return err
|
|
}
|
|
if latency > 1*time.Second {
|
|
slog.Warn("slow request", attrs...)
|
|
return err
|
|
}
|
|
|
|
// info level: skip health checks and high-frequency polling endpoints
|
|
if isNoisyEndpoint(method, path) {
|
|
slog.Debug("request", attrs...)
|
|
return err
|
|
}
|
|
|
|
// Everything else at info
|
|
slog.Info("request", attrs...)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// isNoisyEndpoint returns true for endpoints that fire on a recurring polling
|
|
// interval and would flood the logs at info level.
|
|
func isNoisyEndpoint(method, path string) bool {
|
|
if method != "GET" {
|
|
return false
|
|
}
|
|
switch {
|
|
case path == "/health":
|
|
return true
|
|
case path == "/host/" || path == "/host":
|
|
return true
|
|
case strings.HasPrefix(path, "/host/statistics/"):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|