- README: version v0.2.6.8, corrected stale gate claim, updated changelog - .env.example: merged two competing files into one, deleted bootstrap duplicate - ErrorBoundary: new component wrapping app, prevents white-screen crashes - Layout sidebar: version display from /api/health, Docs link to GitHub - client-logger: debug/trace logger gated behind localStorage.redflag_debug=1, routes through existing /logs/client-error server endpoint (ETHOS #1) - All web console.log calls rerouted through client-logger instead of deleted - Server health endpoint returns version field - Server accepts client_debug/client_trace in error_type validation - Dockerfiles: pinned alpine:latest->3.21, nginx:alpine->1.27-alpine, added HEALTHCHECK directives - docker-compose: healthcheck blocks for server and web services - .dockerignore: created to slim Docker build context
562 lines
19 KiB
Go
562 lines
19 KiB
Go
package services
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"net/http"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/Fimeg/RedFlag/server/internal/circuitbreaker"
|
||
"github.com/Fimeg/RedFlag/server/internal/httpx"
|
||
"github.com/gofrs/uuid/v5"
|
||
)
|
||
|
||
// OSVQueryRequest is sent to the OSV.dev API.
|
||
type OSVQueryRequest struct {
|
||
Package OSVPackage `json:"package"`
|
||
Version string `json:"version"`
|
||
}
|
||
|
||
// OSVPackage identifies a package in a specific ecosystem.
|
||
type OSVPackage struct {
|
||
Name string `json:"name"`
|
||
Ecosystem string `json:"ecosystem"`
|
||
}
|
||
|
||
// OSVQueryResponse is the response from the OSV.dev API.
|
||
type OSVQueryResponse struct {
|
||
Vulns []OSVVuln `json:"vulns"`
|
||
}
|
||
|
||
// OSVVuln represents a single vulnerability from OSV.dev. We decode the subset
|
||
// of the OSV schema we surface in the UI: identity, qualitative severity
|
||
// (database_specific.severity, used by GHSA for npm/PyPI), CVSS vectors, the
|
||
// first fixed version (from affected ranges), and the publish date.
|
||
type OSVVuln struct {
|
||
ID string `json:"id"`
|
||
Summary string `json:"summary"`
|
||
Aliases []string `json:"aliases"`
|
||
Published string `json:"published"`
|
||
Severity []OSVSeverity `json:"severity"`
|
||
Affected []OSVAffected `json:"affected"`
|
||
DatabaseSpecific map[string]interface{} `json:"database_specific"`
|
||
}
|
||
|
||
// OSVSeverity is one severity record (typically a CVSS vector string).
|
||
type OSVSeverity struct {
|
||
Type string `json:"type"`
|
||
Score string `json:"score"`
|
||
}
|
||
|
||
// OSVAffected carries the affected version ranges for a package.
|
||
type OSVAffected struct {
|
||
Ranges []OSVRange `json:"ranges"`
|
||
}
|
||
|
||
// OSVRange is an introduced/fixed event sequence over a version space.
|
||
type OSVRange struct {
|
||
Type string `json:"type"`
|
||
Events []OSVEvent `json:"events"`
|
||
}
|
||
|
||
// OSVEvent is a single boundary in a range (introduced or fixed).
|
||
type OSVEvent struct {
|
||
Introduced string `json:"introduced"`
|
||
Fixed string `json:"fixed"`
|
||
}
|
||
|
||
// SupplyChainCheckResult is returned by the vulnerability check.
|
||
type SupplyChainCheckResult struct {
|
||
Vulnerabilities []VulnerabilityInfo `json:"vulnerabilities"`
|
||
CheckedAt time.Time `json:"checked_at"`
|
||
}
|
||
|
||
// VulnerabilityInfo is a display-oriented vulnerability record. Stored as JSON
|
||
// in current_package_state.metadata.supply_chain_vulns and rendered by the UI.
|
||
// Severity is qualitative (CRITICAL/HIGH/MODERATE/LOW) when OSV provides it;
|
||
// CVSSVector is the raw v3/v4 vector for operators who want the dimensions.
|
||
type VulnerabilityInfo struct {
|
||
ID string `json:"id"`
|
||
Summary string `json:"summary"`
|
||
Aliases []string `json:"aliases"`
|
||
Severity string `json:"severity,omitempty"`
|
||
CVSSVector string `json:"cvss_vector,omitempty"`
|
||
FixedVersion string `json:"fixed_version,omitempty"`
|
||
Published string `json:"published,omitempty"`
|
||
}
|
||
|
||
// toVulnerabilityInfo maps a raw OSV record into the display struct, pulling
|
||
// qualitative severity, the first CVSS vector, the first fixed version, and the
|
||
// publish date out of the OSV schema's various nesting points.
|
||
func toVulnerabilityInfo(v OSVVuln) VulnerabilityInfo {
|
||
info := VulnerabilityInfo{
|
||
ID: v.ID,
|
||
Summary: v.Summary,
|
||
Aliases: v.Aliases,
|
||
Published: v.Published,
|
||
}
|
||
|
||
// Qualitative severity: GHSA puts it in database_specific.severity.
|
||
if v.DatabaseSpecific != nil {
|
||
if s, ok := v.DatabaseSpecific["severity"].(string); ok && s != "" {
|
||
info.Severity = strings.ToUpper(s)
|
||
}
|
||
}
|
||
|
||
// First CVSS vector (prefer v4, else v3, else whatever is present).
|
||
for _, s := range v.Severity {
|
||
if strings.HasPrefix(s.Score, "CVSS:") {
|
||
info.CVSSVector = s.Score
|
||
if strings.HasPrefix(s.Score, "CVSS:4") {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// First fixed version across affected ranges.
|
||
for _, a := range v.Affected {
|
||
for _, r := range a.Ranges {
|
||
for _, e := range r.Events {
|
||
if e.Fixed != "" {
|
||
info.FixedVersion = e.Fixed
|
||
break
|
||
}
|
||
}
|
||
if info.FixedVersion != "" {
|
||
break
|
||
}
|
||
}
|
||
if info.FixedVersion != "" {
|
||
break
|
||
}
|
||
}
|
||
|
||
return info
|
||
}
|
||
|
||
var osvHTTPClient = httpx.NewClient(30 * time.Second)
|
||
|
||
// osvBreaker wraps OSV.dev calls (SCALE-001 S8). Both the batch and single-query
|
||
// paths hit api.osv.dev, so they share one breaker: when OSV is down or slow,
|
||
// the breaker opens and subsequent checks fail fast instead of each timing out.
|
||
// Callers fail OPEN on an open breaker (record the check as unrun) — sovereignty:
|
||
// an unreachable advisory feed never blocks a patch, same as today's transport
|
||
// errors.
|
||
var osvBreaker = circuitbreaker.New("osv", circuitbreaker.Config{
|
||
FailureThreshold: 5,
|
||
FailureWindow: 60 * time.Second,
|
||
OpenDuration: 30 * time.Second,
|
||
HalfOpenAttempts: 2,
|
||
})
|
||
|
||
// OSVBreakerStats exposes the OSV breaker state for /health/tasks (OBS-001).
|
||
func OSVBreakerStats() circuitbreaker.Stats { return osvBreaker.GetStats() }
|
||
|
||
// OSVRecheckInterval is how long a stored supply-chain result is treated as
|
||
// fresh. The scan path skips packages checked more recently than this so a
|
||
// frequent scan cadence does not re-hit OSV.dev for every package every cycle;
|
||
// the periodic recheck still picks up newly published advisories.
|
||
const OSVRecheckInterval = 6 * time.Hour
|
||
|
||
// osvBatchSize is the max queries per /v1/querybatch POST. OSV.dev accepts up
|
||
// to 1000; 100 keeps each request under 2s and the response payload manageable.
|
||
const osvBatchSize = 100
|
||
|
||
// osvBatchesInFlight caps the number of concurrent batch requests to OSV.dev
|
||
// across the entire process. At 10,000 agents × ~300 packages each, we could
|
||
// have millions of checks queued — this semaphore ensures we don't hammer the
|
||
// API regardless of how many callers invoke RunOSVChecks simultaneously.
|
||
const osvBatchesInFlight = 4
|
||
|
||
// osvBatchSem is the process-wide semaphore for concurrent OSV.dev batch
|
||
// requests. Shared across all RunOSVChecks calls so that N agents reporting
|
||
// simultaneously total at most osvBatchesInFlight requests in flight.
|
||
var osvBatchSem = make(chan struct{}, osvBatchesInFlight)
|
||
|
||
// OSVCheckRequest is one package to check against OSV.dev.
|
||
// Namespace routes results to different metadata keys:
|
||
// - "" or "remediation" → supply_chain_checked_at / supply_chain_vulns (available-version check)
|
||
// - "installed" → installed_checked_at / installed_vulns (installed-version threat check)
|
||
type OSVCheckRequest struct {
|
||
AgentID uuid.UUID
|
||
PkgType string
|
||
PkgName string
|
||
Version string
|
||
Namespace string // "" = remediation (default)
|
||
}
|
||
|
||
// OSVStoreFunc persists a supply-chain result (or a recorded failure) for one
|
||
// package. Implemented by the caller against current_package_state so this
|
||
// package keeps no database dependency.
|
||
type OSVStoreFunc func(agentID uuid.UUID, pkgType, pkgName string, meta map[string]interface{}) error
|
||
|
||
// osvBatchQuery is one entry in the /v1/querybatch request.
|
||
type osvBatchQuery struct {
|
||
Package OSVPackage `json:"package"`
|
||
Version string `json:"version"`
|
||
}
|
||
|
||
// osvBatchRequest is the POST body for /v1/querybatch.
|
||
type osvBatchRequest struct {
|
||
Queries []osvBatchQuery `json:"queries"`
|
||
}
|
||
|
||
// osvBatchResult is one result from /v1/querybatch.
|
||
type osvBatchResult struct {
|
||
Vulns []OSVVuln `json:"vulns"`
|
||
}
|
||
|
||
// osvBatchResponse is the response from /v1/querybatch.
|
||
type osvBatchResponse struct {
|
||
Results []osvBatchResult `json:"results"`
|
||
}
|
||
|
||
// RunOSVChecks queries OSV.dev for each request using the batch endpoint with
|
||
// bounded, process-wide concurrency, and persists every outcome through store.
|
||
// A clean result records supply_chain_checked_at; a hit also records
|
||
// supply_chain_vulns; a query failure records supply_chain_check_error WITHOUT
|
||
// a checked_at timestamp, so the package stays a candidate for the next run
|
||
// rather than being silently dropped (ETHOS: errors are history, assume failure).
|
||
// Blocks until done.
|
||
func RunOSVChecks(reqs []OSVCheckRequest, store OSVStoreFunc) {
|
||
if len(reqs) == 0 {
|
||
return
|
||
}
|
||
|
||
var wg sync.WaitGroup
|
||
for i := 0; i < len(reqs); i += osvBatchSize {
|
||
end := i + osvBatchSize
|
||
if end > len(reqs) {
|
||
end = len(reqs)
|
||
}
|
||
batch := reqs[i:end]
|
||
|
||
osvBatchSem <- struct{}{}
|
||
wg.Add(1)
|
||
go func(b []OSVCheckRequest) {
|
||
defer wg.Done()
|
||
defer func() { <-osvBatchSem }()
|
||
osvBatchRun(b, store)
|
||
}(batch)
|
||
}
|
||
wg.Wait()
|
||
}
|
||
|
||
// osvBatchRun sends one batch to /v1/querybatch and persists all results.
|
||
func osvBatchRun(reqs []OSVCheckRequest, store OSVStoreFunc) {
|
||
queries := make([]osvBatchQuery, len(reqs))
|
||
for i, r := range reqs {
|
||
queries[i] = osvBatchQuery{
|
||
Package: OSVPackage{
|
||
Name: r.PkgName,
|
||
Ecosystem: EcosystemFromPackageType(r.PkgType),
|
||
},
|
||
Version: r.Version,
|
||
}
|
||
}
|
||
|
||
body, err := json.Marshal(osvBatchRequest{Queries: queries})
|
||
if err != nil {
|
||
log.Printf("[WARNING] [supply_chain] batch_marshal_failed count=%d error=%v", len(reqs), err)
|
||
recordBatchFailure(reqs, store)
|
||
return
|
||
}
|
||
|
||
// Breaker-wrapped (SCALE-001 S8): a down/slow OSV trips the breaker so the
|
||
// rest of this batch — and the single-query path — fail fast rather than each
|
||
// blocking on a 30s timeout. An open breaker takes the same fail-open path as
|
||
// a transport error below.
|
||
var batchResp osvBatchResponse
|
||
callErr := osvBreaker.Call(func() error {
|
||
resp, err := osvHTTPClient.Post("https://api.osv.dev/v1/querybatch", "application/json", bytes.NewReader(body))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != http.StatusOK {
|
||
return fmt.Errorf("osv batch status %d", resp.StatusCode)
|
||
}
|
||
if err := json.NewDecoder(resp.Body).Decode(&batchResp); err != nil {
|
||
return fmt.Errorf("decode: %w", err)
|
||
}
|
||
return nil
|
||
})
|
||
if callErr != nil {
|
||
log.Printf("[WARNING] [supply_chain] batch_query_failed count=%d error=%v", len(reqs), callErr)
|
||
recordBatchFailure(reqs, store)
|
||
return
|
||
}
|
||
|
||
if len(batchResp.Results) != len(reqs) {
|
||
log.Printf("[WARNING] [supply_chain] batch_count_mismatch sent=%d got=%d", len(reqs), len(batchResp.Results))
|
||
recordBatchFailure(reqs, store)
|
||
return
|
||
}
|
||
|
||
now := time.Now().UTC()
|
||
for i, r := range reqs {
|
||
result := batchResp.Results[i]
|
||
checkedKey, vulnsKey, errorKey := osvMetaKeys(r.Namespace)
|
||
meta := map[string]interface{}{
|
||
checkedKey: now.Format(time.RFC3339),
|
||
errorKey: nil,
|
||
}
|
||
|
||
if len(result.Vulns) > 0 {
|
||
vulnJSON, err := json.Marshal(result.Vulns)
|
||
if err != nil {
|
||
log.Printf("[WARNING] [supply_chain] vuln_marshal_failed pkg=%s error=%v", r.PkgName, err)
|
||
continue
|
||
}
|
||
meta[vulnsKey] = string(vulnJSON)
|
||
log.Printf("[SECURITY] [supply_chain] vulns_found namespace=%s pkg=%s type=%s ecosystem=%s count=%d",
|
||
r.Namespace, r.PkgName, r.PkgType, EcosystemFromPackageType(r.PkgType), len(result.Vulns))
|
||
} else {
|
||
log.Printf("[INFO] [supply_chain] clean namespace=%s pkg=%s type=%s ecosystem=%s version=%s",
|
||
r.Namespace, r.PkgName, r.PkgType, EcosystemFromPackageType(r.PkgType), r.Version)
|
||
}
|
||
|
||
if err := store(r.AgentID, r.PkgType, r.PkgName, meta); err != nil {
|
||
log.Printf("[WARNING] [supply_chain] metadata_store_failed pkg=%s error=%v", r.PkgName, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// osvMetaKeys returns the metadata key names for a given check namespace.
|
||
func osvMetaKeys(namespace string) (checkedAt, vulns, checkError string) {
|
||
if namespace == "installed" {
|
||
return "installed_checked_at", "installed_vulns", "installed_check_error"
|
||
}
|
||
return "supply_chain_checked_at", "supply_chain_vulns", "supply_chain_check_error"
|
||
}
|
||
|
||
// recordBatchFailure persists a failure record for every request in a batch
|
||
// that could not be checked (HTTP error, decode failure, count mismatch).
|
||
// No checked_at is set so these packages are retried next cycle.
|
||
func recordBatchFailure(reqs []OSVCheckRequest, store OSVStoreFunc) {
|
||
for _, r := range reqs {
|
||
_, _, errorKey := osvMetaKeys(r.Namespace)
|
||
meta := map[string]interface{}{
|
||
errorKey: "osv_batch_failed",
|
||
"supply_chain_error_at": time.Now().UTC().Format(time.RFC3339),
|
||
}
|
||
if err := store(r.AgentID, r.PkgType, r.PkgName, meta); err != nil {
|
||
log.Printf("[WARNING] [supply_chain] failure_record_failed pkg=%s error=%v", r.PkgName, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ClosurePkg is one resolved artifact (name + version) in a dependency closure
|
||
// to be checked against OSV.dev.
|
||
type ClosurePkg struct {
|
||
Name string
|
||
Version string
|
||
}
|
||
|
||
// ClosureVuln records a closure artifact that has known vulnerabilities.
|
||
type ClosureVuln struct {
|
||
Name string `json:"name"`
|
||
Version string `json:"version"`
|
||
Vulns []OSVVuln `json:"vulns"`
|
||
}
|
||
|
||
// CheckClosureOSV queries OSV.dev for every artifact in a resolved dependency
|
||
// closure and returns the subset with known vulnerabilities. This is the
|
||
// dependency-level supply-chain gate: the closure is the exact set of artifacts
|
||
// the capability token authorizes the network-less executor to install, so each
|
||
// transitive artifact is queried — not just the top-level package that was
|
||
// checked at discovery.
|
||
//
|
||
// Unlike CheckOSVVulnerabilities (fail-open, for advisory display), this is
|
||
// FAIL-CLOSED: if any batch cannot be queried (HTTP error, bad status, decode
|
||
// failure, count mismatch), it returns ok=false. The caller must treat a
|
||
// not-ok result as "closure not cleared" and never as clean — auto-confirm must
|
||
// not mint a token over a closure OSV could not vet. All entries share the
|
||
// update's ecosystem.
|
||
func CheckClosureOSV(pkgType string, entries []ClosurePkg) (found []ClosureVuln, ok bool) {
|
||
if len(entries) == 0 {
|
||
return nil, true // empty closure is trivially clean
|
||
}
|
||
ecosystem := EcosystemFromPackageType(pkgType)
|
||
|
||
for i := 0; i < len(entries); i += osvBatchSize {
|
||
end := i + osvBatchSize
|
||
if end > len(entries) {
|
||
end = len(entries)
|
||
}
|
||
batch := entries[i:end]
|
||
hits, batchOK := osvClosureBatch(ecosystem, batch)
|
||
if !batchOK {
|
||
return found, false // fail closed: a batch we couldn't check is not "clean"
|
||
}
|
||
found = append(found, hits...)
|
||
}
|
||
return found, true
|
||
}
|
||
|
||
// osvClosureBatch sends one closure batch to /v1/querybatch under the
|
||
// process-wide semaphore and returns the entries with vulns. ok=false on any
|
||
// query/parse failure.
|
||
func osvClosureBatch(ecosystem string, batch []ClosurePkg) (found []ClosureVuln, ok bool) {
|
||
queries := make([]osvBatchQuery, len(batch))
|
||
for i, e := range batch {
|
||
queries[i] = osvBatchQuery{Package: OSVPackage{Name: e.Name, Ecosystem: ecosystem}, Version: e.Version}
|
||
}
|
||
body, err := json.Marshal(osvBatchRequest{Queries: queries})
|
||
if err != nil {
|
||
log.Printf("[WARNING] [supply_chain] closure_batch_marshal_failed count=%d error=%v", len(batch), err)
|
||
return nil, false
|
||
}
|
||
|
||
osvBatchSem <- struct{}{}
|
||
defer func() { <-osvBatchSem }()
|
||
|
||
resp, err := osvHTTPClient.Post("https://api.osv.dev/v1/querybatch", "application/json", bytes.NewReader(body))
|
||
if err != nil {
|
||
log.Printf("[WARNING] [supply_chain] closure_batch_query_failed count=%d error=%v", len(batch), err)
|
||
return nil, false
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
log.Printf("[WARNING] [supply_chain] closure_batch_query_status=%d count=%d", resp.StatusCode, len(batch))
|
||
return nil, false
|
||
}
|
||
|
||
var batchResp osvBatchResponse
|
||
if err := json.NewDecoder(resp.Body).Decode(&batchResp); err != nil {
|
||
log.Printf("[WARNING] [supply_chain] closure_batch_decode_failed count=%d error=%v", len(batch), err)
|
||
return nil, false
|
||
}
|
||
if len(batchResp.Results) != len(batch) {
|
||
log.Printf("[WARNING] [supply_chain] closure_batch_count_mismatch sent=%d got=%d", len(batch), len(batchResp.Results))
|
||
return nil, false
|
||
}
|
||
|
||
for i, e := range batch {
|
||
if len(batchResp.Results[i].Vulns) > 0 {
|
||
found = append(found, ClosureVuln{Name: e.Name, Version: e.Version, Vulns: batchResp.Results[i].Vulns})
|
||
log.Printf("[SECURITY] [supply_chain] closure_vuln pkg=%s version=%s ecosystem=%s count=%d",
|
||
e.Name, e.Version, ecosystem, len(batchResp.Results[i].Vulns))
|
||
}
|
||
}
|
||
return found, true
|
||
}
|
||
|
||
// CheckOSVVulnerabilities queries the OSV.dev API for known vulnerabilities
|
||
// matching the given package name, ecosystem, and version. Returns nil slice
|
||
// with no error on clean results or API failure (fail-open).
|
||
func CheckOSVVulnerabilities(pkgName, ecosystem, version string) *SupplyChainCheckResult {
|
||
reqBody := OSVQueryRequest{
|
||
Package: OSVPackage{
|
||
Name: pkgName,
|
||
Ecosystem: ecosystem,
|
||
},
|
||
Version: version,
|
||
}
|
||
|
||
body, err := json.Marshal(reqBody)
|
||
if err != nil {
|
||
log.Printf("[WARNING] [supply_chain] marshal_failed pkg=%s error=%v", pkgName, err)
|
||
return nil
|
||
}
|
||
|
||
// Breaker-wrapped (SCALE-001 S8); shares osvBreaker with the batch path. An
|
||
// open breaker returns nil (fail-open) — same as a transport error: the check
|
||
// is recorded as unrun, never blocking a patch (sovereignty).
|
||
var result OSVQueryResponse
|
||
callErr := osvBreaker.Call(func() error {
|
||
resp, err := osvHTTPClient.Post("https://api.osv.dev/v1/query", "application/json", bytes.NewReader(body))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != http.StatusOK {
|
||
return fmt.Errorf("osv status %d", resp.StatusCode)
|
||
}
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
return fmt.Errorf("decode: %w", err)
|
||
}
|
||
return nil
|
||
})
|
||
if callErr != nil {
|
||
log.Printf("[WARNING] [supply_chain] query_failed pkg=%s error=%v", pkgName, callErr)
|
||
return nil
|
||
}
|
||
|
||
if len(result.Vulns) == 0 {
|
||
return &SupplyChainCheckResult{
|
||
CheckedAt: time.Now().UTC(),
|
||
}
|
||
}
|
||
|
||
vulns := make([]VulnerabilityInfo, len(result.Vulns))
|
||
for i, v := range result.Vulns {
|
||
vulns[i] = toVulnerabilityInfo(v)
|
||
}
|
||
|
||
return &SupplyChainCheckResult{
|
||
Vulnerabilities: vulns,
|
||
CheckedAt: time.Now().UTC(),
|
||
}
|
||
}
|
||
|
||
// NeedsSupplyChainCheck returns true if the given package ecosystem can be
|
||
// checked against OSV.dev. Broader than before — runs for all ecosystems that
|
||
// have an OSV.dev mapping, even when coverage is sparse. A nil result is honest
|
||
// visibility (the check ran, nothing found).
|
||
func NeedsSupplyChainCheck(pkgType string) bool {
|
||
switch pkgType {
|
||
case "npm", "pypi", "apt", "dnf":
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// CanServerFetchArtifact returns true if the server can download package
|
||
// artifacts from the public registry for the given ecosystem. Agent-sourced
|
||
// ecosystems (dnf, apt) return false — their artifacts come from the agent's
|
||
// own repos, which the server may not be able to reach (custom mirrors,
|
||
// air-gapped networks). The mirror tier may add server-side fetching for
|
||
// these ecosystems later.
|
||
func CanServerFetchArtifact(pkgType string) bool {
|
||
switch pkgType {
|
||
case "npm", "pypi":
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// NeedsCapabilityGate returns true if the ecosystem routes mutation through
|
||
// the capability-token path (consumer.go → redflag-helper). Server-fetched
|
||
// ecosystems (npm/pypi) and agent-sourced ecosystems (dnf/apt) both use it
|
||
// when the minter is enabled.
|
||
func NeedsCapabilityGate(pkgType string) bool {
|
||
switch pkgType {
|
||
case "dnf", "apt", "npm", "pypi":
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// EcosystemFromPackageType maps RedFlag package types to OSV.dev ecosystems.
|
||
// Best-effort: dnf maps to AlmaLinux (closest supported RHEL-family ecosystem),
|
||
// apt maps to Debian. Unmapped types return the raw package type — OSV.dev will
|
||
// return empty results for unrecognized ecosystems rather than error.
|
||
func EcosystemFromPackageType(pkgType string) string {
|
||
switch pkgType {
|
||
case "npm":
|
||
return "npm"
|
||
case "pypi":
|
||
return "PyPI"
|
||
case "apt":
|
||
return "Debian"
|
||
case "dnf":
|
||
return "AlmaLinux"
|
||
}
|
||
return pkgType
|
||
}
|