This commit is contained in:
@@ -38,6 +38,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
npm install --no-save markdown-link-check remark-cli remark-preset-lint-recommended ajv ajv-cli ajv-formats
|
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
|
- name: Link check
|
||||||
run: |
|
run: |
|
||||||
find docs -name '*.md' -print0 | \
|
find docs -name '*.md' -print0 | \
|
||||||
@@ -63,6 +68,10 @@ jobs:
|
|||||||
npx ajv validate -c ajv-formats -s "$schema_path" -d "$sample"
|
npx ajv validate -c ajv-formats -s "$schema_path" -d "$sample"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
- 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
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
318
EXECPLAN.md
318
EXECPLAN.md
@@ -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 – 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 – 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 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 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 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.
|
- 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 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 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 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 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.
|
- 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 – 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 – 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 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 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 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.
|
- 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, 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 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 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 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 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.
|
- 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 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 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 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.
|
- 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
|
### 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: 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 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 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 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.
|
- 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.
|
- 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
|
### 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
|
### 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
|
### 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.
|
- 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
|
- **Sprint 1** · Backlog
|
||||||
- Team: UX Specialist, Angular Eng
|
- Team: UX Specialist, Angular Eng
|
||||||
- Path: `src/StellaOps.Web/TASKS.md`
|
- 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.
|
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: —
|
|
||||||
• 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.
|
|
||||||
• Prereqs: WEB1.TRIVY-SETTINGS
|
• 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
|
- **Sprint 1** · Developer Tooling
|
||||||
- Team: DevEx/CLI
|
- Team: DevEx/CLI
|
||||||
- Path: `src/StellaOps.Cli/TASKS.md`
|
- 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.
|
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: —
|
• Prereqs: —
|
||||||
• Current: DOING (2025-10-10)
|
• 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.
|
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: —
|
• Prereqs: —
|
||||||
• Current: BLOCKED (2025-10-10)
|
• 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.
|
• Current: TODO – Add verification helpers for Worker/WebService, metrics/logging hooks, and negative-path regression tests.
|
||||||
- Team: Team Excititor WebService
|
- Team: Team Excititor WebService
|
||||||
- Path: `src/StellaOps.Excititor.WebService/TASKS.md`
|
- 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
|
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)
|
• 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.
|
• 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
|
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)
|
• 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.
|
• 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
|
- **Sprint 7** · Contextual Truth Foundations
|
||||||
- Team: Team Excititor Export
|
- Team: Team Excititor Export
|
||||||
- Path: `src/StellaOps.Excititor.Export/TASKS.md`
|
- 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)
|
• 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.
|
• 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 24–48h 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
|
- **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
|
- Team: Runtime Guild
|
||||||
- Path: `docs/TASKS.md`
|
- Path: `docs/TASKS.md`
|
||||||
1. [TODO] RUNTIME-GUILD-09-402 — Confirm Scanner WebService surfaces `quietedFindingCount` and progress hints to runtime consumers; document readiness checklist.
|
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)
|
• Prereqs: SCANNER-POLICY-09-107 (external/completed)
|
||||||
• Current: TODO
|
• 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
|
- **Sprint 10** · Backlog
|
||||||
- Team: TBD
|
- Team: TBD
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`
|
- 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.
|
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)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-302B (external/completed)
|
||||||
• Current: TODO
|
• 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
|
- **Sprint 10** · Scanner Analyzers & SBOM
|
||||||
- Team: Diff Guild
|
- Team: Diff Guild
|
||||||
- Path: `src/StellaOps.Scanner.Diff/TASKS.md`
|
- 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
|
• Current: TODO
|
||||||
- Team: Authority Core & Security Guild
|
- Team: Authority Core & Security Guild
|
||||||
- Path: `src/StellaOps.Authority/TASKS.md`
|
- 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.
|
2. [DOING] AUTH-MTLS-11-002 — Add OAuth mTLS client credential support with certificate-bound tokens and introspection updates.
|
||||||
• Prereqs: —
|
• Prereqs: —
|
||||||
• Current: DOING (2025-10-19)
|
• 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
|
- **Sprint 12** · Runtime Guardrails
|
||||||
- Team: Zastava Core Guild
|
- Team: Zastava Core Guild
|
||||||
- Path: `src/StellaOps.Zastava.Core/TASKS.md`
|
- 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
|
• Current: TODO
|
||||||
- Team: Scanner WebService Guild
|
- Team: Scanner WebService Guild
|
||||||
- Path: `src/StellaOps.Scanner.WebService/TASKS.md`
|
- 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.
|
2. [BLOCKED] SCANNER-EVENTS-16-301 — Redis publisher integration tests once Notify queue adapter ships.
|
||||||
• Prereqs: NOTIFY-QUEUE-15-401 (Wave 1)
|
• Prereqs: NOTIFY-QUEUE-15-401 (Wave 1)
|
||||||
• Current: BLOCKED – waiting on Notify queue abstraction and Redis adapter deliverables for end-to-end validation.
|
• Current: BLOCKED – waiting on Notify queue abstraction and Redis adapter deliverables for end-to-end validation.
|
||||||
- **Sprint 16** · Scheduler Intelligence
|
- **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
|
- Team: Scheduler Storage Guild
|
||||||
- Path: `src/StellaOps.Scheduler.Storage.Mongo/TASKS.md`
|
- 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.
|
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
|
- **Sprint 7** · Contextual Truth Foundations
|
||||||
- Team: Team Excititor Export
|
- Team: Team Excititor Export
|
||||||
- Path: `src/StellaOps.Excititor.Export/TASKS.md`
|
- 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)
|
• 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.
|
• 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
|
- **Sprint 10** · Backlog
|
||||||
- Team: TBD
|
- Team: TBD
|
||||||
- Path: `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`
|
- 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.
|
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)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
|
||||||
• Current: TODO
|
• 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
|
- **Sprint 10** · Scanner Analyzers & SBOM
|
||||||
- Team: Emit Guild
|
- Team: Emit Guild
|
||||||
- Path: `src/StellaOps.Scanner.Emit/TASKS.md`
|
- 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
|
- **Sprint 12** · Runtime Guardrails
|
||||||
- Team: Scanner WebService Guild
|
- Team: Scanner WebService Guild
|
||||||
- Path: `src/StellaOps.Scanner.WebService/TASKS.md`
|
- 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.
|
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)
|
• 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.
|
• 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
|
1. [TODO] CLI-RUNTIME-13-009 — CLI-RUNTIME-13-009 – Runtime policy smoke fixture
|
||||||
• Prereqs: CLI-RUNTIME-13-005 (Wave 0)
|
• 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.
|
• 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
|
- Team: UI Guild
|
||||||
- Path: `src/StellaOps.UI/TASKS.md`
|
- Path: `src/StellaOps.UI/TASKS.md`
|
||||||
1. [TODO] UI-VEX-13-003 — Implement VEX explorer + policy editor with preview integration.
|
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.
|
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)
|
• Prereqs: SCHED-WEB-16-101 (Wave 0)
|
||||||
• Current: TODO
|
• 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
|
- **Sprint 14** · Release & Offline Ops
|
||||||
- Team: DevOps Guild
|
- Team: DevOps Guild
|
||||||
- Path: `ops/devops/TASKS.md`
|
- 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.
|
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)
|
• Prereqs: NOTIFY-MODELS-15-101 (Wave 0)
|
||||||
• Current: TODO
|
• 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
|
- **Sprint 16** · Scheduler Intelligence
|
||||||
- Team: Scheduler ImpactIndex Guild
|
- Team: Scheduler ImpactIndex Guild
|
||||||
- Path: `src/StellaOps.Scheduler.ImpactIndex/TASKS.md`
|
- 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).
|
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)
|
• Prereqs: SCANNER-EMIT-10-605 (Wave 0)
|
||||||
• Current: TODO
|
• 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
|
- Team: Scheduler Storage Guild
|
||||||
- Path: `src/StellaOps.Scheduler.Storage.Mongo/TASKS.md`
|
- Path: `src/StellaOps.Scheduler.Storage.Mongo/TASKS.md`
|
||||||
1. [TODO] SCHED-STORAGE-16-203 — Audit/logging pipeline + run stats materialized views for UI.
|
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
|
- **Sprint 7** · Contextual Truth Foundations
|
||||||
- Team: Team Excititor Export
|
- Team: Team Excititor Export
|
||||||
- Path: `src/StellaOps.Excititor.Export/TASKS.md`
|
- 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)
|
• 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.
|
• 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
|
- **Sprint 9** · DevOps Foundations
|
||||||
- Team: DevOps Guild, Notify Guild
|
- Team: DevOps Guild, Notify Guild
|
||||||
- Path: `ops/devops/TASKS.md`
|
- 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
|
- **Sprint 7** · Contextual Truth Foundations
|
||||||
- Team: Excititor Connectors – Stella
|
- Team: Excititor Connectors – Stella
|
||||||
- Path: `src/StellaOps.Excititor.Connectors.StellaOpsMirror/TASKS.md`
|
- 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)
|
• Prereqs: EXCITITOR-EXPORT-01-007 (Wave 2)
|
||||||
• Current: TODO
|
• Current: TODO
|
||||||
- **Sprint 10** · Backlog
|
- **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)
|
• Prereqs: ZASTAVA-OBS-12-002 (Wave 2)
|
||||||
• Current: TODO
|
• Current: TODO
|
||||||
- **Sprint 13** · UX & CLI Experience
|
- **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
|
- Team: DevEx/CLI, Scanner WebService Guild
|
||||||
- Path: `src/StellaOps.Cli/TASKS.md`
|
- Path: `src/StellaOps.Cli/TASKS.md`
|
||||||
1. [TODO] CLI-RUNTIME-13-008 — CLI-RUNTIME-13-008 – Runtime policy contract sync
|
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)
|
• Prereqs: NOTIFY-CONN-EMAIL-15-701 (Wave 4)
|
||||||
• Current: BLOCKED – waiting on base SMTP connector implementation (NOTIFY-CONN-EMAIL-15-701).
|
• Current: BLOCKED – waiting on base SMTP connector implementation (NOTIFY-CONN-EMAIL-15-701).
|
||||||
- Path: `src/StellaOps.Notify.Connectors.Slack/TASKS.md`
|
- 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`
|
- 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`
|
- 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.
|
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)
|
• 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.
|
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)
|
• Prereqs: SCANNER-ANALYZERS-LANG-10-308R (Wave 5)
|
||||||
• Current: TODO
|
• 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
|
- **Sprint 7** · Contextual Truth Foundations
|
||||||
- Team: Team Normalization & Storage Backbone
|
- Team: Team Normalization & Storage Backbone
|
||||||
- Path: `src/StellaOps.Concelier.Storage.Mongo/TASKS.md`
|
- 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)
|
• 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.
|
• 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
|
## 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.
|
|
||||||
|
|||||||
36
SPRINTS.md
36
SPRINTS.md
@@ -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 | 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:00–16:05 UTC; 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-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-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. |
|
| 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-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-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 | 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 | 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 <5 s SBOM compose. |
|
| Sprint 10 | DevOps Perf | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-PERF-10-001 | Perf smoke job ensuring <5 s 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.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-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-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. |
|
| 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-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-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.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 | 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-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. |
|
| 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 | 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.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 | 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.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/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/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. |
|
| 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-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-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.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-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-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. |
|
| 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
40
SPRINTS_PRIOR_20251021.md
Normal 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:00–16:05 UTC; 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. |
|
||||||
@@ -44,7 +44,26 @@ Excititor:
|
|||||||
vulnId: CVE-2025-0001
|
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 |
|
| Field | Required | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -59,7 +78,7 @@ Export-level fields:
|
|||||||
|
|
||||||
| Field | Required | Description |
|
| 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`. |
|
| `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`). |
|
| `filters` | – | Key/value pairs executed via `VexQueryFilter`. Keys must match export data source columns (e.g., `vulnId`, `productKey`). |
|
||||||
| `sort` | – | Key/boolean map (false = descending). |
|
| `sort` | – | Key/boolean map (false = descending). |
|
||||||
@@ -117,7 +136,14 @@ Recommended workflow:
|
|||||||
* `GET /download` when new
|
* `GET /download` when new
|
||||||
* Verify digest + attestation
|
* 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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-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-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. |
|
| 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. |
|
| 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-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. |
|
| 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. |
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ All event envelopes share the same deterministic header. Use the following table
|
|||||||
| `ts` | `date-time` | RFC 3339 UTC timestamp. Use monotonic clocks or atomic offsets so ordering survives retries. |
|
| `ts` | `date-time` | RFC 3339 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`). |
|
| `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 schema’s `description` block and update the consumer checklist in the next sprint sync.
|
When adding new optional fields, document the behaviour in the schema’s `description` block and update the consumer checklist in the next sprint sync.
|
||||||
|
|
||||||
## Canonical samples & validation
|
## 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:
|
Run the following loop offline to validate both schemas and samples:
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Optional event attributes for downstream correlation.",
|
||||||
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
@@ -78,6 +78,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Optional event attributes for downstream correlation.",
|
||||||
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
@@ -91,6 +91,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Optional event attributes for downstream correlation.",
|
||||||
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
@@ -27,6 +27,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Optional event attributes for downstream correlation.",
|
||||||
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
@@ -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-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-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-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-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.
|
> Remark (2025-10-21): Compose/Helm profiles now surface `SCANNER__EVENTS__*` toggles with docs pointing at new `.env` placeholders.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.|
|
|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-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-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.|
|
||||||
|
|||||||
19
src/StellaOps.Concelier.Core/Unknown/IUnknownStateLedger.cs
Normal file
19
src/StellaOps.Concelier.Core/Unknown/IUnknownStateLedger.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
313
src/StellaOps.Concelier.Core/Unknown/UnknownStateLedger.cs
Normal file
313
src/StellaOps.Concelier.Core/Unknown/UnknownStateLedger.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
73
src/StellaOps.Concelier.Core/Unknown/UnknownStateSnapshot.cs
Normal file
73
src/StellaOps.Concelier.Core/Unknown/UnknownStateSnapshot.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| 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-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. |
|
| 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. |
|
||||||
|
|||||||
110
src/StellaOps.Excititor.Core/MirrorDistributionOptions.cs
Normal file
110
src/StellaOps.Excititor.Core/MirrorDistributionOptions.cs
Normal 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; }
|
||||||
|
}
|
||||||
47
src/StellaOps.Excititor.Core/MirrorExportPlanner.cs
Normal file
47
src/StellaOps.Excititor.Core/MirrorExportPlanner.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -233,10 +233,30 @@ public static class VexCanonicalJsonSerializer
|
|||||||
"policyDigest",
|
"policyDigest",
|
||||||
"consensusDigest",
|
"consensusDigest",
|
||||||
"scoreDigest",
|
"scoreDigest",
|
||||||
|
"quietProvenance",
|
||||||
"attestation",
|
"attestation",
|
||||||
"sizeBytes",
|
"sizeBytes",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
typeof(VexQuietProvenance),
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"vulnerabilityId",
|
||||||
|
"productKey",
|
||||||
|
"statements",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typeof(VexQuietStatement),
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
"providerId",
|
||||||
|
"statementId",
|
||||||
|
"justification",
|
||||||
|
"signature",
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
typeof(VexScoreEnvelope),
|
typeof(VexScoreEnvelope),
|
||||||
new[]
|
new[]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ public sealed record VexExportManifest
|
|||||||
string? policyDigest = null,
|
string? policyDigest = null,
|
||||||
VexContentAddress? consensusDigest = null,
|
VexContentAddress? consensusDigest = null,
|
||||||
VexContentAddress? scoreDigest = null,
|
VexContentAddress? scoreDigest = null,
|
||||||
|
IEnumerable<VexQuietProvenance>? quietProvenance = null,
|
||||||
VexAttestationMetadata? attestation = null,
|
VexAttestationMetadata? attestation = null,
|
||||||
long sizeBytes = 0)
|
long sizeBytes = 0)
|
||||||
{
|
{
|
||||||
@@ -51,6 +53,7 @@ public sealed record VexExportManifest
|
|||||||
PolicyDigest = string.IsNullOrWhiteSpace(policyDigest) ? null : policyDigest.Trim();
|
PolicyDigest = string.IsNullOrWhiteSpace(policyDigest) ? null : policyDigest.Trim();
|
||||||
ConsensusDigest = consensusDigest;
|
ConsensusDigest = consensusDigest;
|
||||||
ScoreDigest = scoreDigest;
|
ScoreDigest = scoreDigest;
|
||||||
|
QuietProvenance = NormalizeQuietProvenance(quietProvenance);
|
||||||
Attestation = attestation;
|
Attestation = attestation;
|
||||||
SizeBytes = sizeBytes;
|
SizeBytes = sizeBytes;
|
||||||
}
|
}
|
||||||
@@ -81,6 +84,8 @@ public sealed record VexExportManifest
|
|||||||
|
|
||||||
public VexContentAddress? ScoreDigest { get; }
|
public VexContentAddress? ScoreDigest { get; }
|
||||||
|
|
||||||
|
public ImmutableArray<VexQuietProvenance> QuietProvenance { get; }
|
||||||
|
|
||||||
public VexAttestationMetadata? Attestation { get; }
|
public VexAttestationMetadata? Attestation { get; }
|
||||||
|
|
||||||
public long SizeBytes { get; }
|
public long SizeBytes { get; }
|
||||||
@@ -107,6 +112,19 @@ public sealed record VexExportManifest
|
|||||||
? ImmutableArray<string>.Empty
|
? ImmutableArray<string>.Empty
|
||||||
: set.ToImmutableArray();
|
: 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
|
public sealed record VexContentAddress
|
||||||
|
|||||||
78
src/StellaOps.Excititor.Core/VexQuietProvenance.cs
Normal file
78
src/StellaOps.Excititor.Core/VexQuietProvenance.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Globalization;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using StellaOps.Excititor.Core;
|
using StellaOps.Excititor.Core;
|
||||||
@@ -32,6 +33,18 @@ public sealed class ExportEngineTests
|
|||||||
Assert.Equal(VexExportFormat.Json, manifest.Format);
|
Assert.Equal(VexExportFormat.Json, manifest.Format);
|
||||||
Assert.Equal("baseline/v1", manifest.ConsensusRevision);
|
Assert.Equal("baseline/v1", manifest.ConsensusRevision);
|
||||||
Assert.Equal(1, manifest.ClaimCount);
|
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
|
// second call hits cache
|
||||||
var cached = await engine.ExportAsync(context, CancellationToken.None);
|
var cached = await engine.ExportAsync(context, CancellationToken.None);
|
||||||
@@ -114,13 +127,82 @@ public sealed class ExportEngineTests
|
|||||||
var manifest = await engine.ExportAsync(context, CancellationToken.None);
|
var manifest = await engine.ExportAsync(context, CancellationToken.None);
|
||||||
|
|
||||||
Assert.NotNull(attestation.LastRequest);
|
Assert.NotNull(attestation.LastRequest);
|
||||||
|
Assert.NotNull(dataSource.LastDataSet);
|
||||||
|
var expectedEnvelopes = VexExportEnvelopeBuilder.Build(
|
||||||
|
dataSource.LastDataSet!,
|
||||||
|
VexPolicySnapshot.Default,
|
||||||
|
requestedAt);
|
||||||
Assert.Equal(manifest.ExportId, attestation.LastRequest!.ExportId);
|
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.NotNull(manifest.Attestation);
|
||||||
Assert.Equal(attestation.Response.Attestation.EnvelopeDigest, manifest.Attestation!.EnvelopeDigest);
|
Assert.Equal(attestation.Response.Attestation.EnvelopeDigest, manifest.Attestation!.EnvelopeDigest);
|
||||||
Assert.Equal(attestation.Response.Attestation.PredicateType, manifest.Attestation.PredicateType);
|
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.NotNull(store.LastSavedManifest);
|
||||||
Assert.Equal(manifest.Attestation, store.LastSavedManifest!.Attestation);
|
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
|
private sealed class InMemoryExportStore : IVexExportStore
|
||||||
@@ -148,6 +230,48 @@ public sealed class ExportEngineTests
|
|||||||
=> FormattableString.Invariant($"{signature}|{format}");
|
=> 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
|
private sealed class RecordingAttestationClient : IVexAttestationClient
|
||||||
{
|
{
|
||||||
public VexAttestationRequest? LastRequest { get; private set; }
|
public VexAttestationRequest? LastRequest { get; private set; }
|
||||||
@@ -226,6 +350,8 @@ public sealed class ExportEngineTests
|
|||||||
|
|
||||||
private sealed class InMemoryExportDataSource : IVexExportDataSource
|
private sealed class InMemoryExportDataSource : IVexExportDataSource
|
||||||
{
|
{
|
||||||
|
public VexExportDataSet? LastDataSet { get; private set; }
|
||||||
|
|
||||||
public ValueTask<VexExportDataSet> FetchAsync(VexQuery query, CancellationToken cancellationToken)
|
public ValueTask<VexExportDataSet> FetchAsync(VexQuery query, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var claim = new VexClaim(
|
var claim = new VexClaim(
|
||||||
@@ -247,10 +373,13 @@ public sealed class ExportEngineTests
|
|||||||
policyVersion: "baseline/v1",
|
policyVersion: "baseline/v1",
|
||||||
summary: "affected");
|
summary: "affected");
|
||||||
|
|
||||||
return ValueTask.FromResult(new VexExportDataSet(
|
var dataSet = new VexExportDataSet(
|
||||||
ImmutableArray.Create(consensus),
|
ImmutableArray.Create(consensus),
|
||||||
ImmutableArray.Create(claim),
|
ImmutableArray.Create(claim),
|
||||||
ImmutableArray.Create("vendor")));
|
ImmutableArray.Create("vendor"));
|
||||||
|
|
||||||
|
LastDataSet = dataSet;
|
||||||
|
return ValueTask.FromResult(dataSet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Globalization;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using StellaOps.Excititor.Core;
|
using StellaOps.Excititor.Core;
|
||||||
@@ -42,6 +43,7 @@ public sealed class VexExportEngine : IExportEngine
|
|||||||
private readonly IVexCacheIndex? _cacheIndex;
|
private readonly IVexCacheIndex? _cacheIndex;
|
||||||
private readonly IReadOnlyList<IVexArtifactStore> _artifactStores;
|
private readonly IReadOnlyList<IVexArtifactStore> _artifactStores;
|
||||||
private readonly IVexAttestationClient? _attestationClient;
|
private readonly IVexAttestationClient? _attestationClient;
|
||||||
|
private readonly IVexMirrorBundlePublisher? _mirrorPublisher;
|
||||||
|
|
||||||
public VexExportEngine(
|
public VexExportEngine(
|
||||||
IVexExportStore exportStore,
|
IVexExportStore exportStore,
|
||||||
@@ -51,7 +53,8 @@ public sealed class VexExportEngine : IExportEngine
|
|||||||
ILogger<VexExportEngine> logger,
|
ILogger<VexExportEngine> logger,
|
||||||
IVexCacheIndex? cacheIndex = null,
|
IVexCacheIndex? cacheIndex = null,
|
||||||
IEnumerable<IVexArtifactStore>? artifactStores = null,
|
IEnumerable<IVexArtifactStore>? artifactStores = null,
|
||||||
IVexAttestationClient? attestationClient = null)
|
IVexAttestationClient? attestationClient = null,
|
||||||
|
IVexMirrorBundlePublisher? mirrorPublisher = null)
|
||||||
{
|
{
|
||||||
_exportStore = exportStore ?? throw new ArgumentNullException(nameof(exportStore));
|
_exportStore = exportStore ?? throw new ArgumentNullException(nameof(exportStore));
|
||||||
_policyEvaluator = policyEvaluator ?? throw new ArgumentNullException(nameof(policyEvaluator));
|
_policyEvaluator = policyEvaluator ?? throw new ArgumentNullException(nameof(policyEvaluator));
|
||||||
@@ -60,6 +63,7 @@ public sealed class VexExportEngine : IExportEngine
|
|||||||
_cacheIndex = cacheIndex;
|
_cacheIndex = cacheIndex;
|
||||||
_artifactStores = artifactStores?.ToArray() ?? Array.Empty<IVexArtifactStore>();
|
_artifactStores = artifactStores?.ToArray() ?? Array.Empty<IVexArtifactStore>();
|
||||||
_attestationClient = attestationClient;
|
_attestationClient = attestationClient;
|
||||||
|
_mirrorPublisher = mirrorPublisher;
|
||||||
|
|
||||||
if (exporters is null)
|
if (exporters is null)
|
||||||
{
|
{
|
||||||
@@ -105,12 +109,13 @@ public sealed class VexExportEngine : IExportEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dataset = await _dataSource.FetchAsync(context.Query, cancellationToken).ConfigureAwait(false);
|
var dataset = await _dataSource.FetchAsync(context.Query, cancellationToken).ConfigureAwait(false);
|
||||||
var exporter = ResolveExporter(context.Format);
|
|
||||||
var policySnapshot = _policyEvaluator.Snapshot;
|
var policySnapshot = _policyEvaluator.Snapshot;
|
||||||
|
var envelopeContext = VexExportEnvelopeBuilder.Build(dataset, policySnapshot, context.RequestedAt);
|
||||||
|
var exporter = ResolveExporter(context.Format);
|
||||||
|
|
||||||
var exportRequest = new VexExportRequest(
|
var exportRequest = new VexExportRequest(
|
||||||
context.Query,
|
context.Query,
|
||||||
dataset.Consensus,
|
envelopeContext.Consensus,
|
||||||
dataset.Claims,
|
dataset.Claims,
|
||||||
context.RequestedAt);
|
context.RequestedAt);
|
||||||
|
|
||||||
@@ -120,6 +125,37 @@ public sealed class VexExportEngine : IExportEngine
|
|||||||
await using var buffer = new MemoryStream();
|
await using var buffer = new MemoryStream();
|
||||||
var result = await exporter.SerializeAsync(exportRequest, buffer, cancellationToken).ConfigureAwait(false);
|
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)
|
if (_artifactStores.Count > 0)
|
||||||
{
|
{
|
||||||
var writtenBytes = buffer.ToArray();
|
var writtenBytes = buffer.ToArray();
|
||||||
@@ -129,7 +165,7 @@ public sealed class VexExportEngine : IExportEngine
|
|||||||
result.Digest,
|
result.Digest,
|
||||||
context.Format,
|
context.Format,
|
||||||
writtenBytes,
|
writtenBytes,
|
||||||
result.Metadata);
|
exportMetadata);
|
||||||
|
|
||||||
foreach (var store in _artifactStores)
|
foreach (var store in _artifactStores)
|
||||||
{
|
{
|
||||||
@@ -155,7 +191,7 @@ public sealed class VexExportEngine : IExportEngine
|
|||||||
context.Format,
|
context.Format,
|
||||||
context.RequestedAt,
|
context.RequestedAt,
|
||||||
dataset.SourceProviders,
|
dataset.SourceProviders,
|
||||||
result.Metadata);
|
exportMetadata);
|
||||||
|
|
||||||
var response = await _attestationClient.SignAsync(attestationRequest, cancellationToken).ConfigureAwait(false);
|
var response = await _attestationClient.SignAsync(attestationRequest, cancellationToken).ConfigureAwait(false);
|
||||||
attestationMetadata = response.Attestation;
|
attestationMetadata = response.Attestation;
|
||||||
@@ -175,8 +211,8 @@ public sealed class VexExportEngine : IExportEngine
|
|||||||
_logger.LogInformation("Attestation generated for export {ExportId}", exportId);
|
_logger.LogInformation("Attestation generated for export {ExportId}", exportId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var consensusDigestAddress = TryGetContentAddress(result.Metadata, "consensusDigest");
|
var consensusDigestAddress = TryGetContentAddress(exportMetadata, "consensusDigest");
|
||||||
var scoreDigestAddress = TryGetContentAddress(result.Metadata, "scoreDigest");
|
var scoreDigestAddress = TryGetContentAddress(exportMetadata, "scoreDigest");
|
||||||
|
|
||||||
var manifest = new VexExportManifest(
|
var manifest = new VexExportManifest(
|
||||||
exportId,
|
exportId,
|
||||||
@@ -192,11 +228,24 @@ public sealed class VexExportEngine : IExportEngine
|
|||||||
policyDigest: policySnapshot.Digest,
|
policyDigest: policySnapshot.Digest,
|
||||||
consensusDigest: consensusDigestAddress,
|
consensusDigest: consensusDigestAddress,
|
||||||
scoreDigest: scoreDigestAddress,
|
scoreDigest: scoreDigestAddress,
|
||||||
|
quietProvenance: envelopeContext.QuietProvenance,
|
||||||
attestation: attestationMetadata,
|
attestation: attestationMetadata,
|
||||||
sizeBytes: result.BytesWritten);
|
sizeBytes: result.BytesWritten);
|
||||||
|
|
||||||
await _exportStore.SaveAsync(manifest, cancellationToken).ConfigureAwait(false);
|
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(
|
_logger.LogInformation(
|
||||||
"Export generated for {Signature} ({Format}) size={SizeBytes} bytes",
|
"Export generated for {Signature} ({Format}) size={SizeBytes} bytes",
|
||||||
signature.Value,
|
signature.Value,
|
||||||
@@ -237,6 +286,7 @@ public static class VexExportServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
public static IServiceCollection AddVexExportEngine(this IServiceCollection services)
|
public static IServiceCollection AddVexExportEngine(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
services.AddSingleton<IVexMirrorBundlePublisher, VexMirrorBundlePublisher>();
|
||||||
services.AddSingleton<IExportEngine, VexExportEngine>();
|
services.AddSingleton<IExportEngine, VexExportEngine>();
|
||||||
services.AddVexExportCacheServices();
|
services.AddVexExportCacheServices();
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@@ -15,5 +15,6 @@
|
|||||||
<ProjectReference Include="..\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
|
<ProjectReference Include="..\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
|
||||||
<ProjectReference Include="..\StellaOps.Excititor.Policy\StellaOps.Excititor.Policy.csproj" />
|
<ProjectReference Include="..\StellaOps.Excititor.Policy\StellaOps.Excititor.Policy.csproj" />
|
||||||
<ProjectReference Include="..\StellaOps.Excititor.Storage.Mongo\StellaOps.Excititor.Storage.Mongo.csproj" />
|
<ProjectReference Include="..\StellaOps.Excititor.Storage.Mongo\StellaOps.Excititor.Storage.Mongo.csproj" />
|
||||||
|
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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-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-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-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-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|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-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|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-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.|
|
||||||
|
|||||||
140
src/StellaOps.Excititor.Export/VexExportEnvelopeBuilder.cs
Normal file
140
src/StellaOps.Excititor.Export/VexExportEnvelopeBuilder.cs
Normal 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);
|
||||||
716
src/StellaOps.Excititor.Export/VexMirrorBundlePublisher.cs
Normal file
716
src/StellaOps.Excititor.Export/VexMirrorBundlePublisher.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -108,6 +108,8 @@ internal sealed class VexExportManifestRecord
|
|||||||
public string? ScoreDigestValue { get; set; }
|
public string? ScoreDigestValue { get; set; }
|
||||||
= null;
|
= null;
|
||||||
|
|
||||||
|
public List<VexQuietProvenanceRecord> QuietProvenance { get; set; } = new();
|
||||||
|
|
||||||
public string? PredicateType { get; set; }
|
public string? PredicateType { get; set; }
|
||||||
= null;
|
= null;
|
||||||
|
|
||||||
@@ -151,6 +153,7 @@ internal sealed class VexExportManifestRecord
|
|||||||
ConsensusDigestValue = manifest.ConsensusDigest?.Digest,
|
ConsensusDigestValue = manifest.ConsensusDigest?.Digest,
|
||||||
ScoreDigestAlgorithm = manifest.ScoreDigest?.Algorithm,
|
ScoreDigestAlgorithm = manifest.ScoreDigest?.Algorithm,
|
||||||
ScoreDigestValue = manifest.ScoreDigest?.Digest,
|
ScoreDigestValue = manifest.ScoreDigest?.Digest,
|
||||||
|
QuietProvenance = manifest.QuietProvenance.Select(VexQuietProvenanceRecord.FromDomain).ToList(),
|
||||||
PredicateType = manifest.Attestation?.PredicateType,
|
PredicateType = manifest.Attestation?.PredicateType,
|
||||||
RekorApiVersion = manifest.Attestation?.Rekor?.ApiVersion,
|
RekorApiVersion = manifest.Attestation?.Rekor?.ApiVersion,
|
||||||
RekorLocation = manifest.Attestation?.Rekor?.Location,
|
RekorLocation = manifest.Attestation?.Rekor?.Location,
|
||||||
@@ -203,6 +206,11 @@ internal sealed class VexExportManifestRecord
|
|||||||
PolicyDigest,
|
PolicyDigest,
|
||||||
consensusDigest,
|
consensusDigest,
|
||||||
scoreDigest,
|
scoreDigest,
|
||||||
|
quietProvenance: QuietProvenance.Count == 0
|
||||||
|
? ImmutableArray<VexQuietProvenance>.Empty
|
||||||
|
: QuietProvenance
|
||||||
|
.Select(static record => record.ToDomain())
|
||||||
|
.ToImmutableArray(),
|
||||||
attestation,
|
attestation,
|
||||||
SizeBytes);
|
SizeBytes);
|
||||||
}
|
}
|
||||||
@@ -211,6 +219,63 @@ internal sealed class VexExportManifestRecord
|
|||||||
=> string.Format(CultureInfo.InvariantCulture, "{0}|{1}", signature.Value, format.ToString().ToLowerInvariant());
|
=> string.Format(CultureInfo.InvariantCulture, "{0}|{1}", signature.Value, format.ToString().ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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]
|
[BsonIgnoreExtraElements]
|
||||||
internal sealed class VexProviderRecord
|
internal sealed class VexProviderRecord
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ using StellaOps.Excititor.Connectors.Abstractions;
|
|||||||
using StellaOps.Excititor.Export;
|
using StellaOps.Excititor.Export;
|
||||||
using StellaOps.Excititor.Policy;
|
using StellaOps.Excititor.Policy;
|
||||||
using StellaOps.Excititor.Storage.Mongo;
|
using StellaOps.Excititor.Storage.Mongo;
|
||||||
using StellaOps.Excititor.WebService.Options;
|
|
||||||
|
|
||||||
namespace StellaOps.Excititor.WebService.Tests;
|
namespace StellaOps.Excititor.WebService.Tests;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ using Microsoft.Extensions.Options;
|
|||||||
using StellaOps.Excititor.Core;
|
using StellaOps.Excititor.Core;
|
||||||
using StellaOps.Excititor.Export;
|
using StellaOps.Excititor.Export;
|
||||||
using StellaOps.Excititor.Storage.Mongo;
|
using StellaOps.Excititor.Storage.Mongo;
|
||||||
using StellaOps.Excititor.WebService.Options;
|
|
||||||
using StellaOps.Excititor.WebService.Services;
|
using StellaOps.Excititor.WebService.Services;
|
||||||
|
|
||||||
namespace StellaOps.Excititor.WebService.Endpoints;
|
namespace StellaOps.Excititor.WebService.Endpoints;
|
||||||
@@ -101,7 +100,7 @@ internal static class MirrorEndpoints
|
|||||||
var resolvedExports = new List<MirrorExportIndexEntry>();
|
var resolvedExports = new List<MirrorExportIndexEntry>();
|
||||||
foreach (var exportOption in domain.Exports)
|
foreach (var exportOption in domain.Exports)
|
||||||
{
|
{
|
||||||
if (!TryBuildExportPlan(exportOption, out var plan, out var error))
|
if (!MirrorExportPlanner.TryBuild(exportOption, out var plan, out var error))
|
||||||
{
|
{
|
||||||
resolvedExports.Add(new MirrorExportIndexEntry(
|
resolvedExports.Add(new MirrorExportIndexEntry(
|
||||||
exportOption.Key,
|
exportOption.Key,
|
||||||
@@ -183,7 +182,7 @@ internal static class MirrorEndpoints
|
|||||||
return Results.NotFound();
|
return Results.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryBuildExportPlan(exportOptions, out var plan, out var error))
|
if (!MirrorExportPlanner.TryBuild(exportOptions, out var plan, out var error))
|
||||||
{
|
{
|
||||||
await WritePlainTextAsync(httpContext, error ?? "invalid_export_configuration", StatusCodes.Status503ServiceUnavailable, cancellationToken).ConfigureAwait(false);
|
await WritePlainTextAsync(httpContext, error ?? "invalid_export_configuration", StatusCodes.Status503ServiceUnavailable, cancellationToken).ConfigureAwait(false);
|
||||||
return Results.Empty;
|
return Results.Empty;
|
||||||
@@ -242,7 +241,7 @@ internal static class MirrorEndpoints
|
|||||||
return Results.Empty;
|
return Results.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryFindExport(domain, exportKey, out var exportOptions) || !TryBuildExportPlan(exportOptions, out var plan, out _))
|
if (!TryFindExport(domain, exportKey, out var exportOptions) || !MirrorExportPlanner.TryBuild(exportOptions, out var plan, out _))
|
||||||
{
|
{
|
||||||
return Results.NotFound();
|
return Results.NotFound();
|
||||||
}
|
}
|
||||||
@@ -293,32 +292,6 @@ internal static class MirrorEndpoints
|
|||||||
return export is not null;
|
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 string ResolveContentType(VexExportFormat format)
|
private static string ResolveContentType(VexExportFormat format)
|
||||||
=> format switch
|
=> format switch
|
||||||
{
|
{
|
||||||
@@ -359,10 +332,6 @@ internal static class MirrorEndpoints
|
|||||||
await context.Response.WriteAsync(json, cancellationToken);
|
await context.Response.WriteAsync(json, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record MirrorExportPlan(
|
|
||||||
VexExportFormat Format,
|
|
||||||
VexQuery Query,
|
|
||||||
VexQuerySignature Signature);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record MirrorDomainListResponse(IReadOnlyList<MirrorDomainSummary> Domains);
|
internal sealed record MirrorDomainListResponse(IReadOnlyList<MirrorDomainSummary> Domains);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,8 @@ using StellaOps.Excititor.Formats.OpenVEX;
|
|||||||
using StellaOps.Excititor.Policy;
|
using StellaOps.Excititor.Policy;
|
||||||
using StellaOps.Excititor.Storage.Mongo;
|
using StellaOps.Excititor.Storage.Mongo;
|
||||||
using StellaOps.Excititor.WebService.Endpoints;
|
using StellaOps.Excititor.WebService.Endpoints;
|
||||||
using StellaOps.Excititor.WebService.Options;
|
|
||||||
using StellaOps.Excititor.WebService.Services;
|
using StellaOps.Excititor.WebService.Services;
|
||||||
|
using StellaOps.Excititor.Core;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
var configuration = builder.Configuration;
|
var configuration = builder.Configuration;
|
||||||
|
|||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,9 +10,13 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="NJsonSchema" Version="10.9.0" />
|
||||||
<None Include="../../docs/events/samples/*.json">
|
<None Include="../../docs/events/samples/*.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Include="../../docs/events/*.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
<None Include="../../docs/notify/samples/*.json">
|
<None Include="../../docs/notify/samples/*.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@
|
|||||||
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<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="coverlet.collector" Version="6.0.4" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.2" />
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# TASKS
|
# TASKS
|
||||||
| Task | Owner(s) | Depends on | Notes |
|
| 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:00–16:05 UTC; 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.COORD Authority scoped-service handshake|Plugin Platform Guild, Authority Core (DONE 2025-10-20)|PLUGIN-DI-08-001|Workshop held 2025-10-20 15:00–16:05 UTC; 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-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`.|
|
|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`.|
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ Run `ng build` to build the project. The build artifacts will be stored in the `
|
|||||||
|
|
||||||
## Running unit tests
|
## Running unit tests
|
||||||
|
|
||||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
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
|
## Running end-to-end tests
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,5 @@
|
|||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| 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 | 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. |
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
"zone.js/testing"
|
"zone.js/testing"
|
||||||
],
|
],
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"karmaConfig": "karma.conf.cjs",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
|
|||||||
49
src/StellaOps.Web/karma.conf.cjs
Normal file
49
src/StellaOps.Web/karma.conf.cjs
Normal 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
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test"
|
"test": "ng test --watch=false",
|
||||||
|
"test:watch": "ng test --watch"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
signal,
|
signal,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
FormBuilder,
|
NonNullableFormBuilder,
|
||||||
FormGroup,
|
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
} from '@angular/forms';
|
} from '@angular/forms';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
@@ -28,19 +27,21 @@ type StatusKind = 'idle' | 'loading' | 'saving' | 'running' | 'success' | 'error
|
|||||||
styleUrls: ['./trivy-db-settings-page.component.scss'],
|
styleUrls: ['./trivy-db-settings-page.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
|
type TrivyDbSettingsFormValue = TrivyDbSettingsDto;
|
||||||
|
|
||||||
export class TrivyDbSettingsPageComponent implements OnInit {
|
export class TrivyDbSettingsPageComponent implements OnInit {
|
||||||
private readonly client = inject(ConcelierExporterClient);
|
private readonly client = inject(ConcelierExporterClient);
|
||||||
private readonly formBuilder = inject(FormBuilder);
|
private readonly formBuilder = inject(NonNullableFormBuilder);
|
||||||
|
|
||||||
readonly status = signal<StatusKind>('idle');
|
readonly status = signal<StatusKind>('idle');
|
||||||
readonly message = signal<string | null>(null);
|
readonly message = signal<string | null>(null);
|
||||||
readonly lastRun = signal<TrivyDbRunResponseDto | null>(null);
|
readonly lastRun = signal<TrivyDbRunResponseDto | null>(null);
|
||||||
|
|
||||||
readonly form: FormGroup = this.formBuilder.group({
|
readonly form = this.formBuilder.group<TrivyDbSettingsFormValue>({
|
||||||
publishFull: [true],
|
publishFull: true,
|
||||||
publishDelta: [true],
|
publishDelta: true,
|
||||||
includeFull: [true],
|
includeFull: true,
|
||||||
includeDelta: [true],
|
includeDelta: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -52,7 +53,7 @@ export class TrivyDbSettingsPageComponent implements OnInit {
|
|||||||
this.message.set(null);
|
this.message.set(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = await firstValueFrom(
|
const settings: TrivyDbSettingsDto = await firstValueFrom(
|
||||||
this.client.getTrivyDbSettings()
|
this.client.getTrivyDbSettings()
|
||||||
);
|
);
|
||||||
this.form.patchValue(settings);
|
this.form.patchValue(settings);
|
||||||
@@ -73,7 +74,7 @@ export class TrivyDbSettingsPageComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = this.buildPayload();
|
const payload = this.buildPayload();
|
||||||
const updated = await firstValueFrom(
|
const updated: TrivyDbSettingsDto = await firstValueFrom(
|
||||||
this.client.updateTrivyDbSettings(payload)
|
this.client.updateTrivyDbSettings(payload)
|
||||||
);
|
);
|
||||||
this.form.patchValue(updated);
|
this.form.patchValue(updated);
|
||||||
@@ -98,7 +99,7 @@ export class TrivyDbSettingsPageComponent implements OnInit {
|
|||||||
|
|
||||||
// Persist overrides before triggering a run, ensuring parity.
|
// Persist overrides before triggering a run, ensuring parity.
|
||||||
await firstValueFrom(this.client.updateTrivyDbSettings(payload));
|
await firstValueFrom(this.client.updateTrivyDbSettings(payload));
|
||||||
const response = await firstValueFrom(
|
const response: TrivyDbRunResponseDto = await firstValueFrom(
|
||||||
this.client.runTrivyDbExport(payload)
|
this.client.runTrivyDbExport(payload)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -124,7 +125,7 @@ export class TrivyDbSettingsPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildPayload(): TrivyDbSettingsDto {
|
private buildPayload(): TrivyDbSettingsDto {
|
||||||
const raw = this.form.getRawValue() as TrivyDbSettingsDto;
|
const raw = this.form.getRawValue();
|
||||||
return {
|
return {
|
||||||
publishFull: !!raw.publishFull,
|
publishFull: !!raw.publishFull,
|
||||||
publishDelta: !!raw.publishDelta,
|
publishDelta: !!raw.publishDelta,
|
||||||
|
|||||||
Reference in New Issue
Block a user