Watch
1
0
Fork
You've already forked RedFlag
0
RedFlag/server/internal/services/soak_gate.go
Fimeg d1424e8377 feat: FEAT-002 metadata pipeline, GATE-005 soak gate, BRIDGE-001 auto-discovery
FEAT-002: Server-side metadata pipeline
- ReportUpdates merges PackageDescription/CVEList/KBID/SizeBytes into metadata JSONB
- UpdateCurrentStateInTx uses JSONB merge (||) instead of replace
- Fixes current_version bug (missing EXCLUDED.current_version)
- EnrichFromMetadata() on UpdateState populates display fields from metadata
- mergedVulnerabilities() deduplicates agent CVEs with OSV.dev results
- VulnerabilityEntry struct for unified vulnerability representation

GATE-005: Version soak-gate
- Migration 050: selected_version column, version_soak_overrides table
- soak_gate.go: SoakGateConfig/EvaluateSoakGate (14-day default, block enforcement)
- POST /updates/:id/install-version endpoint with soak evaluation
- EnqueueDryRun reads COALESCE(selected_version, available_version)
- Override journals to system_events (recordSoakOverride)

BRIDGE-001: Tracked software auto-discovery
- Migration 051: repology_slug/container_image_pattern/binary_probe on tracked_software,
  repology_aliases table, match_method/package_name on agent_tracked_software
- RepologyCache: fetches /api/v1/project/{slug}/packages, normalizes repos to ecosystems
- ReconciliationQueries: MatchByRepology/MatchByContainer/MatchByExactName
- Reconciler: cascade matching service with hourly loop, hooks into ReportUpdates
- UpsertReconciled: preserves manual operator bindings
- COMMON_SEEDS updated with repology_slug values
- Syncer updated with 24h alias refresh ticker
2026-06-05 17:43:11 -04:00

80 lines
2.2 KiB
Go

package services
import (
"fmt"
"os"
"strconv"
"strings"
"time"
)
// SoakGateDecision is the verdict the install-version handler uses to decide
// whether to block, warn, or pass a version selection.
type SoakGateDecision struct {
Eligible bool `json:"eligible"`
FirstScannedAt time.Time `json:"first_scanned_at"`
SoakDays float64 `json:"soak_days"`
RequiredDays float64 `json:"required_days"`
DaysRemaining float64 `json:"days_remaining"`
Enforcement string `json:"enforcement"` // "off" | "warn" | "block"
Unknown bool `json:"unknown"`
}
// SoakGateConfig reads the version soak gate configuration.
// Default: 14-day soak window, "block" enforcement.
func SoakGateConfig() (requiredDays float64, enforcement string) {
requiredDays = 14.0
enforcement = "block"
if v := os.Getenv("REDFLAG_SUPPLY_CHAIN_SOAK_WINDOW_DAYS"); v != "" {
if f, err := strconv.ParseFloat(v, 64); err == nil && f >= 0 {
requiredDays = f
}
}
if v := os.Getenv("REDFLAG_SUPPLY_CHAIN_SOAK_ENFORCEMENT"); v != "" {
switch strings.ToLower(v) {
case "off", "warn", "block":
enforcement = strings.ToLower(v)
}
}
return
}
// EvaluateSoakGate computes the soak gate decision for a single version.
func EvaluateSoakGate(firstScannedAt time.Time, requiredDays float64, enforcement string) SoakGateDecision {
dec := SoakGateDecision{
RequiredDays: requiredDays,
Enforcement: enforcement,
}
if firstScannedAt.IsZero() {
dec.Unknown = true
return dec
}
dec.FirstScannedAt = firstScannedAt
dec.SoakDays = time.Since(firstScannedAt).Hours() / 24.0
dec.DaysRemaining = requiredDays - dec.SoakDays
if dec.DaysRemaining < 0 {
dec.DaysRemaining = 0
}
if dec.SoakDays >= requiredDays {
dec.Eligible = true
return dec
}
if enforcement == "block" {
dec.Eligible = false
} else {
dec.Eligible = true
}
return dec
}
// SoakGateWarnMessage returns a human-readable message for non-eligible decisions.
func SoakGateWarnMessage(dec SoakGateDecision) string {
return fmt.Sprintf(
"version has been in the fleet for %.1f days (soak threshold %.1f days, %.1f days remaining)",
dec.SoakDays, dec.RequiredDays, dec.DaysRemaining,
)
}