StellaOps Scanner
Layer-by-layer container image analysis that produces deterministic SBOM fragments, diffs, and signed reports.
Scanner is the analysis core of the Stella Ops vulnerability scanner. It decomposes container images layer-by-layer, runs OS, language, and native-ecosystem analyzers, and emits deterministic SBOM fragments, three-way diffs, and DSSE-signed reports for downstream policy evaluation, attestation, and export.
This page is the module map and product overview. For the full route catalog see endpoint-registration-matrix.md; for analyzer behavior and operations see the Related resources section.
Responsibilities
- Expose APIs (WebService) for scan orchestration, diffing, and artifact retrieval.
- Run Worker analyzers for OS, language, and native ecosystems with restart-only plug-ins.
- Store SBOM fragments and artifacts in RustFS/object storage.
- Publish DSSE-ready metadata for Signer/Attestor and downstream policy evaluation.
Key components
StellaOps.Scanner.WebServiceminimal API host.StellaOps.Scanner.Workeranalyzer executor.- Analyzer libraries under
StellaOps.Scanner.Analyzers.*.
Integrations & dependencies
- Scheduler for job intake and retries.
- Policy Engine for evidence handoff.
- Export Center / Offline Kit for artifact packaging.
Operational notes
- CAS caches, bounded retries, DSSE integration.
- Monitoring dashboards (see the Analyzers Grafana dashboard).
- RustFS migration playbook.
Related resources
Operations
- Analyzers operations guide
- Analyzers Grafana dashboard
- RustFS migration playbook
- Entrypoint resolution
- Secret leak detection
- Proof-of-Entitlement dev harness
- DSSE / Rekor operator guide
- SBOM hot-lookup operations
- Field engagement notes
Analyzers & evidence
- Endpoint registration matrix
- Node analyzer
- Go analyzer
- OS analyzers — evidence model
- Artifact association graph
- Deterministic SBOM composition
- SBOM attestation hot-lookup profile
Design
Benchmarks
Implementation Status
Phase 1 – Control plane & job queue (Complete)
- Scanner WebService with queue abstraction (Valkey/NATS)
- Job leasing with retries and dead-letter handling
- CAS layer cache and artifact catalog
- REST API endpoints for scan management
Phase 2 – Analyzer parity & SBOM assembly (In Progress)
- OS analyzers: apk/dpkg/rpm with deterministic metadata
- Language analyzers: Java, Node, Python, Go, .NET, Rust with lock file support
- Native analyzers: ELF/PE/MachO for binary analysis
- SBOM views: inventory/usage with CycloneDX/SPDX emitters
- Entry trace resolution and dependency analysis
Phase 3 – Diff & attestations (In Progress)
- Three-way diff engine (base, target, runtime)
- DSSE SBOM/report signing pipeline
- Attestation hand-off to Signer/Attestor
- Metadata for Export Center integration
Phase 4 – Integrations & exports (Planned)
- Policy Engine integration for evaluation
- Vuln Explorer metadata delivery
- Export Center artifact packaging
- CLI/Console workflows and buildx plugin
Phase 5 – Observability & resilience (Planned)
- Metrics: queue depth, scan latency, cache hit/miss, analyzer timing
- Queue backpressure handling and cache eviction
- SLO dashboards and alerting
- Smoke tests and runbooks
Key Acceptance Criteria
- Scans produce deterministic SBOM inventory/usage with stable component identity
- Queue/worker pipeline handles retries, backpressure, offline kits
- DSSE attestations exported for Signer/Attestor without transformation
- CLI/Console parity for scan submission, diffing, exports, verification
- Offline scanning supported with local caches and manifest verification
Technical Decisions & Risks
- Analyzer drift prevented via golden fixtures, hash-based regression tests, deterministic sorting
- Queue overload mitigated with adaptive backpressure, worker scaling, priority lanes
- Storage growth managed via CAS dedupe, ILM policies, offline bundle pruning
- Lock file integration (npm/yarn/pnpm, pip/poetry, gradle) with declared-only components
- Surface cache reuse for Linux OS analyzers with rootfs-relative evidence
Recent Enhancements (2025-12-12)
- Deterministic SBOM composition with DSSE fixtures and offline verification
- Node/Python/Java lock file collectors with CLI validation commands
- Platform events rollout with scanner.report.ready@1 and scanner.scan.completed@1
- Surface-cache environment resolution with startup validation
Gating Explainability (Quiet-by-Design Triage)
The Scanner WebService exposes gating explainability through the triage APIs to support the “Quiet-by-Design” UX pattern where noise is gated at the source and proof is surfaced with one click.
Gating Reasons
Findings can be hidden by default based on the GatingReason enum (Contracts/GatingContracts.cs). Enum members and their numeric values:
| Enum member (value) | Description |
|---|---|
None (0) | Not gated; visible in the default view |
Unreachable (1) | Vulnerable code not reachable from any application entrypoint |
PolicyDismissed (2) | Policy rule dismissed/waived/tolerated this finding |
Backported (3) | Patched via distro backport (version comparison confirms fixed) |
VexNotAffected (4) | VEX statement declares not_affected with sufficient trust |
Superseded (5) | Superseded by a newer advisory or CVE |
UserMuted (6) | Explicitly muted by a user decision |
Key DTOs
Source: Contracts/GatingContracts.cs and Contracts/TriageContracts.cs.
FindingGatingStatusDto- CarriesGatingReason,IsHiddenByDefault,SubgraphId,DeltasId,GatingExplanation, andWouldShowIf(criteria that would un-gate the finding). Computed byGatingReasonService.FindingTriageStatusWithGatingDto- Composes the baseFindingTriageStatusDto(BaseStatus) with an optionalGating(FindingGatingStatusDto) andVexTrust(TriageVexTrustStatusDto).FindingTriageStatusDto.Evidence[*].Typeuses the persisted Scanner triage wire vocabulary (SBOM_SLICE,VEX_DOC,PROVENANCE,CALLSTACK_SLICE,REACHABILITY_PROOF,REPLAY_MANIFEST,POLICY,SCAN_LOG,OTHER).TriageStatusServicemaps these values exhaustively and fails on unknown enum members rather than leaking CLR names.TriageVexTrustStatusDto- Wraps the baseTriageVexStatusDto(VexStatus) and addsTrustScore,PolicyTrustThreshold,MeetsPolicyThreshold, andTrustBreakdown(VexTrustBreakdownDto).GatedBucketsSummaryDto- Counts of hidden findings per gating reason (UnreachableCount,PolicyDismissedCount,BackportedCount,VexNotAffectedCount,SupersededCount,UserMutedCount) plus a computedTotalHiddenCount.BulkTriageQueryWithGatingResponseDto- IncludesGatedBuckets(GatedBucketsSummaryDto),TotalCount,VisibleCount,NextCursor, andSummary.
VEX Trust Scoring
VEX statements are evaluated against a policy trust threshold (default 0.7 — GatingReasonService.DefaultPolicyTrustThreshold). The composite trust score is a weighted combination of four factors (VexTrustBreakdownDto):
- Issuer trust (weight 0.4): issuer reputation (e.g.
nvd= 1.0, RedHat/Canonical/Debian = 0.95). - Recency trust (weight 0.2): age of the statement (≤7 days = 1.0, decaying to 0.3 for >1 year).
- Justification trust (weight 0.2): length/detail of the justification text.
- Evidence trust (weight 0.2): presence of DSSE envelope, signature reference, and source reference.
When a not_affected VEX record scores >= DefaultPolicyTrustThreshold, the finding is gated with GatingReason.VexNotAffected.
Unified Evidence Endpoint
GET /api/v1/triage/findings/{findingId}/evidence (route base api/v1/triage, Controllers/TriageController.cs) returns all evidence tabs in one call. Each tab can be toggled via include* query parameters (includeSbom, includeReachability, includeVex, includeAttestations, includeDeltas, includePolicy, includeReplayCommand; all default true). The response is ETag-cached (Cache-Control: private, max-age=300) and honours If-None-Match for 304 Not Modified. Tabs returned:
- SBOM reference and component metadata
- Reachability subgraph with call paths
- VEX claims with trust scores
- Attestation summaries
- Delta comparison
- Policy evaluation results
- Manifest hashes for verification
- Replay command for deterministic reproduction
The canonical findingId is a bare UUID from GET /api/v1/triage/inbox?artifactDigest=..., specifically a value returned in paths[].findingIds[]. The artifact digest is a tenant-scoped multi-row filter; it is not a finding identity. Findings-service matcher tokens are a separate identity namespace and must not be used on Scanner Triage routes.
Real responses include the linked stored scan record and stored finding lifecycle timestamps. A real finding with missing child evidence returns 200 with availability.status=partial and sorted availability.missing[] entries that name the stable absence code, producer, and recovery action. Scanner does not manufacture missing hashes, SBOM/policy versions, attestation verification, or replay inputs: unknown values remain null or unknown. Equal-timestamp child records are selected deterministically using the record UUID as the tie-break. A missing or cross-tenant finding returns a non-echoing typed 404 with producer and inbox-recovery metadata.
Evidence Bundle Export
GET /api/v1/triage/findings/{findingId}/evidence/export?format=zip (also accepts format=tar.gz) returns a downloadable archive built by EvidenceBundleExporter. The response carries an X-Archive-Digest: sha256:... header for verification. A single-finding bundle contains (file names emitted by EvidenceBundleExporter.PrepareEvidenceFilesAsync):
manifest.json- Bundle manifest (ArchiveManifestDto) with per-file SHA-256 hashessbom.cdx.json- CycloneDX SBOM slice (when SBOM evidence is present)reachability.json- Reachability evidence (when present)vex/<source>.json- VEX claims grouped by sourceattestations/<predicate>.dsse.json- DSSE attestation envelopespolicy/- Policy evaluation evidencereplay.sh/replay.ps1- Replay scripts
The run-level export (ExportRunAsync) instead nests each finding under findings/<findingId>/ beneath a top-level MANIFEST.json and README.md.
Replay Command Generation
The IReplayCommandService (ReplayCommandService) generates copy-ready CLI commands. The default binary is stellaops (overridable via scanner:replayCommands:binary). The full command form is:
stellaops replay --target <component-purl> --cve <CVE-ID> --feed-snapshot <hash> --policy-hash <hash> --verify
When a knowledge snapshot is available, a short form is also emitted (stellaops replay --target ... --cve ... --snapshot <snapshotId> --verify). For offline replay (IncludeOffline): stellaops replay --target ... --cve ... --bundle ./evidence-<scanId>-<findingId>.tar.gz --offline --verify.
The response (ReplayCommandResponseDto) also carries the ExpectedVerdictHash so a replayed verdict can be byte-compared against the recorded one.
UI Wireframes
Gated Buckets Summary
┌─────────────────────────────────────────────────────────────────────────────┐
│ Gated Findings Summary │
├─────────────────────────────────────────────────────────────────────────────┤
│ ┌────────────────────────┐ │
│ │ 12 actionable │ (96 hidden) │
│ └────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ +42 │ │ +15 │ │ +8 │ │ +23 │ │
│ │ unreachable │ │ policy │ │ backported │ │ VEX │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ +3 │ │ +5 │ │ [Show all] │ │
│ │ superseded │ │ muted │ └─────────────────┘ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
VEX Trust Display
Illustrative UI mock-up. The default policy trust threshold is
0.7(GatingReasonService.DefaultPolicyTrustThreshold), not0.80, and the canonical breakdown components are Issuer / Recency / Justification / Evidence (VexTrustBreakdownDto) — the “Authority / Accuracy / Timeliness / Verification” labels below are illustrative only. See “VEX Trust Scoring” above for the authoritative values.
┌─────────────────────────────────────────────────────────────────────────────┐
│ VEX Status: not_affected │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Trust Score: ████████░░ 0.85 │
│ Threshold: ──────── 0.80 ✓ Meets policy │
│ │
│ Issuer: vendor.example │
│ Justification: vulnerable_code_not_in_execute_path │
│ │
│ ┌─ Trust Breakdown ─────────────────────────────────────────────────┐ │
│ │ Authority: ██████████░ 0.90 │ │
│ │ Accuracy: ████████░░░ 0.85 │ │
│ │ Timeliness: ████████░░░ 0.80 │ │
│ │ Verification: ████████░░░ 0.85 │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Replay Command Component
┌─────────────────────────────────────────────────────────────────────────────┐
│ Replay Command │
│ Reproduce this verdict deterministically │
├──────────┬─────────┬─────────────────────────────────────────────────────────┤
│ [Full] │ Short │ Offline │
├──────────┴─────────┴─────────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ $ stellaops replay \ │ │
│ │ --target pkg:npm/lodash@4.17.15 \ │ │
│ │ --cve CVE-2024-1234 \ │ │
│ │ --feed-snapshot sha256:feed789... \ │ │
│ │ --policy-hash sha256:policy321... \ │ │
│ │ --verify │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ ┌───────────────┐ │
│ │ 📋 Copy │ │
│ └───────────────┘ │
├─────────────────────────────────────────────────────────────────────────────┤
│ 📦 Download Evidence Bundle 12.5 KB · ZIP │
├─────────────────────────────────────────────────────────────────────────────┤
│ Expected verdict hash: sha256:verdict123... │
└─────────────────────────────────────────────────────────────────────────────┘
Gating Explainer Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ Finding: CVE-2024-1234 │
│ lodash@4.17.15 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Status: Hidden by default [Why hidden?] ←─┐ │
│ │ │
│ ┌───────────────┴───────────────────────────┐ │
│ │ Why is this finding hidden? │ │
│ ├───────────────────────────────────────────┤ │
│ │ │ │
│ │ This finding is gated because: │ │
│ │ │ │
│ │ ✓ VEX not_affected (trust: 0.85) │ │
│ │ Vendor issued not_affected statement │ │
│ │ with justification: │ │
│ │ "vulnerable_code_not_in_execute_path" │ │
│ │ │ │
│ │ Evidence: │ │
│ │ • VEX document: vex-vendor-2025-001 │ │
│ │ • Issued: 2025-12-15T10:00:00Z │ │
│ │ • Signature: ✓ Valid (ES256) │ │
│ │ │ │
│ │ [View Evidence] [Close] │ │
│ └───────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Evidence Bundle Contents
Illustrative UI mock-up. The authoritative single-finding file list is the one emitted by
EvidenceBundleExporter.PrepareEvidenceFilesAsync(see “Evidence Bundle Export” above):manifest.json,sbom.cdx.json,reachability.json,vex/<source>.json,attestations/<predicate>.dsse.json,policy/,replay.sh,replay.ps1.README.mdand the top-levelMANIFEST.jsonare emitted only by the run-level export. Per-VEX-source file names below (nvd.json,cisa-kev.json) are examples, not fixed names.
evidence-f-abc123/
├── manifest.json ← Archive manifest with SHA-256 hashes
├── README.md ← Human-readable documentation
├── sbom.cdx.json ← CycloneDX SBOM slice
├── reachability.json ← Reachability analysis
├── vex/
│ ├── vendor.json ← Vendor VEX statement
│ ├── nvd.json ← NVD data
│ └── cisa-kev.json ← CISA KEV flag
├── attestations/
│ ├── sbom.dsse.json ← SBOM DSSE envelope
│ └── scan.dsse.json ← Scan DSSE envelope
├── policy/
│ └── evaluation.json ← Policy evaluation result
├── delta.json ← Delta comparison
├── replay-command.txt ← Copy-ready CLI command
├── replay.sh ← Bash replay script
└── replay.ps1 ← PowerShell replay script
See Sprint 9200.0001.0001-0004 for implementation details.
Epic alignment
- Epic 6 – Vulnerability Explorer: provide policy-aware scan outputs, explain traces, and findings ledger hooks for triage workflows.
- Epic 10 – Export Center: generate export-ready artefacts, manifests, and DSSE metadata for bundles.
