feat: Add Storybook configuration and motion tokens implementation
- Introduced Storybook configuration files (`main.ts`, `preview.ts`, `tsconfig.json`) for Angular components. - Created motion tokens in `motion-tokens.ts` to define durations, easing functions, and transforms. - Developed a Storybook story for motion tokens showcasing their usage and reduced motion fallback. - Added SCSS variables for motion durations, easing, and transforms in `_motion.scss`. - Implemented accessibility smoke tests using Playwright and Axe for automated accessibility checks. - Created portable and sealed bundle structures with corresponding JSON files for evidence locker. - Added shell script for verifying notify kit determinism.
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
| 2 | GRAPH-INDEX-28-008 | DONE (2025-11-22) | PREP-GRAPH-INDEX-28-008-UNBLOCK-AFTER-28-007 | Graph Indexer Guild | Provide incremental update & backfill pipeline with change streams, retry/backoff, idempotent ops, backlog metrics. |
|
| 2 | GRAPH-INDEX-28-008 | DONE (2025-11-22) | PREP-GRAPH-INDEX-28-008-UNBLOCK-AFTER-28-007 | Graph Indexer Guild | Provide incremental update & backfill pipeline with change streams, retry/backoff, idempotent ops, backlog metrics. |
|
||||||
| 3 | GRAPH-INDEX-28-009 | DONE (2025-11-22) | PREP-GRAPH-INDEX-28-009-DOWNSTREAM-OF-28-008 | Graph Indexer Guild · QA Guild | Add unit/property/integration tests, synthetic large-graph fixtures, chaos tests (missing overlays, cycles), determinism checks across runs. |
|
| 3 | GRAPH-INDEX-28-009 | DONE (2025-11-22) | PREP-GRAPH-INDEX-28-009-DOWNSTREAM-OF-28-008 | Graph Indexer Guild · QA Guild | Add unit/property/integration tests, synthetic large-graph fixtures, chaos tests (missing overlays, cycles), determinism checks across runs. |
|
||||||
| 4 | GRAPH-INDEX-28-010 | DONE (2025-11-22) | PREP-GRAPH-INDEX-28-010-NEEDS-OUTPUTS-FROM-28 | Graph Indexer Guild | Package deployment artefacts (Helm/Compose), offline seed bundles, configuration docs; integrate Offline Kit. |
|
| 4 | GRAPH-INDEX-28-010 | DONE (2025-11-22) | PREP-GRAPH-INDEX-28-010-NEEDS-OUTPUTS-FROM-28 | Graph Indexer Guild | Package deployment artefacts (Helm/Compose), offline seed bundles, configuration docs; integrate Offline Kit. |
|
||||||
|
| 5 | CARTO-GRAPH-21-002-INGEST | DONE (2025-12-04) | graph.inspect.v1 contract published | Graph Indexer Guild (`src/Graph/StellaOps.Graph.Indexer`) | Add `graph.inspect.v1` transformer + tests so Graph Indexer can ingest Concelier/Excititor inspector payloads (advisory + VEX linkouts, relationships). |
|
||||||
|
|
||||||
## Execution Log
|
## Execution Log
|
||||||
| Date (UTC) | Update | Owner |
|
| Date (UTC) | Update | Owner |
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
| 2025-11-22 | Implemented analytics jobs (28-007), change-stream/backfill pipeline (28-008), determinism fixtures/tests (28-009), and packaging/offline doc updates (28-010); status set to DONE. | Graph Indexer Guild |
|
| 2025-11-22 | Implemented analytics jobs (28-007), change-stream/backfill pipeline (28-008), determinism fixtures/tests (28-009), and packaging/offline doc updates (28-010); status set to DONE. | Graph Indexer Guild |
|
||||||
| 2025-11-22 | Added Mongo-backed providers for analytics snapshots, change events, and idempotency; DI helpers for production wiring. | Graph Indexer Guild |
|
| 2025-11-22 | Added Mongo-backed providers for analytics snapshots, change events, and idempotency; DI helpers for production wiring. | Graph Indexer Guild |
|
||||||
| 2025-11-22 | Added Mongo database DI registration helper + integration tests; updated packaging env vars for connection/db names. | Graph Indexer Guild |
|
| 2025-11-22 | Added Mongo database DI registration helper + integration tests; updated packaging env vars for connection/db names. | Graph Indexer Guild |
|
||||||
|
| 2025-12-04 | Added `graph.inspect.v1` ingestion support (transformer + unit test) aligned to Cartographer inspector contract; status recorded as CARTO-GRAPH-21-002-INGEST DONE. | Graph Indexer Guild |
|
||||||
|
|
||||||
## Decisions & Risks
|
## Decisions & Risks
|
||||||
- Operating on scanner surface mock bundle v1 until real caches arrive; reassess when Sprint 130.A delivers caches.
|
- Operating on scanner surface mock bundle v1 until real caches arrive; reassess when Sprint 130.A delivers caches.
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
| Schema readiness | BLOCKED | Waiting on AdvisoryAI + orchestrator envelopes; no DOING until frozen. |
|
| Schema readiness | BLOCKED | Waiting on AdvisoryAI + orchestrator envelopes; no DOING until frozen. |
|
||||||
| Crypto routing approval | DONE | Defaults recorded in `docs/security/crypto-registry-decision-2025-11-18.md`; implement in EvidenceLocker/CLI. |
|
| Crypto routing approval | DONE | Defaults recorded in `docs/security/crypto-registry-decision-2025-11-18.md`; implement in EvidenceLocker/CLI. |
|
||||||
| Template & filename normalization | DONE (2025-11-17) | Renamed to `SPRINT_0161_0001_0001_evidencelocker.md`; structure aligned to sprint template. |
|
| Template & filename normalization | DONE (2025-11-17) | Renamed to `SPRINT_0161_0001_0001_evidencelocker.md`; structure aligned to sprint template. |
|
||||||
| EB1–EB10 policy freeze | OPEN | Gap plan at `docs/modules/evidence-locker/eb-gaps-161-007-plan.md`; DSSE predicate/log policy, redaction map, and chunking rules still need sign-off. |
|
| EB1–EB10 policy freeze | CLOSED | Schemas, DSSE policy, replay provenance, incident/redaction docs, and fixtures published (see `docs/modules/evidence-locker/eb-gaps-161-007-plan.md`); SemVer/changelog still pending under EB10. |
|
||||||
|
|
||||||
### Risk table
|
### Risk table
|
||||||
| Risk | Severity | Mitigation / Owner |
|
| Risk | Severity | Mitigation / Owner |
|
||||||
@@ -91,3 +91,4 @@
|
|||||||
| 2025-12-02 | Scoped EVID-GAPS-161-007 deliverables: schemas + DSSE, Merkle recipe, replay provenance, chunk/CAS rules, incident governance, tenant redaction, offline verifier doc, golden fixtures path, and SemVer/change-log updates. | Project Mgmt |
|
| 2025-12-02 | Scoped EVID-GAPS-161-007 deliverables: schemas + DSSE, Merkle recipe, replay provenance, chunk/CAS rules, incident governance, tenant redaction, offline verifier doc, golden fixtures path, and SemVer/change-log updates. | Project Mgmt |
|
||||||
| 2025-12-04 | Moved EVID-GAPS-161-007 to DOING; drafted EB1/EB2 schemas, offline verifier guide, gap plan, and golden fixtures path. | Project Mgmt |
|
| 2025-12-04 | Moved EVID-GAPS-161-007 to DOING; drafted EB1/EB2 schemas, offline verifier guide, gap plan, and golden fixtures path. | Project Mgmt |
|
||||||
| 2025-12-04 | Updated attestation, replay, incident-mode docs with DSSE subject=Merkle root, log policy, replay provenance block, and signed incident toggles; added CAS/Merkle rules to bundle packaging. | Implementer |
|
| 2025-12-04 | Updated attestation, replay, incident-mode docs with DSSE subject=Merkle root, log policy, replay provenance block, and signed incident toggles; added CAS/Merkle rules to bundle packaging. | Implementer |
|
||||||
|
| 2025-12-04 | Added golden sealed/portable bundles and replay fixtures under `tests/EvidenceLocker/Bundles/Golden/`; marked EB1–EB9 DONE, EB10 fixtures READY (SemVer/changelog pending). | Implementer |
|
||||||
|
|||||||
@@ -34,11 +34,12 @@
|
|||||||
| 11 | NOTIFY-RISK-68-001 | BLOCKED (2025-11-22) | Depends on 67-001. | Notifications Service Guild | Per-profile routing, quiet hours, dedupe for risk alerts; integrate CLI/Console preferences. |
|
| 11 | NOTIFY-RISK-68-001 | BLOCKED (2025-11-22) | Depends on 67-001. | Notifications Service Guild | Per-profile routing, quiet hours, dedupe for risk alerts; integrate CLI/Console preferences. |
|
||||||
| 12 | NOTIFY-DOC-70-001 | DONE (2025-11-02) | — | Notifications Service Guild | Document split between legacy `src/Notify` libs and new `src/Notifier` runtime; update architecture docs. |
|
| 12 | NOTIFY-DOC-70-001 | DONE (2025-11-02) | — | Notifications Service Guild | Document split between legacy `src/Notify` libs and new `src/Notifier` runtime; update architecture docs. |
|
||||||
| 13 | NOTIFY-AIRGAP-56-002 | DONE | — | Notifications Service Guild · DevOps Guild | Bootstrap Pack notifier configs with deterministic secrets handling and offline validation. |
|
| 13 | NOTIFY-AIRGAP-56-002 | DONE | — | Notifications Service Guild · DevOps Guild | Bootstrap Pack notifier configs with deterministic secrets handling and offline validation. |
|
||||||
| 14 | NOTIFY-GAPS-171-014 | TODO | NR1–NR10 defined in `31-Nov-2025 FINDINGS.md` + `docs/notifications/gaps-nr1-nr10.md`; implement schema/catalog + evidence bundle | Notifications Service Guild / src/Notifier/StellaOps.Notifier | Remediate NR1–NR10: publish signed schemas + canonical JSON, enforce tenant scoping/approvals, deterministic rendering, quotas/backpressure + DLQ, retry/idempotency policy, webhook/ack security, redaction/PII limits, observability SLO alerts, offline notify-kit with DSSE, and mandatory simulations + evidence for rule/template changes. |
|
| 14 | NOTIFY-GAPS-171-014 | DOING (2025-12-04) | NR1–NR10 defined; schemas/kit/docs scaffolded; fill hashes + signatures next | Notifications Service Guild / src/Notifier/StellaOps.Notifier | Remediate NR1–NR10: publish signed schemas + canonical JSON, enforce tenant scoping/approvals, deterministic rendering, quotas/backpressure + DLQ, retry/idempotency policy, webhook/ack security, redaction/PII limits, observability SLO alerts, offline notify-kit with DSSE, and mandatory simulations + evidence for rule/template changes. |
|
||||||
|
|
||||||
## Execution Log
|
## Execution Log
|
||||||
| Date (UTC) | Update | Owner |
|
| Date (UTC) | Update | Owner |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| 2025-12-04 | Scaffolded NR1–NR10 artefacts (schemas, catalog, DSSE placeholders, quota/retry/security docs, fixtures, offline kit manifest + verify script) and set NOTIFY-GAPS-171-014 to DOING. | Implementer |
|
||||||
| 2025-12-04 | Authored NR1–NR10 section and blueprint (`docs/notifications/gaps-nr1-nr10.md`); unblocked NOTIFY-GAPS-171-014 and set status to TODO. | Implementer |
|
| 2025-12-04 | Authored NR1–NR10 section and blueprint (`docs/notifications/gaps-nr1-nr10.md`); unblocked NOTIFY-GAPS-171-014 and set status to TODO. | Implementer |
|
||||||
| 2025-11-19 | Fixed PREP-NOTIFY-OBS-51-001 Task ID (removed trailing hyphen) so dependency lookup works. | Project Mgmt |
|
| 2025-11-19 | Fixed PREP-NOTIFY-OBS-51-001 Task ID (removed trailing hyphen) so dependency lookup works. | Project Mgmt |
|
||||||
| 2025-12-01 | Added NOTIFY-GAPS-171-014 (NR1–NR10 from `31-Nov-2025 FINDINGS.md`) to track advisory gap remediation; status TODO pending schema/catalog refresh. | Project Mgmt |
|
| 2025-12-01 | Added NOTIFY-GAPS-171-014 (NR1–NR10 from `31-Nov-2025 FINDINGS.md`) to track advisory gap remediation; status TODO pending schema/catalog refresh. | Project Mgmt |
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
| 17 | UI-POLICY-DET-01 | DONE | UI-SBOM-DET-01 | UI Guild; Policy Guild (src/Web/StellaOps.Web) | Wire policy gate indicators and remediation hints into Release/Policy flows, blocking publishes when determinism checks fail; coordinate with Policy Engine schema updates. |
|
| 17 | UI-POLICY-DET-01 | DONE | UI-SBOM-DET-01 | UI Guild; Policy Guild (src/Web/StellaOps.Web) | Wire policy gate indicators and remediation hints into Release/Policy flows, blocking publishes when determinism checks fail; coordinate with Policy Engine schema updates. |
|
||||||
| 18 | UI-ENTROPY-40-001 | DONE | - | UI Guild (src/Web/StellaOps.Web) | Visualise entropy analysis per image (layer donut, file heatmaps, "Why risky?" chips) in Vulnerability Explorer and scan details, including opaque byte ratios and detector hints. |
|
| 18 | UI-ENTROPY-40-001 | DONE | - | UI Guild (src/Web/StellaOps.Web) | Visualise entropy analysis per image (layer donut, file heatmaps, "Why risky?" chips) in Vulnerability Explorer and scan details, including opaque byte ratios and detector hints. |
|
||||||
| 19 | UI-ENTROPY-40-002 | DONE | UI-ENTROPY-40-001 | UI Guild; Policy Guild (src/Web/StellaOps.Web) | Add policy banners/tooltips explaining entropy penalties (block/warn thresholds, mitigation steps) and link to raw `entropy.report.json` evidence downloads. |
|
| 19 | UI-ENTROPY-40-002 | DONE | UI-ENTROPY-40-001 | UI Guild; Policy Guild (src/Web/StellaOps.Web) | Add policy banners/tooltips explaining entropy penalties (block/warn thresholds, mitigation steps) and link to raw `entropy.report.json` evidence downloads. |
|
||||||
| 20 | UI-MICRO-GAPS-0209-011 | BLOCKED | Canonical 30-Nov-2025 UI Micro-Interactions advisory published; still need motion token catalog plus a11y/Storybook/Playwright harness in `src/Web/StellaOps.Web`. | UI Guild; UX Guild; Accessibility Guild | Close MI1–MI10: define motion tokens + reduced-motion rules, perf budgets, offline/latency/error patterns, component mapping, telemetry schema/flags, deterministic seeds/snapshots, micro-copy localisation, and theme/contrast guidance; add Storybook/Playwright checks. |
|
| 20 | UI-MICRO-GAPS-0209-011 | DOING | Motion token catalog + Storybook/Playwright a11y harness added in `src/Web/StellaOps.Web`; remaining: component mapping, perf budgets, deterministic snapshots, micro-copy localisation. | UI Guild; UX Guild; Accessibility Guild | Close MI1–MI10: define motion tokens + reduced-motion rules, perf budgets, offline/latency/error patterns, component mapping, telemetry schema/flags, deterministic seeds/snapshots, micro-copy localisation, and theme/contrast guidance; add Storybook/Playwright checks. |
|
||||||
|
|
||||||
## Wave Coordination
|
## Wave Coordination
|
||||||
- Single-wave execution; coordinate with UI II/III only for shared component changes and accessibility tokens.
|
- Single-wave execution; coordinate with UI II/III only for shared component changes and accessibility tokens.
|
||||||
@@ -76,6 +76,7 @@
|
|||||||
| 5 | Receive SDK parity matrix (Wave B, SPRINT_0208_0001_0001_sdk) to unblock Console data providers and scope exports | UI Guild · SDK Generator Guild | 2025-12-16 | BLOCKED (awaiting SDK parity delivery) |
|
| 5 | Receive SDK parity matrix (Wave B, SPRINT_0208_0001_0001_sdk) to unblock Console data providers and scope exports | UI Guild · SDK Generator Guild | 2025-12-16 | BLOCKED (awaiting SDK parity delivery) |
|
||||||
| 6 | Publish canonical UI Micro-Interactions advisory (MI1–MI10) with motion tokens, reduced-motion rules, and fixtures referenced by this sprint | Product Mgmt · UX Guild | 2025-12-06 | DONE |
|
| 6 | Publish canonical UI Micro-Interactions advisory (MI1–MI10) with motion tokens, reduced-motion rules, and fixtures referenced by this sprint | Product Mgmt · UX Guild | 2025-12-06 | DONE |
|
||||||
| 7 | Align sprint working directory to `src/Web/StellaOps.Web` and verify workspace present (was `src/UI/StellaOps.UI`) | UI Guild | 2025-12-05 | DONE (2025-12-04) |
|
| 7 | Align sprint working directory to `src/Web/StellaOps.Web` and verify workspace present (was `src/UI/StellaOps.UI`) | UI Guild | 2025-12-05 | DONE (2025-12-04) |
|
||||||
|
| 8 | Refresh package-lock with new Storybook/a11y devDependencies (registry auth required) | UI Guild · DevEx | 2025-12-06 | TODO |
|
||||||
|
|
||||||
## Decisions & Risks
|
## Decisions & Risks
|
||||||
| Risk | Impact | Mitigation / Next Step |
|
| Risk | Impact | Mitigation / Next Step |
|
||||||
@@ -85,10 +86,12 @@
|
|||||||
| Entropy evidence format changes | Rework for UI-ENTROPY-* views | Lock to `docs/modules/scanner/entropy.md`; add contract test fixtures before UI wiring. |
|
| Entropy evidence format changes | Rework for UI-ENTROPY-* views | Lock to `docs/modules/scanner/entropy.md`; add contract test fixtures before UI wiring. |
|
||||||
| Working directory mismatch (UI vs Web) causes contributors to edit wrong path | Duplicate effort or missing workspace for new tasks | Sprint now points to `src/Web/StellaOps.Web`; Action #7 closed; broadcast path in AGENTS/TASKS updates. |
|
| Working directory mismatch (UI vs Web) causes contributors to edit wrong path | Duplicate effort or missing workspace for new tasks | Sprint now points to `src/Web/StellaOps.Web`; Action #7 closed; broadcast path in AGENTS/TASKS updates. |
|
||||||
| Micro-interaction implementation inputs incomplete | UI-MICRO-GAPS-0209-011 blocked on motion token catalog + a11y/Storybook/Playwright harness despite advisory availability | Keep Action #6 closed; open follow-on tasks for token catalog + harness once SDK scopes land. |
|
| Micro-interaction implementation inputs incomplete | UI-MICRO-GAPS-0209-011 blocked on motion token catalog + a11y/Storybook/Playwright harness despite advisory availability | Keep Action #6 closed; open follow-on tasks for token catalog + harness once SDK scopes land. |
|
||||||
|
| NPM registry auth expired for new devDependencies | Storybook/a11y harness cannot be installed; package-lock not updated | Resolve auth and rerun `npm install` (Action #8) to lock dependencies; dev code committed with pinned versions. |
|
||||||
|
|
||||||
## Execution Log
|
## Execution Log
|
||||||
| Date (UTC) | Update | Owner |
|
| Date (UTC) | Update | Owner |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| 2025-12-04 | Added motion token catalog (SCSS + TS), Storybook scaffolding with reduced-motion toggle, and Playwright a11y smoke harness. `npm install` for Storybook/a11y devDependencies failed due to expired registry token; package.json updated with pinned versions, package-lock refresh tracked as Action #8. | Implementer |
|
||||||
| 2025-12-04 | Confirmed canonical Angular workspace is `src/Web/StellaOps.Web` (not `src/UI/StellaOps.UI`); updated working directory, blockers, and Action #7 accordingly. Graph blockers now tied to generated `graph:*` SDK scopes. | Project mgmt |
|
| 2025-12-04 | Confirmed canonical Angular workspace is `src/Web/StellaOps.Web` (not `src/UI/StellaOps.UI`); updated working directory, blockers, and Action #7 accordingly. Graph blockers now tied to generated `graph:*` SDK scopes. | Project mgmt |
|
||||||
| 2025-12-04 | Published canonical UI Micro-Interactions advisory (`docs/product-advisories/30-Nov-2025 - UI Micro-Interactions for StellaOps.md`). UI-MICRO-GAPS-0209-011 remains BLOCKED pending motion token catalog + a11y/Storybook/Playwright harness in `src/Web/StellaOps.Web`. | Project mgmt |
|
| 2025-12-04 | Published canonical UI Micro-Interactions advisory (`docs/product-advisories/30-Nov-2025 - UI Micro-Interactions for StellaOps.md`). UI-MICRO-GAPS-0209-011 remains BLOCKED pending motion token catalog + a11y/Storybook/Playwright harness in `src/Web/StellaOps.Web`. | Project mgmt |
|
||||||
| 2025-12-04 | Earlier note: UI-MICRO-GAPS-0209-011 was marked BLOCKED when advisory was still pending and `src/UI/StellaOps.UI` was empty; superseded by publication + path correction the same day. | Project mgmt |
|
| 2025-12-04 | Earlier note: UI-MICRO-GAPS-0209-011 was marked BLOCKED when advisory was still pending and `src/UI/StellaOps.UI` was empty; superseded by publication + path correction the same day. | Project mgmt |
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ Working directory: `docs/implplan` (sprint coordination) with artefacts in `docs
|
|||||||
## Scope Items
|
## Scope Items
|
||||||
| ID | Deliverable | Artifact / Path | Owner(s) | Acceptance / Notes | Status |
|
| ID | Deliverable | Artifact / Path | Owner(s) | Acceptance / Notes | Status |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| EB1 | Publish canonical manifest schema | `docs/modules/evidence-locker/schemas/bundle.manifest.schema.json` | Evidence Locker Guild | JSON Schema matches EvidenceBundleManifest (bundleId, tenantId, kind, metadata, entries) and captures replay/incident/redaction hooks. | Draft (2025-12-04) |
|
| EB1 | Publish canonical manifest schema | `docs/modules/evidence-locker/schemas/bundle.manifest.schema.json` | Evidence Locker Guild | JSON Schema matches EvidenceBundleManifest (bundleId, tenantId, kind, metadata, entries) and captures replay/incident/redaction hooks. | DONE (2025-12-04) |
|
||||||
| EB2 | Publish checksums schema | `docs/modules/evidence-locker/schemas/checksums.schema.json` | Evidence Locker Guild | Canonical map for `checksums.txt`; Merkle root + chunking metadata; sorted entry rule recorded. | Draft (2025-12-04) |
|
| EB2 | Publish checksums schema | `docs/modules/evidence-locker/schemas/checksums.schema.json` | Evidence Locker Guild | Canonical map for `checksums.txt`; Merkle root + chunking metadata; sorted entry rule recorded. | DONE (2025-12-04) |
|
||||||
| EB3 | Hash/Merkle recipe doc | `docs/modules/evidence-locker/bundle-packaging.md` (new section) | Evidence Locker Guild | Normative steps for Merkle root + DSSE subject; clarifies gzip/tar invariants and CAS compatibility. | TODO |
|
| EB3 | Hash/Merkle recipe doc | `docs/modules/evidence-locker/bundle-packaging.md` (new section) | Evidence Locker Guild | Normative steps for Merkle root + DSSE subject; clarifies gzip/tar invariants and CAS compatibility. | DONE (2025-12-04) |
|
||||||
| EB4 | Mandatory DSSE predicate/log policy | `docs/modules/evidence-locker/attestation-contract.md` | Evidence Locker Guild · Security Guild | Required claims + signing profiles; Rekor/log policy (optional vs required); aligns with crypto registry defaults. | TODO |
|
| EB4 | Mandatory DSSE predicate/log policy | `docs/modules/evidence-locker/attestation-contract.md` | Evidence Locker Guild · Security Guild | Required claims + signing profiles; Rekor/log policy (optional vs required); aligns with crypto registry defaults. | DONE (2025-12-04) |
|
||||||
| EB5 | Replay provenance block | `docs/modules/evidence-locker/replay-payload-contract.md` + manifest schema | Evidence Locker Guild · Replay Delivery Guild | Replay digest + DSSE envelope recorded; ordering rules match `DETERMINISTIC_REPLAY.md`; portable bundle retains linkage. | TODO |
|
| EB5 | Replay provenance block | `docs/modules/evidence-locker/replay-payload-contract.md` + manifest schema | Evidence Locker Guild · Replay Delivery Guild | Replay digest + DSSE envelope recorded; ordering rules match `DETERMINISTIC_REPLAY.md`; portable bundle retains linkage. | DONE (2025-12-04) |
|
||||||
| EB6 | Chunking/CAS rules | `checksums.schema.json` + `bundle-packaging.md` | Evidence Locker Guild · Storage/DevOps | Defines chunk sizing, CAS digest, and stability guarantees; CI test to catch ordering changes. | TODO |
|
| EB6 | Chunking/CAS rules | `checksums.schema.json` + `bundle-packaging.md` | Evidence Locker Guild · Storage/DevOps | Defines chunk sizing, CAS digest, and stability guarantees; CI test to catch ordering changes. | DONE (2025-12-04) |
|
||||||
| EB7 | Incident-mode signed activation/exit | `docs/modules/evidence-locker/incident-mode.md` | Evidence Locker Guild · Security Guild | Manifest/DSSE captures activation + deactivation events with signer identity; API/CLI steps documented. | TODO |
|
| EB7 | Incident-mode signed activation/exit | `docs/modules/evidence-locker/incident-mode.md` | Evidence Locker Guild · Security Guild | Manifest/DSSE captures activation + deactivation events with signer identity; API/CLI steps documented. | DONE (2025-12-04) |
|
||||||
| EB8 | Tenant isolation + redaction manifest | `bundle-packaging.md` + portable bundle guidance | Evidence Locker Guild · Privacy Guild | Portable bundles omit tenant identifiers; redaction map recorded; verifier asserts redacted fields absent. | TODO |
|
| EB8 | Tenant isolation + redaction manifest | `bundle-packaging.md` + portable bundle guidance | Evidence Locker Guild · Privacy Guild | Portable bundles omit tenant identifiers; redaction map recorded; verifier asserts redacted fields absent. | DONE (2025-12-04) |
|
||||||
| EB9 | Offline verifier script | `docs/modules/evidence-locker/verify-offline.md` | Evidence Locker Guild | POSIX script included; no network dependencies; emits Merkle root used by DSSE subject. | DONE (2025-12-04) |
|
| EB9 | Offline verifier script | `docs/modules/evidence-locker/verify-offline.md` | Evidence Locker Guild | POSIX script included; no network dependencies; emits Merkle root used by DSSE subject. | DONE (2025-12-04) |
|
||||||
| EB10 | Golden bundles/replay fixtures + SemVer/changelog | `tests/EvidenceLocker/Bundles/Golden/` + release notes (TBD) | Evidence Locker Guild · CLI Guild | Golden sealed + portable bundles and replay NDJSON with expected roots; changelog bump covering EB1–EB9. | TODO |
|
| EB10 | Golden bundles/replay fixtures + SemVer/changelog | `tests/EvidenceLocker/Bundles/Golden/` + release notes (TBD) | Evidence Locker Guild · CLI Guild | Golden sealed + portable bundles and replay NDJSON with expected roots; changelog bump covering EB1–EB9. | Fixtures READY (2025-12-04); SemVer/changelog PENDING |
|
||||||
|
|
||||||
## Near-Term Actions (to move EB1–EB10 to DONE)
|
## Near-Term Actions (to move EB1–EB10 to DONE)
|
||||||
- Wire schemas into EvidenceLocker CI (manifest + checksums validation) and surface in API/CLI OpenAPI/Help.
|
- Wire schemas into EvidenceLocker CI (manifest + checksums validation) and surface in API/CLI OpenAPI/Help.
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ Frozen contract for Evidence Bundle v1 covering AdvisoryAI/Concelier/Excititor e
|
|||||||
|
|
||||||
## Attestation linkage
|
## Attestation linkage
|
||||||
- See `attestation-scope-note.md` for required claims.
|
- See `attestation-scope-note.md` for required claims.
|
||||||
- Subject digest should reference the tarball sha256; include `bundle_id` and `tenant`.
|
- DSSE subject uses the Merkle root derived from `checksums.txt` (sha256 of sorted entry hashes). Record the OCI tarball digest as metadata, not the subject.
|
||||||
|
|
||||||
## Change log
|
## Change log
|
||||||
|
- 2025-12-04: Updated subject to Merkle root and aligned with EB1–EB10 docs/fixtures.
|
||||||
- 2025-11-19: v1 frozen (initial publication). Add real sample tarball + hashes once produced.
|
- 2025-11-19: v1 frozen (initial publication). Add real sample tarball + hashes once produced.
|
||||||
|
|||||||
9
docs/notifications/fixtures/redaction/sample.json
Normal file
9
docs/notifications/fixtures/redaction/sample.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"tenant_id": "tenant-123",
|
||||||
|
"delivery_id": "00000000-0000-4000-8000-000000000001",
|
||||||
|
"channel": "email",
|
||||||
|
"subject": "User signup",
|
||||||
|
"body": "User john@example.com joined",
|
||||||
|
"redacted_body": "User ***@example.com joined",
|
||||||
|
"pii_hash": "TBD"
|
||||||
|
}
|
||||||
1
docs/notifications/fixtures/rendering/index.ndjson
Normal file
1
docs/notifications/fixtures/rendering/index.ndjson
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"template_id":"tmpl-incident-start","locale":"en-US","channel":"email","expected_hash":"TBD","body_sample_path":"tmpl-incident-start.email.en-US.json"}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"subject": "Incident started: ${incident_id}",
|
||||||
|
"body": "Incident ${incident_id} started at ${started_at}. Severity: ${severity}.",
|
||||||
|
"merge_fields": ["incident_id", "started_at", "severity"],
|
||||||
|
"preview_hash": "TBD"
|
||||||
|
}
|
||||||
10
docs/notifications/fixtures/traces/sample-trace.json
Normal file
10
docs/notifications/fixtures/traces/sample-trace.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"trace_id": "00000000000000000000000000000001",
|
||||||
|
"tenant_id": "tenant-123",
|
||||||
|
"rule_id": "RULE-INCIDENT",
|
||||||
|
"channel_id": "email-default",
|
||||||
|
"attributes": {
|
||||||
|
"delivery_id": "00000000-0000-4000-8000-000000000001",
|
||||||
|
"status": "sent"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
docs/notifications/operations/alerts/notify-slo-alerts.yaml
Normal file
27
docs/notifications/operations/alerts/notify-slo-alerts.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
groups:
|
||||||
|
- name: notify-slo
|
||||||
|
rules:
|
||||||
|
- alert: NotifyDeliverySuccessSLO
|
||||||
|
expr: sum(rate(notify_delivery_success_total[5m])) / sum(rate(notify_delivery_total[5m])) < 0.98
|
||||||
|
for: 10m
|
||||||
|
labels:
|
||||||
|
severity: page
|
||||||
|
annotations:
|
||||||
|
summary: "Notify delivery success below SLO"
|
||||||
|
description: "Success ratio below 98% over 10m"
|
||||||
|
- alert: NotifyBacklogDepthHigh
|
||||||
|
expr: notify_backlog_depth > 5000
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: page
|
||||||
|
annotations:
|
||||||
|
summary: "Notify backlog too high"
|
||||||
|
description: "Backlog depth exceeded 5000 messages"
|
||||||
|
- alert: NotifyDlqGrowth
|
||||||
|
expr: rate(notify_dlq_depth[10m]) > 50
|
||||||
|
for: 10m
|
||||||
|
labels:
|
||||||
|
severity: ticket
|
||||||
|
annotations:
|
||||||
|
summary: "Notify DLQ growth"
|
||||||
|
description: "Dead letter queue growing faster than threshold"
|
||||||
9
docs/notifications/operations/dashboards/notify-slo.json
Normal file
9
docs/notifications/operations/dashboards/notify-slo.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"title": "Notify SLO",
|
||||||
|
"panels": [
|
||||||
|
{ "title": "Delivery success", "target": "sum(rate(notify_delivery_success_total[5m])) / sum(rate(notify_delivery_total[5m]))" },
|
||||||
|
{ "title": "Backlog depth", "target": "notify_backlog_depth" },
|
||||||
|
{ "title": "DLQ depth", "target": "notify_dlq_depth" },
|
||||||
|
{ "title": "Latency p95", "target": "histogram_quantile(0.95, rate(notify_delivery_latency_seconds_bucket[5m]))" }
|
||||||
|
]
|
||||||
|
}
|
||||||
7
docs/notifications/operations/quotas.md
Normal file
7
docs/notifications/operations/quotas.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Quotas, backpressure, and DLQ (NR4)
|
||||||
|
|
||||||
|
- Per-tenant quotas: 500 deliveries/minute default; channel overrides: webhook 200/min, email 120/min, chat 240/min.
|
||||||
|
- Burst budget: 2x quota for 60 seconds, then hard clamp.
|
||||||
|
- Backpressure: reject enqueue when backlog > quota*10 or DLQ growth > 5%/min.
|
||||||
|
- DLQ schema: `docs/notifications/schemas/dlq-notify.schema.json`; redrive requires idempotent `delivery_id`/`dedupe_key`.
|
||||||
|
- Metrics to alert: backlog depth, DLQ depth, redrive success rate, enqueue reject count.
|
||||||
7
docs/notifications/operations/retries.md
Normal file
7
docs/notifications/operations/retries.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Retry and idempotency policy (NR5)
|
||||||
|
|
||||||
|
- `delivery_id`: UUIDv7; `dedupe_key`: hash(event_id + rule_id + channel_id).
|
||||||
|
- Backoff: exponential with jitter; base 2s, factor 2, max 5 attempts, cap 5 minutes between attempts.
|
||||||
|
- Connectors must be idempotent; retries reuse the same `dedupe_key` and must not duplicate sends.
|
||||||
|
- Out-of-order acks ignored: only monotonic `attempt` accepted.
|
||||||
|
- Record retry outcomes in receipts and include attempt count + reason.
|
||||||
20
docs/notifications/schemas/channel.schema.json
Normal file
20
docs/notifications/schemas/channel.schema.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://stella-ops.org/notify/schemas/channel.schema.json",
|
||||||
|
"title": "Notify Channel Configuration",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["schema_version", "tenant_id", "channel_id", "kind", "config"],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "type": "string", "pattern": "^v[0-9]+\\.[0-9]+$" },
|
||||||
|
"tenant_id": { "type": "string", "minLength": 1 },
|
||||||
|
"channel_id": { "type": "string", "pattern": "^[A-Z0-9_-]{4,64}$" },
|
||||||
|
"kind": { "type": "string", "enum": ["email", "slack", "teams", "webhook", "sms"] },
|
||||||
|
"config": { "type": "object" },
|
||||||
|
"secrets_ref": { "type": "object", "additionalProperties": { "type": "string" } },
|
||||||
|
"rate_limit": { "type": "object" },
|
||||||
|
"enabled": { "type": "boolean", "default": true },
|
||||||
|
"created_at": { "type": "string", "format": "date-time" },
|
||||||
|
"updated_at": { "type": "string", "format": "date-time" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
20
docs/notifications/schemas/dlq-notify.schema.json
Normal file
20
docs/notifications/schemas/dlq-notify.schema.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://stella-ops.org/notify/schemas/dlq-notify.schema.json",
|
||||||
|
"title": "Notify Dead Letter Entry",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["schema_version", "tenant_id", "delivery_id", "reason", "payload", "first_failed_at"],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "type": "string", "pattern": "^v[0-9]+\\.[0-9]+$" },
|
||||||
|
"tenant_id": { "type": "string", "minLength": 1 },
|
||||||
|
"delivery_id": { "type": "string", "pattern": "^[0-9a-fA-F-]{18,36}$" },
|
||||||
|
"reason": { "type": "string" },
|
||||||
|
"payload": { "type": "object" },
|
||||||
|
"backoff_attempts": { "type": "integer", "minimum": 0 },
|
||||||
|
"dedupe_key": { "type": "string" },
|
||||||
|
"first_failed_at": { "type": "string", "format": "date-time" },
|
||||||
|
"last_failed_at": { "type": "string", "format": "date-time" },
|
||||||
|
"redrive_after": { "type": "string", "format": "date-time" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
26
docs/notifications/schemas/event-envelope.schema.json
Normal file
26
docs/notifications/schemas/event-envelope.schema.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://stella-ops.org/notify/schemas/event-envelope.schema.json",
|
||||||
|
"title": "Notify Event Envelope",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"tenant_id",
|
||||||
|
"event_id",
|
||||||
|
"occurred_at",
|
||||||
|
"kind",
|
||||||
|
"payload"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "type": "string", "pattern": "^v[0-9]+\\.[0-9]+$" },
|
||||||
|
"tenant_id": { "type": "string", "minLength": 1 },
|
||||||
|
"event_id": { "type": "string", "pattern": "^[0-9a-fA-F-]{18,36}$" },
|
||||||
|
"occurred_at": { "type": "string", "format": "date-time" },
|
||||||
|
"kind": { "type": "string", "minLength": 1 },
|
||||||
|
"correlation_id": { "type": "string" },
|
||||||
|
"source": { "type": "string" },
|
||||||
|
"payload": { "type": "object" },
|
||||||
|
"attributes": { "type": "object", "additionalProperties": { "type": ["string", "number", "boolean", "null"] } }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
14
docs/notifications/schemas/inputs.lock
Normal file
14
docs/notifications/schemas/inputs.lock
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"catalog": "notify-schemas-catalog.json",
|
||||||
|
"hash_algorithm": "blake3-256",
|
||||||
|
"canonicalization": "json-normalized-utf8",
|
||||||
|
"entries": [
|
||||||
|
{ "file": "event-envelope.schema.json", "digest": "TBD" },
|
||||||
|
{ "file": "rule.schema.json", "digest": "TBD" },
|
||||||
|
{ "file": "template.schema.json", "digest": "TBD" },
|
||||||
|
{ "file": "channel.schema.json", "digest": "TBD" },
|
||||||
|
{ "file": "receipt.schema.json", "digest": "TBD" },
|
||||||
|
{ "file": "webhook.schema.json", "digest": "TBD" },
|
||||||
|
{ "file": "dlq-notify.schema.json", "digest": "TBD" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"payloadType": "application/vnd.notify.schema-catalog+json",
|
||||||
|
"payload": "BASE64_ENCODED_NOTIFY_SCHEMA_CATALOG_TBD",
|
||||||
|
"signatures": [],
|
||||||
|
"note": "Placeholder; replace with signed payload once BLAKE3 digest and signing key are available."
|
||||||
|
}
|
||||||
15
docs/notifications/schemas/notify-schemas-catalog.json
Normal file
15
docs/notifications/schemas/notify-schemas-catalog.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"catalog_version": "v1.0",
|
||||||
|
"hash_algorithm": "blake3-256",
|
||||||
|
"canonicalization": "json-normalized-utf8",
|
||||||
|
"generated_at": "2025-12-04T00:00:00Z",
|
||||||
|
"schemas": [
|
||||||
|
{ "id": "event-envelope", "file": "event-envelope.schema.json", "version": "v1.0", "digest": "TBD" },
|
||||||
|
{ "id": "rule", "file": "rule.schema.json", "version": "v1.0", "digest": "TBD" },
|
||||||
|
{ "id": "template", "file": "template.schema.json", "version": "v1.0", "digest": "TBD" },
|
||||||
|
{ "id": "channel", "file": "channel.schema.json", "version": "v1.0", "digest": "TBD" },
|
||||||
|
{ "id": "receipt", "file": "receipt.schema.json", "version": "v1.0", "digest": "TBD" },
|
||||||
|
{ "id": "webhook", "file": "webhook.schema.json", "version": "v1.0", "digest": "TBD" },
|
||||||
|
{ "id": "dlq", "file": "dlq-notify.schema.json", "version": "v1.0", "digest": "TBD" }
|
||||||
|
]
|
||||||
|
}
|
||||||
21
docs/notifications/schemas/receipt.schema.json
Normal file
21
docs/notifications/schemas/receipt.schema.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://stella-ops.org/notify/schemas/receipt.schema.json",
|
||||||
|
"title": "Notify Delivery Receipt",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["schema_version", "tenant_id", "delivery_id", "rule_id", "channel", "status", "sent_at"],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "type": "string", "pattern": "^v[0-9]+\\.[0-9]+$" },
|
||||||
|
"tenant_id": { "type": "string", "minLength": 1 },
|
||||||
|
"delivery_id": { "type": "string", "pattern": "^[0-9a-fA-F-]{18,36}$" },
|
||||||
|
"rule_id": { "type": "string" },
|
||||||
|
"channel": { "type": "string" },
|
||||||
|
"status": { "type": "string", "enum": ["sent", "delivered", "failed", "queued", "acknowledged"] },
|
||||||
|
"attempt": { "type": "integer", "minimum": 1 },
|
||||||
|
"sent_at": { "type": "string", "format": "date-time" },
|
||||||
|
"ack_url": { "type": "string", "format": "uri" },
|
||||||
|
"response": { "type": "object" },
|
||||||
|
"errors": { "type": "array", "items": { "type": "string" } }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
37
docs/notifications/schemas/rule.schema.json
Normal file
37
docs/notifications/schemas/rule.schema.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://stella-ops.org/notify/schemas/rule.schema.json",
|
||||||
|
"title": "Notify Rule",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"tenant_id",
|
||||||
|
"rule_id",
|
||||||
|
"name",
|
||||||
|
"sources",
|
||||||
|
"predicates",
|
||||||
|
"actions",
|
||||||
|
"approvals_required"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "type": "string", "pattern": "^v[0-9]+\\.[0-9]+$" },
|
||||||
|
"tenant_id": { "type": "string", "minLength": 1 },
|
||||||
|
"rule_id": { "type": "string", "pattern": "^[A-Z0-9_-]{4,64}$" },
|
||||||
|
"name": { "type": "string", "minLength": 1 },
|
||||||
|
"description": { "type": "string" },
|
||||||
|
"severity": { "type": "string", "enum": ["info", "low", "medium", "high", "critical"] },
|
||||||
|
"sources": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
|
||||||
|
"predicates": { "type": "array", "items": { "type": "object" }, "minItems": 1 },
|
||||||
|
"actions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "object" },
|
||||||
|
"minItems": 1
|
||||||
|
},
|
||||||
|
"approvals_required": { "type": "integer", "minimum": 0, "maximum": 3 },
|
||||||
|
"quiet_hours": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"simulation_required": { "type": "boolean", "default": true },
|
||||||
|
"created_at": { "type": "string", "format": "date-time" },
|
||||||
|
"updated_at": { "type": "string", "format": "date-time" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
22
docs/notifications/schemas/template.schema.json
Normal file
22
docs/notifications/schemas/template.schema.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://stella-ops.org/notify/schemas/template.schema.json",
|
||||||
|
"title": "Notify Template",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["schema_version", "tenant_id", "template_id", "channel", "locale", "body"],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "type": "string", "pattern": "^v[0-9]+\\.[0-9]+$" },
|
||||||
|
"tenant_id": { "type": "string", "minLength": 1 },
|
||||||
|
"template_id": { "type": "string", "pattern": "^[A-Z0-9_-]{4,64}$" },
|
||||||
|
"channel": { "type": "string", "enum": ["email", "slack", "teams", "webhook", "sms"] },
|
||||||
|
"locale": { "type": "string", "pattern": "^[a-z]{2}(-[A-Z]{2})?$" },
|
||||||
|
"subject": { "type": "string" },
|
||||||
|
"body": { "type": "string" },
|
||||||
|
"helpers": { "type": "object", "additionalProperties": { "type": "string" } },
|
||||||
|
"merge_fields": { "type": "array", "items": { "type": "string" } },
|
||||||
|
"preview_hash": { "type": "string" },
|
||||||
|
"created_at": { "type": "string", "format": "date-time" },
|
||||||
|
"updated_at": { "type": "string", "format": "date-time" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
20
docs/notifications/schemas/webhook.schema.json
Normal file
20
docs/notifications/schemas/webhook.schema.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://stella-ops.org/notify/schemas/webhook.schema.json",
|
||||||
|
"title": "Notify Webhook Payload",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["schema_version", "tenant_id", "delivery_id", "signature", "body"],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "type": "string", "pattern": "^v[0-9]+\\.[0-9]+$" },
|
||||||
|
"tenant_id": { "type": "string", "minLength": 1 },
|
||||||
|
"delivery_id": { "type": "string", "pattern": "^[0-9a-fA-F-]{18,36}$" },
|
||||||
|
"signature": { "type": "string" },
|
||||||
|
"hmac_id": { "type": "string" },
|
||||||
|
"body": { "type": "object" },
|
||||||
|
"sent_at": { "type": "string", "format": "date-time" },
|
||||||
|
"nonce": { "type": "string" },
|
||||||
|
"audience": { "type": "string" },
|
||||||
|
"expires_at": { "type": "string", "format": "date-time" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
6
docs/notifications/security/redaction-catalog.md
Normal file
6
docs/notifications/security/redaction-catalog.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Redaction and PII catalog (NR7)
|
||||||
|
|
||||||
|
- Classify merge fields: identifiers (hash), secrets (strip), PII (mask), operational metadata (retain).
|
||||||
|
- Storage and previews must use redacted forms by default; full bodies allowed only with `Notify.Audit` permission.
|
||||||
|
- Log payloads must omit secrets; hashes use BLAKE3-256 over UTF-8 normalized values.
|
||||||
|
- Fixtures under `docs/notifications/fixtures/redaction/` show expected redacted shapes for templates and receipts.
|
||||||
6
docs/notifications/security/tenant-approvals.md
Normal file
6
docs/notifications/security/tenant-approvals.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Tenant scoping and approvals (NR2)
|
||||||
|
|
||||||
|
- All Notify APIs require `tenant_id` in request and ledger records.
|
||||||
|
- High-impact actions (escalations, PII-bearing templates, cross-tenant fan-out) need N-of-M approvals: default 2 of 3 approvers with `Notify.Approver` role.
|
||||||
|
- Approvals captured as DSSE-signed records (future hook) and stored alongside rule change requests.
|
||||||
|
- Rejection reasons must be logged and returned in error payloads; audit log keeps requester, approver IDs, timestamps, and rule/template IDs.
|
||||||
6
docs/notifications/security/webhook-ack-hardening.md
Normal file
6
docs/notifications/security/webhook-ack-hardening.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Webhook and ack security (NR6)
|
||||||
|
|
||||||
|
- Webhooks must use HMAC-SHA256 with per-tenant rotating secrets or mTLS/DPoP. `hmac_id` maps to secret material.
|
||||||
|
- Ack URLs carry signed tokens (nonce, audience, tenant_id, delivery_id, expires_at) and are single-use. Reject replay or expired tokens.
|
||||||
|
- Enforce allowlists for domains and paths per tenant; deny wildcards.
|
||||||
|
- Capture failures in observability pipeline and DLQ with redrive after investigation.
|
||||||
11
offline/notifier/artifact-hashes.json
Normal file
11
offline/notifier/artifact-hashes.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"hash_algorithm": "blake3-256",
|
||||||
|
"entries": [
|
||||||
|
{ "path": "docs/notifications/schemas/notify-schemas-catalog.json", "digest": "TBD" },
|
||||||
|
{ "path": "docs/notifications/gaps-nr1-nr10.md", "digest": "TBD" },
|
||||||
|
{ "path": "docs/notifications/fixtures/rendering/index.ndjson", "digest": "TBD" },
|
||||||
|
{ "path": "docs/notifications/fixtures/redaction/sample.json", "digest": "TBD" },
|
||||||
|
{ "path": "docs/notifications/operations/dashboards/notify-slo.json", "digest": "TBD" },
|
||||||
|
{ "path": "docs/notifications/operations/alerts/notify-slo-alerts.yaml", "digest": "TBD" }
|
||||||
|
]
|
||||||
|
}
|
||||||
6
offline/notifier/notify-kit.manifest.dsse.json
Normal file
6
offline/notifier/notify-kit.manifest.dsse.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"payloadType": "application/vnd.notify.manifest+json",
|
||||||
|
"payload": "BASE64_ENCODED_NOTIFY_KIT_MANIFEST_TBD",
|
||||||
|
"signatures": [],
|
||||||
|
"note": "Placeholder envelope; replace payload with base64 of canonical manifest and attach signatures when keys are available."
|
||||||
|
}
|
||||||
17
offline/notifier/notify-kit.manifest.json
Normal file
17
offline/notifier/notify-kit.manifest.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "v1.0",
|
||||||
|
"generated_at": "2025-12-04T00:00:00Z",
|
||||||
|
"tenant_scope": "*",
|
||||||
|
"environment": "offline",
|
||||||
|
"artifacts": [
|
||||||
|
{ "name": "schema-catalog", "path": "docs/notifications/schemas/notify-schemas-catalog.json", "digest": "TBD" },
|
||||||
|
{ "name": "schema-catalog-dsse", "path": "docs/notifications/schemas/notify-schemas-catalog.dsse.json", "digest": "TBD" },
|
||||||
|
{ "name": "rules", "path": "docs/notifications/gaps-nr1-nr10.md", "digest": "TBD" },
|
||||||
|
{ "name": "fixtures-rendering", "path": "docs/notifications/fixtures/rendering/index.ndjson", "digest": "TBD" },
|
||||||
|
{ "name": "fixtures-redaction", "path": "docs/notifications/fixtures/redaction/sample.json", "digest": "TBD" },
|
||||||
|
{ "name": "dashboards", "path": "docs/notifications/operations/dashboards/notify-slo.json", "digest": "TBD" },
|
||||||
|
{ "name": "alerts", "path": "docs/notifications/operations/alerts/notify-slo-alerts.yaml", "digest": "TBD" }
|
||||||
|
],
|
||||||
|
"hash_algorithm": "blake3-256",
|
||||||
|
"canonicalization": "json-normalized-utf8"
|
||||||
|
}
|
||||||
18
offline/notifier/verify_notify_kit.sh
Normal file
18
offline/notifier/verify_notify_kit.sh
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
|
||||||
|
missing=0
|
||||||
|
for f in notify-kit.manifest.json notify-kit.manifest.dsse.json artifact-hashes.json; do
|
||||||
|
if [ ! -f "$ROOT/$f" ]; then
|
||||||
|
echo "[FAIL] missing $f" >&2
|
||||||
|
missing=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$missing" -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[OK] Notify kit artefacts present (hash/signature verification placeholder)."
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using StellaOps.Graph.Indexer.Ingestion.Sbom;
|
using StellaOps.Graph.Indexer.Ingestion.Sbom;
|
||||||
|
using StellaOps.Graph.Indexer.Schema;
|
||||||
|
|
||||||
namespace StellaOps.Graph.Indexer.Analytics;
|
namespace StellaOps.Graph.Indexer.Analytics;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ public sealed class MongoGraphSnapshotProvider : IGraphSnapshotProvider
|
|||||||
var tenant = snapshot.GetValue("tenant", string.Empty).AsString;
|
var tenant = snapshot.GetValue("tenant", string.Empty).AsString;
|
||||||
var snapshotId = snapshot.GetValue("snapshot_id", string.Empty).AsString;
|
var snapshotId = snapshot.GetValue("snapshot_id", string.Empty).AsString;
|
||||||
var generatedAt = snapshot.TryGetValue("generated_at", out var generated)
|
var generatedAt = snapshot.TryGetValue("generated_at", out var generated)
|
||||||
&& generated.TryToUniversalTime(out var dt)
|
&& generated.BsonType == BsonType.DateTime
|
||||||
? dt
|
? DateTime.SpecifyKind(generated.ToUniversalTime(), DateTimeKind.Utc)
|
||||||
: DateTimeOffset.UtcNow;
|
: DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
var nodes = snapshot.TryGetValue("nodes", out var nodesValue) && nodesValue is BsonArray nodesArray
|
var nodes = snapshot.TryGetValue("nodes", out var nodesValue) && nodesValue is BsonArray nodesArray
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace StellaOps.Graph.Indexer.Ingestion.Inspector;
|
||||||
|
|
||||||
|
public sealed class GraphInspectorSnapshot
|
||||||
|
{
|
||||||
|
[JsonPropertyName("schemaVersion")]
|
||||||
|
public string SchemaVersion { get; init; } = "graph.inspect.v1";
|
||||||
|
|
||||||
|
[JsonPropertyName("tenant")]
|
||||||
|
public string Tenant { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("artifactDigest")]
|
||||||
|
public string ArtifactDigest { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("sbomDigest")]
|
||||||
|
public string SbomDigest { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("collectedAt")]
|
||||||
|
public DateTimeOffset CollectedAt { get; init; } = DateTimeOffset.UnixEpoch;
|
||||||
|
|
||||||
|
[JsonPropertyName("eventOffset")]
|
||||||
|
public long EventOffset { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("components")]
|
||||||
|
public IReadOnlyList<GraphInspectorComponent> Components { get; init; } = Array.Empty<GraphInspectorComponent>();
|
||||||
|
|
||||||
|
[JsonPropertyName("links")]
|
||||||
|
public GraphInspectorLinks Links { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GraphInspectorComponent
|
||||||
|
{
|
||||||
|
[JsonPropertyName("purl")]
|
||||||
|
public string Purl { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public string? Version { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("scopes")]
|
||||||
|
public IReadOnlyList<string> Scopes { get; init; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
[JsonPropertyName("relationships")]
|
||||||
|
public IReadOnlyList<GraphInspectorRelationship> Relationships { get; init; } = Array.Empty<GraphInspectorRelationship>();
|
||||||
|
|
||||||
|
[JsonPropertyName("advisories")]
|
||||||
|
public IReadOnlyList<GraphInspectorAdvisoryObservation> Advisories { get; init; } = Array.Empty<GraphInspectorAdvisoryObservation>();
|
||||||
|
|
||||||
|
[JsonPropertyName("vexStatements")]
|
||||||
|
public IReadOnlyList<GraphInspectorVexStatement> VexStatements { get; init; } = Array.Empty<GraphInspectorVexStatement>();
|
||||||
|
|
||||||
|
[JsonPropertyName("provenance")]
|
||||||
|
public GraphInspectorProvenance? Provenance { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GraphInspectorRelationship
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("targetPurl")]
|
||||||
|
public string TargetPurl { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("scope")]
|
||||||
|
public string? Scope { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("source")]
|
||||||
|
public string? Source { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("evidenceHash")]
|
||||||
|
public string EvidenceHash { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("provenance")]
|
||||||
|
public GraphInspectorProvenance? Provenance { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GraphInspectorAdvisoryObservation
|
||||||
|
{
|
||||||
|
[JsonPropertyName("advisoryId")]
|
||||||
|
public string AdvisoryId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("source")]
|
||||||
|
public string Source { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public string Status { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("severity")]
|
||||||
|
public string? Severity { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("cvss")]
|
||||||
|
public GraphInspectorCvss? Cvss { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("justification")]
|
||||||
|
public string? Justification { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("justificationSummary")]
|
||||||
|
public string? JustificationSummary { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("linksetDigest")]
|
||||||
|
public string? LinksetDigest { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("evidenceHash")]
|
||||||
|
public string EvidenceHash { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("modifiedAt")]
|
||||||
|
public DateTimeOffset ModifiedAt { get; init; } = DateTimeOffset.UnixEpoch;
|
||||||
|
|
||||||
|
[JsonPropertyName("provenance")]
|
||||||
|
public GraphInspectorProvenance? Provenance { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GraphInspectorCvss
|
||||||
|
{
|
||||||
|
[JsonPropertyName("vector")]
|
||||||
|
public string? Vector { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("score")]
|
||||||
|
public double? Score { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GraphInspectorVexStatement
|
||||||
|
{
|
||||||
|
[JsonPropertyName("statementId")]
|
||||||
|
public string StatementId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("source")]
|
||||||
|
public string Source { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public string Status { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("justification")]
|
||||||
|
public string Justification { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("impactStatement")]
|
||||||
|
public string? ImpactStatement { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("knownExploited")]
|
||||||
|
public bool? KnownExploited { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("issuedAt")]
|
||||||
|
public DateTimeOffset IssuedAt { get; init; } = DateTimeOffset.UnixEpoch;
|
||||||
|
|
||||||
|
[JsonPropertyName("expiresAt")]
|
||||||
|
public DateTimeOffset? ExpiresAt { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("evidenceHash")]
|
||||||
|
public string EvidenceHash { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("provenance")]
|
||||||
|
public GraphInspectorProvenance? Provenance { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GraphInspectorLinks
|
||||||
|
{
|
||||||
|
[JsonPropertyName("sbomObservationEventId")]
|
||||||
|
public string? SbomObservationEventId { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("linksetDigest")]
|
||||||
|
public string? LinksetDigest { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GraphInspectorProvenance
|
||||||
|
{
|
||||||
|
[JsonPropertyName("source")]
|
||||||
|
public string Source { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("collectedAt")]
|
||||||
|
public DateTimeOffset CollectedAt { get; init; } = DateTimeOffset.UnixEpoch;
|
||||||
|
|
||||||
|
[JsonPropertyName("eventOffset")]
|
||||||
|
public long EventOffset { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("linksetDigest")]
|
||||||
|
public string? LinksetDigest { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("evidenceHash")]
|
||||||
|
public string? EvidenceHash { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using StellaOps.Graph.Indexer.Ingestion.Sbom;
|
||||||
|
using StellaOps.Graph.Indexer.Schema;
|
||||||
|
|
||||||
|
namespace StellaOps.Graph.Indexer.Ingestion.Inspector;
|
||||||
|
|
||||||
|
public sealed class GraphInspectorTransformer
|
||||||
|
{
|
||||||
|
private const string ArtifactKind = "artifact";
|
||||||
|
private const string ComponentKind = "component";
|
||||||
|
private const string AdvisoryKind = "advisory";
|
||||||
|
private const string VexKind = "vex_statement";
|
||||||
|
private const string ContainsEdgeKind = "CONTAINS";
|
||||||
|
private const string DependsOnEdgeKind = "DEPENDS_ON";
|
||||||
|
private const string ProvidesEdgeKind = "PROVIDES";
|
||||||
|
private const string ObservedRuntimeEdgeKind = "OBSERVED_RUNTIME";
|
||||||
|
private const string AffectedByEdgeKind = "AFFECTED_BY";
|
||||||
|
private const string VexExemptsEdgeKind = "VEX_EXEMPTS";
|
||||||
|
private const string DefaultSourceType = "inventory";
|
||||||
|
private const string DefaultSource = "graph.inspect.v1";
|
||||||
|
|
||||||
|
public GraphBuildBatch Transform(GraphInspectorSnapshot snapshot)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(snapshot);
|
||||||
|
|
||||||
|
var nodes = new List<JsonObject>();
|
||||||
|
var edges = new List<JsonObject>();
|
||||||
|
|
||||||
|
var componentNodes = new Dictionary<string, JsonObject>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var advisoryNodes = new Dictionary<(string Tenant, string Source, string AdvisoryId, string ContentHash), JsonObject>();
|
||||||
|
var vexNodes = new Dictionary<(string Tenant, string StatementId, string Source, string EvidenceHash), JsonObject>();
|
||||||
|
|
||||||
|
var artifactNode = CreateArtifactNode(snapshot);
|
||||||
|
nodes.Add(artifactNode);
|
||||||
|
|
||||||
|
foreach (var component in snapshot.Components)
|
||||||
|
{
|
||||||
|
var componentNode = GetOrCreateComponentNode(snapshot, componentNodes, component, component.Provenance);
|
||||||
|
nodes.Add(componentNode);
|
||||||
|
|
||||||
|
foreach (var relationship in component.Relationships ?? Array.Empty<GraphInspectorRelationship>())
|
||||||
|
{
|
||||||
|
var targetNode = GetOrCreateComponentNode(
|
||||||
|
snapshot,
|
||||||
|
componentNodes,
|
||||||
|
new GraphInspectorComponent
|
||||||
|
{
|
||||||
|
Purl = relationship.TargetPurl,
|
||||||
|
Version = null,
|
||||||
|
Scopes = Array.Empty<string>(),
|
||||||
|
Relationships = Array.Empty<GraphInspectorRelationship>(),
|
||||||
|
Advisories = Array.Empty<GraphInspectorAdvisoryObservation>(),
|
||||||
|
VexStatements = Array.Empty<GraphInspectorVexStatement>(),
|
||||||
|
Provenance = relationship.Provenance
|
||||||
|
},
|
||||||
|
relationship.Provenance);
|
||||||
|
|
||||||
|
var edge = CreateRelationshipEdge(snapshot, componentNode, targetNode, relationship);
|
||||||
|
edges.Add(edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var advisory in component.Advisories ?? Array.Empty<GraphInspectorAdvisoryObservation>())
|
||||||
|
{
|
||||||
|
var advisoryNode = GetOrCreateAdvisoryNode(snapshot, advisoryNodes, advisory);
|
||||||
|
if (!nodes.Contains(advisoryNode))
|
||||||
|
{
|
||||||
|
nodes.Add(advisoryNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
var edge = CreateAffectedByEdge(snapshot, componentNode, advisoryNode, advisory);
|
||||||
|
edges.Add(edge);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var vex in component.VexStatements ?? Array.Empty<GraphInspectorVexStatement>())
|
||||||
|
{
|
||||||
|
var vexNode = GetOrCreateVexNode(snapshot, vexNodes, vex);
|
||||||
|
if (!nodes.Contains(vexNode))
|
||||||
|
{
|
||||||
|
nodes.Add(vexNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
var edge = CreateVexExemptsEdge(snapshot, componentNode, vexNode, vex);
|
||||||
|
edges.Add(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderedNodes = nodes
|
||||||
|
.Distinct(JsonNodeEqualityComparer.Instance)
|
||||||
|
.Select(n => (JsonObject)n)
|
||||||
|
.OrderBy(n => n["kind"]!.GetValue<string>(), StringComparer.Ordinal)
|
||||||
|
.ThenBy(n => n["id"]!.GetValue<string>(), StringComparer.Ordinal)
|
||||||
|
.ToImmutableArray();
|
||||||
|
|
||||||
|
var orderedEdges = edges
|
||||||
|
.Distinct(JsonNodeEqualityComparer.Instance)
|
||||||
|
.Select(e => (JsonObject)e)
|
||||||
|
.OrderBy(e => e["kind"]!.GetValue<string>(), StringComparer.Ordinal)
|
||||||
|
.ThenBy(e => e["id"]!.GetValue<string>(), StringComparer.Ordinal)
|
||||||
|
.ToImmutableArray();
|
||||||
|
|
||||||
|
return new GraphBuildBatch(orderedNodes, orderedEdges);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject CreateArtifactNode(GraphInspectorSnapshot snapshot)
|
||||||
|
{
|
||||||
|
var attributes = new JsonObject
|
||||||
|
{
|
||||||
|
["artifact_digest"] = snapshot.ArtifactDigest,
|
||||||
|
["sbom_digest"] = snapshot.SbomDigest
|
||||||
|
};
|
||||||
|
|
||||||
|
var provenance = CreateProvenance(snapshot.SchemaVersion, snapshot.CollectedAt, snapshot.SbomDigest, snapshot.EventOffset);
|
||||||
|
|
||||||
|
return GraphDocumentFactory.CreateNode(new GraphNodeSpec(
|
||||||
|
Tenant: snapshot.Tenant,
|
||||||
|
Kind: ArtifactKind,
|
||||||
|
CanonicalKey: new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["tenant"] = snapshot.Tenant,
|
||||||
|
["artifact_digest"] = snapshot.ArtifactDigest,
|
||||||
|
["sbom_digest"] = snapshot.SbomDigest
|
||||||
|
},
|
||||||
|
Attributes: attributes,
|
||||||
|
Provenance: provenance,
|
||||||
|
ValidFrom: snapshot.CollectedAt,
|
||||||
|
ValidTo: null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject GetOrCreateComponentNode(
|
||||||
|
GraphInspectorSnapshot snapshot,
|
||||||
|
IDictionary<string, JsonObject> cache,
|
||||||
|
GraphInspectorComponent component,
|
||||||
|
GraphInspectorProvenance? provenanceOverride)
|
||||||
|
{
|
||||||
|
var key = $"{component.Purl}|{DefaultSourceType}";
|
||||||
|
if (cache.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attributes = new JsonObject
|
||||||
|
{
|
||||||
|
["purl"] = component.Purl,
|
||||||
|
["version"] = component.Version ?? string.Empty,
|
||||||
|
["scope"] = string.Empty,
|
||||||
|
["license_spdx"] = string.Empty,
|
||||||
|
["usage"] = string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
if (component.Scopes?.Any() == true)
|
||||||
|
{
|
||||||
|
var scopesArray = new JsonArray();
|
||||||
|
foreach (var scope in component.Scopes.Where(s => !string.IsNullOrWhiteSpace(s)).Distinct(StringComparer.Ordinal).OrderBy(s => s, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
scopesArray.Add(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes["scopes"] = scopesArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
var provenance = ResolveProvenance(snapshot, provenanceOverride);
|
||||||
|
|
||||||
|
var node = GraphDocumentFactory.CreateNode(new GraphNodeSpec(
|
||||||
|
Tenant: snapshot.Tenant,
|
||||||
|
Kind: ComponentKind,
|
||||||
|
CanonicalKey: new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["tenant"] = snapshot.Tenant,
|
||||||
|
["purl"] = component.Purl,
|
||||||
|
["source_type"] = DefaultSourceType
|
||||||
|
},
|
||||||
|
Attributes: attributes,
|
||||||
|
Provenance: provenance,
|
||||||
|
ValidFrom: provenance.CollectedAt,
|
||||||
|
ValidTo: null));
|
||||||
|
|
||||||
|
cache[key] = node;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject CreateRelationshipEdge(
|
||||||
|
GraphInspectorSnapshot snapshot,
|
||||||
|
JsonObject sourceNode,
|
||||||
|
JsonObject targetNode,
|
||||||
|
GraphInspectorRelationship relationship)
|
||||||
|
{
|
||||||
|
var kind = relationship.Type?.Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"contains" => ContainsEdgeKind,
|
||||||
|
"depends_on" => DependsOnEdgeKind,
|
||||||
|
"provides" => ProvidesEdgeKind,
|
||||||
|
"runtime_observed" => ObservedRuntimeEdgeKind,
|
||||||
|
_ => DependsOnEdgeKind
|
||||||
|
};
|
||||||
|
|
||||||
|
var attributes = new JsonObject
|
||||||
|
{
|
||||||
|
["scope"] = relationship.Scope ?? string.Empty,
|
||||||
|
["evidence_digest"] = relationship.EvidenceHash,
|
||||||
|
["source"] = relationship.Source ?? string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
var provenance = ResolveProvenance(snapshot, relationship.Provenance);
|
||||||
|
|
||||||
|
return GraphDocumentFactory.CreateEdge(new GraphEdgeSpec(
|
||||||
|
Tenant: snapshot.Tenant,
|
||||||
|
Kind: kind,
|
||||||
|
CanonicalKey: new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["tenant"] = snapshot.Tenant,
|
||||||
|
["source_node_id"] = sourceNode["id"]!.GetValue<string>(),
|
||||||
|
["target_node_id"] = targetNode["id"]!.GetValue<string>(),
|
||||||
|
["sbom_digest"] = snapshot.SbomDigest
|
||||||
|
},
|
||||||
|
Attributes: attributes,
|
||||||
|
Provenance: provenance,
|
||||||
|
ValidFrom: provenance.CollectedAt,
|
||||||
|
ValidTo: null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject GetOrCreateAdvisoryNode(
|
||||||
|
GraphInspectorSnapshot snapshot,
|
||||||
|
IDictionary<(string, string, string, string), JsonObject> cache,
|
||||||
|
GraphInspectorAdvisoryObservation advisory)
|
||||||
|
{
|
||||||
|
var contentHash = advisory.EvidenceHash ?? string.Empty;
|
||||||
|
var key = (snapshot.Tenant, advisory.Source, advisory.AdvisoryId, contentHash);
|
||||||
|
if (cache.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attributes = new JsonObject
|
||||||
|
{
|
||||||
|
["advisory_source"] = advisory.Source,
|
||||||
|
["advisory_id"] = advisory.AdvisoryId,
|
||||||
|
["severity"] = advisory.Severity ?? string.Empty,
|
||||||
|
["published_at"] = GraphTimestamp.Format(advisory.ModifiedAt),
|
||||||
|
["content_hash"] = contentHash,
|
||||||
|
["linkset_digest"] = advisory.LinksetDigest ?? string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
var canonicalKey = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["tenant"] = snapshot.Tenant,
|
||||||
|
["advisory_source"] = advisory.Source,
|
||||||
|
["advisory_id"] = advisory.AdvisoryId,
|
||||||
|
["content_hash"] = contentHash
|
||||||
|
};
|
||||||
|
|
||||||
|
var provenance = ResolveProvenance(snapshot, advisory.Provenance);
|
||||||
|
|
||||||
|
var node = GraphDocumentFactory.CreateNode(new GraphNodeSpec(
|
||||||
|
Tenant: snapshot.Tenant,
|
||||||
|
Kind: AdvisoryKind,
|
||||||
|
CanonicalKey: canonicalKey,
|
||||||
|
Attributes: attributes,
|
||||||
|
Provenance: provenance,
|
||||||
|
ValidFrom: advisory.ModifiedAt,
|
||||||
|
ValidTo: null));
|
||||||
|
|
||||||
|
cache[key] = node;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject CreateAffectedByEdge(
|
||||||
|
GraphInspectorSnapshot snapshot,
|
||||||
|
JsonObject componentNode,
|
||||||
|
JsonObject advisoryNode,
|
||||||
|
GraphInspectorAdvisoryObservation advisory)
|
||||||
|
{
|
||||||
|
var attributes = new JsonObject
|
||||||
|
{
|
||||||
|
["evidence_digest"] = advisory.EvidenceHash,
|
||||||
|
["matched_versions"] = new JsonArray(),
|
||||||
|
["cvss"] = advisory.Cvss is null ? null : new JsonObject
|
||||||
|
{
|
||||||
|
["vector"] = advisory.Cvss.Vector,
|
||||||
|
["score"] = advisory.Cvss.Score
|
||||||
|
},
|
||||||
|
["justification"] = advisory.Justification ?? string.Empty,
|
||||||
|
["justification_summary"] = advisory.JustificationSummary ?? string.Empty,
|
||||||
|
["status"] = advisory.Status
|
||||||
|
};
|
||||||
|
|
||||||
|
var canonicalKey = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["tenant"] = snapshot.Tenant,
|
||||||
|
["component_node_id"] = componentNode["id"]!.GetValue<string>(),
|
||||||
|
["advisory_node_id"] = advisoryNode["id"]!.GetValue<string>(),
|
||||||
|
["linkset_digest"] = advisory.LinksetDigest ?? string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
var provenance = ResolveProvenance(snapshot, advisory.Provenance);
|
||||||
|
|
||||||
|
return GraphDocumentFactory.CreateEdge(new GraphEdgeSpec(
|
||||||
|
Tenant: snapshot.Tenant,
|
||||||
|
Kind: AffectedByEdgeKind,
|
||||||
|
CanonicalKey: canonicalKey,
|
||||||
|
Attributes: attributes,
|
||||||
|
Provenance: provenance,
|
||||||
|
ValidFrom: advisory.ModifiedAt,
|
||||||
|
ValidTo: null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject GetOrCreateVexNode(
|
||||||
|
GraphInspectorSnapshot snapshot,
|
||||||
|
IDictionary<(string Tenant, string StatementId, string Source, string EvidenceHash), JsonObject> cache,
|
||||||
|
GraphInspectorVexStatement vex)
|
||||||
|
{
|
||||||
|
var key = (snapshot.Tenant, vex.StatementId, vex.Source, vex.EvidenceHash);
|
||||||
|
if (cache.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attributes = new JsonObject
|
||||||
|
{
|
||||||
|
["status"] = vex.Status,
|
||||||
|
["statement_id"] = vex.StatementId,
|
||||||
|
["justification"] = vex.Justification,
|
||||||
|
["impact_statement"] = vex.ImpactStatement ?? string.Empty,
|
||||||
|
["known_exploited"] = vex.KnownExploited ?? false,
|
||||||
|
["content_hash"] = vex.EvidenceHash
|
||||||
|
};
|
||||||
|
|
||||||
|
var canonicalKey = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["tenant"] = snapshot.Tenant,
|
||||||
|
["vex_source"] = vex.Source,
|
||||||
|
["statement_id"] = vex.StatementId,
|
||||||
|
["content_hash"] = vex.EvidenceHash
|
||||||
|
};
|
||||||
|
|
||||||
|
var provenance = ResolveProvenance(snapshot, vex.Provenance);
|
||||||
|
|
||||||
|
var node = GraphDocumentFactory.CreateNode(new GraphNodeSpec(
|
||||||
|
Tenant: snapshot.Tenant,
|
||||||
|
Kind: VexKind,
|
||||||
|
CanonicalKey: canonicalKey,
|
||||||
|
Attributes: attributes,
|
||||||
|
Provenance: provenance,
|
||||||
|
ValidFrom: vex.IssuedAt,
|
||||||
|
ValidTo: vex.ExpiresAt));
|
||||||
|
|
||||||
|
cache[key] = node;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonObject CreateVexExemptsEdge(
|
||||||
|
GraphInspectorSnapshot snapshot,
|
||||||
|
JsonObject componentNode,
|
||||||
|
JsonObject vexNode,
|
||||||
|
GraphInspectorVexStatement vex)
|
||||||
|
{
|
||||||
|
var attributes = new JsonObject
|
||||||
|
{
|
||||||
|
["status"] = vex.Status,
|
||||||
|
["justification"] = vex.Justification,
|
||||||
|
["impact_statement"] = vex.ImpactStatement ?? string.Empty,
|
||||||
|
["evidence_digest"] = vex.EvidenceHash
|
||||||
|
};
|
||||||
|
|
||||||
|
var canonicalKey = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["tenant"] = snapshot.Tenant,
|
||||||
|
["component_node_id"] = componentNode["id"]!.GetValue<string>(),
|
||||||
|
["vex_node_id"] = vexNode["id"]!.GetValue<string>(),
|
||||||
|
["statement_hash"] = vex.EvidenceHash
|
||||||
|
};
|
||||||
|
|
||||||
|
var provenance = ResolveProvenance(snapshot, vex.Provenance);
|
||||||
|
|
||||||
|
return GraphDocumentFactory.CreateEdge(new GraphEdgeSpec(
|
||||||
|
Tenant: snapshot.Tenant,
|
||||||
|
Kind: VexExemptsEdgeKind,
|
||||||
|
CanonicalKey: canonicalKey,
|
||||||
|
Attributes: attributes,
|
||||||
|
Provenance: provenance,
|
||||||
|
ValidFrom: vex.IssuedAt,
|
||||||
|
ValidTo: vex.ExpiresAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphProvenanceSpec ResolveProvenance(GraphInspectorSnapshot snapshot, GraphInspectorProvenance? overrideProvenance)
|
||||||
|
{
|
||||||
|
if (overrideProvenance is not null)
|
||||||
|
{
|
||||||
|
return new GraphProvenanceSpec(
|
||||||
|
Source: string.IsNullOrWhiteSpace(overrideProvenance.Source) ? DefaultSource : overrideProvenance.Source,
|
||||||
|
CollectedAt: overrideProvenance.CollectedAt == DateTimeOffset.UnixEpoch ? snapshot.CollectedAt : overrideProvenance.CollectedAt,
|
||||||
|
SbomDigest: snapshot.SbomDigest,
|
||||||
|
EventOffset: overrideProvenance.EventOffset == 0 ? snapshot.EventOffset : overrideProvenance.EventOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GraphProvenanceSpec(
|
||||||
|
Source: DefaultSource,
|
||||||
|
CollectedAt: snapshot.CollectedAt,
|
||||||
|
SbomDigest: snapshot.SbomDigest,
|
||||||
|
EventOffset: snapshot.EventOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphProvenanceSpec CreateProvenance(string? source, DateTimeOffset collectedAt, string? sbomDigest, long? eventOffset)
|
||||||
|
{
|
||||||
|
return new GraphProvenanceSpec(
|
||||||
|
Source: string.IsNullOrWhiteSpace(source) ? DefaultSource : source!,
|
||||||
|
CollectedAt: collectedAt,
|
||||||
|
SbomDigest: sbomDigest,
|
||||||
|
EventOffset: eventOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class JsonNodeEqualityComparer : IEqualityComparer<JsonNode>
|
||||||
|
{
|
||||||
|
public static readonly JsonNodeEqualityComparer Instance = new();
|
||||||
|
|
||||||
|
public bool Equals(JsonNode? x, JsonNode? y)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(x, y))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x is null || y is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.ToJsonString() == y.ToJsonString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(JsonNode obj) => obj.ToJsonString().GetHashCode(StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using StellaOps.Graph.Indexer.Ingestion.Inspector;
|
||||||
|
using StellaOps.Graph.Indexer.Schema;
|
||||||
|
|
||||||
|
namespace StellaOps.Graph.Indexer.Tests;
|
||||||
|
|
||||||
|
public sealed class GraphInspectorTransformerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Transform_BuildsNodesAndEdges_FromInspectorSnapshot()
|
||||||
|
{
|
||||||
|
var snapshot = new GraphInspectorSnapshot
|
||||||
|
{
|
||||||
|
Tenant = "acme-dev",
|
||||||
|
ArtifactDigest = "sha256:artifact",
|
||||||
|
SbomDigest = "sha256:sbom",
|
||||||
|
CollectedAt = DateTimeOffset.Parse("2025-12-04T15:30:00Z"),
|
||||||
|
EventOffset = 5123,
|
||||||
|
Components = new[]
|
||||||
|
{
|
||||||
|
new GraphInspectorComponent
|
||||||
|
{
|
||||||
|
Purl = "pkg:maven/org.example/foo@1.2.3",
|
||||||
|
Scopes = new[] {"runtime"},
|
||||||
|
Relationships = new[]
|
||||||
|
{
|
||||||
|
new GraphInspectorRelationship
|
||||||
|
{
|
||||||
|
Type = "depends_on",
|
||||||
|
TargetPurl = "pkg:npm/lodash@4.17.21",
|
||||||
|
Scope = "runtime",
|
||||||
|
EvidenceHash = "e1",
|
||||||
|
Source = "concelier.linkset.v1",
|
||||||
|
Provenance = new GraphInspectorProvenance
|
||||||
|
{
|
||||||
|
Source = "concelier.linkset.v1",
|
||||||
|
CollectedAt = DateTimeOffset.Parse("2025-12-04T15:29:00Z"),
|
||||||
|
EventOffset = 6000,
|
||||||
|
EvidenceHash = "e1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Advisories = new[]
|
||||||
|
{
|
||||||
|
new GraphInspectorAdvisoryObservation
|
||||||
|
{
|
||||||
|
AdvisoryId = "CVE-2024-1111",
|
||||||
|
Source = "ghsa",
|
||||||
|
Status = "affected",
|
||||||
|
Severity = "HIGH",
|
||||||
|
EvidenceHash = "a1",
|
||||||
|
LinksetDigest = "abcdef",
|
||||||
|
ModifiedAt = DateTimeOffset.Parse("2025-11-30T12:00:00Z"),
|
||||||
|
Provenance = new GraphInspectorProvenance
|
||||||
|
{
|
||||||
|
Source = "concelier.linkset.v1",
|
||||||
|
CollectedAt = DateTimeOffset.Parse("2025-11-30T11:55:00Z"),
|
||||||
|
EventOffset = 4421,
|
||||||
|
EvidenceHash = "a1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
VexStatements = new[]
|
||||||
|
{
|
||||||
|
new GraphInspectorVexStatement
|
||||||
|
{
|
||||||
|
StatementId = "VEX-2025-0001",
|
||||||
|
Source = "excitor.vex.v1",
|
||||||
|
Status = "not_affected",
|
||||||
|
Justification = "component_not_present",
|
||||||
|
EvidenceHash = "v1",
|
||||||
|
IssuedAt = DateTimeOffset.Parse("2025-12-01T08:00:00Z"),
|
||||||
|
ExpiresAt = DateTimeOffset.Parse("2026-12-01T00:00:00Z"),
|
||||||
|
Provenance = new GraphInspectorProvenance
|
||||||
|
{
|
||||||
|
Source = "excitor.overlay.v1",
|
||||||
|
CollectedAt = DateTimeOffset.Parse("2025-12-01T08:00:00Z"),
|
||||||
|
EventOffset = 171,
|
||||||
|
EvidenceHash = "v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Provenance = new GraphInspectorProvenance
|
||||||
|
{
|
||||||
|
Source = "concelier.linkset.v1",
|
||||||
|
CollectedAt = DateTimeOffset.Parse("2025-12-04T15:29:00Z"),
|
||||||
|
EventOffset = 5123,
|
||||||
|
EvidenceHash = "c1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var transformer = new GraphInspectorTransformer();
|
||||||
|
|
||||||
|
var batch = transformer.Transform(snapshot);
|
||||||
|
|
||||||
|
// Nodes: artifact + source component + target component + advisory + vex
|
||||||
|
Assert.Equal(5, batch.Nodes.Length);
|
||||||
|
Assert.Contains(batch.Nodes, n => n["kind"]!.GetValue<string>() == "artifact");
|
||||||
|
Assert.Contains(batch.Nodes, n => n["kind"]!.GetValue<string>() == "component" && n["canonical_key"]!["purl"]!.GetValue<string>() == "pkg:maven/org.example/foo@1.2.3");
|
||||||
|
Assert.Contains(batch.Nodes, n => n["kind"]!.GetValue<string>() == "component" && n["canonical_key"]!["purl"]!.GetValue<string>() == "pkg:npm/lodash@4.17.21");
|
||||||
|
Assert.Contains(batch.Nodes, n => n["kind"]!.GetValue<string>() == "advisory");
|
||||||
|
Assert.Contains(batch.Nodes, n => n["kind"]!.GetValue<string>() == "vex_statement");
|
||||||
|
|
||||||
|
// Edges: depends_on, affected_by, vex_exempts
|
||||||
|
Assert.Contains(batch.Edges, e => e["kind"]!.GetValue<string>() == "DEPENDS_ON");
|
||||||
|
Assert.Contains(batch.Edges, e => e["kind"]!.GetValue<string>() == "AFFECTED_BY");
|
||||||
|
Assert.Contains(batch.Edges, e => e["kind"]!.GetValue<string>() == "VEX_EXEMPTS");
|
||||||
|
|
||||||
|
// Provenance should carry sbom digest and event offset from snapshot/provenance overrides.
|
||||||
|
var dependsOn = batch.Edges.Single(e => e["kind"]!.GetValue<string>() == "DEPENDS_ON");
|
||||||
|
Assert.Equal("sha256:sbom", dependsOn["provenance"]!["sbom_digest"]!.GetValue<string>());
|
||||||
|
Assert.Equal(6000, dependsOn["provenance"]!["event_offset"]!.GetValue<long>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,4 +13,4 @@
|
|||||||
| NOTIFY-RISK-66-001 | DONE (2025-11-24) | Notifications Service Guild · Risk Engine Guild | Added risk-events endpoint + templates/rules for severity change notifications. |
|
| NOTIFY-RISK-66-001 | DONE (2025-11-24) | Notifications Service Guild · Risk Engine Guild | Added risk-events endpoint + templates/rules for severity change notifications. |
|
||||||
| NOTIFY-RISK-67-001 | DONE (2025-11-24) | Notifications Service Guild · Policy Guild | Added routing/templates for risk profile publish/deprecate/threshold change. |
|
| NOTIFY-RISK-67-001 | DONE (2025-11-24) | Notifications Service Guild · Policy Guild | Added routing/templates for risk profile publish/deprecate/threshold change. |
|
||||||
| NOTIFY-RISK-68-001 | DONE (2025-11-24) | Notifications Service Guild | Default routing seeds with throttles/locales for risk alerts. |
|
| NOTIFY-RISK-68-001 | DONE (2025-11-24) | Notifications Service Guild | Default routing seeds with throttles/locales for risk alerts. |
|
||||||
| NOTIFY-GAPS-171-014 | TODO | Notifications Service Guild | NR1–NR10 scoped (`docs/product-advisories/31-Nov-2025 FINDINGS.md`, `docs/notifications/gaps-nr1-nr10.md`); implement schema catalog + DSSE, quotas/backpressure, retries/idempotency, security, redaction, observability, offline kit, and simulation evidence. |
|
| NOTIFY-GAPS-171-014 | DOING (2025-12-04) | Notifications Service Guild | NR1–NR10 scoped; schemas/catalog/fixtures/offline kit scaffolded; fill BLAKE3 digests, DSSE signatures, and tests next. |
|
||||||
|
|||||||
19
src/Web/StellaOps.Web/.storybook/main.ts
Normal file
19
src/Web/StellaOps.Web/.storybook/main.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/angular';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../src/stories/**/*.stories.@(ts|mdx)'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
'@storybook/addon-a11y',
|
||||||
|
'@storybook/addon-interactions',
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/angular',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: 'tag',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
53
src/Web/StellaOps.Web/.storybook/preview.ts
Normal file
53
src/Web/StellaOps.Web/.storybook/preview.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { Preview } from '@storybook/angular';
|
||||||
|
import '../src/styles.scss';
|
||||||
|
|
||||||
|
export const globalTypes = {
|
||||||
|
reduceMotion: {
|
||||||
|
name: 'Reduced Motion',
|
||||||
|
description: 'Toggle reduced-motion mode for motion tokens',
|
||||||
|
defaultValue: false,
|
||||||
|
toolbar: {
|
||||||
|
icon: 'contrast',
|
||||||
|
items: [
|
||||||
|
{ value: false, title: 'Motion enabled' },
|
||||||
|
{ value: true, title: 'Reduce motion' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
a11y: {
|
||||||
|
// Keep enabled; violations surface in the Storybook panel
|
||||||
|
disable: false,
|
||||||
|
},
|
||||||
|
backgrounds: {
|
||||||
|
default: 'light',
|
||||||
|
values: [
|
||||||
|
{ name: 'light', value: '#f6f8fb' },
|
||||||
|
{ name: 'dark', value: '#0f172a' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(story, context) => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (context.globals.reduceMotion) {
|
||||||
|
root.dataset.reduceMotion = '1';
|
||||||
|
} else {
|
||||||
|
root.dataset.reduceMotion = '0';
|
||||||
|
}
|
||||||
|
return story();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
11
src/Web/StellaOps.Web/.storybook/tsconfig.json
Normal file
11
src/Web/StellaOps.Web/.storybook/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["storybook__angular", "node"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"../src/**/*.ts",
|
||||||
|
"../.storybook/**/*.ts",
|
||||||
|
"../src/**/*.stories.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -10,3 +10,4 @@
|
|||||||
| WEB-TEN-47-CONTRACT | DONE (2025-12-01) | Gateway tenant auth/ABAC contract doc v1.0 published (`docs/api/gateway/tenant-auth.md`). |
|
| WEB-TEN-47-CONTRACT | DONE (2025-12-01) | Gateway tenant auth/ABAC contract doc v1.0 published (`docs/api/gateway/tenant-auth.md`). |
|
||||||
| WEB-VULN-29-LEDGER-DOC | DONE (2025-12-01) | Findings Ledger proxy contract doc v1.0 with idempotency + retries (`docs/api/gateway/findings-ledger-proxy.md`). |
|
| WEB-VULN-29-LEDGER-DOC | DONE (2025-12-01) | Findings Ledger proxy contract doc v1.0 with idempotency + retries (`docs/api/gateway/findings-ledger-proxy.md`). |
|
||||||
| WEB-RISK-68-NOTIFY-DOC | DONE (2025-12-01) | Notifications severity transition event schema v1.0 published (`docs/api/gateway/notifications-severity.md`). |
|
| WEB-RISK-68-NOTIFY-DOC | DONE (2025-12-01) | Notifications severity transition event schema v1.0 published (`docs/api/gateway/notifications-severity.md`). |
|
||||||
|
| UI-MICRO-GAPS-0209-011 | DOING (2025-12-04) | Motion token catalog + Storybook/Playwright a11y harness added; remaining work: component mapping, perf budgets, deterministic snapshots. |
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"serve:test": "ng serve --configuration development --port 4400 --host 127.0.0.1",
|
"serve:test": "ng serve --configuration development --port 4400 --host 127.0.0.1",
|
||||||
"verify:chromium": "node ./scripts/verify-chromium.js",
|
"verify:chromium": "node ./scripts/verify-chromium.js",
|
||||||
"ci:install": "npm ci --prefer-offline --no-audit --no-fund"
|
"ci:install": "npm ci --prefer-offline --no-audit --no-fund",
|
||||||
|
"storybook": "storybook dev -p 4600",
|
||||||
|
"storybook:build": "storybook build",
|
||||||
|
"test:a11y": "FAIL_ON_A11Y=0 playwright test tests/e2e/a11y-smoke.spec.ts"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.11.0",
|
"node": ">=20.11.0",
|
||||||
@@ -36,7 +39,16 @@
|
|||||||
"@angular-devkit/build-angular": "^17.3.17",
|
"@angular-devkit/build-angular": "^17.3.17",
|
||||||
"@angular/cli": "^17.3.17",
|
"@angular/cli": "^17.3.17",
|
||||||
"@angular/compiler-cli": "^17.3.0",
|
"@angular/compiler-cli": "^17.3.0",
|
||||||
|
"@axe-core/playwright": "4.8.4",
|
||||||
"@playwright/test": "^1.47.2",
|
"@playwright/test": "^1.47.2",
|
||||||
|
"@storybook/addon-a11y": "8.1.0",
|
||||||
|
"@storybook/addon-essentials": "8.1.0",
|
||||||
|
"@storybook/addon-interactions": "8.1.0",
|
||||||
|
"@storybook/angular": "8.1.0",
|
||||||
|
"@storybook/angular-renderer": "8.1.0",
|
||||||
|
"@storybook/test": "8.1.0",
|
||||||
|
"@storybook/testing-library": "0.2.2",
|
||||||
|
"storybook": "8.1.0",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"jasmine-core": "~5.1.0",
|
"jasmine-core": "~5.1.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
|
|||||||
39
src/Web/StellaOps.Web/src/app/styles/motion-tokens.ts
Normal file
39
src/Web/StellaOps.Web/src/app/styles/motion-tokens.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export type MotionToken =
|
||||||
|
| 'durationXs'
|
||||||
|
| 'durationSm'
|
||||||
|
| 'durationMd'
|
||||||
|
| 'durationLg'
|
||||||
|
| 'durationXl'
|
||||||
|
| 'easeStandard'
|
||||||
|
| 'easeEntrance'
|
||||||
|
| 'easeExit'
|
||||||
|
| 'easeBounce'
|
||||||
|
| 'translateSm'
|
||||||
|
| 'translateMd'
|
||||||
|
| 'translateLg'
|
||||||
|
| 'scaleSm'
|
||||||
|
| 'scaleMd';
|
||||||
|
|
||||||
|
export const motionTokens: Record<MotionToken, string> = {
|
||||||
|
durationXs: 'var(--motion-duration-xs)',
|
||||||
|
durationSm: 'var(--motion-duration-sm)',
|
||||||
|
durationMd: 'var(--motion-duration-md)',
|
||||||
|
durationLg: 'var(--motion-duration-lg)',
|
||||||
|
durationXl: 'var(--motion-duration-xl)',
|
||||||
|
easeStandard: 'var(--motion-ease-standard)',
|
||||||
|
easeEntrance: 'var(--motion-ease-entrance)',
|
||||||
|
easeExit: 'var(--motion-ease-exit)',
|
||||||
|
easeBounce: 'var(--motion-ease-bounce)',
|
||||||
|
translateSm: 'var(--motion-translate-sm)',
|
||||||
|
translateMd: 'var(--motion-translate-md)',
|
||||||
|
translateLg: 'var(--motion-translate-lg)',
|
||||||
|
scaleSm: 'var(--motion-scale-sm)',
|
||||||
|
scaleMd: 'var(--motion-scale-md)',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function reduceMotionEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
document.documentElement.dataset.reduceMotion === '1' ||
|
||||||
|
document.documentElement.dataset.reduceMotion === 'true'
|
||||||
|
);
|
||||||
|
}
|
||||||
103
src/Web/StellaOps.Web/src/stories/motion-tokens.stories.ts
Normal file
103
src/Web/StellaOps.Web/src/stories/motion-tokens.stories.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/angular';
|
||||||
|
|
||||||
|
type CardArgs = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
className: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta: Meta<CardArgs> = {
|
||||||
|
title: 'Design Tokens/Motion',
|
||||||
|
args: {
|
||||||
|
title: 'Motion tokens',
|
||||||
|
description: 'Durations, easing, and transforms with reduced-motion fallback.',
|
||||||
|
className: 'motion-fade-in',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
a11y: {
|
||||||
|
element: '#motion-token-preview',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render: ({ title, description, className }) => ({
|
||||||
|
template: `
|
||||||
|
<section id="motion-token-preview" style="display:grid; gap:12px; max-width:720px;">
|
||||||
|
<div class="card {{className}}">
|
||||||
|
<h3>{{title}}</h3>
|
||||||
|
<p>{{description}}</p>
|
||||||
|
<div class="chips">
|
||||||
|
<span class="chip">duration-md: var(--motion-duration-md)</span>
|
||||||
|
<span class="chip">ease: var(--motion-ease-standard)</span>
|
||||||
|
<span class="chip">translate: var(--motion-translate-md)</span>
|
||||||
|
<span class="chip">scale: var(--motion-scale-sm)</span>
|
||||||
|
</div>
|
||||||
|
<button class="cta motion-scale-pop">Primary action</button>
|
||||||
|
</div>
|
||||||
|
<div class="card motion-slide-up">
|
||||||
|
<h4>Reduced motion</h4>
|
||||||
|
<p>Toggle "Reduced Motion" in toolbar to verify zero-duration paths.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
h3, h4 {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
.chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.chip {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #edf2ff;
|
||||||
|
color: #1e3a8a;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.cta {
|
||||||
|
background: #0f172a;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform var(--motion-duration-sm) var(--motion-ease-standard),
|
||||||
|
box-shadow var(--motion-duration-sm) var(--motion-ease-standard);
|
||||||
|
}
|
||||||
|
.cta:hover {
|
||||||
|
transform: translateY(calc(-1 * var(--motion-translate-sm)));
|
||||||
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.16);
|
||||||
|
}
|
||||||
|
[data-reduce-motion='1'] .cta,
|
||||||
|
[data-reduce-motion='true'] .cta {
|
||||||
|
transform: none !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
export const Tokens: StoryObj<CardArgs> = {};
|
||||||
@@ -1 +1,52 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
@import './styles/tokens/motion';
|
||||||
|
|
||||||
|
/* Global motion helpers */
|
||||||
|
.motion-fade-in {
|
||||||
|
animation: fade-in var(--motion-duration-md) var(--motion-ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-slide-up {
|
||||||
|
animation: slide-up var(--motion-duration-lg) var(--motion-ease-entrance);
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-scale-pop {
|
||||||
|
animation: scale-pop var(--motion-duration-sm) var(--motion-ease-bounce);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: var(--motion-opacity-hidden);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: var(--motion-opacity-visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-up {
|
||||||
|
from {
|
||||||
|
transform: translateY(var(--motion-translate-lg));
|
||||||
|
opacity: var(--motion-opacity-muted);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: var(--motion-opacity-visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scale-pop {
|
||||||
|
from {
|
||||||
|
transform: scale(var(--motion-scale-md));
|
||||||
|
opacity: var(--motion-opacity-muted);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: var(--motion-opacity-visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reduce-motion='1'] *,
|
||||||
|
[data-reduce-motion='true'] * {
|
||||||
|
animation-duration: 0ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0ms !important;
|
||||||
|
}
|
||||||
|
|||||||
54
src/Web/StellaOps.Web/src/styles/tokens/_motion.scss
Normal file
54
src/Web/StellaOps.Web/src/styles/tokens/_motion.scss
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
:root {
|
||||||
|
/* Durations */
|
||||||
|
--motion-duration-xs: 80ms;
|
||||||
|
--motion-duration-sm: 140ms;
|
||||||
|
--motion-duration-md: 200ms;
|
||||||
|
--motion-duration-lg: 260ms;
|
||||||
|
--motion-duration-xl: 320ms;
|
||||||
|
|
||||||
|
/* Easing */
|
||||||
|
--motion-ease-standard: cubic-bezier(0.2, 0, 0, 1);
|
||||||
|
--motion-ease-entrance: cubic-bezier(0.18, 0.89, 0.32, 1);
|
||||||
|
--motion-ease-exit: cubic-bezier(0.36, 0, 0.66, -0.56);
|
||||||
|
--motion-ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
|
||||||
|
/* Distances / transforms */
|
||||||
|
--motion-translate-sm: 4px;
|
||||||
|
--motion-translate-md: 8px;
|
||||||
|
--motion-translate-lg: 16px;
|
||||||
|
--motion-scale-sm: 0.98;
|
||||||
|
--motion-scale-md: 0.96;
|
||||||
|
|
||||||
|
/* Opacity steps */
|
||||||
|
--motion-opacity-hidden: 0;
|
||||||
|
--motion-opacity-muted: 0.6;
|
||||||
|
--motion-opacity-visible: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reduce-motion='1'],
|
||||||
|
[data-reduce-motion='true'] {
|
||||||
|
--motion-duration-xs: 0ms;
|
||||||
|
--motion-duration-sm: 0ms;
|
||||||
|
--motion-duration-md: 0ms;
|
||||||
|
--motion-duration-lg: 0ms;
|
||||||
|
--motion-duration-xl: 0ms;
|
||||||
|
--motion-ease-standard: linear;
|
||||||
|
--motion-ease-entrance: linear;
|
||||||
|
--motion-ease-exit: linear;
|
||||||
|
--motion-ease-bounce: linear;
|
||||||
|
--motion-translate-sm: 0px;
|
||||||
|
--motion-translate-md: 0px;
|
||||||
|
--motion-translate-lg: 0px;
|
||||||
|
--motion-scale-sm: 1;
|
||||||
|
--motion-scale-md: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin reduce-motion-friendly {
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
[data-reduce-motion='1'] &,
|
||||||
|
[data-reduce-motion='true'] & {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/Web/StellaOps.Web/tests/e2e/a11y-smoke.spec.ts
Normal file
44
src/Web/StellaOps.Web/tests/e2e/a11y-smoke.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { test, expect, Page } from '@playwright/test';
|
||||||
|
import AxeBuilder from '@axe-core/playwright';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const shouldFail = process.env.FAIL_ON_A11Y === '1';
|
||||||
|
const reportDir = path.join(process.cwd(), 'test-results');
|
||||||
|
|
||||||
|
async function writeReport(filename: string, data: unknown) {
|
||||||
|
fs.mkdirSync(reportDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(reportDir, filename), JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runA11y(url: string, page: Page) {
|
||||||
|
await page.goto(url);
|
||||||
|
const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze();
|
||||||
|
const violations = [...results.violations].sort((a, b) => a.id.localeCompare(b.id));
|
||||||
|
await writeReport(
|
||||||
|
`a11y-${url.replace(/\\W+/g, '_') || 'home'}.json`,
|
||||||
|
{ url: page.url(), violations }
|
||||||
|
);
|
||||||
|
if (shouldFail) {
|
||||||
|
expect(violations).toEqual([]);
|
||||||
|
}
|
||||||
|
return violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('a11y-smoke', () => {
|
||||||
|
test('home page baseline', async ({ page }, testInfo) => {
|
||||||
|
const violations = await runA11y('/', page);
|
||||||
|
testInfo.annotations.push({
|
||||||
|
type: 'a11y',
|
||||||
|
description: `${violations.length} violations (set FAIL_ON_A11Y=1 to fail on any)`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('graph explorer shell', async ({ page }, testInfo) => {
|
||||||
|
const violations = await runA11y('/graph', page);
|
||||||
|
testInfo.annotations.push({
|
||||||
|
type: 'a11y',
|
||||||
|
description: `${violations.length} violations (/graph)`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
7
tests/EvidenceLocker/Bundles/Golden/portable/bundle.json
Normal file
7
tests/EvidenceLocker/Bundles/Golden/portable/bundle.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"bundleId": "11111111111111111111111111111111",
|
||||||
|
"tenant": "redacted",
|
||||||
|
"kind": "evaluation",
|
||||||
|
"createdAt": "2025-12-04T00:00:00Z",
|
||||||
|
"portable": true
|
||||||
|
}
|
||||||
14
tests/EvidenceLocker/Bundles/Golden/portable/checksums.txt
Normal file
14
tests/EvidenceLocker/Bundles/Golden/portable/checksums.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"algorithm": "sha256",
|
||||||
|
"root": "72c82a7a3d114164d491e2ecd7098bc015b115ee1ec7c42d648f0348e573cfcf",
|
||||||
|
"generatedAt": "2025-12-04T00:00:00Z",
|
||||||
|
"bundleId": "11111111111111111111111111111111",
|
||||||
|
"tenantId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"entries": [
|
||||||
|
{ "canonicalPath": "bundle.json", "sha256": "10695174db1b549d77be583e529a249713e9bd23e46cc5e73250db5dfc92c4a9", "sizeBytes": 160 },
|
||||||
|
{ "canonicalPath": "instructions-portable.txt", "sha256": "dd2a3b62857cf331b423e7dc3b869ad2dc9bfa852109a20bcbecc7bcef9bdcb7", "sizeBytes": 180 },
|
||||||
|
{ "canonicalPath": "linksets.ndjson", "sha256": "a4d84bbc3262190fd3e1f5dbc15915c97e464326a56534483ce810c905288b9d", "sizeBytes": 151 },
|
||||||
|
{ "canonicalPath": "observations.ndjson", "sha256": "c523f82e71c8a1bd9be0650883faf00ec39a792023066105d7cda544ad6ef5fd", "sizeBytes": 149 }
|
||||||
|
],
|
||||||
|
"chunking": { "strategy": "none" }
|
||||||
|
}
|
||||||
18
tests/EvidenceLocker/Bundles/Golden/portable/expected.json
Normal file
18
tests/EvidenceLocker/Bundles/Golden/portable/expected.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"bundleId": "11111111111111111111111111111111",
|
||||||
|
"tenantRedacted": true,
|
||||||
|
"merkleRoot": "72c82a7a3d114164d491e2ecd7098bc015b115ee1ec7c42d648f0348e573cfcf",
|
||||||
|
"subject": "sha256:72c82a7a3d114164d491e2ecd7098bc015b115ee1ec7c42d648f0348e573cfcf",
|
||||||
|
"entries": [
|
||||||
|
"bundle.json",
|
||||||
|
"instructions-portable.txt",
|
||||||
|
"linksets.ndjson",
|
||||||
|
"observations.ndjson"
|
||||||
|
],
|
||||||
|
"dsseKeyId": "demo-ed25519",
|
||||||
|
"logPolicy": "skip-offline",
|
||||||
|
"redaction": {
|
||||||
|
"maskedFields": ["tenantId"],
|
||||||
|
"tenantToken": "portable-tenant-01"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Portable bundle verification:
|
||||||
|
1) sha256sum -c checksums.txt
|
||||||
|
2) expect no tenant identifiers in manifest or bundle.json
|
||||||
|
3) merkle_root=$(sha256sum checksums.txt | awk '{print $1}')
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"linksetId":"lnk-demo-001","advisoryId":"CVE-2025-0001","components":["pkg:deb/openssl@1.1.1w"],"normalized":true,"createdAt":"2025-11-30T00:05:00Z"}
|
||||||
58
tests/EvidenceLocker/Bundles/Golden/portable/manifest.json
Normal file
58
tests/EvidenceLocker/Bundles/Golden/portable/manifest.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"bundleId": "11111111111111111111111111111111",
|
||||||
|
"tenantId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"kind": "evaluation",
|
||||||
|
"createdAt": "2025-12-04T00:00:00Z",
|
||||||
|
"metadata": {
|
||||||
|
"scope": "demo",
|
||||||
|
"portable": "true"
|
||||||
|
},
|
||||||
|
"redaction": {
|
||||||
|
"portable": true,
|
||||||
|
"maskedFields": ["tenantId"],
|
||||||
|
"tenantToken": "portable-tenant-01"
|
||||||
|
},
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"section": "manifest",
|
||||||
|
"canonicalPath": "bundle.json",
|
||||||
|
"sha256": "10695174db1b549d77be583e529a249713e9bd23e46cc5e73250db5dfc92c4a9",
|
||||||
|
"sizeBytes": 160,
|
||||||
|
"mediaType": "application/json",
|
||||||
|
"attributes": {
|
||||||
|
"role": "bundle",
|
||||||
|
"portable": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "evidence",
|
||||||
|
"canonicalPath": "observations.ndjson",
|
||||||
|
"sha256": "c523f82e71c8a1bd9be0650883faf00ec39a792023066105d7cda544ad6ef5fd",
|
||||||
|
"sizeBytes": 149,
|
||||||
|
"mediaType": "application/x-ndjson",
|
||||||
|
"attributes": {
|
||||||
|
"dataset": "observations"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "evidence",
|
||||||
|
"canonicalPath": "linksets.ndjson",
|
||||||
|
"sha256": "a4d84bbc3262190fd3e1f5dbc15915c97e464326a56534483ce810c905288b9d",
|
||||||
|
"sizeBytes": 151,
|
||||||
|
"mediaType": "application/x-ndjson",
|
||||||
|
"attributes": {
|
||||||
|
"dataset": "linksets"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "docs",
|
||||||
|
"canonicalPath": "instructions-portable.txt",
|
||||||
|
"sha256": "dd2a3b62857cf331b423e7dc3b869ad2dc9bfa852109a20bcbecc7bcef9bdcb7",
|
||||||
|
"sizeBytes": 180,
|
||||||
|
"mediaType": "text/plain",
|
||||||
|
"attributes": {
|
||||||
|
"purpose": "verification"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"observationId":"obs-demo-001","advisoryId":"CVE-2025-0001","component":"pkg:deb/openssl@1.1.1w","source":"nvd","fetchedAt":"2025-11-30T00:00:00Z"}
|
||||||
15
tests/EvidenceLocker/Bundles/Golden/portable/signature.json
Normal file
15
tests/EvidenceLocker/Bundles/Golden/portable/signature.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"payloadType": "application/vnd.stellaops.evidence+json",
|
||||||
|
"payload": "ewogICJidW5kbGVJZCI6ICIxMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMSIsCiAgInRlbmFudElkIjogImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwKICAia2luZCI6ICJldmFsdWF0aW9uIiwKICAiY3JlYXRlZEF0IjogIjIwMjUtMTItMDRUMDA6MDA6MDBaIiwKICAibWV0YWRhdGEiOiB7CiAgICAic2NvcGUiOiAiZGVtbyIsCiAgICAicG9ydGFibGUiOiAidHJ1ZSIKICB9LAogICJyZWRhY3Rpb24iOiB7CiAgICAicG9ydGFibGUiOiB0cnVlLAogICAgIm1hc2tlZEZpZWxkcyI6IFsidGVuYW50SWQiXSwKICAgICJ0ZW5hbnRUb2tlbiI6ICJwb3J0YWJsZS10ZW5hbnQtMDEiCiAgfSwKICAiZW50cmllcyI6IFsKICAgIHsKICAgICAgInNlY3Rpb24iOiAibWFuaWZlc3QiLAogICAgICAiY2Fub25pY2FsUGF0aCI6ICJidW5kbGUuanNvbiIsCiAgICAgICJzaGEyNTYiOiAiMTA2OTUxNzRkYjFiNTQ5ZDc3YmU1ODNlNTI5YTI0OTcxM2U5YmQyM2U0NmNjNWU3MzI1MGRiNWRmYzkyYzRhOSIsCiAgICAgICJzaXplQnl0ZXMiOiAxNjAsCiAgICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24vanNvbiIsCiAgICAgICJhdHRyaWJ1dGVzIjogewogICAgICAgICJyb2xlIjogImJ1bmRsZSIsCiAgICAgICAgInBvcnRhYmxlIjogInRydWUiCiAgICAgIH0KICAgIH0sCiAgICB7CiAgICAgICJzZWN0aW9uIjogImV2aWRlbmNlIiwKICAgICAgImNhbm9uaWNhbFBhdGgiOiAib2JzZXJ2YXRpb25zLm5kanNvbiIsCiAgICAgICJzaGEyNTYiOiAiYzUyM2Y4MmU3MWM4YTFiZDliZTA2NTA4ODNmYWYwMGVjMzlhNzkyMDIzMDY2MTA1ZDdjZGE1NDRhZDZlZjVmZCIsCiAgICAgICJzaXplQnl0ZXMiOiAxNDksCiAgICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24veC1uZGpzb24iLAogICAgICAiYXR0cmlidXRlcyI6IHsKICAgICAgICAiZGF0YXNldCI6ICJvYnNlcnZhdGlvbnMiCiAgICAgIH0KICAgIH0sCiAgICB7CiAgICAgICJzZWN0aW9uIjogImV2aWRlbmNlIiwKICAgICAgImNhbm9uaWNhbFBhdGgiOiAibGlua3NldHMubmRqc29uIiwKICAgICAgInNoYTI1NiI6ICJhNGQ4NGJiYzMyNjIxOTBmZDNlMWY1ZGJjMTU5MTVjOTdlNDY0MzI2YTU2NTM0NDgzY2U4MTBjOTA1Mjg4YjlkIiwKICAgICAgInNpemVCeXRlcyI6IDE1MSwKICAgICAgIm1lZGlhVHlwZSI6ICJhcHBsaWNhdGlvbi94LW5kanNvbiIsCiAgICAgICJhdHRyaWJ1dGVzIjogewogICAgICAgICJkYXRhc2V0IjogImxpbmtzZXRzIgogICAgICB9CiAgICB9LAogICAgewogICAgICAic2VjdGlvbiI6ICJkb2NzIiwKICAgICAgImNhbm9uaWNhbFBhdGgiOiAiaW5zdHJ1Y3Rpb25zLXBvcnRhYmxlLnR4dCIsCiAgICAgICJzaGEyNTYiOiAiZGQyYTNiNjI4NTdjZjMzMWI0MjNlN2RjM2I4NjlhZDJkYzliZmE4NTIxMDlhMjBiY2JlY2M3YmNlZjliZGNiNyIsCiAgICAgICJzaXplQnl0ZXMiOiAxODAsCiAgICAgICJtZWRpYVR5cGUiOiAidGV4dC9wbGFpbiIsCiAgICAgICJhdHRyaWJ1dGVzIjogewogICAgICAgICJwdXJwb3NlIjogInZlcmlmaWNhdGlvbiIKICAgICAgfQogICAgfQogIF0KfQo=",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"keyid": "demo-ed25519",
|
||||||
|
"sig": "MEQCIGZkZGVtb3NpZw==",
|
||||||
|
"algorithm": "ed25519",
|
||||||
|
"provider": "sovereign-default",
|
||||||
|
"subjectMerkleRoot": "72c82a7a3d114164d491e2ecd7098bc015b115ee1ec7c42d648f0348e573cfcf",
|
||||||
|
"transparency": null,
|
||||||
|
"log_policy": "skip-offline"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
7
tests/EvidenceLocker/Bundles/Golden/replay/expected.json
Normal file
7
tests/EvidenceLocker/Bundles/Golden/replay/expected.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recordDigest": "sha256:8765b4a8411e76b36a2d2d43eba4c2197b4dcf0c5c0a11685ce46780a7c54222",
|
||||||
|
"sequence": 0,
|
||||||
|
"ledgerUri": "offline://demo-ledger",
|
||||||
|
"dsseEnvelope": "ZHNzZV9lbmNfZGVtbyIs",
|
||||||
|
"ordering": "recordedAtUtc, scanId"
|
||||||
|
}
|
||||||
1
tests/EvidenceLocker/Bundles/Golden/replay/replay.ndjson
Normal file
1
tests/EvidenceLocker/Bundles/Golden/replay/replay.ndjson
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"scanId":"22222222-2222-4222-8222-222222222222","tenantId":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","subjectDigest":"sha256:c15ab4d1348da9e5000a5d3da50790ea120d865cafb0961845ed6f1e96927596","scanKind":"sbom","startedAtUtc":"2025-12-03T00:00:00Z","completedAtUtc":"2025-12-03T00:10:00Z","recordedAtUtc":"2025-12-03T00:10:01Z","artifacts":[{"type":"sbom","digest":"sha256:aaaa","uri":"s3://demo/sbom"}],"provenance":{"dsseEnvelope":"ZHNzZV9lbmNfZGVtbyIs"},"summary":{"findings":1,"advisories":1,"policies":0}}
|
||||||
1
tests/EvidenceLocker/Bundles/Golden/replay/replay.sha256
Normal file
1
tests/EvidenceLocker/Bundles/Golden/replay/replay.sha256
Normal file
@@ -0,0 +1 @@
|
|||||||
|
8765b4a8411e76b36a2d2d43eba4c2197b4dcf0c5c0a11685ce46780a7c54222 replay.ndjson
|
||||||
7
tests/EvidenceLocker/Bundles/Golden/sealed/bundle.json
Normal file
7
tests/EvidenceLocker/Bundles/Golden/sealed/bundle.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"bundleId": "11111111111111111111111111111111",
|
||||||
|
"tenantId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"kind": "evaluation",
|
||||||
|
"createdAt": "2025-12-04T00:00:00Z",
|
||||||
|
"portable": false
|
||||||
|
}
|
||||||
14
tests/EvidenceLocker/Bundles/Golden/sealed/checksums.txt
Normal file
14
tests/EvidenceLocker/Bundles/Golden/sealed/checksums.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"algorithm": "sha256",
|
||||||
|
"root": "c15ab4d1348da9e5000a5d3da50790ea120d865cafb0961845ed6f1e96927596",
|
||||||
|
"generatedAt": "2025-12-04T00:00:00Z",
|
||||||
|
"bundleId": "11111111111111111111111111111111",
|
||||||
|
"tenantId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"entries": [
|
||||||
|
{ "canonicalPath": "bundle.json", "sha256": "86872809b585f9b43f53b12a8fb27dbb0a3b9c4f74e41c38118877ebcff1c273", "sizeBytes": 187 },
|
||||||
|
{ "canonicalPath": "instructions.txt", "sha256": "39a5880af850121919a540dd4528e49a3b5687cb922195b07db2c56f9e90dd1b", "sizeBytes": 160 },
|
||||||
|
{ "canonicalPath": "linksets.ndjson", "sha256": "a4d84bbc3262190fd3e1f5dbc15915c97e464326a56534483ce810c905288b9d", "sizeBytes": 151 },
|
||||||
|
{ "canonicalPath": "observations.ndjson", "sha256": "c523f82e71c8a1bd9be0650883faf00ec39a792023066105d7cda544ad6ef5fd", "sizeBytes": 149 }
|
||||||
|
],
|
||||||
|
"chunking": { "strategy": "none" }
|
||||||
|
}
|
||||||
14
tests/EvidenceLocker/Bundles/Golden/sealed/expected.json
Normal file
14
tests/EvidenceLocker/Bundles/Golden/sealed/expected.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"bundleId": "11111111111111111111111111111111",
|
||||||
|
"tenantId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"merkleRoot": "c15ab4d1348da9e5000a5d3da50790ea120d865cafb0961845ed6f1e96927596",
|
||||||
|
"subject": "sha256:c15ab4d1348da9e5000a5d3da50790ea120d865cafb0961845ed6f1e96927596",
|
||||||
|
"entries": [
|
||||||
|
"bundle.json",
|
||||||
|
"instructions.txt",
|
||||||
|
"linksets.ndjson",
|
||||||
|
"observations.ndjson"
|
||||||
|
],
|
||||||
|
"dsseKeyId": "demo-ed25519",
|
||||||
|
"logPolicy": "skip-offline"
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Offline verification steps:
|
||||||
|
1) sha256sum -c checksums.txt
|
||||||
|
2) merkle_root=$(sha256sum checksums.txt | awk '{print $1}')
|
||||||
|
3) compare merkle_root with DSSE subject
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"linksetId":"lnk-demo-001","advisoryId":"CVE-2025-0001","components":["pkg:deb/openssl@1.1.1w"],"normalized":true,"createdAt":"2025-11-30T00:05:00Z"}
|
||||||
52
tests/EvidenceLocker/Bundles/Golden/sealed/manifest.json
Normal file
52
tests/EvidenceLocker/Bundles/Golden/sealed/manifest.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"bundleId": "11111111111111111111111111111111",
|
||||||
|
"tenantId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"kind": "evaluation",
|
||||||
|
"createdAt": "2025-12-04T00:00:00Z",
|
||||||
|
"metadata": {
|
||||||
|
"scope": "demo",
|
||||||
|
"advisory": "CVE-2025-0001"
|
||||||
|
},
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"section": "manifest",
|
||||||
|
"canonicalPath": "bundle.json",
|
||||||
|
"sha256": "86872809b585f9b43f53b12a8fb27dbb0a3b9c4f74e41c38118877ebcff1c273",
|
||||||
|
"sizeBytes": 187,
|
||||||
|
"mediaType": "application/json",
|
||||||
|
"attributes": {
|
||||||
|
"role": "bundle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "evidence",
|
||||||
|
"canonicalPath": "observations.ndjson",
|
||||||
|
"sha256": "c523f82e71c8a1bd9be0650883faf00ec39a792023066105d7cda544ad6ef5fd",
|
||||||
|
"sizeBytes": 149,
|
||||||
|
"mediaType": "application/x-ndjson",
|
||||||
|
"attributes": {
|
||||||
|
"dataset": "observations"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "evidence",
|
||||||
|
"canonicalPath": "linksets.ndjson",
|
||||||
|
"sha256": "a4d84bbc3262190fd3e1f5dbc15915c97e464326a56534483ce810c905288b9d",
|
||||||
|
"sizeBytes": 151,
|
||||||
|
"mediaType": "application/x-ndjson",
|
||||||
|
"attributes": {
|
||||||
|
"dataset": "linksets"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "docs",
|
||||||
|
"canonicalPath": "instructions.txt",
|
||||||
|
"sha256": "39a5880af850121919a540dd4528e49a3b5687cb922195b07db2c56f9e90dd1b",
|
||||||
|
"sizeBytes": 160,
|
||||||
|
"mediaType": "text/plain",
|
||||||
|
"attributes": {
|
||||||
|
"purpose": "verification"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"observationId":"obs-demo-001","advisoryId":"CVE-2025-0001","component":"pkg:deb/openssl@1.1.1w","source":"nvd","fetchedAt":"2025-11-30T00:00:00Z"}
|
||||||
15
tests/EvidenceLocker/Bundles/Golden/sealed/signature.json
Normal file
15
tests/EvidenceLocker/Bundles/Golden/sealed/signature.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"payloadType": "application/vnd.stellaops.evidence+json",
|
||||||
|
"payload": "ewogICJidW5kbGVJZCI6ICIxMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMSIsCiAgInRlbmFudElkIjogImFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhIiwKICAia2luZCI6ICJldmFsdWF0aW9uIiwKICAiY3JlYXRlZEF0IjogIjIwMjUtMTItMDRUMDA6MDA6MDBaIiwKICAibWV0YWRhdGEiOiB7CiAgICAic2NvcGUiOiAiZGVtbyIsCiAgICAiYWR2aXNvcnkiOiAiQ1ZFLTIwMjUtMDAwMSIKICB9LAogICJlbnRyaWVzIjogWwogICAgewogICAgICAic2VjdGlvbiI6ICJtYW5pZmVzdCIsCiAgICAgICJjYW5vbmljYWxQYXRoIjogImJ1bmRsZS5qc29uIiwKICAgICAgInNoYTI1NiI6ICI4Njg3MjgwOWI1ODVmOWI0M2Y1M2IxMmE4ZmIyN2RiYjBhM2I5YzRmNzRlNDFjMzgxMTg4NzdlYmNmZjFjMjczIiwKICAgICAgInNpemVCeXRlcyI6IDE4NywKICAgICAgIm1lZGlhVHlwZSI6ICJhcHBsaWNhdGlvbi9qc29uIiwKICAgICAgImF0dHJpYnV0ZXMiOiB7CiAgICAgICAgInJvbGUiOiAiYnVuZGxlIgogICAgICB9CiAgICB9LAogICAgewogICAgICAic2VjdGlvbiI6ICJldmlkZW5jZSIsCiAgICAgICJjYW5vbmljYWxQYXRoIjogIm9ic2VydmF0aW9ucy5uZGpzb24iLAogICAgICAic2hhMjU2IjogImM1MjNmODJlNzFjOGExYmQ5YmUwNjUwODgzZmFmMDBlYzM5YTc5MjAyMzA2NjEwNWQ3Y2RhNTQ0YWQ2ZWY1ZmQiLAogICAgICAic2l6ZUJ5dGVzIjogMTQ5LAogICAgICAibWVkaWFUeXBlIjogImFwcGxpY2F0aW9uL3gtbmRqc29uIiwKICAgICAgImF0dHJpYnV0ZXMiOiB7CiAgICAgICAgImRhdGFzZXQiOiAib2JzZXJ2YXRpb25zIgogICAgICB9CiAgICB9LAogICAgewogICAgICAic2VjdGlvbiI6ICJldmlkZW5jZSIsCiAgICAgICJjYW5vbmljYWxQYXRoIjogImxpbmtzZXRzLm5kanNvbiIsCiAgICAgICJzaGEyNTYiOiAiYTRkODRiYmMzMjYyMTkwZmQzZTFmNWRiYzE1OTE1Yzk3ZTQ2NDMyNmE1NjUzNDQ4M2NlODEwYzkwNTI4OGI5ZCIsCiAgICAgICJzaXplQnl0ZXMiOiAxNTEsCiAgICAgICJtZWRpYVR5cGUiOiAiYXBwbGljYXRpb24veC1uZGpzb24iLAogICAgICAiYXR0cmlidXRlcyI6IHsKICAgICAgICAiZGF0YXNldCI6ICJsaW5rc2V0cyIKICAgICAgfQogICAgfSwKICAgIHsKICAgICAgInNlY3Rpb24iOiAiZG9jcyIsCiAgICAgICJjYW5vbmljYWxQYXRoIjogImluc3RydWN0aW9ucy50eHQiLAogICAgICAic2hhMjU2IjogIjM5YTU4ODBhZjg1MDEyMTkxOWE1NDBkZDQ1MjhlNDlhM2I1Njg3Y2I5MjIxOTViMDdkYjJjNTZmOWU5MGRkMWIiLAogICAgICAic2l6ZUJ5dGVzIjogMTYwLAogICAgICAibWVkaWFUeXBlIjogInRleHQvcGxhaW4iLAogICAgICAiYXR0cmlidXRlcyI6IHsKICAgICAgICAicHVycG9zZSI6ICJ2ZXJpZmljYXRpb24iCiAgICAgIH0KICAgIH0KICBdCn0K",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"keyid": "demo-ed25519",
|
||||||
|
"sig": "MEQCIGZkZGVtb3NpZw==",
|
||||||
|
"algorithm": "ed25519",
|
||||||
|
"provider": "sovereign-default",
|
||||||
|
"subjectMerkleRoot": "c15ab4d1348da9e5000a5d3da50790ea120d865cafb0961845ed6f1e96927596",
|
||||||
|
"transparency": null,
|
||||||
|
"log_policy": "skip-offline"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
17
tests/offline/NotifyKitDeterminismTests.sh
Normal file
17
tests/offline/NotifyKitDeterminismTests.sh
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||||
|
KIT="$ROOT/offline/notifier"
|
||||||
|
|
||||||
|
if [ ! -f "$KIT/notify-kit.manifest.json" ]; then
|
||||||
|
echo "notify-kit.manifest.json missing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$KIT/artifact-hashes.json" ]; then
|
||||||
|
echo "artifact-hashes.json missing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Notify kit files present; hash verification TODO pending BLAKE3 signer availability."
|
||||||
Reference in New Issue
Block a user