diff --git a/docs/airgap/bundle-repositories.md b/docs/airgap/bundle-repositories.md new file mode 100644 index 000000000..bfe7a98ad --- /dev/null +++ b/docs/airgap/bundle-repositories.md @@ -0,0 +1,38 @@ +# Bundle Catalog & Items Repositories (prep for AIRGAP-IMP-57-001) + +## Scope +- Deterministic storage for offline bundle metadata with tenant isolation (RLS) and stable ordering. +- Ready for Mongo-backed implementation while providing in-memory deterministic reference behavior. + +## Schema (logical) +- `bundle_catalog`: + - `tenant_id` (string, PK part, RLS partition) + - `bundle_id` (string, PK part) + - `digest` (hex string) + - `imported_at_utc` (datetime) + - `content_paths` (array of strings, sorted ordinal) +- `bundle_items`: + - `tenant_id` (string, PK part, RLS partition) + - `bundle_id` (string, PK part) + - `path` (string, PK part) + - `digest` (hex string) + - `size_bytes` (long) + +## Implementation delivered (2025-11-20) +- In-memory repositories enforcing tenant isolation and deterministic ordering: + - `InMemoryBundleCatalogRepository` (upsert + list ordered by `bundle_id`). + - `InMemoryBundleItemRepository` (bulk upsert + list ordered by `path`). +- Models: `BundleCatalogEntry`, `BundleItem`. +- Tests cover upsert overwrite semantics, tenant isolation, and deterministic ordering (`tests/AirGap/StellaOps.AirGap.Importer.Tests/InMemoryBundleRepositoriesTests.cs`). + +## Migration notes (for Mongo/SQL backends) +- Create compound unique indexes on (`tenant_id`, `bundle_id`) for catalog; (`tenant_id`, `bundle_id`, `path`) for items. +- Enforce RLS by always scoping queries to `tenant_id` and validating it at repository boundary (as done in in-memory reference impl). +- Keep paths lowercased or use ordinal comparisons to avoid locale drift; sort before persistence to preserve determinism. + +## Next steps +- Implement Mongo-backed repositories mirroring the deterministic behavior and indexes above. +- Wire repositories into importer service/CLI once storage provider is selected. + +## Owners +- AirGap Importer Guild. diff --git a/docs/airgap/controller-scaffold.md b/docs/airgap/controller-scaffold.md new file mode 100644 index 000000000..8906d54e0 --- /dev/null +++ b/docs/airgap/controller-scaffold.md @@ -0,0 +1,45 @@ +# AirGap Controller Scaffold (Draft) — PREP-AIRGAP-CTL-56-001/002/57-001/57-002/58-001 + +Status: Draft (2025-11-20) +Owners: AirGap Controller Guild · Observability Guild · AirGap Time Guild · DevOps Guild +Scope: Define the baseline project skeleton, APIs, telemetry, and staleness fields needed to unblock controller tasks 56-001 through 58-001. + +## 1) Project layout +- Project: `src/AirGap/StellaOps.AirGap.Controller` (net10.0, minimal API host). +- Tests: `tests/AirGap/StellaOps.AirGap.Controller.Tests` with xunit + deterministic time provider. +- Shared contracts: DTOs under `Endpoints/Contracts`, domain state under `Domain/AirGapState.cs`. + +## 2) State model +- Persistent document `airgap_state` (Mongo): + - `id` (const `singleton`), `tenant_id`, `sealed` (bool), `policy_hash`, `time_anchor` (nullable), `last_transition_at` (UTC), `staleness_budget_seconds` (int?, optional per bundle), `notes`. + - Index on `{tenant_id}`; unique on `singleton` within tenant. +- In-memory cache with monotonic timestamp to avoid stale reads; cache invalidated on transitions. + +## 3) Endpoints (56-002 baseline) +- `GET /system/airgap/status` → returns current state + staleness summary: + - `{sealed, policy_hash, time_anchor:{source, anchored_at, drift_seconds}, staleness:{seconds_remaining?, budget_seconds?}, last_transition_at}`. +- `POST /system/airgap/seal` → body `{policy_hash, time_anchor?, staleness_budget_seconds?}`; requires Authority scopes `airgap:seal` + `effective:write`. +- `POST /system/airgap/unseal` → requires `airgap:seal`. +- Validation: reject seal if missing `policy_hash` or time anchor when platform requires sealed mode. + +## 4) Telemetry (57-002) +- Structured logs: `airgap.sealed`, `airgap.unsealed`, `airgap.status.read` with tenant_id, policy_hash, time_anchor_source, drift_seconds. +- Metrics (Prometheus/OpenTelemetry): counters `airgap_seal_total`, `airgap_unseal_total`; gauges `airgap_time_anchor_drift_seconds`, `airgap_staleness_budget_seconds`. +- Timeline events (Observability stream): `airgap.sealed`, `airgap.unsealed` with correlation_id. + +## 5) Staleness & time (58-001) +- Staleness computation: `drift_seconds = now_utc - time_anchor.anchored_at`; `seconds_remaining = max(0, staleness_budget_seconds - drift_seconds)`. +- Time anchors accept Roughtime or RFC3161 token parsed via AirGap Time component (imported service). +- Status response includes drift and remaining budget; sealed mode refuses to run if budget exceeded. + +## 6) Determinism & offline rules +- No external network calls; time source injected `IClock` seeded in tests. +- All timestamps RFC3339 UTC; responses sorted properties (serializer config). + +## 7) Open decisions +- Final scopes list (Authority) for status read vs seal/unseal. +- Whether to require dual authorization for `seal` (two-man rule) in sealed environments. +- Retention/rotation policy for `airgap_state` audit trail (append-only vs mutation). + +## 8) Handoff +This document satisfies PREP-AIRGAP-CTL-56-001 through 58-001. Update once Authority scopes and time-anchor token format are finalized; then promote to v1 schema doc and wire tests accordingly. diff --git a/docs/airgap/importer-scaffold.md b/docs/airgap/importer-scaffold.md new file mode 100644 index 000000000..a40bd0498 --- /dev/null +++ b/docs/airgap/importer-scaffold.md @@ -0,0 +1,38 @@ +# AirGap Importer Scaffold (prep for AIRGAP-IMP-56-001/56-002/58-002) + +## Scope for prep +- Provide minimal project and test scaffolds so downstream implementation can wire DSSE, TUF, Merkle validation, and audit logging without redoing structure. +- Capture trust-root inputs required (bundle path, signing keys, allowed algorithms, validity window). + +## What landed (2025-11-20) +- New project: `src/AirGap/StellaOps.AirGap.Importer/StellaOps.AirGap.Importer.csproj` (net10.0, deterministic-only dependencies). +- Planning layer: `BundleImportPlanner` emits deterministic plan steps and early validation reasons (`bundle-path-required`, `trust-roots-required`, `invalid-trust-window`). +- Contracts: `TrustRootConfig` record carries root bundle path, trusted key fingerprints, allowed algorithms, and optional validity window. +- Validation shape: `BundleValidationResult` centralises success/failure reasons for replay/capture. +- Tests: `tests/AirGap/StellaOps.AirGap.Importer.Tests` validate planner behavior without external feeds. + +## Updates (2025-11-20) +- Added DSSE verifier (RSA-PSS/SHA256) with PAE encoding + trusted key fingerprint checks. +- Added TUF metadata validator (root/snapshot/timestamp) with hash consistency guard. +- Added deterministic Merkle root calculator for bundle object staging. +- Expanded tests for DSSE, TUF, Merkle helpers. +- Added trust store + root rotation policy (dual approval) and import validator that coordinates DSSE/TUF/Merkle/rotation checks. + +## Next implementation hooks +- Replace placeholder plan with actual DSSE + TUF verifiers; keep step ordering stable. +- Feed trust roots from sealed-mode config and Evidence Locker bundles (once available) before allowing imports. +- Record audit trail for each plan step (success/failure) and a Merkle root of staged content. + +## Determinism/air-gap posture +- No network dependencies; only BCL used. +- Tests use cached local NuGet feed (`local-nugets/`). +- Plan steps are ordered list; do not reorder without bumping downstream replay expectations. + +## How to consume +```bash +# run tests offline once feed is hydrated +DOTNET_NOLOGO=1 dotnet test tests/AirGap/StellaOps.AirGap.Importer.Tests/StellaOps.AirGap.Importer.Tests.csproj --no-build +``` + +## Owners +- AirGap Importer Guild / Security Guild (per sprint 0510). diff --git a/docs/airgap/sealed-startup-diagnostics.md b/docs/airgap/sealed-startup-diagnostics.md new file mode 100644 index 000000000..8cd8cc336 --- /dev/null +++ b/docs/airgap/sealed-startup-diagnostics.md @@ -0,0 +1,32 @@ +# AirGap Sealed-Mode Startup Diagnostics (prep for AIRGAP-CTL-57-001/57-002) + +## Goal +Prevent services from running when sealed-mode requirements are unmet and emit auditable diagnostics + telemetry. + +## Pre-flight checks +1) `airgap_state` indicates `sealed=true`. +2) Egress allowlist configured (non-empty or explicitly `[]`). +3) Trust root bundle + TUF metadata present and unexpired. +4) Time anchor available (see `TimeAnchor` schema) and staleness budget not exceeded. +5) Pending root rotations either applied or flagged with approver IDs. + +## On failure +- Abort host startup with structured error code: `AIRGAP_STARTUP_MISSING_`. +- Emit structured log fields: `airgap.startup.check`, `status=failure`, `reason`, `bundlePath`, `trustRootVersion`, `timeAnchorDigest`. +- Increment counter `airgap_startup_blocked_total{reason}` and gauge `airgap_time_anchor_age_seconds` if anchor missing/stale. + +## Telemetry hooks +- Trace event `airgap.startup.validation` with attributes: `sealed`, `allowlist.count`, `trust_roots.count`, `time_anchor.age_seconds`, `rotation.pending`. +- Timeline events (for 57-002): `airgap.sealed` and `airgap.unsealed` include startup validation results and pending rotations. + +## Integration points +- Controller: run checks during `IHostApplicationLifetime.ApplicationStarted` before exposing endpoints. +- Importer: reuse `ImportValidator` to ensure bundles + trust rotation are valid before proceeding. +- Time component: provide anchor + staleness calculations to the controller checks. + +## Artefacts +- This document (deterministic guardrails for startup diagnostics). +- Code references: `src/AirGap/StellaOps.AirGap.Importer/Validation/*` for trust + bundle validation primitives; `src/AirGap/StellaOps.AirGap.Time/*` for anchors. + +## Owners +- AirGap Controller Guild · Observability Guild. diff --git a/docs/airgap/staleness-and-time.md b/docs/airgap/staleness-and-time.md index 74ab40cc4..3ad784c79 100644 --- a/docs/airgap/staleness-and-time.md +++ b/docs/airgap/staleness-and-time.md @@ -59,6 +59,7 @@ AirGap Time calculates drift = `now(monotonic) - anchor.issued_at` and exposes: - Ensure deterministic JSON serialization (UTC ISO-8601 timestamps, sorted keys). - Test vectors located under `src/AirGap/StellaOps.AirGap.Time/fixtures/`. - For offline testing, simulate monotonic clock via `ITestClock` to avoid system clock drift in CI. +- Staleness calculations use `StalenessCalculator` + `StalenessBudget`/`StalenessEvaluation` (see `src/AirGap/StellaOps.AirGap.Time/Services` and `.Models`); warning/breach thresholds must be non-negative and warning ≤ breach. ## 7. References diff --git a/docs/airgap/time-anchor-scaffold.md b/docs/airgap/time-anchor-scaffold.md new file mode 100644 index 000000000..9a77f0885 --- /dev/null +++ b/docs/airgap/time-anchor-scaffold.md @@ -0,0 +1,35 @@ +# AirGap Time Anchor Scaffold (prep for AIRGAP-TIME-57-001) + +## Scope for prep +- Provide a deterministic parsing surface for signed time tokens (Roughtime, RFC3161) so staleness calculations and telemetry wiring can start without full crypto yet. + +## What landed (2025-11-20) +- New project: `src/AirGap/StellaOps.AirGap.Time/StellaOps.AirGap.Time.csproj` (net10.0), BCL-only. +- Model: `TimeAnchor` canonical record (anchor time, source, format, signature fingerprint placeholder, token digest). +- Parser: `TimeTokenParser` with deterministic SHA-256 digest derivation and structured success/failure reasons. +- Result envelope: `TimeAnchorValidationResult` and `TimeTokenFormat` enum. +- Tests: `tests/AirGap/StellaOps.AirGap.Time.Tests` cover empty-token failure and digest production for Roughtime tokens. + +## Updates (2025-11-20) +- Added staleness calculator (`StalenessCalculator`) and budgets/evaluation models to derive warning/breach states deterministically. +- Added `TimeAnchorLoader` to ingest hex-encoded tokens from fixtures; sample tokens placed under `src/AirGap/StellaOps.AirGap.Time/fixtures/`. +- Added `TimeStatusService` + `InMemoryTimeAnchorStore` for per-tenant anchor/budget status + staleness; tests in `TimeStatusServiceTests`. +- Added verification pipeline (`TimeVerificationService`) with stub Roughtime/RFC3161 verifiers requiring trust roots; loader now verifies using trust roots. +- Added API surface `/api/v1/time/status` (plus POST `/api/v1/time/anchor`) via `TimeStatusController` and web host wiring. + +## Next implementation hooks +- Plug real Roughtime and RFC3161 decoders, verifying against trust roots supplied via sealed-mode config. +- Persist `TimeAnchor` rows under controller/importer once schema is final; emit telemetry counters/alerts. +- Replace placeholder signature fingerprint with actual signer fingerprint post-verification. + +## Determinism/air-gap posture +- Parser avoids wall-clock; anchor time derived deterministically from token digest until real parser is wired. +- No network calls; uses cached NuGet (`local-nugets/`) for tests. + +## How to consume +```bash +DOTNET_NOLOGO=1 dotnet test tests/AirGap/StellaOps.AirGap.Time.Tests/StellaOps.AirGap.Time.Tests.csproj --no-build +``` + +## Owners +- AirGap Time Guild (per sprint 0510). diff --git a/docs/airgap/time-anchor-verification-gap.md b/docs/airgap/time-anchor-verification-gap.md new file mode 100644 index 000000000..b964ebf15 --- /dev/null +++ b/docs/airgap/time-anchor-verification-gap.md @@ -0,0 +1,21 @@ +# Time Anchor Verification Gap (AIRGAP-TIME-57-001 follow-up) + +## Status (2025-11-20) +- Parser: stubbed for Roughtime/RFC3161 with deterministic digest + derived anchor time. +- Staleness: calculator + budgets landed; loader accepts hex fixtures. +- Verification: pipeline exists (`TimeVerificationService`) with stub verifiers; still needs real crypto using guild-provided trust roots. + +## What’s missing +- Roughtime parser: parse signed responses, extract `timestamp`, `radius`, `verifier` public key; verify signature. +- RFC3161 parser: decode ASN.1 TimeStampToken, verify signer chain against provided trust roots, extract nonce/ts. +- Trust roots: final format (JWK vs PEM) and key IDs to align with `TrustRootConfig`/Time service. + +## Proposed plan +1) Receive finalized token format + trust-root bundle from Time Guild. +2) Implement format-specific verifiers with validating tests using provided fixtures. +3) Expose `/api/v1/time/status` returning anchor metadata + staleness; wire telemetry counters/alerts per sealed diagnostics doc. + +## Owners +- AirGap Time Guild (format decision + trust roots) +- AirGap Importer Guild (bundle delivery of anchors) +- Observability Guild (telemetry wiring) diff --git a/docs/implplan/SPRINT_0110_0001_0001_ingestion_evidence.md b/docs/implplan/SPRINT_0110_0001_0001_ingestion_evidence.md index d823f13af..b6161e600 100644 --- a/docs/implplan/SPRINT_0110_0001_0001_ingestion_evidence.md +++ b/docs/implplan/SPRINT_0110_0001_0001_ingestion_evidence.md @@ -22,16 +22,16 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| P1 | PREP-CONCELIER-AIRGAP-56-001-58-001-AWAIT-MIR | TODO | Due 2025-11-21 · Accountable: Concelier Core · AirGap Guilds | Concelier Core · AirGap Guilds | Await Mirror thin-bundle milestone dates and evidence bundle artifacts for offline chain.

Document artefact/deliverable for CONCELIER-AIRGAP-56-001..58-001 and publish location so downstream tasks can proceed. | -| P2 | PREP-CONCELIER-CONSOLE-23-001-003-CONSOLE-SCH | TODO | Due 2025-11-21 · Accountable: Concelier Console Guild | Concelier Console Guild | Console schema samples not yet published alongside frozen LNM; need evidence bundle identifiers.

Document artefact/deliverable for CONCELIER-CONSOLE-23-001..003 and publish location so downstream tasks can proceed. | -| P3 | PREP-CONCELIER-ATTEST-73-001-002-EVIDENCE-LOC | TODO | Due 2025-11-21 · Accountable: Concelier Core · Evidence Locker Guild | Concelier Core · Evidence Locker Guild | Evidence Locker attestation scope sign-off still pending (due 2025-11-19).

Document artefact/deliverable for CONCELIER-ATTEST-73-001/002 and publish location so downstream tasks can proceed. | -| P4 | PREP-FEEDCONN-ICSCISA-02-012-KISA-02-008-FEED | TODO | Due 2025-11-21 · Accountable: Concelier Feed Owners | Concelier Feed Owners | Feed owner remediation plan.

Document artefact/deliverable for FEEDCONN-ICSCISA-02-012 / KISA-02-008 and publish location so downstream tasks can proceed. | +| P1 | PREP-CONCELIER-AIRGAP-56-001-58-001-AWAIT-MIR | DOING (2025-11-20) | Due 2025-11-21 · Accountable: Concelier Core · AirGap Guilds | Concelier Core · AirGap Guilds | Await Mirror thin-bundle milestone dates and evidence bundle artifacts for offline chain.

Document artefact/deliverable for CONCELIER-AIRGAP-56-001..58-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/concelier/prep/2025-11-20-airgap-56-001-58-001-prep.md`. | +| P2 | PREP-CONCELIER-CONSOLE-23-001-003-CONSOLE-SCH | DOING (2025-11-20) | Due 2025-11-21 · Accountable: Concelier Console Guild | Concelier Console Guild | Console schema samples not yet published alongside frozen LNM; need evidence bundle identifiers.

Document artefact/deliverable for CONCELIER-CONSOLE-23-001..003 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/concelier/prep/2025-11-20-console-23-001-prep.md`. | +| P3 | PREP-CONCELIER-ATTEST-73-001-002-EVIDENCE-LOC | DOING (2025-11-20) | Due 2025-11-21 · Accountable: Concelier Core · Evidence Locker Guild | Concelier Core · Evidence Locker Guild | Evidence Locker attestation scope sign-off still pending (due 2025-11-19).

Document artefact/deliverable for CONCELIER-ATTEST-73-001/002 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/concelier/prep/2025-11-20-attest-73-001-prep.md`. | +| P4 | PREP-FEEDCONN-ICSCISA-02-012-KISA-02-008-FEED | DOING (2025-11-20) | Due 2025-11-21 · Accountable: Concelier Feed Owners | Concelier Feed Owners | Feed owner remediation plan.

Document artefact/deliverable for FEEDCONN-ICSCISA-02-012 / KISA-02-008 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/concelier/prep/2025-11-20-feeds-icscisa-kisa-prep.md`. | | 0 | PREP-ART-56-001 | DONE (2025-11-19) | Due 2025-11-21 · Accountable: Mirror Creator Guild | Mirror Creator Guild | Milestone-0 thin bundle sample published at `out/mirror/thin/mirror-thin-m0-sample.tar.gz` (SHA256 `bd1013885a27f651e28331c7a240d417d265bd411d09b51b47bd7c2196659674`) with layout/commands documented in `docs/modules/mirror/milestone-0-thin-bundle.md`. | | 0.1 | PREP-EVIDENCE-BDL-01 | DONE (2025-11-19) | Due 2025-11-21 · Accountable: Evidence Locker Guild · Excititor Guild | Evidence Locker Guild · Excititor Guild | Evidence Bundle v1 contract published at `docs/modules/evidence-locker/evidence-bundle-v1.md` with sample tarball + hashes under `docs/samples/evidence-bundle/`; includes manifest schema, payload ordering, determinism rules, and transparency handling. | | 0.2 | PREP-CONSOLE-FIXTURES-29 | DONE (2025-11-19) | Due 2025-11-21 · Accountable: Console Guild · Docs Guild | Console Guild · Docs Guild | Console fixtures published at `docs/samples/console/console-vuln-29-001.json` and `docs/samples/console/console-vex-30-001.json`; hashes stored with CLI guardrail bundles under `out/console/guardrails/`. Final screenshots still depend on SBOM evidence. | | 0.3 | PREP-CHUNK-API-31 | DONE (2025-11-19) | Due 2025-11-21 · Accountable: Excititor Guild · Advisory AI Guild | Excititor Guild · Advisory AI Guild | Chunk API contract documented and sample NDJSON published at `docs/samples/excititor/chunks-sample.ndjson` (hash in `.sha256`); contract details in `docs/modules/excititor/evidence-contract.md`. | | 0.4 | PREP-ATTEST-SCOPE-73 | DONE (2025-11-19) | Due 2025-11-21 · Accountable: Evidence Locker Guild · Concelier Guild | Evidence Locker Guild · Concelier Guild | Attestation scope note published at `docs/modules/evidence-locker/attestation-scope-note.md` with required claims + builder example; transparency/offline guidance included. | -| 0.5 | PREP-CONN-METADATA-01 | TODO | Due 2025-11-21 · Accountable: Excititor Connectors Guild | Excititor Connectors Guild | Publish connector signer metadata schema (fingerprints, issuer tiers, bundle references) for MSRC/Oracle/Ubuntu/Stella connectors.

Provide JSON schema, migration guidance, and sample records to align trust enrichment across connectors. | +| 0.5 | PREP-CONN-METADATA-01 | DONE (2025-11-20) | Due 2025-11-21 · Accountable: Excititor Connectors Guild | Excititor Connectors Guild | Publish connector signer metadata schema (fingerprints, issuer tiers, bundle references) for MSRC/Oracle/Ubuntu/Stella connectors.

Provide JSON schema, migration guidance, and sample records to align trust enrichment across connectors. | | 0.6 | PREP-BUILD-HARNESS-110 | DONE (2025-11-19) | Due 2025-11-21 · Accountable: Concelier Build/Tooling Guild | Concelier Build/Tooling Guild | Added runner profile `tools/linksets-ci.sh` using `tools/dotnet-filter.sh` with no `workdir:` injection, AppDomain disabled, and deterministic `ResultsDirectory`; documented invocation and cache expectations to unblock `/linksets` tests in CI. | | 0.7 | PREP-FEEDCONN-ICS-KISA-PLAN | DONE (2025-11-19) | Due 2025-11-21 · Accountable: Concelier Feed Owners · Product Advisory Guild | Concelier Feed Owners · Product Advisory Guild | Remediation/runbook plan published at `docs/modules/concelier/feeds/icscisa-kisa.md` with cadence, backlog cleanup, normalized fields, owners, and review date; provenance note at `docs/modules/concelier/feeds/icscisa-kisa-provenance.md`. | | 1 | DOCS-AIAI-31-004 | TODO | CONSOLE-VULN-29-001; CONSOLE-VEX-30-001; SBOM-AIAI-31-003 | Docs Guild · Console Guild | Guardrail console doc; fixtures published at `docs/samples/console/console-vuln-29-001.json` and `docs/samples/console/console-vex-30-001.json`; awaiting SBOM evidence for final screenshots. | @@ -61,6 +61,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Published prep docs for CONCELIER airgap/console/attest feeds; moved PREP P1–P4 to DOING after confirming unowned. | Project Mgmt | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-19 | Reconfirmed SBOM-AIAI-31-003, DOCS-AIAI-31-005/006/008/009, CONCELIER air-gap/console/attest, and FEEDCONN-ICSCISA/KISA tracks remain BLOCKED pending CLI-VULN/CLI-VEX artefacts, Evidence Locker attestation scope, console fixtures, mirror thin bundle, and feed remediation plan (PREP-FEEDCONN-ICS-KISA-PLAN). | Project Mgmt | | 2025-11-19 | Completed PREP-FEEDCONN-ICS-KISA-PLAN: published remediation/runbook plan and provenance note under `docs/modules/concelier/feeds/`; FEEDCONN-ICSCISA-02-012 / KISA-02-008 may proceed once remediation runs start. | Implementer | @@ -69,6 +70,7 @@ | 2025-11-20 | Updated DOCS-AIAI-31-004 with publication-readiness checklist, fixtures, and evidence bundle links; still waiting on SBOM-AIAI-31-003 + live console endpoints for screenshots. | Implementer | | 2025-11-20 | Retried `tools/linksets-ci.sh`; tests now discover but Mongo2Go fails to start (missing `libcrypto.so.1.1` in runner). Aborted at 16s; BUILD-TOOLING-110-001 remains BLOCKED pending runner with OpenSSL 1.1 libs. | Implementer | | 2025-11-20 | Added Mongo linkset collection registration and updated `tools/linksets-ci.sh` to seed `LD_LIBRARY_PATH` with vendored OpenSSL 1.1. Rerun: mongod starts, tests execute but fail on missing `IMongoCollection` before the registration fix; rerun in CI should proceed further. | Implementer | +| 2025-11-20 | Completed PREP-CONN-METADATA-01: published signer metadata schema (`docs/modules/excititor/schemas/connector-signer-metadata.schema.json`), guidance (`docs/modules/excititor/connectors/connector-signer-metadata.md`), and sample + hash (`docs/samples/excititor/connector-signer-metadata-sample.json[.sha256]`). | Implementer | | 2025-11-19 | Retried `dotnet test ... --filter Linksets` (with/without restore, TRX, blame timeout); builds succeed but vstest still discovers zero Linksets tests and emits no TRX. BUILD-TOOLING-110-001 remains BLOCKED; requires CI agent with working test discovery. | Implementer | | 2025-11-19 | Packaged `StellaOps.Policy.AuthSignals` 0.1.0-alpha into `local-nugets/` for CONCELIER/POLICY/EXCITITOR consumers. | Implementer | | 2025-11-19 | Published console fixtures (`docs/samples/console/console-vuln-29-001.json`, `console-vex-30-001.json`) so DOCS-AIAI-31-004 can proceed while awaiting SBOM evidence. | Implementer | diff --git a/docs/implplan/SPRINT_0116_0001_0005_concelier_v.md b/docs/implplan/SPRINT_0116_0001_0005_concelier_v.md index 2d6c60840..1edd0d69a 100644 --- a/docs/implplan/SPRINT_0116_0001_0005_concelier_v.md +++ b/docs/implplan/SPRINT_0116_0001_0005_concelier_v.md @@ -20,7 +20,7 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| P1 | PREP-CONCELIER-WEB-AIRGAP-57-001-DEPENDS-ON-5 | TODO | Due 2025-11-21 · Accountable: Concelier WebService Guild · AirGap Policy Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Concelier WebService Guild · AirGap Policy Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Depends on 56-002.

Document artefact/deliverable for CONCELIER-WEB-AIRGAP-57-001 and publish location so downstream tasks can proceed. | +| P1 | PREP-CONCELIER-WEB-AIRGAP-57-001-DEPENDS-ON-5 | DOING (2025-11-20) | Due 2025-11-21 · Accountable: Concelier WebService Guild · AirGap Policy Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Concelier WebService Guild · AirGap Policy Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Depends on 56-002.

Document artefact/deliverable for CONCELIER-WEB-AIRGAP-57-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/concelier/prep/2025-11-20-web-airgap-57-001-prep.md`. | | 1 | CONCELIER-VULN-29-004 | TODO | Depends on CONCELIER-VULN-29-001 | Concelier WebService Guild · Observability Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Instrument observation/linkset pipelines with metrics for identifier collisions, withdrawn statements, chunk latencies; stream to Vuln Explorer without altering payloads. | | 2 | CONCELIER-WEB-AIRGAP-56-001 | TODO | Start of AirGap chain | Concelier WebService Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Extend ingestion endpoints to register mirror bundle sources, expose bundle catalogs, enforce sealed-mode by blocking direct internet feeds. | | 3 | CONCELIER-WEB-AIRGAP-56-002 | TODO | Depends on 56-001 | Concelier WebService Guild (`src/Concelier/StellaOps.Concelier.WebService`) | Add staleness + bundle provenance metadata to `/advisories/observations` and `/advisories/linksets`; operators see freshness without Excititor-derived outcomes. | @@ -40,6 +40,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Moved PREP-CONCELIER-WEB-AIRGAP-57-001 to DOING after confirming unowned; published prep doc at `docs/modules/concelier/prep/2025-11-20-web-airgap-57-001-prep.md`. | Project Mgmt | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 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 | @@ -48,6 +49,7 @@ - AirGap sealed-mode enforcement must precede staleness surfaces/timeline events to avoid leaking non-mirror sources. - AOC regression fixes are required before large-batch ingest verification; failing to align allowlist/auth configs risks false negatives in tests. - Standardized error envelope is prerequisite for SDK/doc alignment; delays block developer portal updates. + - PREP-CONCELIER-WEB-AIRGAP-57-001 prep doc published at `docs/modules/concelier/prep/2025-11-20-web-airgap-57-001-prep.md`; awaits sealed-mode/staleness inputs from WEB-AIRGAP-56-002 and error envelope standard (WEB-OAS-61-002). ## Next Checkpoints - Plan sealed-mode remediation payload review once WEB-AIRGAP-56-002 is drafted (date TBD). @@ -59,4 +61,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. | \ No newline at end of file +| Observability base (WEB-OBS-50-001) | Tasks 14–15 | Concelier WebService | Upstream dependency for health/timeline surfaces. | diff --git a/docs/implplan/SPRINT_0119_0001_0001_excititor_i.md b/docs/implplan/SPRINT_0119_0001_0001_excititor_i.md index 295ed26db..cd7e77a9f 100644 --- a/docs/implplan/SPRINT_0119_0001_0001_excititor_i.md +++ b/docs/implplan/SPRINT_0119_0001_0001_excititor_i.md @@ -25,8 +25,8 @@ | P1 | PREP-EXCITITOR-AIRGAP-56-001-WAITING-ON-EXPOR | BLOCKED | Due 2025-11-21 · Accountable: Excititor Core Guild | Excititor Core Guild | Waiting on Export Center mirror bundle schema (Sprint 162) to define ingestion shape.

Document artefact/deliverable for EXCITITOR-AIRGAP-56-001 and publish location so downstream tasks can proceed. | | P2 | PREP-EXCITITOR-AIRGAP-57-001-BLOCKED-ON-56-00 | BLOCKED | Due 2025-11-21 · Accountable: Excititor Core Guild · AirGap Policy Guild | Excititor Core Guild · AirGap Policy Guild | Blocked on 56-001 schema; sealed-mode error catalog pending.

Document artefact/deliverable for EXCITITOR-AIRGAP-57-001 and publish location so downstream tasks can proceed. | | P3 | PREP-EXCITITOR-AIRGAP-58-001-DEPENDS-ON-57-00 | BLOCKED | Due 2025-11-21 · Accountable: Excititor Core Guild · Evidence Locker Guild | Excititor Core Guild · Evidence Locker Guild | Depends on 57-001 plus EvidenceLocker portable format (160/161).

Document artefact/deliverable for EXCITITOR-AIRGAP-58-001 and publish location so downstream tasks can proceed. | -| P4 | PREP-EXCITITOR-CONN-TRUST-01-001-CONNECTOR-SI | BLOCKED | Due 2025-11-21 · Accountable: Excititor Connectors Guild | Excititor Connectors Guild | Connector signer metadata schema still unpublished post-2025-11-14 review.

Document artefact/deliverable for EXCITITOR-CONN-TRUST-01-001 and publish location so downstream tasks can proceed. | -| P5 | PREP-ATTESTATION-VERIFIER-REHEARSAL-EXCITITOR | TODO | Due 2025-11-21 · Accountable: Planning | Planning | If issues persist, log BLOCKED status in attestation plan and re-forecast completion.

Document artefact/deliverable for Attestation verifier rehearsal (Excititor Attestation Guild) and publish location so downstream tasks can proceed. | +| P4 | PREP-EXCITITOR-CONN-TRUST-01-001-CONNECTOR-SI | DONE (2025-11-20) | Due 2025-11-21 · Accountable: Excititor Connectors Guild | Excititor Connectors Guild | Connector signer metadata schema and samples published.

Artefacts: schema (`docs/modules/excititor/schemas/connector-signer-metadata.schema.json`), guidance (`docs/modules/excititor/connectors/connector-signer-metadata.md`), sample + hash (`docs/samples/excititor/connector-signer-metadata-sample.json[.sha256]`). | +| P5 | PREP-ATTESTATION-VERIFIER-REHEARSAL-EXCITITOR | DOING (2025-11-20) | Due 2025-11-21 · Accountable: Planning | Planning | If issues persist, log BLOCKED status in attestation plan and re-forecast completion.

Document artefact/deliverable for Attestation verifier rehearsal (Excititor Attestation Guild) and publish location so downstream tasks can proceed. | | 1 | EXCITITOR-AIAI-31-001 | DONE (2025-11-12) | Available to Advisory AI; monitor usage. | Excititor WebService Guild | Expose normalized VEX justifications, scope trees, and anchors via `VexObservation` projections so Advisory AI can cite raw evidence without consensus logic. | | 2 | EXCITITOR-AIAI-31-002 | DONE (2025-11-17) | Start `/vex/evidence/chunks`; reuse 31-001 outputs. | Excititor WebService Guild | Stream raw statements + signature metadata with tenant/policy filters for RAG clients; aggregation-only, reference observation/linkset IDs. | | 3 | EXCITITOR-AIAI-31-003 | DONE (2025-11-17) | Counters/logs-only path delivered; traces remain follow-on once span sink is available. | Excititor WebService Guild · Observability Guild | Instrument evidence APIs with request counters, chunk histograms, signature-failure + AOC guard-violation meters. | @@ -37,7 +37,7 @@ | 8 | EXCITITOR-ATTEST-01-003 | DONE (2025-11-17) | Complete verifier harness + diagnostics. | Excititor Attestation Guild | Finish `IVexAttestationVerifier`, wire structured diagnostics/metrics, and prove DSSE bundle verification without touching consensus results. | | 9 | EXCITITOR-ATTEST-73-001 | DONE (2025-11-17) | Implemented payload spec and storage. | Excititor Core · Attestation Payloads Guild | Emit attestation payloads capturing supplier identity, justification summary, and scope metadata for trust chaining. | | 10 | EXCITITOR-ATTEST-73-002 | DONE (2025-11-17) | Implemented linkage API. | Excititor Core Guild | Provide APIs linking attestation IDs back to observation/linkset/product tuples for provenance citations without derived verdicts. | -| 11 | EXCITITOR-CONN-TRUST-01-001 | BLOCKED | PREP-EXCITITOR-CONN-TRUST-01-001-CONNECTOR-SI | Excititor Connectors Guild | Add signer fingerprints, issuer tiers, and bundle references to MSRC/Oracle/Ubuntu/Stella connectors; document consumer guidance. | +| 11 | EXCITITOR-CONN-TRUST-01-001 | DONE (2025-11-20) | PREP-EXCITITOR-CONN-TRUST-01-001-CONNECTOR-SI | Excititor Connectors Guild | Add signer fingerprints, issuer tiers, and bundle references to MSRC/Oracle/Ubuntu/Stella connectors; document consumer guidance. | ### Task Clusters & Readiness - **Advisory-AI evidence APIs:** 31-001 delivered; 31-003 instrumentation and 31-004 docs pending; ready to start once examples and telemetry fixtures finalize. @@ -68,10 +68,15 @@ | 2025-11-17 | Added chunk request/response telemetry + signature status counters; `/v1/vex/evidence/chunks` now emits metrics without traces. | WebService Guild | | 2025-11-14 | Published `docs/modules/excititor/operations/observability.md` covering new evidence metrics for Ops/Lens dashboards. | Observability Guild | | 2025-11-16 | Normalized sprint file to standard template, renamed to SPRINT_0119_0001_0001_excititor_i.md, and updated tasks-all references. | Planning | +| 2025-11-20 | Started PREP-ATTESTATION-VERIFIER-REHEARSAL-EXCITITOR (status → DOING) after confirming no existing DOING/DONE owner entries. | Planning | | 2025-11-17 | Implemented `/v1/vex/evidence/chunks` NDJSON endpoint and wired DI for chunk service; marked 31-002 DONE. | WebService Guild | | 2025-11-17 | Closed attestation verifier + payload/link API (01-003, 73-001, 73-002); WebService/Worker builds green. | Attestation/Core Guild | | 2025-11-18 | Marked AirGap 56/57/58 and connector trust 01-001 BLOCKED pending mirror schema, sealed-mode errors, portable format, and signer metadata schema. | Implementer | | 2025-11-18 | Authored Advisory-AI evidence contract doc (`docs/modules/excititor/evidence-contract.md`) covering `/v1/vex/evidence/chunks`, schema, determinism, AOC, telemetry; 31-004 doc deliverable ready. | Implementer | +| 2025-11-20 | Completed PREP-EXCITITOR-CONN-TRUST-01-001: published connector signer metadata schema, guidance, and sample bundle hash to unblock connector trust rollout. | Implementer | +| 2025-11-20 | Started EXCITITOR-CONN-TRUST-01-001 (status → DOING); adding loader/enricher for signer metadata and preparing connector wiring. | Implementer | +| 2025-11-20 | Completed EXCITITOR-CONN-TRUST-01-001: loader/enricher wired into MSRC/Oracle/Ubuntu/OpenVEX connectors; env var `STELLAOPS_CONNECTOR_SIGNER_METADATA_PATH`; tests added for MSRC/Ubuntu/OpenVEX provenance enrichment. | Implementer | +| 2025-11-20 | Implemented connector signer metadata loader/enricher with env var `STELLAOPS_CONNECTOR_SIGNER_METADATA_PATH`; plumbed provenance enrichment into MSRC/Oracle/Ubuntu/OpenVEX connectors. | Implementer | ## Decisions & Risks - **Decisions** diff --git a/docs/implplan/SPRINT_0121_0001_0001_policy_reasoning.md b/docs/implplan/SPRINT_0121_0001_0001_policy_reasoning.md index 8627be9e1..f7916b1d8 100644 --- a/docs/implplan/SPRINT_0121_0001_0001_policy_reasoning.md +++ b/docs/implplan/SPRINT_0121_0001_0001_policy_reasoning.md @@ -30,11 +30,11 @@ | P3 | PREP-LEDGER-OAS-61-002-DEPENDS-ON-61-001-CONT | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Findings Ledger Guild / src/Findings/StellaOps.Findings.Ledger | Findings Ledger Guild / src/Findings/StellaOps.Findings.Ledger | Depends on 61-001 contract + HTTP surface.

Document artefact/deliverable for LEDGER-OAS-61-002 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/findings-ledger/prep/2025-11-20-ledger-oas-prep.md`. | | P4 | PREP-LEDGER-OAS-62-001-SDK-GENERATION-PENDING | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Findings Ledger Guild; SDK Generator Guild / src/Findings/StellaOps.Findings.Ledger | Findings Ledger Guild; SDK Generator Guild / src/Findings/StellaOps.Findings.Ledger | SDK generation pending 61-002.

Document artefact/deliverable for LEDGER-OAS-62-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/findings-ledger/prep/2025-11-20-ledger-oas-prep.md`. | | P5 | PREP-LEDGER-OAS-63-001-DEPENDENT-ON-SDK-VALID | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Findings Ledger Guild; API Governance Guild / src/Findings/StellaOps.Findings.Ledger | Findings Ledger Guild; API Governance Guild / src/Findings/StellaOps.Findings.Ledger | Dependent on SDK validation (62-001).

Document artefact/deliverable for LEDGER-OAS-63-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/findings-ledger/prep/2025-11-20-ledger-oas-prep.md`. | -| P6 | PREP-LEDGER-OBS-54-001-NO-HTTP-SURFACE-MINIMA | TODO | Due 2025-11-22 · Accountable: Findings Ledger Guild; Provenance Guild / src/Findings/StellaOps.Findings.Ledger | Findings Ledger Guild; Provenance Guild / src/Findings/StellaOps.Findings.Ledger | No HTTP surface/minimal API present in module to host `/ledger/attestations`; needs API contract + service scaffold.

Document artefact/deliverable for LEDGER-OBS-54-001 and publish location so downstream tasks can proceed. | +| P6 | PREP-LEDGER-OBS-54-001-NO-HTTP-SURFACE-MINIMA | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Findings Ledger Guild; Provenance Guild / src/Findings/StellaOps.Findings.Ledger | Findings Ledger Guild; Provenance Guild / src/Findings/StellaOps.Findings.Ledger | No HTTP surface/minimal API present in module to host `/ledger/attestations`; needs API contract + service scaffold.

Prep artefact now available: `docs/modules/findings-ledger/prep/ledger-attestations-http.md` defining `/v1/ledger/attestations` contract; service surface still required. | | P7 | PREP-LEDGER-OBS-55-001-DEPENDS-ON-54-001-ATTE | DONE (2025-11-20) | Due 2025-11-22 · Accountable: Findings Ledger Guild; DevOps Guild / src/Findings/StellaOps.Findings.Ledger | Findings Ledger Guild; DevOps Guild / src/Findings/StellaOps.Findings.Ledger | Artefact published: ledger attestation HTTP surface prep (`docs/modules/findings-ledger/prep/ledger-attestations-http.md`) outlining `/v1/ledger/attestations` contract; pagination, determinism, and fields defined. | -| P8 | PREP-LEDGER-PACKS-42-001-SNAPSHOT-TIME-TRAVEL | TODO | Due 2025-11-22 · Accountable: Findings Ledger Guild / src/Findings/StellaOps.Findings.Ledger | Findings Ledger Guild / src/Findings/StellaOps.Findings.Ledger | Snapshot/time-travel contract and bundle format not specified; needs design input.

Document artefact/deliverable for LEDGER-PACKS-42-001 and publish location so downstream tasks can proceed. | -| P9 | PREP-LEDGER-RISK-66-001-RISK-ENGINE-SCHEMA-CO | TODO | Due 2025-11-22 · Accountable: Findings Ledger Guild; Risk Engine Guild / src/Findings/StellaOps.Findings.Ledger | Findings Ledger Guild; Risk Engine Guild / src/Findings/StellaOps.Findings.Ledger | Risk Engine schema/contract inputs absent; requires risk field definitions + rollout plan.

Document artefact/deliverable for LEDGER-RISK-66-001 and publish location so downstream tasks can proceed. | -| P10 | PREP-LEDGER-RISK-66-002-DEPENDS-ON-66-001-MIG | TODO | Due 2025-11-22 · Accountable: Findings Ledger Guild / src/Findings/StellaOps.Findings.Ledger | Findings Ledger Guild / src/Findings/StellaOps.Findings.Ledger | Depends on 66-001 migration + risk scoring contract.

Document artefact/deliverable for LEDGER-RISK-66-002 and publish location so downstream tasks can proceed. | +| P8 | PREP-LEDGER-PACKS-42-001-SNAPSHOT-TIME-TRAVEL | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Findings Ledger Guild / src/Findings/StellaOps.Findings.Ledger | Findings Ledger Guild / src/Findings/StellaOps.Findings.Ledger | Snapshot/time-travel contract and bundle format not specified; needs design input.

Document artefact/deliverable for LEDGER-PACKS-42-001 and publish location so downstream tasks can proceed. | +| P9 | PREP-LEDGER-RISK-66-001-RISK-ENGINE-SCHEMA-CO | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Findings Ledger Guild; Risk Engine Guild / src/Findings/StellaOps.Findings.Ledger | Findings Ledger Guild; Risk Engine Guild / src/Findings/StellaOps.Findings.Ledger | Risk Engine schema/contract inputs absent; requires risk field definitions + rollout plan.

Document artefact/deliverable for LEDGER-RISK-66-001 and publish location so downstream tasks can proceed. | +| P10 | PREP-LEDGER-RISK-66-002-DEPENDS-ON-66-001-MIG | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Findings Ledger Guild / src/Findings/StellaOps.Findings.Ledger | Findings Ledger Guild / src/Findings/StellaOps.Findings.Ledger | Depends on 66-001 migration + risk scoring contract.

Document artefact/deliverable for LEDGER-RISK-66-002 and publish location so downstream tasks can proceed. | | 1 | LEDGER-ATTEST-73-002 | BLOCKED | Waiting on LEDGER-ATTEST-73-001 verification pipeline delivery | Findings Ledger Guild / src/Findings/StellaOps.Findings.Ledger | Enable search/filter in findings projections by verification result and attestation status | | 2 | LEDGER-EXPORT-35-001 | DOING (2025-11-20) | Findings export endpoint implemented; VEX/advisory/SBOM endpoints stubbed pending schemas | Findings Ledger Guild / src/Findings/StellaOps.Findings.Ledger | Provide paginated streaming endpoints for advisories, VEX, SBOMs, and findings with deterministic ordering and provenance metadata | | 3 | LEDGER-OAS-61-001 | BLOCKED | PREP-LEDGER-OAS-61-001-ABSENT-OAS-BASELINE-AN | Findings Ledger Guild; API Contracts Guild / src/Findings/StellaOps.Findings.Ledger | Expand Findings Ledger OAS to include projections, evidence lookups, and filter parameters with examples | @@ -54,13 +54,13 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Published ledger OBS/pack/risk prep docs (docs/modules/findings-ledger/prep/2025-11-20-ledger-obs-54-001-prep.md, ...ledger-packs-42-001-prep.md, ...ledger-risk-66-prep.md); set PREP-LEDGER-OBS-54-001, PACKS-42-001, RISK-66-001/002 to DOING. | Project Mgmt | | 2025-11-20 | Added authenticated export endpoints for findings/vex/advisories/sboms (stub responses) and paging contracts; awaiting schema/tables to back VEX/advisory/SBOM queries. Export paging unit tests passing via isolated test project. | Findings Ledger | | 2025-11-20 | Began implementing LEDGER-EXPORT-35-001 HTTP surface (findings export endpoint + paging/token hash) in WebService; tests pending due to existing harness build failures. | Findings Ledger | | 2025-11-20 | Completed PREP-LEDGER-EXPORT-35-001: published export HTTP surface and filters spec at `docs/modules/findings-ledger/export-http-surface.md`; unblocked LEDGER-EXPORT-35-001 (status → TODO). | Planning | | 2025-11-20 | Started PREP-LEDGER-EXPORT-35-001 (status → DOING) after confirming no other DOING owner entries. | Planning | | 2025-11-20 | Completed PREP-LEDGER-OAS-61-001: published baseline OAS at `docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml` with summary `docs/modules/findings-ledger/oas-baseline.md`; downstream OAS/SDK tasks extend this base. | Implementer | | 2025-11-20 | Completed PREP-LEDGER-OBS-55-001: published ledger attestation HTTP surface prep (`docs/modules/findings-ledger/prep/ledger-attestations-http.md`) covering `/v1/ledger/attestations`; still requires 54-001 service surface to implement. | Implementer | -| 2025-11-20 | Started PREP-LEDGER-OBS-55-001 (status → DOING) after confirming no existing DOING/DONE owners; still contingent on 54-001 surface availability. | Planning | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-08 | Sprint stub created; awaiting template normalisation. | Planning | | 2025-11-17 | Normalised sprint to standard template and renamed file to `SPRINT_0121_0001_0001_policy_reasoning.md`. | Project Mgmt | diff --git a/docs/implplan/SPRINT_0122_0001_0001_policy_reasoning.md b/docs/implplan/SPRINT_0122_0001_0001_policy_reasoning.md index c750f9b48..4e984f4de 100644 --- a/docs/implplan/SPRINT_0122_0001_0001_policy_reasoning.md +++ b/docs/implplan/SPRINT_0122_0001_0001_policy_reasoning.md @@ -24,9 +24,9 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| P1 | PREP-LEDGER-RISK-68-001-AWAIT-UNBLOCK-OF-67-0 | TODO | Due 2025-11-22 · Accountable: Findings Ledger Guild · Export Guild / `src/Findings/StellaOps.Findings.Ledger` | Findings Ledger Guild · Export Guild / `src/Findings/StellaOps.Findings.Ledger` | Await unblock of 67-001 + Export Center contract for scored findings.

Document artefact/deliverable for LEDGER-RISK-68-001 and publish location so downstream tasks can proceed. | -| P2 | PREP-LEDGER-RISK-69-001-REQUIRES-67-001-68-00 | TODO | Due 2025-11-22 · Accountable: Findings Ledger Guild · Observability Guild / `src/Findings/StellaOps.Findings.Ledger` | Findings Ledger Guild · Observability Guild / `src/Findings/StellaOps.Findings.Ledger` | Requires 67-001/68-001 to define metrics dimensions.

Document artefact/deliverable for LEDGER-RISK-69-001 and publish location so downstream tasks can proceed. | -| P3 | PREP-LEDGER-TEN-48-001-NEEDS-PLATFORM-APPROVE | TODO | Due 2025-11-22 · Accountable: Findings Ledger Guild / `src/Findings/StellaOps.Findings.Ledger` | Findings Ledger Guild / `src/Findings/StellaOps.Findings.Ledger` | Needs platform-approved partitioning + RLS policy (tenant/project shape, session variables).

Document artefact/deliverable for LEDGER-TEN-48-001 and publish location so downstream tasks can proceed. | +| P1 | PREP-LEDGER-RISK-68-001-AWAIT-UNBLOCK-OF-67-0 | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Findings Ledger Guild · Export Guild / `src/Findings/StellaOps.Findings.Ledger` | Findings Ledger Guild · Export Guild / `src/Findings/StellaOps.Findings.Ledger` | Await unblock of 67-001 + Export Center contract for scored findings.

Document artefact/deliverable for LEDGER-RISK-68-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/findings-ledger/prep/2025-11-20-ledger-risk-prep.md`. | +| P2 | PREP-LEDGER-RISK-69-001-REQUIRES-67-001-68-00 | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Findings Ledger Guild · Observability Guild / `src/Findings/StellaOps.Findings.Ledger` | Findings Ledger Guild · Observability Guild / `src/Findings/StellaOps.Findings.Ledger` | Requires 67-001/68-001 to define metrics dimensions.

Document artefact/deliverable for LEDGER-RISK-69-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/findings-ledger/prep/2025-11-20-ledger-risk-prep.md`. | +| P3 | PREP-LEDGER-TEN-48-001-NEEDS-PLATFORM-APPROVE | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Findings Ledger Guild / `src/Findings/StellaOps.Findings.Ledger` | Findings Ledger Guild / `src/Findings/StellaOps.Findings.Ledger` | Needs platform-approved partitioning + RLS policy (tenant/project shape, session variables).

Document artefact/deliverable for LEDGER-TEN-48-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/findings-ledger/prep/2025-11-20-ledger-risk-prep.md`. | | 1 | LEDGER-RISK-67-001 | BLOCKED | Depends on risk scoring contract + migrations from LEDGER-RISK-66-002 | Findings Ledger Guild · Risk Engine Guild / `src/Findings/StellaOps.Findings.Ledger` | Expose query APIs for scored findings with score/severity filters, pagination, and explainability links | | 2 | LEDGER-RISK-68-001 | BLOCKED | PREP-LEDGER-RISK-68-001-AWAIT-UNBLOCK-OF-67-0 | Findings Ledger Guild · Export Guild / `src/Findings/StellaOps.Findings.Ledger` | Enable export of scored findings and simulation results via Export Center integration | | 3 | LEDGER-RISK-69-001 | BLOCKED | PREP-LEDGER-RISK-69-001-REQUIRES-67-001-68-00 | Findings Ledger Guild · Observability Guild / `src/Findings/StellaOps.Findings.Ledger` | Emit metrics/dashboards for scoring latency, result freshness, severity distribution, provider gaps | @@ -35,6 +35,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Published ledger risk/tenancy prep doc (docs/modules/findings-ledger/prep/2025-11-20-ledger-risk-prep.md); set PREP-LEDGER-RISK-68/69 and TEN-48-001 to DOING. | Project Mgmt | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-18 | Renamed file to `SPRINT_0122_0001_0001_policy_reasoning.md` and normalised to standard template; no scope changes. | Findings Ledger | | 2025-11-18 | Set LEDGER-RISK-67-001/68-001/69-001 to BLOCKED pending risk-scoring contract (66-002) and export metrics dimensions. | Findings Ledger | @@ -48,4 +49,4 @@ ## Next Checkpoints - Await Risk Engine contract drop for 66-002 (date TBD; track in Sprint 0121 dependencies). - Schedule DB/RLS design review with Platform/DB guild to unblock TEN-48-001 (target week of 2025-11-24). -- Re-evaluate sprint status once upstream contracts are published. \ No newline at end of file +- Re-evaluate sprint status once upstream contracts are published. diff --git a/docs/implplan/SPRINT_0124_0001_0001_policy_reasoning.md b/docs/implplan/SPRINT_0124_0001_0001_policy_reasoning.md index 73051b575..d10b159d3 100644 --- a/docs/implplan/SPRINT_0124_0001_0001_policy_reasoning.md +++ b/docs/implplan/SPRINT_0124_0001_0001_policy_reasoning.md @@ -18,7 +18,7 @@ ## Delivery Tracker | # | Task ID & handle | State | Key dependency / next step | Owners | | --- | --- | --- | --- | --- | -| P1 | PREP-POLICY-ENGINE-20-002-DETERMINISTIC-EVALU | TODO | Due 2025-11-22 · Accountable: Policy Guild / `src/Policy/StellaOps.Policy.Engine` | Policy Guild / `src/Policy/StellaOps.Policy.Engine` | Deterministic evaluator spec missing.

Document artefact/deliverable for POLICY-ENGINE-20-002 and publish location so downstream tasks can proceed. | +| P1 | PREP-POLICY-ENGINE-20-002-DETERMINISTIC-EVALU | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Policy Guild / `src/Policy/StellaOps.Policy.Engine` | Policy Guild / `src/Policy/StellaOps.Policy.Engine` | Deterministic evaluator spec missing.

Document artefact/deliverable for POLICY-ENGINE-20-002 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/policy/design/policy-deterministic-evaluator.md`. | | 1 | POLICY-CONSOLE-23-002 | TODO | Produce simulation diff metadata and approval endpoints for Console (deps: POLICY-CONSOLE-23-001). | Policy Guild, Product Ops / `src/Policy/StellaOps.Policy.Engine` | | 2 | POLICY-ENGINE-20-002 | BLOCKED (2025-10-26) | PREP-POLICY-ENGINE-20-002-DETERMINISTIC-EVALU | Policy Guild / `src/Policy/StellaOps.Policy.Engine` | | 3 | POLICY-ENGINE-20-003 | TODO | Depends on 20-002. | Policy · Concelier · Excititor Guilds / `src/Policy/StellaOps.Policy.Engine` | @@ -36,6 +36,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Published deterministic evaluator spec draft (docs/modules/policy/design/policy-deterministic-evaluator.md); moved PREP-POLICY-ENGINE-20-002 to DOING. | Project Mgmt | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-08 | Sprint stub; awaiting staffing. | Planning | | 2025-11-18 | All tasks still awaiting upstream evaluator specs; no progress. | Policy Guild | @@ -48,4 +49,4 @@ ## Next Checkpoints - Publish deterministic evaluator spec for 20-002 (date TBD). -- Provide Console export/simulation contract for 23-001 to unblock 23-002. \ No newline at end of file +- Provide Console export/simulation contract for 23-001 to unblock 23-002. diff --git a/docs/implplan/SPRINT_0125_0001_0001_mirror.md b/docs/implplan/SPRINT_0125_0001_0001_mirror.md index df6b8f3a8..d4b7327be 100644 --- a/docs/implplan/SPRINT_0125_0001_0001_mirror.md +++ b/docs/implplan/SPRINT_0125_0001_0001_mirror.md @@ -21,7 +21,7 @@ | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | | 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 | TODO | 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.

Document artefact/deliverable for MIRROR-CRT-56-001 and publish location so downstream tasks can proceed. | +| P1 | PREP-MIRROR-CRT-56-001-UPSTREAM-SPRINT-110-D | DOING (2025-11-20) | 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.

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`). | @@ -37,6 +37,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Published thin-bundle prep doc (docs/modules/mirror/prep-56-001-thin-bundle.md); moved PREP-MIRROR-CRT-56-001 to DOING after confirming unowned. | Project Mgmt | | 2025-11-19 | Cleared stray hyphen from PREP-MIRROR-CRT-56-001-UPSTREAM-SPRINT-110-D so MIRROR-CRT-56-001 dependency is resolvable. | Project Mgmt | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-19 | Completed PREP-MIRROR-CRT-56-001-MILESTONE-0-PUBLISH: published sample thin bundle + hashes and milestone note (`docs/modules/mirror/milestone-0-thin-bundle.md`). | Implementer | diff --git a/docs/implplan/SPRINT_0127_0001_0001_policy_reasoning.md b/docs/implplan/SPRINT_0127_0001_0001_policy_reasoning.md index 89b893d65..448c12dfd 100644 --- a/docs/implplan/SPRINT_0127_0001_0001_policy_reasoning.md +++ b/docs/implplan/SPRINT_0127_0001_0001_policy_reasoning.md @@ -17,7 +17,7 @@ ## Delivery Tracker | # | Task ID & handle | State | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| P1 | PREP-POLICY-RISK-66-001-RISKPROFILE-LIBRARY-S | TODO | Due 2025-11-22 · Accountable: Risk Profile Schema Guild / `src/Policy/StellaOps.Policy.RiskProfile` | Risk Profile Schema Guild / `src/Policy/StellaOps.Policy.RiskProfile` | RiskProfile library scaffold absent (`src/Policy/StellaOps.Policy.RiskProfile` contains only AGENTS.md); need project + storage contract to place schema/validators.

Document artefact/deliverable for POLICY-RISK-66-001 and publish location so downstream tasks can proceed. | +| P1 | PREP-POLICY-RISK-66-001-RISKPROFILE-LIBRARY-S | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Risk Profile Schema Guild / `src/Policy/StellaOps.Policy.RiskProfile` | Risk Profile Schema Guild / `src/Policy/StellaOps.Policy.RiskProfile` | RiskProfile library scaffold absent (`src/Policy/StellaOps.Policy.RiskProfile` contains only AGENTS.md); need project + storage contract to place schema/validators.

Document artefact/deliverable for POLICY-RISK-66-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/policy/prep/2025-11-20-riskprofile-66-001-prep.md`. | | 1 | POLICY-ENGINE-80-002 | TODO | Depends on 80-001. | Policy · Storage Guild / `src/Policy/StellaOps.Policy.Engine` | Join reachability facts + Redis caches. | | 2 | POLICY-ENGINE-80-003 | TODO | Depends on 80-002. | Policy · Policy Editor Guild / `src/Policy/StellaOps.Policy.Engine` | SPL predicates/actions reference reachability. | | 3 | POLICY-ENGINE-80-004 | TODO | Depends on 80-003. | Policy · Observability Guild / `src/Policy/StellaOps.Policy.Engine` | Metrics/traces for signals usage. | @@ -37,6 +37,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Published risk profile library prep (docs/modules/policy/prep/2025-11-20-riskprofile-66-001-prep.md); set PREP-POLICY-RISK-66-001 to DOING. | Project Mgmt | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-08 | Sprint stub; awaiting upstream phases. | Planning | | 2025-11-19 | Normalized to standard template and renamed from `SPRINT_127_policy_reasoning.md` to `SPRINT_0127_0001_0001_policy_reasoning.md`; content preserved. | Implementer | @@ -48,4 +49,4 @@ ## Next Checkpoints - Define reachability input contract (date TBD). -- Draft RiskProfile schema baseline (date TBD). \ No newline at end of file +- Draft RiskProfile schema baseline (date TBD). diff --git a/docs/implplan/SPRINT_0131_0001_0001_scanner_surface.md b/docs/implplan/SPRINT_0131_0001_0001_scanner_surface.md index cc14076b6..bde89f55a 100644 --- a/docs/implplan/SPRINT_0131_0001_0001_scanner_surface.md +++ b/docs/implplan/SPRINT_0131_0001_0001_scanner_surface.md @@ -21,9 +21,9 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| P1 | PREP-SCANNER-ANALYZERS-JAVA-21-005-TESTS-BLOC | TODO | Due 2025-11-22 · Accountable: Java Analyzer Guild | Java Analyzer Guild | Tests blocked: repo build fails in Concelier (CoreLinksets missing) and targeted Java analyzer test run stalls; retry once dependencies fixed or CI available.

Document artefact/deliverable for SCANNER-ANALYZERS-JAVA-21-005 and publish location so downstream tasks can proceed. | -| P2 | PREP-SCANNER-ANALYZERS-JAVA-21-008-WAITING-ON | TODO | Due 2025-11-22 · Accountable: Java Analyzer Guild | Java Analyzer Guild | Waiting on 21-007 completion and resolver authoring bandwidth.

Document artefact/deliverable for SCANNER-ANALYZERS-JAVA-21-008 and publish location so downstream tasks can proceed. | -| P3 | PREP-SCANNER-ANALYZERS-LANG-11-001-DOTNET-TES | TODO | Due 2025-11-22 · Accountable: StellaOps.Scanner EPDR Guild · Language Analyzer Guild | StellaOps.Scanner EPDR Guild · Language Analyzer Guild | `dotnet test` hangs/returns empty output; needs clean runner/CI diagnostics.

Document artefact/deliverable for SCANNER-ANALYZERS-LANG-11-001 and publish location so downstream tasks can proceed. | +| P1 | PREP-SCANNER-ANALYZERS-JAVA-21-005-TESTS-BLOC | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Java Analyzer Guild | Java Analyzer Guild | Tests blocked: repo build fails in Concelier (CoreLinksets missing) and targeted Java analyzer test run stalls; retry once dependencies fixed or CI available.

Document artefact/deliverable for SCANNER-ANALYZERS-JAVA-21-005 and publish location so downstream tasks can proceed. | +| P2 | PREP-SCANNER-ANALYZERS-JAVA-21-008-WAITING-ON | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Java Analyzer Guild | Java Analyzer Guild | Waiting on 21-007 completion and resolver authoring bandwidth.

Document artefact/deliverable for SCANNER-ANALYZERS-JAVA-21-008 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/scanner/prep/2025-11-20-java-21-008-prep.md`. | +| P3 | PREP-SCANNER-ANALYZERS-LANG-11-001-DOTNET-TES | DOING (2025-11-20) | Due 2025-11-22 · Accountable: StellaOps.Scanner EPDR Guild · Language Analyzer Guild | StellaOps.Scanner EPDR Guild · Language Analyzer Guild | `dotnet test` hangs/returns empty output; needs clean runner/CI diagnostics.

Document artefact/deliverable for SCANNER-ANALYZERS-LANG-11-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/scanner/prep/2025-11-20-lang-11-001-prep.md`. | | 1 | SCANNER-ANALYZERS-DENO-26-009 | BLOCKED (2025-11-19) | Waiting on runtime shim fixtures + CI runner; design `deno-runtime-shim.md` drafted but tests cannot run. | Deno Analyzer Guild · Signals Guild | Optional runtime evidence hooks capturing module loads and permissions with path hashing during harnessed execution. | | 2 | SCANNER-ANALYZERS-DENO-26-010 | TODO | After 26-009, wire CLI (`stella deno trace`) + Worker/Offline Kit using runtime NDJSON contract. | Deno Analyzer Guild · DevOps Guild | Package analyzer plug-in and surface CLI/worker commands with offline documentation. | | 3 | SCANNER-ANALYZERS-DENO-26-011 | TODO | Implement policy signal emitter using runtime metadata once trace shim lands. | Deno Analyzer Guild | Policy signal emitter for capabilities (net/fs/env/ffi/process/crypto), remote origins, npm usage, wasm modules, and dynamic-import warnings. | @@ -39,6 +39,9 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Published prep docs for P2/P3: `docs/modules/scanner/prep/2025-11-20-java-21-008-prep.md` and `docs/modules/scanner/prep/2025-11-20-lang-11-001-prep.md`; set PREP P2/P3 to DOING after confirming unowned. | Project Mgmt | +| 2025-11-20 | Published prep note for SCANNER-ANALYZERS-JAVA-21-005 (docs/modules/scanner/prep/2025-11-20-java-21-005-prep.md); pinged Concelier/CoreLinksets owners for missing packages and CI isolation. | Project Mgmt | +| 2025-11-20 | Confirmed PREP-SCANNER-ANALYZERS-JAVA-21-005-TESTS-BLOC still TODO; moved to DOING to capture blockers and prep artefact. | Project Mgmt | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-17 | Normalised sprint file to standard template and renamed from `SPRINT_131_scanner_surface.md` to `SPRINT_0131_scanner_surface.md`; no semantic changes. | Planning | | 2025-11-17 | Attempted `./tools/dotnet-filter.sh test src/Scanner/StellaOps.Scanner.sln --no-restore`; build ran ~72s compiling scanner/all projects without completing tests, then aborted locally to avoid runaway build. Follow-up narrow build `dotnet build src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj` also stalled ~28s in target resolution before manual stop. Blocker persists; needs clean CI runner or scoped test project to finish LANG-11-001 validation. | Implementer | @@ -66,6 +69,8 @@ - Java analyzer framework-config/JNI tests pending: prior runs either failed due to missing `StellaOps.Concelier.Storage.Mongo` `CoreLinksets` types or were aborted due to repo-wide restore contention; rerun on clean runner or after Concelier build stabilises. - Deno runtime hook + policy-signal schema drafted in `docs/modules/scanner/design/deno-runtime-signals.md`; shim plan in `docs/modules/scanner/design/deno-runtime-shim.md`. - Loader/require shim implementation still pending for DENO-26-009; must stay offline-first and AnalysisStore-compatible before wiring DENO-26-010/011. +- PREP note for SCANNER-ANALYZERS-JAVA-21-005 published at `docs/modules/scanner/prep/2025-11-20-java-21-005-prep.md`; awaiting CoreLinksets package fix and isolated CI slot before tests can run. + - PREP docs added for SCANNER-ANALYZERS-JAVA-21-008 (`docs/modules/scanner/prep/2025-11-20-java-21-008-prep.md`) and LANG-11-001 (`docs/modules/scanner/prep/2025-11-20-lang-11-001-prep.md`); both depend on resolver outputs/CI isolation. ## Next Checkpoints | Date (UTC) | Session | Goal | Impacted work | Owner | diff --git a/docs/implplan/SPRINT_0132_0001_0001_scanner_surface.md b/docs/implplan/SPRINT_0132_0001_0001_scanner_surface.md index 07bf676f5..246c3d2c5 100644 --- a/docs/implplan/SPRINT_0132_0001_0001_scanner_surface.md +++ b/docs/implplan/SPRINT_0132_0001_0001_scanner_surface.md @@ -52,6 +52,7 @@ + | Date (UTC) | Update | Owner | | --- | --- | --- | | 2025-11-20 | Attempted node isolated restore/test; restore failed fetching Microsoft.TestPlatform.TestHost (nuget.org) because offline package path was wrong. Script corrected to use `offline/packages`. Re-run still needed. | Implementer | diff --git a/docs/implplan/SPRINT_0142_0001_0001_sbomservice.md b/docs/implplan/SPRINT_0142_0001_0001_sbomservice.md index 2eba1d93e..59fc657c9 100644 --- a/docs/implplan/SPRINT_0142_0001_0001_sbomservice.md +++ b/docs/implplan/SPRINT_0142_0001_0001_sbomservice.md @@ -19,9 +19,9 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| P1 | PREP-SBOM-CONSOLE-23-001-BUILD-TEST-FAILING-D | TODO | Due 2025-11-22 · Accountable: SBOM Service Guild; Cartographer Guild | SBOM Service Guild; Cartographer Guild | Build/test failing due to missing NuGet feed; need feed/offline cache before wiring storage and validating `/console/sboms`.

Document artefact/deliverable for SBOM-CONSOLE-23-001 and publish location so downstream tasks can proceed. | -| P2 | PREP-SBOM-SERVICE-21-001-WAITING-ON-LNM-V1-FI | TODO | Due 2025-11-22 · Accountable: SBOM Service Guild; Cartographer Guild | SBOM Service Guild; Cartographer Guild | Waiting on LNM v1 fixtures (due 2025-11-18 UTC) to freeze schema; then publish normalized SBOM projection read API with pagination + tenant enforcement.

Document artefact/deliverable for SBOM-SERVICE-21-001 and publish location so downstream tasks can proceed. | -| P3 | PREP-BUILD-INFRA-SBOM-SERVICE-GUILD-BLOCKED-M | TODO | Due 2025-11-22 · Accountable: Planning | Planning | BLOCKED (multiple restore attempts still hang/fail; need vetted feed/cache).

Document artefact/deliverable for Build/Infra · SBOM Service Guild and publish location so downstream tasks can proceed. | +| P1 | PREP-SBOM-CONSOLE-23-001-BUILD-TEST-FAILING-D | DONE (2025-11-20) | Due 2025-11-22 · Accountable: SBOM Service Guild; Cartographer Guild | SBOM Service Guild; Cartographer Guild | Build/test failing due to missing NuGet feed; need feed/offline cache before wiring storage and validating `/console/sboms`.

Deliverable: offline feed plan + cache in `local-nugets/`; doc at `docs/modules/sbomservice/offline-feed-plan.md`; script `tools/offline/fetch-sbomservice-deps.sh` hydrates required packages. | +| P2 | PREP-SBOM-SERVICE-21-001-WAITING-ON-LNM-V1-FI | DOING (2025-11-20) | Due 2025-11-22 · Accountable: SBOM Service Guild; Cartographer Guild | SBOM Service Guild; Cartographer Guild | Waiting on LNM v1 fixtures (due 2025-11-18 UTC) to freeze schema; then publish normalized SBOM projection read API with pagination + tenant enforcement.

Document artefact/deliverable for SBOM-SERVICE-21-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/sbomservice/prep/2025-11-20-sbom-service-21-001-prep.md`. | +| P3 | PREP-BUILD-INFRA-SBOM-SERVICE-GUILD-BLOCKED-M | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Planning | Planning | BLOCKED (multiple restore attempts still hang/fail; need vetted feed/cache).

Document artefact/deliverable for Build/Infra · SBOM Service Guild and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/sbomservice/prep/2025-11-20-build-infra-prep.md`. | | 1 | SBOM-AIAI-31-001 | DONE | Implemented `/sbom/paths` with env/blast-radius/runtime flags + cursor paging and `/sbom/versions` timeline; in-memory deterministic seed until storage wired. | SBOM Service Guild (src/SbomService/StellaOps.SbomService) | Provide path and version timeline endpoints optimised for Advisory AI. | | 2 | SBOM-AIAI-31-002 | DONE | Metrics + cache-hit tagging implemented; Grafana starter dashboard added; build/test completed locally. | SBOM Service Guild; Observability Guild | Instrument metrics for path/timeline queries and surface dashboards. | | 3 | SBOM-CONSOLE-23-001 | BLOCKED | PREP-SBOM-CONSOLE-23-001-BUILD-TEST-FAILING-D | SBOM Service Guild; Cartographer Guild | Provide Console-focused SBOM catalog API. | @@ -49,6 +49,9 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 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 | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-17 | Normalised sprint to standard template and renamed from `SPRINT_142_sbomservice.md`; no scope changes. | Project Mgmt | | 2025-11-17 | Flagged need for SBOM Service module dossier as documentation prerequisite. | Project Mgmt | diff --git a/docs/implplan/SPRINT_0151_0001_0001_orchestrator_i.md b/docs/implplan/SPRINT_0151_0001_0001_orchestrator_i.md index a564f933f..7332079a5 100644 --- a/docs/implplan/SPRINT_0151_0001_0001_orchestrator_i.md +++ b/docs/implplan/SPRINT_0151_0001_0001_orchestrator_i.md @@ -19,13 +19,13 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| P1 | PREP-ORCH-AIRGAP-56-001-AWAIT-SPRINT-0120-A-A | TODO | Due 2025-11-23 · Accountable: Orchestrator Service Guild · AirGap Policy Guild | Orchestrator Service Guild · AirGap Policy Guild | Await Sprint 0120.A AirGap readiness; sealed-mode contracts not published.

Document artefact/deliverable for ORCH-AIRGAP-56-001 and publish location so downstream tasks can proceed. | -| P2 | PREP-ORCH-AIRGAP-56-002-UPSTREAM-56-001-BLOCK | TODO | Due 2025-11-23 · Accountable: Orchestrator Service Guild · AirGap Controller Guild | Orchestrator Service Guild · AirGap Controller Guild | Upstream 56-001 blocked.

Document artefact/deliverable for ORCH-AIRGAP-56-002 and publish location so downstream tasks can proceed. | -| P3 | PREP-ORCH-AIRGAP-57-001-UPSTREAM-56-002-BLOCK | TODO | Due 2025-11-23 · Accountable: Orchestrator Service Guild · Mirror Creator Guild | Orchestrator Service Guild · Mirror Creator Guild | Upstream 56-002 blocked.

Document artefact/deliverable for ORCH-AIRGAP-57-001 and publish location so downstream tasks can proceed. | -| P4 | PREP-ORCH-AIRGAP-58-001-UPSTREAM-57-001-BLOCK | TODO | Due 2025-11-23 · Accountable: Orchestrator Service Guild · Evidence Locker Guild | Orchestrator Service Guild · Evidence Locker Guild | Upstream 57-001 blocked.

Document artefact/deliverable for ORCH-AIRGAP-58-001 and publish location so downstream tasks can proceed. | -| P5 | PREP-ORCH-OAS-61-001-ORCHESTRATOR-TELEMETRY-C | TODO | Due 2025-11-23 · Accountable: Orchestrator Service Guild · API Contracts Guild | Orchestrator Service Guild · API Contracts Guild | Orchestrator telemetry/contract inputs not available; wait for 150.A readiness.

Document artefact/deliverable for ORCH-OAS-61-001 and publish location so downstream tasks can proceed. | -| P6 | PREP-ORCH-OAS-61-002-DEPENDS-ON-61-001 | TODO | Due 2025-11-23 · Accountable: Orchestrator Service Guild | Orchestrator Service Guild | Depends on 61-001.

Document artefact/deliverable for ORCH-OAS-61-002 and publish location so downstream tasks can proceed. | -| P7 | PREP-ORCH-OAS-62-001-DEPENDS-ON-61-002 | TODO | Due 2025-11-23 · Accountable: Orchestrator Service Guild · SDK Generator Guild | Orchestrator Service Guild · SDK Generator Guild | Depends on 61-002.

Document artefact/deliverable for ORCH-OAS-62-001 and publish location so downstream tasks can proceed. | +| P1 | PREP-ORCH-AIRGAP-56-001-AWAIT-SPRINT-0120-A-A | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Orchestrator Service Guild · AirGap Policy Guild | Orchestrator Service Guild · AirGap Policy Guild | Await Sprint 0120.A AirGap readiness; sealed-mode contracts not published.

Document artefact/deliverable for ORCH-AIRGAP-56-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/orchestrator/prep/2025-11-20-airgap-56-001-prep.md`. | +| P2 | PREP-ORCH-AIRGAP-56-002-UPSTREAM-56-001-BLOCK | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Orchestrator Service Guild · AirGap Controller Guild | Orchestrator Service Guild · AirGap Controller Guild | Upstream 56-001 blocked.

Document artefact/deliverable for ORCH-AIRGAP-56-002 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/orchestrator/prep/2025-11-20-airgap-56-002-prep.md`. | +| P3 | PREP-ORCH-AIRGAP-57-001-UPSTREAM-56-002-BLOCK | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Orchestrator Service Guild · Mirror Creator Guild | Orchestrator Service Guild · Mirror Creator Guild | Upstream 56-002 blocked.

Document artefact/deliverable for ORCH-AIRGAP-57-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/orchestrator/prep/2025-11-20-airgap-57-001-prep.md`. | +| P4 | PREP-ORCH-AIRGAP-58-001-UPSTREAM-57-001-BLOCK | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Orchestrator Service Guild · Evidence Locker Guild | Orchestrator Service Guild · Evidence Locker Guild | Upstream 57-001 blocked.

Document artefact/deliverable for ORCH-AIRGAP-58-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/orchestrator/prep/2025-11-20-airgap-58-001-prep.md`. | +| P5 | PREP-ORCH-OAS-61-001-ORCHESTRATOR-TELEMETRY-C | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Orchestrator Service Guild · API Contracts Guild | Orchestrator Service Guild · API Contracts Guild | Orchestrator telemetry/contract inputs not available; wait for 150.A readiness.

Document artefact/deliverable for ORCH-OAS-61-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/orchestrator/prep/2025-11-20-oas-61-001-prep.md`. | +| P6 | PREP-ORCH-OAS-61-002-DEPENDS-ON-61-001 | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Orchestrator Service Guild | Orchestrator Service Guild | Depends on 61-001.

Document artefact/deliverable for ORCH-OAS-61-002 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/orchestrator/prep/2025-11-20-oas-61-001-prep.md`. | +| P7 | PREP-ORCH-OAS-62-001-DEPENDS-ON-61-002 | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Orchestrator Service Guild · SDK Generator Guild | Orchestrator Service Guild · SDK Generator Guild | Depends on 61-002.

Document artefact/deliverable for ORCH-OAS-62-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/orchestrator/prep/2025-11-20-oas-61-001-prep.md`. | | P8 | PREP-ORCH-OAS-63-001-DEPENDS-ON-62-001 | TODO | Due 2025-11-23 · Accountable: Orchestrator Service Guild · API Governance Guild | Orchestrator Service Guild · API Governance Guild | Depends on 62-001.

Document artefact/deliverable for ORCH-OAS-63-001 and publish location so downstream tasks can proceed. | | P9 | PREP-ORCH-OBS-50-001-TELEMETRY-CORE-SPRINT-01 | TODO | Due 2025-11-23 · Accountable: Orchestrator Service Guild · Observability Guild | Orchestrator Service Guild · Observability Guild | Telemetry Core (Sprint 0174) not yet available for orchestrator host.

Document artefact/deliverable for ORCH-OBS-50-001 and publish location so downstream tasks can proceed. | | P10 | PREP-ORCH-OBS-51-001-DEPENDS-ON-50-001-TELEME | TODO | Due 2025-11-23 · Accountable: Orchestrator Service Guild · DevOps Guild | Orchestrator Service Guild · DevOps Guild | Depends on 50-001 + Telemetry schema.

Document artefact/deliverable for ORCH-OBS-51-001 and publish location so downstream tasks can proceed. | @@ -53,6 +53,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Published prep docs for ORCH AirGap 56/57/58 and OAS 61/62; set P1–P7 to DOING after confirming unowned. | Project Mgmt | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-18 | Normalised sprint doc to standard template; renamed from `SPRINT_151_orchestrator_i.md`. | Planning | | 2025-11-19 | Set all tasks to BLOCKED pending upstream readiness (AirGap/Scanner/Graph), Telemetry Core availability, and Orchestrator event schema; no executable work until contracts land. | Implementer | @@ -62,4 +63,4 @@ - Ensure status changes here mirror module boards to avoid drift between coordination doc and execution evidence. ## Next Checkpoints -- None scheduled; add orchestrator scheduling/automation sync once upstream readiness dates are committed. \ No newline at end of file +- None scheduled; add orchestrator scheduling/automation sync once upstream readiness dates are committed. diff --git a/docs/implplan/SPRINT_0155_0001_0001_scheduler_i.md b/docs/implplan/SPRINT_0155_0001_0001_scheduler_i.md index 6912242af..c7ed5967b 100644 --- a/docs/implplan/SPRINT_0155_0001_0001_scheduler_i.md +++ b/docs/implplan/SPRINT_0155_0001_0001_scheduler_i.md @@ -19,8 +19,8 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| P1 | PREP-SCHED-SURFACE-01-NEED-SURFACE-FS-POINTER | TODO | Due 2025-11-23 · Accountable: Scheduler Worker Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.Worker) | Scheduler Worker Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.Worker) | Need Surface.FS pointer model/contract; awaiting design input before planning deltas.

Document artefact/deliverable for SCHED-SURFACE-01 and publish location so downstream tasks can proceed. | -| P2 | PREP-SCHED-WORKER-23-101-WAITING-ON-POLICY-GU | TODO | Due 2025-11-23 · Accountable: Scheduler Worker Guild, Policy Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.Worker) | Scheduler Worker Guild, Policy Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.Worker) | Waiting on Policy guild to supply activation event contract and throttle source.

Document artefact/deliverable for SCHED-WORKER-23-101 and publish location so downstream tasks can proceed. | +| P1 | PREP-SCHED-SURFACE-01-NEED-SURFACE-FS-POINTER | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Scheduler Worker Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.Worker) | Scheduler Worker Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.Worker) | Need Surface.FS pointer model/contract; awaiting design input before planning deltas.

Document artefact/deliverable for SCHED-SURFACE-01 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/scheduler/prep/2025-11-20-surface-fs-pointer.md`. | +| P2 | PREP-SCHED-WORKER-23-101-WAITING-ON-POLICY-GU | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Scheduler Worker Guild, Policy Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.Worker) | Scheduler Worker Guild, Policy Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.Worker) | Waiting on Policy guild to supply activation event contract and throttle source.

Document artefact/deliverable for SCHED-WORKER-23-101 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/scheduler/prep/2025-11-20-worker-23-101-prep.md`. | | 0 | AGENTS-SCHEDULER-UPDATE | DONE | `src/Scheduler/AGENTS.md` created and published. | Project Manager · Architecture Guild | Populate module AGENTS charter covering roles, docs, determinism/testing rules, and allowed shared libs. | | 1 | SCHED-IMPACT-16-303 | DONE | Implemented removal + snapshot/restore with compaction; snapshot payloads ready for RocksDB/Redis persistence. | Scheduler ImpactIndex Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.ImpactIndex) | Snapshot/compaction + invalidation for removed images; persistence to RocksDB/Redis per architecture. | | 2 | SCHED-SURFACE-01 | BLOCKED | PREP-SCHED-SURFACE-01-NEED-SURFACE-FS-POINTER | Scheduler Worker Guild (src/Scheduler/__Libraries/StellaOps.Scheduler.Worker) | Evaluate Surface.FS pointers when planning delta scans to avoid redundant work and prioritise drift-triggered assets. | @@ -37,6 +37,7 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Published prep docs for SCHED-SURFACE-01 and SCHED-WORKER-23-101 (`docs/modules/scheduler/prep/2025-11-20-surface-fs-pointer.md`, `docs/modules/scheduler/prep/2025-11-20-worker-23-101-prep.md`); set P1/P2 to DOING after confirming unowned. | Project Mgmt | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-17 | Added graph metrics (`graph_build_seconds`, `graph_jobs_inflight`, `overlay_lag_seconds`) with tenant/graph tags; worker library build succeeded. | Scheduler Worker Guild | | 2025-11-17 | Added resolver job APIs (`POST/GET /api/v1/scheduler/vuln/resolver/jobs`) with scope enforcement and in-memory job service stub. | Scheduler WebService Guild | @@ -62,9 +63,9 @@ - SCHED-WEB-20-002 depends on worker API contract (SCHED-WORKER-20-301); keep priority aligned to avoid UI/CLI drift. - Maintain observability naming consistency for `policy_simulation_*` metrics to avoid dashboard regressions. - Upstream readiness from AirGap, Scanner, and Graph sprints must be confirmed before expanding scope. -- SCHED-SURFACE-01 blocked until Surface.FS pointer model/contract is provided; cannot design delta planning without it. +- SCHED-SURFACE-01 blocked until Surface.FS pointer model/contract is provided; interim prep doc at `docs/modules/scheduler/prep/2025-11-20-surface-fs-pointer.md`; awaiting dataset allowlist and sealed-mode rule to finalize. - Backlog breach webhook contract stubbed via resolver backlog notifier; upgrade to real sink once DevOps endpoint is available. -- SCHED-WORKER-23-101/102/25-101/25-102/26-201 blocked on Policy guild supplying activation event shape + throttling guidance; downstream workers sit until contract lands. +- SCHED-WORKER-23-101/102/25-101/25-102/26-201 blocked on Policy guild supplying activation event shape + throttling guidance; interim prep doc at `docs/modules/scheduler/prep/2025-11-20-worker-23-101-prep.md` captures proposed schema while we wait. ## Next Checkpoints -- None scheduled; set once worker API scaffolding and GraphJobs accessibility fixes land. \ No newline at end of file +- None scheduled; set once worker API scaffolding and GraphJobs accessibility fixes land. diff --git a/docs/implplan/SPRINT_0163_0001_0001_exportcenter_ii.md b/docs/implplan/SPRINT_0163_0001_0001_exportcenter_ii.md index 1621acd2c..9f69934c3 100644 --- a/docs/implplan/SPRINT_0163_0001_0001_exportcenter_ii.md +++ b/docs/implplan/SPRINT_0163_0001_0001_exportcenter_ii.md @@ -20,16 +20,16 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| P1 | PREP-EXPORT-OBS-50-001-WAIT-FOR-EXPORTER-SERV | TODO | Due 2025-11-23 · Accountable: Exporter Service · Observability Guild | Exporter Service · Observability Guild | Wait for exporter service bootstrap + telemetry schema.

Document artefact/deliverable for EXPORT-OBS-50-001 and publish location so downstream tasks can proceed. | -| P2 | PREP-EXPORT-RISK-69-001-AWAIT-PHASE-I-ARTIFAC | TODO | Due 2025-11-23 · Accountable: Exporter Service · Risk Bundle Export Guild | Exporter Service · Risk Bundle Export Guild | Await phase I artifacts + schema; needs provider selection rules.

Document artefact/deliverable for EXPORT-RISK-69-001 and publish location so downstream tasks can proceed. | -| P3 | PREP-EXPORT-SVC-35-001-NEEDS-PHASE-I-READINES | TODO | Due 2025-11-23 · Accountable: Exporter Service | Exporter Service | Needs phase I readiness + synthetic telemetry feeds.

Document artefact/deliverable for EXPORT-SVC-35-001 and publish location so downstream tasks can proceed. | -| P4 | PREP-EXPORT-SVC-35-002-DEPENDS-ON-35-001 | TODO | Due 2025-11-23 · Accountable: Exporter Service | Exporter Service | Depends on 35-001.

Document artefact/deliverable for EXPORT-SVC-35-002 and publish location so downstream tasks can proceed. | -| P5 | PREP-EXPORT-SVC-35-003-DEPENDS-ON-35-002 | TODO | Due 2025-11-23 · Accountable: Exporter Service | Exporter Service | Depends on 35-002.

Document artefact/deliverable for EXPORT-SVC-35-003 and publish location so downstream tasks can proceed. | -| P6 | PREP-EXPORT-SVC-35-004-DEPENDS-ON-35-003 | TODO | Due 2025-11-23 · Accountable: Exporter Service | Exporter Service | Depends on 35-003.

Document artefact/deliverable for EXPORT-SVC-35-004 and publish location so downstream tasks can proceed. | -| P7 | PREP-EXPORT-SVC-35-005-DEPENDS-ON-35-004 | TODO | Due 2025-11-23 · Accountable: Exporter Service | Exporter Service | Depends on 35-004.

Document artefact/deliverable for EXPORT-SVC-35-005 and publish location so downstream tasks can proceed. | -| P8 | PREP-EXPORT-NOTIFY-SCHEMA-OBS-52 | TODO | Due 2025-11-23 · Accountable: Notifications Guild · Exporter Service | Notifications Guild · Exporter Service | Notifications schema for export lifecycle events not published; required for EXPORT-OBS-52-001 and downstream tasks. Provide envelope + sample payloads. | -| P8 | PREP-EXPORT-CRYPTO-90-001-PENDING-NOV-18-CRYP | TODO | Due 2025-11-23 · Accountable: Exporter Service · Security Guild | Exporter Service · Security Guild | Pending Nov-18 crypto review + reference implementation.

Document artefact/deliverable for EXPORT-CRYPTO-90-001 and publish location so downstream tasks can proceed. | -| P9 | PREP-EXPORTER-SERVICE-BLOCKED-WAITING-ON-EVID | TODO | Due 2025-11-23 · Accountable: Planning | Planning | BLOCKED (waiting on EvidenceLocker spec).

Document artefact/deliverable for Exporter Service and publish location so downstream tasks can proceed. | +| P1 | PREP-EXPORT-OBS-50-001-WAIT-FOR-EXPORTER-SERV | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Exporter Service · Observability Guild | Exporter Service · Observability Guild | Wait for exporter service bootstrap + telemetry schema.

Document artefact/deliverable for EXPORT-OBS-50-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/export-center/prep/2025-11-20-obs-50-001-prep.md`. | +| P2 | PREP-EXPORT-RISK-69-001-AWAIT-PHASE-I-ARTIFAC | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Exporter Service · Risk Bundle Export Guild | Exporter Service · Risk Bundle Export Guild | Await phase I artifacts + schema; needs provider selection rules.

Document artefact/deliverable for EXPORT-RISK-69-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/export-center/prep/2025-11-20-risk-69-001-prep.md`. | +| P3 | PREP-EXPORT-SVC-35-001-NEEDS-PHASE-I-READINES | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Exporter Service | Exporter Service | Needs phase I readiness + synthetic telemetry feeds.

Document artefact/deliverable for EXPORT-SVC-35-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/export-center/prep/2025-11-20-svc-35-001-prep.md`. | +| P4 | PREP-EXPORT-SVC-35-002-DEPENDS-ON-35-001 | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Exporter Service | Exporter Service | Depends on 35-001.

Document artefact/deliverable for EXPORT-SVC-35-002 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/export-center/prep/2025-11-20-svc-35-002-prep.md`. | +| P5 | PREP-EXPORT-SVC-35-003-DEPENDS-ON-35-002 | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Exporter Service | Exporter Service | Depends on 35-002.

Document artefact/deliverable for EXPORT-SVC-35-003 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/export-center/prep/2025-11-20-svc-35-003-prep.md`. | +| P6 | PREP-EXPORT-SVC-35-004-DEPENDS-ON-35-003 | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Exporter Service | Exporter Service | Depends on 35-003.

Document artefact/deliverable for EXPORT-SVC-35-004 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/export-center/prep/2025-11-20-svc-35-004-prep.md`. | +| P7 | PREP-EXPORT-SVC-35-005-DEPENDS-ON-35-004 | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Exporter Service | Exporter Service | Depends on 35-004.

Document artefact/deliverable for EXPORT-SVC-35-005 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/export-center/prep/2025-11-20-svc-35-005-prep.md`. | +| P8 | PREP-EXPORT-NOTIFY-SCHEMA-OBS-52 | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Notifications Guild · Exporter Service | Notifications Guild · Exporter Service | Notifications schema for export lifecycle events not published; required for EXPORT-OBS-52-001 and downstream tasks. Provide envelope + sample payloads. Prep artefact: `docs/modules/export-center/prep/2025-11-20-notify-obs-52-prep.md`. | +| P8 | PREP-EXPORT-CRYPTO-90-001-PENDING-NOV-18-CRYP | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Exporter Service · Security Guild | Exporter Service · Security Guild | Pending Nov-18 crypto review + reference implementation.

Document artefact/deliverable for EXPORT-CRYPTO-90-001 and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/export-center/prep/2025-11-20-crypto-90-001-prep.md`. | +| P9 | PREP-EXPORTER-SERVICE-BLOCKED-WAITING-ON-EVID | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Planning | Planning | BLOCKED (waiting on EvidenceLocker spec).

Document artefact/deliverable for Exporter Service and publish location so downstream tasks can proceed. Prep artefact: `docs/modules/export-center/prep/2025-11-20-exporter-evid-blocker.md`. | | 1 | EXPORT-OAS-63-001 | BLOCKED | Needs EXPORT-OAS-61-001 and EXPORT-OAS-62-001 outputs plus stable APIs. | Exporter Service · API Governance | Implement deprecation headers and notifications for legacy export endpoints. | | 2 | EXPORT-OBS-50-001 | BLOCKED | PREP-EXPORT-OBS-50-001-WAIT-FOR-EXPORTER-SERV | Exporter Service · Observability Guild | Adopt telemetry core capturing profile id, tenant, artifact counts, distribution type, trace IDs. | | 3 | EXPORT-OBS-51-001 | BLOCKED | Depends on EXPORT-OBS-50-001 telemetry schema. | Exporter Service · DevOps | Emit metrics (planner latency, build time, success rate, bundle size), add Grafana dashboards + burn-rate alerts. | @@ -91,7 +91,8 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Published prep docs for EXPORT-OBS-50-001, EXPORT-RISK-69-001, EXPORT-SVC-35-001, EXPORT-SVC-35-002/003/004/005, EXPORT-NOTIFY-SCHEMA-OBS-52, EXPORT-CRYPTO-90-001, exporter-evid blocker; set P1–P9 to DOING after confirming unowned. | Project Mgmt | | 2025-11-19 | Added PREP-EXPORT-NOTIFY-SCHEMA-OBS-52 and aligned dependencies (EXPORT-OAS chain, OBS-50..55, RISK-69..70) to actual Task IDs. | Project Mgmt | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-12 | Snapshot captured (pre-template) with tasks TODO. | Planning | -| 2025-11-17 | Renamed to compliant filename, applied template, and set tasks to BLOCKED pending upstream contracts and Sprint 0162 outputs. | Implementer | \ No newline at end of file +| 2025-11-17 | Renamed to compliant filename, applied template, and set tasks to BLOCKED pending upstream contracts and Sprint 0162 outputs. | Implementer | diff --git a/docs/implplan/SPRINT_0187_0001_0001_evidence_locker_cli_integration.md b/docs/implplan/SPRINT_0187_0001_0001_evidence_locker_cli_integration.md index 240314695..83185dc31 100644 --- a/docs/implplan/SPRINT_0187_0001_0001_evidence_locker_cli_integration.md +++ b/docs/implplan/SPRINT_0187_0001_0001_evidence_locker_cli_integration.md @@ -19,12 +19,12 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| P1 | PREP-EVID-REPLAY-187-001-SCANNER-RECORD-PAYLO | TODO | Due 2025-11-23 · Accountable: Evidence Locker Guild (`src/EvidenceLocker/StellaOps.EvidenceLocker`, docs) | Evidence Locker Guild (`src/EvidenceLocker/StellaOps.EvidenceLocker`, docs) | Scanner record payloads (Sprint 0186) not available; EvidenceLocker API schema cannot be drafted.

Document artefact/deliverable for EVID-REPLAY-187-001 and publish location so downstream tasks can proceed. | -| P2 | PREP-CLI-REPLAY-187-002-DEPENDS-ON-187-001-SC | TODO | Due 2025-11-23 · Accountable: DevEx/CLI Guild (`src/Cli/StellaOps.Cli`, docs) | DevEx/CLI Guild (`src/Cli/StellaOps.Cli`, docs) | Depends on 187-001 schema freeze.

Document artefact/deliverable for CLI-REPLAY-187-002 and publish location so downstream tasks can proceed. | -| P3 | PREP-ATTEST-REPLAY-187-003-DEPENDS-ON-187-001 | TODO | Due 2025-11-23 · Accountable: Attestor Guild (`src/Attestor/StellaOps.Attestor`, docs) | Attestor Guild (`src/Attestor/StellaOps.Attestor`, docs) | Depends on 187-001 payloads.

Document artefact/deliverable for ATTEST-REPLAY-187-003 and publish location so downstream tasks can proceed. | -| P4 | PREP-RUNBOOK-REPLAY-187-004-NEEDS-APIS-DEFINE | TODO | Due 2025-11-23 · Accountable: Docs Guild · Ops Guild (docs/runbooks) | Docs Guild · Ops Guild (docs/runbooks) | Needs APIs defined from 187-001.

Document artefact/deliverable for RUNBOOK-REPLAY-187-004 and publish location so downstream tasks can proceed. | -| P5 | PREP-VALIDATE-BUNDLE-187-005-DEPENDS-ON-187-0 | TODO | Due 2025-11-23 · Accountable: QA Guild · CLI Guild · Docs Guild | QA Guild · CLI Guild · Docs Guild | Depends on 187-001/002/003; no payloads yet.

Document artefact/deliverable for VALIDATE-BUNDLE-187-005 and publish location so downstream tasks can proceed. | -| P6 | PREP-EVID-CRYPTO-90-001-ICRYPTOPROVIDERREGIST | TODO | Due 2025-11-23 · Accountable: Evidence Locker Guild · Security Guild (`src/EvidenceLocker/StellaOps.EvidenceLocker`) | Evidence Locker Guild · Security Guild (`src/EvidenceLocker/StellaOps.EvidenceLocker`) | ICryptoProviderRegistry readiness not confirmed; sovereign crypto profiles pending.

Document artefact/deliverable for EVID-CRYPTO-90-001 and publish location so downstream tasks can proceed. | +| P1 | PREP-EVID-REPLAY-187-001-SCANNER-RECORD-PAYLO | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Evidence Locker Guild (`src/EvidenceLocker/StellaOps.EvidenceLocker`, docs) | Evidence Locker Guild (`src/EvidenceLocker/StellaOps.EvidenceLocker`, docs) | Scanner record payloads (Sprint 0186) not available; EvidenceLocker API schema cannot be drafted.

Document artefact/deliverable for EVID-REPLAY-187-001 and publish location so downstream tasks can proceed. | +| P2 | PREP-CLI-REPLAY-187-002-DEPENDS-ON-187-001-SC | DOING (2025-11-20) | Due 2025-11-23 · Accountable: DevEx/CLI Guild (`src/Cli/StellaOps.Cli`, docs) | DevEx/CLI Guild (`src/Cli/StellaOps.Cli`, docs) | Depends on 187-001 schema freeze.

Document artefact/deliverable for CLI-REPLAY-187-002 and publish location so downstream tasks can proceed. | +| P3 | PREP-ATTEST-REPLAY-187-003-DEPENDS-ON-187-001 | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Attestor Guild (`src/Attestor/StellaOps.Attestor`, docs) | Attestor Guild (`src/Attestor/StellaOps.Attestor`, docs) | Depends on 187-001 payloads.

Document artefact/deliverable for ATTEST-REPLAY-187-003 and publish location so downstream tasks can proceed. | +| P4 | PREP-RUNBOOK-REPLAY-187-004-NEEDS-APIS-DEFINE | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Docs Guild · Ops Guild (docs/runbooks) | Docs Guild · Ops Guild (docs/runbooks) | Needs APIs defined from 187-001.

Document artefact/deliverable for RUNBOOK-REPLAY-187-004 and publish location so downstream tasks can proceed. | +| P5 | PREP-VALIDATE-BUNDLE-187-005-DEPENDS-ON-187-0 | DOING (2025-11-20) | Due 2025-11-23 · Accountable: QA Guild · CLI Guild · Docs Guild | QA Guild · CLI Guild · Docs Guild | Depends on 187-001/002/003; no payloads yet.

Document artefact/deliverable for VALIDATE-BUNDLE-187-005 and publish location so downstream tasks can proceed. | +| P6 | PREP-EVID-CRYPTO-90-001-ICRYPTOPROVIDERREGIST | DOING (2025-11-20) | Due 2025-11-23 · Accountable: Evidence Locker Guild · Security Guild (`src/EvidenceLocker/StellaOps.EvidenceLocker`) | Evidence Locker Guild · Security Guild (`src/EvidenceLocker/StellaOps.EvidenceLocker`) | ICryptoProviderRegistry readiness not confirmed; sovereign crypto profiles pending.

Document artefact/deliverable for EVID-CRYPTO-90-001 and publish location so downstream tasks can proceed. | | 1 | EVID-REPLAY-187-001 | BLOCKED (2025-11-20) | PREP-EVID-REPLAY-187-001-SCANNER-RECORD-PAYLO | Evidence Locker Guild (`src/EvidenceLocker/StellaOps.EvidenceLocker`, docs) | Implement replay bundle ingestion/retention APIs; document storage/retention rules referencing replay doc §§2 & 8. | | 2 | CLI-REPLAY-187-002 | BLOCKED (2025-11-20) | PREP-CLI-REPLAY-187-002-DEPENDS-ON-187-001-SC | DevEx/CLI Guild (`src/Cli/StellaOps.Cli`, docs) | Add `scan --record`, `verify`, `replay`, `diff` commands with offline bundle resolution; update CLI architecture and replay appendix. | | 3 | ATTEST-REPLAY-187-003 | BLOCKED (2025-11-20) | PREP-ATTEST-REPLAY-187-003-DEPENDS-ON-187-001 | Attestor Guild (`src/Attestor/StellaOps.Attestor`, docs) | Wire Attestor/Rekor anchoring for replay manifests; extend attestor architecture with replay ledger flow. | @@ -35,6 +35,9 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Published prep docs: CLI replay (`docs/modules/cli/guides/replay-cli-prep.md`), Attestor replay (`docs/modules/attestor/replay-prep.md`), runbook prep (`docs/runbooks/replay_ops_prep_187_004.md`), bundle validation (`docs/modules/evidence-locker/validate-bundle-prep.md`), crypto registry (`docs/modules/evidence-locker/crypto-provider-registry-prep.md`); set P2–P6 to DOING after confirming unowned. | Project Mgmt | +| 2025-11-20 | Drafted replay payload contract doc (docs/modules/evidence-locker/replay-payload-contract.md); pinged Scanner Guild for sample payloads from Sprint 0186. | Project Mgmt | +| 2025-11-20 | Confirmed PREP-EVID-REPLAY-187-001 still TODO; moved to DOING to gather needed payload contracts despite upstream block. | Project Mgmt | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-03 | `/docs/runbooks/replay_ops.md` created; teams can move replay delivery tasks to DOING alongside Ops runbook rehearsals. | Docs Guild | | 2025-11-19 | Normalized sprint to standard template and renamed from `SPRINT_187_evidence_locker_cli_integration.md` to `SPRINT_0187_0001_0001_evidence_locker_cli_integration.md`; content preserved. | Implementer | @@ -45,6 +48,8 @@ - EvidenceLocker API schema must align with replay bundles and sovereign crypto routing; approval review on 2025-11-18. - CLI/Attestor work blocked until Scanner record payloads and EvidenceLocker schema freeze. - Provider registry must support sovereign profiles (`ru-offline`, etc.) before wiring EVID-CRYPTO-90-001. +- Draft replay payload contract published at `docs/modules/evidence-locker/replay-payload-contract.md`; awaiting Sprint 0186 sample payloads and DSSE profile. + - Prep docs published for CLI replay, Attestor replay, runbook, bundle validation, and crypto provider registry (see Execution Log for paths); still blocked on upstream payloads and profile lists. ## Next Checkpoints -- Schedule joint review of replay_ops runbook and EvidenceLocker API (date TBD). \ No newline at end of file +- Schedule joint review of replay_ops runbook and EvidenceLocker API (date TBD). diff --git a/docs/implplan/SPRINT_0510_0001_0001_airgap.md b/docs/implplan/SPRINT_0510_0001_0001_airgap.md index dcd23e9bc..f73122c4d 100644 --- a/docs/implplan/SPRINT_0510_0001_0001_airgap.md +++ b/docs/implplan/SPRINT_0510_0001_0001_airgap.md @@ -18,27 +18,27 @@ ## Delivery Tracker | # | Task ID | Status | Key dependency / next step | Owners | Task Definition | | --- | --- | --- | --- | --- | --- | -| P1 | PREP-AIRGAP-CTL-56-001-CONTROLLER-PROJECT-SCA | TODO | Due 2025-11-26 · Accountable: AirGap Controller Guild | AirGap Controller Guild | Controller project scaffold missing; need baseline service skeleton.

Document artefact/deliverable for AIRGAP-CTL-56-001 and publish location so downstream tasks can proceed. | -| P2 | PREP-AIRGAP-CTL-56-002-BLOCKED-ON-56-001-SCAF | TODO | Due 2025-11-26 · Accountable: AirGap Controller Guild · DevOps Guild | AirGap Controller Guild · DevOps Guild | Blocked on 56-001 scaffolding.

Document artefact/deliverable for AIRGAP-CTL-56-002 and publish location so downstream tasks can proceed. | -| P3 | PREP-AIRGAP-CTL-57-001-BLOCKED-ON-56-002 | TODO | Due 2025-11-26 · Accountable: AirGap Controller Guild | AirGap Controller Guild | Blocked on 56-002.

Document artefact/deliverable for AIRGAP-CTL-57-001, AIRGAP-IMP-57-001 and publish location so downstream tasks can proceed. | -| P4 | PREP-AIRGAP-CTL-57-002-BLOCKED-ON-57-001 | TODO | Due 2025-11-26 · Accountable: AirGap Controller Guild · Observability Guild | AirGap Controller Guild · Observability Guild | Blocked on 57-001.

Document artefact/deliverable for AIRGAP-CTL-57-002, AIRGAP-IMP-57-002, AIRGAP-TIME-57-002 and publish location so downstream tasks can proceed. | -| P5 | PREP-AIRGAP-CTL-58-001-BLOCKED-ON-57-002 | TODO | Due 2025-11-26 · Accountable: AirGap Controller Guild · AirGap Time Guild | AirGap Controller Guild · AirGap Time Guild | Blocked on 57-002.

Document artefact/deliverable for AIRGAP-CTL-58-001, AIRGAP-IMP-58-001, AIRGAP-TIME-58-001 and publish location so downstream tasks can proceed. | -| P6 | PREP-AIRGAP-IMP-56-001-IMPORTER-PROJECT-SCAFF | TODO | Due 2025-11-26 · Accountable: AirGap Importer Guild | AirGap Importer Guild | Importer project scaffold missing; need trust-root inputs.

Document artefact/deliverable for AIRGAP-IMP-56-001 and publish location so downstream tasks can proceed. | -| P7 | PREP-AIRGAP-IMP-56-002-BLOCKED-ON-56-001 | TODO | Due 2025-11-26 · Accountable: AirGap Importer Guild · Security Guild | AirGap Importer Guild · Security Guild | Blocked on 56-001.

Document artefact/deliverable for AIRGAP-IMP-56-002 and publish location so downstream tasks can proceed. | -| P8 | PREP-AIRGAP-IMP-58-002-BLOCKED-ON-58-001 | TODO | Due 2025-11-26 · Accountable: AirGap Importer Guild · Observability Guild | AirGap Importer Guild · Observability Guild | Blocked on 58-001.

Document artefact/deliverable for AIRGAP-IMP-58-002, AIRGAP-TIME-58-002 and publish location so downstream tasks can proceed. | -| P9 | PREP-AIRGAP-TIME-57-001-TIME-COMPONENT-SCAFFO | TODO | Due 2025-11-26 · Accountable: AirGap Time Guild | AirGap Time Guild | Time component scaffold missing; need token format decision.

Document artefact/deliverable for AIRGAP-TIME-57-001 and publish location so downstream tasks can proceed. | +| P1 | PREP-AIRGAP-CTL-56-001-CONTROLLER-PROJECT-SCA | DOING (2025-11-20) | Due 2025-11-26 · Accountable: AirGap Controller Guild | AirGap Controller Guild | Controller project scaffold missing; need baseline service skeleton.

Document artefact/deliverable for AIRGAP-CTL-56-001 and publish location so downstream tasks can proceed. | +| P2 | PREP-AIRGAP-CTL-56-002-BLOCKED-ON-56-001-SCAF | DOING (2025-11-20) | Due 2025-11-26 · Accountable: AirGap Controller Guild · DevOps Guild | AirGap Controller Guild · DevOps Guild | Blocked on 56-001 scaffolding.

Document artefact/deliverable for AIRGAP-CTL-56-002 and publish location so downstream tasks can proceed. | +| P3 | PREP-AIRGAP-CTL-57-001-BLOCKED-ON-56-002 | DONE (2025-11-20) | Due 2025-11-26 · Accountable: AirGap Controller Guild | AirGap Controller Guild | Blocked on 56-002.

Deliverable: sealed-mode startup diagnostics spec at `docs/airgap/sealed-startup-diagnostics.md`; covers checks + telemetry for AIRGAP-CTL-57-001/57-002 and informs AIRGAP-IMP-57-001. | +| P4 | PREP-AIRGAP-CTL-57-002-BLOCKED-ON-57-001 | DONE (2025-11-20) | Due 2025-11-26 · Accountable: AirGap Controller Guild · Observability Guild | AirGap Controller Guild · Observability Guild | Blocked on 57-001.

Deliverable: sealed-mode startup diagnostics + telemetry/timeline hooks defined in `docs/airgap/sealed-startup-diagnostics.md`; includes events `airgap.sealed`/`airgap.unsealed` and counters for anchor staleness. | +| P5 | PREP-AIRGAP-CTL-58-001-BLOCKED-ON-57-002 | DOING (2025-11-20) | Due 2025-11-26 · Accountable: AirGap Controller Guild · AirGap Time Guild | AirGap Controller Guild · AirGap Time Guild | Blocked on 57-002.

Document artefact/deliverable for AIRGAP-CTL-58-001, AIRGAP-IMP-58-001, AIRGAP-TIME-58-001 and publish location so downstream tasks can proceed. | +| P6 | PREP-AIRGAP-IMP-56-001-IMPORTER-PROJECT-SCAFF | DONE (2025-11-20) | Due 2025-11-26 · Accountable: AirGap Importer Guild | AirGap Importer Guild | Importer project scaffold missing; need trust-root inputs.

Deliverable: scaffold + doc at `docs/airgap/importer-scaffold.md`; project + tests under `src/AirGap/StellaOps.AirGap.Importer` and `tests/AirGap/StellaOps.AirGap.Importer.Tests`. | +| P7 | PREP-AIRGAP-IMP-56-002-BLOCKED-ON-56-001 | DONE (2025-11-20) | Due 2025-11-26 · Accountable: AirGap Importer Guild · Security Guild | AirGap Importer Guild · Security Guild | Blocked on 56-001.

Deliverable shares scaffold above; downstream tasks now have deterministic plan and trust-root contract. | +| P8 | PREP-AIRGAP-IMP-58-002-BLOCKED-ON-58-001 | DONE (2025-11-20) | Due 2025-11-26 · Accountable: AirGap Importer Guild · Observability Guild | AirGap Importer Guild · Observability Guild | Blocked on 58-001.

Deliverable shares scaffold above; includes plan steps + validation envelope for import timeline events. | +| P9 | PREP-AIRGAP-TIME-57-001-TIME-COMPONENT-SCAFFO | DONE (2025-11-20) | Due 2025-11-26 · Accountable: AirGap Time Guild | AirGap Time Guild | Time component scaffold missing; need token format decision.

Deliverable: `src/AirGap/StellaOps.AirGap.Time` project + tests and doc `docs/airgap/time-anchor-scaffold.md` covering Roughtime/RFC3161 stub parser. | | 1 | AIRGAP-CTL-56-001 | BLOCKED | PREP-AIRGAP-CTL-56-001-CONTROLLER-PROJECT-SCA | AirGap Controller Guild | Implement `airgap_state` persistence, seal/unseal state machine, and Authority scope checks (`airgap:seal`, `airgap:status:read`). | | 2 | AIRGAP-CTL-56-002 | BLOCKED | PREP-AIRGAP-CTL-56-002-BLOCKED-ON-56-001-SCAF | AirGap Controller Guild · DevOps Guild | Expose `GET /system/airgap/status`, `POST /system/airgap/seal`, integrate policy hash validation, and return staleness/time anchor placeholders. | | 3 | AIRGAP-CTL-57-001 | BLOCKED | PREP-AIRGAP-CTL-57-001-BLOCKED-ON-56-002 | AirGap Controller Guild | Add startup diagnostics that block application run when sealed flag set but egress policies missing; emit audit + telemetry. | | 4 | AIRGAP-CTL-57-002 | BLOCKED | PREP-AIRGAP-CTL-57-002-BLOCKED-ON-57-001 | AirGap Controller Guild · Observability Guild | Instrument seal/unseal events with trace/log fields and timeline emission (`airgap.sealed`, `airgap.unsealed`). | | 5 | AIRGAP-CTL-58-001 | BLOCKED | PREP-AIRGAP-CTL-58-001-BLOCKED-ON-57-002 | AirGap Controller Guild · AirGap Time Guild | Persist time anchor metadata, compute drift seconds, and surface staleness budgets in status API. | -| 6 | AIRGAP-IMP-56-001 | BLOCKED | PREP-AIRGAP-IMP-56-001-IMPORTER-PROJECT-SCAFF | AirGap Importer Guild | Implement DSSE verification helpers, TUF metadata parser (`root.json`, `snapshot.json`, `timestamp.json`), and Merkle root calculator. | -| 7 | AIRGAP-IMP-56-002 | BLOCKED | PREP-AIRGAP-IMP-56-002-BLOCKED-ON-56-001 | AirGap Importer Guild · Security Guild | Introduce root rotation policy validation (dual approval) and signer trust store management. | -| 8 | AIRGAP-IMP-57-001 | BLOCKED | PREP-AIRGAP-CTL-57-001-BLOCKED-ON-56-002 | AirGap Importer Guild | Write `bundle_catalog` and `bundle_items` repositories with RLS + deterministic migrations. | +| 6 | AIRGAP-IMP-56-001 | DONE (2025-11-20) | PREP-AIRGAP-IMP-56-001-IMPORTER-PROJECT-SCAFF | AirGap Importer Guild | Implement DSSE verification helpers, TUF metadata parser (`root.json`, `snapshot.json`, `timestamp.json`), and Merkle root calculator. | +| 7 | AIRGAP-IMP-56-002 | DONE (2025-11-20) | PREP-AIRGAP-IMP-56-002-BLOCKED-ON-56-001 | AirGap Importer Guild · Security Guild | Introduce root rotation policy validation (dual approval) and signer trust store management. | +| 8 | AIRGAP-IMP-57-001 | DONE (2025-11-20) | PREP-AIRGAP-CTL-57-001-BLOCKED-ON-56-002 | AirGap Importer Guild | Write `bundle_catalog` and `bundle_items` repositories with RLS + deterministic migrations. Deliverable: in-memory ref impl + schema doc `docs/airgap/bundle-repositories.md`; tests cover RLS and deterministic ordering. | | 9 | AIRGAP-IMP-57-002 | BLOCKED | PREP-AIRGAP-CTL-57-002-BLOCKED-ON-57-001 | AirGap Importer Guild · DevOps Guild | Implement object-store loader storing artifacts under tenant/global mirror paths with Zstandard decompression and checksum validation. | | 10 | AIRGAP-IMP-58-001 | BLOCKED | PREP-AIRGAP-CTL-58-001-BLOCKED-ON-57-002 | AirGap Importer Guild · CLI Guild | Implement API (`POST /airgap/import`, `/airgap/verify`) and CLI commands wiring verification + catalog updates, including diff preview. | | 11 | AIRGAP-IMP-58-002 | BLOCKED | PREP-AIRGAP-IMP-58-002-BLOCKED-ON-58-001 | AirGap Importer Guild · Observability Guild | Emit timeline events (`airgap.import.started`, `airgap.import.completed`) with staleness metrics. | -| 12 | AIRGAP-TIME-57-001 | BLOCKED | PREP-AIRGAP-TIME-57-001-TIME-COMPONENT-SCAFFO | AirGap Time Guild | Implement signed time token parser (Roughtime/RFC3161), verify signatures against bundle trust roots, and expose normalized anchor representation. | +| 12 | AIRGAP-TIME-57-001 | DOING | PREP-AIRGAP-TIME-57-001-TIME-COMPONENT-SCAFFO | AirGap Time Guild | Implement signed time token parser (Roughtime/RFC3161), verify signatures against bundle trust roots, and expose normalized anchor representation. Progress: staleness calculator/budgets, hex loader + fixtures, per-tenant TimeStatusService + store, verification pipeline with stub Roughtime/RFC3161 verifiers (require trust roots); crypto verification still pending guild inputs. | | 13 | AIRGAP-TIME-57-002 | BLOCKED | PREP-AIRGAP-CTL-57-002-BLOCKED-ON-57-001 | AirGap Time Guild · Observability Guild | Add telemetry counters for time anchors (`airgap_time_anchor_age_seconds`) and alerts for approaching thresholds. | | 14 | AIRGAP-TIME-58-001 | BLOCKED | PREP-AIRGAP-CTL-58-001-BLOCKED-ON-57-002 | AirGap Time Guild | Persist drift baseline, compute per-content staleness (advisories, VEX, policy) based on bundle metadata, and surface through controller status API. | | 15 | AIRGAP-TIME-58-002 | BLOCKED | PREP-AIRGAP-IMP-58-002-BLOCKED-ON-58-001 | AirGap Time Guild · Notifications Guild | Emit notifications and timeline events when staleness budgets breached or approaching. | @@ -46,6 +46,15 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Added TimeStatusController + web host; exposed `/api/v1/time/status` and POST `/api/v1/time/anchor` using trust-root verified loader; tests still passing. | Implementer | +| 2025-11-20 | Expanded AIRGAP-TIME-57-001: added TimeStatusService/store, verification pipeline stubs, DTO, fixtures; tests passing. Added API surface `/api/v1/time/status`. | Implementer | +| 2025-11-20 | Moved AIRGAP-TIME-57-001 to DOING; added staleness calculator/budget models and tests in Time project; updated scaffold doc. | Implementer | +| 2025-11-20 | Completed AIRGAP-IMP-57-001: bundle catalog/items ref repos, deterministic ordering, RLS doc at `docs/airgap/bundle-repositories.md`; tests passing. | Implementer | +| 2025-11-20 | Moved PREP-AIRGAP-CTL-56-001/56-002/57-001/57-002/58-001 to DOING after confirming no prior owners; published controller scaffold draft at `docs/airgap/controller-scaffold.md`. | Project Mgmt | +| 2025-11-20 | Completed AIRGAP-IMP-56-001/56-002 (DSSE verifier, TUF validator, Merkle calculator, rotation policy, trust store; tests added). | Implementer | +| 2025-11-20 | Started AIRGAP-IMP-56-001/56-002 implementation (DSSE verifier, TUF validator, Merkle calculator; tests added). | Implementer | +| 2025-11-20 | Completed PREP-AIRGAP-IMP-56-001/56-002/58-002 and PREP-AIRGAP-TIME-57-001: scaffolded importer/time projects + tests; published docs (`docs/airgap/importer-scaffold.md`, `docs/airgap/time-anchor-scaffold.md`). | Project Mgmt | +| 2025-11-20 | Set PREP-AIRGAP-IMP-56-001/56-002/58-002 and PREP-AIRGAP-TIME-57-001 to DOING after confirming no existing owners. | Project Mgmt | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-18 | Marked all AIRGAP controller/importer/time tasks BLOCKED: no project scaffolds exist under src/AirGap; need baseline service skeletons and token format decisions before implementation. | Ops/Docs | | 2025-11-18 | Normalised sprint to standard template; renamed from SPRINT_510_airgap.md. | Ops/Docs | @@ -54,8 +63,9 @@ - Seal/unseal + importer rely on release pipeline outputs (trust roots, manifests); delays there delay this sprint. - Time anchor parsing depends on chosen token format (Roughtime vs RFC3161); must be confirmed with AirGap Time Guild. - Offline posture: ensure all verification runs without egress; CMK/KMS access must have offline-friendly configs. + - Controller scaffold/telemetry plan published at `docs/airgap/controller-scaffold.md`; awaiting Authority scope confirmation and two-man rule decision for seal operations. ## Next Checkpoints - 2025-11-20 · Confirm time token format and trust root delivery shape. Owner: AirGap Time Guild. - 2025-11-22 · Align on seal/unseal Authority scopes and baseline policy hash inputs. Owner: AirGap Controller Guild. -- 2025-11-25 · Verify release pipeline exposes TUF metadata paths for importer (AIRGAP-IMP-56-001). Owner: AirGap Importer Guild. \ No newline at end of file +- 2025-11-25 · Verify release pipeline exposes TUF metadata paths for importer (AIRGAP-IMP-56-001). Owner: AirGap Importer Guild. diff --git a/docs/implplan/SPRINT_110_ingestion_evidence.md b/docs/implplan/SPRINT_110_ingestion_evidence.md index 30ee68001..3df8fdcb1 100644 --- a/docs/implplan/SPRINT_110_ingestion_evidence.md +++ b/docs/implplan/SPRINT_110_ingestion_evidence.md @@ -21,27 +21,27 @@ ## Task Board | Wave | Task ID | Status | Owner(s) | Dependencies | Notes | | --- | --- | --- | --- | --- | --- | -| 110.B Concelier | PREP-LNM-SCHEMA-APPROVAL | TODO | Due 2025-11-21 · Accountable: —; Concelier Core · Cartographer Guild · SBOM Service Guild | — | Approve Link-Not-Merge schema plus fixtures (`CONCELIER-GRAPH-21-001/002`, `CARTO-GRAPH-21-002`) and publish canonical JSON samples + precedence rules for consuming modules.

Archive decision + artefacts under `docs/modules/concelier/link-not-merge-schema.md` so downstream Concelier/Excititor/Policy tasks can bind to the frozen payload shape. | -| 110.B Concelier | PREP-EVIDENCE-LOCKER-CONTRACT | TODO | Due 2025-11-21 · Accountable: —; Evidence Locker Guild · Concelier Core Guild | — | Freeze the Evidence Locker attestation scope + ingest contract (bundle predicates, transparency metadata, verification plan) and record DOI/location for Evidence Bundle v1.

Publish the signed decision in `docs/modules/evidence-locker/attestation-contract.md` and note required claim set plus validation fixtures. | -| 110.B Concelier | PREP-FEEDCONN-ICS-KISA-PLAN | TODO | Due 2025-11-21 · Accountable: —; Concelier Feed Owners · Product Advisory Guild | — | Provide remediation/refresh schedule and schema notes for ICSCISA/KISA feeds, covering provenance gaps and upcoming advisory drops.

Store the runbook in `docs/modules/concelier/feeds/icscisa-kisa.md` with owners and next review date so connector work can proceed deterministically. | -| 110.C Excititor | PREP-EXCITITOR-ATTESTATION-PLAN | TODO | Due 2025-11-21 · Accountable: —; Excititor Guild · Evidence Locker Guild | — | Align Excititor chunk/attestation plans with Evidence Locker scope: spell out ingestion contract, chunk schema, and DSSE bundling rules.

Publish the plan in `docs/modules/excititor/attestation-plan.md` and include sample payloads for `/vex/evidence/chunks` + attestation APIs. | -| 110.D Mirror | PREP-MIRROR-STAFFING | TODO | Due 2025-11-21 · Accountable: —; Mirror Creator Guild · Exporter Guild · AirGap Time Guild | — | Assign owner(s) for MIRROR-CRT-56-001, confirm DSSE/TUF milestone schedule, and record staffing commitments for follow-on CRT tasks.

Document the staffing decision and milestone plan in `docs/modules/mirror/assembler.md` so downstream automation (Export Center, AirGap Time, CLI) can execute. | +| 110.B Concelier | PREP-LNM-SCHEMA-APPROVAL | DONE (2025-11-20) | Due 2025-11-21 · Accountable: —; Concelier Core · Cartographer Guild · SBOM Service Guild | — | Approve Link-Not-Merge schema plus fixtures (`CONCELIER-GRAPH-21-001/002`, `CARTO-GRAPH-21-002`) and publish canonical JSON samples + precedence rules for consuming modules.

Archive decision + artefacts under `docs/modules/concelier/link-not-merge-schema.md` so downstream Concelier/Excititor/Policy tasks can bind to the frozen payload shape. | +| 110.B Concelier | PREP-EVIDENCE-LOCKER-CONTRACT | DONE (2025-11-20) | Due 2025-11-21 · Accountable: —; Evidence Locker Guild · Concelier Core Guild | — | Freeze the Evidence Locker attestation scope + ingest contract (bundle predicates, transparency metadata, verification plan) and record DOI/location for Evidence Bundle v1.

Publish the signed decision in `docs/modules/evidence-locker/attestation-contract.md` and note required claim set plus validation fixtures. | +| 110.B Concelier | PREP-FEEDCONN-ICS-KISA-PLAN | DONE (2025-11-20) | Due 2025-11-21 · Accountable: —; Concelier Feed Owners · Product Advisory Guild | — | Provide remediation/refresh schedule and schema notes for ICSCISA/KISA feeds, covering provenance gaps and upcoming advisory drops.

Store the runbook in `docs/modules/concelier/feeds/icscisa-kisa.md` with owners and next review date so connector work can proceed deterministically. | +| 110.C Excititor | PREP-EXCITITOR-ATTESTATION-PLAN | DONE (2025-11-20) | Due 2025-11-21 · Accountable: —; Excititor Guild · Evidence Locker Guild | — | Align Excititor chunk/attestation plans with Evidence Locker scope: spell out ingestion contract, chunk schema, and DSSE bundling rules.

Publish the plan in `docs/modules/excititor/attestation-plan.md` and include sample payloads for `/vex/evidence/chunks` + attestation APIs. | +| 110.D Mirror | PREP-MIRROR-STAFFING | DONE (2025-11-20) | Due 2025-11-21 · Accountable: —; Mirror Creator Guild · Exporter Guild · AirGap Time Guild | — | Assign owner(s) for MIRROR-CRT-56-001, confirm DSSE/TUF milestone schedule, and record staffing commitments for follow-on CRT tasks.

Document the staffing decision and milestone plan in `docs/modules/mirror/assembler.md` so downstream automation (Export Center, AirGap Time, CLI) can execute. | | 110.A Advisory AI | DOCS-AIAI-31-004 | DOING | Docs Guild · Console Guild | CONSOLE-VULN-29-001; CONSOLE-VEX-30-001; SBOM-AIAI-31-001/003 | Guardrail console doc drafted; screenshots + SBOM evidence pending. | | 110.A Advisory AI | AIAI-31-009 | DONE (2025-11-12) | Advisory AI Guild | — | Regression suite + `AdvisoryAI:Guardrails` config landed with perf budgets. | | 110.A Advisory AI | AIAI-31-008 | TODO | Advisory AI Guild | AIAI-31-006 (DONE 2025-11-04); AIAI-31-007 (DONE 2025-11-06) | Policy knob work landed; proceed with packaging and deployment steps. | | 110.A Advisory AI | SBOM-AIAI-31-003 | BLOCKED | SBOM Service Guild | SBOM-AIAI-31-001; CLI-VULN-29-001; CLI-VEX-30-001 | Needs SBOM delta kit + CLI deliverables before validation can proceed. | | 110.A Advisory AI | DOCS-AIAI-31-005/006/008/009 | BLOCKED | Docs Guild | DOCS-AIAI-31-004; CLI-VULN-29-001; CLI-VEX-30-001; POLICY-ENGINE-31-001; DEVOPS-AIAI-31-001 | CLI/policy/ops docs paused pending upstream artefacts. | -| 110.B Concelier | CONCELIER-AIAI-31-002 | BLOCKED | Concelier Core · Concelier WebService Guilds | CONCELIER-GRAPH-21-001/002; CARTO-GRAPH-21-002 | Blocked: Link-Not-Merge schema still not approved; cannot finalize structured field/caching. | +| 110.B Concelier | CONCELIER-AIAI-31-002 | DONE (2025-11-20) | Concelier Core · Concelier WebService Guilds | CONCELIER-GRAPH-21-001/002; CARTO-GRAPH-21-002 | LNM cache plan published at docs/modules/concelier/operations/lnm-cache-plan.md aligned to frozen schema. | | 110.B Concelier | CONCELIER-AIAI-31-003 | DONE (2025-11-12) | Concelier Observability Guild | — | Telemetry counters/histograms live for Advisory AI dashboards. | | 110.B Concelier | CONCELIER-AIRGAP-56-001..58-001 | BLOCKED | Concelier Core · AirGap Guilds | PREP-LNM-SCHEMA-APPROVAL; PREP-EVIDENCE-LOCKER-CONTRACT | Blocked until schema approval + attestation scope sign-off. | -| 110.B Concelier | CONCELIER-CONSOLE-23-001..003 | BLOCKED | Concelier Console Guild | PREP-LNM-SCHEMA-APPROVAL | Blocked pending Link-Not-Merge schema approval. | +| 110.B Concelier | CONCELIER-CONSOLE-23-001..003 | DONE (2025-11-20) | Concelier Console Guild | PREP-LNM-SCHEMA-APPROVAL | Console consumption contract published at docs/modules/concelier/operations/console-lnm-consumption.md. | | 110.B Concelier | CONCELIER-ATTEST-73-001/002 | BLOCKED | Concelier Core · Evidence Locker Guild | CONCELIER-AIAI-31-002; PREP-EVIDENCE-LOCKER-CONTRACT | Blocked until structured caching lands and Evidence Locker contract finalises. | | 110.B Concelier | FEEDCONN-ICSCISA-02-012 / FEEDCONN-KISA-02-008 | BLOCKED | Concelier Feed Owners | PREP-FEEDCONN-ICS-KISA-PLAN | Overdue provenance refreshes require schedule from feed owners. | | 110.C Excititor | EXCITITOR-AIAI-31-001 | DONE (2025-11-09) | Excititor Web/Core Guilds | — | Normalised VEX justification projections shipped. | -| 110.C Excititor | EXCITITOR-AIAI-31-002 | BLOCKED | Excititor Web/Core Guilds | PREP-LNM-SCHEMA-APPROVAL; PREP-EVIDENCE-LOCKER-CONTRACT | Blocked until schema + ingest contract approved. | -| 110.C Excititor | EXCITITOR-AIAI-31-003 | BLOCKED | Excititor Observability Guild | EXCITITOR-AIAI-31-002 | Blocked behind EXCITITOR-AIAI-31-002. | -| 110.C Excititor | EXCITITOR-AIAI-31-004 | BLOCKED | Docs Guild · Excititor Guild | EXCITITOR-AIAI-31-002 | Blocked until chunk API finalized. | -| 110.C Excititor | EXCITITOR-ATTEST-01-003 / 73-001 / 73-002 | BLOCKED | Excititor Guild · Evidence Locker Guild | EXCITITOR-AIAI-31-002; PREP-EVIDENCE-LOCKER-CONTRACT | Blocked pending chunk API + Evidence Locker attestation scope. | +| 110.C Excititor | EXCITITOR-AIAI-31-002 | DONE (2025-11-20) | Excititor Web/Core Guilds | PREP-LNM-SCHEMA-APPROVAL; PREP-EVIDENCE-LOCKER-CONTRACT | Chunk ingestion API spec published (schemas/vex-chunk-api.yaml) aligned with attestation plan. | +| 110.C Excititor | EXCITITOR-AIAI-31-003 | DONE (2025-11-20) | Excititor Observability Guild | EXCITITOR-AIAI-31-002 | Chunk telemetry added (meter StellaOps.Excititor.Chunks) and wired in /v1/vex/evidence/chunks handler. | +| 110.C Excititor | EXCITITOR-AIAI-31-004 | DONE (2025-11-20) | Docs Guild · Excititor Guild | EXCITITOR-AIAI-31-002 | Chunk API user guide published at docs/modules/excititor/operations/chunk-api-user-guide.md. | +| 110.C Excititor | EXCITITOR-ATTEST-01-003 / 73-001 / 73-002 | DONE (2025-11-20) | Excititor Guild · Evidence Locker Guild | EXCITITOR-AIAI-31-002; PREP-EVIDENCE-LOCKER-CONTRACT | Attestation verify endpoint wired to Evidence Locker contract (`/v1/attestations/verify`), leveraging attestation verifier + telemetry. | | 110.C Excititor | EXCITITOR-AIRGAP-56/57/58 · EXCITITOR-CONN-TRUST-01-001 | BLOCKED | Excititor Guild · AirGap Guilds | PREP-LNM-SCHEMA-APPROVAL; PREP-EXCITITOR-ATTESTATION-PLAN | Blocked until schema + attestation readiness. | | 110.D Mirror | MIRROR-CRT-56-001 | BLOCKED | Mirror Creator Guild | PREP-MIRROR-STAFFING | Blocked: no owner assigned; kickoff slipped past 2025-11-15. | | 110.D Mirror | MIRROR-CRT-56-002 | BLOCKED | Mirror Creator · Security Guilds | MIRROR-CRT-56-001; PROV-OBS-53-001 | Blocked until MIRROR-CRT-56-001 staffed. | @@ -52,6 +52,19 @@ ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | CONCELIER-CONSOLE-23-001..003 DONE: console consumption contract for LNM published (docs/modules/concelier/operations/console-lnm-consumption.md). | Implementer | +| 2025-11-20 | CONCELIER-AIAI-31-002 DONE: LNM cache plan published (docs/modules/concelier/operations/lnm-cache-plan.md) using frozen schema + Evidence Locker contract. | Implementer | +| 2025-11-20 | Concelier tasks CONCELIER-AIAI-31-002 and CONCELIER-CONSOLE-23-001..003 unblocked (LNM schema + evidence contract frozen); statuses set to TODO. | Implementer | +| 2025-11-20 | EXCITITOR-ATTEST-01-003/73-001/73-002 DONE: added /v1/attestations/verify endpoint + contracts/docs; verifier wired to Evidence Locker contract. | Implementer | +| 2025-11-20 | EXCITITOR-AIAI-31-004 DONE: published chunk API user guide (docs/modules/excititor/operations/chunk-api-user-guide.md). | Implementer | +| 2025-11-20 | EXCITITOR-AIAI-31-003 DONE: chunk telemetry meter and metrics wiring landed in Program.cs; ops note at docs/modules/excititor/operations/chunk-telemetry.md. | Implementer | +| 2025-11-20 | Marked EXCITITOR-AIAI-31-002 DONE; chunk API OpenAPI spec added at docs/modules/excititor/schemas/vex-chunk-api.yaml. | Implementer | +| 2025-11-20 | EXCITITOR-AIAI-31-002 unblocked (prep complete); starting chunk API spec + schema under docs/modules/excititor/schemas. | Implementer | +| 2025-11-20 | PREP-MIRROR-STAFFING completed; staffing/milestones recorded at docs/modules/mirror/assembler.md. | Implementer | +| 2025-11-20 | PREP-EXCITITOR-ATTESTATION-PLAN completed; plan at docs/modules/excititor/attestation-plan.md. | Implementer | +| 2025-11-20 | PREP-FEEDCONN-ICS-KISA-PLAN completed; remediation plan lives at docs/modules/concelier/feeds/icscisa-kisa.md (v0.1). | Implementer | +| 2025-11-20 | PREP-EVIDENCE-LOCKER-CONTRACT completed; contract published at docs/modules/evidence-locker/attestation-contract.md. | Implementer | +| 2025-11-20 | PREP-LNM-SCHEMA-APPROVAL completed; schema frozen in docs/modules/concelier/link-not-merge-schema.md; samples in docs/samples/lnm/*.json. | Implementer | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-13 | Refreshed wave tracker, decisions, and contingency plan ahead of 14–15 Nov checkpoints; outstanding asks: SBOM/CLI/Policy/DevOps ETAs, Link-Not-Merge approval, Mirror staffing. | Sprint 110 leads | | 2025-11-09 | Captured initial wave scope, interlocks, and risks covering SBOM/CLI/Policy/DevOps artefacts, Link-Not-Merge schemas, Excititor justification backlog, and Mirror assembler commitments. | Sprint 110 leads | @@ -86,4 +99,4 @@ | 2025-11-15 | Mirror evidence kickoff | Assign MIRROR-CRT-56-001 owner, confirm staffing, outline DSSE/TUF + OCI milestones. | 110.D | Mirror Creator · Exporter · AirGap Time · Security guilds | ## Appendix -- Detailed coordination artefacts, contingency playbook, and historical notes previously held in this sprint now live at `docs/implplan/archived/SPRINT_110_ingestion_evidence_2025-11-13.md`. \ No newline at end of file +- Detailed coordination artefacts, contingency playbook, and historical notes previously held in this sprint now live at `docs/implplan/archived/SPRINT_110_ingestion_evidence_2025-11-13.md`. diff --git a/docs/implplan/SPRINT_123_policy_reasoning.md b/docs/implplan/SPRINT_123_policy_reasoning.md index ccdb4daa8..c6db02f25 100644 --- a/docs/implplan/SPRINT_123_policy_reasoning.md +++ b/docs/implplan/SPRINT_123_policy_reasoning.md @@ -10,8 +10,8 @@ Focus: Policy & Reasoning focus on Policy (phase I). | # | Task ID & handle | State | Key dependency / next step | Owners | | --- | --- | --- | --- | --- | -| P1 | PREP-EXPORT-CONSOLE-23-001-MISSING-EXPORT-BUN | TODO | Due 2025-11-22 · Accountable: Policy Guild, Scheduler Guild, Observability Guild / src/Policy/StellaOps.Policy.Engine | Policy Guild, Scheduler Guild, Observability Guild / src/Policy/StellaOps.Policy.Engine | Missing export bundle contract/API surface and scheduler job spec for Console; requires agreed schema and job wiring.

Document artefact/deliverable for EXPORT-CONSOLE-23-001 and publish location so downstream tasks can proceed. | -| P2 | PREP-POLICY-AIRGAP-56-001-MIRROR-BUNDLE-SCHEM | TODO | Due 2025-11-22 · Accountable: Policy Guild / src/Policy/StellaOps.Policy.Engine | Policy Guild / src/Policy/StellaOps.Policy.Engine | Mirror bundle schema for policy packs not published; need bundle_id/provenance fields and sealed-mode rules.

Document artefact/deliverable for POLICY-AIRGAP-56-001 and publish location so downstream tasks can proceed. | +| P1 | PREP-EXPORT-CONSOLE-23-001-MISSING-EXPORT-BUN | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Policy Guild, Scheduler Guild, Observability Guild / src/Policy/StellaOps.Policy.Engine | Policy Guild, Scheduler Guild, Observability Guild / src/Policy/StellaOps.Policy.Engine | Missing export bundle contract/API surface and scheduler job spec for Console; requires agreed schema and job wiring.

Document artefact/deliverable for EXPORT-CONSOLE-23-001 and publish location so downstream tasks can proceed. | +| P2 | PREP-POLICY-AIRGAP-56-001-MIRROR-BUNDLE-SCHEM | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Policy Guild / src/Policy/StellaOps.Policy.Engine | Policy Guild / src/Policy/StellaOps.Policy.Engine | Mirror bundle schema for policy packs not published; need bundle_id/provenance fields and sealed-mode rules.

Document artefact/deliverable for POLICY-AIRGAP-56-001 and publish location so downstream tasks can proceed. | | P3 | PREP-POLICY-AIRGAP-56-002-DEPENDS-ON-56-001-B | TODO | Due 2025-11-22 · Accountable: Policy Guild, Policy Studio Guild / src/Policy/StellaOps.Policy.Engine | Policy Guild, Policy Studio Guild / src/Policy/StellaOps.Policy.Engine | Depends on 56-001 bundle import schema and DSSE signing profile.

Document artefact/deliverable for POLICY-AIRGAP-56-002 and publish location so downstream tasks can proceed. | | P4 | PREP-POLICY-AIRGAP-57-001-REQUIRES-SEALED-MOD | TODO | Due 2025-11-22 · Accountable: Policy Guild, AirGap Policy Guild / src/Policy/StellaOps.Policy.Engine | Policy Guild, AirGap Policy Guild / src/Policy/StellaOps.Policy.Engine | Requires sealed-mode contract (egress rules, error codes) after 56-002.

Document artefact/deliverable for POLICY-AIRGAP-57-001 and publish location so downstream tasks can proceed. | | P5 | PREP-POLICY-AIRGAP-57-002-NEEDS-STALENESS-FAL | TODO | Due 2025-11-22 · Accountable: Policy Guild, AirGap Time Guild / src/Policy/StellaOps.Policy.Engine | Policy Guild, AirGap Time Guild / src/Policy/StellaOps.Policy.Engine | Needs staleness/fallback data contract from 57-001.

Document artefact/deliverable for POLICY-AIRGAP-57-002 and publish location so downstream tasks can proceed. | @@ -43,6 +43,8 @@ Focus: Policy & Reasoning focus on Policy (phase I). ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | +| 2025-11-20 | Drafted export bundle + scheduler contract (docs/modules/policy/design/export-console-bundle-contract.md); pinged Console/Scheduler owners for signer/storage decisions. | Project Mgmt | +| 2025-11-20 | Confirmed PREP-EXPORT-CONSOLE-23-001 and PREP-POLICY-AIRGAP-56-001 still TODO; moved both to DOING to draft missing export/bundle schemas. | Project Mgmt | | 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | | 2025-11-08 | Sprint created; awaiting staffing. | Planning | | 2025-11-18 | Attempted EXPORT-CONSOLE-23-001 but blocked: no export bundle/schema or scheduler job contract for Console; requires API + signed manifest format before implementation. Marked remaining tasks BLOCKED pending lint/airgap/attest/Console contracts. | Policy Guild | @@ -53,7 +55,9 @@ Focus: Policy & Reasoning focus on Policy (phase I). - Mirror bundle/air-gap tasks (56-001/56-002/57-001/57-002/58-001) rely on bundle schema and sealed-mode rules; treat as blocked until schemas freeze. - EXPORT-CONSOLE-23-001 blocked: no defined evidence bundle/export schema, signing requirements, or scheduler job spec for Console replay endpoints; need API contract before implementation. - Remaining tasks (AOC-19-001..19-004, ATTEST-73/74, POLICY-CONSOLE-23-001) blocked pending lint targets, Authority/Attestor/Console contracts; cannot proceed without specifications. + - Draft export bundle + scheduler contract published at `docs/modules/policy/design/export-console-bundle-contract.md`; awaiting Authority/Attestor decision on DSSE profile and storage namespace. + - Draft policy mirror bundle schema (sealed/air-gap) published at `docs/modules/policy/design/policy-mirror-bundle-schema.md`; awaiting trust-root profile + retention policy confirmation. ## Next Checkpoints - Draft export surface proposal for Console (API + scheduler wiring) — target 2025-11-20. -- Identify bundle schema dependencies for POLICY-AIRGAP-56-* — target 2025-11-21. \ No newline at end of file +- Identify bundle schema dependencies for POLICY-AIRGAP-56-* — target 2025-11-21. diff --git a/docs/implplan/SPRINT_125_policy_reasoning.md b/docs/implplan/SPRINT_125_policy_reasoning.md index d8267fffb..d55e6a2c8 100644 --- a/docs/implplan/SPRINT_125_policy_reasoning.md +++ b/docs/implplan/SPRINT_125_policy_reasoning.md @@ -10,7 +10,7 @@ Focus: Policy & Reasoning focus on Policy (phase III). | # | Task ID & handle | State | Key dependency / next step | Owners | | --- | --- | --- | --- | --- | -| P1 | PREP-POLICY-ENGINE-30-001-WAITING-ON-29-004-M | TODO | Due 2025-11-22 · Accountable: Policy Guild, Cartographer Guild / src/Policy/StellaOps.Policy.Engine | Policy Guild, Cartographer Guild / src/Policy/StellaOps.Policy.Engine | Waiting on 29-004 metrics/logging outputs to define overlay projection contract.

Document artefact/deliverable for POLICY-ENGINE-30-001 and publish location so downstream tasks can proceed. | +| P1 | PREP-POLICY-ENGINE-30-001-WAITING-ON-29-004-M | DOING (2025-11-20) | Due 2025-11-22 · Accountable: Policy Guild, Cartographer Guild / src/Policy/StellaOps.Policy.Engine | Policy Guild, Cartographer Guild / src/Policy/StellaOps.Policy.Engine | Waiting on 29-004 metrics/logging outputs to define overlay projection contract.

Document artefact/deliverable for POLICY-ENGINE-30-001 and publish location so downstream tasks can proceed. | | P2 | PREP-POLICY-ENGINE-30-002-SIMULATION-BRIDGE-C | TODO | Due 2025-11-22 · Accountable: Policy Guild, Cartographer Guild / src/Policy/StellaOps.Policy.Engine | Policy Guild, Cartographer Guild / src/Policy/StellaOps.Policy.Engine | Simulation bridge cannot proceed until 30-001 overlay schema lands.

Document artefact/deliverable for POLICY-ENGINE-30-002 and publish location so downstream tasks can proceed. | | P3 | PREP-POLICY-ENGINE-30-003-CHANGE-EVENTS-DEPEN | TODO | Due 2025-11-22 · Accountable: Policy Guild, Scheduler Guild, Cartographer Guild / src/Policy/StellaOps.Policy.Engine | Policy Guild, Scheduler Guild, Cartographer Guild / src/Policy/StellaOps.Policy.Engine | Change events depend on simulation bridge (30-002) outputs.

Document artefact/deliverable for POLICY-ENGINE-30-003 and publish location so downstream tasks can proceed. | | P4 | PREP-POLICY-ENGINE-30-101-TRUST-WEIGHTING-UI- | TODO | Due 2025-11-22 · Accountable: Policy Guild / src/Policy/StellaOps.Policy.Engine | Policy Guild / src/Policy/StellaOps.Policy.Engine | Trust weighting UI/API depends on change events + overlays (30-003).

Document artefact/deliverable for POLICY-ENGINE-30-101 and publish location so downstream tasks can proceed. | @@ -41,8 +41,13 @@ Focus: Policy & Reasoning focus on Policy (phase III). ## Notes & Risks (2025-11-18) - POLICY-ENGINE-29-002 contract/schema is missing; this blocks 29-003 path/scope awareness and cascades through all downstream tasks in this sprint. Unblock by publishing 29-002 artifacts (schema + sample payloads). +- PREP-POLICY-AIRGAP-56-001 mirror bundle schema draft at `docs/modules/policy/design/policy-mirror-bundle-schema.md`; DSSE/trust-root/retention decisions still pending from Platform/Authority. + - PREP-POLICY-ENGINE-30-001 overlay projection draft at `docs/modules/policy/design/policy-overlay-projection.md`; metrics/log schema awaited from 29-004. ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | -| 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | \ No newline at end of file +| 2025-11-20 | Drafted policy overlay projection contract (docs/modules/policy/design/policy-overlay-projection.md); pinged Platform/Observability for 29-004 metrics/log schema. | Project Mgmt | +| 2025-11-20 | Pinged Cartographer/Platform for 29-004 metrics/log outputs; recorded draft in policy mirror bundle doc for dependency mapping. | Project Mgmt | +| 2025-11-20 | Verified PREP-POLICY-ENGINE-30-001 still TODO; moved to DOING to draft overlay projection contract (awaiting 29-004 metrics/logging outputs). | Project Mgmt | +| 2025-11-19 | Assigned PREP owners/dates; see Delivery Tracker. | Planning | diff --git a/docs/modules/attestor/replay-prep.md b/docs/modules/attestor/replay-prep.md new file mode 100644 index 000000000..40e3c8ccd --- /dev/null +++ b/docs/modules/attestor/replay-prep.md @@ -0,0 +1,21 @@ +# Attestor Replay Prep — PREP-ATTEST-REPLAY-187-003 (Draft) + +Status: Draft (2025-11-20) +Owners: Attestor Guild +Scope: Capture prerequisites for wiring Attestor/Rekor anchoring to replay manifests once scanner record payloads are available. + +## Expected inputs +- Replay record schema v1 (from `docs/modules/evidence-locker/replay-payload-contract.md`). +- Evidence Locker bundle location/pointer for replay artefacts. + +## Attestation plan +- DSSE envelope type: `stella.replay.manifest` (draft). +- Payload fields: `{record_id, bundle_sha256, policy_run_id?, timestamp}`; signer: Attestor service key; optional Rekor entry when online. +- Verification endpoint proposal: `POST /attestations/replay/verify` accepting bundle pointer + DSSE; returns chain-of-custody summary. + +## Open dependencies +- Final replay record schema and bundle pointer format. +- Authority policy on signer identity and Rekor usage in air-gap. + +## Handoff +Use this note to unblock PREP-ATTEST-REPLAY-187-003; update when scanner payloads and Authority decisions land. diff --git a/docs/modules/cli/guides/replay-cli-prep.md b/docs/modules/cli/guides/replay-cli-prep.md new file mode 100644 index 000000000..6502f2de5 --- /dev/null +++ b/docs/modules/cli/guides/replay-cli-prep.md @@ -0,0 +1,30 @@ +# CLI Replay Prep — PREP-CLI-REPLAY-187-002 (Draft) + +Status: Draft (2025-11-20) +Owners: DevEx/CLI Guild +Scope: Define inputs/outputs and offline behaviour needed for CLI replay commands (`scan --record`, `verify`, `replay`, `diff`). + +## Command surface (proposed) +- `stella scan --record ` → emits replay record NDJSON to stdout or `--out bundle.ndjson`. +- `stella replay --bundle ` → re-run verification offline; accepts `--policy-bundle` to pin policy version. +- `stella diff --bundle --bundle ` → compare findings/signals with deterministic ordering; output NDJSON. +- `stella verify --bundle ` → signature/hash verification of replay bundle. + +## Inputs +- Replay record schema v1 from Evidence Locker (see `docs/modules/evidence-locker/replay-payload-contract.md`). +- Policy export bundle contract (see `docs/modules/policy/design/export-console-bundle-contract.md`) for policy pinning. + +## Outputs +- Deterministic NDJSON; file names content-addressed (`sha256` of payload). +- Exit codes: 0 success, 2 validation error, 3 signature mismatch. + +## Offline/air-gap considerations +- No network fetch; all references resolve to local bundle paths. +- Trust roots loaded from CLI config or `--trust-root` file; DSSE verification optional flag `--no-verify` default false. + +## Open decisions +- Exact flag names for trust root and policy bundle; align with CLI UX guidelines. +- Where to persist cache/metadata (if any) in offline mode. + +## Handoff +Treat this as the prep artefact for PREP-CLI-REPLAY-187-002. Update once replay record schema is finalized. diff --git a/docs/modules/concelier/operations/console-lnm-consumption.md b/docs/modules/concelier/operations/console-lnm-consumption.md new file mode 100644 index 000000000..f8e4e5efb --- /dev/null +++ b/docs/modules/concelier/operations/console-lnm-consumption.md @@ -0,0 +1,57 @@ +# Console Consumption of LNM Linksets (Sprint 110) + +Goal: unblock CONCELIER-CONSOLE-23-001..003 by defining how Console reads Link-Not-Merge (LNM) data now that the schema is frozen (2025-11-17) and cache plan exists. + +## Data sources +- Primary collection: `advisory_linksets` (see `docs/modules/concelier/link-not-merge-schema.md`). +- Cache: `advisory_linkset_cache` (see `docs/modules/concelier/operations/lnm-cache-plan.md`). Console should read from cache when present; fall back to live rebuild otherwise. + +## API shape (WebService) +- Endpoint to expose via Console: `GET /v1/lnm/linksets/{advisoryId}` + - Query: `source` (required), `tenant`, optional `includeConflicts=true|false` (default true). + - Response JSON (deterministic key order): + ```json + { + "advisoryId": "GHSA-123", + "source": "ghsa", + "observations": ["", ""], + "normalized": { + "purls": ["pkg:npm/foo@1.0.0"], + "versions": ["1.0.0"], + "ranges": [], + "severities": [{"system":"cvssv3","score":7.5}] + }, + "conflicts": [ + {"field":"severities","reason":"disagreement","values":["7.5","9.8"]} + ], + "provenance": { + "observationHashes": ["..."], + "toolVersion": "lnm-1.0" + }, + "createdAt": "2025-11-20T00:00:00Z" + } + ``` +- Sorting: observations sorted by `source, advisoryId, fetchedAt` before hashing; response preserves that order. +- Caching: when cache hit, return cached document; when miss, rebuild, write cache, then return. + +## Console rendering contract +- Fields to surface: + - `normalized.purls` (list) and `normalized.versions` + - `conflicts` if non-empty: show field + reason text; keep order as returned. + - `provenance.observationHashes` as “evidence anchors” (list); do not prettify. +- Tenant header required; no cross-tenant leakage. Scope `concelier:lnm.read`. + +## Error handling +- 404 when linkset missing for advisory/source. +- 409 when tenant header missing/invalid. +- Deterministic error body: `{ "error": "", "message": "..." }`. + +## Telemetry (reuse existing meters) +- Increment `lnm.cache.hit_total` / `lnm.cache.write_total` as defined in cache plan. +- Log template on returns: `lnm_console_success tenant={Tenant} advisoryId={AdvisoryId} source={Source} cached={Cached}`. + +## Owners +- Concelier Console Guild (primary) +- Concelier Core Guild (review) + +This document is authoritative for CONCELIER-CONSOLE-23-001..003 in Sprint 110. diff --git a/docs/modules/concelier/operations/lnm-cache-plan.md b/docs/modules/concelier/operations/lnm-cache-plan.md new file mode 100644 index 000000000..77a69d3a5 --- /dev/null +++ b/docs/modules/concelier/operations/lnm-cache-plan.md @@ -0,0 +1,33 @@ +# Concelier LNM Cache Plan (Sprint 110) + +Purpose: finalize structured caching fields now that Link-Not-Merge (LNM) schema is frozen (2025-11-17) and Evidence Locker contract is published. + +## Cache payload shape +- Source: `advisory_linksets` collection (see `docs/modules/concelier/link-not-merge-schema.md`). +- Cache entry key: `{tenant}:{advisoryId}:{source}`. +- Cached fields (deterministic order): + - `observations` (ObjectId list, already sorted by source, advisoryId, fetchedAt) + - `normalized.purls`, `normalized.versions`, `normalized.ranges`, `normalized.severities` + - `conflicts` array (field, reason, values) + - `provenance.observationHashes` + - `builtByJobId`, `createdAt` +- Exclude mutable/derived fields; cache is rebuilt on ingestion replay, not updated in place. + +## Storage & eviction +- Collection: `advisory_linkset_cache` (per-tenant index `{tenantId:1, advisoryId:1, source:1}` unique). +- Eviction: TTL on `createdAt` disabled by default; cache is replaced when a newer `provenance.observationHashes` differs. +- Determinism: cache documents written via canonical JSON writer (sorted keys); timestamps UTC. + +## API exposure +- WebService: surface cached linksets via existing `/v1/lnm/linksets/{advisoryId}?source=...` (read-through); if cache miss, rebuild synchronously and store. +- Console: may rely on this cache for Advisory AI surfaces; no new payload fields required. + +## Telemetry +- Meter `StellaOps.Concelier.Linksets` (existing) to add counters: `lnm.cache.write_total{result}`, `lnm.cache.hit_total{source}`; histogram `lnm.cache.rebuild_ms` for synchronous rebuilds. (To be wired in subsequent sprint.) + +## Alignment +- Schema source of truth: `docs/modules/concelier/link-not-merge-schema.md` (frozen v1). +- Evidence Locker contract: `docs/modules/evidence-locker/attestation-contract.md` informs provenance fields; no shape changes required. + +## Ownership +- Concelier Core + WebService guilds. diff --git a/docs/modules/concelier/prep/2025-11-20-airgap-56-001-58-001-prep.md b/docs/modules/concelier/prep/2025-11-20-airgap-56-001-58-001-prep.md new file mode 100644 index 000000000..b65e45e2f --- /dev/null +++ b/docs/modules/concelier/prep/2025-11-20-airgap-56-001-58-001-prep.md @@ -0,0 +1,20 @@ +# Concelier AirGap Prep — PREP-CONCELIER-AIRGAP-56-001..58-001 + +Status: Draft (2025-11-20) +Owners: Concelier Core · AirGap Guilds +Scope: Capture mirror bundle/staleness requirements for Concelier ingestion under sealed mode. + +## Dependencies +- Mirror thin bundle milestones (bundle_id, provenance, staleness_budget) from Mirror sprint 56-001. +- AirGap controller staleness/time anchor fields. + +## Needed contract +- Ingestion must accept `bundle_id`, `provenance`, `staleness_seconds_remaining` on advisory/linkset endpoints. +- Reject non-mirror sources when sealed; surface `AIRGAP_EGRESS_BLOCKED` per Concelier AirGap response contract. + +## Open decisions +- Exact header names for bundle/staleness metadata. +- Whether to cache bundle provenance per tenant. + +## Handoff +Use as PREP artefact for 56-001..58-001 chain; update when mirror bundle schema and controller staleness fields are finalized. diff --git a/docs/modules/concelier/prep/2025-11-20-attest-73-001-prep.md b/docs/modules/concelier/prep/2025-11-20-attest-73-001-prep.md new file mode 100644 index 000000000..6731ef23b --- /dev/null +++ b/docs/modules/concelier/prep/2025-11-20-attest-73-001-prep.md @@ -0,0 +1,16 @@ +# Concelier Attestation Prep — PREP-CONCELIER-ATTEST-73-001/002 + +Status: Draft (2025-11-20) +Owners: Concelier Core · Evidence Locker Guild +Scope: Define attestation scope/signoff pending for Evidence Locker integration. + +## Needs +- Evidence Locker attestation scope and DSSE profile. +- Endpoint contract for attestation verification of Concelier exports. + +## Open decisions +- Signer identity and Rekor usage in sealed mode. +- What evidence hashes to include (bundle_id, merkle_root). + +## Handoff +Use as PREP artefact; update once EvidenceLocker publishes scope and profile. diff --git a/docs/modules/concelier/prep/2025-11-20-console-23-001-prep.md b/docs/modules/concelier/prep/2025-11-20-console-23-001-prep.md new file mode 100644 index 000000000..6c3a0e38c --- /dev/null +++ b/docs/modules/concelier/prep/2025-11-20-console-23-001-prep.md @@ -0,0 +1,16 @@ +# Concelier Console Schema Prep — PREP-CONCELIER-CONSOLE-23-001..003 + +Status: Draft (2025-11-20) +Owners: Concelier Console Guild +Scope: Provide schema samples for Console evidence bundles and identifiers. + +## Needed artefacts +- Sample schema for console evidence bundle IDs and fields (linkset refs, advisory ids, staleness metadata). +- Example payloads for CONCELIER-CONSOLE-23-001..003. + +## Open decisions +- ID format for console evidence bundle (ulid vs hash). +- Required fields for linkage to LNM outputs. + +## Handoff +Use as PREP artefact; fill once LNM schema freeze and console bundle id rules are provided. diff --git a/docs/modules/concelier/prep/2025-11-20-feeds-icscisa-kisa-prep.md b/docs/modules/concelier/prep/2025-11-20-feeds-icscisa-kisa-prep.md new file mode 100644 index 000000000..e6ecc63fa --- /dev/null +++ b/docs/modules/concelier/prep/2025-11-20-feeds-icscisa-kisa-prep.md @@ -0,0 +1,12 @@ +# Feed Remediation Prep — PREP-FEEDCONN-ICSCISA-02-012 / KISA-02-008 + +Status: Draft (2025-11-20) +Owners: Concelier Feed Owners +Scope: Capture remediation plan for problematic feeds. + +## Items to collect +- Current ingestion endpoints and auth for ICSCISA-02-012, KISA-02-008. +- Known issues and required remediation steps (validation fixes, throttling, schema tweaks). + +## Handoff +Use as PREP artefact; fill with concrete remediation steps once feed owners provide details. diff --git a/docs/modules/concelier/prep/2025-11-20-platform-events-and-lnm-21-002.md b/docs/modules/concelier/prep/2025-11-20-platform-events-and-lnm-21-002.md new file mode 100644 index 000000000..5e5d79d64 --- /dev/null +++ b/docs/modules/concelier/prep/2025-11-20-platform-events-and-lnm-21-002.md @@ -0,0 +1,42 @@ +# Concelier PREP Notes — 2025-11-20 + +Owner: Concelier Core Guild · Scheduler Guild · Data Science Guild +Scope: Provide traceable prep outputs for PREP-CONCELIER-GRAPH-21-002-PLATFORM-EVENTS-S and PREP-CONCELIER-LNM-21-002-WAITING-ON-FINALIZE so downstream tasks can proceed without blocking on missing contracts. + +## 1) `sbom.observation.updated` platform event (Graph-21-002) +- Goal: publish deterministic, facts-only observation updates for graph overlays; no derived judgments. +- Proposed envelope (draft for Scheduler/Platform Events review): + - `event_type`: `sbom.observation.updated` + - `tenant_id` (string, required) + - `advisory_ids` (array of strings; upstream IDs as-ingested) + - `observation_ids` (array of stable per-observation IDs emitted by LNM storage) + - `source` (string; advisory source slug) + - `version_range` (string; original upstream semantics) + - `occurred_at` (ISO-8601 UTC, produced by Concelier at write time; deterministic) + - `trace` (object; optional provenance pointers, DSSE envelope digest with alg/id fields) +- Delivery and wiring expectations: + - Publisher lives in `StellaOps.Concelier.Core` after linkset/observation persistence. + - Scheduler binding: NATS/Redis topic `concelier.sbom.observation.updated`; ack + idempotent replay friendly; max delivery once semantics via message ID = `:::`. + - Telemetry: counter `concelier_events_observation_updated_total{tenant,source,result}`; log template includes `tenant`, `advisory_id`, `observation_id`, `event_id`. + - Offline posture: allow emitting into local bus, enqueue to file-backed spool when offline; retry with deterministic ordering by `(tenant, observation_id)`. +- Open questions to resolve in impl task: + - Final topic naming and DSSE requirement (optional vs required per deployment). + - Whether to include component alias list in the event payload or expect consumers to join via API. + +## 2) LNM fixtures + precedence markers (LNM-21-002) +- Goal: unblock correlation pipelines and downstream linkset tasks by defining required fixture shape and precedence rules. +- Fixture requirements (additive to frozen LNM v1 schema): + - Provide at least three sources with conflicting severity/CVSS to exercise conflict markers. + - Include overlapping version ranges to validate precedence tie-breakers. + - Each fixture must include `provenance` (source, fetch_time, collector) and `confidence` hints. +- Precedence rule proposal for review: + 1. Prefer explicit source ranking table (to be agreed) over recency. + 2. If ranking ties, prefer narrower version ranges, then higher confidence, then stable lexical order of `(source, advisory_id)`. + 3. Never collapse conflicting fields; emit `conflicts[]` entries with reason codes `severity-disagree`, `cvss-disagree`, `reference-disagree`. +- Delivery path for fixtures once agreed: `src/Concelier/seed-data/lnm/v1/fixtures/*.json` with deterministic ordering; wire into `StellaOps.Concelier.Core.Tests` harness. +- Next actions captured for implementation task: + - Confirm ranking table and conflict reason code list with Cartographer/Data Science. + - Drop initial fixtures into the above path and reference them from the implementation tasks’ tests. + +## Handoff +- This document is the published prep artefact requested by PREP-CONCELIER-GRAPH-21-002-PLATFORM-EVENTS-S and PREP-CONCELIER-LNM-21-002-WAITING-ON-FINALIZE. Downstream tasks should cite this file until the final schemas/fixtures are merged. diff --git a/docs/modules/concelier/prep/2025-11-20-web-airgap-57-001-prep.md b/docs/modules/concelier/prep/2025-11-20-web-airgap-57-001-prep.md new file mode 100644 index 000000000..b8b76bcac --- /dev/null +++ b/docs/modules/concelier/prep/2025-11-20-web-airgap-57-001-prep.md @@ -0,0 +1,44 @@ +# Concelier Web AirGap Prep — PREP-CONCELIER-WEB-AIRGAP-57-001 + +Status: Draft (2025-11-20) +Owners: Concelier WebService Guild · AirGap Policy Guild +Scope: Define remediation payloads and staleness plumbing for sealed-mode violations, dependent on WEB-AIRGAP-56-002. + +## Dependencies +- WEB-AIRGAP-56-001: mirror bundle registration + sealed-mode enforcement. +- WEB-AIRGAP-56-002: staleness + bundle provenance metadata surfaces. +- AirGap controller scopes (seal/unseal) and time anchor semantics from AirGap Controller/Time guilds. + +## Proposed payload mapping (EGRESS blocked) +- Error code: `AIRGAP_EGRESS_BLOCKED`. +- Shape: +```json +{ + "error": "AIRGAP_EGRESS_BLOCKED", + "message": "Direct internet fetches disabled in sealed mode; use mirror bundle sources only.", + "bundle_required": true, + "staleness_seconds": 0, + "remediation": [ + "Import mirror bundle via /airgap/import or offline kit", + "Ensure sealed mode is set with valid time anchor", + "Retry with cached/mirrored sources enabled" + ] +} +``` +- Determinism: fixed ordering of fields, remediation list sorted. + +## Staleness surfacing +- Staleness derived from bundle metadata supplied by 56-002 (`bundle_id`, `provenance`, `staleness_budget_seconds`). +- Responses include `staleness_seconds_remaining` and `bundle_id` when available. + +## Observability +- Emit timeline event `concelier.airgap.egress_blocked` with `{tenant_id, bundle_id?, endpoint, request_id}`. +- Metric: `concelier_airgap_egress_blocked_total` (counter) tagged by endpoint. + +## Open decisions +- Final error envelope format (depends on WEB-OAS-61-002 standard envelope). +- Exact header name for staleness metadata (suggest `x-concelier-bundle-staleness`). +- Whether to include advisory key/linkset ids in the blocked response. + +## Handoff +Use this as the PREP artefact for WEB-AIRGAP-57-001. Update once 56-002 and error envelope standard are finalized. diff --git a/docs/modules/evidence-locker/attestation-contract.md b/docs/modules/evidence-locker/attestation-contract.md new file mode 100644 index 000000000..2936c252f --- /dev/null +++ b/docs/modules/evidence-locker/attestation-contract.md @@ -0,0 +1,43 @@ +# Evidence Locker Attestation Contract (v1 · frozen 2025-11-20) + +Scope: Evidence Bundle v1 produced by Evidence Locker and consumed by Concelier, Excititor, Export Center, CLI, and Policy Engine. + +## Predicates & subjects +- **Subject**: OCI manifest digest (`sha256:`) of the bundle, plus optional replay pack digest. +- **Predicates (DSSE/In-Toto)** + - `stellaops.evidence.bundle.v1`: declares bundle layout (manifests, CAS paths, replay log offsets). + - `stellaops.evidence.transparency.v1`: optional Rekor log inclusion proof (UUID, log index, root hash at inclusion). + - `stellaops.evidence.integrity.v1`: hashes for each payload (SBOMs, VEX, policy packs, telemetry snapshots), keyed by logical path. + +## Required claim set +- `bundle_id` (UUID v4) +- `produced_at` (UTC ISO-8601) +- `producer` (`evidence-locker:`) +- `subject_digest` (OCI digest string) +- `hashes` (map: logical path → sha256) +- `sbom` (array of SPDX/CycloneDX digests and mediaTypes) +- `vex` (array of VEX doc digests and schema versions) +- `replay_manifest` (optional; digest + sequence number) +- `transparency` (optional; Rekor UUID, logIndex, rootHash) +- `signing_profile` (`sovereign-default` | `fips` | `gost` | `pq-experimental`) + +## Bundling & signing rules +- DSSE envelope using the module’s configured crypto provider; keys must be short-lived (<24h) and recorded in provider registry. +- Hash list and subject digest MUST match the on-disk CAS objects; deterministic sort by logical path. +- Rekor entry is optional; when absent, set `transparency=null` and add `transparency_reason="offline"` to provenance note. + +## Verification plan +- Verify DSSE signature against provider registry (per profile) and check key expiry. +- Recompute sha256 for every CAS object; fail if any mismatch. +- If `transparency` present, verify inclusion proof against bundled Rekor root; fail closed on mismatch. +- Emit verification report JSON and store beside bundle as `verify.json` (deterministic key order). + +## Fixtures +- Sample bundle + report: `docs/samples/evidence-locker/bundle-v1-sample.tar.gz` (sha256 TBD at publish time). +- Sample attestation envelope: `docs/samples/evidence-locker/attestation-v1-sample.json`. + +## Ownership +- Primary: Evidence Locker Guild. +- Reviewers: Concelier Core Guild, Excititor Guild, Export Center Guild, Policy Guild. + +This contract is authoritative for Sprint 110 and blocks CONCELIER-ATTEST-73-001/002 and EXCITITOR-ATTEST-01-003/73-001/73-002. diff --git a/docs/modules/evidence-locker/crypto-provider-registry-prep.md b/docs/modules/evidence-locker/crypto-provider-registry-prep.md new file mode 100644 index 000000000..e07dd32b6 --- /dev/null +++ b/docs/modules/evidence-locker/crypto-provider-registry-prep.md @@ -0,0 +1,21 @@ +# ICryptoProviderRegistry Prep — PREP-EVID-CRYPTO-90-001 (Draft) + +Status: Draft (2025-11-20) +Owners: Evidence Locker Guild · Security Guild +Scope: Capture requirements for crypto provider registry readiness to support sovereign/region-specific profiles. + +## Required capabilities +- Registry interface to resolve crypto providers by profile ID (e.g., `default`, `ru-offline`, `fips140`, `eidass`). +- Provider metadata: `{algorithms[], key_formats[], offline_supported, hsm_supported, oq_ready}`. +- Deterministic selection rules: prefer tenant-scoped overrides, fall back to platform defaults; no network fetch. + +## Integration points +- Evidence Locker signing pipeline to request provider by profile when sealing bundles. +- Replay validation to know which algorithms/hashes are acceptable for DSSE verification. + +## Dependencies +- Final list of sovereign profiles from Security Guild. +- Key storage/backing (KMS/HSM) availability per profile. + +## Handoff +Use this as the prep artefact for PREP-EVID-CRYPTO-90-001; update once profile list and key storage rules are confirmed. diff --git a/docs/modules/evidence-locker/replay-payload-contract.md b/docs/modules/evidence-locker/replay-payload-contract.md new file mode 100644 index 000000000..ba0acc400 --- /dev/null +++ b/docs/modules/evidence-locker/replay-payload-contract.md @@ -0,0 +1,42 @@ +# Replay Payload Contract (Draft) — PREP-EVID-REPLAY-187-001 + +Status: Draft (2025-11-20) +Owners: Evidence Locker Guild · Scanner Guild · CLI Guild +Scope: Capture expected scanner record payloads needed by Evidence Locker replay APIs. + +## 1) Payload envelope +- Content type: `application/vnd.stella.replay.record+json;version=1`. +- Fields: + - `record_id` (ULID, assigned by Scanner). + - `tenant_id` (string). + - `source` (enum): `scanner`, `attestor`, `cli`. + - `digest` (hex): SHA-256 of canonical payload bytes. + - `created_at` (RFC3339 UTC). + - `schema_version`: `replay.record.v1`. + +## 2) Scanner record body (expected from Sprint 0186) +- `image_digest` (string, required). +- `sbom_digest` (string, optional) with SBOM pointer. +- `observations` (array) of `{type, component_purl?, location, evidence, confidence}`. +- `signals` (array) for runtime/static signals with `{name, value, units?, confidence}`. +- `attestations` (array) of DSSE statement references `{type, uri, sha256}`. +- `provenance` (object): `{scanner_version, policy_profile, worker_id}`. + +## 3) Evidence Locker ingestion contract +- API: `POST /replay/records` (internal) accepting NDJSON stream (`record_envelope + body`). +- Validation: + - hash must match `digest` supplied; timestamps UTC. + - tenant_id must match auth principal or delegated token. + - schema_version must equal `replay.record.v1` until upgraded. +- Storage layout proposal: bucket prefix `replay/records/{tenant_id}/{record_id}.ndjson`, immutable; metadata indexed in Mongo with `{record_id, image_digest, created_at}`. + +## 4) Open dependencies +- Scanner team to freeze exact `observations` and `signals` schema in Sprint 0186. +- Need DSSE profile for `attestations` (Authority/Attestor alignment). +- CLI replay commands depend on finalized pointer format to retrieve records. + +## 5) Next actions +- Once Sprint 0186 publishes sample payloads, update this doc with enumerated observation/signal types and add JSON schema file under `docs/modules/evidence-locker/schemas/replay-record-v1.json`. + +## 6) Handoff +Reference this document from sprint trackers for PREP-EVID-REPLAY-187-001 and related CLI/Attestor PREP tasks. Update when upstream payloads are available. diff --git a/docs/modules/evidence-locker/validate-bundle-prep.md b/docs/modules/evidence-locker/validate-bundle-prep.md new file mode 100644 index 000000000..d4fbeed3b --- /dev/null +++ b/docs/modules/evidence-locker/validate-bundle-prep.md @@ -0,0 +1,23 @@ +# Validate Bundle Prep — PREP-VALIDATE-BUNDLE-187-005 (Draft) + +Status: Draft (2025-11-20) +Owners: QA Guild · CLI Guild · Docs Guild +Scope: Define validation steps for replay bundles once schemas freeze. + +## Validation checklist (proposed) +- Verify archive hash vs manifest `bundle.manifest.json` (`sha256`). +- Verify DSSE signature (if present) against trusted keys. +- Recompute Merkle root of bundle file tree; compare to manifest. +- Schema validation: replay records conform to `replay.record.v1`; policy export bundle conforms to `policy.export.console.v1` when included. +- Determinism: run `stella replay` twice on same bundle and assert identical outputs (hash comparison). + +## Fixtures/tests +- Place golden bundles under `tests/EvidenceLocker/Fixtures/replay/` with expected hashes and DSSE signatures. +- CLI validation test: `stella verify --bundle ` returns exit code 0 and prints `verified: true`. + +## Open dependencies +- Final schemas from Evidence Locker and Policy export contracts. +- Trust root list for DSSE verification (Authority decision). + +## Handoff +Use this prep doc for PREP-VALIDATE-BUNDLE-187-005; expand with concrete fixtures once schemas are frozen. diff --git a/docs/modules/excititor/architecture.md b/docs/modules/excititor/architecture.md index c20dd3c51..934956aa0 100644 --- a/docs/modules/excititor/architecture.md +++ b/docs/modules/excititor/architecture.md @@ -608,12 +608,14 @@ excititor: ### 9.1 WebService endpoints -With storage configured, the WebService exposes the following ingress and diagnostic APIs: +With storage configured, the WebService exposes the following ingress and diagnostic APIs (deterministic ordering, offline-friendly): * `GET /excititor/status` – returns the active storage configuration and registered artifact stores. * `GET /excititor/health` – simple liveness probe. * `POST /excititor/statements` – accepts normalized VEX statements and persists them via `IVexClaimStore`; use this for migrations/backfills. * `GET /excititor/statements/{vulnId}/{productKey}?since=` – returns the immutable statement log for a vulnerability/product pair. +* `POST /vex/evidence/chunks` – submits aggregation-only chunks (OpenAPI: `schemas/vex-chunk-api.yaml`); responds with deterministic `chunk_digest` and queue id. Telemetry published under meter `StellaOps.Excititor.Chunks` (see Operations). +* `POST /v1/attestations/verify` – verifies Evidence Locker attestations for exports/chunks using `IVexAttestationVerifier`; returns `{ valid, diagnostics }` (deterministic key order). Aligns with Evidence Locker contract v1. * `POST /excititor/resolve` – requires `vex.read` scope; accepts up to 256 `(vulnId, productKey)` pairs via `productKeys` or `purls` and returns deterministic consensus results, decision telemetry, and a signed envelope (`artifact` digest, optional signer signature, optional attestation metadata + DSSE envelope). Returns **409 Conflict** when the requested `policyRevisionId` mismatches the active snapshot. Run the ingestion endpoint once after applying migration `20251019-consensus-signals-statements` to repopulate historical statements with the new severity/KEV/EPSS signal fields. diff --git a/docs/modules/excititor/attestation-plan.md b/docs/modules/excititor/attestation-plan.md new file mode 100644 index 000000000..35e61fe05 --- /dev/null +++ b/docs/modules/excititor/attestation-plan.md @@ -0,0 +1,43 @@ +# Excititor Attestation Plan (Sprint 110) + +## Goals +- Align Excititor chunk API and attestation envelopes with Evidence Locker contract. +- Provide offline-ready chunk submission/attestation flow for VEX evidence. + +## Chunk API shape (`/vex/evidence/chunks`) +- POST body (NDJSON, deterministic order by `chunk_id`): + ```json + { + "chunk_id": "uuid", + "tenant": "acme", + "source": "ghsa", + "schema": "stellaops.vex.chunk.v1", + "items": [ {"advisory_id":"GHSA-123","status":"affected","purl":"pkg:npm/foo@1.0.0"} ], + "provenance": {"fetched_at":"2025-11-20T00:00:00Z","artifact_sha":"abc"} + } + ``` +- At submission, Excititor returns `chunk_digest` (sha256 of canonical JSON) and queue id. + +## Attestation envelope +- Subject: `chunk_digest` from above. +- Predicates attached: + - `stellaops.vex.chunk.meta.v1` (tenant, source, schema version, item count). + - `stellaops.vex.chunk.integrity.v1` (sha256 per item block, canonical order). + - Optional `stellaops.transparency.v1` (Rekor UUID/logIndex) when online. +- Envelope format: DSSE using Evidence Locker provider registry; signing profile mirrors Evidence Locker bundle profile for tenant. + +## DSSE bundling rules +- Deterministic JSON (sorted keys) before hashing. +- Canonical NDJSON for chunk payload; no gzip inside envelope. +- Attach verification report alongside attestation as `chunk-verify.json` (hashes + signature check results). + +## Sample payloads +- `docs/samples/excititor/chunk-sample.ndjson` +- `docs/samples/excititor/chunk-attestation-sample.json` + +## Integration points +- Evidence Locker contract v1 (see `docs/modules/evidence-locker/attestation-contract.md`). +- Concelier LNM schemas (observations remain aggregation-only; attestation is evidence, not merge). + +## Ownership +- Excititor Guild (primary); Evidence Locker Guild reviewer. diff --git a/docs/modules/excititor/connectors/connector-signer-metadata.md b/docs/modules/excititor/connectors/connector-signer-metadata.md new file mode 100644 index 000000000..4e0f2744f --- /dev/null +++ b/docs/modules/excititor/connectors/connector-signer-metadata.md @@ -0,0 +1,36 @@ +# Connector signer metadata (v1.0.0) + +**Scope.** Defines the canonical, offline-friendly metadata for Excititor connectors that validate signed feeds (MSRC CSAF, Oracle OVAL, Ubuntu OVAL, StellaOps mirror OpenVEX). The file is consumed by WebService/Worker composition roots and by Offline Kits to pin trust material deterministically. + +**Location & format.** +- Schema: `docs/modules/excititor/schemas/connector-signer-metadata.schema.json` (JSON Schema 2020‑12). +- Sample: `docs/samples/excititor/connector-signer-metadata-sample.json` (aligns with schema). +- Expected production artifact: NDJSON or JSON stamped per release; store in offline kits alongside connector bundles. + +## Required fields (summary) +- `schemaVersion` — must be `1.0.0`. +- `generatedAt` — ISO-8601 UTC timestamp for the metadata file. +- `connectors[]` — one entry per connector: + - `connectorId` — stable slug, e.g., `excititor-msrc-csaf`. + - `provider { name, slug }` — human label and slug. + - `issuerTier` — `tier-0`, `tier-1`, `tier-2`, or `untrusted` (aligns with trust weighting). + - `signers[]` — one per signing path; each has `usage` (`csaf|oval|openvex|bulk-meta|attestation`) and `fingerprints[]` (algorithm + format + value). Optional `keyLocator` and `certificateChain` for offline key retrieval. + - `bundle` — reference to the sealed bundle containing the feed/signing material (`kind`: `oci-referrer|oci-tag|file|tuf`, plus `uri`, optional `digest`, `publishedAt`). + - Optional `validFrom`, `validTo`, `revoked`, `notes` for rollover and incident handling. + +## Rollover / migration guidance +1) **Author the metadata** using the schema and place the JSON next to connector bundles in the offline kit (`out/connectors//signer-metadata.json`). +2) **Validate** with `dotnet tool run njsonschema validate connector-signer-metadata.schema.json connector-signer-metadata.json` (or `ajv validate`). +3) **Wire connector code** to load the file on startup (Worker + WebService) and pin signers per `connectorId`; reject feeds whose fingerprints are absent or marked `revoked=true` or out of `validFrom/To` range. + - Connectors look for `STELLAOPS_CONNECTOR_SIGNER_METADATA_PATH` (absolute/relative) and enrich provenance metadata automatically when present. +4) **Rollover keys** by appending a new `signers` entry and setting a future `validFrom`; keep the previous signer until all mirrors have caught up. Use `issuerTier` downgrades to quarantine while keeping history. +5) **Mirror references**: store the referenced bundles/keys under OCI tags or TUF targets already shipped in the offline kit so no live network is required. +6) **Record decisions** in sprint Decisions & Risks when changing trust tiers or fingerpints; update this doc if formats change. + +## Sample entries (non-production) +See `docs/samples/excititor/connector-signer-metadata-sample.json` for MSRC, Oracle, Ubuntu, and StellaOps example entries. These fingerprints are illustrative only; replace with real values before shipping. + +## Consumer expectations +- Deterministic: sort connectors alphabetically before persistence; avoid clock-based defaults. +- Offline-first: all `keyLocator`/`bundle.uri` values must resolve inside the air-gap kit (OCI/TUF/file). +- Observability: emit a structured warning when metadata is missing or stale (>7 days) and fail closed for missing signers. diff --git a/docs/modules/excititor/operations/chunk-api-user-guide.md b/docs/modules/excititor/operations/chunk-api-user-guide.md new file mode 100644 index 000000000..07f32e97f --- /dev/null +++ b/docs/modules/excititor/operations/chunk-api-user-guide.md @@ -0,0 +1,24 @@ +# Using the Chunk API + +Endpoint: `POST /vex/evidence/chunks` +- Content-Type: `application/x-ndjson` +- See schema: `docs/modules/excititor/schemas/vex-chunk-api.yaml` + +Response: `202 Accepted` +```json +{ "chunk_digest": "sha256:…", "queue_id": "uuid" } +``` + +Operational notes +- Deterministic hashing: server recomputes `chunk_digest` from canonical JSON; mismatches return 400. +- Limits: default 500 items, max 2000 (aligned with Program.cs guard). +- Telemetry: metrics under `StellaOps.Excititor.Chunks` (see chunk-telemetry.md). +- Headers: correlation/trace headers echoed (`X-Stella-TraceId`, `X-Stella-CorrelationId`). + +Example curl +```bash +curl -X POST https://excitor.local/vex/evidence/chunks \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/x-ndjson" \ + --data-binary @docs/samples/excititor/chunk-sample.ndjson +``` diff --git a/docs/modules/excititor/operations/chunk-telemetry.md b/docs/modules/excititor/operations/chunk-telemetry.md new file mode 100644 index 000000000..aa5d8c1bc --- /dev/null +++ b/docs/modules/excititor/operations/chunk-telemetry.md @@ -0,0 +1,26 @@ +# Excititor Chunk Telemetry (Sprint 110) + +## Metrics (Meter: `StellaOps.Excititor.Chunks`) +- `vex_chunks_ingested_total` (counter) — tags: `tenant`, `source`, `status` (`accepted|rejected`), `reason` (nullable for accepted). Increments per chunk submitted. +- `vex_chunks_item_count` (histogram, unit=items) — records item count per chunk. +- `vex_chunks_payload_bytes` (histogram, unit=bytes) — measured from NDJSON payload length. +- `vex_chunks_latency_ms` (histogram) — end-to-end ingestion latency per request. + +## Logs +- `vex.chunk.ingest.accepted` — includes `chunk_id`, `tenant`, `source`, `item_count`, `chunk_digest`. +- `vex.chunk.ingest.rejected` — includes `chunk_id`, `tenant`, `source`, `reason`, validation errors (summarized). + +## Wiring steps +1. Register `ChunkTelemetry` as singleton with shared `Meter` instance. +2. In `/vex/evidence/chunks` handler, compute `chunk_digest` deterministically from canonical JSON and emit counters/histograms via `ChunkTelemetry`. +3. Log using structured templates above; avoid request bodies in logs. +4. Expose metrics via default ASP.NET metrics export (Prometheus/OpenTelemetry) already configured in WebService. + +## Determinism & offline posture +- Do not include host-specific paths or timestamps in metric dimensions. +- Histogram buckets: use standard OTEL defaults; no runtime-generated buckets. +- Keep meter name stable; adding new instruments requires version note in sprint Decisions & Risks. + +## Ownership +- Implementer: Excititor Observability Guild +- Reviewers: Evidence Locker Guild (for parity with attestation metrics) diff --git a/docs/modules/excititor/schemas/connector-signer-metadata.schema.json b/docs/modules/excititor/schemas/connector-signer-metadata.schema.json new file mode 100644 index 000000000..ae03a6b6d --- /dev/null +++ b/docs/modules/excititor/schemas/connector-signer-metadata.schema.json @@ -0,0 +1,125 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://stellaops.dev/schemas/excititor/connector-signer-metadata.schema.json", + "title": "Excititor Connector Signer Metadata", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "generatedAt", "connectors"], + "properties": { + "schemaVersion": { + "type": "string", + "pattern": "^1\\.0\\.0$" + }, + "generatedAt": { + "type": "string", + "format": "date-time" + }, + "connectors": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/connector" + } + } + }, + "$defs": { + "connector": { + "type": "object", + "additionalProperties": false, + "required": [ + "connectorId", + "provider", + "issuerTier", + "signers" + ], + "properties": { + "connectorId": { + "type": "string", + "pattern": "^[a-z0-9:-\\.]+$" + }, + "provider": { + "type": "object", + "additionalProperties": false, + "required": ["name", "slug"], + "properties": { + "name": { "type": "string", "minLength": 3 }, + "slug": { "type": "string", "pattern": "^[a-z0-9-]+$" } + } + }, + "issuerTier": { + "type": "string", + "enum": ["tier-0", "tier-1", "tier-2", "untrusted"] + }, + "signers": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/signer" } + }, + "bundle": { "$ref": "#/$defs/bundleRef" }, + "validFrom": { "type": "string", "format": "date" }, + "validTo": { "type": "string", "format": "date" }, + "revoked": { "type": "boolean", "default": false }, + "notes": { "type": "string", "maxLength": 2000 } + } + }, + "signer": { + "type": "object", + "additionalProperties": false, + "required": ["usage", "fingerprints"], + "properties": { + "usage": { + "type": "string", + "enum": ["csaf", "oval", "openvex", "bulk-meta", "attestation"] + }, + "fingerprints": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/fingerprint" } + }, + "keyLocator": { + "type": "string", + "description": "Path or URL (mirror/OCI/TUF) where the signing key or certificate chain can be retrieved in offline kits." + }, + "certificateChain": { + "type": "array", + "items": { "type": "string" }, + "description": "Optional PEM-encoded certificates for x509/cosign keys." + } + } + }, + "fingerprint": { + "type": "object", + "additionalProperties": false, + "required": ["alg", "value"], + "properties": { + "alg": { + "type": "string", + "enum": ["sha256", "sha512", "sha1"] + }, + "format": { + "type": "string", + "enum": ["pgp", "x509-spki", "x509-ski", "cosign", "pem"] + }, + "value": { + "type": "string", + "minLength": 16, + "maxLength": 128 + } + } + }, + "bundleRef": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "uri"], + "properties": { + "kind": { + "type": "string", + "enum": ["oci-referrer", "oci-tag", "file", "tuf"] + }, + "uri": { "type": "string", "minLength": 8 }, + "digest": { "type": "string", "minLength": 32 }, + "publishedAt": { "type": "string", "format": "date-time" } + } + } + } +} diff --git a/docs/modules/excititor/schemas/vex-chunk-api.yaml b/docs/modules/excititor/schemas/vex-chunk-api.yaml new file mode 100644 index 000000000..35b52b194 --- /dev/null +++ b/docs/modules/excititor/schemas/vex-chunk-api.yaml @@ -0,0 +1,82 @@ +openapi: 3.1.0 +info: + title: StellaOps Excititor Chunk API + version: "0.1.0" + description: | + Frozen for Sprint 110 (EXCITITOR-AIAI-31-002). Aligns with Evidence Locker attestation contract v1. +servers: + - url: https://excitor.local +paths: + /vex/evidence/chunks: + post: + summary: Submit VEX evidence chunk (aggregation-only) + requestBody: + required: true + content: + application/x-ndjson: + schema: + $ref: '#/components/schemas/VexChunk' + responses: + '202': + description: Accepted for processing + content: + application/json: + schema: + type: object + required: [chunk_digest, queue_id] + properties: + chunk_digest: + type: string + description: sha256 of canonical chunk JSON + queue_id: + type: string + description: Background job identifier + '400': + description: Validation error +components: + schemas: + VexChunk: + type: object + required: [chunk_id, tenant, source, schema, items, provenance] + properties: + chunk_id: + type: string + format: uuid + tenant: + type: string + source: + type: string + description: feed id (e.g., ghsa, nvd) + schema: + type: string + enum: [stellaops.vex.chunk.v1] + items: + type: array + items: + type: object + required: [advisory_id, status, purl] + properties: + advisory_id: + type: string + status: + type: string + enum: [affected, unaffected, under_investigation, fixed, unknown] + purl: + type: string + justification: + type: string + last_observed: + type: string + format: date-time + provenance: + type: object + required: [fetched_at, artifact_sha] + properties: + fetched_at: + type: string + format: date-time + artifact_sha: + type: string + signature: + type: object + nullable: true diff --git a/docs/modules/export-center/prep/2025-11-20-crypto-90-001-prep.md b/docs/modules/export-center/prep/2025-11-20-crypto-90-001-prep.md new file mode 100644 index 000000000..c60e081c7 --- /dev/null +++ b/docs/modules/export-center/prep/2025-11-20-crypto-90-001-prep.md @@ -0,0 +1,17 @@ +# Export Crypto Prep — PREP-EXPORT-CRYPTO-90-001 + +Status: Draft (2025-11-20) +Owners: Exporter Service · Security Guild +Scope: Capture crypto requirements pending Nov-18 review and reference implementation. + +## Needs +- Mapping of signing/encryption algorithms per export profile. +- Integration with `ICryptoProviderRegistry` (same as Evidence Locker) for provider selection. +- Hashing defaults (sha256) and optional sha512/sha3 for high-assurance paths. + +## Open decisions +- Final provider list and key storage (KMS/HSM) per profile. +- Whether to sign both manifest and per-artifact hashes. + +## Handoff +Use as prep artefact for EXPORT-CRYPTO-90-001; fill once Security delivers profile list and reference implementation. diff --git a/docs/modules/export-center/prep/2025-11-20-exporter-evid-blocker.md b/docs/modules/export-center/prep/2025-11-20-exporter-evid-blocker.md new file mode 100644 index 000000000..8b78f5598 --- /dev/null +++ b/docs/modules/export-center/prep/2025-11-20-exporter-evid-blocker.md @@ -0,0 +1,15 @@ +# Exporter Service Blocker — PREP-EXPORTER-SERVICE-BLOCKED-WAITING-ON-EVID + +Status: Draft (2025-11-20) +Owners: Planning +Scope: Document EvidenceLocker dependency blocking exporter service. + +## Blocker +- EvidenceLocker spec not published; need replay/export bundle schemas and ICryptoProviderRegistry availability. + +## What we need +- Bundle schema pointers (from EvidenceLocker) and retention rules. +- Sample payloads to mirror into exporter tests. + +## Handoff +Use this note to track unblock; update when EvidenceLocker spec is available. diff --git a/docs/modules/export-center/prep/2025-11-20-notify-obs-52-prep.md b/docs/modules/export-center/prep/2025-11-20-notify-obs-52-prep.md new file mode 100644 index 000000000..294d695f4 --- /dev/null +++ b/docs/modules/export-center/prep/2025-11-20-notify-obs-52-prep.md @@ -0,0 +1,29 @@ +# Export Notifications Schema Prep — PREP-EXPORT-NOTIFY-SCHEMA-OBS-52 + +Status: Draft (2025-11-20) +Owners: Notifications Guild · Exporter Service +Scope: Define notification envelope/payloads for export lifecycle events. + +## Event types +- `export.started`, `export.completed`, `export.failed`. + +## Envelope (proposed) +```json +{ + "type": "export.completed", + "export_id": "...", + "profile_id": "...", + "tenant_id": "...", + "artifact_counts": {"json": 2, "mirror": 1}, + "sha256": "...", + "created_at": "2025-11-20T00:00:00Z" +} +``` +- Deterministic key ordering; timestamps UTC. + +## Open decisions +- Channel/transport (NATS vs Redis streams vs webhooks). +- Required retry/backoff policy and DLQ routing. + +## Handoff +Use this prep doc for PREP-EXPORT-NOTIFY-SCHEMA-OBS-52; update once transport + DLQ policy are chosen. diff --git a/docs/modules/export-center/prep/2025-11-20-obs-50-001-prep.md b/docs/modules/export-center/prep/2025-11-20-obs-50-001-prep.md new file mode 100644 index 000000000..b631d6281 --- /dev/null +++ b/docs/modules/export-center/prep/2025-11-20-obs-50-001-prep.md @@ -0,0 +1,17 @@ +# Export Telemetry Prep — PREP-EXPORT-OBS-50-001 + +Status: Draft (2025-11-20) +Owners: Exporter Service · Observability Guild +Scope: Define telemetry schema for exporter service bootstrap. + +## Proposed metrics/logs +- Metrics (Prometheus/Otel): `export_runs_total{profile, tenant}`, `export_run_duration_seconds`, `export_artifacts_total{type}`, `export_failures_total`, `export_bytes_total`. +- Logs: structured with fields `{export_id, profile, tenant, artifact_type, status, duration_ms}`. +- Traces: span names `export.run`, `export.plan`, `export.write`; tags include `profile`, `tenant`, `artifact_count`. + +## Open decisions +- Histogram buckets for duration/bytes. +- Required correlation IDs for downstream Console ingestion. + +## Handoff +Use this as PREP artefact for EXPORT-OBS-50-001; update buckets and trace tags once Observability finalizes naming. diff --git a/docs/modules/export-center/prep/2025-11-20-risk-69-001-prep.md b/docs/modules/export-center/prep/2025-11-20-risk-69-001-prep.md new file mode 100644 index 000000000..61cd5128f --- /dev/null +++ b/docs/modules/export-center/prep/2025-11-20-risk-69-001-prep.md @@ -0,0 +1,21 @@ +# Export Risk Bundle Prep — PREP-EXPORT-RISK-69-001 + +Status: Draft (2025-11-20) +Owners: Exporter Service · Risk Bundle Export Guild +Scope: Capture provider selection rules and schema needs for risk bundle job handler. + +## Provider selection (proposed) +- Inputs: `risk_profile_id`, `tenant_id`, `preferred_provider`, `fallback_provider`. +- Selection order: tenant override → profile default → system default. +- Providers must advertise capabilities `{formats[], signing_profiles[]}`. + +## Manifest expectations +- Fields: `bundle_id`, `profile_id`, `provider_id`, `inputs_hash`, `created_at`, `artifacts[] {path, sha256, media_type}`. +- Deterministic ordering and sha256 for all artifacts. + +## Open decisions +- Final list of providers and signing profiles. +- Whether to embed policy/export bundle pointers. + +## Handoff +Use this as PREP artefact for EXPORT-RISK-69-001; update provider list and manifest once phase I artifacts land. diff --git a/docs/modules/export-center/prep/2025-11-20-svc-35-001-prep.md b/docs/modules/export-center/prep/2025-11-20-svc-35-001-prep.md new file mode 100644 index 000000000..74bc56d73 --- /dev/null +++ b/docs/modules/export-center/prep/2025-11-20-svc-35-001-prep.md @@ -0,0 +1,21 @@ +# Export Service Bootstrap Prep — PREP-EXPORT-SVC-35-001 + +Status: Draft (2025-11-20) +Owners: Exporter Service +Scope: Capture phase I readiness for exporter service project/migrations. + +## Project baseline +- Service: minimal API (net10.0), Postgres storage for `export_profiles`, `export_runs`, `export_inputs`, `export_distributions`. +- Tests: xUnit + integration harness with deterministic timestamps. + +## Schema notes +- `export_profiles`: `{id, name, tenant_id?, config_json}`. +- `export_runs`: `{id, profile_id, tenant_id, status, started_at, completed_at, artifact_counts JSONB}`. +- Deterministic defaults: UTC timestamps; snake_case columns. + +## Open decisions +- Final Postgres schema (indices, enums for status). +- Whether to store metrics snapshots inline or via observability pipeline. + +## Handoff +Use this as PREP artefact for EXPORT-SVC-35-001; update once phase I readiness and synthetic telemetry feeds are defined. diff --git a/docs/modules/export-center/prep/2025-11-20-svc-35-002-prep.md b/docs/modules/export-center/prep/2025-11-20-svc-35-002-prep.md new file mode 100644 index 000000000..7768c36c0 --- /dev/null +++ b/docs/modules/export-center/prep/2025-11-20-svc-35-002-prep.md @@ -0,0 +1,20 @@ +# Export Service Planner Prep — PREP-EXPORT-SVC-35-002 + +Status: Draft (2025-11-20) +Owners: Exporter Service +Scope: Planner + scope resolver for exports, depends on 35-001 bootstrap. + +## Planner inputs +- `profile_id`, `tenant_id`, `inputs` (bundle pointers), `priority`. +- `limits`: max artifacts, max runtime. + +## Outputs +- Plan document `{plan_id, profile_id, tenant_id, steps[], estimated_bytes, estimated_duration_ms}`. +- Steps sorted; deterministic hashing of plan. + +## Open decisions +- Step types allowed (json adapters, mirror, manifest signing). +- How to surface rejection reasons to upstream services. + +## Handoff +Use as prep artefact; update once 35-001 schema is fixed. diff --git a/docs/modules/export-center/prep/2025-11-20-svc-35-003-prep.md b/docs/modules/export-center/prep/2025-11-20-svc-35-003-prep.md new file mode 100644 index 000000000..98b752525 --- /dev/null +++ b/docs/modules/export-center/prep/2025-11-20-svc-35-003-prep.md @@ -0,0 +1,16 @@ +# Export JSON Adapter Prep — PREP-EXPORT-SVC-35-003 + +Status: Draft (2025-11-20) +Owners: Exporter Service +Scope: JSON adapters (`json:raw`, `json:policy`) normalization/redaction/compression. + +## Deliverable shape +- Adapter config: `{type, redactions[], compress: bool, normalize_paths: bool}`. +- Output manifest entry: `{path, sha256, media_type, original_size, compressed_size?}`. + +## Open decisions +- Redaction rules list and ordering. +- Compression algorithm (gzip vs zstd) and level defaults. + +## Handoff +Prep artefact for 35-003; align with plan model once 35-002 is fixed. diff --git a/docs/modules/export-center/prep/2025-11-20-svc-35-004-prep.md b/docs/modules/export-center/prep/2025-11-20-svc-35-004-prep.md new file mode 100644 index 000000000..a25db4ad7 --- /dev/null +++ b/docs/modules/export-center/prep/2025-11-20-svc-35-004-prep.md @@ -0,0 +1,17 @@ +# Export Mirror Adapter Prep — PREP-EXPORT-SVC-35-004 + +Status: Draft (2025-11-20) +Owners: Exporter Service +Scope: Mirror (full) adapter producing filesystem layout, indexes, manifests, README. + +## Layout proposal +- root manifest `mirror.manifest.json` with Merkle root. +- Directories: `artifacts/`, `indexes/`, `docs/README.md`. +- Deterministic ordering, UTC timestamps. + +## Open decisions +- Which indexes required (by profile vs dataset). +- Manifest fields alignment with Mirror thin bundle. + +## Handoff +Use as prep artefact; fill fields once 35-003 outputs and mirror schema decisions are in. diff --git a/docs/modules/export-center/prep/2025-11-20-svc-35-005-prep.md b/docs/modules/export-center/prep/2025-11-20-svc-35-005-prep.md new file mode 100644 index 000000000..3c19f072c --- /dev/null +++ b/docs/modules/export-center/prep/2025-11-20-svc-35-005-prep.md @@ -0,0 +1,16 @@ +# Export Manifest/Signing Prep — PREP-EXPORT-SVC-35-005 + +Status: Draft (2025-11-20) +Owners: Exporter Service · Security Guild +Scope: Manifest/provenance writer + KMS signing/attestation. + +## Proposed manifest +- `export_id`, `profile_id`, `tenant_id`, `artifacts[] {path, sha256, media_type}`, `created_at`, `inputs_hash`, `signatures[]`. +- DSSE envelope for manifest; signer from ICryptoProviderRegistry. + +## Open decisions +- Signing profile list and KMS/HSM mapping. +- Rekor/Transparency use in offline vs online. + +## Handoff +Prep artefact for 35-005; update once crypto profile and provider registry decisions land. diff --git a/docs/modules/findings-ledger/README.md b/docs/modules/findings-ledger/README.md new file mode 100644 index 000000000..07bc1b940 --- /dev/null +++ b/docs/modules/findings-ledger/README.md @@ -0,0 +1,4 @@ + +# Findings Ledger + +Start here for ledger docs. diff --git a/docs/modules/findings-ledger/export-http-surface.md b/docs/modules/findings-ledger/export-http-surface.md new file mode 100644 index 000000000..a09a7ce49 --- /dev/null +++ b/docs/modules/findings-ledger/export-http-surface.md @@ -0,0 +1,70 @@ +# Findings Ledger Export HTTP Surface + +Prep task: PREP-LEDGER-EXPORT-35-001-NO-HTTP-API-SURFACE (Sprint 0121) + +## Goals +- Publish an HTTP surface for deterministic, offline-friendly exports of Findings Ledger data (findings, VEX, advisories, SBOMs) so downstream SDK/OpenAPI tasks can proceed. +- Define filter contract, pagination, media types, and provenance fields required by Evidence Locker and Policy Engine consumers. + +## Non-goals +- Implementing the endpoints (covered by LEDGER-EXPORT-35-001). +- Final OAS/SDK generation (tracked in PREP-LEDGER-OAS-61-001/002/62-001/63-001). + +## Base Service +- Host: findings-ledger service (minimal API) under `src/Findings/StellaOps.Findings.Ledger`. +- Base path: `/ledger/export`. +- Auth: service-to-service bearer token; require `scope=ledger.export.read`. +- Tenancy: `X-Stella-Tenant` header (strictly required); responses never mix tenants. +- Determinism: server sorts by `(event_sequence, projection_version, cycle_hash)`; pagination tokens encode the last emitted tuple and filter set. No wall-clock dependence. +- Media types: default `application/x-ndjson`; clients may request `application/json` (array) for small result sets. Always emit `Content-Encoding: gzip` when `Accept-Encoding` allows. + +## Endpoints + +### 1) Findings +- `GET /ledger/export/findings` +- Filters (all optional unless noted): + - `shape` (required): `canonical` \| `compact` (controls payload shape; compact strips verbose provenance fields for air-gap bundles) + - `since_sequence` (long, ≥0), `until_sequence` (long, inclusive) — enables range slicing. + - `since_observed_at`, `until_observed_at` (ISO-8601 UTC) — observation window. + - `advisory_id` (repeatable), `component_purl` (repeatable), `finding_status` (`open|fixed|dismissed`), `severity` (`critical|high|medium|low|unknown`). + - `risk_profile_version` (string) — filters by attached risk profile revision. +- Response item (canonical shape): + - `finding_id`, `event_sequence`, `observed_at`, `component` (purl, version, source), `advisories` (ids, cwes), `status`, `severity`, `risk` (score, severity, profile_version, explanation_id), `projection_version`, `cycle_hash`, `evidence_bundle_ref` (digest, dsse_digest, timeline_ref), `provenance` (ledger_root, projector_version, policy_version, datasource_ids). + +### 2) VEX +- `GET /ledger/export/vex` +- Filters: `shape`, `since_sequence/until_sequence`, `since_observed_at/until_observed_at`, `product_id` (repeatable), `advisory_id`, `status` (`affected|not_affected|under_investigation`), `statement_type` (`exploitation|justification`). +- Response item adds `vex_statement_id`, `product` (purl or CPE), `status_justification`, `known_exploited` (bool), `timestamp`, `projection_version`, `cycle_hash`, `provenance`. + +### 3) Advisories +- `GET /ledger/export/advisories` +- Filters: `shape`, `since_sequence/until_sequence`, `severity`, `source` (feed id), `cwe_id`, `kev` (bool), `cvss_version`, `cvss_score_min`, `cvss_score_max`. +- Response item: `advisory_id`, `source`, `title`, `description`, `cwes`, `cvss` (version, vector, base_score), `published`, `modified`, `status`, `epss` (score, percentile), `projection_version`, `cycle_hash`, `provenance`. + +### 4) SBOMs +- `GET /ledger/export/sboms` +- Filters: `shape`, `since_sequence/until_sequence`, `since_observed_at/until_observed_at`, `subject_digest` (OCI digest), `sbom_format` (`spdx-json` \| `cyclonedx-json`), `component_purl` (repeatable), `contains_native` (bool), `slsa_build_type` (string). +- Response item: `sbom_id`, `subject` (digest, media_type), `sbom_format`, `created_at`, `components_count`, `has_vulnerabilities` (bool), `materials` (digests), `projection_version`, `cycle_hash`, `provenance`. + +## Pagination +- Cursor param: `page_token` (opaque base64url JSON: `{ "last": { "event_sequence": long, "projection_version": string, "cycle_hash": string }, "filters_hash": sha256 }`). +- Page size: `page_size` (default 500, max 5000). Server rejects if `page_size` differs across pages for same token. +- Response envelope (for both NDJSON and JSON array): + - Header `X-Stella-Next-Page-Token` when more data exists. + - Header `X-Stella-Result-Count` with items count. + - When `Prefer: return=minimal` is set, omit envelope body for NDJSON; clients rely on headers. + +## Error Contract +- 400: unknown filter, invalid range, or filter combination mismatch with `filters_hash` inside `page_token`. +- 401/403: missing or insufficient scope. +- 409: requested `shape` not compatible with downstream air-gap mode (compact requested where policy mandates canonical). +- 429: enforcement of determinism guard (server detected projection drift vs cycle_hash); include `X-Stella-Drift-Reason`. + +## Observability & Determinism Hooks +- Emit structured logs `ledger.export.request` and `ledger.export.emit` with tenant, endpoint, filters_hash, page_size, result_count, duration_ms. +- Counters: `ledger_export_items_total{endpoint,tenant}` and `ledger_export_failures_total{endpoint,reason}`. +- Traces: span name `ledger.export.{endpoint}`; attach `filters_hash`, `page_size`, `next_page_token_present` attributes. + +## artefact location +- This document: `docs/modules/findings-ledger/export-http-surface.md` (hash stability: keep deterministic ordering as authored on 2025-11-20). +- Link from sprint 0121 PREP-LEDGER-EXPORT-35-001; downstream tasks should reference this path for contract details. diff --git a/docs/modules/findings-ledger/oas-baseline.md b/docs/modules/findings-ledger/oas-baseline.md new file mode 100644 index 000000000..47aa12c86 --- /dev/null +++ b/docs/modules/findings-ledger/oas-baseline.md @@ -0,0 +1,28 @@ +# Findings Ledger OAS baseline (v1) + +**Scope.** Establish the canonical OpenAPI baseline and API host definitions for Findings Ledger. This satisfies PREP-LEDGER-OAS-61-001 and unblocks downstream OAS/SDK/OBS tasks (61-002..63-001). + +**What shipped (2025-11-20).** +- Published baseline OAS document: `docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml` (OpenAPI 3.0.3). +- Servers: `https://{env}.ledger.api.stellaops.local` (env ∈ dev/staging/prod/airgap) and offline `https://ledger.{region}.offline.bundle`. +- Security: `bearerAuth` (JWT) and `mTLS` declared; tenant header `X-Stella-Tenant` required. +- Paths included: + - `GET /v1/ledger/events` (deterministic paging by chain/sequence; cursor header). + - `POST /v1/ledger/events` (append; validates hashes; 409 on non-deterministic input). + - `GET /v1/ledger/projections/findings` (projection read with `cycleHash`). +- Schemas align with `docs/modules/findings-ledger/schema.md` (ledger events, projections, hashes, provenance fields). + +**Usage / next steps.** +- Export to renderer or client generation from `findings-ledger.v1.yaml`; keep schema source of truth in YAML, not code-first. +- Downstream tasks (61-002, 62-001, 63-001) should extend this spec with SDK/validation/deprecation headers but **must not** change base paths, security schemes, or canonical field names. +- When new fields are added, update both `schema.md` and the YAML and bump `info.version` with changelog entry. + +**Determinism & offline posture.** +- Stable ordering: events sorted by `(chainId, sequence)`; projections do not perform consensus/merges. +- No external calls required at runtime; offline host is declared for bundle deployments. +- Hash fields (`eventHash`, `previousHash`, `merkleLeafHash`, `cycleHash`) remain lowercase hex SHA-256 per schema. + +**Artifact locations.** +- OAS YAML: `docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml` +- Schema reference: `docs/modules/findings-ledger/schema.md` +- Export surface reference (for later OAS extensions): `docs/modules/findings-ledger/export-http-surface.md` diff --git a/docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml b/docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml new file mode 100644 index 000000000..25fa4a90c --- /dev/null +++ b/docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml @@ -0,0 +1,264 @@ +openapi: 3.0.3 +info: + title: StellaOps Findings Ledger API + version: 1.0.0-beta1 + description: >- + Canonical, aggregation-only surface for append-only findings events, projections, and + Merkle anchoring metadata. Aligns with schema in docs/modules/findings-ledger/schema.md. +servers: + - url: https://{env}.ledger.api.stellaops.local + description: Default environment-scoped host + variables: + env: + default: prod + enum: [dev, staging, prod, airgap] + - url: https://ledger.{region}.offline.bundle + description: Offline bundle host for air-gapped deployments + variables: + region: + default: local + enum: [local] +security: + - bearerAuth: [] + - mTLS: [] +paths: + /v1/ledger/events: + get: + summary: List ledger events + operationId: listLedgerEvents + tags: [ledger] + parameters: + - $ref: '#/components/parameters/TenantId' + - name: chainId + in: query + required: false + schema: + type: string + format: uuid + - name: sinceSequence + in: query + schema: + type: integer + minimum: 0 + - name: limit + in: query + schema: + type: integer + default: 200 + maximum: 1000 + responses: + '200': + description: Paged ledger events in deterministic order (chainId, sequence_No asc) + headers: + Req-Cursor: + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/LedgerEventPage' + post: + summary: Append deterministic ledger event + operationId: appendLedgerEvent + tags: [ledger] + parameters: + - $ref: '#/components/parameters/TenantId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LedgerEventAppendRequest' + responses: + '201': + description: Event persisted + content: + application/json: + schema: + $ref: '#/components/schemas/LedgerEvent' + '409': + description: Hash/sequence conflict (non-deterministic input) + /v1/ledger/projections/findings: + get: + summary: Get latest projection for findings + operationId: listFindingProjections + tags: [projections] + parameters: + - $ref: '#/components/parameters/TenantId' + - name: findingId + in: query + schema: + type: string + - name: policyVersion + in: query + schema: + type: string + - name: status + in: query + schema: + type: string + - name: limit + in: query + schema: + type: integer + default: 200 + maximum: 1000 + responses: + '200': + description: Projection rows with cycleHash for replay validation + content: + application/json: + schema: + $ref: '#/components/schemas/FindingProjectionPage' +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + mTLS: + type: mutualTLS + parameters: + TenantId: + name: X-Stella-Tenant + in: header + required: true + schema: + type: string + schemas: + LedgerEvent: + type: object + required: [event] + properties: + event: + type: object + required: [id, type, tenant, chainId, sequence, policyVersion, occurredAt, recordedAt, payload] + properties: + id: + type: string + format: uuid + type: + type: string + description: ledger_event_type (see schema.md §2.2) + tenant: + type: string + chainId: + type: string + format: uuid + sequence: + type: integer + policyVersion: + type: string + finding: + type: object + properties: + id: { type: string } + artifactId: { type: string } + vulnId: { type: string } + actor: + type: object + properties: + id: { type: string } + type: { type: string } + occurredAt: + type: string + format: date-time + recordedAt: + type: string + format: date-time + payload: + type: object + additionalProperties: true + evidenceBundleRef: + type: string + eventHash: + type: string + previousHash: + type: string + merkleLeafHash: + type: string + LedgerEventPage: + type: object + required: [items] + properties: + items: + type: array + items: + $ref: '#/components/schemas/LedgerEvent' + nextCursor: + type: string + LedgerEventAppendRequest: + type: object + required: [id, type, tenant, chainId, sequence, policyVersion, occurredAt, payload] + properties: + id: + type: string + format: uuid + type: + type: string + tenant: + type: string + chainId: + type: string + format: uuid + sequence: + type: integer + policyVersion: + type: string + finding: + type: object + properties: + id: { type: string } + artifactId: { type: string } + vulnId: { type: string } + actor: + type: object + properties: + id: { type: string } + type: { type: string } + occurredAt: + type: string + format: date-time + payload: + type: object + additionalProperties: true + evidenceBundleRef: + type: string + previousHash: + type: string + description: Optional; validated if supplied + merkleLeafHash: + type: string + description: Optional; server recomputes to validate determinism + FindingProjection: + type: object + required: [tenantId, findingId, policyVersion, status, cycleHash] + properties: + tenantId: { type: string } + findingId: { type: string } + policyVersion: { type: string } + status: { type: string } + severity: + type: number + format: double + labels: + type: object + additionalProperties: true + currentEventId: + type: string + format: uuid + cycleHash: + type: string + updatedAt: + type: string + format: date-time + FindingProjectionPage: + type: object + required: [items] + properties: + items: + type: array + items: + $ref: '#/components/schemas/FindingProjection' + nextCursor: + type: string diff --git a/docs/modules/findings-ledger/prep/2025-11-20-ledger-oas-prep.md b/docs/modules/findings-ledger/prep/2025-11-20-ledger-oas-prep.md new file mode 100644 index 000000000..ad0b27fef --- /dev/null +++ b/docs/modules/findings-ledger/prep/2025-11-20-ledger-oas-prep.md @@ -0,0 +1,19 @@ +# Ledger OAS Prep — PREP-LEDGER-OAS-61-001/61-002/62-001/63-001 + +Status: Draft (2025-11-20) +Owners: Findings Ledger Guild · API Contracts Guild · SDK Generator Guild +Scope: Capture OAS baseline, HTTP surface, SDK generation, and deprecation flow for ledger service. + +## What’s needed +- Baseline OAS for ledger API host (61-001). +- Confirm endpoints and auth scopes to publish (61-002 depends on 61-001). +- SDK generation targets and language list (62-001). +- Deprecation header plan once SDK validated (63-001). + +## Open decisions +- Hostname/versioning scheme for ledger API. +- Auth scopes per endpoint. +- Languages for SDK generation and package naming. + +## Handoff +Use this prep doc for LEDGER-OAS-61-001/61-002/62-001/63-001; update once API contract is drafted. diff --git a/docs/modules/findings-ledger/prep/2025-11-20-ledger-obs-54-001-prep.md b/docs/modules/findings-ledger/prep/2025-11-20-ledger-obs-54-001-prep.md new file mode 100644 index 000000000..fe116a7a9 --- /dev/null +++ b/docs/modules/findings-ledger/prep/2025-11-20-ledger-obs-54-001-prep.md @@ -0,0 +1,12 @@ +# Ledger Observability Prep — PREP-LEDGER-OBS-54-001 + +Status: Draft (2025-11-20) +Owners: Findings Ledger Guild · Provenance Guild +Scope: Minimal API surface for `/ledger/attestations` and observability hooks. + +## Needs +- HTTP surface spec (routes, auth scopes) to host `/ledger/attestations`. +- Telemetry fields to include provenance IDs. + +## Handoff +Use as PREP artefact; update once API contract is drafted. diff --git a/docs/modules/findings-ledger/prep/2025-11-20-ledger-packs-42-001-prep.md b/docs/modules/findings-ledger/prep/2025-11-20-ledger-packs-42-001-prep.md new file mode 100644 index 000000000..79b6182c4 --- /dev/null +++ b/docs/modules/findings-ledger/prep/2025-11-20-ledger-packs-42-001-prep.md @@ -0,0 +1,9 @@ +# Ledger Packs Snapshot Prep — PREP-LEDGER-PACKS-42-001 + +Status: Draft (2025-11-20) +Owners: Findings Ledger Guild · Mirror Creator Guild +Scope: Snapshot/time-travel contract for packs simulation. + +## Needs +- Snapshot format and bundle layout for pack simulation/time-travel. + diff --git a/docs/modules/findings-ledger/prep/2025-11-20-ledger-risk-66-prep.md b/docs/modules/findings-ledger/prep/2025-11-20-ledger-risk-66-prep.md new file mode 100644 index 000000000..9eab5ea2f --- /dev/null +++ b/docs/modules/findings-ledger/prep/2025-11-20-ledger-risk-66-prep.md @@ -0,0 +1,11 @@ +# Ledger Risk Schema Prep — PREP-LEDGER-RISK-66-001/002 + +Status: Draft (2025-11-20) +Owners: Findings Ledger Guild · Risk Engine Guild + +## Needs +- Risk engine schema/contract inputs: `risk_score`, `risk_severity`, `profile_version`, `explanation_id`, indexes. +- Migration plan to add fields. + +## Handoff +Use as PREP artefact; update when risk field definitions and rollout plan are available. diff --git a/docs/modules/findings-ledger/prep/2025-11-20-ledger-risk-prep.md b/docs/modules/findings-ledger/prep/2025-11-20-ledger-risk-prep.md new file mode 100644 index 000000000..a99f92dff --- /dev/null +++ b/docs/modules/findings-ledger/prep/2025-11-20-ledger-risk-prep.md @@ -0,0 +1,17 @@ +# Ledger Risk Prep — PREP-LEDGER-RISK-68-001 / 69-001 / TEN-48-001 + +Status: Draft (2025-11-20) +Owners: Findings Ledger Guild · Export Guild · Observability Guild · Platform + +## 68-001 (scored findings export) needs +- Inputs from 67-001 + Export Center contract for scored findings. +- Metrics dimensions alignment with Observability. + +## 69-001 (metrics) needs +- Final metrics fields driven by 67-001/68-001 outputs. + +## TEN-48-001 (RLS/partitioning) needs +- Platform-approved tenant/project partitioning and RLS policy. + +## Handoff +Use as PREP artefact for the above tasks; update when upstream exports and RLS decisions are available. diff --git a/docs/modules/findings-ledger/prep/ledger-attestations-http.md b/docs/modules/findings-ledger/prep/ledger-attestations-http.md new file mode 100644 index 000000000..532178f03 --- /dev/null +++ b/docs/modules/findings-ledger/prep/ledger-attestations-http.md @@ -0,0 +1,38 @@ +# Ledger attestation HTTP surface (prep for LEDGER-OBS-54-001 / 55-001) + +**Goal.** Provide the minimal HTTP contract to expose ledger attestation verifications so PREP-LEDGER-OBS-55-001 can proceed. This complements the OAS baseline (`docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml`) and schema (`docs/modules/findings-ledger/schema.md`). + +## Endpoint +- `GET /v1/ledger/attestations` +- Tenant header: `X-Stella-Tenant` (required). +- Auth: bearer `scope=ledger.attest.read` or mTLS. +- Query params: + - `artifactId` (string, optional; OCI digest or SBOM id) + - `findingId` (string, optional) + - `attestationId` (uuid, optional) + - `status` (`verified|failed|unknown`, optional) + - `sinceRecordedAt` / `untilRecordedAt` (ISO-8601 UTC) + - `limit` (int, default 200, max 1000) +- Ordering: deterministic by `recordedAt ASC, attestationId ASC`. +- Response: JSON array (or NDJSON when `Accept: application/x-ndjson`). Each item: + - `attestationId` (uuid) + - `artifactId` (string) + - `findingId` (string) + - `verificationStatus` (`verified|failed|unknown`) + - `verificationTime` (ISO-8601 UTC) + - `dsseDigest` (sha256) + - `rekorEntryId` (string, optional) + - `evidenceBundleRef` (string, optional) + - `ledgerEventId` (uuid) — source ledger event that linked the attestation + - `recordedAt` (ISO-8601 UTC) + - `merkleLeafHash` (sha256) + - `rootHash` (sha256) + +## Determinism/offline posture +- Sorting keys are fixed; pagination token encodes `{recordedAt, attestationId, filtersHash}`. +- No live Rekor calls; `rekorEntryId` is stored reference only. +- Hashes remain lowercase SHA-256; times are UTC. + +## Artefact location +- This prep doc: `docs/modules/findings-ledger/prep/ledger-attestations-http.md`. +- Add path to OAS in a follow-on increment (LEDGER-OAS-61-002/63-001) once approved. diff --git a/docs/modules/mirror/assembler.md b/docs/modules/mirror/assembler.md new file mode 100644 index 000000000..79f36656e --- /dev/null +++ b/docs/modules/mirror/assembler.md @@ -0,0 +1,26 @@ +# Mirror Assembler Staffing & Milestones (Sprint 110.D) + +## Staffing +- **Primary owner:** Alex Kim (Mirror Creator Guild) +- **Backup:** Priya Desai (Exporter Guild) +- **Observers:** AirGap Time Guild, CLI Guild + +## Milestones +1. **M0 recap (done 2025-11-19):** thin bundle sample + layout note (`docs/modules/mirror/milestone-0-thin-bundle.md`). +2. **M1 (due 2025-11-26):** implement deterministic assembler with manifest + CAS layout; DSSE signing profile `sovereign-default`; produce sample bundle tarball + hash. +3. **M1.1 (due 2025-11-28):** TUF metadata generation (`root.json`, `snapshot.json`, `timestamp.json`) and mirror index manifest. +4. **M2 (due 2025-12-05):** integrate time-anchor metadata (AirGap Time), export automation hooks (Export Center), and CLI verification path. + +## Responsibilities +- Alex Kim: assembler core, DSSE signing, CAS layout. +- Priya Desai: TUF metadata + exporter handoff. +- AirGap Time Guild: time-anchor schema + verification guidance. +- CLI Guild: `stella mirror verify` wiring and offline cache checks. + +## Artefact locations +- Assembler design: `docs/modules/mirror/thin-bundle-assembler.md` +- Staffing/plan (this doc): `docs/modules/mirror/assembler.md` +- Sample bundles: `out/mirror/thin/mirror-thin-m0-sample.tar.gz` (future M1/M2 under `out/mirror/thin/`) + +## Risks +- If M1 slips past 2025-11-26, Export Center and CLI tracks block; escalate to Exporter guild lead. diff --git a/docs/modules/mirror/prep-56-001-thin-bundle.md b/docs/modules/mirror/prep-56-001-thin-bundle.md new file mode 100644 index 000000000..60eac550b --- /dev/null +++ b/docs/modules/mirror/prep-56-001-thin-bundle.md @@ -0,0 +1,22 @@ +# Mirror Thin Bundle Prep — PREP-MIRROR-CRT-56-001 (Draft) + +Status: Draft (2025-11-20) +Owners: Mirror Guild (Assembler) +Scope: Capture requirements to start thin bundle v1 when upstream Sprint 110.D artefacts land. + +## Dependencies +- Sprint 110.D assembler foundation (missing in repo). +- Trust root list and TUF metadata locations from release pipeline. + +## Proposed thin bundle v1 shape +- Container: tar.gz deterministic; root manifest `mirror.thin.manifest.json`. +- Fields: `bundle_id`, `schema_version`=`mirror.thin.v1`, `created_at`, `source_registry`, `artifacts[] {digest, media_type, size}`, `trust_roots[]`, optional `attestations[]`. +- Merkle root over files for audit. + +## Open decisions +- Exact artifact set included in “thin” scope (SBOM only vs SBOM+metadata). +- Required signatures (DSSE/Sigstore) and signer identities. +- Retention/GC policy for thin bundles. + +## Handoff +Use this as the PREP artefact for PREP-MIRROR-CRT-56-001; update when assembler foundation drops so schema can be finalized and aligned with `docs/modules/mirror/thin-bundle-assembler.md`. diff --git a/docs/modules/orchestrator/prep/2025-11-20-airgap-56-001-prep.md b/docs/modules/orchestrator/prep/2025-11-20-airgap-56-001-prep.md new file mode 100644 index 000000000..46dd4857c --- /dev/null +++ b/docs/modules/orchestrator/prep/2025-11-20-airgap-56-001-prep.md @@ -0,0 +1,12 @@ +# Orchestrator AirGap Prep — PREP-ORCH-AIRGAP-56-001 + +Status: Draft (2025-11-20) +Owners: Orchestrator Service Guild · AirGap Policy Guild +Scope: Awaiting AirGap readiness; capture sealed-mode contract needs for orchestrator. + +## Needs +- Sealed-mode contract from AirGap controller (seal/unseal scopes, staleness fields). +- Mirror bundle pointers to include with orchestrator jobs. + +## Handoff +Use as prep artefact; update once AirGap 56-001 contract is published. diff --git a/docs/modules/orchestrator/prep/2025-11-20-airgap-56-002-prep.md b/docs/modules/orchestrator/prep/2025-11-20-airgap-56-002-prep.md new file mode 100644 index 000000000..26521da8d --- /dev/null +++ b/docs/modules/orchestrator/prep/2025-11-20-airgap-56-002-prep.md @@ -0,0 +1,9 @@ +# Orchestrator AirGap Prep — PREP-ORCH-AIRGAP-56-002 + +Status: Draft (2025-11-20) +Scope: Downstream of 56-001; needs sealed-mode staleness propagation. + +## Needs +- From 56-001: seal contract and bundle pointers. +- Staleness propagation rules to orchestrator runs. + diff --git a/docs/modules/orchestrator/prep/2025-11-20-airgap-57-001-prep.md b/docs/modules/orchestrator/prep/2025-11-20-airgap-57-001-prep.md new file mode 100644 index 000000000..ae98b7084 --- /dev/null +++ b/docs/modules/orchestrator/prep/2025-11-20-airgap-57-001-prep.md @@ -0,0 +1,9 @@ +# Orchestrator AirGap Prep — PREP-ORCH-AIRGAP-57-001 + +Status: Draft (2025-11-20) +Scope: Dependent on 56-002; timeline events for AirGap imports. + +## Needs +- Event types/fields for bundle import timeline. +- Alignment with AirGap mirror bundle IDs and staleness. + diff --git a/docs/modules/orchestrator/prep/2025-11-20-airgap-58-001-prep.md b/docs/modules/orchestrator/prep/2025-11-20-airgap-58-001-prep.md new file mode 100644 index 000000000..06764ca8f --- /dev/null +++ b/docs/modules/orchestrator/prep/2025-11-20-airgap-58-001-prep.md @@ -0,0 +1,8 @@ +# Orchestrator AirGap Prep — PREP-ORCH-AIRGAP-58-001 + +Status: Draft (2025-11-20) +Scope: Dependent on 57-001; Evidence Locker integration for sealed mode. + +## Needs +- Evidence Locker bundle pointers and attestation requirements in sealed mode. + diff --git a/docs/modules/orchestrator/prep/2025-11-20-oas-61-001-prep.md b/docs/modules/orchestrator/prep/2025-11-20-oas-61-001-prep.md new file mode 100644 index 000000000..dac546337 --- /dev/null +++ b/docs/modules/orchestrator/prep/2025-11-20-oas-61-001-prep.md @@ -0,0 +1,12 @@ +# Orchestrator OAS Prep — PREP-ORCH-OAS-61-001/61-002/62-001 + +Status: Draft (2025-11-20) +Scope: Telemetry contract inputs and OAS baseline for orchestrator APIs. + +## Needs +- Telemetry/contract inputs from sprint 150.A (not yet published). +- OAS baseline for orchestrator host. +- SDK generation targets (depends on OAS v1). + +## Handoff +Use as prep artefact for OAS 61/62 chain; update when telemetry inputs published. diff --git a/docs/modules/orchestrator/prep/2025-11-20-svc-41-101-prep.md b/docs/modules/orchestrator/prep/2025-11-20-svc-41-101-prep.md new file mode 100644 index 000000000..7c91ad375 --- /dev/null +++ b/docs/modules/orchestrator/prep/2025-11-20-svc-41-101-prep.md @@ -0,0 +1,17 @@ +# Orchestrator Pack-Run Prep — PREP-ORCH-SVC-41-101 / 42-101 / TEN-48-001 + +Status: Draft (2025-11-20) +Owners: Orchestrator Service Guild + +## 41-101 needs +- Envelope + DAL from 38-101 to register pack runs. +- Storage schema for pack-run records. + +## 42-101 needs +- Stream contract from 41-101 (pack-run plumbing) to drive streaming. + +## TEN-48-001 needs +- Tenant context plumbing for job DAL/routes. + +## Handoff +Use as prep artefact; update when 38-101 envelope lands and DAL schema is fixed. diff --git a/docs/modules/policy/TASKS.md b/docs/modules/policy/TASKS.md index 3dc581820..84a611889 100644 --- a/docs/modules/policy/TASKS.md +++ b/docs/modules/policy/TASKS.md @@ -2,5 +2,8 @@ | Task ID | State | Notes | | --- | --- | --- | +| `PREP-EXPORT-CONSOLE-23-001` | DOING (2025-11-20) | Drafted export bundle + scheduler job contract (see `docs/modules/policy/design/export-console-bundle-contract.md`); waiting on DSSE/storage decisions from Console/Scheduler/Authority. | +| `PREP-POLICY-AIRGAP-56-001` | DOING (2025-11-20) | Drafted mirror bundle schema for air-gap/ sealed mode (see `docs/modules/policy/design/policy-mirror-bundle-schema.md`); waiting on trust-root and retention policy decisions. | +| `PREP-POLICY-ENGINE-30-001` | DOING (2025-11-20) | Drafted overlay projection contract (see `docs/modules/policy/design/policy-overlay-projection.md`); waiting on 29-004 metrics/log schema from Platform/Observability. | | `SCANNER-POLICY-0001` | DONE (2025-11-10) | Ruby component predicates implemented in engine/tests, DSL docs updated, offline kit verifies `seed-data/analyzers/ruby/git-sources`. | | `DOCS-AIAI-31-006` | DONE (2025-11-13) | Published `docs/policy/assistant-parameters.md` capturing Advisory AI configuration knobs (inference/guardrails/cache/queue) and linked it from the module architecture dossier. | diff --git a/docs/modules/policy/design/export-console-bundle-contract.md b/docs/modules/policy/design/export-console-bundle-contract.md new file mode 100644 index 000000000..b9b775dcf --- /dev/null +++ b/docs/modules/policy/design/export-console-bundle-contract.md @@ -0,0 +1,55 @@ +# Export Console Bundle Contract (Draft) — PREP-EXPORT-CONSOLE-23-001 + +Status: Draft (2025-11-20) +Owners: Policy Guild · Scheduler Guild · Observability Guild +Scope: Define the evidence bundle exported by Policy Engine for Console replay/verification and the scheduler job contract that produces it. + +## 1) Manifest format +- Container: tar.gz (deterministic ordering, UTC mtime `1970-01-01T00:00:00Z`), content-addressed by SHA-256 of archive bytes. +- Root manifest: `bundle.manifest.json` (unsigned) and `bundle.manifest.dsse` (signed, optional until Rekor/Authority hookup). +- `bundle.manifest.json` fields: + - `bundle_id` (string, required): GUID/ULID assigned by Policy Engine. + - `policy_run_id` (string, required): ID of originating policy run. + - `tenant_id` (string, required): tenant scope. + - `policy_version` (string, required): semantic version or commit hash of the policy set evaluated. + - `inputs_hash` (hex): SHA-256 of normalized inputs (SBOM, advisories, VEX) used for the run. + - `generated_at` (string, RFC3339 UTC): timestamp of bundle creation. + - `schema_version` (string): `policy.export.console.v1`. + - `artifacts`: array of artefact descriptors with `{path, media_type, sha256, purpose}`; expected purposes: `policy-run-summary`, `effective-findings`, `inputs`, `logs`, `metrics`. + - `signatures`: optional array referencing DSSE statement(s) with signer identity and key hint. + +## 2) Artefact layout +- `summary/policy-run.json`: minimal run summary (status, duration, rule counts, decision stats). +- `findings/effective.ndjson`: deterministic NDJSON list of effective findings emitted by the run (sorted by `finding_id`). +- `inputs/sbom/*.json`: normalized SBOM slices (content-addressed references pointing back to source digests). +- `inputs/advisories/*.json`: advisories snapshot used during evaluation. +- `inputs/vex/*.json`: VEX statements snapshot. +- `telemetry/logs.ndjson`: structured logs with correlation IDs and runner metadata (no secrets, scrub file paths). +- `telemetry/metrics.json`: counters/histograms for run (p50/p95 latency, evaluated rules, facts ingested). + +## 3) Scheduler job contract +- Job type: `policy.export.console`. +- Inputs: + - `policy_run_id` (required) + - `tenant_id` (required) + - `priority` (enum: background|interactive; default background) + - `include_logs` (bool, default true) +- Outputs: + - `bundle_pointer` (URI/path) to stored bundle in Evidence Locker/Export Center bucket. + - `bundle_id`, `sha256`. +- Events: + - Progress events to Observability stream `{job_id, state, percent, bundle_id?, error?}`. + - Completion emits `policy.export.console.completed` with bundle metadata for Console ingestion. + +## 4) Determinism & security +- All NDJSON/JSON sorted by stable keys; floats avoided; timestamps UTC RFC3339. +- No inline PII; tenant-scoped file paths must be redacted or normalized. +- Offline-friendly: no external references; all URIs either `bundle://` or content hashes. + +## 5) Open items / decisions needed +- Signer identity and DSSE profile (Rekor optional) — awaiting Authority/Attestor alignment. +- Exact location for bundle storage (Evidence Locker vs Export Center) — propose Evidence Locker namespace `policy-console/` with retention 30d. +- Metrics schema: align with Observability guild’s latest counters/histogram names. + +## 6) Handoff +Cite this file in sprint trackers for PREP-EXPORT-CONSOLE-23-001. When upstream decisions land, update schema_version and finalize DSSE profile. diff --git a/docs/modules/policy/design/policy-deterministic-evaluator.md b/docs/modules/policy/design/policy-deterministic-evaluator.md new file mode 100644 index 000000000..5adb193e1 --- /dev/null +++ b/docs/modules/policy/design/policy-deterministic-evaluator.md @@ -0,0 +1,27 @@ +# Deterministic Evaluator Spec (Draft) — PREP-POLICY-ENGINE-20-002 + +Status: Draft (2025-11-20) +Owners: Policy Guild +Scope: Define deterministic evaluator requirements for Policy Engine to unblock POLICY-ENGINE-20-002. + +## Determinism rules +- Evaluation order: lexical by rule ID within policy set; stable tie-breaker by condition hash. +- Data types: disallow host clock/network; only allow provided inputs; random seeded with fixed seed per run. +- Outputs: sorted by finding_id; numeric outputs with fixed precision (3 decimals) and invariant culture. +- Time handling: all timestamps in inputs are treated as UTC; now() not permitted. + +## Engine contract +- Input envelope: `{policy_set_id, policy_version, inputs_hash, tenant_id, run_id}`. +- Execution context: immutable; no mutations to global state; cache keyed by inputs_hash + policy_version. +- Result envelope: `{run_id, policy_set_id, policy_version, findings[], metrics{duration_ms, rules_evaluated}}`. + +## Testing strategy +- Golden runs: same inputs produce identical outputs/hashes across runs and machines. +- Property tests: altering rule order must not change result ordering; injecting clock skew should be rejected. + +## Open decisions +- Whether to allow parallel rule evaluation if output ordering remains deterministic (requires stable merge logic). +- Exact numeric tolerance/rounding strategy. + +## Handoff +Use this as the PREP artefact for PREP-POLICY-ENGINE-20-002. Update once numeric rounding and parallelism decisions are finalized. diff --git a/docs/modules/policy/design/policy-mirror-bundle-schema.md b/docs/modules/policy/design/policy-mirror-bundle-schema.md new file mode 100644 index 000000000..a1151ae9a --- /dev/null +++ b/docs/modules/policy/design/policy-mirror-bundle-schema.md @@ -0,0 +1,46 @@ +# Policy Mirror Bundle Schema (Draft) — PREP-POLICY-AIRGAP-56-001 + +Status: Draft (2025-11-20) +Owners: Policy Guild · Cartographer Guild +Scope: Define sealed/offline-ready mirror bundle schema for policy packs to unblock air-gap ingestion (Mirror Bundles v1). + +## 1) Bundle container +- Format: tar.gz with deterministic ordering, UTC mtime `1970-01-01T00:00:00Z`. +- Root: `mirror.manifest.json` plus optional `mirror.manifest.dsse` (post-quantum ready profile TBD). + +## 2) Manifest fields +- `bundle_id` (string, required): ULID. +- `schema_version` (string): `policy.mirror.v1`. +- `created_at` (RFC3339 UTC). +- `producer` (object): `{name, version, build}` of the mirror tool. +- `provenance` (object): `{source_registry, source_digest, fetch_time, trust_roots[]}`. +- `policies` (array) of policy pack descriptors: + - `pack_id` (string): content-addressed ID of the pack archive. + - `version` (string): semver or commit hash. + - `sha256` (string): hash of pack bytes. + - `signatures[]` (optional): detached signatures (Sigstore/DSSE) with key hints. + - `metadata` (object): `{rules_count, dependencies, min_engine_version}`. +- `attestations` (optional array): references to DSSE statements covering manifest or pack artefacts. +- `integrity` (object): `{merkle_root, entries[]}` mirroring file tree for audit. + +## 3) File layout +- `packs/{pack_id}.tar.gz`: raw policy pack. +- `signatures/{pack_id}.sig`: detached signature per pack (optional). +- `notes/compliance.md`: optional compliance notes per pack. + +## 4) Sealed-mode rules +- Whole-bundle DSSE signature required when `sealed=true` (header flag in manifest). +- No post-fetch mutation; ingest must verify merkle root and DSSE before unpack. +- Trust roots pinned per bundle (`provenance.trust_roots[]`), defaulting to org-configured roots when absent. + +## 5) Determinism & validation +- All arrays sorted; use lowercase hex for hashes. +- Validation checklist: bundle hash matches manifest; merkle root recomputes; DSSE signer authorized; engine version compatibility enforced before import. + +## 6) Open decisions +- Final DSSE profile (FIPS/eIDAS/GOST) depending on deployment region. +- Retention/GC policy for stale mirror bundles in sealed mode. +- Whether to embed policy docs or keep external references; draft assumes external docs are excluded for air-gap safety. + +## 7) Handoff +Treat this file as the publishable artefact for PREP-POLICY-AIRGAP-56-001. Update when Authority/Platform finalize DSSE and trust-root policies. diff --git a/docs/modules/policy/design/policy-overlay-projection.md b/docs/modules/policy/design/policy-overlay-projection.md new file mode 100644 index 000000000..965b7e7ad --- /dev/null +++ b/docs/modules/policy/design/policy-overlay-projection.md @@ -0,0 +1,55 @@ +# Policy Overlay Projection Contract (Draft) — PREP-POLICY-ENGINE-30-001 + +Status: Draft (2025-11-20) +Owners: Policy Guild · Cartographer Guild · Platform/Observability Guild +Scope: Define the overlay projection output that depends on metrics/logging outputs from POLICY-ENGINE-29-004. Intended to unblock POLICY-ENGINE-30-001 and downstream 30-00x tasks. + +## 1) Inputs +- `policy_run_id` (required) +- `tenant_id` (required) +- Metrics/logging envelope from 29-004 (pending): expected fields include run duration, rule evaluation counts, fact ingest counts, cache hit/miss, scheduler job metadata. +- Optional: advisory/KB versions, SBOM/VEX digests, risk profile version. + +## 2) Overlay projection shape (proposed) +```json +{ + "overlay_id": "ulid", + "policy_run_id": "...", + "tenant_id": "...", + "generated_at": "2025-11-20T00:00:00Z", + "schema_version": "policy.overlay.v1", + "metrics": { + "duration_ms": 1234, + "rules_evaluated": 4200, + "facts_ingested": 98765, + "cache_hit_rate": 0.92, + "p95_rule_latency_ms": 8 + }, + "logs_pointer": "bundle://telemetry/logs.ndjson", + "inputs": { + "sbom_digest": "sha256:...", + "advisories_digest": "sha256:...", + "vex_digest": "sha256:..." + }, + "provenance": { + "engine_version": "x.y.z", + "profile": "policy-default", + "scheduler_job_id": "..." + } +} +``` +- Determinism: sorted keys; timestamps UTC; numeric metrics fixed to 3 decimal places where fractional. +- Overlay acts as the query surface for simulation/change events (30-002/30-003) and UI overlays. + +## 3) Storage & API +- Stored as NDJSON under `overlays/{tenant_id}/{policy_run_id}.ndjson` in policy engine store; referenced by Export/Console bundle. +- API (proposed): `GET /policy-runs/{policy_run_id}/overlay` with ETag = sha256 of payload; `POST /policy-runs/{policy_run_id}/overlay/rebuild` for re-projection when metrics contract changes. + +## 4) Open dependencies / decisions +- Need final metrics/logging schema from 29-004 to lock `metrics` section (owner: Platform/Observability). +- Confirm cache metrics naming and units. +- Confirm whether overlay should embed inline logs vs pointer. +- Clarify retention/GC policy for overlays (suggest 30d, aligned with export bundles). + +## 5) Handoff +Use this document as the PREP artefact for POLICY-ENGINE-30-001. Update once 29-004 publishes metrics/logging outputs; then fix schema_version to `overlay.v1` and add JSON Schema under `docs/modules/policy/schemas/`. diff --git a/docs/modules/policy/prep/2025-11-20-riskprofile-66-001-prep.md b/docs/modules/policy/prep/2025-11-20-riskprofile-66-001-prep.md new file mode 100644 index 000000000..4f3824d8a --- /dev/null +++ b/docs/modules/policy/prep/2025-11-20-riskprofile-66-001-prep.md @@ -0,0 +1,16 @@ +# Risk Profile Library Prep — PREP-POLICY-RISK-66-001 + +Status: Draft (2025-11-20) +Owners: Risk Profile Schema Guild +Scope: Scaffold requirements for `StellaOps.Policy.RiskProfile` project and storage contract. + +## Needs +- Project skeleton (net10.0) under `src/Policy/StellaOps.Policy.RiskProfile` with schema/validators. +- Storage contract: collections/fields for risk profiles; deterministic hashing rules. + +## Open decisions +- Exact schema fields and validation rules. +- Versioning strategy (semver vs commit hash) for profiles. + +## Handoff +Use as PREP artefact; update once schema fields and storage strategy are agreed. diff --git a/docs/modules/sbomservice/offline-feed-plan.md b/docs/modules/sbomservice/offline-feed-plan.md new file mode 100644 index 000000000..d2889e95b --- /dev/null +++ b/docs/modules/sbomservice/offline-feed-plan.md @@ -0,0 +1,27 @@ +# SBOM Service Offline Feed Plan (prep for PREP-SBOM-CONSOLE-23-001) + +## Problem +SbomService builds/tests were failing restore due to missing NuGet packages (notably `Microsoft.IdentityModel.Tokens >= 8.14.0` and `Pkcs11Interop >= 4.1.0`). Offline/air-gap posture requires a cached feed. + +## What landed (2025-11-20) +- Offline cache populated under `local-nugets/packages/` via `tools/offline/fetch-sbomservice-deps.sh`. +- Key package hashes: + - `Microsoft.IdentityModel.Tokens.8.14.0.nupkg` · SHA256 `00b78c7b7023132e1d6b31d305e47524732dce6faca92dd16eb8d05a835bba7a` + - `Pkcs11Interop.4.1.0.nupkg` · SHA256 `8d2b323a3abb9de47a06a3c3b662aa526ee5c1637b70db072c66dc28e6f14c1e` +- Script: `tools/offline/fetch-sbomservice-deps.sh` (idempotent) hydrates required packages into `local-nugets/packages` using a minimal probe project with `--ignore-failed-sources` to stay air-gap friendly. + +## How to use +```bash +# refresh cache if versions change +./tools/offline/fetch-sbomservice-deps.sh + +# run SbomService tests offline +DOTNET_NOLOGO=1 dotnet test src/SbomService/StellaOps.SbomService.Tests/StellaOps.SbomService.Tests.csproj --no-build --ignore-failed-sources +``` + +## Next actions +- If additional packages surface during `dotnet restore`, append them to the probe project in the script and re-run. +- Keep `local-nugets/` under version control for deterministic builds; update hashes when packages change. + +## Owners +- SBOM Service Guild · Build/Infra (sprint 0142_0001_0001). diff --git a/docs/modules/sbomservice/prep/2025-11-20-build-infra-prep.md b/docs/modules/sbomservice/prep/2025-11-20-build-infra-prep.md new file mode 100644 index 000000000..048e5e644 --- /dev/null +++ b/docs/modules/sbomservice/prep/2025-11-20-build-infra-prep.md @@ -0,0 +1,15 @@ +# Build/Infra Prep — PREP-BUILD-INFRA-SBOM-SERVICE-GUILD-BLOCKED-M + +Status: Draft (2025-11-20) +Owners: SBOM Service Guild +Scope: Document restore/build blocking issues awaiting vetted feed/cache. + +## Blocker summary +- Multiple restore attempts hang/fail; need vetted NuGet feed/cache for SBOM Service solution. + +## Needed actions +- Provide approved offline feed snapshot for SBOM Service dependencies. +- CI runner with feed configured to validate restore. + +## Handoff +Use as PREP artefact; update once feed snapshot is supplied and restore passes. diff --git a/docs/modules/sbomservice/prep/2025-11-20-sbom-service-21-001-prep.md b/docs/modules/sbomservice/prep/2025-11-20-sbom-service-21-001-prep.md new file mode 100644 index 000000000..00ffc46e9 --- /dev/null +++ b/docs/modules/sbomservice/prep/2025-11-20-sbom-service-21-001-prep.md @@ -0,0 +1,20 @@ +# SBOM Service Prep — PREP-SBOM-SERVICE-21-001 + +Status: Draft (2025-11-20) +Owners: SBOM Service Guild · Cartographer Guild +Scope: Waiting on LNM v1 fixtures to freeze normalized SBOM projection read API. + +## Needed inputs +- LNM v1 fixtures (due 2025-11-18 UTC) for schema alignment. + +## Proposed API surface (draft) +- `GET /sboms/{id}/projection` with query `page`, `page_size`, `tenant_id`. +- Response includes `components[]`, `licenses[]`, `hashes[]`, `provenance`. +- Deterministic ordering, tenant enforcement. + +## Open decisions +- Pagination defaults and max page size. +- Which hash algorithms to expose. + +## Handoff +Use as PREP artefact; finalize once fixtures arrive. diff --git a/docs/modules/scanner/design/analyzer-prep-0132.md b/docs/modules/scanner/design/analyzer-prep-0132.md new file mode 100644 index 000000000..fc051d139 --- /dev/null +++ b/docs/modules/scanner/design/analyzer-prep-0132.md @@ -0,0 +1,118 @@ +# Scanner Analyzer Prep · Sprint 0132 + +This note captures the unblockers promised in PREP tasks for Sprint 0132. Each subsection gives the artifact location, assumption set, and the handoff needed by downstream implementation tasks. + +## SCANNER-ANALYZERS-LANG-11-003 (runtime fusion) +- **Objective:** Define the runtime evidence ingest contract to merge AssemblyLoad/Resolving/PInvoke signals with static edges from 11-002. +- **Inputs required:** + - Static edge export format from 11-002 (AssemblyRef/ModuleRef/PInvoke with reason codes). + - Event listener tap points: `AssemblyLoadContext.Resolving`, `AssemblyLoad`, `NativeLibrary.SetDllImportResolver`, `DynamicDependency` attributes, and optional ETW provider `Microsoft-Windows-DotNETRuntime` (keyword 0x8, task AssemblyLoad). +- **Runtime evidence envelope (AOC-aligned):** + ```json + { + "runtime_observation_id": "uuid", + "assembly_name": "System.Text.Json", + "kind": "assembly-load|p-invoke|dynamic-dependency", + "source": "Resolving|AssemblyLoad|NativeLibrary|ETW", + "details": { + "requested_name": "System.Text.Json", + "resolved_path": "", + "assembly_version": "8.0.0.0", + "culture": "neutral", + "package_purl": "pkg:nuget/system.text.json@8.0.0", + "confidence": 0.72, + "reason_code": "runtime-resolve" + }, + "timestamp_utc": "2025-11-20T00:00:00Z" + } + ``` +- **Merge rules for downstream 11-003 implementation:** + - De-dup edges by (assembly_name, resolved_path, kind). + - Prefer static edge confidence when present; runtime adds `confidence_bonus = +0.1` but never exceeds 1.0. + - Keep provenance: `edge.provenance = { "static": bool, "runtime": bool }`. +- **Publication:** This doc section is the frozen location for the runtime ingest contract; downstream tasks should reference this path. + +## SCANNER-ANALYZERS-LANG-11-004 (observation export → writer/SBOM) +- **Objective:** Define the observation payload emitted to Scanner writer and SBOM entrypoint tagging. +- **Export envelope (AOC-compliant):** + ```json + { + "entrypoints": [ + { + "label": "app", + "rids": ["win-x64","linux-x64"], + "tfms": ["net8.0","net8.0-windows"], + "command": "dotnet ./bin/app.dll", + "sources": ["src/App/Program.cs"], + "rank": 1 + } + ], + "dependency_edges": [ + { + "from": "app", + "to": "pkg:nuget/system.text.json@8.0.0", + "reason_code": "assembly-ref", + "confidence": 0.86, + "provenance": {"static": true, "runtime": false} + } + ], + "environment_profiles": { + "tfm": "net8.0", + "rid": "linux-x64", + "host_policy": "portable", + "features": ["singlefile:false","trimmed:false","nativeaot:false"] + } + } + ``` +- **Writer handoff:** + - Serialize as deterministic JSON (sorted keys) to the Scanner writer contract `writer/observations/lang/dotnet`. + - Attach `sbom_entrypoint_tags` derived from entrypoint labels to feed SBOM Service tagging. +- **Publication:** Payload shape and field meanings fixed here for Sprint 0132 downstream work. + +## SCANNER-ANALYZERS-LANG-11-005 (fixtures & benchmarks) +- **Objective:** Provide fixture plan so QA can start without waiting on further design. +- **Fixture matrix:** + - Framework-dependent: `net8.0`, `net9.0-preview` sample apps (console + web minimal API). + - Self-contained: `linux-x64` trimmed vs non-trimmed. + - Single-file: `win-x64` single-file publish, include native hosting bundle. + - NativeAOT: `linux-x64` HelloWorld + P/Invoke stub. + - Multi-RID: RID graph `linux-x64`, `linux-arm64`, `win-x64` with RID fallback expectations. +- **Locations:** place fixtures under `src/Scanner/__Tests/Fixtures/DotNet/11-005/*`; store expected observation JSON in `__Tests/Fixtures/DotNet/11-005/expected/*.json` with sorted keys. +- **Bench envelopes:** + - Target <150 ms p95 per project scan on dev laptop, <25 MB heap delta; capture via BenchmarkDotNet and report to `__Benchmarks/11-005.md`. +- **Determinism:** lock timestamps to `1970-01-01T00:00:00Z` in serialized outputs; stable ordering by (entrypoint label, dependency to PURL, reason_code). + +## SCANNER-ANALYZERS-NATIVE-20-002 (ELF declared-dependency writer contract) +- **Objective:** Unblock writer schema so native analyzer can emit DT_NEEDED/DT_RPATH/DT_RUNPATH data. +- **Edge record (per ELF binary):** + ```json + { + "image": "libssl.so.3", + "build_id": "cafef00d", + "rpath": ["$ORIGIN/lib","/usr/lib"], + "runpath": ["$ORIGIN","/opt/openssl"], + "needed": [ + {"name": "libcrypto.so.3", "slot": 0, "version": "OPENSSL_3.0", "reason_code": "elf-dtneeded"}, + {"name": "libpthread.so.0", "slot": 1, "version": null, "reason_code": "elf-dtneeded"} + ], + "interpreter": "/lib64/ld-linux-x86-64.so.2", + "origin": "virtual-fs", + "confidence": 0.82 + } + ``` +- **Writer path:** `writer/observations/native/elf-declared-deps` (append-only NDJSON; sorted by image name then slot). +- **Redaction:** no host absolute paths; resolve `$ORIGIN` using virtual image root only. +- **Publication:** schema above is the agreed baseline for downstream tasks; time-boxed to Sprint 0132. + +## SCANNER-ANALYZERS-NODE-22-001 (isolated runner / scoped build graph) +- **Objective:** Provide a deterministic way to run Node analyzer tests without fanning out the whole solution. +- **Approach:** + - Add target solution filter: `src/Scanner/StellaOps.Scanner.Analyzers.Lang.Node.slnf` including only Node projects + shared test utilities. + - Introduce `Directory.Build.props` override for `Lang.Node` tests to disable cross-solution restore (`DisableTransitiveProjectReferences=true`). + - Test command for CI + local: `dotnet test src/Scanner/StellaOps.Scanner.Analyzers.Lang.Node.Tests/StellaOps.Scanner.Analyzers.Lang.Node.Tests.csproj /p:DisableTransitiveProjectReferences=true --no-restore --logger:"console;verbosity=minimal"`. + - Cache seeds: copy pnpm/Yarn fixtures into `obj/fixtures-cache` during test init; deterministic zip timestamps set to `1980-01-01`. +- **Publication:** This runbook unblocks execution while broader solution build contention is resolved; downstream tasks should adopt this invocation until Sprint 131 completes. + +--- +**Owners:** Scanner EPDR Guild (DotNet), SBOM Service Guild, Native Analyzer Guild, Node Analyzer Guild. +**Status:** All PREP artifacts published 2025-11-20. diff --git a/docs/modules/scanner/prep/2025-11-20-analyzers-prep.md b/docs/modules/scanner/prep/2025-11-20-analyzers-prep.md new file mode 100644 index 000000000..7ecd451ba --- /dev/null +++ b/docs/modules/scanner/prep/2025-11-20-analyzers-prep.md @@ -0,0 +1,33 @@ +# Scanner Analyzers PREP Notes — 2025-11-20 + +Owner: Scanner EPDR Guild · Signals Guild · SBOM Service Guild · Native Analyzer Guild +Scope: Prep deliverables for PREP-SCANNER-ANALYZERS-LANG-11-003-DEPENDS-ON, PREP-SCANNER-ANALYZERS-LANG-11-004-DEPENDS-ON, and PREP-SCANNER-ANALYZERS-NATIVE-20-002-AWAIT-DE. + +## 1) LANG-11-003 runtime evidence ingest (dependent on 11-002) +- Required upstream: static analyzer outputs from 11-002 (AssemblyRef/ModuleRef/PInvoke/reflection edges with reason codes/confidence). +- Runtime harness spec (draft): + - Listener surfaces `AssemblyLoad`, `Resolving`, `FirstChanceException` events; optional OS-specific providers gated by feature flag `scanner:analyzers:dotnet:runtime-capture` (default off). + - Output shape fed into merger: `runtime_edges[]` with fields `{from_entrypoint, to_identity, reason_code, confidence, evidence}`; `evidence` captures event type, payload fragment, timestamp (UTC ISO-8601), and source provider. + - Merge rules: prefer static edges; runtime edges only add new edges or upgrade confidence; no removals. +- Test plan stub: targeted harness tests under `StellaOps.Scanner.Analyzers.Lang.DotNet.Tests` using recorded ETW/EventListener traces; determinism enforced via sorted edges and normalized timestamps. + +## 2) LANG-11-004 normalized observation export (dependent on 11-003) +- Export contract (AOC compliant) to Scanner writer: + - `entrypoints[]` with `{id, assembly_name, mvid, tfm, rid, kind}`. + - `dependency_edges[]` with `{from_entrypoint, to_component, reason_code, confidence, source={static|runtime|declared}, evidence_ref}`. + - `environment_profiles[]` capturing loader configuration (search paths, probing settings) without host-specific absolute paths; deterministic ordering. +- Integration expectations: + - Writer API endpoint path: `scanner/writer/analyzers/dotnet/runtime-static-fusion` (to be aligned with writer team). + - SBOM tagging: entrypoints annotated with export IDs; avoid adding derived severity. +- Testing hook: golden JSON exports under `src/Scanner/__Tests/Fixtures/lang11/export/*.json`, referenced by `ObservationExportTests` once 11-003 is ready. + +## 3) NATIVE-20-002 declared-dependency writer (await declared-dependency contract) +- Scope: emit declared dependencies from ELF dynamic sections with `reason_code=elf-dtneeded`, include `rpath/runpath` and symbol version needs when present. +- Contract expectations: + - Writer record fields: `{binary_id, needed_soname, search_path_hint[], runpath[], build_id, interpreter, version_need[]}`; all ordered deterministically. + - Input parser must normalize duplicate `DT_NEEDED` entries and preserve order of appearance. +- Dependencies: needs finalized declared-dependency writer interface from Scanner writer team; block remains until contract lands, but this prep defines expected payload shape and ordering. +- Test stub guidance: place fixtures under `src/Scanner/__Tests/Fixtures/native/elf-dtneeded/*` with baseline YAML/JSON; benchmark target <25ms per binary on baseline fixtures. + +## Handoff +- This document is the published prep artefact requested by the above PREP tasks. Implementation tasks should cite this file until upstream contracts arrive. diff --git a/docs/modules/scanner/prep/2025-11-20-java-21-005-prep.md b/docs/modules/scanner/prep/2025-11-20-java-21-005-prep.md new file mode 100644 index 000000000..d3e31259d --- /dev/null +++ b/docs/modules/scanner/prep/2025-11-20-java-21-005-prep.md @@ -0,0 +1,26 @@ +# Java Analyzer Prep — SCANNER-ANALYZERS-JAVA-21-005 (PREP) + +Status: Draft (2025-11-20) +Owners: Java Analyzer Guild +Scope: Capture prerequisites and fixture expectations to unblock SCANNER-ANALYZERS-JAVA-21-005 once upstream build issues clear. + +## Blocking issues observed +- Repository build fails in Concelier due to missing `CoreLinksets` Mongo interfaces; prevents running targeted Java analyzer tests. +- Targeted `dotnet test` stalls during restore/build on shared runner; needs clean CI slot or scoped solution. + +## Required upstream artifacts +- Concelier/CoreLinksets packages or mocks to let Java analyzer tests restore/build. +- CI job or local script to run Java analyzer tests in isolation (`StellaOps.Scanner.Analyzers.Lang.Java.Tests.csproj`) without full solution restore. + +## Expected outputs once unblocked +- Framework config extraction evidence covering Spring Boot imports, app/bootstrap configs, web.xml/fragments, JPA/CDI/JAXB configs, logging files, Graal native-image configs. +- JNI/native hint detection: System.load/Library literals, bundled native libs, Graal JNI configs with component metadata. +- Deterministic hashing of config evidence (SHA-256) with stable ordering. + +## Test/fixture plan +- Fixtures under `src/Scanner/__Tests/Fixtures/java/21-005/*` capturing the above configs. +- Regression tests in `StellaOps.Scanner.Analyzers.Lang.Java.Tests/FrameworkConfigTests.cs` asserting evidence presence and hashes. +- Add CI note: prefer `dotnet test ...Java.Tests.csproj --filter Category=FrameworkConfig` once solutions restore cleanly. + +## Handoff +Use this document as the published prep artefact for PREP-SCANNER-ANALYZERS-JAVA-21-005-TESTS-BLOC. Update once Concelier/CoreLinksets dependency is resolved or CI isolation is available. diff --git a/docs/modules/scanner/prep/2025-11-20-java-21-008-prep.md b/docs/modules/scanner/prep/2025-11-20-java-21-008-prep.md new file mode 100644 index 000000000..24aa0d924 --- /dev/null +++ b/docs/modules/scanner/prep/2025-11-20-java-21-008-prep.md @@ -0,0 +1,26 @@ +# Java Analyzer Prep — SCANNER-ANALYZERS-JAVA-21-008 + +Status: Draft (2025-11-20) +Owners: Java Analyzer Guild +Scope: Resolver + AOC writer emitting entrypoints/components/edges with reason codes/confidence; depends on 21-007 outputs. + +## Dependencies +- 21-007 manifest metadata collector outputs (signers, manifest attributes) required to seed resolver inputs. +- CoreLinksets/Concelier build health to allow Java analyzer test runs. + +## Proposed resolver outputs +- `entrypoints[]`: `{id, path, manifest_main_class?, agent_class?, start_class?, module}`. +- `components[]`: modules/JARs with `{purl?, sha256, module_name?, signed?, signer_ids[]}`. +- `edges[]`: `{from_entrypoint, to_component, reason_code (jpms|cp|spi|reflect|jni|runtime), confidence, evidence}`. +- Deterministic ordering: sort edges by (from_entrypoint, to_component, reason_code). + +## Tests/fixtures +- Place fixtures under `src/Scanner/__Tests/Fixtures/java/21-008/*` covering jpms, classpath, SPI, reflection, JNI cases. +- Regression tests: `ResolverOutputs_AreDeterministic` and `EdgesIncludeReasonAndConfidence`. + +## Open decisions +- Exact confidence scale (0–1 vs categorical) to align with downstream Surface. +- Whether to emit runtime edges in this task vs deferring to 21-010. + +## Handoff +Use this doc as the PREP artefact for 21-008; update once 21-007 outputs and confidence scale are finalized. diff --git a/docs/modules/scanner/prep/2025-11-20-lang-11-001-prep.md b/docs/modules/scanner/prep/2025-11-20-lang-11-001-prep.md new file mode 100644 index 000000000..86c33e374 --- /dev/null +++ b/docs/modules/scanner/prep/2025-11-20-lang-11-001-prep.md @@ -0,0 +1,28 @@ +# .NET Lang Analyzer Prep — SCANNER-ANALYZERS-LANG-11-001 + +Status: Draft (2025-11-20) +Owners: Scanner EPDR Guild · Language Analyzer Guild +Scope: Entrypoint resolver mapping project/publish artifacts to deterministic entrypoint identities; PREP covers test isolation and hang debugging. + +## Blocking issues +- `dotnet test` hangs/returns empty output on shared runner; high restore/build fan-out. +- Concelier/CoreLinksets build errors encountered during prior attempts. + +## Proposed mitigation +- Add CI job to run `dotnet test src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj --filter Category=Entrypoints --blame-hang-timeout 45s` on clean agent. +- Provide scoped solution `StellaOps.Scanner.Analyzers.Lang.DotNet.slnf` to limit restore. + +## Expected outputs +- Entrypoints array: `{id, assembly_name, mvid, tfm, rid, kind}` with deterministic ID hashing (`sha256` over path+tfm+rid). +- Environment profiles: loader/probing settings minus host absolute paths. + +## Test/fixtures +- Fixtures under `src/Scanner/__Tests/Fixtures/dotnet/entrypoints/*` for framework-dependent, self-contained, NativeAOT, multi-RID, single-file, trimmed builds. +- Golden outputs sorted by entrypoint id; verify hash stability across OS. + +## Open decisions +- Hash input shape for `id` (include project GUID?): to confirm with Signals guild. +- Whether to allow RID inference from runtimeconfig vs RID graph; requires Signals sign-off. + +## Handoff +Treat this as the PREP artefact for PREP-SCANNER-ANALYZERS-LANG-11-001; update once CI isolation and hash rules are confirmed. diff --git a/docs/modules/scheduler/prep/2025-11-20-surface-fs-pointer.md b/docs/modules/scheduler/prep/2025-11-20-surface-fs-pointer.md new file mode 100644 index 000000000..a84529f6f --- /dev/null +++ b/docs/modules/scheduler/prep/2025-11-20-surface-fs-pointer.md @@ -0,0 +1,27 @@ +# Surface FS Pointer Contract (Draft) — PREP-SCHED-SURFACE-01 + +Status: Draft (2025-11-20) +Owners: Scheduler Worker Guild +Scope: Define pointer model for Surface FS to unblock scheduler worker planning. + +## Pointer model +- Identifier: `surfacefs:////`. +- Fields: + - `tenant_id` + - `dataset` (e.g., `sbom`, `findings`, `reachability`) + - `version` (content hash or monotonic version) + - `storage_uri` (unset/relative in sealed mode; content-addressed path recommended) + - `created_at` (RFC3339 UTC) +- Deterministic JSON serialization with sorted keys. + +## Scheduler usage +- Workers receive pointer in job payload; pointer is immutable per job. +- Cache key = `surface_fs_pointer`. +- Validation: ensure `dataset` is allowlisted; reject untrusted storage_uri when sealed mode is on. + +## Open decisions +- Final allowlist of datasets. +- Whether inline `storage_uri` is permitted under sealed mode or requires local mirror resolution. + +## Handoff +Use this doc as the PREP artefact for PREP-SCHED-SURFACE-01; update once datasets and sealed-mode rule are agreed. diff --git a/docs/modules/scheduler/prep/2025-11-20-worker-23-101-prep.md b/docs/modules/scheduler/prep/2025-11-20-worker-23-101-prep.md new file mode 100644 index 000000000..f7efc69fb --- /dev/null +++ b/docs/modules/scheduler/prep/2025-11-20-worker-23-101-prep.md @@ -0,0 +1,21 @@ +# Scheduler Worker PREP — SCHED-WORKER-23-101 + +Status: Draft (2025-11-20) +Owners: Scheduler Worker Guild · Policy Guild +Scope: Capture activation event contract and throttle source needed for worker 23-101. + +## Required inputs +- Activation event schema from Policy guild: expected fields `{job_id, policy_run_id, tenant_id, priority, throttle_source}`. +- Throttle source definition (static config vs dynamic policy signals). + +## Proposed contract +- Event type: `scheduler.policy.activation.requested`. +- Payload: `{job_id, policy_run_id, tenant_id, priority, requested_at_utc, throttle_source}`. +- Throttle source values: `scheduler-default`, `policy-signal`, `manual-override` (to be confirmed). + +## Open decisions +- Whether to carry bundle pointers (policy/export) in the activation event. +- Retry/backoff policy if Policy service is down. + +## Handoff +Use this doc as PREP artefact for SCHED-WORKER-23-101; update once Policy guild provides final schema/throttle rules. diff --git a/docs/runbooks/replay_ops_prep_187_004.md b/docs/runbooks/replay_ops_prep_187_004.md new file mode 100644 index 000000000..4bf9534ce --- /dev/null +++ b/docs/runbooks/replay_ops_prep_187_004.md @@ -0,0 +1,20 @@ +# Replay Ops Runbook Prep — PREP-RUNBOOK-REPLAY-187-004 (Draft) + +Status: Draft (2025-11-20) +Owners: Docs Guild · Ops Guild +Scope: Capture required sections for the replay operations runbook once APIs are finalized. + +## Runbook sections to include +1) **Prechecks**: confirm trust roots, air-gap status, Evidence Locker availability, and bundle integrity (`sha256`, DSSE verification). +2) **Ingestion**: steps to POST replay records to Evidence Locker (`/replay/records`), expected responses, retry/backoff guidance. +3) **Verification**: CLI commands (`stella verify --bundle`), Attestor verification call once available, interpreting results. +4) **Replay/Compare**: `stella replay` and `stella diff` workflows, deterministic output locations, common failure modes. +5) **Observability**: log/metric names to watch (to be filled after Evidence Locker/CLI finalize telemetry). +6) **Escalation**: contacts for Scanner/CLI/Attestor/Evidence Locker guilds. + +## Dependencies +- Replay payload schema (`docs/modules/evidence-locker/replay-payload-contract.md`). +- CLI and Attestor prep docs for command/API details. + +## Handoff +When APIs finalize, merge this content into `docs/runbooks/replay_ops.md` and retire this prep stub. diff --git a/docs/samples/evidence-locker/attestation-v1-sample.json b/docs/samples/evidence-locker/attestation-v1-sample.json new file mode 100644 index 000000000..26a966f25 --- /dev/null +++ b/docs/samples/evidence-locker/attestation-v1-sample.json @@ -0,0 +1,18 @@ +{ + "subject_digest": "sha256:deadbeef", + "bundle_id": "11111111-2222-3333-4444-555555555555", + "produced_at": "2025-11-20T00:00:00Z", + "producer": "evidence-locker:us-gov-west", + "hashes": { + "sboms/spdx.json": "abcdef", + "vex/osv.json": "123456" + }, + "sbom": [ + {"digest": "sha256:abcdef", "mediaType": "application/spdx+json"} + ], + "vex": [ + {"digest": "sha256:123456", "schema": "openvex-1.0"} + ], + "signing_profile": "sovereign-default", + "transparency": null +} diff --git a/docs/samples/excititor/chunk-attestation-sample.json b/docs/samples/excititor/chunk-attestation-sample.json new file mode 100644 index 000000000..c1bcee751 --- /dev/null +++ b/docs/samples/excititor/chunk-attestation-sample.json @@ -0,0 +1,18 @@ +{ + "subject_digest": "sha256:112233", + "predicates": { + "stellaops.vex.chunk.meta.v1": { + "tenant": "acme", + "source": "ghsa", + "schema": "stellaops.vex.chunk.v1", + "item_count": 1 + }, + "stellaops.vex.chunk.integrity.v1": { + "items": [ + {"ordinal": 0, "sha256": "abc"} + ] + } + }, + "signing_profile": "sovereign-default", + "transparency": null +} diff --git a/docs/samples/excititor/chunk-sample.ndjson b/docs/samples/excititor/chunk-sample.ndjson new file mode 100644 index 000000000..07053ae4f --- /dev/null +++ b/docs/samples/excititor/chunk-sample.ndjson @@ -0,0 +1 @@ +{"chunk_id":"11111111-2222-3333-4444-555555555555","tenant":"acme","source":"ghsa","schema":"stellaops.vex.chunk.v1","items":[{"advisory_id":"GHSA-123","status":"affected","purl":"pkg:npm/foo@1.0.0"}],"provenance":{"fetched_at":"2025-11-20T00:00:00Z","artifact_sha":"abc"}} diff --git a/docs/samples/excititor/connector-signer-metadata-sample.json b/docs/samples/excititor/connector-signer-metadata-sample.json new file mode 100644 index 000000000..ede280283 --- /dev/null +++ b/docs/samples/excititor/connector-signer-metadata-sample.json @@ -0,0 +1,93 @@ +{ + "schemaVersion": "1.0.0", + "generatedAt": "2025-11-20T00:00:00Z", + "connectors": [ + { + "connectorId": "excititor:msrc", + "provider": { "name": "Microsoft Security Response Center", "slug": "msrc" }, + "issuerTier": "tier-1", + "signers": [ + { + "usage": "csaf", + "fingerprints": [ + {"alg": "sha256", "format": "pgp", "value": "F1C3D9E4A7B28C5FD6E1A203B947C2A0C5D8BEEF"}, + {"alg": "sha256", "format": "x509-spki", "value": "5A1F4C0E9B27D0C64EAC1F22C3F501AA9FCB77AC8B1D4F9F3EA7E6B4CE90F311"} + ], + "keyLocator": "oci://mirror.stella.local/keys/msrc-csaf@sha256:793dd8a6..." + } + ], + "bundle": { + "kind": "oci-referrer", + "uri": "oci://mirror.stella.local/msrc/csaf:2025-11-19", + "digest": "sha256:4b8c9fd6e479e1b6dcd2e7ed93a85c1c7d6052f7b4a6b83471e44f5c9c2a1f30", + "publishedAt": "2025-11-19T12:00:00Z" + }, + "validFrom": "2025-11-01" + }, + { + "connectorId": "excititor:oracle", + "provider": { "name": "Oracle", "slug": "oracle" }, + "issuerTier": "tier-1", + "signers": [ + { + "usage": "oval", + "fingerprints": [ + {"alg": "sha256", "format": "x509-spki", "value": "6E3AC4A95BD5402F4C7E9B2371190E0F3B3C11C7B42B88652E7EE0F659A0D202"} + ], + "keyLocator": "file://offline-kits/oracle/oval/signing-chain.pem", + "certificateChain": ["-----BEGIN CERTIFICATE-----\nMIID...oracle-root...\n-----END CERTIFICATE-----"] + } + ], + "bundle": { + "kind": "file", + "uri": "file://offline-kits/oracle/oval/oval-feed-2025-11-18.tar.gz", + "digest": "sha256:b13b1b84af1da7ee3433e0c6c0cc28a8b5c7d3e52d93b9f86d4a4b0f1dcd8f05", + "publishedAt": "2025-11-18T09:30:00Z" + }, + "validFrom": "2025-10-15" + }, + { + "connectorId": "excititor:oci.openvex.attest", + "provider": { "name": "StellaOps Mirror", "slug": "stella-mirror" }, + "issuerTier": "tier-0", + "signers": [ + { + "usage": "openvex", + "fingerprints": [ + {"alg": "sha256", "format": "cosign", "value": "a0c1d4e5f6b7982134d56789e0fab12345cdef6789abcdeffedcba9876543210"} + ], + "keyLocator": "oci://mirror.stella.local/keys/stella-mirror-openvex:1", + "certificateChain": [] + } + ], + "bundle": { + "kind": "oci-tag", + "uri": "oci://mirror.stella.local/stellaops/openvex:2025-11-19", + "digest": "sha256:77f6c0b8f2c9845c7d0a4f3b783b0caf00cce6fb899319ff69cb941fe2c58010", + "publishedAt": "2025-11-19T15:00:00Z" + }, + "validFrom": "2025-11-15" + }, + { + "connectorId": "excititor:ubuntu", + "provider": { "name": "Ubuntu Security", "slug": "ubuntu" }, + "issuerTier": "tier-2", + "signers": [ + { + "usage": "oval", + "fingerprints": [ + {"alg": "sha256", "format": "pgp", "value": "7D19E3B4A5F67C103CB0B4DE0FA28F90D6E4C1D2"} + ], + "keyLocator": "tuf://mirror.stella.local/tuf/ubuntu/targets/oval-signing.pub" + } + ], + "bundle": { + "kind": "tuf", + "uri": "tuf://mirror.stella.local/tuf/ubuntu/oval/targets/oval-2025-11-18.tar.gz", + "digest": "sha256:e41c4fc15132f8848e9924a1a0f1a247d3c56da87b7735b6c6d8cbe64f0f07e5", + "publishedAt": "2025-11-18T07:00:00Z" + }, + "validFrom": "2025-11-01" + } + ] +} diff --git a/docs/samples/excititor/connector-signer-metadata-sample.json.sha256 b/docs/samples/excititor/connector-signer-metadata-sample.json.sha256 new file mode 100644 index 000000000..19e96823e --- /dev/null +++ b/docs/samples/excititor/connector-signer-metadata-sample.json.sha256 @@ -0,0 +1 @@ +a2f0986d938d877adf01a76b7a9e79cc148f330e57348569619485feb994df1d connector-signer-metadata-sample.json diff --git a/src/AirGap/StellaOps.AirGap.Importer/Contracts/TrustRootConfig.cs b/src/AirGap/StellaOps.AirGap.Importer/Contracts/TrustRootConfig.cs new file mode 100644 index 000000000..aea8c2e42 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Contracts/TrustRootConfig.cs @@ -0,0 +1,17 @@ +namespace StellaOps.AirGap.Importer.Contracts; + +/// +/// Describes the minimal trust-root inputs the importer requires before +/// processing any offline bundle. +/// +public sealed record TrustRootConfig( + string RootBundlePath, + IReadOnlyCollection TrustedKeyFingerprints, + IReadOnlyCollection AllowedSignatureAlgorithms, + DateTimeOffset? NotBeforeUtc, + DateTimeOffset? NotAfterUtc, + IReadOnlyDictionary PublicKeys) +{ + public static TrustRootConfig Empty(string rootBundlePath) => + new(rootBundlePath, Array.Empty(), Array.Empty(), null, null, new Dictionary()); +} diff --git a/src/AirGap/StellaOps.AirGap.Importer/Models/BundleCatalogEntry.cs b/src/AirGap/StellaOps.AirGap.Importer/Models/BundleCatalogEntry.cs new file mode 100644 index 000000000..a286e3a11 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Models/BundleCatalogEntry.cs @@ -0,0 +1,8 @@ +namespace StellaOps.AirGap.Importer.Models; + +public sealed record BundleCatalogEntry( + string TenantId, + string BundleId, + string Digest, + DateTimeOffset ImportedAtUtc, + IReadOnlyList ContentPaths); diff --git a/src/AirGap/StellaOps.AirGap.Importer/Models/BundleItem.cs b/src/AirGap/StellaOps.AirGap.Importer/Models/BundleItem.cs new file mode 100644 index 000000000..0ffd07874 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Models/BundleItem.cs @@ -0,0 +1,8 @@ +namespace StellaOps.AirGap.Importer.Models; + +public sealed record BundleItem( + string TenantId, + string BundleId, + string Path, + string Digest, + long SizeBytes); diff --git a/src/AirGap/StellaOps.AirGap.Importer/Planning/BundleImportPlan.cs b/src/AirGap/StellaOps.AirGap.Importer/Planning/BundleImportPlan.cs new file mode 100644 index 000000000..976c9c2c1 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Planning/BundleImportPlan.cs @@ -0,0 +1,34 @@ +using StellaOps.AirGap.Importer.Validation; + +namespace StellaOps.AirGap.Importer.Planning; + +/// +/// Immutable plan describing the deterministic steps taken when importing an offline bundle. +/// This keeps replay/attestation logic stable for audits. +/// +public sealed record BundleImportPlan( + IReadOnlyList Steps, + IReadOnlyDictionary Inputs, + BundleValidationResult InitialState) +{ + public static BundleImportPlan FromDefaults(string bundlePath) => new List + { + "load-trust-roots", + "read-bundle-manifest", + "verify-dsse-signature", + "verify-manifest-digests", + "validate-tuf-metadata", + "compute-merkle-root", + "stage-object-store", + "record-audit-entry" + }.AsReadOnlyPlan(bundlePath); +} + +file static class BundleImportPlanExtensions +{ + public static BundleImportPlan AsReadOnlyPlan(this IReadOnlyList steps, string bundlePath) => + new(steps, new Dictionary + { + ["bundlePath"] = bundlePath + }, BundleValidationResult.Success("plan-staged")); +} diff --git a/src/AirGap/StellaOps.AirGap.Importer/Planning/BundleImportPlanner.cs b/src/AirGap/StellaOps.AirGap.Importer/Planning/BundleImportPlanner.cs new file mode 100644 index 000000000..7a95c80c1 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Planning/BundleImportPlanner.cs @@ -0,0 +1,39 @@ +using StellaOps.AirGap.Importer.Contracts; +using StellaOps.AirGap.Importer.Validation; + +namespace StellaOps.AirGap.Importer.Planning; + +/// +/// Produces deterministic import plans and performs fast pre-flight checks before expensive validation work. +/// +public sealed class BundleImportPlanner +{ + public BundleImportPlan CreatePlan(string bundlePath, TrustRootConfig trustRoot) + { + if (string.IsNullOrWhiteSpace(bundlePath)) + { + return new BundleImportPlan( + Array.Empty(), + new Dictionary { ["bundlePath"] = "(missing)" }, + BundleValidationResult.Failure("bundle-path-required")); + } + + if (trustRoot.TrustedKeyFingerprints.Count == 0) + { + return new BundleImportPlan( + new[] { "load-trust-roots" }, + new Dictionary { ["bundlePath"] = bundlePath }, + BundleValidationResult.Failure("trust-roots-required")); + } + + if (trustRoot.NotAfterUtc is { } notAfter && trustRoot.NotBeforeUtc is { } notBefore && notAfter < notBefore) + { + return new BundleImportPlan( + new[] { "load-trust-roots" }, + new Dictionary { ["bundlePath"] = bundlePath }, + BundleValidationResult.Failure("invalid-trust-window")); + } + + return BundleImportPlan.FromDefaults(bundlePath); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Importer/Repositories/IBundleCatalogRepository.cs b/src/AirGap/StellaOps.AirGap.Importer/Repositories/IBundleCatalogRepository.cs new file mode 100644 index 000000000..586b183d2 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Repositories/IBundleCatalogRepository.cs @@ -0,0 +1,9 @@ +using StellaOps.AirGap.Importer.Models; + +namespace StellaOps.AirGap.Importer.Repositories; + +public interface IBundleCatalogRepository +{ + Task UpsertAsync(BundleCatalogEntry entry, CancellationToken cancellationToken); + Task> ListAsync(string tenantId, CancellationToken cancellationToken); +} diff --git a/src/AirGap/StellaOps.AirGap.Importer/Repositories/IBundleItemRepository.cs b/src/AirGap/StellaOps.AirGap.Importer/Repositories/IBundleItemRepository.cs new file mode 100644 index 000000000..4bb78c5c7 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Repositories/IBundleItemRepository.cs @@ -0,0 +1,9 @@ +using StellaOps.AirGap.Importer.Models; + +namespace StellaOps.AirGap.Importer.Repositories; + +public interface IBundleItemRepository +{ + Task UpsertManyAsync(IEnumerable items, CancellationToken cancellationToken); + Task> ListByBundleAsync(string tenantId, string bundleId, CancellationToken cancellationToken); +} diff --git a/src/AirGap/StellaOps.AirGap.Importer/Repositories/InMemoryBundleRepositories.cs b/src/AirGap/StellaOps.AirGap.Importer/Repositories/InMemoryBundleRepositories.cs new file mode 100644 index 000000000..7c2bc18d8 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Repositories/InMemoryBundleRepositories.cs @@ -0,0 +1,63 @@ +using StellaOps.AirGap.Importer.Models; + +namespace StellaOps.AirGap.Importer.Repositories; + +/// +/// Deterministic in-memory implementations suitable for offline tests and as a template for Mongo-backed repos. +/// Enforces tenant isolation and stable ordering (by BundleId then Path). +/// +public sealed class InMemoryBundleCatalogRepository : IBundleCatalogRepository +{ + private readonly Dictionary> _catalog = new(StringComparer.Ordinal); + + public Task UpsertAsync(BundleCatalogEntry entry, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var bucket = _catalog.GetValueOrDefault(entry.TenantId) ?? new List(); + bucket.RemoveAll(e => e.BundleId == entry.BundleId); + bucket.Add(entry); + _catalog[entry.TenantId] = bucket; + return Task.CompletedTask; + } + + public Task> ListAsync(string tenantId, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var items = _catalog.GetValueOrDefault(tenantId) ?? new List(); + return Task.FromResult>(items + .OrderBy(e => e.BundleId, StringComparer.Ordinal) + .ToList()); + } +} + +public sealed class InMemoryBundleItemRepository : IBundleItemRepository +{ + private readonly Dictionary<(string TenantId, string BundleId), List> _items = new(); + + public Task UpsertManyAsync(IEnumerable items, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + foreach (var item in items) + { + var key = (item.TenantId, item.BundleId); + if (!_items.TryGetValue(key, out var list)) + { + list = new List(); + _items[key] = list; + } + list.RemoveAll(i => i.Path == item.Path); + list.Add(item); + } + return Task.CompletedTask; + } + + public Task> ListByBundleAsync(string tenantId, string bundleId, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var key = (tenantId, bundleId); + var list = _items.GetValueOrDefault(key) ?? new List(); + return Task.FromResult>(list + .OrderBy(i => i.Path, StringComparer.Ordinal) + .ToList()); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Importer/StellaOps.AirGap.Importer.csproj b/src/AirGap/StellaOps.AirGap.Importer/StellaOps.AirGap.Importer.csproj new file mode 100644 index 000000000..baaafad01 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/StellaOps.AirGap.Importer.csproj @@ -0,0 +1,8 @@ + + + net10.0 + enable + enable + StellaOps.AirGap.Importer + + diff --git a/src/AirGap/StellaOps.AirGap.Importer/Validation/BundleValidationResult.cs b/src/AirGap/StellaOps.AirGap.Importer/Validation/BundleValidationResult.cs new file mode 100644 index 000000000..118a4b802 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Validation/BundleValidationResult.cs @@ -0,0 +1,10 @@ +namespace StellaOps.AirGap.Importer.Validation; + +/// +/// Deterministic validation outcome used by importer and CLI surfaces. +/// +public sealed record BundleValidationResult(bool IsValid, string Reason) +{ + public static BundleValidationResult Success(string reason = "ok") => new(true, reason); + public static BundleValidationResult Failure(string reason) => new(false, reason); +} diff --git a/src/AirGap/StellaOps.AirGap.Importer/Validation/DsseEnvelope.cs b/src/AirGap/StellaOps.AirGap.Importer/Validation/DsseEnvelope.cs new file mode 100644 index 000000000..df29d555d --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Validation/DsseEnvelope.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace StellaOps.AirGap.Importer.Validation; + +public sealed record DsseEnvelope( + [property: JsonPropertyName("payloadType")] string PayloadType, + [property: JsonPropertyName("payload")] string Payload, + [property: JsonPropertyName("signatures")] IReadOnlyList Signatures) +{ + public static DsseEnvelope Parse(string json) + { + var envelope = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + if (envelope is null) + { + throw new InvalidDataException("dsse-envelope-invalid-json"); + } + + return envelope; + } +} + +public sealed record DsseSignature( + [property: JsonPropertyName("keyid")] string KeyId, + [property: JsonPropertyName("sig")] string Signature); diff --git a/src/AirGap/StellaOps.AirGap.Importer/Validation/DsseVerifier.cs b/src/AirGap/StellaOps.AirGap.Importer/Validation/DsseVerifier.cs new file mode 100644 index 000000000..652a839cb --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Validation/DsseVerifier.cs @@ -0,0 +1,90 @@ +using System.Security.Cryptography; +using System.Text; +using StellaOps.AirGap.Importer.Contracts; + +namespace StellaOps.AirGap.Importer.Validation; + +/// +/// Minimal DSSE verifier supporting RSA-PSS/SHA256. The implementation focuses on deterministic +/// pre-authentication encoding (PAE) and fingerprint checks so sealed-mode environments can run +/// without dragging additional deps. +/// +public sealed class DsseVerifier +{ + private const string PaePrefix = "DSSEv1"; + + public BundleValidationResult Verify(DsseEnvelope envelope, TrustRootConfig trustRoots) + { + if (trustRoots.TrustedKeyFingerprints.Count == 0 || trustRoots.PublicKeys.Count == 0) + { + return BundleValidationResult.Failure("trust-roots-required"); + } + + foreach (var signature in envelope.Signatures) + { + if (!trustRoots.PublicKeys.TryGetValue(signature.KeyId, out var keyBytes)) + { + continue; + } + + var fingerprint = ComputeFingerprint(keyBytes); + if (!trustRoots.TrustedKeyFingerprints.Contains(fingerprint)) + { + continue; + } + + var pae = BuildPreAuthEncoding(envelope.PayloadType, envelope.Payload); + if (TryVerifyRsaPss(keyBytes, pae, signature.Signature)) + { + return BundleValidationResult.Success("dsse-signature-verified"); + } + } + + return BundleValidationResult.Failure("dsse-signature-untrusted-or-invalid"); + } + + private static byte[] BuildPreAuthEncoding(string payloadType, string payloadBase64) + { + var payloadBytes = Convert.FromBase64String(payloadBase64); + var parts = new[] + { + PaePrefix, + payloadType, + Encoding.UTF8.GetString(payloadBytes) + }; + + var paeBuilder = new StringBuilder(); + paeBuilder.Append("PAE:"); + paeBuilder.Append(parts.Length); + foreach (var part in parts) + { + paeBuilder.Append(' '); + paeBuilder.Append(part.Length); + paeBuilder.Append(' '); + paeBuilder.Append(part); + } + + return Encoding.UTF8.GetBytes(paeBuilder.ToString()); + } + + private static bool TryVerifyRsaPss(byte[] publicKey, byte[] pae, string signatureBase64) + { + try + { + using var rsa = RSA.Create(); + rsa.ImportSubjectPublicKeyInfo(publicKey, out _); + var sig = Convert.FromBase64String(signatureBase64); + return rsa.VerifyData(pae, sig, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); + } + catch + { + return false; + } + } + + private static string ComputeFingerprint(byte[] publicKey) + { + var hash = SHA256.HashData(publicKey); + return Convert.ToHexString(hash).ToLowerInvariant(); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Importer/Validation/ImportValidator.cs b/src/AirGap/StellaOps.AirGap.Importer/Validation/ImportValidator.cs new file mode 100644 index 000000000..6976b8ed7 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Validation/ImportValidator.cs @@ -0,0 +1,61 @@ +using StellaOps.AirGap.Importer.Contracts; + +namespace StellaOps.AirGap.Importer.Validation; + +/// +/// Coordinates DSSE, TUF, and Merkle verification for an offline import. Stateless and deterministic. +/// +public sealed class ImportValidator +{ + private readonly DsseVerifier _dsse; + private readonly TufMetadataValidator _tuf; + private readonly MerkleRootCalculator _merkle; + private readonly RootRotationPolicy _rotation; + + public ImportValidator() + { + _dsse = new DsseVerifier(); + _tuf = new TufMetadataValidator(); + _merkle = new MerkleRootCalculator(); + _rotation = new RootRotationPolicy(); + } + + public BundleValidationResult Validate(ImportValidationRequest request) + { + var tufResult = _tuf.Validate(request.RootJson, request.SnapshotJson, request.TimestampJson); + if (!tufResult.IsValid) + { + return tufResult with { Reason = $"tuf:{tufResult.Reason}" }; + } + + var dsseResult = _dsse.Verify(request.Envelope, request.TrustRoots); + if (!dsseResult.IsValid) + { + return dsseResult with { Reason = $"dsse:{dsseResult.Reason}" }; + } + + var merkleRoot = _merkle.ComputeRoot(request.PayloadEntries); + if (string.IsNullOrEmpty(merkleRoot)) + { + return BundleValidationResult.Failure("merkle-empty"); + } + + var rotationResult = _rotation.Validate(request.TrustStore.ActiveKeys, request.TrustStore.PendingKeys, request.ApproverIds); + if (!rotationResult.IsValid) + { + return rotationResult with { Reason = $"rotation:{rotationResult.Reason}" }; + } + + return BundleValidationResult.Success("import-validated"); + } +} + +public sealed record ImportValidationRequest( + DsseEnvelope Envelope, + TrustRootConfig TrustRoots, + string RootJson, + string SnapshotJson, + string TimestampJson, + IReadOnlyList PayloadEntries, + TrustStore TrustStore, + IReadOnlyCollection ApproverIds); diff --git a/src/AirGap/StellaOps.AirGap.Importer/Validation/MerkleRootCalculator.cs b/src/AirGap/StellaOps.AirGap.Importer/Validation/MerkleRootCalculator.cs new file mode 100644 index 000000000..c19d87326 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Validation/MerkleRootCalculator.cs @@ -0,0 +1,62 @@ +using System.Security.Cryptography; +using System.Text; + +namespace StellaOps.AirGap.Importer.Validation; + +/// +/// Computes a deterministic Merkle-like root by hashing ordered leaf entries (path + SHA256 of content). +/// Paths are sorted ordinally to stay stable across platforms. +/// +public sealed class MerkleRootCalculator +{ + public string ComputeRoot(IEnumerable entries) + { + var leaves = entries + .OrderBy(e => e.Path, StringComparer.Ordinal) + .Select(HashLeaf) + .ToArray(); + + if (leaves.Length == 0) + { + return string.Empty; + } + + while (leaves.Length > 1) + { + leaves = Pairwise(leaves).ToArray(); + } + + return Convert.ToHexString(leaves[0]).ToLowerInvariant(); + } + + private static byte[] HashLeaf(NamedStream entry) + { + using var sha256 = SHA256.Create(); + using var buffer = new MemoryStream(); + entry.Stream.Seek(0, SeekOrigin.Begin); + entry.Stream.CopyTo(buffer); + var contentHash = sha256.ComputeHash(buffer.ToArray()); + + var leafBytes = Encoding.UTF8.GetBytes(entry.Path.ToLowerInvariant() + ":" + Convert.ToHexString(contentHash).ToLowerInvariant()); + return SHA256.HashData(leafBytes); + } + + private static IEnumerable Pairwise(IReadOnlyList nodes) + { + for (var i = 0; i < nodes.Count; i += 2) + { + if (i + 1 >= nodes.Count) + { + yield return SHA256.HashData(nodes[i]); + continue; + } + + var combined = new byte[nodes[i].Length + nodes[i + 1].Length]; + Buffer.BlockCopy(nodes[i], 0, combined, 0, nodes[i].Length); + Buffer.BlockCopy(nodes[i + 1], 0, combined, nodes[i].Length, nodes[i + 1].Length); + yield return SHA256.HashData(combined); + } + } +} + +public sealed record NamedStream(string Path, Stream Stream); diff --git a/src/AirGap/StellaOps.AirGap.Importer/Validation/RootRotationPolicy.cs b/src/AirGap/StellaOps.AirGap.Importer/Validation/RootRotationPolicy.cs new file mode 100644 index 000000000..5c994024e --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Validation/RootRotationPolicy.cs @@ -0,0 +1,32 @@ +namespace StellaOps.AirGap.Importer.Validation; + +/// +/// Enforces root rotation safety by requiring dual approval and non-empty key sets. +/// +public sealed class RootRotationPolicy +{ + /// + /// Validates that pending keys can replace the active set. Requires at least two distinct approvers + /// and non-empty pending keys. Approvers should be identities recorded in audit log. + /// + public BundleValidationResult Validate(IReadOnlyDictionary activeKeys, IReadOnlyDictionary pendingKeys, IReadOnlyCollection approverIds) + { + if (pendingKeys.Count == 0) + { + return BundleValidationResult.Failure("rotation-pending-empty"); + } + + if (approverIds.Count < 2 || approverIds.Distinct(StringComparer.Ordinal).Count() < 2) + { + return BundleValidationResult.Failure("rotation-dual-approval-required"); + } + + // Prevent accidental no-op rotations. + if (activeKeys.Count == pendingKeys.Count && activeKeys.Keys.All(k => pendingKeys.ContainsKey(k))) + { + return BundleValidationResult.Failure("rotation-no-change"); + } + + return BundleValidationResult.Success("rotation-approved"); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Importer/Validation/TrustStore.cs b/src/AirGap/StellaOps.AirGap.Importer/Validation/TrustStore.cs new file mode 100644 index 000000000..1de581d21 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Validation/TrustStore.cs @@ -0,0 +1,53 @@ +namespace StellaOps.AirGap.Importer.Validation; + +/// +/// In-memory trust store for DSSE/TUF public keys. Designed to be deterministic and side-effect free +/// so it can be reused by offline pipelines and tests. +/// +public sealed class TrustStore +{ + private readonly Dictionary _activeKeys = new(StringComparer.Ordinal); + private readonly Dictionary _pendingKeys = new(StringComparer.Ordinal); + + /// + /// Loads the active key set. Existing keys are replaced to keep runs deterministic. + /// + public void LoadActive(IDictionary keys) + { + _activeKeys.Clear(); + foreach (var kvp in keys) + { + _activeKeys[kvp.Key] = kvp.Value.ToArray(); + } + } + + /// + /// Adds pending keys that require rotation approval. + /// + public void StagePending(IDictionary keys) + { + _pendingKeys.Clear(); + foreach (var kvp in keys) + { + _pendingKeys[kvp.Key] = kvp.Value.ToArray(); + } + } + + public bool TryGetActive(string keyId, out byte[] keyBytes) => _activeKeys.TryGetValue(keyId, out keyBytes!); + + public IReadOnlyDictionary ActiveKeys => _activeKeys; + public IReadOnlyDictionary PendingKeys => _pendingKeys; + + /// + /// Promotes pending keys to active, after callers verify rotation policy (dual approval). + /// + public void PromotePending() + { + _activeKeys.Clear(); + foreach (var kvp in _pendingKeys) + { + _activeKeys[kvp.Key] = kvp.Value; + } + _pendingKeys.Clear(); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Importer/Validation/TufMetadataValidator.cs b/src/AirGap/StellaOps.AirGap.Importer/Validation/TufMetadataValidator.cs new file mode 100644 index 000000000..ef68edcb3 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Importer/Validation/TufMetadataValidator.cs @@ -0,0 +1,64 @@ +using System.Text.Json; + +namespace StellaOps.AirGap.Importer.Validation; + +public sealed class TufMetadataValidator +{ + public BundleValidationResult Validate(string rootJson, string snapshotJson, string timestampJson) + { + try + { + var root = JsonSerializer.Deserialize(rootJson, Options()) ?? throw new InvalidDataException(); + var snapshot = JsonSerializer.Deserialize(snapshotJson, Options()) ?? throw new InvalidDataException(); + var timestamp = JsonSerializer.Deserialize(timestampJson, Options()) ?? throw new InvalidDataException(); + + if (root.Version <= 0 || snapshot.Version <= 0 || timestamp.Version <= 0) + { + return BundleValidationResult.Failure("tuf-version-invalid"); + } + + if (root.ExpiresUtc <= DateTimeOffset.UnixEpoch || snapshot.ExpiresUtc <= DateTimeOffset.UnixEpoch || timestamp.ExpiresUtc <= DateTimeOffset.UnixEpoch) + { + return BundleValidationResult.Failure("tuf-expiry-invalid"); + } + + // Minimal consistency check: timestamp references snapshot hash and version. + if (!string.Equals(snapshot.MetadataHash, timestamp.Snapshot.Meta.Hashes.Sha256, StringComparison.OrdinalIgnoreCase)) + { + return BundleValidationResult.Failure("tuf-snapshot-hash-mismatch"); + } + + return BundleValidationResult.Success("tuf-metadata-valid"); + } + catch (Exception ex) + { + return BundleValidationResult.Failure($"tuf-parse-failed:{ex.GetType().Name.ToLowerInvariant()}"); + } + } + + private static JsonSerializerOptions Options() => new() + { + PropertyNameCaseInsensitive = true + }; + + private sealed record TufRoot(int Version, DateTimeOffset ExpiresUtc); + + private sealed record TufSnapshot(int Version, DateTimeOffset ExpiresUtc, SnapshotMeta Meta) + { + public string MetadataHash => Meta?.Snapshot?.Hashes?.Sha256 ?? string.Empty; + } + + private sealed record SnapshotMeta(SnapshotFile Snapshot); + + private sealed record SnapshotFile(SnapshotHashes Hashes); + + private sealed record SnapshotHashes(string Sha256); + + private sealed record TufTimestamp(int Version, DateTimeOffset ExpiresUtc, TimestampMeta Snapshot); + + private sealed record TimestampMeta(TimestampSnapshot Meta); + + private sealed record TimestampSnapshot(TimestampHashes Hashes); + + private sealed record TimestampHashes(string Sha256); +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Controllers/TimeStatusController.cs b/src/AirGap/StellaOps.AirGap.Time/Controllers/TimeStatusController.cs new file mode 100644 index 000000000..cfcbfced0 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Controllers/TimeStatusController.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Mvc; +using StellaOps.AirGap.Time.Models; +using StellaOps.AirGap.Time.Services; + +namespace StellaOps.AirGap.Time.Controllers; + +[ApiController] +[Route("api/v1/time")] +public class TimeStatusController : ControllerBase +{ + private readonly TimeStatusService _statusService; + private readonly TimeAnchorLoader _loader; + + public TimeStatusController(TimeStatusService statusService, TimeAnchorLoader loader) + { + _statusService = statusService; + _loader = loader; + } + + [HttpGet("status")] + public async Task> GetStatus([FromQuery] string tenantId) + { + if (string.IsNullOrWhiteSpace(tenantId)) + { + return BadRequest("tenantId-required"); + } + + var status = await _statusService.GetStatusAsync(tenantId, DateTimeOffset.UtcNow, HttpContext.RequestAborted); + return Ok(TimeStatusDto.FromStatus(status)); + } + + [HttpPost("anchor")] + public async Task> SetAnchor([FromBody] SetAnchorRequest request) + { + if (!ModelState.IsValid) + { + return ValidationProblem(ModelState); + } + + var trustRoot = new TimeTrustRoot( + request.TrustRootKeyId, + Convert.FromBase64String(request.TrustRootPublicKeyBase64), + request.TrustRootAlgorithm); + + var result = _loader.TryLoadHex( + request.HexToken, + request.Format, + new[] { trustRoot }, + out var anchor); + + if (!result.IsValid) + { + return BadRequest(result.Reason); + } + + var budget = new StalenessBudget( + request.WarningSeconds ?? StalenessBudget.Default.WarningSeconds, + request.BreachSeconds ?? StalenessBudget.Default.BreachSeconds); + + await _statusService.SetAnchorAsync(request.TenantId, anchor, budget, HttpContext.RequestAborted); + var status = await _statusService.GetStatusAsync(request.TenantId, DateTimeOffset.UtcNow, HttpContext.RequestAborted); + return Ok(TimeStatusDto.FromStatus(status)); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Models/SetAnchorRequest.cs b/src/AirGap/StellaOps.AirGap.Time/Models/SetAnchorRequest.cs new file mode 100644 index 000000000..9d89d9a4f --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Models/SetAnchorRequest.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using StellaOps.AirGap.Time.Parsing; + +namespace StellaOps.AirGap.Time.Models; + +public sealed class SetAnchorRequest +{ + [Required] + public string TenantId { get; set; } = string.Empty; + + [Required] + public string HexToken { get; set; } = string.Empty; + + [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; } + public long? BreachSeconds { get; set; } +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Models/StalenessBudget.cs b/src/AirGap/StellaOps.AirGap.Time/Models/StalenessBudget.cs new file mode 100644 index 000000000..1b7deefd2 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Models/StalenessBudget.cs @@ -0,0 +1,22 @@ +namespace StellaOps.AirGap.Time.Models; + +/// +/// Represents tolerated staleness for time anchors. Budgets are seconds and must be non-negative. +/// +public sealed record StalenessBudget(long WarningSeconds, long BreachSeconds) +{ + public static StalenessBudget Default => new(3600, 7200); + + public void Validate() + { + if (WarningSeconds < 0 || BreachSeconds < 0) + { + throw new ArgumentOutOfRangeException(nameof(StalenessBudget), "budgets-must-be-non-negative"); + } + + if (WarningSeconds > BreachSeconds) + { + throw new ArgumentOutOfRangeException(nameof(StalenessBudget), "warning-cannot-exceed-breach"); + } + } +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Models/StalenessEvaluation.cs b/src/AirGap/StellaOps.AirGap.Time/Models/StalenessEvaluation.cs new file mode 100644 index 000000000..13a764eaa --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Models/StalenessEvaluation.cs @@ -0,0 +1,11 @@ +namespace StellaOps.AirGap.Time.Models; + +public sealed record StalenessEvaluation( + long AgeSeconds, + long WarningSeconds, + long BreachSeconds, + bool IsWarning, + bool IsBreach) +{ + public static StalenessEvaluation Unknown => new(0, 0, 0, false, false); +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Models/TimeAnchor.cs b/src/AirGap/StellaOps.AirGap.Time/Models/TimeAnchor.cs new file mode 100644 index 000000000..6eade80a6 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Models/TimeAnchor.cs @@ -0,0 +1,14 @@ +namespace StellaOps.AirGap.Time.Models; + +/// +/// Canonical representation of a trusted time anchor extracted from a signed token. +/// +public sealed record TimeAnchor( + DateTimeOffset AnchorTime, + string Source, + string Format, + string SignatureFingerprint, + string TokenDigest) +{ + public static TimeAnchor Unknown => new(DateTimeOffset.MinValue, "unknown", "unknown", "", ""); +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Models/TimeStatus.cs b/src/AirGap/StellaOps.AirGap.Time/Models/TimeStatus.cs new file mode 100644 index 000000000..ceda89350 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Models/TimeStatus.cs @@ -0,0 +1,10 @@ +namespace StellaOps.AirGap.Time.Models; + +public sealed record TimeStatus( + TimeAnchor Anchor, + StalenessEvaluation Staleness, + StalenessBudget Budget, + DateTimeOffset EvaluatedAtUtc) +{ + public static TimeStatus Empty => new(TimeAnchor.Unknown, StalenessEvaluation.Unknown, StalenessBudget.Default, DateTimeOffset.UnixEpoch); +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Models/TimeStatusDto.cs b/src/AirGap/StellaOps.AirGap.Time/Models/TimeStatusDto.cs new file mode 100644 index 000000000..ab27981a9 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Models/TimeStatusDto.cs @@ -0,0 +1,44 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace StellaOps.AirGap.Time.Models; + +public sealed record TimeStatusDto( + [property: JsonPropertyName("anchorTime")] string AnchorTime, + [property: JsonPropertyName("format")] string Format, + [property: JsonPropertyName("source")] string Source, + [property: JsonPropertyName("fingerprint")] string Fingerprint, + [property: JsonPropertyName("digest")] string Digest, + [property: JsonPropertyName("ageSeconds")] long AgeSeconds, + [property: JsonPropertyName("warningSeconds")] long WarningSeconds, + [property: JsonPropertyName("breachSeconds")] long BreachSeconds, + [property: JsonPropertyName("isWarning")] bool IsWarning, + [property: JsonPropertyName("isBreach")] bool IsBreach, + [property: JsonPropertyName("evaluatedAtUtc")] string EvaluatedAtUtc) +{ + public static TimeStatusDto FromStatus(TimeStatus status) + { + return new TimeStatusDto( + status.Anchor.AnchorTime.ToUniversalTime().ToString("O"), + status.Anchor.Format, + status.Anchor.Source, + status.Anchor.SignatureFingerprint, + status.Anchor.TokenDigest, + status.Staleness.AgeSeconds, + status.Staleness.WarningSeconds, + status.Staleness.BreachSeconds, + status.Staleness.IsWarning, + status.Staleness.IsBreach, + status.EvaluatedAtUtc.ToUniversalTime().ToString("O")); + } + + public string ToJson() + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + WriteIndented = false + }; + return JsonSerializer.Serialize(this, options); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Models/TimeTrustRoot.cs b/src/AirGap/StellaOps.AirGap.Time/Models/TimeTrustRoot.cs new file mode 100644 index 000000000..c375faa91 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Models/TimeTrustRoot.cs @@ -0,0 +1,3 @@ +namespace StellaOps.AirGap.Time.Models; + +public sealed record TimeTrustRoot(string KeyId, byte[] PublicKey, string Algorithm); diff --git a/src/AirGap/StellaOps.AirGap.Time/Parsing/TimeAnchorValidationResult.cs b/src/AirGap/StellaOps.AirGap.Time/Parsing/TimeAnchorValidationResult.cs new file mode 100644 index 000000000..8becce085 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Parsing/TimeAnchorValidationResult.cs @@ -0,0 +1,10 @@ +namespace StellaOps.AirGap.Time.Parsing; + +/// +/// Validation result for a time anchor parse/verify attempt. +/// +public sealed record TimeAnchorValidationResult(bool IsValid, string Reason) +{ + public static TimeAnchorValidationResult Success(string reason = "ok") => new(true, reason); + public static TimeAnchorValidationResult Failure(string reason) => new(false, reason); +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Parsing/TimeTokenFormat.cs b/src/AirGap/StellaOps.AirGap.Time/Parsing/TimeTokenFormat.cs new file mode 100644 index 000000000..a5c68e231 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Parsing/TimeTokenFormat.cs @@ -0,0 +1,7 @@ +namespace StellaOps.AirGap.Time.Parsing; + +public enum TimeTokenFormat +{ + Roughtime, + Rfc3161 +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Parsing/TimeTokenParser.cs b/src/AirGap/StellaOps.AirGap.Time/Parsing/TimeTokenParser.cs new file mode 100644 index 000000000..d479b9831 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Parsing/TimeTokenParser.cs @@ -0,0 +1,41 @@ +using System.Security.Cryptography; +using StellaOps.AirGap.Time.Models; + +namespace StellaOps.AirGap.Time.Parsing; + +/// +/// Performs minimal, deterministic parsing of signed time tokens. Full cryptographic verification +/// is intentionally deferred; this parser focuses on structure and hash derivation so downstream +/// components can stub replay flows in sealed environments. +/// +public sealed class TimeTokenParser +{ + public TimeAnchorValidationResult TryParse(ReadOnlySpan tokenBytes, TimeTokenFormat format, out TimeAnchor anchor) + { + anchor = TimeAnchor.Unknown; + + if (tokenBytes.IsEmpty) + { + return TimeAnchorValidationResult.Failure("token-empty"); + } + + var digestBytes = SHA256.HashData(tokenBytes); + var digest = Convert.ToHexString(digestBytes).ToLowerInvariant(); + + // Derive a deterministic anchor time from digest bytes (no wall clock use). + var seconds = BitConverter.ToUInt64(digestBytes.AsSpan(0, 8)); + var anchorTime = DateTimeOffset.UnixEpoch.AddSeconds(seconds % (3600 * 24 * 365)); // wrap within ~1y for stability + + switch (format) + { + case TimeTokenFormat.Roughtime: + anchor = new TimeAnchor(anchorTime, "roughtime-token", "Roughtime", "(pending)", digest); + return TimeAnchorValidationResult.Success("structure-stubbed"); + case TimeTokenFormat.Rfc3161: + anchor = new TimeAnchor(anchorTime, "rfc3161-token", "RFC3161", "(pending)", digest); + return TimeAnchorValidationResult.Success("structure-stubbed"); + default: + return TimeAnchorValidationResult.Failure("unknown-format"); + } + } +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Program.cs b/src/AirGap/StellaOps.AirGap.Time/Program.cs new file mode 100644 index 000000000..00e7ef143 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Program.cs @@ -0,0 +1,19 @@ +using StellaOps.AirGap.Time.Services; +using StellaOps.AirGap.Time.Stores; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.MapControllers(); + +app.Run(); diff --git a/src/AirGap/StellaOps.AirGap.Time/Services/ITimeTokenVerifier.cs b/src/AirGap/StellaOps.AirGap.Time/Services/ITimeTokenVerifier.cs new file mode 100644 index 000000000..6e686fb5a --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Services/ITimeTokenVerifier.cs @@ -0,0 +1,10 @@ +using StellaOps.AirGap.Time.Models; +using StellaOps.AirGap.Time.Parsing; + +namespace StellaOps.AirGap.Time.Services; + +public interface ITimeTokenVerifier +{ + TimeTokenFormat Format { get; } + TimeAnchorValidationResult Verify(ReadOnlySpan tokenBytes, IReadOnlyList trustRoots, out TimeAnchor anchor); +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Services/Rfc3161Verifier.cs b/src/AirGap/StellaOps.AirGap.Time/Services/Rfc3161Verifier.cs new file mode 100644 index 000000000..e9a05406e --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Services/Rfc3161Verifier.cs @@ -0,0 +1,33 @@ +using System.Security.Cryptography; +using StellaOps.AirGap.Time.Models; +using StellaOps.AirGap.Time.Parsing; + +namespace StellaOps.AirGap.Time.Services; + +public sealed class Rfc3161Verifier : ITimeTokenVerifier +{ + public TimeTokenFormat Format => TimeTokenFormat.Rfc3161; + + public TimeAnchorValidationResult Verify(ReadOnlySpan tokenBytes, IReadOnlyList trustRoots, out TimeAnchor anchor) + { + anchor = TimeAnchor.Unknown; + if (trustRoots.Count == 0) + { + return TimeAnchorValidationResult.Failure("trust-roots-required"); + } + + if (tokenBytes.IsEmpty) + { + return TimeAnchorValidationResult.Failure("token-empty"); + } + + // Stub: derive anchor time deterministically; real ASN.1 verification to be added once trust roots finalized. + var digestBytes = SHA256.HashData(tokenBytes); + var digest = Convert.ToHexString(digestBytes).ToLowerInvariant(); + var seconds = BitConverter.ToUInt64(digestBytes.AsSpan(0, 8)); + var anchorTime = DateTimeOffset.UnixEpoch.AddSeconds(seconds % (3600 * 24 * 365)); + + anchor = new TimeAnchor(anchorTime, "rfc3161-token", "RFC3161", trustRoots[0].KeyId, digest); + return TimeAnchorValidationResult.Success("rfc3161-stub-verified"); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Services/RoughtimeVerifier.cs b/src/AirGap/StellaOps.AirGap.Time/Services/RoughtimeVerifier.cs new file mode 100644 index 000000000..7eaa7216d --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Services/RoughtimeVerifier.cs @@ -0,0 +1,33 @@ +using System.Security.Cryptography; +using StellaOps.AirGap.Time.Models; +using StellaOps.AirGap.Time.Parsing; + +namespace StellaOps.AirGap.Time.Services; + +public sealed class RoughtimeVerifier : ITimeTokenVerifier +{ + public TimeTokenFormat Format => TimeTokenFormat.Roughtime; + + public TimeAnchorValidationResult Verify(ReadOnlySpan tokenBytes, IReadOnlyList trustRoots, out TimeAnchor anchor) + { + anchor = TimeAnchor.Unknown; + if (trustRoots.Count == 0) + { + return TimeAnchorValidationResult.Failure("trust-roots-required"); + } + + if (tokenBytes.IsEmpty) + { + return TimeAnchorValidationResult.Failure("token-empty"); + } + + // Stub: derive anchor time deterministically from digest until real Roughtime decoding is wired. + var digestBytes = SHA256.HashData(tokenBytes); + var digest = Convert.ToHexString(digestBytes).ToLowerInvariant(); + var seconds = BitConverter.ToUInt64(digestBytes.AsSpan(0, 8)); + var anchorTime = DateTimeOffset.UnixEpoch.AddSeconds(seconds % (3600 * 24 * 365)); + + anchor = new TimeAnchor(anchorTime, "roughtime-token", "Roughtime", trustRoots[0].KeyId, digest); + return TimeAnchorValidationResult.Success("roughtime-stub-verified"); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Services/StalenessCalculator.cs b/src/AirGap/StellaOps.AirGap.Time/Services/StalenessCalculator.cs new file mode 100644 index 000000000..500a8c978 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Services/StalenessCalculator.cs @@ -0,0 +1,25 @@ +using StellaOps.AirGap.Time.Models; + +namespace StellaOps.AirGap.Time.Services; + +/// +/// Computes staleness for a given time anchor against configured budgets. +/// +public sealed class StalenessCalculator +{ + public StalenessEvaluation Evaluate(TimeAnchor anchor, StalenessBudget budget, DateTimeOffset nowUtc) + { + budget.Validate(); + + if (anchor.AnchorTime == DateTimeOffset.MinValue) + { + return StalenessEvaluation.Unknown; + } + + var ageSeconds = Math.Max(0, (long)(nowUtc - anchor.AnchorTime).TotalSeconds); + var isBreach = ageSeconds >= budget.BreachSeconds; + var isWarning = ageSeconds >= budget.WarningSeconds; + + return new StalenessEvaluation(ageSeconds, budget.WarningSeconds, budget.BreachSeconds, isWarning, isBreach); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Services/TimeAnchorLoader.cs b/src/AirGap/StellaOps.AirGap.Time/Services/TimeAnchorLoader.cs new file mode 100644 index 000000000..76ccd2422 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Services/TimeAnchorLoader.cs @@ -0,0 +1,37 @@ +using StellaOps.AirGap.Time.Models; +using StellaOps.AirGap.Time.Parsing; + +namespace StellaOps.AirGap.Time.Services; + +/// +/// Loads time anchors from hex-encoded fixtures or bundle payloads and validates basic structure. +/// Cryptographic verification is still stubbed; this keeps ingestion deterministic for offline testing. +/// +public sealed class TimeAnchorLoader +{ + private readonly TimeVerificationService _verification; + + public TimeAnchorLoader() + { + _verification = new TimeVerificationService(); + } + + public TimeAnchorValidationResult TryLoadHex(string hex, TimeTokenFormat format, IReadOnlyList trustRoots, out TimeAnchor anchor) + { + anchor = TimeAnchor.Unknown; + if (string.IsNullOrWhiteSpace(hex)) + { + return TimeAnchorValidationResult.Failure("token-empty"); + } + + try + { + var bytes = Convert.FromHexString(hex.Trim()); + return _verification.Verify(bytes, format, trustRoots, out anchor); + } + catch (FormatException) + { + return TimeAnchorValidationResult.Failure("token-hex-invalid"); + } + } +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Services/TimeStatusService.cs b/src/AirGap/StellaOps.AirGap.Time/Services/TimeStatusService.cs new file mode 100644 index 000000000..cc4910a1a --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Services/TimeStatusService.cs @@ -0,0 +1,32 @@ +using StellaOps.AirGap.Time.Models; +using StellaOps.AirGap.Time.Stores; + +namespace StellaOps.AirGap.Time.Services; + +/// +/// Provides current time-anchor status (anchor + staleness) per tenant. +/// +public sealed class TimeStatusService +{ + private readonly ITimeAnchorStore _store; + private readonly StalenessCalculator _calculator; + + public TimeStatusService(ITimeAnchorStore store, StalenessCalculator calculator) + { + _store = store; + _calculator = calculator; + } + + public async Task SetAnchorAsync(string tenantId, TimeAnchor anchor, StalenessBudget budget, CancellationToken cancellationToken = default) + { + budget.Validate(); + await _store.SetAsync(tenantId, anchor, budget, cancellationToken); + } + + public async Task GetStatusAsync(string tenantId, DateTimeOffset nowUtc, CancellationToken cancellationToken = default) + { + var (anchor, budget) = await _store.GetAsync(tenantId, cancellationToken); + var eval = _calculator.Evaluate(anchor, budget, nowUtc); + return new TimeStatus(anchor, eval, budget, nowUtc); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Services/TimeVerificationService.cs b/src/AirGap/StellaOps.AirGap.Time/Services/TimeVerificationService.cs new file mode 100644 index 000000000..5603acc5d --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Services/TimeVerificationService.cs @@ -0,0 +1,26 @@ +using StellaOps.AirGap.Time.Models; +using StellaOps.AirGap.Time.Parsing; + +namespace StellaOps.AirGap.Time.Services; + +public sealed class TimeVerificationService +{ + private readonly IReadOnlyDictionary _verifiers; + + public TimeVerificationService() + { + var verifiers = new ITimeTokenVerifier[] { new RoughtimeVerifier(), new Rfc3161Verifier() }; + _verifiers = verifiers.ToDictionary(v => v.Format, v => v); + } + + public TimeAnchorValidationResult Verify(ReadOnlySpan tokenBytes, TimeTokenFormat format, IReadOnlyList trustRoots, out TimeAnchor anchor) + { + anchor = TimeAnchor.Unknown; + if (!_verifiers.TryGetValue(format, out var verifier)) + { + return TimeAnchorValidationResult.Failure("unknown-format"); + } + + return verifier.Verify(tokenBytes, trustRoots, out anchor); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Time/StellaOps.AirGap.Time.csproj b/src/AirGap/StellaOps.AirGap.Time/StellaOps.AirGap.Time.csproj new file mode 100644 index 000000000..603ddfc9d --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/StellaOps.AirGap.Time.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + StellaOps.AirGap.Time + + + + + diff --git a/src/AirGap/StellaOps.AirGap.Time/Stores/ITimeAnchorStore.cs b/src/AirGap/StellaOps.AirGap.Time/Stores/ITimeAnchorStore.cs new file mode 100644 index 000000000..af2526264 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Stores/ITimeAnchorStore.cs @@ -0,0 +1,9 @@ +using StellaOps.AirGap.Time.Models; + +namespace StellaOps.AirGap.Time.Stores; + +public interface ITimeAnchorStore +{ + Task SetAsync(string tenantId, TimeAnchor anchor, StalenessBudget budget, CancellationToken cancellationToken); + Task<(TimeAnchor Anchor, StalenessBudget Budget)> GetAsync(string tenantId, CancellationToken cancellationToken); +} diff --git a/src/AirGap/StellaOps.AirGap.Time/Stores/InMemoryTimeAnchorStore.cs b/src/AirGap/StellaOps.AirGap.Time/Stores/InMemoryTimeAnchorStore.cs new file mode 100644 index 000000000..fe873a362 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/Stores/InMemoryTimeAnchorStore.cs @@ -0,0 +1,25 @@ +using StellaOps.AirGap.Time.Models; + +namespace StellaOps.AirGap.Time.Stores; + +public sealed class InMemoryTimeAnchorStore : ITimeAnchorStore +{ + private readonly Dictionary _anchors = new(StringComparer.Ordinal); + + public Task SetAsync(string tenantId, TimeAnchor anchor, StalenessBudget budget, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + _anchors[tenantId] = (anchor, budget); + return Task.CompletedTask; + } + + public Task<(TimeAnchor Anchor, StalenessBudget Budget)> GetAsync(string tenantId, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (_anchors.TryGetValue(tenantId, out var value)) + { + return Task.FromResult(value); + } + return Task.FromResult((TimeAnchor.Unknown, StalenessBudget.Default)); + } +} diff --git a/src/AirGap/StellaOps.AirGap.Time/fixtures/rfc3161-sample.txt b/src/AirGap/StellaOps.AirGap.Time/fixtures/rfc3161-sample.txt new file mode 100644 index 000000000..e263f9efe --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/fixtures/rfc3161-sample.txt @@ -0,0 +1 @@ +308201223081c9a0030201020404c78a5540300d06092a864886f70d01010b0500300d310b3009060355040313025441301e170d3233313132303130303030305a170d3234313132393130303030305a300d310b300906035504031302544130820122300d06092a864886f70d01010105000382010f003082010a0282010100c3e8c4a1b2f7f6... diff --git a/src/AirGap/StellaOps.AirGap.Time/fixtures/roughtime-sample.txt b/src/AirGap/StellaOps.AirGap.Time/fixtures/roughtime-sample.txt new file mode 100644 index 000000000..ecae8b573 --- /dev/null +++ b/src/AirGap/StellaOps.AirGap.Time/fixtures/roughtime-sample.txt @@ -0,0 +1 @@ +0102030473616d706c652d726f75676874696d652d746f6b656e00 diff --git a/src/AirGap/TASKS.md b/src/AirGap/TASKS.md new file mode 100644 index 000000000..8b7544ce9 --- /dev/null +++ b/src/AirGap/TASKS.md @@ -0,0 +1,17 @@ +# AirGap Module Tasks (prep sync) + +| Task ID | Status | Notes | Updated (UTC) | +| --- | --- | --- | --- | +| PREP-AIRGAP-IMP-56-001-IMPORTER-PROJECT-SCAFF | DONE | Scaffolded importer project/tests; doc at `docs/airgap/importer-scaffold.md`. | 2025-11-20 | +| PREP-AIRGAP-IMP-56-002-BLOCKED-ON-56-001 | DONE | Unblocked by importer scaffold/trust-root contract. | 2025-11-20 | +| PREP-AIRGAP-IMP-58-002-BLOCKED-ON-58-001 | DONE | Shares importer scaffold + validation envelopes. | 2025-11-20 | +| PREP-AIRGAP-TIME-57-001-TIME-COMPONENT-SCAFFO | DONE | Time anchor parser scaffold; doc at `docs/airgap/time-anchor-scaffold.md`. | 2025-11-20 | +| PREP-AIRGAP-CTL-56-001-CONTROLLER-PROJECT-SCA | DOING | Controller scaffold draft at `docs/airgap/controller-scaffold.md`; awaiting Authority scopes decision. | 2025-11-20 | +| PREP-AIRGAP-CTL-56-002-BLOCKED-ON-56-001-SCAF | DOING | Uses same scaffold doc; pending DevOps alignment on deployment skeleton. | 2025-11-20 | +| PREP-AIRGAP-CTL-57-001-BLOCKED-ON-56-002 | DONE | Diagnostics doc at `docs/airgap/sealed-startup-diagnostics.md`. | 2025-11-20 | +| PREP-AIRGAP-CTL-57-002-BLOCKED-ON-57-001 | DONE | Telemetry/timeline hooks defined in `docs/airgap/sealed-startup-diagnostics.md`. | 2025-11-20 | +| PREP-AIRGAP-CTL-58-001-BLOCKED-ON-57-002 | DOING | Staleness/time-anchor fields specified; awaiting Time Guild token decision. | 2025-11-20 | +| AIRGAP-IMP-56-001 | DONE | DSSE verifier, TUF validator, Merkle root calculator + import coordinator; tests passing. | 2025-11-20 | +| AIRGAP-IMP-56-002 | DONE | Root rotation policy (dual approval) + trust store; integrated into import validator; tests passing. | 2025-11-20 | +| AIRGAP-IMP-57-001 | DONE | In-memory RLS bundle catalog/items repos + schema doc; deterministic ordering and tests passing. | 2025-11-20 | +| AIRGAP-TIME-57-001 | DOING | Staleness calculator/budgets, hex loader, fixtures, TimeStatusService/store, stub verification pipeline added; crypto verification pending guild inputs. | 2025-11-20 | diff --git a/src/Concelier/StellaOps.Concelier.WebService/Contracts/LnmLinksetContracts.cs b/src/Concelier/StellaOps.Concelier.WebService/Contracts/LnmLinksetContracts.cs new file mode 100644 index 000000000..8148fa8c8 --- /dev/null +++ b/src/Concelier/StellaOps.Concelier.WebService/Contracts/LnmLinksetContracts.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace StellaOps.Concelier.WebService.Contracts; + +public sealed record LnmLinksetResponse( + [property: JsonPropertyName("advisoryId")] string AdvisoryId, + [property: JsonPropertyName("source")] string Source, + [property: JsonPropertyName("observations")] IReadOnlyList Observations, + [property: JsonPropertyName("normalized")] LnmLinksetNormalized? Normalized, + [property: JsonPropertyName("conflicts")] IReadOnlyList? Conflicts, + [property: JsonPropertyName("provenance")] LnmLinksetProvenance? Provenance, + [property: JsonPropertyName("createdAt")] DateTimeOffset CreatedAt, + [property: JsonPropertyName("builtByJobId")] string? BuiltByJobId, + [property: JsonPropertyName("cached")] bool Cached); + +public sealed record LnmLinksetNormalized( + [property: JsonPropertyName("purls")] IReadOnlyList? Purls, + [property: JsonPropertyName("versions")] IReadOnlyList? Versions, + [property: JsonPropertyName("ranges")] IReadOnlyList? Ranges, + [property: JsonPropertyName("severities")] IReadOnlyList? Severities); + +public sealed record LnmLinksetConflict( + [property: JsonPropertyName("field")] string Field, + [property: JsonPropertyName("reason")] string Reason, + [property: JsonPropertyName("values")] IReadOnlyList? Values); + +public sealed record LnmLinksetProvenance( + [property: JsonPropertyName("observationHashes")] IReadOnlyList? ObservationHashes, + [property: JsonPropertyName("toolVersion")] string? ToolVersion, + [property: JsonPropertyName("policyHash")] string? PolicyHash); + +public sealed record LnmLinksetQuery( + [Required] + [property: JsonPropertyName("advisoryId")] string AdvisoryId, + [Required] + [property: JsonPropertyName("source")] string Source, + [property: JsonPropertyName("includeConflicts")] bool IncludeConflicts = true); diff --git a/src/Concelier/StellaOps.Concelier.WebService/Program.cs b/src/Concelier/StellaOps.Concelier.WebService/Program.cs index ddb408f6b..c287ce736 100644 --- a/src/Concelier/StellaOps.Concelier.WebService/Program.cs +++ b/src/Concelier/StellaOps.Concelier.WebService/Program.cs @@ -117,6 +117,8 @@ builder.Services.AddOptions() .ValidateOnStart(); builder.Services.AddConcelierAocGuards(); builder.Services.AddConcelierLinksetMappers(); +builder.Services.AddSingleton(MeterProvider.Default.GetMeterProvider()); +builder.Services.AddSingleton(); builder.Services.AddAdvisoryRawServices(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -460,6 +462,66 @@ var observationsEndpoint = app.MapGet("/concelier/observations", async ( return Results.Ok(response); }).WithName("GetConcelierObservations"); +app.MapGet("/v1/lnm/linksets/{advisoryId}", async ( + HttpContext context, + string advisoryId, + [FromQuery(Name = "source")] string source, + [FromQuery(Name = "includeConflicts")] bool includeConflicts, + [FromServices] IAdvisoryLinksetLookup linksetLookup, + [FromServices] LinksetCacheTelemetry telemetry, + TimeProvider timeProvider, + 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; + } + + if (string.IsNullOrWhiteSpace(advisoryId) || string.IsNullOrWhiteSpace(source)) + { + return Results.BadRequest("advisoryId and source are required."); + } + + var stopwatch = Stopwatch.StartNew(); + + var options = new AdvisoryLinksetQueryOptions(tenant!, Source: source.Trim(), AdvisoryId: advisoryId.Trim()); + var linksets = await linksetLookup.FindByTenantAsync(options.TenantId, options.Source, options.AdvisoryId, cancellationToken).ConfigureAwait(false); + + if (linksets.Count == 0) + { + return Results.NotFound(); + } + + var linkset = linksets[0]; + var response = new LnmLinksetResponse( + linkset.AdvisoryId, + linkset.Source, + linkset.Observations, + linkset.Normalized is null + ? null + : new LnmLinksetNormalized(linkset.Normalized.Purls, linkset.Normalized.Versions, linkset.Normalized.Ranges, linkset.Normalized.Severities), + includeConflicts ? linkset.Conflicts : Array.Empty(), + linkset.Provenance is null + ? null + : new LnmLinksetProvenance(linkset.Provenance.ObservationHashes, linkset.Provenance.ToolVersion, linkset.Provenance.PolicyHash), + linkset.CreatedAt, + linkset.BuiltByJobId, + Cached: true); + + telemetry.RecordHit(tenant, linkset.Source); + telemetry.RecordRebuild(tenant, linkset.Source, stopwatch.Elapsed.TotalMilliseconds); + + return Results.Ok(response); +}).WithName("GetLnmLinkset"); + if (authorityConfigured) { observationsEndpoint.RequireAuthorization(ObservationsPolicyName); diff --git a/src/Concelier/StellaOps.Concelier.WebService/Telemetry/LinksetCacheTelemetry.cs b/src/Concelier/StellaOps.Concelier.WebService/Telemetry/LinksetCacheTelemetry.cs new file mode 100644 index 000000000..bff926234 --- /dev/null +++ b/src/Concelier/StellaOps.Concelier.WebService/Telemetry/LinksetCacheTelemetry.cs @@ -0,0 +1,48 @@ +using System.Diagnostics.Metrics; + +namespace StellaOps.Concelier.WebService.Telemetry; + +internal sealed class LinksetCacheTelemetry +{ + private readonly Counter _hitTotal; + private readonly Counter _writeTotal; + private readonly Histogram _rebuildMs; + + public LinksetCacheTelemetry(IMeterFactory meterFactory) + { + var meter = meterFactory.Create("StellaOps.Concelier.Linksets"); + _hitTotal = meter.CreateCounter("lnm.cache.hit_total", unit: "hit", description: "Cache hits for LNM linksets"); + _writeTotal = meter.CreateCounter("lnm.cache.write_total", unit: "write", description: "Cache writes for LNM linksets"); + _rebuildMs = meter.CreateHistogram("lnm.cache.rebuild_ms", unit: "ms", description: "Synchronous rebuild latency for LNM cache"); + } + + public void RecordHit(string? tenant, string source) + { + var tags = new TagList + { + { "tenant", tenant ?? string.Empty }, + { "source", source } + }; + _hitTotal.Add(1, tags); + } + + public void RecordWrite(string? tenant, string source) + { + var tags = new TagList + { + { "tenant", tenant ?? string.Empty }, + { "source", source } + }; + _writeTotal.Add(1, tags); + } + + public void RecordRebuild(string? tenant, string source, double elapsedMs) + { + var tags = new TagList + { + { "tenant", tenant ?? string.Empty }, + { "source", source } + }; + _rebuildMs.Record(elapsedMs, tags); + } +} diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Core/Linksets/ObservationPipelineServiceCollectionExtensions.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Core/Linksets/ObservationPipelineServiceCollectionExtensions.cs index 53b3d730e..4e1f87242 100644 --- a/src/Concelier/__Libraries/StellaOps.Concelier.Core/Linksets/ObservationPipelineServiceCollectionExtensions.cs +++ b/src/Concelier/__Libraries/StellaOps.Concelier.Core/Linksets/ObservationPipelineServiceCollectionExtensions.cs @@ -12,6 +12,7 @@ public static class ObservationPipelineServiceCollectionExtensions ArgumentNullException.ThrowIfNull(services); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); @@ -25,6 +26,12 @@ public static class ObservationPipelineServiceCollectionExtensions => Task.CompletedTask; } + private sealed class NullObservationEventPublisher : IAdvisoryObservationEventPublisher + { + public Task PublishAsync(AdvisoryObservationUpdatedEvent @event, CancellationToken cancellationToken) + => Task.CompletedTask; + } + private sealed class NullLinksetSink : IAdvisoryLinksetSink { public Task UpsertAsync(AdvisoryLinkset linkset, CancellationToken cancellationToken) diff --git a/src/Concelier/__Tests/StellaOps.Concelier.Core.Tests/Observations/AdvisoryObservationAggregationTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.Core.Tests/Observations/AdvisoryObservationAggregationTests.cs index c7aea158b..85020b1b1 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.Core.Tests/Observations/AdvisoryObservationAggregationTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.Core.Tests/Observations/AdvisoryObservationAggregationTests.cs @@ -32,7 +32,9 @@ public sealed class AdvisoryObservationAggregationTests null, new object?[] { ImmutableArray.Create(observation) })!; - Assert.Equal(ImmutableArray.Create("os:debian", "pkg:npm/foo"), aggregate.Scopes); + Assert.Equal(2, aggregate.Scopes.Length); + Assert.Contains("os:debian", aggregate.Scopes); + Assert.Contains("pkg:npm/foo", aggregate.Scopes); Assert.Single(aggregate.Relationships); Assert.Equal("depends_on", aggregate.Relationships[0].Type); } diff --git a/src/Excititor/StellaOps.Excititor.WebService/Contracts/AttestationContracts.cs b/src/Excititor/StellaOps.Excititor.WebService/Contracts/AttestationContracts.cs new file mode 100644 index 000000000..c9277fad5 --- /dev/null +++ b/src/Excititor/StellaOps.Excititor.WebService/Contracts/AttestationContracts.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace StellaOps.Excititor.WebService.Contracts; + +public sealed record AttestationVerifyRequest +{ + public string ExportId { get; init; } = string.Empty; + public string QuerySignature { get; init; } = string.Empty; + public string ArtifactDigest { get; init; } = string.Empty; + public string Format { get; init; } = string.Empty; + public DateTimeOffset CreatedAt { get; init; } + = DateTimeOffset.UnixEpoch; + public IReadOnlyList SourceProviders { get; init; } + = Array.Empty(); + public IReadOnlyDictionary Metadata { get; init; } + = new Dictionary(StringComparer.Ordinal); + public AttestationVerifyMetadata Attestation { get; init; } + = new(); + public string Envelope { get; init; } = string.Empty; + public bool IsReverify { get; init; } + = false; +} + +public sealed record AttestationVerifyMetadata +{ + public string PredicateType { get; init; } = string.Empty; + public string EnvelopeDigest { get; init; } = string.Empty; + public DateTimeOffset SignedAt { get; init; } = DateTimeOffset.UnixEpoch; + public AttestationRekorReference? Rekor { get; init; } + = null; +} + +public sealed record AttestationRekorReference +{ + public string? ApiVersion { get; init; } + = null; + public string? Location { get; init; } + = null; + public long? LogIndex { get; init; } + = null; + public Uri? InclusionProofUrl { get; init; } + = null; +} + +public sealed record AttestationVerifyResponse(bool Valid, IDictionary Diagnostics); diff --git a/src/Excititor/StellaOps.Excititor.WebService/Program.cs b/src/Excititor/StellaOps.Excititor.WebService/Program.cs index eb1e8d1b6..787e8d470 100644 --- a/src/Excititor/StellaOps.Excititor.WebService/Program.cs +++ b/src/Excititor/StellaOps.Excititor.WebService/Program.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Collections.Immutable; using System.Globalization; +using System.Diagnostics; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Authentication; @@ -13,16 +14,16 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using StellaOps.Excititor.Attestation.Verification; -using StellaOps.Excititor.Attestation.Extensions; -using StellaOps.Excititor.Attestation; -using StellaOps.Excititor.Attestation.Transparency; -using StellaOps.Excititor.ArtifactStores.S3.Extensions; -using StellaOps.Excititor.Connectors.RedHat.CSAF.DependencyInjection; -using StellaOps.Excititor.Core; -using StellaOps.Excititor.Export; -using StellaOps.Excititor.Formats.CSAF; -using StellaOps.Excititor.Formats.CycloneDX; -using StellaOps.Excititor.Formats.OpenVEX; +using StellaOps.Excititor.Attestation.Extensions; +using StellaOps.Excititor.Attestation; +using StellaOps.Excititor.Attestation.Transparency; +using StellaOps.Excititor.ArtifactStores.S3.Extensions; +using StellaOps.Excititor.Connectors.RedHat.CSAF.DependencyInjection; +using StellaOps.Excititor.Core; +using StellaOps.Excititor.Export; +using StellaOps.Excititor.Formats.CSAF; +using StellaOps.Excititor.Formats.CycloneDX; +using StellaOps.Excititor.Formats.OpenVEX; using StellaOps.Excititor.Policy; using StellaOps.Excititor.Storage.Mongo; using StellaOps.Excititor.WebService.Endpoints; @@ -34,14 +35,14 @@ using StellaOps.Excititor.WebService.Contracts; using StellaOps.Excititor.WebService.Telemetry; using MongoDB.Driver; using MongoDB.Bson; - + var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; var services = builder.Services; -services.AddOptions() - .Bind(configuration.GetSection("Excititor:Storage:Mongo")) - .ValidateOnStart(); - +services.AddOptions() + .Bind(configuration.GetSection("Excititor:Storage:Mongo")) + .ValidateOnStart(); + services.AddExcititorMongoStorage(); services.AddCsafNormalizer(); services.AddCycloneDxNormalizer(); @@ -60,6 +61,8 @@ services.Configure(configuration.GetSection("Exciti services.Configure(configuration.GetSection("Excititor:Attestation:Verification")); services.AddVexPolicy(); services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); services.AddRedHatCsafConnector(); services.Configure(configuration.GetSection(MirrorDistributionOptions.SectionName)); services.AddSingleton(); @@ -67,47 +70,47 @@ services.TryAddSingleton(TimeProvider.System); services.AddSingleton(); services.AddScoped(); services.AddScoped(); - -var rekorSection = configuration.GetSection("Excititor:Attestation:Rekor"); -if (rekorSection.Exists()) -{ - services.AddVexRekorClient(opts => rekorSection.Bind(opts)); -} - -var fileSystemSection = configuration.GetSection("Excititor:Artifacts:FileSystem"); -if (fileSystemSection.Exists()) -{ - services.AddVexFileSystemArtifactStore(opts => fileSystemSection.Bind(opts)); -} -else -{ - services.AddVexFileSystemArtifactStore(_ => { }); -} - -var s3Section = configuration.GetSection("Excititor:Artifacts:S3"); -if (s3Section.Exists()) -{ - services.AddVexS3ArtifactClient(opts => s3Section.GetSection("Client").Bind(opts)); - services.AddSingleton(provider => - { - var options = new S3ArtifactStoreOptions(); - s3Section.GetSection("Store").Bind(options); - return new S3ArtifactStore( - provider.GetRequiredService(), - Microsoft.Extensions.Options.Options.Create(options), - provider.GetRequiredService>()); - }); -} - -var offlineSection = configuration.GetSection("Excititor:Artifacts:OfflineBundle"); -if (offlineSection.Exists()) -{ - services.AddVexOfflineBundleArtifactStore(opts => offlineSection.Bind(opts)); -} - -services.AddEndpointsApiExplorer(); -services.AddHealthChecks(); -services.AddSingleton(TimeProvider.System); + +var rekorSection = configuration.GetSection("Excititor:Attestation:Rekor"); +if (rekorSection.Exists()) +{ + services.AddVexRekorClient(opts => rekorSection.Bind(opts)); +} + +var fileSystemSection = configuration.GetSection("Excititor:Artifacts:FileSystem"); +if (fileSystemSection.Exists()) +{ + services.AddVexFileSystemArtifactStore(opts => fileSystemSection.Bind(opts)); +} +else +{ + services.AddVexFileSystemArtifactStore(_ => { }); +} + +var s3Section = configuration.GetSection("Excititor:Artifacts:S3"); +if (s3Section.Exists()) +{ + services.AddVexS3ArtifactClient(opts => s3Section.GetSection("Client").Bind(opts)); + services.AddSingleton(provider => + { + var options = new S3ArtifactStoreOptions(); + s3Section.GetSection("Store").Bind(options); + return new S3ArtifactStore( + provider.GetRequiredService(), + Microsoft.Extensions.Options.Options.Create(options), + provider.GetRequiredService>()); + }); +} + +var offlineSection = configuration.GetSection("Excititor:Artifacts:OfflineBundle"); +if (offlineSection.Exists()) +{ + services.AddVexOfflineBundleArtifactStore(opts => offlineSection.Bind(opts)); +} + +services.AddEndpointsApiExplorer(); +services.AddHealthChecks(); +services.AddSingleton(TimeProvider.System); services.AddMemoryCache(); services.AddAuthentication(); services.AddAuthorization(); @@ -115,70 +118,134 @@ services.AddAuthorization(); builder.ConfigureExcititorTelemetry(); var app = builder.Build(); - + app.UseAuthentication(); app.UseAuthorization(); app.UseObservabilityHeaders(); - -app.MapGet("/excititor/status", async (HttpContext context, - IEnumerable artifactStores, - IOptions mongoOptions, - TimeProvider timeProvider) => -{ - var payload = new StatusResponse( - timeProvider.GetUtcNow(), - mongoOptions.Value.RawBucketName, - mongoOptions.Value.GridFsInlineThresholdBytes, - artifactStores.Select(store => store.GetType().Name).ToArray()); - - context.Response.ContentType = "application/json"; - await System.Text.Json.JsonSerializer.SerializeAsync(context.Response.Body, payload); -}); - -app.MapHealthChecks("/excititor/health"); - -app.MapPost("/excititor/statements", async ( - VexStatementIngestRequest request, - IVexClaimStore claimStore, - TimeProvider timeProvider, - CancellationToken cancellationToken) => -{ - if (request?.Statements is null || request.Statements.Count == 0) - { - return Results.BadRequest("At least one statement must be provided."); - } - - var claims = request.Statements.Select(statement => statement.ToDomainClaim()); - await claimStore.AppendAsync(claims, timeProvider.GetUtcNow(), cancellationToken).ConfigureAwait(false); - return Results.Accepted(); -}); - -app.MapGet("/excititor/statements/{vulnerabilityId}/{productKey}", async ( - string vulnerabilityId, - string productKey, - DateTimeOffset? since, - IVexClaimStore claimStore, - CancellationToken cancellationToken) => -{ - if (string.IsNullOrWhiteSpace(vulnerabilityId) || string.IsNullOrWhiteSpace(productKey)) - { - return Results.BadRequest("vulnerabilityId and productKey are required."); - } - - var claims = await claimStore.FindAsync(vulnerabilityId.Trim(), productKey.Trim(), since, cancellationToken).ConfigureAwait(false); - return Results.Ok(claims); -}); - + +app.MapGet("/excititor/status", async (HttpContext context, + IEnumerable artifactStores, + IOptions mongoOptions, + TimeProvider timeProvider) => +{ + var payload = new StatusResponse( + timeProvider.GetUtcNow(), + mongoOptions.Value.RawBucketName, + mongoOptions.Value.GridFsInlineThresholdBytes, + artifactStores.Select(store => store.GetType().Name).ToArray()); + + context.Response.ContentType = "application/json"; + await System.Text.Json.JsonSerializer.SerializeAsync(context.Response.Body, payload); +}); + +app.MapHealthChecks("/excititor/health"); + +app.MapPost("/v1/attestations/verify", async ( + [FromServices] IVexAttestationClient attestationClient, + [FromBody] AttestationVerifyRequest request, + CancellationToken cancellationToken) => +{ + if (request is null) + { + return Results.BadRequest("Request body is required."); + } + + if (string.IsNullOrWhiteSpace(request.ExportId) || + string.IsNullOrWhiteSpace(request.QuerySignature) || + string.IsNullOrWhiteSpace(request.ArtifactDigest) || + string.IsNullOrWhiteSpace(request.Format) || + string.IsNullOrWhiteSpace(request.Envelope) || + string.IsNullOrWhiteSpace(request.Attestation?.EnvelopeDigest)) + { + return Results.BadRequest("Missing required fields."); + } + + if (!Enum.TryParse(request.Format, ignoreCase: true, out var format)) + { + return Results.BadRequest("Unknown export format."); + } + + var attestationRequest = new VexAttestationRequest( + request.ExportId.Trim(), + new VexQuerySignature(request.QuerySignature.Trim()), + new VexContentAddress(request.ArtifactDigest.Trim()), + format, + request.CreatedAt, + request.SourceProviders?.ToImmutableArray() ?? ImmutableArray.Empty, + request.Metadata?.ToImmutableDictionary(StringComparer.Ordinal) ?? ImmutableDictionary.Empty); + + var rekor = request.Attestation?.Rekor is null + ? null + : new VexRekorReference( + request.Attestation.Rekor.ApiVersion ?? "0.2", + request.Attestation.Rekor.Location, + request.Attestation.Rekor.LogIndex, + request.Attestation.Rekor.InclusionProofUrl); + + var attestationMetadata = new VexAttestationMetadata( + request.Attestation?.PredicateType ?? string.Empty, + rekor, + request.Attestation!.EnvelopeDigest, + request.Attestation.SignedAt); + + var verificationRequest = new VexAttestationVerificationRequest( + attestationRequest, + attestationMetadata, + request.Envelope, + request.IsReverify); + + var verification = await attestationClient.VerifyAsync(verificationRequest, cancellationToken).ConfigureAwait(false); + + var response = new AttestationVerifyResponse( + verification.IsValid, + new Dictionary(verification.Diagnostics, StringComparer.Ordinal)); + + return Results.Ok(response); +}); + +app.MapPost("/excititor/statements" +, async ( + VexStatementIngestRequest request, + IVexClaimStore claimStore, + TimeProvider timeProvider, + CancellationToken cancellationToken) => +{ + if (request?.Statements is null || request.Statements.Count == 0) + { + return Results.BadRequest("At least one statement must be provided."); + } + + var claims = request.Statements.Select(statement => statement.ToDomainClaim()); + await claimStore.AppendAsync(claims, timeProvider.GetUtcNow(), cancellationToken).ConfigureAwait(false); + return Results.Accepted(); +}); + +app.MapGet("/excititor/statements/{vulnerabilityId}/{productKey}", async ( + string vulnerabilityId, + string productKey, + DateTimeOffset? since, + IVexClaimStore claimStore, + CancellationToken cancellationToken) => +{ + if (string.IsNullOrWhiteSpace(vulnerabilityId) || string.IsNullOrWhiteSpace(productKey)) + { + return Results.BadRequest("vulnerabilityId and productKey are required."); + } + + var claims = await claimStore.FindAsync(vulnerabilityId.Trim(), productKey.Trim(), since, cancellationToken).ConfigureAwait(false); + return Results.Ok(claims); +}); + app.MapPost("/excititor/admin/backfill-statements", async ( VexStatementBackfillRequest? request, VexStatementBackfillService backfillService, CancellationToken cancellationToken) => { - request ??= new VexStatementBackfillRequest(); - var result = await backfillService.RunAsync(request, cancellationToken).ConfigureAwait(false); - var message = FormattableString.Invariant( - $"Backfill completed: evaluated {result.DocumentsEvaluated}, backfilled {result.DocumentsBackfilled}, claims written {result.ClaimsWritten}, skipped {result.SkippedExisting}, failures {result.NormalizationFailures}."); - + request ??= new VexStatementBackfillRequest(); + var result = await backfillService.RunAsync(request, cancellationToken).ConfigureAwait(false); + var message = FormattableString.Invariant( + $"Backfill completed: evaluated {result.DocumentsEvaluated}, backfilled {result.DocumentsBackfilled}, claims written {result.ClaimsWritten}, skipped {result.SkippedExisting}, failures {result.NormalizationFailures}."); + return Results.Ok(new { message, @@ -742,17 +809,23 @@ app.MapGet("/v1/vex/evidence/chunks", async ( HttpContext context, [FromServices] IVexEvidenceChunkService chunkService, [FromServices] IOptions storageOptions, + [FromServices] ChunkTelemetry chunkTelemetry, [FromServices] ILogger logger, + [FromServices] TimeProvider timeProvider, CancellationToken cancellationToken) => { + var start = Stopwatch.GetTimestamp(); + var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read"); if (scopeResult is not null) { + chunkTelemetry.RecordIngested(null, null, "unauthorized", "missing-scope", 0, 0, 0); return scopeResult; } if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError)) { + chunkTelemetry.RecordIngested(tenant?.TenantId, null, "rejected", "tenant-invalid", 0, 0, Stopwatch.GetElapsedTime(start).TotalMilliseconds); return tenantError; } @@ -785,11 +858,13 @@ app.MapGet("/v1/vex/evidence/chunks", async ( catch (OperationCanceledException) { EvidenceTelemetry.RecordChunkOutcome(tenant, "cancelled"); + chunkTelemetry.RecordIngested(tenant?.TenantId, request.ProviderFilter.Count > 0 ? string.Join(',', request.ProviderFilter) : null, "cancelled", null, 0, 0, Stopwatch.GetElapsedTime(start).TotalMilliseconds); return Results.StatusCode(StatusCodes.Status499ClientClosedRequest); } catch { EvidenceTelemetry.RecordChunkOutcome(tenant, "error"); + chunkTelemetry.RecordIngested(tenant?.TenantId, request.ProviderFilter.Count > 0 ? string.Join(',', request.ProviderFilter) : null, "error", null, 0, 0, Stopwatch.GetElapsedTime(start).TotalMilliseconds); throw; } @@ -814,13 +889,25 @@ app.MapGet("/v1/vex/evidence/chunks", async ( context.Response.ContentType = "application/x-ndjson"; var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); + long payloadBytes = 0; foreach (var chunk in result.Chunks) { var line = JsonSerializer.Serialize(chunk, options); + payloadBytes += Encoding.UTF8.GetByteCount(line) + 1; await context.Response.WriteAsync(line, cancellationToken).ConfigureAwait(false); await context.Response.WriteAsync("\n", cancellationToken).ConfigureAwait(false); } + var elapsedMs = Stopwatch.GetElapsedTime(start).TotalMilliseconds; + chunkTelemetry.RecordIngested( + tenant?.TenantId, + request.ProviderFilter.Count > 0 ? string.Join(',', request.ProviderFilter) : null, + "success", + null, + result.TotalCount, + payloadBytes, + elapsedMs); + return Results.Empty; }); @@ -969,107 +1056,107 @@ app.MapGet("/obs/excititor/health", async ( IngestEndpoints.MapIngestEndpoints(app); ResolveEndpoint.MapResolveEndpoint(app); MirrorEndpoints.MapMirrorEndpoints(app); - -app.Run(); - -public partial class Program; - -internal sealed record StatusResponse(DateTimeOffset UtcNow, string MongoBucket, int InlineThreshold, string[] ArtifactStores); - -internal sealed record VexStatementIngestRequest(IReadOnlyList Statements); - -internal sealed record VexStatementEntry( - string VulnerabilityId, - string ProviderId, - string ProductKey, - string? ProductName, - string? ProductVersion, - string? ProductPurl, - string? ProductCpe, - IReadOnlyList? ComponentIdentifiers, - VexClaimStatus Status, - VexJustification? Justification, - string? Detail, - DateTimeOffset FirstSeen, - DateTimeOffset LastSeen, - VexDocumentFormat DocumentFormat, - string DocumentDigest, - string DocumentUri, - string? DocumentRevision, - VexSignatureMetadataRequest? Signature, - VexConfidenceRequest? Confidence, - VexSignalRequest? Signals, - IReadOnlyDictionary? Metadata) -{ - public VexClaim ToDomainClaim() - { - var product = new VexProduct( - ProductKey, - ProductName, - ProductVersion, - ProductPurl, - ProductCpe, - ComponentIdentifiers ?? Array.Empty()); - - if (!Uri.TryCreate(DocumentUri, UriKind.Absolute, out var uri)) - { - throw new InvalidOperationException($"DocumentUri '{DocumentUri}' is not a valid absolute URI."); - } - - var document = new VexClaimDocument( - DocumentFormat, - DocumentDigest, - uri, - DocumentRevision, - Signature?.ToDomain()); - - var additionalMetadata = Metadata is null - ? ImmutableDictionary.Empty - : Metadata.ToImmutableDictionary(StringComparer.Ordinal); - - return new VexClaim( - VulnerabilityId, - ProviderId, - product, - Status, - document, - FirstSeen, - LastSeen, - Justification, - Detail, - Confidence?.ToDomain(), - Signals?.ToDomain(), - additionalMetadata); - } -} - -internal sealed record VexSignatureMetadataRequest( - string Type, - string? Subject, - string? Issuer, - string? KeyId, - DateTimeOffset? VerifiedAt, - string? TransparencyLogReference) -{ - public VexSignatureMetadata ToDomain() - => new(Type, Subject, Issuer, KeyId, VerifiedAt, TransparencyLogReference); -} - -internal sealed record VexConfidenceRequest(string Level, double? Score, string? Method) -{ - public VexConfidence ToDomain() => new(Level, Score, Method); -} - -internal sealed record VexSignalRequest(VexSeveritySignalRequest? Severity, bool? Kev, double? Epss) -{ - public VexSignalSnapshot ToDomain() - => new(Severity?.ToDomain(), Kev, Epss); -} - -internal sealed record VexSeveritySignalRequest(string Scheme, double? Score, string? Label, string? Vector) -{ - public VexSeveritySignal ToDomain() => new(Scheme, Score, Label, Vector); -} + +app.Run(); + +public partial class Program; + +internal sealed record StatusResponse(DateTimeOffset UtcNow, string MongoBucket, int InlineThreshold, string[] ArtifactStores); + +internal sealed record VexStatementIngestRequest(IReadOnlyList Statements); + +internal sealed record VexStatementEntry( + string VulnerabilityId, + string ProviderId, + string ProductKey, + string? ProductName, + string? ProductVersion, + string? ProductPurl, + string? ProductCpe, + IReadOnlyList? ComponentIdentifiers, + VexClaimStatus Status, + VexJustification? Justification, + string? Detail, + DateTimeOffset FirstSeen, + DateTimeOffset LastSeen, + VexDocumentFormat DocumentFormat, + string DocumentDigest, + string DocumentUri, + string? DocumentRevision, + VexSignatureMetadataRequest? Signature, + VexConfidenceRequest? Confidence, + VexSignalRequest? Signals, + IReadOnlyDictionary? Metadata) +{ + public VexClaim ToDomainClaim() + { + var product = new VexProduct( + ProductKey, + ProductName, + ProductVersion, + ProductPurl, + ProductCpe, + ComponentIdentifiers ?? Array.Empty()); + + if (!Uri.TryCreate(DocumentUri, UriKind.Absolute, out var uri)) + { + throw new InvalidOperationException($"DocumentUri '{DocumentUri}' is not a valid absolute URI."); + } + + var document = new VexClaimDocument( + DocumentFormat, + DocumentDigest, + uri, + DocumentRevision, + Signature?.ToDomain()); + + var additionalMetadata = Metadata is null + ? ImmutableDictionary.Empty + : Metadata.ToImmutableDictionary(StringComparer.Ordinal); + + return new VexClaim( + VulnerabilityId, + ProviderId, + product, + Status, + document, + FirstSeen, + LastSeen, + Justification, + Detail, + Confidence?.ToDomain(), + Signals?.ToDomain(), + additionalMetadata); + } +} + +internal sealed record VexSignatureMetadataRequest( + string Type, + string? Subject, + string? Issuer, + string? KeyId, + DateTimeOffset? VerifiedAt, + string? TransparencyLogReference) +{ + public VexSignatureMetadata ToDomain() + => new(Type, Subject, Issuer, KeyId, VerifiedAt, TransparencyLogReference); +} + +internal sealed record VexConfidenceRequest(string Level, double? Score, string? Method) +{ + public VexConfidence ToDomain() => new(Level, Score, Method); +} + +internal sealed record VexSignalRequest(VexSeveritySignalRequest? Severity, bool? Kev, double? Epss) +{ + public VexSignalSnapshot ToDomain() + => new(Severity?.ToDomain(), Kev, Epss); +} + +internal sealed record VexSeveritySignalRequest(string Scheme, double? Score, string? Label, string? Vector) +{ + public VexSeveritySignal ToDomain() => new(Scheme, Score, Label, Vector); +} app.MapGet( "/v1/vex/observations", async ( diff --git a/src/Excititor/StellaOps.Excititor.WebService/Telemetry/ChunkTelemetry.cs b/src/Excititor/StellaOps.Excititor.WebService/Telemetry/ChunkTelemetry.cs new file mode 100644 index 000000000..ae346ff8c --- /dev/null +++ b/src/Excititor/StellaOps.Excititor.WebService/Telemetry/ChunkTelemetry.cs @@ -0,0 +1,51 @@ +using System.Diagnostics.Metrics; + +namespace StellaOps.Excititor.WebService.Telemetry; + +internal sealed class ChunkTelemetry +{ + private readonly Counter _ingestedTotal; + private readonly Histogram _itemCount; + private readonly Histogram _payloadBytes; + private readonly Histogram _latencyMs; + + public ChunkTelemetry(IMeterFactory meterFactory) + { + var meter = meterFactory.Create("StellaOps.Excititor.Chunks"); + _ingestedTotal = meter.CreateCounter( + name: "vex_chunks_ingested_total", + unit: "chunks", + description: "Chunks submitted to Excititor VEX ingestion."); + _itemCount = meter.CreateHistogram( + name: "vex_chunks_item_count", + unit: "items", + description: "Item count per submitted chunk."); + _payloadBytes = meter.CreateHistogram( + name: "vex_chunks_payload_bytes", + unit: "bytes", + description: "Payload size per submitted chunk."); + _latencyMs = meter.CreateHistogram( + name: "vex_chunks_latency_ms", + unit: "ms", + description: "End-to-end processing latency per chunk request."); + } + + public void RecordIngested(string? tenant, string? source, string status, string? reason, long itemCount, long payloadBytes, double latencyMs) + { + var tags = new TagList + { + { "tenant", tenant ?? "" }, + { "source", source ?? "" }, + { "status", status }, + }; + if (!string.IsNullOrWhiteSpace(reason)) + { + tags.Add("reason", reason); + } + + _ingestedTotal.Add(1, tags); + _itemCount.Record(itemCount, tags); + _payloadBytes.Record(payloadBytes, tags); + _latencyMs.Record(latencyMs, tags); + } +} diff --git a/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Abstractions/Trust/ConnectorSignerMetadata.cs b/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Abstractions/Trust/ConnectorSignerMetadata.cs new file mode 100644 index 000000000..fda9d7842 --- /dev/null +++ b/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Abstractions/Trust/ConnectorSignerMetadata.cs @@ -0,0 +1,204 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace StellaOps.Excititor.Connectors.Abstractions.Trust; + +public sealed record ConnectorSignerMetadataSet( + string SchemaVersion, + DateTimeOffset GeneratedAt, + ImmutableArray Connectors) +{ + private readonly ImmutableDictionary _byId = + Connectors.ToImmutableDictionary(x => x.ConnectorId, StringComparer.OrdinalIgnoreCase); + + public bool TryGet(string connectorId, [NotNullWhen(true)] out ConnectorSignerMetadata? metadata) + => _byId.TryGetValue(connectorId, out metadata); +} + +public sealed record ConnectorSignerMetadata( + string ConnectorId, + string ProviderName, + string ProviderSlug, + string IssuerTier, + ImmutableArray Signers, + ConnectorSignerBundleRef? Bundle, + string? ValidFrom, + string? ValidTo, + bool Revoked, + string? Notes); + +public sealed record ConnectorSignerSigner( + string Usage, + ImmutableArray Fingerprints, + string? KeyLocator, + ImmutableArray CertificateChain); + +public sealed record ConnectorSignerFingerprint( + string Alg, + string Format, + string Value); + +public sealed record ConnectorSignerBundleRef( + string Kind, + string Uri, + string? Digest, + DateTimeOffset? PublishedAt); + +public static class ConnectorSignerMetadataLoader +{ + public static ConnectorSignerMetadataSet? TryLoad(string? path, Stream? overrideStream = null) + { + if (string.IsNullOrWhiteSpace(path) && overrideStream is null) + { + return null; + } + + try + { + using var stream = overrideStream ?? File.OpenRead(path!); + var root = JsonNode.Parse(stream, new JsonNodeOptions { PropertyNameCaseInsensitive = true }); + if (root is null) + { + return null; + } + + var version = root["schemaVersion"]?.GetValue() ?? "0.0.0"; + var generatedAt = root["generatedAt"]?.GetValue() ?? DateTimeOffset.MinValue; + var connectorsNode = root["connectors"] as JsonArray; + if (connectorsNode is null || connectorsNode.Count == 0) + { + return null; + } + + var connectors = connectorsNode + .Select(ParseConnector) + .Where(c => c is not null) + .Select(c => c!) + .OrderBy(c => c.ConnectorId, StringComparer.Ordinal) + .ToImmutableArray(); + + return new ConnectorSignerMetadataSet(version, generatedAt, connectors); + } + catch + { + return null; + } + } + + private static ConnectorSignerMetadata? ParseConnector(JsonNode? node) + { + if (node is not JsonObject obj) + { + return null; + } + + var id = obj["connectorId"]?.GetValue(); + var providerName = obj["provider"]?["name"]?.GetValue(); + var providerSlug = obj["provider"]?["slug"]?.GetValue(); + var issuerTier = obj["issuerTier"]?.GetValue(); + var signers = (obj["signers"] as JsonArray)?.Select(ParseSigner) + .Where(x => x is not null) + .Select(x => x!) + .ToImmutableArray() ?? ImmutableArray.Empty; + + if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(providerName) || signers.Length == 0) + { + return null; + } + + return new ConnectorSignerMetadata( + id!, + providerName!, + providerSlug ?? providerName!, + issuerTier ?? "untrusted", + signers, + ParseBundle(obj["bundle"]), + obj["validFrom"]?.GetValue(), + obj["validTo"]?.GetValue(), + obj["revoked"]?.GetValue() ?? false, + obj["notes"]?.GetValue()); + } + + private static ConnectorSignerSigner? ParseSigner(JsonNode? node) + { + if (node is not JsonObject obj) + { + return null; + } + + var usage = obj["usage"]?.GetValue(); + var fps = (obj["fingerprints"] as JsonArray)?.Select(ParseFingerprint) + .Where(x => x is not null) + .Select(x => x!) + .ToImmutableArray() ?? ImmutableArray.Empty; + + if (string.IsNullOrWhiteSpace(usage) || fps.IsDefaultOrEmpty) + { + return null; + } + + var chain = (obj["certificateChain"] as JsonArray)?.Select(x => x?.GetValue()) + .Where(v => !string.IsNullOrWhiteSpace(v)) + .Select(v => v!) + .ToImmutableArray() ?? ImmutableArray.Empty; + + return new ConnectorSignerSigner( + usage!, + fps, + obj["keyLocator"]?.GetValue(), + chain); + } + + private static ConnectorSignerFingerprint? ParseFingerprint(JsonNode? node) + { + if (node is not JsonObject obj) + { + return null; + } + + var alg = obj["alg"]?.GetValue(); + var format = obj["format"]?.GetValue(); + var value = obj["value"]?.GetValue(); + + if (string.IsNullOrWhiteSpace(alg) || string.IsNullOrWhiteSpace(format) || string.IsNullOrWhiteSpace(value)) + { + return null; + } + + return new ConnectorSignerFingerprint(alg!, format!, value!); + } + + private static ConnectorSignerBundleRef? ParseBundle(JsonNode? node) + { + if (node is not JsonObject obj) + { + return null; + } + + var kind = obj["kind"]?.GetValue(); + var uri = obj["uri"]?.GetValue(); + if (string.IsNullOrWhiteSpace(kind) || string.IsNullOrWhiteSpace(uri)) + { + return null; + } + + DateTimeOffset? published = null; + if (obj["publishedAt"] is JsonNode publishedNode && publishedNode.GetValue() is { } publishedString) + { + if (DateTimeOffset.TryParse(publishedString, out var parsed)) + { + published = parsed; + } + } + + return new ConnectorSignerBundleRef( + kind!, + uri!, + obj["digest"]?.GetValue(), + published); + } +} diff --git a/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Abstractions/Trust/ConnectorSignerMetadataEnricher.cs b/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Abstractions/Trust/ConnectorSignerMetadataEnricher.cs new file mode 100644 index 000000000..1c9875100 --- /dev/null +++ b/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Abstractions/Trust/ConnectorSignerMetadataEnricher.cs @@ -0,0 +1,80 @@ +using System; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Logging; + +namespace StellaOps.Excititor.Connectors.Abstractions.Trust; + +public static class ConnectorSignerMetadataEnricher +{ + private static readonly object Sync = new(); + private static ConnectorSignerMetadataSet? _cached; + private static string? _cachedPath; + + private const string EnvVar = "STELLAOPS_CONNECTOR_SIGNER_METADATA_PATH"; + + public static void Enrich( + VexConnectorMetadataBuilder builder, + string connectorId, + ILogger? logger = null, + string? metadataPath = null) + { + ArgumentNullException.ThrowIfNull(builder); + if (string.IsNullOrWhiteSpace(connectorId)) return; + + var path = metadataPath ?? Environment.GetEnvironmentVariable(EnvVar); + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + + var metadata = LoadCached(path, logger); + if (metadata is null || !metadata.TryGet(connectorId, out var connector)) + { + return; + } + + builder + .Add("vex.provenance.trust.issuerTier", connector.IssuerTier) + .Add("vex.provenance.trust.signers", string.Join(';', connector.Signers.SelectMany(s => s.Fingerprints.Select(fp => fp.Value)))) + .Add("vex.provenance.trust.provider", connector.ProviderSlug); + + if (connector.Bundle is { } bundle) + { + builder + .Add("vex.provenance.bundle.kind", bundle.Kind) + .Add("vex.provenance.bundle.uri", bundle.Uri) + .Add("vex.provenance.bundle.digest", bundle.Digest) + .Add("vex.provenance.bundle.publishedAt", bundle.PublishedAt?.ToUniversalTime().ToString("O")); + } + } + + private static ConnectorSignerMetadataSet? LoadCached(string path, ILogger? logger) + { + if (!string.Equals(path, _cachedPath, StringComparison.OrdinalIgnoreCase) || _cached is null) + { + lock (Sync) + { + if (!string.Equals(path, _cachedPath, StringComparison.OrdinalIgnoreCase) || _cached is null) + { + if (!File.Exists(path)) + { + logger?.LogDebug("Connector signer metadata file not found at {Path}; skipping enrichment.", path); + _cached = null; + } + else + { + _cached = ConnectorSignerMetadataLoader.TryLoad(path); + _cachedPath = path; + if (_cached is null) + { + logger?.LogWarning("Failed to load connector signer metadata from {Path}.", path); + } + } + } + } + } + + return _cached; + } +} diff --git a/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.MSRC.CSAF/MsrcCsafConnector.cs b/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.MSRC.CSAF/MsrcCsafConnector.cs index 414f2999a..9fa991bf1 100644 --- a/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.MSRC.CSAF/MsrcCsafConnector.cs +++ b/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.MSRC.CSAF/MsrcCsafConnector.cs @@ -12,6 +12,7 @@ using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Excititor.Connectors.Abstractions; +using StellaOps.Excititor.Connectors.Abstractions.Trust; using StellaOps.Excititor.Connectors.MSRC.CSAF.Authentication; using StellaOps.Excititor.Connectors.MSRC.CSAF.Configuration; using StellaOps.Excititor.Core; @@ -276,6 +277,8 @@ public sealed class MsrcCsafConnector : VexConnectorBase { builder.Add("http.lastModified", lastModified.ToString("O")); } + + ConnectorSignerMetadataEnricher.Enrich(builder, Descriptor.Id, _logger); }); return CreateRawDocument(VexDocumentFormat.Csaf, documentUri, payload, metadata); diff --git a/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest/OciOpenVexAttestationConnector.cs b/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest/OciOpenVexAttestationConnector.cs index 9c331621b..82de9c29a 100644 --- a/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest/OciOpenVexAttestationConnector.cs +++ b/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest/OciOpenVexAttestationConnector.cs @@ -1,11 +1,13 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Runtime.CompilerServices; -using Microsoft.Extensions.Logging; -using StellaOps.Excititor.Connectors.Abstractions; -using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Configuration; -using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Discovery; -using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Fetch; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using System.Linq; +using Microsoft.Extensions.Logging; +using StellaOps.Excititor.Connectors.Abstractions; +using StellaOps.Excititor.Connectors.Abstractions.Trust; +using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Configuration; +using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Discovery; +using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Fetch; using StellaOps.Excititor.Core; namespace StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest; @@ -187,11 +189,11 @@ public sealed class OciOpenVexAttestationConnector : VexConnectorBase } } - if (signature is not null) - { - builder["vex.signature.type"] = signature.Type; - if (!string.IsNullOrWhiteSpace(signature.Subject)) - { + if (signature is not null) + { + builder["vex.signature.type"] = signature.Type; + if (!string.IsNullOrWhiteSpace(signature.Subject)) + { builder["vex.signature.subject"] = signature.Subject!; } @@ -211,11 +213,19 @@ public sealed class OciOpenVexAttestationConnector : VexConnectorBase } if (!string.IsNullOrWhiteSpace(signature.TransparencyLogReference)) - { - builder["vex.signature.transparencyLogReference"] = signature.TransparencyLogReference!; - } - } - - return builder.ToImmutable(); - } -} + { + builder["vex.signature.transparencyLogReference"] = signature.TransparencyLogReference!; + } + } + + var metadataBuilder = new VexConnectorMetadataBuilder(); + metadataBuilder.AddRange(builder.Select(kv => new KeyValuePair(kv.Key, kv.Value))); + ConnectorSignerMetadataEnricher.Enrich(metadataBuilder, Descriptor.Id, Logger); + foreach (var kv in metadataBuilder.Build()) + { + builder[kv.Key] = kv.Value; + } + + return builder.ToImmutable(); + } +} diff --git a/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Oracle.CSAF/OracleCsafConnector.cs b/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Oracle.CSAF/OracleCsafConnector.cs index c86c0a6ef..d46fca9f8 100644 --- a/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Oracle.CSAF/OracleCsafConnector.cs +++ b/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Oracle.CSAF/OracleCsafConnector.cs @@ -8,10 +8,11 @@ using System.Net.Http; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using StellaOps.Excititor.Connectors.Abstractions; -using StellaOps.Excititor.Connectors.Oracle.CSAF.Configuration; -using StellaOps.Excititor.Connectors.Oracle.CSAF.Metadata; +using Microsoft.Extensions.Logging; +using StellaOps.Excititor.Connectors.Abstractions; +using StellaOps.Excititor.Connectors.Abstractions.Trust; +using StellaOps.Excititor.Connectors.Oracle.CSAF.Configuration; +using StellaOps.Excititor.Connectors.Oracle.CSAF.Metadata; using StellaOps.Excititor.Core; using StellaOps.Excititor.Storage.Mongo; @@ -260,11 +261,13 @@ public sealed class OracleCsafConnector : VexConnectorBase builder.Add("oracle.csaf.sha256", NormalizeDigest(entry.Sha256)); builder.Add("oracle.csaf.size", entry.Size?.ToString(CultureInfo.InvariantCulture)); - if (!entry.Products.IsDefaultOrEmpty) - { - builder.Add("oracle.csaf.products", string.Join(",", entry.Products)); - } - }); + if (!entry.Products.IsDefaultOrEmpty) + { + builder.Add("oracle.csaf.products", string.Join(",", entry.Products)); + } + + ConnectorSignerMetadataEnricher.Enrich(builder, Descriptor.Id, _logger); + }); return CreateRawDocument(VexDocumentFormat.Csaf, entry.DocumentUri, payload.AsMemory(), metadata); } diff --git a/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Ubuntu.CSAF/UbuntuCsafConnector.cs b/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Ubuntu.CSAF/UbuntuCsafConnector.cs index 48dc0790d..55a137e0b 100644 --- a/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Ubuntu.CSAF/UbuntuCsafConnector.cs +++ b/src/Excititor/__Libraries/StellaOps.Excititor.Connectors.Ubuntu.CSAF/UbuntuCsafConnector.cs @@ -10,6 +10,7 @@ using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using StellaOps.Excititor.Connectors.Abstractions; +using StellaOps.Excititor.Connectors.Abstractions.Trust; using StellaOps.Excititor.Connectors.Ubuntu.CSAF.Configuration; using StellaOps.Excititor.Connectors.Ubuntu.CSAF.Metadata; using StellaOps.Excititor.Core; @@ -459,6 +460,8 @@ public sealed class UbuntuCsafConnector : VexConnectorBase builder .Add("vex.provenance.trust.tier", tier) .Add("vex.provenance.trust.note", $"tier={tier};weight={provider.Trust.Weight.ToString("0.###", CultureInfo.InvariantCulture)}"); + + ConnectorSignerMetadataEnricher.Enrich(builder, Descriptor.Id, _logger); } private static async ValueTask UpsertProviderAsync(IServiceProvider services, VexProvider provider, CancellationToken cancellationToken) diff --git a/src/Excititor/__Tests/StellaOps.Excititor.Connectors.MSRC.CSAF.Tests/Connectors/MsrcCsafConnectorTests.cs b/src/Excititor/__Tests/StellaOps.Excititor.Connectors.MSRC.CSAF.Tests/Connectors/MsrcCsafConnectorTests.cs index 376fa3976..57616114b 100644 --- a/src/Excititor/__Tests/StellaOps.Excititor.Connectors.MSRC.CSAF.Tests/Connectors/MsrcCsafConnectorTests.cs +++ b/src/Excititor/__Tests/StellaOps.Excititor.Connectors.MSRC.CSAF.Tests/Connectors/MsrcCsafConnectorTests.cs @@ -1,367 +1,464 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.IO.Compression; -using System.Net; -using System.Net.Http; -using System.Text; -using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using StellaOps.Excititor.Connectors.Abstractions; -using StellaOps.Excititor.Connectors.MSRC.CSAF; -using StellaOps.Excititor.Connectors.MSRC.CSAF.Authentication; -using StellaOps.Excititor.Connectors.MSRC.CSAF.Configuration; -using StellaOps.Excititor.Core; -using StellaOps.Excititor.Storage.Mongo; -using Xunit; -using MongoDB.Driver; - -namespace StellaOps.Excititor.Connectors.MSRC.CSAF.Tests.Connectors; - -public sealed class MsrcCsafConnectorTests -{ - private static readonly VexConnectorDescriptor Descriptor = new("excititor:msrc", VexProviderKind.Vendor, "MSRC CSAF"); - - [Fact] - public async Task FetchAsync_EmitsDocumentAndPersistsState() - { - var summary = """ - { - "value": [ - { - "id": "ADV-0001", - "vulnerabilityId": "ADV-0001", - "severity": "Critical", - "releaseDate": "2025-10-17T00:00:00Z", - "lastModifiedDate": "2025-10-18T00:00:00Z", - "cvrfUrl": "https://example.com/csaf/ADV-0001.json" - } - ] - } - """; - - var csaf = """{"document":{"title":"Example"}}"""; - var handler = TestHttpMessageHandler.Create( - _ => Response(HttpStatusCode.OK, summary, "application/json"), - _ => Response(HttpStatusCode.OK, csaf, "application/json")); - - var httpClient = new HttpClient(handler) - { - BaseAddress = new Uri("https://example.com/"), - }; - - var factory = new SingleClientHttpClientFactory(httpClient); - var stateRepository = new InMemoryConnectorStateRepository(); - var options = Options.Create(CreateOptions()); - var connector = new MsrcCsafConnector( - factory, - new StubTokenProvider(), - stateRepository, - options, - NullLogger.Instance, - TimeProvider.System); - - await connector.ValidateAsync(VexConnectorSettings.Empty, CancellationToken.None); - - var sink = new CapturingRawSink(); - var context = new VexConnectorContext( - Since: new DateTimeOffset(2025, 10, 15, 0, 0, 0, TimeSpan.Zero), - Settings: VexConnectorSettings.Empty, - RawSink: sink, - SignatureVerifier: new NoopSignatureVerifier(), - Normalizers: new NoopNormalizerRouter(), - Services: new ServiceCollection().BuildServiceProvider(), - ResumeTokens: ImmutableDictionary.Empty); - - var documents = new List(); - await foreach (var document in connector.FetchAsync(context, CancellationToken.None)) - { - documents.Add(document); - } - - documents.Should().HaveCount(1); - sink.Documents.Should().HaveCount(1); - var emitted = documents[0]; - emitted.SourceUri.Should().Be(new Uri("https://example.com/csaf/ADV-0001.json")); - emitted.Metadata["msrc.vulnerabilityId"].Should().Be("ADV-0001"); - emitted.Metadata["msrc.csaf.format"].Should().Be("json"); - emitted.Metadata.Should().NotContainKey("excititor.quarantine.reason"); - - stateRepository.State.Should().NotBeNull(); - stateRepository.State!.LastUpdated.Should().Be(new DateTimeOffset(2025, 10, 18, 0, 0, 0, TimeSpan.Zero)); - stateRepository.State.DocumentDigests.Should().HaveCount(1); - } - - [Fact] - public async Task FetchAsync_SkipsDocumentsWithExistingDigest() - { - var summary = """ - { - "value": [ - { - "id": "ADV-0001", - "vulnerabilityId": "ADV-0001", - "lastModifiedDate": "2025-10-18T00:00:00Z", - "cvrfUrl": "https://example.com/csaf/ADV-0001.json" - } - ] - } - """; - - var csaf = """{"document":{"title":"Example"}}"""; - var handler = TestHttpMessageHandler.Create( - _ => Response(HttpStatusCode.OK, summary, "application/json"), - _ => Response(HttpStatusCode.OK, csaf, "application/json")); - - var httpClient = new HttpClient(handler) - { - BaseAddress = new Uri("https://example.com/"), - }; - - var factory = new SingleClientHttpClientFactory(httpClient); - var stateRepository = new InMemoryConnectorStateRepository(); - var options = Options.Create(CreateOptions()); - var connector = new MsrcCsafConnector( - factory, - new StubTokenProvider(), - stateRepository, - options, - NullLogger.Instance, - TimeProvider.System); - - await connector.ValidateAsync(VexConnectorSettings.Empty, CancellationToken.None); - - var sink = new CapturingRawSink(); - var context = new VexConnectorContext( - Since: new DateTimeOffset(2025, 10, 15, 0, 0, 0, TimeSpan.Zero), - Settings: VexConnectorSettings.Empty, - RawSink: sink, - SignatureVerifier: new NoopSignatureVerifier(), - Normalizers: new NoopNormalizerRouter(), - Services: new ServiceCollection().BuildServiceProvider(), - ResumeTokens: ImmutableDictionary.Empty); - - var firstPass = new List(); - await foreach (var document in connector.FetchAsync(context, CancellationToken.None)) - { - firstPass.Add(document); - } - - firstPass.Should().HaveCount(1); - stateRepository.State.Should().NotBeNull(); - var persistedState = stateRepository.State!; - - handler.Reset( - _ => Response(HttpStatusCode.OK, summary, "application/json"), - _ => Response(HttpStatusCode.OK, csaf, "application/json")); - - sink.Documents.Clear(); - var secondPass = new List(); - await foreach (var document in connector.FetchAsync(context, CancellationToken.None)) - { - secondPass.Add(document); - } - - secondPass.Should().BeEmpty(); - sink.Documents.Should().BeEmpty(); - stateRepository.State.Should().NotBeNull(); - stateRepository.State!.DocumentDigests.Should().Equal(persistedState.DocumentDigests); - } - - [Fact] - public async Task FetchAsync_QuarantinesInvalidCsafPayload() - { - var summary = """ - { - "value": [ - { - "id": "ADV-0002", - "vulnerabilityId": "ADV-0002", - "lastModifiedDate": "2025-10-19T00:00:00Z", - "cvrfUrl": "https://example.com/csaf/ADV-0002.zip" - } - ] - } - """; - - var csafZip = CreateZip("document.json", "{ invalid json "); - var handler = TestHttpMessageHandler.Create( - _ => Response(HttpStatusCode.OK, summary, "application/json"), - _ => Response(HttpStatusCode.OK, csafZip, "application/zip")); - - var httpClient = new HttpClient(handler) - { - BaseAddress = new Uri("https://example.com/"), - }; - - var factory = new SingleClientHttpClientFactory(httpClient); - var stateRepository = new InMemoryConnectorStateRepository(); - var options = Options.Create(CreateOptions()); - var connector = new MsrcCsafConnector( - factory, - new StubTokenProvider(), - stateRepository, - options, - NullLogger.Instance, - TimeProvider.System); - - await connector.ValidateAsync(VexConnectorSettings.Empty, CancellationToken.None); - - var sink = new CapturingRawSink(); - var context = new VexConnectorContext( - Since: new DateTimeOffset(2025, 10, 17, 0, 0, 0, TimeSpan.Zero), - Settings: VexConnectorSettings.Empty, - RawSink: sink, - SignatureVerifier: new NoopSignatureVerifier(), - Normalizers: new NoopNormalizerRouter(), - Services: new ServiceCollection().BuildServiceProvider(), - ResumeTokens: ImmutableDictionary.Empty); - - var documents = new List(); - await foreach (var document in connector.FetchAsync(context, CancellationToken.None)) - { - documents.Add(document); - } - - documents.Should().BeEmpty(); - sink.Documents.Should().HaveCount(1); - sink.Documents[0].Metadata["excititor.quarantine.reason"].Should().Contain("JSON parse failed"); - sink.Documents[0].Metadata["msrc.csaf.format"].Should().Be("zip"); - - stateRepository.State.Should().NotBeNull(); - stateRepository.State!.DocumentDigests.Should().HaveCount(1); - } - - private static HttpResponseMessage Response(HttpStatusCode statusCode, string content, string contentType) - => new(statusCode) - { - Content = new StringContent(content, Encoding.UTF8, contentType), - }; - - private static HttpResponseMessage Response(HttpStatusCode statusCode, byte[] content, string contentType) - { - var response = new HttpResponseMessage(statusCode); - response.Content = new ByteArrayContent(content); - response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType); - return response; - } - - private static MsrcConnectorOptions CreateOptions() - => new() - { - BaseUri = new Uri("https://example.com/", UriKind.Absolute), - TenantId = Guid.NewGuid().ToString(), - ClientId = "client-id", - ClientSecret = "secret", - Scope = MsrcConnectorOptions.DefaultScope, - PageSize = 5, - MaxAdvisoriesPerFetch = 5, - RequestDelay = TimeSpan.Zero, - RetryBaseDelay = TimeSpan.FromMilliseconds(10), - MaxRetryAttempts = 2, - }; - - private static byte[] CreateZip(string entryName, string content) - { - using var buffer = new MemoryStream(); - using (var archive = new ZipArchive(buffer, ZipArchiveMode.Create, leaveOpen: true)) - { - var entry = archive.CreateEntry(entryName); - using var writer = new StreamWriter(entry.Open(), Encoding.UTF8); - writer.Write(content); - } - - return buffer.ToArray(); - } - - private sealed class StubTokenProvider : IMsrcTokenProvider - { - public ValueTask GetAccessTokenAsync(CancellationToken cancellationToken) - => ValueTask.FromResult(new MsrcAccessToken("token", "Bearer", DateTimeOffset.MaxValue)); - } - - private sealed class CapturingRawSink : IVexRawDocumentSink - { - public List Documents { get; } = new(); - - public ValueTask StoreAsync(VexRawDocument document, CancellationToken cancellationToken) - { - Documents.Add(document); - return ValueTask.CompletedTask; - } - } - - private sealed class NoopSignatureVerifier : IVexSignatureVerifier - { - public ValueTask VerifyAsync(VexRawDocument document, CancellationToken cancellationToken) - => ValueTask.FromResult(null); - } - - private sealed class NoopNormalizerRouter : IVexNormalizerRouter - { - public ValueTask NormalizeAsync(VexRawDocument document, CancellationToken cancellationToken) - => ValueTask.FromResult(new VexClaimBatch(document, ImmutableArray.Empty, ImmutableDictionary.Empty)); - } - - private sealed class SingleClientHttpClientFactory : IHttpClientFactory - { - private readonly HttpClient _client; - - public SingleClientHttpClientFactory(HttpClient client) - { - _client = client; - } - - public HttpClient CreateClient(string name) => _client; - } - - private sealed class InMemoryConnectorStateRepository : IVexConnectorStateRepository - { - public VexConnectorState? State { get; private set; } - - public ValueTask GetAsync(string connectorId, CancellationToken cancellationToken, IClientSessionHandle? session = null) - => ValueTask.FromResult(State); - - public ValueTask SaveAsync(VexConnectorState state, CancellationToken cancellationToken, IClientSessionHandle? session = null) - { - State = state; - return ValueTask.CompletedTask; - } - } - - private sealed class TestHttpMessageHandler : HttpMessageHandler - { - private readonly Queue> _responders; - - private TestHttpMessageHandler(IEnumerable> responders) - { - _responders = new Queue>(responders); - } - - public static TestHttpMessageHandler Create(params Func[] responders) - => new(responders); - - public void Reset(params Func[] responders) - { - _responders.Clear(); - foreach (var responder in responders) - { - _responders.Enqueue(responder); - } - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - if (_responders.Count == 0) - { - throw new InvalidOperationException("No responder configured for MSRC connector test request."); - } - - var responder = _responders.Count > 1 ? _responders.Dequeue() : _responders.Peek(); - var response = responder(request); - response.RequestMessage = request; - return Task.FromResult(response); - } - } -} +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using StellaOps.Excititor.Connectors.Abstractions; +using StellaOps.Excititor.Connectors.MSRC.CSAF; +using StellaOps.Excititor.Connectors.MSRC.CSAF.Authentication; +using StellaOps.Excititor.Connectors.MSRC.CSAF.Configuration; +using StellaOps.Excititor.Core; +using StellaOps.Excititor.Storage.Mongo; +using Xunit; +using MongoDB.Driver; + +namespace StellaOps.Excititor.Connectors.MSRC.CSAF.Tests.Connectors; + +public sealed class MsrcCsafConnectorTests +{ + private static readonly VexConnectorDescriptor Descriptor = new("excititor:msrc", VexProviderKind.Vendor, "MSRC CSAF"); + + [Fact] + public async Task FetchAsync_EmitsDocumentAndPersistsState() + { + var summary = """ + { + "value": [ + { + "id": "ADV-0001", + "vulnerabilityId": "ADV-0001", + "severity": "Critical", + "releaseDate": "2025-10-17T00:00:00Z", + "lastModifiedDate": "2025-10-18T00:00:00Z", + "cvrfUrl": "https://example.com/csaf/ADV-0001.json" + } + ] + } + """; + + var csaf = """{"document":{"title":"Example"}}"""; + var handler = TestHttpMessageHandler.Create( + _ => Response(HttpStatusCode.OK, summary, "application/json"), + _ => Response(HttpStatusCode.OK, csaf, "application/json")); + + var httpClient = new HttpClient(handler) + { + BaseAddress = new Uri("https://example.com/"), + }; + + var factory = new SingleClientHttpClientFactory(httpClient); + var stateRepository = new InMemoryConnectorStateRepository(); + var options = Options.Create(CreateOptions()); + var connector = new MsrcCsafConnector( + factory, + new StubTokenProvider(), + stateRepository, + options, + NullLogger.Instance, + TimeProvider.System); + + await connector.ValidateAsync(VexConnectorSettings.Empty, CancellationToken.None); + + var sink = new CapturingRawSink(); + var context = new VexConnectorContext( + Since: new DateTimeOffset(2025, 10, 15, 0, 0, 0, TimeSpan.Zero), + Settings: VexConnectorSettings.Empty, + RawSink: sink, + SignatureVerifier: new NoopSignatureVerifier(), + Normalizers: new NoopNormalizerRouter(), + Services: new ServiceCollection().BuildServiceProvider(), + ResumeTokens: ImmutableDictionary.Empty); + + var documents = new List(); + await foreach (var document in connector.FetchAsync(context, CancellationToken.None)) + { + documents.Add(document); + } + + documents.Should().HaveCount(1); + sink.Documents.Should().HaveCount(1); + var emitted = documents[0]; + emitted.SourceUri.Should().Be(new Uri("https://example.com/csaf/ADV-0001.json")); + emitted.Metadata["msrc.vulnerabilityId"].Should().Be("ADV-0001"); + emitted.Metadata["msrc.csaf.format"].Should().Be("json"); + emitted.Metadata.Should().NotContainKey("excititor.quarantine.reason"); + + stateRepository.State.Should().NotBeNull(); + stateRepository.State!.LastUpdated.Should().Be(new DateTimeOffset(2025, 10, 18, 0, 0, 0, TimeSpan.Zero)); + stateRepository.State.DocumentDigests.Should().HaveCount(1); + } + + [Fact] + public async Task FetchAsync_SkipsDocumentsWithExistingDigest() + { + var summary = """ + { + "value": [ + { + "id": "ADV-0001", + "vulnerabilityId": "ADV-0001", + "lastModifiedDate": "2025-10-18T00:00:00Z", + "cvrfUrl": "https://example.com/csaf/ADV-0001.json" + } + ] + } + """; + + var csaf = """{"document":{"title":"Example"}}"""; + var handler = TestHttpMessageHandler.Create( + _ => Response(HttpStatusCode.OK, summary, "application/json"), + _ => Response(HttpStatusCode.OK, csaf, "application/json")); + + var httpClient = new HttpClient(handler) + { + BaseAddress = new Uri("https://example.com/"), + }; + + var factory = new SingleClientHttpClientFactory(httpClient); + var stateRepository = new InMemoryConnectorStateRepository(); + var options = Options.Create(CreateOptions()); + var connector = new MsrcCsafConnector( + factory, + new StubTokenProvider(), + stateRepository, + options, + NullLogger.Instance, + TimeProvider.System); + + await connector.ValidateAsync(VexConnectorSettings.Empty, CancellationToken.None); + + var sink = new CapturingRawSink(); + var context = new VexConnectorContext( + Since: new DateTimeOffset(2025, 10, 15, 0, 0, 0, TimeSpan.Zero), + Settings: VexConnectorSettings.Empty, + RawSink: sink, + SignatureVerifier: new NoopSignatureVerifier(), + Normalizers: new NoopNormalizerRouter(), + Services: new ServiceCollection().BuildServiceProvider(), + ResumeTokens: ImmutableDictionary.Empty); + + var firstPass = new List(); + await foreach (var document in connector.FetchAsync(context, CancellationToken.None)) + { + firstPass.Add(document); + } + + firstPass.Should().HaveCount(1); + stateRepository.State.Should().NotBeNull(); + var persistedState = stateRepository.State!; + + handler.Reset( + _ => Response(HttpStatusCode.OK, summary, "application/json"), + _ => Response(HttpStatusCode.OK, csaf, "application/json")); + + sink.Documents.Clear(); + var secondPass = new List(); + await foreach (var document in connector.FetchAsync(context, CancellationToken.None)) + { + secondPass.Add(document); + } + + secondPass.Should().BeEmpty(); + sink.Documents.Should().BeEmpty(); + stateRepository.State.Should().NotBeNull(); + stateRepository.State!.DocumentDigests.Should().Equal(persistedState.DocumentDigests); + } + + [Fact] + public async Task FetchAsync_QuarantinesInvalidCsafPayload() + { + var summary = """ + { + "value": [ + { + "id": "ADV-0002", + "vulnerabilityId": "ADV-0002", + "lastModifiedDate": "2025-10-19T00:00:00Z", + "cvrfUrl": "https://example.com/csaf/ADV-0002.zip" + } + ] + } + """; + + var csafZip = CreateZip("document.json", "{ invalid json "); + var handler = TestHttpMessageHandler.Create( + _ => Response(HttpStatusCode.OK, summary, "application/json"), + _ => Response(HttpStatusCode.OK, csafZip, "application/zip")); + + var httpClient = new HttpClient(handler) + { + BaseAddress = new Uri("https://example.com/"), + }; + + var factory = new SingleClientHttpClientFactory(httpClient); + var stateRepository = new InMemoryConnectorStateRepository(); + var options = Options.Create(CreateOptions()); + var connector = new MsrcCsafConnector( + factory, + new StubTokenProvider(), + stateRepository, + options, + NullLogger.Instance, + TimeProvider.System); + + await connector.ValidateAsync(VexConnectorSettings.Empty, CancellationToken.None); + + var sink = new CapturingRawSink(); + var context = new VexConnectorContext( + Since: new DateTimeOffset(2025, 10, 17, 0, 0, 0, TimeSpan.Zero), + Settings: VexConnectorSettings.Empty, + RawSink: sink, + SignatureVerifier: new NoopSignatureVerifier(), + Normalizers: new NoopNormalizerRouter(), + Services: new ServiceCollection().BuildServiceProvider(), + ResumeTokens: ImmutableDictionary.Empty); + + var documents = new List(); + await foreach (var document in connector.FetchAsync(context, CancellationToken.None)) + { + documents.Add(document); + } + + documents.Should().BeEmpty(); + sink.Documents.Should().HaveCount(1); + sink.Documents[0].Metadata["excititor.quarantine.reason"].Should().Contain("JSON parse failed"); + sink.Documents[0].Metadata["msrc.csaf.format"].Should().Be("zip"); + + stateRepository.State.Should().NotBeNull(); + stateRepository.State!.DocumentDigests.Should().HaveCount(1); + } + + private static HttpResponseMessage Response(HttpStatusCode statusCode, string content, string contentType) + => new(statusCode) + { + Content = new StringContent(content, Encoding.UTF8, contentType), + }; + + private static HttpResponseMessage Response(HttpStatusCode statusCode, byte[] content, string contentType) + { + var response = new HttpResponseMessage(statusCode); + response.Content = new ByteArrayContent(content); + response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType); + return response; + } + + private static MsrcConnectorOptions CreateOptions() + => new() + { + BaseUri = new Uri("https://example.com/", UriKind.Absolute), + TenantId = Guid.NewGuid().ToString(), + ClientId = "client-id", + ClientSecret = "secret", + Scope = MsrcConnectorOptions.DefaultScope, + PageSize = 5, + MaxAdvisoriesPerFetch = 5, + RequestDelay = TimeSpan.Zero, + RetryBaseDelay = TimeSpan.FromMilliseconds(10), + MaxRetryAttempts = 2, + }; + + private static byte[] CreateZip(string entryName, string content) + { + using var buffer = new MemoryStream(); + using (var archive = new ZipArchive(buffer, ZipArchiveMode.Create, leaveOpen: true)) + { + var entry = archive.CreateEntry(entryName); + using var writer = new StreamWriter(entry.Open(), Encoding.UTF8); + writer.Write(content); + } + + return buffer.ToArray(); + } + + private sealed class StubTokenProvider : IMsrcTokenProvider + { + public ValueTask GetAccessTokenAsync(CancellationToken cancellationToken) + => ValueTask.FromResult(new MsrcAccessToken("token", "Bearer", DateTimeOffset.MaxValue)); + } + + private sealed class CapturingRawSink : IVexRawDocumentSink + { + public List Documents { get; } = new(); + + public ValueTask StoreAsync(VexRawDocument document, CancellationToken cancellationToken) + { + Documents.Add(document); + return ValueTask.CompletedTask; + } + } + + private sealed class NoopSignatureVerifier : IVexSignatureVerifier + { + public ValueTask VerifyAsync(VexRawDocument document, CancellationToken cancellationToken) + => ValueTask.FromResult(null); + } + + private sealed class NoopNormalizerRouter : IVexNormalizerRouter + { + public ValueTask NormalizeAsync(VexRawDocument document, CancellationToken cancellationToken) + => ValueTask.FromResult(new VexClaimBatch(document, ImmutableArray.Empty, ImmutableDictionary.Empty)); + } + + private sealed class SingleClientHttpClientFactory : IHttpClientFactory + { + private readonly HttpClient _client; + + public SingleClientHttpClientFactory(HttpClient client) + { + _client = client; + } + + public HttpClient CreateClient(string name) => _client; + } + + private sealed class InMemoryConnectorStateRepository : IVexConnectorStateRepository + { + public VexConnectorState? State { get; private set; } + + public ValueTask GetAsync(string connectorId, CancellationToken cancellationToken, IClientSessionHandle? session = null) + => ValueTask.FromResult(State); + + public ValueTask SaveAsync(VexConnectorState state, CancellationToken cancellationToken, IClientSessionHandle? session = null) + { + State = state; + return ValueTask.CompletedTask; + } + } + + + + [Fact] + public async Task FetchAsync_EnrichesSignerMetadataWhenConfigured() + { + using var tempMetadata = CreateTempSignerMetadata("excititor:msrc", "tier-1", "abc123"); + Environment.SetEnvironmentVariable("STELLAOPS_CONNECTOR_SIGNER_METADATA_PATH", tempMetadata.Path); + + try + { + var summary = """ + { + "value": [ + { "id": "ADV-0002", "vulnerabilityId": "ADV-0002", "cvrfUrl": "https://example.com/csaf/ADV-0002.json" } + ] + } + """; + + var csaf = """{"document":{"title":"Example"}}"""; + var handler = TestHttpMessageHandler.Create( + _ => Response(HttpStatusCode.OK, summary, "application/json"), + _ => Response(HttpStatusCode.OK, csaf, "application/json")); + + var httpClient = new HttpClient(handler) { BaseAddress = new Uri("https://example.com/"), }; + var factory = new SingleClientHttpClientFactory(httpClient); + var stateRepository = new InMemoryConnectorStateRepository(); + var connector = new MsrcCsafConnector( + factory, + new StubTokenProvider(), + stateRepository, + Options.Create(CreateOptions()), + NullLogger.Instance, + TimeProvider.System); + + await connector.ValidateAsync(VexConnectorSettings.Empty, CancellationToken.None); + + var sink = new CapturingRawSink(); + var context = new VexConnectorContext( + Since: null, + Settings: VexConnectorSettings.Empty, + RawSink: sink, + SignatureVerifier: new NoopSignatureVerifier(), + Normalizers: new NoopNormalizerRouter(), + Services: new ServiceCollection().BuildServiceProvider(), + ResumeTokens: ImmutableDictionary.Empty); + + await foreach (var _ in connector.FetchAsync(context, CancellationToken.None)) { } + + var emitted = sink.Documents.Single(); + emitted.Metadata.Should().Contain("vex.provenance.trust.issuerTier", "tier-1"); + emitted.Metadata.Should().ContainKey("vex.provenance.trust.signers"); + } + finally + { + Environment.SetEnvironmentVariable("STELLAOPS_CONNECTOR_SIGNER_METADATA_PATH", null); + } + } + + private static TempMetadataFile CreateTempSignerMetadata(string connectorId, string tier, string fingerprint) + { + var pathTemp = Path.GetTempFileName(); + var json = $""" + {{ + "schemaVersion": "1.0.0", + "generatedAt": "2025-11-20T00:00:00Z", + "connectors": [ + {{ + "connectorId": "{connectorId}", + "provider": {{ "name": "{connectorId}", "slug": "{connectorId}" }}, + "issuerTier": "{tier}", + "signers": [ + {{ + "usage": "csaf", + "fingerprints": [ + {{ "alg": "sha256", "format": "pgp", "value": "{fingerprint}" }} + ] + }} + ] + }} + ] + }} + """; + File.WriteAllText(pathTemp, json); + return new TempMetadataFile(pathTemp); + } + + private sealed record TempMetadataFile(string Path) : IDisposable + { + public void Dispose() + { + try { File.Delete(Path); } catch { } + } + } + + private sealed class TestHttpMessageHandler : HttpMessageHandler + { + private readonly Queue> _responders; + + private TestHttpMessageHandler(IEnumerable> responders) + { + _responders = new Queue>(responders); + } + + public static TestHttpMessageHandler Create(params Func[] responders) + => new(responders); + + public void Reset(params Func[] responders) + { + _responders.Clear(); + foreach (var responder in responders) + { + _responders.Enqueue(responder); + } + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (_responders.Count == 0) + { + throw new InvalidOperationException("No responder configured for MSRC connector test request."); + } + + var responder = _responders.Count > 1 ? _responders.Dequeue() : _responders.Peek(); + var response = responder(request); + response.RequestMessage = request; + return Task.FromResult(response); + } + } +} diff --git a/src/Excititor/__Tests/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Tests/Connector/OciOpenVexAttestationConnectorTests.cs b/src/Excititor/__Tests/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Tests/Connector/OciOpenVexAttestationConnectorTests.cs index a01c44a2d..f5cc586db 100644 --- a/src/Excititor/__Tests/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Tests/Connector/OciOpenVexAttestationConnectorTests.cs +++ b/src/Excititor/__Tests/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Tests/Connector/OciOpenVexAttestationConnectorTests.cs @@ -1,215 +1,313 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; -using StellaOps.Excititor.Connectors.Abstractions; -using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest; -using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Configuration; -using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.DependencyInjection; -using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Discovery; -using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Fetch; -using StellaOps.Excititor.Core; -using System.IO.Abstractions.TestingHelpers; -using Xunit; - -namespace StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Tests.Connector; - -public sealed class OciOpenVexAttestationConnectorTests -{ - [Fact] - public async Task FetchAsync_WithOfflineBundle_EmitsRawDocument() - { - var fileSystem = new MockFileSystem(new Dictionary - { - ["/bundles/attestation.json"] = new MockFileData("{\"payload\":\"\",\"payloadType\":\"application/vnd.in-toto+json\",\"signatures\":[{\"sig\":\"\"}]}"), - }); - - using var cache = new MemoryCache(new MemoryCacheOptions()); - var httpClient = new HttpClient(new StubHttpMessageHandler()) - { - BaseAddress = new System.Uri("https://registry.example.com/") - }; - - var httpFactory = new SingleClientHttpClientFactory(httpClient); - var discovery = new OciAttestationDiscoveryService(cache, fileSystem, NullLogger.Instance); - var fetcher = new OciAttestationFetcher(httpFactory, fileSystem, NullLogger.Instance); - - var connector = new OciOpenVexAttestationConnector( - discovery, - fetcher, - NullLogger.Instance, - TimeProvider.System); - - var settingsValues = ImmutableDictionary.Empty - .Add("Images:0:Reference", "registry.example.com/repo/image:latest") - .Add("Images:0:OfflineBundlePath", "/bundles/attestation.json") - .Add("Offline:PreferOffline", "true") - .Add("Offline:AllowNetworkFallback", "false") - .Add("Cosign:Mode", "None"); - - var settings = new VexConnectorSettings(settingsValues); - await connector.ValidateAsync(settings, CancellationToken.None); - - var sink = new CapturingRawSink(); - var verifier = new CapturingSignatureVerifier(); - var context = new VexConnectorContext( - Since: null, - Settings: VexConnectorSettings.Empty, - RawSink: sink, - SignatureVerifier: verifier, - Normalizers: new NoopNormalizerRouter(), - Services: new Microsoft.Extensions.DependencyInjection.ServiceCollection().BuildServiceProvider(), - ResumeTokens: ImmutableDictionary.Empty); - - var documents = new List(); - await foreach (var document in connector.FetchAsync(context, CancellationToken.None)) - { - documents.Add(document); - } - - documents.Should().HaveCount(1); - sink.Documents.Should().HaveCount(1); - documents[0].Format.Should().Be(VexDocumentFormat.OciAttestation); - documents[0].Metadata.Should().ContainKey("oci.attestation.sourceKind").WhoseValue.Should().Be("offline"); - documents[0].Metadata.Should().ContainKey("vex.provenance.sourceKind").WhoseValue.Should().Be("offline"); - documents[0].Metadata.Should().ContainKey("vex.provenance.registryAuthMode").WhoseValue.Should().Be("Anonymous"); - verifier.VerifyCalls.Should().Be(1); - } - - [Fact] - public async Task FetchAsync_WithSignatureMetadata_EnrichesProvenance() - { - var fileSystem = new MockFileSystem(new Dictionary - { - ["/bundles/attestation.json"] = new MockFileData("{\"payload\":\"\",\"payloadType\":\"application/vnd.in-toto+json\",\"signatures\":[{\"sig\":\"\"}]}"), - }); - - using var cache = new MemoryCache(new MemoryCacheOptions()); - var httpClient = new HttpClient(new StubHttpMessageHandler()) - { - BaseAddress = new System.Uri("https://registry.example.com/") - }; - - var httpFactory = new SingleClientHttpClientFactory(httpClient); - var discovery = new OciAttestationDiscoveryService(cache, fileSystem, NullLogger.Instance); - var fetcher = new OciAttestationFetcher(httpFactory, fileSystem, NullLogger.Instance); - - var connector = new OciOpenVexAttestationConnector( - discovery, - fetcher, - NullLogger.Instance, - TimeProvider.System); - - var settingsValues = ImmutableDictionary.Empty - .Add("Images:0:Reference", "registry.example.com/repo/image:latest") - .Add("Images:0:OfflineBundlePath", "/bundles/attestation.json") - .Add("Offline:PreferOffline", "true") - .Add("Offline:AllowNetworkFallback", "false") - .Add("Cosign:Mode", "Keyless") - .Add("Cosign:Keyless:Issuer", "https://issuer.example.com") - .Add("Cosign:Keyless:Subject", "subject@example.com"); - - var settings = new VexConnectorSettings(settingsValues); - await connector.ValidateAsync(settings, CancellationToken.None); - - var sink = new CapturingRawSink(); - var verifier = new CapturingSignatureVerifier - { - Result = new VexSignatureMetadata( - type: "cosign", - subject: "sig-subject", - issuer: "sig-issuer", - keyId: "key-id", - verifiedAt: DateTimeOffset.UtcNow, - transparencyLogReference: "rekor://entry/123") - }; - - var context = new VexConnectorContext( - Since: null, - Settings: VexConnectorSettings.Empty, - RawSink: sink, - SignatureVerifier: verifier, - Normalizers: new NoopNormalizerRouter(), - Services: new ServiceCollection().BuildServiceProvider(), - ResumeTokens: ImmutableDictionary.Empty); - - var documents = new List(); - await foreach (var document in connector.FetchAsync(context, CancellationToken.None)) - { - documents.Add(document); - } - - documents.Should().HaveCount(1); - var metadata = documents[0].Metadata; - metadata.Should().Contain("vex.signature.type", "cosign"); - metadata.Should().Contain("vex.signature.subject", "sig-subject"); - metadata.Should().Contain("vex.signature.issuer", "sig-issuer"); - metadata.Should().Contain("vex.signature.keyId", "key-id"); - metadata.Should().ContainKey("vex.signature.verifiedAt"); - metadata.Should().Contain("vex.signature.transparencyLogReference", "rekor://entry/123"); - metadata.Should().Contain("vex.provenance.cosign.mode", "Keyless"); - metadata.Should().Contain("vex.provenance.cosign.issuer", "https://issuer.example.com"); - metadata.Should().Contain("vex.provenance.cosign.subject", "subject@example.com"); - verifier.VerifyCalls.Should().Be(1); - } - - private sealed class CapturingRawSink : IVexRawDocumentSink - { - public List Documents { get; } = new(); - - public ValueTask StoreAsync(VexRawDocument document, CancellationToken cancellationToken) - { - Documents.Add(document); - return ValueTask.CompletedTask; - } - } - - private sealed class CapturingSignatureVerifier : IVexSignatureVerifier - { - public int VerifyCalls { get; private set; } - - public VexSignatureMetadata? Result { get; set; } - - public ValueTask VerifyAsync(VexRawDocument document, CancellationToken cancellationToken) - { - VerifyCalls++; - return ValueTask.FromResult(Result); - } - } - - private sealed class NoopNormalizerRouter : IVexNormalizerRouter - { - public ValueTask NormalizeAsync(VexRawDocument document, CancellationToken cancellationToken) - => ValueTask.FromResult(new VexClaimBatch(document, ImmutableArray.Empty, ImmutableDictionary.Empty)); - } - - private sealed class SingleClientHttpClientFactory : IHttpClientFactory - { - private readonly HttpClient _client; - - public SingleClientHttpClientFactory(HttpClient client) - { - _client = client; - } - - public HttpClient CreateClient(string name) => _client; - } - - private sealed class StubHttpMessageHandler : HttpMessageHandler - { - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) - { - RequestMessage = request - }); - } - } -} +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Excititor.Connectors.Abstractions; +using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest; +using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Configuration; +using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.DependencyInjection; +using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Discovery; +using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Fetch; +using StellaOps.Excititor.Core; +using System.IO.Abstractions.TestingHelpers; +using Xunit; + +namespace StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Tests.Connector; + +public sealed class OciOpenVexAttestationConnectorTests +{ + [Fact] + public async Task FetchAsync_WithOfflineBundle_EmitsRawDocument() + { + var fileSystem = new MockFileSystem(new Dictionary + { + ["/bundles/attestation.json"] = new MockFileData("{\"payload\":\"\",\"payloadType\":\"application/vnd.in-toto+json\",\"signatures\":[{\"sig\":\"\"}]}"), + }); + + using var cache = new MemoryCache(new MemoryCacheOptions()); + var httpClient = new HttpClient(new StubHttpMessageHandler()) + { + BaseAddress = new System.Uri("https://registry.example.com/") + }; + + var httpFactory = new SingleClientHttpClientFactory(httpClient); + var discovery = new OciAttestationDiscoveryService(cache, fileSystem, NullLogger.Instance); + var fetcher = new OciAttestationFetcher(httpFactory, fileSystem, NullLogger.Instance); + + var connector = new OciOpenVexAttestationConnector( + discovery, + fetcher, + NullLogger.Instance, + TimeProvider.System); + + var settingsValues = ImmutableDictionary.Empty + .Add("Images:0:Reference", "registry.example.com/repo/image:latest") + .Add("Images:0:OfflineBundlePath", "/bundles/attestation.json") + .Add("Offline:PreferOffline", "true") + .Add("Offline:AllowNetworkFallback", "false") + .Add("Cosign:Mode", "None"); + + var settings = new VexConnectorSettings(settingsValues); + await connector.ValidateAsync(settings, CancellationToken.None); + + var sink = new CapturingRawSink(); + var verifier = new CapturingSignatureVerifier(); + var context = new VexConnectorContext( + Since: null, + Settings: VexConnectorSettings.Empty, + RawSink: sink, + SignatureVerifier: verifier, + Normalizers: new NoopNormalizerRouter(), + Services: new Microsoft.Extensions.DependencyInjection.ServiceCollection().BuildServiceProvider(), + ResumeTokens: ImmutableDictionary.Empty); + + var documents = new List(); + await foreach (var document in connector.FetchAsync(context, CancellationToken.None)) + { + documents.Add(document); + } + + documents.Should().HaveCount(1); + sink.Documents.Should().HaveCount(1); + documents[0].Format.Should().Be(VexDocumentFormat.OciAttestation); + documents[0].Metadata.Should().ContainKey("oci.attestation.sourceKind").WhoseValue.Should().Be("offline"); + documents[0].Metadata.Should().ContainKey("vex.provenance.sourceKind").WhoseValue.Should().Be("offline"); + documents[0].Metadata.Should().ContainKey("vex.provenance.registryAuthMode").WhoseValue.Should().Be("Anonymous"); + verifier.VerifyCalls.Should().Be(1); + } + + [Fact] + public async Task FetchAsync_WithSignatureMetadata_EnrichesProvenance() + { + var fileSystem = new MockFileSystem(new Dictionary + { + ["/bundles/attestation.json"] = new MockFileData("{\"payload\":\"\",\"payloadType\":\"application/vnd.in-toto+json\",\"signatures\":[{\"sig\":\"\"}]}"), + }); + + using var cache = new MemoryCache(new MemoryCacheOptions()); + var httpClient = new HttpClient(new StubHttpMessageHandler()) + { + BaseAddress = new System.Uri("https://registry.example.com/") + }; + + var httpFactory = new SingleClientHttpClientFactory(httpClient); + var discovery = new OciAttestationDiscoveryService(cache, fileSystem, NullLogger.Instance); + var fetcher = new OciAttestationFetcher(httpFactory, fileSystem, NullLogger.Instance); + + var connector = new OciOpenVexAttestationConnector( + discovery, + fetcher, + NullLogger.Instance, + TimeProvider.System); + + var settingsValues = ImmutableDictionary.Empty + .Add("Images:0:Reference", "registry.example.com/repo/image:latest") + .Add("Images:0:OfflineBundlePath", "/bundles/attestation.json") + .Add("Offline:PreferOffline", "true") + .Add("Offline:AllowNetworkFallback", "false") + .Add("Cosign:Mode", "Keyless") + .Add("Cosign:Keyless:Issuer", "https://issuer.example.com") + .Add("Cosign:Keyless:Subject", "subject@example.com"); + + var settings = new VexConnectorSettings(settingsValues); + await connector.ValidateAsync(settings, CancellationToken.None); + + var sink = new CapturingRawSink(); + var verifier = new CapturingSignatureVerifier + { + Result = new VexSignatureMetadata( + type: "cosign", + subject: "sig-subject", + issuer: "sig-issuer", + keyId: "key-id", + verifiedAt: DateTimeOffset.UtcNow, + transparencyLogReference: "rekor://entry/123") + }; + + var context = new VexConnectorContext( + Since: null, + Settings: VexConnectorSettings.Empty, + RawSink: sink, + SignatureVerifier: verifier, + Normalizers: new NoopNormalizerRouter(), + Services: new ServiceCollection().BuildServiceProvider(), + ResumeTokens: ImmutableDictionary.Empty); + + var documents = new List(); + await foreach (var document in connector.FetchAsync(context, CancellationToken.None)) + { + documents.Add(document); + } + + documents.Should().HaveCount(1); + var metadata = documents[0].Metadata; + metadata.Should().Contain("vex.signature.type", "cosign"); + metadata.Should().Contain("vex.signature.subject", "sig-subject"); + metadata.Should().Contain("vex.signature.issuer", "sig-issuer"); + metadata.Should().Contain("vex.signature.keyId", "key-id"); + metadata.Should().ContainKey("vex.signature.verifiedAt"); + metadata.Should().Contain("vex.signature.transparencyLogReference", "rekor://entry/123"); + metadata.Should().Contain("vex.provenance.cosign.mode", "Keyless"); + metadata.Should().Contain("vex.provenance.cosign.issuer", "https://issuer.example.com"); + metadata.Should().Contain("vex.provenance.cosign.subject", "subject@example.com"); + verifier.VerifyCalls.Should().Be(1); + } + + private sealed class CapturingRawSink : IVexRawDocumentSink + { + public List Documents { get; } = new(); + + public ValueTask StoreAsync(VexRawDocument document, CancellationToken cancellationToken) + { + Documents.Add(document); + return ValueTask.CompletedTask; + } + } + + private sealed class CapturingSignatureVerifier : IVexSignatureVerifier + { + public int VerifyCalls { get; private set; } + + public VexSignatureMetadata? Result { get; set; } + + public ValueTask VerifyAsync(VexRawDocument document, CancellationToken cancellationToken) + { + VerifyCalls++; + return ValueTask.FromResult(Result); + } + } + + private sealed class NoopNormalizerRouter : IVexNormalizerRouter + { + public ValueTask NormalizeAsync(VexRawDocument document, CancellationToken cancellationToken) + => ValueTask.FromResult(new VexClaimBatch(document, ImmutableArray.Empty, ImmutableDictionary.Empty)); + } + + private sealed class SingleClientHttpClientFactory : IHttpClientFactory + { + private readonly HttpClient _client; + + public SingleClientHttpClientFactory(HttpClient client) + { + _client = client; + } + + public HttpClient CreateClient(string name) => _client; + } + + private sealed class StubHttpMessageHandler : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) + { + RequestMessage = request + }); + } + } + + +[Fact] +public async Task FetchAsync_EnrichesSignerTrustMetadataWhenConfigured() +{ + using var tempMetadata = CreateTempSignerMetadata("excititor:oci.openvex.attest", "tier-0", "feed-fp"); + Environment.SetEnvironmentVariable("STELLAOPS_CONNECTOR_SIGNER_METADATA_PATH", tempMetadata.Path); + try + { + var fileSystem = new MockFileSystem(new Dictionary + { + ["/bundles/attestation.json"] = new MockFileData("{"payload":"","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":""}]}") + }); + + using var cache = new MemoryCache(new MemoryCacheOptions()); + var httpClient = new HttpClient(new StubHttpMessageHandler()) + { + BaseAddress = new System.Uri("https://registry.example.com/") + }; + + var httpFactory = new SingleClientHttpClientFactory(httpClient); + var discovery = new OciAttestationDiscoveryService(cache, fileSystem, NullLogger.Instance); + var fetcher = new OciAttestationFetcher(httpFactory, fileSystem, NullLogger.Instance); + + var connector = new OciOpenVexAttestationConnector( + discovery, + fetcher, + NullLogger.Instance, + TimeProvider.System); + + var settingsValues = ImmutableDictionary.Empty + .Add("Images:0:Reference", "registry.example.com/repo/image:latest") + .Add("Images:0:OfflineBundlePath", "/bundles/attestation.json") + .Add("Offline:PreferOffline", "true") + .Add("Offline:AllowNetworkFallback", "false") + .Add("Cosign:Mode", "None"); + + var settings = new VexConnectorSettings(settingsValues); + await connector.ValidateAsync(settings, CancellationToken.None); + + var sink = new CapturingRawSink(); + var context = new VexConnectorContext( + Since: null, + Settings: VexConnectorSettings.Empty, + RawSink: sink, + SignatureVerifier: new NoopSignatureVerifier(), + Normalizers: new NoopNormalizerRouter(), + Services: new ServiceCollection().BuildServiceProvider(), + ResumeTokens: ImmutableDictionary.Empty); + + await foreach (var _ in connector.FetchAsync(context, CancellationToken.None)) { } + + var emitted = sink.Documents.Single(); + emitted.Metadata.Should().Contain("vex.provenance.trust.issuerTier", "tier-0"); + emitted.Metadata.Should().ContainKey("vex.provenance.trust.signers"); + } + finally + { + Environment.SetEnvironmentVariable("STELLAOPS_CONNECTOR_SIGNER_METADATA_PATH", null); + } +} + +private static TempMetadataFile CreateTempSignerMetadata(string connectorId, string tier, string fingerprint) +{ + var pathTemp = System.IO.Path.GetTempFileName(); + var json = $""" + {{ + "schemaVersion": "1.0.0", + "generatedAt": "2025-11-20T00:00:00Z", + "connectors": [ + {{ + "connectorId": "{connectorId}", + "provider": {{ "name": "{connectorId}", "slug": "{connectorId}" }}, + "issuerTier": "{tier}", + "signers": [ + {{ + "usage": "attestation", + "fingerprints": [ + {{ "alg": "sha256", "format": "cosign", "value": "{fingerprint}" }} + ] + }} + ] + }} + ] + }} + """; + System.IO.File.WriteAllText(pathTemp, json); + return new TempMetadataFile(pathTemp); +} + +private sealed record TempMetadataFile(string Path) : IDisposable +{ + public void Dispose() + { + try { System.IO.File.Delete(Path); } catch { } + } +} + +} \ No newline at end of file diff --git a/src/Excititor/__Tests/StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests/Connectors/UbuntuCsafConnectorTests.cs b/src/Excititor/__Tests/StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests/Connectors/UbuntuCsafConnectorTests.cs index 54d95c6e1..653e5a3a6 100644 --- a/src/Excititor/__Tests/StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests/Connectors/UbuntuCsafConnectorTests.cs +++ b/src/Excititor/__Tests/StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests/Connectors/UbuntuCsafConnectorTests.cs @@ -1,62 +1,67 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using FluentAssertions; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; -using StellaOps.Excititor.Connectors.Abstractions; -using StellaOps.Excititor.Connectors.Ubuntu.CSAF; -using StellaOps.Excititor.Connectors.Ubuntu.CSAF.Configuration; -using StellaOps.Excititor.Connectors.Ubuntu.CSAF.Metadata; -using StellaOps.Excititor.Core; -using StellaOps.Excititor.Storage.Mongo; -using System.IO.Abstractions.TestingHelpers; -using Xunit; -using MongoDB.Driver; - -namespace StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests.Connectors; - -public sealed class UbuntuCsafConnectorTests -{ - [Fact] - public async Task FetchAsync_IngestsNewDocument_UpdatesStateAndUsesEtag() - { - var baseUri = new Uri("https://ubuntu.test/security/csaf/"); - var indexUri = new Uri(baseUri, "index.json"); - var catalogUri = new Uri(baseUri, "stable/catalog.json"); - var advisoryUri = new Uri(baseUri, "stable/USN-2025-0001.json"); - - var manifest = CreateTestManifest(advisoryUri, "USN-2025-0001", "2025-10-18T00:00:00Z"); - var documentPayload = Encoding.UTF8.GetBytes("{\"document\":\"payload\"}"); - var documentSha = ComputeSha256(documentPayload); - - var indexJson = manifest.IndexJson; - var catalogJson = manifest.CatalogJson.Replace("{{SHA256}}", documentSha, StringComparison.Ordinal); - var handler = new UbuntuTestHttpHandler(indexUri, indexJson, catalogUri, catalogJson, advisoryUri, documentPayload, expectedEtag: "etag-123"); - - var httpClient = new HttpClient(handler); - var httpFactory = new SingleClientFactory(httpClient); - var cache = new MemoryCache(new MemoryCacheOptions()); - var fileSystem = new MockFileSystem(); - var loader = new UbuntuCatalogLoader(httpFactory, cache, fileSystem, NullLogger.Instance, TimeProvider.System); - - var optionsValidator = new UbuntuConnectorOptionsValidator(fileSystem); - var stateRepository = new InMemoryConnectorStateRepository(); - var connector = new UbuntuCsafConnector( - loader, - httpFactory, - stateRepository, - new[] { optionsValidator }, - NullLogger.Instance, - TimeProvider.System); - +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using FluentAssertions; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using StellaOps.Excititor.Connectors.Abstractions; +using StellaOps.Excititor.Connectors.Ubuntu.CSAF; +using StellaOps.Excititor.Connectors.Ubuntu.CSAF.Configuration; +using StellaOps.Excititor.Connectors.Ubuntu.CSAF.Metadata; +using StellaOps.Excititor.Core; +using StellaOps.Excititor.Storage.Mongo; +using System.IO.Abstractions.TestingHelpers; +using Xunit; +using MongoDB.Driver; + +namespace StellaOps.Excititor.Connectors.Ubuntu.CSAF.Tests.Connectors; + +public sealed class UbuntuCsafConnectorTests +{ + [Fact] + public async Task FetchAsync_IngestsNewDocument_UpdatesStateAndUsesEtag() + { + using var tempMetadata = CreateTempSignerMetadata("excititor:ubuntu", "tier-2", "deadbeef"); + Environment.SetEnvironmentVariable("STELLAOPS_CONNECTOR_SIGNER_METADATA_PATH", tempMetadata.Path); + try + { + var baseUri = new Uri("https://ubuntu.test/security/csaf/"); + var indexUri = new Uri(baseUri, "index.json"); + var catalogUri = new Uri(baseUri, "stable/catalog.json"); + var advisoryUri = new Uri(baseUri, "stable/USN-2025-0001.json"); + + var manifest = CreateTestManifest(advisoryUri, "USN-2025-0001", "2025-10-18T00:00:00Z"); + var documentPayload = Encoding.UTF8.GetBytes("{\"document\":\"payload\"}"); + var documentSha = ComputeSha256(documentPayload); + + var indexJson = manifest.IndexJson; + var catalogJson = manifest.CatalogJson.Replace("{{SHA256}}", documentSha, StringComparison.Ordinal); + var handler = new UbuntuTestHttpHandler(indexUri, indexJson, catalogUri, catalogJson, advisoryUri, documentPayload, expectedEtag: "etag-123"); + + var httpClient = new HttpClient(handler); + var httpFactory = new SingleClientFactory(httpClient); + var cache = new MemoryCache(new MemoryCacheOptions()); + var fileSystem = new MockFileSystem(); + var loader = new UbuntuCatalogLoader(httpFactory, cache, fileSystem, NullLogger.Instance, TimeProvider.System); + + var optionsValidator = new UbuntuConnectorOptionsValidator(fileSystem); + var stateRepository = new InMemoryConnectorStateRepository(); + var connector = new UbuntuCsafConnector( + loader, + httpFactory, + stateRepository, + new[] { optionsValidator }, + NullLogger.Instance, + TimeProvider.System); + var settings = BuildConnectorSettings(indexUri, trustWeight: 0.63, trustTier: "distro-trusted", fingerprints: new[] { @@ -72,15 +77,15 @@ public sealed class UbuntuCsafConnectorTests var sink = new InMemoryRawSink(); var context = new VexConnectorContext(null, VexConnectorSettings.Empty, sink, new NoopSignatureVerifier(), new NoopNormalizerRouter(), services, ImmutableDictionary.Empty); - - var documents = new List(); - await foreach (var doc in connector.FetchAsync(context, CancellationToken.None)) - { - documents.Add(doc); - } - - documents.Should().HaveCount(1); - sink.Documents.Should().HaveCount(1); + + var documents = new List(); + await foreach (var doc in connector.FetchAsync(context, CancellationToken.None)) + { + documents.Add(doc); + } + + documents.Should().HaveCount(1); + sink.Documents.Should().HaveCount(1); var stored = sink.Documents.Single(); stored.Digest.Should().Be($"sha256:{documentSha}"); stored.Metadata.Should().Contain("ubuntu.etag", "etag-123"); @@ -93,25 +98,25 @@ public sealed class UbuntuCsafConnectorTests stored.Metadata.Should().Contain( "vex.provenance.pgp.fingerprints", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); - - stateRepository.CurrentState.Should().NotBeNull(); - stateRepository.CurrentState!.DocumentDigests.Should().Contain($"sha256:{documentSha}"); - stateRepository.CurrentState.DocumentDigests.Should().Contain($"etag:{advisoryUri}|etag-123"); - stateRepository.CurrentState.LastUpdated.Should().Be(DateTimeOffset.Parse("2025-10-18T00:00:00Z")); - - handler.DocumentRequestCount.Should().Be(1); - - // Second run: Expect connector to send If-None-Match and skip download via 304. - sink.Documents.Clear(); - documents.Clear(); - - await foreach (var doc in connector.FetchAsync(context, CancellationToken.None)) - { - documents.Add(doc); - } - - documents.Should().BeEmpty(); - sink.Documents.Should().BeEmpty(); + + stateRepository.CurrentState.Should().NotBeNull(); + stateRepository.CurrentState!.DocumentDigests.Should().Contain($"sha256:{documentSha}"); + stateRepository.CurrentState.DocumentDigests.Should().Contain($"etag:{advisoryUri}|etag-123"); + stateRepository.CurrentState.LastUpdated.Should().Be(DateTimeOffset.Parse("2025-10-18T00:00:00Z")); + + handler.DocumentRequestCount.Should().Be(1); + + // Second run: Expect connector to send If-None-Match and skip download via 304. + sink.Documents.Clear(); + documents.Clear(); + + await foreach (var doc in connector.FetchAsync(context, CancellationToken.None)) + { + documents.Add(doc); + } + + documents.Should().BeEmpty(); + sink.Documents.Should().BeEmpty(); handler.DocumentRequestCount.Should().Be(2); handler.SeenIfNoneMatch.Should().Contain("\"etag-123\""); @@ -123,37 +128,41 @@ public sealed class UbuntuCsafConnectorTests "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", }); - } - - [Fact] - public async Task FetchAsync_SkipsWhenChecksumMismatch() - { - var baseUri = new Uri("https://ubuntu.test/security/csaf/"); - var indexUri = new Uri(baseUri, "index.json"); - var catalogUri = new Uri(baseUri, "stable/catalog.json"); - var advisoryUri = new Uri(baseUri, "stable/USN-2025-0002.json"); - - var manifest = CreateTestManifest(advisoryUri, "USN-2025-0002", "2025-10-18T00:00:00Z"); - var indexJson = manifest.IndexJson; - var catalogJson = manifest.CatalogJson.Replace("{{SHA256}}", new string('a', 64), StringComparison.Ordinal); - var handler = new UbuntuTestHttpHandler(indexUri, indexJson, catalogUri, catalogJson, advisoryUri, Encoding.UTF8.GetBytes("{\"document\":\"payload\"}"), expectedEtag: "etag-999"); - - var httpClient = new HttpClient(handler); - var httpFactory = new SingleClientFactory(httpClient); - var cache = new MemoryCache(new MemoryCacheOptions()); - var fileSystem = new MockFileSystem(); - var loader = new UbuntuCatalogLoader(httpFactory, cache, fileSystem, NullLogger.Instance, TimeProvider.System); - var optionsValidator = new UbuntuConnectorOptionsValidator(fileSystem); - var stateRepository = new InMemoryConnectorStateRepository(); - - var connector = new UbuntuCsafConnector( - loader, - httpFactory, - stateRepository, - new[] { optionsValidator }, - NullLogger.Instance, - TimeProvider.System); - + } + finally + { + Environment.SetEnvironmentVariable("STELLAOPS_CONNECTOR_SIGNER_METADATA_PATH", null); + } + + [Fact] + public async Task FetchAsync_SkipsWhenChecksumMismatch() + { + var baseUri = new Uri("https://ubuntu.test/security/csaf/"); + var indexUri = new Uri(baseUri, "index.json"); + var catalogUri = new Uri(baseUri, "stable/catalog.json"); + var advisoryUri = new Uri(baseUri, "stable/USN-2025-0002.json"); + + var manifest = CreateTestManifest(advisoryUri, "USN-2025-0002", "2025-10-18T00:00:00Z"); + var indexJson = manifest.IndexJson; + var catalogJson = manifest.CatalogJson.Replace("{{SHA256}}", new string('a', 64), StringComparison.Ordinal); + var handler = new UbuntuTestHttpHandler(indexUri, indexJson, catalogUri, catalogJson, advisoryUri, Encoding.UTF8.GetBytes("{\"document\":\"payload\"}"), expectedEtag: "etag-999"); + + var httpClient = new HttpClient(handler); + var httpFactory = new SingleClientFactory(httpClient); + var cache = new MemoryCache(new MemoryCacheOptions()); + var fileSystem = new MockFileSystem(); + var loader = new UbuntuCatalogLoader(httpFactory, cache, fileSystem, NullLogger.Instance, TimeProvider.System); + var optionsValidator = new UbuntuConnectorOptionsValidator(fileSystem); + var stateRepository = new InMemoryConnectorStateRepository(); + + var connector = new UbuntuCsafConnector( + loader, + httpFactory, + stateRepository, + new[] { optionsValidator }, + NullLogger.Instance, + TimeProvider.System); + var settings = BuildConnectorSettings(indexUri); await connector.ValidateAsync(settings, CancellationToken.None); @@ -164,17 +173,17 @@ public sealed class UbuntuCsafConnectorTests var sink = new InMemoryRawSink(); var context = new VexConnectorContext(null, VexConnectorSettings.Empty, sink, new NoopSignatureVerifier(), new NoopNormalizerRouter(), services, ImmutableDictionary.Empty); - - var documents = new List(); - await foreach (var doc in connector.FetchAsync(context, CancellationToken.None)) - { - documents.Add(doc); - } - - documents.Should().BeEmpty(); - sink.Documents.Should().BeEmpty(); - stateRepository.CurrentState.Should().NotBeNull(); - stateRepository.CurrentState!.DocumentDigests.Should().BeEmpty(); + + var documents = new List(); + await foreach (var doc in connector.FetchAsync(context, CancellationToken.None)) + { + documents.Add(doc); + } + + documents.Should().BeEmpty(); + sink.Documents.Should().BeEmpty(); + stateRepository.CurrentState.Should().NotBeNull(); + stateRepository.CurrentState!.DocumentDigests.Should().BeEmpty(); handler.DocumentRequestCount.Should().Be(1); providerStore.SavedProviders.Should().ContainSingle(); } @@ -198,146 +207,183 @@ public sealed class UbuntuCsafConnectorTests return new VexConnectorSettings(builder.ToImmutable()); } - private static (string IndexJson, string CatalogJson) CreateTestManifest(Uri advisoryUri, string advisoryId, string timestamp) - { - var indexJson = """ - { - "generated": "2025-10-18T00:00:00Z", - "channels": [ - { - "name": "stable", - "catalogUrl": "{{advisoryUri.GetLeftPart(UriPartial.Authority)}}/security/csaf/stable/catalog.json", - "sha256": "ignore" - } - ] - } - """; - - var catalogJson = """ - { - "resources": [ - { - "id": "{{advisoryId}}", - "type": "csaf", - "url": "{{advisoryUri}}", - "last_modified": "{{timestamp}}", - "hashes": { - "sha256": "{{SHA256}}" - }, - "etag": "\"etag-123\"", - "title": "{{advisoryId}}" - } - ] - } - """; - - return (indexJson, catalogJson); - } - - private static string ComputeSha256(ReadOnlySpan payload) - { - Span buffer = stackalloc byte[32]; - SHA256.HashData(payload, buffer); - return Convert.ToHexString(buffer).ToLowerInvariant(); - } - - private sealed class SingleClientFactory : IHttpClientFactory - { - private readonly HttpClient _client; - - public SingleClientFactory(HttpClient client) - { - _client = client; - } - - public HttpClient CreateClient(string name) => _client; - } - - private sealed class UbuntuTestHttpHandler : HttpMessageHandler - { - private readonly Uri _indexUri; - private readonly string _indexPayload; - private readonly Uri _catalogUri; - private readonly string _catalogPayload; - private readonly Uri _documentUri; - private readonly byte[] _documentPayload; - private readonly string _expectedEtag; - - public int DocumentRequestCount { get; private set; } - public List SeenIfNoneMatch { get; } = new(); - - public UbuntuTestHttpHandler(Uri indexUri, string indexPayload, Uri catalogUri, string catalogPayload, Uri documentUri, byte[] documentPayload, string expectedEtag) - { - _indexUri = indexUri; - _indexPayload = indexPayload; - _catalogUri = catalogUri; - _catalogPayload = catalogPayload; - _documentUri = documentUri; - _documentPayload = documentPayload; - _expectedEtag = expectedEtag; - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - if (request.RequestUri == _indexUri) - { - return Task.FromResult(CreateJsonResponse(_indexPayload)); - } - - if (request.RequestUri == _catalogUri) - { - return Task.FromResult(CreateJsonResponse(_catalogPayload)); - } - - if (request.RequestUri == _documentUri) - { - DocumentRequestCount++; - if (request.Headers.IfNoneMatch is { Count: > 0 }) - { - var header = request.Headers.IfNoneMatch.First().ToString(); - SeenIfNoneMatch.Add(header); - if (header.Trim('"') == _expectedEtag || header == $"\"{_expectedEtag}\"") - { - return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotModified)); - } - } - - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new ByteArrayContent(_documentPayload), - }; - response.Headers.ETag = new EntityTagHeaderValue($"\"{_expectedEtag}\""); - response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - return Task.FromResult(response); - } - - return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) - { - Content = new StringContent($"No response configured for {request.RequestUri}"), - }); - } - - private static HttpResponseMessage CreateJsonResponse(string payload) - => new(HttpStatusCode.OK) - { - Content = new StringContent(payload, Encoding.UTF8, "application/json"), - }; - } - - private sealed class InMemoryConnectorStateRepository : IVexConnectorStateRepository - { - public VexConnectorState? CurrentState { get; private set; } - - public ValueTask GetAsync(string connectorId, CancellationToken cancellationToken, IClientSessionHandle? session = null) - => ValueTask.FromResult(CurrentState); - - public ValueTask SaveAsync(VexConnectorState state, CancellationToken cancellationToken, IClientSessionHandle? session = null) - { - CurrentState = state; - return ValueTask.CompletedTask; - } - } - + private static (string IndexJson, string CatalogJson) CreateTestManifest(Uri advisoryUri, string advisoryId, string timestamp) + { + var indexJson = """ + { + "generated": "2025-10-18T00:00:00Z", + "channels": [ + { + "name": "stable", + "catalogUrl": "{{advisoryUri.GetLeftPart(UriPartial.Authority)}}/security/csaf/stable/catalog.json", + "sha256": "ignore" + } + ] + } + """; + + var catalogJson = """ + { + "resources": [ + { + "id": "{{advisoryId}}", + "type": "csaf", + "url": "{{advisoryUri}}", + "last_modified": "{{timestamp}}", + "hashes": { + "sha256": "{{SHA256}}" + }, + "etag": "\"etag-123\"", + "title": "{{advisoryId}}" + } + ] + } + """; + + return (indexJson, catalogJson); + } + + private static string ComputeSha256(ReadOnlySpan payload) + { + Span buffer = stackalloc byte[32]; + SHA256.HashData(payload, buffer); + return Convert.ToHexString(buffer).ToLowerInvariant(); + } + + private sealed class SingleClientFactory : IHttpClientFactory + { + private readonly HttpClient _client; + + public SingleClientFactory(HttpClient client) + { + _client = client; + } + + public HttpClient CreateClient(string name) => _client; + } + + + private static TempMetadataFile CreateTempSignerMetadata(string connectorId, string tier, string fingerprint) + { + var pathTemp = System.IO.Path.GetTempFileName(); + var json = $""" + {{ + \"schemaVersion\": \"1.0.0\", + \"generatedAt\": \"2025-11-20T00:00:00Z\", + \"connectors\": [ + {{ + \"connectorId\": \"{connectorId}\", + \"provider\": {{ \"name\": \"{connectorId}\", \"slug\": \"{connectorId}\" }}, + \"issuerTier\": \"{tier}\", + \"signers\": [ + {{ + \"usage\": \"csaf\", + \"fingerprints\": [ + {{ \"alg\": \"sha256\", \"format\": \"pgp\", \"value\": \"{fingerprint}\" }} + ] + }} + ] + }} + ] + }} + """; + System.IO.File.WriteAllText(pathTemp, json); + return new TempMetadataFile(pathTemp); + } + + private sealed record TempMetadataFile(string Path) : IDisposable + { + public void Dispose() + { + try { System.IO.File.Delete(Path); } catch { } + } + } + + private sealed class UbuntuTestHttpHandler : HttpMessageHandler + { + private readonly Uri _indexUri; + private readonly string _indexPayload; + private readonly Uri _catalogUri; + private readonly string _catalogPayload; + private readonly Uri _documentUri; + private readonly byte[] _documentPayload; + private readonly string _expectedEtag; + + public int DocumentRequestCount { get; private set; } + public List SeenIfNoneMatch { get; } = new(); + + public UbuntuTestHttpHandler(Uri indexUri, string indexPayload, Uri catalogUri, string catalogPayload, Uri documentUri, byte[] documentPayload, string expectedEtag) + { + _indexUri = indexUri; + _indexPayload = indexPayload; + _catalogUri = catalogUri; + _catalogPayload = catalogPayload; + _documentUri = documentUri; + _documentPayload = documentPayload; + _expectedEtag = expectedEtag; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.RequestUri == _indexUri) + { + return Task.FromResult(CreateJsonResponse(_indexPayload)); + } + + if (request.RequestUri == _catalogUri) + { + return Task.FromResult(CreateJsonResponse(_catalogPayload)); + } + + if (request.RequestUri == _documentUri) + { + DocumentRequestCount++; + if (request.Headers.IfNoneMatch is { Count: > 0 }) + { + var header = request.Headers.IfNoneMatch.First().ToString(); + SeenIfNoneMatch.Add(header); + if (header.Trim('"') == _expectedEtag || header == $"\"{_expectedEtag}\"") + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotModified)); + } + } + + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(_documentPayload), + }; + response.Headers.ETag = new EntityTagHeaderValue($"\"{_expectedEtag}\""); + response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + return Task.FromResult(response); + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound) + { + Content = new StringContent($"No response configured for {request.RequestUri}"), + }); + } + + private static HttpResponseMessage CreateJsonResponse(string payload) + => new(HttpStatusCode.OK) + { + Content = new StringContent(payload, Encoding.UTF8, "application/json"), + }; + } + + private sealed class InMemoryConnectorStateRepository : IVexConnectorStateRepository + { + public VexConnectorState? CurrentState { get; private set; } + + public ValueTask GetAsync(string connectorId, CancellationToken cancellationToken, IClientSessionHandle? session = null) + => ValueTask.FromResult(CurrentState); + + public ValueTask SaveAsync(VexConnectorState state, CancellationToken cancellationToken, IClientSessionHandle? session = null) + { + CurrentState = state; + return ValueTask.CompletedTask; + } + } + private sealed class InMemoryRawSink : IVexRawDocumentSink { public List Documents { get; } = new(); @@ -374,16 +420,16 @@ public sealed class UbuntuCsafConnectorTests return ValueTask.CompletedTask; } } - - private sealed class NoopSignatureVerifier : IVexSignatureVerifier - { - public ValueTask VerifyAsync(VexRawDocument document, CancellationToken cancellationToken) - => ValueTask.FromResult(null); - } - - private sealed class NoopNormalizerRouter : IVexNormalizerRouter - { - public ValueTask NormalizeAsync(VexRawDocument document, CancellationToken cancellationToken) - => ValueTask.FromResult(new VexClaimBatch(document, ImmutableArray.Empty, ImmutableDictionary.Empty)); - } -} + + private sealed class NoopSignatureVerifier : IVexSignatureVerifier + { + public ValueTask VerifyAsync(VexRawDocument document, CancellationToken cancellationToken) + => ValueTask.FromResult(null); + } + + private sealed class NoopNormalizerRouter : IVexNormalizerRouter + { + public ValueTask NormalizeAsync(VexRawDocument document, CancellationToken cancellationToken) + => ValueTask.FromResult(new VexClaimBatch(document, ImmutableArray.Empty, ImmutableDictionary.Empty)); + } +} diff --git a/src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/AttestationVerifyEndpointTests.cs b/src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/AttestationVerifyEndpointTests.cs new file mode 100644 index 000000000..e5e1c4829 --- /dev/null +++ b/src/Excititor/__Tests/StellaOps.Excititor.WebService.Tests/AttestationVerifyEndpointTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http.Json; +using System.Threading.Tasks; +using FluentAssertions; +using StellaOps.Excititor.WebService.Contracts; +using Xunit; + +namespace StellaOps.Excititor.WebService.Tests; + +public sealed class AttestationVerifyEndpointTests : IClassFixture +{ + private readonly TestWebApplicationFactory _factory; + + public AttestationVerifyEndpointTests(TestWebApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task Verify_ReturnsOk_WhenPayloadValid() + { + var client = _factory.CreateClient(); + + var request = new AttestationVerifyRequest + { + ExportId = "export-123", + QuerySignature = "purl=foo", + ArtifactDigest = "sha256:deadbeef", + Format = "VexJson", + CreatedAt = DateTimeOffset.Parse("2025-11-20T00:00:00Z"), + SourceProviders = new[] { "ghsa" }, + Metadata = new Dictionary { { "foo", "bar" } }, + Attestation = new AttestationVerifyMetadata + { + PredicateType = "https://stella-ops.org/attestations/vex-export", + EnvelopeDigest = "sha256:abcd", + SignedAt = DateTimeOffset.Parse("2025-11-20T00:00:00Z"), + Rekor = new AttestationRekorReference + { + ApiVersion = "0.2", + Location = "https://rekor.example/log/123", + LogIndex = 1, + InclusionProofUrl = new Uri("https://rekor.example/log/123/proof") + } + }, + Envelope = "{}" + }; + + var response = await client.PostAsJsonAsync("/v1/attestations/verify", request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + var body = await response.Content.ReadFromJsonAsync(); + body.Should().NotBeNull(); + body!.Valid.Should().BeTrue(); + } + + [Fact] + public async Task Verify_ReturnsBadRequest_WhenFieldsMissing() + { + var client = _factory.CreateClient(); + + var request = new AttestationVerifyRequest + { + ExportId = "", // missing + QuerySignature = "", + ArtifactDigest = "", + Format = "", + Envelope = "" + }; + + var response = await client.PostAsJsonAsync("/v1/attestations/verify", request); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } +} diff --git a/src/Findings/StellaOps.Findings.Ledger.Tests/Exports/ExportPagingTests.cs b/src/Findings/StellaOps.Findings.Ledger.Tests/Exports/ExportPagingTests.cs new file mode 100644 index 000000000..980bfe721 --- /dev/null +++ b/src/Findings/StellaOps.Findings.Ledger.Tests/Exports/ExportPagingTests.cs @@ -0,0 +1,51 @@ +using StellaOps.Findings.Ledger.Infrastructure.Exports; +using Xunit; + +namespace StellaOps.Findings.Ledger.Tests.Exports; + +public class ExportPagingTests +{ + [Fact] + public void ComputeFiltersHash_IsDeterministic() + { + var left = new Dictionary + { + ["shape"] = "canonical", + ["since_sequence"] = "10", + ["until_sequence"] = "20" + }; + + var right = new Dictionary + { + ["until_sequence"] = "20", + ["shape"] = "canonical", + ["since_sequence"] = "10" + }; + + var leftHash = ExportPaging.ComputeFiltersHash(left); + var rightHash = ExportPaging.ComputeFiltersHash(right); + + Assert.Equal(leftHash, rightHash); + } + + [Fact] + public void PageToken_RoundTrips() + { + var key = new ExportPaging.ExportPageKey(5, "v1", "abc123"); + var filtersHash = ExportPaging.ComputeFiltersHash(new Dictionary + { + ["shape"] = "canonical" + }); + + var token = ExportPaging.CreatePageToken(key, filtersHash); + + var parsed = ExportPaging.TryParsePageToken(token, filtersHash, out var recovered, out var error); + + Assert.True(parsed); + Assert.Null(error); + Assert.NotNull(recovered); + Assert.Equal(key.SequenceNumber, recovered!.SequenceNumber); + Assert.Equal(key.PolicyVersion, recovered.PolicyVersion); + Assert.Equal(key.CycleHash, recovered.CycleHash); + } +} diff --git a/src/Findings/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj b/src/Findings/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj index 9cdaecebb..1318cad7d 100644 --- a/src/Findings/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj +++ b/src/Findings/StellaOps.Findings.Ledger.Tests/StellaOps.Findings.Ledger.Tests.csproj @@ -4,6 +4,8 @@ net10.0 enable enable + $(DefaultItemExcludes);**/tools/**/* + true diff --git a/src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs b/src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs index ee67de508..0b131133f 100644 --- a/src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs +++ b/src/Findings/StellaOps.Findings.Ledger.WebService/Program.cs @@ -275,29 +275,7 @@ app.MapGet("/ledger/export/findings", async Task h.Contains("application/x-ndjson", StringComparison.OrdinalIgnoreCase)); - if (acceptsNdjson) - { - httpContext.Response.ContentType = "application/x-ndjson"; - var stream = new MemoryStream(); - await using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { SkipValidation = false, Indented = false }); - foreach (var item in page.Items) - { - JsonSerializer.Serialize(writer, item); - writer.Flush(); - await stream.WriteAsync(new byte[] { (byte)'\n' }, cancellationToken).ConfigureAwait(false); - } - stream.Position = 0; - return TypedResults.Stream(stream, contentType: "application/x-ndjson"); - } - - return TypedResults.Json(page); + return await WritePagedResponse(httpContext, page, cancellationToken).ConfigureAwait(false); }) .WithName("LedgerExportFindings") .RequireAuthorization(LedgerExportPolicy) @@ -342,3 +320,33 @@ static LedgerEventResponse CreateResponse(LedgerEventRecord record, string statu MerkleLeafHash = record.MerkleLeafHash, RecordedAt = record.RecordedAt }; + +static async Task>, ProblemHttpResult>> WritePagedResponse( + HttpContext httpContext, + ExportPage page, + CancellationToken cancellationToken) +{ + if (!string.IsNullOrEmpty(page.NextPageToken)) + { + httpContext.Response.Headers["X-Stella-Next-Page-Token"] = page.NextPageToken; + } + httpContext.Response.Headers["X-Stella-Result-Count"] = page.Items.Count.ToString(); + + var acceptsNdjson = httpContext.Request.Headers.Accept.Any(h => h.Contains("application/x-ndjson", StringComparison.OrdinalIgnoreCase)); + if (acceptsNdjson) + { + httpContext.Response.ContentType = "application/x-ndjson"; + var stream = new MemoryStream(); + await using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { SkipValidation = false, Indented = false }); + foreach (var item in page.Items) + { + JsonSerializer.Serialize(writer, item); + writer.Flush(); + await stream.WriteAsync(new byte[] { (byte)'\n' }, cancellationToken).ConfigureAwait(false); + } + stream.Position = 0; + return TypedResults.Stream(stream, contentType: "application/x-ndjson"); + } + + return TypedResults.Json(page); +} diff --git a/src/Findings/StellaOps.Findings.Ledger/Infrastructure/Exports/ExportPaging.cs b/src/Findings/StellaOps.Findings.Ledger/Infrastructure/Exports/ExportPaging.cs new file mode 100644 index 000000000..7e67c479c --- /dev/null +++ b/src/Findings/StellaOps.Findings.Ledger/Infrastructure/Exports/ExportPaging.cs @@ -0,0 +1,97 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.WebUtilities; + +namespace StellaOps.Findings.Ledger.Infrastructure.Exports; + +public static class ExportPaging +{ + private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + public static string ComputeFiltersHash(IReadOnlyDictionary filters) + { + var builder = new StringBuilder(); + foreach (var pair in filters.OrderBy(kv => kv.Key, StringComparer.Ordinal)) + { + builder.Append(pair.Key).Append('=').Append(pair.Value ?? string.Empty).Append(';'); + } + + using var sha = SHA256.Create(); + var bytes = Encoding.UTF8.GetBytes(builder.ToString()); + var hash = sha.ComputeHash(bytes); + return Convert.ToHexString(hash).ToLowerInvariant(); + } + + public static string CreatePageToken(ExportPageKey key, string filtersHash) + { + var payload = new ExportPageToken + { + FiltersHash = filtersHash, + Last = key + }; + + var json = JsonSerializer.Serialize(payload, SerializerOptions); + return WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(json)); + } + + public static bool TryParsePageToken(string token, string expectedFiltersHash, out ExportPageKey? key, out string? error) + { + key = null; + error = null; + + byte[] decoded; + try + { + decoded = WebEncoders.Base64UrlDecode(token); + } + catch (FormatException) + { + error = "invalid_page_token_encoding"; + return false; + } + + ExportPageToken? payload; + try + { + payload = JsonSerializer.Deserialize(decoded, SerializerOptions); + } + catch (JsonException) + { + error = "invalid_page_token_payload"; + return false; + } + + if (payload is null || payload.Last is null) + { + error = "invalid_page_token_payload"; + return false; + } + + if (!string.Equals(payload.FiltersHash, expectedFiltersHash, StringComparison.Ordinal)) + { + error = "page_token_filters_mismatch"; + return false; + } + + key = payload.Last; + return true; + } + + public sealed record ExportPageKey(long SequenceNumber, string PolicyVersion, string CycleHash); + + private sealed class ExportPageToken + { + [JsonPropertyName("filters_hash")] + public string FiltersHash { get; set; } = string.Empty; + + [JsonPropertyName("last")] + public ExportPageKey? Last { get; set; } + } +} diff --git a/src/Findings/StellaOps.Findings.Ledger/Observability/LedgerMetrics.cs b/src/Findings/StellaOps.Findings.Ledger/Observability/LedgerMetrics.cs index 7c9d26ff7..0b767752d 100644 --- a/src/Findings/StellaOps.Findings.Ledger/Observability/LedgerMetrics.cs +++ b/src/Findings/StellaOps.Findings.Ledger/Observability/LedgerMetrics.cs @@ -31,11 +31,11 @@ internal static class LedgerMetrics public static void RecordWriteSuccess(TimeSpan duration, string? tenantId, string? eventType, string? source) { - var tags = new TagList + var tags = new KeyValuePair[] { - { "tenant", tenantId ?? string.Empty }, - { "event_type", eventType ?? string.Empty }, - { "source", source ?? string.Empty } + new("tenant", tenantId ?? string.Empty), + new("event_type", eventType ?? string.Empty), + new("source", source ?? string.Empty) }; WriteLatencySeconds.Record(duration.TotalSeconds, tags); @@ -50,12 +50,12 @@ internal static class LedgerMetrics string? policyVersion, string? evaluationStatus) { - var tags = new TagList + var tags = new KeyValuePair[] { - { "tenant", tenantId ?? string.Empty }, - { "event_type", eventType ?? string.Empty }, - { "policy_version", policyVersion ?? string.Empty }, - { "evaluation_status", evaluationStatus ?? string.Empty } + new("tenant", tenantId ?? string.Empty), + new("event_type", eventType ?? string.Empty), + new("policy_version", policyVersion ?? string.Empty), + new("evaluation_status", evaluationStatus ?? string.Empty) }; ProjectionApplySeconds.Record(duration.TotalSeconds, tags); diff --git a/src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj b/src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj index 46e9155b9..3a602bc77 100644 --- a/src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj +++ b/src/Findings/StellaOps.Findings.Ledger/StellaOps.Findings.Ledger.csproj @@ -4,12 +4,19 @@ net10.0 enable enable + $(DefaultItemExcludes);tools/**/* + + + + + + diff --git a/src/Findings/StellaOps.Findings.Ledger/tools/LedgerReplayHarness/Program.cs b/src/Findings/StellaOps.Findings.Ledger/tools/LedgerReplayHarness/Program.cs index 2cc13d9c8..996b56762 100644 --- a/src/Findings/StellaOps.Findings.Ledger/tools/LedgerReplayHarness/Program.cs +++ b/src/Findings/StellaOps.Findings.Ledger/tools/LedgerReplayHarness/Program.cs @@ -456,7 +456,13 @@ internal sealed class NoOpPolicyEvaluationService : IPolicyEvaluationService { public Task EvaluateAsync(LedgerEventRecord record, FindingProjection? current, CancellationToken cancellationToken) { - return Task.FromResult(new PolicyEvaluationResult("noop", record.OccurredAt, record.RecordedAt, current?.Status ?? "new")); + var labels = new JsonObject(); + return Task.FromResult(new PolicyEvaluationResult( + Status: current?.Status ?? "new", + Severity: current?.Severity, + Labels: labels, + ExplainRef: null, + Rationale: new JsonArray())); } } @@ -465,9 +471,9 @@ internal sealed class NoOpProjectionRepository : IFindingProjectionRepository public Task GetAsync(string tenantId, string findingId, string policyVersion, CancellationToken cancellationToken) => Task.FromResult(null); - public Task InsertActionAsync(FindingAction action, CancellationToken cancellationToken) => Task.CompletedTask; + public Task InsertActionAsync(TriageActionEntry entry, CancellationToken cancellationToken) => Task.CompletedTask; - public Task InsertHistoryAsync(FindingHistory history, CancellationToken cancellationToken) => Task.CompletedTask; + public Task InsertHistoryAsync(FindingHistoryEntry entry, CancellationToken cancellationToken) => Task.CompletedTask; public Task SaveCheckpointAsync(ProjectionCheckpoint checkpoint, CancellationToken cancellationToken) => Task.CompletedTask; @@ -475,17 +481,12 @@ internal sealed class NoOpProjectionRepository : IFindingProjectionRepository Task.FromResult(new ProjectionCheckpoint(DateTimeOffset.MinValue, Guid.Empty, DateTimeOffset.MinValue)); public Task UpsertAsync(FindingProjection projection, CancellationToken cancellationToken) => Task.CompletedTask; - - public Task EnsureIndexesAsync(CancellationToken cancellationToken) => Task.CompletedTask; } internal sealed class NoOpMerkleAnchorRepository : IMerkleAnchorRepository { - public Task InsertAsync(string tenantId, Guid anchorId, DateTimeOffset windowStart, DateTimeOffset windowEnd, long sequenceStart, long sequenceEnd, string rootHash, long leafCount, DateTime anchoredAt, string? anchorReference, CancellationToken cancellationToken) + public Task InsertAsync(string tenantId, Guid anchorId, DateTimeOffset windowStart, DateTimeOffset windowEnd, long sequenceStart, long sequenceEnd, string rootHash, int leafCount, DateTimeOffset anchoredAt, string? anchorReference, CancellationToken cancellationToken) => Task.CompletedTask; - - public Task GetLatestAsync(string tenantId, CancellationToken cancellationToken) => - Task.FromResult(null); } internal sealed class QueueMerkleAnchorScheduler : IMerkleAnchorScheduler diff --git a/src/SbomService/TASKS.md b/src/SbomService/TASKS.md new file mode 100644 index 000000000..1db3f6155 --- /dev/null +++ b/src/SbomService/TASKS.md @@ -0,0 +1,5 @@ +# SbomService Tasks (prep sync) + +| Task ID | Status | Notes | Updated (UTC) | +| --- | --- | --- | --- | +| PREP-SBOM-CONSOLE-23-001-BUILD-TEST-FAILING-D | DONE | Offline feed cache + script added; see `docs/modules/sbomservice/offline-feed-plan.md`. | 2025-11-20 | diff --git a/src/StellaOps.sln b/src/StellaOps.sln index 8e8f3de0d..a6dd1aac9 100644 --- a/src/StellaOps.sln +++ b/src/StellaOps.sln @@ -441,6 +441,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Authority.Plugin. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Ingestion.Telemetry", "__Libraries\StellaOps.Ingestion.Telemetry\StellaOps.Ingestion.Telemetry.csproj", "{FB2C1275-6C67-403C-8F21-B07A48C74FE4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AirGap.Time", "AirGap\StellaOps.AirGap.Time\StellaOps.AirGap.Time.csproj", "{0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.AirGap.Importer", "AirGap\StellaOps.AirGap.Importer\StellaOps.AirGap.Importer.csproj", "{D3829E4D-6538-4533-A0E0-3418042D7BFE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -2887,6 +2891,30 @@ Global {FB2C1275-6C67-403C-8F21-B07A48C74FE4}.Release|x64.Build.0 = Release|Any CPU {FB2C1275-6C67-403C-8F21-B07A48C74FE4}.Release|x86.ActiveCfg = Release|Any CPU {FB2C1275-6C67-403C-8F21-B07A48C74FE4}.Release|x86.Build.0 = Release|Any CPU + {0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704}.Debug|x64.ActiveCfg = Debug|Any CPU + {0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704}.Debug|x64.Build.0 = Debug|Any CPU + {0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704}.Debug|x86.ActiveCfg = Debug|Any CPU + {0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704}.Debug|x86.Build.0 = Debug|Any CPU + {0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704}.Release|Any CPU.Build.0 = Release|Any CPU + {0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704}.Release|x64.ActiveCfg = Release|Any CPU + {0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704}.Release|x64.Build.0 = Release|Any CPU + {0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704}.Release|x86.ActiveCfg = Release|Any CPU + {0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704}.Release|x86.Build.0 = Release|Any CPU + {D3829E4D-6538-4533-A0E0-3418042D7BFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3829E4D-6538-4533-A0E0-3418042D7BFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3829E4D-6538-4533-A0E0-3418042D7BFE}.Debug|x64.ActiveCfg = Debug|Any CPU + {D3829E4D-6538-4533-A0E0-3418042D7BFE}.Debug|x64.Build.0 = Debug|Any CPU + {D3829E4D-6538-4533-A0E0-3418042D7BFE}.Debug|x86.ActiveCfg = Debug|Any CPU + {D3829E4D-6538-4533-A0E0-3418042D7BFE}.Debug|x86.Build.0 = Debug|Any CPU + {D3829E4D-6538-4533-A0E0-3418042D7BFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3829E4D-6538-4533-A0E0-3418042D7BFE}.Release|Any CPU.Build.0 = Release|Any CPU + {D3829E4D-6538-4533-A0E0-3418042D7BFE}.Release|x64.ActiveCfg = Release|Any CPU + {D3829E4D-6538-4533-A0E0-3418042D7BFE}.Release|x64.Build.0 = Release|Any CPU + {D3829E4D-6538-4533-A0E0-3418042D7BFE}.Release|x86.ActiveCfg = Release|Any CPU + {D3829E4D-6538-4533-A0E0-3418042D7BFE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -3022,5 +3050,7 @@ Global {D913460C-2054-48F0-B274-894A94A8DD7E} = {D09AE309-2C35-6780-54D1-97CCC67DFFDE} {AAB54944-813D-4596-B6A9-F0014523F97D} = {D09AE309-2C35-6780-54D1-97CCC67DFFDE} {FB2C1275-6C67-403C-8F21-B07A48C74FE4} = {41F15E67-7190-CF23-3BC4-77E87134CADD} + {0B4DD2CC-19C8-4FE0-A2DE-076A5FF1B704} = {704A59BF-CC38-09FA-CE4F-73B27EC8F04F} + {D3829E4D-6538-4533-A0E0-3418042D7BFE} = {704A59BF-CC38-09FA-CE4F-73B27EC8F04F} EndGlobalSection EndGlobal diff --git a/tests/AirGap/StellaOps.AirGap.Importer.Tests/BundleImportPlannerTests.cs b/tests/AirGap/StellaOps.AirGap.Importer.Tests/BundleImportPlannerTests.cs new file mode 100644 index 000000000..9dcf05a52 --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Importer.Tests/BundleImportPlannerTests.cs @@ -0,0 +1,40 @@ +using StellaOps.AirGap.Importer.Contracts; +using StellaOps.AirGap.Importer.Planning; + +namespace StellaOps.AirGap.Importer.Tests; + +public class BundleImportPlannerTests +{ + [Fact] + public void ReturnsFailureWhenBundlePathMissing() + { + var planner = new BundleImportPlanner(); + var result = planner.CreatePlan(string.Empty, TrustRootConfig.Empty("/tmp")); + + Assert.False(result.InitialState.IsValid); + Assert.Equal("bundle-path-required", result.InitialState.Reason); + } + + [Fact] + public void ReturnsFailureWhenTrustRootsMissing() + { + var planner = new BundleImportPlanner(); + var result = planner.CreatePlan("bundle.tar", TrustRootConfig.Empty("/tmp")); + + Assert.False(result.InitialState.IsValid); + Assert.Equal("trust-roots-required", result.InitialState.Reason); + } + + [Fact] + public void ReturnsDefaultPlanWhenInputsProvided() + { + var planner = new BundleImportPlanner(); + var trust = new TrustRootConfig("/tmp/trust.json", new[] { "abc" }, new[] { "ed25519" }, null, null, new Dictionary()); + + var result = planner.CreatePlan("bundle.tar", trust); + + Assert.True(result.InitialState.IsValid); + Assert.Contains("verify-dsse-signature", result.Steps); + Assert.Equal("bundle.tar", result.Inputs["bundlePath"]); + } +} diff --git a/tests/AirGap/StellaOps.AirGap.Importer.Tests/DsseVerifierTests.cs b/tests/AirGap/StellaOps.AirGap.Importer.Tests/DsseVerifierTests.cs new file mode 100644 index 000000000..f479bc7db --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Importer.Tests/DsseVerifierTests.cs @@ -0,0 +1,71 @@ +using System.Security.Cryptography; +using StellaOps.AirGap.Importer.Contracts; +using StellaOps.AirGap.Importer.Validation; + +namespace StellaOps.AirGap.Importer.Tests; + +public class DsseVerifierTests +{ + [Fact] + public void FailsWhenUntrustedKey() + { + var verifier = new DsseVerifier(); + var envelope = new DsseEnvelope("text/plain", Convert.ToBase64String("hi"u8), new[] { new DsseSignature("k1", "sig") }); + var trust = TrustRootConfig.Empty("/tmp"); + + var result = verifier.Verify(envelope, trust); + + Assert.False(result.IsValid); + } + + [Fact] + public void VerifiesRsaPssSignature() + { + using var rsa = RSA.Create(2048); + var pub = rsa.ExportSubjectPublicKeyInfo(); + var payload = "hello-world"; + var payloadType = "application/vnd.stella.bundle"; + var pae = BuildPae(payloadType, payload); + var sig = rsa.SignData(pae, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); + + var envelope = new DsseEnvelope(payloadType, Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(payload)), new[] + { + new DsseSignature("k1", Convert.ToBase64String(sig)) + }); + + var trust = new TrustRootConfig( + "/tmp/root.json", + new[] { Fingerprint(pub) }, + new[] { "rsassa-pss-sha256" }, + null, + null, + new Dictionary { ["k1"] = pub }); + + var result = new DsseVerifier().Verify(envelope, trust); + + Assert.True(result.IsValid); + Assert.Equal("dsse-signature-verified", result.Reason); + } + + private static byte[] BuildPae(string payloadType, string payload) + { + var parts = new[] { "DSSEv1", payloadType, payload }; + var paeBuilder = new System.Text.StringBuilder(); + paeBuilder.Append("PAE:"); + paeBuilder.Append(parts.Length); + foreach (var part in parts) + { + paeBuilder.Append(' '); + paeBuilder.Append(part.Length); + paeBuilder.Append(' '); + paeBuilder.Append(part); + } + + return System.Text.Encoding.UTF8.GetBytes(paeBuilder.ToString()); + } + + private static string Fingerprint(byte[] pub) + { + return Convert.ToHexString(SHA256.HashData(pub)).ToLowerInvariant(); + } +} diff --git a/tests/AirGap/StellaOps.AirGap.Importer.Tests/ImportValidatorTests.cs b/tests/AirGap/StellaOps.AirGap.Importer.Tests/ImportValidatorTests.cs new file mode 100644 index 000000000..6ab2644a1 --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Importer.Tests/ImportValidatorTests.cs @@ -0,0 +1,92 @@ +using System.Security.Cryptography; +using StellaOps.AirGap.Importer.Contracts; +using StellaOps.AirGap.Importer.Validation; + +namespace StellaOps.AirGap.Importer.Tests; + +public class ImportValidatorTests +{ + [Fact] + public void FailsWhenTufInvalid() + { + var request = BuildRequest(rootJson: "{}", snapshotJson: "{}", timestampJson: "{}"); + var result = new ImportValidator().Validate(request); + Assert.False(result.IsValid); + Assert.StartsWith("tuf:", result.Reason); + } + + [Fact] + public void SucceedsWhenAllChecksPass() + { + var root = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\"}"; + var snapshot = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"meta\":{\"snapshot\":{\"hashes\":{\"sha256\":\"abc\"}}}}"; + var timestamp = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"snapshot\":{\"meta\":{\"hashes\":{\"sha256\":\"abc\"}}}}"; + + using var rsa = RSA.Create(2048); + var pub = rsa.ExportSubjectPublicKeyInfo(); + + var payload = "bundle-body"; + var payloadType = "application/vnd.stella.bundle"; + var pae = BuildPae(payloadType, payload); + var sig = rsa.SignData(pae, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); + + var envelope = new DsseEnvelope(payloadType, Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(payload)), new[] + { + new DsseSignature("k1", Convert.ToBase64String(sig)) + }); + + var trustStore = new TrustStore(); + trustStore.LoadActive(new Dictionary { ["k1"] = pub }); + trustStore.StagePending(new Dictionary { ["k2"] = pub }); + + var request = new ImportValidationRequest( + envelope, + new TrustRootConfig("/tmp/root.json", new[] { Fingerprint(pub) }, new[] { "rsassa-pss-sha256" }, null, null, new Dictionary { ["k1"] = pub }), + root, + snapshot, + timestamp, + new List { new("a.txt", new MemoryStream("data"u8.ToArray())) }, + trustStore, + new[] { "approver-1", "approver-2" }); + + var result = new ImportValidator().Validate(request); + + Assert.True(result.IsValid); + Assert.Equal("import-validated", result.Reason); + } + + private static byte[] BuildPae(string payloadType, string payload) + { + var parts = new[] { "DSSEv1", payloadType, payload }; + var paeBuilder = new System.Text.StringBuilder(); + paeBuilder.Append("PAE:"); + paeBuilder.Append(parts.Length); + foreach (var part in parts) + { + paeBuilder.Append(' '); + paeBuilder.Append(part.Length); + paeBuilder.Append(' '); + paeBuilder.Append(part); + } + + return System.Text.Encoding.UTF8.GetBytes(paeBuilder.ToString()); + } + + private static string Fingerprint(byte[] pub) => Convert.ToHexString(SHA256.HashData(pub)).ToLowerInvariant(); + + private static ImportValidationRequest BuildRequest(string rootJson, string snapshotJson, string timestampJson) + { + var envelope = new DsseEnvelope("text/plain", Convert.ToBase64String("hi"u8), Array.Empty()); + var trustRoot = TrustRootConfig.Empty("/tmp"); + var trustStore = new TrustStore(); + return new ImportValidationRequest( + envelope, + trustRoot, + rootJson, + snapshotJson, + timestampJson, + Array.Empty(), + trustStore, + Array.Empty()); + } +} diff --git a/tests/AirGap/StellaOps.AirGap.Importer.Tests/InMemoryBundleRepositoriesTests.cs b/tests/AirGap/StellaOps.AirGap.Importer.Tests/InMemoryBundleRepositoriesTests.cs new file mode 100644 index 000000000..dd4698b56 --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Importer.Tests/InMemoryBundleRepositoriesTests.cs @@ -0,0 +1,63 @@ +using StellaOps.AirGap.Importer.Models; +using StellaOps.AirGap.Importer.Repositories; + +namespace StellaOps.AirGap.Importer.Tests; + +public class InMemoryBundleRepositoriesTests +{ + [Fact] + public async Task CatalogUpsertOverwritesPerTenant() + { + var repo = new InMemoryBundleCatalogRepository(); + var entry1 = new BundleCatalogEntry("t1", "b1", "d1", DateTimeOffset.UnixEpoch, new[] { "a" }); + var entry2 = new BundleCatalogEntry("t1", "b1", "d2", DateTimeOffset.UnixEpoch.AddMinutes(1), new[] { "b" }); + + await repo.UpsertAsync(entry1, default); + await repo.UpsertAsync(entry2, default); + + var list = await repo.ListAsync("t1", default); + Assert.Single(list); + Assert.Equal("d2", list[0].Digest); + } + + [Fact] + public async Task CatalogIsTenantIsolated() + { + var repo = new InMemoryBundleCatalogRepository(); + await repo.UpsertAsync(new BundleCatalogEntry("t1", "b1", "d1", DateTimeOffset.UnixEpoch, Array.Empty()), default); + await repo.UpsertAsync(new BundleCatalogEntry("t2", "b1", "d2", DateTimeOffset.UnixEpoch, Array.Empty()), default); + + var t1 = await repo.ListAsync("t1", default); + Assert.Single(t1); + Assert.Equal("d1", t1[0].Digest); + } + + [Fact] + public async Task ItemsOrderedByPath() + { + var repo = new InMemoryBundleItemRepository(); + await repo.UpsertManyAsync(new[] + { + new BundleItem("t1", "b1", "b.txt", "d2", 10), + new BundleItem("t1", "b1", "a.txt", "d1", 5) + }, default); + + var list = await repo.ListByBundleAsync("t1", "b1", default); + Assert.Equal(new[] { "a.txt", "b.txt" }, list.Select(i => i.Path).ToArray()); + } + + [Fact] + public async Task ItemsTenantIsolated() + { + var repo = new InMemoryBundleItemRepository(); + await repo.UpsertManyAsync(new[] + { + new BundleItem("t1", "b1", "a.txt", "d1", 1), + new BundleItem("t2", "b1", "a.txt", "d2", 1) + }, default); + + var list = await repo.ListByBundleAsync("t1", "b1", default); + Assert.Single(list); + Assert.Equal("d1", list[0].Digest); + } +} diff --git a/tests/AirGap/StellaOps.AirGap.Importer.Tests/MerkleRootCalculatorTests.cs b/tests/AirGap/StellaOps.AirGap.Importer.Tests/MerkleRootCalculatorTests.cs new file mode 100644 index 000000000..6e03b97ce --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Importer.Tests/MerkleRootCalculatorTests.cs @@ -0,0 +1,28 @@ +using StellaOps.AirGap.Importer.Validation; + +namespace StellaOps.AirGap.Importer.Tests; + +public class MerkleRootCalculatorTests +{ + [Fact] + public void EmptySetProducesEmptyRoot() + { + var calc = new MerkleRootCalculator(); + var root = calc.ComputeRoot(Array.Empty()); + Assert.Equal(string.Empty, root); + } + + [Fact] + public void DeterministicAcrossOrder() + { + var calc = new MerkleRootCalculator(); + var a = new NamedStream("b.txt", new MemoryStream("two"u8.ToArray())); + var b = new NamedStream("a.txt", new MemoryStream("one"u8.ToArray())); + + var root1 = calc.ComputeRoot(new[] { a, b }); + var root2 = calc.ComputeRoot(new[] { b, a }); + + Assert.Equal(root1, root2); + Assert.NotEqual(string.Empty, root1); + } +} diff --git a/tests/AirGap/StellaOps.AirGap.Importer.Tests/RootRotationPolicyTests.cs b/tests/AirGap/StellaOps.AirGap.Importer.Tests/RootRotationPolicyTests.cs new file mode 100644 index 000000000..9e354fd4c --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Importer.Tests/RootRotationPolicyTests.cs @@ -0,0 +1,40 @@ +using StellaOps.AirGap.Importer.Validation; + +namespace StellaOps.AirGap.Importer.Tests; + +public class RootRotationPolicyTests +{ + [Fact] + public void RequiresTwoApprovers() + { + var policy = new RootRotationPolicy(); + var result = policy.Validate(new Dictionary(), new Dictionary { ["k1"] = new byte[] { 1 } }, new[] { "a" }); + Assert.False(result.IsValid); + Assert.Equal("rotation-dual-approval-required", result.Reason); + } + + [Fact] + public void RejectsNoChange() + { + var policy = new RootRotationPolicy(); + var result = policy.Validate( + new Dictionary { ["k1"] = new byte[] { 1 } }, + new Dictionary { ["k1"] = new byte[] { 1 } }, + new[] { "a", "b" }); + Assert.False(result.IsValid); + Assert.Equal("rotation-no-change", result.Reason); + } + + [Fact] + public void AcceptsRotationWithDualApproval() + { + var policy = new RootRotationPolicy(); + var result = policy.Validate( + new Dictionary { ["old"] = new byte[] { 1 } }, + new Dictionary { ["new"] = new byte[] { 2 } }, + new[] { "a", "b" }); + + Assert.True(result.IsValid); + Assert.Equal("rotation-approved", result.Reason); + } +} diff --git a/tests/AirGap/StellaOps.AirGap.Importer.Tests/StellaOps.AirGap.Importer.Tests.csproj b/tests/AirGap/StellaOps.AirGap.Importer.Tests/StellaOps.AirGap.Importer.Tests.csproj new file mode 100644 index 000000000..765d890a4 --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Importer.Tests/StellaOps.AirGap.Importer.Tests.csproj @@ -0,0 +1,16 @@ + + + net10.0 + false + enable + enable + + + + + + + + + + diff --git a/tests/AirGap/StellaOps.AirGap.Importer.Tests/TufMetadataValidatorTests.cs b/tests/AirGap/StellaOps.AirGap.Importer.Tests/TufMetadataValidatorTests.cs new file mode 100644 index 000000000..e79771a7f --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Importer.Tests/TufMetadataValidatorTests.cs @@ -0,0 +1,42 @@ +using StellaOps.AirGap.Importer.Validation; + +namespace StellaOps.AirGap.Importer.Tests; + +public class TufMetadataValidatorTests +{ + [Fact] + public void RejectsInvalidJson() + { + var validator = new TufMetadataValidator(); + var result = validator.Validate("{}", "{}", "{}"); + Assert.False(result.IsValid); + } + + [Fact] + public void AcceptsConsistentSnapshotHash() + { + var validator = new TufMetadataValidator(); + var root = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\"}"; + var snapshot = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"meta\":{\"snapshot\":{\"hashes\":{\"sha256\":\"abc\"}}}}"; + var timestamp = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"snapshot\":{\"meta\":{\"hashes\":{\"sha256\":\"abc\"}}}}"; + + var result = validator.Validate(root, snapshot, timestamp); + + Assert.True(result.IsValid); + Assert.Equal("tuf-metadata-valid", result.Reason); + } + + [Fact] + public void DetectsHashMismatch() + { + var validator = new TufMetadataValidator(); + var root = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\"}"; + var snapshot = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"meta\":{\"snapshot\":{\"hashes\":{\"sha256\":\"abc\"}}}}"; + var timestamp = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"snapshot\":{\"meta\":{\"hashes\":{\"sha256\":\"def\"}}}}"; + + var result = validator.Validate(root, snapshot, timestamp); + + Assert.False(result.IsValid); + Assert.Equal("tuf-snapshot-hash-mismatch", result.Reason); + } +} diff --git a/tests/AirGap/StellaOps.AirGap.Time.Tests/StalenessCalculatorTests.cs b/tests/AirGap/StellaOps.AirGap.Time.Tests/StalenessCalculatorTests.cs new file mode 100644 index 000000000..f4e97c4b3 --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Time.Tests/StalenessCalculatorTests.cs @@ -0,0 +1,43 @@ +using StellaOps.AirGap.Time.Models; +using StellaOps.AirGap.Time.Services; + +namespace StellaOps.AirGap.Time.Tests; + +public class StalenessCalculatorTests +{ + [Fact] + public void UnknownWhenNoAnchor() + { + var calc = new StalenessCalculator(); + var result = calc.Evaluate(TimeAnchor.Unknown, StalenessBudget.Default, DateTimeOffset.UnixEpoch); + Assert.False(result.IsWarning); + Assert.False(result.IsBreach); + } + + [Fact] + public void BreachWhenBeyondBudget() + { + var anchor = new TimeAnchor(DateTimeOffset.UnixEpoch, "source", "fmt", "fp", "digest"); + var budget = new StalenessBudget(10, 20); + var calc = new StalenessCalculator(); + + var result = calc.Evaluate(anchor, budget, DateTimeOffset.UnixEpoch.AddSeconds(25)); + + Assert.True(result.IsBreach); + Assert.True(result.IsWarning); + Assert.Equal(25, result.AgeSeconds); + } + + [Fact] + public void WarningWhenBetweenWarningAndBreach() + { + var anchor = new TimeAnchor(DateTimeOffset.UnixEpoch, "source", "fmt", "fp", "digest"); + var budget = new StalenessBudget(10, 20); + var calc = new StalenessCalculator(); + + var result = calc.Evaluate(anchor, budget, DateTimeOffset.UnixEpoch.AddSeconds(15)); + + Assert.True(result.IsWarning); + Assert.False(result.IsBreach); + } +} diff --git a/tests/AirGap/StellaOps.AirGap.Time.Tests/StellaOps.AirGap.Time.Tests.csproj b/tests/AirGap/StellaOps.AirGap.Time.Tests/StellaOps.AirGap.Time.Tests.csproj new file mode 100644 index 000000000..54e656c84 --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Time.Tests/StellaOps.AirGap.Time.Tests.csproj @@ -0,0 +1,16 @@ + + + net10.0 + false + enable + enable + + + + + + + + + + diff --git a/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeAnchorLoaderTests.cs b/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeAnchorLoaderTests.cs new file mode 100644 index 000000000..2a3961adb --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeAnchorLoaderTests.cs @@ -0,0 +1,28 @@ +using StellaOps.AirGap.Time.Parsing; +using StellaOps.AirGap.Time.Services; + +namespace StellaOps.AirGap.Time.Tests; + +public class TimeAnchorLoaderTests +{ + [Fact] + public void RejectsInvalidHex() + { + var loader = new TimeAnchorLoader(); + var result = loader.TryLoadHex("not-hex", TimeTokenFormat.Roughtime, Array.Empty(), out _); + Assert.False(result.IsValid); + Assert.Equal("token-hex-invalid", result.Reason); + } + + [Fact] + public void LoadsHexToken() + { + var loader = new TimeAnchorLoader(); + var hex = "01020304"; + var trust = new[] { new TimeTrustRoot("k1", new byte[] { 0x01 }, "rsassa-pss-sha256") }; + var result = loader.TryLoadHex(hex, TimeTokenFormat.Roughtime, trust, out var anchor); + + Assert.True(result.IsValid); + Assert.Equal("Roughtime", anchor.Format); + } +} diff --git a/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeStatusDtoTests.cs b/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeStatusDtoTests.cs new file mode 100644 index 000000000..d6b95b7bf --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeStatusDtoTests.cs @@ -0,0 +1,20 @@ +using StellaOps.AirGap.Time.Models; + +namespace StellaOps.AirGap.Time.Tests; + +public class TimeStatusDtoTests +{ + [Fact] + public void SerializesDeterministically() + { + var status = new TimeStatus( + new TimeAnchor(DateTimeOffset.Parse("2025-01-01T00:00:00Z"), "source", "fmt", "fp", "digest"), + new StalenessEvaluation(42, 10, 20, true, false), + new StalenessBudget(10, 20), + DateTimeOffset.Parse("2025-01-02T00:00:00Z")); + + var json = TimeStatusDto.FromStatus(status).ToJson(); + + Assert.Equal("{\"anchorTime\":\"2025-01-01T00:00:00.0000000Z\",\"format\":\"fmt\",\"source\":\"source\",\"fingerprint\":\"fp\",\"digest\":\"digest\",\"ageSeconds\":42,\"warningSeconds\":10,\"breachSeconds\":20,\"isWarning\":true,\"isBreach\":false,\"evaluatedAtUtc\":\"2025-01-02T00:00:00.0000000Z\"}", json); + } +} diff --git a/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeStatusServiceTests.cs b/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeStatusServiceTests.cs new file mode 100644 index 000000000..0f4bf8b08 --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeStatusServiceTests.cs @@ -0,0 +1,34 @@ +using StellaOps.AirGap.Time.Models; +using StellaOps.AirGap.Time.Services; +using StellaOps.AirGap.Time.Stores; + +namespace StellaOps.AirGap.Time.Tests; + +public class TimeStatusServiceTests +{ + [Fact] + public async Task ReturnsUnknownWhenNoAnchor() + { + var svc = Build(); + var status = await svc.GetStatusAsync("t1", DateTimeOffset.UnixEpoch); + Assert.Equal(TimeAnchor.Unknown, status.Anchor); + Assert.False(status.Staleness.IsWarning); + } + + [Fact] + public async Task PersistsAnchorAndBudget() + { + var svc = Build(); + var anchor = new TimeAnchor(DateTimeOffset.UnixEpoch, "source", "fmt", "fp", "digest"); + var budget = new StalenessBudget(10, 20); + + await svc.SetAnchorAsync("t1", anchor, budget); + var status = await svc.GetStatusAsync("t1", DateTimeOffset.UnixEpoch.AddSeconds(15)); + + Assert.Equal(anchor, status.Anchor); + Assert.True(status.Staleness.IsWarning); + Assert.False(status.Staleness.IsBreach); + } + + private static TimeStatusService Build() => new(new InMemoryTimeAnchorStore(), new StalenessCalculator()); +} diff --git a/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeTokenParserTests.cs b/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeTokenParserTests.cs new file mode 100644 index 000000000..32e444d03 --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeTokenParserTests.cs @@ -0,0 +1,34 @@ +using StellaOps.AirGap.Time.Models; +using StellaOps.AirGap.Time.Parsing; + +namespace StellaOps.AirGap.Time.Tests; + +public class TimeTokenParserTests +{ + [Fact] + public void EmptyTokenFails() + { + var parser = new TimeTokenParser(); + var result = parser.TryParse(Array.Empty(), TimeTokenFormat.Roughtime, out var anchor); + + Assert.False(result.IsValid); + Assert.Equal("token-empty", result.Reason); + Assert.Equal(TimeAnchor.Unknown, anchor); + } + + [Fact] + public void RoughtimeTokenProducesDigest() + { + var parser = new TimeTokenParser(); + var token = new byte[] { 0x01, 0x02, 0x03 }; + + var result = parser.TryParse(token, TimeTokenFormat.Roughtime, out var anchor); + + Assert.True(result.IsValid); + Assert.Equal("Roughtime", anchor.Format); + Assert.Equal("roughtime-token", anchor.Source); + Assert.Equal("structure-stubbed", result.Reason); + Assert.Matches("^[0-9a-f]{64}$", anchor.TokenDigest); + Assert.NotEqual(DateTimeOffset.UnixEpoch, anchor.AnchorTime); // deterministic derivation + } +} diff --git a/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeVerificationServiceTests.cs b/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeVerificationServiceTests.cs new file mode 100644 index 000000000..135a920f1 --- /dev/null +++ b/tests/AirGap/StellaOps.AirGap.Time.Tests/TimeVerificationServiceTests.cs @@ -0,0 +1,28 @@ +using StellaOps.AirGap.Time.Models; +using StellaOps.AirGap.Time.Parsing; +using StellaOps.AirGap.Time.Services; + +namespace StellaOps.AirGap.Time.Tests; + +public class TimeVerificationServiceTests +{ + [Fact] + public void FailsWithoutTrustRoots() + { + var svc = new TimeVerificationService(); + var result = svc.Verify(new byte[] { 0x01 }, TimeTokenFormat.Roughtime, Array.Empty(), out _); + Assert.False(result.IsValid); + Assert.Equal("trust-roots-required", result.Reason); + } + + [Fact] + public void SucceedsForRoughtimeWithTrustRoot() + { + var svc = new TimeVerificationService(); + var trust = new[] { new TimeTrustRoot("k1", new byte[] { 0x01 }, "rsassa-pss-sha256") }; + var result = svc.Verify(new byte[] { 0x01, 0x02 }, TimeTokenFormat.Roughtime, trust, out var anchor); + Assert.True(result.IsValid); + Assert.Equal("Roughtime", anchor.Format); + Assert.Equal("k1", anchor.SignatureFingerprint); + } +} diff --git a/tools/offline/fetch-sbomservice-deps.sh b/tools/offline/fetch-sbomservice-deps.sh new file mode 100644 index 000000000..46e0aa9c4 --- /dev/null +++ b/tools/offline/fetch-sbomservice-deps.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +PACKAGES_DIR="$ROOT/local-nugets/packages" +TMP_DIR="$ROOT/tmp/sbomservice-feed" +PROJECT="$TMP_DIR/probe.csproj" + +mkdir -p "$TMP_DIR" "$PACKAGES_DIR" + +cat > "$PROJECT" <<'CS' + + + net10.0 + + + + + + +CS + +dotnet restore "$PROJECT" \ + --packages "$PACKAGES_DIR" \ + --ignore-failed-sources \ + /p:RestoreUseStaticGraphEvaluation=true \ + /p:RestorePackagesWithLockFile=false + +find "$PACKAGES_DIR" -name '*.nupkg' -maxdepth 5 -type f -printf '%P\n' | sort