Refactor and enhance scanner worker functionality

- Cleaned up code formatting and organization across multiple files for improved readability.
- Introduced `OsScanAnalyzerDispatcher` to handle OS analyzer execution and plugin loading.
- Updated `ScanJobContext` to include an `Analysis` property for storing scan results.
- Enhanced `ScanJobProcessor` to utilize the new `OsScanAnalyzerDispatcher`.
- Improved logging and error handling in `ScanProgressReporter` for better traceability.
- Updated project dependencies and added references to new analyzer plugins.
- Revised task documentation to reflect current status and dependencies.
This commit is contained in:
2025-10-19 18:34:15 +03:00
parent daa6a4ae8c
commit 7e2fa0a42a
59 changed files with 5563 additions and 2288 deletions

View File

@@ -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"

View File

@@ -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<br>Instructions to work:<br>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<br>Instructions to work:<br>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<br>Instructions to work:<br>`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<br>Instructions to work:<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>`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<br>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<br>Summary planner now drives monthly/yearly VINCE fetches, persists pending summaries/notes, and hydrates VINCE detail queue with telemetry.<br>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<br>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<br>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<br>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<br>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<br>`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<br>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<br>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<br>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<br>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<br>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<br>Instructions to work:<br>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<br>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<br>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<br>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<br>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<br>`UPDATE_APPLE_FIXTURES=1` flow fetches & rewrites fixtures; README documents usage.<br>Instructions to work:<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>Deployment docs and CLI notes explain the LIB5 resilience knobs for rollout.<br>Instructions to work:<br>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.<br>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<br>`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<br>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<br>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<br>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<br>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<br>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<br>Delivered Mongo index/query addendum with `$unwind` recipes, dedupe checks, and operational checklist.<br>Instructions to work:<br>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<br>`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<br>Field precedence tables and tie-breaker metrics wired into the canonical merge flow; docs/tests updated.<br>Instructions to work:<br>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<br>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<br>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<br>Storage tests adjusted for normalized versions/decision reasons.<br>Instructions to work:<br>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<br>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<br>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<br>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<br>Added regression tests (`AdvisoryMergeServiceTests`) covering canonical + precedence flow.<br>Instructions to work:<br>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<br>Instructions to work:<br>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<br>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<br>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)<br>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<br>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<br>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<br>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<br>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<br>Update schemas/offline bundle + fixtures once model/core parity lands.<br>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<br>Extend Bolt builder, metadata, and regression tests for the expanded schema.<br>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<br>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<br>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<br>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<br>Instructions to work:<br>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<br>Instructions to work:<br>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<br>Instructions to work:<br>`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<br>Instructions to work:<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>`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<br>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<br>Summary planner now drives monthly/yearly VINCE fetches, persists pending summaries/notes, and hydrates VINCE detail queue with telemetry.<br>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<br>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<br>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<br>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<br>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<br>`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<br>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<br>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<br>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<br>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<br>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<br>Instructions to work:<br>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<br>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<br>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<br>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<br>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<br>`UPDATE_APPLE_FIXTURES=1` flow fetches & rewrites fixtures; README documents usage.<br>Instructions to work:<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>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<br>Deployment docs and CLI notes explain the LIB5 resilience knobs for rollout.<br>Instructions to work:<br>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.<br>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<br>`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<br>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<br>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<br>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<br>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<br>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<br>Delivered Mongo index/query addendum with `$unwind` recipes, dedupe checks, and operational checklist.<br>Instructions to work:<br>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<br>`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<br>Field precedence tables and tie-breaker metrics wired into the canonical merge flow; docs/tests updated.<br>Instructions to work:<br>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<br>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<br>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<br>Storage tests adjusted for normalized versions/decision reasons.<br>Instructions to work:<br>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<br>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<br>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<br>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<br>Added regression tests (`AdvisoryMergeServiceTests`) covering canonical + precedence flow.<br>Instructions to work:<br>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<br>Instructions to work:<br>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<br>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<br>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)<br>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<br>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<br>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<br>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<br>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<br>Update schemas/offline bundle + fixtures once model/core parity lands.<br>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<br>Extend Bolt builder, metadata, and regression tests for the expanded schema.<br>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<br>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<br>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<br>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 <5s 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 <5s 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. |

View File

@@ -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 (1d ≈ 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, ~3w)
### Group SP9-G1 — Core Contracts & Observability (src/StellaOps.Scanner.Core) ~1w
- Tasks:
- SCANNER-CORE-09-501 · 3d · `/src/StellaOps.Scanner.Core/TASKS.md`
- SCANNER-CORE-09-502 · 2d · same path
- SCANNER-CORE-09-503 · 2d · 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) ~1w
- Tasks: SCANNER-QUEUE-09-401 (3d), -402 (2d), -403 (2d) · `/src/StellaOps.Scanner.Queue/TASKS.md`
- Acceptance: dequeue latency p95 ≤20ms at 40rps; 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) ~1w
- Tasks: SCANNER-STORAGE-09-301 (3d), -302 (2d), -303 (2d)
- Acceptance: majority write/read ≤50ms; 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.2w
- Tasks: SCANNER-WEB-09-101 (2d), -102 (3d), -103 (2d), -104 (2d), SCANNER-POLICY-09-105 (3d), SCANNER-POLICY-09-106 (4d)
- Acceptance: `/api/v1/scans` enqueue p95 ≤50ms 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 (1d ≈ 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, ~3w)
### Group SP9-G1 — Core Contracts & Observability (src/StellaOps.Scanner.Core) ~1w
- Tasks:
- SCANNER-CORE-09-501 · 3d · `/src/StellaOps.Scanner.Core/TASKS.md`
- SCANNER-CORE-09-502 · 2d · same path
- SCANNER-CORE-09-503 · 2d · 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) ~1w
- Tasks: SCANNER-QUEUE-09-401 (3d), -402 (2d), -403 (2d) · `/src/StellaOps.Scanner.Queue/TASKS.md`
- Acceptance: dequeue latency p95 ≤20ms at 40rps; 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) ~1w
- Tasks: SCANNER-STORAGE-09-301 (3d), -302 (2d), -303 (2d)
- Acceptance: majority write/read ≤50ms; 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.2w
- Tasks: SCANNER-WEB-09-101 (2d), -102 (3d), -103 (2d), -104 (2d), SCANNER-POLICY-09-105 (3d), SCANNER-POLICY-09-106 (4d)
- Acceptance: `/api/v1/scans` enqueue p95 ≤50ms 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) ~1w
- Tasks: SCANNER-WORKER-09-201 (3d), -202 (3d), -203 (2d), -204 (2d)
- Tasks: SCANNER-WORKER-09-201 (3d), -202 (3d), -203 (2d), -204 (2d), -205 (1d)
- 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.8w
- Tasks: SP9-BLDX-09-001 (3d), SP9-BLDX-09-002 (2d), SP9-BLDX-09-003 (2d)
- Tasks: SP9-BLDX-09-001 (3d), SP9-BLDX-09-002 (2d), SP9-BLDX-09-003 (2d), SP9-BLDX-09-004 (2d), SP9-BLDX-09-005 (1d)
- Acceptance: build-time overhead 300ms/layer on 4vCPU; 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) ~1w
- Tasks: POLICY-CORE-09-001 (2d) ✅, -002 (3d) ✅, -003 (3d) ✅, -004 (3d), -005 (4d), -006 (2d)
- Acceptance: policy parsing 200 files/s; preview diff response <200ms 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.4w
- Tasks: DEVOPS-HELM-09-001 (3d) **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.4w
- Tasks: DOCS-ADR-09-001 (2d), DOCS-EVENTS-09-002 (2d)
- 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, ~4w)
### Group SP10-G1 — OS Analyzer Plug-ins (src/StellaOps.Scanner.Analyzers.OS) ~1w
- Tasks: SCANNER-ANALYZERS-OS-10-201..207 (durations 23d each)
- Acceptance: analyzer runtime <1.5s/image; memory <250MB.
- 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.5w
- Tasks: SCANNER-ANALYZERS-LANG-10-301..309
- Acceptance: Node analyzer handles 10k modules <2s; Python memory <200MB.
- Gate: golden outputs stored; plugin manifests present.
### Group SP10-G3 — EntryTrace Plug-ins (src/StellaOps.Scanner.EntryTrace) ~0.8w
- 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) ~1w
- Tasks: SCANNER-DIFF-10-501..503, SCANNER-EMIT-10-601..606
- Acceptance: BOM-Index emission <500ms/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.6w
- Tasks: SCANNER-CACHE-10-101..104
- Acceptance: cache hit instrumentation validated; eviction keeps footprint <5GB.
- Gate: cache configuration doc; integration test `LayerCacheRoundTrip` green.
### Group SP10-G6 — Benchmarks & Samples (bench/, samples/, ops/devops) ~0.6w
- Tasks: BENCH-SCANNER-10-001 (2d), SAMPLES-10-001 (finish 3d), DEVOPS-PERF-10-001 (2d)
- Acceptance: analyzer benchmark CSV published; perf CI guard ensures SBOM compose <5s; sample SBOM/BOM-Index committed.
- Gate: bench results stored under `bench/`; `samples/` populated; CI job added.
---
## Sprint 11 Signing Chain Bring-up (ID: SP11, ~3w)
### Group SP11-G1 — Authority Sender Constraints (src/StellaOps.Authority) ~0.8w
- Tasks: AUTH-DPOP-11-001 (3d), AUTH-MTLS-11-002 (2d)
- Acceptance: DPoP nonce dance validated; mTLS tokens issued in 40ms.
- Gate: updated Authority OpenAPI; QA scripts verifying DPoP/mTLS.
### Group SP11-G2 — Signer Service (src/StellaOps.Signer) ~1.2w
- Tasks: SIGNER-API-11-101 (4d), SIGNER-REF-11-102 (2d), SIGNER-QUOTA-11-103 (2d)
- Acceptance: signing throughput 30 req/min; p95 latency 200ms.
- 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) ~1w
- Tasks: ATTESTOR-API-11-201 (3d), ATTESTOR-VERIFY-11-202 (2d), ATTESTOR-OBS-11-203 (2d)
- Acceptance: inclusion proof retrieval <500ms; audit log coverage 100%.
- Gate: Attestor API doc + verification script.
### Group SP11-G4 — UI Attestation Hooks (src/StellaOps.UI) ~0.4w
- Tasks: UI-ATTEST-11-005 (3d)
- Acceptance: attestation panel renders within 200ms; Rekor link verified.
- Gate SP11-G4 SP13-G1: recorded UX walkthrough.
---
## Sprint 12 Runtime Guardrails (ID: SP12, ~3w)
### Group SP12-G1 — Zastava Core (src/StellaOps.Zastava.Core) ~0.8w
- 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.8w
- Tasks: ZASTAVA-OBS-12-001..004
- Acceptance: observer memory <200MB; event flush 2s.
- Gate: sample runtime events stored; offline buffer test passes.
### Group SP12-G3 — Zastava Webhook (src/StellaOps.Zastava.Webhook) ~0.6w
- Tasks: ZASTAVA-WEBHOOK-12-101..103
- Acceptance: admission latency p95 45ms; cache TTL adhered to.
- Gate: TLS rotation procedure documented; readiness probe script.
### Group SP12-G4 — Scanner Runtime APIs (src/StellaOps.Scanner.WebService) ~0.8w
- Tasks: SCANNER-RUNTIME-12-301 (2d), SCANNER-RUNTIME-12-302 (3d)
- 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, ~2w)
### Group SP13-G1 — UI Shell & Panels (src/StellaOps.UI) ~1.6w
- Tasks: UI-AUTH-13-001 (3d), UI-SCANS-13-002 (4d), UI-VEX-13-003 (3d), UI-ADMIN-13-004 (2d), UI-SCHED-13-005 (3d), UI-NOTIFY-13-006 (3d)
- 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.8w
- Tasks: CLI-RUNTIME-13-005 (3d), CLI-OFFLINE-13-006 (3d), CLI-PLUGIN-13-007 (2d)
- Acceptance: runtime policy CLI completes <1s 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, ~2w)
### Group SP14-G1 — Release Automation (ops/devops) ~0.8w
- Tasks: DEVOPS-REL-14-001 (4d)
- 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.6w
- Tasks: DEVOPS-OFFLINE-14-002 (3d)
- Acceptance: kit import <5min with integrity verification CLI.
- Gate: kit doc updated; import script included.
### Group SP14-G3 — Deployment Playbooks (ops/deployment) ~0.4w
- Tasks: DEVOPS-OPS-14-003 (2d)
- Acceptance: rollback drill recorded; compatibility matrix produced.
- Gate: playbook PR merged with Ops sign-off.
### Group SP14-G4 — Licensing Token Service (ops/licensing) ~0.4w
- Tasks: DEVOPS-LIC-14-004 (2d)
- Acceptance: token service handles 100 req/min; revocation latency <60s.
- Gate: monitoring dashboard links; failover doc.
---
## Sprint 15 Notify Foundations (ID: SP15, ~3w)
### Group SP15-G1 — Models & Storage (src/StellaOps.Notify.Models + Storage.Mongo) ~0.8w
- Tasks: NOTIFY-MODELS-15-101 (2d), -102 (2d), -103 (1d); NOTIFY-STORAGE-15-201 (3d), -202 (2d), -203 (1d)
- Acceptance: rule CRUD latency <120ms; delivery retention job verified.
- Gate: schema docs + fixtures published.
### Group SP15-G2 — Engine & Queue (src/StellaOps.Notify.Engine + Queue) ~0.8w
- 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.8w
- Tasks: NOTIFY-WEB-15-101..104, NOTIFY-WORKER-15-201..204
- Acceptance: API p95 <120ms; worker delivery success 99%.
- Gate: end-to-end fixture run producing delivery record.
### Group SP15-G4 — Channel Plug-ins (src/StellaOps.Notify.Connectors.*) ~0.6w
- 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.5w
- Tasks: SCANNER-EVENTS-15-201 (2d), BENCH-NOTIFY-15-001 (2d)
- Acceptance: event emission latency <100ms; throughput bench results stored.
- Gate: `docs/events/samples/` contains sample payloads; bench CSV in repo.
---
## Sprint 16 Scheduler Intelligence (ID: SP16, ~4w)
### Group SP16-G1 — Models & Storage (src/StellaOps.Scheduler.Models + Storage.Mongo) ~1w
- Tasks: SCHED-MODELS-16-101 (3d), -102 (2d), -103 (2d); SCHED-STORAGE-16-201 (3d), -202 (2d), -203 (2d)
- Acceptance: schedule CRUD latency <120ms; run retention TTL enforced.
- Gate: schema doc + integration tests passing.
### Group SP16-G2 — ImpactIndex & Queue (src/StellaOps.Scheduler.ImpactIndex + Queue + Bench) ~1.2w
- Tasks: SCHED-IMPACT-16-300 (2d, DOING), SCHED-IMPACT-16-301 (3d), -302 (3d), -303 (2d); SCHED-QUEUE-16-401..403 (each 2d); BENCH-IMPACT-16-001 (2d)
- Acceptance: impact resolve 10k productKeys <300ms 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.8w
- Tasks: SCHED-WEB-16-101..104 (each 2d)
- Acceptance: preview endpoint <250ms; webhook security enforced.
- Gate: OpenAPI published; dry-run JSON fixtures stored.
### Group SP16-G4 — Scheduler Worker (src/StellaOps.Scheduler.Worker) ~1w
- Tasks: SCHED-WORKER-16-201 (3d), -202 (2d), -203 (3d), -204 (2d), -205 (2d)
- 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.5w)
### Group SP17-G1 — Scanner Forensics (src/StellaOps.Scanner.Emit + WebService) ~1.2w
- Tasks: SCANNER-EMIT-17-701 (4d), SCANNER-RUNTIME-17-401 (3d)
- Acceptance: forensic overlays add 150ms 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.6w
- Tasks: ZASTAVA-OBS-17-005 (3d)
- 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.4w
- Tasks: DEVOPS-REL-17-002 (2d)
- Acceptance: deterministic build verifier job updated to include forensics artifacts.
- Gate: CI pipeline stage `forensics-verify` green.
### Group SP17-G4 — Documentation (docs/) ~0.3w
- Tasks: DOCS-RUNTIME-17-004 (2d)
- Acceptance: runtime forensic guide published with troubleshooting.
- Gate: docs review sign-off; links added to UI help.
---
## Integration Buffers
- **INT-A (0.3w, after SP10):** Image SBOM BOM-Index Scheduler preview UI dry-run using fixtures.
- **INT-B (0.3w, 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 > 8GB in bench |
| R2 | Buildx plugin latency regression | BuildX Guild | DEVOPS-PERF-10-001 guard; fallback to post-build scan | Buildx job >300ms/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 >150ms/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) ~1w
- Tasks: POLICY-CORE-09-001 (2d) ✅, -002 (3d) ✅, -003 (3d) ✅, -004 (3d), -005 (4d), -006 (2d)
- Acceptance: policy parsing 200 files/s; preview diff response <200ms 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.4w
- Tasks: DEVOPS-HELM-09-001 (3d) **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.4w
- Tasks: DOCS-ADR-09-001 (2d), DOCS-EVENTS-09-002 (2d)
- 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, ~4w)
### Group SP10-G1 — OS Analyzer Plug-ins (src/StellaOps.Scanner.Analyzers.OS) ~1w
- Tasks: SCANNER-ANALYZERS-OS-10-201..207 (durations 23d each)
- Acceptance: analyzer runtime <1.5s/image; memory <250MB.
- 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.5w
- Tasks: SCANNER-ANALYZERS-LANG-10-301..309
- Acceptance: Node analyzer handles 10k modules <2s; Python memory <200MB.
- Gate: golden outputs stored; plugin manifests present.
### Group SP10-G3 — EntryTrace Plug-ins (src/StellaOps.Scanner.EntryTrace) ~0.8w
- 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) ~1w
- Tasks: SCANNER-DIFF-10-501..503, SCANNER-EMIT-10-601..606
- Acceptance: BOM-Index emission <500ms/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.6w
- Tasks: SCANNER-CACHE-10-101..104
- Acceptance: cache hit instrumentation validated; eviction keeps footprint <5GB.
- Gate: cache configuration doc; integration test `LayerCacheRoundTrip` green.
### Group SP10-G6 — Benchmarks & Samples (bench/, samples/, ops/devops) ~0.6w
- Tasks: BENCH-SCANNER-10-001 (2d), SAMPLES-10-001 (finish 3d), DEVOPS-PERF-10-001 (2d)
- Acceptance: analyzer benchmark CSV published; perf CI guard ensures SBOM compose <5s; sample SBOM/BOM-Index committed.
- Gate: bench results stored under `bench/`; `samples/` populated; CI job added.
---
## Sprint 11 Signing Chain Bring-up (ID: SP11, ~3w)
### Group SP11-G1 — Authority Sender Constraints (src/StellaOps.Authority) ~0.8w
- Tasks: AUTH-DPOP-11-001 (3d), AUTH-MTLS-11-002 (2d)
- Acceptance: DPoP nonce dance validated; mTLS tokens issued in 40ms.
- Gate: updated Authority OpenAPI; QA scripts verifying DPoP/mTLS.
### Group SP11-G2 — Signer Service (src/StellaOps.Signer) ~1.2w
- Tasks: SIGNER-API-11-101 (4d), SIGNER-REF-11-102 (2d), SIGNER-QUOTA-11-103 (2d)
- Acceptance: signing throughput 30 req/min; p95 latency 200ms.
- 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) ~1w
- Tasks: ATTESTOR-API-11-201 (3d), ATTESTOR-VERIFY-11-202 (2d), ATTESTOR-OBS-11-203 (2d)
- Acceptance: inclusion proof retrieval <500ms; audit log coverage 100%.
- Gate: Attestor API doc + verification script.
### Group SP11-G4 — UI Attestation Hooks (src/StellaOps.UI) ~0.4w
- Tasks: UI-ATTEST-11-005 (3d)
- Acceptance: attestation panel renders within 200ms; Rekor link verified.
- Gate SP11-G4 SP13-G1: recorded UX walkthrough.
---
## Sprint 12 Runtime Guardrails (ID: SP12, ~3w)
### Group SP12-G1 — Zastava Core (src/StellaOps.Zastava.Core) ~0.8w
- 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.8w
- Tasks: ZASTAVA-OBS-12-001..004
- Acceptance: observer memory <200MB; event flush 2s.
- Gate: sample runtime events stored; offline buffer test passes.
### Group SP12-G3 — Zastava Webhook (src/StellaOps.Zastava.Webhook) ~0.6w
- Tasks: ZASTAVA-WEBHOOK-12-101..103
- Acceptance: admission latency p95 45ms; cache TTL adhered to.
- Gate: TLS rotation procedure documented; readiness probe script.
### Group SP12-G4 — Scanner Runtime APIs (src/StellaOps.Scanner.WebService) ~0.8w
- Tasks: SCANNER-RUNTIME-12-301 (2d), SCANNER-RUNTIME-12-302 (3d)
- 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, ~2w)
### Group SP13-G1 — UI Shell & Panels (src/StellaOps.UI) ~1.6w
- Tasks: UI-AUTH-13-001 (3d), UI-SCANS-13-002 (4d), UI-VEX-13-003 (3d), UI-ADMIN-13-004 (2d), UI-SCHED-13-005 (3d), UI-NOTIFY-13-006 (3d)
- 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.8w
- Tasks: CLI-RUNTIME-13-005 (3d), CLI-OFFLINE-13-006 (3d), CLI-PLUGIN-13-007 (2d)
- Acceptance: runtime policy CLI completes <1s 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, ~2w)
### Group SP14-G1 — Release Automation (ops/devops) ~0.8w
- Tasks: DEVOPS-REL-14-001 (4d)
- 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.6w
- Tasks: DEVOPS-OFFLINE-14-002 (3d)
- Acceptance: kit import <5min with integrity verification CLI.
- Gate: kit doc updated; import script included.
### Group SP14-G3 — Deployment Playbooks (ops/deployment) ~0.4w
- Tasks: DEVOPS-OPS-14-003 (2d)
- Acceptance: rollback drill recorded; compatibility matrix produced.
- Gate: playbook PR merged with Ops sign-off.
### Group SP14-G4 — Licensing Token Service (ops/licensing) ~0.4w
- Tasks: DEVOPS-LIC-14-004 (2d)
- Acceptance: token service handles 100 req/min; revocation latency <60s.
- Gate: monitoring dashboard links; failover doc.
---
## Sprint 15 Notify Foundations (ID: SP15, ~3w)
### Group SP15-G1 — Models & Storage (src/StellaOps.Notify.Models + Storage.Mongo) ~0.8w
- Tasks: NOTIFY-MODELS-15-101 (2d), -102 (2d), -103 (1d); NOTIFY-STORAGE-15-201 (3d), -202 (2d), -203 (1d)
- Acceptance: rule CRUD latency <120ms; delivery retention job verified.
- Gate: schema docs + fixtures published.
### Group SP15-G2 — Engine & Queue (src/StellaOps.Notify.Engine + Queue) ~0.8w
- 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.8w
- Tasks: NOTIFY-WEB-15-101..104, NOTIFY-WORKER-15-201..204
- Acceptance: API p95 <120ms; worker delivery success 99%.
- Gate: end-to-end fixture run producing delivery record.
### Group SP15-G4 — Channel Plug-ins (src/StellaOps.Notify.Connectors.*) ~0.6w
- 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.5w
- Tasks: SCANNER-EVENTS-15-201 (2d), BENCH-NOTIFY-15-001 (2d)
- Acceptance: event emission latency <100ms; throughput bench results stored.
- Gate: `docs/events/samples/` contains sample payloads; bench CSV in repo.
---
## Sprint 16 Scheduler Intelligence (ID: SP16, ~4w)
### Group SP16-G1 — Models & Storage (src/StellaOps.Scheduler.Models + Storage.Mongo) ~1w
- Tasks: SCHED-MODELS-16-101 (3d), -102 (2d), -103 (2d); SCHED-STORAGE-16-201 (3d), -202 (2d), -203 (2d)
- Acceptance: schedule CRUD latency <120ms; run retention TTL enforced.
- Gate: schema doc + integration tests passing.
### Group SP16-G2 — ImpactIndex & Queue (src/StellaOps.Scheduler.ImpactIndex + Queue + Bench) ~1.2w
- Tasks: SCHED-IMPACT-16-300 (2d, DOING), SCHED-IMPACT-16-301 (3d), -302 (3d), -303 (2d); SCHED-QUEUE-16-401..403 (each 2d); BENCH-IMPACT-16-001 (2d)
- Acceptance: impact resolve 10k productKeys <300ms 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.8w
- Tasks: SCHED-WEB-16-101..104 (each 2d)
- Acceptance: preview endpoint <250ms; webhook security enforced.
- Gate: OpenAPI published; dry-run JSON fixtures stored.
### Group SP16-G4 — Scheduler Worker (src/StellaOps.Scheduler.Worker) ~1w
- Tasks: SCHED-WORKER-16-201 (3d), -202 (2d), -203 (3d), -204 (2d), -205 (2d)
- 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.5w)
### Group SP17-G1 — Scanner Forensics (src/StellaOps.Scanner.Emit + WebService) ~1.2w
- Tasks: SCANNER-EMIT-17-701 (4d), SCANNER-RUNTIME-17-401 (3d)
- Acceptance: forensic overlays add 150ms 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.6w
- Tasks: ZASTAVA-OBS-17-005 (3d)
- 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.4w
- Tasks: DEVOPS-REL-17-002 (2d)
- Acceptance: deterministic build verifier job updated to include forensics artifacts.
- Gate: CI pipeline stage `forensics-verify` green.
### Group SP17-G4 — Documentation (docs/) ~0.3w
- Tasks: DOCS-RUNTIME-17-004 (2d)
- Acceptance: runtime forensic guide published with troubleshooting.
- Gate: docs review sign-off; links added to UI help.
---
## Integration Buffers
- **INT-A (0.3w, after SP10):** Image SBOM BOM-Index Scheduler preview UI dry-run using fixtures.
- **INT-B (0.3w, 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 > 8GB in bench |
| R2 | Buildx plugin latency regression | BuildX Guild | DEVOPS-PERF-10-001 guard; fallback to post-build scan | Buildx job >300ms/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 >150ms/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.

View File

@@ -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 — BOMIndex sidecar
```

View File

@@ -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.
---

View File

@@ -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": ""
}
}
}

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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<EntryTraceAnalyzer>.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<string, string>.Empty,
ImmutableArray.Create("/usr/bin", "/usr/local/bin"),
"/",
"sha256:image",
"scan-entrytrace-1",
NullLogger.Instance);
var spec = EntrypointSpecification.FromExecForm(new[] { "/entrypoint.sh" }, Array.Empty<string>());
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<string, string>.Empty,
ImmutableArray.Create("/bin"),
"/",
"sha256:image",
"scan-entrytrace-2",
NullLogger.Instance);
var spec = EntrypointSpecification.FromExecForm(new[] { "/entrypoint.sh" }, Array.Empty<string>());
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<string, string>.Empty,
ImmutableArray.Create("/usr/bin"),
"/",
"sha256:image",
"scan-entrytrace-3",
NullLogger.Instance);
var spec = EntrypointSpecification.FromExecForm(new[] { "/entrypoint.sh" }, Array.Empty<string>());
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());
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Scanner.EntryTrace/StellaOps.Scanner.EntryTrace.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -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<string, FileEntry> _entries = new(StringComparer.Ordinal);
private readonly HashSet<string> _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<string> 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<RootFileDescriptor> EnumerateDirectory(string path)
{
var normalized = Normalize(path);
var builder = ImmutableArray.CreateBuilder<RootFileDescriptor>();
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<string>();
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();
}
}

View File

@@ -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.

View File

@@ -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<long> _resolutions;
private readonly Counter<long> _unresolved;
public EntryTraceMetrics()
{
_resolutions = EntryTraceInstrumentation.Meter.CreateCounter<long>(
"entrytrace_resolutions_total",
description: "Number of entry trace attempts by outcome.");
_unresolved = EntryTraceInstrumentation.Meter.CreateCounter<long>(
"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<string, object?>[] CreateTags(string imageDigest, string scanId, params (string Key, object? Value)[] extras)
{
var tags = new List<KeyValuePair<string, object?>>(2 + extras.Length)
{
new("image", imageDigest),
new("scan.id", scanId)
};
foreach (var extra in extras)
{
tags.Add(new KeyValuePair<string, object?>(extra.Key, extra.Value));
}
return tags.ToArray();
}
}

View File

@@ -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<EntryTraceAnalyzer> _logger;
public EntryTraceAnalyzer(
IOptions<EntryTraceAnalyzerOptions> options,
EntryTraceMetrics metrics,
ILogger<EntryTraceAnalyzer> 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<EntryTraceGraph> 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<string> _pathEntries;
private readonly List<EntryTraceNode> _nodes = new();
private readonly List<EntryTraceEdge> _edges = new();
private readonly List<EntryTraceDiagnostic> _diagnostics = new();
private readonly HashSet<string> _visitedScripts = new(StringComparer.Ordinal);
private readonly HashSet<string> _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<string> 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<string>.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<string> 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<string>.Empty;
}
private void ResolveCommand(
ImmutableArray<string> 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<string>(), 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<string, string>
{
["command"] = commandName
});
return true;
}
descriptor = null!;
return false;
}
private bool TryFollowInterpreter(
EntryTraceNode node,
RootFileDescriptor descriptor,
ImmutableArray<string> 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<string> 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<string> 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<string, string>
{
["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<string>.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<string> 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<string>.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<string> 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<string>(), 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<string>.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<string, string>
{
["class"] = mainClass
}));
return true;
}
return false;
}
private bool TryFollowShell(
EntryTraceNode node,
RootFileDescriptor descriptor,
ImmutableArray<string> 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<string>.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<string>.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<string> 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<string>.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<string>.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<string>.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<string> MaterializeArguments(ImmutableArray<ShellToken> tokens)
{
var builder = ImmutableArray.CreateBuilder<string>(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<string> 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<string>();
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);
}
}
}

View File

@@ -0,0 +1,26 @@
namespace StellaOps.Scanner.EntryTrace;
public sealed class EntryTraceAnalyzerOptions
{
public const string SectionName = "Scanner:Analyzers:EntryTrace";
/// <summary>
/// Maximum recursion depth while following includes/run-parts/interpreters.
/// </summary>
public int MaxDepth { get; set; } = 64;
/// <summary>
/// Enables traversal of run-parts directories.
/// </summary>
public bool FollowRunParts { get; set; } = true;
/// <summary>
/// Colon-separated default PATH string used when the environment omits PATH.
/// </summary>
public string DefaultPath { get; set; } = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
/// <summary>
/// Maximum number of scripts considered per run-parts directory to prevent explosion.
/// </summary>
public int RunPartsLimit { get; set; } = 64;
}

View File

@@ -0,0 +1,16 @@
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
namespace StellaOps.Scanner.EntryTrace;
/// <summary>
/// Provides runtime context for entry trace analysis.
/// </summary>
public sealed record EntryTraceContext(
IRootFileSystem FileSystem,
ImmutableDictionary<string, string> Environment,
ImmutableArray<string> Path,
string WorkingDirectory,
string ImageDigest,
string ScanId,
ILogger? Logger);

View File

@@ -0,0 +1,125 @@
using System.Collections.Generic;
using System.Collections.Immutable;
namespace StellaOps.Scanner.EntryTrace;
/// <summary>
/// Outcome classification for entrypoint resolution attempts.
/// </summary>
public enum EntryTraceOutcome
{
Resolved,
PartiallyResolved,
Unresolved
}
/// <summary>
/// Logical classification for nodes in the entry trace graph.
/// </summary>
public enum EntryTraceNodeKind
{
Command,
Script,
Include,
Interpreter,
Executable,
RunPartsDirectory,
RunPartsScript
}
/// <summary>
/// Interpreter categories supported by the analyzer.
/// </summary>
public enum EntryTraceInterpreterKind
{
None,
Python,
Node,
Java
}
/// <summary>
/// Diagnostic severity levels emitted by the analyzer.
/// </summary>
public enum EntryTraceDiagnosticSeverity
{
Info,
Warning,
Error
}
/// <summary>
/// Enumerates the canonical reasons for unresolved edges.
/// </summary>
public enum EntryTraceUnknownReason
{
CommandNotFound,
MissingFile,
DynamicEnvironmentReference,
UnsupportedSyntax,
RecursionLimitReached,
InterpreterNotSupported,
ModuleNotFound,
JarNotFound,
RunPartsEmpty,
PermissionDenied
}
/// <summary>
/// Represents a span within a script file.
/// </summary>
public readonly record struct EntryTraceSpan(
string? Path,
int StartLine,
int StartColumn,
int EndLine,
int EndColumn);
/// <summary>
/// Evidence describing where a node originated from within the image.
/// </summary>
public sealed record EntryTraceEvidence(
string Path,
string? LayerDigest,
string Source,
IReadOnlyDictionary<string, string>? Metadata);
/// <summary>
/// Represents a node in the entry trace graph.
/// </summary>
public sealed record EntryTraceNode(
int Id,
EntryTraceNodeKind Kind,
string DisplayName,
ImmutableArray<string> Arguments,
EntryTraceInterpreterKind InterpreterKind,
EntryTraceEvidence? Evidence,
EntryTraceSpan? Span);
/// <summary>
/// Represents a directed edge in the entry trace graph.
/// </summary>
public sealed record EntryTraceEdge(
int FromNodeId,
int ToNodeId,
string Relationship,
IReadOnlyDictionary<string, string>? Metadata);
/// <summary>
/// Captures diagnostic information regarding resolution gaps.
/// </summary>
public sealed record EntryTraceDiagnostic(
EntryTraceDiagnosticSeverity Severity,
EntryTraceUnknownReason Reason,
string Message,
EntryTraceSpan? Span,
string? RelatedPath);
/// <summary>
/// Final graph output produced by the analyzer.
/// </summary>
public sealed record EntryTraceGraph(
EntryTraceOutcome Outcome,
ImmutableArray<EntryTraceNode> Nodes,
ImmutableArray<EntryTraceEdge> Edges,
ImmutableArray<EntryTraceDiagnostic> Diagnostics);

View File

@@ -0,0 +1,71 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.EntryTrace;
/// <summary>
/// Represents the combined Docker ENTRYPOINT/CMD contract provided to the analyzer.
/// </summary>
public sealed record EntrypointSpecification
{
private EntrypointSpecification(
ImmutableArray<string> entrypoint,
ImmutableArray<string> command,
string? entrypointShell,
string? commandShell)
{
Entrypoint = entrypoint;
Command = command;
EntrypointShell = string.IsNullOrWhiteSpace(entrypointShell) ? null : entrypointShell;
CommandShell = string.IsNullOrWhiteSpace(commandShell) ? null : commandShell;
}
/// <summary>
/// Exec-form ENTRYPOINT arguments.
/// </summary>
public ImmutableArray<string> Entrypoint { get; }
/// <summary>
/// Exec-form CMD arguments.
/// </summary>
public ImmutableArray<string> Command { get; }
/// <summary>
/// Shell-form ENTRYPOINT (if provided).
/// </summary>
public string? EntrypointShell { get; }
/// <summary>
/// Shell-form CMD (if provided).
/// </summary>
public string? CommandShell { get; }
public static EntrypointSpecification FromExecForm(
IEnumerable<string>? entrypoint,
IEnumerable<string>? command)
=> new(
entrypoint is null ? ImmutableArray<string>.Empty : entrypoint.ToImmutableArray(),
command is null ? ImmutableArray<string>.Empty : command.ToImmutableArray(),
entrypointShell: null,
commandShell: null);
public static EntrypointSpecification FromShellForm(
string? entrypoint,
string? command)
=> new(
ImmutableArray<string>.Empty,
ImmutableArray<string>.Empty,
entrypoint,
command);
public EntrypointSpecification WithCommand(IEnumerable<string>? command)
=> new(Entrypoint, command?.ToImmutableArray() ?? ImmutableArray<string>.Empty, EntrypointShell, CommandShell);
public EntrypointSpecification WithCommandShell(string? commandShell)
=> new(Entrypoint, Command, EntrypointShell, commandShell);
public EntrypointSpecification WithEntrypoint(IEnumerable<string>? entrypoint)
=> new(entrypoint?.ToImmutableArray() ?? ImmutableArray<string>.Empty, Command, EntrypointShell, CommandShell);
public EntrypointSpecification WithEntrypointShell(string? entrypointShell)
=> new(Entrypoint, Command, entrypointShell, CommandShell);
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.EntryTrace;
/// <summary>
/// Represents a layered read-only filesystem snapshot built from container layers.
/// </summary>
public interface IRootFileSystem
{
/// <summary>
/// Attempts to resolve an executable by name using the provided PATH entries.
/// </summary>
bool TryResolveExecutable(string name, IReadOnlyList<string> searchPaths, out RootFileDescriptor descriptor);
/// <summary>
/// Attempts to read the contents of a file as UTF-8 text.
/// </summary>
bool TryReadAllText(string path, out RootFileDescriptor descriptor, out string content);
/// <summary>
/// Returns descriptors for entries contained within a directory.
/// </summary>
ImmutableArray<RootFileDescriptor> EnumerateDirectory(string path);
/// <summary>
/// Checks whether a directory exists.
/// </summary>
bool DirectoryExists(string path);
}
/// <summary>
/// Describes a file discovered within the layered filesystem.
/// </summary>
public sealed record RootFileDescriptor(
string Path,
string? LayerDigest,
bool IsExecutable,
bool IsDirectory,
string? ShebangInterpreter);

View File

@@ -0,0 +1,9 @@
namespace StellaOps.Scanner.EntryTrace;
public interface IEntryTraceAnalyzer
{
ValueTask<EntryTraceGraph> ResolveAsync(
EntrypointSpecification entrypoint,
EntryTraceContext context,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,54 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.EntryTrace.Parsing;
public abstract record ShellNode(ShellSpan Span);
public sealed record ShellScript(ImmutableArray<ShellNode> Nodes);
public sealed record ShellSpan(int StartLine, int StartColumn, int EndLine, int EndColumn);
public sealed record ShellCommandNode(
string Command,
ImmutableArray<ShellToken> Arguments,
ShellSpan Span) : ShellNode(Span);
public sealed record ShellIncludeNode(
string PathExpression,
ImmutableArray<ShellToken> Arguments,
ShellSpan Span) : ShellNode(Span);
public sealed record ShellExecNode(
ImmutableArray<ShellToken> Arguments,
ShellSpan Span) : ShellNode(Span);
public sealed record ShellIfNode(
ImmutableArray<ShellConditionalBranch> Branches,
ShellSpan Span) : ShellNode(Span);
public sealed record ShellConditionalBranch(
ShellConditionalKind Kind,
ImmutableArray<ShellNode> Body,
ShellSpan Span,
string? PredicateSummary);
public enum ShellConditionalKind
{
If,
Elif,
Else
}
public sealed record ShellCaseNode(
ImmutableArray<ShellCaseArm> Arms,
ShellSpan Span) : ShellNode(Span);
public sealed record ShellCaseArm(
ImmutableArray<string> Patterns,
ImmutableArray<ShellNode> Body,
ShellSpan Span);
public sealed record ShellRunPartsNode(
string DirectoryExpression,
ImmutableArray<ShellToken> Arguments,
ShellSpan Span) : ShellNode(Span);

View File

@@ -0,0 +1,485 @@
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;
namespace StellaOps.Scanner.EntryTrace.Parsing;
/// <summary>
/// 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.
/// </summary>
public sealed class ShellParser
{
private readonly IReadOnlyList<ShellToken> _tokens;
private int _index;
private ShellParser(IReadOnlyList<ShellToken> 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<ShellNode> ParseNodes(HashSet<string>? untilKeywords)
{
var nodes = new List<ShellNode>();
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<ShellToken>.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<ShellConditionalBranch>();
var predicateSummary = JoinTokens(predicateTokens);
var thenNodes = ParseNodes(new HashSet<string>(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<string>(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<string>(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<ShellCaseArm>();
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<string>(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<string> Values, ShellToken? FirstToken) ReadPatterns()
{
var values = new List<string>();
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<ShellToken> ReadUntilTerminator()
{
var tokens = new List<ShellToken>();
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<ShellToken> ReadUntilKeyword(string keyword)
{
var tokens = new List<ShellToken>();
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<ShellToken> 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<ShellToken> 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<ShellToken> 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;
}
}

View File

@@ -0,0 +1,16 @@
namespace StellaOps.Scanner.EntryTrace.Parsing;
/// <summary>
/// Token produced by the shell lexer.
/// </summary>
public readonly record struct ShellToken(ShellTokenKind Kind, string Value, int Line, int Column);
public enum ShellTokenKind
{
Word,
SingleQuoted,
DoubleQuoted,
Operator,
NewLine,
EndOfFile
}

View File

@@ -0,0 +1,200 @@
using System.Globalization;
using System.Text;
namespace StellaOps.Scanner.EntryTrace.Parsing;
/// <summary>
/// Lightweight Bourne shell tokenizer sufficient for ENTRYPOINT scripts.
/// Deterministic: emits tokens in source order without normalization.
/// </summary>
public sealed class ShellTokenizer
{
public IReadOnlyList<ShellToken> Tokenize(string source)
{
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
var tokens = new List<ShellToken>();
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);
}
}

View File

@@ -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<EntryTraceAnalyzerOptions>? configure = null)
{
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddOptions<EntryTraceAnalyzerOptions>()
.BindConfiguration(EntryTraceAnalyzerOptions.SectionName);
if (configure is not null)
{
services.Configure(configure);
}
services.TryAddSingleton<EntryTraceMetrics>();
services.TryAddSingleton<IEntryTraceAnalyzer, EntryTraceAnalyzer>();
return services;
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
</ItemGroup>
</Project>

View File

@@ -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`. |

View File

@@ -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;
}
}
}

View File

@@ -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"
}
}

View File

@@ -1,11 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Scanner.Sbomer.BuildXPlugin\StellaOps.Scanner.Sbomer.BuildXPlugin.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\descriptor.baseline.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -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;
/// <summary>
/// Builds immutable OCI descriptors enriched with provenance placeholders.
/// </summary>
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<DescriptorDocument> 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;
/// <summary>
/// Builds immutable OCI descriptors enriched with provenance placeholders.
/// </summary>
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<DescriptorDocument> 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<string> 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<byte> hash = stackalloc byte[32];
SHA256.HashData(bytes, hash);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
SHA256.HashData(payload, hash);
Span<byte> nonceBytes = stackalloc byte[16];
hash[..16].CopyTo(nonceBytes);
return Convert.ToHexString(nonceBytes).ToLowerInvariant();
}
private static IReadOnlyDictionary<string, string> BuildArtifactAnnotations(DescriptorRequest request, string nonce, string expectedDsse)
{
var annotations = new Dictionary<string, string>(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<string, string> BuildDocumentMetadata(DescriptorRequest request, FileInfo fileInfo, string sbomDigest)
{
var metadata = new Dictionary<string, string>(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<string> 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<byte> hash = stackalloc byte[32];
SHA256.HashData(bytes, hash);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
private static IReadOnlyDictionary<string, string> BuildArtifactAnnotations(DescriptorRequest request, string nonce, string expectedDsse)
{
var annotations = new Dictionary<string, string>(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<string, string> BuildDocumentMetadata(DescriptorRequest request, FileInfo fileInfo, string sbomDigest)
{
var metadata = new Dictionary<string, string>(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;
}
}

View File

@@ -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 5s 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. |

View File

@@ -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.

View File

@@ -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");
}

View File

@@ -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<double> _queueLatencyMs;
private readonly Histogram<double> _jobDurationMs;
private readonly Histogram<double> _stageDurationMs;
private readonly Counter<long> _jobsCompleted;
private readonly Counter<long> _jobsFailed;
public ScannerWorkerMetrics()
{
_queueLatencyMs = ScannerWorkerInstrumentation.Meter.CreateHistogram<double>(
"scanner_worker_queue_latency_ms",
unit: "ms",
description: "Time from job enqueue to lease acquisition.");
_jobDurationMs = ScannerWorkerInstrumentation.Meter.CreateHistogram<double>(
"scanner_worker_job_duration_ms",
unit: "ms",
description: "Total processing duration per job.");
_stageDurationMs = ScannerWorkerInstrumentation.Meter.CreateHistogram<double>(
"scanner_worker_stage_duration_ms",
unit: "ms",
description: "Stage execution duration per job.");
_jobsCompleted = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
"scanner_worker_jobs_completed_total",
description: "Number of successfully completed scan jobs.");
_jobsFailed = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
"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<string, object?>[] CreateTags(ScanJobContext context, string? stage = null, string? failureReason = null)
{
var tags = new List<KeyValuePair<string, object?>>(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<string, object?>("queue", queueName));
}
if (context.Lease.Metadata.TryGetValue("job.kind", out var jobKind) && !string.IsNullOrWhiteSpace(jobKind))
{
tags.Add(new KeyValuePair<string, object?>("job.kind", jobKind));
}
if (!string.IsNullOrWhiteSpace(stage))
{
tags.Add(new KeyValuePair<string, object?>("stage", stage));
}
if (!string.IsNullOrWhiteSpace(failureReason))
{
tags.Add(new KeyValuePair<string, object?>("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<double> _queueLatencyMs;
private readonly Histogram<double> _jobDurationMs;
private readonly Histogram<double> _stageDurationMs;
private readonly Counter<long> _jobsCompleted;
private readonly Counter<long> _jobsFailed;
public ScannerWorkerMetrics()
{
_queueLatencyMs = ScannerWorkerInstrumentation.Meter.CreateHistogram<double>(
"scanner_worker_queue_latency_ms",
unit: "ms",
description: "Time from job enqueue to lease acquisition.");
_jobDurationMs = ScannerWorkerInstrumentation.Meter.CreateHistogram<double>(
"scanner_worker_job_duration_ms",
unit: "ms",
description: "Total processing duration per job.");
_stageDurationMs = ScannerWorkerInstrumentation.Meter.CreateHistogram<double>(
"scanner_worker_stage_duration_ms",
unit: "ms",
description: "Stage execution duration per job.");
_jobsCompleted = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
"scanner_worker_jobs_completed_total",
description: "Number of successfully completed scan jobs.");
_jobsFailed = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
"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<string, object?>[] CreateTags(ScanJobContext context, string? stage = null, string? failureReason = null)
{
var tags = new List<KeyValuePair<string, object?>>(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<string, object?>("queue", queueName));
}
if (context.Lease.Metadata.TryGetValue("job.kind", out var jobKind) && !string.IsNullOrWhiteSpace(jobKind))
{
tags.Add(new KeyValuePair<string, object?>("job.kind", jobKind));
}
if (!string.IsNullOrWhiteSpace(stage))
{
tags.Add(new KeyValuePair<string, object?>("stage", stage));
}
if (!string.IsNullOrWhiteSpace(failureReason))
{
tags.Add(new KeyValuePair<string, object?>("reason", failureReason));
}
return tags.ToArray();
}
}

View File

@@ -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<string, object>("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<string, object>(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<string, object>("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<string, object>(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();
}
}
}

View File

@@ -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<ScannerWorkerOptions> _options;
private readonly ILogger<ScannerWorkerHostedService> _logger;
private readonly IDelayScheduler _delayScheduler;
public ScannerWorkerHostedService(
IScanJobSource jobSource,
ScanJobProcessor processor,
LeaseHeartbeatService heartbeatService,
ScannerWorkerMetrics metrics,
TimeProvider timeProvider,
IDelayScheduler delayScheduler,
IOptionsMonitor<ScannerWorkerOptions> options,
ILogger<ScannerWorkerHostedService> 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<Task>();
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<ScannerWorkerOptions> _options;
private readonly ILogger<ScannerWorkerHostedService> _logger;
private readonly IDelayScheduler _delayScheduler;
public ScannerWorkerHostedService(
IScanJobSource jobSource,
ScanJobProcessor processor,
LeaseHeartbeatService heartbeatService,
ScannerWorkerMetrics metrics,
TimeProvider timeProvider,
IDelayScheduler delayScheduler,
IOptionsMonitor<ScannerWorkerOptions> options,
ILogger<ScannerWorkerHostedService> 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<Task>();
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);
}

View File

@@ -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<TimeSpan> HeartbeatRetryDelays => _heartbeatRetryDelays;
public TimeSpan MinHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(10);
public TimeSpan MaxHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(30);
public void SetHeartbeatRetryDelays(IEnumerable<TimeSpan> delays)
{
_heartbeatRetryDelays = NormalizeDelays(delays);
}
internal IReadOnlyList<TimeSpan> NormalizedHeartbeatRetryDelays => _heartbeatRetryDelays;
private static IReadOnlyList<TimeSpan> NormalizeDelays(IEnumerable<TimeSpan> delays)
{
var buffer = new List<TimeSpan>();
foreach (var delay in delays)
{
if (delay <= TimeSpan.Zero)
{
continue;
}
buffer.Add(delay);
}
buffer.Sort();
return new ReadOnlyCollection<TimeSpan>(buffer);
}
private IReadOnlyList<TimeSpan> _heartbeatRetryDelays = new ReadOnlyCollection<TimeSpan>(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<string> Scopes { get; } = new List<string> { "scanner.scan" };
public ResilienceOptions Resilience { get; } = new();
}
public sealed class ResilienceOptions
{
public bool? EnableRetries { get; set; }
public IList<TimeSpan> RetryDelays { get; } = new List<TimeSpan>
{
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<string, string?> ResourceAttributes { get; } = new ConcurrentDictionary<string, string?>(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<TimeSpan> HeartbeatRetryDelays => _heartbeatRetryDelays;
public TimeSpan MinHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(10);
public TimeSpan MaxHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(30);
public void SetHeartbeatRetryDelays(IEnumerable<TimeSpan> delays)
{
_heartbeatRetryDelays = NormalizeDelays(delays);
}
internal IReadOnlyList<TimeSpan> NormalizedHeartbeatRetryDelays => _heartbeatRetryDelays;
private static IReadOnlyList<TimeSpan> NormalizeDelays(IEnumerable<TimeSpan> delays)
{
var buffer = new List<TimeSpan>();
foreach (var delay in delays)
{
if (delay <= TimeSpan.Zero)
{
continue;
}
buffer.Add(delay);
}
buffer.Sort();
return new ReadOnlyCollection<TimeSpan>(buffer);
}
private IReadOnlyList<TimeSpan> _heartbeatRetryDelays = new ReadOnlyCollection<TimeSpan>(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<string> Scopes { get; } = new List<string> { "scanner.scan" };
public ResilienceOptions Resilience { get; } = new();
}
public sealed class ResilienceOptions
{
public bool? EnableRetries { get; set; }
public IList<TimeSpan> RetryDelays { get; } = new List<TimeSpan>
{
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<string, string?> ResourceAttributes { get; } = new ConcurrentDictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
}
public sealed class ShutdownOptions
{
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
}
public sealed class AnalyzerOptions
{
public AnalyzerOptions()
{
PluginDirectories = new List<string>
{
Path.Combine("plugins", "scanner", "analyzers", "os"),
};
}
public IList<string> PluginDirectories { get; }
public string RootFilesystemMetadataKey { get; set; } = ScanMetadataKeys.RootFilesystemPath;
public string WorkspaceMetadataKey { get; set; } = ScanMetadataKeys.WorkspacePath;
}
}

View File

@@ -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<ScannerWorkerOptions>
{
public ValidateOptionsResult Validate(string? name, ScannerWorkerOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var failures = new List<string>();
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<ScannerWorkerOptions>
{
public ValidateOptionsResult Validate(string? name, ScannerWorkerOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var failures = new List<string>();
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<ScannerWork
}
}
if (string.IsNullOrWhiteSpace(options.Analyzers.RootFilesystemMetadataKey))
{
failures.Add("Scanner.Worker:Analyzers:RootFilesystemMetadataKey must be provided.");
}
return failures.Count == 0 ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail(failures);
}
}

View File

@@ -1,20 +1,20 @@
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);
}
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);
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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<string, string> 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<string, string> Metadata { get; }
ValueTask RenewAsync(CancellationToken cancellationToken);
ValueTask CompleteAsync(CancellationToken cancellationToken);
ValueTask AbandonAsync(string reason, CancellationToken cancellationToken);
ValueTask PoisonAsync(string reason, CancellationToken cancellationToken);
}

View File

@@ -1,9 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Scanner.Worker.Processing;
public interface IScanJobSource
{
Task<IScanJobLease?> TryAcquireAsync(CancellationToken cancellationToken);
}
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Scanner.Worker.Processing;
public interface IScanJobSource
{
Task<IScanJobLease?> TryAcquireAsync(CancellationToken cancellationToken);
}

View File

@@ -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);
}

View File

@@ -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<ScannerWorkerOptions> _options;
private readonly IDelayScheduler _delayScheduler;
private readonly ILogger<LeaseHeartbeatService> _logger;
public LeaseHeartbeatService(TimeProvider timeProvider, IDelayScheduler delayScheduler, IOptionsMonitor<ScannerWorkerOptions> options, ILogger<LeaseHeartbeatService> 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<ScannerWorkerOptions> _options;
private readonly IDelayScheduler _delayScheduler;
private readonly ILogger<LeaseHeartbeatService> _logger;
public LeaseHeartbeatService(TimeProvider timeProvider, IDelayScheduler delayScheduler, IOptionsMonitor<ScannerWorkerOptions> options, ILogger<LeaseHeartbeatService> 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<bool> 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;
}
}

View File

@@ -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;
}

View File

@@ -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<NullScanJobSource> _logger;
private int _logged;
public NullScanJobSource(ILogger<NullScanJobSource> logger)
{
_logger = logger;
}
public Task<IScanJobLease?> 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<IScanJobLease?>(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<NullScanJobSource> _logger;
private int _logged;
public NullScanJobSource(ILogger<NullScanJobSource> logger)
{
_logger = logger;
}
public Task<IScanJobLease?> 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<IScanJobLease?>(null);
}
}

View File

@@ -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<OsScanAnalyzerDispatcher> _logger;
private IReadOnlyList<string> _pluginDirectories = Array.Empty<string>();
public OsScanAnalyzerDispatcher(
IServiceScopeFactory scopeFactory,
OsAnalyzerPluginCatalog catalog,
IOptions<ScannerWorkerOptions> options,
ILogger<OsScanAnalyzerDispatcher> 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<string, string>(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<ILoggerFactory>();
var results = new List<OSPackageAnalyzerResult>(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<string>();
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<string>(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<string, string> 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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; }
}

View File

@@ -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<string, IScanStageExecutor> _executors;
private readonly ScanProgressReporter _progressReporter;
private readonly ILogger<ScanJobProcessor> _logger;
public ScanJobProcessor(IEnumerable<IScanStageExecutor> executors, ScanProgressReporter progressReporter, ILogger<ScanJobProcessor> logger)
{
_progressReporter = progressReporter ?? throw new ArgumentNullException(nameof(progressReporter));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
var map = new Dictionary<string, IScanStageExecutor>(StringComparer.OrdinalIgnoreCase);
foreach (var executor in executors ?? Array.Empty<IScanStageExecutor>())
{
if (executor is null || string.IsNullOrWhiteSpace(executor.StageName))
{
continue;
}
map[executor.StageName] = executor;
}
foreach (var stage in ScanStageNames.Ordered)
{
if (map.ContainsKey(stage))
{
continue;
}
map[stage] = new NoOpStageExecutor(stage);
_logger.LogDebug("No executor registered for stage {Stage}; using no-op placeholder.", stage);
}
_executors = map;
}
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
foreach (var stage in ScanStageNames.Ordered)
{
cancellationToken.ThrowIfCancellationRequested();
if (!_executors.TryGetValue(stage, out var executor))
{
continue;
}
await _progressReporter.ExecuteStageAsync(
context,
stage,
executor.ExecuteAsync,
cancellationToken).ConfigureAwait(false);
}
}
}
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace StellaOps.Scanner.Worker.Processing;
public sealed class ScanJobProcessor
{
private readonly IReadOnlyDictionary<string, IScanStageExecutor> _executors;
private readonly ScanProgressReporter _progressReporter;
private readonly ILogger<ScanJobProcessor> _logger;
public ScanJobProcessor(IEnumerable<IScanStageExecutor> executors, ScanProgressReporter progressReporter, ILogger<ScanJobProcessor> logger)
{
_progressReporter = progressReporter ?? throw new ArgumentNullException(nameof(progressReporter));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
var map = new Dictionary<string, IScanStageExecutor>(StringComparer.OrdinalIgnoreCase);
foreach (var executor in executors ?? Array.Empty<IScanStageExecutor>())
{
if (executor is null || string.IsNullOrWhiteSpace(executor.StageName))
{
continue;
}
map[executor.StageName] = executor;
}
foreach (var stage in ScanStageNames.Ordered)
{
if (map.ContainsKey(stage))
{
continue;
}
map[stage] = new NoOpStageExecutor(stage);
_logger.LogDebug("No executor registered for stage {Stage}; using no-op placeholder.", stage);
}
_executors = map;
}
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
foreach (var stage in ScanStageNames.Ordered)
{
cancellationToken.ThrowIfCancellationRequested();
if (!_executors.TryGetValue(stage, out var executor))
{
continue;
}
await _progressReporter.ExecuteStageAsync(
context,
stage,
executor.ExecuteAsync,
cancellationToken).ConfigureAwait(false);
}
}
}

View File

@@ -1,86 +1,86 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Worker.Diagnostics;
namespace StellaOps.Scanner.Worker.Processing;
public sealed partial class ScanProgressReporter
{
private readonly ScannerWorkerMetrics _metrics;
private readonly ILogger<ScanProgressReporter> _logger;
public ScanProgressReporter(ScannerWorkerMetrics metrics, ILogger<ScanProgressReporter> logger)
{
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async ValueTask ExecuteStageAsync(
ScanJobContext context,
string stageName,
Func<ScanJobContext, CancellationToken, ValueTask> stageWork,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentException.ThrowIfNullOrWhiteSpace(stageName);
ArgumentNullException.ThrowIfNull(stageWork);
StageStarting(_logger, context.JobId, context.ScanId, stageName, context.Lease.Attempt);
var start = context.TimeProvider.GetUtcNow();
using var activity = ScannerWorkerInstrumentation.ActivitySource.StartActivity(
$"scanner.worker.{stageName}",
ActivityKind.Internal);
activity?.SetTag("scanner.worker.job_id", context.JobId);
activity?.SetTag("scanner.worker.scan_id", context.ScanId);
activity?.SetTag("scanner.worker.stage", stageName);
try
{
await stageWork(context, cancellationToken).ConfigureAwait(false);
var duration = context.TimeProvider.GetUtcNow() - start;
_metrics.RecordStageDuration(context, stageName, duration);
StageCompleted(_logger, context.JobId, context.ScanId, stageName, duration.TotalMilliseconds);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
StageCancelled(_logger, context.JobId, context.ScanId, stageName);
throw;
}
catch (Exception ex)
{
var duration = context.TimeProvider.GetUtcNow() - start;
_metrics.RecordStageDuration(context, stageName, duration);
StageFailed(_logger, context.JobId, context.ScanId, stageName, ex);
throw;
}
}
[LoggerMessage(
EventId = 1000,
Level = LogLevel.Information,
Message = "Job {JobId} (scan {ScanId}) entering stage {Stage} (attempt {Attempt}).")]
private static partial void StageStarting(ILogger logger, string jobId, string scanId, string stage, int attempt);
[LoggerMessage(
EventId = 1001,
Level = LogLevel.Information,
Message = "Job {JobId} (scan {ScanId}) finished stage {Stage} in {ElapsedMs:F0} ms.")]
private static partial void StageCompleted(ILogger logger, string jobId, string scanId, string stage, double elapsedMs);
[LoggerMessage(
EventId = 1002,
Level = LogLevel.Warning,
Message = "Job {JobId} (scan {ScanId}) stage {Stage} cancelled by request.")]
private static partial void StageCancelled(ILogger logger, string jobId, string scanId, string stage);
[LoggerMessage(
EventId = 1003,
Level = LogLevel.Error,
Message = "Job {JobId} (scan {ScanId}) stage {Stage} failed.")]
private static partial void StageFailed(ILogger logger, string jobId, string scanId, string stage, Exception exception);
}
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Worker.Diagnostics;
namespace StellaOps.Scanner.Worker.Processing;
public sealed partial class ScanProgressReporter
{
private readonly ScannerWorkerMetrics _metrics;
private readonly ILogger<ScanProgressReporter> _logger;
public ScanProgressReporter(ScannerWorkerMetrics metrics, ILogger<ScanProgressReporter> logger)
{
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async ValueTask ExecuteStageAsync(
ScanJobContext context,
string stageName,
Func<ScanJobContext, CancellationToken, ValueTask> stageWork,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentException.ThrowIfNullOrWhiteSpace(stageName);
ArgumentNullException.ThrowIfNull(stageWork);
StageStarting(_logger, context.JobId, context.ScanId, stageName, context.Lease.Attempt);
var start = context.TimeProvider.GetUtcNow();
using var activity = ScannerWorkerInstrumentation.ActivitySource.StartActivity(
$"scanner.worker.{stageName}",
ActivityKind.Internal);
activity?.SetTag("scanner.worker.job_id", context.JobId);
activity?.SetTag("scanner.worker.scan_id", context.ScanId);
activity?.SetTag("scanner.worker.stage", stageName);
try
{
await stageWork(context, cancellationToken).ConfigureAwait(false);
var duration = context.TimeProvider.GetUtcNow() - start;
_metrics.RecordStageDuration(context, stageName, duration);
StageCompleted(_logger, context.JobId, context.ScanId, stageName, duration.TotalMilliseconds);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
StageCancelled(_logger, context.JobId, context.ScanId, stageName);
throw;
}
catch (Exception ex)
{
var duration = context.TimeProvider.GetUtcNow() - start;
_metrics.RecordStageDuration(context, stageName, duration);
StageFailed(_logger, context.JobId, context.ScanId, stageName, ex);
throw;
}
}
[LoggerMessage(
EventId = 1000,
Level = LogLevel.Information,
Message = "Job {JobId} (scan {ScanId}) entering stage {Stage} (attempt {Attempt}).")]
private static partial void StageStarting(ILogger logger, string jobId, string scanId, string stage, int attempt);
[LoggerMessage(
EventId = 1001,
Level = LogLevel.Information,
Message = "Job {JobId} (scan {ScanId}) finished stage {Stage} in {ElapsedMs:F0} ms.")]
private static partial void StageCompleted(ILogger logger, string jobId, string scanId, string stage, double elapsedMs);
[LoggerMessage(
EventId = 1002,
Level = LogLevel.Warning,
Message = "Job {JobId} (scan {ScanId}) stage {Stage} cancelled by request.")]
private static partial void StageCancelled(ILogger logger, string jobId, string scanId, string stage);
[LoggerMessage(
EventId = 1003,
Level = LogLevel.Error,
Message = "Job {JobId} (scan {ScanId}) stage {Stage} failed.")]
private static partial void StageFailed(ILogger logger, string jobId, string scanId, string stage, Exception exception);
}

View File

@@ -1,23 +1,23 @@
using System.Collections.Generic;
namespace StellaOps.Scanner.Worker.Processing;
public static class ScanStageNames
{
public const string ResolveImage = "resolve-image";
public const string PullLayers = "pull-layers";
public const string BuildFilesystem = "build-filesystem";
public const string ExecuteAnalyzers = "execute-analyzers";
public const string ComposeArtifacts = "compose-artifacts";
public const string EmitReports = "emit-reports";
public static readonly IReadOnlyList<string> Ordered = new[]
{
ResolveImage,
PullLayers,
BuildFilesystem,
ExecuteAnalyzers,
ComposeArtifacts,
EmitReports,
};
}
using System.Collections.Generic;
namespace StellaOps.Scanner.Worker.Processing;
public static class ScanStageNames
{
public const string ResolveImage = "resolve-image";
public const string PullLayers = "pull-layers";
public const string BuildFilesystem = "build-filesystem";
public const string ExecuteAnalyzers = "execute-analyzers";
public const string ComposeArtifacts = "compose-artifacts";
public const string EmitReports = "emit-reports";
public static readonly IReadOnlyList<string> Ordered = new[]
{
ResolveImage,
PullLayers,
BuildFilesystem,
ExecuteAnalyzers,
ComposeArtifacts,
EmitReports,
};
}

View File

@@ -1,18 +1,18 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Scanner.Worker.Processing;
public sealed class SystemDelayScheduler : IDelayScheduler
{
public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
{
if (delay <= TimeSpan.Zero)
{
return Task.CompletedTask;
}
return Task.Delay(delay, cancellationToken);
}
}
using System;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Scanner.Worker.Processing;
public sealed class SystemDelayScheduler : IDelayScheduler
{
public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
{
if (delay <= TimeSpan.Zero)
{
return Task.CompletedTask;
}
return Task.Delay(delay, cancellationToken);
}
}

View File

@@ -1,98 +1,103 @@
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Auth.Client;
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Auth.Client;
using StellaOps.Scanner.Analyzers.OS.Plugin;
using StellaOps.Scanner.EntryTrace;
using StellaOps.Scanner.Worker.Diagnostics;
using StellaOps.Scanner.Worker.Hosting;
using StellaOps.Scanner.Worker.Options;
using StellaOps.Scanner.Worker.Processing;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddOptions<ScannerWorkerOptions>()
.BindConfiguration(ScannerWorkerOptions.SectionName)
.ValidateOnStart();
builder.Services.AddSingleton<IValidateOptions<ScannerWorkerOptions>, ScannerWorkerOptionsValidator>();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<ScannerWorkerMetrics>();
builder.Services.AddSingleton<ScanProgressReporter>();
builder.Services.AddSingleton<ScanJobProcessor>();
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddOptions<ScannerWorkerOptions>()
.BindConfiguration(ScannerWorkerOptions.SectionName)
.ValidateOnStart();
builder.Services.AddSingleton<IValidateOptions<ScannerWorkerOptions>, ScannerWorkerOptionsValidator>();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<ScannerWorkerMetrics>();
builder.Services.AddSingleton<ScanProgressReporter>();
builder.Services.AddSingleton<ScanJobProcessor>();
builder.Services.AddSingleton<LeaseHeartbeatService>();
builder.Services.AddSingleton<IDelayScheduler, SystemDelayScheduler>();
builder.Services.AddEntryTraceAnalyzer();
builder.Services.TryAddSingleton<IScanJobSource, NullScanJobSource>();
builder.Services.TryAddSingleton<IScanAnalyzerDispatcher, NullScanAnalyzerDispatcher>();
builder.Services.AddSingleton<IScanStageExecutor, AnalyzerStageExecutor>();
builder.Services.AddSingleton<ScannerWorkerHostedService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<ScannerWorkerHostedService>());
var workerOptions = builder.Configuration.GetSection(ScannerWorkerOptions.SectionName).Get<ScannerWorkerOptions>() ?? new ScannerWorkerOptions();
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = workerOptions.Shutdown.Timeout;
});
builder.ConfigureScannerWorkerTelemetry(workerOptions);
if (workerOptions.Authority.Enabled)
{
builder.Services.AddStellaOpsAuthClient(clientOptions =>
{
clientOptions.Authority = workerOptions.Authority.Issuer?.Trim() ?? string.Empty;
clientOptions.ClientId = workerOptions.Authority.ClientId?.Trim() ?? string.Empty;
clientOptions.ClientSecret = workerOptions.Authority.ClientSecret;
clientOptions.EnableRetries = workerOptions.Authority.Resilience.EnableRetries ?? true;
clientOptions.HttpTimeout = TimeSpan.FromSeconds(workerOptions.Authority.BackchannelTimeoutSeconds);
clientOptions.DefaultScopes.Clear();
foreach (var scope in workerOptions.Authority.Scopes)
{
if (string.IsNullOrWhiteSpace(scope))
{
continue;
}
clientOptions.DefaultScopes.Add(scope);
}
clientOptions.RetryDelays.Clear();
foreach (var delay in workerOptions.Authority.Resilience.RetryDelays)
{
if (delay <= TimeSpan.Zero)
{
continue;
}
clientOptions.RetryDelays.Add(delay);
}
if (workerOptions.Authority.Resilience.AllowOfflineCacheFallback is bool allowOffline)
{
clientOptions.AllowOfflineCacheFallback = allowOffline;
}
if (workerOptions.Authority.Resilience.OfflineCacheTolerance is { } tolerance && tolerance > TimeSpan.Zero)
{
clientOptions.OfflineCacheTolerance = tolerance;
}
});
}
builder.Logging.Configure(options =>
{
options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId
| ActivityTrackingOptions.TraceId
| ActivityTrackingOptions.ParentId;
});
var host = builder.Build();
await host.RunAsync();
public partial class Program;
builder.Services.AddSingleton<OsAnalyzerPluginCatalog>();
builder.Services.AddSingleton<IScanAnalyzerDispatcher, OsScanAnalyzerDispatcher>();
builder.Services.AddSingleton<IScanStageExecutor, AnalyzerStageExecutor>();
builder.Services.AddSingleton<ScannerWorkerHostedService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<ScannerWorkerHostedService>());
var workerOptions = builder.Configuration.GetSection(ScannerWorkerOptions.SectionName).Get<ScannerWorkerOptions>() ?? new ScannerWorkerOptions();
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = workerOptions.Shutdown.Timeout;
});
builder.ConfigureScannerWorkerTelemetry(workerOptions);
if (workerOptions.Authority.Enabled)
{
builder.Services.AddStellaOpsAuthClient(clientOptions =>
{
clientOptions.Authority = workerOptions.Authority.Issuer?.Trim() ?? string.Empty;
clientOptions.ClientId = workerOptions.Authority.ClientId?.Trim() ?? string.Empty;
clientOptions.ClientSecret = workerOptions.Authority.ClientSecret;
clientOptions.EnableRetries = workerOptions.Authority.Resilience.EnableRetries ?? true;
clientOptions.HttpTimeout = TimeSpan.FromSeconds(workerOptions.Authority.BackchannelTimeoutSeconds);
clientOptions.DefaultScopes.Clear();
foreach (var scope in workerOptions.Authority.Scopes)
{
if (string.IsNullOrWhiteSpace(scope))
{
continue;
}
clientOptions.DefaultScopes.Add(scope);
}
clientOptions.RetryDelays.Clear();
foreach (var delay in workerOptions.Authority.Resilience.RetryDelays)
{
if (delay <= TimeSpan.Zero)
{
continue;
}
clientOptions.RetryDelays.Add(delay);
}
if (workerOptions.Authority.Resilience.AllowOfflineCacheFallback is bool allowOffline)
{
clientOptions.AllowOfflineCacheFallback = allowOffline;
}
if (workerOptions.Authority.Resilience.OfflineCacheTolerance is { } tolerance && tolerance > TimeSpan.Zero)
{
clientOptions.OfflineCacheTolerance = tolerance;
}
});
}
builder.Logging.Configure(options =>
{
options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId
| ActivityTrackingOptions.TraceId
| ActivityTrackingOptions.ParentId;
});
var host = builder.Build();
await host.RunAsync();
public partial class Program;

View File

@@ -1,20 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="1.12.0-beta.1" />
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="1.12.0-beta.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
<ProjectReference Include="..\StellaOps.Authority\StellaOps.Auth.Client\StellaOps.Auth.Client.csproj" />
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.OS\StellaOps.Scanner.Analyzers.OS.csproj" />
<ProjectReference Include="..\StellaOps.Scanner.EntryTrace\StellaOps.Scanner.EntryTrace.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,8 +1,9 @@
# Scanner Worker Task Board
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| SCANNER-WORKER-09-201 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-CORE-09-501 | Worker host bootstrap with Authority auth, hosted services, and graceful shutdown semantics. | `Program.cs` binds `Scanner:Worker` options, registers delay scheduler, configures telemetry + Authority client, and enforces shutdown timeout. |
| SCANNER-WORKER-09-202 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-WORKER-09-201, SCANNER-QUEUE-09-401 | Lease/heartbeat loop with retry+jitter, poison-job quarantine, structured logging. | `ScannerWorkerHostedService` + `LeaseHeartbeatService` manage concurrency, renewal margins, poison handling, and structured logs exercised by integration fixture. |
| SCANNER-WORKER-09-203 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-WORKER-09-202, SCANNER-STORAGE-09-301 | Analyzer dispatch skeleton emitting deterministic stage progress and honoring cancellation tokens. | Deterministic stage list + `ScanProgressReporter`; `WorkerBasicScanScenario` validates ordering and cancellation propagation. |
# Scanner Worker Task Board
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| SCANNER-WORKER-09-201 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-CORE-09-501 | Worker host bootstrap with Authority auth, hosted services, and graceful shutdown semantics. | `Program.cs` binds `Scanner:Worker` options, registers delay scheduler, configures telemetry + Authority client, and enforces shutdown timeout. |
| SCANNER-WORKER-09-202 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-WORKER-09-201, SCANNER-QUEUE-09-401 | Lease/heartbeat loop with retry+jitter, poison-job quarantine, structured logging. | `ScannerWorkerHostedService` + `LeaseHeartbeatService` manage concurrency, renewal margins, poison handling, and structured logs exercised by integration fixture. |
| SCANNER-WORKER-09-203 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-WORKER-09-202, SCANNER-STORAGE-09-301 | Analyzer dispatch skeleton emitting deterministic stage progress and honoring cancellation tokens. | Deterministic stage list + `ScanProgressReporter`; `WorkerBasicScanScenario` validates ordering and cancellation propagation. |
| SCANNER-WORKER-09-204 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-WORKER-09-203 | Worker metrics (queue latency, stage duration, failure counts) with OpenTelemetry resource wiring. | `ScannerWorkerMetrics` records queue/job/stage metrics; integration test asserts analyzer stage histogram entries. |
| SCANNER-WORKER-09-205 | DONE (2025-10-19) | Scanner Worker Guild | SCANNER-WORKER-09-202 | Harden heartbeat jitter so lease safety margin stays ≥3× and cover with regression tests. | `LeaseHeartbeatService` clamps jitter to safety window, validator enforces ≥3 safety factor, regression tests cover heartbeat scheduling and metrics. |