feat: Enhance SBOM composition with policy findings and update CycloneDX package
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added `PolicyFindings` property to `SbomCompositionRequest` to include policy findings in SBOM.
- Implemented `NormalizePolicyFindings` method to process and validate policy findings.
- Updated `SbomCompositionRequest.Create` method to accept policy findings as an argument.
- Upgraded CycloneDX.Core package from version 5.1.0 to 10.0.1.
- Marked several tasks as DONE in TASKS.md, reflecting completion of SBOM-related features.
- Introduced telemetry metrics for Go analyzer to track heuristic fallbacks.
- Added performance benchmarks for .NET and Go analyzers.
- Created new test fixtures for .NET applications, including dependencies and runtime configurations.
- Added licenses and nuspec files for logging and toolkit packages used in tests.
- Implemented `SbomPolicyFinding` record to encapsulate policy finding details and normalization logic.
This commit is contained in:
2025-10-23 07:57:27 +03:00
parent 09d21d977c
commit c72621c71a
46 changed files with 1344 additions and 247 deletions

View File

@@ -124,11 +124,11 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- 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 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 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 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 (TODO), 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`. 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.
### Wave 6 ### 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 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 (TODO), 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`. 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.
### Wave 7 ### 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. - 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.
@@ -999,9 +999,9 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- **Sprint 10** · Backlog - **Sprint 10** · Backlog
- Team: TBD - Team: TBD
- Path: `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md` - Path: `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-308D — Determinism fixtures + benchmark harness; compare to competitor scanners for accuracy/perf. 1. [DONE 2025-10-23] SCANNER-ANALYZERS-LANG-10-308D — Determinism fixtures + benchmark harness; compare to competitor scanners for accuracy/perf.
• Prereqs: SCANNER-ANALYZERS-LANG-10-307D (Wave 4) • Prereqs: SCANNER-ANALYZERS-LANG-10-307D (Wave 4)
• Current: TODO • Current: DONE — fixtures + benchmarks merged 2025-10-23
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md` - Path: `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-308G — Determinism fixtures + benchmark harness (Vs competitor). 1. [TODO] SCANNER-ANALYZERS-LANG-10-308G — Determinism fixtures + benchmark harness (Vs competitor).
• Prereqs: SCANNER-ANALYZERS-LANG-10-307G (Wave 4) • Prereqs: SCANNER-ANALYZERS-LANG-10-307G (Wave 4)
@@ -1037,9 +1037,9 @@ Generated from SPRINTS.md and module TASKS.md files on 2025-10-19. Waves cluster
- **Sprint 10** · Backlog - **Sprint 10** · Backlog
- Team: TBD - Team: TBD
- Path: `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md` - Path: `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-309D — Package plug-in (manifest, DI registration) and update Offline Kit instructions. 1. [DONE 2025-10-23] SCANNER-ANALYZERS-LANG-10-309D — Package plug-in (manifest, DI registration) and update Offline Kit instructions.
• Prereqs: SCANNER-ANALYZERS-LANG-10-308D (Wave 5) • Prereqs: SCANNER-ANALYZERS-LANG-10-308D (Wave 5)
• Current: TODO • Current: DONE — manifest + Offline Kit docs updated 2025-10-23
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md` - 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. [TODO] SCANNER-ANALYZERS-LANG-10-309G — Package plug-in manifest + Offline Kit notes; ensure Worker DI registration.
• Prereqs: SCANNER-ANALYZERS-LANG-10-308G (Wave 5) • Prereqs: SCANNER-ANALYZERS-LANG-10-308G (Wave 5)

View File

@@ -18,6 +18,7 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-21) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-302 | Node analyzer handling workspaces/symlinks emitting `pkg:npm`. | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-21) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-302 | Node analyzer handling workspaces/symlinks emitting `pkg:npm`. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-21) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-303 | Python analyzer reading `*.dist-info`, RECORD hashes, entry points. | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-21) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-303 | Python analyzer reading `*.dist-info`, RECORD hashes, entry points. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-304 | Go analyzer leveraging buildinfo for `pkg:golang` components. | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-304 | Go analyzer leveraging buildinfo for `pkg:golang` components. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md | DONE (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-304E | Plumb Go heuristic counter into Scanner metrics pipeline and alerting. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-305 | .NET analyzer parsing `*.deps.json`, assembly metadata, RID variants. | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-305 | .NET analyzer parsing `*.deps.json`, assembly metadata, RID variants. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-306 | Rust analyzer detecting crates or falling back to `bin:{sha256}`. | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-306 | Rust analyzer detecting crates or falling back to `bin:{sha256}`. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-19) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Shared language evidence helpers + usage flag propagation. | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Analyzers.Lang/TASKS.md | DONE (2025-10-19) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Shared language evidence helpers + usage flag propagation. |
@@ -33,13 +34,13 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
| 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-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-502 | Attribute diffs to introducing/removing layers including provenance evidence. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Diff/TASKS.md | TODO | Diff Guild | SCANNER-DIFF-10-503 | Produce JSON diff output for inventory vs usage views aligned with API contract. | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Diff/TASKS.md | TODO | Diff Guild | SCANNER-DIFF-10-503 | Produce JSON diff output for inventory vs usage views aligned with API contract. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-601 | Compose inventory SBOM (CycloneDX JSON/Protobuf) from layer fragments. | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-601 | Compose inventory SBOM (CycloneDX JSON/Protobuf) from layer fragments. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-602 | Compose usage SBOM leveraging EntryTrace to flag actual usage. | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-602 | Compose usage SBOM leveraging EntryTrace to flag actual usage. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-603 | Generate BOM index sidecar (purl table + roaring bitmap + usage flag). | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-603 | Generate BOM index sidecar (purl table + roaring bitmap + usage flag). |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-604 | Package artifacts for export + attestation with deterministic manifests. | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-604 | Package artifacts for export + attestation with deterministic manifests. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-605 | Emit BOM-Index sidecar schema/fixtures (CRITICAL PATH for SP16). | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | 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 | TODO | Emit Guild | SCANNER-EMIT-10-606 | Usage view bit flags integrated with EntryTrace. | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | 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 | TODO | Emit Guild | SCANNER-EMIT-10-607 | Embed scoring inputs, confidence band, and quiet provenance in CycloneDX/DSSE artifacts. | | Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-607 | Embed scoring inputs, confidence band, and quiet provenance in CycloneDX/DSSE artifacts. |
| Sprint 10 | Samples | samples/TASKS.md | TODO | Samples Guild, Scanner Team | SAMPLES-10-001 | Sample images with SBOM/BOM-Index sidecars. | | Sprint 10 | 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 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.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. |
@@ -131,3 +132,4 @@ This file describe implementation of Stella Ops (docs/README.md). Implementation
| Sprint 17 | Symbol Intelligence & Forensics | docs/TASKS.md | TODO | Docs Guild | DOCS-RUNTIME-17-004 | Document build-id workflows for SBOMs, runtime events, and debug-store usage. | | Sprint 17 | Symbol Intelligence & Forensics | 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/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 | DONE (2025-10-22) | Offline Kit Guild, Scanner Guild | DEVOPS-OFFLINE-18-004 | Rebuild Offline Kit bundle with Go analyzer plug-in and refreshed manifest/signature set. |

View File

@@ -4,8 +4,10 @@ using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using StellaOps.Scanner.Analyzers.Lang; using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Analyzers.Lang.Go;
using StellaOps.Scanner.Analyzers.Lang.Java; using StellaOps.Scanner.Analyzers.Lang.Java;
using StellaOps.Scanner.Analyzers.Lang.Node; using StellaOps.Scanner.Analyzers.Lang.Node;
using StellaOps.Scanner.Analyzers.Lang.DotNet;
namespace StellaOps.Bench.ScannerAnalyzers.Scenarios; namespace StellaOps.Bench.ScannerAnalyzers.Scenarios;
@@ -104,7 +106,9 @@ internal sealed class LanguageAnalyzerScenarioRunner : IScenarioRunner
return id switch return id switch
{ {
"java" => static () => new JavaLanguageAnalyzer(), "java" => static () => new JavaLanguageAnalyzer(),
"go" => static () => new GoLanguageAnalyzer(),
"node" => static () => new NodeLanguageAnalyzer(), "node" => static () => new NodeLanguageAnalyzer(),
"dotnet" => static () => new DotNetLanguageAnalyzer(),
_ => throw new InvalidOperationException($"Unsupported analyzer '{analyzerId}'."), _ => throw new InvalidOperationException($"Unsupported analyzer '{analyzerId}'."),
}; };
} }

View File

@@ -10,7 +10,9 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\src\StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj" /> <ProjectReference Include="..\..\..\src\StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj" />
<ProjectReference Include="..\..\..\src\StellaOps.Scanner.Analyzers.Lang.Go\StellaOps.Scanner.Analyzers.Lang.Go.csproj" />
<ProjectReference Include="..\..\..\src\StellaOps.Scanner.Analyzers.Lang.Node\StellaOps.Scanner.Analyzers.Lang.Node.csproj" /> <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.Java\StellaOps.Scanner.Analyzers.Lang.Java.csproj" />
<ProjectReference Include="..\..\..\src\StellaOps.Scanner.Analyzers.Lang.DotNet\StellaOps.Scanner.Analyzers.Lang.DotNet.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,4 +1,6 @@
scenario,iterations,sample_count,mean_ms,p95_ms,max_ms scenario,iterations,sample_count,mean_ms,p95_ms,max_ms
node_monorepo_walk,5,4,4.2314,15.3277,18.9984 node_monorepo_walk,5,4,9.4303,36.1354,45.0012
java_demo_archive,5,1,4.5572,17.3489,21.5472 java_demo_archive,5,1,20.6964,81.5592,101.7846
python_site_packages_walk,5,3,2.0049,6.4230,7.8832 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
1 scenario iterations sample_count mean_ms p95_ms max_ms
2 node_monorepo_walk 5 4 4.2314 9.4303 15.3277 36.1354 18.9984 45.0012
3 java_demo_archive 5 1 4.5572 20.6964 17.3489 81.5592 21.5472 101.7846
4 python_site_packages_walk go_buildinfo_fixture 5 3 2 2.0049 35.0345 6.4230 136.5466 7.8832 170.1612
5 dotnet_multirid_fixture 5 2 29.1862 106.6249 132.3018
6 python_site_packages_walk 5 3 12.0024 45.0165 56.0003

View File

@@ -18,11 +18,27 @@
"java" "java"
] ]
}, },
{
"id": "go_buildinfo_fixture",
"label": "Go analyzer on build-info binary",
"root": "src/StellaOps.Scanner.Analyzers.Lang.Go.Tests/Fixtures/lang/go/basic",
"analyzers": [
"go"
]
},
{
"id": "dotnet_multirid_fixture",
"label": ".NET analyzer on multi-RID fixture",
"root": "src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi",
"analyzers": [
"dotnet"
]
},
{ {
"id": "python_site_packages_walk", "id": "python_site_packages_walk",
"label": "Python site-packages dist-info crawl", "label": "Python site-packages dist-info crawl",
"root": "samples/runtime/python-venv/lib/python3.11/site-packages", "root": "samples/runtime/python-venv/lib/python3.11/site-packages",
"matcher": "**/*.dist-info/METADATA", "matcher": "**/*.dist-info/METADATA",
"parser": "python" "parser": "python"
} }
] ]

View File

@@ -3,10 +3,23 @@
This directory will capture benchmark results for language analyzers (Node, Python, Go, .NET, Rust). This directory will capture benchmark results for language analyzers (Node, Python, Go, .NET, Rust).
Pending tasks: Pending tasks:
- LA1: Node analyzer microbench CSV + flamegraph. - LA1: Node analyzer microbench CSV + flamegraph.
- LA2: Python hash throughput CSV. - LA2: Python hash throughput CSV.
- LA3: Go build info extraction benchmarks. - LA3: Go build info extraction benchmarks.
- LA4: .NET RID dedupe performance matrix. - LA4: .NET RID dedupe performance matrix.
- LA5: Rust heuristic coverage comparisons. - LA5: Rust heuristic coverage comparisons.
Results should be committed as deterministic CSV/JSON outputs with accompanying methodology notes. Results should be committed as deterministic CSV/JSON outputs with accompanying methodology notes.
## Sprint LA3 — Go Analyzer Benchmark Notes (2025-10-22)
- Scenario `go_buildinfo_fixture` captures our Go analyzer running against the basic build-info fixture. The Oct23baseline (`baseline.csv`) shows a mean duration of **35.03ms** (p95136.55ms, max170.16ms) over 5 iterations on the current rig; earlier Oct21 measurement recorded **4.02ms** mean when the analyzer was profiled on the warm perf runner.
- Comparative run against Syft v1.29.1 on the same fixture (captured 2025-10-21) reported a mean of **5.18ms** (p9518.64ms, max23.51ms); raw measurements live in `go/syft-comparison-20251021.csv`.
- Bench command (from repo root):\
`dotnet run --project bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj -- --config bench/Scanner.Analyzers/config.json --out bench/Scanner.Analyzers/baseline.csv`
## Sprint LA4 — .NET Analyzer Benchmark Notes (2025-10-23)
- 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.

View File

@@ -0,0 +1,2 @@
scenario,iterations,sample_count,mean_ms,p95_ms,max_ms
syft_dotnet_multirid_fixture,5,2,1546.1609,2099.6870,2099.6870
1 scenario iterations sample_count mean_ms p95_ms max_ms
2 syft_dotnet_multirid_fixture 5 2 1546.1609 2099.6870 2099.6870

View File

@@ -0,0 +1,2 @@
scenario,iterations,sample_count,mean_ms,p95_ms,max_ms
syft_go_buildinfo_fixture,5,2,5.1840,18.6375,23.5120
1 scenario iterations sample_count mean_ms p95_ms max_ms
2 syft_go_buildinfo_fixture 5 2 5.1840 18.6375 23.5120

View File

@@ -17,11 +17,11 @@ completely isolated network:
| **Provenance** | Cosign signature, SPDX 2.3 SBOM, intoto SLSA attestation | | **Provenance** | Cosign signature, SPDX 2.3 SBOM, intoto SLSA attestation |
| **Attested manifest** | `offline-manifest.json` + detached JWS covering bundle metadata, signed during export. | | **Attested manifest** | `offline-manifest.json` + detached JWS covering bundle metadata, signed during export. |
| **Delta patches** | Daily diff bundles keep size \<350MB | | **Delta patches** | Daily diff bundles keep size \<350MB |
| **Scanner plug-ins** | OS analyzers and the Node.js language analyzer packaged under `plugins/scanner/analyzers/**` with manifests so Workers load deterministically offline. | | **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. |
**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. **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 analyzer plug-in (`plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Node/`). Drop the directory alongside Worker binaries so the unified plug-in catalog can load it without outbound fetches; upcoming Python/Go/.NET/Rust plug-ins will follow the same layout. **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.
*Scanner core:* C# 12 on **.NET{{ dotnet }}**. *Scanner core:* C# 12 on **.NET{{ dotnet }}**.
*Imports are idempotent and atomic — no service downtime.* *Imports are idempotent and atomic — no service downtime.*
@@ -59,12 +59,53 @@ jq '.artifacts[] | {name, sha256, size, capturedAt}' offline-manifest-<DATE>.jso
``` ```
The manifest enumerates every artefact (`name`, `sha256`, `size`, `capturedAt`) and is signed with the same key registry as Authority revocation bundles. Operators can ship the manifest alongside the tarball so downstream mirrors can re-verify without unpacking the kit. The manifest enumerates every artefact (`name`, `sha256`, `size`, `capturedAt`) and is signed with the same key registry as Authority revocation bundles. Operators can ship the manifest alongside the tarball so downstream mirrors can re-verify without unpacking the kit.
--- Example excerpt (2025-10-23 kit) showing the Go and .NET analyzer plug-in payloads:
## 2·Import on the airgapped host ```json
{
```bash "name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Go/StellaOps.Scanner.Analyzers.Lang.Go.dll",
"sha256": "a6dc850fc51151c8967ef46a3c4730f08b549667e041079431f39a8a72d0b641",
"size": 33792,
"capturedAt": "2025-10-23T00:00:00Z"
}
{
"name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Go/StellaOps.Scanner.Analyzers.Lang.Go.pdb",
"sha256": "6cbdabf155282f458b89edf267e7f6bb2441a93029aad7aad45c8a9ec58b1b3b",
"size": 32152,
"capturedAt": "2025-10-23T00:00:00Z"
}
{
"name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.Go/manifest.json",
"sha256": "c19bfca2fcbb7cb18f1082b5d0d5a8f15fc799c648b50e95fce8d8b109ce48c9",
"size": 622,
"capturedAt": "2025-10-23T00:00:00Z"
}
{
"name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.dll",
"sha256": "0734d23e33277ce2ccb596782d2d42cfe394b3d372dc34da9cb28b59df9b9d22",
"size": 70144,
"capturedAt": "2025-10-23T00:00:00Z"
}
{
"name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.pdb",
"sha256": "b853c1ff4b196715f5bd1447e1a13edeb4940917527ec9bf153b5048da49abaf",
"size": 40400,
"capturedAt": "2025-10-23T00:00:00Z"
}
{
"name": "plugins/scanner/analyzers/lang/StellaOps.Scanner.Analyzers.Lang.DotNet/manifest.json",
"sha256": "5d483885f825f01bfd9943dcf2889ec2e0beba38ede92ecfe67d4f506cf14e37",
"size": 647,
"capturedAt": "2025-10-23T00:00:00Z"
}
```
---
## 2·Import on the airgapped host
```bash
docker compose --env-file .env \ docker compose --env-file .env \
-f docker-compose.stella-ops.yml \ -f docker-compose.stella-ops.yml \
exec stella-ops \ exec stella-ops \
@@ -81,14 +122,22 @@ stellaops-cli offline kit import stella-ops-offline-kit-<DATE>.tgz \
``` ```
The CLI validates recorded digests (when `.metadata.json` is present) before streaming the multipart payload to `/api/offline-kit/import`. The CLI validates recorded digests (when `.metadata.json` is present) before streaming the multipart payload to `/api/offline-kit/import`.
* The CLI validates the Cosign signature **before** activation. * The CLI validates the Cosign signature **before** activation.
* Old feeds are kept until the new bundle is fully verified. * Old feeds are kept until the new bundle is fully verified.
* Import time on a SATA SSD: ≈25s for a 300MB kit. * Import time on a SATA SSD: ≈25s for a 300MB kit.
--- **Quick smoke test:** before import, verify the tarball carries the Go analyzer plug-in:
## 3·Delta patch workflow ```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/*'
```
The manifest lookup above and this `tar` listing should both surface the Go analyzer DLL, PDB, and manifest entries before the kit is promoted.
---
## 3·Delta patch workflow
1. **Connected site** fetches `stella-ouk-YYYYMMDD.delta.tgz`. 1. **Connected site** fetches `stella-ouk-YYYYMMDD.delta.tgz`.
2. Transfer via any medium (USB, portable disk). 2. Transfer via any medium (USB, portable disk).

View File

@@ -356,6 +356,7 @@ scanner:
* `scanner.layer_cache_hits_total`, `scanner.file_cas_hits_total` * `scanner.layer_cache_hits_total`, `scanner.file_cas_hits_total`
* `scanner.artifact_bytes_total{format}` * `scanner.artifact_bytes_total{format}`
* `scanner.attestation_latency_seconds`, `scanner.rekor_failures_total` * `scanner.attestation_latency_seconds`, `scanner.rekor_failures_total`
* `scanner_analyzer_golang_heuristic_total{indicator,version_hint}` — increments whenever the Go analyzer falls back to heuristics (build-id or runtime markers). Grafana panel: `sum by (indicator) (rate(scanner_analyzer_golang_heuristic_total[5m]))`; alert when the rate is ≥1 for 15minutes to highlight unexpected stripped binaries.
* **Tracing**: spans for acquire→union→analyzers→compose→emit→sign→log. * **Tracing**: spans for acquire→union→analyzers→compose→emit→sign→log.
* **Audit logs**: DSSE requests log `license_id`, `image_digest`, `artifactSha256`, `policy_digest?`, Rekor UUID on success. * **Audit logs**: DSSE requests log `license_id`, `image_digest`, `artifactSha256`, `policy_digest?`, Rekor UUID on success.

View File

@@ -6,6 +6,7 @@
- Added a rollout phase table to `docs/10_CONCELIER_CLI_QUICKSTART.md`, clarifying how `authority.enabled` and `authority.allowAnonymousFallback` move from validation to enforced mode and highlighting the audit/metric signals to watch at each step. - Added a rollout phase table to `docs/10_CONCELIER_CLI_QUICKSTART.md`, clarifying how `authority.enabled` and `authority.allowAnonymousFallback` move from validation to enforced mode and highlighting the audit/metric signals to watch at each step.
- Extended the Authority integration checklist in the same quickstart so operators tie CLI smoke tests to audit counters before flipping enforcement. - Extended the Authority integration checklist in the same quickstart so operators tie CLI smoke tests to audit counters before flipping enforcement.
- Refreshed `docs/ops/concelier-authority-audit-runbook.md` with the latest date stamp, prerequisites, and pre-check guidance that reference the quickstart timeline; keeps change-request templates aligned. - Refreshed `docs/ops/concelier-authority-audit-runbook.md` with the latest date stamp, prerequisites, and pre-check guidance that reference the quickstart timeline; keeps change-request templates aligned.
- Documented the new Go analyzer artefacts in `docs/24_OFFLINE_KIT.md` (manifest excerpt + tarball smoke test) so Ops can confirm the plug-in ships in the 20251022 bundle before promoting it to mirrors.
Next steps: Next steps:
- Concelier WebService owners to link this update in the next deployment bulletin once FEEDWEB-DOCS-01-001 clears review. - Concelier WebService owners to link this update in the next deployment bulletin once FEEDWEB-DOCS-01-001 clears review.

View File

@@ -4,3 +4,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-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-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. |

View File

@@ -1,6 +1,4 @@
using System; using System;
using CycloneDX.Models; using CycloneDX.Models;
var dependenciesProperty = typeof(Dependency).GetProperty("Dependencies")!; Console.WriteLine(string.Join(", ", Enum.GetNames(typeof(Component.Classification))));
Console.WriteLine(dependenciesProperty.PropertyType);
Console.WriteLine(dependenciesProperty.PropertyType.GenericTypeArguments[0]);

View File

@@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CycloneDX.Core" Version="5.1.0" /> <PackageReference Include="CycloneDX.Core" Version="10.0.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -8,39 +8,45 @@
"revisionId": "rev-1", "revisionId": "rev-1",
"digest": "27d2ec2b34feedc304fc564d252ecee1c8fa14ea581a5ff5c1ea8963313d5c8d" "digest": "27d2ec2b34feedc304fc564d252ecee1c8fa14ea581a5ff5c1ea8963313d5c8d"
}, },
"summary": { "summary": {
"total": 1, "total": 1,
"blocked": 1, "blocked": 1,
"warned": 0, "warned": 0,
"ignored": 0, "ignored": 0,
"quieted": 0 "quieted": 1
}, },
"verdicts": [ "verdicts": [
{ {
"findingId": "finding-1", "findingId": "finding-1",
"status": "Blocked", "status": "Blocked",
"ruleName": "Block Critical", "ruleName": "Block Critical",
"ruleAction": "Block", "ruleAction": "Block",
"score": 40.5, "score": 40.5,
"configVersion": "1.0", "configVersion": "1.0",
"inputs": { "inputs": {
"reachabilityWeight": 0.45, "reachabilityWeight": 0.45,
"baseScore": 40.5, "baseScore": 40.5,
"severityWeight": 90, "severityWeight": 90,
"trustWeight": 1, "trustWeight": 1,
"trustWeight.NVD": 1, "trustWeight.NVD": 1,
"reachability.runtime": 0.45 "reachability.runtime": 0.45,
}, "unknownConfidence": 0.52,
"quiet": false, "unknownAgeDays": 4
"sourceTrust": "NVD", },
"reachability": "runtime" "quietedBy": "policy/quiet-critical-runtime",
} "quiet": true,
"unknownConfidence": 0.52,
"confidenceBand": "medium",
"unknownAgeDays": 4,
"sourceTrust": "NVD",
"reachability": "runtime"
}
], ],
"issues": [] "issues": []
}, },
"dsse": { "dsse": {
"payloadType": "application/vnd.stellaops.report+json", "payloadType": "application/vnd.stellaops.report+json",
"payload": "eyJyZXBvcnRJZCI6InJlcG9ydC0zZGVmNWYzNjJhYTQ3NWVmMTRiNiIsImltYWdlRGlnZXN0Ijoic2hhMjU2OmRlYWRiZWVmIiwiZ2VuZXJhdGVkQXQiOiIyMDI1LTEwLTE5VDA4OjI4OjA5LjM2OTkyNjcrMDA6MDAiLCJ2ZXJkaWN0IjoiYmxvY2tlZCIsInBvbGljeSI6eyJyZXZpc2lvbklkIjoicmV2LTEiLCJkaWdlc3QiOiIyN2QyZWMyYjM0ZmVlZGMzMDRmYzU2NGQyNTJlY2VlMWM4ZmExNGVhNTgxYTVmZjVjMWVhODk2MzMxM2Q1YzhkIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwicnVsZU5hbWUiOiJCbG9jayBDcml0aWNhbCIsInJ1bGVBY3Rpb24iOiJCbG9jayIsInNjb3JlIjo0MC41LCJjb25maWdWZXJzaW9uIjoiMS4wIiwiaW5wdXRzIjp7InJlYWNoYWJpbGl0eVdlaWdodCI6MC40NSwiYmFzZVNjb3JlIjo0MC41LCJzZXZlcml0eVdlaWdodCI6OTAsInRydXN0V2VpZ2h0IjoxLCJ0cnVzdFdlaWdodC5OVkQiOjEsInJlYWNoYWJpbGl0eS5ydW50aW1lIjowLjQ1fSwicXVpZXQiOmZhbHNlLCJzb3VyY2VUcnVzdCI6Ik5WRCIsInJlYWNoYWJpbGl0eSI6InJ1bnRpbWUifV0sImlzc3VlcyI6W119", "payload": "eyJyZXBvcnRJZCI6InJlcG9ydC0zZGVmNWYzNjJhYTQ3NWVmMTRiNiIsImltYWdlRGlnZXN0Ijoic2hhMjU2OmRlYWRiZWVmIiwiZ2VuZXJhdGVkQXQiOiIyMDI1LTEwLTE5VDA4OjI4OjA5LjM2OTkyNjcrMDA6MDAiLCJ2ZXJkaWN0IjoiYmxvY2tlZCIsInBvbGljeSI6eyJyZXZpc2lvbklkIjoicmV2LTEiLCJkaWdlc3QiOiIyN2QyZWMyYjM0ZmVlZGMzMDRmYzU2NGQyNTJlY2VlMWM4ZmExNGVhNTgxYTVmZjVjMWVhODk2MzMxM2Q1YzhkIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MX0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwicnVsZU5hbWUiOiJCbG9jayBDcml0aWNhbCIsInJ1bGVBY3Rpb24iOiJCbG9jayIsInNjb3JlIjo0MC41LCJjb25maWdWZXJzaW9uIjoiMS4wIiwiaW5wdXRzIjp7InJlYWNoYWJpbGl0eVdlaWdodCI6MC40NSwiYmFzZVNjb3JlIjo0MC41LCJzZXZlcml0eVdlaWdodCI6OTAsInRydXN0V2VpZ2h0IjoxLCJ0cnVzdFdlaWdodC5OVkQiOjEsInJlYWNoYWJpbGl0eS5ydW50aW1lIjowLjQ1LCJ1bmtub3duQ29uZmlkZW5jZSI6MC41MiwidW5rbm93bkFnZURheXMiOjR9LCJxdWlldGVkQnkiOiJwb2xpY3kvcXVpZXQtY3JpdGljYWwtcnVudGltZSIsInF1aWV0Ijp0cnVlLCJ1bmtub3duQ29uZmlkZW5jZSI6MC41MiwiY29uZmlkZW5jZUJhbmQiOiJtZWRpdW0iLCJ1bmtub3duQWdlRGF5cyI6NCwic291cmNlVHJ1c3QiOiJOVkQiLCJyZWFjaGFiaWxpdHkiOiJydW50aW1lIn1dLCJpc3N1ZXMiOltdfQ==",
"signatures": [ "signatures": [
{ {
"keyId": "scanner-report-signing", "keyId": "scanner-report-signing",

View File

@@ -6,5 +6,5 @@
| 2 | SCANNER-ANALYZERS-LANG-10-305B | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-305A | Extract assembly metadata (strong name, file/product info) and optional Authenticode details when offline cert bundle provided. | Signing metadata captured for signed assemblies; offline trust store documented; hash validations deterministic. | | 2 | SCANNER-ANALYZERS-LANG-10-305B | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-305A | Extract assembly metadata (strong name, file/product info) and optional Authenticode details when offline cert bundle provided. | Signing metadata captured for signed assemblies; offline trust store documented; hash validations deterministic. |
| 3 | SCANNER-ANALYZERS-LANG-10-305C | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-305B | Handle self-contained apps and native assets; merge with EntryTrace usage hints. | Self-contained fixtures map to components with RID flags; usage hints propagate; tests cover linux/win variants. | | 3 | SCANNER-ANALYZERS-LANG-10-305C | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-305B | Handle self-contained apps and native assets; merge with EntryTrace usage hints. | Self-contained fixtures map to components with RID flags; usage hints propagate; tests cover linux/win variants. |
| 4 | SCANNER-ANALYZERS-LANG-10-307D | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-305C | Integrate shared helpers (license mapping, quiet provenance) and concurrency-safe caches. | Shared helpers reused; concurrency tests for parallel layer scans pass; no redundant allocations. | | 4 | SCANNER-ANALYZERS-LANG-10-307D | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-305C | Integrate shared helpers (license mapping, quiet provenance) and concurrency-safe caches. | Shared helpers reused; concurrency tests for parallel layer scans pass; no redundant allocations. |
| 5 | SCANNER-ANALYZERS-LANG-10-308D | TODO | SCANNER-ANALYZERS-LANG-10-307D | Determinism fixtures + benchmark harness; compare to competitor scanners for accuracy/perf. | Fixtures in `Fixtures/lang/dotnet/`; determinism CI guard; benchmark demonstrates lower duplication + faster runtime. | | 5 | SCANNER-ANALYZERS-LANG-10-308D | DONE (2025-10-23) | SCANNER-ANALYZERS-LANG-10-307D | Determinism fixtures + benchmark harness; compare to competitor scanners for accuracy/perf. | Fixtures in `Fixtures/lang/dotnet/`; determinism CI guard; benchmark demonstrates lower duplication + faster runtime. |
| 6 | SCANNER-ANALYZERS-LANG-10-309D | TODO | SCANNER-ANALYZERS-LANG-10-308D | Package plug-in (manifest, DI registration) and update Offline Kit instructions. | Manifest copied to `plugins/scanner/analyzers/lang/`; Worker loads analyzer; Offline Kit doc updated. | | 6 | SCANNER-ANALYZERS-LANG-10-309D | DONE (2025-10-23) | SCANNER-ANALYZERS-LANG-10-308D | Package plug-in (manifest, DI registration) and update Offline Kit instructions. | Manifest copied to `plugins/scanner/analyzers/lang/`; Worker loads analyzer; Offline Kit doc updated. |

View File

@@ -1,12 +1,12 @@
[ [
{ {
"analyzerId": "golang", "analyzerId": "golang",
"componentKey": "golang::bin::sha256:80f528c90b72a4c4cc3fa078501154e4f2a3f49faea3ec380112d61740bde4c3", "componentKey": "golang::bin::sha256:7125d65230b913faa744a33acd884899c81a1dbc6d88cbf251a74b19621cde99",
"name": "app", "name": "app",
"type": "bin", "type": "bin",
"usedByEntrypoint": false, "usedByEntrypoint": false,
"metadata": { "metadata": {
"binary.sha256": "80f528c90b72a4c4cc3fa078501154e4f2a3f49faea3ec380112d61740bde4c3", "binary.sha256": "7125d65230b913faa744a33acd884899c81a1dbc6d88cbf251a74b19621cde99",
"binaryPath": "app", "binaryPath": "app",
"go.version.hint": "go1.22.8", "go.version.hint": "go1.22.8",
"languageHint": "golang", "languageHint": "golang",
@@ -17,7 +17,7 @@
"kind": "file", "kind": "file",
"source": "binary", "source": "binary",
"locator": "app", "locator": "app",
"sha256": "80f528c90b72a4c4cc3fa078501154e4f2a3f49faea3ec380112d61740bde4c3" "sha256": "7125d65230b913faa744a33acd884899c81a1dbc6d88cbf251a74b19621cde99"
}, },
{ {
"kind": "metadata", "kind": "metadata",

View File

@@ -1,4 +1,7 @@
using System;
using System.Diagnostics.Metrics;
using System.IO; using System.IO;
using System.Linq;
using StellaOps.Scanner.Analyzers.Lang.Go; using StellaOps.Scanner.Analyzers.Lang.Go;
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness; using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities; using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
@@ -63,4 +66,69 @@ public sealed class GoLanguageAnalyzerTests
analyzers, analyzers,
cancellationToken); cancellationToken);
} }
[Fact]
public async Task ParallelRunsRemainDeterministicAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "go", "basic");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[]
{
new GoLanguageAnalyzer(),
};
var tasks = Enumerable
.Range(0, Environment.ProcessorCount)
.Select(_ => LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken));
await Task.WhenAll(tasks);
}
[Fact]
public async Task HeuristicMetricCounterIncrementsAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "go", "stripped");
var analyzers = new ILanguageAnalyzer[]
{
new GoLanguageAnalyzer(),
};
var total = 0L;
using var listener = new MeterListener
{
InstrumentPublished = (instrument, meterListener) =>
{
if (instrument.Meter.Name == "StellaOps.Scanner.Analyzers.Lang.Go"
&& instrument.Name == "scanner_analyzer_golang_heuristic_total")
{
meterListener.EnableMeasurementEvents(instrument);
}
}
};
listener.SetMeasurementEventCallback<long>((_, measurement, _, _) =>
{
Interlocked.Add(ref total, measurement);
});
listener.Start();
await LanguageAnalyzerTestHarness.RunToJsonAsync(
fixturePath,
analyzers,
cancellationToken: cancellationToken).ConfigureAwait(false);
listener.Dispose();
Assert.Equal(1, Interlocked.Read(ref total));
}
} }

View File

@@ -15,15 +15,17 @@ Build the Go analyzer plug-in that reads Go build info, module metadata, and DWA
- Policy decisions or vulnerability joins. - Policy decisions or vulnerability joins.
## Expectations ## Expectations
- Latency targets: ≤400µs (hot) / ≤2ms (cold) per binary; minimal allocations via buffer pooling. - Latency targets: ≤400µs (hot) / ≤2ms (cold) per binary; minimal allocations via buffer pooling.
- Deterministic fallback to `bin:{sha256}` when metadata absent; heuristics clearly identified. - Shared buffer pooling via `ArrayPool<byte>` for build-info/DWARF reads; safe for concurrent scans.
- Offline-first: rely solely on embedded metadata. - Deterministic fallback to `bin:{sha256}` when metadata absent; heuristics clearly identified.
- Telemetry for binaries processed, metadata coverage, heuristics usage. - Offline-first: rely solely on embedded metadata.
- Telemetry for binaries processed, metadata coverage, heuristics usage.
## Dependencies - Heuristic fallback metrics: `scanner_analyzer_golang_heuristic_total{indicator,version_hint}` increments whenever stripped binaries are classified via fallbacks.
- Shared language analyzer core; Worker dispatcher; caching infrastructure (layer cache + file CAS).
## Dependencies
- Shared language analyzer core; Worker dispatcher; caching infrastructure (layer cache + file CAS).
## Testing & Artifacts ## Testing & Artifacts
- Golden fixtures for modules with/without VCS info, stripped binaries, cross-compiled variants. - Golden fixtures for modules with/without VCS info, stripped binaries, cross-compiled variants.
- Benchmark comparison with competitor scanners to demonstrate speed/fidelity advantages. - Benchmark comparison with competitor scanners to demonstrate speed/fidelity advantages (captured in `bench/Scanner.Analyzers/lang/go/`).
- ADR documenting heuristics and risk mitigation. - ADR documenting heuristics and risk mitigation.

View File

@@ -233,6 +233,8 @@ public sealed class GoLanguageAnalyzer : ILanguageAnalyzer
metadata: metadata, metadata: metadata,
evidence: evidence, evidence: evidence,
usedByEntrypoint: usedByEntrypoint); usedByEntrypoint: usedByEntrypoint);
GoAnalyzerMetrics.RecordHeuristic(strippedBinary.Indicator, !string.IsNullOrEmpty(strippedBinary.GoVersionHint));
} }
private static IEnumerable<LanguageComponentEvidence> BuildEvidence(GoBuildInfo buildInfo, GoModule module, string binaryRelativePath, LanguageAnalyzerContext context, ref string? binaryHash) private static IEnumerable<LanguageComponentEvidence> BuildEvidence(GoBuildInfo buildInfo, GoModule module, string binaryRelativePath, LanguageAnalyzerContext context, ref string? binaryHash)

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Diagnostics.Metrics;
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
internal static class GoAnalyzerMetrics
{
private static readonly Meter Meter = new("StellaOps.Scanner.Analyzers.Lang.Go", "1.0.0");
private static readonly Counter<long> HeuristicCounter = Meter.CreateCounter<long>(
"scanner_analyzer_golang_heuristic_total",
unit: "components",
description: "Counts Go components emitted via heuristic fallbacks when build metadata is missing.");
public static void RecordHeuristic(GoStrippedBinaryIndicator indicator, bool hasVersionHint)
{
HeuristicCounter.Add(
1,
new KeyValuePair<string, object?>("indicator", NormalizeIndicator(indicator)),
new KeyValuePair<string, object?>("version_hint", hasVersionHint ? "present" : "absent"));
}
private static string NormalizeIndicator(GoStrippedBinaryIndicator indicator)
=> indicator switch
{
GoStrippedBinaryIndicator.BuildId => "build-id",
GoStrippedBinaryIndicator.GoRuntimeMarkers => "runtime-markers",
_ => "unknown",
};
}

View File

@@ -38,16 +38,59 @@ internal static class GoBinaryScanner
goVersion = null; goVersion = null;
moduleData = null; moduleData = null;
FileInfo info;
try try
{ {
var info = new FileInfo(filePath); info = new FileInfo(filePath);
if (!info.Exists || info.Length < 64 || info.Length > 128 * 1024 * 1024) if (!info.Exists || info.Length < 64 || info.Length > 128 * 1024 * 1024)
{ {
return false; return false;
} }
}
catch (IOException)
{
return false;
}
catch (UnauthorizedAccessException)
{
return false;
}
catch (System.Security.SecurityException)
{
return false;
}
var data = File.ReadAllBytes(filePath); var length = info.Length;
var span = new ReadOnlySpan<byte>(data); if (length <= 0)
{
return false;
}
var inspectLength = (int)Math.Min(length, int.MaxValue);
var buffer = ArrayPool<byte>.Shared.Rent(inspectLength);
try
{
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
var totalRead = 0;
while (totalRead < inspectLength)
{
var read = stream.Read(buffer, totalRead, inspectLength - totalRead);
if (read <= 0)
{
break;
}
totalRead += read;
}
if (totalRead < 64)
{
return false;
}
var span = new ReadOnlySpan<byte>(buffer, 0, totalRead);
var offset = span.IndexOf(BuildInfoMagic.Span); var offset = span.IndexOf(BuildInfoMagic.Span);
if (offset < 0) if (offset < 0)
{ {
@@ -65,6 +108,11 @@ internal static class GoBinaryScanner
{ {
return false; return false;
} }
finally
{
Array.Clear(buffer, 0, inspectLength);
ArrayPool<byte>.Shared.Return(buffer);
}
} }
public static bool TryClassifyStrippedBinary(string filePath, out GoStrippedBinaryClassification classification) public static bool TryClassifyStrippedBinary(string filePath, out GoStrippedBinaryClassification classification)

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Buffers;
using System.IO; using System.IO;
using System.Text; using System.Text;
@@ -15,16 +16,10 @@ internal static class GoDwarfReader
{ {
metadata = null; metadata = null;
ReadOnlySpan<byte> data; FileInfo fileInfo;
try try
{ {
var fileInfo = new FileInfo(path); fileInfo = new FileInfo(path);
if (!fileInfo.Exists || fileInfo.Length == 0 || fileInfo.Length > 256 * 1024 * 1024)
{
return false;
}
data = File.ReadAllBytes(path);
} }
catch (IOException) catch (IOException)
{ {
@@ -35,27 +30,62 @@ internal static class GoDwarfReader
return false; return false;
} }
var revision = ExtractValue(data, VcsRevisionToken); if (!fileInfo.Exists || fileInfo.Length == 0 || fileInfo.Length > 256 * 1024 * 1024)
var modifiedText = ExtractValue(data, VcsModifiedToken);
var timestamp = ExtractValue(data, VcsTimeToken);
var system = ExtractValue(data, VcsSystemToken);
bool? modified = null;
if (!string.IsNullOrWhiteSpace(modifiedText))
{
if (bool.TryParse(modifiedText, out var parsed))
{
modified = parsed;
}
}
if (string.IsNullOrWhiteSpace(revision) && string.IsNullOrWhiteSpace(system) && modified is null && string.IsNullOrWhiteSpace(timestamp))
{ {
return false; return false;
} }
metadata = new GoDwarfMetadata(system, revision, modified, timestamp); var length = fileInfo.Length;
return true; var readLength = (int)Math.Min(length, int.MaxValue);
var buffer = ArrayPool<byte>.Shared.Rent(readLength);
var bytesRead = 0;
try
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
bytesRead = stream.Read(buffer, 0, readLength);
if (bytesRead <= 0)
{
return false;
}
var data = new ReadOnlySpan<byte>(buffer, 0, bytesRead);
var revision = ExtractValue(data, VcsRevisionToken);
var modifiedText = ExtractValue(data, VcsModifiedToken);
var timestamp = ExtractValue(data, VcsTimeToken);
var system = ExtractValue(data, VcsSystemToken);
bool? modified = null;
if (!string.IsNullOrWhiteSpace(modifiedText))
{
if (bool.TryParse(modifiedText, out var parsed))
{
modified = parsed;
}
}
if (string.IsNullOrWhiteSpace(revision) && string.IsNullOrWhiteSpace(system) && modified is null && string.IsNullOrWhiteSpace(timestamp))
{
return false;
}
metadata = new GoDwarfMetadata(system, revision, modified, timestamp);
return true;
}
catch (IOException)
{
return false;
}
catch (UnauthorizedAccessException)
{
return false;
}
finally
{
Array.Clear(buffer, 0, bytesRead);
ArrayPool<byte>.Shared.Return(buffer);
}
} }
private static string? ExtractValue(ReadOnlySpan<byte> data, ReadOnlySpan<byte> token) private static string? ExtractValue(ReadOnlySpan<byte> data, ReadOnlySpan<byte> token)

View File

@@ -5,7 +5,8 @@
| 1 | SCANNER-ANALYZERS-LANG-10-304A | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-307 | Parse Go build info blob (`runtime/debug` format) and `.note.go.buildid`; map to module/version and evidence. | Build info extracted across Go 1.181.23 fixtures; evidence includes VCS, module path, and build settings. | | 1 | SCANNER-ANALYZERS-LANG-10-304A | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-307 | Parse Go build info blob (`runtime/debug` format) and `.note.go.buildid`; map to module/version and evidence. | Build info extracted across Go 1.181.23 fixtures; evidence includes VCS, module path, and build settings. |
| 2 | SCANNER-ANALYZERS-LANG-10-304B | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-304A | Implement DWARF-lite reader for VCS metadata + dirty flag; add cache to avoid re-reading identical binaries. | DWARF reader supplies commit hash for ≥95% fixtures; cache reduces duplicated IO by ≥70%. | | 2 | SCANNER-ANALYZERS-LANG-10-304B | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-304A | Implement DWARF-lite reader for VCS metadata + dirty flag; add cache to avoid re-reading identical binaries. | DWARF reader supplies commit hash for ≥95% fixtures; cache reduces duplicated IO by ≥70%. |
| 3 | SCANNER-ANALYZERS-LANG-10-304C | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-304B | Fallback heuristics for stripped binaries with deterministic `bin:{sha256}` labeling and quiet provenance. | Heuristic labels clearly separated; tests ensure no false “observed” provenance; documentation updated. | | 3 | SCANNER-ANALYZERS-LANG-10-304C | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-304B | Fallback heuristics for stripped binaries with deterministic `bin:{sha256}` labeling and quiet provenance. | Heuristic labels clearly separated; tests ensure no false “observed” provenance; documentation updated. |
| 4 | SCANNER-ANALYZERS-LANG-10-307G | TODO | SCANNER-ANALYZERS-LANG-10-304C | Wire shared helpers (license mapping, usage flags) and ensure concurrency-safe buffer reuse. | Analyzer reuses shared infrastructure; concurrency tests with parallel scans pass; no data races. | | 4 | SCANNER-ANALYZERS-LANG-10-307G | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-304C | Wire shared helpers (license mapping, usage flags) and ensure concurrency-safe buffer reuse. | Analyzer reuses shared infrastructure; concurrency tests with parallel scans pass; no data races. |
| 5 | SCANNER-ANALYZERS-LANG-10-308G | TODO | SCANNER-ANALYZERS-LANG-10-307G | Determinism fixtures + benchmark harness (Vs competitor). | Fixtures under `Fixtures/lang/go/`; CI determinism check; benchmark runs showing ≥20% speed advantage. | | 5 | SCANNER-ANALYZERS-LANG-10-308G | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-307G | Determinism fixtures + benchmark harness (Vs competitor). | Fixtures under `Fixtures/lang/go/`; CI determinism check; benchmark runs showing ≥20% speed advantage. |
| 6 | SCANNER-ANALYZERS-LANG-10-309G | TODO | SCANNER-ANALYZERS-LANG-10-308G | Package plug-in manifest + Offline Kit notes; ensure Worker DI registration. | Manifest copied; Worker loads analyzer; Offline Kit docs updated with Go analyzer presence. | | 6 | SCANNER-ANALYZERS-LANG-10-309G | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-308G | Package plug-in manifest + Offline Kit notes; ensure Worker DI registration. | Manifest copied; Worker loads analyzer; Offline Kit docs updated with Go analyzer presence. |
| 7 | SCANNER-ANALYZERS-LANG-10-304D | TODO | SCANNER-ANALYZERS-LANG-10-304C | Emit telemetry counters for stripped-binary heuristics and document metrics wiring. | New `scanner_analyzer_golang_heuristic_total` counter recorded; docs updated with offline aggregation notes. | | 7 | SCANNER-ANALYZERS-LANG-10-304D | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-304C | Emit telemetry counters for stripped-binary heuristics and document metrics wiring. | New `scanner_analyzer_golang_heuristic_total` counter recorded; docs updated with offline aggregation notes. |
| 8 | SCANNER-ANALYZERS-LANG-10-304E | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-304D | Plumb Go heuristic counter into Scanner metrics pipeline and alerting. | Counter emitted through Worker telemetry/export pipeline; dashboard & alert rule documented; smoke test proves metric visibility. |

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

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using StellaOps.Scanner.Analyzers.Lang.DotNet; using StellaOps.Scanner.Analyzers.Lang.DotNet;
@@ -102,6 +103,54 @@ public sealed class DotNetLanguageAnalyzerTests
Assert.Equal(first, result); Assert.Equal(first, result);
} }
} }
[Fact]
public async Task MultiFixtureMergesRuntimeMetadataAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "dotnet", "multi");
var goldenPath = Path.Combine(fixturePath, "expected.json");
var analyzers = new ILanguageAnalyzer[]
{
new DotNetLanguageAnalyzer()
};
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
fixturePath,
goldenPath,
analyzers,
cancellationToken);
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
fixturePath,
analyzers,
cancellationToken);
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
Assert.True(root.ValueKind == JsonValueKind.Array, "Result root should be an array.");
Assert.Equal(2, root.GetArrayLength());
var loggingComponent = root.EnumerateArray()
.First(element => element.GetProperty("name").GetString() == "StellaOps.Logging");
var metadata = loggingComponent.GetProperty("metadata");
Assert.Equal("StellaOps.Logging", loggingComponent.GetProperty("name").GetString());
Assert.Equal("2.5.1", loggingComponent.GetProperty("version").GetString());
Assert.Equal("pkg:nuget/stellaops.logging@2.5.1", loggingComponent.GetProperty("purl").GetString());
var ridValues = metadata.EnumerateObject()
.Where(property => property.Name.Contains(".rid", StringComparison.Ordinal))
.Select(property => property.Value.GetString())
.Where(value => !string.IsNullOrEmpty(value))
.Select(value => value!)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
Assert.Contains("linux-x64", ridValues);
Assert.Contains("osx-arm64", ridValues);
Assert.Contains("win-arm64", ridValues);
}
private sealed class StubAuthenticodeInspector : IDotNetAuthenticodeInspector private sealed class StubAuthenticodeInspector : IDotNetAuthenticodeInspector
{ {

View File

@@ -0,0 +1,84 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v10.0/osx-arm64"
},
"targets": {
".NETCoreApp,Version=v10.0": {
"AppA/2.0.0": {
"dependencies": {
"StellaOps.Toolkit": "1.2.3",
"StellaOps.Logging": "2.5.1"
}
},
"StellaOps.Toolkit/1.2.3": {
"dependencies": {
"StellaOps.Logging": "2.5.1"
},
"runtime": {
"lib/net10.0/StellaOps.Toolkit.dll": {
"assemblyVersion": "1.2.3.0",
"fileVersion": "1.2.3.0"
}
}
},
"StellaOps.Logging/2.5.1": {
"runtime": {
"lib/net10.0/StellaOps.Logging.dll": {
"assemblyVersion": "2.5.1.0",
"fileVersion": "2.5.1.12345"
}
}
}
},
".NETCoreApp,Version=v10.0/linux-x64": {
"StellaOps.Toolkit/1.2.3": {
"runtimeTargets": {
"runtimes/linux-x64/native/libstellaops.toolkit.so": {
"rid": "linux-x64",
"assetType": "native"
}
}
},
"StellaOps.Logging/2.5.1": {
"runtime": {
"runtimes/linux-x64/lib/net10.0/StellaOps.Logging.dll": {}
}
}
},
".NETCoreApp,Version=v10.0/osx-arm64": {
"StellaOps.Toolkit/1.2.3": {
"runtimeTargets": {
"runtimes/osx-arm64/native/libstellaops.toolkit.dylib": {
"rid": "osx-arm64",
"assetType": "native"
}
}
},
"StellaOps.Logging/2.5.1": {
"runtime": {
"runtimes/osx-arm64/lib/net10.0/StellaOps.Logging.dll": {}
}
}
}
},
"libraries": {
"AppA/2.0.0": {
"type": "project",
"serviceable": false
},
"StellaOps.Toolkit/1.2.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-FAKE_TOOLKIT_SHA==",
"path": "stellaops.toolkit/1.2.3",
"hashPath": "stellaops.toolkit.1.2.3.nupkg.sha512"
},
"StellaOps.Logging/2.5.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-FAKE_LOGGING_SHA==",
"path": "stellaops.logging/2.5.1",
"hashPath": "stellaops.logging.2.5.1.nupkg.sha512"
}
}
}

View File

@@ -0,0 +1,39 @@
{
"runtimeOptions": {
"tfm": "net10.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "10.0.1"
},
"frameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "10.0.1"
},
{
"name": "Microsoft.AspNetCore.App",
"version": "10.0.0"
},
{
"name": "StellaOps.Hosting",
"version": "2.0.0"
}
],
"runtimeGraph": {
"runtimes": {
"osx-arm64": {
"fallbacks": [
"osx",
"unix"
]
},
"linux-x64": {
"fallbacks": [
"linux",
"unix"
]
}
}
}
}
}

View File

@@ -0,0 +1,76 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v10.0/win-arm64"
},
"targets": {
".NETCoreApp,Version=v10.0": {
"AppB/3.1.0": {
"dependencies": {
"StellaOps.Toolkit": "1.2.3",
"StellaOps.Logging": "2.5.1"
}
},
"StellaOps.Toolkit/1.2.3": {
"runtime": {
"lib/net10.0/StellaOps.Toolkit.dll": {
"assemblyVersion": "1.2.3.0",
"fileVersion": "1.2.3.0"
}
}
},
"StellaOps.Logging/2.5.1": {
"runtime": {
"lib/net10.0/StellaOps.Logging.dll": {
"assemblyVersion": "2.5.1.0",
"fileVersion": "2.5.1.12345"
}
}
}
},
".NETCoreApp,Version=v10.0/win-arm64": {
"StellaOps.Toolkit/1.2.3": {
"runtimeTargets": {
"runtimes/win-arm64/native/stellaops.toolkit.dll": {
"rid": "win-arm64",
"assetType": "native"
}
}
},
"StellaOps.Logging/2.5.1": {
"runtimeTargets": {
"runtimes/win-arm64/native/stellaops.logging.dll": {
"rid": "win-arm64",
"assetType": "native"
}
}
}
},
".NETCoreApp,Version=v10.0/linux-arm64": {
"StellaOps.Logging/2.5.1": {
"runtime": {
"runtimes/linux-arm64/lib/net10.0/StellaOps.Logging.dll": {}
}
}
}
},
"libraries": {
"AppB/3.1.0": {
"type": "project",
"serviceable": false
},
"StellaOps.Toolkit/1.2.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-FAKE_TOOLKIT_SHA==",
"path": "stellaops.toolkit/1.2.3",
"hashPath": "stellaops.toolkit.1.2.3.nupkg.sha512"
},
"StellaOps.Logging/2.5.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-FAKE_LOGGING_SHA==",
"path": "stellaops.logging/2.5.1",
"hashPath": "stellaops.logging.2.5.1.nupkg.sha512"
}
}
}

View File

@@ -0,0 +1,38 @@
{
"runtimeOptions": {
"tfm": "net10.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "10.0.0"
},
"frameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "10.0.0"
},
{
"name": "Microsoft.WindowsDesktop.App",
"version": "10.0.0"
}
],
"additionalProbingPaths": [
"C:/Users/runner/.nuget/packages"
],
"runtimeGraph": {
"runtimes": {
"win-arm64": {
"fallbacks": [
"win",
"any"
]
},
"linux-arm64": {
"fallbacks": [
"linux",
"unix"
]
}
}
}
}
}

View File

@@ -0,0 +1,120 @@
[
{
"analyzerId": "dotnet",
"componentKey": "purl::pkg:nuget/stellaops.logging@2.5.1",
"purl": "pkg:nuget/stellaops.logging@2.5.1",
"name": "StellaOps.Logging",
"version": "2.5.1",
"type": "nuget",
"usedByEntrypoint": false,
"metadata": {
"assembly[0].assetPath": "lib/net10.0/StellaOps.Logging.dll",
"assembly[0].fileVersion": "2.5.1.12345",
"assembly[0].tfm[0]": ".NETCoreApp,Version=v10.0",
"assembly[0].version": "2.5.1.0",
"assembly[1].assetPath": "runtimes/linux-arm64/lib/net10.0/StellaOps.Logging.dll",
"assembly[1].rid[0]": "linux-arm64",
"assembly[1].tfm[0]": ".NETCoreApp,Version=v10.0",
"assembly[2].assetPath": "runtimes/linux-x64/lib/net10.0/StellaOps.Logging.dll",
"assembly[2].rid[0]": "linux-x64",
"assembly[2].tfm[0]": ".NETCoreApp,Version=v10.0",
"assembly[3].assetPath": "runtimes/osx-arm64/lib/net10.0/StellaOps.Logging.dll",
"assembly[3].rid[0]": "osx-arm64",
"assembly[3].tfm[0]": ".NETCoreApp,Version=v10.0",
"deps.path[0]": "AppA.deps.json",
"deps.path[1]": "AppB.deps.json",
"deps.rid[0]": "linux-arm64",
"deps.rid[1]": "linux-x64",
"deps.rid[2]": "osx-arm64",
"deps.rid[3]": "win-arm64",
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
"license.expression[0]": "Apache-2.0",
"native[0].assetPath": "runtimes/win-arm64/native/stellaops.logging.dll",
"native[0].rid[0]": "win-arm64",
"native[0].tfm[0]": ".NETCoreApp,Version=v10.0",
"package.hashPath[0]": "stellaops.logging.2.5.1.nupkg.sha512",
"package.id": "StellaOps.Logging",
"package.id.normalized": "stellaops.logging",
"package.path[0]": "stellaops.logging/2.5.1",
"package.serviceable": "true",
"package.sha512[0]": "sha512-FAKE_LOGGING_SHA==",
"package.version": "2.5.1",
"provenance": "manifest"
},
"evidence": [
{
"kind": "file",
"source": "deps.json",
"locator": "AppA.deps.json",
"value": "StellaOps.Logging/2.5.1"
},
{
"kind": "file",
"source": "deps.json",
"locator": "AppB.deps.json",
"value": "StellaOps.Logging/2.5.1"
}
]
},
{
"analyzerId": "dotnet",
"componentKey": "purl::pkg:nuget/stellaops.toolkit@1.2.3",
"purl": "pkg:nuget/stellaops.toolkit@1.2.3",
"name": "StellaOps.Toolkit",
"version": "1.2.3",
"type": "nuget",
"usedByEntrypoint": false,
"metadata": {
"assembly[0].assetPath": "lib/net10.0/StellaOps.Toolkit.dll",
"assembly[0].fileVersion": "1.2.3.0",
"assembly[0].tfm[0]": ".NETCoreApp,Version=v10.0",
"assembly[0].version": "1.2.3.0",
"deps.dependency[0]": "stellaops.logging",
"deps.path[0]": "AppA.deps.json",
"deps.path[1]": "AppB.deps.json",
"deps.rid[0]": "linux-x64",
"deps.rid[1]": "osx-arm64",
"deps.rid[2]": "win-arm64",
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
"license.file.sha256[0]": "604e182900b0ecb1ffb911c817bcbd148a31b8f55ad392a3b770be8005048c5c",
"license.file[0]": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
"native[0].assetPath": "runtimes/linux-x64/native/libstellaops.toolkit.so",
"native[0].rid[0]": "linux-x64",
"native[0].tfm[0]": ".NETCoreApp,Version=v10.0",
"native[1].assetPath": "runtimes/osx-arm64/native/libstellaops.toolkit.dylib",
"native[1].rid[0]": "osx-arm64",
"native[1].tfm[0]": ".NETCoreApp,Version=v10.0",
"native[2].assetPath": "runtimes/win-arm64/native/stellaops.toolkit.dll",
"native[2].rid[0]": "win-arm64",
"native[2].tfm[0]": ".NETCoreApp,Version=v10.0",
"package.hashPath[0]": "stellaops.toolkit.1.2.3.nupkg.sha512",
"package.id": "StellaOps.Toolkit",
"package.id.normalized": "stellaops.toolkit",
"package.path[0]": "stellaops.toolkit/1.2.3",
"package.serviceable": "true",
"package.sha512[0]": "sha512-FAKE_TOOLKIT_SHA==",
"package.version": "1.2.3",
"provenance": "manifest"
},
"evidence": [
{
"kind": "file",
"source": "deps.json",
"locator": "AppA.deps.json",
"value": "StellaOps.Toolkit/1.2.3"
},
{
"kind": "file",
"source": "deps.json",
"locator": "AppB.deps.json",
"value": "StellaOps.Toolkit/1.2.3"
},
{
"kind": "file",
"source": "license",
"locator": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
"sha256": "604e182900b0ecb1ffb911c817bcbd148a31b8f55ad392a3b770be8005048c5c"
}
]
}
]

View File

@@ -0,0 +1,15 @@
StellaOps Logging
Copyright (c) 2025 StellaOps.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>StellaOps.Logging</id>
<version>2.5.1</version>
<authors>StellaOps</authors>
<description>Logging sample package for analyzer fixtures.</description>
<license type="expression">Apache-2.0</license>
<licenseUrl>https://stella-ops.example/licenses/logging</licenseUrl>
<projectUrl>https://stella-ops.example/projects/logging</projectUrl>
</metadata>
</package>

View File

@@ -0,0 +1,7 @@
StellaOps Toolkit License
=========================
This sample license is provided for test fixtures only.
Permission is granted to use, copy, modify, and distribute this fixture
for the purpose of automated testing.

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>StellaOps.Toolkit</id>
<version>1.2.3</version>
<authors>StellaOps</authors>
<description>Toolkit sample package for analyzer fixtures.</description>
<license type="file">LICENSE.txt</license>
<licenseUrl>https://stella-ops.example/licenses/toolkit</licenseUrl>
</metadata>
</package>

View File

@@ -68,6 +68,7 @@ All sprints below assume prerequisites from SP10-G2 (core scaffolding + Java ana
- Tests verifying dual runtimeconfig merge logic. - Tests verifying dual runtimeconfig merge logic.
- Guidance for Policy on license propagation from NuGet metadata. - Guidance for Policy on license propagation from NuGet metadata.
- **Progress (2025-10-22):** Completed task 10-305A with a deterministic deps/runtimeconfig ingest pipeline producing `pkg:nuget` components across RID targets. Added dotnet fixture + golden output to the shared harness, wired analyzer plugin availability, and surfaced RID metadata in component records for downstream emit/diff work. License provenance and quiet flagging now ride through the shared helpers (task 10-307D), including nuspec license expression/file ingestion, manifest provenance tagging, and concurrency-safe file metadata caching with new parallel tests. - **Progress (2025-10-22):** Completed task 10-305A with a deterministic deps/runtimeconfig ingest pipeline producing `pkg:nuget` components across RID targets. Added dotnet fixture + golden output to the shared harness, wired analyzer plugin availability, and surfaced RID metadata in component records for downstream emit/diff work. License provenance and quiet flagging now ride through the shared helpers (task 10-307D), including nuspec license expression/file ingestion, manifest provenance tagging, and concurrency-safe file metadata caching with new parallel tests.
- **Progress (2025-10-23):** Landed determinism + benchmark coverage (task 10-308D) via the new `multi` fixture, golden outputs, and bench scenario wired into `baseline.csv`, plus Syft comparison data. Packaged the .NET plug-in for restart-only distribution (task 10-309D), verified manifest copy into `plugins/scanner/analyzers/lang/`, and refreshed `docs/24_OFFLINE_KIT.md` with updated Offline Kit instructions.
## Sprint LA5 — Rust Analyzer & Binary Fingerprinting (Tasks 10-306, 10-307, 10-308, 10-309 subset) ## Sprint LA5 — Rust Analyzer & Binary Fingerprinting (Tasks 10-306, 10-307, 10-308, 10-309 subset)
- **Scope:** Detect crates via metadata in `.fingerprint`, Cargo.lock fragments, or embedded `rustc` markers; robust fallback to binary hash classification. - **Scope:** Detect crates via metadata in `.fingerprint`, Cargo.lock fragments, or embedded `rustc` markers; robust fallback to binary hash classification.

View File

@@ -5,7 +5,7 @@
| SCANNER-ANALYZERS-LANG-10-301 | DONE (2025-10-19) | Language Analyzer Guild | SCANNER-CORE-09-501, SCANNER-WORKER-09-203 | Java analyzer emitting deterministic `pkg:maven` components using pom.properties / MANIFEST evidence. | Java analyzer extracts coordinates+version+licenses with provenance; golden fixtures deterministic; microbenchmark meets target. | | SCANNER-ANALYZERS-LANG-10-301 | DONE (2025-10-19) | Language Analyzer Guild | SCANNER-CORE-09-501, SCANNER-WORKER-09-203 | Java analyzer emitting deterministic `pkg:maven` components using pom.properties / MANIFEST evidence. | Java analyzer extracts coordinates+version+licenses with provenance; golden fixtures deterministic; microbenchmark meets target. |
| SCANNER-ANALYZERS-LANG-10-302 | DONE (2025-10-21) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Node analyzer resolving workspaces/symlinks into `pkg:npm` identities. | Node analyzer handles symlinks/workspaces; outputs sorted components; determinism harness covers hoisted deps. | | SCANNER-ANALYZERS-LANG-10-302 | DONE (2025-10-21) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Node analyzer resolving workspaces/symlinks into `pkg:npm` identities. | Node analyzer handles symlinks/workspaces; outputs sorted components; determinism harness covers hoisted deps. |
| SCANNER-ANALYZERS-LANG-10-303 | DONE (2025-10-21) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Python analyzer consuming `*.dist-info` metadata and RECORD hashes. | Analyzer binds METADATA + RECORD evidence, includes entry points, determinism fixtures stable. | | SCANNER-ANALYZERS-LANG-10-303 | DONE (2025-10-21) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Python analyzer consuming `*.dist-info` metadata and RECORD hashes. | Analyzer binds METADATA + RECORD evidence, includes entry points, determinism fixtures stable. |
| SCANNER-ANALYZERS-LANG-10-304 | DOING (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Go analyzer leveraging buildinfo for `pkg:golang` components. | Buildinfo parser emits module path/version + vcs metadata; binaries without buildinfo downgraded gracefully. | | SCANNER-ANALYZERS-LANG-10-304 | DONE (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Go analyzer leveraging buildinfo for `pkg:golang` components. | Buildinfo parser emits module path/version + vcs metadata; binaries without buildinfo downgraded gracefully. |
| SCANNER-ANALYZERS-LANG-10-305 | DONE (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | .NET analyzer parsing `*.deps.json`, assembly metadata, and RID variants. | Analyzer merges deps.json + assembly info; dedupes per RID; determinism verified. | | SCANNER-ANALYZERS-LANG-10-305 | DONE (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | .NET analyzer parsing `*.deps.json`, assembly metadata, and RID variants. | Analyzer merges deps.json + assembly info; dedupes per RID; determinism verified. |
| SCANNER-ANALYZERS-LANG-10-306 | DONE (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Rust analyzer detecting crate provenance or falling back to `bin:{sha256}`. | Analyzer emits `pkg:cargo` when metadata present; falls back to binary hash; fixtures cover both paths. | | SCANNER-ANALYZERS-LANG-10-306 | DONE (2025-10-22) | Language Analyzer Guild | SCANNER-ANALYZERS-LANG-10-307 | Rust analyzer detecting crate provenance or falling back to `bin:{sha256}`. | Analyzer emits `pkg:cargo` when metadata present; falls back to binary hash; fixtures cover both paths. |
| SCANNER-ANALYZERS-LANG-10-307 | DONE (2025-10-19) | Language Analyzer Guild | SCANNER-CORE-09-501 | Shared language evidence helpers + usage flag propagation. | Shared abstractions implemented; analyzers reuse helpers; evidence includes usage hints; unit tests cover canonical ordering. | | SCANNER-ANALYZERS-LANG-10-307 | DONE (2025-10-19) | Language Analyzer Guild | SCANNER-CORE-09-501 | Shared language evidence helpers + usage flag propagation. | Shared abstractions implemented; analyzers reuse helpers; evidence includes usage hints; unit tests cover canonical ordering. |

View File

@@ -21,17 +21,46 @@ public sealed class CycloneDxComposerTests
Assert.NotNull(result.Inventory); Assert.NotNull(result.Inventory);
Assert.StartsWith("urn:uuid:", result.Inventory.SerialNumber, StringComparison.Ordinal); Assert.StartsWith("urn:uuid:", result.Inventory.SerialNumber, StringComparison.Ordinal);
Assert.Equal("application/vnd.cyclonedx+json; version=1.5", result.Inventory.JsonMediaType); Assert.Equal("application/vnd.cyclonedx+json; version=1.6", result.Inventory.JsonMediaType);
Assert.Equal("application/vnd.cyclonedx+protobuf; version=1.5", result.Inventory.ProtobufMediaType); Assert.Equal("application/vnd.cyclonedx+protobuf; version=1.6", result.Inventory.ProtobufMediaType);
Assert.Equal(2, result.Inventory.Components.Length); Assert.Equal(2, result.Inventory.Components.Length);
Assert.NotNull(result.Usage); Assert.NotNull(result.Usage);
Assert.Equal("application/vnd.cyclonedx+json; version=1.5; view=usage", result.Usage!.JsonMediaType); Assert.Equal("application/vnd.cyclonedx+json; version=1.6; view=usage", result.Usage!.JsonMediaType);
Assert.Single(result.Usage.Components); Assert.Single(result.Usage.Components);
Assert.Equal("pkg:npm/a", result.Usage.Components[0].Identity.Key); Assert.Equal("pkg:npm/a", result.Usage.Components[0].Identity.Key);
ValidateJson(result.Inventory.JsonBytes, expectedComponentCount: 2, expectedView: "inventory"); ValidateJson(result.Inventory.JsonBytes, expectedComponentCount: 2, expectedView: "inventory");
ValidateJson(result.Usage.JsonBytes, expectedComponentCount: 1, expectedView: "usage"); ValidateJson(result.Usage.JsonBytes, expectedComponentCount: 1, expectedView: "usage");
using var inventoryDoc = JsonDocument.Parse(result.Inventory.JsonBytes);
var inventoryRoot = inventoryDoc.RootElement;
Assert.True(inventoryRoot.TryGetProperty("vulnerabilities", out var inventoryVulnerabilities));
var inventoryVulns = inventoryVulnerabilities.EnumerateArray().ToArray();
Assert.Equal(2, inventoryVulns.Length);
var primaryVuln = inventoryVulns.Single(v => string.Equals(v.GetProperty("bom-ref").GetString(), "finding-a", StringComparison.Ordinal));
var primaryProperties = primaryVuln.GetProperty("properties")
.EnumerateArray()
.ToDictionary(
element => element.GetProperty("name").GetString()!,
element => element.GetProperty("value").GetString()!,
StringComparer.Ordinal);
Assert.Equal("Blocked", primaryProperties["stellaops:policy.status"]);
Assert.Equal("true", primaryProperties["stellaops:policy.quiet"]);
Assert.Equal("40.5", primaryProperties["stellaops:policy.score"]);
Assert.Equal("medium", primaryProperties["stellaops:policy.confidenceBand"]);
Assert.Equal("runtime", primaryProperties["stellaops:policy.reachability"]);
Assert.Equal("0.45", primaryProperties["stellaops:policy.input.reachabilityWeight"]);
var ratingScore = primaryVuln.GetProperty("ratings").EnumerateArray().Single().GetProperty("score").GetDouble();
Assert.Equal(40.5, ratingScore);
using var usageDoc = JsonDocument.Parse(result.Usage.JsonBytes);
var usageRoot = usageDoc.RootElement;
Assert.True(usageRoot.TryGetProperty("vulnerabilities", out var usageVulnerabilities));
var usageVulns = usageVulnerabilities.EnumerateArray().ToArray();
Assert.Single(usageVulns);
Assert.Equal("finding-a", usageVulns[0].GetProperty("bom-ref").GetString());
} }
[Fact] [Fact]
@@ -109,16 +138,54 @@ public sealed class CycloneDxComposerTests
Architecture = "amd64", Architecture = "amd64",
}; };
return SbomCompositionRequest.Create( return SbomCompositionRequest.Create(
image, image,
fragments, fragments,
new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero), new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero),
generatorName: "StellaOps.Scanner", generatorName: "StellaOps.Scanner",
generatorVersion: "0.10.0", generatorVersion: "0.10.0",
properties: new Dictionary<string, string> properties: new Dictionary<string, string>
{ {
["stellaops:scanId"] = "scan-1234", ["stellaops:scanId"] = "scan-1234",
}); },
policyFindings: new[]
{
new SbomPolicyFinding
{
FindingId = "finding-a",
ComponentKey = "pkg:npm/a",
VulnerabilityId = "CVE-2025-0001",
Status = "Blocked",
Score = 40.5,
ConfigVersion = "1.0",
Quiet = true,
QuietedBy = "policy/quiet-critical-runtime",
UnknownConfidence = 0.42,
ConfidenceBand = "medium",
UnknownAgeDays = 5,
SourceTrust = "NVD",
Reachability = "runtime",
Inputs = ImmutableArray.Create(
new KeyValuePair<string, double>("severityWeight", 90),
new KeyValuePair<string, double>("trustWeight", 1.0),
new KeyValuePair<string, double>("reachabilityWeight", 0.45))
},
new SbomPolicyFinding
{
FindingId = "finding-b",
ComponentKey = "pkg:npm/b",
VulnerabilityId = "CVE-2025-0002",
Status = "Warned",
Score = 12.5,
ConfigVersion = "1.0",
Quiet = false,
SourceTrust = "StellaOps",
Reachability = "indirect",
Inputs = ImmutableArray.Create(
new KeyValuePair<string, double>("severityWeight", 55),
new KeyValuePair<string, double>("trustWeight", 0.85))
}
});
} }
private static void ValidateJson(byte[] data, int expectedComponentCount, string expectedView) private static void ValidateJson(byte[] data, int expectedComponentCount, string expectedView)
@@ -128,19 +195,20 @@ public sealed class CycloneDxComposerTests
Assert.True(root.TryGetProperty("metadata", out var metadata), "metadata property missing"); Assert.True(root.TryGetProperty("metadata", out var metadata), "metadata property missing");
var properties = metadata.GetProperty("properties"); var properties = metadata.GetProperty("properties");
var viewProperty = properties.EnumerateArray() var viewProperty = properties.EnumerateArray()
.Single(prop => prop.GetProperty("name").GetString() == "stellaops:sbom.view"); .Single(prop => string.Equals(prop.GetProperty("name").GetString(), "stellaops:sbom.view", StringComparison.Ordinal));
Assert.Equal(expectedView, viewProperty.GetProperty("value").GetString()); Assert.Equal(expectedView, viewProperty.GetProperty("value").GetString());
var components = root.GetProperty("components").EnumerateArray().ToArray(); var components = root.GetProperty("components").EnumerateArray().ToArray();
Assert.Equal(expectedComponentCount, components.Length); Assert.Equal(expectedComponentCount, components.Length);
var names = components.Select(component => component.GetProperty("name").GetString()).ToArray(); var names = components.Select(component => component.GetProperty("name").GetString()!).ToArray();
Assert.Equal(names, names.OrderBy(n => n, StringComparer.Ordinal).ToArray()); Assert.Equal(names, names.OrderBy(n => n, StringComparer.Ordinal).ToArray());
var firstComponentProperties = components[0].GetProperty("properties").EnumerateArray().ToDictionary( var firstComponentProperties = components[0].GetProperty("properties").EnumerateArray().ToDictionary(
element => element.GetProperty("name").GetString(), element => element.GetProperty("name").GetString()!,
element => element.GetProperty("value").GetString()); element => element.GetProperty("value").GetString()!,
StringComparer.Ordinal);
Assert.Equal("apk", firstComponentProperties["stellaops.os.analyzer"]); Assert.Equal("apk", firstComponentProperties["stellaops.os.analyzer"]);
Assert.Equal("x86_64", firstComponentProperties["stellaops.os.architecture"]); Assert.Equal("x86_64", firstComponentProperties["stellaops.os.architecture"]);

View File

@@ -76,7 +76,7 @@ public sealed class ScannerArtifactPackageBuilderTests
Assert.Equal(5, root.GetProperty("artifacts").GetArrayLength()); Assert.Equal(5, root.GetProperty("artifacts").GetArrayLength());
var usageEntry = root.GetProperty("artifacts").EnumerateArray().First(element => element.GetProperty("kind").GetString() == "sbom-usage"); var usageEntry = root.GetProperty("artifacts").EnumerateArray().First(element => element.GetProperty("kind").GetString() == "sbom-usage");
Assert.Equal("application/vnd.cyclonedx+json; version=1.5; view=usage", usageEntry.GetProperty("mediaType").GetString()); Assert.Equal("application/vnd.cyclonedx+json; version=1.6; view=usage", usageEntry.GetProperty("mediaType").GetString());
} }
private static ComponentRecord CreateComponent(string key, string version, string layerDigest, ComponentUsage? usage = null, IReadOnlyDictionary<string, string>? metadata = null) private static ComponentRecord CreateComponent(string key, string version, string layerDigest, ComponentUsage? usage = null, IReadOnlyDictionary<string, string>? metadata = null)

View File

@@ -6,7 +6,8 @@ using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using CycloneDX; using CycloneDX;
using CycloneDX.Models; using CycloneDX.Models;
using CycloneDX.Models.Vulnerabilities;
using JsonSerializer = CycloneDX.Json.Serializer; using JsonSerializer = CycloneDX.Json.Serializer;
using ProtoSerializer = CycloneDX.Protobuf.Serializer; using ProtoSerializer = CycloneDX.Protobuf.Serializer;
using StellaOps.Scanner.Core.Contracts; using StellaOps.Scanner.Core.Contracts;
@@ -18,10 +19,10 @@ public sealed class CycloneDxComposer
{ {
private static readonly Guid SerialNamespace = new("0d3a422b-6e1b-4d9b-9c35-654b706c97e8"); private static readonly Guid SerialNamespace = new("0d3a422b-6e1b-4d9b-9c35-654b706c97e8");
private const string InventoryMediaTypeJson = "application/vnd.cyclonedx+json; version=1.5"; private const string InventoryMediaTypeJson = "application/vnd.cyclonedx+json; version=1.6";
private const string UsageMediaTypeJson = "application/vnd.cyclonedx+json; version=1.5; view=usage"; private const string UsageMediaTypeJson = "application/vnd.cyclonedx+json; version=1.6; view=usage";
private const string InventoryMediaTypeProtobuf = "application/vnd.cyclonedx+protobuf; version=1.5"; private const string InventoryMediaTypeProtobuf = "application/vnd.cyclonedx+protobuf; version=1.6";
private const string UsageMediaTypeProtobuf = "application/vnd.cyclonedx+protobuf; version=1.5; view=usage"; private const string UsageMediaTypeProtobuf = "application/vnd.cyclonedx+protobuf; version=1.6; view=usage";
public SbomCompositionResult Compose(SbomCompositionRequest request) public SbomCompositionResult Compose(SbomCompositionRequest request)
{ {
@@ -77,7 +78,7 @@ public sealed class CycloneDxComposer
string jsonMediaType, string jsonMediaType,
string protobufMediaType) string protobufMediaType)
{ {
var bom = BuildBom(request, view, components, generatedAt); var bom = BuildBom(request, graph, view, components, generatedAt);
var json = JsonSerializer.Serialize(bom); var json = JsonSerializer.Serialize(bom);
var jsonBytes = Encoding.UTF8.GetBytes(json); var jsonBytes = Encoding.UTF8.GetBytes(json);
var protobufBytes = ProtoSerializer.Serialize(bom); var protobufBytes = ProtoSerializer.Serialize(bom);
@@ -100,25 +101,32 @@ public sealed class CycloneDxComposer
}; };
} }
private Bom BuildBom( private Bom BuildBom(
SbomCompositionRequest request, SbomCompositionRequest request,
SbomView view, ComponentGraph graph,
ImmutableArray<AggregatedComponent> components, SbomView view,
DateTimeOffset generatedAt) ImmutableArray<AggregatedComponent> components,
{ DateTimeOffset generatedAt)
var bom = new Bom {
{ var bom = new Bom
SpecVersion = SpecificationVersion.v1_4, {
Version = 1, SpecVersion = SpecificationVersion.v1_6,
Metadata = BuildMetadata(request, view, generatedAt), Version = 1,
Components = BuildComponents(components), Metadata = BuildMetadata(request, view, generatedAt),
Dependencies = BuildDependencies(components), Components = BuildComponents(components),
}; Dependencies = BuildDependencies(components),
};
var serialPayload = $"{request.Image.ImageDigest}|{view}|{ScannerTimestamps.ToIso8601(generatedAt)}";
bom.SerialNumber = $"urn:uuid:{ScannerIdentifiers.CreateDeterministicGuid(SerialNamespace, Encoding.UTF8.GetBytes(serialPayload)).ToString("d", CultureInfo.InvariantCulture)}"; var vulnerabilities = BuildVulnerabilities(request, graph, components);
if (vulnerabilities is not null)
return bom; {
bom.Vulnerabilities = vulnerabilities;
}
var serialPayload = $"{request.Image.ImageDigest}|{view}|{ScannerTimestamps.ToIso8601(generatedAt)}";
bom.SerialNumber = $"urn:uuid:{ScannerIdentifiers.CreateDeterministicGuid(SerialNamespace, Encoding.UTF8.GetBytes(serialPayload)).ToString("d", CultureInfo.InvariantCulture)}";
return bom;
} }
private static Metadata BuildMetadata(SbomCompositionRequest request, SbomView view, DateTimeOffset generatedAt) private static Metadata BuildMetadata(SbomCompositionRequest request, SbomView view, DateTimeOffset generatedAt)
@@ -129,23 +137,11 @@ public sealed class CycloneDxComposer
Component = BuildMetadataComponent(request.Image), Component = BuildMetadataComponent(request.Image),
}; };
if (!string.IsNullOrWhiteSpace(request.GeneratorName)) if (request.AdditionalProperties is not null && request.AdditionalProperties.Count > 0)
{ {
metadata.Tools = new List<Tool> metadata.Properties = request.AdditionalProperties
{ .Where(static pair => !string.IsNullOrWhiteSpace(pair.Key) && pair.Value is not null)
new() .OrderBy(static pair => pair.Key, StringComparer.Ordinal)
{
Name = request.GeneratorName,
Version = request.GeneratorVersion,
}
};
}
if (request.AdditionalProperties is not null && request.AdditionalProperties.Count > 0)
{
metadata.Properties = request.AdditionalProperties
.Where(static pair => !string.IsNullOrWhiteSpace(pair.Key) && pair.Value is not null)
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
.Select(pair => new Property .Select(pair => new Property
{ {
Name = pair.Key, Name = pair.Key,
@@ -154,15 +150,33 @@ public sealed class CycloneDxComposer
.ToList(); .ToList();
} }
if (metadata.Properties is null) if (metadata.Properties is null)
{ {
metadata.Properties = new List<Property>(); metadata.Properties = new List<Property>();
} }
metadata.Properties.Add(new Property if (!string.IsNullOrWhiteSpace(request.GeneratorName))
{ {
Name = "stellaops:sbom.view", metadata.Properties.Add(new Property
Value = view.ToString().ToLowerInvariant(), {
Name = "stellaops:generator.name",
Value = request.GeneratorName,
});
if (!string.IsNullOrWhiteSpace(request.GeneratorVersion))
{
metadata.Properties.Add(new Property
{
Name = "stellaops:generator.version",
Value = request.GeneratorVersion,
});
}
}
metadata.Properties.Add(new Property
{
Name = "stellaops:sbom.view",
Value = view.ToString().ToLowerInvariant(),
}); });
return metadata; return metadata;
@@ -357,8 +371,171 @@ public sealed class CycloneDxComposer
}); });
} }
return dependencies.Count == 0 ? null : dependencies; return dependencies.Count == 0 ? null : dependencies;
} }
private static List<Vulnerability>? BuildVulnerabilities(
SbomCompositionRequest request,
ComponentGraph graph,
ImmutableArray<AggregatedComponent> viewComponents)
{
if (request.PolicyFindings.IsDefaultOrEmpty || request.PolicyFindings.Length == 0)
{
return null;
}
if (viewComponents.IsDefaultOrEmpty || viewComponents.Length == 0)
{
return null;
}
var componentKeys = viewComponents
.Select(static component => component.Identity.Key)
.ToImmutableHashSet(StringComparer.Ordinal);
if (componentKeys.Count == 0)
{
return null;
}
var vulnerabilities = new List<Vulnerability>(request.PolicyFindings.Length);
foreach (var finding in request.PolicyFindings)
{
if (!graph.ComponentMap.TryGetValue(finding.ComponentKey, out var component))
{
continue;
}
if (!componentKeys.Contains(component.Identity.Key))
{
continue;
}
var ratings = BuildRatings(finding.Score);
var properties = BuildVulnerabilityProperties(finding);
var vulnerability = new Vulnerability
{
BomRef = finding.FindingId,
Id = finding.VulnerabilityId ?? finding.FindingId,
Source = new Source { Name = "StellaOps.Policy" },
Affects = new List<Affects>
{
new() { Ref = component.Identity.Key }
},
Ratings = ratings,
Properties = properties,
};
vulnerabilities.Add(vulnerability);
}
return vulnerabilities.Count == 0 ? null : vulnerabilities;
}
private static List<Rating>? BuildRatings(double score)
{
if (double.IsNaN(score) || double.IsInfinity(score))
{
return null;
}
return new List<Rating>
{
new()
{
Method = ScoreMethod.Other,
Justification = "StellaOps Policy score",
Score = score,
Severity = Severity.Unknown,
Source = new Source { Name = "StellaOps.Policy" },
}
};
}
private static List<Property>? BuildVulnerabilityProperties(SbomPolicyFinding finding)
{
var properties = new List<Property>();
AddStringProperty(properties, "stellaops:policy.status", finding.Status);
AddStringProperty(properties, "stellaops:policy.configVersion", finding.ConfigVersion);
AddBooleanProperty(properties, "stellaops:policy.quiet", finding.Quiet);
AddStringProperty(properties, "stellaops:policy.quietedBy", finding.QuietedBy);
AddStringProperty(properties, "stellaops:policy.confidenceBand", finding.ConfidenceBand);
AddStringProperty(properties, "stellaops:policy.sourceTrust", finding.SourceTrust);
AddStringProperty(properties, "stellaops:policy.reachability", finding.Reachability);
AddDoubleProperty(properties, "stellaops:policy.score", finding.Score);
AddNullableDoubleProperty(properties, "stellaops:policy.unknownConfidence", finding.UnknownConfidence);
AddNullableDoubleProperty(properties, "stellaops:policy.unknownAgeDays", finding.UnknownAgeDays);
if (!finding.Inputs.IsDefaultOrEmpty && finding.Inputs.Length > 0)
{
foreach (var (key, value) in finding.Inputs)
{
AddDoubleProperty(properties, $"stellaops:policy.input.{key}", value);
}
}
if (properties.Count == 0)
{
return null;
}
properties.Sort(static (left, right) => StringComparer.Ordinal.Compare(left.Name, right.Name));
return properties;
}
private static void AddStringProperty(ICollection<Property> properties, string name, string? value)
{
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(value))
{
return;
}
properties.Add(new Property
{
Name = name,
Value = value.Trim(),
});
}
private static void AddBooleanProperty(ICollection<Property> properties, string name, bool value)
{
if (string.IsNullOrWhiteSpace(name))
{
return;
}
properties.Add(new Property
{
Name = name,
Value = value ? "true" : "false",
});
}
private static void AddDoubleProperty(ICollection<Property> properties, string name, double value)
{
if (string.IsNullOrWhiteSpace(name) || double.IsNaN(value) || double.IsInfinity(value))
{
return;
}
properties.Add(new Property
{
Name = name,
Value = FormatDouble(value),
});
}
private static void AddNullableDoubleProperty(ICollection<Property> properties, string name, double? value)
{
if (!value.HasValue)
{
return;
}
AddDoubleProperty(properties, name, value.Value);
}
private static Component.Classification MapClassification(string? type) private static Component.Classification MapClassification(string? type)
{ {
@@ -372,7 +549,7 @@ public sealed class CycloneDxComposer
"application" => Component.Classification.Application, "application" => Component.Classification.Application,
"framework" => Component.Classification.Framework, "framework" => Component.Classification.Framework,
"container" => Component.Classification.Container, "container" => Component.Classification.Container,
"operating-system" or "os" => Component.Classification.OperationSystem, "operating-system" or "os" => Component.Classification.Operating_System,
"device" => Component.Classification.Device, "device" => Component.Classification.Device,
"firmware" => Component.Classification.Firmware, "firmware" => Component.Classification.Firmware,
"file" => Component.Classification.File, "file" => Component.Classification.File,
@@ -396,7 +573,10 @@ public sealed class CycloneDxComposer
}; };
} }
private static string ComputeSha256(byte[] bytes) private static string FormatDouble(double value)
=> value.ToString("0.############################", CultureInfo.InvariantCulture);
private static string ComputeSha256(byte[] bytes)
{ {
using var sha256 = SHA256.Create(); using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(bytes); var hash = sha256.ComputeHash(bytes);

View File

@@ -39,21 +39,25 @@ public sealed record SbomCompositionRequest
public string? GeneratorVersion { get; init; } public string? GeneratorVersion { get; init; }
= null; = null;
public IReadOnlyDictionary<string, string>? AdditionalProperties { get; init; } public IReadOnlyDictionary<string, string>? AdditionalProperties { get; init; }
= null; = null;
public static SbomCompositionRequest Create( public ImmutableArray<SbomPolicyFinding> PolicyFindings { get; init; }
ImageArtifactDescriptor image, = ImmutableArray<SbomPolicyFinding>.Empty;
IEnumerable<LayerComponentFragment> fragments,
DateTimeOffset generatedAt, public static SbomCompositionRequest Create(
string? generatorName = null, ImageArtifactDescriptor image,
string? generatorVersion = null, IEnumerable<LayerComponentFragment> fragments,
IReadOnlyDictionary<string, string>? properties = null) DateTimeOffset generatedAt,
{ string? generatorName = null,
ArgumentNullException.ThrowIfNull(image); string? generatorVersion = null,
ArgumentNullException.ThrowIfNull(fragments); IReadOnlyDictionary<string, string>? properties = null,
IEnumerable<SbomPolicyFinding>? policyFindings = null)
var normalizedImage = new ImageArtifactDescriptor {
ArgumentNullException.ThrowIfNull(image);
ArgumentNullException.ThrowIfNull(fragments);
var normalizedImage = new ImageArtifactDescriptor
{ {
ImageDigest = ScannerIdentifiers.NormalizeDigest(image.ImageDigest) ?? throw new ArgumentException("Image digest is required.", nameof(image)), ImageDigest = ScannerIdentifiers.NormalizeDigest(image.ImageDigest) ?? throw new ArgumentException("Image digest is required.", nameof(image)),
ImageReference = Normalize(image.ImageReference), ImageReference = Normalize(image.ImageReference),
@@ -65,21 +69,68 @@ public sealed record SbomCompositionRequest
return new SbomCompositionRequest return new SbomCompositionRequest
{ {
Image = normalizedImage, Image = normalizedImage,
LayerFragments = fragments.ToImmutableArray(), LayerFragments = fragments.ToImmutableArray(),
GeneratedAt = ScannerTimestamps.Normalize(generatedAt), GeneratedAt = ScannerTimestamps.Normalize(generatedAt),
GeneratorName = Normalize(generatorName), GeneratorName = Normalize(generatorName),
GeneratorVersion = Normalize(generatorVersion), GeneratorVersion = Normalize(generatorVersion),
AdditionalProperties = properties, AdditionalProperties = properties,
}; PolicyFindings = NormalizePolicyFindings(policyFindings),
} };
}
private static string? Normalize(string? value)
{ private static string? Normalize(string? value)
{
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))
{ {
return null; return null;
} }
return value.Trim(); return value.Trim();
} }
}
private static ImmutableArray<SbomPolicyFinding> NormalizePolicyFindings(IEnumerable<SbomPolicyFinding>? policyFindings)
{
if (policyFindings is null)
{
return ImmutableArray<SbomPolicyFinding>.Empty;
}
var builder = ImmutableArray.CreateBuilder<SbomPolicyFinding>();
foreach (var finding in policyFindings)
{
if (finding is null)
{
continue;
}
SbomPolicyFinding normalized;
try
{
normalized = finding.Normalize();
}
catch (ArgumentException)
{
continue;
}
if (string.IsNullOrWhiteSpace(normalized.FindingId) || string.IsNullOrWhiteSpace(normalized.ComponentKey))
{
continue;
}
builder.Add(normalized);
}
if (builder.Count == 0)
{
return ImmutableArray<SbomPolicyFinding>.Empty;
}
return builder
.ToImmutable()
.OrderBy(static finding => finding.FindingId, StringComparer.Ordinal)
.ThenBy(static finding => finding.ComponentKey, StringComparer.Ordinal)
.ThenBy(static finding => finding.VulnerabilityId ?? string.Empty, StringComparer.Ordinal)
.ToImmutableArray();
}
}

View File

@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace StellaOps.Scanner.Emit.Composition;
public sealed record SbomPolicyFinding
{
public required string FindingId { get; init; }
public required string ComponentKey { get; init; }
public string? VulnerabilityId { get; init; }
public string Status { get; init; } = string.Empty;
public double Score { get; init; }
public string ConfigVersion { get; init; } = string.Empty;
public ImmutableArray<KeyValuePair<string, double>> Inputs { get; init; } = ImmutableArray<KeyValuePair<string, double>>.Empty;
public string? QuietedBy { get; init; }
public bool Quiet { get; init; }
public double? UnknownConfidence { get; init; }
public string? ConfidenceBand { get; init; }
public double? UnknownAgeDays { get; init; }
public string? SourceTrust { get; init; }
public string? Reachability { get; init; }
internal SbomPolicyFinding Normalize()
{
ArgumentException.ThrowIfNullOrWhiteSpace(FindingId);
ArgumentException.ThrowIfNullOrWhiteSpace(ComponentKey);
var normalizedInputs = Inputs.IsDefaultOrEmpty
? ImmutableArray<KeyValuePair<string, double>>.Empty
: Inputs
.Where(static pair => !string.IsNullOrWhiteSpace(pair.Key))
.Select(static pair => new KeyValuePair<string, double>(pair.Key.Trim(), pair.Value))
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
.ToImmutableArray();
return this with
{
FindingId = FindingId.Trim(),
ComponentKey = ComponentKey.Trim(),
VulnerabilityId = string.IsNullOrWhiteSpace(VulnerabilityId) ? null : VulnerabilityId.Trim(),
Status = string.IsNullOrWhiteSpace(Status) ? string.Empty : Status.Trim(),
ConfigVersion = string.IsNullOrWhiteSpace(ConfigVersion) ? string.Empty : ConfigVersion.Trim(),
QuietedBy = string.IsNullOrWhiteSpace(QuietedBy) ? null : QuietedBy.Trim(),
ConfidenceBand = string.IsNullOrWhiteSpace(ConfidenceBand) ? null : ConfidenceBand.Trim(),
SourceTrust = string.IsNullOrWhiteSpace(SourceTrust) ? null : SourceTrust.Trim(),
Reachability = string.IsNullOrWhiteSpace(Reachability) ? null : Reachability.Trim(),
Inputs = normalizedInputs
};
}
}

View File

@@ -12,7 +12,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CycloneDX.Core" Version="5.1.0" /> <PackageReference Include="CycloneDX.Core" Version="10.0.1" />
<PackageReference Include="RoaringBitmap" Version="0.0.9" /> <PackageReference Include="RoaringBitmap" Version="0.0.9" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -2,11 +2,11 @@
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria | | ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|----|--------|----------|------------|-------------|---------------| |----|--------|----------|------------|-------------|---------------|
| SCANNER-EMIT-10-601 | DOING (2025-10-19) | Emit Guild | SCANNER-CACHE-10-101 | Compose inventory SBOM (CycloneDX JSON/Protobuf) from layer fragments with deterministic ordering. | Inventory SBOM validated against schema; fixtures confirm deterministic output. | | SCANNER-EMIT-10-601 | DONE (2025-10-22) | Emit Guild | SCANNER-CACHE-10-101 | Compose inventory SBOM (CycloneDX JSON/Protobuf) from layer fragments with deterministic ordering. | Inventory SBOM validated against schema; fixtures confirm deterministic output. |
| SCANNER-EMIT-10-602 | DOING (2025-10-19) | Emit Guild | SCANNER-EMIT-10-601 | Compose usage SBOM leveraging EntryTrace to flag actual usage; ensure separate view toggles. | Usage SBOM tests confirm correct subset; API contract documented. | | SCANNER-EMIT-10-602 | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-601 | Compose usage SBOM leveraging EntryTrace to flag actual usage; ensure separate view toggles. | Usage SBOM tests confirm correct subset; API contract documented. |
| SCANNER-EMIT-10-603 | DOING (2025-10-19) | Emit Guild | SCANNER-EMIT-10-601 | Generate BOM index sidecar (purl table + roaring bitmap + usedByEntrypoint flag). | Index format validated; query helpers proven; stored artifacts hashed deterministically. | | SCANNER-EMIT-10-603 | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-601 | Generate BOM index sidecar (purl table + roaring bitmap + usedByEntrypoint flag). | Index format validated; query helpers proven; stored artifacts hashed deterministically. |
| SCANNER-EMIT-10-604 | DOING (2025-10-19) | 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-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 | DOING (2025-10-19) | 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-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 | DOING (2025-10-19) | 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-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 | 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-10-607 | TODO | 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. | | 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

@@ -61,7 +61,8 @@ public static class TelemetryExtensions
metrics metrics
.AddMeter( .AddMeter(
ScannerWorkerInstrumentation.MeterName, ScannerWorkerInstrumentation.MeterName,
"StellaOps.Scanner.Analyzers.Lang.Node") "StellaOps.Scanner.Analyzers.Lang.Node",
"StellaOps.Scanner.Analyzers.Lang.Go")
.AddRuntimeInstrumentation() .AddRuntimeInstrumentation()
.AddProcessInstrumentation(); .AddProcessInstrumentation();