Update
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
2025-10-21 18:54:26 +03:00
committed by Vladimir Moushkov
parent 791e12baab
commit cfaea5efd9
50 changed files with 3027 additions and 596 deletions

View File

@@ -37,7 +37,12 @@ jobs:
- name: Install documentation toolchain
run: |
npm install --no-save markdown-link-check remark-cli remark-preset-lint-recommended ajv ajv-cli ajv-formats
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.100-rc.2.25502.107'
- name: Link check
run: |
find docs -name '*.md' -print0 | \
@@ -62,9 +67,13 @@ jobs:
fi
npx ajv validate -c ajv-formats -s "$schema_path" -d "$sample"
done
- name: Setup Python
uses: actions/setup-python@v5
- name: Run Notify schema validation tests
run: |
dotnet test src/StellaOps.Notify.Models.Tests/StellaOps.Notify.Models.Tests.csproj --configuration Release --nologo
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}

View File

@@ -38,7 +38,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- Team Team Excititor Connectors Oracle: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.Oracle.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-ORACLE-01-001 (DOING). Confirm prerequisites (external: EXCITITOR-CONN-ABS-01-001) before starting and report status in module TASKS.md.
- Team Team Excititor Connectors SUSE: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.SUSE.RancherVEXHub/TASKS.md`. Focus on EXCITITOR-CONN-SUSE-01-002 (TODO). Confirm prerequisites (external: EXCITITOR-CONN-SUSE-01-001, EXCITITOR-STORAGE-01-003) before starting and report status in module TASKS.md.
- Team Team Excititor Connectors Ubuntu: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.Ubuntu.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-UBUNTU-01-002 (TODO). Confirm prerequisites (external: EXCITITOR-CONN-UBUNTU-01-001, EXCITITOR-STORAGE-01-003) before starting and report status in module TASKS.md.
- Team Team Excititor Export: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Excititor.Export/TASKS.md`. Focus on EXCITITOR-EXPORT-01-005 (TODO). Confirm prerequisites (external: EXCITITOR-CORE-02-001, EXCITITOR-EXPORT-01-004) before starting and report status in module TASKS.md.
- Team Team Excititor Export: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Excititor.Export/TASKS.md`. Focus on EXCITITOR-EXPORT-01-005 (DONE 2025-10-21). Confirm prerequisites (external: EXCITITOR-CORE-02-001, EXCITITOR-EXPORT-01-004) before starting and report status in module TASKS.md.
- Team Team Excititor Formats: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Excititor.Formats.CSAF/TASKS.md`, `src/StellaOps.Excititor.Formats.CycloneDX/TASKS.md`, `src/StellaOps.Excititor.Formats.OpenVEX/TASKS.md`. Focus on EXCITITOR-FMT-CSAF-01-002 (TODO), EXCITITOR-FMT-CSAF-01-003 (TODO), EXCITITOR-FMT-CYCLONE-01-002 (TODO), EXCITITOR-FMT-CYCLONE-01-003 (TODO), EXCITITOR-FMT-OPENVEX-01-002 (TODO), EXCITITOR-FMT-OPENVEX-01-003 (TODO). Confirm prerequisites (external: EXCITITOR-EXPORT-01-001, EXCITITOR-FMT-CSAF-01-001, EXCITITOR-FMT-CYCLONE-01-001, EXCITITOR-FMT-OPENVEX-01-001, EXCITITOR-POLICY-01-001) before starting and report status in module TASKS.md.
- Team Team Excititor Storage: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Excititor.Storage.Mongo/TASKS.md`. Focus on EXCITITOR-STORAGE-MONGO-08-001 (DONE 2025-10-19), EXCITITOR-STORAGE-03-001 (TODO). Confirm prerequisites (external: EXCITITOR-STORAGE-01-003, EXCITITOR-STORAGE-02-001) before starting and report status in module TASKS.md.
- Team Team Excititor WebService: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Excititor.WebService/TASKS.md`. Focus on EXCITITOR-WEB-01-002 (DONE 2025-10-20), EXCITITOR-WEB-01-003 (TODO), EXCITITOR-WEB-01-004 (DONE 2025-10-20). Confirm prerequisites (external: EXCITITOR-ATTEST-01-001, EXCITITOR-EXPORT-01-001, EXCITITOR-WEB-01-001) before starting and report status in module TASKS.md.
@@ -47,7 +47,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- Team Team Normalization & Storage Backbone: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Concelier.Storage.Mongo/TASKS.md`. Focus on FEEDSTORAGE-MONGO-08-001 (DONE 2025-10-19). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team Team WebService & Authority: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md`, `src/StellaOps.Concelier.WebService/TASKS.md`. Focus on SEC2.PLG (DOING), SEC3.PLG (DOING), SEC5.PLG (DOING), PLG4-6.CAPABILITIES (BLOCKED), PLG6.DIAGRAM (TODO), PLG7.RFC (REVIEW), FEEDWEB-DOCS-01-001 (DOING), FEEDWEB-OPS-01-006 (TODO), FEEDWEB-OPS-01-007 (BLOCKED). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team Tools Guild, BE-Conn-MSRC: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Concelier.Connector.Common/TASKS.md`. Focus on FEEDCONN-SHARED-STATE-003 (**TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team UX Specialist, Angular Eng: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Web/TASKS.md`. Focus on WEB1.TRIVY-SETTINGS (DONE 2025-10-21) and WEB1.TRIVY-SETTINGS-TESTS (BLOCKED 2025-10-21). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team UX Specialist, Angular Eng: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Web/TASKS.md`. Focus on WEB1.TRIVY-SETTINGS (DONE 2025-10-21), WEB1.TRIVY-SETTINGS-TESTS (DONE 2025-10-21), and WEB1.DEPS-13-001 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team Zastava Core Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Zastava.Core/TASKS.md`. Focus on ZASTAVA-CORE-12-201 (TODO), ZASTAVA-CORE-12-202 (TODO), ZASTAVA-CORE-12-203 (TODO), ZASTAVA-OPS-12-204 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team Zastava Webhook Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Zastava.Webhook/TASKS.md`. Focus on ZASTAVA-WEBHOOK-12-101 (TODO), ZASTAVA-WEBHOOK-12-102 (TODO), ZASTAVA-WEBHOOK-12-103 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
@@ -73,7 +73,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- Team Team Excititor Connectors Oracle: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.Oracle.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-ORACLE-01-002 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-ORACLE-01-001 (Wave 0); external: EXCITITOR-STORAGE-01-003) before starting and report status in module TASKS.md.
- Team Team Excititor Connectors SUSE: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.SUSE.RancherVEXHub/TASKS.md`. Focus on EXCITITOR-CONN-SUSE-01-003 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-SUSE-01-002 (Wave 0); external: EXCITITOR-POLICY-01-001) before starting and report status in module TASKS.md.
- Team Team Excititor Connectors Ubuntu: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.Ubuntu.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-UBUNTU-01-003 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-UBUNTU-01-002 (Wave 0); external: EXCITITOR-POLICY-01-001) before starting and report status in module TASKS.md.
- Team Team Excititor Export: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Export/TASKS.md`. Focus on EXCITITOR-EXPORT-01-006 (TODO). Confirm prerequisites (internal: EXCITITOR-EXPORT-01-005 (Wave 0), POLICY-CORE-09-005 (Wave 0)) before starting and report status in module TASKS.md.
- Team Team Excititor Export: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Export/TASKS.md`. Focus on EXCITITOR-EXPORT-01-006 (DONE 2025-10-21). Confirm prerequisites (internal: EXCITITOR-EXPORT-01-005 (Wave 0), POLICY-CORE-09-005 (Wave 0)) before starting and report status in module TASKS.md.
- Team Team Excititor Worker: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Worker/TASKS.md`. Focus on EXCITITOR-WORKER-01-003 (TODO). Confirm prerequisites (internal: EXCITITOR-ATTEST-01-003 (Wave 0); external: EXCITITOR-EXPORT-01-002, EXCITITOR-WORKER-01-001) before starting and report status in module TASKS.md.
- Team UI Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.UI/TASKS.md`. Focus on UI-ATTEST-11-005 (TODO), UI-VEX-13-003 (TODO), UI-POLICY-13-007 (TODO), UI-ADMIN-13-004 (TODO), UI-AUTH-13-001 (TODO), UI-SCANS-13-002 (TODO), UI-NOTIFY-13-006 (DOING), UI-SCHED-13-005 (TODO). Confirm prerequisites (internal: ATTESTOR-API-11-201 (Wave 0), AUTH-DPOP-11-001 (Wave 0), AUTH-MTLS-11-002 (Wave 0), EXCITITOR-EXPORT-01-005 (Wave 0), NOTIFY-WEB-15-101 (Wave 0), POLICY-CORE-09-006 (Wave 0), SCHED-WEB-16-101 (Wave 0), SIGNER-API-11-101 (Wave 0); external: EXCITITOR-CORE-02-001, SCANNER-WEB-09-102, SCANNER-WEB-09-103) before starting and report status in module TASKS.md.
- Team Zastava Observer Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Zastava.Observer/TASKS.md`. Focus on ZASTAVA-OBS-12-001 (TODO). Confirm prerequisites (internal: ZASTAVA-CORE-12-201 (Wave 0)) before starting and report status in module TASKS.md.
@@ -82,7 +82,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- Team Bench Guild, Notify Team: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `bench/TASKS.md`. Focus on BENCH-NOTIFY-15-001 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-301 (Wave 1)) before starting and report status in module TASKS.md.
- Team Bench Guild, Scheduler Team: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `bench/TASKS.md`. Focus on BENCH-IMPACT-16-001 (TODO). Confirm prerequisites (internal: SCHED-IMPACT-16-301 (Wave 1)) before starting and report status in module TASKS.md.
- Team Deployment Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `ops/deployment/TASKS.md`. Focus on DEVOPS-OPS-14-003 (TODO). Confirm prerequisites (internal: DEVOPS-REL-14-001 (Wave 1)) before starting and report status in module TASKS.md.
- Team DevOps Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `ops/devops/TASKS.md`. Focus on DEVOPS-MIRROR-08-001 (DONE 2025-10-19), DEVOPS-PERF-10-002 (TODO), DEVOPS-REL-17-002 (TODO). Confirm prerequisites (internal: BENCH-SCANNER-10-002 (Wave 1), DEVOPS-REL-14-001 (Wave 1), SCANNER-EMIT-17-701 (Wave 1)) before starting and report status in module TASKS.md.
- Team DevOps Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `ops/devops/TASKS.md`. Focus on DEVOPS-MIRROR-08-001 (DONE 2025-10-19), DEVOPS-PERF-10-002 (TODO), DEVOPS-REL-17-002 (TODO), and DEVOPS-NUGET-13-001 (TODO). Confirm prerequisites (internal: BENCH-SCANNER-10-002 (Wave 1), DEVOPS-REL-14-001 (Wave 1), SCANNER-EMIT-17-701 (Wave 1)) before starting and report status in module TASKS.md.
- Team DevOps Guild, Notify Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `ops/devops/TASKS.md`. Focus on DEVOPS-SCANNER-09-205 (TODO). Confirm prerequisites (internal: DEVOPS-SCANNER-09-204 (Wave 1)) before starting and report status in module TASKS.md.
- Team Notify Engine Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Notify.Engine/TASKS.md`. Focus on NOTIFY-ENGINE-15-302 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-301 (Wave 1)) before starting and report status in module TASKS.md.
- Team Notify Queue Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Notify.Queue/TASKS.md`. Focus on NOTIFY-QUEUE-15-403 (TODO), NOTIFY-QUEUE-15-402 (TODO). Confirm prerequisites (internal: NOTIFY-QUEUE-15-401 (Wave 1)) before starting and report status in module TASKS.md.
@@ -96,13 +96,13 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- Team Scheduler Worker Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scheduler.Worker/TASKS.md`. Focus on SCHED-WORKER-16-202 (TODO), SCHED-WORKER-16-205 (TODO). Confirm prerequisites (internal: SCHED-IMPACT-16-301 (Wave 1), SCHED-WORKER-16-201 (Wave 1)) before starting and report status in module TASKS.md.
- Team TBD: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-305B (TODO), SCANNER-ANALYZERS-LANG-10-304B (TODO), SCANNER-ANALYZERS-LANG-10-308N (TODO), SCANNER-ANALYZERS-LANG-10-303B (TODO), SCANNER-ANALYZERS-LANG-10-306B (TODO). Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-303A (Wave 1), SCANNER-ANALYZERS-LANG-10-304A (Wave 1), SCANNER-ANALYZERS-LANG-10-305A (Wave 1), SCANNER-ANALYZERS-LANG-10-306A (Wave 1), SCANNER-ANALYZERS-LANG-10-307N (Wave 1)) before starting and report status in module TASKS.md.
- Team Team Excititor Connectors Oracle: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.Oracle.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-ORACLE-01-003 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-ORACLE-01-002 (Wave 1); external: EXCITITOR-POLICY-01-001) before starting and report status in module TASKS.md.
- Team Team Excititor Export: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Excititor.Export/TASKS.md`. Focus on EXCITITOR-EXPORT-01-007 (TODO). Confirm prerequisites (internal: EXCITITOR-EXPORT-01-006 (Wave 1)) before starting and report status in module TASKS.md.
- Team Team Excititor Export: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Excititor.Export/TASKS.md`. Focus on EXCITITOR-EXPORT-01-007 (DONE 2025-10-21). Confirm prerequisites (internal: EXCITITOR-EXPORT-01-006 (Wave 1)) before starting and report status in module TASKS.md.
- Team Zastava Observer Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Zastava.Observer/TASKS.md`. Focus on ZASTAVA-OBS-12-002 (TODO). Confirm prerequisites (internal: ZASTAVA-OBS-12-001 (Wave 1)) before starting and report status in module TASKS.md.
### Wave 3
- Team DevEx/CLI: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Cli/TASKS.md`. Focus on CLI-OFFLINE-13-006 (DONE 2025-10-21). Confirm prerequisites (internal: DEVOPS-OFFLINE-14-002 (Wave 2)) before starting and report status in module TASKS.md.
- Team DevEx/CLI, Scanner WebService Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Cli/TASKS.md`. Focus on CLI-RUNTIME-13-008 (TODO). Confirm prerequisites (internal: SCANNER-RUNTIME-12-302 (Wave 2)) before starting and report status in module TASKS.md.
- Team Excititor Connectors Stella: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.StellaOpsMirror/TASKS.md`. Focus on EXCITITOR-CONN-STELLA-07-001 (TODO). Confirm prerequisites (internal: EXCITITOR-EXPORT-01-007 (Wave 2)) before starting and report status in module TASKS.md.
- Team Excititor Connectors Stella: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.StellaOpsMirror/TASKS.md`. Focus on EXCITITOR-CONN-STELLA-07-001 (DONE 2025-10-21). Confirm prerequisites (internal: EXCITITOR-EXPORT-01-007 (Wave 2)) before starting and report status in module TASKS.md.
- Team Notify Engine Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Notify.Engine/TASKS.md`. Focus on NOTIFY-ENGINE-15-303 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-302 (Wave 2)) before starting and report status in module TASKS.md.
- Team Notify Worker Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Notify.Worker/TASKS.md`. Focus on NOTIFY-WORKER-15-203 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-302 (Wave 2)) before starting and report status in module TASKS.md.
- Team Scheduler Worker Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Scheduler.Worker/TASKS.md`. Focus on SCHED-WORKER-16-203 (TODO). Confirm prerequisites (internal: SCHED-WORKER-16-202 (Wave 2)) before starting and report status in module TASKS.md.
@@ -137,10 +137,10 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- Team Team Core Engine & Data Science: read EXECPLAN.md Wave 8 and SPRINTS.md rows for `src/StellaOps.Concelier.Core/TASKS.md`. Focus on FEEDCORE-ENGINE-07-002 (DONE 2025-10-21). Confirm prerequisites (internal: FEEDCORE-ENGINE-07-001 (Wave 7)) before starting and report status in module TASKS.md.
### Wave 9
- Team Team Core Engine & Storage Analytics: read EXECPLAN.md Wave 9 and SPRINTS.md rows for `src/StellaOps.Concelier.Core/TASKS.md`. Focus on FEEDCORE-ENGINE-07-003 (TODO). Confirm prerequisites (internal: FEEDCORE-ENGINE-07-001 (Wave 7)) before starting and report status in module TASKS.md.
- Team Team Core Engine & Storage Analytics: read EXECPLAN.md Wave 9 and SPRINTS.md rows for `src/StellaOps.Concelier.Core/TASKS.md`. FEEDCORE-ENGINE-07-003 marked DONE (2025-10-21); share ledger heuristics with Policy when integrating confidence decay.
### Wave 10
- Team Team Normalization & Storage Backbone: read EXECPLAN.md Wave 10 and SPRINTS.md rows for `src/StellaOps.Concelier.Storage.Mongo/TASKS.md`. Focus on FEEDSTORAGE-DATA-07-001 (TODO). Confirm prerequisites (internal: FEEDMERGE-ENGINE-07-001 (Wave 11)) before starting and report status in module TASKS.md.
- Team Team Normalization & Storage Backbone: read EXECPLAN.md Wave 10 and SPRINTS.md rows for `src/StellaOps.Concelier.Storage.Mongo/TASKS.md`. Focus on FEEDSTORAGE-DATA-07-001 (DONE 2025-10-19). Confirm prerequisites (internal: FEEDMERGE-ENGINE-07-001 (Wave 11)) before starting and report status in module TASKS.md.
### Wave 11
- Team BE-Merge: read EXECPLAN.md Wave 11 and SPRINTS.md rows for `src/StellaOps.Concelier.Merge/TASKS.md`. FEEDMERGE-ENGINE-07-001 marked DONE (2025-10-20); share conflict explainer rollout notes with Storage before Wave 10 resumes.
@@ -167,12 +167,12 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- **Sprint 1** · Backlog
- Team: UX Specialist, Angular Eng
- Path: `src/StellaOps.Web/TASKS.md`
1. [DONE] WEB1.TRIVY-SETTINGS — Implement Trivy DB exporter settings panel with `publishFull`, `publishDelta`, `includeFull`, `includeDelta` toggles and “Run export now” action using future `/exporters/trivy-db/settings` API.
• Prereqs: —
• Current: DONE (2025-10-21) Angular route `/concelier/trivy-db-settings` with reactive form, API client, and run-now workflow built; see `TrivyDbSettingsPageComponent`.
2. [BLOCKED] WEB1.TRIVY-SETTINGS-TESTS — Add headless UI test run (`ng test --watch=false`) and document prerequisites once Angular tooling is chained up.
2. [DONE 2025-10-21] WEB1.TRIVY-SETTINGS-TESTS — Add headless UI test run (`ng test --watch=false`) and document prerequisites once Angular tooling is chained up.
• Prereqs: WEB1.TRIVY-SETTINGS
• Current: BLOCKED (2025-10-21) Awaiting Angular CLI/toolchain availability in CI/local dev environments before wiring Karma tests for the new screen.
• Current: DONE (2025-10-21) ChromeHeadless launcher + README updates merged; awaiting dependency hardening follow-up (WEB1.DEPS-13-001).
3. [TODO] WEB1.DEPS-13-001 — Stabilise Angular workspace dependencies for headless CI installs (`npm install`, Chromium handling, docs).
• Prereqs: WEB1.TRIVY-SETTINGS-TESTS
• Current: TODO Capture deterministic lockfile flow, cache Puppeteer downloads, and validate `npm test` from clean checkout in air-gapped mode.
- **Sprint 1** · Developer Tooling
- Team: DevEx/CLI
- Path: `src/StellaOps.Cli/TASKS.md`
@@ -214,9 +214,6 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
1. [DOING] FEEDWEB-DOCS-01-001 — Document authority toggle & scope requirements — Quickstart updates are staged; awaiting Docs guild review before publishing operator guide refresh.
• Prereqs: —
• Current: DOING (2025-10-10)
2. [DONE] FEEDWEB-OPS-01-006 — Rename plugin drop directory to namespaced path — Build outputs now target `StellaOps.Concelier.PluginBinaries`/`StellaOps.Authority.PluginBinaries`, plugin host defaults updated, and docs/tests refreshed (see `dotnet test src/StellaOps.Concelier.WebService.Tests/StellaOps.Concelier.WebService.Tests.csproj --no-restore`).
• Prereqs: —
• Current: TODO
3. [BLOCKED] FEEDWEB-OPS-01-007 — Authority resilience adoption — Roll out retry/offline knobs to deployment docs and align CLI parity once LIB5 resilience options land; unblock when library release is available and docs review completes.
• Prereqs: —
• Current: BLOCKED (2025-10-10)
@@ -245,9 +242,6 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
• Current: TODO Add verification helpers for Worker/WebService, metrics/logging hooks, and negative-path regression tests.
- Team: Team Excititor WebService
- Path: `src/StellaOps.Excititor.WebService/TASKS.md`
1. [DONE] EXCITITOR-WEB-01-002 — EXCITITOR-WEB-01-002 Ingest & reconcile endpoints
• Prereqs: EXCITITOR-WEB-01-001 (external/completed)
• Current: DONE (2025-10-20) `/excititor/init`, `/excititor/ingest/run`, `/excititor/ingest/resume`, `/excititor/reconcile` enforce `vex.admin`, normalize provider inputs, and emit deterministic summaries; verified via `dotnet test src/StellaOps.Excititor.WebService.Tests/StellaOps.Excititor.WebService.Tests.csproj --filter FullyQualifiedName~IngestEndpointsTests`.
2. [TODO] EXCITITOR-WEB-01-003 — EXCITITOR-WEB-01-003 Export & verify endpoints
• Prereqs: EXCITITOR-WEB-01-001 (external/completed), EXCITITOR-EXPORT-01-001 (external/completed), EXCITITOR-ATTEST-01-001 (external/completed)
• Current: TODO Add `/excititor/export`, `/excititor/export/{id}`, `/excititor/export/{id}/download`, `/excititor/verify`, returning artifact + attestation metadata with cache awareness.
@@ -299,100 +293,27 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
2. [TODO] EXCITITOR-FMT-OPENVEX-01-003 — EXCITITOR-FMT-OPENVEX-01-003 OpenVEX export writer
• Prereqs: EXCITITOR-EXPORT-01-001 (external/completed), EXCITITOR-FMT-OPENVEX-01-001 (external/completed)
• Current: TODO Provide export serializer generating canonical OpenVEX documents with optional SBOM references and hash-stable ordering.
- Team: Team Excititor Worker
- Path: `src/StellaOps.Excititor.Worker/TASKS.md`
1. [DONE 2025-10-21] EXCITITOR-WORKER-01-002 — EXCITITOR-WORKER-01-002 Resume tokens & retry policy
• Prereqs: EXCITITOR-WORKER-01-001 (external/completed)
• Current: DONE Worker updates connector state with resume tokens + success/failure metadata and applies jittered exponential backoff with quarantine scheduling; unit coverage added for skip/backoff/resume flows.
- **Sprint 7** · Contextual Truth Foundations
- Team: Team Excititor Export
- Path: `src/StellaOps.Excititor.Export/TASKS.md`
1. [TODO] EXCITITOR-EXPORT-01-005 — EXCITITOR-EXPORT-01-005 Score & resolve envelope surfaces
1. [DONE 2025-10-21] EXCITITOR-EXPORT-01-005 — EXCITITOR-EXPORT-01-005 Score & resolve envelope surfaces
• Prereqs: EXCITITOR-EXPORT-01-004 (external/completed), EXCITITOR-CORE-02-001 (external/completed)
• Current: TODO Emit consensus+score envelopes in export manifests, include policy/scoring digests, and update offline bundle/ORAS layouts to carry signed VEX responses.
- Team: Team Excititor WebService
- Path: `src/StellaOps.Excititor.WebService/TASKS.md`
1. [DONE 2025-10-20] EXCITITOR-WEB-01-004 — Resolve API & signed responses expose `/excititor/resolve`, return signed consensus/score envelopes, document auth.
• Prereqs: —
• Current: TODO
- Team: Team Excititor Worker
- Path: `src/StellaOps.Excititor.Worker/TASKS.md`
1. [DONE 2025-10-21] EXCITITOR-WORKER-01-004 — EXCITITOR-WORKER-01-004 TTL refresh & stability damper
• Prereqs: EXCITITOR-WORKER-01-001 (external/completed), EXCITITOR-CORE-02-001 (external/completed)
• Current: TODO Monitor consensus/VEX TTLs, apply 2448h dampers before flipping published status/score, and trigger re-resolve when base image or kernel fingerprints change.
- **Sprint 8** · Mongo strengthening
- Team: Authority Core & Storage Guild
- Path: `src/StellaOps.Authority/TASKS.md`
1. [DONE] AUTHSTORAGE-MONGO-08-001 — Harden Authority Mongo usage — Scoped Mongo sessions with majority read/write concerns wired through stores and GraphQL/HTTP pipelines; replica-set election regression validated.
• Prereqs: —
• Current: BLOCKED (2025-10-19)
- Team: Team Excititor Storage
- Path: `src/StellaOps.Excititor.Storage.Mongo/TASKS.md`
1. [DONE 2025-10-19] EXCITITOR-STORAGE-MONGO-08-001 — Session + causal consistency hardening shipped with scoped session provider, repository updates, and replica-set consistency tests (`dotnet test src/StellaOps.Excititor.Storage.Mongo.Tests/StellaOps.Excititor.Storage.Mongo.Tests.csproj`)
• Prereqs: EXCITITOR-STORAGE-01-003 (external/completed)
• Current: DONE Scoped sessions with causal consistency in place; repositories/tests updated for deterministic read-your-write semantics.
- Team: Team Normalization & Storage Backbone
- Path: `src/StellaOps.Concelier.Storage.Mongo/TASKS.md`
1. [DONE] FEEDSTORAGE-MONGO-08-001 — Causal-consistent Concelier storage sessions — Scoped session facilitator registered, repositories accept optional session handles, and replica-set failover tests verify read-your-write + monotonic reads.
• Prereqs: —
• Current: TODO
- **Sprint 8** · Platform Maintenance
- Team: Team Excititor Storage
- Path: `src/StellaOps.Excititor.Storage.Mongo/TASKS.md`
1. [DONE 2025-10-19] EXCITITOR-STORAGE-03-001 — Statement backfill tooling
• Prereqs: EXCITITOR-STORAGE-02-001 (external/completed)
• Current: DONE Admin backfill endpoint, CLI command (`stellaops excititor backfill-statements`), integration coverage, and operator runbook published; further automation tracked separately if needed.
- Team: Team Excititor Worker
- Path: `src/StellaOps.Excititor.Worker/TASKS.md`
1. [DONE 2025-10-21] EXCITITOR-WORKER-02-001 — EXCITITOR-WORKER-02-001 Resolve Microsoft.Extensions.Caching.Memory advisory
• Prereqs: EXCITITOR-WORKER-01-001 (external/completed)
• Current: DONE (2025-10-21) Upgraded Excititor workers/connectors to `Microsoft.Extensions.*` 10.0.0-preview.7.25380.108, restored attestation diagnostics, and re-ran worker + webservice test suites with no NU1903 vulnerabilities.
- **Sprint 8** · Plugin Infrastructure
- Team: Plugin Platform Guild
- Path: `src/StellaOps.Plugin/TASKS.md`
1. [TODO] PLUGIN-DI-08-001 — Scoped service support in plugin bootstrap — Teach the plugin loader/registrar to surface services with scoped lifetimes, honour `StellaOps.DependencyInjection` metadata, and document the new contract.
• Prereqs: —
• Current: TODO
- Team: Plugin Platform Guild, Authority Core
- Path: `src/StellaOps.Plugin/TASKS.md`
1. [DONE] PLUGIN-DI-08-002 — Update Authority plugin integration — Flow scoped services through identity-provider registrars, bootstrap flows, and background jobs; add regression coverage around scoped lifetimes. (Implemented 2025-10-20 with scoped Standard plugin registrations and registry handles.)
• Prereqs: —
• Current: DONE (2025-10-20) Standard registrar registers scoped credential/provisioning stores and identity-provider plugins, registry Acquire returns scoped handles, and tests `dotnet test src/StellaOps.Authority/StellaOps.Authority.Plugin.Standard.Tests/StellaOps.Authority.Plugin.Standard.Tests.csproj` + `dotnet test src/StellaOps.Authority/StellaOps.Authority.Tests/StellaOps.Authority.Tests.csproj` validate behaviour.
- **Sprint 9** · Docs & Governance
- Team: Platform Events Guild
- Path: `docs/TASKS.md`
1. [TODO] PLATFORM-EVENTS-09-401 — Embed canonical event samples into contract/integration tests and ensure CI validates payloads against published schemas.
• Prereqs: DOCS-EVENTS-09-003 (external/completed)
• Current: TODO
- Team: Runtime Guild
- Path: `docs/TASKS.md`
1. [TODO] RUNTIME-GUILD-09-402 — Confirm Scanner WebService surfaces `quietedFindingCount` and progress hints to runtime consumers; document readiness checklist.
• Prereqs: SCANNER-POLICY-09-107 (external/completed)
• Current: TODO
- **Sprint 9** · Policy Foundations
- Team: Policy Guild
- Path: `src/StellaOps.Policy/TASKS.md`
1. [DONE] POLICY-CORE-09-004 — Versioned scoring config with schema validation, trust table, and golden fixtures. (2025-10-19)
• Prereqs: —
• Current: DONE (2025-10-19)
2. [DONE] POLICY-CORE-09-005 — Scoring/quiet engine compute score, enforce VEX-only quiet rules, emit inputs and provenance. (2025-10-19)
• Prereqs: —
• Current: DONE (2025-10-19)
3. [DONE] POLICY-CORE-09-006 — Unknown state & confidence decay deterministic bands surfaced in policy outputs. (2025-10-19)
• Prereqs: —
• Current: DONE (2025-10-19)
- **Sprint 10** · Backlog
- Team: TBD
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-302C — Surface script metadata (postinstall/preinstall) and policy hints; emit telemetry counters and evidence records.
• Prereqs: SCANNER-ANALYZERS-LANG-10-302B (external/completed)
• Current: TODO
- **Sprint 10** · DevOps Perf
- Team: DevOps Guild
- Path: `ops/devops/TASKS.md`
1. [DONE] DEVOPS-SEC-10-301 — Address NU1902/NU1903 advisories for `MongoDB.Driver` 2.12.0 and `SharpCompress` 0.23.0 surfaced during scanner cache and worker test runs (2025-10-20) local Mongo2Go feed repacked to require MongoDB.Driver 3.5.0 and SharpCompress 0.41.0; targeted cache tests green.
• Prereqs: —
• Current: TODO
- **Sprint 10** · Scanner Analyzers & SBOM
- Team: Diff Guild
- Path: `src/StellaOps.Scanner.Diff/TASKS.md`
@@ -477,23 +398,10 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
• Current: TODO
- Team: Authority Core & Security Guild
- Path: `src/StellaOps.Authority/TASKS.md`
1. [DONE] AUTH-DPOP-11-001 — Implement DPoP proof validation + nonce handling for high-value audiences per architecture. (Redis-configurable nonce store + docs landed 2025-10-20)
• Prereqs: —
• Current: DOING (2025-10-19)
2. [DOING] AUTH-MTLS-11-002 — Add OAuth mTLS client credential support with certificate-bound tokens and introspection updates.
• Prereqs: —
• Current: DOING (2025-10-19)
- Team: Signer Guild
- Path: `src/StellaOps.Signer/TASKS.md`
1. [DONE] SIGNER-API-11-101 — `/sign/dsse` pipeline with Authority auth, PoE introspection, release verification, DSSE signing.
• Prereqs: —
• Current: DONE (2025-10-21) Minimal API host now issues DSSE bundles with PoE validation, release verification, and quota enforcement; integration tests cover success/error paths via `dotnet test src/StellaOps.Signer/StellaOps.Signer.Tests/StellaOps.Signer.Tests.csproj`.
2. [DONE] SIGNER-REF-11-102 — `/verify/referrers` endpoint with OCI lookup, caching, and policy enforcement.
• Prereqs: —
• Current: DONE (2025-10-21) Added `/api/v1/signer/verify/referrers` returning deterministic JSON responses for trusted/untrusted digests with regression coverage.
3. [DONE] SIGNER-QUOTA-11-103 — Enforce plan quotas, concurrency/QPS limits, artifact size caps with metrics/audit logs.
• Prereqs: —
• Current: DONE (2025-10-21) In-memory quota service applies payload caps and per-tenant QPS throttles; tests cover oversize and throttled cases.
- **Sprint 12** · Runtime Guardrails
- Team: Zastava Core Guild
- Path: `src/StellaOps.Zastava.Core/TASKS.md`
@@ -559,28 +467,11 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
• Current: TODO
- Team: Scanner WebService Guild
- Path: `src/StellaOps.Scanner.WebService/TASKS.md`
1. [DONE] SCANNER-EVENTS-15-201 — Emit `scanner.report.ready` + `scanner.scan.completed` events.
• Prereqs: —
• Current: TODO
2. [BLOCKED] SCANNER-EVENTS-16-301 — Redis publisher integration tests once Notify queue adapter ships.
• Prereqs: NOTIFY-QUEUE-15-401 (Wave 1)
• Current: BLOCKED waiting on Notify queue abstraction and Redis adapter deliverables for end-to-end validation.
- **Sprint 16** · Scheduler Intelligence
- Team: Scheduler ImpactIndex Guild
- Path: `src/StellaOps.Scheduler.ImpactIndex/TASKS.md`
1. [DONE (2025-10-20)] SCHED-IMPACT-16-300 — **STUB** ingest/query using fixtures to unblock Scheduler planning (remove by SP16 end).
• Prereqs: SAMPLES-10-001 (external/completed)
• Current: DOING
- Team: Scheduler Models Guild
- Path: `src/StellaOps.Scheduler.Models/TASKS.md`
1. [DONE (2025-10-20)] SCHED-MODELS-16-103 - Versioning/migration helpers (schedule evolution, run state transitions).
• Prereqs: SCHED-MODELS-16-101 (external/completed)
• Current: DONE
- Team: Scheduler Queue Guild
- Path: `src/StellaOps.Scheduler.Queue/TASKS.md`
1. [DONE (2025-10-20)] SCHED-QUEUE-16-401 - Implement queue abstraction + Redis Streams adapter (planner inputs, runner segments) with ack/lease semantics.
• Prereqs: SCHED-MODELS-16-101 (external/completed)
• Current: DONE
- Team: Scheduler Storage Guild
- Path: `src/StellaOps.Scheduler.Storage.Mongo/TASKS.md`
1. [TODO] SCHED-STORAGE-16-201 — Create Mongo collections (schedules, runs, impact_cursors, locks, audit) with indexes/migrations per architecture.
@@ -634,18 +525,9 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- **Sprint 7** · Contextual Truth Foundations
- Team: Team Excititor Export
- Path: `src/StellaOps.Excititor.Export/TASKS.md`
1. [TODO] EXCITITOR-EXPORT-01-006 — EXCITITOR-EXPORT-01-006 Quiet provenance packaging
1. [DONE 2025-10-21] EXCITITOR-EXPORT-01-006 — EXCITITOR-EXPORT-01-006 Quiet provenance packaging
• Prereqs: EXCITITOR-EXPORT-01-005 (Wave 0), POLICY-CORE-09-005 (Wave 0)
• Current: TODO Attach `quietedBy` statement IDs, signers, and justification codes to exports/offline bundles, mirror metadata into attested manifest, and add regression fixtures.
- **Sprint 9** · DevOps Foundations
- Team: DevOps Guild, Scanner WebService Guild
- Path: `ops/devops/TASKS.md`
1. [DONE] DEVOPS-SCANNER-09-204 — Surface `SCANNER__EVENTS__*` environment variables across docker-compose (dev/stage/airgap) and Helm values, defaulting to share the Redis queue DSN. (2025-10-21)
• Prereqs: SCANNER-EVENTS-15-201 (Wave 0)
• Current: DONE (2025-10-21) Compose dev/stage/airgap profiles and Helm values now expose the SCANNER__EVENTS__* toggles; docs (deploy/compose/README.md, docs/ARCHITECTURE_SCANNER.md) call out the new configuration knobs.
2. [DONE] DEVOPS-SCANNER-09-205 — Add Notify smoke stage that tails the Redis stream and asserts `scanner.report.ready`/`scanner.scan.completed` reach Notify WebService in staging. (2025-10-21)
• Prereqs: DEVOPS-SCANNER-09-204 (Wave 0)
• Current: DONE (2025-10-21) `notify-smoke` CI job runs the NotifySmokeCheck tool against staging Redis/Notify using configured secrets; deploy docs enumerate required configuration.
- **Sprint 10** · Backlog
- Team: TBD
- Path: `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`
@@ -668,12 +550,6 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
1. [TODO] SCANNER-ANALYZERS-LANG-10-306A — Parse Cargo metadata (`Cargo.lock`, `.fingerprint`, `.metadata`) and map crates to components with evidence.
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
• Current: TODO
- **Sprint 10** · Benchmarks
- Team: Bench Guild, Language Analyzer Guild
- Path: `bench/TASKS.md`
1. [DONE] BENCH-SCANNER-10-002 — Wire real language analyzers into bench harness & refresh baselines post-implementation. (2025-10-21)
• Prereqs: SCANNER-ANALYZERS-LANG-10-301 (Wave 0)
• Current: DONE (2025-10-21) Harness now invokes language analyzers via `StellaOps.Bench.ScannerAnalyzers`, baseline refreshed against samples/runtime fixtures, and README/config updated for the new flow.
- **Sprint 10** · Scanner Analyzers & SBOM
- Team: Emit Guild
- Path: `src/StellaOps.Scanner.Emit/TASKS.md`
@@ -709,9 +585,6 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- **Sprint 12** · Runtime Guardrails
- Team: Scanner WebService Guild
- Path: `src/StellaOps.Scanner.WebService/TASKS.md`
1. [DONE] SCANNER-RUNTIME-12-301 — Implement `/runtime/events` ingestion endpoint with validation, batching, and storage hooks per Zastava contract. (2025-10-20)
• Prereqs: ZASTAVA-CORE-12-201 (Wave 0)
• Current: DONE (2025-10-20) — Mongo persistence + rate limiting shipped; observer fixtures can replay batches end-to-end.
2. [DOING] SCANNER-RUNTIME-12-302 — Implement `/policy/runtime` endpoint joining SBOM baseline + policy verdict, returning admission guidance.
• Prereqs: SCANNER-RUNTIME-12-301 (Wave 1), ZASTAVA-CORE-12-201 (Wave 0)
• Current: DOING (2025-10-20) — Locking response schema with Policy/CLI guilds, wiring determinism tests.
@@ -729,6 +602,11 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
1. [TODO] CLI-RUNTIME-13-009 — CLI-RUNTIME-13-009 Runtime policy smoke fixture
• Prereqs: CLI-RUNTIME-13-005 (Wave 0)
• Current: TODO Build Spectre test harness exercising `runtime policy test` against a stubbed backend to lock output shape (table + `--json`) and guard regressions. Integrate into `dotnet test` suite.
- Team: UX Specialist, Angular Eng, DevEx
- Path: `src/StellaOps.Web/TASKS.md`
1. [TODO] WEB1.DEPS-13-001 — Stabilise Angular workspace dependencies for headless CI installs (`npm install`, Chromium handling, docs).
• Prereqs: WEB1.TRIVY-SETTINGS-TESTS (Wave 0)
• Current: TODO Capture deterministic lockfile flow, cache Puppeteer downloads, validate `npm test` from clean checkout offline, and update README.
- Team: UI Guild
- Path: `src/StellaOps.UI/TASKS.md`
1. [TODO] UI-VEX-13-003 — Implement VEX explorer + policy editor with preview integration.
@@ -752,6 +630,12 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
7. [TODO] UI-SCHED-13-005 — Scheduler panel: schedules CRUD, run history, dry-run preview using API/mocks.
• Prereqs: SCHED-WEB-16-101 (Wave 0)
• Current: TODO
- **Sprint 13** · Platform Reliability
- Team: DevOps Guild, Platform Leads
- Path: `ops/devops/TASKS.md`
1. [TODO] DEVOPS-NUGET-13-001 — Add .NET 10 preview feeds/local mirrors so `dotnet restore` succeeds offline; document updated NuGet bootstrap.
• Prereqs: DEVOPS-REL-14-001 (Wave 1)
• Current: TODO Mirror preview packages into Offline Kit/allowlisted feeds, update NuGet.config mapping, and refresh restore documentation.
- **Sprint 14** · Release & Offline Ops
- Team: DevOps Guild
- Path: `ops/devops/TASKS.md`
@@ -774,25 +658,14 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
1. [TODO] NOTIFY-QUEUE-15-401 — Build queue abstraction + Redis Streams adapter with ack/claim APIs, idempotency tokens, serialization contracts.
• Prereqs: NOTIFY-MODELS-15-101 (Wave 0)
• Current: TODO
- Team: Notify WebService Guild
- Path: `src/StellaOps.Notify.WebService/TASKS.md`
1. [DONE] NOTIFY-WEB-15-103 — Delivery history + test-send endpoints with rate limits.
• Prereqs: NOTIFY-WEB-15-102 (Wave 0)
• Current: TODO
- **Sprint 16** · Scheduler Intelligence
- Team: Scheduler ImpactIndex Guild
- Path: `src/StellaOps.Scheduler.ImpactIndex/TASKS.md`
1. [TODO] SCHED-IMPACT-16-301 — Implement ingestion of per-image BOM-Index sidecars into roaring bitmap store (contains/usedBy).
• Prereqs: SCANNER-EMIT-10-605 (Wave 0)
• Current: TODO
- Team: Scheduler Queue Guild
- Path: `src/StellaOps.Scheduler.Queue/TASKS.md`
1. [DONE (2025-10-20)] SCHED-QUEUE-16-402 - Add NATS JetStream adapter with configuration binding, health probes, failover.
• Prereqs: SCHED-QUEUE-16-401 (Wave 0)
• Current: DONE
2. [DONE (2025-10-20)] SCHED-QUEUE-16-403 - Dead-letter handling + metrics (queue depth, retry counts), configuration toggles.
• Prereqs: SCHED-QUEUE-16-401 (Wave 0)
• Current: DONE
- Team: Scheduler Storage Guild
- Path: `src/StellaOps.Scheduler.Storage.Mongo/TASKS.md`
1. [TODO] SCHED-STORAGE-16-203 — Audit/logging pipeline + run stats materialized views for UI.
@@ -831,15 +704,9 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- **Sprint 7** · Contextual Truth Foundations
- Team: Team Excititor Export
- Path: `src/StellaOps.Excititor.Export/TASKS.md`
1. [TODO] EXCITITOR-EXPORT-01-007 — EXCITITOR-EXPORT-01-007 Mirror bundle + domain manifest
1. [DONE 2025-10-21] EXCITITOR-EXPORT-01-007 — EXCITITOR-EXPORT-01-007 Mirror bundle + domain manifest
• Prereqs: EXCITITOR-EXPORT-01-006 (Wave 1)
• Current: TODO Create per-domain mirror bundles with consensus/score artifacts, publish signed index for downstream Excititor sync, and ensure deterministic digests + fixtures.
- **Sprint 8** · Mirror Distribution
- Team: DevOps Guild
- Path: `ops/devops/TASKS.md`
1. [DONE] DEVOPS-MIRROR-08-001 — Stand up managed mirror profiles for `*.stella-ops.org` (Concelier/Excititor), including Helm/Compose overlays, multi-tenant secrets, CDN caching, and sync documentation.
• Prereqs: DEVOPS-REL-14-001 (Wave 1)
• Current: DONE (2025-10-19)
- **Sprint 9** · DevOps Foundations
- Team: DevOps Guild, Notify Guild
- Path: `ops/devops/TASKS.md`
@@ -974,7 +841,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- **Sprint 7** · Contextual Truth Foundations
- Team: Excititor Connectors Stella
- Path: `src/StellaOps.Excititor.Connectors.StellaOpsMirror/TASKS.md`
1. [TODO] EXCITITOR-CONN-STELLA-07-001 — Implement mirror fetch client consuming `https://<domain>.stella-ops.org/excititor/exports/index.json`, validating signatures/digests, storing raw consensus bundles with provenance.
1. [DONE 2025-10-21] EXCITITOR-CONN-STELLA-07-001 — Implement mirror fetch client consuming `https://<domain>.stella-ops.org/excititor/exports/index.json`, validating signatures/digests, storing raw consensus bundles with provenance.
• Prereqs: EXCITITOR-EXPORT-01-007 (Wave 2)
• Current: TODO
- **Sprint 10** · Backlog
@@ -1009,11 +876,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
• Prereqs: ZASTAVA-OBS-12-002 (Wave 2)
• Current: TODO
- **Sprint 13** · UX & CLI Experience
- Team: DevEx/CLI
- Path: `src/StellaOps.Cli/TASKS.md`
1. [DONE] CLI-OFFLINE-13-006 — CLI-OFFLINE-13-006 Offline kit workflows
• Prereqs: DEVOPS-OFFLINE-14-002 (Wave 2)
• Current: DONE (2025-10-21) Delivered `offline kit pull/import/status` commands with resumable downloads, digest/metadata validation, CLI metrics + docs, and regression coverage (`dotnet test src/StellaOps.Cli.Tests`).
- Team: DevEx/CLI, Scanner WebService Guild
- Path: `src/StellaOps.Cli/TASKS.md`
1. [TODO] CLI-RUNTIME-13-008 — CLI-RUNTIME-13-008 Runtime policy contract sync
@@ -1153,16 +1016,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
• Prereqs: NOTIFY-CONN-EMAIL-15-701 (Wave 4)
• Current: BLOCKED waiting on base SMTP connector implementation (NOTIFY-CONN-EMAIL-15-701).
- Path: `src/StellaOps.Notify.Connectors.Slack/TASKS.md`
1. [DONE] NOTIFY-CONN-SLACK-15-502 — Health check & test-send support with minimal scopes and redacted tokens.
• Prereqs: NOTIFY-CONN-SLACK-15-501 (Wave 4)
• Current: TODO
- Path: `src/StellaOps.Notify.Connectors.Teams/TASKS.md`
1. [DONE] NOTIFY-CONN-TEAMS-15-602 — Provide health/test-send support with fallback text for legacy clients.
• Prereqs: NOTIFY-CONN-TEAMS-15-601 (Wave 4)
• Current: TODO
2. [DONE] NOTIFY-CONN-TEAMS-15-604 — Align Teams health endpoint output with preview metadata redaction.
• Prereqs: NOTIFY-CONN-TEAMS-15-602 (Wave 5)
• Current: DONE
- Path: `src/StellaOps.Notify.Connectors.Webhook/TASKS.md`
1. [DOING] NOTIFY-CONN-WEBHOOK-15-802 — Health/test-send support with signature validation hints and secret management.
• Prereqs: NOTIFY-CONN-WEBHOOK-15-801 (Wave 4)
@@ -1193,109 +1047,11 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
1. [TODO] SCANNER-ANALYZERS-LANG-10-309R — Package plug-in manifest + Offline Kit documentation; ensure Worker integration.
• Prereqs: SCANNER-ANALYZERS-LANG-10-308R (Wave 5)
• Current: TODO
- **Sprint 15** · Notify Foundations
- Team: Notify Connectors Guild
- Path: `src/StellaOps.Notify.Connectors.Email/TASKS.md`
1. [DONE] NOTIFY-CONN-EMAIL-15-703 — Package Email connector as restart-time plug-in (manifest + host registration).
• Prereqs: NOTIFY-CONN-EMAIL-15-702 (Wave 5)
• Current: TODO
- Path: `src/StellaOps.Notify.Connectors.Slack/TASKS.md`
1. [DONE] NOTIFY-CONN-SLACK-15-503 — Package Slack connector as restart-time plug-in (manifest + host registration).
• Prereqs: NOTIFY-CONN-SLACK-15-502 (Wave 5)
• Current: TODO
- Path: `src/StellaOps.Notify.Connectors.Teams/TASKS.md`
1. [DONE] NOTIFY-CONN-TEAMS-15-603 — Package Teams connector as restart-time plug-in (manifest + host registration).
• Prereqs: NOTIFY-CONN-TEAMS-15-602 (Wave 5)
• Current: TODO
- Path: `src/StellaOps.Notify.Connectors.Webhook/TASKS.md`
1. [DONE] NOTIFY-CONN-WEBHOOK-15-803 — Package Webhook connector as restart-time plug-in (manifest + host registration).
• Prereqs: NOTIFY-CONN-WEBHOOK-15-802 (Wave 5)
• Current: TODO
## Wave 7 — 1 task(s) ready after Wave 6
- **Sprint 7** · Contextual Truth Foundations
- Team: Team Core Engine & Storage Analytics
- Path: `src/StellaOps.Concelier.Core/TASKS.md`
1. [DONE] FEEDCORE-ENGINE-07-001 — FEEDCORE-ENGINE-07-001 Advisory event log & asOf queries
• Prereqs: FEEDSTORAGE-DATA-07-001 (Wave 10)
• Current: DONE (2025-10-19) `AdvisoryEventLog` service and repository abstractions landed with canonical hashing, lower-cased keys, replay API, and doc updates. Tests: `dotnet test src/StellaOps.Concelier.Core.Tests/StellaOps.Concelier.Core.Tests.csproj`.
## Wave 8 — 1 task(s) ready after Wave 7
- **Sprint 7** · Contextual Truth Foundations
- Team: Team Core Engine & Data Science
- Path: `src/StellaOps.Concelier.Core/TASKS.md`
1. [DONE] FEEDCORE-ENGINE-07-002 — FEEDCORE-ENGINE-07-002 Noise prior computation service
• Prereqs: FEEDCORE-ENGINE-07-001 (Wave 7)
• Current: DONE (2025-10-21) Added NoisePriorService with rule-based aggregation of advisory statements, repository contracts for deterministic summaries, DI helper, and unit tests covering heuristics and persistence.
## Wave 9 — 1 task(s) ready after Wave 8
- **Sprint 7** · Contextual Truth Foundations
- Team: Team Core Engine & Storage Analytics
- Path: `src/StellaOps.Concelier.Core/TASKS.md`
1. [TODO] FEEDCORE-ENGINE-07-003 — FEEDCORE-ENGINE-07-003 Unknown state ledger & confidence seeding
• Prereqs: FEEDCORE-ENGINE-07-001 (Wave 7)
• Current: TODO Persist `unknown_vuln_range/unknown_origin/ambiguous_fix` markers with initial confidence bands, expose query surface for Policy, and add fixtures validating canonical serialization.
## Wave 10 — 1 task(s) ready after Wave 9
- **Sprint 7** · Contextual Truth Foundations
- Team: Team Normalization & Storage Backbone
- Path: `src/StellaOps.Concelier.Storage.Mongo/TASKS.md`
1. [TODO] FEEDSTORAGE-DATA-07-001 — FEEDSTORAGE-DATA-07-001 Advisory statement & conflict collections
1. [DONE 2025-10-19] FEEDSTORAGE-DATA-07-001 — FEEDSTORAGE-DATA-07-001 Advisory statement & conflict collections
• Prereqs: FEEDMERGE-ENGINE-07-001 (Wave 11)
• Current: TODO Create `advisory_statements` (immutable) and `advisory_conflicts` collections, define `asOf`/`vulnerabilityKey` indexes, and document migration/rollback steps for event-sourced merge.
## Wave 11 — 1 task(s) ready after Wave 10
- **Sprint 7** · Contextual Truth Foundations
- Team: BE-Merge
- Path: `src/StellaOps.Concelier.Merge/TASKS.md`
1. [DONE] FEEDMERGE-ENGINE-07-001 — Conflict sets & explainers (2025-10-20) Merge now returns conflict summaries with hashes and WebService exposes structured explainers.
• Prereqs: FEEDSTORAGE-DATA-07-001 (Wave 10)
• Current: TODO Persist conflict sets referencing advisory statements, output rule/explainer payloads with replay hashes, and add integration tests covering deterministic `asOf` evaluations.
## Wave 12 — 1 task(s) ready after Wave 11
- **Sprint 8** · Mirror Distribution
- Team: Concelier Export Guild
- Path: `src/StellaOps.Concelier.Exporter.Json/TASKS.md`
1. [DONE] CONCELIER-EXPORT-08-201 — CONCELIER-EXPORT-08-201 Mirror bundle + domain manifest
• Prereqs: FEEDCORE-ENGINE-07-001 (Wave 7)
• Current: DONE (2025-10-19) Mirror bundles + manifests + signed index shipped; regression coverage via `dotnet test src/StellaOps.Concelier.Exporter.Json.Tests/StellaOps.Concelier.Exporter.Json.Tests.csproj` (2025-10-19).
## Wave 13 — 1 task(s) ready after Wave 12
- **Sprint 8** · Mirror Distribution
- Team: Concelier Export Guild
- Path: `src/StellaOps.Concelier.Exporter.TrivyDb/TASKS.md`
1. [DONE] CONCELIER-EXPORT-08-202 — CONCELIER-EXPORT-08-202 Mirror-ready Trivy DB bundles
• Prereqs: CONCELIER-EXPORT-08-201 (Wave 12)
• Current: DONE (2025-10-19) Trivy exporter mirror options produce `mirror/index.json` plus per-domain manifest/metadata/db files with reproducible SHA-256 digests; validated via `dotnet test src/StellaOps.Concelier.Exporter.TrivyDb.Tests/StellaOps.Concelier.Exporter.TrivyDb.Tests.csproj`.
## Wave 14 — 1 task(s) ready after Wave 13
- **Sprint 8** · Mirror Distribution
- Team: Concelier WebService Guild
- Path: `src/StellaOps.Concelier.WebService/TASKS.md`
1. [DONE] CONCELIER-WEB-08-201 — Mirror distribution endpoints (2025-10-20) Service enforces Authority/bypass rules, issues cache headers, rate limits per domain, and ops docs list smoke tests.
• Prereqs: CONCELIER-EXPORT-08-201 (Wave 12), DEVOPS-MIRROR-08-001 (Wave 2)
• Current: DONE (2025-10-20) See `docs/ops/concelier-mirror-operations.md` for updated auth + rate-limit guidance; tests `WebServiceEndpointsTests` cover 401/Retry-After.
## Wave 15 — 1 task(s) ready after Wave 14
- **Sprint 8** · Mirror Distribution
- Team: BE-Conn-Stella
- Path: `src/StellaOps.Concelier.Connector.StellaOpsMirror/TASKS.md`
1. [DONE] FEEDCONN-STELLA-08-001 — Implement Concelier mirror fetcher hitting `https://<domain>.stella-ops.org/concelier/exports/index.json`, verify signatures/digests, and persist raw documents with provenance.
• Prereqs: CONCELIER-EXPORT-08-201 (Wave 12)
• Current: DONE (2025-10-20) Fetch job persists manifest/bundle metadata, enforces digest and detached JWS verification (fallback PEM support), and regression coverage captured via `dotnet test src/StellaOps.Concelier.Connector.StellaOpsMirror.Tests/StellaOps.Concelier.Connector.StellaOpsMirror.Tests.csproj`.
## Wave 16 — 1 task(s) ready after Wave 15
- **Sprint 8** · Mirror Distribution
- Team: BE-Conn-Stella
- Path: `src/StellaOps.Concelier.Connector.StellaOpsMirror/TASKS.md`
1. [DONE] FEEDCONN-STELLA-08-002 — Map mirror payloads into canonical advisory DTOs with provenance referencing mirror domain + original source metadata. (2025-10-20)
• Prereqs: FEEDCONN-STELLA-08-001 (Wave 15)
• Current: DONE (2025-10-20) `MirrorAdvisoryMapper` emits canonical advisories and fixtures assert parity with exporter outputs.
## Wave 17 — 1 task(s) ready after Wave 16
- **Sprint 8** · Mirror Distribution
- Team: BE-Conn-Stella
- Path: `src/StellaOps.Concelier.Connector.StellaOpsMirror/TASKS.md`
1. [DONE] FEEDCONN-STELLA-08-003 — Add incremental cursor + resume support (per-export fingerprint) and document configuration for downstream Concelier instances. (2025-10-20)
• Prereqs: FEEDCONN-STELLA-08-002 (Wave 16)
• Current: DONE (2025-10-20) Connector records per-export fingerprints, resumes pending documents, and ops guide documents offline configuration knobs.

View File

@@ -2,33 +2,6 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
| Sprint | Theme | Tasks File Path | Status | Type of Specialist | Task ID | Task Description |
| --- | --- | --- | --- | --- | --- | --- |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.WebService/TASKS.md | DONE (2025-10-20) | Team Excititor WebService | EXCITITOR-WEB-01-002 | Ingest & reconcile endpoints scope-enforced `/excititor/init`, `/excititor/ingest/run`, `/excititor/ingest/resume`, `/excititor/reconcile`; regression via `dotnet test … --filter FullyQualifiedName~IngestEndpointsTests`. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.WebService/TASKS.md | DONE (2025-10-20) | Team Excititor WebService | EXCITITOR-WEB-01-004 | Resolve API & signed responses expose `/excititor/resolve`, return signed consensus/score envelopes, document auth. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.WebService/TASKS.md | TODO | Team Excititor WebService | EXCITITOR-WEB-01-005 | Mirror distribution endpoints expose download APIs for downstream Excititor instances. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Worker/TASKS.md | DONE (2025-10-21) | Team Excititor Worker | EXCITITOR-WORKER-01-004 | TTL refresh & stability damper schedule re-resolve loops and guard against status flapping. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Export/TASKS.md | TODO | Team Excititor Export | EXCITITOR-EXPORT-01-005 | Score & resolve envelope surfaces include signed consensus/score artifacts in exports. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Export/TASKS.md | TODO | Team Excititor Export | EXCITITOR-EXPORT-01-006 | Quiet provenance packaging attach quieted-by statement IDs, signers, justification codes to exports and attestations. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Export/TASKS.md | TODO | Team Excititor Export | EXCITITOR-EXPORT-01-007 | Mirror bundle + domain manifest publish signed consensus bundles for mirrors. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Connectors.StellaOpsMirror/TASKS.md | TODO | Excititor Connectors Stella | EXCITITOR-CONN-STELLA-07-001 | Excititor mirror connector ingest signed mirror bundles and map to VexClaims with resume handling. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Core/TASKS.md | DONE (2025-10-21) | Team Core Engine & Data Science | FEEDCORE-ENGINE-07-002 | Noise prior computation service learn false-positive priors and expose deterministic summaries. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Core/TASKS.md | TODO | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-07-003 | Unknown state ledger & confidence seeding persist unknown flags, seed confidence bands, expose query surface. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-07-001 | Advisory statement & conflict collections provision Mongo schema/indexes for event-sourced merge. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Web/TASKS.md | BLOCKED (2025-10-21) | UX Specialist, Angular Eng | WEB1.TRIVY-SETTINGS-TESTS | Add headless UI test run (`ng test --watch=false`) and document prerequisites once Angular tooling is chained up. |
| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.Connector.StellaOpsMirror/TASKS.md | DONE (2025-10-20) | BE-Conn-Stella | FEEDCONN-STELLA-08-001 | Concelier mirror connector fetch mirror manifest, verify signatures, and hydrate canonical DTOs with resume support. |
| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.Connector.StellaOpsMirror/TASKS.md | DONE (2025-10-20) | BE-Conn-Stella | FEEDCONN-STELLA-08-002 | Map mirror payloads into canonical advisory DTOs with provenance referencing mirror domain + original source metadata. |
| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.Connector.StellaOpsMirror/TASKS.md | DONE (2025-10-20) | BE-Conn-Stella | FEEDCONN-STELLA-08-003 | Add incremental cursor + resume support (per-export fingerprint) and document configuration for downstream Concelier instances. |
| Sprint 8 | Plugin Infrastructure | src/StellaOps.Plugin/TASKS.md | DONE (2025-10-20) | Plugin Platform Guild, Authority Core | PLUGIN-DI-08-002.COORD | Authority scoped-service integration handshake<br>Workshop concluded 2025-10-20 15:0016:05UTC; decisions + follow-ups recorded in `docs/dev/authority-plugin-di-coordination.md`. |
| Sprint 8 | Plugin Infrastructure | src/StellaOps.Plugin/TASKS.md | DONE (2025-10-20) | Plugin Platform Guild, Authority Core | PLUGIN-DI-08-002 | Authority plugin integration updates scoped identity-provider services with registry handles; regression coverage via scoped registrar/unit tests. |
| Sprint 8 | Plugin Infrastructure | src/StellaOps.Authority/TASKS.md | DONE (2025-10-20) | Authority Core, Plugin Platform Guild | AUTH-PLUGIN-COORD-08-002 | Coordinate scoped-service adoption for Authority plug-in registrars<br>Workshop notes and follow-up backlog captured 2025-10-20 in `docs/dev/authority-plugin-di-coordination.md`. |
| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-WEB-09-103 | Progress streaming (SSE/JSONL) with correlation IDs and ISO-8601 UTC timestamps, documented in API reference. |
| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-POLICY-09-105 | Policy snapshot loader + schema + OpenAPI (YAML ignore rules, VEX include/exclude, vendor precedence). |
| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-POLICY-09-106 | `/reports` verdict assembly (Feedser+Vexer+Policy) + signed response envelope. |
| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-POLICY-09-107 | Expose score inputs, config version, and quiet provenance in `/reports` JSON and signed payload. |
| Sprint 9 | DevOps Foundations | ops/devops/TASKS.md | DONE (2025-10-21) | DevOps Guild, Scanner WebService Guild | DEVOPS-SCANNER-09-204 | Surface `SCANNER__EVENTS__*` env config across Compose/Helm and document overrides. |
| Sprint 9 | DevOps Foundations | ops/devops/TASKS.md | DONE (2025-10-21) | DevOps Guild, Notify Guild | DEVOPS-SCANNER-09-205 | Notify smoke job validates Redis stream + Notify deliveries after staging deploys. |
| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | DONE (2025-10-19) | Policy Guild | POLICY-CORE-09-004 | Versioned scoring config with schema validation, trust table, and golden fixtures. |
| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | DONE (2025-10-19) | Policy Guild | POLICY-CORE-09-005 | Scoring/quiet engine compute score, enforce VEX-only quiet rules, emit inputs and provenance. |
| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | DONE (2025-10-19) | Policy Guild | POLICY-CORE-09-006 | Unknown state & confidence decay deterministic bands surfaced in policy outputs. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-101 | Implement layer cache store keyed by layer digest with metadata retention per architecture §3.3. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-102 | Build file CAS with dedupe, TTL enforcement, and offline import/export hooks. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-103 | Expose cache metrics/logging and configuration toggles for warm/cold thresholds. |
@@ -66,13 +39,9 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-605 | Emit BOM-Index sidecar schema/fixtures (CRITICAL PATH for SP16). |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-606 | Usage view bit flags integrated with EntryTrace. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-607 | Embed scoring inputs, confidence band, and quiet provenance in CycloneDX/DSSE artifacts. |
| Sprint 10 | Benchmarks | bench/TASKS.md | DONE (2025-10-21) | Bench Guild, Language Analyzer Guild | BENCH-SCANNER-10-002 | Wire real language analyzers into bench harness & refresh baselines post-implementation. |
| Sprint 10 | Samples | samples/TASKS.md | TODO | Samples Guild, Scanner Team | SAMPLES-10-001 | Sample images with SBOM/BOM-Index sidecars. |
| Sprint 10 | DevOps Perf | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-PERF-10-001 | Perf smoke job ensuring <5s SBOM compose. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Authority/TASKS.md | DOING (2025-10-19) | Authority Core & Security Guild | AUTH-MTLS-11-002 | Add OAuth mTLS client credential support with certificate-bound tokens and introspection updates. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Signer/TASKS.md | DONE (2025-10-21) | Signer Guild | SIGNER-API-11-101 | `/sign/dsse` pipeline with Authority auth, PoE introspection, release verification, DSSE signing. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Signer/TASKS.md | DONE (2025-10-21) | Signer Guild | SIGNER-REF-11-102 | `/verify/referrers` endpoint with OCI lookup, caching, and policy enforcement. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Signer/TASKS.md | DONE (2025-10-21) | Signer Guild | SIGNER-QUOTA-11-103 | Enforce plan quotas, concurrency/QPS limits, artifact size caps with metrics/audit logs. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Attestor/TASKS.md | TODO | Attestor Guild | ATTESTOR-API-11-201 | `/rekor/entries` submission pipeline with dedupe, proof acquisition, and persistence. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Attestor/TASKS.md | TODO | Attestor Guild | ATTESTOR-VERIFY-11-202 | `/rekor/verify` + retrieval endpoints validating signatures and Merkle proofs. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Attestor/TASKS.md | TODO | Attestor Guild | ATTESTOR-OBS-11-203 | Telemetry, alerting, mTLS hardening, and archive workflow for Attestor. |
@@ -88,7 +57,6 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Webhook/TASKS.md | TODO | Zastava Webhook Guild | ZASTAVA-WEBHOOK-12-101 | Admission controller host with TLS bootstrap and Authority auth. |
| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Webhook/TASKS.md | TODO | Zastava Webhook Guild | ZASTAVA-WEBHOOK-12-102 | Query Scanner `/policy/runtime`, resolve digests, enforce verdicts. |
| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Webhook/TASKS.md | TODO | Zastava Webhook Guild | ZASTAVA-WEBHOOK-12-103 | Caching, fail-open/closed toggles, metrics/logging for admission decisions. |
| Sprint 12 | Runtime Guardrails | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-20) | Scanner WebService Guild | SCANNER-RUNTIME-12-301 | `/runtime/events` ingestion endpoint with validation, batching, storage hooks. |
| Sprint 12 | Runtime Guardrails | src/StellaOps.Scanner.WebService/TASKS.md | DOING (2025-10-20) | Scanner WebService Guild | SCANNER-RUNTIME-12-302 | `/policy/runtime` endpoint joining SBOM baseline + policy verdict, returning admission guidance. |
| Sprint 12 | Runtime Guardrails | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-RUNTIME-12-303 | Align `/policy/runtime` verdicts with canonical policy evaluation (Feedser/Vexer). |
| Sprint 12 | Runtime Guardrails | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-RUNTIME-12-304 | Integrate attestation verification into runtime policy metadata. |
@@ -100,8 +68,9 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SCHED-13-005 | Scheduler panel: schedules CRUD, run history, dry-run preview. |
| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | DOING (2025-10-19) | UI Guild | UI-NOTIFY-13-006 | Notify panel: channels/rules CRUD, deliveries view, test send. |
| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI | CLI-RUNTIME-13-005 | Add runtime policy test verbs that consume `/policy/runtime` and display verdicts. |
| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | DONE (2025-10-21) | DevEx/CLI | CLI-OFFLINE-13-006 | Implement offline kit pull/import/status commands with integrity checks. |
| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI | CLI-PLUGIN-13-007 | Package non-core CLI verbs as restart-time plug-ins (manifest + loader tests). |
| Sprint 13 | UX & CLI Experience | src/StellaOps.Web/TASKS.md | TODO | UX Specialist, Angular Eng, DevEx | WEB1.DEPS-13-001 | Stabilise Angular workspace dependencies for headless CI installs (`npm install`, Chromium handling, docs). |
| Sprint 13 | Platform Reliability | ops/devops/TASKS.md | TODO | DevOps Guild, Platform Leads | DEVOPS-NUGET-13-001 | Wire up .NET 10 preview feeds/local mirrors so `dotnet restore` succeeds offline; document updated NuGet bootstrap. |
| Sprint 14 | Release & Offline Ops | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-REL-14-001 | Deterministic build/release pipeline with SBOM/provenance, signing, and manifest generation. |
| Sprint 14 | Release & Offline Ops | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild | DEVOPS-OFFLINE-14-002 | Offline kit packaging workflow with integrity verification and documentation. |
| Sprint 14 | Release & Offline Ops | ops/deployment/TASKS.md | TODO | Deployment Guild | DEVOPS-OPS-14-003 | Deployment/update/rollback automation and channel management documentation. |
@@ -139,7 +108,6 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Storage.Mongo/TASKS.md | TODO | Scheduler Storage Guild | SCHED-STORAGE-16-201 | Mongo schemas/indexes for Scheduler state. |
| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Storage.Mongo/TASKS.md | TODO | Scheduler Storage Guild | SCHED-STORAGE-16-202 | Repositories with tenant scoping, TTL, causal consistency. |
| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Storage.Mongo/TASKS.md | TODO | Scheduler Storage Guild | SCHED-STORAGE-16-203 | Audit + stats materialization for UI. |
| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Queue/TASKS.md | DONE (2025-10-20) | Scheduler Queue Guild | SCHED-QUEUE-16-403 | Dead-letter handling + metrics. |
| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.ImpactIndex/TASKS.md | TODO | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-301 | Ingest BOM-Index into roaring bitmap store. |
| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.ImpactIndex/TASKS.md | TODO | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-302 | Query APIs for ResolveByPurls/ResolveByVulns/ResolveAll. |
| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.ImpactIndex/TASKS.md | TODO | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-303 | Snapshot/compaction/invalidation workflow. |

40
SPRINTS_PRIOR_20251021.md Normal file
View File

@@ -0,0 +1,40 @@
This file describe implementation of Stella Ops (docs/README.md). Implementation must respect rules from AGENTS.md (read if you have not).
| Sprint | Theme | Tasks File Path | Status | Type of Specialist | Task ID | Task Description |
| --- | --- | --- | --- | --- | --- | --- |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.WebService/TASKS.md | DONE (2025-10-20) | Team Excititor WebService | EXCITITOR-WEB-01-002 | Ingest & reconcile endpoints scope-enforced `/excititor/init`, `/excititor/ingest/run`, `/excititor/ingest/resume`, `/excititor/reconcile`; regression via `dotnet test … --filter FullyQualifiedName~IngestEndpointsTests`. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.WebService/TASKS.md | DONE (2025-10-20) | Team Excititor WebService | EXCITITOR-WEB-01-004 | Resolve API & signed responses expose `/excititor/resolve`, return signed consensus/score envelopes, document auth. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Worker/TASKS.md | DONE (2025-10-21) | Team Excititor Worker | EXCITITOR-WORKER-01-004 | TTL refresh & stability damper schedule re-resolve loops and guard against status flapping. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Core/TASKS.md | DONE (2025-10-21) | Team Core Engine & Data Science | FEEDCORE-ENGINE-07-002 | Noise prior computation service learn false-positive priors and expose deterministic summaries. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Core/TASKS.md | DONE (2025-10-21) | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-07-003 | Unknown state ledger & confidence seeding persist unknown flags, seed confidence bands, expose query surface. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.WebService/TASKS.md | DONE (2025-10-19) | Team Excititor WebService | EXCITITOR-WEB-01-005 | Mirror distribution endpoints expose download APIs for downstream Excititor instances. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Export/TASKS.md | DONE (2025-10-21) | Team Excititor Export | EXCITITOR-EXPORT-01-005 | Score & resolve envelope surfaces include signed consensus/score artifacts in exports. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Export/TASKS.md | DONE (2025-10-21) | Team Excititor Export | EXCITITOR-EXPORT-01-006 | Quiet provenance packaging attach quieted-by statement IDs, signers, justification codes to exports and attestations. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Export/TASKS.md | DONE (2025-10-21) | Team Excititor Export | EXCITITOR-EXPORT-01-007 | Mirror bundle + domain manifest publish signed consensus bundles for mirrors. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Connectors.StellaOpsMirror/TASKS.md | DONE (2025-10-21) | Excititor Connectors Stella | EXCITITOR-CONN-STELLA-07-001 | Excititor mirror connector ingest signed mirror bundles and map to VexClaims with resume handling. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-19) | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-07-001 | Advisory statement & conflict collections provision Mongo schema/indexes for event-sourced merge. |
| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Web/TASKS.md | DONE (2025-10-21) | UX Specialist, Angular Eng | WEB1.TRIVY-SETTINGS-TESTS | Add headless UI test run (`ng test --watch=false`) and document prerequisites once Angular tooling is chained up. |
| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.Connector.StellaOpsMirror/TASKS.md | DONE (2025-10-20) | BE-Conn-Stella | FEEDCONN-STELLA-08-001 | Concelier mirror connector fetch mirror manifest, verify signatures, and hydrate canonical DTOs with resume support. |
| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.Connector.StellaOpsMirror/TASKS.md | DONE (2025-10-20) | BE-Conn-Stella | FEEDCONN-STELLA-08-002 | Map mirror payloads into canonical advisory DTOs with provenance referencing mirror domain + original source metadata. |
| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.Connector.StellaOpsMirror/TASKS.md | DONE (2025-10-20) | BE-Conn-Stella | FEEDCONN-STELLA-08-003 | Add incremental cursor + resume support (per-export fingerprint) and document configuration for downstream Concelier instances. |
| Sprint 8 | Plugin Infrastructure | src/StellaOps.Plugin/TASKS.md | DONE (2025-10-21) | Plugin Platform Guild | PLUGIN-DI-08-001 | Scoped service support in plugin bootstrap added dynamic plugin tests ensuring `[ServiceBinding]` metadata flows through plugin hosts and remains idempotent. |
| Sprint 8 | Plugin Infrastructure | src/StellaOps.Plugin/TASKS.md | DONE (2025-10-20) | Plugin Platform Guild, Authority Core | PLUGIN-DI-08-002.COORD | Authority scoped-service integration handshake<br>Workshop concluded 2025-10-20 15:0016:05UTC; decisions + follow-ups recorded in `docs/dev/authority-plugin-di-coordination.md`. |
| Sprint 8 | Plugin Infrastructure | src/StellaOps.Plugin/TASKS.md | DONE (2025-10-20) | Plugin Platform Guild, Authority Core | PLUGIN-DI-08-002 | Authority plugin integration updates scoped identity-provider services with registry handles; regression coverage via scoped registrar/unit tests. |
| Sprint 8 | Plugin Infrastructure | src/StellaOps.Authority/TASKS.md | DONE (2025-10-20) | Authority Core, Plugin Platform Guild | AUTH-PLUGIN-COORD-08-002 | Coordinate scoped-service adoption for Authority plug-in registrars<br>Workshop notes and follow-up backlog captured 2025-10-20 in `docs/dev/authority-plugin-di-coordination.md`. |
| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-WEB-09-103 | Progress streaming (SSE/JSONL) with correlation IDs and ISO-8601 UTC timestamps, documented in API reference. |
| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-POLICY-09-105 | Policy snapshot loader + schema + OpenAPI (YAML ignore rules, VEX include/exclude, vendor precedence). |
| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-POLICY-09-106 | `/reports` verdict assembly (Feedser+Vexer+Policy) + signed response envelope. |
| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-POLICY-09-107 | Expose score inputs, config version, and quiet provenance in `/reports` JSON and signed payload. |
| Sprint 9 | DevOps Foundations | ops/devops/TASKS.md | DONE (2025-10-21) | DevOps Guild, Scanner WebService Guild | DEVOPS-SCANNER-09-204 | Surface `SCANNER__EVENTS__*` env config across Compose/Helm and document overrides. |
| Sprint 9 | DevOps Foundations | ops/devops/TASKS.md | DONE (2025-10-21) | DevOps Guild, Notify Guild | DEVOPS-SCANNER-09-205 | Notify smoke job validates Redis stream + Notify deliveries after staging deploys. |
| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | DONE (2025-10-19) | Policy Guild | POLICY-CORE-09-004 | Versioned scoring config with schema validation, trust table, and golden fixtures. |
| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | DONE (2025-10-19) | Policy Guild | POLICY-CORE-09-005 | Scoring/quiet engine compute score, enforce VEX-only quiet rules, emit inputs and provenance. |
| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | DONE (2025-10-19) | Policy Guild | POLICY-CORE-09-006 | Unknown state & confidence decay deterministic bands surfaced in policy outputs. |
| Sprint 9 | Docs & Governance | docs/TASKS.md | DONE (2025-10-21) | Platform Events Guild | PLATFORM-EVENTS-09-401 | Embed canonical event samples into contract/integration tests and ensure CI validates payloads against published schemas. |
| Sprint 10 | Benchmarks | bench/TASKS.md | DONE (2025-10-21) | Bench Guild, Language Analyzer Guild | BENCH-SCANNER-10-002 | Wire real language analyzers into bench harness & refresh baselines post-implementation. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Signer/TASKS.md | DONE (2025-10-21) | Signer Guild | SIGNER-API-11-101 | `/sign/dsse` pipeline with Authority auth, PoE introspection, release verification, DSSE signing. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Signer/TASKS.md | DONE (2025-10-21) | Signer Guild | SIGNER-REF-11-102 | `/verify/referrers` endpoint with OCI lookup, caching, and policy enforcement. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Signer/TASKS.md | DONE (2025-10-21) | Signer Guild | SIGNER-QUOTA-11-103 | Enforce plan quotas, concurrency/QPS limits, artifact size caps with metrics/audit logs. |
| Sprint 12 | Runtime Guardrails | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-20) | Scanner WebService Guild | SCANNER-RUNTIME-12-301 | `/runtime/events` ingestion endpoint with validation, batching, storage hooks. |
| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | DONE (2025-10-21) | DevEx/CLI | CLI-OFFLINE-13-006 | Implement offline kit pull/import/status commands with integrity checks. |
| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Queue/TASKS.md | DONE (2025-10-20) | Scheduler Queue Guild | SCHED-QUEUE-16-403 | Dead-letter handling + metrics. |

View File

@@ -44,7 +44,26 @@ Excititor:
vulnId: CVE-2025-0001
```
### Field reference
### Root settings
| Field | Required | Description |
| --- | --- | --- |
| `outputRoot` | | Filesystem root where mirror artefacts are written. Defaults to the Excititor file-system artifact store root when omitted. |
| `directoryName` | | Optional subdirectory created under `outputRoot`; defaults to `mirror`. |
| `targetRepository` | | Hint propagated to manifests/index files indicating the operator-visible location (for example `s3://mirror/excititor`). |
| `signing` | | Bundle signing configuration. When enabled, the exporter emits a detached JWS (`bundle.json.jws`) alongside each domain bundle. |
`signing` supports the following fields:
| Field | Required | Description |
| --- | --- | --- |
| `enabled` | | Toggles detached signing for domain bundles. |
| `algorithm` | | Signing algorithm identifier (default `ES256`). |
| `keyId` | ✅ (when `enabled`) | Signing key identifier resolved via the configured crypto provider registry. |
| `provider` | | Optional provider hint when multiple registries are available. |
| `keyPath` | | Optional PEM path used to seed the provider when the key is not already loaded. |
### Domain field reference
| Field | Required | Description |
| --- | --- | --- |
@@ -53,13 +72,13 @@ Excititor:
| `requireAuthentication` | | When `true` the service enforces that the caller is authenticated (Authority token). |
| `maxIndexRequestsPerHour` | | Per-domain quota for index endpoints. `0`/negative disables the guard. |
| `maxDownloadRequestsPerHour` | | Per-domain quota for artifact downloads. |
| `exports` | ✅ | Collection of export projections. |
| `exports` | ✅ | Collection of export projections. |
Export-level fields:
| Field | Required | Description |
| --- | --- | --- |
| `key` | ✅ | Unique key within the domain. Used in URLs (`/exports/{key}`) and filenames. |
| `key` | ✅ | Unique key within the domain. Used in URLs (`/exports/{key}`) and filenames/bundle entries. |
| `format` | ✅ | One of `json`, `jsonl`, `openvex`, `csaf`. Maps to `VexExportFormat`. |
| `filters` | | Key/value pairs executed via `VexQueryFilter`. Keys must match export data source columns (e.g., `vulnId`, `productKey`). |
| `sort` | | Key/boolean map (false = descending). |
@@ -117,7 +136,14 @@ Recommended workflow:
* `GET /download` when new
* Verify digest + attestation
When the export team lands deterministic mirror bundles (Sprint 7 tasks 01-005/006/007), these configurations can be generated automatically.
When the export engine runs, it materializes the following artefacts under `outputRoot/<directoryName>`:
- `index.json` canonical index listing each configured domain, manifest/bundle descriptors (with SHA-256 digests), and available export keys.
- `<domain>/manifest.json` per-domain summary with export metadata (query signature, consensus/score digests, source providers) and a descriptor pointing at the bundle.
- `<domain>/bundle.json` canonical payload containing serialized consensus, score envelopes, and normalized VEX claims for the matching export definitions.
- `<domain>/bundle.json.jws` optional detached JWS when signing is enabled.
Downstream automation reads `manifest.json`/`bundle.json` directly, while `/excititor/mirror` endpoints stream the same artefacts through authenticated HTTP.
---

View File

@@ -13,7 +13,7 @@
| DOCS-EVENTS-09-002 | DONE (2025-10-19) | Docs Guild, Platform Events | SCANNER-EVENTS-15-201 | Publish event schema catalog (`docs/events/`) for `scanner.report.ready@1`, `scheduler.rescan.delta@1`, `attestor.logged@1`. | Schemas validated (Ajv CI hooked); docs/events/README summarises usage; Platform Events notified via `docs/updates/2025-10-18-docs-guild.md`. |
| DOCS-EVENTS-09-003 | DONE (2025-10-19) | Docs Guild | DOCS-EVENTS-09-002 | Add human-readable envelope field references and canonical payload samples for published events, including offline validation workflow. | Tables explain common headers/payload segments; versioned sample payloads committed; README links to validation instructions and samples. |
| DOCS-EVENTS-09-004 | DONE (2025-10-19) | Docs Guild, Scanner WebService | SCANNER-EVENTS-15-201 | Refresh scanner event docs to mirror DSSE-backed report fields, document `scanner.scan.completed`, and capture canonical sample validation. | Schemas updated for new payload shape; README references DSSE reuse and validation test; samples align with emitted events. |
| PLATFORM-EVENTS-09-401 | DONE (2025-10-19) | Platform Events Guild | DOCS-EVENTS-09-003 | Embed canonical event samples into contract/integration tests and ensure CI validates payloads against published schemas. | Notify/Scheduler contract suites exercise samples; CI job validates samples with `ajv-cli`; Platform Events changelog notes coverage. |
| PLATFORM-EVENTS-09-401 | DONE (2025-10-21) | Platform Events Guild | DOCS-EVENTS-09-003 | Embed canonical event samples into contract/integration tests and ensure CI validates payloads against published schemas. | Notify models tests now run schema validation against `docs/events/*.json`, event schemas allow optional `attributes`, and docs capture the new validation workflow. |
| RUNTIME-GUILD-09-402 | DONE (2025-10-19) | Runtime Guild | SCANNER-POLICY-09-107 | Confirm Scanner WebService surfaces `quietedFindingCount` and progress hints to runtime consumers; document readiness checklist. | Runtime verification run captures enriched payload; checklist/doc updates merged; stakeholders acknowledge availability. |
| DOCS-CONCELIER-07-201 | TODO | Docs Guild, Concelier WebService | FEEDWEB-DOCS-01-001 | Final editorial review and publish pass for Concelier authority toggle documentation (Quickstart + operator guide). | Review feedback resolved, publish PR merged, release notes updated with documentation pointer. |
| DOCS-RUNTIME-17-004 | TODO | Docs Guild, Runtime Guild | SCANNER-EMIT-17-701, ZASTAVA-OBS-17-005, DEVOPS-REL-17-002 | Document build-id workflows: SBOM exposure, runtime event payloads, debug-store layout, and operator guidance for symbol retrieval. | Architecture + operator docs updated with build-id sections, examples show `readelf` output + debuginfod usage, references linked from Offline Kit/Release guides. |

View File

@@ -20,12 +20,13 @@ All event envelopes share the same deterministic header. Use the following table
| `tenant` | `string` | Multitenant isolation key; mirror the value recorded in queue/Mongo metadata. |
| `ts` | `date-time` | RFC3339 UTC timestamp. Use monotonic clocks or atomic offsets so ordering survives retries. |
| `scope` | `object` | Optional block used when the event concerns a specific image or repository. See schema for required fields (e.g., `repo`, `digest`). |
| `payload` | `object` | Event-specific body. Schemas allow additional properties so producers can add optional hints (e.g., `reportId`, `quietedFindingCount`) without breaking consumers. For scanner events, payloads embed both the canonical report document and the DSSE envelope so consumers can reuse signatures without recomputing them. See `docs/runtime/SCANNER_RUNTIME_READINESS.md` for the runtime consumer checklist covering these hints. |
| `payload` | `object` | Event-specific body. Schemas allow additional properties so producers can add optional hints (e.g., `reportId`, `quietedFindingCount`) without breaking consumers. For scanner events, payloads embed both the canonical report document and the DSSE envelope so consumers can reuse signatures without recomputing them. See `docs/runtime/SCANNER_RUNTIME_READINESS.md` for the runtime consumer checklist covering these hints. |
| `attributes` | `object` | Optional metadata bag (`string` keys/values) for downstream correlation (e.g., pipeline identifiers). Omit when unused to keep payloads concise. |
When adding new optional fields, document the behaviour in the schemas `description` block and update the consumer checklist in the next sprint sync.
## Canonical samples & validation
Reference payloads live under `docs/events/samples/`, mirroring the schema version (`<event-name>@<version>.sample.json`). They illustrate common field combinations, including the optional attributes that downstream teams rely on for UI affordances and audit trails. Scanner samples reuse the exact DSSE envelope checked into `samples/api/reports/report-sample.dsse.json`, and a unit test (`ReportSamplesTests`) guards that the payload/base64 remain canonical.
Reference payloads live under `docs/events/samples/`, mirroring the schema version (`<event-name>@<version>.sample.json`). They illustrate common field combinations, including the optional attributes that downstream teams rely on for UI affordances and audit trails. Scanner samples reuse the exact DSSE envelope checked into `samples/api/reports/report-sample.dsse.json`, and unit tests (`ReportSamplesTests`, `PlatformEventSchemaValidationTests`) guard that payloads stay canonical and continue to satisfy the published schemas.
Run the following loop offline to validate both schemas and samples:

View File

@@ -8,8 +8,8 @@
"kind": {"const": "attestor.logged"},
"tenant": {"type": "string"},
"ts": {"type": "string", "format": "date-time"},
"payload": {
"type": "object",
"payload": {
"type": "object",
"required": ["artifactSha256", "rekor", "subject"],
"properties": {
"artifactSha256": {"type": "string"},
@@ -30,9 +30,14 @@
"name": {"type": "string"}
}
}
},
"additionalProperties": true
}
},
"additionalProperties": false
}
},
"additionalProperties": true
},
"attributes": {
"type": "object",
"description": "Optional event attributes for downstream correlation.",
"additionalProperties": true
}
},
"additionalProperties": false
}

View File

@@ -17,10 +17,10 @@
"digest": {"type": "string"}
}
},
"payload": {
"type": "object",
"required": ["verdict", "delta", "links"],
"properties": {
"payload": {
"type": "object",
"required": ["verdict", "delta", "links"],
"properties": {
"reportId": {"type": "string"},
"generatedAt": {"type": "string", "format": "date-time"},
"verdict": {"enum": ["pass", "warn", "fail"]},
@@ -76,9 +76,14 @@
},
"additionalProperties": false
}
},
"additionalProperties": true
}
},
"additionalProperties": false
}
},
"additionalProperties": true
},
"attributes": {
"type": "object",
"description": "Optional event attributes for downstream correlation.",
"additionalProperties": true
}
},
"additionalProperties": false
}

View File

@@ -17,8 +17,8 @@
"digest": {"type": "string"}
}
},
"payload": {
"type": "object",
"payload": {
"type": "object",
"required": ["reportId", "digest", "verdict", "summary"],
"properties": {
"reportId": {"type": "string"},
@@ -90,8 +90,13 @@
"additionalProperties": false
}
},
"additionalProperties": true
}
},
"additionalProperties": false
}
"additionalProperties": true
},
"attributes": {
"type": "object",
"description": "Optional event attributes for downstream correlation.",
"additionalProperties": true
}
},
"additionalProperties": false
}

View File

@@ -8,8 +8,8 @@
"kind": {"const": "scheduler.rescan.delta"},
"tenant": {"type": "string"},
"ts": {"type": "string", "format": "date-time"},
"payload": {
"type": "object",
"payload": {
"type": "object",
"required": ["scheduleId", "impactedDigests", "summary"],
"properties": {
"scheduleId": {"type": "string"},
@@ -26,8 +26,13 @@
}
}
},
"additionalProperties": true
}
},
"additionalProperties": false
}
"additionalProperties": true
},
"attributes": {
"type": "object",
"description": "Optional event attributes for downstream correlation.",
"additionalProperties": true
}
},
"additionalProperties": false
}

View File

@@ -14,5 +14,6 @@
| DEVOPS-LAUNCH-18-100 | TODO | DevOps Guild | - | Finalise production environment footprint (clusters, secrets, network overlays) for full-platform go-live. | IaC/compose overlays committed, secrets placeholders documented, dry-run deploy succeeds in staging. |
| DEVOPS-LAUNCH-18-900 | TODO | DevOps Guild, Module Leads | Wave 0 completion | Collect full implementation sign-off from module owners and consolidate launch readiness checklist. | Sign-off record stored under `docs/ops/launch-readiness.md`; outstanding gaps triaged; checklist approved. |
| DEVOPS-LAUNCH-18-001 | TODO | DevOps Guild | DEVOPS-LAUNCH-18-100, DEVOPS-LAUNCH-18-900 | Production launch cutover rehearsal and runbook publication. | `docs/ops/launch-cutover.md` drafted, rehearsal executed with rollback drill, approvals captured. |
| DEVOPS-NUGET-13-001 | TODO | DevOps Guild, Platform Leads | DEVOPS-REL-14-001 | Add .NET 10 preview feeds / local mirrors so `Microsoft.Extensions.*` 10.0 preview packages restore offline; refresh restore docs. | NuGet.config maps preview feeds (or local mirrored packages), `dotnet restore` succeeds for Excititor/Concelier solutions without ad-hoc feed edits, docs updated for offline bootstrap. |
> Remark (2025-10-20): Repacked `Mongo2Go` local feed to require MongoDB.Driver 3.5.0 + SharpCompress 0.41.0; cache regression tests green and NU1902/NU1903 suppressed.
> Remark (2025-10-21): Compose/Helm profiles now surface `SCANNER__EVENTS__*` toggles with docs pointing at new `.env` placeholders.

View File

@@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Concelier.Core.Unknown;
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Core.Tests.Unknown;
public sealed class UnknownStateLedgerTests
{
[Fact]
public async Task RecordAsync_DetectsMarkersAndPersistsLedger()
{
var repository = new FakeUnknownStateRepository();
var observedAt = DateTimeOffset.Parse("2025-10-20T08:00:00Z");
var recordedAt = DateTimeOffset.Parse("2025-10-21T00:00:00Z");
var ledger = new UnknownStateLedger(repository, new FixedTimeProvider(recordedAt));
var advisory = BuildAdvisory(
provenance: Array.Empty<AdvisoryProvenance>(),
packages: new[]
{
BuildPackage(
statuses: new[] { BuildStatus(AffectedPackageStatusCatalog.KnownAffected, source: "Vendor") },
versionRanges: Array.Empty<AffectedVersionRange>()),
BuildPackage(
statuses: new[] { BuildStatus(AffectedPackageStatusCatalog.Fixed) },
versionRanges: new[]
{
new AffectedVersionRange(
rangeKind: "semver",
introducedVersion: null,
fixedVersion: null,
lastAffectedVersion: null,
rangeExpression: null,
provenance: new AdvisoryProvenance("unknown", "range", string.Empty, recordedAt, fieldMask: null)),
}),
});
var request = new UnknownStateLedgerRequest("CVE-2025-1111", advisory, observedAt);
var result = await ledger.RecordAsync(request, CancellationToken.None);
Assert.True(advisory.Provenance.IsDefaultOrEmpty);
Assert.Equal("cve-2025-1111", result.VulnerabilityKey);
Assert.Equal(observedAt.ToUniversalTime(), result.AsOf);
var markerNames = result.Markers.Select(marker => marker.Marker).OrderBy(name => name, StringComparer.Ordinal).ToArray();
Assert.True(markerNames.Length == 3, "Markers: " + string.Join(",", markerNames));
Assert.Single(repository.Upserts);
Assert.Equal("cve-2025-1111", repository.Upserts.Single().VulnerabilityKey);
Assert.Equal(3, repository.Upserts.Single().Snapshots.Count);
var markers = result.Markers.ToDictionary(marker => marker.Marker, marker => marker, StringComparer.Ordinal);
Assert.True(markers.ContainsKey(UnknownStateMarkerKinds.UnknownVulnerabilityRange));
Assert.True(markers.ContainsKey(UnknownStateMarkerKinds.UnknownOrigin));
Assert.True(markers.ContainsKey(UnknownStateMarkerKinds.AmbiguousFix));
var fixMarker = markers[UnknownStateMarkerKinds.AmbiguousFix];
Assert.Equal(0.45, fixMarker.Confidence, 3);
Assert.Equal("medium", fixMarker.ConfidenceBand);
Assert.Contains("explicit fixed version", fixMarker.Evidence, StringComparison.OrdinalIgnoreCase);
var repositoryMarkers = await repository.GetByVulnerabilityAsync("cve-2025-1111", CancellationToken.None);
Assert.Equal(3, repositoryMarkers.Count);
}
[Fact]
public async Task RecordAsync_NoUnknownSignals_ClearsLedger()
{
var repository = new FakeUnknownStateRepository();
var observedAt = DateTimeOffset.Parse("2025-10-19T09:00:00Z");
var recordedAt = DateTimeOffset.Parse("2025-10-21T03:00:00Z");
var ledger = new UnknownStateLedger(repository, new FixedTimeProvider(recordedAt));
var advisory = BuildAdvisory(
provenance: new[] { new AdvisoryProvenance("NVD", "merge", "nvd-source", recordedAt, fieldMask: null) },
packages: new[]
{
BuildPackage(
statuses: new[] { BuildStatus(AffectedPackageStatusCatalog.KnownAffected, source: "Vendor") },
versionRanges: new[]
{
new AffectedVersionRange("semver", "1.0.0", "1.0.5", null, ">=1.0.0,<1.0.5", new AdvisoryProvenance("NVD", "range", string.Empty, recordedAt, fieldMask: null)),
},
provenance: new[]
{
new AdvisoryProvenance("Vendor", "advisory", "vendor", recordedAt, fieldMask: null),
}),
});
var result = await ledger.RecordAsync(new UnknownStateLedgerRequest("GHSA-1234", advisory, observedAt), CancellationToken.None);
Assert.Equal("ghsa-1234", result.VulnerabilityKey);
Assert.Empty(result.Markers);
Assert.Single(repository.Upserts);
var stored = repository.Upserts.Single();
Assert.Empty(stored.Snapshots);
}
[Fact]
public async Task GetByVulnerabilityAsync_NormalizesKey()
{
var repository = new FakeUnknownStateRepository();
var snapshot = new UnknownStateSnapshot(
UnknownStateMarkerKinds.UnknownOrigin,
0.6,
"medium",
DateTimeOffset.Parse("2025-10-19T00:00:00Z"),
DateTimeOffset.Parse("2025-10-19T01:00:00Z"),
"evidence");
repository.Stored["cve-2025-0001"] = new List<UnknownStateSnapshot> { snapshot };
var ledger = new UnknownStateLedger(repository);
var markers = await ledger.GetByVulnerabilityAsync("CVE-2025-0001", CancellationToken.None);
Assert.Single(markers);
Assert.Equal(snapshot, markers[0]);
}
[Fact]
public void CanonicalSerialization_IsDeterministic()
{
var snapshot = new UnknownStateSnapshot(
UnknownStateMarkerKinds.UnknownOrigin,
0.6,
"medium",
DateTimeOffset.Parse("2025-10-19T00:00:00Z"),
DateTimeOffset.Parse("2025-10-21T12:00:00Z"),
"Provenance missing");
var json = CanonicalJsonSerializer.Serialize(snapshot);
const string expected = "{\"confidence\":0.6,\"confidenceBand\":\"medium\",\"evidence\":\"Provenance missing\",\"marker\":\"unknown_origin\",\"observedAt\":\"2025-10-19T00:00:00+00:00\",\"recordedAt\":\"2025-10-21T12:00:00+00:00\"}";
Assert.Equal(expected, json);
}
private static Advisory BuildAdvisory(
IEnumerable<AdvisoryProvenance> provenance,
IEnumerable<AffectedPackage> packages)
=> new(
advisoryKey: "ADV-1",
title: "Sample advisory",
summary: null,
language: "en",
published: null,
modified: null,
severity: "High",
exploitKnown: false,
aliases: new[] { "CVE-2025-1111" },
references: Array.Empty<AdvisoryReference>(),
affectedPackages: packages,
cvssMetrics: Array.Empty<CvssMetric>(),
provenance: provenance,
description: null,
cwes: Array.Empty<AdvisoryWeakness>(),
canonicalMetricId: null);
private static AffectedPackage BuildPackage(
IEnumerable<AffectedPackageStatus> statuses,
IEnumerable<AffectedVersionRange> versionRanges,
IEnumerable<AdvisoryProvenance>? provenance = null)
=> new(
type: AffectedPackageTypes.SemVer,
identifier: "pkg/example",
platform: null,
versionRanges: versionRanges,
statuses: statuses,
provenance: provenance,
normalizedVersions: Array.Empty<NormalizedVersionRule>());
private static AffectedPackageStatus BuildStatus(string status, string source = "unknown")
=> new(status, new AdvisoryProvenance(source, "status", string.Empty, DateTimeOffset.Parse("2025-10-15T00:00:00Z"), fieldMask: null));
private sealed class FakeUnknownStateRepository : IUnknownStateRepository
{
public List<(string VulnerabilityKey, IReadOnlyCollection<UnknownStateSnapshot> Snapshots)> Upserts { get; } = new();
public Dictionary<string, List<UnknownStateSnapshot>> Stored { get; } = new(StringComparer.Ordinal);
public ValueTask UpsertAsync(string vulnerabilityKey, IReadOnlyCollection<UnknownStateSnapshot> snapshots, CancellationToken cancellationToken)
{
Upserts.Add((vulnerabilityKey, snapshots));
Stored[vulnerabilityKey] = snapshots?.ToList() ?? new List<UnknownStateSnapshot>();
return ValueTask.CompletedTask;
}
public ValueTask<IReadOnlyList<UnknownStateSnapshot>> GetByVulnerabilityAsync(string vulnerabilityKey, CancellationToken cancellationToken)
{
Stored.TryGetValue(vulnerabilityKey, out var snapshots);
return ValueTask.FromResult<IReadOnlyList<UnknownStateSnapshot>>(snapshots ?? new List<UnknownStateSnapshot>());
}
}
private sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _now;
public FixedTimeProvider(DateTimeOffset now)
{
_now = now.ToUniversalTime();
}
public override DateTimeOffset GetUtcNow() => _now;
}
}

View File

@@ -18,4 +18,4 @@
|Reference normalization & freshness instrumentation cleanup|BE-Core, QA|Models|DONE (2025-10-15) reference keys normalized, freshness overrides applied to union fields, and new tests assert decision logging.|
|FEEDCORE-ENGINE-07-001 Advisory event log & asOf queries|Team Core Engine & Storage Analytics|FEEDSTORAGE-DATA-07-001|**DONE (2025-10-19)** Implemented `AdvisoryEventLog` service plus repository contracts, canonical hashing, and lower-cased key normalization with replay support; documented determinism guarantees. Tests: `dotnet test src/StellaOps.Concelier.Core.Tests/StellaOps.Concelier.Core.Tests.csproj`.|
|FEEDCORE-ENGINE-07-002 Noise prior computation service|Team Core Engine & Data Science|FEEDCORE-ENGINE-07-001|**DONE (2025-10-21)** Build rule-based learner capturing false-positive priors per package/env, persist summaries, and expose APIs for Excititor/scan suppressors with reproducible statistics.|
|FEEDCORE-ENGINE-07-003 Unknown state ledger & confidence seeding|Team Core Engine & Storage Analytics|FEEDCORE-ENGINE-07-001|TODO Persist `unknown_vuln_range/unknown_origin/ambiguous_fix` markers with initial confidence bands, expose query surface for Policy, and add fixtures validating canonical serialization.|
|FEEDCORE-ENGINE-07-003 Unknown state ledger & confidence seeding|Team Core Engine & Storage Analytics|FEEDCORE-ENGINE-07-001|DONE (2025-10-21) Persisted `unknown_vuln_range/unknown_origin/ambiguous_fix` markers with seeded confidence bands, exposed query surface for Policy, and added canonical serialization fixtures + regression tests.|

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Concelier.Core.Unknown;
/// <summary>
/// Surface for recording and querying unknown-state markers.
/// </summary>
public interface IUnknownStateLedger
{
ValueTask<UnknownStateLedgerResult> RecordAsync(
UnknownStateLedgerRequest request,
CancellationToken cancellationToken);
ValueTask<IReadOnlyList<UnknownStateSnapshot>> GetByVulnerabilityAsync(
string vulnerabilityKey,
CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Concelier.Core.Unknown;
/// <summary>
/// Persistence abstraction for unknown-state ledger entries.
/// </summary>
public interface IUnknownStateRepository
{
ValueTask UpsertAsync(
string vulnerabilityKey,
IReadOnlyCollection<UnknownStateSnapshot> snapshots,
CancellationToken cancellationToken);
ValueTask<IReadOnlyList<UnknownStateSnapshot>> GetByVulnerabilityAsync(
string vulnerabilityKey,
CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,313 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Core.Unknown;
/// <summary>
/// Default implementation that derives unknown-state markers from canonical advisories.
/// </summary>
public sealed class UnknownStateLedger : IUnknownStateLedger
{
private static readonly ImmutableHashSet<string> ImpactStatuses = ImmutableHashSet.Create(
StringComparer.Ordinal,
AffectedPackageStatusCatalog.KnownAffected,
AffectedPackageStatusCatalog.Affected,
AffectedPackageStatusCatalog.UnderInvestigation,
AffectedPackageStatusCatalog.Pending,
AffectedPackageStatusCatalog.Unknown);
private static readonly ImmutableHashSet<string> FixStatuses = ImmutableHashSet.Create(
StringComparer.Ordinal,
AffectedPackageStatusCatalog.Fixed,
AffectedPackageStatusCatalog.FirstFixed,
AffectedPackageStatusCatalog.Mitigated);
private static readonly ImmutableDictionary<string, UnknownMarkerSeed> MarkerSeeds = new Dictionary<string, UnknownMarkerSeed>(StringComparer.Ordinal)
{
[UnknownStateMarkerKinds.UnknownVulnerabilityRange] = new UnknownMarkerSeed(0.8, "high"),
[UnknownStateMarkerKinds.UnknownOrigin] = new UnknownMarkerSeed(0.6, "medium"),
[UnknownStateMarkerKinds.AmbiguousFix] = new UnknownMarkerSeed(0.45, "medium"),
}.ToImmutableDictionary(StringComparer.Ordinal);
private readonly IUnknownStateRepository _repository;
private readonly TimeProvider _timeProvider;
public UnknownStateLedger(IUnknownStateRepository repository, TimeProvider? timeProvider = null)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public async ValueTask<UnknownStateLedgerResult> RecordAsync(
UnknownStateLedgerRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var recordedAt = _timeProvider.GetUtcNow();
var markers = EvaluateMarkers(request.Advisory, request.AsOf, recordedAt);
await _repository.UpsertAsync(request.VulnerabilityKey, markers, cancellationToken).ConfigureAwait(false);
return new UnknownStateLedgerResult(request.VulnerabilityKey, request.AsOf, markers);
}
public ValueTask<IReadOnlyList<UnknownStateSnapshot>> GetByVulnerabilityAsync(
string vulnerabilityKey,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(vulnerabilityKey);
var normalizedKey = vulnerabilityKey.Trim().ToLowerInvariant();
return _repository.GetByVulnerabilityAsync(normalizedKey, cancellationToken);
}
private static ImmutableArray<UnknownStateSnapshot> EvaluateMarkers(
Advisory advisory,
DateTimeOffset observedAt,
DateTimeOffset recordedAt)
{
var builder = ImmutableArray.CreateBuilder<UnknownStateSnapshot>(initialCapacity: 3);
if (advisory is not null)
{
if (TryCreateUnknownVulnerabilityRangeMarker(advisory, observedAt, recordedAt, out var unknownRange))
{
builder.Add(unknownRange);
}
if (TryCreateUnknownOriginMarker(advisory, observedAt, recordedAt, out var unknownOrigin))
{
builder.Add(unknownOrigin);
}
if (TryCreateAmbiguousFixMarker(advisory, observedAt, recordedAt, out var ambiguousFix))
{
builder.Add(ambiguousFix);
}
}
if (builder.Count == 0)
{
return ImmutableArray<UnknownStateSnapshot>.Empty;
}
builder.Sort(static (left, right) => StringComparer.Ordinal.Compare(left.Marker, right.Marker));
return builder.ToImmutable();
}
private static bool TryCreateUnknownVulnerabilityRangeMarker(
Advisory advisory,
DateTimeOffset observedAt,
DateTimeOffset recordedAt,
out UnknownStateSnapshot snapshot)
{
snapshot = null!;
if (advisory.AffectedPackages.IsDefaultOrEmpty)
{
return false;
}
var lackingPackages = 0;
foreach (var package in advisory.AffectedPackages)
{
if (package is null)
{
continue;
}
if (!HasImpactStatus(package))
{
continue;
}
if (!HasConcreteRange(package))
{
lackingPackages++;
}
}
if (lackingPackages == 0)
{
return false;
}
var seed = MarkerSeeds[UnknownStateMarkerKinds.UnknownVulnerabilityRange];
var evidence = lackingPackages == 1
? "1 affected package lacks explicit version ranges."
: $"{lackingPackages} affected packages lack explicit version ranges.";
snapshot = new UnknownStateSnapshot(
UnknownStateMarkerKinds.UnknownVulnerabilityRange,
seed.Confidence,
seed.Band,
observedAt,
recordedAt,
evidence);
return true;
}
private static bool TryCreateUnknownOriginMarker(
Advisory advisory,
DateTimeOffset observedAt,
DateTimeOffset recordedAt,
out UnknownStateSnapshot snapshot)
{
snapshot = null!;
if (ContainsKnownProvenance(advisory.Provenance))
{
return false;
}
var seed = MarkerSeeds[UnknownStateMarkerKinds.UnknownOrigin];
var evidence = advisory.Provenance.IsDefaultOrEmpty
? "Advisory provenance is missing; falling back to inferred sources."
: "All advisory provenance sources resolve to 'unknown'.";
snapshot = new UnknownStateSnapshot(
UnknownStateMarkerKinds.UnknownOrigin,
seed.Confidence,
seed.Band,
observedAt,
recordedAt,
evidence);
return true;
}
private static bool TryCreateAmbiguousFixMarker(
Advisory advisory,
DateTimeOffset observedAt,
DateTimeOffset recordedAt,
out UnknownStateSnapshot snapshot)
{
snapshot = null!;
if (advisory.AffectedPackages.IsDefaultOrEmpty)
{
return false;
}
var ambiguousPackages = 0;
foreach (var package in advisory.AffectedPackages)
{
if (package is null)
{
continue;
}
if (!package.Statuses.IsDefaultOrEmpty && package.Statuses.Any(status => FixStatuses.Contains(status.Status)))
{
var hasFixedRange = package.VersionRanges.Any(static range => !string.IsNullOrWhiteSpace(range.FixedVersion));
if (!hasFixedRange)
{
ambiguousPackages++;
}
}
}
if (ambiguousPackages == 0)
{
return false;
}
var seed = MarkerSeeds[UnknownStateMarkerKinds.AmbiguousFix];
var evidence = ambiguousPackages == 1
? "Fix status published without explicit fixed version details."
: $"Fix status published without explicit fixed versions for {ambiguousPackages} packages.";
snapshot = new UnknownStateSnapshot(
UnknownStateMarkerKinds.AmbiguousFix,
seed.Confidence,
seed.Band,
observedAt,
recordedAt,
evidence);
return true;
}
private static bool HasImpactStatus(AffectedPackage package)
{
if (package.Statuses.IsDefaultOrEmpty)
{
return false;
}
foreach (var status in package.Statuses)
{
if (status is null)
{
continue;
}
if (ImpactStatuses.Contains(status.Status))
{
return true;
}
}
return false;
}
private static bool HasConcreteRange(AffectedPackage package)
{
if (package.VersionRanges.IsDefaultOrEmpty)
{
return false;
}
foreach (var range in package.VersionRanges)
{
if (range is null)
{
continue;
}
if (!string.IsNullOrWhiteSpace(range.IntroducedVersion) ||
!string.IsNullOrWhiteSpace(range.FixedVersion) ||
!string.IsNullOrWhiteSpace(range.LastAffectedVersion) ||
!string.IsNullOrWhiteSpace(range.RangeExpression))
{
return true;
}
}
return false;
}
private static bool ContainsKnownProvenance(ImmutableArray<AdvisoryProvenance> provenance)
{
if (provenance.IsDefaultOrEmpty)
{
return false;
}
foreach (var entry in provenance)
{
if (entry is null)
{
continue;
}
if (IsKnownSource(entry.Source))
{
return true;
}
}
return false;
}
private static bool IsKnownSource(string? source)
=> !string.IsNullOrWhiteSpace(source) &&
!string.Equals(source, "unknown", StringComparison.OrdinalIgnoreCase);
private readonly record struct UnknownMarkerSeed(double Confidence, string Band);
}

View File

@@ -0,0 +1,29 @@
using System;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Core.Unknown;
/// <summary>
/// Input payload describing the advisory snapshot used to derive unknown-state markers.
/// </summary>
public sealed record UnknownStateLedgerRequest
{
public UnknownStateLedgerRequest(string vulnerabilityKey, Advisory advisory, DateTimeOffset asOf)
{
VulnerabilityKey = NormalizeKey(vulnerabilityKey);
Advisory = advisory ?? throw new ArgumentNullException(nameof(advisory));
AsOf = asOf.ToUniversalTime();
}
public string VulnerabilityKey { get; init; }
public Advisory Advisory { get; init; }
public DateTimeOffset AsOf { get; init; }
private static string NormalizeKey(string key)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key);
return key.Trim().ToLowerInvariant();
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Immutable;
namespace StellaOps.Concelier.Core.Unknown;
/// <summary>
/// Result emitted after unknown-state markers are derived and persisted.
/// </summary>
public sealed record UnknownStateLedgerResult
{
public UnknownStateLedgerResult(string vulnerabilityKey, DateTimeOffset asOf, ImmutableArray<UnknownStateSnapshot> markers)
{
if (string.IsNullOrWhiteSpace(vulnerabilityKey))
{
throw new ArgumentException("Vulnerability key must be provided.", nameof(vulnerabilityKey));
}
VulnerabilityKey = vulnerabilityKey.Trim().ToLowerInvariant();
AsOf = asOf.ToUniversalTime();
Markers = markers.IsDefault ? ImmutableArray<UnknownStateSnapshot>.Empty : markers;
}
public string VulnerabilityKey { get; init; }
public DateTimeOffset AsOf { get; init; }
public ImmutableArray<UnknownStateSnapshot> Markers { get; init; }
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace StellaOps.Concelier.Core.Unknown;
/// <summary>
/// Known unknown-state markers emitted from advisory analysis.
/// </summary>
public static class UnknownStateMarkerKinds
{
public const string UnknownVulnerabilityRange = "unknown_vuln_range";
public const string UnknownOrigin = "unknown_origin";
public const string AmbiguousFix = "ambiguous_fix";
public static IReadOnlyList<string> All { get; } = new[]
{
UnknownVulnerabilityRange,
UnknownOrigin,
AmbiguousFix,
};
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Globalization;
namespace StellaOps.Concelier.Core.Unknown;
/// <summary>
/// Describes a persisted unknown-state marker for a vulnerability.
/// </summary>
public sealed record UnknownStateSnapshot
{
public UnknownStateSnapshot(
string marker,
double confidence,
string confidenceBand,
DateTimeOffset observedAt,
DateTimeOffset recordedAt,
string evidence)
{
Marker = NormalizeMarker(marker);
Confidence = NormalizeConfidence(confidence);
ConfidenceBand = NormalizeBand(confidenceBand);
ObservedAt = observedAt.ToUniversalTime();
RecordedAt = recordedAt.ToUniversalTime();
Evidence = NormalizeEvidence(evidence);
}
public string Marker { get; init; }
public double Confidence { get; init; }
public string ConfidenceBand { get; init; }
public DateTimeOffset ObservedAt { get; init; }
public DateTimeOffset RecordedAt { get; init; }
public string Evidence { get; init; }
public override string ToString()
=> string.Create(CultureInfo.InvariantCulture, $"{Marker}:{Confidence:0.###}@{ObservedAt:O}");
private static string NormalizeMarker(string value)
{
ArgumentException.ThrowIfNullOrWhiteSpace(value);
return value.Trim().ToLowerInvariant();
}
private static double NormalizeConfidence(double value)
{
if (double.IsNaN(value) || double.IsInfinity(value))
{
return 0d;
}
return Math.Clamp(value, 0d, 1d);
}
private static string NormalizeBand(string value)
{
ArgumentException.ThrowIfNullOrWhiteSpace(value);
return value.Trim().ToLowerInvariant();
}
private static string NormalizeEvidence(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
return value.Trim();
}
}

View File

@@ -2,6 +2,6 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| EXCITITOR-CONN-STELLA-07-001 | TODO | Excititor Connectors Stella | EXCITITOR-EXPORT-01-007 | Implement mirror fetch client consuming `https://<domain>.stella-ops.org/excititor/exports/index.json`, validating signatures/digests, storing raw consensus bundles with provenance. | Fetch job downloads mirror manifest, verifies DSSE/signature, stores raw documents + provenance; unit tests cover happy path and tampered manifest failure. |
| EXCITITOR-CONN-STELLA-07-001 | DONE (2025-10-21) | Excititor Connectors Stella | EXCITITOR-EXPORT-01-007 | **DONE (2025-10-21)** Implemented `StellaOpsMirrorConnector` with `MirrorManifestClient` + `MirrorSignatureVerifier`, digest validation, signature enforcement, raw document + DTO persistence, and resume cursor updates. Added fixture-backed tests covering happy path and tampered manifest rejection. | Fetch job downloads mirror manifest, verifies DSSE/signature, stores raw documents + provenance; unit tests cover happy path and tampered manifest failure. |
| EXCITITOR-CONN-STELLA-07-002 | TODO | Excititor Connectors Stella | EXCITITOR-CONN-STELLA-07-001 | Normalize mirror bundles into VexClaim sets referencing original provider metadata and mirror provenance. | Normalizer emits VexClaims with mirror provenance + policy metadata, fixtures assert deterministic output parity vs local exports. |
| EXCITITOR-CONN-STELLA-07-003 | TODO | Excititor Connectors Stella | EXCITITOR-CONN-STELLA-07-002 | Implement incremental cursor handling per-export digest, support resume, and document configuration for downstream Excititor mirrors. | Connector resumes from last export digest, handles delta/export rotation, docs show configuration; integration test covers resume + new export ingest. |

View File

@@ -0,0 +1,110 @@
using System.Collections.Generic;
namespace StellaOps.Excititor.Core;
public sealed class MirrorDistributionOptions
{
public const string SectionName = "Excititor:Mirror";
/// <summary>
/// Global enable flag for mirror distribution surfaces and bundle generation.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Optional absolute or relative path for mirror artifacts. When unset, publishers
/// may fall back to artifact-store specific defaults.
/// </summary>
public string? OutputRoot { get; set; }
/// <summary>
/// Directory name created under <see cref="OutputRoot"/> that holds mirror artifacts.
/// Defaults to <c>mirror</c> to align with offline kit layouts.
/// </summary>
public string DirectoryName { get; set; } = "mirror";
/// <summary>
/// Optional human-readable hint describing where downstream mirrors should publish
/// bundles (e.g., s3://mirror/excititor). Propagated to manifests and index payloads.
/// </summary>
public string? TargetRepository { get; set; }
/// <summary>
/// Signing configuration applied to generated bundle payloads.
/// </summary>
public MirrorSigningOptions Signing { get; } = new();
/// <summary>
/// Domains exposed for mirror consumption. Each domain groups a set of export plans.
/// </summary>
public List<MirrorDomainOptions> Domains { get; } = new();
}
public sealed class MirrorDomainOptions
{
public string Id { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public bool RequireAuthentication { get; set; } = false;
/// <summary>
/// Maximum index requests allowed per rolling window.
/// </summary>
public int MaxIndexRequestsPerHour { get; set; } = 120;
/// <summary>
/// Maximum export downloads allowed per rolling window.
/// </summary>
public int MaxDownloadRequestsPerHour { get; set; } = 600;
public List<MirrorExportOptions> Exports { get; } = new();
}
public sealed class MirrorExportOptions
{
public string Key { get; set; } = string.Empty;
public string Format { get; set; } = string.Empty;
public Dictionary<string, string> Filters { get; } = new();
public Dictionary<string, bool> Sort { get; } = new();
public int? Limit { get; set; } = null;
public int? Offset { get; set; } = null;
public string? View { get; set; } = null;
}
public sealed class MirrorSigningOptions
{
/// <summary>
/// Enables signing of mirror bundle payloads when true. When false the publisher
/// omits detached JWS artifacts.
/// </summary>
public bool Enabled { get; set; } = false;
/// <summary>
/// Signing algorithm requested (for example, ES256). The publisher validates that
/// the selected provider can satisfy the requested algorithm.
/// </summary>
public string? Algorithm { get; set; }
/// <summary>
/// Optional key identifier resolved against the configured crypto provider registry.
/// </summary>
public string? KeyId { get; set; }
/// <summary>
/// Optional provider hint used to resolve signing providers when multiple are registered.
/// </summary>
public string? Provider { get; set; }
/// <summary>
/// Optional file path to a signing key (PEM). Used when the requested provider does
/// not already have the key loaded into its key store.
/// </summary>
public string? KeyPath { get; set; }
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Linq;
namespace StellaOps.Excititor.Core;
public sealed record MirrorExportPlan(
string Key,
VexExportFormat Format,
VexQuery Query,
VexQuerySignature Signature);
public static class MirrorExportPlanner
{
public static bool TryBuild(MirrorExportOptions exportOptions, out MirrorExportPlan plan, out string? error)
{
if (exportOptions is null)
{
plan = null!;
error = "invalid_export_configuration";
return false;
}
if (string.IsNullOrWhiteSpace(exportOptions.Key))
{
plan = null!;
error = "missing_export_key";
return false;
}
if (string.IsNullOrWhiteSpace(exportOptions.Format) ||
!Enum.TryParse(exportOptions.Format, ignoreCase: true, out VexExportFormat format))
{
plan = null!;
error = "unsupported_export_format";
return false;
}
var filters = exportOptions.Filters.Select(pair => new VexQueryFilter(pair.Key, pair.Value));
var sorts = exportOptions.Sort.Select(pair => new VexQuerySort(pair.Key, pair.Value));
var query = VexQuery.Create(filters, sorts, exportOptions.Limit, exportOptions.Offset, exportOptions.View);
var signature = VexQuerySignature.FromQuery(query);
plan = new MirrorExportPlan(exportOptions.Key.Trim(), format, query, signature);
error = null;
return true;
}
}

View File

@@ -230,13 +230,33 @@ public static class VexCanonicalJsonSerializer
"sourceProviders",
"consensusRevision",
"policyRevisionId",
"policyDigest",
"consensusDigest",
"scoreDigest",
"attestation",
"sizeBytes",
}
},
"policyDigest",
"consensusDigest",
"scoreDigest",
"quietProvenance",
"attestation",
"sizeBytes",
}
},
{
typeof(VexQuietProvenance),
new[]
{
"vulnerabilityId",
"productKey",
"statements",
}
},
{
typeof(VexQuietStatement),
new[]
{
"providerId",
"statementId",
"justification",
"signature",
}
},
{
typeof(VexScoreEnvelope),
new[]

View File

@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Runtime.Serialization;
using System.Text;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
namespace StellaOps.Excititor.Core;
@@ -19,9 +20,10 @@ public sealed record VexExportManifest
string? policyRevisionId = null,
string? policyDigest = null,
VexContentAddress? consensusDigest = null,
VexContentAddress? scoreDigest = null,
VexAttestationMetadata? attestation = null,
long sizeBytes = 0)
VexContentAddress? scoreDigest = null,
IEnumerable<VexQuietProvenance>? quietProvenance = null,
VexAttestationMetadata? attestation = null,
long sizeBytes = 0)
{
if (string.IsNullOrWhiteSpace(exportId))
{
@@ -48,11 +50,12 @@ public sealed record VexExportManifest
SourceProviders = NormalizeProviders(sourceProviders);
ConsensusRevision = string.IsNullOrWhiteSpace(consensusRevision) ? null : consensusRevision.Trim();
PolicyRevisionId = string.IsNullOrWhiteSpace(policyRevisionId) ? null : policyRevisionId.Trim();
PolicyDigest = string.IsNullOrWhiteSpace(policyDigest) ? null : policyDigest.Trim();
ConsensusDigest = consensusDigest;
ScoreDigest = scoreDigest;
Attestation = attestation;
SizeBytes = sizeBytes;
PolicyDigest = string.IsNullOrWhiteSpace(policyDigest) ? null : policyDigest.Trim();
ConsensusDigest = consensusDigest;
ScoreDigest = scoreDigest;
QuietProvenance = NormalizeQuietProvenance(quietProvenance);
Attestation = attestation;
SizeBytes = sizeBytes;
}
public string ExportId { get; }
@@ -79,13 +82,15 @@ public sealed record VexExportManifest
public VexContentAddress? ConsensusDigest { get; }
public VexContentAddress? ScoreDigest { get; }
public VexAttestationMetadata? Attestation { get; }
public VexContentAddress? ScoreDigest { get; }
public ImmutableArray<VexQuietProvenance> QuietProvenance { get; }
public VexAttestationMetadata? Attestation { get; }
public long SizeBytes { get; }
private static ImmutableArray<string> NormalizeProviders(IEnumerable<string> providers)
private static ImmutableArray<string> NormalizeProviders(IEnumerable<string> providers)
{
if (providers is null)
{
@@ -103,11 +108,24 @@ public sealed record VexExportManifest
set.Add(provider.Trim());
}
return set.Count == 0
? ImmutableArray<string>.Empty
: set.ToImmutableArray();
}
}
return set.Count == 0
? ImmutableArray<string>.Empty
: set.ToImmutableArray();
}
private static ImmutableArray<VexQuietProvenance> NormalizeQuietProvenance(IEnumerable<VexQuietProvenance>? quietProvenance)
{
if (quietProvenance is null)
{
return ImmutableArray<VexQuietProvenance>.Empty;
}
return quietProvenance
.OrderBy(static entry => entry.VulnerabilityId, StringComparer.Ordinal)
.ThenBy(static entry => entry.ProductKey, StringComparer.Ordinal)
.ToImmutableArray();
}
}
public sealed record VexContentAddress
{

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace StellaOps.Excititor.Core;
public sealed record VexQuietProvenance
{
public VexQuietProvenance(string vulnerabilityId, string productKey, IEnumerable<VexQuietStatement> statements)
{
if (string.IsNullOrWhiteSpace(vulnerabilityId))
{
throw new ArgumentException("Vulnerability id must be provided.", nameof(vulnerabilityId));
}
if (string.IsNullOrWhiteSpace(productKey))
{
throw new ArgumentException("Product key must be provided.", nameof(productKey));
}
VulnerabilityId = vulnerabilityId.Trim();
ProductKey = productKey.Trim();
Statements = NormalizeStatements(statements);
}
public string VulnerabilityId { get; }
public string ProductKey { get; }
public ImmutableArray<VexQuietStatement> Statements { get; }
private static ImmutableArray<VexQuietStatement> NormalizeStatements(IEnumerable<VexQuietStatement> statements)
{
if (statements is null)
{
throw new ArgumentNullException(nameof(statements));
}
return statements
.OrderBy(static s => s.ProviderId, StringComparer.Ordinal)
.ThenBy(static s => s.StatementId, StringComparer.Ordinal)
.ToImmutableArray();
}
}
public sealed record VexQuietStatement
{
public VexQuietStatement(
string providerId,
string statementId,
VexJustification? justification,
VexSignatureMetadata? signature)
{
if (string.IsNullOrWhiteSpace(providerId))
{
throw new ArgumentException("Provider id must be provided.", nameof(providerId));
}
if (string.IsNullOrWhiteSpace(statementId))
{
throw new ArgumentException("Statement id must be provided.", nameof(statementId));
}
ProviderId = providerId.Trim();
StatementId = statementId.Trim();
Justification = justification;
Signature = signature;
}
public string ProviderId { get; }
public string StatementId { get; }
public VexJustification? Justification { get; }
public VexSignatureMetadata? Signature { get; }
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Immutable;
using System.IO;
using System.Text;
using System.Globalization;
using Microsoft.Extensions.Logging.Abstractions;
using MongoDB.Driver;
using StellaOps.Excititor.Core;
@@ -32,6 +33,18 @@ public sealed class ExportEngineTests
Assert.Equal(VexExportFormat.Json, manifest.Format);
Assert.Equal("baseline/v1", manifest.ConsensusRevision);
Assert.Equal(1, manifest.ClaimCount);
Assert.NotNull(dataSource.LastDataSet);
var expectedEnvelopes = VexExportEnvelopeBuilder.Build(
dataSource.LastDataSet!,
VexPolicySnapshot.Default,
context.RequestedAt);
Assert.NotNull(manifest.ConsensusDigest);
Assert.Equal(expectedEnvelopes.ConsensusDigest.Algorithm, manifest.ConsensusDigest!.Algorithm);
Assert.Equal(expectedEnvelopes.ConsensusDigest.Digest, manifest.ConsensusDigest.Digest);
Assert.NotNull(manifest.ScoreDigest);
Assert.Equal(expectedEnvelopes.ScoreDigest.Algorithm, manifest.ScoreDigest!.Algorithm);
Assert.Equal(expectedEnvelopes.ScoreDigest.Digest, manifest.ScoreDigest.Digest);
Assert.Empty(manifest.QuietProvenance);
// second call hits cache
var cached = await engine.ExportAsync(context, CancellationToken.None);
@@ -114,13 +127,82 @@ public sealed class ExportEngineTests
var manifest = await engine.ExportAsync(context, CancellationToken.None);
Assert.NotNull(attestation.LastRequest);
Assert.NotNull(dataSource.LastDataSet);
var expectedEnvelopes = VexExportEnvelopeBuilder.Build(
dataSource.LastDataSet!,
VexPolicySnapshot.Default,
requestedAt);
Assert.Equal(manifest.ExportId, attestation.LastRequest!.ExportId);
var metadata = attestation.LastRequest.Metadata;
Assert.True(metadata.ContainsKey("consensusDigest"), "Consensus digest metadata missing");
Assert.Equal(expectedEnvelopes.ConsensusDigest.ToUri(), metadata["consensusDigest"]);
Assert.True(metadata.ContainsKey("scoreDigest"), "Score digest metadata missing");
Assert.Equal(expectedEnvelopes.ScoreDigest.ToUri(), metadata["scoreDigest"]);
Assert.Equal(expectedEnvelopes.Consensus.Length.ToString(CultureInfo.InvariantCulture), metadata["consensusEntryCount"]);
Assert.Equal(expectedEnvelopes.ScoreEnvelope.Entries.Length.ToString(CultureInfo.InvariantCulture), metadata["scoreEntryCount"]);
Assert.Equal(VexPolicySnapshot.Default.RevisionId, metadata["policyRevisionId"]);
Assert.Equal(VexPolicySnapshot.Default.Version, metadata["policyVersion"]);
Assert.Equal(VexPolicySnapshot.Default.ConsensusOptions.Alpha.ToString("G17", CultureInfo.InvariantCulture), metadata["scoreAlpha"]);
Assert.Equal(VexPolicySnapshot.Default.ConsensusOptions.Beta.ToString("G17", CultureInfo.InvariantCulture), metadata["scoreBeta"]);
Assert.Equal(VexPolicySnapshot.Default.ConsensusOptions.WeightCeiling.ToString("G17", CultureInfo.InvariantCulture), metadata["scoreWeightCeiling"]);
Assert.NotNull(manifest.Attestation);
Assert.Equal(attestation.Response.Attestation.EnvelopeDigest, manifest.Attestation!.EnvelopeDigest);
Assert.Equal(attestation.Response.Attestation.PredicateType, manifest.Attestation.PredicateType);
Assert.NotNull(manifest.ConsensusDigest);
Assert.Equal(expectedEnvelopes.ConsensusDigest.Digest, manifest.ConsensusDigest!.Digest);
Assert.NotNull(manifest.ScoreDigest);
Assert.Equal(expectedEnvelopes.ScoreDigest.Digest, manifest.ScoreDigest!.Digest);
Assert.Empty(manifest.QuietProvenance);
Assert.NotNull(store.LastSavedManifest);
Assert.Equal(manifest.Attestation, store.LastSavedManifest!.Attestation);
Assert.Equal(manifest.QuietProvenance, store.LastSavedManifest!.QuietProvenance);
}
[Fact]
public async Task ExportAsync_IncludesQuietProvenanceMetadata()
{
var store = new InMemoryExportStore();
var evaluator = new StaticPolicyEvaluator("baseline/v1");
var dataSource = new QuietExportDataSource();
var exporter = new DummyExporter(VexExportFormat.Json);
var attestation = new RecordingAttestationClient();
var engine = new VexExportEngine(
store,
evaluator,
dataSource,
new[] { exporter },
NullLogger<VexExportEngine>.Instance,
cacheIndex: null,
artifactStores: null,
attestationClient: attestation);
var query = VexQuery.Create(new[] { new VexQueryFilter("vulnId", "CVE-2025-0002") });
var requestedAt = DateTimeOffset.UtcNow;
var context = new VexExportRequestContext(query, VexExportFormat.Json, requestedAt);
var manifest = await engine.ExportAsync(context, CancellationToken.None);
var quiet = Assert.Single(manifest.QuietProvenance);
Assert.Equal("CVE-2025-0002", quiet.VulnerabilityId);
Assert.Equal("pkg:demo/app", quiet.ProductKey);
var statement = Assert.Single(quiet.Statements);
Assert.Equal("vendor", statement.ProviderId);
Assert.Equal("sha256:quiet", statement.StatementId);
Assert.Equal(VexJustification.ComponentNotPresent, statement.Justification);
Assert.NotNull(statement.Signature);
Assert.Equal("quiet-signer", statement.Signature!.Subject);
Assert.Equal("quiet-key", statement.Signature.KeyId);
var expectedQuietJson = VexCanonicalJsonSerializer.Serialize(manifest.QuietProvenance);
Assert.NotNull(attestation.LastRequest);
Assert.True(attestation.LastRequest!.Metadata.TryGetValue("quietedBy", out var quietJson));
Assert.Equal(expectedQuietJson, quietJson);
Assert.True(attestation.LastRequest.Metadata.TryGetValue("quietedByStatementCount", out var quietCount));
Assert.Equal("1", quietCount);
Assert.NotNull(store.LastSavedManifest);
Assert.Equal(manifest.QuietProvenance, store.LastSavedManifest!.QuietProvenance);
}
private sealed class InMemoryExportStore : IVexExportStore
@@ -148,6 +230,48 @@ public sealed class ExportEngineTests
=> FormattableString.Invariant($"{signature}|{format}");
}
private sealed class QuietExportDataSource : IVexExportDataSource
{
public ValueTask<VexExportDataSet> FetchAsync(VexQuery query, CancellationToken cancellationToken)
{
var signature = new VexSignatureMetadata(
type: "pgp",
subject: "quiet-signer",
issuer: "quiet-ca",
keyId: "quiet-key",
verifiedAt: DateTimeOffset.UnixEpoch,
transparencyLogReference: "rekor://quiet");
var claim = new VexClaim(
"CVE-2025-0002",
"vendor",
new VexProduct("pkg:demo/app", "Demo"),
VexClaimStatus.NotAffected,
new VexClaimDocument(VexDocumentFormat.OpenVex, "sha256:quiet", new Uri("https://example.org/quiet"), signature: signature),
DateTimeOffset.UtcNow,
DateTimeOffset.UtcNow,
justification: VexJustification.ComponentNotPresent);
var consensus = new VexConsensus(
"CVE-2025-0002",
claim.Product,
VexConsensusStatus.NotAffected,
DateTimeOffset.UtcNow,
new[]
{
new VexConsensusSource("vendor", VexClaimStatus.NotAffected, "sha256:quiet", 1.0, claim.Justification),
},
conflicts: null,
policyVersion: "baseline/v1",
summary: "not_affected");
return ValueTask.FromResult(new VexExportDataSet(
ImmutableArray.Create(consensus),
ImmutableArray.Create(claim),
ImmutableArray.Create("vendor")));
}
}
private sealed class RecordingAttestationClient : IVexAttestationClient
{
public VexAttestationRequest? LastRequest { get; private set; }
@@ -226,6 +350,8 @@ public sealed class ExportEngineTests
private sealed class InMemoryExportDataSource : IVexExportDataSource
{
public VexExportDataSet? LastDataSet { get; private set; }
public ValueTask<VexExportDataSet> FetchAsync(VexQuery query, CancellationToken cancellationToken)
{
var claim = new VexClaim(
@@ -247,10 +373,13 @@ public sealed class ExportEngineTests
policyVersion: "baseline/v1",
summary: "affected");
return ValueTask.FromResult(new VexExportDataSet(
var dataSet = new VexExportDataSet(
ImmutableArray.Create(consensus),
ImmutableArray.Create(claim),
ImmutableArray.Create("vendor")));
ImmutableArray.Create("vendor"));
LastDataSet = dataSet;
return ValueTask.FromResult(dataSet);
}
}

View File

@@ -0,0 +1,324 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Export;
using StellaOps.Excititor.Policy;
using System.Collections.Immutable;
using System.IO.Abstractions.TestingHelpers;
using Xunit;
namespace StellaOps.Excititor.Export.Tests;
public sealed class MirrorBundlePublisherTests
{
[Fact]
public async Task PublishAsync_WritesMirrorArtifacts()
{
var generatedAt = DateTimeOffset.Parse("2025-10-21T12:00:00Z");
var timeProvider = new FixedTimeProvider(generatedAt);
var fileSystem = new MockFileSystem();
var options = new MirrorDistributionOptions
{
OutputRoot = @"C:\exports",
DirectoryName = "mirror",
TargetRepository = "s3://mirror/excititor",
};
var domain = new MirrorDomainOptions
{
Id = "primary",
DisplayName = "Primary Mirror",
};
var exportOptions = new MirrorExportOptions
{
Key = "consensus-json",
Format = "json",
};
exportOptions.Filters["vulnId"] = "CVE-2025-0001";
domain.Exports.Add(exportOptions);
options.Domains.Add(domain);
var publisher = new VexMirrorBundlePublisher(
new StaticOptionsMonitor<MirrorDistributionOptions>(options),
NullLogger<VexMirrorBundlePublisher>.Instance,
timeProvider,
fileSystem,
cryptoRegistry: null,
Options.Create(new FileSystemArtifactStoreOptions { RootPath = @"C:\exports" }));
var sample = CreateSampleExport(generatedAt);
var manifest = sample.Manifest;
var envelope = sample.Envelope;
var dataSet = sample.DataSet;
await publisher.PublishAsync(manifest, envelope, dataSet, CancellationToken.None);
await publisher.PublishAsync(manifest, envelope, dataSet, CancellationToken.None);
var mirrorRoot = @"C:\exports\mirror";
var domainRoot = Path.Combine(mirrorRoot, "primary");
var bundlePath = Path.Combine(domainRoot, "bundle.json");
var manifestPath = Path.Combine(domainRoot, "manifest.json");
var indexPath = Path.Combine(mirrorRoot, "index.json");
var signaturePath = Path.Combine(domainRoot, "bundle.json.jws");
Assert.True(fileSystem.File.Exists(bundlePath));
Assert.True(fileSystem.File.Exists(manifestPath));
Assert.True(fileSystem.File.Exists(indexPath));
Assert.False(fileSystem.File.Exists(signaturePath));
var bundleBytes = fileSystem.File.ReadAllBytes(bundlePath);
var manifestBytes = fileSystem.File.ReadAllBytes(manifestPath);
var indexBytes = fileSystem.File.ReadAllBytes(indexPath);
var expectedBundleDigest = ComputeSha256(bundleBytes);
var expectedManifestDigest = ComputeSha256(manifestBytes);
var expectedConsensusJson = envelope.ConsensusCanonicalJson;
var expectedScoreJson = envelope.ScoreCanonicalJson;
var expectedClaimsJson = SerializeClaims(dataSet.Claims);
var expectedQuietJson = VexCanonicalJsonSerializer.Serialize(envelope.QuietProvenance);
using (var bundleDocument = JsonDocument.Parse(bundleBytes))
{
var root = bundleDocument.RootElement;
Assert.Equal(1, root.GetProperty("schemaVersion").GetInt32());
Assert.Equal("primary", root.GetProperty("domainId").GetString());
Assert.Equal("Primary Mirror", root.GetProperty("displayName").GetString());
Assert.Equal("s3://mirror/excititor", root.GetProperty("targetRepository").GetString());
var exports = root.GetProperty("exports").EnumerateArray().ToArray();
Assert.Single(exports);
var export = exports[0];
Assert.Equal("consensus-json", export.GetProperty("key").GetString());
Assert.Equal("json", export.GetProperty("format").GetString());
Assert.Equal(manifest.ExportId, export.GetProperty("exportId").GetString());
Assert.Equal(manifest.QuerySignature.Value, export.GetProperty("querySignature").GetString());
Assert.Equal(manifest.Artifact.ToUri(), export.GetProperty("artifactDigest").GetString());
Assert.Equal(manifest.SizeBytes, export.GetProperty("artifactSizeBytes").GetInt64());
Assert.Equal(manifest.ConsensusRevision, export.GetProperty("consensusRevision").GetString());
Assert.Equal(manifest.PolicyRevisionId, export.GetProperty("policyRevisionId").GetString());
Assert.Equal(manifest.PolicyDigest, export.GetProperty("policyDigest").GetString());
Assert.Equal(expectedConsensusJson, export.GetProperty("consensusDocument").GetString());
Assert.Equal(expectedScoreJson, export.GetProperty("scoreDocument").GetString());
Assert.Equal(expectedClaimsJson, export.GetProperty("claimsDocument").GetString());
Assert.Equal(expectedQuietJson, export.GetProperty("quietDocument").GetString());
var providers = export.GetProperty("sourceProviders").EnumerateArray().Select(p => p.GetString()).ToArray();
Assert.Single(providers);
Assert.Equal("vendor", providers[0]);
}
using (var manifestDocument = JsonDocument.Parse(manifestBytes))
{
var root = manifestDocument.RootElement;
Assert.Equal(1, root.GetProperty("schemaVersion").GetInt32());
Assert.Equal("primary", root.GetProperty("domainId").GetString());
Assert.Equal("Primary Mirror", root.GetProperty("displayName").GetString());
Assert.Equal("s3://mirror/excititor", root.GetProperty("targetRepository").GetString());
var bundleDescriptor = root.GetProperty("bundle");
Assert.Equal("primary/bundle.json", bundleDescriptor.GetProperty("path").GetString());
Assert.Equal(expectedBundleDigest, bundleDescriptor.GetProperty("digest").GetString());
Assert.Equal(bundleBytes.LongLength, bundleDescriptor.GetProperty("sizeBytes").GetInt64());
Assert.False(bundleDescriptor.TryGetProperty("signature", out _));
var exports = root.GetProperty("exports").EnumerateArray().ToArray();
Assert.Single(exports);
var export = exports[0];
Assert.Equal("consensus-json", export.GetProperty("key").GetString());
Assert.Equal("json", export.GetProperty("format").GetString());
Assert.Equal(manifest.ExportId, export.GetProperty("exportId").GetString());
Assert.Equal(manifest.QuerySignature.Value, export.GetProperty("querySignature").GetString());
Assert.Equal(manifest.Artifact.ToUri(), export.GetProperty("artifactDigest").GetString());
Assert.Equal(manifest.SizeBytes, export.GetProperty("artifactSizeBytes").GetInt64());
Assert.Equal(manifest.ConsensusRevision, export.GetProperty("consensusRevision").GetString());
Assert.Equal(manifest.PolicyRevisionId, export.GetProperty("policyRevisionId").GetString());
Assert.Equal(manifest.PolicyDigest, export.GetProperty("policyDigest").GetString());
Assert.False(export.TryGetProperty("attestation", out _));
}
using (var indexDocument = JsonDocument.Parse(indexBytes))
{
var root = indexDocument.RootElement;
Assert.Equal(1, root.GetProperty("schemaVersion").GetInt32());
Assert.Equal("s3://mirror/excititor", root.GetProperty("targetRepository").GetString());
var domains = root.GetProperty("domains").EnumerateArray().ToArray();
Assert.Single(domains);
var entry = domains[0];
Assert.Equal("primary", entry.GetProperty("domainId").GetString());
Assert.Equal("Primary Mirror", entry.GetProperty("displayName").GetString());
Assert.Equal(generatedAt, entry.GetProperty("generatedAt").GetDateTimeOffset());
Assert.Equal(1, entry.GetProperty("exportCount").GetInt32());
var manifestDescriptor = entry.GetProperty("manifest");
Assert.Equal("primary/manifest.json", manifestDescriptor.GetProperty("path").GetString());
Assert.Equal(expectedManifestDigest, manifestDescriptor.GetProperty("digest").GetString());
Assert.Equal(manifestBytes.LongLength, manifestDescriptor.GetProperty("sizeBytes").GetInt64());
var bundleDescriptor = entry.GetProperty("bundle");
Assert.Equal("primary/bundle.json", bundleDescriptor.GetProperty("path").GetString());
Assert.Equal(expectedBundleDigest, bundleDescriptor.GetProperty("digest").GetString());
Assert.Equal(bundleBytes.LongLength, bundleDescriptor.GetProperty("sizeBytes").GetInt64());
var exportKeys = entry.GetProperty("exportKeys").EnumerateArray().Select(x => x.GetString()).ToArray();
Assert.Single(exportKeys);
Assert.Equal("consensus-json", exportKeys[0]);
}
}
[Fact]
public async Task PublishAsync_NoMatchingDomain_DoesNotWriteArtifacts()
{
var generatedAt = DateTimeOffset.Parse("2025-10-21T12:00:00Z");
var timeProvider = new FixedTimeProvider(generatedAt);
var fileSystem = new MockFileSystem();
var options = new MirrorDistributionOptions
{
OutputRoot = @"C:\exports",
DirectoryName = "mirror",
};
var domain = new MirrorDomainOptions
{
Id = "primary",
DisplayName = "Primary Mirror",
};
var exportOptions = new MirrorExportOptions
{
Key = "consensus-json",
Format = "json",
};
exportOptions.Filters["vulnId"] = "CVE-2099-9999";
domain.Exports.Add(exportOptions);
options.Domains.Add(domain);
var publisher = new VexMirrorBundlePublisher(
new StaticOptionsMonitor<MirrorDistributionOptions>(options),
NullLogger<VexMirrorBundlePublisher>.Instance,
timeProvider,
fileSystem,
cryptoRegistry: null,
Options.Create(new FileSystemArtifactStoreOptions { RootPath = @"C:\exports" }));
var sample = CreateSampleExport(generatedAt);
await publisher.PublishAsync(sample.Manifest, sample.Envelope, sample.DataSet, CancellationToken.None);
Assert.False(fileSystem.Directory.Exists(@"C:\exports\mirror"));
}
private static SampleExport CreateSampleExport(DateTimeOffset generatedAt)
{
var query = VexQuery.Create(new[] { new VexQueryFilter("vulnId", "CVE-2025-0001") });
var signature = VexQuerySignature.FromQuery(query);
var product = new VexProduct("pkg:demo/app", "Demo");
var document = new VexClaimDocument(VexDocumentFormat.OpenVex, "sha256:quiet", new Uri("https://example.org/vex.json"));
var claim = new VexClaim(
"CVE-2025-0001",
"vendor",
product,
VexClaimStatus.NotAffected,
document,
generatedAt.AddDays(-1),
generatedAt,
justification: VexJustification.ComponentNotPresent);
var consensus = new VexConsensus(
"CVE-2025-0001",
product,
VexConsensusStatus.NotAffected,
generatedAt,
new[] { new VexConsensusSource("vendor", VexClaimStatus.NotAffected, document.Digest, 1.0, claim.Justification) },
conflicts: null,
signals: null,
policyVersion: "baseline/v1",
summary: "not_affected",
policyRevisionId: "policy/v1",
policyDigest: "sha256:policy");
var dataSet = new VexExportDataSet(
ImmutableArray.Create(consensus),
ImmutableArray.Create(claim),
ImmutableArray.Create("vendor"));
var envelope = VexExportEnvelopeBuilder.Build(dataSet, VexPolicySnapshot.Default, generatedAt);
var manifest = new VexExportManifest(
"exports/20251021T120000000Z/abcdef",
signature,
VexExportFormat.Json,
generatedAt,
new VexContentAddress("sha256", "deadbeef"),
dataSet.Claims.Length,
dataSet.SourceProviders,
consensusRevision: "baseline/v1",
policyRevisionId: "policy/v1",
policyDigest: "sha256:policy",
consensusDigest: envelope.ConsensusDigest,
scoreDigest: envelope.ScoreDigest,
quietProvenance: envelope.QuietProvenance,
attestation: null,
sizeBytes: 1024);
return new SampleExport(manifest, envelope, dataSet);
}
private static string SerializeClaims(ImmutableArray<VexClaim> claims)
=> VexCanonicalJsonSerializer.Serialize(
claims
.OrderBy(claim => claim.VulnerabilityId, StringComparer.Ordinal)
.ThenBy(claim => claim.Product.Key, StringComparer.Ordinal)
.ThenBy(claim => claim.ProviderId, StringComparer.Ordinal)
.ToImmutableArray());
private static string ComputeSha256(byte[] bytes)
{
using var sha = SHA256.Create();
var digest = sha.ComputeHash(bytes);
return "sha256:" + Convert.ToHexString(digest).ToLowerInvariant();
}
private sealed record SampleExport(
VexExportManifest Manifest,
VexExportEnvelopeContext Envelope,
VexExportDataSet DataSet);
private sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _value;
public FixedTimeProvider(DateTimeOffset value) => _value = value;
public override DateTimeOffset GetUtcNow() => _value;
}
private sealed class StaticOptionsMonitor<T> : IOptionsMonitor<T>
{
public StaticOptionsMonitor(T value) => CurrentValue = value;
public T CurrentValue { get; private set; }
public T Get(string? name) => CurrentValue;
public IDisposable OnChange(Action<T, string> listener) => NullDisposable.Instance;
private sealed class NullDisposable : IDisposable
{
public static readonly NullDisposable Instance = new();
public void Dispose()
{
}
}
}
}

View File

@@ -1,8 +1,9 @@
using System;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Excititor.Core;
@@ -42,6 +43,7 @@ public sealed class VexExportEngine : IExportEngine
private readonly IVexCacheIndex? _cacheIndex;
private readonly IReadOnlyList<IVexArtifactStore> _artifactStores;
private readonly IVexAttestationClient? _attestationClient;
private readonly IVexMirrorBundlePublisher? _mirrorPublisher;
public VexExportEngine(
IVexExportStore exportStore,
@@ -51,7 +53,8 @@ public sealed class VexExportEngine : IExportEngine
ILogger<VexExportEngine> logger,
IVexCacheIndex? cacheIndex = null,
IEnumerable<IVexArtifactStore>? artifactStores = null,
IVexAttestationClient? attestationClient = null)
IVexAttestationClient? attestationClient = null,
IVexMirrorBundlePublisher? mirrorPublisher = null)
{
_exportStore = exportStore ?? throw new ArgumentNullException(nameof(exportStore));
_policyEvaluator = policyEvaluator ?? throw new ArgumentNullException(nameof(policyEvaluator));
@@ -60,6 +63,7 @@ public sealed class VexExportEngine : IExportEngine
_cacheIndex = cacheIndex;
_artifactStores = artifactStores?.ToArray() ?? Array.Empty<IVexArtifactStore>();
_attestationClient = attestationClient;
_mirrorPublisher = mirrorPublisher;
if (exporters is null)
{
@@ -105,12 +109,13 @@ public sealed class VexExportEngine : IExportEngine
}
var dataset = await _dataSource.FetchAsync(context.Query, cancellationToken).ConfigureAwait(false);
var exporter = ResolveExporter(context.Format);
var policySnapshot = _policyEvaluator.Snapshot;
var envelopeContext = VexExportEnvelopeBuilder.Build(dataset, policySnapshot, context.RequestedAt);
var exporter = ResolveExporter(context.Format);
var exportRequest = new VexExportRequest(
context.Query,
dataset.Consensus,
envelopeContext.Consensus,
dataset.Claims,
context.RequestedAt);
@@ -120,6 +125,37 @@ public sealed class VexExportEngine : IExportEngine
await using var buffer = new MemoryStream();
var result = await exporter.SerializeAsync(exportRequest, buffer, cancellationToken).ConfigureAwait(false);
var metadataBuilder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
foreach (var kvp in result.Metadata)
{
metadataBuilder[kvp.Key] = kvp.Value;
}
metadataBuilder["consensusDigest"] = envelopeContext.ConsensusDigest.ToUri();
metadataBuilder["consensusEntryCount"] = envelopeContext.Consensus.Length.ToString(CultureInfo.InvariantCulture);
metadataBuilder["scoreDigest"] = envelopeContext.ScoreDigest.ToUri();
metadataBuilder["scoreEntryCount"] = envelopeContext.ScoreEnvelope.Entries.Length.ToString(CultureInfo.InvariantCulture);
metadataBuilder["policyRevisionId"] = policySnapshot.RevisionId;
metadataBuilder["policyVersion"] = policySnapshot.Version;
if (!string.IsNullOrWhiteSpace(policySnapshot.Digest))
{
metadataBuilder["policyDigest"] = policySnapshot.Digest;
}
metadataBuilder["scoreAlpha"] = policySnapshot.ConsensusOptions.Alpha.ToString("G17", CultureInfo.InvariantCulture);
metadataBuilder["scoreBeta"] = policySnapshot.ConsensusOptions.Beta.ToString("G17", CultureInfo.InvariantCulture);
metadataBuilder["scoreWeightCeiling"] = policySnapshot.ConsensusOptions.WeightCeiling.ToString("G17", CultureInfo.InvariantCulture);
if (!envelopeContext.QuietProvenance.IsDefaultOrEmpty && envelopeContext.QuietProvenance.Length > 0)
{
metadataBuilder["quietedBy"] = VexCanonicalJsonSerializer.Serialize(envelopeContext.QuietProvenance);
var quietStatementCount = envelopeContext.QuietProvenance.Sum(static entry => entry.Statements.Length);
metadataBuilder["quietedByStatementCount"] = quietStatementCount.ToString(CultureInfo.InvariantCulture);
}
var exportMetadata = metadataBuilder.ToImmutable();
if (_artifactStores.Count > 0)
{
var writtenBytes = buffer.ToArray();
@@ -129,7 +165,7 @@ public sealed class VexExportEngine : IExportEngine
result.Digest,
context.Format,
writtenBytes,
result.Metadata);
exportMetadata);
foreach (var store in _artifactStores)
{
@@ -155,7 +191,7 @@ public sealed class VexExportEngine : IExportEngine
context.Format,
context.RequestedAt,
dataset.SourceProviders,
result.Metadata);
exportMetadata);
var response = await _attestationClient.SignAsync(attestationRequest, cancellationToken).ConfigureAwait(false);
attestationMetadata = response.Attestation;
@@ -175,8 +211,8 @@ public sealed class VexExportEngine : IExportEngine
_logger.LogInformation("Attestation generated for export {ExportId}", exportId);
}
var consensusDigestAddress = TryGetContentAddress(result.Metadata, "consensusDigest");
var scoreDigestAddress = TryGetContentAddress(result.Metadata, "scoreDigest");
var consensusDigestAddress = TryGetContentAddress(exportMetadata, "consensusDigest");
var scoreDigestAddress = TryGetContentAddress(exportMetadata, "scoreDigest");
var manifest = new VexExportManifest(
exportId,
@@ -192,11 +228,24 @@ public sealed class VexExportEngine : IExportEngine
policyDigest: policySnapshot.Digest,
consensusDigest: consensusDigestAddress,
scoreDigest: scoreDigestAddress,
quietProvenance: envelopeContext.QuietProvenance,
attestation: attestationMetadata,
sizeBytes: result.BytesWritten);
await _exportStore.SaveAsync(manifest, cancellationToken).ConfigureAwait(false);
if (_mirrorPublisher is not null)
{
try
{
await _mirrorPublisher.PublishAsync(manifest, envelopeContext, dataset, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Mirror bundle publishing failed for export {ExportId}", manifest.ExportId);
}
}
_logger.LogInformation(
"Export generated for {Signature} ({Format}) size={SizeBytes} bytes",
signature.Value,
@@ -237,6 +286,7 @@ public static class VexExportServiceCollectionExtensions
{
public static IServiceCollection AddVexExportEngine(this IServiceCollection services)
{
services.AddSingleton<IVexMirrorBundlePublisher, VexMirrorBundlePublisher>();
services.AddSingleton<IExportEngine, VexExportEngine>();
services.AddVexExportCacheServices();
return services;

View File

@@ -15,5 +15,6 @@
<ProjectReference Include="..\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
<ProjectReference Include="..\StellaOps.Excititor.Policy\StellaOps.Excititor.Policy.csproj" />
<ProjectReference Include="..\StellaOps.Excititor.Storage.Mongo\StellaOps.Excititor.Storage.Mongo.csproj" />
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,6 +6,6 @@ If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md
|EXCITITOR-EXPORT-01-002 Cache index & eviction hooks|Team Excititor Export|EXCITITOR-EXPORT-01-001, EXCITITOR-STORAGE-01-003|**DONE (2025-10-16)** Export engine now invalidates cache entries on force refresh, cache services expose prune/invalidate APIs, and storage maintenance trims expired/dangling records with Mongo2Go coverage.|
|EXCITITOR-EXPORT-01-003 Artifact store adapters|Team Excititor Export|EXCITITOR-EXPORT-01-001|**DONE (2025-10-16)** Implemented multi-store pipeline with filesystem, S3-compatible, and offline bundle adapters (hash verification + manifest/zip output) plus unit coverage and DI hooks.|
|EXCITITOR-EXPORT-01-004 Attestation handoff integration|Team Excititor Export|EXCITITOR-EXPORT-01-001, EXCITITOR-ATTEST-01-001|**DONE (2025-10-17)** Export engine now invokes attestation client, logs diagnostics, and persists Rekor/envelope metadata on manifests; regression coverage added in `ExportEngineTests.ExportAsync_AttachesAttestationMetadata`.|
|EXCITITOR-EXPORT-01-005 Score & resolve envelope surfaces|Team Excititor Export|EXCITITOR-EXPORT-01-004, EXCITITOR-CORE-02-001|**DOING (2025-10-19)** Prereqs EXCITITOR-EXPORT-01-004 and EXCITITOR-CORE-02-001 confirmed DONE; planning export updates to emit consensus+score envelopes, include policy/scoring digests, and extend offline bundle/ORAS layouts for signed VEX responses.|
|EXCITITOR-EXPORT-01-006 Quiet provenance packaging|Team Excititor Export|EXCITITOR-EXPORT-01-005, POLICY-CORE-09-005|TODO Attach `quietedBy` statement IDs, signers, and justification codes to exports/offline bundles, mirror metadata into attested manifest, and add regression fixtures.|
|EXCITITOR-EXPORT-01-007 Mirror bundle + domain manifest|Team Excititor Export|EXCITITOR-EXPORT-01-006|TODO Create per-domain mirror bundles with consensus/score artifacts, publish signed index for downstream Excititor sync, and ensure deterministic digests + fixtures.|
|EXCITITOR-EXPORT-01-005 Score & resolve envelope surfaces|Team Excititor Export|EXCITITOR-EXPORT-01-004, EXCITITOR-CORE-02-001|**DONE (2025-10-21)** Export engine now canonicalizes consensus/score envelopes, persists their SHA-256 digests into manifests/attestation metadata, and regression tests validate metadata wiring via `ExportEngineTests`.|
|EXCITITOR-EXPORT-01-006 Quiet provenance packaging|Team Excititor Export|EXCITITOR-EXPORT-01-005, POLICY-CORE-09-005|**DONE (2025-10-21)** Export manifests now carry quiet-provenance entries (statement digests, signers, justification codes); metadata flows into offline bundles & attestations with regression coverage in `ExportEngineTests`.|
|EXCITITOR-EXPORT-01-007 Mirror bundle + domain manifest|Team Excititor Export|EXCITITOR-EXPORT-01-006|**DONE (2025-10-21)** Created per-domain mirror bundles with consensus/score artefacts, published signed-ready manifests/index for downstream Excititor sync, and added regression coverage.|

View File

@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Linq;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Policy;
namespace StellaOps.Excititor.Export;
internal static class VexExportEnvelopeBuilder
{
public static VexExportEnvelopeContext Build(
VexExportDataSet dataSet,
VexPolicySnapshot policySnapshot,
DateTimeOffset generatedAt)
{
ArgumentNullException.ThrowIfNull(dataSet);
ArgumentNullException.ThrowIfNull(policySnapshot);
var orderedConsensus = dataSet.Consensus
.OrderBy(static consensus => consensus.VulnerabilityId, StringComparer.Ordinal)
.ThenBy(static consensus => consensus.Product.Key, StringComparer.Ordinal)
.ToImmutableArray();
var claimsByKey = dataSet.Claims
.GroupBy(static claim => (claim.VulnerabilityId, claim.Product.Key))
.ToDictionary(
static group => group.Key,
static group => group.ToImmutableArray());
var quietEntries = ImmutableArray.CreateBuilder<VexQuietProvenance>();
foreach (var consensus in orderedConsensus)
{
if (consensus.Status != VexConsensusStatus.NotAffected)
{
continue;
}
if (!claimsByKey.TryGetValue((consensus.VulnerabilityId, consensus.Product.Key), out var claimsForKey) ||
claimsForKey.IsDefaultOrEmpty)
{
continue;
}
var statementsBuilder = ImmutableArray.CreateBuilder<VexQuietStatement>();
foreach (var source in consensus.Sources)
{
if (source.Status != VexClaimStatus.NotAffected)
{
continue;
}
var matchingClaim = claimsForKey.FirstOrDefault(claim =>
string.Equals(claim.ProviderId, source.ProviderId, StringComparison.Ordinal) &&
string.Equals(claim.Document.Digest, source.DocumentDigest, StringComparison.Ordinal));
if (matchingClaim is null)
{
continue;
}
var justification = matchingClaim.Justification ?? source.Justification;
statementsBuilder.Add(new VexQuietStatement(
matchingClaim.ProviderId,
matchingClaim.Document.Digest,
justification,
matchingClaim.Document.Signature));
}
if (statementsBuilder.Count == 0)
{
continue;
}
quietEntries.Add(new VexQuietProvenance(
consensus.VulnerabilityId,
consensus.Product.Key,
statementsBuilder.ToImmutable()));
}
var consensusJson = VexCanonicalJsonSerializer.Serialize(orderedConsensus);
var consensusDigest = ComputeAddress(consensusJson);
var scoreEntries = orderedConsensus
.Select(static consensus => new VexScoreEntry(
consensus.VulnerabilityId,
consensus.Product.Key,
consensus.Status,
consensus.CalculatedAt,
consensus.Signals,
score: null))
.ToImmutableArray();
var options = policySnapshot.ConsensusOptions;
var scoreEnvelope = new VexScoreEnvelope(
generatedAt.ToUniversalTime(),
policySnapshot.RevisionId,
NormalizeDigest(policySnapshot.Digest),
options.Alpha,
options.Beta,
options.WeightCeiling,
scoreEntries);
var scoreJson = VexCanonicalJsonSerializer.Serialize(scoreEnvelope);
var scoreDigest = ComputeAddress(scoreJson);
return new VexExportEnvelopeContext(
orderedConsensus,
consensusJson,
consensusDigest,
scoreEnvelope,
scoreJson,
scoreDigest,
quietEntries.ToImmutable());
}
private static string? NormalizeDigest(string? digest)
=> string.IsNullOrWhiteSpace(digest) ? null : digest.Trim();
private static VexContentAddress ComputeAddress(string canonicalJson)
{
var bytes = Encoding.UTF8.GetBytes(canonicalJson);
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
SHA256.HashData(bytes, hash);
var digest = Convert.ToHexString(hash).ToLowerInvariant();
return new VexContentAddress("sha256", digest);
}
}
internal sealed record VexExportEnvelopeContext(
ImmutableArray<VexConsensus> Consensus,
string ConsensusCanonicalJson,
VexContentAddress ConsensusDigest,
VexScoreEnvelope ScoreEnvelope,
string ScoreCanonicalJson,
VexContentAddress ScoreDigest,
ImmutableArray<VexQuietProvenance> QuietProvenance);

View File

@@ -0,0 +1,716 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography;
using StellaOps.Excititor.Core;
namespace StellaOps.Excititor.Export;
public interface IVexMirrorBundlePublisher
{
ValueTask PublishAsync(
VexExportManifest manifest,
VexExportEnvelopeContext envelope,
VexExportDataSet dataSet,
CancellationToken cancellationToken);
}
public sealed class VexMirrorBundlePublisher : IVexMirrorBundlePublisher
{
private const int SchemaVersion = 1;
private const string BundleFileName = "bundle.json";
private const string BundleSignatureFileName = "bundle.json.jws";
private const string ManifestFileName = "manifest.json";
private const string IndexFileName = "index.json";
private const string SignatureMediaType = "application/vnd.stellaops.excititor.mirror-bundle+jws";
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.General)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false,
};
private readonly IOptionsMonitor<MirrorDistributionOptions> _optionsMonitor;
private readonly ILogger<VexMirrorBundlePublisher> _logger;
private readonly TimeProvider _timeProvider;
private readonly IFileSystem _fileSystem;
private readonly ICryptoProviderRegistry? _cryptoRegistry;
private readonly IOptions<FileSystemArtifactStoreOptions>? _fileSystemOptions;
private readonly SemaphoreSlim _mutex = new(1, 1);
public VexMirrorBundlePublisher(
IOptionsMonitor<MirrorDistributionOptions> optionsMonitor,
ILogger<VexMirrorBundlePublisher> logger,
TimeProvider timeProvider,
IFileSystem? fileSystem = null,
ICryptoProviderRegistry? cryptoRegistry = null,
IOptions<FileSystemArtifactStoreOptions>? fileSystemOptions = null)
{
_optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_fileSystem = fileSystem ?? new FileSystem();
_cryptoRegistry = cryptoRegistry;
_fileSystemOptions = fileSystemOptions;
}
public async ValueTask PublishAsync(
VexExportManifest manifest,
VexExportEnvelopeContext envelope,
VexExportDataSet dataSet,
CancellationToken cancellationToken)
{
if (manifest is null)
{
throw new ArgumentNullException(nameof(manifest));
}
if (envelope is null)
{
throw new ArgumentNullException(nameof(envelope));
}
if (dataSet is null)
{
throw new ArgumentNullException(nameof(dataSet));
}
var options = _optionsMonitor.CurrentValue;
if (!options.Enabled || options.Domains.Count == 0)
{
return;
}
var matches = ResolveDomainMatches(options, manifest);
if (matches.Count == 0)
{
return;
}
await _mutex.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var outputRoot = ResolveOutputRoot(options);
var mirrorRoot = _fileSystem.Path.Combine(outputRoot, options.DirectoryName ?? "mirror");
_fileSystem.Directory.CreateDirectory(mirrorRoot);
foreach (var match in matches)
{
await UpdateDomainAsync(
options,
match.Domain,
match.Plan,
manifest,
envelope,
dataSet,
mirrorRoot,
cancellationToken).ConfigureAwait(false);
}
await WriteIndexAsync(options, mirrorRoot, cancellationToken).ConfigureAwait(false);
}
finally
{
_mutex.Release();
}
}
private string ResolveOutputRoot(MirrorDistributionOptions options)
{
if (!string.IsNullOrWhiteSpace(options.OutputRoot))
{
return _fileSystem.Path.GetFullPath(options.OutputRoot);
}
if (_fileSystemOptions?.Value is { RootPath: { Length: > 0 } root })
{
return _fileSystem.Path.GetFullPath(root);
}
return _fileSystem.Path.GetFullPath(_fileSystem.Path.Combine(AppContext.BaseDirectory, "mirror"));
}
private static List<DomainMatch> ResolveDomainMatches(MirrorDistributionOptions options, VexExportManifest manifest)
{
var matches = new List<DomainMatch>();
foreach (var domain in options.Domains)
{
foreach (var export in domain.Exports)
{
if (!MirrorExportPlanner.TryBuild(export, out var plan, out _))
{
continue;
}
if (!string.Equals(plan.Signature.Value, manifest.QuerySignature.Value, StringComparison.Ordinal))
{
continue;
}
if (plan.Format != manifest.Format)
{
continue;
}
matches.Add(new DomainMatch(domain, plan));
}
}
return matches;
}
private async Task UpdateDomainAsync(
MirrorDistributionOptions options,
MirrorDomainOptions domain,
MirrorExportPlan plan,
VexExportManifest manifest,
VexExportEnvelopeContext envelope,
VexExportDataSet dataSet,
string mirrorRoot,
CancellationToken cancellationToken)
{
var domainDirectory = _fileSystem.Path.Combine(mirrorRoot, domain.Id);
_fileSystem.Directory.CreateDirectory(domainDirectory);
var bundlePath = _fileSystem.Path.Combine(domainDirectory, BundleFileName);
var existingBundle = await ReadDocumentAsync<MirrorBundleDocument>(bundlePath, cancellationToken).ConfigureAwait(false);
var exports = existingBundle?.Exports.ToDictionary(entry => entry.Key, StringComparer.Ordinal)
?? new Dictionary<string, MirrorBundleExportEntry>(StringComparer.Ordinal);
var exportEntry = CreateExportEntry(plan, manifest, envelope, dataSet);
exports[exportEntry.Key] = exportEntry;
var orderedExports = exports.Values.OrderBy(entry => entry.Key, StringComparer.Ordinal).ToArray();
var generatedAt = _timeProvider.GetUtcNow();
var bundleDocument = new MirrorBundleDocument(
SchemaVersion,
generatedAt,
options.TargetRepository,
domain.Id,
string.IsNullOrWhiteSpace(domain.DisplayName) ? domain.Id : domain.DisplayName,
orderedExports);
var bundleBytes = Serialize(bundleDocument);
var bundleDigest = ComputeDigest(bundleBytes);
await WriteFileAsync(bundlePath, bundleBytes, cancellationToken).ConfigureAwait(false);
MirrorSignatureDescriptor? signatureDescriptor = null;
if (options.Signing.Enabled)
{
signatureDescriptor = await WriteSignatureAsync(
options.Signing,
mirrorRoot,
domainDirectory,
bundleBytes,
cancellationToken).ConfigureAwait(false);
}
else
{
var signaturePath = _fileSystem.Path.Combine(domainDirectory, BundleSignatureFileName);
if (_fileSystem.File.Exists(signaturePath))
{
_fileSystem.File.Delete(signaturePath);
}
}
var manifestDocument = new MirrorDomainManifestDocument(
SchemaVersion,
generatedAt,
domain.Id,
string.IsNullOrWhiteSpace(domain.DisplayName) ? domain.Id : domain.DisplayName,
options.TargetRepository,
new MirrorFileDescriptor(
ToRelativePath(mirrorRoot, bundlePath),
bundleBytes.LongLength,
bundleDigest,
signatureDescriptor),
orderedExports.Select(CreateManifestExportEntry).ToArray());
var manifestBytes = Serialize(manifestDocument);
await WriteFileAsync(_fileSystem.Path.Combine(domainDirectory, ManifestFileName), manifestBytes, cancellationToken).ConfigureAwait(false);
_logger.LogInformation(
"Updated mirror bundle for domain {DomainId} export {ExportKey} (digest {Digest}).",
domain.Id,
plan.Key,
bundleDigest);
}
private async Task WriteIndexAsync(MirrorDistributionOptions options, string mirrorRoot, CancellationToken cancellationToken)
{
var entries = new List<MirrorIndexDomainEntry>();
foreach (var domain in options.Domains.OrderBy(d => d.Id, StringComparer.Ordinal))
{
var domainDirectory = _fileSystem.Path.Combine(mirrorRoot, domain.Id);
var manifestPath = _fileSystem.Path.Combine(domainDirectory, ManifestFileName);
var bundlePath = _fileSystem.Path.Combine(domainDirectory, BundleFileName);
var manifestBytes = await ReadAllBytesAsync(manifestPath, cancellationToken).ConfigureAwait(false);
var bundleBytes = await ReadAllBytesAsync(bundlePath, cancellationToken).ConfigureAwait(false);
if (manifestBytes is null || bundleBytes is null)
{
continue;
}
var manifestDocument = JsonSerializer.Deserialize<MirrorDomainManifestDocument>(manifestBytes, SerializerOptions);
if (manifestDocument is null)
{
continue;
}
var manifestDescriptor = new MirrorFileDescriptor(
ToRelativePath(mirrorRoot, manifestPath),
manifestBytes.LongLength,
ComputeDigest(manifestBytes),
signature: null);
var bundleDescriptor = manifestDocument.Bundle with
{
Path = ToRelativePath(mirrorRoot, bundlePath),
SizeBytes = bundleBytes.LongLength,
Digest = ComputeDigest(bundleBytes),
};
var exportKeys = manifestDocument.Exports
.Select(export => export.Key)
.OrderBy(key => key, StringComparer.Ordinal)
.ToArray();
entries.Add(new MirrorIndexDomainEntry(
manifestDocument.DomainId,
manifestDocument.DisplayName,
manifestDocument.GeneratedAt,
manifestDocument.Exports.Length,
manifestDescriptor,
bundleDescriptor,
exportKeys));
}
var indexDocument = new MirrorIndexDocument(
SchemaVersion,
_timeProvider.GetUtcNow(),
options.TargetRepository,
entries.OrderBy(entry => entry.DomainId, StringComparer.Ordinal).ToArray());
var indexBytes = Serialize(indexDocument);
var indexPath = _fileSystem.Path.Combine(mirrorRoot, IndexFileName);
await WriteFileAsync(indexPath, indexBytes, cancellationToken).ConfigureAwait(false);
}
private MirrorBundleExportEntry CreateExportEntry(
MirrorExportPlan plan,
VexExportManifest manifest,
VexExportEnvelopeContext envelope,
VexExportDataSet dataSet)
{
var consensusJson = envelope.ConsensusCanonicalJson;
var scoreJson = envelope.ScoreCanonicalJson;
var claimsJson = SerializeClaims(dataSet.Claims);
var quietJson = SerializeQuiet(envelope.QuietProvenance);
return new MirrorBundleExportEntry(
plan.Key,
plan.Format.ToString().ToLowerInvariant(),
manifest.ExportId,
manifest.QuerySignature.Value,
manifest.CreatedAt,
manifest.SizeBytes,
manifest.Artifact.ToUri(),
manifest.ConsensusRevision,
manifest.PolicyRevisionId,
manifest.PolicyDigest,
manifest.ConsensusDigest?.ToUri(),
manifest.ScoreDigest?.ToUri(),
manifest.SourceProviders.ToArray(),
consensusJson,
scoreJson,
claimsJson,
quietJson,
manifest.Attestation is null
? null
: new MirrorExportAttestationDescriptor(
manifest.Attestation.PredicateType,
manifest.Attestation.Rekor?.Location,
manifest.Attestation.EnvelopeDigest,
manifest.Attestation.SignedAt));
}
private static MirrorManifestExportEntry CreateManifestExportEntry(MirrorBundleExportEntry entry)
=> new(
entry.Key,
entry.Format,
entry.ExportId,
entry.QuerySignature,
entry.CreatedAt,
entry.ArtifactDigest,
entry.ArtifactSizeBytes,
entry.ConsensusRevision,
entry.PolicyRevisionId,
entry.PolicyDigest,
entry.ConsensusDigest,
entry.ScoreDigest,
entry.SourceProviders,
entry.Attestation);
private static string? SerializeClaims(ImmutableArray<VexClaim> claims)
{
if (claims.IsDefaultOrEmpty || claims.Length == 0)
{
return null;
}
var ordered = claims
.OrderBy(c => c.VulnerabilityId, StringComparer.Ordinal)
.ThenBy(c => c.Product.Key, StringComparer.Ordinal)
.ThenBy(c => c.ProviderId, StringComparer.Ordinal)
.ToImmutableArray();
return VexCanonicalJsonSerializer.Serialize(ordered);
}
private static string? SerializeQuiet(ImmutableArray<VexQuietProvenance> quiet)
{
if (quiet.IsDefaultOrEmpty || quiet.Length == 0)
{
return null;
}
return VexCanonicalJsonSerializer.Serialize(quiet);
}
private static byte[] Serialize<T>(T document)
=> JsonSerializer.SerializeToUtf8Bytes(document, SerializerOptions);
private async Task<T?> ReadDocumentAsync<T>(string path, CancellationToken cancellationToken)
{
if (!_fileSystem.File.Exists(path))
{
return default;
}
await using var stream = _fileSystem.File.OpenRead(path);
return await JsonSerializer.DeserializeAsync<T>(stream, SerializerOptions, cancellationToken).ConfigureAwait(false);
}
private async Task<byte[]?> ReadAllBytesAsync(string path, CancellationToken cancellationToken)
{
if (!_fileSystem.File.Exists(path))
{
return null;
}
await using var stream = _fileSystem.File.OpenRead(path);
using var buffer = new MemoryStream();
await stream.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
return buffer.ToArray();
}
private async Task WriteFileAsync(string path, byte[] content, CancellationToken cancellationToken)
{
var directory = _fileSystem.Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory))
{
_fileSystem.Directory.CreateDirectory(directory);
}
await using var stream = _fileSystem.File.Create(path);
await stream.WriteAsync(content, 0, content.Length, cancellationToken).ConfigureAwait(false);
}
private static string ComputeDigest(ReadOnlySpan<byte> content)
{
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
SHA256.HashData(content, hash);
return FormattableString.Invariant($"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}");
}
private async Task<MirrorSignatureDescriptor?> WriteSignatureAsync(
MirrorSigningOptions signingOptions,
string mirrorRoot,
string domainDirectory,
ReadOnlyMemory<byte> payload,
CancellationToken cancellationToken)
{
if (!signingOptions.Enabled)
{
return null;
}
if (_cryptoRegistry is null)
{
throw new InvalidOperationException("Mirror signing requires ICryptoProviderRegistry to be registered.");
}
var context = PrepareSigningContext(signingOptions);
var (signature, signedAt) = await CreateSignatureAsync(context, payload, cancellationToken).ConfigureAwait(false);
var signaturePath = _fileSystem.Path.Combine(domainDirectory, BundleSignatureFileName);
await WriteFileAsync(signaturePath, Utf8NoBom.GetBytes(signature), cancellationToken).ConfigureAwait(false);
return new MirrorSignatureDescriptor(
ToRelativePath(mirrorRoot, signaturePath),
context.Algorithm,
context.Signer.KeyId,
context.Provider,
signedAt);
}
private JsonMirrorSigningContext PrepareSigningContext(MirrorSigningOptions signingOptions)
{
var algorithm = string.IsNullOrWhiteSpace(signingOptions.Algorithm)
? SignatureAlgorithms.Es256
: signingOptions.Algorithm.Trim();
var keyId = signingOptions.KeyId?.Trim();
if (string.IsNullOrEmpty(keyId))
{
throw new InvalidOperationException("Mirror signing requires Excititor:Mirror:Signing:KeyId to be configured.");
}
var providerHint = signingOptions.Provider?.Trim();
CryptoSignerResolution resolved;
try
{
resolved = _cryptoRegistry!.ResolveSigner(CryptoCapability.Signing, algorithm, new CryptoKeyReference(keyId, providerHint), providerHint);
}
catch (KeyNotFoundException)
{
var provider = ResolveProvider(algorithm, providerHint);
var signingKey = LoadSigningKey(signingOptions, provider, algorithm);
provider.UpsertSigningKey(signingKey);
resolved = _cryptoRegistry.ResolveSigner(CryptoCapability.Signing, algorithm, new CryptoKeyReference(keyId, provider.Name), provider.Name);
}
return new JsonMirrorSigningContext(resolved.Signer, algorithm, resolved.ProviderName, _timeProvider);
}
private ICryptoProvider ResolveProvider(string algorithm, string? providerHint)
{
if (!string.IsNullOrWhiteSpace(providerHint) && _cryptoRegistry!.TryResolve(providerHint, out var hinted))
{
if (!hinted.Supports(CryptoCapability.Signing, algorithm))
{
throw new InvalidOperationException(FormattableString.Invariant(
$"Crypto provider '{providerHint}' does not support signing algorithm '{algorithm}'."));
}
return hinted;
}
return _cryptoRegistry!.ResolveOrThrow(CryptoCapability.Signing, algorithm);
}
private CryptoSigningKey LoadSigningKey(MirrorSigningOptions signingOptions, ICryptoProvider provider, string algorithm)
{
var keyPath = signingOptions.KeyPath?.Trim();
if (string.IsNullOrEmpty(keyPath))
{
throw new InvalidOperationException("Mirror signing requires Excititor:Mirror:Signing:KeyPath when the key is not already loaded.");
}
var resolvedPath = _fileSystem.Path.IsPathRooted(keyPath)
? keyPath
: _fileSystem.Path.GetFullPath(_fileSystem.Path.Combine(AppContext.BaseDirectory, keyPath));
if (!_fileSystem.File.Exists(resolvedPath))
{
throw new FileNotFoundException($"Mirror signing key '{resolvedPath}' not found.", resolvedPath);
}
var pem = _fileSystem.File.ReadAllText(resolvedPath);
using var ecdsa = ECDsa.Create();
try
{
ecdsa.ImportFromPem(pem);
}
catch (CryptographicException ex)
{
throw new InvalidOperationException("Failed to import mirror signing key. Ensure the PEM contains an EC private key.", ex);
}
var parameters = ecdsa.ExportParameters(includePrivateParameters: true);
return new CryptoSigningKey(
new CryptoKeyReference(signingOptions.KeyId!, provider.Name),
algorithm,
in parameters,
_timeProvider.GetUtcNow());
}
private static async Task<(string Signature, DateTimeOffset SignedAt)> CreateSignatureAsync(
JsonMirrorSigningContext context,
ReadOnlyMemory<byte> payload,
CancellationToken cancellationToken)
{
var header = new Dictionary<string, object>
{
["alg"] = context.Algorithm,
["kid"] = context.Signer.KeyId,
["typ"] = SignatureMediaType,
["b64"] = false,
["crit"] = new[] { "b64" },
};
if (!string.IsNullOrWhiteSpace(context.Provider))
{
header["provider"] = context.Provider;
}
var headerJson = JsonSerializer.Serialize(header, SerializerOptions);
var protectedHeader = Base64UrlEncode(Utf8NoBom.GetBytes(headerJson));
var signingInputLength = protectedHeader.Length + 1 + payload.Length;
var buffer = ArrayPool<byte>.Shared.Rent(signingInputLength);
try
{
var headerBytes = Encoding.ASCII.GetBytes(protectedHeader);
Buffer.BlockCopy(headerBytes, 0, buffer, 0, headerBytes.Length);
buffer[headerBytes.Length] = (byte)'.';
payload.Span.CopyTo(new Span<byte>(buffer, headerBytes.Length + 1, payload.Length));
var signingInput = new ReadOnlyMemory<byte>(buffer, 0, signingInputLength);
var signatureBytes = await context.Signer.SignAsync(signingInput, cancellationToken).ConfigureAwait(false);
var encodedSignature = Base64UrlEncode(signatureBytes);
var signedAt = context.TimeProvider.GetUtcNow();
return (string.Concat(protectedHeader, "..", encodedSignature), signedAt);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
private static string Base64UrlEncode(ReadOnlySpan<byte> value)
=> Convert.ToBase64String(value)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
private string ToRelativePath(string root, string path)
{
if (string.IsNullOrEmpty(path))
{
return string.Empty;
}
var fullRoot = _fileSystem.Path.GetFullPath(root);
var fullPath = _fileSystem.Path.GetFullPath(path);
if (!fullPath.StartsWith(fullRoot, StringComparison.OrdinalIgnoreCase))
{
return fullPath.Replace('\\', '/');
}
var relative = fullPath[fullRoot.Length..]
.TrimStart(_fileSystem.Path.DirectorySeparatorChar, _fileSystem.Path.AltDirectorySeparatorChar);
return relative.Replace('\\', '/');
}
private readonly record struct DomainMatch(MirrorDomainOptions Domain, MirrorExportPlan Plan);
private sealed record JsonMirrorSigningContext(ICryptoSigner Signer, string Algorithm, string Provider, TimeProvider TimeProvider);
private sealed record MirrorBundleDocument(
int SchemaVersion,
DateTimeOffset GeneratedAt,
string? TargetRepository,
string DomainId,
string DisplayName,
IReadOnlyList<MirrorBundleExportEntry> Exports);
private sealed record MirrorBundleExportEntry(
string Key,
string Format,
string ExportId,
string QuerySignature,
DateTimeOffset CreatedAt,
long ArtifactSizeBytes,
string ArtifactDigest,
string? ConsensusRevision,
string? PolicyRevisionId,
string? PolicyDigest,
string? ConsensusDigest,
string? ScoreDigest,
IReadOnlyList<string> SourceProviders,
string? ConsensusDocument,
string? ScoreDocument,
string? ClaimsDocument,
string? QuietDocument,
MirrorExportAttestationDescriptor? Attestation);
private sealed record MirrorExportAttestationDescriptor(
string PredicateType,
string? RekorLocation,
string? EnvelopeDigest,
DateTimeOffset? SignedAt);
private sealed record MirrorDomainManifestDocument(
int SchemaVersion,
DateTimeOffset GeneratedAt,
string DomainId,
string DisplayName,
string? TargetRepository,
MirrorFileDescriptor Bundle,
IReadOnlyList<MirrorManifestExportEntry> Exports);
private sealed record MirrorManifestExportEntry(
string Key,
string Format,
string ExportId,
string QuerySignature,
DateTimeOffset CreatedAt,
string ArtifactDigest,
long ArtifactSizeBytes,
string? ConsensusRevision,
string? PolicyRevisionId,
string? PolicyDigest,
string? ConsensusDigest,
string? ScoreDigest,
IReadOnlyList<string> SourceProviders,
MirrorExportAttestationDescriptor? Attestation);
private sealed record MirrorFileDescriptor(
string Path,
long SizeBytes,
string Digest,
MirrorSignatureDescriptor? Signature);
private sealed record MirrorSignatureDescriptor(
string Path,
string Algorithm,
string KeyId,
string? Provider,
DateTimeOffset SignedAt);
private sealed record MirrorIndexDocument(
int SchemaVersion,
DateTimeOffset GeneratedAt,
string? TargetRepository,
IReadOnlyList<MirrorIndexDomainEntry> Domains);
private sealed record MirrorIndexDomainEntry(
string DomainId,
string DisplayName,
DateTimeOffset GeneratedAt,
int ExportCount,
MirrorFileDescriptor Manifest,
MirrorFileDescriptor Bundle,
IReadOnlyList<string> ExportKeys);
}

View File

@@ -105,11 +105,13 @@ internal sealed class VexExportManifestRecord
public string? ScoreDigestAlgorithm { get; set; }
= null;
public string? ScoreDigestValue { get; set; }
= null;
public string? PredicateType { get; set; }
= null;
public string? ScoreDigestValue { get; set; }
= null;
public List<VexQuietProvenanceRecord> QuietProvenance { get; set; } = new();
public string? PredicateType { get; set; }
= null;
public string? RekorApiVersion { get; set; }
= null;
@@ -150,10 +152,11 @@ internal sealed class VexExportManifestRecord
ConsensusDigestAlgorithm = manifest.ConsensusDigest?.Algorithm,
ConsensusDigestValue = manifest.ConsensusDigest?.Digest,
ScoreDigestAlgorithm = manifest.ScoreDigest?.Algorithm,
ScoreDigestValue = manifest.ScoreDigest?.Digest,
PredicateType = manifest.Attestation?.PredicateType,
RekorApiVersion = manifest.Attestation?.Rekor?.ApiVersion,
RekorLocation = manifest.Attestation?.Rekor?.Location,
ScoreDigestValue = manifest.ScoreDigest?.Digest,
QuietProvenance = manifest.QuietProvenance.Select(VexQuietProvenanceRecord.FromDomain).ToList(),
PredicateType = manifest.Attestation?.PredicateType,
RekorApiVersion = manifest.Attestation?.Rekor?.ApiVersion,
RekorLocation = manifest.Attestation?.Rekor?.Location,
RekorLogIndex = manifest.Attestation?.Rekor?.LogIndex,
RekorInclusionProofUri = manifest.Attestation?.Rekor?.InclusionProofUri?.ToString(),
EnvelopeDigest = manifest.Attestation?.EnvelopeDigest,
@@ -201,19 +204,81 @@ internal sealed class VexExportManifestRecord
ConsensusRevision,
PolicyRevisionId,
PolicyDigest,
consensusDigest,
scoreDigest,
attestation,
SizeBytes);
consensusDigest,
scoreDigest,
quietProvenance: QuietProvenance.Count == 0
? ImmutableArray<VexQuietProvenance>.Empty
: QuietProvenance
.Select(static record => record.ToDomain())
.ToImmutableArray(),
attestation,
SizeBytes);
}
public static string CreateId(VexQuerySignature signature, VexExportFormat format)
=> string.Format(CultureInfo.InvariantCulture, "{0}|{1}", signature.Value, format.ToString().ToLowerInvariant());
}
[BsonIgnoreExtraElements]
internal sealed class VexProviderRecord
{
}
[BsonIgnoreExtraElements]
internal sealed class VexQuietProvenanceRecord
{
public string VulnerabilityId { get; set; } = default!;
public string ProductKey { get; set; } = default!;
public List<VexQuietStatementRecord> Statements { get; set; } = new();
public static VexQuietProvenanceRecord FromDomain(VexQuietProvenance provenance)
=> new()
{
VulnerabilityId = provenance.VulnerabilityId,
ProductKey = provenance.ProductKey,
Statements = provenance.Statements.Select(VexQuietStatementRecord.FromDomain).ToList(),
};
public VexQuietProvenance ToDomain()
=> new(VulnerabilityId, ProductKey, Statements.Select(static statement => statement.ToDomain()));
}
[BsonIgnoreExtraElements]
internal sealed class VexQuietStatementRecord
{
public string ProviderId { get; set; } = default!;
public string StatementId { get; set; } = default!;
public string? Justification { get; set; }
= null;
public VexSignatureMetadataDocument? Signature { get; set; }
= null;
public static VexQuietStatementRecord FromDomain(VexQuietStatement statement)
=> new()
{
ProviderId = statement.ProviderId,
StatementId = statement.StatementId,
Justification = statement.Justification?.ToString().ToLowerInvariant(),
Signature = VexSignatureMetadataDocument.FromDomain(statement.Signature),
};
public VexQuietStatement ToDomain()
{
var justification = string.IsNullOrWhiteSpace(Justification)
? (VexJustification?)null
: Enum.Parse<VexJustification>(Justification, ignoreCase: true);
return new VexQuietStatement(
ProviderId,
StatementId,
justification,
Signature?.ToDomain());
}
}
[BsonIgnoreExtraElements]
internal sealed class VexProviderRecord
{
[BsonId]
public string Id { get; set; } = default!;

View File

@@ -15,7 +15,6 @@ using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Export;
using StellaOps.Excititor.Policy;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.WebService.Options;
namespace StellaOps.Excititor.WebService.Tests;

View File

@@ -8,9 +8,8 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Export;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.WebService.Options;
using StellaOps.Excititor.WebService.Services;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.WebService.Services;
namespace StellaOps.Excititor.WebService.Endpoints;
@@ -99,13 +98,13 @@ internal static class MirrorEndpoints
}
var resolvedExports = new List<MirrorExportIndexEntry>();
foreach (var exportOption in domain.Exports)
{
if (!TryBuildExportPlan(exportOption, out var plan, out var error))
{
resolvedExports.Add(new MirrorExportIndexEntry(
exportOption.Key,
null,
foreach (var exportOption in domain.Exports)
{
if (!MirrorExportPlanner.TryBuild(exportOption, out var plan, out var error))
{
resolvedExports.Add(new MirrorExportIndexEntry(
exportOption.Key,
null,
null,
exportOption.Format,
null,
@@ -117,7 +116,7 @@ internal static class MirrorEndpoints
continue;
}
var manifest = await exportStore.FindAsync(plan.Signature, plan.Format, cancellationToken).ConfigureAwait(false);
var manifest = await exportStore.FindAsync(plan.Signature, plan.Format, cancellationToken).ConfigureAwait(false);
if (manifest is null)
{
@@ -178,16 +177,16 @@ internal static class MirrorEndpoints
return Results.Unauthorized();
}
if (!TryFindExport(domain, exportKey, out var exportOptions))
{
return Results.NotFound();
}
if (!TryBuildExportPlan(exportOptions, out var plan, out var error))
{
await WritePlainTextAsync(httpContext, error ?? "invalid_export_configuration", StatusCodes.Status503ServiceUnavailable, cancellationToken).ConfigureAwait(false);
return Results.Empty;
}
if (!TryFindExport(domain, exportKey, out var exportOptions))
{
return Results.NotFound();
}
if (!MirrorExportPlanner.TryBuild(exportOptions, out var plan, out var error))
{
await WritePlainTextAsync(httpContext, error ?? "invalid_export_configuration", StatusCodes.Status503ServiceUnavailable, cancellationToken).ConfigureAwait(false);
return Results.Empty;
}
var manifest = await exportStore.FindAsync(plan.Signature, plan.Format, cancellationToken).ConfigureAwait(false);
if (manifest is null)
@@ -242,10 +241,10 @@ internal static class MirrorEndpoints
return Results.Empty;
}
if (!TryFindExport(domain, exportKey, out var exportOptions) || !TryBuildExportPlan(exportOptions, out var plan, out _))
{
return Results.NotFound();
}
if (!TryFindExport(domain, exportKey, out var exportOptions) || !MirrorExportPlanner.TryBuild(exportOptions, out var plan, out _))
{
return Results.NotFound();
}
var manifest = await exportStore.FindAsync(plan.Signature, plan.Format, cancellationToken).ConfigureAwait(false);
if (manifest is null)
@@ -287,37 +286,11 @@ internal static class MirrorEndpoints
return domain is not null;
}
private static bool TryFindExport(MirrorDomainOptions domain, string exportKey, out MirrorExportOptions export)
{
export = domain.Exports.FirstOrDefault(e => string.Equals(e.Key, exportKey, StringComparison.OrdinalIgnoreCase))!;
return export is not null;
}
private static bool TryBuildExportPlan(MirrorExportOptions exportOptions, out MirrorExportPlan plan, out string? error)
{
plan = null!;
error = null;
if (string.IsNullOrWhiteSpace(exportOptions.Key))
{
error = "missing_export_key";
return false;
}
if (string.IsNullOrWhiteSpace(exportOptions.Format) || !Enum.TryParse<VexExportFormat>(exportOptions.Format, ignoreCase: true, out var format))
{
error = "unsupported_export_format";
return false;
}
var filters = exportOptions.Filters.Select(pair => new KeyValuePair<string, string>(pair.Key, pair.Value)).ToArray();
var sorts = exportOptions.Sort.Select(pair => new VexQuerySort(pair.Key, pair.Value)).ToArray();
var query = VexQuery.Create(filters.Select(kv => new VexQueryFilter(kv.Key, kv.Value)), sorts, exportOptions.Limit, exportOptions.Offset, exportOptions.View);
var signature = VexQuerySignature.FromQuery(query);
plan = new MirrorExportPlan(format, query, signature);
return true;
}
private static bool TryFindExport(MirrorDomainOptions domain, string exportKey, out MirrorExportOptions export)
{
export = domain.Exports.FirstOrDefault(e => string.Equals(e.Key, exportKey, StringComparison.OrdinalIgnoreCase))!;
return export is not null;
}
private static string ResolveContentType(VexExportFormat format)
=> format switch
@@ -351,19 +324,15 @@ internal static class MirrorEndpoints
await context.Response.WriteAsync(message, cancellationToken);
}
private static async Task WriteJsonAsync<T>(HttpContext context, T payload, int statusCode, CancellationToken cancellationToken)
{
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
var json = VexCanonicalJsonSerializer.Serialize(payload);
await context.Response.WriteAsync(json, cancellationToken);
private static async Task WriteJsonAsync<T>(HttpContext context, T payload, int statusCode, CancellationToken cancellationToken)
{
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
var json = VexCanonicalJsonSerializer.Serialize(payload);
await context.Response.WriteAsync(json, cancellationToken);
}
private sealed record MirrorExportPlan(
VexExportFormat Format,
VexQuery Query,
VexQuerySignature Signature);
}
}
internal sealed record MirrorDomainListResponse(IReadOnlyList<MirrorDomainSummary> Domains);

View File

@@ -1,52 +0,0 @@
using System.Collections.Generic;
namespace StellaOps.Excititor.WebService.Options;
public sealed class MirrorDistributionOptions
{
public const string SectionName = "Excititor:Mirror";
public List<MirrorDomainOptions> Domains { get; } = new();
}
public sealed class MirrorDomainOptions
{
public string Id { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public bool RequireAuthentication { get; set; }
= false;
/// <summary>
/// Maximum index requests allowed per rolling window.
/// </summary>
public int MaxIndexRequestsPerHour { get; set; } = 120;
/// <summary>
/// Maximum export downloads allowed per rolling window.
/// </summary>
public int MaxDownloadRequestsPerHour { get; set; } = 600;
public List<MirrorExportOptions> Exports { get; } = new();
}
public sealed class MirrorExportOptions
{
public string Key { get; set; } = string.Empty;
public string Format { get; set; } = string.Empty;
public Dictionary<string, string> Filters { get; } = new();
public Dictionary<string, bool> Sort { get; } = new();
public int? Limit { get; set; }
= null;
public int? Offset { get; set; }
= null;
public string? View { get; set; }
= null;
}

View File

@@ -13,11 +13,11 @@ using StellaOps.Excititor.Export;
using StellaOps.Excititor.Formats.CSAF;
using StellaOps.Excititor.Formats.CycloneDX;
using StellaOps.Excititor.Formats.OpenVEX;
using StellaOps.Excititor.Policy;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.WebService.Endpoints;
using StellaOps.Excititor.WebService.Options;
using StellaOps.Excititor.WebService.Services;
using StellaOps.Excititor.Policy;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.WebService.Endpoints;
using StellaOps.Excititor.WebService.Services;
using StellaOps.Excititor.Core;
var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NJsonSchema;
using Xunit;
using System.Threading.Tasks;
namespace StellaOps.Notify.Models.Tests;
public sealed class PlatformEventSchemaValidationTests
{
public static IEnumerable<object[]> SampleFiles() => new[]
{
new object[] { "scanner.report.ready@1.sample.json", "scanner.report.ready@1.json" },
new object[] { "scanner.scan.completed@1.sample.json", "scanner.scan.completed@1.json" },
new object[] { "scheduler.rescan.delta@1.sample.json", "scheduler.rescan.delta@1.json" },
new object[] { "attestor.logged@1.sample.json", "attestor.logged@1.json" }
};
[Theory]
[MemberData(nameof(SampleFiles))]
public async Task EventSamplesConformToPublishedSchemas(string sampleFile, string schemaFile)
{
var baseDirectory = AppContext.BaseDirectory;
var samplePath = Path.Combine(baseDirectory, sampleFile);
var schemaPath = Path.Combine(baseDirectory, schemaFile);
Assert.True(File.Exists(samplePath), $"Sample '{sampleFile}' not found at '{samplePath}'.");
Assert.True(File.Exists(schemaPath), $"Schema '{schemaFile}' not found at '{schemaPath}'.");
var schema = await JsonSchema.FromJsonAsync(File.ReadAllText(schemaPath));
var errors = schema.Validate(File.ReadAllText(samplePath));
if (errors.Count > 0)
{
var formatted = string.Join(
Environment.NewLine,
errors.Select(error => $"{error.Path}: {error.Kind} ({error})"));
Assert.True(errors.Count == 0, $"Schema validation failed for '{sampleFile}':{Environment.NewLine}{formatted}");
}
}
}

View File

@@ -5,16 +5,20 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Notify.Models/StellaOps.Notify.Models.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="../../docs/events/samples/*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="../../docs/notify/samples/*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Notify.Models/StellaOps.Notify.Models.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NJsonSchema" Version="10.9.0" />
<None Include="../../docs/events/samples/*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="../../docs/events/*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="../../docs/notify/samples/*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.DependencyInjection;
using StellaOps.Plugin.DependencyInjection;
using StellaOps.Plugin.Hosting;
using Xunit;
namespace StellaOps.Plugin.Tests.DependencyInjection;
public sealed class PluginDependencyInjectionExtensionsTests
{
[Fact]
public void RegisterPluginRoutines_RegistersServiceBindingsAndHonoursLifetimes()
{
const string source = """
using Microsoft.Extensions.DependencyInjection;
using StellaOps.DependencyInjection;
namespace SamplePlugin;
public interface IScopedExample {}
public interface ISingletonExample {}
[ServiceBinding(typeof(IScopedExample), ServiceLifetime.Scoped, RegisterAsSelf = true)]
public sealed class ScopedExample : IScopedExample {}
[ServiceBinding(typeof(ISingletonExample), ServiceLifetime.Singleton)]
public sealed class SingletonExample : ISingletonExample {}
""";
using var plugin = TestPluginAssembly.Create(source);
var configuration = new ConfigurationBuilder().Build();
var services = new ServiceCollection();
services.RegisterPluginRoutines(configuration, plugin.Options, NullLogger.Instance);
var scopedDescriptor = Assert.Single(
services,
static d => d.ServiceType.FullName == "SamplePlugin.IScopedExample");
Assert.Equal(ServiceLifetime.Scoped, scopedDescriptor.Lifetime);
Assert.Equal("SamplePlugin.ScopedExample", scopedDescriptor.ImplementationType?.FullName);
var scopedSelfDescriptor = Assert.Single(
services,
static d => d.ServiceType.FullName == "SamplePlugin.ScopedExample");
Assert.Equal(ServiceLifetime.Scoped, scopedSelfDescriptor.Lifetime);
var singletonDescriptor = Assert.Single(
services,
static d => d.ServiceType.FullName == "SamplePlugin.ISingletonExample");
Assert.Equal(ServiceLifetime.Singleton, singletonDescriptor.Lifetime);
using var provider = services.BuildServiceProvider();
object firstScopeInstance;
using (var scope = provider.CreateScope())
{
var resolvedFirst = ServiceProviderServiceExtensions.GetRequiredService(scope.ServiceProvider, scopedDescriptor.ServiceType);
var resolvedSecond = ServiceProviderServiceExtensions.GetRequiredService(scope.ServiceProvider, scopedDescriptor.ServiceType);
Assert.Same(resolvedFirst, resolvedSecond);
firstScopeInstance = resolvedFirst;
}
using (var scope = provider.CreateScope())
{
var resolved = ServiceProviderServiceExtensions.GetRequiredService(scope.ServiceProvider, scopedDescriptor.ServiceType);
Assert.NotSame(firstScopeInstance, resolved);
}
var singletonFirst = ServiceProviderServiceExtensions.GetRequiredService(provider, singletonDescriptor.ServiceType);
var singletonSecond = ServiceProviderServiceExtensions.GetRequiredService(provider, singletonDescriptor.ServiceType);
Assert.Same(singletonFirst, singletonSecond);
services.RegisterPluginRoutines(configuration, plugin.Options, NullLogger.Instance);
var scopedRegistrations = services.Count(d =>
d.ServiceType.FullName == "SamplePlugin.IScopedExample" &&
d.ImplementationType?.FullName == "SamplePlugin.ScopedExample");
Assert.Equal(1, scopedRegistrations);
}
private sealed class TestPluginAssembly : IDisposable
{
private TestPluginAssembly(string directoryPath, string assemblyPath)
{
DirectoryPath = directoryPath;
AssemblyPath = assemblyPath;
Options = new PluginHostOptions
{
PluginsDirectory = directoryPath,
EnsureDirectoryExists = false,
RecursiveSearch = false,
};
Options.SearchPatterns.Add(Path.GetFileName(assemblyPath));
}
public string DirectoryPath { get; }
public string AssemblyPath { get; }
public PluginHostOptions Options { get; }
public static TestPluginAssembly Create(string source)
{
var directoryPath = Path.Combine(Path.GetTempPath(), "stellaops-plugin-tests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directoryPath);
var assemblyName = "SamplePlugin" + Guid.NewGuid().ToString("N");
var assemblyPath = Path.Combine(directoryPath, assemblyName + ".dll");
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var references = CollectMetadataReferences();
var compilation = CSharpCompilation.Create(
assemblyName,
new[] { syntaxTree },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release));
var emitResult = compilation.Emit(assemblyPath);
if (!emitResult.Success)
{
var diagnostics = string.Join(Environment.NewLine, emitResult.Diagnostics);
throw new InvalidOperationException("Failed to compile plugin assembly:" + Environment.NewLine + diagnostics);
}
return new TestPluginAssembly(directoryPath, assemblyPath);
}
public void Dispose()
{
try
{
if (Directory.Exists(DirectoryPath))
{
Directory.Delete(DirectoryPath, recursive: true);
}
}
catch
{
// Ignore cleanup failures plugin load contexts may keep files locked on Windows.
}
}
private static IReadOnlyCollection<MetadataReference> CollectMetadataReferences()
{
var referencePaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string tpa)
{
foreach (var path in tpa.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
{
referencePaths.Add(path);
}
}
referencePaths.Add(typeof(object).Assembly.Location);
referencePaths.Add(typeof(ServiceBindingAttribute).Assembly.Location);
referencePaths.Add(typeof(IDependencyInjectionRoutine).Assembly.Location);
referencePaths.Add(typeof(ServiceLifetime).Assembly.Location);
return referencePaths
.Select(path => MetadataReference.CreateFromFile(path))
.ToArray();
}
}
}

View File

@@ -9,10 +9,13 @@
<ProjectReference Include="..\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj" />
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
</Project>

View File

@@ -1,7 +1,7 @@
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|PLUGIN-DI-08-001 Scoped service support in plugin bootstrap|Plugin Platform Guild (DONE 2025-10-19)|StellaOps.DependencyInjection|Introduced `ServiceBindingAttribute` metadata for scoped DI, taught plugin/job loaders to consume it with duplicate-safe registration, added coverage, and refreshed the plug-in SDK guide.|
|PLUGIN-DI-08-001 Scoped service support in plugin bootstrap|Plugin Platform Guild (DONE 2025-10-21)|StellaOps.DependencyInjection|Scoped DI metadata primitives landed; dynamic plugin integration tests now verify `RegisterPluginRoutines` honours `[ServiceBinding]` lifetimes and remains idempotent.|
|PLUGIN-DI-08-002.COORD Authority scoped-service handshake|Plugin Platform Guild, Authority Core (DONE 2025-10-20)|PLUGIN-DI-08-001|Workshop held 2025-10-20 15:0016:05UTC; outcomes/notes captured in `docs/dev/authority-plugin-di-coordination.md`, follow-up action items assigned for PLUGIN-DI-08-002 implementation plan.|
|PLUGIN-DI-08-002 Authority plugin integration updates|Plugin Platform Guild, Authority Core (DONE 2025-10-20)|PLUGIN-DI-08-001, PLUGIN-DI-08-002.COORD|Standard registrar now registers scoped credential/provisioning stores + identity-provider plugins, registry Acquire scopes instances, and regression suites (`dotnet test src/StellaOps.Authority/StellaOps.Authority.Plugin.Standard.Tests/StellaOps.Authority.Plugin.Standard.Tests.csproj`, `dotnet test src/StellaOps.Authority/StellaOps.Authority.Tests/StellaOps.Authority.Tests.csproj`) cover scoped lifetimes + handles.|
|PLUGIN-DI-08-003 Authority registry scoped resolution|Plugin Platform Guild, Authority Core (DONE 2025-10-20)|PLUGIN-DI-08-002.COORD|Reworked `IAuthorityIdentityProviderRegistry` to expose metadata + scoped handles, updated OpenIddict flows/Program health endpoints, and added coverage via `AuthorityIdentityProviderRegistryTests`.|

View File

@@ -14,9 +14,14 @@ Run `ng generate component component-name` to generate a new component. You can
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running unit tests
The suite runs headlessly through Karma. Install dependencies with `npm install`, then execute `npm test` (alias for `ng test --watch=false`). The run expects a Chromium-compatible browser:
- On developer machines ensure Google Chrome or Chromium is available on `PATH`; otherwise set `CHROME_BIN` to the browser executable.
- In CI you can rely on Puppeteer by exporting `PUPPETEER_EXECUTABLE_PATH` to the downloaded Chromium binary; the test harness automatically adopts it.
For interactive development, use `npm run test:watch` (invokes `ng test --watch`) and optionally pass `--browsers=Chrome` to open a full browser.
## Running end-to-end tests

View File

@@ -3,4 +3,5 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| WEB1.TRIVY-SETTINGS | DONE (2025-10-21) | UX Specialist, Angular Eng | Backend `/exporters/trivy-db` contract | Implement Trivy DB exporter settings panel with `publishFull`, `publishDelta`, `includeFull`, `includeDelta` toggles and “Run export now” action using future `/exporters/trivy-db/settings` API. | ✅ Angular route `/concelier/trivy-db-settings` backed by `TrivyDbSettingsPageComponent` with reactive form; ✅ Overrides persisted via `ConcelierExporterClient` (`settings`/`run` endpoints); ✅ Manual run button saves current overrides then triggers export and surfaces run metadata. |
| WEB1.TRIVY-SETTINGS-TESTS | BLOCKED (2025-10-21) | UX Specialist, Angular Eng | WEB1.TRIVY-SETTINGS | Add headless UI test run (`ng test --watch=false`) and document steps once Angular CLI tooling is available in CI/local environment. | Angular CLI available (npm scripts chained), Karma suite for Trivy DB components passing locally and in CI, docs note required prerequisites. |
| WEB1.TRIVY-SETTINGS-TESTS | DONE (2025-10-21) | UX Specialist, Angular Eng | WEB1.TRIVY-SETTINGS | **DONE (2025-10-21)** Added headless Karma harness (`ng test --watch=false`) wired to ChromeHeadless/CI launcher, created `karma.conf.cjs`, updated npm scripts + docs with Chromium prerequisites so CI/offline runners can execute specs deterministically. | Angular CLI available (npm scripts chained), Karma suite for Trivy DB components passing locally and in CI, docs note required prerequisites. |
| WEB1.DEPS-13-001 | TODO | UX Specialist, Angular Eng, DevEx | WEB1.TRIVY-SETTINGS-TESTS | Stabilise Angular workspace dependencies for CI/offline nodes: refresh `package-lock.json`, ensure Puppeteer/Chromium binaries optional, document deterministic install workflow. | `npm install` completes without manual intervention on air-gapped nodes, `npm test` headless run succeeds from clean checkout, README updated with lockfile + cache steps. |

View File

@@ -76,19 +76,20 @@
"buildTarget": "stellaops-web:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.cjs",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],

View File

@@ -0,0 +1,49 @@
const { join } = require('path');
const { env } = process;
if (!env.CHROME_BIN && env.PUPPETEER_EXECUTABLE_PATH) {
env.CHROME_BIN = env.PUPPETEER_EXECUTABLE_PATH;
}
const isCI = env.CI === 'true' || env.CI === '1';
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false
},
jasmineHtmlReporter: {
suppressAll: true
},
coverageReporter: {
dir: join(__dirname, './coverage/stellaops-web'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
browsers: [isCI ? 'ChromeHeadlessCI' : 'ChromeHeadless'],
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
}
},
restartOnFileChange: false
});
};

View File

@@ -1,13 +1,14 @@
{
"name": "stellaops-web",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test --watch=false",
"test:watch": "ng test --watch"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.3.0",

View File

@@ -7,8 +7,7 @@ import {
signal,
} from '@angular/core';
import {
FormBuilder,
FormGroup,
NonNullableFormBuilder,
ReactiveFormsModule,
} from '@angular/forms';
import { firstValueFrom } from 'rxjs';
@@ -28,19 +27,21 @@ type StatusKind = 'idle' | 'loading' | 'saving' | 'running' | 'success' | 'error
styleUrls: ['./trivy-db-settings-page.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
type TrivyDbSettingsFormValue = TrivyDbSettingsDto;
export class TrivyDbSettingsPageComponent implements OnInit {
private readonly client = inject(ConcelierExporterClient);
private readonly formBuilder = inject(FormBuilder);
private readonly formBuilder = inject(NonNullableFormBuilder);
readonly status = signal<StatusKind>('idle');
readonly message = signal<string | null>(null);
readonly lastRun = signal<TrivyDbRunResponseDto | null>(null);
readonly form: FormGroup = this.formBuilder.group({
publishFull: [true],
publishDelta: [true],
includeFull: [true],
includeDelta: [true],
readonly form = this.formBuilder.group<TrivyDbSettingsFormValue>({
publishFull: true,
publishDelta: true,
includeFull: true,
includeDelta: true,
});
ngOnInit(): void {
@@ -52,7 +53,7 @@ export class TrivyDbSettingsPageComponent implements OnInit {
this.message.set(null);
try {
const settings = await firstValueFrom(
const settings: TrivyDbSettingsDto = await firstValueFrom(
this.client.getTrivyDbSettings()
);
this.form.patchValue(settings);
@@ -73,7 +74,7 @@ export class TrivyDbSettingsPageComponent implements OnInit {
try {
const payload = this.buildPayload();
const updated = await firstValueFrom(
const updated: TrivyDbSettingsDto = await firstValueFrom(
this.client.updateTrivyDbSettings(payload)
);
this.form.patchValue(updated);
@@ -98,7 +99,7 @@ export class TrivyDbSettingsPageComponent implements OnInit {
// Persist overrides before triggering a run, ensuring parity.
await firstValueFrom(this.client.updateTrivyDbSettings(payload));
const response = await firstValueFrom(
const response: TrivyDbRunResponseDto = await firstValueFrom(
this.client.runTrivyDbExport(payload)
);
@@ -124,7 +125,7 @@ export class TrivyDbSettingsPageComponent implements OnInit {
}
private buildPayload(): TrivyDbSettingsDto {
const raw = this.form.getRawValue() as TrivyDbSettingsDto;
const raw = this.form.getRawValue();
return {
publishFull: !!raw.publishFull,
publishDelta: !!raw.publishDelta,