# Competitor Ingest Fallback Hierarchy (CM6) Status: Draft · Date: 2025-12-04 Scope: Establish fallback hierarchy when external SBOM/scan data is incomplete, with explicit decision traces. ## Objectives - Define clear fallback levels for incomplete data. - Ensure transparent decision tracking. - Enable policy-based confidence scoring. - Support offline fallback evaluation. ## Fallback Levels ### Hierarchy Definition ``` Level 1: Signed SBOM with valid provenance │ └──► Level 2: Unsigned SBOM with tool metadata │ └──► Level 3: Scan-only results │ └──► Level 4: Reject (no evidence) ``` ### Level Details | Level | Source | Confidence | Requirements | Warnings | |-------|--------|------------|--------------|----------| | 1 | Signed SBOM | 1.0 | Valid signature, valid provenance | None | | 2 | Unsigned SBOM | 0.7 | Tool metadata, component purl, scan timestamp | `provenance_unknown` | | 3 | Scan-only | 0.5 | Scan timestamp | `degraded_confidence`, `no_sbom` | | 4 | Reject | 0.0 | None met | - | ### Level 1: Signed SBOM Requirements: - DSSE/COSE/JWS signature present - Signature verification passes - Signer key in trusted keyring - Provenance metadata valid ```json { "fallback": { "level": 1, "source": "signed_sbom", "confidence": 1.0, "decision": { "reason": "Valid signature and provenance", "checks": { "signaturePresent": true, "signatureValid": true, "keyTrusted": true, "provenanceValid": true } } } } ``` ### Level 2: Unsigned SBOM Requirements (all must be present): - Tool name and version - Component list with PURLs - At least one SHA-256 hash per component - Scan timestamp ```json { "fallback": { "level": 2, "source": "unsigned_sbom", "confidence": 0.7, "decision": { "reason": "Valid SBOM without signature", "checks": { "signaturePresent": false, "toolMetadata": true, "componentPurls": true, "componentHashes": true, "scanTimestamp": true }, "warnings": ["provenance_unknown"] } } } ``` ### Level 3: Scan-only Requirements: - Scan timestamp present - At least one finding or component ```json { "fallback": { "level": 3, "source": "scan_only", "confidence": 0.5, "decision": { "reason": "Scan results without SBOM", "checks": { "signaturePresent": false, "toolMetadata": false, "scanTimestamp": true, "hasFindings": true }, "warnings": ["degraded_confidence", "no_sbom"] } } } ``` ### Level 4: Reject When no requirements met: ```json { "fallback": { "level": 4, "source": "reject", "confidence": 0.0, "decision": { "reason": "No acceptable evidence found", "checks": { "signaturePresent": false, "toolMetadata": false, "scanTimestamp": false, "hasFindings": false }, "action": "reject", "errorCode": "E2010" } } } ``` ## Decision Evaluation ### Evaluation Algorithm ```python def evaluate_fallback(input_data: dict) -> FallbackDecision: checks = { "signaturePresent": has_signature(input_data), "signatureValid": False, "keyTrusted": False, "provenanceValid": False, "toolMetadata": has_tool_metadata(input_data), "componentPurls": has_component_purls(input_data), "componentHashes": has_component_hashes(input_data), "scanTimestamp": has_scan_timestamp(input_data), "hasFindings": has_findings(input_data) } # Level 1 check if checks["signaturePresent"]: sig_result = verify_signature(input_data) checks["signatureValid"] = sig_result.valid checks["keyTrusted"] = sig_result.key_trusted checks["provenanceValid"] = verify_provenance(input_data) if all([checks["signatureValid"], checks["keyTrusted"], checks["provenanceValid"]]): return FallbackDecision(level=1, confidence=1.0, checks=checks) # Level 2 check if all([checks["toolMetadata"], checks["componentPurls"], checks["componentHashes"], checks["scanTimestamp"]]): return FallbackDecision( level=2, confidence=0.7, checks=checks, warnings=["provenance_unknown"] ) # Level 3 check if checks["scanTimestamp"] and checks["hasFindings"]: return FallbackDecision( level=3, confidence=0.5, checks=checks, warnings=["degraded_confidence", "no_sbom"] ) # Level 4: Reject return FallbackDecision( level=4, confidence=0.0, checks=checks, action="reject", error_code="E2010" ) ``` ## Decision Trace ### Trace Format ```json { "trace": { "id": "trace-12345", "timestamp": "2025-12-04T12:00:00Z", "input": { "hash": "b3:...", "size": 12345, "format": "cyclonedx-1.6" }, "evaluation": { "steps": [ { "check": "signaturePresent", "result": false, "details": "No DSSE/COSE/JWS envelope found" }, { "check": "toolMetadata", "result": true, "details": "Found tool: syft v1.0.0" }, { "check": "componentPurls", "result": true, "details": "42 components with valid PURLs" }, { "check": "componentHashes", "result": true, "details": "42 components with SHA-256 hashes" }, { "check": "scanTimestamp", "result": true, "details": "Timestamp: 2025-12-04T00:00:00Z" } ], "decision": { "level": 2, "confidence": 0.7, "warnings": ["provenance_unknown"] } } } } ``` ### Trace Persistence Decision traces are: - Stored with normalized output - Included in API responses - Available for audit queries - Deterministic (same input = same trace) ## Policy Integration ### Confidence Thresholds ```json { "policy": { "minConfidence": { "production": 0.8, "staging": 0.5, "development": 0.0 }, "allowedLevels": { "production": [1], "staging": [1, 2], "development": [1, 2, 3] } } } ``` ### Policy Evaluation ```rego # policy/ingest/fallback.rego package ingest.fallback import rego.v1 default allow = false allow if { input.fallback.level <= max_allowed_level input.fallback.confidence >= min_confidence } max_allowed_level := data.policy.allowedLevels[input.environment][_] min_confidence := data.policy.minConfidence[input.environment] deny contains msg if { input.fallback.level > max_allowed_level msg := sprintf("Fallback level %d not allowed in %s", [input.fallback.level, input.environment]) } warn contains msg if { warning := input.fallback.decision.warnings[_] msg := sprintf("Fallback warning: %s", [warning]) } ``` ## Override Mechanism ### Manual Override ```bash # Accept unsigned SBOM in production (requires approval) stellaops ingest import \ --input external-sbom.json \ --allow-unsigned \ --override-reason "Emergency import per ticket INC-12345" \ --override-approver security-admin@example.com ``` ### Override Record ```json { "override": { "enabled": true, "level": 2, "originalDecision": { "level": 4, "reason": "Would normally reject" }, "overrideReason": "Emergency import per ticket INC-12345", "approver": "security-admin@example.com", "approvedAt": "2025-12-04T12:00:00Z", "expiresAt": "2025-12-05T12:00:00Z" } } ``` ## Links - Sprint: `docs/implplan/SPRINT_0186_0001_0001_record_deterministic_execution.md` (CM6) - Verification: `docs/modules/scanner/design/competitor-signature-verification.md` (CM2) - Normalization: `docs/modules/scanner/design/competitor-ingest-normalization.md` (CM1)