diff --git a/EXECPLAN.md b/EXECPLAN.md
index c044cbb6..d72393e5 100644
--- a/EXECPLAN.md
+++ b/EXECPLAN.md
@@ -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 Notify Connectors Guild: read EXECPLAN.md Wave 5 and SPRINTS.md rows for `src/StellaOps.Notify.Connectors.Email/TASKS.md`, `src/StellaOps.Notify.Connectors.Slack/TASKS.md`, `src/StellaOps.Notify.Connectors.Teams/TASKS.md`, `src/StellaOps.Notify.Connectors.Webhook/TASKS.md`. Focus on NOTIFY-CONN-SLACK-15-502 (DONE), NOTIFY-CONN-TEAMS-15-602 (DONE), NOTIFY-CONN-EMAIL-15-702 (BLOCKED 2025-10-20), NOTIFY-CONN-WEBHOOK-15-802 (BLOCKED 2025-10-20). Confirm prerequisites (internal: NOTIFY-CONN-EMAIL-15-701 (Wave 4), NOTIFY-CONN-SLACK-15-501 (Wave 4), NOTIFY-CONN-TEAMS-15-601 (Wave 4), NOTIFY-CONN-WEBHOOK-15-801 (Wave 4)) before starting and report status in module TASKS.md.
- Team Scanner WebService Guild: read EXECPLAN.md Wave 5 and SPRINTS.md rows for `src/StellaOps.Scanner.WebService/TASKS.md`. Focus on SCANNER-RUNTIME-17-401 (TODO). Confirm prerequisites (internal: POLICY-RUNTIME-17-201 (Wave 4), SCANNER-EMIT-17-701 (Wave 1), SCANNER-RUNTIME-12-301 (Wave 1), ZASTAVA-OBS-17-005 (Wave 3)) before starting and report status in module TASKS.md.
-- Team TBD: read EXECPLAN.md Wave 5 and SPRINTS.md rows for `src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md`, `src/StellaOps.Scanner.Analyzers.Lang.Rust/TASKS.md`. Focus on SCANNER-ANALYZERS-LANG-10-308D (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
- 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
- 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
- Team: TBD
- 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)
- • Current: TODO
+ • Current: DONE — fixtures + benchmarks merged 2025-10-23
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-308G — Determinism fixtures + benchmark harness (Vs competitor).
• 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
- Team: TBD
- 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)
- • Current: TODO
+ • Current: DONE — manifest + Offline Kit docs updated 2025-10-23
- Path: `src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md`
1. [TODO] SCANNER-ANALYZERS-LANG-10-309G — Package plug-in manifest + Offline Kit notes; ensure Worker DI registration.
• Prereqs: SCANNER-ANALYZERS-LANG-10-308G (Wave 5)
diff --git a/SPRINTS.md b/SPRINTS.md
index 24e80a8d..ca4c5b51 100644
--- a/SPRINTS.md
+++ b/SPRINTS.md
@@ -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-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.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-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. |
@@ -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-502 | Attribute diffs to introducing/removing layers including provenance evidence. |
| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Diff/TASKS.md | TODO | Diff Guild | SCANNER-DIFF-10-503 | Produce JSON diff output for inventory vs usage views aligned with API contract. |
-| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-601 | Compose inventory SBOM (CycloneDX JSON/Protobuf) from layer fragments. |
-| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-602 | Compose usage SBOM leveraging EntryTrace to flag actual usage. |
-| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-603 | Generate BOM index sidecar (purl table + roaring bitmap + usage flag). |
-| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-604 | Package artifacts for export + attestation with deterministic manifests. |
-| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-605 | Emit BOM-Index sidecar schema/fixtures (CRITICAL PATH for SP16). |
-| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-606 | Usage view bit flags integrated with EntryTrace. |
-| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | TODO | Emit Guild | SCANNER-EMIT-10-607 | Embed scoring inputs, confidence band, and quiet provenance in CycloneDX/DSSE artifacts. |
+| Sprint 10 | 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 | 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 | 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 | 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 | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-605 | Emit BOM-Index sidecar schema/fixtures (CRITICAL PATH for SP16). |
+| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-606 | Usage view bit flags integrated with EntryTrace. |
+| Sprint 10 | Scanner Analyzers & SBOM | src/StellaOps.Scanner.Emit/TASKS.md | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-607 | Embed scoring inputs, confidence band, and quiet provenance in CycloneDX/DSSE artifacts. |
| Sprint 10 | Samples | samples/TASKS.md | TODO | Samples Guild, Scanner Team | SAMPLES-10-001 | Sample images with SBOM/BOM-Index sidecars. |
| Sprint 10 | DevOps Perf | ops/devops/TASKS.md | TODO | DevOps Guild | DEVOPS-PERF-10-001 | Perf smoke job ensuring <5 s SBOM compose. |
| Sprint 11 | Signing Chain Bring-up | src/StellaOps.Authority/TASKS.md | 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 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 | 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. |
diff --git a/bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/ScenarioRunners.cs b/bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/ScenarioRunners.cs
index 854c4668..2f3c72a7 100644
--- a/bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/ScenarioRunners.cs
+++ b/bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/ScenarioRunners.cs
@@ -4,8 +4,10 @@ using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using StellaOps.Scanner.Analyzers.Lang;
+using StellaOps.Scanner.Analyzers.Lang.Go;
using StellaOps.Scanner.Analyzers.Lang.Java;
using StellaOps.Scanner.Analyzers.Lang.Node;
+using StellaOps.Scanner.Analyzers.Lang.DotNet;
namespace StellaOps.Bench.ScannerAnalyzers.Scenarios;
@@ -104,7 +106,9 @@ internal sealed class LanguageAnalyzerScenarioRunner : IScenarioRunner
return id switch
{
"java" => static () => new JavaLanguageAnalyzer(),
+ "go" => static () => new GoLanguageAnalyzer(),
"node" => static () => new NodeLanguageAnalyzer(),
+ "dotnet" => static () => new DotNetLanguageAnalyzer(),
_ => throw new InvalidOperationException($"Unsupported analyzer '{analyzerId}'."),
};
}
diff --git a/bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj b/bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj
index e7467b36..04f0e626 100644
--- a/bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj
+++ b/bench/Scanner.Analyzers/StellaOps.Bench.ScannerAnalyzers/StellaOps.Bench.ScannerAnalyzers.csproj
@@ -10,7 +10,9 @@
+
+
diff --git a/bench/Scanner.Analyzers/baseline.csv b/bench/Scanner.Analyzers/baseline.csv
index 10fbdcbd..fac7745d 100644
--- a/bench/Scanner.Analyzers/baseline.csv
+++ b/bench/Scanner.Analyzers/baseline.csv
@@ -1,4 +1,6 @@
scenario,iterations,sample_count,mean_ms,p95_ms,max_ms
-node_monorepo_walk,5,4,4.2314,15.3277,18.9984
-java_demo_archive,5,1,4.5572,17.3489,21.5472
-python_site_packages_walk,5,3,2.0049,6.4230,7.8832
+node_monorepo_walk,5,4,9.4303,36.1354,45.0012
+java_demo_archive,5,1,20.6964,81.5592,101.7846
+go_buildinfo_fixture,5,2,35.0345,136.5466,170.1612
+dotnet_multirid_fixture,5,2,29.1862,106.6249,132.3018
+python_site_packages_walk,5,3,12.0024,45.0165,56.0003
diff --git a/bench/Scanner.Analyzers/config.json b/bench/Scanner.Analyzers/config.json
index 0c9383bd..45c1d3d2 100644
--- a/bench/Scanner.Analyzers/config.json
+++ b/bench/Scanner.Analyzers/config.json
@@ -18,11 +18,27 @@
"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",
"label": "Python site-packages dist-info crawl",
"root": "samples/runtime/python-venv/lib/python3.11/site-packages",
- "matcher": "**/*.dist-info/METADATA",
+ "matcher": "**/*.dist-info/METADATA",
"parser": "python"
}
]
diff --git a/bench/Scanner.Analyzers/lang/README.md b/bench/Scanner.Analyzers/lang/README.md
index 747e52f6..5aeb2671 100644
--- a/bench/Scanner.Analyzers/lang/README.md
+++ b/bench/Scanner.Analyzers/lang/README.md
@@ -3,10 +3,23 @@
This directory will capture benchmark results for language analyzers (Node, Python, Go, .NET, Rust).
Pending tasks:
-- LA1: Node analyzer microbench CSV + flamegraph.
-- LA2: Python hash throughput CSV.
-- LA3: Go build info extraction benchmarks.
-- LA4: .NET RID dedupe performance matrix.
-- LA5: Rust heuristic coverage comparisons.
-
-Results should be committed as deterministic CSV/JSON outputs with accompanying methodology notes.
+- LA1: Node analyzer microbench CSV + flamegraph.
+- LA2: Python hash throughput CSV.
+- LA3: Go build info extraction benchmarks.
+- LA4: .NET RID dedupe performance matrix.
+- LA5: Rust heuristic coverage comparisons.
+
+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 Oct 23 baseline (`baseline.csv`) shows a mean duration of **35.03 ms** (p95 136.55 ms, max 170.16 ms) over 5 iterations on the current rig; earlier Oct 21 measurement recorded **4.02 ms** 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.18 ms** (p95 18.64 ms, max 23.51 ms); 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.19 ms** (p95 106.62 ms, max 132.30 ms) with a stable component count of 2.
+- Syft v1.29.1 scanning the same fixture (`syft scan dir:…`) averaged **1 546 ms** (p95 ≈2 100 ms, max ≈2 100 ms) 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.
diff --git a/bench/Scanner.Analyzers/lang/dotnet/syft-comparison-20251023.csv b/bench/Scanner.Analyzers/lang/dotnet/syft-comparison-20251023.csv
new file mode 100644
index 00000000..014278a4
--- /dev/null
+++ b/bench/Scanner.Analyzers/lang/dotnet/syft-comparison-20251023.csv
@@ -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
diff --git a/bench/Scanner.Analyzers/lang/go/syft-comparison-20251021.csv b/bench/Scanner.Analyzers/lang/go/syft-comparison-20251021.csv
new file mode 100644
index 00000000..62bb4b2d
--- /dev/null
+++ b/bench/Scanner.Analyzers/lang/go/syft-comparison-20251021.csv
@@ -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
diff --git a/docs/24_OFFLINE_KIT.md b/docs/24_OFFLINE_KIT.md
index dc681c83..974c6e97 100755
--- a/docs/24_OFFLINE_KIT.md
+++ b/docs/24_OFFLINE_KIT.md
@@ -17,11 +17,11 @@ completely isolated network:
| **Provenance** | Cosign signature, SPDX 2.3 SBOM, in‑toto SLSA attestation |
| **Attested manifest** | `offline-manifest.json` + detached JWS covering bundle metadata, signed during export. |
| **Delta patches** | Daily diff bundles keep size \< 350 MB |
-| **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 air‑gapped 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 }}**.
*Imports are idempotent and atomic — no service downtime.*
@@ -59,12 +59,53 @@ jq '.artifacts[] | {name, sha256, size, capturedAt}' offline-manifest-.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.
-
----
-
-## 2 · Import on the air‑gapped host
-
-```bash
+
+Example excerpt (2025-10-23 kit) showing the Go and .NET analyzer plug-in payloads:
+
+```json
+{
+ "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 air‑gapped host
+
+```bash
docker compose --env-file .env \
-f docker-compose.stella-ops.yml \
exec stella-ops \
@@ -81,14 +122,22 @@ stellaops-cli offline kit import stella-ops-offline-kit-.tgz \
```
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.
-* Old feeds are kept until the new bundle is fully verified.
-* Import time on a SATA SSD: ≈ 25 s for a 300 MB kit.
-
----
-
-## 3 · Delta patch workflow
+
+* The CLI validates the Cosign signature **before** activation.
+* Old feeds are kept until the new bundle is fully verified.
+* Import time on a SATA SSD: ≈ 25 s for a 300 MB kit.
+
+**Quick smoke test:** before import, verify the tarball carries the Go analyzer plug-in:
+
+```bash
+tar -tzf stella-ops-offline-kit-.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-YYYY‑MM‑DD.delta.tgz`.
2. Transfer via any medium (USB, portable disk).
diff --git a/docs/ARCHITECTURE_SCANNER.md b/docs/ARCHITECTURE_SCANNER.md
index fd6fc1dd..19fa34ee 100644
--- a/docs/ARCHITECTURE_SCANNER.md
+++ b/docs/ARCHITECTURE_SCANNER.md
@@ -356,6 +356,7 @@ scanner:
* `scanner.layer_cache_hits_total`, `scanner.file_cas_hits_total`
* `scanner.artifact_bytes_total{format}`
* `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 15 minutes to highlight unexpected stripped binaries.
* **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.
diff --git a/docs/updates/2025-10-22-docs-guild.md b/docs/updates/2025-10-22-docs-guild.md
index 5a4525f2..6ffadc98 100644
--- a/docs/updates/2025-10-22-docs-guild.md
+++ b/docs/updates/2025-10-22-docs-guild.md
@@ -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.
- 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.
+- 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 2025‑10‑22 bundle before promoting it to mirrors.
Next steps:
- Concelier WebService owners to link this update in the next deployment bulletin once FEEDWEB-DOCS-01-001 clears review.
diff --git a/ops/offline-kit/TASKS.md b/ops/offline-kit/TASKS.md
index dd231ecf..19a19e68 100644
--- a/ops/offline-kit/TASKS.md
+++ b/ops/offline-kit/TASKS.md
@@ -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-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. |
diff --git a/out/tmp-cdx/Program.cs b/out/tmp-cdx/Program.cs
index 479258bd..cbdb864e 100644
--- a/out/tmp-cdx/Program.cs
+++ b/out/tmp-cdx/Program.cs
@@ -1,6 +1,4 @@
-using System;
-using CycloneDX.Models;
-
-var dependenciesProperty = typeof(Dependency).GetProperty("Dependencies")!;
-Console.WriteLine(dependenciesProperty.PropertyType);
-Console.WriteLine(dependenciesProperty.PropertyType.GenericTypeArguments[0]);
+using System;
+using CycloneDX.Models;
+
+Console.WriteLine(string.Join(", ", Enum.GetNames(typeof(Component.Classification))));
diff --git a/out/tmp-cdx/tmp-cdx.csproj b/out/tmp-cdx/tmp-cdx.csproj
index 1670a49c..6b239424 100644
--- a/out/tmp-cdx/tmp-cdx.csproj
+++ b/out/tmp-cdx/tmp-cdx.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/samples/api/reports/report-sample.dsse.json b/samples/api/reports/report-sample.dsse.json
index 9ae89de4..b56bf2c5 100644
--- a/samples/api/reports/report-sample.dsse.json
+++ b/samples/api/reports/report-sample.dsse.json
@@ -8,39 +8,45 @@
"revisionId": "rev-1",
"digest": "27d2ec2b34feedc304fc564d252ecee1c8fa14ea581a5ff5c1ea8963313d5c8d"
},
- "summary": {
- "total": 1,
- "blocked": 1,
- "warned": 0,
- "ignored": 0,
- "quieted": 0
- },
- "verdicts": [
- {
- "findingId": "finding-1",
- "status": "Blocked",
- "ruleName": "Block Critical",
- "ruleAction": "Block",
- "score": 40.5,
- "configVersion": "1.0",
- "inputs": {
- "reachabilityWeight": 0.45,
- "baseScore": 40.5,
- "severityWeight": 90,
- "trustWeight": 1,
- "trustWeight.NVD": 1,
- "reachability.runtime": 0.45
- },
- "quiet": false,
- "sourceTrust": "NVD",
- "reachability": "runtime"
- }
+ "summary": {
+ "total": 1,
+ "blocked": 1,
+ "warned": 0,
+ "ignored": 0,
+ "quieted": 1
+ },
+ "verdicts": [
+ {
+ "findingId": "finding-1",
+ "status": "Blocked",
+ "ruleName": "Block Critical",
+ "ruleAction": "Block",
+ "score": 40.5,
+ "configVersion": "1.0",
+ "inputs": {
+ "reachabilityWeight": 0.45,
+ "baseScore": 40.5,
+ "severityWeight": 90,
+ "trustWeight": 1,
+ "trustWeight.NVD": 1,
+ "reachability.runtime": 0.45,
+ "unknownConfidence": 0.52,
+ "unknownAgeDays": 4
+ },
+ "quietedBy": "policy/quiet-critical-runtime",
+ "quiet": true,
+ "unknownConfidence": 0.52,
+ "confidenceBand": "medium",
+ "unknownAgeDays": 4,
+ "sourceTrust": "NVD",
+ "reachability": "runtime"
+ }
],
"issues": []
},
"dsse": {
"payloadType": "application/vnd.stellaops.report+json",
- "payload": "eyJyZXBvcnRJZCI6InJlcG9ydC0zZGVmNWYzNjJhYTQ3NWVmMTRiNiIsImltYWdlRGlnZXN0Ijoic2hhMjU2OmRlYWRiZWVmIiwiZ2VuZXJhdGVkQXQiOiIyMDI1LTEwLTE5VDA4OjI4OjA5LjM2OTkyNjcrMDA6MDAiLCJ2ZXJkaWN0IjoiYmxvY2tlZCIsInBvbGljeSI6eyJyZXZpc2lvbklkIjoicmV2LTEiLCJkaWdlc3QiOiIyN2QyZWMyYjM0ZmVlZGMzMDRmYzU2NGQyNTJlY2VlMWM4ZmExNGVhNTgxYTVmZjVjMWVhODk2MzMxM2Q1YzhkIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MH0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwicnVsZU5hbWUiOiJCbG9jayBDcml0aWNhbCIsInJ1bGVBY3Rpb24iOiJCbG9jayIsInNjb3JlIjo0MC41LCJjb25maWdWZXJzaW9uIjoiMS4wIiwiaW5wdXRzIjp7InJlYWNoYWJpbGl0eVdlaWdodCI6MC40NSwiYmFzZVNjb3JlIjo0MC41LCJzZXZlcml0eVdlaWdodCI6OTAsInRydXN0V2VpZ2h0IjoxLCJ0cnVzdFdlaWdodC5OVkQiOjEsInJlYWNoYWJpbGl0eS5ydW50aW1lIjowLjQ1fSwicXVpZXQiOmZhbHNlLCJzb3VyY2VUcnVzdCI6Ik5WRCIsInJlYWNoYWJpbGl0eSI6InJ1bnRpbWUifV0sImlzc3VlcyI6W119",
+ "payload": "eyJyZXBvcnRJZCI6InJlcG9ydC0zZGVmNWYzNjJhYTQ3NWVmMTRiNiIsImltYWdlRGlnZXN0Ijoic2hhMjU2OmRlYWRiZWVmIiwiZ2VuZXJhdGVkQXQiOiIyMDI1LTEwLTE5VDA4OjI4OjA5LjM2OTkyNjcrMDA6MDAiLCJ2ZXJkaWN0IjoiYmxvY2tlZCIsInBvbGljeSI6eyJyZXZpc2lvbklkIjoicmV2LTEiLCJkaWdlc3QiOiIyN2QyZWMyYjM0ZmVlZGMzMDRmYzU2NGQyNTJlY2VlMWM4ZmExNGVhNTgxYTVmZjVjMWVhODk2MzMxM2Q1YzhkIn0sInN1bW1hcnkiOnsidG90YWwiOjEsImJsb2NrZWQiOjEsIndhcm5lZCI6MCwiaWdub3JlZCI6MCwicXVpZXRlZCI6MX0sInZlcmRpY3RzIjpbeyJmaW5kaW5nSWQiOiJmaW5kaW5nLTEiLCJzdGF0dXMiOiJCbG9ja2VkIiwicnVsZU5hbWUiOiJCbG9jayBDcml0aWNhbCIsInJ1bGVBY3Rpb24iOiJCbG9jayIsInNjb3JlIjo0MC41LCJjb25maWdWZXJzaW9uIjoiMS4wIiwiaW5wdXRzIjp7InJlYWNoYWJpbGl0eVdlaWdodCI6MC40NSwiYmFzZVNjb3JlIjo0MC41LCJzZXZlcml0eVdlaWdodCI6OTAsInRydXN0V2VpZ2h0IjoxLCJ0cnVzdFdlaWdodC5OVkQiOjEsInJlYWNoYWJpbGl0eS5ydW50aW1lIjowLjQ1LCJ1bmtub3duQ29uZmlkZW5jZSI6MC41MiwidW5rbm93bkFnZURheXMiOjR9LCJxdWlldGVkQnkiOiJwb2xpY3kvcXVpZXQtY3JpdGljYWwtcnVudGltZSIsInF1aWV0Ijp0cnVlLCJ1bmtub3duQ29uZmlkZW5jZSI6MC41MiwiY29uZmlkZW5jZUJhbmQiOiJtZWRpdW0iLCJ1bmtub3duQWdlRGF5cyI6NCwic291cmNlVHJ1c3QiOiJOVkQiLCJyZWFjaGFiaWxpdHkiOiJydW50aW1lIn1dLCJpc3N1ZXMiOltdfQ==",
"signatures": [
{
"keyId": "scanner-report-signing",
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md b/src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md
index 1e9540c6..759ef04c 100644
--- a/src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md
+++ b/src/StellaOps.Scanner.Analyzers.Lang.DotNet/TASKS.md
@@ -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. |
| 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. |
-| 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. |
-| 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. |
+| 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 | 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. |
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Go.Tests/Fixtures/lang/go/stripped/expected.json b/src/StellaOps.Scanner.Analyzers.Lang.Go.Tests/Fixtures/lang/go/stripped/expected.json
index 815ffbd9..bdd1a771 100644
--- a/src/StellaOps.Scanner.Analyzers.Lang.Go.Tests/Fixtures/lang/go/stripped/expected.json
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Go.Tests/Fixtures/lang/go/stripped/expected.json
@@ -1,12 +1,12 @@
[
{
"analyzerId": "golang",
- "componentKey": "golang::bin::sha256:80f528c90b72a4c4cc3fa078501154e4f2a3f49faea3ec380112d61740bde4c3",
+ "componentKey": "golang::bin::sha256:7125d65230b913faa744a33acd884899c81a1dbc6d88cbf251a74b19621cde99",
"name": "app",
"type": "bin",
"usedByEntrypoint": false,
"metadata": {
- "binary.sha256": "80f528c90b72a4c4cc3fa078501154e4f2a3f49faea3ec380112d61740bde4c3",
+ "binary.sha256": "7125d65230b913faa744a33acd884899c81a1dbc6d88cbf251a74b19621cde99",
"binaryPath": "app",
"go.version.hint": "go1.22.8",
"languageHint": "golang",
@@ -17,7 +17,7 @@
"kind": "file",
"source": "binary",
"locator": "app",
- "sha256": "80f528c90b72a4c4cc3fa078501154e4f2a3f49faea3ec380112d61740bde4c3"
+ "sha256": "7125d65230b913faa744a33acd884899c81a1dbc6d88cbf251a74b19621cde99"
},
{
"kind": "metadata",
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Go.Tests/Go/GoLanguageAnalyzerTests.cs b/src/StellaOps.Scanner.Analyzers.Lang.Go.Tests/Go/GoLanguageAnalyzerTests.cs
index a87e8005..373f7cde 100644
--- a/src/StellaOps.Scanner.Analyzers.Lang.Go.Tests/Go/GoLanguageAnalyzerTests.cs
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Go.Tests/Go/GoLanguageAnalyzerTests.cs
@@ -1,4 +1,7 @@
+using System;
+using System.Diagnostics.Metrics;
using System.IO;
+using System.Linq;
using StellaOps.Scanner.Analyzers.Lang.Go;
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
@@ -63,4 +66,69 @@ public sealed class GoLanguageAnalyzerTests
analyzers,
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((_, 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));
+ }
}
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Go/AGENTS.md b/src/StellaOps.Scanner.Analyzers.Lang.Go/AGENTS.md
index 8abf0f95..c219287c 100644
--- a/src/StellaOps.Scanner.Analyzers.Lang.Go/AGENTS.md
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Go/AGENTS.md
@@ -15,15 +15,17 @@ Build the Go analyzer plug-in that reads Go build info, module metadata, and DWA
- Policy decisions or vulnerability joins.
## Expectations
-- Latency targets: ≤400 µs (hot) / ≤2 ms (cold) per binary; minimal allocations via buffer pooling.
-- Deterministic fallback to `bin:{sha256}` when metadata absent; heuristics clearly identified.
-- Offline-first: rely solely on embedded metadata.
-- Telemetry for binaries processed, metadata coverage, heuristics usage.
-
-## Dependencies
-- Shared language analyzer core; Worker dispatcher; caching infrastructure (layer cache + file CAS).
+- Latency targets: ≤400 µs (hot) / ≤2 ms (cold) per binary; minimal allocations via buffer pooling.
+- Shared buffer pooling via `ArrayPool` for build-info/DWARF reads; safe for concurrent scans.
+- Deterministic fallback to `bin:{sha256}` when metadata absent; heuristics clearly identified.
+- Offline-first: rely solely on embedded metadata.
+- Telemetry for binaries processed, metadata coverage, heuristics usage.
+- Heuristic fallback metrics: `scanner_analyzer_golang_heuristic_total{indicator,version_hint}` increments whenever stripped binaries are classified via fallbacks.
+
+## Dependencies
+- Shared language analyzer core; Worker dispatcher; caching infrastructure (layer cache + file CAS).
## Testing & Artifacts
-- Golden fixtures for modules with/without VCS info, stripped binaries, cross-compiled variants.
-- Benchmark comparison with competitor scanners to demonstrate speed/fidelity advantages.
-- ADR documenting heuristics and risk mitigation.
+- Golden fixtures for modules with/without VCS info, stripped binaries, cross-compiled variants.
+- Benchmark comparison with competitor scanners to demonstrate speed/fidelity advantages (captured in `bench/Scanner.Analyzers/lang/go/`).
+- ADR documenting heuristics and risk mitigation.
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Go/GoLanguageAnalyzer.cs b/src/StellaOps.Scanner.Analyzers.Lang.Go/GoLanguageAnalyzer.cs
index 41a6642a..16d9a26c 100644
--- a/src/StellaOps.Scanner.Analyzers.Lang.Go/GoLanguageAnalyzer.cs
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Go/GoLanguageAnalyzer.cs
@@ -233,6 +233,8 @@ public sealed class GoLanguageAnalyzer : ILanguageAnalyzer
metadata: metadata,
evidence: evidence,
usedByEntrypoint: usedByEntrypoint);
+
+ GoAnalyzerMetrics.RecordHeuristic(strippedBinary.Indicator, !string.IsNullOrEmpty(strippedBinary.GoVersionHint));
}
private static IEnumerable BuildEvidence(GoBuildInfo buildInfo, GoModule module, string binaryRelativePath, LanguageAnalyzerContext context, ref string? binaryHash)
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Go/Internal/GoAnalyzerMetrics.cs b/src/StellaOps.Scanner.Analyzers.Lang.Go/Internal/GoAnalyzerMetrics.cs
new file mode 100644
index 00000000..801c1d6a
--- /dev/null
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Go/Internal/GoAnalyzerMetrics.cs
@@ -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 HeuristicCounter = Meter.CreateCounter(
+ "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("indicator", NormalizeIndicator(indicator)),
+ new KeyValuePair("version_hint", hasVersionHint ? "present" : "absent"));
+ }
+
+ private static string NormalizeIndicator(GoStrippedBinaryIndicator indicator)
+ => indicator switch
+ {
+ GoStrippedBinaryIndicator.BuildId => "build-id",
+ GoStrippedBinaryIndicator.GoRuntimeMarkers => "runtime-markers",
+ _ => "unknown",
+ };
+}
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Go/Internal/GoBinaryScanner.cs b/src/StellaOps.Scanner.Analyzers.Lang.Go/Internal/GoBinaryScanner.cs
index 8676c3a4..cc1f5774 100644
--- a/src/StellaOps.Scanner.Analyzers.Lang.Go/Internal/GoBinaryScanner.cs
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Go/Internal/GoBinaryScanner.cs
@@ -38,16 +38,59 @@ internal static class GoBinaryScanner
goVersion = null;
moduleData = null;
+ FileInfo info;
try
{
- var info = new FileInfo(filePath);
+ info = new FileInfo(filePath);
if (!info.Exists || info.Length < 64 || info.Length > 128 * 1024 * 1024)
{
return false;
}
+ }
+ catch (IOException)
+ {
+ return false;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return false;
+ }
+ catch (System.Security.SecurityException)
+ {
+ return false;
+ }
- var data = File.ReadAllBytes(filePath);
- var span = new ReadOnlySpan(data);
+ var length = info.Length;
+ if (length <= 0)
+ {
+ return false;
+ }
+
+ var inspectLength = (int)Math.Min(length, int.MaxValue);
+ var buffer = ArrayPool.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(buffer, 0, totalRead);
var offset = span.IndexOf(BuildInfoMagic.Span);
if (offset < 0)
{
@@ -65,6 +108,11 @@ internal static class GoBinaryScanner
{
return false;
}
+ finally
+ {
+ Array.Clear(buffer, 0, inspectLength);
+ ArrayPool.Shared.Return(buffer);
+ }
}
public static bool TryClassifyStrippedBinary(string filePath, out GoStrippedBinaryClassification classification)
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Go/Internal/GoDwarfReader.cs b/src/StellaOps.Scanner.Analyzers.Lang.Go/Internal/GoDwarfReader.cs
index 96b8f1bf..e1fa5f76 100644
--- a/src/StellaOps.Scanner.Analyzers.Lang.Go/Internal/GoDwarfReader.cs
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Go/Internal/GoDwarfReader.cs
@@ -1,4 +1,5 @@
using System;
+using System.Buffers;
using System.IO;
using System.Text;
@@ -15,16 +16,10 @@ internal static class GoDwarfReader
{
metadata = null;
- ReadOnlySpan data;
+ FileInfo fileInfo;
try
{
- var fileInfo = new FileInfo(path);
- if (!fileInfo.Exists || fileInfo.Length == 0 || fileInfo.Length > 256 * 1024 * 1024)
- {
- return false;
- }
-
- data = File.ReadAllBytes(path);
+ fileInfo = new FileInfo(path);
}
catch (IOException)
{
@@ -35,27 +30,62 @@ internal static class GoDwarfReader
return false;
}
- 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))
+ if (!fileInfo.Exists || fileInfo.Length == 0 || fileInfo.Length > 256 * 1024 * 1024)
{
return false;
}
- metadata = new GoDwarfMetadata(system, revision, modified, timestamp);
- return true;
+ var length = fileInfo.Length;
+ var readLength = (int)Math.Min(length, int.MaxValue);
+ var buffer = ArrayPool.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(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.Shared.Return(buffer);
+ }
}
private static string? ExtractValue(ReadOnlySpan data, ReadOnlySpan token)
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md b/src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md
index 5e1490d3..8e1a38e3 100644
--- a/src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Go/TASKS.md
@@ -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.18–1.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 %. |
| 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. |
-| 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. |
-| 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. |
-| 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. |
+| 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 | 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 | 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 | 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. |
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md b/src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md
index a4ecd1e7..0144765d 100644
--- a/src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Python/TASKS.md
@@ -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.8–3.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 5 GB RECORD fixture without allocations >2 MB; mismatches produce deterministic evidence records. |
| 3 | SCANNER-ANALYZERS-LANG-10-303C | DONE (2025-10-21) | SCANNER-ANALYZERS-LANG-10-303B | Editable install + pip cache detection; integrate EntryTrace hints for runtime usage flags. | Editable installs resolved to source path; usage flags propagated; regression tests cover mixed editable + wheel installs. |
-| 4 | SCANNER-ANALYZERS-LANG-10-307P | 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. |
| 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. |
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Tests/DotNet/DotNetLanguageAnalyzerTests.cs b/src/StellaOps.Scanner.Analyzers.Lang.Tests/DotNet/DotNetLanguageAnalyzerTests.cs
index 722b7675..9a9416a0 100644
--- a/src/StellaOps.Scanner.Analyzers.Lang.Tests/DotNet/DotNetLanguageAnalyzerTests.cs
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Tests/DotNet/DotNetLanguageAnalyzerTests.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Scanner.Analyzers.Lang.DotNet;
@@ -102,6 +103,54 @@ public sealed class DotNetLanguageAnalyzerTests
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
{
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/AppA.deps.json b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/AppA.deps.json
new file mode 100644
index 00000000..b5412459
--- /dev/null
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/AppA.deps.json
@@ -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"
+ }
+ }
+}
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/AppA.runtimeconfig.json b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/AppA.runtimeconfig.json
new file mode 100644
index 00000000..36677e13
--- /dev/null
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/AppA.runtimeconfig.json
@@ -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"
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/AppB.deps.json b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/AppB.deps.json
new file mode 100644
index 00000000..0a5b09f1
--- /dev/null
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/AppB.deps.json
@@ -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"
+ }
+ }
+}
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/AppB.runtimeconfig.json b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/AppB.runtimeconfig.json
new file mode 100644
index 00000000..049280f8
--- /dev/null
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/AppB.runtimeconfig.json
@@ -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"
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/expected.json b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/expected.json
new file mode 100644
index 00000000..181578b8
--- /dev/null
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/expected.json
@@ -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"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/packages/stellaops.logging/2.5.1/LICENSE.txt b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/packages/stellaops.logging/2.5.1/LICENSE.txt
new file mode 100644
index 00000000..a2662911
--- /dev/null
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/packages/stellaops.logging/2.5.1/LICENSE.txt
@@ -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.
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/packages/stellaops.logging/2.5.1/stellaops.logging.nuspec b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/packages/stellaops.logging/2.5.1/stellaops.logging.nuspec
new file mode 100644
index 00000000..e8fe924b
--- /dev/null
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/packages/stellaops.logging/2.5.1/stellaops.logging.nuspec
@@ -0,0 +1,12 @@
+
+
+
+ StellaOps.Logging
+ 2.5.1
+ StellaOps
+ Logging sample package for analyzer fixtures.
+ Apache-2.0
+ https://stella-ops.example/licenses/logging
+ https://stella-ops.example/projects/logging
+
+
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/packages/stellaops.toolkit/1.2.3/LICENSE.txt b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/packages/stellaops.toolkit/1.2.3/LICENSE.txt
new file mode 100644
index 00000000..0b18126c
--- /dev/null
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/packages/stellaops.toolkit/1.2.3/LICENSE.txt
@@ -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.
diff --git a/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/packages/stellaops.toolkit/1.2.3/stellaops.toolkit.nuspec b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/packages/stellaops.toolkit/1.2.3/stellaops.toolkit.nuspec
new file mode 100644
index 00000000..0889b15f
--- /dev/null
+++ b/src/StellaOps.Scanner.Analyzers.Lang.Tests/Fixtures/lang/dotnet/multi/packages/stellaops.toolkit/1.2.3/stellaops.toolkit.nuspec
@@ -0,0 +1,11 @@
+
+
+
+ StellaOps.Toolkit
+ 1.2.3
+ StellaOps
+ Toolkit sample package for analyzer fixtures.
+ LICENSE.txt
+ https://stella-ops.example/licenses/toolkit
+
+
diff --git a/src/StellaOps.Scanner.Analyzers.Lang/SPRINTS_LANG_IMPLEMENTATION_PLAN.md b/src/StellaOps.Scanner.Analyzers.Lang/SPRINTS_LANG_IMPLEMENTATION_PLAN.md
index 1327d2a9..3d325471 100644
--- a/src/StellaOps.Scanner.Analyzers.Lang/SPRINTS_LANG_IMPLEMENTATION_PLAN.md
+++ b/src/StellaOps.Scanner.Analyzers.Lang/SPRINTS_LANG_IMPLEMENTATION_PLAN.md
@@ -68,6 +68,7 @@ All sprints below assume prerequisites from SP10-G2 (core scaffolding + Java ana
- Tests verifying dual runtimeconfig merge logic.
- 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-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)
- **Scope:** Detect crates via metadata in `.fingerprint`, Cargo.lock fragments, or embedded `rustc` markers; robust fallback to binary hash classification.
diff --git a/src/StellaOps.Scanner.Analyzers.Lang/TASKS.md b/src/StellaOps.Scanner.Analyzers.Lang/TASKS.md
index 8c6e75b0..e53f4d52 100644
--- a/src/StellaOps.Scanner.Analyzers.Lang/TASKS.md
+++ b/src/StellaOps.Scanner.Analyzers.Lang/TASKS.md
@@ -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-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-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-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. |
diff --git a/src/StellaOps.Scanner.Emit.Tests/Composition/CycloneDxComposerTests.cs b/src/StellaOps.Scanner.Emit.Tests/Composition/CycloneDxComposerTests.cs
index 4f325250..460d1c42 100644
--- a/src/StellaOps.Scanner.Emit.Tests/Composition/CycloneDxComposerTests.cs
+++ b/src/StellaOps.Scanner.Emit.Tests/Composition/CycloneDxComposerTests.cs
@@ -21,17 +21,46 @@ public sealed class CycloneDxComposerTests
Assert.NotNull(result.Inventory);
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+protobuf; version=1.5", result.Inventory.ProtobufMediaType);
- Assert.Equal(2, result.Inventory.Components.Length);
-
- Assert.NotNull(result.Usage);
- Assert.Equal("application/vnd.cyclonedx+json; version=1.5; view=usage", result.Usage!.JsonMediaType);
- Assert.Single(result.Usage.Components);
- Assert.Equal("pkg:npm/a", result.Usage.Components[0].Identity.Key);
-
- ValidateJson(result.Inventory.JsonBytes, expectedComponentCount: 2, expectedView: "inventory");
- ValidateJson(result.Usage.JsonBytes, expectedComponentCount: 1, expectedView: "usage");
+ Assert.Equal("application/vnd.cyclonedx+json; version=1.6", result.Inventory.JsonMediaType);
+ Assert.Equal("application/vnd.cyclonedx+protobuf; version=1.6", result.Inventory.ProtobufMediaType);
+ Assert.Equal(2, result.Inventory.Components.Length);
+
+ Assert.NotNull(result.Usage);
+ Assert.Equal("application/vnd.cyclonedx+json; version=1.6; view=usage", result.Usage!.JsonMediaType);
+ Assert.Single(result.Usage.Components);
+ Assert.Equal("pkg:npm/a", result.Usage.Components[0].Identity.Key);
+
+ ValidateJson(result.Inventory.JsonBytes, expectedComponentCount: 2, expectedView: "inventory");
+ 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]
@@ -109,16 +138,54 @@ public sealed class CycloneDxComposerTests
Architecture = "amd64",
};
- return SbomCompositionRequest.Create(
- image,
- fragments,
- new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero),
- generatorName: "StellaOps.Scanner",
- generatorVersion: "0.10.0",
- properties: new Dictionary
- {
- ["stellaops:scanId"] = "scan-1234",
- });
+ return SbomCompositionRequest.Create(
+ image,
+ fragments,
+ new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero),
+ generatorName: "StellaOps.Scanner",
+ generatorVersion: "0.10.0",
+ properties: new Dictionary
+ {
+ ["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("severityWeight", 90),
+ new KeyValuePair("trustWeight", 1.0),
+ new KeyValuePair("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("severityWeight", 55),
+ new KeyValuePair("trustWeight", 0.85))
+ }
+ });
}
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");
var properties = metadata.GetProperty("properties");
- var viewProperty = properties.EnumerateArray()
- .Single(prop => prop.GetProperty("name").GetString() == "stellaops:sbom.view");
+ var viewProperty = properties.EnumerateArray()
+ .Single(prop => string.Equals(prop.GetProperty("name").GetString(), "stellaops:sbom.view", StringComparison.Ordinal));
Assert.Equal(expectedView, viewProperty.GetProperty("value").GetString());
var components = root.GetProperty("components").EnumerateArray().ToArray();
Assert.Equal(expectedComponentCount, components.Length);
- var names = components.Select(component => component.GetProperty("name").GetString()).ToArray();
- Assert.Equal(names, names.OrderBy(n => n, StringComparer.Ordinal).ToArray());
-
- var firstComponentProperties = components[0].GetProperty("properties").EnumerateArray().ToDictionary(
- element => element.GetProperty("name").GetString(),
- element => element.GetProperty("value").GetString());
+ var names = components.Select(component => component.GetProperty("name").GetString()!).ToArray();
+ Assert.Equal(names, names.OrderBy(n => n, StringComparer.Ordinal).ToArray());
+
+ var firstComponentProperties = components[0].GetProperty("properties").EnumerateArray().ToDictionary(
+ element => element.GetProperty("name").GetString()!,
+ element => element.GetProperty("value").GetString()!,
+ StringComparer.Ordinal);
Assert.Equal("apk", firstComponentProperties["stellaops.os.analyzer"]);
Assert.Equal("x86_64", firstComponentProperties["stellaops.os.architecture"]);
diff --git a/src/StellaOps.Scanner.Emit.Tests/Packaging/ScannerArtifactPackageBuilderTests.cs b/src/StellaOps.Scanner.Emit.Tests/Packaging/ScannerArtifactPackageBuilderTests.cs
index 3e7b089d..e961634d 100644
--- a/src/StellaOps.Scanner.Emit.Tests/Packaging/ScannerArtifactPackageBuilderTests.cs
+++ b/src/StellaOps.Scanner.Emit.Tests/Packaging/ScannerArtifactPackageBuilderTests.cs
@@ -76,7 +76,7 @@ public sealed class ScannerArtifactPackageBuilderTests
Assert.Equal(5, root.GetProperty("artifacts").GetArrayLength());
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? metadata = null)
diff --git a/src/StellaOps.Scanner.Emit/Composition/CycloneDxComposer.cs b/src/StellaOps.Scanner.Emit/Composition/CycloneDxComposer.cs
index 498127c1..577d29b1 100644
--- a/src/StellaOps.Scanner.Emit/Composition/CycloneDxComposer.cs
+++ b/src/StellaOps.Scanner.Emit/Composition/CycloneDxComposer.cs
@@ -6,7 +6,8 @@ using System.Linq;
using System.Security.Cryptography;
using System.Text;
using CycloneDX;
-using CycloneDX.Models;
+using CycloneDX.Models;
+using CycloneDX.Models.Vulnerabilities;
using JsonSerializer = CycloneDX.Json.Serializer;
using ProtoSerializer = CycloneDX.Protobuf.Serializer;
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 const string InventoryMediaTypeJson = "application/vnd.cyclonedx+json; version=1.5";
- private const string UsageMediaTypeJson = "application/vnd.cyclonedx+json; version=1.5; view=usage";
- private const string InventoryMediaTypeProtobuf = "application/vnd.cyclonedx+protobuf; version=1.5";
- private const string UsageMediaTypeProtobuf = "application/vnd.cyclonedx+protobuf; version=1.5; view=usage";
+ private const string InventoryMediaTypeJson = "application/vnd.cyclonedx+json; version=1.6";
+ private const string UsageMediaTypeJson = "application/vnd.cyclonedx+json; version=1.6; view=usage";
+ private const string InventoryMediaTypeProtobuf = "application/vnd.cyclonedx+protobuf; version=1.6";
+ private const string UsageMediaTypeProtobuf = "application/vnd.cyclonedx+protobuf; version=1.6; view=usage";
public SbomCompositionResult Compose(SbomCompositionRequest request)
{
@@ -77,7 +78,7 @@ public sealed class CycloneDxComposer
string jsonMediaType,
string protobufMediaType)
{
- var bom = BuildBom(request, view, components, generatedAt);
+ var bom = BuildBom(request, graph, view, components, generatedAt);
var json = JsonSerializer.Serialize(bom);
var jsonBytes = Encoding.UTF8.GetBytes(json);
var protobufBytes = ProtoSerializer.Serialize(bom);
@@ -100,25 +101,32 @@ public sealed class CycloneDxComposer
};
}
- private Bom BuildBom(
- SbomCompositionRequest request,
- SbomView view,
- ImmutableArray components,
- DateTimeOffset generatedAt)
- {
- var bom = new Bom
- {
- SpecVersion = SpecificationVersion.v1_4,
- Version = 1,
- Metadata = BuildMetadata(request, view, generatedAt),
- 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)}";
-
- return bom;
+ private Bom BuildBom(
+ SbomCompositionRequest request,
+ ComponentGraph graph,
+ SbomView view,
+ ImmutableArray components,
+ DateTimeOffset generatedAt)
+ {
+ var bom = new Bom
+ {
+ SpecVersion = SpecificationVersion.v1_6,
+ Version = 1,
+ Metadata = BuildMetadata(request, view, generatedAt),
+ Components = BuildComponents(components),
+ Dependencies = BuildDependencies(components),
+ };
+
+ var vulnerabilities = BuildVulnerabilities(request, graph, components);
+ if (vulnerabilities is not null)
+ {
+ 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)
@@ -129,23 +137,11 @@ public sealed class CycloneDxComposer
Component = BuildMetadataComponent(request.Image),
};
- if (!string.IsNullOrWhiteSpace(request.GeneratorName))
- {
- metadata.Tools = new List
- {
- new()
- {
- 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)
+ 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
{
Name = pair.Key,
@@ -154,15 +150,33 @@ public sealed class CycloneDxComposer
.ToList();
}
- if (metadata.Properties is null)
- {
- metadata.Properties = new List();
- }
-
- metadata.Properties.Add(new Property
- {
- Name = "stellaops:sbom.view",
- Value = view.ToString().ToLowerInvariant(),
+ if (metadata.Properties is null)
+ {
+ metadata.Properties = new List();
+ }
+
+ if (!string.IsNullOrWhiteSpace(request.GeneratorName))
+ {
+ metadata.Properties.Add(new Property
+ {
+ 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;
@@ -357,8 +371,171 @@ public sealed class CycloneDxComposer
});
}
- return dependencies.Count == 0 ? null : dependencies;
- }
+ return dependencies.Count == 0 ? null : dependencies;
+ }
+
+ private static List? BuildVulnerabilities(
+ SbomCompositionRequest request,
+ ComponentGraph graph,
+ ImmutableArray 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(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
+ {
+ new() { Ref = component.Identity.Key }
+ },
+ Ratings = ratings,
+ Properties = properties,
+ };
+
+ vulnerabilities.Add(vulnerability);
+ }
+
+ return vulnerabilities.Count == 0 ? null : vulnerabilities;
+ }
+
+ private static List? BuildRatings(double score)
+ {
+ if (double.IsNaN(score) || double.IsInfinity(score))
+ {
+ return null;
+ }
+
+ return new List
+ {
+ new()
+ {
+ Method = ScoreMethod.Other,
+ Justification = "StellaOps Policy score",
+ Score = score,
+ Severity = Severity.Unknown,
+ Source = new Source { Name = "StellaOps.Policy" },
+ }
+ };
+ }
+
+ private static List? BuildVulnerabilityProperties(SbomPolicyFinding finding)
+ {
+ var properties = new List();
+
+ 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 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 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 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 properties, string name, double? value)
+ {
+ if (!value.HasValue)
+ {
+ return;
+ }
+
+ AddDoubleProperty(properties, name, value.Value);
+ }
private static Component.Classification MapClassification(string? type)
{
@@ -372,7 +549,7 @@ public sealed class CycloneDxComposer
"application" => Component.Classification.Application,
"framework" => Component.Classification.Framework,
"container" => Component.Classification.Container,
- "operating-system" or "os" => Component.Classification.OperationSystem,
+ "operating-system" or "os" => Component.Classification.Operating_System,
"device" => Component.Classification.Device,
"firmware" => Component.Classification.Firmware,
"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();
var hash = sha256.ComputeHash(bytes);
diff --git a/src/StellaOps.Scanner.Emit/Composition/SbomCompositionRequest.cs b/src/StellaOps.Scanner.Emit/Composition/SbomCompositionRequest.cs
index c5fb0bd8..7125dd60 100644
--- a/src/StellaOps.Scanner.Emit/Composition/SbomCompositionRequest.cs
+++ b/src/StellaOps.Scanner.Emit/Composition/SbomCompositionRequest.cs
@@ -39,21 +39,25 @@ public sealed record SbomCompositionRequest
public string? GeneratorVersion { get; init; }
= null;
- public IReadOnlyDictionary? AdditionalProperties { get; init; }
- = null;
-
- public static SbomCompositionRequest Create(
- ImageArtifactDescriptor image,
- IEnumerable fragments,
- DateTimeOffset generatedAt,
- string? generatorName = null,
- string? generatorVersion = null,
- IReadOnlyDictionary? properties = null)
- {
- ArgumentNullException.ThrowIfNull(image);
- ArgumentNullException.ThrowIfNull(fragments);
-
- var normalizedImage = new ImageArtifactDescriptor
+ public IReadOnlyDictionary? AdditionalProperties { get; init; }
+ = null;
+
+ public ImmutableArray PolicyFindings { get; init; }
+ = ImmutableArray.Empty;
+
+ public static SbomCompositionRequest Create(
+ ImageArtifactDescriptor image,
+ IEnumerable fragments,
+ DateTimeOffset generatedAt,
+ string? generatorName = null,
+ string? generatorVersion = null,
+ IReadOnlyDictionary? properties = null,
+ IEnumerable? policyFindings = null)
+ {
+ ArgumentNullException.ThrowIfNull(image);
+ ArgumentNullException.ThrowIfNull(fragments);
+
+ var normalizedImage = new ImageArtifactDescriptor
{
ImageDigest = ScannerIdentifiers.NormalizeDigest(image.ImageDigest) ?? throw new ArgumentException("Image digest is required.", nameof(image)),
ImageReference = Normalize(image.ImageReference),
@@ -65,21 +69,68 @@ public sealed record SbomCompositionRequest
return new SbomCompositionRequest
{
Image = normalizedImage,
- LayerFragments = fragments.ToImmutableArray(),
- GeneratedAt = ScannerTimestamps.Normalize(generatedAt),
- GeneratorName = Normalize(generatorName),
- GeneratorVersion = Normalize(generatorVersion),
- AdditionalProperties = properties,
- };
- }
-
- private static string? Normalize(string? value)
- {
+ LayerFragments = fragments.ToImmutableArray(),
+ GeneratedAt = ScannerTimestamps.Normalize(generatedAt),
+ GeneratorName = Normalize(generatorName),
+ GeneratorVersion = Normalize(generatorVersion),
+ AdditionalProperties = properties,
+ PolicyFindings = NormalizePolicyFindings(policyFindings),
+ };
+ }
+
+ private static string? Normalize(string? value)
+ {
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
-
- return value.Trim();
- }
-}
+
+ return value.Trim();
+ }
+
+ private static ImmutableArray NormalizePolicyFindings(IEnumerable? policyFindings)
+ {
+ if (policyFindings is null)
+ {
+ return ImmutableArray.Empty;
+ }
+
+ var builder = ImmutableArray.CreateBuilder();
+ 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.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();
+ }
+}
diff --git a/src/StellaOps.Scanner.Emit/Composition/SbomPolicyFinding.cs b/src/StellaOps.Scanner.Emit/Composition/SbomPolicyFinding.cs
new file mode 100644
index 00000000..d0d77ea8
--- /dev/null
+++ b/src/StellaOps.Scanner.Emit/Composition/SbomPolicyFinding.cs
@@ -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> Inputs { get; init; } = ImmutableArray>.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>.Empty
+ : Inputs
+ .Where(static pair => !string.IsNullOrWhiteSpace(pair.Key))
+ .Select(static pair => new KeyValuePair(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
+ };
+ }
+}
diff --git a/src/StellaOps.Scanner.Emit/StellaOps.Scanner.Emit.csproj b/src/StellaOps.Scanner.Emit/StellaOps.Scanner.Emit.csproj
index 911c78f5..e4103827 100644
--- a/src/StellaOps.Scanner.Emit/StellaOps.Scanner.Emit.csproj
+++ b/src/StellaOps.Scanner.Emit/StellaOps.Scanner.Emit.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/src/StellaOps.Scanner.Emit/TASKS.md b/src/StellaOps.Scanner.Emit/TASKS.md
index 4da81e1b..b0e36c10 100644
--- a/src/StellaOps.Scanner.Emit/TASKS.md
+++ b/src/StellaOps.Scanner.Emit/TASKS.md
@@ -2,11 +2,11 @@
| 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-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-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-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-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-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-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 | 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 | 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 | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-602 | Package artifacts for export + attestation (naming, compression, manifests). | Export pipeline produces deterministic file paths/hashes; integration test with storage passes. |
+| SCANNER-EMIT-10-605 | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-603 | Emit BOM-Index sidecar schema/fixtures (`bom-index@1`) and note CRITICAL PATH for Scheduler. | Schema + fixtures in docs/artifacts/bom-index; tests `BOMIndexGoldenIsStable` green. |
+| SCANNER-EMIT-10-606 | DONE (2025-10-22) | Emit Guild | SCANNER-EMIT-10-605 | Integrate EntryTrace usage flags into BOM-Index; document semantics. | Usage bits present in sidecar; integration tests with EntryTrace fixtures pass. |
| SCANNER-EMIT-17-701 | TODO | Emit Guild, Native Analyzer Guild | SCANNER-EMIT-10-602 | Record GNU build-id for ELF components and surface it in inventory/usage SBOM plus diff payloads with deterministic ordering. | Native analyzer emits buildId for every ELF executable/library, SBOM/diff fixtures updated with canonical `buildId` field, regression tests prove stability, docs call out debug-symbol lookup flow. |
-| SCANNER-EMIT-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. |
diff --git a/src/StellaOps.Scanner.Worker/Diagnostics/TelemetryExtensions.cs b/src/StellaOps.Scanner.Worker/Diagnostics/TelemetryExtensions.cs
index 2b085dd0..122efcaf 100644
--- a/src/StellaOps.Scanner.Worker/Diagnostics/TelemetryExtensions.cs
+++ b/src/StellaOps.Scanner.Worker/Diagnostics/TelemetryExtensions.cs
@@ -61,7 +61,8 @@ public static class TelemetryExtensions
metrics
.AddMeter(
ScannerWorkerInstrumentation.MeterName,
- "StellaOps.Scanner.Analyzers.Lang.Node")
+ "StellaOps.Scanner.Analyzers.Lang.Node",
+ "StellaOps.Scanner.Analyzers.Lang.Go")
.AddRuntimeInstrumentation()
.AddProcessInstrumentation();