StellaOps Scanner

Verified against commit 942a18727c (2026-09-26). Re-verify with: grep -nE "^(app|apiGroup)\.Map" src/Scanner/StellaOps.Scanner.WebService/Program.cs and grep -n "^ scanner-" devops/release/bundle/docker-compose.yml.

The estate-wide service, database, compose and bundle wiring is generated in the service inventory; this dossier describes the module and defers to that inventory for the service list.

Layer-by-layer container image analysis that produces deterministic SBOMs, per-layer fragments, diffs, reachability evidence and signed reports.

Scanner decomposes container images layer by layer, runs signed OS, language and native analyzers, and emits deterministic SBOMs, per-layer fragments, diffs, reachability evidence and DSSE-signed reports for Policy, Release Orchestrator, Findings and the Console. Advisory knowledge (matching, affected symbols, EPSS) comes from the Vulnerabilities hub over its owner API; Scanner computes no policy verdicts.

This page is the module map. The full contract is architecture.md; the route catalogue is endpoint-registration-matrix.md.

Deployables

DeployableSourceRole
scanner-websrc/Scanner/StellaOps.Scanner.WebServiceREST control plane (port 8444, alias scanner.stella-ops.local, Router microservice scanner): scan submission and status, exports, triage, reports, sources, settings, OCI referrer store (/v2/...), Doctor checks, sole migration authority for the scanner schema.
scanner-workersrc/Scanner/StellaOps.Scanner.WorkerQueue consumer (scanner:jobs on Valkey) that pulls images, builds the filesystem, runs analyzers, and publishes SBOM referrers, reachability evidence and surface manifests.
scanner-cache-initworker image, one-shotCreates and chowns the cache and surface volumes before both hosts start.

Other projects at the module root are not deployables: StellaOps.Scanner.Sbomer.BuildXPlugin (BuildKit generator CLI, packaged through devops/release/components.json) and StellaOps.Scanner.Analyzers.Native (ELF/PE/Mach-O readers, a class library).

Persistence

Integrations & dependencies

Recent changes (2026-09-24…26)

See architecture.mdsections 3.2, 4 and 5.2.1 for the details.

Operations

Contracts

Analyzers & evidence

Design

Benchmarks

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.

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):

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:

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):

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.

The gated-bucket summary, VEX trust display, replay command and gating explainer are rendered by the Console (src/Web/); the authoritative values are the DTOs and constants named above.