Scanner Endpoint Registration Matrix
Verified against commit
942a18727c(2026-09-26). Re-verify with:grep -nE "^(app|apiGroup)\.Map" src/Scanner/StellaOps.Scanner.WebService/Program.csandgrep -rnE "RequireAuthorization|AllowAnonymous" src/Scanner/StellaOps.Scanner.WebService/Endpoints/.
This file is the Scanner WebService source-of-truth for endpoint registration intent and authorization posture. Program.cs is the authority; policy names below are the ScannerPolicies constants (src/Scanner/StellaOps.Scanner.WebService/Security/ScannerPolicies.cs), and the scopes each policy accepts are bound in Program.cs (see architecture.md section 10). Routes under apiGroup are relative to scanner:api:basePath (default /api/v1); every apiGroup route passes through ScannerCapacityPauseFilter.
Q-25 assigns canonical HTTP import/status/manifest/validate, carrier quarantine/CAS, lifecycle and import-ledger ownership to OfflineKit at /api/offlinekit/v1. Scanner accepts the exact Scanner artifact-BOM part through its bounded owner seam; MapOfflineKitEndpoints and the route-only MapObservabilityEndpoints are not Scanner endpoint maps and must not be reintroduced. /metrics is not an active Scanner route.
Captured openapi_* snapshots describe their capture-time route surface and are not current source authority; the live document is GET /openapi/v1.json (anonymous, app.MapOpenApi()).
Host-level routes (outside the API base path)
| Registration | Routes | Authorization | Notes |
|---|---|---|---|
MapHealthEndpoints | GET /healthz, GET /readyz | ScansRead | Health and readiness are authenticated. |
MapServiceDoctorEndpoints("scanner-web") | doctor-check/v1 read surface | DoctorHealth (ops:health) | Runs the doctor.scanner.* checks from StellaOps.Scanner.Operations. |
MapBuildInfoEndpoint | GET /buildinfo.json | anonymous | Build identity baked into the image. |
MapScannerArtifactBomExchangeEndpoints | GET /api/internal/v1/offlinekit/artifact-boms; POST /api/internal/v1/offlinekit/artifact-boms/activate | ArtifactBomExchange | Bounded exact-tenant export/activation for scanner/artifact-boms.v1.json; Scanner remains the sole persistence owner. |
| inline | GET /internal/plugins/status, POST /internal/plugins/probe | anonymous | Signed analyzer bundle status/probe for the mounted plug-in directory. |
MapControllers | api/v1/triage/* (gating, gated buckets, unified evidence, evidence export, replay commands, rationale), api/v1/findings/{findingId}/evidence + evidence/batch, api/v1/vulnerabilities* | per controller: TriageRead (triage, findings evidence); ScansRead (vulnerabilities) | Every controller must declare a named policy or a registered anonymous opt-out (ScannerControllerAuthorizationConformanceTests); the host has no fallback policy. VulnerabilitiesController serves the development fixture only (404 scanner.vulnerabilities.not_available otherwise). The VEX gate controller (api/v1/scans/{scanId}/gate-*, gate-policy, vex-reachability/filter) was withdrawn by PCD-222 (architecture.md section 5.5.6). |
MapOpenApi | /openapi/v1.json | anonymous | Read by the gateway aggregator. |
MapSliceEndpoints | /api/slices (query, {digest}, replay, cache/stats) | ScansRead; cache/stats Admin | Reachability slices. |
MapOciReferrersReadEndpoints | `GET | HEAD /v2/(anonymous ping),GET | HEAD /v2/{**path}` |
MapFunctionMapEndpoints | /api/v1/function-maps (list, create, get, delete, verify, coverage) | ScansRead / ScansWrite | Relocated from Platform. |
API-group maps (registered in Program.cs)
| Map method | Base path | Authorization | Notes |
|---|---|---|---|
MapScanEndpoints | /scans | ScansEnqueue (submit), ScansRead, ScansWrite | Submit, status, entropy, events, entrytrace, ruby/bun packages; nests the maps below. |
nested MapScanDigestDetailEndpoints | /scans/by-digest/{imageDigest}/detail | ScansDigestDetailRead | DC-15 digest-scoped read (ScanCompletedContract.DetailRouteTemplate). Tenant from the validated claim only; 404 when no succeeded scan exists. |
nested MapScanCompletedStreamEndpoints (inventory) | /scans/completed/inventory | ScansCompletedProjectionRead | Fenced current successful inventory for the authenticated tenant. |
nested MapScanCompletedStreamEndpoints | /scans/completed/events, POST /scans/completed/consumers/{consumerId} | ScansCompletedProjectionRead | DC-15 tenant catch-up feed and remote-consumer cursor. |
nested MapCallGraphEndpoints | POST /scans/{scanId}/callgraphs | CallGraphIngest | |
nested MapSbomEndpoints | POST /scans/{scanId}/sbom | ScansWrite | |
nested MapLayerSbomEndpoints | /scans/{scanId}/layers, /layers/{layerDigest}/sbom, /composition-recipe, POST /composition-recipe/verify | ScansRead | Served from the Worker’s surface-manifest layer.fragments artifact (DEF-11). |
nested MapReachabilityEndpoints | /scans/{scanId}/compute-reachability, /reachability/{components,findings,explain,traces/export} | ScansWrite / ScansRead | |
nested MapReachabilityDriftScanEndpoints | /scans/{scanId}/drift | ScansRead | |
nested MapExportEndpoints | /scans/{scanId}/exports/{sbom,sarif,cdxr,openvex,signed-sbom-archive}, POST .../signed-sbom-material | ScansRead; material SignedSbomMaterialWrite | SBOM export reads the Worker-persisted CycloneDX document (DAY-3). |
nested MapEvidenceEndpoints | /scans/{scanId}/evidence, /evidence/{**findingId} | ScansRead | |
nested MapApprovalEndpoints | /scans/{scanId}/approvals[/{findingId}] | ScansApprove / ScansRead | Resolves the scan through the tenant-aware IScanCoordinator first; foreign or unknown scan: 404, no body (PCD-864). |
nested MapManifestEndpoints | /scans/{scanId}/manifest, /proofs[/{rootHash}] | ScansRead | Resolves the scan through the tenant-aware IScanCoordinator first; foreign, unknown or ownerless scan: 404, no body (PCD-864). |
nested MapGitHubCodeScanningEndpoints | /scans/{scanId}/github/{upload-sarif,upload-status/{sarifId},alerts[/{alertNumber}]} | ScansWrite / ScansRead | 503 github_code_scanning_not_configured when disabled. |
MapSbomReadinessEndpoints | /sbom-readiness, /sbom-attachments | ScansRead | Release gate readiness contract. |
MapSourcesEndpoints | /sources | SourcesRead / SourcesWrite / SourcesAdmin | Tenant-aware source CRUD and source runs. |
MapScmImportEndpoints | /scm/{integrationId}/bulk-import, /scm/imports/{batchId} | SourcesWrite / SourcesRead | |
MapWebhookEndpoints | /webhooks | AllowAnonymous | Quarantined: every route returns 501 source_scan_not_available. |
MapSbomUploadEndpoints | /byos-sboms, /byos-sboms/{sbomId}; nests /sbom/hot-lookup/* and /sbom/{scanId}/evidence | ScansWrite / ScansRead | |
MapOciReferrerIngestEndpoints | POST /oci-referrers | ScansWrite | CLI/CI referrer ingest into the internal store. |
MapReachabilityDriftRootEndpoints | /drift/{driftId}/sinks | ScansRead | |
MapDeltaCompareEndpoints | /delta (compare, quick, {comparisonId}) | ScansRead | |
MapSmartDiffEndpoints | /smart-diff (scans/{scanId}/changes, scans/{scanId}/sarif) | ScansRead | Material-change retrieval and its SARIF rendering; no automatic generation pipeline. The VEX-candidate list and review routes were withdrawn by PCD-222. |
MapBaselineEndpoints | /baselines | ScansRead | Reserved shapes; deterministic 501 scanner_baseline_recommendations_unavailable. |
MapActionablesEndpoints | /actionables | ScansRead | |
MapCounterfactualEndpoints | /counterfactuals | ScansRead | |
MapProofSpineEndpoints | /spines/{spineId}, /scans/{scanId}/spines | ScansRead | Tenant required. |
MapReplayEndpoints | POST /replay/{scanId}/attach | ScansWrite | |
MapScoreReplayEndpoints (feature-gated) | /scans/{scanId}/score/{replay,bundle,verify,history} | ScansRead / ScansWrite | Only when scanner:scoreReplay:enabled=true. No /score/{scanId} alias. Same tenant gate as MapManifestEndpoints (PCD-864). |
MapWitnessEndpoints | /witnesses | ScansRead | |
MapEpssEndpoints | /epss | ScansRead | Reads EPSS from the Vulnerabilities hub source. |
MapTriageStatusEndpoints | /triage/findings/{findingId}, /status, /vex, /query, /summary | TriageRead / TriageWrite | Owned status/VEX mutations return 501 scanner.triage.mutation_unavailable; missing/foreign findings remain 404. No decision, snapshot or VEX record is created. |
MapTriageInboxEndpoints | /triage/inbox | TriageRead | Requires artifactDigest; returns canonical bare UUIDs in findingIds[]. |
MapBatchTriageEndpoints | /triage/inbox/clusters/stats, /triage/inbox/clusters/{pathId}/actions | TriageRead / TriageWrite | Owned cluster actions return 501 scanner.triage.mutation_unavailable before returning updated IDs or an action record. Missing/foreign clusters remain 404; statistics remain readable. |
MapProofBundleEndpoints | POST /triage/proof-bundle | TriageWrite | Reserved shape; IProofBundleGenerator has no implementation or DI registration. |
MapUnknownsEndpoints | /unknowns | ScansRead | Tenant-scoped list/detail/evidence/history/stats/bands. |
MapSecretDetectionSettingsEndpoints | /secrets/config/{settings,exceptions,rules} | SecretSettings* / SecretExceptions* | |
MapScannerTenantSettingsEndpoints | /scanner/settings | OperationsRead / OperationsWrite | Strong ETag compare-and-swap. |
MapScannerAnalyzerEndpoints | /scanner/analyzers | OperationsRead | |
MapScannerPerformanceEndpoints | /scanner/performance | OperationsRead | |
MapSecretsBundleEndpoints | /secrets/bundles | SecretsBundleRead / SecretsBundleWrite | |
MapScanPolicyEndpoints | /scan-policies | ScansRead / ScansWrite | |
MapPolicyEndpoints (feature-gated) | /policy (schema, diagnostics, preview, runtime, overlay; linksets withdrawn 2026-09-27, PCD-47) | Reports | Registered when scanner:features:enablePolicyPreview is true (the default). |
MapReportEndpoints | /reports, /reports/artifact-vulnerabilities | Reports | |
MapScanAttestationKeyEndpoints | GET /scan-attestation/keys | anonymous | Public JWKS for scan-result attestation verification. |
MapRuntimeEndpoints | /runtime/events, /runtime/reconcile | RuntimeIngest | |
MapReachabilityStackEndpoints | /reachability/{findingId}/stack, /stack/layer/{layerNumber}, /reachability/{scanId}/stack/frames | ScansRead | |
MapReachabilityEvidenceEndpoints | /reachability/{analyze,result/{jobId},mapping/{cveId},vex,verdicts/{imageDigest}[/{vulnerabilityId}]} | ScansRead / ScansWrite | Analyze, result and VEX store and read job results under the claim tenant; no claim: 400 tenant_missing (PCD-864). |
MapAdvancedAssuranceReachabilityFixtureEndpoints, MapAdvancedAssuranceSbomFixtureEndpoints | /qa/fixtures/advanced-assurance-golden/* | ScansWrite | QA fixture import/readback surfaces. The reachability import stores results under the claim tenant (PCD-864). |
__auth-probe (Testing environment only) | /__auth-probe | ScansEnqueue | Registered only when the host environment is Testing. |
Webhook tenant contract (name routes)
All webhook routes are quarantined: RejectQuarantinedScanTrigger answers 501 source_scan_not_available before any dispatch (source-triggered scanning has no production IScanJobQueue; see src/Scanner/AGENTS.md). The tenant rules below describe the preserved, unwired receiver contract.
- Name-based webhook routes are:
POST /api/v1/webhooks/docker/{sourceName}POST /api/v1/webhooks/github/{sourceName}POST /api/v1/webhooks/gitlab/{sourceName}POST /api/v1/webhooks/harbor/{sourceName}- Tenant context is required for name-based routes (claim or
X-StellaOps-TenantId/compatibility alias). Missing tenant returns400withtenant_missing; claim/header mismatch returns400withtenant_conflict. - Source lookup is tenant-scoped via
GetByNameAsync(tenantId, sourceName), so same-name sources in different tenants cannot cross-dispatch. POST /api/v1/webhooks/{sourceId}accepts optional tenant context; if present and it does not match source ownership, the endpoint returns404.
Triage tenant contract
- Triage and finding-evidence routes resolve tenant context via
ScannerRequestContextResolver. - Resolution uses the validated tenant claim through the shared
StellaOpsTenantResolver; there is no default-tenant fallback. The canonical tenant-switch header is honored only for authenticated global-admin principals, and legacy tenant headers are not tenancy sources. - Missing or conflicting validated tenancy returns a deterministic request error rather than selecting a fallback tenant.
- Triage-facing contracts now require tenant input (
ITriageStatusService,ITriageQueryService,IFindingQueryService,IGatingReasonService,IUnifiedEvidenceService,IReplayCommandService,IFindingRationaleService) and enforce tenant-filtered data access. - Triage persistence is tenant partitioned (
triage_scan.tenant_id,triage_finding.tenant_id), and the active-case uniqueness key includestenant_idto avoid cross-tenant collisions. - Cross-tenant finding lookups are treated as deterministic not-found responses (
404) on triage/finding endpoints. /api/v1/triage/inbox?artifactDigest=...is the canonical identity bridge:artifactDigestfilters all matching tenant rows andpaths[].findingIds[]returns bare UUIDs consumed unchanged by/findings/{findingId}/evidence. Findings-service matcher tokens are not Scanner Triage aliases.- Existing findings may return partial unified evidence as
200with typedavailability.missing[]; missing and cross-tenant findings return a non-echoing404 scanner.triage.finding_not_foundwith producer and recovery metadata.
Unknowns tenant contract
- Unknowns routes resolve tenant context via
ScannerRequestContextResolver, a facade over the shared claims-onlyStellaOpsTenantResolver(thestellaops:tenantclaim; a global admin may select a tenant withX-StellaOps-TenantId); a request without a tenant fails with400(tenant_missing). - Unknown queries are tenant-scoped by a UUID predicate (
tenant_id = @tenantId) in the Scanner WebService query service.scanner.unknowns.tenant_idisUUID; the request tenant is converted withScannerTenantScope.Resolve, the same identity Scanner’s repositories store in their UUID tenant columns (a canonical UUID passes through; a slug maps to a deterministic name-based UUID). The earliertenant_id::text = @tenantIdtext comparison against the slug never matched (SPRINT_20261017_006 PCD-847 item a). No production path writesscanner.unknowns(verified 2026-10-18), so the routes return empty results until a producer exists. - Missing records across tenant boundaries resolve as deterministic
404for detail/evidence/history endpoints.
Reachability + SmartDiff tenant contract (SCAN-TEN-11)
- Reachability drift routes (
/api/v1/scans/{scanId}/drift,/api/v1/drift/{driftId}/sinks) now resolve tenant context viaScannerRequestContextResolverbefore repository access. - Drift persistence/retrieval paths are tenant-parameterized end-to-end (
call_graph_snapshots,code_changes,reachability_drift_results,drifted_sinks) instead of using a fixed tenant scope. Both drift routes are read-only: the drift GET computes a drift for a suppliedbaseScanIdwithout storing it (SPRINT_20261017_006 PCD-847 item d). The stored results they read are written by the Worker’sreachability-driftstage for scans submitted withreachability.drift.base_scan_id(PCD-874). - SmartDiff routes (
/api/v1/smart-diff/**) now resolve tenant context and pass it explicitly to the material-change store. - SmartDiff persistence/retrieval paths are tenant-parameterized (
material_risk_changes). The VEX-candidate store overvex_candidatesand its routes were withdrawn by PCD-222. - Remaining reachability/SmartDiff storage adapters used by these flows are tenant-parameterized as well (
risk_state_snapshots,reachability_results), removing fixed-tenant constants from Postgres adapter implementations.
Source-runs + secret-exception tenant contract (SCAN-TEN-13)
- Source run routes (
/api/v1/sources/{sourceId}/runs,/api/v1/sources/{sourceId}/runs/{runId}) now pass resolved tenant context intoISbomSourceRunRepositoryand SQL predicates (tenant_id = @tenantId) forscanner.sbom_source_runs. - Secret exception routes (
/api/v1/secrets/config/exceptions/{tenantId}/{exceptionId}) now use tenant-scoped repository methods for get/update/delete againstsecret_exception_patternrather than ID-only lookups. - The generic webhook route (
POST /api/v1/webhooks/{sourceId}) remains compatibility-tolerant for missing tenant context, but enforces ownership match when tenant context is present.
Deferred endpoint maps (present in source, not registered)
| Map method | Source | Reason | Activation prerequisite |
|---|---|---|---|
MapValidationEndpoints | Endpoints/ValidationEndpoints.cs (/api/v1/sbom/validate, /validators) | Validator service graph and option bindings are not part of the startup contract. | Explicit validation DI and contract docs, then registration in Program.cs. |
MapFidelityEndpoints | Endpoints/FidelityEndpoints.cs (/api/v1/scan/analyze, /findings/{findingId}/upgrade) | Experimental; needs an analyzer service contract. | Product sign-off and DI contract hardening. |
MapDeltaEvidenceEndpoints | Endpoints/DeltaEvidenceEndpoints.cs (/delta/evidence/*) | Not wired in Program.cs. | Contract decision and a backing evidence store. |
The Scanner-side /api/v1/registries/* group and the fabricated /api/v1/security/* adapter were removed; Platform owns both surfaces.
