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