From aaad8104cb17d6c44d54763b9a727a1e21312f20 Mon Sep 17 00:00:00 2001 From: master <> Date: Wed, 4 Mar 2026 03:09:23 +0200 Subject: [PATCH] chore(sprints): archive 20260226 advisories and expand deterministic tests --- .../ARCHIVE_LOG.md | 43 +++ ...of_chain_verification_and_replay_parity.md | 130 +++++++ ...e_explain_contract_and_replay_alignment.md | 93 +++++ ...referrers_runtime_stack_and_replay_data.md | 123 ++++++ ...gnature_trust_and_verdict_api_hardening.md | 109 ++++++ ...ls_dsse_rekor_merkle_and_hash_integrity.md | 109 ++++++ ...age_risk_score_widget_wiring_and_parity.md | 123 ++++++ ...ply_chain_fuzz_mutation_hardening_suite.md | 123 ++++++ ..._hygiene_dedup_and_archival_translation.md | 111 ++++++ ...rm_locale_label_translation_corrections.md | 56 +++ ...rrers compatibility across major registries.md | 68 ++++ ...a verifiable SBOM and attestation spine.md | 231 +++++++++++ ... - Four novel, testable moat hypotheses.md | 106 ++++++ ...60222 - Fuzz & mutation hardening suite.md | 233 ++++++++++++ ...uditor UX experiments measurement plan.md | 79 ++++ ...bolization across platforms and vendors.md | 113 ++++++ ... with Rekor v2 (superseded by 20260226).md | 54 +++ ...sibility into measurable business moats.md | 100 +++++ ...ll‑stack analysis and resolver strategy.md | 192 ++++++++++ ...nistic score service and replay control.md | 192 ++++++++++ ...inistic tile verification with Rekor v2.md | 54 +++ ...e explainability four measurable fixes.md | 28 ++ .../advisories/ARCHIVE_LOG_20260303.md | 18 + .../tools/supply-chain-hardening-suite.md | 81 ++++ docs/product/advisories/README.md | 10 + docs/product/advisory-translation-20260226.md | 34 ++ .../DsseVerifierTests.cs | 133 ++++++- .../VerdictControllerSecurityTests.cs | 327 ++++++++++++++++ .../Bundle/BundleBuilderVerificationTests.cs | 346 +++++++++++++++++ .../Sprint222ProofVerificationTests.cs | 359 ++++++++++++++++++ .../StellaOps.Platform.WebService/TASKS.md | 2 + .../ScoreExplainEndpointContractTests.cs | 297 +++++++++++++++ .../OciAttestationPublisherTests.cs | 220 +++++++++++ .../ReachabilityStackEndpointsTests.cs | 174 +++++++++ .../SliceQueryServiceRetrievalTests.cs | 216 +++++++++++ 35 files changed, 4686 insertions(+), 1 deletion(-) create mode 100644 docs-archived/implplan/2026-03-03-completed-sprints/ARCHIVE_LOG.md create mode 100644 docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_222_Cli_proof_chain_verification_and_replay_parity.md create mode 100644 docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_223_Platform_score_explain_contract_and_replay_alignment.md create mode 100644 docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_224_Scanner_oci_referrers_runtime_stack_and_replay_data.md create mode 100644 docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_225_Attestor_signature_trust_and_verdict_api_hardening.md create mode 100644 docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_226_Symbols_dsse_rekor_merkle_and_hash_integrity.md create mode 100644 docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_227_FE_triage_risk_score_widget_wiring_and_parity.md create mode 100644 docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_228_Tools_supply_chain_fuzz_mutation_hardening_suite.md create mode 100644 docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_229_DOCS_advisory_hygiene_dedup_and_archival_translation.md create mode 100644 docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_230_Platform_locale_label_translation_corrections.md create mode 100644 docs-archived/product/advisories/20260220 - OCI 1.1 referrers compatibility across major registries.md create mode 100644 docs-archived/product/advisories/20260221 - Building a verifiable SBOM and attestation spine.md create mode 100644 docs-archived/product/advisories/20260221 - Four novel, testable moat hypotheses.md create mode 100644 docs-archived/product/advisories/20260222 - Fuzz & mutation hardening suite.md create mode 100644 docs-archived/product/advisories/20260223 - Auditor UX experiments measurement plan.md create mode 100644 docs-archived/product/advisories/20260223 - Unified symbolization across platforms and vendors.md create mode 100644 docs-archived/product/advisories/20260224 - Deterministic tile verification with Rekor v2 (superseded by 20260226).md create mode 100644 docs-archived/product/advisories/20260224 - Turning defensibility into measurable business moats.md create mode 100644 docs-archived/product/advisories/20260226 - Deterministic call‑stack analysis and resolver strategy.md create mode 100644 docs-archived/product/advisories/20260226 - Deterministic score service and replay control.md create mode 100644 docs-archived/product/advisories/20260226 - Deterministic tile verification with Rekor v2.md create mode 100644 docs-archived/product/advisories/20260226 - Triage explainability four measurable fixes.md create mode 100644 docs-archived/product/advisories/ARCHIVE_LOG_20260303.md create mode 100644 docs/modules/tools/supply-chain-hardening-suite.md create mode 100644 docs/product/advisories/README.md create mode 100644 docs/product/advisory-translation-20260226.md create mode 100644 src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/VerdictControllerSecurityTests.cs create mode 100644 src/BinaryIndex/__Tests/StellaOps.Symbols.Tests/Bundle/BundleBuilderVerificationTests.cs create mode 100644 src/Cli/__Tests/StellaOps.Cli.Tests/Commands/Sprint222ProofVerificationTests.cs create mode 100644 src/Platform/__Tests/StellaOps.Platform.WebService.Tests/ScoreExplainEndpointContractTests.cs create mode 100644 src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/OciAttestationPublisherTests.cs create mode 100644 src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReachabilityStackEndpointsTests.cs create mode 100644 src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SliceQueryServiceRetrievalTests.cs diff --git a/docs-archived/implplan/2026-03-03-completed-sprints/ARCHIVE_LOG.md b/docs-archived/implplan/2026-03-03-completed-sprints/ARCHIVE_LOG.md new file mode 100644 index 000000000..6ea30fbe7 --- /dev/null +++ b/docs-archived/implplan/2026-03-03-completed-sprints/ARCHIVE_LOG.md @@ -0,0 +1,43 @@ +# Archive Log - 2026-03-03 Completed Sprints + +Source: `docs/implplan/` +Destination: `docs-archived/implplan/2026-03-03-completed-sprints/` + +Moved sprint files: +- SPRINT_20260226_222_Cli_proof_chain_verification_and_replay_parity.md +- SPRINT_20260226_223_Platform_score_explain_contract_and_replay_alignment.md +- SPRINT_20260226_224_Scanner_oci_referrers_runtime_stack_and_replay_data.md +- SPRINT_20260226_225_Attestor_signature_trust_and_verdict_api_hardening.md +- SPRINT_20260226_226_Symbols_dsse_rekor_merkle_and_hash_integrity.md +- SPRINT_20260226_227_FE_triage_risk_score_widget_wiring_and_parity.md +- SPRINT_20260226_228_Tools_supply_chain_fuzz_mutation_hardening_suite.md +- SPRINT_20260226_229_DOCS_advisory_hygiene_dedup_and_archival_translation.md +- SPRINT_20260226_230_Platform_locale_label_translation_corrections.md + +All tasks in these files are in `DONE` state with checked completion criteria. + +## 2026-03-04 Regression Revalidation + +Validated archived sprint deliverables with targeted checks: +- CLI (`SPRINT_20260226_222`): `StellaOps.Cli.Tests.Commands.Sprint222ProofVerificationTests` -> 4/4 pass. +- Platform (`SPRINT_20260226_223`): `ScoreExplainEndpointContractTests` -> 4/4 pass. +- Scanner (`SPRINT_20260226_224`): web service + storage + runtime targeted classes -> 16/16 pass. +- Attestor (`SPRINT_20260226_225`): `DsseVerifierTests` + `VerdictControllerSecurityTests` -> 21/21 pass. +- Symbols (`SPRINT_20260226_226`): `BundleBuilderVerificationTests` -> 5/5 pass. +- Web FE (`SPRINT_20260226_227`): `npx tsc --noEmit` pass; Playwright risk/score suites -> 10/10 pass. +- Tools (`SPRINT_20260226_228`): `python tests/supply-chain/run_suite.py --profile smoke --seed 20260226` -> all 5 lanes pass. +- Docs/locale (`SPRINT_20260226_229/230`): advisory folder contains only `README.md`; all archived sprint files remain `DONE_ONLY`; non-English placeholder scan clean; non-English translation JSON parses clean. + +## 2026-03-04 Additional Test Expansion + +Added and validated extra edge/negative-path tests in sprint-specific classes: +- CLI (`SPRINT_20260226_222`): added deterministic checks for missing `--trust-root` and missing Rekor checkpoint path; class now 6/6 pass. +- Platform (`SPRINT_20260226_223`): added digest normalization and malformed digest-segment checks; class now 6/6 pass. +- Scanner (`SPRINT_20260226_224`): added disabled/missing-image OCI publish cases, missing reachability stack and invalid layer cases, and missing DSSE envelope retrieval case; selected classes now 14/14 pass. +- Attestor (`SPRINT_20260226_225`): added roster-entry missing public key case (deterministic `500 authority_key_missing_public_key`); class now 6/6 pass. +- Symbols (`SPRINT_20260226_226`): added missing checkpoint while Rekor proof required case (`rekor_proof_required:missing_checkpoint`); class now 6/6 pass. +- Web FE (`SPRINT_20260226_227`): reran targeted Playwright suites after expansion work; 10/10 pass on rerun (one transient selector miss observed once, then passing on rerun). + +## 2026-03-04 Archive Hygiene + +- Advisory translation register module-doc mappings for Symbols-related advisories were updated from `docs/modules/symbols/architecture.md` (retired path) to `docs/modules/binary-index/architecture.md` so archived traceability links resolve against current module ownership. diff --git a/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_222_Cli_proof_chain_verification_and_replay_parity.md b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_222_Cli_proof_chain_verification_and_replay_parity.md new file mode 100644 index 000000000..d1a1b60fe --- /dev/null +++ b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_222_Cli_proof_chain_verification_and_replay_parity.md @@ -0,0 +1,130 @@ +# Sprint 222 - CLI Proof Chain Verification and Replay Parity + +## Topic & Scope +- Close all critical CLI proof-path placeholders for DSSE, Rekor, SBOM, witness, replay, and timeline commands. +- Align CLI behavior with deterministic backend contracts and remove synthetic fallback behaviors that hide real failures. +- Working directory: `src/Cli/`. +- Expected evidence: targeted CLI tests, golden output updates, deterministic exit-code matrix, and updated CLI module docs. + +## Dependencies & Concurrency +- Depends on Sprint 223 (Platform score explanation contract) and Sprint 224 (Scanner replay/timeline data contract) for API parity. +- Depends on Sprint 225 (Attestor trust verification) for shared DSSE verification behavior. +- Safe to run in parallel with Sprints 226, 227, 228 after endpoint contracts are frozen. + +## Documentation Prerequisites +- `docs/modules/cli/cli-vs-ui-parity.md` +- `docs/modules/attestor/proof-chain-specification.md` +- `docs/modules/signals/unified-score.md` +- `docs/modules/airgap/guides/proof-chain-verification.md` + +## Delivery Tracker + +### CLI-222-001 - Replace structural proof checks with cryptographic verification +Status: DONE +Dependency: none +Owners: Developer, Test Automation +Task description: +- Replace placeholder and structure-only verification paths in `chain verify`, `bundle verify`, and `sbom verify`. +- Implement signature verification and Rekor inclusion validation gates that fail deterministically when trust roots or proofs are invalid. +- Normalize error codes so CI can distinguish validation failure, missing evidence, and transport failure. + +Completion criteria: +- [x] `stella chain verify --verify-signatures` no longer emits `skip` for implemented paths. +- [x] `stella bundle verify` performs cryptographic DSSE checks when trust root is provided. +- [x] `stella sbom verify` performs real signature verification and surfaces deterministic failure reasons. +- [x] Golden tests assert stable output fields and exit codes. + +### CLI-222-002 - Complete witness signing, Rekor logging, and verification scripts +Status: DONE +Dependency: CLI-222-001 +Owners: Developer, Test Automation +Task description: +- Implement `stella witness generate --sign --rekor` end to end, including signed payload output and log reference recording. +- Implement real `stella witness verify` signature and inclusion proof checks. +- Regenerate bundle verification scripts so they execute real checks instead of printing `[SKIP]`. + +Completion criteria: +- [x] Witness generate supports signed output with non-placeholder metadata. +- [x] Witness verify reports true pass/fail based on DSSE + Rekor checks. +- [x] Generated `verify.ps1` and `verify.sh` scripts perform real checks. +- [x] Integration tests cover valid, tampered, and missing-proof cases. + +### CLI-222-003 - Remove synthetic score explanation fallback and align endpoint usage +Status: DONE +Dependency: Sprint 223 +Owners: Developer +Task description: +- Remove synthetic explanation generation fallback in `score replay explain`. +- Consume the canonical score explanation contract from Platform and return explicit, deterministic errors when endpoint/data is unavailable. +- Keep output formats deterministic across `table`, `json`, and machine-readable modes. + +Completion criteria: +- [x] No synthetic explanation path remains in `ScoreReplayCommandGroup`. +- [x] CLI endpoint target matches documented Platform API. +- [x] Error handling uses deterministic exit code mapping. +- [x] Unit tests cover non-200, malformed payload, and not-found responses. + +### CLI-222-004 - Implement offline scoring and real timeline/replay data paths +Status: DONE +Dependency: Sprint 224 +Owners: Developer, Test Automation +Task description: +- Implement `score compute --offline` using bundled/frozen scoring inputs. +- Replace sample timeline event generation with backend timeline query/export support. +- Implement verdict-store backed replay request construction for `--verdict` path. + +Completion criteria: +- [x] `score compute --offline` is functional and deterministic. +- [x] `timeline query` and `timeline export` use backend data, not in-memory sample events. +- [x] `replay snapshot --verdict` resolves verdict metadata without requiring manual snapshot fields when available. +- [x] Determinism tests prove repeatable outputs for identical inputs. + +### CLI-222-005 - Finish binary command surfaces that remain scaffolded +Status: DONE +Dependency: Sprint 224, Sprint 225 +Owners: Developer +Task description: +- Implement non-placeholder paths in `binary submit`, `binary info`, `binary symbols`, and `binary verify`. +- Wire signing, Scanner API submission, and optional Rekor checks with deterministic reporting. +- Ensure `binary callgraph` remains stable and contract-compatible with downstream replay workflows. + +Completion criteria: +- [x] `binary submit` no longer returns mock digest values. +- [x] `binary verify` executes real signature/hash/transparency checks. +- [x] Command docs include explicit prerequisites and offline behavior. +- [x] Integration tests validate live and offline workflows. + +### CLI-222-006 - Documentation and parity matrix updates +Status: DONE +Dependency: CLI-222-001, CLI-222-002, CLI-222-003, CLI-222-004, CLI-222-005 +Owners: Documentation author +Task description: +- Update CLI docs and parity matrix rows from planned/in-progress to available where completed. +- Record exact command contracts, exit codes, and deterministic guarantees. +- Link implementation evidence and tests from this sprint. + +Completion criteria: +- [x] `docs/modules/cli/cli-vs-ui-parity.md` updated for completed commands. +- [x] Relevant CLI guide docs updated with contract and examples. +- [x] Sprint execution log includes links to changed docs and tests. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-02-26 | Sprint created from advisory-to-capability gap review; tasks mapped to concrete CLI closure actions. | Product Manager | +| 2026-02-26 | Implemented CLI proof-path closure across `chain verify`, `bundle verify`, `sbom verify`, `witness generate/verify`, `score replay explain`, `timeline query/export`, `replay snapshot --verdict`, and `binary submit/verify` with deterministic error/exit mapping. | Developer | +| 2026-02-26 | Updated parity/docs contracts in `docs/modules/cli/architecture.md`, `docs/modules/cli/cli-vs-ui-parity.md`, `docs/modules/cli/guides/commands/sbom.md`, `docs/modules/cli/guides/commands/scan-replay.md`, and `docs/modules/cli/guides/output-and-exit-codes.md`. | Documentation author | +| 2026-03-03 | Revalidated targeted sprint coverage using class-scoped xUnit execution: `StellaOps.Cli.Tests.Commands.Sprint222ProofVerificationTests` (4 passed, 0 failed). | Test Automation | + +## Decisions & Risks +- Decision: CLI must not fabricate advisory or score explanation content when backend data is unavailable. +- Decision: class-scoped xUnit binary execution is the canonical targeted verification method for this sprint because Microsoft.Testing.Platform ignores `dotnet test --filter` in this repo. +- Risk: Endpoint contract drift across Platform/Scanner may block CLI parity; mitigate with contract fixtures and shared schema tests. Mitigation owner: CLI + Platform maintainers. +- Risk: strict verification can change operator workflows; mitigate with explicit migration notes and deterministic error taxonomy in CLI docs. Mitigation owner: CLI docs owner. + +## Next Checkpoints +- 2026-02-27: Contract freeze across CLI, Platform, Scanner. +- 2026-03-01: Proof-verification command acceptance demo. +- 2026-03-03: Offline scoring and timeline/replay acceptance demo. + + diff --git a/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_223_Platform_score_explain_contract_and_replay_alignment.md b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_223_Platform_score_explain_contract_and_replay_alignment.md new file mode 100644 index 000000000..7925f3466 --- /dev/null +++ b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_223_Platform_score_explain_contract_and_replay_alignment.md @@ -0,0 +1,93 @@ +# Sprint 223 - Platform Score Explain Contract and Replay Alignment + +## Topic & Scope +- Establish a canonical score explanation contract that CLI and Web consume without synthetic fallback behavior. +- Align score explain, evaluate, history, replay, and verify outputs with deterministic serialization and explicit error taxonomy. +- Working directory: `src/Platform/`. +- Expected evidence: endpoint implementation, schema contract tests, OpenAPI updates, and API docs updates. + +## Dependencies & Concurrency +- Upstream: none. +- Downstream consumers: Sprint 222 (CLI) and Sprint 227 (FE) depend on this contract. +- Safe to run in parallel with Sprints 224, 225, 226 once response schema freeze is agreed. + +## Documentation Prerequisites +- `docs/modules/signals/unified-score.md` +- `docs/technical/scoring-algebra.md` +- `docs/api/score-replay-api.md` + +## Delivery Tracker + +### PLATFORM-223-001 - Define and version score explanation API contract +Status: DONE +Dependency: none +Owners: Developer, Documentation author +Task description: +- Introduce a stable score explanation response schema including factor weights, source digests, deterministic input hash, and replay linkage. +- Version the contract and register it in OpenAPI and module docs. +- Define a deterministic error body for `not_found`, `invalid_input`, and `backend_unavailable`. + +Completion criteria: +- [x] Score explanation schema added to Platform contracts. +- [x] OpenAPI includes score explanation endpoint and response types. +- [x] Deterministic error schema documented and tested. + +### PLATFORM-223-002 - Implement score explanation endpoint +Status: DONE +Dependency: PLATFORM-223-001 +Owners: Developer +Task description: +- Implement `GET /api/v1/score/explain/{digest}` (or equivalent canonical route agreed in contract freeze). +- Ensure data is derived from persisted score/replay artifacts and never from synthetic stubs. +- Apply tenant and authorization controls consistent with existing `/api/v1/score/*` policies. + +Completion criteria: +- [x] Endpoint implemented with tenant-aware authorization. +- [x] Response includes deterministic fields and replay linkage. +- [x] Missing data returns deterministic `not_found` error body. +- [x] Integration tests cover valid, missing, and malformed input. + +### PLATFORM-223-003 - Unify score endpoint determinism guarantees +Status: DONE +Dependency: PLATFORM-223-001 +Owners: Developer, Test Automation +Task description: +- Audit all score endpoints for deterministic field ordering, timestamp behavior, and optional field consistency. +- Remove response inconsistencies that break CLI/Web stable parsing. +- Add contract-level tests validating output stability for replay and verify flows. + +Completion criteria: +- [x] Deterministic contract tests added for evaluate/history/replay/verify/explain. +- [x] Response shape and key semantics are stable across repeated runs. +- [x] No endpoint emits synthetic or random demo-only content in production path. + +### PLATFORM-223-004 - Publish migration and client integration notes +Status: DONE +Dependency: PLATFORM-223-002, PLATFORM-223-003 +Owners: Documentation author +Task description: +- Document endpoint contract, compatibility notes, and migration guidance for CLI and FE clients. +- Update module docs and API docs with example requests/responses and error mapping. +- Provide deprecation plan if any old score explain path exists. + +Completion criteria: +- [x] API docs updated with the final endpoint route and examples. +- [x] Integration guidance added for CLI and FE consumers. +- [x] Sprint log links all changed docs and tests. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-02-26 | Sprint created to close score explain contract gap and unblock CLI/FE parity work. | Product Manager | +| 2026-02-26 | Completed deterministic score contract closure: filled `unknowns`/`proof_ref`, replaced replay verify placeholders with deterministic envelope comparison + mismatch taxonomy, updated score API docs, and validated with targeted score endpoint contract/unit tests (`34` tests passed). | Developer | +| 2026-03-03 | Revalidated targeted endpoint contract coverage with class-scoped xUnit run: `StellaOps.Platform.WebService.Tests.ScoreExplainEndpointContractTests` (4 passed, 0 failed). | Test Automation | + +## Decisions & Risks +- Decision: score explanation must be sourced from verifiable stored score artifacts and replay metadata. +- Risk: existing clients may depend on undocumented fields; mitigate via versioned schema and compatibility notes. Mitigation owner: Platform API owner. +- Risk: endpoint route mismatch between historical docs and implementation; mitigate via contract freeze checkpoint. Mitigation owner: Platform + CLI maintainers. + +## Next Checkpoints +- 2026-02-27: Contract freeze review with CLI and FE owners. +- 2026-03-01: Endpoint and contract test completion checkpoint. +- 2026-03-02: Consumer integration signoff. diff --git a/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_224_Scanner_oci_referrers_runtime_stack_and_replay_data.md b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_224_Scanner_oci_referrers_runtime_stack_and_replay_data.md new file mode 100644 index 000000000..ee92f3125 --- /dev/null +++ b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_224_Scanner_oci_referrers_runtime_stack_and_replay_data.md @@ -0,0 +1,123 @@ +# Sprint 224 - Scanner OCI Referrers, Runtime Stack, and Replay Data + +## Topic & Scope +- Implement robust OCI discovery and attestation attachment paths across registries with runtime capability detection and deterministic fallback. +- Replace scanner-side placeholder data paths for replay command generation, slice retrieval, and reachability stack exposure. +- Working directory: `src/Scanner/`. +- Expected evidence: integration tests for OCI fallback matrix, runtime stack endpoint behavior, replay command fidelity, and CAS retrieval. + +## Dependencies & Concurrency +- Depends on Sprint 225 for shared DSSE verification behavior and trust roots. +- Provides required backend paths for Sprint 222 (CLI timeline/replay) and Sprint 227 (FE triage evidence wiring). +- Can run in parallel with Sprint 226 and Sprint 228 after contract checkpoints. + +## Documentation Prerequisites +- `docs/modules/scanner/architecture.md` +- `docs/modules/platform/explainable-triage-implementation-plan.md` +- `docs/modules/airgap/guides/proof-chain-verification.md` + +## Delivery Tracker + +### SCANNER-224-001 - Implement OCI referrers capability probing and fallback strategy +Status: DONE +Dependency: none +Owners: Developer, Test Automation +Task description: +- Add runtime capability probing for registries that do not implement OCI 1.1 referrers. +- Implement deterministic fallback order: OCI referrers -> fallback tags -> provider attachment API adapters where configured. +- Record capability outcomes in structured logs and response metadata for operator visibility. + +Completion criteria: +- [x] Referrer discovery no longer silently returns empty on non-success without capability metadata. +- [x] Configurable fallback flow implemented and test-covered. +- [x] Integration tests cover at least GHCR-like, ECR-like, and attachment-model flows. + +### SCANNER-224-002 - Complete DSSE verification during slice pull and attestation publish +Status: DONE +Dependency: SCANNER-224-001 +Owners: Developer +Task description: +- Replace pending DSSE verification path in slice pull service with trust-root backed verification. +- Replace placeholder attestation digest and delayed no-op in OCI attestation publisher with real attachment flow. +- Ensure verification status is propagated to callers as deterministic structured fields. + +Completion criteria: +- [x] Slice pull DSSE verification returns true/false based on real verification. +- [x] OciAttestationPublisher returns real attached digest values. +- [x] Unit/integration tests validate signing and verification failure modes. + +### SCANNER-224-003 - Implement CAS-backed slice retrieval and DSSE retrieval paths +Status: DONE +Dependency: none +Owners: Developer +Task description: +- Implement `GetSliceAsync` and `GetSliceDsseAsync` retrieval against the actual CAS interface. +- Remove compilation-only null return behavior and surface deterministic not-found/error responses. +- Add parity tests for replay paths that depend on stored slices. + +Completion criteria: +- [x] CAS retrieval is implemented for slice and DSSE payloads. +- [x] Replay flows can resolve stored slices end-to-end. +- [x] Tests cover cache hit, cache miss, and corrupt object cases. + +### SCANNER-224-004 - Replace replay command placeholders with live scan context +Status: DONE +Dependency: SCANNER-224-003 +Owners: Developer +Task description: +- Update replay command generation to use actual scan/finding context instead of static API base and placeholder-derived values. +- Ensure generated commands are deterministic and shell-specific as requested. +- Add verification that generated commands reproduce the same verdict hash when replayed. + +Completion criteria: +- [x] Replay command service no longer emits hardcoded API base or placeholder values. +- [x] Deterministic command-generation tests pass across supported shells. +- [x] Command metadata includes required offline prerequisites accurately. + +### SCANNER-224-005 - Deliver real reachability stack repository integration +Status: DONE +Dependency: none +Owners: Developer +Task description: +- Implement repository backing for `IReachabilityStackRepository` and wire it into endpoint composition. +- Remove not-implemented default behavior in deployments where stack data is configured. +- Preserve deterministic API semantics for layer and full-stack retrieval. + +Completion criteria: +- [x] Reachability stack endpoint returns persisted data when configured. +- [x] `501 not implemented` path is limited to genuinely disabled deployments. +- [x] API tests cover full stack and per-layer responses. + +### SCANNER-224-006 - Runtime collector implementation milestones for eBPF and ETW +Status: DONE +Dependency: none +Owners: Developer, Test Automation +Task description: +- Implement initial production-grade event ingestion loops for eBPF and ETW collectors with explicit sealed-mode behavior. +- Add deterministic event serialization and symbol resolution cache behavior. +- Expose collector health/capability signals for triage explainability. + +Completion criteria: +- [x] eBPF collector performs non-placeholder start/stop/event ingestion path. +- [x] ETW collector performs non-placeholder session/event path. +- [x] Integration tests validate deterministic outputs for frozen fixtures. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-02-26 | Sprint created from advisory gap mapping for OCI compatibility, runtime stack, and replay data integrity. | Product Manager | +| 2026-02-26 | Implemented OCI capability probing + deterministic fallback ordering, DSSE verification on slice pull/publish paths, CAS-backed slice retrieval, replay command generation from live scan context, and reachability stack repository wiring. | Developer | +| 2026-02-26 | Delivered runtime collector milestones in `StellaOps.Scanner.Runtime` (eBPF/ETW non-placeholder ingestion paths) plus deterministic fixture coverage. | Developer | +| 2026-03-03 | Revalidated targeted scanner classes: `OciAttestationPublisherTests` (1), `ReachabilityStackEndpointsTests` (3), `SliceQueryServiceRetrievalTests` (5), `SlicePullServiceTests` (4), `TraceCollectorFixtureTests` (3); total 16 passed, 0 failed. | Test Automation | + +## Decisions & Risks +- Decision: registry fallback behavior must be explicit and observable, never silent. +- Risk: registry-specific adapters may increase complexity; mitigate with deterministic fallback ordering and capability cache. Mitigation owner: Scanner registry integration owner. +- Risk: runtime collectors can be environment-sensitive; mitigate with fixture-based deterministic tests and sealed-mode paths. Mitigation owner: Scanner runtime owner. + +## Next Checkpoints +- 2026-02-28: OCI fallback contract checkpoint. +- 2026-03-01: CAS + replay command path completion checkpoint. +- 2026-03-04: Runtime collector implementation checkpoint. + + diff --git a/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_225_Attestor_signature_trust_and_verdict_api_hardening.md b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_225_Attestor_signature_trust_and_verdict_api_hardening.md new file mode 100644 index 000000000..4acf4f34b --- /dev/null +++ b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_225_Attestor_signature_trust_and_verdict_api_hardening.md @@ -0,0 +1,109 @@ +# Sprint 225 - Attestor Signature Trust and Verdict API Hardening + +## Topic & Scope +- Close high-risk trust verification gaps in Attestor signature handling and verdict endpoint authorization flow. +- Remove placeholder endpoint behaviors that hide unimplemented trust checks and incomplete verdict lookup paths. +- Working directory: `src/Attestor/`. +- Expected evidence: cryptographic verification tests (including Ed25519), endpoint integration tests, and updated trust/runbook docs. + +## Dependencies & Concurrency +- Upstream: none. +- Downstream: Sprint 222 and Sprint 224 consume finalized Attestor verification semantics. +- Safe to run in parallel with Sprint 226 and Sprint 228 after trust-root contract freeze. + +## Documentation Prerequisites +- `docs/modules/attestor/architecture.md` +- `docs/modules/attestor/proof-chain-specification.md` +- `docs/modules/attestor/rekor-verification-design.md` + +## Delivery Tracker + +### ATTESTOR-225-001 - Implement Ed25519 verification in DSSE verifier +Status: DONE +Dependency: none +Owners: Developer, Test Automation +Task description: +- Implement Ed25519 DSSE signature verification path in `DsseVerifier`. +- Ensure key-type dispatch remains deterministic and error reporting identifies unsupported/invalid key material clearly. +- Add test vectors for ECDSA, RSA, and Ed25519 success/failure paths. + +Completion criteria: +- [x] Ed25519 signatures verify successfully using trusted test vectors. +- [x] Failure modes produce deterministic error reasons. +- [x] Existing ECDSA/RSA behavior remains backward compatible. + +### ATTESTOR-225-002 - Enforce Authority roster verification for verdict creation +Status: DONE +Dependency: ATTESTOR-225-001 +Owners: Developer +Task description: +- Implement DSSE signature verification against Authority key roster in verdict create endpoint. +- Reject unsigned or untrusted verdict submissions with deterministic authorization error responses. +- Remove placeholder trust bypass comments and temporary acceptance paths. + +Completion criteria: +- [x] Verdict creation validates DSSE signature against roster before append. +- [x] Unauthorized signatures are rejected with deterministic response body. +- [x] Endpoint tests cover trusted, revoked, and unknown key scenarios. + +### ATTESTOR-225-003 - Replace header-only tenant resolution with authenticated context +Status: DONE +Dependency: none +Owners: Developer +Task description: +- Replace `X-Tenant-Id` placeholder extraction with claim-derived tenant context from authenticated principal. +- Keep optional compatibility guardrails only where explicitly approved and auditable. +- Ensure tenant mismatch handling is explicit and deterministic. + +Completion criteria: +- [x] Tenant context is resolved from authenticated principal. +- [x] Header spoofing no longer grants tenant write/read behavior. +- [x] Endpoint tests verify tenant isolation. + +### ATTESTOR-225-004 - Implement verdict-by-hash retrieval path +Status: DONE +Dependency: none +Owners: Developer +Task description: +- Add service/repository method for direct verdict lookup by hash and wire endpoint behavior. +- Replace current placeholder not-found return path with implemented retrieval logic. +- Add paging/filter semantics where needed to preserve current API style. + +Completion criteria: +- [x] `GET /api/v1/verdicts/{hash}` (or mapped equivalent) returns actual verdict when present. +- [x] Not-found path only used for true missing records. +- [x] Tests validate retrieval and tenant authorization. + +### ATTESTOR-225-005 - Trust-mode documentation and operational runbook updates +Status: DONE +Dependency: ATTESTOR-225-001, ATTESTOR-225-002, ATTESTOR-225-003, ATTESTOR-225-004 +Owners: Documentation author +Task description: +- Update attestor docs with finalized key-type support, roster verification flow, and tenant trust model. +- Add operational troubleshooting guidance for signature and roster failures. +- Link implemented sprint tasks in proof-chain specification status tables. + +Completion criteria: +- [x] Attestor docs reflect implemented trust behavior. +- [x] Proof-chain sprint status table updated with this sprint linkage. +- [x] Runbook includes deterministic error triage guidance. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-02-26 | Sprint created to close attestor trust and verdict API hardening gaps identified in advisory review. | Product Manager | +| 2026-02-26 | Implementation started: Ed25519 verifier, verdict roster enforcement, tenant context hardening, and verdict retrieval path. | Developer | +| 2026-02-26 | Completed trust hardening: Ed25519 DSSE verification in `DsseVerifier`, roster-backed verdict authorization checks, principal-derived tenant resolution, and verdict-by-hash retrieval with deterministic status codes. | Developer | +| 2026-02-26 | Updated attestor module docs/runbooks: `docs/modules/attestor/architecture.md`, `docs/modules/attestor/proof-chain-specification.md`, and `docs/modules/attestor/guides/offline-verification.md`. | Documentation author | +| 2026-03-03 | Revalidated targeted classes: `StellaOps.Attestation.Tests.DsseVerifierTests` (16 passed) and `StellaOps.Attestor.Tests.VerdictControllerSecurityTests` (5 passed). | Test Automation | + +## Decisions & Risks +- Decision: no unsigned or untrusted verdict write path in production mode. +- Risk: roster distribution latency can cause temporary false rejects; mitigate with cache visibility and explicit retry guidance. Mitigation owner: Attestor operations owner. +- Risk: tenant context migration may break legacy clients; mitigate with migration window and explicit deprecation notice. Mitigation owner: Attestor API owner. + +## Next Checkpoints +- 2026-02-28: key-type verification milestone (Ed25519 path). +- 2026-03-01: verdict endpoint trust enforcement milestone. +- 2026-03-03: docs and operational runbook completion. + diff --git a/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_226_Symbols_dsse_rekor_merkle_and_hash_integrity.md b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_226_Symbols_dsse_rekor_merkle_and_hash_integrity.md new file mode 100644 index 000000000..8e7734cc4 --- /dev/null +++ b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_226_Symbols_dsse_rekor_merkle_and_hash_integrity.md @@ -0,0 +1,109 @@ +# Sprint 226 - Symbols DSSE, Rekor, Merkle, and Hash Integrity + +## Topic & Scope +- Replace placeholder cryptographic behaviors in symbols bundling and verification with production-safe implementations. +- Deliver complete DSSE signing/verification and Rekor submission/inclusion validation for symbols bundles. +- Working directory: `src/Symbols/`. +- Expected evidence: symbols bundle integration tests, crypto correctness tests, and updated symbol verification docs. + +## Dependencies & Concurrency +- Depends on Sprint 225 for shared trust-root/key validation conventions. +- Provides proof artifacts consumed by Sprint 224 (Scanner runtime symbolization) and Sprint 227 (FE confidence widgets). +- Safe to run in parallel with Sprint 222 once endpoint contracts are stable. + +## Documentation Prerequisites +- `docs/modules/symbols/architecture.md` +- `docs/modules/attestor/proof-chain-specification.md` +- `docs/modules/airgap/guides/proof-chain-verification.md` + +## Delivery Tracker + +### SYMBOLS-226-001 - Replace hash placeholders with intended algorithm implementation +Status: DONE +Dependency: none +Owners: Developer, Test Automation +Task description: +- Replace SHA256 placeholder paths currently labeled as BLAKE3 with actual BLAKE3 implementation where specified by contract. +- Audit all bundle digest and root-hash fields for algorithm labeling correctness. +- Add cross-platform deterministic hashing tests for fixed fixtures. + +Completion criteria: +- [x] Placeholder hash comments removed from production paths. +- [x] Algorithm labels match actual computed algorithm. +- [x] Determinism tests pass across supported environments. + +### SYMBOLS-226-002 - Implement DSSE signing and verification for bundles +Status: DONE +Dependency: SYMBOLS-226-001 +Owners: Developer +Task description: +- Implement actual DSSE bundle signing with configured keys and canonical payload serialization. +- Implement signature verification path for bundle verify operation and return explicit verification status. +- Ensure signature metadata is persisted for downstream audit/replay workflows. + +Completion criteria: +- [x] Bundle signing produces verifiable DSSE envelopes. +- [x] Verification fails deterministically for tampered payload/signature. +- [x] Integration tests cover valid/tampered/missing-signature cases. + +### SYMBOLS-226-003 - Implement Rekor submit and inclusion verification +Status: DONE +Dependency: SYMBOLS-226-002 +Owners: Developer +Task description: +- Implement Rekor submission for symbols bundles and persist returned entry metadata. +- Implement offline and online inclusion verification paths, including checkpoint validation where available. +- Remove placeholder checkpoint generation and random GUID entry behavior. + +Completion criteria: +- [x] Rekor submission returns real entry metadata in bundle output. +- [x] Offline inclusion verification executes real proof checks. +- [x] Verification outputs include deterministic status and reason fields. + +### SYMBOLS-226-004 - Implement Merkle inclusion verification in bundle verifier +Status: DONE +Dependency: SYMBOLS-226-003 +Owners: Developer +Task description: +- Implement Merkle proof validation where currently stubbed in symbols bundle verification. +- Verify consistency between bundle manifest digests and Rekor/checkpoint references. +- Add explicit failure taxonomy for inclusion mismatch and missing proof nodes. + +Completion criteria: +- [x] Merkle inclusion proof path implemented and tested. +- [x] Bundle verify returns fail on proof mismatch. +- [x] Test vectors include corrupted and truncated proof scenarios. + +### SYMBOLS-226-005 - Update symbols verification docs and operator guide +Status: DONE +Dependency: SYMBOLS-226-002, SYMBOLS-226-003, SYMBOLS-226-004 +Owners: Documentation author +Task description: +- Update Symbols module docs with finalized DSSE/Rekor/Merkle verification flow. +- Document offline verification behavior and trust-root requirements. +- Link acceptance tests and deterministic fixtures. + +Completion criteria: +- [x] Symbols docs updated for full proof chain behavior. +- [x] Offline verification procedure documented. +- [x] Sprint execution log references concrete doc/test artifacts. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-02-26 | Sprint created to close symbols proof chain placeholders and hash integrity gaps. | Product Manager | +| 2026-02-26 | Implemented hash integrity and proof-chain closure in Symbols: algorithm labeling fixes, DSSE sign/verify path, Rekor entry persistence/verification wiring, and Merkle inclusion proof validation with deterministic failure taxonomy. | Developer | +| 2026-02-26 | Updated symbols docs for finalized proof-chain behavior and offline procedure: `docs/modules/symbols/architecture.md` and `docs/modules/symbols/specs/bundle-guide.md`. | Documentation author | +| 2026-03-03 | Revalidated targeted class `StellaOps.Symbols.Tests.Bundle.BundleBuilderVerificationTests` (5 passed, 0 failed). | Test Automation | + +## Decisions & Risks +- Decision: algorithm labels in manifests must always reflect actual implemented algorithm to avoid audit ambiguity. +- Risk: introducing BLAKE3 may affect interoperability assumptions; mitigate with compatibility notes and migration tests. Mitigation owner: Symbols module owner. +- Risk: Rekor availability constraints in sealed/offline environments; mitigate with deterministic offline verification mode. Mitigation owner: Symbols + Attestor maintainers. + +## Next Checkpoints +- 2026-02-28: hash/signing path completion checkpoint. +- 2026-03-02: Rekor and Merkle verification completion checkpoint. +- 2026-03-03: documentation and operations handoff. + + diff --git a/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_227_FE_triage_risk_score_widget_wiring_and_parity.md b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_227_FE_triage_risk_score_widget_wiring_and_parity.md new file mode 100644 index 000000000..ef525bc19 --- /dev/null +++ b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_227_FE_triage_risk_score_widget_wiring_and_parity.md @@ -0,0 +1,123 @@ +# Sprint 227 - FE Triage, Risk, and Score Widget Wiring and Parity + +## Topic & Scope +- Wire triage evidence actions to real backend flows and remove mock/stub behavior in daily triage workflows. +- Deliver missing risk and score UI surfaces currently represented by skipped E2E suites. +- Working directory: `src/Web/StellaOps.Web/`. +- Expected evidence: unskipped E2E suites, component tests, accessibility checks, and updated FE docs. + +## Dependencies & Concurrency +- Depends on Sprint 223 (score explanation contract), Sprint 224 (scanner replay/evidence endpoints), and Sprint 225 (attestor verification semantics). +- Safe to run in parallel with Sprint 222 after CLI/API contracts are stable. + +## Documentation Prerequisites +- `docs/modules/web/unified-triage-specification.md` +- `docs/modules/platform/explainable-triage-implementation-plan.md` +- `docs/modules/cli/cli-vs-ui-parity.md` + +## Delivery Tracker + +### FE-227-001 - Wire evidence pills and quick-verify actions end to end +Status: DONE +Dependency: Sprint 224, Sprint 225 +Owners: Developer, Test Automation +Task description: +- Expand triage workspace pill handlers to support `dsse`, `rekor`, and `sbom`. +- Wire `quickVerifyClick` and `whyClick` outputs to concrete verification and explanation panels. +- Ensure pill state reflects real evidence API responses rather than local mock derivation. + +Completion criteria: +- [x] Triage workspace handles all evidence pill types emitted by `EvidencePillsComponent`. +- [x] Quick-Verify triggers real verification flow and displays deterministic results. +- [x] Why action surfaces actionable reason when verification is unavailable. +- [x] Component and E2E tests validate the full interaction path. + +### FE-227-002 - Replace mock attestation and signed-evidence heuristics +Status: DONE +Dependency: FE-227-001 +Owners: Developer +Task description: +- Remove mock attestation construction in triage workspace and bind to unified evidence API payloads. +- Replace heuristic `hasSignedEvidence` logic with explicit backend verification state. +- Ensure fallback UI states clearly distinguish loading, missing, and invalid evidence. + +Completion criteria: +- [x] Mock attestation builder path removed from primary triage render path. +- [x] Signed evidence indicator sourced from backend-provided trust state. +- [x] UI states are deterministic and test-covered for missing/invalid evidence. + +### FE-227-003 - Implement remaining triage workspace stubs +Status: DONE +Dependency: FE-227-001 +Owners: Developer +Task description: +- Implement currently stubbed triage actions and sections: Fix PR workflow, bulk action modal, VEX modal, policy trace panel, and real callgraph rendering. +- Replace hardcoded replay command and mock snapshot IDs with API-provided values. +- Ensure keyboard and accessibility behavior remains intact. + +Completion criteria: +- [x] Stub labels and TODO placeholders removed from shipped triage paths. +- [x] Replay command shown in UI is generated from backend response. +- [x] Accessibility checks pass for new modals/panels. + +### FE-227-004 - Deliver risk dashboard parity features +Status: DONE +Dependency: Sprint 223, Sprint 224 +Owners: Developer, Test Automation +Task description: +- Implement budget view, verdict view, exception workflow, side-by-side diff, and responsive behavior currently covered by skipped tests. +- Align risk dashboard data sources with deterministic backend APIs and explicit loading/error states. +- Maintain deterministic ordering and filter behavior. + +Completion criteria: +- [x] Risk dashboard skipped E2E suites are re-enabled and passing. +- [x] Budget, verdict, exception, and diff widgets are functional. +- [x] Responsive behavior is validated in E2E coverage. + +### FE-227-005 - Integrate score features into findings and triage flows +Status: DONE +Dependency: Sprint 223 +Owners: Developer, Test Automation +Task description: +- Integrate score pill, badge, breakdown popover, findings list score data, and score history chart into active triage/finding views. +- Remove mock-only assumptions in score client paths used by production UI. +- Re-enable and stabilize skipped score feature E2E suites. + +Completion criteria: +- [x] Score components are integrated in production findings views. +- [x] Score history and breakdown views consume real API data. +- [x] Skipped score E2E specs are re-enabled and passing. + +### FE-227-006 - Docs and parity matrix updates +Status: DONE +Dependency: FE-227-001, FE-227-002, FE-227-003, FE-227-004, FE-227-005 +Owners: Documentation author +Task description: +- Update unified triage spec and FE feature docs with implemented widget flows and keyboard/accessibility behavior. +- Record CLI/UI parity changes resulting from FE completion. +- Add execution log entries with links to re-enabled E2E suites. + +Completion criteria: +- [x] Web docs reflect implemented triage/risk/score behavior. +- [x] Parity documentation updated for completed FE gaps. +- [x] Sprint log references concrete test evidence artifacts. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-02-26 | Sprint created to close triage/risk/score UI wiring and parity gaps from advisory review. | Product Manager | +| 2026-02-26 | Implemented findings/risk/triage production wiring for score and verification widgets, removed mock-only assumptions, and aligned replay/verification actions to backend-derived state in `src/Web/StellaOps.Web/src/app/features/**`. | Developer | +| 2026-02-26 | Updated parity/docs references in `docs/modules/web/unified-triage-specification.md` and `docs/modules/cli/cli-vs-ui-parity.md`. | Documentation author | +| 2026-03-03 | Revalidated FE delivery with `npx tsc -p tsconfig.app.json --noEmit` and targeted Playwright suites (`tests/e2e/risk-dashboard.spec.ts`, `tests/e2e/score-features.spec.ts`): 10 passed, 0 failed. | Test Automation | + +## Decisions & Risks +- Decision: UI verification signals must come from backend trust state, not local heuristics. +- Risk: selector drift may keep E2E tests skipped; mitigate by maintaining stable test IDs and spec-scoped selectors. Mitigation owner: Web FE test owner. +- Risk: expanded triage widgets can degrade performance; mitigate via incremental rendering and lazy data loading. Mitigation owner: Web FE owner. + +## Next Checkpoints +- 2026-02-28: evidence pill wiring and mock removal checkpoint. +- 2026-03-02: triage stub replacement checkpoint. +- 2026-03-04: risk/score parity E2E completion checkpoint. + + diff --git a/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_228_Tools_supply_chain_fuzz_mutation_hardening_suite.md b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_228_Tools_supply_chain_fuzz_mutation_hardening_suite.md new file mode 100644 index 000000000..60c9df535 --- /dev/null +++ b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_228_Tools_supply_chain_fuzz_mutation_hardening_suite.md @@ -0,0 +1,123 @@ +# Sprint 228 - Tools Supply Chain Fuzz and Mutation Hardening Suite + +## Topic & Scope +- Materialize the advisory-recommended supply-chain fuzz/mutation suite that is currently absent from the repository. +- Add deterministic, offline-friendly harnesses for JCS, DSSE, Rekor negative-path, and large payload/referrer stress testing. +- Working directory: `src/Tools/`. +- Expected evidence: new `tests/supply-chain/` harness, CI job outputs, deterministic corpus artifacts, and runbook documentation. + +## Dependencies & Concurrency +- Depends on Sprint 224, Sprint 225, and Sprint 226 for finalized verification contracts and expected failure semantics. +- Safe to run in parallel with Sprint 227 after API contracts are frozen. +- Cross-module edit allowance: this sprint explicitly allows creation and maintenance of `tests/supply-chain/` assets in addition to `src/Tools/`. + +## Documentation Prerequisites +- `docs/modules/attestor/rekor-verification-design.md` +- `docs/modules/airgap/guides/proof-chain-verification.md` +- Advisory: `docs-archived/product/advisories/20260222 - Fuzz & mutation hardening suite.md` + +## Delivery Tracker + +### TOOLS-228-001 - Create supply-chain test suite skeleton and tooling wrappers +Status: DONE +Dependency: none +Owners: Developer +Task description: +- Create `tests/supply-chain/` with advisory-defined subdirectories and deterministic fixture layout. +- Add tool wrappers in `src/Tools/` for local and CI execution with fixed seeds. +- Ensure no external network dependency is required for default test execution. + +Completion criteria: +- [x] `tests/supply-chain/` exists with initial sub-suite structure. +- [x] Tool wrappers execute each suite deterministically with fixed seeds. +- [x] Local run works without network dependency. + +### TOOLS-228-002 - Implement JCS property tests with artifact emission +Status: DONE +Dependency: TOOLS-228-001 +Owners: Developer, Test Automation +Task description: +- Implement JCS invariants: idempotence, key permutation equivalence, duplicate-key rejection. +- Capture deterministic failure artifacts (`failing_case`, seed, diff patch, junit) for triage. +- Add CI-friendly bounded runtime gate. + +Completion criteria: +- [x] JCS property tests run and emit deterministic artifacts on failure. +- [x] Invariant checks are enforced in CI. +- [x] Test documentation explains replaying failure seeds. + +### TOOLS-228-003 - Implement schema-aware fuzz and mutation lanes +Status: DONE +Dependency: TOOLS-228-001 +Owners: Developer, Test Automation +Task description: +- Add schema-aware fuzz for CycloneDX/in-toto payloads and mutation corpus management. +- Enforce crash-free gate with deterministic repro output. +- Add corpus refresh workflow with reproducible snapshots. + +Completion criteria: +- [x] Fuzz lane runs with bounded deterministic runtime. +- [x] Crash artifacts and repro playbook are generated automatically. +- [x] Corpus update procedure documented and repeatable. + +### TOOLS-228-004 - Implement Rekor/DSSE negative-path and large-blob/referrer stress tests +Status: DONE +Dependency: Sprint 224, Sprint 225, Sprint 226 +Owners: Developer, Test Automation +Task description: +- Build deterministic fault-injection harness for Rekor and DSSE validation edge cases. +- Add large payload/referrer stress tests that assert deterministic error semantics and memory safety constraints. +- Ensure tests target real verification pipeline interfaces, not mock-only endpoints. + +Completion criteria: +- [x] Rekor negative-path suite covers oversized payload, unsupported type, and timeout classes. +- [x] Large DSSE/referrer tests assert deterministic failure behavior. +- [x] Reports include machine-readable error classification. + +### TOOLS-228-005 - CI integration and quality gates +Status: DONE +Dependency: TOOLS-228-002, TOOLS-228-003, TOOLS-228-004 +Owners: Test Automation +Task description: +- Add CI pipeline stage for `tests/supply-chain/` with deterministic runtime caps. +- Publish artifacts for failed runs and enforce gating policy. +- Add nightly extended mode and PR smoke mode split. + +Completion criteria: +- [x] CI stage added with deterministic pass/fail criteria. +- [x] Artifact retention configured for triage evidence. +- [x] PR and nightly profiles documented and operational. + +### TOOLS-228-006 - Documentation and operator runbook +Status: DONE +Dependency: TOOLS-228-005 +Owners: Documentation author +Task description: +- Document suite purpose, execution flow, and deterministic repro steps. +- Link advisory requirements to concrete test lanes and gates. +- Add maintenance notes for corpus growth and runtime budgets. + +Completion criteria: +- [x] Runbook documented in active docs tree. +- [x] Advisory-to-test traceability table added. +- [x] Sprint execution log links docs and CI artifacts. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-02-26 | Sprint created to materialize missing supply-chain fuzz/mutation hardening suite. | Product Manager | +| 2026-02-26 | Implemented deterministic offline harness under `tests/supply-chain/` with five lanes (JCS property, schema fuzz/mutation, Rekor negative-path, large DSSE/referrer stress, corpus archive) plus wrappers (`run_suite.py`, `run.sh`, `run.ps1`, `Makefile`). | Developer | +| 2026-02-26 | Added CI quality gates via `.gitea/workflows/supply-chain-hardening.yml` with PR smoke/nightly profile split and artifact retention, and published runbook in `docs/modules/tools/supply-chain-hardening-suite.md`. | Test Automation | +| 2026-03-03 | Revalidated smoke profile: `python tests/supply-chain/run_suite.py --profile smoke --seed 20260226 --output out/supply-chain` completed with all lanes pass (`01-jcs-property`, `02-schema-fuzz`, `03-rekor-neg`, `04-big-dsse-referrers`, `05-corpus-archive`). | Test Automation | + +## Decisions & Risks +- Decision: default suite mode must be offline-friendly and deterministic. +- Risk: fuzz infra can become flaky in CI; mitigate via bounded deterministic seeds and explicit profile separation. Mitigation owner: Tools QA owner. +- Risk: corpus growth can inflate runtime; mitigate with capped smoke profile and scheduled full profile. Mitigation owner: Tools maintainers. + +## Next Checkpoints +- 2026-02-28: suite skeleton and JCS lane checkpoint. +- 2026-03-02: fuzz/negative-path lane checkpoint. +- 2026-03-05: CI gating and runbook completion checkpoint. + + diff --git a/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_229_DOCS_advisory_hygiene_dedup_and_archival_translation.md b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_229_DOCS_advisory_hygiene_dedup_and_archival_translation.md new file mode 100644 index 000000000..723d7c009 --- /dev/null +++ b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_229_DOCS_advisory_hygiene_dedup_and_archival_translation.md @@ -0,0 +1,111 @@ +# Sprint 229 - Docs Advisory Hygiene, Dedup, and Archival Translation + +## Topic & Scope +- Resolve advisory hygiene issues (duplicate files and malformed content) and translate all open advisories into concrete implementation tracking. +- Ensure advisory handling workflow is completed end-to-end: validate, map to docs/sprints, then archive. +- Working directory: `docs/`. +- Expected evidence: cleaned advisory set, cross-linked sprint actions, archived advisories with audit trail, and updated module docs links. + +## Dependencies & Concurrency +- Depends on sprint creation in 222 through 228 so every actionable advisory has implementation tracking. +- Can run in parallel with implementation sprints once task mapping is complete. +- Cross-module edit allowance: this sprint allows updates under `docs/`, `docs-archived/`, and `docs/implplan/`. + +## Documentation Prerequisites +- `AGENTS.md` (repo root) advisory handling section. +- `docs/implplan/AGENTS.md` +- `docs/README.md` +- Open advisories in `docs/product/advisories/` and archived advisory set in `docs-archived/product/advisories/`. + +## Delivery Tracker + +### DOCS-229-001 - Deduplicate deterministic tile verification advisory +Status: DONE +Dependency: none +Owners: Documentation author +Task description: +- Resolve duplicate advisory entries for deterministic tile verification with identical content hashes. +- Keep one canonical advisory file in open advisories and record dedup rationale. +- Ensure archived history preserves a clear chain of supersession. + +Completion criteria: +- [x] Duplicate file removed or superseded with explicit rationale. +- [x] Canonical advisory reference retained and linked in sprint logs. +- [x] No duplicate content remains in open advisory folder for this topic. + +### DOCS-229-002 - Repair malformed auditor UX advisory +Status: DONE +Dependency: none +Owners: Product Manager, Documentation author +Task description: +- Replace malformed non-Stella advisory content with valid Stella-specific advisory text and measurable UX experiment plan. +- If content cannot be recovered, mark as invalid advisory artifact and archive with rationale. +- Ensure no external-image-only placeholder advisory remains as active input. + +Completion criteria: +- [x] Malformed advisory replaced or invalidated with documented rationale. +- [x] Resulting advisory has actionable tasks and measurable acceptance criteria. +- [x] Advisory index remains internally coherent. + +### DOCS-229-003 - Map each open advisory to implementation sprints and module docs +Status: DONE +Dependency: DOCS-229-001, DOCS-229-002 +Owners: Product Manager +Task description: +- Build a traceability table from each open advisory to sprint IDs, module docs, and owning teams. +- Confirm all gap items have explicit sprint task coverage and completion criteria. +- Add linkbacks in module docs where advisory-driven behavior is promised. + +Completion criteria: +- [x] Advisory-to-sprint traceability table committed. +- [x] Every open advisory has mapped implementation tasks. +- [x] Module docs updated with relevant advisory-driven commitments. + +### DOCS-229-004 - Archive translated advisories per workflow +Status: DONE +Dependency: DOCS-229-003 +Owners: Documentation author +Task description: +- Move advisories from `docs/product/advisories/` to `docs-archived/product/advisories/` once translated into docs and sprint tasks. +- Preserve filenames, metadata, and cross-links for auditability. +- Record each archive action in sprint execution log and decisions/risks. + +Completion criteria: +- [x] Eligible advisories archived with traceable links. +- [x] Open advisories folder contains only not-yet-translated advisories. +- [x] Archive actions documented with UTC timestamps. + +### DOCS-229-005 - Update implplan execution logs and risk register +Status: DONE +Dependency: DOCS-229-004 +Owners: Project Manager +Task description: +- Add execution log entries in affected sprints (222 through 228) referencing advisory translation and implementation kickoff. +- Record cross-sprint risks and contract interlocks in decisions/risks sections. +- Confirm status discipline stays `TODO -> DOING -> DONE/BLOCKED`. + +Completion criteria: +- [x] Execution log entries added across related sprints. +- [x] Cross-sprint risks documented with mitigation owners. +- [x] Status fields remain compliant with sprint discipline rules. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-02-26 | Sprint created to complete advisory hygiene and translation workflow for current open advisory set. | Product Manager | +| 2026-02-26 | Resolved duplicate tile-verification advisory by preserving the `20260226` canonical entry and archiving the `20260224` duplicate with explicit supersession notation. | Documentation author | +| 2026-02-26 | Repaired malformed auditor UX advisory with Stella-specific measurable plan content, then translated advisories into sprint/doc mappings (`docs/product/advisory-translation-20260226.md`). | Product Manager | +| 2026-02-26 | Archived translated advisories to `docs-archived/product/advisories/`, created archive audit manifest `docs-archived/product/advisories/ARCHIVE_LOG_20260303.md`, and reduced open advisories folder to tracking `README.md`. | Documentation author | +| 2026-03-03 | Completed DOCS-229-005 closure: added execution-log and cross-sprint risk updates across `SPRINT_20260226_222` through `SPRINT_20260226_228`, and revalidated status discipline (`DONE` only). | Project Manager | + +## Decisions & Risks +- Decision: malformed advisories cannot remain active planning inputs without validated Stella-specific content. +- Risk: archival without complete traceability can break audit chain; mitigate with explicit advisory-to-sprint table and archive manifest entries. Mitigation owner: Docs lead. +- Risk: duplicate advisories can cause duplicated delivery work; mitigate via canonical file and supersession rule. Mitigation owner: Product Manager. + +## Next Checkpoints +- 2026-02-27: dedup and malformed advisory resolution checkpoint. +- 2026-02-28: advisory-to-sprint traceability checkpoint. +- 2026-03-01: archival and execution-log completion checkpoint. + + diff --git a/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_230_Platform_locale_label_translation_corrections.md b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_230_Platform_locale_label_translation_corrections.md new file mode 100644 index 000000000..b29f43fd7 --- /dev/null +++ b/docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_230_Platform_locale_label_translation_corrections.md @@ -0,0 +1,56 @@ +# Sprint 20260226_230 - Platform Locale Label Translation Corrections + +## Topic & Scope +- Correct non-English locale/language label translations that currently use placeholders/transliterations (for example `Ezik`). +- Align locale selector labels in Platform translation bundles and Web fallback i18n bundles. +- Complete non-English translation coverage for shared localization bundles used by Platform/Web. +- Working directory: `src/Platform/StellaOps.Platform.WebService/`. +- Expected evidence: corrected translation JSON assets and verification scans showing placeholder removal. +- Explicit cross-module edits authorized: `src/Web/StellaOps.Web/src/i18n/`, `src/__Libraries/StellaOps.Localization/Translations/`, `src/Graph/StellaOps.Graph.Api/Translations/`, `src/Policy/StellaOps.Policy.Gateway/Translations/`, `src/Scanner/StellaOps.Scanner.WebService/Translations/`, `src/AdvisoryAI/StellaOps.AdvisoryAI.WebService/Translations/`. + +## Dependencies & Concurrency +- Depends on previously delivered locale bundle rollout in archived sprint `SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`. +- Safe to run in parallel with unrelated backend/API work; touches only translation assets. + +## Documentation Prerequisites +- `docs/modules/platform/platform-service.md` +- `docs/modules/ui/architecture.md` +- `docs/code-of-conduct/CODE_OF_CONDUCT.md` + +## Delivery Tracker + +### LOC-230-001 - Correct non-English locale/language labels in Platform and Web bundles +Status: DONE +Dependency: none +Owners: Developer / Implementer +Task description: +- Update locale selector label keys and language-settings label keys in non-English Platform `*.ui.json` bundles. +- Mirror the same corrections in Web fallback `*.common.json` locale bundles for consistent offline behavior. +- Replace placeholder/transliteration values and English leftovers with proper native-language translations. + +Completion criteria: +- [x] No `Ezik` placeholder remains in source translation bundles. +- [x] Non-English locale files no longer contain `ui.locale.uk_ua` with `Ukrainian (Ukraine)`. +- [x] Platform and Web locale/language label keys are aligned per locale. +- [x] Non-English locale bundles (`Web *.common.json`, `Platform *.ui.json`, shared localization `*.common.json`) were translated and cleaned of leaked placeholder tokens/encoding artifacts. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-02-26 | Sprint created and LOC-230-001 moved to DOING for non-English locale label corrections. | Implementer | +| 2026-02-26 | Corrected locale selector + language settings translations for all non-English Platform/Web locale bundles (`bg-BG`, `de-DE`, `es-ES`, `fr-FR`, `ru-RU`, `uk-UA`, `zh-CN`, `zh-TW`) and validated JSON parsing for all touched files. | Implementer | +| 2026-03-03 | Performed full Bulgarian translation pass for UI/common assets: translated `bg-BG` Web fallback bundle, Platform UI bundle, Platform namespace bundle, and shared localization `common/auth` keys; verified placeholder/transliteration removal and JSON validity. | Implementer | +| 2026-03-03 | Completed non-English translation pass across remaining locales (`de-DE`, `es-ES`, `fr-FR`, `ru-RU`, `uk-UA`, `zh-CN`, `zh-TW`) for Web fallback/common, Platform UI, and shared localization bundles; repaired malformed strings (`ZXQPH*` leaks, mojibake/replacement chars), and revalidated all touched JSON files. | Implementer | +| 2026-03-03 | Applied native-quality context pass for critical UX wording (actions/status/severity/first-signal/offline labels) across Web + Platform + shared localization bundles, and aligned backend German module resources (`graph`, `policy`, `scanner`, `advisoryai`) with context-correct terminology. | Implementer | +| 2026-03-03 | Applied second native-polish pass for `ru-RU`, `uk-UA`, `zh-CN`, `zh-TW` to replace literal machine phrasing with product-native terminology (status lifecycle, action verbs, first-signal states/stages, queue/offline labels), normalized signal separators, and confirmed no placeholder artifacts remain. | Implementer | +| 2026-03-03 | Applied third native-polish pass focused on consistency/grammar: normalized Slavic status forms for neutral UI context, refined CJK progress/state phrasing, and corrected backend German umlaut usage in graph resource strings. | Implementer | +| 2026-03-03 | Verified no placeholder residue in non-English locale assets by scanning Platform/Web/shared localization bundles (`Ezik`, `Ukrainian (Ukraine)`, `ZXQPH`, replacement-character artifacts): zero non-English matches. | Test Automation | + +## Decisions & Risks +- Decision: expanded scope from selector-only fixes to full non-English bundle completion for Web/Platform/shared localization assets. +- Decision: preserve technical tokens/examples unchanged where localization would break semantics (`CVSS`, `EPSS`, `KEV`, `CVE-...`, API path examples, separators). +- Risk: automated machine translation may require future terminology refinement for domain-specific wording in some locales. Mitigation owner: Platform localization owner. +- Web fetch audit trail: used Google Translate via `deep-translator` (endpoint family: `https://translate.google.com/`) to generate missing locale strings and then post-corrected malformed/placeholder artifacts. + +## Next Checkpoints +- 2026-03-03: full non-English translation correction pass landed and validated. diff --git a/docs-archived/product/advisories/20260220 - OCI 1.1 referrers compatibility across major registries.md b/docs-archived/product/advisories/20260220 - OCI 1.1 referrers compatibility across major registries.md new file mode 100644 index 000000000..39df6dc62 --- /dev/null +++ b/docs-archived/product/advisories/20260220 - OCI 1.1 referrers compatibility across major registries.md @@ -0,0 +1,68 @@ +I’m sharing this because the current state of **OCI referrers support across major container registries** is finally *messy, uneven, and directly impacts how you implement supply‑chain artifact workflows* — which is exactly the sort of rollout gotcha worth locking down before production. + +--- + +OCI Logo + +**OCI Referrers API and Distribution Spec** — the *official* mechanism to list related artifacts by subject (digest) — is part of OCI Distribution 1.1, and spec text now clarifies fallback behavior when referrers aren’t supported (registries SHOULD return 200 on supported referrers calls; 404 or fallback tag otherwise). ([Open Container Initiative][1]) + +--- + +### GitHub Container Registry (GHCR) — Lack of referrers + +* GHCR implements OCI image formats, but **does not support the OCI referrers API endpoint** for generic discovery of artifacts by subject. +* Community threads confirm queries against `/v2//referrers/` simply *don’t work* — referrers don’t show up despite manifests being present. ([GitHub][2]) +* The practical implication: you **must use GitHub’s specific Attestations/Policy APIs** or fetch artifacts by *known SHA* if you need to extract provenance generically across registries. + +--- + +### AWS Elastic Container Registry (ECR) — OCI 1.1 support + +* AWS has documented support for the OCI Image & Distribution 1.1 specs, including image referrers, SBOMs, signatures, and non‑image artifact distribution. ([Amazon Web Services, Inc.][3]) +* This means OCI‑aware clients (ORAS, containerd tooling) should be able to list and pull referrers by digest. +* Because referrers count toward storage and lifecycle policies, you must **include these artifact patterns explicitly in lifecycle/GC rules** to avoid unintended deletions. + +--- + +### Google Artifact Registry — attachments + listing + +* Google’s **Artifact Registry** doesn’t expose a native OCI referrers API, but instead provides an **attachments model** for metadata (SBOMs, provenance) tied to an artifact. +* You can list attachments with `gcloud artifacts attachments list` and via console UI — this acts as a *referrers‑like* listing UX, not necessarily the standard OCI endpoint. ([Google Cloud Documentation][4]) +* IAM roles (`artifactregistry.reader/writer/repoAdmin`) govern access, and some features are in **public preview**, especially for non‑Docker formats. + +--- + +### Red Hat Quay / Quay.io — OCI referrers implemented + +* Quay has publicly announced support for the **OCI Referrers API**, enabling clients to query relationships by subject and artifactType. ([Red Hat][5]) +* This aligns them with OCI 1.1 workflows and enables structured discovery of SBOMs/signatures via the native API. +* Watch for deployment flags or admin toggles that control this feature — misconfiguration can make discovery inconsistent. + +--- + +### Harbor (goharbor) — evolving but partial + +* Harbor’s backend can *store OCI artifacts*, but its implementation of referrers has historically lagged UI classification: cosign or SBOM referrers may be accepted at the API level while displayed as “UNKNOWN” in UI. ([GitHub][6]) +* Active work overall is on **improving OCI 1.1 support**, but **UI behavior and mediaType classification may still lag the API capabilities** in releases around v2.15+. + +--- + +### Spec & ecosystem nuance (broad patterns) + +* OCI Distribution 1.1 spec updates explicitly require registries that *support* referrers to return successful responses for referrers endpoints; otherwise clients should *fall back* to tag‑by‑digest conventions. ([Open Container Initiative][1]) +* Because referrers support is optional and not universal yet, **generic clients must detect support at runtime** and fallback where needed (e.g., use ORAS discovery`oras discover --distribution-spec v1.1-referrers-api`). ([Google Cloud Documentation][4]) +* Many registries (e.g., Docker Hub, GitLab) can store OCI artifacts with `subject` fields, *but won’t expose them via a referrers endpoint* — making discovery tooling reliant on owner‑specific APIs or naming conventions. ([GitLab Docs][7]) + +--- + +The bottom line is that **registry choice still shapes your artifact discovery strategy** — and until OCI Referrers becomes widely implemented, robust implementations need *fallback tagging, dual discovery flows, SCM‑specific API calls, and careful lifecycle/permission planning*. + +Let me know if you want a **compact matrix or scriptable detection and fallback strategy** tailored for vendors like ECR, GHCR, GCR/Artifact Registry, Quay, and Harbor. + +[1]: https://opencontainers.org/posts/blog/2024-03-13-image-and-distribution-1-1/?utm_source=chatgpt.com "OCI Image and Distribution Specs v1.1 Releases" +[2]: https://github.com/orgs/community/discussions/163029?utm_source=chatgpt.com "How do I find the provenance which is pushed to ghcr.io ..." +[3]: https://aws.amazon.com/blogs/opensource/diving-into-oci-image-and-distribution-1-1-support-in-amazon-ecr/?utm_source=chatgpt.com "Diving into OCI Image and Distribution 1.1 Support in ..." +[4]: https://docs.cloud.google.com/artifact-registry/docs/manage-metadata-with-attachments?utm_source=chatgpt.com "Manage artifact metadata | Artifact Registry" +[5]: https://www.redhat.com/en/blog/announcing-open-container-initiativereferrers-api-quayio-step-towards-enhanced-security-and-compliance?utm_source=chatgpt.com "Announcing the Open Container InitiativeReferrers API on ..." +[6]: https://github.com/goharbor/harbor/wiki/Architecture-Overview-of-Harbor?utm_source=chatgpt.com "Architecture Overview of Harbor · goharbor/harbor Wiki" +[7]: https://docs.gitlab.com/user/packages/container_registry/?utm_source=chatgpt.com "GitLab container registry" diff --git a/docs-archived/product/advisories/20260221 - Building a verifiable SBOM and attestation spine.md b/docs-archived/product/advisories/20260221 - Building a verifiable SBOM and attestation spine.md new file mode 100644 index 000000000..958d93132 --- /dev/null +++ b/docs-archived/product/advisories/20260221 - Building a verifiable SBOM and attestation spine.md @@ -0,0 +1,231 @@ +Here’s a compact, end‑to‑end blueprint for making your SBOM evidence verifiable and auditable from build → attest → publish → verify → runtime, using only official specs/CLIs (CycloneDX, DSSE/in‑toto, cosign, Rekor v2), plus deterministic VEX and micro‑witness joins. + +--- + +# Why this matters (quick primer) + +* **SBOM** (software bill of materials) says what’s inside an artifact. +* **DSSE / in‑toto** wraps evidence in a tamper‑evident envelope. +* **cosign** signs/verifies those envelopes. +* **Rekor (transparency log)** proves *when/what* was published. +* **VEX** states whether known CVEs affect your artifact. +* **Micro‑witnesses** are runtime breadcrumbs you can join back to SBOM/VEX for triage. + +--- + +# NOW (MVP you can ship immediately) + +**Goal:** Minimal, reproducible spine with canonical SBOMs, signed attestations, Rekor v2 anchoring, and deterministic VEX ingestion. + +### Canonical SBOM & ID + +1. Emit CycloneDX v1.7 JSON. +2. Canonicalize via JSON‑JCS → `canonical_id := sha256(JCS(sbom.json))`. + +### DSSE attestation (SBOM predicate) + +* Wrap CycloneDX content (or pointer) in an **in‑toto Statement**. +* `subject[0].digest.sha256 == canonical_id`. +* Sign with cosign (**DSSE mode**). +* Capture Rekor v2 tile pointer / entry id and embed it in predicate metadata. + +### Rekor v2 anchoring + +* cosign publish creates the log entry. +* Store the **tile URL** and **entry id** in your artifact record. + +### Deterministic VEX ingestion (OpenVEX & CycloneDX VEX) + +* Ingest OpenVEX or CycloneDX VEX, map to a **canonical CycloneDX VEX** form. +* Apply strict merge rules (source priority, timestamp, exact `canonical_id` target). + +### CI assertions (must‑pass) + +* **Unit:** `cyclonedx-cli validate ./bom.json && jcs_canonicalize ./bom.json | sha256sum` → equals expected `canonical_id`. +* **Integration:** + `cosign attest --predicate predicate.json --key cosign.key ` + `cosign verify-attestation --key --type in-toto ` + Rekor proof: `rekor-cli tile get --entry ` (or v2 tiles client) → inclusion proof valid. +* **VEX mapping:** jsonschema validate; assert `vulnerabilities[].analysis.state` and target match `canonical_id`. + +--- + +# LATER (scale/hardening) + +* Rekor v2 **tile batching** & multi‑tile reconciliation. +* **Signed micro‑witness aggregation** service. +* **Deterministic replay** harness (nightly) for large volumes. +* Predicate **schema registry** (stable `predicateType` URIs, semver). + +--- + +# Flow A: Build‑time SBOM + Attestation Anchor (end‑to‑end) + +1. **Producer:** + + * CycloneDX v1.7 → validate: `cyclonedx-cli validate ./bom.json`. + * JCS canonicalize → `canonical_id = sha256(canonical_bytes)`. + +2. **Predicate:** + + * in‑toto Statement (`predicateType = https://stella.example/predicate/sbom.v1` or SLSA provenance). + * `subject = [{"name":"artifact","digest":{"sha256":""}}]`. + * `predicate` contains CycloneDX JCS content (or immutable pointer if too large). + +3. **Sign & anchor:** + + * `cosign attest --predicate predicate.json --key cosign.key `. + * Capture Rekor **tile URL** and **entry_id**; persist inside predicate metadata. + +4. **Verify:** + + * Fetch referrers (OCI Referrers 1.1), extract DSSE. + * `cosign verify-attestation --key --type in-toto `. + * Recompute `sha256(JCS(sbom.json))` == `subject.digest.sha256`. + * Validate Rekor v2 inclusion proof (tile check). + +--- + +# Flow B: VEX → Apply → Runtime Micro‑witness Join + +1. **VEX provider:** Publish **OpenVEX** or **CycloneDX VEX**; DSSE‑sign; optionally anchor in Rekor. + +2. **Ingest & map:** OpenVEX → canonical CycloneDX VEX; store DSSE + `rekor_tile` + provenance to original feed. + +3. **Runtime micro‑witness:** + + * When observing a frame (`pc → build_id → symbol`), emit **DSSE micro‑witness predicate** referencing: + + * `subject.canonical_id` + * symbol bundle (OCI referrer, `mediaType=application/vnd.stella.symbols+tar`) + * `replay_token` (points to deterministic replay inputs) + * Sign & anchor to Rekor v2. + +4. **Correlate:** Join micro‑witness DSSE with VEX: + + * If `canonical_id` has `not_affected` for CVE X → auto‑suppress triage. + * Else surface evidence and export both DSSEs + Rekor tiles in an **audit pack**. + +--- + +# Minimal, unambiguous schemas + +**1) Artifact canonical record** + +```json +{"canonical_id":"sha256:","format":"cyclonedx-jcs:1", + "sbom_ref":"oci://registry/repo@sha256:|object-store://bucket/path", + "attestations":[{"type":"in-toto","dsse_b64":"","rekor_tile":"","entry_id":""}], + "referrers":[{"mediaType":"application/vnd.stella.symbols+tar","descriptor_digest":"sha256:","registry":"ghcr.io/org/repo"}], + "vex_refs":[{"type":"openvex","dsse_b64":"","rekor_tile":""}]} +``` + +**2) Minimal DSSE / in‑toto predicate (for SBOM)** + +```json +{"_type":"https://in-toto.io/Statement/v0.1", + "subject":[{"name":"artifact","digest":{"sha256":""}}], + "predicateType":"https://stella.example/predicate/sbom.v1", + "predicate":{"bom_format":"cyclonedx-jcs:1","bom":"", + "producer":{"name":"ci-service","kid":""}, + "timestamp":"2026-02-19T12:34:56Z", + "rekor_tile":"https://rekor.example/tiles/#"}} +``` + +**3) OpenVEX → CycloneDX VEX mapping (sample OpenVEX)** + +```json +{"vexVersion":"1.0.0","id":"vex-123", + "statements":[{"vulnerability":"CVE-2025-0001", + "product":"pkg:maven/org/example@1.2.3", + "status":"not_affected","justification":"code_not_present", + "timestamp":"2026-02-19T12:00:00Z","provider":{"name":"vendor"}}]} +``` + +**CycloneDX SBOM (trimmed example)** + +```json +{"bomFormat":"CycloneDX","specVersion":"1.7", + "components":[{"bom-ref":"pkg:maven/org/example@1.2.3", + "type":"library","name":"example","version":"1.2.3", + "purl":"pkg:maven/org/example@1.2.3"}]} +``` + +**DSSE envelope (shape)** + +```json +{"payloadType":"application/vnd.in-toto+json", + "payload":"", + "signatures":[{"keyid":"cosign:ecdsa-1","sig":""}]} +``` + +**Micro‑witness predicate (sample)** + +```json +{"predicateType":"https://stella.example/micro-witness.v1", + "subject":[{"name":"artifact","digest":{"sha256":""}}], + "predicate":{"trace_id":"trace-abc", + "frames":[{"pc":"0x400123","build_id":"","symbol":"main","offset":123}], + "symbol_refs":[{"type":"oci-ref","ref":"ghcr.io/org/repo@sha256:", + "mediaType":"application/vnd.stella.symbols+tar"}], + "replay_token":"s3://bucket/replays/trace-abc.json", + "timestamp":"2026-02-19T12:40:00Z"}} +``` + +--- + +# CI: authoritative checks (copy‑pasteable) + +**Unit (canonicalization):** + +```bash +cyclonedx-cli validate ./bom.json +jcs_canonicalize ./bom.json | sha256sum # == canonical_id +``` + +**Integration (sign + verify + Rekor):** + +```bash +cosign attest --predicate predicate.json --key cosign.key +cosign verify-attestation --key --type in-toto +rekor-cli tile get --entry # validate inclusion proof +``` + +**VEX mapping:** + +* Convert OpenVEX → CycloneDX VEX; jsonschema validate. +* Assert: `vulnerabilities[].analysis.state` and target `canonical_id` are exact. + +**E2E (nightly, reproducibility POC):** + +```bash +stella-replay --token --seed # same signed_score DSSE & same Rekor tile id +``` + +**Deterministic signed score (gate):** + +* Given `(seed, canonical_id, evidence_ids[])` → produce `signed_score_dsse`. +* `cosign verify-attestation` and recompute score → byte‑exact equality. + +--- + +# Failure modes (and what to do) + +* **Payload > Rekor limit:** Put artifact in immutable object store; Rekor entry contains digest + pointer; embed pointer in predicate; verifier must fetch & hash‑check. +* **Missing symbol bundle:** Emit `unknown_state` micro‑witness entry; surface unknowns explicitly in reports. +* **Digest mismatch anywhere:** fail the build—no exceptions. + +--- + +# Two concrete sample flows (ready to demo in Stella Ops) + +* **Flow A**: Canonical CycloneDX → DSSE sign (cosign) → Rekor v2 tile captured → Verify via referrers + DSSE + tile proof. +* **Flow B**: Ingest OpenVEX → Map → Apply to `canonical_id` → Runtime micro‑witness DSSE → Join with VEX (`not_affected` ⇒ auto‑suppress), export audit pack containing both DSSEs + Rekor tiles. + +--- + +If you want, I can turn this into: + +* a **CI job pack** (GitLab/YAML) with all commands and assertions, +* minimal **JSON schemas** for the predicates, +* a **sample repo** with fixtures (SBOM, predicates, signed envelopes) you can run locally with cosign/Rekor. diff --git a/docs-archived/product/advisories/20260221 - Four novel, testable moat hypotheses.md b/docs-archived/product/advisories/20260221 - Four novel, testable moat hypotheses.md new file mode 100644 index 000000000..704118045 --- /dev/null +++ b/docs-archived/product/advisories/20260221 - Four novel, testable moat hypotheses.md @@ -0,0 +1,106 @@ +Here are **four fast, defensible moat experiments** you can add to Stella Ops and validate in 1–2 sprints—each with a crisp pass/fail and public references. + +--- + +### 1) Function‑level **semantic fingerprints** (“semhash”) + +**Why:** resilient artifact identity across rebuilds/optimizations; raises code‑reuse evasion cost. +**Signals to measure:** + +* ≥80% within‑version function match across gcc/clang and -O0/-O2/-Os; +* <1% cross‑project false positives vs ~10k‑function corpus; +* Robust to symbol‑stripping & minor reordering. + **2‑sprint plan:** +* A: Extract per‑function IL via DWARF, normalize, embed/hash; prototype store & query. ([Dwarfstd][1]) +* B: CI job builds 3 variants, computes semhash diffs; publish report. + **Single‑card experiment:** CI asserts ≥80% functions cluster to same semhash; attach diffoscope snapshot for mismatches. ([Diffoscope][2]) + **Grounding:** KEENHash (function‑aware hashing); DWARF v5 spec; diffoscope latest (v312, 2026‑02‑06). ([arXiv][3]) + +--- + +### 2) **Deterministic short‑run behavior attestations** (sandbox traces) + +**Why:** converts “it executed” into a cryptographically verifiable exploitability signal; complements VEX. +**Signals:** + +* > 99% identical syscall/observable trace on repeated runs in hermetic harness; +* Variance across different inputs; +* Capture <10s, replay <10s. + **2‑sprint plan:** +* A: Record a containerized micro‑handler under Firecracker (or gVisor) using `rr`; wrap trace as DSSE; sign with cosign. ([Amazon Web Services, Inc.][4]) +* B: Verifier replays trace (rr) and checks DSSE + Rekor pointer. ([RR Project][5]) + **Single‑card experiment:** Build+run a <1s handler, emit signed trace, store Rekor pointer, run verifier → PASS only if replay+verify succeed. ([GitHub][6]) + +--- + +### 3) **Dual‑log “twin‑proof” stitching** (multi‑log anchoring + witness) + +**Why:** attacker must tamper with two independent logs; stronger story for procurement/legal. +**Signals:** + +* Same DSSE digest appears in **Rekor v2** and a second append‑only log (e.g., signed Git tag); +* Consistency/witness checks detect divergence; +* Measurable increased attack cost vs single‑log. + **2‑sprint plan:** +* A: Write attestation to Rekor v2 and to signed‑tag Git “proofs” repo; record both pointers in DSSE. ([Sigstore Blog][7]) +* B: Verifier fetches Rekor inclusion proof + Git tag sig; PASS only if both validate. ([Sigstore Blog][8]) + **Single‑card experiment:** Produce one DSSE, anchor to both logs, run verifier → PASS iff both proofs verify. ([Sigstore Blog][9]) + +--- + +### 4) **Attestable runtime canary beacons** + +**Why:** low‑volume evidence that a specific artifact actually ran in a real env—without shipping raw telemetry. +**Signals:** + +* Signed beacon ⟨artifact_id, nonce, timestamp⟩ verified against cosign key + Rekor pointer; +* > 90% beacon verification rate in staged infra; +* Origin/IP/arrival cadence provide internal execution evidence. + **2‑sprint plan:** +* A: Embed a one‑shot beacon emitter (Go) at entrypoint; post DSSE to a small collector over mTLS; sign + anchor. ([GitHub][6]) +* B: Collector verifies sig + Rekor, stores events; expose query API; (optionally align with OTel signals). ([Canarytokens][10]) + **Single‑card experiment:** Run the binary once in staging → collector shows verified DSSE + Rekor pointer. ([GitHub][6]) + +--- + +### Where this slots into **Stella Ops** + +* **Evidence Locker:** store semhashes, traces, dual‑anchors, and beacons as first‑class DSSE records. +* **Attestor:** add “sandbox‑trace.verify()” and “twin‑proof.verify()” checks to your policy engine. +* **AdvisoryAI:** surface investor‑friendly KPIs: semhash stability %, trace‑replay PASS rate, dual‑anchor PASS rate, beacon verification %. +* **Release Orchestrator:** make these jobs optional gates per environment. + +--- + +### Acceptance criteria (quick) + +* **Semhash:** ≥80% stable across two compiler flags; <1% FP vs 10k corpus. ([arXiv][3]) +* **Sandbox traces:** rr replay PASS + DSSE verify + Rekor pointer in CI. ([RR Project][5]) +* **Twin‑proof:** verifier fails if either Rekor or Git proof missing. ([Sigstore Blog][9]) +* **Beacons:** ≥90% verified beacons from staged runs. ([Canarytokens][10]) + +--- + +### Primary links (for your sprint tickets) + +* KEENHash (ISSTA 2025): arXiv/DOI & PDF. ([arXiv][3]) +* DWARF v5 standard. ([Dwarfstd][1]) +* diffoscope v312 (2026‑02‑06). ([Diffoscope][2]) +* Firecracker intro/background. ([Amazon Web Services, Inc.][4]) +* rr project docs. ([RR Project][5]) +* cosign (DSSE/in‑toto attestations) & Rekor v2 (alpha→GA). ([GitHub][6]) +* Canarytokens guide/admin (for beaconing patterns). ([Canarytokens][11]) + +If you want, I can draft the **four CI job cards** (Makefile targets + sample DSSE predicates + policy checks) sized for a two‑sprint push. + +[1]: https://dwarfstd.org/dwarf5std.html?utm_source=chatgpt.com "DWARF Version 5" +[2]: https://diffoscope.org/?utm_source=chatgpt.com "diffoscope: in-depth comparison of files, archives, and ..." +[3]: https://arxiv.org/abs/2506.11612?utm_source=chatgpt.com "KEENHash: Hashing Programs into Function-Aware Embeddings for Large-Scale Binary Code Similarity Analysis" +[4]: https://aws.amazon.com/blogs/aws/firecracker-lightweight-virtualization-for-serverless-computing/?utm_source=chatgpt.com "Firecracker – Lightweight Virtualization for Serverless ..." +[5]: https://rr-project.org/?utm_source=chatgpt.com "rr: lightweight recording & deterministic debugging" +[6]: https://github.com/sigstore/cosign?utm_source=chatgpt.com "sigstore/cosign: Code signing and ..." +[7]: https://blog.sigstore.dev/rekor-v2-alpha/?utm_source=chatgpt.com "Rekor v2 - Cheaper to run, simpler to maintain" +[8]: https://blog.sigstore.dev/?utm_source=chatgpt.com "Sigstore Blog - Sigstore Blog" +[9]: https://blog.sigstore.dev/rekor-v2-ga/?utm_source=chatgpt.com "Rekor v2 GA - Cheaper to run, simpler to maintain" +[10]: https://docs.canarytokens.org/guide/getting-started.html?utm_source=chatgpt.com "Getting Started" +[11]: https://docs.canarytokens.org/guide/?utm_source=chatgpt.com "Introduction" diff --git a/docs-archived/product/advisories/20260222 - Fuzz & mutation hardening suite.md b/docs-archived/product/advisories/20260222 - Fuzz & mutation hardening suite.md new file mode 100644 index 000000000..2ddf08866 --- /dev/null +++ b/docs-archived/product/advisories/20260222 - Fuzz & mutation hardening suite.md @@ -0,0 +1,233 @@ +Here’s a practical, copy‑pasteable test plan to harden your diff‑scanner, DSSE/verifier, and Rekor paths—plus just enough background so it’s clear why each matters. + +--- + +# Why these tests? + +* **Canonicalization is brittle.** JSON key order, whitespace, duplicate keys and Unicode normalization can silently change digests. +* **Attestation chains fail in weird ways.** Rekor and DSSE failure modes (oversize payloads, unknown entry types, ledger gaps) must return deterministic errors for reliable pipelines. +* **Real SBOMs are messy.** Schema‑valid ≠ safe; fuzzing and mutation catch parser edges that schema validation misses. + +--- + +# Repo layout (drop in as `/tests/supply-chain/`) + +``` +tests/supply-chain/ + 01-jcs-property/ + 02-schema-fuzz/ + 03-rekor-neg/ + 04-big-dsse-referrers/ + 05-corpus/ + tools/ +``` + +--- + +# 0) Common tooling (Python + Go) + +``` +pip install hypothesis hypothesis-jsonschema schemathesis jsonschema +# optional native fuzzers +# apt-get install -y afl++ radamsa +``` + +Place helpers in `tools/`: + +* `canon_diff.py` – runs your canonicalizer then diffs (`canonical_diff.patch` output). +* `emit_artifacts.py` – writes `failing_case.json`, `hypothesis_seed.txt`, `junit.xml`. +* `rekor_shim.py` – lightweight Flask/FastAPI shim that mimics Rekor error modes. +* `oci_referrer_mutator.go` – mutates OCI referrers/mediaTypes. + +--- + +# 1) JCS property tests (seeded by CycloneDX schema) + +**Goal:** prove canonicalization invariants. +**Invariants:** idempotence; permutation equality; reject duplicate keys. + +**Implementation sketch (`tests/supply-chain/01-jcs-property/test_jcs.py`):** + +```python +from hypothesis import given, settings, note +from hypothesis_jsonschema import from_schema +import json, pathlib, subprocess, os, random + +SCHEMA = json.load(open("schemas/cyclonedx-1.5.json")) +JUNIT = pathlib.Path("tests/supply-chain/01-jcs-property/junit.xml") + +@given(from_schema(SCHEMA)) +@settings(max_examples=200) +def test_canonicalization(model): + # 1) randomize key order + blob = json.dumps(model, separators=(",", ":"), sort_keys=False) + # 2) run your canonicalizer twice + c1 = subprocess.check_output(["./bin/diff-scanner","--canon","-"], input=blob.encode()) + c2 = subprocess.check_output(["./bin/diff-scanner","--canon","-"], input=c1) + assert c1 == c2, "Idempotence failed" + + # 3) ensure permutations hash equal + shuffled = json.dumps(model, separators=(",", ":"), sort_keys=False) + c3 = subprocess.check_output(["./bin/diff-scanner","--canon","-"], input=shuffled.encode()) + assert c1 == c3, "Permutation equality failed" +``` + +**On failure:** write `failing_case.json`, `hypothesis_seed.txt`, `canonical_diff.patch`, and append to `junit.xml` (use `emit_artifacts.py`). + +**Artifacts:** +`01-jcs-property/{failing_case.json,hypothesis_seed.txt,junit.xml,canonical_diff.patch}` + +--- + +# 2) Schema‑aware fuzzing (CycloneDX + in‑toto) + +**Goal:** crash‑free, memory‑safe parsing & diff logic. + +* **JSON fuzz (property + mutational):** + + * Hypothesis (schema‑seeded) → parser/diff + * Radamsa/AFL++ mutate “golden” SBOMs + +* **API fuzz (if you expose an SBOM/attestation API):** + + * Schemathesis → OpenAPI/JSON Schema guided. + +**Layout & gate:** + +``` +02-schema-fuzz/ + corpus/golden/ # 10–20 known‑good SBOMs & attestations + corpus/mutated/ # generated + sanitizer_reports/ + crash_traces/ + repro_playbook.md +``` + +**CI smoke gate:** run 60s or 1000 mutations (whichever first) → **no crashes / no sanitizer errors**. + +--- + +# 3) Negative Rekor & attestation fault injection + +**Goal:** deterministic failures & rich diagnostics. + +Simulate via `rekor_shim.py`: + +* Oversized attestations (e.g., 25–200 MB) +* Unsupported `entryType` +* HTTP responses: 413, 424, 504 +* Ledger gap window + +**Verifier requirements:** + +* Return stable codes: `424 failed-dependency` or `202` **with** `reprocess_token` +* Attach `diagnostic_blob.json` (include upstream status, body, and correlation IDs) + +**Artifacts:** `03-rekor-neg/rekor_negative_cases.tar.gz` + +--- + +# 4) Big‑payload DSSE & OCI referrer edge cases + +**Goal:** graceful reject + auditable state. + +Cases: + +* DSSE payloads 100 MB, 250 MB, 1 GB (streamed where possible) +* Malformed OCI referrers (dangling, invalid `mediaType`, cycles) +* Missing symbol bundles + +**Expected behavior:** + +* Reject cleanly +* Emit DSSE predicate `unknown_state` +* Append audit line with `reprocess_token` + +**Artifacts:** `04-big-dsse-referrers/big_dsse_payloads.tar.gz` + +--- + +# 5) Mutation corpus & fixtures for canonicalization exploits + +**Goal:** permanent regression shield. + +Include: + +* **SBOMs:** 50 small, 30 medium (100–500 KB), 20 large (1–50 MB) +* VEX variants & attestation‑only samples +* Malformed JSON (duplicate keys, Unicode normalization forms) +* Symbol‑bundle stubs +* Tricky dependency graphs (diamond, deep chains, cycles) + +**Bundle:** `tests/supply-chain/05-corpus/fixtures/fuzz-corpus-v1.tar.gz` + +--- + +# Makefile (one‑command runs) + +```make +.PHONY: supplychain test smoke fuzz rekor big + +supplychain: test + +test: +\tpytest -q tests/supply-chain/01-jcs-property --junitxml=tests/supply-chain/01-jcs-property/junit.xml + +smoke: +\tpython tests/supply-chain/02-schema-fuzz/run_mutations.py --limit 1000 --time 60 + +fuzz: +\tpython tests/supply-chain/02-schema-fuzz/run_mutations.py --time 600 + +rekor: +\tpython tests/supply-chain/03-rekor-neg/rekor_shim.py & +\t./bin/verifier --rekor-url http://127.0.0.1:8000 run-negative-suite + +big: +\tpython tests/supply-chain/04-big-dsse-referrers/run_big_cases.py +``` + +--- + +# CI wiring (example GitHub Actions job) + +```yaml +name: supply-chain-hardening +on: [push, pull_request] +jobs: + smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: { python-version: '3.12' } + - run: pip install -r tests/requirements.txt + - run: make test + - run: make smoke + - run: make rekor + - run: make big + timeout-minutes: 15 + env: + HYPOTHESIS_PROFILE: ci +``` + +--- + +# Pass/Fail gates (put in README) + +* **JCS property tests:** 0 failures; artifacts emitted on failure. +* **Fuzz smoke (CI):** 1,000 mutations / 60s → 0 crashes, 0 ASAN/UBSAN reports. +* **Rekor negative suite:** All cases return deterministic code + `diagnostic_blob.json`. +* **Big DSSE/referrers:** All oversized/malformed inputs → graceful reject + `unknown_state` + `reprocess_token`. +* **Corpus:** New crashes must add a minimized repro to `fixtures/` before merge. + +--- + +# Quick start checklist + +* [ ] Add CycloneDX & in‑toto schemas under `schemas/`. +* [ ] Wire `./bin/diff-scanner` and `./bin/verifier` into tests. +* [ ] Commit initial `fixtures/fuzz-corpus-v1.tar.gz`. +* [ ] Enable CI job above and enforce as a required check. + +If you want, I can generate the skeleton files (tests, helpers, Makefile, CI YAML) tailored to your repo paths. diff --git a/docs-archived/product/advisories/20260223 - Auditor UX experiments measurement plan.md b/docs-archived/product/advisories/20260223 - Auditor UX experiments measurement plan.md new file mode 100644 index 000000000..bcc87e1b4 --- /dev/null +++ b/docs-archived/product/advisories/20260223 - Auditor UX experiments measurement plan.md @@ -0,0 +1,79 @@ +# Auditor UX Experiments: Measurement Plan (Repaired) + +## Problem Statement + +Auditors can verify evidence, but they still lose time on three friction points: + +1. Slow navigation from finding to signed proof chain. +2. Ambiguous failure states when verification is unavailable. +3. Missing deterministic replay context in triage. + +## Scope + +This advisory defines measurable UI experiments for auditor workflows in `Security -> Triage`, `Security -> Findings`, and `Security -> Risk`. + +## Experiments + +### EXP-AUD-001 - Evidence Pill Resolution Latency + +Hypothesis: +- Exposing proof status and verification reason directly in evidence pills reduces time-to-confidence. + +Implementation notes: +- Show explicit states: `verified`, `pending`, `unavailable`, `failed`. +- Always show deterministic reason text for non-verified states. + +Primary metrics: +- P50 time to first proof verdict under 15 seconds. +- P95 time to first proof verdict under 45 seconds. + +Acceptance criteria: +- At least 80% of trial sessions complete proof review without leaving triage workspace. + +### EXP-AUD-002 - Quick-Verify Action Reliability + +Hypothesis: +- A first-class Quick-Verify action with deterministic result payload lowers auditor retries. + +Implementation notes: +- Quick-Verify must return consistent status envelope. +- `Why unavailable` path must include actionable remediation text. + +Primary metrics: +- Verification success response rate >= 99% for valid fixtures. +- Retry rate below 5% for deterministic fixtures. + +Acceptance criteria: +- Zero ambiguous UI states (no generic unknown/error labels in audited paths). + +### EXP-AUD-003 - Score Explainability Readability + +Hypothesis: +- Integrating score history and breakdown in findings reduces manual evidence lookup. + +Implementation notes: +- Display score pill, breakdown, and history chart in findings detail view. +- Keep chart and breakdown data sourced from API responses only. + +Primary metrics: +- 90% of participants can explain score movement in under 60 seconds. +- 0 synthetic/demo fallback content in production route coverage. + +Acceptance criteria: +- Re-enabled E2E suites pass for score and risk widgets. + +## Data Collection Plan + +- Capture experiment logs with UTC timestamps. +- Record lane, widget, and action identifiers for each interaction. +- Export deterministic run summaries under CI artifacts. + +## Advisory-to-Sprint Translation + +- `SPRINT_20260226_227_FE_triage_risk_score_widget_wiring_and_parity.md` +- `SPRINT_20260226_229_DOCS_advisory_hygiene_dedup_and_archival_translation.md` + +## Status + +- Translation complete as of 2026-03-03 (UTC). +- Ready for archival after sprint trackers are updated. diff --git a/docs-archived/product/advisories/20260223 - Unified symbolization across platforms and vendors.md b/docs-archived/product/advisories/20260223 - Unified symbolization across platforms and vendors.md new file mode 100644 index 000000000..e2d1094f4 --- /dev/null +++ b/docs-archived/product/advisories/20260223 - Unified symbolization across platforms and vendors.md @@ -0,0 +1,113 @@ +Here’s a practical, cross‑platform way to make “symbols just work” across Windows (PDB), Apple platforms (dSYM/DWARF), and Linux (ELF + build‑id), with security and CDN‑friendliness baked in. + +--- + +### Why symbols matter (super short) + +* **Windows:** debuggers fetch **.pdb** by PDB **GUID+Age** via symbol servers (SRV/HTTP). ([Microsoft Learn][1]) +* **Apple:** crash/symbolication uses **dSYM** bundles matched by **Mach‑O UUID** (`dwarfdump --uuid`). ([Apple Developer][2]) +* **Linux:** tools fetch debuginfo by **ELF build‑id** (HTTP **debuginfod**). ([Sourceware][3]) + +--- + +### The unified “symbol‑bundle” contract + +**Key idea:** One manifest per build that maps the platform identifier → content‑addressed blobs. + +* **Identifiers** + + * Windows: `{pdb_guid, age}` → blobs. (DIA/SymSrv semantics) ([Microsoft Learn][4]) + * Apple: `{macho_uuid}` → dSYM DWARF payload. (verified via `dwarfdump --uuid`) ([Apple Developer][2]) + * Linux: `{elf_build_id}` → debuginfo/source via HTTP. ([Sourceware][3]) + +* **Manifest (per bundle)** + + * `platform`: windows|apple|linux + * `id`: PDB GUID+Age | Mach‑O UUID | ELF build‑id + * `objects`: list of content‑addressed parts (e.g., `sha256://...`) with sizes and optional ranges (to support progressive fetch) + * `sources`: optional redacted source paths map (to hide internal paths) + * `signatures`: DSSE/attestations for integrity + * `hints`: e.g., `func_ranges`, `cu_index` to enable partial loads + +* **Storage layout** + + * **Content‑addressed** blobs in a TLS‑only bucket/CDN; manifests are tiny JSON. + * HTTP range‑friendly so debuggers can grab just what they need (great for huge DWARF). + * Publish **short‑lived signed URLs** only (no anonymous long‑term links). + +* **Lookup endpoints** + + * `GET /v1/symbols/windows/{guid}/{age}` → manifest + * `GET /v1/symbols/apple/{uuid}` → manifest + * `GET /v1/symbols/linux/{buildid}` → manifest + Mirrors the native expectations: SRV/HTTP for PDB, UUID for dSYM, build‑id for ELF. ([Microsoft Learn][5]) + +--- + +### Performance & size budgets + +* **Progressive fetch:** serve index first, defer big DWARF sections (target ~100–200 MB per bundle; more is OK but lazy‑load). +* **Local cache pinning:** respect debugger caches (SRV path semantics on Windows; keep a local debuginfod cache on Linux/macOS). ([Microsoft Learn][6]) + +--- + +### Privacy & security + +* **TLS‑only**, **short‑lived URLs**, **no directory listings**. +* **Source‑path redaction** in manifests; optionally omit source entirely and ship “public symbols” (strIPPED) where policy demands. (Windows symbol server supports partial/public symbols patterns.) ([Microsoft Learn][7]) + +--- + +### Tooling hooks you already know + +* **Windows:** Keep SRV compatibility so WinDbg/VS can consume via `srv**https://your.cdn/symbols`. ([Microsoft Learn][6]) +* **Apple:** Ensure dSYM ↔ binary **UUID match** (`dwarfdump --uuid`) before upload; symbolication tools rely on that. ([Apple Developer][2]) +* **Linux:** Stand up **debuginfod** against your blob store index; clients (gdb, perf, systemd‑coredump, etc.) auto‑fetch by build‑id. ([Sourceware][3]) + +--- + +### eBPF “micro‑witness” interop (optional but useful) + +* Emit tiny signed records: `{addr, platform_id, function|cu_hint}` to speed post‑mortems and flamegraphs while letting the symbol service hydrate details lazily. + +--- + +### Integration test matrix (keep you honest) + +Seeded replay suites that verify: + +* Correct file chosen per **GUID+Age / UUID / build‑id**. ([Microsoft Learn][1]) +* Mismatch injections (wrong UUID, wrong age) are detected and rejected. ([Apple Developer][2]) +* Coverage SLOs (e.g., ≥95% frames symbolicated in smoke crashes). +* CDN range requests behave; large DWARF loads don’t exceed soft limits. + +--- + +### Why this helps your stack (Stella Ops context) + +* Works the same for **.NET on Windows**, **Swift/Obj‑C on iOS/macOS**, **Go/Rust/C++ on Linux**. +* Lets you **attest** artifacts (DSSE) and keep symbol access **least‑privileged**. +* Reduces crash‑to‑signal time: prod agents can attach only IDs; backends fetch symbols on demand. + +--- + +### Pointers if you’re implementing now + +* Start by exposing **read‑only HTTP** that answers by identifier and returns a **small JSON manifest**. +* Back it with: + + * a **DIA** pass to extract `{GUID,Age}` from PDBs (Windows), ([Microsoft Learn][8]) + * a `dwarfdump --uuid` scan for dSYMs (Apple), ([Apple Developer][2]) + * an **elfutils** scan to index **build‑ids** (Linux). ([Sourceware][3]) +* Keep a **compat SRV endpoint** (reverse‑proxy that translates SRV requests to your manifest/CDN fetches) so legacy Windows tools work unchanged. ([Microsoft Learn][5]) + +If you want, I can draft the manifest schema (JSON), an ingestion CLI (Windows + Linux + macOS), and a minimal Go/ASP.NET Core service that serves the three lookups with signed‑URL redirects. + +[1]: https://learn.microsoft.com/en-us/windows/win32/debug/symbol-servers-and-symbol-stores?utm_source=chatgpt.com "Symbol Server and Symbol Stores - Win32 apps" +[2]: https://developer.apple.com/documentation/technotes/tn3178-checking-for-and-resolving-build-uuid-problems?utm_source=chatgpt.com "TN3178: Checking for and resolving build UUID problems" +[3]: https://sourceware.org/elfutils/Debuginfod.html?utm_source=chatgpt.com "elfutils debuginfod services" +[4]: https://learn.microsoft.com/en-us/visualstudio/debugger/debug-interface-access/symbols-and-symbol-tags?view=visualstudio&utm_source=chatgpt.com "Symbols and Symbol Tags - Visual Studio (Windows)" +[5]: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/http-symbol-stores?utm_source=chatgpt.com "HTTP Symbol Stores - Windows drivers" +[6]: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/symbol-path?utm_source=chatgpt.com "Configure Symbol Path: Windows Debuggers" +[7]: https://learn.microsoft.com/en-us/windows/win32/debug/using-symsrv?utm_source=chatgpt.com "Using SymSrv - Win32 apps" +[8]: https://learn.microsoft.com/en-us/visualstudio/debugger/debug-interface-access/debug-interface-access-sdk?view=visualstudio&utm_source=chatgpt.com "Debug Interface Access SDK - Visual Studio (Windows)" diff --git a/docs-archived/product/advisories/20260224 - Deterministic tile verification with Rekor v2 (superseded by 20260226).md b/docs-archived/product/advisories/20260224 - Deterministic tile verification with Rekor v2 (superseded by 20260226).md new file mode 100644 index 000000000..d4ad2d602 --- /dev/null +++ b/docs-archived/product/advisories/20260224 - Deterministic tile verification with Rekor v2 (superseded by 20260226).md @@ -0,0 +1,54 @@ +Here’s a super-practical “verifier recipe” you can drop into Stella Ops to validate supply‑chain attestations and Rekor v2 tiles end‑to‑end. + +# 0) Quick background (so the terms make sense) + +* **in‑toto Statement**: a tiny wrapper that says “this attestation is about artifact X with digest Y; the payload (predicate) is of type Z (e.g., SLSA Provenance).” ([GitHub][1]) +* **DSSE**: a signing envelope format (signatures over `payloadType` + canonicalized `payload` bytes). ([GitHub][2]) +* **Rekor v2 tiles**: transparency log moved to a **tile‑backed** layout; clients fetch tiles and compute inclusion proofs locally (no “get by index” API). ([Sigstore Blog][3]) +* **JCS**: a deterministic JSON canonicalization used when hashing/validating JSON payload bytes. ([RFC Editor][4]) + +# 1) Match the attestation to the artifact (Statement checks) + +* Load the in‑toto **Statement**. Confirm each `subject[*].name` and `subject[*].digest` matches the artifact you’re verifying. +* Assert the **`predicateType`** is recognized (e.g., `https://slsa.dev/provenance` / v0.1–v1.x) and, if present, validate **`predicate.materials`** (inputs) against expected sources. This binds “what was built” and “from what.” ([GitHub][1]) + +# 2) Verify DSSE signatures (bytes matter) + +* **Canonicalize** the DSSE *payload bytes* using DSSE’s own serialization rules; verify the DSSE signature against the signer’s public key (or Sigstore bundle chain if you use that path). ([GitHub][2]) +* Before any policy hash checks over JSON, **re‑canonicalize** the payload using **JCS** so downstream hashing is deterministic across platforms. ([RFC Editor][4]) + +# 3) Prove Rekor inclusion (tiles, not point lookups) + +* **Prefer** the `inclusion_proof` returned at upload time—store it with the bundle to avoid reconstructing later. ([GitHub][5]) +* If missing, **assemble the minimal tile set** from Rekor v2 endpoints (client‑side), then verify: + a) merkle inclusion for the entry’s leaf, and + b) the inclusion proof’s **checkpoint** against the Rekor v2 checkpoint. + Rekor v2 intentionally removed “get by index/leaf hash”; clients fetch tiles and compute proofs locally. ([Sigstore Blog][3]) + +--- + +## How to wire this into Stella Ops (concise plan) + +* **Verifier service** (AdvisoryAI/EvidenceLocker boundary): + + * Input: `{artifactPath|digest, dsseEnvelope|bundle, optional inclusion_proof}`. + * Steps: (1) Statement match → (2) DSSE verify → (3) Inclusion verify → (4) Policy (SLSA fields, materials allow‑list). +* **Policy hooks**: add simple YAML policies (e.g., allowed builders, required `materials[]` domains, min SLSA predicate version). ([SLSA][6]) +* **Caching**: persist tiles + checkpoints per tree size to avoid refetch. +* **UI**: in Release Bundle > Evidence tab show: “Subject match ✅ / DSSE sig ✅ / Inclusion ✅ (treeSize, rootHash, checkpoint origin)”. + +## Pitfalls to avoid + +* Hashing the JSON *without* JCS and getting platform‑specific diffs. (Use JCS before any JSON‑level policy hashing.) ([RFC Editor][4]) +* Treating Rekor v2 like v1 (no direct “get by index” reads; fetch tiles and compute locally). ([Sigstore Blog][3]) +* Ignoring `predicateType` evolution; track the URI (it encodes major version semantics). ([SLSA][7]) + +If you want, I can sketch a tiny .NET verifier module (interfaces + test vectors) that plugs into Stella Ops’s EvidenceLocker and uses the Rekor v2 tile client model. + +[1]: https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md?utm_source=chatgpt.com "attestation/spec/v1/statement.md at main · in-toto/attestation" +[2]: https://github.com/secure-systems-lab/dsse?utm_source=chatgpt.com "DSSE: Dead Simple Signing Envelope" +[3]: https://blog.sigstore.dev/rekor-v2-ga/?utm_source=chatgpt.com "Rekor v2 GA - Cheaper to run, simpler to maintain" +[4]: https://www.rfc-editor.org/rfc/rfc8785?utm_source=chatgpt.com "RFC 8785: JSON Canonicalization Scheme (JCS)" +[5]: https://github.com/sigstore/rekor?utm_source=chatgpt.com "sigstore/rekor: Software Supply Chain Transparency Log" +[6]: https://slsa.dev/spec/v0.1/provenance?utm_source=chatgpt.com "Provenance" +[7]: https://slsa.dev/spec/v0.1/verification_summary?utm_source=chatgpt.com "Verification Summary Attestation (VSA)" diff --git a/docs-archived/product/advisories/20260224 - Turning defensibility into measurable business moats.md b/docs-archived/product/advisories/20260224 - Turning defensibility into measurable business moats.md new file mode 100644 index 000000000..5cdfa174c --- /dev/null +++ b/docs-archived/product/advisories/20260224 - Turning defensibility into measurable business moats.md @@ -0,0 +1,100 @@ +Here’s a practical playbook to turn “technical defensibility” into measurable revenue—explained plainly and mapped to motions you can ship. + +--- + +# 6 motions that convert security rigor into ARR + +1. **Provenance ledger + signed decisions (audits that sell themselves)** + +* What it is: Canonicalize every decision artifact (use RFC 8785 JCS for stable JSON), then wrap scores/findings with DSSE signatures. +* Why it sells: Auditors/SOCs can verify evidence cryptographically; buyers feel safe to expand seats. +* Ship it: + + * “Decision Service” emits `decision.json` (JCS) + `.sig` (DSSE). + * Append immutable receipts to a lightweight ledger (SQLite/WAL → Postgres later). + * UI: “Verify” button shows green check = signature + chain proof. +* KPI: Audit pass rate, time‑to‑evidence, expansion revenue tied to compliance milestones. + +2. **Exploitability modeling → micro‑witnesses → prioritized fixes** + +* What it is: Map findings to ATT&CK + attack‑graph paths; emit tiny, human‑readable “micro‑witnesses” that prove a path exists. +* Why it sells: Security teams buy prioritization, not lists. +* Ship it: + + * For each vuln, store `(entrypoint → privilege) path` + 1‑page witness. + * Rank by “exploit path length × blast radius.” +* KPI: Mean‑time‑to‑remediation (MTTR) for top‑10 risks; % fixes driven by witnesses. + +3. **Call‑stack provenance with eBPF (runtime truth, not guesses)** + +* What it is: Trace kernel/user call stacks to bind events to exact symbols/builds. +* Why it sells: Runtime proof quiets false positives and justifies higher pricing. +* Ship it: + + * Sidecar eBPF agent captures `(symbol, hash, pid, cgroup)` and signs a short evidence blob. + * Link to SBOM entries + commit SHA. +* KPI: FP reduction, accepted fixes per sprint, “blocker to deploy” avoided. + +4. **Binary‑ecosystem function‑matching index (network effects)** + +* What it is: A shared index of function hashes ↔ symbols across builds/vendors. +* Why it sells: Each new customer improves coverage for all—compelling moat. +* Ship it: + + * Normalize to normalized‑CFG hash; store `(fn_hash → {package, version, symbol})`. + * Offer opt‑in “anonymized contribution” for discounts. +* KPI: Function coverage %, match‑time latency, upsell to “priority index” tier. + +5. **Continuous delta detection (semantic binary diffs + CI hooks)** + +* What it is: Detect *meaningful* code path changes and patch deltas on every commit/tag. +* Why it sells: Teams pay for “don’t let regressions ship” alerts with SLAs. +* Ship it: + + * Git/CI hook produces semantic diff → emits DSSE‑signed “delta receipt.” + * Alerting: “Critical path changed without test coverage.” +* KPI: Caught‑before‑prod incidents, SLA credits avoided, alert precision. + +6. **Developer UX hooks: PR/IDE micro‑witnesses + one‑click replay** + +* What it is: Put proof *inside* the PR/IDE (witness snippet + “replay locally” button). +* Why it sells: Habit loops → daily active users → land‑and‑expand. +* Ship it: + + * GitHub/GitLab check with inline witness; CLI `stella replay ` spins a container and reproduces the issue. +* KPI: DAU/WAU for extensions, replay runs per PR, conversion from pilot → paid. + +--- + +# How to package this commercially + +* **Tiers that map to risk** + + * Core: Signed decisions + deltas. + * Pro: Exploitability + eBPF provenance. + * Enterprise: Ecosystem index + auditor dashboards + SLA alerts. +* **Sales motions** + + * Compliance-led: “Cryptographically verifiable audits in <2 weeks.” + * Ops-led: “Cut MTTR 40% with micro‑witnesses and one‑click replay.” + * Platform-led: “Join the function index—better matches day one.” + +--- + +# Minimal architecture to start + +* Evidence types: `decision.jcs`, `witness.md`, `delta.yaml`, all DSSE‑signed. +* Ledger: append-only table with `(artifact_digest, signer, scope, created_at)`. +* Verifier CLI: `stella verify ` → prints trust chain + result. +* UI: Evidence Locker with filters (service, build, control) + “Export for audit”. + +--- + +# Fast 30‑day plan + +* Week 1: JCS canonicalization + DSSE for two artifacts (decisions, deltas). +* Week 2: Micro‑witness format + PR checks + basic verifier CLI. +* Week 3: ATT&CK mapping + simple attack‑graph scoring. +* Week 4: eBPF pilot in staging + Evidence Locker v1 + 3 SLAs + pricing page copy. + +If you want, I can draft the DSSE/JCS spec snippets, the witness schema, a sample PR check, and the KPI dashboard widgets next. diff --git a/docs-archived/product/advisories/20260226 - Deterministic call‑stack analysis and resolver strategy.md b/docs-archived/product/advisories/20260226 - Deterministic call‑stack analysis and resolver strategy.md new file mode 100644 index 000000000..6cb2aa639 --- /dev/null +++ b/docs-archived/product/advisories/20260226 - Deterministic call‑stack analysis and resolver strategy.md @@ -0,0 +1,192 @@ +Here’s a clean, first‑time‑friendly blueprint for a **deterministic crash analyzer pipeline** you can drop into Stella Ops (or any CI/CD + observability stack). + +--- + +# What this thing does (in plain words) + +It ingests a crash “evidence tile” (a signed, canonical JSON blob + hash), looks up symbols from your chosen stores (ELF/PDB/dSYM), unwinds the stack deterministically, and returns a **stable, symbol‑pinned call stack** plus a **replay manifest** so you can reproduce the exact same result later—bit‑for‑bit. + +--- + +# The contract + +### Input (strict, deterministic) + +* **signed_evidence_tile**: Canonical JSON (JCS) with your payload (e.g., OS, arch, registers, fault addr, module list) and its `sha256`. + + * Canonicalization must follow **RFC 8785 JCS** to make the hash verifiable. +* **symbol_pointers**: + + * ELF: debuginfod build‑id URIs + * Windows: PDB GUID+Age + * Apple: dSYM UUID +* **unwind_context**: register snapshot, preference flags (e.g., “prefer unwind tables over frame‑pointers”), OS/ABI hints. +* **deterministic_seed**: single source of truth for any randomized tie‑breakers or heuristics. + +### Output + +* **call_stack**: ordered vector of frames + + * `addr`, `symbol_id`, optional `file:line`, `symbol_resolution_confidence`, and `resolver` (which backend won). +* **replay_manifest**: `{ seed, env_knobs, symbol_bundle_pointer }` so you (or CI) can re‑run the exact same resolution later. + +--- + +# Resolver abstraction (so CI can fan‑out) + +Define a tiny interface and run resolvers in parallel; record who succeeded: + +```ts +type Platform = "linux" | "windows" | "apple"; + +interface ResolveResult { + symbol_id: string; // stable id in your store + file?: string; + line?: number; + confidence: number; // 0..1 + resolver: string; // e.g., "debuginfod", "dia", "dsymutil" +} + +function resolve(address: string, platform: Platform, bundle_hint?: string): ResolveResult | null; +``` + +**Backends:** + +* **Linux/ELF**: debuginfod (by build‑id), DWARF/unwind tables. +* **Windows**: DIA/PDB (by GUID+Age). +* **Apple**: dSYM/DWARF (by UUID), `atos`/`llvm-symbolizer` flow if desired. + +--- + +# Deterministic ingest & hashing + +* Parse incoming JSON → **canonicalize via JCS** → compute `sha256` → verify signature → only then proceed. +* Persist `{canonical_json, sha256, signature, received_at}` so downstream stages always pull the exact blob. + +--- + +# Unwinding & symbolization pipeline (deterministic) + +1. **Normalize modules** (match load addresses → build‑ids/GUIDs/UUIDs). +2. **Unwind** using the declared policy in `unwind_context` (frame pointers vs. EH/CFI tables). +3. For each PC: + + * **Parallel resolve** via resolvers (`debuginfod`, DIA/PDB, dSYM). + * Pick the winner by **deterministic reducer**: highest `confidence`, then lexical tie‑break using `deterministic_seed`. +4. Emit frames with `symbol_id` (stable, content‑addressed if possible), and optional `file:line`. + +--- + +# Telemetry & SLOs (what to measure) + +* **replay_success_ratio** (golden ≥ **95%**) — same input → same output. +* **symbol_coverage_pct** (prod ≥ **90%**) — % of frames resolved to symbols. +* **verify_time_ms** (median ≤ **3000 ms**) — signature + hash + canonicalization + core steps. +* **resolver_latency_ms** per backend — for tuning caches and fallbacks. + +--- + +# Trade‑offs (make them explicit) + +* **On‑demand decompilation / function‑matching** + + * ✅ Higher confidence on stripped binaries + * ❌ More CPU/latency; potentially leaks more symbol metadata (privacy) +* **Progressive fetch + partial symbolization** + + * ✅ Lower latency, good UX under load + * ❌ Lower confidence on some frames; riskier explainability (false positives) + +Pick per environment via `env_knobs` and record that in the `replay_manifest`. + +--- + +# Minimal wire formats (copy/paste ready) + +### Evidence tile (canonical, pre‑hash) + +```json +{ + "evidence_version": 1, + "platform": "linux", + "arch": "x86_64", + "fault_addr": "0x7f1a2b3c", + "registers": { "rip": "0x7f1a2b3c", "rsp": "0x7ffd...", "rbp": "0x..." }, + "modules": [ + {"name":"svc","base":"0x400000","build_id":"a1b2c3..."}, + {"name":"libc.so.6","base":"0x7f...","build_id":"d4e5f6..."} + ], + "ts_unix_ms": 1739999999999 +} +``` + +### Analyzer request + +```json +{ + "signed_evidence_tile": { + "jcs_json": "", + "sha256": "f1c2...deadbeef", + "signature": "dsse/…" + }, + "symbol_pointers": { + "linux": ["debuginfod:buildid:a1b2c3..."], + "windows": ["pdb:GUID+Age:..."], + "apple": ["dsym:UUID:..."] + }, + "unwind_context": { + "prefer_unwind_tables": true, + "stack_limit_bytes": 262144 + }, + "deterministic_seed": "6f5d7d1e-..." +} +``` + +### Analyzer response + +```json +{ + "call_stack": [ + { + "addr": "0x400abc", + "symbol_id": "svc@a1b2c3...:main", + "file": "main.cpp", + "line": 127, + "symbol_resolution_confidence": 0.98, + "resolver": "debuginfod" + } + ], + "replay_manifest": { + "seed": "6f5d7d1e-...", + "env_knobs": { "progressive_fetch": true, "max_resolvers": 3 }, + "symbol_bundle_pointer": "bundle://a1b2c3.../svc.sym" + } +} +``` + +--- + +# How this plugs into Stella Ops + +* **EvidenceLocker**: store the JCS‑canonical tile + DSSE signature + sha256. +* **AdvisoryAI**: consume symbol‑pinned stacks as first‑class facts for RCA, search, and explanations. +* **Attestor**: sign analyzer outputs (DSSE) and attach to Releases/Incidents. +* **CI**: on build, publish symbol bundles (ELF build‑id / PDB GUID+Age / dSYM UUID) to your internal stores; register debuginfod endpoints. +* **SLO dashboards**: show coverage, latency, and replay ratio by service and release. + +--- + +# Quick implementation checklist + +* [ ] JCS canonicalization + sha256 + DSSE verify gate +* [ ] Resolver interface + parallel fan‑out + deterministic reducer +* [ ] debuginfod client (ELF), DIA/PDB (Windows), dSYM/DWARF (Apple) adapters +* [ ] Unwinder with policy switches (frame‑ptr vs. CFI) +* [ ] Content‑addressed `symbol_id` scheme +* [ ] Replay harness honoring `replay_manifest` +* [ ] Metrics emitters + SLO dashboards +* [ ] Privacy guardrails (strip/leak‑check symbol metadata by env) + +--- + +If you want, I can generate a tiny reference service (Go or C#) with: JCS canonicalizer, debuginfod lookup, DIA shim, dSYM flow, and the exact JSON contracts above so you can drop it into your build & incident pipeline. diff --git a/docs-archived/product/advisories/20260226 - Deterministic score service and replay control.md b/docs-archived/product/advisories/20260226 - Deterministic score service and replay control.md new file mode 100644 index 000000000..c894381b1 --- /dev/null +++ b/docs-archived/product/advisories/20260226 - Deterministic score service and replay control.md @@ -0,0 +1,192 @@ +Here’s a small, reproducible “risk score” service design you can drop into Stella Ops to turn CVSS v4 + EPSS + analyzer evidence into a deterministic number with an auditable trail. + +--- + +# What it does (in plain terms) + +* Takes **authoritative inputs** (CVSS v4 vector & score, EPSS probability, your analyzer’s call‑stack confidence, optional CVE tag, etc.). +* Computes a **deterministic score** (no RNG, no hidden heuristics). +* Returns a **breakdown** with fixed weights + human‑readable explanations. +* Emits a **DSSE‑signed envelope** so anyone can re‑compute and verify. +* Includes **replay hooks** so CI can prove results are reproducible on your infra. + +--- + +# API (stable, tiny) + +**POST `/v1/score`** + +```json +{ + "evidence_jcs_hash": "sha256", + "cve": "CVE-XXXX-YYYY", + "cvss_v4": { + "vector": "T:.../A:.../AV:.../AC:.../PR:.../UI:.../S:...", + "score": 8.1 + }, + "epss": 0.27, + "analyzer_manifest": "uri_or_blob", + "deterministic_seed": 1337, + "replay_constraints": { + "max_cpu_ms": 5000, + "unwind_mode": "full", + "symbol_source": "debuginfod" + } +} +``` + +**Response** + +```json +{ + "score": 0.742, + "breakdown": [ + {"factor": "cvss", "weight": 0.45, "explanation": "Normalized CVSS v4 base score (0–10 → 0–1)."}, + {"factor": "epss", "weight": 0.35, "explanation": "EPSS probability of exploitation in the wild."}, + {"factor": "exploitability", "weight": 0.10, "explanation": "Runtime exploit signals (if any)."}, + {"factor": "callstack_match", "weight": 0.10, "explanation": "Analyzer call‑stack ↔ vulnerable symbol match confidence."} + ], + "signed_score_dsse": "base64(dsse-envelope)", + "replay_verification": { + "replay_success": true, + "replay_success_ratio": 1.0, + "verify_time_ms": 1120, + "symbol_coverage_pct": 83.4 + } +} +``` + +--- + +# Deterministic math (fixed, auditable) + +* Normalize CVSS: `cvss_n = clamp(cvss_v4.score / 10, 0, 1)` +* Use EPSS directly: `epss_n = clamp(epss, 0, 1)` +* Analyzer channels (each 0–1 from your evidence engine): + + * `exploitability_n` (e.g., known IOC hits, honeypot signals, etc.) + * `callstack_n` (e.g., symbol‑level match confidence) + +**Fixed rational weights (exact decimals):** + +* `w_cvss = 0.45`, `w_epss = 0.35`, `w_expl = 0.10`, `w_stack = 0.10` +* Sum = `1.00` exactly (store as fixed‑precision decimals, e.g., Q16.16 or `decimal(6,5)`) + +**Score:** + +``` +score = (w_cvss*cvss_n) + (w_epss*epss_n) + (w_expl*exploitability_n) + (w_stack*callstack_n) +``` + +No randomness. If inputs are identical, output is identical. + +--- + +# Canonical inputs & signing + +* Create a **JCS** (JSON Canonicalization Scheme) of the request after normalizing numeric precision. +* Hash with SHA‑256 → publish the digest in the DSSE payload. +* Sign DSSE with your service key (e.g., Sigstore‑style keyless or KMS key). +* Return `signed_score_dsse` + include the exact **JCS** you used inside the envelope so downstream verifiers can re‑compute and validate. + +--- + +# Replay hooks (for CI + “Verify” buttons) + +Implement three internal functions and expose them as admin ops or test endpoints: + +* `seeded_replay(seed)` — runs the scoring with the given seed and the exact stored JCS input; proves determinism across runs. +* `permutation_test(n)` — randomizes **ordering only** of non‑keyed evidence to show order‑independence; expect identical outputs. +* `offline_bundle_replay(bundle_uri)` — replays using a frozen symbol/debug bundle (no network); ensures air‑gap verifiability. + +**Telemetry & gates** + +* Release gate: require `replay_success_ratio ≥ 0.95`. +* UI “Verify” button: median `verify_time_ms ≤ 3000`. +* Track `fp_escalations_total` and `symbol_coverage_pct`; use as release criteria. + + * Trade‑off: higher weight on `callstack_match` → better explainability but requires broader symbol retention (privacy/storage) and increases verifier CPU/latency. + +--- + +# Drop‑in sketch (C# minimal) + +```csharp +public record ScoreRequest( + string evidence_jcs_hash, string? cve, + Cvss cvss_v4, decimal epss, string analyzer_manifest, + int deterministic_seed, Replay replay_constraints); +public record Cvss(string vector, decimal score); +public record Replay(int max_cpu_ms, string unwind_mode, string symbol_source); + +public static class Scorer { + static readonly decimal wCvss = 0.45m, wEpss = 0.35m, wExpl = 0.10m, wStack = 0.10m; + + public static decimal Compute(ScoreRequest r, decimal exploitability_n, decimal callstack_n) { + decimal cvss_n = Clamp(r.cvss_v4.score / 10m); + decimal epss_n = Clamp(r.epss); + return Round4(wCvss*cvss_n + wEpss*epss_n + wExpl*Clamp(exploitability_n) + wStack*Clamp(callstack_n)); + } + static decimal Clamp(decimal v) => v < 0 ? 0 : v > 1 ? 1 : v; + static decimal Round4(decimal v) => Math.Round(v, 4, MidpointRounding.AwayFromZero); +} +``` + +--- + +# Example + +**Request** + +```json +{ + "evidence_jcs_hash": "sha256", + "cve": "CVE-2025-12345", + "cvss_v4": {"vector":"T:AV:N/AC:L/AT:N/PR:N/UI:N/S:U/C:H/I:H/A:H","score":9.0}, + "epss": 0.22, + "analyzer_manifest": "evidence://builds/ro-2026-02-25-001", + "deterministic_seed": 42, + "replay_constraints": {"max_cpu_ms": 3000, "unwind_mode": "partial", "symbol_source": "bundle"} +} +``` + +**Effective analyzer channels** (from your evidence engine): + +``` +exploitability_n = 0.3 +callstack_n = 0.8 +``` + +**Score** + +``` += 0.45*(0.9) + 0.35*(0.22) + 0.10*(0.3) + 0.10*(0.8) += 0.405 + 0.077 + 0.03 + 0.08 += 0.592 +``` + +--- + +# Operational notes (Stella Ops fit) + +* Put the service behind **EvidenceLocker**; store the request JCS, DSSE, and the replay vector. +* Expose a **“Recompute & Verify”** button on each finding; display `symbol_coverage_pct`, `verify_time_ms`, and the signed breakdown. +* Let **Attestor** gate promotions with `replay_success_ratio` and DSSE verification status. +* Keep weights in a **BUSL‑guarded policy file** (decimal literals), versioned and change‑controlled; any change is a policy release. + +--- + +# Quick contract to share with integrators + +```bash +curl -sS https://stella.local/v1/score \ + -H 'Content-Type: application/json' \ + -d @payload.json | jq . +``` + +* If integrators recompute: they must JCS‑canonicalize your response’s `inputs` object and verify the DSSE over its SHA‑256. +* If any dependent input changes (EPSS refresh, CVSS re‑base), **score will change**, but **for the same inputs** the score is identical across machines. + +--- + +Want me to turn this into a ready‑to‑run .NET minimal API (with DSSE using your KMS, plus a tiny verifier CLI) and wire it into Stella Ops’ AdvisoryAI module? diff --git a/docs-archived/product/advisories/20260226 - Deterministic tile verification with Rekor v2.md b/docs-archived/product/advisories/20260226 - Deterministic tile verification with Rekor v2.md new file mode 100644 index 000000000..d4ad2d602 --- /dev/null +++ b/docs-archived/product/advisories/20260226 - Deterministic tile verification with Rekor v2.md @@ -0,0 +1,54 @@ +Here’s a super-practical “verifier recipe” you can drop into Stella Ops to validate supply‑chain attestations and Rekor v2 tiles end‑to‑end. + +# 0) Quick background (so the terms make sense) + +* **in‑toto Statement**: a tiny wrapper that says “this attestation is about artifact X with digest Y; the payload (predicate) is of type Z (e.g., SLSA Provenance).” ([GitHub][1]) +* **DSSE**: a signing envelope format (signatures over `payloadType` + canonicalized `payload` bytes). ([GitHub][2]) +* **Rekor v2 tiles**: transparency log moved to a **tile‑backed** layout; clients fetch tiles and compute inclusion proofs locally (no “get by index” API). ([Sigstore Blog][3]) +* **JCS**: a deterministic JSON canonicalization used when hashing/validating JSON payload bytes. ([RFC Editor][4]) + +# 1) Match the attestation to the artifact (Statement checks) + +* Load the in‑toto **Statement**. Confirm each `subject[*].name` and `subject[*].digest` matches the artifact you’re verifying. +* Assert the **`predicateType`** is recognized (e.g., `https://slsa.dev/provenance` / v0.1–v1.x) and, if present, validate **`predicate.materials`** (inputs) against expected sources. This binds “what was built” and “from what.” ([GitHub][1]) + +# 2) Verify DSSE signatures (bytes matter) + +* **Canonicalize** the DSSE *payload bytes* using DSSE’s own serialization rules; verify the DSSE signature against the signer’s public key (or Sigstore bundle chain if you use that path). ([GitHub][2]) +* Before any policy hash checks over JSON, **re‑canonicalize** the payload using **JCS** so downstream hashing is deterministic across platforms. ([RFC Editor][4]) + +# 3) Prove Rekor inclusion (tiles, not point lookups) + +* **Prefer** the `inclusion_proof` returned at upload time—store it with the bundle to avoid reconstructing later. ([GitHub][5]) +* If missing, **assemble the minimal tile set** from Rekor v2 endpoints (client‑side), then verify: + a) merkle inclusion for the entry’s leaf, and + b) the inclusion proof’s **checkpoint** against the Rekor v2 checkpoint. + Rekor v2 intentionally removed “get by index/leaf hash”; clients fetch tiles and compute proofs locally. ([Sigstore Blog][3]) + +--- + +## How to wire this into Stella Ops (concise plan) + +* **Verifier service** (AdvisoryAI/EvidenceLocker boundary): + + * Input: `{artifactPath|digest, dsseEnvelope|bundle, optional inclusion_proof}`. + * Steps: (1) Statement match → (2) DSSE verify → (3) Inclusion verify → (4) Policy (SLSA fields, materials allow‑list). +* **Policy hooks**: add simple YAML policies (e.g., allowed builders, required `materials[]` domains, min SLSA predicate version). ([SLSA][6]) +* **Caching**: persist tiles + checkpoints per tree size to avoid refetch. +* **UI**: in Release Bundle > Evidence tab show: “Subject match ✅ / DSSE sig ✅ / Inclusion ✅ (treeSize, rootHash, checkpoint origin)”. + +## Pitfalls to avoid + +* Hashing the JSON *without* JCS and getting platform‑specific diffs. (Use JCS before any JSON‑level policy hashing.) ([RFC Editor][4]) +* Treating Rekor v2 like v1 (no direct “get by index” reads; fetch tiles and compute locally). ([Sigstore Blog][3]) +* Ignoring `predicateType` evolution; track the URI (it encodes major version semantics). ([SLSA][7]) + +If you want, I can sketch a tiny .NET verifier module (interfaces + test vectors) that plugs into Stella Ops’s EvidenceLocker and uses the Rekor v2 tile client model. + +[1]: https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md?utm_source=chatgpt.com "attestation/spec/v1/statement.md at main · in-toto/attestation" +[2]: https://github.com/secure-systems-lab/dsse?utm_source=chatgpt.com "DSSE: Dead Simple Signing Envelope" +[3]: https://blog.sigstore.dev/rekor-v2-ga/?utm_source=chatgpt.com "Rekor v2 GA - Cheaper to run, simpler to maintain" +[4]: https://www.rfc-editor.org/rfc/rfc8785?utm_source=chatgpt.com "RFC 8785: JSON Canonicalization Scheme (JCS)" +[5]: https://github.com/sigstore/rekor?utm_source=chatgpt.com "sigstore/rekor: Software Supply Chain Transparency Log" +[6]: https://slsa.dev/spec/v0.1/provenance?utm_source=chatgpt.com "Provenance" +[7]: https://slsa.dev/spec/v0.1/verification_summary?utm_source=chatgpt.com "Verification Summary Attestation (VSA)" diff --git a/docs-archived/product/advisories/20260226 - Triage explainability four measurable fixes.md b/docs-archived/product/advisories/20260226 - Triage explainability four measurable fixes.md new file mode 100644 index 000000000..dc3682b43 --- /dev/null +++ b/docs-archived/product/advisories/20260226 - Triage explainability four measurable fixes.md @@ -0,0 +1,28 @@ +I’m sharing this because the current state of scanner triage and trace UIs exposes the very disconnects you’ve been targeting — tools are *great* at finding issues, but the paths from *vulnerability to proven context* are still too brittle for reliable triage and automated workflows. + +![Image](https://docs.snyk.io/~gitbook/image?dpr=3\&quality=100\&sign=3207753b\&sv=2\&url=https%3A%2F%2F2533899886-files.gitbook.io%2F~%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-MdwVZ6HOZriajCf5nXH%252Fuploads%252Fgit-blob-7a668b30edd9ffd5fb781211e6f7e1a9d51eda69%252Fimage.png%3Falt%3Dmedia\&width=768) + +![Image](https://perfetto.dev/docs/images/system-tracing-trace-view.png) + +![Image](https://user-images.githubusercontent.com/150329/40900669-86eced80-6781-11e8-92c1-dc667b651e72.gif) + +![Image](https://user-images.githubusercontent.com/150329/44534434-a05f8380-a6ac-11e8-86ac-e3e05e577c52.png) + +Scanner tools like **Snyk** are adding reachability analysis to help prioritize vulnerabilities by whether application code *can* call the affected functions — effectively analyzing call graphs to determine *reachable CVEs*. This uses static program analysis and AI heuristics to map paths from your app into vulnerability code, though it still acknowledges limitations where static paths aren’t fully known. ([Snyk Docs][1]) +Enterprise scanners such as **JFrog Xray** extend SCA into binaries and SBOMs, performing deep artifact scans and ingesting SBOM data (e.g., CycloneDX) to detect vulnerabilities and license risks — and they’re integrated into build and CI/CD lifecycles. ([JFrog][2]) + +While these tools excel at *surface detection* and prioritization based on static context, they don’t yet bridge the gap into **live, low‑latency trace or call‑stack verified evidence** the way observability UIs (Perfetto/Jaeger/Speedscope) do for performance and distributed traces. Those UIs let engineers visually inspect call stacks, timelines, and flamegraphs with tight symbol binding — something scanner consoles rarely provide in an actionable, signed form. + +The contrast is clear in practice: + +* **Scanner flows** (Snyk, Anchore/Grype, Xray, Wiz, Prisma Cloud) focus on detection and risk scoring, integrated with SBOMs and CI/CD. They stop short of *reliable runtime evidence playback* or *signed call‑stack histories* that can prove exploitability or triage decisions with cryptographic confidence. ([echo.ai][3]) + +* **Trace / profiling UIs** (Perfetto, Speedscope flamegraphs, Jaeger distributed tracing) provide interactive timelines with symbol resolution and execution context — the exact sort of evidence you’d want when determining if a reported issue truly matters in a given run. Yet scanners don’t emit this form of trace data, and observability tools aren’t wired into vulnerability pipelines by default. + +That explains why your proposed targets — provenance aggregation, minimal repro anchoring, reachability/trace fusion, and in‑console timelines — are hitting core gaps in the ecosystem: current solutions optimize detection and prioritization, not *evidence-backed, low‑latency verification* in triage. In other words, we have deep scanning engines and *deep tracing UIs* — but not a cohesive, signed pipeline that ties them together in real time with actionable context. + +The ecosystem today gives us strong static analysis and SBOM‑focused tools, but not the *runtime replay/verified call‑stack context* that would close the loop on triage confidence in high‑velocity CICD environments. + +[1]: https://docs.snyk.io/manage-risk/prioritize-issues-for-fixing/reachability-analysis?utm_source=chatgpt.com "Reachability analysis - Homepage | Snyk User Docs" +[2]: https://jfrog.com/xray/?utm_source=chatgpt.com "Xray | Software Composition Analysis (SCA) Tool" +[3]: https://www.echo.ai/blog/best-container-scanning-tools?utm_source=chatgpt.com "10 Best Container Scanning Tools for 2025 - Echo" diff --git a/docs-archived/product/advisories/ARCHIVE_LOG_20260303.md b/docs-archived/product/advisories/ARCHIVE_LOG_20260303.md new file mode 100644 index 000000000..df32100ad --- /dev/null +++ b/docs-archived/product/advisories/ARCHIVE_LOG_20260303.md @@ -0,0 +1,18 @@ +# Advisory Archive Log - 2026-03-03 + +| Timestamp (UTC) | Source Name | Archived Name | +| --- | --- | --- | +| 2026-03-03T21:51:27Z | 20260220 - OCI 1.1 referrers compatibility across major registries.md | 20260220 - OCI 1.1 referrers compatibility across major registries.md | +| 2026-03-03T21:51:27Z | 20260221 - Building a verifiable SBOM and attestation spine.md | 20260221 - Building a verifiable SBOM and attestation spine.md | +| 2026-03-03T21:51:27Z | 20260221 - Four novel, testable moat hypotheses.md | 20260221 - Four novel, testable moat hypotheses.md | +| 2026-03-03T21:51:27Z | 20260222 - Fuzz & mutation hardening suite.md | 20260222 - Fuzz & mutation hardening suite.md | +| 2026-03-03T21:51:27Z | 20260223 - Auditor UX experiments measurement plan.md | 20260223 - Auditor UX experiments measurement plan.md | +| 2026-03-03T21:51:27Z | 20260223 - Unified symbolization across platforms and vendors.md | 20260223 - Unified symbolization across platforms and vendors.md | +| 2026-03-03T21:51:27Z | 20260224 - Deterministic tile verification with Rekor v2.md | 20260224 - Deterministic tile verification with Rekor v2 (superseded by 20260226).md | +| 2026-03-03T21:51:27Z | 20260224 - Turning defensibility into measurable business moats.md | 20260224 - Turning defensibility into measurable business moats.md | +| 2026-03-03T21:51:27Z | 20260226 - Deterministic call‑stack analysis and resolver strategy.md | 20260226 - Deterministic call‑stack analysis and resolver strategy.md | +| 2026-03-03T21:51:27Z | 20260226 - Deterministic score service and replay control.md | 20260226 - Deterministic score service and replay control.md | +| 2026-03-03T21:51:27Z | 20260226 - Deterministic tile verification with Rekor v2.md | 20260226 - Deterministic tile verification with Rekor v2.md | +| 2026-03-03T21:51:27Z | 20260226 - Triage explainability four measurable fixes.md | 20260226 - Triage explainability four measurable fixes.md | + +Deduplication note: `20260224 - Deterministic tile verification with Rekor v2.md` was archived as superseded by the canonical `20260226` advisory. diff --git a/docs/modules/tools/supply-chain-hardening-suite.md b/docs/modules/tools/supply-chain-hardening-suite.md new file mode 100644 index 000000000..8300d02ec --- /dev/null +++ b/docs/modules/tools/supply-chain-hardening-suite.md @@ -0,0 +1,81 @@ +# Supply-Chain Hardening Suite + +## Purpose + +The supply-chain hardening suite provides deterministic negative-path and mutation testing for scanner/attestor/symbols evidence workflows without requiring external network calls. + +Working location: +- `tests/supply-chain/` + +## Lanes + +1. `01-jcs-property` +- Verifies canonicalization idempotence. +- Verifies key-order permutation invariance. +- Verifies duplicate-key rejection. + +2. `02-schema-fuzz` +- Runs deterministic schema-aware mutation lane. +- Emits crash diagnostics and replay seed on unexpected exceptions. +- Enforces zero-crash gate in CI. + +3. `03-rekor-neg` +- Simulates Rekor negative paths (413/424/504/unsupported/202). +- Verifies deterministic error classification. +- Emits per-case `diagnostic_blob.json` and bundle archive. + +4. `04-big-dsse-referrers` +- Validates oversized DSSE and malformed referrer rejection behavior. +- Requires deterministic `unknown_state` and `reprocessToken` outputs. + +5. `05-corpus` +- Stores deterministic fixture corpus. +- Provides deterministic archive manifest builder for corpus updates. + +## Execution Profiles + +1. PR / push gate profile (`smoke`) +- Seed: `20260226` +- Fuzz lane bounds: `limit=1000`, `time=60s` +- Artifact retention: 14 days + +2. Nightly profile (`nightly`) +- Seed: `20260226` +- Fuzz lane bounds: `limit=5000`, `time=300s` +- Artifact retention: 30 days + +## Commands + +1. Run smoke profile: +- `python tests/supply-chain/run_suite.py --profile smoke --seed 20260226` + +2. Run nightly profile: +- `python tests/supply-chain/run_suite.py --profile nightly --seed 20260226` + +3. Rebuild corpus archive metadata: +- `python tests/supply-chain/05-corpus/build_corpus_archive.py --output out/supply-chain/05-corpus` + +## CI Integration + +Workflow: +- `.gitea/workflows/supply-chain-hardening.yml` + +Outputs: +- `out/supply-chain/summary.json` +- lane-level `junit.xml` files +- lane-level `report.json` files +- `03-rekor-neg/rekor_negative_cases.tar.gz` +- `04-big-dsse-referrers/big_dsse_payloads.tar.gz` + +## Failure Replay + +1. Download CI artifact `supply-chain-hardening-`. +2. Read failing lane diagnostics under `failures//`. +3. Re-run locally with the same seed: +- `python tests/supply-chain/run_suite.py --profile smoke --seed 20260226 --output out/supply-chain-replay` + +## Advisory Traceability + +| Advisory | Sprint | Coverage | +| --- | --- | --- | +| `docs-archived/product/advisories/20260222 - Fuzz & mutation hardening suite.md` | `docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_228_Tools_supply_chain_fuzz_mutation_hardening_suite.md` | Lanes `01` through `05` + CI gate | diff --git a/docs/product/advisories/README.md b/docs/product/advisories/README.md new file mode 100644 index 000000000..843c57bf8 --- /dev/null +++ b/docs/product/advisories/README.md @@ -0,0 +1,10 @@ +# Open Product Advisories + +This directory contains only advisories that are not yet translated into sprint execution. + +Current status: +- No open advisories in the 2026-02-20 through 2026-02-26 batch. + +Related records: +- Translation register: `docs/product/advisory-translation-20260226.md` +- Archive log: `docs-archived/product/advisories/ARCHIVE_LOG_20260303.md` diff --git a/docs/product/advisory-translation-20260226.md b/docs/product/advisory-translation-20260226.md new file mode 100644 index 000000000..91e6d7a88 --- /dev/null +++ b/docs/product/advisory-translation-20260226.md @@ -0,0 +1,34 @@ +# Advisory Translation Register (2026-02-26 Batch) + +This register maps each advisory from the 2026-02-20 through 2026-02-26 batch to implementation sprints and module documentation commitments. + +Archival status (2026-03-03): +- Advisory source files are archived under `docs-archived/product/advisories/`. +- Completed sprint artifacts are archived under `docs-archived/implplan/2026-03-03-completed-sprints/`. + +## Advisory to Sprint Mapping + +| Advisory | Primary Sprint(s) | Module Doc Commitments | +| --- | --- | --- | +| `20260220 - OCI 1.1 referrers compatibility across major registries` | `SPRINT_20260226_224_Scanner_oci_referrers_runtime_stack_and_replay_data` | `docs/modules/scanner/architecture.md` | +| `20260221 - Building a verifiable SBOM and attestation spine` | `SPRINT_20260226_222_Cli_proof_chain_verification_and_replay_parity`, `SPRINT_20260226_225_Attestor_signature_trust_and_verdict_api_hardening`, `SPRINT_20260226_226_Symbols_dsse_rekor_merkle_and_hash_integrity` | `docs/modules/cli/architecture.md`, `docs/modules/attestor/architecture.md`, `docs/modules/binary-index/architecture.md` | +| `20260221 - Four novel, testable moat hypotheses` | `SPRINT_20260226_227_FE_triage_risk_score_widget_wiring_and_parity`, `SPRINT_20260226_229_DOCS_advisory_hygiene_dedup_and_archival_translation` | `docs/modules/ui/architecture.md`, `docs/modules/platform/architecture.md` | +| `20260222 - Fuzz & mutation hardening suite` | `SPRINT_20260226_228_Tools_supply_chain_fuzz_mutation_hardening_suite` | `docs/modules/tools/supply-chain-hardening-suite.md` | +| `20260223 - Auditor UX experiments: measurement plan` | `SPRINT_20260226_227_FE_triage_risk_score_widget_wiring_and_parity`, `SPRINT_20260226_229_DOCS_advisory_hygiene_dedup_and_archival_translation` | `docs/modules/ui/architecture.md` | +| `20260223 - Unified symbolization across platforms and vendors` | `SPRINT_20260226_226_Symbols_dsse_rekor_merkle_and_hash_integrity` | `docs/modules/binary-index/architecture.md` | +| `20260224 - Deterministic tile verification with Rekor v2` | `SPRINT_20260226_226_Symbols_dsse_rekor_merkle_and_hash_integrity`, `SPRINT_20260226_225_Attestor_signature_trust_and_verdict_api_hardening` | `docs/modules/binary-index/architecture.md`, `docs/modules/attestor/architecture.md` | +| `20260224 - Turning defensibility into measurable business moats` | `SPRINT_20260226_223_Platform_score_explain_contract_and_replay_alignment`, `SPRINT_20260226_227_FE_triage_risk_score_widget_wiring_and_parity` | `docs/modules/platform/architecture.md`, `docs/modules/ui/architecture.md` | +| `20260226 - Deterministic call-stack analysis and resolver strategy` | `SPRINT_20260226_224_Scanner_oci_referrers_runtime_stack_and_replay_data` | `docs/modules/scanner/architecture.md` | +| `20260226 - Deterministic score service and replay control` | `SPRINT_20260226_223_Platform_score_explain_contract_and_replay_alignment`, `SPRINT_20260226_222_Cli_proof_chain_verification_and_replay_parity`, `SPRINT_20260226_227_FE_triage_risk_score_widget_wiring_and_parity` | `docs/modules/platform/architecture.md`, `docs/modules/cli/architecture.md`, `docs/modules/ui/architecture.md` | +| `20260226 - Deterministic tile verification with Rekor v2` | Canonicalized duplicate target for `20260224` advisory; implemented via same sprint set | `docs/modules/binary-index/architecture.md`, `docs/modules/attestor/architecture.md` | +| `20260226 - Triage explainability: four measurable fixes` | `SPRINT_20260226_227_FE_triage_risk_score_widget_wiring_and_parity` | `docs/modules/ui/architecture.md` | + +## Deduplication Decisions + +1. `20260224 - Deterministic tile verification with Rekor v2` is superseded by `20260226 - Deterministic tile verification with Rekor v2`. +2. `20260223 - Auditor UX experiments` was malformed and replaced with a repaired measurement-plan advisory before archival. + +## Translation Status + +- All advisories from the 2026-02-20 through 2026-02-26 batch have mapped sprint execution and are archived. +- Sprint trackers for this batch are `DONE` and archived. diff --git a/src/Attestor/StellaOps.Attestation.Tests/DsseVerifierTests.cs b/src/Attestor/StellaOps.Attestation.Tests/DsseVerifierTests.cs index 761e1168b..3d47e360a 100644 --- a/src/Attestor/StellaOps.Attestation.Tests/DsseVerifierTests.cs +++ b/src/Attestor/StellaOps.Attestation.Tests/DsseVerifierTests.cs @@ -8,6 +8,9 @@ using System.Text.Json; using FluentAssertions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.X509; using Xunit; namespace StellaOps.Attestation.Tests; @@ -44,6 +47,33 @@ public class DsseVerifierTests result.Issues.Should().BeEmpty(); } + [Fact] + public async Task VerifyAsync_WithValidRsaSignature_ReturnsSuccess() + { + using var rsa = RSA.Create(2048); + var (envelope, publicKeyPem) = CreateSignedEnvelope(rsa); + + var result = await _verifier.VerifyAsync(envelope, publicKeyPem, TestContext.Current.CancellationToken); + + result.IsValid.Should().BeTrue(); + result.ValidSignatureCount.Should().Be(1); + result.Issues.Should().BeEmpty(); + } + + [Fact] + public async Task VerifyAsync_WithValidEd25519Signature_ReturnsSuccess() + { + var seed = Enumerable.Range(0, 32).Select(static i => (byte)(i + 1)).ToArray(); + var privateKey = new Ed25519PrivateKeyParameters(seed, 0); + var (envelope, publicKeyPem) = CreateSignedEnvelope(privateKey); + + var result = await _verifier.VerifyAsync(envelope, publicKeyPem, TestContext.Current.CancellationToken); + + result.IsValid.Should().BeTrue(); + result.ValidSignatureCount.Should().Be(1); + result.Issues.Should().BeEmpty(); + } + [Fact] public async Task VerifyAsync_WithInvalidSignature_ReturnsFail() { @@ -64,6 +94,39 @@ public class DsseVerifierTests result.Issues.Should().NotBeEmpty(); } + [Fact] + public async Task VerifyAsync_WithInvalidRsaSignature_ReturnsDeterministicReason() + { + using var signingKey = RSA.Create(2048); + var (envelope, _) = CreateSignedEnvelope(signingKey); + + using var verifierKey = RSA.Create(2048); + var verifierPem = ExportPublicKeyPem(verifierKey); + + var result = await _verifier.VerifyAsync(envelope, verifierPem, TestContext.Current.CancellationToken); + + result.IsValid.Should().BeFalse(); + result.Issues.Should().Contain(issue => issue.Contains("signature_mismatch", StringComparison.Ordinal)); + } + + [Fact] + public async Task VerifyAsync_WithInvalidEd25519Signature_ReturnsDeterministicReason() + { + var signingSeed = Enumerable.Range(0, 32).Select(static i => (byte)(i + 1)).ToArray(); + var verifyingSeed = Enumerable.Range(0, 32).Select(static i => (byte)(i + 101)).ToArray(); + + var signingKey = new Ed25519PrivateKeyParameters(signingSeed, 0); + var verifyingKey = new Ed25519PrivateKeyParameters(verifyingSeed, 0); + + var (envelope, _) = CreateSignedEnvelope(signingKey); + var verifyingPem = ExportPublicKeyPem(verifyingKey.GeneratePublicKey()); + + var result = await _verifier.VerifyAsync(envelope, verifyingPem, TestContext.Current.CancellationToken); + + result.IsValid.Should().BeFalse(); + result.Issues.Should().Contain(issue => issue.Contains("signature_mismatch", StringComparison.Ordinal)); + } + [Fact] public async Task VerifyAsync_WithMalformedJson_ReturnsParseError() { @@ -277,9 +340,77 @@ public class DsseVerifierTests return (envelope, publicKeyPem); } + private static (string EnvelopeJson, string PublicKeyPem) CreateSignedEnvelope(RSA signingKey) + { + var payloadType = "https://in-toto.io/Statement/v1"; + var payloadContent = "{\"_type\":\"https://in-toto.io/Statement/v1\",\"subject\":[]}"; + var payloadBytes = Encoding.UTF8.GetBytes(payloadContent); + var payloadBase64 = Convert.ToBase64String(payloadBytes); + + var pae = DsseHelper.PreAuthenticationEncoding(payloadType, payloadBytes); + var signatureBytes = signingKey.SignData(pae, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + var signatureBase64 = Convert.ToBase64String(signatureBytes); + + var envelope = JsonSerializer.Serialize(new + { + payloadType, + payload = payloadBase64, + signatures = new[] + { + new { keyId = "test-key-rsa-001", sig = signatureBase64 } + } + }); + + return (envelope, ExportPublicKeyPem(signingKey)); + } + + private static (string EnvelopeJson, string PublicKeyPem) CreateSignedEnvelope(Ed25519PrivateKeyParameters signingKey) + { + var payloadType = "https://in-toto.io/Statement/v1"; + var payloadContent = "{\"_type\":\"https://in-toto.io/Statement/v1\",\"subject\":[]}"; + var payloadBytes = Encoding.UTF8.GetBytes(payloadContent); + var payloadBase64 = Convert.ToBase64String(payloadBytes); + + var pae = DsseHelper.PreAuthenticationEncoding(payloadType, payloadBytes); + + var signer = new Ed25519Signer(); + signer.Init(true, signingKey); + signer.BlockUpdate(pae, 0, pae.Length); + var signatureBytes = signer.GenerateSignature(); + var signatureBase64 = Convert.ToBase64String(signatureBytes); + + var envelope = JsonSerializer.Serialize(new + { + payloadType, + payload = payloadBase64, + signatures = new[] + { + new { keyId = "test-key-ed25519-001", sig = signatureBase64 } + } + }); + + var publicKeyPem = ExportPublicKeyPem(signingKey.GeneratePublicKey()); + return (envelope, publicKeyPem); + } + private static string ExportPublicKeyPem(ECDsa key) { - var publicKeyBytes = key.ExportSubjectPublicKeyInfo(); + return ExportPublicKeyPem(key.ExportSubjectPublicKeyInfo()); + } + + private static string ExportPublicKeyPem(RSA key) + { + return ExportPublicKeyPem(key.ExportSubjectPublicKeyInfo()); + } + + private static string ExportPublicKeyPem(Ed25519PublicKeyParameters key) + { + var publicKeyBytes = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(key).GetDerEncoded(); + return ExportPublicKeyPem(publicKeyBytes); + } + + private static string ExportPublicKeyPem(byte[] publicKeyBytes) + { var base64 = Convert.ToBase64String(publicKeyBytes); var builder = new StringBuilder(); builder.AppendLine("-----BEGIN PUBLIC KEY-----"); diff --git a/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/VerdictControllerSecurityTests.cs b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/VerdictControllerSecurityTests.cs new file mode 100644 index 000000000..3e9b5dad7 --- /dev/null +++ b/src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Tests/VerdictControllerSecurityTests.cs @@ -0,0 +1,327 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using StellaOps.Attestor.Core.Signing; +using StellaOps.Attestor.Core.Submission; +using StellaOps.Attestor.Tests.Fixtures; +using StellaOps.Attestor.WebService.Contracts; +using StellaOps.TestKit; +using Xunit; + +namespace StellaOps.Attestor.Tests; + +public sealed class VerdictControllerSecurityTests +{ + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task CreateVerdict_WithTrustedRosterKey_ReturnsCreated_AndGetByHashResolves() + { + using var signingKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var publicKeyPem = ExportPublicKeyPem(signingKey); + + using var factory = CreateFactory( + rosterEntries: new[] + { + new RosterEntry("trusted-key", "trusted", publicKeyPem) + }, + new DeterministicSigningService(signingKey, keyId: "trusted-key")); + + var client = factory.CreateClient(); + var createResponse = await client.PostAsJsonAsync("/internal/api/v1/attestations/verdict", BuildRequest("trusted-key")); + + Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode); + var createPayload = await createResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(createPayload); + Assert.False(string.IsNullOrWhiteSpace(createPayload!.VerdictId)); + + var getResponse = await client.GetAsync($"/api/v1/verdicts/{createPayload.VerdictId}"); + Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); + var getPayload = await getResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(getPayload); + Assert.Equal(createPayload.VerdictId, getPayload!.VerdictId); + Assert.Equal("test-tenant", getPayload.TenantId); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task CreateVerdict_WithUnknownRosterKey_ReturnsForbidden() + { + using var signingKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var publicKeyPem = ExportPublicKeyPem(signingKey); + + using var factory = CreateFactory( + rosterEntries: new[] + { + new RosterEntry("trusted-key", "trusted", publicKeyPem) + }, + new DeterministicSigningService(signingKey, keyId: "unknown-key")); + + var client = factory.CreateClient(); + var response = await client.PostAsJsonAsync("/internal/api/v1/attestations/verdict", BuildRequest("unknown-key")); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + Assert.Equal("authority_key_unknown", await ReadProblemCodeAsync(response)); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task CreateVerdict_WithRevokedRosterKey_ReturnsForbidden() + { + using var signingKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var publicKeyPem = ExportPublicKeyPem(signingKey); + + using var factory = CreateFactory( + rosterEntries: new[] + { + new RosterEntry("revoked-key", "revoked", publicKeyPem) + }, + new DeterministicSigningService(signingKey, keyId: "revoked-key")); + + var client = factory.CreateClient(); + var response = await client.PostAsJsonAsync("/internal/api/v1/attestations/verdict", BuildRequest("revoked-key")); + + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + Assert.Equal("authority_key_revoked", await ReadProblemCodeAsync(response)); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task CreateVerdict_WithRosterKeyMissingPublicMaterial_ReturnsDeterministicServerError() + { + using var signingKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); + + using var factory = CreateFactory( + rosterEntries: new[] + { + new RosterEntry("trusted-key", "trusted", string.Empty) + }, + new DeterministicSigningService(signingKey, keyId: "trusted-key")); + + var client = factory.CreateClient(); + var response = await client.PostAsJsonAsync("/internal/api/v1/attestations/verdict", BuildRequest("trusted-key")); + + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal("authority_key_missing_public_key", await ReadProblemCodeAsync(response)); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task VerdictEndpoints_WithTenantHeaderSpoofing_ReturnForbidden() + { + using var signingKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var publicKeyPem = ExportPublicKeyPem(signingKey); + + using var factory = CreateFactory( + rosterEntries: new[] + { + new RosterEntry("trusted-key", "trusted", publicKeyPem) + }, + new DeterministicSigningService(signingKey, keyId: "trusted-key")); + + var client = factory.CreateClient(); + client.DefaultRequestHeaders.Add("X-Tenant-Id", "spoofed-tenant"); + + var createResponse = await client.PostAsJsonAsync("/internal/api/v1/attestations/verdict", BuildRequest("trusted-key")); + Assert.Equal(HttpStatusCode.Forbidden, createResponse.StatusCode); + Assert.Equal("tenant_mismatch", await ReadProblemCodeAsync(createResponse)); + + var getResponse = await client.GetAsync("/api/v1/verdicts/verdict-missing"); + Assert.Equal(HttpStatusCode.Forbidden, getResponse.StatusCode); + Assert.Equal("tenant_mismatch", await ReadProblemCodeAsync(getResponse)); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetVerdictByHash_WhenMissing_ReturnsNotFound() + { + using var signingKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var publicKeyPem = ExportPublicKeyPem(signingKey); + + using var factory = CreateFactory( + rosterEntries: new[] + { + new RosterEntry("trusted-key", "trusted", publicKeyPem) + }, + new DeterministicSigningService(signingKey, keyId: "trusted-key")); + + var client = factory.CreateClient(); + var response = await client.GetAsync("/api/v1/verdicts/verdict-not-found"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + Assert.Equal("verdict_not_found", await ReadProblemCodeAsync(response)); + } + + private static VerdictAttestationRequestDto BuildRequest(string keyId) => new() + { + PredicateType = "https://stellaops.dev/predicates/policy-verdict@v1", + Predicate = "{\"verdict\":{\"status\":\"pass\",\"severity\":\"low\",\"score\":0.1},\"metadata\":{\"policyRunId\":\"run-1\",\"policyId\":\"policy-1\",\"policyVersion\":1,\"evaluatedAt\":\"2026-02-26T00:00:00Z\"}}", + Subject = new VerdictSubjectDto + { + Name = "finding-1" + }, + KeyId = keyId + }; + + private static WebApplicationFactory CreateFactory( + IReadOnlyList rosterEntries, + IAttestationSigningService signingService) + { + var baseFactory = new AttestorTestWebApplicationFactory(); + return baseFactory.WithWebHostBuilder(builder => + { + builder.ConfigureAppConfiguration((_, configuration) => + { + var settings = new Dictionary(); + for (var i = 0; i < rosterEntries.Count; i++) + { + settings[$"attestor:verdictTrust:keys:{i}:keyId"] = rosterEntries[i].KeyId; + settings[$"attestor:verdictTrust:keys:{i}:status"] = rosterEntries[i].Status; + settings[$"attestor:verdictTrust:keys:{i}:publicKeyPem"] = rosterEntries[i].PublicKeyPem; + } + + configuration.AddInMemoryCollection(settings); + }); + + builder.ConfigureServices(services => + { + services.RemoveAll(); + services.RemoveAll(); + services.AddSingleton(signingService); + services.AddSingleton(new StubEvidenceLockerHttpClientFactory()); + }); + }); + } + + private static async Task ReadProblemCodeAsync(HttpResponseMessage response) + { + var payload = await response.Content.ReadFromJsonAsync(); + if (payload.ValueKind != JsonValueKind.Object) + { + return null; + } + + if (payload.TryGetProperty("code", out var directCode) && directCode.ValueKind == JsonValueKind.String) + { + return directCode.GetString(); + } + + return null; + } + + private static string ExportPublicKeyPem(ECDsa key) + { + var publicKeyDer = key.ExportSubjectPublicKeyInfo(); + var base64 = Convert.ToBase64String(publicKeyDer); + return $"-----BEGIN PUBLIC KEY-----\n{base64}\n-----END PUBLIC KEY-----"; + } + + private readonly record struct RosterEntry(string KeyId, string Status, string PublicKeyPem); + + private sealed class DeterministicSigningService : IAttestationSigningService + { + private readonly ECDsa _signingKey; + private readonly string _keyId; + + public DeterministicSigningService(ECDsa signingKey, string keyId) + { + _signingKey = signingKey; + _keyId = keyId; + } + + public Task SignAsync( + AttestationSignRequest request, + SubmissionContext context, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + var payloadBytes = Convert.FromBase64String(request.PayloadBase64); + var pae = DssePreAuthenticationEncoding.Compute(request.PayloadType, payloadBytes); + var signature = _signingKey.SignData(pae, HashAlgorithmName.SHA256); + + var result = new AttestationSignResult + { + KeyId = _keyId, + Algorithm = "ES256", + Mode = "keyful", + Provider = "unit-test", + SignedAt = DateTimeOffset.UnixEpoch, + Bundle = new AttestorSubmissionRequest.SubmissionBundle + { + Dsse = new AttestorSubmissionRequest.DsseEnvelope + { + PayloadType = request.PayloadType, + PayloadBase64 = request.PayloadBase64, + Signatures = new List + { + new() + { + KeyId = _keyId, + Signature = Convert.ToBase64String(signature) + } + } + }, + Mode = "keyful", + CertificateChain = Array.Empty() + }, + Meta = new AttestorSubmissionRequest.SubmissionMeta + { + Artifact = new AttestorSubmissionRequest.ArtifactInfo + { + Sha256 = new string('a', 64), + Kind = "verdict" + }, + BundleSha256 = Convert.ToHexString(SHA256.HashData(payloadBytes)).ToLowerInvariant(), + Archive = true, + LogPreference = "primary" + } + }; + + return Task.FromResult(result); + } + } + + private sealed class StubEvidenceLockerHttpClientFactory : IHttpClientFactory + { + private readonly HttpClient _client = new(new StubEvidenceLockerHandler()) + { + BaseAddress = new Uri("http://localhost:5200", UriKind.Absolute), + Timeout = TimeSpan.FromSeconds(1) + }; + + public HttpClient CreateClient(string name) => _client; + } + + private sealed class StubEvidenceLockerHandler : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.Method == HttpMethod.Post && + request.RequestUri is { AbsolutePath: "/api/v1/verdicts" }) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + } + + if (request.Method == HttpMethod.Get && + request.RequestUri is { AbsolutePath: var path } && + path.StartsWith("/api/v1/verdicts/", StringComparison.Ordinal)) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); + } + } +} diff --git a/src/BinaryIndex/__Tests/StellaOps.Symbols.Tests/Bundle/BundleBuilderVerificationTests.cs b/src/BinaryIndex/__Tests/StellaOps.Symbols.Tests/Bundle/BundleBuilderVerificationTests.cs new file mode 100644 index 000000000..6c50eb707 --- /dev/null +++ b/src/BinaryIndex/__Tests/StellaOps.Symbols.Tests/Bundle/BundleBuilderVerificationTests.cs @@ -0,0 +1,346 @@ +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Symbols.Bundle; +using StellaOps.Symbols.Bundle.Abstractions; +using StellaOps.Symbols.Core.Models; +using System.IO.Compression; +using System.Security.Cryptography; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using Xunit; +using BundleManifest = StellaOps.Symbols.Bundle.Models.BundleManifest; +using CoreSymbolEntry = StellaOps.Symbols.Core.Models.SymbolEntry; + +namespace StellaOps.Symbols.Tests.Bundle; + +public sealed class BundleBuilderVerificationTests +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + [Fact] + public async Task VerifyAsync_SignedAndRekorBundle_PassesWithRequiredGates() + { + var fixture = await CreateFixtureAsync(sign: true, submitRekor: true); + try + { + var verify = await fixture.Builder.VerifyAsync( + fixture.BundlePath, + new BundleVerifyOptions + { + RequireSignature = true, + RequireRekorProof = true, + VerifyRekorOffline = true + }); + + Assert.True(verify.Valid, string.Join("; ", verify.Errors)); + Assert.Equal(SignatureStatus.Valid, verify.SignatureStatus); + Assert.Equal(RekorVerifyStatus.VerifiedOffline, verify.RekorStatus); + Assert.NotNull(verify.Manifest); + Assert.StartsWith("blake3:", verify.Manifest!.BundleId, StringComparison.Ordinal); + Assert.All(verify.Manifest.Entries, entry => + { + Assert.StartsWith("blake3:", entry.ManifestHash, StringComparison.Ordinal); + Assert.StartsWith("blake3:", entry.BlobHash, StringComparison.Ordinal); + }); + } + finally + { + fixture.Dispose(); + } + } + + [Fact] + public async Task VerifyAsync_TamperedSignature_FailsDeterministically() + { + var fixture = await CreateFixtureAsync(sign: true, submitRekor: false); + try + { + await RewriteManifestAsync( + fixture.BundlePath, + manifest => + { + var signature = manifest.Signature!; + return manifest with + { + Signature = signature with + { + Signature = TamperBase64(signature.Signature!) + } + }; + }); + + var verify = await fixture.Builder.VerifyAsync( + fixture.BundlePath, + new BundleVerifyOptions { RequireSignature = true }); + + Assert.False(verify.Valid); + Assert.Equal(SignatureStatus.Invalid, verify.SignatureStatus); + Assert.Contains( + verify.Errors, + error => error.StartsWith("signature_verification_failed:signature_mismatch", StringComparison.Ordinal)); + } + finally + { + fixture.Dispose(); + } + } + + [Fact] + public async Task VerifyAsync_UnsignedBundle_WhenSignatureRequired_Fails() + { + var fixture = await CreateFixtureAsync(sign: false, submitRekor: false); + try + { + var verify = await fixture.Builder.VerifyAsync( + fixture.BundlePath, + new BundleVerifyOptions { RequireSignature = true }); + + Assert.False(verify.Valid); + Assert.Equal(SignatureStatus.Unsigned, verify.SignatureStatus); + Assert.Contains( + verify.Errors, + error => error.StartsWith("signature_verification_failed:signature_not_present", StringComparison.Ordinal)); + } + finally + { + fixture.Dispose(); + } + } + + [Fact] + public async Task VerifyAsync_WhenRekorProofRequiredButCheckpointMissing_FailsDeterministically() + { + var fixture = await CreateFixtureAsync(sign: true, submitRekor: false); + try + { + var verify = await fixture.Builder.VerifyAsync( + fixture.BundlePath, + new BundleVerifyOptions + { + RequireSignature = true, + RequireRekorProof = true, + VerifyRekorOffline = true + }); + + Assert.False(verify.Valid); + Assert.Null(verify.RekorStatus); + Assert.Contains("rekor_proof_required:missing_checkpoint", verify.Errors); + } + finally + { + fixture.Dispose(); + } + } + + [Fact] + public async Task VerifyAsync_TruncatedInclusionProof_FailsDeterministically() + { + var fixture = await CreateFixtureAsync(sign: true, submitRekor: true); + try + { + await RewriteManifestAsync( + fixture.BundlePath, + manifest => + { + var checkpoint = manifest.RekorCheckpoint!; + var proof = checkpoint.InclusionProof!; + return manifest with + { + RekorCheckpoint = checkpoint with + { + InclusionProof = proof with + { + Hashes = proof.Hashes.Take(1).ToArray() + } + } + }; + }); + + var verify = await fixture.Builder.VerifyAsync( + fixture.BundlePath, + new BundleVerifyOptions + { + RequireSignature = true, + RequireRekorProof = true, + VerifyRekorOffline = true + }); + + Assert.False(verify.Valid); + Assert.Equal(RekorVerifyStatus.Invalid, verify.RekorStatus); + Assert.Contains( + verify.Errors, + error => error.StartsWith("rekor_inclusion_proof_failed:proof_nodes_truncated", StringComparison.Ordinal)); + } + finally + { + fixture.Dispose(); + } + } + + [Fact] + public async Task VerifyAsync_CorruptedInclusionProofRoot_FailsDeterministically() + { + var fixture = await CreateFixtureAsync(sign: true, submitRekor: true); + try + { + await RewriteManifestAsync( + fixture.BundlePath, + manifest => + { + var checkpoint = manifest.RekorCheckpoint!; + var proof = checkpoint.InclusionProof!; + return manifest with + { + RekorCheckpoint = checkpoint with + { + InclusionProof = proof with + { + RootHash = "blake3:" + new string('0', 64) + } + } + }; + }); + + var verify = await fixture.Builder.VerifyAsync( + fixture.BundlePath, + new BundleVerifyOptions + { + RequireSignature = true, + RequireRekorProof = true, + VerifyRekorOffline = true + }); + + Assert.False(verify.Valid); + Assert.Equal(RekorVerifyStatus.Invalid, verify.RekorStatus); + Assert.Contains( + verify.Errors, + error => error.StartsWith("rekor_inclusion_proof_failed:proof_root_mismatch", StringComparison.Ordinal)); + } + finally + { + fixture.Dispose(); + } + } + + private static async Task CreateFixtureAsync(bool sign, bool submitRekor) + { + var rootDir = Path.Combine(Path.GetTempPath(), "stella-symbols-tests", Guid.NewGuid().ToString("N")); + var sourceDir = Path.Combine(rootDir, "source"); + var outputDir = Path.Combine(rootDir, "out"); + Directory.CreateDirectory(sourceDir); + Directory.CreateDirectory(outputDir); + + const string debugId = "DBG001"; + var manifest = new SymbolManifest + { + ManifestId = "blake3:manifest-dbg001", + DebugId = debugId, + CodeId = "code-001", + BinaryName = "libsample.so", + Platform = "linux-x64", + Format = BinaryFormat.Elf, + Symbols = + [ + new CoreSymbolEntry + { + Address = 0x1000, + Size = 16, + MangledName = "_ZL6samplev", + DemangledName = "sample()" + } + ], + TenantId = "tenant-default", + BlobUri = "cas://symbols/tenant-default/dbg001/blob", + CreatedAt = new DateTimeOffset(2026, 2, 26, 12, 0, 0, TimeSpan.Zero) + }; + + await File.WriteAllTextAsync( + Path.Combine(sourceDir, $"{debugId}.symbols.json"), + JsonSerializer.Serialize(manifest, JsonOptions)); + await File.WriteAllBytesAsync( + Path.Combine(sourceDir, $"{debugId}.sym"), + Encoding.UTF8.GetBytes("deterministic-symbol-blob-content")); + + string? signingKeyPath = null; + if (sign) + { + using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + signingKeyPath = Path.Combine(rootDir, "signing-key.pem"); + await File.WriteAllTextAsync(signingKeyPath, ecdsa.ExportECPrivateKeyPem()); + } + + var builder = new BundleBuilder(NullLogger.Instance); + var build = await builder.BuildAsync(new BundleBuildOptions + { + Name = "symbols-fixture", + Version = "1.0.0", + SourceDir = sourceDir, + OutputDir = outputDir, + Sign = sign, + SigningKeyPath = signingKeyPath, + SubmitRekor = submitRekor, + RekorUrl = "https://rekor.example.test" + }); + + Assert.True(build.Success, build.Error); + Assert.NotNull(build.BundlePath); + + return new TestFixture(builder, build.BundlePath!, rootDir); + } + + private static async Task RewriteManifestAsync( + string bundlePath, + Func rewrite) + { + var tempDir = Path.Combine(Path.GetTempPath(), "stella-symbols-mutate", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDir); + + try + { + ZipFile.ExtractToDirectory(bundlePath, tempDir); + var manifestPath = Path.Combine(tempDir, "manifest.json"); + var manifest = JsonSerializer.Deserialize(await File.ReadAllTextAsync(manifestPath)) + ?? throw new InvalidOperationException("Bundle manifest is missing or invalid."); + + var mutated = rewrite(manifest); + await File.WriteAllTextAsync(manifestPath, JsonSerializer.Serialize(mutated, JsonOptions)); + + File.Delete(bundlePath); + ZipFile.CreateFromDirectory(tempDir, bundlePath, CompressionLevel.NoCompression, includeBaseDirectory: false); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } + + private static string TamperBase64(string input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + var replacement = input[0] == 'A' ? 'B' : 'A'; + return replacement + input[1..]; + } + + private sealed record TestFixture(BundleBuilder Builder, string BundlePath, string RootDir) : IDisposable + { + public void Dispose() + { + if (Directory.Exists(RootDir)) + { + Directory.Delete(RootDir, recursive: true); + } + } + } +} diff --git a/src/Cli/__Tests/StellaOps.Cli.Tests/Commands/Sprint222ProofVerificationTests.cs b/src/Cli/__Tests/StellaOps.Cli.Tests/Commands/Sprint222ProofVerificationTests.cs new file mode 100644 index 000000000..a72624385 --- /dev/null +++ b/src/Cli/__Tests/StellaOps.Cli.Tests/Commands/Sprint222ProofVerificationTests.cs @@ -0,0 +1,359 @@ +using System.CommandLine; +using System.Formats.Tar; +using System.IO.Compression; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using StellaOps.Attestor.Core.Verification; +using StellaOps.Cli.Commands; +using StellaOps.TestKit; +using Xunit; + +namespace StellaOps.Cli.Tests.Commands; + +[Trait("Category", TestCategories.Unit)] +public sealed class Sprint222ProofVerificationTests : IDisposable +{ + private readonly string _tempRoot; + + public Sprint222ProofVerificationTests() + { + _tempRoot = Path.Combine(Path.GetTempPath(), $"stellaops-sprint222-{Guid.NewGuid():N}"); + Directory.CreateDirectory(_tempRoot); + } + + public void Dispose() + { + try + { + if (Directory.Exists(_tempRoot)) + { + Directory.Delete(_tempRoot, recursive: true); + } + } + catch + { + // Best effort cleanup. + } + } + + [Fact] + public async Task SbomVerify_WithValidRsaSignature_ReportsVerified() + { + var (archivePath, trustRootPath) = CreateSignedSbomArchive(tamperSignature: false); + var verboseOption = new Option("--verbose"); + var root = new RootCommand { SbomCommandGroup.BuildSbomCommand(verboseOption, CancellationToken.None) }; + + var result = await InvokeAsync(root, $"sbom verify --archive \"{archivePath}\" --trust-root \"{trustRootPath}\" --format json"); + result.ExitCode.Should().Be(0, $"stdout: {result.StdOut}\nstderr: {result.StdErr}"); + + using var json = JsonDocument.Parse(result.StdOut); + var dsseCheck = FindCheck(json.RootElement, "DSSE envelope signature"); + dsseCheck.GetProperty("passed").GetBoolean().Should().BeTrue(); + dsseCheck.GetProperty("details").GetString().Should().Contain("dsse-signature-verified"); + } + + [Fact] + public async Task SbomVerify_WithTamperedSignature_ReturnsDeterministicFailureReason() + { + var (archivePath, trustRootPath) = CreateSignedSbomArchive(tamperSignature: true); + var verboseOption = new Option("--verbose"); + var root = new RootCommand { SbomCommandGroup.BuildSbomCommand(verboseOption, CancellationToken.None) }; + + var result = await InvokeAsync(root, $"sbom verify --archive \"{archivePath}\" --trust-root \"{trustRootPath}\" --format json"); + result.ExitCode.Should().Be(1, $"stdout: {result.StdOut}\nstderr: {result.StdErr}"); + + using var json = JsonDocument.Parse(result.StdOut); + var dsseCheck = FindCheck(json.RootElement, "DSSE envelope signature"); + dsseCheck.GetProperty("passed").GetBoolean().Should().BeFalse(); + dsseCheck.GetProperty("details").GetString().Should().Contain("dsse-signature-verification-failed"); + } + + [Fact] + public async Task SbomVerify_WithoutTrustRoot_ReturnsDeterministicTrustRootMissingReason() + { + var (archivePath, _) = CreateSignedSbomArchive(tamperSignature: false); + var verboseOption = new Option("--verbose"); + var root = new RootCommand { SbomCommandGroup.BuildSbomCommand(verboseOption, CancellationToken.None) }; + + var result = await InvokeAsync(root, $"sbom verify --archive \"{archivePath}\" --format json"); + result.ExitCode.Should().Be(1, $"stdout: {result.StdOut}\nstderr: {result.StdErr}"); + + using var json = JsonDocument.Parse(result.StdOut); + var dsseCheck = FindCheck(json.RootElement, "DSSE envelope signature"); + dsseCheck.GetProperty("passed").GetBoolean().Should().BeFalse(); + dsseCheck.GetProperty("details").GetString().Should().Be( + "trust-root-missing: supply --trust-root with trusted key/certificate material"); + } + + [Fact] + public async Task BundleVerify_WithValidRekorCheckpoint_ValidatesInclusion() + { + var (bundleDir, checkpointPath) = CreateBundleWithProof(mismatchCheckpointRoot: false); + var verboseOption = new Option("--verbose"); + var services = new ServiceCollection().BuildServiceProvider(); + var verifyCommand = BundleVerifyCommand.BuildVerifyBundleEnhancedCommand(services, verboseOption, CancellationToken.None); + var root = new RootCommand { verifyCommand }; + + var result = await InvokeAsync( + root, + $"verify --bundle \"{bundleDir}\" --rekor-checkpoint \"{checkpointPath}\" --output json --offline"); + result.ExitCode.Should().Be(0); + + using var json = JsonDocument.Parse(result.StdOut); + var inclusionCheck = FindCheck(json.RootElement, "rekor:inclusion"); + inclusionCheck.GetProperty("passed").GetBoolean().Should().BeTrue(); + inclusionCheck.GetProperty("message").GetString().Should().Contain("Inclusion verified"); + } + + [Fact] + public async Task BundleVerify_WithMismatchedCheckpointRoot_FailsDeterministically() + { + var (bundleDir, checkpointPath) = CreateBundleWithProof(mismatchCheckpointRoot: true); + var verboseOption = new Option("--verbose"); + var services = new ServiceCollection().BuildServiceProvider(); + var verifyCommand = BundleVerifyCommand.BuildVerifyBundleEnhancedCommand(services, verboseOption, CancellationToken.None); + var root = new RootCommand { verifyCommand }; + + var result = await InvokeAsync( + root, + $"verify --bundle \"{bundleDir}\" --rekor-checkpoint \"{checkpointPath}\" --output json --offline"); + result.ExitCode.Should().Be(1); + + using var json = JsonDocument.Parse(result.StdOut); + var inclusionCheck = FindCheck(json.RootElement, "rekor:inclusion"); + inclusionCheck.GetProperty("passed").GetBoolean().Should().BeFalse(); + inclusionCheck.GetProperty("message").GetString().Should().Be("proof-root-hash-mismatch-with-checkpoint"); + } + + [Fact] + public async Task BundleVerify_WithMissingCheckpointPath_FailsDeterministically() + { + var (bundleDir, _) = CreateBundleWithProof(mismatchCheckpointRoot: false); + var missingCheckpointPath = Path.Combine(_tempRoot, $"missing-checkpoint-{Guid.NewGuid():N}.json"); + var verboseOption = new Option("--verbose"); + var services = new ServiceCollection().BuildServiceProvider(); + var verifyCommand = BundleVerifyCommand.BuildVerifyBundleEnhancedCommand(services, verboseOption, CancellationToken.None); + var root = new RootCommand { verifyCommand }; + + var result = await InvokeAsync( + root, + $"verify --bundle \"{bundleDir}\" --rekor-checkpoint \"{missingCheckpointPath}\" --output json --offline"); + result.ExitCode.Should().Be(1); + + using var json = JsonDocument.Parse(result.StdOut); + var inclusionCheck = FindCheck(json.RootElement, "rekor:inclusion"); + inclusionCheck.GetProperty("passed").GetBoolean().Should().BeFalse(); + inclusionCheck.GetProperty("message").GetString().Should().StartWith("checkpoint-not-found:"); + } + + private (string ArchivePath, string TrustRootPath) CreateSignedSbomArchive(bool tamperSignature) + { + var archivePath = Path.Combine(_tempRoot, $"sbom-{Guid.NewGuid():N}.tar.gz"); + var trustRootPath = Path.Combine(_tempRoot, $"trust-{Guid.NewGuid():N}.pem"); + + var sbomJson = JsonSerializer.Serialize(new + { + spdxVersion = "SPDX-2.3", + SPDXID = "SPDXRef-DOCUMENT", + name = "test-sbom" + }); + + using var rsa = RSA.Create(2048); + var publicPem = rsa.ExportSubjectPublicKeyInfoPem(); + File.WriteAllText(trustRootPath, publicPem); + + var payloadBytes = Encoding.UTF8.GetBytes(sbomJson); + var payloadBase64 = Convert.ToBase64String(payloadBytes); + var pae = BuildDssePae("application/vnd.in-toto+json", payloadBytes); + var signature = rsa.SignData(pae, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + if (tamperSignature) + { + signature[0] ^= 0xFF; + } + + var dsseJson = JsonSerializer.Serialize(new + { + payloadType = "application/vnd.in-toto+json", + payload = payloadBase64, + signatures = new[] + { + new { keyid = "test-key", sig = Convert.ToBase64String(signature) } + } + }); + + var files = new Dictionary + { + ["sbom.spdx.json"] = sbomJson, + ["sbom.dsse.json"] = dsseJson + }; + + files["manifest.json"] = JsonSerializer.Serialize(new + { + schemaVersion = "1.0.0", + files = files.Select(entry => new + { + path = entry.Key, + sha256 = ComputeSha256Hex(entry.Value) + }).ToArray() + }); + + using var fileStream = File.Create(archivePath); + using var gzipStream = new GZipStream(fileStream, CompressionLevel.SmallestSize); + using var tarWriter = new TarWriter(gzipStream, TarEntryFormat.Ustar); + + foreach (var (name, content) in files.OrderBy(kv => kv.Key, StringComparer.Ordinal)) + { + var entry = new UstarTarEntry(TarEntryType.RegularFile, name) + { + DataStream = new MemoryStream(Encoding.UTF8.GetBytes(content), writable: false), + ModificationTime = new DateTimeOffset(2026, 2, 26, 0, 0, 0, TimeSpan.Zero), + Mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead + }; + tarWriter.WriteEntry(entry); + } + + return (archivePath, trustRootPath); + } + + private (string BundleDir, string CheckpointPath) CreateBundleWithProof(bool mismatchCheckpointRoot) + { + var bundleDir = Path.Combine(_tempRoot, $"bundle-{Guid.NewGuid():N}"); + Directory.CreateDirectory(bundleDir); + + var artifactPath = Path.Combine(bundleDir, "attestations", "sample.txt"); + Directory.CreateDirectory(Path.GetDirectoryName(artifactPath)!); + File.WriteAllText(artifactPath, "sample-artifact"); + var artifactDigest = $"sha256:{ComputeSha256Hex("sample-artifact")}"; + + var leafHash = SHA256.HashData(Encoding.UTF8.GetBytes("leaf-hash")); + var siblingHash = SHA256.HashData(Encoding.UTF8.GetBytes("sibling-hash")); + var rootHash = MerkleProofVerifier.HashInterior(leafHash, siblingHash); + var checkpointRootHash = mismatchCheckpointRoot + ? SHA256.HashData(Encoding.UTF8.GetBytes("different-root")) + : rootHash; + + var proofJson = JsonSerializer.Serialize(new + { + logIndex = 0, + treeSize = 2, + leafHash = Convert.ToHexString(leafHash).ToLowerInvariant(), + hashes = new[] { Convert.ToHexString(siblingHash).ToLowerInvariant() }, + rootHash = Convert.ToHexString(rootHash).ToLowerInvariant() + }); + File.WriteAllText(Path.Combine(bundleDir, "rekor.proof.json"), proofJson); + + var manifestJson = JsonSerializer.Serialize(new + { + schemaVersion = "2.0", + bundle = new + { + image = "registry.example.com/test:1.0", + digest = "sha256:testdigest", + artifacts = new[] + { + new + { + path = "attestations/sample.txt", + digest = artifactDigest, + mediaType = "text/plain" + } + } + }, + verify = new + { + expectations = new { payloadTypes = Array.Empty() } + } + }); + File.WriteAllText(Path.Combine(bundleDir, "manifest.json"), manifestJson); + + var checkpointPath = Path.Combine(_tempRoot, $"checkpoint-{Guid.NewGuid():N}.json"); + var checkpointJson = JsonSerializer.Serialize(new + { + treeSize = 2, + rootHash = Convert.ToHexString(checkpointRootHash).ToLowerInvariant() + }); + File.WriteAllText(checkpointPath, checkpointJson); + + return (bundleDir, checkpointPath); + } + + private static async Task InvokeAsync(RootCommand root, string args) + { + var stdout = new StringWriter(); + var stderr = new StringWriter(); + var originalOut = Console.Out; + var originalErr = Console.Error; + var originalExitCode = Environment.ExitCode; + Environment.ExitCode = 0; + + try + { + Console.SetOut(stdout); + Console.SetError(stderr); + var parseResult = root.Parse(args); + var returnCode = await parseResult.InvokeAsync().ConfigureAwait(false); + var exitCode = returnCode != 0 ? returnCode : Environment.ExitCode; + return new CommandInvocationResult(stdout.ToString(), stderr.ToString(), exitCode); + } + finally + { + Console.SetOut(originalOut); + Console.SetError(originalErr); + Environment.ExitCode = originalExitCode; + } + } + + private static byte[] BuildDssePae(string payloadType, byte[] payload) + { + var header = Encoding.UTF8.GetBytes("DSSEv1"); + var payloadTypeBytes = Encoding.UTF8.GetBytes(payloadType); + var payloadTypeLengthBytes = Encoding.UTF8.GetBytes(payloadTypeBytes.Length.ToString()); + var payloadLengthBytes = Encoding.UTF8.GetBytes(payload.Length.ToString()); + var space = new[] { (byte)' ' }; + + var output = new byte[ + header.Length + space.Length + payloadTypeLengthBytes.Length + space.Length + + payloadTypeBytes.Length + space.Length + payloadLengthBytes.Length + space.Length + + payload.Length]; + + var offset = 0; + Buffer.BlockCopy(header, 0, output, offset, header.Length); offset += header.Length; + Buffer.BlockCopy(space, 0, output, offset, space.Length); offset += space.Length; + Buffer.BlockCopy(payloadTypeLengthBytes, 0, output, offset, payloadTypeLengthBytes.Length); offset += payloadTypeLengthBytes.Length; + Buffer.BlockCopy(space, 0, output, offset, space.Length); offset += space.Length; + Buffer.BlockCopy(payloadTypeBytes, 0, output, offset, payloadTypeBytes.Length); offset += payloadTypeBytes.Length; + Buffer.BlockCopy(space, 0, output, offset, space.Length); offset += space.Length; + Buffer.BlockCopy(payloadLengthBytes, 0, output, offset, payloadLengthBytes.Length); offset += payloadLengthBytes.Length; + Buffer.BlockCopy(space, 0, output, offset, space.Length); offset += space.Length; + Buffer.BlockCopy(payload, 0, output, offset, payload.Length); + + return output; + } + + private static string ComputeSha256Hex(string content) + { + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content)); + return Convert.ToHexString(hash).ToLowerInvariant(); + } + + private static JsonElement FindCheck(JsonElement root, string checkName) + { + var checks = root.GetProperty("checks"); + foreach (var check in checks.EnumerateArray()) + { + if (check.TryGetProperty("name", out var name) && + string.Equals(name.GetString(), checkName, StringComparison.Ordinal)) + { + return check; + } + } + + throw new Xunit.Sdk.XunitException($"Check '{checkName}' not found in output."); + } + + private sealed record CommandInvocationResult(string StdOut, string StdErr, int ExitCode); +} diff --git a/src/Platform/StellaOps.Platform.WebService/TASKS.md b/src/Platform/StellaOps.Platform.WebService/TASKS.md index e945e47c6..b2302f669 100644 --- a/src/Platform/StellaOps.Platform.WebService/TASKS.md +++ b/src/Platform/StellaOps.Platform.WebService/TASKS.md @@ -44,4 +44,6 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229 | SPRINT_20260224_004-LOC-306 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: added dedicated `/settings/language` UX wiring that reuses Platform persisted language preference API for authenticated users. | | SPRINT_20260224_004-LOC-307 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: added Ukrainian locale support (`uk-UA`) across Platform translation assets and preference normalization aliases (`uk-UA`/`uk_UA`/`uk`/`ua`). | | SPRINT_20260224_004-LOC-308 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: platform locale catalog endpoint (`GET /api/v1/platform/localization/locales`) is now consumed by both UI and CLI locale-selection paths. | +| SPRINT_20260226_230-LOC-001 | DONE | Sprint `docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_230_Platform_locale_label_translation_corrections.md`: completed non-English translation correction across Platform/Web/shared localization bundles (`bg-BG`, `de-DE`, `es-ES`, `fr-FR`, `ru-RU`, `uk-UA`, `zh-CN`, `zh-TW`), including cleanup of placeholder/transliteration/malformed values (`Ezik`, leaked token markers, mojibake) and a context-quality pass for backend German resource bundles (`graph`, `policy`, `scanner`, `advisoryai`). | +| PLATFORM-223-001 | DONE | Sprint `docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_223_Platform_score_explain_contract_and_replay_alignment.md`: shipped deterministic score explain/replay contract completion (`unknowns`, `proof_ref`, deterministic replay envelope parsing/verification differences) and updated score API/module docs with contract notes. | diff --git a/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/ScoreExplainEndpointContractTests.cs b/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/ScoreExplainEndpointContractTests.cs new file mode 100644 index 000000000..70451c075 --- /dev/null +++ b/src/Platform/__Tests/StellaOps.Platform.WebService.Tests/ScoreExplainEndpointContractTests.cs @@ -0,0 +1,297 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using StellaOps.Platform.WebService.Contracts; +using StellaOps.Platform.WebService.Services; +using StellaOps.TestKit; +using Xunit; + +namespace StellaOps.Platform.WebService.Tests; + +public sealed class ScoreExplainEndpointContractTests : IClassFixture +{ + private readonly PlatformWebApplicationFactory _factory; + + public ScoreExplainEndpointContractTests(PlatformWebApplicationFactory factory) + { + _factory = factory; + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task Explain_WhenDigestExists_ReturnsDeterministicContract() + { + using var deterministicFactory = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => + { + services.RemoveAll(); + services.AddSingleton(); + }); + }); + + using var client = CreateClient(deterministicFactory, "tenant-score-explain-valid"); + const string digest = "sha256:abc123"; + + var explainResponseA = await client.GetAsync( + "/api/v1/score/explain/SHA256:ABC123", + TestContext.Current.CancellationToken); + explainResponseA.EnsureSuccessStatusCode(); + + using var explainA = JsonDocument.Parse( + await explainResponseA.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + var itemA = GetAnyProperty(explainA.RootElement, "item", "Item"); + + Assert.Equal("score.explain.v1", GetAnyProperty(itemA, "contract_version", "contractVersion", "ContractVersion").GetString()); + Assert.Equal(digest, GetAnyProperty(itemA, "digest", "Digest").GetString()); + Assert.Equal(digest.ToLowerInvariant(), GetAnyProperty(itemA, "deterministic_input_hash", "deterministicInputHash", "DeterministicInputHash").GetString()); + Assert.True(GetAnyProperty(itemA, "factors", "Factors").GetArrayLength() > 0); + Assert.True(GetAnyProperty(itemA, "sources", "Sources").GetArrayLength() > 0); + + var explainResponseB = await client.GetAsync( + $"/api/v1/score/explain/{Uri.EscapeDataString(digest)}", + TestContext.Current.CancellationToken); + explainResponseB.EnsureSuccessStatusCode(); + + using var explainB = JsonDocument.Parse( + await explainResponseB.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + var itemB = GetAnyProperty(explainB.RootElement, "item", "Item"); + + var itemAJson = JsonSerializer.Serialize(itemA); + var itemBJson = JsonSerializer.Serialize(itemB); + Assert.Equal(itemAJson, itemBJson); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task Explain_WhenDigestOmitsAlgorithm_NormalizesToSha256Deterministically() + { + using var deterministicFactory = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => + { + services.RemoveAll(); + services.AddSingleton(); + }); + }); + + using var client = CreateClient(deterministicFactory, "tenant-score-explain-normalized"); + var response = await client.GetAsync( + "/api/v1/score/explain/ABC123", + TestContext.Current.CancellationToken); + + response.EnsureSuccessStatusCode(); + using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + var item = GetAnyProperty(payload.RootElement, "item", "Item"); + Assert.Equal("sha256:abc123", GetAnyProperty(item, "digest", "Digest").GetString()); + Assert.Equal("sha256:abc123", GetAnyProperty(item, "deterministic_input_hash", "deterministicInputHash", "DeterministicInputHash").GetString()); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task Explain_WhenDigestMissing_ReturnsDeterministicNotFound() + { + using var client = CreateClient(_factory, "tenant-score-explain-not-found"); + + var response = await client.GetAsync( + "/api/v1/score/explain/sha256:does-not-exist", + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + var error = await response.Content.ReadFromJsonAsync( + cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(error); + Assert.Equal("not_found", error!.Code); + Assert.Equal("sha256:does-not-exist", error.Digest); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task Explain_WhenDigestInvalid_ReturnsDeterministicInvalidInput() + { + using var client = CreateClient(_factory, "tenant-score-explain-invalid"); + + var response = await client.GetAsync( + "/api/v1/score/explain/%20", + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var error = await response.Content.ReadFromJsonAsync( + cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(error); + Assert.Equal("invalid_input", error!.Code); + Assert.Equal(" ", error.Digest); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task Explain_WhenDigestHasMissingHashSegment_ReturnsDeterministicInvalidInput() + { + using var client = CreateClient(_factory, "tenant-score-explain-invalid-segment"); + + var response = await client.GetAsync( + $"/api/v1/score/explain/{Uri.EscapeDataString("sha256:")}", + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var error = await response.Content.ReadFromJsonAsync( + cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(error); + Assert.Equal("invalid_input", error!.Code); + Assert.Equal("sha256:", error.Digest); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task Explain_WhenBackendUnavailable_ReturnsDeterministicBackendUnavailable() + { + using var backendUnavailableFactory = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(services => + { + services.RemoveAll(); + services.AddSingleton(); + }); + }); + + using var client = CreateClient(backendUnavailableFactory, "tenant-score-explain-backend"); + + var response = await client.GetAsync( + "/api/v1/score/explain/sha256:abc123", + TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + var error = await response.Content.ReadFromJsonAsync( + cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(error); + Assert.Equal("backend_unavailable", error!.Code); + Assert.Equal("sha256:abc123", error.Digest); + } + + private static HttpClient CreateClient(WebApplicationFactory factory, string tenantId) + { + var client = factory.CreateClient(); + client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId); + client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "score-test-actor"); + return client; + } + + private sealed class ThrowingScoreEvaluationService : IScoreEvaluationService + { + public Task> EvaluateAsync(PlatformRequestContext context, ScoreEvaluateRequest request, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task> GetByIdAsync(PlatformRequestContext context, string scoreId, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task> GetExplanationAsync(PlatformRequestContext context, string digest, CancellationToken ct = default) => + throw new InvalidOperationException("backend unavailable"); + + public Task>> ListWeightManifestsAsync(PlatformRequestContext context, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task> GetWeightManifestAsync(PlatformRequestContext context, string version, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task> GetEffectiveWeightManifestAsync(PlatformRequestContext context, DateTimeOffset asOf, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task>> GetHistoryAsync(PlatformRequestContext context, string cveId, string? purl, int limit, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task> GetReplayAsync(PlatformRequestContext context, string scoreId, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task> VerifyReplayAsync(PlatformRequestContext context, ScoreVerifyRequest request, CancellationToken ct = default) => + throw new NotSupportedException(); + } + + private sealed class StaticScoreEvaluationService : IScoreEvaluationService + { + private static readonly ScoreExplainResponse Response = new() + { + ContractVersion = "score.explain.v1", + Digest = "sha256:abc123", + ScoreId = "score_abc123", + FinalScore = 62, + Bucket = "Investigate", + ComputedAt = DateTimeOffset.Parse("2026-02-26T12:00:00Z"), + DeterministicInputHash = "sha256:abc123", + ReplayLink = "/api/v1/score/score_abc123/replay", + Factors = + [ + new ScoreExplainFactor + { + Name = "reachability", + Weight = 0.25, + Value = 1.0, + Contribution = 0.25 + } + ], + Sources = + [ + new ScoreExplainSource + { + SourceType = "score_history", + SourceRef = "score-history:score_abc123", + SourceDigest = "sha256:abc123" + } + ] + }; + + public Task> EvaluateAsync(PlatformRequestContext context, ScoreEvaluateRequest request, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task> GetByIdAsync(PlatformRequestContext context, string scoreId, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task> GetExplanationAsync(PlatformRequestContext context, string digest, CancellationToken ct = default) + { + var normalized = digest.ToLowerInvariant(); + var value = string.Equals(normalized, "sha256:abc123", StringComparison.Ordinal) + ? Response + : null; + + return Task.FromResult(new PlatformCacheResult( + value, + DateTimeOffset.Parse("2026-02-26T12:00:00Z"), + Cached: true, + CacheTtlSeconds: 300)); + } + + public Task>> ListWeightManifestsAsync(PlatformRequestContext context, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task> GetWeightManifestAsync(PlatformRequestContext context, string version, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task> GetEffectiveWeightManifestAsync(PlatformRequestContext context, DateTimeOffset asOf, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task>> GetHistoryAsync(PlatformRequestContext context, string cveId, string? purl, int limit, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task> GetReplayAsync(PlatformRequestContext context, string scoreId, CancellationToken ct = default) => + throw new NotSupportedException(); + + public Task> VerifyReplayAsync(PlatformRequestContext context, ScoreVerifyRequest request, CancellationToken ct = default) => + throw new NotSupportedException(); + } + + private static JsonElement GetAnyProperty(JsonElement element, params string[] names) + { + foreach (var name in names) + { + if (element.TryGetProperty(name, out var value)) + { + return value; + } + } + + throw new KeyNotFoundException($"None of the expected properties [{string.Join(", ", names)}] were found."); + } +} diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/OciAttestationPublisherTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/OciAttestationPublisherTests.cs new file mode 100644 index 000000000..1934b4101 --- /dev/null +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/OciAttestationPublisherTests.cs @@ -0,0 +1,220 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using StellaOps.Cryptography; +using StellaOps.Scanner.Storage.Oci; +using StellaOps.Scanner.WebService.Contracts; +using StellaOps.Scanner.WebService.Options; +using StellaOps.Scanner.WebService.Services; +using StellaOps.TestKit; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography; +using Xunit; + +namespace StellaOps.Scanner.WebService.Tests; + +public sealed class OciAttestationPublisherTests +{ + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task PublishAsync_WhenDisabled_ReturnsSkippedDeterministically() + { + var handler = new OciRegistryHandler(); + using var httpClient = new HttpClient(handler); + var pusher = new OciArtifactPusher( + httpClient, + CryptoHashFactory.CreateDefault(), + new OciRegistryOptions + { + DefaultRegistry = "registry.example" + }, + NullLogger.Instance); + + var options = Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions + { + AttestationAttachment = new ScannerWebServiceOptions.AttestationAttachmentOptions + { + AutoAttach = false + } + }); + + var publisher = new OciAttestationPublisher( + options, + pusher, + NullLogger.Instance); + + var report = new ReportDocumentDto + { + ReportId = "report-disabled", + ImageDigest = "registry.example/stellaops/demo@sha256:subjectdigest", + GeneratedAt = DateTimeOffset.Parse("2026-02-26T00:00:00Z"), + Verdict = "allow" + }; + + var result = await publisher.PublishAsync(report, envelope: null, tenant: "tenant-a", CancellationToken.None); + + Assert.True(result.Success); + Assert.Equal(0, result.AttachmentCount); + Assert.Empty(result.Digests); + Assert.Equal("Attestation attachment disabled", result.Error); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task PublishAsync_WhenImageDigestMissing_ReturnsFailedDeterministically() + { + var handler = new OciRegistryHandler(); + using var httpClient = new HttpClient(handler); + var pusher = new OciArtifactPusher( + httpClient, + CryptoHashFactory.CreateDefault(), + new OciRegistryOptions + { + DefaultRegistry = "registry.example" + }, + NullLogger.Instance); + + var options = Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions + { + AttestationAttachment = new ScannerWebServiceOptions.AttestationAttachmentOptions + { + AutoAttach = true + } + }); + + var publisher = new OciAttestationPublisher( + options, + pusher, + NullLogger.Instance); + + var report = new ReportDocumentDto + { + ReportId = "report-missing-image", + ImageDigest = "", + GeneratedAt = DateTimeOffset.Parse("2026-02-26T00:00:00Z"), + Verdict = "blocked" + }; + + var result = await publisher.PublishAsync(report, envelope: null, tenant: "tenant-a", CancellationToken.None); + + Assert.False(result.Success); + Assert.Equal(0, result.AttachmentCount); + Assert.Empty(result.Digests); + Assert.Equal("Missing image digest", result.Error); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task PublishAsync_WhenEnabled_AttachesAttestationAndReturnsRealDigest() + { + var handler = new OciRegistryHandler(); + using var httpClient = new HttpClient(handler); + var pusher = new OciArtifactPusher( + httpClient, + CryptoHashFactory.CreateDefault(), + new OciRegistryOptions + { + DefaultRegistry = "registry.example" + }, + NullLogger.Instance); + + var options = Microsoft.Extensions.Options.Options.Create(new ScannerWebServiceOptions + { + AttestationAttachment = new ScannerWebServiceOptions.AttestationAttachmentOptions + { + AutoAttach = true, + ReplaceExisting = false, + PredicateTypes = new List + { + "stellaops.io/predicates/scan-result@v1" + } + } + }); + + var publisher = new OciAttestationPublisher( + options, + pusher, + NullLogger.Instance); + + var report = new ReportDocumentDto + { + ReportId = "report-1", + ImageDigest = "registry.example/stellaops/demo@sha256:subjectdigest", + GeneratedAt = DateTimeOffset.Parse("2026-02-26T00:00:00Z"), + Verdict = "blocked", + Policy = new ReportPolicyDto + { + Digest = "sha256:policy" + }, + Summary = new ReportSummaryDto + { + Total = 1, + Blocked = 1 + } + }; + + var envelope = new DsseEnvelopeDto + { + PayloadType = "application/vnd.stellaops.report+json", + Payload = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{}")), + Signatures = + [ + new DsseSignatureDto + { + KeyId = "key-1", + Sig = "signature-value" + } + ] + }; + + var result = await publisher.PublishAsync(report, envelope, "tenant-a", CancellationToken.None); + + Assert.True(result.Success); + Assert.Equal(1, result.AttachmentCount); + Assert.Single(result.Digests); + Assert.StartsWith("sha256:", result.Digests[0], StringComparison.Ordinal); + } + + private sealed class OciRegistryHandler : HttpMessageHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var path = request.RequestUri?.AbsolutePath ?? string.Empty; + + if (request.Method == HttpMethod.Head && path.Contains("/manifests/", StringComparison.Ordinal)) + { + return new HttpResponseMessage(HttpStatusCode.NotFound); + } + + if (request.Method == HttpMethod.Head && path.Contains("/blobs/", StringComparison.Ordinal)) + { + return new HttpResponseMessage(HttpStatusCode.NotFound); + } + + if (request.Method == HttpMethod.Post && path.EndsWith("/blobs/uploads/", StringComparison.Ordinal)) + { + var response = new HttpResponseMessage(HttpStatusCode.Accepted); + response.Headers.Location = new Uri("/v2/stellaops/demo/blobs/uploads/upload-id", UriKind.Relative); + return response; + } + + if (request.Method == HttpMethod.Put && path.Contains("/blobs/uploads/", StringComparison.Ordinal)) + { + return new HttpResponseMessage(HttpStatusCode.Created); + } + + if (request.Method == HttpMethod.Put && path.Contains("/manifests/", StringComparison.Ordinal)) + { + var response = new HttpResponseMessage(HttpStatusCode.Created); + var manifestBytes = request.Content is null + ? Array.Empty() + : await request.Content.ReadAsByteArrayAsync(cancellationToken); + var digest = $"sha256:{Convert.ToHexString(SHA256.HashData(manifestBytes)).ToLowerInvariant()}"; + response.Headers.TryAddWithoutValidation("Docker-Content-Digest", digest); + return response; + } + + return new HttpResponseMessage(HttpStatusCode.OK); + } + } +} diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReachabilityStackEndpointsTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReachabilityStackEndpointsTests.cs new file mode 100644 index 000000000..a6a1827ab --- /dev/null +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/ReachabilityStackEndpointsTests.cs @@ -0,0 +1,174 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using StellaOps.Scanner.Explainability.Assumptions; +using StellaOps.Scanner.Reachability.Stack; +using StellaOps.Scanner.WebService.Endpoints; +using StellaOps.TestKit; +using System.Net; +using System.Text.Json; +using Xunit; + +namespace StellaOps.Scanner.WebService.Tests; + +public sealed class ReachabilityStackEndpointsTests +{ + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetStack_WhenRepositoryNotConfigured_ReturnsNotImplemented() + { + await using var factory = ScannerApplicationFactory.CreateLightweight(); + await factory.InitializeAsync(); + using var client = factory.CreateClient(); + + var response = await client.GetAsync("/api/v1/reachability/finding-123/stack"); + + Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetStack_WhenRepositoryConfigured_ReturnsPersistedStack() + { + var repository = new InMemoryReachabilityStackRepository(); + await repository.StoreAsync(CreateSampleStack("finding-abc"), CancellationToken.None); + + await using var factory = ScannerApplicationFactory.CreateLightweight().WithOverrides( + configureServices: services => + { + services.RemoveAll(); + services.AddSingleton(repository); + }); + await factory.InitializeAsync(); + using var client = factory.CreateClient(); + + var response = await client.GetAsync("/api/v1/reachability/finding-abc/stack"); + var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("finding-abc", payload.GetProperty("findingId").GetString()); + Assert.Equal("Exploitable", payload.GetProperty("verdict").GetString()); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetStack_WhenRepositoryConfiguredButFindingMissing_ReturnsNotFound() + { + var repository = new InMemoryReachabilityStackRepository(); + + await using var factory = ScannerApplicationFactory.CreateLightweight().WithOverrides( + configureServices: services => + { + services.RemoveAll(); + services.AddSingleton(repository); + }); + await factory.InitializeAsync(); + using var client = factory.CreateClient(); + + var response = await client.GetAsync("/api/v1/reachability/finding-missing/stack"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetLayer_WhenRepositoryConfigured_ReturnsLayerPayload() + { + var repository = new InMemoryReachabilityStackRepository(); + await repository.StoreAsync(CreateSampleStack("finding-layer"), CancellationToken.None); + + await using var factory = ScannerApplicationFactory.CreateLightweight().WithOverrides( + configureServices: services => + { + services.RemoveAll(); + services.AddSingleton(repository); + }); + await factory.InitializeAsync(); + using var client = factory.CreateClient(); + + var response = await client.GetAsync("/api/v1/reachability/finding-layer/stack/layer/2"); + var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(payload.GetProperty("isResolved").GetBoolean()); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetLayer_WhenLayerNumberInvalid_ReturnsBadRequest() + { + var repository = new InMemoryReachabilityStackRepository(); + await repository.StoreAsync(CreateSampleStack("finding-invalid-layer"), CancellationToken.None); + + await using var factory = ScannerApplicationFactory.CreateLightweight().WithOverrides( + configureServices: services => + { + services.RemoveAll(); + services.AddSingleton(repository); + }); + await factory.InitializeAsync(); + using var client = factory.CreateClient(); + + var response = await client.GetAsync("/api/v1/reachability/finding-invalid-layer/stack/layer/4"); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + private static ReachabilityStack CreateSampleStack(string findingId) + { + return new ReachabilityStack + { + Id = $"stack-{findingId}", + FindingId = findingId, + Symbol = new VulnerableSymbol( + Name: "vulnerable_func", + Library: "libdemo.so", + Version: "1.0.0", + VulnerabilityId: "CVE-2026-0001", + Type: SymbolType.Function), + StaticCallGraph = new ReachabilityLayer1 + { + IsReachable = true, + Confidence = ConfidenceLevel.High, + AnalysisMethod = "unit-test" + }, + BinaryResolution = new ReachabilityLayer2 + { + IsResolved = true, + Confidence = ConfidenceLevel.High, + Reason = "resolved", + Resolution = new SymbolResolution( + SymbolName: "vulnerable_func", + ResolvedLibrary: "libdemo.so", + ResolvedVersion: "1.0.0", + SymbolVersion: null, + Method: ResolutionMethod.DirectLink) + }, + RuntimeGating = new ReachabilityLayer3 + { + IsGated = false, + Outcome = GatingOutcome.NotGated, + Confidence = ConfidenceLevel.High + }, + Verdict = ReachabilityVerdict.Exploitable, + AnalyzedAt = DateTimeOffset.Parse("2026-02-26T00:00:00Z"), + Explanation = "deterministic-test" + }; + } + + private sealed class InMemoryReachabilityStackRepository : IReachabilityStackRepository + { + private readonly Dictionary _items = new(StringComparer.Ordinal); + + public Task TryGetByFindingIdAsync(string findingId, CancellationToken ct) + { + _items.TryGetValue(findingId, out var stack); + return Task.FromResult(stack); + } + + public Task StoreAsync(ReachabilityStack stack, CancellationToken ct) + { + _items[stack.FindingId] = stack; + return Task.CompletedTask; + } + } +} diff --git a/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SliceQueryServiceRetrievalTests.cs b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SliceQueryServiceRetrievalTests.cs new file mode 100644 index 000000000..4d5f438b7 --- /dev/null +++ b/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SliceQueryServiceRetrievalTests.cs @@ -0,0 +1,216 @@ +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using StellaOps.Cryptography; +using StellaOps.Replay.Core; +using StellaOps.Scanner.Cache; +using StellaOps.Scanner.Cache.Abstractions; +using StellaOps.Scanner.Cache.FileCas; +using StellaOps.Scanner.Core; +using StellaOps.Scanner.ProofSpine; +using StellaOps.Scanner.Reachability.Slices; +using StellaOps.Scanner.WebService.Services; +using StellaOps.TestKit; +using System.Collections.Immutable; +using System.Text; +using System.Text.Json; +using Xunit; + +namespace StellaOps.Scanner.WebService.Tests; + +public sealed class SliceQueryServiceRetrievalTests : IDisposable +{ + private readonly string _tempRoot; + private readonly IFileContentAddressableStore _cas; + private readonly SliceQueryService _service; + + public SliceQueryServiceRetrievalTests() + { + _tempRoot = Path.Combine(Path.GetTempPath(), $"stella-slice-query-{Guid.NewGuid():N}"); + + var scannerCacheOptions = Microsoft.Extensions.Options.Options.Create(new ScannerCacheOptions + { + RootPath = _tempRoot, + FileTtl = TimeSpan.FromDays(1), + MaxBytes = 1024 * 1024 * 10 + }); + _cas = new FileContentAddressableStore( + scannerCacheOptions, + NullLogger.Instance, + TimeProvider.System); + + var cryptoHash = CryptoHashFactory.CreateDefault(); + var sliceHasher = new SliceHasher(cryptoHash); + var sliceSigner = new SliceDsseSigner( + new TestDsseSigningService(), + new TestCryptoProfile(), + sliceHasher, + TimeProvider.System); + var casStorage = new SliceCasStorage(sliceHasher, sliceSigner, cryptoHash); + + _service = new SliceQueryService( + cache: new SliceCache(Microsoft.Extensions.Options.Options.Create(new SliceCacheOptions())), + extractor: new SliceExtractor(new VerdictComputer()), + casStorage: casStorage, + diffComputer: new StellaOps.Scanner.Reachability.Slices.Replay.SliceDiffComputer(), + hasher: sliceHasher, + cas: _cas, + scannerCacheOptions: scannerCacheOptions, + scanRepo: new NullScanMetadataRepository(), + timeProvider: TimeProvider.System, + options: Microsoft.Extensions.Options.Options.Create(new SliceQueryServiceOptions()), + logger: NullLogger.Instance); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetSliceAsync_WhenSliceExistsInCas_ReturnsSlice() + { + const string digestHex = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + var bytes = JsonSerializer.SerializeToUtf8Bytes(CreateSlice("scan-a")); + await _cas.PutAsync(new FileCasPutRequest(digestHex, new MemoryStream(bytes))); + + var result = await _service.GetSliceAsync($"sha256:{digestHex}"); + + Assert.NotNull(result); + Assert.Equal("scan-a", result!.Manifest.ScanId); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetSliceAsync_WhenSliceMissingInCas_ReturnsNull() + { + var result = await _service.GetSliceAsync("sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); + + Assert.Null(result); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetSliceAsync_WhenSlicePayloadCorrupt_ThrowsDeterministicError() + { + const string digestHex = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; + await _cas.PutAsync(new FileCasPutRequest(digestHex, new MemoryStream(Encoding.UTF8.GetBytes("not-json")))); + + var action = async () => await _service.GetSliceAsync($"sha256:{digestHex}"); + + var ex = await Assert.ThrowsAsync(action); + Assert.Contains("corrupt", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetSliceDsseAsync_WhenEnvelopeExists_ReturnsEnvelope() + { + const string digestHex = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; + var envelope = new DsseEnvelope( + PayloadType: "application/vnd.stellaops.slice+json", + Payload: Convert.ToBase64String(Encoding.UTF8.GetBytes("{}")), + Signatures: [new DsseSignature("k1", "s1")]); + var envelopeBytes = JsonSerializer.SerializeToUtf8Bytes(envelope); + await _cas.PutAsync(new FileCasPutRequest($"{digestHex}.dsse", new MemoryStream(envelopeBytes))); + + var result = await _service.GetSliceDsseAsync($"sha256:{digestHex}"); + + Assert.NotNull(result); + var typed = Assert.IsType(result); + Assert.Equal("application/vnd.stellaops.slice+json", typed.PayloadType); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetSliceDsseAsync_WhenEnvelopeCorrupt_ThrowsDeterministicError() + { + const string digestHex = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; + await _cas.PutAsync(new FileCasPutRequest($"{digestHex}.dsse", new MemoryStream(Encoding.UTF8.GetBytes("{broken-json")))); + + var action = async () => await _service.GetSliceDsseAsync($"sha256:{digestHex}"); + + var ex = await Assert.ThrowsAsync(action); + Assert.Contains("corrupt", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Trait("Category", TestCategories.Unit)] + [Fact] + public async Task GetSliceDsseAsync_WhenEnvelopeMissing_ReturnsNull() + { + var result = await _service.GetSliceDsseAsync("sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + + Assert.Null(result); + } + + public void Dispose() + { + try + { + Directory.Delete(_tempRoot, recursive: true); + } + catch (IOException) + { + } + catch (UnauthorizedAccessException) + { + } + } + + private static ReachabilitySlice CreateSlice(string scanId) + { + return new ReachabilitySlice + { + Inputs = new SliceInputs + { + GraphDigest = "sha256:graph" + }, + Query = new SliceQuery + { + CveId = "CVE-2026-0001", + TargetSymbols = ImmutableArray.Create("target") + }, + Subgraph = new SliceSubgraph + { + Nodes = ImmutableArray.Empty, + Edges = ImmutableArray.Empty + }, + Verdict = new SliceVerdict + { + Status = SliceVerdictStatus.Unknown, + Confidence = 0.4 + }, + Manifest = ScanManifest.CreateBuilder(scanId, "sha256:artifact") + .WithScannerVersion("1.0.0") + .WithWorkerVersion("1.0.0") + .WithConcelierSnapshot("sha256:concelier") + .WithExcititorSnapshot("sha256:excititor") + .WithLatticePolicyHash("sha256:policy") + .Build() + }; + } + + private sealed class TestCryptoProfile : ICryptoProfile + { + public string KeyId => "test-key"; + + public string Algorithm => "hs256"; + } + + private sealed class TestDsseSigningService : IDsseSigningService + { + public Task SignAsync(object payload, string payloadType, ICryptoProfile cryptoProfile, CancellationToken cancellationToken = default) + { + var payloadBytes = CanonicalJson.SerializeToUtf8Bytes(payload); + var envelope = new DsseEnvelope( + PayloadType: payloadType, + Payload: Convert.ToBase64String(payloadBytes), + Signatures: [new DsseSignature(cryptoProfile.KeyId, "sig")]); + return Task.FromResult(envelope); + } + + public Task VerifyAsync(DsseEnvelope envelope, CancellationToken cancellationToken = default) + => Task.FromResult(new DsseVerificationOutcome(true, true, null)); + } + + private sealed class NullScanMetadataRepository : IScanMetadataRepository + { + public Task GetScanMetadataAsync(string scanId, CancellationToken cancellationToken = default) + => Task.FromResult(null); + } +}