diff --git a/.gitea/workflows/build-test-deploy.yml b/.gitea/workflows/build-test-deploy.yml index 09b4287a..3ee81d74 100644 --- a/.gitea/workflows/build-test-deploy.yml +++ b/.gitea/workflows/build-test-deploy.yml @@ -35,7 +35,22 @@ env: CI_CACHE_ROOT: /data/.cache/stella-ops/feedser RUNNER_TOOL_CACHE: /toolcache -jobs: +jobs: + profile-validation: + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Helm + run: | + curl -fsSL https://get.helm.sh/helm-v3.16.0-linux-amd64.tar.gz -o /tmp/helm.tgz + tar -xzf /tmp/helm.tgz -C /tmp + sudo install -m 0755 /tmp/linux-amd64/helm /usr/local/bin/helm + + - name: Validate deployment profiles + run: ./deploy/tools/validate-profiles.sh + build-test: runs-on: ubuntu-22.04 environment: ${{ github.event_name == 'pull_request' && 'preview' || 'staging' }} @@ -61,15 +76,82 @@ jobs: - name: Build solution (warnings as errors) run: dotnet build src/StellaOps.Feedser.sln --configuration $BUILD_CONFIGURATION --no-restore -warnaserror - - name: Run unit and integration tests - run: | - mkdir -p "$TEST_RESULTS_DIR" - dotnet test src/StellaOps.Feedser.sln \ - --configuration $BUILD_CONFIGURATION \ - --no-build \ - --logger "trx;LogFileName=stellaops-feedser-tests.trx" \ - --results-directory "$TEST_RESULTS_DIR" - + - name: Run unit and integration tests + run: | + mkdir -p "$TEST_RESULTS_DIR" + dotnet test src/StellaOps.Feedser.sln \ + --configuration $BUILD_CONFIGURATION \ + --no-build \ + --logger "trx;LogFileName=stellaops-feedser-tests.trx" \ + --results-directory "$TEST_RESULTS_DIR" + + - name: Publish BuildX SBOM generator + run: | + dotnet publish src/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj \ + --configuration $BUILD_CONFIGURATION \ + --output out/buildx + + - name: Verify BuildX descriptor determinism + run: | + dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll handshake \ + --manifest out/buildx \ + --cas out/cas + + cat <<'JSON' > out/buildx-sbom.cdx.json +{"bomFormat":"CycloneDX","specVersion":"1.5"} +JSON + + dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll descriptor \ + --manifest out/buildx \ + --image sha256:5c2c5bfe0d4d77f1a0f9866fd415dd8da5b62af05d7c3d4b53f28de3ebef0101 \ + --sbom out/buildx-sbom.cdx.json \ + --sbom-name buildx-sbom.cdx.json \ + --artifact-type application/vnd.stellaops.sbom.layer+json \ + --sbom-format cyclonedx-json \ + --sbom-kind inventory \ + --repository ${{ github.repository }} \ + --build-ref ${{ github.sha }} \ + > out/buildx-descriptor.json + + dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll descriptor \ + --manifest out/buildx \ + --image sha256:5c2c5bfe0d4d77f1a0f9866fd415dd8da5b62af05d7c3d4b53f28de3ebef0101 \ + --sbom out/buildx-sbom.cdx.json \ + --sbom-name buildx-sbom.cdx.json \ + --artifact-type application/vnd.stellaops.sbom.layer+json \ + --sbom-format cyclonedx-json \ + --sbom-kind inventory \ + --repository ${{ github.repository }} \ + --build-ref ${{ github.sha }} \ + > out/buildx-descriptor-repeat.json + + python - <<'PY' +import json, sys +from pathlib import Path + +def normalize(path: str) -> dict: + data = json.loads(Path(path).read_text(encoding='utf-8')) + data.pop('generatedAt', None) + return data + +baseline = normalize('out/buildx-descriptor.json') +repeat = normalize('out/buildx-descriptor-repeat.json') + +if baseline != repeat: + sys.exit('BuildX descriptor output changed between runs.') +PY + + - name: Upload BuildX determinism artifacts + uses: actions/upload-artifact@v4 + with: + name: buildx-determinism + path: | + out/buildx-descriptor.json + out/buildx-descriptor-repeat.json + out/buildx-sbom.cdx.json + if-no-files-found: error + retention-days: 7 + - name: Publish Feedser web service run: | mkdir -p "$PUBLISH_DIR" diff --git a/SPRINTS.md b/SPRINTS.md index 17bb5faa..87bb31c5 100644 --- a/SPRINTS.md +++ b/SPRINTS.md @@ -1,340 +1,343 @@ -This file describe implementation of Stella Ops (docs/README.md). Implementation must respect rules from AGENTS.md (read if you have not). - -| Sprint | Theme | Tasks File Path | Status | Type of Specialist | Task ID | Task Description | -| --- | --- | --- | --- | --- | --- | --- | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Models/TASKS.md | DONE (2025-10-12) | Team Models & Merge Leads | FEEDMODELS-SCHEMA-01-001 | SemVer primitive range-style metadata
Instructions to work:
DONE Read ./AGENTS.md and src/StellaOps.Concelier.Models/AGENTS.md. This task lays the groundwork—complete the SemVer helper updates before teammates pick up FEEDMODELS-SCHEMA-01-002/003 and FEEDMODELS-SCHEMA-02-900. Use ./src/FASTER_MODELING_AND_NORMALIZATION.md for the target rule structure. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Models/TASKS.md | DONE (2025-10-11) | Team Models & Merge Leads | FEEDMODELS-SCHEMA-01-002 | Provenance decision rationale field
Instructions to work:
AdvisoryProvenance now carries `decisionReason` and docs/tests were updated. Connectors and merge tasks should populate the field when applying precedence/freshness/tie-breaker logic; see src/StellaOps.Concelier.Models/PROVENANCE_GUIDELINES.md for usage guidance. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Models/TASKS.md | DONE (2025-10-11) | Team Models & Merge Leads | FEEDMODELS-SCHEMA-01-003 | Normalized version rules collection
Instructions to work:
`AffectedPackage.NormalizedVersions` and supporting comparer/docs/tests shipped. Connector owners must emit rule arrays per ./src/FASTER_MODELING_AND_NORMALIZATION.md and report progress via FEEDMERGE-COORD-02-900 so merge/storage backfills can proceed. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Models/TASKS.md | DONE (2025-10-12) | Team Models & Merge Leads | FEEDMODELS-SCHEMA-02-900 | Range primitives for SemVer/EVR/NEVRA metadata
Instructions to work:
DONE Read ./AGENTS.md and src/StellaOps.Concelier.Models/AGENTS.md before resuming this stalled effort. Confirm helpers align with the new `NormalizedVersions` representation so connectors finishing in Sprint 2 can emit consistent metadata. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Normalization/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDNORM-NORM-02-001 | SemVer normalized rule emitter
Shared `SemVerRangeRuleBuilder` now outputs primitives + normalized rules per `FASTER_MODELING_AND_NORMALIZATION.md`; CVE/GHSA connectors consuming the API have verified fixtures. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-02-001 | Normalized range dual-write + backfill
AdvisoryStore dual-writes flattened `normalizedVersions` when `concelier.storage.enableSemVerStyle` is set; migration `20251011-semver-style-backfill` updates historical records and docs outline the rollout. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-02-002 | Provenance decision reason persistence
Storage now persists `provenance.decisionReason` for advisories and merge events; tests cover round-trips. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-02-003 | Normalized versions indexing
Bootstrapper seeds compound/sparse indexes for flattened normalized rules and `docs/dev/mongo_indices.md` documents query guidance. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-TESTS-02-004 | Restore AdvisoryStore build after normalized versions refactor
Updated constructors/tests keep storage suites passing with the new feature flag defaults. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-ENGINE-01-002 | Plumb Authority client resilience options
WebService wires `authority.resilience.*` into `AddStellaOpsAuthClient` and adds binding coverage via `AuthorityClientResilienceOptionsAreBound`. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-DOCS-01-003 | Author ops guidance for resilience tuning
Install/runbooks document connected vs air-gapped resilience profiles and monitoring hooks. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-DOCS-01-004 | Document authority bypass logging patterns
Operator guides now call out `route/status/subject/clientId/scopes/bypass/remote` audit fields and SIEM triggers. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-DOCS-01-005 | Update Concelier operator guide for enforcement cutoff
Install guide reiterates the 2025-12-31 cutoff and links audit signals to the rollout checklist. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Authority/TASKS.md | DONE (2025-10-11) | Team WebService & Authority | SEC3.HOST | Rate limiter policy binding
Authority host now applies configuration-driven fixed windows to `/token`, `/authorize`, and `/internal/*`; integration tests assert 429 + `Retry-After` headers; docs/config samples refreshed for Docs guild diagrams. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Authority/TASKS.md | DONE (2025-10-11) | Team WebService & Authority | SEC3.BUILD | Authority rate-limiter follow-through
`Security.RateLimiting` now fronts token/authorize/internal limiters; Authority + Configuration matrices (`dotnet test src/StellaOps.Authority/StellaOps.Authority.sln`, `dotnet test src/StellaOps.Configuration.Tests/StellaOps.Configuration.Tests.csproj`) passed on 2025-10-11; awaiting #authority-core broadcast. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Authority/TASKS.md | DONE (2025-10-14) | Team Authority Platform & Security Guild | AUTHCORE-BUILD-OPENIDDICT / AUTHCORE-STORAGE-DEVICE-TOKENS / AUTHCORE-BOOTSTRAP-INVITES | Address remaining Authority compile blockers (OpenIddict transaction shim, token device document, bootstrap invite cleanup) so `dotnet build src/StellaOps.Authority.sln` returns success. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md | DONE (2025-10-11) | Team WebService & Authority | PLG6.DOC | Plugin developer guide polish
Section 9 now documents rate limiter metadata, config keys, and lockout interplay; YAML samples updated alongside Authority config templates. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-001 | Fetch pipeline & state tracking
Summary planner now drives monthly/yearly VINCE fetches, persists pending summaries/notes, and hydrates VINCE detail queue with telemetry.
Team instructions: Read ./AGENTS.md and src/StellaOps.Concelier.Connector.CertCc/AGENTS.md. Coordinate daily with Models/Merge leads so new normalizedVersions output and provenance tags stay aligned with ./src/FASTER_MODELING_AND_NORMALIZATION.md. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-002 | VINCE note detail fetcher
Summary planner queues VINCE note detail endpoints, persists raw JSON with SHA/ETag metadata, and records retry/backoff metrics. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-003 | DTO & parser implementation
Added VINCE DTO aggregate, Markdown→text sanitizer, vendor/status/vulnerability parsers, and parser regression fixture. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-004 | Canonical mapping & range primitives
VINCE DTO aggregate flows through `CertCcMapper`, emitting vendor range primitives + normalized version rules that persist via `_advisoryStore`. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-12) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-005 | Deterministic fixtures/tests
Snapshot harness refreshed 2025-10-12; `certcc-*.snapshot.json` regenerated and regression suite green without UPDATE flag drift. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-12) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-006 | Telemetry & documentation
`CertCcDiagnostics` publishes summary/detail/parse/map metrics (meter `StellaOps.Concelier.Connector.CertCc`), README documents instruments, and log guidance captured for Ops on 2025-10-12. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-12) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-007 | Connector test harness remediation
Harness now wires `AddSourceCommon`, resets `FakeTimeProvider`, and passes canned-response regression run dated 2025-10-12. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-008 | Snapshot coverage handoff
Fixtures regenerated with normalized ranges + provenance fields on 2025-10-11; QA handoff notes published and merge backfill unblocked. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-12) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-012 | Schema sync & snapshot regen follow-up
Fixtures regenerated with normalizedVersions + provenance decision reasons; handoff notes updated for Merge backfill 2025-10-12. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-009 | Detail/map reintegration plan
Staged reintegration plan published in `src/StellaOps.Concelier.Connector.CertCc/FEEDCONN-CERTCC-02-009_PLAN.md`; coordinates enablement with FEEDCONN-CERTCC-02-004. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-12) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-010 | Partial-detail graceful degradation
Detail fetch now tolerates 404/403/410 responses and regression tests cover mixed endpoint availability. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Distro.RedHat/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-REDHAT-02-001 | Fixture validation sweep
Instructions to work:
Fixtures regenerated post-model-helper rollout; provenance ordering and normalizedVersions scaffolding verified via tests. Conflict resolver deltas logged in src/StellaOps.Concelier.Connector.Distro.RedHat/CONFLICT_RESOLVER_NOTES.md for Sprint 3 consumers. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Vndr.Apple/TASKS.md | DONE (2025-10-12) | Team Vendor Apple Specialists | FEEDCONN-APPLE-02-001 | Canonical mapping & range primitives
Mapper emits SemVer rules (`scheme=apple:*`); fixtures regenerated with trimmed references + new RSR coverage, update tooling finalized. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Vndr.Apple/TASKS.md | DONE (2025-10-11) | Team Vendor Apple Specialists | FEEDCONN-APPLE-02-002 | Deterministic fixtures/tests
Sanitized live fixtures + regression snapshots wired into tests; normalized rule coverage asserted. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Vndr.Apple/TASKS.md | DONE (2025-10-11) | Team Vendor Apple Specialists | FEEDCONN-APPLE-02-003 | Telemetry & documentation
Apple meter metrics wired into Concelier WebService OpenTelemetry configuration; README and fixtures document normalizedVersions coverage. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Vndr.Apple/TASKS.md | DONE (2025-10-12) | Team Vendor Apple Specialists | FEEDCONN-APPLE-02-004 | Live HTML regression sweep
Sanitised HT125326/HT125328/HT106355/HT214108/HT215500 fixtures recorded and regression tests green on 2025-10-12. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Vndr.Apple/TASKS.md | DONE (2025-10-11) | Team Vendor Apple Specialists | FEEDCONN-APPLE-02-005 | Fixture regeneration tooling
`UPDATE_APPLE_FIXTURES=1` flow fetches & rewrites fixtures; README documents usage.
Instructions to work:
DONE Read ./AGENTS.md and src/StellaOps.Concelier.Connector.Vndr.Apple/AGENTS.md. Resume stalled tasks, ensuring normalizedVersions output and fixtures align with ./src/FASTER_MODELING_AND_NORMALIZATION.md before handing data to the conflict sprint. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-12) | Team Connector Normalized Versions Rollout | FEEDCONN-GHSA-02-001 | GHSA normalized versions & provenance
Team instructions: Read ./AGENTS.md and each module's AGENTS file. Adopt the `NormalizedVersions` array emitted by the models sprint, wiring provenance `decisionReason` where merge overrides occur. Follow ./src/FASTER_MODELING_AND_NORMALIZATION.md; report via src/StellaOps.Concelier.Merge/TASKS.md (FEEDMERGE-COORD-02-900). Progress 2025-10-11: GHSA/OSV emit normalized arrays with refreshed fixtures; CVE mapper now surfaces SemVer normalized ranges; NVD/KEV adoption pending; outstanding follow-ups include FEEDSTORAGE-DATA-02-001, FEEDMERGE-ENGINE-02-002, and rolling `tools/FixtureUpdater` updates across connectors. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-12) | Team Connector Normalized Versions Rollout | FEEDCONN-OSV-02-003 | OSV normalized versions & freshness | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Nvd/TASKS.md | DONE (2025-10-12) | Team Connector Normalized Versions Rollout | FEEDCONN-NVD-02-002 | NVD normalized versions & timestamps | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Cve/TASKS.md | DONE (2025-10-12) | Team Connector Normalized Versions Rollout | FEEDCONN-CVE-02-003 | CVE normalized versions uplift | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Kev/TASKS.md | DONE (2025-10-12) | Team Connector Normalized Versions Rollout | FEEDCONN-KEV-02-003 | KEV normalized versions propagation | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-12) | Team Connector Normalized Versions Rollout | FEEDCONN-OSV-04-003 | OSV parity fixture refresh | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-10) | Team WebService & Authority | FEEDWEB-DOCS-01-001 | Document authority toggle & scope requirements
Quickstart carries toggle/scope guidance pending docs guild review (no change this sprint). | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-ENGINE-01-002 | Plumb Authority client resilience options
WebService wires `authority.resilience.*` into `AddStellaOpsAuthClient` and adds binding coverage via `AuthorityClientResilienceOptionsAreBound`. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-DOCS-01-003 | Author ops guidance for resilience tuning
Operator docs now outline connected vs air-gapped resilience profiles and monitoring cues. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-DOCS-01-004 | Document authority bypass logging patterns
Audit logging guidance highlights `route/status/subject/clientId/scopes/bypass/remote` fields and SIEM alerts. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-DOCS-01-005 | Update Concelier operator guide for enforcement cutoff
Install guide reiterates the 2025-12-31 cutoff and ties audit signals to rollout checks. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-11) | Team WebService & Authority | FEEDWEB-OPS-01-006 | Rename plugin drop directory to namespaced path
Build outputs, tests, and docs now target `StellaOps.Concelier.PluginBinaries`/`StellaOps.Authority.PluginBinaries`. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-11) | Team WebService & Authority | FEEDWEB-OPS-01-007 | Authority resilience adoption
Deployment docs and CLI notes explain the LIB5 resilience knobs for rollout.
Instructions to work:
DONE Read ./AGENTS.md and src/StellaOps.Concelier.WebService/AGENTS.md. These items were mid-flight; resume implementation ensuring docs/operators receive timely updates. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Authority/TASKS.md | DONE (2025-10-11) | Team Authority Platform & Security Guild | AUTHCORE-ENGINE-01-001 | CORE8.RL — Rate limiter plumbing validated; integration tests green and docs handoff recorded for middleware ordering + Retry-After headers (see `docs/dev/authority-rate-limit-tuning-outline.md` for continuing guidance). | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Cryptography/TASKS.md | DONE (2025-10-11) | Team Authority Platform & Security Guild | AUTHCRYPTO-ENGINE-01-001 | SEC3.A — Shared metadata resolver confirmed via host test run; SEC3.B now unblocked for tuning guidance (outline captured in `docs/dev/authority-rate-limit-tuning-outline.md`). | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Cryptography/TASKS.md | DONE (2025-10-13) | Team Authority Platform & Security Guild | AUTHSEC-DOCS-01-002 | SEC3.B — Published `docs/security/rate-limits.md` with tuning matrix, alert thresholds, and lockout interplay guidance; Docs guild can lift copy into plugin guide. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Cryptography/TASKS.md | DONE (2025-10-14) | Team Authority Platform & Security Guild | AUTHSEC-CRYPTO-02-001 | SEC5.B1 — Introduce libsodium signing provider and parity tests to unblock CLI verification enhancements. | -| Sprint 1 | Bootstrap & Replay Hardening | src/StellaOps.Cryptography/TASKS.md | DONE (2025-10-14) | Security Guild | AUTHSEC-CRYPTO-02-004 | SEC5.D/E — Finish bootstrap invite lifecycle (API/store/cleanup) and token device heuristics; build currently red due to pending handler integration. | -| Sprint 1 | Developer Tooling | src/StellaOps.Cli/TASKS.md | DONE (2025-10-15) | DevEx/CLI | AUTHCLI-DIAG-01-001 | Surface password policy diagnostics in CLI startup/output so operators see weakened overrides immediately.
CLI now loads Authority plug-ins at startup, logs weakened password policies (length/complexity), and regression coverage lives in `StellaOps.Cli.Tests/Services/AuthorityDiagnosticsReporterTests`. | -| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md | DONE (2025-10-11) | Team Authority Platform & Security Guild | AUTHPLUG-DOCS-01-001 | PLG6.DOC — Developer guide copy + diagrams merged 2025-10-11; limiter guidance incorporated and handed to Docs guild for asset export. | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Normalization/TASKS.md | DONE (2025-10-12) | Team Normalization & Storage Backbone | FEEDNORM-NORM-02-001 | SemVer normalized rule emitter
`SemVerRangeRuleBuilder` shipped 2025-10-12 with comparator/`||` support and fixtures aligning to `FASTER_MODELING_AND_NORMALIZATION.md`. | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-02-001 | Normalized range dual-write + backfill | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-02-002 | Provenance decision reason persistence | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-02-003 | Normalized versions indexing
Indexes seeded + docs updated 2025-10-11 to cover flattened normalized rules for connector adoption. | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDMERGE-ENGINE-02-002 | Normalized versions union & dedupe
Affected package resolver unions/dedupes normalized rules, stamps merge provenance with `decisionReason`, and tests cover the rollout. | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-11) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-GHSA-02-001 | GHSA normalized versions & provenance | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-11) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-GHSA-02-004 | GHSA credits & ecosystem severity mapping | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-12) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-GHSA-02-005 | GitHub quota monitoring & retries | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-12) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-GHSA-02-006 | Production credential & scheduler rollout | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-12) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-GHSA-02-007 | Credit parity regression fixtures | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Nvd/TASKS.md | DONE (2025-10-11) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-NVD-02-002 | NVD normalized versions & timestamps | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Nvd/TASKS.md | DONE (2025-10-11) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-NVD-02-004 | NVD CVSS & CWE precedence payloads | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Nvd/TASKS.md | DONE (2025-10-12) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-NVD-02-005 | NVD merge/export parity regression | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-11) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-OSV-02-003 | OSV normalized versions & freshness | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-11) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-OSV-02-004 | OSV references & credits alignment | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-12) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-OSV-02-005 | Fixture updater workflow
Resolved 2025-10-12: OSV mapper now derives canonical PURLs for Go + scoped npm packages when raw payloads omit `purl`; conflict fixtures unchanged for invalid npm names. Verified via `dotnet test src/StellaOps.Concelier.Connector.Osv.Tests`, `src/StellaOps.Concelier.Connector.Ghsa.Tests`, `src/StellaOps.Concelier.Connector.Nvd.Tests`, and backbone normalization/storage suites. | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Acsc/TASKS.md | DONE (2025-10-12) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-ACSC-02-001 … 02-008 | Fetch→parse→map pipeline, fixtures, diagnostics, and README finished 2025-10-12; downstream export parity captured via FEEDEXPORT-JSON-04-001 / FEEDEXPORT-TRIVY-04-001 (completed). | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Cccs/TASKS.md | DONE (2025-10-16) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-CCCS-02-001 … 02-008 | Observability meter, historical harvest plan, and DOM sanitizer refinements wrapped; ops notes live under `docs/ops/concelier-cccs-operations.md` with fixtures validating EN/FR list handling. | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.CertBund/TASKS.md | DONE (2025-10-15) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-CERTBUND-02-001 … 02-008 | Telemetry/docs (02-006) and history/locale sweep (02-007) completed alongside pipeline; runbook `docs/ops/concelier-certbund-operations.md` captures locale guidance and offline packaging. | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Kisa/TASKS.md | DONE (2025-10-14) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-KISA-02-001 … 02-007 | Connector, tests, and telemetry/docs (02-006) finalized; localisation notes in `docs/dev/kisa_connector_notes.md` complete rollout. | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ru.Bdu/TASKS.md | DONE (2025-10-14) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-RUBDU-02-001 … 02-008 | Fetch/parser/mapper refinements, regression fixtures, telemetry/docs, access options, and trusted root packaging all landed; README documents offline access strategy. | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ru.Nkcki/TASKS.md | DONE (2025-10-13) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-NKCKI-02-001 … 02-008 | Listing fetch, parser, mapper, fixtures, telemetry/docs, and archive plan finished; Mongo2Go/libcrypto dependency resolved via bundled OpenSSL noted in ops guide. | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ics.Cisa/TASKS.md | DONE (2025-10-16) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-ICSCISA-02-001 … 02-011 | Feed parser attachment fixes, SemVer exact values, regression suites, telemetry/docs updates, and handover complete; ops runbook now details attachment verification + proxy usage. | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Vndr.Cisco/TASKS.md | DONE (2025-10-14) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-CISCO-02-001 … 02-007 | OAuth fetch pipeline, DTO/mapping, tests, and telemetry/docs shipped; monitoring/export integration follow-ups recorded in Ops docs and exporter backlog (completed). | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Vndr.Msrc/TASKS.md | DONE (2025-10-15) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-MSRC-02-001 … 02-008 | Azure AD onboarding (02-008) unblocked fetch/parse/map pipeline; fixtures, telemetry/docs, and Offline Kit guidance published in `docs/ops/concelier-msrc-operations.md`. | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Cve/TASKS.md | DONE (2025-10-15) | Team Connector Support & Monitoring | FEEDCONN-CVE-02-001 … 02-002 | CVE data-source selection, fetch pipeline, and docs landed 2025-10-10. 2025-10-15: smoke verified using the seeded mirror fallback; connector now logs a warning and pulls from `seed-data/cve/` until live CVE Services credentials arrive. | -| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Kev/TASKS.md | DONE (2025-10-12) | Team Connector Support & Monitoring | FEEDCONN-KEV-02-001 … 02-002 | KEV catalog ingestion, fixtures, telemetry, and schema validation completed 2025-10-12; ops dashboard published. | -| Sprint 2 | Connector & Data Implementation Wave | docs/TASKS.md | DONE (2025-10-11) | Team Docs & Knowledge Base | FEEDDOCS-DOCS-01-001 | Canonical schema docs refresh
Updated canonical schema + provenance guides with SemVer style, normalized version rules, decision reason change log, and migration notes. | -| Sprint 2 | Connector & Data Implementation Wave | docs/TASKS.md | DONE (2025-10-11) | Team Docs & Knowledge Base | FEEDDOCS-DOCS-02-001 | Concelier-SemVer Playbook
Published merge playbook covering mapper patterns, dedupe flow, indexes, and rollout checklist. | -| Sprint 2 | Connector & Data Implementation Wave | docs/TASKS.md | DONE (2025-10-11) | Team Docs & Knowledge Base | FEEDDOCS-DOCS-02-002 | Normalized versions query guide
Delivered Mongo index/query addendum with `$unwind` recipes, dedupe checks, and operational checklist.
Instructions to work:
DONE Read ./AGENTS.md and docs/AGENTS.md. Document every schema/index/query change produced in Sprint 1-2 leveraging ./src/FASTER_MODELING_AND_NORMALIZATION.md. | -| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Core/TASKS.md | DONE (2025-10-11) | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-03-001 | Canonical merger implementation
`CanonicalMerger` ships with freshness/tie-breaker logic, provenance, and unit coverage feeding Merge. | -| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Core/TASKS.md | DONE (2025-10-11) | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-03-002 | Field precedence and tie-breaker map
Field precedence tables and tie-breaker metrics wired into the canonical merge flow; docs/tests updated.
Instructions to work:
Read ./AGENTS.md and core AGENTS. Implement the conflict resolver exactly as specified in ./src/DEDUP_CONFLICTS_RESOLUTION_ALGO.md, coordinating with Merge and Storage teammates. | -| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Core Engine & Storage Analytics | FEEDSTORAGE-DATA-03-001 | Merge event provenance audit prep
Merge events now persist `fieldDecisions` and analytics-ready provenance snapshots. | -| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Core Engine & Storage Analytics | FEEDSTORAGE-DATA-02-001 | Normalized range dual-write + backfill
Dual-write/backfill flag delivered; migration + options validated in tests. | -| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Core Engine & Storage Analytics | FEEDSTORAGE-TESTS-02-004 | Restore AdvisoryStore build after normalized versions refactor
Storage tests adjusted for normalized versions/decision reasons.
Instructions to work:
Read ./AGENTS.md and storage AGENTS. Extend merge events with decision reasons and analytics views to support the conflict rules, and deliver the dual-write/backfill for `NormalizedVersions` + `decisionReason` so connectors can roll out safely. | -| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-11) | Team Merge & QA Enforcement | FEEDMERGE-ENGINE-04-001 | GHSA/NVD/OSV conflict rules
Merge pipeline consumes `CanonicalMerger` output prior to precedence merge. | -| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-11) | Team Merge & QA Enforcement | FEEDMERGE-ENGINE-04-002 | Override metrics instrumentation
Merge events capture per-field decisions; counters/logs align with conflict rules. | -| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-11) | Team Merge & QA Enforcement | FEEDMERGE-ENGINE-04-003 | Reference & credit union pipeline
Canonical merge preserves unions with updated tests. | -| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-11) | Team Merge & QA Enforcement | FEEDMERGE-QA-04-001 | End-to-end conflict regression suite
Added regression tests (`AdvisoryMergeServiceTests`) covering canonical + precedence flow.
Instructions to work:
Read ./AGENTS.md and merge AGENTS. Integrate the canonical merger, instrument metrics, and deliver comprehensive regression tests following ./src/DEDUP_CONFLICTS_RESOLUTION_ALGO.md. | -| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-12) | Team Connector Regression Fixtures | FEEDCONN-GHSA-04-002 | GHSA conflict regression fixtures | -| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Connector.Nvd/TASKS.md | DONE (2025-10-12) | Team Connector Regression Fixtures | FEEDCONN-NVD-04-002 | NVD conflict regression fixtures | -| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-12) | Team Connector Regression Fixtures | FEEDCONN-OSV-04-002 | OSV conflict regression fixtures
Instructions to work:
Read ./AGENTS.md and module AGENTS. Produce fixture triples supporting the precedence/tie-breaker paths defined in ./src/DEDUP_CONFLICTS_RESOLUTION_ALGO.md and hand them to Merge QA. | -| Sprint 3 | Conflict Resolution Integration & Communications | docs/TASKS.md | DONE (2025-10-11) | Team Documentation Guild – Conflict Guidance | FEEDDOCS-DOCS-05-001 | Concelier Conflict Rules
Runbook published at `docs/ops/concelier-conflict-resolution.md`; metrics/log guidance aligned with Sprint 3 merge counters. | -| Sprint 3 | Conflict Resolution Integration & Communications | docs/TASKS.md | DONE (2025-10-16) | Team Documentation Guild – Conflict Guidance | FEEDDOCS-DOCS-05-002 | Conflict runbook ops rollout
Ops review completed, alert thresholds applied, and change log appended in `docs/ops/concelier-conflict-resolution.md`; task closed after connector signals verified. | -| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Models/TASKS.md | DONE (2025-10-15) | Team Models & Merge Leads | FEEDMODELS-SCHEMA-04-001 | Advisory schema parity (description/CWE/canonical metric)
Extend `Advisory` and related records with description text, CWE collection, and canonical metric pointer; refresh validation + serializer determinism tests. | -| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Core/TASKS.md | DONE (2025-10-15) | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-04-003 | Canonical merger parity for new fields
Teach `CanonicalMerger` to populate description, CWEResults, and canonical metric pointer with provenance + regression coverage. | -| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Core/TASKS.md | DONE (2025-10-15) | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-04-004 | Reference normalization & freshness instrumentation cleanup
Implement URL normalization for reference dedupe, align freshness-sensitive instrumentation, and add analytics tests. | -| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-15) | Team Merge & QA Enforcement | FEEDMERGE-ENGINE-04-004 | Merge pipeline parity for new advisory fields
Ensure merge service + merge events surface description/CWE/canonical metric decisions with updated metrics/tests. | -| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-15) | Team Merge & QA Enforcement | FEEDMERGE-ENGINE-04-005 | Connector coordination for new advisory fields
GHSA/NVD/OSV connectors now ship description, CWE, and canonical metric data with refreshed fixtures; merge coordination log updated and exporters notified. | -| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Exporter.Json/TASKS.md | DONE (2025-10-15) | Team Exporters – JSON | FEEDEXPORT-JSON-04-001 | Surface new advisory fields in JSON exporter
Update schemas/offline bundle + fixtures once model/core parity lands.
2025-10-15: `dotnet test src/StellaOps.Concelier.Exporter.Json.Tests` validated canonical metric/CWE emission. | -| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Exporter.TrivyDb/TASKS.md | DONE (2025-10-15) | Team Exporters – Trivy DB | FEEDEXPORT-TRIVY-04-001 | Propagate new advisory fields into Trivy DB package
Extend Bolt builder, metadata, and regression tests for the expanded schema.
2025-10-15: `dotnet test src/StellaOps.Concelier.Exporter.TrivyDb.Tests` confirmed canonical metric/CWE propagation. | -| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-16) | Team Connector Regression Fixtures | FEEDCONN-GHSA-04-004 | Harden CVSS fallback so canonical metric ids persist when GitHub omits vectors; extend fixtures and document severity precedence hand-off to Merge. | -| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-16) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-OSV-04-005 | Map OSV advisories lacking CVSS vectors to canonical metric ids/notes and document CWE provenance quirks; schedule parity fixture updates. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Core/TASKS.md | DONE (2025-10-15) | Team Excititor Core & Policy | EXCITITOR-CORE-01-001 | Stand up canonical VEX claim/consensus records with deterministic serializers so Storage/Exports share a stable contract. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Core/TASKS.md | DONE (2025-10-15) | Team Excititor Core & Policy | EXCITITOR-CORE-01-002 | Implement trust-weighted consensus resolver with baseline policy weights, justification gates, telemetry output, and majority/tie handling. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Core/TASKS.md | DONE (2025-10-15) | Team Excititor Core & Policy | EXCITITOR-CORE-01-003 | Publish shared connector/exporter/attestation abstractions and deterministic query signature utilities for cache/attestation workflows. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Policy/TASKS.md | DONE (2025-10-15) | Team Excititor Policy | EXCITITOR-POLICY-01-001 | Established policy options & snapshot provider covering baseline weights/overrides. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Policy/TASKS.md | DONE (2025-10-15) | Team Excititor Policy | EXCITITOR-POLICY-01-002 | Policy evaluator now feeds consensus resolver with immutable snapshots. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Policy/TASKS.md | DONE (2025-10-16) | Team Excititor Policy | EXCITITOR-POLICY-01-003 | Author policy diagnostics, CLI/WebService surfacing, and documentation updates. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Policy/TASKS.md | DONE (2025-10-16) | Team Excititor Policy | EXCITITOR-POLICY-01-004 | Implement YAML/JSON schema validation and deterministic diagnostics for operator bundles. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Policy/TASKS.md | DONE (2025-10-16) | Team Excititor Policy | EXCITITOR-POLICY-01-005 | Add policy change tracking, snapshot digests, and telemetry/logging hooks. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Storage.Mongo/TASKS.md | DONE (2025-10-15) | Team Excititor Storage | EXCITITOR-STORAGE-01-001 | Mongo mapping registry plus raw/export entities and DI extensions in place. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Storage.Mongo/TASKS.md | DONE (2025-10-16) | Team Excititor Storage | EXCITITOR-STORAGE-01-004 | Build provider/consensus/cache class maps and related collections. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Export/TASKS.md | DONE (2025-10-15) | Team Excititor Export | EXCITITOR-EXPORT-01-001 | Export engine delivers cache lookup, manifest creation, and policy integration. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Export/TASKS.md | DONE (2025-10-17) | Team Excititor Export | EXCITITOR-EXPORT-01-004 | Connect export engine to attestation client and persist Rekor metadata. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Attestation/TASKS.md | DONE (2025-10-16) | Team Excititor Attestation | EXCITITOR-ATTEST-01-001 | Implement in-toto predicate + DSSE builder providing envelopes for export attestation. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Connectors.Abstractions/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors | EXCITITOR-CONN-ABS-01-001 | Deliver shared connector context/base classes so provider plug-ins can be activated via WebService/Worker. | -| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.WebService/TASKS.md | DONE (2025-10-17) | Team Excititor WebService | EXCITITOR-WEB-01-001 | Scaffold minimal API host, DI, and `/excititor/status` endpoint integrating policy, storage, export, and attestation services. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Worker/TASKS.md | DONE (2025-10-17) | Team Excititor Worker | EXCITITOR-WORKER-01-001 | Create Worker host with provider scheduling and logging to drive recurring pulls/reconciliation. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Formats.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Formats | EXCITITOR-FMT-CSAF-01-001 | Implement CSAF normalizer foundation translating provider documents into `VexClaim` entries. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Formats.CycloneDX/TASKS.md | DONE (2025-10-17) | Team Excititor Formats | EXCITITOR-FMT-CYCLONE-01-001 | Implement CycloneDX VEX normalizer capturing `analysis` state and component references. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Formats.OpenVEX/TASKS.md | DONE (2025-10-17) | Team Excititor Formats | EXCITITOR-FMT-OPENVEX-01-001 | Implement OpenVEX normalizer to ingest attestations into canonical claims with provenance. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Red Hat | EXCITITOR-CONN-RH-01-001 | Ship Red Hat CSAF provider metadata discovery enabling incremental pulls. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Red Hat | EXCITITOR-CONN-RH-01-002 | Fetch CSAF windows with ETag handling, resume tokens, quarantine on schema errors, and persist raw docs. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Red Hat | EXCITITOR-CONN-RH-01-003 | Populate provider trust overrides (cosign issuer, identity regex) and provenance hints for policy evaluation/logging. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Red Hat | EXCITITOR-CONN-RH-01-004 | Persist resume cursors (last updated timestamp/document hashes) in storage and reload during fetch to avoid duplicates. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Red Hat | EXCITITOR-CONN-RH-01-005 | Register connector in Worker/WebService DI, add scheduled jobs, and document CLI triggers for Red Hat CSAF pulls. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Red Hat | EXCITITOR-CONN-RH-01-006 | Add CSAF normalization parity fixtures ensuring RHSA-specific metadata is preserved. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.Cisco.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Cisco | EXCITITOR-CONN-CISCO-01-001 | Implement Cisco CSAF endpoint discovery/auth to unlock paginated pulls. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.Cisco.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Cisco | EXCITITOR-CONN-CISCO-01-002 | Implement Cisco CSAF paginated fetch loop with dedupe and raw persistence support. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.SUSE.RancherVEXHub/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – SUSE | EXCITITOR-CONN-SUSE-01-001 | Build Rancher VEX Hub discovery/subscription path with offline snapshot support. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.MSRC.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – MSRC | EXCITITOR-CONN-MS-01-001 | Deliver AAD onboarding/token cache for MSRC CSAF ingestion. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.Oracle.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Oracle | EXCITITOR-CONN-ORACLE-01-001 | Implement Oracle CSAF catalogue discovery with CPU calendar awareness. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.Ubuntu.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Ubuntu | EXCITITOR-CONN-UBUNTU-01-001 | Implement Ubuntu CSAF discovery and channel selection for USN ingestion. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest/TASKS.md | DONE (2025-10-18) | Team Excititor Connectors – OCI | EXCITITOR-CONN-OCI-01-001 | Wire OCI discovery/auth to fetch OpenVEX attestations for configured images. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest/TASKS.md | DONE (2025-10-18) | Team Excititor Connectors – OCI | EXCITITOR-CONN-OCI-01-002 | Attestation fetch & verify loop – download DSSE attestations, trigger verification, handle retries/backoff, persist raw statements. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest/TASKS.md | DONE (2025-10-18) | Team Excititor Connectors – OCI | EXCITITOR-CONN-OCI-01-003 | Provenance metadata & policy hooks – emit image, subject digest, issuer, and trust metadata for policy weighting/logging. | -| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Cli/TASKS.md | DONE (2025-10-18) | DevEx/CLI | EXCITITOR-CLI-01-001 | Add `excititor` CLI verbs bridging to WebService with consistent auth and offline UX. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Core/TASKS.md | TODO | Team Excititor Core & Policy | EXCITITOR-CORE-02-001 | Context signal schema prep – extend consensus models with severity/KEV/EPSS fields and update canonical serializers. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Policy/TASKS.md | TODO | Team Excititor Policy | EXCITITOR-POLICY-02-001 | Scoring coefficients & weight ceilings – add α/β options, weight boosts, and validation guidance. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Team Excititor Storage | EXCITITOR-STORAGE-02-001 | Statement events & scoring signals – create immutable VEX statement store plus consensus extensions with indexes/migrations. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.WebService/TASKS.md | TODO | Team Excititor WebService | EXCITITOR-WEB-01-004 | Resolve API & signed responses – expose `/excititor/resolve`, return signed consensus/score envelopes, document auth. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.WebService/TASKS.md | TODO | Team Excititor WebService | EXCITITOR-WEB-01-005 | Mirror distribution endpoints – expose download APIs for downstream Excititor instances. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Attestation/TASKS.md | DONE (2025-10-16) | Team Excititor Attestation | EXCITITOR-ATTEST-01-002 | Rekor v2 client integration – ship transparency log client with retries and offline queue. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Worker/TASKS.md | TODO | Team Excititor Worker | EXCITITOR-WORKER-01-004 | TTL refresh & stability damper – schedule re-resolve loops and guard against status flapping. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Export/TASKS.md | TODO | Team Excititor Export | EXCITITOR-EXPORT-01-005 | Score & resolve envelope surfaces – include signed consensus/score artifacts in exports. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Export/TASKS.md | TODO | Team Excititor Export | EXCITITOR-EXPORT-01-006 | Quiet provenance packaging – attach quieted-by statement IDs, signers, justification codes to exports and attestations. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Export/TASKS.md | TODO | Team Excititor Export | EXCITITOR-EXPORT-01-007 | Mirror bundle + domain manifest – publish signed consensus bundles for mirrors. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Connectors.StellaOpsMirror/TASKS.md | TODO | Excititor Connectors – Stella | EXCITITOR-CONN-STELLA-07-001 | Excititor mirror connector – ingest signed mirror bundles and map to VexClaims with resume handling. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Core/TASKS.md | TODO | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-07-001 | Advisory event log & asOf queries – surface immutable statements and replay capability. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Core/TASKS.md | TODO | Team Core Engine & Data Science | FEEDCORE-ENGINE-07-002 | Noise prior computation service – learn false-positive priors and expose deterministic summaries. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Core/TASKS.md | TODO | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-07-003 | Unknown state ledger & confidence seeding – persist unknown flags, seed confidence bands, expose query surface. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-07-001 | Advisory statement & conflict collections – provision Mongo schema/indexes for event-sourced merge. | -| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Merge/TASKS.md | TODO | BE-Merge | FEEDMERGE-ENGINE-07-001 | Conflict sets & explainers – persist conflict materialization and replay hashes for merge decisions. | -| Sprint 8 | Mongo strengthening | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Team Normalization & Storage Backbone | FEEDSTORAGE-MONGO-08-001 | Causal-consistent Concelier storage sessions
Ensure `AddMongoStorage` registers a scoped session facilitator (causal consistency + majority concerns), update repositories to accept optional session handles, and add integration coverage proving read-your-write and monotonic reads across a replica set/election scenario. | -| Sprint 8 | Mongo strengthening | src/StellaOps.Authority/TASKS.md | TODO | Authority Core & Storage Guild | AUTHSTORAGE-MONGO-08-001 | Harden Authority Mongo usage
Introduce scoped MongoDB sessions with `writeConcern`/`readConcern` majority defaults, flow the session through stores used in mutations + follow-up reads, and document middleware pattern for web/API & GraphQL layers. | -| Sprint 8 | Mongo strengthening | src/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Team Excititor Storage | EXCITITOR-STORAGE-MONGO-08-001 | Causal consistency for Excititor repositories
Register Mongo options with majority defaults, push session-aware overloads through raw/export/consensus/cache stores, and extend migration/tests to validate causal reads after writes (including GridFS-backed content) under replica-set failover. | -| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.Exporter.Json/TASKS.md | TODO | Concelier Export Guild | CONCELIER-EXPORT-08-201 | Mirror bundle + domain manifest – produce signed JSON aggregates for `*.stella-ops.org` mirrors. | -| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.Exporter.TrivyDb/TASKS.md | TODO | Concelier Export Guild | CONCELIER-EXPORT-08-202 | Mirror-ready Trivy DB bundles – ship domain-specific archives + metadata for downstream sync. | -| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-08-201 | Mirror distribution endpoints – expose domain-scoped index/download APIs with auth/quota. | -| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.Connector.StellaOpsMirror/TASKS.md | TODO | BE-Conn-Stella | FEEDCONN-STELLA-08-001 | Concelier mirror connector – fetch mirror manifest, verify signatures, and hydrate canonical DTOs with resume support. | -| Sprint 8 | Mirror Distribution | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-MIRROR-08-001 | Managed mirror deployments for `*.stella-ops.org` – Helm/Compose overlays, CDN, runbooks. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Core/TASKS.md | DONE (2025-10-18) | Team Scanner Core | SCANNER-CORE-09-501 | Define shared DTOs (ScanJob, ProgressEvent), error taxonomy, and deterministic ID/timestamp helpers aligning with `ARCHITECTURE_SCANNER.md` §3–§4. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Core/TASKS.md | DONE (2025-10-18) | Team Scanner Core | SCANNER-CORE-09-502 | Observability helpers (correlation IDs, logging scopes, metric namespacing, deterministic hashes) consumed by WebService/Worker. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Core/TASKS.md | DONE (2025-10-18) | Team Scanner Core | SCANNER-CORE-09-503 | Security utilities: Authority client factory, OpTok caching, DPoP verifier, restart-time plug-in guardrails for scanner components. | -| Sprint 9 | Scanner Build-time | src/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md | DONE | BuildX Guild | SP9-BLDX-09-001 | Buildx driver scaffold + handshake with Scanner.Emit (local CAS). | -| Sprint 9 | Scanner Build-time | src/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md | DONE | BuildX Guild | SP9-BLDX-09-002 | OCI annotations + provenance hand-off to Attestor. | -| Sprint 9 | Scanner Build-time | src/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md | DONE | BuildX Guild | SP9-BLDX-09-003 | CI demo: minimal SBOM push & backend report wiring. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-18) | Team Scanner WebService | SCANNER-WEB-09-101 | Minimal API host with Authority enforcement, health/ready endpoints, and restart-time plug-in loader per architecture §1, §4. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-18) | Team Scanner WebService | SCANNER-WEB-09-102 | `/api/v1/scans` submission/status endpoints with deterministic IDs, validation, and cancellation support. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Team Scanner WebService | SCANNER-WEB-09-103 | Progress streaming (SSE/JSONL) with correlation IDs and ISO-8601 UTC timestamps, documented in API reference. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-WEB-09-104 | Configuration binding for Mongo, MinIO, queue, feature flags; startup diagnostics and fail-fast policy. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Team Scanner WebService | SCANNER-POLICY-09-105 | Policy snapshot loader + schema + OpenAPI (YAML ignore rules, VEX include/exclude, vendor precedence). | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Team Scanner WebService | SCANNER-POLICY-09-106 | `/reports` verdict assembly (Feedser+Vexer+Policy) + signed response envelope. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Team Scanner WebService | SCANNER-POLICY-09-107 | Expose score inputs, config version, and quiet provenance in `/reports` JSON and signed payload. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Worker/TASKS.md | DONE (2025-10-19) | Team Scanner Worker | SCANNER-WORKER-09-201 | Worker host bootstrap with Authority auth, hosted services, and graceful shutdown semantics. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Worker/TASKS.md | DONE (2025-10-19) | Team Scanner Worker | SCANNER-WORKER-09-202 | Lease/heartbeat loop with retry+jitter, poison-job quarantine, structured logging. | +This file describe implementation of Stella Ops (docs/README.md). Implementation must respect rules from AGENTS.md (read if you have not). + +| Sprint | Theme | Tasks File Path | Status | Type of Specialist | Task ID | Task Description | +| --- | --- | --- | --- | --- | --- | --- | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Models/TASKS.md | DONE (2025-10-12) | Team Models & Merge Leads | FEEDMODELS-SCHEMA-01-001 | SemVer primitive range-style metadata
Instructions to work:
DONE Read ./AGENTS.md and src/StellaOps.Concelier.Models/AGENTS.md. This task lays the groundwork—complete the SemVer helper updates before teammates pick up FEEDMODELS-SCHEMA-01-002/003 and FEEDMODELS-SCHEMA-02-900. Use ./src/FASTER_MODELING_AND_NORMALIZATION.md for the target rule structure. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Models/TASKS.md | DONE (2025-10-11) | Team Models & Merge Leads | FEEDMODELS-SCHEMA-01-002 | Provenance decision rationale field
Instructions to work:
AdvisoryProvenance now carries `decisionReason` and docs/tests were updated. Connectors and merge tasks should populate the field when applying precedence/freshness/tie-breaker logic; see src/StellaOps.Concelier.Models/PROVENANCE_GUIDELINES.md for usage guidance. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Models/TASKS.md | DONE (2025-10-11) | Team Models & Merge Leads | FEEDMODELS-SCHEMA-01-003 | Normalized version rules collection
Instructions to work:
`AffectedPackage.NormalizedVersions` and supporting comparer/docs/tests shipped. Connector owners must emit rule arrays per ./src/FASTER_MODELING_AND_NORMALIZATION.md and report progress via FEEDMERGE-COORD-02-900 so merge/storage backfills can proceed. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Models/TASKS.md | DONE (2025-10-12) | Team Models & Merge Leads | FEEDMODELS-SCHEMA-02-900 | Range primitives for SemVer/EVR/NEVRA metadata
Instructions to work:
DONE Read ./AGENTS.md and src/StellaOps.Concelier.Models/AGENTS.md before resuming this stalled effort. Confirm helpers align with the new `NormalizedVersions` representation so connectors finishing in Sprint 2 can emit consistent metadata. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Normalization/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDNORM-NORM-02-001 | SemVer normalized rule emitter
Shared `SemVerRangeRuleBuilder` now outputs primitives + normalized rules per `FASTER_MODELING_AND_NORMALIZATION.md`; CVE/GHSA connectors consuming the API have verified fixtures. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-02-001 | Normalized range dual-write + backfill
AdvisoryStore dual-writes flattened `normalizedVersions` when `concelier.storage.enableSemVerStyle` is set; migration `20251011-semver-style-backfill` updates historical records and docs outline the rollout. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-02-002 | Provenance decision reason persistence
Storage now persists `provenance.decisionReason` for advisories and merge events; tests cover round-trips. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-02-003 | Normalized versions indexing
Bootstrapper seeds compound/sparse indexes for flattened normalized rules and `docs/dev/mongo_indices.md` documents query guidance. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-TESTS-02-004 | Restore AdvisoryStore build after normalized versions refactor
Updated constructors/tests keep storage suites passing with the new feature flag defaults. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-ENGINE-01-002 | Plumb Authority client resilience options
WebService wires `authority.resilience.*` into `AddStellaOpsAuthClient` and adds binding coverage via `AuthorityClientResilienceOptionsAreBound`. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-DOCS-01-003 | Author ops guidance for resilience tuning
Install/runbooks document connected vs air-gapped resilience profiles and monitoring hooks. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-DOCS-01-004 | Document authority bypass logging patterns
Operator guides now call out `route/status/subject/clientId/scopes/bypass/remote` audit fields and SIEM triggers. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-DOCS-01-005 | Update Concelier operator guide for enforcement cutoff
Install guide reiterates the 2025-12-31 cutoff and links audit signals to the rollout checklist. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Authority/TASKS.md | DONE (2025-10-11) | Team WebService & Authority | SEC3.HOST | Rate limiter policy binding
Authority host now applies configuration-driven fixed windows to `/token`, `/authorize`, and `/internal/*`; integration tests assert 429 + `Retry-After` headers; docs/config samples refreshed for Docs guild diagrams. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Authority/TASKS.md | DONE (2025-10-11) | Team WebService & Authority | SEC3.BUILD | Authority rate-limiter follow-through
`Security.RateLimiting` now fronts token/authorize/internal limiters; Authority + Configuration matrices (`dotnet test src/StellaOps.Authority/StellaOps.Authority.sln`, `dotnet test src/StellaOps.Configuration.Tests/StellaOps.Configuration.Tests.csproj`) passed on 2025-10-11; awaiting #authority-core broadcast. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Authority/TASKS.md | DONE (2025-10-14) | Team Authority Platform & Security Guild | AUTHCORE-BUILD-OPENIDDICT / AUTHCORE-STORAGE-DEVICE-TOKENS / AUTHCORE-BOOTSTRAP-INVITES | Address remaining Authority compile blockers (OpenIddict transaction shim, token device document, bootstrap invite cleanup) so `dotnet build src/StellaOps.Authority.sln` returns success. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md | DONE (2025-10-11) | Team WebService & Authority | PLG6.DOC | Plugin developer guide polish
Section 9 now documents rate limiter metadata, config keys, and lockout interplay; YAML samples updated alongside Authority config templates. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-001 | Fetch pipeline & state tracking
Summary planner now drives monthly/yearly VINCE fetches, persists pending summaries/notes, and hydrates VINCE detail queue with telemetry.
Team instructions: Read ./AGENTS.md and src/StellaOps.Concelier.Connector.CertCc/AGENTS.md. Coordinate daily with Models/Merge leads so new normalizedVersions output and provenance tags stay aligned with ./src/FASTER_MODELING_AND_NORMALIZATION.md. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-002 | VINCE note detail fetcher
Summary planner queues VINCE note detail endpoints, persists raw JSON with SHA/ETag metadata, and records retry/backoff metrics. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-003 | DTO & parser implementation
Added VINCE DTO aggregate, Markdown→text sanitizer, vendor/status/vulnerability parsers, and parser regression fixture. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-004 | Canonical mapping & range primitives
VINCE DTO aggregate flows through `CertCcMapper`, emitting vendor range primitives + normalized version rules that persist via `_advisoryStore`. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-12) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-005 | Deterministic fixtures/tests
Snapshot harness refreshed 2025-10-12; `certcc-*.snapshot.json` regenerated and regression suite green without UPDATE flag drift. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-12) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-006 | Telemetry & documentation
`CertCcDiagnostics` publishes summary/detail/parse/map metrics (meter `StellaOps.Concelier.Connector.CertCc`), README documents instruments, and log guidance captured for Ops on 2025-10-12. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-12) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-007 | Connector test harness remediation
Harness now wires `AddSourceCommon`, resets `FakeTimeProvider`, and passes canned-response regression run dated 2025-10-12. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-008 | Snapshot coverage handoff
Fixtures regenerated with normalized ranges + provenance fields on 2025-10-11; QA handoff notes published and merge backfill unblocked. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-12) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-012 | Schema sync & snapshot regen follow-up
Fixtures regenerated with normalizedVersions + provenance decision reasons; handoff notes updated for Merge backfill 2025-10-12. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-009 | Detail/map reintegration plan
Staged reintegration plan published in `src/StellaOps.Concelier.Connector.CertCc/FEEDCONN-CERTCC-02-009_PLAN.md`; coordinates enablement with FEEDCONN-CERTCC-02-004. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.CertCc/TASKS.md | DONE (2025-10-12) | Team Connector Resumption – CERT/RedHat | FEEDCONN-CERTCC-02-010 | Partial-detail graceful degradation
Detail fetch now tolerates 404/403/410 responses and regression tests cover mixed endpoint availability. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Distro.RedHat/TASKS.md | DONE (2025-10-11) | Team Connector Resumption – CERT/RedHat | FEEDCONN-REDHAT-02-001 | Fixture validation sweep
Instructions to work:
Fixtures regenerated post-model-helper rollout; provenance ordering and normalizedVersions scaffolding verified via tests. Conflict resolver deltas logged in src/StellaOps.Concelier.Connector.Distro.RedHat/CONFLICT_RESOLVER_NOTES.md for Sprint 3 consumers. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Vndr.Apple/TASKS.md | DONE (2025-10-12) | Team Vendor Apple Specialists | FEEDCONN-APPLE-02-001 | Canonical mapping & range primitives
Mapper emits SemVer rules (`scheme=apple:*`); fixtures regenerated with trimmed references + new RSR coverage, update tooling finalized. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Vndr.Apple/TASKS.md | DONE (2025-10-11) | Team Vendor Apple Specialists | FEEDCONN-APPLE-02-002 | Deterministic fixtures/tests
Sanitized live fixtures + regression snapshots wired into tests; normalized rule coverage asserted. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Vndr.Apple/TASKS.md | DONE (2025-10-11) | Team Vendor Apple Specialists | FEEDCONN-APPLE-02-003 | Telemetry & documentation
Apple meter metrics wired into Concelier WebService OpenTelemetry configuration; README and fixtures document normalizedVersions coverage. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Vndr.Apple/TASKS.md | DONE (2025-10-12) | Team Vendor Apple Specialists | FEEDCONN-APPLE-02-004 | Live HTML regression sweep
Sanitised HT125326/HT125328/HT106355/HT214108/HT215500 fixtures recorded and regression tests green on 2025-10-12. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Vndr.Apple/TASKS.md | DONE (2025-10-11) | Team Vendor Apple Specialists | FEEDCONN-APPLE-02-005 | Fixture regeneration tooling
`UPDATE_APPLE_FIXTURES=1` flow fetches & rewrites fixtures; README documents usage.
Instructions to work:
DONE Read ./AGENTS.md and src/StellaOps.Concelier.Connector.Vndr.Apple/AGENTS.md. Resume stalled tasks, ensuring normalizedVersions output and fixtures align with ./src/FASTER_MODELING_AND_NORMALIZATION.md before handing data to the conflict sprint. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-12) | Team Connector Normalized Versions Rollout | FEEDCONN-GHSA-02-001 | GHSA normalized versions & provenance
Team instructions: Read ./AGENTS.md and each module's AGENTS file. Adopt the `NormalizedVersions` array emitted by the models sprint, wiring provenance `decisionReason` where merge overrides occur. Follow ./src/FASTER_MODELING_AND_NORMALIZATION.md; report via src/StellaOps.Concelier.Merge/TASKS.md (FEEDMERGE-COORD-02-900). Progress 2025-10-11: GHSA/OSV emit normalized arrays with refreshed fixtures; CVE mapper now surfaces SemVer normalized ranges; NVD/KEV adoption pending; outstanding follow-ups include FEEDSTORAGE-DATA-02-001, FEEDMERGE-ENGINE-02-002, and rolling `tools/FixtureUpdater` updates across connectors. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-12) | Team Connector Normalized Versions Rollout | FEEDCONN-OSV-02-003 | OSV normalized versions & freshness | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Nvd/TASKS.md | DONE (2025-10-12) | Team Connector Normalized Versions Rollout | FEEDCONN-NVD-02-002 | NVD normalized versions & timestamps | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Cve/TASKS.md | DONE (2025-10-12) | Team Connector Normalized Versions Rollout | FEEDCONN-CVE-02-003 | CVE normalized versions uplift | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Kev/TASKS.md | DONE (2025-10-12) | Team Connector Normalized Versions Rollout | FEEDCONN-KEV-02-003 | KEV normalized versions propagation | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-12) | Team Connector Normalized Versions Rollout | FEEDCONN-OSV-04-003 | OSV parity fixture refresh | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-10) | Team WebService & Authority | FEEDWEB-DOCS-01-001 | Document authority toggle & scope requirements
Quickstart carries toggle/scope guidance pending docs guild review (no change this sprint). | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-ENGINE-01-002 | Plumb Authority client resilience options
WebService wires `authority.resilience.*` into `AddStellaOpsAuthClient` and adds binding coverage via `AuthorityClientResilienceOptionsAreBound`. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-DOCS-01-003 | Author ops guidance for resilience tuning
Operator docs now outline connected vs air-gapped resilience profiles and monitoring cues. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-DOCS-01-004 | Document authority bypass logging patterns
Audit logging guidance highlights `route/status/subject/clientId/scopes/bypass/remote` fields and SIEM alerts. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-12) | Team WebService & Authority | FEEDWEB-DOCS-01-005 | Update Concelier operator guide for enforcement cutoff
Install guide reiterates the 2025-12-31 cutoff and ties audit signals to rollout checks. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-11) | Team WebService & Authority | FEEDWEB-OPS-01-006 | Rename plugin drop directory to namespaced path
Build outputs, tests, and docs now target `StellaOps.Concelier.PluginBinaries`/`StellaOps.Authority.PluginBinaries`. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Concelier.WebService/TASKS.md | DONE (2025-10-11) | Team WebService & Authority | FEEDWEB-OPS-01-007 | Authority resilience adoption
Deployment docs and CLI notes explain the LIB5 resilience knobs for rollout.
Instructions to work:
DONE Read ./AGENTS.md and src/StellaOps.Concelier.WebService/AGENTS.md. These items were mid-flight; resume implementation ensuring docs/operators receive timely updates. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Authority/TASKS.md | DONE (2025-10-11) | Team Authority Platform & Security Guild | AUTHCORE-ENGINE-01-001 | CORE8.RL — Rate limiter plumbing validated; integration tests green and docs handoff recorded for middleware ordering + Retry-After headers (see `docs/dev/authority-rate-limit-tuning-outline.md` for continuing guidance). | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Cryptography/TASKS.md | DONE (2025-10-11) | Team Authority Platform & Security Guild | AUTHCRYPTO-ENGINE-01-001 | SEC3.A — Shared metadata resolver confirmed via host test run; SEC3.B now unblocked for tuning guidance (outline captured in `docs/dev/authority-rate-limit-tuning-outline.md`). | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Cryptography/TASKS.md | DONE (2025-10-13) | Team Authority Platform & Security Guild | AUTHSEC-DOCS-01-002 | SEC3.B — Published `docs/security/rate-limits.md` with tuning matrix, alert thresholds, and lockout interplay guidance; Docs guild can lift copy into plugin guide. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Cryptography/TASKS.md | DONE (2025-10-14) | Team Authority Platform & Security Guild | AUTHSEC-CRYPTO-02-001 | SEC5.B1 — Introduce libsodium signing provider and parity tests to unblock CLI verification enhancements. | +| Sprint 1 | Bootstrap & Replay Hardening | src/StellaOps.Cryptography/TASKS.md | DONE (2025-10-14) | Security Guild | AUTHSEC-CRYPTO-02-004 | SEC5.D/E — Finish bootstrap invite lifecycle (API/store/cleanup) and token device heuristics; build currently red due to pending handler integration. | +| Sprint 1 | Developer Tooling | src/StellaOps.Cli/TASKS.md | DONE (2025-10-15) | DevEx/CLI | AUTHCLI-DIAG-01-001 | Surface password policy diagnostics in CLI startup/output so operators see weakened overrides immediately.
CLI now loads Authority plug-ins at startup, logs weakened password policies (length/complexity), and regression coverage lives in `StellaOps.Cli.Tests/Services/AuthorityDiagnosticsReporterTests`. | +| Sprint 1 | Stabilize In-Progress Foundations | src/StellaOps.Authority/StellaOps.Authority.Plugin.Standard/TASKS.md | DONE (2025-10-11) | Team Authority Platform & Security Guild | AUTHPLUG-DOCS-01-001 | PLG6.DOC — Developer guide copy + diagrams merged 2025-10-11; limiter guidance incorporated and handed to Docs guild for asset export. | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Normalization/TASKS.md | DONE (2025-10-12) | Team Normalization & Storage Backbone | FEEDNORM-NORM-02-001 | SemVer normalized rule emitter
`SemVerRangeRuleBuilder` shipped 2025-10-12 with comparator/`||` support and fixtures aligning to `FASTER_MODELING_AND_NORMALIZATION.md`. | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-02-001 | Normalized range dual-write + backfill | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-02-002 | Provenance decision reason persistence | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-02-003 | Normalized versions indexing
Indexes seeded + docs updated 2025-10-11 to cover flattened normalized rules for connector adoption. | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-11) | Team Normalization & Storage Backbone | FEEDMERGE-ENGINE-02-002 | Normalized versions union & dedupe
Affected package resolver unions/dedupes normalized rules, stamps merge provenance with `decisionReason`, and tests cover the rollout. | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-11) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-GHSA-02-001 | GHSA normalized versions & provenance | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-11) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-GHSA-02-004 | GHSA credits & ecosystem severity mapping | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-12) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-GHSA-02-005 | GitHub quota monitoring & retries | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-12) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-GHSA-02-006 | Production credential & scheduler rollout | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-12) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-GHSA-02-007 | Credit parity regression fixtures | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Nvd/TASKS.md | DONE (2025-10-11) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-NVD-02-002 | NVD normalized versions & timestamps | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Nvd/TASKS.md | DONE (2025-10-11) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-NVD-02-004 | NVD CVSS & CWE precedence payloads | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Nvd/TASKS.md | DONE (2025-10-12) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-NVD-02-005 | NVD merge/export parity regression | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-11) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-OSV-02-003 | OSV normalized versions & freshness | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-11) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-OSV-02-004 | OSV references & credits alignment | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-12) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-OSV-02-005 | Fixture updater workflow
Resolved 2025-10-12: OSV mapper now derives canonical PURLs for Go + scoped npm packages when raw payloads omit `purl`; conflict fixtures unchanged for invalid npm names. Verified via `dotnet test src/StellaOps.Concelier.Connector.Osv.Tests`, `src/StellaOps.Concelier.Connector.Ghsa.Tests`, `src/StellaOps.Concelier.Connector.Nvd.Tests`, and backbone normalization/storage suites. | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Acsc/TASKS.md | DONE (2025-10-12) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-ACSC-02-001 … 02-008 | Fetch→parse→map pipeline, fixtures, diagnostics, and README finished 2025-10-12; downstream export parity captured via FEEDEXPORT-JSON-04-001 / FEEDEXPORT-TRIVY-04-001 (completed). | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Cccs/TASKS.md | DONE (2025-10-16) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-CCCS-02-001 … 02-008 | Observability meter, historical harvest plan, and DOM sanitizer refinements wrapped; ops notes live under `docs/ops/concelier-cccs-operations.md` with fixtures validating EN/FR list handling. | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.CertBund/TASKS.md | DONE (2025-10-15) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-CERTBUND-02-001 … 02-008 | Telemetry/docs (02-006) and history/locale sweep (02-007) completed alongside pipeline; runbook `docs/ops/concelier-certbund-operations.md` captures locale guidance and offline packaging. | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Kisa/TASKS.md | DONE (2025-10-14) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-KISA-02-001 … 02-007 | Connector, tests, and telemetry/docs (02-006) finalized; localisation notes in `docs/dev/kisa_connector_notes.md` complete rollout. | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ru.Bdu/TASKS.md | DONE (2025-10-14) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-RUBDU-02-001 … 02-008 | Fetch/parser/mapper refinements, regression fixtures, telemetry/docs, access options, and trusted root packaging all landed; README documents offline access strategy. | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ru.Nkcki/TASKS.md | DONE (2025-10-13) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-NKCKI-02-001 … 02-008 | Listing fetch, parser, mapper, fixtures, telemetry/docs, and archive plan finished; Mongo2Go/libcrypto dependency resolved via bundled OpenSSL noted in ops guide. | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Ics.Cisa/TASKS.md | DONE (2025-10-16) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-ICSCISA-02-001 … 02-011 | Feed parser attachment fixes, SemVer exact values, regression suites, telemetry/docs updates, and handover complete; ops runbook now details attachment verification + proxy usage. | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Vndr.Cisco/TASKS.md | DONE (2025-10-14) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-CISCO-02-001 … 02-007 | OAuth fetch pipeline, DTO/mapping, tests, and telemetry/docs shipped; monitoring/export integration follow-ups recorded in Ops docs and exporter backlog (completed). | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Vndr.Msrc/TASKS.md | DONE (2025-10-15) | Team Connector Expansion – Regional & Vendor Feeds | FEEDCONN-MSRC-02-001 … 02-008 | Azure AD onboarding (02-008) unblocked fetch/parse/map pipeline; fixtures, telemetry/docs, and Offline Kit guidance published in `docs/ops/concelier-msrc-operations.md`. | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Cve/TASKS.md | DONE (2025-10-15) | Team Connector Support & Monitoring | FEEDCONN-CVE-02-001 … 02-002 | CVE data-source selection, fetch pipeline, and docs landed 2025-10-10. 2025-10-15: smoke verified using the seeded mirror fallback; connector now logs a warning and pulls from `seed-data/cve/` until live CVE Services credentials arrive. | +| Sprint 2 | Connector & Data Implementation Wave | src/StellaOps.Concelier.Connector.Kev/TASKS.md | DONE (2025-10-12) | Team Connector Support & Monitoring | FEEDCONN-KEV-02-001 … 02-002 | KEV catalog ingestion, fixtures, telemetry, and schema validation completed 2025-10-12; ops dashboard published. | +| Sprint 2 | Connector & Data Implementation Wave | docs/TASKS.md | DONE (2025-10-11) | Team Docs & Knowledge Base | FEEDDOCS-DOCS-01-001 | Canonical schema docs refresh
Updated canonical schema + provenance guides with SemVer style, normalized version rules, decision reason change log, and migration notes. | +| Sprint 2 | Connector & Data Implementation Wave | docs/TASKS.md | DONE (2025-10-11) | Team Docs & Knowledge Base | FEEDDOCS-DOCS-02-001 | Concelier-SemVer Playbook
Published merge playbook covering mapper patterns, dedupe flow, indexes, and rollout checklist. | +| Sprint 2 | Connector & Data Implementation Wave | docs/TASKS.md | DONE (2025-10-11) | Team Docs & Knowledge Base | FEEDDOCS-DOCS-02-002 | Normalized versions query guide
Delivered Mongo index/query addendum with `$unwind` recipes, dedupe checks, and operational checklist.
Instructions to work:
DONE Read ./AGENTS.md and docs/AGENTS.md. Document every schema/index/query change produced in Sprint 1-2 leveraging ./src/FASTER_MODELING_AND_NORMALIZATION.md. | +| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Core/TASKS.md | DONE (2025-10-11) | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-03-001 | Canonical merger implementation
`CanonicalMerger` ships with freshness/tie-breaker logic, provenance, and unit coverage feeding Merge. | +| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Core/TASKS.md | DONE (2025-10-11) | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-03-002 | Field precedence and tie-breaker map
Field precedence tables and tie-breaker metrics wired into the canonical merge flow; docs/tests updated.
Instructions to work:
Read ./AGENTS.md and core AGENTS. Implement the conflict resolver exactly as specified in ./src/DEDUP_CONFLICTS_RESOLUTION_ALGO.md, coordinating with Merge and Storage teammates. | +| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Core Engine & Storage Analytics | FEEDSTORAGE-DATA-03-001 | Merge event provenance audit prep
Merge events now persist `fieldDecisions` and analytics-ready provenance snapshots. | +| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Core Engine & Storage Analytics | FEEDSTORAGE-DATA-02-001 | Normalized range dual-write + backfill
Dual-write/backfill flag delivered; migration + options validated in tests. | +| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | DONE (2025-10-11) | Team Core Engine & Storage Analytics | FEEDSTORAGE-TESTS-02-004 | Restore AdvisoryStore build after normalized versions refactor
Storage tests adjusted for normalized versions/decision reasons.
Instructions to work:
Read ./AGENTS.md and storage AGENTS. Extend merge events with decision reasons and analytics views to support the conflict rules, and deliver the dual-write/backfill for `NormalizedVersions` + `decisionReason` so connectors can roll out safely. | +| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-11) | Team Merge & QA Enforcement | FEEDMERGE-ENGINE-04-001 | GHSA/NVD/OSV conflict rules
Merge pipeline consumes `CanonicalMerger` output prior to precedence merge. | +| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-11) | Team Merge & QA Enforcement | FEEDMERGE-ENGINE-04-002 | Override metrics instrumentation
Merge events capture per-field decisions; counters/logs align with conflict rules. | +| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-11) | Team Merge & QA Enforcement | FEEDMERGE-ENGINE-04-003 | Reference & credit union pipeline
Canonical merge preserves unions with updated tests. | +| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-11) | Team Merge & QA Enforcement | FEEDMERGE-QA-04-001 | End-to-end conflict regression suite
Added regression tests (`AdvisoryMergeServiceTests`) covering canonical + precedence flow.
Instructions to work:
Read ./AGENTS.md and merge AGENTS. Integrate the canonical merger, instrument metrics, and deliver comprehensive regression tests following ./src/DEDUP_CONFLICTS_RESOLUTION_ALGO.md. | +| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-12) | Team Connector Regression Fixtures | FEEDCONN-GHSA-04-002 | GHSA conflict regression fixtures | +| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Connector.Nvd/TASKS.md | DONE (2025-10-12) | Team Connector Regression Fixtures | FEEDCONN-NVD-04-002 | NVD conflict regression fixtures | +| Sprint 3 | Conflict Resolution Integration & Communications | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-12) | Team Connector Regression Fixtures | FEEDCONN-OSV-04-002 | OSV conflict regression fixtures
Instructions to work:
Read ./AGENTS.md and module AGENTS. Produce fixture triples supporting the precedence/tie-breaker paths defined in ./src/DEDUP_CONFLICTS_RESOLUTION_ALGO.md and hand them to Merge QA. | +| Sprint 3 | Conflict Resolution Integration & Communications | docs/TASKS.md | DONE (2025-10-11) | Team Documentation Guild – Conflict Guidance | FEEDDOCS-DOCS-05-001 | Concelier Conflict Rules
Runbook published at `docs/ops/concelier-conflict-resolution.md`; metrics/log guidance aligned with Sprint 3 merge counters. | +| Sprint 3 | Conflict Resolution Integration & Communications | docs/TASKS.md | DONE (2025-10-16) | Team Documentation Guild – Conflict Guidance | FEEDDOCS-DOCS-05-002 | Conflict runbook ops rollout
Ops review completed, alert thresholds applied, and change log appended in `docs/ops/concelier-conflict-resolution.md`; task closed after connector signals verified. | +| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Models/TASKS.md | DONE (2025-10-15) | Team Models & Merge Leads | FEEDMODELS-SCHEMA-04-001 | Advisory schema parity (description/CWE/canonical metric)
Extend `Advisory` and related records with description text, CWE collection, and canonical metric pointer; refresh validation + serializer determinism tests. | +| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Core/TASKS.md | DONE (2025-10-15) | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-04-003 | Canonical merger parity for new fields
Teach `CanonicalMerger` to populate description, CWEResults, and canonical metric pointer with provenance + regression coverage. | +| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Core/TASKS.md | DONE (2025-10-15) | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-04-004 | Reference normalization & freshness instrumentation cleanup
Implement URL normalization for reference dedupe, align freshness-sensitive instrumentation, and add analytics tests. | +| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-15) | Team Merge & QA Enforcement | FEEDMERGE-ENGINE-04-004 | Merge pipeline parity for new advisory fields
Ensure merge service + merge events surface description/CWE/canonical metric decisions with updated metrics/tests. | +| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Merge/TASKS.md | DONE (2025-10-15) | Team Merge & QA Enforcement | FEEDMERGE-ENGINE-04-005 | Connector coordination for new advisory fields
GHSA/NVD/OSV connectors now ship description, CWE, and canonical metric data with refreshed fixtures; merge coordination log updated and exporters notified. | +| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Exporter.Json/TASKS.md | DONE (2025-10-15) | Team Exporters – JSON | FEEDEXPORT-JSON-04-001 | Surface new advisory fields in JSON exporter
Update schemas/offline bundle + fixtures once model/core parity lands.
2025-10-15: `dotnet test src/StellaOps.Concelier.Exporter.Json.Tests` validated canonical metric/CWE emission. | +| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Exporter.TrivyDb/TASKS.md | DONE (2025-10-15) | Team Exporters – Trivy DB | FEEDEXPORT-TRIVY-04-001 | Propagate new advisory fields into Trivy DB package
Extend Bolt builder, metadata, and regression tests for the expanded schema.
2025-10-15: `dotnet test src/StellaOps.Concelier.Exporter.TrivyDb.Tests` confirmed canonical metric/CWE propagation. | +| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Connector.Ghsa/TASKS.md | DONE (2025-10-16) | Team Connector Regression Fixtures | FEEDCONN-GHSA-04-004 | Harden CVSS fallback so canonical metric ids persist when GitHub omits vectors; extend fixtures and document severity precedence hand-off to Merge. | +| Sprint 4 | Schema Parity & Freshness Alignment | src/StellaOps.Concelier.Connector.Osv/TASKS.md | DONE (2025-10-16) | Team Connector Expansion – GHSA/NVD/OSV | FEEDCONN-OSV-04-005 | Map OSV advisories lacking CVSS vectors to canonical metric ids/notes and document CWE provenance quirks; schedule parity fixture updates. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Core/TASKS.md | DONE (2025-10-15) | Team Excititor Core & Policy | EXCITITOR-CORE-01-001 | Stand up canonical VEX claim/consensus records with deterministic serializers so Storage/Exports share a stable contract. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Core/TASKS.md | DONE (2025-10-15) | Team Excititor Core & Policy | EXCITITOR-CORE-01-002 | Implement trust-weighted consensus resolver with baseline policy weights, justification gates, telemetry output, and majority/tie handling. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Core/TASKS.md | DONE (2025-10-15) | Team Excititor Core & Policy | EXCITITOR-CORE-01-003 | Publish shared connector/exporter/attestation abstractions and deterministic query signature utilities for cache/attestation workflows. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Policy/TASKS.md | DONE (2025-10-15) | Team Excititor Policy | EXCITITOR-POLICY-01-001 | Established policy options & snapshot provider covering baseline weights/overrides. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Policy/TASKS.md | DONE (2025-10-15) | Team Excititor Policy | EXCITITOR-POLICY-01-002 | Policy evaluator now feeds consensus resolver with immutable snapshots. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Policy/TASKS.md | DONE (2025-10-16) | Team Excititor Policy | EXCITITOR-POLICY-01-003 | Author policy diagnostics, CLI/WebService surfacing, and documentation updates. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Policy/TASKS.md | DONE (2025-10-16) | Team Excititor Policy | EXCITITOR-POLICY-01-004 | Implement YAML/JSON schema validation and deterministic diagnostics for operator bundles. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Policy/TASKS.md | DONE (2025-10-16) | Team Excititor Policy | EXCITITOR-POLICY-01-005 | Add policy change tracking, snapshot digests, and telemetry/logging hooks. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Storage.Mongo/TASKS.md | DONE (2025-10-15) | Team Excititor Storage | EXCITITOR-STORAGE-01-001 | Mongo mapping registry plus raw/export entities and DI extensions in place. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Storage.Mongo/TASKS.md | DONE (2025-10-16) | Team Excititor Storage | EXCITITOR-STORAGE-01-004 | Build provider/consensus/cache class maps and related collections. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Export/TASKS.md | DONE (2025-10-15) | Team Excititor Export | EXCITITOR-EXPORT-01-001 | Export engine delivers cache lookup, manifest creation, and policy integration. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Export/TASKS.md | DONE (2025-10-17) | Team Excititor Export | EXCITITOR-EXPORT-01-004 | Connect export engine to attestation client and persist Rekor metadata. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Attestation/TASKS.md | DONE (2025-10-16) | Team Excititor Attestation | EXCITITOR-ATTEST-01-001 | Implement in-toto predicate + DSSE builder providing envelopes for export attestation. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.Connectors.Abstractions/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors | EXCITITOR-CONN-ABS-01-001 | Deliver shared connector context/base classes so provider plug-ins can be activated via WebService/Worker. | +| Sprint 5 | Excititor Core Foundations | src/StellaOps.Excititor.WebService/TASKS.md | DONE (2025-10-17) | Team Excititor WebService | EXCITITOR-WEB-01-001 | Scaffold minimal API host, DI, and `/excititor/status` endpoint integrating policy, storage, export, and attestation services. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Worker/TASKS.md | DONE (2025-10-17) | Team Excititor Worker | EXCITITOR-WORKER-01-001 | Create Worker host with provider scheduling and logging to drive recurring pulls/reconciliation. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Formats.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Formats | EXCITITOR-FMT-CSAF-01-001 | Implement CSAF normalizer foundation translating provider documents into `VexClaim` entries. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Formats.CycloneDX/TASKS.md | DONE (2025-10-17) | Team Excititor Formats | EXCITITOR-FMT-CYCLONE-01-001 | Implement CycloneDX VEX normalizer capturing `analysis` state and component references. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Formats.OpenVEX/TASKS.md | DONE (2025-10-17) | Team Excititor Formats | EXCITITOR-FMT-OPENVEX-01-001 | Implement OpenVEX normalizer to ingest attestations into canonical claims with provenance. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Red Hat | EXCITITOR-CONN-RH-01-001 | Ship Red Hat CSAF provider metadata discovery enabling incremental pulls. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Red Hat | EXCITITOR-CONN-RH-01-002 | Fetch CSAF windows with ETag handling, resume tokens, quarantine on schema errors, and persist raw docs. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Red Hat | EXCITITOR-CONN-RH-01-003 | Populate provider trust overrides (cosign issuer, identity regex) and provenance hints for policy evaluation/logging. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Red Hat | EXCITITOR-CONN-RH-01-004 | Persist resume cursors (last updated timestamp/document hashes) in storage and reload during fetch to avoid duplicates. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Red Hat | EXCITITOR-CONN-RH-01-005 | Register connector in Worker/WebService DI, add scheduled jobs, and document CLI triggers for Red Hat CSAF pulls. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.RedHat.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Red Hat | EXCITITOR-CONN-RH-01-006 | Add CSAF normalization parity fixtures ensuring RHSA-specific metadata is preserved. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.Cisco.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Cisco | EXCITITOR-CONN-CISCO-01-001 | Implement Cisco CSAF endpoint discovery/auth to unlock paginated pulls. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.Cisco.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Cisco | EXCITITOR-CONN-CISCO-01-002 | Implement Cisco CSAF paginated fetch loop with dedupe and raw persistence support. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.SUSE.RancherVEXHub/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – SUSE | EXCITITOR-CONN-SUSE-01-001 | Build Rancher VEX Hub discovery/subscription path with offline snapshot support. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.MSRC.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – MSRC | EXCITITOR-CONN-MS-01-001 | Deliver AAD onboarding/token cache for MSRC CSAF ingestion. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.Oracle.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Oracle | EXCITITOR-CONN-ORACLE-01-001 | Implement Oracle CSAF catalogue discovery with CPU calendar awareness. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.Ubuntu.CSAF/TASKS.md | DONE (2025-10-17) | Team Excititor Connectors – Ubuntu | EXCITITOR-CONN-UBUNTU-01-001 | Implement Ubuntu CSAF discovery and channel selection for USN ingestion. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest/TASKS.md | DONE (2025-10-18) | Team Excititor Connectors – OCI | EXCITITOR-CONN-OCI-01-001 | Wire OCI discovery/auth to fetch OpenVEX attestations for configured images. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest/TASKS.md | DONE (2025-10-18) | Team Excititor Connectors – OCI | EXCITITOR-CONN-OCI-01-002 | Attestation fetch & verify loop – download DSSE attestations, trigger verification, handle retries/backoff, persist raw statements. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest/TASKS.md | DONE (2025-10-18) | Team Excititor Connectors – OCI | EXCITITOR-CONN-OCI-01-003 | Provenance metadata & policy hooks – emit image, subject digest, issuer, and trust metadata for policy weighting/logging. | +| Sprint 6 | Excititor Ingest & Formats | src/StellaOps.Cli/TASKS.md | DONE (2025-10-18) | DevEx/CLI | EXCITITOR-CLI-01-001 | Add `excititor` CLI verbs bridging to WebService with consistent auth and offline UX. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Core/TASKS.md | DONE (2025-10-19) | Team Excititor Core & Policy | EXCITITOR-CORE-02-001 | Context signal schema prep – extend consensus models with severity/KEV/EPSS fields and update canonical serializers. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Policy/TASKS.md | DONE (2025-10-19) | Team Excititor Policy | EXCITITOR-POLICY-02-001 | Scoring coefficients & weight ceilings – add α/β options, weight boosts, and validation guidance. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Team Excititor Storage | EXCITITOR-STORAGE-02-001 | Statement events & scoring signals – create immutable VEX statement store plus consensus extensions with indexes/migrations. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.WebService/TASKS.md | TODO | Team Excititor WebService | EXCITITOR-WEB-01-004 | Resolve API & signed responses – expose `/excititor/resolve`, return signed consensus/score envelopes, document auth. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.WebService/TASKS.md | TODO | Team Excititor WebService | EXCITITOR-WEB-01-005 | Mirror distribution endpoints – expose download APIs for downstream Excititor instances. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Attestation/TASKS.md | DONE (2025-10-16) | Team Excititor Attestation | EXCITITOR-ATTEST-01-002 | Rekor v2 client integration – ship transparency log client with retries and offline queue. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Worker/TASKS.md | TODO | Team Excititor Worker | EXCITITOR-WORKER-01-004 | TTL refresh & stability damper – schedule re-resolve loops and guard against status flapping. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Export/TASKS.md | TODO | Team Excititor Export | EXCITITOR-EXPORT-01-005 | Score & resolve envelope surfaces – include signed consensus/score artifacts in exports. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Export/TASKS.md | TODO | Team Excititor Export | EXCITITOR-EXPORT-01-006 | Quiet provenance packaging – attach quieted-by statement IDs, signers, justification codes to exports and attestations. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Export/TASKS.md | TODO | Team Excititor Export | EXCITITOR-EXPORT-01-007 | Mirror bundle + domain manifest – publish signed consensus bundles for mirrors. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Excititor.Connectors.StellaOpsMirror/TASKS.md | TODO | Excititor Connectors – Stella | EXCITITOR-CONN-STELLA-07-001 | Excititor mirror connector – ingest signed mirror bundles and map to VexClaims with resume handling. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Core/TASKS.md | TODO | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-07-001 | Advisory event log & asOf queries – surface immutable statements and replay capability. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Core/TASKS.md | TODO | Team Core Engine & Data Science | FEEDCORE-ENGINE-07-002 | Noise prior computation service – learn false-positive priors and expose deterministic summaries. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Core/TASKS.md | TODO | Team Core Engine & Storage Analytics | FEEDCORE-ENGINE-07-003 | Unknown state ledger & confidence seeding – persist unknown flags, seed confidence bands, expose query surface. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Team Normalization & Storage Backbone | FEEDSTORAGE-DATA-07-001 | Advisory statement & conflict collections – provision Mongo schema/indexes for event-sourced merge. | +| Sprint 7 | Contextual Truth Foundations | src/StellaOps.Concelier.Merge/TASKS.md | TODO | BE-Merge | FEEDMERGE-ENGINE-07-001 | Conflict sets & explainers – persist conflict materialization and replay hashes for merge decisions. | +| Sprint 8 | Mongo strengthening | src/StellaOps.Concelier.Storage.Mongo/TASKS.md | TODO | Team Normalization & Storage Backbone | FEEDSTORAGE-MONGO-08-001 | Causal-consistent Concelier storage sessions
Ensure `AddMongoStorage` registers a scoped session facilitator (causal consistency + majority concerns), update repositories to accept optional session handles, and add integration coverage proving read-your-write and monotonic reads across a replica set/election scenario. | +| Sprint 8 | Mongo strengthening | src/StellaOps.Authority/TASKS.md | TODO | Authority Core & Storage Guild | AUTHSTORAGE-MONGO-08-001 | Harden Authority Mongo usage
Introduce scoped MongoDB sessions with `writeConcern`/`readConcern` majority defaults, flow the session through stores used in mutations + follow-up reads, and document middleware pattern for web/API & GraphQL layers. | +| Sprint 8 | Mongo strengthening | src/StellaOps.Excititor.Storage.Mongo/TASKS.md | TODO | Team Excititor Storage | EXCITITOR-STORAGE-MONGO-08-001 | Causal consistency for Excititor repositories
Register Mongo options with majority defaults, push session-aware overloads through raw/export/consensus/cache stores, and extend migration/tests to validate causal reads after writes (including GridFS-backed content) under replica-set failover. | +| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.Exporter.Json/TASKS.md | TODO | Concelier Export Guild | CONCELIER-EXPORT-08-201 | Mirror bundle + domain manifest – produce signed JSON aggregates for `*.stella-ops.org` mirrors. | +| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.Exporter.TrivyDb/TASKS.md | TODO | Concelier Export Guild | CONCELIER-EXPORT-08-202 | Mirror-ready Trivy DB bundles – ship domain-specific archives + metadata for downstream sync. | +| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.WebService/TASKS.md | TODO | Concelier WebService Guild | CONCELIER-WEB-08-201 | Mirror distribution endpoints – expose domain-scoped index/download APIs with auth/quota. | +| Sprint 8 | Mirror Distribution | src/StellaOps.Concelier.Connector.StellaOpsMirror/TASKS.md | TODO | BE-Conn-Stella | FEEDCONN-STELLA-08-001 | Concelier mirror connector – fetch mirror manifest, verify signatures, and hydrate canonical DTOs with resume support. | +| Sprint 8 | Mirror Distribution | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-MIRROR-08-001 | Managed mirror deployments for `*.stella-ops.org` – Helm/Compose overlays, CDN, runbooks. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Core/TASKS.md | DONE (2025-10-18) | Team Scanner Core | SCANNER-CORE-09-501 | Define shared DTOs (ScanJob, ProgressEvent), error taxonomy, and deterministic ID/timestamp helpers aligning with `ARCHITECTURE_SCANNER.md` §3–§4. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Core/TASKS.md | DONE (2025-10-18) | Team Scanner Core | SCANNER-CORE-09-502 | Observability helpers (correlation IDs, logging scopes, metric namespacing, deterministic hashes) consumed by WebService/Worker. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Core/TASKS.md | DONE (2025-10-18) | Team Scanner Core | SCANNER-CORE-09-503 | Security utilities: Authority client factory, OpTok caching, DPoP verifier, restart-time plug-in guardrails for scanner components. | +| Sprint 9 | Scanner Build-time | src/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md | DONE (2025-10-19) | BuildX Guild | SP9-BLDX-09-001 | Buildx driver scaffold + handshake with Scanner.Emit (local CAS). | +| Sprint 9 | Scanner Build-time | src/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md | DONE (2025-10-19) | BuildX Guild | SP9-BLDX-09-002 | OCI annotations + provenance hand-off to Attestor. | +| Sprint 9 | Scanner Build-time | src/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md | DONE (2025-10-19) | BuildX Guild | SP9-BLDX-09-003 | CI demo: minimal SBOM push & backend report wiring. | +| Sprint 9 | Scanner Build-time | src/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md | DONE (2025-10-19) | BuildX Guild | SP9-BLDX-09-004 | Stabilize descriptor nonce derivation so repeated builds emit deterministic placeholders. | +| Sprint 9 | Scanner Build-time | src/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md | DONE (2025-10-19) | BuildX Guild | SP9-BLDX-09-005 | Integrate determinism guard into GitHub/Gitea workflows and archive proof artifacts. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-18) | Team Scanner WebService | SCANNER-WEB-09-101 | Minimal API host with Authority enforcement, health/ready endpoints, and restart-time plug-in loader per architecture §1, §4. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-18) | Team Scanner WebService | SCANNER-WEB-09-102 | `/api/v1/scans` submission/status endpoints with deterministic IDs, validation, and cancellation support. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Team Scanner WebService | SCANNER-WEB-09-103 | Progress streaming (SSE/JSONL) with correlation IDs and ISO-8601 UTC timestamps, documented in API reference. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-19) | Team Scanner WebService | SCANNER-WEB-09-104 | Configuration binding for Mongo, MinIO, queue, feature flags; startup diagnostics and fail-fast policy. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Team Scanner WebService | SCANNER-POLICY-09-105 | Policy snapshot loader + schema + OpenAPI (YAML ignore rules, VEX include/exclude, vendor precedence). | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Team Scanner WebService | SCANNER-POLICY-09-106 | `/reports` verdict assembly (Feedser+Vexer+Policy) + signed response envelope. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Team Scanner WebService | SCANNER-POLICY-09-107 | Expose score inputs, config version, and quiet provenance in `/reports` JSON and signed payload. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Worker/TASKS.md | DONE (2025-10-19) | Team Scanner Worker | SCANNER-WORKER-09-201 | Worker host bootstrap with Authority auth, hosted services, and graceful shutdown semantics. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Worker/TASKS.md | DONE (2025-10-19) | Team Scanner Worker | SCANNER-WORKER-09-202 | Lease/heartbeat loop with retry+jitter, poison-job quarantine, structured logging. | | Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Worker/TASKS.md | DONE (2025-10-19) | Team Scanner Worker | SCANNER-WORKER-09-203 | Analyzer dispatch skeleton emitting deterministic stage progress and honoring cancellation tokens. | | Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Worker/TASKS.md | DONE (2025-10-19) | Team Scanner Worker | SCANNER-WORKER-09-204 | Worker metrics (queue latency, stage duration, failure counts) with OpenTelemetry resource wiring. | -| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | DONE | Policy Guild | POLICY-CORE-09-001 | Policy schema + binder + diagnostics. | -| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | DONE | Policy Guild | POLICY-CORE-09-002 | Policy snapshot store + revision digests. | -| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | DONE | Policy Guild | POLICY-CORE-09-003 | `/policy/preview` API (image digest → projected verdict diff). | -| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-CORE-09-004 | Versioned scoring config with schema validation, trust table, and golden fixtures. | -| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-CORE-09-005 | Scoring/quiet engine – compute score, enforce VEX-only quiet rules, emit inputs and provenance. | -| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-CORE-09-006 | Unknown state & confidence decay – deterministic bands surfaced in policy outputs. | -| Sprint 9 | DevOps Foundations | ops/devops/TASKS.md | DONE (2025-10-19) | DevOps Guild | DEVOPS-HELM-09-001 | Helm/Compose environment profiles (dev/staging/airgap) with deterministic digests. | -| Sprint 9 | Docs & Governance | docs/TASKS.md | DONE (2025-10-19) | Docs Guild, DevEx | DOCS-ADR-09-001 | Establish ADR process and template. | -| Sprint 9 | Docs & Governance | docs/TASKS.md | DONE (2025-10-19) | Docs Guild, Platform Events | DOCS-EVENTS-09-002 | Publish event schema catalog (`docs/events/`) for critical envelopes. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Storage/TASKS.md | DONE (2025-10-19) | Team Scanner Storage | SCANNER-STORAGE-09-301 | Mongo catalog schemas/indexes for images, layers, artifacts, jobs, lifecycle rules plus migrations. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Storage/TASKS.md | DONE (2025-10-19) | Team Scanner Storage | SCANNER-STORAGE-09-302 | MinIO layout, immutability policies, client abstraction, and configuration binding. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Storage/TASKS.md | DONE (2025-10-19) | Team Scanner Storage | SCANNER-STORAGE-09-303 | Repositories/services with dual-write feature flag, deterministic digests, TTL enforcement tests. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Queue/TASKS.md | DONE (2025-10-19) | Team Scanner Queue | SCANNER-QUEUE-09-401 | Queue abstraction + Redis Streams adapter with ack/claim APIs and idempotency tokens. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Queue/TASKS.md | DONE (2025-10-19) | Team Scanner Queue | SCANNER-QUEUE-09-402 | Pluggable backend support (Redis, NATS) with configuration binding, health probes, failover docs. | -| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Queue/TASKS.md | DONE (2025-10-19) | Team Scanner Queue | SCANNER-QUEUE-09-403 | Retry + dead-letter strategy with structured logs/metrics for offline deployments. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-101 | Implement layer cache store keyed by layer digest with metadata retention per architecture §3.3. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-102 | Build file CAS with dedupe, TTL enforcement, and offline import/export hooks. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-103 | Expose cache metrics/logging and configuration toggles for warm/cold thresholds. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-104 | Implement cache invalidation workflows (layer delete, TTL expiry, diff invalidation). | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-201 | Alpine/apk analyzer emitting deterministic components with provenance. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-202 | Debian/dpkg analyzer mapping packages to purl identity with evidence. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-203 | RPM analyzer capturing EVR, file listings, provenance. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-204 | Shared OS evidence helpers for package identity + provenance. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-205 | Vendor metadata enrichment (source packages, license, CVE hints). | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-206 | Determinism harness + fixtures for OS analyzers. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-207 | Package OS analyzers as restart-time plug-ins (manifest + host registration). | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-301 | Java analyzer emitting `pkg:maven` with provenance. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-302 | Node analyzer handling workspaces/symlinks emitting `pkg:npm`. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-303 | Python analyzer reading `*.dist-info`, RECORD hashes, entry points. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-304 | Go analyzer leveraging buildinfo for `pkg:golang` components. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-305 | .NET analyzer parsing `*.deps.json`, assembly metadata, RID variants. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-306 | Rust analyzer detecting crates or falling back to `bin:{sha256}`. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Shared language evidence helpers + usage flag propagation. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-308 | Determinism + fixture harness for language analyzers. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-309 | Package language analyzers as restart-time plug-ins (manifest + host registration). | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-401 | POSIX shell AST parser with deterministic output. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-402 | Command resolution across layered rootfs with evidence attribution. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-403 | Interpreter tracing for shell wrappers to Python/Node/Java launchers. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-404 | Python entry analyzer (venv shebang, module invocation, usage flag). | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-405 | Node/Java launcher analyzer capturing script/jar targets. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-406 | Explainability + diagnostics for unresolved constructs with metrics. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-407 | Package EntryTrace analyzers as restart-time plug-ins (manifest + host registration). | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Diff/TASKS.md | TODO | Diff Guild | SCANNER-DIFF-10-501 | Build component differ tracking add/remove/version changes with deterministic ordering. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Diff/TASKS.md | TODO | Diff Guild | SCANNER-DIFF-10-502 | Attribute diffs to introducing/removing layers including provenance evidence. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Diff/TASKS.md | TODO | Diff Guild | SCANNER-DIFF-10-503 | Produce JSON diff output for inventory vs usage views aligned with API contract. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-601 | Compose inventory SBOM (CycloneDX JSON/Protobuf) from layer fragments. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-602 | Compose usage SBOM leveraging EntryTrace to flag actual usage. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-603 | Generate BOM index sidecar (purl table + roaring bitmap + usage flag). | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-604 | Package artifacts for export + attestation with deterministic manifests. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-605 | Emit BOM-Index sidecar schema/fixtures (CRITICAL PATH for SP16). | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-606 | Usage view bit flags integrated with EntryTrace. | -| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-607 | Embed scoring inputs, confidence band, and quiet provenance in CycloneDX/DSSE artifacts. | -| Sprint 10 | Benchmarks | bench/TASKS.md | TODO | Bench Guild, Scanner Team | BENCH-SCANNER-10-001 | Analyzer microbench harness + baseline CSV. | -| Sprint 10 | Samples | samples/TASKS.md | TODO | Samples Guild, Scanner Team | SAMPLES-10-001 | Sample images with SBOM/BOM-Index sidecars. | -| Sprint 10 | DevOps Perf | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-PERF-10-001 | Perf smoke job ensuring <5 s SBOM compose. | -| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-DPOP-11-001 | Implement DPoP proof validation + nonce handling for high-value audiences per architecture. | -| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-MTLS-11-002 | Add OAuth mTLS client credential support with certificate-bound tokens and introspection updates. | -| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Signer/TASKS.md | TODO | Signer Guild | SIGNER-API-11-101 | `/sign/dsse` pipeline with Authority auth, PoE introspection, release verification, DSSE signing. | -| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Signer/TASKS.md | TODO | Signer Guild | SIGNER-REF-11-102 | `/verify/referrers` endpoint with OCI lookup, caching, and policy enforcement. | -| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Signer/TASKS.md | TODO | Signer Guild | SIGNER-QUOTA-11-103 | Enforce plan quotas, concurrency/QPS limits, artifact size caps with metrics/audit logs. | -| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Attestor/TASKS.md | TODO | Attestor Guild | ATTESTOR-API-11-201 | `/rekor/entries` submission pipeline with dedupe, proof acquisition, and persistence. | -| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Attestor/TASKS.md | TODO | Attestor Guild | ATTESTOR-VERIFY-11-202 | `/rekor/verify` + retrieval endpoints validating signatures and Merkle proofs. | -| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Attestor/TASKS.md | TODO | Attestor Guild | ATTESTOR-OBS-11-203 | Telemetry, alerting, mTLS hardening, and archive workflow for Attestor. | -| Sprint 11 | UI Integration | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-ATTEST-11-005 | Attestation visibility (Rekor id, status) on Scan Detail. | -| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Core/TASKS.md | TODO | Zastava Core Guild | ZASTAVA-CORE-12-201 | Define runtime event/admission DTOs, hashing helpers, and versioning strategy. | -| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Core/TASKS.md | TODO | Zastava Core Guild | ZASTAVA-CORE-12-202 | Provide configuration/logging/metrics utilities shared by Observer/Webhook. | -| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Core/TASKS.md | TODO | Zastava Core Guild | ZASTAVA-CORE-12-203 | Authority client helpers, OpTok caching, and security guardrails for runtime services. | -| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Core/TASKS.md | TODO | Zastava Core Guild | ZASTAVA-OPS-12-204 | Operational runbooks, alert rules, and dashboard exports for runtime plane. | -| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Observer/TASKS.md | TODO | Zastava Observer Guild | ZASTAVA-OBS-12-001 | Container lifecycle watcher emitting deterministic runtime events with buffering. | -| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Observer/TASKS.md | TODO | Zastava Observer Guild | ZASTAVA-OBS-12-002 | Capture entrypoint traces + loaded libraries, hashing binaries and linking to baseline SBOM. | -| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Observer/TASKS.md | TODO | Zastava Observer Guild | ZASTAVA-OBS-12-003 | Posture checks for signatures/SBOM/attestation with offline caching. | -| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Observer/TASKS.md | TODO | Zastava Observer Guild | ZASTAVA-OBS-12-004 | Batch `/runtime/events` submissions with disk-backed buffer and rate limits. | -| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Webhook/TASKS.md | TODO | Zastava Webhook Guild | ZASTAVA-WEBHOOK-12-101 | Admission controller host with TLS bootstrap and Authority auth. | -| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Webhook/TASKS.md | TODO | Zastava Webhook Guild | ZASTAVA-WEBHOOK-12-102 | Query Scanner `/policy/runtime`, resolve digests, enforce verdicts. | -| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Webhook/TASKS.md | TODO | Zastava Webhook Guild | ZASTAVA-WEBHOOK-12-103 | Caching, fail-open/closed toggles, metrics/logging for admission decisions. | -| Sprint 12 | Runtime Guardrails | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-RUNTIME-12-301 | `/runtime/events` ingestion endpoint with validation, batching, storage hooks. | -| Sprint 12 | Runtime Guardrails | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-RUNTIME-12-302 | `/policy/runtime` endpoint joining SBOM baseline + policy verdict with TTL guidance. | -| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-AUTH-13-001 | Integrate Authority OIDC + DPoP flows with session management. | -| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SCANS-13-002 | Build scans module (list/detail/SBOM/diff/attestation) with performance + accessibility targets. | -| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-VEX-13-003 | Implement VEX explorer + policy editor with preview integration. | -| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-ADMIN-13-004 | Deliver admin area (tenants/clients/quotas/licensing) with RBAC + audit hooks. | -| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SCHED-13-005 | Scheduler panel: schedules CRUD, run history, dry-run preview. | -| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-NOTIFY-13-006 | Notify panel: channels/rules CRUD, deliveries view, test send. | -| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI | CLI-RUNTIME-13-005 | Add runtime policy test verbs that consume `/policy/runtime` and display verdicts. | -| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI | CLI-OFFLINE-13-006 | Implement offline kit pull/import/status commands with integrity checks. | -| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI | CLI-PLUGIN-13-007 | Package non-core CLI verbs as restart-time plug-ins (manifest + loader tests). | -| Sprint 14 | Release & Offline Ops | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-REL-14-001 | Deterministic build/release pipeline with SBOM/provenance, signing, and manifest generation. | -| Sprint 14 | Release & Offline Ops | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild | DEVOPS-OFFLINE-14-002 | Offline kit packaging workflow with integrity verification and documentation. | -| Sprint 14 | Release & Offline Ops | ops/deployment/TASKS.md | TODO | Deployment Guild | DEVOPS-OPS-14-003 | Deployment/update/rollback automation and channel management documentation. | -| Sprint 14 | Release & Offline Ops | ops/licensing/TASKS.md | TODO | Licensing Guild | DEVOPS-LIC-14-004 | Registry token service tied to Authority, plan gating, revocation handling, monitoring. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Models/TASKS.md | TODO | Notify Models Guild | NOTIFY-MODELS-15-101 | Define core Notify DTOs, validation helpers, canonical serialization. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Models/TASKS.md | TODO | Notify Models Guild | NOTIFY-MODELS-15-102 | Publish schema docs and sample payloads for Notify. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Models/TASKS.md | TODO | Notify Models Guild | NOTIFY-MODELS-15-103 | Versioning/migration helpers for rules/templates/deliveries. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Storage.Mongo/TASKS.md | TODO | Notify Storage Guild | NOTIFY-STORAGE-15-201 | Mongo schemas/indexes for rules, channels, deliveries, digests, locks, audit. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Storage.Mongo/TASKS.md | TODO | Notify Storage Guild | NOTIFY-STORAGE-15-202 | Repositories with tenant scoping, soft delete, TTL, causal consistency options. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Storage.Mongo/TASKS.md | TODO | Notify Storage Guild | NOTIFY-STORAGE-15-203 | Delivery history retention and query APIs. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Queue/TASKS.md | TODO | Notify Queue Guild | NOTIFY-QUEUE-15-401 | Bus abstraction + Redis Streams adapter with ordering/idempotency. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Queue/TASKS.md | TODO | Notify Queue Guild | NOTIFY-QUEUE-15-402 | NATS JetStream adapter with health probes and failover. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Queue/TASKS.md | TODO | Notify Queue Guild | NOTIFY-QUEUE-15-403 | Delivery queue with retry/dead-letter + metrics. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Engine/TASKS.md | TODO | Notify Engine Guild | NOTIFY-ENGINE-15-301 | Rules evaluation core (filters, throttles, idempotency). | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Engine/TASKS.md | TODO | Notify Engine Guild | NOTIFY-ENGINE-15-302 | Action planner + digest coalescer. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Engine/TASKS.md | TODO | Notify Engine Guild | NOTIFY-ENGINE-15-303 | Template rendering engine (Slack/Teams/Email/Webhook). | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Engine/TASKS.md | TODO | Notify Engine Guild | NOTIFY-ENGINE-15-304 | Test-send sandbox + preview utilities. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.WebService/TASKS.md | TODO | Notify WebService Guild | NOTIFY-WEB-15-101 | Minimal API host with Authority enforcement and plug-in loading. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.WebService/TASKS.md | TODO | Notify WebService Guild | NOTIFY-WEB-15-102 | Rules/channel/template CRUD with audit logging. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.WebService/TASKS.md | TODO | Notify WebService Guild | NOTIFY-WEB-15-103 | Delivery history & test-send endpoints. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.WebService/TASKS.md | TODO | Notify WebService Guild | NOTIFY-WEB-15-104 | Configuration binding + startup diagnostics. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Worker/TASKS.md | TODO | Notify Worker Guild | NOTIFY-WORKER-15-201 | Bus subscription + leasing loop with backoff. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Worker/TASKS.md | TODO | Notify Worker Guild | NOTIFY-WORKER-15-202 | Rules evaluation pipeline integration. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Worker/TASKS.md | TODO | Notify Worker Guild | NOTIFY-WORKER-15-203 | Channel dispatch orchestration with retries. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Worker/TASKS.md | TODO | Notify Worker Guild | NOTIFY-WORKER-15-204 | Metrics/telemetry for Notify workers. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Slack/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-SLACK-15-501 | Slack connector with rate-limit aware delivery. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Slack/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-SLACK-15-502 | Slack health/test-send support. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Teams/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-TEAMS-15-601 | Teams connector with Adaptive Cards. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Teams/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-TEAMS-15-602 | Teams health/test-send support. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Email/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-EMAIL-15-701 | SMTP connector with TLS + rendering. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Email/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-EMAIL-15-702 | DKIM + health/test-send flows. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Webhook/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-WEBHOOK-15-801 | Webhook connector with signing/retries. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Webhook/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-WEBHOOK-15-802 | Webhook health/test-send support. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Slack/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-SLACK-15-503 | Package Slack connector as restart-time plug-in (manifest + host registration). | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Teams/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-TEAMS-15-603 | Package Teams connector as restart-time plug-in (manifest + host registration). | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Email/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-EMAIL-15-703 | Package Email connector as restart-time plug-in (manifest + host registration). | -| Sprint 15 | Notify Foundations | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-EVENTS-15-201 | Emit `scanner.report.ready` + `scanner.scan.completed` events. | -| Sprint 15 | Benchmarks | bench/TASKS.md | TODO | Bench Guild, Notify Team | BENCH-NOTIFY-15-001 | Notify dispatch throughput bench with results CSV. | -| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Webhook/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-WEBHOOK-15-803 | Package Webhook connector as restart-time plug-in (manifest + host registration). | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Models/TASKS.md | TODO | Scheduler Models Guild | SCHED-MODELS-16-101 | Define Scheduler DTOs & validation. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Models/TASKS.md | TODO | Scheduler Models Guild | SCHED-MODELS-16-102 | Publish schema docs/sample payloads. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Models/TASKS.md | TODO | Scheduler Models Guild | SCHED-MODELS-16-103 | Versioning/migration helpers for schedules/runs. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Storage.Mongo/TASKS.md | TODO | Scheduler Storage Guild | SCHED-STORAGE-16-201 | Mongo schemas/indexes for Scheduler state. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Storage.Mongo/TASKS.md | TODO | Scheduler Storage Guild | SCHED-STORAGE-16-202 | Repositories with tenant scoping, TTL, causal consistency. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Storage.Mongo/TASKS.md | TODO | Scheduler Storage Guild | SCHED-STORAGE-16-203 | Audit + stats materialization for UI. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Queue/TASKS.md | TODO | Scheduler Queue Guild | SCHED-QUEUE-16-401 | Queue abstraction + Redis Streams adapter. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Queue/TASKS.md | TODO | Scheduler Queue Guild | SCHED-QUEUE-16-402 | NATS JetStream adapter with health probes. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Queue/TASKS.md | TODO | Scheduler Queue Guild | SCHED-QUEUE-16-403 | Dead-letter handling + metrics. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.ImpactIndex/TASKS.md | TODO | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-301 | Ingest BOM-Index into roaring bitmap store. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.ImpactIndex/TASKS.md | TODO | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-302 | Query APIs for ResolveByPurls/ResolveByVulns/ResolveAll. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.ImpactIndex/TASKS.md | TODO | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-303 | Snapshot/compaction/invalidation workflow. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.ImpactIndex/TASKS.md | DOING | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-300 | **STUB** ImpactIndex ingest/query using fixtures (to be removed by SP16 completion). | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-WEB-16-101 | Minimal API host with Authority enforcement. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-WEB-16-102 | Schedules CRUD (cron validation, pause/resume, audit). | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-WEB-16-103 | Runs API (list/detail/cancel) + impact previews. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-WEB-16-104 | Feedser/Vexer webhook handlers with security enforcement. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-16-201 | Planner loop (cron/event triggers, leases, fairness). | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-16-202 | ImpactIndex targeting and shard planning. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-16-203 | Runner execution invoking Scanner analysis/content refresh. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-16-204 | Emit rescan/report events for Notify/UI. | -| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-16-205 | Metrics/telemetry for Scheduler planners/runners. | -| Sprint 16 | Benchmarks | bench/TASKS.md | TODO | Bench Guild, Scheduler Team | BENCH-IMPACT-16-001 | ImpactIndex throughput bench + RAM profile. | -| Sprint 17 | Symbol Intelligence & Forensics | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-17-701 | Record GNU build-id for ELF components and surface it in SBOM/diff outputs. | -| Sprint 17 | Symbol Intelligence & Forensics | src/StellaOps.Zastava.Observer/TASKS.md | TODO | Zastava Observer Guild | ZASTAVA-OBS-17-005 | Collect GNU build-id during runtime observation and attach it to emitted events. | -| Sprint 17 | Symbol Intelligence & Forensics | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-RUNTIME-17-401 | Persist runtime build-id observations and expose them for debug-symbol correlation. | -| Sprint 17 | Symbol Intelligence & Forensics | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-REL-17-002 | Ship stripped debug artifacts organised by build-id within release/offline kits. | -| Sprint 17 | Symbol Intelligence & Forensics | docs/TASKS.md | TODO | Docs Guild | DOCS-RUNTIME-17-004 | Document build-id workflows for SBOMs, runtime events, and debug-store usage. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Worker/TASKS.md | DONE (2025-10-19) | Team Scanner Worker | SCANNER-WORKER-09-205 | Harden heartbeat jitter so lease safety margin stays ≥3× and cover with regression tests + optional live queue smoke run. | +| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | DONE | Policy Guild | POLICY-CORE-09-001 | Policy schema + binder + diagnostics. | +| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | DONE | Policy Guild | POLICY-CORE-09-002 | Policy snapshot store + revision digests. | +| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | DONE | Policy Guild | POLICY-CORE-09-003 | `/policy/preview` API (image digest → projected verdict diff). | +| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-CORE-09-004 | Versioned scoring config with schema validation, trust table, and golden fixtures. | +| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-CORE-09-005 | Scoring/quiet engine – compute score, enforce VEX-only quiet rules, emit inputs and provenance. | +| Sprint 9 | Policy Foundations | src/StellaOps.Policy/TASKS.md | TODO | Policy Guild | POLICY-CORE-09-006 | Unknown state & confidence decay – deterministic bands surfaced in policy outputs. | +| Sprint 9 | DevOps Foundations | ops/devops/TASKS.md | DONE (2025-10-19) | DevOps Guild | DEVOPS-HELM-09-001 | Helm/Compose environment profiles (dev/staging/airgap) with deterministic digests. | +| Sprint 9 | Docs & Governance | docs/TASKS.md | DONE (2025-10-19) | Docs Guild, DevEx | DOCS-ADR-09-001 | Establish ADR process and template. | +| Sprint 9 | Docs & Governance | docs/TASKS.md | DONE (2025-10-19) | Docs Guild, Platform Events | DOCS-EVENTS-09-002 | Publish event schema catalog (`docs/events/`) for critical envelopes. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Storage/TASKS.md | DONE (2025-10-19) | Team Scanner Storage | SCANNER-STORAGE-09-301 | Mongo catalog schemas/indexes for images, layers, artifacts, jobs, lifecycle rules plus migrations. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Storage/TASKS.md | DONE (2025-10-19) | Team Scanner Storage | SCANNER-STORAGE-09-302 | MinIO layout, immutability policies, client abstraction, and configuration binding. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Storage/TASKS.md | DONE (2025-10-19) | Team Scanner Storage | SCANNER-STORAGE-09-303 | Repositories/services with dual-write feature flag, deterministic digests, TTL enforcement tests. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Queue/TASKS.md | DONE (2025-10-19) | Team Scanner Queue | SCANNER-QUEUE-09-401 | Queue abstraction + Redis Streams adapter with ack/claim APIs and idempotency tokens. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Queue/TASKS.md | DONE (2025-10-19) | Team Scanner Queue | SCANNER-QUEUE-09-402 | Pluggable backend support (Redis, NATS) with configuration binding, health probes, failover docs. | +| Sprint 9 | Scanner Core Foundations | src/StellaOps.Scanner.Queue/TASKS.md | DONE (2025-10-19) | Team Scanner Queue | SCANNER-QUEUE-09-403 | Retry + dead-letter strategy with structured logs/metrics for offline deployments. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-101 | Implement layer cache store keyed by layer digest with metadata retention per architecture §3.3. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-102 | Build file CAS with dedupe, TTL enforcement, and offline import/export hooks. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-103 | Expose cache metrics/logging and configuration toggles for warm/cold thresholds. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-104 | Implement cache invalidation workflows (layer delete, TTL expiry, diff invalidation). | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-201 | Alpine/apk analyzer emitting deterministic components with provenance. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-202 | Debian/dpkg analyzer mapping packages to purl identity with evidence. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-203 | RPM analyzer capturing EVR, file listings, provenance. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-204 | Shared OS evidence helpers for package identity + provenance. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-205 | Vendor metadata enrichment (source packages, license, CVE hints). | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-206 | Determinism harness + fixtures for OS analyzers. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-207 | Package OS analyzers as restart-time plug-ins (manifest + host registration). | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-301 | Java analyzer emitting `pkg:maven` with provenance. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-302 | Node analyzer handling workspaces/symlinks emitting `pkg:npm`. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-303 | Python analyzer reading `*.dist-info`, RECORD hashes, entry points. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-304 | Go analyzer leveraging buildinfo for `pkg:golang` components. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-305 | .NET analyzer parsing `*.deps.json`, assembly metadata, RID variants. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-306 | Rust analyzer detecting crates or falling back to `bin:{sha256}`. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Shared language evidence helpers + usage flag propagation. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-308 | Determinism + fixture harness for language analyzers. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-309 | Package language analyzers as restart-time plug-ins (manifest + host registration). | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-401 | POSIX shell AST parser with deterministic output. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-402 | Command resolution across layered rootfs with evidence attribution. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-403 | Interpreter tracing for shell wrappers to Python/Node/Java launchers. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-404 | Python entry analyzer (venv shebang, module invocation, usage flag). | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-405 | Node/Java launcher analyzer capturing script/jar targets. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-406 | Explainability + diagnostics for unresolved constructs with metrics. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-407 | Package EntryTrace analyzers as restart-time plug-ins (manifest + host registration). | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Diff/TASKS.md | TODO | Diff Guild | SCANNER-DIFF-10-501 | Build component differ tracking add/remove/version changes with deterministic ordering. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Diff/TASKS.md | TODO | Diff Guild | SCANNER-DIFF-10-502 | Attribute diffs to introducing/removing layers including provenance evidence. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Diff/TASKS.md | TODO | Diff Guild | SCANNER-DIFF-10-503 | Produce JSON diff output for inventory vs usage views aligned with API contract. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-601 | Compose inventory SBOM (CycloneDX JSON/Protobuf) from layer fragments. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-602 | Compose usage SBOM leveraging EntryTrace to flag actual usage. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-603 | Generate BOM index sidecar (purl table + roaring bitmap + usage flag). | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-604 | Package artifacts for export + attestation with deterministic manifests. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-605 | Emit BOM-Index sidecar schema/fixtures (CRITICAL PATH for SP16). | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-606 | Usage view bit flags integrated with EntryTrace. | +| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-607 | Embed scoring inputs, confidence band, and quiet provenance in CycloneDX/DSSE artifacts. | +| Sprint 10 | Benchmarks | bench/TASKS.md | TODO | Bench Guild, Scanner Team | BENCH-SCANNER-10-001 | Analyzer microbench harness + baseline CSV. | +| Sprint 10 | Samples | samples/TASKS.md | TODO | Samples Guild, Scanner Team | SAMPLES-10-001 | Sample images with SBOM/BOM-Index sidecars. | +| Sprint 10 | DevOps Perf | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-PERF-10-001 | Perf smoke job ensuring <5 s SBOM compose. | +| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-DPOP-11-001 | Implement DPoP proof validation + nonce handling for high-value audiences per architecture. | +| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-MTLS-11-002 | Add OAuth mTLS client credential support with certificate-bound tokens and introspection updates. | +| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Signer/TASKS.md | TODO | Signer Guild | SIGNER-API-11-101 | `/sign/dsse` pipeline with Authority auth, PoE introspection, release verification, DSSE signing. | +| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Signer/TASKS.md | TODO | Signer Guild | SIGNER-REF-11-102 | `/verify/referrers` endpoint with OCI lookup, caching, and policy enforcement. | +| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Signer/TASKS.md | TODO | Signer Guild | SIGNER-QUOTA-11-103 | Enforce plan quotas, concurrency/QPS limits, artifact size caps with metrics/audit logs. | +| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Attestor/TASKS.md | TODO | Attestor Guild | ATTESTOR-API-11-201 | `/rekor/entries` submission pipeline with dedupe, proof acquisition, and persistence. | +| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Attestor/TASKS.md | TODO | Attestor Guild | ATTESTOR-VERIFY-11-202 | `/rekor/verify` + retrieval endpoints validating signatures and Merkle proofs. | +| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Attestor/TASKS.md | TODO | Attestor Guild | ATTESTOR-OBS-11-203 | Telemetry, alerting, mTLS hardening, and archive workflow for Attestor. | +| Sprint 11 | UI Integration | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-ATTEST-11-005 | Attestation visibility (Rekor id, status) on Scan Detail. | +| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Core/TASKS.md | TODO | Zastava Core Guild | ZASTAVA-CORE-12-201 | Define runtime event/admission DTOs, hashing helpers, and versioning strategy. | +| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Core/TASKS.md | TODO | Zastava Core Guild | ZASTAVA-CORE-12-202 | Provide configuration/logging/metrics utilities shared by Observer/Webhook. | +| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Core/TASKS.md | TODO | Zastava Core Guild | ZASTAVA-CORE-12-203 | Authority client helpers, OpTok caching, and security guardrails for runtime services. | +| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Core/TASKS.md | TODO | Zastava Core Guild | ZASTAVA-OPS-12-204 | Operational runbooks, alert rules, and dashboard exports for runtime plane. | +| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Observer/TASKS.md | TODO | Zastava Observer Guild | ZASTAVA-OBS-12-001 | Container lifecycle watcher emitting deterministic runtime events with buffering. | +| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Observer/TASKS.md | TODO | Zastava Observer Guild | ZASTAVA-OBS-12-002 | Capture entrypoint traces + loaded libraries, hashing binaries and linking to baseline SBOM. | +| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Observer/TASKS.md | TODO | Zastava Observer Guild | ZASTAVA-OBS-12-003 | Posture checks for signatures/SBOM/attestation with offline caching. | +| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Observer/TASKS.md | TODO | Zastava Observer Guild | ZASTAVA-OBS-12-004 | Batch `/runtime/events` submissions with disk-backed buffer and rate limits. | +| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Webhook/TASKS.md | TODO | Zastava Webhook Guild | ZASTAVA-WEBHOOK-12-101 | Admission controller host with TLS bootstrap and Authority auth. | +| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Webhook/TASKS.md | TODO | Zastava Webhook Guild | ZASTAVA-WEBHOOK-12-102 | Query Scanner `/policy/runtime`, resolve digests, enforce verdicts. | +| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Webhook/TASKS.md | TODO | Zastava Webhook Guild | ZASTAVA-WEBHOOK-12-103 | Caching, fail-open/closed toggles, metrics/logging for admission decisions. | +| Sprint 12 | Runtime Guardrails | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-RUNTIME-12-301 | `/runtime/events` ingestion endpoint with validation, batching, storage hooks. | +| Sprint 12 | Runtime Guardrails | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-RUNTIME-12-302 | `/policy/runtime` endpoint joining SBOM baseline + policy verdict with TTL guidance. | +| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-AUTH-13-001 | Integrate Authority OIDC + DPoP flows with session management. | +| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SCANS-13-002 | Build scans module (list/detail/SBOM/diff/attestation) with performance + accessibility targets. | +| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-VEX-13-003 | Implement VEX explorer + policy editor with preview integration. | +| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-ADMIN-13-004 | Deliver admin area (tenants/clients/quotas/licensing) with RBAC + audit hooks. | +| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-SCHED-13-005 | Scheduler panel: schedules CRUD, run history, dry-run preview. | +| Sprint 13 | UX & CLI Experience | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-NOTIFY-13-006 | Notify panel: channels/rules CRUD, deliveries view, test send. | +| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI | CLI-RUNTIME-13-005 | Add runtime policy test verbs that consume `/policy/runtime` and display verdicts. | +| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI | CLI-OFFLINE-13-006 | Implement offline kit pull/import/status commands with integrity checks. | +| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | TODO | DevEx/CLI | CLI-PLUGIN-13-007 | Package non-core CLI verbs as restart-time plug-ins (manifest + loader tests). | +| Sprint 14 | Release & Offline Ops | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-REL-14-001 | Deterministic build/release pipeline with SBOM/provenance, signing, and manifest generation. | +| Sprint 14 | Release & Offline Ops | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild | DEVOPS-OFFLINE-14-002 | Offline kit packaging workflow with integrity verification and documentation. | +| Sprint 14 | Release & Offline Ops | ops/deployment/TASKS.md | TODO | Deployment Guild | DEVOPS-OPS-14-003 | Deployment/update/rollback automation and channel management documentation. | +| Sprint 14 | Release & Offline Ops | ops/licensing/TASKS.md | TODO | Licensing Guild | DEVOPS-LIC-14-004 | Registry token service tied to Authority, plan gating, revocation handling, monitoring. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Models/TASKS.md | TODO | Notify Models Guild | NOTIFY-MODELS-15-101 | Define core Notify DTOs, validation helpers, canonical serialization. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Models/TASKS.md | TODO | Notify Models Guild | NOTIFY-MODELS-15-102 | Publish schema docs and sample payloads for Notify. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Models/TASKS.md | TODO | Notify Models Guild | NOTIFY-MODELS-15-103 | Versioning/migration helpers for rules/templates/deliveries. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Storage.Mongo/TASKS.md | TODO | Notify Storage Guild | NOTIFY-STORAGE-15-201 | Mongo schemas/indexes for rules, channels, deliveries, digests, locks, audit. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Storage.Mongo/TASKS.md | TODO | Notify Storage Guild | NOTIFY-STORAGE-15-202 | Repositories with tenant scoping, soft delete, TTL, causal consistency options. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Storage.Mongo/TASKS.md | TODO | Notify Storage Guild | NOTIFY-STORAGE-15-203 | Delivery history retention and query APIs. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Queue/TASKS.md | TODO | Notify Queue Guild | NOTIFY-QUEUE-15-401 | Bus abstraction + Redis Streams adapter with ordering/idempotency. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Queue/TASKS.md | TODO | Notify Queue Guild | NOTIFY-QUEUE-15-402 | NATS JetStream adapter with health probes and failover. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Queue/TASKS.md | TODO | Notify Queue Guild | NOTIFY-QUEUE-15-403 | Delivery queue with retry/dead-letter + metrics. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Engine/TASKS.md | TODO | Notify Engine Guild | NOTIFY-ENGINE-15-301 | Rules evaluation core (filters, throttles, idempotency). | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Engine/TASKS.md | TODO | Notify Engine Guild | NOTIFY-ENGINE-15-302 | Action planner + digest coalescer. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Engine/TASKS.md | TODO | Notify Engine Guild | NOTIFY-ENGINE-15-303 | Template rendering engine (Slack/Teams/Email/Webhook). | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Engine/TASKS.md | TODO | Notify Engine Guild | NOTIFY-ENGINE-15-304 | Test-send sandbox + preview utilities. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.WebService/TASKS.md | TODO | Notify WebService Guild | NOTIFY-WEB-15-101 | Minimal API host with Authority enforcement and plug-in loading. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.WebService/TASKS.md | TODO | Notify WebService Guild | NOTIFY-WEB-15-102 | Rules/channel/template CRUD with audit logging. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.WebService/TASKS.md | TODO | Notify WebService Guild | NOTIFY-WEB-15-103 | Delivery history & test-send endpoints. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.WebService/TASKS.md | TODO | Notify WebService Guild | NOTIFY-WEB-15-104 | Configuration binding + startup diagnostics. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Worker/TASKS.md | TODO | Notify Worker Guild | NOTIFY-WORKER-15-201 | Bus subscription + leasing loop with backoff. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Worker/TASKS.md | TODO | Notify Worker Guild | NOTIFY-WORKER-15-202 | Rules evaluation pipeline integration. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Worker/TASKS.md | TODO | Notify Worker Guild | NOTIFY-WORKER-15-203 | Channel dispatch orchestration with retries. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Worker/TASKS.md | TODO | Notify Worker Guild | NOTIFY-WORKER-15-204 | Metrics/telemetry for Notify workers. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Slack/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-SLACK-15-501 | Slack connector with rate-limit aware delivery. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Slack/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-SLACK-15-502 | Slack health/test-send support. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Teams/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-TEAMS-15-601 | Teams connector with Adaptive Cards. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Teams/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-TEAMS-15-602 | Teams health/test-send support. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Email/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-EMAIL-15-701 | SMTP connector with TLS + rendering. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Email/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-EMAIL-15-702 | DKIM + health/test-send flows. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Webhook/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-WEBHOOK-15-801 | Webhook connector with signing/retries. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Webhook/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-WEBHOOK-15-802 | Webhook health/test-send support. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Slack/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-SLACK-15-503 | Package Slack connector as restart-time plug-in (manifest + host registration). | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Teams/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-TEAMS-15-603 | Package Teams connector as restart-time plug-in (manifest + host registration). | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Email/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-EMAIL-15-703 | Package Email connector as restart-time plug-in (manifest + host registration). | +| Sprint 15 | Notify Foundations | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-EVENTS-15-201 | Emit `scanner.report.ready` + `scanner.scan.completed` events. | +| Sprint 15 | Benchmarks | bench/TASKS.md | TODO | Bench Guild, Notify Team | BENCH-NOTIFY-15-001 | Notify dispatch throughput bench with results CSV. | +| Sprint 15 | Notify Foundations | src/StellaOps.Notify.Connectors.Webhook/TASKS.md | TODO | Notify Connectors Guild | NOTIFY-CONN-WEBHOOK-15-803 | Package Webhook connector as restart-time plug-in (manifest + host registration). | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Models/TASKS.md | TODO | Scheduler Models Guild | SCHED-MODELS-16-101 | Define Scheduler DTOs & validation. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Models/TASKS.md | TODO | Scheduler Models Guild | SCHED-MODELS-16-102 | Publish schema docs/sample payloads. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Models/TASKS.md | TODO | Scheduler Models Guild | SCHED-MODELS-16-103 | Versioning/migration helpers for schedules/runs. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Storage.Mongo/TASKS.md | TODO | Scheduler Storage Guild | SCHED-STORAGE-16-201 | Mongo schemas/indexes for Scheduler state. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Storage.Mongo/TASKS.md | TODO | Scheduler Storage Guild | SCHED-STORAGE-16-202 | Repositories with tenant scoping, TTL, causal consistency. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Storage.Mongo/TASKS.md | TODO | Scheduler Storage Guild | SCHED-STORAGE-16-203 | Audit + stats materialization for UI. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Queue/TASKS.md | TODO | Scheduler Queue Guild | SCHED-QUEUE-16-401 | Queue abstraction + Redis Streams adapter. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Queue/TASKS.md | TODO | Scheduler Queue Guild | SCHED-QUEUE-16-402 | NATS JetStream adapter with health probes. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Queue/TASKS.md | TODO | Scheduler Queue Guild | SCHED-QUEUE-16-403 | Dead-letter handling + metrics. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.ImpactIndex/TASKS.md | TODO | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-301 | Ingest BOM-Index into roaring bitmap store. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.ImpactIndex/TASKS.md | TODO | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-302 | Query APIs for ResolveByPurls/ResolveByVulns/ResolveAll. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.ImpactIndex/TASKS.md | TODO | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-303 | Snapshot/compaction/invalidation workflow. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.ImpactIndex/TASKS.md | DOING | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-300 | **STUB** ImpactIndex ingest/query using fixtures (to be removed by SP16 completion). | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-WEB-16-101 | Minimal API host with Authority enforcement. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-WEB-16-102 | Schedules CRUD (cron validation, pause/resume, audit). | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-WEB-16-103 | Runs API (list/detail/cancel) + impact previews. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.WebService/TASKS.md | TODO | Scheduler WebService Guild | SCHED-WEB-16-104 | Feedser/Vexer webhook handlers with security enforcement. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-16-201 | Planner loop (cron/event triggers, leases, fairness). | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-16-202 | ImpactIndex targeting and shard planning. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-16-203 | Runner execution invoking Scanner analysis/content refresh. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-16-204 | Emit rescan/report events for Notify/UI. | +| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Worker/TASKS.md | TODO | Scheduler Worker Guild | SCHED-WORKER-16-205 | Metrics/telemetry for Scheduler planners/runners. | +| Sprint 16 | Benchmarks | bench/TASKS.md | TODO | Bench Guild, Scheduler Team | BENCH-IMPACT-16-001 | ImpactIndex throughput bench + RAM profile. | +| Sprint 17 | Symbol Intelligence & Forensics | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-17-701 | Record GNU build-id for ELF components and surface it in SBOM/diff outputs. | +| Sprint 17 | Symbol Intelligence & Forensics | src/StellaOps.Zastava.Observer/TASKS.md | TODO | Zastava Observer Guild | ZASTAVA-OBS-17-005 | Collect GNU build-id during runtime observation and attach it to emitted events. | +| Sprint 17 | Symbol Intelligence & Forensics | src/StellaOps.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-RUNTIME-17-401 | Persist runtime build-id observations and expose them for debug-symbol correlation. | +| Sprint 17 | Symbol Intelligence & Forensics | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-REL-17-002 | Ship stripped debug artifacts organised by build-id within release/offline kits. | +| Sprint 17 | Symbol Intelligence & Forensics | docs/TASKS.md | TODO | Docs Guild | DOCS-RUNTIME-17-004 | Document build-id workflows for SBOMs, runtime events, and debug-store usage. | diff --git a/SPRINTS_IMPLEMENTION_PLAN.md b/SPRINTS_IMPLEMENTION_PLAN.md index 92952176..aaeedffa 100644 --- a/SPRINTS_IMPLEMENTION_PLAN.md +++ b/SPRINTS_IMPLEMENTION_PLAN.md @@ -1,295 +1,295 @@ -# StellaOps Multi-Sprint Implementation Plan (Agile Track) - -This plan translates the current `SPRINTS.md` (read the file if you have not) backlog into parallel-friendly execution clusters. Each sprint is decomposed into **groups** that can run concurrently without stepping on the same directories. For every group we capture: - -- **Tasks** (ID · est. effort · path) -- **Acceptance metrics** (quantitative targets to reduce rework) -- **Gate** artifacts required before dependent groups can start - -Durations are estimated work sizes (1 d ≈ one focused engineer day). Milestones are gated by artifacts—not calendar dates—to keep us agile and adaptable to competitor pressure. - ---- - -## Sprint 9 – Scanner Core Foundations (ID: SP9, ~3 w) - -### Group SP9-G1 — Core Contracts & Observability (src/StellaOps.Scanner.Core) ~1 w -- Tasks: - - SCANNER-CORE-09-501 · 3 d · `/src/StellaOps.Scanner.Core/TASKS.md` - - SCANNER-CORE-09-502 · 2 d · same path - - SCANNER-CORE-09-503 · 2 d · same path -- Acceptance metrics: DTO round-trip tests stable; middleware adds ≤5 µs per call. -- Gate SP9-G1 → WebService: `scanner-core-contracts.md` snippet plus `ScannerCoreContractsTests` green. - -### Group SP9-G2 — Queue Backbone (src/StellaOps.Scanner.Queue) ~1 w -- Tasks: SCANNER-QUEUE-09-401 (3 d), -402 (2 d), -403 (2 d) · `/src/StellaOps.Scanner.Queue/TASKS.md` -- Acceptance: dequeue latency p95 ≤20 ms at 40 rps; chaos test retains leases. -- Gate: Redis/NATS adapters docs + `QueueLeaseIntegrationTests` passing. -- Status: **DONE (2025-10-19)** – Gate satisfied via Redis/NATS adapter docs and `QueueLeaseIntegrationTests` run under fake clock. - -### Group SP9-G3 — Storage Backbone (src/StellaOps.Scanner.Storage) ~1 w -- Tasks: SCANNER-STORAGE-09-301 (3 d), -302 (2 d), -303 (2 d) -- Acceptance: majority write/read ≤50 ms; TTL verified. -- Gate: migrations checked in; `StorageDualWriteFixture` passes. -- Status: **DONE (2025-10-19)** – Mongo bootstrapper + migrations committed; MinIO dual-write service wired; `StorageDualWriteFixture` green on Mongo2Go. - -### Group SP9-G4 — WebService Host & Policy Surfacing (src/StellaOps.Scanner.WebService) ~1.2 w -- Tasks: SCANNER-WEB-09-101 (2 d), -102 (3 d), -103 (2 d), -104 (2 d), SCANNER-POLICY-09-105 (3 d), SCANNER-POLICY-09-106 (4 d) -- Acceptance: `/api/v1/scans` enqueue p95 ≤50 ms under synthetic load; policy validation errors actionable; `/reports` response signed. -- Gate SP9-G4 → SP10/SP11: `/reports` OpenAPI frozen; sample signed envelope committed in `samples/api/reports/`. -- Status: **IN PROGRESS (2025-10-19)** – Minimal host and `/api/v1/scans` endpoints delivered (SCANNER-WEB-09-101/102 done); progress streaming and policy/report surfaces remain. - +# StellaOps Multi-Sprint Implementation Plan (Agile Track) + +This plan translates the current `SPRINTS.md` (read the file if you have not) backlog into parallel-friendly execution clusters. Each sprint is decomposed into **groups** that can run concurrently without stepping on the same directories. For every group we capture: + +- **Tasks** (ID · est. effort · path) +- **Acceptance metrics** (quantitative targets to reduce rework) +- **Gate** artifacts required before dependent groups can start + +Durations are estimated work sizes (1 d ≈ one focused engineer day). Milestones are gated by artifacts—not calendar dates—to keep us agile and adaptable to competitor pressure. + +--- + +## Sprint 9 – Scanner Core Foundations (ID: SP9, ~3 w) + +### Group SP9-G1 — Core Contracts & Observability (src/StellaOps.Scanner.Core) ~1 w +- Tasks: + - SCANNER-CORE-09-501 · 3 d · `/src/StellaOps.Scanner.Core/TASKS.md` + - SCANNER-CORE-09-502 · 2 d · same path + - SCANNER-CORE-09-503 · 2 d · same path +- Acceptance metrics: DTO round-trip tests stable; middleware adds ≤5 µs per call. +- Gate SP9-G1 → WebService: `scanner-core-contracts.md` snippet plus `ScannerCoreContractsTests` green. + +### Group SP9-G2 — Queue Backbone (src/StellaOps.Scanner.Queue) ~1 w +- Tasks: SCANNER-QUEUE-09-401 (3 d), -402 (2 d), -403 (2 d) · `/src/StellaOps.Scanner.Queue/TASKS.md` +- Acceptance: dequeue latency p95 ≤20 ms at 40 rps; chaos test retains leases. +- Gate: Redis/NATS adapters docs + `QueueLeaseIntegrationTests` passing. +- Status: **DONE (2025-10-19)** – Gate satisfied via Redis/NATS adapter docs and `QueueLeaseIntegrationTests` run under fake clock. + +### Group SP9-G3 — Storage Backbone (src/StellaOps.Scanner.Storage) ~1 w +- Tasks: SCANNER-STORAGE-09-301 (3 d), -302 (2 d), -303 (2 d) +- Acceptance: majority write/read ≤50 ms; TTL verified. +- Gate: migrations checked in; `StorageDualWriteFixture` passes. +- Status: **DONE (2025-10-19)** – Mongo bootstrapper + migrations committed; MinIO dual-write service wired; `StorageDualWriteFixture` green on Mongo2Go. + +### Group SP9-G4 — WebService Host & Policy Surfacing (src/StellaOps.Scanner.WebService) ~1.2 w +- Tasks: SCANNER-WEB-09-101 (2 d), -102 (3 d), -103 (2 d), -104 (2 d), SCANNER-POLICY-09-105 (3 d), SCANNER-POLICY-09-106 (4 d) +- Acceptance: `/api/v1/scans` enqueue p95 ≤50 ms under synthetic load; policy validation errors actionable; `/reports` response signed. +- Gate SP9-G4 → SP10/SP11: `/reports` OpenAPI frozen; sample signed envelope committed in `samples/api/reports/`. +- Status: **IN PROGRESS (2025-10-19)** – Minimal host and `/api/v1/scans` endpoints delivered (SCANNER-WEB-09-101/102 done); progress streaming and policy/report surfaces remain. + ### Group SP9-G5 — Worker Host (src/StellaOps.Scanner.Worker) ~1 w -- Tasks: SCANNER-WORKER-09-201 (3 d), -202 (3 d), -203 (2 d), -204 (2 d) +- Tasks: SCANNER-WORKER-09-201 (3 d), -202 (3 d), -203 (2 d), -204 (2 d), -205 (1 d) - Acceptance: job lease never drops <3× heartbeat; progress events deterministic. -- Gate: `WorkerBasicScanScenario` integration recorded. -- Status: **DONE (2025-10-19)** – Host bootstrap + authority wiring, heartbeat loop, deterministic stage pipeline, and metrics landed; `WorkerBasicScanScenarioTests` green. - +- Gate: `WorkerBasicScanScenario` integration recorded + optional live queue smoke validation. +- Status: **DONE (2025-10-19)** – Host bootstrap, heartbeat jitter clamp, deterministic stage pipeline, metrics, and Redis-backed smoke harness landed; `WorkerBasicScanScenarioTests` and `RedisWorkerSmokeTests` (flagged) green. + ### Group SP9-G6 — Buildx Plug-in (src/StellaOps.Scanner.Sbomer.BuildXPlugin) ~0.8 w -- Tasks: SP9-BLDX-09-001 (3 d), SP9-BLDX-09-002 (2 d), SP9-BLDX-09-003 (2 d) +- Tasks: SP9-BLDX-09-001 (3 d), SP9-BLDX-09-002 (2 d), SP9-BLDX-09-003 (2 d), SP9-BLDX-09-004 (2 d), SP9-BLDX-09-005 (1 d) - Acceptance: build-time overhead ≤300 ms/layer on 4 vCPU; CAS handshake reliable in CI sample. -- Gate: buildx demo workflow artifact + quickstart doc. -- Status: **DONE** (2025-10-19) — manifest+CAS scaffold, descriptor/Attestor hand-off, GitHub demo workflow, and quickstart committed. - -### Group SP9-G7 — Policy Engine Core (src/StellaOps.Policy) ~1 w -- Tasks: POLICY-CORE-09-001 (2 d) ✅, -002 (3 d) ✅, -003 (3 d) ✅, -004 (3 d), -005 (4 d), -006 (2 d) -- Acceptance: policy parsing ≥200 files/s; preview diff response <200 ms for 500-component SBOM; quieting logic audited. -- Gate: `policy-schema@1` published; revision digests stored; preview API doc updated. - -### Group SP9-G8 — DevOps Early Guardrails (ops/devops) ~0.4 w -- Tasks: DEVOPS-HELM-09-001 (3 d) — **DONE (2025-10-19)** -- Acceptance: helm/compose profiles for dev/stage/airgap lint + dry-run clean; manifests pinned to digest. -- Gate: profiles merged under `deploy/`; install guide cross-link satisfied via `deploy/compose/` bundles and `docs/21_INSTALL_GUIDE.md`. - -### Group SP9-G9 — Documentation & Events (docs/) ~0.4 w -- Tasks: DOCS-ADR-09-001 (2 d), DOCS-EVENTS-09-002 (2 d) -- Acceptance: ADR process broadcast; event schemas validated via CI. -- Gate: `docs/adr/index.md` linking template; `docs/events/README.md` referencing schemas. -- Status: **DONE (2025-10-19)** – ADR contribution guide + template updates merged, Docs CI Ajv validation wired, events catalog documented, guild announcement recorded. - ---- - -## Sprint 10 – Scanner Analyzers & SBOM (ID: SP10, ~4 w) - -### Group SP10-G1 — OS Analyzer Plug-ins (src/StellaOps.Scanner.Analyzers.OS) ~1 w -- Tasks: SCANNER-ANALYZERS-OS-10-201..207 (durations 2–3 d each) -- Acceptance: analyzer runtime <1.5 s/image; memory <250 MB. -- Gate: plug-ins packaged under `plugins/scanner/analyzers/os/`; determinism CI job green. - -### Group SP10-G2 — Language Analyzer Plug-ins (src/StellaOps.Scanner.Analyzers.Lang) ~1.5 w -- Tasks: SCANNER-ANALYZERS-LANG-10-301..309 -- Acceptance: Node analyzer handles 10 k modules <2 s; Python memory <200 MB. -- Gate: golden outputs stored; plugin manifests present. - -### Group SP10-G3 — EntryTrace Plug-ins (src/StellaOps.Scanner.EntryTrace) ~0.8 w -- Tasks: SCANNER-ENTRYTRACE-10-401..407 -- Acceptance: ≥95 % launcher resolution success on samples; unknown reasons enumerated. -- Gate: entrytrace plug-ins packaged; explainability doc updated. - -### Group SP10-G4 — SBOM Composition & BOM Index (src/StellaOps.Scanner.Diff + Emit) ~1 w -- Tasks: SCANNER-DIFF-10-501..503, SCANNER-EMIT-10-601..606 -- Acceptance: BOM-Index emission <500 ms/image; diff output deterministic across runs. -- Gate SP10-G4 → SP16: `docs/artifacts/bom-index/` schema + fixtures; tests `BOMIndexGoldenIsStable` & `UsageFlagsAreAccurate` green. - -### Group SP10-G5 — Cache Subsystem (src/StellaOps.Scanner.Cache) ~0.6 w -- Tasks: SCANNER-CACHE-10-101..104 -- Acceptance: cache hit instrumentation validated; eviction keeps footprint <5 GB. -- Gate: cache configuration doc; integration test `LayerCacheRoundTrip` green. - -### Group SP10-G6 — Benchmarks & Samples (bench/, samples/, ops/devops) ~0.6 w -- Tasks: BENCH-SCANNER-10-001 (2 d), SAMPLES-10-001 (finish – 3 d), DEVOPS-PERF-10-001 (2 d) -- Acceptance: analyzer benchmark CSV published; perf CI guard ensures SBOM compose <5 s; sample SBOM/BOM-Index committed. -- Gate: bench results stored under `bench/`; `samples/` populated; CI job added. - ---- - -## Sprint 11 – Signing Chain Bring-up (ID: SP11, ~3 w) - -### Group SP11-G1 — Authority Sender Constraints (src/StellaOps.Authority) ~0.8 w -- Tasks: AUTH-DPOP-11-001 (3 d), AUTH-MTLS-11-002 (2 d) -- Acceptance: DPoP nonce dance validated; mTLS tokens issued in ≤40 ms. -- Gate: updated Authority OpenAPI; QA scripts verifying DPoP/mTLS. - -### Group SP11-G2 — Signer Service (src/StellaOps.Signer) ~1.2 w -- Tasks: SIGNER-API-11-101 (4 d), SIGNER-REF-11-102 (2 d), SIGNER-QUOTA-11-103 (2 d) -- Acceptance: signing throughput ≥30 req/min; p95 latency ≤200 ms. -- Gate SP11-G2 → Attestor/UI: `/sign/dsse` OpenAPI frozen; signed DSSE bundle in repo; Rekor interop test passing. - -### Group SP11-G3 — Attestor Service (src/StellaOps.Attestor) ~1 w -- Tasks: ATTESTOR-API-11-201 (3 d), ATTESTOR-VERIFY-11-202 (2 d), ATTESTOR-OBS-11-203 (2 d) -- Acceptance: inclusion proof retrieval <500 ms; audit log coverage 100 %. -- Gate: Attestor API doc + verification script. - -### Group SP11-G4 — UI Attestation Hooks (src/StellaOps.UI) ~0.4 w -- Tasks: UI-ATTEST-11-005 (3 d) -- Acceptance: attestation panel renders within 200 ms; Rekor link verified. -- Gate SP11-G4 → SP13-G1: recorded UX walkthrough. - ---- - -## Sprint 12 – Runtime Guardrails (ID: SP12, ~3 w) - -### Group SP12-G1 — Zastava Core (src/StellaOps.Zastava.Core) ~0.8 w -- Tasks: ZASTAVA-CORE-12-201..204 -- Acceptance: DTO tests stable; configuration docs produced. -- Gate: schema doc + logging helpers integrated. - -### Group SP12-G2 — Zastava Observer (src/StellaOps.Zastava.Observer) ~0.8 w -- Tasks: ZASTAVA-OBS-12-001..004 -- Acceptance: observer memory <200 MB; event flush ≤2 s. -- Gate: sample runtime events stored; offline buffer test passes. - -### Group SP12-G3 — Zastava Webhook (src/StellaOps.Zastava.Webhook) ~0.6 w -- Tasks: ZASTAVA-WEBHOOK-12-101..103 -- Acceptance: admission latency p95 ≤45 ms; cache TTL adhered to. -- Gate: TLS rotation procedure documented; readiness probe script. - -### Group SP12-G4 — Scanner Runtime APIs (src/StellaOps.Scanner.WebService) ~0.8 w -- Tasks: SCANNER-RUNTIME-12-301 (2 d), SCANNER-RUNTIME-12-302 (3 d) -- Acceptance: `/runtime/events` handles 500 events/sec; `/policy/runtime` output matches webhook decisions. -- Gate SP12-G4 → SP13/SP15: API documented, fixtures updated. - ---- - -## Sprint 13 – UX & CLI Experience (ID: SP13, ~2 w) - -### Group SP13-G1 — UI Shell & Panels (src/StellaOps.UI) ~1.6 w -- Tasks: UI-AUTH-13-001 (3 d), UI-SCANS-13-002 (4 d), UI-VEX-13-003 (3 d), UI-ADMIN-13-004 (2 d), UI-SCHED-13-005 (3 d), UI-NOTIFY-13-006 (3 d) -- Acceptance: Lighthouse ≥85; Scheduler/Notify panels function against mocked APIs. -- Gate: UI dev server fixtures committed; QA sign-off captured. - -### Group SP13-G2 — CLI Enhancements (src/StellaOps.Cli) ~0.8 w -- Tasks: CLI-RUNTIME-13-005 (3 d), CLI-OFFLINE-13-006 (3 d), CLI-PLUGIN-13-007 (2 d) -- Acceptance: runtime policy CLI completes <1 s for 10 images; offline kit commands resume downloads. -- Gate: CLI plugin manifest doc; smoke tests covering new verbs. - ---- - -## Sprint 14 – Release & Offline Ops (ID: SP14, ~2 w) - -### Group SP14-G1 — Release Automation (ops/devops) ~0.8 w -- Tasks: DEVOPS-REL-14-001 (4 d) -- Acceptance: reproducible build diff tool shows zero drift across two runs; signing pipeline green. -- Gate: signed manifest + provenance published. - -### Group SP14-G2 — Offline Kit Packaging (ops/offline-kit) ~0.6 w -- Tasks: DEVOPS-OFFLINE-14-002 (3 d) -- Acceptance: kit import <5 min with integrity verification CLI. -- Gate: kit doc updated; import script included. - -### Group SP14-G3 — Deployment Playbooks (ops/deployment) ~0.4 w -- Tasks: DEVOPS-OPS-14-003 (2 d) -- Acceptance: rollback drill recorded; compatibility matrix produced. -- Gate: playbook PR merged with Ops sign-off. - -### Group SP14-G4 — Licensing Token Service (ops/licensing) ~0.4 w -- Tasks: DEVOPS-LIC-14-004 (2 d) -- Acceptance: token service handles 100 req/min; revocation latency <60 s. -- Gate: monitoring dashboard links; failover doc. - ---- - -## Sprint 15 – Notify Foundations (ID: SP15, ~3 w) - -### Group SP15-G1 — Models & Storage (src/StellaOps.Notify.Models + Storage.Mongo) ~0.8 w -- Tasks: NOTIFY-MODELS-15-101 (2 d), -102 (2 d), -103 (1 d); NOTIFY-STORAGE-15-201 (3 d), -202 (2 d), -203 (1 d) -- Acceptance: rule CRUD latency <120 ms; delivery retention job verified. -- Gate: schema docs + fixtures published. - -### Group SP15-G2 — Engine & Queue (src/StellaOps.Notify.Engine + Queue) ~0.8 w -- Tasks: NOTIFY-ENGINE-15-301..304, NOTIFY-QUEUE-15-401..403 -- Acceptance: rules evaluation ≥5k events/min; queue dead-letter <0.5 %. -- Gate: digest outputs committed; queue config doc updated. - -### Group SP15-G3 — WebService & Worker (src/StellaOps.Notify.WebService + Worker) ~0.8 w -- Tasks: NOTIFY-WEB-15-101..104, NOTIFY-WORKER-15-201..204 -- Acceptance: API p95 <120 ms; worker delivery success ≥99 %. -- Gate: end-to-end fixture run producing delivery record. - -### Group SP15-G4 — Channel Plug-ins (src/StellaOps.Notify.Connectors.*) ~0.6 w -- Tasks: NOTIFY-CONN-SLACK-15-501..503, NOTIFY-CONN-TEAMS-15-601..603, NOTIFY-CONN-EMAIL-15-701..703, NOTIFY-CONN-WEBHOOK-15-801..803 -- Acceptance: channel-specific retry policies verified; rate limits respected. -- Gate: plug-in manifests inside `plugins/notify/**`; test-send docs. - -### Group SP15-G5 — Events & Benchmarks (src/StellaOps.Scanner.WebService + bench) ~0.5 w -- Tasks: SCANNER-EVENTS-15-201 (2 d), BENCH-NOTIFY-15-001 (2 d) -- Acceptance: event emission latency <100 ms; throughput bench results stored. -- Gate: `docs/events/samples/` contains sample payloads; bench CSV in repo. - ---- - -## Sprint 16 – Scheduler Intelligence (ID: SP16, ~4 w) - -### Group SP16-G1 — Models & Storage (src/StellaOps.Scheduler.Models + Storage.Mongo) ~1 w -- Tasks: SCHED-MODELS-16-101 (3 d), -102 (2 d), -103 (2 d); SCHED-STORAGE-16-201 (3 d), -202 (2 d), -203 (2 d) -- Acceptance: schedule CRUD latency <120 ms; run retention TTL enforced. -- Gate: schema doc + integration tests passing. - -### Group SP16-G2 — ImpactIndex & Queue (src/StellaOps.Scheduler.ImpactIndex + Queue + Bench) ~1.2 w -- Tasks: SCHED-IMPACT-16-300 (2 d, DOING), SCHED-IMPACT-16-301 (3 d), -302 (3 d), -303 (2 d); SCHED-QUEUE-16-401..403 (each 2 d); BENCH-IMPACT-16-001 (2 d) -- Acceptance: impact resolve 10k productKeys <300 ms hot; stub removed by sprint end. -- Gate: roaring snapshot stored; bench CSV published; removal plan for stub recorded. - -### Group SP16-G3 — Scheduler WebService (src/StellaOps.Scheduler.WebService) ~0.8 w -- Tasks: SCHED-WEB-16-101..104 (each 2 d) -- Acceptance: preview endpoint <250 ms; webhook security enforced. -- Gate: OpenAPI published; dry-run JSON fixtures stored. - -### Group SP16-G4 — Scheduler Worker (src/StellaOps.Scheduler.Worker) ~1 w -- Tasks: SCHED-WORKER-16-201 (3 d), -202 (2 d), -203 (3 d), -204 (2 d), -205 (2 d) -- Acceptance: planner fairness metrics captured; runner success ≥98 % across 1k sims. -- Gate: event emission to Notify verified; metrics dashboards live. - ---- - -## Sprint 17 – Symbol Intelligence & Forensics (ID: SP17, ~2.5 w) - -### Group SP17-G1 — Scanner Forensics (src/StellaOps.Scanner.Emit + WebService) ~1.2 w -- Tasks: SCANNER-EMIT-17-701 (4 d), SCANNER-RUNTIME-17-401 (3 d) -- Acceptance: forensic overlays add ≤150 ms per image; runtime API exposes symbol hints with feature flag. -- Gate: forensic SBOM samples committed; API doc updated. - -### Group SP17-G2 — Zastava Observability (src/StellaOps.Zastava.Observer) ~0.6 w -- Tasks: ZASTAVA-OBS-17-005 (3 d) -- Acceptance: new telemetry surfaces symbol diffs; observer CPU <10 % under load. -- Gate: Grafana dashboard export, alert thresholds defined. - -### Group SP17-G3 — Release Hardening (ops/devops) ~0.4 w -- Tasks: DEVOPS-REL-17-002 (2 d) -- Acceptance: deterministic build verifier job updated to include forensics artifacts. -- Gate: CI pipeline stage `forensics-verify` green. - -### Group SP17-G4 — Documentation (docs/) ~0.3 w -- Tasks: DOCS-RUNTIME-17-004 (2 d) -- Acceptance: runtime forensic guide published with troubleshooting. -- Gate: docs review sign-off; links added to UI help. - ---- - -## Integration Buffers -- **INT-A (0.3 w, after SP10):** Image → SBOM → BOM-Index → Scheduler preview → UI dry-run using fixtures. -- **INT-B (0.3 w, after SP11 & SP15):** SBOM → policy verdict → signed DSSE → Rekor entry → Notify delivery end-to-end. - -## Parallelisation Strategy -- SP9 core modules and SP11 authority upgrades can progress in parallel; scanner clients rely on feature flags while DPoP/mTLS hardening lands. -- SP10 SBOM emission may start alongside Scheduler ImpactIndex using `samples/` fixtures; stub SCHED-IMPACT-16-300 keeps velocity while awaiting roaring index. -- Notify foundations (SP15) can begin once event schemas freeze (delivered in SP9-G9/SP12-G4), consuming canned events until Scanner emits live ones. -- UI (SP13) uses mocked endpoints early, decoupling front-end delivery from backend readiness. - -## Risk Registry - -| Risk ID | Description | Owner | Mitigation | Trigger | -|---------|-------------|-------|-----------|---------| -| R1 | BOM-Index memory blow-up on large fleets | Scheduler ImpactIndex Guild | Shard + mmap plan; monitor BENCH-IMPACT-16-001 | RAM > 8 GB in bench | -| R2 | Buildx plugin latency regression | BuildX Guild | DEVOPS-PERF-10-001 guard; fallback to post-build scan | Buildx job >300 ms/layer | -| R3 | Notify digests flooding Slack | Notify Engine Guild | throttle defaults, BENCH-NOTIFY-15-001 coverage | Dropped messages >1 % | -| R4 | Policy precedence confusion | Policy Guild | ADR, preview API, unit tests | Operator escalation about precedence | -| R5 | ImpactIndex stub lingers | Scheduler ImpactIndex Guild | Track SCHED-IMPACT-16-300 removal in sprint review | Stub present past SP16 | -| R6 | Symbol forensics slows runtime | Scanner Emit Guild | Feature flag; perf tests in SP17-G1 | Forensics adds >150 ms/image | - -## Envelope & ADR Governance -- Event schemas (`docs/events/*.json`) versioned; producers must bump suffix on breaking changes. -- ADR template (`docs/adr/0000-template.md`) mandatory for BOM-Index format, event envelopes, DPoP nonce policy, Rekor migration. - ---- - -**Summary:** The plan keeps high-impact artifacts (policy engine, BOM-Index, signing chain) on the critical path while unlocking parallel tracks (Notify, Scheduler, UI) through early schema freezes and fixtures. Integration buffers ensure cross-team touchpoints are validated continuously, supporting rapid iteration against competitive pressure. +- Gate: buildx demo workflow artifact + quickstart doc + determinism regression guard in CI. +- Status: **DONE (2025-10-19)** — manifest+CAS scaffold, descriptor/Attestor hand-off, GitHub/Gitea determinism workflows, quickstart update, and golden tests committed. + +### Group SP9-G7 — Policy Engine Core (src/StellaOps.Policy) ~1 w +- Tasks: POLICY-CORE-09-001 (2 d) ✅, -002 (3 d) ✅, -003 (3 d) ✅, -004 (3 d), -005 (4 d), -006 (2 d) +- Acceptance: policy parsing ≥200 files/s; preview diff response <200 ms for 500-component SBOM; quieting logic audited. +- Gate: `policy-schema@1` published; revision digests stored; preview API doc updated. + +### Group SP9-G8 — DevOps Early Guardrails (ops/devops) ~0.4 w +- Tasks: DEVOPS-HELM-09-001 (3 d) — **DONE (2025-10-19)** +- Acceptance: helm/compose profiles for dev/stage/airgap lint + dry-run clean; manifests pinned to digest. +- Gate: profiles merged under `deploy/`; install guide cross-link satisfied via `deploy/compose/` bundles and `docs/21_INSTALL_GUIDE.md`. + +### Group SP9-G9 — Documentation & Events (docs/) ~0.4 w +- Tasks: DOCS-ADR-09-001 (2 d), DOCS-EVENTS-09-002 (2 d) +- Acceptance: ADR process broadcast; event schemas validated via CI. +- Gate: `docs/adr/index.md` linking template; `docs/events/README.md` referencing schemas. +- Status: **DONE (2025-10-19)** – ADR contribution guide + template updates merged, Docs CI Ajv validation wired, events catalog documented, guild announcement recorded. + +--- + +## Sprint 10 – Scanner Analyzers & SBOM (ID: SP10, ~4 w) + +### Group SP10-G1 — OS Analyzer Plug-ins (src/StellaOps.Scanner.Analyzers.OS) ~1 w +- Tasks: SCANNER-ANALYZERS-OS-10-201..207 (durations 2–3 d each) +- Acceptance: analyzer runtime <1.5 s/image; memory <250 MB. +- Gate: plug-ins packaged under `plugins/scanner/analyzers/os/`; determinism CI job green. + +### Group SP10-G2 — Language Analyzer Plug-ins (src/StellaOps.Scanner.Analyzers.Lang) ~1.5 w +- Tasks: SCANNER-ANALYZERS-LANG-10-301..309 +- Acceptance: Node analyzer handles 10 k modules <2 s; Python memory <200 MB. +- Gate: golden outputs stored; plugin manifests present. + +### Group SP10-G3 — EntryTrace Plug-ins (src/StellaOps.Scanner.EntryTrace) ~0.8 w +- Tasks: SCANNER-ENTRYTRACE-10-401..407 +- Acceptance: ≥95 % launcher resolution success on samples; unknown reasons enumerated. +- Gate: entrytrace plug-ins packaged; explainability doc updated. + +### Group SP10-G4 — SBOM Composition & BOM Index (src/StellaOps.Scanner.Diff + Emit) ~1 w +- Tasks: SCANNER-DIFF-10-501..503, SCANNER-EMIT-10-601..606 +- Acceptance: BOM-Index emission <500 ms/image; diff output deterministic across runs. +- Gate SP10-G4 → SP16: `docs/artifacts/bom-index/` schema + fixtures; tests `BOMIndexGoldenIsStable` & `UsageFlagsAreAccurate` green. + +### Group SP10-G5 — Cache Subsystem (src/StellaOps.Scanner.Cache) ~0.6 w +- Tasks: SCANNER-CACHE-10-101..104 +- Acceptance: cache hit instrumentation validated; eviction keeps footprint <5 GB. +- Gate: cache configuration doc; integration test `LayerCacheRoundTrip` green. + +### Group SP10-G6 — Benchmarks & Samples (bench/, samples/, ops/devops) ~0.6 w +- Tasks: BENCH-SCANNER-10-001 (2 d), SAMPLES-10-001 (finish – 3 d), DEVOPS-PERF-10-001 (2 d) +- Acceptance: analyzer benchmark CSV published; perf CI guard ensures SBOM compose <5 s; sample SBOM/BOM-Index committed. +- Gate: bench results stored under `bench/`; `samples/` populated; CI job added. + +--- + +## Sprint 11 – Signing Chain Bring-up (ID: SP11, ~3 w) + +### Group SP11-G1 — Authority Sender Constraints (src/StellaOps.Authority) ~0.8 w +- Tasks: AUTH-DPOP-11-001 (3 d), AUTH-MTLS-11-002 (2 d) +- Acceptance: DPoP nonce dance validated; mTLS tokens issued in ≤40 ms. +- Gate: updated Authority OpenAPI; QA scripts verifying DPoP/mTLS. + +### Group SP11-G2 — Signer Service (src/StellaOps.Signer) ~1.2 w +- Tasks: SIGNER-API-11-101 (4 d), SIGNER-REF-11-102 (2 d), SIGNER-QUOTA-11-103 (2 d) +- Acceptance: signing throughput ≥30 req/min; p95 latency ≤200 ms. +- Gate SP11-G2 → Attestor/UI: `/sign/dsse` OpenAPI frozen; signed DSSE bundle in repo; Rekor interop test passing. + +### Group SP11-G3 — Attestor Service (src/StellaOps.Attestor) ~1 w +- Tasks: ATTESTOR-API-11-201 (3 d), ATTESTOR-VERIFY-11-202 (2 d), ATTESTOR-OBS-11-203 (2 d) +- Acceptance: inclusion proof retrieval <500 ms; audit log coverage 100 %. +- Gate: Attestor API doc + verification script. + +### Group SP11-G4 — UI Attestation Hooks (src/StellaOps.UI) ~0.4 w +- Tasks: UI-ATTEST-11-005 (3 d) +- Acceptance: attestation panel renders within 200 ms; Rekor link verified. +- Gate SP11-G4 → SP13-G1: recorded UX walkthrough. + +--- + +## Sprint 12 – Runtime Guardrails (ID: SP12, ~3 w) + +### Group SP12-G1 — Zastava Core (src/StellaOps.Zastava.Core) ~0.8 w +- Tasks: ZASTAVA-CORE-12-201..204 +- Acceptance: DTO tests stable; configuration docs produced. +- Gate: schema doc + logging helpers integrated. + +### Group SP12-G2 — Zastava Observer (src/StellaOps.Zastava.Observer) ~0.8 w +- Tasks: ZASTAVA-OBS-12-001..004 +- Acceptance: observer memory <200 MB; event flush ≤2 s. +- Gate: sample runtime events stored; offline buffer test passes. + +### Group SP12-G3 — Zastava Webhook (src/StellaOps.Zastava.Webhook) ~0.6 w +- Tasks: ZASTAVA-WEBHOOK-12-101..103 +- Acceptance: admission latency p95 ≤45 ms; cache TTL adhered to. +- Gate: TLS rotation procedure documented; readiness probe script. + +### Group SP12-G4 — Scanner Runtime APIs (src/StellaOps.Scanner.WebService) ~0.8 w +- Tasks: SCANNER-RUNTIME-12-301 (2 d), SCANNER-RUNTIME-12-302 (3 d) +- Acceptance: `/runtime/events` handles 500 events/sec; `/policy/runtime` output matches webhook decisions. +- Gate SP12-G4 → SP13/SP15: API documented, fixtures updated. + +--- + +## Sprint 13 – UX & CLI Experience (ID: SP13, ~2 w) + +### Group SP13-G1 — UI Shell & Panels (src/StellaOps.UI) ~1.6 w +- Tasks: UI-AUTH-13-001 (3 d), UI-SCANS-13-002 (4 d), UI-VEX-13-003 (3 d), UI-ADMIN-13-004 (2 d), UI-SCHED-13-005 (3 d), UI-NOTIFY-13-006 (3 d) +- Acceptance: Lighthouse ≥85; Scheduler/Notify panels function against mocked APIs. +- Gate: UI dev server fixtures committed; QA sign-off captured. + +### Group SP13-G2 — CLI Enhancements (src/StellaOps.Cli) ~0.8 w +- Tasks: CLI-RUNTIME-13-005 (3 d), CLI-OFFLINE-13-006 (3 d), CLI-PLUGIN-13-007 (2 d) +- Acceptance: runtime policy CLI completes <1 s for 10 images; offline kit commands resume downloads. +- Gate: CLI plugin manifest doc; smoke tests covering new verbs. + +--- + +## Sprint 14 – Release & Offline Ops (ID: SP14, ~2 w) + +### Group SP14-G1 — Release Automation (ops/devops) ~0.8 w +- Tasks: DEVOPS-REL-14-001 (4 d) +- Acceptance: reproducible build diff tool shows zero drift across two runs; signing pipeline green. +- Gate: signed manifest + provenance published. + +### Group SP14-G2 — Offline Kit Packaging (ops/offline-kit) ~0.6 w +- Tasks: DEVOPS-OFFLINE-14-002 (3 d) +- Acceptance: kit import <5 min with integrity verification CLI. +- Gate: kit doc updated; import script included. + +### Group SP14-G3 — Deployment Playbooks (ops/deployment) ~0.4 w +- Tasks: DEVOPS-OPS-14-003 (2 d) +- Acceptance: rollback drill recorded; compatibility matrix produced. +- Gate: playbook PR merged with Ops sign-off. + +### Group SP14-G4 — Licensing Token Service (ops/licensing) ~0.4 w +- Tasks: DEVOPS-LIC-14-004 (2 d) +- Acceptance: token service handles 100 req/min; revocation latency <60 s. +- Gate: monitoring dashboard links; failover doc. + +--- + +## Sprint 15 – Notify Foundations (ID: SP15, ~3 w) + +### Group SP15-G1 — Models & Storage (src/StellaOps.Notify.Models + Storage.Mongo) ~0.8 w +- Tasks: NOTIFY-MODELS-15-101 (2 d), -102 (2 d), -103 (1 d); NOTIFY-STORAGE-15-201 (3 d), -202 (2 d), -203 (1 d) +- Acceptance: rule CRUD latency <120 ms; delivery retention job verified. +- Gate: schema docs + fixtures published. + +### Group SP15-G2 — Engine & Queue (src/StellaOps.Notify.Engine + Queue) ~0.8 w +- Tasks: NOTIFY-ENGINE-15-301..304, NOTIFY-QUEUE-15-401..403 +- Acceptance: rules evaluation ≥5k events/min; queue dead-letter <0.5 %. +- Gate: digest outputs committed; queue config doc updated. + +### Group SP15-G3 — WebService & Worker (src/StellaOps.Notify.WebService + Worker) ~0.8 w +- Tasks: NOTIFY-WEB-15-101..104, NOTIFY-WORKER-15-201..204 +- Acceptance: API p95 <120 ms; worker delivery success ≥99 %. +- Gate: end-to-end fixture run producing delivery record. + +### Group SP15-G4 — Channel Plug-ins (src/StellaOps.Notify.Connectors.*) ~0.6 w +- Tasks: NOTIFY-CONN-SLACK-15-501..503, NOTIFY-CONN-TEAMS-15-601..603, NOTIFY-CONN-EMAIL-15-701..703, NOTIFY-CONN-WEBHOOK-15-801..803 +- Acceptance: channel-specific retry policies verified; rate limits respected. +- Gate: plug-in manifests inside `plugins/notify/**`; test-send docs. + +### Group SP15-G5 — Events & Benchmarks (src/StellaOps.Scanner.WebService + bench) ~0.5 w +- Tasks: SCANNER-EVENTS-15-201 (2 d), BENCH-NOTIFY-15-001 (2 d) +- Acceptance: event emission latency <100 ms; throughput bench results stored. +- Gate: `docs/events/samples/` contains sample payloads; bench CSV in repo. + +--- + +## Sprint 16 – Scheduler Intelligence (ID: SP16, ~4 w) + +### Group SP16-G1 — Models & Storage (src/StellaOps.Scheduler.Models + Storage.Mongo) ~1 w +- Tasks: SCHED-MODELS-16-101 (3 d), -102 (2 d), -103 (2 d); SCHED-STORAGE-16-201 (3 d), -202 (2 d), -203 (2 d) +- Acceptance: schedule CRUD latency <120 ms; run retention TTL enforced. +- Gate: schema doc + integration tests passing. + +### Group SP16-G2 — ImpactIndex & Queue (src/StellaOps.Scheduler.ImpactIndex + Queue + Bench) ~1.2 w +- Tasks: SCHED-IMPACT-16-300 (2 d, DOING), SCHED-IMPACT-16-301 (3 d), -302 (3 d), -303 (2 d); SCHED-QUEUE-16-401..403 (each 2 d); BENCH-IMPACT-16-001 (2 d) +- Acceptance: impact resolve 10k productKeys <300 ms hot; stub removed by sprint end. +- Gate: roaring snapshot stored; bench CSV published; removal plan for stub recorded. + +### Group SP16-G3 — Scheduler WebService (src/StellaOps.Scheduler.WebService) ~0.8 w +- Tasks: SCHED-WEB-16-101..104 (each 2 d) +- Acceptance: preview endpoint <250 ms; webhook security enforced. +- Gate: OpenAPI published; dry-run JSON fixtures stored. + +### Group SP16-G4 — Scheduler Worker (src/StellaOps.Scheduler.Worker) ~1 w +- Tasks: SCHED-WORKER-16-201 (3 d), -202 (2 d), -203 (3 d), -204 (2 d), -205 (2 d) +- Acceptance: planner fairness metrics captured; runner success ≥98 % across 1k sims. +- Gate: event emission to Notify verified; metrics dashboards live. + +--- + +## Sprint 17 – Symbol Intelligence & Forensics (ID: SP17, ~2.5 w) + +### Group SP17-G1 — Scanner Forensics (src/StellaOps.Scanner.Emit + WebService) ~1.2 w +- Tasks: SCANNER-EMIT-17-701 (4 d), SCANNER-RUNTIME-17-401 (3 d) +- Acceptance: forensic overlays add ≤150 ms per image; runtime API exposes symbol hints with feature flag. +- Gate: forensic SBOM samples committed; API doc updated. + +### Group SP17-G2 — Zastava Observability (src/StellaOps.Zastava.Observer) ~0.6 w +- Tasks: ZASTAVA-OBS-17-005 (3 d) +- Acceptance: new telemetry surfaces symbol diffs; observer CPU <10 % under load. +- Gate: Grafana dashboard export, alert thresholds defined. + +### Group SP17-G3 — Release Hardening (ops/devops) ~0.4 w +- Tasks: DEVOPS-REL-17-002 (2 d) +- Acceptance: deterministic build verifier job updated to include forensics artifacts. +- Gate: CI pipeline stage `forensics-verify` green. + +### Group SP17-G4 — Documentation (docs/) ~0.3 w +- Tasks: DOCS-RUNTIME-17-004 (2 d) +- Acceptance: runtime forensic guide published with troubleshooting. +- Gate: docs review sign-off; links added to UI help. + +--- + +## Integration Buffers +- **INT-A (0.3 w, after SP10):** Image → SBOM → BOM-Index → Scheduler preview → UI dry-run using fixtures. +- **INT-B (0.3 w, after SP11 & SP15):** SBOM → policy verdict → signed DSSE → Rekor entry → Notify delivery end-to-end. + +## Parallelisation Strategy +- SP9 core modules and SP11 authority upgrades can progress in parallel; scanner clients rely on feature flags while DPoP/mTLS hardening lands. +- SP10 SBOM emission may start alongside Scheduler ImpactIndex using `samples/` fixtures; stub SCHED-IMPACT-16-300 keeps velocity while awaiting roaring index. +- Notify foundations (SP15) can begin once event schemas freeze (delivered in SP9-G9/SP12-G4), consuming canned events until Scanner emits live ones. +- UI (SP13) uses mocked endpoints early, decoupling front-end delivery from backend readiness. + +## Risk Registry + +| Risk ID | Description | Owner | Mitigation | Trigger | +|---------|-------------|-------|-----------|---------| +| R1 | BOM-Index memory blow-up on large fleets | Scheduler ImpactIndex Guild | Shard + mmap plan; monitor BENCH-IMPACT-16-001 | RAM > 8 GB in bench | +| R2 | Buildx plugin latency regression | BuildX Guild | DEVOPS-PERF-10-001 guard; fallback to post-build scan | Buildx job >300 ms/layer | +| R3 | Notify digests flooding Slack | Notify Engine Guild | throttle defaults, BENCH-NOTIFY-15-001 coverage | Dropped messages >1 % | +| R4 | Policy precedence confusion | Policy Guild | ADR, preview API, unit tests | Operator escalation about precedence | +| R5 | ImpactIndex stub lingers | Scheduler ImpactIndex Guild | Track SCHED-IMPACT-16-300 removal in sprint review | Stub present past SP16 | +| R6 | Symbol forensics slows runtime | Scanner Emit Guild | Feature flag; perf tests in SP17-G1 | Forensics adds >150 ms/image | + +## Envelope & ADR Governance +- Event schemas (`docs/events/*.json`) versioned; producers must bump suffix on breaking changes. +- ADR template (`docs/adr/0000-template.md`) mandatory for BOM-Index format, event envelopes, DPoP nonce policy, Rekor migration. + +--- + +**Summary:** The plan keeps high-impact artifacts (policy engine, BOM-Index, signing chain) on the critical path while unlocking parallel tracks (Notify, Scheduler, UI) through early schema freezes and fixtures. Integration buffers ensure cross-team touchpoints are validated continuously, supporting rapid iteration against competitive pressure. diff --git a/docs/ARCHITECTURE_SCANNER.md b/docs/ARCHITECTURE_SCANNER.md index 68641aa6..a296db3e 100644 --- a/docs/ARCHITECTURE_SCANNER.md +++ b/docs/ARCHITECTURE_SCANNER.md @@ -162,6 +162,10 @@ GET /catalog/artifacts/{id} → { meta } GET /healthz | /readyz | /metrics ``` +### Report events + +When `scanner.events.enabled = true`, the WebService serialises the signed report (canonical JSON + DSSE envelope) with `NotifyCanonicalJsonSerializer` and publishes two Redis Stream entries (`scanner.report.ready`, `scanner.scan.completed`) to the configured stream (default `stella.events`). The stream fields carry the whole envelope plus lightweight headers (`kind`, `tenant`, `ts`) so Notify and UI timelines can consume the event bus without recomputing signatures. Publish timeouts and bounded stream length are controlled via `scanner:events:publishTimeoutSeconds` and `scanner:events:maxStreamLength`. If the queue driver is already Redis and no explicit events DSN is provided, the host reuses the queue connection and auto-enables event emission so deployments get live envelopes without extra wiring. + --- ## 5) Execution flow (Worker) @@ -433,6 +437,26 @@ ResolveEntrypoint(ImageConfig cfg, RootFs fs): return Unknown(reason) ``` +### Appendix A.1 — EntryTrace Explainability + +EntryTrace emits structured diagnostics and metrics so operators can quickly understand why resolution succeeded or degraded: + +| Reason | Description | Typical Mitigation | +|--------|-------------|--------------------| +| `CommandNotFound` | A command referenced in the script cannot be located in the layered root filesystem or `PATH`. | Ensure binaries exist in the image or extend `PATH` hints. | +| `MissingFile` | `source`/`.`/`run-parts` targets are missing. | Bundle the script or guard the include. | +| `DynamicEnvironmentReference` | Path depends on `$VARS` that are unknown at scan time. | Provide defaults via scan metadata or accept partial usage. | +| `RecursionLimitReached` | Nested includes exceeded the analyzer depth limit (default 64). | Flatten indirection or increase the limit in options. | +| `RunPartsEmpty` | `run-parts` directory contained no executable entries. | Remove empty directories or ignore if intentional. | +| `JarNotFound` / `ModuleNotFound` | Java/Python targets missing, preventing interpreter tracing. | Ship the jar/module with the image or adjust the launcher. | + +Diagnostics drive two metrics published by `EntryTraceMetrics`: + +- `entrytrace_resolutions_total{outcome}` — resolution attempts segmented by outcome (`resolved`, `partiallyresolved`, `unresolved`). +- `entrytrace_unresolved_total{reason}` — diagnostic counts keyed by reason. + +Structured logs include `entrytrace.path`, `entrytrace.command`, `entrytrace.reason`, and `entrytrace.depth`, all correlated with scan/job IDs. Timestamps are normalized to UTC (microsecond precision) to keep DSSE attestations and UI traces explainable. + ### Appendix B — BOM‑Index sidecar ``` diff --git a/docs/dev/BUILDX_PLUGIN_QUICKSTART.md b/docs/dev/BUILDX_PLUGIN_QUICKSTART.md index f4bdea5e..91c5c104 100644 --- a/docs/dev/BUILDX_PLUGIN_QUICKSTART.md +++ b/docs/dev/BUILDX_PLUGIN_QUICKSTART.md @@ -1,116 +1,116 @@ -# BuildX Generator Quickstart - -This quickstart explains how to run the StellaOps **BuildX SBOM generator** offline, verify the CAS handshake, and emit OCI descriptors that downstream services can attest. - -## 1. Prerequisites - -- Docker 25+ with BuildKit enabled (`docker buildx` available). -- .NET 10 (preview) SDK matching the repository `global.json`. -- Optional: network access to a StellaOps Attestor endpoint (the quickstart uses a mock service). - -## 2. Publish the plug-in binaries - -The BuildX generator publishes as a .NET self-contained executable with its manifest under `plugins/scanner/buildx/`. - -```bash -# From the repository root -DOTNET_CLI_HOME="${PWD}/.dotnet" \ -dotnet publish src/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj \ - -c Release \ - -o out/buildx -``` - -- `out/buildx/` now contains `StellaOps.Scanner.Sbomer.BuildXPlugin.dll` and the manifest `stellaops.sbom-indexer.manifest.json`. -- `plugins/scanner/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin/` receives the same artefacts for release packaging. - -## 3. Verify the CAS handshake - -```bash -dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll handshake \ - --manifest out/buildx \ - --cas out/cas -``` - -The command performs a deterministic probe write (`sha256`) into the provided CAS directory and prints the resolved path. - -## 4. Emit a descriptor + provenance placeholder - -1. Build or identify the image you want to describe and capture its digest: - - ```bash - docker buildx build --load -t stellaops/buildx-demo:ci samples/ci/buildx-demo - DIGEST=$(docker image inspect stellaops/buildx-demo:ci --format '{{index .RepoDigests 0}}') - ``` - -2. Generate a CycloneDX SBOM for the built image (any tool works; here we use `docker sbom`): - - ```bash - docker sbom stellaops/buildx-demo:ci --format cyclonedx-json > out/buildx-sbom.cdx.json - ``` - -3. Invoke the `descriptor` command, pointing at the SBOM file and optional metadata: - - ```bash - dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll descriptor \ - --manifest out/buildx \ - --image "$DIGEST" \ - --sbom out/buildx-sbom.cdx.json \ - --sbom-name buildx-sbom.cdx.json \ - --artifact-type application/vnd.stellaops.sbom.layer+json \ - --sbom-format cyclonedx-json \ - --sbom-kind inventory \ - --repository git.stella-ops.org/stellaops/buildx-demo \ - --build-ref $(git rev-parse HEAD) \ - > out/buildx-descriptor.json - ``` - +# BuildX Generator Quickstart + +This quickstart explains how to run the StellaOps **BuildX SBOM generator** offline, verify the CAS handshake, and emit OCI descriptors that downstream services can attest. + +## 1. Prerequisites + +- Docker 25+ with BuildKit enabled (`docker buildx` available). +- .NET 10 (preview) SDK matching the repository `global.json`. +- Optional: network access to a StellaOps Attestor endpoint (the quickstart uses a mock service). + +## 2. Publish the plug-in binaries + +The BuildX generator publishes as a .NET self-contained executable with its manifest under `plugins/scanner/buildx/`. + +```bash +# From the repository root +DOTNET_CLI_HOME="${PWD}/.dotnet" \ +dotnet publish src/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj \ + -c Release \ + -o out/buildx +``` + +- `out/buildx/` now contains `StellaOps.Scanner.Sbomer.BuildXPlugin.dll` and the manifest `stellaops.sbom-indexer.manifest.json`. +- `plugins/scanner/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin/` receives the same artefacts for release packaging. + +## 3. Verify the CAS handshake + +```bash +dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll handshake \ + --manifest out/buildx \ + --cas out/cas +``` + +The command performs a deterministic probe write (`sha256`) into the provided CAS directory and prints the resolved path. + +## 4. Emit a descriptor + provenance placeholder + +1. Build or identify the image you want to describe and capture its digest: + + ```bash + docker buildx build --load -t stellaops/buildx-demo:ci samples/ci/buildx-demo + DIGEST=$(docker image inspect stellaops/buildx-demo:ci --format '{{index .RepoDigests 0}}') + ``` + +2. Generate a CycloneDX SBOM for the built image (any tool works; here we use `docker sbom`): + + ```bash + docker sbom stellaops/buildx-demo:ci --format cyclonedx-json > out/buildx-sbom.cdx.json + ``` + +3. Invoke the `descriptor` command, pointing at the SBOM file and optional metadata: + + ```bash + dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll descriptor \ + --manifest out/buildx \ + --image "$DIGEST" \ + --sbom out/buildx-sbom.cdx.json \ + --sbom-name buildx-sbom.cdx.json \ + --artifact-type application/vnd.stellaops.sbom.layer+json \ + --sbom-format cyclonedx-json \ + --sbom-kind inventory \ + --repository git.stella-ops.org/stellaops/buildx-demo \ + --build-ref $(git rev-parse HEAD) \ + > out/buildx-descriptor.json + ``` + The output JSON captures: - OCI artifact descriptor including size, digest, and annotations (`org.stellaops.*`). -- Provenance placeholder (`expectedDsseSha256`, `nonce`, `attestorUri` when provided). +- Provenance placeholder (`expectedDsseSha256`, `nonce`, `attestorUri` when provided). `nonce` is derived deterministically from the image + SBOM metadata so repeated runs produce identical placeholders for identical inputs. - Generator metadata and deterministic timestamps. - -## 5. (Optional) Send the placeholder to an Attestor - -The plug-in can POST the descriptor metadata to an Attestor endpoint, returning once it receives an HTTP 202. - -```bash -python3 - <<'PY' & -from http.server import BaseHTTPRequestHandler, HTTPServer -class Handler(BaseHTTPRequestHandler): - def do_POST(self): - _ = self.rfile.read(int(self.headers.get('Content-Length', 0))) - self.send_response(202); self.end_headers(); self.wfile.write(b'accepted') - def log_message(self, fmt, *args): - return -server = HTTPServer(('127.0.0.1', 8085), Handler) -try: - server.serve_forever() -except KeyboardInterrupt: - pass -finally: - server.server_close() -PY -MOCK_PID=$! - -dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll descriptor \ - --manifest out/buildx \ - --image "$DIGEST" \ - --sbom out/buildx-sbom.cdx.json \ - --attestor http://127.0.0.1:8085/provenance \ - --attestor-token "$STELLAOPS_ATTESTOR_TOKEN" \ - > out/buildx-descriptor.json - -kill $MOCK_PID -``` - -Set `STELLAOPS_ATTESTOR_TOKEN` (or pass `--attestor-token`) when the Attestor requires bearer authentication. Use `--attestor-insecure` for lab environments with self-signed certificates. - -## 6. CI workflow example - + +## 5. (Optional) Send the placeholder to an Attestor + +The plug-in can POST the descriptor metadata to an Attestor endpoint, returning once it receives an HTTP 202. + +```bash +python3 - <<'PY' & +from http.server import BaseHTTPRequestHandler, HTTPServer +class Handler(BaseHTTPRequestHandler): + def do_POST(self): + _ = self.rfile.read(int(self.headers.get('Content-Length', 0))) + self.send_response(202); self.end_headers(); self.wfile.write(b'accepted') + def log_message(self, fmt, *args): + return +server = HTTPServer(('127.0.0.1', 8085), Handler) +try: + server.serve_forever() +except KeyboardInterrupt: + pass +finally: + server.server_close() +PY +MOCK_PID=$! + +dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll descriptor \ + --manifest out/buildx \ + --image "$DIGEST" \ + --sbom out/buildx-sbom.cdx.json \ + --attestor http://127.0.0.1:8085/provenance \ + --attestor-token "$STELLAOPS_ATTESTOR_TOKEN" \ + > out/buildx-descriptor.json + +kill $MOCK_PID +``` + +Set `STELLAOPS_ATTESTOR_TOKEN` (or pass `--attestor-token`) when the Attestor requires bearer authentication. Use `--attestor-insecure` for lab environments with self-signed certificates. + +## 6. CI workflow example + A reusable GitHub Actions workflow is provided under `samples/ci/buildx-demo/github-actions-buildx-demo.yml`. It publishes the plug-in, runs the handshake, builds the demo image, emits a descriptor, and uploads both the descriptor and the mock-Attestor request as artefacts. -Add the workflow to your repository (or call it via `workflow_call`) and adjust the SBOM path + Attestor URL as needed. +Add the workflow to your repository (or call it via `workflow_call`) and adjust the SBOM path + Attestor URL as needed. The workflow also re-runs the `descriptor` command and diffs the results (ignoring the `generatedAt` timestamp) so you catch regressions that would break deterministic CI use. --- diff --git a/plugins/scanner/entrytrace/StellaOps.Scanner.EntryTrace/StellaOps.Scanner.EntryTrace.deps.json b/plugins/scanner/entrytrace/StellaOps.Scanner.EntryTrace/StellaOps.Scanner.EntryTrace.deps.json new file mode 100644 index 00000000..219e38a6 --- /dev/null +++ b/plugins/scanner/entrytrace/StellaOps.Scanner.EntryTrace/StellaOps.Scanner.EntryTrace.deps.json @@ -0,0 +1,191 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v10.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v10.0": { + "StellaOps.Scanner.EntryTrace/1.0.0": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.0", + "StellaOps.Plugin": "1.0.0" + }, + "runtime": { + "StellaOps.Scanner.EntryTrace.dll": {} + } + }, + "Microsoft.Extensions.Configuration.Abstractions/9.0.0": { + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.0" + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Configuration.Abstractions.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.24.52809" + } + } + }, + "Microsoft.Extensions.Configuration.Binder/9.0.0": { + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0" + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Configuration.Binder.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.24.52809" + } + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/9.0.0": { + "runtime": { + "lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.24.52809" + } + } + }, + "Microsoft.Extensions.Logging.Abstractions/9.0.0": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Logging.Abstractions.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.24.52809" + } + } + }, + "Microsoft.Extensions.Options/9.0.0": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Options.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.24.52809" + } + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions/9.0.0": { + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.Configuration.Binder": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Options.ConfigurationExtensions.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.24.52809" + } + } + }, + "Microsoft.Extensions.Primitives/9.0.0": { + "runtime": { + "lib/net9.0/Microsoft.Extensions.Primitives.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.24.52809" + } + } + }, + "StellaOps.DependencyInjection/1.0.0": { + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" + }, + "runtime": { + "StellaOps.DependencyInjection.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.0.0" + } + } + }, + "StellaOps.Plugin/1.0.0": { + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "StellaOps.DependencyInjection": "1.0.0" + }, + "runtime": { + "StellaOps.Plugin.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.0.0" + } + } + } + } + }, + "libraries": { + "StellaOps.Scanner.EntryTrace/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Microsoft.Extensions.Configuration.Abstractions/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-lqvd7W3FGKUO1+ZoUEMaZ5XDJeWvjpy2/M/ptCGz3tXLD4HWVaSzjufsAsjemasBEg+2SxXVtYVvGt5r2nKDlg==", + "path": "microsoft.extensions.configuration.abstractions/9.0.0", + "hashPath": "microsoft.extensions.configuration.abstractions.9.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Configuration.Binder/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-RiScL99DcyngY9zJA2ROrri7Br8tn5N4hP4YNvGdTN/bvg1A3dwvDOxHnNZ3Im7x2SJ5i4LkX1uPiR/MfSFBLQ==", + "path": "microsoft.extensions.configuration.binder/9.0.0", + "hashPath": "microsoft.extensions.configuration.binder.9.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==", + "path": "microsoft.extensions.dependencyinjection.abstractions/9.0.0", + "hashPath": "microsoft.extensions.dependencyinjection.abstractions.9.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Logging.Abstractions/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==", + "path": "microsoft.extensions.logging.abstractions/9.0.0", + "hashPath": "microsoft.extensions.logging.abstractions.9.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Options/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==", + "path": "microsoft.extensions.options/9.0.0", + "hashPath": "microsoft.extensions.options.9.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Options.ConfigurationExtensions/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Ob3FXsXkcSMQmGZi7qP07EQ39kZpSBlTcAZLbJLdI4FIf0Jug8biv2HTavWmnTirchctPlq9bl/26CXtQRguzA==", + "path": "microsoft.extensions.options.configurationextensions/9.0.0", + "hashPath": "microsoft.extensions.options.configurationextensions.9.0.0.nupkg.sha512" + }, + "Microsoft.Extensions.Primitives/9.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg==", + "path": "microsoft.extensions.primitives/9.0.0", + "hashPath": "microsoft.extensions.primitives.9.0.0.nupkg.sha512" + }, + "StellaOps.DependencyInjection/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "StellaOps.Plugin/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/plugins/scanner/entrytrace/StellaOps.Scanner.EntryTrace/manifest.json b/plugins/scanner/entrytrace/StellaOps.Scanner.EntryTrace/manifest.json new file mode 100644 index 00000000..a1bc5bc2 --- /dev/null +++ b/plugins/scanner/entrytrace/StellaOps.Scanner.EntryTrace/manifest.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": "1.0", + "id": "stellaops.entrytrace.analyzers", + "displayName": "StellaOps EntryTrace Analyzer Pack", + "version": "0.1.0-alpha", + "requiresRestart": true, + "entryPoint": { + "type": "dotnet", + "executable": "StellaOps.Scanner.EntryTrace.dll", + "arguments": [ + "handshake" + ] + }, + "capabilities": [ + "entrytrace", + "analyzer" + ], + "metadata": { + "org.stellaops.plugin.kind": "entrytrace-analyzer", + "org.stellaops.restart.required": "true" + } +} diff --git a/samples/ci/buildx-demo/github-actions-buildx-demo.yml b/samples/ci/buildx-demo/github-actions-buildx-demo.yml index 1dd4ef9b..c79a08ab 100644 --- a/samples/ci/buildx-demo/github-actions-buildx-demo.yml +++ b/samples/ci/buildx-demo/github-actions-buildx-demo.yml @@ -1,85 +1,85 @@ -name: Buildx SBOM Demo -on: - workflow_dispatch: - push: - branches: [ demo/buildx ] - -jobs: - buildx-sbom: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Set up .NET 10 preview - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Publish StellaOps BuildX generator - run: | - dotnet publish src/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj \ - -c Release \ - -o out/buildx - - - name: Handshake CAS - run: | - dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll handshake \ - --manifest out/buildx \ - --cas out/cas - - - name: Build demo container image - run: | - docker buildx build --load -t stellaops/buildx-demo:ci samples/ci/buildx-demo - - - name: Capture image digest - id: digest - run: | - DIGEST=$(docker image inspect stellaops/buildx-demo:ci --format '{{index .RepoDigests 0}}') - echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" - - - name: Generate SBOM from built image - run: | - mkdir -p out - docker sbom stellaops/buildx-demo:ci --format cyclonedx-json > out/buildx-sbom.cdx.json - - - name: Start mock Attestor - id: attestor - run: | - mkdir -p out - cat <<'PY' > out/mock-attestor.py -import json -import os -from http.server import BaseHTTPRequestHandler, HTTPServer - -class Handler(BaseHTTPRequestHandler): - def do_POST(self): - length = int(self.headers.get('Content-Length') or 0) - body = self.rfile.read(length) - with open(os.path.join('out', 'provenance-request.json'), 'wb') as fp: - fp.write(body) - self.send_response(202) - self.end_headers() - self.wfile.write(b'accepted') - - def log_message(self, format, *args): - return - -if __name__ == '__main__': - server = HTTPServer(('127.0.0.1', 8085), Handler) - try: - server.serve_forever() - except KeyboardInterrupt: - pass - finally: - server.server_close() -PY - touch out/provenance-request.json - python3 out/mock-attestor.py & - echo $! > out/mock-attestor.pid - +name: Buildx SBOM Demo +on: + workflow_dispatch: + push: + branches: [ demo/buildx ] + +jobs: + buildx-sbom: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Set up .NET 10 preview + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Publish StellaOps BuildX generator + run: | + dotnet publish src/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj \ + -c Release \ + -o out/buildx + + - name: Handshake CAS + run: | + dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll handshake \ + --manifest out/buildx \ + --cas out/cas + + - name: Build demo container image + run: | + docker buildx build --load -t stellaops/buildx-demo:ci samples/ci/buildx-demo + + - name: Capture image digest + id: digest + run: | + DIGEST=$(docker image inspect stellaops/buildx-demo:ci --format '{{index .RepoDigests 0}}') + echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" + + - name: Generate SBOM from built image + run: | + mkdir -p out + docker sbom stellaops/buildx-demo:ci --format cyclonedx-json > out/buildx-sbom.cdx.json + + - name: Start mock Attestor + id: attestor + run: | + mkdir -p out + cat <<'PY' > out/mock-attestor.py +import json +import os +from http.server import BaseHTTPRequestHandler, HTTPServer + +class Handler(BaseHTTPRequestHandler): + def do_POST(self): + length = int(self.headers.get('Content-Length') or 0) + body = self.rfile.read(length) + with open(os.path.join('out', 'provenance-request.json'), 'wb') as fp: + fp.write(body) + self.send_response(202) + self.end_headers() + self.wfile.write(b'accepted') + + def log_message(self, format, *args): + return + +if __name__ == '__main__': + server = HTTPServer(('127.0.0.1', 8085), Handler) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + finally: + server.server_close() +PY + touch out/provenance-request.json + python3 out/mock-attestor.py & + echo $! > out/mock-attestor.pid + - name: Emit descriptor with provenance placeholder env: IMAGE_DIGEST: ${{ steps.digest.outputs.digest }} @@ -99,22 +99,55 @@ PY --attestor http://127.0.0.1:8085/provenance \ > out/buildx-descriptor.json + - name: Verify descriptor determinism + env: + IMAGE_DIGEST: ${{ steps.digest.outputs.digest }} + run: | + dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll descriptor \ + --manifest out/buildx \ + --image "$IMAGE_DIGEST" \ + --sbom out/buildx-sbom.cdx.json \ + --sbom-name buildx-sbom.cdx.json \ + --artifact-type application/vnd.stellaops.sbom.layer+json \ + --sbom-format cyclonedx-json \ + --sbom-kind inventory \ + --repository ${{ github.repository }} \ + --build-ref ${{ github.sha }} \ + > out/buildx-descriptor-repeat.json + + python - <<'PY' +import json + +def normalize(path: str) -> dict: + with open(path, 'r', encoding='utf-8') as handle: + data = json.load(handle) + data.pop('generatedAt', None) + return data + +baseline = normalize('out/buildx-descriptor.json') +repeat = normalize('out/buildx-descriptor-repeat.json') + +if baseline != repeat: + raise SystemExit('Descriptor output changed between runs.') +PY + - name: Stop mock Attestor if: always() run: | if [ -f out/mock-attestor.pid ]; then - kill $(cat out/mock-attestor.pid) - fi - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: stellaops-buildx-demo + kill $(cat out/mock-attestor.pid) + fi + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: stellaops-buildx-demo path: | out/buildx-descriptor.json out/buildx-sbom.cdx.json out/provenance-request.json - - - name: Show descriptor summary - run: | - cat out/buildx-descriptor.json + out/buildx-descriptor-repeat.json + + - name: Show descriptor summary + run: | + cat out/buildx-descriptor.json diff --git a/src/StellaOps.Scanner.EntryTrace.Tests/EntryTraceAnalyzerTests.cs b/src/StellaOps.Scanner.EntryTrace.Tests/EntryTraceAnalyzerTests.cs new file mode 100644 index 00000000..7583b2ba --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace.Tests/EntryTraceAnalyzerTests.cs @@ -0,0 +1,136 @@ +using System.Collections.Immutable; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using StellaOps.Scanner.EntryTrace.Diagnostics; +using Xunit; + +namespace StellaOps.Scanner.EntryTrace.Tests; + +public sealed class EntryTraceAnalyzerTests +{ + private static EntryTraceAnalyzer CreateAnalyzer() + { + var options = Options.Create(new EntryTraceAnalyzerOptions + { + MaxDepth = 32, + FollowRunParts = true + }); + return new EntryTraceAnalyzer(options, new EntryTraceMetrics(), NullLogger.Instance); + } + + [Fact] + public async Task ResolveAsync_FollowsShellIncludeAndPythonModule() + { + var fs = new TestRootFileSystem(); + fs.AddFile("/entrypoint.sh", """ + #!/bin/sh + source /opt/setup.sh + exec python -m app.main --flag + """); + fs.AddFile("/opt/setup.sh", """ + #!/bin/sh + run-parts /opt/setup.d + """); + fs.AddDirectory("/opt/setup.d"); + fs.AddFile("/opt/setup.d/001-node.sh", """ + #!/bin/sh + exec node /app/server.js + """); + fs.AddFile("/opt/setup.d/010-java.sh", """ + #!/bin/sh + java -jar /app/app.jar + """); + fs.AddFile("/usr/bin/python", "#!/usr/bin/env python3\n", executable: true); + fs.AddFile("/usr/bin/node", "#!/usr/bin/env node\n", executable: true); + fs.AddFile("/usr/bin/java", "", executable: true); + fs.AddFile("/app/server.js", "console.log('hello');", executable: true); + fs.AddFile("/app/app.jar", string.Empty, executable: true); + + var analyzer = CreateAnalyzer(); + var context = new EntryTraceContext( + fs, + ImmutableDictionary.Empty, + ImmutableArray.Create("/usr/bin", "/usr/local/bin"), + "/", + "sha256:image", + "scan-entrytrace-1", + NullLogger.Instance); + + var spec = EntrypointSpecification.FromExecForm(new[] { "/entrypoint.sh" }, Array.Empty()); + var result = await analyzer.ResolveAsync(spec, context); + + Assert.Equal(EntryTraceOutcome.Resolved, result.Outcome); + Assert.Empty(result.Diagnostics); + + var nodeNames = result.Nodes.Select(n => (n.Kind, n.DisplayName)).ToArray(); + Assert.Contains((EntryTraceNodeKind.Command, "/entrypoint.sh"), nodeNames); + Assert.Contains((EntryTraceNodeKind.Include, "/opt/setup.sh"), nodeNames); + Assert.Contains(nodeNames, tuple => tuple.Kind == EntryTraceNodeKind.Command && tuple.DisplayName == "python"); + Assert.Contains(nodeNames, tuple => tuple.Kind == EntryTraceNodeKind.Command && tuple.DisplayName == "node"); + Assert.Contains(nodeNames, tuple => tuple.Kind == EntryTraceNodeKind.Command && tuple.DisplayName == "java"); + Assert.Contains(nodeNames, tuple => tuple.Kind == EntryTraceNodeKind.RunPartsDirectory && tuple.DisplayName == "/opt/setup.d"); + + Assert.Contains(result.Edges, edge => edge.Relationship == "python-module" && edge.Metadata is { } metadata && metadata.TryGetValue("module", out var module) && module == "app.main"); + } + + [Fact] + public async Task ResolveAsync_RecordsDiagnosticsForMissingInclude() + { + var fs = new TestRootFileSystem(); + fs.AddFile("/entrypoint.sh", """ + #!/bin/sh + source /missing/setup.sh + exec /bin/true + """); + fs.AddFile("/bin/true", string.Empty, executable: true); + + var analyzer = CreateAnalyzer(); + var context = new EntryTraceContext( + fs, + ImmutableDictionary.Empty, + ImmutableArray.Create("/bin"), + "/", + "sha256:image", + "scan-entrytrace-2", + NullLogger.Instance); + + var spec = EntrypointSpecification.FromExecForm(new[] { "/entrypoint.sh" }, Array.Empty()); + var result = await analyzer.ResolveAsync(spec, context); + + Assert.Equal(EntryTraceOutcome.PartiallyResolved, result.Outcome); + Assert.Single(result.Diagnostics); + Assert.Equal(EntryTraceUnknownReason.MissingFile, result.Diagnostics[0].Reason); + } + + [Fact] + public async Task ResolveAsync_IsDeterministic() + { + var fs = new TestRootFileSystem(); + fs.AddFile("/entrypoint.sh", """ + #!/bin/sh + exec node /app/index.js + """); + fs.AddFile("/usr/bin/node", string.Empty, executable: true); + fs.AddFile("/app/index.js", "console.log('deterministic');", executable: true); + + var analyzer = CreateAnalyzer(); + var context = new EntryTraceContext( + fs, + ImmutableDictionary.Empty, + ImmutableArray.Create("/usr/bin"), + "/", + "sha256:image", + "scan-entrytrace-3", + NullLogger.Instance); + + var spec = EntrypointSpecification.FromExecForm(new[] { "/entrypoint.sh" }, Array.Empty()); + var first = await analyzer.ResolveAsync(spec, context); + var second = await analyzer.ResolveAsync(spec, context); + + Assert.Equal(first.Outcome, second.Outcome); + Assert.Equal(first.Diagnostics, second.Diagnostics); + Assert.Equal(first.Nodes.Select(n => (n.Kind, n.DisplayName)).ToArray(), second.Nodes.Select(n => (n.Kind, n.DisplayName)).ToArray()); + Assert.Equal(first.Edges.Select(e => (e.FromNodeId, e.ToNodeId, e.Relationship)).ToArray(), + second.Edges.Select(e => (e.FromNodeId, e.ToNodeId, e.Relationship)).ToArray()); + } +} diff --git a/src/StellaOps.Scanner.EntryTrace.Tests/ShellParserTests.cs b/src/StellaOps.Scanner.EntryTrace.Tests/ShellParserTests.cs new file mode 100644 index 00000000..f8e0e25a --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace.Tests/ShellParserTests.cs @@ -0,0 +1,33 @@ +using StellaOps.Scanner.EntryTrace.Parsing; +using Xunit; + +namespace StellaOps.Scanner.EntryTrace.Tests; + +public sealed class ShellParserTests +{ + [Fact] + public void Parse_ProducesDeterministicNodes() + { + const string script = """ + #!/bin/sh + source /opt/init.sh + if [ -f /etc/profile ]; then + . /etc/profile + fi + + run-parts /etc/entry.d + exec python -m app.main --flag + """; + + var first = ShellParser.Parse(script); + var second = ShellParser.Parse(script); + + Assert.Equal(first.Nodes.Length, second.Nodes.Length); + var actual = first.Nodes.Select(n => n.GetType().Name).ToArray(); + var expected = new[] { nameof(ShellIncludeNode), nameof(ShellIfNode), nameof(ShellRunPartsNode), nameof(ShellExecNode) }; + Assert.Equal(expected, actual); + + var actualSecond = second.Nodes.Select(n => n.GetType().Name).ToArray(); + Assert.Equal(expected, actualSecond); + } +} diff --git a/src/StellaOps.Scanner.EntryTrace.Tests/StellaOps.Scanner.EntryTrace.Tests.csproj b/src/StellaOps.Scanner.EntryTrace.Tests/StellaOps.Scanner.EntryTrace.Tests.csproj new file mode 100644 index 00000000..d1b8d058 --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace.Tests/StellaOps.Scanner.EntryTrace.Tests.csproj @@ -0,0 +1,13 @@ + + + net10.0 + enable + enable + + + + + + + + diff --git a/src/StellaOps.Scanner.EntryTrace.Tests/TestRootFileSystem.cs b/src/StellaOps.Scanner.EntryTrace.Tests/TestRootFileSystem.cs new file mode 100644 index 00000000..7e0e6930 --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace.Tests/TestRootFileSystem.cs @@ -0,0 +1,180 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using StellaOps.Scanner.EntryTrace; + +namespace StellaOps.Scanner.EntryTrace.Tests; + +internal sealed class TestRootFileSystem : IRootFileSystem +{ + private readonly Dictionary _entries = new(StringComparer.Ordinal); + private readonly HashSet _directories = new(StringComparer.Ordinal); + + public TestRootFileSystem() + { + _directories.Add("/"); + } + + public void AddFile(string path, string content, bool executable = true, string? layer = "sha256:layer-a") + { + var normalized = Normalize(path); + var directory = Path.GetDirectoryName(normalized); + if (!string.IsNullOrEmpty(directory)) + { + _directories.Add(directory!); + } + + _entries[normalized] = new FileEntry(normalized, content, executable, layer, IsDirectory: false); + } + + public void AddDirectory(string path) + { + var normalized = Normalize(path); + _directories.Add(normalized); + } + + public bool TryResolveExecutable(string name, IReadOnlyList searchPaths, out RootFileDescriptor descriptor) + { + if (name.Contains('/', StringComparison.Ordinal)) + { + var normalized = Normalize(name); + if (_entries.TryGetValue(normalized, out var file) && file.IsExecutable) + { + descriptor = file.ToDescriptor(); + return true; + } + + descriptor = null!; + return false; + } + + foreach (var prefix in searchPaths) + { + var candidate = Combine(prefix, name); + if (_entries.TryGetValue(candidate, out var file) && file.IsExecutable) + { + descriptor = file.ToDescriptor(); + return true; + } + } + + descriptor = null!; + return false; + } + + public bool TryReadAllText(string path, out RootFileDescriptor descriptor, out string content) + { + var normalized = Normalize(path); + if (_entries.TryGetValue(normalized, out var file)) + { + descriptor = file.ToDescriptor(); + content = file.Content; + return true; + } + + descriptor = null!; + content = string.Empty; + return false; + } + + public ImmutableArray EnumerateDirectory(string path) + { + var normalized = Normalize(path); + var builder = ImmutableArray.CreateBuilder(); + + foreach (var file in _entries.Values) + { + var directory = Normalize(Path.GetDirectoryName(file.Path) ?? "/"); + if (string.Equals(directory, normalized, StringComparison.Ordinal)) + { + builder.Add(file.ToDescriptor()); + } + } + + return builder.ToImmutable(); + } + + public bool DirectoryExists(string path) + { + var normalized = Normalize(path); + return _directories.Contains(normalized); + } + + private static string Combine(string prefix, string name) + { + var normalizedPrefix = Normalize(prefix); + if (normalizedPrefix == "/") + { + return Normalize("/" + name); + } + + return Normalize($"{normalizedPrefix}/{name}"); + } + + private static string Normalize(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return "/"; + } + + var text = path.Replace('\\', '/').Trim(); + if (!text.StartsWith("/", StringComparison.Ordinal)) + { + text = "/" + text; + } + + var parts = new List(); + foreach (var part in text.Split('/', StringSplitOptions.RemoveEmptyEntries)) + { + if (part == ".") + { + continue; + } + + if (part == "..") + { + if (parts.Count > 0) + { + parts.RemoveAt(parts.Count - 1); + } + continue; + } + + parts.Add(part); + } + + return "/" + string.Join('/', parts); + } + + private sealed record FileEntry(string Path, string Content, bool IsExecutable, string? Layer, bool IsDirectory) + { + public RootFileDescriptor ToDescriptor() + { + var shebang = ExtractShebang(Content); + return new RootFileDescriptor(Path, Layer, IsExecutable, IsDirectory, shebang); + } + } + + private static string? ExtractShebang(string content) + { + if (string.IsNullOrEmpty(content)) + { + return null; + } + + using var reader = new StringReader(content); + var firstLine = reader.ReadLine(); + if (firstLine is null) + { + return null; + } + + if (!firstLine.StartsWith("#!", StringComparison.Ordinal)) + { + return null; + } + + return firstLine[2..].Trim(); + } +} diff --git a/src/StellaOps.Scanner.EntryTrace/AGENTS.md b/src/StellaOps.Scanner.EntryTrace/AGENTS.md new file mode 100644 index 00000000..cbbd0c60 --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/AGENTS.md @@ -0,0 +1,32 @@ +# StellaOps.Scanner.EntryTrace — Agent Charter + +## Mission +Resolve container `ENTRYPOINT`/`CMD` chains into deterministic call graphs that fuel usage-aware SBOMs, policy explainability, and runtime drift detection. Implement the EntryTrace analyzers and expose them as restart-time plug-ins for the Scanner Worker. + +## Scope +- Parse POSIX/Bourne shell constructs (exec, command, case, if, source/run-parts) with deterministic AST output. +- Walk layered root filesystems to resolve PATH lookups, interpreter hand-offs (Python/Node/Java), and record evidence. +- Surface explainable diagnostics for unresolved branches (env indirection, missing files, unsupported syntax) and emit metrics. +- Package analyzers as signed plug-ins under `plugins/scanner/entrytrace/`, guarded by restart-only policy. + +## Out of Scope +- SBOM emission/diffing (owned by `Scanner.Emit`/`Scanner.Diff`). +- Runtime enforcement or live drift reconciliation (owned by Zastava). +- Registry/network fetchers beyond file lookups inside extracted layers. + +## Interfaces & Contracts +- Primary entry point: `IEntryTraceAnalyzer.ResolveAsync` returning a deterministic `EntryTraceGraph`. +- Graph nodes must include file path, line span, interpreter classification, evidence source, and follow `Scanner.Core` timestamp/ID helpers when emitting events. +- Diagnostics must enumerate unknown reasons from fixed enum; metrics tagged `entrytrace.*`. +- Plug-ins register via `IEntryTraceAnalyzerFactory` and must validate against `IPluginCatalogGuard`. + +## Observability & Security +- No dynamic assembly loading beyond restart-time plug-in catalog. +- Structured logs include `scanId`, `imageDigest`, `layerDigest`, `command`, `reason`. +- Metrics counters: `entrytrace_resolutions_total{result}`, `entrytrace_unresolved_total{reason}`. +- Deny `source` directives outside image root; sandbox file IO via provided `IRootFileSystem`. + +## Testing +- Unit tests live in `../StellaOps.Scanner.EntryTrace.Tests` with golden fixtures under `Fixtures/`. +- Determinism harness: same inputs produce byte-identical serialized graphs. +- Parser fuzz seeds captured for regression; interpreter tracers validated with sample scripts for Python, Node, Java launchers. diff --git a/src/StellaOps.Scanner.EntryTrace/Diagnostics/EntryTraceMetrics.cs b/src/StellaOps.Scanner.EntryTrace/Diagnostics/EntryTraceMetrics.cs new file mode 100644 index 00000000..087514b9 --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/Diagnostics/EntryTraceMetrics.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Diagnostics.Metrics; + +namespace StellaOps.Scanner.EntryTrace.Diagnostics; + +public static class EntryTraceInstrumentation +{ + public static readonly Meter Meter = new("stellaops.scanner.entrytrace", "1.0.0"); +} + +public sealed class EntryTraceMetrics +{ + private readonly Counter _resolutions; + private readonly Counter _unresolved; + + public EntryTraceMetrics() + { + _resolutions = EntryTraceInstrumentation.Meter.CreateCounter( + "entrytrace_resolutions_total", + description: "Number of entry trace attempts by outcome."); + _unresolved = EntryTraceInstrumentation.Meter.CreateCounter( + "entrytrace_unresolved_total", + description: "Number of unresolved entry trace hops by reason."); + } + + public void RecordOutcome(string imageDigest, string scanId, EntryTraceOutcome outcome) + { + _resolutions.Add(1, CreateTags(imageDigest, scanId, ("outcome", outcome.ToString().ToLowerInvariant()))); + } + + public void RecordUnknown(string imageDigest, string scanId, EntryTraceUnknownReason reason) + { + _unresolved.Add(1, CreateTags(imageDigest, scanId, ("reason", reason.ToString().ToLowerInvariant()))); + } + + private static KeyValuePair[] CreateTags(string imageDigest, string scanId, params (string Key, object? Value)[] extras) + { + var tags = new List>(2 + extras.Length) + { + new("image", imageDigest), + new("scan.id", scanId) + }; + + foreach (var extra in extras) + { + tags.Add(new KeyValuePair(extra.Key, extra.Value)); + } + + return tags.ToArray(); + } +} diff --git a/src/StellaOps.Scanner.EntryTrace/EntryTraceAnalyzer.cs b/src/StellaOps.Scanner.EntryTrace/EntryTraceAnalyzer.cs new file mode 100644 index 00000000..7b821a87 --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/EntryTraceAnalyzer.cs @@ -0,0 +1,963 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StellaOps.Scanner.EntryTrace.Diagnostics; +using StellaOps.Scanner.EntryTrace.Parsing; + +namespace StellaOps.Scanner.EntryTrace; + +public sealed class EntryTraceAnalyzer : IEntryTraceAnalyzer +{ + private readonly EntryTraceAnalyzerOptions _options; + private readonly EntryTraceMetrics _metrics; + private readonly ILogger _logger; + + public EntryTraceAnalyzer( + IOptions options, + EntryTraceMetrics metrics, + ILogger logger) + { + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _metrics = metrics ?? throw new ArgumentNullException(nameof(metrics)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + if (_options.MaxDepth <= 0) + { + _options.MaxDepth = 32; + } + + if (string.IsNullOrWhiteSpace(_options.DefaultPath)) + { + _options.DefaultPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; + } + } + + public ValueTask ResolveAsync( + EntrypointSpecification entrypoint, + EntryTraceContext context, + CancellationToken cancellationToken = default) + { + if (entrypoint is null) + { + throw new ArgumentNullException(nameof(entrypoint)); + } + + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + cancellationToken.ThrowIfCancellationRequested(); + + var builder = new Builder( + entrypoint, + context, + _options, + _metrics, + _logger); + + var graph = builder.BuildGraph(); + _metrics.RecordOutcome(context.ImageDigest, context.ScanId, graph.Outcome); + foreach (var diagnostic in graph.Diagnostics) + { + _metrics.RecordUnknown(context.ImageDigest, context.ScanId, diagnostic.Reason); + } + + return ValueTask.FromResult(graph); + } + + private sealed class Builder + { + private readonly EntrypointSpecification _entrypoint; + private readonly EntryTraceContext _context; + private readonly EntryTraceAnalyzerOptions _options; + private readonly EntryTraceMetrics _metrics; + private readonly ILogger _logger; + private readonly ImmutableArray _pathEntries; + private readonly List _nodes = new(); + private readonly List _edges = new(); + private readonly List _diagnostics = new(); + private readonly HashSet _visitedScripts = new(StringComparer.Ordinal); + private readonly HashSet _visitedCommands = new(StringComparer.Ordinal); + private int _nextNodeId = 1; + + public Builder( + EntrypointSpecification entrypoint, + EntryTraceContext context, + EntryTraceAnalyzerOptions options, + EntryTraceMetrics metrics, + ILogger logger) + { + _entrypoint = entrypoint; + _context = context; + _options = options; + _metrics = metrics; + _logger = logger; + _pathEntries = DeterminePath(context); + } + + private static ImmutableArray DeterminePath(EntryTraceContext context) + { + if (context.Path.Length > 0) + { + return context.Path; + } + + if (context.Environment.TryGetValue("PATH", out var raw) && !string.IsNullOrWhiteSpace(raw)) + { + return raw.Split(':').Select(p => p.Trim()).Where(p => p.Length > 0).ToImmutableArray(); + } + + return ImmutableArray.Empty; + } + + public EntryTraceGraph BuildGraph() + { + var initialArgs = ComposeInitialCommand(_entrypoint); + if (initialArgs.Length == 0) + { + _diagnostics.Add(new EntryTraceDiagnostic( + EntryTraceDiagnosticSeverity.Error, + EntryTraceUnknownReason.CommandNotFound, + "ENTRYPOINT/CMD yielded no executable command.", + Span: null, + RelatedPath: null)); + return ToGraph(EntryTraceOutcome.Unresolved); + } + + ResolveCommand(initialArgs, parent: null, originSpan: null, depth: 0, relationship: "entrypoint"); + + var outcome = DetermineOutcome(); + return ToGraph(outcome); + } + + private EntryTraceOutcome DetermineOutcome() + { + if (_diagnostics.Count == 0) + { + return EntryTraceOutcome.Resolved; + } + + return _diagnostics.Any(d => d.Severity == EntryTraceDiagnosticSeverity.Error) + ? EntryTraceOutcome.Unresolved + : EntryTraceOutcome.PartiallyResolved; + } + + private EntryTraceGraph ToGraph(EntryTraceOutcome outcome) + { + return new EntryTraceGraph( + outcome, + _nodes.ToImmutableArray(), + _edges.ToImmutableArray(), + _diagnostics.ToImmutableArray()); + } + + private ImmutableArray ComposeInitialCommand(EntrypointSpecification specification) + { + if (specification.Entrypoint.Length > 0) + { + if (specification.Command.Length > 0) + { + return specification.Entrypoint.Concat(specification.Command).ToImmutableArray(); + } + + return specification.Entrypoint; + } + + if (specification.Command.Length > 0) + { + return specification.Command; + } + + if (!string.IsNullOrWhiteSpace(specification.EntrypointShell)) + { + return ImmutableArray.Create("/bin/sh", "-c", specification.EntrypointShell!); + } + + if (!string.IsNullOrWhiteSpace(specification.CommandShell)) + { + return ImmutableArray.Create("/bin/sh", "-c", specification.CommandShell!); + } + + return ImmutableArray.Empty; + } + + private void ResolveCommand( + ImmutableArray arguments, + EntryTraceNode? parent, + EntryTraceSpan? originSpan, + int depth, + string relationship) + { + if (arguments.Length == 0) + { + return; + } + + if (depth >= _options.MaxDepth) + { + _diagnostics.Add(new EntryTraceDiagnostic( + EntryTraceDiagnosticSeverity.Warning, + EntryTraceUnknownReason.RecursionLimitReached, + $"Recursion depth limit {_options.MaxDepth} reached while resolving '{arguments[0]}'.", + originSpan, + RelatedPath: null)); + return; + } + + var commandName = arguments[0]; + var evidence = default(EntryTraceEvidence?); + var descriptor = default(RootFileDescriptor); + + if (!TryResolveExecutable(commandName, out descriptor, out evidence)) + { + _diagnostics.Add(new EntryTraceDiagnostic( + EntryTraceDiagnosticSeverity.Warning, + EntryTraceUnknownReason.CommandNotFound, + $"Command '{commandName}' not found in PATH.", + originSpan, + RelatedPath: null)); + return; + } + + var node = AddNode( + EntryTraceNodeKind.Command, + commandName, + arguments, + DetermineInterpreterKind(descriptor), + evidence, + originSpan); + + if (parent is not null) + { + _edges.Add(new EntryTraceEdge(parent.Id, node.Id, relationship, Metadata: null)); + } + + if (!_visitedCommands.Add(descriptor.Path)) + { + // Prevent infinite loops when scripts call themselves recursively. + return; + } + + if (TryFollowInterpreter(node, descriptor, arguments, depth)) + { + return; + } + + if (TryFollowShell(node, descriptor, arguments, depth)) + { + return; + } + + // Terminal executable. + } + + private bool TryResolveExecutable( + string commandName, + out RootFileDescriptor descriptor, + out EntryTraceEvidence? evidence) + { + evidence = null; + + if (commandName.Contains('/', StringComparison.Ordinal)) + { + if (_context.FileSystem.TryReadAllText(commandName, out descriptor, out _)) + { + evidence = new EntryTraceEvidence(commandName, descriptor.LayerDigest, "path", null); + return true; + } + + if (_context.FileSystem.TryResolveExecutable(commandName, Array.Empty(), out descriptor)) + { + evidence = new EntryTraceEvidence(descriptor.Path, descriptor.LayerDigest, "path", null); + return true; + } + } + + if (_context.FileSystem.TryResolveExecutable(commandName, _pathEntries, out descriptor)) + { + evidence = new EntryTraceEvidence(descriptor.Path, descriptor.LayerDigest, "path-search", new Dictionary + { + ["command"] = commandName + }); + return true; + } + + descriptor = null!; + return false; + } + + private bool TryFollowInterpreter( + EntryTraceNode node, + RootFileDescriptor descriptor, + ImmutableArray arguments, + int depth) + { + var interpreter = DetermineInterpreterKind(descriptor); + if (interpreter == EntryTraceInterpreterKind.None) + { + interpreter = DetectInterpreterFromCommand(arguments); + } + + if (interpreter == EntryTraceInterpreterKind.None) + { + return false; + } + + switch (interpreter) + { + case EntryTraceInterpreterKind.Python: + return HandlePython(node, arguments, descriptor, depth); + case EntryTraceInterpreterKind.Node: + return HandleNode(node, arguments, descriptor, depth); + case EntryTraceInterpreterKind.Java: + return HandleJava(node, arguments, descriptor, depth); + default: + return false; + } + } + + private EntryTraceInterpreterKind DetermineInterpreterKind(RootFileDescriptor descriptor) + { + if (descriptor.ShebangInterpreter is null) + { + return EntryTraceInterpreterKind.None; + } + + var shebang = descriptor.ShebangInterpreter.ToLowerInvariant(); + if (shebang.Contains("python", StringComparison.Ordinal)) + { + return EntryTraceInterpreterKind.Python; + } + + if (shebang.Contains("node", StringComparison.Ordinal)) + { + return EntryTraceInterpreterKind.Node; + } + + if (shebang.Contains("java", StringComparison.Ordinal)) + { + return EntryTraceInterpreterKind.Java; + } + + if (shebang.Contains("sh", StringComparison.Ordinal) || shebang.Contains("bash", StringComparison.Ordinal)) + { + return EntryTraceInterpreterKind.None; + } + + return EntryTraceInterpreterKind.None; + } + + private EntryTraceInterpreterKind DetectInterpreterFromCommand(ImmutableArray arguments) + { + if (arguments.Length == 0) + { + return EntryTraceInterpreterKind.None; + } + + var command = arguments[0]; + if (command.Equals("python", StringComparison.OrdinalIgnoreCase) || + command.StartsWith("python", StringComparison.OrdinalIgnoreCase)) + { + return EntryTraceInterpreterKind.Python; + } + + if (command.Equals("node", StringComparison.OrdinalIgnoreCase) || + command.Equals("nodejs", StringComparison.OrdinalIgnoreCase)) + { + return EntryTraceInterpreterKind.Node; + } + + if (command.Equals("java", StringComparison.OrdinalIgnoreCase)) + { + return EntryTraceInterpreterKind.Java; + } + + return EntryTraceInterpreterKind.None; + } + + private bool HandlePython( + EntryTraceNode node, + ImmutableArray arguments, + RootFileDescriptor descriptor, + int depth) + { + if (arguments.Length < 2) + { + return false; + } + + var argIndex = 1; + var moduleMode = false; + string? moduleName = null; + string? scriptPath = null; + + while (argIndex < arguments.Length) + { + var current = arguments[argIndex]; + if (current == "-m" && argIndex + 1 < arguments.Length) + { + moduleMode = true; + moduleName = arguments[argIndex + 1]; + break; + } + + if (!current.StartsWith("-", StringComparison.Ordinal)) + { + scriptPath = current; + break; + } + + argIndex++; + } + + if (moduleMode && moduleName is not null) + { + _edges.Add(new EntryTraceEdge(node.Id, node.Id, "python-module", new Dictionary + { + ["module"] = moduleName + })); + return true; + } + + if (scriptPath is null) + { + return false; + } + + if (!_context.FileSystem.TryReadAllText(scriptPath, out var scriptDescriptor, out var content)) + { + _diagnostics.Add(new EntryTraceDiagnostic( + EntryTraceDiagnosticSeverity.Warning, + EntryTraceUnknownReason.MissingFile, + $"Python script '{scriptPath}' was not found.", + Span: null, + RelatedPath: scriptPath)); + return true; + } + + var scriptNode = AddNode( + EntryTraceNodeKind.Script, + scriptPath, + ImmutableArray.Empty, + EntryTraceInterpreterKind.Python, + new EntryTraceEvidence(scriptDescriptor.Path, scriptDescriptor.LayerDigest, "script", null), + null); + + _edges.Add(new EntryTraceEdge(node.Id, scriptNode.Id, "executes", null)); + + if (IsLikelyShell(content)) + { + ResolveShellScript(content, scriptDescriptor.Path, scriptNode, depth + 1); + } + + return true; + } + + private bool HandleNode( + EntryTraceNode node, + ImmutableArray arguments, + RootFileDescriptor descriptor, + int depth) + { + if (arguments.Length < 2) + { + return false; + } + + var scriptArg = arguments.Skip(1).FirstOrDefault(a => !a.StartsWith("-", StringComparison.Ordinal)); + if (string.IsNullOrWhiteSpace(scriptArg)) + { + return false; + } + + if (!_context.FileSystem.TryReadAllText(scriptArg, out var scriptDescriptor, out var content)) + { + _diagnostics.Add(new EntryTraceDiagnostic( + EntryTraceDiagnosticSeverity.Warning, + EntryTraceUnknownReason.MissingFile, + $"Node script '{scriptArg}' was not found.", + Span: null, + RelatedPath: scriptArg)); + return true; + } + + var scriptNode = AddNode( + EntryTraceNodeKind.Script, + scriptArg, + ImmutableArray.Empty, + EntryTraceInterpreterKind.Node, + new EntryTraceEvidence(scriptDescriptor.Path, scriptDescriptor.LayerDigest, "script", null), + null); + + _edges.Add(new EntryTraceEdge(node.Id, scriptNode.Id, "executes", null)); + return true; + } + + private bool HandleJava( + EntryTraceNode node, + ImmutableArray arguments, + RootFileDescriptor descriptor, + int depth) + { + if (arguments.Length < 2) + { + return false; + } + + string? jar = null; + string? mainClass = null; + + for (var i = 1; i < arguments.Length; i++) + { + var arg = arguments[i]; + if (arg == "-jar" && i + 1 < arguments.Length) + { + jar = arguments[i + 1]; + break; + } + + if (!arg.StartsWith("-", StringComparison.Ordinal) && mainClass is null) + { + mainClass = arg; + } + } + + if (jar is not null) + { + if (!_context.FileSystem.TryResolveExecutable(jar, Array.Empty(), out var jarDescriptor)) + { + _diagnostics.Add(new EntryTraceDiagnostic( + EntryTraceDiagnosticSeverity.Warning, + EntryTraceUnknownReason.JarNotFound, + $"Java JAR '{jar}' not found.", + Span: null, + RelatedPath: jar)); + } + else + { + var jarNode = AddNode( + EntryTraceNodeKind.Executable, + jarDescriptor.Path, + ImmutableArray.Empty, + EntryTraceInterpreterKind.Java, + new EntryTraceEvidence(jarDescriptor.Path, jarDescriptor.LayerDigest, "jar", null), + null); + _edges.Add(new EntryTraceEdge(node.Id, jarNode.Id, "executes", null)); + } + + return true; + } + + if (mainClass is not null) + { + _edges.Add(new EntryTraceEdge(node.Id, node.Id, "java-main", new Dictionary + { + ["class"] = mainClass + })); + return true; + } + + return false; + } + + private bool TryFollowShell( + EntryTraceNode node, + RootFileDescriptor descriptor, + ImmutableArray arguments, + int depth) + { + if (!IsShellExecutable(descriptor, arguments)) + { + return false; + } + + if (arguments.Length >= 2 && arguments[1] == "-c" && arguments.Length >= 3) + { + var scriptText = arguments[2]; + ResolveShellScript(scriptText, descriptor.Path, node, depth + 1); + return true; + } + + if (arguments.Length >= 2) + { + var candidate = arguments[1]; + if (_context.FileSystem.TryReadAllText(candidate, out var scriptDescriptor, out var content)) + { + var scriptNode = AddNode( + EntryTraceNodeKind.Script, + candidate, + ImmutableArray.Empty, + EntryTraceInterpreterKind.None, + new EntryTraceEvidence(scriptDescriptor.Path, scriptDescriptor.LayerDigest, "script", null), + null); + _edges.Add(new EntryTraceEdge(node.Id, scriptNode.Id, "executes", null)); + ResolveShellScript(content, scriptDescriptor.Path, scriptNode, depth + 1); + return true; + } + } + + if (arguments.Length == 1) + { + if (_context.FileSystem.TryReadAllText(descriptor.Path, out var scriptDescriptor, out var content)) + { + var scriptNode = AddNode( + EntryTraceNodeKind.Script, + descriptor.Path, + ImmutableArray.Empty, + EntryTraceInterpreterKind.None, + new EntryTraceEvidence(scriptDescriptor.Path, scriptDescriptor.LayerDigest, "script", null), + null); + _edges.Add(new EntryTraceEdge(node.Id, scriptNode.Id, "executes", null)); + ResolveShellScript(content, scriptDescriptor.Path, scriptNode, depth + 1); + return true; + } + } + + return false; + } + + private static bool IsShellExecutable(RootFileDescriptor descriptor, ImmutableArray arguments) + { + if (descriptor.ShebangInterpreter is not null && + (descriptor.ShebangInterpreter.Contains("sh", StringComparison.OrdinalIgnoreCase) || + descriptor.ShebangInterpreter.Contains("bash", StringComparison.OrdinalIgnoreCase))) + { + return true; + } + + var command = arguments[0]; + return command is "/bin/sh" or "sh" or "bash" or "/bin/bash"; + } + + private void ResolveShellScript( + string scriptContent, + string scriptPath, + EntryTraceNode parent, + int depth) + { + if (_visitedScripts.Contains(scriptPath)) + { + return; + } + + _visitedScripts.Add(scriptPath); + + ShellScript ast; + try + { + ast = ShellParser.Parse(scriptContent); + } + catch (Exception ex) + { + _diagnostics.Add(new EntryTraceDiagnostic( + EntryTraceDiagnosticSeverity.Warning, + EntryTraceUnknownReason.UnsupportedSyntax, + $"Failed to parse shell script '{scriptPath}': {ex.Message}", + Span: null, + RelatedPath: scriptPath)); + return; + } + + foreach (var node in ast.Nodes) + { + HandleShellNode(node, parent, scriptPath, depth); + } + } + + private void HandleShellNode( + ShellNode node, + EntryTraceNode parent, + string scriptPath, + int depth) + { + switch (node) + { + case ShellExecNode execNode: + { + var args = MaterializeArguments(execNode.Arguments); + if (args.Length <= 1) + { + break; + } + + var execArgs = args.RemoveAt(0); + ResolveCommand(execArgs, parent, ToEntryTraceSpan(execNode.Span, scriptPath), depth + 1, "executes"); + break; + } + case ShellIncludeNode includeNode: + { + var includeArg = includeNode.PathExpression; + var includePath = ResolveScriptPath(scriptPath, includeArg); + if (!_context.FileSystem.TryReadAllText(includePath, out var descriptor, out var content)) + { + _diagnostics.Add(new EntryTraceDiagnostic( + EntryTraceDiagnosticSeverity.Warning, + EntryTraceUnknownReason.MissingFile, + $"Included script '{includePath}' not found.", + ToEntryTraceSpan(includeNode.Span, scriptPath), + includePath)); + break; + } + + var includeTraceNode = AddNode( + EntryTraceNodeKind.Include, + includePath, + ImmutableArray.Empty, + EntryTraceInterpreterKind.None, + new EntryTraceEvidence(descriptor.Path, descriptor.LayerDigest, "include", null), + ToEntryTraceSpan(includeNode.Span, scriptPath)); + + _edges.Add(new EntryTraceEdge(parent.Id, includeTraceNode.Id, "includes", null)); + ResolveShellScript(content, descriptor.Path, includeTraceNode, depth + 1); + break; + } + case ShellRunPartsNode runPartsNode when _options.FollowRunParts: + { + var directory = ResolveScriptPath(scriptPath, runPartsNode.DirectoryExpression); + if (!_context.FileSystem.DirectoryExists(directory)) + { + _diagnostics.Add(new EntryTraceDiagnostic( + EntryTraceDiagnosticSeverity.Warning, + EntryTraceUnknownReason.MissingFile, + $"run-parts directory '{directory}' not found.", + ToEntryTraceSpan(runPartsNode.Span, scriptPath), + directory)); + break; + } + + var entries = _context.FileSystem.EnumerateDirectory(directory) + .Where(e => !e.IsDirectory && e.IsExecutable) + .OrderBy(e => e.Path, StringComparer.Ordinal) + .Take(_options.RunPartsLimit) + .ToList(); + + if (entries.Count == 0) + { + _diagnostics.Add(new EntryTraceDiagnostic( + EntryTraceDiagnosticSeverity.Info, + EntryTraceUnknownReason.RunPartsEmpty, + $"run-parts directory '{directory}' contained no executable files.", + ToEntryTraceSpan(runPartsNode.Span, scriptPath), + directory)); + break; + } + + var dirNode = AddNode( + EntryTraceNodeKind.RunPartsDirectory, + directory, + ImmutableArray.Empty, + EntryTraceInterpreterKind.None, + new EntryTraceEvidence(directory, null, "run-parts", null), + ToEntryTraceSpan(runPartsNode.Span, scriptPath)); + _edges.Add(new EntryTraceEdge(parent.Id, dirNode.Id, "run-parts", null)); + + foreach (var entry in entries) + { + var childNode = AddNode( + EntryTraceNodeKind.RunPartsScript, + entry.Path, + ImmutableArray.Empty, + EntryTraceInterpreterKind.None, + new EntryTraceEvidence(entry.Path, entry.LayerDigest, "run-parts", null), + null); + _edges.Add(new EntryTraceEdge(dirNode.Id, childNode.Id, "executes", null)); + + if (_context.FileSystem.TryReadAllText(entry.Path, out var childDescriptor, out var content)) + { + ResolveShellScript(content, childDescriptor.Path, childNode, depth + 1); + } + } + + break; + } + case ShellIfNode ifNode: + { + foreach (var branch in ifNode.Branches) + { + foreach (var inner in branch.Body) + { + HandleShellNode(inner, parent, scriptPath, depth + 1); + } + } + + break; + } + case ShellCaseNode caseNode: + { + foreach (var arm in caseNode.Arms) + { + foreach (var inner in arm.Body) + { + HandleShellNode(inner, parent, scriptPath, depth + 1); + } + } + + break; + } + case ShellCommandNode commandNode: + { + var args = MaterializeArguments(commandNode.Arguments); + if (args.Length == 0) + { + break; + } + + // Skip shell built-in wrappers. + if (args[0] is "command" or "env") + { + var sliced = args.Skip(1).ToImmutableArray(); + ResolveCommand(sliced, parent, ToEntryTraceSpan(commandNode.Span, scriptPath), depth + 1, "calls"); + } + else + { + ResolveCommand(args, parent, ToEntryTraceSpan(commandNode.Span, scriptPath), depth + 1, "calls"); + } + + break; + } + default: + break; + } + } + + private static EntryTraceSpan? ToEntryTraceSpan(ShellSpan span, string path) + => new(path, span.StartLine, span.StartColumn, span.EndLine, span.EndColumn); + + private static ImmutableArray MaterializeArguments(ImmutableArray tokens) + { + var builder = ImmutableArray.CreateBuilder(tokens.Length); + foreach (var token in tokens) + { + builder.Add(token.Value); + } + + return builder.ToImmutable(); + } + + private string ResolveScriptPath(string currentScript, string candidate) + { + if (string.IsNullOrWhiteSpace(candidate)) + { + return candidate; + } + + if (candidate.StartsWith("/", StringComparison.Ordinal)) + { + return NormalizeUnixPath(candidate); + } + + if (candidate.StartsWith("$", StringComparison.Ordinal)) + { + _diagnostics.Add(new EntryTraceDiagnostic( + EntryTraceDiagnosticSeverity.Warning, + EntryTraceUnknownReason.DynamicEnvironmentReference, + $"Path '{candidate}' depends on environment variable expansion and cannot be resolved statically.", + Span: null, + RelatedPath: candidate)); + return candidate; + } + + var normalizedScript = NormalizeUnixPath(currentScript); + var lastSlash = normalizedScript.LastIndexOf('/'); + var baseDirectory = lastSlash <= 0 ? "/" : normalizedScript[..lastSlash]; + return CombineUnixPath(baseDirectory, candidate); + } + + private static bool IsLikelyShell(string content) + { + if (string.IsNullOrEmpty(content)) + { + return false; + } + + if (content.StartsWith("#!", StringComparison.Ordinal)) + { + return content.Contains("sh", StringComparison.OrdinalIgnoreCase); + } + + return content.Contains("#!/bin/sh", StringComparison.Ordinal); + } + + private EntryTraceNode AddNode( + EntryTraceNodeKind kind, + string displayName, + ImmutableArray arguments, + EntryTraceInterpreterKind interpreterKind, + EntryTraceEvidence? evidence, + EntryTraceSpan? span) + { + var node = new EntryTraceNode( + _nextNodeId++, + kind, + displayName, + arguments, + interpreterKind, + evidence, + span); + _nodes.Add(node); + return node; + } + + private static string CombineUnixPath(string baseDirectory, string relative) + { + var normalizedBase = NormalizeUnixPath(baseDirectory); + var trimmedRelative = relative.Replace('\\', '/').Trim(); + if (string.IsNullOrEmpty(trimmedRelative)) + { + return normalizedBase; + } + + if (trimmedRelative.StartsWith('/')) + { + return NormalizeUnixPath(trimmedRelative); + } + + if (!normalizedBase.EndsWith('/')) + { + normalizedBase += "/"; + } + + return NormalizeUnixPath(normalizedBase + trimmedRelative); + } + + private static string NormalizeUnixPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return "/"; + } + + var text = path.Replace('\\', '/').Trim(); + if (!text.StartsWith('/')) + { + text = "/" + text; + } + + var segments = new List(); + foreach (var part in text.Split('/', StringSplitOptions.RemoveEmptyEntries)) + { + if (part == ".") + { + continue; + } + + if (part == "..") + { + if (segments.Count > 0) + { + segments.RemoveAt(segments.Count - 1); + } + continue; + } + + segments.Add(part); + } + + return segments.Count == 0 ? "/" : "/" + string.Join('/', segments); + } + } +} diff --git a/src/StellaOps.Scanner.EntryTrace/EntryTraceAnalyzerOptions.cs b/src/StellaOps.Scanner.EntryTrace/EntryTraceAnalyzerOptions.cs new file mode 100644 index 00000000..d92a9100 --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/EntryTraceAnalyzerOptions.cs @@ -0,0 +1,26 @@ +namespace StellaOps.Scanner.EntryTrace; + +public sealed class EntryTraceAnalyzerOptions +{ + public const string SectionName = "Scanner:Analyzers:EntryTrace"; + + /// + /// Maximum recursion depth while following includes/run-parts/interpreters. + /// + public int MaxDepth { get; set; } = 64; + + /// + /// Enables traversal of run-parts directories. + /// + public bool FollowRunParts { get; set; } = true; + + /// + /// Colon-separated default PATH string used when the environment omits PATH. + /// + public string DefaultPath { get; set; } = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; + + /// + /// Maximum number of scripts considered per run-parts directory to prevent explosion. + /// + public int RunPartsLimit { get; set; } = 64; +} diff --git a/src/StellaOps.Scanner.EntryTrace/EntryTraceContext.cs b/src/StellaOps.Scanner.EntryTrace/EntryTraceContext.cs new file mode 100644 index 00000000..c6815ada --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/EntryTraceContext.cs @@ -0,0 +1,16 @@ +using System.Collections.Immutable; +using Microsoft.Extensions.Logging; + +namespace StellaOps.Scanner.EntryTrace; + +/// +/// Provides runtime context for entry trace analysis. +/// +public sealed record EntryTraceContext( + IRootFileSystem FileSystem, + ImmutableDictionary Environment, + ImmutableArray Path, + string WorkingDirectory, + string ImageDigest, + string ScanId, + ILogger? Logger); diff --git a/src/StellaOps.Scanner.EntryTrace/EntryTraceTypes.cs b/src/StellaOps.Scanner.EntryTrace/EntryTraceTypes.cs new file mode 100644 index 00000000..5130e497 --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/EntryTraceTypes.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace StellaOps.Scanner.EntryTrace; + +/// +/// Outcome classification for entrypoint resolution attempts. +/// +public enum EntryTraceOutcome +{ + Resolved, + PartiallyResolved, + Unresolved +} + +/// +/// Logical classification for nodes in the entry trace graph. +/// +public enum EntryTraceNodeKind +{ + Command, + Script, + Include, + Interpreter, + Executable, + RunPartsDirectory, + RunPartsScript +} + +/// +/// Interpreter categories supported by the analyzer. +/// +public enum EntryTraceInterpreterKind +{ + None, + Python, + Node, + Java +} + +/// +/// Diagnostic severity levels emitted by the analyzer. +/// +public enum EntryTraceDiagnosticSeverity +{ + Info, + Warning, + Error +} + +/// +/// Enumerates the canonical reasons for unresolved edges. +/// +public enum EntryTraceUnknownReason +{ + CommandNotFound, + MissingFile, + DynamicEnvironmentReference, + UnsupportedSyntax, + RecursionLimitReached, + InterpreterNotSupported, + ModuleNotFound, + JarNotFound, + RunPartsEmpty, + PermissionDenied +} + +/// +/// Represents a span within a script file. +/// +public readonly record struct EntryTraceSpan( + string? Path, + int StartLine, + int StartColumn, + int EndLine, + int EndColumn); + +/// +/// Evidence describing where a node originated from within the image. +/// +public sealed record EntryTraceEvidence( + string Path, + string? LayerDigest, + string Source, + IReadOnlyDictionary? Metadata); + +/// +/// Represents a node in the entry trace graph. +/// +public sealed record EntryTraceNode( + int Id, + EntryTraceNodeKind Kind, + string DisplayName, + ImmutableArray Arguments, + EntryTraceInterpreterKind InterpreterKind, + EntryTraceEvidence? Evidence, + EntryTraceSpan? Span); + +/// +/// Represents a directed edge in the entry trace graph. +/// +public sealed record EntryTraceEdge( + int FromNodeId, + int ToNodeId, + string Relationship, + IReadOnlyDictionary? Metadata); + +/// +/// Captures diagnostic information regarding resolution gaps. +/// +public sealed record EntryTraceDiagnostic( + EntryTraceDiagnosticSeverity Severity, + EntryTraceUnknownReason Reason, + string Message, + EntryTraceSpan? Span, + string? RelatedPath); + +/// +/// Final graph output produced by the analyzer. +/// +public sealed record EntryTraceGraph( + EntryTraceOutcome Outcome, + ImmutableArray Nodes, + ImmutableArray Edges, + ImmutableArray Diagnostics); diff --git a/src/StellaOps.Scanner.EntryTrace/EntrypointSpecification.cs b/src/StellaOps.Scanner.EntryTrace/EntrypointSpecification.cs new file mode 100644 index 00000000..25c58c48 --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/EntrypointSpecification.cs @@ -0,0 +1,71 @@ +using System.Collections.Immutable; + +namespace StellaOps.Scanner.EntryTrace; + +/// +/// Represents the combined Docker ENTRYPOINT/CMD contract provided to the analyzer. +/// +public sealed record EntrypointSpecification +{ + private EntrypointSpecification( + ImmutableArray entrypoint, + ImmutableArray command, + string? entrypointShell, + string? commandShell) + { + Entrypoint = entrypoint; + Command = command; + EntrypointShell = string.IsNullOrWhiteSpace(entrypointShell) ? null : entrypointShell; + CommandShell = string.IsNullOrWhiteSpace(commandShell) ? null : commandShell; + } + + /// + /// Exec-form ENTRYPOINT arguments. + /// + public ImmutableArray Entrypoint { get; } + + /// + /// Exec-form CMD arguments. + /// + public ImmutableArray Command { get; } + + /// + /// Shell-form ENTRYPOINT (if provided). + /// + public string? EntrypointShell { get; } + + /// + /// Shell-form CMD (if provided). + /// + public string? CommandShell { get; } + + public static EntrypointSpecification FromExecForm( + IEnumerable? entrypoint, + IEnumerable? command) + => new( + entrypoint is null ? ImmutableArray.Empty : entrypoint.ToImmutableArray(), + command is null ? ImmutableArray.Empty : command.ToImmutableArray(), + entrypointShell: null, + commandShell: null); + + public static EntrypointSpecification FromShellForm( + string? entrypoint, + string? command) + => new( + ImmutableArray.Empty, + ImmutableArray.Empty, + entrypoint, + command); + + public EntrypointSpecification WithCommand(IEnumerable? command) + => new(Entrypoint, command?.ToImmutableArray() ?? ImmutableArray.Empty, EntrypointShell, CommandShell); + + public EntrypointSpecification WithCommandShell(string? commandShell) + => new(Entrypoint, Command, EntrypointShell, commandShell); + + public EntrypointSpecification WithEntrypoint(IEnumerable? entrypoint) + => new(entrypoint?.ToImmutableArray() ?? ImmutableArray.Empty, Command, EntrypointShell, CommandShell); + + public EntrypointSpecification WithEntrypointShell(string? entrypointShell) + => new(Entrypoint, Command, entrypointShell, CommandShell); +} diff --git a/src/StellaOps.Scanner.EntryTrace/FileSystem/IRootFileSystem.cs b/src/StellaOps.Scanner.EntryTrace/FileSystem/IRootFileSystem.cs new file mode 100644 index 00000000..e06609a9 --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/FileSystem/IRootFileSystem.cs @@ -0,0 +1,39 @@ +using System.Collections.Immutable; + +namespace StellaOps.Scanner.EntryTrace; + +/// +/// Represents a layered read-only filesystem snapshot built from container layers. +/// +public interface IRootFileSystem +{ + /// + /// Attempts to resolve an executable by name using the provided PATH entries. + /// + bool TryResolveExecutable(string name, IReadOnlyList searchPaths, out RootFileDescriptor descriptor); + + /// + /// Attempts to read the contents of a file as UTF-8 text. + /// + bool TryReadAllText(string path, out RootFileDescriptor descriptor, out string content); + + /// + /// Returns descriptors for entries contained within a directory. + /// + ImmutableArray EnumerateDirectory(string path); + + /// + /// Checks whether a directory exists. + /// + bool DirectoryExists(string path); +} + +/// +/// Describes a file discovered within the layered filesystem. +/// +public sealed record RootFileDescriptor( + string Path, + string? LayerDigest, + bool IsExecutable, + bool IsDirectory, + string? ShebangInterpreter); diff --git a/src/StellaOps.Scanner.EntryTrace/IEntryTraceAnalyzer.cs b/src/StellaOps.Scanner.EntryTrace/IEntryTraceAnalyzer.cs new file mode 100644 index 00000000..ed947179 --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/IEntryTraceAnalyzer.cs @@ -0,0 +1,9 @@ +namespace StellaOps.Scanner.EntryTrace; + +public interface IEntryTraceAnalyzer +{ + ValueTask ResolveAsync( + EntrypointSpecification entrypoint, + EntryTraceContext context, + CancellationToken cancellationToken = default); +} diff --git a/src/StellaOps.Scanner.EntryTrace/Parsing/ShellNodes.cs b/src/StellaOps.Scanner.EntryTrace/Parsing/ShellNodes.cs new file mode 100644 index 00000000..7f558682 --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/Parsing/ShellNodes.cs @@ -0,0 +1,54 @@ +using System.Collections.Immutable; + +namespace StellaOps.Scanner.EntryTrace.Parsing; + +public abstract record ShellNode(ShellSpan Span); + +public sealed record ShellScript(ImmutableArray Nodes); + +public sealed record ShellSpan(int StartLine, int StartColumn, int EndLine, int EndColumn); + +public sealed record ShellCommandNode( + string Command, + ImmutableArray Arguments, + ShellSpan Span) : ShellNode(Span); + +public sealed record ShellIncludeNode( + string PathExpression, + ImmutableArray Arguments, + ShellSpan Span) : ShellNode(Span); + +public sealed record ShellExecNode( + ImmutableArray Arguments, + ShellSpan Span) : ShellNode(Span); + +public sealed record ShellIfNode( + ImmutableArray Branches, + ShellSpan Span) : ShellNode(Span); + +public sealed record ShellConditionalBranch( + ShellConditionalKind Kind, + ImmutableArray Body, + ShellSpan Span, + string? PredicateSummary); + +public enum ShellConditionalKind +{ + If, + Elif, + Else +} + +public sealed record ShellCaseNode( + ImmutableArray Arms, + ShellSpan Span) : ShellNode(Span); + +public sealed record ShellCaseArm( + ImmutableArray Patterns, + ImmutableArray Body, + ShellSpan Span); + +public sealed record ShellRunPartsNode( + string DirectoryExpression, + ImmutableArray Arguments, + ShellSpan Span) : ShellNode(Span); diff --git a/src/StellaOps.Scanner.EntryTrace/Parsing/ShellParser.cs b/src/StellaOps.Scanner.EntryTrace/Parsing/ShellParser.cs new file mode 100644 index 00000000..c92e4c2a --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/Parsing/ShellParser.cs @@ -0,0 +1,485 @@ +using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace StellaOps.Scanner.EntryTrace.Parsing; + +/// +/// Deterministic parser producing a lightweight AST for Bourne shell constructs needed by EntryTrace. +/// Supports: simple commands, exec, source/dot, run-parts, if/elif/else/fi, case/esac. +/// +public sealed class ShellParser +{ + private readonly IReadOnlyList _tokens; + private int _index; + + private ShellParser(IReadOnlyList tokens) + { + _tokens = tokens; + } + + public static ShellScript Parse(string source) + { + var tokenizer = new ShellTokenizer(); + var tokens = tokenizer.Tokenize(source); + var parser = new ShellParser(tokens); + var nodes = parser.ParseNodes(untilKeywords: null); + return new ShellScript(nodes.ToImmutableArray()); + } + + private List ParseNodes(HashSet? untilKeywords) + { + var nodes = new List(); + + while (true) + { + SkipNewLines(); + var token = Peek(); + + if (token.Kind == ShellTokenKind.EndOfFile) + { + break; + } + + if (token.Kind == ShellTokenKind.Word && untilKeywords is not null && untilKeywords.Contains(token.Value)) + { + break; + } + + ShellNode? node = token.Kind switch + { + ShellTokenKind.Word when token.Value == "if" => ParseIf(), + ShellTokenKind.Word when token.Value == "case" => ParseCase(), + _ => ParseCommandLike() + }; + + if (node is not null) + { + nodes.Add(node); + } + + SkipCommandSeparators(); + } + + return nodes; + } + + private ShellNode ParseCommandLike() + { + var start = Peek(); + var tokens = ReadUntilTerminator(); + + if (tokens.Count == 0) + { + return new ShellCommandNode(string.Empty, ImmutableArray.Empty, CreateSpan(start, start)); + } + + var normalizedName = ExtractCommandName(tokens); + var immutableTokens = tokens.ToImmutableArray(); + var span = CreateSpan(tokens[0], tokens[^1]); + + return normalizedName switch + { + "exec" => new ShellExecNode(immutableTokens, span), + "source" or "." => new ShellIncludeNode( + ExtractPrimaryArgument(immutableTokens), + immutableTokens, + span), + "run-parts" => new ShellRunPartsNode( + ExtractPrimaryArgument(immutableTokens), + immutableTokens, + span), + _ => new ShellCommandNode(normalizedName, immutableTokens, span) + }; + } + + private ShellIfNode ParseIf() + { + var start = Expect(ShellTokenKind.Word, "if"); + var predicateTokens = ReadUntilKeyword("then"); + Expect(ShellTokenKind.Word, "then"); + + var branches = new List(); + var predicateSummary = JoinTokens(predicateTokens); + var thenNodes = ParseNodes(new HashSet(StringComparer.Ordinal) + { + "elif", + "else", + "fi" + }); + + branches.Add(new ShellConditionalBranch( + ShellConditionalKind.If, + thenNodes.ToImmutableArray(), + CreateSpan(start, thenNodes.LastOrDefault()?.Span ?? CreateSpan(start, start)), + predicateSummary)); + + while (true) + { + SkipNewLines(); + var next = Peek(); + if (next.Kind != ShellTokenKind.Word) + { + break; + } + + if (next.Value == "elif") + { + var elifStart = Advance(); + var elifPredicate = ReadUntilKeyword("then"); + Expect(ShellTokenKind.Word, "then"); + var elifBody = ParseNodes(new HashSet(StringComparer.Ordinal) + { + "elif", + "else", + "fi" + }); + var span = elifBody.Count > 0 + ? CreateSpan(elifStart, elifBody[^1].Span) + : CreateSpan(elifStart, elifStart); + + branches.Add(new ShellConditionalBranch( + ShellConditionalKind.Elif, + elifBody.ToImmutableArray(), + span, + JoinTokens(elifPredicate))); + continue; + } + + if (next.Value == "else") + { + var elseStart = Advance(); + var elseBody = ParseNodes(new HashSet(StringComparer.Ordinal) + { + "fi" + }); + branches.Add(new ShellConditionalBranch( + ShellConditionalKind.Else, + elseBody.ToImmutableArray(), + elseBody.Count > 0 ? CreateSpan(elseStart, elseBody[^1].Span) : CreateSpan(elseStart, elseStart), + null)); + break; + } + + break; + } + + Expect(ShellTokenKind.Word, "fi"); + var end = Previous(); + return new ShellIfNode(branches.ToImmutableArray(), CreateSpan(start, end)); + } + + private ShellCaseNode ParseCase() + { + var start = Expect(ShellTokenKind.Word, "case"); + var selectorTokens = ReadUntilKeyword("in"); + Expect(ShellTokenKind.Word, "in"); + + var arms = new List(); + while (true) + { + SkipNewLines(); + var token = Peek(); + if (token.Kind == ShellTokenKind.Word && token.Value == "esac") + { + break; + } + + if (token.Kind == ShellTokenKind.EndOfFile) + { + throw new FormatException("Unexpected end of file while parsing case arms."); + } + + var patterns = ReadPatterns(); + Expect(ShellTokenKind.Operator, ")"); + + var body = ParseNodes(new HashSet(StringComparer.Ordinal) + { + ";;", + "esac" + }); + + ShellSpan span; + if (body.Count > 0) + { + span = CreateSpan(patterns.FirstToken ?? token, body[^1].Span); + } + else + { + span = CreateSpan(patterns.FirstToken ?? token, token); + } + + arms.Add(new ShellCaseArm( + patterns.Values.ToImmutableArray(), + body.ToImmutableArray(), + span)); + + SkipNewLines(); + var separator = Peek(); + if (separator.Kind == ShellTokenKind.Operator && separator.Value == ";;") + { + Advance(); + continue; + } + + if (separator.Kind == ShellTokenKind.Word && separator.Value == "esac") + { + break; + } + } + + Expect(ShellTokenKind.Word, "esac"); + return new ShellCaseNode(arms.ToImmutableArray(), CreateSpan(start, Previous())); + + (List Values, ShellToken? FirstToken) ReadPatterns() + { + var values = new List(); + ShellToken? first = null; + var sb = new StringBuilder(); + + while (true) + { + var current = Peek(); + if (current.Kind is ShellTokenKind.Operator && current.Value is ")" or "|") + { + if (sb.Length > 0) + { + values.Add(sb.ToString()); + sb.Clear(); + } + + if (current.Value == "|") + { + Advance(); + continue; + } + + break; + } + + if (current.Kind == ShellTokenKind.EndOfFile) + { + throw new FormatException("Unexpected EOF in case arm pattern."); + } + + if (first is null) + { + first = current; + } + + sb.Append(current.Value); + Advance(); + } + + if (values.Count == 0 && sb.Length > 0) + { + values.Add(sb.ToString()); + } + + return (values, first); + } + } + + private List ReadUntilTerminator() + { + var tokens = new List(); + while (true) + { + var token = Peek(); + if (token.Kind is ShellTokenKind.EndOfFile or ShellTokenKind.NewLine) + { + break; + } + + if (token.Kind == ShellTokenKind.Operator && token.Value is ";" or "&&" or "||") + { + break; + } + + tokens.Add(Advance()); + } + + return tokens; + } + + private ImmutableArray ReadUntilKeyword(string keyword) + { + var tokens = new List(); + while (true) + { + var token = Peek(); + if (token.Kind == ShellTokenKind.EndOfFile) + { + throw new FormatException($"Unexpected EOF while looking for keyword '{keyword}'."); + } + + if (token.Kind == ShellTokenKind.Word && token.Value == keyword) + { + break; + } + + tokens.Add(Advance()); + } + + return tokens.ToImmutableArray(); + } + + private static string ExtractCommandName(IReadOnlyList tokens) + { + foreach (var token in tokens) + { + if (token.Kind is not ShellTokenKind.Word and not ShellTokenKind.SingleQuoted and not ShellTokenKind.DoubleQuoted) + { + continue; + } + + if (token.Value.Contains('=', StringComparison.Ordinal)) + { + // Skip environment assignments e.g. FOO=bar exec /app + var eqIndex = token.Value.IndexOf('=', StringComparison.Ordinal); + if (eqIndex > 0 && token.Value[..eqIndex].All(IsIdentifierChar)) + { + continue; + } + } + + return NormalizeCommandName(token.Value); + } + + return string.Empty; + + static bool IsIdentifierChar(char c) => char.IsLetterOrDigit(c) || c == '_'; + } + + private static string NormalizeCommandName(string value) + { + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + + return value switch + { + "." => ".", + _ => value.Trim() + }; + } + + private void SkipCommandSeparators() + { + while (true) + { + var token = Peek(); + if (token.Kind == ShellTokenKind.NewLine) + { + Advance(); + continue; + } + + if (token.Kind == ShellTokenKind.Operator && (token.Value == ";" || token.Value == "&")) + { + Advance(); + continue; + } + + break; + } + } + + private void SkipNewLines() + { + while (Peek().Kind == ShellTokenKind.NewLine) + { + Advance(); + } + } + + private ShellToken Expect(ShellTokenKind kind, string? value = null) + { + var token = Peek(); + if (token.Kind != kind || (value is not null && token.Value != value)) + { + throw new FormatException($"Unexpected token '{token.Value}' at line {token.Line}, expected {value ?? kind.ToString()}."); + } + + return Advance(); + } + + private ShellToken Advance() + { + if (_index >= _tokens.Count) + { + return _tokens[^1]; + } + + return _tokens[_index++]; + } + + private ShellToken Peek() + { + if (_index >= _tokens.Count) + { + return _tokens[^1]; + } + + return _tokens[_index]; + } + + private ShellToken Previous() + { + if (_index == 0) + { + return _tokens[0]; + } + + return _tokens[_index - 1]; + } + + private static ShellSpan CreateSpan(ShellToken start, ShellToken end) + { + return new ShellSpan(start.Line, start.Column, end.Line, end.Column + end.Value.Length); + } + + private static ShellSpan CreateSpan(ShellToken start, ShellSpan end) + { + return new ShellSpan(start.Line, start.Column, end.EndLine, end.EndColumn); + } + + private static string JoinTokens(IEnumerable tokens) + { + var builder = new StringBuilder(); + var first = true; + foreach (var token in tokens) + { + if (!first) + { + builder.Append(' '); + } + + builder.Append(token.Value); + first = false; + } + + return builder.ToString(); + } + + private static string ExtractPrimaryArgument(ImmutableArray tokens) + { + if (tokens.Length <= 1) + { + return string.Empty; + } + + for (var i = 1; i < tokens.Length; i++) + { + var token = tokens[i]; + if (token.Kind is ShellTokenKind.Word or ShellTokenKind.SingleQuoted or ShellTokenKind.DoubleQuoted) + { + return token.Value; + } + } + + return string.Empty; + } +} diff --git a/src/StellaOps.Scanner.EntryTrace/Parsing/ShellToken.cs b/src/StellaOps.Scanner.EntryTrace/Parsing/ShellToken.cs new file mode 100644 index 00000000..71a5e52a --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/Parsing/ShellToken.cs @@ -0,0 +1,16 @@ +namespace StellaOps.Scanner.EntryTrace.Parsing; + +/// +/// Token produced by the shell lexer. +/// +public readonly record struct ShellToken(ShellTokenKind Kind, string Value, int Line, int Column); + +public enum ShellTokenKind +{ + Word, + SingleQuoted, + DoubleQuoted, + Operator, + NewLine, + EndOfFile +} diff --git a/src/StellaOps.Scanner.EntryTrace/Parsing/ShellTokenizer.cs b/src/StellaOps.Scanner.EntryTrace/Parsing/ShellTokenizer.cs new file mode 100644 index 00000000..2b3b06d0 --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/Parsing/ShellTokenizer.cs @@ -0,0 +1,200 @@ +using System.Globalization; +using System.Text; + +namespace StellaOps.Scanner.EntryTrace.Parsing; + +/// +/// Lightweight Bourne shell tokenizer sufficient for ENTRYPOINT scripts. +/// Deterministic: emits tokens in source order without normalization. +/// +public sealed class ShellTokenizer +{ + public IReadOnlyList Tokenize(string source) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + var tokens = new List(); + var line = 1; + var column = 1; + var index = 0; + + while (index < source.Length) + { + var ch = source[index]; + + if (ch == '\r') + { + index++; + continue; + } + + if (ch == '\n') + { + tokens.Add(new ShellToken(ShellTokenKind.NewLine, "\n", line, column)); + index++; + line++; + column = 1; + continue; + } + + if (char.IsWhiteSpace(ch)) + { + index++; + column++; + continue; + } + + if (ch == '#') + { + // Comment: skip until newline. + while (index < source.Length && source[index] != '\n') + { + index++; + } + continue; + } + + if (IsOperatorStart(ch)) + { + var opStartColumn = column; + var op = ConsumeOperator(source, ref index, ref column); + tokens.Add(new ShellToken(ShellTokenKind.Operator, op, line, opStartColumn)); + continue; + } + + if (ch == '\'') + { + var (value, length) = ConsumeSingleQuoted(source, index + 1); + tokens.Add(new ShellToken(ShellTokenKind.SingleQuoted, value, line, column)); + index += length + 2; + column += length + 2; + continue; + } + + if (ch == '"') + { + var (value, length) = ConsumeDoubleQuoted(source, index + 1); + tokens.Add(new ShellToken(ShellTokenKind.DoubleQuoted, value, line, column)); + index += length + 2; + column += length + 2; + continue; + } + + var (word, consumed) = ConsumeWord(source, index); + tokens.Add(new ShellToken(ShellTokenKind.Word, word, line, column)); + index += consumed; + column += consumed; + } + + tokens.Add(new ShellToken(ShellTokenKind.EndOfFile, string.Empty, line, column)); + return tokens; + } + + private static bool IsOperatorStart(char ch) => ch switch + { + ';' or '&' or '|' or '(' or ')' => true, + _ => false + }; + + private static string ConsumeOperator(string source, ref int index, ref int column) + { + var start = index; + var ch = source[index]; + index++; + column++; + + if (index < source.Length) + { + var next = source[index]; + if ((ch == '&' && next == '&') || + (ch == '|' && next == '|') || + (ch == ';' && next == ';')) + { + index++; + column++; + } + } + + return source[start..index]; + } + + private static (string Value, int Length) ConsumeSingleQuoted(string source, int startIndex) + { + var end = startIndex; + while (end < source.Length && source[end] != '\'') + { + end++; + } + + if (end >= source.Length) + { + throw new FormatException("Unterminated single-quoted string in entrypoint script."); + } + + return (source[startIndex..end], end - startIndex); + } + + private static (string Value, int Length) ConsumeDoubleQuoted(string source, int startIndex) + { + var builder = new StringBuilder(); + var index = startIndex; + + while (index < source.Length) + { + var ch = source[index]; + if (ch == '"') + { + return (builder.ToString(), index - startIndex); + } + + if (ch == '\\' && index + 1 < source.Length) + { + var next = source[index + 1]; + if (next is '"' or '\\' or '$' or '`' or '\n') + { + builder.Append(next); + index += 2; + continue; + } + } + + builder.Append(ch); + index++; + } + + throw new FormatException("Unterminated double-quoted string in entrypoint script."); + } + + private static (string Value, int Length) ConsumeWord(string source, int startIndex) + { + var index = startIndex; + while (index < source.Length) + { + var ch = source[index]; + if (char.IsWhiteSpace(ch) || ch == '\n' || ch == '\r' || IsOperatorStart(ch) || ch == '#' ) + { + break; + } + + if (ch == '\\' && index + 1 < source.Length && source[index + 1] == '\n') + { + // Line continuation. + index += 2; + continue; + } + + index++; + } + + if (index == startIndex) + { + throw new InvalidOperationException("Tokenizer failed to advance while consuming word."); + } + + var text = source[startIndex..index]; + return (text, index - startIndex); + } +} diff --git a/src/StellaOps.Scanner.EntryTrace/ServiceCollectionExtensions.cs b/src/StellaOps.Scanner.EntryTrace/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..793de140 --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/ServiceCollectionExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using StellaOps.Scanner.EntryTrace.Diagnostics; + +namespace StellaOps.Scanner.EntryTrace; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddEntryTraceAnalyzer(this IServiceCollection services, Action? configure = null) + { + if (services is null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddOptions() + .BindConfiguration(EntryTraceAnalyzerOptions.SectionName); + + if (configure is not null) + { + services.Configure(configure); + } + + services.TryAddSingleton(); + services.TryAddSingleton(); + return services; + } +} diff --git a/src/StellaOps.Scanner.EntryTrace/StellaOps.Scanner.EntryTrace.csproj b/src/StellaOps.Scanner.EntryTrace/StellaOps.Scanner.EntryTrace.csproj new file mode 100644 index 00000000..1580c34d --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/StellaOps.Scanner.EntryTrace.csproj @@ -0,0 +1,18 @@ + + + net10.0 + preview + enable + enable + true + + + + + + + + + + + diff --git a/src/StellaOps.Scanner.EntryTrace/TASKS.md b/src/StellaOps.Scanner.EntryTrace/TASKS.md new file mode 100644 index 00000000..3a8b94af --- /dev/null +++ b/src/StellaOps.Scanner.EntryTrace/TASKS.md @@ -0,0 +1,11 @@ +# EntryTrace Analyzer Task Board (Sprint 10) + +| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | +|----|--------|----------|------------|-------------|---------------| +| SCANNER-ENTRYTRACE-10-401 | DONE (2025-10-19) | EntryTrace Guild | Scanner Core contracts | Implement deterministic POSIX shell AST parser covering exec/command/source/run-parts/case/if used by ENTRYPOINT scripts. | Parser emits stable AST and serialization tests prove determinism for representative fixtures; see `ShellParserTests`. | +| SCANNER-ENTRYTRACE-10-402 | DONE (2025-10-19) | EntryTrace Guild | SCANNER-ENTRYTRACE-10-401 | Resolve commands across layered rootfs, tracking evidence per hop (PATH hit, layer origin, shebang). | Resolver returns terminal program path with layer attribution for fixtures; deterministic traversal asserted in `EntryTraceAnalyzerTests.ResolveAsync_IsDeterministic`. | +| SCANNER-ENTRYTRACE-10-403 | DONE (2025-10-19) | EntryTrace Guild | SCANNER-ENTRYTRACE-10-402 | Follow interpreter wrappers (shell → Python/Node/Java launchers) to terminal target, including module/jar detection. | Interpreter tracer reports correct module/script for language launchers; tests cover Python/Node/Java wrappers. | +| SCANNER-ENTRYTRACE-10-404 | DONE (2025-10-19) | EntryTrace Guild | SCANNER-ENTRYTRACE-10-403 | Build Python entry analyzer detecting venv shebangs, module invocations, `-m` usage and record usage flag. | Python fixtures produce expected module metadata (`python-module` edge) and diagnostics for missing scripts. | +| SCANNER-ENTRYTRACE-10-405 | DONE (2025-10-19) | EntryTrace Guild | SCANNER-ENTRYTRACE-10-403 | Implement Node/Java launcher analyzer capturing script/jar targets including npm lifecycle wrappers. | Node/Java fixtures resolved with evidence chain; `RunParts` coverage ensures child scripts traced. | +| SCANNER-ENTRYTRACE-10-406 | DONE (2025-10-19) | EntryTrace Guild | SCANNER-ENTRYTRACE-10-402 | Surface explainability + diagnostics for unresolved constructs and emit metrics counters. | Diagnostics catalog enumerates unknown reasons; metrics wired via `EntryTraceMetrics`; explainability doc updated. | +| SCANNER-ENTRYTRACE-10-407 | DONE (2025-10-19) | EntryTrace Guild | SCANNER-ENTRYTRACE-10-401..406 | Package EntryTrace analyzers as restart-time plug-ins with manifest + host registration. | Plug-in manifest under `plugins/scanner/entrytrace/`; restart-only policy documented; DI extension exposes `AddEntryTraceAnalyzer`. | diff --git a/src/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Descriptor/DescriptorGoldenTests.cs b/src/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Descriptor/DescriptorGoldenTests.cs new file mode 100644 index 00000000..d0c2f8b9 --- /dev/null +++ b/src/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Descriptor/DescriptorGoldenTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; +using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor; +using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities; +using Xunit; + +namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Descriptor; + +public sealed class DescriptorGoldenTests +{ + private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + [Fact] + public async Task DescriptorMatchesBaselineFixture() + { + await using var temp = new TempDirectory(); + var sbomPath = Path.Combine(temp.Path, "sample.cdx.json"); + await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.5\"}"); + + var request = new DescriptorRequest + { + ImageDigest = "sha256:0123456789abcdef", + SbomPath = sbomPath, + SbomMediaType = "application/vnd.cyclonedx+json", + SbomFormat = "cyclonedx-json", + SbomKind = "inventory", + SbomArtifactType = "application/vnd.stellaops.sbom.layer+json", + SubjectMediaType = "application/vnd.oci.image.manifest.v1+json", + GeneratorVersion = "1.2.3", + GeneratorName = "StellaOps.Scanner.Sbomer.BuildXPlugin", + LicenseId = "lic-123", + SbomName = "sample.cdx.json", + Repository = "git.stella-ops.org/stellaops", + BuildRef = "refs/heads/main", + AttestorUri = "https://attestor.local/api/v1/provenance" + }.Validate(); + + var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero)); + var generator = new DescriptorGenerator(fakeTime); + var document = await generator.CreateAsync(request, CancellationToken.None); + var actualJson = JsonSerializer.Serialize(document, SerializerOptions); + var normalizedJson = NormalizeDescriptorJson(actualJson, Path.GetFileName(sbomPath)); + + var projectRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..")); + var fixturePath = Path.Combine(projectRoot, "Fixtures", "descriptor.baseline.json"); + var updateRequested = string.Equals(Environment.GetEnvironmentVariable("UPDATE_BUILDX_FIXTURES"), "1", StringComparison.OrdinalIgnoreCase); + + if (updateRequested) + { + Directory.CreateDirectory(Path.GetDirectoryName(fixturePath)!); + await File.WriteAllTextAsync(fixturePath, normalizedJson); + return; + } + + if (!File.Exists(fixturePath)) + { + throw new InvalidOperationException($"Baseline fixture '{fixturePath}' is missing. Set UPDATE_BUILDX_FIXTURES=1 and re-run the tests to generate it."); + } + + var baselineJson = await File.ReadAllTextAsync(fixturePath); + + using var baselineDoc = JsonDocument.Parse(baselineJson); + using var actualDoc = JsonDocument.Parse(normalizedJson); + + AssertJsonEquivalent(baselineDoc.RootElement, actualDoc.RootElement); + } + + private static string NormalizeDescriptorJson(string json, string sbomFileName) + { + var node = JsonNode.Parse(json)?.AsObject() + ?? throw new InvalidOperationException("Failed to parse descriptor JSON for normalization."); + + if (node["metadata"] is JsonObject metadata) + { + metadata["sbomPath"] = sbomFileName; + } + + return node.ToJsonString(SerializerOptions); + } + + private static void AssertJsonEquivalent(JsonElement expected, JsonElement actual) + { + if (expected.ValueKind != actual.ValueKind) + { + throw new Xunit.Sdk.XunitException($"Value kind mismatch. Expected '{expected.ValueKind}' but found '{actual.ValueKind}'."); + } + + switch (expected.ValueKind) + { + case JsonValueKind.Object: + var expectedProperties = expected.EnumerateObject().ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal); + var actualProperties = actual.EnumerateObject().ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal); + + Assert.Equal( + expectedProperties.Keys.OrderBy(static name => name).ToArray(), + actualProperties.Keys.OrderBy(static name => name).ToArray()); + + foreach (var propertyName in expectedProperties.Keys) + { + AssertJsonEquivalent(expectedProperties[propertyName], actualProperties[propertyName]); + } + + break; + case JsonValueKind.Array: + var expectedItems = expected.EnumerateArray().ToArray(); + var actualItems = actual.EnumerateArray().ToArray(); + + Assert.Equal(expectedItems.Length, actualItems.Length); + for (var i = 0; i < expectedItems.Length; i++) + { + AssertJsonEquivalent(expectedItems[i], actualItems[i]); + } + + break; + default: + Assert.Equal(expected.ToString(), actual.ToString()); + break; + } + } +} diff --git a/src/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Fixtures/descriptor.baseline.json b/src/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Fixtures/descriptor.baseline.json new file mode 100644 index 00000000..4a707a28 --- /dev/null +++ b/src/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/Fixtures/descriptor.baseline.json @@ -0,0 +1,45 @@ +{ + "schema": "stellaops.buildx.descriptor.v1", + "generatedAt": "2025-10-18T12:00:00\u002B00:00", + "generator": { + "name": "StellaOps.Scanner.Sbomer.BuildXPlugin", + "version": "1.2.3" + }, + "subject": { + "mediaType": "application/vnd.oci.image.manifest.v1\u002Bjson", + "digest": "sha256:0123456789abcdef" + }, + "artifact": { + "mediaType": "application/vnd.cyclonedx\u002Bjson", + "digest": "sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c", + "size": 45, + "annotations": { + "org.opencontainers.artifact.type": "application/vnd.stellaops.sbom.layer\u002Bjson", + "org.stellaops.scanner.version": "1.2.3", + "org.stellaops.sbom.kind": "inventory", + "org.stellaops.sbom.format": "cyclonedx-json", + "org.stellaops.provenance.status": "pending", + "org.stellaops.provenance.dsse.sha256": "sha256:1b364a6b888d580feb8565f7b6195b24535ca8201b4bcac58da063b32c47220d", + "org.stellaops.provenance.nonce": "a608acf859cd58a8389816b8d9eb2a07", + "org.stellaops.license.id": "lic-123", + "org.opencontainers.image.title": "sample.cdx.json", + "org.stellaops.repository": "git.stella-ops.org/stellaops" + } + }, + "provenance": { + "status": "pending", + "expectedDsseSha256": "sha256:1b364a6b888d580feb8565f7b6195b24535ca8201b4bcac58da063b32c47220d", + "nonce": "a608acf859cd58a8389816b8d9eb2a07", + "attestorUri": "https://attestor.local/api/v1/provenance", + "predicateType": "https://slsa.dev/provenance/v1" + }, + "metadata": { + "sbomDigest": "sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c", + "sbomPath": "sample.cdx.json", + "sbomMediaType": "application/vnd.cyclonedx\u002Bjson", + "subjectMediaType": "application/vnd.oci.image.manifest.v1\u002Bjson", + "repository": "git.stella-ops.org/stellaops", + "buildRef": "refs/heads/main", + "attestorUri": "https://attestor.local/api/v1/provenance" + } +} \ No newline at end of file diff --git a/src/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj b/src/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj index c250c620..55c965cb 100644 --- a/src/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj +++ b/src/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests/StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.csproj @@ -1,11 +1,17 @@ - - - net10.0 - enable - enable - - + + + net10.0 + enable + enable + + + + + + PreserveNewest + + diff --git a/src/StellaOps.Scanner.Sbomer.BuildXPlugin/Descriptor/DescriptorGenerator.cs b/src/StellaOps.Scanner.Sbomer.BuildXPlugin/Descriptor/DescriptorGenerator.cs index de88c7be..d0d4abd7 100644 --- a/src/StellaOps.Scanner.Sbomer.BuildXPlugin/Descriptor/DescriptorGenerator.cs +++ b/src/StellaOps.Scanner.Sbomer.BuildXPlugin/Descriptor/DescriptorGenerator.cs @@ -1,180 +1,209 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor; - -/// -/// Builds immutable OCI descriptors enriched with provenance placeholders. -/// -public sealed class DescriptorGenerator -{ - public const string Schema = "stellaops.buildx.descriptor.v1"; - - private readonly TimeProvider timeProvider; - - public DescriptorGenerator(TimeProvider timeProvider) - { - timeProvider ??= TimeProvider.System; - this.timeProvider = timeProvider; - } - - public async Task CreateAsync(DescriptorRequest request, CancellationToken cancellationToken) - { - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } - - if (string.IsNullOrWhiteSpace(request.ImageDigest)) - { - throw new BuildxPluginException("Image digest must be provided."); - } - - if (string.IsNullOrWhiteSpace(request.SbomPath)) - { - throw new BuildxPluginException("SBOM path must be provided."); - } - - var sbomFile = new FileInfo(request.SbomPath); - if (!sbomFile.Exists) - { - throw new BuildxPluginException($"SBOM file '{request.SbomPath}' was not found."); - } - +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor; + +/// +/// Builds immutable OCI descriptors enriched with provenance placeholders. +/// +public sealed class DescriptorGenerator +{ + public const string Schema = "stellaops.buildx.descriptor.v1"; + + private readonly TimeProvider timeProvider; + + public DescriptorGenerator(TimeProvider timeProvider) + { + timeProvider ??= TimeProvider.System; + this.timeProvider = timeProvider; + } + + public async Task CreateAsync(DescriptorRequest request, CancellationToken cancellationToken) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (string.IsNullOrWhiteSpace(request.ImageDigest)) + { + throw new BuildxPluginException("Image digest must be provided."); + } + + if (string.IsNullOrWhiteSpace(request.SbomPath)) + { + throw new BuildxPluginException("SBOM path must be provided."); + } + + var sbomFile = new FileInfo(request.SbomPath); + if (!sbomFile.Exists) + { + throw new BuildxPluginException($"SBOM file '{request.SbomPath}' was not found."); + } + var sbomDigest = await ComputeFileDigestAsync(sbomFile, cancellationToken).ConfigureAwait(false); - - var nonce = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + var nonce = ComputeDeterministicNonce(request, sbomFile, sbomDigest); var expectedDsseSha = ComputeExpectedDsseDigest(request.ImageDigest, sbomDigest, nonce); var artifactAnnotations = BuildArtifactAnnotations(request, nonce, expectedDsseSha); - - var subject = new DescriptorSubject( - MediaType: request.SubjectMediaType, - Digest: request.ImageDigest); - - var artifact = new DescriptorArtifact( - MediaType: request.SbomMediaType, - Digest: sbomDigest, - Size: sbomFile.Length, - Annotations: artifactAnnotations); - - var provenance = new DescriptorProvenance( - Status: "pending", - ExpectedDsseSha256: expectedDsseSha, - Nonce: nonce, - AttestorUri: request.AttestorUri, - PredicateType: request.PredicateType); - - var generatorMetadata = new DescriptorGeneratorMetadata( - Name: request.GeneratorName ?? "StellaOps.Scanner.Sbomer.BuildXPlugin", - Version: request.GeneratorVersion); - - var metadata = BuildDocumentMetadata(request, sbomFile, sbomDigest); - + + var subject = new DescriptorSubject( + MediaType: request.SubjectMediaType, + Digest: request.ImageDigest); + + var artifact = new DescriptorArtifact( + MediaType: request.SbomMediaType, + Digest: sbomDigest, + Size: sbomFile.Length, + Annotations: artifactAnnotations); + + var provenance = new DescriptorProvenance( + Status: "pending", + ExpectedDsseSha256: expectedDsseSha, + Nonce: nonce, + AttestorUri: request.AttestorUri, + PredicateType: request.PredicateType); + + var generatorMetadata = new DescriptorGeneratorMetadata( + Name: request.GeneratorName ?? "StellaOps.Scanner.Sbomer.BuildXPlugin", + Version: request.GeneratorVersion); + + var metadata = BuildDocumentMetadata(request, sbomFile, sbomDigest); + return new DescriptorDocument( Schema: Schema, GeneratedAt: timeProvider.GetUtcNow(), - Generator: generatorMetadata, - Subject: subject, - Artifact: artifact, - Provenance: provenance, + Generator: generatorMetadata, + Subject: subject, + Artifact: artifact, + Provenance: provenance, Metadata: metadata); } - private static async Task ComputeFileDigestAsync(FileInfo file, CancellationToken cancellationToken) + private static string ComputeDeterministicNonce(DescriptorRequest request, FileInfo sbomFile, string sbomDigest) { - await using var stream = new FileStream( - file.FullName, - FileMode.Open, - FileAccess.Read, - FileShare.Read, - bufferSize: 128 * 1024, - FileOptions.Asynchronous | FileOptions.SequentialScan); + var builder = new StringBuilder(); + builder.AppendLine("stellaops.buildx.nonce.v1"); + builder.AppendLine(request.ImageDigest); + builder.AppendLine(sbomDigest); + builder.AppendLine(sbomFile.Length.ToString(CultureInfo.InvariantCulture)); + builder.AppendLine(request.SbomMediaType); + builder.AppendLine(request.SbomFormat); + builder.AppendLine(request.SbomKind); + builder.AppendLine(request.SbomArtifactType); + builder.AppendLine(request.SubjectMediaType); + builder.AppendLine(request.GeneratorVersion); + builder.AppendLine(request.GeneratorName ?? string.Empty); + builder.AppendLine(request.LicenseId ?? string.Empty); + builder.AppendLine(request.SbomName ?? string.Empty); + builder.AppendLine(request.Repository ?? string.Empty); + builder.AppendLine(request.BuildRef ?? string.Empty); + builder.AppendLine(request.AttestorUri ?? string.Empty); + builder.AppendLine(request.PredicateType); - using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); - - var buffer = new byte[128 * 1024]; - int bytesRead; - while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0) - { - hash.AppendData(buffer, 0, bytesRead); - } - - var digest = hash.GetHashAndReset(); - return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}"; - } - - private static string ComputeExpectedDsseDigest(string imageDigest, string sbomDigest, string nonce) - { - var payload = $"{imageDigest}\n{sbomDigest}\n{nonce}"; - var bytes = System.Text.Encoding.UTF8.GetBytes(payload); + var payload = Encoding.UTF8.GetBytes(builder.ToString()); Span hash = stackalloc byte[32]; - SHA256.HashData(bytes, hash); - return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}"; + SHA256.HashData(payload, hash); + + Span nonceBytes = stackalloc byte[16]; + hash[..16].CopyTo(nonceBytes); + return Convert.ToHexString(nonceBytes).ToLowerInvariant(); } - private static IReadOnlyDictionary BuildArtifactAnnotations(DescriptorRequest request, string nonce, string expectedDsse) - { - var annotations = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - ["org.opencontainers.artifact.type"] = request.SbomArtifactType, - ["org.stellaops.scanner.version"] = request.GeneratorVersion, - ["org.stellaops.sbom.kind"] = request.SbomKind, - ["org.stellaops.sbom.format"] = request.SbomFormat, - ["org.stellaops.provenance.status"] = "pending", - ["org.stellaops.provenance.dsse.sha256"] = expectedDsse, - ["org.stellaops.provenance.nonce"] = nonce - }; - - if (!string.IsNullOrWhiteSpace(request.LicenseId)) - { - annotations["org.stellaops.license.id"] = request.LicenseId!; - } - - if (!string.IsNullOrWhiteSpace(request.SbomName)) - { - annotations["org.opencontainers.image.title"] = request.SbomName!; - } - - if (!string.IsNullOrWhiteSpace(request.Repository)) - { - annotations["org.stellaops.repository"] = request.Repository!; - } - - return annotations; - } - - private static IReadOnlyDictionary BuildDocumentMetadata(DescriptorRequest request, FileInfo fileInfo, string sbomDigest) - { - var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - ["sbomDigest"] = sbomDigest, - ["sbomPath"] = fileInfo.FullName, - ["sbomMediaType"] = request.SbomMediaType, - ["subjectMediaType"] = request.SubjectMediaType - }; - - if (!string.IsNullOrWhiteSpace(request.Repository)) - { - metadata["repository"] = request.Repository!; - } - - if (!string.IsNullOrWhiteSpace(request.BuildRef)) - { - metadata["buildRef"] = request.BuildRef!; - } - - if (!string.IsNullOrWhiteSpace(request.AttestorUri)) - { - metadata["attestorUri"] = request.AttestorUri!; - } - - return metadata; - } -} + private static async Task ComputeFileDigestAsync(FileInfo file, CancellationToken cancellationToken) + { + await using var stream = new FileStream( + file.FullName, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + bufferSize: 128 * 1024, + FileOptions.Asynchronous | FileOptions.SequentialScan); + + using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); + + var buffer = new byte[128 * 1024]; + int bytesRead; + while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0) + { + hash.AppendData(buffer, 0, bytesRead); + } + + var digest = hash.GetHashAndReset(); + return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}"; + } + + private static string ComputeExpectedDsseDigest(string imageDigest, string sbomDigest, string nonce) + { + var payload = $"{imageDigest}\n{sbomDigest}\n{nonce}"; + var bytes = System.Text.Encoding.UTF8.GetBytes(payload); + Span hash = stackalloc byte[32]; + SHA256.HashData(bytes, hash); + return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}"; + } + + private static IReadOnlyDictionary BuildArtifactAnnotations(DescriptorRequest request, string nonce, string expectedDsse) + { + var annotations = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["org.opencontainers.artifact.type"] = request.SbomArtifactType, + ["org.stellaops.scanner.version"] = request.GeneratorVersion, + ["org.stellaops.sbom.kind"] = request.SbomKind, + ["org.stellaops.sbom.format"] = request.SbomFormat, + ["org.stellaops.provenance.status"] = "pending", + ["org.stellaops.provenance.dsse.sha256"] = expectedDsse, + ["org.stellaops.provenance.nonce"] = nonce + }; + + if (!string.IsNullOrWhiteSpace(request.LicenseId)) + { + annotations["org.stellaops.license.id"] = request.LicenseId!; + } + + if (!string.IsNullOrWhiteSpace(request.SbomName)) + { + annotations["org.opencontainers.image.title"] = request.SbomName!; + } + + if (!string.IsNullOrWhiteSpace(request.Repository)) + { + annotations["org.stellaops.repository"] = request.Repository!; + } + + return annotations; + } + + private static IReadOnlyDictionary BuildDocumentMetadata(DescriptorRequest request, FileInfo fileInfo, string sbomDigest) + { + var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["sbomDigest"] = sbomDigest, + ["sbomPath"] = fileInfo.FullName, + ["sbomMediaType"] = request.SbomMediaType, + ["subjectMediaType"] = request.SubjectMediaType + }; + + if (!string.IsNullOrWhiteSpace(request.Repository)) + { + metadata["repository"] = request.Repository!; + } + + if (!string.IsNullOrWhiteSpace(request.BuildRef)) + { + metadata["buildRef"] = request.BuildRef!; + } + + if (!string.IsNullOrWhiteSpace(request.AttestorUri)) + { + metadata["attestorUri"] = request.AttestorUri!; + } + + return metadata; + } +} diff --git a/src/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md b/src/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md index 58015509..99d42c3d 100644 --- a/src/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md +++ b/src/StellaOps.Scanner.Sbomer.BuildXPlugin/TASKS.md @@ -5,3 +5,5 @@ | SP9-BLDX-09-001 | DONE | BuildX Guild | SCANNER-EMIT-10-601 (awareness) | Scaffold buildx driver, manifest, local CAS handshake; ensure plugin loads from `plugins/scanner/buildx/`. | Plugin manifest + loader tests; local CAS writes succeed; restart required to activate. | | SP9-BLDX-09-002 | DONE | BuildX Guild | SP9-BLDX-09-001 | Emit OCI annotations + provenance metadata for Attestor handoff (image + SBOM). | OCI descriptors include DSSE/provenance placeholders; Attestor mock accepts payload. | | SP9-BLDX-09-003 | DONE | BuildX Guild | SP9-BLDX-09-002 | CI demo pipeline: build sample image, produce SBOM, verify backend report wiring. | GitHub/CI job runs sample build within 5 s overhead; artifacts saved; documentation updated. | +| SP9-BLDX-09-004 | DONE (2025-10-19) | BuildX Guild | SP9-BLDX-09-002 | Stabilize descriptor nonce derivation so repeated builds emit deterministic placeholders. | Repeated descriptor runs with fixed inputs yield identical JSON; regression tests cover nonce determinism. | +| SP9-BLDX-09-005 | DONE (2025-10-19) | BuildX Guild | SP9-BLDX-09-004 | Integrate determinism check in GitHub/Gitea workflows and capture sample artifacts. | Determinism step runs in `.gitea/workflows/build-test-deploy.yml` and `samples/ci/buildx-demo`, producing matching descriptors + archived artifacts. | diff --git a/src/StellaOps.Scanner.Worker/AGENTS.md b/src/StellaOps.Scanner.Worker/AGENTS.md index f816382d..ae5d9277 100644 --- a/src/StellaOps.Scanner.Worker/AGENTS.md +++ b/src/StellaOps.Scanner.Worker/AGENTS.md @@ -1,26 +1,26 @@ -# AGENTS -## Role -Scanner.Worker engineers own the queue-driven execution host that turns scan jobs into SBOM artefacts with deterministic progress reporting. -## Scope -- Host bootstrap: configuration binding, Authority client wiring, graceful shutdown, restart-time plug-in discovery hooks. -- Job acquisition & lease renewal semantics backed by the Scanner queue abstraction. -- Analyzer orchestration skeleton: stage pipeline, cancellation awareness, deterministic progress emissions. -- Telemetry: structured logging, OpenTelemetry metrics/traces, health counters for offline diagnostics. -## Participants -- Consumes jobs from `StellaOps.Scanner.Queue`. -- Persists progress/artifacts via `StellaOps.Scanner.Storage` once those modules land. -- Emits metrics and structured logs consumed by Observability stack & WebService status endpoints. -## Interfaces & contracts -- Queue lease abstraction (`IScanJobLease`, `IScanJobSource`) with deterministic identifiers and attempt counters. -- Analyzer dispatcher contracts for OS/lang/native analyzers and emitters. -- Telemetry resource attributes shared with Scanner.WebService and Scheduler. -## In/Out of scope -In scope: worker host, concurrency orchestration, lease renewal, cancellation wiring, deterministic logging/metrics. -Out of scope: queue provider implementations, analyzer business logic, Mongo/object-store repositories. -## Observability expectations -- Meter `StellaOps.Scanner.Worker` with queue latency, stage duration, failure counters. -- Activity source `StellaOps.Scanner.Worker.Job` for per-job tracing. -- Log correlation IDs (`jobId`, `leaseId`, `scanId`) with structured payloads; avoid dumping secrets or full manifests. -## Tests -- Integration fixture `WorkerBasicScanScenario` verifying acquisition → heartbeat → analyzer stages → completion. -- Unit tests around retry/jitter calculators as they are introduced. +# AGENTS +## Role +Scanner.Worker engineers own the queue-driven execution host that turns scan jobs into SBOM artefacts with deterministic progress reporting. +## Scope +- Host bootstrap: configuration binding, Authority client wiring, graceful shutdown, restart-time plug-in discovery hooks. +- Job acquisition & lease renewal semantics backed by the Scanner queue abstraction. +- Analyzer orchestration skeleton: stage pipeline, cancellation awareness, deterministic progress emissions. +- Telemetry: structured logging, OpenTelemetry metrics/traces, health counters for offline diagnostics. +## Participants +- Consumes jobs from `StellaOps.Scanner.Queue`. +- Persists progress/artifacts via `StellaOps.Scanner.Storage` once those modules land. +- Emits metrics and structured logs consumed by Observability stack & WebService status endpoints. +## Interfaces & contracts +- Queue lease abstraction (`IScanJobLease`, `IScanJobSource`) with deterministic identifiers and attempt counters. +- Analyzer dispatcher contracts for OS/lang/native analyzers and emitters. +- Telemetry resource attributes shared with Scanner.WebService and Scheduler. +## In/Out of scope +In scope: worker host, concurrency orchestration, lease renewal, cancellation wiring, deterministic logging/metrics. +Out of scope: queue provider implementations, analyzer business logic, Mongo/object-store repositories. +## Observability expectations +- Meter `StellaOps.Scanner.Worker` with queue latency, stage duration, failure counters. +- Activity source `StellaOps.Scanner.Worker.Job` for per-job tracing. +- Log correlation IDs (`jobId`, `leaseId`, `scanId`) with structured payloads; avoid dumping secrets or full manifests. +## Tests +- Integration fixture `WorkerBasicScanScenario` verifying acquisition → heartbeat → analyzer stages → completion. +- Unit tests around retry/jitter calculators as they are introduced. diff --git a/src/StellaOps.Scanner.Worker/Diagnostics/ScannerWorkerInstrumentation.cs b/src/StellaOps.Scanner.Worker/Diagnostics/ScannerWorkerInstrumentation.cs index 81762e5b..aeecaef9 100644 --- a/src/StellaOps.Scanner.Worker/Diagnostics/ScannerWorkerInstrumentation.cs +++ b/src/StellaOps.Scanner.Worker/Diagnostics/ScannerWorkerInstrumentation.cs @@ -1,15 +1,15 @@ -using System.Diagnostics; -using System.Diagnostics.Metrics; - -namespace StellaOps.Scanner.Worker.Diagnostics; - -public static class ScannerWorkerInstrumentation -{ - public const string ActivitySourceName = "StellaOps.Scanner.Worker.Job"; - - public const string MeterName = "StellaOps.Scanner.Worker"; - - public static ActivitySource ActivitySource { get; } = new(ActivitySourceName); - - public static Meter Meter { get; } = new(MeterName, version: "1.0.0"); -} +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace StellaOps.Scanner.Worker.Diagnostics; + +public static class ScannerWorkerInstrumentation +{ + public const string ActivitySourceName = "StellaOps.Scanner.Worker.Job"; + + public const string MeterName = "StellaOps.Scanner.Worker"; + + public static ActivitySource ActivitySource { get; } = new(ActivitySourceName); + + public static Meter Meter { get; } = new(MeterName, version: "1.0.0"); +} diff --git a/src/StellaOps.Scanner.Worker/Diagnostics/ScannerWorkerMetrics.cs b/src/StellaOps.Scanner.Worker/Diagnostics/ScannerWorkerMetrics.cs index 9af0164b..ff6005f5 100644 --- a/src/StellaOps.Scanner.Worker/Diagnostics/ScannerWorkerMetrics.cs +++ b/src/StellaOps.Scanner.Worker/Diagnostics/ScannerWorkerMetrics.cs @@ -1,109 +1,109 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; -using StellaOps.Scanner.Worker.Processing; - -namespace StellaOps.Scanner.Worker.Diagnostics; - -public sealed class ScannerWorkerMetrics -{ - private readonly Histogram _queueLatencyMs; - private readonly Histogram _jobDurationMs; - private readonly Histogram _stageDurationMs; - private readonly Counter _jobsCompleted; - private readonly Counter _jobsFailed; - - public ScannerWorkerMetrics() - { - _queueLatencyMs = ScannerWorkerInstrumentation.Meter.CreateHistogram( - "scanner_worker_queue_latency_ms", - unit: "ms", - description: "Time from job enqueue to lease acquisition."); - _jobDurationMs = ScannerWorkerInstrumentation.Meter.CreateHistogram( - "scanner_worker_job_duration_ms", - unit: "ms", - description: "Total processing duration per job."); - _stageDurationMs = ScannerWorkerInstrumentation.Meter.CreateHistogram( - "scanner_worker_stage_duration_ms", - unit: "ms", - description: "Stage execution duration per job."); - _jobsCompleted = ScannerWorkerInstrumentation.Meter.CreateCounter( - "scanner_worker_jobs_completed_total", - description: "Number of successfully completed scan jobs."); - _jobsFailed = ScannerWorkerInstrumentation.Meter.CreateCounter( - "scanner_worker_jobs_failed_total", - description: "Number of scan jobs that failed permanently."); - } - - public void RecordQueueLatency(ScanJobContext context, TimeSpan latency) - { - if (latency <= TimeSpan.Zero) - { - return; - } - - _queueLatencyMs.Record(latency.TotalMilliseconds, CreateTags(context)); - } - - public void RecordJobDuration(ScanJobContext context, TimeSpan duration) - { - if (duration <= TimeSpan.Zero) - { - return; - } - - _jobDurationMs.Record(duration.TotalMilliseconds, CreateTags(context)); - } - - public void RecordStageDuration(ScanJobContext context, string stage, TimeSpan duration) - { - if (duration <= TimeSpan.Zero) - { - return; - } - - _stageDurationMs.Record(duration.TotalMilliseconds, CreateTags(context, stage: stage)); - } - - public void IncrementJobCompleted(ScanJobContext context) - { - _jobsCompleted.Add(1, CreateTags(context)); - } - - public void IncrementJobFailed(ScanJobContext context, string failureReason) - { - _jobsFailed.Add(1, CreateTags(context, failureReason: failureReason)); - } - - private static KeyValuePair[] CreateTags(ScanJobContext context, string? stage = null, string? failureReason = null) - { - var tags = new List>(stage is null ? 5 : 6) - { - new("job.id", context.JobId), - new("scan.id", context.ScanId), - new("attempt", context.Lease.Attempt), - }; - - if (context.Lease.Metadata.TryGetValue("queue", out var queueName) && !string.IsNullOrWhiteSpace(queueName)) - { - tags.Add(new KeyValuePair("queue", queueName)); - } - - if (context.Lease.Metadata.TryGetValue("job.kind", out var jobKind) && !string.IsNullOrWhiteSpace(jobKind)) - { - tags.Add(new KeyValuePair("job.kind", jobKind)); - } - - if (!string.IsNullOrWhiteSpace(stage)) - { - tags.Add(new KeyValuePair("stage", stage)); - } - - if (!string.IsNullOrWhiteSpace(failureReason)) - { - tags.Add(new KeyValuePair("reason", failureReason)); - } - - return tags.ToArray(); - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using StellaOps.Scanner.Worker.Processing; + +namespace StellaOps.Scanner.Worker.Diagnostics; + +public sealed class ScannerWorkerMetrics +{ + private readonly Histogram _queueLatencyMs; + private readonly Histogram _jobDurationMs; + private readonly Histogram _stageDurationMs; + private readonly Counter _jobsCompleted; + private readonly Counter _jobsFailed; + + public ScannerWorkerMetrics() + { + _queueLatencyMs = ScannerWorkerInstrumentation.Meter.CreateHistogram( + "scanner_worker_queue_latency_ms", + unit: "ms", + description: "Time from job enqueue to lease acquisition."); + _jobDurationMs = ScannerWorkerInstrumentation.Meter.CreateHistogram( + "scanner_worker_job_duration_ms", + unit: "ms", + description: "Total processing duration per job."); + _stageDurationMs = ScannerWorkerInstrumentation.Meter.CreateHistogram( + "scanner_worker_stage_duration_ms", + unit: "ms", + description: "Stage execution duration per job."); + _jobsCompleted = ScannerWorkerInstrumentation.Meter.CreateCounter( + "scanner_worker_jobs_completed_total", + description: "Number of successfully completed scan jobs."); + _jobsFailed = ScannerWorkerInstrumentation.Meter.CreateCounter( + "scanner_worker_jobs_failed_total", + description: "Number of scan jobs that failed permanently."); + } + + public void RecordQueueLatency(ScanJobContext context, TimeSpan latency) + { + if (latency <= TimeSpan.Zero) + { + return; + } + + _queueLatencyMs.Record(latency.TotalMilliseconds, CreateTags(context)); + } + + public void RecordJobDuration(ScanJobContext context, TimeSpan duration) + { + if (duration <= TimeSpan.Zero) + { + return; + } + + _jobDurationMs.Record(duration.TotalMilliseconds, CreateTags(context)); + } + + public void RecordStageDuration(ScanJobContext context, string stage, TimeSpan duration) + { + if (duration <= TimeSpan.Zero) + { + return; + } + + _stageDurationMs.Record(duration.TotalMilliseconds, CreateTags(context, stage: stage)); + } + + public void IncrementJobCompleted(ScanJobContext context) + { + _jobsCompleted.Add(1, CreateTags(context)); + } + + public void IncrementJobFailed(ScanJobContext context, string failureReason) + { + _jobsFailed.Add(1, CreateTags(context, failureReason: failureReason)); + } + + private static KeyValuePair[] CreateTags(ScanJobContext context, string? stage = null, string? failureReason = null) + { + var tags = new List>(stage is null ? 5 : 6) + { + new("job.id", context.JobId), + new("scan.id", context.ScanId), + new("attempt", context.Lease.Attempt), + }; + + if (context.Lease.Metadata.TryGetValue("queue", out var queueName) && !string.IsNullOrWhiteSpace(queueName)) + { + tags.Add(new KeyValuePair("queue", queueName)); + } + + if (context.Lease.Metadata.TryGetValue("job.kind", out var jobKind) && !string.IsNullOrWhiteSpace(jobKind)) + { + tags.Add(new KeyValuePair("job.kind", jobKind)); + } + + if (!string.IsNullOrWhiteSpace(stage)) + { + tags.Add(new KeyValuePair("stage", stage)); + } + + if (!string.IsNullOrWhiteSpace(failureReason)) + { + tags.Add(new KeyValuePair("reason", failureReason)); + } + + return tags.ToArray(); + } +} diff --git a/src/StellaOps.Scanner.Worker/Diagnostics/TelemetryExtensions.cs b/src/StellaOps.Scanner.Worker/Diagnostics/TelemetryExtensions.cs index e1db1b89..58bc5027 100644 --- a/src/StellaOps.Scanner.Worker/Diagnostics/TelemetryExtensions.cs +++ b/src/StellaOps.Scanner.Worker/Diagnostics/TelemetryExtensions.cs @@ -1,102 +1,102 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; -using StellaOps.Scanner.Worker.Options; - -namespace StellaOps.Scanner.Worker.Diagnostics; - -public static class TelemetryExtensions -{ - public static void ConfigureScannerWorkerTelemetry(this IHostApplicationBuilder builder, ScannerWorkerOptions options) - { - ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(options); - - var telemetry = options.Telemetry; - if (!telemetry.EnableTelemetry) - { - return; - } - - var openTelemetry = builder.Services.AddOpenTelemetry(); - - openTelemetry.ConfigureResource(resource => - { - var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; - resource.AddService(telemetry.ServiceName, serviceVersion: version, serviceInstanceId: Environment.MachineName); - resource.AddAttributes(new[] - { - new KeyValuePair("deployment.environment", builder.Environment.EnvironmentName), - }); - - foreach (var kvp in telemetry.ResourceAttributes) - { - if (string.IsNullOrWhiteSpace(kvp.Key) || kvp.Value is null) - { - continue; - } - - resource.AddAttributes(new[] { new KeyValuePair(kvp.Key, kvp.Value) }); - } - }); - - if (telemetry.EnableTracing) - { - openTelemetry.WithTracing(tracing => - { - tracing.AddSource(ScannerWorkerInstrumentation.ActivitySourceName); - ConfigureExporter(tracing, telemetry); - }); - } - - if (telemetry.EnableMetrics) - { - openTelemetry.WithMetrics(metrics => - { - metrics - .AddMeter(ScannerWorkerInstrumentation.MeterName) - .AddRuntimeInstrumentation() - .AddProcessInstrumentation(); - - ConfigureExporter(metrics, telemetry); - }); - } - } - - private static void ConfigureExporter(TracerProviderBuilder tracing, ScannerWorkerOptions.TelemetryOptions telemetry) - { - if (!string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint)) - { - tracing.AddOtlpExporter(options => - { - options.Endpoint = new Uri(telemetry.OtlpEndpoint); - }); - } - - if (telemetry.ExportConsole || string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint)) - { - tracing.AddConsoleExporter(); - } - } - - private static void ConfigureExporter(MeterProviderBuilder metrics, ScannerWorkerOptions.TelemetryOptions telemetry) - { - if (!string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint)) - { - metrics.AddOtlpExporter(options => - { - options.Endpoint = new Uri(telemetry.OtlpEndpoint); - }); - } - - if (telemetry.ExportConsole || string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint)) - { - metrics.AddConsoleExporter(); - } - } -} +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using StellaOps.Scanner.Worker.Options; + +namespace StellaOps.Scanner.Worker.Diagnostics; + +public static class TelemetryExtensions +{ + public static void ConfigureScannerWorkerTelemetry(this IHostApplicationBuilder builder, ScannerWorkerOptions options) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(options); + + var telemetry = options.Telemetry; + if (!telemetry.EnableTelemetry) + { + return; + } + + var openTelemetry = builder.Services.AddOpenTelemetry(); + + openTelemetry.ConfigureResource(resource => + { + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; + resource.AddService(telemetry.ServiceName, serviceVersion: version, serviceInstanceId: Environment.MachineName); + resource.AddAttributes(new[] + { + new KeyValuePair("deployment.environment", builder.Environment.EnvironmentName), + }); + + foreach (var kvp in telemetry.ResourceAttributes) + { + if (string.IsNullOrWhiteSpace(kvp.Key) || kvp.Value is null) + { + continue; + } + + resource.AddAttributes(new[] { new KeyValuePair(kvp.Key, kvp.Value) }); + } + }); + + if (telemetry.EnableTracing) + { + openTelemetry.WithTracing(tracing => + { + tracing.AddSource(ScannerWorkerInstrumentation.ActivitySourceName); + ConfigureExporter(tracing, telemetry); + }); + } + + if (telemetry.EnableMetrics) + { + openTelemetry.WithMetrics(metrics => + { + metrics + .AddMeter(ScannerWorkerInstrumentation.MeterName) + .AddRuntimeInstrumentation() + .AddProcessInstrumentation(); + + ConfigureExporter(metrics, telemetry); + }); + } + } + + private static void ConfigureExporter(TracerProviderBuilder tracing, ScannerWorkerOptions.TelemetryOptions telemetry) + { + if (!string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint)) + { + tracing.AddOtlpExporter(options => + { + options.Endpoint = new Uri(telemetry.OtlpEndpoint); + }); + } + + if (telemetry.ExportConsole || string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint)) + { + tracing.AddConsoleExporter(); + } + } + + private static void ConfigureExporter(MeterProviderBuilder metrics, ScannerWorkerOptions.TelemetryOptions telemetry) + { + if (!string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint)) + { + metrics.AddOtlpExporter(options => + { + options.Endpoint = new Uri(telemetry.OtlpEndpoint); + }); + } + + if (telemetry.ExportConsole || string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint)) + { + metrics.AddConsoleExporter(); + } + } +} diff --git a/src/StellaOps.Scanner.Worker/Hosting/ScannerWorkerHostedService.cs b/src/StellaOps.Scanner.Worker/Hosting/ScannerWorkerHostedService.cs index 2f22294a..5b2bdcf9 100644 --- a/src/StellaOps.Scanner.Worker/Hosting/ScannerWorkerHostedService.cs +++ b/src/StellaOps.Scanner.Worker/Hosting/ScannerWorkerHostedService.cs @@ -1,201 +1,202 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using StellaOps.Scanner.Worker.Diagnostics; -using StellaOps.Scanner.Worker.Options; -using StellaOps.Scanner.Worker.Processing; - -namespace StellaOps.Scanner.Worker.Hosting; - -public sealed partial class ScannerWorkerHostedService : BackgroundService -{ - private readonly IScanJobSource _jobSource; - private readonly ScanJobProcessor _processor; - private readonly LeaseHeartbeatService _heartbeatService; - private readonly ScannerWorkerMetrics _metrics; - private readonly TimeProvider _timeProvider; - private readonly IOptionsMonitor _options; - private readonly ILogger _logger; - private readonly IDelayScheduler _delayScheduler; - - public ScannerWorkerHostedService( - IScanJobSource jobSource, - ScanJobProcessor processor, - LeaseHeartbeatService heartbeatService, - ScannerWorkerMetrics metrics, - TimeProvider timeProvider, - IDelayScheduler delayScheduler, - IOptionsMonitor options, - ILogger logger) - { - _jobSource = jobSource ?? throw new ArgumentNullException(nameof(jobSource)); - _processor = processor ?? throw new ArgumentNullException(nameof(processor)); - _heartbeatService = heartbeatService ?? throw new ArgumentNullException(nameof(heartbeatService)); - _metrics = metrics ?? throw new ArgumentNullException(nameof(metrics)); - _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); - _delayScheduler = delayScheduler ?? throw new ArgumentNullException(nameof(delayScheduler)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - var runningJobs = new HashSet(); - var delayStrategy = new PollDelayStrategy(_options.CurrentValue.Polling); - - WorkerStarted(_logger); - - while (!stoppingToken.IsCancellationRequested) - { - runningJobs.RemoveWhere(static task => task.IsCompleted); - - var options = _options.CurrentValue; - if (runningJobs.Count >= options.MaxConcurrentJobs) - { - var completed = await Task.WhenAny(runningJobs).ConfigureAwait(false); - runningJobs.Remove(completed); - continue; - } - - IScanJobLease? lease = null; - try - { - lease = await _jobSource.TryAcquireAsync(stoppingToken).ConfigureAwait(false); - } - catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) - { - break; - } - catch (Exception ex) - { - _logger.LogError(ex, "Scanner worker failed to acquire job lease; backing off."); - } - - if (lease is null) - { - var delay = delayStrategy.NextDelay(); - await _delayScheduler.DelayAsync(delay, stoppingToken).ConfigureAwait(false); - continue; - } - - delayStrategy.Reset(); - runningJobs.Add(RunJobAsync(lease, stoppingToken)); - } - - if (runningJobs.Count > 0) - { - await Task.WhenAll(runningJobs).ConfigureAwait(false); - } - - WorkerStopping(_logger); - } - - private async Task RunJobAsync(IScanJobLease lease, CancellationToken stoppingToken) - { - var options = _options.CurrentValue; - var jobStart = _timeProvider.GetUtcNow(); - var queueLatency = jobStart - lease.EnqueuedAtUtc; - var jobCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); - var jobToken = jobCts.Token; - var context = new ScanJobContext(lease, _timeProvider, jobStart, jobToken); - +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StellaOps.Scanner.Worker.Diagnostics; +using StellaOps.Scanner.Worker.Options; +using StellaOps.Scanner.Worker.Processing; + +namespace StellaOps.Scanner.Worker.Hosting; + +public sealed partial class ScannerWorkerHostedService : BackgroundService +{ + private readonly IScanJobSource _jobSource; + private readonly ScanJobProcessor _processor; + private readonly LeaseHeartbeatService _heartbeatService; + private readonly ScannerWorkerMetrics _metrics; + private readonly TimeProvider _timeProvider; + private readonly IOptionsMonitor _options; + private readonly ILogger _logger; + private readonly IDelayScheduler _delayScheduler; + + public ScannerWorkerHostedService( + IScanJobSource jobSource, + ScanJobProcessor processor, + LeaseHeartbeatService heartbeatService, + ScannerWorkerMetrics metrics, + TimeProvider timeProvider, + IDelayScheduler delayScheduler, + IOptionsMonitor options, + ILogger logger) + { + _jobSource = jobSource ?? throw new ArgumentNullException(nameof(jobSource)); + _processor = processor ?? throw new ArgumentNullException(nameof(processor)); + _heartbeatService = heartbeatService ?? throw new ArgumentNullException(nameof(heartbeatService)); + _metrics = metrics ?? throw new ArgumentNullException(nameof(metrics)); + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _delayScheduler = delayScheduler ?? throw new ArgumentNullException(nameof(delayScheduler)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var runningJobs = new HashSet(); + var delayStrategy = new PollDelayStrategy(_options.CurrentValue.Polling); + + WorkerStarted(_logger); + + while (!stoppingToken.IsCancellationRequested) + { + runningJobs.RemoveWhere(static task => task.IsCompleted); + + var options = _options.CurrentValue; + if (runningJobs.Count >= options.MaxConcurrentJobs) + { + var completed = await Task.WhenAny(runningJobs).ConfigureAwait(false); + runningJobs.Remove(completed); + continue; + } + + IScanJobLease? lease = null; + try + { + lease = await _jobSource.TryAcquireAsync(stoppingToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Scanner worker failed to acquire job lease; backing off."); + } + + if (lease is null) + { + var delay = delayStrategy.NextDelay(); + await _delayScheduler.DelayAsync(delay, stoppingToken).ConfigureAwait(false); + continue; + } + + delayStrategy.Reset(); + runningJobs.Add(RunJobAsync(lease, stoppingToken)); + } + + if (runningJobs.Count > 0) + { + await Task.WhenAll(runningJobs).ConfigureAwait(false); + } + + WorkerStopping(_logger); + } + + private async Task RunJobAsync(IScanJobLease lease, CancellationToken stoppingToken) + { + var options = _options.CurrentValue; + var jobStart = _timeProvider.GetUtcNow(); + var queueLatency = jobStart - lease.EnqueuedAtUtc; + var jobCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); + var jobToken = jobCts.Token; + var context = new ScanJobContext(lease, _timeProvider, jobStart, jobToken); + _metrics.RecordQueueLatency(context, queueLatency); JobAcquired(_logger, lease.JobId, lease.ScanId, lease.Attempt, queueLatency.TotalMilliseconds); + var processingTask = _processor.ExecuteAsync(context, jobToken).AsTask(); var heartbeatTask = _heartbeatService.RunAsync(lease, jobToken); Exception? processingException = null; try { - await _processor.ExecuteAsync(context, jobToken).ConfigureAwait(false); + await processingTask.ConfigureAwait(false); jobCts.Cancel(); await heartbeatTask.ConfigureAwait(false); await lease.CompleteAsync(stoppingToken).ConfigureAwait(false); var duration = _timeProvider.GetUtcNow() - jobStart; _metrics.RecordJobDuration(context, duration); - _metrics.IncrementJobCompleted(context); - JobCompleted(_logger, lease.JobId, lease.ScanId, duration.TotalMilliseconds); - } - catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) - { - processingException = null; - await lease.AbandonAsync("host-stopping", CancellationToken.None).ConfigureAwait(false); - JobAbandoned(_logger, lease.JobId, lease.ScanId); - } - catch (Exception ex) - { - processingException = ex; - var duration = _timeProvider.GetUtcNow() - jobStart; - _metrics.RecordJobDuration(context, duration); - - var reason = ex.GetType().Name; - var maxAttempts = options.Queue.MaxAttempts; - if (lease.Attempt >= maxAttempts) - { - await lease.PoisonAsync(reason, CancellationToken.None).ConfigureAwait(false); - _metrics.IncrementJobFailed(context, reason); - JobPoisoned(_logger, lease.JobId, lease.ScanId, lease.Attempt, maxAttempts, ex); - } - else - { - await lease.AbandonAsync(reason, CancellationToken.None).ConfigureAwait(false); - JobAbandonedWithError(_logger, lease.JobId, lease.ScanId, lease.Attempt, maxAttempts, ex); - } - } - finally - { - jobCts.Cancel(); - try - { - await heartbeatTask.ConfigureAwait(false); - } - catch (Exception ex) when (processingException is null && ex is not OperationCanceledException) - { - _logger.LogWarning(ex, "Heartbeat loop ended with an exception for job {JobId}.", lease.JobId); - } - - await lease.DisposeAsync().ConfigureAwait(false); - jobCts.Dispose(); - } - } - - [LoggerMessage(EventId = 2000, Level = LogLevel.Information, Message = "Scanner worker host started.")] - private static partial void WorkerStarted(ILogger logger); - - [LoggerMessage(EventId = 2001, Level = LogLevel.Information, Message = "Scanner worker host stopping.")] - private static partial void WorkerStopping(ILogger logger); - - [LoggerMessage( - EventId = 2002, - Level = LogLevel.Information, - Message = "Leased job {JobId} (scan {ScanId}) attempt {Attempt}; queue latency {LatencyMs:F0} ms.")] - private static partial void JobAcquired(ILogger logger, string jobId, string scanId, int attempt, double latencyMs); - - [LoggerMessage( - EventId = 2003, - Level = LogLevel.Information, - Message = "Job {JobId} (scan {ScanId}) completed in {DurationMs:F0} ms.")] - private static partial void JobCompleted(ILogger logger, string jobId, string scanId, double durationMs); - - [LoggerMessage( - EventId = 2004, - Level = LogLevel.Warning, - Message = "Job {JobId} (scan {ScanId}) abandoned due to host shutdown.")] - private static partial void JobAbandoned(ILogger logger, string jobId, string scanId); - - [LoggerMessage( - EventId = 2005, - Level = LogLevel.Warning, - Message = "Job {JobId} (scan {ScanId}) attempt {Attempt}/{MaxAttempts} abandoned after failure; job will be retried.")] - private static partial void JobAbandonedWithError(ILogger logger, string jobId, string scanId, int attempt, int maxAttempts, Exception exception); - - [LoggerMessage( - EventId = 2006, - Level = LogLevel.Error, - Message = "Job {JobId} (scan {ScanId}) attempt {Attempt}/{MaxAttempts} exceeded retry budget; quarantining job.")] - private static partial void JobPoisoned(ILogger logger, string jobId, string scanId, int attempt, int maxAttempts, Exception exception); -} + _metrics.IncrementJobCompleted(context); + JobCompleted(_logger, lease.JobId, lease.ScanId, duration.TotalMilliseconds); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + processingException = null; + await lease.AbandonAsync("host-stopping", CancellationToken.None).ConfigureAwait(false); + JobAbandoned(_logger, lease.JobId, lease.ScanId); + } + catch (Exception ex) + { + processingException = ex; + var duration = _timeProvider.GetUtcNow() - jobStart; + _metrics.RecordJobDuration(context, duration); + + var reason = ex.GetType().Name; + var maxAttempts = options.Queue.MaxAttempts; + if (lease.Attempt >= maxAttempts) + { + await lease.PoisonAsync(reason, CancellationToken.None).ConfigureAwait(false); + _metrics.IncrementJobFailed(context, reason); + JobPoisoned(_logger, lease.JobId, lease.ScanId, lease.Attempt, maxAttempts, ex); + } + else + { + await lease.AbandonAsync(reason, CancellationToken.None).ConfigureAwait(false); + JobAbandonedWithError(_logger, lease.JobId, lease.ScanId, lease.Attempt, maxAttempts, ex); + } + } + finally + { + jobCts.Cancel(); + try + { + await heartbeatTask.ConfigureAwait(false); + } + catch (Exception ex) when (processingException is null && ex is not OperationCanceledException) + { + _logger.LogWarning(ex, "Heartbeat loop ended with an exception for job {JobId}.", lease.JobId); + } + + await lease.DisposeAsync().ConfigureAwait(false); + jobCts.Dispose(); + } + } + + [LoggerMessage(EventId = 2000, Level = LogLevel.Information, Message = "Scanner worker host started.")] + private static partial void WorkerStarted(ILogger logger); + + [LoggerMessage(EventId = 2001, Level = LogLevel.Information, Message = "Scanner worker host stopping.")] + private static partial void WorkerStopping(ILogger logger); + + [LoggerMessage( + EventId = 2002, + Level = LogLevel.Information, + Message = "Leased job {JobId} (scan {ScanId}) attempt {Attempt}; queue latency {LatencyMs:F0} ms.")] + private static partial void JobAcquired(ILogger logger, string jobId, string scanId, int attempt, double latencyMs); + + [LoggerMessage( + EventId = 2003, + Level = LogLevel.Information, + Message = "Job {JobId} (scan {ScanId}) completed in {DurationMs:F0} ms.")] + private static partial void JobCompleted(ILogger logger, string jobId, string scanId, double durationMs); + + [LoggerMessage( + EventId = 2004, + Level = LogLevel.Warning, + Message = "Job {JobId} (scan {ScanId}) abandoned due to host shutdown.")] + private static partial void JobAbandoned(ILogger logger, string jobId, string scanId); + + [LoggerMessage( + EventId = 2005, + Level = LogLevel.Warning, + Message = "Job {JobId} (scan {ScanId}) attempt {Attempt}/{MaxAttempts} abandoned after failure; job will be retried.")] + private static partial void JobAbandonedWithError(ILogger logger, string jobId, string scanId, int attempt, int maxAttempts, Exception exception); + + [LoggerMessage( + EventId = 2006, + Level = LogLevel.Error, + Message = "Job {JobId} (scan {ScanId}) attempt {Attempt}/{MaxAttempts} exceeded retry budget; quarantining job.")] + private static partial void JobPoisoned(ILogger logger, string jobId, string scanId, int attempt, int maxAttempts, Exception exception); +} diff --git a/src/StellaOps.Scanner.Worker/Options/ScannerWorkerOptions.cs b/src/StellaOps.Scanner.Worker/Options/ScannerWorkerOptions.cs index 7a99bc2b..464ee264 100644 --- a/src/StellaOps.Scanner.Worker/Options/ScannerWorkerOptions.cs +++ b/src/StellaOps.Scanner.Worker/Options/ScannerWorkerOptions.cs @@ -2,141 +2,162 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; - -namespace StellaOps.Scanner.Worker.Options; - -public sealed class ScannerWorkerOptions -{ - public const string SectionName = "Scanner:Worker"; - - public int MaxConcurrentJobs { get; set; } = 2; - - public QueueOptions Queue { get; } = new(); - - public PollingOptions Polling { get; } = new(); - - public AuthorityOptions Authority { get; } = new(); - +using System.IO; +using StellaOps.Scanner.Core.Contracts; + +namespace StellaOps.Scanner.Worker.Options; + +public sealed class ScannerWorkerOptions +{ + public const string SectionName = "Scanner:Worker"; + + public int MaxConcurrentJobs { get; set; } = 2; + + public QueueOptions Queue { get; } = new(); + + public PollingOptions Polling { get; } = new(); + + public AuthorityOptions Authority { get; } = new(); + public TelemetryOptions Telemetry { get; } = new(); public ShutdownOptions Shutdown { get; } = new(); - public sealed class QueueOptions - { - public int MaxAttempts { get; set; } = 5; - - public double HeartbeatSafetyFactor { get; set; } = 3.0; - - public int MaxHeartbeatJitterMilliseconds { get; set; } = 750; - - public IReadOnlyList HeartbeatRetryDelays => _heartbeatRetryDelays; - - public TimeSpan MinHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(10); - - public TimeSpan MaxHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(30); - - public void SetHeartbeatRetryDelays(IEnumerable delays) - { - _heartbeatRetryDelays = NormalizeDelays(delays); - } - - internal IReadOnlyList NormalizedHeartbeatRetryDelays => _heartbeatRetryDelays; - - private static IReadOnlyList NormalizeDelays(IEnumerable delays) - { - var buffer = new List(); - foreach (var delay in delays) - { - if (delay <= TimeSpan.Zero) - { - continue; - } - - buffer.Add(delay); - } - - buffer.Sort(); - return new ReadOnlyCollection(buffer); - } - - private IReadOnlyList _heartbeatRetryDelays = new ReadOnlyCollection(new TimeSpan[] - { - TimeSpan.FromSeconds(2), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10), - }); - } - - public sealed class PollingOptions - { - public TimeSpan InitialDelay { get; set; } = TimeSpan.FromMilliseconds(200); - - public TimeSpan MaxDelay { get; set; } = TimeSpan.FromSeconds(5); - - public double JitterRatio { get; set; } = 0.2; - } - - public sealed class AuthorityOptions - { - public bool Enabled { get; set; } - - public string? Issuer { get; set; } - - public string? ClientId { get; set; } - - public string? ClientSecret { get; set; } - - public bool RequireHttpsMetadata { get; set; } = true; - - public string? MetadataAddress { get; set; } - - public int BackchannelTimeoutSeconds { get; set; } = 20; - - public int TokenClockSkewSeconds { get; set; } = 30; - - public IList Scopes { get; } = new List { "scanner.scan" }; - - public ResilienceOptions Resilience { get; } = new(); - } - - public sealed class ResilienceOptions - { - public bool? EnableRetries { get; set; } - - public IList RetryDelays { get; } = new List - { - TimeSpan.FromMilliseconds(250), - TimeSpan.FromMilliseconds(500), - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(5), - }; - - public bool? AllowOfflineCacheFallback { get; set; } - - public TimeSpan? OfflineCacheTolerance { get; set; } - } - - public sealed class TelemetryOptions - { - public bool EnableLogging { get; set; } = true; - - public bool EnableTelemetry { get; set; } = true; - - public bool EnableTracing { get; set; } - - public bool EnableMetrics { get; set; } = true; - - public string ServiceName { get; set; } = "stellaops-scanner-worker"; - - public string? OtlpEndpoint { get; set; } - - public bool ExportConsole { get; set; } - - public IDictionary ResourceAttributes { get; } = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - } - + public AnalyzerOptions Analyzers { get; } = new(); + + public sealed class QueueOptions + { + public int MaxAttempts { get; set; } = 5; + + public double HeartbeatSafetyFactor { get; set; } = 3.0; + + public int MaxHeartbeatJitterMilliseconds { get; set; } = 750; + + public IReadOnlyList HeartbeatRetryDelays => _heartbeatRetryDelays; + + public TimeSpan MinHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(10); + + public TimeSpan MaxHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(30); + + public void SetHeartbeatRetryDelays(IEnumerable delays) + { + _heartbeatRetryDelays = NormalizeDelays(delays); + } + + internal IReadOnlyList NormalizedHeartbeatRetryDelays => _heartbeatRetryDelays; + + private static IReadOnlyList NormalizeDelays(IEnumerable delays) + { + var buffer = new List(); + foreach (var delay in delays) + { + if (delay <= TimeSpan.Zero) + { + continue; + } + + buffer.Add(delay); + } + + buffer.Sort(); + return new ReadOnlyCollection(buffer); + } + + private IReadOnlyList _heartbeatRetryDelays = new ReadOnlyCollection(new TimeSpan[] + { + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10), + }); + } + + public sealed class PollingOptions + { + public TimeSpan InitialDelay { get; set; } = TimeSpan.FromMilliseconds(200); + + public TimeSpan MaxDelay { get; set; } = TimeSpan.FromSeconds(5); + + public double JitterRatio { get; set; } = 0.2; + } + + public sealed class AuthorityOptions + { + public bool Enabled { get; set; } + + public string? Issuer { get; set; } + + public string? ClientId { get; set; } + + public string? ClientSecret { get; set; } + + public bool RequireHttpsMetadata { get; set; } = true; + + public string? MetadataAddress { get; set; } + + public int BackchannelTimeoutSeconds { get; set; } = 20; + + public int TokenClockSkewSeconds { get; set; } = 30; + + public IList Scopes { get; } = new List { "scanner.scan" }; + + public ResilienceOptions Resilience { get; } = new(); + } + + public sealed class ResilienceOptions + { + public bool? EnableRetries { get; set; } + + public IList RetryDelays { get; } = new List + { + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMilliseconds(500), + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(5), + }; + + public bool? AllowOfflineCacheFallback { get; set; } + + public TimeSpan? OfflineCacheTolerance { get; set; } + } + + public sealed class TelemetryOptions + { + public bool EnableLogging { get; set; } = true; + + public bool EnableTelemetry { get; set; } = true; + + public bool EnableTracing { get; set; } + + public bool EnableMetrics { get; set; } = true; + + public string ServiceName { get; set; } = "stellaops-scanner-worker"; + + public string? OtlpEndpoint { get; set; } + + public bool ExportConsole { get; set; } + + public IDictionary ResourceAttributes { get; } = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + } + public sealed class ShutdownOptions { public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30); } + + public sealed class AnalyzerOptions + { + public AnalyzerOptions() + { + PluginDirectories = new List + { + Path.Combine("plugins", "scanner", "analyzers", "os"), + }; + } + + public IList PluginDirectories { get; } + + public string RootFilesystemMetadataKey { get; set; } = ScanMetadataKeys.RootFilesystemPath; + + public string WorkspaceMetadataKey { get; set; } = ScanMetadataKeys.WorkspacePath; + } } diff --git a/src/StellaOps.Scanner.Worker/Options/ScannerWorkerOptionsValidator.cs b/src/StellaOps.Scanner.Worker/Options/ScannerWorkerOptionsValidator.cs index 2b302c54..66ac9879 100644 --- a/src/StellaOps.Scanner.Worker/Options/ScannerWorkerOptionsValidator.cs +++ b/src/StellaOps.Scanner.Worker/Options/ScannerWorkerOptionsValidator.cs @@ -1,91 +1,91 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Options; - -namespace StellaOps.Scanner.Worker.Options; - -public sealed class ScannerWorkerOptionsValidator : IValidateOptions -{ - public ValidateOptionsResult Validate(string? name, ScannerWorkerOptions options) - { - ArgumentNullException.ThrowIfNull(options); - - var failures = new List(); - - if (options.MaxConcurrentJobs <= 0) +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Options; + +namespace StellaOps.Scanner.Worker.Options; + +public sealed class ScannerWorkerOptionsValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, ScannerWorkerOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + var failures = new List(); + + if (options.MaxConcurrentJobs <= 0) + { + failures.Add("Scanner.Worker:MaxConcurrentJobs must be greater than zero."); + } + + if (options.Queue.HeartbeatSafetyFactor < 3.0) { - failures.Add("Scanner.Worker:MaxConcurrentJobs must be greater than zero."); + failures.Add("Scanner.Worker:Queue:HeartbeatSafetyFactor must be at least 3."); } - - if (options.Queue.HeartbeatSafetyFactor < 2.0) - { - failures.Add("Scanner.Worker:Queue:HeartbeatSafetyFactor must be at least 2."); - } - - if (options.Queue.MaxAttempts <= 0) - { - failures.Add("Scanner.Worker:Queue:MaxAttempts must be greater than zero."); - } - - if (options.Queue.MinHeartbeatInterval <= TimeSpan.Zero) - { - failures.Add("Scanner.Worker:Queue:MinHeartbeatInterval must be greater than zero."); - } - - if (options.Queue.MaxHeartbeatInterval <= options.Queue.MinHeartbeatInterval) - { - failures.Add("Scanner.Worker:Queue:MaxHeartbeatInterval must be greater than MinHeartbeatInterval."); - } - - if (options.Polling.InitialDelay <= TimeSpan.Zero) - { - failures.Add("Scanner.Worker:Polling:InitialDelay must be greater than zero."); - } - - if (options.Polling.MaxDelay < options.Polling.InitialDelay) - { - failures.Add("Scanner.Worker:Polling:MaxDelay must be greater than or equal to InitialDelay."); - } - - if (options.Polling.JitterRatio is < 0 or > 1) - { - failures.Add("Scanner.Worker:Polling:JitterRatio must be between 0 and 1."); - } - - if (options.Authority.Enabled) - { - if (string.IsNullOrWhiteSpace(options.Authority.Issuer)) - { - failures.Add("Scanner.Worker:Authority requires Issuer when Enabled is true."); - } - - if (string.IsNullOrWhiteSpace(options.Authority.ClientId)) - { - failures.Add("Scanner.Worker:Authority requires ClientId when Enabled is true."); - } - - if (options.Authority.BackchannelTimeoutSeconds <= 0) - { - failures.Add("Scanner.Worker:Authority:BackchannelTimeoutSeconds must be greater than zero."); - } - - if (options.Authority.TokenClockSkewSeconds < 0) - { - failures.Add("Scanner.Worker:Authority:TokenClockSkewSeconds cannot be negative."); - } - - if (options.Authority.Resilience.RetryDelays.Any(delay => delay <= TimeSpan.Zero)) - { - failures.Add("Scanner.Worker:Authority:Resilience:RetryDelays must be positive durations."); - } - } - - if (options.Shutdown.Timeout < TimeSpan.FromSeconds(5)) - { - failures.Add("Scanner.Worker:Shutdown:Timeout must be at least 5 seconds to allow lease completion."); - } - + + if (options.Queue.MaxAttempts <= 0) + { + failures.Add("Scanner.Worker:Queue:MaxAttempts must be greater than zero."); + } + + if (options.Queue.MinHeartbeatInterval <= TimeSpan.Zero) + { + failures.Add("Scanner.Worker:Queue:MinHeartbeatInterval must be greater than zero."); + } + + if (options.Queue.MaxHeartbeatInterval <= options.Queue.MinHeartbeatInterval) + { + failures.Add("Scanner.Worker:Queue:MaxHeartbeatInterval must be greater than MinHeartbeatInterval."); + } + + if (options.Polling.InitialDelay <= TimeSpan.Zero) + { + failures.Add("Scanner.Worker:Polling:InitialDelay must be greater than zero."); + } + + if (options.Polling.MaxDelay < options.Polling.InitialDelay) + { + failures.Add("Scanner.Worker:Polling:MaxDelay must be greater than or equal to InitialDelay."); + } + + if (options.Polling.JitterRatio is < 0 or > 1) + { + failures.Add("Scanner.Worker:Polling:JitterRatio must be between 0 and 1."); + } + + if (options.Authority.Enabled) + { + if (string.IsNullOrWhiteSpace(options.Authority.Issuer)) + { + failures.Add("Scanner.Worker:Authority requires Issuer when Enabled is true."); + } + + if (string.IsNullOrWhiteSpace(options.Authority.ClientId)) + { + failures.Add("Scanner.Worker:Authority requires ClientId when Enabled is true."); + } + + if (options.Authority.BackchannelTimeoutSeconds <= 0) + { + failures.Add("Scanner.Worker:Authority:BackchannelTimeoutSeconds must be greater than zero."); + } + + if (options.Authority.TokenClockSkewSeconds < 0) + { + failures.Add("Scanner.Worker:Authority:TokenClockSkewSeconds cannot be negative."); + } + + if (options.Authority.Resilience.RetryDelays.Any(delay => delay <= TimeSpan.Zero)) + { + failures.Add("Scanner.Worker:Authority:Resilience:RetryDelays must be positive durations."); + } + } + + if (options.Shutdown.Timeout < TimeSpan.FromSeconds(5)) + { + failures.Add("Scanner.Worker:Shutdown:Timeout must be at least 5 seconds to allow lease completion."); + } + if (options.Telemetry.EnableTelemetry) { if (!options.Telemetry.EnableMetrics && !options.Telemetry.EnableTracing) @@ -94,6 +94,11 @@ public sealed class ScannerWorkerOptionsValidator : IValidateOptions ScanStageNames.ExecuteAnalyzers; - - public ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken) - => _dispatcher.ExecuteAsync(context, cancellationToken); -} +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.Scanner.Worker.Processing; + +public sealed class AnalyzerStageExecutor : IScanStageExecutor +{ + private readonly IScanAnalyzerDispatcher _dispatcher; + + public AnalyzerStageExecutor(IScanAnalyzerDispatcher dispatcher) + { + _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + } + + public string StageName => ScanStageNames.ExecuteAnalyzers; + + public ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken) + => _dispatcher.ExecuteAsync(context, cancellationToken); +} diff --git a/src/StellaOps.Scanner.Worker/Processing/IDelayScheduler.cs b/src/StellaOps.Scanner.Worker/Processing/IDelayScheduler.cs index 2be99eba..49870639 100644 --- a/src/StellaOps.Scanner.Worker/Processing/IDelayScheduler.cs +++ b/src/StellaOps.Scanner.Worker/Processing/IDelayScheduler.cs @@ -1,10 +1,10 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace StellaOps.Scanner.Worker.Processing; - -public interface IDelayScheduler -{ - Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken); -} +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.Scanner.Worker.Processing; + +public interface IDelayScheduler +{ + Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken); +} diff --git a/src/StellaOps.Scanner.Worker/Processing/IScanAnalyzerDispatcher.cs b/src/StellaOps.Scanner.Worker/Processing/IScanAnalyzerDispatcher.cs index e6677fc9..7f998b87 100644 --- a/src/StellaOps.Scanner.Worker/Processing/IScanAnalyzerDispatcher.cs +++ b/src/StellaOps.Scanner.Worker/Processing/IScanAnalyzerDispatcher.cs @@ -1,15 +1,15 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace StellaOps.Scanner.Worker.Processing; - -public interface IScanAnalyzerDispatcher -{ - ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken); -} - -public sealed class NullScanAnalyzerDispatcher : IScanAnalyzerDispatcher -{ - public ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken) - => ValueTask.CompletedTask; -} +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.Scanner.Worker.Processing; + +public interface IScanAnalyzerDispatcher +{ + ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken); +} + +public sealed class NullScanAnalyzerDispatcher : IScanAnalyzerDispatcher +{ + public ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken) + => ValueTask.CompletedTask; +} diff --git a/src/StellaOps.Scanner.Worker/Processing/IScanJobLease.cs b/src/StellaOps.Scanner.Worker/Processing/IScanJobLease.cs index 705c77d4..f7e2bafe 100644 --- a/src/StellaOps.Scanner.Worker/Processing/IScanJobLease.cs +++ b/src/StellaOps.Scanner.Worker/Processing/IScanJobLease.cs @@ -1,31 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace StellaOps.Scanner.Worker.Processing; - -public interface IScanJobLease : IAsyncDisposable -{ - string JobId { get; } - - string ScanId { get; } - - int Attempt { get; } - - DateTimeOffset EnqueuedAtUtc { get; } - - DateTimeOffset LeasedAtUtc { get; } - - TimeSpan LeaseDuration { get; } - - IReadOnlyDictionary Metadata { get; } - - ValueTask RenewAsync(CancellationToken cancellationToken); - - ValueTask CompleteAsync(CancellationToken cancellationToken); - - ValueTask AbandonAsync(string reason, CancellationToken cancellationToken); - - ValueTask PoisonAsync(string reason, CancellationToken cancellationToken); -} +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.Scanner.Worker.Processing; + +public interface IScanJobLease : IAsyncDisposable +{ + string JobId { get; } + + string ScanId { get; } + + int Attempt { get; } + + DateTimeOffset EnqueuedAtUtc { get; } + + DateTimeOffset LeasedAtUtc { get; } + + TimeSpan LeaseDuration { get; } + + IReadOnlyDictionary Metadata { get; } + + ValueTask RenewAsync(CancellationToken cancellationToken); + + ValueTask CompleteAsync(CancellationToken cancellationToken); + + ValueTask AbandonAsync(string reason, CancellationToken cancellationToken); + + ValueTask PoisonAsync(string reason, CancellationToken cancellationToken); +} diff --git a/src/StellaOps.Scanner.Worker/Processing/IScanJobSource.cs b/src/StellaOps.Scanner.Worker/Processing/IScanJobSource.cs index b37dba2b..d11bf48f 100644 --- a/src/StellaOps.Scanner.Worker/Processing/IScanJobSource.cs +++ b/src/StellaOps.Scanner.Worker/Processing/IScanJobSource.cs @@ -1,9 +1,9 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace StellaOps.Scanner.Worker.Processing; - -public interface IScanJobSource -{ - Task TryAcquireAsync(CancellationToken cancellationToken); -} +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.Scanner.Worker.Processing; + +public interface IScanJobSource +{ + Task TryAcquireAsync(CancellationToken cancellationToken); +} diff --git a/src/StellaOps.Scanner.Worker/Processing/IScanStageExecutor.cs b/src/StellaOps.Scanner.Worker/Processing/IScanStageExecutor.cs index bf93169b..f30f2fb0 100644 --- a/src/StellaOps.Scanner.Worker/Processing/IScanStageExecutor.cs +++ b/src/StellaOps.Scanner.Worker/Processing/IScanStageExecutor.cs @@ -1,11 +1,11 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace StellaOps.Scanner.Worker.Processing; - -public interface IScanStageExecutor -{ - string StageName { get; } - - ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken); -} +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.Scanner.Worker.Processing; + +public interface IScanStageExecutor +{ + string StageName { get; } + + ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken); +} diff --git a/src/StellaOps.Scanner.Worker/Processing/LeaseHeartbeatService.cs b/src/StellaOps.Scanner.Worker/Processing/LeaseHeartbeatService.cs index 6dae7c3b..0e5c30fe 100644 --- a/src/StellaOps.Scanner.Worker/Processing/LeaseHeartbeatService.cs +++ b/src/StellaOps.Scanner.Worker/Processing/LeaseHeartbeatService.cs @@ -1,148 +1,155 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using StellaOps.Scanner.Worker.Options; - -namespace StellaOps.Scanner.Worker.Processing; - -public sealed class LeaseHeartbeatService -{ - private readonly TimeProvider _timeProvider; - private readonly IOptionsMonitor _options; - private readonly IDelayScheduler _delayScheduler; - private readonly ILogger _logger; - - public LeaseHeartbeatService(TimeProvider timeProvider, IDelayScheduler delayScheduler, IOptionsMonitor options, ILogger logger) - { - _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); - _delayScheduler = delayScheduler ?? throw new ArgumentNullException(nameof(delayScheduler)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StellaOps.Scanner.Worker.Options; + +namespace StellaOps.Scanner.Worker.Processing; + +public sealed class LeaseHeartbeatService +{ + private readonly TimeProvider _timeProvider; + private readonly IOptionsMonitor _options; + private readonly IDelayScheduler _delayScheduler; + private readonly ILogger _logger; + + public LeaseHeartbeatService(TimeProvider timeProvider, IDelayScheduler delayScheduler, IOptionsMonitor options, ILogger logger) + { + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + _delayScheduler = delayScheduler ?? throw new ArgumentNullException(nameof(delayScheduler)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + public async Task RunAsync(IScanJobLease lease, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(lease); - var options = _options.CurrentValue; - var interval = ComputeInterval(options, lease); + await Task.Yield(); while (!cancellationToken.IsCancellationRequested) { - options = _options.CurrentValue; - var delay = ApplyJitter(interval, options.Queue.MaxHeartbeatJitterMilliseconds); + var options = _options.CurrentValue; + var interval = ComputeInterval(options, lease); + var delay = ApplyJitter(interval, options.Queue); try { await _delayScheduler.DelayAsync(delay, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - break; - } - - if (cancellationToken.IsCancellationRequested) - { - break; - } - - if (await TryRenewAsync(options, lease, cancellationToken).ConfigureAwait(false)) - { - continue; - } - - _logger.LogError( - "Job {JobId} (scan {ScanId}) lease renewal exhausted retries; cancelling processing.", - lease.JobId, - lease.ScanId); - throw new InvalidOperationException("Lease renewal retries exhausted."); - } - } + { + break; + } + + if (cancellationToken.IsCancellationRequested) + { + break; + } + + if (await TryRenewAsync(options, lease, cancellationToken).ConfigureAwait(false)) + { + continue; + } + + _logger.LogError( + "Job {JobId} (scan {ScanId}) lease renewal exhausted retries; cancelling processing.", + lease.JobId, + lease.ScanId); + throw new InvalidOperationException("Lease renewal retries exhausted."); + } + } private static TimeSpan ComputeInterval(ScannerWorkerOptions options, IScanJobLease lease) { var divisor = options.Queue.HeartbeatSafetyFactor <= 0 ? 3.0 : options.Queue.HeartbeatSafetyFactor; - var recommended = TimeSpan.FromTicks((long)(lease.LeaseDuration.Ticks / Math.Max(2.0, divisor))); + var safetyFactor = Math.Max(3.0, divisor); + var recommended = TimeSpan.FromTicks((long)(lease.LeaseDuration.Ticks / safetyFactor)); if (recommended < options.Queue.MinHeartbeatInterval) { recommended = options.Queue.MinHeartbeatInterval; } else if (recommended > options.Queue.MaxHeartbeatInterval) - { - recommended = options.Queue.MaxHeartbeatInterval; - } + { + recommended = options.Queue.MaxHeartbeatInterval; + } return recommended; } - private static TimeSpan ApplyJitter(TimeSpan duration, int maxJitterMilliseconds) + private static TimeSpan ApplyJitter(TimeSpan duration, ScannerWorkerOptions.QueueOptions queueOptions) { - if (maxJitterMilliseconds <= 0) + if (queueOptions.MaxHeartbeatJitterMilliseconds <= 0) { return duration; } - var offset = Random.Shared.NextDouble() * maxJitterMilliseconds; - return duration + TimeSpan.FromMilliseconds(offset); + var offsetMs = Random.Shared.NextDouble() * queueOptions.MaxHeartbeatJitterMilliseconds; + var adjusted = duration - TimeSpan.FromMilliseconds(offsetMs); + if (adjusted < queueOptions.MinHeartbeatInterval) + { + return queueOptions.MinHeartbeatInterval; + } + + return adjusted > TimeSpan.Zero ? adjusted : queueOptions.MinHeartbeatInterval; } private async Task TryRenewAsync(ScannerWorkerOptions options, IScanJobLease lease, CancellationToken cancellationToken) { try - { - await lease.RenewAsync(cancellationToken).ConfigureAwait(false); - return true; - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - return false; - } - catch (Exception ex) - { - _logger.LogWarning( - ex, - "Job {JobId} (scan {ScanId}) heartbeat failed; retrying.", - lease.JobId, - lease.ScanId); - } - - foreach (var delay in options.Queue.NormalizedHeartbeatRetryDelays) - { - if (cancellationToken.IsCancellationRequested) - { - return false; - } - - try - { - await _delayScheduler.DelayAsync(delay, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - return false; - } - - try - { - await lease.RenewAsync(cancellationToken).ConfigureAwait(false); - return true; - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - return false; - } - catch (Exception ex) - { - _logger.LogWarning( - ex, - "Job {JobId} (scan {ScanId}) heartbeat retry failed; will retry after {Delay}.", - lease.JobId, - lease.ScanId, - delay); - } - } - - return false; - } -} + { + await lease.RenewAsync(cancellationToken).ConfigureAwait(false); + return true; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + return false; + } + catch (Exception ex) + { + _logger.LogWarning( + ex, + "Job {JobId} (scan {ScanId}) heartbeat failed; retrying.", + lease.JobId, + lease.ScanId); + } + + foreach (var delay in options.Queue.NormalizedHeartbeatRetryDelays) + { + if (cancellationToken.IsCancellationRequested) + { + return false; + } + + try + { + await _delayScheduler.DelayAsync(delay, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + return false; + } + + try + { + await lease.RenewAsync(cancellationToken).ConfigureAwait(false); + return true; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + return false; + } + catch (Exception ex) + { + _logger.LogWarning( + ex, + "Job {JobId} (scan {ScanId}) heartbeat retry failed; will retry after {Delay}.", + lease.JobId, + lease.ScanId, + delay); + } + } + + return false; + } +} diff --git a/src/StellaOps.Scanner.Worker/Processing/NoOpStageExecutor.cs b/src/StellaOps.Scanner.Worker/Processing/NoOpStageExecutor.cs index c9ec93a6..0b27a2e0 100644 --- a/src/StellaOps.Scanner.Worker/Processing/NoOpStageExecutor.cs +++ b/src/StellaOps.Scanner.Worker/Processing/NoOpStageExecutor.cs @@ -1,18 +1,18 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace StellaOps.Scanner.Worker.Processing; - -public sealed class NoOpStageExecutor : IScanStageExecutor -{ - public NoOpStageExecutor(string stageName) - { - StageName = stageName ?? throw new ArgumentNullException(nameof(stageName)); - } - - public string StageName { get; } - - public ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken) - => ValueTask.CompletedTask; -} +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.Scanner.Worker.Processing; + +public sealed class NoOpStageExecutor : IScanStageExecutor +{ + public NoOpStageExecutor(string stageName) + { + StageName = stageName ?? throw new ArgumentNullException(nameof(stageName)); + } + + public string StageName { get; } + + public ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken) + => ValueTask.CompletedTask; +} diff --git a/src/StellaOps.Scanner.Worker/Processing/NullScanJobSource.cs b/src/StellaOps.Scanner.Worker/Processing/NullScanJobSource.cs index 2f972f88..4efc29e4 100644 --- a/src/StellaOps.Scanner.Worker/Processing/NullScanJobSource.cs +++ b/src/StellaOps.Scanner.Worker/Processing/NullScanJobSource.cs @@ -1,26 +1,26 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace StellaOps.Scanner.Worker.Processing; - -public sealed class NullScanJobSource : IScanJobSource -{ - private readonly ILogger _logger; - private int _logged; - - public NullScanJobSource(ILogger logger) - { - _logger = logger; - } - - public Task TryAcquireAsync(CancellationToken cancellationToken) - { - if (Interlocked.Exchange(ref _logged, 1) == 0) - { - _logger.LogWarning("No queue provider registered. Scanner worker will idle until a queue adapter is configured."); - } - - return Task.FromResult(null); - } -} +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace StellaOps.Scanner.Worker.Processing; + +public sealed class NullScanJobSource : IScanJobSource +{ + private readonly ILogger _logger; + private int _logged; + + public NullScanJobSource(ILogger logger) + { + _logger = logger; + } + + public Task TryAcquireAsync(CancellationToken cancellationToken) + { + if (Interlocked.Exchange(ref _logged, 1) == 0) + { + _logger.LogWarning("No queue provider registered. Scanner worker will idle until a queue adapter is configured."); + } + + return Task.FromResult(null); + } +} diff --git a/src/StellaOps.Scanner.Worker/Processing/OsScanAnalyzerDispatcher.cs b/src/StellaOps.Scanner.Worker/Processing/OsScanAnalyzerDispatcher.cs new file mode 100644 index 00000000..07733fed --- /dev/null +++ b/src/StellaOps.Scanner.Worker/Processing/OsScanAnalyzerDispatcher.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StellaOps.Scanner.Analyzers.OS; +using StellaOps.Scanner.Analyzers.OS.Abstractions; +using StellaOps.Scanner.Analyzers.OS.Plugin; +using StellaOps.Scanner.Core.Contracts; +using StellaOps.Scanner.Worker.Options; + +namespace StellaOps.Scanner.Worker.Processing; + +internal sealed class OsScanAnalyzerDispatcher : IScanAnalyzerDispatcher +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly OsAnalyzerPluginCatalog _catalog; + private readonly ScannerWorkerOptions _options; + private readonly ILogger _logger; + private IReadOnlyList _pluginDirectories = Array.Empty(); + + public OsScanAnalyzerDispatcher( + IServiceScopeFactory scopeFactory, + OsAnalyzerPluginCatalog catalog, + IOptions options, + ILogger logger) + { + _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); + _catalog = catalog ?? throw new ArgumentNullException(nameof(catalog)); + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + LoadPlugins(); + } + + public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(context); + + using var scope = _scopeFactory.CreateScope(); + var services = scope.ServiceProvider; + var analyzers = _catalog.CreateAnalyzers(services); + if (analyzers.Count == 0) + { + _logger.LogWarning("No OS analyzers available; skipping analyzer stage for job {JobId}.", context.JobId); + return; + } + + var metadata = new Dictionary(context.Lease.Metadata, StringComparer.Ordinal); + var rootfsPath = ResolvePath(metadata, _options.Analyzers.RootFilesystemMetadataKey); + if (rootfsPath is null) + { + _logger.LogWarning( + "Metadata key '{MetadataKey}' missing for job {JobId}; unable to locate root filesystem. OS analyzers skipped.", + _options.Analyzers.RootFilesystemMetadataKey, + context.JobId); + return; + } + + var workspacePath = ResolvePath(metadata, _options.Analyzers.WorkspaceMetadataKey); + var loggerFactory = services.GetRequiredService(); + + var results = new List(analyzers.Count); + + foreach (var analyzer in analyzers) + { + cancellationToken.ThrowIfCancellationRequested(); + var analyzerLogger = loggerFactory.CreateLogger(analyzer.GetType()); + var analyzerContext = new OSPackageAnalyzerContext(rootfsPath, workspacePath, context.TimeProvider, analyzerLogger, metadata); + + try + { + var result = await analyzer.AnalyzeAsync(analyzerContext, cancellationToken).ConfigureAwait(false); + results.Add(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "Analyzer {AnalyzerId} failed for job {JobId}.", analyzer.AnalyzerId, context.JobId); + } + } + + if (results.Count > 0) + { + var dictionary = results.ToDictionary(result => result.AnalyzerId, StringComparer.OrdinalIgnoreCase); + context.Analysis.Set(ScanAnalysisKeys.OsPackageAnalyzers, dictionary); + } + } + + private void LoadPlugins() + { + var directories = new List(); + foreach (var configured in _options.Analyzers.PluginDirectories) + { + if (string.IsNullOrWhiteSpace(configured)) + { + continue; + } + + var path = configured; + if (!Path.IsPathRooted(path)) + { + path = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, path)); + } + + directories.Add(path); + } + + if (directories.Count == 0) + { + directories.Add(Path.Combine(AppContext.BaseDirectory, "plugins", "scanner", "analyzers", "os")); + } + + _pluginDirectories = new ReadOnlyCollection(directories); + + for (var i = 0; i < _pluginDirectories.Count; i++) + { + var directory = _pluginDirectories[i]; + var seal = i == _pluginDirectories.Count - 1; + + try + { + _catalog.LoadFromDirectory(directory, seal); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to load analyzer plug-ins from {Directory}.", directory); + } + } + } + + private static string? ResolvePath(IReadOnlyDictionary metadata, string key) + { + if (string.IsNullOrWhiteSpace(key)) + { + return null; + } + + if (!metadata.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value)) + { + return null; + } + + var trimmed = value.Trim(); + return Path.IsPathRooted(trimmed) + ? trimmed + : Path.GetFullPath(trimmed); + } +} diff --git a/src/StellaOps.Scanner.Worker/Processing/PollDelayStrategy.cs b/src/StellaOps.Scanner.Worker/Processing/PollDelayStrategy.cs index cf4a386e..48d3dc96 100644 --- a/src/StellaOps.Scanner.Worker/Processing/PollDelayStrategy.cs +++ b/src/StellaOps.Scanner.Worker/Processing/PollDelayStrategy.cs @@ -1,49 +1,49 @@ -using System; - -using StellaOps.Scanner.Worker.Options; - -namespace StellaOps.Scanner.Worker.Processing; - -public sealed class PollDelayStrategy -{ - private readonly ScannerWorkerOptions.PollingOptions _options; - private TimeSpan _currentDelay; - - public PollDelayStrategy(ScannerWorkerOptions.PollingOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - public TimeSpan NextDelay() - { - if (_currentDelay == TimeSpan.Zero) - { - _currentDelay = _options.InitialDelay; - return ApplyJitter(_currentDelay); - } - - var doubled = _currentDelay + _currentDelay; - _currentDelay = doubled < _options.MaxDelay ? doubled : _options.MaxDelay; - return ApplyJitter(_currentDelay); - } - - public void Reset() => _currentDelay = TimeSpan.Zero; - - private TimeSpan ApplyJitter(TimeSpan duration) - { - if (_options.JitterRatio <= 0) - { - return duration; - } - - var maxOffset = duration.TotalMilliseconds * _options.JitterRatio; - if (maxOffset <= 0) - { - return duration; - } - - var offset = (Random.Shared.NextDouble() * 2.0 - 1.0) * maxOffset; - var adjustedMs = Math.Max(0, duration.TotalMilliseconds + offset); - return TimeSpan.FromMilliseconds(adjustedMs); - } -} +using System; + +using StellaOps.Scanner.Worker.Options; + +namespace StellaOps.Scanner.Worker.Processing; + +public sealed class PollDelayStrategy +{ + private readonly ScannerWorkerOptions.PollingOptions _options; + private TimeSpan _currentDelay; + + public PollDelayStrategy(ScannerWorkerOptions.PollingOptions options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public TimeSpan NextDelay() + { + if (_currentDelay == TimeSpan.Zero) + { + _currentDelay = _options.InitialDelay; + return ApplyJitter(_currentDelay); + } + + var doubled = _currentDelay + _currentDelay; + _currentDelay = doubled < _options.MaxDelay ? doubled : _options.MaxDelay; + return ApplyJitter(_currentDelay); + } + + public void Reset() => _currentDelay = TimeSpan.Zero; + + private TimeSpan ApplyJitter(TimeSpan duration) + { + if (_options.JitterRatio <= 0) + { + return duration; + } + + var maxOffset = duration.TotalMilliseconds * _options.JitterRatio; + if (maxOffset <= 0) + { + return duration; + } + + var offset = (Random.Shared.NextDouble() * 2.0 - 1.0) * maxOffset; + var adjustedMs = Math.Max(0, duration.TotalMilliseconds + offset); + return TimeSpan.FromMilliseconds(adjustedMs); + } +} diff --git a/src/StellaOps.Scanner.Worker/Processing/ScanJobContext.cs b/src/StellaOps.Scanner.Worker/Processing/ScanJobContext.cs index 8b985eeb..6bcd9e56 100644 --- a/src/StellaOps.Scanner.Worker/Processing/ScanJobContext.cs +++ b/src/StellaOps.Scanner.Worker/Processing/ScanJobContext.cs @@ -1,27 +1,31 @@ using System; using System.Threading; - -namespace StellaOps.Scanner.Worker.Processing; - -public sealed class ScanJobContext -{ +using StellaOps.Scanner.Core.Contracts; + +namespace StellaOps.Scanner.Worker.Processing; + +public sealed class ScanJobContext +{ public ScanJobContext(IScanJobLease lease, TimeProvider timeProvider, DateTimeOffset startUtc, CancellationToken cancellationToken) { Lease = lease ?? throw new ArgumentNullException(nameof(lease)); TimeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); StartUtc = startUtc; CancellationToken = cancellationToken; + Analysis = new ScanAnalysisStore(); } - - public IScanJobLease Lease { get; } - - public TimeProvider TimeProvider { get; } - - public DateTimeOffset StartUtc { get; } - - public CancellationToken CancellationToken { get; } - - public string JobId => Lease.JobId; - + + public IScanJobLease Lease { get; } + + public TimeProvider TimeProvider { get; } + + public DateTimeOffset StartUtc { get; } + + public CancellationToken CancellationToken { get; } + + public string JobId => Lease.JobId; + public string ScanId => Lease.ScanId; + + public ScanAnalysisStore Analysis { get; } } diff --git a/src/StellaOps.Scanner.Worker/Processing/ScanJobProcessor.cs b/src/StellaOps.Scanner.Worker/Processing/ScanJobProcessor.cs index 5263d555..7fb48c5f 100644 --- a/src/StellaOps.Scanner.Worker/Processing/ScanJobProcessor.cs +++ b/src/StellaOps.Scanner.Worker/Processing/ScanJobProcessor.cs @@ -1,65 +1,65 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace StellaOps.Scanner.Worker.Processing; - -public sealed class ScanJobProcessor -{ - private readonly IReadOnlyDictionary _executors; - private readonly ScanProgressReporter _progressReporter; - private readonly ILogger _logger; - - public ScanJobProcessor(IEnumerable executors, ScanProgressReporter progressReporter, ILogger logger) - { - _progressReporter = progressReporter ?? throw new ArgumentNullException(nameof(progressReporter)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - var map = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var executor in executors ?? Array.Empty()) - { - if (executor is null || string.IsNullOrWhiteSpace(executor.StageName)) - { - continue; - } - - map[executor.StageName] = executor; - } - - foreach (var stage in ScanStageNames.Ordered) - { - if (map.ContainsKey(stage)) - { - continue; - } - - map[stage] = new NoOpStageExecutor(stage); - _logger.LogDebug("No executor registered for stage {Stage}; using no-op placeholder.", stage); - } - - _executors = map; - } - - public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(context); - - foreach (var stage in ScanStageNames.Ordered) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (!_executors.TryGetValue(stage, out var executor)) - { - continue; - } - - await _progressReporter.ExecuteStageAsync( - context, - stage, - executor.ExecuteAsync, - cancellationToken).ConfigureAwait(false); - } - } -} +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace StellaOps.Scanner.Worker.Processing; + +public sealed class ScanJobProcessor +{ + private readonly IReadOnlyDictionary _executors; + private readonly ScanProgressReporter _progressReporter; + private readonly ILogger _logger; + + public ScanJobProcessor(IEnumerable executors, ScanProgressReporter progressReporter, ILogger logger) + { + _progressReporter = progressReporter ?? throw new ArgumentNullException(nameof(progressReporter)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var executor in executors ?? Array.Empty()) + { + if (executor is null || string.IsNullOrWhiteSpace(executor.StageName)) + { + continue; + } + + map[executor.StageName] = executor; + } + + foreach (var stage in ScanStageNames.Ordered) + { + if (map.ContainsKey(stage)) + { + continue; + } + + map[stage] = new NoOpStageExecutor(stage); + _logger.LogDebug("No executor registered for stage {Stage}; using no-op placeholder.", stage); + } + + _executors = map; + } + + public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(context); + + foreach (var stage in ScanStageNames.Ordered) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!_executors.TryGetValue(stage, out var executor)) + { + continue; + } + + await _progressReporter.ExecuteStageAsync( + context, + stage, + executor.ExecuteAsync, + cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/StellaOps.Scanner.Worker/Processing/ScanProgressReporter.cs b/src/StellaOps.Scanner.Worker/Processing/ScanProgressReporter.cs index 228a02ac..a2cccc49 100644 --- a/src/StellaOps.Scanner.Worker/Processing/ScanProgressReporter.cs +++ b/src/StellaOps.Scanner.Worker/Processing/ScanProgressReporter.cs @@ -1,86 +1,86 @@ -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using StellaOps.Scanner.Worker.Diagnostics; - -namespace StellaOps.Scanner.Worker.Processing; - -public sealed partial class ScanProgressReporter -{ - private readonly ScannerWorkerMetrics _metrics; - private readonly ILogger _logger; - - public ScanProgressReporter(ScannerWorkerMetrics metrics, ILogger logger) - { - _metrics = metrics ?? throw new ArgumentNullException(nameof(metrics)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public async ValueTask ExecuteStageAsync( - ScanJobContext context, - string stageName, - Func stageWork, - CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(context); - ArgumentException.ThrowIfNullOrWhiteSpace(stageName); - ArgumentNullException.ThrowIfNull(stageWork); - - StageStarting(_logger, context.JobId, context.ScanId, stageName, context.Lease.Attempt); - - var start = context.TimeProvider.GetUtcNow(); - using var activity = ScannerWorkerInstrumentation.ActivitySource.StartActivity( - $"scanner.worker.{stageName}", - ActivityKind.Internal); - - activity?.SetTag("scanner.worker.job_id", context.JobId); - activity?.SetTag("scanner.worker.scan_id", context.ScanId); - activity?.SetTag("scanner.worker.stage", stageName); - - try - { - await stageWork(context, cancellationToken).ConfigureAwait(false); - var duration = context.TimeProvider.GetUtcNow() - start; - _metrics.RecordStageDuration(context, stageName, duration); - StageCompleted(_logger, context.JobId, context.ScanId, stageName, duration.TotalMilliseconds); - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - StageCancelled(_logger, context.JobId, context.ScanId, stageName); - throw; - } - catch (Exception ex) - { - var duration = context.TimeProvider.GetUtcNow() - start; - _metrics.RecordStageDuration(context, stageName, duration); - StageFailed(_logger, context.JobId, context.ScanId, stageName, ex); - throw; - } - } - - [LoggerMessage( - EventId = 1000, - Level = LogLevel.Information, - Message = "Job {JobId} (scan {ScanId}) entering stage {Stage} (attempt {Attempt}).")] - private static partial void StageStarting(ILogger logger, string jobId, string scanId, string stage, int attempt); - - [LoggerMessage( - EventId = 1001, - Level = LogLevel.Information, - Message = "Job {JobId} (scan {ScanId}) finished stage {Stage} in {ElapsedMs:F0} ms.")] - private static partial void StageCompleted(ILogger logger, string jobId, string scanId, string stage, double elapsedMs); - - [LoggerMessage( - EventId = 1002, - Level = LogLevel.Warning, - Message = "Job {JobId} (scan {ScanId}) stage {Stage} cancelled by request.")] - private static partial void StageCancelled(ILogger logger, string jobId, string scanId, string stage); - - [LoggerMessage( - EventId = 1003, - Level = LogLevel.Error, - Message = "Job {JobId} (scan {ScanId}) stage {Stage} failed.")] - private static partial void StageFailed(ILogger logger, string jobId, string scanId, string stage, Exception exception); -} +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using StellaOps.Scanner.Worker.Diagnostics; + +namespace StellaOps.Scanner.Worker.Processing; + +public sealed partial class ScanProgressReporter +{ + private readonly ScannerWorkerMetrics _metrics; + private readonly ILogger _logger; + + public ScanProgressReporter(ScannerWorkerMetrics metrics, ILogger logger) + { + _metrics = metrics ?? throw new ArgumentNullException(nameof(metrics)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async ValueTask ExecuteStageAsync( + ScanJobContext context, + string stageName, + Func stageWork, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentException.ThrowIfNullOrWhiteSpace(stageName); + ArgumentNullException.ThrowIfNull(stageWork); + + StageStarting(_logger, context.JobId, context.ScanId, stageName, context.Lease.Attempt); + + var start = context.TimeProvider.GetUtcNow(); + using var activity = ScannerWorkerInstrumentation.ActivitySource.StartActivity( + $"scanner.worker.{stageName}", + ActivityKind.Internal); + + activity?.SetTag("scanner.worker.job_id", context.JobId); + activity?.SetTag("scanner.worker.scan_id", context.ScanId); + activity?.SetTag("scanner.worker.stage", stageName); + + try + { + await stageWork(context, cancellationToken).ConfigureAwait(false); + var duration = context.TimeProvider.GetUtcNow() - start; + _metrics.RecordStageDuration(context, stageName, duration); + StageCompleted(_logger, context.JobId, context.ScanId, stageName, duration.TotalMilliseconds); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + StageCancelled(_logger, context.JobId, context.ScanId, stageName); + throw; + } + catch (Exception ex) + { + var duration = context.TimeProvider.GetUtcNow() - start; + _metrics.RecordStageDuration(context, stageName, duration); + StageFailed(_logger, context.JobId, context.ScanId, stageName, ex); + throw; + } + } + + [LoggerMessage( + EventId = 1000, + Level = LogLevel.Information, + Message = "Job {JobId} (scan {ScanId}) entering stage {Stage} (attempt {Attempt}).")] + private static partial void StageStarting(ILogger logger, string jobId, string scanId, string stage, int attempt); + + [LoggerMessage( + EventId = 1001, + Level = LogLevel.Information, + Message = "Job {JobId} (scan {ScanId}) finished stage {Stage} in {ElapsedMs:F0} ms.")] + private static partial void StageCompleted(ILogger logger, string jobId, string scanId, string stage, double elapsedMs); + + [LoggerMessage( + EventId = 1002, + Level = LogLevel.Warning, + Message = "Job {JobId} (scan {ScanId}) stage {Stage} cancelled by request.")] + private static partial void StageCancelled(ILogger logger, string jobId, string scanId, string stage); + + [LoggerMessage( + EventId = 1003, + Level = LogLevel.Error, + Message = "Job {JobId} (scan {ScanId}) stage {Stage} failed.")] + private static partial void StageFailed(ILogger logger, string jobId, string scanId, string stage, Exception exception); +} diff --git a/src/StellaOps.Scanner.Worker/Processing/ScanStageNames.cs b/src/StellaOps.Scanner.Worker/Processing/ScanStageNames.cs index f4aea671..d1529ae0 100644 --- a/src/StellaOps.Scanner.Worker/Processing/ScanStageNames.cs +++ b/src/StellaOps.Scanner.Worker/Processing/ScanStageNames.cs @@ -1,23 +1,23 @@ -using System.Collections.Generic; - -namespace StellaOps.Scanner.Worker.Processing; - -public static class ScanStageNames -{ - public const string ResolveImage = "resolve-image"; - public const string PullLayers = "pull-layers"; - public const string BuildFilesystem = "build-filesystem"; - public const string ExecuteAnalyzers = "execute-analyzers"; - public const string ComposeArtifacts = "compose-artifacts"; - public const string EmitReports = "emit-reports"; - - public static readonly IReadOnlyList Ordered = new[] - { - ResolveImage, - PullLayers, - BuildFilesystem, - ExecuteAnalyzers, - ComposeArtifacts, - EmitReports, - }; -} +using System.Collections.Generic; + +namespace StellaOps.Scanner.Worker.Processing; + +public static class ScanStageNames +{ + public const string ResolveImage = "resolve-image"; + public const string PullLayers = "pull-layers"; + public const string BuildFilesystem = "build-filesystem"; + public const string ExecuteAnalyzers = "execute-analyzers"; + public const string ComposeArtifacts = "compose-artifacts"; + public const string EmitReports = "emit-reports"; + + public static readonly IReadOnlyList Ordered = new[] + { + ResolveImage, + PullLayers, + BuildFilesystem, + ExecuteAnalyzers, + ComposeArtifacts, + EmitReports, + }; +} diff --git a/src/StellaOps.Scanner.Worker/Processing/SystemDelayScheduler.cs b/src/StellaOps.Scanner.Worker/Processing/SystemDelayScheduler.cs index cf5f3b9e..b167974c 100644 --- a/src/StellaOps.Scanner.Worker/Processing/SystemDelayScheduler.cs +++ b/src/StellaOps.Scanner.Worker/Processing/SystemDelayScheduler.cs @@ -1,18 +1,18 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace StellaOps.Scanner.Worker.Processing; - -public sealed class SystemDelayScheduler : IDelayScheduler -{ - public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken) - { - if (delay <= TimeSpan.Zero) - { - return Task.CompletedTask; - } - - return Task.Delay(delay, cancellationToken); - } -} +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.Scanner.Worker.Processing; + +public sealed class SystemDelayScheduler : IDelayScheduler +{ + public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken) + { + if (delay <= TimeSpan.Zero) + { + return Task.CompletedTask; + } + + return Task.Delay(delay, cancellationToken); + } +} diff --git a/src/StellaOps.Scanner.Worker/Program.cs b/src/StellaOps.Scanner.Worker/Program.cs index d2275074..267eb3f2 100644 --- a/src/StellaOps.Scanner.Worker/Program.cs +++ b/src/StellaOps.Scanner.Worker/Program.cs @@ -1,98 +1,103 @@ -using System.Diagnostics; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.DependencyInjection.Extensions; -using StellaOps.Auth.Client; +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection.Extensions; +using StellaOps.Auth.Client; +using StellaOps.Scanner.Analyzers.OS.Plugin; +using StellaOps.Scanner.EntryTrace; using StellaOps.Scanner.Worker.Diagnostics; using StellaOps.Scanner.Worker.Hosting; using StellaOps.Scanner.Worker.Options; using StellaOps.Scanner.Worker.Processing; - -var builder = Host.CreateApplicationBuilder(args); - -builder.Services.AddOptions() - .BindConfiguration(ScannerWorkerOptions.SectionName) - .ValidateOnStart(); - -builder.Services.AddSingleton, ScannerWorkerOptionsValidator>(); -builder.Services.AddSingleton(TimeProvider.System); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); + +var builder = Host.CreateApplicationBuilder(args); + +builder.Services.AddOptions() + .BindConfiguration(ScannerWorkerOptions.SectionName) + .ValidateOnStart(); + +builder.Services.AddSingleton, ScannerWorkerOptionsValidator>(); +builder.Services.AddSingleton(TimeProvider.System); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddEntryTraceAnalyzer(); + builder.Services.TryAddSingleton(); -builder.Services.TryAddSingleton(); -builder.Services.AddSingleton(); - -builder.Services.AddSingleton(); -builder.Services.AddHostedService(sp => sp.GetRequiredService()); - -var workerOptions = builder.Configuration.GetSection(ScannerWorkerOptions.SectionName).Get() ?? new ScannerWorkerOptions(); - -builder.Services.Configure(options => -{ - options.ShutdownTimeout = workerOptions.Shutdown.Timeout; -}); - -builder.ConfigureScannerWorkerTelemetry(workerOptions); - -if (workerOptions.Authority.Enabled) -{ - builder.Services.AddStellaOpsAuthClient(clientOptions => - { - clientOptions.Authority = workerOptions.Authority.Issuer?.Trim() ?? string.Empty; - clientOptions.ClientId = workerOptions.Authority.ClientId?.Trim() ?? string.Empty; - clientOptions.ClientSecret = workerOptions.Authority.ClientSecret; - clientOptions.EnableRetries = workerOptions.Authority.Resilience.EnableRetries ?? true; - clientOptions.HttpTimeout = TimeSpan.FromSeconds(workerOptions.Authority.BackchannelTimeoutSeconds); - - clientOptions.DefaultScopes.Clear(); - foreach (var scope in workerOptions.Authority.Scopes) - { - if (string.IsNullOrWhiteSpace(scope)) - { - continue; - } - - clientOptions.DefaultScopes.Add(scope); - } - - clientOptions.RetryDelays.Clear(); - foreach (var delay in workerOptions.Authority.Resilience.RetryDelays) - { - if (delay <= TimeSpan.Zero) - { - continue; - } - - clientOptions.RetryDelays.Add(delay); - } - - if (workerOptions.Authority.Resilience.AllowOfflineCacheFallback is bool allowOffline) - { - clientOptions.AllowOfflineCacheFallback = allowOffline; - } - - if (workerOptions.Authority.Resilience.OfflineCacheTolerance is { } tolerance && tolerance > TimeSpan.Zero) - { - clientOptions.OfflineCacheTolerance = tolerance; - } - }); -} - -builder.Logging.Configure(options => -{ - options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId - | ActivityTrackingOptions.TraceId - | ActivityTrackingOptions.ParentId; -}); - -var host = builder.Build(); - -await host.RunAsync(); - -public partial class Program; +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddSingleton(); +builder.Services.AddHostedService(sp => sp.GetRequiredService()); + +var workerOptions = builder.Configuration.GetSection(ScannerWorkerOptions.SectionName).Get() ?? new ScannerWorkerOptions(); + +builder.Services.Configure(options => +{ + options.ShutdownTimeout = workerOptions.Shutdown.Timeout; +}); + +builder.ConfigureScannerWorkerTelemetry(workerOptions); + +if (workerOptions.Authority.Enabled) +{ + builder.Services.AddStellaOpsAuthClient(clientOptions => + { + clientOptions.Authority = workerOptions.Authority.Issuer?.Trim() ?? string.Empty; + clientOptions.ClientId = workerOptions.Authority.ClientId?.Trim() ?? string.Empty; + clientOptions.ClientSecret = workerOptions.Authority.ClientSecret; + clientOptions.EnableRetries = workerOptions.Authority.Resilience.EnableRetries ?? true; + clientOptions.HttpTimeout = TimeSpan.FromSeconds(workerOptions.Authority.BackchannelTimeoutSeconds); + + clientOptions.DefaultScopes.Clear(); + foreach (var scope in workerOptions.Authority.Scopes) + { + if (string.IsNullOrWhiteSpace(scope)) + { + continue; + } + + clientOptions.DefaultScopes.Add(scope); + } + + clientOptions.RetryDelays.Clear(); + foreach (var delay in workerOptions.Authority.Resilience.RetryDelays) + { + if (delay <= TimeSpan.Zero) + { + continue; + } + + clientOptions.RetryDelays.Add(delay); + } + + if (workerOptions.Authority.Resilience.AllowOfflineCacheFallback is bool allowOffline) + { + clientOptions.AllowOfflineCacheFallback = allowOffline; + } + + if (workerOptions.Authority.Resilience.OfflineCacheTolerance is { } tolerance && tolerance > TimeSpan.Zero) + { + clientOptions.OfflineCacheTolerance = tolerance; + } + }); +} + +builder.Logging.Configure(options => +{ + options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId + | ActivityTrackingOptions.TraceId + | ActivityTrackingOptions.ParentId; +}); + +var host = builder.Build(); + +await host.RunAsync(); + +public partial class Program; diff --git a/src/StellaOps.Scanner.Worker/StellaOps.Scanner.Worker.csproj b/src/StellaOps.Scanner.Worker/StellaOps.Scanner.Worker.csproj index 7d9307e3..d4eedf0d 100644 --- a/src/StellaOps.Scanner.Worker/StellaOps.Scanner.Worker.csproj +++ b/src/StellaOps.Scanner.Worker/StellaOps.Scanner.Worker.csproj @@ -1,20 +1,22 @@ - - - net10.0 - preview - enable - enable - true - - - - - - - - + + + net10.0 + preview + enable + enable + true + + + + + + + + + + diff --git a/src/StellaOps.Scanner.Worker/TASKS.md b/src/StellaOps.Scanner.Worker/TASKS.md index c7be4b7b..16f3b032 100644 --- a/src/StellaOps.Scanner.Worker/TASKS.md +++ b/src/StellaOps.Scanner.Worker/TASKS.md @@ -1,8 +1,9 @@ -# Scanner Worker Task Board - -| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | -|----|--------|----------|------------|-------------|---------------| -| SCANNER-WORKER-09-201 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-CORE-09-501 | Worker host bootstrap with Authority auth, hosted services, and graceful shutdown semantics. | `Program.cs` binds `Scanner:Worker` options, registers delay scheduler, configures telemetry + Authority client, and enforces shutdown timeout. | -| SCANNER-WORKER-09-202 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-WORKER-09-201, SCANNER-QUEUE-09-401 | Lease/heartbeat loop with retry+jitter, poison-job quarantine, structured logging. | `ScannerWorkerHostedService` + `LeaseHeartbeatService` manage concurrency, renewal margins, poison handling, and structured logs exercised by integration fixture. | -| SCANNER-WORKER-09-203 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-WORKER-09-202, SCANNER-STORAGE-09-301 | Analyzer dispatch skeleton emitting deterministic stage progress and honoring cancellation tokens. | Deterministic stage list + `ScanProgressReporter`; `WorkerBasicScanScenario` validates ordering and cancellation propagation. | +# Scanner Worker Task Board + +| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | +|----|--------|----------|------------|-------------|---------------| +| SCANNER-WORKER-09-201 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-CORE-09-501 | Worker host bootstrap with Authority auth, hosted services, and graceful shutdown semantics. | `Program.cs` binds `Scanner:Worker` options, registers delay scheduler, configures telemetry + Authority client, and enforces shutdown timeout. | +| SCANNER-WORKER-09-202 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-WORKER-09-201, SCANNER-QUEUE-09-401 | Lease/heartbeat loop with retry+jitter, poison-job quarantine, structured logging. | `ScannerWorkerHostedService` + `LeaseHeartbeatService` manage concurrency, renewal margins, poison handling, and structured logs exercised by integration fixture. | +| SCANNER-WORKER-09-203 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-WORKER-09-202, SCANNER-STORAGE-09-301 | Analyzer dispatch skeleton emitting deterministic stage progress and honoring cancellation tokens. | Deterministic stage list + `ScanProgressReporter`; `WorkerBasicScanScenario` validates ordering and cancellation propagation. | | SCANNER-WORKER-09-204 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-WORKER-09-203 | Worker metrics (queue latency, stage duration, failure counts) with OpenTelemetry resource wiring. | `ScannerWorkerMetrics` records queue/job/stage metrics; integration test asserts analyzer stage histogram entries. | +| SCANNER-WORKER-09-205 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-WORKER-09-202 | Harden heartbeat jitter so lease safety margin stays ≥3× and cover with regression tests. | `LeaseHeartbeatService` clamps jitter to safety window, validator enforces ≥3 safety factor, regression tests cover heartbeat scheduling and metrics. |