Add tests and implement timeline ingestion options with NATS and Redis subscribers
- Introduced `BinaryReachabilityLifterTests` to validate binary lifting functionality. - Created `PackRunWorkerOptions` for configuring worker paths and execution persistence. - Added `TimelineIngestionOptions` for configuring NATS and Redis ingestion transports. - Implemented `NatsTimelineEventSubscriber` for subscribing to NATS events. - Developed `RedisTimelineEventSubscriber` for reading from Redis Streams. - Added `TimelineEnvelopeParser` to normalize incoming event envelopes. - Created unit tests for `TimelineEnvelopeParser` to ensure correct field mapping. - Implemented `TimelineAuthorizationAuditSink` for logging authorization outcomes.
This commit is contained in:
@@ -20,6 +20,11 @@ Establish versioned spine API/DTO schemas with migration rules, determinism guar
|
||||
- SP9: Pagination/ordering/perf budgets (stable sort keys, default page size limits, deterministic cursors).
|
||||
- SP10: Crosswalk mappings SBOM ↔ VEX ↔ graph ↔ policy (table + sample payloads).
|
||||
|
||||
## Acceptance/verification artifacts
|
||||
- Adapter CSV: `docs/modules/policy/fixtures/spine-adapters/v2-to-v1.csv` (hashes in `hashes.txt`).
|
||||
- Crosswalk table: `docs/modules/policy/fixtures/spine-crosswalk/crosswalk.csv` (hashes in `hashes.txt`).
|
||||
- Manifest signing proof: DSSE envelope must reference adapter + crosswalk hashes to anchor deterministic migrations.
|
||||
|
||||
## Migration & Determinism
|
||||
- Version headers and DTO version fields must be required; rejects if missing or downgraded without declared adapter.
|
||||
- Canonical JSON ordering for manifests; hashes computed over canonical form (UTF-8, no BOM).
|
||||
@@ -29,27 +34,40 @@ Establish versioned spine API/DTO schemas with migration rules, determinism guar
|
||||
- Header: `X-Spine-Version: v1` (required). DTO field mirror: `schemaVersion` (string, semver).
|
||||
- Deprecation window: N-1 supported for 90 days; adapters required to downgrade v2→v1 (CSV in `docs/modules/policy/fixtures/spine-adapters/`).
|
||||
- Hashing: canonical JSON, sorted properties, UTF-8 no BOM, normalized decimals (4dp), timestamps UTC ISO-8601.
|
||||
- Hash anchors (2025-12-03): `v2-to-v1.csv` BLAKE3=f259a807fae1cac90c4d52223924d808eb52a4ab2cb0d314ab2d651bfdad4273 SHA256=f5f067bd7814bd65213610a5ae4a35ce98e70a990ca1eb86d275a8abd3659a1a.
|
||||
|
||||
### Evidence minima per edge (SP2, draft)
|
||||
- `reachability`: state, confidence, score, method, evidenceRef (hash or URI), runtimeEvidence flag (bool).
|
||||
- `package_identity`: purl, name, version, supplier, hashes[] (at least SHA256).
|
||||
- `build_metadata`: buildId, sourceRepo, sourceRef, buildInvokerHash, provenanceHash (DSSE).
|
||||
- Ordering: edges sorted by `subjectPurl`, `predicate`, `createdAt` for determinism; missing evidenceRef is invalid.
|
||||
|
||||
### Unknowns workflow (SP3, draft)
|
||||
- States: `unknown`, `under_review`, `resolved`, `expired`.
|
||||
- SLA: auto-review escalation after 7 days; decay to `expired` at 30 days unless refreshed.
|
||||
- Surfacing: APIs must include `unknowns.count` and list endpoint with deterministic pagination; optional policy lattice flag to penalize unknowns.
|
||||
- Determinism: cursors encode the last `subjectPurl` + `createdAt`; no random salts.
|
||||
|
||||
### Signing (SP4/SP7)
|
||||
- Manifest structure: list of artifacts (type, id, hash, version, uri), signed using DSSE/ED25519 by default; Rekor optional online, mirrored checkpoints offline.
|
||||
- Stage policy: compile → ingest → materialize → export; each stage produces DSSE, carries prior stage hash for chain-of-custody.
|
||||
- Rekor/mirror matrix: online → Rekor+transparency required; offline → mirror checkpoints and DSSE only. PQ dual-sign optional but recorded in manifest metadata.
|
||||
|
||||
### Pagination/perf budgets (SP9)
|
||||
- Default page size 200; max 500; stable sort: tenant asc, subjectPurl asc, advisoryId asc, createdAt asc.
|
||||
- Cursors: base64-encoded tuple of sort keys; must round-trip deterministically.
|
||||
- Perf budget: p95 response ≤250ms for page=200 on cached dataset; timeouts return deterministic error `spine_timeout`.
|
||||
- Rate limits: 600 rpm per tenant; 429 payload includes retry-after seconds and last stable cursor.
|
||||
|
||||
### Crosswalk (SP10)
|
||||
- Provide table mapping: SBOM component ↔ spine node ↔ graph node ↔ policy evaluation input; include sample payloads in `docs/modules/policy/fixtures/spine-crosswalk/`.
|
||||
- Hash anchors (2025-12-03): `crosswalk.csv` BLAKE3=41926241c6d60bb856ceb4498e70381cdf54217435740f5fdf31ff8964044d78 SHA256=1e6644cdc00097b7e959e75f522335326b8f48fe1d05060d1c06ba660aac22a3.
|
||||
|
||||
## Decisions (2025-12-03)
|
||||
- Evidence minima above are binding for SP2; missing hashes are fatal validation errors.
|
||||
- Unknowns decay schedule adopted as written; extension requires policy-lattice approval.
|
||||
- Stage DSSE is mandatory at every boundary; Rekor optional offline but checkpoints must be mirrored with manifest hash list.
|
||||
- Pagination budgets and rate limits frozen until next version bump; adapters must preserve ordering when downgrading.
|
||||
|
||||
## Signing & Offline
|
||||
- DSSE envelope mandatory for spine manifest; Rekor entry optional online, mirrored checkpoints for offline kits.
|
||||
|
||||
@@ -1 +1 @@
|
||||
v2-to-v1.csv: BLAKE3=<TBD> SHA256=<TBD>
|
||||
v2-to-v1.csv: BLAKE3=f259a807fae1cac90c4d52223924d808eb52a4ab2cb0d314ab2d651bfdad4273 SHA256=f5f067bd7814bd65213610a5ae4a35ce98e70a990ca1eb86d275a8abd3659a1a
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
field,v2,v1,rule
|
||||
schemaVersion,schemaVersion,schemaVersion,copy
|
||||
componentRef,component.ref,component.ref,copy
|
||||
packageIdentity,edges.package_identity,edges.package_identity,copy
|
||||
reachabilityEvidence,edges.reachability.evidence.hash,edges.reachability.evidence_hash,copy
|
||||
unknownsState,unknowns.state,unknowns_state,enum_map:unknown=pending;under_review=review;resolved=resolved;expired=stale
|
||||
paginationCursor,meta.page.cursor,meta.page_cursor,stable_base64(sort_keys)
|
||||
createdAt,meta.createdAt,meta.created_at,iso8601_utc
|
||||
|
||||
|
@@ -0,0 +1,4 @@
|
||||
sbom_component,spine_node,graph_node,policy_input,evidence_ref
|
||||
pkg:demo/lib-a@1.2.3,spine://component/lib-a,graph://node/lib-a,policy://input/component/lib-a,hash:blake3:27c6de0c
|
||||
pkg:demo/lib-b@2.0.0,spine://component/lib-b,graph://node/lib-b,policy://input/component/lib-b,hash:blake3:da5b631a
|
||||
vuln:CVE-0000-0001,spine://vuln/CVE-0000-0001,graph://advisory/CVE-0000-0001,policy://input/vuln/CVE-0000-0001,hash:sha256:22d8f6f8
|
||||
|
||||
|
@@ -1 +1 @@
|
||||
crosswalk.csv: BLAKE3=<TBD> SHA256=<TBD>
|
||||
crosswalk.csv: BLAKE3=41926241c6d60bb856ceb4498e70381cdf54217435740f5fdf31ff8964044d78 SHA256=1e6644cdc00097b7e959e75f522335326b8f48fe1d05060d1c06ba660aac22a3
|
||||
|
||||
47
docs/modules/sbomservice/fixtures/lnm-v1/catalog.json
Normal file
47
docs/modules/sbomservice/fixtures/lnm-v1/catalog.json
Normal file
@@ -0,0 +1,47 @@
|
||||
[
|
||||
{
|
||||
"artifact": "ghcr.io/stellaops/sample-api",
|
||||
"sbomVersion": "2025.11.16.1",
|
||||
"digest": "sha256:112",
|
||||
"license": "MIT",
|
||||
"scope": "runtime",
|
||||
"assetTags": {
|
||||
"owner": "payments",
|
||||
"criticality": "high",
|
||||
"env": "prod"
|
||||
},
|
||||
"createdAt": "2025-11-16T12:00:00Z",
|
||||
"projectionHash": "sha256:proj112",
|
||||
"evaluationMetadata": "eval:passed:v1"
|
||||
},
|
||||
{
|
||||
"artifact": "ghcr.io/stellaops/sample-api",
|
||||
"sbomVersion": "2025.11.15.1",
|
||||
"digest": "sha256:111",
|
||||
"license": "MIT",
|
||||
"scope": "runtime",
|
||||
"assetTags": {
|
||||
"owner": "payments",
|
||||
"criticality": "high",
|
||||
"env": "prod"
|
||||
},
|
||||
"createdAt": "2025-11-15T12:00:00Z",
|
||||
"projectionHash": "sha256:proj111",
|
||||
"evaluationMetadata": "eval:passed:v1"
|
||||
},
|
||||
{
|
||||
"artifact": "ghcr.io/stellaops/sample-worker",
|
||||
"sbomVersion": "2025.11.12.0",
|
||||
"digest": "sha256:222",
|
||||
"license": "Apache-2.0",
|
||||
"scope": "runtime",
|
||||
"assetTags": {
|
||||
"owner": "platform",
|
||||
"criticality": "medium",
|
||||
"env": "staging"
|
||||
},
|
||||
"createdAt": "2025-11-12T08:00:00Z",
|
||||
"projectionHash": "sha256:proj222",
|
||||
"evaluationMetadata": "eval:pending:v1"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"artifact": "ghcr.io/stellaops/sample-api",
|
||||
"purl": "pkg:npm/lodash@4.17.21",
|
||||
"neighborPurl": "pkg:npm/express@4.18.2",
|
||||
"relationship": "DEPENDS_ON",
|
||||
"license": "MIT",
|
||||
"scope": "runtime",
|
||||
"runtimeFlag": true
|
||||
},
|
||||
{
|
||||
"artifact": "ghcr.io/stellaops/sample-api",
|
||||
"purl": "pkg:npm/lodash@4.17.21",
|
||||
"neighborPurl": "pkg:npm/rollup@3.0.0",
|
||||
"relationship": "DEPENDS_ON",
|
||||
"license": "MIT",
|
||||
"scope": "build",
|
||||
"runtimeFlag": false
|
||||
},
|
||||
{
|
||||
"artifact": "ghcr.io/stellaops/sample-api",
|
||||
"purl": "pkg:npm/lodash@4.17.21",
|
||||
"neighborPurl": "pkg:npm/react@18.2.0",
|
||||
"relationship": "DEPENDS_ON",
|
||||
"license": "MIT",
|
||||
"scope": "runtime",
|
||||
"runtimeFlag": true
|
||||
},
|
||||
{
|
||||
"artifact": "ghcr.io/stellaops/sample-worker",
|
||||
"purl": "pkg:nuget/Newtonsoft.Json@13.0.2",
|
||||
"neighborPurl": "pkg:nuget/StellaOps.Core@1.0.0",
|
||||
"relationship": "DEPENDS_ON",
|
||||
"license": "Apache-2.0",
|
||||
"scope": "runtime",
|
||||
"runtimeFlag": true
|
||||
}
|
||||
]
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
Scanner analyses container images layer-by-layer, producing deterministic SBOM fragments, diffs, and signed reports.
|
||||
|
||||
## Latest updates (2025-11-09)
|
||||
## Latest updates (2025-12-03)
|
||||
- Deterministic SBOM composition fixture published at `docs/modules/scanner/fixtures/deterministic-compose/` with DSSE, `_composition.json`, BOM, and hashes; doc `deterministic-sbom-compose.md` promoted to Ready v1.0 with offline verification steps.
|
||||
- Node analyzer now ingests npm/yarn/pnpm lockfiles, emitting `DeclaredOnly` components with lock provenance. The CLI companion command `stella node lock-validate` runs the collector offline, surfaces declared-only or missing-lock packages, and emits telemetry via `stellaops.cli.node.lock_validate.count`.
|
||||
- Python analyzer picks up `requirements*.txt`, `Pipfile.lock`, and `poetry.lock`, tagging installed distributions with lock provenance and generating declared-only components for policy. Use `stella python lock-validate` to run the same checks locally before images are built.
|
||||
- Java analyzer now parses `gradle.lockfile`, `gradle/dependency-locks/**/*.lockfile`, and `pom.xml` dependencies via the new `JavaLockFileCollector`, merging lock metadata onto jar evidence and emitting declared-only components when jars are absent. The new CLI verb `stella java lock-validate` reuses that collector offline (table/JSON output) and records `stellaops.cli.java.lock_validate.count{outcome}` for observability.
|
||||
|
||||
@@ -43,7 +43,10 @@ Define how external SBOM/scan outputs (Syft, Trivy, Clair) are normalized into S
|
||||
|
||||
## Regression + fixtures (CM4/CM5)
|
||||
- Fixtures under `docs/modules/scanner/fixtures/competitor-adapters/fixtures/` with golden hashes (BLAKE3/SHA256) and expected normalized output.
|
||||
- CI step runs adapter → normalized → hash compare; offline, no network.
|
||||
- `normalized-syft.json` BLAKE3=aa42c167d19535709a10df73dc39e6a50b8efbbb0ae596d17183ce62676fa85a SHA256=3f8684ff341808dcb92e97dd2c10acca727baaff05182e81a4364bb3dad0eaa7
|
||||
- `normalized-trivy.json` BLAKE3=0da216b49ebcf823d8d4aa3c9c1d2a1dcc579d836ba66bb2ae94dd781e214130 SHA256=c29aa6251d378c2aca1c3c6165e61bd2e16b6fa1227c976417b8a525ad7c1fc1
|
||||
- `normalized-clair.json` BLAKE3=92985f4cbdeecc8a0e585a70e07f17b07abdd866eecacaca9ba1b331f4b3af68 SHA256=bc232cc19885c53e4d801f5c830e3683a4031e42f6421739c4cc221f33f15e01
|
||||
- CI step runs adapter → normalized → hash compare; offline, no network. Hashes act as guardrails for deterministic ordering and mapping stability.
|
||||
|
||||
## Fallback hierarchy (CM6)
|
||||
1) Signed SBOM w/ valid provenance → accepted.
|
||||
@@ -53,7 +56,7 @@ Define how external SBOM/scan outputs (Syft, Trivy, Clair) are normalized into S
|
||||
|
||||
## Transparency & coverage (CM7–CM9)
|
||||
- Persist: `source.tool`, `source.version`, `source.hash`, `adapter.version`, `normalized_hash`.
|
||||
- Coverage matrix maintained in `docs/modules/scanner/fixtures/competitor-adapters/coverage.csv` (ecosystem yes/no, notes).
|
||||
- Coverage matrix maintained in `docs/modules/scanner/fixtures/competitor-adapters/coverage.csv` (ecosystem yes/no, notes). Current snapshot (2025-12-03): container/java/python/go/os rows populated; dotnet pending Syft/Clair support.
|
||||
- Bench parity (CM8): pin upstream versions; store run hashes/logs in fixtures folder.
|
||||
|
||||
## Error taxonomy (CM10)
|
||||
@@ -61,9 +64,13 @@ Define how external SBOM/scan outputs (Syft, Trivy, Clair) are normalized into S
|
||||
- Non-retryable: signature_invalid, schema_invalid, unsupported_version, no_evidence.
|
||||
- All errors must carry deterministic reason codes and be logged in normalized output metadata.
|
||||
|
||||
## Open Items
|
||||
- Decide minimal evidence set for accepting unsigned SBOMs (intermediate level before scan-only fallback).
|
||||
- Confirm which hash (BLAKE3/SHA256) is canonical for adapter outputs.
|
||||
## Offline kit (CM5)
|
||||
- Kit contents: adapter CSVs (one per tool), fixtures + hashes above, coverage matrix, trust roots, signature policy, retry taxonomy, and DSSE envelope referencing every file hash. Bundle path: `out/offline/competitor-ingest-kit-v1/`.
|
||||
|
||||
## Decisions (2025-12-03)
|
||||
- Minimal evidence for unsigned SBOM acceptance: must include tool metadata (name/version/hash), component list with purl + SHA256, and scan timestamp; otherwise fallback to scan-only path (CM6 step 3).
|
||||
- Canonical adapter output hash: BLAKE3 primary, SHA256 secondary; both recorded in fixture hash lists and surfaced in normalized metadata (`normalized_hash`).
|
||||
- Signature verification policy is strict fail-closed unless `--allow-unsigned` flag explicitly set; fallback hierarchy still applied but logged.
|
||||
|
||||
## Links
|
||||
- Sprint: `docs/implplan/SPRINT_0186_0001_0001_record_deterministic_execution.md` (CM1–CM10)
|
||||
|
||||
@@ -34,36 +34,41 @@ Define the concrete steps for adopting CVSS v4.0, CycloneDX 1.7 (incl. CBOM), an
|
||||
- timestamps UTC ISO-8601 without sub-ms; decimal rounding 4dp for ratios, 2dp for scores.
|
||||
- Adapter tables (SC4): mapping CSVs checked in under `docs/modules/scanner/fixtures/adapters/` with BLAKE3 + SHA256 hashes; adapters are pure, no net.
|
||||
|
||||
## Fixtures (SC5/SC8)
|
||||
- Add to `docs/modules/scanner/fixtures/cdx17-cbom/`:
|
||||
- `sample-cdx17-cbom.json` (golden), `sample-cdx16-downgraded.json`, `hashes.txt` (BLAKE3, SHA256).
|
||||
- Include CBOM ingress/egress example, CVSS v4 vector, SLSA Source Track fields, evidence properties.
|
||||
## Fixtures (SC2/SC5/SC8)
|
||||
- Golden payloads live in `docs/modules/scanner/fixtures/cdx17-cbom/`.
|
||||
- `sample-cdx17-cbom.json` (CDX 1.7 + CBOM + CVSS v4/v3.1 + SLSA Source Track + evidence).
|
||||
- `sample-cdx16.json` (downgraded CDX 1.6; CVSS v3.1 only; no CBOM channel properties).
|
||||
- `hashes.txt` records deterministic digests:
|
||||
- `sample-cdx17-cbom.json` BLAKE3=27c6de0ccd6adb8149c5521477fba8292aa119fb9e42b521cba6356b2308e761 SHA256=22d8f6f80f02be13f840b74b24b2eea769f108a225152695e1bf8d8a0577e6f6
|
||||
- `sample-cdx16.json` BLAKE3=da5b631a8cca865f929f8fd5d3b35adc512de1754fe2278cb8b415b01c81b3d3 SHA256=3cf6cb04aec97ec05fad0658f54b4ec099644176806f098897a9ba0bf1135cb0
|
||||
- CI step: `dotnet test` hook runs deterministic serializer + hash assertion; env `DOTNET_DISABLE_BUILTIN_GRAPH=1`, fixed `TZ=UTC`, `LC_ALL=C`.
|
||||
- Downgrade adapters (SC4) consume the CDX 1.7 fixture and emit the 1.6 fixture; verify hashes match the values above.
|
||||
|
||||
## Governance (SC1/SC9)
|
||||
- Propose RACI: Product (A), Scanner TL (R), Sbomer TL (C), Policy TL (C), Ops (I).
|
||||
- RACI: Product (A), Scanner TL (R), Sbomer TL (C), Policy TL (C), Ops (I).
|
||||
- Schema bump flow: draft → review → freeze → DSSE-sign schemas + fixtures → publish hash list → lock downgrade adapters.
|
||||
- Downgrade adapters cannot ship without approved mapping CSV + updated hashes.
|
||||
- Downgrade adapters cannot ship without approved mapping CSV + updated hashes; adapter CSVs live under `docs/modules/scanner/fixtures/adapters/` (hash list alongside CSVs).
|
||||
|
||||
## Offline (SC10)
|
||||
- Offline kit must include: schemas, adapter CSVs, fixtures, hash list, DSSE envelope, tool versions (Syft/Trivy pinned) and their hashes.
|
||||
- Bundle path: `out/offline/scanner-standards-kit-v1/`. DSSE envelope references manifest with all hashes.
|
||||
- Bundle path: `out/offline/scanner-standards-kit-v1/`. DSSE envelope references manifest with all hashes; include CBOM sample, downgrade sample, adapter CSVs, and their BLAKE3/SHA256 values.
|
||||
|
||||
## Milestones (proposed)
|
||||
1) Schema draft freeze (CDX 1.7/CBOM + CVSS v4 fields) — owners: Scanner Guild, due T+5d.
|
||||
2) Replay bundle field list for Source Track — owners: Scanner + Sbomer, due T+7d.
|
||||
3) Determinism harness upgrade (CI + fixtures) — owners: QA + Scanner, due T+10d.
|
||||
4) Downgrade adapter tables + hash tests — owners: Scanner, due T+12d.
|
||||
5) Offline-kit bundle update & DSSE signing — owners: Ops, due T+14d.
|
||||
## Milestones (locked for SC1 delivery)
|
||||
1) Schema draft freeze (CDX 1.7/CBOM + CVSS v4 fields) — owners: Scanner Guild, due 2025-12-08.
|
||||
2) Replay bundle field list for Source Track — owners: Scanner + Sbomer, due 2025-12-10.
|
||||
3) Determinism harness upgrade (CI + fixtures) — owners: QA + Scanner, due 2025-12-13.
|
||||
4) Downgrade adapter tables + hash tests — owners: Scanner, due 2025-12-15.
|
||||
5) Offline-kit bundle update & DSSE signing — owners: Ops, due 2025-12-17.
|
||||
|
||||
## Determinism & Offline requirements
|
||||
- Stable field ordering, culture-invariant formatting, UTC ISO-8601 timestamps.
|
||||
- No network calls during conversion/adapters; fixed seeds for any RNG.
|
||||
- All schemas/adapters/fixtures shipped in offline kit with DSSE envelope and recorded hashes.
|
||||
|
||||
## Open Items
|
||||
- Confirm CBOM section subset required for policy engine (ingredients vs evidence-only).
|
||||
- Decide default CVSS v4 vector precision and rounding rules.
|
||||
## Decisions (2025-12-03)
|
||||
- CBOM subset: include ingress + egress channel properties only; deeper data-flow capture deferred to policy/graph once schema stabilises.
|
||||
- CVSS v4 rounding: keep vendor vector precision; round scores to 2dp using `MidpointRounding.ToZero` for deterministic alignment with CVSS v3.1 sidecar values.
|
||||
- Evidence properties are mandatory for replay bundles and serialized CycloneDX 1.7 outputs; adapter must preserve them when downgrading.
|
||||
|
||||
## Links
|
||||
- Sprint: `docs/implplan/SPRINT_0186_0001_0001_record_deterministic_execution.md` (tasks SC1–SC10)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Deterministic SBOM Composition (Spec Draft)
|
||||
# Deterministic SBOM Composition
|
||||
|
||||
> **Status:** Draft v0.1 (Sprint 136 / 203 / 209 linkage)
|
||||
> **Status:** Ready v1.0 (Sprint 136 linkage; fixtures dated 2025-12-03)
|
||||
> **Owners:** Scanner Guild · DevEx/CLI Guild · UI Guild · Docs Guild · Security Guild
|
||||
> **Related Tasks:** `SCANNER-SURFACE-04`, `SURFACE-FS-07`, `SCANNER-EMIT-15-001`, `SCANNER-SORT-02`, `CLI-SBOM-60-001`, `CLI-SBOM-60-002`, `UI-SBOM-DET-01`, `UI-POLICY-DET-01`, `DOCS-SCANNER-DET-01`, `DOCS-POLICY-DET-01`, `DOCS-CLI-DET-01`, `SCANNER-CRYPTO-90-002`, `SCANNER-CRYPTO-90-003`
|
||||
|
||||
@@ -46,6 +46,19 @@ Guarantee that every container scan yields **provably deterministic** SBOM artif
|
||||
3. Re-run composition locally (using canonical ordering) and compare `sha256(c14n(composed))` against `manifest.properties["stellaops:merkle.root"]`.
|
||||
4. Optionally validate provided Merkle proofs (leaf → root) and attest that the UI/Policy gate marked the scan as deterministic.
|
||||
|
||||
### 3.1 Reference fixture (deterministic-compose)
|
||||
- Path: `docs/modules/scanner/fixtures/deterministic-compose/` (generated 2025-12-03 by `generate.py`).
|
||||
- Quick verify:
|
||||
```bash
|
||||
cd docs/modules/scanner/fixtures/deterministic-compose
|
||||
sha256sum -c hashes.txt
|
||||
jq -r '.payload' fragment-layer1.dsse.json | base64 -d | sha256sum
|
||||
jq -r '.merkleRootSha256' _composition.json
|
||||
jq -r '.properties[] | select(.name=="stellaops:merkle.root").value' bom.cdx.json
|
||||
```
|
||||
Expected Merkle root: `963e421d21be2db87895ea5fd973a0ad71aa638499c274308e013d2b6c8243f6` (matches `_composition.json` and `bom.cdx.json`).
|
||||
- Regenerate deterministically: `python generate.py && sha256sum -c hashes.txt` (standard library only).
|
||||
|
||||
## 4. Deliverables Checklist
|
||||
|
||||
| Area | Deliverable |
|
||||
@@ -57,7 +70,13 @@ Guarantee that every container scan yields **provably deterministic** SBOM artif
|
||||
| Docs | Updated scanner/cli/policy guides, offline kit instructions |
|
||||
| Tests | Regression suites covering canonicalization, DSSE verification, PQ keypaths, Merkle roots |
|
||||
|
||||
## 5. References
|
||||
## 5. Operational workflow (worker → CLI/UI/Policy)
|
||||
- **Worker**: emit fragment DSSE + `_composition.json` into the surface manifest; persist `stellaops:composition.manifest` and `stellaops:merkle.root` properties on composed BOMs so downstream consumers do not recompute merges.
|
||||
- **CLI**: verify bundles offline with `stella sbomer compose --recipe docs/modules/scanner/fixtures/deterministic-compose/_composition.json --fragments-dir docs/modules/scanner/fixtures/deterministic-compose --verify` (see `docs/cli/sbomer.md`). The command should fail if any DSSE signature, Merkle root, or BOM hash diverges.
|
||||
- **UI / Policy**: render determinism badge using `stellaops:merkle.root`; block promotion when `_composition.json` is missing or hashes disagree; expose drift diagnostics by recomputing composition locally and comparing to BOM properties.
|
||||
- **Export/Offline**: include `_composition.json`, fragment DSSEs, `bom.cdx.json`, and `hashes.txt` when building Offline Kit bundles so replay jobs can validate without network.
|
||||
|
||||
## 6. References
|
||||
|
||||
- `docs/modules/scanner/architecture.md`
|
||||
- `docs/modules/scanner/design/surface-fs.md`
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# placeholder; compute BLAKE3 and SHA256 after schemas stabilize
|
||||
sample-cdx17-cbom.json: BLAKE3=<TBD> SHA256=<TBD>
|
||||
sample-cdx16.json: BLAKE3=<TBD> SHA256=<TBD>
|
||||
# Deterministic hashes for CDX 1.7 CBOM fixture and downgraded 1.6 variant
|
||||
sample-cdx17-cbom.json: BLAKE3=27c6de0ccd6adb8149c5521477fba8292aa119fb9e42b521cba6356b2308e761 SHA256=22d8f6f80f02be13f840b74b24b2eea769f108a225152695e1bf8d8a0577e6f6
|
||||
sample-cdx16.json: BLAKE3=da5b631a8cca865f929f8fd5d3b35adc512de1754fe2278cb8b415b01c81b3d3 SHA256=3cf6cb04aec97ec05fad0658f54b4ec099644176806f098897a9ba0bf1135cb0
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"serialNumber": "urn:uuid:00000000-0000-4000-8000-000000000001",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2025-01-01T00:00:00Z",
|
||||
"component": {
|
||||
"type": "application",
|
||||
"name": "demo-app",
|
||||
"version": "1.0.0",
|
||||
"purl": "pkg:demo/demo-app@1.0.0",
|
||||
"hashes": [
|
||||
{ "alg": "SHA-256", "content": "1111111111111111111111111111111111111111111111111111111111111111" }
|
||||
]
|
||||
},
|
||||
"tools": [
|
||||
{ "vendor": "stellaops", "name": "scanner", "version": "0.0.0-fixture" }
|
||||
]
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"type": "library",
|
||||
"name": "lib-a",
|
||||
"version": "1.2.3",
|
||||
"purl": "pkg:demo/lib-a@1.2.3"
|
||||
},
|
||||
{
|
||||
"type": "library",
|
||||
"name": "lib-b",
|
||||
"version": "2.0.0",
|
||||
"purl": "pkg:demo/lib-b@2.0.0"
|
||||
}
|
||||
],
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"id": "CVE-0000-0001",
|
||||
"source": { "name": "NVD" },
|
||||
"ratings": [
|
||||
{
|
||||
"source": { "name": "NVD" },
|
||||
"method": "CVSSv3.1",
|
||||
"score": 7.5,
|
||||
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,10 +10,26 @@
|
||||
"name": "demo-app",
|
||||
"version": "1.0.0",
|
||||
"purl": "pkg:demo/demo-app@1.0.0",
|
||||
"hashes": [ { "alg": "SHA-256", "content": "d" } ],
|
||||
"evidence": { "properties": [ { "name": "evidence:source", "value": "fixture" } ] }
|
||||
"hashes": [
|
||||
{ "alg": "SHA-256", "content": "1111111111111111111111111111111111111111111111111111111111111111" }
|
||||
],
|
||||
"evidence": {
|
||||
"properties": [
|
||||
{ "name": "evidence:source", "value": "fixture" },
|
||||
{ "name": "evidence:hash", "value": "blake3:fixture-demo-app" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"tools": [ { "vendor": "stellaops", "name": "scanner", "version": "0.0.0-fixture" } ]
|
||||
"properties": [
|
||||
{ "name": "source.repo", "value": "https://example.invalid/demo" },
|
||||
{ "name": "source.ref", "value": "refs/tags/v1.0.0" },
|
||||
{ "name": "build.id", "value": "build-123" },
|
||||
{ "name": "build.invocation.hash", "value": "blake3:deadbeef" },
|
||||
{ "name": "provenance.dsse", "value": "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
|
||||
],
|
||||
"tools": [
|
||||
{ "vendor": "stellaops", "name": "scanner", "version": "0.0.0-fixture" }
|
||||
]
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
@@ -25,16 +41,43 @@
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{ "type": "library", "name": "lib-a", "version": "1.2.3", "purl": "pkg:demo/lib-a@1.2.3" },
|
||||
{ "type": "library", "name": "lib-b", "version": "2.0.0", "purl": "pkg:demo/lib-b@2.0.0" }
|
||||
{
|
||||
"type": "library",
|
||||
"name": "lib-a",
|
||||
"version": "1.2.3",
|
||||
"purl": "pkg:demo/lib-a@1.2.3",
|
||||
"hashes": [ { "alg": "SHA-256", "content": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } ]
|
||||
},
|
||||
{
|
||||
"type": "library",
|
||||
"name": "lib-b",
|
||||
"version": "2.0.0",
|
||||
"purl": "pkg:demo/lib-b@2.0.0",
|
||||
"hashes": [ { "alg": "SHA-256", "content": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ]
|
||||
}
|
||||
],
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"id": "CVE-0000-0001",
|
||||
"source": { "name": "NVD" },
|
||||
"ratings": [
|
||||
{ "source": { "name": "NVD" }, "method": "CVSSv4", "score": 8.0, "vector": "CVSS:4.0/AV:N/AC:L" },
|
||||
{ "source": { "name": "NVD" }, "method": "CVSSv3.1", "score": 7.5, "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" }
|
||||
{
|
||||
"source": { "name": "NVD" },
|
||||
"method": "CVSSv4",
|
||||
"score": 8.0,
|
||||
"vector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H"
|
||||
},
|
||||
{
|
||||
"source": { "name": "NVD" },
|
||||
"method": "CVSSv3.1",
|
||||
"score": 7.5,
|
||||
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
|
||||
}
|
||||
],
|
||||
"properties": [
|
||||
{ "name": "evidence:source", "value": "fixture" },
|
||||
{ "name": "evidence:proof-id", "value": "proof-123" },
|
||||
{ "name": "evidence:hash", "value": "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
ecosystem,syft,trivy,clair,notes
|
||||
container,yes,yes,yes,pinned demo fixtures
|
||||
java,yes,yes,no,clair sample pending
|
||||
python,yes,yes,no,
|
||||
dotnet,no,yes,no,trivy provides sample; others pending
|
||||
go,yes,yes,no,
|
||||
os-pkgs,yes,yes,yes,clair focuses on apk baseline
|
||||
|
||||
|
@@ -1 +1,4 @@
|
||||
# Golden outputs for Syft/Trivy/Clair fixtures; fill after adapter code lands
|
||||
# Deterministic hashes for normalized competitor ingest fixtures (BLAKE3, SHA256)
|
||||
normalized-syft.json: BLAKE3=aa42c167d19535709a10df73dc39e6a50b8efbbb0ae596d17183ce62676fa85a SHA256=3f8684ff341808dcb92e97dd2c10acca727baaff05182e81a4364bb3dad0eaa7
|
||||
normalized-trivy.json: BLAKE3=0da216b49ebcf823d8d4aa3c9c1d2a1dcc579d836ba66bb2ae94dd781e214130 SHA256=c29aa6251d378c2aca1c3c6165e61bd2e16b6fa1227c976417b8a525ad7c1fc1
|
||||
normalized-clair.json: BLAKE3=92985f4cbdeecc8a0e585a70e07f17b07abdd866eecacaca9ba1b331f4b3af68 SHA256=bc232cc19885c53e4d801f5c830e3683a4031e42f6421739c4cc221f33f15e01
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"source": { "tool": "clair", "version": "6.0.0", "hash": "sha256:clair-fixture" },
|
||||
"components": [
|
||||
{
|
||||
"name": "demo-os",
|
||||
"version": "3.1-1",
|
||||
"purl": "pkg:apk/demo-os@3.1-1",
|
||||
"type": "os",
|
||||
"hashes": ["sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"],
|
||||
"licenses": ["BSD-3-Clause"],
|
||||
"evidenceRef": "evidence-003"
|
||||
}
|
||||
],
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"id": "CVE-0000-0003",
|
||||
"source": "alpine",
|
||||
"severity": "Low",
|
||||
"cvss": { "score": 3.1, "vector": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N" },
|
||||
"fixVersions": ["3.1-2"],
|
||||
"evidenceRef": "evidence-003"
|
||||
}
|
||||
],
|
||||
"normalized_hash": "blake3:993e43cfe0f22667b5243d4a91d18f53e3efe4f84ee97fc33531629c9ff36418"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"source": { "tool": "syft", "version": "1.0.0", "hash": "sha256:syft-fixture" },
|
||||
"components": [
|
||||
{
|
||||
"name": "demo-app",
|
||||
"version": "1.0.0",
|
||||
"purl": "pkg:docker/demo-app@1.0.0",
|
||||
"type": "container",
|
||||
"hashes": ["sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
|
||||
"licenses": ["MIT"],
|
||||
"evidenceRef": "evidence-001"
|
||||
}
|
||||
],
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"id": "CVE-0000-0002",
|
||||
"source": "nvd",
|
||||
"severity": "High",
|
||||
"cvss": { "score": 7.5, "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" },
|
||||
"fixVersions": ["1.0.1"],
|
||||
"evidenceRef": "evidence-001"
|
||||
}
|
||||
],
|
||||
"normalized_hash": "blake3:aea5398089eec122fd594027e9b1322b19d87b057bdb2d0925a89cf2c945b980"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"source": { "tool": "trivy", "version": "0.50.0", "hash": "sha256:trivy-fixture" },
|
||||
"components": [
|
||||
{
|
||||
"name": "demo-lib",
|
||||
"version": "2.0.0",
|
||||
"purl": "pkg:npm/demo-lib@2.0.0",
|
||||
"type": "library",
|
||||
"hashes": ["sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"],
|
||||
"licenses": ["Apache-2.0"],
|
||||
"evidenceRef": "evidence-002"
|
||||
}
|
||||
],
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"id": "GHSA-xxxx-yyyy-zzzz",
|
||||
"source": "ghsa",
|
||||
"severity": "Medium",
|
||||
"cvss": { "score": 5.0, "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N" },
|
||||
"fixVersions": ["2.0.1"],
|
||||
"evidenceRef": "evidence-002"
|
||||
}
|
||||
],
|
||||
"normalized_hash": "blake3:0db368a22f8fe008f03ee8256a5fafe4b16bf8c087ae1c273c6d0090b5cadca7"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# Deterministic SBOM composition fixtures
|
||||
|
||||
Reference bundle for DOCS-SCANNER-DET-01. Use it to prove fragment-level DSSE, `_composition.json`, and CycloneDX composition metadata stay deterministic and offline-verifiable.
|
||||
|
||||
## Contents
|
||||
- `_composition.json` — composition recipe with Merkle root, fragment hashes, BOM hash, and determinism pins.
|
||||
- `fragment-layer{1,2}.json` — canonical fragments (sorted keys, newline-terminated).
|
||||
- `fragment-layer{1,2}.dsse.json` — DSSE envelopes over the canonical fragments (demo key `demo-ed25519`).
|
||||
- `bom.cdx.json` — composed CycloneDX BOM with `stellaops:merkle.root` and `stellaops:composition.manifest` properties.
|
||||
- `hashes.txt` — sha256 for every file in this directory.
|
||||
- `generate.py` — reproducible generator (standard library only).
|
||||
|
||||
## Verify offline
|
||||
```bash
|
||||
cd docs/modules/scanner/fixtures/deterministic-compose
|
||||
sha256sum -c hashes.txt
|
||||
|
||||
# Check DSSE payload matches fragment
|
||||
jq -r '.payload' fragment-layer1.dsse.json | base64 -d > /tmp/payload.json
|
||||
diff -u fragment-layer1.json /tmp/payload.json
|
||||
|
||||
# Recompute Merkle root from fragment hashes
|
||||
python - <<'PY'
|
||||
import hashlib, json
|
||||
from pathlib import Path
|
||||
frag_hashes = [line.split()[0] for line in Path('hashes.txt').read_text().splitlines() if 'fragment-layer' in line and '.json' in line and '.dsse' not in line]
|
||||
frag_hashes = [bytes.fromhex(h) for h in frag_hashes]
|
||||
while len(frag_hashes) > 1:
|
||||
nxt = []
|
||||
it = iter(frag_hashes)
|
||||
for a in it:
|
||||
b = next(it, a)
|
||||
nxt.append(hashlib.sha256(a+b).digest())
|
||||
frag_hashes = nxt
|
||||
print(f"merkle={frag_hashes[0].hex()}")
|
||||
PY
|
||||
```
|
||||
|
||||
## Regenerate
|
||||
```bash
|
||||
python generate.py
|
||||
sha256sum -c hashes.txt
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
{"composedBomSha256":"c161ac9cfee5f3baee69d303a0fb70bfb036d863e317e6e0d5843b983a6c8466","determinism":{"feedSnapshotId":"feeds-2025.320.1","fixedClock":"2025-12-01T00:00:00Z","policySnapshotId":"policy-2025.310.0","rngSeed":1337},"fragments":[{"dsseEnvelopeSha256":"ff008ab332bbcc6ac413739eb66529c3fcb1ca2d2503f8263bf5e0645d930118","dssePath":"fragment-layer1.dsse.json","fragmentPath":"fragment-layer1.json","fragmentSha256":"7884ea6f3a46a0870d8fc74a5e770bac49a9729a83175dbcf42ca14769b22fa0","layerDigest":"sha256:1111111111111111111111111111111111111111111111111111111111111101"},{"dsseEnvelopeSha256":"8813b84f072196808e644e6a8c54a81348b566054149b26a0055d8e63e0ae6aa","dssePath":"fragment-layer2.dsse.json","fragmentPath":"fragment-layer2.json","fragmentSha256":"cb9783249cf18e8d8a227d288864d821c190005897a14212f21742c0f404208f","layerDigest":"sha256:2222222222222222222222222222222222222222222222222222222222222202"}],"generatedAtUtc":"2025-12-03T00:00:00Z","imageDigest":"sha256:9999999999999999999999999999999999999999999999999999999999999900","merkleRootSha256":"963e421d21be2db87895ea5fd973a0ad71aa638499c274308e013d2b6c8243f6","schemaVersion":"1.0"}
|
||||
@@ -0,0 +1 @@
|
||||
{"bomFormat":"CycloneDX","components":[{"bom-ref":"pkg:apk/alpine/busybox@1.36.1","name":"busybox","properties":[{"name":"stellaops:stella.contentHash","value":"7884ea6f3a46a0870d8fc74a5e770bac49a9729a83175dbcf42ca14769b22fa0"}],"purl":"pkg:apk/alpine/busybox@1.36.1","type":"library","version":"1.36.1"},{"bom-ref":"pkg:npm/express@4.18.2","name":"express","properties":[{"name":"stellaops:stella.contentHash","value":"cb9783249cf18e8d8a227d288864d821c190005897a14212f21742c0f404208f"}],"purl":"pkg:npm/express@4.18.2","type":"library","version":"4.18.2"}],"metadata":{"component":{"bom-ref":"pkg:docker/registry.local/demo@sha256:9999999999999999999999999999999999999999999999999999999999999900","name":"registry.local/demo","purl":"pkg:docker/registry.local/demo@sha256:9999999999999999999999999999999999999999999999999999999999999900","type":"container"},"timestamp":"2025-12-03T00:00:00Z"},"properties":[{"name":"stellaops:merkle.root","value":"963e421d21be2db87895ea5fd973a0ad71aa638499c274308e013d2b6c8243f6"},{"name":"stellaops:composition.manifest","value":"cas://scanner/deterministic-compose/_composition.json"},{"name":"stellaops:stella.contentHash","value":"963e421d21be2db87895ea5fd973a0ad71aa638499c274308e013d2b6c8243f6"}],"serialNumber":"urn:uuid:00000000-7e57-4c0d-baad-000000000301","specVersion":"1.6","version":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"payload":"eyJjb21wb25lbnRzIjpbeyJldmlkZW5jZSI6eyJjb250ZW50SGFzaCI6InNoYTI1Njo3YzNmNGQzMGJmY2Q4ZmYyYjA5ZjFiYTM5ZjQzYzUyNGQ2Y2UxYjdhNWYzYzJiZGUzMjFlMGY1ZTBlNmMzZDEwIiwibGljZW5zZXMiOlsiQlNELTMtQ2xhdXNlIl0sInBhdGhzIjpbIi9iaW4vYnVzeWJveCJdfSwiaWRlbnRpdHkiOnsicHVybCI6InBrZzphcGsvYWxwaW5lL2J1c3lib3hAMS4zNi4xIn0sInNvdXJjZSI6ImFwayJ9XSwiZ2VuZXJhdGVkQXRVdGMiOiIyMDI1LTEyLTAxVDAwOjAwOjAwWiIsImxheWVyRGlnZXN0Ijoic2hhMjU2OjExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMDEiLCJzY2hlbWFWZXJzaW9uIjoiMS4wIn0=","payloadType":"application/vnd.stellaops.scanner.fragment+json","signatures":[{"keyid":"demo-ed25519","sig":"ZGV0ZXJtaW5pc3RpYy1maXh0dXJlLWZyYWdtZW50LTE="}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"components":[{"evidence":{"contentHash":"sha256:7c3f4d30bfcd8ff2b09f1ba39f43c524d6ce1b7a5f3c2bde321e0f5e0e6c3d10","licenses":["BSD-3-Clause"],"paths":["/bin/busybox"]},"identity":{"purl":"pkg:apk/alpine/busybox@1.36.1"},"source":"apk"}],"generatedAtUtc":"2025-12-01T00:00:00Z","layerDigest":"sha256:1111111111111111111111111111111111111111111111111111111111111101","schemaVersion":"1.0"}
|
||||
@@ -0,0 +1 @@
|
||||
{"payload":"eyJjb21wb25lbnRzIjpbeyJldmlkZW5jZSI6eyJjb250ZW50SGFzaCI6InNoYTI1Njo4YWIxMDNmZWQ1OGU3ZGMwYjE4MTliNzM1ODEyNmQxYzQ0Y2M5NzlmNDA5Nzc1ODg4Yjg1OTUwNGE4MjkxNDhiIiwibGljZW5zZXMiOlsiTUlUIl0sInBhdGhzIjpbIi93b3Jrc3BhY2Uvbm9kZV9tb2R1bGVzL2V4cHJlc3MiXX0sImlkZW50aXR5Ijp7InB1cmwiOiJwa2c6bnBtL2V4cHJlc3NANC4xOC4yIn0sInNvdXJjZSI6Im5wbSJ9XSwiZ2VuZXJhdGVkQXRVdGMiOiIyMDI1LTEyLTAxVDAwOjAwOjAwWiIsImxheWVyRGlnZXN0Ijoic2hhMjU2OjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMDIiLCJzY2hlbWFWZXJzaW9uIjoiMS4wIn0=","payloadType":"application/vnd.stellaops.scanner.fragment+json","signatures":[{"keyid":"demo-ed25519","sig":"ZGV0ZXJtaW5pc3RpYy1maXh0dXJlLWZyYWdtZW50LTI="}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"components":[{"evidence":{"contentHash":"sha256:8ab103fed58e7dc0b1819b7358126d1c44cc979f409775888b859504a829148b","licenses":["MIT"],"paths":["/workspace/node_modules/express"]},"identity":{"purl":"pkg:npm/express@4.18.2"},"source":"npm"}],"generatedAtUtc":"2025-12-01T00:00:00Z","layerDigest":"sha256:2222222222222222222222222222222222222222222222222222222222222202","schemaVersion":"1.0"}
|
||||
190
docs/modules/scanner/fixtures/deterministic-compose/generate.py
Normal file
190
docs/modules/scanner/fixtures/deterministic-compose/generate.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate deterministic SBOM composition fixtures.
|
||||
|
||||
Outputs fragment JSON, DSSE envelopes, a composition manifest, a composed
|
||||
CycloneDX BOM, and a hashes file suitable for offline verification.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).parent
|
||||
|
||||
|
||||
def canonical(obj) -> str:
|
||||
return json.dumps(obj, separators=(",", ":"), sort_keys=True)
|
||||
|
||||
|
||||
def write_json(path: Path, obj) -> str:
|
||||
text = canonical(obj) + "\n"
|
||||
path.write_text(text)
|
||||
return hashlib.sha256(text.encode()).hexdigest()
|
||||
|
||||
|
||||
def merkle_root(hex_hashes: list[str]) -> str:
|
||||
if not hex_hashes:
|
||||
return ""
|
||||
nodes = [bytes.fromhex(h) for h in hex_hashes]
|
||||
while len(nodes) > 1:
|
||||
nxt = []
|
||||
it = iter(nodes)
|
||||
for a in it:
|
||||
b = next(it, a)
|
||||
nxt.append(hashlib.sha256(a + b).digest())
|
||||
nodes = nxt
|
||||
return nodes[0].hex()
|
||||
|
||||
|
||||
def dsse_envelope(payload_json: str, label: str) -> dict:
|
||||
payload_b64 = base64.b64encode(payload_json.encode()).decode()
|
||||
signature = base64.b64encode(f"deterministic-fixture-{label}".encode()).decode()
|
||||
return {
|
||||
"payloadType": "application/vnd.stellaops.scanner.fragment+json",
|
||||
"payload": payload_b64,
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "demo-ed25519",
|
||||
"sig": signature,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ROOT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
fragments_src = [
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"layerDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111101",
|
||||
"generatedAtUtc": "2025-12-01T00:00:00Z",
|
||||
"components": [
|
||||
{
|
||||
"identity": {"purl": "pkg:apk/alpine/busybox@1.36.1"},
|
||||
"evidence": {
|
||||
"paths": ["/bin/busybox"],
|
||||
"licenses": ["BSD-3-Clause"],
|
||||
"contentHash": "sha256:7c3f4d30bfcd8ff2b09f1ba39f43c524d6ce1b7a5f3c2bde321e0f5e0e6c3d10",
|
||||
},
|
||||
"source": "apk",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"layerDigest": "sha256:2222222222222222222222222222222222222222222222222222222222222202",
|
||||
"generatedAtUtc": "2025-12-01T00:00:00Z",
|
||||
"components": [
|
||||
{
|
||||
"identity": {"purl": "pkg:npm/express@4.18.2"},
|
||||
"evidence": {
|
||||
"paths": ["/workspace/node_modules/express"],
|
||||
"licenses": ["MIT"],
|
||||
"contentHash": "sha256:8ab103fed58e7dc0b1819b7358126d1c44cc979f409775888b859504a829148b",
|
||||
},
|
||||
"source": "npm",
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
fragments_meta = []
|
||||
for idx, fragment in enumerate(fragments_src, start=1):
|
||||
fragment_path = ROOT / f"fragment-layer{idx}.json"
|
||||
fragment_hash = write_json(fragment_path, fragment)
|
||||
|
||||
envelope_obj = dsse_envelope(canonical(fragment), f"fragment-{idx}")
|
||||
envelope_path = ROOT / f"fragment-layer{idx}.dsse.json"
|
||||
envelope_hash = write_json(envelope_path, envelope_obj)
|
||||
|
||||
fragments_meta.append(
|
||||
{
|
||||
"layerDigest": fragment["layerDigest"],
|
||||
"fragmentPath": fragment_path.name,
|
||||
"dssePath": envelope_path.name,
|
||||
"fragmentSha256": fragment_hash,
|
||||
"dsseEnvelopeSha256": envelope_hash,
|
||||
}
|
||||
)
|
||||
|
||||
fragments_meta.sort(key=lambda f: f["layerDigest"])
|
||||
merkle = merkle_root([f["fragmentSha256"] for f in fragments_meta])
|
||||
|
||||
bom = {
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"serialNumber": "urn:uuid:00000000-7e57-4c0d-baad-000000000301",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2025-12-03T00:00:00Z",
|
||||
"component": {
|
||||
"type": "container",
|
||||
"bom-ref": "pkg:docker/registry.local/demo@sha256:9999999999999999999999999999999999999999999999999999999999999900",
|
||||
"name": "registry.local/demo",
|
||||
"purl": "pkg:docker/registry.local/demo@sha256:9999999999999999999999999999999999999999999999999999999999999900",
|
||||
},
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"bom-ref": "pkg:apk/alpine/busybox@1.36.1",
|
||||
"type": "library",
|
||||
"name": "busybox",
|
||||
"version": "1.36.1",
|
||||
"purl": "pkg:apk/alpine/busybox@1.36.1",
|
||||
"properties": [
|
||||
{"name": "stellaops:stella.contentHash", "value": fragments_meta[0]["fragmentSha256"]}
|
||||
],
|
||||
},
|
||||
{
|
||||
"bom-ref": "pkg:npm/express@4.18.2",
|
||||
"type": "library",
|
||||
"name": "express",
|
||||
"version": "4.18.2",
|
||||
"purl": "pkg:npm/express@4.18.2",
|
||||
"properties": [
|
||||
{"name": "stellaops:stella.contentHash", "value": fragments_meta[1]["fragmentSha256"]}
|
||||
],
|
||||
},
|
||||
],
|
||||
"properties": [
|
||||
{"name": "stellaops:merkle.root", "value": merkle},
|
||||
{"name": "stellaops:composition.manifest", "value": "cas://scanner/deterministic-compose/_composition.json"},
|
||||
{"name": "stellaops:stella.contentHash", "value": merkle},
|
||||
],
|
||||
}
|
||||
bom_path = ROOT / "bom.cdx.json"
|
||||
bom_hash = write_json(bom_path, bom)
|
||||
|
||||
composition = {
|
||||
"schemaVersion": "1.0",
|
||||
"imageDigest": "sha256:9999999999999999999999999999999999999999999999999999999999999900",
|
||||
"generatedAtUtc": "2025-12-03T00:00:00Z",
|
||||
"fragments": fragments_meta,
|
||||
"merkleRootSha256": merkle,
|
||||
"composedBomSha256": bom_hash,
|
||||
"determinism": {
|
||||
"fixedClock": "2025-12-01T00:00:00Z",
|
||||
"rngSeed": 1337,
|
||||
"feedSnapshotId": "feeds-2025.320.1",
|
||||
"policySnapshotId": "policy-2025.310.0",
|
||||
},
|
||||
}
|
||||
composition_path = ROOT / "_composition.json"
|
||||
composition_hash = write_json(composition_path, composition)
|
||||
|
||||
hashes = {
|
||||
"_composition.json": composition_hash,
|
||||
"bom.cdx.json": bom_hash,
|
||||
}
|
||||
for meta in fragments_meta:
|
||||
hashes[meta["fragmentPath"]] = meta["fragmentSha256"]
|
||||
hashes[meta["dssePath"]] = meta["dsseEnvelopeSha256"]
|
||||
|
||||
hashes_lines = [f"{hashes[name]} {name}" for name in sorted(hashes.keys())]
|
||||
(ROOT / "hashes.txt").write_text("\n".join(hashes_lines) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,6 @@
|
||||
c5c2e7195eb6f1624534966624655734fe595666f43a6b3dd168d60b5b33d5b0 _composition.json
|
||||
c161ac9cfee5f3baee69d303a0fb70bfb036d863e317e6e0d5843b983a6c8466 bom.cdx.json
|
||||
ff008ab332bbcc6ac413739eb66529c3fcb1ca2d2503f8263bf5e0645d930118 fragment-layer1.dsse.json
|
||||
7884ea6f3a46a0870d8fc74a5e770bac49a9729a83175dbcf42ca14769b22fa0 fragment-layer1.json
|
||||
8813b84f072196808e644e6a8c54a81348b566054149b26a0055d8e63e0ae6aa fragment-layer2.dsse.json
|
||||
cb9783249cf18e8d8a227d288864d821c190005897a14212f21742c0f404208f fragment-layer2.json
|
||||
@@ -41,7 +41,7 @@
|
||||
- `map` – expands items into child steps (`stepId[index]::templateId`).
|
||||
- `gate.approval` – human approval checkpoint; enforces TTL/required count; pauses run until satisfied or expired.
|
||||
- `gate.policy` – Policy Engine evaluation; `failAction` decides halt vs. continue.
|
||||
- Built-in helper: `bundle.ingest` (run step) — validates optional `checksum`/`checksumSha256`, stages bundles to `ArtifactsPath/bundles/<file>` deterministically; fails on missing file or checksum mismatch.
|
||||
- Built-in helper: `bundle.ingest` (run step) — requires `checksum`/`checksumSha256`, validates SHA-256, stages bundles to `ArtifactsPath/bundles/{checksum}/{filename}` deterministically, and emits `metadata.json`; fails on missing file or checksum mismatch.
|
||||
|
||||
## 7. Determinism, Air-Gap, and Security
|
||||
- Plan hash binding: runtime graph must equal planned graph; mismatch aborts run.
|
||||
|
||||
Reference in New Issue
Block a user