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
80 lines
2.2 KiB
Go
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,
|
|
)
|
|
}
|