feat: Add RustFS artifact object store and migration tool
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Implemented RustFsArtifactObjectStore for managing artifacts in RustFS.
- Added unit tests for RustFsArtifactObjectStore functionality.
- Created a RustFS migrator tool to transfer objects from S3 to RustFS.
- Introduced policy preview and report models for API integration.
- Added fixtures and tests for policy preview and report functionality.
- Included necessary metadata and scripts for cache_pkg package.
This commit is contained in:
Vladimir Moushkov
2025-10-23 18:53:18 +03:00
parent aaa5fbfb78
commit f4d7a15a00
117 changed files with 4849 additions and 725 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Ensure analyzer fixture assets keep LF endings for deterministic hashes
src/StellaOps.Scanner.Analyzers.Lang.Python.Tests/Fixtures/** text eol=lf

View File

@@ -108,6 +108,36 @@ jobs:
--logger "trx;LogFileName=stellaops-scanner-lang-tests.trx" \
--results-directory "$TEST_RESULTS_DIR"
- name: Run scanner analyzer performance benchmark
env:
PERF_OUTPUT_DIR: ${{ github.workspace }}/artifacts/perf/scanner-analyzers
PERF_ENVIRONMENT: ${{ github.event_name == 'pull_request' && 'preview' || 'staging' }}
run: |
set -euo pipefail
mkdir -p "$PERF_OUTPUT_DIR"
CAPTURED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
dotnet run \
--project bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj \
--configuration $BUILD_CONFIGURATION \
-- \
--repo-root . \
--baseline bench/Scanner.Analyzers/baseline.csv \
--out "$PERF_OUTPUT_DIR/latest.csv" \
--json "$PERF_OUTPUT_DIR/report.json" \
--prom "$PERF_OUTPUT_DIR/metrics.prom" \
--commit "${GITHUB_SHA}" \
--environment "$PERF_ENVIRONMENT" \
--captured-at "$CAPTURED_AT"
- name: Upload scanner analyzer benchmark artifacts
uses: actions/upload-artifact@v4
with:
name: scanner-analyzers-benchmark
path: artifacts/perf/scanner-analyzers
if-no-files-found: error
retention-days: 14
- name: Publish BuildX SBOM generator
run: |
dotnet publish src/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj \

View File

@@ -4,16 +4,17 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
## Wave Instructions
### Wave 0
- Team Attestor Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Attestor/TASKS.md`. Focus on ATTESTOR-API-11-201 (TODO), ATTESTOR-VERIFY-11-202 (TODO), ATTESTOR-OBS-11-203 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team Authority Core & Security Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Authority/TASKS.md`. Focus on AUTH-DPOP-11-001 (DONE 2025-10-20), AUTH-MTLS-11-002 (DOING 2025-10-19). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team Authority Core & Security Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Authority/TASKS.md`. Focus on AUTH-DPOP-11-001 (DONE 2025-10-20), AUTH-MTLS-11-002 (DONE 2025-10-23). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team Authority Core & Storage Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Authority/TASKS.md`. Focus on AUTHSTORAGE-MONGO-08-001 (DONE 2025-10-19). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team DevEx/CLI: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Cli/TASKS.md`. Focus on EXCITITOR-CLI-01-002 (TODO), CLI-RUNTIME-13-005 (TODO). Confirm prerequisites (external: EXCITITOR-CLI-01-001, EXCITITOR-EXPORT-01-001) before starting and report status in module TASKS.md.
- Team DevOps Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `ops/devops/TASKS.md`. Focus on DEVOPS-SEC-10-301 (DONE 2025-10-20); Wave0A prerequisites reconfirmed so remediation work may proceed. Keep module TASKS.md/Sprints in sync as patches land.
- Team Diff Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.Diff/TASKS.md`. Focus on SCANNER-DIFF-10-501 (TODO), SCANNER-DIFF-10-502 (TODO), SCANNER-DIFF-10-503 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team Diff Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.Diff/TASKS.md`. SCANNER-DIFF-10-501/502/503 all closed on 2025-10-19; keep determinism fixtures green and sync downstream consumers as Emit/Diff integration tickets arise.
- Team Scanner Storage Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.Storage/TASKS.md`. Focus on SCANNER-STORAGE-11-401 (DONE 2025-10-23) to migrate MinIO integrations to RustFS; ensure prerequisites (SCANNER-STORAGE-09-302) stay satisfied before execution and record status in module TASKS.md.
- Team Docs Guild, Plugin Team: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `docs/TASKS.md`. Focus on DOC4.AUTH-PDG (REVIEW). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team Docs/CLI: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Cli/TASKS.md`. Focus on EXCITITOR-CLI-01-003 (TODO). Confirm prerequisites (external: EXCITITOR-CLI-01-001) before starting and report status in module TASKS.md.
- Team Emit Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.Emit/TASKS.md`. Focus on SCANNER-EMIT-10-601 (TODO), SCANNER-EMIT-10-602 (TODO), SCANNER-EMIT-10-603 (TODO), SCANNER-EMIT-10-604 (TODO), SCANNER-EMIT-10-605 (TODO), SCANNER-EMIT-10-606 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team EntryTrace Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.EntryTrace/TASKS.md`. Focus on SCANNER-ENTRYTRACE-10-401 (TODO), SCANNER-ENTRYTRACE-10-402 (TODO), SCANNER-ENTRYTRACE-10-403 (TODO), SCANNER-ENTRYTRACE-10-404 (TODO), SCANNER-ENTRYTRACE-10-405 (TODO), SCANNER-ENTRYTRACE-10-406 (TODO), SCANNER-ENTRYTRACE-10-407 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team Language Analyzer Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang/SPRINTS_LANG_IMPLEMENTATION_PLAN.md`, `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-301 (TODO) and the upcoming Python/Go/.NET/Rust analyzers (10-303..306). Node sprint items 10-302/307/308/309 are DONE (latest 2025-10-21); shift coordination to remaining ecosystem analyzers and track follow-up work via module TASKS.md.
- Team Emit Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.Emit/TASKS.md`. Sprint 10 composition milestones (10-601..10-606) wrapped 2025-10-22 and SCANNER-EMIT-10-607 completed alongside; remaining watch item is SCANNER-EMIT-17-701 (Wave1) with build-id enrichment.
- Team EntryTrace Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.EntryTrace/TASKS.md`. SCANNER-ENTRYTRACE-10-401..407 landed 2025-10-19; continue monitoring determinism harness outputs and raise follow-ups if new interpreter cases appear.
- Team Language Analyzer Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang/SPRINTS_LANG_IMPLEMENTATION_PLAN.md`, `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`. Java, shared helpers, determinism harness, and the Sprint10 analyzers (10-301..10-309) are DONE (latest 2025-10-22); keep fixture refresh notes current and pivot to Wave1 benchmarking/packaging follow-ups.
- Team Notify Models Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Notify.Models/TASKS.md`. Focus on NOTIFY-MODELS-15-101 (TODO), NOTIFY-MODELS-15-102 (TODO), NOTIFY-MODELS-15-103 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team Notify Storage Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Notify.Storage.Mongo/TASKS.md`. Focus on NOTIFY-STORAGE-15-201 (TODO), NOTIFY-STORAGE-15-202 (TODO), NOTIFY-STORAGE-15-203 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
- Team Notify WebService Guild: read EXECPLAN.md Wave 0 and SPRINTS.md rows for `src/StellaOps.Notify.WebService/TASKS.md`. Focus on NOTIFY-WEB-15-101 (TODO), NOTIFY-WEB-15-102 (TODO). Confirm prerequisites (none) before starting and report status in module TASKS.md.
@@ -56,8 +57,8 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- Team DevEx/CLI, QA Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Cli/TASKS.md`. Focus on CLI-RUNTIME-13-009 (TODO). Confirm prerequisites (internal: CLI-RUNTIME-13-005 (Wave 0)) before starting and report status in module TASKS.md.
- Team DevOps Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `ops/devops/TASKS.md`. Focus on DEVOPS-REL-14-001 (TODO). Confirm prerequisites (internal: ATTESTOR-API-11-201 (Wave 0), SIGNER-API-11-101 (Wave 0)) before starting and report status in module TASKS.md.
- Team DevOps Guild, Scanner WebService Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `ops/devops/TASKS.md`. Focus on DEVOPS-SCANNER-09-204 (TODO). Confirm prerequisites (internal: SCANNER-EVENTS-15-201 (Wave 0)) before starting and report status in module TASKS.md.
- Team Emit Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scanner.Emit/TASKS.md`. Focus on SCANNER-EMIT-10-607 (TODO), SCANNER-EMIT-17-701 (TODO). Confirm prerequisites (internal: POLICY-CORE-09-005 (Wave 0), SCANNER-EMIT-10-602 (Wave 0), SCANNER-EMIT-10-604 (Wave 0)) before starting and report status in module TASKS.md.
- Team Language Analyzer Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-303 (DONE 2025-10-21), SCANNER-ANALYZERS-LANG-10-304 (DOING 2025-10-22), SCANNER-ANALYZERS-LANG-10-305 (DOING 2025-10-22), SCANNER-ANALYZERS-LANG-10-306 (TODO). Node stream (tasks 10-302/309) closed on 2025-10-21; verify prereqs SCANNER-ANALYZERS-LANG-10-301/307 remain satisfied before pivoting to the remaining language analyzers.
- Team Emit Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scanner.Emit/TASKS.md`. SCANNER-EMIT-10-607 shipped 2025-10-22; remaining focus is SCANNER-EMIT-17-701 (build-id enrichment). Confirm prerequisites (internal: POLICY-CORE-09-005 (Wave 0), SCANNER-EMIT-10-602 (Wave 0), SCANNER-EMIT-10-604 (Wave 0)) before starting and report status in module TASKS.md.
- Team Language Analyzer Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`. Sprint10 language analyzers (10-303..10-306) wrapped by 2025-10-22; shift to Wave1 benchmarking/packaging follow-ups (10-308+/309 variants) and ensure shared helpers stay stable. Node stream (tasks 10-302/309) closed on 2025-10-21; verify prereqs SCANNER-ANALYZERS-LANG-10-301/307 remain satisfied before new work.
- Team Licensing Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `ops/licensing/TASKS.md`. Focus on DEVOPS-LIC-14-004 (TODO). Confirm prerequisites (internal: AUTH-MTLS-11-002 (Wave 0)) before starting and report status in module TASKS.md.
- Team Notify Engine Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Notify.Engine/TASKS.md`. Focus on NOTIFY-ENGINE-15-301 (TODO). Confirm prerequisites (internal: NOTIFY-MODELS-15-101 (Wave 0)) before starting and report status in module TASKS.md.
- Team Notify Queue Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Notify.Queue/TASKS.md`. Focus on NOTIFY-QUEUE-15-401 (TODO). Confirm prerequisites (internal: NOTIFY-MODELS-15-101 (Wave 0)) before starting and report status in module TASKS.md.
@@ -68,7 +69,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- Team Scheduler Storage Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scheduler.Storage.Mongo/TASKS.md`. Focus on SCHED-STORAGE-16-203 (TODO), SCHED-STORAGE-16-202 (TODO). Confirm prerequisites (internal: SCHED-STORAGE-16-201 (Wave 0)) before starting and report status in module TASKS.md.
- Team Scheduler WebService Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scheduler.WebService/TASKS.md`. Focus on SCHED-WEB-16-104 (TODO), SCHED-WEB-16-102 (TODO). Confirm prerequisites (internal: SCHED-QUEUE-16-401 (Wave 0), SCHED-STORAGE-16-201 (Wave 0), SCHED-WEB-16-101 (Wave 0)) before starting and report status in module TASKS.md.
- Team Scheduler Worker Guild: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scheduler.Worker/TASKS.md`. Focus on SCHED-WORKER-16-201 (TODO). Confirm prerequisites (internal: SCHED-QUEUE-16-401 (Wave 0)) before starting and report status in module TASKS.md.
- Team TBD: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-305A (DONE 2025-10-22), SCANNER-ANALYZERS-LANG-10-304A (DONE 2025-10-22), SCANNER-ANALYZERS-LANG-10-303A (DONE 2025-10-21), SCANNER-ANALYZERS-LANG-10-306A (TODO); Node add-ons 10-307N/10-308N/10-309N now DONE with restart-time packaging verified 2025-10-21. Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-302C (Wave 0), SCANNER-ANALYZERS-LANG-10-307 (Wave 0)) before starting and report status in module TASKS.md.
- Team TBD: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. SCANNER-ANALYZERS-LANG-10-305A/304A/303A/306A all closed by 2025-10-22; use this slot to review cross-language fixture hygiene and prep Wave1 benchmarking tickets. Node add-ons 10-307N/10-308N/10-309N remain DONE with restart-time packaging verified 2025-10-21. Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-302C (Wave 0), SCANNER-ANALYZERS-LANG-10-307 (Wave 0)) before starting any new follow-ups and report status in module TASKS.md.
- Team Team Excititor Connectors MSRC: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.MSRC.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-MS-01-003 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-MS-01-002 (Wave 0); external: EXCITITOR-POLICY-01-001) before starting and report status in module TASKS.md.
- Team Team Excititor Connectors Oracle: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.Oracle.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-ORACLE-01-002 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-ORACLE-01-001 (Wave 0); external: EXCITITOR-STORAGE-01-003) before starting and report status in module TASKS.md.
- Team Team Excititor Connectors SUSE: read EXECPLAN.md Wave 1 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.SUSE.RancherVEXHub/TASKS.md`. Focus on EXCITITOR-CONN-SUSE-01-003 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-SUSE-01-002 (Wave 0); external: EXCITITOR-POLICY-01-001) before starting and report status in module TASKS.md.
@@ -82,19 +83,19 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- Team Bench Guild, Notify Team: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `bench/TASKS.md`. Focus on BENCH-NOTIFY-15-001 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-301 (Wave 1)) before starting and report status in module TASKS.md.
- Team Bench Guild, Scheduler Team: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `bench/TASKS.md`. Focus on BENCH-IMPACT-16-001 (TODO). Confirm prerequisites (internal: SCHED-IMPACT-16-301 (Wave 1)) before starting and report status in module TASKS.md.
- Team Deployment Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `ops/deployment/TASKS.md`. Focus on DEVOPS-OPS-14-003 (TODO). Confirm prerequisites (internal: DEVOPS-REL-14-001 (Wave 1)) before starting and report status in module TASKS.md.
- Team DevOps Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `ops/devops/TASKS.md`. Focus on DEVOPS-MIRROR-08-001 (DONE 2025-10-19), DEVOPS-PERF-10-002 (TODO), DEVOPS-REL-17-002 (TODO), and DEVOPS-NUGET-13-001 (TODO). Confirm prerequisites (internal: BENCH-SCANNER-10-002 (Wave 1), DEVOPS-REL-14-001 (Wave 1), SCANNER-EMIT-17-701 (Wave 1)) before starting and report status in module TASKS.md.
- Team DevOps Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `ops/devops/TASKS.md`. Focus on DEVOPS-MIRROR-08-001 (DONE 2025-10-19), DEVOPS-PERF-10-002 (TODO), DEVOPS-REL-14-004 (TODO), DEVOPS-REL-17-002 (TODO), and DEVOPS-NUGET-13-001 (TODO). Confirm prerequisites (internal: BENCH-SCANNER-10-002 (Wave 1), DEVOPS-REL-14-001 (Wave 1), SCANNER-EMIT-17-701 (Wave 1)) before starting and report status in module TASKS.md.
- Team DevOps Guild, Notify Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `ops/devops/TASKS.md`. Focus on DEVOPS-SCANNER-09-205 (TODO). Confirm prerequisites (internal: DEVOPS-SCANNER-09-204 (Wave 1)) before starting and report status in module TASKS.md.
- Team Notify Engine Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Notify.Engine/TASKS.md`. Focus on NOTIFY-ENGINE-15-302 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-301 (Wave 1)) before starting and report status in module TASKS.md.
- Team Notify Queue Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Notify.Queue/TASKS.md`. Focus on NOTIFY-QUEUE-15-403 (TODO), NOTIFY-QUEUE-15-402 (TODO). Confirm prerequisites (internal: NOTIFY-QUEUE-15-401 (Wave 1)) before starting and report status in module TASKS.md.
- Team Notify WebService Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Notify.WebService/TASKS.md`. Focus on NOTIFY-WEB-15-104 (TODO). Confirm prerequisites (internal: NOTIFY-QUEUE-15-401 (Wave 1), NOTIFY-STORAGE-15-201 (Wave 0)) before starting and report status in module TASKS.md.
- Team Notify Worker Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Notify.Worker/TASKS.md`. Focus on NOTIFY-WORKER-15-201 (TODO), NOTIFY-WORKER-15-202 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-301 (Wave 1), NOTIFY-QUEUE-15-401 (Wave 1)) before starting and report status in module TASKS.md.
- Team Offline Kit Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `ops/offline-kit/TASKS.md`. Focus on DEVOPS-OFFLINE-14-002 (TODO) and DEVOPS-OFFLINE-18-003 (TODO). Confirm prerequisites (internal: DEVOPS-REL-14-001 (Wave 1)) before starting and report status in module TASKS.md.
- Team Offline Kit Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `ops/offline-kit/TASKS.md`. Focus on DEVOPS-OFFLINE-14-002 (TODO), DEVOPS-OFFLINE-18-003 (TODO), and DEVOPS-OFFLINE-18-005 (TODO). Confirm prerequisites (internal: DEVOPS-REL-14-001 (Wave 1), DEVOPS-REL-14-004 (Wave 2)) before starting and report status in module TASKS.md.
- Team Samples Guild, Policy Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `samples/TASKS.md`. Focus on SAMPLES-13-004 (TODO). Confirm prerequisites (internal: POLICY-CORE-09-006 (Wave 0), UI-POLICY-13-007 (Wave 1)) before starting and report status in module TASKS.md.
- Team Scanner WebService Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scanner.WebService/TASKS.md`. Focus on SCANNER-RUNTIME-12-302 (TODO). Confirm prerequisites (internal: SCANNER-RUNTIME-12-301 (Wave 1), ZASTAVA-CORE-12-201 (Wave 0)) before starting and report status in module TASKS.md.
- Team Scheduler ImpactIndex Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scheduler.ImpactIndex/TASKS.md`. Focus on SCHED-IMPACT-16-303 (TODO), SCHED-IMPACT-16-302 (TODO). Confirm prerequisites (internal: SCHED-IMPACT-16-301 (Wave 1)) before starting and report status in module TASKS.md.
- Team Scheduler WebService Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scheduler.WebService/TASKS.md`. Focus on SCHED-WEB-16-103 (TODO). Confirm prerequisites (internal: SCHED-WEB-16-102 (Wave 1)) before starting and report status in module TASKS.md.
- Team Scheduler Worker Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scheduler.Worker/TASKS.md`. Focus on SCHED-WORKER-16-202 (TODO), SCHED-WORKER-16-205 (TODO). Confirm prerequisites (internal: SCHED-IMPACT-16-301 (Wave 1), SCHED-WORKER-16-201 (Wave 1)) before starting and report status in module TASKS.md.
- Team TBD: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-305B (DONE 2025-10-22), SCANNER-ANALYZERS-LANG-10-304B (DONE 2025-10-22), SCANNER-ANALYZERS-LANG-10-303B (DONE 2025-10-21), SCANNER-ANALYZERS-LANG-10-306B (TODO); Node packaging milestone 10-308N closed 2025-10-21. Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-303A (Wave 1), SCANNER-ANALYZERS-LANG-10-304A (Wave 1), SCANNER-ANALYZERS-LANG-10-305A (Wave 1), SCANNER-ANALYZERS-LANG-10-306A (Wave 1), SCANNER-ANALYZERS-LANG-10-307N (Wave 1)) before starting and report status in module TASKS.md.
- Team TBD: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. SCANNER-ANALYZERS-LANG-10-305B/304B/303B/306B wrapped on 2025-10-22; next focus moves to `10-307*` shared helper integration and Wave2 benchmark polish. Node packaging milestone 10-308N closed 2025-10-21. Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-303A (Wave 1), SCANNER-ANALYZERS-LANG-10-304A (Wave 1), SCANNER-ANALYZERS-LANG-10-305A (Wave 1), SCANNER-ANALYZERS-LANG-10-306A (Wave 1), SCANNER-ANALYZERS-LANG-10-307N (Wave 1)) before starting new work and report status in module TASKS.md.
- Team Team Excititor Connectors Oracle: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.Oracle.CSAF/TASKS.md`. Focus on EXCITITOR-CONN-ORACLE-01-003 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-ORACLE-01-002 (Wave 1); external: EXCITITOR-POLICY-01-001) before starting and report status in module TASKS.md.
- Team Team Excititor Export: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Excititor.Export/TASKS.md`. Focus on EXCITITOR-EXPORT-01-007 (DONE 2025-10-21). Confirm prerequisites (internal: EXCITITOR-EXPORT-01-006 (Wave 1)) before starting and report status in module TASKS.md.
- Team Zastava Observer Guild: read EXECPLAN.md Wave 2 and SPRINTS.md rows for `src/StellaOps.Zastava.Observer/TASKS.md`. Focus on ZASTAVA-OBS-12-002 (TODO). Confirm prerequisites (internal: ZASTAVA-OBS-12-001 (Wave 1)) before starting and report status in module TASKS.md.
@@ -106,7 +107,7 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- Team Notify Engine Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Notify.Engine/TASKS.md`. Focus on NOTIFY-ENGINE-15-303 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-302 (Wave 2)) before starting and report status in module TASKS.md.
- Team Notify Worker Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Notify.Worker/TASKS.md`. Focus on NOTIFY-WORKER-15-203 (TODO). Confirm prerequisites (internal: NOTIFY-ENGINE-15-302 (Wave 2)) before starting and report status in module TASKS.md.
- Team Scheduler Worker Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Scheduler.Worker/TASKS.md`. Focus on SCHED-WORKER-16-203 (TODO). Confirm prerequisites (internal: SCHED-WORKER-16-202 (Wave 2)) before starting and report status in module TASKS.md.
- Team TBD: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-305C (DONE 2025-10-22), SCANNER-ANALYZERS-LANG-10-304C (TODO), SCANNER-ANALYZERS-LANG-10-309N (TODO), SCANNER-ANALYZERS-LANG-10-303C (DONE 2025-10-21), SCANNER-ANALYZERS-LANG-10-306C (TODO). Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-303B (Wave 2), SCANNER-ANALYZERS-LANG-10-304B (Wave 2), SCANNER-ANALYZERS-LANG-10-305B (Wave 2), SCANNER-ANALYZERS-LANG-10-306B (Wave 2), SCANNER-ANALYZERS-LANG-10-308N (Wave 2)) before starting and report status in module TASKS.md.
- Team TBD: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. SCANNER-ANALYZERS-LANG-10-305C/304C/309N/303C/306C are all DONE (latest 2025-10-22); remaining Wave3 attention shifts to 10-307* helper consolidation and subsequent benchmarking tickets. Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-303B (Wave 2), SCANNER-ANALYZERS-LANG-10-304B (Wave 2), SCANNER-ANALYZERS-LANG-10-305B (Wave 2), SCANNER-ANALYZERS-LANG-10-306B (Wave 2), SCANNER-ANALYZERS-LANG-10-308N (Wave 2)) before scheduling new work and report status in module TASKS.md.
- Team Zastava Observer Guild: read EXECPLAN.md Wave 3 and SPRINTS.md rows for `src/StellaOps.Zastava.Observer/TASKS.md`. Focus on ZASTAVA-OBS-12-003 (TODO), ZASTAVA-OBS-12-004 (TODO), ZASTAVA-OBS-17-005 (TODO). Confirm prerequisites (internal: ZASTAVA-OBS-12-002 (Wave 2)) before starting and report status in module TASKS.md.
### Wave 4
@@ -118,17 +119,17 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- Team Notify Worker Guild: read EXECPLAN.md Wave 4 and SPRINTS.md rows for `src/StellaOps.Notify.Worker/TASKS.md`. Focus on NOTIFY-WORKER-15-204 (TODO). Confirm prerequisites (internal: NOTIFY-WORKER-15-203 (Wave 3)) before starting and report status in module TASKS.md.
- Team Policy Guild, Scanner WebService Guild: read EXECPLAN.md Wave 4 and SPRINTS.md rows for `src/StellaOps.Policy/TASKS.md`. Focus on POLICY-RUNTIME-17-201 (TODO). Confirm prerequisites (internal: ZASTAVA-OBS-17-005 (Wave 3)) before starting and report status in module TASKS.md.
- Team Scheduler Worker Guild: read EXECPLAN.md Wave 4 and SPRINTS.md rows for `src/StellaOps.Scheduler.Worker/TASKS.md`. Focus on SCHED-WORKER-16-204 (TODO). Confirm prerequisites (internal: SCHED-WORKER-16-203 (Wave 3)) before starting and report status in module TASKS.md.
- Team TBD: read EXECPLAN.md Wave 4 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-307D (DONE 2025-10-22), SCANNER-ANALYZERS-LANG-10-307G (TODO), SCANNER-ANALYZERS-LANG-10-307P (TODO), SCANNER-ANALYZERS-LANG-10-307R (TODO). Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-303C (Wave 3), SCANNER-ANALYZERS-LANG-10-304C (Wave 3), SCANNER-ANALYZERS-LANG-10-305C (Wave 3), SCANNER-ANALYZERS-LANG-10-306C (Wave 3)) before starting and report status in module TASKS.md.
- Team TBD: read EXECPLAN.md Wave 4 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. SCANNER-ANALYZERS-LANG-10-307D/G/P are DONE (latest 2025-10-23); remaining focus is SCANNER-ANALYZERS-LANG-10-307R (TODO). Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-303C (Wave 3), SCANNER-ANALYZERS-LANG-10-304C (Wave 3), SCANNER-ANALYZERS-LANG-10-305C (Wave 3), SCANNER-ANALYZERS-LANG-10-306C (Wave 3)) before progressing and report status in module TASKS.md.
### Wave 5
- Team Excititor Connectors Stella: read EXECPLAN.md Wave 5 and SPRINTS.md rows for `src/StellaOps.Excititor.Connectors.StellaOpsMirror/TASKS.md`. Focus on EXCITITOR-CONN-STELLA-07-003 (TODO). Confirm prerequisites (internal: EXCITITOR-CONN-STELLA-07-002 (Wave 4)) before starting and report status in module TASKS.md.
- Team Notify Connectors Guild: read EXECPLAN.md Wave 5 and SPRINTS.md rows for `src/StellaOps.Notify.Connectors.Email/TASKS.md`, `src/StellaOps.Notify.Connectors.Slack/TASKS.md`, `src/StellaOps.Notify.Connectors.Teams/TASKS.md`, `src/StellaOps.Notify.Connectors.Webhook/TASKS.md`. Focus on NOTIFY-CONN-SLACK-15-502 (DONE), NOTIFY-CONN-TEAMS-15-602 (DONE), NOTIFY-CONN-EMAIL-15-702 (BLOCKED 2025-10-20), NOTIFY-CONN-WEBHOOK-15-802 (BLOCKED 2025-10-20). Confirm prerequisites (internal: NOTIFY-CONN-EMAIL-15-701 (Wave 4), NOTIFY-CONN-SLACK-15-501 (Wave 4), NOTIFY-CONN-TEAMS-15-601 (Wave 4), NOTIFY-CONN-WEBHOOK-15-801 (Wave 4)) before starting and report status in module TASKS.md.
- Team Scanner WebService Guild: read EXECPLAN.md Wave 5 and SPRINTS.md rows for `src/StellaOps.Scanner.WebService/TASKS.md`. Focus on SCANNER-RUNTIME-17-401 (TODO). Confirm prerequisites (internal: POLICY-RUNTIME-17-201 (Wave 4), SCANNER-EMIT-17-701 (Wave 1), SCANNER-RUNTIME-12-301 (Wave 1), ZASTAVA-OBS-17-005 (Wave 3)) before starting and report status in module TASKS.md.
- Team TBD: read EXECPLAN.md Wave 5 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-308D (DONE 2025-10-23), SCANNER-ANALYZERS-LANG-10-308G (TODO), SCANNER-ANALYZERS-LANG-10-308P (TODO), SCANNER-ANALYZERS-LANG-10-308R (TODO). Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-307D (Wave 4), SCANNER-ANALYZERS-LANG-10-307G (Wave 4), SCANNER-ANALYZERS-LANG-10-307P (Wave 4), SCANNER-ANALYZERS-LANG-10-307R (Wave 4)) before starting and report status in module TASKS.md.
- Team TBD: read EXECPLAN.md Wave 5 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. SCANNER-ANALYZERS-LANG-10-308D/G/P completed (2025-10-23/2025-10-22/2025-10-23); pending items are SCANNER-ANALYZERS-LANG-10-308R (TODO). Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-307D (Wave 4), SCANNER-ANALYZERS-LANG-10-307G (Wave 4), SCANNER-ANALYZERS-LANG-10-307P (Wave 4), SCANNER-ANALYZERS-LANG-10-307R (Wave 4)) before starting and report status in module TASKS.md.
### Wave 6
- Team Notify Connectors Guild: read EXECPLAN.md Wave 6 and SPRINTS.md rows for `src/StellaOps.Notify.Connectors.Email/TASKS.md`, `src/StellaOps.Notify.Connectors.Slack/TASKS.md`, `src/StellaOps.Notify.Connectors.Teams/TASKS.md`, `src/StellaOps.Notify.Connectors.Webhook/TASKS.md`. Focus on NOTIFY-CONN-SLACK-15-503 (DONE), NOTIFY-CONN-TEAMS-15-603 (DONE), NOTIFY-CONN-EMAIL-15-703 (DONE), NOTIFY-CONN-WEBHOOK-15-803 (DONE). Confirm packaging outputs remain deterministic while upstream implementation tasks (15-702/802) stay blocked.
- Team TBD: read EXECPLAN.md Wave 6 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-309D (DONE 2025-10-23), SCANNER-ANALYZERS-LANG-10-309G (TODO), SCANNER-ANALYZERS-LANG-10-309P (TODO), SCANNER-ANALYZERS-LANG-10-309R (TODO). Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-308D (Wave 5), SCANNER-ANALYZERS-LANG-10-308G (Wave 5), SCANNER-ANALYZERS-LANG-10-308P (Wave 5), SCANNER-ANALYZERS-LANG-10-308R (Wave 5)) before starting and report status in module TASKS.md.
- Team TBD: read EXECPLAN.md Wave 6 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. SCANNER-ANALYZERS-LANG-10-309D/G/P completed (2025-10-23/2025-10-22/2025-10-23); remaining item is SCANNER-ANALYZERS-LANG-10-309R (TODO). Confirm prerequisites (internal: SCANNER-ANALYZERS-LANG-10-308D (Wave 5), SCANNER-ANALYZERS-LANG-10-308G (Wave 5), SCANNER-ANALYZERS-LANG-10-308P (Wave 5), SCANNER-ANALYZERS-LANG-10-308R (Wave 5)) before starting and report status in module TASKS.md.
### Wave 7
- Team Team Core Engine & Storage Analytics: read EXECPLAN.md Wave 7 and SPRINTS.md rows for `src/StellaOps.Concelier.Core/TASKS.md`. Focus on FEEDCORE-ENGINE-07-001 (DONE 2025-10-19). Confirm prerequisites (internal: FEEDSTORAGE-DATA-07-001 (Wave 10)) before starting and report status in module TASKS.md.
@@ -317,63 +318,63 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- **Sprint 10** · Scanner Analyzers & SBOM
- Team: Diff Guild
- Path: `src/StellaOps.Scanner.Diff/TASKS.md`
1. [TODO] SCANNER-DIFF-10-501 — Build component differ tracking add/remove/version changes with deterministic ordering.
1. [DONE 2025-10-19] SCANNER-DIFF-10-501 — Build component differ tracking add/remove/version changes with deterministic ordering.
• Prereqs: —
• Current: TODO
2. [TODO] SCANNER-DIFF-10-502 — Attribute diffs to introducing/removing layers including provenance evidence.
• Current: DONE — Diff engine produces deterministic add/remove/version deltas; regression suite covers warm/cold path parity.
2. [DONE 2025-10-19] SCANNER-DIFF-10-502 — Attribute diffs to introducing/removing layers including provenance evidence.
• Prereqs: —
• Current: TODO
3. [TODO] SCANNER-DIFF-10-503 — Produce JSON diff output for inventory vs usage views aligned with API contract.
• Current: DONE — Layer attribution recorded on every change; fixtures assert provenance integrity.
3. [DONE 2025-10-19] SCANNER-DIFF-10-503 — Produce JSON diff output for inventory vs usage views aligned with API contract.
• Prereqs: —
• Current: TODO
• Current: DONE — JSON serializer emits stable ordering; golden outputs locked in tests.
- Team: Emit Guild
- Path: `src/StellaOps.Scanner.Emit/TASKS.md`
1. [TODO] SCANNER-EMIT-10-601 — Compose inventory SBOM (CycloneDX JSON/Protobuf) from layer fragments.
1. [DONE 2025-10-22] SCANNER-EMIT-10-601 — Compose inventory SBOM (CycloneDX JSON/Protobuf) from layer fragments.
• Prereqs: —
• Current: TODO
2. [TODO] SCANNER-EMIT-10-602 — Compose usage SBOM leveraging EntryTrace to flag actual usage.
• Current: DONE — Inventory builder validated against CycloneDX schema; deterministic fixtures added.
2. [DONE 2025-10-22] SCANNER-EMIT-10-602 — Compose usage SBOM leveraging EntryTrace to flag actual usage.
• Prereqs: —
• Current: TODO
3. [TODO] SCANNER-EMIT-10-603 — Generate BOM index sidecar (purl table + roaring bitmap + usage flag).
• Current: DONE — Usage view toggles wired; tests confirm subset alignment with EntryTrace signals.
3. [DONE 2025-10-22] SCANNER-EMIT-10-603 — Generate BOM index sidecar (purl table + roaring bitmap + usage flag).
• Prereqs: —
• Current: TODO
4. [TODO] SCANNER-EMIT-10-604 — Package artifacts for export + attestation with deterministic manifests.
• Current: DONE — BOM Index format published with roaring bitmap helpers; golden fixtures locked.
4. [DONE 2025-10-22] SCANNER-EMIT-10-604 — Package artifacts for export + attestation with deterministic manifests.
• Prereqs: —
• Current: TODO
5. [TODO] SCANNER-EMIT-10-605 — Emit BOM-Index sidecar schema/fixtures (CRITICAL PATH for SP16).
• Current: DONE — Export packaging deterministic; integration test with storage succeeds.
5. [DONE 2025-10-22] SCANNER-EMIT-10-605 — Emit BOM-Index sidecar schema/fixtures (CRITICAL PATH for SP16).
• Prereqs: —
• Current: TODO
6. [TODO] SCANNER-EMIT-10-606 — Usage view bit flags integrated with EntryTrace.
• Current: DONE — `bom-index@1` schema + fixtures published; Scheduler notes updated.
6. [DONE 2025-10-22] SCANNER-EMIT-10-606 — Usage view bit flags integrated with EntryTrace.
• Prereqs: —
• Current: TODO
• Current: DONE — EntryTrace usage bits round-trip in BOM Index; regression harness verified.
- Team: EntryTrace Guild
- Path: `src/StellaOps.Scanner.EntryTrace/TASKS.md`
1. [TODO] SCANNER-ENTRYTRACE-10-401 — POSIX shell AST parser with deterministic output.
1. [DONE 2025-10-19] SCANNER-ENTRYTRACE-10-401 — POSIX shell AST parser with deterministic output.
• Prereqs: —
• Current: TODO
2. [TODO] SCANNER-ENTRYTRACE-10-402 — Command resolution across layered rootfs with evidence attribution.
• Current: DONE — Parser emits stable AST; determinism tests captured.
2. [DONE 2025-10-19] SCANNER-ENTRYTRACE-10-402 — Command resolution across layered rootfs with evidence attribution.
• Prereqs: —
• Current: TODO
3. [TODO] SCANNER-ENTRYTRACE-10-403 — Interpreter tracing for shell wrappers to Python/Node/Java launchers.
• Current: DONE — Resolver walks layered PATH with provenance evidence; fixtures validate.
3. [DONE 2025-10-19] SCANNER-ENTRYTRACE-10-403 — Interpreter tracing for shell wrappers to Python/Node/Java launchers.
• Prereqs: —
• Current: TODO
4. [TODO] SCANNER-ENTRYTRACE-10-404 — Python entry analyzer (venv shebang, module invocation, usage flag).
• Current: DONE — Interpreter tracer resolves Python/Node/Java hand-offs; golden graphs updated.
4. [DONE 2025-10-19] SCANNER-ENTRYTRACE-10-404 — Python entry analyzer (venv shebang, module invocation, usage flag).
• Prereqs: —
• Current: TODO
5. [TODO] SCANNER-ENTRYTRACE-10-405 — Node/Java launcher analyzer capturing script/jar targets.
• Current: DONE — Python analyzer surfaces venv/module details; usage flag propagated.
5. [DONE 2025-10-19] SCANNER-ENTRYTRACE-10-405 — Node/Java launcher analyzer capturing script/jar targets.
• Prereqs: —
• Current: TODO
6. [TODO] SCANNER-ENTRYTRACE-10-406 — Explainability + diagnostics for unresolved constructs with metrics.
• Current: DONE — Node/Java launchers traced end-to-end; evidence attached for each hop.
6. [DONE 2025-10-19] SCANNER-ENTRYTRACE-10-406 — Explainability + diagnostics for unresolved constructs with metrics.
• Prereqs: —
• Current: TODO
7. [TODO] SCANNER-ENTRYTRACE-10-407 — Package EntryTrace analyzers as restart-time plug-ins (manifest + host registration).
• Current: DONE — Diagnostics enumerated, metrics emitted via `EntryTraceMetrics`.
7. [DONE 2025-10-19] SCANNER-ENTRYTRACE-10-407 — Package EntryTrace analyzers as restart-time plug-ins (manifest + host registration).
• Prereqs: —
• Current: TODO
• Current: DONE — Plug-in manifests under `plugins/scanner/entrytrace`; restart-only guard documented.
- Team: Language Analyzer Guild
- Path: `src/StellaOps.Scanner.Analyzers.Lang/SPRINTS_LANG_IMPLEMENTATION_PLAN.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-302..309 — Detailed per-language sprint plan (Node, Python, Go, .NET, Rust) with gates and benchmarks.
1. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-302..309 — Detailed per-language sprint plan (Node, Python, Go, .NET, Rust) with gates and benchmarks.
• Prereqs: —
• Current: TODO
• Current: DONE — Implementation plan captured per language with progress notes through 2025-10-22.
- Path: `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`
1. [DONE 2025-10-19] SCANNER-ANALYZERS-LANG-10-301 — Java analyzer emitting `pkg:maven` with provenance.
• Prereqs: —
@@ -389,18 +390,23 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- Path: `src/StellaOps.Attestor/TASKS.md`
1. [TODO] ATTESTOR-API-11-201 — `/rekor/entries` submission pipeline with dedupe, proof acquisition, and persistence.
• Prereqs: —
• Current: TODO
• Current: DOING (2025-10-23) — RustFS migration underway.
2. [TODO] ATTESTOR-VERIFY-11-202 — `/rekor/verify` + retrieval endpoints validating signatures and Merkle proofs.
• Prereqs: —
• Current: TODO
3. [TODO] ATTESTOR-OBS-11-203 — Telemetry, alerting, mTLS hardening, and archive workflow for Attestor.
• Prereqs: —
• Current: TODO
- Team: Scanner Storage Guild
- Path: `src/StellaOps.Scanner.Storage/TASKS.md`
1. [DONE 2025-10-23] SCANNER-STORAGE-11-401 — Migrate scanner artifact storage from MinIO to RustFS, including driver, configuration, and migration tooling.
• Prereqs: SCANNER-STORAGE-09-302 (Wave 0)
• Current: DONE — RustFS driver, deployment manifests, migration tool, and test coverage shipped.
- Team: Authority Core & Security Guild
- Path: `src/StellaOps.Authority/TASKS.md`
2. [DOING] AUTH-MTLS-11-002 — Add OAuth mTLS client credential support with certificate-bound tokens and introspection updates.
2. [DONE 2025-10-23] AUTH-MTLS-11-002 — Add OAuth mTLS client credential support with certificate-bound tokens and introspection updates.
• Prereqs: —
• Current: DOING (2025-10-19)
• Current: DONE — mTLS audience enforcement + certificate binding validation shipped; docs/tests updated.
- **Sprint 12** · Runtime Guardrails
- Team: Zastava Core Guild
@@ -552,32 +558,32 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
• Current: DONE — Python analyzer ingests METADATA/WHEEL/entry_points with deterministic ordering and UTF-8 normalization. Fixtures updated (`simple-venv`).
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-306A — Parse Cargo metadata (`Cargo.lock`, `.fingerprint`, `.metadata`) and map crates to components with evidence.
1. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-306A — Parse Cargo metadata (`Cargo.lock`, `.fingerprint`, `.metadata`) and map crates to components with evidence.
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
• Current: TODO
• Current: DONE — Cargo metadata walker emits `pkg:cargo` components with provenance and deterministic fixtures.
- **Sprint 10** · Scanner Analyzers & SBOM
- Team: Emit Guild
- Path: `src/StellaOps.Scanner.Emit/TASKS.md`
1. [TODO] SCANNER-EMIT-10-607 — Embed scoring inputs, confidence band, and `quietedBy` provenance into CycloneDX 1.6 and DSSE predicates; verify deterministic serialization.
1. [DONE 2025-10-22] SCANNER-EMIT-10-607 — Embed scoring inputs, confidence band, and `quietedBy` provenance into CycloneDX 1.6 and DSSE predicates; verify deterministic serialization.
• Prereqs: SCANNER-EMIT-10-604 (Wave 0), POLICY-CORE-09-005 (Wave 0)
• Current: TODO
• Current: DONE — SBOM/attestation fixtures include scoring metadata and serialize deterministically.
- Team: Language Analyzer Guild
- Path: `src/StellaOps.Scanner.Analyzers.Lang/TASKS.md`
1. [DONE 2025-10-21] SCANNER-ANALYZERS-LANG-10-309 — Package language analyzers as restart-time plug-ins (manifest + host registration).
• Prereqs: SCANNER-ANALYZERS-LANG-10-301 (Wave 0)
• Current: DONE — Manifest published under `plugins/scanner/analyzers/lang/`, Worker loader wired, integration tests updated.
2. [TODO] SCANNER-ANALYZERS-LANG-10-306 — Rust analyzer detecting crate provenance or falling back to `bin:{sha256}`.
2. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-306 — Rust analyzer detecting crate provenance or falling back to `bin:{sha256}`.
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
• Current: TODO
• Current: DONE — Rust analyzer emits cargo components with provenance and deterministic fallbacks.
3. [DONE 2025-10-21] SCANNER-ANALYZERS-LANG-10-302 — Node analyzer resolving workspaces/symlinks into `pkg:npm` identities.
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
• Current: DONE — Workspace/symlink coverage validated via determinism fixtures; metrics + lifecycle script evidence landed.
4. [DOING 2025-10-22] SCANNER-ANALYZERS-LANG-10-304 — Go analyzer leveraging buildinfo for `pkg:golang` components.
4. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-304 — Go analyzer leveraging buildinfo for `pkg:golang` components.
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
• Current: TODO
5. [DOING 2025-10-22] SCANNER-ANALYZERS-LANG-10-305 — .NET analyzer parsing `*.deps.json`, assembly metadata, and RID variants.
• Current: DONE — Buildinfo decoder + DWARF fallbacks captured; fixtures and benchmarks green.
5. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-305 — .NET analyzer parsing `*.deps.json`, assembly metadata, and RID variants.
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
• Current: DOING — Implementing initial deps/runtimeconfig parsing for RID-aware components.
• Current: DONE — RID-aware deps/runtimeconfig parser emits deterministic NuGet components; tests landed.
6. [DONE 2025-10-21] SCANNER-ANALYZERS-LANG-10-303 — Python analyzer consuming `*.dist-info` metadata and RECORD hashes.
• Prereqs: SCANNER-ANALYZERS-LANG-10-307 (Wave 0)
• Current: DONE — Dist-info parser, RECORD verifier, editable install metadata, and entrypoint usage hints shipped with deterministic fixture/tests.
@@ -737,21 +743,26 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
• Prereqs: SCANNER-ANALYZERS-LANG-10-303A (Wave 1)
• Current: DONE — Streaming SHA-256 verification with deterministic mismatch evidence; unsupported algorithms tracked; fixtures validated.
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-306B — Implement heuristic classifier using ELF section names, symbol mangling, and `.comment` data for stripped binaries.
1. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-306B — Implement heuristic classifier using ELF section names, symbol mangling, and `.comment` data for stripped binaries.
• Prereqs: SCANNER-ANALYZERS-LANG-10-306A (Wave 1)
• Current: TODO
• Current: DONE — Heuristic classifier flags stripped binaries, regression tests guard false positives.
- **Sprint 10** · DevOps Perf
- Team: DevOps Guild
- Path: `ops/devops/TASKS.md`
1. [TODO] DEVOPS-PERF-10-002 — Publish analyzer bench metrics to Grafana/perf workbook and alarm on ≥20% regressions.
1. [DONE (2025-10-23)] DEVOPS-PERF-10-002 — Publish analyzer bench metrics to Grafana/perf workbook and alarm on ≥20% regressions.
• Prereqs: BENCH-SCANNER-10-002 (Wave 1)
• Current: TODO
• Current: DONE (2025-10-23)
- **Sprint 10** · Samples
- Team: Samples Guild, Policy Guild
- Path: `samples/TASKS.md`
1. [TODO] SAMPLES-13-004 — Add policy preview/report fixtures showing confidence bands and unknown-age tags.
1. [DONE (2025-10-23)] SAMPLES-13-004 — Add policy preview/report fixtures showing confidence bands and unknown-age tags.
• Prereqs: POLICY-CORE-09-006 (Wave 0), UI-POLICY-13-007 (Wave 1)
• Current: TODO
• Current: DONE (2025-10-23)
- Team: UI Guild
- Path: `src/StellaOps.Web/TASKS.md`
1. [DONE (2025-10-23)] WEB-POLICY-FIXTURES-10-001 — Wire policy preview/report doc fixtures into UI harness (test utility or Storybook substitute) with type bindings and validation guard.
• Prereqs: SAMPLES-13-004 (Wave 0)
• Current: DONE (2025-10-23)
- **Sprint 12** · Runtime Guardrails
- Team: Scanner WebService Guild
- Path: `src/StellaOps.Scanner.WebService/TASKS.md`
@@ -854,11 +865,11 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- Path: `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`
1. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-305C — Handle self-contained apps and native assets; merge with EntryTrace usage hints.
• Prereqs: SCANNER-ANALYZERS-LANG-10-305A (Wave 1)
• Current: TODO
• Current: DONE — Self-contained fixtures emit components with RID flags; EntryTrace usage hints preserved.
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-304C — Fallback heuristics for stripped binaries with deterministic `bin:{sha256}` labeling and quiet provenance.
1. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-304C — Fallback heuristics for stripped binaries with deterministic `bin:{sha256}` labeling and quiet provenance.
• Prereqs: SCANNER-ANALYZERS-LANG-10-304B (Wave 2)
• Current: TODO
• Current: DONE — `bin:{sha256}` fallback + quiet provenance docs shipped with determinism fixtures.
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Node/TASKS.md`
1. [DONE 2025-10-21] SCANNER-ANALYZERS-LANG-10-309N — Package Node analyzer as restart-time plug-in (manifest, DI registration, Offline Kit notes).
• Prereqs: SCANNER-ANALYZERS-LANG-10-308N (Wave 2)
@@ -868,9 +879,9 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
• Prereqs: SCANNER-ANALYZERS-LANG-10-303B (Wave 2)
• Current: DONE — `direct_url.json` editable insights surfaced; EntryTrace usage hints mark console scripts; deterministic fixture covers editable vs wheel installs.
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-306C — Integrate binary hash fallback (`bin:{sha256}`) and tie into shared quiet provenance helpers.
1. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-306C — Integrate binary hash fallback (`bin:{sha256}`) and tie into shared quiet provenance helpers.
• Prereqs: SCANNER-ANALYZERS-LANG-10-306B (Wave 2)
• Current: TODO
• Current: DONE — Hash fallback wired through shared helpers; fixtures ensure deterministic output.
- **Sprint 12** · Runtime Guardrails
- Team: Zastava Observer Guild
- Path: `src/StellaOps.Zastava.Observer/TASKS.md`
@@ -931,9 +942,9 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
• Prereqs: SCANNER-ANALYZERS-LANG-10-305C (Wave 3)
• Current: DONE 2025-10-22
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-307G — Wire shared helpers (license mapping, usage flags) and ensure concurrency-safe buffer reuse.
1. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-307G — Wire shared helpers (license mapping, usage flags) and ensure concurrency-safe buffer reuse.
• Prereqs: SCANNER-ANALYZERS-LANG-10-304C (Wave 3)
• Current: TODO
• Current: DONE — Shared helpers integrated; concurrency tests verify buffer reuse.
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-307P — Shared helper integration (license metadata, quiet provenance, component merging).
• Prereqs: SCANNER-ANALYZERS-LANG-10-303C (Wave 3)
@@ -1003,13 +1014,13 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
• Prereqs: SCANNER-ANALYZERS-LANG-10-307D (Wave 4)
• Current: DONE — fixtures + benchmarks merged 2025-10-23
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-308G — Determinism fixtures + benchmark harness (Vs competitor).
1. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-308G — Determinism fixtures + benchmark harness (Vs competitor).
• Prereqs: SCANNER-ANALYZERS-LANG-10-307G (Wave 4)
• Current: TODO
• Current: DONE — Fixtures and benchmark harness merged; perf delta captured vs competitor.
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-308P — Golden fixtures + determinism harness for Python analyzer; add benchmark and hash throughput reporting.
1. [DONE 2025-10-23] SCANNER-ANALYZERS-LANG-10-308P — Golden fixtures + determinism harness for Python analyzer; add benchmark and hash throughput reporting.
• Prereqs: SCANNER-ANALYZERS-LANG-10-307P (Wave 4)
• Current: TODO
• Current: DONE — Fixtures `simple-venv`, `pip-cache`, `layered-editable` + hash throughput benchmarks merged 2025-10-23.
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-308R — Determinism fixtures + performance benchmarks; compare against competitor heuristic coverage.
• Prereqs: SCANNER-ANALYZERS-LANG-10-307R (Wave 4)
@@ -1041,13 +1052,13 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
• Prereqs: SCANNER-ANALYZERS-LANG-10-308D (Wave 5)
• Current: DONE — manifest + Offline Kit docs updated 2025-10-23
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-309G — Package plug-in manifest + Offline Kit notes; ensure Worker DI registration.
1. [DONE 2025-10-22] SCANNER-ANALYZERS-LANG-10-309G — Package plug-in manifest + Offline Kit notes; ensure Worker DI registration.
• Prereqs: SCANNER-ANALYZERS-LANG-10-308G (Wave 5)
• Current: TODO
• Current: DONE — Manifest copied, Worker DI registration verified, Offline Kit docs updated.
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-309P — Package plug-in (manifest, DI registration) and document Offline Kit bundling of Python stdlib metadata if needed.
1. [DONE 2025-10-23] SCANNER-ANALYZERS-LANG-10-309P — Package plug-in (manifest, DI registration) and document Offline Kit bundling of Python stdlib metadata if needed.
• Prereqs: SCANNER-ANALYZERS-LANG-10-308P (Wave 5)
• Current: TODO
• Current: DONE — Manifest copied, Worker integration verified, Offline Kit docs updated with Python plug-in guidance.
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-309R — Package plug-in manifest + Offline Kit documentation; ensure Worker integration.
• Prereqs: SCANNER-ANALYZERS-LANG-10-308R (Wave 5)

View File

@@ -2,34 +2,10 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
| Sprint | Theme | Tasks File Path | Status | Type of Specialist | Task ID | Task Description |
| --- | --- | --- | --- | --- | --- | --- |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-101 | Implement layer cache store keyed by layer digest with metadata retention per architecture §3.3. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-102 | Build file CAS with dedupe, TTL enforcement, and offline import/export hooks. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-103 | Expose cache metrics/logging and configuration toggles for warm/cold thresholds. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Cache/TASKS.md | TODO | Scanner Cache Guild | SCANNER-CACHE-10-104 | Implement cache invalidation workflows (layer delete, TTL expiry, diff invalidation). |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-201 | Alpine/apk analyzer emitting deterministic components with provenance. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-202 | Debian/dpkg analyzer mapping packages to purl identity with evidence. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-203 | RPM analyzer capturing EVR, file listings, provenance. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-204 | Shared OS evidence helpers for package identity + provenance. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-205 | Vendor metadata enrichment (source packages, license, CVE hints). |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-206 | Determinism harness + fixtures for OS analyzers. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.OS/TASKS.md | TODO | OS Analyzer Guild | SCANNER-ANALYZERS-OS-10-207 | Package OS analyzers as restart-time plug-ins (manifest + host registration). |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | TODO | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-301 | Java analyzer emitting `pkg:maven` with provenance. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-401 | POSIX shell AST parser with deterministic output. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-402 | Command resolution across layered rootfs with evidence attribution. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-403 | Interpreter tracing for shell wrappers to Python/Node/Java launchers. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-404 | Python entry analyzer (venv shebang, module invocation, usage flag). |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-405 | Node/Java launcher analyzer capturing script/jar targets. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-406 | Explainability + diagnostics for unresolved constructs with metrics. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | TODO | EntryTrace Guild | SCANNER-ENTRYTRACE-10-407 | Package EntryTrace analyzers as restart-time plug-ins (manifest + host registration). |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Diff/TASKS.md | TODO | Diff Guild | SCANNER-DIFF-10-501 | Build component differ tracking add/remove/version changes with deterministic ordering. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Diff/TASKS.md | TODO | Diff Guild | SCANNER-DIFF-10-502 | Attribute diffs to introducing/removing layers including provenance evidence. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Diff/TASKS.md | TODO | Diff Guild | SCANNER-DIFF-10-503 | Produce JSON diff output for inventory vs usage views aligned with API contract. |
| Sprint 10 | Samples | samples/TASKS.md | TODO | Samples Guild, Scanner Team | SAMPLES-10-001 | Sample images with SBOM/BOM-Index sidecars. |
| Sprint 10 | DevOps Perf | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-PERF-10-001 | Perf smoke job ensuring <5s SBOM compose. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Authority/TASKS.md | DOING (2025-10-19) | 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.Attestor/TASKS.md | TODO | Attestor Guild | ATTESTOR-API-11-201 | `/rekor/entries` submission pipeline with dedupe, proof acquisition, and persistence. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Attestor/TASKS.md | TODO | Attestor Guild | ATTESTOR-VERIFY-11-202 | `/rekor/verify` + retrieval endpoints validating signatures and Merkle proofs. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Attestor/TASKS.md | TODO | Attestor Guild | ATTESTOR-OBS-11-203 | Telemetry, alerting, mTLS hardening, and archive workflow for Attestor. |
| Sprint 11 | Storage Platform Hardening | src/StellaOps.Scanner.Storage/TASKS.md | DONE (2025-10-23) | Scanner Storage Guild | SCANNER-STORAGE-11-401 | Migrate scanner object storage integration from MinIO to RustFS with data migration plan. |
| Sprint 11 | UI Integration | src/StellaOps.UI/TASKS.md | TODO | UI Guild | UI-ATTEST-11-005 | Attestation visibility (Rekor id, status) on Scan Detail. |
| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Core/TASKS.md | TODO | Zastava Core Guild | ZASTAVA-CORE-12-201 | Define runtime event/admission DTOs, hashing helpers, and versioning strategy. |
| Sprint 12 | Runtime Guardrails | src/StellaOps.Zastava.Core/TASKS.md | TODO | Zastava Core Guild | ZASTAVA-CORE-12-202 | Provide configuration/logging/metrics utilities shared by Observer/Webhook. |
@@ -57,6 +33,7 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
| Sprint 13 | Platform Reliability | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-NUGET-13-002 | Ensure all solutions/projects prioritize `local-nuget` before public feeds and add restore-order validation. |
| Sprint 13 | Platform Reliability | ops/devops/TASKS.md | TODO | DevOps Guild, Platform Leads | DEVOPS-NUGET-13-003 | Upgrade `Microsoft.*` dependencies pinned to 8.* to their latest .NET 10 (or 9.x) releases and refresh guidance. |
| 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/devops/TASKS.md | TODO | DevOps Guild, Scanner Guild | DEVOPS-REL-14-004 | Extend release/offline smoke jobs to cover Python analyzer plug-ins (warm/cold, determinism, signing). |
| Sprint 14 | Release & Offline Ops | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild | DEVOPS-OFFLINE-14-002 | Offline kit packaging workflow with integrity verification and documentation. |
| Sprint 14 | Release & Offline Ops | ops/deployment/TASKS.md | TODO | Deployment Guild | DEVOPS-OPS-14-003 | Deployment/update/rollback automation and channel management documentation. |
| Sprint 14 | Release & Offline Ops | ops/licensing/TASKS.md | TODO | Licensing Guild | DEVOPS-LIC-14-004 | Registry token service tied to Authority, plan gating, revocation handling, monitoring. |
@@ -112,4 +89,5 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
| Sprint 17 | Symbol Intelligence & Forensics | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-REL-17-002 | Ship stripped debug artifacts organised by build-id within release/offline kits. |
| Sprint 17 | Symbol Intelligence & Forensics | docs/TASKS.md | TODO | Docs Guild | DOCS-RUNTIME-17-004 | Document build-id workflows for SBOMs, runtime events, and debug-store usage. |
| Sprint 18 | Launch Readiness | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-LAUNCH-18-001 | Production launch cutover rehearsal and runbook publication (blocked on implementation sign-off and environment setup). |
| Sprint 18 | Launch Readiness | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild, UX Specialist | DEVOPS-OFFLINE-18-003 | Capture Angular workspace npm cache + Chromium bundle for Offline Kit distribution and document refresh cadence. |
| Sprint 18 | Launch Readiness | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild, UX Specialist | DEVOPS-OFFLINE-18-003 | Capture Angular workspace npm cache + Chromium bundle for Offline Kit distribution and document refresh cadence. |
| Sprint 18 | Launch Readiness | ops/offline-kit/TASKS.md | TODO | Offline Kit Guild, Scanner Guild | DEVOPS-OFFLINE-18-005 | Rebuild Offline Kit with Python analyzer artefacts and refreshed manifest/signature pair. |

View File

@@ -49,9 +49,37 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | DONE (2025-10-22) | 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 | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-606 | Usage view bit flags integrated with EntryTrace. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | DONE (2025-10-22) | 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.Cache/TASKS.md | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-301 | Java analyzer emitting `pkg:maven` with provenance. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | DONE (2025-10-19) | EntryTrace Guild | SCANNER-ENTRYTRACE-10-401 | POSIX shell AST parser with deterministic output. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | EntryTrace Guild | SCANNER-ENTRYTRACE-10-406 | Explainability + diagnostics for unresolved constructs with metrics. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.EntryTrace/TASKS.md | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | 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 | DONE (2025-10-19) | Diff Guild | SCANNER-DIFF-10-503 | Produce JSON diff output for inventory vs usage views aligned with API contract. |
| Sprint 10 | Samples | samples/TASKS.md | DONE (2025-10-20) | Samples Guild, Scanner Team | SAMPLES-10-001 | Sample images with SBOM/BOM-Index sidecars. |
| Sprint 10 | DevOps Perf | ops/devops/TASKS.md | DONE (2025-10-22) | DevOps Guild | DEVOPS-PERF-10-001 | Perf smoke job ensuring <5s SBOM compose. |
| Sprint 10 | DevOps Perf | ops/devops/TASKS.md | DONE (2025-10-23) | DevOps Guild | DEVOPS-PERF-10-002 | Publish analyzer bench metrics to Grafana/perf workbook and alarm on 20% regressions. |
| Sprint 10 | Policy Samples | samples/TASKS.md | DONE (2025-10-23) | Samples Guild, Policy Guild | SAMPLES-13-004 | Add policy preview/report fixtures showing confidence bands and unknown-age tags. |
| Sprint 10 | Policy Samples | src/StellaOps.Web/TASKS.md | DONE (2025-10-23) | UI Guild | WEB-POLICY-FIXTURES-10-001 | Wire policy preview/report doc fixtures into UI harness (test utility or Storybook substitute) with type bindings and validation guard so UI stays aligned with documented payloads. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Signer/TASKS.md | DONE (2025-10-21) | 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 | DONE (2025-10-21) | 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 | DONE (2025-10-21) | 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.Authority/TASKS.md | DONE (2025-10-23) | Authority Core & Security Guild | AUTH-MTLS-11-002 | Add OAuth mTLS client credential support with certificate-bound tokens and introspection updates. |
| Sprint 12 | Runtime Guardrails | src/StellaOps.Scanner.WebService/TASKS.md | DONE (2025-10-20) | Scanner WebService Guild | SCANNER-RUNTIME-12-301 | `/runtime/events` ingestion endpoint with validation, batching, storage hooks. |
| Sprint 13 | UX & CLI Experience | src/StellaOps.Cli/TASKS.md | DONE (2025-10-21) | 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 | DONE (2025-10-22) | DevEx/CLI | CLI-PLUGIN-13-007 | Package non-core CLI verbs as restart-time plug-ins (manifest + loader tests). |

View File

@@ -19,7 +19,10 @@ dotnet run \
--project bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj \
-- \
--repo-root . \
--out bench/Scanner.Analyzers/baseline.csv
--out bench/Scanner.Analyzers/baseline.csv \
--json out/bench/scanner-analyzers/latest.json \
--prom out/bench/scanner-analyzers/latest.prom \
--commit "$(git rev-parse HEAD)"
```
The harness prints a table to stdout and writes the CSV (if `--out` is specified) with the following headers:
@@ -28,7 +31,16 @@ The harness prints a table to stdout and writes the CSV (if `--out` is specified
scenario,iterations,sample_count,mean_ms,p95_ms,max_ms
```
Use `--iterations` to override the default (5 passes per scenario) and `--threshold-ms` to customize the failure budget. Budgets default to 5000ms (or per-scenario overrides in `config.json`), aligned with the SBOM compose objective.
Additional outputs:
- `--json` emits a deterministic report consumable by Grafana/automation (schema `1.0`, see `docs/12_PERFORMANCE_WORKBOOK.md`).
- `--prom` exports Prometheus-compatible gauges (`scanner_analyzer_bench_*`), which CI uploads for dashboards and alerts.
Use `--iterations` to override the default (5 passes per scenario) and `--threshold-ms` to customize the failure budget. Budgets default to 5000ms (or per-scenario overrides in `config.json`), aligned with the SBOM compose objective. Provide `--baseline path/to/baseline.csv` (defaults to the repo baseline) to compare against historical numbers—regressions ≥20% on the `max_ms` metric or breaches of the configured threshold will fail the run.
Metadata options:
- `--captured-at 2025-10-23T12:00:00Z` to inject a deterministic timestamp (otherwise `UtcNow`).
- `--commit` and `--environment` annotate the JSON report for dashboards.
- `--regression-limit 1.15` adjusts the ratio guard (default 1.20 ⇒ +20%).
## Adding scenarios
1. Drop the fixture tree under `samples/<area>/...`.
@@ -38,5 +50,5 @@ Use `--iterations` to override the default (5 passes per scenario) and `--thresh
- `root` path to the directory that will be scanned.
- For analyzer-backed scenarios, set `analyzers` to the list of language analyzer ids (for example, `["node"]`).
- For temporary metadata walks (used until the analyzer ships), provide `parser` (`node` or `python`) and the `matcher` glob describing files to parse.
3. Re-run the harness (`dotnet run … --out baseline.csv`).
3. Re-run the harness (`dotnet run … --out baseline.csv --json out/.../new.json --prom out/.../new.prom`).
4. Commit both the fixture and updated baseline.

View File

@@ -0,0 +1,37 @@
using System.Text;
using StellaOps.Bench.ScannerAnalyzers.Baseline;
using Xunit;
namespace StellaOps.Bench.ScannerAnalyzers.Tests;
public sealed class BaselineLoaderTests
{
[Fact]
public async Task LoadAsync_ReadsCsvIntoDictionary()
{
var csv = """
scenario,iterations,sample_count,mean_ms,p95_ms,max_ms
node_monorepo_walk,5,4,9.4303,36.1354,45.0012
python_site_packages_walk,5,10,12.1000,18.2000,26.3000
""";
var path = await WriteTempFileAsync(csv);
var result = await BaselineLoader.LoadAsync(path, CancellationToken.None);
Assert.Equal(2, result.Count);
var entry = Assert.Contains("node_monorepo_walk", result);
Assert.Equal(5, entry.Iterations);
Assert.Equal(4, entry.SampleCount);
Assert.Equal(9.4303, entry.MeanMs, 4);
Assert.Equal(36.1354, entry.P95Ms, 4);
Assert.Equal(45.0012, entry.MaxMs, 4);
}
private static async Task<string> WriteTempFileAsync(string content)
{
var path = Path.Combine(Path.GetTempPath(), $"baseline-{Guid.NewGuid():N}.csv");
await File.WriteAllTextAsync(path, content, Encoding.UTF8);
return path;
}
}

View File

@@ -0,0 +1,41 @@
using System.Text.Json;
using StellaOps.Bench.ScannerAnalyzers;
using StellaOps.Bench.ScannerAnalyzers.Baseline;
using StellaOps.Bench.ScannerAnalyzers.Reporting;
using Xunit;
namespace StellaOps.Bench.ScannerAnalyzers.Tests;
public sealed class BenchmarkJsonWriterTests
{
[Fact]
public async Task WriteAsync_EmitsMetadataAndScenarioDetails()
{
var metadata = new BenchmarkJsonMetadata("1.0", DateTimeOffset.Parse("2025-10-23T12:00:00Z"), "abc123", "ci");
var result = new ScenarioResult(
"scenario",
"Scenario",
SampleCount: 5,
MeanMs: 10,
P95Ms: 12,
MaxMs: 20,
Iterations: 5,
ThresholdMs: 5000);
var baseline = new BaselineEntry("scenario", 5, 5, 9, 11, 10);
var report = new BenchmarkScenarioReport(result, baseline, 1.2);
var path = Path.Combine(Path.GetTempPath(), $"bench-{Guid.NewGuid():N}.json");
await BenchmarkJsonWriter.WriteAsync(path, metadata, new[] { report }, CancellationToken.None);
using var document = JsonDocument.Parse(await File.ReadAllTextAsync(path));
var root = document.RootElement;
Assert.Equal("1.0", root.GetProperty("schemaVersion").GetString());
Assert.Equal("abc123", root.GetProperty("commit").GetString());
var scenario = root.GetProperty("scenarios")[0];
Assert.Equal("scenario", scenario.GetProperty("id").GetString());
Assert.Equal(20, scenario.GetProperty("maxMs").GetDouble());
Assert.Equal(10, scenario.GetProperty("baseline").GetProperty("maxMs").GetDouble());
Assert.True(scenario.GetProperty("regression").GetProperty("breached").GetBoolean());
}
}

View File

@@ -0,0 +1,58 @@
using StellaOps.Bench.ScannerAnalyzers;
using StellaOps.Bench.ScannerAnalyzers.Baseline;
using StellaOps.Bench.ScannerAnalyzers.Reporting;
using Xunit;
namespace StellaOps.Bench.ScannerAnalyzers.Tests;
public sealed class BenchmarkScenarioReportTests
{
[Fact]
public void RegressionRatio_ComputedWhenBaselinePresent()
{
var result = new ScenarioResult(
"scenario",
"Scenario",
SampleCount: 5,
MeanMs: 10,
P95Ms: 12,
MaxMs: 20,
Iterations: 5,
ThresholdMs: 5000);
var baseline = new BaselineEntry(
"scenario",
Iterations: 5,
SampleCount: 5,
MeanMs: 8,
P95Ms: 11,
MaxMs: 15);
var report = new BenchmarkScenarioReport(result, baseline, regressionLimit: 1.2);
Assert.True(report.MaxRegressionRatio.HasValue);
Assert.Equal(20d / 15d, report.MaxRegressionRatio.Value, 6);
Assert.True(report.RegressionBreached);
Assert.Contains("+33.3%", report.BuildRegressionFailureMessage());
}
[Fact]
public void RegressionRatio_NullWhenBaselineMissing()
{
var result = new ScenarioResult(
"scenario",
"Scenario",
SampleCount: 5,
MeanMs: 10,
P95Ms: 12,
MaxMs: 20,
Iterations: 5,
ThresholdMs: 5000);
var report = new BenchmarkScenarioReport(result, baseline: null, regressionLimit: 1.2);
Assert.Null(report.MaxRegressionRatio);
Assert.False(report.RegressionBreached);
Assert.Null(report.BuildRegressionFailureMessage());
}
}

View File

@@ -0,0 +1,32 @@
using StellaOps.Bench.ScannerAnalyzers;
using StellaOps.Bench.ScannerAnalyzers.Baseline;
using StellaOps.Bench.ScannerAnalyzers.Reporting;
using Xunit;
namespace StellaOps.Bench.ScannerAnalyzers.Tests;
public sealed class PrometheusWriterTests
{
[Fact]
public void Write_EmitsMetricsForScenario()
{
var result = new ScenarioResult(
"scenario_a",
"Scenario A",
SampleCount: 5,
MeanMs: 10,
P95Ms: 12,
MaxMs: 20,
Iterations: 5,
ThresholdMs: 5000);
var baseline = new BaselineEntry("scenario_a", 5, 5, 9, 11, 18);
var report = new BenchmarkScenarioReport(result, baseline, 1.2);
var path = Path.Combine(Path.GetTempPath(), $"metrics-{Guid.NewGuid():N}.prom");
PrometheusWriter.Write(path, new[] { report });
var contents = File.ReadAllText(path);
Assert.Contains("scanner_analyzer_bench_max_ms{scenario=\"scenario_a\"} 20", contents);
Assert.Contains("scanner_analyzer_bench_regression_ratio{scenario=\"scenario_a\"}", contents);
}
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Bench.ScannerAnalyzers\StellaOps.Bench.ScannerAnalyzers.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
namespace StellaOps.Bench.ScannerAnalyzers.Baseline;
internal sealed record BaselineEntry(
string ScenarioId,
int Iterations,
int SampleCount,
double MeanMs,
double P95Ms,
double MaxMs);

View File

@@ -0,0 +1,88 @@
using System.Globalization;
namespace StellaOps.Bench.ScannerAnalyzers.Baseline;
internal static class BaselineLoader
{
public static async Task<IReadOnlyDictionary<string, BaselineEntry>> LoadAsync(string path, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException("Baseline path must be provided.", nameof(path));
}
var resolved = Path.GetFullPath(path);
if (!File.Exists(resolved))
{
throw new FileNotFoundException($"Baseline file not found at {resolved}", resolved);
}
var result = new Dictionary<string, BaselineEntry>(StringComparer.OrdinalIgnoreCase);
await using var stream = new FileStream(resolved, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream);
string? line;
var isFirst = true;
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) is not null)
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
if (isFirst)
{
isFirst = false;
if (line.StartsWith("scenario,", StringComparison.OrdinalIgnoreCase))
{
continue;
}
}
var entry = ParseLine(line);
result[entry.ScenarioId] = entry;
}
return result;
}
private static BaselineEntry ParseLine(string line)
{
var parts = line.Split(',', StringSplitOptions.TrimEntries);
if (parts.Length < 6)
{
throw new InvalidDataException($"Baseline CSV row malformed: '{line}'");
}
var scenarioId = parts[0];
var iterations = ParseInt(parts[1], nameof(BaselineEntry.Iterations));
var sampleCount = ParseInt(parts[2], nameof(BaselineEntry.SampleCount));
var meanMs = ParseDouble(parts[3], nameof(BaselineEntry.MeanMs));
var p95Ms = ParseDouble(parts[4], nameof(BaselineEntry.P95Ms));
var maxMs = ParseDouble(parts[5], nameof(BaselineEntry.MaxMs));
return new BaselineEntry(scenarioId, iterations, sampleCount, meanMs, p95Ms, maxMs);
}
private static int ParseInt(string value, string field)
{
if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
{
throw new InvalidDataException($"Failed to parse integer {field} from '{value}'.");
}
return parsed;
}
private static double ParseDouble(string value, string field)
{
if (!double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
{
throw new InvalidDataException($"Failed to parse double {field} from '{value}'.");
}
return parsed;
}
}

View File

@@ -1,4 +1,6 @@
using System.Globalization;
using StellaOps.Bench.ScannerAnalyzers.Baseline;
using StellaOps.Bench.ScannerAnalyzers.Reporting;
using StellaOps.Bench.ScannerAnalyzers.Scenarios;
namespace StellaOps.Bench.ScannerAnalyzers;
@@ -15,8 +17,13 @@ internal static class Program
var iterations = options.Iterations ?? config.Iterations ?? 5;
var thresholdMs = options.ThresholdMs ?? config.ThresholdMs ?? 5000;
var repoRoot = ResolveRepoRoot(options.RepoRoot, options.ConfigPath);
var regressionLimit = options.RegressionLimit ?? 1.2d;
var capturedAt = (options.CapturedAtUtc ?? DateTimeOffset.UtcNow).ToUniversalTime();
var baseline = await LoadBaselineDictionaryAsync(options.BaselinePath, CancellationToken.None).ConfigureAwait(false);
var results = new List<ScenarioResult>();
var reports = new List<BenchmarkScenarioReport>();
var failures = new List<string>();
foreach (var scenario in config.Scenarios)
@@ -28,26 +35,54 @@ internal static class Program
var stats = ScenarioStatistics.FromDurations(execution.Durations);
var scenarioThreshold = scenario.ThresholdMs ?? thresholdMs;
results.Add(new ScenarioResult(
var result = new ScenarioResult(
scenario.Id!,
scenario.Label ?? scenario.Id!,
execution.SampleCount,
stats.MeanMs,
stats.P95Ms,
stats.MaxMs,
iterations));
iterations,
scenarioThreshold);
results.Add(result);
if (stats.MaxMs > scenarioThreshold)
{
failures.Add($"{scenario.Id} exceeded threshold: {stats.MaxMs:F2} ms > {scenarioThreshold:F2} ms");
}
baseline.TryGetValue(result.Id, out var baselineEntry);
var report = new BenchmarkScenarioReport(result, baselineEntry, regressionLimit);
if (report.BuildRegressionFailureMessage() is { } regressionFailure)
{
failures.Add(regressionFailure);
}
reports.Add(report);
}
TablePrinter.Print(results);
if (!string.IsNullOrWhiteSpace(options.OutPath))
if (!string.IsNullOrWhiteSpace(options.CsvOutPath))
{
CsvWriter.Write(options.OutPath!, results);
CsvWriter.Write(options.CsvOutPath!, results);
}
if (!string.IsNullOrWhiteSpace(options.JsonOutPath))
{
var metadata = new BenchmarkJsonMetadata(
"1.0",
capturedAt,
options.Commit,
options.Environment);
await BenchmarkJsonWriter.WriteAsync(options.JsonOutPath!, metadata, reports, CancellationToken.None).ConfigureAwait(false);
}
if (!string.IsNullOrWhiteSpace(options.PrometheusOutPath))
{
PrometheusWriter.Write(options.PrometheusOutPath!, reports);
}
if (failures.Count > 0)
@@ -71,6 +106,22 @@ internal static class Program
}
}
private static async Task<IReadOnlyDictionary<string, BaselineEntry>> LoadBaselineDictionaryAsync(string? baselinePath, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(baselinePath))
{
return new Dictionary<string, BaselineEntry>(StringComparer.OrdinalIgnoreCase);
}
var resolved = Path.GetFullPath(baselinePath);
if (!File.Exists(resolved))
{
return new Dictionary<string, BaselineEntry>(StringComparer.OrdinalIgnoreCase);
}
return await BaselineLoader.LoadAsync(resolved, cancellationToken).ConfigureAwait(false);
}
private static string ResolveRepoRoot(string? overridePath, string configPath)
{
if (!string.IsNullOrWhiteSpace(overridePath))
@@ -108,15 +159,34 @@ internal static class Program
return combined;
}
private sealed record ProgramOptions(string ConfigPath, int? Iterations, double? ThresholdMs, string? OutPath, string? RepoRoot)
private sealed record ProgramOptions(
string ConfigPath,
int? Iterations,
double? ThresholdMs,
string? CsvOutPath,
string? JsonOutPath,
string? PrometheusOutPath,
string? RepoRoot,
string? BaselinePath,
DateTimeOffset? CapturedAtUtc,
string? Commit,
string? Environment,
double? RegressionLimit)
{
public static ProgramOptions Parse(string[] args)
{
var configPath = DefaultConfigPath();
var baselinePath = DefaultBaselinePath();
int? iterations = null;
double? thresholdMs = null;
string? outPath = null;
string? csvOut = null;
string? jsonOut = null;
string? promOut = null;
string? repoRoot = null;
DateTimeOffset? capturedAt = null;
string? commit = null;
string? environment = null;
double? regressionLimit = null;
for (var index = 0; index < args.Length; index++)
{
@@ -136,20 +206,50 @@ internal static class Program
thresholdMs = double.Parse(args[++index], CultureInfo.InvariantCulture);
break;
case "--out":
case "--csv":
EnsureNext(args, index);
outPath = args[++index];
csvOut = args[++index];
break;
case "--json":
EnsureNext(args, index);
jsonOut = args[++index];
break;
case "--prom":
case "--prometheus":
EnsureNext(args, index);
promOut = args[++index];
break;
case "--baseline":
EnsureNext(args, index);
baselinePath = args[++index];
break;
case "--repo-root":
case "--samples":
EnsureNext(args, index);
repoRoot = args[++index];
break;
case "--captured-at":
EnsureNext(args, index);
capturedAt = DateTimeOffset.Parse(args[++index], CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
break;
case "--commit":
EnsureNext(args, index);
commit = args[++index];
break;
case "--environment":
EnsureNext(args, index);
environment = args[++index];
break;
case "--regression-limit":
EnsureNext(args, index);
regressionLimit = double.Parse(args[++index], CultureInfo.InvariantCulture);
break;
default:
throw new ArgumentException($"Unknown argument: {current}", nameof(args));
}
}
return new ProgramOptions(configPath, iterations, thresholdMs, outPath, repoRoot);
return new ProgramOptions(configPath, iterations, thresholdMs, csvOut, jsonOut, promOut, repoRoot, baselinePath, capturedAt, commit, environment, regressionLimit);
}
private static string DefaultConfigPath()
@@ -160,6 +260,15 @@ internal static class Program
return Path.Combine(configDirectory, "config.json");
}
private static string? DefaultBaselinePath()
{
var binaryDir = AppContext.BaseDirectory;
var projectRoot = Path.GetFullPath(Path.Combine(binaryDir, "..", "..", ".."));
var benchRoot = Path.GetFullPath(Path.Combine(projectRoot, ".."));
var baselinePath = Path.Combine(benchRoot, "baseline.csv");
return File.Exists(baselinePath) ? baselinePath : baselinePath;
}
private static void EnsureNext(string[] args, int index)
{
if (index + 1 >= args.Length)
@@ -169,15 +278,6 @@ internal static class Program
}
}
private sealed record ScenarioResult(
string Id,
string Label,
int SampleCount,
double MeanMs,
double P95Ms,
double MaxMs,
int Iterations);
private sealed record ScenarioStatistics(double MeanMs, double P95Ms, double MaxMs)
{
public static ScenarioStatistics FromDurations(IReadOnlyList<double> durations)
@@ -232,25 +332,16 @@ internal static class Program
Console.WriteLine("---------------------------- | ----- | --------- | --------- | ----------");
foreach (var row in results)
{
Console.WriteLine(FormatRow(row));
Console.WriteLine(string.Join(" | ", new[]
{
row.IdColumn,
row.SampleCountColumn,
row.MeanColumn,
row.P95Column,
row.MaxColumn
}));
}
}
private static string FormatRow(ScenarioResult row)
{
var idColumn = row.Id.Length <= 28
? row.Id.PadRight(28)
: row.Id[..28];
return string.Join(" | ", new[]
{
idColumn,
row.SampleCount.ToString(CultureInfo.InvariantCulture).PadLeft(5),
row.MeanMs.ToString("F2", CultureInfo.InvariantCulture).PadLeft(9),
row.P95Ms.ToString("F2", CultureInfo.InvariantCulture).PadLeft(9),
row.MaxMs.ToString("F2", CultureInfo.InvariantCulture).PadLeft(10),
});
}
}
private static class CsvWriter

View File

@@ -0,0 +1,108 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Bench.ScannerAnalyzers.Baseline;
namespace StellaOps.Bench.ScannerAnalyzers.Reporting;
internal static class BenchmarkJsonWriter
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
public static async Task WriteAsync(
string path,
BenchmarkJsonMetadata metadata,
IReadOnlyList<BenchmarkScenarioReport> reports,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);
ArgumentNullException.ThrowIfNull(metadata);
ArgumentNullException.ThrowIfNull(reports);
var resolved = Path.GetFullPath(path);
var directory = Path.GetDirectoryName(resolved);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var document = new BenchmarkJsonDocument(
metadata.SchemaVersion,
metadata.CapturedAtUtc,
metadata.Commit,
metadata.Environment,
reports.Select(CreateScenario).ToArray());
await using var stream = new FileStream(resolved, FileMode.Create, FileAccess.Write, FileShare.None);
await JsonSerializer.SerializeAsync(stream, document, SerializerOptions, cancellationToken).ConfigureAwait(false);
await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
private static BenchmarkJsonScenario CreateScenario(BenchmarkScenarioReport report)
{
var baseline = report.Baseline;
return new BenchmarkJsonScenario(
report.Result.Id,
report.Result.Label,
report.Result.Iterations,
report.Result.SampleCount,
report.Result.MeanMs,
report.Result.P95Ms,
report.Result.MaxMs,
report.Result.ThresholdMs,
baseline is null
? null
: new BenchmarkJsonScenarioBaseline(
baseline.Iterations,
baseline.SampleCount,
baseline.MeanMs,
baseline.P95Ms,
baseline.MaxMs),
new BenchmarkJsonScenarioRegression(
report.MaxRegressionRatio,
report.MeanRegressionRatio,
report.RegressionLimit,
report.RegressionBreached));
}
private sealed record BenchmarkJsonDocument(
string SchemaVersion,
DateTimeOffset CapturedAt,
string? Commit,
string? Environment,
IReadOnlyList<BenchmarkJsonScenario> Scenarios);
private sealed record BenchmarkJsonScenario(
string Id,
string Label,
int Iterations,
int SampleCount,
double MeanMs,
double P95Ms,
double MaxMs,
double ThresholdMs,
BenchmarkJsonScenarioBaseline? Baseline,
BenchmarkJsonScenarioRegression Regression);
private sealed record BenchmarkJsonScenarioBaseline(
int Iterations,
int SampleCount,
double MeanMs,
double P95Ms,
double MaxMs);
private sealed record BenchmarkJsonScenarioRegression(
double? MaxRatio,
double? MeanRatio,
double Limit,
bool Breached);
}
internal sealed record BenchmarkJsonMetadata(
string SchemaVersion,
DateTimeOffset CapturedAtUtc,
string? Commit,
string? Environment);

View File

@@ -0,0 +1,55 @@
using StellaOps.Bench.ScannerAnalyzers.Baseline;
namespace StellaOps.Bench.ScannerAnalyzers.Reporting;
internal sealed class BenchmarkScenarioReport
{
private const double RegressionLimitDefault = 1.2d;
public BenchmarkScenarioReport(ScenarioResult result, BaselineEntry? baseline, double? regressionLimit = null)
{
Result = result ?? throw new ArgumentNullException(nameof(result));
Baseline = baseline;
RegressionLimit = regressionLimit is { } limit && limit > 0 ? limit : RegressionLimitDefault;
MaxRegressionRatio = CalculateRatio(result.MaxMs, baseline?.MaxMs);
MeanRegressionRatio = CalculateRatio(result.MeanMs, baseline?.MeanMs);
}
public ScenarioResult Result { get; }
public BaselineEntry? Baseline { get; }
public double RegressionLimit { get; }
public double? MaxRegressionRatio { get; }
public double? MeanRegressionRatio { get; }
public bool RegressionBreached => MaxRegressionRatio.HasValue && MaxRegressionRatio.Value >= RegressionLimit;
public string? BuildRegressionFailureMessage()
{
if (!RegressionBreached || MaxRegressionRatio is null)
{
return null;
}
var percentage = (MaxRegressionRatio.Value - 1d) * 100d;
return $"{Result.Id} exceeded regression budget: max {Result.MaxMs:F2} ms vs baseline {Baseline!.MaxMs:F2} ms (+{percentage:F1}%)";
}
private static double? CalculateRatio(double current, double? baseline)
{
if (!baseline.HasValue)
{
return null;
}
if (baseline.Value <= 0d)
{
return null;
}
return current / baseline.Value;
}
}

View File

@@ -0,0 +1,59 @@
using System.Globalization;
using System.Text;
namespace StellaOps.Bench.ScannerAnalyzers.Reporting;
internal static class PrometheusWriter
{
public static void Write(string path, IReadOnlyList<BenchmarkScenarioReport> reports)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);
ArgumentNullException.ThrowIfNull(reports);
var resolved = Path.GetFullPath(path);
var directory = Path.GetDirectoryName(resolved);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var builder = new StringBuilder();
builder.AppendLine("# HELP scanner_analyzer_bench_duration_ms Analyzer benchmark duration metrics in milliseconds.");
builder.AppendLine("# TYPE scanner_analyzer_bench_duration_ms gauge");
foreach (var report in reports)
{
var scenarioLabel = Escape(report.Result.Id);
AppendMetric(builder, "scanner_analyzer_bench_mean_ms", scenarioLabel, report.Result.MeanMs);
AppendMetric(builder, "scanner_analyzer_bench_p95_ms", scenarioLabel, report.Result.P95Ms);
AppendMetric(builder, "scanner_analyzer_bench_max_ms", scenarioLabel, report.Result.MaxMs);
AppendMetric(builder, "scanner_analyzer_bench_threshold_ms", scenarioLabel, report.Result.ThresholdMs);
if (report.Baseline is { } baseline)
{
AppendMetric(builder, "scanner_analyzer_bench_baseline_max_ms", scenarioLabel, baseline.MaxMs);
AppendMetric(builder, "scanner_analyzer_bench_baseline_mean_ms", scenarioLabel, baseline.MeanMs);
}
if (report.MaxRegressionRatio is { } ratio)
{
AppendMetric(builder, "scanner_analyzer_bench_regression_ratio", scenarioLabel, ratio);
AppendMetric(builder, "scanner_analyzer_bench_regression_limit", scenarioLabel, report.RegressionLimit);
AppendMetric(builder, "scanner_analyzer_bench_regression_breached", scenarioLabel, report.RegressionBreached ? 1 : 0);
}
}
File.WriteAllText(resolved, builder.ToString(), Encoding.UTF8);
}
private static void AppendMetric(StringBuilder builder, string metric, string scenarioLabel, double value)
{
builder.Append(metric);
builder.Append("{scenario=\"");
builder.Append(scenarioLabel);
builder.Append("\"} ");
builder.AppendLine(value.ToString("G17", CultureInfo.InvariantCulture));
}
private static string Escape(string value) => value.Replace("\\", "\\\\", StringComparison.Ordinal).Replace("\"", "\\\"", StringComparison.Ordinal);
}

View File

@@ -0,0 +1,24 @@
using System.Globalization;
namespace StellaOps.Bench.ScannerAnalyzers;
internal sealed record ScenarioResult(
string Id,
string Label,
int SampleCount,
double MeanMs,
double P95Ms,
double MaxMs,
int Iterations,
double ThresholdMs)
{
public string IdColumn => Id.Length <= 28 ? Id.PadRight(28) : Id[..28];
public string SampleCountColumn => SampleCount.ToString(CultureInfo.InvariantCulture).PadLeft(5);
public string MeanColumn => MeanMs.ToString("F2", CultureInfo.InvariantCulture).PadLeft(9);
public string P95Column => P95Ms.ToString("F2", CultureInfo.InvariantCulture).PadLeft(9);
public string MaxColumn => MaxMs.ToString("F2", CultureInfo.InvariantCulture).PadLeft(10);
}

View File

@@ -8,6 +8,7 @@ using StellaOps.Scanner.Analyzers.Lang.Go;
using StellaOps.Scanner.Analyzers.Lang.Java;
using StellaOps.Scanner.Analyzers.Lang.Node;
using StellaOps.Scanner.Analyzers.Lang.DotNet;
using StellaOps.Scanner.Analyzers.Lang.Python;
namespace StellaOps.Bench.ScannerAnalyzers.Scenarios;
@@ -109,6 +110,7 @@ internal sealed class LanguageAnalyzerScenarioRunner : IScenarioRunner
"go" => static () => new GoLanguageAnalyzer(),
"node" => static () => new NodeLanguageAnalyzer(),
"dotnet" => static () => new DotNetLanguageAnalyzer(),
"python" => static () => new PythonLanguageAnalyzer(),
_ => throw new InvalidOperationException($"Unsupported analyzer '{analyzerId}'."),
};
}

View File

@@ -14,5 +14,10 @@
<ProjectReference Include="..\..\..\src\StellaOps.Scanner.Analyzers.Lang.Node\StellaOps.Scanner.Analyzers.Lang.Node.csproj" />
<ProjectReference Include="..\..\..\src\StellaOps.Scanner.Analyzers.Lang.Java\StellaOps.Scanner.Analyzers.Lang.Java.csproj" />
<ProjectReference Include="..\..\..\src\StellaOps.Scanner.Analyzers.Lang.DotNet\StellaOps.Scanner.Analyzers.Lang.DotNet.csproj" />
<ProjectReference Include="..\..\..\src\StellaOps.Scanner.Analyzers.Lang.Python\StellaOps.Scanner.Analyzers.Lang.Python.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.Bench.ScannerAnalyzers.Tests" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,7 @@
scenario,iterations,sample_count,mean_ms,p95_ms,max_ms
node_monorepo_walk,5,4,9.4303,36.1354,45.0012
java_demo_archive,5,1,20.6964,81.5592,101.7846
go_buildinfo_fixture,5,2,35.0345,136.5466,170.1612
dotnet_multirid_fixture,5,2,29.1862,106.6249,132.3018
python_site_packages_walk,5,3,12.0024,45.0165,56.0003
node_monorepo_walk,5,4,6.0975,21.7421,26.8537
java_demo_archive,5,1,6.2007,23.4837,29.1143
go_buildinfo_fixture,5,2,6.1949,22.6851,27.9196
dotnet_multirid_fixture,5,2,11.4884,37.7460,46.4850
python_site_packages_scan,5,3,5.6420,18.2943,22.3739
python_pip_cache_fixture,5,1,5.8598,13.2855,15.6256
1 scenario iterations sample_count mean_ms p95_ms max_ms
2 node_monorepo_walk 5 4 9.4303 6.0975 36.1354 21.7421 45.0012 26.8537
3 java_demo_archive 5 1 20.6964 6.2007 81.5592 23.4837 101.7846 29.1143
4 go_buildinfo_fixture 5 2 35.0345 6.1949 136.5466 22.6851 170.1612 27.9196
5 dotnet_multirid_fixture 5 2 29.1862 11.4884 106.6249 37.7460 132.3018 46.4850
6 python_site_packages_walk python_site_packages_scan 5 3 12.0024 5.6420 45.0165 18.2943 56.0003 22.3739
7 python_pip_cache_fixture 5 1 5.8598 13.2855 15.6256

View File

@@ -35,11 +35,20 @@
]
},
{
"id": "python_site_packages_walk",
"label": "Python site-packages dist-info crawl",
"root": "samples/runtime/python-venv/lib/python3.11/site-packages",
"matcher": "**/*.dist-info/METADATA",
"parser": "python"
}
]
}
"id": "python_site_packages_scan",
"label": "Python analyzer on sample virtualenv",
"root": "samples/runtime/python-venv",
"analyzers": [
"python"
]
},
{
"id": "python_pip_cache_fixture",
"label": "Python analyzer verifying RECORD hashes",
"root": "src/StellaOps.Scanner.Analyzers.Lang.Python.Tests/Fixtures/lang/python/pip-cache",
"analyzers": [
"python"
]
}
]
}

View File

@@ -23,3 +23,9 @@ Results should be committed as deterministic CSV/JSON outputs with accompanying
- Scenario `dotnet_multirid_fixture` exercises the .NET analyzer against the multi-RID test fixture that merges two applications and four runtime identifiers. Latest baseline run (Release build, 5 iterations) records a mean duration of **29.19ms** (p95106.62ms, max132.30ms) with a stable component count of 2.
- Syft v1.29.1 scanning the same fixture (`syft scan dir:…`) averaged **1546ms** (p95≈2100ms, max≈2100ms) while also reporting duplicate packages; raw numbers captured in `dotnet/syft-comparison-20251023.csv`.
- The new scenario is declared in `bench/Scanner.Analyzers/config.json`; rerun the bench command above after rebuilding analyzers to refresh baselines and comparison data.
## Sprint LA2 — Python Analyzer Benchmark Notes (2025-10-23)
- Added two Python scenarios to `config.json`: the virtualenv sample (`python_site_packages_scan`) and the RECORD-heavy pip cache fixture (`python_pip_cache_fixture`).
- Baseline run (Release build, 5 iterations) records means of **5.64ms** (p9518.29ms) for the virtualenv and **5.86ms** (p9513.29ms) for the pip cache verifier; raw numbers stored in `python/hash-throughput-20251023.csv`.
- The pip cache fixture exercises `PythonRecordVerifier` with 12 RECORD rows (7 hashed) and mismatched layer coverage, giving a repeatable hash-validation throughput reference for regression gating.

View File

@@ -0,0 +1,3 @@
scenario,iterations,sample_count,mean_ms,p95_ms,max_ms
python_site_packages_scan,5,3,5.6420,18.2943,22.3739
python_pip_cache_fixture,5,1,5.8598,13.2855,15.6256
1 scenario iterations sample_count mean_ms p95_ms max_ms
2 python_site_packages_scan 5 3 5.6420 18.2943 22.3739
3 python_pip_cache_fixture 5 1 5.8598 13.2855 15.6256

View File

@@ -7,11 +7,12 @@ networks:
stellaops:
driver: bridge
volumes:
mongo-data:
minio-data:
concelier-jobs:
nats-data:
volumes:
mongo-data:
minio-data:
rustfs-data:
concelier-jobs:
nats-data:
services:
mongo:
@@ -27,8 +28,8 @@ services:
- stellaops
labels: *release-labels
minio:
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
minio:
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
command: ["server", "/data", "--console-address", ":9001"]
restart: unless-stopped
environment:
@@ -40,7 +41,22 @@ services:
- "${MINIO_CONSOLE_PORT:-29001}:9001"
networks:
- stellaops
labels: *release-labels
labels: *release-labels
rustfs:
image: registry.stella-ops.org/stellaops/rustfs:2025.10.0-edge
command: ["serve", "--listen", "0.0.0.0:8080", "--root", "/data"]
restart: unless-stopped
environment:
RUSTFS__LOG__LEVEL: info
RUSTFS__STORAGE__PATH: /data
volumes:
- rustfs-data:/data
ports:
- "${RUSTFS_HTTP_PORT:-8080}:8080"
networks:
- stellaops
labels: *release-labels
nats:
image: docker.io/library/nats@sha256:c82559e4476289481a8a5196e675ebfe67eea81d95e5161e3e78eccfe766608e
@@ -127,18 +143,19 @@ services:
- stellaops
labels: *release-labels
scanner-web:
scanner-web:
image: registry.stella-ops.org/stellaops/scanner-web@sha256:3df8ca21878126758203c1a0444e39fd97f77ddacf04a69685cda9f1e5e94718
restart: unless-stopped
depends_on:
- concelier
- minio
- nats
environment:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017"
SCANNER__STORAGE__S3__ENDPOINT: "http://minio:9000"
SCANNER__STORAGE__S3__ACCESSKEYID: "${MINIO_ROOT_USER}"
SCANNER__STORAGE__S3__SECRETACCESSKEY: "${MINIO_ROOT_PASSWORD}"
depends_on:
- concelier
- rustfs
- nats
environment:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017"
SCANNER__ARTIFACTSTORE__DRIVER: "rustfs"
SCANNER__ARTIFACTSTORE__ENDPOINT: "http://rustfs:8080/api/v1"
SCANNER__ARTIFACTSTORE__BUCKET: "scanner-artifacts"
SCANNER__ARTIFACTSTORE__TIMEOUTSECONDS: "30"
SCANNER__QUEUE__BROKER: "${SCANNER_QUEUE_BROKER}"
SCANNER__EVENTS__ENABLED: "${SCANNER_EVENTS_ENABLED:-false}"
SCANNER__EVENTS__DRIVER: "${SCANNER_EVENTS_DRIVER:-redis}"
@@ -152,17 +169,19 @@ services:
- stellaops
labels: *release-labels
scanner-worker:
scanner-worker:
image: registry.stella-ops.org/stellaops/scanner-worker@sha256:eea5d6cfe7835950c5ec7a735a651f2f0d727d3e470cf9027a4a402ea89c4fb5
restart: unless-stopped
depends_on:
- scanner-web
- nats
environment:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017"
SCANNER__STORAGE__S3__ENDPOINT: "http://minio:9000"
SCANNER__STORAGE__S3__ACCESSKEYID: "${MINIO_ROOT_USER}"
SCANNER__STORAGE__S3__SECRETACCESSKEY: "${MINIO_ROOT_PASSWORD}"
depends_on:
- scanner-web
- rustfs
- nats
environment:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017"
SCANNER__ARTIFACTSTORE__DRIVER: "rustfs"
SCANNER__ARTIFACTSTORE__ENDPOINT: "http://rustfs:8080/api/v1"
SCANNER__ARTIFACTSTORE__BUCKET: "scanner-artifacts"
SCANNER__ARTIFACTSTORE__TIMEOUTSECONDS: "30"
SCANNER__QUEUE__BROKER: "${SCANNER_QUEUE_BROKER}"
networks:
- stellaops

View File

@@ -7,11 +7,12 @@ networks:
stellaops:
driver: bridge
volumes:
mongo-data:
minio-data:
concelier-jobs:
nats-data:
volumes:
mongo-data:
minio-data:
rustfs-data:
concelier-jobs:
nats-data:
services:
mongo:
@@ -27,9 +28,9 @@ services:
- stellaops
labels: *release-labels
minio:
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
command: ["server", "/data", "--console-address", ":9001"]
minio:
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
command: ["server", "/data", "--console-address", ":9001"]
restart: unless-stopped
environment:
MINIO_ROOT_USER: "${MINIO_ROOT_USER}"
@@ -38,9 +39,24 @@ services:
- minio-data:/data
ports:
- "${MINIO_CONSOLE_PORT:-9001}:9001"
networks:
- stellaops
labels: *release-labels
networks:
- stellaops
labels: *release-labels
rustfs:
image: registry.stella-ops.org/stellaops/rustfs:2025.10.0-edge
command: ["serve", "--listen", "0.0.0.0:8080", "--root", "/data"]
restart: unless-stopped
environment:
RUSTFS__LOG__LEVEL: info
RUSTFS__STORAGE__PATH: /data
volumes:
- rustfs-data:/data
ports:
- "${RUSTFS_HTTP_PORT:-8080}:8080"
networks:
- stellaops
labels: *release-labels
nats:
image: docker.io/library/nats@sha256:c82559e4476289481a8a5196e675ebfe67eea81d95e5161e3e78eccfe766608e
@@ -125,18 +141,19 @@ services:
- stellaops
labels: *release-labels
scanner-web:
scanner-web:
image: registry.stella-ops.org/stellaops/scanner-web@sha256:e0dfdb087e330585a5953029fb4757f5abdf7610820a085bd61b457dbead9a11
restart: unless-stopped
depends_on:
- concelier
- minio
- nats
environment:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017"
SCANNER__STORAGE__S3__ENDPOINT: "http://minio:9000"
SCANNER__STORAGE__S3__ACCESSKEYID: "${MINIO_ROOT_USER}"
SCANNER__STORAGE__S3__SECRETACCESSKEY: "${MINIO_ROOT_PASSWORD}"
depends_on:
- concelier
- rustfs
- nats
environment:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017"
SCANNER__ARTIFACTSTORE__DRIVER: "rustfs"
SCANNER__ARTIFACTSTORE__ENDPOINT: "http://rustfs:8080/api/v1"
SCANNER__ARTIFACTSTORE__BUCKET: "scanner-artifacts"
SCANNER__ARTIFACTSTORE__TIMEOUTSECONDS: "30"
SCANNER__QUEUE__BROKER: "${SCANNER_QUEUE_BROKER}"
SCANNER__EVENTS__ENABLED: "${SCANNER_EVENTS_ENABLED:-false}"
SCANNER__EVENTS__DRIVER: "${SCANNER_EVENTS_DRIVER:-redis}"
@@ -150,18 +167,20 @@ services:
- stellaops
labels: *release-labels
scanner-worker:
scanner-worker:
image: registry.stella-ops.org/stellaops/scanner-worker@sha256:92dda42f6f64b2d9522104a5c9ffb61d37b34dd193132b68457a259748008f37
restart: unless-stopped
depends_on:
- scanner-web
- nats
environment:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017"
SCANNER__STORAGE__S3__ENDPOINT: "http://minio:9000"
SCANNER__STORAGE__S3__ACCESSKEYID: "${MINIO_ROOT_USER}"
SCANNER__STORAGE__S3__SECRETACCESSKEY: "${MINIO_ROOT_PASSWORD}"
SCANNER__QUEUE__BROKER: "${SCANNER_QUEUE_BROKER}"
depends_on:
- scanner-web
- rustfs
- nats
environment:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017"
SCANNER__ARTIFACTSTORE__DRIVER: "rustfs"
SCANNER__ARTIFACTSTORE__ENDPOINT: "http://rustfs:8080/api/v1"
SCANNER__ARTIFACTSTORE__BUCKET: "scanner-artifacts"
SCANNER__ARTIFACTSTORE__TIMEOUTSECONDS: "30"
SCANNER__QUEUE__BROKER: "${SCANNER_QUEUE_BROKER}"
networks:
- stellaops
labels: *release-labels

View File

@@ -7,11 +7,12 @@ networks:
stellaops:
driver: bridge
volumes:
mongo-data:
minio-data:
concelier-jobs:
nats-data:
volumes:
mongo-data:
minio-data:
rustfs-data:
concelier-jobs:
nats-data:
services:
mongo:
@@ -27,8 +28,8 @@ services:
- stellaops
labels: *release-labels
minio:
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
minio:
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
command: ["server", "/data", "--console-address", ":9001"]
restart: unless-stopped
environment:
@@ -40,7 +41,22 @@ services:
- "${MINIO_CONSOLE_PORT:-9001}:9001"
networks:
- stellaops
labels: *release-labels
labels: *release-labels
rustfs:
image: registry.stella-ops.org/stellaops/rustfs:2025.10.0-edge
command: ["serve", "--listen", "0.0.0.0:8080", "--root", "/data"]
restart: unless-stopped
environment:
RUSTFS__LOG__LEVEL: info
RUSTFS__STORAGE__PATH: /data
volumes:
- rustfs-data:/data
ports:
- "${RUSTFS_HTTP_PORT:-8080}:8080"
networks:
- stellaops
labels: *release-labels
nats:
image: docker.io/library/nats@sha256:c82559e4476289481a8a5196e675ebfe67eea81d95e5161e3e78eccfe766608e
@@ -125,18 +141,19 @@ services:
- stellaops
labels: *release-labels
scanner-web:
scanner-web:
image: registry.stella-ops.org/stellaops/scanner-web@sha256:14b23448c3f9586a9156370b3e8c1991b61907efa666ca37dd3aaed1e79fe3b7
restart: unless-stopped
depends_on:
- concelier
- minio
- nats
environment:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017"
SCANNER__STORAGE__S3__ENDPOINT: "http://minio:9000"
SCANNER__STORAGE__S3__ACCESSKEYID: "${MINIO_ROOT_USER}"
SCANNER__STORAGE__S3__SECRETACCESSKEY: "${MINIO_ROOT_PASSWORD}"
depends_on:
- concelier
- rustfs
- nats
environment:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017"
SCANNER__ARTIFACTSTORE__DRIVER: "rustfs"
SCANNER__ARTIFACTSTORE__ENDPOINT: "http://rustfs:8080/api/v1"
SCANNER__ARTIFACTSTORE__BUCKET: "scanner-artifacts"
SCANNER__ARTIFACTSTORE__TIMEOUTSECONDS: "30"
SCANNER__QUEUE__BROKER: "${SCANNER_QUEUE_BROKER}"
SCANNER__EVENTS__ENABLED: "${SCANNER_EVENTS_ENABLED:-false}"
SCANNER__EVENTS__DRIVER: "${SCANNER_EVENTS_DRIVER:-redis}"
@@ -150,17 +167,19 @@ services:
- stellaops
labels: *release-labels
scanner-worker:
scanner-worker:
image: registry.stella-ops.org/stellaops/scanner-worker@sha256:32e25e76386eb9ea8bee0a1ad546775db9a2df989fab61ac877e351881960dab
restart: unless-stopped
depends_on:
- scanner-web
- nats
environment:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017"
SCANNER__STORAGE__S3__ENDPOINT: "http://minio:9000"
SCANNER__STORAGE__S3__ACCESSKEYID: "${MINIO_ROOT_USER}"
SCANNER__STORAGE__S3__SECRETACCESSKEY: "${MINIO_ROOT_PASSWORD}"
depends_on:
- scanner-web
- rustfs
- nats
environment:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongo:27017"
SCANNER__ARTIFACTSTORE__DRIVER: "rustfs"
SCANNER__ARTIFACTSTORE__ENDPOINT: "http://rustfs:8080/api/v1"
SCANNER__ARTIFACTSTORE__BUCKET: "scanner-artifacts"
SCANNER__ARTIFACTSTORE__TIMEOUTSECONDS: "30"
SCANNER__QUEUE__BROKER: "${SCANNER_QUEUE_BROKER}"
networks:
- stellaops

View File

@@ -2,9 +2,10 @@
MONGO_INITDB_ROOT_USERNAME=stellaops
MONGO_INITDB_ROOT_PASSWORD=airgap-password
MINIO_ROOT_USER=stellaops-offline
MINIO_ROOT_PASSWORD=airgap-minio-secret
MINIO_CONSOLE_PORT=29001
AUTHORITY_ISSUER=https://authority.airgap.local
MINIO_ROOT_PASSWORD=airgap-minio-secret
MINIO_CONSOLE_PORT=29001
RUSTFS_HTTP_PORT=8080
AUTHORITY_ISSUER=https://authority.airgap.local
AUTHORITY_PORT=8440
SIGNER_POE_INTROSPECT_URL=file:///offline/poe/introspect.json
SIGNER_PORT=8441

View File

@@ -3,8 +3,9 @@ MONGO_INITDB_ROOT_USERNAME=stellaops
MONGO_INITDB_ROOT_PASSWORD=dev-password
MINIO_ROOT_USER=stellaops
MINIO_ROOT_PASSWORD=dev-minio-secret
MINIO_CONSOLE_PORT=9001
AUTHORITY_ISSUER=https://authority.localtest.me
MINIO_CONSOLE_PORT=9001
RUSTFS_HTTP_PORT=8080
AUTHORITY_ISSUER=https://authority.localtest.me
AUTHORITY_PORT=8440
SIGNER_POE_INTROSPECT_URL=https://licensing.svc.local/introspect
SIGNER_PORT=8441

View File

@@ -3,8 +3,9 @@
# Core infrastructure credentials
MONGO_INITDB_ROOT_USERNAME=stellaops_mirror
MONGO_INITDB_ROOT_PASSWORD=mirror-password
MINIO_ROOT_USER=stellaops-mirror
MINIO_ROOT_PASSWORD=mirror-minio-secret
MINIO_ROOT_USER=stellaops-mirror
MINIO_ROOT_PASSWORD=mirror-minio-secret
RUSTFS_HTTP_PORT=8080
# Mirror HTTP listeners
MIRROR_GATEWAY_HTTP_PORT=8080

View File

@@ -2,9 +2,10 @@
MONGO_INITDB_ROOT_USERNAME=stellaops
MONGO_INITDB_ROOT_PASSWORD=stage-password
MINIO_ROOT_USER=stellaops-stage
MINIO_ROOT_PASSWORD=stage-minio-secret
MINIO_CONSOLE_PORT=19001
AUTHORITY_ISSUER=https://authority.stage.stella-ops.internal
MINIO_ROOT_PASSWORD=stage-minio-secret
MINIO_CONSOLE_PORT=19001
RUSTFS_HTTP_PORT=8080
AUTHORITY_ISSUER=https://authority.stage.stella-ops.internal
AUTHORITY_PORT=8440
SIGNER_POE_INTROSPECT_URL=https://licensing.stage.stella-ops.internal/introspect
SIGNER_PORT=8441

View File

@@ -93,15 +93,16 @@ services:
volumeClaims:
- name: concelier-jobs
claimName: stellaops-concelier-jobs
scanner-web:
scanner-web:
image: registry.stella-ops.org/stellaops/scanner-web@sha256:3df8ca21878126758203c1a0444e39fd97f77ddacf04a69685cda9f1e5e94718
service:
port: 8444
env:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://stellaops-airgap:stellaops-airgap@stellaops-mongo:27017"
SCANNER__STORAGE__S3__ENDPOINT: "http://stellaops-minio:9000"
SCANNER__STORAGE__S3__ACCESSKEYID: "stellaops-airgap"
SCANNER__STORAGE__S3__SECRETACCESSKEY: "airgap-minio-secret"
SCANNER__ARTIFACTSTORE__DRIVER: "rustfs"
SCANNER__ARTIFACTSTORE__ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
SCANNER__ARTIFACTSTORE__BUCKET: "scanner-artifacts"
SCANNER__ARTIFACTSTORE__TIMEOUTSECONDS: "30"
SCANNER__QUEUE__BROKER: "nats://stellaops-nats:4222"
SCANNER__EVENTS__ENABLED: "false"
SCANNER__EVENTS__DRIVER: "redis"
@@ -109,13 +110,14 @@ services:
SCANNER__EVENTS__STREAM: "stella.events"
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "5"
SCANNER__EVENTS__MAXSTREAMLENGTH: "10000"
scanner-worker:
scanner-worker:
image: registry.stella-ops.org/stellaops/scanner-worker@sha256:eea5d6cfe7835950c5ec7a735a651f2f0d727d3e470cf9027a4a402ea89c4fb5
env:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://stellaops-airgap:stellaops-airgap@stellaops-mongo:27017"
SCANNER__STORAGE__S3__ENDPOINT: "http://stellaops-minio:9000"
SCANNER__STORAGE__S3__ACCESSKEYID: "stellaops-airgap"
SCANNER__STORAGE__S3__SECRETACCESSKEY: "airgap-minio-secret"
SCANNER__ARTIFACTSTORE__DRIVER: "rustfs"
SCANNER__ARTIFACTSTORE__ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
SCANNER__ARTIFACTSTORE__BUCKET: "scanner-artifacts"
SCANNER__ARTIFACTSTORE__TIMEOUTSECONDS: "30"
SCANNER__QUEUE__BROKER: "nats://stellaops-nats:4222"
SCANNER__EVENTS__ENABLED: "false"
SCANNER__EVENTS__DRIVER: "redis"
@@ -163,11 +165,11 @@ services:
volumeClaims:
- name: mongo-data
claimName: stellaops-mongo-data
minio:
class: infrastructure
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
service:
port: 9000
minio:
class: infrastructure
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
service:
port: 9000
command:
- server
- /data
@@ -179,9 +181,29 @@ services:
volumeMounts:
- name: minio-data
mountPath: /data
volumeClaims:
- name: minio-data
claimName: stellaops-minio-data
volumeClaims:
- name: minio-data
claimName: stellaops-minio-data
rustfs:
class: infrastructure
image: registry.stella-ops.org/stellaops/rustfs:2025.10.0-edge
service:
port: 8080
command:
- serve
- --listen
- 0.0.0.0:8080
- --root
- /data
env:
RUSTFS__LOG__LEVEL: info
RUSTFS__STORAGE__PATH: /data
volumeMounts:
- name: rustfs-data
mountPath: /data
volumeClaims:
- name: rustfs-data
claimName: stellaops-rustfs-data
nats:
class: infrastructure
image: docker.io/library/nats@sha256:c82559e4476289481a8a5196e675ebfe67eea81d95e5161e3e78eccfe766608e

View File

@@ -51,7 +51,7 @@ configMaps:
telemetry:
enableRequestLogging: true
minimumLogLevel: Debug
services:
services:
authority:
image: registry.stella-ops.org/stellaops/authority@sha256:a8e8faec44a579aa5714e58be835f25575710430b1ad2ccd1282a018cd9ffcdd
service:
@@ -92,15 +92,16 @@ services:
volumes:
- name: concelier-jobs
emptyDir: {}
scanner-web:
scanner-web:
image: registry.stella-ops.org/stellaops/scanner-web@sha256:e0dfdb087e330585a5953029fb4757f5abdf7610820a085bd61b457dbead9a11
service:
port: 8444
env:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://stellaops:stellaops@stellaops-mongo:27017"
SCANNER__STORAGE__S3__ENDPOINT: "http://stellaops-minio:9000"
SCANNER__STORAGE__S3__ACCESSKEYID: "stellaops"
SCANNER__STORAGE__S3__SECRETACCESSKEY: "dev-minio-secret"
SCANNER__ARTIFACTSTORE__DRIVER: "rustfs"
SCANNER__ARTIFACTSTORE__ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
SCANNER__ARTIFACTSTORE__BUCKET: "scanner-artifacts"
SCANNER__ARTIFACTSTORE__TIMEOUTSECONDS: "30"
SCANNER__QUEUE__BROKER: "nats://stellaops-nats:4222"
SCANNER__EVENTS__ENABLED: "false"
SCANNER__EVENTS__DRIVER: "redis"
@@ -108,13 +109,14 @@ services:
SCANNER__EVENTS__STREAM: "stella.events"
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "5"
SCANNER__EVENTS__MAXSTREAMLENGTH: "10000"
scanner-worker:
scanner-worker:
image: registry.stella-ops.org/stellaops/scanner-worker@sha256:92dda42f6f64b2d9522104a5c9ffb61d37b34dd193132b68457a259748008f37
env:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://stellaops:stellaops@stellaops-mongo:27017"
SCANNER__STORAGE__S3__ENDPOINT: "http://stellaops-minio:9000"
SCANNER__STORAGE__S3__ACCESSKEYID: "stellaops"
SCANNER__STORAGE__S3__SECRETACCESSKEY: "dev-minio-secret"
SCANNER__ARTIFACTSTORE__DRIVER: "rustfs"
SCANNER__ARTIFACTSTORE__ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
SCANNER__ARTIFACTSTORE__BUCKET: "scanner-artifacts"
SCANNER__ARTIFACTSTORE__TIMEOUTSECONDS: "30"
SCANNER__QUEUE__BROKER: "nats://stellaops-nats:4222"
SCANNER__EVENTS__ENABLED: "false"
SCANNER__EVENTS__DRIVER: "redis"
@@ -161,9 +163,9 @@ services:
volumes:
- name: mongo-data
emptyDir: {}
minio:
class: infrastructure
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
minio:
class: infrastructure
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
service:
port: 9000
command:
@@ -177,9 +179,23 @@ services:
volumeMounts:
- name: minio-data
mountPath: /data
volumes:
- name: minio-data
emptyDir: {}
volumes:
- name: minio-data
emptyDir: {}
rustfs:
class: infrastructure
image: registry.stella-ops.org/stellaops/rustfs:2025.10.0-edge
service:
port: 8080
env:
RUSTFS__LOG__LEVEL: info
RUSTFS__STORAGE__PATH: /data
volumeMounts:
- name: rustfs-data
mountPath: /data
volumes:
- name: rustfs-data
emptyDir: {}
nats:
class: infrastructure
image: docker.io/library/nats@sha256:c82559e4476289481a8a5196e675ebfe67eea81d95e5161e3e78eccfe766608e

View File

@@ -92,15 +92,16 @@ services:
volumeClaims:
- name: concelier-jobs
claimName: stellaops-concelier-jobs
scanner-web:
scanner-web:
image: registry.stella-ops.org/stellaops/scanner-web@sha256:14b23448c3f9586a9156370b3e8c1991b61907efa666ca37dd3aaed1e79fe3b7
service:
port: 8444
env:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://stellaops-stage:stellaops-stage@stellaops-mongo:27017"
SCANNER__STORAGE__S3__ENDPOINT: "http://stellaops-minio:9000"
SCANNER__STORAGE__S3__ACCESSKEYID: "stellaops-stage"
SCANNER__STORAGE__S3__SECRETACCESSKEY: "stage-minio-secret"
SCANNER__ARTIFACTSTORE__DRIVER: "rustfs"
SCANNER__ARTIFACTSTORE__ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
SCANNER__ARTIFACTSTORE__BUCKET: "scanner-artifacts"
SCANNER__ARTIFACTSTORE__TIMEOUTSECONDS: "30"
SCANNER__QUEUE__BROKER: "nats://stellaops-nats:4222"
SCANNER__EVENTS__ENABLED: "false"
SCANNER__EVENTS__DRIVER: "redis"
@@ -108,14 +109,15 @@ services:
SCANNER__EVENTS__STREAM: "stella.events"
SCANNER__EVENTS__PUBLISHTIMEOUTSECONDS: "5"
SCANNER__EVENTS__MAXSTREAMLENGTH: "10000"
scanner-worker:
scanner-worker:
image: registry.stella-ops.org/stellaops/scanner-worker@sha256:32e25e76386eb9ea8bee0a1ad546775db9a2df989fab61ac877e351881960dab
replicas: 2
env:
SCANNER__STORAGE__MONGO__CONNECTIONSTRING: "mongodb://stellaops-stage:stellaops-stage@stellaops-mongo:27017"
SCANNER__STORAGE__S3__ENDPOINT: "http://stellaops-minio:9000"
SCANNER__STORAGE__S3__ACCESSKEYID: "stellaops-stage"
SCANNER__STORAGE__S3__SECRETACCESSKEY: "stage-minio-secret"
SCANNER__ARTIFACTSTORE__DRIVER: "rustfs"
SCANNER__ARTIFACTSTORE__ENDPOINT: "http://stellaops-rustfs:8080/api/v1"
SCANNER__ARTIFACTSTORE__BUCKET: "scanner-artifacts"
SCANNER__ARTIFACTSTORE__TIMEOUTSECONDS: "30"
SCANNER__QUEUE__BROKER: "nats://stellaops-nats:4222"
SCANNER__EVENTS__ENABLED: "false"
SCANNER__EVENTS__DRIVER: "redis"
@@ -162,11 +164,11 @@ services:
volumeClaims:
- name: mongo-data
claimName: stellaops-mongo-data
minio:
class: infrastructure
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
service:
port: 9000
minio:
class: infrastructure
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
service:
port: 9000
command:
- server
- /data
@@ -178,9 +180,29 @@ services:
volumeMounts:
- name: minio-data
mountPath: /data
volumeClaims:
- name: minio-data
claimName: stellaops-minio-data
volumeClaims:
- name: minio-data
claimName: stellaops-minio-data
rustfs:
class: infrastructure
image: registry.stella-ops.org/stellaops/rustfs:2025.10.0-edge
service:
port: 8080
command:
- serve
- --listen
- 0.0.0.0:8080
- --root
- /data
env:
RUSTFS__LOG__LEVEL: info
RUSTFS__STORAGE__PATH: /data
volumeMounts:
- name: rustfs-data
mountPath: /data
volumeClaims:
- name: rustfs-data
claimName: stellaops-rustfs-data
nats:
class: infrastructure
image: docker.io/library/nats@sha256:c82559e4476289481a8a5196e675ebfe67eea81d95e5161e3e78eccfe766608e

View File

@@ -1,8 +1,8 @@
release:
version: "2025.10.0-edge"
channel: "edge"
date: "2025-10-01T00:00:00Z"
calendar: "2025.10"
release:
version: "2025.10.0-edge"
channel: "edge"
date: "2025-10-01T00:00:00Z"
calendar: "2025.10"
components:
- name: authority
image: registry.stella-ops.org/stellaops/authority@sha256:a8e8faec44a579aa5714e58be835f25575710430b1ad2ccd1282a018cd9ffcdd
@@ -20,10 +20,12 @@ release:
image: registry.stella-ops.org/stellaops/excititor@sha256:d9bd5cadf1eab427447ce3df7302c30ded837239771cc6433b9befb895054285
- name: web-ui
image: registry.stella-ops.org/stellaops/web-ui@sha256:38b225fa7767a5b94ebae4dae8696044126aac429415e93de514d5dd95748dcf
infrastructure:
mongo:
image: docker.io/library/mongo@sha256:c258b26dbb7774f97f52aff52231ca5f228273a84329c5f5e451c3739457db49
minio:
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
checksums:
releaseManifestSha256: 822f82987529ea38d2321dbdd2ef6874a4062a117116a20861c26a8df1807beb
infrastructure:
mongo:
image: docker.io/library/mongo@sha256:c258b26dbb7774f97f52aff52231ca5f228273a84329c5f5e451c3739457db49
minio:
image: docker.io/minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
rustfs:
image: registry.stella-ops.org/stellaops/rustfs:2025.10.0-edge
checksums:
releaseManifestSha256: 64d5b05c864bbfaeb29dad3958f4e7ff43d13393059da558ab355cebb9aba2b7

View File

@@ -49,7 +49,7 @@
* **Fulcio** (Sigstore CA) — issues shortlived signing certs (keyless).
* **Rekor v2** (tilebacked transparency log).
* **MinIO** — S3compatible object store with lifecycle & Object Lock.
* **RustFS** — offline-first object store with deterministic REST API (S3/MinIO fallback available for legacy installs).
* **MongoDB** — catalog, advisories, VEX, scheduler, notify.
* **Queue** — Redis Streams / NATS / RabbitMQ (pluggable).
* **OCI Registry** — must support **Referrers API** (discover SBOMs/signatures).
@@ -81,7 +81,7 @@ flowchart LR
ATT[Attestor\n(Rekor v2 submit/verify)]
UI[Web UI (Angular)]
Z[Zastava\n(Runtime Inspector/Enforcer)]
MIN[(MinIO S3)]
RFS[(RustFS object store)]
MGO[(MongoDB)]
QUE[(Queue/Streams)]
end
@@ -94,7 +94,7 @@ flowchart LR
CLI -->|scan/build| SW
SW -->|jobs| QUE
QUE --> WK
WK --> MIN
WK --> RFS
SW --> MGO
CONC --> MGO
EXC --> MGO
@@ -225,13 +225,13 @@ LS --> IA: PoE (mTLS client cert or JWT with cnf=K_inst), CRL/OCSP/introspect
---
## 6) Storage & catalogs (MinIO/Mongo)
## 6) Storage & catalogs (RustFS/Mongo)
**RustFS layout (default)**
**MinIO layout**
```
s3://stellaops/
layers/<sha256>/sbom.cdx.json.zst
```
rustfs://stellaops/
layers/<sha256>/sbom.cdx.json.zst
layers/<sha256>/sbom.spdx.json.zst
images/<imgDigest>/inventory.cdx.pb
images/<imgDigest>/usage.cdx.pb
@@ -248,7 +248,7 @@ s3://stellaops/
**Retention**
* MinIO **ILM** for coarse TTL; Scanner.WebService GC decrements `refCount` and deletes unreferenced metadata; **Object Lock** for immutable classes (auditable artifacts).
* RustFS applies retention via `X-RustFS-Retain-Seconds`; Scanner.WebService GC decrements `refCount` and deletes unreferenced metadata; S3/MinIO fallback retains native Object Lock when enabled.
---
@@ -395,7 +395,7 @@ services:
ui: { image: stellaops/ui, depends_on: [scanner-web, concelier, excititor, scheduler-web, notify-web] }
```
* **Backups:** Mongo dumps; MinIO versioned buckets & replication; Rekor v2 DB snapshots; JWKS/Fulcio/KMS key rotation.
* **Backups:** Mongo dumps; RustFS snapshots (or S3 versioning when fallback driver is used); Rekor v2 DB snapshots; JWKS/Fulcio/KMS key rotation.
* **Ops runbooks:** Scheduler catchup after Concelier/Excititor recovery; connector key rotation (Slack/Teams/SMTP).
* **SLOs & alerts:** lag between Concelier/Excititor export and first rescan verdict; delivery failure rates by channel.
@@ -408,7 +408,7 @@ services:
* **Notify metrics:** `notify.sent_total{channel}`, `notify.dropped_total{reason}`, `notify.digest_coalesced_total`, `notify.latency_ms`.
* **Tracing:** perstage spans; correlation IDs across Scanner→Signer→Attestor and Concelier/Excititor→Scheduler→Scanner→Notify.
* **Audit logs:** every signing records `license_id`, `image_digest`, `policy_digest`, and Rekor UUID; Scheduler records who scheduled what; Notify records where, when, and why messages were sent or deduped.
* **Compliance:** MinIO **Object Lock** for immutable artifacts; reproducible outputs via policy digest + SBOM digest in predicate.
* **Compliance:** RustFS retention headers (or MinIO Object Lock when operating in S3 mode) keep immutable artifacts tamperresistant; reproducible outputs via policy digest + SBOM digest in predicate.
---

View File

@@ -382,28 +382,65 @@ Request body mirrors policy preview inputs (image digest plus findings). The ser
```json
{
"report": {
"reportId": "report-3def5f362aa475ef14b6",
"imageDigest": "sha256:deadbeef",
"reportId": "report-9f8cde21aab54321",
"imageDigest": "sha256:7dbe0c9a5d4f1c8184007e9d94dbe55928f8a2db5ab9c1c2d4a2f7bbcdfe1234",
"generatedAt": "2025-10-23T15:32:22Z",
"verdict": "blocked",
"policy": { "revisionId": "rev-1", "digest": "27d2ec2b34feedc304fc564d252ecee1c8fa14ea581a5ff5c1ea8963313d5c8d" },
"summary": { "total": 1, "blocked": 1, "warned": 0, "ignored": 0, "quieted": 0 },
"policy": {
"revisionId": "rev-42",
"digest": "8a0f72f8dc5c51c46991db3bba34e9b3c0c8e944a7a6d0a9c29a9aa6b8439876"
},
"summary": { "total": 2, "blocked": 1, "warned": 1, "ignored": 0, "quieted": 0 },
"verdicts": [
{
"findingId": "finding-1",
"findingId": "library:pkg/openssl@1.1.1w",
"status": "Blocked",
"ruleName": "Block Critical",
"ruleAction": "Block",
"score": 40.5,
"ruleName": "Block vendor unknowns",
"ruleAction": "block",
"notes": "Unknown vendor telemetry — medium confidence band.",
"score": 19.5,
"configVersion": "1.0",
"inputs": {
"reachabilityWeight": 0.45,
"baseScore": 40.5,
"severityWeight": 90,
"trustWeight": 1,
"trustWeight.NVD": 1,
"reachability.runtime": 0.45
"severityWeight": 50,
"trustWeight": 0.65,
"reachabilityWeight": 0.6,
"baseScore": 19.5,
"trustWeight.vendor": 0.65,
"reachability.unknown": 0.6,
"unknownConfidence": 0.55,
"unknownAgeDays": 5
},
"quietedBy": null,
"quiet": false,
"unknownConfidence": 0.55,
"confidenceBand": "medium",
"unknownAgeDays": 5,
"sourceTrust": "vendor",
"reachability": "unknown"
},
{
"findingId": "library:pkg/zlib@1.3.1",
"status": "Warned",
"ruleName": "Runtime mitigation required",
"ruleAction": "warn",
"notes": "Runtime reachable unknown — mitigation window required.",
"score": 18.75,
"configVersion": "1.0",
"inputs": {
"severityWeight": 75,
"trustWeight": 1,
"reachabilityWeight": 0.45,
"baseScore": 33.75,
"reachability.runtime": 0.45,
"warnPenalty": 15,
"unknownConfidence": 0.35,
"unknownAgeDays": 13
},
"quietedBy": null,
"quiet": false,
"unknownConfidence": 0.35,
"confidenceBand": "medium",
"unknownAgeDays": 13,
"sourceTrust": "NVD",
"reachability": "runtime"
}
@@ -412,21 +449,21 @@ Request body mirrors policy preview inputs (image digest plus findings). The ser
},
"dsse": {
"payloadType": "application/vnd.stellaops.report+json",
"payload": "<base64 canonical report>",
"payload": "eyJyZXBvcnQiOnsicmVwb3J0SWQiOiJyZXBvcnQtOWY4Y2RlMjFhYWI1NDMyMSJ9fQ==",
"signatures": [
{
"keyId": "scanner-report-signing",
"algorithm": "hs256",
"signature": "<base64 signature>"
"signature": "MEQCIGHscnJ2bm9wYXlsb2FkZXIAIjANBgkqhkiG9w0BAQsFAAOCAQEASmFja3Nvbk1ldGE="
}
]
}
}
```
- The `report` object omits null fields and is deterministic (ISO timestamps, sorted keys).
- The `report` object omits null fields and is deterministic (ISO timestamps, sorted keys) while surfacing `unknownConfidence`, `confidenceBand`, and `unknownAgeDays` for auditability.
- `dsse` follows the DSSE (Dead Simple Signing Envelope) shape; `payload` is the canonical UTF-8 JSON and `signatures[0].signature` is the base64 HMAC/Ed25519 value depending on configuration.
- A runnable sample envelope is available at `samples/api/reports/report-sample.dsse.json` for tooling tests or signature verification.
- Full offline samples live at `samples/policy/policy-report-unknown.json` (request + response) and `samples/api/reports/report-sample.dsse.json` (envelope fixture) for tooling tests or signature verification.
**Response 404** `application/problem+json` payload with type `https://stellaops.org/problems/not-found` when the scan identifier is unknown.

View File

@@ -176,8 +176,9 @@ Authority now understands two flavours of sender-constrained OAuth clients:
```
Operators can override any field via environment variables (e.g. `STELLAOPS_AUTHORITY__SECURITY__SENDERCONSTRAINTS__DPOP__NONCE__STORE=redis`).
- Declare client `audiences` in bootstrap manifests or plug-in provisioning metadata; Authority now defaults the token `aud` claim and `resource` indicator from this list, which is also used to trigger nonce enforcement for audiences such as `signer` and `attestor`.
- **Mutual TLS clients** client registrations may declare an mTLS binding (`senderConstraint: mtls`). When enabled via `security.senderConstraints.mtls`, Authority validates the presented client certificate against stored bindings (`certificateBindings[]`), optional chain verification, and timing windows. Successful requests embed `cnf.x5t#S256` into the access token so resource servers can enforce the certificate thumbprint.
- Certificate bindings record the certificate thumbprint, optional SANs, subject/issuer metadata, and activation windows. Operators can enforce subject regexes, SAN type allow-lists (`dns`, `uri`, `ip`), trusted certificate authorities, and rotation grace via `security.senderConstraints.mtls.*`.
- **Mutual TLS clients** client registrations may declare an mTLS binding (`senderConstraint: mtls`). When enabled via `security.senderConstraints.mtls`, Authority validates the presented client certificate against stored bindings (`certificateBindings[]`), optional chain verification, and timing windows. Successful requests embed `cnf.x5t#S256` into the access token (and introspection output) so resource servers can enforce the certificate thumbprint.
- `security.senderConstraints.mtls.enforceForAudiences` forces mTLS whenever the requested `aud`/`resource` (or the client's configured audiences) intersect the configured allow-list (default includes `signer`). Clients configured for different sender constraints are rejected early so operator policy remains consistent.
- Certificate bindings now act as an allow-list: Authority verifies thumbprint, subject, issuer, serial number, and any declared SAN values against the presented certificate, with rotation grace windows applied to `notBefore/notAfter`. Operators can enforce subject regexes, SAN type allow-lists (`dns`, `uri`, `ip`), trusted certificate authorities, and rotation grace via `security.senderConstraints.mtls.*`.
Both modes persist additional metadata in `authority_tokens`: `senderConstraint` records the enforced policy, while `senderKeyThumbprint` stores the DPoP JWK thumbprint or mTLS certificate hash captured at issuance. Downstream services can rely on these fields (and the corresponding `cnf` claim) when auditing offline copies of the token store.

View File

@@ -306,7 +306,22 @@ Validation occurs alongside policy binding (`PolicyScoringConfigBinder`), produc
**Runtime usage**
- `trustOverrides` are matched against `finding.tags` (`trust:<key>`) first, then `finding.source`/`finding.vendor`; missing keys default to `1.0`.
- `reachabilityBuckets` consume `finding.tags` with prefix `reachability:` (fallback `usage:` or `unknown`). Missing buckets fall back to `unknown` weight when present, otherwise `1.0`.
- Policy verdicts expose scoring inputs (`severityWeight`, `trustWeight`, `reachabilityWeight`, `baseScore`, penalties) plus unknown-state metadata (`unknownConfidence`, `unknownAgeDays`, `confidenceBand`) for auditability. See `samples/policy/policy-preview-unknown.json` for an end-to-end preview payload.
- Policy verdicts expose scoring inputs (`severityWeight`, `trustWeight`, `reachabilityWeight`, `baseScore`, penalties) plus unknown-state metadata (`unknownConfidence`, `unknownAgeDays`, `confidenceBand`) for auditability. See `samples/policy/policy-preview-unknown.json` and `samples/policy/policy-report-unknown.json` for offline reference payloads validated against the published schemas below.
Validate the samples locally with **Ajv** before publishing changes:
```bash
# install once per checkout (offline-safe):
npm install --no-save ajv-cli@5 ajv-formats@2
npx ajv validate --spec=draft2020 -c ajv-formats \
-s docs/schemas/policy-preview-sample@1.json \
-d samples/policy/policy-preview-unknown.json
npx ajv validate --spec=draft2020 -c ajv-formats \
-s docs/schemas/policy-report-sample@1.json \
-d samples/policy/policy-report-unknown.json
```
- Unknown confidence derives from `unknown-age-days:` (preferred) or `unknown-since:` + `observed-at:` tags; with no hints the engine keeps `initial` confidence. Values decay by `decayPerDay` down to `floor`, then resolve to the first matching `bands[].name`.
---

View File

@@ -56,7 +56,7 @@
##3Test Harness
* **Runner** `perf/run.sh`, accepts `--phase` and `--samples`.
* **Language analyzers microbench** `dotnet run --project bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj -- --repo-root . --out bench/Scanner.Analyzers/baseline.csv` produces deterministic CSVs for analyzer scenarios (Node today, others as they land).
* **Language analyzers microbench** `dotnet run --project bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj -- --repo-root . --out bench/Scanner.Analyzers/baseline.csv --json out/bench/scanner-analyzers/latest.json --prom out/bench/scanner-analyzers/latest.prom --commit $(git rev-parse HEAD)` produces CSV + JSON + Prometheus gauges for analyzer scenarios. Runs fail if `max_ms` regresses ≥20% against `baseline.csv` or if thresholds are exceeded.
* **Metrics** Prometheus + `jq` extracts; aggregated via `scripts/aggregate.ts`.
* **CI** GitLab CI job *benchmark* publishes JSON to `benchartifacts/`.
* **Visualisation** Grafana dashboard *StellaPerf* (provisioned JSON).
@@ -143,7 +143,9 @@ P99=48ms. Meets 50ms gate.
##8Trend Snapshot
![Perf trend sparkline placeholder](perftrend.svg)
![Perf trend sparkline placeholder](perftrend.svg)
> **Grafana/Alerting** Import `docs/ops/scanner-analyzers-grafana-dashboard.json` and point it at the Prometheus datasource storing `scanner_analyzer_bench_*` metrics. Configure an alert on `scanner_analyzer_bench_regression_ratio` ≥1.20 (default limit); the bundled Stat panel surfaces breached scenarios (non-zero values). On-call runbook: `docs/ops/scanner-analyzers-operations.md`.
_Plot generated weekly by `scripts/updatetrend.py`; shows last 12 weeks P95 per phase._

View File

@@ -96,11 +96,29 @@ _No external fonts or JS true offline guarantee._
| **Import / Export** | Buttons map to `/policy/import` and `/policy/export`. Accepts `.yaml`, `.rego`, `.zip` (bundle). |
| **History** | Immutable audit log; diff viewer highlights rule changes. |
####3.3.1YAML → Rego Bridge
If you paste YAML but enable **Strict Mode** (toggle), backend converts to Rego under the hood, stores both representations, and shows a sidebyside diff.
###3.4📌Settings Enhancements
####3.3.1YAML → Rego Bridge
If you paste YAML but enable **Strict Mode** (toggle), backend converts to Rego under the hood, stores both representations, and shows a sidebyside diff.
####3.3.2Preview / Report Fixtures
- Use the offline fixtures (`samples/policy/policy-preview-unknown.json` and `samples/policy/policy-report-unknown.json`) to exercise the Policies screens without a live backend; both payloads include confidence bands, unknown-age tags, and scoring inputs that map directly to the UI panels.
- Keep them in lock-step with the API by validating any edits with Ajv:
```bash
# install once per checkout (offline-safe):
npm install --no-save ajv-cli@5 ajv-formats@2
npx ajv validate --spec=draft2020 -c ajv-formats \
-s docs/schemas/policy-preview-sample@1.json \
-d samples/policy/policy-preview-unknown.json
npx ajv validate --spec=draft2020 -c ajv-formats \
-s docs/schemas/policy-report-sample@1.json \
-d samples/policy/policy-report-unknown.json
```
###3.4📌Settings Enhancements
| Setting | Details |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -17,11 +17,11 @@ completely isolated network:
| **Provenance** | Cosign signature, SPDX 2.3 SBOM, intoto SLSA attestation |
| **Attested manifest** | `offline-manifest.json` + detached JWS covering bundle metadata, signed during export. |
| **Delta patches** | Daily diff bundles keep size \<350MB |
| **Scanner plug-ins** | OS analyzers plus the Node.js, Go, and .NET language analyzers packaged under `plugins/scanner/analyzers/**` with manifests so Workers load deterministically offline. |
| **Scanner plug-ins** | OS analyzers plus the Node.js, Go, .NET, and Python language analyzers packaged under `plugins/scanner/analyzers/**` with manifests so Workers load deterministically offline. |
**RU BDU note:** ship the official Russian Trusted Root/Sub CA bundle (`certificates/russian_trusted_bundle.pem`) inside the kit so `concelier:httpClients:source.bdu:trustedRootPaths` can resolve it when the service runs in an airgapped network. Drop the most recent `vulxml.zip` alongside the kit if operators need a cold-start cache.
**Language analyzers:** the kit now carries the restart-only Node.js, Go, and .NET analyzer plug-ins (`plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Node/`, `plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Go/`, `plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.DotNet/`). Drop the directories alongside Worker binaries so the unified plug-in catalog can load them without outbound fetches; upcoming Python/Rust plug-ins will follow the same layout.
**Language analyzers:** the kit now carries the restart-only Node.js, Go, .NET, and Python analyzer plug-ins (`plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Node/`, `...Lang.Go/`, `...Lang.DotNet/`, `...Lang.Python/`). Drop the directories alongside Worker binaries so the unified plug-in catalog can load them without outbound fetches; Rust remains on the Wave4 roadmap.
*Scanner core:* C# 12 on **.NET{{ dotnet }}**.
*Imports are idempotent and atomic — no service downtime.*
@@ -99,6 +99,24 @@ Example excerpt (2025-10-23 kit) showing the Go and .NET analyzer plug-in payloa
"size": 647,
"capturedAt": "2025-10-23T00:00:00Z"
}
{
"name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Python/StellaOps.Scanner.Analyzers.Lang.Python.dll",
"sha256": "28b6e06c7cabf3b78f13f801cbb14962093f3d42c4ae9ec01babbcd14cda4644",
"size": 53760,
"capturedAt": "2025-10-23T00:00:00Z"
}
{
"name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Python/StellaOps.Scanner.Analyzers.Lang.Python.pdb",
"sha256": "be4e34b4dc9a790fe1299e84213343b7c8ea90a2d22e5d7d1aa7585b8fedc946",
"size": 34516,
"capturedAt": "2025-10-23T00:00:00Z"
}
{
"name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Python/manifest.json",
"sha256": "bceea1e7542aae860b0ec5ba7b8b3aa960b21edc4d1efe60afc98ce289341ac3",
"size": 671,
"capturedAt": "2025-10-23T00:00:00Z"
}
```
---
@@ -130,7 +148,7 @@ The CLI validates recorded digests (when `.metadata.json` is present) before str
**Quick smoke test:** before import, verify the tarball carries the Go analyzer plug-in:
```bash
tar -tzf stella-ops-offline-kit-<DATE>.tgz 'plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Go/*' 'plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.DotNet/*'
tar -tzf stella-ops-offline-kit-<DATE>.tgz 'plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Go/*' 'plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.DotNet/*' 'plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Python/*'
```
The manifest lookup above and this `tar` listing should both surface the Go analyzer DLL, PDB, and manifest entries before the kit is promoted.

View File

@@ -99,6 +99,8 @@ plan? = <plan name> // optional hint for UIs; not used for e
* **Client Credentials** (service→service):
* **mTLS**: mutual TLS + `client_id` → bound token (`cnf.x5t#S256`)
* `security.senderConstraints.mtls.enforceForAudiences` forces the mTLS path when requested `aud`/`resource` values intersect high-value audiences (defaults include `signer`). Authority rejects clients attempting to use DPoP/basic secrets for these audiences.
* Stored `certificateBindings` are authoritative: thumbprint, subject, issuer, serial number, and SAN values are matched against the presented certificate, with rotation grace applied to activation windows. Failures surface deterministic error codes (e.g. `certificate_binding_subject_mismatch`).
* **private_key_jwt**: JWTbased client auth + **DPoP** header (preferred for tools and CLI)
* **Device Code** (CLI): `POST /oauth/device/code` + `POST /oauth/token` poll
* **Authorization Code + PKCE** (UI): standard

View File

@@ -1,6 +1,6 @@
# component_architecture_devops.md — **StellaOps Release & Operations** (2025Q4)
> **Scope.** Implementationready blueprint for **how StellaOps is built, versioned, signed, distributed, upgraded, licensed (PoE)**, and operated in customer environments (online and airgapped). Covers reproducible builds, supplychain attestations, registries, offline kits, migration/rollback, artifact lifecycle (MinIO/Mongo), monitoring SLOs, and customer activation.
> **Scope.** Implementationready blueprint for **how StellaOps is built, versioned, signed, distributed, upgraded, licensed (PoE)**, and operated in customer environments (online and airgapped). Covers reproducible builds, supplychain attestations, registries, offline kits, migration/rollback, artifact lifecycle (RustFS default + Mongo, S3 fallback), monitoring SLOs, and customer activation.
---
@@ -257,12 +257,12 @@ Signer validates **scanner** images cosign identity + calendar tag for **rele
---
## 7) Artifact lifecycle & storage (MinIO/Mongo)
## 7) Artifact lifecycle & storage (RustFS/Mongo)
### 7.1 Buckets & prefixes (MinIO)
### 7.1 Buckets & prefixes (RustFS)
```
s3://stellaops/
rustfs://stellaops/
scanner/
layers/<sha256>/sbom.cdx.json.zst
images/<imgDigest>/inventory.cdx.pb
@@ -283,7 +283,7 @@ s3://stellaops/
* **`short`**: working artifacts (diffs, queues) — TTL 714 days.
* **`default`**: SBOMs & indexes — TTL 90180 days (configurable).
* **`compliance`**: signed reports & attested exports — **Object Lock** (governance/compliance) 17 years.
* **`compliance`**: signed reports & attested exports — retention enforced via RustFS hold or S3 Object Lock (governance/compliance) 17 years.
### 7.3 Artifact Lifecycle Controller (ALC)
@@ -292,6 +292,9 @@ s3://stellaops/
* Artifacts referenced by **reports** or **tickets** are pinned.
* ILM actions logged; UI shows perclass usage & upcoming purges.
> **Migration note.** Follow `docs/ops/scanner-rustfs-migration.md` when transitioning existing
> MinIO buckets to RustFS. The provided migrator is idempotent and safe to rerun per prefix.
### 7.4 Mongo retention
* **Scanner**: `runtime.events` use TTL (e.g., 3090 days); **catalog** permanent.
@@ -313,7 +316,7 @@ s3://stellaops/
* **Golden signals**:
* **Latency**: token issuance, sign→attest roundtrip, scan enqueue→emit, export build.
* **Saturation**: queue depth, Mongo write IOPS, MinIO net throughput.
* **Saturation**: queue depth, Mongo write IOPS, RustFS throughput / queue depth (or S3 metrics when in fallback mode).
* **Traffic**: scans/min, attestations/min, webhook admits/min.
* **Errors**: 5xx rates, cosign verification failures, Rekor timeouts.
@@ -460,7 +463,7 @@ services:
* `attestor.submit_latency_seconds{quantile=0.95}` < 0.3.
* `scanner.scan_latency_seconds{quantile=0.95}` < target per image size.
* `concelier.export.duration_seconds` stable; `excititor.consensus.conflicts_total` not exploding after policy changes.
* MinIO `s3_requests_errors_total` near zero; Mongo `opcounters` hit expected baseline.
* RustFS request error rate near zero (or `s3_requests_errors_total` when operating against S3); Mongo `opcounters` hit expected baseline.
### Appendix B — Upgrade safety checklist

View File

@@ -1,6 +1,6 @@
# component_architecture_scanner.md — **StellaOps Scanner** (2025Q4)
> **Scope.** Implementationready architecture for the **Scanner** subsystem: WebService, Workers, analyzers, SBOM assembly (inventory & usage), perlayer caching, threeway diffs, artifact catalog (MinIO+Mongo), attestation handoff, and scale/security posture. This document is the contract between the scanning plane and everything else (Policy, Excititor, Concelier, UI, CLI).
> **Scope.** Implementationready architecture for the **Scanner** subsystem: WebService, Workers, analyzers, SBOM assembly (inventory & usage), perlayer caching, threeway diffs, artifact catalog (RustFS default + Mongo, S3-compatible fallback), attestation handoff, and scale/security posture. This document is the contract between the scanning plane and everything else (Policy, Excititor, Concelier, UI, CLI).
---
@@ -23,7 +23,7 @@ src/
├─ StellaOps.Scanner.WebService/ # REST control plane, catalog, diff, exports
├─ StellaOps.Scanner.Worker/ # queue consumer; executes analyzers
├─ StellaOps.Scanner.Models/ # DTOs, evidence, graph nodes, CDX/SPDX adapters
├─ StellaOps.Scanner.Storage/ # Mongo repositories; MinIO object client; ILM/GC
├─ StellaOps.Scanner.Storage/ # Mongo repositories; RustFS object client (default) + S3 fallback; ILM/GC
├─ StellaOps.Scanner.Queue/ # queue abstraction (Redis/NATS/RabbitMQ)
├─ StellaOps.Scanner.Cache/ # layer cache; file CAS; bloom/bitmap indexes
├─ StellaOps.Scanner.EntryTrace/ # ENTRYPOINT/CMD → terminal program resolver (shell AST)
@@ -81,7 +81,7 @@ The DI extension (`AddScannerQueue`) wires the selected transport, so future add
## 2) External dependencies
* **OCI registry** with **Referrers API** (discover attached SBOMs/signatures).
* **MinIO** (S3compatible) for SBOM artifacts; **Object Lock** for immutable classes; **ILM** for TTL.
* **RustFS** (default, offline-first) for SBOM artifacts; optional S3/MinIO compatibility retained for migration; **Object Lock** semantics emulated via retention headers; **ILM** for TTL.
* **MongoDB** for catalog, job state, diffs, ILM rules.
* **Queue** (Redis Streams/NATS/RabbitMQ).
* **Authority** (onprem OIDC) for **OpToks** (DPoP/mTLS).
@@ -133,7 +133,7 @@ No confidences. Either a fact is proven with listed mechanisms, or it is not cla
* `jobs { _id, kind, args, state, startedAt, heartbeatAt, endedAt, error }`
* `lifecycleRules { ruleId, scope, ttlDays, retainIfReferenced, immutable }`
### 3.3 Object store layout (MinIO)
### 3.3 Object store layout (RustFS)
```
layers/<sha256>/sbom.cdx.json.zst
@@ -145,6 +145,13 @@ diffs/<old>_<new>/diff.json.zst
attest/<artifactSha256>.dsse.json # DSSE bundle (cert chain + Rekor proof)
```
RustFS exposes a deterministic HTTP API (`PUT|GET|DELETE /api/v1/buckets/{bucket}/objects/{key}`).
Scanner clients tag immutable uploads with `X-RustFS-Immutable: true` and, when retention applies,
`X-RustFS-Retain-Seconds: <ttlSeconds>`. Additional headers can be injected via
`scanner.artifactStore.headers` to support custom auth or proxy requirements. Legacy MinIO/S3
deployments remain supported by setting `scanner.artifactStore.driver = "s3"` during phased
migrations.
---
## 4) REST API (Scanner.WebService)
@@ -396,7 +403,7 @@ scanner:
* **HA**: WebService horizontal scale; Workers autoscale by queue depth & CPU; distributed locks on layers.
* **Retention**: ILM rules per artifact class (`short`, `default`, `compliance`); **Object Lock** for compliance artifacts (reports, signed SBOMs).
* **Upgrades**: bump **cache schema** when analyzer outputs change; WebService triggers refresh of dependent artifacts.
* **Backups**: Mongo (daily dumps); MinIO (versioned buckets, replication); Rekor v2 DB snapshots.
* **Backups**: Mongo (daily dumps); RustFS snapshots (filesystem-level rsync/ZFS) or S3 versioning when legacy driver enabled; Rekor v2 DB snapshots.
---

View File

@@ -80,6 +80,8 @@ Everything here is opensource and versioned— when you check out a git ta
- **29[Concelier CISA ICS Connector Operations](ops/concelier-icscisa-operations.md)**
- **30[Concelier CERT-Bund Connector Operations](ops/concelier-certbund-operations.md)**
- **31[Concelier MSRC Connector AAD Onboarding](ops/concelier-msrc-operations.md)**
- **32[Scanner Analyzer Bench Operations](ops/scanner-analyzers-operations.md)**
- **33[Scanner Artifact Store Migration](ops/scanner-rustfs-migration.md)**
### Legal & licence
- **32[Legal & Quota FAQ](29_LEGAL_FAQ_QUOTA.md)**

View File

@@ -14,6 +14,8 @@
> **Status update (2025-10-19):** `ValidateDpopProofHandler`, `AuthorityClientCertificateValidator`, and the supporting storage/audit plumbing now live in `src/StellaOps.Authority`. DPoP proofs populate `cnf.jkt`, mTLS bindings enforce certificate thumbprints via `cnf.x5t#S256`, and token documents persist the sender constraint metadata. In-memory nonce issuance is wired (Redis implementation to follow). Documentation and configuration references were updated (`docs/11_AUTHORITY.md`). Targeted unit/integration tests were added; running the broader test suite is currently blocked by pre-existing `StellaOps.Concelier.Storage.Mongo` build errors.
>
> **Status update (2025-10-20):** Redis-backed nonce configuration is exposed through `security.senderConstraints.dpop.nonce` with sample YAML (`etc/authority.yaml.sample`) and architecture docs refreshed. Operator guide now includes concrete Redis/required audiences snippet; nonce challenge regression remains covered by `ValidateDpopProof_IssuesNonceChallenge_WhenNonceMissing`.
>
> **Status update (2025-10-23):** mTLS enforcement now honours `security.senderConstraints.mtls.enforceForAudiences`, automatically rejecting non-mTLS clients targeting audiences such as `signer`. Certificate bindings validate thumbprint, issuer, subject, serial number, and SAN values, producing deterministic error codes for operators. Introspection responses include `cnf.x5t#S256`, and new unit tests cover audience enforcement, binding mismatches, and bootstrap storage. Docs/sample config updated accordingly.
## Design Summary
- Extract the existing Scanner `DpopProofValidator` stack into a shared `StellaOps.Auth.Security` library used by Authority and resource servers.

View File

@@ -0,0 +1,155 @@
{
"title": "StellaOps Scanner Analyzer Benchmarks",
"uid": "scanner-analyzer-bench",
"schemaVersion": 38,
"version": 1,
"editable": true,
"timezone": "",
"graphTooltip": 0,
"time": {
"from": "now-24h",
"to": "now"
},
"templating": {
"list": [
{
"name": "datasource",
"type": "datasource",
"query": "prometheus",
"refresh": 1,
"hide": 0,
"current": {}
}
]
},
"annotations": {
"list": []
},
"panels": [
{
"id": 1,
"title": "Max Duration (ms)",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"fieldConfig": {
"defaults": {
"unit": "ms",
"displayName": "{{scenario}}"
},
"overrides": []
},
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"expr": "scanner_analyzer_bench_max_ms",
"legendFormat": "{{scenario}}",
"refId": "A"
},
{
"expr": "scanner_analyzer_bench_baseline_max_ms",
"legendFormat": "{{scenario}} baseline",
"refId": "B"
}
]
},
{
"id": 2,
"title": "Regression Ratio vs Limit",
"type": "timeseries",
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"displayName": "{{scenario}}",
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 20
}
]
}
},
"overrides": []
},
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom"
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"expr": "(scanner_analyzer_bench_regression_ratio - 1) * 100",
"legendFormat": "{{scenario}} regression %",
"refId": "A"
},
{
"expr": "(scanner_analyzer_bench_regression_limit - 1) * 100",
"legendFormat": "{{scenario}} limit %",
"refId": "B"
}
]
},
{
"id": 3,
"title": "Breached Scenarios",
"type": "stat",
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"fieldConfig": {
"defaults": {
"displayName": "{{scenario}}",
"unit": "short"
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"reduceOptions": {
"calcs": [
"last"
],
"fields": "",
"values": false
}
},
"targets": [
{
"expr": "scanner_analyzer_bench_regression_breached",
"legendFormat": "{{scenario}}",
"refId": "A"
}
]
}
]
}

View File

@@ -0,0 +1,48 @@
# Scanner Analyzer Benchmarks Operations Guide
## Purpose
Keep the language analyzer microbench under the <5s SBOM pledge. CI emits Prometheus metrics and JSON fixtures so trend dashboards and alerts stay in lockstep with the repository baseline.
> **Grafana note:** Import `docs/ops/scanner-analyzers-grafana-dashboard.json` into your Prometheus-backed Grafana stack to monitor `scanner_analyzer_bench_*` metrics and alert on regressions.
## Publishing workflow
1. CI (or engineers running locally) execute:
```bash
dotnet run \
--project bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj \
-- \
--repo-root . \
--out bench/Scanner.Analyzers/baseline.csv \
--json out/bench/scanner-analyzers/latest.json \
--prom out/bench/scanner-analyzers/latest.prom \
--commit "$(git rev-parse HEAD)" \
--environment "${CI_ENVIRONMENT_NAME:-local}"
```
2. Publish the artefacts (`baseline.csv`, `latest.json`, `latest.prom`) to `bench-artifacts/<date>/`.
3. Promtail (or the CI job) pushes `latest.prom` into Prometheus; JSON lands in long-term storage for workbook snapshots.
4. The harness exits non-zero if:
- `max_ms` for any scenario breaches its configured threshold; or
- `max_ms` regresses ≥20% versus `baseline.csv`.
## Grafana dashboard
- Import `docs/ops/scanner-analyzers-grafana-dashboard.json`.
- Point the template variable `datasource` to the Prometheus instance ingesting `scanner_analyzer_bench_*` metrics.
- Panels:
- **Max Duration (ms)** compares live runs vs baseline.
- **Regression Ratio vs Limit** plots `(max / baseline_max - 1) * 100`.
- **Breached Scenarios** stat panel sourced from `scanner_analyzer_bench_regression_breached`.
## Alerting & on-call response
- **Primary alert**: fire when `scanner_analyzer_bench_regression_ratio{scenario=~".+"} >= 1.20` for 2 consecutive samples (10min default). Suggested PromQL:
```
max_over_time(scanner_analyzer_bench_regression_ratio[10m]) >= 1.20
```
- Suppress duplicates using the `scenario` label.
- Pager payload should include `scenario`, `max_ms`, `baseline_max_ms`, and `commit`.
- Immediate triage steps:
1. Check `latest.json` artefact for the failing scenario confirm commit and environment.
2. Re-run the harness with `--captured-at` and `--baseline` pointing at the last known good CSV to verify determinism.
3. If regression persists, open an incident ticket tagged `scanner-analyzer-perf` and page the owning language guild.
4. Roll back the offending change or update the baseline after sign-off from the guild lead and Perf captain.
Document the outcome in `docs/12_PERFORMANCE_WORKBOOK.md` (section 8) so trendlines reflect any accepted regressions.

View File

@@ -0,0 +1,88 @@
# Scanner Artifact Store Migration (MinIO → RustFS)
## Overview
Sprint 11 introduces **RustFS** as the default artifact store for the Scanner plane. Existing
deployments running MinIO (or any S3-compatible backend) must migrate stored SBOM artefacts to RustFS
before switching the Scanner hosts to `scanner.artifactStore.driver = "rustfs"`.
This runbook covers the recommended migration workflow and validation steps.
## Prerequisites
- RustFS service deployed and reachable from the Scanner control plane (`http(s)://rustfs:8080`).
- Existing MinIO/S3 credentials with read access to the current bucket.
- CLI environment with the StellaOps source tree (for the migration tool) and `dotnet 10` SDK.
- Maintenance window sized to copy all artefacts (migration is read-only on the source bucket).
## 1. Snapshot source bucket (optional but recommended)
If the MinIO deployment offers versioning or snapshots, take one before migrating. For non-versioned
deployments, capture an external backup (e.g., `mc mirror` to offline storage).
## 2. Dry-run the migrator
```
dotnet run --project tools/RustFsMigrator -- \
--s3-bucket scanner-artifacts \
--s3-endpoint http://stellaops-minio:9000 \
--s3-access-key stellaops \
--s3-secret-key dev-minio-secret \
--rustfs-endpoint http://stellaops-rustfs:8080 \
--rustfs-bucket scanner-artifacts \
--prefix scanner/ \
--dry-run
```
The dry-run enumerates keys and reports the object count without writing to RustFS. Use this to
estimate migration time.
## 3. Execute migration
Remove the `--dry-run` flag to copy data. Optional flags:
- `--immutable` mark all migrated objects as immutable (`X-RustFS-Immutable`).
- `--retain-days 365` request retention (in days) via `X-RustFS-Retain-Seconds`.
- `--rustfs-api-key-header` / `--rustfs-api-key` provide auth headers when RustFS is protected.
The tool streams each object from S3 and performs an idempotent `PUT` to RustFS preserving the key
structure (e.g., `scanner/layers/<sha256>/sbom.cdx.json.zst`).
## 4. Verify sample objects
Pick a handful of SBOM digests and confirm:
1. `GET /api/v1/buckets/<bucket>/objects/<key>` returns the expected payload (size + SHA-256).
2. Scanner WebService configured with `scanner.artifactStore.driver = "rustfs"` can fetch the same
artefacts (Smoke test: `GET /api/v1/scanner/sboms/<digest>?format=cdx-json`).
## 5. Switch Scanner hosts
Update configuration (Helm/Compose/environment) to set:
```
scanner:
artifactStore:
driver: rustfs
endpoint: http://stellaops-rustfs:8080
bucket: scanner-artifacts
timeoutSeconds: 30
```
Redeploy Scanner WebService and Worker. Monitor logs for `RustFS` upload/download messages and
Prometheus scrape (`rustfs_requests_total`).
## 6. Cleanup legacy MinIO (optional)
After a complete migration and validation period, decommission the MinIO bucket or repurpose it for
other components (Concelier still supports S3). Ensure backups reference RustFS snapshots going
forward.
## Troubleshooting
- **Uploads fail (HTTP 4xx/5xx):** Check RustFS logs and confirm API key headers. Re-run the migrator
for the affected keys.
- **Missing objects post-cutover:** Re-run the migrator with the specific `--prefix`. The tool is
idempotent and safely overwrites existing objects.
- **Performance tuning:** Run multiple instances of the migrator with disjoint prefixes if needed; the
RustFS API is stateless and supports parallel PUTs.

View File

@@ -0,0 +1,314 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.stella-ops.org/policy/policy-preview-sample@1.json",
"title": "Policy Preview Sample",
"type": "object",
"additionalProperties": false,
"required": [
"previewRequest",
"previewResponse"
],
"properties": {
"previewRequest": {
"type": "object",
"additionalProperties": false,
"required": [
"imageDigest",
"findings"
],
"properties": {
"imageDigest": {
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$"
},
"findings": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/finding"
}
},
"baseline": {
"type": "array",
"items": {
"$ref": "#/$defs/baselineVerdict"
}
}
}
},
"previewResponse": {
"type": "object",
"additionalProperties": false,
"required": [
"success",
"policyDigest",
"revisionId",
"changed",
"diffs",
"issues"
],
"properties": {
"success": {
"type": "boolean"
},
"policyDigest": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"revisionId": {
"type": "string"
},
"changed": {
"type": "integer",
"minimum": 0
},
"diffs": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"findingId",
"baseline",
"projected",
"changed"
],
"properties": {
"findingId": {
"type": "string"
},
"baseline": {
"$ref": "#/$defs/baselineVerdict"
},
"projected": {
"$ref": "#/$defs/projectedVerdict"
},
"changed": {
"type": "boolean"
}
}
}
},
"issues": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"code",
"message",
"severity",
"path"
],
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"severity": {
"type": "string"
},
"path": {
"type": "string"
}
}
}
}
}
}
},
"$defs": {
"finding": {
"type": "object",
"required": [
"id",
"severity",
"source"
],
"properties": {
"id": {
"type": "string"
},
"severity": {
"type": "string"
},
"source": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": true
},
"inputs": {
"type": "object",
"minProperties": 1,
"propertyNames": {
"type": "string",
"maxLength": 64
},
"additionalProperties": {
"type": "number"
}
},
"baselineVerdict": {
"type": "object",
"additionalProperties": false,
"required": [
"findingId",
"status",
"configVersion",
"score"
],
"properties": {
"findingId": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"Pass",
"Blocked",
"Warned",
"Ignored",
"Deferred",
"Escalated",
"RequiresVex"
]
},
"ruleName": {
"type": [
"string",
"null"
]
},
"ruleAction": {
"type": [
"string",
"null"
]
},
"notes": {
"type": [
"string",
"null"
]
},
"score": {
"type": "number"
},
"configVersion": {
"type": "string"
},
"inputs": {
"$ref": "#/$defs/inputs"
},
"quietedBy": {
"type": [
"string",
"null"
]
},
"quiet": {
"type": "boolean"
},
"unknownConfidence": {
"type": "number",
"minimum": 0
},
"confidenceBand": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"unspecified"
]
},
"unknownAgeDays": {
"type": "number",
"minimum": 0
},
"sourceTrust": {
"type": "string"
},
"reachability": {
"type": "string",
"enum": [
"unknown",
"runtime",
"entrypoint",
"direct",
"indirect",
"unreachable"
]
}
}
},
"projectedVerdict": {
"allOf": [
{
"$ref": "#/$defs/baselineVerdict"
},
{
"type": "object",
"required": [
"ruleName",
"ruleAction",
"unknownConfidence",
"confidenceBand",
"unknownAgeDays",
"sourceTrust",
"reachability"
],
"properties": {
"ruleName": {
"type": "string"
},
"ruleAction": {
"type": "string"
},
"unknownConfidence": {
"type": "number",
"minimum": 0
},
"confidenceBand": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"unspecified"
]
},
"unknownAgeDays": {
"type": "number",
"minimum": 0
},
"sourceTrust": {
"type": "string"
},
"reachability": {
"type": "string",
"enum": [
"unknown",
"runtime",
"entrypoint",
"direct",
"indirect",
"unreachable"
]
}
}
}
]
}
}
}

View File

@@ -0,0 +1,396 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.stella-ops.org/policy/policy-report-sample@1.json",
"title": "Policy Report Sample",
"type": "object",
"additionalProperties": false,
"required": [
"reportRequest",
"reportResponse"
],
"properties": {
"reportRequest": {
"type": "object",
"additionalProperties": false,
"required": [
"imageDigest",
"findings"
],
"properties": {
"imageDigest": {
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$"
},
"findings": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/finding"
}
},
"baseline": {
"type": "array",
"items": {
"$ref": "#/$defs/baselineVerdict"
}
}
}
},
"reportResponse": {
"type": "object",
"additionalProperties": false,
"required": [
"report",
"dsse"
],
"properties": {
"report": {
"type": "object",
"additionalProperties": false,
"required": [
"reportId",
"imageDigest",
"generatedAt",
"verdict",
"policy",
"summary",
"verdicts",
"issues"
],
"properties": {
"reportId": {
"type": "string"
},
"imageDigest": {
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$"
},
"generatedAt": {
"type": "string",
"format": "date-time"
},
"verdict": {
"type": "string"
},
"policy": {
"type": "object",
"additionalProperties": false,
"required": [
"revisionId",
"digest"
],
"properties": {
"revisionId": {
"type": "string"
},
"digest": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
}
}
},
"summary": {
"type": "object",
"additionalProperties": false,
"required": [
"total",
"blocked",
"warned",
"ignored",
"quieted"
],
"properties": {
"total": {
"type": "integer",
"minimum": 0
},
"blocked": {
"type": "integer",
"minimum": 0
},
"warned": {
"type": "integer",
"minimum": 0
},
"ignored": {
"type": "integer",
"minimum": 0
},
"quieted": {
"type": "integer",
"minimum": 0
}
}
},
"verdicts": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/projectedVerdict"
}
},
"issues": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"code",
"message",
"severity",
"path"
],
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"severity": {
"type": "string"
},
"path": {
"type": "string"
}
}
}
}
}
},
"dsse": {
"type": "object",
"additionalProperties": false,
"required": [
"payloadType",
"payload",
"signatures"
],
"properties": {
"payloadType": {
"type": "string"
},
"payload": {
"type": "string"
},
"signatures": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"keyId",
"algorithm",
"signature"
],
"properties": {
"keyId": {
"type": "string"
},
"algorithm": {
"type": "string"
},
"signature": {
"type": "string"
}
}
}
}
}
}
}
}
},
"$defs": {
"finding": {
"type": "object",
"required": [
"id",
"severity",
"source"
],
"properties": {
"id": {
"type": "string"
},
"severity": {
"type": "string"
},
"source": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": true
},
"inputs": {
"type": "object",
"minProperties": 1,
"propertyNames": {
"type": "string",
"maxLength": 64
},
"additionalProperties": {
"type": "number"
}
},
"baselineVerdict": {
"type": "object",
"additionalProperties": false,
"required": [
"findingId",
"status",
"configVersion",
"score"
],
"properties": {
"findingId": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"Pass",
"Blocked",
"Warned",
"Ignored",
"Deferred",
"Escalated",
"RequiresVex"
]
},
"ruleName": {
"type": [
"string",
"null"
]
},
"ruleAction": {
"type": [
"string",
"null"
]
},
"notes": {
"type": [
"string",
"null"
]
},
"score": {
"type": "number"
},
"configVersion": {
"type": "string"
},
"inputs": {
"$ref": "#/$defs/inputs"
},
"quietedBy": {
"type": [
"string",
"null"
]
},
"quiet": {
"type": "boolean"
},
"unknownConfidence": {
"type": "number",
"minimum": 0
},
"confidenceBand": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"unspecified"
]
},
"unknownAgeDays": {
"type": "number",
"minimum": 0
},
"sourceTrust": {
"type": "string"
},
"reachability": {
"type": "string",
"enum": [
"unknown",
"runtime",
"entrypoint",
"direct",
"indirect",
"unreachable"
]
}
}
},
"projectedVerdict": {
"allOf": [
{
"$ref": "#/$defs/baselineVerdict"
},
{
"type": "object",
"required": [
"ruleName",
"ruleAction",
"unknownConfidence",
"confidenceBand",
"unknownAgeDays",
"sourceTrust",
"reachability"
],
"properties": {
"ruleName": {
"type": "string"
},
"ruleAction": {
"type": "string"
},
"unknownConfidence": {
"type": "number",
"minimum": 0
},
"confidenceBand": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"unspecified"
]
},
"unknownAgeDays": {
"type": "number",
"minimum": 0
},
"sourceTrust": {
"type": "string"
},
"reachability": {
"type": "string",
"enum": [
"unknown",
"runtime",
"entrypoint",
"direct",
"indirect",
"unreachable"
]
}
}
}
]
}
}
}

View File

@@ -151,7 +151,7 @@ security:
requireChainValidation: true
rotationGrace: "00:15:00"
enforceForAudiences:
- "signer"
- "signer" # Requests for these audiences force mTLS sender constraints
allowedSanTypes:
- "dns"
- "uri"

View File

@@ -6,8 +6,9 @@
| DEVOPS-SCANNER-09-204 | DONE (2025-10-21) | DevOps Guild, Scanner WebService Guild | SCANNER-EVENTS-15-201 | Surface `SCANNER__EVENTS__*` environment variables across docker-compose (dev/stage/airgap) and Helm values, defaulting to share the Redis queue DSN. | Compose/Helm configs ship enabled Redis event publishing with documented overrides; lint jobs updated; docs cross-link to new knobs. |
| DEVOPS-SCANNER-09-205 | DONE (2025-10-21) | DevOps Guild, Notify Guild | DEVOPS-SCANNER-09-204 | Add Notify smoke stage that tails the Redis stream and asserts `scanner.report.ready`/`scanner.scan.completed` reach Notify WebService in staging. | CI job reads Redis stream during scanner smoke deploy, confirms Notify ingestion via API, alerts on failure. |
| DEVOPS-PERF-10-001 | DONE | DevOps Guild | BENCH-SCANNER-10-001 | Add perf smoke job (SBOM compose <5s target) to CI. | CI job runs sample build verifying <5s; alerts configured. |
| DEVOPS-PERF-10-002 | TODO | DevOps Guild | BENCH-SCANNER-10-002 | Publish analyzer bench metrics to Grafana/perf workbook and alarm on 20% regressions. | CI exports JSON for dashboards; Grafana panel wired; Ops on-call doc updated with alert hook. |
| DEVOPS-REL-14-001 | TODO | DevOps Guild | SIGNER-API-11-101, ATTESTOR-API-11-201 | Deterministic build/release pipeline with SBOM/provenance, signing, manifest generation. | CI pipeline produces signed images + SBOM/attestations, manifests published with verified hashes, docs updated. |
| DEVOPS-PERF-10-002 | DONE (2025-10-23) | DevOps Guild | BENCH-SCANNER-10-002 | Publish analyzer bench metrics to Grafana/perf workbook and alarm on 20% regressions. | CI exports JSON for dashboards; Grafana panel wired; Ops on-call doc updated with alert hook. |
| DEVOPS-REL-14-001 | TODO | DevOps Guild | SIGNER-API-11-101, ATTESTOR-API-11-201 | Deterministic build/release pipeline with SBOM/provenance, signing, manifest generation. | CI pipeline produces signed images + SBOM/attestations, manifests published with verified hashes, docs updated. |
| DEVOPS-REL-14-004 | TODO | DevOps Guild, Scanner Guild | DEVOPS-REL-14-001, SCANNER-ANALYZERS-LANG-10-309P | Extend release/offline smoke jobs to exercise the Python analyzer plug-in (warm/cold scans, determinism, signature checks). | Release/Offline pipelines run Python analyzer smoke suite; alerts hooked; docs updated with new coverage matrix. |
| DEVOPS-REL-17-002 | TODO | DevOps Guild | DEVOPS-REL-14-001, SCANNER-EMIT-17-701 | Persist stripped-debug artifacts organised by GNU build-id and bundle them into release/offline kits with checksum manifests. | CI job writes `.debug` files under `artifacts/debug/.build-id/`, manifest + checksums published, offline kit includes cache, smoke job proves symbol lookup via build-id. |
| DEVOPS-MIRROR-08-001 | DONE (2025-10-19) | DevOps Guild | DEVOPS-REL-14-001 | Stand up managed mirror profiles for `*.stella-ops.org` (Concelier/Excititor), including Helm/Compose overlays, multi-tenant secrets, CDN caching, and sync documentation. | Infra overlays committed, CI smoke deploy hits mirror endpoints, runbooks published for downstream sync and quota management. |
| DEVOPS-SEC-10-301 | DONE (2025-10-20) | DevOps Guild | Wave 0A complete | Address NU1902/NU1903 advisories for `MongoDB.Driver` 2.12.0 and `SharpCompress` 0.23.0 surfaced during scanner cache and worker test runs. | Dependencies bumped to patched releases, audit logs free of NU1902/NU1903 warnings, regression tests green, change log documents upgrade guidance. |

View File

@@ -5,3 +5,4 @@
| DEVOPS-OFFLINE-14-002 | TODO | Offline Kit Guild | DEVOPS-REL-14-001 | Build offline kit packaging workflow (artifact bundling, manifest generation, signature verification). | Offline tarball generated with manifest + checksums + signatures; import script verifies integrity; docs updated. |
| DEVOPS-OFFLINE-18-003 | TODO | Offline Kit Guild, UX Specialist | DEVOPS-OFFLINE-14-002 | Capture Angular workspace npm cache + Chromium bundle in Offline Kit (`out/offline-kit/web/`) and document refresh cadence. | Web cache directory added to kit manifest; documentation updated with `npm run ci:install`/`verify:chromium` workflow; periodic refresh SOP recorded in Offline Kit guide. |
| DEVOPS-OFFLINE-18-004 | DONE (2025-10-22) | Offline Kit Guild, Scanner Guild | DEVOPS-OFFLINE-18-003, SCANNER-ANALYZERS-LANG-10-309G | Rebuild Offline Kit bundle with Go analyzer plug-in and updated manifest/signature set. | Kit tarball includes Go analyzer artifacts; manifest/signature refreshed; verification steps executed and logged; docs updated with new bundle version. |
| DEVOPS-OFFLINE-18-005 | TODO | Offline Kit Guild, Scanner Guild | DEVOPS-REL-14-004, SCANNER-ANALYZERS-LANG-10-309P | Repackage Offline Kit with Python analyzer plug-in artefacts and refreshed manifest/signature set. | Kit tarball includes Python analyzer DLL/PDB/manifest; signature + manifest updated; Offline Kit guide references Python coverage; smoke import validated. |

View File

@@ -3,4 +3,4 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------|
| SAMPLES-10-001 | DONE | Samples Guild, Scanner Team | SCANNER-EMIT-10-605 | Curate sample images (nginx, alpine+busybox, distroless+go, .NET AOT, python venv, npm monorepo) with expected SBOM/BOM-Index sidecars. | Samples committed under `samples/`; golden SBOM/BOM-Index files present; documented usage. |
| SAMPLES-13-004 | TODO | Samples Guild, Policy Guild | POLICY-CORE-09-006, UI-POLICY-13-007 | Add policy preview/report fixtures showing confidence bands and unknown-age tags. | Confidence sample (`samples/policy/policy-preview-unknown.json`) reviewed, documented usage in UI dev guide, ajv validation hook updated. |
| SAMPLES-13-004 | DONE (2025-10-23) | Samples Guild, Policy Guild | POLICY-CORE-09-006, UI-POLICY-13-007 | Add policy preview/report fixtures showing confidence bands and unknown-age tags. | Confidence sample (`samples/policy/policy-preview-unknown.json`) reviewed, documented usage in UI dev guide, ajv validation hook updated. |

View File

@@ -1,98 +1,151 @@
{
"previewRequest": {
"imageDigest": "sha256:7dbe0c9a5d4f1c8184007e9d94dbe55928f8a2db5ab9c1c2d4a2f7bbcdfe1234",
"findings": [
{
"id": "library:pkg/openssl@1.1.1w",
"severity": "Unknown",
"source": "NVD",
"tags": [
"trust:vendor",
"reachability:unknown",
"unknown-age-days:5"
]
},
{
"id": "library:pkg/zlib@1.3.1",
"severity": "High",
"source": "NVD",
"tags": [
"state:unknown",
"reachability:runtime",
"unknown-since:2025-10-10T00:00:00Z",
"observed-at:2025-10-19T12:00:00Z"
]
}
]
},
"previewResponse": {
"success": true,
"policyDigest": "8a0f72f8dc5c51c46991db3bba34e9b3c0c8e944a7a6d0a9c29a9aa6b8439876",
"revisionId": "rev-42",
"changed": 2,
"diffs": [
{
"findingId": "library:pkg/openssl@1.1.1w",
"baseline": {
"findingId": "library:pkg/openssl@1.1.1w",
"status": "Pass",
"score": 0,
"configVersion": "1.0"
},
"projected": {
"findingId": "library:pkg/openssl@1.1.1w",
"status": "Blocked",
"ruleName": "Block vendor unknowns",
"ruleAction": "block",
"score": 19.5,
"configVersion": "1.0",
"inputs": {
"severityWeight": 50,
"trustWeight": 0.65,
"reachabilityWeight": 0.6,
"baseScore": 19.5,
"trustWeight.vendor": 0.65,
"reachability.unknown": 0.6,
"unknownConfidence": 0.55,
"unknownAgeDays": 5
},
"unknownConfidence": 0.55,
"confidenceBand": "medium",
"unknownAgeDays": 5
},
"changed": true
},
{
"findingId": "library:pkg/zlib@1.3.1",
"baseline": {
"findingId": "library:pkg/zlib@1.3.1",
"status": "Pass",
"score": 0,
"configVersion": "1.0"
},
"projected": {
"findingId": "library:pkg/zlib@1.3.1",
"status": "Warned",
"ruleName": "Runtime mitigation required",
"ruleAction": "warn",
"score": 33.75,
"configVersion": "1.0",
"inputs": {
"severityWeight": 75,
"trustWeight": 1,
"reachabilityWeight": 0.45,
"baseScore": 33.75,
"reachability.runtime": 0.45,
"warnPenalty": 15,
"unknownConfidence": 0.35,
"unknownAgeDays": 9
},
"unknownConfidence": 0.35,
"confidenceBand": "medium",
"unknownAgeDays": 9
},
"changed": true
}
]
}
}
{
"previewRequest": {
"imageDigest": "sha256:7dbe0c9a5d4f1c8184007e9d94dbe55928f8a2db5ab9c1c2d4a2f7bbcdfe1234",
"findings": [
{
"id": "library:pkg/openssl@1.1.1w",
"severity": "Unknown",
"source": "NVD",
"tags": [
"trust:vendor",
"reachability:unknown",
"unknown-age-days:5"
]
},
{
"id": "library:pkg/zlib@1.3.1",
"severity": "High",
"source": "NVD",
"tags": [
"state:unknown",
"reachability:runtime",
"unknown-since:2025-10-10T00:00:00Z",
"observed-at:2025-10-19T12:00:00Z"
]
}
],
"baseline": [
{
"findingId": "library:pkg/openssl@1.1.1w",
"status": "Pass",
"score": 0,
"configVersion": "1.0",
"inputs": {
"severityWeight": 25,
"trustWeight": 1,
"reachabilityWeight": 0.45,
"baseScore": 11.25
},
"quiet": false
},
{
"findingId": "library:pkg/zlib@1.3.1",
"status": "Pass",
"score": 0,
"configVersion": "1.0",
"inputs": {
"severityWeight": 75,
"trustWeight": 1,
"reachabilityWeight": 0.45,
"baseScore": 33.75
},
"quiet": false
}
]
},
"previewResponse": {
"success": true,
"policyDigest": "8a0f72f8dc5c51c46991db3bba34e9b3c0c8e944a7a6d0a9c29a9aa6b8439876",
"revisionId": "rev-42",
"changed": 2,
"diffs": [
{
"findingId": "library:pkg/openssl@1.1.1w",
"baseline": {
"findingId": "library:pkg/openssl@1.1.1w",
"status": "Pass",
"score": 0,
"configVersion": "1.0",
"inputs": {
"severityWeight": 25,
"trustWeight": 1,
"reachabilityWeight": 0.45,
"baseScore": 11.25
},
"quiet": false
},
"projected": {
"findingId": "library:pkg/openssl@1.1.1w",
"status": "Blocked",
"ruleName": "Block vendor unknowns",
"ruleAction": "block",
"notes": "Unknown vendor telemetry — medium confidence band.",
"score": 19.5,
"configVersion": "1.0",
"inputs": {
"severityWeight": 50,
"trustWeight": 0.65,
"reachabilityWeight": 0.6,
"baseScore": 19.5,
"trustWeight.vendor": 0.65,
"reachability.unknown": 0.6,
"unknownConfidence": 0.55,
"unknownAgeDays": 5
},
"quietedBy": null,
"quiet": false,
"unknownConfidence": 0.55,
"confidenceBand": "medium",
"unknownAgeDays": 5,
"sourceTrust": "vendor",
"reachability": "unknown"
},
"changed": true
},
{
"findingId": "library:pkg/zlib@1.3.1",
"baseline": {
"findingId": "library:pkg/zlib@1.3.1",
"status": "Pass",
"score": 0,
"configVersion": "1.0",
"inputs": {
"severityWeight": 75,
"trustWeight": 1,
"reachabilityWeight": 0.45,
"baseScore": 33.75
},
"quiet": false
},
"projected": {
"findingId": "library:pkg/zlib@1.3.1",
"status": "Warned",
"ruleName": "Runtime mitigation required",
"ruleAction": "warn",
"notes": "Runtime reachable unknown — mitigation window required.",
"score": 18.75,
"configVersion": "1.0",
"inputs": {
"severityWeight": 75,
"trustWeight": 1,
"reachabilityWeight": 0.45,
"baseScore": 33.75,
"reachability.runtime": 0.45,
"warnPenalty": 15,
"unknownConfidence": 0.35,
"unknownAgeDays": 13
},
"quietedBy": null,
"quiet": false,
"unknownConfidence": 0.35,
"confidenceBand": "medium",
"unknownAgeDays": 13,
"sourceTrust": "NVD",
"reachability": "runtime"
},
"changed": true
}
],
"issues": []
}
}

View File

@@ -0,0 +1,141 @@
{
"reportRequest": {
"imageDigest": "sha256:7dbe0c9a5d4f1c8184007e9d94dbe55928f8a2db5ab9c1c2d4a2f7bbcdfe1234",
"findings": [
{
"id": "library:pkg/openssl@1.1.1w",
"severity": "Unknown",
"source": "NVD",
"tags": [
"trust:vendor",
"reachability:unknown",
"unknown-age-days:5"
]
},
{
"id": "library:pkg/zlib@1.3.1",
"severity": "High",
"source": "NVD",
"tags": [
"state:unknown",
"reachability:runtime",
"unknown-since:2025-10-10T00:00:00Z",
"observed-at:2025-10-19T12:00:00Z"
]
}
],
"baseline": [
{
"findingId": "library:pkg/openssl@1.1.1w",
"status": "Pass",
"score": 0,
"configVersion": "1.0",
"inputs": {
"severityWeight": 25,
"trustWeight": 1,
"reachabilityWeight": 0.45,
"baseScore": 11.25
},
"quiet": false
},
{
"findingId": "library:pkg/zlib@1.3.1",
"status": "Pass",
"score": 0,
"configVersion": "1.0",
"inputs": {
"severityWeight": 75,
"trustWeight": 1,
"reachabilityWeight": 0.45,
"baseScore": 33.75
},
"quiet": false
}
]
},
"reportResponse": {
"report": {
"reportId": "report-9f8cde21aab54321",
"imageDigest": "sha256:7dbe0c9a5d4f1c8184007e9d94dbe55928f8a2db5ab9c1c2d4a2f7bbcdfe1234",
"generatedAt": "2025-10-23T15:32:22Z",
"verdict": "blocked",
"policy": {
"revisionId": "rev-42",
"digest": "8a0f72f8dc5c51c46991db3bba34e9b3c0c8e944a7a6d0a9c29a9aa6b8439876"
},
"summary": {
"total": 2,
"blocked": 1,
"warned": 1,
"ignored": 0,
"quieted": 0
},
"verdicts": [
{
"findingId": "library:pkg/openssl@1.1.1w",
"status": "Blocked",
"ruleName": "Block vendor unknowns",
"ruleAction": "block",
"notes": "Unknown vendor telemetry — medium confidence band.",
"score": 19.5,
"configVersion": "1.0",
"inputs": {
"severityWeight": 50,
"trustWeight": 0.65,
"reachabilityWeight": 0.6,
"baseScore": 19.5,
"trustWeight.vendor": 0.65,
"reachability.unknown": 0.6,
"unknownConfidence": 0.55,
"unknownAgeDays": 5
},
"quietedBy": null,
"quiet": false,
"unknownConfidence": 0.55,
"confidenceBand": "medium",
"unknownAgeDays": 5,
"sourceTrust": "vendor",
"reachability": "unknown"
},
{
"findingId": "library:pkg/zlib@1.3.1",
"status": "Warned",
"ruleName": "Runtime mitigation required",
"ruleAction": "warn",
"notes": "Runtime reachable unknown — mitigation window required.",
"score": 18.75,
"configVersion": "1.0",
"inputs": {
"severityWeight": 75,
"trustWeight": 1,
"reachabilityWeight": 0.45,
"baseScore": 33.75,
"reachability.runtime": 0.45,
"warnPenalty": 15,
"unknownConfidence": 0.35,
"unknownAgeDays": 13
},
"quietedBy": null,
"quiet": false,
"unknownConfidence": 0.35,
"confidenceBand": "medium",
"unknownAgeDays": 13,
"sourceTrust": "NVD",
"reachability": "runtime"
}
],
"issues": []
},
"dsse": {
"payloadType": "application/vnd.stellaops.report+json",
"payload": "eyJyZXBvcnQiOnsicmVwb3J0SWQiOiJyZXBvcnQtOWY4Y2RlMjFhYWI1NDMyMSJ9fQ==",
"signatures": [
{
"keyId": "scanner-report-signing",
"algorithm": "hs256",
"signature": "MEQCIGHscnJ2bm9wYXlsb2FkZXIAIjANBgkqhkiG9w0BAQsFAAOCAQEASmFja3Nvbk1ldGE="
}
]
}
}
}

View File

@@ -50,6 +50,7 @@ public class ClientCredentialsHandlersTests
allowedScopes: "jobs:read");
var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
var options = TestHelpers.CreateAuthorityOptions();
var handler = new ValidateClientCredentialsHandler(
new TestClientStore(clientDocument),
registry,
@@ -59,6 +60,7 @@ public class ClientCredentialsHandlersTests
TimeProvider.System,
new NoopCertificateValidator(),
new HttpContextAccessor(),
options,
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:write");
@@ -80,6 +82,7 @@ public class ClientCredentialsHandlersTests
allowedScopes: "jobs:read jobs:trigger");
var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
var options = TestHelpers.CreateAuthorityOptions();
var handler = new ValidateClientCredentialsHandler(
new TestClientStore(clientDocument),
registry,
@@ -89,6 +92,7 @@ public class ClientCredentialsHandlersTests
TimeProvider.System,
new NoopCertificateValidator(),
new HttpContextAccessor(),
options,
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
@@ -114,6 +118,7 @@ public class ClientCredentialsHandlersTests
var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
var sink = new TestAuthEventSink();
var options = TestHelpers.CreateAuthorityOptions();
var handler = new ValidateClientCredentialsHandler(
new TestClientStore(clientDocument),
registry,
@@ -123,6 +128,7 @@ public class ClientCredentialsHandlersTests
TimeProvider.System,
new NoopCertificateValidator(),
new HttpContextAccessor(),
options,
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
@@ -139,12 +145,11 @@ public class ClientCredentialsHandlersTests
[Fact]
public async Task ValidateDpopProof_AllowsSenderConstrainedClient()
{
var options = new StellaOpsAuthorityOptions
var options = TestHelpers.CreateAuthorityOptions(opts =>
{
Issuer = new Uri("https://authority.test")
};
options.Security.SenderConstraints.Dpop.Enabled = true;
options.Security.SenderConstraints.Dpop.Nonce.Enabled = false;
opts.Security.SenderConstraints.Dpop.Enabled = true;
opts.Security.SenderConstraints.Dpop.Nonce.Enabled = false;
});
var clientDocument = CreateClient(
secret: "s3cr3t!",
@@ -214,6 +219,7 @@ public class ClientCredentialsHandlersTests
TimeProvider.System,
new NoopCertificateValidator(),
new HttpContextAccessor(),
options,
NullLogger<ValidateClientCredentialsHandler>.Instance);
await validateHandler.HandleAsync(validateContext);
@@ -389,6 +395,7 @@ public class ClientCredentialsHandlersTests
TimeProvider.System,
validator,
httpContextAccessor,
options,
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
@@ -435,6 +442,7 @@ public class ClientCredentialsHandlersTests
TimeProvider.System,
validator,
httpContextAccessor,
options,
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
@@ -446,6 +454,94 @@ public class ClientCredentialsHandlersTests
Assert.Equal(OpenIddictConstants.Errors.InvalidClient, context.Error);
}
[Fact]
public async Task ValidateClientCredentials_Rejects_WhenAudienceRequiresMtlsButClientConfiguredForDpop()
{
var options = TestHelpers.CreateAuthorityOptions(opts =>
{
opts.Security.SenderConstraints.Mtls.Enabled = true;
opts.Security.SenderConstraints.Mtls.EnforceForAudiences.Clear();
opts.Security.SenderConstraints.Mtls.EnforceForAudiences.Add("signer");
});
var clientDocument = CreateClient(
secret: "s3cr3t!",
allowedGrantTypes: "client_credentials",
allowedScopes: "jobs:read",
allowedAudiences: "signer");
clientDocument.SenderConstraint = AuthoritySenderConstraintKinds.Dpop;
clientDocument.Properties[AuthorityClientMetadataKeys.SenderConstraint] = AuthoritySenderConstraintKinds.Dpop;
var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
var handler = new ValidateClientCredentialsHandler(
new TestClientStore(clientDocument),
registry,
TestActivitySource,
new TestAuthEventSink(),
new TestRateLimiterMetadataAccessor(),
TimeProvider.System,
new NoopCertificateValidator(),
new HttpContextAccessor(),
options,
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
Assert.True(context.IsRejected);
Assert.Equal(OpenIddictConstants.Errors.InvalidClient, context.Error);
Assert.Equal("Requested audiences require mutual TLS sender constraint.", context.ErrorDescription);
}
[Fact]
public async Task ValidateClientCredentials_RequiresMtlsWhenAudienceMatchesEnforcement()
{
var options = TestHelpers.CreateAuthorityOptions(opts =>
{
opts.Security.SenderConstraints.Mtls.Enabled = true;
opts.Security.SenderConstraints.Mtls.EnforceForAudiences.Clear();
opts.Security.SenderConstraints.Mtls.EnforceForAudiences.Add("signer");
});
var clientDocument = CreateClient(
secret: "s3cr3t!",
allowedGrantTypes: "client_credentials",
allowedScopes: "jobs:read",
allowedAudiences: "signer");
clientDocument.CertificateBindings.Add(new AuthorityClientCertificateBinding
{
Thumbprint = "DEADBEEF"
});
var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
var certificateValidator = new RecordingCertificateValidator();
var httpContextAccessor = new HttpContextAccessor { HttpContext = new DefaultHttpContext() };
var handler = new ValidateClientCredentialsHandler(
new TestClientStore(clientDocument),
registry,
TestActivitySource,
new TestAuthEventSink(),
new TestRateLimiterMetadataAccessor(),
TimeProvider.System,
certificateValidator,
httpContextAccessor,
options,
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
await handler.HandleAsync(context);
Assert.True(context.IsRejected);
Assert.Equal(OpenIddictConstants.Errors.InvalidClient, context.Error);
Assert.Equal("client_certificate_required", context.ErrorDescription);
Assert.True(certificateValidator.Invoked);
}
[Fact]
public async Task HandleClientCredentials_PersistsTokenAndEnrichesClaims()
{
@@ -462,6 +558,7 @@ public class ClientCredentialsHandlersTests
var sessionAccessor = new NullMongoSessionAccessor();
var authSink = new TestAuthEventSink();
var metadataAccessor = new TestRateLimiterMetadataAccessor();
var options = TestHelpers.CreateAuthorityOptions();
var validateHandler = new ValidateClientCredentialsHandler(
new TestClientStore(clientDocument),
registry,
@@ -471,6 +568,7 @@ public class ClientCredentialsHandlersTests
TimeProvider.System,
new NoopCertificateValidator(),
new HttpContextAccessor(),
options,
NullLogger<ValidateClientCredentialsHandler>.Instance);
var transaction = CreateTokenTransaction(clientDocument.ClientId, secret: null, scope: "jobs:trigger");
@@ -828,6 +926,82 @@ public class AuthorityClientCertificateValidatorTests
Assert.True(result.Succeeded);
Assert.Equal(thumbprint, result.HexThumbprint);
}
[Fact]
public async Task ValidateAsync_Rejects_WhenBindingSubjectMismatch()
{
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.test")
};
options.Security.SenderConstraints.Mtls.Enabled = true;
options.Security.SenderConstraints.Mtls.RequireChainValidation = false;
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Storage.ConnectionString = "mongodb://localhost/test";
using var rsa = RSA.Create(2048);
var request = new CertificateRequest("CN=mtls-client", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var sanBuilder = new SubjectAlternativeNameBuilder();
sanBuilder.AddDnsName("client.mtls.test");
request.CertificateExtensions.Add(sanBuilder.Build());
using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddMinutes(5));
var clientDocument = CreateClient();
clientDocument.SenderConstraint = AuthoritySenderConstraintKinds.Mtls;
clientDocument.CertificateBindings.Add(new AuthorityClientCertificateBinding
{
Thumbprint = Convert.ToHexString(certificate.GetCertHash(HashAlgorithmName.SHA256)),
Subject = "CN=different-client"
});
var httpContext = new DefaultHttpContext();
httpContext.Connection.ClientCertificate = certificate;
var validator = new AuthorityClientCertificateValidator(options, TimeProvider.System, NullLogger<AuthorityClientCertificateValidator>.Instance);
var result = await validator.ValidateAsync(httpContext, clientDocument, CancellationToken.None);
Assert.False(result.Succeeded);
Assert.Equal("certificate_binding_subject_mismatch", result.Error);
}
[Fact]
public async Task ValidateAsync_Rejects_WhenBindingSansMissing()
{
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.test")
};
options.Security.SenderConstraints.Mtls.Enabled = true;
options.Security.SenderConstraints.Mtls.RequireChainValidation = false;
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Storage.ConnectionString = "mongodb://localhost/test";
using var rsa = RSA.Create(2048);
var request = new CertificateRequest("CN=mtls-client", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var sanBuilder = new SubjectAlternativeNameBuilder();
sanBuilder.AddDnsName("client.mtls.test");
request.CertificateExtensions.Add(sanBuilder.Build());
using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddMinutes(5));
var clientDocument = CreateClient();
clientDocument.SenderConstraint = AuthoritySenderConstraintKinds.Mtls;
clientDocument.CertificateBindings.Add(new AuthorityClientCertificateBinding
{
Thumbprint = Convert.ToHexString(certificate.GetCertHash(HashAlgorithmName.SHA256)),
SubjectAlternativeNames = new List<string> { "spiffe://client" }
});
var httpContext = new DefaultHttpContext();
httpContext.Connection.ClientCertificate = certificate;
var validator = new AuthorityClientCertificateValidator(options, TimeProvider.System, NullLogger<AuthorityClientCertificateValidator>.Instance);
var result = await validator.ValidateAsync(httpContext, clientDocument, CancellationToken.None);
Assert.False(result.Succeeded);
Assert.Equal("certificate_binding_san_mismatch", result.Error);
}
}
internal sealed class TestClientStore : IAuthorityClientStore
@@ -1011,6 +1185,33 @@ internal sealed class NoopCertificateValidator : IAuthorityClientCertificateVali
}
}
internal sealed class RecordingCertificateValidator : IAuthorityClientCertificateValidator
{
public bool Invoked { get; private set; }
public ValueTask<AuthorityClientCertificateValidationResult> ValidateAsync(HttpContext httpContext, AuthorityClientDocument client, CancellationToken cancellationToken)
{
Invoked = true;
if (httpContext.Connection.ClientCertificate is null)
{
return ValueTask.FromResult(AuthorityClientCertificateValidationResult.Failure("client_certificate_required"));
}
AuthorityClientCertificateBinding binding;
if (client.CertificateBindings.Count > 0)
{
binding = client.CertificateBindings[0];
}
else
{
binding = new AuthorityClientCertificateBinding { Thumbprint = "stub" };
}
return ValueTask.FromResult(AuthorityClientCertificateValidationResult.Success("stub", binding.Thumbprint, binding));
}
}
internal sealed class NullMongoSessionAccessor : IAuthorityMongoSessionAccessor
{
public ValueTask<IClientSessionHandle> GetSessionAsync(CancellationToken cancellationToken = default)
@@ -1021,6 +1222,21 @@ internal sealed class NullMongoSessionAccessor : IAuthorityMongoSessionAccessor
internal static class TestHelpers
{
public static StellaOpsAuthorityOptions CreateAuthorityOptions(Action<StellaOpsAuthorityOptions>? configure = null)
{
var options = new StellaOpsAuthorityOptions
{
Issuer = new Uri("https://authority.test")
};
options.Signing.ActiveKeyId = "test-key";
options.Signing.KeyPath = "/tmp/test-key.pem";
options.Storage.ConnectionString = "mongodb://localhost/test";
configure?.Invoke(options);
return options;
}
public static AuthorityClientDocument CreateClient(
string? secret = "s3cr3t!",
string clientType = "confidential",

View File

@@ -64,7 +64,8 @@ public sealed class TokenPersistenceIntegrationTests
var metadataAccessor = new TestRateLimiterMetadataAccessor();
await using var scope = provider.CreateAsyncScope();
var sessionAccessor = scope.ServiceProvider.GetRequiredService<IAuthorityMongoSessionAccessor>();
var validateHandler = new ValidateClientCredentialsHandler(clientStore, registry, TestActivitySource, authSink, metadataAccessor, clock, new NoopCertificateValidator(), new HttpContextAccessor(), NullLogger<ValidateClientCredentialsHandler>.Instance);
var options = TestHelpers.CreateAuthorityOptions();
var validateHandler = new ValidateClientCredentialsHandler(clientStore, registry, TestActivitySource, authSink, metadataAccessor, clock, new NoopCertificateValidator(), new HttpContextAccessor(), options, NullLogger<ValidateClientCredentialsHandler>.Instance);
var handleHandler = new HandleClientCredentialsHandler(registry, tokenStore, sessionAccessor, clock, TestActivitySource, NullLogger<HandleClientCredentialsHandler>.Instance);
var persistHandler = new PersistTokensHandler(tokenStore, sessionAccessor, clock, TestActivitySource, NullLogger<PersistTokensHandler>.Instance);

View File

@@ -10,18 +10,19 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using OpenIddict.Abstractions;
using OpenIddict.Extensions;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
using MongoDB.Driver;
using StellaOps.Auth.Abstractions;
using StellaOps.Authority.OpenIddict;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Storage.Mongo.Documents;
using StellaOps.Authority.Storage.Mongo.Sessions;
using StellaOps.Authority.Storage.Mongo.Stores;
using StellaOps.Authority.RateLimiting;
using StellaOps.Authority.Security;
using StellaOps.Cryptography.Audit;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
using MongoDB.Driver;
using StellaOps.Auth.Abstractions;
using StellaOps.Authority.OpenIddict;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Storage.Mongo.Documents;
using StellaOps.Authority.Storage.Mongo.Sessions;
using StellaOps.Authority.Storage.Mongo.Stores;
using StellaOps.Authority.RateLimiting;
using StellaOps.Authority.Security;
using StellaOps.Configuration;
using StellaOps.Cryptography.Audit;
namespace StellaOps.Authority.OpenIddict.Handlers;
@@ -31,33 +32,36 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle
private readonly IAuthorityIdentityProviderRegistry registry;
private readonly ActivitySource activitySource;
private readonly IAuthEventSink auditSink;
private readonly IAuthorityRateLimiterMetadataAccessor metadataAccessor;
private readonly TimeProvider timeProvider;
private readonly IAuthorityClientCertificateValidator certificateValidator;
private readonly IHttpContextAccessor httpContextAccessor;
private readonly ILogger<ValidateClientCredentialsHandler> logger;
private readonly IAuthorityRateLimiterMetadataAccessor metadataAccessor;
private readonly TimeProvider timeProvider;
private readonly IAuthorityClientCertificateValidator certificateValidator;
private readonly IHttpContextAccessor httpContextAccessor;
private readonly StellaOpsAuthorityOptions authorityOptions;
private readonly ILogger<ValidateClientCredentialsHandler> logger;
public ValidateClientCredentialsHandler(
IAuthorityClientStore clientStore,
IAuthorityIdentityProviderRegistry registry,
ActivitySource activitySource,
IAuthEventSink auditSink,
IAuthorityRateLimiterMetadataAccessor metadataAccessor,
TimeProvider timeProvider,
IAuthorityClientCertificateValidator certificateValidator,
IHttpContextAccessor httpContextAccessor,
ILogger<ValidateClientCredentialsHandler> logger)
{
this.clientStore = clientStore ?? throw new ArgumentNullException(nameof(clientStore));
this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
this.activitySource = activitySource ?? throw new ArgumentNullException(nameof(activitySource));
this.auditSink = auditSink ?? throw new ArgumentNullException(nameof(auditSink));
this.metadataAccessor = metadataAccessor ?? throw new ArgumentNullException(nameof(metadataAccessor));
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
this.certificateValidator = certificateValidator ?? throw new ArgumentNullException(nameof(certificateValidator));
this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
IAuthorityRateLimiterMetadataAccessor metadataAccessor,
TimeProvider timeProvider,
IAuthorityClientCertificateValidator certificateValidator,
IHttpContextAccessor httpContextAccessor,
StellaOpsAuthorityOptions authorityOptions,
ILogger<ValidateClientCredentialsHandler> logger)
{
this.clientStore = clientStore ?? throw new ArgumentNullException(nameof(clientStore));
this.registry = registry ?? throw new ArgumentNullException(nameof(registry));
this.activitySource = activitySource ?? throw new ArgumentNullException(nameof(activitySource));
this.auditSink = auditSink ?? throw new ArgumentNullException(nameof(auditSink));
this.metadataAccessor = metadataAccessor ?? throw new ArgumentNullException(nameof(metadataAccessor));
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
this.certificateValidator = certificateValidator ?? throw new ArgumentNullException(nameof(certificateValidator));
this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
this.authorityOptions = authorityOptions ?? throw new ArgumentNullException(nameof(authorityOptions));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async ValueTask HandleAsync(OpenIddictServerEvents.ValidateTokenRequestContext context)
{
@@ -124,14 +128,30 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle
? existingConstraint
: null;
var normalizedSenderConstraint = !string.IsNullOrWhiteSpace(existingSenderConstraint)
? existingSenderConstraint
: ClientCredentialHandlerHelpers.NormalizeSenderConstraint(document);
if (!string.IsNullOrWhiteSpace(normalizedSenderConstraint))
{
context.Transaction.Properties[AuthorityOpenIddictConstants.ClientSenderConstraintProperty] = normalizedSenderConstraint;
}
var normalizedSenderConstraint = !string.IsNullOrWhiteSpace(existingSenderConstraint)
? existingSenderConstraint
: ClientCredentialHandlerHelpers.NormalizeSenderConstraint(document);
var (mtlsRequired, matchedAudiences) = EvaluateMtlsRequirement(context.Request, document);
if (mtlsRequired)
{
if (string.IsNullOrWhiteSpace(normalizedSenderConstraint))
{
normalizedSenderConstraint = AuthoritySenderConstraintKinds.Mtls;
logger.LogDebug("Enforcing mTLS sender constraint for {ClientId} due to audiences {Audiences}.", document.ClientId, string.Join(",", matchedAudiences));
}
else if (!string.Equals(normalizedSenderConstraint, AuthoritySenderConstraintKinds.Mtls, StringComparison.Ordinal))
{
context.Reject(OpenIddictConstants.Errors.InvalidClient, "Requested audiences require mutual TLS sender constraint.");
logger.LogWarning("Client credentials validation failed for {ClientId}: mTLS required for audiences {Audiences} but client sender constraint was {Constraint}.", context.ClientId, string.Join(",", matchedAudiences), normalizedSenderConstraint);
return;
}
}
if (!string.IsNullOrWhiteSpace(normalizedSenderConstraint))
{
context.Transaction.Properties[AuthorityOpenIddictConstants.ClientSenderConstraintProperty] = normalizedSenderConstraint;
}
if (string.Equals(normalizedSenderConstraint, AuthoritySenderConstraintKinds.Mtls, StringComparison.Ordinal))
{
@@ -281,8 +301,95 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle
await auditSink.WriteAsync(record, context.CancellationToken).ConfigureAwait(false);
}
}
}
}
private (bool Required, string[] Audiences) EvaluateMtlsRequirement(OpenIddictRequest? request, AuthorityClientDocument document)
{
var mtlsOptions = authorityOptions.Security.SenderConstraints.Mtls;
if (!mtlsOptions.Enabled)
{
return (false, Array.Empty<string>());
}
var enforcedAudiences = ResolveEnforcedAudiences(mtlsOptions);
if (enforcedAudiences.Count == 0)
{
return (false, Array.Empty<string>());
}
static void CollectMatches(IEnumerable<string?> values, ISet<string> enforced, HashSet<string> matches)
{
foreach (var value in values)
{
if (string.IsNullOrWhiteSpace(value))
{
continue;
}
var candidate = value.Trim();
if (candidate.Length == 0)
{
continue;
}
if (enforced.Contains(candidate))
{
matches.Add(candidate);
}
}
}
var matched = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (request?.Resources is { } resources)
{
CollectMatches(resources, enforcedAudiences, matched);
}
if (request?.Audiences is { } audiences)
{
CollectMatches(audiences, enforcedAudiences, matched);
}
var configuredAudiences = ClientCredentialHandlerHelpers.Split(document.Properties, AuthorityClientMetadataKeys.Audiences);
if (configuredAudiences.Count > 0)
{
CollectMatches(configuredAudiences, enforcedAudiences, matched);
}
return matched.Count == 0
? (false, Array.Empty<string>())
: (true, matched.OrderBy(value => value, StringComparer.OrdinalIgnoreCase).ToArray());
}
private static HashSet<string> ResolveEnforcedAudiences(AuthorityMtlsOptions mtlsOptions)
{
if (mtlsOptions.NormalizedAudiences.Count > 0)
{
return new HashSet<string>(mtlsOptions.NormalizedAudiences, StringComparer.OrdinalIgnoreCase);
}
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var audience in mtlsOptions.EnforceForAudiences)
{
if (string.IsNullOrWhiteSpace(audience))
{
continue;
}
var trimmed = audience.Trim();
if (trimmed.Length == 0)
{
continue;
}
set.Add(trimmed);
}
return set;
}
}
internal sealed class HandleClientCredentialsHandler : IOpenIddictServerHandler<OpenIddictServerEvents.HandleTokenRequestContext>
{

View File

@@ -1,10 +1,11 @@
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using System.Formats.Asn1;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using System.Formats.Asn1;
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -145,12 +146,47 @@ internal sealed class AuthorityClientCertificateValidator : IAuthorityClientCert
return ValueTask.FromResult(AuthorityClientCertificateValidationResult.Failure("certificate_unbound"));
}
if (binding.NotBefore is { } bindingNotBefore)
{
var effectiveNotBefore = bindingNotBefore - mtlsOptions.RotationGrace;
if (now < effectiveNotBefore)
{
logger.LogWarning("mTLS validation failed for {ClientId}: certificate binding not active until {NotBefore:o} (grace applied).", client.ClientId, bindingNotBefore);
if (!string.IsNullOrWhiteSpace(binding.Subject) &&
!string.Equals(binding.Subject, certificate.Subject, StringComparison.OrdinalIgnoreCase))
{
logger.LogWarning("mTLS validation failed for {ClientId}: certificate subject {Subject} did not match binding subject {BindingSubject}.", client.ClientId, certificate.Subject, binding.Subject);
return ValueTask.FromResult(AuthorityClientCertificateValidationResult.Failure("certificate_binding_subject_mismatch"));
}
if (!string.IsNullOrWhiteSpace(binding.SerialNumber))
{
var normalizedCertificateSerial = NormalizeSerialNumber(certificate.SerialNumber);
var normalizedBindingSerial = NormalizeSerialNumber(binding.SerialNumber);
if (!string.Equals(normalizedCertificateSerial, normalizedBindingSerial, StringComparison.OrdinalIgnoreCase))
{
logger.LogWarning("mTLS validation failed for {ClientId}: certificate serial {Serial} did not match binding serial {BindingSerial}.", client.ClientId, normalizedCertificateSerial, normalizedBindingSerial);
return ValueTask.FromResult(AuthorityClientCertificateValidationResult.Failure("certificate_binding_serial_mismatch"));
}
}
if (!string.IsNullOrWhiteSpace(binding.Issuer) &&
!string.Equals(binding.Issuer, certificate.Issuer, StringComparison.OrdinalIgnoreCase))
{
logger.LogWarning("mTLS validation failed for {ClientId}: certificate issuer {Issuer} did not match binding issuer {BindingIssuer}.", client.ClientId, certificate.Issuer, binding.Issuer);
return ValueTask.FromResult(AuthorityClientCertificateValidationResult.Failure("certificate_binding_issuer_mismatch"));
}
if (binding.SubjectAlternativeNames.Count > 0)
{
var certificateSans = new HashSet<string>(subjectAlternativeNames.Select(san => san.Value), StringComparer.OrdinalIgnoreCase);
if (!binding.SubjectAlternativeNames.All(san => certificateSans.Contains(san)))
{
logger.LogWarning("mTLS validation failed for {ClientId}: certificate SANs did not include all binding values.", client.ClientId);
return ValueTask.FromResult(AuthorityClientCertificateValidationResult.Failure("certificate_binding_san_mismatch"));
}
}
if (binding.NotBefore is { } bindingNotBefore)
{
var effectiveNotBefore = bindingNotBefore - mtlsOptions.RotationGrace;
if (now < effectiveNotBefore)
{
logger.LogWarning("mTLS validation failed for {ClientId}: certificate binding not active until {NotBefore:o} (grace applied).", client.ClientId, bindingNotBefore);
return ValueTask.FromResult(AuthorityClientCertificateValidationResult.Failure("certificate_binding_inactive"));
}
}
@@ -197,11 +233,11 @@ internal sealed class AuthorityClientCertificateValidator : IAuthorityClientCert
}
}
private static IReadOnlyList<(string Type, string Value)> GetSubjectAlternativeNames(X509Certificate2 certificate)
{
foreach (var extension in certificate.Extensions)
{
if (!string.Equals(extension.Oid?.Value, "2.5.29.17", StringComparison.Ordinal))
private static IReadOnlyList<(string Type, string Value)> GetSubjectAlternativeNames(X509Certificate2 certificate)
{
foreach (var extension in certificate.Extensions)
{
if (!string.Equals(extension.Oid?.Value, "2.5.29.17", StringComparison.Ordinal))
{
continue;
}
@@ -256,28 +292,28 @@ internal sealed class AuthorityClientCertificateValidator : IAuthorityClientCert
}
}
return Array.Empty<(string, string)>();
}
private bool ValidateCertificateChain(X509Certificate2 certificate)
{
using var chain = new X509Chain
{
ChainPolicy =
{
RevocationMode = X509RevocationMode.NoCheck,
RevocationFlag = X509RevocationFlag.ExcludeRoot,
VerificationFlags = X509VerificationFlags.IgnoreWrongUsage
}
};
try
{
return chain.Build(certificate);
}
catch (Exception ex)
{
logger.LogWarning(ex, "mTLS chain validation threw an exception.");
return false;
}
}
}
return Array.Empty<(string, string)>();
}
private static string NormalizeSerialNumber(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
var buffer = new char[value.Length];
var length = 0;
foreach (var character in value)
{
if (character is ':' or ' ')
{
continue;
}
buffer[length++] = char.ToUpperInvariant(character);
}
return new string(buffer, 0, length);
}
}

View File

@@ -23,10 +23,9 @@
| AUTH-PLUGIN-COORD-08-002 | DONE (2025-10-20) | Authority Core, Plugin Platform Guild | PLUGIN-DI-08-001 | Coordinate scoped-service adoption for Authority plug-in registrars and background jobs ahead of PLUGIN-DI-08-002 implementation. | ✅ Workshop completed 2025-10-20 15:0016:05UTC with notes/action log in `docs/dev/authority-plugin-di-coordination.md`; ✅ Follow-up backlog updates assigned via documented action items ahead of PLUGIN-DI-08-002 delivery. |
| AUTH-DPOP-11-001 | DONE (2025-10-20) | Authority Core & Security Guild | — | Implement DPoP proof validation + nonce handling for high-value audiences per architecture. | ✅ Redis-configurable nonce store surfaced via `security.senderConstraints.dpop.nonce` with sample YAML and architecture docs refreshed<br>✅ High-value audience enforcement uses normalised required audiences to avoid whitespace/case drift<br>✅ Operator guide updated with Redis-backed nonce snippet and env-var override guidance; integration test already covers nonce challenge |
> Remark (2025-10-20): `etc/authority.yaml.sample` gains senderConstraint sections (rate limits, DPoP, mTLS), docs (`docs/ARCHITECTURE_AUTHORITY.md`, `docs/11_AUTHORITY.md`, plan) refreshed. `ResolveNonceAudience` now relies on `NormalizedAudiences` and options trim persisted values. `dotnet test StellaOps.Authority.sln` attempted (2025-10-20 15:12UTC) but failed on `NU1900` because the mirrored NuGet service index `https://mirrors.ablera.dev/nuget/nuget-mirror/v3/index.json` was unreachable; no project build executed.
| AUTH-MTLS-11-002 | DOING (2025-10-19) | Authority Core & Security Guild | — | Add OAuth mTLS client credential support with certificate-bound tokens and introspection updates. | • Certificate validator scaffold plus cnf stamping present; tokens persist sender thumbprints<br>• Remaining: provisioning/storage for certificate bindings, SAN/CA validation, introspection propagation, integration tests/docs before marking DONE |
> Remark (2025-10-19): Client provisioning accepts certificate bindings; validator enforces SAN types/CA allow-list with rotation grace; mtls integration tests updated (full suite still blocked by upstream build).
| AUTH-MTLS-11-002 | DONE (2025-10-23) | Authority Core & Security Guild | — | Add OAuth mTLS client credential support with certificate-bound tokens and introspection updates. | ✅ Deterministic provisioning/storage for certificate bindings (thumbprint/subject/issuer/serial/SAN)<br>✅ Audience enforcement auto-switches to mTLS via `security.senderConstraints.mtls.enforceForAudiences`<br>✅ Validator matches binding metadata with rotation grace and emits confirmation thumbprints<br>✅ Introspection returns `cnf.x5t#S256`; docs & sample config refreshed; Authority test suite green |
> Remark (2025-10-23): Audience enforcement now rejects non-mTLS clients targeting high-value audiences; certificate validator checks binding subject/issuer/serial/SAN values and returns deterministic error codes. Docs (`docs/11_AUTHORITY.md`, `docs/ARCHITECTURE_AUTHORITY.md`, `docs/dev/authority-dpop-mtls-plan.md`) and `etc/authority.yaml.sample` updated. `dotnet test src/StellaOps.Authority/StellaOps.Authority.sln` (2025-10-23 18:07UTC) succeeded.
> Remark (2025-10-19, AUTHSTORAGE-MONGO-08-001): Prerequisites re-checked (none outstanding). Session accessor wired through Authority pipeline; stores accept optional sessions; added replica-set election regression test for read-your-write.
> Remark (2025-10-19, AUTH-DPOP-11-001): Handler, nonce store, and persistence hooks merged; Redis-backed configuration + end-to-end nonce enforcement still open. (Superseded by 2025-10-20 update above.)
> Remark (2025-10-19, AUTH-MTLS-11-002): Certificate validator + cnf stamping delivered; binding storage, CA/SAN validation, integration suites outstanding before status can move to DONE.
> Update status columns (TODO / DOING / DONE / BLOCKED) together with code changes. Always run `dotnet test src/StellaOps.Authority.sln` when touching host logic.

View File

@@ -0,0 +1,110 @@
[
{
"analyzerId": "python",
"componentKey": "purl::pkg:pypi/layered@2.0",
"purl": "pkg:pypi/layered@2.0",
"name": "layered",
"version": "2.0",
"type": "pypi",
"usedByEntrypoint": true,
"metadata": {
"author": "Layered Maintainer",
"authorEmail": "maintainer@example.com",
"classifier[0]": "Programming Language :: Python :: 3",
"classifiers": "Programming Language :: Python :: 3",
"distInfoPath": "layer1/usr/lib/python3.11/site-packages/layered-2.0.dist-info",
"editable": "true",
"entryPoints.console_scripts": "layered-cli=layered.cli:main",
"entryPoints.layered.hooks": "register=layered.plugins:register",
"installer": "pip",
"license": "Apache-2.0",
"license.classifier[0]": "License :: OSI Approved :: Apache Software License",
"license.file[0]": "layer2/usr/lib/python3.11/site-packages/LICENSE",
"licenseExpression": "Apache-2.0",
"name": "layered",
"normalizedName": "layered",
"projectUrl": "Documentation, https://example.com/layered/docs",
"provenance": "dist-info",
"record.hashMismatches": "0",
"record.hashedEntries": "8",
"record.ioErrors": "0",
"record.missingFiles": "0",
"record.totalEntries": "9",
"requiresDist": "requests",
"requiresPython": "\u003E=3.9",
"sourceCommit": "abc123",
"sourceSubdirectory": "src/layered",
"sourceUrl": "https://git.example.com/layered",
"sourceVcs": "git",
"summary": "Base layer metadata",
"version": "2.0",
"wheel.generator": "pip 24.0",
"wheel.rootIsPurelib": "true",
"wheel.tags": "py3-none-any",
"wheel.version": "1.0"
},
"evidence": [
{
"kind": "file",
"source": "INSTALLER",
"locator": "layer1/usr/lib/python3.11/site-packages/layered-2.0.dist-info/INSTALLER"
},
{
"kind": "file",
"source": "INSTALLER",
"locator": "layer2/usr/lib/python3.11/site-packages/layered-2.0.dist-info/INSTALLER"
},
{
"kind": "file",
"source": "METADATA",
"locator": "layer1/usr/lib/python3.11/site-packages/layered-2.0.dist-info/METADATA"
},
{
"kind": "file",
"source": "METADATA",
"locator": "layer2/usr/lib/python3.11/site-packages/layered-2.0.dist-info/METADATA"
},
{
"kind": "file",
"source": "RECORD",
"locator": "layer1/usr/lib/python3.11/site-packages/layered-2.0.dist-info/RECORD"
},
{
"kind": "file",
"source": "RECORD",
"locator": "layer2/usr/lib/python3.11/site-packages/layered-2.0.dist-info/RECORD"
},
{
"kind": "file",
"source": "WHEEL",
"locator": "layer1/usr/lib/python3.11/site-packages/layered-2.0.dist-info/WHEEL"
},
{
"kind": "file",
"source": "WHEEL",
"locator": "layer2/usr/lib/python3.11/site-packages/layered-2.0.dist-info/WHEEL"
},
{
"kind": "file",
"source": "entry_points.txt",
"locator": "layer1/usr/lib/python3.11/site-packages/layered-2.0.dist-info/entry_points.txt"
},
{
"kind": "file",
"source": "entry_points.txt",
"locator": "layer2/usr/lib/python3.11/site-packages/layered-2.0.dist-info/entry_points.txt"
},
{
"kind": "file",
"source": "license",
"locator": "layer2/usr/lib/python3.11/site-packages/LICENSE"
},
{
"kind": "metadata",
"source": "direct_url.json",
"locator": "layer2/usr/lib/python3.11/site-packages/layered-2.0.dist-info/direct_url.json",
"value": "https://git.example.com/layered"
}
]
}
]

View File

@@ -0,0 +1,8 @@
Metadata-Version: 2.1
Name: layered
Version: 2.0
Summary: Base layer metadata
License: Apache-2.0
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.9
Requires-Dist: requests

View File

@@ -0,0 +1,9 @@
layered/__init__.py,sha256=3Q3bv/BWCZLW2p2dVYw8QfAfBYV2YBtuYtT9TIJCmFM=,139
layered/core.py,sha256=izBXI4cRE/cgf7hgc/gHfTp1OyIIJ23NgUJXaHWVFpU=,80
layered/cli.py,sha256=xQrTznF7ch6C9qyQALJpsqRTIh9DVCRG4IXoQ1eLLnY=,126
../../../bin/layered-cli,sha256=6IGTGCEapolFoAUnXGNrWIfPSXU8W7bsZ07DYF/wmNc=,91
layered-2.0.dist-info/METADATA,sha256=jNEi7xsj4V+SSzOJJToxMoZmZ7gxyto7zuKxCjxUFjk=,193
layered-2.0.dist-info/WHEEL,sha256=m8MHT7vQnqC5W8H/y4uJdEpx9ijH0jJGpuoaxRbcsQg=,79
layered-2.0.dist-info/entry_points.txt,sha256=hm8bgJUYe2zoYNATyAsQzQKQTdQtFe4ctbf5kSlxFj0=,47
layered-2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ+UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg=,4
layered-2.0.dist-info/RECORD,,

View File

@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: pip 24.0
Root-Is-Purelib: true
Tag: py3-none-any

View File

@@ -0,0 +1,5 @@
"""Layered package demonstrating merged metadata across layers."""
from .core import get_version # noqa: F401
__all__ = ["get_version"]

View File

@@ -0,0 +1,7 @@
from __future__ import annotations
from .core import get_version
def main() -> None:
print(f"layered {get_version()}")

View File

@@ -0,0 +1,5 @@
from __future__ import annotations
def get_version() -> str:
return "2.0"

View File

@@ -0,0 +1,10 @@
Metadata-Version: 2.1
Name: layered
Version: 2.0
Summary: Overlay metadata adding direct URL information
License-Expression: Apache-2.0
License-File: LICENSE
Author: Layered Maintainer
Author-email: maintainer@example.com
Project-URL: Documentation, https://example.com/layered/docs
Classifier: License :: OSI Approved :: Apache Software License

View File

@@ -0,0 +1,9 @@
layered/plugins/__init__.py,sha256=hMd8TidtznWDaiA4biHZ04ZoVXcAc7z/p77bIdAsPyE=,53
layered/plugins/plugin.py,sha256=PBVqd9coVIzSBTQ2qdL5qxoK0fnsRZZ1DkhqnaVySPA=,87
LICENSE,sha256=cXKP+wQk9Jyqh8mUi7nURl9jOOjojDqrabZ119S2EzM=,27
layered-2.0.dist-info/METADATA,sha256=V09W93ILksWKLP8My6UatmScZ+5MCLiQ/5ieWzb585M=,346
layered-2.0.dist-info/WHEEL,sha256=m8MHT7vQnqC5W8H/y4uJdEpx9ijH0jJGpuoaxRbcsQg=,79
layered-2.0.dist-info/entry_points.txt,sha256=JYpkYczwozo6Ek7diDPgPj8ReYv5wTpaW0pFjL82bGU=,50
layered-2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ+UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg=,4
layered-2.0.dist-info/direct_url.json,sha256=8NtnZQZq2S5tcEn+P5fH6/EpABJ9+Ha5aIq8Sn2szig=,189
layered-2.0.dist-info/RECORD,,

View File

@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: pip 24.0
Root-Is-Purelib: true
Tag: py3-none-any

View File

@@ -0,0 +1,11 @@
{
"url": "https://git.example.com/layered",
"dir_info": {
"editable": true,
"subdirectory": "src/layered"
},
"vcs_info": {
"vcs": "git",
"commit_id": "abc123"
}
}

View File

@@ -0,0 +1,5 @@
from __future__ import annotations
def register() -> str:
return "layer2-plugin"

View File

@@ -0,0 +1,87 @@
[
{
"analyzerId": "python",
"componentKey": "purl::pkg:pypi/cache-pkg@1.2.3",
"purl": "pkg:pypi/cache-pkg@1.2.3",
"name": "Cache-Pkg",
"version": "1.2.3",
"type": "pypi",
"usedByEntrypoint": true,
"metadata": {
"classifier[0]": "Intended Audience :: Developers",
"classifier[1]": "License :: OSI Approved :: BSD License",
"classifier[2]": "Programming Language :: Python :: 3",
"classifiers": "Intended Audience :: Developers;License :: OSI Approved :: BSD License;Programming Language :: Python :: 3",
"distInfoPath": "lib/python3.11/site-packages/cache_pkg-1.2.3.dist-info",
"entryPoints.console_scripts": "cache-tool=cache_pkg:main",
"installer": "pip",
"license": "BSD-3-Clause",
"license.classifier[0]": "License :: OSI Approved :: BSD License",
"license.file[0]": "LICENSE",
"name": "Cache-Pkg",
"normalizedName": "cache-pkg",
"projectUrl": "Source, https://example.com/cache-pkg",
"provenance": "dist-info",
"record.hashMismatches": "1",
"record.hashedEntries": "9",
"record.ioErrors": "0",
"record.missingFiles": "2",
"record.totalEntries": "12",
"record.unsupportedAlgorithms": "md5",
"requiresDist": "click",
"requiresPython": "\u003E=3.8",
"summary": "Cache test package for hashed RECORD coverage",
"version": "1.2.3",
"wheel.generator": "pip 24.0",
"wheel.rootIsPurelib": "true",
"wheel.tags": "py3-none-any",
"wheel.version": "1.0"
},
"evidence": [
{
"kind": "derived",
"source": "RECORD",
"locator": "../etc/passwd",
"value": "outside-root"
},
{
"kind": "derived",
"source": "RECORD",
"locator": "lib/python3.11/site-packages/cache_pkg/LICENSE",
"value": "sha256 mismatch expected=pdUY6NGoyWCpcK8ThpBPUIVXbLvU9PzP8lsXEVEnFd0= actual=pdUY6NGoyWCpcK8ThpBPUIVXbLvU9PzP8lsXEVEnFdk=",
"sha256": "pdUY6NGoyWCpcK8ThpBPUIVXbLvU9PzP8lsXEVEnFdk="
},
{
"kind": "derived",
"source": "RECORD",
"locator": "lib/python3.11/site-packages/cache_pkg/missing/data.json",
"value": "missing"
},
{
"kind": "file",
"source": "INSTALLER",
"locator": "lib/python3.11/site-packages/cache_pkg-1.2.3.dist-info/INSTALLER"
},
{
"kind": "file",
"source": "METADATA",
"locator": "lib/python3.11/site-packages/cache_pkg-1.2.3.dist-info/METADATA"
},
{
"kind": "file",
"source": "RECORD",
"locator": "lib/python3.11/site-packages/cache_pkg-1.2.3.dist-info/RECORD"
},
{
"kind": "file",
"source": "WHEEL",
"locator": "lib/python3.11/site-packages/cache_pkg-1.2.3.dist-info/WHEEL"
},
{
"kind": "file",
"source": "entry_points.txt",
"locator": "lib/python3.11/site-packages/cache_pkg-1.2.3.dist-info/entry_points.txt"
}
]
}
]

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env python3
from cache_pkg import main
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,12 @@
Metadata-Version: 2.1
Name: Cache-Pkg
Version: 1.2.3
Summary: Cache test package for hashed RECORD coverage
License: BSD-3-Clause
License-File: LICENSE
Classifier: Programming Language :: Python :: 3
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Requires-Python: >=3.8
Requires-Dist: click
Project-URL: Source, https://example.com/cache-pkg

View File

@@ -0,0 +1,12 @@
cache_pkg/__init__.py,sha256=iw2XGXcGU2Si1KAQ7o82tPSKxwEFc6UNZfITpGPh7mM=,189
cache_pkg/data/config.json,sha256=Oa/wlgi1qJHC93RF5vwoOFffyviGtm5ccL7lrI0gkeY=,49
cache_pkg/LICENSE,sha256=pdUY6NGoyWCpcK8ThpBPUIVXbLvU9PzP8lsXEVEnFd0=,73
cache_pkg/md5only.txt,md5=Zm9v,4
cache_pkg-1.2.3.data/scripts/cache-tool,sha256=2rsv/gnYOtlJZCy75Wz0rCADxYPnQAkyKvNbuoquZQ4=,89
cache_pkg-1.2.3.dist-info/METADATA,sha256=DXPSItxOR1kPkVzjyq8F50jf8FOR9brSs/TGcZmcEHo=,390
cache_pkg-1.2.3.dist-info/WHEEL,sha256=m8MHT7vQnqC5W8H/y4uJdEpx9ijH0jJGpuoaxRbcsQg=,79
cache_pkg-1.2.3.dist-info/entry_points.txt,sha256=S1tGoBGlzWL6jeECCTw1pZP09HM8voEm/qQ7DtOHPyc=,44
cache_pkg-1.2.3.dist-info/INSTALLER,sha256=zuuue4knoyJ+UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg=,4
cache_pkg-1.2.3.dist-info/RECORD,,
cache_pkg/missing/data.json,sha256=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=,12
../../../../etc/passwd,,

View File

@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: pip 24.0
Root-Is-Purelib: true
Tag: py3-none-any

View File

@@ -0,0 +1,4 @@
BSD 3-Clause License
Copyright (c) 2025, StellaOps
All rights reserved.

View File

@@ -0,0 +1,7 @@
"""Cache fixture package for determinism tests."""
from .data import config # noqa: F401
def main() -> None:
"""Entry point used by console script."""
print("cache-pkg running")

View File

@@ -10,19 +10,24 @@
"metadata": {
"author": "Example Dev",
"authorEmail": "dev@example.com",
"classifiers": "Programming Language :: Python :: 3;License :: OSI Approved :: Apache Software License",
"classifier[0]": "License :: OSI Approved :: Apache Software License",
"classifier[1]": "Programming Language :: Python :: 3",
"classifiers": "License :: OSI Approved :: Apache Software License;Programming Language :: Python :: 3",
"distInfoPath": "lib/python3.11/site-packages/simple-1.0.0.dist-info",
"editable": "true",
"entryPoints.console_scripts": "simple-tool=simple.core:main",
"homePage": "https://example.com/simple",
"installer": "pip",
"license": "Apache-2.0",
"license.classifier[0]": "License :: OSI Approved :: Apache Software License",
"name": "simple",
"normalizedName": "simple",
"projectUrl": "Source, https://example.com/simple/src",
"provenance": "dist-info",
"record.hashMismatches": "0",
"record.hashedEntries": "9",
"record.hashedEntries": "8",
"record.ioErrors": "0",
"record.missingFiles": "0",
"record.missingFiles": "1",
"record.totalEntries": "10",
"requiresDist": "requests (\u003E=2.0)",
"requiresPython": "\u003E=3.9",
@@ -38,11 +43,27 @@
"wheel.version": "1.0"
},
"evidence": [
{
"kind": "derived",
"source": "RECORD",
"locator": "bin/simple-tool",
"value": "missing"
},
{
"kind": "file",
"source": "INSTALLER",
"locator": "lib/python3.11/site-packages/simple-1.0.0.dist-info/INSTALLER"
},
{
"kind": "file",
"source": "METADATA",
"locator": "lib/python3.11/site-packages/simple-1.0.0.dist-info/METADATA"
},
{
"kind": "file",
"source": "RECORD",
"locator": "lib/python3.11/site-packages/simple-1.0.0.dist-info/RECORD"
},
{
"kind": "file",
"source": "WHEEL",
@@ -61,4 +82,4 @@
}
]
}
]
]

View File

@@ -30,4 +30,54 @@ public sealed class PythonLanguageAnalyzerTests
cancellationToken,
usageHints);
}
[Fact]
public async Task PipCacheFixtureProducesDeterministicOutputAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "python", "pip-cache");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var usageHints = new LanguageUsageHints(new[]
{
Path.Combine(fixturePath, "lib", "python3.11", "site-packages", "cache_pkg-1.2.3.data", "scripts", "cache-tool")
});
var analyzers = new ILanguageAnalyzer[]
{
new PythonLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken,
usageHints);
}
[Fact]
public async Task LayeredEditableFixtureMergesAcrossLayersAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "python", "layered-editable");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var usageHints = new LanguageUsageHints(new[]
{
Path.Combine(fixturePath, "layer1", "usr", "bin", "layered-cli")
});
var analyzers = new ILanguageAnalyzer[]
{
new PythonLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken,
usageHints);
}
}

View File

@@ -45,22 +45,97 @@ internal static class PythonDistributionLoader
AddFileEvidence(context, metadataPath, "METADATA", evidenceEntries);
AddFileEvidence(context, wheelPath, "WHEEL", evidenceEntries);
AddFileEvidence(context, entryPointsPath, "entry_points.txt", evidenceEntries);
AddFileEvidence(context, installerPath, "INSTALLER", evidenceEntries);
AddFileEvidence(context, recordPath, "RECORD", evidenceEntries);
AppendMetadata(metadataEntries, "distInfoPath", PythonPathHelper.NormalizeRelative(context, distInfoPath));
AppendMetadata(metadataEntries, "name", trimmedName);
AppendMetadata(metadataEntries, "version", trimmedVersion);
AppendMetadata(metadataEntries, "normalizedName", normalizedName);
AppendMetadata(metadataEntries, "summary", metadataDocument.GetFirst("Summary"));
AppendMetadata(metadataEntries, "license", metadataDocument.GetFirst("License"));
AppendMetadata(metadataEntries, "licenseExpression", metadataDocument.GetFirst("License-Expression"));
AppendMetadata(metadataEntries, "homePage", metadataDocument.GetFirst("Home-page"));
AppendMetadata(metadataEntries, "author", metadataDocument.GetFirst("Author"));
AppendMetadata(metadataEntries, "authorEmail", metadataDocument.GetFirst("Author-email"));
AppendMetadata(metadataEntries, "projectUrl", metadataDocument.GetFirst("Project-URL"));
AppendMetadata(metadataEntries, "requiresPython", metadataDocument.GetFirst("Requires-Python"));
var licenseFiles = metadataDocument.GetAll("License-File");
if (licenseFiles.Count > 0)
{
var packageRoot = ResolvePackageRoot(distInfoPath);
var licenseIndex = 0;
var seenLicensePaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var licenseFile in licenseFiles)
{
if (string.IsNullOrWhiteSpace(licenseFile))
{
continue;
}
var trimmed = licenseFile.Trim();
var resolved = TryResolvePackagePath(packageRoot, trimmed);
string metadataValue;
string? evidenceLocator = null;
if (!string.IsNullOrEmpty(resolved) && File.Exists(resolved))
{
metadataValue = PythonPathHelper.NormalizeRelative(context, resolved);
evidenceLocator = metadataValue;
}
else
{
metadataValue = trimmed;
}
if (metadataValue.Length == 0 || !seenLicensePaths.Add(metadataValue))
{
continue;
}
AppendMetadata(metadataEntries, $"license.file[{licenseIndex}]", metadataValue);
licenseIndex++;
if (!string.IsNullOrEmpty(evidenceLocator))
{
evidenceEntries.Add(new LanguageComponentEvidence(
LanguageEvidenceKind.File,
"license",
evidenceLocator,
Value: null,
Sha256: null));
}
}
}
var classifiers = metadataDocument.GetAll("Classifier");
if (classifiers.Count > 0)
{
AppendMetadata(metadataEntries, "classifiers", string.Join(';', classifiers));
var orderedClassifiers = classifiers
.Where(static classifier => !string.IsNullOrWhiteSpace(classifier))
.Select(static classifier => classifier.Trim())
.OrderBy(static classifier => classifier, StringComparer.Ordinal)
.ToArray();
if (orderedClassifiers.Length > 0)
{
AppendMetadata(metadataEntries, "classifiers", string.Join(';', orderedClassifiers));
var licenseClassifierIndex = 0;
for (var index = 0; index < orderedClassifiers.Length; index++)
{
var classifier = orderedClassifiers[index];
AppendMetadata(metadataEntries, $"classifier[{index}]", classifier);
if (classifier.StartsWith("License ::", StringComparison.OrdinalIgnoreCase))
{
AppendMetadata(metadataEntries, $"license.classifier[{licenseClassifierIndex}]", classifier);
licenseClassifierIndex++;
}
}
}
}
var requiresDist = metadataDocument.GetAll("Requires-Dist");
@@ -125,6 +200,7 @@ internal static class PythonDistributionLoader
evidenceEntries.AddRange(verification.Evidence);
var usedByEntrypoint = verification.UsedByEntrypoint || EvaluateEntryPointUsage(context, distInfoPath, entryPoints);
AppendMetadata(metadataEntries, "provenance", "dist-info");
return new PythonDistribution(
trimmedName,
@@ -267,6 +343,24 @@ internal static class PythonDistributionLoader
return builder.ToString();
}
private static string ResolvePackageRoot(string distInfoPath)
{
var parent = Directory.GetParent(distInfoPath);
return parent?.FullName ?? distInfoPath;
}
private static string? TryResolvePackagePath(string basePath, string relativePath)
{
try
{
return Path.GetFullPath(Path.Combine(basePath, relativePath));
}
catch
{
return null;
}
}
private static async Task<string?> ReadSingleLineAsync(string path, CancellationToken cancellationToken)
{
if (!File.Exists(path))
@@ -769,6 +863,11 @@ internal static class PythonRecordVerifier
var entryPath = entry.Path.Replace('/', Path.DirectorySeparatorChar);
var fullPath = Path.GetFullPath(Path.Combine(parent, entryPath));
if (context.UsageHints.IsPathUsed(fullPath))
{
usedByEntrypoint = true;
}
if (!fullPath.StartsWith(root, StringComparison.Ordinal))
{
missing++;
@@ -793,11 +892,6 @@ internal static class PythonRecordVerifier
continue;
}
if (context.UsageHints.IsPathUsed(fullPath))
{
usedByEntrypoint = true;
}
if (string.IsNullOrWhiteSpace(entry.HashAlgorithm) || string.IsNullOrWhiteSpace(entry.HashValue))
{
continue;

View File

@@ -5,6 +5,6 @@
| 1 | SCANNER-ANALYZERS-LANG-10-303A | DONE (2025-10-21) | SCANNER-ANALYZERS-LANG-10-307 | STREAM-based parser for `*.dist-info` (`METADATA`, `WHEEL`, `entry_points.txt`) with normalization + evidence capture. | Parser handles CPython 3.83.12 metadata variations; fixtures confirm canonical ordering and UTF-8 handling. |
| 2 | SCANNER-ANALYZERS-LANG-10-303B | DONE (2025-10-21) | SCANNER-ANALYZERS-LANG-10-303A | RECORD hash verifier with chunked hashing, Zip64 support, and mismatch diagnostics. | Verifier processes 5GB RECORD fixture without allocations >2MB; mismatches produce deterministic evidence records. |
| 3 | SCANNER-ANALYZERS-LANG-10-303C | DONE (2025-10-21) | SCANNER-ANALYZERS-LANG-10-303B | Editable install + pip cache detection; integrate EntryTrace hints for runtime usage flags. | Editable installs resolved to source path; usage flags propagated; regression tests cover mixed editable + wheel installs. |
| 4 | SCANNER-ANALYZERS-LANG-10-307P | DOING (2025-10-23) | SCANNER-ANALYZERS-LANG-10-303C | Shared helper integration (license metadata, quiet provenance, component merging). | Shared helpers reused; analyzer-specific metadata minimal; deterministic merge tests pass. |
| 5 | SCANNER-ANALYZERS-LANG-10-308P | TODO | SCANNER-ANALYZERS-LANG-10-307P | Golden fixtures + determinism harness for Python analyzer; add benchmark and hash throughput reporting. | Fixtures under `Fixtures/lang/python/`; determinism CI guard; benchmark CSV added with threshold alerts. |
| 6 | SCANNER-ANALYZERS-LANG-10-309P | TODO | SCANNER-ANALYZERS-LANG-10-308P | Package plug-in (manifest, DI registration) and document Offline Kit bundling of Python stdlib metadata if needed. | Manifest copied to `plugins/scanner/analyzers/lang/`; Worker loads analyzer; Offline Kit doc updated. |
| 4 | SCANNER-ANALYZERS-LANG-10-307P | DONE (2025-10-23) | SCANNER-ANALYZERS-LANG-10-303C | Shared helper integration (license metadata, quiet provenance, component merging). | Shared helpers reused; analyzer-specific metadata minimal; deterministic merge tests pass. |
| 5 | SCANNER-ANALYZERS-LANG-10-308P | DONE (2025-10-23) | SCANNER-ANALYZERS-LANG-10-307P | Golden fixtures + determinism harness for Python analyzer; add benchmark and hash throughput reporting. | Fixtures under `Fixtures/lang/python/`; determinism CI guard; benchmark CSV added with threshold alerts. |
| 6 | SCANNER-ANALYZERS-LANG-10-309P | DONE (2025-10-23) | SCANNER-ANALYZERS-LANG-10-308P | Package plug-in (manifest, DI registration) and document Offline Kit bundling of Python stdlib metadata if needed. | Manifest copied to `plugins/scanner/analyzers/lang/`; Worker loads analyzer; Offline Kit doc updated. |

View File

@@ -5,6 +5,6 @@
| 1 | SCANNER-ANALYZERS-LANG-10-306A | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-307 | Parse Cargo metadata (`Cargo.lock`, `.fingerprint`, `.metadata`) and map crates to components with evidence. | Fixtures confirm crate attribution ≥85% coverage; metadata normalized; evidence includes path + hash. |
| 2 | SCANNER-ANALYZERS-LANG-10-306B | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-306A | Implement heuristic classifier using ELF section names, symbol mangling, and `.comment` data for stripped binaries. | Heuristic output flagged as `heuristic`; regression tests ensure no false “observed” classifications. |
| 3 | SCANNER-ANALYZERS-LANG-10-306C | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-306B | Integrate binary hash fallback (`bin:{sha256}`) and tie into shared quiet provenance helpers. | Fallback path deterministic; shared helpers reused; tests verify consistent hashing. |
| 4 | SCANNER-ANALYZERS-LANG-10-307R | TODO | SCANNER-ANALYZERS-LANG-10-306C | Finalize shared helper usage (license, usage flags) and concurrency-safe caches. | Analyzer uses shared utilities; concurrency tests pass; no race conditions. |
| 4 | SCANNER-ANALYZERS-LANG-10-307R | DOING (2025-10-23) | SCANNER-ANALYZERS-LANG-10-306C | Finalize shared helper usage (license, usage flags) and concurrency-safe caches. | Analyzer uses shared utilities; concurrency tests pass; no race conditions. |
| 5 | SCANNER-ANALYZERS-LANG-10-308R | TODO | SCANNER-ANALYZERS-LANG-10-307R | Determinism fixtures + performance benchmarks; compare against competitor heuristic coverage. | Fixtures `Fixtures/lang/rust/` committed; determinism guard; benchmark shows ≥15% better coverage vs competitor. |
| 6 | SCANNER-ANALYZERS-LANG-10-309R | TODO | SCANNER-ANALYZERS-LANG-10-308R | Package plug-in manifest + Offline Kit documentation; ensure Worker integration. | Manifest copied; Worker loads analyzer; Offline Kit doc updated. |

View File

@@ -37,6 +37,7 @@ All sprints below assume prerequisites from SP10-G2 (core scaffolding + Java ana
- Usage hint propagation tests (EntryTrace analyzer SBOM).
- Metrics counters (`scanner_analyzer_python_components_total`) documented.
- **Progress (2025-10-21):** Python analyzer landed; Tasks 10-303A/B/C are DONE with dist-info parsing, RECORD verification, editable install detection, and deterministic `simple-venv` fixture + benchmark hooks recorded.
- **Progress (2025-10-23):** Closed Tasks 10-307P/308P/309P Python analyzer now emits quiet-provenance metadata via shared helpers, determinism harness covers `simple-venv`, `pip-cache`, and `layered-editable` fixtures, bench suite reports hash throughput (`python/hash-throughput-20251023.csv`), and Offline Kit docs list the Python plug-in in the language bundle manifest.
## Sprint LA3 — Go Analyzer & Build Info Synthesis (Tasks 10-304, 10-307, 10-308, 10-309 subset)
- **Scope:** Extract Go build metadata from `.note.go.buildid`, embedded module info, and fallback to `bin:{sha256}`; surface VCS provenance.

View File

@@ -8,5 +8,5 @@
| SCANNER-EMIT-10-604 | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-602 | Package artifacts for export + attestation (naming, compression, manifests). | Export pipeline produces deterministic file paths/hashes; integration test with storage passes. |
| SCANNER-EMIT-10-605 | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-603 | Emit BOM-Index sidecar schema/fixtures (`bom-index@1`) and note CRITICAL PATH for Scheduler. | Schema + fixtures in docs/artifacts/bom-index; tests `BOMIndexGoldenIsStable` green. |
| SCANNER-EMIT-10-606 | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-605 | Integrate EntryTrace usage flags into BOM-Index; document semantics. | Usage bits present in sidecar; integration tests with EntryTrace fixtures pass. |
| SCANNER-EMIT-17-701 | TODO | Emit Guild, Native Analyzer Guild | SCANNER-EMIT-10-602 | Record GNU build-id for ELF components and surface it in inventory/usage SBOM plus diff payloads with deterministic ordering. | Native analyzer emits buildId for every ELF executable/library, SBOM/diff fixtures updated with canonical `buildId` field, regression tests prove stability, docs call out debug-symbol lookup flow. |
| SCANNER-EMIT-17-701 | DOING (2025-10-23) | Emit Guild, Native Analyzer Guild | SCANNER-EMIT-10-602 | Record GNU build-id for ELF components and surface it in inventory/usage SBOM plus diff payloads with deterministic ordering. | Native analyzer emits buildId for every ELF executable/library, SBOM/diff fixtures updated with canonical `buildId` field, regression tests prove stability, docs call out debug-symbol lookup flow. |
| SCANNER-EMIT-10-607 | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-604, POLICY-CORE-09-005 | Embed scoring inputs, confidence band, and `quietedBy` provenance into CycloneDX 1.6 and DSSE predicates; verify deterministic serialization. | SBOM/attestation fixtures include score, inputs, configVersion, quiet metadata; golden tests confirm canonical output. |

View File

@@ -0,0 +1,168 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Storage;
using StellaOps.Scanner.Storage.ObjectStore;
using Xunit;
namespace StellaOps.Scanner.Storage.Tests;
public sealed class RustFsArtifactObjectStoreTests
{
[Fact]
public async Task PutAsync_PreservesStreamAndSendsImmutableHeaders()
{
var handler = new RecordingHttpMessageHandler();
handler.Responses.Enqueue(new HttpResponseMessage(HttpStatusCode.OK));
var factory = new SingleHttpClientFactory(new HttpClient(handler)
{
BaseAddress = new Uri("https://rustfs.test/api/v1/"),
});
var options = Options.Create(new ScannerStorageOptions
{
ObjectStore =
{
Driver = ScannerStorageDefaults.ObjectStoreProviders.RustFs,
BucketName = "scanner-artifacts",
RustFs =
{
BaseUrl = "https://rustfs.test/api/v1/",
Timeout = TimeSpan.FromSeconds(10),
},
},
});
options.Value.ObjectStore.Headers["X-Custom-Header"] = "custom-value";
var store = new RustFsArtifactObjectStore(factory, options, NullLogger<RustFsArtifactObjectStore>.Instance);
var payload = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("rustfs artifact payload"));
var descriptor = new ArtifactObjectDescriptor("scanner-artifacts", "scanner/layers/digest/file.bin", true, TimeSpan.FromHours(1));
await store.PutAsync(descriptor, payload, CancellationToken.None);
Assert.True(payload.CanRead);
Assert.Equal(0, payload.Position);
var request = Assert.Single(handler.CapturedRequests);
Assert.Equal(HttpMethod.Put, request.Method);
Assert.Equal("https://rustfs.test/api/v1/buckets/scanner-artifacts/objects/scanner/layers/digest/file.bin", request.RequestUri.ToString());
Assert.Contains("X-Custom-Header", request.Headers.Keys);
Assert.Equal("custom-value", Assert.Single(request.Headers["X-Custom-Header"]));
Assert.Equal("true", Assert.Single(request.Headers["X-RustFS-Immutable"]));
Assert.Equal("3600", Assert.Single(request.Headers["X-RustFS-Retain-Seconds"]));
Assert.Equal("application/octet-stream", Assert.Single(request.Headers["Content-Type"]));
}
[Fact]
public async Task GetAsync_ReturnsNullOnNotFound()
{
var handler = new RecordingHttpMessageHandler();
handler.Responses.Enqueue(new HttpResponseMessage(HttpStatusCode.NotFound));
var factory = new SingleHttpClientFactory(new HttpClient(handler)
{
BaseAddress = new Uri("https://rustfs.test/api/v1/"),
});
var options = Options.Create(new ScannerStorageOptions
{
ObjectStore =
{
Driver = ScannerStorageDefaults.ObjectStoreProviders.RustFs,
BucketName = "scanner-artifacts",
RustFs =
{
BaseUrl = "https://rustfs.test/api/v1/",
},
},
});
var store = new RustFsArtifactObjectStore(factory, options, NullLogger<RustFsArtifactObjectStore>.Instance);
var descriptor = new ArtifactObjectDescriptor("scanner-artifacts", "scanner/indexes/digest/index.bin", false);
var result = await store.GetAsync(descriptor, CancellationToken.None);
Assert.Null(result);
var request = Assert.Single(handler.CapturedRequests);
Assert.Equal(HttpMethod.Get, request.Method);
}
[Fact]
public async Task DeleteAsync_IgnoresNotFound()
{
var handler = new RecordingHttpMessageHandler();
handler.Responses.Enqueue(new HttpResponseMessage(HttpStatusCode.NotFound));
var factory = new SingleHttpClientFactory(new HttpClient(handler)
{
BaseAddress = new Uri("https://rustfs.test/api/v1/"),
});
var options = Options.Create(new ScannerStorageOptions
{
ObjectStore =
{
Driver = ScannerStorageDefaults.ObjectStoreProviders.RustFs,
BucketName = "scanner-artifacts",
RustFs =
{
BaseUrl = "https://rustfs.test/api/v1/",
},
},
});
var store = new RustFsArtifactObjectStore(factory, options, NullLogger<RustFsArtifactObjectStore>.Instance);
var descriptor = new ArtifactObjectDescriptor("scanner-artifacts", "scanner/attest/digest/attest.bin", false);
await store.DeleteAsync(descriptor, CancellationToken.None);
var request = Assert.Single(handler.CapturedRequests);
Assert.Equal(HttpMethod.Delete, request.Method);
}
private sealed record CapturedRequest(HttpMethod Method, Uri RequestUri, IReadOnlyDictionary<string, string[]> Headers);
private sealed class RecordingHttpMessageHandler : HttpMessageHandler
{
public Queue<HttpResponseMessage> Responses { get; } = new();
public List<CapturedRequest> CapturedRequests { get; } = new();
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var headerSnapshot = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase);
foreach (var header in request.Headers)
{
headerSnapshot[header.Key] = header.Value.ToArray();
}
if (request.Content is not null)
{
foreach (var header in request.Content.Headers)
{
headerSnapshot[header.Key] = header.Value.ToArray();
}
// Materialize content to ensure downstream callers can inspect it.
_ = await request.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
}
CapturedRequests.Add(new CapturedRequest(request.Method, request.RequestUri!, headerSnapshot));
return Responses.Count > 0 ? Responses.Dequeue() : new HttpResponseMessage(HttpStatusCode.OK);
}
}
private sealed class SingleHttpClientFactory : IHttpClientFactory
{
private readonly HttpClient _client;
public SingleHttpClientFactory(HttpClient client)
{
_client = client;
}
public HttpClient CreateClient(string name) => _client;
}
}

Some files were not shown because too many files have changed in this diff Show More