feat(docs): Add comprehensive documentation for Vexer, Vulnerability Explorer, and Zastava modules

- Introduced AGENTS.md, README.md, TASKS.md, and implementation_plan.md for Vexer, detailing mission, responsibilities, key components, and operational notes.
- Established similar documentation structure for Vulnerability Explorer and Zastava modules, including their respective workflows, integrations, and observability notes.
- Created risk scoring profiles documentation outlining the core workflow, factor model, governance, and deliverables.
- Ensured all modules adhere to the Aggregation-Only Contract and maintain determinism and provenance in outputs.
This commit is contained in:
2025-10-30 00:09:39 +02:00
parent 3154c67978
commit 7b5bdcf4d3
503 changed files with 16136 additions and 54638 deletions

View File

@@ -10,4 +10,4 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| ATTEST-TYPES-73-001 | TODO | Attestation Payloads Guild | ATTEST-TYPES-72-002 | Create golden payload samples for each type; integrate into tests and documentation. | Golden fixtures stored; tests compare outputs; docs embed examples. |
| ATTEST-TYPES-73-002 | TODO | Attestation Payloads Guild, Docs Guild | ATTEST-TYPES-73-001 | Publish schema reference docs (`/docs/attestor/payloads.md`) with annotated JSON examples. | Doc merged with banner; examples validated by tests. |
| ATTEST-TYPES-73-002 | TODO | Attestation Payloads Guild, Docs Guild | ATTEST-TYPES-73-001 | Publish schema reference docs (`/docs/modules/attestor/payloads.md`) with annotated JSON examples. | Doc merged with banner; examples validated by tests. |

View File

@@ -10,4 +10,4 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| ATTEST-VERIFY-74-001 | TODO | Verification Guild, Observability Guild | ATTEST-VERIFY-73-001 | Emit telemetry (spans/metrics) tagged by subject, issuer, policy, result; integrate with dashboards. | Metrics visible; spans present; SLO thresholds defined. |
| ATTEST-VERIFY-74-002 | TODO | Verification Guild, Docs Guild | ATTEST-VERIFY-73-001 | Document verification report schema and explainability in `/docs/attestor/workflows.md`. | Documentation merged; examples verified via tests. |
| ATTEST-VERIFY-74-002 | TODO | Verification Guild, Docs Guild | ATTEST-VERIFY-73-001 | Document verification report schema and explainability in `/docs/modules/attestor/workflows.md`. | Documentation merged; examples verified via tests. |

View File

@@ -21,7 +21,7 @@
- Update `TASKS.md` as states change (TODO → DOING → DONE/BLOCKED) and record added tests/fixtures alongside implementation notes.
## Reference Materials
- `docs/ARCHITECTURE_CONCELIER.md` for database operations surface area.
- `docs/modules/concelier/ARCHITECTURE.md` for database operations surface area.
- Backend OpenAPI/contract docs (once available) for job triggers and scanner endpoints.
- Existing module AGENTS/TASKS files for style and coordination cues.
- `docs/09_API_CLI_REFERENCE.md` (section 3) for the user-facing synopsis of the CLI verbs and flags.

View File

@@ -2,7 +2,7 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| CLI-AOC-19-001 | DONE (2025-10-27) | DevEx/CLI Guild | CONCELIER-WEB-AOC-19-001, EXCITITOR-WEB-AOC-19-001 | Implement `stella sources ingest --dry-run` printing would-write payloads with forbidden field scan results and guard status. | Command displays diff-safe JSON, highlights forbidden fields, exits non-zero on guard violation, and has unit tests. |
> Docs ready (2025-10-26): Reference behaviour/spec in `docs/cli/cli-reference.md` §2 and AOC reference §5.
> Docs ready (2025-10-26): Reference behaviour/spec in `docs/modules/cli/guides/cli-reference.md` §2 and AOC reference §5.
> 2025-10-27: CLI command scaffolded with backend client call, JSON/table output, gzip/base64 normalisation, and exit-code mapping. Awaiting Concelier dry-run endpoint + integration tests once backend lands.
> 2025-10-27: Progress paused before adding CLI unit tests; blocked on extending `StubBackendClient` + fixtures for `ExecuteAocIngestDryRunAsync` coverage.
> 2025-10-27: Added stubbed ingest responses + unit tests covering success/violation paths, output writing, and exit-code mapping.
@@ -11,7 +11,7 @@
> 2025-10-27: CLI wiring in progress; backend client/command surface being added with table/JSON output.
> 2025-10-27: Added JSON/table Spectre output, integration tests for exit-code handling, CLI metrics, and updated quickstart/architecture docs to cover guard workflows.
| CLI-AOC-19-003 | DONE (2025-10-27) | Docs/CLI Guild | CLI-AOC-19-001, CLI-AOC-19-002 | Update CLI reference and quickstart docs to cover new commands, exit codes, and offline verification workflows. | Docs updated; examples recorded; release notes mention new commands. |
> Docs note (2025-10-26): `docs/cli/cli-reference.md` now describes both commands, exit codes, and offline usage—sync help text once implementation lands.
> Docs note (2025-10-26): `docs/modules/cli/guides/cli-reference.md` now describes both commands, exit codes, and offline usage—sync help text once implementation lands.
> 2025-10-27: CLI reference now reflects final summary fields/JSON schema, quickstart includes verification/dry-run workflows, and API reference tables list both `sources ingest --dry-run` and `aoc verify`.
## Policy Engine v2

View File

@@ -1,12 +1,12 @@
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|FEEDCONN-CCCS-02-001 Catalogue official CCCS advisory feeds|BE-Conn-CCCS|Research|**DONE (2025-10-11)** Resolved RSS→Atom redirects (`/api/cccs/rss/v1/get?...``/api/cccs/atom/v1/get?...`), confirmed feed caps at 50 entries with inline HTML bodies, no `Last-Modified`/`ETag`, and `updated` timestamps in UTC. Findings and packet captures parked in `docs/concelier-connector-research-20251011.md`; retention sweep follow-up tracked in 02-007.|
|FEEDCONN-CCCS-02-002 Implement fetch & source state handling|BE-Conn-CCCS|Source.Common, Storage.Mongo|**DONE (2025-10-14)** `CccsConnector.FetchAsync` now hydrates feeds via `CccsFeedClient`, persists per-entry JSON payloads with SHA256 dedupe and cursor state, throttles requests, and records taxonomy + language metadata in document state.|
|FEEDCONN-CCCS-02-003 DTO/parser implementation|BE-Conn-CCCS|Source.Common|**DONE (2025-10-14)** Added `CccsHtmlParser` to sanitize Atom body HTML, extract serial/date/product bullets, collapse whitespace, and emit normalized reference URLs; `ParseAsync` now persists DTO records under schema `cccs.dto.v1`.|
|FEEDCONN-CCCS-02-004 Canonical mapping & range primitives|BE-Conn-CCCS|Models|**DONE (2025-10-14)** `CccsMapper` now materializes canonical advisories (aliases from serial/source/CVEs, references incl. canonical URL, vendor package records) with provenance masks; `MapAsync` stores results in `AdvisoryStore`.|
|FEEDCONN-CCCS-02-005 Deterministic fixtures & tests|QA|Testing|**DONE (2025-10-14)** Added English/French fixtures plus parser + connector end-to-end tests (`StellaOps.Concelier.Connector.Cccs.Tests`). Canned HTTP handler + Mongo fixture enables fetch→parse→map regression; fixtures refresh via `UPDATE_CCCS_FIXTURES=1`.|
|FEEDCONN-CCCS-02-006 Observability & documentation|DevEx|Docs|**DONE (2025-10-15)** Added `CccsDiagnostics` meter (fetch/parse/map counters), enriched connector logs with document counts, and published `docs/ops/concelier-cccs-operations.md` covering config, telemetry, and sanitiser guidance.|
|FEEDCONN-CCCS-02-007 Historical advisory harvesting plan|BE-Conn-CCCS|Research|**DONE (2025-10-15)** Measured `/api/cccs/threats/v1/get` inventory (~5.1k rows/lang; earliest 2018-06-08), documented backfill workflow + language split strategy, and linked the runbook for Offline Kit execution.|
|FEEDCONN-CCCS-02-008 Raw DOM parsing refinement|BE-Conn-CCCS|Source.Common|**DONE (2025-10-15)** Parser now walks unsanitised DOM (heading + nested list coverage), sanitizer keeps `<h#>`/`section` nodes, and regression fixtures/tests assert EN/FR list handling + preserved HTML structure.|
|FEEDCONN-CCCS-02-009 Normalized versions rollout (Oct 2025)|BE-Conn-CCCS|Merge coordination (`FEEDMERGE-COORD-02-900`)|**TODO (due 2025-10-21)** Implement trailing-version split helper per Merge guidance (see `../Merge/RANGE_PRIMITIVES_COORDINATION.md` “Helper snippets”) to emit `NormalizedVersions` via `SemVerRangeRuleBuilder`; refresh mapper tests/fixtures to assert provenance notes (`cccs:{serial}:{index}`) and confirm merge counters drop.|
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|FEEDCONN-CCCS-02-001 Catalogue official CCCS advisory feeds|BE-Conn-CCCS|Research|**DONE (2025-10-11)** Resolved RSS→Atom redirects (`/api/cccs/rss/v1/get?...``/api/cccs/atom/v1/get?...`), confirmed feed caps at 50 entries with inline HTML bodies, no `Last-Modified`/`ETag`, and `updated` timestamps in UTC. Findings and packet captures parked in `docs/concelier-connector-research-20251011.md`; retention sweep follow-up tracked in 02-007.|
|FEEDCONN-CCCS-02-002 Implement fetch & source state handling|BE-Conn-CCCS|Source.Common, Storage.Mongo|**DONE (2025-10-14)** `CccsConnector.FetchAsync` now hydrates feeds via `CccsFeedClient`, persists per-entry JSON payloads with SHA256 dedupe and cursor state, throttles requests, and records taxonomy + language metadata in document state.|
|FEEDCONN-CCCS-02-003 DTO/parser implementation|BE-Conn-CCCS|Source.Common|**DONE (2025-10-14)** Added `CccsHtmlParser` to sanitize Atom body HTML, extract serial/date/product bullets, collapse whitespace, and emit normalized reference URLs; `ParseAsync` now persists DTO records under schema `cccs.dto.v1`.|
|FEEDCONN-CCCS-02-004 Canonical mapping & range primitives|BE-Conn-CCCS|Models|**DONE (2025-10-14)** `CccsMapper` now materializes canonical advisories (aliases from serial/source/CVEs, references incl. canonical URL, vendor package records) with provenance masks; `MapAsync` stores results in `AdvisoryStore`.|
|FEEDCONN-CCCS-02-005 Deterministic fixtures & tests|QA|Testing|**DONE (2025-10-14)** Added English/French fixtures plus parser + connector end-to-end tests (`StellaOps.Concelier.Connector.Cccs.Tests`). Canned HTTP handler + Mongo fixture enables fetch→parse→map regression; fixtures refresh via `UPDATE_CCCS_FIXTURES=1`.|
|FEEDCONN-CCCS-02-006 Observability & documentation|DevEx|Docs|**DONE (2025-10-15)** Added `CccsDiagnostics` meter (fetch/parse/map counters), enriched connector logs with document counts, and published `docs/modules/concelier/operations/connectors/cccs.md` covering config, telemetry, and sanitiser guidance.|
|FEEDCONN-CCCS-02-007 Historical advisory harvesting plan|BE-Conn-CCCS|Research|**DONE (2025-10-15)** Measured `/api/cccs/threats/v1/get` inventory (~5.1k rows/lang; earliest 2018-06-08), documented backfill workflow + language split strategy, and linked the runbook for Offline Kit execution.|
|FEEDCONN-CCCS-02-008 Raw DOM parsing refinement|BE-Conn-CCCS|Source.Common|**DONE (2025-10-15)** Parser now walks unsanitised DOM (heading + nested list coverage), sanitizer keeps `<h#>`/`section` nodes, and regression fixtures/tests assert EN/FR list handling + preserved HTML structure.|
|FEEDCONN-CCCS-02-009 Normalized versions rollout (Oct 2025)|BE-Conn-CCCS|Merge coordination (`FEEDMERGE-COORD-02-900`)|**TODO (due 2025-10-21)** Implement trailing-version split helper per Merge guidance (see `../Merge/RANGE_PRIMITIVES_COORDINATION.md` “Helper snippets”) to emit `NormalizedVersions` via `SemVerRangeRuleBuilder`; refresh mapper tests/fixtures to assert provenance notes (`cccs:{serial}:{index}`) and confirm merge counters drop.|

View File

@@ -1,13 +1,13 @@
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|FEEDCONN-CERTBUND-02-001 Research CERT-Bund advisory endpoints|BE-Conn-CERTBUND|Research|**DONE (2025-10-11)** Confirmed public RSS at `https://wid.cert-bund.de/content/public/securityAdvisory/rss` (HTTP200 w/out cookies), 250-item window, German titles/categories, and detail links pointing to Angular SPA. Captured header profile (no cache hints) and logged open item to discover the JSON API used by `portal` frontend.|
|FEEDCONN-CERTBUND-02-002 Fetch job & state persistence|BE-Conn-CERTBUND|Source.Common, Storage.Mongo|**DONE (2025-10-14)** `CertBundConnector.FetchAsync` consumes RSS via session-bootstrapped client, stores per-advisory JSON documents with metadata + SHA, throttles detail requests, and maintains cursor state (pending docs/mappings, known advisory IDs, last published).|
|FEEDCONN-CERTBUND-02-003 Parser/DTO implementation|BE-Conn-CERTBUND|Source.Common|**DONE (2025-10-14)** Detail JSON piped through `CertBundDetailParser` (raw DOM sanitised to HTML), capturing severity, CVEs, product list, and references into DTO records (`cert-bund.detail.v1`).|
|FEEDCONN-CERTBUND-02-004 Canonical mapping & range primitives|BE-Conn-CERTBUND|Models|**DONE (2025-10-14)** `CertBundMapper` emits canonical advisories (aliases, references, vendor package ranges, provenance) with severity normalisation and deterministic ordering.|
|FEEDCONN-CERTBUND-02-005 Regression fixtures & tests|QA|Testing|**DONE (2025-10-14)** Added `StellaOps.Concelier.Connector.CertBund.Tests` covering fetch→parse→map against canned RSS/JSON fixtures; integration harness uses Mongo2Go + canned HTTP handler; fixtures regenerate via `UPDATE_CERTBUND_FIXTURES=1`.|
|FEEDCONN-CERTBUND-02-006 Telemetry & documentation|DevEx|Docs|**DONE (2025-10-15)** Added `CertBundDiagnostics` (meter `StellaOps.Concelier.Connector.CertBund`) with fetch/parse/map counters + histograms, recorded coverage days, wired stage summary logs, and published the ops runbook (`docs/ops/concelier-certbund-operations.md`).|
|FEEDCONN-CERTBUND-02-007 Feed history & locale assessment|BE-Conn-CERTBUND|Research|**DONE (2025-10-15)** Measured RSS retention (~6days/≈250 items), captured connector-driven backfill guidance in the runbook, and aligned locale guidance (preserve `language=de`, Docs glossary follow-up). **Next:** coordinate with Tools to land the state-seeding helper so scripted backfills replace manual Mongo tweaks.|
|FEEDCONN-CERTBUND-02-008 Session bootstrap & cookie strategy|BE-Conn-CERTBUND|Source.Common|**DONE (2025-10-14)** Feed client primes the portal session (cookie container via `SocketsHttpHandler`), shares cookies across detail requests, and documents bootstrap behaviour in options (`PortalBootstrapUri`).|
|FEEDCONN-CERTBUND-02-009 Offline Kit export packaging|BE-Conn-CERTBUND, Docs|Offline Kit|**DONE (2025-10-17)** Added `tools/certbund_offline_snapshot.py` to capture search/export JSON, emit deterministic manifests + SHA files, and refreshed docs (`docs/ops/concelier-certbund-operations.md`, `docs/24_OFFLINE_KIT.md`) with offline-kit instructions and manifest layout guidance. Seed data README/ignore rules cover local snapshot hygiene.|
|FEEDCONN-CERTBUND-02-010 Normalized range translator|BE-Conn-CERTBUND|Merge coordination (`FEEDMERGE-COORD-02-900`)|**TODO (due 2025-10-22)** Translate `product.Versions` phrases (e.g., `2023.1 bis 2024.2`, `alle`) into comparator strings for `SemVerRangeRuleBuilder`, emit `NormalizedVersions` with `certbund:{advisoryId}:{vendor}` provenance, and extend tests/README with localisation notes.|
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|FEEDCONN-CERTBUND-02-001 Research CERT-Bund advisory endpoints|BE-Conn-CERTBUND|Research|**DONE (2025-10-11)** Confirmed public RSS at `https://wid.cert-bund.de/content/public/securityAdvisory/rss` (HTTP200 w/out cookies), 250-item window, German titles/categories, and detail links pointing to Angular SPA. Captured header profile (no cache hints) and logged open item to discover the JSON API used by `portal` frontend.|
|FEEDCONN-CERTBUND-02-002 Fetch job & state persistence|BE-Conn-CERTBUND|Source.Common, Storage.Mongo|**DONE (2025-10-14)** `CertBundConnector.FetchAsync` consumes RSS via session-bootstrapped client, stores per-advisory JSON documents with metadata + SHA, throttles detail requests, and maintains cursor state (pending docs/mappings, known advisory IDs, last published).|
|FEEDCONN-CERTBUND-02-003 Parser/DTO implementation|BE-Conn-CERTBUND|Source.Common|**DONE (2025-10-14)** Detail JSON piped through `CertBundDetailParser` (raw DOM sanitised to HTML), capturing severity, CVEs, product list, and references into DTO records (`cert-bund.detail.v1`).|
|FEEDCONN-CERTBUND-02-004 Canonical mapping & range primitives|BE-Conn-CERTBUND|Models|**DONE (2025-10-14)** `CertBundMapper` emits canonical advisories (aliases, references, vendor package ranges, provenance) with severity normalisation and deterministic ordering.|
|FEEDCONN-CERTBUND-02-005 Regression fixtures & tests|QA|Testing|**DONE (2025-10-14)** Added `StellaOps.Concelier.Connector.CertBund.Tests` covering fetch→parse→map against canned RSS/JSON fixtures; integration harness uses Mongo2Go + canned HTTP handler; fixtures regenerate via `UPDATE_CERTBUND_FIXTURES=1`.|
|FEEDCONN-CERTBUND-02-006 Telemetry & documentation|DevEx|Docs|**DONE (2025-10-15)** Added `CertBundDiagnostics` (meter `StellaOps.Concelier.Connector.CertBund`) with fetch/parse/map counters + histograms, recorded coverage days, wired stage summary logs, and published the ops runbook (`docs/modules/concelier/operations/connectors/certbund.md`).|
|FEEDCONN-CERTBUND-02-007 Feed history & locale assessment|BE-Conn-CERTBUND|Research|**DONE (2025-10-15)** Measured RSS retention (~6days/≈250 items), captured connector-driven backfill guidance in the runbook, and aligned locale guidance (preserve `language=de`, Docs glossary follow-up). **Next:** coordinate with Tools to land the state-seeding helper so scripted backfills replace manual Mongo tweaks.|
|FEEDCONN-CERTBUND-02-008 Session bootstrap & cookie strategy|BE-Conn-CERTBUND|Source.Common|**DONE (2025-10-14)** Feed client primes the portal session (cookie container via `SocketsHttpHandler`), shares cookies across detail requests, and documents bootstrap behaviour in options (`PortalBootstrapUri`).|
|FEEDCONN-CERTBUND-02-009 Offline Kit export packaging|BE-Conn-CERTBUND, Docs|Offline Kit|**DONE (2025-10-17)** Added `src/Tools/certbund_offline_snapshot.py` to capture search/export JSON, emit deterministic manifests + SHA files, and refreshed docs (`docs/modules/concelier/operations/connectors/certbund.md`, `docs/24_OFFLINE_KIT.md`) with offline-kit instructions and manifest layout guidance. Seed data README/ignore rules cover local snapshot hygiene.|
|FEEDCONN-CERTBUND-02-010 Normalized range translator|BE-Conn-CERTBUND|Merge coordination (`FEEDMERGE-COORD-02-900`)|**TODO (due 2025-10-22)** Translate `product.Versions` phrases (e.g., `2023.1 bis 2024.2`, `alle`) into comparator strings for `SemVerRangeRuleBuilder`, emit `NormalizedVersions` with `certbund:{advisoryId}:{vendor}` provenance, and extend tests/README with localisation notes.|

View File

@@ -34,7 +34,7 @@
- Dashboards: `certcc.*` meters (plan, summary fetch, detail fetch) plus `concelier.range.primitives` with tag `scheme=certcc.vendor`.
- Logs: ensure Parse/Map jobs emit `correlationId` aligned with fetch events for traceability.
- Data QA: run `tools/dump_advisory` against two VINCE notes (one multi-vendor, one single-vendor) every phase to spot-check normalized versions ordering and provenance.
- Data QA: run `src/Tools/dump_advisory` against two VINCE notes (one multi-vendor, one single-vendor) every phase to spot-check normalized versions ordering and provenance.
- Storage: verify Mongo TTL/size for `raw_documents` and `dtos`—detail payload volume increases by ~3× when mapping resumes.
## 5. Rollback / Contingency Playbook

View File

@@ -7,6 +7,6 @@
|Canonical mapping & range primitives|BE-Conn-CVE|Models|**DONE (2025-10-10)** `CveMapper` emits canonical advisories, vendor range primitives, SemVer/range statuses, references, CVSS normalization.<br>2025-10-11 research trail: confirm subsequent MR adds `NormalizedVersions` shaped like `[{"scheme":"semver","type":"range","min":"<min>","minInclusive":true,"max":"<max>","maxInclusive":false,"notes":"nvd:CVE-2025-XXXX"}]` so storage provenance joins continue to work.|
|Deterministic tests & fixtures|QA|Testing|**DONE (2025-10-10)** Added `StellaOps.Concelier.Connector.Cve.Tests` harness with canned fixtures + snapshot regression covering fetch/parse/map.|
|Observability & docs|DevEx|Docs|**DONE (2025-10-10)** Diagnostics meter (`cve.fetch.*`, etc.) wired; options/usage documented via `CveServiceCollectionExtensions`.|
|Operator rollout playbook|BE-Conn-CVE, Ops|Docs|**DONE (2025-10-12)** Refreshed `docs/ops/concelier-cve-kev-operations.md` with credential checklist, smoke book, PromQL guardrails, and linked Grafana pack (`docs/ops/concelier-cve-kev-grafana-dashboard.json`).|
|Live smoke & monitoring|QA, BE-Conn-CVE|WebService, Observability|**DONE (2025-10-15)** Executed connector harness smoke using CVE Services sample window (CVE-2024-0001), confirmed fetch/parse/map telemetry (`cve.fetch.*`, `cve.map.success`) all incremented once, and archived the summary log + Grafana import guidance in `docs/ops/concelier-cve-kev-operations.md` (“Staging smoke 2025-10-15”).|
|Operator rollout playbook|BE-Conn-CVE, Ops|Docs|**DONE (2025-10-12)** Refreshed `docs/modules/concelier/operations/connectors/cve-kev.md` with credential checklist, smoke book, PromQL guardrails, and linked Grafana pack (`docs/modules/concelier/operations/connectors/cve-kev-grafana-dashboard.json`).|
|Live smoke & monitoring|QA, BE-Conn-CVE|WebService, Observability|**DONE (2025-10-15)** Executed connector harness smoke using CVE Services sample window (CVE-2024-0001), confirmed fetch/parse/map telemetry (`cve.fetch.*`, `cve.map.success`) all incremented once, and archived the summary log + Grafana import guidance in `docs/modules/concelier/operations/connectors/cve-kev.md` (“Staging smoke 2025-10-15”).|
|FEEDCONN-CVE-02-003 Normalized versions rollout|BE-Conn-CVE|Models `FEEDMODELS-SCHEMA-01-003`, Normalization playbook|**DONE (2025-10-12)** Confirmed SemVer primitives map to normalized rules with `cve:{cveId}:{identifier}` notes and refreshed snapshots; `dotnet test src/Concelier/StellaOps.Concelier.PluginBinaries/StellaOps.Concelier.Connector.Cve.Tests` passes on net10 preview.|

View File

@@ -8,10 +8,10 @@
|Deterministic fixtures & tests|QA|Testing|**DONE (2025-10-10)** New `StellaOps.Concelier.Connector.Ghsa.Tests` regression covers fetch/parse/map via canned GHSA fixtures and snapshot assertions.|
|Telemetry & documentation|DevEx|Docs|**DONE (2025-10-10)** Diagnostics meter (`ghsa.fetch.*`) wired; DI extension documents token/headers and job registrations.|
|GitHub quota monitoring & retries|BE-Conn-GHSA, Observability|Source.Common|**DONE (2025-10-12)** Rate-limit metrics/logs added, retry/backoff handles 403 secondary limits, and ops runbook documents dashboards + mitigation steps.|
|Production credential & scheduler rollout|Ops, BE-Conn-GHSA|Docs, WebService|**DONE (2025-10-12)** Scheduler defaults registered via `JobSchedulerBuilder`, credential provisioning documented (Compose/Helm samples), and staged backfill guidance captured in `docs/ops/concelier-ghsa-operations.md`.|
|Production credential & scheduler rollout|Ops, BE-Conn-GHSA|Docs, WebService|**DONE (2025-10-12)** Scheduler defaults registered via `JobSchedulerBuilder`, credential provisioning documented (Compose/Helm samples), and staged backfill guidance captured in `docs/modules/concelier/operations/connectors/ghsa.md`.|
|FEEDCONN-GHSA-04-002 Conflict regression fixtures|BE-Conn-GHSA, QA|Merge `FEEDMERGE-ENGINE-04-001`|**DONE (2025-10-12)** Added `conflict-ghsa.canonical.json` + `GhsaConflictFixtureTests`; SemVer ranges and credits align with merge precedence triple and shareable with QA. Validation: `dotnet test src/Concelier/StellaOps.Concelier.PluginBinaries/StellaOps.Concelier.Connector.Ghsa.Tests/StellaOps.Concelier.Connector.Ghsa.Tests.csproj --filter GhsaConflictFixtureTests`.|
|FEEDCONN-GHSA-02-004 GHSA credits & ecosystem severity mapping|BE-Conn-GHSA|Models `FEEDMODELS-SCHEMA-01-002`|**DONE (2025-10-11)** Mapper emits advisory credits with provenance masks, fixtures assert role/contact ordering, and severity normalization remains unchanged.|
|FEEDCONN-GHSA-02-007 Credit parity regression fixtures|BE-Conn-GHSA, QA|Source.Nvd, Source.Osv|**DONE (2025-10-12)** Parity fixtures regenerated via `tools/FixtureUpdater`, normalized SemVer notes verified against GHSA/NVD/OSV snapshots, and the fixtures guide now documents the headroom checks.|
|FEEDCONN-GHSA-02-007 Credit parity regression fixtures|BE-Conn-GHSA, QA|Source.Nvd, Source.Osv|**DONE (2025-10-12)** Parity fixtures regenerated via `src/Tools/FixtureUpdater`, normalized SemVer notes verified against GHSA/NVD/OSV snapshots, and the fixtures guide now documents the headroom checks.|
|FEEDCONN-GHSA-02-001 Normalized versions rollout|BE-Conn-GHSA|Models `FEEDMODELS-SCHEMA-01-003`, Normalization playbook|**DONE (2025-10-11)** GHSA mapper now emits SemVer primitives + normalized ranges, fixtures refreshed, connector tests passing; report logged via FEEDMERGE-COORD-02-900.|
|FEEDCONN-GHSA-02-005 Quota monitoring hardening|BE-Conn-GHSA, Observability|Source.Common metrics|**DONE (2025-10-12)** Diagnostics expose headroom histograms/gauges, warning logs dedupe below the configured threshold, and the ops runbook gained alerting and mitigation guidance.|
|FEEDCONN-GHSA-02-006 Scheduler rollout integration|BE-Conn-GHSA, Ops|Job scheduler|**DONE (2025-10-12)** Dependency routine tests assert cron/timeouts, and the runbook highlights cron overrides plus backoff toggles for staged rollouts.|

View File

@@ -9,7 +9,7 @@
|FEEDCONN-ICSCISA-02-006 Telemetry & documentation|DevEx|Docs|**DONE (2025-10-16)** Ops guide documents attachment checks, SemVer exact values, and proxy guidance; diagnostics remain unchanged.|
|FEEDCONN-ICSCISA-02-007 Detail document inventory|BE-Conn-ICS-CISA|Research|**DONE (2025-10-16)** Validated canned detail pages vs feed output so attachment inventories stay aligned; archived expectations noted in `HANDOVER.md`.|
|FEEDCONN-ICSCISA-02-008 Distribution fallback strategy|BE-Conn-ICS-CISA|Research|**DONE (2025-10-11)** Outlined GovDelivery token request, HTML scrape + email digest fallback, and dependency on Ops for credential workflow; awaiting decision before fetch implementation.|
|FEEDCONN-ICSCISA-02-009 GovDelivery credential onboarding|Ops, BE-Conn-ICS-CISA|Ops|**DONE (2025-10-14)** GovDelivery onboarding runbook captured in `docs/ops/concelier-icscisa-operations.md`; secret vault path and Offline Kit handling documented.|
|FEEDCONN-ICSCISA-02-009 GovDelivery credential onboarding|Ops, BE-Conn-ICS-CISA|Ops|**DONE (2025-10-14)** GovDelivery onboarding runbook captured in `docs/modules/concelier/operations/connectors/ics-cisa.md`; secret vault path and Offline Kit handling documented.|
|FEEDCONN-ICSCISA-02-010 Mitigation & SemVer polish|BE-Conn-ICS-CISA|02-003, 02-004|**DONE (2025-10-16)** Attachment + mitigation references now land as expected and SemVer primitives carry exact values; end-to-end suite green (see `HANDOVER.md`).|
|FEEDCONN-ICSCISA-02-011 Docs & telemetry refresh|DevEx|02-006|**DONE (2025-10-16)** Ops documentation refreshed (attachments, SemVer validation, proxy knobs) and telemetry notes verified.|
|FEEDCONN-ICSCISA-02-012 Normalized version decision|BE-Conn-ICS-CISA|Merge coordination (`FEEDMERGE-COORD-02-900`)|**TODO (due 2025-10-23)** Promote existing `SemVerPrimitive` exact values into `NormalizedVersions` via `.ToNormalizedVersionRule("ics-cisa:{advisoryId}:{product}")`, add regression coverage, and open Models ticket if non-SemVer firmware requires a new scheme.|

View File

@@ -8,5 +8,5 @@
|Deterministic fixtures/tests|QA|Testing|**DONE** End-to-end fetch→parse→map test with canned catalog + snapshot (`UPDATE_KEV_FIXTURES=1`) guards determinism.|
|Telemetry & docs|DevEx|Docs|**DONE** Connector emits structured logs + meters for catalog entries/advisories and AGENTS docs cover cadence/allowlist guidance.|
|Schema validation & anomaly surfacing|BE-Conn-KEV, QA|Source.Common|**DONE (2025-10-12)** Wired `IJsonSchemaValidator` + embedded schema, added failure reasons (`schema`, `download`, `invalidJson`, etc.), anomaly counters (`missingCveId`, `countMismatch`, `nullEntry`), and kept `dotnet test src/Concelier/StellaOps.Concelier.PluginBinaries/StellaOps.Concelier.Connector.Kev.Tests` passing.|
|Metrics export wiring|DevOps, DevEx|Observability|**DONE (2025-10-12)** Added `kev.fetch.*` counters, parse failure/anomaly tags, refreshed ops runbook + Grafana dashboard (`docs/ops/concelier-cve-kev-grafana-dashboard.json`) with PromQL guidance.|
|Metrics export wiring|DevOps, DevEx|Observability|**DONE (2025-10-12)** Added `kev.fetch.*` counters, parse failure/anomaly tags, refreshed ops runbook + Grafana dashboard (`docs/modules/concelier/operations/connectors/cve-kev-grafana-dashboard.json`) with PromQL guidance.|
|FEEDCONN-KEV-02-003 Normalized versions propagation|BE-Conn-KEV|Models `FEEDMODELS-SCHEMA-01-003`, Normalization playbook|**DONE (2025-10-12)** Validated catalog/date/due normalized rules emission + ordering; fixtures assert rule set and `dotnet test src/Concelier/StellaOps.Concelier.PluginBinaries/StellaOps.Concelier.Connector.Kev.Tests` remains green.|

View File

@@ -17,4 +17,4 @@
|FEEDCONN-OSV-04-003 Parity fixture refresh|QA, BE-Conn-OSV|Normalized versions rollout, GHSA parity tests|**DONE (2025-10-12)** Parity fixtures include normalizedVersions notes (`osv:<ecosystem>:<id>:<purl>`); regression math rerun via `dotnet test src/Concelier/StellaOps.Concelier.PluginBinaries/StellaOps.Concelier.Connector.Osv.Tests` and docs flagged for workflow sync.|
|FEEDCONN-OSV-04-002 Conflict regression fixtures|BE-Conn-OSV, QA|Merge `FEEDMERGE-ENGINE-04-001`|**DONE (2025-10-12)** Added `conflict-osv.canonical.json` + regression asserting SemVer range + CVSS medium severity; dataset matches GHSA/NVD fixtures for merge tests. Validation: `dotnet test src/Concelier/StellaOps.Concelier.PluginBinaries/StellaOps.Concelier.Connector.Osv.Tests/StellaOps.Concelier.Connector.Osv.Tests.csproj --filter OsvConflictFixtureTests`.|
|FEEDCONN-OSV-04-004 Description/CWE/metric parity rollout|BE-Conn-OSV|Models, Core|**DONE (2025-10-15)** OSV mapper writes advisory descriptions, `database_specific.cwe_ids` weaknesses, and canonical CVSS metric id. Parity fixtures (`osv-ghsa.*`, `osv-npm.snapshot.json`, `osv-pypi.snapshot.json`) refreshed and status communicated to Merge coordination.|
|FEEDCONN-OSV-04-005 Canonical metric fallbacks & CWE notes|BE-Conn-OSV|Models, Merge|**DONE (2025-10-16)** Add fallback logic and metrics for advisories lacking CVSS vectors, enrich CWE provenance notes, and document merge/export expectations; refresh parity fixtures accordingly.<br>2025-10-16: Mapper now emits `osv:severity/<level>` canonical ids for severity-only advisories, weakness provenance carries `database_specific.cwe_ids`, diagnostics expose `osv.map.canonical_metric_fallbacks`, parity fixtures regenerated, and ops notes added in `docs/ops/concelier-osv-operations.md`. Tests: `dotnet test src/Concelier/StellaOps.Concelier.PluginBinaries/StellaOps.Concelier.Connector.Osv.Tests/StellaOps.Concelier.Connector.Osv.Tests.csproj`.|
|FEEDCONN-OSV-04-005 Canonical metric fallbacks & CWE notes|BE-Conn-OSV|Models, Merge|**DONE (2025-10-16)** Add fallback logic and metrics for advisories lacking CVSS vectors, enrich CWE provenance notes, and document merge/export expectations; refresh parity fixtures accordingly.<br>2025-10-16: Mapper now emits `osv:severity/<level>` canonical ids for severity-only advisories, weakness provenance carries `database_specific.cwe_ids`, diagnostics expose `osv.map.canonical_metric_fallbacks`, parity fixtures regenerated, and ops notes added in `docs/modules/concelier/operations/connectors/osv.md`. Tests: `dotnet test src/Concelier/StellaOps.Concelier.PluginBinaries/StellaOps.Concelier.Connector.Osv.Tests/StellaOps.Concelier.Connector.Osv.Tests.csproj`.|

View File

@@ -1,11 +1,11 @@
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|FEEDCONN-NKCKI-02-001 Research NKTsKI advisory feeds|BE-Conn-Nkcki|Research|**DONE (2025-10-11)** Candidate RSS locations (`https://cert.gov.ru/rss/advisories.xml`, `https://www.cert.gov.ru/...`) return 403/404 even with `Accept-Language: ru-RU` and `--insecure`; site is Bitrix-backed and expects Russian Trusted Sub CA plus session cookies. Logged packet captures + needed cert list in `docs/concelier-connector-research-20251011.md`; waiting on Ops for sanctioned trust bundle.|
|FEEDCONN-NKCKI-02-002 Fetch pipeline & state persistence|BE-Conn-Nkcki|Source.Common, Storage.Mongo|**DONE (2025-10-13)** Listing fetch now honours `maxListingPagesPerFetch`, persists cache hits when listing access fails, and records telemetry via `RuNkckiDiagnostics`. Cursor tracking covers pending documents/mappings and the known bulletin ring buffer.|
|FEEDCONN-NKCKI-02-003 DTO & parser implementation|BE-Conn-Nkcki|Source.Common|**DONE (2025-10-13)** Parser normalises nested arrays (ICS categories, vulnerable software lists, optional tags), flattens multiline `software_text`, and guarantees deterministic ordering for URLs and tags.|
|FEEDCONN-NKCKI-02-004 Canonical mapping & range primitives|BE-Conn-Nkcki|Models|**DONE (2025-10-13)** Mapper splits structured software entries, emits SemVer range primitives + normalized rules, deduplicates references, and surfaces CVSS v4 metadata alongside existing metrics.|
|FEEDCONN-NKCKI-02-005 Deterministic fixtures & tests|QA|Testing|**DONE (2025-10-13)** Fixtures refreshed with multi-page pagination + multi-entry bulletins. Tests exercise cache replay and rely on bundled OpenSSL 1.1 libs in `tools/openssl/linux-x64` to keep Mongo2Go green on modern distros.|
|FEEDCONN-NKCKI-02-006 Telemetry & documentation|DevEx|Docs|**DONE (2025-10-13)** Added connector-specific metrics (`nkcki.*`) and documented configuration/operational guidance in `docs/ops/concelier-nkcki-operations.md`.|
|FEEDCONN-NKCKI-02-007 Archive ingestion strategy|BE-Conn-Nkcki|Research|**DONE (2025-10-13)** Documented Bitrix pagination/backfill plan (cache-first, offline replay, HTML/PDF capture) in `docs/ops/concelier-nkcki-operations.md`.|
|FEEDCONN-NKCKI-02-008 Access enablement plan|BE-Conn-Nkcki|Source.Common|**DONE (2025-10-11)** Documented trust-store requirement, optional SOCKS proxy fallback, and monitoring plan; shared TLS support now available via `SourceHttpClientOptions.TrustedRootCertificates` (`concelier:httpClients:source.nkcki:*`), awaiting Ops-sourced cert bundle before fetch implementation.|
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|FEEDCONN-NKCKI-02-001 Research NKTsKI advisory feeds|BE-Conn-Nkcki|Research|**DONE (2025-10-11)** Candidate RSS locations (`https://cert.gov.ru/rss/advisories.xml`, `https://www.cert.gov.ru/...`) return 403/404 even with `Accept-Language: ru-RU` and `--insecure`; site is Bitrix-backed and expects Russian Trusted Sub CA plus session cookies. Logged packet captures + needed cert list in `docs/concelier-connector-research-20251011.md`; waiting on Ops for sanctioned trust bundle.|
|FEEDCONN-NKCKI-02-002 Fetch pipeline & state persistence|BE-Conn-Nkcki|Source.Common, Storage.Mongo|**DONE (2025-10-13)** Listing fetch now honours `maxListingPagesPerFetch`, persists cache hits when listing access fails, and records telemetry via `RuNkckiDiagnostics`. Cursor tracking covers pending documents/mappings and the known bulletin ring buffer.|
|FEEDCONN-NKCKI-02-003 DTO & parser implementation|BE-Conn-Nkcki|Source.Common|**DONE (2025-10-13)** Parser normalises nested arrays (ICS categories, vulnerable software lists, optional tags), flattens multiline `software_text`, and guarantees deterministic ordering for URLs and tags.|
|FEEDCONN-NKCKI-02-004 Canonical mapping & range primitives|BE-Conn-Nkcki|Models|**DONE (2025-10-13)** Mapper splits structured software entries, emits SemVer range primitives + normalized rules, deduplicates references, and surfaces CVSS v4 metadata alongside existing metrics.|
|FEEDCONN-NKCKI-02-005 Deterministic fixtures & tests|QA|Testing|**DONE (2025-10-13)** Fixtures refreshed with multi-page pagination + multi-entry bulletins. Tests exercise cache replay and rely on bundled OpenSSL 1.1 libs in `src/Tools/openssl/linux-x64` to keep Mongo2Go green on modern distros.|
|FEEDCONN-NKCKI-02-006 Telemetry & documentation|DevEx|Docs|**DONE (2025-10-13)** Added connector-specific metrics (`nkcki.*`) and documented configuration/operational guidance in `docs/modules/concelier/operations/connectors/nkcki.md`.|
|FEEDCONN-NKCKI-02-007 Archive ingestion strategy|BE-Conn-Nkcki|Research|**DONE (2025-10-13)** Documented Bitrix pagination/backfill plan (cache-first, offline replay, HTML/PDF capture) in `docs/modules/concelier/operations/connectors/nkcki.md`.|
|FEEDCONN-NKCKI-02-008 Access enablement plan|BE-Conn-Nkcki|Source.Common|**DONE (2025-10-11)** Documented trust-store requirement, optional SOCKS proxy fallback, and monitoring plan; shared TLS support now available via `SourceHttpClientOptions.TrustedRootCertificates` (`concelier:httpClients:source.nkcki:*`), awaiting Ops-sourced cert bundle before fetch implementation.|

View File

@@ -1,11 +1,11 @@
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|Catalogue Apple security bulletin sources|BE-Conn-Apple|Research|**DONE** Feed contract documented in README (Software Lookup Service JSON + HT article hub) with rate-limit notes.|
|Fetch pipeline & state persistence|BE-Conn-Apple|Source.Common, Storage.Mongo|**DONE** Index fetch + detail ingestion with SourceState cursoring/allowlists committed; awaiting live smoke run before enabling in scheduler defaults.|
|Parser & DTO implementation|BE-Conn-Apple|Source.Common|**DONE** AngleSharp detail parser produces canonical DTO payloads (CVE list, timestamps, affected tables) persisted via DTO store.|
|Canonical mapping & range primitives|BE-Conn-Apple|Models|**DONE** Mapper now emits SemVer-derived normalizedVersions with `apple:<platform>:<product>` notes; fixtures updated to assert canonical rules while we continue tracking multi-device coverage in follow-up tasks.<br>2025-10-11 research trail: confirmed payload aligns with `[{"scheme":"semver","type":"range","min":"<build-start>","minInclusive":true,"max":"<build-end>","maxInclusive":false,"notes":"apple:ios:17.1"}]`; continue using `notes` to surface build identifiers for storage provenance.|
|Deterministic fixtures/tests|QA|Testing|**DONE (2025-10-12)** Parser now scopes references to article content, sorts affected rows deterministically, and regenerated fixtures (125326/125328/106355/HT214108/HT215500) produce stable JSON + sanitizer HTML in English.|
|Telemetry & documentation|DevEx|Docs|**DONE (2025-10-12)** OpenTelemetry pipeline exports `StellaOps.Concelier.Connector.Vndr.Apple`; runbook `docs/ops/concelier-apple-operations.md` added with metrics + monitoring guidance.|
|Live HTML regression sweep|QA|Source.Common|**DONE (2025-10-12)** Captured latest support.apple.com articles for 125326/125328/106355/HT214108/HT215500, trimmed nav noise, and committed sanitized HTML + expected DTOs with invariant timestamps.|
|Fixture regeneration tooling|DevEx|Testing|**DONE (2025-10-12)** `scripts/update-apple-fixtures.(sh|ps1)` set the env flag + sentinel, forward through WSLENV, and clean up after regeneration; README references updated usage.|
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|Catalogue Apple security bulletin sources|BE-Conn-Apple|Research|**DONE** Feed contract documented in README (Software Lookup Service JSON + HT article hub) with rate-limit notes.|
|Fetch pipeline & state persistence|BE-Conn-Apple|Source.Common, Storage.Mongo|**DONE** Index fetch + detail ingestion with SourceState cursoring/allowlists committed; awaiting live smoke run before enabling in scheduler defaults.|
|Parser & DTO implementation|BE-Conn-Apple|Source.Common|**DONE** AngleSharp detail parser produces canonical DTO payloads (CVE list, timestamps, affected tables) persisted via DTO store.|
|Canonical mapping & range primitives|BE-Conn-Apple|Models|**DONE** Mapper now emits SemVer-derived normalizedVersions with `apple:<platform>:<product>` notes; fixtures updated to assert canonical rules while we continue tracking multi-device coverage in follow-up tasks.<br>2025-10-11 research trail: confirmed payload aligns with `[{"scheme":"semver","type":"range","min":"<build-start>","minInclusive":true,"max":"<build-end>","maxInclusive":false,"notes":"apple:ios:17.1"}]`; continue using `notes` to surface build identifiers for storage provenance.|
|Deterministic fixtures/tests|QA|Testing|**DONE (2025-10-12)** Parser now scopes references to article content, sorts affected rows deterministically, and regenerated fixtures (125326/125328/106355/HT214108/HT215500) produce stable JSON + sanitizer HTML in English.|
|Telemetry & documentation|DevEx|Docs|**DONE (2025-10-12)** OpenTelemetry pipeline exports `StellaOps.Concelier.Connector.Vndr.Apple`; runbook `docs/modules/concelier/operations/connectors/apple.md` added with metrics + monitoring guidance.|
|Live HTML regression sweep|QA|Source.Common|**DONE (2025-10-12)** Captured latest support.apple.com articles for 125326/125328/106355/HT214108/HT215500, trimmed nav noise, and committed sanitized HTML + expected DTOs with invariant timestamps.|
|Fixture regeneration tooling|DevEx|Testing|**DONE (2025-10-12)** `scripts/update-apple-fixtures.(sh|ps1)` set the env flag + sentinel, forward through WSLENV, and clean up after regeneration; README references updated usage.|

View File

@@ -1,12 +1,12 @@
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|FEEDCONN-CISCO-02-001 Confirm Cisco PSIRT data source|BE-Conn-Cisco|Research|**DONE (2025-10-11)** Selected openVuln REST API (`https://apix.cisco.com/security/advisories/v2/…`) as primary (structured JSON, CSAF/CVRF links) with RSS as fallback. Documented OAuth2 client-credentials flow (`cloudsso.cisco.com/as/token.oauth2`), baseline quotas (5 req/s, 30 req/min, 5000 req/day), and pagination contract (`pageIndex`, `pageSize≤100`) in `docs/concelier-connector-research-20251011.md`.|
|FEEDCONN-CISCO-02-002 Fetch pipeline & state persistence|BE-Conn-Cisco|Source.Common, Storage.Mongo|**DONE (2025-10-14)** Fetch job now streams openVuln pages with OAuth bearer handler, honours 429 `Retry-After`, persists per-advisory JSON + metadata into GridFS, and updates cursor (`lastModified`, advisory ID, pending docs).|
|FEEDCONN-CISCO-02-003 Parser & DTO implementation|BE-Conn-Cisco|Source.Common|**DONE (2025-10-14)** DTO factory normalizes SIR, folds CSAF product statuses, and persists `cisco.dto.v1` payloads (see `CiscoDtoFactory`).|
|FEEDCONN-CISCO-02-004 Canonical mapping & range primitives|BE-Conn-Cisco|Models|**DONE (2025-10-14)** `CiscoMapper` emits canonical advisories with vendor + SemVer primitives, provenance, and status tags.|
|FEEDCONN-CISCO-02-005 Deterministic fixtures & tests|QA|Testing|**DONE (2025-10-14)** Added unit tests (`StellaOps.Concelier.Connector.Vndr.Cisco.Tests`) exercising DTO/mapper pipelines; `dotnet test` validated.|
|FEEDCONN-CISCO-02-006 Telemetry & documentation|DevEx|Docs|**DONE (2025-10-14)** Cisco diagnostics counters exposed and ops runbook updated with telemetry guidance (`docs/ops/concelier-cisco-operations.md`).|
|FEEDCONN-CISCO-02-007 API selection decision memo|BE-Conn-Cisco|Research|**DONE (2025-10-11)** Drafted decision matrix: openVuln (structured/delta filters, OAuth throttle) vs RSS (delayed/minimal metadata). Pending OAuth onboarding (`FEEDCONN-CISCO-02-008`) before final recommendation circulated.|
|FEEDCONN-CISCO-02-008 OAuth client provisioning|Ops, BE-Conn-Cisco|Ops|**DONE (2025-10-14)** `docs/ops/concelier-cisco-operations.md` documents OAuth provisioning/rotation, quotas, and Offline Kit distribution guidance.|
|FEEDCONN-CISCO-02-009 Normalized SemVer promotion|BE-Conn-Cisco|Merge coordination (`FEEDMERGE-COORD-02-900`)|**TODO (due 2025-10-21)** Use helper from `../Merge/RANGE_PRIMITIVES_COORDINATION.md` to convert `SemVerPrimitive` outputs into `NormalizedVersionRule` with provenance (`cisco:{productId}`), update mapper/tests, and confirm merge normalized-rule counters drop.|
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|FEEDCONN-CISCO-02-001 Confirm Cisco PSIRT data source|BE-Conn-Cisco|Research|**DONE (2025-10-11)** Selected openVuln REST API (`https://apix.cisco.com/security/advisories/v2/…`) as primary (structured JSON, CSAF/CVRF links) with RSS as fallback. Documented OAuth2 client-credentials flow (`cloudsso.cisco.com/as/token.oauth2`), baseline quotas (5 req/s, 30 req/min, 5000 req/day), and pagination contract (`pageIndex`, `pageSize≤100`) in `docs/concelier-connector-research-20251011.md`.|
|FEEDCONN-CISCO-02-002 Fetch pipeline & state persistence|BE-Conn-Cisco|Source.Common, Storage.Mongo|**DONE (2025-10-14)** Fetch job now streams openVuln pages with OAuth bearer handler, honours 429 `Retry-After`, persists per-advisory JSON + metadata into GridFS, and updates cursor (`lastModified`, advisory ID, pending docs).|
|FEEDCONN-CISCO-02-003 Parser & DTO implementation|BE-Conn-Cisco|Source.Common|**DONE (2025-10-14)** DTO factory normalizes SIR, folds CSAF product statuses, and persists `cisco.dto.v1` payloads (see `CiscoDtoFactory`).|
|FEEDCONN-CISCO-02-004 Canonical mapping & range primitives|BE-Conn-Cisco|Models|**DONE (2025-10-14)** `CiscoMapper` emits canonical advisories with vendor + SemVer primitives, provenance, and status tags.|
|FEEDCONN-CISCO-02-005 Deterministic fixtures & tests|QA|Testing|**DONE (2025-10-14)** Added unit tests (`StellaOps.Concelier.Connector.Vndr.Cisco.Tests`) exercising DTO/mapper pipelines; `dotnet test` validated.|
|FEEDCONN-CISCO-02-006 Telemetry & documentation|DevEx|Docs|**DONE (2025-10-14)** Cisco diagnostics counters exposed and ops runbook updated with telemetry guidance (`docs/modules/concelier/operations/connectors/cisco.md`).|
|FEEDCONN-CISCO-02-007 API selection decision memo|BE-Conn-Cisco|Research|**DONE (2025-10-11)** Drafted decision matrix: openVuln (structured/delta filters, OAuth throttle) vs RSS (delayed/minimal metadata). Pending OAuth onboarding (`FEEDCONN-CISCO-02-008`) before final recommendation circulated.|
|FEEDCONN-CISCO-02-008 OAuth client provisioning|Ops, BE-Conn-Cisco|Ops|**DONE (2025-10-14)** `docs/modules/concelier/operations/connectors/cisco.md` documents OAuth provisioning/rotation, quotas, and Offline Kit distribution guidance.|
|FEEDCONN-CISCO-02-009 Normalized SemVer promotion|BE-Conn-Cisco|Merge coordination (`FEEDMERGE-COORD-02-900`)|**TODO (due 2025-10-21)** Use helper from `../Merge/RANGE_PRIMITIVES_COORDINATION.md` to convert `SemVerPrimitive` outputs into `NormalizedVersionRule` with provenance (`cisco:{productId}`), update mapper/tests, and confirm merge normalized-rule counters drop.|

View File

@@ -1,11 +1,11 @@
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|FEEDCONN-MSRC-02-001 Document MSRC Security Update Guide API|BE-Conn-MSRC|Research|**DONE (2025-10-11)** Confirmed REST endpoint (`https://api.msrc.microsoft.com/sug/v2.0/en-US/vulnerabilities`) + CVRF ZIP download flow, required Azure AD client-credentials scope (`api://api.msrc.microsoft.com/.default`), mandatory `api-version=2024-08-01` header, and delta params (`lastModifiedStartDateTime`, `lastModifiedEndDateTime`). Findings recorded in `docs/concelier-connector-research-20251011.md`.|
|FEEDCONN-MSRC-02-002 Fetch pipeline & source state|BE-Conn-MSRC|Source.Common, Storage.Mongo|**DONE (2025-10-15)** Added `MsrcApiClient` + token provider, cursor overlap handling, and detail persistence via GridFS (metadata carries CVRF URL + timestamps). State tracks `lastModifiedCursor` with configurable overlap/backoff. **Next:** coordinate with Tools on shared state-seeding helper once CVRF download flag stabilises.|
|FEEDCONN-MSRC-02-003 Parser & DTO implementation|BE-Conn-MSRC|Source.Common|**DONE (2025-10-15)** Implemented `MsrcDetailParser`/DTOs capturing threats, remediations, KB IDs, CVEs, CVSS, and affected products (build/platform metadata preserved).|
|FEEDCONN-MSRC-02-004 Canonical mapping & range primitives|BE-Conn-MSRC|Models|**DONE (2025-10-15)** `MsrcMapper` emits aliases (MSRC ID/CVE/KB), references (release notes + CVRF), vendor packages with `msrc.build` normalized rules, and CVSS provenance.|
|FEEDCONN-MSRC-02-005 Deterministic fixtures/tests|QA|Testing|**DONE (2025-10-15)** Added `StellaOps.Concelier.Connector.Vndr.Msrc.Tests` with canned token/summary/detail responses and snapshot assertions via Mongo2Go. Fixtures regenerate via `UPDATE_MSRC_FIXTURES`.|
|FEEDCONN-MSRC-02-006 Telemetry & documentation|DevEx|Docs|**DONE (2025-10-15)** Introduced `MsrcDiagnostics` meter (summary/detail/parse/map metrics), structured fetch logs, README updates, and Ops brief `docs/ops/concelier-msrc-operations.md` covering AAD onboarding + CVRF handling.|
|FEEDCONN-MSRC-02-007 API contract comparison memo|BE-Conn-MSRC|Research|**DONE (2025-10-11)** Completed memo outline recommending dual-path (REST for incremental, CVRF for offline); implementation hinges on `FEEDCONN-MSRC-02-008` AAD onboarding for token acquisition.|
|FEEDCONN-MSRC-02-008 Azure AD application onboarding|Ops, BE-Conn-MSRC|Ops|**DONE (2025-10-15)** Coordinated Ops handoff; drafted AAD onboarding brief (`docs/ops/concelier-msrc-operations.md`) with app registration requirements, secret rotation policy, sample configuration, and CVRF mirroring guidance for Offline Kit.|
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|FEEDCONN-MSRC-02-001 Document MSRC Security Update Guide API|BE-Conn-MSRC|Research|**DONE (2025-10-11)** Confirmed REST endpoint (`https://api.msrc.microsoft.com/sug/v2.0/en-US/vulnerabilities`) + CVRF ZIP download flow, required Azure AD client-credentials scope (`api://api.msrc.microsoft.com/.default`), mandatory `api-version=2024-08-01` header, and delta params (`lastModifiedStartDateTime`, `lastModifiedEndDateTime`). Findings recorded in `docs/concelier-connector-research-20251011.md`.|
|FEEDCONN-MSRC-02-002 Fetch pipeline & source state|BE-Conn-MSRC|Source.Common, Storage.Mongo|**DONE (2025-10-15)** Added `MsrcApiClient` + token provider, cursor overlap handling, and detail persistence via GridFS (metadata carries CVRF URL + timestamps). State tracks `lastModifiedCursor` with configurable overlap/backoff. **Next:** coordinate with Tools on shared state-seeding helper once CVRF download flag stabilises.|
|FEEDCONN-MSRC-02-003 Parser & DTO implementation|BE-Conn-MSRC|Source.Common|**DONE (2025-10-15)** Implemented `MsrcDetailParser`/DTOs capturing threats, remediations, KB IDs, CVEs, CVSS, and affected products (build/platform metadata preserved).|
|FEEDCONN-MSRC-02-004 Canonical mapping & range primitives|BE-Conn-MSRC|Models|**DONE (2025-10-15)** `MsrcMapper` emits aliases (MSRC ID/CVE/KB), references (release notes + CVRF), vendor packages with `msrc.build` normalized rules, and CVSS provenance.|
|FEEDCONN-MSRC-02-005 Deterministic fixtures/tests|QA|Testing|**DONE (2025-10-15)** Added `StellaOps.Concelier.Connector.Vndr.Msrc.Tests` with canned token/summary/detail responses and snapshot assertions via Mongo2Go. Fixtures regenerate via `UPDATE_MSRC_FIXTURES`.|
|FEEDCONN-MSRC-02-006 Telemetry & documentation|DevEx|Docs|**DONE (2025-10-15)** Introduced `MsrcDiagnostics` meter (summary/detail/parse/map metrics), structured fetch logs, README updates, and Ops brief `docs/modules/concelier/operations/connectors/msrc.md` covering AAD onboarding + CVRF handling.|
|FEEDCONN-MSRC-02-007 API contract comparison memo|BE-Conn-MSRC|Research|**DONE (2025-10-11)** Completed memo outline recommending dual-path (REST for incremental, CVRF for offline); implementation hinges on `FEEDCONN-MSRC-02-008` AAD onboarding for token acquisition.|
|FEEDCONN-MSRC-02-008 Azure AD application onboarding|Ops, BE-Conn-MSRC|Ops|**DONE (2025-10-15)** Coordinated Ops handoff; drafted AAD onboarding brief (`docs/modules/concelier/operations/connectors/msrc.md`) with app registration requirements, secret rotation policy, sample configuration, and CVRF mirroring guidance for Offline Kit.|

View File

@@ -1,143 +0,0 @@
````markdown
# Concelier Vulnerability Conflict Resolution Rules
This document defines the canonical, deterministic conflict resolution strategy for merging vulnerability data from **NVD**, **GHSA**, and **OSV** in Concelier.
---
## 🧭 Source Precedence
1. **Primary order:**
`GHSA > NVD > OSV`
**Rationale:**
GHSA advisories are human-curated and fast to correct; NVD has the broadest CVE coverage; OSV excels in ecosystem-specific precision.
2. **Freshness override (≥48 h):**
If a **lower-priority** source is **newer by at least 48 hours** for a freshness-sensitive field, its value overrides the higher-priority one.
Always store the decision in a provenance record.
3. **Merge scope:**
Only merge data referring to the **same CVE ID** or the same GHSA/OSV advisory explicitly mapped to that CVE.
---
## 🧩 Field-Level Precedence
| Field | Priority | Freshness-Sensitive | Notes |
|-------|-----------|--------------------|-------|
| Title / Summary | GHSA → NVD → OSV | ✅ | Prefer concise structured titles |
| Description | GHSA → NVD → OSV | ✅ | |
| Severity (CVSS) | NVD → GHSA → OSV | ❌ | Keep all under `metrics[]`, mark `canonicalMetric` by order |
| Ecosystem Severity Label | GHSA → OSV | ❌ | Supplemental tag only |
| Affected Packages / Ranges | OSV → GHSA → NVD | ✅ | OSV strongest for SemVer normalization |
| CWE(s) | NVD → GHSA → OSV | ❌ | NVD taxonomy most stable |
| References / Links | Union of all | ✅ | Deduplicate by normalized URL |
| Credits / Acknowledgements | Union of all | ✅ | Sort by role, displayName |
| Published / Modified timestamps | Earliest published / Latest modified | ✅ | |
| EPSS / KEV / Exploit status | Specialized feed only | ❌ | Do not override manually |
---
## ⚖️ Deterministic Tie-Breakers
If precedence and freshness both tie:
1. **Source order:** GHSA > NVD > OSV
2. **Lexicographic stability:** Prefer shorter normalized text; if equal, ASCIIbetical
3. **Stable hash of payload:** Lowest hash wins
Each chosen value must store the merge rationale:
```json
{
"provenance": {
"source": "GHSA",
"kind": "merge",
"value": "description",
"decisionReason": "precedence"
}
}
````
---
## 🧮 Merge Algorithm (Pseudocode)
```csharp
inputs: records = {ghsa?, nvd?, osv?}
out = new CanonicalVuln(CVE)
foreach field in CANONICAL_SCHEMA:
candidates = collect(values, source, lastModified)
if freshnessSensitive(field) and newerBy48h(lowerPriority):
pick newest
else:
pick by precedence(field)
if tie:
applyTieBreakers()
out.field = normalize(field, value)
out.provenance[field] = decisionTrail
out.references = dedupe(union(all.references))
out.affected = normalizeAndUnion(OSV, GHSA, NVD)
out.metrics = rankAndSetCanonical(NVDv3 → GHSA → OSV → v2)
return out
```
---
## 🔧 Normalization Rules
* **SemVer:**
Parse with tolerant builder; normalize `v` prefixes; map comparators (`<=`, `<`, `>=`, `>`); expand OSV events into continuous ranges.
* **Packages:**
Canonical key = `(ecosystem, packageName, language?)`; maintain aliases (purl, npm, Maven GAV, etc.).
* **CWE:**
Store both ID and name; validate against current CWE catalog.
* **CVSS:**
Preserve provided vector and base score; recompute only for validation.
---
## ✅ Output Guarantees
| Property | Description |
| ---------------- | ------------------------------------------------------------------------------- |
| **Reproducible** | Same input → same canonical output |
| **Auditable** | Provenance stored per field |
| **Complete** | Unions with de-duplication |
| **Composable** | Future layers (KEV, EPSS, vendor advisories) can safely extend precedence rules |
---
## 🧠 Example
* GHSA summary updated on *2025-10-09*
* NVD last modified *2025-10-05*
* OSV updated *2025-10-10*
→ **Summary:** OSV wins (freshness override)
→ **CVSS:** NVD v3.1 remains canonical
→ **Affected:** OSV ranges canonical; GHSA aliases merged
---
## 🧰 Optional C# Helper Class
`StellaOps.Concelier.Core/CanonicalMerger.cs`
Implements:
* `FieldPrecedenceMap`
* `FreshnessSensitiveFields`
* `ApplyTieBreakers()`
* `NormalizeAndUnion()`
Deterministically builds `CanonicalVuln` with full provenance tracking.
```
```

View File

@@ -1,94 +1,94 @@
# TASKS — Epic 1: Aggregation-Only Contract
> **AOC Reminder:** Excititor WebService publishes raw statements/linksets only; derived precedence/severity belongs to Policy overlays.
| ID | Status | Owner(s) | Depends on | Notes |
|---|---|---|---|---|
| EXCITITOR-WEB-AOC-19-001 `Raw VEX ingestion APIs` | TODO | Excititor WebService Guild | EXCITITOR-CORE-AOC-19-001, EXCITITOR-STORE-AOC-19-001 | Implement `POST /ingest/vex`, `GET /vex/raw*`, and `POST /aoc/verify` endpoints. Enforce Authority scopes, tenant injection, and guard pipeline to ensure only immutable VEX facts are persisted. |
> Docs alignment (2025-10-26): See AOC reference §45 and authority scopes doc for required tokens/behaviour.
| EXCITITOR-WEB-AOC-19-002 `AOC observability + metrics` | TODO | Excititor WebService Guild, Observability Guild | EXCITITOR-WEB-AOC-19-001 | Export metrics (`ingestion_write_total`, `aoc_violation_total`, signature verification counters) and tracing spans matching Conseiller naming. Ensure structured logging includes tenant, source vendor, upstream id, and content hash. |
> Docs alignment (2025-10-26): Metrics/traces/log schema in `docs/observability/observability.md`.
| EXCITITOR-WEB-AOC-19-003 `Guard + schema test harness` | TODO | QA Guild | EXCITITOR-WEB-AOC-19-001 | Add unit/integration tests for schema validation, forbidden field rejection (`ERR_AOC_001/006/007`), and supersedes behavior using CycloneDX-VEX & CSAF fixtures with deterministic expectations. |
> Docs alignment (2025-10-26): Error codes + CLI verification in `docs/cli/cli-reference.md`.
| EXCITITOR-WEB-AOC-19-004 `Batch ingest validation` | TODO | Excititor WebService Guild, QA Guild | EXCITITOR-WEB-AOC-19-003, EXCITITOR-CORE-AOC-19-002 | Build large fixture ingest covering mixed VEX statuses, verifying raw storage parity, metrics, and CLI `aoc verify` compatibility. Document load test/runbook updates. |
> Docs alignment (2025-10-26): Offline/air-gap workflows captured in `docs/deploy/containers.md` §5.
## Policy Engine v2
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-POLICY-20-001 `Policy selection endpoints` | TODO | Excititor WebService Guild | WEB-POLICY-20-001, EXCITITOR-CORE-AOC-19-004 | Provide VEX lookup APIs supporting PURL/advisory batching, scope filtering, and tenant enforcement with deterministic ordering + pagination. |
## StellaOps Console (Sprint 23)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-CONSOLE-23-001 `VEX aggregation views` | TODO | Excititor WebService Guild, BE-Base Platform Guild | EXCITITOR-LNM-21-201, EXCITITOR-LNM-21-202 | Expose `/console/vex` endpoints returning grouped VEX statements per advisory/component with status chips, justification metadata, precedence trace pointers, and tenant-scoped filters for Console explorer. |
| EXCITITOR-CONSOLE-23-002 `Dashboard VEX deltas` | TODO | Excititor WebService Guild | EXCITITOR-CONSOLE-23-001, EXCITITOR-LNM-21-203 | Provide aggregated counts for VEX overrides (new, not_affected, revoked) powering Console dashboard + live status ticker; emit metrics for policy explain integration. |
| EXCITITOR-CONSOLE-23-003 `VEX search helpers` | TODO | Excititor WebService Guild | EXCITITOR-CONSOLE-23-001 | Deliver rapid lookup endpoints of VEX by advisory/component for Console global search; ensure response includes provenance and precedence context; include caching and RBAC. |
## Graph Explorer v1
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
## Link-Not-Merge v1
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-LNM-21-201 `Observation APIs` | TODO | Excititor WebService Guild, BE-Base Platform Guild | EXCITITOR-LNM-21-001 | Add VEX observation read endpoints with filters, pagination, RBAC, and tenant scoping. |
| EXCITITOR-LNM-21-202 `Linkset APIs` | TODO | Excititor WebService Guild | EXCITITOR-LNM-21-002, EXCITITOR-LNM-21-003 | Implement linkset read/export/evidence endpoints returning correlation/conflict payloads and map errors to `ERR_AGG_*`. |
| EXCITITOR-LNM-21-203 `Event publishing` | TODO | Excititor WebService Guild, Platform Events Guild | EXCITITOR-LNM-21-005 | Publish `vex.linkset.updated` events, document schema, and ensure idempotent delivery. |
## Graph & Vuln Explorer v1
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-GRAPH-24-101 `VEX summary API` | TODO | Excititor WebService Guild | EXCITITOR-GRAPH-24-001 | Provide endpoints delivering VEX status summaries per component/asset for Vuln Explorer integration. |
| EXCITITOR-GRAPH-24-102 `Evidence batch API` | TODO | Excititor WebService Guild | EXCITITOR-LNM-21-201 | Add batch VEX observation retrieval optimized for Graph overlays/tooltips. |
## VEX Lens (Sprint 30)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-VEXLENS-30-001 `VEX evidence enrichers` | TODO | Excititor WebService Guild, VEX Lens Guild | EXCITITOR-VULN-29-001, VEXLENS-30-005 | Include issuer hints, signatures, and product trees in evidence payloads for VEX Lens; Label: VEX-Lens. |
## Vulnerability Explorer (Sprint 29)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-VULN-29-001 `VEX key canonicalization` | TODO | Excititor WebService Guild | EXCITITOR-LNM-21-001 | Canonicalize (lossless) VEX advisory/product keys (map to `advisory_key`, capture product scopes); expose original sources in `links[]`; AOC-compliant: no merge, no derived fields, no suppression; backfill existing records. |
| EXCITITOR-VULN-29-002 `Evidence retrieval` | TODO | Excititor WebService Guild | EXCITITOR-VULN-29-001, VULN-API-29-003 | Provide `/vuln/evidence/vex/{advisory_key}` returning raw VEX statements filtered by tenant/product scope for Explorer evidence tabs. |
| EXCITITOR-VULN-29-004 `Observability` | TODO | Excititor WebService Guild, Observability Guild | EXCITITOR-VULN-29-001 | Add metrics/logs for VEX normalization, suppression scopes, withdrawn statements; emit events consumed by Vuln Explorer resolver. |
## Advisory AI (Sprint 31)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-AIAI-31-001 `Justification enrichment` | TODO | Excititor WebService Guild | EXCITITOR-VULN-29-001 | Expose normalized VEX justifications, product trees, and paragraph anchors for Advisory AI conflict explanations. |
| EXCITITOR-AIAI-31-002 `VEX chunk API` | TODO | Excititor WebService Guild | EXCITITOR-AIAI-31-001, VEXLENS-30-006 | Provide `/vex/evidence/chunks` endpoint returning tenant-scoped VEX statements with signature metadata and scope scores for RAG. |
| EXCITITOR-AIAI-31-003 `Telemetry` | TODO | Excititor WebService Guild, Observability Guild | EXCITITOR-AIAI-31-001 | Emit metrics/logs for VEX chunk usage, signature verification failures, and guardrail triggers. |
## Observability & Forensics (Epic 15)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-WEB-OBS-50-001 `Telemetry adoption` | TODO | Excititor WebService Guild | TELEMETRY-OBS-50-001, EXCITITOR-OBS-50-001 | Adopt telemetry core for VEX APIs, ensure responses include trace IDs & correlation headers, and update structured logging for read endpoints. |
| EXCITITOR-WEB-OBS-51-001 `Observability health endpoints` | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-50-001, WEB-OBS-51-001 | Implement `/obs/excititor/health` summarizing ingest/link SLOs, signature failure counts, and conflict trends for Console dashboards. |
| EXCITITOR-WEB-OBS-52-001 `Timeline streaming` | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-50-001, TIMELINE-OBS-52-003 | Provide SSE bridge for VEX timeline events with tenant filters, pagination, and guardrails. |
| EXCITITOR-WEB-OBS-53-001 `Evidence APIs` | TODO | Excititor WebService Guild, Evidence Locker Guild | EXCITITOR-OBS-53-001, EVID-OBS-53-003 | Expose `/evidence/vex/*` endpoints that fetch locker bundles, enforce scopes, and surface verification metadata. |
| EXCITITOR-WEB-OBS-54-001 `Attestation APIs` | TODO | Excititor WebService Guild | EXCITITOR-OBS-54-001, PROV-OBS-54-001 | Add `/attestations/vex/*` endpoints returning DSSE verification state, builder identity, and chain-of-custody links. |
| EXCITITOR-WEB-OBS-55-001 `Incident mode toggles` | TODO | Excititor WebService Guild, DevOps Guild | EXCITITOR-OBS-55-001, WEB-OBS-55-001 | Provide incident mode API for VEX pipelines with activation audit logs and retention override previews. |
## Air-Gapped Mode (Epic 16)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-WEB-AIRGAP-56-001 | TODO | Excititor WebService Guild | AIRGAP-IMP-58-001, EXCITITOR-AIRGAP-56-001 | Support mirror bundle registration via APIs, expose bundle provenance in VEX responses, and block external connectors in sealed mode. |
| EXCITITOR-WEB-AIRGAP-56-002 | TODO | Excititor WebService Guild, AirGap Time Guild | EXCITITOR-WEB-AIRGAP-56-001, AIRGAP-TIME-58-001 | Return VEX staleness metrics and time anchor info in API responses for Console/CLI use. |
| EXCITITOR-WEB-AIRGAP-57-001 | TODO | Excititor WebService Guild, AirGap Policy Guild | AIRGAP-POL-56-001 | Map sealed-mode violations to standardized error payload with remediation guidance. |
| EXCITITOR-WEB-AIRGAP-58-001 | TODO | Excititor WebService Guild, AirGap Importer Guild | EXCITITOR-WEB-AIRGAP-56-001, TIMELINE-OBS-53-001 | Emit timeline events for VEX bundle imports with bundle ID, scope, and actor metadata. |
## SDKs & OpenAPI (Epic 17)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-WEB-OAS-61-001 | TODO | Excititor WebService Guild | OAS-61-001 | Implement `/.well-known/openapi` discovery endpoint with spec version metadata. |
| EXCITITOR-WEB-OAS-61-002 | TODO | Excititor WebService Guild | APIGOV-61-001 | Standardize error envelope responses and update controller/unit tests. |
| EXCITITOR-WEB-OAS-62-001 | TODO | Excititor WebService Guild | EXCITITOR-OAS-61-002 | Add curated examples for VEX observation/linkset endpoints and ensure portal displays them. |
| EXCITITOR-WEB-OAS-63-001 | TODO | Excititor WebService Guild, API Governance Guild | APIGOV-63-001 | Emit deprecation headers and update docs for retiring VEX APIs. |
# TASKS — Epic 1: Aggregation-Only Contract
> **AOC Reminder:** Excititor WebService publishes raw statements/linksets only; derived precedence/severity belongs to Policy overlays.
| ID | Status | Owner(s) | Depends on | Notes |
|---|---|---|---|---|
| EXCITITOR-WEB-AOC-19-001 `Raw VEX ingestion APIs` | TODO | Excititor WebService Guild | EXCITITOR-CORE-AOC-19-001, EXCITITOR-STORE-AOC-19-001 | Implement `POST /ingest/vex`, `GET /vex/raw*`, and `POST /aoc/verify` endpoints. Enforce Authority scopes, tenant injection, and guard pipeline to ensure only immutable VEX facts are persisted. |
> Docs alignment (2025-10-26): See AOC reference §45 and authority scopes doc for required tokens/behaviour.
| EXCITITOR-WEB-AOC-19-002 `AOC observability + metrics` | TODO | Excititor WebService Guild, Observability Guild | EXCITITOR-WEB-AOC-19-001 | Export metrics (`ingestion_write_total`, `aoc_violation_total`, signature verification counters) and tracing spans matching Conseiller naming. Ensure structured logging includes tenant, source vendor, upstream id, and content hash. |
> Docs alignment (2025-10-26): Metrics/traces/log schema in `docs/observability/observability.md`.
| EXCITITOR-WEB-AOC-19-003 `Guard + schema test harness` | TODO | QA Guild | EXCITITOR-WEB-AOC-19-001 | Add unit/integration tests for schema validation, forbidden field rejection (`ERR_AOC_001/006/007`), and supersedes behavior using CycloneDX-VEX & CSAF fixtures with deterministic expectations. |
> Docs alignment (2025-10-26): Error codes + CLI verification in `docs/modules/cli/guides/cli-reference.md`.
| EXCITITOR-WEB-AOC-19-004 `Batch ingest validation` | TODO | Excititor WebService Guild, QA Guild | EXCITITOR-WEB-AOC-19-003, EXCITITOR-CORE-AOC-19-002 | Build large fixture ingest covering mixed VEX statuses, verifying raw storage parity, metrics, and CLI `aoc verify` compatibility. Document load test/runbook updates. |
> Docs alignment (2025-10-26): Offline/air-gap workflows captured in `docs/deploy/containers.md` §5.
## Policy Engine v2
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-POLICY-20-001 `Policy selection endpoints` | TODO | Excititor WebService Guild | WEB-POLICY-20-001, EXCITITOR-CORE-AOC-19-004 | Provide VEX lookup APIs supporting PURL/advisory batching, scope filtering, and tenant enforcement with deterministic ordering + pagination. |
## StellaOps Console (Sprint 23)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-CONSOLE-23-001 `VEX aggregation views` | TODO | Excititor WebService Guild, BE-Base Platform Guild | EXCITITOR-LNM-21-201, EXCITITOR-LNM-21-202 | Expose `/console/vex` endpoints returning grouped VEX statements per advisory/component with status chips, justification metadata, precedence trace pointers, and tenant-scoped filters for Console explorer. |
| EXCITITOR-CONSOLE-23-002 `Dashboard VEX deltas` | TODO | Excititor WebService Guild | EXCITITOR-CONSOLE-23-001, EXCITITOR-LNM-21-203 | Provide aggregated counts for VEX overrides (new, not_affected, revoked) powering Console dashboard + live status ticker; emit metrics for policy explain integration. |
| EXCITITOR-CONSOLE-23-003 `VEX search helpers` | TODO | Excititor WebService Guild | EXCITITOR-CONSOLE-23-001 | Deliver rapid lookup endpoints of VEX by advisory/component for Console global search; ensure response includes provenance and precedence context; include caching and RBAC. |
## Graph Explorer v1
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
## Link-Not-Merge v1
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-LNM-21-201 `Observation APIs` | TODO | Excititor WebService Guild, BE-Base Platform Guild | EXCITITOR-LNM-21-001 | Add VEX observation read endpoints with filters, pagination, RBAC, and tenant scoping. |
| EXCITITOR-LNM-21-202 `Linkset APIs` | TODO | Excititor WebService Guild | EXCITITOR-LNM-21-002, EXCITITOR-LNM-21-003 | Implement linkset read/export/evidence endpoints returning correlation/conflict payloads and map errors to `ERR_AGG_*`. |
| EXCITITOR-LNM-21-203 `Event publishing` | TODO | Excititor WebService Guild, Platform Events Guild | EXCITITOR-LNM-21-005 | Publish `vex.linkset.updated` events, document schema, and ensure idempotent delivery. |
## Graph & Vuln Explorer v1
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-GRAPH-24-101 `VEX summary API` | TODO | Excititor WebService Guild | EXCITITOR-GRAPH-24-001 | Provide endpoints delivering VEX status summaries per component/asset for Vuln Explorer integration. |
| EXCITITOR-GRAPH-24-102 `Evidence batch API` | TODO | Excititor WebService Guild | EXCITITOR-LNM-21-201 | Add batch VEX observation retrieval optimized for Graph overlays/tooltips. |
## VEX Lens (Sprint 30)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-VEXLENS-30-001 `VEX evidence enrichers` | TODO | Excititor WebService Guild, VEX Lens Guild | EXCITITOR-VULN-29-001, VEXLENS-30-005 | Include issuer hints, signatures, and product trees in evidence payloads for VEX Lens; Label: VEX-Lens. |
## Vulnerability Explorer (Sprint 29)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-VULN-29-001 `VEX key canonicalization` | TODO | Excititor WebService Guild | EXCITITOR-LNM-21-001 | Canonicalize (lossless) VEX advisory/product keys (map to `advisory_key`, capture product scopes); expose original sources in `links[]`; AOC-compliant: no merge, no derived fields, no suppression; backfill existing records. |
| EXCITITOR-VULN-29-002 `Evidence retrieval` | TODO | Excititor WebService Guild | EXCITITOR-VULN-29-001, VULN-API-29-003 | Provide `/vuln/evidence/vex/{advisory_key}` returning raw VEX statements filtered by tenant/product scope for Explorer evidence tabs. |
| EXCITITOR-VULN-29-004 `Observability` | TODO | Excititor WebService Guild, Observability Guild | EXCITITOR-VULN-29-001 | Add metrics/logs for VEX normalization, suppression scopes, withdrawn statements; emit events consumed by Vuln Explorer resolver. |
## Advisory AI (Sprint 31)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-AIAI-31-001 `Justification enrichment` | TODO | Excititor WebService Guild | EXCITITOR-VULN-29-001 | Expose normalized VEX justifications, product trees, and paragraph anchors for Advisory AI conflict explanations. |
| EXCITITOR-AIAI-31-002 `VEX chunk API` | TODO | Excititor WebService Guild | EXCITITOR-AIAI-31-001, VEXLENS-30-006 | Provide `/vex/evidence/chunks` endpoint returning tenant-scoped VEX statements with signature metadata and scope scores for RAG. |
| EXCITITOR-AIAI-31-003 `Telemetry` | TODO | Excititor WebService Guild, Observability Guild | EXCITITOR-AIAI-31-001 | Emit metrics/logs for VEX chunk usage, signature verification failures, and guardrail triggers. |
## Observability & Forensics (Epic 15)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-WEB-OBS-50-001 `Telemetry adoption` | TODO | Excititor WebService Guild | TELEMETRY-OBS-50-001, EXCITITOR-OBS-50-001 | Adopt telemetry core for VEX APIs, ensure responses include trace IDs & correlation headers, and update structured logging for read endpoints. |
| EXCITITOR-WEB-OBS-51-001 `Observability health endpoints` | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-50-001, WEB-OBS-51-001 | Implement `/obs/excititor/health` summarizing ingest/link SLOs, signature failure counts, and conflict trends for Console dashboards. |
| EXCITITOR-WEB-OBS-52-001 `Timeline streaming` | TODO | Excititor WebService Guild | EXCITITOR-WEB-OBS-50-001, TIMELINE-OBS-52-003 | Provide SSE bridge for VEX timeline events with tenant filters, pagination, and guardrails. |
| EXCITITOR-WEB-OBS-53-001 `Evidence APIs` | TODO | Excititor WebService Guild, Evidence Locker Guild | EXCITITOR-OBS-53-001, EVID-OBS-53-003 | Expose `/evidence/vex/*` endpoints that fetch locker bundles, enforce scopes, and surface verification metadata. |
| EXCITITOR-WEB-OBS-54-001 `Attestation APIs` | TODO | Excititor WebService Guild | EXCITITOR-OBS-54-001, PROV-OBS-54-001 | Add `/attestations/vex/*` endpoints returning DSSE verification state, builder identity, and chain-of-custody links. |
| EXCITITOR-WEB-OBS-55-001 `Incident mode toggles` | TODO | Excititor WebService Guild, DevOps Guild | EXCITITOR-OBS-55-001, WEB-OBS-55-001 | Provide incident mode API for VEX pipelines with activation audit logs and retention override previews. |
## Air-Gapped Mode (Epic 16)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-WEB-AIRGAP-56-001 | TODO | Excititor WebService Guild | AIRGAP-IMP-58-001, EXCITITOR-AIRGAP-56-001 | Support mirror bundle registration via APIs, expose bundle provenance in VEX responses, and block external connectors in sealed mode. |
| EXCITITOR-WEB-AIRGAP-56-002 | TODO | Excititor WebService Guild, AirGap Time Guild | EXCITITOR-WEB-AIRGAP-56-001, AIRGAP-TIME-58-001 | Return VEX staleness metrics and time anchor info in API responses for Console/CLI use. |
| EXCITITOR-WEB-AIRGAP-57-001 | TODO | Excititor WebService Guild, AirGap Policy Guild | AIRGAP-POL-56-001 | Map sealed-mode violations to standardized error payload with remediation guidance. |
| EXCITITOR-WEB-AIRGAP-58-001 | TODO | Excititor WebService Guild, AirGap Importer Guild | EXCITITOR-WEB-AIRGAP-56-001, TIMELINE-OBS-53-001 | Emit timeline events for VEX bundle imports with bundle ID, scope, and actor metadata. |
## SDKs & OpenAPI (Epic 17)
| ID | Status | Owner(s) | Depends on | Notes |
|----|--------|----------|------------|-------|
| EXCITITOR-WEB-OAS-61-001 | TODO | Excititor WebService Guild | OAS-61-001 | Implement `/.well-known/openapi` discovery endpoint with spec version metadata. |
| EXCITITOR-WEB-OAS-61-002 | TODO | Excititor WebService Guild | APIGOV-61-001 | Standardize error envelope responses and update controller/unit tests. |
| EXCITITOR-WEB-OAS-62-001 | TODO | Excititor WebService Guild | EXCITITOR-OAS-61-002 | Add curated examples for VEX observation/linkset endpoints and ensure portal displays them. |
| EXCITITOR-WEB-OAS-63-001 | TODO | Excititor WebService Guild, API Governance Guild | APIGOV-63-001 | Emit deprecation headers and update docs for retiring VEX APIs. |

View File

@@ -1,9 +1,9 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-ATTEST-01-001 In-toto predicate & DSSE builder|Team Excititor Attestation|EXCITITOR-CORE-01-001|**DONE (2025-10-16)** Added deterministic in-toto predicate/statement models, DSSE envelope builder wired to signer abstraction, and attestation client producing metadata + diagnostics.|
|EXCITITOR-ATTEST-01-002 Rekor v2 client integration|Team Excititor Attestation|EXCITITOR-ATTEST-01-001|**DONE (2025-10-16)** Implemented Rekor HTTP client with retry/backoff, transparency log abstraction, DI helpers, and attestation client integration capturing Rekor metadata + diagnostics.|
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-ATTEST-01-001 In-toto predicate & DSSE builder|Team Excititor Attestation|EXCITITOR-CORE-01-001|**DONE (2025-10-16)** Added deterministic in-toto predicate/statement models, DSSE envelope builder wired to signer abstraction, and attestation client producing metadata + diagnostics.|
|EXCITITOR-ATTEST-01-002 Rekor v2 client integration|Team Excititor Attestation|EXCITITOR-ATTEST-01-001|**DONE (2025-10-16)** Implemented Rekor HTTP client with retry/backoff, transparency log abstraction, DI helpers, and attestation client integration capturing Rekor metadata + diagnostics.|
|EXCITITOR-ATTEST-01-003 Verification suite & observability|Team Excititor Attestation|EXCITITOR-ATTEST-01-002|DOING (2025-10-22) Continuing implementation: build `IVexAttestationVerifier`, wire metrics/logging, and add regression tests. Draft plan in `EXCITITOR-ATTEST-01-003-plan.md` (2025-10-19) guides scope; updating with worknotes as progress lands.|
> Remark (2025-10-22): Added verifier implementation + metrics/tests; next steps include wiring into WebService/Worker flows and expanding negative-path coverage.

View File

@@ -1,7 +1,7 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-ABS-01-001 Connector context & base classes|Team Excititor Connectors|EXCITITOR-CORE-01-003|**DONE (2025-10-17)** Added `StellaOps.Excititor.Connectors.Abstractions` project with `VexConnectorBase`, deterministic logging scopes, metadata builder helpers, and connector descriptors; docs updated to highlight the shared abstractions.|
|EXCITITOR-CONN-ABS-01-002 YAML options & validation|Team Excititor Connectors|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Delivered `VexConnectorOptionsBinder` + binder options/validators, environment-variable expansion, data-annotation checks, and custom validation hooks with documentation updates covering the workflow.|
|EXCITITOR-CONN-ABS-01-003 Plugin packaging & docs|Team Excititor Connectors|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Authored `docs/dev/30_EXCITITOR_CONNECTOR_GUIDE.md`, added quick-start template under `docs/dev/templates/excititor-connector/`, and updated module docs to reference the packaging workflow.|
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-ABS-01-001 Connector context & base classes|Team Excititor Connectors|EXCITITOR-CORE-01-003|**DONE (2025-10-17)** Added `StellaOps.Excititor.Connectors.Abstractions` project with `VexConnectorBase`, deterministic logging scopes, metadata builder helpers, and connector descriptors; docs updated to highlight the shared abstractions.|
|EXCITITOR-CONN-ABS-01-002 YAML options & validation|Team Excititor Connectors|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Delivered `VexConnectorOptionsBinder` + binder options/validators, environment-variable expansion, data-annotation checks, and custom validation hooks with documentation updates covering the workflow.|
|EXCITITOR-CONN-ABS-01-003 Plugin packaging & docs|Team Excititor Connectors|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Authored `docs/dev/30_EXCITITOR_CONNECTOR_GUIDE.md`, added quick-start template under `docs/dev/templates/excititor-connector/`, and updated module docs to reference the packaging workflow.|

View File

@@ -1,7 +1,7 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-CISCO-01-001 Endpoint discovery & auth plumbing|Team Excititor Connectors Cisco|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Added `CiscoProviderMetadataLoader` with bearer token support, offline snapshot fallback, DI helpers, and tests covering network/offline discovery to unblock subsequent fetch work.|
|EXCITITOR-CONN-CISCO-01-002 CSAF pull loop & pagination|Team Excititor Connectors Cisco|EXCITITOR-CONN-CISCO-01-001, EXCITITOR-STORAGE-01-003|**DONE (2025-10-17)** Implemented paginated advisory fetch using provider directories, raw document persistence with dedupe/state tracking, offline resiliency, and unit coverage.|
|EXCITITOR-CONN-CISCO-01-003 Provider trust metadata|Team Excititor Connectors Cisco|EXCITITOR-CONN-CISCO-01-002, EXCITITOR-POLICY-01-001|**DOING (2025-10-19)** Prereqs confirmed (both DONE); implementing cosign/PGP trust metadata emission and advisory provenance hints for policy weighting.|
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-CISCO-01-001 Endpoint discovery & auth plumbing|Team Excititor Connectors Cisco|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Added `CiscoProviderMetadataLoader` with bearer token support, offline snapshot fallback, DI helpers, and tests covering network/offline discovery to unblock subsequent fetch work.|
|EXCITITOR-CONN-CISCO-01-002 CSAF pull loop & pagination|Team Excititor Connectors Cisco|EXCITITOR-CONN-CISCO-01-001, EXCITITOR-STORAGE-01-003|**DONE (2025-10-17)** Implemented paginated advisory fetch using provider directories, raw document persistence with dedupe/state tracking, offline resiliency, and unit coverage.|
|EXCITITOR-CONN-CISCO-01-003 Provider trust metadata|Team Excititor Connectors Cisco|EXCITITOR-CONN-CISCO-01-002, EXCITITOR-POLICY-01-001|**DOING (2025-10-19)** Prereqs confirmed (both DONE); implementing cosign/PGP trust metadata emission and advisory provenance hints for policy weighting.|

View File

@@ -1,7 +1,7 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-MS-01-001 AAD onboarding & token cache|Team Excititor Connectors MSRC|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Added MSRC connector project with configurable AAD options, token provider (offline/online modes), DI wiring, and unit tests covering caching and fallback scenarios.|
|EXCITITOR-CONN-MS-01-002 CSAF download pipeline|Team Excititor Connectors MSRC|EXCITITOR-CONN-MS-01-001, EXCITITOR-STORAGE-01-003|**DOING (2025-10-19)** Prereqs verified (EXCITITOR-CONN-MS-01-001, EXCITITOR-STORAGE-01-003); drafting fetch/retry plan and storage wiring before implementation of CSAF package download, checksum validation, and quarantine flows.|
|EXCITITOR-CONN-MS-01-003 Trust metadata & provenance hints|Team Excititor Connectors MSRC|EXCITITOR-CONN-MS-01-002, EXCITITOR-POLICY-01-001|TODO Emit cosign/AAD issuer metadata, attach provenance details, and document policy integration.|
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-MS-01-001 AAD onboarding & token cache|Team Excititor Connectors MSRC|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Added MSRC connector project with configurable AAD options, token provider (offline/online modes), DI wiring, and unit tests covering caching and fallback scenarios.|
|EXCITITOR-CONN-MS-01-002 CSAF download pipeline|Team Excititor Connectors MSRC|EXCITITOR-CONN-MS-01-001, EXCITITOR-STORAGE-01-003|**DOING (2025-10-19)** Prereqs verified (EXCITITOR-CONN-MS-01-001, EXCITITOR-STORAGE-01-003); drafting fetch/retry plan and storage wiring before implementation of CSAF package download, checksum validation, and quarantine flows.|
|EXCITITOR-CONN-MS-01-003 Trust metadata & provenance hints|Team Excititor Connectors MSRC|EXCITITOR-CONN-MS-01-002, EXCITITOR-POLICY-01-001|TODO Emit cosign/AAD issuer metadata, attach provenance details, and document policy integration.|

View File

@@ -1,7 +1,7 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-OCI-01-001 OCI discovery & auth plumbing|Team Excititor Connectors OCI|EXCITITOR-CONN-ABS-01-001|DONE (2025-10-18) Added connector skeleton, options/validators, discovery caching, cosign/auth descriptors, offline bundle resolution, DI wiring, and regression tests.|
|EXCITITOR-CONN-OCI-01-002 Attestation fetch & verify loop|Team Excititor Connectors OCI|EXCITITOR-CONN-OCI-01-001, EXCITITOR-ATTEST-01-002|DONE (2025-10-18) Added offline/registry fetch services, DSSE retrieval with retries, signature verification callout, and raw persistence coverage.|
|EXCITITOR-CONN-OCI-01-003 Provenance metadata & policy hooks|Team Excititor Connectors OCI|EXCITITOR-CONN-OCI-01-002, EXCITITOR-POLICY-01-001|DONE (2025-10-18) Enriched attestation metadata with provenance hints, cosign expectations, registry auth context, and signature diagnostics for policy consumption.|
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-OCI-01-001 OCI discovery & auth plumbing|Team Excititor Connectors OCI|EXCITITOR-CONN-ABS-01-001|DONE (2025-10-18) Added connector skeleton, options/validators, discovery caching, cosign/auth descriptors, offline bundle resolution, DI wiring, and regression tests.|
|EXCITITOR-CONN-OCI-01-002 Attestation fetch & verify loop|Team Excititor Connectors OCI|EXCITITOR-CONN-OCI-01-001, EXCITITOR-ATTEST-01-002|DONE (2025-10-18) Added offline/registry fetch services, DSSE retrieval with retries, signature verification callout, and raw persistence coverage.|
|EXCITITOR-CONN-OCI-01-003 Provenance metadata & policy hooks|Team Excititor Connectors OCI|EXCITITOR-CONN-OCI-01-002, EXCITITOR-POLICY-01-001|DONE (2025-10-18) Enriched attestation metadata with provenance hints, cosign expectations, registry auth context, and signature diagnostics for policy consumption.|

View File

@@ -1,7 +1,7 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-ORACLE-01-001 Oracle CSAF catalogue discovery|Team Excititor Connectors Oracle|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-19)** Implemented cached Oracle CSAF catalog loader with CPU calendar merge, offline snapshot ingest/persist, options validation + DI wiring, and regression tests; prerequisite EXCITITOR-CONN-ABS-01-001 verified DONE per Sprint 5 log (2025-10-19).|
|EXCITITOR-CONN-ORACLE-01-002 CSAF download & dedupe pipeline|Team Excititor Connectors Oracle|EXCITITOR-CONN-ORACLE-01-001, EXCITITOR-STORAGE-01-003|**DONE (2025-10-19)** Added Oracle CSAF fetch loop with retry/backoff, checksum validation, resume-aware state persistence, digest dedupe, configurable throttling, and raw storage wiring; regression tests cover new ingestion and mismatch handling.|
|EXCITITOR-CONN-ORACLE-01-003 Trust metadata + provenance|Team Excititor Connectors Oracle|EXCITITOR-CONN-ORACLE-01-002, EXCITITOR-POLICY-01-001|TODO Emit Oracle signing metadata (PGP/cosign) and provenance hints for consensus weighting.|
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-ORACLE-01-001 Oracle CSAF catalogue discovery|Team Excititor Connectors Oracle|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-19)** Implemented cached Oracle CSAF catalog loader with CPU calendar merge, offline snapshot ingest/persist, options validation + DI wiring, and regression tests; prerequisite EXCITITOR-CONN-ABS-01-001 verified DONE per Sprint 5 log (2025-10-19).|
|EXCITITOR-CONN-ORACLE-01-002 CSAF download & dedupe pipeline|Team Excititor Connectors Oracle|EXCITITOR-CONN-ORACLE-01-001, EXCITITOR-STORAGE-01-003|**DONE (2025-10-19)** Added Oracle CSAF fetch loop with retry/backoff, checksum validation, resume-aware state persistence, digest dedupe, configurable throttling, and raw storage wiring; regression tests cover new ingestion and mismatch handling.|
|EXCITITOR-CONN-ORACLE-01-003 Trust metadata + provenance|Team Excititor Connectors Oracle|EXCITITOR-CONN-ORACLE-01-002, EXCITITOR-POLICY-01-001|TODO Emit Oracle signing metadata (PGP/cosign) and provenance hints for consensus weighting.|

View File

@@ -1,10 +1,10 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-RH-01-001 Provider metadata discovery|Team Excititor Connectors Red Hat|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Added `RedHatProviderMetadataLoader` with HTTP/ETag caching, offline snapshot handling, and validation; exposed DI helper + tests covering live, cached, and offline scenarios.|
|EXCITITOR-CONN-RH-01-002 Incremental CSAF pulls|Team Excititor Connectors Red Hat|EXCITITOR-CONN-RH-01-001, EXCITITOR-STORAGE-01-003|**DONE (2025-10-17)** Implemented `RedHatCsafConnector` with ROLIE feed parsing, incremental filtering via `context.Since`, CSAF document download + metadata capture, and persistence through `IVexRawDocumentSink`; tests cover live fetch/cache/offline scenarios with ETag handling.|
|EXCITITOR-CONN-RH-01-003 Trust metadata emission|Team Excititor Connectors Red Hat|EXCITITOR-CONN-RH-01-002, EXCITITOR-POLICY-01-001|**DONE (2025-10-17)** Provider metadata loader now emits trust overrides (weight, cosign issuer/pattern, PGP fingerprints) and the connector surfaces provenance hints for policy/consensus layers.|
|EXCITITOR-CONN-RH-01-004 Resume state persistence|Team Excititor Connectors Red Hat|EXCITITOR-CONN-RH-01-002, EXCITITOR-STORAGE-01-003|**DONE (2025-10-17)** Connector now loads/saves resume state via `IVexConnectorStateRepository`, tracking last update timestamp and recent document digests to avoid duplicate CSAF ingestion; regression covers state persistence and duplicate skips.|
|EXCITITOR-CONN-RH-01-005 Worker/WebService integration|Team Excititor Connectors Red Hat|EXCITITOR-CONN-RH-01-002|**DONE (2025-10-17)** Worker/WebService now call `AddRedHatCsafConnector`, register the connector + state repo, and default worker scheduling adds the `excititor:redhat` provider so background jobs and orchestration can activate the connector without extra wiring.|
|EXCITITOR-CONN-RH-01-006 CSAF normalization parity tests|Team Excititor Connectors Red Hat|EXCITITOR-CONN-RH-01-002, EXCITITOR-FMT-CSAF-01-001|**DONE (2025-10-17)** Added RHSA fixture-driven regression verifying CSAF normalizer retains Red Hat product metadata, tracking fields, and timestamps (`rhsa-sample.json` + `CsafNormalizerTests.NormalizeAsync_PreservesRedHatSpecificMetadata`).|
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-RH-01-001 Provider metadata discovery|Team Excititor Connectors Red Hat|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Added `RedHatProviderMetadataLoader` with HTTP/ETag caching, offline snapshot handling, and validation; exposed DI helper + tests covering live, cached, and offline scenarios.|
|EXCITITOR-CONN-RH-01-002 Incremental CSAF pulls|Team Excititor Connectors Red Hat|EXCITITOR-CONN-RH-01-001, EXCITITOR-STORAGE-01-003|**DONE (2025-10-17)** Implemented `RedHatCsafConnector` with ROLIE feed parsing, incremental filtering via `context.Since`, CSAF document download + metadata capture, and persistence through `IVexRawDocumentSink`; tests cover live fetch/cache/offline scenarios with ETag handling.|
|EXCITITOR-CONN-RH-01-003 Trust metadata emission|Team Excititor Connectors Red Hat|EXCITITOR-CONN-RH-01-002, EXCITITOR-POLICY-01-001|**DONE (2025-10-17)** Provider metadata loader now emits trust overrides (weight, cosign issuer/pattern, PGP fingerprints) and the connector surfaces provenance hints for policy/consensus layers.|
|EXCITITOR-CONN-RH-01-004 Resume state persistence|Team Excititor Connectors Red Hat|EXCITITOR-CONN-RH-01-002, EXCITITOR-STORAGE-01-003|**DONE (2025-10-17)** Connector now loads/saves resume state via `IVexConnectorStateRepository`, tracking last update timestamp and recent document digests to avoid duplicate CSAF ingestion; regression covers state persistence and duplicate skips.|
|EXCITITOR-CONN-RH-01-005 Worker/WebService integration|Team Excititor Connectors Red Hat|EXCITITOR-CONN-RH-01-002|**DONE (2025-10-17)** Worker/WebService now call `AddRedHatCsafConnector`, register the connector + state repo, and default worker scheduling adds the `excititor:redhat` provider so background jobs and orchestration can activate the connector without extra wiring.|
|EXCITITOR-CONN-RH-01-006 CSAF normalization parity tests|Team Excititor Connectors Red Hat|EXCITITOR-CONN-RH-01-002, EXCITITOR-FMT-CSAF-01-001|**DONE (2025-10-17)** Added RHSA fixture-driven regression verifying CSAF normalizer retains Red Hat product metadata, tracking fields, and timestamps (`rhsa-sample.json` + `CsafNormalizerTests.NormalizeAsync_PreservesRedHatSpecificMetadata`).|

View File

@@ -1,7 +1,7 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-SUSE-01-001 Rancher hub discovery & auth|Team Excititor Connectors SUSE|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Added Rancher hub options/token provider, discovery metadata loader with offline snapshots + caching, connector shell, DI wiring, and unit tests covering network/offline paths.|
|EXCITITOR-CONN-SUSE-01-002 Checkpointed event ingestion|Team Excititor Connectors SUSE|EXCITITOR-CONN-SUSE-01-001, EXCITITOR-STORAGE-01-003|**DOING (2025-10-19)** Process hub events with resume checkpoints, deduplication, and quarantine path for malformed payloads.<br>2025-10-19: Prereqs EXCITITOR-CONN-SUSE-01-001 & EXCITITOR-STORAGE-01-003 confirmed complete; initiating checkpoint/resume implementation plan.|
|EXCITITOR-CONN-SUSE-01-003 Trust metadata & policy hints|Team Excititor Connectors SUSE|EXCITITOR-CONN-SUSE-01-002, EXCITITOR-POLICY-01-001|TODO Emit provider trust configuration (signers, weight overrides) and attach provenance hints for consensus engine.|
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-SUSE-01-001 Rancher hub discovery & auth|Team Excititor Connectors SUSE|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Added Rancher hub options/token provider, discovery metadata loader with offline snapshots + caching, connector shell, DI wiring, and unit tests covering network/offline paths.|
|EXCITITOR-CONN-SUSE-01-002 Checkpointed event ingestion|Team Excititor Connectors SUSE|EXCITITOR-CONN-SUSE-01-001, EXCITITOR-STORAGE-01-003|**DOING (2025-10-19)** Process hub events with resume checkpoints, deduplication, and quarantine path for malformed payloads.<br>2025-10-19: Prereqs EXCITITOR-CONN-SUSE-01-001 & EXCITITOR-STORAGE-01-003 confirmed complete; initiating checkpoint/resume implementation plan.|
|EXCITITOR-CONN-SUSE-01-003 Trust metadata & policy hints|Team Excititor Connectors SUSE|EXCITITOR-CONN-SUSE-01-002, EXCITITOR-POLICY-01-001|TODO Emit provider trust configuration (signers, weight overrides) and attach provenance hints for consensus engine.|

View File

@@ -1,8 +1,8 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-UBUNTU-01-001 Ubuntu CSAF discovery & channels|Team Excititor Connectors Ubuntu|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Added Ubuntu connector project with configurable channel options, catalog loader (network/offline), DI wiring, and discovery unit tests.|
|EXCITITOR-CONN-UBUNTU-01-002 Incremental fetch & deduplication|Team Excititor Connectors Ubuntu|EXCITITOR-CONN-UBUNTU-01-001, EXCITITOR-STORAGE-01-003|**DOING (2025-10-19)** Fetch CSAF bundles with ETag handling, checksum validation, deduplication, and raw persistence.|
|EXCITITOR-CONN-UBUNTU-01-003 Trust metadata & provenance|Team Excititor Connectors Ubuntu|EXCITITOR-CONN-UBUNTU-01-002, EXCITITOR-POLICY-01-001|TODO Emit Ubuntu signing metadata (GPG fingerprints) plus provenance hints for policy weighting and diagnostics.|
> Remark (2025-10-19, EXCITITOR-CONN-UBUNTU-01-002): Prerequisites EXCITITOR-CONN-UBUNTU-01-001 and EXCITITOR-STORAGE-01-003 verified as **DONE**; advancing to DOING per Wave 0 kickoff.
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-CONN-UBUNTU-01-001 Ubuntu CSAF discovery & channels|Team Excititor Connectors Ubuntu|EXCITITOR-CONN-ABS-01-001|**DONE (2025-10-17)** Added Ubuntu connector project with configurable channel options, catalog loader (network/offline), DI wiring, and discovery unit tests.|
|EXCITITOR-CONN-UBUNTU-01-002 Incremental fetch & deduplication|Team Excititor Connectors Ubuntu|EXCITITOR-CONN-UBUNTU-01-001, EXCITITOR-STORAGE-01-003|**DOING (2025-10-19)** Fetch CSAF bundles with ETag handling, checksum validation, deduplication, and raw persistence.|
|EXCITITOR-CONN-UBUNTU-01-003 Trust metadata & provenance|Team Excititor Connectors Ubuntu|EXCITITOR-CONN-UBUNTU-01-002, EXCITITOR-POLICY-01-001|TODO Emit Ubuntu signing metadata (GPG fingerprints) plus provenance hints for policy weighting and diagnostics.|
> Remark (2025-10-19, EXCITITOR-CONN-UBUNTU-01-002): Prerequisites EXCITITOR-CONN-UBUNTU-01-001 and EXCITITOR-STORAGE-01-003 verified as **DONE**; advancing to DOING per Wave 0 kickoff.

View File

@@ -1,11 +1,11 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-EXPORT-01-001 Export engine orchestration|Team Excititor Export|EXCITITOR-CORE-01-003|DONE (2025-10-15) Export engine scaffolding with cache lookup, data source hooks, and deterministic manifest emission.|
|EXCITITOR-EXPORT-01-002 Cache index & eviction hooks|Team Excititor Export|EXCITITOR-EXPORT-01-001, EXCITITOR-STORAGE-01-003|**DONE (2025-10-16)** Export engine now invalidates cache entries on force refresh, cache services expose prune/invalidate APIs, and storage maintenance trims expired/dangling records with Mongo2Go coverage.|
|EXCITITOR-EXPORT-01-003 Artifact store adapters|Team Excititor Export|EXCITITOR-EXPORT-01-001|**DONE (2025-10-16)** Implemented multi-store pipeline with filesystem, S3-compatible, and offline bundle adapters (hash verification + manifest/zip output) plus unit coverage and DI hooks.|
|EXCITITOR-EXPORT-01-004 Attestation handoff integration|Team Excititor Export|EXCITITOR-EXPORT-01-001, EXCITITOR-ATTEST-01-001|**DONE (2025-10-17)** Export engine now invokes attestation client, logs diagnostics, and persists Rekor/envelope metadata on manifests; regression coverage added in `ExportEngineTests.ExportAsync_AttachesAttestationMetadata`.|
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-EXPORT-01-001 Export engine orchestration|Team Excititor Export|EXCITITOR-CORE-01-003|DONE (2025-10-15) Export engine scaffolding with cache lookup, data source hooks, and deterministic manifest emission.|
|EXCITITOR-EXPORT-01-002 Cache index & eviction hooks|Team Excititor Export|EXCITITOR-EXPORT-01-001, EXCITITOR-STORAGE-01-003|**DONE (2025-10-16)** Export engine now invalidates cache entries on force refresh, cache services expose prune/invalidate APIs, and storage maintenance trims expired/dangling records with Mongo2Go coverage.|
|EXCITITOR-EXPORT-01-003 Artifact store adapters|Team Excititor Export|EXCITITOR-EXPORT-01-001|**DONE (2025-10-16)** Implemented multi-store pipeline with filesystem, S3-compatible, and offline bundle adapters (hash verification + manifest/zip output) plus unit coverage and DI hooks.|
|EXCITITOR-EXPORT-01-004 Attestation handoff integration|Team Excititor Export|EXCITITOR-EXPORT-01-001, EXCITITOR-ATTEST-01-001|**DONE (2025-10-17)** Export engine now invokes attestation client, logs diagnostics, and persists Rekor/envelope metadata on manifests; regression coverage added in `ExportEngineTests.ExportAsync_AttachesAttestationMetadata`.|
|EXCITITOR-EXPORT-01-005 Score & resolve envelope surfaces|Team Excititor Export|EXCITITOR-EXPORT-01-004, EXCITITOR-CORE-02-001|**DONE (2025-10-21)** Export engine now canonicalizes consensus/score envelopes, persists their SHA-256 digests into manifests/attestation metadata, and regression tests validate metadata wiring via `ExportEngineTests`.|
|EXCITITOR-EXPORT-01-006 Quiet provenance packaging|Team Excititor Export|EXCITITOR-EXPORT-01-005, POLICY-CORE-09-005|**DONE (2025-10-21)** Export manifests now carry quiet-provenance entries (statement digests, signers, justification codes); metadata flows into offline bundles & attestations with regression coverage in `ExportEngineTests`.|
|EXCITITOR-EXPORT-01-007 Mirror bundle + domain manifest|Team Excititor Export|EXCITITOR-EXPORT-01-006|**DONE (2025-10-21)** Created per-domain mirror bundles with consensus/score artefacts, published signed-ready manifests/index for downstream Excititor sync, and added regression coverage.|

View File

@@ -1,7 +1,7 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-FMT-CSAF-01-001 CSAF normalizer foundation|Team Excititor Formats|EXCITITOR-CORE-01-001|**DONE (2025-10-17)** Implemented CSAF normalizer + DI hook, parsing tracking metadata, product tree branches/full names, and mapping product statuses into canonical `VexClaim`s with baseline precedence. Regression added in `CsafNormalizerTests`.|
|EXCITITOR-FMT-CSAF-01-002 Status/justification mapping|Team Excititor Formats|EXCITITOR-FMT-CSAF-01-001, EXCITITOR-POLICY-01-001|**DOING (2025-10-19)** Prereqs EXCITITOR-FMT-CSAF-01-001 & EXCITITOR-POLICY-01-001 verified DONE; starting normalization of `product_status`/`justification` values with policy-aligned diagnostics.|
|EXCITITOR-FMT-CSAF-01-003 CSAF export adapter|Team Excititor Formats|EXCITITOR-EXPORT-01-001, EXCITITOR-FMT-CSAF-01-001|**DOING (2025-10-19)** Prereqs EXCITITOR-EXPORT-01-001 & EXCITITOR-FMT-CSAF-01-001 confirmed DONE; drafting deterministic CSAF exporter and manifest metadata flow.|
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-FMT-CSAF-01-001 CSAF normalizer foundation|Team Excititor Formats|EXCITITOR-CORE-01-001|**DONE (2025-10-17)** Implemented CSAF normalizer + DI hook, parsing tracking metadata, product tree branches/full names, and mapping product statuses into canonical `VexClaim`s with baseline precedence. Regression added in `CsafNormalizerTests`.|
|EXCITITOR-FMT-CSAF-01-002 Status/justification mapping|Team Excititor Formats|EXCITITOR-FMT-CSAF-01-001, EXCITITOR-POLICY-01-001|**DOING (2025-10-19)** Prereqs EXCITITOR-FMT-CSAF-01-001 & EXCITITOR-POLICY-01-001 verified DONE; starting normalization of `product_status`/`justification` values with policy-aligned diagnostics.|
|EXCITITOR-FMT-CSAF-01-003 CSAF export adapter|Team Excititor Formats|EXCITITOR-EXPORT-01-001, EXCITITOR-FMT-CSAF-01-001|**DOING (2025-10-19)** Prereqs EXCITITOR-EXPORT-01-001 & EXCITITOR-FMT-CSAF-01-001 confirmed DONE; drafting deterministic CSAF exporter and manifest metadata flow.|

View File

@@ -1,7 +1,7 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-FMT-CYCLONE-01-001 CycloneDX VEX normalizer|Team Excititor Formats|EXCITITOR-CORE-01-001|**DONE (2025-10-17)** CycloneDX normalizer parses `analysis` data, resolves component references, and emits canonical `VexClaim`s; regression lives in `CycloneDxNormalizerTests`.|
|EXCITITOR-FMT-CYCLONE-01-002 Component reference reconciliation|Team Excititor Formats|EXCITITOR-FMT-CYCLONE-01-001|**DOING (2025-10-19)** Prereq EXCITITOR-FMT-CYCLONE-01-001 confirmed DONE; proceeding with reference reconciliation helpers and diagnostics for missing SBOM links.|
|EXCITITOR-FMT-CYCLONE-01-003 CycloneDX export serializer|Team Excititor Formats|EXCITITOR-EXPORT-01-001, EXCITITOR-FMT-CYCLONE-01-001|**DOING (2025-10-19)** Prereqs EXCITITOR-EXPORT-01-001 & EXCITITOR-FMT-CYCLONE-01-001 verified DONE; initiating deterministic CycloneDX VEX exporter work.|
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-FMT-CYCLONE-01-001 CycloneDX VEX normalizer|Team Excititor Formats|EXCITITOR-CORE-01-001|**DONE (2025-10-17)** CycloneDX normalizer parses `analysis` data, resolves component references, and emits canonical `VexClaim`s; regression lives in `CycloneDxNormalizerTests`.|
|EXCITITOR-FMT-CYCLONE-01-002 Component reference reconciliation|Team Excititor Formats|EXCITITOR-FMT-CYCLONE-01-001|**DOING (2025-10-19)** Prereq EXCITITOR-FMT-CYCLONE-01-001 confirmed DONE; proceeding with reference reconciliation helpers and diagnostics for missing SBOM links.|
|EXCITITOR-FMT-CYCLONE-01-003 CycloneDX export serializer|Team Excititor Formats|EXCITITOR-EXPORT-01-001, EXCITITOR-FMT-CYCLONE-01-001|**DOING (2025-10-19)** Prereqs EXCITITOR-EXPORT-01-001 & EXCITITOR-FMT-CYCLONE-01-001 verified DONE; initiating deterministic CycloneDX VEX exporter work.|

View File

@@ -1,7 +1,7 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-FMT-OPENVEX-01-001 OpenVEX normalizer|Team Excititor Formats|EXCITITOR-CORE-01-001|**DONE (2025-10-17)** OpenVEX normalizer parses statements/products, maps status/justification, and surfaces provenance metadata; coverage in `OpenVexNormalizerTests`.|
|EXCITITOR-FMT-OPENVEX-01-002 Statement merge utilities|Team Excititor Formats|EXCITITOR-FMT-OPENVEX-01-001|**DOING (2025-10-19)** Prereq EXCITITOR-FMT-OPENVEX-01-001 confirmed DONE; building deterministic merge reducers with policy diagnostics.|
|EXCITITOR-FMT-OPENVEX-01-003 OpenVEX export writer|Team Excititor Formats|EXCITITOR-EXPORT-01-001, EXCITITOR-FMT-OPENVEX-01-001|**DOING (2025-10-19)** Prereqs EXCITITOR-EXPORT-01-001 & EXCITITOR-FMT-OPENVEX-01-001 verified DONE; starting canonical OpenVEX exporter with stable ordering/SBOM references.|
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-FMT-OPENVEX-01-001 OpenVEX normalizer|Team Excititor Formats|EXCITITOR-CORE-01-001|**DONE (2025-10-17)** OpenVEX normalizer parses statements/products, maps status/justification, and surfaces provenance metadata; coverage in `OpenVexNormalizerTests`.|
|EXCITITOR-FMT-OPENVEX-01-002 Statement merge utilities|Team Excititor Formats|EXCITITOR-FMT-OPENVEX-01-001|**DOING (2025-10-19)** Prereq EXCITITOR-FMT-OPENVEX-01-001 confirmed DONE; building deterministic merge reducers with policy diagnostics.|
|EXCITITOR-FMT-OPENVEX-01-003 OpenVEX export writer|Team Excititor Formats|EXCITITOR-EXPORT-01-001, EXCITITOR-FMT-OPENVEX-01-001|**DOING (2025-10-19)** Prereqs EXCITITOR-EXPORT-01-001 & EXCITITOR-FMT-OPENVEX-01-001 verified DONE; starting canonical OpenVEX exporter with stable ordering/SBOM references.|

View File

@@ -1,11 +1,11 @@
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
If you are working on this file you need to read docs/modules/excititor/ARCHITECTURE.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
|EXCITITOR-POLICY-01-001 Policy schema & binding|Team Excititor Policy|EXCITITOR-CORE-01-001|DONE (2025-10-15) Established `VexPolicyOptions`, options binding, and snapshot provider covering baseline weights/overrides.|
|EXCITITOR-POLICY-01-002 Policy evaluator service|Team Excititor Policy|EXCITITOR-POLICY-01-001|DONE (2025-10-15) `VexPolicyEvaluator` exposes immutable snapshots to consensus and normalizes rejection reasons.|
|EXCITITOR-POLICY-01-003 Operator diagnostics & docs|Team Excititor Policy|EXCITITOR-POLICY-01-001|**DONE (2025-10-16)** Surface structured diagnostics (CLI/WebService) and author policy upgrade guidance in docs/ARCHITECTURE_EXCITITOR.md appendix.<br>2025-10-16: Added `IVexPolicyDiagnostics`/`VexPolicyDiagnosticsReport`, sorted issue ordering, recommendations, and appendix guidance. Tests: `dotnet test src/Excititor/__Tests/StellaOps.Excititor.Core.Tests/StellaOps.Excititor.Core.Tests.csproj`.|
|EXCITITOR-POLICY-01-003 Operator diagnostics & docs|Team Excititor Policy|EXCITITOR-POLICY-01-001|**DONE (2025-10-16)** Surface structured diagnostics (CLI/WebService) and author policy upgrade guidance in docs/modules/excititor/ARCHITECTURE.md appendix.<br>2025-10-16: Added `IVexPolicyDiagnostics`/`VexPolicyDiagnosticsReport`, sorted issue ordering, recommendations, and appendix guidance. Tests: `dotnet test src/Excititor/__Tests/StellaOps.Excititor.Core.Tests/StellaOps.Excititor.Core.Tests.csproj`.|
|EXCITITOR-POLICY-01-004 Policy schema validation & YAML binding|Team Excititor Policy|EXCITITOR-POLICY-01-001|**DONE (2025-10-16)** Added strongly-typed YAML/JSON binding, schema validation, and deterministic diagnostics for operator-supplied policy bundles.|
|EXCITITOR-POLICY-01-005 Policy change tracking & telemetry|Team Excititor Policy|EXCITITOR-POLICY-01-002|**DONE (2025-10-16)** Emit revision history, expose snapshot digests via CLI/WebService, and add structured logging/metrics for policy reloads.<br>2025-10-16: `VexPolicySnapshot` now carries revision/digest, provider logs reloads, `vex.policy.reloads` metric emitted, binder/diagnostics expose digest metadata. Tests: `dotnet test src/Excititor/__Tests/StellaOps.Excititor.Core.Tests/StellaOps.Excititor.Core.Tests.csproj`.|
|EXCITITOR-POLICY-02-001 Scoring coefficients & weight ceilings|Team Excititor Policy|EXCITITOR-POLICY-01-004|DONE (2025-10-19) Added `weights.ceiling` + `scoring.{alpha,beta}` options with normalization warnings, extended consensus policy/digest, refreshed docs (`docs/ARCHITECTURE_EXCITITOR.md`, `docs/EXCITITOR_SCORRING.md`), and validated via `dotnet test` for core/policy suites.|
|EXCITITOR-POLICY-02-001 Scoring coefficients & weight ceilings|Team Excititor Policy|EXCITITOR-POLICY-01-004|DONE (2025-10-19) Added `weights.ceiling` + `scoring.{alpha,beta}` options with normalization warnings, extended consensus policy/digest, refreshed docs (`docs/modules/excititor/ARCHITECTURE.md`, `docs/modules/excititor/scoring.md`), and validated via `dotnet test` for core/policy suites.|
|EXCITITOR-POLICY-02-002 Diagnostics for scoring signals|Team Excititor Policy|EXCITITOR-POLICY-02-001|BACKLOG Update diagnostics reports to surface missing severity/KEV/EPSS mappings, coefficient overrides, and provide actionable recommendations for policy tuning.|

View File

@@ -1,87 +1,87 @@
using System;
using System.Collections.Immutable;
using System.Linq;
namespace StellaOps.Excititor.Policy;
public interface IVexPolicyDiagnostics
{
VexPolicyDiagnosticsReport GetDiagnostics();
}
public sealed record VexPolicyDiagnosticsReport(
string Version,
string RevisionId,
string Digest,
int ErrorCount,
int WarningCount,
DateTimeOffset GeneratedAt,
ImmutableArray<VexPolicyIssue> Issues,
ImmutableArray<string> Recommendations,
ImmutableDictionary<string, double> ActiveOverrides);
public sealed class VexPolicyDiagnostics : IVexPolicyDiagnostics
{
private readonly IVexPolicyProvider _policyProvider;
private readonly TimeProvider _timeProvider;
public VexPolicyDiagnostics(
IVexPolicyProvider policyProvider,
TimeProvider? timeProvider = null)
{
_policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public VexPolicyDiagnosticsReport GetDiagnostics()
{
var snapshot = _policyProvider.GetSnapshot();
var issues = snapshot.Issues;
var errorCount = issues.Count(static issue => issue.Severity == VexPolicyIssueSeverity.Error);
var warningCount = issues.Count(static issue => issue.Severity == VexPolicyIssueSeverity.Warning);
var overrides = snapshot.ConsensusOptions.ProviderOverrides
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
.ToImmutableDictionary();
var recommendations = BuildRecommendations(errorCount, warningCount, overrides);
return new VexPolicyDiagnosticsReport(
snapshot.Version,
snapshot.RevisionId,
snapshot.Digest,
errorCount,
warningCount,
_timeProvider.GetUtcNow(),
issues,
recommendations,
overrides);
}
private static ImmutableArray<string> BuildRecommendations(
int errorCount,
int warningCount,
ImmutableDictionary<string, double> overrides)
{
var messages = ImmutableArray.CreateBuilder<string>();
if (errorCount > 0)
{
messages.Add("Resolve policy errors before running consensus; defaults are used while errors persist.");
}
if (warningCount > 0)
{
messages.Add("Review policy warnings via CLI/Web diagnostics and adjust configuration as needed.");
}
if (overrides.Count > 0)
{
messages.Add($"Provider overrides active for: {string.Join(", ", overrides.Keys)}.");
}
messages.Add("Refer to docs/ARCHITECTURE_EXCITITOR.md for policy upgrade and diagnostics guidance.");
return messages.ToImmutable();
}
}
using System;
using System.Collections.Immutable;
using System.Linq;
namespace StellaOps.Excititor.Policy;
public interface IVexPolicyDiagnostics
{
VexPolicyDiagnosticsReport GetDiagnostics();
}
public sealed record VexPolicyDiagnosticsReport(
string Version,
string RevisionId,
string Digest,
int ErrorCount,
int WarningCount,
DateTimeOffset GeneratedAt,
ImmutableArray<VexPolicyIssue> Issues,
ImmutableArray<string> Recommendations,
ImmutableDictionary<string, double> ActiveOverrides);
public sealed class VexPolicyDiagnostics : IVexPolicyDiagnostics
{
private readonly IVexPolicyProvider _policyProvider;
private readonly TimeProvider _timeProvider;
public VexPolicyDiagnostics(
IVexPolicyProvider policyProvider,
TimeProvider? timeProvider = null)
{
_policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
_timeProvider = timeProvider ?? TimeProvider.System;
}
public VexPolicyDiagnosticsReport GetDiagnostics()
{
var snapshot = _policyProvider.GetSnapshot();
var issues = snapshot.Issues;
var errorCount = issues.Count(static issue => issue.Severity == VexPolicyIssueSeverity.Error);
var warningCount = issues.Count(static issue => issue.Severity == VexPolicyIssueSeverity.Warning);
var overrides = snapshot.ConsensusOptions.ProviderOverrides
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
.ToImmutableDictionary();
var recommendations = BuildRecommendations(errorCount, warningCount, overrides);
return new VexPolicyDiagnosticsReport(
snapshot.Version,
snapshot.RevisionId,
snapshot.Digest,
errorCount,
warningCount,
_timeProvider.GetUtcNow(),
issues,
recommendations,
overrides);
}
private static ImmutableArray<string> BuildRecommendations(
int errorCount,
int warningCount,
ImmutableDictionary<string, double> overrides)
{
var messages = ImmutableArray.CreateBuilder<string>();
if (errorCount > 0)
{
messages.Add("Resolve policy errors before running consensus; defaults are used while errors persist.");
}
if (warningCount > 0)
{
messages.Add("Review policy warnings via CLI/Web diagnostics and adjust configuration as needed.");
}
if (overrides.Count > 0)
{
messages.Add($"Provider overrides active for: {string.Join(", ", overrides.Keys)}.");
}
messages.Add("Refer to docs/modules/excititor/architecture.md for policy upgrade and diagnostics guidance.");
return messages.ToImmutable();
}
}

View File

@@ -1,169 +1,169 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Policy;
using System.Diagnostics.Metrics;
namespace StellaOps.Excititor.Core.Tests;
public class VexPolicyDiagnosticsTests
{
[Fact]
public void GetDiagnostics_ReportsCountsRecommendationsAndOverrides()
{
var overrides = new[]
{
new KeyValuePair<string, double>("provider-a", 0.8),
new KeyValuePair<string, double>("provider-b", 0.6),
};
var snapshot = new VexPolicySnapshot(
"custom/v1",
new VexConsensusPolicyOptions(
version: "custom/v1",
providerOverrides: overrides),
new BaselineVexConsensusPolicy(),
ImmutableArray.Create(
new VexPolicyIssue("sample.error", "Blocking issue.", VexPolicyIssueSeverity.Error),
new VexPolicyIssue("sample.warning", "Non-blocking issue.", VexPolicyIssueSeverity.Warning)),
"rev-test",
"ABCDEF");
var fakeProvider = new FakePolicyProvider(snapshot);
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 16, 17, 0, 0, TimeSpan.Zero));
var diagnostics = new VexPolicyDiagnostics(fakeProvider, fakeTime);
var report = diagnostics.GetDiagnostics();
Assert.Equal("custom/v1", report.Version);
Assert.Equal("rev-test", report.RevisionId);
Assert.Equal("ABCDEF", report.Digest);
Assert.Equal(1, report.ErrorCount);
Assert.Equal(1, report.WarningCount);
Assert.Equal(fakeTime.GetUtcNow(), report.GeneratedAt);
Assert.Collection(report.Issues,
issue => Assert.Equal("sample.error", issue.Code),
issue => Assert.Equal("sample.warning", issue.Code));
Assert.Equal(new[] { "provider-a", "provider-b" }, report.ActiveOverrides.Keys.OrderBy(static key => key, StringComparer.Ordinal));
Assert.Contains(report.Recommendations, message => message.Contains("Resolve policy errors", StringComparison.OrdinalIgnoreCase));
Assert.Contains(report.Recommendations, message => message.Contains("provider-a", StringComparison.OrdinalIgnoreCase));
Assert.Contains(report.Recommendations, message => message.Contains("docs/ARCHITECTURE_EXCITITOR.md", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void GetDiagnostics_WhenNoIssues_StillReturnsDefaultRecommendation()
{
var fakeProvider = new FakePolicyProvider(VexPolicySnapshot.Default);
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 16, 17, 0, 0, TimeSpan.Zero));
var diagnostics = new VexPolicyDiagnostics(fakeProvider, fakeTime);
var report = diagnostics.GetDiagnostics();
Assert.Equal(0, report.ErrorCount);
Assert.Equal(0, report.WarningCount);
Assert.Empty(report.ActiveOverrides);
Assert.Single(report.Recommendations);
}
[Fact]
public void PolicyProvider_ComputesRevisionAndDigest_AndEmitsTelemetry()
{
using var listener = new MeterListener();
var reloadMeasurements = 0;
string? lastRevision = null;
listener.InstrumentPublished += (instrument, _) =>
{
if (instrument.Meter.Name == "StellaOps.Excititor.Policy" &&
instrument.Name == "vex.policy.reloads")
{
listener.EnableMeasurementEvents(instrument);
}
};
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
{
reloadMeasurements++;
foreach (var tag in tags)
{
if (tag.Key is "revision" && tag.Value is string revision)
{
lastRevision = revision;
break;
}
}
});
listener.Start();
var optionsMonitor = new MutableOptionsMonitor<VexPolicyOptions>(new VexPolicyOptions());
var provider = new VexPolicyProvider(optionsMonitor, NullLogger<VexPolicyProvider>.Instance);
var snapshot1 = provider.GetSnapshot();
Assert.Equal("rev-1", snapshot1.RevisionId);
Assert.False(string.IsNullOrWhiteSpace(snapshot1.Digest));
var snapshot2 = provider.GetSnapshot();
Assert.Equal("rev-1", snapshot2.RevisionId);
Assert.Equal(snapshot1.Digest, snapshot2.Digest);
optionsMonitor.Update(new VexPolicyOptions
{
ProviderOverrides = new Dictionary<string, double>
{
["provider-a"] = 0.4
}
});
var snapshot3 = provider.GetSnapshot();
Assert.Equal("rev-2", snapshot3.RevisionId);
Assert.NotEqual(snapshot1.Digest, snapshot3.Digest);
listener.Dispose();
Assert.True(reloadMeasurements >= 2);
Assert.Equal("rev-2", lastRevision);
}
private sealed class FakePolicyProvider : IVexPolicyProvider
{
private readonly VexPolicySnapshot _snapshot;
public FakePolicyProvider(VexPolicySnapshot snapshot)
{
_snapshot = snapshot;
}
public VexPolicySnapshot GetSnapshot() => _snapshot;
}
private sealed class MutableOptionsMonitor<T> : IOptionsMonitor<T>
{
private T _value;
public MutableOptionsMonitor(T value)
{
_value = value;
}
public T CurrentValue => _value;
public T Get(string? name) => _value;
public void Update(T newValue) => _value = newValue;
public IDisposable OnChange(Action<T, string?> listener) => NullDisposable.Instance;
private sealed class NullDisposable : IDisposable
{
public static readonly NullDisposable Instance = new();
public void Dispose()
{
}
}
}
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Policy;
using System.Diagnostics.Metrics;
namespace StellaOps.Excititor.Core.Tests;
public class VexPolicyDiagnosticsTests
{
[Fact]
public void GetDiagnostics_ReportsCountsRecommendationsAndOverrides()
{
var overrides = new[]
{
new KeyValuePair<string, double>("provider-a", 0.8),
new KeyValuePair<string, double>("provider-b", 0.6),
};
var snapshot = new VexPolicySnapshot(
"custom/v1",
new VexConsensusPolicyOptions(
version: "custom/v1",
providerOverrides: overrides),
new BaselineVexConsensusPolicy(),
ImmutableArray.Create(
new VexPolicyIssue("sample.error", "Blocking issue.", VexPolicyIssueSeverity.Error),
new VexPolicyIssue("sample.warning", "Non-blocking issue.", VexPolicyIssueSeverity.Warning)),
"rev-test",
"ABCDEF");
var fakeProvider = new FakePolicyProvider(snapshot);
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 16, 17, 0, 0, TimeSpan.Zero));
var diagnostics = new VexPolicyDiagnostics(fakeProvider, fakeTime);
var report = diagnostics.GetDiagnostics();
Assert.Equal("custom/v1", report.Version);
Assert.Equal("rev-test", report.RevisionId);
Assert.Equal("ABCDEF", report.Digest);
Assert.Equal(1, report.ErrorCount);
Assert.Equal(1, report.WarningCount);
Assert.Equal(fakeTime.GetUtcNow(), report.GeneratedAt);
Assert.Collection(report.Issues,
issue => Assert.Equal("sample.error", issue.Code),
issue => Assert.Equal("sample.warning", issue.Code));
Assert.Equal(new[] { "provider-a", "provider-b" }, report.ActiveOverrides.Keys.OrderBy(static key => key, StringComparer.Ordinal));
Assert.Contains(report.Recommendations, message => message.Contains("Resolve policy errors", StringComparison.OrdinalIgnoreCase));
Assert.Contains(report.Recommendations, message => message.Contains("provider-a", StringComparison.OrdinalIgnoreCase));
Assert.Contains(report.Recommendations, message => message.Contains("docs/modules/excititor/architecture.md", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void GetDiagnostics_WhenNoIssues_StillReturnsDefaultRecommendation()
{
var fakeProvider = new FakePolicyProvider(VexPolicySnapshot.Default);
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 16, 17, 0, 0, TimeSpan.Zero));
var diagnostics = new VexPolicyDiagnostics(fakeProvider, fakeTime);
var report = diagnostics.GetDiagnostics();
Assert.Equal(0, report.ErrorCount);
Assert.Equal(0, report.WarningCount);
Assert.Empty(report.ActiveOverrides);
Assert.Single(report.Recommendations);
}
[Fact]
public void PolicyProvider_ComputesRevisionAndDigest_AndEmitsTelemetry()
{
using var listener = new MeterListener();
var reloadMeasurements = 0;
string? lastRevision = null;
listener.InstrumentPublished += (instrument, _) =>
{
if (instrument.Meter.Name == "StellaOps.Excititor.Policy" &&
instrument.Name == "vex.policy.reloads")
{
listener.EnableMeasurementEvents(instrument);
}
};
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
{
reloadMeasurements++;
foreach (var tag in tags)
{
if (tag.Key is "revision" && tag.Value is string revision)
{
lastRevision = revision;
break;
}
}
});
listener.Start();
var optionsMonitor = new MutableOptionsMonitor<VexPolicyOptions>(new VexPolicyOptions());
var provider = new VexPolicyProvider(optionsMonitor, NullLogger<VexPolicyProvider>.Instance);
var snapshot1 = provider.GetSnapshot();
Assert.Equal("rev-1", snapshot1.RevisionId);
Assert.False(string.IsNullOrWhiteSpace(snapshot1.Digest));
var snapshot2 = provider.GetSnapshot();
Assert.Equal("rev-1", snapshot2.RevisionId);
Assert.Equal(snapshot1.Digest, snapshot2.Digest);
optionsMonitor.Update(new VexPolicyOptions
{
ProviderOverrides = new Dictionary<string, double>
{
["provider-a"] = 0.4
}
});
var snapshot3 = provider.GetSnapshot();
Assert.Equal("rev-2", snapshot3.RevisionId);
Assert.NotEqual(snapshot1.Digest, snapshot3.Digest);
listener.Dispose();
Assert.True(reloadMeasurements >= 2);
Assert.Equal("rev-2", lastRevision);
}
private sealed class FakePolicyProvider : IVexPolicyProvider
{
private readonly VexPolicySnapshot _snapshot;
public FakePolicyProvider(VexPolicySnapshot snapshot)
{
_snapshot = snapshot;
}
public VexPolicySnapshot GetSnapshot() => _snapshot;
}
private sealed class MutableOptionsMonitor<T> : IOptionsMonitor<T>
{
private T _value;
public MutableOptionsMonitor(T value)
{
_value = value;
}
public T CurrentValue => _value;
public T Get(string? name) => _value;
public void Update(T newValue) => _value = newValue;
public IDisposable OnChange(Action<T, string?> listener) => NullDisposable.Instance;
private sealed class NullDisposable : IDisposable
{
public static readonly NullDisposable Instance = new();
public void Dispose()
{
}
}
}
}

View File

@@ -10,4 +10,4 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| EXPORT-ATTEST-75-001 | TODO | Attestation Bundle Guild, CLI Attestor Guild | EXPORT-ATTEST-74-001 | Provide CLI command `stella attest bundle verify/import` for air-gap usage. | CLI verifies/signatures; import seeds attestor store; tests cover corrupted bundle. |
| EXPORT-ATTEST-75-002 | TODO | Attestation Bundle Guild, Docs Guild | EXPORT-ATTEST-75-001 | Document `/docs/attestor/airgap.md` with bundle workflows and verification steps. | Doc merged with banner; examples verified. |
| EXPORT-ATTEST-75-002 | TODO | Attestation Bundle Guild, Docs Guild | EXPORT-ATTEST-75-001 | Document `/docs/modules/attestor/airgap.md` with bundle workflows and verification steps. | Doc merged with banner; examples verified. |

View File

@@ -1,140 +0,0 @@
Heres a quick, practical idea to make your version-range modeling cleaner and faster to query.
![A simple diagram showing a Vulnerability doc with an embedded normalizedVersions array next to a pipeline icon labeled “simpler aggregations”.](https://images.unsplash.com/photo-1515879218367-8466d910aaa4?q=80\&w=1470\&auto=format\&fit=crop)
# Rethinking `SemVerRangeBuilder` + MongoDB
**Problem (today):** Version normalization rules live as a nested object (and often as a bespoke structure per source). This can force awkward `$objectToArray`, `$map`, and conditional logic in pipelines when you need to:
* match “is version X affected?”
* flatten ranges for analytics
* de-duplicate across sources
**Proposal:** Store *normalized version rules as an embedded collection (array of small docs)* instead of a single nested object.
## Minimal background
* **SemVer normalization**: converting all source-specific version notations into a single, strict representation (e.g., `>=1.2.3 <2.0.0`, exact pins, wildcards).
* **Embedded collection**: an array of consistently shaped items inside the parent doc—great for `$unwind`-centric analytics and direct matches.
## Suggested shape
```json
{
"_id": "VULN-123",
"packageId": "pkg:npm/lodash",
"source": "NVD",
"normalizedVersions": [
{
"scheme": "semver",
"type": "range", // "range" | "exact" | "lt" | "lte" | "gt" | "gte"
"min": "1.2.3", // optional
"minInclusive": true, // optional
"max": "2.0.0", // optional
"maxInclusive": false, // optional
"notes": "from GHSA GHSA-xxxx" // traceability
},
{
"scheme": "semver",
"type": "exact",
"value": "1.5.0"
}
],
"metadata": { "ingestedAt": "2025-10-10T12:00:00Z" }
}
```
### Why this helps
* **Simpler queries**
* *Is v affected?*
```js
db.vulns.aggregate([
{ $match: { packageId: "pkg:npm/lodash" } },
{ $unwind: "$normalizedVersions" },
{ $match: {
$or: [
{ "normalizedVersions.type": "exact", "normalizedVersions.value": "1.5.0" },
{ "normalizedVersions.type": "range",
"normalizedVersions.min": { $lte: "1.5.0" },
"normalizedVersions.max": { $gt: "1.5.0" } }
]
}},
{ $project: { _id: 1 } }
])
```
* No `$objectToArray`, fewer `$cond`s.
* **Cheaper storage**
* Arrays of tiny docs compress well and avoid wide nested structures with many nulls/keys.
* **Easier dedup/merge**
* `$unwind` → normalize → `$group` by `{scheme,type,min,max,value}` to collapse equivalent rules across sources.
## Builder changes (`SemVerRangeBuilder`)
* **Emit items, not a monolith**: have the builder return `IEnumerable<NormalizedVersionRule>`.
* **Normalize early**: resolve “aliases” (`1.2.x`, `^1.2.3`, distro styles) into canonical `(type,min,max,…)` before persistence.
* **Traceability**: include `notes`/`sourceRef` on each rule so you can re-materialize provenance during audits.
* **Lean projection helper**: when you only need normalized rules (and not the intermediate primitives), prefer `SemVerRangeRuleBuilder.BuildNormalizedRules(rawRange, patchedVersion, provenanceNote)` to skip manual projections.
### C# sketch
```csharp
public record NormalizedVersionRule(
string Scheme, // "semver"
string Type, // "range" | "exact" | ...
string? Min = null,
bool? MinInclusive = null,
string? Max = null,
bool? MaxInclusive = null,
string? Value = null,
string? Notes = null
);
public static class SemVerRangeBuilder
{
public static IEnumerable<NormalizedVersionRule> Build(string raw)
{
// parse raw (^1.2.3, 1.2.x, <=2.0.0, etc.)
// yield canonical rules:
yield return new NormalizedVersionRule(
Scheme: "semver",
Type: "range",
Min: "1.2.3",
MinInclusive: true,
Max: "2.0.0",
MaxInclusive: false,
Notes: "nvd:ABC-123"
);
}
}
```
## Aggregation patterns you unlock
* **Fast “affected version” lookups** via `$unwind + $match` (can complement with a computed sort key).
* **Rollups**: count of vulns per `(major,minor)` by mapping each rule into bucketed segments.
* **Cross-source reconciliation**: group identical rules to de-duplicate.
## Indexing tips
* Compound index on `{ packageId: 1, "normalizedVersions.scheme": 1, "normalizedVersions.type": 1 }`.
* If lookups by exact value are common: add a sparse index on `"normalizedVersions.value"`.
## Migration path (safe + incremental)
1. **Dual-write**: keep old nested object while writing the new `normalizedVersions` array.
2. **Backfill** existing docs with a one-time script using your current builder.
3. **Cutover** queries/aggregations to the new path (behind a feature flag).
4. **Clean up** old field after soak.
If you want, I can draft:
* a one-time Mongo backfill script,
* the new EF/Mongo C# POCOs, and
* a test matrix (edge cases: prerelease tags, build metadata, `0.*` semantics, distro-style ranges).

View File

@@ -1,4 +1,4 @@
# StellaOps.Notify.WebService — Agent Charter
## Mission
Implement Notify control plane per `docs/ARCHITECTURE_NOTIFY.md`.
# StellaOps.Notify.WebService — Agent Charter
## Mission
Implement Notify control plane per `docs/modules/notify/ARCHITECTURE.md`.

View File

@@ -1,4 +1,4 @@
# StellaOps.Notify.Worker — Agent Charter
## Mission
Consume events, evaluate rules, and dispatch deliveries per `docs/ARCHITECTURE_NOTIFY.md`.
# StellaOps.Notify.Worker — Agent Charter
## Mission
Consume events, evaluate rules, and dispatch deliveries per `docs/modules/notify/ARCHITECTURE.md`.

View File

@@ -1,4 +1,4 @@
# StellaOps.Notify.Connectors.Email — Agent Charter
## Mission
Implement SMTP connector plug-in per `docs/ARCHITECTURE_NOTIFY.md`.
# StellaOps.Notify.Connectors.Email — Agent Charter
## Mission
Implement SMTP connector plug-in per `docs/modules/notify/ARCHITECTURE.md`.

View File

@@ -1,4 +1,4 @@
# StellaOps.Notify.Connectors.Slack — Agent Charter
## Mission
Deliver Slack connector plug-in per `docs/ARCHITECTURE_NOTIFY.md`.
# StellaOps.Notify.Connectors.Slack — Agent Charter
## Mission
Deliver Slack connector plug-in per `docs/modules/notify/ARCHITECTURE.md`.

View File

@@ -1,4 +1,4 @@
# StellaOps.Notify.Connectors.Teams — Agent Charter
## Mission
Implement Microsoft Teams connector plug-in per `docs/ARCHITECTURE_NOTIFY.md`.
# StellaOps.Notify.Connectors.Teams — Agent Charter
## Mission
Implement Microsoft Teams connector plug-in per `docs/modules/notify/ARCHITECTURE.md`.

View File

@@ -1,4 +1,4 @@
# StellaOps.Notify.Connectors.Webhook — Agent Charter
## Mission
Implement generic webhook connector plug-in per `docs/ARCHITECTURE_NOTIFY.md`.
# StellaOps.Notify.Connectors.Webhook — Agent Charter
## Mission
Implement generic webhook connector plug-in per `docs/modules/notify/ARCHITECTURE.md`.

View File

@@ -1,4 +1,4 @@
# StellaOps.Notify.Engine — Agent Charter
## Mission
Deliver rule evaluation, digest, and rendering logic per `docs/ARCHITECTURE_NOTIFY.md`.
# StellaOps.Notify.Engine — Agent Charter
## Mission
Deliver rule evaluation, digest, and rendering logic per `docs/modules/notify/ARCHITECTURE.md`.

View File

@@ -1,4 +1,4 @@
# StellaOps.Notify.Models — Agent Charter
## Mission
Define Notify DTOs and contracts per `docs/ARCHITECTURE_NOTIFY.md`.
# StellaOps.Notify.Models — Agent Charter
## Mission
Define Notify DTOs and contracts per `docs/modules/notify/ARCHITECTURE.md`.

View File

@@ -1,4 +1,4 @@
# StellaOps.Notify.Queue — Agent Charter
## Mission
Provide event & delivery queues for Notify per `docs/ARCHITECTURE_NOTIFY.md`.
# StellaOps.Notify.Queue — Agent Charter
## Mission
Provide event & delivery queues for Notify per `docs/modules/notify/ARCHITECTURE.md`.

View File

@@ -1,4 +1,4 @@
# StellaOps.Notify.Storage.Mongo — Agent Charter
## Mission
Implement Mongo persistence (rules, channels, deliveries, digests, locks, audit) per `docs/ARCHITECTURE_NOTIFY.md`.
# StellaOps.Notify.Storage.Mongo — Agent Charter
## Mission
Implement Mongo persistence (rules, channels, deliveries, digests, locks, audit) per `docs/modules/notify/ARCHITECTURE.md`.

View File

@@ -18,7 +18,7 @@
<None Include="../../docs/events/*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="../../docs/notify/samples/*.json">
<None Include="../../../../docs/modules/notify/resources/samples/*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

View File

@@ -22,8 +22,8 @@
</ItemGroup>
<ItemGroup>
<None Include="../../docs/notify/samples/*.json">
<None Include="../../../../docs/modules/notify/resources/samples/*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
</Project>

View File

@@ -12,8 +12,8 @@
</ItemGroup>
<ItemGroup>
<None Include="../../docs/notify/samples/*.json">
<None Include="../../../../docs/modules/notify/resources/samples/*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
</Project>

View File

@@ -1,12 +1,12 @@
# StellaOps.Policy — Agent Charter
## Mission
Deliver the policy engine outlined in `docs/ARCHITECTURE_SCANNER.md` and related prose:
- Define YAML schema (ignore rules, VEX inclusion/exclusion, vendor precedence, license gates).
- Provide policy snapshot storage with revision digests and diagnostics.
- Offer preview APIs to compare policy impacts on existing reports.
## Expectations
- Coordinate with Scanner.WebService, Feedser, Vexer, UI, Notify.
- Maintain deterministic serialization and unit tests for precedence rules.
- Update `TASKS.md` and broadcast contract changes.
# StellaOps.Policy — Agent Charter
## Mission
Deliver the policy engine outlined in `docs/modules/scanner/ARCHITECTURE.md` and related prose:
- Define YAML schema (ignore rules, VEX inclusion/exclusion, vendor precedence, license gates).
- Provide policy snapshot storage with revision digests and diagnostics.
- Offer preview APIs to compare policy impacts on existing reports.
## Expectations
- Coordinate with Scanner.WebService, Feedser, Vexer, UI, Notify.
- Maintain deterministic serialization and unit tests for precedence rules.
- Update `TASKS.md` and broadcast contract changes.

View File

@@ -1,12 +1,12 @@
# StellaOps.Scanner.Sbomer.BuildXPlugin — Agent Charter
## Mission
Implement the build-time SBOM generator described in `docs/ARCHITECTURE_SCANNER.md` and new buildx dossier requirements:
- Provide a deterministic BuildKit/Buildx generator that produces layer SBOM fragments and uploads them to local CAS.
- Emit OCI annotations (+provenance) compatible with Scanner.Emit and Attestor hand-offs.
- Respect restart-time plug-in policy (`plugins/scanner/buildx/` manifests) and keep CI overhead ≤300ms per layer.
## Expectations
- Read architecture + upcoming Buildx addendum before coding.
- Ensure graceful fallback to post-build scan when generator unavailable.
- Provide integration tests with mock BuildKit, and update `TASKS.md` as states change.
# StellaOps.Scanner.Sbomer.BuildXPlugin — Agent Charter
## Mission
Implement the build-time SBOM generator described in `docs/modules/scanner/ARCHITECTURE.md` and new buildx dossier requirements:
- Provide a deterministic BuildKit/Buildx generator that produces layer SBOM fragments and uploads them to local CAS.
- Emit OCI annotations (+provenance) compatible with Scanner.Emit and Attestor hand-offs.
- Respect restart-time plug-in policy (`plugins/scanner/buildx/` manifests) and keep CI overhead ≤300ms per layer.
## Expectations
- Read architecture + upcoming Buildx addendum before coding.
- Ensure graceful fallback to post-build scan when generator unavailable.
- Provide integration tests with mock BuildKit, and update `TASKS.md` as states change.

View File

@@ -1,7 +1,7 @@
# Scanner Core Task Board
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| SCANNER-CORE-09-501 | DONE (2025-10-19) | Scanner Core Guild | — | Define shared DTOs (ScanJob, ProgressEvent), error taxonomy, and deterministic ID/timestamp helpers aligning with `ARCHITECTURE_SCANNER.md` §3§4.<br>2025-10-19: Added golden fixtures + `ScannerCoreContractsTests` to lock canonical JSON.<br>2025-10-19: Published canonical JSON snippet + acceptance notes in `docs/scanner-core-contracts.md`. | DTOs serialize deterministically, helpers produce reproducible IDs/timestamps, tests cover round-trips and hash derivation. |
| SCANNER-CORE-09-502 | DONE (2025-10-19) | Scanner Core Guild | SCANNER-CORE-09-501 | Observability helpers (correlation IDs, logging scopes, metric namespacing, deterministic hashes) consumed by WebService/Worker.<br>2025-10-19: Verified progress scope serialisation via new fixtures/tests.<br>2025-10-19: Added `ScannerLogExtensionsPerformanceTests` to enforce ≤5µs scope overhead + documented micro-bench results. | Logging/metrics helpers allocate minimally, correlation IDs stable, ActivitySource emitted; tests assert determinism. |
| SCANNER-CORE-09-503 | DONE (2025-10-18) | Scanner Core Guild | SCANNER-CORE-09-501, SCANNER-CORE-09-502 | Security utilities: Authority client factory, OpTok caching, DPoP verifier, restart-time plug-in guardrails for scanner components. | Authority helpers cache tokens, DPoP validator rejects invalid proofs, plug-in guard prevents runtime additions; tests cover happy/error paths. |
# Scanner Core Task Board
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| SCANNER-CORE-09-501 | DONE (2025-10-19) | Scanner Core Guild | — | Define shared DTOs (ScanJob, ProgressEvent), error taxonomy, and deterministic ID/timestamp helpers aligning with `modules/scanner/ARCHITECTURE.md` §3§4.<br>2025-10-19: Added golden fixtures + `ScannerCoreContractsTests` to lock canonical JSON.<br>2025-10-19: Published canonical JSON snippet + acceptance notes in `docs/scanner-core-contracts.md`. | DTOs serialize deterministically, helpers produce reproducible IDs/timestamps, tests cover round-trips and hash derivation. |
| SCANNER-CORE-09-502 | DONE (2025-10-19) | Scanner Core Guild | SCANNER-CORE-09-501 | Observability helpers (correlation IDs, logging scopes, metric namespacing, deterministic hashes) consumed by WebService/Worker.<br>2025-10-19: Verified progress scope serialisation via new fixtures/tests.<br>2025-10-19: Added `ScannerLogExtensionsPerformanceTests` to enforce ≤5µs scope overhead + documented micro-bench results. | Logging/metrics helpers allocate minimally, correlation IDs stable, ActivitySource emitted; tests assert determinism. |
| SCANNER-CORE-09-503 | DONE (2025-10-18) | Scanner Core Guild | SCANNER-CORE-09-501, SCANNER-CORE-09-502 | Security utilities: Authority client factory, OpTok caching, DPoP verifier, restart-time plug-in guardrails for scanner components. | Authority helpers cache tokens, DPoP validator rejects invalid proofs, plug-in guard prevents runtime additions; tests cover happy/error paths. |

View File

@@ -1,15 +1,15 @@
# StellaOps.Scanner.Queue — Agent Charter
## Mission
Deliver the scanner job queue backbone defined in `docs/ARCHITECTURE_SCANNER.md`, providing deterministic, offline-friendly leasing semantics for WebService producers and Worker consumers.
## Responsibilities
- Define queue abstractions with idempotent enqueue tokens, acknowledgement, lease renewal, and claim support.
- Ship first-party adapters for Redis Streams and NATS JetStream, respecting offline deployments and allow-listed hosts.
- Surface health probes, structured diagnostics, and metrics needed by Scanner WebService/Worker.
- Document operational expectations and configuration binding hooks.
## Interfaces & Dependencies
- Consumes shared configuration primitives from `StellaOps.Configuration`.
- Exposes dependency injection extensions for `StellaOps.DependencyInjection`.
- Targets `net10.0` (preview) and aligns with scanner DTOs once `StellaOps.Scanner.Core` lands.
# StellaOps.Scanner.Queue — Agent Charter
## Mission
Deliver the scanner job queue backbone defined in `docs/modules/scanner/ARCHITECTURE.md`, providing deterministic, offline-friendly leasing semantics for WebService producers and Worker consumers.
## Responsibilities
- Define queue abstractions with idempotent enqueue tokens, acknowledgement, lease renewal, and claim support.
- Ship first-party adapters for Redis Streams and NATS JetStream, respecting offline deployments and allow-listed hosts.
- Surface health probes, structured diagnostics, and metrics needed by Scanner WebService/Worker.
- Document operational expectations and configuration binding hooks.
## Interfaces & Dependencies
- Consumes shared configuration primitives from `StellaOps.Configuration`.
- Exposes dependency injection extensions for `StellaOps.DependencyInjection`.
- Targets `net10.0` (preview) and aligns with scanner DTOs once `StellaOps.Scanner.Core` lands.

View File

@@ -1,4 +1,4 @@
# StellaOps.Scheduler.WebService — Agent Charter
## Mission
Implement Scheduler control plane per `docs/ARCHITECTURE_SCHEDULER.md`.
# StellaOps.Scheduler.WebService — Agent Charter
## Mission
Implement Scheduler control plane per `docs/modules/scheduler/ARCHITECTURE.md`.

View File

@@ -1,4 +1,4 @@
# StellaOps.Scheduler.ImpactIndex — Agent Charter
## Mission
Build the global impact index per `docs/ARCHITECTURE_SCHEDULER.md` (roaring bitmaps, selectors, snapshotting).
# StellaOps.Scheduler.ImpactIndex — Agent Charter
## Mission
Build the global impact index per `docs/modules/scheduler/ARCHITECTURE.md` (roaring bitmaps, selectors, snapshotting).

View File

@@ -1,4 +1,4 @@
# StellaOps.Scheduler.Models — Agent Charter
## Mission
Define Scheduler DTOs (Schedule, Run, ImpactSet, Selector, DeltaSummary) per `docs/ARCHITECTURE_SCHEDULER.md`.
# StellaOps.Scheduler.Models — Agent Charter
## Mission
Define Scheduler DTOs (Schedule, Run, ImpactSet, Selector, DeltaSummary) per `docs/modules/scheduler/ARCHITECTURE.md`.

View File

@@ -1,4 +1,4 @@
# StellaOps.Scheduler.Queue — Agent Charter
## Mission
Provide queue abstraction (Redis Streams / NATS JetStream) for planner inputs and runner segments per `docs/ARCHITECTURE_SCHEDULER.md`.
# StellaOps.Scheduler.Queue — Agent Charter
## Mission
Provide queue abstraction (Redis Streams / NATS JetStream) for planner inputs and runner segments per `docs/modules/scheduler/ARCHITECTURE.md`.

View File

@@ -1,4 +1,4 @@
# StellaOps.Scheduler.Storage.Mongo — Agent Charter
## Mission
Implement Mongo persistence (schedules, runs, impact cursors, locks, audit) per `docs/ARCHITECTURE_SCHEDULER.md`.
# StellaOps.Scheduler.Storage.Mongo — Agent Charter
## Mission
Implement Mongo persistence (schedules, runs, impact cursors, locks, audit) per `docs/modules/scheduler/ARCHITECTURE.md`.

View File

@@ -1,4 +1,4 @@
# StellaOps.Scheduler.Worker — Agent Charter
## Mission
Implement Scheduler planners/runners per `docs/ARCHITECTURE_SCHEDULER.md`.
# StellaOps.Scheduler.Worker — Agent Charter
## Mission
Implement Scheduler planners/runners per `docs/modules/scheduler/ARCHITECTURE.md`.

View File

@@ -34,10 +34,10 @@ runner throughput, and backlog health.
## Dashboards & alerts
- **Grafana dashboard:** `docs/ops/scheduler-worker-grafana-dashboard.json`
- **Grafana dashboard:** `docs/modules/scheduler/operations/worker-grafana-dashboard.json`
(import into Prometheus-backed Grafana). Panels mirror the metrics above with
mode filters.
- **Prometheus rules:** `docs/ops/scheduler-worker-prometheus-rules.yaml`
- **Prometheus rules:** `docs/modules/scheduler/operations/worker-prometheus-rules.yaml`
provides planner failure/latency, backlog, and stuck-run alerts.
- **Operations guide:** see `docs/ops/scheduler-worker-operations.md` for
- **Operations guide:** see `docs/modules/scheduler/operations/worker.md` for
runbook steps, alert context, and dashboard wiring instructions.

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../Concelier/StellaOps.Concelier.PluginBinaries/StellaOps.Concelier.Connector.Osv/StellaOps.Concelier.Connector.Osv.csproj" />
<ProjectReference Include="../../Concelier/StellaOps.Concelier.PluginBinaries/StellaOps.Concelier.Connector.Ghsa/StellaOps.Concelier.Connector.Ghsa.csproj" />
<ProjectReference Include="../../Concelier/StellaOps.Concelier.PluginBinaries/StellaOps.Concelier.Connector.Nvd/StellaOps.Concelier.Connector.Nvd.csproj" />
<ProjectReference Include="../../Concelier/StellaOps.Concelier.PluginBinaries/StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
<ProjectReference Include="../../Concelier/__Libraries/StellaOps.Concelier.Storage.Mongo/StellaOps.Concelier.Storage.Mongo.csproj" />
<ProjectReference Include="../../Concelier/__Libraries/StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
<ProjectReference Include="../../Concelier/__Libraries/StellaOps.Concelier.Testing/StellaOps.Concelier.Testing.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,378 @@
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using MongoDB.Bson;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Ghsa;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Ghsa.Internal;
using StellaOps.Concelier.Connector.Osv.Internal;
using StellaOps.Concelier.Connector.Osv;
using StellaOps.Concelier.Connector.Nvd;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
var projectRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
var osvFixturesPath = Path.Combine(projectRoot, "src", "StellaOps.Concelier.Connector.Osv.Tests", "Fixtures");
var ghsaFixturesPath = Path.Combine(projectRoot, "src", "StellaOps.Concelier.Connector.Ghsa.Tests", "Fixtures");
var nvdFixturesPath = Path.Combine(projectRoot, "src", "StellaOps.Concelier.Connector.Nvd.Tests", "Nvd", "Fixtures");
RewriteOsvFixtures(osvFixturesPath);
RewriteSnapshotFixtures(osvFixturesPath);
RewriteGhsaFixtures(osvFixturesPath);
RewriteCreditParityFixtures(ghsaFixturesPath, nvdFixturesPath);
return;
void RewriteOsvFixtures(string fixturesPath)
{
var rawPath = Path.Combine(fixturesPath, "osv-ghsa.raw-osv.json");
if (!File.Exists(rawPath))
{
Console.WriteLine($"[FixtureUpdater] OSV raw fixture missing: {rawPath}");
return;
}
using var document = JsonDocument.Parse(File.ReadAllText(rawPath));
var advisories = new List<Advisory>();
foreach (var element in document.RootElement.EnumerateArray())
{
var dto = JsonSerializer.Deserialize<OsvVulnerabilityDto>(element.GetRawText(), serializerOptions);
if (dto is null)
{
continue;
}
var ecosystem = dto.Affected?.FirstOrDefault()?.Package?.Ecosystem ?? "unknown";
var uri = new Uri($"https://osv.dev/vulnerability/{dto.Id}");
var documentRecord = new DocumentRecord(
Guid.NewGuid(),
OsvConnectorPlugin.SourceName,
uri.ToString(),
DateTimeOffset.UtcNow,
"fixture-sha",
DocumentStatuses.PendingMap,
"application/json",
null,
new Dictionary<string, string>(StringComparer.Ordinal)
{
["osv.ecosystem"] = ecosystem,
},
null,
DateTimeOffset.UtcNow,
null,
null);
var payload = BsonDocument.Parse(element.GetRawText());
var dtoRecord = new DtoRecord(
Guid.NewGuid(),
documentRecord.Id,
OsvConnectorPlugin.SourceName,
"osv.v1",
payload,
DateTimeOffset.UtcNow);
var advisory = OsvMapper.Map(dto, documentRecord, dtoRecord, ecosystem);
advisories.Add(advisory);
}
advisories.Sort((left, right) => string.Compare(left.AdvisoryKey, right.AdvisoryKey, StringComparison.Ordinal));
var snapshot = SnapshotSerializer.ToSnapshot(advisories);
File.WriteAllText(Path.Combine(fixturesPath, "osv-ghsa.osv.json"), snapshot);
Console.WriteLine($"[FixtureUpdater] Updated {Path.Combine(fixturesPath, "osv-ghsa.osv.json")}");
}
void RewriteSnapshotFixtures(string fixturesPath)
{
var baselinePublished = new DateTimeOffset(2025, 1, 5, 12, 0, 0, TimeSpan.Zero);
var baselineModified = new DateTimeOffset(2025, 1, 8, 6, 30, 0, TimeSpan.Zero);
var baselineFetched = new DateTimeOffset(2025, 1, 8, 7, 0, 0, TimeSpan.Zero);
var cases = new (string Ecosystem, string Purl, string PackageName, string SnapshotFile)[]
{
("npm", "pkg:npm/%40scope%2Fleft-pad", "@scope/left-pad", "osv-npm.snapshot.json"),
("PyPI", "pkg:pypi/requests", "requests", "osv-pypi.snapshot.json"),
};
foreach (var (ecosystem, purl, packageName, snapshotFile) in cases)
{
var dto = new OsvVulnerabilityDto
{
Id = $"OSV-2025-{ecosystem}-0001",
Summary = $"{ecosystem} package vulnerability",
Details = $"Detailed description for {ecosystem} package {packageName}.",
Published = baselinePublished,
Modified = baselineModified,
Aliases = new[] { $"CVE-2025-11{ecosystem.Length}", $"GHSA-{ecosystem.Length}abc-{ecosystem.Length}def-{ecosystem.Length}ghi" },
Related = new[] { $"OSV-RELATED-{ecosystem}-42" },
References = new[]
{
new OsvReferenceDto { Url = $"https://example.com/{ecosystem}/advisory", Type = "ADVISORY" },
new OsvReferenceDto { Url = $"https://example.com/{ecosystem}/fix", Type = "FIX" },
},
Severity = new[]
{
new OsvSeverityDto { Type = "CVSS_V3", Score = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" },
},
Affected = new[]
{
new OsvAffectedPackageDto
{
Package = new OsvPackageDto
{
Ecosystem = ecosystem,
Name = packageName,
Purl = purl,
},
Ranges = new[]
{
new OsvRangeDto
{
Type = "SEMVER",
Events = new[]
{
new OsvEventDto { Introduced = "0" },
new OsvEventDto { Fixed = "2.0.0" },
},
},
},
Versions = new[] { "1.0.0", "1.5.0" },
EcosystemSpecific = JsonDocument.Parse("{\"severity\":\"high\"}").RootElement.Clone(),
},
},
DatabaseSpecific = JsonDocument.Parse("{\"source\":\"osv.dev\"}").RootElement.Clone(),
};
var document = new DocumentRecord(
Guid.NewGuid(),
OsvConnectorPlugin.SourceName,
$"https://osv.dev/vulnerability/{dto.Id}",
baselineFetched,
"fixture-sha",
DocumentStatuses.PendingParse,
"application/json",
null,
new Dictionary<string, string>(StringComparer.Ordinal) { ["osv.ecosystem"] = ecosystem },
null,
baselineModified,
null);
var payload = BsonDocument.Parse(JsonSerializer.Serialize(dto, serializerOptions));
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, OsvConnectorPlugin.SourceName, "osv.v1", payload, baselineModified);
var advisory = OsvMapper.Map(dto, document, dtoRecord, ecosystem);
var snapshot = SnapshotSerializer.ToSnapshot(advisory);
File.WriteAllText(Path.Combine(fixturesPath, snapshotFile), snapshot);
Console.WriteLine($"[FixtureUpdater] Updated {Path.Combine(fixturesPath, snapshotFile)}");
}
}
void RewriteGhsaFixtures(string fixturesPath)
{
var rawPath = Path.Combine(fixturesPath, "osv-ghsa.raw-ghsa.json");
if (!File.Exists(rawPath))
{
Console.WriteLine($"[FixtureUpdater] GHSA raw fixture missing: {rawPath}");
return;
}
JsonDocument document;
try
{
document = JsonDocument.Parse(File.ReadAllText(rawPath));
}
catch (JsonException ex)
{
Console.WriteLine($"[FixtureUpdater] Failed to parse GHSA raw fixture '{rawPath}': {ex.Message}");
return;
}
using (document)
{
var advisories = new List<Advisory>();
foreach (var element in document.RootElement.EnumerateArray())
{
GhsaRecordDto dto;
try
{
dto = GhsaRecordParser.Parse(Encoding.UTF8.GetBytes(element.GetRawText()));
}
catch (JsonException)
{
continue;
}
var uri = new Uri($"https://github.com/advisories/{dto.GhsaId}");
var documentRecord = new DocumentRecord(
Guid.NewGuid(),
GhsaConnectorPlugin.SourceName,
uri.ToString(),
DateTimeOffset.UtcNow,
"fixture-sha",
DocumentStatuses.PendingMap,
"application/json",
null,
new Dictionary<string, string>(StringComparer.Ordinal),
null,
DateTimeOffset.UtcNow,
null,
null);
var advisory = GhsaMapper.Map(dto, documentRecord, DateTimeOffset.UtcNow);
advisories.Add(advisory);
}
advisories.Sort((left, right) => string.Compare(left.AdvisoryKey, right.AdvisoryKey, StringComparison.Ordinal));
var snapshot = SnapshotSerializer.ToSnapshot(advisories);
File.WriteAllText(Path.Combine(fixturesPath, "osv-ghsa.ghsa.json"), snapshot);
Console.WriteLine($"[FixtureUpdater] Updated {Path.Combine(fixturesPath, "osv-ghsa.ghsa.json")}");
}
}
void RewriteCreditParityFixtures(string ghsaFixturesPath, string nvdFixturesPath)
{
Directory.CreateDirectory(ghsaFixturesPath);
Directory.CreateDirectory(nvdFixturesPath);
var advisoryKeyGhsa = "GHSA-credit-parity";
var advisoryKeyNvd = "CVE-2025-5555";
var recordedAt = new DateTimeOffset(2025, 10, 10, 15, 0, 0, TimeSpan.Zero);
var published = new DateTimeOffset(2025, 10, 9, 18, 30, 0, TimeSpan.Zero);
var modified = new DateTimeOffset(2025, 10, 10, 12, 0, 0, TimeSpan.Zero);
AdvisoryCredit[] CreateCredits(string source) =>
[
CreateCredit("Alice Researcher", "reporter", new[] { "mailto:alice.researcher@example.com" }, source),
CreateCredit("Bob Maintainer", "remediation_developer", new[] { "https://github.com/acme/bob-maintainer" }, source)
];
AdvisoryCredit CreateCredit(string displayName, string role, IReadOnlyList<string> contacts, string source)
{
var provenance = new AdvisoryProvenance(
source,
"credit",
$"{source}:{displayName.ToLowerInvariant().Replace(' ', '-')}",
recordedAt,
new[] { ProvenanceFieldMasks.Credits });
return new AdvisoryCredit(displayName, role, contacts, provenance);
}
AdvisoryReference[] CreateReferences(string sourceName, params (string Url, string Kind)[] entries)
{
if (entries is null || entries.Length == 0)
{
return Array.Empty<AdvisoryReference>();
}
var references = new List<AdvisoryReference>(entries.Length);
foreach (var entry in entries)
{
var provenance = new AdvisoryProvenance(
sourceName,
"reference",
entry.Url,
recordedAt,
new[] { ProvenanceFieldMasks.References });
references.Add(new AdvisoryReference(
entry.Url,
entry.Kind,
sourceTag: null,
summary: null,
provenance));
}
return references.ToArray();
}
Advisory CreateAdvisory(
string sourceName,
string advisoryKey,
IEnumerable<string> aliases,
AdvisoryCredit[] credits,
AdvisoryReference[] references,
string documentValue)
{
var documentProvenance = new AdvisoryProvenance(
sourceName,
"document",
documentValue,
recordedAt,
new[] { ProvenanceFieldMasks.Advisory });
var mappingProvenance = new AdvisoryProvenance(
sourceName,
"mapping",
advisoryKey,
recordedAt,
new[] { ProvenanceFieldMasks.Advisory });
return new Advisory(
advisoryKey,
"Credit parity regression fixture",
"Credit parity regression fixture",
"en",
published,
modified,
"moderate",
exploitKnown: false,
aliases,
credits,
references,
Array.Empty<AffectedPackage>(),
Array.Empty<CvssMetric>(),
new[] { documentProvenance, mappingProvenance });
}
var ghsa = CreateAdvisory(
"ghsa",
advisoryKeyGhsa,
new[] { advisoryKeyGhsa, advisoryKeyNvd },
CreateCredits("ghsa"),
CreateReferences(
"ghsa",
( $"https://github.com/advisories/{advisoryKeyGhsa}", "advisory"),
( "https://example.com/ghsa/patch", "patch")),
$"security/advisories/{advisoryKeyGhsa}");
var osv = CreateAdvisory(
OsvConnectorPlugin.SourceName,
advisoryKeyGhsa,
new[] { advisoryKeyGhsa, advisoryKeyNvd },
CreateCredits(OsvConnectorPlugin.SourceName),
CreateReferences(
OsvConnectorPlugin.SourceName,
( $"https://github.com/advisories/{advisoryKeyGhsa}", "advisory"),
( $"https://osv.dev/vulnerability/{advisoryKeyGhsa}", "advisory")),
$"https://osv.dev/vulnerability/{advisoryKeyGhsa}");
var nvd = CreateAdvisory(
NvdConnectorPlugin.SourceName,
advisoryKeyNvd,
new[] { advisoryKeyNvd, advisoryKeyGhsa },
CreateCredits(NvdConnectorPlugin.SourceName),
CreateReferences(
NvdConnectorPlugin.SourceName,
( $"https://services.nvd.nist.gov/vuln/detail/{advisoryKeyNvd}", "advisory"),
( "https://example.com/nvd/reference", "report")),
$"https://services.nvd.nist.gov/vuln/detail/{advisoryKeyNvd}");
var ghsaSnapshot = SnapshotSerializer.ToSnapshot(ghsa);
var osvSnapshot = SnapshotSerializer.ToSnapshot(osv);
var nvdSnapshot = SnapshotSerializer.ToSnapshot(nvd);
File.WriteAllText(Path.Combine(ghsaFixturesPath, "credit-parity.ghsa.json"), ghsaSnapshot);
File.WriteAllText(Path.Combine(ghsaFixturesPath, "credit-parity.osv.json"), osvSnapshot);
File.WriteAllText(Path.Combine(ghsaFixturesPath, "credit-parity.nvd.json"), nvdSnapshot);
File.WriteAllText(Path.Combine(nvdFixturesPath, "credit-parity.ghsa.json"), ghsaSnapshot);
File.WriteAllText(Path.Combine(nvdFixturesPath, "credit-parity.osv.json"), osvSnapshot);
File.WriteAllText(Path.Combine(nvdFixturesPath, "credit-parity.nvd.json"), nvdSnapshot);
Console.WriteLine($"[FixtureUpdater] Updated credit parity fixtures under {ghsaFixturesPath} and {nvdFixturesPath}");
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj" />
<ProjectReference Include="..\..\src\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,348 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Analyzers.Lang.Plugin;
using StellaOps.Scanner.Core.Security;
internal sealed record SmokeScenario(string Name, string[] UsageHintRelatives)
{
public IReadOnlyList<string> ResolveUsageHints(string scenarioRoot)
=> UsageHintRelatives.Select(relative => Path.GetFullPath(Path.Combine(scenarioRoot, relative))).ToArray();
}
internal sealed class SmokeOptions
{
public string RepoRoot { get; set; } = Directory.GetCurrentDirectory();
public string PluginDirectoryName { get; set; } = "StellaOps.Scanner.Analyzers.Lang.Python";
public string FixtureRelativePath { get; set; } = Path.Combine("src", "StellaOps.Scanner.Analyzers.Lang.Python.Tests", "Fixtures", "lang", "python");
public static SmokeOptions Parse(string[] args)
{
var options = new SmokeOptions();
for (var index = 0; index < args.Length; index++)
{
var current = args[index];
switch (current)
{
case "--repo-root":
case "-r":
options.RepoRoot = RequireValue(args, ref index, current);
break;
case "--plugin-directory":
case "-p":
options.PluginDirectoryName = RequireValue(args, ref index, current);
break;
case "--fixture-path":
case "-f":
options.FixtureRelativePath = RequireValue(args, ref index, current);
break;
case "--help":
case "-h":
PrintUsage();
Environment.Exit(0);
break;
default:
throw new ArgumentException($"Unknown argument '{current}'. Use --help for usage.");
}
}
options.RepoRoot = Path.GetFullPath(options.RepoRoot);
return options;
}
private static string RequireValue(string[] args, ref int index, string switchName)
{
if (index + 1 >= args.Length)
{
throw new ArgumentException($"Missing value for '{switchName}'.");
}
index++;
var value = args[index];
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException($"Value for '{switchName}' cannot be empty.");
}
return value;
}
private static void PrintUsage()
{
Console.WriteLine("Language Analyzer Smoke Harness");
Console.WriteLine("Usage: dotnet run --project src/Tools/LanguageAnalyzerSmoke -- [options]");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine(" -r, --repo-root <path> Repository root (defaults to current working directory)");
Console.WriteLine(" -p, --plugin-directory <name> Analyzer plug-in directory under plugins/scanner/analyzers/lang (defaults to StellaOps.Scanner.Analyzers.Lang.Python)");
Console.WriteLine(" -f, --fixture-path <path> Relative path to fixtures root (defaults to src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Python.Tests/Fixtures/lang/python)");
Console.WriteLine(" -h, --help Show usage information");
}
}
internal sealed record PluginManifest
{
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; init; } = string.Empty;
[JsonPropertyName("id")]
public string Id { get; init; } = string.Empty;
[JsonPropertyName("displayName")]
public string DisplayName { get; init; } = string.Empty;
[JsonPropertyName("version")]
public string Version { get; init; } = string.Empty;
[JsonPropertyName("requiresRestart")]
public bool RequiresRestart { get; init; }
[JsonPropertyName("entryPoint")]
public PluginEntryPoint EntryPoint { get; init; } = new();
[JsonPropertyName("capabilities")]
public IReadOnlyList<string> Capabilities { get; init; } = Array.Empty<string>();
[JsonPropertyName("metadata")]
public IReadOnlyDictionary<string, string> Metadata { get; init; } = ImmutableDictionary<string, string>.Empty;
}
internal sealed record PluginEntryPoint
{
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("assembly")]
public string Assembly { get; init; } = string.Empty;
[JsonPropertyName("typeName")]
public string TypeName { get; init; } = string.Empty;
}
file static class Program
{
private static readonly SmokeScenario[] PythonScenarios =
{
new("simple-venv", new[] { Path.Combine("bin", "simple-tool") }),
new("pip-cache", new[] { Path.Combine("lib", "python3.11", "site-packages", "cache_pkg-1.2.3.data", "scripts", "cache-tool") }),
new("layered-editable", new[] { Path.Combine("layer1", "usr", "bin", "layered-cli") })
};
public static async Task<int> Main(string[] args)
{
try
{
var options = SmokeOptions.Parse(args);
await RunAsync(options).ConfigureAwait(false);
Console.WriteLine("✅ Python analyzer smoke checks passed");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"❌ {ex.Message}");
return 1;
}
}
private static async Task RunAsync(SmokeOptions options)
{
ValidateOptions(options);
var pluginRoot = Path.Combine(options.RepoRoot, "plugins", "scanner", "analyzers", "lang", options.PluginDirectoryName);
var manifestPath = Path.Combine(pluginRoot, "manifest.json");
if (!File.Exists(manifestPath))
{
throw new FileNotFoundException($"Plug-in manifest not found at '{manifestPath}'.", manifestPath);
}
using var manifestStream = File.OpenRead(manifestPath);
var manifest = JsonSerializer.Deserialize<PluginManifest>(manifestStream, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip
}) ?? throw new InvalidOperationException($"Unable to parse manifest '{manifestPath}'.");
ValidateManifest(manifest, options.PluginDirectoryName);
var pluginAssemblyPath = Path.Combine(pluginRoot, manifest.EntryPoint.Assembly);
if (!File.Exists(pluginAssemblyPath))
{
throw new FileNotFoundException($"Plug-in assembly '{manifest.EntryPoint.Assembly}' not found under '{pluginRoot}'.", pluginAssemblyPath);
}
var sha256 = ComputeSha256(pluginAssemblyPath);
Console.WriteLine($"→ Plug-in assembly SHA-256: {sha256}");
using var serviceProvider = BuildServiceProvider();
var catalog = new LanguageAnalyzerPluginCatalog(new RestartOnlyPluginGuard(), NullLogger<LanguageAnalyzerPluginCatalog>.Instance);
catalog.LoadFromDirectory(pluginRoot, seal: true);
if (catalog.Plugins.Count == 0)
{
throw new InvalidOperationException($"No analyzer plug-ins were loaded from '{pluginRoot}'.");
}
var analyzerSet = catalog.CreateAnalyzers(serviceProvider);
if (analyzerSet.Count == 0)
{
throw new InvalidOperationException("Language analyzer plug-ins reported no analyzers.");
}
var analyzerIds = analyzerSet.Select(analyzer => analyzer.Id).ToArray();
Console.WriteLine($"→ Loaded analyzers: {string.Join(", ", analyzerIds)}");
if (!analyzerIds.Contains("python", StringComparer.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Python analyzer was not created by the plug-in.");
}
var fixtureRoot = Path.GetFullPath(Path.Combine(options.RepoRoot, options.FixtureRelativePath));
if (!Directory.Exists(fixtureRoot))
{
throw new DirectoryNotFoundException($"Fixture directory '{fixtureRoot}' does not exist.");
}
foreach (var scenario in PythonScenarios)
{
await RunScenarioAsync(scenario, fixtureRoot, catalog, serviceProvider).ConfigureAwait(false);
}
}
private static ServiceProvider BuildServiceProvider()
{
var services = new ServiceCollection();
services.AddLogging();
return services.BuildServiceProvider();
}
private static async Task RunScenarioAsync(SmokeScenario scenario, string fixtureRoot, ILanguageAnalyzerPluginCatalog catalog, IServiceProvider services)
{
var scenarioRoot = Path.Combine(fixtureRoot, scenario.Name);
if (!Directory.Exists(scenarioRoot))
{
throw new DirectoryNotFoundException($"Scenario '{scenario.Name}' directory missing at '{scenarioRoot}'.");
}
var goldenPath = Path.Combine(scenarioRoot, "expected.json");
string? goldenNormalized = null;
if (File.Exists(goldenPath))
{
goldenNormalized = NormalizeJson(await File.ReadAllTextAsync(goldenPath).ConfigureAwait(false));
}
var usageHints = new LanguageUsageHints(scenario.ResolveUsageHints(scenarioRoot));
var context = new LanguageAnalyzerContext(scenarioRoot, TimeProvider.System, usageHints, services);
var coldEngine = new LanguageAnalyzerEngine(catalog.CreateAnalyzers(services));
var coldStopwatch = Stopwatch.StartNew();
var coldResult = await coldEngine.AnalyzeAsync(context, CancellationToken.None).ConfigureAwait(false);
coldStopwatch.Stop();
if (coldResult.Components.Count == 0)
{
throw new InvalidOperationException($"Scenario '{scenario.Name}' produced no components during cold run.");
}
var coldJson = NormalizeJson(coldResult.ToJson(indent: true));
if (goldenNormalized is string expected && !string.Equals(coldJson, expected, StringComparison.Ordinal))
{
Console.WriteLine($"⚠️ Scenario '{scenario.Name}' output deviates from repository golden snapshot.");
}
var warmEngine = new LanguageAnalyzerEngine(catalog.CreateAnalyzers(services));
var warmStopwatch = Stopwatch.StartNew();
var warmResult = await warmEngine.AnalyzeAsync(context, CancellationToken.None).ConfigureAwait(false);
warmStopwatch.Stop();
var warmJson = NormalizeJson(warmResult.ToJson(indent: true));
if (!string.Equals(coldJson, warmJson, StringComparison.Ordinal))
{
throw new InvalidOperationException($"Scenario '{scenario.Name}' produced different outputs between cold and warm runs.");
}
EnsureDurationWithinBudget(scenario.Name, coldStopwatch.Elapsed, warmStopwatch.Elapsed);
Console.WriteLine($"✓ Scenario '{scenario.Name}' — components {coldResult.Components.Count}, cold {coldStopwatch.Elapsed.TotalMilliseconds:F1} ms, warm {warmStopwatch.Elapsed.TotalMilliseconds:F1} ms");
}
private static void EnsureDurationWithinBudget(string scenarioName, TimeSpan coldDuration, TimeSpan warmDuration)
{
var coldBudget = TimeSpan.FromSeconds(30);
var warmBudget = TimeSpan.FromSeconds(5);
if (coldDuration > coldBudget)
{
throw new InvalidOperationException($"Scenario '{scenarioName}' cold run exceeded budget ({coldDuration.TotalSeconds:F2}s > {coldBudget.TotalSeconds:F2}s).");
}
if (warmDuration > warmBudget)
{
throw new InvalidOperationException($"Scenario '{scenarioName}' warm run exceeded budget ({warmDuration.TotalSeconds:F2}s > {warmBudget.TotalSeconds:F2}s).");
}
}
private static string NormalizeJson(string json)
=> json.Replace("\r\n", "\n", StringComparison.Ordinal).TrimEnd();
private static void ValidateOptions(SmokeOptions options)
{
if (!Directory.Exists(options.RepoRoot))
{
throw new DirectoryNotFoundException($"Repository root '{options.RepoRoot}' does not exist.");
}
}
private static void ValidateManifest(PluginManifest manifest, string expectedDirectory)
{
if (!string.Equals(manifest.SchemaVersion, "1.0", StringComparison.Ordinal))
{
throw new InvalidOperationException($"Unexpected manifest schema version '{manifest.SchemaVersion}'.");
}
if (!manifest.RequiresRestart)
{
throw new InvalidOperationException("Language analyzer plug-in must be marked as restart-only.");
}
if (!string.Equals(manifest.EntryPoint.Type, "dotnet", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Unsupported entry point type '{manifest.EntryPoint.Type}'.");
}
if (!manifest.Capabilities.Contains("python", StringComparer.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Manifest capabilities do not include 'python'.");
}
if (!string.Equals(manifest.EntryPoint.TypeName, "StellaOps.Scanner.Analyzers.Lang.Python.PythonAnalyzerPlugin", StringComparison.Ordinal))
{
throw new InvalidOperationException($"Unexpected entry point type name '{manifest.EntryPoint.TypeName}'.");
}
if (!string.Equals(manifest.Id, "stellaops.analyzer.lang.python", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Manifest id '{manifest.Id}' does not match expected plug-in id for directory '{expectedDirectory}'.");
}
}
private static string ComputeSha256(string path)
{
using var hash = SHA256.Create();
using var stream = File.OpenRead(path);
var digest = hash.ComputeHash(stream);
var builder = new StringBuilder(digest.Length * 2);
foreach (var b in digest)
{
builder.Append(b.ToString("x2"));
}
return builder.ToString();
}
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,198 @@
using System.Globalization;
using System.Net.Http.Headers;
using System.Linq;
using System.Text.Json;
using StackExchange.Redis;
static string RequireEnv(string name)
{
var value = Environment.GetEnvironmentVariable(name);
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException($"Environment variable '{name}' is required for Notify smoke validation.");
}
return value;
}
static string? GetField(StreamEntry entry, string fieldName)
{
foreach (var pair in entry.Values)
{
if (string.Equals(pair.Name, fieldName, StringComparison.OrdinalIgnoreCase))
{
return pair.Value.ToString();
}
}
return null;
}
static void Ensure(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException(message);
}
}
var redisDsn = RequireEnv("NOTIFY_SMOKE_REDIS_DSN");
var redisStream = Environment.GetEnvironmentVariable("NOTIFY_SMOKE_STREAM");
if (string.IsNullOrWhiteSpace(redisStream))
{
redisStream = "stella.events";
}
var expectedKindsEnv = RequireEnv("NOTIFY_SMOKE_EXPECT_KINDS");
var expectedKinds = expectedKindsEnv
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(kind => kind.ToLowerInvariant())
.Distinct()
.ToArray();
Ensure(expectedKinds.Length > 0, "Expected at least one event kind in NOTIFY_SMOKE_EXPECT_KINDS.");
var lookbackMinutesEnv = RequireEnv("NOTIFY_SMOKE_LOOKBACK_MINUTES");
if (!double.TryParse(lookbackMinutesEnv, NumberStyles.Any, CultureInfo.InvariantCulture, out var lookbackMinutes))
{
throw new InvalidOperationException("NOTIFY_SMOKE_LOOKBACK_MINUTES must be numeric.");
}
Ensure(lookbackMinutes > 0, "NOTIFY_SMOKE_LOOKBACK_MINUTES must be greater than zero.");
var now = DateTimeOffset.UtcNow;
var sinceThreshold = now - TimeSpan.FromMinutes(Math.Max(1, lookbackMinutes));
Console.WriteLine($" Checking Redis stream '{redisStream}' for kinds [{string.Join(", ", expectedKinds)}] within the last {lookbackMinutes:F1} minutes.");
var redisConfig = ConfigurationOptions.Parse(redisDsn);
redisConfig.AbortOnConnectFail = false;
await using var redisConnection = await ConnectionMultiplexer.ConnectAsync(redisConfig);
var database = redisConnection.GetDatabase();
var streamEntries = await database.StreamRangeAsync(redisStream, "-", "+", count: 200);
if (streamEntries.Length > 1)
{
Array.Reverse(streamEntries);
}
Ensure(streamEntries.Length > 0, $"Redis stream '{redisStream}' is empty.");
var recentEntries = new List<StreamEntry>();
foreach (var entry in streamEntries)
{
var timestampText = GetField(entry, "ts");
if (timestampText is null)
{
continue;
}
if (!DateTimeOffset.TryParse(timestampText, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var entryTimestamp))
{
continue;
}
if (entryTimestamp >= sinceThreshold)
{
recentEntries.Add(entry);
}
}
Ensure(recentEntries.Count > 0, $"No Redis events newer than {sinceThreshold:u} located in stream '{redisStream}'.");
var missingKinds = new List<string>();
foreach (var kind in expectedKinds)
{
var match = recentEntries.FirstOrDefault(entry =>
{
var entryKind = GetField(entry, "kind")?.ToLowerInvariant();
return entryKind == kind;
});
if (match.Equals(default(StreamEntry)))
{
missingKinds.Add(kind);
}
}
Ensure(missingKinds.Count == 0, $"Missing expected Redis events for kinds: {string.Join(", ", missingKinds)}");
Console.WriteLine("✅ Redis event stream contains the expected scanner events.");
var notifyBaseUrl = RequireEnv("NOTIFY_SMOKE_NOTIFY_BASEURL").TrimEnd('/');
var notifyToken = RequireEnv("NOTIFY_SMOKE_NOTIFY_TOKEN");
var notifyTenant = RequireEnv("NOTIFY_SMOKE_NOTIFY_TENANT");
var notifyTenantHeader = Environment.GetEnvironmentVariable("NOTIFY_SMOKE_NOTIFY_TENANT_HEADER");
if (string.IsNullOrWhiteSpace(notifyTenantHeader))
{
notifyTenantHeader = "X-StellaOps-Tenant";
}
var notifyTimeoutSeconds = 30;
var notifyTimeoutEnv = Environment.GetEnvironmentVariable("NOTIFY_SMOKE_NOTIFY_TIMEOUT_SECONDS");
if (!string.IsNullOrWhiteSpace(notifyTimeoutEnv) && int.TryParse(notifyTimeoutEnv, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedTimeout))
{
notifyTimeoutSeconds = Math.Max(5, parsedTimeout);
}
using var httpClient = new HttpClient
{
Timeout = TimeSpan.FromSeconds(notifyTimeoutSeconds),
};
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", notifyToken);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpClient.DefaultRequestHeaders.Add(notifyTenantHeader, notifyTenant);
var sinceQuery = Uri.EscapeDataString(sinceThreshold.ToString("O", CultureInfo.InvariantCulture));
var deliveriesUrl = $"{notifyBaseUrl}/api/v1/deliveries?since={sinceQuery}&limit=200";
Console.WriteLine($" Querying Notify deliveries via {deliveriesUrl}.");
using var response = await httpClient.GetAsync(deliveriesUrl);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Notify deliveries request failed with {(int)response.StatusCode} {response.ReasonPhrase}: {body}");
}
var json = await response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(json))
{
throw new InvalidOperationException("Notify deliveries response body was empty.");
}
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
IEnumerable<JsonElement> EnumerateDeliveries(JsonElement element)
{
return element.ValueKind switch
{
JsonValueKind.Array => element.EnumerateArray(),
JsonValueKind.Object when element.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Array => items.EnumerateArray(),
_ => throw new InvalidOperationException("Notify deliveries response was not an array or did not contain an 'items' collection.")
};
}
var deliveries = EnumerateDeliveries(root).ToArray();
Ensure(deliveries.Length > 0, "Notify deliveries response did not return any records.");
var missingDeliveryKinds = new List<string>();
foreach (var kind in expectedKinds)
{
var found = deliveries.Any(delivery =>
delivery.TryGetProperty("kind", out var kindProperty) &&
kindProperty.GetString()?.Equals(kind, StringComparison.OrdinalIgnoreCase) == true &&
delivery.TryGetProperty("status", out var statusProperty) &&
!string.Equals(statusProperty.GetString(), "failed", StringComparison.OrdinalIgnoreCase));
if (!found)
{
missingDeliveryKinds.Add(kind);
}
}
Ensure(missingDeliveryKinds.Count == 0, $"Notify deliveries missing successful records for kinds: {string.Join(", ", missingDeliveryKinds)}");
Console.WriteLine("✅ Notify deliveries include the expected scanner events.");
Console.WriteLine("🎉 Notify smoke validation completed successfully.");

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\StellaOps.Policy\StellaOps.Policy.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,56 @@
using StellaOps.Policy;
if (args.Length == 0)
{
Console.Error.WriteLine("Usage: policy-dsl-validator [--strict] [--json] <path-or-glob> [<path-or-glob> ...]");
Console.Error.WriteLine("Example: policy-dsl-validator --strict docs/examples/policies");
return 64; // EX_USAGE
}
var inputs = new List<string>();
var strict = false;
var outputJson = false;
foreach (var arg in args)
{
switch (arg)
{
case "--strict":
case "-s":
strict = true;
break;
case "--json":
case "-j":
outputJson = true;
break;
case "--help":
case "-h":
case "-?":
Console.WriteLine("Usage: policy-dsl-validator [--strict] [--json] <path-or-glob> [<path-or-glob> ...]");
Console.WriteLine("Example: policy-dsl-validator --strict docs/examples/policies");
return 0;
default:
inputs.Add(arg);
break;
}
}
if (inputs.Count == 0)
{
Console.Error.WriteLine("No input files or directories provided.");
return 64; // EX_USAGE
}
var options = new PolicyValidationCliOptions
{
Inputs = inputs,
Strict = strict,
OutputJson = outputJson,
};
var cli = new PolicyValidationCli();
var exitCode = await cli.RunAsync(options, CancellationToken.None);
return exitCode;

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NJsonSchema" Version="11.5.1" />
<PackageReference Include="NJsonSchema.SystemTextJson" Version="11.5.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Scheduler.Models\StellaOps.Scheduler.Models.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,48 @@
using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Serialization;
using NJsonSchema;
using NJsonSchema.Generation;
using NJsonSchema.Generation.SystemTextJson;
using Newtonsoft.Json;
using StellaOps.Scheduler.Models;
var output = args.Length switch
{
0 => Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "docs", "schemas")),
1 => Path.GetFullPath(args[0]),
_ => throw new ArgumentException("Usage: dotnet run --project src/Tools/PolicySchemaExporter -- [outputDirectory]")
};
Directory.CreateDirectory(output);
var generatorSettings = new SystemTextJsonSchemaGeneratorSettings
{
SchemaType = SchemaType.JsonSchema,
DefaultReferenceTypeNullHandling = ReferenceTypeNullHandling.NotNull,
SerializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
},
};
var generator = new JsonSchemaGenerator(generatorSettings);
var exports = ImmutableArray.Create(
(FileName: "policy-run-request.schema.json", Type: typeof(PolicyRunRequest)),
(FileName: "policy-run-status.schema.json", Type: typeof(PolicyRunStatus)),
(FileName: "policy-diff-summary.schema.json", Type: typeof(PolicyDiffSummary)),
(FileName: "policy-explain-trace.schema.json", Type: typeof(PolicyExplainTrace))
);
foreach (var export in exports)
{
var schema = generator.Generate(export.Type);
schema.Title = export.Type.Name;
schema.AllowAdditionalProperties = false;
var outputPath = Path.Combine(output, export.FileName);
await File.WriteAllTextAsync(outputPath, schema.ToJson(Formatting.Indented) + Environment.NewLine);
Console.WriteLine($"Wrote {outputPath}");
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\StellaOps.Policy\StellaOps.Policy.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,291 @@
using System.Collections.Immutable;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Policy;
var scenarioRoot = "samples/policy/simulations";
string? outputDir = null;
for (var i = 0; i < args.Length; i++)
{
var arg = args[i];
switch (arg)
{
case "--scenario-root":
case "-r":
if (i + 1 >= args.Length)
{
Console.Error.WriteLine("Missing value for --scenario-root.");
return 64;
}
scenarioRoot = args[++i];
break;
case "--output":
case "-o":
if (i + 1 >= args.Length)
{
Console.Error.WriteLine("Missing value for --output.");
return 64;
}
outputDir = args[++i];
break;
case "--help":
case "-h":
case "-?":
PrintUsage();
return 0;
default:
Console.Error.WriteLine($"Unknown argument '{arg}'.");
PrintUsage();
return 64;
}
}
if (!Directory.Exists(scenarioRoot))
{
Console.Error.WriteLine($"Scenario root '{scenarioRoot}' does not exist.");
return 66;
}
var scenarioFiles = Directory.GetFiles(scenarioRoot, "scenario.json", SearchOption.AllDirectories);
if (scenarioFiles.Length == 0)
{
Console.Error.WriteLine($"No scenario.json files found under '{scenarioRoot}'.");
return 0;
}
var loggerFactory = NullLoggerFactory.Instance;
var snapshotStore = new PolicySnapshotStore(
new NullPolicySnapshotRepository(),
new NullPolicyAuditRepository(),
TimeProvider.System,
loggerFactory.CreateLogger<PolicySnapshotStore>());
var previewService = new PolicyPreviewService(snapshotStore, loggerFactory.CreateLogger<PolicyPreviewService>());
var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
};
var summary = new List<ScenarioResult>();
var success = true;
foreach (var scenarioFile in scenarioFiles.OrderBy(static f => f, StringComparer.OrdinalIgnoreCase))
{
var scenarioText = await File.ReadAllTextAsync(scenarioFile);
var scenario = JsonSerializer.Deserialize<PolicySimulationScenario>(scenarioText, serializerOptions);
if (scenario is null)
{
Console.Error.WriteLine($"Failed to deserialize scenario '{scenarioFile}'.");
success = false;
continue;
}
var repoRoot = Directory.GetCurrentDirectory();
var policyPath = Path.Combine(repoRoot, scenario.PolicyPath);
if (!File.Exists(policyPath))
{
Console.Error.WriteLine($"Policy file '{scenario.PolicyPath}' referenced by scenario '{scenario.Name}' does not exist.");
success = false;
continue;
}
var policyContent = await File.ReadAllTextAsync(policyPath);
var policyFormat = PolicySchema.DetectFormat(policyPath);
var findings = scenario.Findings.Select(ToPolicyFinding).ToImmutableArray();
var baseline = scenario.Baseline?.Select(ToPolicyVerdict).ToImmutableArray() ?? ImmutableArray<PolicyVerdict>.Empty;
var request = new PolicyPreviewRequest(
ImageDigest: $"sha256:simulation-{scenario.Name}",
Findings: findings,
BaselineVerdicts: baseline,
SnapshotOverride: null,
ProposedPolicy: new PolicySnapshotContent(
Content: policyContent,
Format: policyFormat,
Actor: "ci",
Source: "ci/simulation-smoke",
Description: $"CI simulation for scenario '{scenario.Name}'"));
var response = await previewService.PreviewAsync(request, CancellationToken.None);
var scenarioResult = EvaluateScenario(scenario, response);
summary.Add(scenarioResult);
if (!scenarioResult.Success)
{
success = false;
}
}
if (outputDir is not null)
{
Directory.CreateDirectory(outputDir);
var summaryPath = Path.Combine(outputDir, "policy-simulation-summary.json");
await File.WriteAllTextAsync(summaryPath, JsonSerializer.Serialize(summary, new JsonSerializerOptions { WriteIndented = true }));
}
return success ? 0 : 1;
static void PrintUsage()
{
Console.WriteLine("Usage: policy-simulation-smoke [--scenario-root <path>] [--output <dir>]");
Console.WriteLine("Example: policy-simulation-smoke --scenario-root samples/policy/simulations --output artifacts/policy-simulations");
}
static PolicyFinding ToPolicyFinding(ScenarioFinding finding)
{
var tags = finding.Tags is null ? ImmutableArray<string>.Empty : ImmutableArray.CreateRange(finding.Tags);
var severity = Enum.Parse<PolicySeverity>(finding.Severity, ignoreCase: true);
return new PolicyFinding(
finding.FindingId,
severity,
finding.Environment,
finding.Source,
finding.Vendor,
finding.License,
finding.Image,
finding.Repository,
finding.Package,
finding.Purl,
finding.Cve,
finding.Path,
finding.LayerDigest,
tags);
}
static PolicyVerdict ToPolicyVerdict(ScenarioBaseline baseline)
{
var status = Enum.Parse<PolicyVerdictStatus>(baseline.Status, ignoreCase: true);
var inputs = baseline.Inputs?.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase) ?? ImmutableDictionary<string, double>.Empty;
return new PolicyVerdict(
baseline.FindingId,
status,
RuleName: baseline.RuleName,
RuleAction: baseline.RuleAction,
Notes: baseline.Notes,
Score: baseline.Score,
ConfigVersion: baseline.ConfigVersion ?? PolicyScoringConfig.Default.Version,
Inputs: inputs,
QuietedBy: null,
Quiet: false,
UnknownConfidence: null,
ConfidenceBand: null,
UnknownAgeDays: null,
SourceTrust: null,
Reachability: null);
}
static ScenarioResult EvaluateScenario(PolicySimulationScenario scenario, PolicyPreviewResponse response)
{
var result = new ScenarioResult(scenario.Name);
if (!response.Success)
{
result.Failures.Add("Preview failed.");
return result with { Success = false, ChangedCount = response.ChangedCount };
}
var diffs = response.Diffs.ToDictionary(diff => diff.Projected.FindingId, StringComparer.OrdinalIgnoreCase);
foreach (var expected in scenario.ExpectedDiffs)
{
if (!diffs.TryGetValue(expected.FindingId, out var diff))
{
result.Failures.Add($"Expected finding '{expected.FindingId}' missing from diff.");
continue;
}
var projectedStatus = diff.Projected.Status.ToString();
result.ActualStatuses[expected.FindingId] = projectedStatus;
if (!string.Equals(projectedStatus, expected.Status, StringComparison.OrdinalIgnoreCase))
{
result.Failures.Add($"Finding '{expected.FindingId}' expected status '{expected.Status}' but was '{projectedStatus}'.");
}
}
foreach (var diff in diffs.Values)
{
if (!result.ActualStatuses.ContainsKey(diff.Projected.FindingId))
{
result.ActualStatuses[diff.Projected.FindingId] = diff.Projected.Status.ToString();
}
}
var success = result.Failures.Count == 0;
return result with
{
Success = success,
ChangedCount = response.ChangedCount
};
}
internal sealed record PolicySimulationScenario
{
public string Name { get; init; } = "scenario";
public string PolicyPath { get; init; } = string.Empty;
public List<ScenarioFinding> Findings { get; init; } = new();
public List<ScenarioExpectedDiff> ExpectedDiffs { get; init; } = new();
public List<ScenarioBaseline>? Baseline { get; init; }
}
internal sealed record ScenarioFinding
{
public string FindingId { get; init; } = string.Empty;
public string Severity { get; init; } = "Low";
public string? Environment { get; init; }
public string? Source { get; init; }
public string? Vendor { get; init; }
public string? License { get; init; }
public string? Image { get; init; }
public string? Repository { get; init; }
public string? Package { get; init; }
public string? Purl { get; init; }
public string? Cve { get; init; }
public string? Path { get; init; }
public string? LayerDigest { get; init; }
public string[]? Tags { get; init; }
}
internal sealed record ScenarioExpectedDiff
{
public string FindingId { get; init; } = string.Empty;
public string Status { get; init; } = "Pass";
}
internal sealed record ScenarioBaseline
{
public string FindingId { get; init; } = string.Empty;
public string Status { get; init; } = "Pass";
public string? RuleName { get; init; }
public string? RuleAction { get; init; }
public string? Notes { get; init; }
public double Score { get; init; }
public string? ConfigVersion { get; init; }
public Dictionary<string, double>? Inputs { get; init; }
}
internal sealed record ScenarioResult(string ScenarioName)
{
public bool Success { get; init; } = true;
public int ChangedCount { get; init; }
public List<string> Failures { get; } = new();
public Dictionary<string, string> ActualStatuses { get; } = new(StringComparer.OrdinalIgnoreCase);
}
internal sealed class NullPolicySnapshotRepository : IPolicySnapshotRepository
{
public Task AddAsync(PolicySnapshot snapshot, CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task<PolicySnapshot?> GetLatestAsync(CancellationToken cancellationToken = default) => Task.FromResult<PolicySnapshot?>(null);
public Task<IReadOnlyList<PolicySnapshot>> ListAsync(int limit, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<PolicySnapshot>>(Array.Empty<PolicySnapshot>());
}
internal sealed class NullPolicyAuditRepository : IPolicyAuditRepository
{
public Task AddAsync(PolicyAuditEntry entry, CancellationToken cancellationToken = default) => Task.CompletedTask;
public Task<IReadOnlyList<PolicyAuditEntry>> ListAsync(int limit, CancellationToken cancellationToken = default)
=> Task.FromResult<IReadOnlyList<PolicyAuditEntry>>(Array.Empty<PolicyAuditEntry>());
}

View File

@@ -0,0 +1,286 @@
using Amazon;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using System.Net.Http.Headers;
var options = MigrationOptions.Parse(args);
if (options is null)
{
MigrationOptions.PrintUsage();
return 1;
}
Console.WriteLine($"RustFS migrator starting (prefix: '{options.Prefix ?? "<all>"}')");
if (options.DryRun)
{
Console.WriteLine("Dry-run enabled. No objects will be written to RustFS.");
}
var s3Config = new AmazonS3Config
{
ForcePathStyle = true,
};
if (!string.IsNullOrWhiteSpace(options.S3ServiceUrl))
{
s3Config.ServiceURL = options.S3ServiceUrl;
s3Config.UseHttp = options.S3ServiceUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase);
}
if (!string.IsNullOrWhiteSpace(options.S3Region))
{
s3Config.RegionEndpoint = RegionEndpoint.GetBySystemName(options.S3Region);
}
using var s3Client = CreateS3Client(options, s3Config);
using var httpClient = CreateRustFsClient(options);
var listRequest = new ListObjectsV2Request
{
BucketName = options.S3Bucket,
Prefix = options.Prefix,
MaxKeys = 1000,
};
var migrated = 0;
var skipped = 0;
do
{
var response = await s3Client.ListObjectsV2Async(listRequest).ConfigureAwait(false);
foreach (var entry in response.S3Objects)
{
if (entry.Size == 0 && entry.Key.EndsWith('/'))
{
skipped++;
continue;
}
Console.WriteLine($"Migrating {entry.Key} ({entry.Size} bytes)...");
if (options.DryRun)
{
migrated++;
continue;
}
using var getResponse = await s3Client.GetObjectAsync(new GetObjectRequest
{
BucketName = options.S3Bucket,
Key = entry.Key,
}).ConfigureAwait(false);
await using var memory = new MemoryStream();
await getResponse.ResponseStream.CopyToAsync(memory).ConfigureAwait(false);
memory.Position = 0;
using var request = new HttpRequestMessage(HttpMethod.Put, BuildRustFsUri(options, entry.Key))
{
Content = new ByteArrayContent(memory.ToArray()),
};
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/octet-stream");
if (options.Immutable)
{
request.Headers.TryAddWithoutValidation("X-RustFS-Immutable", "true");
}
if (options.RetentionSeconds is { } retainSeconds)
{
request.Headers.TryAddWithoutValidation("X-RustFS-Retain-Seconds", retainSeconds.ToString());
}
if (!string.IsNullOrWhiteSpace(options.RustFsApiKeyHeader) && !string.IsNullOrWhiteSpace(options.RustFsApiKey))
{
request.Headers.TryAddWithoutValidation(options.RustFsApiKeyHeader!, options.RustFsApiKey!);
}
using var responseMessage = await httpClient.SendAsync(request).ConfigureAwait(false);
if (!responseMessage.IsSuccessStatusCode)
{
var error = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
Console.Error.WriteLine($"Failed to upload {entry.Key}: {(int)responseMessage.StatusCode} {responseMessage.ReasonPhrase}\n{error}");
return 2;
}
migrated++;
}
listRequest.ContinuationToken = response.NextContinuationToken;
} while (!string.IsNullOrEmpty(listRequest.ContinuationToken));
Console.WriteLine($"Migration complete. Migrated {migrated} objects. Skipped {skipped} directory markers.");
return 0;
static AmazonS3Client CreateS3Client(MigrationOptions options, AmazonS3Config config)
{
if (!string.IsNullOrWhiteSpace(options.S3AccessKey) && !string.IsNullOrWhiteSpace(options.S3SecretKey))
{
var credentials = new BasicAWSCredentials(options.S3AccessKey, options.S3SecretKey);
return new AmazonS3Client(credentials, config);
}
return new AmazonS3Client(config);
}
static HttpClient CreateRustFsClient(MigrationOptions options)
{
var client = new HttpClient
{
BaseAddress = new Uri(options.RustFsEndpoint, UriKind.Absolute),
Timeout = TimeSpan.FromMinutes(5),
};
if (!string.IsNullOrWhiteSpace(options.RustFsApiKeyHeader) && !string.IsNullOrWhiteSpace(options.RustFsApiKey))
{
client.DefaultRequestHeaders.TryAddWithoutValidation(options.RustFsApiKeyHeader, options.RustFsApiKey);
}
return client;
}
static Uri BuildRustFsUri(MigrationOptions options, string key)
{
var normalized = string.Join('/', key
.Split('/', StringSplitOptions.RemoveEmptyEntries)
.Select(Uri.EscapeDataString));
var builder = new UriBuilder(options.RustFsEndpoint)
{
Path = $"/api/v1/buckets/{Uri.EscapeDataString(options.RustFsBucket)}/objects/{normalized}",
};
return builder.Uri;
}
internal sealed record MigrationOptions
{
public string S3Bucket { get; init; } = string.Empty;
public string? S3ServiceUrl { get; init; }
= null;
public string? S3Region { get; init; }
= null;
public string? S3AccessKey { get; init; }
= null;
public string? S3SecretKey { get; init; }
= null;
public string RustFsEndpoint { get; init; } = string.Empty;
public string RustFsBucket { get; init; } = string.Empty;
public string? RustFsApiKeyHeader { get; init; }
= null;
public string? RustFsApiKey { get; init; }
= null;
public string? Prefix { get; init; }
= null;
public bool Immutable { get; init; }
= false;
public int? RetentionSeconds { get; init; }
= null;
public bool DryRun { get; init; }
= false;
public static MigrationOptions? Parse(string[] args)
{
var builder = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < args.Length; i++)
{
var key = args[i];
if (key.StartsWith("--", StringComparison.OrdinalIgnoreCase))
{
var normalized = key[2..];
if (string.Equals(normalized, "immutable", StringComparison.OrdinalIgnoreCase) || string.Equals(normalized, "dry-run", StringComparison.OrdinalIgnoreCase))
{
builder[normalized] = "true";
continue;
}
if (i + 1 >= args.Length)
{
Console.Error.WriteLine($"Missing value for argument '{key}'.");
return null;
}
builder[normalized] = args[++i];
}
}
if (!builder.TryGetValue("s3-bucket", out var bucket) || string.IsNullOrWhiteSpace(bucket))
{
Console.Error.WriteLine("--s3-bucket is required.");
return null;
}
if (!builder.TryGetValue("rustfs-endpoint", out var rustFsEndpoint) || string.IsNullOrWhiteSpace(rustFsEndpoint))
{
Console.Error.WriteLine("--rustfs-endpoint is required.");
return null;
}
if (!builder.TryGetValue("rustfs-bucket", out var rustFsBucket) || string.IsNullOrWhiteSpace(rustFsBucket))
{
Console.Error.WriteLine("--rustfs-bucket is required.");
return null;
}
int? retentionSeconds = null;
if (builder.TryGetValue("retain-days", out var retainStr) && !string.IsNullOrWhiteSpace(retainStr))
{
if (double.TryParse(retainStr, out var days) && days > 0)
{
retentionSeconds = (int)Math.Ceiling(days * 24 * 60 * 60);
}
else
{
Console.Error.WriteLine("--retain-days must be a positive number.");
return null;
}
}
return new MigrationOptions
{
S3Bucket = bucket,
S3ServiceUrl = builder.TryGetValue("s3-endpoint", out var s3Endpoint) ? s3Endpoint : null,
S3Region = builder.TryGetValue("s3-region", out var s3Region) ? s3Region : null,
S3AccessKey = builder.TryGetValue("s3-access-key", out var s3AccessKey) ? s3AccessKey : null,
S3SecretKey = builder.TryGetValue("s3-secret-key", out var s3SecretKey) ? s3SecretKey : null,
RustFsEndpoint = rustFsEndpoint!,
RustFsBucket = rustFsBucket!,
RustFsApiKeyHeader = builder.TryGetValue("rustfs-api-key-header", out var apiKeyHeader) ? apiKeyHeader : null,
RustFsApiKey = builder.TryGetValue("rustfs-api-key", out var apiKey) ? apiKey : null,
Prefix = builder.TryGetValue("prefix", out var prefix) ? prefix : null,
Immutable = builder.ContainsKey("immutable"),
RetentionSeconds = retentionSeconds,
DryRun = builder.ContainsKey("dry-run"),
};
}
public static void PrintUsage()
{
Console.WriteLine(@"Usage: dotnet run --project src/Tools/RustFsMigrator -- \
--s3-bucket <name> \
[--s3-endpoint http://minio:9000] \
[--s3-region us-east-1] \
[--s3-access-key key --s3-secret-key secret] \
--rustfs-endpoint http://rustfs:8080 \
--rustfs-bucket scanner-artifacts \
[--rustfs-api-key-header X-API-Key --rustfs-api-key token] \
[--prefix scanner/] \
[--immutable] \
[--retain-days 365] \
[--dry-run]");
}
}

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.7.305.6" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,346 @@
using System.Globalization;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using MongoDB.Driver;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Common.State;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Documents;
namespace SourceStateSeeder;
internal static class Program
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};
public static async Task<int> Main(string[] args)
{
try
{
var options = SeedOptions.Parse(args);
if (options is null)
{
SeedOptions.PrintUsage();
return 1;
}
var seed = await LoadSpecificationAsync(options.InputPath).ConfigureAwait(false);
var sourceName = seed.Source ?? options.SourceName;
if (string.IsNullOrWhiteSpace(sourceName))
{
Console.Error.WriteLine("Source name must be supplied via --source or the seed file.");
return 1;
}
var specification = await BuildSpecificationAsync(seed, sourceName, options.InputPath, CancellationToken.None).ConfigureAwait(false);
var client = new MongoClient(options.ConnectionString);
var database = client.GetDatabase(options.DatabaseName);
var loggerFactory = NullLoggerFactory.Instance;
var documentStore = new DocumentStore(database, loggerFactory.CreateLogger<DocumentStore>());
var rawStorage = new RawDocumentStorage(database);
var stateRepository = new MongoSourceStateRepository(database, loggerFactory.CreateLogger<MongoSourceStateRepository>());
var processor = new SourceStateSeedProcessor(
documentStore,
rawStorage,
stateRepository,
TimeProvider.System,
loggerFactory.CreateLogger<SourceStateSeedProcessor>());
var result = await processor.ProcessAsync(specification, CancellationToken.None).ConfigureAwait(false);
Console.WriteLine(
$"Seeded {result.DocumentsProcessed} document(s) for {sourceName} " +
$"(pendingDocuments+= {result.PendingDocumentsAdded}, pendingMappings+= {result.PendingMappingsAdded}, knownAdvisories+= {result.KnownAdvisoriesAdded.Count}).");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
return 1;
}
}
private static async Task<StateSeed> LoadSpecificationAsync(string inputPath)
{
await using var stream = File.OpenRead(inputPath);
var seed = await JsonSerializer.DeserializeAsync<StateSeed>(stream, JsonOptions).ConfigureAwait(false)
?? throw new InvalidOperationException("Input file deserialized to null.");
return seed;
}
private static async Task<SourceStateSeedSpecification> BuildSpecificationAsync(
StateSeed seed,
string sourceName,
string inputPath,
CancellationToken cancellationToken)
{
var baseDirectory = Path.GetDirectoryName(Path.GetFullPath(inputPath)) ?? Directory.GetCurrentDirectory();
var documents = new List<SourceStateSeedDocument>(seed.Documents.Count);
foreach (var documentSeed in seed.Documents)
{
documents.Add(await BuildDocumentAsync(documentSeed, baseDirectory, cancellationToken).ConfigureAwait(false));
}
return new SourceStateSeedSpecification
{
Source = sourceName,
Documents = documents.AsReadOnly(),
Cursor = BuildCursor(seed.Cursor),
KnownAdvisories = NormalizeStrings(seed.KnownAdvisories),
CompletedAt = seed.CompletedAt,
};
}
private static async Task<SourceStateSeedDocument> BuildDocumentAsync(
DocumentSeed seed,
string baseDirectory,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(seed.Uri))
{
throw new InvalidOperationException("Seed entry missing 'uri'.");
}
if (string.IsNullOrWhiteSpace(seed.ContentFile))
{
throw new InvalidOperationException($"Seed entry for '{seed.Uri}' missing 'contentFile'.");
}
var contentPath = ResolvePath(seed.ContentFile, baseDirectory);
if (!File.Exists(contentPath))
{
throw new FileNotFoundException($"Content file not found for '{seed.Uri}'.", contentPath);
}
var contentBytes = await File.ReadAllBytesAsync(contentPath, cancellationToken).ConfigureAwait(false);
var metadata = seed.Metadata is null
? null
: new Dictionary<string, string>(seed.Metadata, StringComparer.OrdinalIgnoreCase);
var headers = seed.Headers is null
? null
: new Dictionary<string, string>(seed.Headers, StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(seed.ContentType))
{
headers ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (!headers.ContainsKey("content-type"))
{
headers["content-type"] = seed.ContentType!;
}
}
return new SourceStateSeedDocument
{
Uri = seed.Uri,
DocumentId = seed.DocumentId,
Content = contentBytes,
ContentType = seed.ContentType,
Status = string.IsNullOrWhiteSpace(seed.Status) ? DocumentStatuses.PendingParse : seed.Status,
Headers = headers,
Metadata = metadata,
Etag = seed.Etag,
LastModified = ParseOptionalDate(seed.LastModified),
ExpiresAt = seed.ExpiresAt,
FetchedAt = ParseOptionalDate(seed.FetchedAt),
AddToPendingDocuments = seed.AddToPendingDocuments,
AddToPendingMappings = seed.AddToPendingMappings,
KnownIdentifiers = NormalizeStrings(seed.KnownIdentifiers),
};
}
private static SourceStateSeedCursor? BuildCursor(CursorSeed? cursorSeed)
{
if (cursorSeed is null)
{
return null;
}
return new SourceStateSeedCursor
{
PendingDocuments = NormalizeGuids(cursorSeed.PendingDocuments),
PendingMappings = NormalizeGuids(cursorSeed.PendingMappings),
KnownAdvisories = NormalizeStrings(cursorSeed.KnownAdvisories),
LastModifiedCursor = cursorSeed.LastModifiedCursor,
LastFetchAt = cursorSeed.LastFetchAt,
Additional = cursorSeed.Additional is null
? null
: new Dictionary<string, string>(cursorSeed.Additional, StringComparer.OrdinalIgnoreCase),
};
}
private static IReadOnlyCollection<Guid>? NormalizeGuids(IEnumerable<Guid>? values)
{
if (values is null)
{
return null;
}
var set = new HashSet<Guid>();
foreach (var guid in values)
{
if (guid != Guid.Empty)
{
set.Add(guid);
}
}
return set.Count == 0 ? null : set.ToList();
}
private static IReadOnlyCollection<string>? NormalizeStrings(IEnumerable<string>? values)
{
if (values is null)
{
return null;
}
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var value in values)
{
if (!string.IsNullOrWhiteSpace(value))
{
set.Add(value.Trim());
}
}
return set.Count == 0 ? null : set.ToList();
}
private static DateTimeOffset? ParseOptionalDate(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return DateTimeOffset.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
}
private static string ResolvePath(string path, string baseDirectory)
=> Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(baseDirectory, path));
}
internal sealed record SeedOptions
{
public required string ConnectionString { get; init; }
public required string DatabaseName { get; init; }
public required string InputPath { get; init; }
public string? SourceName { get; init; }
public static SeedOptions? Parse(string[] args)
{
string? connectionString = null;
string? database = null;
string? input = null;
string? source = null;
for (var i = 0; i < args.Length; i++)
{
var arg = args[i];
switch (arg)
{
case "--connection-string":
case "-c":
connectionString = TakeValue(args, ref i, arg);
break;
case "--database":
case "-d":
database = TakeValue(args, ref i, arg);
break;
case "--input":
case "-i":
input = TakeValue(args, ref i, arg);
break;
case "--source":
case "-s":
source = TakeValue(args, ref i, arg);
break;
case "--help":
case "-h":
return null;
default:
Console.Error.WriteLine($"Unrecognized argument '{arg}'.");
return null;
}
}
if (string.IsNullOrWhiteSpace(connectionString) || string.IsNullOrWhiteSpace(database) || string.IsNullOrWhiteSpace(input))
{
return null;
}
return new SeedOptions
{
ConnectionString = connectionString,
DatabaseName = database,
InputPath = input,
SourceName = source,
};
}
public static void PrintUsage()
{
Console.WriteLine("Usage: dotnet run --project src/Tools/SourceStateSeeder -- --connection-string <connection> --database <name> --input <seed.json> [--source <source>]");
}
private static string TakeValue(string[] args, ref int index, string arg)
{
if (index + 1 >= args.Length)
{
throw new ArgumentException($"Missing value for {arg}.");
}
index++;
return args[index];
}
}
internal sealed record StateSeed
{
public string? Source { get; init; }
public List<DocumentSeed> Documents { get; init; } = new();
public CursorSeed? Cursor { get; init; }
public List<string>? KnownAdvisories { get; init; }
public DateTimeOffset? CompletedAt { get; init; }
}
internal sealed record DocumentSeed
{
public string Uri { get; init; } = string.Empty;
public string ContentFile { get; init; } = string.Empty;
public Guid? DocumentId { get; init; }
public string? ContentType { get; init; }
public Dictionary<string, string>? Metadata { get; init; }
public Dictionary<string, string>? Headers { get; init; }
public string Status { get; init; } = DocumentStatuses.PendingParse;
public bool AddToPendingDocuments { get; init; } = true;
public bool AddToPendingMappings { get; init; }
public string? LastModified { get; init; }
public string? FetchedAt { get; init; }
public string? Etag { get; init; }
public DateTimeOffset? ExpiresAt { get; init; }
public List<string>? KnownIdentifiers { get; init; }
}
internal sealed record CursorSeed
{
public List<Guid>? PendingDocuments { get; init; }
public List<Guid>? PendingMappings { get; init; }
public List<string>? KnownAdvisories { get; init; }
public DateTimeOffset? LastModifiedCursor { get; init; }
public DateTimeOffset? LastFetchAt { get; init; }
public Dictionary<string, string>? Additional { get; init; }
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\StellaOps.Concelier.Connector.Common\StellaOps.Concelier.Connector.Common.csproj" />
<ProjectReference Include="..\..\src\StellaOps.Concelier.Storage.Mongo\StellaOps.Concelier.Storage.Mongo.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,444 @@
#!/usr/bin/env python3
"""
Capture CERT-Bund search/export JSON snapshots and generate Offline Kit manifests.
The script can bootstrap a session against https://wid.cert-bund.de, fetch
paginated search results plus per-year export payloads, and emit a manifest
that records source, date range, SHA-256, and capture timestamps for each artefact.
"""
from __future__ import annotations
import argparse
import datetime as dt
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from http.cookiejar import MozillaCookieJar
from typing import Any, Dict, Iterable, List, Optional
PORTAL_ROOT = "https://wid.cert-bund.de/portal/"
SEARCH_ENDPOINT = "https://wid.cert-bund.de/portal/api/securityadvisory/search"
EXPORT_ENDPOINT = "https://wid.cert-bund.de/portal/api/securityadvisory/export"
CSRF_ENDPOINT = "https://wid.cert-bund.de/portal/api/security/csrf"
USER_AGENT = "StellaOps.CertBundOffline/0.1"
UTC = dt.timezone.utc
class CertBundClient:
def __init__(
self,
cookie_file: Optional[Path] = None,
xsrf_token: Optional[str] = None,
auto_bootstrap: bool = True,
) -> None:
self.cookie_path = cookie_file
self.cookie_jar = MozillaCookieJar()
if self.cookie_path and self.cookie_path.exists():
self.cookie_jar.load(self.cookie_path, ignore_discard=True, ignore_expires=True)
self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.cookie_jar))
self.opener.addheaders = [("User-Agent", USER_AGENT)]
self._xsrf_token = xsrf_token
self.auto_bootstrap = auto_bootstrap
if self.auto_bootstrap and not self._xsrf_token:
self._bootstrap()
@property
def xsrf_token(self) -> str:
if self._xsrf_token:
return self._xsrf_token
token = _extract_cookie_value(self.cookie_jar, "XSRF-TOKEN")
if token:
self._xsrf_token = token
return token
raise RuntimeError(
"CERT-Bund XSRF token not available. Provide --xsrf-token or a cookie file "
"containing XSRF-TOKEN (see docs/modules/concelier/operations/connectors/certbund.md)."
)
def fetch_search_pages(
self,
destination: Path,
page_size: int,
max_pages: int,
) -> None:
destination.mkdir(parents=True, exist_ok=True)
for page in range(max_pages):
payload = {
"page": page,
"size": page_size,
"sort": ["published,desc"],
}
try:
document = self._post_json(SEARCH_ENDPOINT, payload)
except urllib.error.HTTPError as exc:
raise RuntimeError(
f"Failed to fetch CERT-Bund search page {page}: HTTP {exc.code}. "
"Double-check the XSRF token or portal cookies."
) from exc
content = document.get("content") or []
if not content and page > 0:
break
file_path = destination / f"certbund-search-page-{page:02d}.json"
_write_pretty_json(file_path, document)
print(f"[certbund] wrote search page {page:02d}{file_path}")
if not content:
break
self._persist_cookies()
def fetch_exports(self, destination: Path, start_year: int, end_year: int) -> None:
destination.mkdir(parents=True, exist_ok=True)
for year in range(start_year, end_year + 1):
from_value = f"{year}-01-01"
to_value = f"{year}-12-31"
query = urllib.parse.urlencode({"format": "json", "from": from_value, "to": to_value})
url = f"{EXPORT_ENDPOINT}?{query}"
try:
document = self._get_json(url)
except urllib.error.HTTPError as exc:
raise RuntimeError(
f"Failed to fetch CERT-Bund export for {year}: HTTP {exc.code}. "
"Ensure the XSRF token and cookies are valid."
) from exc
file_path = destination / f"certbund-export-{year}.json"
_write_pretty_json(file_path, document)
print(f"[certbund] wrote export {year}{file_path}")
self._persist_cookies()
def _bootstrap(self) -> None:
try:
self._request("GET", PORTAL_ROOT, headers={"Accept": "text/html,application/xhtml+xml"})
except urllib.error.HTTPError as exc:
raise RuntimeError(f"Failed to bootstrap CERT-Bund session: HTTP {exc.code}") from exc
# First attempt to obtain CSRF token directly.
self._attempt_csrf_fetch()
if _extract_cookie_value(self.cookie_jar, "XSRF-TOKEN"):
return
# If the token is still missing, trigger the search endpoint once (likely 403)
# to make the portal materialise JSESSIONID, then retry token acquisition.
try:
payload = {"page": 0, "size": 1, "sort": ["published,desc"]}
self._post_json(SEARCH_ENDPOINT, payload, include_token=False)
except urllib.error.HTTPError:
pass
self._attempt_csrf_fetch()
token = _extract_cookie_value(self.cookie_jar, "XSRF-TOKEN")
if token:
self._xsrf_token = token
else:
print(
"[certbund] warning: automatic XSRF token retrieval failed. "
"Supply --xsrf-token or reuse a browser-exported cookies file.",
file=sys.stderr,
)
def _attempt_csrf_fetch(self) -> None:
headers = {
"Accept": "application/json, text/plain, */*",
"X-Requested-With": "XMLHttpRequest",
"Origin": "https://wid.cert-bund.de",
"Referer": PORTAL_ROOT,
}
try:
self._request("GET", CSRF_ENDPOINT, headers=headers)
except urllib.error.HTTPError:
pass
def _request(self, method: str, url: str, data: Optional[bytes] = None, headers: Optional[Dict[str, str]] = None) -> bytes:
request = urllib.request.Request(url, data=data, method=method)
default_headers = {
"User-Agent": USER_AGENT,
"Accept": "application/json",
}
for key, value in default_headers.items():
request.add_header(key, value)
if headers:
for key, value in headers.items():
request.add_header(key, value)
return self.opener.open(request, timeout=60).read()
def _post_json(self, url: str, payload: Dict[str, Any], include_token: bool = True) -> Dict[str, Any]:
data = json.dumps(payload).encode("utf-8")
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Requested-With": "XMLHttpRequest",
"Origin": "https://wid.cert-bund.de",
"Referer": PORTAL_ROOT,
}
if include_token:
headers["X-XSRF-TOKEN"] = self.xsrf_token
raw = self._request("POST", url, data=data, headers=headers)
return json.loads(raw.decode("utf-8"))
def _get_json(self, url: str) -> Any:
headers = {
"Accept": "application/json",
"X-Requested-With": "XMLHttpRequest",
"Referer": PORTAL_ROOT,
}
headers["X-XSRF-TOKEN"] = self.xsrf_token
raw = self._request("GET", url, headers=headers)
return json.loads(raw.decode("utf-8"))
def _persist_cookies(self) -> None:
if not self.cookie_path:
return
self.cookie_path.parent.mkdir(parents=True, exist_ok=True)
self.cookie_jar.save(self.cookie_path, ignore_discard=True, ignore_expires=True)
def _extract_cookie_value(jar: MozillaCookieJar, name: str) -> Optional[str]:
for cookie in jar:
if cookie.name == name:
return cookie.value
return None
def _write_pretty_json(path: Path, document: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as handle:
json.dump(document, handle, ensure_ascii=False, indent=2, sort_keys=True)
handle.write("\n")
def scan_artifacts(root: Path) -> List[Dict[str, Any]]:
records: List[Dict[str, Any]] = []
search_dir = root / "search"
export_dir = root / "export"
if search_dir.exists():
for file_path in sorted(search_dir.glob("certbund-search-page-*.json")):
record = _build_search_record(file_path)
records.append(record)
if export_dir.exists():
for file_path in sorted(export_dir.glob("certbund-export-*.json")):
record = _build_export_record(file_path)
records.append(record)
return records
def _build_search_record(path: Path) -> Dict[str, Any]:
with path.open("r", encoding="utf-8") as handle:
data = json.load(handle)
content = data.get("content") or []
published_values: List[str] = []
for item in content:
published = (
item.get("published")
or item.get("publishedAt")
or item.get("datePublished")
or item.get("published_date")
)
if isinstance(published, str):
published_values.append(published)
if published_values:
try:
ordered = sorted(_parse_iso_timestamp(value) for value in published_values if value)
range_from = ordered[0].isoformat()
range_to = ordered[-1].isoformat()
except ValueError:
range_from = range_to = None
else:
range_from = range_to = None
return {
"type": "search",
"path": path,
"source": "concelier.cert-bund.search",
"itemCount": len(content),
"from": range_from,
"to": range_to,
"capturedAt": _timestamp_from_stat(path),
}
def _build_export_record(path: Path) -> Dict[str, Any]:
year = _extract_year_from_filename(path.name)
if year is not None:
from_value = f"{year}-01-01"
to_value = f"{year}-12-31"
else:
from_value = None
to_value = None
return {
"type": "export",
"path": path,
"source": "concelier.cert-bund.export",
"itemCount": None,
"from": from_value,
"to": to_value,
"capturedAt": _timestamp_from_stat(path),
}
def _timestamp_from_stat(path: Path) -> str:
stat = path.stat()
return dt.datetime.fromtimestamp(stat.st_mtime, tz=UTC).isoformat()
def _extract_year_from_filename(name: str) -> Optional[int]:
stem = Path(name).stem
parts = stem.split("-")
if parts and parts[-1].isdigit() and len(parts[-1]) == 4:
return int(parts[-1])
return None
def _parse_iso_timestamp(value: str) -> dt.datetime:
try:
return dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
# Fallback for formats like 2025-10-14T06:24:49
return dt.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S").replace(tzinfo=UTC)
def build_manifest(root: Path, records: Iterable[Dict[str, Any]], manifest_path: Path) -> None:
manifest_entries = []
for record in records:
path = record["path"]
rel_path = PurePosixPath(path.relative_to(root).as_posix())
sha256 = hashlib.sha256(path.read_bytes()).hexdigest()
size = path.stat().st_size
entry = {
"source": record["source"],
"type": record["type"],
"path": str(rel_path),
"sha256": sha256,
"sizeBytes": size,
"capturedAt": record["capturedAt"],
"from": record.get("from"),
"to": record.get("to"),
"itemCount": record.get("itemCount"),
}
manifest_entries.append(entry)
sha_file = path.with_suffix(path.suffix + ".sha256")
_write_sha_file(sha_file, sha256, path.name)
manifest_entries.sort(key=lambda item: item["path"])
manifest_path.parent.mkdir(parents=True, exist_ok=True)
manifest_document = {
"source": "concelier.cert-bund",
"generatedAt": dt.datetime.now(tz=UTC).isoformat(),
"artifacts": manifest_entries,
}
with manifest_path.open("w", encoding="utf-8") as handle:
json.dump(manifest_document, handle, ensure_ascii=False, indent=2, sort_keys=True)
handle.write("\n")
manifest_sha = hashlib.sha256(manifest_path.read_bytes()).hexdigest()
_write_sha_file(manifest_path.with_suffix(".sha256"), manifest_sha, manifest_path.name)
print(f"[certbund] manifest generated → {manifest_path}")
def _write_sha_file(path: Path, digest: str, filename: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as handle:
handle.write(f"{digest} {filename}\n")
def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Capture CERT-Bund search/export snapshots for Offline Kit packaging.",
)
parser.add_argument("--output", default="seed-data/cert-bund", help="Destination directory for artefacts.")
parser.add_argument("--start-year", type=int, default=2014, help="First year (inclusive) for export snapshots.")
parser.add_argument(
"--end-year",
type=int,
default=dt.datetime.now(tz=UTC).year,
help="Last year (inclusive) for export snapshots.",
)
parser.add_argument("--page-size", type=int, default=100, help="Search page size.")
parser.add_argument("--max-pages", type=int, default=12, help="Maximum number of search result pages to capture.")
parser.add_argument("--cookie-file", type=Path, help="Path to a Netscape cookie file to reuse/persist session cookies.")
parser.add_argument("--xsrf-token", help="Optional explicit XSRF token value (overrides cookie discovery).")
parser.add_argument(
"--skip-fetch",
action="store_true",
help="Skip HTTP fetches and only regenerate manifest from existing files.",
)
parser.add_argument(
"--no-bootstrap",
action="store_true",
help="Do not attempt automatic session bootstrap (use with --skip-fetch or pre-populated cookies).",
)
return parser.parse_args(argv)
def main(argv: Optional[List[str]] = None) -> int:
args = parse_args(argv)
output_dir = Path(args.output).expanduser().resolve()
if not args.skip_fetch:
client = CertBundClient(
cookie_file=args.cookie_file,
xsrf_token=args.xsrf_token,
auto_bootstrap=not args.no_bootstrap,
)
start_year = args.start_year
end_year = args.end_year
if start_year > end_year:
raise SystemExit("start-year cannot be greater than end-year.")
client.fetch_search_pages(output_dir / "search", args.page_size, args.max_pages)
client.fetch_exports(output_dir / "export", start_year, end_year)
records = scan_artifacts(output_dir)
if not records:
print(
"[certbund] no artefacts discovered. Fetch data first or point --output to the dataset directory.",
file=sys.stderr,
)
return 1
manifest_path = output_dir / "manifest" / "certbund-offline-manifest.json"
build_manifest(output_dir, records, manifest_path)
return 0
if __name__ == "__main__":
sys.exit(main())

Binary file not shown.

Binary file not shown.

View File

@@ -1,105 +1,105 @@
# Zastava Webhook · Wave 0 Implementation Notes
> Authored 2025-10-19 by Zastava Webhook Guild.
## ZASTAVA-WEBHOOK-12-101 — Admission Controller Host (TLS bootstrap + Authority auth)
**Objectives**
- Provide a deterministic, restart-safe .NET 10 host that exposes a Kubernetes ValidatingAdmissionWebhook endpoint.
- Load serving certificates at start-up only (per restart-time plug-in rule) and surface reload guidance via documentation rather than hot-reload.
- Authenticate outbound calls to Authority/Scanner using OpTok + DPoP as defined in `docs/ARCHITECTURE_ZASTAVA.md`.
**Plan**
1. **Project scaffolding**
- Create `StellaOps.Zastava.Webhook` project with minimal API pipeline (`Program.cs`, `Startup` equivalent via extension methods).
- Reference shared helpers once `ZASTAVA-CORE-12-201/202` land; temporarily stub interfaces behind `IZastavaAdmissionRequest`/`IZastavaAdmissionResult`.
2. **TLS bootstrap**
- Support two certificate sources:
1. Mounted secret path (`/var/run/secrets/zastava-webhook/tls.{crt,key}`) with optional CA bundle.
2. CSR workflow: generate CSR + private key, submit to Kubernetes Certificates API when `admission.tls.autoApprove` enabled; persist signed cert/key to mounted emptyDir for reuse across replicas.
- Validate cert/key pair on boot; abort start-up if invalid to preserve deterministic behavior.
- Configure Kestrel for mutual TLS off (API Server already provides client auth) but enforce minimum TLS 1.3, strong cipher suite list, HTTP/2 disabled (K8s uses HTTP/1.1).
# Zastava Webhook · Wave 0 Implementation Notes
> Authored 2025-10-19 by Zastava Webhook Guild.
## ZASTAVA-WEBHOOK-12-101 — Admission Controller Host (TLS bootstrap + Authority auth)
**Objectives**
- Provide a deterministic, restart-safe .NET 10 host that exposes a Kubernetes ValidatingAdmissionWebhook endpoint.
- Load serving certificates at start-up only (per restart-time plug-in rule) and surface reload guidance via documentation rather than hot-reload.
- Authenticate outbound calls to Authority/Scanner using OpTok + DPoP as defined in `docs/modules/zastava/ARCHITECTURE.md`.
**Plan**
1. **Project scaffolding**
- Create `StellaOps.Zastava.Webhook` project with minimal API pipeline (`Program.cs`, `Startup` equivalent via extension methods).
- Reference shared helpers once `ZASTAVA-CORE-12-201/202` land; temporarily stub interfaces behind `IZastavaAdmissionRequest`/`IZastavaAdmissionResult`.
2. **TLS bootstrap**
- Support two certificate sources:
1. Mounted secret path (`/var/run/secrets/zastava-webhook/tls.{crt,key}`) with optional CA bundle.
2. CSR workflow: generate CSR + private key, submit to Kubernetes Certificates API when `admission.tls.autoApprove` enabled; persist signed cert/key to mounted emptyDir for reuse across replicas.
- Validate cert/key pair on boot; abort start-up if invalid to preserve deterministic behavior.
- Configure Kestrel for mutual TLS off (API Server already provides client auth) but enforce minimum TLS 1.3, strong cipher suite list, HTTP/2 disabled (K8s uses HTTP/1.1).
3. **Authority auth**
- Bootstrap Authority client via shared runtime core (`AddZastavaRuntimeCore` + `IZastavaAuthorityTokenProvider`) so webhook reuses multitenant OpTok caching and guardrails.
- Implement DPoP proof generator bound to webhook host keypair (prefer Ed25519) with configurable rotation period (default 24h, triggered at restart).
- Add background health check verifying token freshness and surfacing metrics (`zastava.authority_token_renew_failures_total`).
4. **Hosting concerns**
- Configure structured logging with correlation id from AdmissionReview UID.
- Expose `/healthz` (reads cert expiry, Authority token status) and `/metrics` (Prometheus).
- Add readiness gate that requires initial TLS and Authority bootstrap to succeed.
**Deliverables**
- Compilable host project with integration tests covering TLS load (mounted files + CSR mock) and Authority token acquisition.
- Documentation snippet for deploy charts describing secret/CSR wiring.
**Open Questions**
- Need confirmation from Core guild on DTO naming (`AdmissionReviewEnvelope`, `AdmissionDecision`) to avoid rework.
- Determine whether CSR auto-approval is acceptable for air-gapped clusters without Kubernetes cert-manager; may require fallback manual cert import path.
## ZASTAVA-WEBHOOK-12-102 — Backend policy query & digest resolution
**Objectives**
- Resolve all images within AdmissionReview to immutable digests before policy evaluation.
- Call Scanner WebService `/api/v1/scanner/policy/runtime` with namespace/labels/images payload, enforce verdicts with deterministic error messaging.
**Plan**
1. **Image resolution**
- Implement resolver service with pluggable strategies:
- Use existing digest if present.
- Resolve tags via registry HEAD (respecting `admission.resolveTags` flag); fallback to Observer-provided digest once core DTOs available.
- Cache per-registry auth to minimise latency; adhere to allow/deny lists from configuration.
2. **Scanner client**
- Define typed request/response models mirroring `docs/ARCHITECTURE_ZASTAVA.md` structure (`ttlSeconds`, `results[digest] -> { signed, hasSbom, policyVerdict, reasons, rekor }`).
- Implement retry policy (3 attempts, exponential backoff) and map HTTP errors to webhook fail-open/closed depending on namespace configuration.
- Instrument latency (`zastava.backend_latency_seconds`) and failure counts.
3. **Verdict enforcement**
- Evaluate per-image results: if any `policyVerdict != pass` (or `warn` when `enforceWarnings=false`), deny with aggregated reasons.
- Attach `ttlSeconds` to admission response annotations for auditing.
- Record structured logs with namespace, pod, image digest, decision, reasons, backend latency.
4. **Contract coordination**
- Schedule joint review with Scanner WebService guild once SCANNER-RUNTIME-12-302 schema stabilises; track in TASKS sub-items.
- Provide sample payload fixtures for CLI team (`CLI-RUNTIME-13-005`) to validate table output; ensure field names stay aligned.
**Deliverables**
- Registry resolver unit tests (tag->digest) with deterministic fixtures.
- HTTP client integration tests using Scanner stub returning varied verdict combinations.
- Documentation update summarising contract and failure handling.
**Open Questions**
- Confirm expected policy verdict enumeration (`pass|warn|fail|error`?) and textual reason codes.
- Need TTL behaviour: should webhook reduce TTL when backend returns > configured max?
## ZASTAVA-WEBHOOK-12-103 — Caching, fail-open/closed toggles, metrics/logging
**Objectives**
- Provide deterministic caching layer respecting backend TTL while ensuring eviction on policy mutation.
- Allow namespace-scoped fail-open behaviour with explicit metrics and alerts.
- Surface actionable metrics/logging aligned with Architecture doc.
**Plan**
1. **Cache design**
- In-memory LRU keyed by image digest; value carries verdict payload + expiry timestamp.
- Support optional persistent seed (read-only) to prime hot digests for offline clusters (config: `admission.cache.seedPath`).
- On startup, load seed file and emit metric `zastava.cache_seed_entries_total`.
- Evict entries on TTL or when `policyRevision` annotation in AdmissionReview changes (requires hook from Core DTO).
2. **Fail-open/closed toggles**
- Configuration: global default + namespace overrides through `admission.failOpenNamespaces`, `admission.failClosedNamespaces`.
- Decision matrix:
- Backend success + verdict PASS → allow.
- Backend success + non-pass → deny unless namespace override says warn allowed.
- Backend failure → allow if namespace fail-open, deny otherwise; annotate response with `zastava.ops/fail-open=true`.
- Implement policy change event hook (future) to clear cache if observer signals revocation.
3. **Metrics & logging**
- Counters: `zastava.admission_requests_total{decision}`, `zastava.cache_hits_total{result=hit|miss}`, `zastava.fail_open_total`, `zastava.backend_failures_total{stage}`.
- Histograms: `zastava.admission_latency_seconds` (overall), `zastava.resolve_latency_seconds`.
- Logs: structured JSON with `decision`, `namespace`, `pod`, `imageDigest`, `reasons`, `cacheStatus`, `failMode`.
- Optionally emit OpenTelemetry span for admission path with attributes capturing backend latency + cache path.
4. **Testing & ops hooks**
- Unit tests for cache TTL, namespace override logic, fail-open metric increments.
- Integration test simulating backend outage ensuring fail-open/closed behaviour matches config.
- Document runbook snippet describing interpreting metrics and toggling namespaces.
**Open Questions**
- Confirm whether cache entries should include `policyRevision` to detect backend policy updates; requires coordination with Policy guild.
- Need guidance on maximum cache size (default suggestions: 5k entries per replica?) to avoid memory blow-up.
- Implement DPoP proof generator bound to webhook host keypair (prefer Ed25519) with configurable rotation period (default 24h, triggered at restart).
- Add background health check verifying token freshness and surfacing metrics (`zastava.authority_token_renew_failures_total`).
4. **Hosting concerns**
- Configure structured logging with correlation id from AdmissionReview UID.
- Expose `/healthz` (reads cert expiry, Authority token status) and `/metrics` (Prometheus).
- Add readiness gate that requires initial TLS and Authority bootstrap to succeed.
**Deliverables**
- Compilable host project with integration tests covering TLS load (mounted files + CSR mock) and Authority token acquisition.
- Documentation snippet for deploy charts describing secret/CSR wiring.
**Open Questions**
- Need confirmation from Core guild on DTO naming (`AdmissionReviewEnvelope`, `AdmissionDecision`) to avoid rework.
- Determine whether CSR auto-approval is acceptable for air-gapped clusters without Kubernetes cert-manager; may require fallback manual cert import path.
## ZASTAVA-WEBHOOK-12-102 — Backend policy query & digest resolution
**Objectives**
- Resolve all images within AdmissionReview to immutable digests before policy evaluation.
- Call Scanner WebService `/api/v1/scanner/policy/runtime` with namespace/labels/images payload, enforce verdicts with deterministic error messaging.
**Plan**
1. **Image resolution**
- Implement resolver service with pluggable strategies:
- Use existing digest if present.
- Resolve tags via registry HEAD (respecting `admission.resolveTags` flag); fallback to Observer-provided digest once core DTOs available.
- Cache per-registry auth to minimise latency; adhere to allow/deny lists from configuration.
2. **Scanner client**
- Define typed request/response models mirroring `docs/modules/zastava/ARCHITECTURE.md` structure (`ttlSeconds`, `results[digest] -> { signed, hasSbom, policyVerdict, reasons, rekor }`).
- Implement retry policy (3 attempts, exponential backoff) and map HTTP errors to webhook fail-open/closed depending on namespace configuration.
- Instrument latency (`zastava.backend_latency_seconds`) and failure counts.
3. **Verdict enforcement**
- Evaluate per-image results: if any `policyVerdict != pass` (or `warn` when `enforceWarnings=false`), deny with aggregated reasons.
- Attach `ttlSeconds` to admission response annotations for auditing.
- Record structured logs with namespace, pod, image digest, decision, reasons, backend latency.
4. **Contract coordination**
- Schedule joint review with Scanner WebService guild once SCANNER-RUNTIME-12-302 schema stabilises; track in TASKS sub-items.
- Provide sample payload fixtures for CLI team (`CLI-RUNTIME-13-005`) to validate table output; ensure field names stay aligned.
**Deliverables**
- Registry resolver unit tests (tag->digest) with deterministic fixtures.
- HTTP client integration tests using Scanner stub returning varied verdict combinations.
- Documentation update summarising contract and failure handling.
**Open Questions**
- Confirm expected policy verdict enumeration (`pass|warn|fail|error`?) and textual reason codes.
- Need TTL behaviour: should webhook reduce TTL when backend returns > configured max?
## ZASTAVA-WEBHOOK-12-103 — Caching, fail-open/closed toggles, metrics/logging
**Objectives**
- Provide deterministic caching layer respecting backend TTL while ensuring eviction on policy mutation.
- Allow namespace-scoped fail-open behaviour with explicit metrics and alerts.
- Surface actionable metrics/logging aligned with Architecture doc.
**Plan**
1. **Cache design**
- In-memory LRU keyed by image digest; value carries verdict payload + expiry timestamp.
- Support optional persistent seed (read-only) to prime hot digests for offline clusters (config: `admission.cache.seedPath`).
- On startup, load seed file and emit metric `zastava.cache_seed_entries_total`.
- Evict entries on TTL or when `policyRevision` annotation in AdmissionReview changes (requires hook from Core DTO).
2. **Fail-open/closed toggles**
- Configuration: global default + namespace overrides through `admission.failOpenNamespaces`, `admission.failClosedNamespaces`.
- Decision matrix:
- Backend success + verdict PASS → allow.
- Backend success + non-pass → deny unless namespace override says warn allowed.
- Backend failure → allow if namespace fail-open, deny otherwise; annotate response with `zastava.ops/fail-open=true`.
- Implement policy change event hook (future) to clear cache if observer signals revocation.
3. **Metrics & logging**
- Counters: `zastava.admission_requests_total{decision}`, `zastava.cache_hits_total{result=hit|miss}`, `zastava.fail_open_total`, `zastava.backend_failures_total{stage}`.
- Histograms: `zastava.admission_latency_seconds` (overall), `zastava.resolve_latency_seconds`.
- Logs: structured JSON with `decision`, `namespace`, `pod`, `imageDigest`, `reasons`, `cacheStatus`, `failMode`.
- Optionally emit OpenTelemetry span for admission path with attributes capturing backend latency + cache path.
4. **Testing & ops hooks**
- Unit tests for cache TTL, namespace override logic, fail-open metric increments.
- Integration test simulating backend outage ensuring fail-open/closed behaviour matches config.
- Document runbook snippet describing interpreting metrics and toggling namespaces.
**Open Questions**
- Confirm whether cache entries should include `policyRevision` to detect backend policy updates; requires coordination with Policy guild.
- Need guidance on maximum cache size (default suggestions: 5k entries per replica?) to avoid memory blow-up.