Files
siriusscan--sirius/sirius-api/main.go
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:32:25 +08:00

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
}
}