Add signal contracts for reachability, exploitability, trust, and unknown symbols
- Introduced `ReachabilityState`, `RuntimeHit`, `ExploitabilitySignal`, `ReachabilitySignal`, `SignalEnvelope`, `SignalType`, `TrustSignal`, and `UnknownSymbolSignal` records to define various signal types and their properties. - Implemented JSON serialization attributes for proper data interchange. - Created project files for the new signal contracts library and corresponding test projects. - Added deterministic test fixtures for micro-interaction testing. - Included cryptographic keys for secure operations with cosign.
This commit is contained in:
339
docs/modules/scanner/design/competitor-fallback-hierarchy.md
Normal file
339
docs/modules/scanner/design/competitor-fallback-hierarchy.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user