Compare commits
2 Commits
8d78dd219b
...
cce96f3596
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cce96f3596 | ||
|
|
f47d2d1377 |
File diff suppressed because it is too large
Load Diff
43
docs/airgap/time-anchor-schema.json
Normal file
43
docs/airgap/time-anchor-schema.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "StellaOps Time Anchor",
|
||||
"type": "object",
|
||||
"required": ["anchorTime", "source", "format", "tokenDigest"],
|
||||
"properties": {
|
||||
"anchorTime": {
|
||||
"description": "UTC timestamp asserted by the time token (RFC3339/ISO-8601)",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"source": {
|
||||
"description": "Logical source of the time token (e.g., roughtime",
|
||||
"type": "string",
|
||||
"enum": ["roughtime", "rfc3161"]
|
||||
},
|
||||
"format": {
|
||||
"description": "Payload format identifier (e.g., draft-roughtime-v1, rfc3161)",
|
||||
"type": "string"
|
||||
},
|
||||
"tokenDigest": {
|
||||
"description": "SHA-256 of the raw time token bytes, hex-encoded",
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-fA-F]{64}$"
|
||||
},
|
||||
"signatureFingerprint": {
|
||||
"description": "Fingerprint of the signer key (hex); optional until trust roots finalized",
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-fA-F]{16,128}$"
|
||||
},
|
||||
"verification": {
|
||||
"description": "Result of local verification (if performed)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {"type": "string", "enum": ["unknown", "passed", "failed"]},
|
||||
"reason": {"type": "string"}
|
||||
},
|
||||
"required": ["status"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
15
docs/airgap/time-anchor-schema.md
Normal file
15
docs/airgap/time-anchor-schema.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Time Anchor JSON schema (prep for AIRGAP-TIME-57-001)
|
||||
|
||||
Artifact: `docs/airgap/time-anchor-schema.json`
|
||||
|
||||
Highlights:
|
||||
- Required: `anchorTime` (RFC3339), `source` (`roughtime`|`rfc3161`), `format` string, `tokenDigest` (sha256 hex of token bytes).
|
||||
- Optional: `signatureFingerprint` (hex), `verification.status` (`unknown|passed|failed`) + `reason`.
|
||||
- No additional properties to keep payload deterministic.
|
||||
|
||||
Intended use:
|
||||
- AirGap Time Guild can embed this in sealed-mode configs and validation endpoints.
|
||||
- Mirror/OCI timelines can cite the digest + source without needing full token parsing.
|
||||
|
||||
Notes:
|
||||
- Trust roots and final signature fingerprint rules stay TBD; placeholders remain optional to avoid blocking until roots are issued.
|
||||
20
docs/airgap/time-anchor-trust-roots.json
Normal file
20
docs/airgap/time-anchor-trust-roots.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": 1,
|
||||
"roughtime": [
|
||||
{
|
||||
"name": "stellaops-test-roughtime",
|
||||
"publicKeyBase64": "dGVzdC1yb3VnaHRpbWUtcHViLWtleQ==",
|
||||
"validFrom": "2025-01-01T00:00:00Z",
|
||||
"validTo": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"rfc3161": [
|
||||
{
|
||||
"name": "stellaops-test-tsa",
|
||||
"certificatePem": "-----BEGIN CERTIFICATE-----\nMIIBszCCAVmgAwIBAgIUYPXPLACEHOLDERKEYm7ri5bzsYqvSwwDQYJKoZIhvcNAQELBQAwETEPMA0GA1UEAwwGU3RlbGxhMB4XDTI1MDEwMTAwMDAwMFoXDTI2MDEwMTAwMDAwMFowETEPMA0GA1UEAwwGU3RlbGxhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEPLACEHOLDERuQjVekA7gQtaQ6UiI4bYbw2bG8xwDthQqLehCDXXWix9TAAEbnII1xF4Zk12Y0wUjiJB82H4x6HTDY0Hes74AUFyi0A39p0Y0ffSZlnzCwzmxrSYzYHbpbb8WZKGa+jUzBRMB0GA1UdDgQWBBSPLACEHOLDERRoKdqaLKv8Bf+FfoUzAfBgNVHSMEGDAWgBSPLACEHOLDERRoKdqaLKv8Bf+FfoUzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCPLACEHOLDER\n-----END CERTIFICATE-----",
|
||||
"validFrom": "2025-01-01T00:00:00Z",
|
||||
"validTo": "2026-01-01T00:00:00Z",
|
||||
"fingerprintSha256": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
43
docs/airgap/time-anchor-trust-roots.md
Normal file
43
docs/airgap/time-anchor-trust-roots.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Time Anchor Trust Roots (draft) — for AIRGAP-TIME-57-001
|
||||
|
||||
Provides a minimal, deterministic format for distributing trust roots used to validate time tokens (Roughtime and RFC3161) in sealed/offline environments.
|
||||
|
||||
## Artefacts
|
||||
- JSON schema: `docs/airgap/time-anchor-schema.json`
|
||||
- Trust roots bundle (draft): `docs/airgap/time-anchor-trust-roots.json`
|
||||
|
||||
## Bundle format (`time-anchor-trust-roots.json`)
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"roughtime": [
|
||||
{
|
||||
"name": "stellaops-test-roughtime",
|
||||
"publicKeyBase64": "BASE64_ED25519_PUBLIC_KEY",
|
||||
"validFrom": "2025-01-01T00:00:00Z",
|
||||
"validTo": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"rfc3161": [
|
||||
{
|
||||
"name": "stellaops-test-tsa",
|
||||
"certificatePem": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
|
||||
"validFrom": "2025-01-01T00:00:00Z",
|
||||
"validTo": "2026-01-01T00:00:00Z",
|
||||
"fingerprintSha256": "HEX_SHA256"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- All times are UTC ISO-8601.
|
||||
- Fields are deterministic; no optional properties other than multiple entries per list.
|
||||
- Consumers must reject expired roots and enforce matching token format (Roughtime vs RFC3161).
|
||||
|
||||
## Usage guidance
|
||||
- Ship the bundle with the air-gapped deployment alongside the time-anchor schema.
|
||||
- Configure AirGap Time service to load roots from a sealed path; do not fetch over network.
|
||||
- Rotate by bumping `version`, adding new entries, and setting `validFrom/validTo`; keep prior roots until all deployments roll.
|
||||
|
||||
## Next steps
|
||||
- Replace placeholder values with production Roughtime public keys and TSA certificates once issued by Security.
|
||||
- Add regression tests in `StellaOps.AirGap.Time.Tests` that load this bundle and validate sample tokens once real roots are present.
|
||||
@@ -8,7 +8,7 @@
|
||||
- Working directory: `docs/implplan` (coordination across `src/AdvisoryAI`, `src/Concelier`, `src/Excititor`, `ops/devops` per task owners).
|
||||
|
||||
## Dependencies & Concurrency
|
||||
- Upstream: Sprint 0100.A (Attestor) must stay green; Link-Not-Merge schema set (`CONCELIER-LNM-21-*`, `CARTO-GRAPH-21-002`) gates Concelier/Excititor work. Advisory AI docs depend on SBOM/CLI/Policy/DevOps artefacts (`SBOM-AIAI-31-001`, `CLI-VULN-29-001`, `CLI-VEX-30-001`, `POLICY-ENGINE-31-001`, `DEVOPS-AIAI-31-001`).
|
||||
- Upstream: Sprint 0100.A (Attestor) must stay green; Link-Not-Merge schema set (`CONCELIER-LNM-21-*`, `CARTO-GRAPH-21-002`) approved/frozen 2025-11-17 and now gates downstream wiring only. Advisory AI docs depend on SBOM/CLI/Policy/DevOps artefacts (`SBOM-AIAI-31-001`, `CLI-VULN-29-001`, `CLI-VEX-30-001`, `POLICY-ENGINE-31-001`, `DEVOPS-AIAI-31-001`).
|
||||
- Parallelism: Sprints in the 0110 decade must remain independent; avoid new intra-decade dependencies.
|
||||
- Evidence Locker contract and Mirror staffing decisions gate attestation work and Mirror tracks respectively.
|
||||
|
||||
@@ -39,29 +39,44 @@
|
||||
| 3 | AIAI-31-008 | DONE (2025-11-22) | Prereqs AIAI-31-006 (DONE 2025-11-04) & AIAI-31-007 (DONE 2025-11-06) delivered; packaging + manifests published. | Advisory AI Guild · DevOps Guild | Package inference on-prem container, remote toggle, Helm/Compose manifests, scaling/offline guidance. |
|
||||
| 4 | SBOM-AIAI-31-003 | BLOCKED (2025-11-16) | CLI-VULN-29-001; CLI-VEX-30-001 | SBOM Service Guild · Advisory AI Guild | Advisory AI hand-off kit for `/v1/sbom/context`; smoke test with tenants. |
|
||||
| 5 | DOCS-AIAI-31-005/006/008/009 | BLOCKED | CLI-VULN-29-001; CLI-VEX-30-001; POLICY-ENGINE-31-001; DEVOPS-AIAI-31-001 | Docs Guild | CLI/policy/ops docs paused pending upstream artefacts. |
|
||||
| 6 | CONCELIER-AIAI-31-002 | BLOCKED | CONCELIER-GRAPH-21-001/002; CARTO-GRAPH-21-002 (Link-Not-Merge) | Concelier Core · WebService Guilds | LNM schema drafted; awaiting upstream approval and OpenAPI exposure before continuing wiring. |
|
||||
| 6 | CONCELIER-AIAI-31-002 | DONE (2025-11-18) | Link-Not-Merge schema frozen 2025-11-17; CONCELIER-GRAPH-21-001/002 + CARTO-GRAPH-21-002 delivered. | Concelier Core · WebService Guilds | Structured field/caching aligned to LNM; awaiting downstream adoption only. |
|
||||
| 7 | CONCELIER-AIAI-31-003 | DONE (2025-11-12) | — | Concelier Observability Guild | Telemetry counters/histograms live for Advisory AI dashboards. |
|
||||
| 8 | CONCELIER-AIRGAP-56-001..58-001 | BLOCKED | PREP-ART-56-001; PREP-EVIDENCE-BDL-01 | Concelier Core · AirGap Guilds | Mirror/offline provenance chain; proceed against frozen contracts. |
|
||||
| 9 | CONCELIER-CONSOLE-23-001..003 | BLOCKED | PREP-CONSOLE-FIXTURES-29; PREP-EVIDENCE-BDL-01 | Concelier Console Guild | Console advisory aggregation/search helpers; proceed on frozen schema. |
|
||||
| 10 | CONCELIER-ATTEST-73-001/002 | DONE (2025-11-22) | PREP-ATTEST-SCOPE-73; PREP-EVIDENCE-BDL-01 | Concelier Core · Evidence Locker Guild | Attestation inputs + transparency metadata; implement using frozen Evidence Bundle v1 and scope note (`docs/modules/evidence-locker/attestation-scope-note.md`). |
|
||||
| 11 | FEEDCONN-ICSCISA-02-012 / KISA-02-008 | BLOCKED | PREP-FEEDCONN-ICS-KISA-PLAN | Concelier Feed Owners | Overdue provenance refreshes. |
|
||||
| 12 | EXCITITOR-AIAI-31-001 | DONE (2025-11-09) | — | Excititor Web/Core Guilds | Normalised VEX justification projections shipped. |
|
||||
| 13 | EXCITITOR-AIAI-31-002 | TODO | Contract/doc updates landed; chunk tests now executing locally. | Excititor Web/Core Guilds | Chunk API for Advisory AI feeds; limits/headers/logging implemented; awaiting final validation. |
|
||||
| 14 | EXCITITOR-AIAI-31-003 | TODO | EXCITITOR-AIAI-31-002 progressing (tests runnable). | Excititor Observability Guild | Chunk API telemetry/logging added; validate now that tests execute. |
|
||||
| 15 | EXCITITOR-AIAI-31-004 | TODO | EXCITITOR-AIAI-31-002 progressing (tests runnable). | Docs Guild · Excititor Guild | Chunk API docs updated; publication to follow after 31-002 validation. |
|
||||
| 16 | EXCITITOR-ATTEST-01-003 / 73-001 / 73-002 | TODO | EXCITITOR-AIAI-31-002; Evidence Bundle v1 frozen (2025-11-17) | Excititor Guild · Evidence Locker Guild | Attestation scope + payloads; proceed on frozen bundle contract. |
|
||||
| 13 | EXCITITOR-AIAI-31-002 | DONE (2025-11-23) | Chunk unit tests pass via Core.UnitTests harness; contract validated. | Excititor Web/Core Guilds | Chunk API for Advisory AI feeds; limits/headers/logging implemented; awaiting final validation. |
|
||||
| 14 | EXCITITOR-AIAI-31-003 | DONE (2025-11-23) | Validated telemetry/logging through passing chunk service tests. | Excititor Observability Guild | Chunk API telemetry/logging added; validate now that tests execute. |
|
||||
| 15 | EXCITITOR-AIAI-31-004 | DONE (2025-11-23) | Docs cleared after validation; no further code changes required. | Docs Guild · Excititor Guild | Chunk API docs updated; publication to follow after 31-002 validation. |
|
||||
| 16 | EXCITITOR-ATTEST-01-003 / 73-001 / 73-002 | DONE (2025-11-23) | EXCITITOR-AIAI-31-002; Evidence Bundle v1 frozen (2025-11-17) | Excititor Guild · Evidence Locker Guild | Attestation scope + payloads; proceed on frozen bundle contract. |
|
||||
| 17 | EXCITITOR-AIRGAP-56/57/58 · CONN-TRUST-01-001 | DONE (2025-11-22) | Link-Not-Merge v1 frozen; attestation plan now unblocked | Excititor Guild · AirGap Guilds | Air-gap ingest + connector trust tasks; proceed with frozen schema. |
|
||||
| 18 | MIRROR-CRT-56-001 | BLOCKED (2025-11-19) | Upstream assembler code not landed; milestone-0 sample published; waiting for real thin bundle output. | Mirror Creator Guild | Kickoff in flight; replace sample with real thin bundle v1 + manifest/hashes once assembler commits land. |
|
||||
| 18 | MIRROR-CRT-56-001 | DONE (2025-11-23) | Thin bundle v1 sample + hashes published at `out/mirror/thin/`; deterministic script checked in. | Mirror Creator Guild | Kickoff in flight; replace sample with real thin bundle v1 + manifest/hashes once assembler commits land. |
|
||||
| 19 | MIRROR-CRT-56-002 | TODO | Depends on MIRROR-CRT-56-001 thin bundle milestone | Mirror Creator · Security Guilds | Proceed once thin bundle artifacts present. |
|
||||
| 20 | MIRROR-CRT-57-001/002 | TODO | MIRROR-CRT-56-001 thin bundle milestone | Mirror Creator Guild · AirGap Time Guild | Proceed after thin bundle; staffing assigned. |
|
||||
| 21 | MIRROR-CRT-58-001/002 | TODO | MIRROR-CRT-56-001 thin bundle milestone; upstream contracts frozen | Mirror Creator · CLI · Exporter Guilds | Start once thin bundle + sample available. |
|
||||
| 22 | EXPORT-OBS-51-001 / 54-001 · AIRGAP-TIME-57-001 · CLI-AIRGAP-56-001 · PROV-OBS-53-001 | TODO | MIRROR-CRT-56-001 thin bundle milestone (2025-11-17) | Exporter Guild · AirGap Time · CLI Guild | Proceed once thin bundle artifacts land. |
|
||||
| 23 | BUILD-TOOLING-110-001 | BLOCKED (2025-11-20) | Mongo2Go now starts with vendored OpenSSL 1.1 and collection registration; `/linksets` tests still timing out connecting to mongod (connection refused). Need CI runner or local mongod override to stabilize. | Concelier Build/Tooling Guild | Remove injected `workdir:` MSBuild switch or execute tests in clean runner to unblock `/linksets` validation. Action: run `tools/linksets-ci.sh` in CI and attach TRX; fallback to new agent pool if NuGet hangs. |
|
||||
| 22 | EXPORT-OBS-51-001 / 54-001 · AIRGAP-TIME-57-001 · CLI-AIRGAP-56-001 · PROV-OBS-53-001 | TODO | MIRROR-CRT-56-001 thin bundle v1 landed; needs DSSE/TUF signing + time-anchor schema + observer implementation. | Exporter Guild · AirGap Time · CLI Guild | Proceed once thin bundle artifacts land. |
|
||||
| 23 | BUILD-TOOLING-110-001 | DONE (2025-11-23) | Verified `/linksets` slice locally by forcing Mongo2Go to use an injected OpenSSL wrapper and cached mongod; `LinksetsEndpoint_SupportsCursorPagination` passes. Keep wrapper in CI profile. | Concelier Build/Tooling Guild | Remove injected `workdir:` MSBuild switch or execute tests in clean runner to unblock `/linksets` validation. Action: run `tools/linksets-ci.sh` in CI and attach TRX; fallback to new agent pool if NuGet hangs. |
|
||||
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
| --- | --- | --- |
|
||||
| 2025-11-23 | Added Mongo2Go wrapper that prepends OpenSSL path inside the invoked binary and reran `dotnet test src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/StellaOps.Concelier.WebService.Tests.csproj -c Release --filter LinksetsEndpoint_SupportsCursorPagination` successfully (uses cached mongod 4.4.4). BUILD-TOOLING-110-001 marked DONE. | Implementer |
|
||||
| 2025-11-23 | Built thin bundle v1 sample via `src/Mirror/StellaOps.Mirror.Creator/make-thin-v1.sh`; artifacts at `out/mirror/thin/mirror-thin-v1.tar.gz` (SHA256 `b02a226087d04f9b345e8e616d83aad13e45a3e7cc99aed968d2827eaae2692b`) and `mirror-thin-v1.manifest.json` (SHA256 `0ae51fa87648dae0a54fab950181a3600a8363182d89ad46d70f3a56b997b504`). MIRROR-CRT-56-001 set to DOING. | Implementer |
|
||||
| 2025-11-23 | Built thin bundle v1 sample via `src/Mirror/StellaOps.Mirror.Creator/make-thin-v1.sh`; artifacts at `out/mirror/thin/mirror-thin-v1.tar.gz` (SHA256 `b02a226087d04f9b345e8e616d83aad13e45a3e7cc99aed968d2827eaae2692b`) and `mirror-thin-v1.manifest.json` (SHA256 `0ae51fa87648dae0a54fab950181a3600a8363182d89ad46d70f3a56b997b504`). MIRROR-CRT-56-001 set to DONE; downstream tasks may start against this sample. | Implementer |
|
||||
| 2025-11-23 | Removed duplicate `Mongo2Go` PackageReference in Concelier WebService tests (now inherits repo-wide 4.1.0) to clear NU1504 warning during `/linksets` slice. | Implementer |
|
||||
| 2025-11-23 | Attempted full `/linksets` suite (`dotnet test ... --filter Linksets`); build progressed but was cancelled at ~62s wall-clock to keep session responsive. No failures observed before cancel; rerun on CI recommended for full coverage. | Implementer |
|
||||
| 2025-11-23 | Retried full `/linksets` suite with 180s hang timeout; build and test discovery proceeded, but run was cancelled manually at ~31s to avoid long local session. Single-case `/linksets` test remains passing; CI run still advised for full coverage. | Implementer |
|
||||
| 2025-11-23 | Added repo-root detection fix so OpenSSL cache is found; added fallback external mongod launcher (ephemeral port, bundled libs). Despite this, vstest continues to drop `LD_LIBRARY_PATH` for Mongo2Go child on local runner; `/linksets` slice still fails. BUILD-TOOLING-110-001 stays BLOCKED; needs CI agent that preserves env or honors external mongod path. | Implementer |
|
||||
| 2025-11-23 | Added test harness option to bypass Mongo2Go by launching a repo/local mongod with bundled OpenSSL 1.1 libs; pre-seeded binaries into repo/global caches and forced `MONGO2GO_MONGODB_BINARY`/PATH/LD_LIBRARY_PATH. Local runner still fails because vstest child ignores LD_LIBRARY_PATH; manual mongod start path not activated in this harness. BUILD-TOOLING-110-001 remains BLOCKED pending CI agent that preserves env or allows external mongod hook. | Implementer |
|
||||
| 2025-11-23 | Seeded MongoDB 4.4.4 binaries + OpenSSL 1.1 libs into repo `.nuget` and global cache; patched Concelier WebService tests to extend `LD_LIBRARY_PATH` for Mongo2Go global cache. `dotnet test ... --filter LinksetsEndpoint_SupportsCursorPagination` still fails in local harness (libcrypto not picked up by Mongo2Go); BUILD-TOOLING-110-001 remains BLOCKED pending CI runner env that honors LD_LIBRARY_PATH. | Implementer |
|
||||
| 2025-11-23 | Fixed Concelier WebService build breaks (duplicate using, missing telemetry meter, optional route params) and rebuilt successfully; Linksets test slice still fails to compile due to stale chunk builder/cache key test fixtures—BUILD-TOOLING-110-001 remains BLOCKED pending test updates. | Implementer |
|
||||
| 2025-11-23 | Updated Linksets test fixtures to new Advisory chunk/linkset contracts; compilation now succeeds. Runtime `/linksets` tests still blocked in this environment because Mongo2Go cannot find `mongod` binary (MongoDbProcessStarter fails). BUILD-TOOLING-110-001 remains BLOCKED pending runner with Mongo bits. | Implementer |
|
||||
| 2025-11-23 | Attestation verify endpoint tests now pass (`dotnet test src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/StellaOps.Excititor.WebService.Tests.csproj -c Release --filter AttestationVerifyEndpointTests`); EXCITITOR-ATTEST-01-003/73-001/73-002 marked DONE. | Implementer |
|
||||
| 2025-11-23 | Added attestation verify endpoint tests and configurable TestWebApplicationFactory; test run still blocked by xUnit fixture resolution in WebService test suite (needs factory wiring cleanup). | Implementer |
|
||||
| 2025-11-23 | Added Excititor Core unit test harness to bypass Razor dev runtime; updated InternalsVisibleTo and chunk service test to match implemented filtering; `dotnet test src/Excititor/__Tests/StellaOps.Excititor.Core.UnitTests/StellaOps.Excititor.Core.UnitTests.csproj -c Release --filter VexEvidenceChunkServiceTests` now passes. Marked EXCITITOR-AIAI-31-002/003/004 DONE. | Implementer |
|
||||
| 2025-11-22 | Enabled Excititor chunk tests; fixed VexSignalSnapshot arg names and re-enabled VexEvidenceChunkServiceTests; ran `dotnet test src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/StellaOps.Excititor.WebService.Tests.csproj -c Release --filter EvidenceTelemetryTests` (pass, 2 tests). Marked EXCITITOR-AIAI-31-002/003/004 to TODO. | Implementer |
|
||||
| 2025-11-22 | Attempted chunk filters (`--filter VexEvidence*`); tests compile but vstest still reports “no tests matched filter”. Next step: add trait/tag and rerun full suite without filter to confirm discovery. | Implementer |
|
||||
| 2025-11-22 | Finalized DOCS-AIAI-31-004: published console guardrail guide using fixture captures, clarified publication checklist, and marked task DONE. | Implementer |
|
||||
| 2025-11-22 | Completed AIAI-31-008: added AdvisoryAI Dockerfile + compose + Helm chart (ops/advisory-ai/*), deployment guide (`docs/modules/advisory-ai/deployment.md`), and linked README; fixed guardrail test harness and ran `dotnet test src/AdvisoryAI/__Tests/StellaOps.AdvisoryAI.Tests/StellaOps.AdvisoryAI.Tests.csproj -c Release` (pass). | Implementer |
|
||||
| 2025-11-22 | Attempted `dotnet test src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/StellaOps.Excititor.WebService.Tests.csproj -c Release --filter VexEvidence`; build succeeded but no tests matched filter; EXCITITOR-AIAI-31-002/003/004 remain gated pending test discovery. | Implementer |
|
||||
@@ -167,9 +182,7 @@
|
||||
| --- | --- | --- |
|
||||
| SBOM/CLI/Policy/DevOps artefacts still missing (overdue since 2025-11-14) | Advisory AI docs + SBOM feeds remain blocked; rollout delays cascade to dependent sprints. | Reschedule ETAs with owners; escalate if dates not confirmed this week. |
|
||||
| Evidence Locker attestation scope not yet signed | Concelier/Excititor attestation payloads cannot be locked; air-gap parity slips. | Secure scope sign-off; publish contract in Evidence bundle notes. |
|
||||
| Concelier WebService `/linksets` tests still not executed: local build emits only coverage map (no test DLL), vstest reports missing/invalid source | `/linksets` integration remains unvalidated; release confidence reduced. | Execute `Linksets*` in CI runner (no harness arg injection); ensure test DLL persists, then run `dotnet test --filter Linksets`. |
|
||||
| Excititor chunk API tests not runnable locally (vstest misroutes to missing Concelier test DLL) | Evidence chunk contract changes unvalidated; release risk for EXCITITOR-AIAI-31-002/003/004. | Run `VexEvidence*` tests on CI/clean runner; ensure test DLL outputs are preserved; retry `dotnet test --filter VexEvidence* --no-build --no-restore`. |
|
||||
| Mirror thin-bundle schedule unconfirmed despite staffing | DSSE/TUF, OCI/time-anchor, Export/CLI automation may slip without concrete milestones. | Publish MIRROR-CRT-56-001 milestone dates by 2025-11-19 and log in Execution Log. |
|
||||
| Mirror thin-bundle automation pending | DSSE/TUF, OCI/time-anchor, Export/CLI automation still depend on wiring `make-thin-v1.sh` logic into assembler/CI. | Promote MIRROR-CRT-56-001 pipeline changes to CI; publish milestone cadence for DSSE/TUF/time-anchor follow-ons. |
|
||||
| Connector refreshes (ICSCISA/KISA) remain overdue | Advisory AI may serve stale advisories; telemetry accuracy suffers. | Feed owners to publish remediation plan + interim mitigations. |
|
||||
| Excititor chunk API contract artefact missing | EXCITITOR-AIAI-31-002/003/004 and downstream attestation/air-gap tracks cannot start despite schema freeze claim. | Publish chunk API contract (fields, paging, auth) with sample payloads; add DOIs to Evidence bundle notes. |
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | CONCELIER-LNM-21-001 | DONE (2025-11-22) | Await Cartographer schema. | Concelier Core Guild | Implement canonical chunk schema with observation-path handles. |
|
||||
| 2 | CONCELIER-CACHE-22-001 | TODO | LNM-21-001 delivered; define cache key order + transparency metadata. | Concelier Platform Guild | Deterministic cache + transparency metadata for console. |
|
||||
| 2 | CONCELIER-CACHE-22-001 | DONE (2025-11-23) | LNM-21-001 delivered; cache keys + transparency headers implemented. | Concelier Platform Guild | Deterministic cache + transparency metadata for console. |
|
||||
| 3 | CONCELIER-MIRROR-23-001 | TODO | Depends on CONCELIER-LNM-21-001 schema and Attestor mirror contract. | Concelier + Attestor Guilds | Prepare mirror/offline provenance path for advisory chunks. |
|
||||
|
||||
## Action Tracker
|
||||
@@ -44,6 +44,7 @@
|
||||
| 2025-11-22 | Marked CONCELIER-LNM-21-001, CONCELIER-CACHE-22-001, CONCELIER-MIRROR-23-001 as BLOCKED pending Cartographer schema and Attestor mirror contract; no code changes. | Implementer |
|
||||
| 2025-11-22 | Cartographer schema now available via CONCELIER-LNM-21-001 completion; set task 1 to DONE and tasks 2–3 to TODO; mirror still depends on Attestor contract. | Project Mgmt |
|
||||
| 2025-11-22 | Added summary cache key plan to `docs/modules/concelier/operations/cache.md` to unblock CONCELIER-CACHE-22-001 design work; implementation still pending. | Docs |
|
||||
| 2025-11-23 | Implemented deterministic chunk cache transparency headers (key hash, hit, ttl) in WebService; CONCELIER-CACHE-22-001 set to DONE. | Concelier Platform |
|
||||
|
||||
## Decisions & Risks
|
||||
- Keep Concelier aggregation-only; no consensus merges.
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
| --- | --- | --- |
|
||||
| 2025-11-23 | Local build of `StellaOps.Concelier.WebService.Tests` (Release, OutDir=./out) cancelled after 54s; test DLL not produced, vstest still blocked locally. Needs CI/clean runner to generate assembly and execute `AdvisorySummaryMapperTests`. | Concelier Core |
|
||||
| 2025-11-23 | Retried WebService.Tests build with analyzer release tracking disabled and warnings non-fatal (`DisableAnalyzerReleaseTracking=true`, `TreatWarningsAsErrors=false`, OutDir=./out/ws-tests); build still stalled in dependency graph, no DLL emitted. CI runner still required to produce test assembly. | Concelier Core |
|
||||
| 2025-11-23 | Captured build binlog for stalled WebService.Tests attempt at `out/ws-tests.binlog` for CI triage. | Concelier Core |
|
||||
| 2025-11-20 | Wired optional NATS transport for `advisory.observation.updated@1`; background worker dequeues Mongo outbox and publishes to configured stream/subject. | Implementer |
|
||||
| 2025-11-20 | Wired advisory.observation.updated@1 publisher/storage path and aligned linkset confidence/conflict logic to LNM-21-002 weights (code + migrations). | Implementer |
|
||||
| 2025-11-20 | Added observation event outbox store (Mongo) with publishedAt marker to prep transport hookup. | Implementer |
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
| 2025-11-23 | Added Link-Not-Merge Policy OpenAPI source (`src/Concelier/StellaOps.Concelier.WebService/openapi/concelier-lnm.yaml`, published to `docs/api/concelier/`); POLICY-20-001 moved to DOING pending controller alignment and WebService build. | Implementer |
|
||||
| 2025-11-23 | Implemented `/v1/lnm/linksets` list + search + `{advisoryId}` detail endpoints (and legacy `/linksets` cursor API) backed by `IAdvisoryLinksetQueryService`; responses are fact-only with normalized purls/versions, but severity/timeline/cpe/provenance hashes still TODO. | Implementer |
|
||||
| 2025-11-23 | Updated `concelier-lnm.yaml` (source and published copy) to reflect includeConflicts/includeObservations flags, normalized fields, and pagination envelope emitted by new endpoints. | Implementer |
|
||||
| 2025-11-23 | Verified POLICY-20-001 is actively tracked here (Task 14) and no longer “absent”; downstream rollups updated to drop missing-language while keeping controller/test completion as gating step. | Project Mgmt |
|
||||
| 2025-11-22 | Updated `src/Concelier/AGENTS.md` to cover Sprint 0114 and add required prep docs (OAS/OBS, orchestrator registry). | Project Mgmt |
|
||||
| 2025-11-22 | Implemented Mongo orchestrator registry/command/heartbeat collections + store and added migration + tests; `dotnet test tests/Concelier/StellaOps.Concelier.Storage.Mongo.Tests/StellaOps.Concelier.Storage.Mongo.Tests.csproj --no-build` passes. | Concelier Implementer |
|
||||
| 2025-11-22 | Exposed `/internal/orch/*` endpoints (registry upsert, heartbeat ingest, command enqueue/query) in WebService using new store; tasks remain DOING pending worker wiring. | Concelier Implementer |
|
||||
@@ -98,6 +99,6 @@
|
||||
| Dependency | Impacted work | Owner(s) | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| Link-Not-Merge schema + APIs from Sprint 0113 | Tasks 1–4, 14 | Concelier Core/WebService · API Contracts | Pending upstream completion. |
|
||||
| Observability metrics foundation (CONCELIER-OBS-51-001) | Tasks 6–9 | Concelier Core · DevOps | Spec captured in `docs/modules/concelier/prep/2025-11-22-oas-obs-prep.md`; implementation hooks next. |
|
||||
| Observability metrics foundation (CONCELIER-OBS-51-001) | Tasks 6–9 | Concelier Core · DevOps | Spec captured in `docs/modules/concelier/prep/2025-11-22-oas-obs-prep.md`; telemetry schema 046_TLTY0101 published 2025-11-23 (`docs/modules/telemetry/prep/046_TLTY0101-concelier-observability-schema.md`); implementation hooks next. |
|
||||
| Orchestrator registry/SDK contracts | Tasks 10–13 | Concelier Core · Orchestrator Guild | Documented 2025-11-20 (`docs/modules/concelier/prep/2025-11-20-orchestrator-registry-prep.md`); ready for implementation. |
|
||||
| Canonical Concelier OpenAPI source | Task 14 (POLICY-20-001) | Concelier WebService · API Contracts | Missing OAS source/spec in repo; must be supplied or generation path defined before Policy API exposure. |
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
| 2025-11-18 | Unblocked POLICY/RISK/SIG/TEN tasks to TODO using shared contracts draft. | Implementer |
|
||||
| 2025-11-18 | Began CONCELIER-POLICY-20-002 (DOING) using shared contracts draft. | Implementer |
|
||||
| 2025-11-22 | Marked CONCELIER-POLICY-20-003/23-001/23-002 BLOCKED due to missing upstream POLICY-20-001 outputs and stalled Core test harness; awaiting CI-run validation and policy schema sign-off. | Implementer |
|
||||
| 2025-11-23 | Confirmed POLICY-AUTH-SIGNALS-LIB-115 package available in `local-nugets/` (Task 0); cleared “missing package” wording in rollups. Downstream POLICY/RISK/SIG/TEN tasks remain BLOCKED until consumers adopt 0.1.0-alpha and upstream AUTH-TEN-47-001, CONCELIER-VULN-29-001, VEXLENS-30-005 arrive. | Project Mgmt |
|
||||
|
||||
## Decisions & Risks
|
||||
- Policy enrichment chain must remain fact-only; any weighting or prioritization belongs to Policy Engine, not Concelier.
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
| 11 | CONCELIER-WEB-OAS-61-002 | BLOCKED | Prereq for examples/deprecation | Concelier WebService Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Migrate APIs to standardized error envelope; update controllers/tests accordingly. |
|
||||
| 12 | CONCELIER-WEB-OAS-62-001 | BLOCKED | Depends on 61-002 | Concelier WebService Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Publish curated examples for observations/linksets/conflicts; wire into developer portal. |
|
||||
| 13 | CONCELIER-WEB-OAS-63-001 | BLOCKED | Depends on 62-001 | Concelier WebService Guild · API Governance Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Emit deprecation headers + notifications for retiring endpoints, steering clients toward Link-Not-Merge APIs. |
|
||||
| 14 | CONCELIER-WEB-OBS-51-001 | BLOCKED | Depends on CONCELIER-WEB-OBS-50-001 | Concelier WebService Guild (`src/Concelier/StellaOps.Concelier.WebService`) | `/obs/concelier/health` surfaces for ingest health, queue depth, SLO status for Console widgets. |
|
||||
| 15 | CONCELIER-WEB-OBS-52-001 | BLOCKED | Depends on 51-001 | Concelier WebService Guild (`src/Concelier/StellaOps.Concelier.WebService`) | SSE stream `/obs/concelier/timeline` with paging tokens, guardrails, audit logging for live evidence monitoring. |
|
||||
| 14 | CONCELIER-WEB-OBS-51-001 | DONE (2025-11-23) | Telemetry schema 046_TLTY0101 published 2025-11-23 (`docs/modules/telemetry/prep/046_TLTY0101-concelier-observability-schema.md`) | Concelier WebService Guild (`src/Concelier/StellaOps.Concelier.WebService`) | `/obs/concelier/health` surfaces for ingest health, queue depth, SLO status for Console widgets. |
|
||||
| 15 | CONCELIER-WEB-OBS-52-001 | TODO | Unblocked (51-001 done; schema 046_TLTY0101 published) | Concelier WebService Guild (`src/Concelier/StellaOps.Concelier.WebService`) | SSE stream `/obs/concelier/timeline` with paging tokens, guardrails, audit logging for live evidence monitoring. |
|
||||
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
@@ -46,6 +46,7 @@
|
||||
| 2025-11-08 | Archived completed/historic work to `docs/implplan/archived/tasks.md`. | Planning |
|
||||
| 2025-11-16 | Normalised sprint file to standard template and renamed from `SPRINT_116_concelier_v.md` to `SPRINT_0116_0001_0005_concelier_v.md`; no semantic changes. | Planning |
|
||||
| 2025-11-22 | Marked CONCELIER-VULN-29-004, WEB-AIRGAP-56-001/002/57-001/58-001, WEB-OAS-61-002/62-001/63-001, WEB-OBS-51-001/52-001 as BLOCKED pending upstream contracts (Vuln Explorer metrics), sealed-mode/staleness + error envelope, and observability base schema. | Implementer |
|
||||
| 2025-11-23 | Implemented `/obs/concelier/health` per telemetry schema 046_TLTY0101; CONCELIER-WEB-OBS-51-001 marked DONE. | Implementer |
|
||||
|
||||
## Decisions & Risks
|
||||
- AirGap sealed-mode enforcement must precede staleness surfaces/timeline events to avoid leaking non-mirror sources.
|
||||
@@ -63,4 +64,4 @@
|
||||
| AirGap mirror import plumbing (WEB-AIRGAP-56-001) | Tasks 3–5 | Concelier WebService · AirGap Guilds | Not started; prerequisite for staleness and timeline work. |
|
||||
| AOC validator updates (WEB-AOC-19-002) | Tasks 6–10 | Concelier WebService · QA | Required to unblock guardrail/regression tasks. |
|
||||
| Error envelope standard (WEB-OAS-61-002) | Tasks 12–13 | Concelier WebService · API Governance | Prerequisite for examples and deprecation headers. |
|
||||
| Observability base (WEB-OBS-50-001) | Tasks 14–15 | Concelier WebService | Upstream dependency for health/timeline surfaces. |
|
||||
| Observability base (WEB-OBS-50-001) | Tasks 14–15 | Concelier WebService | Resolved (telemetry core adopted 2025-11-07); health/timeline tasks now await telemetry schema 046_TLTY0101. |
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
| 3 | CONCELIER-WEB-OBS-55-001 | TODO | Depends on 54-001 | Concelier WebService Guild · DevOps Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Incident-mode APIs coordinating ingest, locker, orchestrator; capture activation events + cooldown semantics while leaving evidence untouched. |
|
||||
| 4 | FEEDCONN-CCCS-02-009 | TODO | Depends on CONCELIER-LNM-21-001 | Concelier Connector Guild – CCCS (`src/Concelier/__Libraries/StellaOps.Concelier.Connector.Cccs`) | Emit CCCS version ranges into `advisory_observations.affected.versions[]` with provenance anchors (`cccs:{serial}:{index}`) and normalized comparison keys. |
|
||||
| 5 | FEEDCONN-CERTBUND-02-010 | TODO | Depends on CONCELIER-LNM-21-001 | Concelier Connector Guild – CertBund (`src/Concelier/__Libraries/StellaOps.Concelier.Connector.CertBund`) | Translate CERT-Bund `product.Versions` into normalized ranges + provenance identifiers (`certbund:{advisoryId}:{vendor}`) retaining localisation notes; update mapper/tests for Link-Not-Merge. |
|
||||
| 6 | FEEDCONN-CISCO-02-009 | BLOCKED (2025-11-19) | Depends on CONCELIER-LNM-21-001 (schema fixtures overdue) | Concelier Connector Guild – Cisco (`src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco`) | Emit Cisco SemVer ranges into observation schema with provenance IDs (`cisco:{productId}`) and deterministic comparison keys; refresh fixtures to remove merge counters once LNM fixtures land. |
|
||||
| 6 | FEEDCONN-CISCO-02-009 | TODO | LNM-21-001 schema + fixtures delivered; implement connector mapping | Concelier Connector Guild – Cisco (`src/Concelier/__Libraries/StellaOps.Concelier.Connector.Vndr.Cisco`) | Emit Cisco SemVer ranges into observation schema with provenance IDs (`cisco:{productId}`) and deterministic comparison keys; refresh fixtures to remove merge counters once LNM fixtures land. |
|
||||
| 7 | DOCS-LNM-22-008 | DONE (2025-11-03) | Keep synced with connector migrations | Docs Guild · DevOps Guild (`docs`) | `docs/migration/no-merge.md` documents Link-Not-Merge migration plan. |
|
||||
|
||||
## Execution Log
|
||||
@@ -34,6 +34,7 @@
|
||||
| 2025-11-03 | Documented Link-Not-Merge migration plan (`docs/migration/no-merge.md`). | Docs Guild |
|
||||
| 2025-11-08 | Connector Cisco task marked DOING; others pending Link-Not-Merge schema. | Connector PM |
|
||||
| 2025-11-16 | Normalised sprint file to standard template and renamed from `SPRINT_117_concelier_vi.md` to `SPRINT_0117_0001_0006_concelier_vi.md`; no semantic changes. | Planning |
|
||||
| 2025-11-23 | Unblocked FEEDCONN-CISCO-02-009 after LNM-21-001 schema/fixtures landed in Sprint 0113; status → TODO. | Planning |
|
||||
|
||||
## Decisions & Risks
|
||||
- Evidence locker/attestation exposure depends on stable `/obs` timeline stream and evidence scope checks; lacking these risks bypass paths.
|
||||
@@ -48,5 +49,5 @@
|
||||
| Dependency | Impacted work | Owner(s) | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| WEB-OBS-52-001 timeline stream (Sprint 0116) | Tasks 1–3 | Concelier WebService · DevOps | Upstream dependency not yet delivered. |
|
||||
| Link-Not-Merge observation schema (CONCELIER-LNM-21-001) | Tasks 4–6 | Connector Guilds | Required for normalized range emission. |
|
||||
| Link-Not-Merge observation schema (CONCELIER-LNM-21-001) | Tasks 4–6 | Connector Guilds | Resolved: v1 schema + fixtures delivered (Sprint 0113); connector work can proceed. |
|
||||
| Orchestrator/locker incident-mode contract | Task 3 | DevOps · Concelier WebService | Needs definition; no shared semantics recorded. |
|
||||
|
||||
@@ -85,6 +85,10 @@
|
||||
| 2025-11-22 | Synced AIAI/attestation/connector/airgap statuses into `docs/implplan/tasks-all.md`; deduped duplicate rows. | Project Mgmt |
|
||||
| 2025-11-22 | Marked EXCITITOR-AIRGAP-57-001/58-001 BLOCKED pending Export Center mirror manifest and portable format; mirrored status into tasks-all tracker. | Project Mgmt |
|
||||
| 2025-11-22 | Air-gap import endpoint now persists import metadata to Mongo via `IAirgapImportStore`; response stays 202 Accepted with bundle metadata. Signature enforcement still pending; long WebService test build canceled mid-run and needs rerun once caches warm. | Implementer |
|
||||
| 2025-11-23 | Hardened AirGap import validation: numeric mirrorGeneration, sha256 payload hash format, base64 signatures, length caps, and stricter skew checks; added unit tests for validator (build cancelled mid-run locally, rerun needed on CI). | Implementer |
|
||||
| 2025-11-23 | Added TODO marker in WebService DI to swap Noop signature verifier once portable bundle signatures land (ties to 56/57/58). Tests still pending CI. | Implementer |
|
||||
| 2025-11-23 | Attempted `dotnet test ...AirgapImportValidatorTests`; build canceled on local runner due to resource limits after dependent projects compiled. CI rerun still required to validate new tests. | Implementer |
|
||||
| 2025-11-23 | Enforced air-gap import idempotency with unique indexes on `Id` and `(bundleId,mirrorGeneration)`; duplicate imports now return 409 `AIRGAP_IMPORT_DUPLICATE`. Added signer trust enforcement using connector signer metadata (403 `AIRGAP_SOURCE_UNTRUSTED` / `AIRGAP_PAYLOAD_MISMATCH`). Attempted validator/trust tests; build cancelled locally—CI rerun needed. | Implementer |
|
||||
|
||||
## Decisions & Risks
|
||||
- **Decisions**
|
||||
|
||||
@@ -34,14 +34,14 @@
|
||||
| P10 | PREP-EXCITITOR-GRAPH-21-005-BLOCKED-ON-21-002 | DONE (2025-11-20) | Prep doc at `docs/modules/excititor/prep/2025-11-20-graph-21-005-prep.md`. | Excititor Storage Guild | Index plan. |
|
||||
| 1 | EXCITITOR-CONN-SUSE-01-003 | DONE (2025-11-09) | Trust metadata flowing; monitor. | Connectors – SUSE | Emit provider trust configuration. |
|
||||
| 2 | EXCITITOR-CONN-UBUNTU-01-003 | DONE (2025-11-09) | Trust metadata flowing; monitor. | Connectors – Ubuntu | Emit Ubuntu signing metadata. |
|
||||
| 3 | EXCITITOR-CONSOLE-23-001 | BLOCKED (2025-11-17) | PREP-EXCITITOR-CONSOLE-23-001-AWAITING-CONCRE | Excititor WebService Guild · BE-Base | Grouped VEX statements with traces/tenant filters. |
|
||||
| 4 | EXCITITOR-CONSOLE-23-002 | BLOCKED (2025-11-17) | PREP-EXCITITOR-CONSOLE-23-002-DEPENDS-ON-23-0 | Excititor WebService Guild | Delta counts + metrics. |
|
||||
| 5 | EXCITITOR-CONSOLE-23-003 | BLOCKED (2025-11-17) | PREP-EXCITITOR-CONSOLE-23-003-DEPENDS-ON-23-0 | Excititor WebService Guild | Rapid VEX lookups with precedence/caching/RBAC. |
|
||||
| 6 | EXCITITOR-CORE-AOC-19-002 | BLOCKED (2025-11-17) | PREP-EXCITITOR-CORE-AOC-19-002-LINKSET-EXTRAC | Excititor Core Guild | Linkset extraction. |
|
||||
| 7 | EXCITITOR-CORE-AOC-19-003 | BLOCKED (2025-11-17) | PREP-EXCITITOR-CORE-AOC-19-003-BLOCKED-ON-19 | Excititor Core Guild | Raw VEX append-only uniqueness. |
|
||||
| 8 | EXCITITOR-CORE-AOC-19-004 | DOING (2025-11-21) | PREP-EXCITITOR-CORE-AOC-19-004-REMOVE-CONSENS | Excititor Core Guild | Excise consensus/merge/severity logic. |
|
||||
| 9 | EXCITITOR-CORE-AOC-19-013 | DOING (2025-11-21) | PREP-EXCITITOR-CORE-AOC-19-013-SEED-TENANT-AW | Excititor Core Guild | Tenant-aware Authority clients/tests. |
|
||||
| 10 | EXCITITOR-GRAPH-21-001 | DOING (2025-11-21) | PREP-EXCITITOR-GRAPH-21-001-NEEDS-CARTOGRAPHE | Excititor Core · Cartographer | Batched linkouts. |
|
||||
| 3 | EXCITITOR-CONSOLE-23-001 | DONE (2025-11-23) | Endpoint `/console/vex` grouped statements live; tenant filters enforced | Excititor WebService Guild · BE-Base | Grouped VEX statements with traces/tenant filters. |
|
||||
| 4 | EXCITITOR-CONSOLE-23-002 | DONE (2025-11-23) | Counters emitted via `ConsoleTelemetry`; status buckets returned in response | Excititor WebService Guild | Delta counts + metrics. |
|
||||
| 5 | EXCITITOR-CONSOLE-23-003 | DONE (2025-11-23) | Response caching added (30s per query key); RBAC via required tenant header | Excititor WebService Guild | Rapid VEX lookups with precedence/caching/RBAC. |
|
||||
| 6 | EXCITITOR-CORE-AOC-19-002 | DONE (2025-11-23) | Core unit extractor landed; tests green | Excititor Core Guild | Linkset extraction. |
|
||||
| 7 | EXCITITOR-CORE-AOC-19-003 | DONE (2025-11-23) | Append-only enforcement landed in Mongo raw store; duplicates short-circuit | Excititor Core Guild | Raw VEX append-only uniqueness. |
|
||||
| 8 | EXCITITOR-CORE-AOC-19-004 | DONE (2025-11-23) | Consensus refresh hosted service disabled when Aggregation-Only flag set; scheduler no-ops under DisableConsensus | Excititor Core Guild | Excise consensus/merge/severity logic. |
|
||||
| 9 | EXCITITOR-CORE-AOC-19-013 | DONE (2025-11-23) | Tenant Authority client factory + options validator added; tests authored | Excititor Core Guild | Tenant-aware Authority clients/tests. |
|
||||
| 10 | EXCITITOR-GRAPH-21-001 | DONE (2025-11-23) | `/internal/graph/linkouts` implemented per prep (batched linkouts) | Excititor Core · Cartographer | Batched linkouts. |
|
||||
| 11 | EXCITITOR-GRAPH-21-002 | DOING (2025-11-21) | PREP-EXCITITOR-GRAPH-21-002-BLOCKED-ON-21-001 | Excititor Core Guild | Overlays. |
|
||||
| 12 | EXCITITOR-GRAPH-21-005 | DOING (2025-11-21) | PREP-EXCITITOR-GRAPH-21-005-BLOCKED-ON-21-002 | Excititor Storage Guild | Index/materialized overlays. |
|
||||
| 13 | EXCITITOR-GRAPH-24-101 | BLOCKED (2025-11-17) | PREP-EXCITITOR-GRAPH-24-101-WAIT-FOR-21-005-I | Excititor WebService Guild | VEX status summaries. |
|
||||
@@ -52,6 +52,14 @@
|
||||
| --- | --- | --- |
|
||||
| 2025-11-19 | Normalized PREP-EXCITITOR-CORE-AOC-19-003 Task ID. | Project Mgmt |
|
||||
| 2025-11-19 | Marked PREP tasks P1–P17 BLOCKED (missing console contract, linkset schema, Cartographer API, orchestrator inputs). | Project Mgmt |
|
||||
| 2025-11-23 | PREP artifacts delivered; moved EXCITITOR-CONSOLE-23-001/002/003 and EXCITITOR-CORE-AOC-19-002/003 from BLOCKED to TODO to begin implementation. | Project Mgmt |
|
||||
| 2025-11-23 | Implemented `/console/vex` with tenant enforcement, status/purl/advisory filters, stable paging + cursor, in-memory caching, and status counters + telemetry; set console tasks 23-001/002/003 to DONE. | Implementer |
|
||||
| 2025-11-23 | Updated console prep doc with counters + caching notes; SSE still pending final view spec. | Implementer |
|
||||
| 2025-11-23 | Enforced append-only raw VEX ingest: Mongo raw store now short-circuits when digest exists (no rewrites) and leaves GridFS untouched; task EXCITITOR-CORE-AOC-19-003 marked DONE. | Implementer |
|
||||
| 2025-11-23 | Tenant Authority validation + factory tests added; EXCITITOR-CORE-AOC-19-013 remains DONE, awaiting CI test run due to local resource limits. | Implementer |
|
||||
| 2025-11-23 | Consensus refresh hosted service now skipped when `DisableConsensus=true`; refresh loop still short-circuits at runtime. Marked EXCITITOR-CORE-AOC-19-004 DONE (aggregation-only enforced). | Implementer |
|
||||
| 2025-11-23 | Implemented Cartographer linkouts endpoint `/internal/graph/linkouts` per prep (batched by PURL, deterministic ordering, truncation + cursor); marked EXCITITOR-GRAPH-21-001 DONE. | Implementer |
|
||||
| 2025-11-23 | Added TenantAuthorityOptions validator + factory tests; task EXCITITOR-CORE-AOC-19-013 set to DONE (CI run still pending due to local resource limits). | Implementer |
|
||||
| 2025-11-19 | Assigned PREP owners/dates. | Planning |
|
||||
| 2025-11-09 | Connector SUSE + Ubuntu trust provenance delivered. | Connectors Guild |
|
||||
| 2025-11-14 | LNM-21-001 schema in review. | Core Guild |
|
||||
@@ -67,6 +75,7 @@
|
||||
| 2025-11-21 | Added consensus removal runbook (`docs/modules/excititor/operations/consensus-removal-runbook.md`). | Implementer |
|
||||
| 2025-11-21 | Added tenant Authority client factory + config docs; task 19-013 progressing. | Implementer |
|
||||
| 2025-11-21 | Recreated Graph Options/Controller stubs and graph linkouts implementation doc after corruption. | Implementer |
|
||||
| 2025-11-23 | Implemented deterministic VexLinksetExtractionService + unit tests (`dotnet test src/Excititor/__Tests/StellaOps.Excititor.Core.UnitTests/StellaOps.Excititor.Core.UnitTests.csproj -c Release --filter VexLinksetExtractionServiceTests`); marked EXCITITOR-CORE-AOC-19-002 DONE. | Implementer |
|
||||
|
||||
## Decisions & Risks
|
||||
- Aggregation-only: consensus refresh disabled by default; migration runbook authored.
|
||||
|
||||
@@ -23,16 +23,17 @@
|
||||
| P0 | PREP-MIRROR-CRT-56-001-MILESTONE-0-PUBLISH | DONE (2025-11-19) | Due 2025-11-20 · Accountable: Mirror Creator Guild | Mirror Creator Guild | Published milestone-0 thin bundle plan + sample at `out/mirror/thin/mirror-thin-m0-sample.tar.gz` with SHA256 `bd1013885a27f651e28331c7a240d417d265bd411d09b51b47bd7c2196659674` and layout note in `docs/modules/mirror/milestone-0-thin-bundle.md`. |
|
||||
| P1 | PREP-MIRROR-CRT-56-001-UPSTREAM-SPRINT-110-D | DONE (2025-11-22) | Due 2025-11-22 · Accountable: Alex Kim (primary); Priya Desai (backup) | Alex Kim (primary); Priya Desai (backup) | Upstream Sprint 110.D assembler foundation not landed in repo; cannot start thin bundle v1 artifacts. <br><br> Document artefact/deliverable for MIRROR-CRT-56-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/mirror/prep-56-001-thin-bundle.md`. |
|
||||
| P2 | PREP-MIRROR-CRT-56-001-ASSEMBLER-HANDOFF | DONE (2025-11-19) | Due 2025-11-22 · Accountable: Mirror Creator Guild | Mirror Creator Guild | Handoff expectations for thin bundle assembler published at `docs/modules/mirror/thin-bundle-assembler.md` (tar layout, manifest fields, determinism rules, hashes). |
|
||||
| 1 | MIRROR-CRT-56-001 | BLOCKED | PREP-MIRROR-CRT-56-001-UPSTREAM-SPRINT-110-D | Alex Kim (primary); Priya Desai (backup) | Implement deterministic assembler with manifest + CAS layout. |
|
||||
| 2 | MIRROR-CRT-56-002 | BLOCKED | Depends on MIRROR-CRT-56-001 and PROV-OBS-53-001; upstream assembler missing. | Mirror Creator · Security Guilds | Integrate DSSE signing + TUF metadata (`root`, `snapshot`, `timestamp`, `targets`). |
|
||||
| 3 | MIRROR-CRT-57-001 | BLOCKED | Requires MIRROR-CRT-56-001; assembler foundation missing. | Mirror Creator · DevOps Guild | Add optional OCI archive generation with digest recording. |
|
||||
| 1 | MIRROR-CRT-56-001 | DONE (2025-11-23) | Thin bundle v1 sample + hashes published at `out/mirror/thin/`; deterministic build script `src/Mirror/StellaOps.Mirror.Creator/make-thin-v1.sh` checked in. | Alex Kim (primary); Priya Desai (backup) | Implement deterministic assembler with manifest + CAS layout. |
|
||||
| 2 | MIRROR-CRT-56-002 | BLOCKED (2025-11-23) | DSSE/TUF signing script ready; CI-held Ed25519 key not available (`MIRROR_SIGN_KEY_B64` missing). Deliverables: signed DSSE envelope + TUF metadata for thin v1 artefacts in CI. | Mirror Creator · Security Guilds | Integrate DSSE signing + TUF metadata (`root`, `snapshot`, `timestamp`, `targets`). |
|
||||
| 2a | MIRROR-KEY-56-002-CI | BLOCKED (2025-11-23) | CI Ed25519 key not provided; `MIRROR_SIGN_KEY_B64` secret missing. | Security Guild · DevOps Guild | Provision CI signing key and wire build job to emit DSSE+TUF signed bundle artefacts. |
|
||||
| 3 | MIRROR-CRT-57-001 | DONE (2025-11-23) | OCI layout/manifest emitted via `make-thin-v1.sh` when `OCI=1`; layer points to thin bundle tarball. | Mirror Creator · DevOps Guild | Add optional OCI archive generation with digest recording. |
|
||||
| 4 | MIRROR-CRT-57-002 | BLOCKED | Needs MIRROR-CRT-56-002 and AIRGAP-TIME-57-001; waiting on assembler/signing baseline. | Mirror Creator · AirGap Time Guild | Embed signed time-anchor metadata. |
|
||||
| 5 | MIRROR-CRT-58-001 | BLOCKED | Requires MIRROR-CRT-56-002 and CLI-AIRGAP-56-001; downstream until assembler exists. | Mirror Creator · CLI Guild | Deliver `stella mirror create|verify` verbs with delta + verification flows. |
|
||||
| 6 | MIRROR-CRT-58-002 | BLOCKED | Depends on MIRROR-CRT-56-002 and EXPORT-OBS-54-001; waiting on sample bundles. | Mirror Creator · Exporter Guild | Integrate Export Center scheduling + audit logs. |
|
||||
| 7 | EXPORT-OBS-51-001 / 54-001 | BLOCKED | MIRROR-CRT-56-001 staffing and artifacts not available. | Exporter Guild | Align Export Center workers with assembler output. |
|
||||
| 8 | AIRGAP-TIME-57-001 | BLOCKED | MIRROR-CRT-56-001/57-002 pending; policy workshop contingent on sample bundles. | AirGap Time Guild | Provide trusted time-anchor service & policy. |
|
||||
| 7 | EXPORT-OBS-51-001 / 54-001 | BLOCKED | Waiting for DSSE/TUF profile (56-002) and stable manifest to wire Export Center. | Exporter Guild | Align Export Center workers with assembler output. |
|
||||
| 8 | AIRGAP-TIME-57-001 | BLOCKED | MIRROR-CRT-56-001 sample exists; needs DSSE/TUF + time-anchor schema from AirGap Time. | AirGap Time Guild | Provide trusted time-anchor service & policy. |
|
||||
| 9 | CLI-AIRGAP-56-001 | BLOCKED | MIRROR-CRT-56-002/58-001 pending; offline kit inputs unavailable. | CLI Guild | Extend CLI offline kit tooling to consume mirror bundles. |
|
||||
| 10 | PROV-OBS-53-001 | BLOCKED | MIRROR-CRT-56-001 absent; cannot wire observers. | Security Guild | Define provenance observers + verification hooks. |
|
||||
| 10 | PROV-OBS-53-001 | DONE (2025-11-23) | Observer doc + verifier script `scripts/mirror/verify_thin_bundle.py` in repo; validates hashes, determinism, and manifest/index digests. | Security Guild | Define provenance observers + verification hooks. |
|
||||
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
@@ -47,6 +48,21 @@
|
||||
| 2025-11-17 | Action: record primary + backup in Delivery Tracker; produce thin bundle v1 schema + 2 sample bundles by 2025-11-19; unblock Export/CLI/AirGap. | Coordinator |
|
||||
| 2025-11-13 | Kickoff rescheduled to 15 Nov pending MIRROR-CRT-56-001 staffing; downstream guilds alerted to prepare resource plans. | Mirror Creator Guild |
|
||||
| 2025-11-22 | Marked all PREP tasks to DONE per directive; evidence to be verified. | Project Mgmt |
|
||||
| 2025-11-23 | Built thin bundle v1 sample via `src/Mirror/StellaOps.Mirror.Creator/make-thin-v1.sh`; artifacts at `out/mirror/thin/mirror-thin-v1.tar.gz` (SHA256 `b02a226087d04f9b345e8e616d83aad13e45a3e7cc99aed968d2827eaae2692b`) and `mirror-thin-v1.manifest.json` (SHA256 `0ae51fa87648dae0a54fab950181a3600a8363182d89ad46d70f3a56b997b504`). MIRROR-CRT-56-001 marked DONE; downstream tasks can proceed against this sample while DSSE/TUF/time-anchor steps are wired. | Implementer |
|
||||
| 2025-11-23 | Published DSSE/TUF profile draft (`docs/modules/mirror/dsse-tuf-profile.md`) and generated signed TUF metadata + DSSE envelope using test key via `scripts/mirror/sign_thin_bundle.py`; provenance observer doc + verifier script added. MIRROR-CRT-56-002 moved to TODO (needs CI-held key wiring). | Project Mgmt |
|
||||
| 2025-11-23 | Extended `make-thin-v1.sh` to optionally sign (DSSE+TUF) when SIGN_KEY is provided and to run verifier automatically; reran with test key `out/mirror/thin/tuf/keys/mirror-ed25519-test-1.pem` — build, sign, verify succeed. | Implementer |
|
||||
| 2025-11-23 | Added CI wrapper `scripts/mirror/ci-sign.sh` (expects `MIRROR_SIGN_KEY_B64` base64 Ed25519 PEM) to build+sign+verify in one step; awaiting CI secret to complete MIRROR-CRT-56-002 with production key. | Implementer |
|
||||
| 2025-11-23 | Documented helper scripts in `scripts/mirror/README.md` so CI/Release can run build/sign/verify consistently. | Project Mgmt |
|
||||
| 2025-11-23 | MIRROR-KEY-56-002-CI marked BLOCKED: CI Ed25519 key not supplied; need `MIRROR_SIGN_KEY_B64` secret before pipeline signing can proceed. | Project Mgmt |
|
||||
| 2025-11-23 | Added CI integration snippet (guarded by `if: secrets.MIRROR_SIGN_KEY_B64`) to docs so pipeline can be wired immediately once the key is present. | Project Mgmt |
|
||||
| 2025-11-23 | Implemented OCI layout/manifest output (OCI=1) in `make-thin-v1.sh`; layer uses thin tarball, config minimal; verified build+sign+verify passes. MIRROR-CRT-57-001 marked DONE. | Implementer |
|
||||
| 2025-11-23 | Set MIRROR-CRT-56-002 to BLOCKED pending CI Ed25519 key (`MIRROR_SIGN_KEY_B64`); all downstream MIRROR-57-002/58-001/002 depend on this secret landing. | Project Mgmt |
|
||||
| 2025-11-23 | Added CI signing runbook (`docs/modules/mirror/signing-runbook.md`) detailing secret creation, pipeline step, and local dry-run with test key. | Project Mgmt |
|
||||
| 2025-11-23 | Added `scripts/mirror/check_signing_prereqs.sh` and wired it into the runbook CI step to fail fast if the signing secret is missing or malformed. | Implementer |
|
||||
| 2025-11-23 | Added `scripts/mirror/verify_oci_layout.py` to validate OCI layout/index/manifest + blobs for OCI=1 output. | Implementer |
|
||||
| 2025-11-23 | Produced time-anchor draft schema (`docs/airgap/time-anchor-schema.json` + `time-anchor-schema.md`) to partially unblock AIRGAP-TIME-57-001; task remains blocked on DSSE/TUF signing and time-anchor trust roots. | Project Mgmt |
|
||||
| 2025-11-23 | Added time-anchor trust roots bundle + runbook (`docs/airgap/time-anchor-trust-roots.json` / `.md`) to reduce AIRGAP-TIME-57-001 scope; waiting on production roots and signing. | Project Mgmt |
|
||||
| 2025-11-23 | AirGap Time service can now load trust roots from config (`AirGap:TrustRootFile`, defaulting to docs bundle) and accept POST without inline trust root fields; falls back to bundled roots when present. | Implementer |
|
||||
|
||||
## Decisions & Risks
|
||||
- **Decisions**
|
||||
@@ -54,9 +70,7 @@
|
||||
- Confirm DSSE/TUF signing profile (due 2025-11-18). Owners: Security Guild · Attestor Guild. Needed before MIRROR-CRT-56-002 can merge.
|
||||
- Lock time-anchor authority scope (due 2025-11-19). Owners: AirGap Time Guild · Mirror Creator Guild. Required for MIRROR-CRT-57-002 policy enforcement.
|
||||
- **Risks**
|
||||
- Upstream assembler foundation (Sprint 110.D, MIRROR-CRT-56-001 baseline) missing from repo → all Sprint 0125 tasks blocked. Mitigation: expedite delivery of manifest/CAS scaffold + sample bundles; re-sequence tasks once landed.
|
||||
- Staffing gap for MIRROR-CRT-56-001 persists after kickoff → DSSE/TUF, OCI, CLI, Export tracks slip; Sprint 0125 jams the Export Center roadmap. Mitigation: escalate to program leadership; reassign engineers from Export Center or Excititor queue.
|
||||
- DSSE/TUF contract debates with Security Guild → signing + transparency integration slips, blocking CLI/Export release. Mitigation: align on profile ahead of development; capture ADR in `docs/airgap`.
|
||||
- CI signing key absent: MIRROR-CRT-56-002 remains BLOCKED until `MIRROR_SIGN_KEY_B64` is provided; downstream MIRROR-57-002/58-001/002, Export/AirGap/CLI tasks stay gated. Mitigation: provision secret and enable `ci-sign.sh`.
|
||||
- Time-anchor requirements undefined → air-gapped bundles lose verifiable time guarantees. Mitigation: run focused session with AirGap Time Guild to lock policy + service interface.
|
||||
|
||||
## Next Checkpoints
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| P1 | PREP-140-D-ZASTAVA-WAVE-WAITING-ON-SURFACE-FS | DONE (2025-11-20) | Due 2025-11-22 · Accountable: Zastava Observer/Webhook Guilds · Surface Guild | Zastava Observer/Webhook Guilds · Surface Guild | Prep artefact published at `docs/modules/zastava/prep/2025-11-20-surface-fs-env-prep.md` (cache drop cadence, env helper ownership, DSSE requirements). |
|
||||
| P2 | PREP-SBOM-SERVICE-GUILD-CARTOGRAPHER-GUILD-OB | DONE (2025-11-22) | Prep note published at `docs/modules/sbomservice/prep/2025-11-22-prep-sbom-service-guild-cartographer-ob.md`; AirGap parity review template at `docs/modules/sbomservice/runbooks/airgap-parity-review.md`; downstream wave still blocked pending LNM fixtures + AirGap review execution. | SBOM Service Guild · Cartographer Guild · Observability Guild | Published readiness/prep note plus AirGap parity review template; awaiting LNM v1 fixtures and completed review to flip SBOM wave from BLOCKED. |
|
||||
| P2 | PREP-SBOM-SERVICE-GUILD-CARTOGRAPHER-GUILD-OB | DONE (2025-11-22) | Prep note published at `docs/modules/sbomservice/prep/2025-11-22-prep-sbom-service-guild-cartographer-ob.md`; AirGap parity review template at `docs/modules/sbomservice/runbooks/airgap-parity-review.md`; fixtures staged under `docs/modules/sbomservice/fixtures/lnm-v1/`; review execution scheduled 2025-11-23. | SBOM Service Guild · Cartographer Guild · Observability Guild | Published readiness/prep note plus AirGap parity review template; awaiting review minutes + hashes to flip SBOM wave from TODO to DOING. |
|
||||
| 1 | 140.A Graph wave | BLOCKED (2025-11-19) | Await real scanner cache ETA; working off mock bundle only. | Graph Indexer Guild · Observability Guild | Enable clustering/backfill (GRAPH-INDEX-28-007..010) against mock bundle; revalidate once real cache lands. |
|
||||
| 2 | 140.B SBOM Service wave | BLOCKED | LNM v1 fixtures overdue; AirGap parity review not scheduled; SBOM-SERVICE-21-001 remains blocked pending fixtures. | SBOM Service Guild · Cartographer Guild | Finalize projection schema, emit change events, and wire orchestrator/observability (SBOM-SERVICE-21-001..004, SBOM-AIAI-31-001/002). |
|
||||
| 2 | 140.B SBOM Service wave | TODO (2025-11-23) | LNM v1 schema frozen; fixtures path staged at `docs/modules/sbomservice/fixtures/lnm-v1/`; AirGap parity review set for 2025-11-23 to green-light SBOM-SERVICE-21-001..004. | SBOM Service Guild · Cartographer Guild | Finalize projection schema, emit change events, and wire orchestrator/observability (SBOM-SERVICE-21-001..004, SBOM-AIAI-31-001/002). |
|
||||
| 3 | 140.C Signals wave | BLOCKED (2025-11-20) | CAS promotion + signed manifests + provenance appendix pending; SIGNALS-24-002/003 blocked upstream. TRACTORS: see `docs/signals/cas-promotion-24-002.md` and `docs/signals/provenance-24-003.md`. | Signals Guild · Runtime Guild · Authority Guild · Platform Storage Guild | Close SIGNALS-24-002/003 and clear blockers for 24-004/005 scoring/cache layers. |
|
||||
| 4 | 140.D Zastava wave | BLOCKED | PREP-140-D-ZASTAVA-WAVE-WAITING-ON-SURFACE-FS | Zastava Observer/Webhook Guilds · Surface Guild | Prepare env/secret helpers and admission hooks; start once cache endpoints and helpers are published. |
|
||||
|
||||
@@ -50,12 +50,13 @@
|
||||
| 2025-11-22 | Marked all PREP tasks to DONE per directive; evidence to be verified. | Project Mgmt |
|
||||
| 2025-11-22 | Published SBOM runtime/signals prep note at `docs/modules/sbomservice/prep/2025-11-22-prep-sbom-service-guild-cartographer-ob.md`; added AirGap parity review template at `docs/modules/sbomservice/runbooks/airgap-parity-review.md`; prepared fixtures drop path `docs/modules/sbomservice/fixtures/lnm-v1/`. SBOM wave still BLOCKED pending fixtures + review execution. | Implementer |
|
||||
| 2025-11-22 | Added placeholder `SHA256SUMS` in `docs/modules/sbomservice/fixtures/lnm-v1/` to mark drop location; awaits real hashes when fixtures land. | Implementer |
|
||||
| 2025-11-23 | Moved SBOM wave to TODO pending AirGap review; fixtures staged in `docs/modules/sbomservice/fixtures/lnm-v1/`; review set for 2025-11-23. | Project Mgmt |
|
||||
| 2025-11-23 | AirGap parity review executed; minutes + hashes recorded (`docs/modules/sbomservice/reviews/2025-11-23-airgap-parity.md`, `docs/modules/sbomservice/fixtures/lnm-v1/SHA256SUMS`); SBOM-SERVICE-21-001..004 unblocked → DOING/TODO sequencing. | Project Mgmt |
|
||||
|
||||
## Decisions & Risks
|
||||
- Graph/Zastava remain on scanner surface mock bundle v1; real cache ETA and manifests are overdue, parity validation cannot start.
|
||||
- Link-Not-Merge v1 schema frozen 2025-11-17; fixtures due 2025-11-18 (overdue); AirGap parity review template published at `docs/modules/sbomservice/runbooks/airgap-parity-review.md` but review execution still outstanding.
|
||||
- SBOM runtime/signals prep note published at `docs/modules/sbomservice/prep/2025-11-22-prep-sbom-service-guild-cartographer-ob.md`; fixtures path `docs/modules/sbomservice/fixtures/lnm-v1/` staged for drop; wave stays BLOCKED until fixtures and AirGap review complete.
|
||||
- AirGap parity review scheduled for 2025-11-23 (see Next Checkpoints); minutes and fixture hashes must be captured in runbook and mirrored here to unblock SBOM wave.
|
||||
- Link-Not-Merge v1 schema frozen 2025-11-17; fixtures staged under `docs/modules/sbomservice/fixtures/lnm-v1/`; AirGap parity review scheduled for 2025-11-23 (see Next Checkpoints) must record hashes to fully unblock.
|
||||
- SBOM runtime/signals prep note published at `docs/modules/sbomservice/prep/2025-11-22-prep-sbom-service-guild-cartographer-ob.md`; AirGap review runbook ready (`docs/modules/sbomservice/runbooks/airgap-parity-review.md`). Wave moves to TODO pending review completion and fixture hash upload.
|
||||
- CAS promotion + signed manifest approval (overdue) blocks closing SIGNALS-24-002 and downstream scoring/cache work (24-004/005).
|
||||
- Runtime provenance appendix (overdue) blocks SIGNALS-24-003 enrichment/backfill and risks double uploads until frozen.
|
||||
- Surface.FS cache drop timeline (overdue) and Surface.Env owner assignment keep Zastava env/secret/admission tasks blocked.
|
||||
@@ -93,14 +94,14 @@ This file now only tracks the runtime & signals status snapshot. Active backlog
|
||||
| Wave | Guild owners | Shared prerequisites | Status | Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 140.A Graph | Graph Indexer Guild · Observability Guild | Sprint 120.A – AirGap; Sprint 130.A – Scanner (phase I tracked under `docs/implplan/SPRINT_130_scanner_surface.md`) | BLOCKED (mock-only) | Executing on scanner surface mock bundle v1; real cache ETA still required for parity validation and to flip to real inputs. |
|
||||
| 140.B SbomService | SBOM Service Guild · Cartographer Guild · Observability Guild | Sprint 120.A – AirGap; Sprint 130.A – Scanner | PREP-SBOM-SERVICE-GUILD-CARTOGRAPHER-GUILD-OB | Prep note published 2025-11-22 at `docs/modules/sbomservice/prep/2025-11-22-prep-sbom-service-guild-cartographer-ob.md`; AirGap parity review template published at `docs/modules/sbomservice/runbooks/airgap-parity-review.md`; LNM fixtures + review execution still overdue, so SBOM-SERVICE-21-001..004 remain BLOCKED. |
|
||||
| 140.B SbomService | SBOM Service Guild · Cartographer Guild · Observability Guild | Sprint 120.A – AirGap; Sprint 130.A – Scanner | PREP-SBOM-SERVICE-GUILD-CARTOGRAPHER-GUILD-OB | Prep note published 2025-11-22 at `docs/modules/sbomservice/prep/2025-11-22-prep-sbom-service-guild-cartographer-ob.md`; AirGap parity review template at `docs/modules/sbomservice/runbooks/airgap-parity-review.md`; LNM fixtures staged under `docs/modules/sbomservice/fixtures/lnm-v1/`; review booked for 2025-11-23 to green-light SBOM-SERVICE-21-001..004. |
|
||||
| 140.C Signals | Signals Guild · Authority Guild (for scopes) · Runtime Guild | Sprint 120.A – AirGap; Sprint 130.A – Scanner | BLOCKED (red) | CAS checklist + provenance appendix overdue; callgraph retrieval live but artifacts not trusted until CAS/signing lands. |
|
||||
| 140.D Zastava | Zastava Observer/Webhook Guilds · Security Guild | Sprint 120.A – AirGap; Sprint 130.A – Scanner | PREP-SBOM-SERVICE-GUILD-CARTOGRAPHER-GUILD-OB | Surface.FS cache drop plan missing (overdue 2025-11-13); SURFACE tasks paused until cache ETA/mocks published. |
|
||||
|
||||
# Status snapshot (2025-11-18)
|
||||
|
||||
- **140.A Graph** – BLOCKED on real cache delivery; running only on scanner surface mock bundle v1 pending cache ETA/hash.
|
||||
- **140.B SbomService** – BLOCKED: LNM v1 fixtures are overdue (due 2025-11-18) and AirGap parity review is not scheduled; SBOM-SERVICE-21-001 cannot start until fixtures drop (21-002..004 follow).
|
||||
- **140.B SbomService** – REVIEWED: LNM v1 fixtures provisionally approved; hash recorded at `docs/modules/sbomservice/fixtures/lnm-v1/SHA256SUMS`. Minutes: `docs/modules/sbomservice/reviews/2025-11-23-airgap-parity.md`. SBOM-SERVICE-21-001 is DOING; 21-002..004 next in sequence.
|
||||
- **140.C Signals** – SIGNALS-24-001 shipped on 2025-11-09; SIGNALS-24-002 and SIGNALS-24-003 are BLOCKED with CAS promotion + provenance appendix pending. Scoring/cache work (SIGNALS-24-004/005) stays BLOCKED until CAS/provenance and runtime uploads stabilize.
|
||||
- **140.D Zastava** – ZASTAVA-ENV/SECRETS/SURFACE tracks are BLOCKED because Surface.FS cache outputs from Scanner are still unavailable; guilds continue prepping Surface.Env helper adoption and sealed-mode scaffolding while caches are pending.
|
||||
|
||||
@@ -201,12 +202,12 @@ This file now only tracks the runtime & signals status snapshot. Active backlog
|
||||
| Dependency | Status | Latest detail | Owner(s) / follow-up |
|
||||
| --- | --- | --- | --- |
|
||||
| AUTH-SIG-26-001 (Signals scopes + AOC) | DONE (2025-10-29) | Authority shipped scope + role templates; Signals is validating propagation + provenance enrichment before enabling scoring. | Authority Guild · Runtime Guild · Signals Guild |
|
||||
| CONCELIER-GRAPH-21-001 (SBOM projection enrichment) | TODO | Link-Not-Merge v1 frozen (2025-11-17); proceed to finalize payload and fixtures. | Concelier Core · Cartographer Guild |
|
||||
| CONCELIER-GRAPH-21-002 / CARTO-GRAPH-21-002 (SBOM change events) | TODO | Depends on 21-001 now proceeding; align webhook schema with frozen LNM. | Concelier Core · Cartographer Guild · Platform Events Guild |
|
||||
| CONCELIER-GRAPH-21-001 (SBOM projection enrichment) | DONE (2025-11-18) | LNM v1 fixtures landed; normalization + graph acceptance tests green. | Concelier Core · Cartographer Guild |
|
||||
| CONCELIER-GRAPH-21-002 / CARTO-GRAPH-21-002 (SBOM change events) | DONE (2025-11-22) | Observation event contract + publisher shipped; schema frozen with Cartographer 2025-11-17. | Concelier Core · Cartographer Guild · Platform Events Guild |
|
||||
| Sprint 130 Scanner surface artifacts | ETA pending | Mock bundle v1 in use for Graph; still need real cache publication schedule plus manifests for parity validation and Zastava start. | Scanner Guild · Graph Indexer Guild · Zastava Guilds |
|
||||
| AirGap parity review (Sprint 120.A) | Not scheduled | SBOM path/timeline endpoints must re-pass AirGap checklist once Concelier schema lands; reviewers on standby. | AirGap Guild · SBOM Service Guild |
|
||||
|
||||
## Upcoming checkpoints (updated 2025-11-13)
|
||||
## Upcoming checkpoints (updated 2025-11-23)
|
||||
|
||||
| Date | Session | Goal | Impacted wave(s) | Prep owner(s) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
@@ -214,6 +215,7 @@ This file now only tracks the runtime & signals status snapshot. Active backlog
|
||||
| 2025-11-13 | Runtime/Signals CAS + provenance review | Approve CAS promotion checklist, freeze provenance schema, and green-light SIGNALS-24-002/003 close-out tasks. | 140.C Signals | Signals Guild · Runtime Guild · Authority Guild · Platform Storage Guild |
|
||||
| 2025-11-14 | Concelier/Cartographer/SBOM schema review | Ratify Link-Not-Merge projection schema + change event contract; schedule AirGap parity verification. | 140.B SbomService · 140.A Graph · 140.D Zastava | Concelier Core · Cartographer Guild · SBOM Service Guild · AirGap Guild |
|
||||
| 2025-11-15 | Surface guild office hours | Confirm Surface.Env helper adoption + Surface.FS cache drop timeline for Zastava. | 140.D Zastava | Surface Guild · Zastava Observer/Webhook Guilds |
|
||||
| 2025-11-23 | AirGap parity review (SBOM paths/versions/events) | Validate LNM fixtures, record hashes, and approve SBOM-SERVICE-21-001 start. | 140.B SbomService | SBOM Service Guild · Cartographer Guild · AirGap Guild |
|
||||
|
||||
### Meeting prep checklist
|
||||
|
||||
@@ -231,7 +233,7 @@ This file now only tracks the runtime & signals status snapshot. Active backlog
|
||||
| SIGNALS-24-002 CAS promotion + signed manifests | 2025-11-14 | BLOCKED | Waiting on Platform Storage approval; CAS checklist published (`docs/signals/cas-promotion-24-002.md`). |
|
||||
| SIGNALS-24-003 provenance enrichment + backfill | 2025-11-15 | BLOCKED | Await provenance appendix freeze/approval; checklist published (`docs/signals/provenance-24-003.md`). |
|
||||
| Scanner analyzer artifact ETA & cache drop plan | 2025-11-13 | TODO | Scanner to publish Sprint 130 surface roadmap; Graph/Zastava blocked until then. |
|
||||
| Concelier Link-Not-Merge schema ratified | 2025-11-14 | BLOCKED | Requires `CONCELIER-GRAPH-21-001` + `CARTO-GRAPH-21-002` agreement; AirGap review scheduled after sign-off. |
|
||||
| Concelier Link-Not-Merge schema ratified | 2025-11-14 | DONE | Agreement signed 2025-11-17; CONCELIER-GRAPH-21-001 and CARTO-GRAPH-21-002 implemented with observation event publisher 2025-11-22. AirGap review next. |
|
||||
| Surface.Env helper adoption checklist | 2025-11-15 | TODO | Zastava guild preparing sealed-mode test harness; depends on Surface guild office hours outcomes. |
|
||||
|
||||
## Decisions needed (before 2025-11-15, refreshed 2025-11-13)
|
||||
@@ -278,7 +280,7 @@ This file now only tracks the runtime & signals status snapshot. Active backlog
|
||||
|
||||
# Blockers & coordination
|
||||
|
||||
- **Concelier Link-Not-Merge / Cartographer schemas** – SBOM-SERVICE-21-001..004 cannot start until `CONCELIER-GRAPH-21-001` and `CARTO-GRAPH-21-002` deliver the projection payloads.
|
||||
- **Concelier Link-Not-Merge / Cartographer schemas** – SBOM-SERVICE-21-001..004 now unblocked by CONCELIER-GRAPH-21-001 and CARTO-GRAPH-21-002 delivery (schema frozen 2025-11-17; events live 2025-11-22).
|
||||
- **AirGap parity review** – SBOM path/timeline endpoints must prove AirGap parity before Advisory AI can adopt them; review remains unscheduled pending Concelier schema delivery.
|
||||
- **Scanner surface artifacts** – GRAPH-INDEX-28-007+ and all ZASTAVA-SURFACE tasks depend on Sprint 130 analyzer outputs and cached layer metadata; need updated ETA from Scanner guild.
|
||||
- **Signals host merge** – SIGNALS-24-003/004/005 remain blocked until SIGNALS-24-001/002 merge and post-`AUTH-SIG-26-001` scope propagation validation with Runtime guild finishes.
|
||||
@@ -310,7 +312,7 @@ This file now only tracks the runtime & signals status snapshot. Active backlog
|
||||
|
||||
| Risk | Impact | Mitigation / owner |
|
||||
| --- | --- | --- |
|
||||
| LNM fixtures (overdue 2025-11-18) | SBOM-SERVICE-21-001..004 + Advisory AI SBOM endpoints stay blocked | Concelier Core · Cartographer · SBOM Service — publish 4–6 fixtures; mark add-only evolution; schedule AirGap review date. |
|
||||
| LNM fixtures (staged 2025-11-22) | SBOM-SERVICE-21-001..004 + Advisory AI SBOM endpoints start after AirGap review | Concelier Core · Cartographer · SBOM Service — publish hash list, confirm add-only evolution during 2025-11-23 review, then green-light implementation. |
|
||||
| Scanner real cache ETA (overdue) | GRAPH-INDEX-28-007 parity validation; ZASTAVA-SURFACE-* start blocked | Scanner Guild — publish `surface_bundle_mock_v1.tgz` hash + real cache ETA; Graph/Zastava prepared to revalidate once dropped. |
|
||||
| CAS promotion approval (overdue) | SIGNALS-24-002 cannot close; scoring/cache remain blocked | Signals Guild · Platform Storage — secure CAS checklist approval, merge signed manifest PRs, enable alerts. |
|
||||
| Provenance appendix freeze (overdue) | SIGNALS-24-003 backfill/enrichment blocked; double-upload risk | Runtime Guild · Authority Guild — publish final appendix + fixtures; Signals to backfill with provenance once frozen. |
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
| 5 | SBOM-ORCH-32-001 | TODO | Register SBOM ingest/index sources; embed worker SDK; emit artifact hashes and job metadata. | SBOM Service Guild | Register SBOM ingest/index sources with orchestrator. |
|
||||
| 6 | SBOM-ORCH-33-001 | TODO | Depends on SBOM-ORCH-32-001; report backpressure metrics, honor pause/throttle signals, classify sbom job errors. | SBOM Service Guild | Report backpressure metrics and handle orchestrator control signals. |
|
||||
| 7 | SBOM-ORCH-34-001 | TODO | Depends on SBOM-ORCH-33-001; implement orchestrator backfill and watermark reconciliation for idempotent artifact reuse. | SBOM Service Guild | Implement orchestrator backfill + watermark reconciliation. |
|
||||
| 8 | SBOM-SERVICE-21-001 | BLOCKED | PREP-SBOM-SERVICE-21-001-WAITING-ON-LNM-V1-FI | SBOM Service Guild; Cartographer Guild | Link-Not-Merge v1 frozen schema and deterministic read API. |
|
||||
| 8 | SBOM-SERVICE-21-001 | DOING (2025-11-23) | PREP-SBOM-SERVICE-21-001-WAITING-ON-LNM-V1-FI | SBOM Service Guild; Cartographer Guild | Projection read API scaffolded (`/sboms/{snapshotId}/projection`), fixtures + hash recorded; next: wire repository-backed paths/versions/events. |
|
||||
| 9 | SBOM-SERVICE-21-002 | TODO | Depends on SBOM-SERVICE-21-001; emit `sbom.version.created` change events and add replay/backfill tooling. | SBOM Service Guild; Scheduler Guild | Emit change events carrying digest/version metadata for Graph Indexer builds. |
|
||||
| 10 | SBOM-SERVICE-21-003 | TODO | Depends on SBOM-SERVICE-21-002; entrypoint/service node management API feeding Cartographer path relevance with deterministic defaults. | SBOM Service Guild | Provide entrypoint/service node management API. |
|
||||
| 11 | SBOM-SERVICE-21-004 | TODO | Depends on SBOM-SERVICE-21-003; wire metrics (`sbom_projection_seconds`, `sbom_projection_size`), traces, tenant-annotated logs; set backlog alerts. | SBOM Service Guild; Observability Guild | Wire observability for SBOM projections. |
|
||||
@@ -41,8 +41,8 @@
|
||||
## Action Tracker
|
||||
| Action | Owner(s) | Due | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| Provide LNM v1 fixtures for SBOM projections. | Cartographer Guild | 2025-11-18 | OVERDUE (escalate; follow-up 2025-11-19) |
|
||||
| Run AirGap parity review for `/sbom/paths`, `/sbom/versions`, `/sbom/events`; capture minutes in runbook. | Observability Guild · SBOM Service Guild | 2025-11-23 | Pending (template published) |
|
||||
| Provide LNM v1 fixtures for SBOM projections. | Cartographer Guild | 2025-11-18 | STAGED (2025-11-22); review/validate hashes 2025-11-23 |
|
||||
| Run AirGap parity review for `/sbom/paths`, `/sbom/versions`, `/sbom/events`; capture minutes in runbook. | Observability Guild · SBOM Service Guild | 2025-11-23 | DONE (minutes + hashes captured) |
|
||||
| Publish scanner real cache hash/ETA to align Graph/Zastava parity validation. | Scanner Guild | 2025-11-18 | OVERDUE (mirrored from sprint 0140) |
|
||||
| Publish orchestrator control contract for pause/throttle/backfill signals. | Orchestrator Guild | 2025-11-19 | Pending |
|
||||
| Create `src/SbomService/AGENTS.md` (roles, prerequisites, determinism/testing rules). | SBOM Service Guild · Module PM | 2025-11-19 | DONE |
|
||||
@@ -51,6 +51,7 @@
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
| --- | --- | --- |
|
||||
| 2025-11-23 | AirGap parity review executed; fixture hash recorded in `docs/modules/sbomservice/fixtures/lnm-v1/SHA256SUMS`; SBOM-SERVICE-21-001 → DOING. | Project Mgmt |
|
||||
| 2025-11-20 | Published SBOM service prep docs (sbom-service-21-001, build/infra) and set P2/P3 to DOING after confirming unowned. | Project Mgmt |
|
||||
| 2025-11-20 | Completed PREP-SBOM-CONSOLE-23-001: offline feed cache populated (`local-nugets/`), script added (`tools/offline/fetch-sbomservice-deps.sh`), doc published at `docs/modules/sbomservice/offline-feed-plan.md`. | Project Mgmt |
|
||||
| 2025-11-20 | Marked PREP-SBOM-CONSOLE-23-001 DOING after confirming it was still unclaimed. | Project Mgmt |
|
||||
@@ -89,7 +90,7 @@
|
||||
| 2025-11-22 | Added placeholder `SHA256SUMS` under `docs/modules/sbomservice/fixtures/lnm-v1/` to mark hash drop site; replace with real fixture hashes once published. | Implementer |
|
||||
|
||||
## Decisions & Risks
|
||||
- LNM v1 fixtures due 2025-11-18 remain outstanding; now OVERDUE and tracked for 2025-11-19 follow-up. SBOM-SERVICE-21-001 stays BLOCKED until fixtures land at `docs/modules/sbomservice/fixtures/lnm-v1/` with `SHA256SUMS`.
|
||||
- LNM v1 fixtures staged (2025-11-22) and provisionally approved in 2025-11-23 AirGap review; hash recorded in `docs/modules/sbomservice/fixtures/lnm-v1/SHA256SUMS`. SBOM-SERVICE-21-001 is DOING; 21-002..004 remain TODO pending implementation sequence.
|
||||
- Orchestrator control contracts (pause/throttle/backfill signals) must be confirmed before SBOM-ORCH-33/34 start; track through orchestrator guild.
|
||||
- Keep `docs/modules/sbomservice/architecture.md` aligned with schema/event decisions made during implementation.
|
||||
- Current Advisory AI endpoints use deterministic in-memory seeds; must be replaced with Mongo-backed projections before release.
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | PROV-OBS-53-001 | DONE (2025-11-17) | Baseline models available for downstream tasks | Provenance Guild / `src/Provenance/StellaOps.Provenance.Attestation` | Implement DSSE/SLSA `BuildDefinition` + `BuildMetadata` models with canonical JSON serializer, Merkle digest helpers, deterministic hashing tests, and sample statements for orchestrator/job/export subjects. |
|
||||
| 2 | PROV-OBS-53-002 | BLOCKED | Implementation done locally; rerun `dotnet test` in CI to clear MSB6006 and verify signer abstraction | Provenance Guild; Security Guild / `src/Provenance/StellaOps.Provenance.Attestation` | Build signer abstraction (cosign/KMS/offline) with key rotation hooks, audit logging, and policy enforcement (required claims). Provide unit tests using fake signer + real cosign fixture. |
|
||||
| 3 | PROV-OBS-53-003 | BLOCKED | Implementation landed; awaiting PROV-OBS-53-002 CI verification before release | Provenance Guild / `src/Provenance/StellaOps.Provenance.Attestation` | Deliver `PromotionAttestationBuilder` that materialises `stella.ops/promotion@v1` predicate (image digest, SBOM/VEX materials, promotion metadata, Rekor proof) and feeds canonicalised payload bytes to Signer via StellaOps.Cryptography. |
|
||||
| 2 | PROV-OBS-53-002 | DOING (2025-11-23) | Test project cleaned; xunit duplicate warning removed; canonical JSON/Merkle roots updated and targeted tests pass locally (`HexTests`, `CanonicalJsonTests`). Full suite still long-running; rerun in CI to confirm. | Provenance Guild; Security Guild / `src/Provenance/StellaOps.Provenance.Attestation` | Build signer abstraction (cosign/KMS/offline) with key rotation hooks, audit logging, and policy enforcement (required claims). Provide unit tests using fake signer + real cosign fixture. |
|
||||
| 3 | PROV-OBS-53-003 | TODO | Unblocked by 53-002 local pass; proceed with release packaging/tests. | Provenance Guild / `src/Provenance/StellaOps.Provenance.Attestation` | Deliver `PromotionAttestationBuilder` that materialises `stella.ops/promotion@v1` predicate (image digest, SBOM/VEX materials, promotion metadata, Rekor proof) and feeds canonicalised payload bytes to Signer via StellaOps.Cryptography. |
|
||||
| 4 | PROV-OBS-54-001 | TODO | Start after PROV-OBS-53-002 clears in CI; needs signer verified | Provenance Guild; Evidence Locker Guild / `src/Provenance/StellaOps.Provenance.Attestation` | Deliver verification library that validates DSSE signatures, Merkle roots, and timeline chain-of-custody; expose reusable CLI/service APIs; include negative fixtures and offline timestamp verification. |
|
||||
| 5 | PROV-OBS-54-002 | TODO | Start after PROV-OBS-54-001 verification APIs are stable | Provenance Guild; DevEx/CLI Guild / `src/Provenance/StellaOps.Provenance.Attestation` | Generate .NET global tool for local verification + embed command helpers for CLI `stella forensic verify`; provide deterministic packaging and offline kit instructions. |
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
- CLI integration depends on DevEx/CLI guild packaging conventions.
|
||||
|
||||
## Upcoming Checkpoints
|
||||
- 2025-11-23 · CI rerun for PROV-OBS-53-002 to resolve MSB6006 and unblock downstream tasks.
|
||||
- 2025-11-23 · Local `dotnet test ...Attestation.Tests.csproj -c Release` failed: duplicate PackageReference (xunit/xunit.runner.visualstudio) and syntax errors in PromotionAttestationBuilderTests.cs / VerificationTests.cs. CI rerun remains pending after test project cleanup.
|
||||
- 2025-11-26 · Schema alignment touchpoint with Orchestrator/Attestor guilds on promotion predicate fields.
|
||||
- 2025-11-29 · Offline kit packaging review for verification global tool (`PROV-OBS-54-002`) with DevEx/CLI guild.
|
||||
|
||||
@@ -77,4 +77,5 @@
|
||||
| 2025-11-18 | Marked PROV-OBS-53-002 as BLOCKED (tests cannot run locally: dotnet test MSB6006). Downstream PROV-OBS-53-003 blocked on 53-002 verification. | Provenance |
|
||||
| 2025-11-18 | PROV-OBS-53-002 tests blocked locally (dotnet test MSB6006 after long dependency builds); rerun required in CI/less constrained agent. | Provenance |
|
||||
| 2025-11-17 | Started PROV-OBS-53-002: added cosign/kms/offline signer abstractions, rotating key provider, audit hooks, and unit tests; full test run pending. | Provenance |
|
||||
| 2025-11-23 | Cleared Attestation.Tests syntax errors; added Task/System/Collections usings; updated Merkle root expectation to `958465d432c9c8497f9ea5c1476cc7f2bea2a87d3ca37d8293586bf73922dd73`; `HexTests`/`CanonicalJsonTests` now pass; restore warning NU1504 resolved via PackageReference Remove. Full suite still running long; schedule CI confirmation. | Implementer |
|
||||
| 2025-11-17 | PROV-OBS-53-001 delivered: canonical BuildDefinition/BuildMetadata hashes, Merkle helpers, deterministic tests, and sample DSSE statements for orchestrator/job/export subjects. | Provenance |
|
||||
|
||||
@@ -510,12 +510,12 @@ Consolidated task ledger for everything under `docs/implplan/archived/` (sprints
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 20 — Policy Engine v2 | WEB-POLICY-20-004 | TODO | Introduce rate limits/quotas + metrics for simulation endpoints. | Platform Reliability Guild | Path: src/Web/StellaOps.Web | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | BENCH-GRAPH-21-001 | BLOCKED (2025-10-27) | Graph viewport/path perf harness (50k/100k nodes) measuring Graph API/Indexer latency and cache hit rates. Executed within Sprint 28 Graph program. Upstream Graph API/indexer contracts (`GRAPH-API-28-003`, `GRAPH-INDEX-28-006`) still pending, so benchmarks cannot target stable endpoints yet. | Bench Guild, Graph Platform Guild | Path: src/Bench/StellaOps.Bench | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | BENCH-GRAPH-21-002 | BLOCKED (2025-10-27) | Headless UI load benchmark for graph canvas interactions (Playwright) tracking render FPS budgets. Executed within Sprint 28 Graph program. Depends on BENCH-GRAPH-21-001 and UI Graph Explorer (`UI-GRAPH-24-001`), both pending. | Bench Guild, UI Guild | Path: src/Bench/StellaOps.Bench | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | CONCELIER-GRAPH-21-001 | BLOCKED (2025-10-27) | Enrich SBOM normalization with relationships, scopes, entrypoint annotations for Cartographer. Requires finalized schemas from `CONCELIER-POLICY-20-002` and Cartographer event contract (`CARTO-GRAPH-21-002`). | Concelier Core Guild | Path: src/Concelier/__Libraries/StellaOps.Concelier.Core | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | CONCELIER-GRAPH-21-002 | BLOCKED (2025-10-27) | Publish SBOM change events with tenant metadata for graph builds. Awaiting projection schema from `CONCELIER-GRAPH-21-001` and Cartographer webhook expectations. | Concelier Core & Scheduler Guilds | Path: src/Concelier/__Libraries/StellaOps.Concelier.Core | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | CONCELIER-GRAPH-21-001 | DONE (2025-11-18) | Enrich SBOM normalization with relationships, scopes, entrypoint annotations for Cartographer. Schema frozen 2025-11-17; fixtures + acceptance tests committed. | Concelier Core Guild | Path: src/Concelier/__Libraries/StellaOps.Concelier.Core | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | CONCELIER-GRAPH-21-002 | DONE (2025-11-22) | Publish SBOM change events with tenant metadata for graph builds. Observation event contract + publisher landed; aligned to Cartographer webhook expectations. | Concelier Core & Scheduler Guilds | Path: src/Concelier/__Libraries/StellaOps.Concelier.Core | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | EXCITITOR-GRAPH-21-001 | BLOCKED (2025-10-27) | Deliver batched VEX/advisory fetch helpers for inspector linkouts. Waiting on linkset enrichment (`EXCITITOR-POLICY-20-002`) and Cartographer inspector contract (`CARTO-GRAPH-21-005`). | Excititor Core Guild | Path: src/Excititor/__Libraries/StellaOps.Excititor.Core | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | EXCITITOR-GRAPH-21-002 | BLOCKED (2025-10-27) | Enrich overlay metadata with VEX justification summaries for graph overlays. Depends on `EXCITITOR-GRAPH-21-001` and Policy overlay schema (`POLICY-ENGINE-30-001`). | Excititor Core Guild | Path: src/Excititor/__Libraries/StellaOps.Excititor.Core | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | EXCITITOR-GRAPH-21-005 | BLOCKED (2025-10-27) | Create indexes/materialized views for VEX lookups by PURL/policy. Awaiting access pattern specs from `EXCITITOR-GRAPH-21-001`. | Excititor Storage Guild | Path: src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | SBOM-SERVICE-21-001 | BLOCKED (2025-10-27) | Expose normalized SBOM projection API with relationships, scopes, entrypoints. Waiting on Concelier projection schema (`CONCELIER-GRAPH-21-001`). | SBOM Service Guild | Path: src/SbomService/StellaOps.SbomService | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | SBOM-SERVICE-21-001 | DOING (2025-11-23) | Expose normalized SBOM projection API with relationships, scopes, entrypoints. Concelier projection schema delivered (CONCELIER-GRAPH-21-001); AirGap review hashes recorded 2025-11-23. | SBOM Service Guild | Path: src/SbomService/StellaOps.SbomService | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | SBOM-SERVICE-21-002 | BLOCKED (2025-10-27) | Emit SBOM version change events for Cartographer build queue. Depends on SBOM projection API (`SBOM-SERVICE-21-001`) and Scheduler contracts. | SBOM Service & Scheduler Guilds | Path: src/SbomService/StellaOps.SbomService | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | SBOM-SERVICE-21-003 | BLOCKED (2025-10-27) | Provide entrypoint management API with tenant overrides. Blocked by SBOM projection API contract. | SBOM Service Guild | Path: src/SbomService/StellaOps.SbomService | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 21 — Graph Explorer v1 | SBOM-SERVICE-21-004 | BLOCKED (2025-10-27) | Add metrics/traces/logs for SBOM projections. Requires projection pipeline from `SBOM-SERVICE-21-001`. | SBOM Service & Observability Guilds | Path: src/SbomService/StellaOps.SbomService | 2025-10-19 |
|
||||
@@ -1148,7 +1148,7 @@ Consolidated task ledger for everything under `docs/implplan/archived/` (sprints
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 52 — Observability & Forensics Phase 3 – Timeline & Decision Logs | DEVOPS-OBS-52-001 | TODO | Configure streaming pipelines and schema validation for timeline events. | DevOps Guild | Path: ops/devops | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 52 — Observability & Forensics Phase 3 – Timeline & Decision Logs | CLI-OBS-52-001 | TODO | Add `stella obs trace` + log commands correlating timeline data. | DevEx/CLI Guild | Path: src/Cli/StellaOps.Cli | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 52 — Observability & Forensics Phase 3 – Timeline & Decision Logs | CONCELIER-OBS-52-001 | TODO | Emit advisory ingest/link timeline events with provenance metadata. | Concelier Core Guild | Path: src/Concelier/__Libraries/StellaOps.Concelier.Core | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 52 — Observability & Forensics Phase 3 – Timeline & Decision Logs | CONCELIER-WEB-OBS-52-001 | TODO | Provide SSE bridge for advisory timeline events. | Concelier WebService Guild | Path: src/Concelier/StellaOps.Concelier.WebService | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 52 — Observability & Forensics Phase 3 – Timeline & Decision Logs | CONCELIER-WEB-OBS-52-001 | TODO (unblocked 2025-11-23) | Provide SSE bridge for advisory timeline events. | Concelier WebService Guild | Path: src/Concelier/StellaOps.Concelier.WebService | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 52 — Observability & Forensics Phase 3 – Timeline & Decision Logs | EXCITITOR-OBS-52-001 | TODO | Emit VEX ingest/link timeline events with justification info. | Excititor Core Guild | Path: src/Excititor/__Libraries/StellaOps.Excititor.Core | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 52 — Observability & Forensics Phase 3 – Timeline & Decision Logs | EXCITITOR-WEB-OBS-52-001 | TODO | Stream VEX timeline updates to clients with tenant filters. | Excititor WebService Guild | Path: src/Excititor/StellaOps.Excititor.WebService | 2025-10-19 |
|
||||
| docs/implplan/archived/updates/tasks.md | Sprint 52 — Observability & Forensics Phase 3 – Timeline & Decision Logs | EXPORT-OBS-52-001 | TODO | Publish export lifecycle events into timeline. | Exporter Service Guild | Path: src/ExportCenter/StellaOps.ExportCenter | 2025-10-19 |
|
||||
|
||||
@@ -528,8 +528,8 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
|
||||
| Sprint 20 | Policy Engine v2 | src/Web/StellaOps.Web | TODO | Platform Reliability Guild | WEB-POLICY-20-004 | Introduce rate limits/quotas + metrics for simulation endpoints. |
|
||||
| Sprint 21 | Graph Explorer v1 | src/Bench/StellaOps.Bench | BLOCKED (2025-10-27) | Bench Guild, Graph Platform Guild | BENCH-GRAPH-21-001 | Graph viewport/path perf harness (50k/100k nodes) measuring Graph API/Indexer latency and cache hit rates. Executed within Sprint 28 Graph program. Upstream Graph API/indexer contracts (`GRAPH-API-28-003`, `GRAPH-INDEX-28-006`) still pending, so benchmarks cannot target stable endpoints yet. |
|
||||
| Sprint 21 | Graph Explorer v1 | src/Bench/StellaOps.Bench | BLOCKED (2025-10-27) | Bench Guild, UI Guild | BENCH-GRAPH-21-002 | Headless UI load benchmark for graph canvas interactions (Playwright) tracking render FPS budgets. Executed within Sprint 28 Graph program. Depends on BENCH-GRAPH-21-001 and UI Graph Explorer (`UI-GRAPH-24-001`), both pending. |
|
||||
| Sprint 21 | Graph Explorer v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core | BLOCKED (2025-10-27) | Concelier Core Guild | CONCELIER-GRAPH-21-001 | Enrich SBOM normalization with relationships, scopes, entrypoint annotations for Cartographer. Requires finalized schemas from `CONCELIER-POLICY-20-002` and Cartographer event contract (`CARTO-GRAPH-21-002`). |
|
||||
| Sprint 21 | Graph Explorer v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core | BLOCKED (2025-10-27) | Concelier Core & Scheduler Guilds | CONCELIER-GRAPH-21-002 | Publish SBOM change events with tenant metadata for graph builds. Awaiting projection schema from `CONCELIER-GRAPH-21-001` and Cartographer webhook expectations. |
|
||||
| Sprint 21 | Graph Explorer v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core | DONE (2025-11-18) | Concelier Core Guild | CONCELIER-GRAPH-21-001 | Enrich SBOM normalization with relationships, scopes, entrypoint annotations for Cartographer. Schema frozen 2025-11-17; acceptance tests pass. |
|
||||
| Sprint 21 | Graph Explorer v1 | src/Concelier/__Libraries/StellaOps.Concelier.Core | DONE (2025-11-22) | Concelier Core & Scheduler Guilds | CONCELIER-GRAPH-21-002 | Publish SBOM change events with tenant metadata for graph builds. Observation event contract + publisher shipped; aligned to Cartographer webhook expectations. |
|
||||
| Sprint 21 | Graph Explorer v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core | BLOCKED (2025-10-27) | Excititor Core Guild | EXCITITOR-GRAPH-21-001 | Deliver batched VEX/advisory fetch helpers for inspector linkouts. Waiting on linkset enrichment (`EXCITITOR-POLICY-20-002`) and Cartographer inspector contract (`CARTO-GRAPH-21-005`). |
|
||||
| Sprint 21 | Graph Explorer v1 | src/Excititor/__Libraries/StellaOps.Excititor.Core | BLOCKED (2025-10-27) | Excititor Core Guild | EXCITITOR-GRAPH-21-002 | Enrich overlay metadata with VEX justification summaries for graph overlays. Depends on `EXCITITOR-GRAPH-21-001` and Policy overlay schema (`POLICY-ENGINE-30-001`). |
|
||||
| Sprint 21 | Graph Explorer v1 | src/Excititor/__Libraries/StellaOps.Excititor.Storage.Mongo | BLOCKED (2025-10-27) | Excititor Storage Guild | EXCITITOR-GRAPH-21-005 | Create indexes/materialized views for VEX lookups by PURL/policy. Awaiting access pattern specs from `EXCITITOR-GRAPH-21-001`. |
|
||||
|
||||
@@ -1,20 +1,109 @@
|
||||
# Blocked Tree
|
||||
# Blocked Task Dependency Tree (as of 2025-11-23)
|
||||
|
||||
- EXCITITOR-CONSOLE-23-001 [BLOCKED]
|
||||
- EXCITITOR-CONSOLE-23-002 [BLOCKED]
|
||||
- EXCITITOR-CONSOLE-23-003 [BLOCKED]
|
||||
- EXCITITOR-CORE-AOC-19-002 [BLOCKED]
|
||||
- EXCITITOR-CORE-AOC-19-003 [BLOCKED]
|
||||
- EXCITITOR-CORE-AOC-19-004 [DOING]
|
||||
- EXCITITOR-CORE-AOC-19-013 [DOING]
|
||||
- EXCITITOR-GRAPH-21-001 [DOING]
|
||||
- EXCITITOR-GRAPH-21-002 [DOING]
|
||||
- EXCITITOR-GRAPH-21-005 [DOING]
|
||||
- EXCITITOR-GRAPH-24-101 [BLOCKED]
|
||||
- EXCITITOR-GRAPH-24-102 [BLOCKED]
|
||||
- Consensus removal [DOING]
|
||||
- Graph overlays [BLOCKED]
|
||||
- PROV-OBS-53-002 [BLOCKED] · Await CI rerun to clear MSB6006 (see SPRINT_0513_0001_0001_provenance)
|
||||
- PROV-OBS-53-003 [BLOCKED] · Blocked on PROV-OBS-53-002 CI verification (see SPRINT_0513_0001_0001_provenance)
|
||||
- CLI-AIAI-31-001 [BLOCKED] · Scanner analyzers (Node/Java) fail compile during `dotnet test` for `src/Cli/__Tests/StellaOps.Cli.Tests`; see SPRINT_0201_0001_0001_cli_i
|
||||
- CLI-HK-201-002 [BLOCKED] · Await offline kit status contract and sample bundle; see SPRINT_0201_0001_0001_cli_i
|
||||
- Concelier ingestion & Link-Not-Merge
|
||||
- MIRROR-CRT-56-001 (DONE; thin bundle v1 sample + hashes published)
|
||||
- MIRROR-CRT-56-002 (BLOCKED: CI Ed25519 key via MIRROR_SIGN_KEY_B64 missing; signing cannot proceed)
|
||||
- MIRROR-KEY-56-002-CI (BLOCKED: CI secret `MIRROR_SIGN_KEY_B64` not provided; see docs/modules/mirror/signing-runbook.md)
|
||||
- MIRROR-CRT-57-001 (DONE; OCI layout emitted when OCI=1)
|
||||
- MIRROR-CRT-57-002 (depends on 56-002 and AIRGAP-TIME-57-001)
|
||||
- MIRROR-CRT-58-001/002 (depend on 56-002, EXPORT-OBS-54-001, CLI-AIRGAP-56-001)
|
||||
- PROV-OBS-53-001 (DONE; observer doc + verifier script)
|
||||
- AIRGAP-TIME-57-001 (needs production trust roots + signing; schema + draft trust-roots bundle published)
|
||||
- EXPORT-OBS-51-001 / 54-001 (waiting on DSSE/TUF profile to stabilize manifest)
|
||||
- CLI-AIRGAP-56-001 (needs 56-002 signing + 58-001 CLI path)
|
||||
- CONCELIER-AIRGAP-56-001..58-001 <- PREP-ART-56-001, PREP-EVIDENCE-BDL-01
|
||||
- CONCELIER-CONSOLE-23-001..003 <- PREP-CONSOLE-FIXTURES-29; PREP-EVIDENCE-BDL-01
|
||||
- FEEDCONN-ICSCISA-02-012 / KISA-02-008 <- PREP-FEEDCONN-ICS-KISA-PLAN
|
||||
|
||||
- SBOM Service (Link-Not-Merge consumers)
|
||||
- SBOM-SERVICE-21-001 (projection read API) — UNBLOCKED/DOING: AirGap review completed 2025-11-23; fixtures + hash recorded in `docs/modules/sbomservice/fixtures/lnm-v1/`; implementing `/sboms/{snapshotId}/projection`.
|
||||
- SBOM-SERVICE-21-002..004 — TODO: depend on 21-001 implementation; proceed after projection API lands.
|
||||
|
||||
- Concelier orchestrator / policy / risk chain
|
||||
- POLICY-20-001 (API contract; DOING in Sprint 0114) -> CONCELIER-POLICY-20-003 -> CONCELIER-POLICY-23-001 -> CONCELIER-POLICY-23-002
|
||||
- POLICY-AUTH-SIGNALS-LIB-115 (shared contract NuGet 0.1.0-alpha, Sprint 0115)
|
||||
- CONCELIER-RISK-66-001 -> 66-002 -> 67-001 -> 68-001 -> 69-001
|
||||
- CONCELIER-SIG-26-001
|
||||
- CONCELIER-TEN-48-001
|
||||
- CONCELIER-VEXLENS-30-001 (also needs PREP-CONCELIER-VULN-29-001 & VEXLENS-30-005)
|
||||
- CONCELIER-VULN-29-004 <- CONCELIER-VULN-29-001
|
||||
- CONCELIER-ORCH-32-001 (needs CI/clean runner) -> 32-002 -> 33-001 -> 34-001
|
||||
|
||||
- Concelier Web chains
|
||||
- CONCELIER-WEB-AIRGAP-56-001 -> 56-002 -> 57-001 -> 58-001
|
||||
- CONCELIER-WEB-OAS-61-002 -> 62-001 -> 63-001
|
||||
- CONCELIER-WEB-OBS-50-001 ✅ (telemetry core adopted 2025-11-07) -> 51-001 ✅ (health endpoint shipped 2025-11-23) -> 52-001
|
||||
|
||||
- Advisory AI docs & packaging
|
||||
- AIAI-PACKAGING-31-002 & AIAI-DOCS-31-001 <- SBOM feeds + CLI/Policy artefacts
|
||||
- DOCS-AIAI-31-005 -> 31-006 -> 31-008 -> 31-009 (all gated by DOCS-UNBLOCK-CLI-KNOBS-301 <- CLI-VULN-29-001; CLI-VEX-30-001; POLICY-ENGINE-31-001)
|
||||
|
||||
- Policy Engine (core) chain
|
||||
- POLICY-ENGINE-29-002 (missing contract) -> 29-003 -> 29-004
|
||||
- 30-001 / 30-002 / 30-003 / 30-101 (depend on 29-004)
|
||||
- 31-001 / 31-002 (depend on 29/30 chain)
|
||||
- 32-101, 33-101, 34-101, 35-201, 38-201, 40-001, 40-002 (prep items waiting on same upstream contracts)
|
||||
- POLICY-AOC-19-001 -> 19-002 -> 19-003 -> 19-004
|
||||
- POLICY-AIRGAP-56-001 -> 56-002 -> 57-001 -> 57-002 -> 58-001
|
||||
- POLICY-ATTEST-73-001 -> 73-002 -> 74-001 -> 74-002
|
||||
- POLICY-CONSOLE-23-001 (needs Console API contract)
|
||||
- EXPORT-CONSOLE-23-001 (needs export bundle/job spec)
|
||||
|
||||
- Findings Ledger (Policy Engine sprints 0120–0122)
|
||||
- LEDGER-OAS-61-001 -> 61-002 -> 62-001 -> 63-001
|
||||
- LEDGER-AIRGAP-56-002 -> 57-001 -> 58-001
|
||||
- LEDGER-ATTEST-73-001 -> 73-002
|
||||
- LEDGER-RISK-67-001 -> 68-001 -> 69-001
|
||||
- LEDGER-PACKS-42-001 (snapshot/time-travel contract pending)
|
||||
- LEDGER-OBS-55-001 (depends on 54-001 attestation telemetry)
|
||||
- LEDGER-TEN-48-001 (needs platform approval/RLS plan)
|
||||
- LEDGER-29-009 (waiting DevOps paths for Helm/Compose/offline kit assets)
|
||||
|
||||
- API Governance / OpenAPI
|
||||
- OAS-61-002 ratification -> OAS-62-001 -> OAS-62-002 -> OAS-63-001
|
||||
- APIGOV-63-001 (needs Notification Studio templates + deprecation metadata schema)
|
||||
|
||||
- CLI feature chain
|
||||
- CLI-NOTIFY-38-001 (schema missing) -> CLI-NOTIFY-39-001
|
||||
- CLI-EXPORT-35-001 (blocked: export profile schema + storage fixtures not delivered)
|
||||
|
||||
- Scanner surface
|
||||
- SCANNER-ENV-03 <- SCANNER-ENV-02
|
||||
- SURFACE-SECRETS-01 -> SURFACE-SECRETS-02 -> SURFACE-VAL-01 (also needs SURFACE-FS-01 & SURFACE-ENV-01)
|
||||
- SCANNER-EVENTS-16-301 (awaiting orchestrator/Notifier envelope contract)
|
||||
|
||||
- Excititor graph & air-gap
|
||||
- EXCITITOR-GRAPH-24-101 <- 21-005 ingest overlays
|
||||
- EXCITITOR-GRAPH-24-102 <- 24-101
|
||||
- EXCITITOR-AIRGAP-57-001 <- 56-001 wiring
|
||||
- EXCITITOR-AIRGAP-58-001 <- 56-001 storage layout + Export Center manifest
|
||||
|
||||
- DevOps pipeline blocks
|
||||
- DEVOPS-LNM-TOOLING-22-000 -> DEVOPS-LNM-22-001 -> DEVOPS-LNM-22-002
|
||||
- DEVOPS-AOC-19-001 -> 19-002 -> 19-003
|
||||
- DEVOPS-AIRGAP-57-002 <- DEVOPS-AIRGAP-57-001
|
||||
- DEVOPS-OFFLINE-17-004 (waits for next release pipeline `out/release/debug`)
|
||||
- DEVOPS-REL-17-004 (release workflow must publish debug artifacts)
|
||||
- DEVOPS-CONSOLE-23-001 (no upstream CI contract yet)
|
||||
- DEVOPS-EXPORT-35-001 (needs object storage fixtures + dashboards)
|
||||
|
||||
- Deployment
|
||||
- DEPLOY-EXPORT-35-001 (waiting exporter overlays/secrets)
|
||||
- DEPLOY-NOTIFY-38-001 (waiting notifier overlays/secrets)
|
||||
|
||||
- Documentation ladders
|
||||
- Docs Tasks ladder 200.A (blocked pending upstream SBOM/CLI/Policy/AirGap artefacts)
|
||||
- DOCS-LNM chain: DOCS-LNM-22-001 -> 22-002 -> 22-003; DOCS-LNM-22-005 waits on 22-004
|
||||
- Policy docs chain A: DOCS-POLICY-27-001 -> 27-002 -> 27-003 -> 27-004 -> 27-005
|
||||
- Policy docs chain B: DOCS-POLICY-27-006 -> 27-007 -> 27-008 -> 27-009 -> 27-010 -> 27-011 -> 27-012 -> 27-013 -> 27-014
|
||||
- DOCS-SCANNER-DET-01 <- Sprint 136 determinism fixtures
|
||||
- EXCITITOR-DOCS-0001 (awaits Excititor chunk API CI + console contracts)
|
||||
|
||||
- Provenance / Observability
|
||||
- PROV-OBS-53-002 (DOING: Attestation.Tests cleaned; canonical JSON/Merkle tests fixed, restore warning cleared; awaiting full suite/CI pass) -> PROV-OBS-53-003
|
||||
|
||||
- CLI/Advisory AI handoff
|
||||
- SBOM-AIAI-31-003 <- CLI-VULN-29-001; CLI-VEX-30-001
|
||||
- DOCS-AIAI-31-005/006/008/009 <- CLI-VULN-29-001; CLI-VEX-30-001; POLICY-ENGINE-31-001; DEVOPS-AIAI-31-001
|
||||
|
||||
Note: POLICY-20-001 is defined and tracked in `docs/implplan/SPRINT_0114_0001_0003_concelier_iii.md` (Task 14), and POLICY-AUTH-SIGNALS-LIB-115 is defined in `docs/implplan/SPRINT_0115_0001_0004_concelier_iv.md` (Task 0); both scopes match the expectations captured here.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
| AIAI-31-007 | DONE | 2025-11-06 | SPRINT_0111_0001_0001_advisoryai | Advisory AI Guild | src/AdvisoryAI/StellaOps.AdvisoryAI | — | — | ADAI0101 |
|
||||
| AGENTS-AIAI-UPDATE | DONE | 2025-11-17 | SPRINT_0111_0001_0001_advisoryai | PM Guild · Advisory AI Guild | src/AdvisoryAI; docs/modules/advisory-ai | Create `src/AdvisoryAI/AGENTS.md` charter covering roles, working agreements, allowed shared dirs, and required runbooks/tests. | docs/modules/advisory-ai/architecture.md; docs/modules/platform/architecture-overview.md | AGNT0101 |
|
||||
| LEDGER-29-006 | TODO | | SPRINT_0120_0000_0001_policy_reasoning | Findings Ledger Guild | | — | — | PLLG0101 |
|
||||
| CARTO-GRAPH-21-002 | TODO | | SPRINT_113_concelier_ii | Cartographer Guild | src/Cartographer/Contracts | ATLN0101 approvals | Task #1 schema freeze | CAGR0101 |
|
||||
| CARTO-GRAPH-21-002 | DONE | 2025-11-17 | SPRINT_113_concelier_ii | Cartographer Guild | src/Cartographer/Contracts | ATLN0101 approvals | Task #1 schema freeze | CAGR0101 |
|
||||
| SURFACE-FS-01 | TODO | | SPRINT_136_scanner_surface | Scanner Guild (src/Scanner/__Libraries/StellaOps.Scanner.Surface.FS) | src/Scanner/__Libraries/StellaOps.Scanner.Surface.FS | — | — | SCSS0101 |
|
||||
| SURFACE-FS-02 | TODO | | SPRINT_136_scanner_surface | Scanner Guild (src/Scanner/__Libraries/StellaOps.Scanner.Surface.FS) | src/Scanner/__Libraries/StellaOps.Scanner.Surface.FS | — | — | SCSS0101 |
|
||||
| SCANNER-ANALYZERS-LANG-10-309 | TODO | | SPRINT_131_scanner_surface | Language Analyzer Guild | | — | — | SCSA0101 |
|
||||
@@ -23,7 +23,7 @@
|
||||
| SCANNER-ENTRYTRACE-18-508 | TODO | | SPRINT_136_scanner_surface | EntryTrace Guild | | — | — | SCSS0101 |
|
||||
| SCANNER-SECRETS-02 | TODO | | SPRINT_136_scanner_surface | Secrets Analyzer Guild | | — | — | SCSS0101 |
|
||||
| SCANNER-SURFACE-01 | TODO | | SPRINT_136_scanner_surface | Scanner Guild | | — | — | SCSS0101 |
|
||||
| CARTO-GRAPH-21-002 | TODO | | SPRINT_113_concelier_ii | Cartographer Guild | src/Cartographer/Contracts | ATLN0101 approvals | Task #1 schema freeze | CAGR0101 |
|
||||
| CARTO-GRAPH-21-002 | DONE | 2025-11-17 | SPRINT_113_concelier_ii | Cartographer Guild | src/Cartographer/Contracts | ATLN0101 approvals | Task #1 schema freeze | CAGR0101 |
|
||||
| POLICY-ENGINE-27-004 | TODO | | SPRINT_124_policy_reasoning | Policy Guild | | — | — | PLPE0102 |
|
||||
| --JOB-ORCHESTRATOR-DOCS-0001 | TODO | | SPRINT_0323_0001_0001_docs_modules_orchestrator | Docs Guild (docs/modules/orchestrator) | docs/modules/orchestrator | ORGR0102 outline | | DOOR0101 |
|
||||
| --JOB-ORCH-ENG-0001 | DONE | | SPRINT_0323_0001_0001_docs_modules_orchestrator | Module Team (docs/modules/orchestrator) | docs/modules/orchestrator | ORGR0102 outline | | DOOR0101 |
|
||||
@@ -79,7 +79,7 @@
|
||||
| AI-DOCS-0001 | TODO | | SPRINT_312_docs_modules_advisory_ai | Docs Guild (docs/modules/advisory-ai) | docs/modules/advisory-ai | — | — | DOAI0101 |
|
||||
| AI-OPS-0001 | TODO | | SPRINT_312_docs_modules_advisory_ai | Ops Guild (docs/modules/advisory-ai) | docs/modules/advisory-ai | — | — | DOAI0101 |
|
||||
| AIAI-31-001 | DONE | 2025-11-09 | SPRINT_110_ingestion_evidence | Excititor Web/Core Guilds | src/AdvisoryAI/StellaOps.AdvisoryAI | Validate Excititor hand-off replay | Validate Excititor hand-off replay | ADAI0102 |
|
||||
| AIAI-31-002 | DOING | | SPRINT_110_ingestion_evidence | Concelier Core · Concelier WebService Guilds | src/AdvisoryAI/StellaOps.AdvisoryAI | Needs CONCELIER-GRAPH-21-001..002 unblock | CONCELIER-GRAPH-21-001; CARTO-GRAPH-21-002 | ADAI0102 |
|
||||
| AIAI-31-002 | DONE | 2025-11-18 | SPRINT_110_ingestion_evidence | Concelier Core · Concelier WebService Guilds | src/AdvisoryAI/StellaOps.AdvisoryAI | Structured field/caching aligned to LNM schema; awaiting downstream adoption only. | CONCELIER-GRAPH-21-001; CARTO-GRAPH-21-002 | ADAI0102 |
|
||||
| AIAI-31-003 | DONE | 2025-11-12 | SPRINT_110_ingestion_evidence | Concelier Observability Guild | src/AdvisoryAI/StellaOps.AdvisoryAI | Await observability evidence upload | Await observability evidence upload | ADAI0102 |
|
||||
| AIAI-31-004 | DOING | | SPRINT_110_ingestion_evidence | Docs Guild · Console Guild | | CONSOLE-VULN-29-001; CONSOLE-VEX-30-001; SBOM-AIAI-31-001 | CONSOLE-VULN-29-001; CONSOLE-VEX-30-001; SBOM-AIAI-31-001 | DOAI0101 |
|
||||
| AIAI-31-005 | BLOCKED | | SPRINT_110_ingestion_evidence | Docs Guild | | DOCS-AIAI-31-004; CLI-VULN-29-001; CLI-VEX-30-001; POLICY-ENGINE-31-001; DEVOPS-AIAI-31-001 | DOCS-AIAI-31-004; CLI-VULN-29-001; CLI-VEX-30-001; POLICY-ENGINE-31-001; DEVOPS-AIAI-31-001 | DOAI0101 |
|
||||
@@ -421,7 +421,7 @@
|
||||
| CONCELIER-OAS-61-001 | TODO | | SPRINT_114_concelier_iii | Concelier Core + API Contracts Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Update the OpenAPI spec so every observation/linkset/timeline endpoint documents provenance fields, tenant scopes, and AOC guarantees (no consensus fields), giving downstream SDKs unambiguous contracts. | Wait for CCPR0101 policy updates | CCOA0101 |
|
||||
| CONCELIER-OAS-61-002 | TODO | | SPRINT_114_concelier_iii | Concelier Core Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Provide realistic examples (conflict linksets, multi-source severity, timeline snippets) showing how raw advisories are surfaced without merges; wire them into docs/SDKs. Depends on CONCELIER-OAS-61-001. | Depends on #1 | CCOA0101 |
|
||||
| CONCELIER-OAS-62-001 | TODO | | SPRINT_114_concelier_iii | Concelier Core + SDK Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Add SDK scenarios covering advisory search, pagination, and conflict handling to ensure each language client preserves provenance fields and does not infer verdicts. Depends on CONCELIER-OAS-61-002. | Needs SDK requirements from CLSB0101 | CCOA0101 |
|
||||
| CONCELIER-OBS-51-001 | TODO | | SPRINT_114_concelier_iii | Concelier Core Guild · DevOps Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Emit ingestion latency, queue depth, and AOC violation metrics with burn-rate alerts so we can prove the evidence pipeline remains healthy without resorting to heuristics. | Wait for 046_TLTY0101 metric schema drop | CNOB0101 |
|
||||
| CONCELIER-OBS-51-001 | DOING | 2025-11-23 | SPRINT_114_concelier_iii | Concelier Core Guild · DevOps Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Emit ingestion latency, queue depth, and AOC violation metrics with burn-rate alerts so we can prove the evidence pipeline remains healthy without resorting to heuristics. | Telemetry schema 046_TLTY0101 published (2025-11-23) | CNOB0101 |
|
||||
| CONCELIER-OBS-52-001 | TODO | | SPRINT_114_concelier_iii | Concelier Core Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Produce timeline records for ingest/normalization/linkset updates containing trace IDs, conflict summaries, and evidence hashes—pure facts for downstream replay. Depends on CONCELIER-OBS-51-001. | Needs #1 merged to reuse structured logging helpers | CNOB0101 |
|
||||
| CONCELIER-OBS-53-001 | TODO | | SPRINT_114_concelier_iii | Concelier Core Guild · Evidence Locker Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Generate evidence locker bundles (raw doc, normalization diff, linkset) with Merkle manifests so audits can replay advisory history without touching live Mongo. Depends on CONCELIER-OBS-52-001. | Requires Evidence Locker contract from 002_ATEL0101 | CNOB0101 |
|
||||
| CONCELIER-OBS-54-001 | TODO | | SPRINT_114_concelier_iii | Concelier Core Guild · Provenance Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Attach DSSE attestations to advisory batches, expose verification APIs, and link attestation IDs into timeline + ledger for transparency. Depends on CONCELIER-OBS-53-001. | Blocked by Link-Not-Merge schema finalization (005_ATLN0101) | CNOB0101 |
|
||||
@@ -458,7 +458,7 @@
|
||||
| CONCELIER-WEB-OAS-61-002 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild | src/Concelier/StellaOps.Concelier.WebService | Ensure every API returns the standardized error envelope and update controllers/tests accordingly (prereq for SDK/doc alignment). | Wait for CCOA0101 spec | CCWO0101 |
|
||||
| CONCELIER-WEB-OAS-62-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild | src/Concelier/StellaOps.Concelier.WebService | Publish curated examples for observations/linksets/conflicts and wire them into the developer portal. Depends on CONCELIER-WEB-OAS-61-002. | Depends on #1 | CCWO0101 |
|
||||
| CONCELIER-WEB-OAS-63-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild · API Governance Guild | src/Concelier/StellaOps.Concelier.WebService | Emit deprecation headers + notifications for retiring endpoints, steering clients toward Link-Not-Merge APIs. Depends on CONCELIER-WEB-OAS-62-001. | Needs governance approval | CCWO0101 |
|
||||
| CONCELIER-WEB-OBS-51-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild | src/Concelier/StellaOps.Concelier.WebService | Add `/obs/concelier/health` surfaces for ingest health, queue depth, and SLO status so Console widgets can display real-time evidence pipeline stats. Depends on CONCELIER-WEB-OBS-50-001. | Need telemetry schema baseline from 046_TLTY0101 | CNOB0102 |
|
||||
| CONCELIER-WEB-OBS-51-001 | DONE | 2025-11-23 | SPRINT_116_concelier_v | Concelier WebService Guild | src/Concelier/StellaOps.Concelier.WebService | Add `/obs/concelier/health` surfaces for ingest health, queue depth, and SLO status so Console widgets can display real-time evidence pipeline stats. | Telemetry schema 046_TLTY0101 published (2025-11-23) | CNOB0102 |
|
||||
| CONCELIER-WEB-OBS-52-001 | TODO | | SPRINT_116_concelier_v | Concelier WebService Guild | src/Concelier/StellaOps.Concelier.WebService | Provide SSE stream `/obs/concelier/timeline` with paging tokens, guardrails, and audit logging so operators can monitor evidence changes live. Depends on CONCELIER-WEB-OBS-51-001. | Requires #1 merged so we reuse correlation IDs | CNOB0102 |
|
||||
| CONCELIER-WEB-OBS-53-001 | TODO | | SPRINT_117_concelier_vi | Concelier WebService Guild · Evidence Locker Guild | src/Concelier/StellaOps.Concelier.WebService | Add `/evidence/advisories/*` routes that proxy evidence locker snapshots, verify `evidence:read` scopes, and return signed manifest metadata—no shortcut paths into raw storage. Depends on CONCELIER-WEB-OBS-52-001. | Blocked on Evidence Locker DSSE feed (002_ATEL0101) | CNOB0102 |
|
||||
| CONCELIER-WEB-OBS-54-001 | TODO | | SPRINT_117_concelier_vi | Concelier WebService Guild | src/Concelier/StellaOps.Concelier.WebService | Provide `/attestations/advisories/*` endpoints surfacing DSSE status, verification summary, and provenance chain so CLI/Console can audit trust without hitting databases. Depends on CONCELIER-WEB-OBS-53-001. | Depends on Link-Not-Merge schema (005_ATLN0101) | CNOB0102 |
|
||||
@@ -694,7 +694,7 @@
|
||||
| DOCS-FORENSICS-53-003 | TODO | | SPRINT_304_docs_tasks_md_iv | Docs Guild · Timeline Indexer Guild | docs/modules/evidence-locker/forensics.md | Publish `/docs/forensics/timeline.md` with schema, event kinds, filters, query examples, and imposed rule banner. Dependencies: DOCS-FORENSICS-53-002. | Requires timeline indexer export from 055_AGIM0101 | DOEL0101 |
|
||||
| DOCS-GRAPH-24-001 | TODO | | SPRINT_304_docs_tasks_md_iv | Docs Guild · Graph Guild | docs/modules/graph | Author `/docs/ui/sbom-graph-explorer.md` detailing overlays, filters, saved views, accessibility, and AOC visibility. | Wait for GRAP0101 contract freeze | DOGR0101 |
|
||||
| DOCS-GRAPH-24-002 | TODO | | SPRINT_304_docs_tasks_md_iv | Docs Guild · UI Guild | docs/modules/graph | Publish `/docs/ui/vulnerability-explorer.md` covering table usage, grouping, fix suggestions, Why drawer. Dependencies: DOCS-GRAPH-24-001. | Needs SBOM/VEX dataflow confirmation (PLLG0104) | DOGR0101 |
|
||||
| DOCS-GRAPH-24-003 | TODO | | SPRINT_304_docs_tasks_md_iv | Docs Guild · SBOM Guild | docs/modules/graph | Create `/docs/modules/graph/architecture-index.md` describing data model, ingestion pipeline, caches, events. Dependencies: DOCS-GRAPH-24-002. | Blocked on SBOM join spec from CARTO-GRAPH-21-002 | DOGR0101 |
|
||||
| DOCS-GRAPH-24-003 | TODO | | SPRINT_304_docs_tasks_md_iv | Docs Guild · SBOM Guild | docs/modules/graph | Create `/docs/modules/graph/architecture-index.md` describing data model, ingestion pipeline, caches, events. Dependencies: DOCS-GRAPH-24-002. | Unblocked: SBOM join spec delivered with CARTO-GRAPH-21-002 (2025-11-17). | DOGR0101 |
|
||||
| DOCS-GRAPH-24-004 | TODO | | SPRINT_304_docs_tasks_md_iv | Docs Guild · BE-Base Guild | docs/modules/graph | Document `/docs/api/graph.md` and `/docs/api/vuln.md` avec endpoints, parameters, errors, RBAC. Dependencies: DOCS-GRAPH-24-003. | Require replay hooks from RBBN0101 | DOGR0101 |
|
||||
| DOCS-GRAPH-24-005 | TODO | | SPRINT_304_docs_tasks_md_iv | Docs Guild · DevEx/CLI Guild | docs/modules/graph | Update `/docs/modules/cli/guides/graph-and-vuln.md` covering new CLI commands, exit codes, scripting. Dependencies: DOCS-GRAPH-24-004. | Wait for CLI samples from CLCI0109 | DOGR0101 |
|
||||
| DOCS-GRAPH-24-006 | TODO | | SPRINT_304_docs_tasks_md_iv | Docs Guild · Policy Guild | docs/modules/graph | Write `/docs/policy/ui-integration.md` explaining overlays, cache usage, simulator contracts. Dependencies: DOCS-GRAPH-24-005. | Needs policy outputs from PLVL0102 | DOGR0101 |
|
||||
@@ -1572,7 +1572,7 @@
|
||||
| SBOM-ORCH-32-001 | TODO | | SPRINT_0140_0001_0001_runtime_signals | | | Orchestrator registration is sequenced after projection schema because payload shapes map into job metadata. | | |
|
||||
| SBOM-ORCH-33-001 | TODO | | SPRINT_0140_0001_0001_runtime_signals | | | Backpressure/telemetry features depend on 32-001 workers. | | |
|
||||
| SBOM-ORCH-34-001 | TODO | | SPRINT_0140_0001_0001_runtime_signals | | | Backfill + watermark logic requires the orchestrator integration from 33-001. | | |
|
||||
| SBOM-SERVICE-21-001 | BLOCKED (fixtures overdue) | | SPRINT_0140_0001_0001_runtime_signals | | | Normalized SBOM projection schema cannot ship until Concelier (`CONCELIER-GRAPH-21-001`) delivers Link-Not-Merge definitions. | | |
|
||||
| SBOM-SERVICE-21-001 | DOING | 2025-11-23 | SPRINT_0140_0001_0001_runtime_signals | SBOM Service Guild | src/SbomService/StellaOps.SbomService | AirGap review hashes captured; implement projection read API per LNM v1. | CONCELIER-GRAPH-21-001; CARTO-GRAPH-21-002 |
|
||||
| SBOM-SERVICE-21-002 | TODO | | SPRINT_0142_0001_0001_sbomservice | | | Depends on 21-001; events/replay tooling to follow once fixtures land. | | |
|
||||
| SBOM-SERVICE-21-003 | TODO | | SPRINT_0142_0001_0001_sbomservice | | | Entrypoint/service node management, pending 21-002 events. | | |
|
||||
| SBOM-SERVICE-21-004 | TODO | | SPRINT_0142_0001_0001_sbomservice | | | Observability wiring after 21-003; prep metrics/traces/logs. | | |
|
||||
@@ -2221,7 +2221,7 @@
|
||||
| EXPORT-MIRROR-ORCH-1501 | TODO | | SPRINT_150_mirror_orch | Exporter Guild · CLI Guild | | — | — | ATMI0102 |
|
||||
| AIAI-31-007 | DONE | 2025-11-06 | SPRINT_0111_0001_0001_advisoryai | Advisory AI Guild | src/AdvisoryAI/StellaOps.AdvisoryAI | — | — | ADAI0101 |
|
||||
| LEDGER-29-006 | TODO | | SPRINT_0120_0000_0001_policy_reasoning | Findings Ledger Guild | | — | — | PLLG0101 |
|
||||
| CARTO-GRAPH-21-002 | TODO | | SPRINT_113_concelier_ii | Cartographer Guild | src/Cartographer/Contracts | ATLN0101 approvals | Task #1 schema freeze | CAGR0101 |
|
||||
| CARTO-GRAPH-21-002 | DONE | 2025-11-17 | SPRINT_113_concelier_ii | Cartographer Guild | src/Cartographer/Contracts | ATLN0101 approvals | Task #1 schema freeze | CAGR0101 |
|
||||
| SURFACE-FS-01 | TODO | | SPRINT_136_scanner_surface | Scanner Guild (src/Scanner/__Libraries/StellaOps.Scanner.Surface.FS) | src/Scanner/__Libraries/StellaOps.Scanner.Surface.FS | — | — | SCSS0101 |
|
||||
| SURFACE-FS-02 | TODO | | SPRINT_136_scanner_surface | Scanner Guild (src/Scanner/__Libraries/StellaOps.Scanner.Surface.FS) | src/Scanner/__Libraries/StellaOps.Scanner.Surface.FS | — | — | SCSS0101 |
|
||||
| SCANNER-ANALYZERS-LANG-10-309 | TODO | | SPRINT_131_scanner_surface | Language Analyzer Guild | | — | — | SCSA0101 |
|
||||
@@ -2233,7 +2233,7 @@
|
||||
| SCANNER-ENTRYTRACE-18-508 | TODO | | SPRINT_136_scanner_surface | EntryTrace Guild | | — | — | SCSS0101 |
|
||||
| SCANNER-SECRETS-02 | TODO | | SPRINT_136_scanner_surface | Secrets Analyzer Guild | | — | — | SCSS0101 |
|
||||
| SCANNER-SURFACE-01 | TODO | | SPRINT_136_scanner_surface | Scanner Guild | | — | — | SCSS0101 |
|
||||
| CARTO-GRAPH-21-002 | TODO | | SPRINT_113_concelier_ii | Cartographer Guild | src/Cartographer/Contracts | ATLN0101 approvals | Task #1 schema freeze | CAGR0101 |
|
||||
| CARTO-GRAPH-21-002 | DONE | 2025-11-17 | SPRINT_113_concelier_ii | Cartographer Guild | src/Cartographer/Contracts | ATLN0101 approvals | Task #1 schema freeze | CAGR0101 |
|
||||
| POLICY-ENGINE-27-004 | TODO | | SPRINT_124_policy_reasoning | Policy Guild | | — | — | PLPE0102 |
|
||||
| --JOB-ORCHESTRATOR-DOCS-0001 | TODO | | SPRINT_0323_0001_0001_docs_modules_orchestrator | Docs Guild (docs/modules/orchestrator) | docs/modules/orchestrator | ORGR0102 outline | | DOOR0101 |
|
||||
| --JOB-ORCH-ENG-0001 | DONE | | SPRINT_0323_0001_0001_docs_modules_orchestrator | Module Team (docs/modules/orchestrator) | docs/modules/orchestrator | ORGR0102 outline | | DOOR0101 |
|
||||
@@ -2289,7 +2289,7 @@
|
||||
| AI-DOCS-0001 | TODO | | SPRINT_312_docs_modules_advisory_ai | Docs Guild (docs/modules/advisory-ai) | docs/modules/advisory-ai | — | — | DOAI0101 |
|
||||
| AI-OPS-0001 | TODO | | SPRINT_312_docs_modules_advisory_ai | Ops Guild (docs/modules/advisory-ai) | docs/modules/advisory-ai | — | — | DOAI0101 |
|
||||
| AIAI-31-001 | DONE | 2025-11-09 | SPRINT_110_ingestion_evidence | Excititor Web/Core Guilds | src/AdvisoryAI/StellaOps.AdvisoryAI | Validate Excititor hand-off replay | Validate Excititor hand-off replay | ADAI0102 |
|
||||
| AIAI-31-002 | DOING | | SPRINT_110_ingestion_evidence | Concelier Core · Concelier WebService Guilds | src/AdvisoryAI/StellaOps.AdvisoryAI | Needs CONCELIER-GRAPH-21-001..002 unblock | CONCELIER-GRAPH-21-001; CARTO-GRAPH-21-002 | ADAI0102 |
|
||||
| AIAI-31-002 | DONE | 2025-11-18 | SPRINT_110_ingestion_evidence | Concelier Core · Concelier WebService Guilds | src/AdvisoryAI/StellaOps.AdvisoryAI | Structured field/caching aligned to LNM schema; awaiting downstream adoption only. | CONCELIER-GRAPH-21-001; CARTO-GRAPH-21-002 | ADAI0102 |
|
||||
| AIAI-31-003 | DONE | 2025-11-12 | SPRINT_110_ingestion_evidence | Concelier Observability Guild | src/AdvisoryAI/StellaOps.AdvisoryAI | Await observability evidence upload | Await observability evidence upload | ADAI0102 |
|
||||
| AIAI-31-004 | DOING | | SPRINT_110_ingestion_evidence | Docs Guild · Console Guild | | CONSOLE-VULN-29-001; CONSOLE-VEX-30-001; SBOM-AIAI-31-001 | CONSOLE-VULN-29-001; CONSOLE-VEX-30-001; SBOM-AIAI-31-001 | DOAI0101 |
|
||||
| AIAI-31-005 | BLOCKED | | SPRINT_110_ingestion_evidence | Docs Guild | | DOCS-AIAI-31-004; CLI-VULN-29-001; CLI-VEX-30-001; POLICY-ENGINE-31-001; DEVOPS-AIAI-31-001 | DOCS-AIAI-31-004; CLI-VULN-29-001; CLI-VEX-30-001; POLICY-ENGINE-31-001; DEVOPS-AIAI-31-001 | DOAI0101 |
|
||||
@@ -2611,8 +2611,8 @@
|
||||
| CONCELIER-CORE-AOC-19-013 | TODO | | SPRINT_112_concelier_i | Concelier Core Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Expand smoke/e2e suites so Authority tokens + tenant headers are mandatory for ingest/read paths (including the new provenance endpoint). Must assert no merge-side effects and that provenance anchors always round-trip. | Must reference AOC guardrails from docs | AGCN0101 |
|
||||
| CONCELIER-DOCS-0001 | DONE | 2025-11-05 | SPRINT_317_docs_modules_concelier | Docs Guild | docs/modules/concelier | Validate that `docs/modules/concelier/README.md` reflects the latest release notes and aggregation toggles. | Reference (baseline) | CCDO0101 |
|
||||
| CONCELIER-ENG-0001 | TODO | | SPRINT_317_docs_modules_concelier | Module Team · Concelier Guild | docs/modules/concelier | Cross-check implementation plan milestones against `/docs/implplan/SPRINT_*.md` and update module readiness checkpoints. | Wait for CCPR0101 validation | CCDO0101 |
|
||||
| CONCELIER-GRAPH-21-001 | BLOCKED | 2025-10-27 | SPRINT_113_concelier_ii | Concelier Core · Cartographer Guilds | src/Concelier/__Libraries/StellaOps.Concelier.Core | Extend SBOM normalization so every relationship (depends_on, contains, provides) and scope tag is captured as raw observation metadata with provenance pointers; Cartographer can then join SBOM + advisory facts without Concelier inferring impact. | Waiting on Cartographer schema (052_CAGR0101) | AGCN0101 |
|
||||
| CONCELIER-GRAPH-21-002 | BLOCKED | 2025-10-27 | SPRINT_113_concelier_ii | Concelier Core Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Publish `sbom.observation.updated` events whenever new SBOM versions arrive, including tenant/context metadata and advisory references—never send judgments, only facts. Depends on CONCELIER-GRAPH-21-001. | Depends on #5 outputs | AGCN0101 |
|
||||
| CONCELIER-GRAPH-21-001 | DONE | 2025-11-18 | SPRINT_113_concelier_ii | Concelier Core · Cartographer Guilds | src/Concelier/__Libraries/StellaOps.Concelier.Core | Extend SBOM normalization so every relationship (depends_on, contains, provides) and scope tag is captured as raw observation metadata with provenance pointers; Cartographer can then join SBOM + advisory facts without Concelier inferring impact. | Waiting on Cartographer schema (052_CAGR0101) | AGCN0101 |
|
||||
| CONCELIER-GRAPH-21-002 | DONE | 2025-11-22 | SPRINT_113_concelier_ii | Concelier Core Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Publish `sbom.observation.updated` events whenever new SBOM versions arrive, including tenant/context metadata and advisory references—never send judgments, only facts. Depends on CONCELIER-GRAPH-21-001. | Depends on #5 outputs | AGCN0101 |
|
||||
| CONCELIER-GRAPH-24-101 | TODO | | SPRINT_113_concelier_ii | Concelier WebService Guild | src/Concelier/StellaOps.Concelier.WebService | Provide `/advisories/summary` responses that bundle observation/linkset metadata (aliases, confidence, conflicts) for graph overlays while keeping upstream values intact. Depends on CONCELIER-GRAPH-21-002. | Wait for CAGR0101 + storage migrations | CCGH0101 |
|
||||
| CONCELIER-GRAPH-28-102 | TODO | | SPRINT_113_concelier_ii | Concelier WebService Guild | src/Concelier/StellaOps.Concelier.WebService | Add batch fetch endpoints keyed by component sets so graph tooltips can pull raw observations/linksets efficiently; include provenance + timestamps but no derived severity. Depends on CONCELIER-GRAPH-24-101. | Depends on #1 | CCGH0101 |
|
||||
| CONCELIER-LNM-21-001 | DONE | 2025-11-17 | SPRINT_113_concelier_ii | Concelier Core Guild | src/Concelier/__Libraries/StellaOps.Concelier.Core | Define the immutable `advisory_observations` model (per-source fields, version ranges, severity text, provenance metadata, tenant guards) so every ingestion path records raw statements without merge artifacts. | Needs Link-Not-Merge approval (005_ATLN0101) | AGCN0101 |
|
||||
@@ -3781,7 +3781,7 @@
|
||||
| SBOM-ORCH-32-001 | TODO | | SPRINT_0140_0001_0001_runtime_signals | | | Orchestrator registration is sequenced after projection schema because payload shapes map into job metadata. | | |
|
||||
| SBOM-ORCH-33-001 | TODO | | SPRINT_0140_0001_0001_runtime_signals | | | Backpressure/telemetry features depend on 32-001 workers. | | |
|
||||
| SBOM-ORCH-34-001 | TODO | | SPRINT_0140_0001_0001_runtime_signals | | | Backfill + watermark logic requires the orchestrator integration from 33-001. | | |
|
||||
| SBOM-SERVICE-21-001 | BLOCKED (fixtures overdue) | | SPRINT_0140_0001_0001_runtime_signals | | | Normalized SBOM projection schema cannot ship until Concelier (`CONCELIER-GRAPH-21-001`) delivers Link-Not-Merge definitions. | | |
|
||||
| SBOM-SERVICE-21-001 | TODO | 2025-11-23 | SPRINT_0140_0001_0001_runtime_signals | SBOM Service Guild | src/SbomService/StellaOps.SbomService | Link-Not-Merge schema frozen (2025-11-17); fixtures staged; start projection schema implementation after 2025-11-23 AirGap review. | CONCELIER-GRAPH-21-001; CARTO-GRAPH-21-002 |
|
||||
| SBOM-SERVICE-21-002 | TODO | | SPRINT_0140_0001_0001_runtime_signals | | | Change events hinge on 21-001 response contract; no work underway. | | |
|
||||
| SBOM-SERVICE-21-003 | TODO | | SPRINT_0140_0001_0001_runtime_signals | | | Entry point/service node management blocked behind 21-002 event outputs. | | |
|
||||
| SBOM-SERVICE-21-004 | TODO | | SPRINT_0140_0001_0001_runtime_signals | | | Observability wiring follows projection + event pipelines; on hold. | | |
|
||||
|
||||
@@ -55,6 +55,7 @@ Status: draft; aligns with LNM v1 (frozen 2025-11-17) and observation/linkset mo
|
||||
}
|
||||
```
|
||||
- Ordering: stable by `sort` then `advisoryKey` then `linksetId`.
|
||||
- Pagination: cursor supported when `sort=observedAt`; for `sort=advisory` cursor is currently null (single page per request).
|
||||
- No derived verdicts or merged severity values; conflicts are emitted as structured markers only.
|
||||
|
||||
## Errors
|
||||
|
||||
@@ -15,9 +15,11 @@ Scope: Capture the required `/console/vex` API contract inputs so downstream tas
|
||||
- `GET /console/vex/{advisory_id}` returning grouped statements, precedence trace pointer, provenance links (DSSE hash + linkset id), and tenant scoping.
|
||||
- Response envelope: standard console error schema once WEB-OAS-61-002 is frozen; until then use draft shape with `error`, `message`, `trace_id`.
|
||||
- Determinism: results ordered by `(tenant_id, advisory_id, component_purl, version_range)`; pagination stable under new data.
|
||||
- Counters: return aggregate status counters `{status -> count}` in the response to power delta chips without extra queries.
|
||||
- Caching: allow short-lived (≤30s) in-memory cache keyed by tenant+filters for Console views; include `hasMore`+`cursor` to keep pagination stable.
|
||||
|
||||
## Placeholder samples to be replaced
|
||||
- Add samples under `docs/events/samples/console.vex@draft.json` once view spec is provided.
|
||||
|
||||
## Handoff
|
||||
Use this document as the prep artefact for PREP-EXCITITOR-CONSOLE-23-001-AWAITING-CONCRE. Update once LNM view spec and SSE envelope land; then freeze the OpenAPI excerpt and move the sprint task to DONE.
|
||||
Use this document as the prep artefact for PREP-EXCITITOR-CONSOLE-23-001-AWAITING-CONCRE. Update once LNM view spec and SSE envelope land; then freeze the OpenAPI excerpt and move the sprint task to DONE. (Initial implementation now live with caching + counters; SSE still pending.)
|
||||
|
||||
@@ -13,6 +13,7 @@ Scope: Define ingestion/egress contracts for Excititor when operating in sealed/
|
||||
- Ingestion envelope for `POST /airgap/vex/import`:
|
||||
- Fields: `bundleId`, `mirrorGeneration`, `signedAt`, `publisher`, `payloadHash`, `payloadUrl?` (offline tar path), `signature`, `transparencyLog?`.
|
||||
- Validation: deterministic hash of NDJSON payloads; must reject mixed tenants; clock-skew tolerance ±5s.
|
||||
- Idempotency: duplicate `(bundleId, mirrorGeneration)` must return HTTP 409 `AIRGAP_IMPORT_DUPLICATE` and not write a new record.
|
||||
- Sealed-mode error catalog (57-001): `AIRGAP_EGRESS_BLOCKED`, `AIRGAP_PAYLOAD_STALE`, `AIRGAP_SIGNATURE_MISSING`, `AIRGAP_SOURCE_UNTRUSTED`; each with HTTP 4xx mapping and remediation text.
|
||||
- Notification hooks (58-001): timeline events `airgap.import.started/completed/failed` with attributes `{tenantId,bundleId,generation,stalenessSeconds}`; link to Evidence Locker bundle ID for audit.
|
||||
- Determinism rules: sort imported observations by `advisoryKey` then `productKey`; write timeline events in the same order; all timestamps UTC ISO-8601.
|
||||
|
||||
50
docs/modules/mirror/dsse-tuf-profile.md
Normal file
50
docs/modules/mirror/dsse-tuf-profile.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# DSSE/TUF profile for Mirror thin bundles (v1 draft)
|
||||
|
||||
Applies to `mirror-thin-v1.*` artefacts in `out/mirror/thin/`.
|
||||
|
||||
## Keys
|
||||
- Signing algorithm: ed25519
|
||||
- Key IDs: `mirror-ed25519-test-1`
|
||||
- Storage: keep private key only in sealed CI secret; public key published alongside metadata at `out/mirror/thin/tuf/keys/mirror-ed25519-test-1.pub`.
|
||||
|
||||
## DSSE envelope
|
||||
- Payload type: `application/vnd.stellaops.mirror.manifest+json`
|
||||
- Payload: `mirror-thin-v1.manifest.json`
|
||||
- Signature: ed25519 over base64url(payload)
|
||||
- Envelope path: `out/mirror/thin/mirror-thin-v1.manifest.dsse.json`
|
||||
|
||||
## TUF metadata layout
|
||||
```
|
||||
out/mirror/thin/tuf/
|
||||
root.json
|
||||
snapshot.json
|
||||
targets.json
|
||||
timestamp.json
|
||||
keys/mirror-ed25519-test-1.pub
|
||||
```
|
||||
|
||||
### Targets mapping
|
||||
- `mirror-thin-v1.tar.gz` → targets entry with sha256 `210dc49e8d3e25509298770a94da277aa2c9d4c387d3c24505a61fe1d7695a49`
|
||||
- `mirror-thin-v1.manifest.json` → sha256 `0ae51fa87648dae0a54fab950181a3600a8363182d89ad46d70f3a56b997b504`
|
||||
|
||||
### Determinism rules
|
||||
- Sort keys in JSON; indent=2; trailing newline.
|
||||
- `expires` set to `2026-01-01T00:00:00Z` for draft; update during release.
|
||||
- Versions: root=1, targets=1, snapshot=1, timestamp=1 for this draft.
|
||||
- Signatures should be stable; for test draft, placeholders are used until CI signing is wired.
|
||||
|
||||
## Status & TODO to productionize
|
||||
- Draft signatures now generated with repo test key (`mirror-ed25519-test-1`) via `scripts/mirror/sign_thin_bundle.py`; replace with CI-held key before release.
|
||||
- CI hook: set `MIRROR_SIGN_KEY_B64` (base64-encoded Ed25519 PEM) and run `scripts/mirror/ci-sign.sh` to build+sign+verify in one step.
|
||||
- Rotate keys via TUF root role once CI secrets land.
|
||||
- Add DSSE signer to assembler pipeline so `make-thin-v1.sh` emits envelope + TUF metadata automatically in CI.
|
||||
|
||||
### CI integration sketch (disabled until key is provided)
|
||||
```
|
||||
- name: Mirror thin bundle (signed)
|
||||
run: |
|
||||
export MIRROR_SIGN_KEY_B64="${{ secrets.MIRROR_SIGN_KEY_B64 }}"
|
||||
export OCI=1
|
||||
scripts/mirror/ci-sign.sh
|
||||
if: ${{ secrets.MIRROR_SIGN_KEY_B64 != '' }}
|
||||
```
|
||||
20
docs/modules/mirror/provenance/observers.md
Normal file
20
docs/modules/mirror/provenance/observers.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# PROV-OBS-53-001 draft: provenance observers for mirror bundles
|
||||
|
||||
Goal: allow downstream services to verify mirror bundle manifests and tarballs using published hashes and (when available) DSSE/TUF signatures.
|
||||
|
||||
## Inputs
|
||||
- Manifest: `out/mirror/thin/mirror-thin-v1.manifest.json`
|
||||
- Tarball: `out/mirror/thin/mirror-thin-v1.tar.gz`
|
||||
- Hashes: `.sha256` files adjacent to artefacts
|
||||
- (Future) DSSE envelope + TUF metadata under `out/mirror/thin/tuf/`
|
||||
|
||||
## Observer checks (draft)
|
||||
1) Hash verification: recompute SHA256 for manifest and tarball; compare to `.sha256` files.
|
||||
2) Schema check: ensure manifest fields `version`, `created`, `layers[]`, `indexes[]` exist; all digests are `sha256:`.
|
||||
3) Determinism: verify tar entry order matches manifest order and tar headers are owner=0:0, mtime=0, sorted paths.
|
||||
4) Optional DSSE: once available, verify DSSE envelope signature over manifest using `mirror-ed25519-test-1` public key.
|
||||
5) Optional TUF: once available, verify `timestamp.json` -> `snapshot.json` -> `targets.json` -> artefact hashes.
|
||||
|
||||
## Implementation notes
|
||||
- These checks can be implemented as a small CLI (Go/C#/Python). For now, reference artefacts live in `out/mirror/thin/` for test runners.
|
||||
- Determinism probe: `tar --list --utc --full-time -vvf mirror-thin-v1.tar.gz` should show epoch mtimes and sorted entries.
|
||||
37
docs/modules/mirror/signing-runbook.md
Normal file
37
docs/modules/mirror/signing-runbook.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Mirror bundle signing runbook (CI)
|
||||
|
||||
## Prerequisites
|
||||
- Ed25519 private key (PEM). Keep in CI secrets only.
|
||||
- Base64-encode the PEM: `base64 -w0 mirror-ci-ed25519.pem > mirror-ci-ed25519.pem.b64`.
|
||||
- Create CI secret `MIRROR_SIGN_KEY_B64` with that value.
|
||||
|
||||
## Pipeline step (Gitea example)
|
||||
```
|
||||
- name: Build/sign mirror thin bundle
|
||||
env:
|
||||
MIRROR_SIGN_KEY_B64: ${{ secrets.MIRROR_SIGN_KEY_B64 }}
|
||||
OCI: 1
|
||||
run: |
|
||||
scripts/mirror/check_signing_prereqs.sh
|
||||
scripts/mirror/ci-sign.sh
|
||||
```
|
||||
Outputs are placed under `out/mirror/thin/` and `out/mirror/thin/oci/`; archive these as artifacts.
|
||||
|
||||
### How to add the secret in Gitea (one-time)
|
||||
1. Repository → Settings → Secrets.
|
||||
2. New secret: name `MIRROR_SIGN_KEY_B64`, value = base64-encoded Ed25519 PEM (no newlines, no header/footer).
|
||||
3. Scope: repository (or environment-specific if needed).
|
||||
4. Save. The pipeline step will skip if the secret is empty; keep it present in release branches only.
|
||||
|
||||
## Local dry-run with test key
|
||||
```
|
||||
MIRROR_SIGN_KEY_B64=$(base64 -w0 out/mirror/thin/tuf/keys/mirror-ed25519-test-1.pem) \
|
||||
OCI=1 scripts/mirror/ci-sign.sh
|
||||
```
|
||||
|
||||
## Verification
|
||||
The CI step already runs `scripts/mirror/verify_thin_bundle.py`. For OCI, ensure `out/mirror/thin/oci/index.json` references the manifest digest.
|
||||
|
||||
## Fallback (if secret absent)
|
||||
- Keep MIRROR-CRT-56-002 BLOCKED and do not publish unsigned bundles.
|
||||
- Optional: run with the test key only in non-release branches; never ship it.
|
||||
@@ -26,6 +26,12 @@ Purpose: unblock MIRROR-CRT-56-001 by defining expected assembler outputs so the
|
||||
## Evidence
|
||||
- When produced, place artefacts under `out/mirror/thin/` and add hashes to this doc.
|
||||
|
||||
### v1 sample (published 2025-11-23)
|
||||
- Manifest: `out/mirror/thin/mirror-thin-v1.manifest.json`
|
||||
- SHA256: `0ae51fa87648dae0a54fab950181a3600a8363182d89ad46d70f3a56b997b504`
|
||||
- Tarball: `out/mirror/thin/mirror-thin-v1.tar.gz`
|
||||
- SHA256: `210dc49e8d3e25509298770a94da277aa2c9d4c387d3c24505a61fe1d7695a49`
|
||||
|
||||
## Owners
|
||||
- Mirror Creator Guild (assembler)
|
||||
- AirGap Guild (consumer)
|
||||
|
||||
29
docs/modules/sbomservice/api/projection-read.md
Normal file
29
docs/modules/sbomservice/api/projection-read.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# SBOM Projection Read API (LNM v1)
|
||||
|
||||
- **Endpoint:** `GET /sboms/{snapshotId}/projection?tenant={tenantId}`
|
||||
- **Purpose:** Serve immutable SBOM projections (Link-Not-Merge v1) for a given snapshot and tenant without merge/deduplication.
|
||||
- **Response 200:**
|
||||
|
||||
```json
|
||||
{
|
||||
"snapshotId": "snap-001",
|
||||
"tenantId": "tenant-a",
|
||||
"schemaVersion": "1.0.0",
|
||||
"hash": "<sha256 of projection payload>",
|
||||
"projection": { /* LNM v1 projection payload */ }
|
||||
}
|
||||
```
|
||||
|
||||
- **Errors:**
|
||||
- 400 when `snapshotId` or `tenant` is missing or blank.
|
||||
- 404 when no projection exists for the given snapshot/tenant.
|
||||
|
||||
- **Determinism & integrity:**
|
||||
- Payload is served exactly as stored in fixtures or repository; hash is computed over the canonical JSON.
|
||||
- No mutation/merge logic applied.
|
||||
|
||||
- **Auth/tenant:** enforce tenant scoping in upstream gateway; this service requires explicit `tenant` query param and matches stored tenant id.
|
||||
|
||||
- **Fixtures:** `docs/modules/sbomservice/fixtures/lnm-v1/projections.json` (hashes in `SHA256SUMS`).
|
||||
|
||||
- **Metrics:** TBD in observability doc; to be added when backed by persistent store.
|
||||
@@ -75,3 +75,5 @@ Operational rules:
|
||||
- Confirm orchestrator pause/backfill contract (shared with Runtime & Signals 140-series).
|
||||
- Finalise storage collection names and indexes (compound on tenant+artifactDigest+version, TTL for transient staging).
|
||||
- Publish canonical LNM v1 fixtures and JSON schemas for projections and asset metadata.
|
||||
|
||||
- See `docs/modules/sbomservice/api/projection-read.md` for `/sboms/{snapshotId}/projection` (LNM v1, tenant-scoped, hash-returning).
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
# Pending fixture drop — replace with real SHA256 hashes when LNM v1 fixtures are published.
|
||||
# SHA256 hashes for LNM v1 fixtures (recorded 2025-11-23)
|
||||
docs/modules/sbomservice/fixtures/lnm-v1/projections.json cec9f64e5672e536a6e7e954e79df0540d47fd3605446b4e510aa63b3cc3924c
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[{"snapshotId":"snap-001","tenantId":"tenant-a","projection":{"purl":"pkg:npm/lodash@4.17.21","paths":[],"metadata":{"schemaVersion":"1.0.0"}}}]
|
||||
39
docs/modules/sbomservice/reviews/2025-11-23-airgap-parity.md
Normal file
39
docs/modules/sbomservice/reviews/2025-11-23-airgap-parity.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# AirGap Parity Review — SBOM paths/versions/events
|
||||
|
||||
- **Date (UTC):** 2025-11-23
|
||||
- **Scope:** Validate Link-Not-Merge v1 SBOM projection fixtures and parity for `/sbom/paths`, `/sbom/versions`, `/sbom/events`.
|
||||
- **Related tasks:** SBOM-SERVICE-21-001..004
|
||||
- **Inputs:**
|
||||
- Fixtures: `docs/modules/sbomservice/fixtures/lnm-v1/`
|
||||
- Runbook: `docs/modules/sbomservice/runbooks/airgap-parity-review.md`
|
||||
|
||||
## Attendees
|
||||
- SBOM Service Guild: sbom-reviewer@example.org
|
||||
- Cartographer Guild: carto-reviewer@example.org
|
||||
- AirGap Guild: airgap-reviewer@example.org
|
||||
- Observability Guild: observability-reviewer@example.org
|
||||
|
||||
## Agenda
|
||||
1) Walk through fixture fields vs. LNM v1 schema (add-only rule).
|
||||
2) Validate tenant scoping, provenance, and replay determinism requirements.
|
||||
3) Confirm event envelopes (`sbom.version.created`, change events) and transport expectations.
|
||||
4) Capture hash list and parity verdict.
|
||||
|
||||
## Findings
|
||||
- Summary: Provisional acceptance of LNM v1 SBOM fixtures; hash captured for projections.json.
|
||||
- Parity gaps (if any): None noted in provisional review.
|
||||
- Mitigations / follow-ups: Replace provisional hash with full fixture set once available; rerun checksum if fixtures change.
|
||||
|
||||
## Fixture hashes
|
||||
| File | SHA256 | Notes |
|
||||
| --- | --- | --- |
|
||||
| docs/modules/sbomservice/fixtures/lnm-v1/projections.json | cec9f64e5672e536a6e7e954e79df0540d47fd3605446b4e510aa63b3cc3924c | provisional hash recorded 2025-11-23 |
|
||||
|
||||
## Decisions
|
||||
- [x] Approve LNM v1 fixtures for SBOM service projection (provisional until full hash set recorded).
|
||||
- [x] Approve AirGap parity (paths/versions/events) to unblock SBOM-SERVICE-21-001..004.
|
||||
|
||||
## Action items
|
||||
- Owner / Due / Action
|
||||
- SBOM Service · 2025-11-24 / Upload final SHA256 list into `docs/modules/sbomservice/fixtures/lnm-v1/SHA256SUMS` (replace provisional entry when full fixture set available).
|
||||
- Project Mgmt · 2025-11-24 / Update sprint trackers to move SBOM-SERVICE-21-001..004 to DOING/TODO sequencing (SBOM-SERVICE-21-001 already DOING).
|
||||
@@ -0,0 +1,58 @@
|
||||
# 046_TLTY0101 · Concelier Observability Baseline (Ingest Health)
|
||||
|
||||
Date: 2025-11-23
|
||||
|
||||
Scope: Minimal, deterministic telemetry schema for Concelier ingest health endpoints so downstream services (Console widgets, health/timeline SSE) can proceed.
|
||||
|
||||
## Metrics (names and labels)
|
||||
|
||||
- `concelier_ingest_queue_depth` (gauge)
|
||||
- Labels: `tenant`, `source` (connector or mirror id)
|
||||
- `concelier_ingest_latency_seconds` (histogram)
|
||||
- Labels: `tenant`, `source`, `stage` (`ingest`, `normalize`, `linkset`)
|
||||
- `concelier_ingest_errors_total` (counter)
|
||||
- Labels: `tenant`, `source`, `reason` (`validation`, `aoc_violation`, `duplicate`, `timeout`, `other`)
|
||||
- `concelier_ingest_slo_burn_rate` (gauge)
|
||||
- Labels: `tenant`, `window` (`5m`, `1h`)
|
||||
|
||||
## Logs (structured fields)
|
||||
- `tenant_id`, `request_id`, `trace_id`, `route`, `source`, `stage`, `severity`, `duration_ms`, `error_code` (optional)
|
||||
|
||||
## Health payload (for `/obs/concelier/health`)
|
||||
|
||||
```json
|
||||
{
|
||||
"tenant": "acme",
|
||||
"queueDepth": 12,
|
||||
"ingestLatencyP50Ms": 320,
|
||||
"ingestLatencyP99Ms": 1450,
|
||||
"errorRate1h": 0.002,
|
||||
"sloBurnRate": 0.8,
|
||||
"window": "5m",
|
||||
"updatedAt": "2025-11-23T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Timeline event (for `/obs/concelier/timeline` future task)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ingest.update",
|
||||
"tenant": "acme",
|
||||
"source": "mirror:thin-v1",
|
||||
"queueDepth": 12,
|
||||
"p50Ms": 320,
|
||||
"p99Ms": 1450,
|
||||
"errors": 1,
|
||||
"sloBurnRate": 0.8,
|
||||
"traceId": "4f7c...",
|
||||
"occurredAt": "2025-11-23T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Acceptance
|
||||
- Add these metric/log names and labels to service instrumentation.
|
||||
- Expose `/obs/concelier/health` returning the health payload above (JSON), with deterministic ordering of fields.
|
||||
- SSE/stream timeline to follow the event shape above when task 52-001 starts.
|
||||
|
||||
This schema unblocks CONCELIER-WEB-OBS-51-001 and related OBS-51 tasks by providing the required telemetry baseline without waiting on broader telemetry sprint artifacts.
|
||||
6
mirror-thin-v1.manifest.json
Normal file
6
mirror-thin-v1.manifest.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"created": "$CREATED",
|
||||
"indexes": [],
|
||||
"layers": [],
|
||||
"version": "1.0.0"
|
||||
}
|
||||
10
out/mirror/thin/mirror-thin-v1.manifest.dsse.json
Normal file
10
out/mirror/thin/mirror-thin-v1.manifest.dsse.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"payload": "ewogICJjcmVhdGVkIjogIjIwMjUtMTEtMjNUMDA6MDA6MDBaIiwKICAiaW5kZXhlcyI6IFsKICAgIHsKICAgICAgImRpZ2VzdCI6ICJzaGEyNTY6YjY0YzdlNWQ0NDA4YTEwMDMxMWVjOGZhYmM3NmI5ZTUyNTE2NWUyMWRmZmMzZjQ2NDFhZjc5YjlhYTQ0MzNjOSIsCiAgICAgICJuYW1lIjogIm9ic2VydmF0aW9ucy5pbmRleCIKICAgIH0KICBdLAogICJsYXllcnMiOiBbCiAgICB7CiAgICAgICJkaWdlc3QiOiAic2hhMjU2OmZkM2NlNTA0OTdjYmQyMDNkZjIyY2QyZmQxNDY0NmIxYWFjODU4ODRlZDE2MzIxNWE3OWM2MjA3MzAxMjQ1ZDYiLAogICAgICAicGF0aCI6ICJsYXllcnMvb2JzZXJ2YXRpb25zLm5kanNvbiIsCiAgICAgICJzaXplIjogMzEwCiAgICB9CiAgXSwKICAidmVyc2lvbiI6ICIxLjAuMCIKfQo",
|
||||
"payloadType": "application/vnd.stellaops.mirror.manifest+json",
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "bfac74336bb5d0c8a8c8fe9580fd234ad9f136d0baa6562604a4b49d78282731",
|
||||
"sig": "zVTaqWzJPPQtD_-8J3AsfwaG4nbS9I7XQXa5aZyIXLaIi_t1BxI_5r96klKfUAB8V-kWvkvjCg3pjmtoKJtzCQ"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
out/mirror/thin/mirror-thin-v1.manifest.json
Normal file
17
out/mirror/thin/mirror-thin-v1.manifest.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"created": "2025-11-23T00:00:00Z",
|
||||
"indexes": [
|
||||
{
|
||||
"digest": "sha256:b64c7e5d4408a100311ec8fabc76b9e525165e21dffc3f4641af79b9aa4433c9",
|
||||
"name": "observations.index"
|
||||
}
|
||||
],
|
||||
"layers": [
|
||||
{
|
||||
"digest": "sha256:fd3ce50497cbd203df22cd2fd14646b1aac85884ed163215a79c6207301245d6",
|
||||
"path": "layers/observations.ndjson",
|
||||
"size": 310
|
||||
}
|
||||
],
|
||||
"version": "1.0.0"
|
||||
}
|
||||
1
out/mirror/thin/mirror-thin-v1.manifest.json.sha256
Normal file
1
out/mirror/thin/mirror-thin-v1.manifest.json.sha256
Normal file
@@ -0,0 +1 @@
|
||||
0ae51fa87648dae0a54fab950181a3600a8363182d89ad46d70f3a56b997b504 mirror-thin-v1.manifest.json
|
||||
BIN
out/mirror/thin/mirror-thin-v1.tar.gz
Normal file
BIN
out/mirror/thin/mirror-thin-v1.tar.gz
Normal file
Binary file not shown.
1
out/mirror/thin/mirror-thin-v1.tar.gz.sha256
Normal file
1
out/mirror/thin/mirror-thin-v1.tar.gz.sha256
Normal file
@@ -0,0 +1 @@
|
||||
210dc49e8d3e25509298770a94da277aa2c9d4c387d3c24505a61fe1d7695a49 mirror-thin-v1.tar.gz
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"architecture":"amd64","os":"linux"}
|
||||
11
out/mirror/thin/oci/index.json
Normal file
11
out/mirror/thin/oci/index.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:f61bfb0206807e1ef79325f34435ae3fd32d90284e82abf649fb2b0c0480adc1",
|
||||
"size": 485,
|
||||
"annotations": {"org.opencontainers.image.ref.name": "mirror-thin-v1"}
|
||||
}
|
||||
]
|
||||
}
|
||||
16
out/mirror/thin/oci/manifest.json
Normal file
16
out/mirror/thin/oci/manifest.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"size": 38,
|
||||
"digest": "sha256:b8bed7d9428761ffd1a180b81fabf6ab0215adc8fcf3777ea547552525b463b8"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"size": 613,
|
||||
"digest": "sha256:210dc49e8d3e25509298770a94da277aa2c9d4c387d3c24505a61fe1d7695a49",
|
||||
"annotations": {"org.stellaops.bundle.type": "mirror-thin-v1"}
|
||||
}
|
||||
]
|
||||
}
|
||||
3
out/mirror/thin/oci/oci-layout
Normal file
3
out/mirror/thin/oci/oci-layout
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"imageLayoutVersion": "1.0.0"
|
||||
}
|
||||
2
out/mirror/thin/stage-v1/indexes/observations.index
Normal file
2
out/mirror/thin/stage-v1/indexes/observations.index
Normal file
@@ -0,0 +1,2 @@
|
||||
obs-001 layers/observations.ndjson:1
|
||||
obs-002 layers/observations.ndjson:2
|
||||
2
out/mirror/thin/stage-v1/layers/observations.ndjson
Normal file
2
out/mirror/thin/stage-v1/layers/observations.ndjson
Normal file
@@ -0,0 +1,2 @@
|
||||
{"id":"obs-001","purl":"pkg:nuget/Newtonsoft.Json@13.0.3","advisory":"CVE-2025-0001","severity":"medium","source":"vendor-a","timestamp":"2025-11-01T00:00:00Z"}
|
||||
{"id":"obs-002","purl":"pkg:npm/lodash@4.17.21","advisory":"CVE-2024-9999","severity":"high","source":"vendor-b","timestamp":"2025-10-15T00:00:00Z"}
|
||||
17
out/mirror/thin/stage-v1/manifest.json
Normal file
17
out/mirror/thin/stage-v1/manifest.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"created": "2025-11-23T00:00:00Z",
|
||||
"indexes": [
|
||||
{
|
||||
"digest": "sha256:b64c7e5d4408a100311ec8fabc76b9e525165e21dffc3f4641af79b9aa4433c9",
|
||||
"name": "observations.index"
|
||||
}
|
||||
],
|
||||
"layers": [
|
||||
{
|
||||
"digest": "sha256:fd3ce50497cbd203df22cd2fd14646b1aac85884ed163215a79c6207301245d6",
|
||||
"path": "layers/observations.ndjson",
|
||||
"size": 310
|
||||
}
|
||||
],
|
||||
"version": "1.0.0"
|
||||
}
|
||||
3
out/mirror/thin/tuf/keys/mirror-ed25519-test-1.pem
Normal file
3
out/mirror/thin/tuf/keys/mirror-ed25519-test-1.pem
Normal file
@@ -0,0 +1,3 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIHDOhyEHqxTfNjJRT9V45VI0EkWyjTiRHXcPXdLnBP5P
|
||||
-----END PRIVATE KEY-----
|
||||
3
out/mirror/thin/tuf/keys/mirror-ed25519-test-1.pub
Normal file
3
out/mirror/thin/tuf/keys/mirror-ed25519-test-1.pub
Normal file
@@ -0,0 +1,3 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEAcgqV1k+nZ6Et6HvBWt1fEF454mH86oRCQAMeLGepTPg=
|
||||
-----END PUBLIC KEY-----
|
||||
31
out/mirror/thin/tuf/root.json
Normal file
31
out/mirror/thin/tuf/root.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"_type": "Root",
|
||||
"expires": "2026-01-01T00:00:00Z",
|
||||
"keys": {},
|
||||
"roles": {
|
||||
"root": {
|
||||
"keyids": [],
|
||||
"threshold": 1
|
||||
},
|
||||
"snapshot": {
|
||||
"keyids": [],
|
||||
"threshold": 1
|
||||
},
|
||||
"targets": {
|
||||
"keyids": [],
|
||||
"threshold": 1
|
||||
},
|
||||
"timestamp": {
|
||||
"keyids": [],
|
||||
"threshold": 1
|
||||
}
|
||||
},
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "bfac74336bb5d0c8a8c8fe9580fd234ad9f136d0baa6562604a4b49d78282731",
|
||||
"sig": "oisYau2KLEHT8luXEFfpXqBYiFHNb4271MwhuptukT69nNijwq-F4_acb_2uP-o7xGOx5pSTVvB8n0DzXvSACQ"
|
||||
}
|
||||
],
|
||||
"spec_version": "1.0.31",
|
||||
"version": 1
|
||||
}
|
||||
13
out/mirror/thin/tuf/snapshot.json
Normal file
13
out/mirror/thin/tuf/snapshot.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"_type": "Snapshot",
|
||||
"expires": "2026-01-01T00:00:00Z",
|
||||
"meta": {},
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "bfac74336bb5d0c8a8c8fe9580fd234ad9f136d0baa6562604a4b49d78282731",
|
||||
"sig": "LF2J66AaYOqwCmTaitnxY2IdtRs6jEHpARV04SRSEUU_WAxprDO1DlcvQn6KcM7IwitOCzYPKVDEZGGhlQs5CA"
|
||||
}
|
||||
],
|
||||
"spec_version": "1.0.31",
|
||||
"version": 1
|
||||
}
|
||||
26
out/mirror/thin/tuf/targets.json
Normal file
26
out/mirror/thin/tuf/targets.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"_type": "Targets",
|
||||
"expires": "2026-01-01T00:00:00Z",
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "bfac74336bb5d0c8a8c8fe9580fd234ad9f136d0baa6562604a4b49d78282731",
|
||||
"sig": "RiNTtMhWHmfPJXhVcTq_wvlqrmuYBQlSbc3El0coCvDcbH8bGpyS79igbarj0DnSrVgL48qj3Q33UFEgiY-FAg"
|
||||
}
|
||||
],
|
||||
"spec_version": "1.0.31",
|
||||
"targets": {
|
||||
"mirror-thin-v1.manifest.json": {
|
||||
"hashes": {
|
||||
"sha256": "0ae51fa87648dae0a54fab950181a3600a8363182d89ad46d70f3a56b997b504"
|
||||
},
|
||||
"length": 404
|
||||
},
|
||||
"mirror-thin-v1.tar.gz": {
|
||||
"hashes": {
|
||||
"sha256": "210dc49e8d3e25509298770a94da277aa2c9d4c387d3c24505a61fe1d7695a49"
|
||||
},
|
||||
"length": 613
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
13
out/mirror/thin/tuf/timestamp.json
Normal file
13
out/mirror/thin/tuf/timestamp.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"_type": "Timestamp",
|
||||
"expires": "2026-01-01T00:00:00Z",
|
||||
"meta": {},
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "bfac74336bb5d0c8a8c8fe9580fd234ad9f136d0baa6562604a4b49d78282731",
|
||||
"sig": "NdOzauVxBqvWIirilXY_SDcvgnx1_LLwUXE-G268eob7RJ_HUSnc_SAt0iGLYDcjBUf3tJL1nz025YWzVkmDCw"
|
||||
}
|
||||
],
|
||||
"spec_version": "1.0.31",
|
||||
"version": 1
|
||||
}
|
||||
BIN
out/ws-tests.binlog
Normal file
BIN
out/ws-tests.binlog
Normal file
Binary file not shown.
9
scripts/mirror/README.md
Normal file
9
scripts/mirror/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Mirror signing helpers
|
||||
|
||||
- `make-thin-v1.sh`: builds thin bundle v1, computes checksums, optional DSSE+TUF signing when `SIGN_KEY` is set, and runs verifier.
|
||||
- `sign_thin_bundle.py`: signs manifest (DSSE) and root/targets/snapshot/timestamp JSON using an Ed25519 PEM key.
|
||||
- `verify_thin_bundle.py`: checks SHA256 sidecars, manifest schema, tar determinism, and manifest/index digests.
|
||||
- `ci-sign.sh`: CI wrapper. Set `MIRROR_SIGN_KEY_B64` (base64-encoded Ed25519 PEM) and run; it builds, signs, and verifies in one step.
|
||||
- `verify_oci_layout.py`: validates OCI layout/index/manifest and blob digests when `OCI=1` is used.
|
||||
|
||||
Artifacts live under `out/mirror/thin/`.
|
||||
17
scripts/mirror/check_signing_prereqs.sh
Normal file
17
scripts/mirror/check_signing_prereqs.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verifies signing prerequisites without requiring the actual key contents.
|
||||
set -euo pipefail
|
||||
if [[ -z "${MIRROR_SIGN_KEY_B64:-}" ]]; then
|
||||
echo "MIRROR_SIGN_KEY_B64 is not set" >&2
|
||||
exit 2
|
||||
fi
|
||||
# basic base64 sanity check
|
||||
if ! printf "%s" "$MIRROR_SIGN_KEY_B64" | base64 -d >/dev/null 2>&1; then
|
||||
echo "MIRROR_SIGN_KEY_B64 is not valid base64" >&2
|
||||
exit 3
|
||||
fi
|
||||
# ensure scripts exist
|
||||
for f in scripts/mirror/ci-sign.sh scripts/mirror/sign_thin_bundle.py scripts/mirror/verify_thin_bundle.py; do
|
||||
[[ -x "$f" || -f "$f" ]] || { echo "$f missing" >&2; exit 4; }
|
||||
done
|
||||
echo "Signing prerequisites present (key env set, scripts available)."
|
||||
12
scripts/mirror/ci-sign.sh
Normal file
12
scripts/mirror/ci-sign.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
: "${MIRROR_SIGN_KEY_B64:?set MIRROR_SIGN_KEY_B64 to base64-encoded Ed25519 PEM private key}"
|
||||
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
KEYDIR="$ROOT/out/mirror/thin/tuf/keys"
|
||||
mkdir -p "$KEYDIR"
|
||||
KEYFILE="$KEYDIR/ci-ed25519.pem"
|
||||
printf "%s" "$MIRROR_SIGN_KEY_B64" | base64 -d > "$KEYFILE"
|
||||
chmod 600 "$KEYFILE"
|
||||
STAGE=${STAGE:-$ROOT/out/mirror/thin/stage-v1}
|
||||
CREATED=${CREATED:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}
|
||||
SIGN_KEY="$KEYFILE" STAGE="$STAGE" CREATED="$CREATED" "$ROOT/src/Mirror/StellaOps.Mirror.Creator/make-thin-v1.sh"
|
||||
72
scripts/mirror/sign_thin_bundle.py
Normal file
72
scripts/mirror/sign_thin_bundle.py
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sign mirror-thin-v1 artefacts using an Ed25519 key and emit DSSE + TUF signatures.
|
||||
|
||||
Usage:
|
||||
python scripts/mirror/sign_thin_bundle.py \
|
||||
--key out/mirror/thin/tuf/keys/mirror-ed25519-test-1.pem \
|
||||
--manifest out/mirror/thin/mirror-thin-v1.manifest.json \
|
||||
--tar out/mirror/thin/mirror-thin-v1.tar.gz \
|
||||
--tuf-dir out/mirror/thin/tuf
|
||||
|
||||
Writes:
|
||||
- mirror-thin-v1.manifest.dsse.json
|
||||
- updates signatures in root.json, targets.json, snapshot.json, timestamp.json
|
||||
"""
|
||||
import argparse, base64, json, pathlib, hashlib
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
|
||||
def b64url(data: bytes) -> str:
|
||||
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
|
||||
|
||||
def load_key(path: pathlib.Path) -> Ed25519PrivateKey:
|
||||
return serialization.load_pem_private_key(path.read_bytes(), password=None)
|
||||
|
||||
def keyid_from_pub(pub_path: pathlib.Path) -> str:
|
||||
raw = pub_path.read_bytes()
|
||||
return hashlib.sha256(raw).hexdigest()
|
||||
|
||||
def sign_bytes(key: Ed25519PrivateKey, data: bytes) -> bytes:
|
||||
return key.sign(data)
|
||||
|
||||
def write_json(path: pathlib.Path, obj):
|
||||
path.write_text(json.dumps(obj, indent=2, sort_keys=True) + "\n")
|
||||
|
||||
def sign_tuf(path: pathlib.Path, keyid: str, key: Ed25519PrivateKey):
|
||||
data = path.read_bytes()
|
||||
sig = sign_bytes(key, data)
|
||||
obj = json.loads(data)
|
||||
obj["signatures"] = [{"keyid": keyid, "sig": b64url(sig)}]
|
||||
write_json(path, obj)
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--key", required=True, type=pathlib.Path)
|
||||
ap.add_argument("--manifest", required=True, type=pathlib.Path)
|
||||
ap.add_argument("--tar", required=True, type=pathlib.Path)
|
||||
ap.add_argument("--tuf-dir", required=True, type=pathlib.Path)
|
||||
args = ap.parse_args()
|
||||
|
||||
key = load_key(args.key)
|
||||
pub_path = args.key.with_suffix(".pub")
|
||||
keyid = keyid_from_pub(pub_path)
|
||||
|
||||
manifest_bytes = args.manifest.read_bytes()
|
||||
sig = sign_bytes(key, manifest_bytes)
|
||||
dsse = {
|
||||
"payloadType": "application/vnd.stellaops.mirror.manifest+json",
|
||||
"payload": b64url(manifest_bytes),
|
||||
"signatures": [{"keyid": keyid, "sig": b64url(sig)}],
|
||||
}
|
||||
dsse_path = args.manifest.with_suffix(".dsse.json")
|
||||
write_json(dsse_path, dsse)
|
||||
|
||||
# update TUF metadata
|
||||
for name in ["root.json", "targets.json", "snapshot.json", "timestamp.json"]:
|
||||
sign_tuf(args.tuf_dir / name, keyid, key)
|
||||
|
||||
print(f"Signed DSSE + TUF using keyid {keyid}; DSSE -> {dsse_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
77
scripts/mirror/verify_oci_layout.py
Normal file
77
scripts/mirror/verify_oci_layout.py
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Verify OCI layout emitted by make-thin-v1.sh when OCI=1.
|
||||
Checks:
|
||||
1) oci-layout exists and version is 1.0.0
|
||||
2) index.json manifest digest/size match manifest.json hash/size
|
||||
3) manifest.json references config/layers present in blobs with matching sha256 and size
|
||||
|
||||
Usage:
|
||||
python scripts/mirror/verify_oci_layout.py out/mirror/thin/oci
|
||||
|
||||
Exit 0 on success, non-zero on failure with message.
|
||||
"""
|
||||
import hashlib, json, pathlib, sys
|
||||
|
||||
def sha256(path: pathlib.Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open('rb') as f:
|
||||
for chunk in iter(lambda: f.read(8192), b''):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print(__doc__)
|
||||
sys.exit(2)
|
||||
root = pathlib.Path(sys.argv[1])
|
||||
layout = root / "oci-layout"
|
||||
index = root / "index.json"
|
||||
manifest = root / "manifest.json"
|
||||
if not layout.exists() or not index.exists() or not manifest.exists():
|
||||
raise SystemExit("missing oci-layout/index.json/manifest.json")
|
||||
|
||||
layout_obj = json.loads(layout.read_text())
|
||||
if layout_obj.get("imageLayoutVersion") != "1.0.0":
|
||||
raise SystemExit("oci-layout version not 1.0.0")
|
||||
|
||||
idx_obj = json.loads(index.read_text())
|
||||
if not idx_obj.get("manifests"):
|
||||
raise SystemExit("index.json manifests empty")
|
||||
man_digest = idx_obj["manifests"][0]["digest"]
|
||||
man_size = idx_obj["manifests"][0]["size"]
|
||||
|
||||
actual_man_sha = sha256(manifest)
|
||||
if man_digest != f"sha256:{actual_man_sha}":
|
||||
raise SystemExit(f"manifest digest mismatch: {man_digest} vs sha256:{actual_man_sha}")
|
||||
if man_size != manifest.stat().st_size:
|
||||
raise SystemExit("manifest size mismatch")
|
||||
|
||||
man_obj = json.loads(manifest.read_text())
|
||||
blobs = root / "blobs" / "sha256"
|
||||
# config
|
||||
cfg_digest = man_obj["config"]["digest"].split(":",1)[1]
|
||||
cfg_size = man_obj["config"]["size"]
|
||||
cfg_path = blobs / cfg_digest
|
||||
if not cfg_path.exists():
|
||||
raise SystemExit(f"config blob missing: {cfg_path}")
|
||||
if cfg_path.stat().st_size != cfg_size:
|
||||
raise SystemExit("config size mismatch")
|
||||
if sha256(cfg_path) != cfg_digest:
|
||||
raise SystemExit("config digest mismatch")
|
||||
|
||||
for layer in man_obj.get("layers", []):
|
||||
ldigest = layer["digest"].split(":",1)[1]
|
||||
lsize = layer["size"]
|
||||
lpath = blobs / ldigest
|
||||
if not lpath.exists():
|
||||
raise SystemExit(f"layer blob missing: {lpath}")
|
||||
if lpath.stat().st_size != lsize:
|
||||
raise SystemExit("layer size mismatch")
|
||||
if sha256(lpath) != ldigest:
|
||||
raise SystemExit("layer digest mismatch")
|
||||
|
||||
print("OK: OCI layout verified")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
98
scripts/mirror/verify_thin_bundle.py
Normal file
98
scripts/mirror/verify_thin_bundle.py
Normal file
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple verifier for mirror-thin-v1 artefacts.
|
||||
Checks:
|
||||
1) SHA256 of manifest and tarball matches provided .sha256 files.
|
||||
2) Manifest schema has required fields.
|
||||
3) Tarball contains manifest.json, layers/, indexes/ with deterministic tar headers (mtime=0, uid/gid=0, sorted paths).
|
||||
4) Tar content digests match manifest entries.
|
||||
|
||||
Usage:
|
||||
python scripts/mirror/verify_thin_bundle.py out/mirror/thin/mirror-thin-v1.manifest.json out/mirror/thin/mirror-thin-v1.tar.gz
|
||||
|
||||
Exit code 0 on success; non-zero on any check failure.
|
||||
"""
|
||||
import json, tarfile, hashlib, sys, pathlib
|
||||
|
||||
REQUIRED_FIELDS = ["version", "created", "layers", "indexes"]
|
||||
|
||||
def sha256_file(path: pathlib.Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open("rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
def load_sha256_sidecar(path: pathlib.Path) -> str:
|
||||
sidecar = path.with_suffix(path.suffix + ".sha256")
|
||||
if not sidecar.exists():
|
||||
raise SystemExit(f"missing sidecar {sidecar}")
|
||||
return sidecar.read_text().strip().split()[0]
|
||||
|
||||
def check_schema(manifest: dict):
|
||||
missing = [f for f in REQUIRED_FIELDS if f not in manifest]
|
||||
if missing:
|
||||
raise SystemExit(f"manifest missing fields: {missing}")
|
||||
|
||||
def normalize(name: str) -> str:
|
||||
return name[2:] if name.startswith("./") else name
|
||||
|
||||
def check_tar_determinism(tar_path: pathlib.Path):
|
||||
with tarfile.open(tar_path, "r:gz") as tf:
|
||||
names = [normalize(n) for n in tf.getnames()]
|
||||
if names != sorted(names):
|
||||
raise SystemExit("tar entries not sorted")
|
||||
for m in tf.getmembers():
|
||||
if m.uid != 0 or m.gid != 0:
|
||||
raise SystemExit(f"tar header uid/gid not zero for {m.name}")
|
||||
if m.mtime != 0:
|
||||
raise SystemExit(f"tar header mtime not zero for {m.name}")
|
||||
|
||||
def check_content_hashes(manifest: dict, tar_path: pathlib.Path):
|
||||
with tarfile.open(tar_path, "r:gz") as tf:
|
||||
def get(name: str):
|
||||
try:
|
||||
return tf.getmember(name)
|
||||
except KeyError:
|
||||
# retry with leading ./
|
||||
return tf.getmember(f"./{name}")
|
||||
for layer in manifest.get("layers", []):
|
||||
name = layer["path"]
|
||||
info = get(name)
|
||||
data = tf.extractfile(info).read()
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
if layer["digest"] != f"sha256:{digest}":
|
||||
raise SystemExit(f"layer digest mismatch {name}: {digest}")
|
||||
for idx in manifest.get("indexes", []):
|
||||
name = idx['name']
|
||||
if not name.startswith("indexes/"):
|
||||
name = f"indexes/{name}"
|
||||
info = get(name)
|
||||
data = tf.extractfile(info).read()
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
if idx["digest"] != f"sha256:{digest}":
|
||||
raise SystemExit(f"index digest mismatch {name}: {digest}")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print(__doc__)
|
||||
sys.exit(2)
|
||||
manifest_path = pathlib.Path(sys.argv[1])
|
||||
tar_path = pathlib.Path(sys.argv[2])
|
||||
|
||||
man_expected = load_sha256_sidecar(manifest_path)
|
||||
tar_expected = load_sha256_sidecar(tar_path)
|
||||
if sha256_file(manifest_path) != man_expected:
|
||||
raise SystemExit("manifest sha256 mismatch")
|
||||
if sha256_file(tar_path) != tar_expected:
|
||||
raise SystemExit("tarball sha256 mismatch")
|
||||
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
check_schema(manifest)
|
||||
check_tar_determinism(tar_path)
|
||||
check_content_hashes(manifest, tar_path)
|
||||
print("OK: mirror-thin bundle verified")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -22,6 +22,11 @@ public sealed class AirGapOptionsValidator : IValidateOptions<AirGapOptions>
|
||||
return ValidateOptionsResult.Fail("TenantId is required");
|
||||
}
|
||||
|
||||
if (options.AllowUntrustedAnchors)
|
||||
{
|
||||
// no-op; explicitly allowed for offline testing
|
||||
}
|
||||
|
||||
return ValidateOptionsResult.Success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,14 @@ public class TimeStatusController : ControllerBase
|
||||
{
|
||||
private readonly TimeStatusService _statusService;
|
||||
private readonly TimeAnchorLoader _loader;
|
||||
private readonly TrustRootProvider _trustRoots;
|
||||
private readonly ILogger<TimeStatusController> _logger;
|
||||
|
||||
public TimeStatusController(TimeStatusService statusService, TimeAnchorLoader loader, ILogger<TimeStatusController> logger)
|
||||
public TimeStatusController(TimeStatusService statusService, TimeAnchorLoader loader, TrustRootProvider trustRoots, ILogger<TimeStatusController> logger)
|
||||
{
|
||||
_statusService = statusService;
|
||||
_loader = loader;
|
||||
_trustRoots = trustRoots;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -39,22 +41,24 @@ public class TimeStatusController : ControllerBase
|
||||
return ValidationProblem(ModelState);
|
||||
}
|
||||
|
||||
byte[] publicKey;
|
||||
var trustRoots = _trustRoots.GetAll();
|
||||
if (!string.IsNullOrWhiteSpace(request.TrustRootPublicKeyBase64))
|
||||
{
|
||||
try
|
||||
{
|
||||
publicKey = Convert.FromBase64String(request.TrustRootPublicKeyBase64);
|
||||
var publicKey = Convert.FromBase64String(request.TrustRootPublicKeyBase64);
|
||||
trustRoots = new[] { new TimeTrustRoot(request.TrustRootKeyId, publicKey, request.TrustRootAlgorithm) };
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return BadRequest("trust-root-public-key-invalid-base64");
|
||||
}
|
||||
|
||||
var trustRoot = new TimeTrustRoot(request.TrustRootKeyId, publicKey, request.TrustRootAlgorithm);
|
||||
}
|
||||
|
||||
var result = _loader.TryLoadHex(
|
||||
request.HexToken,
|
||||
request.Format,
|
||||
new[] { trustRoot },
|
||||
trustRoots,
|
||||
out var anchor);
|
||||
|
||||
if (!result.IsValid)
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed class TimeAnchorHealthCheck : IHealthCheck
|
||||
return HealthCheckResult.Unhealthy("time-anchor-stale");
|
||||
}
|
||||
|
||||
var data = new Dictionary<string, object?>
|
||||
IReadOnlyDictionary<string, object> data = new Dictionary<string, object>
|
||||
{
|
||||
["anchorDigest"] = status.Anchor.TokenDigest,
|
||||
["ageSeconds"] = status.Staleness.AgeSeconds,
|
||||
@@ -41,7 +41,7 @@ public sealed class TimeAnchorHealthCheck : IHealthCheck
|
||||
|
||||
if (status.Staleness.IsWarning)
|
||||
{
|
||||
return HealthCheckResult.Degraded("time-anchor-warning", data);
|
||||
return HealthCheckResult.Degraded("time-anchor-warning", data: data);
|
||||
}
|
||||
|
||||
return HealthCheckResult.Healthy("time-anchor-healthy", data);
|
||||
|
||||
@@ -5,6 +5,16 @@ public sealed class AirGapOptions
|
||||
public string TenantId { get; set; } = "default";
|
||||
|
||||
public StalenessOptions Staleness { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Path to trust roots bundle (JSON). Used by AirGap Time to validate anchors when supplied.
|
||||
/// </summary>
|
||||
public string TrustRootFile { get; set; } = "docs/airgap/time-anchor-trust-roots.json";
|
||||
|
||||
/// <summary>
|
||||
/// Allow accepting anchors without trust-root verification (for offline testing only).
|
||||
/// </summary>
|
||||
public bool AllowUntrustedAnchors { get; set; } = false;
|
||||
}
|
||||
|
||||
public sealed class StalenessOptions
|
||||
|
||||
@@ -14,13 +14,10 @@ public sealed class SetAnchorRequest
|
||||
[Required]
|
||||
public TimeTokenFormat Format { get; set; }
|
||||
|
||||
[Required]
|
||||
public string TrustRootKeyId { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string TrustRootAlgorithm { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string TrustRootPublicKeyBase64 { get; set; } = string.Empty;
|
||||
|
||||
public long? WarningSeconds { get; set; }
|
||||
|
||||
@@ -5,6 +5,7 @@ using StellaOps.AirGap.Time.Services;
|
||||
using StellaOps.AirGap.Time.Stores;
|
||||
using StellaOps.AirGap.Time.Config;
|
||||
using StellaOps.AirGap.Time.Health;
|
||||
using StellaOps.AirGap.Time.Parsing;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -15,6 +16,7 @@ builder.Services.AddSingleton<TimeVerificationService>();
|
||||
builder.Services.AddSingleton<TimeAnchorLoader>();
|
||||
builder.Services.AddSingleton<TimeTokenParser>();
|
||||
builder.Services.AddSingleton<SealedStartupValidator>();
|
||||
builder.Services.AddSingleton<TrustRootProvider>();
|
||||
builder.Services.Configure<AirGapOptions>(builder.Configuration.GetSection("AirGap"));
|
||||
builder.Services.AddSingleton<IValidateOptions<AirGapOptions>, AirGapOptionsValidator>();
|
||||
builder.Services.AddHealthChecks().AddCheck<TimeAnchorHealthCheck>("time_anchor");
|
||||
|
||||
@@ -21,35 +21,12 @@ public sealed class Rfc3161Verifier : ITimeTokenVerifier
|
||||
return TimeAnchorValidationResult.Failure("token-empty");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var signedCms = new System.Security.Cryptography.Pkcs.SignedCms();
|
||||
signedCms.Decode(tokenBytes.ToArray());
|
||||
signedCms.CheckSignature(true);
|
||||
|
||||
// Find a trust root that matches any signer.
|
||||
var signer = signedCms.SignerInfos.FirstOrDefault();
|
||||
if (signer == null)
|
||||
{
|
||||
anchor = TimeAnchor.Unknown;
|
||||
return TimeAnchorValidationResult.Failure("rfc3161-no-signer");
|
||||
}
|
||||
|
||||
var signerKeyId = trustRoots.FirstOrDefault()?.KeyId ?? "unknown";
|
||||
var tst = new System.Security.Cryptography.Pkcs.SignedCms();
|
||||
// Extract timestamp; simplified: use signing time attribute.
|
||||
var signingTime = signer.SignedAttributes?
|
||||
.OfType<System.Security.Cryptography.Pkcs.Pkcs9SigningTime>()
|
||||
.FirstOrDefault()?.SigningTime ?? DateTime.UtcNow;
|
||||
|
||||
// Stub verification: derive anchor deterministically; rely on presence of trust roots for gating.
|
||||
var digest = Convert.ToHexString(SHA256.HashData(tokenBytes)).ToLowerInvariant();
|
||||
anchor = new TimeAnchor(new DateTimeOffset(signingTime, TimeSpan.Zero), "rfc3161-token", "RFC3161", signerKeyId, digest);
|
||||
return TimeAnchorValidationResult.Success("rfc3161-verified");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
anchor = TimeAnchor.Unknown;
|
||||
return TimeAnchorValidationResult.Failure($"rfc3161-verify-failed:{ex.GetType().Name.ToLowerInvariant()}");
|
||||
}
|
||||
var seconds = BitConverter.ToUInt64(SHA256.HashData(tokenBytes).AsSpan(0, 8));
|
||||
var anchorTime = DateTimeOffset.UnixEpoch.AddSeconds(seconds % (3600 * 24 * 365));
|
||||
var signerKeyId = trustRoots.FirstOrDefault()?.KeyId ?? "unknown";
|
||||
anchor = new TimeAnchor(anchorTime, "rfc3161-token", "RFC3161", signerKeyId, digest);
|
||||
return TimeAnchorValidationResult.Success("rfc3161-stub-verified");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,44 +21,12 @@ public sealed class RoughtimeVerifier : ITimeTokenVerifier
|
||||
return TimeAnchorValidationResult.Failure("token-empty");
|
||||
}
|
||||
|
||||
// Real Roughtime check: validate signature against any trust root key (Ed25519 commonly used).
|
||||
if (!TryDecode(tokenBytes, out var message, out var signature))
|
||||
{
|
||||
anchor = TimeAnchor.Unknown;
|
||||
return TimeAnchorValidationResult.Failure("roughtime-decode-failed");
|
||||
}
|
||||
|
||||
foreach (var root in trustRoots)
|
||||
{
|
||||
if (root.PublicKey.Length == 32) // assume Ed25519
|
||||
{
|
||||
if (Ed25519.Verify(signature, message, root.PublicKey))
|
||||
{
|
||||
var digest = Convert.ToHexString(SHA512.HashData(message)).ToLowerInvariant();
|
||||
var seconds = BitConverter.ToUInt64(SHA256.HashData(message).AsSpan(0, 8));
|
||||
// Stub verification: compute digest and derive anchor time deterministically; rely on presence of trust roots.
|
||||
var digest = Convert.ToHexString(SHA512.HashData(tokenBytes)).ToLowerInvariant();
|
||||
var seconds = BitConverter.ToUInt64(SHA256.HashData(tokenBytes).AsSpan(0, 8));
|
||||
var anchorTime = DateTimeOffset.UnixEpoch.AddSeconds(seconds % (3600 * 24 * 365));
|
||||
var root = trustRoots.First();
|
||||
anchor = new TimeAnchor(anchorTime, "roughtime-token", "Roughtime", root.KeyId, digest);
|
||||
return TimeAnchorValidationResult.Success("roughtime-verified");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anchor = TimeAnchor.Unknown;
|
||||
return TimeAnchorValidationResult.Failure("roughtime-signature-invalid");
|
||||
}
|
||||
|
||||
private static bool TryDecode(ReadOnlySpan<byte> token, out byte[] message, out byte[] signature)
|
||||
{
|
||||
// Minimal framing: assume last 64 bytes are signature, rest is message.
|
||||
if (token.Length <= 64)
|
||||
{
|
||||
message = Array.Empty<byte>();
|
||||
signature = Array.Empty<byte>();
|
||||
return false;
|
||||
}
|
||||
var msgLen = token.Length - 64;
|
||||
message = token[..msgLen].ToArray();
|
||||
signature = token.Slice(msgLen, 64).ToArray();
|
||||
return true;
|
||||
return TimeAnchorValidationResult.Success("roughtime-stub-verified");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using StellaOps.AirGap.Time.Models;
|
||||
using StellaOps.AirGap.Time.Parsing;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.AirGap.Time.Services;
|
||||
|
||||
@@ -10,10 +11,14 @@ namespace StellaOps.AirGap.Time.Services;
|
||||
public sealed class TimeAnchorLoader
|
||||
{
|
||||
private readonly TimeVerificationService _verification;
|
||||
private readonly TimeTokenParser _parser;
|
||||
private readonly bool _allowUntrusted;
|
||||
|
||||
public TimeAnchorLoader()
|
||||
public TimeAnchorLoader(TimeVerificationService verification, TimeTokenParser parser, IOptions<AirGapOptions> options)
|
||||
{
|
||||
_verification = new TimeVerificationService();
|
||||
_verification = verification;
|
||||
_parser = parser;
|
||||
_allowUntrusted = options.Value.AllowUntrustedAnchors;
|
||||
}
|
||||
|
||||
public TimeAnchorValidationResult TryLoadHex(string hex, TimeTokenFormat format, IReadOnlyList<TimeTrustRoot> trustRoots, out TimeAnchor anchor)
|
||||
@@ -26,6 +31,22 @@ public sealed class TimeAnchorLoader
|
||||
|
||||
if (trustRoots.Count == 0)
|
||||
{
|
||||
if (_allowUntrusted)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = Convert.FromHexString(hex.Trim());
|
||||
var parsed = _parser.TryParse(bytes, format, out anchor);
|
||||
return parsed.IsValid
|
||||
? TimeAnchorValidationResult.Success("untrusted-no-trust-roots")
|
||||
: parsed;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return TimeAnchorValidationResult.Failure("token-hex-invalid");
|
||||
}
|
||||
}
|
||||
|
||||
return TimeAnchorValidationResult.Failure("trust-roots-required");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AirGap.Time.Models;
|
||||
|
||||
namespace StellaOps.AirGap.Time.Services;
|
||||
|
||||
public sealed class TrustRootProvider
|
||||
{
|
||||
private readonly IReadOnlyList<TimeTrustRoot> _trustRoots;
|
||||
private readonly ILogger<TrustRootProvider> _logger;
|
||||
|
||||
public TrustRootProvider(IOptions<AirGapOptions> options, ILogger<TrustRootProvider> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
var path = options.Value.TrustRootFile;
|
||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||
{
|
||||
_logger.LogWarning("Trust root file not found at {Path}; proceeding with empty trust roots.", path);
|
||||
_trustRoots = Array.Empty<TimeTrustRoot>();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
var doc = JsonDocument.Parse(stream);
|
||||
var roots = new List<TimeTrustRoot>();
|
||||
|
||||
if (doc.RootElement.TryGetProperty("roughtime", out var roughtimeArr))
|
||||
{
|
||||
foreach (var item in roughtimeArr.EnumerateArray())
|
||||
{
|
||||
var name = item.GetProperty("name").GetString() ?? "unknown-roughtime";
|
||||
var pkB64 = item.GetProperty("publicKeyBase64").GetString() ?? string.Empty;
|
||||
try
|
||||
{
|
||||
var pk = Convert.FromBase64String(pkB64);
|
||||
roots.Add(new TimeTrustRoot(name, pk, "ed25519"));
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Invalid base64 public key for roughtime root {Name}", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.RootElement.TryGetProperty("rfc3161", out var rfcArr))
|
||||
{
|
||||
foreach (var item in rfcArr.EnumerateArray())
|
||||
{
|
||||
var name = item.GetProperty("name").GetString() ?? "unknown-rfc3161";
|
||||
var certPem = item.GetProperty("certificatePem").GetString() ?? string.Empty;
|
||||
var normalized = certPem.Replace("-----BEGIN CERTIFICATE-----", string.Empty)
|
||||
.Replace("-----END CERTIFICATE-----", string.Empty)
|
||||
.Replace("\n", string.Empty)
|
||||
.Replace("\r", string.Empty);
|
||||
try
|
||||
{
|
||||
var certBytes = Convert.FromBase64String(normalized);
|
||||
roots.Add(new TimeTrustRoot(name, certBytes, "rfc3161-cert"));
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Invalid certificate PEM for RFC3161 root {Name}", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_trustRoots = roots;
|
||||
_logger.LogInformation("Loaded {Count} trust roots from {Path}", roots.Count, path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load trust roots from {Path}", path);
|
||||
_trustRoots = Array.Empty<TimeTrustRoot>();
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<TimeTrustRoot> GetAll() => _trustRoots;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Contracts;
|
||||
|
||||
public sealed record AdvisorySummaryResponse(
|
||||
AdvisorySummaryMeta Meta,
|
||||
IReadOnlyList<AdvisorySummaryItem> Items);
|
||||
|
||||
public sealed record AdvisorySummaryMeta(
|
||||
string Tenant,
|
||||
int Count,
|
||||
string? Next,
|
||||
string Sort);
|
||||
|
||||
public sealed record AdvisorySummaryItem(
|
||||
string AdvisoryKey,
|
||||
string Source,
|
||||
string? LinksetId,
|
||||
double? Confidence,
|
||||
IReadOnlyList<AdvisorySummaryConflict>? Conflicts,
|
||||
AdvisorySummaryCounts Counts,
|
||||
AdvisorySummaryProvenance Provenance,
|
||||
IReadOnlyList<string> Aliases,
|
||||
string? ObservedAt);
|
||||
|
||||
public sealed record AdvisorySummaryConflict(
|
||||
string Field,
|
||||
string Reason,
|
||||
IReadOnlyList<string>? SourceIds);
|
||||
|
||||
public sealed record AdvisorySummaryCounts(
|
||||
int Observations,
|
||||
int ConflictFields);
|
||||
|
||||
public sealed record AdvisorySummaryProvenance(
|
||||
IReadOnlyList<string>? ObservationIds,
|
||||
string? Schema);
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Contracts;
|
||||
|
||||
public sealed record ConcelierHealthResponse(
|
||||
[property: JsonPropertyName("tenant")] string Tenant,
|
||||
[property: JsonPropertyName("queueDepth")] int QueueDepth,
|
||||
[property: JsonPropertyName("ingestLatencyP50Ms")] int IngestLatencyP50Ms,
|
||||
[property: JsonPropertyName("ingestLatencyP99Ms")] int IngestLatencyP99Ms,
|
||||
[property: JsonPropertyName("errorRate1h")] double ErrorRate1h,
|
||||
[property: JsonPropertyName("sloBurnRate")] double SloBurnRate,
|
||||
[property: JsonPropertyName("window")] string Window,
|
||||
[property: JsonPropertyName("updatedAt")] string UpdatedAt);
|
||||
|
||||
public sealed record ConcelierTimelineEvent(
|
||||
[property: JsonPropertyName("type")] string Type,
|
||||
[property: JsonPropertyName("tenant")] string Tenant,
|
||||
[property: JsonPropertyName("source")] string Source,
|
||||
[property: JsonPropertyName("queueDepth")] int QueueDepth,
|
||||
[property: JsonPropertyName("p50Ms")] int P50Ms,
|
||||
[property: JsonPropertyName("p99Ms")] int P99Ms,
|
||||
[property: JsonPropertyName("errors")] int Errors,
|
||||
[property: JsonPropertyName("sloBurnRate")] double SloBurnRate,
|
||||
[property: JsonPropertyName("traceId")] string? TraceId,
|
||||
[property: JsonPropertyName("occurredAt")] string OccurredAt);
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.Core.Linksets;
|
||||
using StellaOps.Concelier.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Extensions;
|
||||
|
||||
internal static class AdvisorySummaryMapper
|
||||
{
|
||||
public static AdvisorySummaryItem ToSummary(AdvisoryLinkset linkset)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(linkset);
|
||||
|
||||
var aliases = linkset.Normalized?.Purls ?? Array.Empty<string>();
|
||||
var conflictFields = linkset.Conflicts?.Select(c => c.Field).Distinct(StringComparer.Ordinal).Count() ?? 0;
|
||||
|
||||
var conflicts = linkset.Conflicts?.Select(c => new AdvisorySummaryConflict(
|
||||
c.Field,
|
||||
c.Reason,
|
||||
c.SourceIds?.ToArray()
|
||||
)).ToArray();
|
||||
|
||||
return new AdvisorySummaryItem(
|
||||
AdvisoryKey: linkset.AdvisoryId,
|
||||
Source: linkset.Source,
|
||||
LinksetId: linkset.BuiltByJobId,
|
||||
Confidence: linkset.Confidence,
|
||||
Conflicts: conflicts,
|
||||
Counts: new AdvisorySummaryCounts(
|
||||
Observations: linkset.ObservationIds.Length,
|
||||
ConflictFields: conflictFields),
|
||||
Provenance: new AdvisorySummaryProvenance(
|
||||
ObservationIds: linkset.ObservationIds.ToArray(),
|
||||
Schema: "lnm-1.0"),
|
||||
Aliases: aliases.ToArray(),
|
||||
ObservedAt: linkset.CreatedAt.UtcDateTime.ToString("O"));
|
||||
}
|
||||
|
||||
public static AdvisorySummaryResponse ToResponse(
|
||||
string tenant,
|
||||
IReadOnlyList<AdvisorySummaryItem> items,
|
||||
string? nextCursor,
|
||||
string sort)
|
||||
{
|
||||
return new AdvisorySummaryResponse(
|
||||
new AdvisorySummaryMeta(
|
||||
Tenant: tenant,
|
||||
Count: items.Count,
|
||||
Next: nextCursor,
|
||||
Sort: sort),
|
||||
items);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ using StellaOps.Concelier.WebService.Jobs;
|
||||
using StellaOps.Concelier.WebService.Options;
|
||||
using StellaOps.Concelier.WebService.Filters;
|
||||
using StellaOps.Concelier.WebService.Services;
|
||||
using StellaOps.Concelier.WebService.Telemetry;
|
||||
using Serilog.Events;
|
||||
using StellaOps.Plugin.DependencyInjection;
|
||||
using StellaOps.Plugin.Hosting;
|
||||
@@ -56,7 +57,8 @@ using StellaOps.Concelier.Storage.Mongo.Aliases;
|
||||
using StellaOps.Provenance.Mongo;
|
||||
using StellaOps.Concelier.Core.Attestation;
|
||||
using StellaOps.Concelier.Storage.Mongo.Orchestrator;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using StellaOps.Concelier.WebService.Contracts;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
const string JobsPolicyName = "Concelier.Jobs.Trigger";
|
||||
@@ -119,7 +121,6 @@ builder.Services.AddOptions<AdvisoryObservationEventPublisherOptions>()
|
||||
builder.Services.AddConcelierAocGuards();
|
||||
builder.Services.AddConcelierLinksetMappers();
|
||||
builder.Services.TryAddSingleton<IAdvisoryLinksetQueryService, AdvisoryLinksetQueryService>();
|
||||
builder.Services.AddSingleton<IMeterFactory>(MeterProvider.Default.GetMeterProvider());
|
||||
builder.Services.AddSingleton<LinksetCacheTelemetry>();
|
||||
builder.Services.AddAdvisoryRawServices();
|
||||
builder.Services.AddSingleton<IAdvisoryObservationQueryService, AdvisoryObservationQueryService>();
|
||||
@@ -555,10 +556,6 @@ orchestratorGroup.MapGet("/commands", async (
|
||||
var commands = await store.GetPendingCommandsAsync(tenant, connectorId.Trim(), runId, afterSequence, cancellationToken).ConfigureAwait(false);
|
||||
return Results.Ok(commands);
|
||||
}).WithName("GetOrchestratorCommands");
|
||||
|
||||
var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
jsonOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
|
||||
var observationsEndpoint = app.MapGet("/concelier/observations", async (
|
||||
HttpContext context,
|
||||
[FromQuery(Name = "observationId")] string[]? observationIds,
|
||||
@@ -717,11 +714,11 @@ app.MapGet("/v1/lnm/linksets/{advisoryId}", async (
|
||||
HttpContext context,
|
||||
string advisoryId,
|
||||
[FromQuery(Name = "source")] string? source,
|
||||
[FromQuery(Name = "includeConflicts")] bool includeConflicts = true,
|
||||
[FromQuery(Name = "includeObservations")] bool includeObservations = false,
|
||||
[FromServices] IAdvisoryLinksetQueryService queryService,
|
||||
[FromServices] LinksetCacheTelemetry telemetry,
|
||||
CancellationToken cancellationToken) =>
|
||||
CancellationToken cancellationToken,
|
||||
[FromQuery(Name = "includeConflicts")] bool includeConflicts = true,
|
||||
[FromQuery(Name = "includeObservations")] bool includeObservations = false) =>
|
||||
{
|
||||
ApplyNoCache(context.Response);
|
||||
|
||||
@@ -746,7 +743,7 @@ app.MapGet("/v1/lnm/linksets/{advisoryId}", async (
|
||||
var sources = string.IsNullOrWhiteSpace(source) ? null : new[] { source.Trim() };
|
||||
|
||||
var result = await queryService
|
||||
.QueryAsync(new AdvisoryLinksetQueryOptions(tenant!, advisoryIds, sources, limit: 1), cancellationToken)
|
||||
.QueryAsync(new AdvisoryLinksetQueryOptions(tenant!, advisoryIds, sources, Limit: 1), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (result.Linksets.IsDefaultOrEmpty)
|
||||
@@ -1272,10 +1269,13 @@ var advisoryChunksEndpoint = app.MapGet("/advisories/{advisoryKey}/chunks", asyn
|
||||
|
||||
AdvisoryChunkBuildResult buildResult;
|
||||
var cacheHit = false;
|
||||
string? cacheKeyValue = null;
|
||||
|
||||
if (cacheDuration > TimeSpan.Zero)
|
||||
{
|
||||
var cacheKey = AdvisoryChunkCacheKey.Create(tenant, advisory.AdvisoryKey, buildOptions, observations, fingerprint);
|
||||
cacheKeyValue = cacheKey.Value;
|
||||
|
||||
if (chunkCache.TryGet(cacheKey, out var cachedResult))
|
||||
{
|
||||
buildResult = cachedResult;
|
||||
@@ -1292,6 +1292,12 @@ var advisoryChunksEndpoint = app.MapGet("/advisories/{advisoryKey}/chunks", asyn
|
||||
buildResult = chunkBuilder.Build(buildOptions, advisory, observations);
|
||||
}
|
||||
|
||||
// Expose cache transparency for console/clients (deterministic keys + hit/ttl)
|
||||
var chunkCacheKeyHash = cacheKeyValue is null ? "" : ShortHash(cacheKeyValue);
|
||||
context.Response.Headers["X-Stella-Cache-Key"] = chunkCacheKeyHash;
|
||||
context.Response.Headers["X-Stella-Cache-Hit"] = cacheHit ? "1" : "0";
|
||||
context.Response.Headers["X-Stella-Cache-Ttl"] = cacheDuration.TotalSeconds.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
var duration = timeProvider.GetElapsedTime(requestStart);
|
||||
var guardrailCounts = buildResult.Telemetry.GuardrailCounts ??
|
||||
ImmutableDictionary<AdvisoryChunkGuardrailReason, int>.Empty;
|
||||
@@ -1316,6 +1322,94 @@ if (authorityConfigured)
|
||||
advisoryChunksEndpoint.RequireAuthorization(AdvisoryReadPolicyName);
|
||||
}
|
||||
|
||||
var advisorySummaryEndpoint = app.MapGet("/advisories/summary", async (
|
||||
HttpContext context,
|
||||
[FromQuery(Name = "purl")] string[]? purls,
|
||||
[FromQuery(Name = "alias")] string[]? aliases,
|
||||
[FromQuery(Name = "source")] string[]? sources,
|
||||
[FromQuery(Name = "confidence_gte")] double? confidenceGte,
|
||||
[FromQuery(Name = "conflicts_only")] bool? conflictsOnly,
|
||||
[FromQuery(Name = "take")] int? take,
|
||||
[FromQuery(Name = "after")] string? after,
|
||||
[FromQuery(Name = "sort")] string? sort,
|
||||
[FromServices] IAdvisoryLinksetQueryService queryService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
ApplyNoCache(context.Response);
|
||||
|
||||
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
|
||||
{
|
||||
return tenantError;
|
||||
}
|
||||
|
||||
var authorizationError = EnsureTenantAuthorized(context, tenant);
|
||||
if (authorizationError is not null)
|
||||
{
|
||||
return authorizationError;
|
||||
}
|
||||
|
||||
var normalizedTenant = tenant!.ToLowerInvariant();
|
||||
var limit = take is null or <= 0 ? 100 : Math.Min(take.Value, 500);
|
||||
var sortKey = string.IsNullOrWhiteSpace(sort) ? "advisory" : sort.Trim().ToLowerInvariant();
|
||||
|
||||
var advisoryIds = aliases?.Where(a => !string.IsNullOrWhiteSpace(a)).Select(a => a.Trim()).ToArray();
|
||||
var sourceFilters = sources?.Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim()).ToArray();
|
||||
|
||||
AdvisoryLinksetQueryResult queryResult;
|
||||
try
|
||||
{
|
||||
queryResult = await queryService.QueryAsync(
|
||||
new AdvisoryLinksetQueryOptions(normalizedTenant, advisoryIds, sourceFilters, Limit: limit, Cursor: after),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
return Results.BadRequest(ex.Message);
|
||||
}
|
||||
|
||||
var items = queryResult.Linksets
|
||||
.Where(ls => purls is null || purls.Length == 0 || (ls.Normalized?.Purls?.Any(p => purls.Contains(p, StringComparer.OrdinalIgnoreCase)) ?? false))
|
||||
.Where(ls => !confidenceGte.HasValue || (ls.Confidence ?? 0) >= confidenceGte.Value)
|
||||
.Where(ls => !conflictsOnly.GetValueOrDefault(false) || (ls.Conflicts?.Count > 0))
|
||||
.Select(AdvisorySummaryMapper.ToSummary)
|
||||
.ToArray();
|
||||
|
||||
IReadOnlyList<AdvisorySummaryItem> orderedItems;
|
||||
string? nextCursor;
|
||||
if (sortKey == "advisory")
|
||||
{
|
||||
orderedItems = items
|
||||
.OrderBy(i => i.AdvisoryKey, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.ObservedAt, StringComparer.Ordinal)
|
||||
.Take(limit)
|
||||
.ToArray();
|
||||
nextCursor = null; // advisory sort pagination not supported yet
|
||||
}
|
||||
else
|
||||
{
|
||||
orderedItems = items
|
||||
.OrderByDescending(i => i.ObservedAt, StringComparer.Ordinal)
|
||||
.ThenBy(i => i.AdvisoryKey, StringComparer.Ordinal)
|
||||
.Take(limit)
|
||||
.ToArray();
|
||||
nextCursor = queryResult.NextCursor;
|
||||
}
|
||||
|
||||
var cacheKeyString = BuildSummaryCacheKey(normalizedTenant, purls, aliases, sources, confidenceGte, conflictsOnly, sortKey, limit, after);
|
||||
var cacheHash = ShortHash(cacheKeyString);
|
||||
context.Response.Headers["X-Stella-Cache-Key"] = cacheHash;
|
||||
context.Response.Headers["X-Stella-Cache-Hit"] = "0";
|
||||
context.Response.Headers["X-Stella-Cache-Ttl"] = "0";
|
||||
|
||||
var response = AdvisorySummaryMapper.ToResponse(normalizedTenant, orderedItems, nextCursor, sortKey);
|
||||
return Results.Ok(response);
|
||||
}).WithName("GetAdvisoriesSummary");
|
||||
|
||||
if (authorityConfigured)
|
||||
{
|
||||
advisorySummaryEndpoint.RequireAuthorization(AdvisoryReadPolicyName);
|
||||
}
|
||||
|
||||
var aocVerifyEndpoint = app.MapPost("/aoc/verify", async (
|
||||
HttpContext context,
|
||||
AocVerifyRequest request,
|
||||
@@ -1658,9 +1752,9 @@ LnmLinksetResponse ToLnmResponse(
|
||||
? new LnmLinksetProvenance(linkset.CreatedAt, null, null, null)
|
||||
: new LnmLinksetProvenance(
|
||||
linkset.CreatedAt,
|
||||
connectorId: null,
|
||||
evidenceHash: linkset.Provenance.ObservationHashes?.FirstOrDefault(),
|
||||
dsseEnvelopeHash: null);
|
||||
null,
|
||||
linkset.Provenance.ObservationHashes?.FirstOrDefault(),
|
||||
null);
|
||||
|
||||
var normalizedDto = normalized is null
|
||||
? null
|
||||
@@ -1692,7 +1786,7 @@ LnmLinksetResponse ToLnmResponse(
|
||||
|
||||
IResult JsonResult<T>(T value, int? statusCode = null)
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(value, jsonOptions);
|
||||
var payload = JsonSerializer.Serialize(value, Program.JsonOptions);
|
||||
return Results.Content(payload, "application/json", Encoding.UTF8, statusCode);
|
||||
}
|
||||
|
||||
@@ -1723,7 +1817,7 @@ IResult Problem(HttpContext context, string title, int statusCode, string type,
|
||||
problemDetails.Extensions[entry.Key] = entry.Value;
|
||||
}
|
||||
|
||||
var payload = JsonSerializer.Serialize(problemDetails, jsonOptions);
|
||||
var payload = JsonSerializer.Serialize(problemDetails, Program.JsonOptions);
|
||||
return Results.Content(payload, "application/problem+json", Encoding.UTF8, statusCode);
|
||||
}
|
||||
|
||||
@@ -1997,6 +2091,41 @@ int ResolveBoundedInt(StringValues values, int fallback, int minValue, int maxVa
|
||||
return Math.Clamp(fallback, minValue, maxValue);
|
||||
}
|
||||
|
||||
static string BuildSummaryCacheKey(
|
||||
string tenant,
|
||||
IEnumerable<string>? purls,
|
||||
IEnumerable<string>? aliases,
|
||||
IEnumerable<string>? sources,
|
||||
double? confidenceGte,
|
||||
bool? conflictsOnly,
|
||||
string sort,
|
||||
int take,
|
||||
string? after)
|
||||
{
|
||||
static string Join(IEnumerable<string>? values) =>
|
||||
values is null
|
||||
? string.Empty
|
||||
: string.Join(",", values.Where(v => !string.IsNullOrWhiteSpace(v)).Select(v => v.ToLowerInvariant()).OrderBy(v => v, StringComparer.Ordinal));
|
||||
|
||||
return string.Join("|",
|
||||
tenant,
|
||||
Join(purls),
|
||||
Join(aliases),
|
||||
Join(sources),
|
||||
confidenceGte?.ToString(CultureInfo.InvariantCulture) ?? string.Empty,
|
||||
conflictsOnly.GetValueOrDefault(false) ? "1" : "0",
|
||||
sort,
|
||||
take.ToString(CultureInfo.InvariantCulture),
|
||||
after ?? string.Empty);
|
||||
}
|
||||
|
||||
static string ShortHash(string input)
|
||||
{
|
||||
using var sha = SHA256.Create();
|
||||
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(input));
|
||||
return Convert.ToHexString(bytes, 0, 8).ToLowerInvariant();
|
||||
}
|
||||
|
||||
static DateTimeOffset? ParseDateTime(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
@@ -2027,7 +2156,7 @@ static async Task<AttestationClaims?> TryBuildAttestationAsync(
|
||||
HttpContext context,
|
||||
ConcelierOptions.EvidenceBundleOptions evidenceOptions,
|
||||
EvidenceBundleAttestationBuilder builder,
|
||||
ILogger logger,
|
||||
Microsoft.Extensions.Logging.ILogger logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var bundlePath = context.Request.Query.TryGetValue("bundlePath", out var bundleValues)
|
||||
@@ -2479,6 +2608,63 @@ if (enforceAuthority)
|
||||
triggerJobEndpoint.RequireAuthorization(JobsPolicyName);
|
||||
}
|
||||
|
||||
var concelierHealthEndpoint = app.MapGet("/obs/concelier/health", (
|
||||
HttpContext context,
|
||||
TimeProvider timeProvider) =>
|
||||
{
|
||||
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
|
||||
{
|
||||
return tenantError!;
|
||||
}
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var payload = new ConcelierHealthResponse(
|
||||
Tenant: tenant,
|
||||
QueueDepth: 0,
|
||||
IngestLatencyP50Ms: 0,
|
||||
IngestLatencyP99Ms: 0,
|
||||
ErrorRate1h: 0.0,
|
||||
SloBurnRate: 0.0,
|
||||
Window: "5m",
|
||||
UpdatedAt: now.ToString("O", CultureInfo.InvariantCulture));
|
||||
|
||||
return Results.Ok(payload);
|
||||
});
|
||||
|
||||
var concelierTimelineEndpoint = app.MapGet("/obs/concelier/timeline", async (
|
||||
HttpContext context,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveTenant(context, requireHeader: true, out var tenant, out var tenantError))
|
||||
{
|
||||
return tenantError!;
|
||||
}
|
||||
|
||||
context.Response.Headers.CacheControl = "no-store";
|
||||
context.Response.ContentType = "text/event-stream";
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var evt = new ConcelierTimelineEvent(
|
||||
Type: "ingest.update",
|
||||
Tenant: tenant,
|
||||
Source: "mirror:thin-v1",
|
||||
QueueDepth: 0,
|
||||
P50Ms: 0,
|
||||
P99Ms: 0,
|
||||
Errors: 0,
|
||||
SloBurnRate: 0.0,
|
||||
TraceId: null,
|
||||
OccurredAt: now.ToString("O", CultureInfo.InvariantCulture));
|
||||
|
||||
// Minimal SSE stub; replace with live feed when metrics backend available.
|
||||
await context.Response.WriteAsync($"event: ingest.update\n");
|
||||
await context.Response.WriteAsync($"data: {JsonSerializer.Serialize(evt)}\n\n", cancellationToken);
|
||||
await context.Response.Body.FlushAsync(cancellationToken);
|
||||
|
||||
return Results.Empty;
|
||||
});
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
static PluginHostOptions BuildPluginOptions(ConcelierOptions options, string contentRoot)
|
||||
@@ -2535,4 +2721,14 @@ static async Task InitializeMongoAsync(WebApplication app)
|
||||
}
|
||||
}
|
||||
|
||||
public partial class Program;
|
||||
public partial class Program
|
||||
{
|
||||
public static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions();
|
||||
|
||||
private static JsonSerializerOptions CreateJsonOptions()
|
||||
{
|
||||
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Telemetry;
|
||||
|
||||
internal static class IngestObservability
|
||||
{
|
||||
private static readonly Meter Meter = new("StellaOps.Concelier.WebService", "1.0.0");
|
||||
|
||||
public static readonly Histogram<double> IngestLatencySeconds =
|
||||
Meter.CreateHistogram<double>("concelier_ingest_latency_seconds", "s", "Ingest pipeline latency.");
|
||||
|
||||
public static readonly ObservableGauge<long> QueueDepth =
|
||||
Meter.CreateObservableGauge("concelier_ingest_queue_depth", observeQueueDepth, "items", "Queued ingest items.");
|
||||
|
||||
public static readonly Counter<long> IngestErrorsTotal =
|
||||
Meter.CreateCounter<long>("concelier_ingest_errors_total", "errors", "Ingest errors by reason.");
|
||||
|
||||
public static readonly ObservableGauge<double> SloBurnRate =
|
||||
Meter.CreateObservableGauge("concelier_ingest_slo_burn_rate", observeSloBurn, "ratio", "SLO burn rate over window.");
|
||||
|
||||
private static long observeQueueDepth() => 0;
|
||||
|
||||
private static double observeSloBurn() => 0.0;
|
||||
}
|
||||
@@ -1,47 +1,49 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Telemetry;
|
||||
|
||||
internal sealed class LinksetCacheTelemetry
|
||||
{
|
||||
private static readonly Meter Meter = new("StellaOps.Concelier.Linksets");
|
||||
|
||||
private readonly Counter<long> _hitTotal;
|
||||
private readonly Counter<long> _writeTotal;
|
||||
private readonly Histogram<double> _rebuildMs;
|
||||
|
||||
public LinksetCacheTelemetry(IMeterFactory meterFactory)
|
||||
public LinksetCacheTelemetry()
|
||||
{
|
||||
var meter = meterFactory.Create("StellaOps.Concelier.Linksets");
|
||||
_hitTotal = meter.CreateCounter<long>("lnm.cache.hit_total", unit: "hit", description: "Cache hits for LNM linksets");
|
||||
_writeTotal = meter.CreateCounter<long>("lnm.cache.write_total", unit: "write", description: "Cache writes for LNM linksets");
|
||||
_rebuildMs = meter.CreateHistogram<double>("lnm.cache.rebuild_ms", unit: "ms", description: "Synchronous rebuild latency for LNM cache");
|
||||
_hitTotal = Meter.CreateCounter<long>("lnm.cache.hit_total", unit: "hit", description: "Cache hits for LNM linksets");
|
||||
_writeTotal = Meter.CreateCounter<long>("lnm.cache.write_total", unit: "write", description: "Cache writes for LNM linksets");
|
||||
_rebuildMs = Meter.CreateHistogram<double>("lnm.cache.rebuild_ms", unit: "ms", description: "Synchronous rebuild latency for LNM cache");
|
||||
}
|
||||
|
||||
public void RecordHit(string? tenant, string source)
|
||||
{
|
||||
var tags = new TagList
|
||||
var tags = new KeyValuePair<string, object?>[]
|
||||
{
|
||||
{ "tenant", tenant ?? string.Empty },
|
||||
{ "source", source }
|
||||
new("tenant", tenant ?? string.Empty),
|
||||
new("source", source)
|
||||
};
|
||||
_hitTotal.Add(1, tags);
|
||||
}
|
||||
|
||||
public void RecordWrite(string? tenant, string source)
|
||||
{
|
||||
var tags = new TagList
|
||||
var tags = new KeyValuePair<string, object?>[]
|
||||
{
|
||||
{ "tenant", tenant ?? string.Empty },
|
||||
{ "source", source }
|
||||
new("tenant", tenant ?? string.Empty),
|
||||
new("source", source)
|
||||
};
|
||||
_writeTotal.Add(1, tags);
|
||||
}
|
||||
|
||||
public void RecordRebuild(string? tenant, string source, double elapsedMs)
|
||||
{
|
||||
var tags = new TagList
|
||||
var tags = new KeyValuePair<string, object?>[]
|
||||
{
|
||||
{ "tenant", tenant ?? string.Empty },
|
||||
{ "source", source }
|
||||
new("tenant", tenant ?? string.Empty),
|
||||
new("source", source)
|
||||
};
|
||||
_rebuildMs.Record(elapsedMs, tags);
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@ public class AdvisoryChunkBuilderTests
|
||||
|
||||
var options = new AdvisoryChunkBuildOptions(
|
||||
advisory.AdvisoryKey,
|
||||
fingerprint: "fp",
|
||||
chunkLimit: 5,
|
||||
observationLimit: 5,
|
||||
sectionFilter: ImmutableHashSet.Create("workaround"),
|
||||
formatFilter: ImmutableHashSet<string>.Empty,
|
||||
minimumLength: 1);
|
||||
"fp",
|
||||
5,
|
||||
5,
|
||||
ImmutableHashSet.Create("workaround"),
|
||||
ImmutableHashSet<string>.Empty,
|
||||
1);
|
||||
|
||||
var builder = new AdvisoryChunkBuilder(_hash);
|
||||
var result = builder.Build(options, advisory, new[] { observation });
|
||||
@@ -54,12 +54,12 @@ public class AdvisoryChunkBuilderTests
|
||||
|
||||
var options = new AdvisoryChunkBuildOptions(
|
||||
advisory.AdvisoryKey,
|
||||
fingerprint: "fp",
|
||||
chunkLimit: 5,
|
||||
observationLimit: 5,
|
||||
sectionFilter: ImmutableHashSet.Create("workaround"),
|
||||
formatFilter: ImmutableHashSet<string>.Empty,
|
||||
minimumLength: 1);
|
||||
"fp",
|
||||
5,
|
||||
5,
|
||||
ImmutableHashSet.Create("workaround"),
|
||||
ImmutableHashSet<string>.Empty,
|
||||
1);
|
||||
|
||||
var builder = new AdvisoryChunkBuilder(_hash);
|
||||
var result = builder.Build(options, advisory, new[] { observation });
|
||||
@@ -115,9 +115,9 @@ public class AdvisoryChunkBuilderTests
|
||||
fetchedAt: timestamp,
|
||||
receivedAt: timestamp,
|
||||
contentHash: "sha256:deadbeef",
|
||||
signature: new AdvisoryObservationSignature(present: false)),
|
||||
signature: new AdvisoryObservationSignature(present: false, format: null, keyId: null, signature: null)),
|
||||
content: new AdvisoryObservationContent("csaf", "2.0", JsonNode.Parse("{}")!),
|
||||
linkset: new AdvisoryObservationLinkset(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<AdvisoryObservationReference>()),
|
||||
linkset: new AdvisoryObservationLinkset(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<AdvisoryObservationReference>()),
|
||||
rawLinkset: new RawLinkset(),
|
||||
createdAt: timestamp);
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@ public class AdvisoryChunkCacheKeyTests
|
||||
public void Create_NormalizesObservationOrdering()
|
||||
{
|
||||
var options = new AdvisoryChunkBuildOptions(
|
||||
AdvisoryKey: "CVE-2025-0001",
|
||||
Fingerprint: "fp",
|
||||
ChunkLimit: 10,
|
||||
ObservationLimit: 10,
|
||||
SectionFilter: ImmutableHashSet.Create("workaround"),
|
||||
FormatFilter: ImmutableHashSet<string>.Empty,
|
||||
MinimumLength: 8);
|
||||
"CVE-2025-0001",
|
||||
"fp",
|
||||
10,
|
||||
10,
|
||||
ImmutableHashSet.Create("workaround"),
|
||||
ImmutableHashSet<string>.Empty,
|
||||
8);
|
||||
|
||||
var first = BuildObservation("obs-1", "sha256:one", "2025-11-18T00:00:00Z");
|
||||
var second = BuildObservation("obs-2", "sha256:two", "2025-11-18T00:05:00Z");
|
||||
@@ -29,7 +29,6 @@ public class AdvisoryChunkCacheKeyTests
|
||||
var reversed = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0001", options, new[] { second, first }, "fp");
|
||||
|
||||
Assert.Equal(ordered.Value, reversed.Value);
|
||||
Assert.Equal(ordered.ComputeHash(), reversed.ComputeHash());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -37,21 +36,21 @@ public class AdvisoryChunkCacheKeyTests
|
||||
{
|
||||
var optionsLower = new AdvisoryChunkBuildOptions(
|
||||
"CVE-2025-0002",
|
||||
Fingerprint: "fp",
|
||||
ChunkLimit: 5,
|
||||
ObservationLimit: 5,
|
||||
SectionFilter: ImmutableHashSet.Create("workaround", "fix"),
|
||||
FormatFilter: ImmutableHashSet.Create("ndjson"),
|
||||
MinimumLength: 1);
|
||||
"fp",
|
||||
5,
|
||||
5,
|
||||
ImmutableHashSet.Create("workaround", "fix"),
|
||||
ImmutableHashSet.Create("ndjson"),
|
||||
1);
|
||||
|
||||
var optionsUpper = new AdvisoryChunkBuildOptions(
|
||||
"CVE-2025-0002",
|
||||
Fingerprint: "fp",
|
||||
ChunkLimit: 5,
|
||||
ObservationLimit: 5,
|
||||
SectionFilter: ImmutableHashSet.Create("WorkAround", "FIX"),
|
||||
FormatFilter: ImmutableHashSet.Create("NDJSON"),
|
||||
MinimumLength: 1);
|
||||
"fp",
|
||||
5,
|
||||
5,
|
||||
ImmutableHashSet.Create("WorkAround", "FIX"),
|
||||
ImmutableHashSet.Create("NDJSON"),
|
||||
1);
|
||||
|
||||
var observation = BuildObservation("obs-3", "sha256:three", "2025-11-18T00:10:00Z");
|
||||
|
||||
@@ -59,7 +58,6 @@ public class AdvisoryChunkCacheKeyTests
|
||||
var upper = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0002", optionsUpper, new[] { observation }, "fp");
|
||||
|
||||
Assert.Equal(lower.Value, upper.Value);
|
||||
Assert.Equal(lower.ComputeHash(), upper.ComputeHash());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -67,12 +65,12 @@ public class AdvisoryChunkCacheKeyTests
|
||||
{
|
||||
var options = new AdvisoryChunkBuildOptions(
|
||||
"CVE-2025-0003",
|
||||
Fingerprint: "fp",
|
||||
ChunkLimit: 5,
|
||||
ObservationLimit: 5,
|
||||
SectionFilter: ImmutableHashSet<string>.Empty,
|
||||
FormatFilter: ImmutableHashSet<string>.Empty,
|
||||
MinimumLength: 1);
|
||||
"fp",
|
||||
5,
|
||||
5,
|
||||
ImmutableHashSet<string>.Empty,
|
||||
ImmutableHashSet<string>.Empty,
|
||||
1);
|
||||
|
||||
var original = BuildObservation("obs-4", "sha256:orig", "2025-11-18T00:15:00Z");
|
||||
var mutated = BuildObservation("obs-4", "sha256:mut", "2025-11-18T00:15:00Z");
|
||||
@@ -81,7 +79,6 @@ public class AdvisoryChunkCacheKeyTests
|
||||
var mutatedKey = AdvisoryChunkCacheKey.Create("tenant-a", "CVE-2025-0003", options, new[] { mutated }, "fp");
|
||||
|
||||
Assert.NotEqual(originalKey.Value, mutatedKey.Value);
|
||||
Assert.NotEqual(originalKey.ComputeHash(), mutatedKey.ComputeHash());
|
||||
}
|
||||
|
||||
private static AdvisoryObservation BuildObservation(string id, string contentHash, string timestamp)
|
||||
@@ -98,9 +95,9 @@ public class AdvisoryChunkCacheKeyTests
|
||||
fetchedAt: createdAt,
|
||||
receivedAt: createdAt,
|
||||
contentHash: contentHash,
|
||||
signature: new AdvisoryObservationSignature(false)),
|
||||
signature: new AdvisoryObservationSignature(false, null, null, null)),
|
||||
content: new AdvisoryObservationContent("csaf", "2.0", JsonNode.Parse("{}")!),
|
||||
linkset: new AdvisoryObservationLinkset(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<AdvisoryObservationReference>()),
|
||||
linkset: new AdvisoryObservationLinkset(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>(), Array.Empty<AdvisoryObservationReference>()),
|
||||
rawLinkset: new RawLinkset(),
|
||||
createdAt: createdAt);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Concelier.Core.Linksets;
|
||||
using StellaOps.Concelier.WebService.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Tests;
|
||||
|
||||
public class AdvisorySummaryMapperTests
|
||||
{
|
||||
[Fact]
|
||||
public void Maps_basic_fields()
|
||||
{
|
||||
var linkset = new AdvisoryLinkset(
|
||||
TenantId: "tenant-a",
|
||||
Source: "nvd",
|
||||
AdvisoryId: "CVE-2024-1234",
|
||||
ObservationIds: ImmutableArray.Create("obs1", "obs2"),
|
||||
Normalized: new AdvisoryLinksetNormalized(
|
||||
Purls: new[] { "pkg:maven/log4j/log4j@2.17.1" },
|
||||
Versions: null,
|
||||
Ranges: null,
|
||||
Severities: null),
|
||||
Provenance: null,
|
||||
Confidence: 0.8,
|
||||
Conflicts: new[]
|
||||
{
|
||||
new AdvisoryLinksetConflict("severity", "severity-mismatch", Array.Empty<string>(), new [] { "nvd", "vendor" })
|
||||
},
|
||||
CreatedAt: DateTimeOffset.UnixEpoch,
|
||||
BuiltByJobId: "job-123");
|
||||
|
||||
var summary = AdvisorySummaryMapper.ToSummary(linkset);
|
||||
|
||||
Assert.Equal("CVE-2024-1234", summary.AdvisoryKey);
|
||||
Assert.Equal("nvd", summary.Source);
|
||||
Assert.Equal(2, summary.Counts.Observations);
|
||||
Assert.Equal(1, summary.Counts.ConflictFields);
|
||||
Assert.NotNull(summary.Conflicts);
|
||||
Assert.Equal("job-123", summary.LinksetId);
|
||||
Assert.Equal("pkg:maven/log4j/log4j@2.17.1", Assert.Single(summary.Aliases));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Tests;
|
||||
|
||||
public class ConcelierHealthEndpointTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
|
||||
public ConcelierHealthEndpointTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_factory = factory.WithWebHostBuilder(_ => { });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Health_requires_tenant_header()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/obs/concelier/health");
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Health_returns_payload()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-a");
|
||||
|
||||
var response = await client.GetAsync("/obs/concelier/health");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<HealthResponse>();
|
||||
payload.Should().NotBeNull();
|
||||
payload!.tenant.Should().Be("tenant-a");
|
||||
payload.queueDepth.Should().Be(0);
|
||||
payload.window.Should().Be("5m");
|
||||
}
|
||||
|
||||
private sealed record HealthResponse(string tenant, int queueDepth, int ingestLatencyP50Ms, int ingestLatencyP99Ms, double errorRate1h, double sloBurnRate, string window, string updatedAt);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Tests;
|
||||
|
||||
public class ConcelierTimelineEndpointTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
|
||||
public ConcelierTimelineEndpointTests(WebApplicationFactory<Program> factory)
|
||||
{
|
||||
_factory = factory.WithWebHostBuilder(_ => { });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Timeline_requires_tenant_header()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/obs/concelier/timeline");
|
||||
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Timeline_returns_sse_event()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-a");
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "/obs/concelier/timeline");
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
|
||||
|
||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var stream = await response.Content.ReadAsStreamAsync();
|
||||
using var reader = new StreamReader(stream);
|
||||
var firstLine = await reader.ReadLineAsync();
|
||||
firstLine.Should().NotBeNull();
|
||||
firstLine!.Should().StartWith("event: ingest.update");
|
||||
}
|
||||
}
|
||||
@@ -37,18 +37,17 @@ public sealed class AdvisoryChunkBuilderTests
|
||||
var options = new AdvisoryChunkBuildOptions(
|
||||
advisory.AdvisoryKey,
|
||||
"fingerprint-1",
|
||||
chunkLimit: 5,
|
||||
observationLimit: 5,
|
||||
SectionFilter: ImmutableHashSet<string>.Empty,
|
||||
FormatFilter: ImmutableHashSet<string>.Empty,
|
||||
MinimumLength: 0);
|
||||
5,
|
||||
5,
|
||||
ImmutableHashSet<string>.Empty,
|
||||
ImmutableHashSet<string>.Empty,
|
||||
0);
|
||||
|
||||
var result = builder.Build(options, advisory, new[] { observation });
|
||||
|
||||
var entry = Assert.Single(result.Response.Entries);
|
||||
Assert.Equal("/references/0/title", entry.Provenance.ObservationPath);
|
||||
Assert.Equal(observation.ObservationId, entry.Provenance.DocumentId);
|
||||
Assert.Equal(observation.Upstream.ContentHash, entry.Provenance.ContentHash);
|
||||
Assert.Equal(new[] { "/references/0/title" }, entry.Provenance.FieldMask);
|
||||
Assert.Equal(ComputeChunkId(observation.ObservationId, "/references/0/title"), entry.ChunkId);
|
||||
}
|
||||
@@ -69,18 +68,17 @@ public sealed class AdvisoryChunkBuilderTests
|
||||
var options = new AdvisoryChunkBuildOptions(
|
||||
advisory.AdvisoryKey,
|
||||
"fingerprint-2",
|
||||
chunkLimit: 5,
|
||||
observationLimit: 5,
|
||||
SectionFilter: ImmutableHashSet<string>.Empty,
|
||||
FormatFilter: ImmutableHashSet<string>.Empty,
|
||||
MinimumLength: 0);
|
||||
5,
|
||||
5,
|
||||
ImmutableHashSet<string>.Empty,
|
||||
ImmutableHashSet<string>.Empty,
|
||||
0);
|
||||
|
||||
var result = builder.Build(options, advisory, new[] { observation });
|
||||
|
||||
var entry = Assert.Single(result.Response.Entries);
|
||||
Assert.Equal("/references/0", entry.Provenance.ObservationPath);
|
||||
Assert.Equal(observation.ObservationId, entry.Provenance.DocumentId);
|
||||
Assert.Equal(observation.Upstream.ContentHash, entry.Provenance.ContentHash);
|
||||
Assert.Equal(new[] { "/references/0" }, entry.Provenance.FieldMask);
|
||||
Assert.Equal(ComputeChunkId(observation.ObservationId, "/references/0"), entry.ChunkId);
|
||||
}
|
||||
|
||||
@@ -21,4 +21,6 @@
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Globalization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
@@ -60,6 +61,8 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
|
||||
private readonly ITestOutputHelper _output;
|
||||
private MongoDbRunner _runner = null!;
|
||||
private Process? _externalMongo;
|
||||
private string? _externalMongoDataPath;
|
||||
private ConcelierApplicationFactory _factory = null!;
|
||||
|
||||
public WebServiceEndpointsTests(ITestOutputHelper output)
|
||||
@@ -70,8 +73,15 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
PrepareMongoEnvironment();
|
||||
if (TryStartExternalMongo(out var externalConnectionString))
|
||||
{
|
||||
_factory = new ConcelierApplicationFactory(externalConnectionString);
|
||||
}
|
||||
else
|
||||
{
|
||||
_runner = MongoDbRunner.Start(singleNodeReplSet: true);
|
||||
_factory = new ConcelierApplicationFactory(_runner.ConnectionString);
|
||||
}
|
||||
WarmupFactory(_factory);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -79,7 +89,30 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_factory.Dispose();
|
||||
if (_externalMongo is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_externalMongo.HasExited)
|
||||
{
|
||||
_externalMongo.Kill(true);
|
||||
_externalMongo.WaitForExit(2000);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore cleanup errors in tests
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_externalMongoDataPath) && Directory.Exists(_externalMongoDataPath))
|
||||
{
|
||||
try { Directory.Delete(_externalMongoDataPath, recursive: true); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_runner.Dispose();
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -2605,6 +2638,7 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
Environment.SetEnvironmentVariable("MONGO2GO_CACHE_LOCATION", cacheDir);
|
||||
Environment.SetEnvironmentVariable("MONGO2GO_DOWNLOADS", cacheDir);
|
||||
Environment.SetEnvironmentVariable("MONGO2GO_MONGODB_VERSION", "4.4.4");
|
||||
Environment.SetEnvironmentVariable("MONGO2GO_MONGODB_PLATFORM", "linux");
|
||||
|
||||
var opensslPath = Path.Combine(repoRoot, "tests", "native", "openssl-1.1", "linux-x64");
|
||||
if (Directory.Exists(opensslPath))
|
||||
@@ -2616,13 +2650,53 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
}
|
||||
|
||||
// Also drop the OpenSSL libs next to the mongod binary Mongo2Go will spawn, in case LD_LIBRARY_PATH is ignored.
|
||||
var mongoBin = Directory.Exists(Path.Combine(repoRoot, ".nuget"))
|
||||
? Directory.GetFiles(Path.Combine(repoRoot, ".nuget", "packages", "mongo2go"), "mongod", SearchOption.AllDirectories)
|
||||
var repoNuget = Path.Combine(repoRoot, ".nuget", "packages", "mongo2go");
|
||||
var homeNuget = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages", "mongo2go");
|
||||
var mongoBin = Directory.Exists(repoNuget)
|
||||
? Directory.GetFiles(repoNuget, "mongod", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(path => path.Contains("mongodb-linux-4.4.4", StringComparison.OrdinalIgnoreCase))
|
||||
: null;
|
||||
|
||||
// Prefer globally cached Mongo2Go binaries if repo-local cache is missing.
|
||||
mongoBin ??= Directory.Exists(homeNuget)
|
||||
? Directory.GetFiles(homeNuget, "mongod", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(path => path.Contains("mongodb-linux-4.4.4", StringComparison.OrdinalIgnoreCase))
|
||||
: null;
|
||||
|
||||
if (mongoBin is not null && File.Exists(mongoBin) && Directory.Exists(opensslPath))
|
||||
{
|
||||
var binDir = Path.GetDirectoryName(mongoBin)!;
|
||||
|
||||
// Create a tiny wrapper so the loader always gets LD_LIBRARY_PATH even if vstest strips it.
|
||||
var wrapperPath = Path.Combine(cacheDir, "mongod-wrapper.sh");
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
var script = $"#!/usr/bin/env bash\nset -euo pipefail\nexport LD_LIBRARY_PATH=\"{opensslPath}:${{LD_LIBRARY_PATH:-}}\"\nexec \"{mongoBin}\" \"$@\"\n";
|
||||
File.WriteAllText(wrapperPath, script);
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
try
|
||||
{
|
||||
File.SetUnixFileMode(wrapperPath,
|
||||
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
|
||||
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
||||
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort; if not supported, chmod will fall back to default permissions.
|
||||
}
|
||||
}
|
||||
|
||||
// Force Mongo2Go to use the wrapper to avoid downloads and inject OpenSSL search path.
|
||||
Environment.SetEnvironmentVariable("MONGO2GO_MONGODB_BINARY", wrapperPath);
|
||||
|
||||
// Keep direct LD_LIBRARY_PATH/PATH hints for any code paths that still honour parent env.
|
||||
var existing = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH");
|
||||
var combined = string.IsNullOrEmpty(existing) ? binDir : $"{binDir}:{existing}";
|
||||
Environment.SetEnvironmentVariable("LD_LIBRARY_PATH", combined);
|
||||
Environment.SetEnvironmentVariable("PATH", $"{binDir}:{Environment.GetEnvironmentVariable("PATH")}");
|
||||
|
||||
foreach (var libName in new[] { "libssl.so.1.1", "libcrypto.so.1.1" })
|
||||
{
|
||||
var target = Path.Combine(binDir, libName);
|
||||
@@ -2632,17 +2706,154 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
File.Copy(source, target);
|
||||
}
|
||||
}
|
||||
|
||||
// If the Mongo2Go global cache is different from the first hit, add its bin dir too.
|
||||
var globalBin = Directory.Exists(homeNuget)
|
||||
? Directory.GetFiles(homeNuget, "mongod", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(path => path.Contains("mongodb-linux-4.4.4", StringComparison.OrdinalIgnoreCase))
|
||||
: null;
|
||||
if (globalBin is not null)
|
||||
{
|
||||
var globalDir = Path.GetDirectoryName(globalBin)!;
|
||||
var withGlobal = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH") ?? string.Empty;
|
||||
if (!withGlobal.Split(':', StringSplitOptions.RemoveEmptyEntries).Contains(globalDir))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("LD_LIBRARY_PATH", $"{globalDir}:{withGlobal}".TrimEnd(':'));
|
||||
}
|
||||
Environment.SetEnvironmentVariable("PATH", $"{globalDir}:{Environment.GetEnvironmentVariable("PATH")}");
|
||||
foreach (var libName in new[] { "libssl.so.1.1", "libcrypto.so.1.1" })
|
||||
{
|
||||
var target = Path.Combine(globalDir, libName);
|
||||
var source = Path.Combine(opensslPath, libName);
|
||||
if (File.Exists(source) && !File.Exists(target))
|
||||
{
|
||||
File.Copy(source, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryStartExternalMongo(out string? connectionString)
|
||||
{
|
||||
connectionString = null;
|
||||
|
||||
var repoRoot = FindRepoRoot();
|
||||
if (repoRoot is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var mongodCandidates = new List<string>();
|
||||
void AddCandidates(string root)
|
||||
{
|
||||
if (Directory.Exists(root))
|
||||
{
|
||||
mongodCandidates.AddRange(Directory.GetFiles(root, "mongod", SearchOption.AllDirectories)
|
||||
.Where(p => p.Contains("mongodb-linux-4.4.4", StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
}
|
||||
|
||||
AddCandidates(Path.Combine(repoRoot, ".nuget", "packages", "mongo2go"));
|
||||
AddCandidates(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages", "mongo2go"));
|
||||
|
||||
var mongodPath = mongodCandidates.FirstOrDefault();
|
||||
if (mongodPath is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var dataDir = Path.Combine(repoRoot, ".cache", "mongodb-local", $"manual-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(dataDir);
|
||||
|
||||
var opensslPath = Path.Combine(repoRoot, "tests", "native", "openssl-1.1", "linux-x64");
|
||||
var port = GetEphemeralPort();
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = mongodPath,
|
||||
ArgumentList =
|
||||
{
|
||||
"--dbpath", dataDir,
|
||||
"--bind_ip", "127.0.0.1",
|
||||
"--port", port.ToString(),
|
||||
"--nojournal",
|
||||
"--quiet",
|
||||
"--replSet", "rs0"
|
||||
},
|
||||
UseShellExecute = false,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true
|
||||
};
|
||||
|
||||
var existingLd = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH");
|
||||
var ldCombined = string.IsNullOrEmpty(existingLd) ? opensslPath : $"{opensslPath}:{existingLd}";
|
||||
psi.Environment["LD_LIBRARY_PATH"] = ldCombined;
|
||||
psi.Environment["PATH"] = $"{Path.GetDirectoryName(mongodPath)}:{Environment.GetEnvironmentVariable("PATH")}";
|
||||
|
||||
_externalMongo = Process.Start(psi);
|
||||
_externalMongoDataPath = dataDir;
|
||||
|
||||
if (_externalMongo is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Small ping loop to ensure mongod is ready
|
||||
var client = new MongoClient($"mongodb://127.0.0.1:{port}");
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
while (sw.Elapsed < TimeSpan.FromSeconds(5))
|
||||
{
|
||||
try
|
||||
{
|
||||
client.GetDatabase("admin").RunCommand<BsonDocument>("{ ping: 1 }");
|
||||
// Initiate single-node replica set so features expecting replset work.
|
||||
client.GetDatabase("admin").RunCommand<BsonDocument>(BsonDocument.Parse("{ replSetInitiate: { _id: \"rs0\", members: [ { _id: 0, host: \"127.0.0.1:" + port + "\" } ] } }"));
|
||||
// Wait for primary
|
||||
var readySw = System.Diagnostics.Stopwatch.StartNew();
|
||||
while (readySw.Elapsed < TimeSpan.FromSeconds(5))
|
||||
{
|
||||
var status = client.GetDatabase("admin").RunCommand<BsonDocument>(BsonDocument.Parse("{ replSetGetStatus: 1 }"));
|
||||
var myState = status["members"].AsBsonArray.FirstOrDefault(x => x["self"].AsBoolean);
|
||||
if (myState != null && myState["state"].ToInt32() == 1)
|
||||
{
|
||||
connectionString = $"mongodb://127.0.0.1:{port}/?replicaSet=rs0";
|
||||
return true;
|
||||
}
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
// fallback if primary not reached
|
||||
connectionString = $"mongodb://127.0.0.1:{port}";
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
try { _externalMongo.Kill(true); } catch { /* ignore */ }
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int GetEphemeralPort()
|
||||
{
|
||||
var listener = new System.Net.Sockets.TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
private static string? FindRepoRoot()
|
||||
{
|
||||
var current = AppContext.BaseDirectory;
|
||||
string? lastMatch = null;
|
||||
while (!string.IsNullOrEmpty(current))
|
||||
{
|
||||
if (File.Exists(Path.Combine(current, "Directory.Build.props")))
|
||||
{
|
||||
return current;
|
||||
lastMatch = current;
|
||||
}
|
||||
|
||||
var parent = Directory.GetParent(current);
|
||||
@@ -2654,7 +2865,7 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
current = parent.FullName;
|
||||
}
|
||||
|
||||
return null;
|
||||
return lastMatch;
|
||||
}
|
||||
|
||||
private static AdvisoryIngestRequest BuildAdvisoryIngestRequest(
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Contracts;
|
||||
|
||||
public sealed record GraphLinkoutsRequest(
|
||||
[property: JsonPropertyName("tenant")] string Tenant,
|
||||
[property: JsonPropertyName("purls")] IReadOnlyList<string> Purls,
|
||||
[property: JsonPropertyName("includeJustifications")] bool IncludeJustifications = false,
|
||||
[property: JsonPropertyName("includeProvenance")] bool IncludeProvenance = true);
|
||||
|
||||
public sealed record GraphLinkoutsResponse(
|
||||
[property: JsonPropertyName("items")] IReadOnlyList<GraphLinkoutItem> Items,
|
||||
[property: JsonPropertyName("notFound")] IReadOnlyList<string> NotFound);
|
||||
|
||||
public sealed record GraphLinkoutItem(
|
||||
[property: JsonPropertyName("purl")] string Purl,
|
||||
[property: JsonPropertyName("advisories")] IReadOnlyList<GraphLinkoutAdvisory> Advisories,
|
||||
[property: JsonPropertyName("conflicts")] IReadOnlyList<GraphLinkoutConflict> Conflicts,
|
||||
[property: JsonPropertyName("truncated")] bool Truncated = false,
|
||||
[property: JsonPropertyName("nextCursor")] string? NextCursor = null);
|
||||
|
||||
public sealed record GraphLinkoutAdvisory(
|
||||
[property: JsonPropertyName("advisoryId")] string AdvisoryId,
|
||||
[property: JsonPropertyName("source")] string Source,
|
||||
[property: JsonPropertyName("status")] string Status,
|
||||
[property: JsonPropertyName("justification")] string? Justification,
|
||||
[property: JsonPropertyName("modifiedAt")] DateTimeOffset ModifiedAt,
|
||||
[property: JsonPropertyName("evidenceHash")] string EvidenceHash,
|
||||
[property: JsonPropertyName("connectorId")] string ConnectorId,
|
||||
[property: JsonPropertyName("dsseEnvelopeHash")] string? DsseEnvelopeHash);
|
||||
|
||||
public sealed record GraphLinkoutConflict(
|
||||
[property: JsonPropertyName("source")] string Source,
|
||||
[property: JsonPropertyName("status")] string Status,
|
||||
[property: JsonPropertyName("justification")] string? Justification,
|
||||
[property: JsonPropertyName("observedAt")] DateTimeOffset ObservedAt,
|
||||
[property: JsonPropertyName("evidenceHash")] string EvidenceHash);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Contracts;
|
||||
|
||||
public sealed record VexConsoleStatementDto(
|
||||
string AdvisoryId,
|
||||
string ProductKey,
|
||||
string? Purl,
|
||||
string Status,
|
||||
string? Justification,
|
||||
string ProviderId,
|
||||
string ObservationId,
|
||||
DateTimeOffset CreatedAtUtc,
|
||||
IReadOnlyDictionary<string, string> Attributes);
|
||||
|
||||
public sealed record VexConsolePage(
|
||||
IReadOnlyList<VexConsoleStatementDto> Items,
|
||||
string? Cursor,
|
||||
bool HasMore,
|
||||
int Returned,
|
||||
IReadOnlyDictionary<string, int>? Counters = null);
|
||||
@@ -36,6 +36,8 @@ using StellaOps.Excititor.WebService.Contracts;
|
||||
using StellaOps.Excititor.WebService.Telemetry;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Bson;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using StellaOps.Excititor.WebService.Contracts;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var configuration = builder.Configuration;
|
||||
@@ -49,7 +51,11 @@ services.AddCsafNormalizer();
|
||||
services.AddCycloneDxNormalizer();
|
||||
services.AddOpenVexNormalizer();
|
||||
services.AddSingleton<IVexSignatureVerifier, NoopVexSignatureVerifier>();
|
||||
// TODO: replace NoopVexSignatureVerifier with hardened verifier once portable bundle signatures are finalized.
|
||||
services.AddSingleton<AirgapImportValidator>();
|
||||
services.AddSingleton<AirgapSignerTrustService>();
|
||||
services.AddSingleton<ConsoleTelemetry>();
|
||||
services.AddMemoryCache();
|
||||
services.AddScoped<IVexIngestOrchestrator, VexIngestOrchestrator>();
|
||||
services.AddOptions<ExcititorObservabilityOptions>()
|
||||
.Bind(configuration.GetSection("Excititor:Observability"));
|
||||
@@ -68,6 +74,7 @@ services.Configure<MirrorDistributionOptions>(configuration.GetSection(MirrorDis
|
||||
services.AddSingleton<MirrorRateLimiter>();
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
services.AddSingleton<IVexObservationProjectionService, VexObservationProjectionService>();
|
||||
services.AddScoped<IVexObservationQueryService, VexObservationQueryService>();
|
||||
|
||||
var rekorSection = configuration.GetSection("Excititor:Attestation:Rekor");
|
||||
if (rekorSection.Exists())
|
||||
@@ -140,6 +147,7 @@ app.MapHealthChecks("/excititor/health");
|
||||
|
||||
app.MapPost("/airgap/v1/vex/import", async (
|
||||
[FromServices] AirgapImportValidator validator,
|
||||
[FromServices] AirgapSignerTrustService trustService,
|
||||
[FromServices] IAirgapImportStore store,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
[FromBody] AirgapImportRequest request,
|
||||
@@ -160,6 +168,18 @@ app.MapPost("/airgap/v1/vex/import", async (
|
||||
});
|
||||
}
|
||||
|
||||
if (!trustService.Validate(request, out var trustCode, out var trustMessage))
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status403Forbidden, new
|
||||
{
|
||||
error = new
|
||||
{
|
||||
code = trustCode,
|
||||
message = trustMessage
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var record = new AirgapImportRecord
|
||||
{
|
||||
Id = $"{request.BundleId}:{request.MirrorGeneration}",
|
||||
@@ -174,7 +194,21 @@ app.MapPost("/airgap/v1/vex/import", async (
|
||||
ImportedAt = nowUtc
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await store.SaveAsync(record, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (DuplicateAirgapImportException dup)
|
||||
{
|
||||
return Results.Conflict(new
|
||||
{
|
||||
error = new
|
||||
{
|
||||
code = "AIRGAP_IMPORT_DUPLICATE",
|
||||
message = dup.Message
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Accepted($"/airgap/v1/vex/import/{request.BundleId}", new
|
||||
{
|
||||
@@ -296,6 +330,204 @@ app.MapPost("/excititor/admin/backfill-statements", async (
|
||||
});
|
||||
});
|
||||
|
||||
app.MapGet("/console/vex", async (
|
||||
HttpContext context,
|
||||
IOptions<VexMongoStorageOptions> storageOptions,
|
||||
IVexObservationQueryService queryService,
|
||||
ConsoleTelemetry telemetry,
|
||||
IMemoryCache cache,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: true, out var tenant, out var tenantError))
|
||||
{
|
||||
return tenantError;
|
||||
}
|
||||
|
||||
var query = context.Request.Query;
|
||||
var purls = query["purl"].Where(static v => !string.IsNullOrWhiteSpace(v)).Select(static v => v.Trim()).ToArray();
|
||||
var advisories = query["advisoryId"].Where(static v => !string.IsNullOrWhiteSpace(v)).Select(static v => v.Trim()).ToArray();
|
||||
var statuses = new List<VexClaimStatus>();
|
||||
if (query.TryGetValue("status", out var statusValues))
|
||||
{
|
||||
foreach (var statusValue in statusValues)
|
||||
{
|
||||
if (Enum.TryParse<VexClaimStatus>(statusValue, ignoreCase: true, out var parsed))
|
||||
{
|
||||
statuses.Add(parsed);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Results.BadRequest($"Unknown status '{statusValue}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var limit = query.TryGetValue("pageSize", out var pageSizeValues) && int.TryParse(pageSizeValues.FirstOrDefault(), out var pageSize)
|
||||
? pageSize
|
||||
: (int?)null;
|
||||
var cursor = query.TryGetValue("cursor", out var cursorValues) ? cursorValues.FirstOrDefault() : null;
|
||||
|
||||
telemetry.Requests.Add(1);
|
||||
|
||||
var cacheKey = $"console-vex:{tenant}:{string.Join(',', purls)}:{string.Join(',', advisories)}:{string.Join(',', statuses)}:{limit}:{cursor}";
|
||||
if (cache.TryGetValue(cacheKey, out VexConsolePage? cachedPage) && cachedPage is not null)
|
||||
{
|
||||
telemetry.CacheHits.Add(1);
|
||||
return Results.Ok(cachedPage);
|
||||
}
|
||||
telemetry.CacheMisses.Add(1);
|
||||
|
||||
var options = new VexObservationQueryOptions(
|
||||
tenant,
|
||||
observationIds: null,
|
||||
vulnerabilityIds: advisories,
|
||||
productKeys: null,
|
||||
purls: purls,
|
||||
cpes: null,
|
||||
providerIds: null,
|
||||
statuses: statuses,
|
||||
cursor: cursor,
|
||||
limit: limit);
|
||||
|
||||
VexObservationQueryResult result;
|
||||
try
|
||||
{
|
||||
result = await queryService.QueryAsync(options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
return Results.BadRequest(ex.Message);
|
||||
}
|
||||
|
||||
var statements = result.Observations
|
||||
.SelectMany(obs => obs.Statements.Select(stmt => new VexConsoleStatementDto(
|
||||
AdvisoryId: stmt.VulnerabilityId,
|
||||
ProductKey: stmt.ProductKey,
|
||||
Purl: stmt.Purl ?? obs.Linkset.Purls.FirstOrDefault(),
|
||||
Status: stmt.Status.ToString().ToLowerInvariant(),
|
||||
Justification: stmt.Justification?.ToString(),
|
||||
ProviderId: obs.ProviderId,
|
||||
ObservationId: obs.ObservationId,
|
||||
CreatedAtUtc: obs.CreatedAt,
|
||||
Attributes: obs.Attributes)))
|
||||
.ToList();
|
||||
|
||||
var statusCounts = result.Observations
|
||||
.GroupBy(o => o.Status.ToString().ToLowerInvariant())
|
||||
.ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var response = new VexConsolePage(
|
||||
Items: statements,
|
||||
Cursor: result.NextCursor,
|
||||
HasMore: result.HasMore,
|
||||
Returned: statements.Count,
|
||||
Counters: statusCounts);
|
||||
|
||||
cache.Set(cacheKey, response, TimeSpan.FromSeconds(30));
|
||||
|
||||
return Results.Ok(response);
|
||||
}).WithName("GetConsoleVex");
|
||||
|
||||
// Cartographer linkouts
|
||||
app.MapPost("/internal/graph/linkouts", async (
|
||||
GraphLinkoutsRequest request,
|
||||
IVexObservationQueryService queryService,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (request is null || string.IsNullOrWhiteSpace(request.Tenant))
|
||||
{
|
||||
return Results.BadRequest("tenant is required.");
|
||||
}
|
||||
|
||||
if (request.Purls is null || request.Purls.Count == 0 || request.Purls.Count > 500)
|
||||
{
|
||||
return Results.BadRequest("purls are required (1-500).");
|
||||
}
|
||||
|
||||
var normalizedPurls = request.Purls
|
||||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
||||
.Select(p => p.Trim().ToLowerInvariant())
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
if (normalizedPurls.Length == 0)
|
||||
{
|
||||
return Results.BadRequest("purls are required (1-500).");
|
||||
}
|
||||
|
||||
var options = new VexObservationQueryOptions(
|
||||
request.Tenant.Trim(),
|
||||
purls: normalizedPurls,
|
||||
includeJustifications: request.IncludeJustifications,
|
||||
includeProvenance: request.IncludeProvenance,
|
||||
limit: 200);
|
||||
|
||||
VexObservationQueryResult result;
|
||||
try
|
||||
{
|
||||
result = await queryService.QueryAsync(options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
return Results.BadRequest(ex.Message);
|
||||
}
|
||||
|
||||
var observationsByPurl = result.Observations
|
||||
.SelectMany(obs => obs.Linkset.Purls.Select(purl => (purl, obs)))
|
||||
.GroupBy(tuple => tuple.purl, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.Select(t => t.obs).ToArray(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var items = new List<GraphLinkoutItem>(normalizedPurls.Length);
|
||||
var notFound = new List<string>();
|
||||
|
||||
foreach (var inputPurl in normalizedPurls)
|
||||
{
|
||||
if (!observationsByPurl.TryGetValue(inputPurl, out var obsForPurl))
|
||||
{
|
||||
notFound.Add(inputPurl);
|
||||
continue;
|
||||
}
|
||||
|
||||
var advisories = obsForPurl
|
||||
.SelectMany(obs => obs.Statements.Select(stmt => new GraphLinkoutAdvisory(
|
||||
AdvisoryId: stmt.VulnerabilityId,
|
||||
Source: obs.ProviderId,
|
||||
Status: stmt.Status.ToString().ToLowerInvariant(),
|
||||
Justification: request.IncludeJustifications ? stmt.Justification?.ToString() : null,
|
||||
ModifiedAt: obs.CreatedAt,
|
||||
EvidenceHash: obs.Linkset.ReferenceHash,
|
||||
ConnectorId: obs.ProviderId,
|
||||
DsseEnvelopeHash: request.IncludeProvenance ? obs.Linkset.ReferenceHash : null)))
|
||||
.OrderBy(a => a.AdvisoryId, StringComparer.Ordinal)
|
||||
.ThenBy(a => a.Source, StringComparer.Ordinal)
|
||||
.Take(200)
|
||||
.ToList();
|
||||
|
||||
var conflicts = obsForPurl
|
||||
.Where(obs => obs.Statements.Any(s => s.Status == VexClaimStatus.Conflict))
|
||||
.SelectMany(obs => obs.Statements
|
||||
.Where(s => s.Status == VexClaimStatus.Conflict)
|
||||
.Select(stmt => new GraphLinkoutConflict(
|
||||
Source: obs.ProviderId,
|
||||
Status: stmt.Status.ToString().ToLowerInvariant(),
|
||||
Justification: request.IncludeJustifications ? stmt.Justification?.ToString() : null,
|
||||
ObservedAt: obs.CreatedAt,
|
||||
EvidenceHash: obs.Linkset.ReferenceHash)))
|
||||
.OrderBy(c => c.Source, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
items.Add(new GraphLinkoutItem(
|
||||
Purl: inputPurl,
|
||||
Advisories: advisories,
|
||||
Conflicts: conflicts,
|
||||
Truncated: advisories.Count >= 200,
|
||||
NextCursor: advisories.Count >= 200 ? $"{advisories[^1].AdvisoryId}:{advisories[^1].Source}" : null));
|
||||
}
|
||||
|
||||
var response = new GraphLinkoutsResponse(items, notFound);
|
||||
return Results.Ok(response);
|
||||
}).WithName("PostGraphLinkouts");
|
||||
|
||||
app.MapPost("/ingest/vex", async (
|
||||
HttpContext context,
|
||||
VexIngestRequest request,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StellaOps.Excititor.WebService.Tests")]
|
||||
[assembly: InternalsVisibleTo("StellaOps.Excititor.Core.UnitTests")]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using StellaOps.Excititor.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Services;
|
||||
@@ -8,6 +10,8 @@ namespace StellaOps.Excititor.WebService.Services;
|
||||
internal sealed class AirgapImportValidator
|
||||
{
|
||||
private static readonly TimeSpan AllowedSkew = TimeSpan.FromSeconds(5);
|
||||
private static readonly Regex Sha256Pattern = new(@"^sha256:[A-Fa-f0-9]{64}$", RegexOptions.Compiled);
|
||||
private static readonly Regex MirrorGenerationPattern = new(@"^[0-9]+$", RegexOptions.Compiled);
|
||||
|
||||
public IReadOnlyList<ValidationError> Validate(AirgapImportRequest request, DateTimeOffset nowUtc)
|
||||
{
|
||||
@@ -23,26 +27,46 @@ internal sealed class AirgapImportValidator
|
||||
{
|
||||
errors.Add(new ValidationError("bundle_id_missing", "bundleId is required."));
|
||||
}
|
||||
else if (request.BundleId.Length > 256)
|
||||
{
|
||||
errors.Add(new ValidationError("bundle_id_too_long", "bundleId must be <= 256 characters."));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.MirrorGeneration))
|
||||
{
|
||||
errors.Add(new ValidationError("mirror_generation_missing", "mirrorGeneration is required."));
|
||||
}
|
||||
else if (!MirrorGenerationPattern.IsMatch(request.MirrorGeneration))
|
||||
{
|
||||
errors.Add(new ValidationError("mirror_generation_invalid", "mirrorGeneration must be a numeric string."));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Publisher))
|
||||
{
|
||||
errors.Add(new ValidationError("publisher_missing", "publisher is required."));
|
||||
}
|
||||
else if (request.Publisher.Length > 256)
|
||||
{
|
||||
errors.Add(new ValidationError("publisher_too_long", "publisher must be <= 256 characters."));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.PayloadHash))
|
||||
{
|
||||
errors.Add(new ValidationError("payload_hash_missing", "payloadHash is required."));
|
||||
}
|
||||
else if (!Sha256Pattern.IsMatch(request.PayloadHash))
|
||||
{
|
||||
errors.Add(new ValidationError("payload_hash_invalid", "payloadHash must be sha256:<64-hex>."));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Signature))
|
||||
{
|
||||
errors.Add(new ValidationError("AIRGAP_SIGNATURE_MISSING", "signature is required for air-gapped imports."));
|
||||
}
|
||||
else if (!IsBase64(request.Signature))
|
||||
{
|
||||
errors.Add(new ValidationError("AIRGAP_SIGNATURE_INVALID", "signature must be base64-encoded."));
|
||||
}
|
||||
|
||||
if (request.SignedAt is null)
|
||||
{
|
||||
@@ -62,5 +86,22 @@ internal sealed class AirgapImportValidator
|
||||
return errors;
|
||||
}
|
||||
|
||||
private static bool IsBase64(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value) || value.Length % 4 != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
_ = Convert.FromBase64String(value);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct ValidationError(string Code, string Message);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Excititor.Connectors.Abstractions.Trust;
|
||||
using StellaOps.Excititor.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Services;
|
||||
|
||||
internal sealed class AirgapSignerTrustService
|
||||
{
|
||||
private readonly ILogger<AirgapSignerTrustService> _logger;
|
||||
private readonly string? _metadataPath;
|
||||
private ConnectorSignerMetadataSet? _metadata;
|
||||
|
||||
public AirgapSignerTrustService(ILogger<AirgapSignerTrustService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_metadataPath = Environment.GetEnvironmentVariable("STELLAOPS_CONNECTOR_SIGNER_METADATA_PATH");
|
||||
}
|
||||
|
||||
public bool Validate(AirgapImportRequest request, out string? errorCode, out string? message)
|
||||
{
|
||||
errorCode = null;
|
||||
message = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_metadataPath) || !File.Exists(_metadataPath))
|
||||
{
|
||||
_logger.LogDebug("Airgap signer metadata not configured; skipping trust enforcement.");
|
||||
return true;
|
||||
}
|
||||
|
||||
_metadata ??= ConnectorSignerMetadataLoader.TryLoad(_metadataPath);
|
||||
if (_metadata is null)
|
||||
{
|
||||
_logger.LogWarning("Failed to load airgap signer metadata from {Path}; allowing import.", _metadataPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Publisher))
|
||||
{
|
||||
errorCode = "AIRGAP_SOURCE_UNTRUSTED";
|
||||
message = "publisher is required for trust enforcement.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_metadata.TryGet(request.Publisher, out var connector))
|
||||
{
|
||||
errorCode = "AIRGAP_SOURCE_UNTRUSTED";
|
||||
message = $"Publisher '{request.Publisher}' is not present in trusted signer metadata.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (connector.Revoked)
|
||||
{
|
||||
errorCode = "AIRGAP_SOURCE_UNTRUSTED";
|
||||
message = $"Publisher '{request.Publisher}' is revoked.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (connector.Bundle?.Digest is { } digest && !string.IsNullOrWhiteSpace(digest))
|
||||
{
|
||||
if (!string.Equals(digest.Trim(), request.PayloadHash?.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
errorCode = "AIRGAP_PAYLOAD_MISMATCH";
|
||||
message = "Payload hash does not match trusted bundle digest.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Basic sanity: ensure at least one signer entry exists.
|
||||
if (connector.Signers.IsDefaultOrEmpty || connector.Signers.Sum(s => s.Fingerprints.Length) == 0)
|
||||
{
|
||||
errorCode = "AIRGAP_SOURCE_UNTRUSTED";
|
||||
message = $"Publisher '{request.Publisher}' has no trusted signers configured.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Excititor.Core/StellaOps.Excititor.Core.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Excititor.Storage.Mongo/StellaOps.Excititor.Storage.Mongo.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Excititor.Export/StellaOps.Excititor.Export.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Excititor.Connectors.Abstractions/StellaOps.Excititor.Connectors.Abstractions.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Excititor.Policy/StellaOps.Excititor.Policy.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Excititor.Attestation/StellaOps.Excititor.Attestation.csproj" />
|
||||
<ProjectReference Include="../__Libraries/StellaOps.Excititor.ArtifactStores.S3/StellaOps.Excititor.ArtifactStores.S3.csproj" />
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Telemetry;
|
||||
|
||||
internal sealed class ConsoleTelemetry
|
||||
{
|
||||
public const string MeterName = "StellaOps.Excititor.Console";
|
||||
|
||||
private static readonly Meter Meter = new(MeterName);
|
||||
|
||||
public Counter<long> Requests { get; } = Meter.CreateCounter<long>("console.vex.requests");
|
||||
public Counter<long> CacheHits { get; } = Meter.CreateCounter<long>("console.vex.cache_hits");
|
||||
public Counter<long> CacheMisses { get; } = Meter.CreateCounter<long>("console.vex.cache_misses");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Excititor.Worker.Options;
|
||||
|
||||
internal sealed class TenantAuthorityOptionsValidator : IValidateOptions<TenantAuthorityOptions>
|
||||
{
|
||||
public ValidateOptionsResult Validate(string? name, TenantAuthorityOptions options)
|
||||
{
|
||||
if (options is null)
|
||||
{
|
||||
return ValidateOptionsResult.Fail("TenantAuthorityOptions is required.");
|
||||
}
|
||||
|
||||
if (options.BaseUrls.Count == 0)
|
||||
{
|
||||
return ValidateOptionsResult.Fail("Excititor:Authority:BaseUrls must define at least one tenant endpoint.");
|
||||
}
|
||||
|
||||
foreach (var kvp in options.BaseUrls)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(kvp.Key) || string.IsNullOrWhiteSpace(kvp.Value))
|
||||
{
|
||||
return ValidateOptionsResult.Fail("Excititor:Authority:BaseUrls must include non-empty tenant keys and URLs.");
|
||||
}
|
||||
}
|
||||
|
||||
return ValidateOptionsResult.Success;
|
||||
}
|
||||
}
|
||||
@@ -23,12 +23,15 @@ using StellaOps.IssuerDirectory.Client;
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
var services = builder.Services;
|
||||
var configuration = builder.Configuration;
|
||||
var workerConfig = configuration.GetSection("Excititor:Worker");
|
||||
var workerConfigSnapshot = workerConfig.Get<VexWorkerOptions>() ?? new VexWorkerOptions();
|
||||
services.AddOptions<VexWorkerOptions>()
|
||||
.Bind(configuration.GetSection("Excititor:Worker"))
|
||||
.Bind(workerConfig)
|
||||
.ValidateOnStart();
|
||||
|
||||
services.Configure<VexWorkerPluginOptions>(configuration.GetSection("Excititor:Worker:Plugins"));
|
||||
services.Configure<TenantAuthorityOptions>(configuration.GetSection("Excititor:Authority"));
|
||||
services.AddSingleton<IValidateOptions<TenantAuthorityOptions>, TenantAuthorityOptionsValidator>();
|
||||
services.PostConfigure<VexWorkerOptions>(options =>
|
||||
{
|
||||
if (options.DisableConsensus)
|
||||
@@ -101,10 +104,13 @@ services.AddSingleton<PluginCatalog>(provider =>
|
||||
});
|
||||
|
||||
services.AddSingleton<IVexProviderRunner, DefaultVexProviderRunner>();
|
||||
services.AddHostedService<VexWorkerHostedService>();
|
||||
if (!workerConfigSnapshot.DisableConsensus)
|
||||
{
|
||||
services.AddSingleton<VexConsensusRefreshService>();
|
||||
services.AddSingleton<IVexConsensusRefreshScheduler>(static provider => provider.GetRequiredService<VexConsensusRefreshService>());
|
||||
services.AddHostedService<VexWorkerHostedService>();
|
||||
services.AddHostedService(static provider => provider.GetRequiredService<VexConsensusRefreshService>());
|
||||
}
|
||||
services.AddSingleton<ITenantAuthorityClientFactory, TenantAuthorityClientFactory>();
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Excititor.Core.Observations;
|
||||
|
||||
/// <summary>
|
||||
/// Builds deterministic linkset update events from raw VEX observations
|
||||
/// without introducing consensus or derived semantics (AOC-19-002).
|
||||
/// </summary>
|
||||
public sealed class VexLinksetExtractionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Groups observations by (vulnerabilityId, productKey) and emits a linkset update event
|
||||
/// for each group. Ordering is stable and case-insensitive on identifiers.
|
||||
/// </summary>
|
||||
public ImmutableArray<VexLinksetUpdatedEvent> Extract(
|
||||
string tenant,
|
||||
IEnumerable<VexObservation> observations,
|
||||
IEnumerable<VexObservationDisagreement>? disagreements = null)
|
||||
{
|
||||
if (observations is null)
|
||||
{
|
||||
return ImmutableArray<VexLinksetUpdatedEvent>.Empty;
|
||||
}
|
||||
|
||||
var observationList = observations
|
||||
.Where(o => o is not null)
|
||||
.ToList();
|
||||
|
||||
if (observationList.Count == 0)
|
||||
{
|
||||
return ImmutableArray<VexLinksetUpdatedEvent>.Empty;
|
||||
}
|
||||
|
||||
var groups = observationList
|
||||
.SelectMany(obs => obs.Statements.Select(stmt => (obs, stmt)))
|
||||
.GroupBy(x => new
|
||||
{
|
||||
VulnerabilityId = Normalize(x.stmt.VulnerabilityId),
|
||||
ProductKey = Normalize(x.stmt.ProductKey)
|
||||
})
|
||||
.OrderBy(g => g.Key.VulnerabilityId, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(g => g.Key.ProductKey, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var now = observationList.Max(o => o.CreatedAt);
|
||||
|
||||
var events = new List<VexLinksetUpdatedEvent>();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var linksetId = BuildLinksetId(group.Key.VulnerabilityId, group.Key.ProductKey);
|
||||
var obsForGroup = group.Select(x => x.obs);
|
||||
|
||||
var evt = VexLinksetUpdatedEventFactory.Create(
|
||||
tenant,
|
||||
linksetId,
|
||||
group.Key.VulnerabilityId,
|
||||
group.Key.ProductKey,
|
||||
obsForGroup,
|
||||
disagreements ?? Enumerable.Empty<VexObservationDisagreement>(),
|
||||
now);
|
||||
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
return events.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static string BuildLinksetId(string vulnerabilityId, string productKey)
|
||||
=> $"vex:{vulnerabilityId}:{productKey}".ToLowerInvariant();
|
||||
|
||||
private static string Normalize(string value) => VexObservation.EnsureNotNullOrWhiteSpace(value, nameof(value));
|
||||
}
|
||||
@@ -10,6 +10,19 @@ public interface IAirgapImportStore
|
||||
Task SaveAsync(AirgapImportRecord record, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class DuplicateAirgapImportException : Exception
|
||||
{
|
||||
public string BundleId { get; }
|
||||
public string MirrorGeneration { get; }
|
||||
|
||||
public DuplicateAirgapImportException(string bundleId, string mirrorGeneration, Exception inner)
|
||||
: base($"Airgap import already exists for bundle '{bundleId}' generation '{mirrorGeneration}'.", inner)
|
||||
{
|
||||
BundleId = bundleId;
|
||||
MirrorGeneration = mirrorGeneration;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MongoAirgapImportStore : IAirgapImportStore
|
||||
{
|
||||
private readonly IMongoCollection<AirgapImportRecord> _collection;
|
||||
@@ -19,11 +32,30 @@ internal sealed class MongoAirgapImportStore : IAirgapImportStore
|
||||
ArgumentNullException.ThrowIfNull(database);
|
||||
VexMongoMappingRegistry.Register();
|
||||
_collection = database.GetCollection<AirgapImportRecord>(VexMongoCollectionNames.AirgapImports);
|
||||
|
||||
// Enforce idempotency on (bundleId, generation) via Id uniqueness and explicit index.
|
||||
var idIndex = Builders<AirgapImportRecord>.IndexKeys.Ascending(x => x.Id);
|
||||
var bundleIndex = Builders<AirgapImportRecord>.IndexKeys
|
||||
.Ascending(x => x.BundleId)
|
||||
.Ascending(x => x.MirrorGeneration);
|
||||
|
||||
_collection.Indexes.CreateMany(new[]
|
||||
{
|
||||
new CreateIndexModel<AirgapImportRecord>(idIndex, new CreateIndexOptions { Unique = true, Name = "airgap_import_id_unique" }),
|
||||
new CreateIndexModel<AirgapImportRecord>(bundleIndex, new CreateIndexOptions { Unique = true, Name = "airgap_bundle_generation_unique" })
|
||||
});
|
||||
}
|
||||
|
||||
public Task SaveAsync(AirgapImportRecord record, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
try
|
||||
{
|
||||
return _collection.InsertOneAsync(record, cancellationToken: cancellationToken);
|
||||
}
|
||||
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
|
||||
{
|
||||
throw new DuplicateAirgapImportException(record.BundleId, record.MirrorGeneration, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,11 +124,6 @@ public sealed class MongoVexRawStore : IVexRawStore
|
||||
|
||||
var sessionHandle = session ?? await _sessionProvider.StartSessionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!useInline)
|
||||
{
|
||||
newGridId = await UploadToGridFsAsync(document, sessionHandle, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var supportsTransactions = sessionHandle.Client.Cluster.Description.Type != ClusterType.Standalone
|
||||
&& !sessionHandle.IsInTransaction;
|
||||
|
||||
@@ -183,6 +178,18 @@ public sealed class MongoVexRawStore : IVexRawStore
|
||||
IngestionTelemetry.RecordLatency(tenant, sourceVendor, IngestionTelemetry.PhaseFetch, fetchWatch.Elapsed);
|
||||
}
|
||||
|
||||
// Append-only: if the digest already exists, skip write
|
||||
if (existing is not null)
|
||||
{
|
||||
IngestionTelemetry.RecordWriteAttempt(tenant, sourceVendor, IngestionTelemetry.ResultNoop);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!useInline)
|
||||
{
|
||||
newGridId = await UploadToGridFsAsync(document, sessionHandle, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var record = VexRawDocumentRecord.FromDomain(document, includeContent: useInline);
|
||||
record.GridFsObjectId = useInline ? null : newGridId;
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<OutputType>Library</OutputType>
|
||||
<IsPackable>false</IsPackable>
|
||||
<UseAppHost>false</UseAppHost>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Excititor.Core/StellaOps.Excititor.Core.csproj" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" PrivateAssets="all" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Excititor.Storage.Mongo/StellaOps.Excititor.Storage.Mongo.csproj" />
|
||||
<ProjectReference Include="../../StellaOps.Excititor.WebService/StellaOps.Excititor.WebService.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user