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:
@@ -35,7 +35,22 @@ env:
|
|||||||
CI_CACHE_ROOT: /data/.cache/stella-ops/feedser
|
CI_CACHE_ROOT: /data/.cache/stella-ops/feedser
|
||||||
RUNNER_TOOL_CACHE: /toolcache
|
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:
|
build-test:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
environment: ${{ github.event_name == 'pull_request' && 'preview' || 'staging' }}
|
environment: ${{ github.event_name == 'pull_request' && 'preview' || 'staging' }}
|
||||||
@@ -61,15 +76,82 @@ jobs:
|
|||||||
- name: Build solution (warnings as errors)
|
- name: Build solution (warnings as errors)
|
||||||
run: dotnet build src/StellaOps.Feedser.sln --configuration $BUILD_CONFIGURATION --no-restore -warnaserror
|
run: dotnet build src/StellaOps.Feedser.sln --configuration $BUILD_CONFIGURATION --no-restore -warnaserror
|
||||||
|
|
||||||
- name: Run unit and integration tests
|
- name: Run unit and integration tests
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$TEST_RESULTS_DIR"
|
mkdir -p "$TEST_RESULTS_DIR"
|
||||||
dotnet test src/StellaOps.Feedser.sln \
|
dotnet test src/StellaOps.Feedser.sln \
|
||||||
--configuration $BUILD_CONFIGURATION \
|
--configuration $BUILD_CONFIGURATION \
|
||||||
--no-build \
|
--no-build \
|
||||||
--logger "trx;LogFileName=stellaops-feedser-tests.trx" \
|
--logger "trx;LogFileName=stellaops-feedser-tests.trx" \
|
||||||
--results-directory "$TEST_RESULTS_DIR"
|
--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
|
- name: Publish Feedser web service
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$PUBLISH_DIR"
|
mkdir -p "$PUBLISH_DIR"
|
||||||
|
|||||||
679
SPRINTS.md
679
SPRINTS.md
@@ -1,340 +1,343 @@
|
|||||||
This file describe implementation of Stella Ops (docs/README.md). Implementation must respect rules from AGENTS.md (read if you have not).
|
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 | 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-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-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-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.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.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-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-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-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.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-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-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-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.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.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-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/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.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-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-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-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-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-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-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-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-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-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-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.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.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-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-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-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-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.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.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.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.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.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.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.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-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-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-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-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-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-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.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.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-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-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 | 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 | 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 | 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 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.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-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-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.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.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-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-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-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-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.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-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-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.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-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-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.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.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.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.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.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.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.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.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.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.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.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 | 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-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-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 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-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.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-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-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.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-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-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-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.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.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.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 | 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-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 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.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-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.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-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.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.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.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.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 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-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-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.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-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-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-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-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.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-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.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-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.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.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.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 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.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.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.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.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-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-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-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-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-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.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-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.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.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.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.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.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-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-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.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 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.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 | 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.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.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-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.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.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.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-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-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.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.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 & 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 & 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.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.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 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.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.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 | 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.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.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.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 | 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 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-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-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 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 (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 | 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-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 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 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 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 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 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 | 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-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-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 | 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-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-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 | 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 | 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-107 | Expose score inputs, config version, and quiet provenance in `/reports` JSON and signed payload. |
|
| 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.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.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.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.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-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 | 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 | 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-002 | Policy snapshot store + revision digests. |
|
| 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-003 | `/policy/preview` API (image digest → projected verdict diff). |
|
| 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 | 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 | 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-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-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-006 | Unknown state & confidence decay – deterministic bands surfaced in policy outputs. |
|
| 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 | 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 | 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 | Docs & Governance | docs/TASKS.md | DONE (2025-10-19) | Docs Guild, DevEx | DOCS-ADR-09-001 | Establish ADR process and template. |
|
| 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, Platform Events | DOCS-EVENTS-09-002 | Publish event schema catalog (`docs/events/`) for critical envelopes. |
|
| 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 | 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 | 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-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-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-303 | Repositories/services with dual-write feature flag, deterministic digests, TTL enforcement tests. |
|
| 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.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.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-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-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-403 | Retry + dead-letter strategy with structured logs/metrics for offline deployments. |
|
| 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 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 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-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-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-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-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-104 | Implement cache invalidation workflows (layer delete, TTL expiry, diff invalidation). |
|
| 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.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.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-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-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-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-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-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-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-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-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-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-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-207 | Package OS analyzers as restart-time plug-ins (manifest + host registration). |
|
| 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.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.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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-309 | Package language 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-308 | Determinism + fixture harness for language analyzers. |
|
||||||
| 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.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-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-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-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-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-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-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-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-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-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-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-407 | Package EntryTrace 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-406 | Explainability + diagnostics for unresolved constructs with metrics. |
|
||||||
| 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.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-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-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-503 | Produce JSON diff output for inventory vs usage views aligned with API contract. |
|
| 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.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.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-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-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-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-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-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-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-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-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-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-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-607 | Embed scoring inputs, confidence band, and quiet provenance in CycloneDX/DSSE artifacts. |
|
| 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 | Benchmarks | bench/TASKS.md | TODO | Bench Guild, Scanner Team | BENCH-SCANNER-10-001 | Analyzer microbench harness + baseline CSV. |
|
| 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 | Samples | samples/TASKS.md | TODO | Samples Guild, Scanner Team | SAMPLES-10-001 | Sample images with SBOM/BOM-Index sidecars. |
|
| Sprint 10 | Benchmarks | bench/TASKS.md | TODO | Bench Guild, Scanner Team | BENCH-SCANNER-10-001 | Analyzer microbench harness + baseline CSV. |
|
||||||
| Sprint 10 | DevOps Perf | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-PERF-10-001 | Perf smoke job ensuring <5 s SBOM compose. |
|
| Sprint 10 | Samples | samples/TASKS.md | TODO | Samples Guild, Scanner Team | SAMPLES-10-001 | Sample images with SBOM/BOM-Index sidecars. |
|
||||||
| 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 10 | DevOps Perf | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-PERF-10-001 | Perf smoke job ensuring <5 s SBOM compose. |
|
||||||
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Authority/TASKS.md | TODO | Authority Core & Security Guild | AUTH-MTLS-11-002 | Add OAuth mTLS client credential support with certificate-bound tokens and introspection updates. |
|
| 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.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.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-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-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-QUOTA-11-103 | Enforce plan quotas, concurrency/QPS limits, artifact size caps with metrics/audit logs. |
|
| 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.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.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-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-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-OBS-11-203 | Telemetry, alerting, mTLS hardening, and archive workflow for Attestor. |
|
| 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 | UI Integration | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-ATTEST-11-005 | Attestation visibility (Rekor id, status) on Scan Detail. |
|
| 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 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 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-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-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-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-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-OPS-12-204 | Operational runbooks, alert rules, and dashboard exports for runtime plane. |
|
| 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.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.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-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-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-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-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-004 | Batch `/runtime/events` submissions with disk-backed buffer and rate limits. |
|
| 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.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.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-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-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-103 | Caching, fail-open/closed toggles, metrics/logging for admission decisions. |
|
| 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.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.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-302 | `/policy/runtime` endpoint joining SBOM baseline + policy verdict with TTL guidance. |
|
| 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 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 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-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-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-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-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-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-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-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-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-NOTIFY-13-006 | Notify panel: channels/rules CRUD, deliveries view, test send. |
|
| 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.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.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-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-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-PLUGIN-13-007 | Package non-core CLI verbs as restart-time plug-ins (manifest + loader tests). |
|
| 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 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 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/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/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/deployment/TASKS.md | TODO | Deployment Guild | DEVOPS-OPS-14-003 | Deployment/update/rollback automation and channel management documentation. |
|
| 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/licensing/TASKS.md | TODO | Licensing Guild | DEVOPS-LIC-14-004 | Registry token service tied to Authority, plan gating, revocation handling, monitoring. |
|
| 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 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 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-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-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-103 | Versioning/migration helpers for rules/templates/deliveries. |
|
| 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.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.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-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-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-203 | Delivery history retention and query APIs. |
|
| 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.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.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-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-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-403 | Delivery queue with retry/dead-letter + metrics. |
|
| 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.Engine/TASKS.md | TODO | Notify Engine Guild | NOTIFY-ENGINE-15-301 | Rules evaluation core (filters, throttles, idempotency). |
|
| 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-302 | Action planner + digest coalescer. |
|
| 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-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-302 | Action planner + digest coalescer. |
|
||||||
| 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.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.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.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-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-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-103 | Delivery history & test-send endpoints. |
|
| 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-104 | Configuration binding + startup diagnostics. |
|
| 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.Worker/TASKS.md | TODO | Notify Worker Guild | NOTIFY-WORKER-15-201 | Bus subscription + leasing loop with backoff. |
|
| 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-202 | Rules evaluation pipeline integration. |
|
| 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-203 | Channel dispatch orchestration with retries. |
|
| 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-204 | Metrics/telemetry for Notify workers. |
|
| 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.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.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-502 | Slack health/test-send support. |
|
| 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.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.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-602 | Teams 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.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.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-702 | DKIM + health/test-send flows. |
|
| 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.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.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-802 | Webhook health/test-send support. |
|
| 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.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.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.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.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.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.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.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-EVENTS-15-201 | Emit `scanner.report.ready` + `scanner.scan.completed` events. |
|
| 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 | 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.Scanner.WebService/TASKS.md | TODO | Scanner WebService Guild | SCANNER-EVENTS-15-201 | Emit `scanner.report.ready` + `scanner.scan.completed` events. |
|
||||||
| 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 15 | Benchmarks | bench/TASKS.md | TODO | Bench Guild, Notify Team | BENCH-NOTIFY-15-001 | Notify dispatch throughput bench with results CSV. |
|
||||||
| Sprint 16 | Scheduler Intelligence | src/StellaOps.Scheduler.Models/TASKS.md | TODO | Scheduler Models Guild | SCHED-MODELS-16-101 | Define Scheduler DTOs & validation. |
|
| 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-102 | Publish schema docs/sample payloads. |
|
| 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-103 | Versioning/migration helpers for schedules/runs. |
|
| 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.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.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-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-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-203 | Audit + stats materialization for UI. |
|
| 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.Queue/TASKS.md | TODO | Scheduler Queue Guild | SCHED-QUEUE-16-401 | Queue abstraction + Redis Streams adapter. |
|
| 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-402 | NATS JetStream adapter with health probes. |
|
| 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-403 | Dead-letter handling + metrics. |
|
| 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.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.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-302 | Query APIs for ResolveByPurls/ResolveByVulns/ResolveAll. |
|
| 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-303 | Snapshot/compaction/invalidation workflow. |
|
| 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 | 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.ImpactIndex/TASKS.md | TODO | Scheduler ImpactIndex Guild | SCHED-IMPACT-16-303 | Snapshot/compaction/invalidation workflow. |
|
||||||
| 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.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-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-101 | Minimal API host with Authority enforcement. |
|
||||||
| 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-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-104 | Feedser/Vexer webhook handlers with security enforcement. |
|
| 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.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.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-202 | ImpactIndex targeting and shard planning. |
|
| 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-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-202 | ImpactIndex targeting and shard planning. |
|
||||||
| 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-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-205 | Metrics/telemetry for Scheduler planners/runners. |
|
| 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 | Benchmarks | bench/TASKS.md | TODO | Bench Guild, Scheduler Team | BENCH-IMPACT-16-001 | ImpactIndex throughput bench + RAM profile. |
|
| 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 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 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.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.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.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 | 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 | 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 | 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 | docs/TASKS.md | TODO | Docs Guild | DOCS-RUNTIME-17-004 | Document build-id workflows for SBOMs, runtime events, and debug-store usage. |
|
| 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. |
|
||||||
|
|||||||
@@ -1,295 +1,295 @@
|
|||||||
# StellaOps Multi-Sprint Implementation Plan (Agile Track)
|
# 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:
|
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)
|
- **Tasks** (ID · est. effort · path)
|
||||||
- **Acceptance metrics** (quantitative targets to reduce rework)
|
- **Acceptance metrics** (quantitative targets to reduce rework)
|
||||||
- **Gate** artifacts required before dependent groups can start
|
- **Gate** artifacts required before dependent groups can start
|
||||||
|
|
||||||
Durations are estimated work sizes (1 d ≈ one focused engineer day). Milestones are gated by artifacts—not calendar dates—to keep us agile and adaptable to competitor pressure.
|
Durations are estimated work sizes (1 d ≈ one focused engineer day). Milestones are gated by artifacts—not calendar dates—to keep us agile and adaptable to competitor pressure.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 9 – Scanner Core Foundations (ID: SP9, ~3 w)
|
## Sprint 9 – Scanner Core Foundations (ID: SP9, ~3 w)
|
||||||
|
|
||||||
### Group SP9-G1 — Core Contracts & Observability (src/StellaOps.Scanner.Core) ~1 w
|
### Group SP9-G1 — Core Contracts & Observability (src/StellaOps.Scanner.Core) ~1 w
|
||||||
- Tasks:
|
- Tasks:
|
||||||
- SCANNER-CORE-09-501 · 3 d · `/src/StellaOps.Scanner.Core/TASKS.md`
|
- SCANNER-CORE-09-501 · 3 d · `/src/StellaOps.Scanner.Core/TASKS.md`
|
||||||
- SCANNER-CORE-09-502 · 2 d · same path
|
- SCANNER-CORE-09-502 · 2 d · same path
|
||||||
- SCANNER-CORE-09-503 · 2 d · same path
|
- SCANNER-CORE-09-503 · 2 d · same path
|
||||||
- Acceptance metrics: DTO round-trip tests stable; middleware adds ≤5 µs per call.
|
- 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.
|
- Gate SP9-G1 → WebService: `scanner-core-contracts.md` snippet plus `ScannerCoreContractsTests` green.
|
||||||
|
|
||||||
### Group SP9-G2 — Queue Backbone (src/StellaOps.Scanner.Queue) ~1 w
|
### Group SP9-G2 — Queue Backbone (src/StellaOps.Scanner.Queue) ~1 w
|
||||||
- Tasks: SCANNER-QUEUE-09-401 (3 d), -402 (2 d), -403 (2 d) · `/src/StellaOps.Scanner.Queue/TASKS.md`
|
- Tasks: SCANNER-QUEUE-09-401 (3 d), -402 (2 d), -403 (2 d) · `/src/StellaOps.Scanner.Queue/TASKS.md`
|
||||||
- Acceptance: dequeue latency p95 ≤20 ms at 40 rps; chaos test retains leases.
|
- Acceptance: dequeue latency p95 ≤20 ms at 40 rps; chaos test retains leases.
|
||||||
- Gate: Redis/NATS adapters docs + `QueueLeaseIntegrationTests` passing.
|
- 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.
|
- Status: **DONE (2025-10-19)** – Gate satisfied via Redis/NATS adapter docs and `QueueLeaseIntegrationTests` run under fake clock.
|
||||||
|
|
||||||
### Group SP9-G3 — Storage Backbone (src/StellaOps.Scanner.Storage) ~1 w
|
### Group SP9-G3 — Storage Backbone (src/StellaOps.Scanner.Storage) ~1 w
|
||||||
- Tasks: SCANNER-STORAGE-09-301 (3 d), -302 (2 d), -303 (2 d)
|
- Tasks: SCANNER-STORAGE-09-301 (3 d), -302 (2 d), -303 (2 d)
|
||||||
- Acceptance: majority write/read ≤50 ms; TTL verified.
|
- Acceptance: majority write/read ≤50 ms; TTL verified.
|
||||||
- Gate: migrations checked in; `StorageDualWriteFixture` passes.
|
- Gate: migrations checked in; `StorageDualWriteFixture` passes.
|
||||||
- Status: **DONE (2025-10-19)** – Mongo bootstrapper + migrations committed; MinIO dual-write service wired; `StorageDualWriteFixture` green on Mongo2Go.
|
- Status: **DONE (2025-10-19)** – Mongo bootstrapper + migrations committed; MinIO dual-write service wired; `StorageDualWriteFixture` green on Mongo2Go.
|
||||||
|
|
||||||
### Group SP9-G4 — WebService Host & Policy Surfacing (src/StellaOps.Scanner.WebService) ~1.2 w
|
### Group SP9-G4 — WebService Host & Policy Surfacing (src/StellaOps.Scanner.WebService) ~1.2 w
|
||||||
- Tasks: SCANNER-WEB-09-101 (2 d), -102 (3 d), -103 (2 d), -104 (2 d), SCANNER-POLICY-09-105 (3 d), SCANNER-POLICY-09-106 (4 d)
|
- Tasks: SCANNER-WEB-09-101 (2 d), -102 (3 d), -103 (2 d), -104 (2 d), SCANNER-POLICY-09-105 (3 d), SCANNER-POLICY-09-106 (4 d)
|
||||||
- Acceptance: `/api/v1/scans` enqueue p95 ≤50 ms under synthetic load; policy validation errors actionable; `/reports` response signed.
|
- Acceptance: `/api/v1/scans` enqueue p95 ≤50 ms under synthetic load; policy validation errors actionable; `/reports` response signed.
|
||||||
- Gate SP9-G4 → SP10/SP11: `/reports` OpenAPI frozen; sample signed envelope committed in `samples/api/reports/`.
|
- 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.
|
- Status: **IN PROGRESS (2025-10-19)** – Minimal host and `/api/v1/scans` endpoints delivered (SCANNER-WEB-09-101/102 done); progress streaming and policy/report surfaces remain.
|
||||||
|
|
||||||
### Group SP9-G5 — Worker Host (src/StellaOps.Scanner.Worker) ~1 w
|
### Group SP9-G5 — Worker Host (src/StellaOps.Scanner.Worker) ~1 w
|
||||||
- Tasks: SCANNER-WORKER-09-201 (3 d), -202 (3 d), -203 (2 d), -204 (2 d)
|
- Tasks: SCANNER-WORKER-09-201 (3 d), -202 (3 d), -203 (2 d), -204 (2 d), -205 (1 d)
|
||||||
- Acceptance: job lease never drops <3× heartbeat; progress events deterministic.
|
- Acceptance: job lease never drops <3× heartbeat; progress events deterministic.
|
||||||
- Gate: `WorkerBasicScanScenario` integration recorded.
|
- Gate: `WorkerBasicScanScenario` integration recorded + optional live queue smoke validation.
|
||||||
- Status: **DONE (2025-10-19)** – Host bootstrap + authority wiring, heartbeat loop, deterministic stage pipeline, and metrics landed; `WorkerBasicScanScenarioTests` green.
|
- Status: **DONE (2025-10-19)** – Host bootstrap, heartbeat jitter clamp, deterministic stage pipeline, metrics, and Redis-backed smoke harness landed; `WorkerBasicScanScenarioTests` and `RedisWorkerSmokeTests` (flagged) green.
|
||||||
|
|
||||||
### Group SP9-G6 — Buildx Plug-in (src/StellaOps.Scanner.Sbomer.BuildXPlugin) ~0.8 w
|
### Group SP9-G6 — Buildx Plug-in (src/StellaOps.Scanner.Sbomer.BuildXPlugin) ~0.8 w
|
||||||
- Tasks: SP9-BLDX-09-001 (3 d), SP9-BLDX-09-002 (2 d), SP9-BLDX-09-003 (2 d)
|
- Tasks: SP9-BLDX-09-001 (3 d), SP9-BLDX-09-002 (2 d), SP9-BLDX-09-003 (2 d), SP9-BLDX-09-004 (2 d), SP9-BLDX-09-005 (1 d)
|
||||||
- Acceptance: build-time overhead ≤300 ms/layer on 4 vCPU; CAS handshake reliable in CI sample.
|
- Acceptance: build-time overhead ≤300 ms/layer on 4 vCPU; CAS handshake reliable in CI sample.
|
||||||
- Gate: buildx demo workflow artifact + quickstart doc.
|
- 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 demo workflow, and quickstart committed.
|
- Status: **DONE (2025-10-19)** — manifest+CAS scaffold, descriptor/Attestor hand-off, GitHub/Gitea determinism workflows, quickstart update, and golden tests committed.
|
||||||
|
|
||||||
### Group SP9-G7 — Policy Engine Core (src/StellaOps.Policy) ~1 w
|
### Group SP9-G7 — Policy Engine Core (src/StellaOps.Policy) ~1 w
|
||||||
- Tasks: POLICY-CORE-09-001 (2 d) ✅, -002 (3 d) ✅, -003 (3 d) ✅, -004 (3 d), -005 (4 d), -006 (2 d)
|
- Tasks: POLICY-CORE-09-001 (2 d) ✅, -002 (3 d) ✅, -003 (3 d) ✅, -004 (3 d), -005 (4 d), -006 (2 d)
|
||||||
- Acceptance: policy parsing ≥200 files/s; preview diff response <200 ms for 500-component SBOM; quieting logic audited.
|
- Acceptance: policy parsing ≥200 files/s; preview diff response <200 ms for 500-component SBOM; quieting logic audited.
|
||||||
- Gate: `policy-schema@1` published; revision digests stored; preview API doc updated.
|
- Gate: `policy-schema@1` published; revision digests stored; preview API doc updated.
|
||||||
|
|
||||||
### Group SP9-G8 — DevOps Early Guardrails (ops/devops) ~0.4 w
|
### Group SP9-G8 — DevOps Early Guardrails (ops/devops) ~0.4 w
|
||||||
- Tasks: DEVOPS-HELM-09-001 (3 d) — **DONE (2025-10-19)**
|
- Tasks: DEVOPS-HELM-09-001 (3 d) — **DONE (2025-10-19)**
|
||||||
- Acceptance: helm/compose profiles for dev/stage/airgap lint + dry-run clean; manifests pinned to digest.
|
- 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`.
|
- Gate: profiles merged under `deploy/`; install guide cross-link satisfied via `deploy/compose/` bundles and `docs/21_INSTALL_GUIDE.md`.
|
||||||
|
|
||||||
### Group SP9-G9 — Documentation & Events (docs/) ~0.4 w
|
### Group SP9-G9 — Documentation & Events (docs/) ~0.4 w
|
||||||
- Tasks: DOCS-ADR-09-001 (2 d), DOCS-EVENTS-09-002 (2 d)
|
- Tasks: DOCS-ADR-09-001 (2 d), DOCS-EVENTS-09-002 (2 d)
|
||||||
- Acceptance: ADR process broadcast; event schemas validated via CI.
|
- Acceptance: ADR process broadcast; event schemas validated via CI.
|
||||||
- Gate: `docs/adr/index.md` linking template; `docs/events/README.md` referencing schemas.
|
- 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.
|
- Status: **DONE (2025-10-19)** – ADR contribution guide + template updates merged, Docs CI Ajv validation wired, events catalog documented, guild announcement recorded.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 10 – Scanner Analyzers & SBOM (ID: SP10, ~4 w)
|
## Sprint 10 – Scanner Analyzers & SBOM (ID: SP10, ~4 w)
|
||||||
|
|
||||||
### Group SP10-G1 — OS Analyzer Plug-ins (src/StellaOps.Scanner.Analyzers.OS) ~1 w
|
### Group SP10-G1 — OS Analyzer Plug-ins (src/StellaOps.Scanner.Analyzers.OS) ~1 w
|
||||||
- Tasks: SCANNER-ANALYZERS-OS-10-201..207 (durations 2–3 d each)
|
- Tasks: SCANNER-ANALYZERS-OS-10-201..207 (durations 2–3 d each)
|
||||||
- Acceptance: analyzer runtime <1.5 s/image; memory <250 MB.
|
- Acceptance: analyzer runtime <1.5 s/image; memory <250 MB.
|
||||||
- Gate: plug-ins packaged under `plugins/scanner/analyzers/os/`; determinism CI job green.
|
- Gate: plug-ins packaged under `plugins/scanner/analyzers/os/`; determinism CI job green.
|
||||||
|
|
||||||
### Group SP10-G2 — Language Analyzer Plug-ins (src/StellaOps.Scanner.Analyzers.Lang) ~1.5 w
|
### Group SP10-G2 — Language Analyzer Plug-ins (src/StellaOps.Scanner.Analyzers.Lang) ~1.5 w
|
||||||
- Tasks: SCANNER-ANALYZERS-LANG-10-301..309
|
- Tasks: SCANNER-ANALYZERS-LANG-10-301..309
|
||||||
- Acceptance: Node analyzer handles 10 k modules <2 s; Python memory <200 MB.
|
- Acceptance: Node analyzer handles 10 k modules <2 s; Python memory <200 MB.
|
||||||
- Gate: golden outputs stored; plugin manifests present.
|
- Gate: golden outputs stored; plugin manifests present.
|
||||||
|
|
||||||
### Group SP10-G3 — EntryTrace Plug-ins (src/StellaOps.Scanner.EntryTrace) ~0.8 w
|
### Group SP10-G3 — EntryTrace Plug-ins (src/StellaOps.Scanner.EntryTrace) ~0.8 w
|
||||||
- Tasks: SCANNER-ENTRYTRACE-10-401..407
|
- Tasks: SCANNER-ENTRYTRACE-10-401..407
|
||||||
- Acceptance: ≥95 % launcher resolution success on samples; unknown reasons enumerated.
|
- Acceptance: ≥95 % launcher resolution success on samples; unknown reasons enumerated.
|
||||||
- Gate: entrytrace plug-ins packaged; explainability doc updated.
|
- Gate: entrytrace plug-ins packaged; explainability doc updated.
|
||||||
|
|
||||||
### Group SP10-G4 — SBOM Composition & BOM Index (src/StellaOps.Scanner.Diff + Emit) ~1 w
|
### Group SP10-G4 — SBOM Composition & BOM Index (src/StellaOps.Scanner.Diff + Emit) ~1 w
|
||||||
- Tasks: SCANNER-DIFF-10-501..503, SCANNER-EMIT-10-601..606
|
- Tasks: SCANNER-DIFF-10-501..503, SCANNER-EMIT-10-601..606
|
||||||
- Acceptance: BOM-Index emission <500 ms/image; diff output deterministic across runs.
|
- Acceptance: BOM-Index emission <500 ms/image; diff output deterministic across runs.
|
||||||
- Gate SP10-G4 → SP16: `docs/artifacts/bom-index/` schema + fixtures; tests `BOMIndexGoldenIsStable` & `UsageFlagsAreAccurate` green.
|
- Gate SP10-G4 → SP16: `docs/artifacts/bom-index/` schema + fixtures; tests `BOMIndexGoldenIsStable` & `UsageFlagsAreAccurate` green.
|
||||||
|
|
||||||
### Group SP10-G5 — Cache Subsystem (src/StellaOps.Scanner.Cache) ~0.6 w
|
### Group SP10-G5 — Cache Subsystem (src/StellaOps.Scanner.Cache) ~0.6 w
|
||||||
- Tasks: SCANNER-CACHE-10-101..104
|
- Tasks: SCANNER-CACHE-10-101..104
|
||||||
- Acceptance: cache hit instrumentation validated; eviction keeps footprint <5 GB.
|
- Acceptance: cache hit instrumentation validated; eviction keeps footprint <5 GB.
|
||||||
- Gate: cache configuration doc; integration test `LayerCacheRoundTrip` green.
|
- Gate: cache configuration doc; integration test `LayerCacheRoundTrip` green.
|
||||||
|
|
||||||
### Group SP10-G6 — Benchmarks & Samples (bench/, samples/, ops/devops) ~0.6 w
|
### Group SP10-G6 — Benchmarks & Samples (bench/, samples/, ops/devops) ~0.6 w
|
||||||
- Tasks: BENCH-SCANNER-10-001 (2 d), SAMPLES-10-001 (finish – 3 d), DEVOPS-PERF-10-001 (2 d)
|
- Tasks: BENCH-SCANNER-10-001 (2 d), SAMPLES-10-001 (finish – 3 d), DEVOPS-PERF-10-001 (2 d)
|
||||||
- Acceptance: analyzer benchmark CSV published; perf CI guard ensures SBOM compose <5 s; sample SBOM/BOM-Index committed.
|
- Acceptance: analyzer benchmark CSV published; perf CI guard ensures SBOM compose <5 s; sample SBOM/BOM-Index committed.
|
||||||
- Gate: bench results stored under `bench/`; `samples/` populated; CI job added.
|
- Gate: bench results stored under `bench/`; `samples/` populated; CI job added.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 11 – Signing Chain Bring-up (ID: SP11, ~3 w)
|
## Sprint 11 – Signing Chain Bring-up (ID: SP11, ~3 w)
|
||||||
|
|
||||||
### Group SP11-G1 — Authority Sender Constraints (src/StellaOps.Authority) ~0.8 w
|
### Group SP11-G1 — Authority Sender Constraints (src/StellaOps.Authority) ~0.8 w
|
||||||
- Tasks: AUTH-DPOP-11-001 (3 d), AUTH-MTLS-11-002 (2 d)
|
- Tasks: AUTH-DPOP-11-001 (3 d), AUTH-MTLS-11-002 (2 d)
|
||||||
- Acceptance: DPoP nonce dance validated; mTLS tokens issued in ≤40 ms.
|
- Acceptance: DPoP nonce dance validated; mTLS tokens issued in ≤40 ms.
|
||||||
- Gate: updated Authority OpenAPI; QA scripts verifying DPoP/mTLS.
|
- Gate: updated Authority OpenAPI; QA scripts verifying DPoP/mTLS.
|
||||||
|
|
||||||
### Group SP11-G2 — Signer Service (src/StellaOps.Signer) ~1.2 w
|
### Group SP11-G2 — Signer Service (src/StellaOps.Signer) ~1.2 w
|
||||||
- Tasks: SIGNER-API-11-101 (4 d), SIGNER-REF-11-102 (2 d), SIGNER-QUOTA-11-103 (2 d)
|
- Tasks: SIGNER-API-11-101 (4 d), SIGNER-REF-11-102 (2 d), SIGNER-QUOTA-11-103 (2 d)
|
||||||
- Acceptance: signing throughput ≥30 req/min; p95 latency ≤200 ms.
|
- Acceptance: signing throughput ≥30 req/min; p95 latency ≤200 ms.
|
||||||
- Gate SP11-G2 → Attestor/UI: `/sign/dsse` OpenAPI frozen; signed DSSE bundle in repo; Rekor interop test passing.
|
- Gate SP11-G2 → Attestor/UI: `/sign/dsse` OpenAPI frozen; signed DSSE bundle in repo; Rekor interop test passing.
|
||||||
|
|
||||||
### Group SP11-G3 — Attestor Service (src/StellaOps.Attestor) ~1 w
|
### Group SP11-G3 — Attestor Service (src/StellaOps.Attestor) ~1 w
|
||||||
- Tasks: ATTESTOR-API-11-201 (3 d), ATTESTOR-VERIFY-11-202 (2 d), ATTESTOR-OBS-11-203 (2 d)
|
- Tasks: ATTESTOR-API-11-201 (3 d), ATTESTOR-VERIFY-11-202 (2 d), ATTESTOR-OBS-11-203 (2 d)
|
||||||
- Acceptance: inclusion proof retrieval <500 ms; audit log coverage 100 %.
|
- Acceptance: inclusion proof retrieval <500 ms; audit log coverage 100 %.
|
||||||
- Gate: Attestor API doc + verification script.
|
- Gate: Attestor API doc + verification script.
|
||||||
|
|
||||||
### Group SP11-G4 — UI Attestation Hooks (src/StellaOps.UI) ~0.4 w
|
### Group SP11-G4 — UI Attestation Hooks (src/StellaOps.UI) ~0.4 w
|
||||||
- Tasks: UI-ATTEST-11-005 (3 d)
|
- Tasks: UI-ATTEST-11-005 (3 d)
|
||||||
- Acceptance: attestation panel renders within 200 ms; Rekor link verified.
|
- Acceptance: attestation panel renders within 200 ms; Rekor link verified.
|
||||||
- Gate SP11-G4 → SP13-G1: recorded UX walkthrough.
|
- Gate SP11-G4 → SP13-G1: recorded UX walkthrough.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 12 – Runtime Guardrails (ID: SP12, ~3 w)
|
## Sprint 12 – Runtime Guardrails (ID: SP12, ~3 w)
|
||||||
|
|
||||||
### Group SP12-G1 — Zastava Core (src/StellaOps.Zastava.Core) ~0.8 w
|
### Group SP12-G1 — Zastava Core (src/StellaOps.Zastava.Core) ~0.8 w
|
||||||
- Tasks: ZASTAVA-CORE-12-201..204
|
- Tasks: ZASTAVA-CORE-12-201..204
|
||||||
- Acceptance: DTO tests stable; configuration docs produced.
|
- Acceptance: DTO tests stable; configuration docs produced.
|
||||||
- Gate: schema doc + logging helpers integrated.
|
- Gate: schema doc + logging helpers integrated.
|
||||||
|
|
||||||
### Group SP12-G2 — Zastava Observer (src/StellaOps.Zastava.Observer) ~0.8 w
|
### Group SP12-G2 — Zastava Observer (src/StellaOps.Zastava.Observer) ~0.8 w
|
||||||
- Tasks: ZASTAVA-OBS-12-001..004
|
- Tasks: ZASTAVA-OBS-12-001..004
|
||||||
- Acceptance: observer memory <200 MB; event flush ≤2 s.
|
- Acceptance: observer memory <200 MB; event flush ≤2 s.
|
||||||
- Gate: sample runtime events stored; offline buffer test passes.
|
- Gate: sample runtime events stored; offline buffer test passes.
|
||||||
|
|
||||||
### Group SP12-G3 — Zastava Webhook (src/StellaOps.Zastava.Webhook) ~0.6 w
|
### Group SP12-G3 — Zastava Webhook (src/StellaOps.Zastava.Webhook) ~0.6 w
|
||||||
- Tasks: ZASTAVA-WEBHOOK-12-101..103
|
- Tasks: ZASTAVA-WEBHOOK-12-101..103
|
||||||
- Acceptance: admission latency p95 ≤45 ms; cache TTL adhered to.
|
- Acceptance: admission latency p95 ≤45 ms; cache TTL adhered to.
|
||||||
- Gate: TLS rotation procedure documented; readiness probe script.
|
- Gate: TLS rotation procedure documented; readiness probe script.
|
||||||
|
|
||||||
### Group SP12-G4 — Scanner Runtime APIs (src/StellaOps.Scanner.WebService) ~0.8 w
|
### Group SP12-G4 — Scanner Runtime APIs (src/StellaOps.Scanner.WebService) ~0.8 w
|
||||||
- Tasks: SCANNER-RUNTIME-12-301 (2 d), SCANNER-RUNTIME-12-302 (3 d)
|
- Tasks: SCANNER-RUNTIME-12-301 (2 d), SCANNER-RUNTIME-12-302 (3 d)
|
||||||
- Acceptance: `/runtime/events` handles 500 events/sec; `/policy/runtime` output matches webhook decisions.
|
- Acceptance: `/runtime/events` handles 500 events/sec; `/policy/runtime` output matches webhook decisions.
|
||||||
- Gate SP12-G4 → SP13/SP15: API documented, fixtures updated.
|
- Gate SP12-G4 → SP13/SP15: API documented, fixtures updated.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 13 – UX & CLI Experience (ID: SP13, ~2 w)
|
## Sprint 13 – UX & CLI Experience (ID: SP13, ~2 w)
|
||||||
|
|
||||||
### Group SP13-G1 — UI Shell & Panels (src/StellaOps.UI) ~1.6 w
|
### Group SP13-G1 — UI Shell & Panels (src/StellaOps.UI) ~1.6 w
|
||||||
- Tasks: UI-AUTH-13-001 (3 d), UI-SCANS-13-002 (4 d), UI-VEX-13-003 (3 d), UI-ADMIN-13-004 (2 d), UI-SCHED-13-005 (3 d), UI-NOTIFY-13-006 (3 d)
|
- Tasks: UI-AUTH-13-001 (3 d), UI-SCANS-13-002 (4 d), UI-VEX-13-003 (3 d), UI-ADMIN-13-004 (2 d), UI-SCHED-13-005 (3 d), UI-NOTIFY-13-006 (3 d)
|
||||||
- Acceptance: Lighthouse ≥85; Scheduler/Notify panels function against mocked APIs.
|
- Acceptance: Lighthouse ≥85; Scheduler/Notify panels function against mocked APIs.
|
||||||
- Gate: UI dev server fixtures committed; QA sign-off captured.
|
- Gate: UI dev server fixtures committed; QA sign-off captured.
|
||||||
|
|
||||||
### Group SP13-G2 — CLI Enhancements (src/StellaOps.Cli) ~0.8 w
|
### Group SP13-G2 — CLI Enhancements (src/StellaOps.Cli) ~0.8 w
|
||||||
- Tasks: CLI-RUNTIME-13-005 (3 d), CLI-OFFLINE-13-006 (3 d), CLI-PLUGIN-13-007 (2 d)
|
- Tasks: CLI-RUNTIME-13-005 (3 d), CLI-OFFLINE-13-006 (3 d), CLI-PLUGIN-13-007 (2 d)
|
||||||
- Acceptance: runtime policy CLI completes <1 s for 10 images; offline kit commands resume downloads.
|
- Acceptance: runtime policy CLI completes <1 s for 10 images; offline kit commands resume downloads.
|
||||||
- Gate: CLI plugin manifest doc; smoke tests covering new verbs.
|
- Gate: CLI plugin manifest doc; smoke tests covering new verbs.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 14 – Release & Offline Ops (ID: SP14, ~2 w)
|
## Sprint 14 – Release & Offline Ops (ID: SP14, ~2 w)
|
||||||
|
|
||||||
### Group SP14-G1 — Release Automation (ops/devops) ~0.8 w
|
### Group SP14-G1 — Release Automation (ops/devops) ~0.8 w
|
||||||
- Tasks: DEVOPS-REL-14-001 (4 d)
|
- Tasks: DEVOPS-REL-14-001 (4 d)
|
||||||
- Acceptance: reproducible build diff tool shows zero drift across two runs; signing pipeline green.
|
- Acceptance: reproducible build diff tool shows zero drift across two runs; signing pipeline green.
|
||||||
- Gate: signed manifest + provenance published.
|
- Gate: signed manifest + provenance published.
|
||||||
|
|
||||||
### Group SP14-G2 — Offline Kit Packaging (ops/offline-kit) ~0.6 w
|
### Group SP14-G2 — Offline Kit Packaging (ops/offline-kit) ~0.6 w
|
||||||
- Tasks: DEVOPS-OFFLINE-14-002 (3 d)
|
- Tasks: DEVOPS-OFFLINE-14-002 (3 d)
|
||||||
- Acceptance: kit import <5 min with integrity verification CLI.
|
- Acceptance: kit import <5 min with integrity verification CLI.
|
||||||
- Gate: kit doc updated; import script included.
|
- Gate: kit doc updated; import script included.
|
||||||
|
|
||||||
### Group SP14-G3 — Deployment Playbooks (ops/deployment) ~0.4 w
|
### Group SP14-G3 — Deployment Playbooks (ops/deployment) ~0.4 w
|
||||||
- Tasks: DEVOPS-OPS-14-003 (2 d)
|
- Tasks: DEVOPS-OPS-14-003 (2 d)
|
||||||
- Acceptance: rollback drill recorded; compatibility matrix produced.
|
- Acceptance: rollback drill recorded; compatibility matrix produced.
|
||||||
- Gate: playbook PR merged with Ops sign-off.
|
- Gate: playbook PR merged with Ops sign-off.
|
||||||
|
|
||||||
### Group SP14-G4 — Licensing Token Service (ops/licensing) ~0.4 w
|
### Group SP14-G4 — Licensing Token Service (ops/licensing) ~0.4 w
|
||||||
- Tasks: DEVOPS-LIC-14-004 (2 d)
|
- Tasks: DEVOPS-LIC-14-004 (2 d)
|
||||||
- Acceptance: token service handles 100 req/min; revocation latency <60 s.
|
- Acceptance: token service handles 100 req/min; revocation latency <60 s.
|
||||||
- Gate: monitoring dashboard links; failover doc.
|
- Gate: monitoring dashboard links; failover doc.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 15 – Notify Foundations (ID: SP15, ~3 w)
|
## Sprint 15 – Notify Foundations (ID: SP15, ~3 w)
|
||||||
|
|
||||||
### Group SP15-G1 — Models & Storage (src/StellaOps.Notify.Models + Storage.Mongo) ~0.8 w
|
### Group SP15-G1 — Models & Storage (src/StellaOps.Notify.Models + Storage.Mongo) ~0.8 w
|
||||||
- Tasks: NOTIFY-MODELS-15-101 (2 d), -102 (2 d), -103 (1 d); NOTIFY-STORAGE-15-201 (3 d), -202 (2 d), -203 (1 d)
|
- Tasks: NOTIFY-MODELS-15-101 (2 d), -102 (2 d), -103 (1 d); NOTIFY-STORAGE-15-201 (3 d), -202 (2 d), -203 (1 d)
|
||||||
- Acceptance: rule CRUD latency <120 ms; delivery retention job verified.
|
- Acceptance: rule CRUD latency <120 ms; delivery retention job verified.
|
||||||
- Gate: schema docs + fixtures published.
|
- Gate: schema docs + fixtures published.
|
||||||
|
|
||||||
### Group SP15-G2 — Engine & Queue (src/StellaOps.Notify.Engine + Queue) ~0.8 w
|
### Group SP15-G2 — Engine & Queue (src/StellaOps.Notify.Engine + Queue) ~0.8 w
|
||||||
- Tasks: NOTIFY-ENGINE-15-301..304, NOTIFY-QUEUE-15-401..403
|
- Tasks: NOTIFY-ENGINE-15-301..304, NOTIFY-QUEUE-15-401..403
|
||||||
- Acceptance: rules evaluation ≥5k events/min; queue dead-letter <0.5 %.
|
- Acceptance: rules evaluation ≥5k events/min; queue dead-letter <0.5 %.
|
||||||
- Gate: digest outputs committed; queue config doc updated.
|
- Gate: digest outputs committed; queue config doc updated.
|
||||||
|
|
||||||
### Group SP15-G3 — WebService & Worker (src/StellaOps.Notify.WebService + Worker) ~0.8 w
|
### Group SP15-G3 — WebService & Worker (src/StellaOps.Notify.WebService + Worker) ~0.8 w
|
||||||
- Tasks: NOTIFY-WEB-15-101..104, NOTIFY-WORKER-15-201..204
|
- Tasks: NOTIFY-WEB-15-101..104, NOTIFY-WORKER-15-201..204
|
||||||
- Acceptance: API p95 <120 ms; worker delivery success ≥99 %.
|
- Acceptance: API p95 <120 ms; worker delivery success ≥99 %.
|
||||||
- Gate: end-to-end fixture run producing delivery record.
|
- Gate: end-to-end fixture run producing delivery record.
|
||||||
|
|
||||||
### Group SP15-G4 — Channel Plug-ins (src/StellaOps.Notify.Connectors.*) ~0.6 w
|
### Group SP15-G4 — Channel Plug-ins (src/StellaOps.Notify.Connectors.*) ~0.6 w
|
||||||
- Tasks: NOTIFY-CONN-SLACK-15-501..503, NOTIFY-CONN-TEAMS-15-601..603, NOTIFY-CONN-EMAIL-15-701..703, NOTIFY-CONN-WEBHOOK-15-801..803
|
- 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.
|
- Acceptance: channel-specific retry policies verified; rate limits respected.
|
||||||
- Gate: plug-in manifests inside `plugins/notify/**`; test-send docs.
|
- Gate: plug-in manifests inside `plugins/notify/**`; test-send docs.
|
||||||
|
|
||||||
### Group SP15-G5 — Events & Benchmarks (src/StellaOps.Scanner.WebService + bench) ~0.5 w
|
### Group SP15-G5 — Events & Benchmarks (src/StellaOps.Scanner.WebService + bench) ~0.5 w
|
||||||
- Tasks: SCANNER-EVENTS-15-201 (2 d), BENCH-NOTIFY-15-001 (2 d)
|
- Tasks: SCANNER-EVENTS-15-201 (2 d), BENCH-NOTIFY-15-001 (2 d)
|
||||||
- Acceptance: event emission latency <100 ms; throughput bench results stored.
|
- Acceptance: event emission latency <100 ms; throughput bench results stored.
|
||||||
- Gate: `docs/events/samples/` contains sample payloads; bench CSV in repo.
|
- Gate: `docs/events/samples/` contains sample payloads; bench CSV in repo.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 16 – Scheduler Intelligence (ID: SP16, ~4 w)
|
## Sprint 16 – Scheduler Intelligence (ID: SP16, ~4 w)
|
||||||
|
|
||||||
### Group SP16-G1 — Models & Storage (src/StellaOps.Scheduler.Models + Storage.Mongo) ~1 w
|
### Group SP16-G1 — Models & Storage (src/StellaOps.Scheduler.Models + Storage.Mongo) ~1 w
|
||||||
- Tasks: SCHED-MODELS-16-101 (3 d), -102 (2 d), -103 (2 d); SCHED-STORAGE-16-201 (3 d), -202 (2 d), -203 (2 d)
|
- Tasks: SCHED-MODELS-16-101 (3 d), -102 (2 d), -103 (2 d); SCHED-STORAGE-16-201 (3 d), -202 (2 d), -203 (2 d)
|
||||||
- Acceptance: schedule CRUD latency <120 ms; run retention TTL enforced.
|
- Acceptance: schedule CRUD latency <120 ms; run retention TTL enforced.
|
||||||
- Gate: schema doc + integration tests passing.
|
- Gate: schema doc + integration tests passing.
|
||||||
|
|
||||||
### Group SP16-G2 — ImpactIndex & Queue (src/StellaOps.Scheduler.ImpactIndex + Queue + Bench) ~1.2 w
|
### Group SP16-G2 — ImpactIndex & Queue (src/StellaOps.Scheduler.ImpactIndex + Queue + Bench) ~1.2 w
|
||||||
- Tasks: SCHED-IMPACT-16-300 (2 d, DOING), SCHED-IMPACT-16-301 (3 d), -302 (3 d), -303 (2 d); SCHED-QUEUE-16-401..403 (each 2 d); BENCH-IMPACT-16-001 (2 d)
|
- Tasks: SCHED-IMPACT-16-300 (2 d, DOING), SCHED-IMPACT-16-301 (3 d), -302 (3 d), -303 (2 d); SCHED-QUEUE-16-401..403 (each 2 d); BENCH-IMPACT-16-001 (2 d)
|
||||||
- Acceptance: impact resolve 10k productKeys <300 ms hot; stub removed by sprint end.
|
- Acceptance: impact resolve 10k productKeys <300 ms hot; stub removed by sprint end.
|
||||||
- Gate: roaring snapshot stored; bench CSV published; removal plan for stub recorded.
|
- Gate: roaring snapshot stored; bench CSV published; removal plan for stub recorded.
|
||||||
|
|
||||||
### Group SP16-G3 — Scheduler WebService (src/StellaOps.Scheduler.WebService) ~0.8 w
|
### Group SP16-G3 — Scheduler WebService (src/StellaOps.Scheduler.WebService) ~0.8 w
|
||||||
- Tasks: SCHED-WEB-16-101..104 (each 2 d)
|
- Tasks: SCHED-WEB-16-101..104 (each 2 d)
|
||||||
- Acceptance: preview endpoint <250 ms; webhook security enforced.
|
- Acceptance: preview endpoint <250 ms; webhook security enforced.
|
||||||
- Gate: OpenAPI published; dry-run JSON fixtures stored.
|
- Gate: OpenAPI published; dry-run JSON fixtures stored.
|
||||||
|
|
||||||
### Group SP16-G4 — Scheduler Worker (src/StellaOps.Scheduler.Worker) ~1 w
|
### Group SP16-G4 — Scheduler Worker (src/StellaOps.Scheduler.Worker) ~1 w
|
||||||
- Tasks: SCHED-WORKER-16-201 (3 d), -202 (2 d), -203 (3 d), -204 (2 d), -205 (2 d)
|
- Tasks: SCHED-WORKER-16-201 (3 d), -202 (2 d), -203 (3 d), -204 (2 d), -205 (2 d)
|
||||||
- Acceptance: planner fairness metrics captured; runner success ≥98 % across 1k sims.
|
- Acceptance: planner fairness metrics captured; runner success ≥98 % across 1k sims.
|
||||||
- Gate: event emission to Notify verified; metrics dashboards live.
|
- Gate: event emission to Notify verified; metrics dashboards live.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint 17 – Symbol Intelligence & Forensics (ID: SP17, ~2.5 w)
|
## Sprint 17 – Symbol Intelligence & Forensics (ID: SP17, ~2.5 w)
|
||||||
|
|
||||||
### Group SP17-G1 — Scanner Forensics (src/StellaOps.Scanner.Emit + WebService) ~1.2 w
|
### Group SP17-G1 — Scanner Forensics (src/StellaOps.Scanner.Emit + WebService) ~1.2 w
|
||||||
- Tasks: SCANNER-EMIT-17-701 (4 d), SCANNER-RUNTIME-17-401 (3 d)
|
- Tasks: SCANNER-EMIT-17-701 (4 d), SCANNER-RUNTIME-17-401 (3 d)
|
||||||
- Acceptance: forensic overlays add ≤150 ms per image; runtime API exposes symbol hints with feature flag.
|
- Acceptance: forensic overlays add ≤150 ms per image; runtime API exposes symbol hints with feature flag.
|
||||||
- Gate: forensic SBOM samples committed; API doc updated.
|
- Gate: forensic SBOM samples committed; API doc updated.
|
||||||
|
|
||||||
### Group SP17-G2 — Zastava Observability (src/StellaOps.Zastava.Observer) ~0.6 w
|
### Group SP17-G2 — Zastava Observability (src/StellaOps.Zastava.Observer) ~0.6 w
|
||||||
- Tasks: ZASTAVA-OBS-17-005 (3 d)
|
- Tasks: ZASTAVA-OBS-17-005 (3 d)
|
||||||
- Acceptance: new telemetry surfaces symbol diffs; observer CPU <10 % under load.
|
- Acceptance: new telemetry surfaces symbol diffs; observer CPU <10 % under load.
|
||||||
- Gate: Grafana dashboard export, alert thresholds defined.
|
- Gate: Grafana dashboard export, alert thresholds defined.
|
||||||
|
|
||||||
### Group SP17-G3 — Release Hardening (ops/devops) ~0.4 w
|
### Group SP17-G3 — Release Hardening (ops/devops) ~0.4 w
|
||||||
- Tasks: DEVOPS-REL-17-002 (2 d)
|
- Tasks: DEVOPS-REL-17-002 (2 d)
|
||||||
- Acceptance: deterministic build verifier job updated to include forensics artifacts.
|
- Acceptance: deterministic build verifier job updated to include forensics artifacts.
|
||||||
- Gate: CI pipeline stage `forensics-verify` green.
|
- Gate: CI pipeline stage `forensics-verify` green.
|
||||||
|
|
||||||
### Group SP17-G4 — Documentation (docs/) ~0.3 w
|
### Group SP17-G4 — Documentation (docs/) ~0.3 w
|
||||||
- Tasks: DOCS-RUNTIME-17-004 (2 d)
|
- Tasks: DOCS-RUNTIME-17-004 (2 d)
|
||||||
- Acceptance: runtime forensic guide published with troubleshooting.
|
- Acceptance: runtime forensic guide published with troubleshooting.
|
||||||
- Gate: docs review sign-off; links added to UI help.
|
- Gate: docs review sign-off; links added to UI help.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Integration Buffers
|
## Integration Buffers
|
||||||
- **INT-A (0.3 w, after SP10):** Image → SBOM → BOM-Index → Scheduler preview → UI dry-run using fixtures.
|
- **INT-A (0.3 w, after SP10):** Image → SBOM → BOM-Index → Scheduler preview → UI dry-run using fixtures.
|
||||||
- **INT-B (0.3 w, after SP11 & SP15):** SBOM → policy verdict → signed DSSE → Rekor entry → Notify delivery end-to-end.
|
- **INT-B (0.3 w, after SP11 & SP15):** SBOM → policy verdict → signed DSSE → Rekor entry → Notify delivery end-to-end.
|
||||||
|
|
||||||
## Parallelisation Strategy
|
## Parallelisation Strategy
|
||||||
- SP9 core modules and SP11 authority upgrades can progress in parallel; scanner clients rely on feature flags while DPoP/mTLS hardening lands.
|
- 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.
|
- 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.
|
- 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.
|
- UI (SP13) uses mocked endpoints early, decoupling front-end delivery from backend readiness.
|
||||||
|
|
||||||
## Risk Registry
|
## Risk Registry
|
||||||
|
|
||||||
| Risk ID | Description | Owner | Mitigation | Trigger |
|
| Risk ID | Description | Owner | Mitigation | Trigger |
|
||||||
|---------|-------------|-------|-----------|---------|
|
|---------|-------------|-------|-----------|---------|
|
||||||
| R1 | BOM-Index memory blow-up on large fleets | Scheduler ImpactIndex Guild | Shard + mmap plan; monitor BENCH-IMPACT-16-001 | RAM > 8 GB in bench |
|
| R1 | BOM-Index memory blow-up on large fleets | Scheduler ImpactIndex Guild | Shard + mmap plan; monitor BENCH-IMPACT-16-001 | RAM > 8 GB in bench |
|
||||||
| R2 | Buildx plugin latency regression | BuildX Guild | DEVOPS-PERF-10-001 guard; fallback to post-build scan | Buildx job >300 ms/layer |
|
| R2 | Buildx plugin latency regression | BuildX Guild | DEVOPS-PERF-10-001 guard; fallback to post-build scan | Buildx job >300 ms/layer |
|
||||||
| R3 | Notify digests flooding Slack | Notify Engine Guild | throttle defaults, BENCH-NOTIFY-15-001 coverage | Dropped messages >1 % |
|
| 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 |
|
| 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 |
|
| R5 | ImpactIndex stub lingers | Scheduler ImpactIndex Guild | Track SCHED-IMPACT-16-300 removal in sprint review | Stub present past SP16 |
|
||||||
| R6 | Symbol forensics slows runtime | Scanner Emit Guild | Feature flag; perf tests in SP17-G1 | Forensics adds >150 ms/image |
|
| R6 | Symbol forensics slows runtime | Scanner Emit Guild | Feature flag; perf tests in SP17-G1 | Forensics adds >150 ms/image |
|
||||||
|
|
||||||
## Envelope & ADR Governance
|
## Envelope & ADR Governance
|
||||||
- Event schemas (`docs/events/*.json`) versioned; producers must bump suffix on breaking changes.
|
- 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.
|
- 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.
|
**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.
|
||||||
|
|||||||
@@ -162,6 +162,10 @@ GET /catalog/artifacts/{id} → { meta }
|
|||||||
GET /healthz | /readyz | /metrics
|
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)
|
## 5) Execution flow (Worker)
|
||||||
@@ -433,6 +437,26 @@ ResolveEntrypoint(ImageConfig cfg, RootFs fs):
|
|||||||
return Unknown(reason)
|
return Unknown(reason)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Appendix A.1 — EntryTrace Explainability
|
||||||
|
|
||||||
|
EntryTrace emits structured diagnostics and metrics so operators can quickly understand why resolution succeeded or degraded:
|
||||||
|
|
||||||
|
| Reason | Description | Typical Mitigation |
|
||||||
|
|--------|-------------|--------------------|
|
||||||
|
| `CommandNotFound` | A command referenced in the script cannot be located in the layered root filesystem or `PATH`. | Ensure binaries exist in the image or extend `PATH` hints. |
|
||||||
|
| `MissingFile` | `source`/`.`/`run-parts` targets are missing. | Bundle the script or guard the include. |
|
||||||
|
| `DynamicEnvironmentReference` | Path depends on `$VARS` that are unknown at scan time. | Provide defaults via scan metadata or accept partial usage. |
|
||||||
|
| `RecursionLimitReached` | Nested includes exceeded the analyzer depth limit (default 64). | Flatten indirection or increase the limit in options. |
|
||||||
|
| `RunPartsEmpty` | `run-parts` directory contained no executable entries. | Remove empty directories or ignore if intentional. |
|
||||||
|
| `JarNotFound` / `ModuleNotFound` | Java/Python targets missing, preventing interpreter tracing. | Ship the jar/module with the image or adjust the launcher. |
|
||||||
|
|
||||||
|
Diagnostics drive two metrics published by `EntryTraceMetrics`:
|
||||||
|
|
||||||
|
- `entrytrace_resolutions_total{outcome}` — resolution attempts segmented by outcome (`resolved`, `partiallyresolved`, `unresolved`).
|
||||||
|
- `entrytrace_unresolved_total{reason}` — diagnostic counts keyed by reason.
|
||||||
|
|
||||||
|
Structured logs include `entrytrace.path`, `entrytrace.command`, `entrytrace.reason`, and `entrytrace.depth`, all correlated with scan/job IDs. Timestamps are normalized to UTC (microsecond precision) to keep DSSE attestations and UI traces explainable.
|
||||||
|
|
||||||
### Appendix B — BOM‑Index sidecar
|
### Appendix B — BOM‑Index sidecar
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,116 +1,116 @@
|
|||||||
# BuildX Generator Quickstart
|
# 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.
|
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
|
## 1. Prerequisites
|
||||||
|
|
||||||
- Docker 25+ with BuildKit enabled (`docker buildx` available).
|
- Docker 25+ with BuildKit enabled (`docker buildx` available).
|
||||||
- .NET 10 (preview) SDK matching the repository `global.json`.
|
- .NET 10 (preview) SDK matching the repository `global.json`.
|
||||||
- Optional: network access to a StellaOps Attestor endpoint (the quickstart uses a mock service).
|
- Optional: network access to a StellaOps Attestor endpoint (the quickstart uses a mock service).
|
||||||
|
|
||||||
## 2. Publish the plug-in binaries
|
## 2. Publish the plug-in binaries
|
||||||
|
|
||||||
The BuildX generator publishes as a .NET self-contained executable with its manifest under `plugins/scanner/buildx/`.
|
The BuildX generator publishes as a .NET self-contained executable with its manifest under `plugins/scanner/buildx/`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# From the repository root
|
# From the repository root
|
||||||
DOTNET_CLI_HOME="${PWD}/.dotnet" \
|
DOTNET_CLI_HOME="${PWD}/.dotnet" \
|
||||||
dotnet publish src/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj \
|
dotnet publish src/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj \
|
||||||
-c Release \
|
-c Release \
|
||||||
-o out/buildx
|
-o out/buildx
|
||||||
```
|
```
|
||||||
|
|
||||||
- `out/buildx/` now contains `StellaOps.Scanner.Sbomer.BuildXPlugin.dll` and the manifest `stellaops.sbom-indexer.manifest.json`.
|
- `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.
|
- `plugins/scanner/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin/` receives the same artefacts for release packaging.
|
||||||
|
|
||||||
## 3. Verify the CAS handshake
|
## 3. Verify the CAS handshake
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll handshake \
|
dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll handshake \
|
||||||
--manifest out/buildx \
|
--manifest out/buildx \
|
||||||
--cas out/cas
|
--cas out/cas
|
||||||
```
|
```
|
||||||
|
|
||||||
The command performs a deterministic probe write (`sha256`) into the provided CAS directory and prints the resolved path.
|
The command performs a deterministic probe write (`sha256`) into the provided CAS directory and prints the resolved path.
|
||||||
|
|
||||||
## 4. Emit a descriptor + provenance placeholder
|
## 4. Emit a descriptor + provenance placeholder
|
||||||
|
|
||||||
1. Build or identify the image you want to describe and capture its digest:
|
1. Build or identify the image you want to describe and capture its digest:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker buildx build --load -t stellaops/buildx-demo:ci samples/ci/buildx-demo
|
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}}')
|
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`):
|
2. Generate a CycloneDX SBOM for the built image (any tool works; here we use `docker sbom`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker sbom stellaops/buildx-demo:ci --format cyclonedx-json > out/buildx-sbom.cdx.json
|
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:
|
3. Invoke the `descriptor` command, pointing at the SBOM file and optional metadata:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll descriptor \
|
dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll descriptor \
|
||||||
--manifest out/buildx \
|
--manifest out/buildx \
|
||||||
--image "$DIGEST" \
|
--image "$DIGEST" \
|
||||||
--sbom out/buildx-sbom.cdx.json \
|
--sbom out/buildx-sbom.cdx.json \
|
||||||
--sbom-name buildx-sbom.cdx.json \
|
--sbom-name buildx-sbom.cdx.json \
|
||||||
--artifact-type application/vnd.stellaops.sbom.layer+json \
|
--artifact-type application/vnd.stellaops.sbom.layer+json \
|
||||||
--sbom-format cyclonedx-json \
|
--sbom-format cyclonedx-json \
|
||||||
--sbom-kind inventory \
|
--sbom-kind inventory \
|
||||||
--repository git.stella-ops.org/stellaops/buildx-demo \
|
--repository git.stella-ops.org/stellaops/buildx-demo \
|
||||||
--build-ref $(git rev-parse HEAD) \
|
--build-ref $(git rev-parse HEAD) \
|
||||||
> out/buildx-descriptor.json
|
> out/buildx-descriptor.json
|
||||||
```
|
```
|
||||||
|
|
||||||
The output JSON captures:
|
The output JSON captures:
|
||||||
|
|
||||||
- OCI artifact descriptor including size, digest, and annotations (`org.stellaops.*`).
|
- 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.
|
- Generator metadata and deterministic timestamps.
|
||||||
|
|
||||||
## 5. (Optional) Send the placeholder to an Attestor
|
## 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.
|
The plug-in can POST the descriptor metadata to an Attestor endpoint, returning once it receives an HTTP 202.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 - <<'PY' &
|
python3 - <<'PY' &
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
_ = self.rfile.read(int(self.headers.get('Content-Length', 0)))
|
_ = self.rfile.read(int(self.headers.get('Content-Length', 0)))
|
||||||
self.send_response(202); self.end_headers(); self.wfile.write(b'accepted')
|
self.send_response(202); self.end_headers(); self.wfile.write(b'accepted')
|
||||||
def log_message(self, fmt, *args):
|
def log_message(self, fmt, *args):
|
||||||
return
|
return
|
||||||
server = HTTPServer(('127.0.0.1', 8085), Handler)
|
server = HTTPServer(('127.0.0.1', 8085), Handler)
|
||||||
try:
|
try:
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
server.server_close()
|
server.server_close()
|
||||||
PY
|
PY
|
||||||
MOCK_PID=$!
|
MOCK_PID=$!
|
||||||
|
|
||||||
dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll descriptor \
|
dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll descriptor \
|
||||||
--manifest out/buildx \
|
--manifest out/buildx \
|
||||||
--image "$DIGEST" \
|
--image "$DIGEST" \
|
||||||
--sbom out/buildx-sbom.cdx.json \
|
--sbom out/buildx-sbom.cdx.json \
|
||||||
--attestor http://127.0.0.1:8085/provenance \
|
--attestor http://127.0.0.1:8085/provenance \
|
||||||
--attestor-token "$STELLAOPS_ATTESTOR_TOKEN" \
|
--attestor-token "$STELLAOPS_ATTESTOR_TOKEN" \
|
||||||
> out/buildx-descriptor.json
|
> out/buildx-descriptor.json
|
||||||
|
|
||||||
kill $MOCK_PID
|
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.
|
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
|
## 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.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,85 +1,85 @@
|
|||||||
name: Buildx SBOM Demo
|
name: Buildx SBOM Demo
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [ demo/buildx ]
|
branches: [ demo/buildx ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
buildx-sbom:
|
buildx-sbom:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Set up .NET 10 preview
|
- name: Set up .NET 10 preview
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: '10.0.x'
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
- name: Publish StellaOps BuildX generator
|
- name: Publish StellaOps BuildX generator
|
||||||
run: |
|
run: |
|
||||||
dotnet publish src/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj \
|
dotnet publish src/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj \
|
||||||
-c Release \
|
-c Release \
|
||||||
-o out/buildx
|
-o out/buildx
|
||||||
|
|
||||||
- name: Handshake CAS
|
- name: Handshake CAS
|
||||||
run: |
|
run: |
|
||||||
dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll handshake \
|
dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll handshake \
|
||||||
--manifest out/buildx \
|
--manifest out/buildx \
|
||||||
--cas out/cas
|
--cas out/cas
|
||||||
|
|
||||||
- name: Build demo container image
|
- name: Build demo container image
|
||||||
run: |
|
run: |
|
||||||
docker buildx build --load -t stellaops/buildx-demo:ci samples/ci/buildx-demo
|
docker buildx build --load -t stellaops/buildx-demo:ci samples/ci/buildx-demo
|
||||||
|
|
||||||
- name: Capture image digest
|
- name: Capture image digest
|
||||||
id: digest
|
id: digest
|
||||||
run: |
|
run: |
|
||||||
DIGEST=$(docker image inspect stellaops/buildx-demo:ci --format '{{index .RepoDigests 0}}')
|
DIGEST=$(docker image inspect stellaops/buildx-demo:ci --format '{{index .RepoDigests 0}}')
|
||||||
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
|
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Generate SBOM from built image
|
- name: Generate SBOM from built image
|
||||||
run: |
|
run: |
|
||||||
mkdir -p out
|
mkdir -p out
|
||||||
docker sbom stellaops/buildx-demo:ci --format cyclonedx-json > out/buildx-sbom.cdx.json
|
docker sbom stellaops/buildx-demo:ci --format cyclonedx-json > out/buildx-sbom.cdx.json
|
||||||
|
|
||||||
- name: Start mock Attestor
|
- name: Start mock Attestor
|
||||||
id: attestor
|
id: attestor
|
||||||
run: |
|
run: |
|
||||||
mkdir -p out
|
mkdir -p out
|
||||||
cat <<'PY' > out/mock-attestor.py
|
cat <<'PY' > out/mock-attestor.py
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
length = int(self.headers.get('Content-Length') or 0)
|
length = int(self.headers.get('Content-Length') or 0)
|
||||||
body = self.rfile.read(length)
|
body = self.rfile.read(length)
|
||||||
with open(os.path.join('out', 'provenance-request.json'), 'wb') as fp:
|
with open(os.path.join('out', 'provenance-request.json'), 'wb') as fp:
|
||||||
fp.write(body)
|
fp.write(body)
|
||||||
self.send_response(202)
|
self.send_response(202)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(b'accepted')
|
self.wfile.write(b'accepted')
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
def log_message(self, format, *args):
|
||||||
return
|
return
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
server = HTTPServer(('127.0.0.1', 8085), Handler)
|
server = HTTPServer(('127.0.0.1', 8085), Handler)
|
||||||
try:
|
try:
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
server.server_close()
|
server.server_close()
|
||||||
PY
|
PY
|
||||||
touch out/provenance-request.json
|
touch out/provenance-request.json
|
||||||
python3 out/mock-attestor.py &
|
python3 out/mock-attestor.py &
|
||||||
echo $! > out/mock-attestor.pid
|
echo $! > out/mock-attestor.pid
|
||||||
|
|
||||||
- name: Emit descriptor with provenance placeholder
|
- name: Emit descriptor with provenance placeholder
|
||||||
env:
|
env:
|
||||||
IMAGE_DIGEST: ${{ steps.digest.outputs.digest }}
|
IMAGE_DIGEST: ${{ steps.digest.outputs.digest }}
|
||||||
@@ -99,22 +99,55 @@ PY
|
|||||||
--attestor http://127.0.0.1:8085/provenance \
|
--attestor http://127.0.0.1:8085/provenance \
|
||||||
> out/buildx-descriptor.json
|
> 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
|
- name: Stop mock Attestor
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
if [ -f out/mock-attestor.pid ]; then
|
if [ -f out/mock-attestor.pid ]; then
|
||||||
kill $(cat out/mock-attestor.pid)
|
kill $(cat out/mock-attestor.pid)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: stellaops-buildx-demo
|
name: stellaops-buildx-demo
|
||||||
path: |
|
path: |
|
||||||
out/buildx-descriptor.json
|
out/buildx-descriptor.json
|
||||||
out/buildx-sbom.cdx.json
|
out/buildx-sbom.cdx.json
|
||||||
out/provenance-request.json
|
out/provenance-request.json
|
||||||
|
out/buildx-descriptor-repeat.json
|
||||||
- name: Show descriptor summary
|
|
||||||
run: |
|
- name: Show descriptor summary
|
||||||
cat out/buildx-descriptor.json
|
run: |
|
||||||
|
cat out/buildx-descriptor.json
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/StellaOps.Scanner.EntryTrace.Tests/ShellParserTests.cs
Normal file
33
src/StellaOps.Scanner.EntryTrace.Tests/ShellParserTests.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
180
src/StellaOps.Scanner.EntryTrace.Tests/TestRootFileSystem.cs
Normal file
180
src/StellaOps.Scanner.EntryTrace.Tests/TestRootFileSystem.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/StellaOps.Scanner.EntryTrace/AGENTS.md
Normal file
32
src/StellaOps.Scanner.EntryTrace/AGENTS.md
Normal 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.
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
963
src/StellaOps.Scanner.EntryTrace/EntryTraceAnalyzer.cs
Normal file
963
src/StellaOps.Scanner.EntryTrace/EntryTraceAnalyzer.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
16
src/StellaOps.Scanner.EntryTrace/EntryTraceContext.cs
Normal file
16
src/StellaOps.Scanner.EntryTrace/EntryTraceContext.cs
Normal 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);
|
||||||
125
src/StellaOps.Scanner.EntryTrace/EntryTraceTypes.cs
Normal file
125
src/StellaOps.Scanner.EntryTrace/EntryTraceTypes.cs
Normal 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);
|
||||||
71
src/StellaOps.Scanner.EntryTrace/EntrypointSpecification.cs
Normal file
71
src/StellaOps.Scanner.EntryTrace/EntrypointSpecification.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
9
src/StellaOps.Scanner.EntryTrace/IEntryTraceAnalyzer.cs
Normal file
9
src/StellaOps.Scanner.EntryTrace/IEntryTraceAnalyzer.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace StellaOps.Scanner.EntryTrace;
|
||||||
|
|
||||||
|
public interface IEntryTraceAnalyzer
|
||||||
|
{
|
||||||
|
ValueTask<EntryTraceGraph> ResolveAsync(
|
||||||
|
EntrypointSpecification entrypoint,
|
||||||
|
EntryTraceContext context,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
54
src/StellaOps.Scanner.EntryTrace/Parsing/ShellNodes.cs
Normal file
54
src/StellaOps.Scanner.EntryTrace/Parsing/ShellNodes.cs
Normal 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);
|
||||||
485
src/StellaOps.Scanner.EntryTrace/Parsing/ShellParser.cs
Normal file
485
src/StellaOps.Scanner.EntryTrace/Parsing/ShellParser.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/StellaOps.Scanner.EntryTrace/Parsing/ShellToken.cs
Normal file
16
src/StellaOps.Scanner.EntryTrace/Parsing/ShellToken.cs
Normal 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
|
||||||
|
}
|
||||||
200
src/StellaOps.Scanner.EntryTrace/Parsing/ShellTokenizer.cs
Normal file
200
src/StellaOps.Scanner.EntryTrace/Parsing/ShellTokenizer.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
11
src/StellaOps.Scanner.EntryTrace/TASKS.md
Normal file
11
src/StellaOps.Scanner.EntryTrace/TASKS.md
Normal 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`. |
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\StellaOps.Scanner.Sbomer.BuildXPlugin\StellaOps.Scanner.Sbomer.BuildXPlugin.csproj" />
|
<ProjectReference Include="..\StellaOps.Scanner.Sbomer.BuildXPlugin\StellaOps.Scanner.Sbomer.BuildXPlugin.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="Fixtures\descriptor.baseline.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,180 +1,209 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds immutable OCI descriptors enriched with provenance placeholders.
|
/// Builds immutable OCI descriptors enriched with provenance placeholders.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class DescriptorGenerator
|
public sealed class DescriptorGenerator
|
||||||
{
|
{
|
||||||
public const string Schema = "stellaops.buildx.descriptor.v1";
|
public const string Schema = "stellaops.buildx.descriptor.v1";
|
||||||
|
|
||||||
private readonly TimeProvider timeProvider;
|
private readonly TimeProvider timeProvider;
|
||||||
|
|
||||||
public DescriptorGenerator(TimeProvider timeProvider)
|
public DescriptorGenerator(TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
timeProvider ??= TimeProvider.System;
|
timeProvider ??= TimeProvider.System;
|
||||||
this.timeProvider = timeProvider;
|
this.timeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DescriptorDocument> CreateAsync(DescriptorRequest request, CancellationToken cancellationToken)
|
public async Task<DescriptorDocument> CreateAsync(DescriptorRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (request is null)
|
if (request is null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(request));
|
throw new ArgumentNullException(nameof(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(request.ImageDigest))
|
if (string.IsNullOrWhiteSpace(request.ImageDigest))
|
||||||
{
|
{
|
||||||
throw new BuildxPluginException("Image digest must be provided.");
|
throw new BuildxPluginException("Image digest must be provided.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(request.SbomPath))
|
if (string.IsNullOrWhiteSpace(request.SbomPath))
|
||||||
{
|
{
|
||||||
throw new BuildxPluginException("SBOM path must be provided.");
|
throw new BuildxPluginException("SBOM path must be provided.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var sbomFile = new FileInfo(request.SbomPath);
|
var sbomFile = new FileInfo(request.SbomPath);
|
||||||
if (!sbomFile.Exists)
|
if (!sbomFile.Exists)
|
||||||
{
|
{
|
||||||
throw new BuildxPluginException($"SBOM file '{request.SbomPath}' was not found.");
|
throw new BuildxPluginException($"SBOM file '{request.SbomPath}' was not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var sbomDigest = await ComputeFileDigestAsync(sbomFile, cancellationToken).ConfigureAwait(false);
|
var sbomDigest = await ComputeFileDigestAsync(sbomFile, cancellationToken).ConfigureAwait(false);
|
||||||
|
var nonce = ComputeDeterministicNonce(request, sbomFile, sbomDigest);
|
||||||
var nonce = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
|
||||||
var expectedDsseSha = ComputeExpectedDsseDigest(request.ImageDigest, sbomDigest, nonce);
|
var expectedDsseSha = ComputeExpectedDsseDigest(request.ImageDigest, sbomDigest, nonce);
|
||||||
|
|
||||||
var artifactAnnotations = BuildArtifactAnnotations(request, nonce, expectedDsseSha);
|
var artifactAnnotations = BuildArtifactAnnotations(request, nonce, expectedDsseSha);
|
||||||
|
|
||||||
var subject = new DescriptorSubject(
|
var subject = new DescriptorSubject(
|
||||||
MediaType: request.SubjectMediaType,
|
MediaType: request.SubjectMediaType,
|
||||||
Digest: request.ImageDigest);
|
Digest: request.ImageDigest);
|
||||||
|
|
||||||
var artifact = new DescriptorArtifact(
|
var artifact = new DescriptorArtifact(
|
||||||
MediaType: request.SbomMediaType,
|
MediaType: request.SbomMediaType,
|
||||||
Digest: sbomDigest,
|
Digest: sbomDigest,
|
||||||
Size: sbomFile.Length,
|
Size: sbomFile.Length,
|
||||||
Annotations: artifactAnnotations);
|
Annotations: artifactAnnotations);
|
||||||
|
|
||||||
var provenance = new DescriptorProvenance(
|
var provenance = new DescriptorProvenance(
|
||||||
Status: "pending",
|
Status: "pending",
|
||||||
ExpectedDsseSha256: expectedDsseSha,
|
ExpectedDsseSha256: expectedDsseSha,
|
||||||
Nonce: nonce,
|
Nonce: nonce,
|
||||||
AttestorUri: request.AttestorUri,
|
AttestorUri: request.AttestorUri,
|
||||||
PredicateType: request.PredicateType);
|
PredicateType: request.PredicateType);
|
||||||
|
|
||||||
var generatorMetadata = new DescriptorGeneratorMetadata(
|
var generatorMetadata = new DescriptorGeneratorMetadata(
|
||||||
Name: request.GeneratorName ?? "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
Name: request.GeneratorName ?? "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
||||||
Version: request.GeneratorVersion);
|
Version: request.GeneratorVersion);
|
||||||
|
|
||||||
var metadata = BuildDocumentMetadata(request, sbomFile, sbomDigest);
|
var metadata = BuildDocumentMetadata(request, sbomFile, sbomDigest);
|
||||||
|
|
||||||
return new DescriptorDocument(
|
return new DescriptorDocument(
|
||||||
Schema: Schema,
|
Schema: Schema,
|
||||||
GeneratedAt: timeProvider.GetUtcNow(),
|
GeneratedAt: timeProvider.GetUtcNow(),
|
||||||
Generator: generatorMetadata,
|
Generator: generatorMetadata,
|
||||||
Subject: subject,
|
Subject: subject,
|
||||||
Artifact: artifact,
|
Artifact: artifact,
|
||||||
Provenance: provenance,
|
Provenance: provenance,
|
||||||
Metadata: metadata);
|
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(
|
var builder = new StringBuilder();
|
||||||
file.FullName,
|
builder.AppendLine("stellaops.buildx.nonce.v1");
|
||||||
FileMode.Open,
|
builder.AppendLine(request.ImageDigest);
|
||||||
FileAccess.Read,
|
builder.AppendLine(sbomDigest);
|
||||||
FileShare.Read,
|
builder.AppendLine(sbomFile.Length.ToString(CultureInfo.InvariantCulture));
|
||||||
bufferSize: 128 * 1024,
|
builder.AppendLine(request.SbomMediaType);
|
||||||
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
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 payload = Encoding.UTF8.GetBytes(builder.ToString());
|
||||||
|
|
||||||
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];
|
Span<byte> hash = stackalloc byte[32];
|
||||||
SHA256.HashData(bytes, hash);
|
SHA256.HashData(payload, hash);
|
||||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
|
||||||
|
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)
|
private static async Task<string> ComputeFileDigestAsync(FileInfo file, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var annotations = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
await using var stream = new FileStream(
|
||||||
{
|
file.FullName,
|
||||||
["org.opencontainers.artifact.type"] = request.SbomArtifactType,
|
FileMode.Open,
|
||||||
["org.stellaops.scanner.version"] = request.GeneratorVersion,
|
FileAccess.Read,
|
||||||
["org.stellaops.sbom.kind"] = request.SbomKind,
|
FileShare.Read,
|
||||||
["org.stellaops.sbom.format"] = request.SbomFormat,
|
bufferSize: 128 * 1024,
|
||||||
["org.stellaops.provenance.status"] = "pending",
|
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||||
["org.stellaops.provenance.dsse.sha256"] = expectedDsse,
|
|
||||||
["org.stellaops.provenance.nonce"] = nonce
|
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
|
||||||
};
|
|
||||||
|
var buffer = new byte[128 * 1024];
|
||||||
if (!string.IsNullOrWhiteSpace(request.LicenseId))
|
int bytesRead;
|
||||||
{
|
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
|
||||||
annotations["org.stellaops.license.id"] = request.LicenseId!;
|
{
|
||||||
}
|
hash.AppendData(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
if (!string.IsNullOrWhiteSpace(request.SbomName))
|
|
||||||
{
|
var digest = hash.GetHashAndReset();
|
||||||
annotations["org.opencontainers.image.title"] = request.SbomName!;
|
return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.Repository))
|
private static string ComputeExpectedDsseDigest(string imageDigest, string sbomDigest, string nonce)
|
||||||
{
|
{
|
||||||
annotations["org.stellaops.repository"] = request.Repository!;
|
var payload = $"{imageDigest}\n{sbomDigest}\n{nonce}";
|
||||||
}
|
var bytes = System.Text.Encoding.UTF8.GetBytes(payload);
|
||||||
|
Span<byte> hash = stackalloc byte[32];
|
||||||
return annotations;
|
SHA256.HashData(bytes, hash);
|
||||||
}
|
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||||
|
}
|
||||||
private static IReadOnlyDictionary<string, string> BuildDocumentMetadata(DescriptorRequest request, FileInfo fileInfo, string sbomDigest)
|
|
||||||
{
|
private static IReadOnlyDictionary<string, string> BuildArtifactAnnotations(DescriptorRequest request, string nonce, string expectedDsse)
|
||||||
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
{
|
||||||
{
|
var annotations = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
["sbomDigest"] = sbomDigest,
|
{
|
||||||
["sbomPath"] = fileInfo.FullName,
|
["org.opencontainers.artifact.type"] = request.SbomArtifactType,
|
||||||
["sbomMediaType"] = request.SbomMediaType,
|
["org.stellaops.scanner.version"] = request.GeneratorVersion,
|
||||||
["subjectMediaType"] = request.SubjectMediaType
|
["org.stellaops.sbom.kind"] = request.SbomKind,
|
||||||
};
|
["org.stellaops.sbom.format"] = request.SbomFormat,
|
||||||
|
["org.stellaops.provenance.status"] = "pending",
|
||||||
if (!string.IsNullOrWhiteSpace(request.Repository))
|
["org.stellaops.provenance.dsse.sha256"] = expectedDsse,
|
||||||
{
|
["org.stellaops.provenance.nonce"] = nonce
|
||||||
metadata["repository"] = request.Repository!;
|
};
|
||||||
}
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.LicenseId))
|
||||||
if (!string.IsNullOrWhiteSpace(request.BuildRef))
|
{
|
||||||
{
|
annotations["org.stellaops.license.id"] = request.LicenseId!;
|
||||||
metadata["buildRef"] = request.BuildRef!;
|
}
|
||||||
}
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.SbomName))
|
||||||
if (!string.IsNullOrWhiteSpace(request.AttestorUri))
|
{
|
||||||
{
|
annotations["org.opencontainers.image.title"] = request.SbomName!;
|
||||||
metadata["attestorUri"] = request.AttestorUri!;
|
}
|
||||||
}
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.Repository))
|
||||||
return metadata;
|
{
|
||||||
}
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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-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-002 | DONE | BuildX Guild | SP9-BLDX-09-001 | Emit OCI annotations + provenance metadata for Attestor handoff (image + SBOM). | OCI descriptors include DSSE/provenance placeholders; Attestor mock accepts payload. |
|
||||||
| SP9-BLDX-09-003 | DONE | BuildX Guild | SP9-BLDX-09-002 | CI demo pipeline: build sample image, produce SBOM, verify backend report wiring. | GitHub/CI job runs sample build within 5 s overhead; artifacts saved; documentation updated. |
|
| SP9-BLDX-09-003 | DONE | BuildX Guild | SP9-BLDX-09-002 | CI demo pipeline: build sample image, produce SBOM, verify backend report wiring. | GitHub/CI job runs sample build within 5 s overhead; artifacts saved; documentation updated. |
|
||||||
|
| SP9-BLDX-09-004 | DONE (2025-10-19) | BuildX Guild | SP9-BLDX-09-002 | Stabilize descriptor nonce derivation so repeated builds emit deterministic placeholders. | Repeated descriptor runs with fixed inputs yield identical JSON; regression tests cover nonce determinism. |
|
||||||
|
| SP9-BLDX-09-005 | DONE (2025-10-19) | BuildX Guild | SP9-BLDX-09-004 | Integrate determinism check in GitHub/Gitea workflows and capture sample artifacts. | Determinism step runs in `.gitea/workflows/build-test-deploy.yml` and `samples/ci/buildx-demo`, producing matching descriptors + archived artifacts. |
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
# AGENTS
|
# AGENTS
|
||||||
## Role
|
## Role
|
||||||
Scanner.Worker engineers own the queue-driven execution host that turns scan jobs into SBOM artefacts with deterministic progress reporting.
|
Scanner.Worker engineers own the queue-driven execution host that turns scan jobs into SBOM artefacts with deterministic progress reporting.
|
||||||
## Scope
|
## Scope
|
||||||
- Host bootstrap: configuration binding, Authority client wiring, graceful shutdown, restart-time plug-in discovery hooks.
|
- 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.
|
- Job acquisition & lease renewal semantics backed by the Scanner queue abstraction.
|
||||||
- Analyzer orchestration skeleton: stage pipeline, cancellation awareness, deterministic progress emissions.
|
- Analyzer orchestration skeleton: stage pipeline, cancellation awareness, deterministic progress emissions.
|
||||||
- Telemetry: structured logging, OpenTelemetry metrics/traces, health counters for offline diagnostics.
|
- Telemetry: structured logging, OpenTelemetry metrics/traces, health counters for offline diagnostics.
|
||||||
## Participants
|
## Participants
|
||||||
- Consumes jobs from `StellaOps.Scanner.Queue`.
|
- Consumes jobs from `StellaOps.Scanner.Queue`.
|
||||||
- Persists progress/artifacts via `StellaOps.Scanner.Storage` once those modules land.
|
- Persists progress/artifacts via `StellaOps.Scanner.Storage` once those modules land.
|
||||||
- Emits metrics and structured logs consumed by Observability stack & WebService status endpoints.
|
- Emits metrics and structured logs consumed by Observability stack & WebService status endpoints.
|
||||||
## Interfaces & contracts
|
## Interfaces & contracts
|
||||||
- Queue lease abstraction (`IScanJobLease`, `IScanJobSource`) with deterministic identifiers and attempt counters.
|
- Queue lease abstraction (`IScanJobLease`, `IScanJobSource`) with deterministic identifiers and attempt counters.
|
||||||
- Analyzer dispatcher contracts for OS/lang/native analyzers and emitters.
|
- Analyzer dispatcher contracts for OS/lang/native analyzers and emitters.
|
||||||
- Telemetry resource attributes shared with Scanner.WebService and Scheduler.
|
- Telemetry resource attributes shared with Scanner.WebService and Scheduler.
|
||||||
## In/Out of scope
|
## In/Out of scope
|
||||||
In scope: worker host, concurrency orchestration, lease renewal, cancellation wiring, deterministic logging/metrics.
|
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.
|
Out of scope: queue provider implementations, analyzer business logic, Mongo/object-store repositories.
|
||||||
## Observability expectations
|
## Observability expectations
|
||||||
- Meter `StellaOps.Scanner.Worker` with queue latency, stage duration, failure counters.
|
- Meter `StellaOps.Scanner.Worker` with queue latency, stage duration, failure counters.
|
||||||
- Activity source `StellaOps.Scanner.Worker.Job` for per-job tracing.
|
- 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.
|
- Log correlation IDs (`jobId`, `leaseId`, `scanId`) with structured payloads; avoid dumping secrets or full manifests.
|
||||||
## Tests
|
## Tests
|
||||||
- Integration fixture `WorkerBasicScanScenario` verifying acquisition → heartbeat → analyzer stages → completion.
|
- Integration fixture `WorkerBasicScanScenario` verifying acquisition → heartbeat → analyzer stages → completion.
|
||||||
- Unit tests around retry/jitter calculators as they are introduced.
|
- Unit tests around retry/jitter calculators as they are introduced.
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.Metrics;
|
using System.Diagnostics.Metrics;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Diagnostics;
|
namespace StellaOps.Scanner.Worker.Diagnostics;
|
||||||
|
|
||||||
public static class ScannerWorkerInstrumentation
|
public static class ScannerWorkerInstrumentation
|
||||||
{
|
{
|
||||||
public const string ActivitySourceName = "StellaOps.Scanner.Worker.Job";
|
public const string ActivitySourceName = "StellaOps.Scanner.Worker.Job";
|
||||||
|
|
||||||
public const string MeterName = "StellaOps.Scanner.Worker";
|
public const string MeterName = "StellaOps.Scanner.Worker";
|
||||||
|
|
||||||
public static ActivitySource ActivitySource { get; } = new(ActivitySourceName);
|
public static ActivitySource ActivitySource { get; } = new(ActivitySourceName);
|
||||||
|
|
||||||
public static Meter Meter { get; } = new(MeterName, version: "1.0.0");
|
public static Meter Meter { get; } = new(MeterName, version: "1.0.0");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,109 +1,109 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.Metrics;
|
using System.Diagnostics.Metrics;
|
||||||
using StellaOps.Scanner.Worker.Processing;
|
using StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Diagnostics;
|
namespace StellaOps.Scanner.Worker.Diagnostics;
|
||||||
|
|
||||||
public sealed class ScannerWorkerMetrics
|
public sealed class ScannerWorkerMetrics
|
||||||
{
|
{
|
||||||
private readonly Histogram<double> _queueLatencyMs;
|
private readonly Histogram<double> _queueLatencyMs;
|
||||||
private readonly Histogram<double> _jobDurationMs;
|
private readonly Histogram<double> _jobDurationMs;
|
||||||
private readonly Histogram<double> _stageDurationMs;
|
private readonly Histogram<double> _stageDurationMs;
|
||||||
private readonly Counter<long> _jobsCompleted;
|
private readonly Counter<long> _jobsCompleted;
|
||||||
private readonly Counter<long> _jobsFailed;
|
private readonly Counter<long> _jobsFailed;
|
||||||
|
|
||||||
public ScannerWorkerMetrics()
|
public ScannerWorkerMetrics()
|
||||||
{
|
{
|
||||||
_queueLatencyMs = ScannerWorkerInstrumentation.Meter.CreateHistogram<double>(
|
_queueLatencyMs = ScannerWorkerInstrumentation.Meter.CreateHistogram<double>(
|
||||||
"scanner_worker_queue_latency_ms",
|
"scanner_worker_queue_latency_ms",
|
||||||
unit: "ms",
|
unit: "ms",
|
||||||
description: "Time from job enqueue to lease acquisition.");
|
description: "Time from job enqueue to lease acquisition.");
|
||||||
_jobDurationMs = ScannerWorkerInstrumentation.Meter.CreateHistogram<double>(
|
_jobDurationMs = ScannerWorkerInstrumentation.Meter.CreateHistogram<double>(
|
||||||
"scanner_worker_job_duration_ms",
|
"scanner_worker_job_duration_ms",
|
||||||
unit: "ms",
|
unit: "ms",
|
||||||
description: "Total processing duration per job.");
|
description: "Total processing duration per job.");
|
||||||
_stageDurationMs = ScannerWorkerInstrumentation.Meter.CreateHistogram<double>(
|
_stageDurationMs = ScannerWorkerInstrumentation.Meter.CreateHistogram<double>(
|
||||||
"scanner_worker_stage_duration_ms",
|
"scanner_worker_stage_duration_ms",
|
||||||
unit: "ms",
|
unit: "ms",
|
||||||
description: "Stage execution duration per job.");
|
description: "Stage execution duration per job.");
|
||||||
_jobsCompleted = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
|
_jobsCompleted = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
|
||||||
"scanner_worker_jobs_completed_total",
|
"scanner_worker_jobs_completed_total",
|
||||||
description: "Number of successfully completed scan jobs.");
|
description: "Number of successfully completed scan jobs.");
|
||||||
_jobsFailed = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
|
_jobsFailed = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
|
||||||
"scanner_worker_jobs_failed_total",
|
"scanner_worker_jobs_failed_total",
|
||||||
description: "Number of scan jobs that failed permanently.");
|
description: "Number of scan jobs that failed permanently.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RecordQueueLatency(ScanJobContext context, TimeSpan latency)
|
public void RecordQueueLatency(ScanJobContext context, TimeSpan latency)
|
||||||
{
|
{
|
||||||
if (latency <= TimeSpan.Zero)
|
if (latency <= TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_queueLatencyMs.Record(latency.TotalMilliseconds, CreateTags(context));
|
_queueLatencyMs.Record(latency.TotalMilliseconds, CreateTags(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RecordJobDuration(ScanJobContext context, TimeSpan duration)
|
public void RecordJobDuration(ScanJobContext context, TimeSpan duration)
|
||||||
{
|
{
|
||||||
if (duration <= TimeSpan.Zero)
|
if (duration <= TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_jobDurationMs.Record(duration.TotalMilliseconds, CreateTags(context));
|
_jobDurationMs.Record(duration.TotalMilliseconds, CreateTags(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RecordStageDuration(ScanJobContext context, string stage, TimeSpan duration)
|
public void RecordStageDuration(ScanJobContext context, string stage, TimeSpan duration)
|
||||||
{
|
{
|
||||||
if (duration <= TimeSpan.Zero)
|
if (duration <= TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_stageDurationMs.Record(duration.TotalMilliseconds, CreateTags(context, stage: stage));
|
_stageDurationMs.Record(duration.TotalMilliseconds, CreateTags(context, stage: stage));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void IncrementJobCompleted(ScanJobContext context)
|
public void IncrementJobCompleted(ScanJobContext context)
|
||||||
{
|
{
|
||||||
_jobsCompleted.Add(1, CreateTags(context));
|
_jobsCompleted.Add(1, CreateTags(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void IncrementJobFailed(ScanJobContext context, string failureReason)
|
public void IncrementJobFailed(ScanJobContext context, string failureReason)
|
||||||
{
|
{
|
||||||
_jobsFailed.Add(1, CreateTags(context, failureReason: failureReason));
|
_jobsFailed.Add(1, CreateTags(context, failureReason: failureReason));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static KeyValuePair<string, object?>[] CreateTags(ScanJobContext context, string? stage = null, string? failureReason = null)
|
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)
|
var tags = new List<KeyValuePair<string, object?>>(stage is null ? 5 : 6)
|
||||||
{
|
{
|
||||||
new("job.id", context.JobId),
|
new("job.id", context.JobId),
|
||||||
new("scan.id", context.ScanId),
|
new("scan.id", context.ScanId),
|
||||||
new("attempt", context.Lease.Attempt),
|
new("attempt", context.Lease.Attempt),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (context.Lease.Metadata.TryGetValue("queue", out var queueName) && !string.IsNullOrWhiteSpace(queueName))
|
if (context.Lease.Metadata.TryGetValue("queue", out var queueName) && !string.IsNullOrWhiteSpace(queueName))
|
||||||
{
|
{
|
||||||
tags.Add(new KeyValuePair<string, object?>("queue", queueName));
|
tags.Add(new KeyValuePair<string, object?>("queue", queueName));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.Lease.Metadata.TryGetValue("job.kind", out var jobKind) && !string.IsNullOrWhiteSpace(jobKind))
|
if (context.Lease.Metadata.TryGetValue("job.kind", out var jobKind) && !string.IsNullOrWhiteSpace(jobKind))
|
||||||
{
|
{
|
||||||
tags.Add(new KeyValuePair<string, object?>("job.kind", jobKind));
|
tags.Add(new KeyValuePair<string, object?>("job.kind", jobKind));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(stage))
|
if (!string.IsNullOrWhiteSpace(stage))
|
||||||
{
|
{
|
||||||
tags.Add(new KeyValuePair<string, object?>("stage", stage));
|
tags.Add(new KeyValuePair<string, object?>("stage", stage));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(failureReason))
|
if (!string.IsNullOrWhiteSpace(failureReason))
|
||||||
{
|
{
|
||||||
tags.Add(new KeyValuePair<string, object?>("reason", failureReason));
|
tags.Add(new KeyValuePair<string, object?>("reason", failureReason));
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags.ToArray();
|
return tags.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +1,102 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using OpenTelemetry.Metrics;
|
using OpenTelemetry.Metrics;
|
||||||
using OpenTelemetry.Resources;
|
using OpenTelemetry.Resources;
|
||||||
using OpenTelemetry.Trace;
|
using OpenTelemetry.Trace;
|
||||||
using StellaOps.Scanner.Worker.Options;
|
using StellaOps.Scanner.Worker.Options;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Diagnostics;
|
namespace StellaOps.Scanner.Worker.Diagnostics;
|
||||||
|
|
||||||
public static class TelemetryExtensions
|
public static class TelemetryExtensions
|
||||||
{
|
{
|
||||||
public static void ConfigureScannerWorkerTelemetry(this IHostApplicationBuilder builder, ScannerWorkerOptions options)
|
public static void ConfigureScannerWorkerTelemetry(this IHostApplicationBuilder builder, ScannerWorkerOptions options)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(builder);
|
ArgumentNullException.ThrowIfNull(builder);
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
|
|
||||||
var telemetry = options.Telemetry;
|
var telemetry = options.Telemetry;
|
||||||
if (!telemetry.EnableTelemetry)
|
if (!telemetry.EnableTelemetry)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var openTelemetry = builder.Services.AddOpenTelemetry();
|
var openTelemetry = builder.Services.AddOpenTelemetry();
|
||||||
|
|
||||||
openTelemetry.ConfigureResource(resource =>
|
openTelemetry.ConfigureResource(resource =>
|
||||||
{
|
{
|
||||||
var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
|
var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
|
||||||
resource.AddService(telemetry.ServiceName, serviceVersion: version, serviceInstanceId: Environment.MachineName);
|
resource.AddService(telemetry.ServiceName, serviceVersion: version, serviceInstanceId: Environment.MachineName);
|
||||||
resource.AddAttributes(new[]
|
resource.AddAttributes(new[]
|
||||||
{
|
{
|
||||||
new KeyValuePair<string, object>("deployment.environment", builder.Environment.EnvironmentName),
|
new KeyValuePair<string, object>("deployment.environment", builder.Environment.EnvironmentName),
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach (var kvp in telemetry.ResourceAttributes)
|
foreach (var kvp in telemetry.ResourceAttributes)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(kvp.Key) || kvp.Value is null)
|
if (string.IsNullOrWhiteSpace(kvp.Key) || kvp.Value is null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
resource.AddAttributes(new[] { new KeyValuePair<string, object>(kvp.Key, kvp.Value) });
|
resource.AddAttributes(new[] { new KeyValuePair<string, object>(kvp.Key, kvp.Value) });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (telemetry.EnableTracing)
|
if (telemetry.EnableTracing)
|
||||||
{
|
{
|
||||||
openTelemetry.WithTracing(tracing =>
|
openTelemetry.WithTracing(tracing =>
|
||||||
{
|
{
|
||||||
tracing.AddSource(ScannerWorkerInstrumentation.ActivitySourceName);
|
tracing.AddSource(ScannerWorkerInstrumentation.ActivitySourceName);
|
||||||
ConfigureExporter(tracing, telemetry);
|
ConfigureExporter(tracing, telemetry);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (telemetry.EnableMetrics)
|
if (telemetry.EnableMetrics)
|
||||||
{
|
{
|
||||||
openTelemetry.WithMetrics(metrics =>
|
openTelemetry.WithMetrics(metrics =>
|
||||||
{
|
{
|
||||||
metrics
|
metrics
|
||||||
.AddMeter(ScannerWorkerInstrumentation.MeterName)
|
.AddMeter(ScannerWorkerInstrumentation.MeterName)
|
||||||
.AddRuntimeInstrumentation()
|
.AddRuntimeInstrumentation()
|
||||||
.AddProcessInstrumentation();
|
.AddProcessInstrumentation();
|
||||||
|
|
||||||
ConfigureExporter(metrics, telemetry);
|
ConfigureExporter(metrics, telemetry);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureExporter(TracerProviderBuilder tracing, ScannerWorkerOptions.TelemetryOptions telemetry)
|
private static void ConfigureExporter(TracerProviderBuilder tracing, ScannerWorkerOptions.TelemetryOptions telemetry)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint))
|
if (!string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint))
|
||||||
{
|
{
|
||||||
tracing.AddOtlpExporter(options =>
|
tracing.AddOtlpExporter(options =>
|
||||||
{
|
{
|
||||||
options.Endpoint = new Uri(telemetry.OtlpEndpoint);
|
options.Endpoint = new Uri(telemetry.OtlpEndpoint);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (telemetry.ExportConsole || string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint))
|
if (telemetry.ExportConsole || string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint))
|
||||||
{
|
{
|
||||||
tracing.AddConsoleExporter();
|
tracing.AddConsoleExporter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureExporter(MeterProviderBuilder metrics, ScannerWorkerOptions.TelemetryOptions telemetry)
|
private static void ConfigureExporter(MeterProviderBuilder metrics, ScannerWorkerOptions.TelemetryOptions telemetry)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint))
|
if (!string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint))
|
||||||
{
|
{
|
||||||
metrics.AddOtlpExporter(options =>
|
metrics.AddOtlpExporter(options =>
|
||||||
{
|
{
|
||||||
options.Endpoint = new Uri(telemetry.OtlpEndpoint);
|
options.Endpoint = new Uri(telemetry.OtlpEndpoint);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (telemetry.ExportConsole || string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint))
|
if (telemetry.ExportConsole || string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint))
|
||||||
{
|
{
|
||||||
metrics.AddConsoleExporter();
|
metrics.AddConsoleExporter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,201 +1,202 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using StellaOps.Scanner.Worker.Diagnostics;
|
using StellaOps.Scanner.Worker.Diagnostics;
|
||||||
using StellaOps.Scanner.Worker.Options;
|
using StellaOps.Scanner.Worker.Options;
|
||||||
using StellaOps.Scanner.Worker.Processing;
|
using StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Hosting;
|
namespace StellaOps.Scanner.Worker.Hosting;
|
||||||
|
|
||||||
public sealed partial class ScannerWorkerHostedService : BackgroundService
|
public sealed partial class ScannerWorkerHostedService : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly IScanJobSource _jobSource;
|
private readonly IScanJobSource _jobSource;
|
||||||
private readonly ScanJobProcessor _processor;
|
private readonly ScanJobProcessor _processor;
|
||||||
private readonly LeaseHeartbeatService _heartbeatService;
|
private readonly LeaseHeartbeatService _heartbeatService;
|
||||||
private readonly ScannerWorkerMetrics _metrics;
|
private readonly ScannerWorkerMetrics _metrics;
|
||||||
private readonly TimeProvider _timeProvider;
|
private readonly TimeProvider _timeProvider;
|
||||||
private readonly IOptionsMonitor<ScannerWorkerOptions> _options;
|
private readonly IOptionsMonitor<ScannerWorkerOptions> _options;
|
||||||
private readonly ILogger<ScannerWorkerHostedService> _logger;
|
private readonly ILogger<ScannerWorkerHostedService> _logger;
|
||||||
private readonly IDelayScheduler _delayScheduler;
|
private readonly IDelayScheduler _delayScheduler;
|
||||||
|
|
||||||
public ScannerWorkerHostedService(
|
public ScannerWorkerHostedService(
|
||||||
IScanJobSource jobSource,
|
IScanJobSource jobSource,
|
||||||
ScanJobProcessor processor,
|
ScanJobProcessor processor,
|
||||||
LeaseHeartbeatService heartbeatService,
|
LeaseHeartbeatService heartbeatService,
|
||||||
ScannerWorkerMetrics metrics,
|
ScannerWorkerMetrics metrics,
|
||||||
TimeProvider timeProvider,
|
TimeProvider timeProvider,
|
||||||
IDelayScheduler delayScheduler,
|
IDelayScheduler delayScheduler,
|
||||||
IOptionsMonitor<ScannerWorkerOptions> options,
|
IOptionsMonitor<ScannerWorkerOptions> options,
|
||||||
ILogger<ScannerWorkerHostedService> logger)
|
ILogger<ScannerWorkerHostedService> logger)
|
||||||
{
|
{
|
||||||
_jobSource = jobSource ?? throw new ArgumentNullException(nameof(jobSource));
|
_jobSource = jobSource ?? throw new ArgumentNullException(nameof(jobSource));
|
||||||
_processor = processor ?? throw new ArgumentNullException(nameof(processor));
|
_processor = processor ?? throw new ArgumentNullException(nameof(processor));
|
||||||
_heartbeatService = heartbeatService ?? throw new ArgumentNullException(nameof(heartbeatService));
|
_heartbeatService = heartbeatService ?? throw new ArgumentNullException(nameof(heartbeatService));
|
||||||
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
|
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
|
||||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||||
_delayScheduler = delayScheduler ?? throw new ArgumentNullException(nameof(delayScheduler));
|
_delayScheduler = delayScheduler ?? throw new ArgumentNullException(nameof(delayScheduler));
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
var runningJobs = new HashSet<Task>();
|
var runningJobs = new HashSet<Task>();
|
||||||
var delayStrategy = new PollDelayStrategy(_options.CurrentValue.Polling);
|
var delayStrategy = new PollDelayStrategy(_options.CurrentValue.Polling);
|
||||||
|
|
||||||
WorkerStarted(_logger);
|
WorkerStarted(_logger);
|
||||||
|
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
runningJobs.RemoveWhere(static task => task.IsCompleted);
|
runningJobs.RemoveWhere(static task => task.IsCompleted);
|
||||||
|
|
||||||
var options = _options.CurrentValue;
|
var options = _options.CurrentValue;
|
||||||
if (runningJobs.Count >= options.MaxConcurrentJobs)
|
if (runningJobs.Count >= options.MaxConcurrentJobs)
|
||||||
{
|
{
|
||||||
var completed = await Task.WhenAny(runningJobs).ConfigureAwait(false);
|
var completed = await Task.WhenAny(runningJobs).ConfigureAwait(false);
|
||||||
runningJobs.Remove(completed);
|
runningJobs.Remove(completed);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
IScanJobLease? lease = null;
|
IScanJobLease? lease = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
lease = await _jobSource.TryAcquireAsync(stoppingToken).ConfigureAwait(false);
|
lease = await _jobSource.TryAcquireAsync(stoppingToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Scanner worker failed to acquire job lease; backing off.");
|
_logger.LogError(ex, "Scanner worker failed to acquire job lease; backing off.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lease is null)
|
if (lease is null)
|
||||||
{
|
{
|
||||||
var delay = delayStrategy.NextDelay();
|
var delay = delayStrategy.NextDelay();
|
||||||
await _delayScheduler.DelayAsync(delay, stoppingToken).ConfigureAwait(false);
|
await _delayScheduler.DelayAsync(delay, stoppingToken).ConfigureAwait(false);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
delayStrategy.Reset();
|
delayStrategy.Reset();
|
||||||
runningJobs.Add(RunJobAsync(lease, stoppingToken));
|
runningJobs.Add(RunJobAsync(lease, stoppingToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runningJobs.Count > 0)
|
if (runningJobs.Count > 0)
|
||||||
{
|
{
|
||||||
await Task.WhenAll(runningJobs).ConfigureAwait(false);
|
await Task.WhenAll(runningJobs).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
WorkerStopping(_logger);
|
WorkerStopping(_logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunJobAsync(IScanJobLease lease, CancellationToken stoppingToken)
|
private async Task RunJobAsync(IScanJobLease lease, CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
var options = _options.CurrentValue;
|
var options = _options.CurrentValue;
|
||||||
var jobStart = _timeProvider.GetUtcNow();
|
var jobStart = _timeProvider.GetUtcNow();
|
||||||
var queueLatency = jobStart - lease.EnqueuedAtUtc;
|
var queueLatency = jobStart - lease.EnqueuedAtUtc;
|
||||||
var jobCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
|
var jobCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
|
||||||
var jobToken = jobCts.Token;
|
var jobToken = jobCts.Token;
|
||||||
var context = new ScanJobContext(lease, _timeProvider, jobStart, jobToken);
|
var context = new ScanJobContext(lease, _timeProvider, jobStart, jobToken);
|
||||||
|
|
||||||
_metrics.RecordQueueLatency(context, queueLatency);
|
_metrics.RecordQueueLatency(context, queueLatency);
|
||||||
JobAcquired(_logger, lease.JobId, lease.ScanId, lease.Attempt, queueLatency.TotalMilliseconds);
|
JobAcquired(_logger, lease.JobId, lease.ScanId, lease.Attempt, queueLatency.TotalMilliseconds);
|
||||||
|
|
||||||
|
var processingTask = _processor.ExecuteAsync(context, jobToken).AsTask();
|
||||||
var heartbeatTask = _heartbeatService.RunAsync(lease, jobToken);
|
var heartbeatTask = _heartbeatService.RunAsync(lease, jobToken);
|
||||||
Exception? processingException = null;
|
Exception? processingException = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _processor.ExecuteAsync(context, jobToken).ConfigureAwait(false);
|
await processingTask.ConfigureAwait(false);
|
||||||
jobCts.Cancel();
|
jobCts.Cancel();
|
||||||
await heartbeatTask.ConfigureAwait(false);
|
await heartbeatTask.ConfigureAwait(false);
|
||||||
await lease.CompleteAsync(stoppingToken).ConfigureAwait(false);
|
await lease.CompleteAsync(stoppingToken).ConfigureAwait(false);
|
||||||
var duration = _timeProvider.GetUtcNow() - jobStart;
|
var duration = _timeProvider.GetUtcNow() - jobStart;
|
||||||
_metrics.RecordJobDuration(context, duration);
|
_metrics.RecordJobDuration(context, duration);
|
||||||
_metrics.IncrementJobCompleted(context);
|
_metrics.IncrementJobCompleted(context);
|
||||||
JobCompleted(_logger, lease.JobId, lease.ScanId, duration.TotalMilliseconds);
|
JobCompleted(_logger, lease.JobId, lease.ScanId, duration.TotalMilliseconds);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
processingException = null;
|
processingException = null;
|
||||||
await lease.AbandonAsync("host-stopping", CancellationToken.None).ConfigureAwait(false);
|
await lease.AbandonAsync("host-stopping", CancellationToken.None).ConfigureAwait(false);
|
||||||
JobAbandoned(_logger, lease.JobId, lease.ScanId);
|
JobAbandoned(_logger, lease.JobId, lease.ScanId);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
processingException = ex;
|
processingException = ex;
|
||||||
var duration = _timeProvider.GetUtcNow() - jobStart;
|
var duration = _timeProvider.GetUtcNow() - jobStart;
|
||||||
_metrics.RecordJobDuration(context, duration);
|
_metrics.RecordJobDuration(context, duration);
|
||||||
|
|
||||||
var reason = ex.GetType().Name;
|
var reason = ex.GetType().Name;
|
||||||
var maxAttempts = options.Queue.MaxAttempts;
|
var maxAttempts = options.Queue.MaxAttempts;
|
||||||
if (lease.Attempt >= maxAttempts)
|
if (lease.Attempt >= maxAttempts)
|
||||||
{
|
{
|
||||||
await lease.PoisonAsync(reason, CancellationToken.None).ConfigureAwait(false);
|
await lease.PoisonAsync(reason, CancellationToken.None).ConfigureAwait(false);
|
||||||
_metrics.IncrementJobFailed(context, reason);
|
_metrics.IncrementJobFailed(context, reason);
|
||||||
JobPoisoned(_logger, lease.JobId, lease.ScanId, lease.Attempt, maxAttempts, ex);
|
JobPoisoned(_logger, lease.JobId, lease.ScanId, lease.Attempt, maxAttempts, ex);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await lease.AbandonAsync(reason, CancellationToken.None).ConfigureAwait(false);
|
await lease.AbandonAsync(reason, CancellationToken.None).ConfigureAwait(false);
|
||||||
JobAbandonedWithError(_logger, lease.JobId, lease.ScanId, lease.Attempt, maxAttempts, ex);
|
JobAbandonedWithError(_logger, lease.JobId, lease.ScanId, lease.Attempt, maxAttempts, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
jobCts.Cancel();
|
jobCts.Cancel();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await heartbeatTask.ConfigureAwait(false);
|
await heartbeatTask.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (processingException is null && ex is not OperationCanceledException)
|
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);
|
_logger.LogWarning(ex, "Heartbeat loop ended with an exception for job {JobId}.", lease.JobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await lease.DisposeAsync().ConfigureAwait(false);
|
await lease.DisposeAsync().ConfigureAwait(false);
|
||||||
jobCts.Dispose();
|
jobCts.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[LoggerMessage(EventId = 2000, Level = LogLevel.Information, Message = "Scanner worker host started.")]
|
[LoggerMessage(EventId = 2000, Level = LogLevel.Information, Message = "Scanner worker host started.")]
|
||||||
private static partial void WorkerStarted(ILogger logger);
|
private static partial void WorkerStarted(ILogger logger);
|
||||||
|
|
||||||
[LoggerMessage(EventId = 2001, Level = LogLevel.Information, Message = "Scanner worker host stopping.")]
|
[LoggerMessage(EventId = 2001, Level = LogLevel.Information, Message = "Scanner worker host stopping.")]
|
||||||
private static partial void WorkerStopping(ILogger logger);
|
private static partial void WorkerStopping(ILogger logger);
|
||||||
|
|
||||||
[LoggerMessage(
|
[LoggerMessage(
|
||||||
EventId = 2002,
|
EventId = 2002,
|
||||||
Level = LogLevel.Information,
|
Level = LogLevel.Information,
|
||||||
Message = "Leased job {JobId} (scan {ScanId}) attempt {Attempt}; queue latency {LatencyMs:F0} ms.")]
|
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);
|
private static partial void JobAcquired(ILogger logger, string jobId, string scanId, int attempt, double latencyMs);
|
||||||
|
|
||||||
[LoggerMessage(
|
[LoggerMessage(
|
||||||
EventId = 2003,
|
EventId = 2003,
|
||||||
Level = LogLevel.Information,
|
Level = LogLevel.Information,
|
||||||
Message = "Job {JobId} (scan {ScanId}) completed in {DurationMs:F0} ms.")]
|
Message = "Job {JobId} (scan {ScanId}) completed in {DurationMs:F0} ms.")]
|
||||||
private static partial void JobCompleted(ILogger logger, string jobId, string scanId, double durationMs);
|
private static partial void JobCompleted(ILogger logger, string jobId, string scanId, double durationMs);
|
||||||
|
|
||||||
[LoggerMessage(
|
[LoggerMessage(
|
||||||
EventId = 2004,
|
EventId = 2004,
|
||||||
Level = LogLevel.Warning,
|
Level = LogLevel.Warning,
|
||||||
Message = "Job {JobId} (scan {ScanId}) abandoned due to host shutdown.")]
|
Message = "Job {JobId} (scan {ScanId}) abandoned due to host shutdown.")]
|
||||||
private static partial void JobAbandoned(ILogger logger, string jobId, string scanId);
|
private static partial void JobAbandoned(ILogger logger, string jobId, string scanId);
|
||||||
|
|
||||||
[LoggerMessage(
|
[LoggerMessage(
|
||||||
EventId = 2005,
|
EventId = 2005,
|
||||||
Level = LogLevel.Warning,
|
Level = LogLevel.Warning,
|
||||||
Message = "Job {JobId} (scan {ScanId}) attempt {Attempt}/{MaxAttempts} abandoned after failure; job will be retried.")]
|
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);
|
private static partial void JobAbandonedWithError(ILogger logger, string jobId, string scanId, int attempt, int maxAttempts, Exception exception);
|
||||||
|
|
||||||
[LoggerMessage(
|
[LoggerMessage(
|
||||||
EventId = 2006,
|
EventId = 2006,
|
||||||
Level = LogLevel.Error,
|
Level = LogLevel.Error,
|
||||||
Message = "Job {JobId} (scan {ScanId}) attempt {Attempt}/{MaxAttempts} exceeded retry budget; quarantining job.")]
|
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);
|
private static partial void JobPoisoned(ILogger logger, string jobId, string scanId, int attempt, int maxAttempts, Exception exception);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,141 +2,162 @@ using System;
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
namespace StellaOps.Scanner.Worker.Options;
|
using StellaOps.Scanner.Core.Contracts;
|
||||||
|
|
||||||
public sealed class ScannerWorkerOptions
|
namespace StellaOps.Scanner.Worker.Options;
|
||||||
{
|
|
||||||
public const string SectionName = "Scanner:Worker";
|
public sealed class ScannerWorkerOptions
|
||||||
|
{
|
||||||
public int MaxConcurrentJobs { get; set; } = 2;
|
public const string SectionName = "Scanner:Worker";
|
||||||
|
|
||||||
public QueueOptions Queue { get; } = new();
|
public int MaxConcurrentJobs { get; set; } = 2;
|
||||||
|
|
||||||
public PollingOptions Polling { get; } = new();
|
public QueueOptions Queue { get; } = new();
|
||||||
|
|
||||||
public AuthorityOptions Authority { get; } = new();
|
public PollingOptions Polling { get; } = new();
|
||||||
|
|
||||||
|
public AuthorityOptions Authority { get; } = new();
|
||||||
|
|
||||||
public TelemetryOptions Telemetry { get; } = new();
|
public TelemetryOptions Telemetry { get; } = new();
|
||||||
|
|
||||||
public ShutdownOptions Shutdown { get; } = new();
|
public ShutdownOptions Shutdown { get; } = new();
|
||||||
|
|
||||||
public sealed class QueueOptions
|
public AnalyzerOptions Analyzers { get; } = new();
|
||||||
{
|
|
||||||
public int MaxAttempts { get; set; } = 5;
|
public sealed class QueueOptions
|
||||||
|
{
|
||||||
public double HeartbeatSafetyFactor { get; set; } = 3.0;
|
public int MaxAttempts { get; set; } = 5;
|
||||||
|
|
||||||
public int MaxHeartbeatJitterMilliseconds { get; set; } = 750;
|
public double HeartbeatSafetyFactor { get; set; } = 3.0;
|
||||||
|
|
||||||
public IReadOnlyList<TimeSpan> HeartbeatRetryDelays => _heartbeatRetryDelays;
|
public int MaxHeartbeatJitterMilliseconds { get; set; } = 750;
|
||||||
|
|
||||||
public TimeSpan MinHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(10);
|
public IReadOnlyList<TimeSpan> HeartbeatRetryDelays => _heartbeatRetryDelays;
|
||||||
|
|
||||||
public TimeSpan MaxHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(30);
|
public TimeSpan MinHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
public void SetHeartbeatRetryDelays(IEnumerable<TimeSpan> delays)
|
public TimeSpan MaxHeartbeatInterval { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
{
|
|
||||||
_heartbeatRetryDelays = NormalizeDelays(delays);
|
public void SetHeartbeatRetryDelays(IEnumerable<TimeSpan> delays)
|
||||||
}
|
{
|
||||||
|
_heartbeatRetryDelays = NormalizeDelays(delays);
|
||||||
internal IReadOnlyList<TimeSpan> NormalizedHeartbeatRetryDelays => _heartbeatRetryDelays;
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<TimeSpan> NormalizeDelays(IEnumerable<TimeSpan> delays)
|
internal IReadOnlyList<TimeSpan> NormalizedHeartbeatRetryDelays => _heartbeatRetryDelays;
|
||||||
{
|
|
||||||
var buffer = new List<TimeSpan>();
|
private static IReadOnlyList<TimeSpan> NormalizeDelays(IEnumerable<TimeSpan> delays)
|
||||||
foreach (var delay in delays)
|
{
|
||||||
{
|
var buffer = new List<TimeSpan>();
|
||||||
if (delay <= TimeSpan.Zero)
|
foreach (var delay in delays)
|
||||||
{
|
{
|
||||||
continue;
|
if (delay <= TimeSpan.Zero)
|
||||||
}
|
{
|
||||||
|
continue;
|
||||||
buffer.Add(delay);
|
}
|
||||||
}
|
|
||||||
|
buffer.Add(delay);
|
||||||
buffer.Sort();
|
}
|
||||||
return new ReadOnlyCollection<TimeSpan>(buffer);
|
|
||||||
}
|
buffer.Sort();
|
||||||
|
return new ReadOnlyCollection<TimeSpan>(buffer);
|
||||||
private IReadOnlyList<TimeSpan> _heartbeatRetryDelays = new ReadOnlyCollection<TimeSpan>(new TimeSpan[]
|
}
|
||||||
{
|
|
||||||
TimeSpan.FromSeconds(2),
|
private IReadOnlyList<TimeSpan> _heartbeatRetryDelays = new ReadOnlyCollection<TimeSpan>(new TimeSpan[]
|
||||||
TimeSpan.FromSeconds(5),
|
{
|
||||||
TimeSpan.FromSeconds(10),
|
TimeSpan.FromSeconds(2),
|
||||||
});
|
TimeSpan.FromSeconds(5),
|
||||||
}
|
TimeSpan.FromSeconds(10),
|
||||||
|
});
|
||||||
public sealed class PollingOptions
|
}
|
||||||
{
|
|
||||||
public TimeSpan InitialDelay { get; set; } = TimeSpan.FromMilliseconds(200);
|
public sealed class PollingOptions
|
||||||
|
{
|
||||||
public TimeSpan MaxDelay { get; set; } = TimeSpan.FromSeconds(5);
|
public TimeSpan InitialDelay { get; set; } = TimeSpan.FromMilliseconds(200);
|
||||||
|
|
||||||
public double JitterRatio { get; set; } = 0.2;
|
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 sealed class AuthorityOptions
|
||||||
|
{
|
||||||
public string? Issuer { get; set; }
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
public string? ClientId { get; set; }
|
public string? Issuer { get; set; }
|
||||||
|
|
||||||
public string? ClientSecret { get; set; }
|
public string? ClientId { get; set; }
|
||||||
|
|
||||||
public bool RequireHttpsMetadata { get; set; } = true;
|
public string? ClientSecret { get; set; }
|
||||||
|
|
||||||
public string? MetadataAddress { get; set; }
|
public bool RequireHttpsMetadata { get; set; } = true;
|
||||||
|
|
||||||
public int BackchannelTimeoutSeconds { get; set; } = 20;
|
public string? MetadataAddress { get; set; }
|
||||||
|
|
||||||
public int TokenClockSkewSeconds { get; set; } = 30;
|
public int BackchannelTimeoutSeconds { get; set; } = 20;
|
||||||
|
|
||||||
public IList<string> Scopes { get; } = new List<string> { "scanner.scan" };
|
public int TokenClockSkewSeconds { get; set; } = 30;
|
||||||
|
|
||||||
public ResilienceOptions Resilience { get; } = new();
|
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 sealed class ResilienceOptions
|
||||||
|
{
|
||||||
public IList<TimeSpan> RetryDelays { get; } = new List<TimeSpan>
|
public bool? EnableRetries { get; set; }
|
||||||
{
|
|
||||||
TimeSpan.FromMilliseconds(250),
|
public IList<TimeSpan> RetryDelays { get; } = new List<TimeSpan>
|
||||||
TimeSpan.FromMilliseconds(500),
|
{
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromMilliseconds(250),
|
||||||
TimeSpan.FromSeconds(5),
|
TimeSpan.FromMilliseconds(500),
|
||||||
};
|
TimeSpan.FromSeconds(1),
|
||||||
|
TimeSpan.FromSeconds(5),
|
||||||
public bool? AllowOfflineCacheFallback { get; set; }
|
};
|
||||||
|
|
||||||
public TimeSpan? OfflineCacheTolerance { get; set; }
|
public bool? AllowOfflineCacheFallback { get; set; }
|
||||||
}
|
|
||||||
|
public TimeSpan? OfflineCacheTolerance { get; set; }
|
||||||
public sealed class TelemetryOptions
|
}
|
||||||
{
|
|
||||||
public bool EnableLogging { get; set; } = true;
|
public sealed class TelemetryOptions
|
||||||
|
{
|
||||||
public bool EnableTelemetry { get; set; } = true;
|
public bool EnableLogging { get; set; } = true;
|
||||||
|
|
||||||
public bool EnableTracing { get; set; }
|
public bool EnableTelemetry { get; set; } = true;
|
||||||
|
|
||||||
public bool EnableMetrics { get; set; } = true;
|
public bool EnableTracing { get; set; }
|
||||||
|
|
||||||
public string ServiceName { get; set; } = "stellaops-scanner-worker";
|
public bool EnableMetrics { get; set; } = true;
|
||||||
|
|
||||||
public string? OtlpEndpoint { get; set; }
|
public string ServiceName { get; set; } = "stellaops-scanner-worker";
|
||||||
|
|
||||||
public bool ExportConsole { get; set; }
|
public string? OtlpEndpoint { get; set; }
|
||||||
|
|
||||||
public IDictionary<string, string?> ResourceAttributes { get; } = new ConcurrentDictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
public bool ExportConsole { get; set; }
|
||||||
}
|
|
||||||
|
public IDictionary<string, string?> ResourceAttributes { get; } = new ConcurrentDictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class ShutdownOptions
|
public sealed class ShutdownOptions
|
||||||
{
|
{
|
||||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,91 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Options;
|
namespace StellaOps.Scanner.Worker.Options;
|
||||||
|
|
||||||
public sealed class ScannerWorkerOptionsValidator : IValidateOptions<ScannerWorkerOptions>
|
public sealed class ScannerWorkerOptionsValidator : IValidateOptions<ScannerWorkerOptions>
|
||||||
{
|
{
|
||||||
public ValidateOptionsResult Validate(string? name, ScannerWorkerOptions options)
|
public ValidateOptionsResult Validate(string? name, ScannerWorkerOptions options)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
|
|
||||||
var failures = new List<string>();
|
var failures = new List<string>();
|
||||||
|
|
||||||
if (options.MaxConcurrentJobs <= 0)
|
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)
|
if (options.Queue.MaxAttempts <= 0)
|
||||||
{
|
{
|
||||||
failures.Add("Scanner.Worker:Queue:HeartbeatSafetyFactor must be at least 2.");
|
failures.Add("Scanner.Worker:Queue:MaxAttempts must be greater than zero.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.Queue.MaxAttempts <= 0)
|
if (options.Queue.MinHeartbeatInterval <= TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
failures.Add("Scanner.Worker:Queue:MaxAttempts must be greater than zero.");
|
failures.Add("Scanner.Worker:Queue:MinHeartbeatInterval must be greater than zero.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.Queue.MinHeartbeatInterval <= TimeSpan.Zero)
|
if (options.Queue.MaxHeartbeatInterval <= options.Queue.MinHeartbeatInterval)
|
||||||
{
|
{
|
||||||
failures.Add("Scanner.Worker:Queue:MinHeartbeatInterval must be greater than zero.");
|
failures.Add("Scanner.Worker:Queue:MaxHeartbeatInterval must be greater than MinHeartbeatInterval.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.Queue.MaxHeartbeatInterval <= options.Queue.MinHeartbeatInterval)
|
if (options.Polling.InitialDelay <= TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
failures.Add("Scanner.Worker:Queue:MaxHeartbeatInterval must be greater than MinHeartbeatInterval.");
|
failures.Add("Scanner.Worker:Polling:InitialDelay must be greater than zero.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.Polling.InitialDelay <= TimeSpan.Zero)
|
if (options.Polling.MaxDelay < options.Polling.InitialDelay)
|
||||||
{
|
{
|
||||||
failures.Add("Scanner.Worker:Polling:InitialDelay must be greater than zero.");
|
failures.Add("Scanner.Worker:Polling:MaxDelay must be greater than or equal to InitialDelay.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.Polling.MaxDelay < options.Polling.InitialDelay)
|
if (options.Polling.JitterRatio is < 0 or > 1)
|
||||||
{
|
{
|
||||||
failures.Add("Scanner.Worker:Polling:MaxDelay must be greater than or equal to InitialDelay.");
|
failures.Add("Scanner.Worker:Polling:JitterRatio must be between 0 and 1.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.Polling.JitterRatio is < 0 or > 1)
|
if (options.Authority.Enabled)
|
||||||
{
|
{
|
||||||
failures.Add("Scanner.Worker:Polling:JitterRatio must be between 0 and 1.");
|
if (string.IsNullOrWhiteSpace(options.Authority.Issuer))
|
||||||
}
|
{
|
||||||
|
failures.Add("Scanner.Worker:Authority requires Issuer when Enabled is true.");
|
||||||
if (options.Authority.Enabled)
|
}
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(options.Authority.Issuer))
|
if (string.IsNullOrWhiteSpace(options.Authority.ClientId))
|
||||||
{
|
{
|
||||||
failures.Add("Scanner.Worker:Authority requires Issuer when Enabled is true.");
|
failures.Add("Scanner.Worker:Authority requires ClientId when Enabled is true.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(options.Authority.ClientId))
|
if (options.Authority.BackchannelTimeoutSeconds <= 0)
|
||||||
{
|
{
|
||||||
failures.Add("Scanner.Worker:Authority requires ClientId when Enabled is true.");
|
failures.Add("Scanner.Worker:Authority:BackchannelTimeoutSeconds must be greater than zero.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.Authority.BackchannelTimeoutSeconds <= 0)
|
if (options.Authority.TokenClockSkewSeconds < 0)
|
||||||
{
|
{
|
||||||
failures.Add("Scanner.Worker:Authority:BackchannelTimeoutSeconds must be greater than zero.");
|
failures.Add("Scanner.Worker:Authority:TokenClockSkewSeconds cannot be negative.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.Authority.TokenClockSkewSeconds < 0)
|
if (options.Authority.Resilience.RetryDelays.Any(delay => delay <= TimeSpan.Zero))
|
||||||
{
|
{
|
||||||
failures.Add("Scanner.Worker:Authority:TokenClockSkewSeconds cannot be negative.");
|
failures.Add("Scanner.Worker:Authority:Resilience:RetryDelays must be positive durations.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (options.Authority.Resilience.RetryDelays.Any(delay => delay <= TimeSpan.Zero))
|
|
||||||
{
|
if (options.Shutdown.Timeout < TimeSpan.FromSeconds(5))
|
||||||
failures.Add("Scanner.Worker:Authority:Resilience:RetryDelays must be positive durations.");
|
{
|
||||||
}
|
failures.Add("Scanner.Worker:Shutdown:Timeout must be at least 5 seconds to allow lease completion.");
|
||||||
}
|
}
|
||||||
|
|
||||||
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.EnableTelemetry)
|
||||||
{
|
{
|
||||||
if (!options.Telemetry.EnableMetrics && !options.Telemetry.EnableTracing)
|
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);
|
return failures.Count == 0 ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail(failures);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public sealed class AnalyzerStageExecutor : IScanStageExecutor
|
public sealed class AnalyzerStageExecutor : IScanStageExecutor
|
||||||
{
|
{
|
||||||
private readonly IScanAnalyzerDispatcher _dispatcher;
|
private readonly IScanAnalyzerDispatcher _dispatcher;
|
||||||
|
|
||||||
public AnalyzerStageExecutor(IScanAnalyzerDispatcher dispatcher)
|
public AnalyzerStageExecutor(IScanAnalyzerDispatcher dispatcher)
|
||||||
{
|
{
|
||||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||||
}
|
}
|
||||||
|
|
||||||
public string StageName => ScanStageNames.ExecuteAnalyzers;
|
public string StageName => ScanStageNames.ExecuteAnalyzers;
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
public ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||||
=> _dispatcher.ExecuteAsync(context, cancellationToken);
|
=> _dispatcher.ExecuteAsync(context, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public interface IDelayScheduler
|
public interface IDelayScheduler
|
||||||
{
|
{
|
||||||
Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken);
|
Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public interface IScanAnalyzerDispatcher
|
public interface IScanAnalyzerDispatcher
|
||||||
{
|
{
|
||||||
ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken);
|
ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class NullScanAnalyzerDispatcher : IScanAnalyzerDispatcher
|
public sealed class NullScanAnalyzerDispatcher : IScanAnalyzerDispatcher
|
||||||
{
|
{
|
||||||
public ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
public ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||||
=> ValueTask.CompletedTask;
|
=> ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public interface IScanJobLease : IAsyncDisposable
|
public interface IScanJobLease : IAsyncDisposable
|
||||||
{
|
{
|
||||||
string JobId { get; }
|
string JobId { get; }
|
||||||
|
|
||||||
string ScanId { get; }
|
string ScanId { get; }
|
||||||
|
|
||||||
int Attempt { get; }
|
int Attempt { get; }
|
||||||
|
|
||||||
DateTimeOffset EnqueuedAtUtc { get; }
|
DateTimeOffset EnqueuedAtUtc { get; }
|
||||||
|
|
||||||
DateTimeOffset LeasedAtUtc { get; }
|
DateTimeOffset LeasedAtUtc { get; }
|
||||||
|
|
||||||
TimeSpan LeaseDuration { get; }
|
TimeSpan LeaseDuration { get; }
|
||||||
|
|
||||||
IReadOnlyDictionary<string, string> Metadata { get; }
|
IReadOnlyDictionary<string, string> Metadata { get; }
|
||||||
|
|
||||||
ValueTask RenewAsync(CancellationToken cancellationToken);
|
ValueTask RenewAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
ValueTask CompleteAsync(CancellationToken cancellationToken);
|
ValueTask CompleteAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
ValueTask AbandonAsync(string reason, CancellationToken cancellationToken);
|
ValueTask AbandonAsync(string reason, CancellationToken cancellationToken);
|
||||||
|
|
||||||
ValueTask PoisonAsync(string reason, CancellationToken cancellationToken);
|
ValueTask PoisonAsync(string reason, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public interface IScanJobSource
|
public interface IScanJobSource
|
||||||
{
|
{
|
||||||
Task<IScanJobLease?> TryAcquireAsync(CancellationToken cancellationToken);
|
Task<IScanJobLease?> TryAcquireAsync(CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public interface IScanStageExecutor
|
public interface IScanStageExecutor
|
||||||
{
|
{
|
||||||
string StageName { get; }
|
string StageName { get; }
|
||||||
|
|
||||||
ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken);
|
ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,148 +1,155 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using StellaOps.Scanner.Worker.Options;
|
using StellaOps.Scanner.Worker.Options;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public sealed class LeaseHeartbeatService
|
public sealed class LeaseHeartbeatService
|
||||||
{
|
{
|
||||||
private readonly TimeProvider _timeProvider;
|
private readonly TimeProvider _timeProvider;
|
||||||
private readonly IOptionsMonitor<ScannerWorkerOptions> _options;
|
private readonly IOptionsMonitor<ScannerWorkerOptions> _options;
|
||||||
private readonly IDelayScheduler _delayScheduler;
|
private readonly IDelayScheduler _delayScheduler;
|
||||||
private readonly ILogger<LeaseHeartbeatService> _logger;
|
private readonly ILogger<LeaseHeartbeatService> _logger;
|
||||||
|
|
||||||
public LeaseHeartbeatService(TimeProvider timeProvider, IDelayScheduler delayScheduler, IOptionsMonitor<ScannerWorkerOptions> options, ILogger<LeaseHeartbeatService> logger)
|
public LeaseHeartbeatService(TimeProvider timeProvider, IDelayScheduler delayScheduler, IOptionsMonitor<ScannerWorkerOptions> options, ILogger<LeaseHeartbeatService> logger)
|
||||||
{
|
{
|
||||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||||
_delayScheduler = delayScheduler ?? throw new ArgumentNullException(nameof(delayScheduler));
|
_delayScheduler = delayScheduler ?? throw new ArgumentNullException(nameof(delayScheduler));
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RunAsync(IScanJobLease lease, CancellationToken cancellationToken)
|
public async Task RunAsync(IScanJobLease lease, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(lease);
|
ArgumentNullException.ThrowIfNull(lease);
|
||||||
|
|
||||||
var options = _options.CurrentValue;
|
await Task.Yield();
|
||||||
var interval = ComputeInterval(options, lease);
|
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
options = _options.CurrentValue;
|
var options = _options.CurrentValue;
|
||||||
var delay = ApplyJitter(interval, options.Queue.MaxHeartbeatJitterMilliseconds);
|
var interval = ComputeInterval(options, lease);
|
||||||
|
var delay = ApplyJitter(interval, options.Queue);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _delayScheduler.DelayAsync(delay, cancellationToken).ConfigureAwait(false);
|
await _delayScheduler.DelayAsync(delay, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await TryRenewAsync(options, lease, cancellationToken).ConfigureAwait(false))
|
if (await TryRenewAsync(options, lease, cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogError(
|
_logger.LogError(
|
||||||
"Job {JobId} (scan {ScanId}) lease renewal exhausted retries; cancelling processing.",
|
"Job {JobId} (scan {ScanId}) lease renewal exhausted retries; cancelling processing.",
|
||||||
lease.JobId,
|
lease.JobId,
|
||||||
lease.ScanId);
|
lease.ScanId);
|
||||||
throw new InvalidOperationException("Lease renewal retries exhausted.");
|
throw new InvalidOperationException("Lease renewal retries exhausted.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TimeSpan ComputeInterval(ScannerWorkerOptions options, IScanJobLease lease)
|
private static TimeSpan ComputeInterval(ScannerWorkerOptions options, IScanJobLease lease)
|
||||||
{
|
{
|
||||||
var divisor = options.Queue.HeartbeatSafetyFactor <= 0 ? 3.0 : options.Queue.HeartbeatSafetyFactor;
|
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)
|
if (recommended < options.Queue.MinHeartbeatInterval)
|
||||||
{
|
{
|
||||||
recommended = options.Queue.MinHeartbeatInterval;
|
recommended = options.Queue.MinHeartbeatInterval;
|
||||||
}
|
}
|
||||||
else if (recommended > options.Queue.MaxHeartbeatInterval)
|
else if (recommended > options.Queue.MaxHeartbeatInterval)
|
||||||
{
|
{
|
||||||
recommended = options.Queue.MaxHeartbeatInterval;
|
recommended = options.Queue.MaxHeartbeatInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
return recommended;
|
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;
|
return duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset = Random.Shared.NextDouble() * maxJitterMilliseconds;
|
var offsetMs = Random.Shared.NextDouble() * queueOptions.MaxHeartbeatJitterMilliseconds;
|
||||||
return duration + TimeSpan.FromMilliseconds(offset);
|
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)
|
private async Task<bool> TryRenewAsync(ScannerWorkerOptions options, IScanJobLease lease, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await lease.RenewAsync(cancellationToken).ConfigureAwait(false);
|
await lease.RenewAsync(cancellationToken).ConfigureAwait(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(
|
_logger.LogWarning(
|
||||||
ex,
|
ex,
|
||||||
"Job {JobId} (scan {ScanId}) heartbeat failed; retrying.",
|
"Job {JobId} (scan {ScanId}) heartbeat failed; retrying.",
|
||||||
lease.JobId,
|
lease.JobId,
|
||||||
lease.ScanId);
|
lease.ScanId);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var delay in options.Queue.NormalizedHeartbeatRetryDelays)
|
foreach (var delay in options.Queue.NormalizedHeartbeatRetryDelays)
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _delayScheduler.DelayAsync(delay, cancellationToken).ConfigureAwait(false);
|
await _delayScheduler.DelayAsync(delay, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await lease.RenewAsync(cancellationToken).ConfigureAwait(false);
|
await lease.RenewAsync(cancellationToken).ConfigureAwait(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(
|
_logger.LogWarning(
|
||||||
ex,
|
ex,
|
||||||
"Job {JobId} (scan {ScanId}) heartbeat retry failed; will retry after {Delay}.",
|
"Job {JobId} (scan {ScanId}) heartbeat retry failed; will retry after {Delay}.",
|
||||||
lease.JobId,
|
lease.JobId,
|
||||||
lease.ScanId,
|
lease.ScanId,
|
||||||
delay);
|
delay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public sealed class NoOpStageExecutor : IScanStageExecutor
|
public sealed class NoOpStageExecutor : IScanStageExecutor
|
||||||
{
|
{
|
||||||
public NoOpStageExecutor(string stageName)
|
public NoOpStageExecutor(string stageName)
|
||||||
{
|
{
|
||||||
StageName = stageName ?? throw new ArgumentNullException(nameof(stageName));
|
StageName = stageName ?? throw new ArgumentNullException(nameof(stageName));
|
||||||
}
|
}
|
||||||
|
|
||||||
public string StageName { get; }
|
public string StageName { get; }
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
public ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||||
=> ValueTask.CompletedTask;
|
=> ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public sealed class NullScanJobSource : IScanJobSource
|
public sealed class NullScanJobSource : IScanJobSource
|
||||||
{
|
{
|
||||||
private readonly ILogger<NullScanJobSource> _logger;
|
private readonly ILogger<NullScanJobSource> _logger;
|
||||||
private int _logged;
|
private int _logged;
|
||||||
|
|
||||||
public NullScanJobSource(ILogger<NullScanJobSource> logger)
|
public NullScanJobSource(ILogger<NullScanJobSource> logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IScanJobLease?> TryAcquireAsync(CancellationToken cancellationToken)
|
public Task<IScanJobLease?> TryAcquireAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (Interlocked.Exchange(ref _logged, 1) == 0)
|
if (Interlocked.Exchange(ref _logged, 1) == 0)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No queue provider registered. Scanner worker will idle until a queue adapter is configured.");
|
_logger.LogWarning("No queue provider registered. Scanner worker will idle until a queue adapter is configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult<IScanJobLease?>(null);
|
return Task.FromResult<IScanJobLease?>(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +1,49 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
using StellaOps.Scanner.Worker.Options;
|
using StellaOps.Scanner.Worker.Options;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public sealed class PollDelayStrategy
|
public sealed class PollDelayStrategy
|
||||||
{
|
{
|
||||||
private readonly ScannerWorkerOptions.PollingOptions _options;
|
private readonly ScannerWorkerOptions.PollingOptions _options;
|
||||||
private TimeSpan _currentDelay;
|
private TimeSpan _currentDelay;
|
||||||
|
|
||||||
public PollDelayStrategy(ScannerWorkerOptions.PollingOptions options)
|
public PollDelayStrategy(ScannerWorkerOptions.PollingOptions options)
|
||||||
{
|
{
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
public TimeSpan NextDelay()
|
public TimeSpan NextDelay()
|
||||||
{
|
{
|
||||||
if (_currentDelay == TimeSpan.Zero)
|
if (_currentDelay == TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
_currentDelay = _options.InitialDelay;
|
_currentDelay = _options.InitialDelay;
|
||||||
return ApplyJitter(_currentDelay);
|
return ApplyJitter(_currentDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
var doubled = _currentDelay + _currentDelay;
|
var doubled = _currentDelay + _currentDelay;
|
||||||
_currentDelay = doubled < _options.MaxDelay ? doubled : _options.MaxDelay;
|
_currentDelay = doubled < _options.MaxDelay ? doubled : _options.MaxDelay;
|
||||||
return ApplyJitter(_currentDelay);
|
return ApplyJitter(_currentDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Reset() => _currentDelay = TimeSpan.Zero;
|
public void Reset() => _currentDelay = TimeSpan.Zero;
|
||||||
|
|
||||||
private TimeSpan ApplyJitter(TimeSpan duration)
|
private TimeSpan ApplyJitter(TimeSpan duration)
|
||||||
{
|
{
|
||||||
if (_options.JitterRatio <= 0)
|
if (_options.JitterRatio <= 0)
|
||||||
{
|
{
|
||||||
return duration;
|
return duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxOffset = duration.TotalMilliseconds * _options.JitterRatio;
|
var maxOffset = duration.TotalMilliseconds * _options.JitterRatio;
|
||||||
if (maxOffset <= 0)
|
if (maxOffset <= 0)
|
||||||
{
|
{
|
||||||
return duration;
|
return duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset = (Random.Shared.NextDouble() * 2.0 - 1.0) * maxOffset;
|
var offset = (Random.Shared.NextDouble() * 2.0 - 1.0) * maxOffset;
|
||||||
var adjustedMs = Math.Max(0, duration.TotalMilliseconds + offset);
|
var adjustedMs = Math.Max(0, duration.TotalMilliseconds + offset);
|
||||||
return TimeSpan.FromMilliseconds(adjustedMs);
|
return TimeSpan.FromMilliseconds(adjustedMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,31 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using StellaOps.Scanner.Core.Contracts;
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
|
||||||
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
public sealed class ScanJobContext
|
|
||||||
{
|
public sealed class ScanJobContext
|
||||||
|
{
|
||||||
public ScanJobContext(IScanJobLease lease, TimeProvider timeProvider, DateTimeOffset startUtc, CancellationToken cancellationToken)
|
public ScanJobContext(IScanJobLease lease, TimeProvider timeProvider, DateTimeOffset startUtc, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Lease = lease ?? throw new ArgumentNullException(nameof(lease));
|
Lease = lease ?? throw new ArgumentNullException(nameof(lease));
|
||||||
TimeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
TimeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||||
StartUtc = startUtc;
|
StartUtc = startUtc;
|
||||||
CancellationToken = cancellationToken;
|
CancellationToken = cancellationToken;
|
||||||
|
Analysis = new ScanAnalysisStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IScanJobLease Lease { get; }
|
public IScanJobLease Lease { get; }
|
||||||
|
|
||||||
public TimeProvider TimeProvider { get; }
|
public TimeProvider TimeProvider { get; }
|
||||||
|
|
||||||
public DateTimeOffset StartUtc { get; }
|
public DateTimeOffset StartUtc { get; }
|
||||||
|
|
||||||
public CancellationToken CancellationToken { get; }
|
public CancellationToken CancellationToken { get; }
|
||||||
|
|
||||||
public string JobId => Lease.JobId;
|
public string JobId => Lease.JobId;
|
||||||
|
|
||||||
public string ScanId => Lease.ScanId;
|
public string ScanId => Lease.ScanId;
|
||||||
|
|
||||||
|
public ScanAnalysisStore Analysis { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,65 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public sealed class ScanJobProcessor
|
public sealed class ScanJobProcessor
|
||||||
{
|
{
|
||||||
private readonly IReadOnlyDictionary<string, IScanStageExecutor> _executors;
|
private readonly IReadOnlyDictionary<string, IScanStageExecutor> _executors;
|
||||||
private readonly ScanProgressReporter _progressReporter;
|
private readonly ScanProgressReporter _progressReporter;
|
||||||
private readonly ILogger<ScanJobProcessor> _logger;
|
private readonly ILogger<ScanJobProcessor> _logger;
|
||||||
|
|
||||||
public ScanJobProcessor(IEnumerable<IScanStageExecutor> executors, ScanProgressReporter progressReporter, ILogger<ScanJobProcessor> logger)
|
public ScanJobProcessor(IEnumerable<IScanStageExecutor> executors, ScanProgressReporter progressReporter, ILogger<ScanJobProcessor> logger)
|
||||||
{
|
{
|
||||||
_progressReporter = progressReporter ?? throw new ArgumentNullException(nameof(progressReporter));
|
_progressReporter = progressReporter ?? throw new ArgumentNullException(nameof(progressReporter));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
|
||||||
var map = new Dictionary<string, IScanStageExecutor>(StringComparer.OrdinalIgnoreCase);
|
var map = new Dictionary<string, IScanStageExecutor>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var executor in executors ?? Array.Empty<IScanStageExecutor>())
|
foreach (var executor in executors ?? Array.Empty<IScanStageExecutor>())
|
||||||
{
|
{
|
||||||
if (executor is null || string.IsNullOrWhiteSpace(executor.StageName))
|
if (executor is null || string.IsNullOrWhiteSpace(executor.StageName))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
map[executor.StageName] = executor;
|
map[executor.StageName] = executor;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var stage in ScanStageNames.Ordered)
|
foreach (var stage in ScanStageNames.Ordered)
|
||||||
{
|
{
|
||||||
if (map.ContainsKey(stage))
|
if (map.ContainsKey(stage))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
map[stage] = new NoOpStageExecutor(stage);
|
map[stage] = new NoOpStageExecutor(stage);
|
||||||
_logger.LogDebug("No executor registered for stage {Stage}; using no-op placeholder.", stage);
|
_logger.LogDebug("No executor registered for stage {Stage}; using no-op placeholder.", stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
_executors = map;
|
_executors = map;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
public async ValueTask ExecuteAsync(ScanJobContext context, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(context);
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
|
||||||
foreach (var stage in ScanStageNames.Ordered)
|
foreach (var stage in ScanStageNames.Ordered)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (!_executors.TryGetValue(stage, out var executor))
|
if (!_executors.TryGetValue(stage, out var executor))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _progressReporter.ExecuteStageAsync(
|
await _progressReporter.ExecuteStageAsync(
|
||||||
context,
|
context,
|
||||||
stage,
|
stage,
|
||||||
executor.ExecuteAsync,
|
executor.ExecuteAsync,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +1,86 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using StellaOps.Scanner.Worker.Diagnostics;
|
using StellaOps.Scanner.Worker.Diagnostics;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public sealed partial class ScanProgressReporter
|
public sealed partial class ScanProgressReporter
|
||||||
{
|
{
|
||||||
private readonly ScannerWorkerMetrics _metrics;
|
private readonly ScannerWorkerMetrics _metrics;
|
||||||
private readonly ILogger<ScanProgressReporter> _logger;
|
private readonly ILogger<ScanProgressReporter> _logger;
|
||||||
|
|
||||||
public ScanProgressReporter(ScannerWorkerMetrics metrics, ILogger<ScanProgressReporter> logger)
|
public ScanProgressReporter(ScannerWorkerMetrics metrics, ILogger<ScanProgressReporter> logger)
|
||||||
{
|
{
|
||||||
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
|
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask ExecuteStageAsync(
|
public async ValueTask ExecuteStageAsync(
|
||||||
ScanJobContext context,
|
ScanJobContext context,
|
||||||
string stageName,
|
string stageName,
|
||||||
Func<ScanJobContext, CancellationToken, ValueTask> stageWork,
|
Func<ScanJobContext, CancellationToken, ValueTask> stageWork,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(context);
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(stageName);
|
ArgumentException.ThrowIfNullOrWhiteSpace(stageName);
|
||||||
ArgumentNullException.ThrowIfNull(stageWork);
|
ArgumentNullException.ThrowIfNull(stageWork);
|
||||||
|
|
||||||
StageStarting(_logger, context.JobId, context.ScanId, stageName, context.Lease.Attempt);
|
StageStarting(_logger, context.JobId, context.ScanId, stageName, context.Lease.Attempt);
|
||||||
|
|
||||||
var start = context.TimeProvider.GetUtcNow();
|
var start = context.TimeProvider.GetUtcNow();
|
||||||
using var activity = ScannerWorkerInstrumentation.ActivitySource.StartActivity(
|
using var activity = ScannerWorkerInstrumentation.ActivitySource.StartActivity(
|
||||||
$"scanner.worker.{stageName}",
|
$"scanner.worker.{stageName}",
|
||||||
ActivityKind.Internal);
|
ActivityKind.Internal);
|
||||||
|
|
||||||
activity?.SetTag("scanner.worker.job_id", context.JobId);
|
activity?.SetTag("scanner.worker.job_id", context.JobId);
|
||||||
activity?.SetTag("scanner.worker.scan_id", context.ScanId);
|
activity?.SetTag("scanner.worker.scan_id", context.ScanId);
|
||||||
activity?.SetTag("scanner.worker.stage", stageName);
|
activity?.SetTag("scanner.worker.stage", stageName);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await stageWork(context, cancellationToken).ConfigureAwait(false);
|
await stageWork(context, cancellationToken).ConfigureAwait(false);
|
||||||
var duration = context.TimeProvider.GetUtcNow() - start;
|
var duration = context.TimeProvider.GetUtcNow() - start;
|
||||||
_metrics.RecordStageDuration(context, stageName, duration);
|
_metrics.RecordStageDuration(context, stageName, duration);
|
||||||
StageCompleted(_logger, context.JobId, context.ScanId, stageName, duration.TotalMilliseconds);
|
StageCompleted(_logger, context.JobId, context.ScanId, stageName, duration.TotalMilliseconds);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
StageCancelled(_logger, context.JobId, context.ScanId, stageName);
|
StageCancelled(_logger, context.JobId, context.ScanId, stageName);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var duration = context.TimeProvider.GetUtcNow() - start;
|
var duration = context.TimeProvider.GetUtcNow() - start;
|
||||||
_metrics.RecordStageDuration(context, stageName, duration);
|
_metrics.RecordStageDuration(context, stageName, duration);
|
||||||
StageFailed(_logger, context.JobId, context.ScanId, stageName, ex);
|
StageFailed(_logger, context.JobId, context.ScanId, stageName, ex);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[LoggerMessage(
|
[LoggerMessage(
|
||||||
EventId = 1000,
|
EventId = 1000,
|
||||||
Level = LogLevel.Information,
|
Level = LogLevel.Information,
|
||||||
Message = "Job {JobId} (scan {ScanId}) entering stage {Stage} (attempt {Attempt}).")]
|
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);
|
private static partial void StageStarting(ILogger logger, string jobId, string scanId, string stage, int attempt);
|
||||||
|
|
||||||
[LoggerMessage(
|
[LoggerMessage(
|
||||||
EventId = 1001,
|
EventId = 1001,
|
||||||
Level = LogLevel.Information,
|
Level = LogLevel.Information,
|
||||||
Message = "Job {JobId} (scan {ScanId}) finished stage {Stage} in {ElapsedMs:F0} ms.")]
|
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);
|
private static partial void StageCompleted(ILogger logger, string jobId, string scanId, string stage, double elapsedMs);
|
||||||
|
|
||||||
[LoggerMessage(
|
[LoggerMessage(
|
||||||
EventId = 1002,
|
EventId = 1002,
|
||||||
Level = LogLevel.Warning,
|
Level = LogLevel.Warning,
|
||||||
Message = "Job {JobId} (scan {ScanId}) stage {Stage} cancelled by request.")]
|
Message = "Job {JobId} (scan {ScanId}) stage {Stage} cancelled by request.")]
|
||||||
private static partial void StageCancelled(ILogger logger, string jobId, string scanId, string stage);
|
private static partial void StageCancelled(ILogger logger, string jobId, string scanId, string stage);
|
||||||
|
|
||||||
[LoggerMessage(
|
[LoggerMessage(
|
||||||
EventId = 1003,
|
EventId = 1003,
|
||||||
Level = LogLevel.Error,
|
Level = LogLevel.Error,
|
||||||
Message = "Job {JobId} (scan {ScanId}) stage {Stage} failed.")]
|
Message = "Job {JobId} (scan {ScanId}) stage {Stage} failed.")]
|
||||||
private static partial void StageFailed(ILogger logger, string jobId, string scanId, string stage, Exception exception);
|
private static partial void StageFailed(ILogger logger, string jobId, string scanId, string stage, Exception exception);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public static class ScanStageNames
|
public static class ScanStageNames
|
||||||
{
|
{
|
||||||
public const string ResolveImage = "resolve-image";
|
public const string ResolveImage = "resolve-image";
|
||||||
public const string PullLayers = "pull-layers";
|
public const string PullLayers = "pull-layers";
|
||||||
public const string BuildFilesystem = "build-filesystem";
|
public const string BuildFilesystem = "build-filesystem";
|
||||||
public const string ExecuteAnalyzers = "execute-analyzers";
|
public const string ExecuteAnalyzers = "execute-analyzers";
|
||||||
public const string ComposeArtifacts = "compose-artifacts";
|
public const string ComposeArtifacts = "compose-artifacts";
|
||||||
public const string EmitReports = "emit-reports";
|
public const string EmitReports = "emit-reports";
|
||||||
|
|
||||||
public static readonly IReadOnlyList<string> Ordered = new[]
|
public static readonly IReadOnlyList<string> Ordered = new[]
|
||||||
{
|
{
|
||||||
ResolveImage,
|
ResolveImage,
|
||||||
PullLayers,
|
PullLayers,
|
||||||
BuildFilesystem,
|
BuildFilesystem,
|
||||||
ExecuteAnalyzers,
|
ExecuteAnalyzers,
|
||||||
ComposeArtifacts,
|
ComposeArtifacts,
|
||||||
EmitReports,
|
EmitReports,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace StellaOps.Scanner.Worker.Processing;
|
namespace StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
public sealed class SystemDelayScheduler : IDelayScheduler
|
public sealed class SystemDelayScheduler : IDelayScheduler
|
||||||
{
|
{
|
||||||
public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
|
public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (delay <= TimeSpan.Zero)
|
if (delay <= TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.Delay(delay, cancellationToken);
|
return Task.Delay(delay, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,98 +1,103 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using StellaOps.Auth.Client;
|
using StellaOps.Auth.Client;
|
||||||
|
using StellaOps.Scanner.Analyzers.OS.Plugin;
|
||||||
|
using StellaOps.Scanner.EntryTrace;
|
||||||
using StellaOps.Scanner.Worker.Diagnostics;
|
using StellaOps.Scanner.Worker.Diagnostics;
|
||||||
using StellaOps.Scanner.Worker.Hosting;
|
using StellaOps.Scanner.Worker.Hosting;
|
||||||
using StellaOps.Scanner.Worker.Options;
|
using StellaOps.Scanner.Worker.Options;
|
||||||
using StellaOps.Scanner.Worker.Processing;
|
using StellaOps.Scanner.Worker.Processing;
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
builder.Services.AddOptions<ScannerWorkerOptions>()
|
builder.Services.AddOptions<ScannerWorkerOptions>()
|
||||||
.BindConfiguration(ScannerWorkerOptions.SectionName)
|
.BindConfiguration(ScannerWorkerOptions.SectionName)
|
||||||
.ValidateOnStart();
|
.ValidateOnStart();
|
||||||
|
|
||||||
builder.Services.AddSingleton<IValidateOptions<ScannerWorkerOptions>, ScannerWorkerOptionsValidator>();
|
builder.Services.AddSingleton<IValidateOptions<ScannerWorkerOptions>, ScannerWorkerOptionsValidator>();
|
||||||
builder.Services.AddSingleton(TimeProvider.System);
|
builder.Services.AddSingleton(TimeProvider.System);
|
||||||
builder.Services.AddSingleton<ScannerWorkerMetrics>();
|
builder.Services.AddSingleton<ScannerWorkerMetrics>();
|
||||||
builder.Services.AddSingleton<ScanProgressReporter>();
|
builder.Services.AddSingleton<ScanProgressReporter>();
|
||||||
builder.Services.AddSingleton<ScanJobProcessor>();
|
builder.Services.AddSingleton<ScanJobProcessor>();
|
||||||
builder.Services.AddSingleton<LeaseHeartbeatService>();
|
builder.Services.AddSingleton<LeaseHeartbeatService>();
|
||||||
builder.Services.AddSingleton<IDelayScheduler, SystemDelayScheduler>();
|
builder.Services.AddSingleton<IDelayScheduler, SystemDelayScheduler>();
|
||||||
|
|
||||||
|
builder.Services.AddEntryTraceAnalyzer();
|
||||||
|
|
||||||
builder.Services.TryAddSingleton<IScanJobSource, NullScanJobSource>();
|
builder.Services.TryAddSingleton<IScanJobSource, NullScanJobSource>();
|
||||||
builder.Services.TryAddSingleton<IScanAnalyzerDispatcher, NullScanAnalyzerDispatcher>();
|
builder.Services.AddSingleton<OsAnalyzerPluginCatalog>();
|
||||||
builder.Services.AddSingleton<IScanStageExecutor, AnalyzerStageExecutor>();
|
builder.Services.AddSingleton<IScanAnalyzerDispatcher, OsScanAnalyzerDispatcher>();
|
||||||
|
builder.Services.AddSingleton<IScanStageExecutor, AnalyzerStageExecutor>();
|
||||||
builder.Services.AddSingleton<ScannerWorkerHostedService>();
|
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ScannerWorkerHostedService>());
|
builder.Services.AddSingleton<ScannerWorkerHostedService>();
|
||||||
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<ScannerWorkerHostedService>());
|
||||||
var workerOptions = builder.Configuration.GetSection(ScannerWorkerOptions.SectionName).Get<ScannerWorkerOptions>() ?? new ScannerWorkerOptions();
|
|
||||||
|
var workerOptions = builder.Configuration.GetSection(ScannerWorkerOptions.SectionName).Get<ScannerWorkerOptions>() ?? new ScannerWorkerOptions();
|
||||||
builder.Services.Configure<HostOptions>(options =>
|
|
||||||
{
|
builder.Services.Configure<HostOptions>(options =>
|
||||||
options.ShutdownTimeout = workerOptions.Shutdown.Timeout;
|
{
|
||||||
});
|
options.ShutdownTimeout = workerOptions.Shutdown.Timeout;
|
||||||
|
});
|
||||||
builder.ConfigureScannerWorkerTelemetry(workerOptions);
|
|
||||||
|
builder.ConfigureScannerWorkerTelemetry(workerOptions);
|
||||||
if (workerOptions.Authority.Enabled)
|
|
||||||
{
|
if (workerOptions.Authority.Enabled)
|
||||||
builder.Services.AddStellaOpsAuthClient(clientOptions =>
|
{
|
||||||
{
|
builder.Services.AddStellaOpsAuthClient(clientOptions =>
|
||||||
clientOptions.Authority = workerOptions.Authority.Issuer?.Trim() ?? string.Empty;
|
{
|
||||||
clientOptions.ClientId = workerOptions.Authority.ClientId?.Trim() ?? string.Empty;
|
clientOptions.Authority = workerOptions.Authority.Issuer?.Trim() ?? string.Empty;
|
||||||
clientOptions.ClientSecret = workerOptions.Authority.ClientSecret;
|
clientOptions.ClientId = workerOptions.Authority.ClientId?.Trim() ?? string.Empty;
|
||||||
clientOptions.EnableRetries = workerOptions.Authority.Resilience.EnableRetries ?? true;
|
clientOptions.ClientSecret = workerOptions.Authority.ClientSecret;
|
||||||
clientOptions.HttpTimeout = TimeSpan.FromSeconds(workerOptions.Authority.BackchannelTimeoutSeconds);
|
clientOptions.EnableRetries = workerOptions.Authority.Resilience.EnableRetries ?? true;
|
||||||
|
clientOptions.HttpTimeout = TimeSpan.FromSeconds(workerOptions.Authority.BackchannelTimeoutSeconds);
|
||||||
clientOptions.DefaultScopes.Clear();
|
|
||||||
foreach (var scope in workerOptions.Authority.Scopes)
|
clientOptions.DefaultScopes.Clear();
|
||||||
{
|
foreach (var scope in workerOptions.Authority.Scopes)
|
||||||
if (string.IsNullOrWhiteSpace(scope))
|
{
|
||||||
{
|
if (string.IsNullOrWhiteSpace(scope))
|
||||||
continue;
|
{
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
clientOptions.DefaultScopes.Add(scope);
|
|
||||||
}
|
clientOptions.DefaultScopes.Add(scope);
|
||||||
|
}
|
||||||
clientOptions.RetryDelays.Clear();
|
|
||||||
foreach (var delay in workerOptions.Authority.Resilience.RetryDelays)
|
clientOptions.RetryDelays.Clear();
|
||||||
{
|
foreach (var delay in workerOptions.Authority.Resilience.RetryDelays)
|
||||||
if (delay <= TimeSpan.Zero)
|
{
|
||||||
{
|
if (delay <= TimeSpan.Zero)
|
||||||
continue;
|
{
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
clientOptions.RetryDelays.Add(delay);
|
|
||||||
}
|
clientOptions.RetryDelays.Add(delay);
|
||||||
|
}
|
||||||
if (workerOptions.Authority.Resilience.AllowOfflineCacheFallback is bool allowOffline)
|
|
||||||
{
|
if (workerOptions.Authority.Resilience.AllowOfflineCacheFallback is bool allowOffline)
|
||||||
clientOptions.AllowOfflineCacheFallback = allowOffline;
|
{
|
||||||
}
|
clientOptions.AllowOfflineCacheFallback = allowOffline;
|
||||||
|
}
|
||||||
if (workerOptions.Authority.Resilience.OfflineCacheTolerance is { } tolerance && tolerance > TimeSpan.Zero)
|
|
||||||
{
|
if (workerOptions.Authority.Resilience.OfflineCacheTolerance is { } tolerance && tolerance > TimeSpan.Zero)
|
||||||
clientOptions.OfflineCacheTolerance = tolerance;
|
{
|
||||||
}
|
clientOptions.OfflineCacheTolerance = tolerance;
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
|
}
|
||||||
builder.Logging.Configure(options =>
|
|
||||||
{
|
builder.Logging.Configure(options =>
|
||||||
options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId
|
{
|
||||||
| ActivityTrackingOptions.TraceId
|
options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId
|
||||||
| ActivityTrackingOptions.ParentId;
|
| ActivityTrackingOptions.TraceId
|
||||||
});
|
| ActivityTrackingOptions.ParentId;
|
||||||
|
});
|
||||||
var host = builder.Build();
|
|
||||||
|
var host = builder.Build();
|
||||||
await host.RunAsync();
|
|
||||||
|
await host.RunAsync();
|
||||||
public partial class Program;
|
|
||||||
|
public partial class Program;
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.Console" 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.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="1.12.0-beta.1" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="1.12.0-beta.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
<ProjectReference Include="..\StellaOps.Plugin\StellaOps.Plugin.csproj" />
|
||||||
<ProjectReference Include="..\StellaOps.Authority\StellaOps.Auth.Client\StellaOps.Auth.Client.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>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# Scanner Worker Task Board
|
# Scanner Worker Task Board
|
||||||
|
|
||||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
| 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-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-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-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-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. |
|
||||||
|
|||||||
Reference in New Issue
Block a user