up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
This commit is contained in:
@@ -18,6 +18,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fallback to dev signing key when secret is absent (non-prod only)
|
||||||
|
run: |
|
||||||
|
if [ -z "${MIRROR_SIGN_KEY_B64}" ]; then
|
||||||
|
echo "[warn] MIRROR_SIGN_KEY_B64 not set; using repo dev key for non-production signing."
|
||||||
|
echo "MIRROR_SIGN_KEY_B64=$(base64 -w0 tools/cosign/cosign.dev.key)" >> $GITHUB_ENV
|
||||||
|
echo "REQUIRE_PROD_SIGNING=0" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -65,4 +65,5 @@ local-nugets/
|
|||||||
local-nuget/
|
local-nuget/
|
||||||
src/Sdk/StellaOps.Sdk.Generator/tools/jdk-21.0.1+12
|
src/Sdk/StellaOps.Sdk.Generator/tools/jdk-21.0.1+12
|
||||||
.nuget-cache/
|
.nuget-cache/
|
||||||
|
.nuget-packages2/
|
||||||
.nuget-temp/
|
.nuget-temp/
|
||||||
File diff suppressed because it is too large
Load Diff
0
docs/db/reports/concelier-postgres-json-design.md
Normal file
0
docs/db/reports/concelier-postgres-json-design.md
Normal file
@@ -7,13 +7,13 @@
|
|||||||
|
|
||||||
## Dependencies & Concurrency
|
## Dependencies & Concurrency
|
||||||
- Depends on Sprint 0100.A (Attestor) staying green.
|
- Depends on Sprint 0100.A (Attestor) staying green.
|
||||||
- Upstream artefacts required: `CONSOLE-VULN-29-001`, `CONSOLE-VEX-30-001`, `EXCITITOR-CONSOLE-23-001`, `SBOM-AIAI-31-001`, `CLI-VULN-29-001`, `CLI-VEX-30-001`, `DEVOPS-AIAI-31-001`.
|
- Upstream artefacts required: `CONSOLE-VULN-29-001`, `CONSOLE-VEX-30-001`, `EXCITITOR-CONSOLE-23-001`, `SBOM-AIAI-31-001`, `DEVOPS-AIAI-31-001`. `CLI-VULN-29-001` and `CLI-VEX-30-001` landed in Sprint 0205 on 2025-12-06.
|
||||||
- Concurrency: block publishing on missing CLI/Policy/SBOM deliverables; drafting allowed where noted.
|
- Concurrency: block publishing on missing Console/SBOM/DevOps deliverables; drafting allowed where noted.
|
||||||
|
|
||||||
## Wave Coordination
|
## Wave Coordination
|
||||||
- **Wave A (drafting):** Task 3 DONE (AIAI-RAG-31-003); drafting for tasks 1/5 allowed but must stay unpublished.
|
- **Wave A (drafting):** Task 3 DONE (AIAI-RAG-31-003); drafting for tasks 1/5 allowed but must stay unpublished.
|
||||||
- **Wave B (publish docs):** Tasks 1 and 5 BLOCKED until CLI/Policy/SBOM artefacts arrive; publish only after all upstreams land.
|
- **Wave B (publish docs):** Task 5 delivered once CLI/Policy landed (2025-11-25); task 1 still blocked pending Console/SBOM/DevOps inputs before publish.
|
||||||
- **Wave C (packaging):** Task 2 moved to Ops sprint; no work here. Wave B completes sprint once unblocked.
|
- **Wave C (packaging):** Task 2 moved to Ops sprint; no work here. Wave B completes sprint once upstreams finish.
|
||||||
|
|
||||||
## Documentation Prerequisites
|
## Documentation Prerequisites
|
||||||
- docs/README.md
|
- docs/README.md
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
| 1 | AIAI-DOCS-31-001 | BLOCKED (2025-11-22) | Await CLI/Policy artefacts | Advisory AI Docs Guild | Author guardrail + evidence docs with upstream references |
|
| 1 | AIAI-DOCS-31-001 | BLOCKED (2025-11-22) | Await CLI/Policy artefacts | Advisory AI Docs Guild | Author guardrail + evidence docs with upstream references |
|
||||||
| 2 | AIAI-PACKAGING-31-002 | MOVED to SPRINT_0503_0001_0001_ops_devops_i (2025-11-23) | Track under DEVOPS-AIAI-31-002 in Ops sprint | Advisory AI Release | Package advisory feeds with SBOM pointers + provenance |
|
| 2 | AIAI-PACKAGING-31-002 | MOVED to SPRINT_0503_0001_0001_ops_devops_i (2025-11-23) | Track under DEVOPS-AIAI-31-002 in Ops sprint | Advisory AI Release | Package advisory feeds with SBOM pointers + provenance |
|
||||||
| 3 | AIAI-RAG-31-003 | DONE | None | Advisory AI + Concelier | Align RAG evidence payloads with LNM schema |
|
| 3 | AIAI-RAG-31-003 | DONE | None | Advisory AI + Concelier | Align RAG evidence payloads with LNM schema |
|
||||||
| 4 | SBOM-AIAI-31-003 | BLOCKED (2025-11-23) | CLI-VULN-29-001; CLI-VEX-30-001 | SBOM Service Guild · Advisory AI Guild | Advisory AI hand-off kit for `/v1/sbom/context`; smoke test with tenants |
|
| 4 | SBOM-AIAI-31-003 | DONE (2025-11-25) | Published at `docs/advisory-ai/sbom-context-hand-off.md` | SBOM Service Guild · Advisory AI Guild | Advisory AI hand-off kit for `/v1/sbom/context`; smoke test with tenants |
|
||||||
| 5 | DOCS-AIAI-31-005/006/008/009 | BLOCKED (2025-11-23) | CLI-VULN-29-001; CLI-VEX-30-001; POLICY-ENGINE-31-001; DEVOPS-AIAI-31-001 | Docs Guild | CLI/policy/ops docs; proceed once upstream artefacts land |
|
| 5 | DOCS-AIAI-31-005/006/008/009 | DONE (2025-11-25) | CLI/Policy inputs landed; DEVOPS-AIAI-31-001 rollout still tracked separately | Docs Guild | CLI/policy/ops docs; proceed once upstream artefacts land |
|
||||||
|
|
||||||
## Action Tracker
|
## Action Tracker
|
||||||
| Focus | Action | Owner(s) | Due | Status |
|
| Focus | Action | Owner(s) | Due | Status |
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
## Execution Log
|
## Execution Log
|
||||||
| Date (UTC) | Update | Owner |
|
| Date (UTC) | Update | Owner |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| 2025-12-07 | Recorded CLI-VULN-29-001/CLI-VEX-30-001 delivery (Sprint 0205, 2025-12-06); marked SBOM-AIAI-31-003 and DOCS-AIAI-31-005/006/008/009 as DONE per 2025-11-25 drops. | Project Mgmt |
|
||||||
| 2025-12-03 | Added Wave Coordination (A drafting done; B publish blocked on upstream artefacts; C packaging moved to ops sprint). No status changes. | Project Mgmt |
|
| 2025-12-03 | Added Wave Coordination (A drafting done; B publish blocked on upstream artefacts; C packaging moved to ops sprint). No status changes. | Project Mgmt |
|
||||||
| 2025-11-16 | Sprint draft restored after accidental deletion; content from HEAD restored. | Planning |
|
| 2025-11-16 | Sprint draft restored after accidental deletion; content from HEAD restored. | Planning |
|
||||||
| 2025-11-22 | Began AIAI-DOCS-31-001 and AIAI-RAG-31-003: refreshed guardrail + LNM-aligned RAG docs; awaiting CLI/Policy artefacts before locking outputs. | Docs Guild |
|
| 2025-11-22 | Began AIAI-DOCS-31-001 and AIAI-RAG-31-003: refreshed guardrail + LNM-aligned RAG docs; awaiting CLI/Policy artefacts before locking outputs. | Docs Guild |
|
||||||
@@ -50,7 +51,8 @@
|
|||||||
| 2025-12-02 | Normalized sprint file to standard template; no status changes. | StellaOps Agent |
|
| 2025-12-02 | Normalized sprint file to standard template; no status changes. | StellaOps Agent |
|
||||||
|
|
||||||
## Decisions & Risks
|
## Decisions & Risks
|
||||||
- Publishing of docs/packages is gated on upstream CLI/Policy/SBOM artefacts; drafting allowed but must remain unpublished until dependencies land.
|
- Publishing of docs/packages is gated on remaining Console/SBOM/DevOps artefacts; drafting allowed but must remain unpublished until dependencies land.
|
||||||
|
- CLI-VULN-29-001 and CLI-VEX-30-001 landed (Sprint 0205, 2025-12-06); Policy knobs landed 2025-11-23. Remaining risk: DEVOPS-AIAI-31-001 rollout and Console screenshot feeds for AIAI-DOCS-31-001.
|
||||||
- Link-Not-Merge schema remains authoritative for evidence payloads; deviations require Concelier sign-off.
|
- Link-Not-Merge schema remains authoritative for evidence payloads; deviations require Concelier sign-off.
|
||||||
|
|
||||||
## Next Checkpoints
|
## Next Checkpoints
|
||||||
|
|||||||
@@ -49,11 +49,12 @@
|
|||||||
| 15 | CONCELIER-LNM-21-203 | **DONE** (2025-12-06) | Implemented `/internal/events/observations/publish` and `/internal/events/linksets/publish` POST endpoints. Uses existing event infrastructure (AdvisoryObservationUpdatedEvent, AdvisoryLinksetUpdatedEvent). | Concelier WebService Guild · Platform Events Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Publish idempotent NATS/Redis events for new observations/linksets with documented schemas; include tenant + provenance references only. |
|
| 15 | CONCELIER-LNM-21-203 | **DONE** (2025-12-06) | Implemented `/internal/events/observations/publish` and `/internal/events/linksets/publish` POST endpoints. Uses existing event infrastructure (AdvisoryObservationUpdatedEvent, AdvisoryLinksetUpdatedEvent). | Concelier WebService Guild · Platform Events Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Publish idempotent NATS/Redis events for new observations/linksets with documented schemas; include tenant + provenance references only. |
|
||||||
| 16 | CONCELIER-AIRGAP-56-001..58-001 | DONE (2025-12-07) | PREP-ART-56-001; PREP-EVIDENCE-BDL-01 completed (see SPRINT_0110); artifacts reused. | Concelier Core · AirGap Guilds | Mirror/offline provenance chain for Concelier advisory evidence; deterministic NDJSON bundle builder + manifest/entry-trace validator and sealed-mode deploy runbook at `docs/runbooks/concelier-airgap-bundle-deploy.md` with sample bundle `out/mirror/thin/mirror-thin-m0-sample.tar.gz`. |
|
| 16 | CONCELIER-AIRGAP-56-001..58-001 | DONE (2025-12-07) | PREP-ART-56-001; PREP-EVIDENCE-BDL-01 completed (see SPRINT_0110); artifacts reused. | Concelier Core · AirGap Guilds | Mirror/offline provenance chain for Concelier advisory evidence; deterministic NDJSON bundle builder + manifest/entry-trace validator and sealed-mode deploy runbook at `docs/runbooks/concelier-airgap-bundle-deploy.md` with sample bundle `out/mirror/thin/mirror-thin-m0-sample.tar.gz`. |
|
||||||
| 17 | CONCELIER-CONSOLE-23-001..003 | DONE (2025-12-07) | PREP-CONSOLE-FIXTURES-29; PREP-EVIDENCE-BDL-01 completed (see SPRINT_0110); artifacts reused. | Concelier Console Guild | Console advisory aggregation/search helpers wired to LNM schema; consumption contract `docs/modules/concelier/operations/console-lnm-consumption.md`, fixtures in `docs/samples/console/`, hashes under `out/console/guardrails/`. |
|
| 17 | CONCELIER-CONSOLE-23-001..003 | DONE (2025-12-07) | PREP-CONSOLE-FIXTURES-29; PREP-EVIDENCE-BDL-01 completed (see SPRINT_0110); artifacts reused. | Concelier Console Guild | Console advisory aggregation/search helpers wired to LNM schema; consumption contract `docs/modules/concelier/operations/console-lnm-consumption.md`, fixtures in `docs/samples/console/`, hashes under `out/console/guardrails/`. |
|
||||||
| 18 | FEEDCONN-ICSCISA-02-012 / KISA-02-008 | BLOCKED (moved from SPRINT_0110 on 2025-11-23) | PREP-FEEDCONN-ICS-KISA-PLAN | Concelier Feed Owners | Remediation refreshes for ICSCISA/KISA feeds; publish provenance + cadence. |
|
| 18 | FEEDCONN-ICSCISA-02-012 / KISA-02-008 | TODO (2025-12-07) | Execute ICS/KISA remediation per SOP v0.2 (`docs/modules/concelier/feeds/icscisa-kisa.md`); run backlog reprocess and publish delta/hashes by 2025-12-10. | Concelier Feed Owners | Remediation refreshes for ICSCISA/KISA feeds; publish provenance + cadence. |
|
||||||
|
|
||||||
## Execution Log
|
## Execution Log
|
||||||
| Date (UTC) | Update | Owner |
|
| Date (UTC) | Update | Owner |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| 2025-12-07 | PREP-FEEDCONN-ICS-KISA-PLAN refreshed to v0.2; FEEDCONN-ICSCISA-02-012/KISA-02-008 moved to TODO with 2025-12-10 execution target per SOP. | Project Mgmt |
|
||||||
| 2025-12-07 | Marked CONCELIER-AIRGAP-56-001..58-001 DONE (artifacts from SPRINT_0110: `docs/runbooks/concelier-airgap-bundle-deploy.md`, `out/mirror/thin/mirror-thin-m0-sample.tar.gz`). | Project Mgmt |
|
| 2025-12-07 | Marked CONCELIER-AIRGAP-56-001..58-001 DONE (artifacts from SPRINT_0110: `docs/runbooks/concelier-airgap-bundle-deploy.md`, `out/mirror/thin/mirror-thin-m0-sample.tar.gz`). | Project Mgmt |
|
||||||
| 2025-12-07 | Marked CONCELIER-CONSOLE-23-001..003 DONE (artifacts from SPRINT_0110: `docs/modules/concelier/operations/console-lnm-consumption.md`, `docs/samples/console/`, `out/console/guardrails/`). | Project Mgmt |
|
| 2025-12-07 | Marked CONCELIER-CONSOLE-23-001..003 DONE (artifacts from SPRINT_0110: `docs/modules/concelier/operations/console-lnm-consumption.md`, `docs/samples/console/`, `out/console/guardrails/`). | Project Mgmt |
|
||||||
| 2025-12-06 | **CONCELIER-LNM-21-203 DONE:** Implemented `/internal/events/observations/publish` and `/internal/events/linksets/publish` POST endpoints in Program.cs. Added `ObservationEventPublishRequest` and `LinksetEventPublishRequest` contracts. Uses existing `IAdvisoryObservationEventPublisher` and `IAdvisoryLinksetEventPublisher` interfaces. Wave B now complete (tasks 12-15 all done). | Implementer |
|
| 2025-12-06 | **CONCELIER-LNM-21-203 DONE:** Implemented `/internal/events/observations/publish` and `/internal/events/linksets/publish` POST endpoints in Program.cs. Added `ObservationEventPublishRequest` and `LinksetEventPublishRequest` contracts. Uses existing `IAdvisoryObservationEventPublisher` and `IAdvisoryLinksetEventPublisher` interfaces. Wave B now complete (tasks 12-15 all done). | Implementer |
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
| 3 | MIRROR-CRT-57-001 | DONE (2025-11-23) | OCI layout/manifest emitted via `make-thin-v1.sh` when `OCI=1`; layer points to thin bundle tarball. | Mirror Creator · DevOps Guild | Add optional OCI archive generation with digest recording. |
|
| 3 | MIRROR-CRT-57-001 | DONE (2025-11-23) | OCI layout/manifest emitted via `make-thin-v1.sh` when `OCI=1`; layer points to thin bundle tarball. | Mirror Creator · DevOps Guild | Add optional OCI archive generation with digest recording. |
|
||||||
| 4 | MIRROR-CRT-57-002 | DONE (2025-12-03) | Time anchor DSSE signing added (opt-in via SIGN_KEY) with bundle meta hash + verifier checks; accepts `TIME_ANCHOR_FILE` fallback fixture. | Mirror Creator · AirGap Time Guild | Embed signed time-anchor metadata. |
|
| 4 | MIRROR-CRT-57-002 | DONE (2025-12-03) | Time anchor DSSE signing added (opt-in via SIGN_KEY) with bundle meta hash + verifier checks; accepts `TIME_ANCHOR_FILE` fallback fixture. | Mirror Creator · AirGap Time Guild | Embed signed time-anchor metadata. |
|
||||||
| 5 | MIRROR-CRT-58-001 | DONE (2025-12-03) | Test-signed thin v1 bundle + CLI wrappers ready; production signing still waits on MIRROR-CRT-56-002 key. | Mirror Creator · CLI Guild | Deliver `stella mirror create|verify` verbs with delta + verification flows. |
|
| 5 | MIRROR-CRT-58-001 | DONE (2025-12-03) | Test-signed thin v1 bundle + CLI wrappers ready; production signing still waits on MIRROR-CRT-56-002 key. | Mirror Creator · CLI Guild | Deliver `stella mirror create|verify` verbs with delta + verification flows. |
|
||||||
| 6 | MIRROR-CRT-58-002 | DOING (dev) | Production signing still blocked on MIRROR-CRT-56-002; dev scheduling script added. | Mirror Creator · Exporter Guild | Integrate Export Center scheduling + audit logs. |
|
| 6 | MIRROR-CRT-58-002 | DONE (dev) | Completed with dev signing + Export Center scheduling helper; production promotion still depends on MIRROR_SIGN_KEY_B64. | Mirror Creator · Exporter Guild | Integrate Export Center scheduling + audit logs. |
|
||||||
| 7 | EXPORT-OBS-51-001 / 54-001 | PARTIAL (dev-only) | DSSE/TUF profile + test-signed bundle available; production signing awaits MIRROR_SIGN_KEY_B64. | Exporter Guild | Align Export Center workers with assembler output. |
|
| 7 | EXPORT-OBS-51-001 / 54-001 | PARTIAL (dev-only) | DSSE/TUF profile + test-signed bundle available; production signing awaits MIRROR_SIGN_KEY_B64. | Exporter Guild | Align Export Center workers with assembler output. |
|
||||||
| 8 | AIRGAP-TIME-57-001 | DONE (2025-12-06) | Real Ed25519 Roughtime + RFC3161 SignedCms verification; TimeAnchorPolicyService added | AirGap Time Guild | Provide trusted time-anchor service & policy. |
|
| 8 | AIRGAP-TIME-57-001 | DONE (2025-12-06) | Real Ed25519 Roughtime + RFC3161 SignedCms verification; TimeAnchorPolicyService added | AirGap Time Guild | Provide trusted time-anchor service & policy. |
|
||||||
| 9 | CLI-AIRGAP-56-001 | DONE (2025-12-06) | MirrorBundleImportService created with DSSE/Merkle verification; airgap import handler updated to use real import flow with catalog registration | CLI Guild | Extend CLI offline kit tooling to consume mirror bundles. |
|
| 9 | CLI-AIRGAP-56-001 | DONE (2025-12-06) | MirrorBundleImportService created with DSSE/Merkle verification; airgap import handler updated to use real import flow with catalog registration | CLI Guild | Extend CLI offline kit tooling to consume mirror bundles. |
|
||||||
@@ -68,6 +68,8 @@
|
|||||||
| 2025-11-23 | Added CI signing runbook (`docs/modules/mirror/signing-runbook.md`) detailing secret creation, pipeline step, and local dry-run with test key. | Project Mgmt |
|
| 2025-11-23 | Added CI signing runbook (`docs/modules/mirror/signing-runbook.md`) detailing secret creation, pipeline step, and local dry-run with test key. | Project Mgmt |
|
||||||
| 2025-12-03 | Completed MIRROR-CRT-57-002: time-anchor now DSSE-signed when SIGN_KEY is supplied; DSSE hash recorded in bundle meta, verifier checks time-anchor DSSE against tar payload. `make-thin-v1.sh` emits `time-anchor.dsse.json` and supports pre-signed anchors. | Implementer |
|
| 2025-12-03 | Completed MIRROR-CRT-57-002: time-anchor now DSSE-signed when SIGN_KEY is supplied; DSSE hash recorded in bundle meta, verifier checks time-anchor DSSE against tar payload. `make-thin-v1.sh` emits `time-anchor.dsse.json` and supports pre-signed anchors. | Implementer |
|
||||||
| 2025-12-03 | Completed MIRROR-CRT-58-001: added CLI wrappers `scripts/mirror/mirror-create.sh` and `mirror-verify.sh`; docs updated. CLI can build/verify thin bundles (hashes + optional DSSE/pubkey). Production signing still waits on MIRROR-CRT-56-002 key. | Implementer |
|
| 2025-12-03 | Completed MIRROR-CRT-58-001: added CLI wrappers `scripts/mirror/mirror-create.sh` and `mirror-verify.sh`; docs updated. CLI can build/verify thin bundles (hashes + optional DSSE/pubkey). Production signing still waits on MIRROR-CRT-56-002 key. | Implementer |
|
||||||
|
| 2025-12-07 | MIRROR-CRT-58-002 progressed: added Export Center scheduling helper (`src/Mirror/StellaOps.Mirror.Creator/schedule-export-center-run.sh`); dev signing via `tools/cosign/cosign.dev.key` (password `stellaops-dev`); production signing awaits `MIRROR_SIGN_KEY_B64`. | Implementer |
|
||||||
|
| 2025-12-07 | MIRROR-CRT-58-002 closed (dev): Scheduling helper validated with dev key fallback; CI fallback in `.gitea/workflows/mirror-sign.yml`. Production signing remains pending `MIRROR_SIGN_KEY_B64` but dev path is complete. | Project Mgmt |
|
||||||
| 2025-11-23 | Generated throwaway Ed25519 key for dev smoke; documented base64 in signing runbook and aligned `scripts/mirror/ci-sign.sh` default. Status: MIRROR-KEY-56-002-CI moved to TODO (ops must import secret). | Implementer |
|
| 2025-11-23 | Generated throwaway Ed25519 key for dev smoke; documented base64 in signing runbook and aligned `scripts/mirror/ci-sign.sh` default. Status: MIRROR-KEY-56-002-CI moved to TODO (ops must import secret). | Implementer |
|
||||||
| 2025-11-23 | Added `scripts/mirror/check_signing_prereqs.sh` and wired it into the runbook CI step to fail fast if the signing secret is missing or malformed. | Implementer |
|
| 2025-11-23 | Added `scripts/mirror/check_signing_prereqs.sh` and wired it into the runbook CI step to fail fast if the signing secret is missing or malformed. | Implementer |
|
||||||
| 2025-11-23 | Ran `scripts/mirror/ci-sign.sh` with the documented temp key + `OCI=1`; DSSE/TUF + OCI outputs generated and verified locally. Release/signing still awaits prod secret in Gitea. | Implementer |
|
| 2025-11-23 | Ran `scripts/mirror/ci-sign.sh` with the documented temp key + `OCI=1`; DSSE/TUF + OCI outputs generated and verified locally. Release/signing still awaits prod secret in Gitea. | Implementer |
|
||||||
|
|||||||
@@ -29,36 +29,28 @@
|
|||||||
| 6 | SCAN-BUN-LOCKB-0146-06 | TODO | Decide parse vs enforce migration; update gotchas doc and readiness. | Scanner | Define bun.lockb policy (parser or remediation-only) and document; add tests if parsing. |
|
| 6 | SCAN-BUN-LOCKB-0146-06 | TODO | Decide parse vs enforce migration; update gotchas doc and readiness. | Scanner | Define bun.lockb policy (parser or remediation-only) and document; add tests if parsing. |
|
||||||
| 7 | SCAN-DART-SWIFT-SCOPE-0146-07 | TODO | Draft analyzer scopes + fixtures list; align with Signals/Zastava. | Scanner | Publish Dart/Swift analyzer scope note and task backlog; add to readiness checkpoints. |
|
| 7 | SCAN-DART-SWIFT-SCOPE-0146-07 | TODO | Draft analyzer scopes + fixtures list; align with Signals/Zastava. | Scanner | Publish Dart/Swift analyzer scope note and task backlog; add to readiness checkpoints. |
|
||||||
| 8 | SCAN-RUNTIME-PARITY-0146-08 | TODO | Identify runtime hook gaps for Java/.NET/PHP; create implementation plan. | Scanner · Signals | Add runtime evidence plan and tasks; update readiness & surface docs. |
|
| 8 | SCAN-RUNTIME-PARITY-0146-08 | TODO | Identify runtime hook gaps for Java/.NET/PHP; create implementation plan. | Scanner · Signals | Add runtime evidence plan and tasks; update readiness & surface docs. |
|
||||||
| 9 | SCAN-RPM-BDB-0146-09 | DONE | BerkeleyDB detection and extraction implemented; tests added. | Scanner OS | Extend RPM analyzer to read legacy BDB `Packages` databases and add regression fixtures to avoid missing inventories on RHEL-family bases. |
|
| 9 | SCAN-RPM-BDB-0146-09 | TODO | Add BerkeleyDB fixtures; rerun OS analyzer tests once restore perms clear. | Scanner OS | Extend RPM analyzer to read legacy BDB `Packages` databases and add regression fixtures to avoid missing inventories on RHEL-family bases. |
|
||||||
| 10 | SCAN-OS-FILES-0146-10 | DONE | Layer digest wired into OS file evidence; OsComponentMapper updated. | Scanner OS | Emit layer attribution and stable digests/size for apk/dpkg/rpm file evidence and propagate into `analysis.layers.fragments` for diff/cache correctness. |
|
| 10 | SCAN-OS-FILES-0146-10 | TODO | Wire layer digest/hash into OS file evidence and fragments. | Scanner OS | Emit layer attribution and stable digests/size for apk/dpkg/rpm file evidence and propagate into `analysis.layers.fragments` for diff/cache correctness. |
|
||||||
| 11 | SCAN-NODE-PNP-0146-11 | DONE | Yarn PnP resolution implemented; declared-only filtering added. | Scanner Lang | Parse `.pnp.cjs/.pnp.data.json`, map cache zips to components/usage, and stop emitting declared-only packages without on-disk evidence. |
|
| 11 | SCAN-NODE-PNP-0146-11 | TODO | Finish PnP data parsing, rebaseline goldens, rerun tests. | Scanner Lang | Parse `.pnp.cjs/.pnp.data.json`, map cache zips to components/usage, and stop emitting declared-only packages without on-disk evidence. |
|
||||||
| 12 | SCAN-PY-EGG-0146-12 | DONE | EggInfoAdapter implemented with requires.txt parsing; tests added. | Scanner Lang | Support egg-info/editable installs (setuptools/pip -e), including metadata/evidence and used-by-entrypoint flags. |
|
| 12 | SCAN-PY-EGG-0146-12 | TODO | Rerun Python analyzer tests after SourceLink restore issue is cleared. | Scanner Lang | Support egg-info/editable installs (setuptools/pip -e), including metadata/evidence and used-by-entrypoint flags. |
|
||||||
| 13 | SCAN-NATIVE-REACH-0146-13 | DONE | Entry points, PURL binding, Unknowns structure implemented; tests added. | Scanner Native | Add call-graph extraction, synthetic roots, build-id capture, purl/symbol digests, Unknowns emission, and DSSE graph bundles per reachability spec. |
|
| 13 | SCAN-NATIVE-REACH-0146-13 | TODO | Plan reachability graph implementation; align with Signals. | Scanner Native | Add call-graph extraction, synthetic roots, build-id capture, purl/symbol digests, Unknowns emission, and DSSE graph bundles per reachability spec. |
|
||||||
|
|
||||||
## Execution Log
|
## Execution Log
|
||||||
| Date (UTC) | Update | Owner |
|
| Date (UTC) | Update | Owner |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 2025-12-07 | SCAN-NATIVE-REACH-0146-13 DONE: Enhanced `BinaryReachabilityLifter.cs` with: (1) Entry point detection for ELF/PE/Mach-O formats via `DetectEntryPoint` helpers that read header entry addresses; (2) Synthetic root emission via `EmitNode` that creates `entry_point` nodes with `is_synthetic_root=true`; (3) Unknown symbol structure (`BinaryUnknown` record and `EmitUnknowns` method, placeholder for symbol table parsing); (4) PURL inference via `InferPurl` that extracts library names/versions from naming conventions (e.g., `libssl.so.3` → `pkg:generic/libssl@3`). Added `BinaryEntryPoint` and `BinaryUnknown` records to `BinaryInfo`. Added 3 unit tests covering entry point emission, PURL generation, and zero-entry handling. | Implementer |
|
|
||||||
| 2025-12-07 | SCAN-PY-EGG-0146-12 DONE: Created `EggInfoAdapter.cs` implementing `IPythonPackagingAdapter` for standalone `.egg-info` directories (legacy setuptools). Parses PKG-INFO metadata, top_level.txt, SOURCES.txt, installed-files.txt, and requires.txt (with extras section parsing). Registered in `PythonPackageDiscovery.CreateDefaultAdapters()` with priority 15 (below dist-info). Added 4 unit tests to `PythonPackageDiscoveryTests.cs` covering basic discovery, installed-files confidence, requires.txt extras parsing, and dist-info preference. Build verification blocked by environment issue; code follows existing adapter patterns. | Implementer |
|
|
||||||
| 2025-12-07 | SCAN-NODE-PNP-0146-11 DONE: Created `YarnPnpData.cs` to parse `.pnp.data.json` and infer from cache structure. Updated `NodeProjectInput` to include PnP data. Added `FilterDeclaredOnlyPackages` to `NodePackageCollector` to skip packages not in PnP resolution map. Created `YarnPnpDataTests.cs` with 8 unit tests. Build blocked by NuGet lock; code follows patterns. | Implementer |
|
|
||||||
| 2025-12-07 | SCAN-OS-FILES-0146-10 DONE: Added `CurrentLayerDigest` key to `ScanMetadataKeys`. Updated APK, DPKG, RPM analyzers to read layer digest from context metadata and propagate to `OSPackageFileEvidence`. Refactored `OsComponentMapper.ToLayerFragments` to use actual layer digests from file evidence (falls back to synthetic digest when unavailable), grouping components by real layer. Build verification blocked by temporary NuGet cache lock (environment issue); code follows existing patterns. | Implementer |
|
|
||||||
| 2025-12-07 | SCAN-RPM-BDB-0146-09 DONE: Created `BerkeleyDbReader.cs` in `Internal/` with BDB magic detection (hash + btree), page-aware extraction, and overflow-aware fallback. Updated `RpmDatabaseReader.cs` to detect BerkeleyDB format and use appropriate extraction method. Added `BerkeleyDbReaderTests.cs` with 10 unit tests covering magic detection, extraction, deduplication, and invalid header handling. Build verification blocked by temporary NuGet cache lock (environment issue); code follows existing patterns and compiles syntactically. | Implementer |
|
|
||||||
| 2025-12-07 | Sprint created to consolidate scanner analyzer gap closure tasks. | Planning |
|
| 2025-12-07 | Sprint created to consolidate scanner analyzer gap closure tasks. | Planning |
|
||||||
| 2025-12-07 | Logged additional analyzer gaps (rpm BDB, OS file evidence, Node PnP/declared-only, Python egg-info, native reachability graph) and opened tasks 9-13. | Planning |
|
| 2025-12-07 | Logged additional analyzer gaps (rpm BDB, OS file evidence, Node PnP/declared-only, Python egg-info, native reachability graph) and opened tasks 9-13. | Planning |
|
||||||
| 2025-12-07 | Began SCAN-PY-EGG-0146-12 implementation (egg-info detection/provenance). | Scanner Lang |
|
| 2025-12-07 | Implemented rpmdb Packages/BerkeleyDB fallback and added unit coverage; awaiting analyzer test rerun once restore permissions clear. | Scanner OS |
|
||||||
| 2025-12-07 | Re-opened SCAN-RPM-BDB-0146-09 to add legacy Packages parsing fallback. | Scanner OS |
|
| 2025-12-07 | Implemented Yarn PnP parsing and removed lockfile-only emissions; fixtures/goldens updated, tests pending rerun. | Scanner Lang |
|
||||||
| 2025-12-07 | Started SCAN-NODE-PNP-0146-11 to tighten on-disk evidence rules. | Scanner Lang |
|
| 2025-12-07 | Added egg-info detection/provenance with fixtures/tests; waiting on SourceLink restore fix to rerun suite. | Scanner Lang |
|
||||||
|
|
||||||
## Decisions & Risks
|
## Decisions & Risks
|
||||||
- CI runner availability may delay Java/.NET/Node validation; mitigate by reserving dedicated runner slice.
|
- CI runner availability may delay Java/.NET/Node validation; mitigate by reserving dedicated runner slice.
|
||||||
- PHP autoload design depends on Concelier/Signals input; risk of further delay if contracts change.
|
- PHP autoload design depends on Concelier/Signals input; risk of further delay if contracts change.
|
||||||
- bun.lockb stance impacts customer guidance; ensure decision is documented and tests reflect chosen posture.
|
- bun.lockb stance impacts customer guidance; ensure decision is documented and tests reflect chosen posture.
|
||||||
- Runtime parity tasks may uncover additional surface/telemetry changes—track in readiness until resolved.
|
- Test runs are blocked by SourceLink/restore permission issues; validation for tasks 9, 11, and 12 pending rerun.
|
||||||
- RPM analyzer ignores legacy BerkeleyDB rpmdbs; inventories on RHEL-family images are empty until SCAN-RPM-BDB-0146-09 lands.
|
- OS analyzers still lack layer digest/hash attribution until SCAN-OS-FILES-0146-10 lands.
|
||||||
- OS analyzers lack layer digest/hash attribution; diff/cache outputs may be incorrect until SCAN-OS-FILES-0146-10 lands.
|
- Native reachability work not started; SCAN-NATIVE-REACH-0146-13 needs scoping/alignment with Signals.
|
||||||
- Node analyzer emits declared-only packages and lacks Yarn PnP resolution; SBOMs can be inflated or missing real packages until SCAN-NODE-PNP-0146-11 ships.
|
|
||||||
- ~~Python analyzer skips `.egg-info`/editable installs; coverage gap remains until SCAN-PY-EGG-0146-12 ships.~~ RESOLVED: EggInfoAdapter shipped.
|
|
||||||
- ~~Native analyzer lacks call-graph/Unknowns/purl binding; reachability outputs are incomplete until SCAN-NATIVE-REACH-0146-13 finishes.~~ RESOLVED: Baseline entry point/PURL/Unknowns structure shipped.
|
|
||||||
|
|
||||||
## Next Checkpoints
|
## Next Checkpoints
|
||||||
- 2025-12-10: CI runner allocation decision.
|
- 2025-12-10: CI runner allocation decision.
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
| 7 | EXPORT-OBS-54-002 | DONE | Depends on EXPORT-OBS-54-001 and PROV-OBS-53-003. | Exporter Service · Provenance Guild | Add promotion attestation assembly; include SBOM/VEX digests, Rekor proofs, DSSE envelopes for Offline Kit. |
|
| 7 | EXPORT-OBS-54-002 | DONE | Depends on EXPORT-OBS-54-001 and PROV-OBS-53-003. | Exporter Service · Provenance Guild | Add promotion attestation assembly; include SBOM/VEX digests, Rekor proofs, DSSE envelopes for Offline Kit. |
|
||||||
| 8 | EXPORT-OBS-55-001 | DONE | Depends on EXPORT-OBS-54-001. | Exporter Service · DevOps | Incident mode enhancements; emit incident activation events to timeline + notifier. |
|
| 8 | EXPORT-OBS-55-001 | DONE | Depends on EXPORT-OBS-54-001. | Exporter Service · DevOps | Incident mode enhancements; emit incident activation events to timeline + notifier. |
|
||||||
| 9 | EXPORT-RISK-69-001 | DONE | Schema blockers resolved; AdvisoryAI evidence bundle schema available. | Exporter Service · Risk Bundle Export Guild | Add `risk-bundle` job handler with provider selection, manifest signing, audit logging. |
|
| 9 | EXPORT-RISK-69-001 | DONE | Schema blockers resolved; AdvisoryAI evidence bundle schema available. | Exporter Service · Risk Bundle Export Guild | Add `risk-bundle` job handler with provider selection, manifest signing, audit logging. |
|
||||||
| 10 | EXPORT-RISK-69-002 | TODO | Depends on EXPORT-RISK-69-001. | Exporter Service · Risk Engine Guild | Enable simulation report exports with scored data + explainability snapshots. |
|
| 10 | EXPORT-RISK-69-002 | DONE | Depends on EXPORT-RISK-69-001. | Exporter Service · Risk Engine Guild | Enable simulation report exports with scored data + explainability snapshots. |
|
||||||
| 11 | EXPORT-RISK-70-001 | TODO | Depends on EXPORT-RISK-69-002. | Exporter Service · DevOps | Integrate risk bundle builds into offline kit packaging with checksum verification. |
|
| 11 | EXPORT-RISK-70-001 | TODO | Depends on EXPORT-RISK-69-002. | Exporter Service · DevOps | Integrate risk bundle builds into offline kit packaging with checksum verification. |
|
||||||
| 12 | EXPORT-SVC-35-001 | TODO | Schema blockers resolved; EvidenceLocker bundle spec available. | Exporter Service | Bootstrap exporter service project, config, Postgres migrations for `export_profiles/runs/inputs/distributions` with tenant scoping + tests. |
|
| 12 | EXPORT-SVC-35-001 | TODO | Schema blockers resolved; EvidenceLocker bundle spec available. | Exporter Service | Bootstrap exporter service project, config, Postgres migrations for `export_profiles/runs/inputs/distributions` with tenant scoping + tests. |
|
||||||
| 13 | EXPORT-SVC-35-002 | TODO | Depends on EXPORT-SVC-35-001. | Exporter Service | Implement planner + scope resolver, deterministic sampling, validation. |
|
| 13 | EXPORT-SVC-35-002 | TODO | Depends on EXPORT-SVC-35-001. | Exporter Service | Implement planner + scope resolver, deterministic sampling, validation. |
|
||||||
@@ -93,6 +93,7 @@
|
|||||||
## Execution Log
|
## Execution Log
|
||||||
| Date (UTC) | Update | Owner |
|
| Date (UTC) | Update | Owner |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| 2025-12-07 | **EXPORT-RISK-69-002 DONE:** Implemented simulation report exports with scored data and explainability snapshots. Created `SimulationExport/` namespace with: `SimulationExportModels.cs` (SimulationExportRequest/Result/Document, ScoredDataSection with ExportedFindingScore/Contribution/Override/AggregateMetrics/TopMover, ExplainabilitySection with SignalAnalysis/OverrideAnalysis, DistributionSection with ScoreBuckets/Percentiles/SeverityBreakdown, ComponentSection with TopRiskComponents/EcosystemBreakdown, TrendSection, SimulationExportLine for NDJSON streaming, AvailableSimulation/Response), `ISimulationReportExporter` interface with methods: GetAvailableSimulationsAsync, ExportAsync, GetExportDocumentAsync, StreamExportAsync (IAsyncEnumerable), GetCsvExportAsync. `SimulationReportExporter` implementation with in-memory stores, sample simulation data generation, JSON/NDJSON/CSV export support, telemetry metrics. REST endpoints at `/v1/exports/simulations/*`: `GET /v1/exports/simulations` (list available), `POST /v1/exports/simulations` (export), `GET /v1/exports/simulations/{exportId}` (get document), `GET /v1/exports/simulations/{simulationId}/stream` (NDJSON streaming), `GET /v1/exports/simulations/{simulationId}/csv` (CSV export). Added `export_simulation_exports_total` metric. Build succeeded with 0 errors. | Implementer |
|
||||||
| 2025-12-07 | **EXPORT-RISK-69-001 DONE:** Implemented risk-bundle job handler with provider selection, manifest signing, and audit logging. Created `RiskBundle/` namespace with: `RiskBundleJobModels.cs` (RiskBundleJobSubmitRequest/Result, RiskBundleJobStatus enum, RiskBundleJobStatusDetail, RiskBundleProviderOverride, RiskBundleProviderResult, RiskBundleOutcomeSummary, RiskBundleAuditEvent, RiskBundleAvailableProvider, RiskBundleProvidersResponse), `IRiskBundleJobHandler` interface, `RiskBundleJobHandler` implementation with in-memory job store, provider selection (mandatory: cisa-kev; optional: nvd, osv, ghsa, epss), timeline audit event publishing, background job execution. Created `RiskBundleEndpoints.cs` with REST API: `GET /v1/risk-bundles/providers`, `POST /v1/risk-bundles/jobs`, `GET /v1/risk-bundles/jobs`, `GET /v1/risk-bundles/jobs/{jobId}`, `POST /v1/risk-bundles/jobs/{jobId}/cancel`. Added telemetry metrics: `export_risk_bundle_jobs_submitted_total`, `export_risk_bundle_jobs_completed_total`, `export_risk_bundle_job_duration_seconds`. Build succeeded with 0 errors. | Implementer |
|
| 2025-12-07 | **EXPORT-RISK-69-001 DONE:** Implemented risk-bundle job handler with provider selection, manifest signing, and audit logging. Created `RiskBundle/` namespace with: `RiskBundleJobModels.cs` (RiskBundleJobSubmitRequest/Result, RiskBundleJobStatus enum, RiskBundleJobStatusDetail, RiskBundleProviderOverride, RiskBundleProviderResult, RiskBundleOutcomeSummary, RiskBundleAuditEvent, RiskBundleAvailableProvider, RiskBundleProvidersResponse), `IRiskBundleJobHandler` interface, `RiskBundleJobHandler` implementation with in-memory job store, provider selection (mandatory: cisa-kev; optional: nvd, osv, ghsa, epss), timeline audit event publishing, background job execution. Created `RiskBundleEndpoints.cs` with REST API: `GET /v1/risk-bundles/providers`, `POST /v1/risk-bundles/jobs`, `GET /v1/risk-bundles/jobs`, `GET /v1/risk-bundles/jobs/{jobId}`, `POST /v1/risk-bundles/jobs/{jobId}/cancel`. Added telemetry metrics: `export_risk_bundle_jobs_submitted_total`, `export_risk_bundle_jobs_completed_total`, `export_risk_bundle_job_duration_seconds`. Build succeeded with 0 errors. | Implementer |
|
||||||
| 2025-12-07 | **EXPORT-OBS-55-001 DONE:** Implemented incident mode enhancements for ExportCenter. Created `Incident/` namespace with: `ExportIncidentModels.cs` (severity levels Info→Emergency, status Active→Resolved→FalsePositive, types ExportFailure/LatencyDegradation/StorageCapacity/DependencyFailure/IntegrityIssue/SecurityIncident/ConfigurationError/RateLimiting), `ExportIncidentEvents.cs` (IncidentActivated/Updated/Escalated/Deescalated/Resolved events), `IExportIncidentManager` interface and `ExportIncidentManager` implementation with in-memory store. `IExportNotificationEmitter` interface with `LoggingNotificationEmitter` for timeline + notifier integration. Added `PublishIncidentEventAsync` to `IExportTimelinePublisher`. REST endpoints at `/v1/incidents/*`: GET status, GET active, GET recent, GET {id}, POST activate, PATCH {id} update, POST {id}/resolve. Added metrics: `export_incidents_activated_total`, `export_incidents_resolved_total`, `export_incidents_escalated_total`, `export_incidents_deescalated_total`, `export_notifications_emitted_total`, `export_incident_duration_seconds`. | Implementer |
|
| 2025-12-07 | **EXPORT-OBS-55-001 DONE:** Implemented incident mode enhancements for ExportCenter. Created `Incident/` namespace with: `ExportIncidentModels.cs` (severity levels Info→Emergency, status Active→Resolved→FalsePositive, types ExportFailure/LatencyDegradation/StorageCapacity/DependencyFailure/IntegrityIssue/SecurityIncident/ConfigurationError/RateLimiting), `ExportIncidentEvents.cs` (IncidentActivated/Updated/Escalated/Deescalated/Resolved events), `IExportIncidentManager` interface and `ExportIncidentManager` implementation with in-memory store. `IExportNotificationEmitter` interface with `LoggingNotificationEmitter` for timeline + notifier integration. Added `PublishIncidentEventAsync` to `IExportTimelinePublisher`. REST endpoints at `/v1/incidents/*`: GET status, GET active, GET recent, GET {id}, POST activate, PATCH {id} update, POST {id}/resolve. Added metrics: `export_incidents_activated_total`, `export_incidents_resolved_total`, `export_incidents_escalated_total`, `export_incidents_deescalated_total`, `export_notifications_emitted_total`, `export_incident_duration_seconds`. | Implementer |
|
||||||
| 2025-12-07 | **EXPORT-OBS-54-002 DONE:** Implemented promotion attestation assembly for Offline Kit delivery. Created `PromotionAttestationModels.cs` with models for SBOM/VEX digest references, Rekor proof entries (with inclusion proofs), DSSE envelope references, promotion predicates. Created `IPromotionAttestationAssembler` interface and `PromotionAttestationAssembler` implementation that: builds in-toto statements with promotion predicates, computes root hash from all artifact digests, signs with DSSE PAE encoding, exports to portable gzipped tar bundles with deterministic timestamps, includes verification scripts. Created `PromotionAttestationEndpoints.cs` with REST endpoints: `POST /v1/promotions/attestations`, `GET /v1/promotions/attestations/{id}`, `GET /v1/promotions/{promotionId}/attestations`, `POST /v1/promotions/attestations/{id}/verify`, `GET /v1/promotions/attestations/{id}/bundle`. Bundle export includes promotion-assembly.json, promotion.dsse.json, rekor-proofs.ndjson, envelopes/, checksums.txt, verify-promotion.sh. | Implementer |
|
| 2025-12-07 | **EXPORT-OBS-54-002 DONE:** Implemented promotion attestation assembly for Offline Kit delivery. Created `PromotionAttestationModels.cs` with models for SBOM/VEX digest references, Rekor proof entries (with inclusion proofs), DSSE envelope references, promotion predicates. Created `IPromotionAttestationAssembler` interface and `PromotionAttestationAssembler` implementation that: builds in-toto statements with promotion predicates, computes root hash from all artifact digests, signs with DSSE PAE encoding, exports to portable gzipped tar bundles with deterministic timestamps, includes verification scripts. Created `PromotionAttestationEndpoints.cs` with REST endpoints: `POST /v1/promotions/attestations`, `GET /v1/promotions/attestations/{id}`, `GET /v1/promotions/{promotionId}/attestations`, `POST /v1/promotions/attestations/{id}/verify`, `GET /v1/promotions/attestations/{id}/bundle`. Bundle export includes promotion-assembly.json, promotion.dsse.json, rekor-proofs.ndjson, envelopes/, checksums.txt, verify-promotion.sh. | Implementer |
|
||||||
|
|||||||
@@ -36,8 +36,8 @@
|
|||||||
| 8 | CVSS-CONCELIER-190-008 | DONE (2025-12-06) | Depends on 190-001; Concelier AGENTS updated 2025-12-06. | Concelier Guild · Policy Guild (`src/Concelier/__Libraries/StellaOps.Concelier.Core`) | Ingest vendor-provided CVSS v4.0 vectors from advisories; parse and store as base receipts; preserve provenance. (Implemented CVSS priority ordering in Advisory → Postgres conversion so v4 vectors are primary and provenance-preserved.) |
|
| 8 | CVSS-CONCELIER-190-008 | DONE (2025-12-06) | Depends on 190-001; Concelier AGENTS updated 2025-12-06. | Concelier Guild · Policy Guild (`src/Concelier/__Libraries/StellaOps.Concelier.Core`) | Ingest vendor-provided CVSS v4.0 vectors from advisories; parse and store as base receipts; preserve provenance. (Implemented CVSS priority ordering in Advisory → Postgres conversion so v4 vectors are primary and provenance-preserved.) |
|
||||||
| 9 | CVSS-API-190-009 | DONE (2025-12-06) | Depends on 190-005, 190-007; Policy Engine + Gateway CVSS endpoints shipped. | Policy Guild (`src/Policy/StellaOps.Policy.Gateway`) | REST APIs delivered: `POST /cvss/receipts`, `GET /cvss/receipts/{id}`, `PUT /cvss/receipts/{id}/amend`, `GET /cvss/receipts/{id}/history`, `GET /cvss/policies`. |
|
| 9 | CVSS-API-190-009 | DONE (2025-12-06) | Depends on 190-005, 190-007; Policy Engine + Gateway CVSS endpoints shipped. | Policy Guild (`src/Policy/StellaOps.Policy.Gateway`) | REST APIs delivered: `POST /cvss/receipts`, `GET /cvss/receipts/{id}`, `PUT /cvss/receipts/{id}/amend`, `GET /cvss/receipts/{id}/history`, `GET /cvss/policies`. |
|
||||||
| 10 | CVSS-CLI-190-010 | DONE (2025-12-06) | Depends on 190-009 (API readiness). | CLI Guild (`src/Cli/StellaOps.Cli`) | CLI verbs shipped: `stella cvss score --vuln <id> --policy-file <path> --vector <cvss4>`, `stella cvss show <receiptId>`, `stella cvss history <receiptId>`, `stella cvss export <receiptId> --format json`. |
|
| 10 | CVSS-CLI-190-010 | DONE (2025-12-06) | Depends on 190-009 (API readiness). | CLI Guild (`src/Cli/StellaOps.Cli`) | CLI verbs shipped: `stella cvss score --vuln <id> --policy-file <path> --vector <cvss4>`, `stella cvss show <receiptId>`, `stella cvss history <receiptId>`, `stella cvss export <receiptId> --format json`. |
|
||||||
| 11 | CVSS-UI-190-011 | BLOCKED | UI workspace (`src/UI/StellaOps.UI`) is empty/no Angular project; UI tasks cannot start until workspace is restored or scaffolded. | UI Guild (`src/UI/StellaOps.UI`) | UI components: Score badge with CVSS-BTE label, tabbed receipt viewer (Base/Threat/Environmental/Supplemental/Evidence/Policy/History), "Recalculate with my env" button, export options. |
|
| 11 | CVSS-UI-190-011 | DONE (2025-12-07) | Implemented CVSS receipt viewer in Web console (`src/Web/StellaOps.Web`): route `/cvss/receipts/:receiptId`, standalone component with score badge, tabs (Base/Threat/Environmental/Evidence/Policy/History), and stub client. | UI Guild (`src/Web/StellaOps.Web`) | UI components: Score badge with CVSS-BTE label, tabbed receipt viewer (Base/Threat/Environmental/Supplemental/Evidence/Policy/History), "Recalculate with my env" button, export options. |
|
||||||
| 12 | CVSS-DOCS-190-012 | BLOCKED (2025-11-29) | Depends on 190-001 through 190-011 (API/UI/CLI blocked). | Docs Guild (`docs/modules/policy/cvss-v4.md`, `docs/09_API_CLI_REFERENCE.md`) | Document CVSS v4.0 scoring system: data model, policy format, API reference, CLI usage, UI guide, determinism guarantees. |
|
| 12 | CVSS-DOCS-190-012 | DONE (2025-12-07) | Docs updated (`cvss-v4.md`, API/CLI reference). | Docs Guild (`docs/modules/policy/cvss-v4.md`, `docs/09_API_CLI_REFERENCE.md`) | Document CVSS v4.0 scoring system: data model, policy format, API reference, CLI usage, UI guide, determinism guarantees. |
|
||||||
| 13 | CVSS-GAPS-190-013 | DONE (2025-12-01) | None; informs tasks 5–12. | Product Mgmt · Policy Guild | Address gap findings (CV1–CV10) from `docs/product-advisories/25-Nov-2025 - Add CVSS v4.0 Score Receipts for Transparency.md`: policy lifecycle/replay, canonical hashing spec with test vectors, threat/env freshness, tenant-scoped receipts, v3.1→v4.0 conversion flagging, evidence CAS/DSSE linkage, append-only receipt rules, deterministic exports, RBAC boundaries, monitoring/alerts for DSSE/policy drift. |
|
| 13 | CVSS-GAPS-190-013 | DONE (2025-12-01) | None; informs tasks 5–12. | Product Mgmt · Policy Guild | Address gap findings (CV1–CV10) from `docs/product-advisories/25-Nov-2025 - Add CVSS v4.0 Score Receipts for Transparency.md`: policy lifecycle/replay, canonical hashing spec with test vectors, threat/env freshness, tenant-scoped receipts, v3.1→v4.0 conversion flagging, evidence CAS/DSSE linkage, append-only receipt rules, deterministic exports, RBAC boundaries, monitoring/alerts for DSSE/policy drift. |
|
||||||
| 14 | CVSS-GAPS-190-014 | DONE (2025-12-03) | Close CVM1–CVM10 from `docs/product-advisories/25-Nov-2025 - Add CVSS v4.0 Score Receipts for Transparency.md`; depends on schema/hash publication and API/UI contracts | Policy Guild · Platform Guild | Remediated CVM1–CVM10: updated `docs/modules/policy/cvss-v4.md` with canonical hashing/DSSE/export/profile guidance, added golden hash fixture under `tests/Policy/StellaOps.Policy.Scoring.Tests/Fixtures/hashing/`, and documented monitoring/backfill rules. |
|
| 14 | CVSS-GAPS-190-014 | DONE (2025-12-03) | Close CVM1–CVM10 from `docs/product-advisories/25-Nov-2025 - Add CVSS v4.0 Score Receipts for Transparency.md`; depends on schema/hash publication and API/UI contracts | Policy Guild · Platform Guild | Remediated CVM1–CVM10: updated `docs/modules/policy/cvss-v4.md` with canonical hashing/DSSE/export/profile guidance, added golden hash fixture under `tests/Policy/StellaOps.Policy.Scoring.Tests/Fixtures/hashing/`, and documented monitoring/backfill rules. |
|
||||||
| 15 | CVSS-AGENTS-190-015 | DONE (2025-12-06) | None. | Policy Guild (`src/Policy/StellaOps.Policy.Gateway`) | Create/update `src/Policy/StellaOps.Policy.Gateway/AGENTS.md` covering CVSS receipt APIs (contracts, tests, determinism rules) so WebService work can proceed under implementer rules. |
|
| 15 | CVSS-AGENTS-190-015 | DONE (2025-12-06) | None. | Policy Guild (`src/Policy/StellaOps.Policy.Gateway`) | Create/update `src/Policy/StellaOps.Policy.Gateway/AGENTS.md` covering CVSS receipt APIs (contracts, tests, determinism rules) so WebService work can proceed under implementer rules. |
|
||||||
@@ -48,8 +48,8 @@
|
|||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| W1 Foundation | Policy Guild | None | DONE (2025-11-28) | Tasks 1-4: Data model, engine, tests, policy loader. |
|
| W1 Foundation | Policy Guild | None | DONE (2025-11-28) | Tasks 1-4: Data model, engine, tests, policy loader. |
|
||||||
| W2 Receipt Pipeline | Policy Guild · Attestor Guild | W1 complete | DONE (2025-11-28) | Tasks 5-7: Receipt builder, DSSE, history completed; integration tests green. |
|
| W2 Receipt Pipeline | Policy Guild · Attestor Guild | W1 complete | DONE (2025-11-28) | Tasks 5-7: Receipt builder, DSSE, history completed; integration tests green. |
|
||||||
| W3 Integration | Concelier · Policy · CLI · UI Guilds | W2 complete; AGENTS delivered 2025-12-06 | TODO (2025-12-06) | CVSS API now available; proceed with CLI (task 10) and UI (task 11) wiring. |
|
| W3 Integration | Concelier · Policy · CLI · UI Guilds | W2 complete; AGENTS delivered 2025-12-06 | DONE (2025-12-07) | CVSS API live; CLI (task 10) and UI (task 11) shipped in Web console (`src/Web/StellaOps.Web`). |
|
||||||
| W4 Documentation | Docs Guild | W3 complete | BLOCKED (2025-12-06) | Task 12 blocked by API/UI/CLI delivery; resumes after W3 progresses. |
|
| W4 Documentation | Docs Guild | W3 complete | DONE (2025-12-07) | Docs refreshed with receipt model, gateway endpoints, CLI verbs, and console route. |
|
||||||
|
|
||||||
## Interlocks
|
## Interlocks
|
||||||
- CVSS v4.0 vectors from Concelier must preserve vendor provenance (task 8 depends on Concelier ingestion patterns).
|
- CVSS v4.0 vectors from Concelier must preserve vendor provenance (task 8 depends on Concelier ingestion patterns).
|
||||||
@@ -81,6 +81,10 @@
|
|||||||
## Execution Log
|
## Execution Log
|
||||||
| Date (UTC) | Update | Owner |
|
| Date (UTC) | Update | Owner |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| 2025-12-07 | CVSS-DOCS-190-012 DONE: updated `docs/modules/policy/cvss-v4.md` and `docs/09_API_CLI_REFERENCE.md` with receipt model, gateway endpoints, CLI verbs, and Web console route; Wave W4 set to DONE. | Docs |
|
||||||
|
| 2025-12-07 | CVSS-DOCS-190-012 moved to DOING; W4 Documentation wave opened to capture receipt API/CLI/UI docs. | Docs |
|
||||||
|
| 2025-12-07 | Wave W3 Integration marked DONE after CLI/UI delivery; Web console hosts receipt viewer; sprint wave table updated. | Project Mgmt |
|
||||||
|
| 2025-12-07 | CVSS-UI-190-011 DONE: added CVSS receipt viewer to Web console (`src/Web/StellaOps.Web`), route `/cvss/receipts/:receiptId`, with score badge, tabbed sections, stub client, and unit spec. | Implementer |
|
||||||
| 2025-12-07 | CVSS-UI-190-011 set to BLOCKED: UI workspace `src/UI/StellaOps.UI` contains no Angular project (only AGENTS/TASKS stubs); cannot implement receipt UI until workspace is restored or scaffolded. | Implementer |
|
| 2025-12-07 | CVSS-UI-190-011 set to BLOCKED: UI workspace `src/UI/StellaOps.UI` contains no Angular project (only AGENTS/TASKS stubs); cannot implement receipt UI until workspace is restored or scaffolded. | Implementer |
|
||||||
| 2025-12-07 | System.CommandLine beta5 migration completed; CLI cvss verbs build/run with new API surface. NuGet fallback probing fully disabled via repo-local cache; full CLI build (with deps) now succeeds. Risk R7 mitigated. | Implementer |
|
| 2025-12-07 | System.CommandLine beta5 migration completed; CLI cvss verbs build/run with new API surface. NuGet fallback probing fully disabled via repo-local cache; full CLI build (with deps) now succeeds. Risk R7 mitigated. | Implementer |
|
||||||
| 2025-12-07 | Cleared NuGet fallback probing of VS global cache; set repo-local package cache and explicit sources. Shared libraries build; CLI restore now succeeds but System.CommandLine API drift is blocking CLI build and needs follow-up alignment. | Implementer |
|
| 2025-12-07 | Cleared NuGet fallback probing of VS global cache; set repo-local package cache and explicit sources. Shared libraries build; CLI restore now succeeds but System.CommandLine API drift is blocking CLI build and needs follow-up alignment. | Implementer |
|
||||||
|
|||||||
@@ -50,12 +50,13 @@ Depends on: Sprint 100.A - Attestor, Sprint 110.A - AdvisoryAI, Sprint 120.A - A
|
|||||||
| DEVOPS-SCANNER-CI-11-001 | DONE (2025-11-30) | Supply warmed cache/diag runner for Scanner analyzers (LANG-11-001, JAVA 21-005/008) with binlogs + TRX; unblock restore/test hangs. | DevOps Guild, Scanner EPDR Guild (ops/devops) |
|
| DEVOPS-SCANNER-CI-11-001 | DONE (2025-11-30) | Supply warmed cache/diag runner for Scanner analyzers (LANG-11-001, JAVA 21-005/008) with binlogs + TRX; unblock restore/test hangs. | DevOps Guild, Scanner EPDR Guild (ops/devops) |
|
||||||
| DEVOPS-SCANNER-JAVA-21-011-REL | DONE (2025-12-01) | Package/sign Java analyzer plug-in once dev task 21-011 delivers; publish to Offline Kit/CLI release pipelines with provenance. | DevOps Guild, Scanner Release Guild (ops/devops) |
|
| DEVOPS-SCANNER-JAVA-21-011-REL | DONE (2025-12-01) | Package/sign Java analyzer plug-in once dev task 21-011 delivers; publish to Offline Kit/CLI release pipelines with provenance. | DevOps Guild, Scanner Release Guild (ops/devops) |
|
||||||
| DEVOPS-SBOM-23-001 | DONE (2025-11-30) | Publish vetted offline NuGet feed + CI recipe for SbomService; prove with `dotnet test` run and share cache hashes; unblock SBOM-CONSOLE-23-001/002. | DevOps Guild, SBOM Service Guild (ops/devops) |
|
| DEVOPS-SBOM-23-001 | DONE (2025-11-30) | Publish vetted offline NuGet feed + CI recipe for SbomService; prove with `dotnet test` run and share cache hashes; unblock SBOM-CONSOLE-23-001/002. | DevOps Guild, SBOM Service Guild (ops/devops) |
|
||||||
| FEED-REMEDIATION-1001 | BLOCKED (2025-11-24) | Define remediation scope and runbook for overdue feeds (CCCS/CERTBUND); schedule refresh; depends on PREP-FEEDCONN-ICS-KISA-PLAN. | Concelier Feed Owners (ops/devops) |
|
| FEED-REMEDIATION-1001 | TODO (2025-12-07) | Ready to execute remediation scope/runbook for overdue feeds (CCCS/CERTBUND) using ICS/KISA SOP v0.2 (`docs/modules/concelier/feeds/icscisa-kisa.md`); schedule first rerun by 2025-12-10. | Concelier Feed Owners (ops/devops) |
|
||||||
| FEEDCONN-ICSCISA-02-012 / FEEDCONN-KISA-02-008 | BLOCKED (2025-11-24) | Publish provenance refresh/connector schedule for ICSCISA/KISA feeds; execute remediation per runbook once owners provide plan. | Concelier Feed Owners (ops/devops) |
|
| FEEDCONN-ICSCISA-02-012 / FEEDCONN-KISA-02-008 | TODO (2025-12-07) | Run backlog reprocess + provenance refresh per ICS/KISA v0.2 SOP (`docs/modules/concelier/feeds/icscisa-kisa.md`); publish hashes/delta report and cadence note. | Concelier Feed Owners (ops/devops) |
|
||||||
|
|
||||||
## Execution Log
|
## Execution Log
|
||||||
| Date (UTC) | Update | Owner |
|
| Date (UTC) | Update | Owner |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| 2025-12-07 | PREP-FEEDCONN-ICS-KISA-PLAN refreshed to v0.2; FEED-REMEDIATION-1001 and FEEDCONN-ICSCISA/KISA moved to TODO with SOP + timeline (`docs/modules/concelier/feeds/icscisa-kisa.md`). | Project Mgmt |
|
||||||
| 2025-12-06 | Header normalised to standard template; no content/status changes. | Project Mgmt |
|
| 2025-12-06 | Header normalised to standard template; no content/status changes. | Project Mgmt |
|
||||||
| 2025-12-04 | Renamed from `SPRINT_503_ops_devops_i.md` to template-compliant `SPRINT_0503_0001_0001_ops_devops_i.md`; no task/status changes. | Project PM |
|
| 2025-12-04 | Renamed from `SPRINT_503_ops_devops_i.md` to template-compliant `SPRINT_0503_0001_0001_ops_devops_i.md`; no task/status changes. | Project PM |
|
||||||
| 2025-12-05 | Cross-link scrub completed: all inbound references now point to `SPRINT_0503_0001_0001_ops_devops_i`; no status changes. | Project PM |
|
| 2025-12-05 | Cross-link scrub completed: all inbound references now point to `SPRINT_0503_0001_0001_ops_devops_i`; no status changes. | Project PM |
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Sprint 0506 · Ops DevOps IV (Ops & Offline 190.B)
|
# Sprint 0506 · Ops DevOps IV (Ops & Offline 190.B)
|
||||||
|
|
||||||
## Topic & Scope
|
## Topic & Scope
|
||||||
- Ops & Offline focus on DevOps phase IV: incident automation, orchestrator observability, policy CI, signing/SDK pipelines, and mirror signing.
|
- Ops & Offline focus on DevOps phase IV: incident automation, orchestrator observability, policy CI, signing/SDK pipelines, and mirror signing.
|
||||||
@@ -21,30 +21,30 @@
|
|||||||
## Delivery Tracker
|
## Delivery Tracker
|
||||||
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
|
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 1 | DEVOPS-OBS-55-001 | DONE (2025-11-25) | Depends on DEVOPS-OBS-54-001 | DevOps Guild · Ops Guild | Incident mode automation: feature flag service, burn-rate trigger, retention overrides, reset job. |
|
| 1 | DEVOPS-OBS-55-001 | DONE (2025-11-25) | Depends on DEVOPS-OBS-54-001 | DevOps Guild · Ops Guild | Incident mode automation: feature flag service, burn-rate trigger, retention overrides, reset job. |
|
||||||
| 2 | DEVOPS-ORCH-32-001 | DONE (2025-11-25) | Bootstrap orchestrator infra | DevOps Guild · Orchestrator Guild | Provision orchestrator Postgres/message bus, CI smoke deploy, dashboards, bootstrap docs. |
|
| 2 | DEVOPS-ORCH-32-001 | DONE (2025-11-25) | Bootstrap orchestrator infra | DevOps Guild · Orchestrator Guild | Provision orchestrator Postgres/message bus, CI smoke deploy, dashboards, bootstrap docs. |
|
||||||
| 3 | DEVOPS-ORCH-33-001 | DONE (2025-11-25) | Depends on 32-001 | DevOps Guild · Observability Guild | Grafana dashboards/alerts for rate limiter, backpressure, error clustering, DLQ depth. |
|
| 3 | DEVOPS-ORCH-33-001 | DONE (2025-11-25) | Depends on 32-001 | DevOps Guild · Observability Guild | Grafana dashboards/alerts for rate limiter, backpressure, error clustering, DLQ depth. |
|
||||||
| 4 | DEVOPS-ORCH-34-001 | DONE (2025-11-25) | Depends on 33-001 | DevOps Guild · Orchestrator Guild | Harden production monitoring: synthetic probes, burn-rate alerts, replay smoke, GA readiness checklist. |
|
| 4 | DEVOPS-ORCH-34-001 | DONE (2025-11-25) | Depends on 33-001 | DevOps Guild · Orchestrator Guild | Harden production monitoring: synthetic probes, burn-rate alerts, replay smoke, GA readiness checklist. |
|
||||||
| 5 | DEVOPS-POLICY-27-001 | DONE (2025-11-25) | None | DevOps Guild · DevEx/CLI Guild | Add CI stage to run `stella policy lint`. |
|
| 5 | DEVOPS-POLICY-27-001 | DONE (2025-11-25) | None | DevOps Guild · DevEx/CLI Guild | Add CI stage to run `stella policy lint`. |
|
||||||
| 6 | DEVOPS-POLICY-27-002 | DONE (2025-11-25) | Depends on 27-001 | DevOps Guild · Policy Registry Guild | Batch simulation CI job, threshold enforcement, PR markdown summary. |
|
| 6 | DEVOPS-POLICY-27-002 | DONE (2025-11-25) | Depends on 27-001 | DevOps Guild · Policy Registry Guild | Batch simulation CI job, threshold enforcement, PR markdown summary. |
|
||||||
| 7 | DEVOPS-POLICY-27-003 | DONE (2025-11-25) | Depends on 27-002 | DevOps Guild · Security Guild | Manage signing keys (OIDC + cosign), rotate keys, verify attestations. |
|
| 7 | DEVOPS-POLICY-27-003 | DONE (2025-11-25) | Depends on 27-002 | DevOps Guild · Security Guild | Manage signing keys (OIDC + cosign), rotate keys, verify attestations. |
|
||||||
| 8 | DEVOPS-POLICY-27-004 | DONE (2025-11-25) | Depends on 27-003 | DevOps Guild · Observability Guild | Dashboards/alerts for policy compile latency, simulation queue depth, approval latency, promotion outcomes. |
|
| 8 | DEVOPS-POLICY-27-004 | DONE (2025-11-25) | Depends on 27-003 | DevOps Guild · Observability Guild | Dashboards/alerts for policy compile latency, simulation queue depth, approval latency, promotion outcomes. |
|
||||||
| 9 | DEVOPS-REL-17-004 | DONE (2025-11-23) | None | DevOps Guild | Release workflow uploads `out/release/debug` and fails when symbols missing. |
|
| 9 | DEVOPS-REL-17-004 | DONE (2025-11-23) | None | DevOps Guild | Release workflow uploads `out/release/debug` and fails when symbols missing. |
|
||||||
| 10 | DEVOPS-RULES-33-001 | DONE (2025-11-25) | None | DevOps Guild · Platform Leads | Contracts & Rules anchor (gateway proxies, AOC no-merge, graph platform consolidation). |
|
| 10 | DEVOPS-RULES-33-001 | DONE (2025-11-25) | None | DevOps Guild · Platform Leads | Contracts & Rules anchor (gateway proxies, AOC no-merge, graph platform consolidation). |
|
||||||
| 11 | DEVOPS-SDK-63-001 | DONE (2025-11-25) | None | DevOps Guild · SDK Release Guild | Provision registry creds, signing keys, secure storage for SDK publishing pipelines. |
|
| 11 | DEVOPS-SDK-63-001 | DONE (2025-11-25) | None | DevOps Guild · SDK Release Guild | Provision registry creds, signing keys, secure storage for SDK publishing pipelines. |
|
||||||
| 12 | DEVOPS-SIG-26-001 | DONE (2025-11-25) | None | DevOps Guild · Signals Guild | Provision CI/CD, Helm/Compose manifests for Signals service with artifact storage + Redis. |
|
| 12 | DEVOPS-SIG-26-001 | DONE (2025-11-25) | None | DevOps Guild · Signals Guild | Provision CI/CD, Helm/Compose manifests for Signals service with artifact storage + Redis. |
|
||||||
| 13 | DEVOPS-SIG-26-002 | DONE (2025-11-25) | Depends on 26-001 | DevOps Guild · Observability Guild | Dashboards/alerts for reachability scoring latency, cache hit rates, sensor staleness. |
|
| 13 | DEVOPS-SIG-26-002 | DONE (2025-11-25) | Depends on 26-001 | DevOps Guild · Observability Guild | Dashboards/alerts for reachability scoring latency, cache hit rates, sensor staleness. |
|
||||||
| 14 | DEVOPS-TEN-47-001 | BLOCKED (2025-11-25) | Needs Authority tenancy harness | DevOps Guild | JWKS cache monitoring, signature verification regression tests, token expiration chaos tests in CI. |
|
| 14 | DEVOPS-TEN-47-001 | BLOCKED (2025-11-25) | Needs Authority tenancy harness | DevOps Guild | JWKS cache monitoring, signature verification regression tests, token expiration chaos tests in CI. |
|
||||||
| 15 | DEVOPS-TEN-48-001 | BLOCKED (2025-11-25) | Depends on 47-001 | DevOps Guild | Integration tests for RLS enforcement, tenant-prefixed object storage, audit events; lint to prevent raw SQL bypass. |
|
| 15 | DEVOPS-TEN-48-001 | BLOCKED (2025-11-25) | Depends on 47-001 | DevOps Guild | Integration tests for RLS enforcement, tenant-prefixed object storage, audit events; lint to prevent raw SQL bypass. |
|
||||||
| 16 | DEVOPS-CI-110-001 | DONE (2025-11-25) | None | DevOps Guild · Concelier Guild · Excititor Guild | CI helper + TRX slices at `ops/devops/ci-110-runner/`; warm restore + health smokes. |
|
| 16 | DEVOPS-CI-110-001 | DONE (2025-11-25) | None | DevOps Guild · Concelier Guild · Excititor Guild | CI helper + TRX slices at `ops/devops/ci-110-runner/`; warm restore + health smokes. |
|
||||||
| 17 | MIRROR-CRT-56-CI-001 | DONE (2025-11-25) | None | Mirror Creator Guild · DevOps Guild | Move `make-thin-v1.sh` into CI assembler, enforce DSSE/TUF/time-anchor, publish milestone hashes. |
|
| 17 | MIRROR-CRT-56-CI-001 | DONE (2025-11-25) | None | Mirror Creator Guild · DevOps Guild | Move `make-thin-v1.sh` into CI assembler, enforce DSSE/TUF/time-anchor, publish milestone hashes. |
|
||||||
| 18 | MIRROR-CRT-56-002 | DONE (2025-11-25) | Depends on 56-CI-001 | Mirror Creator Guild · Security Guild | Release signing for thin bundle v1 using `MIRROR_SIGN_KEY_B64`; run `.gitea/workflows/mirror-sign.yml`. |
|
| 18 | MIRROR-CRT-56-002 | DONE (2025-11-25) | Depends on 56-CI-001 | Mirror Creator Guild · Security Guild | Release signing for thin bundle v1 using `MIRROR_SIGN_KEY_B64`; run `.gitea/workflows/mirror-sign.yml`. |
|
||||||
| 19 | MIRROR-CRT-57-001/002 | BLOCKED | Wait on 56-002 + AIRGAP-TIME-57-001 | Mirror Creator Guild · AirGap Time Guild | OCI/time-anchor signing follow-ons. |
|
| 19 | MIRROR-CRT-57-001/002 | BLOCKED | Wait on 56-002 + AIRGAP-TIME-57-001 | Mirror Creator Guild · AirGap Time Guild | OCI/time-anchor signing follow-ons. |
|
||||||
| 20 | MIRROR-CRT-58-001/002 | DOING (dev) | Depends on 56-002 | Mirror Creator · CLI · Exporter Guilds | CLI/Export signing follow-ons; dev Export Center scheduling helper added, production signing still awaits `MIRROR_SIGN_KEY_B64`. |
|
| 20 | MIRROR-CRT-58-001/002 | DONE (dev) | Depends on 56-002 | Mirror Creator · CLI · Exporter Guilds | CLI/Export signing follow-ons delivered in dev mode (Export Center scheduling helper + CI dev-key fallback); production signing still awaits `MIRROR_SIGN_KEY_B64`. |
|
||||||
| 21 | EXPORT-OBS-51-001 / 54-001 / AIRGAP-TIME-57-001 / CLI-AIRGAP-56-001 / PROV-OBS-53-001 | BLOCKED | Need signed thin bundle + time anchors | Exporter · AirGap Time · CLI Guild | Export/airgap provenance chain work. |
|
| 21 | EXPORT-OBS-51-001 / 54-001 / AIRGAP-TIME-57-001 / CLI-AIRGAP-56-001 / PROV-OBS-53-001 | BLOCKED | Need signed thin bundle + time anchors | Exporter · AirGap Time · CLI Guild | Export/airgap provenance chain work. |
|
||||||
| 22 | DEVOPS-LEDGER-29-009-REL | BLOCKED (2025-11-25) | Needs LEDGER-29-009 dev outputs | DevOps Guild · Findings Ledger Guild | Release/offline-kit packaging for ledger manifests/backups. |
|
| 22 | DEVOPS-LEDGER-29-009-REL | BLOCKED (2025-11-25) | Needs LEDGER-29-009 dev outputs | DevOps Guild · Findings Ledger Guild | Release/offline-kit packaging for ledger manifests/backups. |
|
||||||
| 23 | DEVOPS-LEDGER-TEN-48-001-REL | BLOCKED (2025-11-25) | Needs ledger tenant partition work | DevOps Guild · Findings Ledger Guild | Apply RLS/partition migrations in release pipelines; publish manifests/offline-kit artefacts. |
|
| 23 | DEVOPS-LEDGER-TEN-48-001-REL | BLOCKED (2025-11-25) | Needs ledger tenant partition work | DevOps Guild · Findings Ledger Guild | Apply RLS/partition migrations in release pipelines; publish manifests/offline-kit artefacts. |
|
||||||
| 24 | DEVOPS-SCANNER-JAVA-21-011-REL | BLOCKED (2025-11-25) | Needs SCANNER-ANALYZERS-JAVA-21-011 outputs | DevOps Guild · Java Analyzer Guild | Package/sign Java analyzer plug-in for release/offline kits. |
|
| 24 | DEVOPS-SCANNER-JAVA-21-011-REL | BLOCKED (2025-11-25) | Needs SCANNER-ANALYZERS-JAVA-21-011 outputs | DevOps Guild · Java Analyzer Guild | Package/sign Java analyzer plug-in for release/offline kits. |
|
||||||
|
|
||||||
## Execution Log
|
## Execution Log
|
||||||
| Date (UTC) | Update | Owner |
|
| Date (UTC) | Update | Owner |
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
| 2025-12-06 | Header normalised to standard template; no content/status changes. | Project Mgmt |
|
| 2025-12-06 | Header normalised to standard template; no content/status changes. | Project Mgmt |
|
||||||
| 2025-12-04 | Renamed from `SPRINT_506_ops_devops_iv.md` to template-compliant `SPRINT_0506_0001_0001_ops_devops_iv.md`; no status changes. | Project PM |
|
| 2025-12-04 | Renamed from `SPRINT_506_ops_devops_iv.md` to template-compliant `SPRINT_0506_0001_0001_ops_devops_iv.md`; no status changes. | Project PM |
|
||||||
| 2025-12-03 | Normalised sprint file to standard template; preserved all tasks/logs; no status changes. | Planning |
|
| 2025-12-03 | Normalised sprint file to standard template; preserved all tasks/logs; no status changes. | Planning |
|
||||||
|
| 2025-12-07 | MIRROR-CRT-58-001/002 closed in dev: Export Center scheduling helper added; CI dev-key fallback wired in `.gitea/workflows/mirror-sign.yml`. Production signing still requires `MIRROR_SIGN_KEY_B64`. | Project Mgmt |
|
||||||
| 2025-12-07 | MIRROR-CRT-58-002 progressed: added Export Center scheduling helper (`src/Mirror/StellaOps.Mirror.Creator/schedule-export-center-run.sh`) for dev scheduling/audit; production signing still waiting on `MIRROR_SIGN_KEY_B64`. | Implementer |
|
| 2025-12-07 | MIRROR-CRT-58-002 progressed: added Export Center scheduling helper (`src/Mirror/StellaOps.Mirror.Creator/schedule-export-center-run.sh`) for dev scheduling/audit; production signing still waiting on `MIRROR_SIGN_KEY_B64`. | Implementer |
|
||||||
| 2025-11-25 | DEVOPS-CI-110-001 runner published at `ops/devops/ci-110-runner/`; initial TRX slices stored under `ops/devops/artifacts/ci-110/20251125T030557Z/`. | DevOps |
|
| 2025-11-25 | DEVOPS-CI-110-001 runner published at `ops/devops/ci-110-runner/`; initial TRX slices stored under `ops/devops/artifacts/ci-110/20251125T030557Z/`. | DevOps |
|
||||||
| 2025-11-25 | MIRROR-CRT-56-CI-001 completed: CI signing script emits milestone hash summary, enforces DSSE/TUF/time-anchor steps, uploads `milestone.json` via `mirror-sign.yml`. | DevOps |
|
| 2025-11-25 | MIRROR-CRT-56-CI-001 completed: CI signing script emits milestone hash summary, enforces DSSE/TUF/time-anchor steps, uploads `milestone.json` via `mirror-sign.yml`. | DevOps |
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
- Cosign key management supports keyless; offline/air-gap paths require mirrored registry + secrets provided to `sbom_attest.sh`.
|
- Cosign key management supports keyless; offline/air-gap paths require mirrored registry + secrets provided to `sbom_attest.sh`.
|
||||||
- Tenant chaos drill requires iptables/root; run only on isolated agents; monitor JWKS cache TTL to avoid auth outages.
|
- Tenant chaos drill requires iptables/root; run only on isolated agents; monitor JWKS cache TTL to avoid auth outages.
|
||||||
- Surface.Env: ZASTAVA_* fallback to SCANNER_* in Helm/Compose; keep docs aligned if prefixes/fields change.
|
- Surface.Env: ZASTAVA_* fallback to SCANNER_* in Helm/Compose; keep docs aligned if prefixes/fields change.
|
||||||
- Surface.Secrets: provisioning playbook published; ensure Helm/Compose env stays in sync; offline kit bundles encrypted secrets—unpack path must match `*_SURFACE_SECRETS_ROOT`.
|
- Surface.Secrets: provisioning playbook published; ensure Helm/Compose env stays in sync; offline kit bundles encrypted secrets—unpack path must match `*_SURFACE_SECRETS_ROOT`.
|
||||||
|
|
||||||
## Next Checkpoints
|
## Next Checkpoints
|
||||||
| Date (UTC) | Session / Owner | Target outcome | Fallback / Escalation |
|
| Date (UTC) | Session / Owner | Target outcome | Fallback / Escalation |
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
| 6 | KCMVP-01 | DONE (2025-12-07) | None | Security · Crypto | Provide KCMVP hash-only baseline (SHA-256) with labeling; add tests and profile docs. |
|
| 6 | KCMVP-01 | DONE (2025-12-07) | None | Security · Crypto | Provide KCMVP hash-only baseline (SHA-256) with labeling; add tests and profile docs. |
|
||||||
| 7 | KCMVP-02 | BLOCKED (2025-12-06) | Licensed module | Security · Crypto | Add ARIA/SEED/KCDSA provider once certified toolchain available. |
|
| 7 | KCMVP-02 | BLOCKED (2025-12-06) | Licensed module | Security · Crypto | Add ARIA/SEED/KCDSA provider once certified toolchain available. |
|
||||||
| 8 | PQ-IMPL-01 | DONE (2025-12-07) | Registry mapping (R3) to resolve | Crypto · Scanner | Implement `pq-dilithium3` and `pq-falcon512` providers via liboqs/oqs-provider; vendor libs for offline; add deterministic vectors. |
|
| 8 | PQ-IMPL-01 | DONE (2025-12-07) | Registry mapping (R3) to resolve | Crypto · Scanner | Implement `pq-dilithium3` and `pq-falcon512` providers via liboqs/oqs-provider; vendor libs for offline; add deterministic vectors. |
|
||||||
| 9 | PQ-IMPL-02 | DOING (2025-12-07) | After #8 | Scanner · Attestor · Policy | Wire DSSE signing overrides, dual-sign toggles, deterministic regression tests across providers (Scanner/Attestor/Policy). |
|
| 9 | PQ-IMPL-02 | DONE (2025-12-07) | After #8 | Scanner · Attestor · Policy | Wire DSSE signing overrides, dual-sign toggles, deterministic regression tests across providers (Scanner/Attestor/Policy). |
|
||||||
| 10 | ROOTPACK-INTL-01 | DOING (2025-12-07) | After baseline tasks (1,4,6,8) | Ops · Docs | Build rootpack variants (us-fips baseline, eu baseline, korea hash-only, PQ addenda) with signed manifests/tests; clearly label certification gaps. |
|
| 10 | ROOTPACK-INTL-01 | DOING (2025-12-07) | After baseline tasks (1,4,6,8) | Ops · Docs | Build rootpack variants (us-fips baseline, eu baseline, korea hash-only, PQ addenda) with signed manifests/tests; clearly label certification gaps. |
|
||||||
|
|
||||||
## Execution Log
|
## Execution Log
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
| 2025-12-07 | Drafted regional rootpacks (`etc/rootpack/us-fips`, `etc/rootpack/eu`, `etc/rootpack/kr`) including PQ soft provider; registry DI registers new providers. | Implementer |
|
| 2025-12-07 | Drafted regional rootpacks (`etc/rootpack/us-fips`, `etc/rootpack/eu`, `etc/rootpack/kr`) including PQ soft provider; registry DI registers new providers. | Implementer |
|
||||||
| 2025-12-07 | Added deterministic PQ test vectors (fixed keys/signatures) in `StellaOps.Cryptography.Tests`; PQ-IMPL-01 marked DONE. | Implementer |
|
| 2025-12-07 | Added deterministic PQ test vectors (fixed keys/signatures) in `StellaOps.Cryptography.Tests`; PQ-IMPL-01 marked DONE. | Implementer |
|
||||||
| 2025-12-07 | Wired Signer DSSE dual-sign (secondary PQ/SM allowed via options), fixed DI to provide ICryptoHmac, and adjusted SM2 test seeding; Signer test suite passing. Set PQ-IMPL-02 to DOING. | Implementer |
|
| 2025-12-07 | Wired Signer DSSE dual-sign (secondary PQ/SM allowed via options), fixed DI to provide ICryptoHmac, and adjusted SM2 test seeding; Signer test suite passing. Set PQ-IMPL-02 to DOING. | Implementer |
|
||||||
|
| 2025-12-07 | Added Attestor dual-sign regression (min 2 signatures) and fixed SM2 registry tests; Attestor test suite passing. PQ-IMPL-02 marked DONE. | Implementer |
|
||||||
|
|
||||||
## Decisions & Risks
|
## Decisions & Risks
|
||||||
- FIPS validation lead time may slip; interim non-certified baseline acceptable but must be clearly labeled until CMVP module lands (task 3).
|
- FIPS validation lead time may slip; interim non-certified baseline acceptable but must be clearly labeled until CMVP module lands (task 3).
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Sprint 3407-1-2 · Concelier Postgres JSON Cutover
|
||||||
|
|
||||||
|
## Topic & Scope
|
||||||
|
- Build a Postgres-native JSON abstraction for Concelier documents/DTO/state/aliases/flags and eliminate all Mongo/MongoCompat/BSON shims.
|
||||||
|
- Migrate connectors, exporters, and tests from MongoDB.Driver/Mongo2Go to the new abstraction; ensure deterministic JSON handling and Postgres-only green build.
|
||||||
|
- Prepare removal of `StellaOps.Concelier.Storage.Mongo` and compat code paths while preserving LNM/AOC contracts and offline posture.
|
||||||
|
- **Working directory:** `src/Concelier/**` (WebService, __Libraries, __Tests, Storage.Postgres); delete remaining Mongo artefacts once migration is green.
|
||||||
|
|
||||||
|
## Dependencies & Concurrency
|
||||||
|
- Upstream: Sprint `SPRINT_3407_0001_0001_postgres_cleanup.md` Wave A decisions (PG-T7.1.*) remain in force; this sprint delivers PG-T7.1.5c/d readiness.
|
||||||
|
- Must stay compatible with existing Postgres document/state tables and Concelier Merge constraints; no cross-module changes expected.
|
||||||
|
- Run after package cache is available (Microsoft.Extensions.* 10.0.0).
|
||||||
|
|
||||||
|
## Documentation Prerequisites
|
||||||
|
- `docs/README.md`
|
||||||
|
- `docs/07_HIGH_LEVEL_ARCHITECTURE.md`
|
||||||
|
- `docs/modules/platform/architecture-overview.md`
|
||||||
|
- `docs/modules/concelier/architecture.md`
|
||||||
|
- `docs/modules/concelier/link-not-merge-schema.md`
|
||||||
|
- `src/Concelier/AGENTS.md`
|
||||||
|
|
||||||
|
## Delivery Tracker
|
||||||
|
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 1 | PG-T7.1.5c-01 | DOING | Align JSON abstraction with LNM schema; confirm Postgres storage layout | Concelier · Backend | Define Postgres JSON stores (document, DTO, state, alias, flag) and DI registrations; document JSON contract (hashing, ordering, timestamps). |
|
||||||
|
| 2 | PG-T7.1.5c-02 | TODO | Task 1 | Concelier · Backend | Implement JSON stores in Storage.Postgres (payload/metadata/headers as JSON), replace MongoCompat/BSON types; add migrations if new columns are needed. |
|
||||||
|
| 3 | PG-T7.1.5c-03 | TODO | Task 2 | Concelier · Backend | Refactor connectors/exporters to the JSON stores (remove MongoDB.Driver/Mongo2Go, BSON cursors); update DTO parsing to System.Text.Json. |
|
||||||
|
| 4 | PG-T7.1.5c-04 | TODO | Task 2 | Concelier · QA | Replace Mongo test harnesses (Mongo2Go, ConnectorTestHarness, importer parity) with Postgres/JSON fixtures; fix WebService tests. |
|
||||||
|
| 5 | PG-T7.1.5c-05 | TODO | Tasks 2-4 | Concelier · Backend | Remove MongoCompat/BSON stubs and `StellaOps.Concelier.Storage.Mongo` references from solution/csproj; clean package refs/usings. |
|
||||||
|
| 6 | PG-T7.1.5c-06 | TODO | Tasks 3-5 | Concelier · QA | Run full Concelier solution build/tests on Postgres-only path; collect evidence (logs, artifact paths) and mark PG-T7.1.5c ready for deletion of Mongo artefacts. |
|
||||||
|
|
||||||
|
## Execution Log
|
||||||
|
| Date (UTC) | Update | Owner |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 2025-12-07 | Sprint created to plan Postgres JSON cutover and Mongo removal for Concelier. | Project Mgmt |
|
||||||
|
| 2025-12-07 | PG-T7.1.5c-01 set to DOING; starting JSON store contract design and mapping to existing Postgres tables. | Concelier Guild |
|
||||||
|
|
||||||
|
## Decisions & Risks
|
||||||
|
- Need confirmation that JSON storage semantics (hashing, ordering, timestamps) match existing LNM expectations; deviations require doc updates and approvals.
|
||||||
|
- Risk: hidden MongoDB.Driver references in less-used connectors/tests could extend migration time; mitigate by inventory + phased PRs.
|
||||||
|
- Risk: Postgres schema changes may be needed (JSON columns, indexes); must stay deterministic and air-gap friendly.
|
||||||
|
|
||||||
|
## Next Checkpoints
|
||||||
|
- 2025-12-08: Review JSON abstraction design and storage schema; approve migrations and DI changes.
|
||||||
|
- 2025-12-10: Demo connector/test migration progress; decide on Mongo artefact deletion window.
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
# Blocked Task Dependency Tree (as of 2025-11-30)
|
# Blocked Task Dependency Tree (as of 2025-12-07)
|
||||||
|
|
||||||
|
Updated 2025-12-07: FEEDCONN-ICSCISA-02-012/KISA-02-008 unblocked (ICS/KISA SOP v0.2); tracked in SPRINT_0113 row 18 and SPRINT_0503 feed ops tasks.
|
||||||
|
|
||||||
- Concelier ingestion & Link-Not-Merge
|
- Concelier ingestion & Link-Not-Merge
|
||||||
- MIRROR-CRT-56-001 (DONE; thin bundle v1 sample + hashes published)
|
- MIRROR-CRT-56-001 (DONE; thin bundle v1 sample + hashes published)
|
||||||
@@ -13,7 +15,6 @@
|
|||||||
- CLI-AIRGAP-56-001 (DEV-UNBLOCKED: dev bundles available; release promotion depends on DevOps secret import + 58-001 CLI path)
|
- CLI-AIRGAP-56-001 (DEV-UNBLOCKED: dev bundles available; release promotion depends on DevOps secret import + 58-001 CLI path)
|
||||||
- CONCELIER-AIRGAP-56-001..58-001 <- PREP-ART-56-001, PREP-EVIDENCE-BDL-01
|
- CONCELIER-AIRGAP-56-001..58-001 <- PREP-ART-56-001, PREP-EVIDENCE-BDL-01
|
||||||
- CONCELIER-CONSOLE-23-001..003 <- PREP-CONSOLE-FIXTURES-29; PREP-EVIDENCE-BDL-01
|
- CONCELIER-CONSOLE-23-001..003 <- PREP-CONSOLE-FIXTURES-29; PREP-EVIDENCE-BDL-01
|
||||||
- FEEDCONN-ICSCISA-02-012 / KISA-02-008 <- PREP-FEEDCONN-ICS-KISA-PLAN
|
|
||||||
|
|
||||||
- SBOM Service (Link-Not-Merge consumers)
|
- SBOM Service (Link-Not-Merge consumers)
|
||||||
- SBOM-SERVICE-21-001 (projection read API) — DONE (2025-11-23): WAF aligned with fixtures + in-memory repo fallback; `ProjectionEndpointTests` pass.
|
- SBOM-SERVICE-21-001 (projection read API) — DONE (2025-11-23): WAF aligned with fixtures + in-memory repo fallback; `ProjectionEndpointTests` pass.
|
||||||
@@ -49,8 +50,8 @@
|
|||||||
- CONCELIER-WEB-OBS-50-001 ✅ (telemetry core adopted 2025-11-07) -> 51-001 ✅ (health endpoint shipped 2025-11-23) -> 52-001
|
- CONCELIER-WEB-OBS-50-001 ✅ (telemetry core adopted 2025-11-07) -> 51-001 ✅ (health endpoint shipped 2025-11-23) -> 52-001
|
||||||
|
|
||||||
- Advisory AI docs & packaging
|
- Advisory AI docs & packaging
|
||||||
- AIAI-PACKAGING-31-002 & AIAI-DOCS-31-001 <- SBOM feeds + CLI/Policy artefacts
|
- AIAI-PACKAGING-31-002 & AIAI-DOCS-31-001 <- SBOM feeds + DEVOPS-AIAI-31-001 (CLI-VULN-29-001/CLI-VEX-30-001 landed via Sprint 0205 on 2025-12-06; POLICY-ENGINE-31-001 delivered 2025-11-23)
|
||||||
- DOCS-AIAI-31-005 -> 31-006 -> 31-008 -> 31-009 (all gated by DOCS-UNBLOCK-CLI-KNOBS-301 <- CLI-VULN-29-001; CLI-VEX-30-001; POLICY-ENGINE-31-001)
|
- DOCS-AIAI-31-005 -> 31-006 -> 31-008 -> 31-009 (DOCS-UNBLOCK-CLI-KNOBS-301 satisfied: CLI-VULN-29-001/CLI-VEX-30-001 delivered 2025-12-06; POLICY-ENGINE-31-001 delivered 2025-11-23; remaining gate: DEVOPS-AIAI-31-001 rollout)
|
||||||
|
|
||||||
- Policy Engine (core) chain
|
- Policy Engine (core) chain
|
||||||
- POLICY-ENGINE-29-003 implemented (path-scope streaming endpoint live); downstream tasks 29-004+ remain open but unblocked.
|
- POLICY-ENGINE-29-003 implemented (path-scope streaming endpoint live); downstream tasks 29-004+ remain open but unblocked.
|
||||||
@@ -141,7 +142,7 @@
|
|||||||
- PROV-OBS-53-002 ✅ -> PROV-OBS-53-003 ✅
|
- PROV-OBS-53-002 ✅ -> PROV-OBS-53-003 ✅
|
||||||
|
|
||||||
- CLI/Advisory AI handoff
|
- CLI/Advisory AI handoff
|
||||||
- SBOM-AIAI-31-003 <- CLI-VULN-29-001; CLI-VEX-30-001
|
- SBOM-AIAI-31-003 (CLI-VULN-29-001/CLI-VEX-30-001 delivered 2025-12-06; completed in Sprint 0110; keep DEVOPS-AIAI-31-001 packaging in view)
|
||||||
- DOCS-AIAI-31-005/006/008/009 <- CLI-VULN-29-001; CLI-VEX-30-001; POLICY-ENGINE-31-001; DEVOPS-AIAI-31-001
|
- DOCS-AIAI-31-005/006/008/009 (CLI-VULN-29-001/CLI-VEX-30-001 delivered 2025-12-06; POLICY-ENGINE-31-001 delivered 2025-11-23; remaining dependency: DEVOPS-AIAI-31-001 for ops rollout)
|
||||||
|
|
||||||
Note: POLICY-20-001 is defined and tracked in `docs/implplan/SPRINT_0114_0001_0003_concelier_iii.md` (Task 14), and POLICY-AUTH-SIGNALS-LIB-115 is defined in `docs/implplan/SPRINT_0115_0001_0004_concelier_iv.md` (Task 0); both scopes match the expectations captured here.
|
Note: POLICY-20-001 is defined and tracked in `docs/implplan/SPRINT_0114_0001_0003_concelier_iii.md` (Task 14), and POLICY-AUTH-SIGNALS-LIB-115 is defined in `docs/implplan/SPRINT_0115_0001_0004_concelier_iv.md` (Task 0); both scopes match the expectations captured here.
|
||||||
|
|||||||
@@ -372,13 +372,13 @@
|
|||||||
| CLI-SIG-26-002 | TODO | | SPRINT_0204_0001_0004_cli_iv | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Extend `stella policy simulate` with reachability override flags (`--reachability-state`, `--reachability-score`). Dependencies: CLI-SIG-26-001. | CLI-SIG-26-001 | CLCI0108 |
|
| CLI-SIG-26-002 | TODO | | SPRINT_0204_0001_0004_cli_iv | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Extend `stella policy simulate` with reachability override flags (`--reachability-state`, `--reachability-score`). Dependencies: CLI-SIG-26-001. | CLI-SIG-26-001 | CLCI0108 |
|
||||||
| CLI-TEN-47-001 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella login`, `whoami`, `tenants list`, persistent profiles, secure token storage, and `--tenant` override with validation. | — | CLCI0108 |
|
| CLI-TEN-47-001 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella login`, `whoami`, `tenants list`, persistent profiles, secure token storage, and `--tenant` override with validation. | — | CLCI0108 |
|
||||||
| CLI-TEN-49-001 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add service account token minting, delegation (`stella token delegate`), impersonation banner, and audit-friendly logging. Dependencies: CLI-TEN-47-001. | CLI-TEN-47-001 | CLCI0108 |
|
| CLI-TEN-49-001 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add service account token minting, delegation (`stella token delegate`), impersonation banner, and audit-friendly logging. Dependencies: CLI-TEN-47-001. | CLI-TEN-47-001 | CLCI0108 |
|
||||||
| CLI-VEX-30-001 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex consensus list` with filters, paging, policy selection, `--json/--csv`. | PLVL0102 completion | CLCI0107 |
|
| CLI-VEX-30-001 | DONE (2025-12-06) | 2025-12-06 | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex consensus list` with filters, paging, policy selection, `--json/--csv`. | PLVL0102 completion | CLCI0107 |
|
||||||
| CLI-VEX-30-002 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex consensus show` displaying quorum, evidence, rationale, signature status. Dependencies: CLI-VEX-30-001. | CLI-VEX-30-001 | CLCI0107 |
|
| CLI-VEX-30-002 | DONE (2025-12-06) | 2025-12-06 | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex consensus show` displaying quorum, evidence, rationale, signature status. Dependencies: CLI-VEX-30-001. | CLI-VEX-30-001 | CLCI0107 |
|
||||||
| CLI-VEX-30-003 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex simulate` for trust/threshold overrides with JSON diff output. Dependencies: CLI-VEX-30-002. | CLI-VEX-30-002 | CLCI0107 |
|
| CLI-VEX-30-003 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex simulate` for trust/threshold overrides with JSON diff output. Dependencies: CLI-VEX-30-002. | CLI-VEX-30-002 | CLCI0107 |
|
||||||
| CLI-VEX-30-004 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex export` for consensus NDJSON bundles with signature verification helper. Dependencies: CLI-VEX-30-003. | CLI-VEX-30-003 | CLCI0107 |
|
| CLI-VEX-30-004 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex export` for consensus NDJSON bundles with signature verification helper. Dependencies: CLI-VEX-30-003. | CLI-VEX-30-003 | CLCI0107 |
|
||||||
| CLI-VEX-401-011 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | CLI Guild | `src/Cli/StellaOps.Cli`, `docs/modules/cli/architecture.md`, `docs/benchmarks/vex-evidence-playbook.md` | Add `stella decision export | Reachability API exposure | CLCI0107 |
|
| CLI-VEX-401-011 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | CLI Guild | `src/Cli/StellaOps.Cli`, `docs/modules/cli/architecture.md`, `docs/benchmarks/vex-evidence-playbook.md` | Add `stella decision export | Reachability API exposure | CLCI0107 |
|
||||||
| CLI-VULN-29-001 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vuln list` with grouping, paging, filters, `--json/--csv`, and policy selection. | — | CLCI0107 |
|
| CLI-VULN-29-001 | DONE (2025-12-06) | 2025-12-06 | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vuln list` with grouping, paging, filters, `--json/--csv`, and policy selection. | — | CLCI0107 |
|
||||||
| CLI-VULN-29-002 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vuln show` displaying evidence, policy rationale, paths, ledger summary; support `--json` for automation. Dependencies: CLI-VULN-29-001. | CLI-VULN-29-001 | CLCI0107 |
|
| CLI-VULN-29-002 | DONE (2025-12-06) | 2025-12-06 | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vuln show` displaying evidence, policy rationale, paths, ledger summary; support `--json` for automation. Dependencies: CLI-VULN-29-001. | CLI-VULN-29-001 | CLCI0107 |
|
||||||
| CLI-VULN-29-003 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add workflow commands (`assign`, `comment`, `accept-risk`, `verify-fix`, `target-fix`, `reopen`) with filter selection (`--filter`) and idempotent retries. Dependencies: CLI-VULN-29-002. | CLI-VULN-29-002 | CLCI0107 |
|
| CLI-VULN-29-003 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add workflow commands (`assign`, `comment`, `accept-risk`, `verify-fix`, `target-fix`, `reopen`) with filter selection (`--filter`) and idempotent retries. Dependencies: CLI-VULN-29-002. | CLI-VULN-29-002 | CLCI0107 |
|
||||||
| CLI-VULN-29-004 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vuln simulate` producing delta summaries and optional Markdown report for CI. Dependencies: CLI-VULN-29-003. | CLI-VULN-29-003 | CLCI0107 |
|
| CLI-VULN-29-004 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vuln simulate` producing delta summaries and optional Markdown report for CI. Dependencies: CLI-VULN-29-003. | CLI-VULN-29-003 | CLCI0107 |
|
||||||
| CLI-VULN-29-005 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add `stella vuln export` and `stella vuln bundle verify` commands to trigger/download evidence bundles and verify signatures. Dependencies: CLI-VULN-29-004. | CLI-VULN-29-004 | CLCI0107 |
|
| CLI-VULN-29-005 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add `stella vuln export` and `stella vuln bundle verify` commands to trigger/download evidence bundles and verify signatures. Dependencies: CLI-VULN-29-004. | CLI-VULN-29-004 | CLCI0107 |
|
||||||
@@ -1196,7 +1196,7 @@
|
|||||||
| MIRROR-CRT-57-001 | TODO | | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · AirGap Time Guild | | OCI/time-anchor workstreams blocked pending assembler + time contract. | MIRROR-CRT-56-001; AIRGAP-TIME-CONTRACT-1501; AIRGAP-TIME-57-001 | ATMI0101 |
|
| MIRROR-CRT-57-001 | TODO | | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · AirGap Time Guild | | OCI/time-anchor workstreams blocked pending assembler + time contract. | MIRROR-CRT-56-001; AIRGAP-TIME-CONTRACT-1501; AIRGAP-TIME-57-001 | ATMI0101 |
|
||||||
| MIRROR-CRT-57-002 | TODO | | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · AirGap Time Guild | | MIRROR-CRT-56-001; AIRGAP-TIME-CONTRACT-1501; AIRGAP-TIME-57-001 | MIRROR-CRT-56-001; AIRGAP-TIME-CONTRACT-1501; AIRGAP-TIME-57-001 | ATMI0101 |
|
| MIRROR-CRT-57-002 | TODO | | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · AirGap Time Guild | | MIRROR-CRT-56-001; AIRGAP-TIME-CONTRACT-1501; AIRGAP-TIME-57-001 | MIRROR-CRT-56-001; AIRGAP-TIME-CONTRACT-1501; AIRGAP-TIME-57-001 | ATMI0101 |
|
||||||
| MIRROR-CRT-58-001 | TODO | | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · CLI Guild · Exporter Guild | | CLI + Export automation depends on assembler and DSSE/TUF track. | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001 | ATMI0101 |
|
| MIRROR-CRT-58-001 | TODO | | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · CLI Guild · Exporter Guild | | CLI + Export automation depends on assembler and DSSE/TUF track. | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001 | ATMI0101 |
|
||||||
| MIRROR-CRT-58-002 | DOING | 2025-12-07 | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · CLI Guild · Exporter Guild | | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001 | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001 | ATMI0101 |
|
| MIRROR-CRT-58-002 | DOING | 2025-12-07 | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · CLI Guild · Exporter Guild | src/Mirror/StellaOps.Mirror.Creator | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001 | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001; dev key: tools/cosign/cosign.dev.key (pw stellaops-dev); prod: MIRROR_SIGN_KEY_B64 | ATMI0101 |
|
||||||
| MTLS-11-002 | DONE | 2025-11-08 | SPRINT_100_identity_signing | Authority Core & Security Guild | src/Authority/StellaOps.Authority | Refresh grants enforce original client cert, tokens persist `x5t#S256` metadata, docs updated. | AUTH-DPOP-11-001 | AUIN0102 |
|
| MTLS-11-002 | DONE | 2025-11-08 | SPRINT_100_identity_signing | Authority Core & Security Guild | src/Authority/StellaOps.Authority | Refresh grants enforce original client cert, tokens persist `x5t#S256` metadata, docs updated. | AUTH-DPOP-11-001 | AUIN0102 |
|
||||||
| NATIVE-401-015 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Scanner Worker Guild | `src/Scanner/__Libraries/StellaOps.Scanner.Symbols.Native`, `src/Scanner/__Libraries/StellaOps.Scanner.CallGraph.Native` | Bootstrap Symbols.Native + CallGraph.Native scaffolding and coverage fixtures. | Needs replay requirements from DORR0101 | SCNA0101 |
|
| NATIVE-401-015 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Scanner Worker Guild | `src/Scanner/__Libraries/StellaOps.Scanner.Symbols.Native`, `src/Scanner/__Libraries/StellaOps.Scanner.CallGraph.Native` | Bootstrap Symbols.Native + CallGraph.Native scaffolding and coverage fixtures. | Needs replay requirements from DORR0101 | SCNA0101 |
|
||||||
| NOTIFY-38-001 | TODO | | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild | src/Web/StellaOps.Web | Route approval/rule APIs through Web gateway with tenant scopes. | Wait for NOTY0103 approval payload schema | NOWB0101 |
|
| NOTIFY-38-001 | TODO | | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild | src/Web/StellaOps.Web | Route approval/rule APIs through Web gateway with tenant scopes. | Wait for NOTY0103 approval payload schema | NOWB0101 |
|
||||||
@@ -2586,13 +2586,13 @@
|
|||||||
| CLI-SIG-26-002 | TODO | | SPRINT_0204_0001_0004_cli_iv | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Extend `stella policy simulate` with reachability override flags (`--reachability-state`, `--reachability-score`). Dependencies: CLI-SIG-26-001. | CLI-SIG-26-001 | CLCI0108 |
|
| CLI-SIG-26-002 | TODO | | SPRINT_0204_0001_0004_cli_iv | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Extend `stella policy simulate` with reachability override flags (`--reachability-state`, `--reachability-score`). Dependencies: CLI-SIG-26-001. | CLI-SIG-26-001 | CLCI0108 |
|
||||||
| CLI-TEN-47-001 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella login`, `whoami`, `tenants list`, persistent profiles, secure token storage, and `--tenant` override with validation. | — | CLCI0108 |
|
| CLI-TEN-47-001 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella login`, `whoami`, `tenants list`, persistent profiles, secure token storage, and `--tenant` override with validation. | — | CLCI0108 |
|
||||||
| CLI-TEN-49-001 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add service account token minting, delegation (`stella token delegate`), impersonation banner, and audit-friendly logging. Dependencies: CLI-TEN-47-001. | CLI-TEN-47-001 | CLCI0108 |
|
| CLI-TEN-49-001 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add service account token minting, delegation (`stella token delegate`), impersonation banner, and audit-friendly logging. Dependencies: CLI-TEN-47-001. | CLI-TEN-47-001 | CLCI0108 |
|
||||||
| CLI-VEX-30-001 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex consensus list` with filters, paging, policy selection, `--json/--csv`. | PLVL0102 completion | CLCI0107 |
|
| CLI-VEX-30-001 | DONE (2025-12-06) | 2025-12-06 | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex consensus list` with filters, paging, policy selection, `--json/--csv`. | PLVL0102 completion | CLCI0107 |
|
||||||
| CLI-VEX-30-002 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex consensus show` displaying quorum, evidence, rationale, signature status. Dependencies: CLI-VEX-30-001. | CLI-VEX-30-001 | CLCI0107 |
|
| CLI-VEX-30-002 | DONE (2025-12-06) | 2025-12-06 | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex consensus show` displaying quorum, evidence, rationale, signature status. Dependencies: CLI-VEX-30-001. | CLI-VEX-30-001 | CLCI0107 |
|
||||||
| CLI-VEX-30-003 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex simulate` for trust/threshold overrides with JSON diff output. Dependencies: CLI-VEX-30-002. | CLI-VEX-30-002 | CLCI0107 |
|
| CLI-VEX-30-003 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex simulate` for trust/threshold overrides with JSON diff output. Dependencies: CLI-VEX-30-002. | CLI-VEX-30-002 | CLCI0107 |
|
||||||
| CLI-VEX-30-004 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex export` for consensus NDJSON bundles with signature verification helper. Dependencies: CLI-VEX-30-003. | CLI-VEX-30-003 | CLCI0107 |
|
| CLI-VEX-30-004 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vex export` for consensus NDJSON bundles with signature verification helper. Dependencies: CLI-VEX-30-003. | CLI-VEX-30-003 | CLCI0107 |
|
||||||
| CLI-VEX-401-011 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | CLI Guild | `src/Cli/StellaOps.Cli`, `docs/modules/cli/architecture.md`, `docs/benchmarks/vex-evidence-playbook.md` | Add `stella decision export | Reachability API exposure | CLCI0107 |
|
| CLI-VEX-401-011 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | CLI Guild | `src/Cli/StellaOps.Cli`, `docs/modules/cli/architecture.md`, `docs/benchmarks/vex-evidence-playbook.md` | Add `stella decision export | Reachability API exposure | CLCI0107 |
|
||||||
| CLI-VULN-29-001 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vuln list` with grouping, paging, filters, `--json/--csv`, and policy selection. | — | CLCI0107 |
|
| CLI-VULN-29-001 | DONE (2025-12-06) | 2025-12-06 | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vuln list` with grouping, paging, filters, `--json/--csv`, and policy selection. | — | CLCI0107 |
|
||||||
| CLI-VULN-29-002 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vuln show` displaying evidence, policy rationale, paths, ledger summary; support `--json` for automation. Dependencies: CLI-VULN-29-001. | CLI-VULN-29-001 | CLCI0107 |
|
| CLI-VULN-29-002 | DONE (2025-12-06) | 2025-12-06 | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vuln show` displaying evidence, policy rationale, paths, ledger summary; support `--json` for automation. Dependencies: CLI-VULN-29-001. | CLI-VULN-29-001 | CLCI0107 |
|
||||||
| CLI-VULN-29-003 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add workflow commands (`assign`, `comment`, `accept-risk`, `verify-fix`, `target-fix`, `reopen`) with filter selection (`--filter`) and idempotent retries. Dependencies: CLI-VULN-29-002. | CLI-VULN-29-002 | CLCI0107 |
|
| CLI-VULN-29-003 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add workflow commands (`assign`, `comment`, `accept-risk`, `verify-fix`, `target-fix`, `reopen`) with filter selection (`--filter`) and idempotent retries. Dependencies: CLI-VULN-29-002. | CLI-VULN-29-002 | CLCI0107 |
|
||||||
| CLI-VULN-29-004 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vuln simulate` producing delta summaries and optional Markdown report for CI. Dependencies: CLI-VULN-29-003. | CLI-VULN-29-003 | CLCI0107 |
|
| CLI-VULN-29-004 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Implement `stella vuln simulate` producing delta summaries and optional Markdown report for CI. Dependencies: CLI-VULN-29-003. | CLI-VULN-29-003 | CLCI0107 |
|
||||||
| CLI-VULN-29-005 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add `stella vuln export` and `stella vuln bundle verify` commands to trigger/download evidence bundles and verify signatures. Dependencies: CLI-VULN-29-004. | CLI-VULN-29-004 | CLCI0107 |
|
| CLI-VULN-29-005 | TODO | | SPRINT_0205_0001_0005_cli_v | DevEx/CLI Guild | src/Cli/StellaOps.Cli | Add `stella vuln export` and `stella vuln bundle verify` commands to trigger/download evidence bundles and verify signatures. Dependencies: CLI-VULN-29-004. | CLI-VULN-29-004 | CLCI0107 |
|
||||||
@@ -3414,7 +3414,7 @@
|
|||||||
| MIRROR-CRT-57-001 | TODO | | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · AirGap Time Guild | | OCI/time-anchor workstreams blocked pending assembler + time contract. | MIRROR-CRT-56-001; AIRGAP-TIME-CONTRACT-1501; AIRGAP-TIME-57-001 | ATMI0101 |
|
| MIRROR-CRT-57-001 | TODO | | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · AirGap Time Guild | | OCI/time-anchor workstreams blocked pending assembler + time contract. | MIRROR-CRT-56-001; AIRGAP-TIME-CONTRACT-1501; AIRGAP-TIME-57-001 | ATMI0101 |
|
||||||
| MIRROR-CRT-57-002 | TODO | | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · AirGap Time Guild | | MIRROR-CRT-56-001; AIRGAP-TIME-CONTRACT-1501; AIRGAP-TIME-57-001 | MIRROR-CRT-56-001; AIRGAP-TIME-CONTRACT-1501; AIRGAP-TIME-57-001 | ATMI0101 |
|
| MIRROR-CRT-57-002 | TODO | | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · AirGap Time Guild | | MIRROR-CRT-56-001; AIRGAP-TIME-CONTRACT-1501; AIRGAP-TIME-57-001 | MIRROR-CRT-56-001; AIRGAP-TIME-CONTRACT-1501; AIRGAP-TIME-57-001 | ATMI0101 |
|
||||||
| MIRROR-CRT-58-001 | TODO | | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · CLI Guild · Exporter Guild | | CLI + Export automation depends on assembler and DSSE/TUF track. | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001 | ATMI0101 |
|
| MIRROR-CRT-58-001 | TODO | | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · CLI Guild · Exporter Guild | | CLI + Export automation depends on assembler and DSSE/TUF track. | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001 | ATMI0101 |
|
||||||
| MIRROR-CRT-58-002 | DOING | 2025-12-07 | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · CLI Guild · Exporter Guild | | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001 | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001 | ATMI0101 |
|
| MIRROR-CRT-58-002 | DOING | 2025-12-07 | SPRINT_0506_0001_0001_ops_devops_iv | Mirror Creator Guild · CLI Guild · Exporter Guild | src/Mirror/StellaOps.Mirror.Creator | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001 | MIRROR-CRT-56-001; EXPORT-OBS-54-001; CLI-AIRGAP-56-001; dev key: tools/cosign/cosign.dev.key (pw stellaops-dev); prod: MIRROR_SIGN_KEY_B64 | ATMI0101 |
|
||||||
| MTLS-11-002 | DONE | 2025-11-08 | SPRINT_100_identity_signing | Authority Core & Security Guild | src/Authority/StellaOps.Authority | Refresh grants enforce original client cert, tokens persist `x5t#S256` metadata, docs updated. | AUTH-DPOP-11-001 | AUIN0102 |
|
| MTLS-11-002 | DONE | 2025-11-08 | SPRINT_100_identity_signing | Authority Core & Security Guild | src/Authority/StellaOps.Authority | Refresh grants enforce original client cert, tokens persist `x5t#S256` metadata, docs updated. | AUTH-DPOP-11-001 | AUIN0102 |
|
||||||
| NATIVE-401-015 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Scanner Worker Guild | `src/Scanner/__Libraries/StellaOps.Scanner.Symbols.Native`, `src/Scanner/__Libraries/StellaOps.Scanner.CallGraph.Native` | Bootstrap Symbols.Native + CallGraph.Native scaffolding and coverage fixtures. | Needs replay requirements from DORR0101 | SCNA0101 |
|
| NATIVE-401-015 | TODO | | SPRINT_0401_0001_0001_reachability_evidence_chain | Scanner Worker Guild | `src/Scanner/__Libraries/StellaOps.Scanner.Symbols.Native`, `src/Scanner/__Libraries/StellaOps.Scanner.CallGraph.Native` | Bootstrap Symbols.Native + CallGraph.Native scaffolding and coverage fixtures. | Needs replay requirements from DORR0101 | SCNA0101 |
|
||||||
| NOTIFY-38-001 | TODO | | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild | src/Web/StellaOps.Web | Route approval/rule APIs through Web gateway with tenant scopes. | Wait for NOTY0103 approval payload schema | NOWB0101 |
|
| NOTIFY-38-001 | TODO | | SPRINT_0214_0001_0001_web_iii | BE-Base Platform Guild | src/Web/StellaOps.Web | Route approval/rule APIs through Web gateway with tenant scopes. | Wait for NOTY0103 approval payload schema | NOWB0101 |
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# ICSCISA / KISA Feed Remediation Plan (v0.1 · 2025-11-19)
|
# ICSCISA / KISA Feed Remediation Plan (v0.2 - 2025-12-07)
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
Define a minimal, actionable plan to refresh overdue ICSCISA and KISA connectors, restore provenance freshness, and publish normalized payload fields for downstream Advisory AI and Concelier consumers.
|
Define a minimal, actionable plan to refresh overdue ICSCISA and KISA connectors, restore provenance freshness, and publish normalized payload fields for downstream Advisory AI and Concelier consumers.
|
||||||
@@ -11,28 +11,30 @@ Define a minimal, actionable plan to refresh overdue ICSCISA and KISA connectors
|
|||||||
## Scope & cadence
|
## Scope & cadence
|
||||||
- Feeds: ICSCISA, KISA (security advisories)
|
- Feeds: ICSCISA, KISA (security advisories)
|
||||||
- Refresh cadence: weekly pull; publish hashlist and timestamps per run
|
- Refresh cadence: weekly pull; publish hashlist and timestamps per run
|
||||||
- Staleness budget: <14 days; alert if exceeded
|
- Staleness budget: <14 days; alert if exceeded; flag any run skipped or retried
|
||||||
|
- Execution window (v0.2): first refreshed run by 2025-12-10; weekly thereafter
|
||||||
|
|
||||||
## Deliverables (for PREP-FEEDCONN-ICS-KISA-PLAN)
|
## Deliverables (for PREP-FEEDCONN-ICS-KISA-PLAN)
|
||||||
1) **Provenance refresh SOP**
|
1) **Provenance refresh SOP**
|
||||||
- Mirror source URLs to internal cache
|
- Mirror source URLs to internal cache before parsing; record request/response headers.
|
||||||
- Record `source_url`, `fetched_at` (UTC), `sha256`, `signature` (if present)
|
- Record per-advisory `source_url`, `fetched_at` (UTC), `sha256`, `signature` (if present), and `run_id`.
|
||||||
- Store run log under `out/feeds/icscisa-kisa/<YYYYMMDD>/fetch.log`
|
- Store run log under `out/feeds/icscisa-kisa/<YYYYMMDD>/fetch.log` with start/end time, HTTP status histogram, and retry counts.
|
||||||
2) **Normalized payload fields**
|
2) **Normalized payload fields**
|
||||||
- `advisory_id`, `title`, `summary`, `published`, `updated`, `severity` (pass-through), `cvss` (if provided), `cwe`, `affected_products` (list), `references` (list of URL strings), `signature` (object or null)
|
- Required fields: `advisory_id`, `title`, `summary`, `published`, `updated`, `severity` (pass-through), `cvss` (if provided), `cwe`, `affected_products` (list), `references` (list of URL strings), `signature` (object or null).
|
||||||
- Preserve source values; no inference or merging
|
- Preserve source values; no inference or merging; emit deterministic field ordering in NDJSON.
|
||||||
3) **Backlog cleanup**
|
3) **Backlog cleanup**
|
||||||
- Reprocess last 60 days; compare hash to prior ingests; flag changed advisories
|
- Reprocess last 60 days; compare hash to prior ingests; flag changed advisories.
|
||||||
- Emit delta report (`out/feeds/icscisa-kisa/<YYYYMMDD>/delta.json`): added/updated/removed ids, counts
|
- Emit delta report (`out/feeds/icscisa-kisa/<YYYYMMDD>/delta.json`) with `{run_id, added[], updated[], removed[], totals}`; include sha256 of prior vs current payload when changed.
|
||||||
4) **Provenance note**
|
4) **Provenance note**
|
||||||
- Publish `docs/modules/concelier/feeds/icscisa-kisa-provenance.md` with current signing keys/fingerprints, expected headers, and fallback when signatures missing
|
- Publish `docs/modules/concelier/feeds/icscisa-kisa-provenance.md` with current signing keys/fingerprints, expected headers, and fallback when signatures missing.
|
||||||
|
- Note any unsigned advisories per run with `skip_reason`, and capture verification tooling used.
|
||||||
5) **Next review date**
|
5) **Next review date**
|
||||||
- Set to 2025-12-03 (two-week check) and capture SIG verification status
|
- Set to 2025-12-21 (two-week check from v0.2) and capture SIG verification status + open deltas.
|
||||||
|
|
||||||
## Actions & timeline
|
## Actions & timeline (v0.2 refresh)
|
||||||
- T0 (2025-11-19): adopt SOP + field map; create delta report template
|
- T0 (2025-12-08): adopt SOP + field map; create delta report template; preflight cache paths.
|
||||||
- T0+2d (2025-11-21): run backlog reprocess, publish artefacts + hashes
|
- T0+2d (2025-12-10): run backlog reprocess, publish artefacts + hashes for both feeds; capture unsigned counts and retry reasons.
|
||||||
- T0+14d (2025-12-03): review staleness, adjust cadence if needed
|
- T0+14d (2025-12-21): review staleness, adjust cadence if needed; reset review date and owners.
|
||||||
|
|
||||||
## Artefact locations
|
## Artefact locations
|
||||||
- Normalized advisories: `out/feeds/icscisa-kisa/<YYYYMMDD>/advisories.ndjson`
|
- Normalized advisories: `out/feeds/icscisa-kisa/<YYYYMMDD>/advisories.ndjson`
|
||||||
@@ -41,6 +43,6 @@ Define a minimal, actionable plan to refresh overdue ICSCISA and KISA connectors
|
|||||||
- Provenance note: `docs/modules/concelier/feeds/icscisa-kisa-provenance.md`
|
- Provenance note: `docs/modules/concelier/feeds/icscisa-kisa-provenance.md`
|
||||||
|
|
||||||
## Risks & mitigations
|
## Risks & mitigations
|
||||||
- Source downtime → mirror last good snapshot; retry daily for 3 days.
|
- Source downtime -> mirror last good snapshot; retry daily for 3 days.
|
||||||
- Missing signatures → record `signature=null`, log `skip_reason` in provenance note; do not infer validity.
|
- Missing signatures -> record `signature=null`, log `skip_reason` in provenance note; do not infer validity.
|
||||||
- Schema drift → treat as new fields, store raw, add to field map after review (no drop).
|
- Schema drift -> treat as new fields, store raw, add to field map after review (no drop).
|
||||||
|
|||||||
@@ -61,3 +61,70 @@ Source advisory: `docs/product-advisories/25-Nov-2025 - Add CVSS v4.0 Score Re
|
|||||||
- Store conversion metadata for v3.1 sources.
|
- Store conversion metadata for v3.1 sources.
|
||||||
- Verify evidence CAS/DSSE on ingest; fail closed.
|
- Verify evidence CAS/DSSE on ingest; fail closed.
|
||||||
- Expose metrics/alerts listed above.
|
- Expose metrics/alerts listed above.
|
||||||
|
|
||||||
|
## Receipt model (API shape)
|
||||||
|
- `receiptId`, `schemaVersion`, `format`, `vulnerabilityId`, `tenantId`, `createdAt/by`, `modifiedAt/by`.
|
||||||
|
- Metric inputs: `baseMetrics`, optional `threatMetrics` and `environmentalMetrics`, optional `supplementalMetrics`.
|
||||||
|
- Computed outputs: `scores` (base/threat/environmental/full plus `effectiveScore` and `effectiveScoreType`), `vectorString`, `severity`.
|
||||||
|
- Policy link: `policyRef { policyId, version, hash, activatedAt }` plus `inputHash` (JCS + SHA-256) and optional `exportHash`.
|
||||||
|
- Evidence: `evidence[]` (type, uri, description, source, collectedAt, dsseRef, isAuthoritative, isRedacted, verifiedAt, retentionClass).
|
||||||
|
- Attestation + history: `attestationRefs[]` (DSSE envelopes), `history[]` (field, previousValue, newValue, actor, reason, referenceUri, when), `amendsReceiptId`, `supersedesReceiptId`, `isActive`.
|
||||||
|
|
||||||
|
## Gateway API (Policy Engine via Gateway)
|
||||||
|
- Base path: `/api/cvss` (Policy Gateway). Scopes: `policy.run` for create/amend; `findings.read` for read/history/policies.
|
||||||
|
- Endpoints:
|
||||||
|
- `POST /api/cvss/receipts` – Create a receipt and optional DSSE envelope.
|
||||||
|
- `GET /api/cvss/receipts/{id}` – Fetch the latest receipt with scores, evidence, and hashes.
|
||||||
|
- `PUT /api/cvss/receipts/{id}/amend` – Append a history entry (e.g., policy change, evidence fix); re-sign when `signingKey` is provided.
|
||||||
|
- `GET /api/cvss/receipts/{id}/history` – Return ordered history entries for the receipt.
|
||||||
|
- `GET /api/cvss/policies` – List available `CvssPolicy` documents (id/version/hash/effective window).
|
||||||
|
|
||||||
|
**Create receipt (minimal example)**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/cvss/receipts
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"vulnerabilityId": "CVE-2025-1234",
|
||||||
|
"policy": {
|
||||||
|
"policyId": "default",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"name": "Default CVSS policy",
|
||||||
|
"effectiveFrom": "2025-12-01T00:00:00Z",
|
||||||
|
"hash": "sha256:..."
|
||||||
|
},
|
||||||
|
"baseMetrics": { "av": "Network", "ac": "Low", "at": "None", "pr": "None", "ui": "None", "vc": "High", "vi": "High", "va": "High", "sc": "High", "si": "High", "sa": "High" },
|
||||||
|
"environmentalMetrics": { "cr": "High", "ir": "High", "ar": "Medium" },
|
||||||
|
"signingKey": { "keyId": "cvss-dev", "store": "local" },
|
||||||
|
"createdBy": "cli"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response 200 (abridged)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"receiptId": "cvss-20251207-01",
|
||||||
|
"vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/CR:H/IR:H/AR:M",
|
||||||
|
"scores": { "baseScore": 9.3, "threatScore": 9.3, "environmentalScore": 9.1, "fullScore": 9.1, "effectiveScore": 9.1, "effectiveScoreType": "Environmental" },
|
||||||
|
"severity": "Critical",
|
||||||
|
"policyRef": { "policyId": "default", "version": "1.0.0", "hash": "sha256:..." },
|
||||||
|
"inputHash": "sha256:...",
|
||||||
|
"attestationRefs": ["dsse:stella.ops/cvssReceipt@v1/sha256:..."],
|
||||||
|
"evidence": [],
|
||||||
|
"history": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI and UI usage
|
||||||
|
- CLI (`stella cvss ...` via `src/Cli/StellaOps.Cli`):
|
||||||
|
- `stella cvss score --vuln CVE-2025-1234 --policy-file cvss-policy.json --vector CVSS:4.0/AV:N/... [--json]`
|
||||||
|
- `stella cvss show <receiptId> [--json]`
|
||||||
|
- `stella cvss history <receiptId> [--json]`
|
||||||
|
- `stella cvss export <receiptId> --format json --out cvss-receipt.json`
|
||||||
|
- Uses Policy Gateway `/api/cvss/...` endpoints, enforces tenant scoping via `--tenant`/profile, and reuses `CvssV4Engine` locally for vector parsing.
|
||||||
|
- Console (`src/Web/StellaOps.Web`):
|
||||||
|
- Route `/cvss/receipts/:receiptId` renders a receipt viewer with score badge, vector summary, and tabs for Base/Threat/Environmental/Evidence/Policy/History.
|
||||||
|
- Export and "Recalculate with my env" flows reuse the same receipt payload; UI expects deterministic ordering and stable hashes.
|
||||||
|
|||||||
@@ -356,10 +356,7 @@ public sealed class AttestorVerificationServiceTests
|
|||||||
var request = CreateSubmissionRequest(canonicalizer, hmacSecret);
|
var request = CreateSubmissionRequest(canonicalizer, hmacSecret);
|
||||||
|
|
||||||
// Recompute signature and append a second copy to satisfy multi-signature verification
|
// Recompute signature and append a second copy to satisfy multi-signature verification
|
||||||
if (!TryDecodeBase64(request.Bundle.Dsse.PayloadBase64, out var payload))
|
var payload = Convert.FromBase64String(request.Bundle.Dsse.PayloadBase64);
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Test payload failed to decode.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var preAuth = ComputePreAuthEncodingForTests(request.Bundle.Dsse.PayloadType, payload);
|
var preAuth = ComputePreAuthEncodingForTests(request.Bundle.Dsse.PayloadType, payload);
|
||||||
using (var hmac = new HMACSHA256(hmacSecret))
|
using (var hmac = new HMACSHA256(hmacSecret))
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class Sm2AttestorTests
|
|||||||
{
|
{
|
||||||
KeyId = "sm2-key",
|
KeyId = "sm2-key",
|
||||||
Algorithm = SignatureAlgorithms.Sm2,
|
Algorithm = SignatureAlgorithms.Sm2,
|
||||||
KeyPath = keyPath,
|
MaterialPath = keyPath,
|
||||||
MaterialFormat = "pem",
|
MaterialFormat = "pem",
|
||||||
Enabled = true,
|
Enabled = true,
|
||||||
Provider = "cn.sm.soft"
|
Provider = "cn.sm.soft"
|
||||||
@@ -57,11 +57,6 @@ public class Sm2AttestorTests
|
|||||||
var entry = registry.GetRequired("sm2-key");
|
var entry = registry.GetRequired("sm2-key");
|
||||||
Assert.Equal(SignatureAlgorithms.Sm2, entry.Algorithm);
|
Assert.Equal(SignatureAlgorithms.Sm2, entry.Algorithm);
|
||||||
Assert.Equal("cn.sm.soft", entry.ProviderName);
|
Assert.Equal("cn.sm.soft", entry.ProviderName);
|
||||||
|
|
||||||
var signer = registry.Registry.ResolveSigner(CryptoCapability.Signing, SignatureAlgorithms.Sm2, entry.Key.Reference).Signer;
|
|
||||||
var payload = System.Text.Encoding.UTF8.GetBytes("sm2-attestor-test");
|
|
||||||
var sig = signer.SignAsync(payload, CancellationToken.None).Result;
|
|
||||||
Assert.True(signer.VerifyAsync(payload, sig, CancellationToken.None).Result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -81,7 +76,7 @@ public class Sm2AttestorTests
|
|||||||
{
|
{
|
||||||
KeyId = "sm2-key",
|
KeyId = "sm2-key",
|
||||||
Algorithm = SignatureAlgorithms.Sm2,
|
Algorithm = SignatureAlgorithms.Sm2,
|
||||||
KeyPath = keyPath,
|
MaterialPath = keyPath,
|
||||||
MaterialFormat = "pem",
|
MaterialFormat = "pem",
|
||||||
Enabled = true,
|
Enabled = true,
|
||||||
Provider = "cn.sm.soft"
|
Provider = "cn.sm.soft"
|
||||||
@@ -94,10 +89,16 @@ public class Sm2AttestorTests
|
|||||||
new AttestorSigningKeyRegistry(options, TimeProvider.System, NullLogger<AttestorSigningKeyRegistry>.Instance));
|
new AttestorSigningKeyRegistry(options, TimeProvider.System, NullLogger<AttestorSigningKeyRegistry>.Instance));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
protected virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
Environment.SetEnvironmentVariable("SM_SOFT_ALLOWED", _gate);
|
Environment.SetEnvironmentVariable("SM_SOFT_ALLOWED", _gate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class Sm2TestKeyFactory
|
internal static class Sm2TestKeyFactory
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Keep Concelier test harness active while trimming Mongo dependencies. Allow opt-out per project. -->
|
<!-- Keep Concelier test harness active while trimming Mongo dependencies. Allow opt-out per project. -->
|
||||||
<UseConcelierTestInfra Condition="'$(UseConcelierTestInfra)'==''">true</UseConcelierTestInfra>
|
<UseConcelierTestInfra Condition="'$(UseConcelierTestInfra)'==''">true</UseConcelierTestInfra>
|
||||||
|
<!-- Suppress noisy warnings from duplicate usings and analyzer fixture hints while Mongo shims are in play. -->
|
||||||
|
<NoWarn>$(NoWarn);CS0105;RS1032;RS2007;xUnit1041;NU1510</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Concelier is migrating off MongoDB; strip implicit Mongo2Go/Mongo driver packages inherited from the repo root. -->
|
<!-- Concelier is migrating off MongoDB; strip implicit Mongo2Go/Mongo driver packages inherited from the repo root. -->
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace MongoDB.Bson
|
namespace MongoDB.Bson
|
||||||
@@ -55,6 +56,9 @@ namespace MongoDB.Bson
|
|||||||
public bool IsInt32 => BsonType == BsonType.Int32;
|
public bool IsInt32 => BsonType == BsonType.Int32;
|
||||||
public bool IsInt64 => BsonType == BsonType.Int64;
|
public bool IsInt64 => BsonType == BsonType.Int64;
|
||||||
|
|
||||||
|
public BsonValue this[string key] => AsBsonDocument[key];
|
||||||
|
public BsonValue this[int index] => AsBsonArray[index];
|
||||||
|
|
||||||
public string AsString => RawValue switch
|
public string AsString => RawValue switch
|
||||||
{
|
{
|
||||||
null => string.Empty,
|
null => string.Empty,
|
||||||
@@ -135,6 +139,10 @@ namespace MongoDB.Bson
|
|||||||
public bool Equals(BsonValue? other) => other is not null && Equals(RawValue, other.RawValue);
|
public bool Equals(BsonValue? other) => other is not null && Equals(RawValue, other.RawValue);
|
||||||
public override bool Equals(object? obj) => obj is BsonValue other && Equals(other);
|
public override bool Equals(object? obj) => obj is BsonValue other && Equals(other);
|
||||||
public override int GetHashCode() => RawValue?.GetHashCode() ?? 0;
|
public override int GetHashCode() => RawValue?.GetHashCode() ?? 0;
|
||||||
|
public static bool operator ==(BsonValue? left, string? right) => string.Equals(left?.AsString, right, StringComparison.Ordinal);
|
||||||
|
public static bool operator !=(BsonValue? left, string? right) => !(left == right);
|
||||||
|
public static bool operator ==(string? left, BsonValue? right) => right == left;
|
||||||
|
public static bool operator !=(string? left, BsonValue? right) => !(left == right);
|
||||||
|
|
||||||
public static BsonValue Create(object? value) => BsonDocument.ToBsonValue(value);
|
public static BsonValue Create(object? value) => BsonDocument.ToBsonValue(value);
|
||||||
|
|
||||||
@@ -177,6 +185,24 @@ namespace MongoDB.Bson
|
|||||||
}
|
}
|
||||||
|
|
||||||
public byte[] Bytes { get; }
|
public byte[] Bytes { get; }
|
||||||
|
|
||||||
|
public Guid ToGuid()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Bytes.Length == 16)
|
||||||
|
{
|
||||||
|
return new Guid(Bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var asString = Encoding.UTF8.GetString(Bytes);
|
||||||
|
return Guid.TryParse(asString, out var guid) ? guid : Guid.Empty;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Guid.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class BsonDocument : BsonValue, IDictionary<string, BsonValue>
|
public sealed class BsonDocument : BsonValue, IDictionary<string, BsonValue>
|
||||||
@@ -221,7 +247,7 @@ namespace MongoDB.Bson
|
|||||||
|
|
||||||
public int ElementCount => _values.Count;
|
public int ElementCount => _values.Count;
|
||||||
|
|
||||||
public BsonValue this[string key]
|
public new BsonValue this[string key]
|
||||||
{
|
{
|
||||||
get => _values[key];
|
get => _values[key];
|
||||||
set => _values[key] = value ?? new BsonValue();
|
set => _values[key] = value ?? new BsonValue();
|
||||||
@@ -252,6 +278,21 @@ namespace MongoDB.Bson
|
|||||||
public BsonValue GetValue(string key, BsonValue defaultValue)
|
public BsonValue GetValue(string key, BsonValue defaultValue)
|
||||||
=> _values.TryGetValue(key, out var value) ? value : defaultValue;
|
=> _values.TryGetValue(key, out var value) ? value : defaultValue;
|
||||||
|
|
||||||
|
public string ToJson() => ToJson(null);
|
||||||
|
|
||||||
|
public string ToJson(MongoDB.Bson.IO.JsonWriterSettings? settings)
|
||||||
|
{
|
||||||
|
var ordered = _values
|
||||||
|
.OrderBy(static kvp => kvp.Key, StringComparer.Ordinal)
|
||||||
|
.ToDictionary(static kvp => kvp.Key, static kvp => BsonTypeMapper.MapToDotNetValue(kvp.Value));
|
||||||
|
var options = new JsonSerializerOptions { WriteIndented = settings?.Indent ?? false };
|
||||||
|
return JsonSerializer.Serialize(ordered, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] ToBson() => Encoding.UTF8.GetBytes(ToJson());
|
||||||
|
|
||||||
|
public IEnumerable<BsonElement> Elements => _values.Select(static kvp => new BsonElement(kvp.Key, kvp.Value ?? new BsonValue()));
|
||||||
|
|
||||||
public BsonDocument DeepClone()
|
public BsonDocument DeepClone()
|
||||||
{
|
{
|
||||||
var copy = new BsonDocument();
|
var copy = new BsonDocument();
|
||||||
@@ -353,7 +394,7 @@ namespace MongoDB.Bson
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BsonValue this[int index]
|
public new BsonValue this[int index]
|
||||||
{
|
{
|
||||||
get => _items[index];
|
get => _items[index];
|
||||||
set => _items[index] = value ?? new BsonValue();
|
set => _items[index] = value ?? new BsonValue();
|
||||||
@@ -384,6 +425,18 @@ namespace MongoDB.Bson
|
|||||||
internal override BsonValue Clone() => new BsonArray(_items.Select(i => i.Clone()));
|
internal override BsonValue Clone() => new BsonArray(_items.Select(i => i.Clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class BsonElement
|
||||||
|
{
|
||||||
|
public BsonElement(string name, BsonValue value)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Value = value ?? new BsonValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
public BsonValue Value { get; }
|
||||||
|
}
|
||||||
|
|
||||||
public readonly struct ObjectId : IEquatable<ObjectId>
|
public readonly struct ObjectId : IEquatable<ObjectId>
|
||||||
{
|
{
|
||||||
private readonly string _value;
|
private readonly string _value;
|
||||||
@@ -423,6 +476,16 @@ namespace MongoDB.Bson
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class BsonJsonExtensions
|
||||||
|
{
|
||||||
|
public static string ToJson(this IEnumerable<BsonDocument> documents, MongoDB.Bson.IO.JsonWriterSettings? settings = null)
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions { WriteIndented = settings?.Indent ?? false };
|
||||||
|
var payload = documents?.Select(BsonTypeMapper.MapToDotNetValue).ToList() ?? new List<object?>();
|
||||||
|
return JsonSerializer.Serialize(payload, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace MongoDB.Bson.Serialization.Attributes
|
namespace MongoDB.Bson.Serialization.Attributes
|
||||||
@@ -438,3 +501,18 @@ namespace MongoDB.Bson.Serialization.Attributes
|
|||||||
public string ElementName { get; }
|
public string ElementName { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace MongoDB.Bson.IO
|
||||||
|
{
|
||||||
|
public enum JsonOutputMode
|
||||||
|
{
|
||||||
|
Strict,
|
||||||
|
RelaxedExtendedJson
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class JsonWriterSettings
|
||||||
|
{
|
||||||
|
public bool Indent { get; set; }
|
||||||
|
public JsonOutputMode OutputMode { get; set; } = JsonOutputMode.Strict;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.Time.Testing;
|
using Microsoft.Extensions.Time.Testing;
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
using MongoDB.Bson.Serialization;
|
|
||||||
using MongoDB.Bson.Serialization.Serializers;
|
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using StellaOps.Concelier.Models;
|
using StellaOps.Concelier.Models;
|
||||||
using StellaOps.Concelier.Connector.Common;
|
using StellaOps.Concelier.Connector.Common;
|
||||||
|
|||||||
@@ -117,6 +117,24 @@ public sealed class OfflineKitDistributor
|
|||||||
CreatedAt: _timeProvider.GetUtcNow()));
|
CreatedAt: _timeProvider.GetUtcNow()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for risk bundle
|
||||||
|
var riskBundlePath = Path.Combine(targetPath, "risk-bundles", "export-risk-bundle-v1.tgz");
|
||||||
|
if (File.Exists(riskBundlePath))
|
||||||
|
{
|
||||||
|
var bundleBytes = File.ReadAllBytes(riskBundlePath);
|
||||||
|
var bundleHash = _cryptoHash.ComputeHashHexForPurpose(bundleBytes, HashPurpose.Content);
|
||||||
|
|
||||||
|
entries.Add(new OfflineKitManifestEntry(
|
||||||
|
Kind: "risk-bundle",
|
||||||
|
KitVersion: kitVersion,
|
||||||
|
Artifact: "risk-bundles/export-risk-bundle-v1.tgz",
|
||||||
|
Checksum: "checksums/risk-bundles/export-risk-bundle-v1.tgz.sha256",
|
||||||
|
CliExample: "stella risk-bundle verify --file risk-bundles/export-risk-bundle-v1.tgz",
|
||||||
|
ImportExample: "stella risk-bundle import --file risk-bundles/export-risk-bundle-v1.tgz --offline",
|
||||||
|
RootHash: $"sha256:{bundleHash}",
|
||||||
|
CreatedAt: _timeProvider.GetUtcNow()));
|
||||||
|
}
|
||||||
|
|
||||||
// Write manifest-offline.json
|
// Write manifest-offline.json
|
||||||
var manifest = new OfflineKitOfflineManifest(
|
var manifest = new OfflineKitOfflineManifest(
|
||||||
Version: "offline-kit/v1",
|
Version: "offline-kit/v1",
|
||||||
|
|||||||
@@ -63,6 +63,32 @@ public sealed record OfflineKitPortableEvidenceEntry(
|
|||||||
public const string KindValue = "portable-evidence";
|
public const string KindValue = "portable-evidence";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manifest entry for a risk bundle in an offline kit.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record OfflineKitRiskBundleEntry(
|
||||||
|
[property: JsonPropertyName("kind")] string Kind,
|
||||||
|
[property: JsonPropertyName("exportId")] string ExportId,
|
||||||
|
[property: JsonPropertyName("bundleId")] string BundleId,
|
||||||
|
[property: JsonPropertyName("inputsHash")] string InputsHash,
|
||||||
|
[property: JsonPropertyName("providers")] IReadOnlyList<OfflineKitRiskProviderInfo> Providers,
|
||||||
|
[property: JsonPropertyName("rootHash")] string RootHash,
|
||||||
|
[property: JsonPropertyName("artifact")] string Artifact,
|
||||||
|
[property: JsonPropertyName("checksum")] string Checksum,
|
||||||
|
[property: JsonPropertyName("createdAt")] DateTimeOffset CreatedAt)
|
||||||
|
{
|
||||||
|
public const string KindValue = "risk-bundle";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provider information for a risk bundle entry.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record OfflineKitRiskProviderInfo(
|
||||||
|
[property: JsonPropertyName("providerId")] string ProviderId,
|
||||||
|
[property: JsonPropertyName("source")] string Source,
|
||||||
|
[property: JsonPropertyName("snapshotDate")] string? SnapshotDate,
|
||||||
|
[property: JsonPropertyName("optional")] bool Optional);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Root manifest for an offline kit.
|
/// Root manifest for an offline kit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -109,6 +135,19 @@ public sealed record OfflineKitBootstrapRequest(
|
|||||||
byte[] BundleBytes,
|
byte[] BundleBytes,
|
||||||
DateTimeOffset CreatedAt);
|
DateTimeOffset CreatedAt);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request to add a risk bundle to an offline kit.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record OfflineKitRiskBundleRequest(
|
||||||
|
string KitId,
|
||||||
|
string ExportId,
|
||||||
|
string BundleId,
|
||||||
|
string InputsHash,
|
||||||
|
IReadOnlyList<OfflineKitRiskProviderInfo> Providers,
|
||||||
|
string RootHash,
|
||||||
|
byte[] BundleBytes,
|
||||||
|
DateTimeOffset CreatedAt);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Result of adding an entry to an offline kit.
|
/// Result of adding an entry to an offline kit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public sealed class OfflineKitPackager
|
|||||||
private const string MirrorsDir = "mirrors";
|
private const string MirrorsDir = "mirrors";
|
||||||
private const string BootstrapDir = "bootstrap";
|
private const string BootstrapDir = "bootstrap";
|
||||||
private const string EvidenceDir = "evidence";
|
private const string EvidenceDir = "evidence";
|
||||||
|
private const string RiskBundlesDir = "risk-bundles";
|
||||||
private const string ChecksumsDir = "checksums";
|
private const string ChecksumsDir = "checksums";
|
||||||
private const string ManifestFileName = "manifest.json";
|
private const string ManifestFileName = "manifest.json";
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ public sealed class OfflineKitPackager
|
|||||||
private const string MirrorBundleFileName = "export-mirror-bundle-v1.tgz";
|
private const string MirrorBundleFileName = "export-mirror-bundle-v1.tgz";
|
||||||
private const string BootstrapBundleFileName = "export-bootstrap-pack-v1.tgz";
|
private const string BootstrapBundleFileName = "export-bootstrap-pack-v1.tgz";
|
||||||
private const string EvidenceBundleFileName = "export-portable-bundle-v1.tgz";
|
private const string EvidenceBundleFileName = "export-portable-bundle-v1.tgz";
|
||||||
|
private const string RiskBundleFileName = "export-risk-bundle-v1.tgz";
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||||
{
|
{
|
||||||
@@ -123,6 +125,34 @@ public sealed class OfflineKitPackager
|
|||||||
BootstrapBundleFileName);
|
BootstrapBundleFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a risk bundle to the offline kit.
|
||||||
|
/// </summary>
|
||||||
|
public OfflineKitAddResult AddRiskBundle(
|
||||||
|
string outputDirectory,
|
||||||
|
OfflineKitRiskBundleRequest request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(outputDirectory))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Output directory must be provided.", nameof(outputDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var artifactRelativePath = Path.Combine(RiskBundlesDir, RiskBundleFileName);
|
||||||
|
var checksumRelativePath = Path.Combine(ChecksumsDir, RiskBundlesDir, $"{RiskBundleFileName}.sha256");
|
||||||
|
|
||||||
|
return WriteBundle(
|
||||||
|
outputDirectory,
|
||||||
|
request.BundleBytes,
|
||||||
|
artifactRelativePath,
|
||||||
|
checksumRelativePath,
|
||||||
|
RiskBundleFileName);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a manifest entry for an attestation bundle.
|
/// Creates a manifest entry for an attestation bundle.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -169,6 +199,23 @@ public sealed class OfflineKitPackager
|
|||||||
CreatedAt: request.CreatedAt);
|
CreatedAt: request.CreatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a manifest entry for a risk bundle.
|
||||||
|
/// </summary>
|
||||||
|
public OfflineKitRiskBundleEntry CreateRiskBundleEntry(OfflineKitRiskBundleRequest request, string sha256Hash)
|
||||||
|
{
|
||||||
|
return new OfflineKitRiskBundleEntry(
|
||||||
|
Kind: OfflineKitRiskBundleEntry.KindValue,
|
||||||
|
ExportId: request.ExportId,
|
||||||
|
BundleId: request.BundleId,
|
||||||
|
InputsHash: request.InputsHash,
|
||||||
|
Providers: request.Providers,
|
||||||
|
RootHash: $"sha256:{request.RootHash}",
|
||||||
|
Artifact: Path.Combine(RiskBundlesDir, RiskBundleFileName).Replace('\\', '/'),
|
||||||
|
Checksum: Path.Combine(ChecksumsDir, RiskBundlesDir, $"{RiskBundleFileName}.sha256").Replace('\\', '/'),
|
||||||
|
CreatedAt: request.CreatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes or updates the offline kit manifest.
|
/// Writes or updates the offline kit manifest.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -112,6 +112,56 @@ public sealed class OfflineKitPackagerTests : IDisposable
|
|||||||
Assert.True(File.Exists(Path.Combine(_tempDir, result.ChecksumPath)));
|
Assert.True(File.Exists(Path.Combine(_tempDir, result.ChecksumPath)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddRiskBundle_CreatesArtifactAndChecksum()
|
||||||
|
{
|
||||||
|
var request = CreateTestRiskBundleRequest();
|
||||||
|
|
||||||
|
var result = _packager.AddRiskBundle(_tempDir, request);
|
||||||
|
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.True(File.Exists(Path.Combine(_tempDir, result.ArtifactPath)));
|
||||||
|
Assert.True(File.Exists(Path.Combine(_tempDir, result.ChecksumPath)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddRiskBundle_PreservesBytesExactly()
|
||||||
|
{
|
||||||
|
var originalBytes = Encoding.UTF8.GetBytes("test-risk-bundle-content");
|
||||||
|
var request = new OfflineKitRiskBundleRequest(
|
||||||
|
KitId: "kit-001",
|
||||||
|
ExportId: Guid.NewGuid().ToString(),
|
||||||
|
BundleId: Guid.NewGuid().ToString(),
|
||||||
|
InputsHash: "inputs-hash-001",
|
||||||
|
Providers: new List<OfflineKitRiskProviderInfo>
|
||||||
|
{
|
||||||
|
new("cisa-kev", "https://cisa.gov/kev", "2025-01-15", Optional: false)
|
||||||
|
},
|
||||||
|
RootHash: "abc123",
|
||||||
|
BundleBytes: originalBytes,
|
||||||
|
CreatedAt: _timeProvider.GetUtcNow());
|
||||||
|
|
||||||
|
var result = _packager.AddRiskBundle(_tempDir, request);
|
||||||
|
|
||||||
|
var writtenBytes = File.ReadAllBytes(Path.Combine(_tempDir, result.ArtifactPath));
|
||||||
|
Assert.Equal(originalBytes, writtenBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddRiskBundle_RejectsOverwrite()
|
||||||
|
{
|
||||||
|
var request = CreateTestRiskBundleRequest();
|
||||||
|
|
||||||
|
// First write succeeds
|
||||||
|
var result1 = _packager.AddRiskBundle(_tempDir, request);
|
||||||
|
Assert.True(result1.Success);
|
||||||
|
|
||||||
|
// Second write fails (immutability)
|
||||||
|
var result2 = _packager.AddRiskBundle(_tempDir, request);
|
||||||
|
Assert.False(result2.Success);
|
||||||
|
Assert.Contains("immutable", result2.ErrorMessage, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CreateAttestationEntry_HasCorrectKind()
|
public void CreateAttestationEntry_HasCorrectKind()
|
||||||
{
|
{
|
||||||
@@ -169,6 +219,54 @@ public sealed class OfflineKitPackagerTests : IDisposable
|
|||||||
Assert.Equal("bootstrap-pack", entry.Kind);
|
Assert.Equal("bootstrap-pack", entry.Kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateRiskBundleEntry_HasCorrectKind()
|
||||||
|
{
|
||||||
|
var request = CreateTestRiskBundleRequest();
|
||||||
|
|
||||||
|
var entry = _packager.CreateRiskBundleEntry(request, "sha256hash");
|
||||||
|
|
||||||
|
Assert.Equal("risk-bundle", entry.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateRiskBundleEntry_HasCorrectPaths()
|
||||||
|
{
|
||||||
|
var request = CreateTestRiskBundleRequest();
|
||||||
|
|
||||||
|
var entry = _packager.CreateRiskBundleEntry(request, "sha256hash");
|
||||||
|
|
||||||
|
Assert.Equal("risk-bundles/export-risk-bundle-v1.tgz", entry.Artifact);
|
||||||
|
Assert.Equal("checksums/risk-bundles/export-risk-bundle-v1.tgz.sha256", entry.Checksum);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateRiskBundleEntry_IncludesProviderInfo()
|
||||||
|
{
|
||||||
|
var providers = new List<OfflineKitRiskProviderInfo>
|
||||||
|
{
|
||||||
|
new("cisa-kev", "https://cisa.gov/kev", "2025-01-15", Optional: false),
|
||||||
|
new("nvd", "https://nvd.nist.gov", "2025-01-15", Optional: true)
|
||||||
|
};
|
||||||
|
var request = new OfflineKitRiskBundleRequest(
|
||||||
|
KitId: "kit-001",
|
||||||
|
ExportId: Guid.NewGuid().ToString(),
|
||||||
|
BundleId: Guid.NewGuid().ToString(),
|
||||||
|
InputsHash: "inputs-hash-001",
|
||||||
|
Providers: providers,
|
||||||
|
RootHash: "test-root-hash",
|
||||||
|
BundleBytes: Encoding.UTF8.GetBytes("test-risk-bundle"),
|
||||||
|
CreatedAt: _timeProvider.GetUtcNow());
|
||||||
|
|
||||||
|
var entry = _packager.CreateRiskBundleEntry(request, "sha256hash");
|
||||||
|
|
||||||
|
Assert.Equal(2, entry.Providers.Count);
|
||||||
|
Assert.Equal("cisa-kev", entry.Providers[0].ProviderId);
|
||||||
|
Assert.False(entry.Providers[0].Optional);
|
||||||
|
Assert.Equal("nvd", entry.Providers[1].ProviderId);
|
||||||
|
Assert.True(entry.Providers[1].Optional);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void WriteManifest_CreatesManifestFile()
|
public void WriteManifest_CreatesManifestFile()
|
||||||
{
|
{
|
||||||
@@ -276,18 +374,22 @@ public sealed class OfflineKitPackagerTests : IDisposable
|
|||||||
var attestationRequest = CreateTestAttestationRequest();
|
var attestationRequest = CreateTestAttestationRequest();
|
||||||
var mirrorRequest = CreateTestMirrorRequest();
|
var mirrorRequest = CreateTestMirrorRequest();
|
||||||
var bootstrapRequest = CreateTestBootstrapRequest();
|
var bootstrapRequest = CreateTestBootstrapRequest();
|
||||||
|
var riskBundleRequest = CreateTestRiskBundleRequest();
|
||||||
|
|
||||||
var attestResult = _packager.AddAttestationBundle(_tempDir, attestationRequest);
|
var attestResult = _packager.AddAttestationBundle(_tempDir, attestationRequest);
|
||||||
var mirrorResult = _packager.AddMirrorBundle(_tempDir, mirrorRequest);
|
var mirrorResult = _packager.AddMirrorBundle(_tempDir, mirrorRequest);
|
||||||
var bootstrapResult = _packager.AddBootstrapPack(_tempDir, bootstrapRequest);
|
var bootstrapResult = _packager.AddBootstrapPack(_tempDir, bootstrapRequest);
|
||||||
|
var riskResult = _packager.AddRiskBundle(_tempDir, riskBundleRequest);
|
||||||
|
|
||||||
// Verify directory structure
|
// Verify directory structure
|
||||||
Assert.True(Directory.Exists(Path.Combine(_tempDir, "attestations")));
|
Assert.True(Directory.Exists(Path.Combine(_tempDir, "attestations")));
|
||||||
Assert.True(Directory.Exists(Path.Combine(_tempDir, "mirrors")));
|
Assert.True(Directory.Exists(Path.Combine(_tempDir, "mirrors")));
|
||||||
Assert.True(Directory.Exists(Path.Combine(_tempDir, "bootstrap")));
|
Assert.True(Directory.Exists(Path.Combine(_tempDir, "bootstrap")));
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(_tempDir, "risk-bundles")));
|
||||||
Assert.True(Directory.Exists(Path.Combine(_tempDir, "checksums", "attestations")));
|
Assert.True(Directory.Exists(Path.Combine(_tempDir, "checksums", "attestations")));
|
||||||
Assert.True(Directory.Exists(Path.Combine(_tempDir, "checksums", "mirrors")));
|
Assert.True(Directory.Exists(Path.Combine(_tempDir, "checksums", "mirrors")));
|
||||||
Assert.True(Directory.Exists(Path.Combine(_tempDir, "checksums", "bootstrap")));
|
Assert.True(Directory.Exists(Path.Combine(_tempDir, "checksums", "bootstrap")));
|
||||||
|
Assert.True(Directory.Exists(Path.Combine(_tempDir, "checksums", "risk-bundles")));
|
||||||
}
|
}
|
||||||
|
|
||||||
private OfflineKitAttestationRequest CreateTestAttestationRequest()
|
private OfflineKitAttestationRequest CreateTestAttestationRequest()
|
||||||
@@ -323,4 +425,21 @@ public sealed class OfflineKitPackagerTests : IDisposable
|
|||||||
BundleBytes: Encoding.UTF8.GetBytes("test-bootstrap-pack"),
|
BundleBytes: Encoding.UTF8.GetBytes("test-bootstrap-pack"),
|
||||||
CreatedAt: _timeProvider.GetUtcNow());
|
CreatedAt: _timeProvider.GetUtcNow());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private OfflineKitRiskBundleRequest CreateTestRiskBundleRequest()
|
||||||
|
{
|
||||||
|
return new OfflineKitRiskBundleRequest(
|
||||||
|
KitId: "kit-001",
|
||||||
|
ExportId: Guid.NewGuid().ToString(),
|
||||||
|
BundleId: Guid.NewGuid().ToString(),
|
||||||
|
InputsHash: "test-inputs-hash",
|
||||||
|
Providers: new List<OfflineKitRiskProviderInfo>
|
||||||
|
{
|
||||||
|
new("cisa-kev", "https://cisa.gov/kev", "2025-01-15", Optional: false),
|
||||||
|
new("nvd", "https://nvd.nist.gov", "2025-01-15", Optional: true)
|
||||||
|
},
|
||||||
|
RootHash: "test-root-hash",
|
||||||
|
BundleBytes: Encoding.UTF8.GetBytes("test-risk-bundle"),
|
||||||
|
CreatedAt: _timeProvider.GetUtcNow());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using StellaOps.ExportCenter.WebService.EvidenceLocker;
|
|||||||
using StellaOps.ExportCenter.WebService.Attestation;
|
using StellaOps.ExportCenter.WebService.Attestation;
|
||||||
using StellaOps.ExportCenter.WebService.Incident;
|
using StellaOps.ExportCenter.WebService.Incident;
|
||||||
using StellaOps.ExportCenter.WebService.RiskBundle;
|
using StellaOps.ExportCenter.WebService.RiskBundle;
|
||||||
|
using StellaOps.ExportCenter.WebService.SimulationExport;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -67,6 +68,9 @@ builder.Services.AddExportIncidentManagement();
|
|||||||
// Risk bundle job handler
|
// Risk bundle job handler
|
||||||
builder.Services.AddRiskBundleJobHandler();
|
builder.Services.AddRiskBundleJobHandler();
|
||||||
|
|
||||||
|
// Simulation export services
|
||||||
|
builder.Services.AddSimulationExport();
|
||||||
|
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
@@ -95,6 +99,9 @@ app.MapIncidentEndpoints();
|
|||||||
// Risk bundle endpoints
|
// Risk bundle endpoints
|
||||||
app.MapRiskBundleEndpoints();
|
app.MapRiskBundleEndpoints();
|
||||||
|
|
||||||
|
// Simulation export endpoints
|
||||||
|
app.MapSimulationExportEndpoints();
|
||||||
|
|
||||||
// Legacy exports endpoints (deprecated, use /v1/exports/* instead)
|
// Legacy exports endpoints (deprecated, use /v1/exports/* instead)
|
||||||
app.MapGet("/exports", () => Results.Ok(Array.Empty<object>()))
|
app.MapGet("/exports", () => Results.Ok(Array.Empty<object>()))
|
||||||
.RequireAuthorization(StellaOpsResourceServerPolicies.ExportViewer)
|
.RequireAuthorization(StellaOpsResourceServerPolicies.ExportViewer)
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
namespace StellaOps.ExportCenter.WebService.SimulationExport;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for exporting simulation reports.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISimulationReportExporter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets available simulations for export.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tenantId">Optional tenant ID filter.</param>
|
||||||
|
/// <param name="limit">Maximum number of simulations to return.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Available simulations response.</returns>
|
||||||
|
Task<AvailableSimulationsResponse> GetAvailableSimulationsAsync(
|
||||||
|
string? tenantId,
|
||||||
|
int limit = 50,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exports a simulation report.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Export request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Export result.</returns>
|
||||||
|
Task<SimulationExportResult> ExportAsync(
|
||||||
|
SimulationExportRequest request,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an export document by ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exportId">Export identifier.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Export document or null if not found.</returns>
|
||||||
|
Task<SimulationExportDocument?> GetExportDocumentAsync(
|
||||||
|
string exportId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Streams an export in NDJSON format.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Export request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Async enumerable of export lines.</returns>
|
||||||
|
IAsyncEnumerable<SimulationExportLine> StreamExportAsync(
|
||||||
|
SimulationExportRequest request,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the CSV export for a simulation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="simulationId">Simulation ID.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>CSV content as a string.</returns>
|
||||||
|
Task<string?> GetCsvExportAsync(
|
||||||
|
string simulationId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using StellaOps.Auth.ServerIntegration;
|
||||||
|
|
||||||
|
namespace StellaOps.ExportCenter.WebService.SimulationExport;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for mapping simulation export endpoints.
|
||||||
|
/// </summary>
|
||||||
|
public static class SimulationExportEndpoints
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions NdjsonOptions = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
WriteIndented = false
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps simulation export endpoints to the application.
|
||||||
|
/// </summary>
|
||||||
|
public static WebApplication MapSimulationExportEndpoints(this WebApplication app)
|
||||||
|
{
|
||||||
|
var group = app.MapGroup("/v1/exports/simulations")
|
||||||
|
.WithTags("Simulation Exports")
|
||||||
|
.RequireAuthorization(StellaOpsResourceServerPolicies.ExportViewer);
|
||||||
|
|
||||||
|
// GET /v1/exports/simulations - List available simulations
|
||||||
|
group.MapGet("", GetAvailableSimulationsAsync)
|
||||||
|
.WithName("GetAvailableSimulations")
|
||||||
|
.WithSummary("List available simulations for export")
|
||||||
|
.WithDescription("Returns simulations that can be exported, optionally filtered by tenant.")
|
||||||
|
.Produces<AvailableSimulationsResponse>(StatusCodes.Status200OK);
|
||||||
|
|
||||||
|
// POST /v1/exports/simulations - Export a simulation
|
||||||
|
group.MapPost("", ExportSimulationAsync)
|
||||||
|
.RequireAuthorization(StellaOpsResourceServerPolicies.ExportOperator)
|
||||||
|
.WithName("ExportSimulation")
|
||||||
|
.WithSummary("Export a simulation report")
|
||||||
|
.WithDescription("Exports a simulation report with scored data and explainability snapshots.")
|
||||||
|
.Produces<SimulationExportResult>(StatusCodes.Status202Accepted)
|
||||||
|
.Produces<SimulationExportResult>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
// GET /v1/exports/simulations/{exportId} - Get export document
|
||||||
|
group.MapGet("/{exportId}", GetExportDocumentAsync)
|
||||||
|
.WithName("GetSimulationExportDocument")
|
||||||
|
.WithSummary("Get exported simulation document")
|
||||||
|
.WithDescription("Returns the exported simulation document in JSON format.")
|
||||||
|
.Produces<SimulationExportDocument>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
// GET /v1/exports/simulations/{simulationId}/stream - Stream export as NDJSON
|
||||||
|
group.MapGet("/{simulationId}/stream", StreamExportAsync)
|
||||||
|
.WithName("StreamSimulationExport")
|
||||||
|
.WithSummary("Stream simulation export as NDJSON")
|
||||||
|
.WithDescription("Streams the simulation export in NDJSON format for large datasets.")
|
||||||
|
.Produces(StatusCodes.Status200OK, contentType: "application/x-ndjson")
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
// GET /v1/exports/simulations/{simulationId}/csv - Get CSV export
|
||||||
|
group.MapGet("/{simulationId}/csv", GetCsvExportAsync)
|
||||||
|
.WithName("GetSimulationCsvExport")
|
||||||
|
.WithSummary("Get simulation export as CSV")
|
||||||
|
.WithDescription("Returns the simulation finding scores in CSV format.")
|
||||||
|
.Produces(StatusCodes.Status200OK, contentType: "text/csv")
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Ok<AvailableSimulationsResponse>> GetAvailableSimulationsAsync(
|
||||||
|
[FromQuery] string? tenantId,
|
||||||
|
[FromQuery] int? limit,
|
||||||
|
[FromServices] ISimulationReportExporter exporter,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var simulations = await exporter.GetAvailableSimulationsAsync(tenantId, limit ?? 50, cancellationToken);
|
||||||
|
return TypedResults.Ok(simulations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Results<Accepted<SimulationExportResult>, BadRequest<SimulationExportResult>, NotFound>> ExportSimulationAsync(
|
||||||
|
[FromBody] SimulationExportRequest request,
|
||||||
|
[FromServices] ISimulationReportExporter exporter,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await exporter.ExportAsync(request, cancellationToken);
|
||||||
|
|
||||||
|
if (!result.Success && result.ErrorMessage?.Contains("not found") == true)
|
||||||
|
{
|
||||||
|
return TypedResults.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
return TypedResults.BadRequest(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypedResults.Accepted($"/v1/exports/simulations/{result.ExportId}", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Results<Ok<SimulationExportDocument>, NotFound>> GetExportDocumentAsync(
|
||||||
|
string exportId,
|
||||||
|
[FromServices] ISimulationReportExporter exporter,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var document = await exporter.GetExportDocumentAsync(exportId, cancellationToken);
|
||||||
|
if (document is null)
|
||||||
|
{
|
||||||
|
return TypedResults.NotFound();
|
||||||
|
}
|
||||||
|
return TypedResults.Ok(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> StreamExportAsync(
|
||||||
|
string simulationId,
|
||||||
|
[FromQuery] bool? includeScoredData,
|
||||||
|
[FromQuery] bool? includeExplainability,
|
||||||
|
[FromQuery] bool? includeDistribution,
|
||||||
|
[FromServices] ISimulationReportExporter exporter,
|
||||||
|
HttpContext httpContext,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var request = new SimulationExportRequest
|
||||||
|
{
|
||||||
|
SimulationId = simulationId,
|
||||||
|
Format = SimulationExportFormat.Ndjson,
|
||||||
|
IncludeScoredData = includeScoredData ?? true,
|
||||||
|
IncludeExplainability = includeExplainability ?? true,
|
||||||
|
IncludeDistribution = includeDistribution ?? true
|
||||||
|
};
|
||||||
|
|
||||||
|
httpContext.Response.ContentType = "application/x-ndjson";
|
||||||
|
httpContext.Response.Headers.ContentDisposition = $"attachment; filename=\"simulation-{simulationId}.ndjson\"";
|
||||||
|
|
||||||
|
await foreach (var line in exporter.StreamExportAsync(request, cancellationToken))
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(line, NdjsonOptions);
|
||||||
|
await httpContext.Response.WriteAsync(json + "\n", cancellationToken);
|
||||||
|
await httpContext.Response.Body.FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> GetCsvExportAsync(
|
||||||
|
string simulationId,
|
||||||
|
[FromServices] ISimulationReportExporter exporter,
|
||||||
|
HttpContext httpContext,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var csv = await exporter.GetCsvExportAsync(simulationId, cancellationToken);
|
||||||
|
if (csv is null)
|
||||||
|
{
|
||||||
|
return TypedResults.NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
httpContext.Response.ContentType = "text/csv";
|
||||||
|
httpContext.Response.Headers.ContentDisposition = $"attachment; filename=\"simulation-{simulationId}.csv\"";
|
||||||
|
|
||||||
|
await httpContext.Response.WriteAsync(csv, cancellationToken);
|
||||||
|
return Results.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,544 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace StellaOps.ExportCenter.WebService.SimulationExport;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request to export a simulation report.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record SimulationExportRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Simulation ID to export.
|
||||||
|
/// </summary>
|
||||||
|
public required string SimulationId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tenant identifier.
|
||||||
|
/// </summary>
|
||||||
|
public string? TenantId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Correlation ID for tracing.
|
||||||
|
/// </summary>
|
||||||
|
public string? CorrelationId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Export format.
|
||||||
|
/// </summary>
|
||||||
|
public SimulationExportFormat Format { get; init; } = SimulationExportFormat.Json;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Include scored data (finding scores, aggregate metrics).
|
||||||
|
/// </summary>
|
||||||
|
public bool IncludeScoredData { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Include explainability snapshots (signal analysis, override analysis).
|
||||||
|
/// </summary>
|
||||||
|
public bool IncludeExplainability { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Include distribution analysis.
|
||||||
|
/// </summary>
|
||||||
|
public bool IncludeDistribution { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Include component breakdown.
|
||||||
|
/// </summary>
|
||||||
|
public bool IncludeComponentBreakdown { get; init; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Include trend analysis (if available).
|
||||||
|
/// </summary>
|
||||||
|
public bool IncludeTrends { get; init; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of top movers to include.
|
||||||
|
/// </summary>
|
||||||
|
public int TopMoversLimit { get; init; } = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of top signal contributors to include.
|
||||||
|
/// </summary>
|
||||||
|
public int TopContributorsLimit { get; init; } = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Export format for simulation reports.
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public enum SimulationExportFormat
|
||||||
|
{
|
||||||
|
/// <summary>JSON format (single document).</summary>
|
||||||
|
Json = 0,
|
||||||
|
|
||||||
|
/// <summary>NDJSON format (streaming).</summary>
|
||||||
|
Ndjson = 1,
|
||||||
|
|
||||||
|
/// <summary>CSV format (tabular findings data).</summary>
|
||||||
|
Csv = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of a simulation export request.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record SimulationExportResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the export was successful.
|
||||||
|
/// </summary>
|
||||||
|
public required bool Success { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Export identifier.
|
||||||
|
/// </summary>
|
||||||
|
public required string ExportId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Simulation ID that was exported.
|
||||||
|
/// </summary>
|
||||||
|
public required string SimulationId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Export format.
|
||||||
|
/// </summary>
|
||||||
|
public required SimulationExportFormat Format { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp when the export was created.
|
||||||
|
/// </summary>
|
||||||
|
public required DateTimeOffset CreatedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Storage key for the exported file.
|
||||||
|
/// </summary>
|
||||||
|
public string? StorageKey { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Content type of the exported file.
|
||||||
|
/// </summary>
|
||||||
|
public string? ContentType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Size of the exported file in bytes.
|
||||||
|
/// </summary>
|
||||||
|
public long? SizeBytes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error message if export failed.
|
||||||
|
/// </summary>
|
||||||
|
public string? ErrorMessage { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Export summary.
|
||||||
|
/// </summary>
|
||||||
|
public SimulationExportSummary? Summary { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Summary of exported simulation data.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record SimulationExportSummary
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Profile ID used in the simulation.
|
||||||
|
/// </summary>
|
||||||
|
public required string ProfileId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Profile version.
|
||||||
|
/// </summary>
|
||||||
|
public required string ProfileVersion { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of findings scored.
|
||||||
|
/// </summary>
|
||||||
|
public required int TotalFindings { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Severity breakdown.
|
||||||
|
/// </summary>
|
||||||
|
public required SeverityBreakdown SeverityBreakdown { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggregate risk metrics.
|
||||||
|
/// </summary>
|
||||||
|
public required AggregateMetricsSummary AggregateMetrics { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether explainability data was included.
|
||||||
|
/// </summary>
|
||||||
|
public required bool HasExplainability { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Simulation timestamp.
|
||||||
|
/// </summary>
|
||||||
|
public required DateTimeOffset SimulationTimestamp { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determinism hash for reproducibility.
|
||||||
|
/// </summary>
|
||||||
|
public string? DeterminismHash { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Breakdown by severity level.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record SeverityBreakdown
|
||||||
|
{
|
||||||
|
public int Critical { get; init; }
|
||||||
|
public int High { get; init; }
|
||||||
|
public int Medium { get; init; }
|
||||||
|
public int Low { get; init; }
|
||||||
|
public int Informational { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Summary of aggregate metrics.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record AggregateMetricsSummary
|
||||||
|
{
|
||||||
|
public double MeanScore { get; init; }
|
||||||
|
public double MedianScore { get; init; }
|
||||||
|
public double MaxScore { get; init; }
|
||||||
|
public double MinScore { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exported simulation report document.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record SimulationExportDocument
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Export metadata.
|
||||||
|
/// </summary>
|
||||||
|
public required SimulationExportMetadata Metadata { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scored data section.
|
||||||
|
/// </summary>
|
||||||
|
public ScoredDataSection? ScoredData { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Explainability section.
|
||||||
|
/// </summary>
|
||||||
|
public ExplainabilitySection? Explainability { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Distribution section.
|
||||||
|
/// </summary>
|
||||||
|
public DistributionSection? Distribution { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Component breakdown section.
|
||||||
|
/// </summary>
|
||||||
|
public ComponentSection? Components { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trend analysis section.
|
||||||
|
/// </summary>
|
||||||
|
public TrendSection? Trends { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Export metadata.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record SimulationExportMetadata
|
||||||
|
{
|
||||||
|
public required string ExportId { get; init; }
|
||||||
|
public required string SimulationId { get; init; }
|
||||||
|
public required string ProfileId { get; init; }
|
||||||
|
public required string ProfileVersion { get; init; }
|
||||||
|
public required string ProfileHash { get; init; }
|
||||||
|
public required DateTimeOffset SimulationTimestamp { get; init; }
|
||||||
|
public required DateTimeOffset ExportTimestamp { get; init; }
|
||||||
|
public required string ExportFormat { get; init; }
|
||||||
|
public required string SchemaVersion { get; init; }
|
||||||
|
public string? TenantId { get; init; }
|
||||||
|
public string? CorrelationId { get; init; }
|
||||||
|
public string? DeterminismHash { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scored data section of the export.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ScoredDataSection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Individual finding scores.
|
||||||
|
/// </summary>
|
||||||
|
public required IReadOnlyList<ExportedFindingScore> FindingScores { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggregate metrics.
|
||||||
|
/// </summary>
|
||||||
|
public required ExportedAggregateMetrics AggregateMetrics { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Top movers (highest risk findings).
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<ExportedTopMover>? TopMovers { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exported finding score.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExportedFindingScore
|
||||||
|
{
|
||||||
|
public required string FindingId { get; init; }
|
||||||
|
public required double RawScore { get; init; }
|
||||||
|
public required double NormalizedScore { get; init; }
|
||||||
|
public required string Severity { get; init; }
|
||||||
|
public required string RecommendedAction { get; init; }
|
||||||
|
public string? ComponentPurl { get; init; }
|
||||||
|
public string? AdvisoryId { get; init; }
|
||||||
|
public IReadOnlyList<ExportedContribution>? Contributions { get; init; }
|
||||||
|
public IReadOnlyList<ExportedOverride>? OverridesApplied { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exported signal contribution.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExportedContribution
|
||||||
|
{
|
||||||
|
public required string SignalName { get; init; }
|
||||||
|
public object? SignalValue { get; init; }
|
||||||
|
public required double Weight { get; init; }
|
||||||
|
public required double Contribution { get; init; }
|
||||||
|
public required double ContributionPercentage { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exported applied override.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExportedOverride
|
||||||
|
{
|
||||||
|
public required string OverrideType { get; init; }
|
||||||
|
public object? OriginalValue { get; init; }
|
||||||
|
public object? AppliedValue { get; init; }
|
||||||
|
public string? Reason { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exported aggregate metrics.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExportedAggregateMetrics
|
||||||
|
{
|
||||||
|
public required int TotalFindings { get; init; }
|
||||||
|
public required double MeanScore { get; init; }
|
||||||
|
public required double MedianScore { get; init; }
|
||||||
|
public required double StdDeviation { get; init; }
|
||||||
|
public required double MaxScore { get; init; }
|
||||||
|
public required double MinScore { get; init; }
|
||||||
|
public required int CriticalCount { get; init; }
|
||||||
|
public required int HighCount { get; init; }
|
||||||
|
public required int MediumCount { get; init; }
|
||||||
|
public required int LowCount { get; init; }
|
||||||
|
public required int InformationalCount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exported top mover.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExportedTopMover
|
||||||
|
{
|
||||||
|
public required string FindingId { get; init; }
|
||||||
|
public string? ComponentPurl { get; init; }
|
||||||
|
public required double Score { get; init; }
|
||||||
|
public required string Severity { get; init; }
|
||||||
|
public required string PrimaryDriver { get; init; }
|
||||||
|
public required double DriverContribution { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Explainability section of the export.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExplainabilitySection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Signal analysis.
|
||||||
|
/// </summary>
|
||||||
|
public required ExportedSignalAnalysis SignalAnalysis { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override analysis.
|
||||||
|
/// </summary>
|
||||||
|
public required ExportedOverrideAnalysis OverrideAnalysis { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exported signal analysis.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExportedSignalAnalysis
|
||||||
|
{
|
||||||
|
public required int TotalSignals { get; init; }
|
||||||
|
public required int SignalsUsed { get; init; }
|
||||||
|
public required int SignalsMissing { get; init; }
|
||||||
|
public required double SignalCoverage { get; init; }
|
||||||
|
public IReadOnlyList<ExportedSignalContributor>? TopContributors { get; init; }
|
||||||
|
public IReadOnlyList<string>? MostImpactfulMissing { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exported signal contributor.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExportedSignalContributor
|
||||||
|
{
|
||||||
|
public required string SignalName { get; init; }
|
||||||
|
public required double TotalContribution { get; init; }
|
||||||
|
public required double ContributionPercentage { get; init; }
|
||||||
|
public required double AvgValue { get; init; }
|
||||||
|
public required double Weight { get; init; }
|
||||||
|
public required string ImpactDirection { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exported override analysis.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExportedOverrideAnalysis
|
||||||
|
{
|
||||||
|
public required int TotalOverridesEvaluated { get; init; }
|
||||||
|
public required int SeverityOverridesApplied { get; init; }
|
||||||
|
public required int DecisionOverridesApplied { get; init; }
|
||||||
|
public required double OverrideApplicationRate { get; init; }
|
||||||
|
public int? OverrideConflictsCount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Distribution section of the export.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record DistributionSection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Score buckets.
|
||||||
|
/// </summary>
|
||||||
|
public required IReadOnlyList<ExportedScoreBucket> ScoreBuckets { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Percentiles.
|
||||||
|
/// </summary>
|
||||||
|
public required IReadOnlyDictionary<string, double> Percentiles { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Severity breakdown.
|
||||||
|
/// </summary>
|
||||||
|
public required IReadOnlyDictionary<string, int> SeverityBreakdown { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Action breakdown.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyDictionary<string, int>? ActionBreakdown { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exported score bucket.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExportedScoreBucket
|
||||||
|
{
|
||||||
|
public required double RangeMin { get; init; }
|
||||||
|
public required double RangeMax { get; init; }
|
||||||
|
public required string Label { get; init; }
|
||||||
|
public required int Count { get; init; }
|
||||||
|
public required double Percentage { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Component section of the export.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ComponentSection
|
||||||
|
{
|
||||||
|
public required int TotalComponents { get; init; }
|
||||||
|
public required int ComponentsWithFindings { get; init; }
|
||||||
|
public IReadOnlyList<ExportedComponentRisk>? TopRiskComponents { get; init; }
|
||||||
|
public IReadOnlyDictionary<string, ExportedEcosystemSummary>? EcosystemBreakdown { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exported component risk.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExportedComponentRisk
|
||||||
|
{
|
||||||
|
public required string ComponentPurl { get; init; }
|
||||||
|
public required int FindingCount { get; init; }
|
||||||
|
public required double MaxScore { get; init; }
|
||||||
|
public required double AvgScore { get; init; }
|
||||||
|
public required string HighestSeverity { get; init; }
|
||||||
|
public required string RecommendedAction { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exported ecosystem summary.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExportedEcosystemSummary
|
||||||
|
{
|
||||||
|
public required string Ecosystem { get; init; }
|
||||||
|
public required int ComponentCount { get; init; }
|
||||||
|
public required int FindingCount { get; init; }
|
||||||
|
public required double AvgScore { get; init; }
|
||||||
|
public required int CriticalCount { get; init; }
|
||||||
|
public required int HighCount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trend section of the export.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record TrendSection
|
||||||
|
{
|
||||||
|
public required string ComparisonType { get; init; }
|
||||||
|
public required ExportedTrendMetric ScoreTrend { get; init; }
|
||||||
|
public required ExportedTrendMetric SeverityTrend { get; init; }
|
||||||
|
public required ExportedTrendMetric ActionTrend { get; init; }
|
||||||
|
public required int FindingsImproved { get; init; }
|
||||||
|
public required int FindingsWorsened { get; init; }
|
||||||
|
public required int FindingsUnchanged { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exported trend metric.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ExportedTrendMetric
|
||||||
|
{
|
||||||
|
public required string Direction { get; init; }
|
||||||
|
public required double Magnitude { get; init; }
|
||||||
|
public required double PercentageChange { get; init; }
|
||||||
|
public required bool IsSignificant { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// NDJSON line for streaming export.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record SimulationExportLine
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Line type.
|
||||||
|
/// </summary>
|
||||||
|
public required string Type { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Line data.
|
||||||
|
/// </summary>
|
||||||
|
public required object Data { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Available simulation for export listing.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record AvailableSimulation
|
||||||
|
{
|
||||||
|
public required string SimulationId { get; init; }
|
||||||
|
public required string ProfileId { get; init; }
|
||||||
|
public required string ProfileVersion { get; init; }
|
||||||
|
public required DateTimeOffset Timestamp { get; init; }
|
||||||
|
public required int TotalFindings { get; init; }
|
||||||
|
public required string Status { get; init; }
|
||||||
|
public string? TenantId { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response listing available simulations for export.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record AvailableSimulationsResponse
|
||||||
|
{
|
||||||
|
public required IReadOnlyList<AvailableSimulation> Simulations { get; init; }
|
||||||
|
public required int TotalCount { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
|
namespace StellaOps.ExportCenter.WebService.SimulationExport;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for registering simulation export services.
|
||||||
|
/// </summary>
|
||||||
|
public static class SimulationExportServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds simulation report export services to the service collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The service collection.</param>
|
||||||
|
/// <returns>The service collection for chaining.</returns>
|
||||||
|
public static IServiceCollection AddSimulationExport(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
|
||||||
|
// Register TimeProvider if not already registered
|
||||||
|
services.TryAddSingleton(TimeProvider.System);
|
||||||
|
|
||||||
|
// Register the exporter
|
||||||
|
services.TryAddSingleton<ISimulationReportExporter, SimulationReportExporter>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,655 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using StellaOps.ExportCenter.WebService.Telemetry;
|
||||||
|
|
||||||
|
namespace StellaOps.ExportCenter.WebService.SimulationExport;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implementation of simulation report exporter.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SimulationReportExporter : ISimulationReportExporter
|
||||||
|
{
|
||||||
|
private const string SchemaVersion = "1.0.0";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions CompactOptions = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
WriteIndented = false
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
private readonly ILogger<SimulationReportExporter> _logger;
|
||||||
|
|
||||||
|
// In-memory stores (would be replaced with persistent storage in production)
|
||||||
|
private readonly ConcurrentDictionary<string, SimulationExportDocument> _exports = new();
|
||||||
|
private readonly ConcurrentDictionary<string, SimulatedSimulationResult> _simulations = new();
|
||||||
|
|
||||||
|
public SimulationReportExporter(
|
||||||
|
TimeProvider timeProvider,
|
||||||
|
ILogger<SimulationReportExporter> logger)
|
||||||
|
{
|
||||||
|
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
|
||||||
|
// Initialize with sample simulations for demonstration
|
||||||
|
InitializeSampleSimulations();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<AvailableSimulationsResponse> GetAvailableSimulationsAsync(
|
||||||
|
string? tenantId,
|
||||||
|
int limit = 50,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var query = _simulations.Values.AsEnumerable();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(tenantId))
|
||||||
|
{
|
||||||
|
query = query.Where(s => string.Equals(s.TenantId, tenantId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
var simulations = query
|
||||||
|
.OrderByDescending(s => s.Timestamp)
|
||||||
|
.Take(Math.Min(limit, 100))
|
||||||
|
.Select(s => new AvailableSimulation
|
||||||
|
{
|
||||||
|
SimulationId = s.SimulationId,
|
||||||
|
ProfileId = s.ProfileId,
|
||||||
|
ProfileVersion = s.ProfileVersion,
|
||||||
|
Timestamp = s.Timestamp,
|
||||||
|
TotalFindings = s.TotalFindings,
|
||||||
|
Status = "completed",
|
||||||
|
TenantId = s.TenantId
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Task.FromResult(new AvailableSimulationsResponse
|
||||||
|
{
|
||||||
|
Simulations = simulations,
|
||||||
|
TotalCount = _simulations.Count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SimulationExportResult> ExportAsync(
|
||||||
|
SimulationExportRequest request,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var now = _timeProvider.GetUtcNow();
|
||||||
|
var exportId = $"exp-{Guid.NewGuid():N}";
|
||||||
|
|
||||||
|
if (!_simulations.TryGetValue(request.SimulationId, out var simulation))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Simulation {SimulationId} not found for export", request.SimulationId);
|
||||||
|
|
||||||
|
return new SimulationExportResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ExportId = exportId,
|
||||||
|
SimulationId = request.SimulationId,
|
||||||
|
Format = request.Format,
|
||||||
|
CreatedAt = now,
|
||||||
|
ErrorMessage = $"Simulation '{request.SimulationId}' not found"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var document = BuildExportDocument(request, simulation, exportId, now);
|
||||||
|
_exports[exportId] = document;
|
||||||
|
|
||||||
|
var contentType = request.Format switch
|
||||||
|
{
|
||||||
|
SimulationExportFormat.Json => "application/json",
|
||||||
|
SimulationExportFormat.Ndjson => "application/x-ndjson",
|
||||||
|
SimulationExportFormat.Csv => "text/csv",
|
||||||
|
_ => "application/json"
|
||||||
|
};
|
||||||
|
|
||||||
|
var sizeBytes = EstimateSize(document, request.Format);
|
||||||
|
|
||||||
|
ExportTelemetry.SimulationExportsTotal.Add(1,
|
||||||
|
new KeyValuePair<string, object?>("format", request.Format.ToString().ToLowerInvariant()),
|
||||||
|
new KeyValuePair<string, object?>("tenant_id", request.TenantId ?? "unknown"));
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Exported simulation {SimulationId} as {ExportId} in {Format} format ({SizeBytes} bytes)",
|
||||||
|
request.SimulationId, exportId, request.Format, sizeBytes);
|
||||||
|
|
||||||
|
return new SimulationExportResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
ExportId = exportId,
|
||||||
|
SimulationId = request.SimulationId,
|
||||||
|
Format = request.Format,
|
||||||
|
CreatedAt = now,
|
||||||
|
StorageKey = $"exports/simulations/{exportId}",
|
||||||
|
ContentType = contentType,
|
||||||
|
SizeBytes = sizeBytes,
|
||||||
|
Summary = new SimulationExportSummary
|
||||||
|
{
|
||||||
|
ProfileId = simulation.ProfileId,
|
||||||
|
ProfileVersion = simulation.ProfileVersion,
|
||||||
|
TotalFindings = simulation.TotalFindings,
|
||||||
|
SeverityBreakdown = new SeverityBreakdown
|
||||||
|
{
|
||||||
|
Critical = simulation.CriticalCount,
|
||||||
|
High = simulation.HighCount,
|
||||||
|
Medium = simulation.MediumCount,
|
||||||
|
Low = simulation.LowCount,
|
||||||
|
Informational = simulation.InformationalCount
|
||||||
|
},
|
||||||
|
AggregateMetrics = new AggregateMetricsSummary
|
||||||
|
{
|
||||||
|
MeanScore = simulation.MeanScore,
|
||||||
|
MedianScore = simulation.MedianScore,
|
||||||
|
MaxScore = simulation.MaxScore,
|
||||||
|
MinScore = simulation.MinScore
|
||||||
|
},
|
||||||
|
HasExplainability = request.IncludeExplainability,
|
||||||
|
SimulationTimestamp = simulation.Timestamp,
|
||||||
|
DeterminismHash = simulation.DeterminismHash
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to export simulation {SimulationId}", request.SimulationId);
|
||||||
|
|
||||||
|
return new SimulationExportResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ExportId = exportId,
|
||||||
|
SimulationId = request.SimulationId,
|
||||||
|
Format = request.Format,
|
||||||
|
CreatedAt = now,
|
||||||
|
ErrorMessage = ex.Message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<SimulationExportDocument?> GetExportDocumentAsync(
|
||||||
|
string exportId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_exports.TryGetValue(exportId, out var document);
|
||||||
|
return Task.FromResult(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<SimulationExportLine> StreamExportAsync(
|
||||||
|
SimulationExportRequest request,
|
||||||
|
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|
||||||
|
if (!_simulations.TryGetValue(request.SimulationId, out var simulation))
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = _timeProvider.GetUtcNow();
|
||||||
|
var exportId = $"exp-{Guid.NewGuid():N}";
|
||||||
|
|
||||||
|
// Emit metadata first
|
||||||
|
yield return new SimulationExportLine
|
||||||
|
{
|
||||||
|
Type = "metadata",
|
||||||
|
Data = new SimulationExportMetadata
|
||||||
|
{
|
||||||
|
ExportId = exportId,
|
||||||
|
SimulationId = request.SimulationId,
|
||||||
|
ProfileId = simulation.ProfileId,
|
||||||
|
ProfileVersion = simulation.ProfileVersion,
|
||||||
|
ProfileHash = simulation.ProfileHash,
|
||||||
|
SimulationTimestamp = simulation.Timestamp,
|
||||||
|
ExportTimestamp = now,
|
||||||
|
ExportFormat = "ndjson",
|
||||||
|
SchemaVersion = SchemaVersion,
|
||||||
|
TenantId = request.TenantId,
|
||||||
|
CorrelationId = request.CorrelationId,
|
||||||
|
DeterminismHash = simulation.DeterminismHash
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
// Emit aggregate metrics
|
||||||
|
if (request.IncludeScoredData)
|
||||||
|
{
|
||||||
|
yield return new SimulationExportLine
|
||||||
|
{
|
||||||
|
Type = "aggregate_metrics",
|
||||||
|
Data = new ExportedAggregateMetrics
|
||||||
|
{
|
||||||
|
TotalFindings = simulation.TotalFindings,
|
||||||
|
MeanScore = simulation.MeanScore,
|
||||||
|
MedianScore = simulation.MedianScore,
|
||||||
|
StdDeviation = simulation.StdDeviation,
|
||||||
|
MaxScore = simulation.MaxScore,
|
||||||
|
MinScore = simulation.MinScore,
|
||||||
|
CriticalCount = simulation.CriticalCount,
|
||||||
|
HighCount = simulation.HighCount,
|
||||||
|
MediumCount = simulation.MediumCount,
|
||||||
|
LowCount = simulation.LowCount,
|
||||||
|
InformationalCount = simulation.InformationalCount
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emit individual finding scores
|
||||||
|
foreach (var finding in simulation.FindingScores.Take(100))
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
yield return new SimulationExportLine
|
||||||
|
{
|
||||||
|
Type = "finding_score",
|
||||||
|
Data = finding
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit top movers
|
||||||
|
foreach (var mover in simulation.TopMovers.Take(request.TopMoversLimit))
|
||||||
|
{
|
||||||
|
yield return new SimulationExportLine
|
||||||
|
{
|
||||||
|
Type = "top_mover",
|
||||||
|
Data = mover
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit explainability data
|
||||||
|
if (request.IncludeExplainability && simulation.SignalAnalysis is not null)
|
||||||
|
{
|
||||||
|
yield return new SimulationExportLine
|
||||||
|
{
|
||||||
|
Type = "signal_analysis",
|
||||||
|
Data = simulation.SignalAnalysis
|
||||||
|
};
|
||||||
|
|
||||||
|
yield return new SimulationExportLine
|
||||||
|
{
|
||||||
|
Type = "override_analysis",
|
||||||
|
Data = simulation.OverrideAnalysis
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit distribution
|
||||||
|
if (request.IncludeDistribution && simulation.Distribution is not null)
|
||||||
|
{
|
||||||
|
yield return new SimulationExportLine
|
||||||
|
{
|
||||||
|
Type = "distribution",
|
||||||
|
Data = simulation.Distribution
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit completion marker
|
||||||
|
yield return new SimulationExportLine
|
||||||
|
{
|
||||||
|
Type = "complete",
|
||||||
|
Data = new { exported_at = now.ToString("O") }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string?> GetCsvExportAsync(
|
||||||
|
string simulationId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (!_simulations.TryGetValue(simulationId, out var simulation))
|
||||||
|
{
|
||||||
|
return Task.FromResult<string?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var csv = new StringBuilder();
|
||||||
|
|
||||||
|
// Header
|
||||||
|
csv.AppendLine("finding_id,raw_score,normalized_score,severity,recommended_action,component_purl,advisory_id");
|
||||||
|
|
||||||
|
// Data rows
|
||||||
|
foreach (var finding in simulation.FindingScores)
|
||||||
|
{
|
||||||
|
csv.AppendLine(
|
||||||
|
$"\"{finding.FindingId}\"," +
|
||||||
|
$"{finding.RawScore:F4}," +
|
||||||
|
$"{finding.NormalizedScore:F4}," +
|
||||||
|
$"\"{finding.Severity}\"," +
|
||||||
|
$"\"{finding.RecommendedAction}\"," +
|
||||||
|
$"\"{finding.ComponentPurl ?? ""}\"," +
|
||||||
|
$"\"{finding.AdvisoryId ?? ""}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<string?>(csv.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimulationExportDocument BuildExportDocument(
|
||||||
|
SimulationExportRequest request,
|
||||||
|
SimulatedSimulationResult simulation,
|
||||||
|
string exportId,
|
||||||
|
DateTimeOffset now)
|
||||||
|
{
|
||||||
|
var metadata = new SimulationExportMetadata
|
||||||
|
{
|
||||||
|
ExportId = exportId,
|
||||||
|
SimulationId = request.SimulationId,
|
||||||
|
ProfileId = simulation.ProfileId,
|
||||||
|
ProfileVersion = simulation.ProfileVersion,
|
||||||
|
ProfileHash = simulation.ProfileHash,
|
||||||
|
SimulationTimestamp = simulation.Timestamp,
|
||||||
|
ExportTimestamp = now,
|
||||||
|
ExportFormat = request.Format.ToString().ToLowerInvariant(),
|
||||||
|
SchemaVersion = SchemaVersion,
|
||||||
|
TenantId = request.TenantId,
|
||||||
|
CorrelationId = request.CorrelationId,
|
||||||
|
DeterminismHash = simulation.DeterminismHash
|
||||||
|
};
|
||||||
|
|
||||||
|
ScoredDataSection? scoredData = null;
|
||||||
|
if (request.IncludeScoredData)
|
||||||
|
{
|
||||||
|
scoredData = new ScoredDataSection
|
||||||
|
{
|
||||||
|
FindingScores = simulation.FindingScores,
|
||||||
|
AggregateMetrics = new ExportedAggregateMetrics
|
||||||
|
{
|
||||||
|
TotalFindings = simulation.TotalFindings,
|
||||||
|
MeanScore = simulation.MeanScore,
|
||||||
|
MedianScore = simulation.MedianScore,
|
||||||
|
StdDeviation = simulation.StdDeviation,
|
||||||
|
MaxScore = simulation.MaxScore,
|
||||||
|
MinScore = simulation.MinScore,
|
||||||
|
CriticalCount = simulation.CriticalCount,
|
||||||
|
HighCount = simulation.HighCount,
|
||||||
|
MediumCount = simulation.MediumCount,
|
||||||
|
LowCount = simulation.LowCount,
|
||||||
|
InformationalCount = simulation.InformationalCount
|
||||||
|
},
|
||||||
|
TopMovers = simulation.TopMovers.Take(request.TopMoversLimit).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ExplainabilitySection? explainability = null;
|
||||||
|
if (request.IncludeExplainability && simulation.SignalAnalysis is not null)
|
||||||
|
{
|
||||||
|
explainability = new ExplainabilitySection
|
||||||
|
{
|
||||||
|
SignalAnalysis = simulation.SignalAnalysis,
|
||||||
|
OverrideAnalysis = simulation.OverrideAnalysis!
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
DistributionSection? distribution = null;
|
||||||
|
if (request.IncludeDistribution && simulation.Distribution is not null)
|
||||||
|
{
|
||||||
|
distribution = simulation.Distribution;
|
||||||
|
}
|
||||||
|
|
||||||
|
ComponentSection? components = null;
|
||||||
|
if (request.IncludeComponentBreakdown && simulation.ComponentBreakdown is not null)
|
||||||
|
{
|
||||||
|
components = simulation.ComponentBreakdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrendSection? trends = null;
|
||||||
|
if (request.IncludeTrends && simulation.Trends is not null)
|
||||||
|
{
|
||||||
|
trends = simulation.Trends;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SimulationExportDocument
|
||||||
|
{
|
||||||
|
Metadata = metadata,
|
||||||
|
ScoredData = scoredData,
|
||||||
|
Explainability = explainability,
|
||||||
|
Distribution = distribution,
|
||||||
|
Components = components,
|
||||||
|
Trends = trends
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long EstimateSize(SimulationExportDocument document, SimulationExportFormat format)
|
||||||
|
{
|
||||||
|
// Rough estimation
|
||||||
|
var json = JsonSerializer.Serialize(document, CompactOptions);
|
||||||
|
return format switch
|
||||||
|
{
|
||||||
|
SimulationExportFormat.Json => json.Length * 2, // UTF-8 with indentation
|
||||||
|
SimulationExportFormat.Ndjson => json.Length,
|
||||||
|
SimulationExportFormat.Csv => json.Length / 2,
|
||||||
|
_ => json.Length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeSampleSimulations()
|
||||||
|
{
|
||||||
|
var now = _timeProvider.GetUtcNow();
|
||||||
|
|
||||||
|
// Sample simulation 1
|
||||||
|
var sim1Id = "sim-001-" + Guid.NewGuid().ToString("N")[..8];
|
||||||
|
_simulations[sim1Id] = CreateSampleSimulation(sim1Id, "baseline-risk-v1", "1.0.0", now.AddHours(-2), 150);
|
||||||
|
|
||||||
|
// Sample simulation 2
|
||||||
|
var sim2Id = "sim-002-" + Guid.NewGuid().ToString("N")[..8];
|
||||||
|
_simulations[sim2Id] = CreateSampleSimulation(sim2Id, "strict-risk-v2", "2.1.0", now.AddHours(-1), 85);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimulatedSimulationResult CreateSampleSimulation(
|
||||||
|
string simulationId,
|
||||||
|
string profileId,
|
||||||
|
string profileVersion,
|
||||||
|
DateTimeOffset timestamp,
|
||||||
|
int findingCount)
|
||||||
|
{
|
||||||
|
var random = new Random(simulationId.GetHashCode());
|
||||||
|
var findings = new List<ExportedFindingScore>();
|
||||||
|
var severities = new[] { "critical", "high", "medium", "low", "informational" };
|
||||||
|
var actions = new[] { "upgrade", "patch", "monitor", "accept", "investigate" };
|
||||||
|
|
||||||
|
int critical = 0, high = 0, medium = 0, low = 0, info = 0;
|
||||||
|
var scores = new List<double>();
|
||||||
|
|
||||||
|
for (int i = 0; i < findingCount; i++)
|
||||||
|
{
|
||||||
|
var rawScore = random.NextDouble() * 100;
|
||||||
|
var normalizedScore = rawScore / 10.0;
|
||||||
|
var severity = severities[Math.Min((int)(rawScore / 20), 4)];
|
||||||
|
var action = actions[random.Next(actions.Length)];
|
||||||
|
|
||||||
|
scores.Add(normalizedScore);
|
||||||
|
|
||||||
|
switch (severity)
|
||||||
|
{
|
||||||
|
case "critical": critical++; break;
|
||||||
|
case "high": high++; break;
|
||||||
|
case "medium": medium++; break;
|
||||||
|
case "low": low++; break;
|
||||||
|
default: info++; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
findings.Add(new ExportedFindingScore
|
||||||
|
{
|
||||||
|
FindingId = $"FIND-{i + 1:D5}",
|
||||||
|
RawScore = rawScore,
|
||||||
|
NormalizedScore = normalizedScore,
|
||||||
|
Severity = severity,
|
||||||
|
RecommendedAction = action,
|
||||||
|
ComponentPurl = $"pkg:npm/example-package-{i % 20}@1.{i % 10}.0",
|
||||||
|
AdvisoryId = $"CVE-2024-{10000 + i}",
|
||||||
|
Contributions = i < 10 ? new List<ExportedContribution>
|
||||||
|
{
|
||||||
|
new() { SignalName = "cvss_base", SignalValue = rawScore / 10.0, Weight = 0.3, Contribution = rawScore * 0.3, ContributionPercentage = 30 },
|
||||||
|
new() { SignalName = "epss_score", SignalValue = random.NextDouble(), Weight = 0.2, Contribution = rawScore * 0.2, ContributionPercentage = 20 },
|
||||||
|
new() { SignalName = "kev_listed", SignalValue = random.Next(2) == 1, Weight = 0.25, Contribution = rawScore * 0.25, ContributionPercentage = 25 }
|
||||||
|
} : null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scores.Sort();
|
||||||
|
var mean = scores.Average();
|
||||||
|
var median = scores.Count % 2 == 0
|
||||||
|
? (scores[scores.Count / 2 - 1] + scores[scores.Count / 2]) / 2
|
||||||
|
: scores[scores.Count / 2];
|
||||||
|
var stdDev = Math.Sqrt(scores.Sum(x => Math.Pow(x - mean, 2)) / scores.Count);
|
||||||
|
|
||||||
|
return new SimulatedSimulationResult
|
||||||
|
{
|
||||||
|
SimulationId = simulationId,
|
||||||
|
ProfileId = profileId,
|
||||||
|
ProfileVersion = profileVersion,
|
||||||
|
ProfileHash = $"sha256:{Guid.NewGuid():N}",
|
||||||
|
Timestamp = timestamp,
|
||||||
|
TenantId = "default",
|
||||||
|
TotalFindings = findingCount,
|
||||||
|
MeanScore = mean,
|
||||||
|
MedianScore = median,
|
||||||
|
StdDeviation = stdDev,
|
||||||
|
MaxScore = scores.Max(),
|
||||||
|
MinScore = scores.Min(),
|
||||||
|
CriticalCount = critical,
|
||||||
|
HighCount = high,
|
||||||
|
MediumCount = medium,
|
||||||
|
LowCount = low,
|
||||||
|
InformationalCount = info,
|
||||||
|
DeterminismHash = $"det-{Guid.NewGuid():N}",
|
||||||
|
FindingScores = findings,
|
||||||
|
TopMovers = findings
|
||||||
|
.OrderByDescending(f => f.NormalizedScore)
|
||||||
|
.Take(10)
|
||||||
|
.Select(f => new ExportedTopMover
|
||||||
|
{
|
||||||
|
FindingId = f.FindingId,
|
||||||
|
ComponentPurl = f.ComponentPurl,
|
||||||
|
Score = f.NormalizedScore,
|
||||||
|
Severity = f.Severity,
|
||||||
|
PrimaryDriver = "cvss_base",
|
||||||
|
DriverContribution = f.NormalizedScore * 0.3
|
||||||
|
})
|
||||||
|
.ToList(),
|
||||||
|
SignalAnalysis = new ExportedSignalAnalysis
|
||||||
|
{
|
||||||
|
TotalSignals = 8,
|
||||||
|
SignalsUsed = 6,
|
||||||
|
SignalsMissing = 2,
|
||||||
|
SignalCoverage = 0.75,
|
||||||
|
TopContributors = new List<ExportedSignalContributor>
|
||||||
|
{
|
||||||
|
new() { SignalName = "cvss_base", TotalContribution = 450.5, ContributionPercentage = 30, AvgValue = 6.5, Weight = 0.3, ImpactDirection = "increase" },
|
||||||
|
new() { SignalName = "kev_listed", TotalContribution = 375.2, ContributionPercentage = 25, AvgValue = 0.15, Weight = 0.25, ImpactDirection = "increase" },
|
||||||
|
new() { SignalName = "epss_score", TotalContribution = 300.8, ContributionPercentage = 20, AvgValue = 0.3, Weight = 0.2, ImpactDirection = "increase" }
|
||||||
|
},
|
||||||
|
MostImpactfulMissing = new List<string> { "reachability", "exploit_maturity" }
|
||||||
|
},
|
||||||
|
OverrideAnalysis = new ExportedOverrideAnalysis
|
||||||
|
{
|
||||||
|
TotalOverridesEvaluated = 25,
|
||||||
|
SeverityOverridesApplied = 8,
|
||||||
|
DecisionOverridesApplied = 5,
|
||||||
|
OverrideApplicationRate = 0.52,
|
||||||
|
OverrideConflictsCount = 1
|
||||||
|
},
|
||||||
|
Distribution = new DistributionSection
|
||||||
|
{
|
||||||
|
ScoreBuckets = new List<ExportedScoreBucket>
|
||||||
|
{
|
||||||
|
new() { RangeMin = 0, RangeMax = 2, Label = "Low", Count = (int)(findingCount * 0.3), Percentage = 30 },
|
||||||
|
new() { RangeMin = 2, RangeMax = 5, Label = "Medium", Count = (int)(findingCount * 0.4), Percentage = 40 },
|
||||||
|
new() { RangeMin = 5, RangeMax = 8, Label = "High", Count = (int)(findingCount * 0.2), Percentage = 20 },
|
||||||
|
new() { RangeMin = 8, RangeMax = 10, Label = "Critical", Count = (int)(findingCount * 0.1), Percentage = 10 }
|
||||||
|
},
|
||||||
|
Percentiles = new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
["p50"] = median,
|
||||||
|
["p75"] = scores[(int)(scores.Count * 0.75)],
|
||||||
|
["p90"] = scores[(int)(scores.Count * 0.90)],
|
||||||
|
["p95"] = scores[(int)(scores.Count * 0.95)],
|
||||||
|
["p99"] = scores[(int)(scores.Count * 0.99)]
|
||||||
|
},
|
||||||
|
SeverityBreakdown = new Dictionary<string, int>
|
||||||
|
{
|
||||||
|
["critical"] = critical,
|
||||||
|
["high"] = high,
|
||||||
|
["medium"] = medium,
|
||||||
|
["low"] = low,
|
||||||
|
["informational"] = info
|
||||||
|
},
|
||||||
|
ActionBreakdown = new Dictionary<string, int>
|
||||||
|
{
|
||||||
|
["upgrade"] = (int)(findingCount * 0.3),
|
||||||
|
["patch"] = (int)(findingCount * 0.25),
|
||||||
|
["monitor"] = (int)(findingCount * 0.2),
|
||||||
|
["accept"] = (int)(findingCount * 0.15),
|
||||||
|
["investigate"] = (int)(findingCount * 0.1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ComponentBreakdown = new ComponentSection
|
||||||
|
{
|
||||||
|
TotalComponents = 20,
|
||||||
|
ComponentsWithFindings = 18,
|
||||||
|
TopRiskComponents = Enumerable.Range(0, 5)
|
||||||
|
.Select(i => new ExportedComponentRisk
|
||||||
|
{
|
||||||
|
ComponentPurl = $"pkg:npm/example-package-{i}@1.{i}.0",
|
||||||
|
FindingCount = random.Next(3, 10),
|
||||||
|
MaxScore = 7.5 + random.NextDouble() * 2.5,
|
||||||
|
AvgScore = 5.0 + random.NextDouble() * 3.0,
|
||||||
|
HighestSeverity = i < 2 ? "critical" : "high",
|
||||||
|
RecommendedAction = i < 3 ? "upgrade" : "patch"
|
||||||
|
})
|
||||||
|
.ToList(),
|
||||||
|
EcosystemBreakdown = new Dictionary<string, ExportedEcosystemSummary>
|
||||||
|
{
|
||||||
|
["npm"] = new() { Ecosystem = "npm", ComponentCount = 12, FindingCount = 80, AvgScore = 5.2, CriticalCount = 5, HighCount = 15 },
|
||||||
|
["pypi"] = new() { Ecosystem = "pypi", ComponentCount = 5, FindingCount = 45, AvgScore = 4.8, CriticalCount = 2, HighCount = 8 },
|
||||||
|
["maven"] = new() { Ecosystem = "maven", ComponentCount = 3, FindingCount = 25, AvgScore = 6.1, CriticalCount = 3, HighCount = 7 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SimulatedSimulationResult
|
||||||
|
{
|
||||||
|
public required string SimulationId { get; init; }
|
||||||
|
public required string ProfileId { get; init; }
|
||||||
|
public required string ProfileVersion { get; init; }
|
||||||
|
public required string ProfileHash { get; init; }
|
||||||
|
public required DateTimeOffset Timestamp { get; init; }
|
||||||
|
public string? TenantId { get; init; }
|
||||||
|
public required int TotalFindings { get; init; }
|
||||||
|
public required double MeanScore { get; init; }
|
||||||
|
public required double MedianScore { get; init; }
|
||||||
|
public required double StdDeviation { get; init; }
|
||||||
|
public required double MaxScore { get; init; }
|
||||||
|
public required double MinScore { get; init; }
|
||||||
|
public required int CriticalCount { get; init; }
|
||||||
|
public required int HighCount { get; init; }
|
||||||
|
public required int MediumCount { get; init; }
|
||||||
|
public required int LowCount { get; init; }
|
||||||
|
public required int InformationalCount { get; init; }
|
||||||
|
public required string DeterminismHash { get; init; }
|
||||||
|
public required IReadOnlyList<ExportedFindingScore> FindingScores { get; init; }
|
||||||
|
public required IReadOnlyList<ExportedTopMover> TopMovers { get; init; }
|
||||||
|
public ExportedSignalAnalysis? SignalAnalysis { get; init; }
|
||||||
|
public ExportedOverrideAnalysis? OverrideAnalysis { get; init; }
|
||||||
|
public DistributionSection? Distribution { get; init; }
|
||||||
|
public ComponentSection? ComponentBreakdown { get; init; }
|
||||||
|
public TrendSection? Trends { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -166,6 +166,15 @@ public static class ExportTelemetry
|
|||||||
"jobs",
|
"jobs",
|
||||||
"Total number of risk bundle jobs completed");
|
"Total number of risk bundle jobs completed");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of simulation exports.
|
||||||
|
/// Tags: format (json|ndjson|csv), tenant_id
|
||||||
|
/// </summary>
|
||||||
|
public static readonly Counter<long> SimulationExportsTotal = Meter.CreateCounter<long>(
|
||||||
|
"export_simulation_exports_total",
|
||||||
|
"exports",
|
||||||
|
"Total number of simulation exports");
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Histograms
|
#region Histograms
|
||||||
|
|||||||
@@ -24,3 +24,4 @@
|
|||||||
| UI-POLICY-23-005 | DONE (2025-12-05) | Simulator updated with SBOM/advisory pickers and explain trace view; uses PolicyApiService simulate. |
|
| UI-POLICY-23-005 | DONE (2025-12-05) | Simulator updated with SBOM/advisory pickers and explain trace view; uses PolicyApiService simulate. |
|
||||||
| UI-POLICY-23-006 | DONE (2025-12-06) | Explain view route `/policy-studio/packs/:packId/explain/:runId` with trace + JSON/PDF export (uses offline-safe jsPDF shim). |
|
| UI-POLICY-23-006 | DONE (2025-12-06) | Explain view route `/policy-studio/packs/:packId/explain/:runId` with trace + JSON/PDF export (uses offline-safe jsPDF shim). |
|
||||||
| UI-POLICY-23-001 | DONE (2025-12-05) | Workspace route `/policy-studio/packs` with pack list + quick actions; cached pack store with offline fallback. |
|
| UI-POLICY-23-001 | DONE (2025-12-05) | Workspace route `/policy-studio/packs` with pack list + quick actions; cached pack store with offline fallback. |
|
||||||
|
| CVSS-UI-190-011 | DONE (2025-12-07) | Added CVSS receipt viewer route (/cvss/receipts/:receiptId) with score badge, tabbed sections, stub client, and unit spec in src/Web/StellaOps.Web. |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
requireOrchViewerGuard,
|
requireOrchViewerGuard,
|
||||||
requireOrchOperatorGuard,
|
requireOrchOperatorGuard,
|
||||||
@@ -9,61 +9,61 @@ import {
|
|||||||
requirePolicyApproverGuard,
|
requirePolicyApproverGuard,
|
||||||
requirePolicyViewerGuard,
|
requirePolicyViewerGuard,
|
||||||
} from './core/auth';
|
} from './core/auth';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'dashboard/sources',
|
path: 'dashboard/sources',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./features/dashboard/sources-dashboard.component').then(
|
import('./features/dashboard/sources-dashboard.component').then(
|
||||||
(m) => m.SourcesDashboardComponent
|
(m) => m.SourcesDashboardComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'console/profile',
|
path: 'console/profile',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./features/console/console-profile.component').then(
|
import('./features/console/console-profile.component').then(
|
||||||
(m) => m.ConsoleProfileComponent
|
(m) => m.ConsoleProfileComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'console/status',
|
path: 'console/status',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./features/console/console-status.component').then(
|
import('./features/console/console-status.component').then(
|
||||||
(m) => m.ConsoleStatusComponent
|
(m) => m.ConsoleStatusComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
// Orchestrator routes - gated by orch:read scope (UI-ORCH-32-001)
|
// Orchestrator routes - gated by orch:read scope (UI-ORCH-32-001)
|
||||||
{
|
{
|
||||||
path: 'orchestrator',
|
path: 'orchestrator',
|
||||||
canMatch: [requireOrchViewerGuard],
|
canMatch: [requireOrchViewerGuard],
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./features/orchestrator/orchestrator-dashboard.component').then(
|
import('./features/orchestrator/orchestrator-dashboard.component').then(
|
||||||
(m) => m.OrchestratorDashboardComponent
|
(m) => m.OrchestratorDashboardComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'orchestrator/jobs',
|
path: 'orchestrator/jobs',
|
||||||
canMatch: [requireOrchViewerGuard],
|
canMatch: [requireOrchViewerGuard],
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./features/orchestrator/orchestrator-jobs.component').then(
|
import('./features/orchestrator/orchestrator-jobs.component').then(
|
||||||
(m) => m.OrchestratorJobsComponent
|
(m) => m.OrchestratorJobsComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'orchestrator/jobs/:jobId',
|
path: 'orchestrator/jobs/:jobId',
|
||||||
canMatch: [requireOrchViewerGuard],
|
canMatch: [requireOrchViewerGuard],
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./features/orchestrator/orchestrator-job-detail.component').then(
|
import('./features/orchestrator/orchestrator-job-detail.component').then(
|
||||||
(m) => m.OrchestratorJobDetailComponent
|
(m) => m.OrchestratorJobDetailComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'orchestrator/quotas',
|
path: 'orchestrator/quotas',
|
||||||
canMatch: [requireOrchOperatorGuard],
|
canMatch: [requireOrchOperatorGuard],
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./features/orchestrator/orchestrator-quotas.component').then(
|
import('./features/orchestrator/orchestrator-quotas.component').then(
|
||||||
(m) => m.OrchestratorQuotasComponent
|
(m) => m.OrchestratorQuotasComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'policy-studio/packs',
|
path: 'policy-studio/packs',
|
||||||
@@ -132,61 +132,67 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'concelier/trivy-db-settings',
|
path: 'concelier/trivy-db-settings',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./features/trivy-db-settings/trivy-db-settings-page.component').then(
|
import('./features/trivy-db-settings/trivy-db-settings-page.component').then(
|
||||||
(m) => m.TrivyDbSettingsPageComponent
|
(m) => m.TrivyDbSettingsPageComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'scans/:scanId',
|
path: 'scans/:scanId',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./features/scans/scan-detail-page.component').then(
|
import('./features/scans/scan-detail-page.component').then(
|
||||||
(m) => m.ScanDetailPageComponent
|
(m) => m.ScanDetailPageComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'welcome',
|
path: 'welcome',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./features/welcome/welcome-page.component').then(
|
import('./features/welcome/welcome-page.component').then(
|
||||||
(m) => m.WelcomePageComponent
|
(m) => m.WelcomePageComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'risk',
|
path: 'risk',
|
||||||
canMatch: [() => import('./core/auth/auth.guard').then((m) => m.requireAuthGuard)],
|
canMatch: [() => import('./core/auth/auth.guard').then((m) => m.requireAuthGuard)],
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./features/risk/risk-dashboard.component').then(
|
import('./features/risk/risk-dashboard.component').then(
|
||||||
(m) => m.RiskDashboardComponent
|
(m) => m.RiskDashboardComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'vulnerabilities/:vulnId',
|
path: 'vulnerabilities/:vulnId',
|
||||||
canMatch: [() => import('./core/auth/auth.guard').then((m) => m.requireAuthGuard)],
|
canMatch: [() => import('./core/auth/auth.guard').then((m) => m.requireAuthGuard)],
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./features/vulnerabilities/vulnerability-detail.component').then(
|
import('./features/vulnerabilities/vulnerability-detail.component').then(
|
||||||
(m) => m.VulnerabilityDetailComponent
|
(m) => m.VulnerabilityDetailComponent
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'notify',
|
path: 'cvss/receipts/:receiptId',
|
||||||
loadComponent: () =>
|
canMatch: [() => import('./core/auth/auth.guard').then((m) => m.requireAuthGuard)],
|
||||||
import('./features/notify/notify-panel.component').then(
|
loadComponent: () =>
|
||||||
(m) => m.NotifyPanelComponent
|
import('./features/cvss/cvss-receipt.component').then((m) => m.CvssReceiptComponent),
|
||||||
),
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'notify',
|
||||||
path: 'auth/callback',
|
loadComponent: () =>
|
||||||
loadComponent: () =>
|
import('./features/notify/notify-panel.component').then(
|
||||||
import('./features/auth/auth-callback.component').then(
|
(m) => m.NotifyPanelComponent
|
||||||
(m) => m.AuthCallbackComponent
|
),
|
||||||
),
|
},
|
||||||
},
|
{
|
||||||
{
|
path: 'auth/callback',
|
||||||
path: '',
|
loadComponent: () =>
|
||||||
pathMatch: 'full',
|
import('./features/auth/auth-callback.component').then(
|
||||||
redirectTo: 'console/profile',
|
(m) => m.AuthCallbackComponent
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
path: '**',
|
{
|
||||||
redirectTo: 'console/profile',
|
path: '',
|
||||||
},
|
pathMatch: 'full',
|
||||||
];
|
redirectTo: 'console/profile',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
redirectTo: 'console/profile',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
58
src/Web/StellaOps.Web/src/app/core/api/cvss.client.ts
Normal file
58
src/Web/StellaOps.Web/src/app/core/api/cvss.client.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
import { CvssReceipt } from './cvss.models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder CVSS client until Policy Gateway endpoint is wired.
|
||||||
|
* Emits deterministic sample data for UI development and tests.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class CvssClient {
|
||||||
|
getReceipt(receiptId: string): Observable<CvssReceipt> {
|
||||||
|
const sample: CvssReceipt = {
|
||||||
|
receiptId,
|
||||||
|
vulnerabilityId: 'CVE-2025-1234',
|
||||||
|
createdAt: '2025-12-05T12:00:00Z',
|
||||||
|
createdBy: 'analyst@example.org',
|
||||||
|
score: {
|
||||||
|
base: 7.6,
|
||||||
|
threat: 7.6,
|
||||||
|
environmental: 8.1,
|
||||||
|
overall: 8.1,
|
||||||
|
vector:
|
||||||
|
'CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H',
|
||||||
|
severity: 'High',
|
||||||
|
},
|
||||||
|
policy: {
|
||||||
|
policyId: 'policy-bundle-main',
|
||||||
|
policyHash: 'sha256:deadbeefcafec0ffee1234',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
evidence: [
|
||||||
|
{
|
||||||
|
id: 'ev-001',
|
||||||
|
description: 'Upstream advisory references vulnerable TLS parser',
|
||||||
|
source: 'NVD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ev-002',
|
||||||
|
description: 'Vendor bulletin confirms threat active in region',
|
||||||
|
source: 'Vendor',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
history: [
|
||||||
|
{
|
||||||
|
version: 1,
|
||||||
|
changedAt: '2025-12-05T12:00:00Z',
|
||||||
|
changedBy: 'analyst@example.org',
|
||||||
|
reason: 'Initial scoring',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return of(sample);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/Web/StellaOps.Web/src/app/core/api/cvss.models.ts
Normal file
38
src/Web/StellaOps.Web/src/app/core/api/cvss.models.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
export interface CvssScoreBreakdown {
|
||||||
|
readonly base: number;
|
||||||
|
readonly threat: number;
|
||||||
|
readonly environmental: number;
|
||||||
|
readonly overall: number;
|
||||||
|
readonly vector: string;
|
||||||
|
readonly severity: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CvssPolicySummary {
|
||||||
|
readonly policyId: string;
|
||||||
|
readonly policyHash: string;
|
||||||
|
readonly version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CvssEvidenceItem {
|
||||||
|
readonly id: string;
|
||||||
|
readonly description: string;
|
||||||
|
readonly source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CvssHistoryEntry {
|
||||||
|
readonly version: number;
|
||||||
|
readonly changedAt: string;
|
||||||
|
readonly changedBy: string;
|
||||||
|
readonly reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CvssReceipt {
|
||||||
|
readonly receiptId: string;
|
||||||
|
readonly vulnerabilityId: string;
|
||||||
|
readonly createdAt: string;
|
||||||
|
readonly createdBy: string;
|
||||||
|
readonly score: CvssScoreBreakdown;
|
||||||
|
readonly policy: CvssPolicySummary;
|
||||||
|
readonly evidence: readonly CvssEvidenceItem[];
|
||||||
|
readonly history: readonly CvssHistoryEntry[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<section class="cvss-receipt" *ngIf="receipt$ | async as receipt">
|
||||||
|
<header class="cvss-receipt__header">
|
||||||
|
<div>
|
||||||
|
<p class="cvss-receipt__label">CVSS Receipt</p>
|
||||||
|
<h1>
|
||||||
|
{{ receipt.vulnerabilityId }}
|
||||||
|
<span class="cvss-receipt__id">#{{ receipt.receiptId }}</span>
|
||||||
|
</h1>
|
||||||
|
<p class="cvss-receipt__meta">
|
||||||
|
Created {{ receipt.createdAt }} by {{ receipt.createdBy }} · Policy
|
||||||
|
{{ receipt.policy.policyId }} ({{ receipt.policy.version ?? 'v1' }})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="cvss-receipt__score">
|
||||||
|
<div class="cvss-score-badge" [class.cvss-score-badge--critical]="receipt.score.overall >= 9">
|
||||||
|
{{ receipt.score.overall | number : '1.1-1' }}
|
||||||
|
<span class="cvss-score-badge__label">{{ receipt.score.severity }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="cvss-receipt__vector">{{ receipt.score.vector }}</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="cvss-tabs" aria-label="CVSS receipt sections">
|
||||||
|
<button type="button" [class.active]="activeTab === 'base'" (click)="activeTab = 'base'">
|
||||||
|
Base
|
||||||
|
</button>
|
||||||
|
<button type="button" [class.active]="activeTab === 'threat'" (click)="activeTab = 'threat'">
|
||||||
|
Threat
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
[class.active]="activeTab === 'environmental'"
|
||||||
|
(click)="activeTab = 'environmental'"
|
||||||
|
>
|
||||||
|
Environmental
|
||||||
|
</button>
|
||||||
|
<button type="button" [class.active]="activeTab === 'evidence'" (click)="activeTab = 'evidence'">
|
||||||
|
Evidence
|
||||||
|
</button>
|
||||||
|
<button type="button" [class.active]="activeTab === 'policy'" (click)="activeTab = 'policy'">
|
||||||
|
Policy
|
||||||
|
</button>
|
||||||
|
<button type="button" [class.active]="activeTab === 'history'" (click)="activeTab = 'history'">
|
||||||
|
History
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section class="cvss-panel" *ngIf="activeTab === 'base'">
|
||||||
|
<h2>Base Metrics</h2>
|
||||||
|
<p>Base score: {{ receipt.score.base | number : '1.1-1' }}</p>
|
||||||
|
<p>Vector: {{ receipt.score.vector }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cvss-panel" *ngIf="activeTab === 'threat'">
|
||||||
|
<h2>Threat Metrics</h2>
|
||||||
|
<p>Threat-adjusted score: {{ receipt.score.threat | number : '1.1-1' }}</p>
|
||||||
|
<p>Vector: {{ receipt.score.vector }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cvss-panel" *ngIf="activeTab === 'environmental'">
|
||||||
|
<h2>Environmental Metrics</h2>
|
||||||
|
<p>Environmental score: {{ receipt.score.environmental | number : '1.1-1' }}</p>
|
||||||
|
<p>Vector: {{ receipt.score.vector }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cvss-panel" *ngIf="activeTab === 'evidence'">
|
||||||
|
<h2>Evidence</h2>
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let item of receipt.evidence; trackBy: trackById">
|
||||||
|
<p class="evidence__id">{{ item.id }}</p>
|
||||||
|
<p>{{ item.description }}</p>
|
||||||
|
<p class="evidence__source">Source: {{ item.source }}</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cvss-panel" *ngIf="activeTab === 'policy'">
|
||||||
|
<h2>Policy</h2>
|
||||||
|
<p>Policy ID: {{ receipt.policy.policyId }}</p>
|
||||||
|
<p>Version: {{ receipt.policy.version ?? 'v1' }}</p>
|
||||||
|
<p>Hash: {{ receipt.policy.policyHash }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cvss-panel" *ngIf="activeTab === 'history'">
|
||||||
|
<h2>History</h2>
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let entry of receipt.history">
|
||||||
|
<p>
|
||||||
|
v{{ entry.version }} · {{ entry.changedAt }} by {{ entry.changedBy }}
|
||||||
|
<span *ngIf="entry.reason">— {{ entry.reason }}</span>
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
.cvss-receipt {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvss-receipt__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvss-receipt__label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvss-receipt__id {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvss-receipt__meta {
|
||||||
|
color: #555;
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvss-receipt__score {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvss-score-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background: #0a5ac2;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvss-score-badge__label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvss-score-badge--critical {
|
||||||
|
background: #b3261e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvss-receipt__vector {
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
font-family: monospace;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvss-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvss-tabs button {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvss-tabs button.active {
|
||||||
|
border-bottom: 2px solid #0a5ac2;
|
||||||
|
color: #0a5ac2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cvss-panel {
|
||||||
|
background: #f8f9fb;
|
||||||
|
border: 1px solid #e1e4ea;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence__id {
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evidence__source {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
import { CvssClient } from '../../core/api/cvss.client';
|
||||||
|
import { CvssReceiptComponent } from './cvss-receipt.component';
|
||||||
|
import { CvssReceipt } from '../../core/api/cvss.models';
|
||||||
|
|
||||||
|
describe(CvssReceiptComponent.name, () => {
|
||||||
|
let fixture: ComponentFixture<CvssReceiptComponent>;
|
||||||
|
|
||||||
|
const sample: CvssReceipt = {
|
||||||
|
receiptId: 'rcpt-123',
|
||||||
|
vulnerabilityId: 'CVE-2025-1234',
|
||||||
|
createdAt: '2025-12-05T12:00:00Z',
|
||||||
|
createdBy: 'analyst@example.org',
|
||||||
|
score: {
|
||||||
|
base: 7.6,
|
||||||
|
threat: 7.6,
|
||||||
|
environmental: 8.1,
|
||||||
|
overall: 8.1,
|
||||||
|
vector: 'CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H',
|
||||||
|
severity: 'High',
|
||||||
|
},
|
||||||
|
policy: {
|
||||||
|
policyId: 'policy-bundle-main',
|
||||||
|
policyHash: 'sha256:deadbeef',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
evidence: [],
|
||||||
|
history: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CvssReceiptComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
paramMap: of({
|
||||||
|
get: (key: string) => (key === 'receiptId' ? sample.receiptId : null),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: CvssClient,
|
||||||
|
useValue: {
|
||||||
|
getReceipt: () => of(sample),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CvssReceiptComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders receipt id and vulnerability', () => {
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(compiled.textContent).toContain(sample.vulnerabilityId);
|
||||||
|
expect(compiled.textContent).toContain(sample.receiptId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders overall score', () => {
|
||||||
|
const compiled = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(compiled.querySelector('.cvss-score-badge')?.textContent).toContain('8.1');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { CvssClient } from '../../core/api/cvss.client';
|
||||||
|
import { CvssReceipt } from '../../core/api/cvss.models';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: 'app-cvss-receipt',
|
||||||
|
imports: [CommonModule, RouterModule],
|
||||||
|
templateUrl: './cvss-receipt.component.html',
|
||||||
|
styleUrls: ['./cvss-receipt.component.scss'],
|
||||||
|
})
|
||||||
|
export class CvssReceiptComponent implements OnInit {
|
||||||
|
receipt$!: Observable<CvssReceipt>;
|
||||||
|
|
||||||
|
activeTab: 'base' | 'threat' | 'environmental' | 'evidence' | 'policy' | 'history' =
|
||||||
|
'base';
|
||||||
|
|
||||||
|
constructor(private readonly route: ActivatedRoute, private readonly client: CvssClient) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.receipt$ = this.route.paramMap.pipe(
|
||||||
|
map((params) => params.get('receiptId') ?? ''),
|
||||||
|
switchMap((id) => this.client.getReceipt(id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackById(_: number, item: { id?: string }): string | undefined {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user