feat: Enhance SBOM composition with policy findings and update CycloneDX package
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added `PolicyFindings` property to `SbomCompositionRequest` to include policy findings in SBOM. - Implemented `NormalizePolicyFindings` method to process and validate policy findings. - Updated `SbomCompositionRequest.Create` method to accept policy findings as an argument. - Upgraded CycloneDX.Core package from version 5.1.0 to 10.0.1. - Marked several tasks as DONE in TASKS.md, reflecting completion of SBOM-related features. - Introduced telemetry metrics for Go analyzer to track heuristic fallbacks. - Added performance benchmarks for .NET and Go analyzers. - Created new test fixtures for .NET applications, including dependencies and runtime configurations. - Added licenses and nuspec files for logging and toolkit packages used in tests. - Implemented `SbomPolicyFinding` record to encapsulate policy finding details and normalization logic.
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<long>((_, measurement, _, _) =>
|
||||
{
|
||||
Interlocked.Add(ref total, measurement);
|
||||
});
|
||||
|
||||
listener.Start();
|
||||
|
||||
await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
listener.Dispose();
|
||||
|
||||
Assert.Equal(1, Interlocked.Read(ref total));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<byte>` 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.
|
||||
|
||||
@@ -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<LanguageComponentEvidence> BuildEvidence(GoBuildInfo buildInfo, GoModule module, string binaryRelativePath, LanguageAnalyzerContext context, ref string? binaryHash)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
||||
|
||||
internal static class GoAnalyzerMetrics
|
||||
{
|
||||
private static readonly Meter Meter = new("StellaOps.Scanner.Analyzers.Lang.Go", "1.0.0");
|
||||
|
||||
private static readonly Counter<long> HeuristicCounter = Meter.CreateCounter<long>(
|
||||
"scanner_analyzer_golang_heuristic_total",
|
||||
unit: "components",
|
||||
description: "Counts Go components emitted via heuristic fallbacks when build metadata is missing.");
|
||||
|
||||
public static void RecordHeuristic(GoStrippedBinaryIndicator indicator, bool hasVersionHint)
|
||||
{
|
||||
HeuristicCounter.Add(
|
||||
1,
|
||||
new KeyValuePair<string, object?>("indicator", NormalizeIndicator(indicator)),
|
||||
new KeyValuePair<string, object?>("version_hint", hasVersionHint ? "present" : "absent"));
|
||||
}
|
||||
|
||||
private static string NormalizeIndicator(GoStrippedBinaryIndicator indicator)
|
||||
=> indicator switch
|
||||
{
|
||||
GoStrippedBinaryIndicator.BuildId => "build-id",
|
||||
GoStrippedBinaryIndicator.GoRuntimeMarkers => "runtime-markers",
|
||||
_ => "unknown",
|
||||
};
|
||||
}
|
||||
@@ -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<byte>(data);
|
||||
var length = info.Length;
|
||||
if (length <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var inspectLength = (int)Math.Min(length, int.MaxValue);
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(inspectLength);
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var totalRead = 0;
|
||||
|
||||
while (totalRead < inspectLength)
|
||||
{
|
||||
var read = stream.Read(buffer, totalRead, inspectLength - totalRead);
|
||||
if (read <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
totalRead += read;
|
||||
}
|
||||
|
||||
if (totalRead < 64)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var span = new ReadOnlySpan<byte>(buffer, 0, totalRead);
|
||||
var offset = span.IndexOf(BuildInfoMagic.Span);
|
||||
if (offset < 0)
|
||||
{
|
||||
@@ -65,6 +108,11 @@ internal static class GoBinaryScanner
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Array.Clear(buffer, 0, inspectLength);
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryClassifyStrippedBinary(string filePath, out GoStrippedBinaryClassification classification)
|
||||
|
||||
@@ -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<byte> 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<byte>.Shared.Rent(readLength);
|
||||
var bytesRead = 0;
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
bytesRead = stream.Read(buffer, 0, readLength);
|
||||
if (bytesRead <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var data = new ReadOnlySpan<byte>(buffer, 0, bytesRead);
|
||||
|
||||
var revision = ExtractValue(data, VcsRevisionToken);
|
||||
var modifiedText = ExtractValue(data, VcsModifiedToken);
|
||||
var timestamp = ExtractValue(data, VcsTimeToken);
|
||||
var system = ExtractValue(data, VcsSystemToken);
|
||||
|
||||
bool? modified = null;
|
||||
if (!string.IsNullOrWhiteSpace(modifiedText))
|
||||
{
|
||||
if (bool.TryParse(modifiedText, out var parsed))
|
||||
{
|
||||
modified = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(revision) && string.IsNullOrWhiteSpace(system) && modified is null && string.IsNullOrWhiteSpace(timestamp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
metadata = new GoDwarfMetadata(system, revision, modified, timestamp);
|
||||
return true;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Array.Clear(buffer, 0, bytesRead);
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ExtractValue(ReadOnlySpan<byte> data, ReadOnlySpan<byte> token)
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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.
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>StellaOps.Logging</id>
|
||||
<version>2.5.1</version>
|
||||
<authors>StellaOps</authors>
|
||||
<description>Logging sample package for analyzer fixtures.</description>
|
||||
<license type="expression">Apache-2.0</license>
|
||||
<licenseUrl>https://stella-ops.example/licenses/logging</licenseUrl>
|
||||
<projectUrl>https://stella-ops.example/projects/logging</projectUrl>
|
||||
</metadata>
|
||||
</package>
|
||||
@@ -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.
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>StellaOps.Toolkit</id>
|
||||
<version>1.2.3</version>
|
||||
<authors>StellaOps</authors>
|
||||
<description>Toolkit sample package for analyzer fixtures.</description>
|
||||
<license type="file">LICENSE.txt</license>
|
||||
<licenseUrl>https://stella-ops.example/licenses/toolkit</licenseUrl>
|
||||
</metadata>
|
||||
</package>
|
||||
@@ -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.
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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<string, string>
|
||||
{
|
||||
["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<string, string>
|
||||
{
|
||||
["stellaops:scanId"] = "scan-1234",
|
||||
},
|
||||
policyFindings: new[]
|
||||
{
|
||||
new SbomPolicyFinding
|
||||
{
|
||||
FindingId = "finding-a",
|
||||
ComponentKey = "pkg:npm/a",
|
||||
VulnerabilityId = "CVE-2025-0001",
|
||||
Status = "Blocked",
|
||||
Score = 40.5,
|
||||
ConfigVersion = "1.0",
|
||||
Quiet = true,
|
||||
QuietedBy = "policy/quiet-critical-runtime",
|
||||
UnknownConfidence = 0.42,
|
||||
ConfidenceBand = "medium",
|
||||
UnknownAgeDays = 5,
|
||||
SourceTrust = "NVD",
|
||||
Reachability = "runtime",
|
||||
Inputs = ImmutableArray.Create(
|
||||
new KeyValuePair<string, double>("severityWeight", 90),
|
||||
new KeyValuePair<string, double>("trustWeight", 1.0),
|
||||
new KeyValuePair<string, double>("reachabilityWeight", 0.45))
|
||||
},
|
||||
new SbomPolicyFinding
|
||||
{
|
||||
FindingId = "finding-b",
|
||||
ComponentKey = "pkg:npm/b",
|
||||
VulnerabilityId = "CVE-2025-0002",
|
||||
Status = "Warned",
|
||||
Score = 12.5,
|
||||
ConfigVersion = "1.0",
|
||||
Quiet = false,
|
||||
SourceTrust = "StellaOps",
|
||||
Reachability = "indirect",
|
||||
Inputs = ImmutableArray.Create(
|
||||
new KeyValuePair<string, double>("severityWeight", 55),
|
||||
new KeyValuePair<string, double>("trustWeight", 0.85))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void ValidateJson(byte[] data, int expectedComponentCount, string expectedView)
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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<string, string>? metadata = null)
|
||||
|
||||
@@ -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<AggregatedComponent> 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<AggregatedComponent> 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<Tool>
|
||||
{
|
||||
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<Property>();
|
||||
}
|
||||
|
||||
metadata.Properties.Add(new Property
|
||||
{
|
||||
Name = "stellaops:sbom.view",
|
||||
Value = view.ToString().ToLowerInvariant(),
|
||||
if (metadata.Properties is null)
|
||||
{
|
||||
metadata.Properties = new List<Property>();
|
||||
}
|
||||
|
||||
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<Vulnerability>? BuildVulnerabilities(
|
||||
SbomCompositionRequest request,
|
||||
ComponentGraph graph,
|
||||
ImmutableArray<AggregatedComponent> viewComponents)
|
||||
{
|
||||
if (request.PolicyFindings.IsDefaultOrEmpty || request.PolicyFindings.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (viewComponents.IsDefaultOrEmpty || viewComponents.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var componentKeys = viewComponents
|
||||
.Select(static component => component.Identity.Key)
|
||||
.ToImmutableHashSet(StringComparer.Ordinal);
|
||||
|
||||
if (componentKeys.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var vulnerabilities = new List<Vulnerability>(request.PolicyFindings.Length);
|
||||
foreach (var finding in request.PolicyFindings)
|
||||
{
|
||||
if (!graph.ComponentMap.TryGetValue(finding.ComponentKey, out var component))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!componentKeys.Contains(component.Identity.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var ratings = BuildRatings(finding.Score);
|
||||
var properties = BuildVulnerabilityProperties(finding);
|
||||
|
||||
var vulnerability = new Vulnerability
|
||||
{
|
||||
BomRef = finding.FindingId,
|
||||
Id = finding.VulnerabilityId ?? finding.FindingId,
|
||||
Source = new Source { Name = "StellaOps.Policy" },
|
||||
Affects = new List<Affects>
|
||||
{
|
||||
new() { Ref = component.Identity.Key }
|
||||
},
|
||||
Ratings = ratings,
|
||||
Properties = properties,
|
||||
};
|
||||
|
||||
vulnerabilities.Add(vulnerability);
|
||||
}
|
||||
|
||||
return vulnerabilities.Count == 0 ? null : vulnerabilities;
|
||||
}
|
||||
|
||||
private static List<Rating>? BuildRatings(double score)
|
||||
{
|
||||
if (double.IsNaN(score) || double.IsInfinity(score))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new List<Rating>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Method = ScoreMethod.Other,
|
||||
Justification = "StellaOps Policy score",
|
||||
Score = score,
|
||||
Severity = Severity.Unknown,
|
||||
Source = new Source { Name = "StellaOps.Policy" },
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static List<Property>? BuildVulnerabilityProperties(SbomPolicyFinding finding)
|
||||
{
|
||||
var properties = new List<Property>();
|
||||
|
||||
AddStringProperty(properties, "stellaops:policy.status", finding.Status);
|
||||
AddStringProperty(properties, "stellaops:policy.configVersion", finding.ConfigVersion);
|
||||
AddBooleanProperty(properties, "stellaops:policy.quiet", finding.Quiet);
|
||||
AddStringProperty(properties, "stellaops:policy.quietedBy", finding.QuietedBy);
|
||||
AddStringProperty(properties, "stellaops:policy.confidenceBand", finding.ConfidenceBand);
|
||||
AddStringProperty(properties, "stellaops:policy.sourceTrust", finding.SourceTrust);
|
||||
AddStringProperty(properties, "stellaops:policy.reachability", finding.Reachability);
|
||||
AddDoubleProperty(properties, "stellaops:policy.score", finding.Score);
|
||||
AddNullableDoubleProperty(properties, "stellaops:policy.unknownConfidence", finding.UnknownConfidence);
|
||||
AddNullableDoubleProperty(properties, "stellaops:policy.unknownAgeDays", finding.UnknownAgeDays);
|
||||
|
||||
if (!finding.Inputs.IsDefaultOrEmpty && finding.Inputs.Length > 0)
|
||||
{
|
||||
foreach (var (key, value) in finding.Inputs)
|
||||
{
|
||||
AddDoubleProperty(properties, $"stellaops:policy.input.{key}", value);
|
||||
}
|
||||
}
|
||||
|
||||
if (properties.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
properties.Sort(static (left, right) => StringComparer.Ordinal.Compare(left.Name, right.Name));
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static void AddStringProperty(ICollection<Property> properties, string name, string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
properties.Add(new Property
|
||||
{
|
||||
Name = name,
|
||||
Value = value.Trim(),
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddBooleanProperty(ICollection<Property> properties, string name, bool value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
properties.Add(new Property
|
||||
{
|
||||
Name = name,
|
||||
Value = value ? "true" : "false",
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddDoubleProperty(ICollection<Property> properties, string name, double value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name) || double.IsNaN(value) || double.IsInfinity(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
properties.Add(new Property
|
||||
{
|
||||
Name = name,
|
||||
Value = FormatDouble(value),
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddNullableDoubleProperty(ICollection<Property> properties, string name, double? value)
|
||||
{
|
||||
if (!value.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddDoubleProperty(properties, name, value.Value);
|
||||
}
|
||||
|
||||
private static Component.Classification MapClassification(string? type)
|
||||
{
|
||||
@@ -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);
|
||||
|
||||
@@ -39,21 +39,25 @@ public sealed record SbomCompositionRequest
|
||||
public string? GeneratorVersion { get; init; }
|
||||
= null;
|
||||
|
||||
public IReadOnlyDictionary<string, string>? AdditionalProperties { get; init; }
|
||||
= null;
|
||||
|
||||
public static SbomCompositionRequest Create(
|
||||
ImageArtifactDescriptor image,
|
||||
IEnumerable<LayerComponentFragment> fragments,
|
||||
DateTimeOffset generatedAt,
|
||||
string? generatorName = null,
|
||||
string? generatorVersion = null,
|
||||
IReadOnlyDictionary<string, string>? properties = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(image);
|
||||
ArgumentNullException.ThrowIfNull(fragments);
|
||||
|
||||
var normalizedImage = new ImageArtifactDescriptor
|
||||
public IReadOnlyDictionary<string, string>? AdditionalProperties { get; init; }
|
||||
= null;
|
||||
|
||||
public ImmutableArray<SbomPolicyFinding> PolicyFindings { get; init; }
|
||||
= ImmutableArray<SbomPolicyFinding>.Empty;
|
||||
|
||||
public static SbomCompositionRequest Create(
|
||||
ImageArtifactDescriptor image,
|
||||
IEnumerable<LayerComponentFragment> fragments,
|
||||
DateTimeOffset generatedAt,
|
||||
string? generatorName = null,
|
||||
string? generatorVersion = null,
|
||||
IReadOnlyDictionary<string, string>? properties = null,
|
||||
IEnumerable<SbomPolicyFinding>? 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<SbomPolicyFinding> NormalizePolicyFindings(IEnumerable<SbomPolicyFinding>? policyFindings)
|
||||
{
|
||||
if (policyFindings is null)
|
||||
{
|
||||
return ImmutableArray<SbomPolicyFinding>.Empty;
|
||||
}
|
||||
|
||||
var builder = ImmutableArray.CreateBuilder<SbomPolicyFinding>();
|
||||
foreach (var finding in policyFindings)
|
||||
{
|
||||
if (finding is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
SbomPolicyFinding normalized;
|
||||
try
|
||||
{
|
||||
normalized = finding.Normalize();
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(normalized.FindingId) || string.IsNullOrWhiteSpace(normalized.ComponentKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Add(normalized);
|
||||
}
|
||||
|
||||
if (builder.Count == 0)
|
||||
{
|
||||
return ImmutableArray<SbomPolicyFinding>.Empty;
|
||||
}
|
||||
|
||||
return builder
|
||||
.ToImmutable()
|
||||
.OrderBy(static finding => finding.FindingId, StringComparer.Ordinal)
|
||||
.ThenBy(static finding => finding.ComponentKey, StringComparer.Ordinal)
|
||||
.ThenBy(static finding => finding.VulnerabilityId ?? string.Empty, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
65
src/StellaOps.Scanner.Emit/Composition/SbomPolicyFinding.cs
Normal file
65
src/StellaOps.Scanner.Emit/Composition/SbomPolicyFinding.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Scanner.Emit.Composition;
|
||||
|
||||
public sealed record SbomPolicyFinding
|
||||
{
|
||||
public required string FindingId { get; init; }
|
||||
|
||||
public required string ComponentKey { get; init; }
|
||||
|
||||
public string? VulnerabilityId { get; init; }
|
||||
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
public double Score { get; init; }
|
||||
|
||||
public string ConfigVersion { get; init; } = string.Empty;
|
||||
|
||||
public ImmutableArray<KeyValuePair<string, double>> Inputs { get; init; } = ImmutableArray<KeyValuePair<string, double>>.Empty;
|
||||
|
||||
public string? QuietedBy { get; init; }
|
||||
|
||||
public bool Quiet { get; init; }
|
||||
|
||||
public double? UnknownConfidence { get; init; }
|
||||
|
||||
public string? ConfidenceBand { get; init; }
|
||||
|
||||
public double? UnknownAgeDays { get; init; }
|
||||
|
||||
public string? SourceTrust { get; init; }
|
||||
|
||||
public string? Reachability { get; init; }
|
||||
|
||||
internal SbomPolicyFinding Normalize()
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(FindingId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(ComponentKey);
|
||||
|
||||
var normalizedInputs = Inputs.IsDefaultOrEmpty
|
||||
? ImmutableArray<KeyValuePair<string, double>>.Empty
|
||||
: Inputs
|
||||
.Where(static pair => !string.IsNullOrWhiteSpace(pair.Key))
|
||||
.Select(static pair => new KeyValuePair<string, double>(pair.Key.Trim(), pair.Value))
|
||||
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
|
||||
.ToImmutableArray();
|
||||
|
||||
return this with
|
||||
{
|
||||
FindingId = FindingId.Trim(),
|
||||
ComponentKey = ComponentKey.Trim(),
|
||||
VulnerabilityId = string.IsNullOrWhiteSpace(VulnerabilityId) ? null : VulnerabilityId.Trim(),
|
||||
Status = string.IsNullOrWhiteSpace(Status) ? string.Empty : Status.Trim(),
|
||||
ConfigVersion = string.IsNullOrWhiteSpace(ConfigVersion) ? string.Empty : ConfigVersion.Trim(),
|
||||
QuietedBy = string.IsNullOrWhiteSpace(QuietedBy) ? null : QuietedBy.Trim(),
|
||||
ConfidenceBand = string.IsNullOrWhiteSpace(ConfidenceBand) ? null : ConfidenceBand.Trim(),
|
||||
SourceTrust = string.IsNullOrWhiteSpace(SourceTrust) ? null : SourceTrust.Trim(),
|
||||
Reachability = string.IsNullOrWhiteSpace(Reachability) ? null : Reachability.Trim(),
|
||||
Inputs = normalizedInputs
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CycloneDX.Core" Version="5.1.0" />
|
||||
<PackageReference Include="CycloneDX.Core" Version="10.0.1" />
|
||||
<PackageReference Include="RoaringBitmap" Version="0.0.9" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user