save checkpoint

This commit is contained in:
master
2026-02-12 21:02:43 +02:00
parent 5bca406787
commit 9911b7d73c
593 changed files with 174390 additions and 1376 deletions

View File

@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.Analyzers.OS;
using StellaOps.Scanner.Analyzers.OS.Dpkg;
using StellaOps.Scanner.Core.Contracts;
using Xunit;
namespace StellaOps.Scanner.Analyzers.OS.Tests.Dpkg;
public sealed class DpkgChangelogBugCorrelationTests
{
[Fact]
public async Task AnalyzeAsync_ExtractsBugReferencesAndCveMappingsFromDpkgChangelog()
{
var rootPath = Path.Combine(Path.GetTempPath(), $"stellaops-dpkg-{Guid.NewGuid():N}");
Directory.CreateDirectory(rootPath);
try
{
WriteFixture(rootPath);
var analyzer = new DpkgPackageAnalyzer(NullLogger<DpkgPackageAnalyzer>.Instance);
var metadata = new Dictionary<string, string> { [ScanMetadataKeys.RootFilesystemPath] = rootPath };
var context = new OSPackageAnalyzerContext(
rootPath,
workspacePath: null,
TimeProvider.System,
NullLoggerFactory.Instance.CreateLogger("dpkg-changelog-tests"),
metadata);
var result = await analyzer.AnalyzeAsync(context, TestContext.Current.CancellationToken);
var package = Assert.Single(result.Packages);
Assert.Equal("debian:#123456,launchpad:#7654321,rhbz:#424242", package.VendorMetadata["changelogBugRefs"]);
Assert.Equal(
"debian:#123456=>CVE-2026-1000;launchpad:#7654321=>CVE-2026-1000;rhbz:#424242=>CVE-2026-1001",
package.VendorMetadata["changelogBugToCves"]);
Assert.Contains("CVE-2026-1000", package.CveHints);
Assert.Contains("CVE-2026-1001", package.CveHints);
}
finally
{
if (Directory.Exists(rootPath))
{
Directory.Delete(rootPath, recursive: true);
}
}
}
private static void WriteFixture(string rootPath)
{
var statusPath = Path.Combine(rootPath, "var", "lib", "dpkg", "status");
Directory.CreateDirectory(Path.GetDirectoryName(statusPath)!);
File.WriteAllText(
statusPath,
"""
Package: bash
Status: install ok installed
Priority: important
Section: shells
Installed-Size: 1024
Maintainer: Debian Developers <debian-devel@lists.debian.org>
Architecture: amd64
Version: 5.2.21-2
Description: GNU Bourne Again SHell
""");
var listPath = Path.Combine(rootPath, "var", "lib", "dpkg", "info", "bash.list");
Directory.CreateDirectory(Path.GetDirectoryName(listPath)!);
File.WriteAllText(
listPath,
"""
/usr/share/doc/bash/changelog.Debian.gz
""");
var changelogPath = Path.Combine(rootPath, "usr", "share", "doc", "bash", "changelog.Debian.gz");
Directory.CreateDirectory(Path.GetDirectoryName(changelogPath)!);
using var fs = File.Create(changelogPath);
using var gzip = new GZipStream(fs, CompressionMode.Compress);
using var writer = new StreamWriter(gzip, Encoding.UTF8);
writer.Write(
"""
bash (5.2.21-2) unstable; urgency=medium
* Backport parser fix (Closes: #123456; LP: #7654321; CVE-2026-1000).
* Hardening update (RHBZ#424242; CVE-2026-1001).
-- Debian Maintainer <maintainer@example.org> Thu, 12 Feb 2026 09:00:00 +0000
""");
}
}

View File

@@ -15,10 +15,11 @@
"release": "8.el9",
"sourcePackage": "openssl-3.2.1-8.el9.src.rpm",
"license": "OpenSSL",
"evidenceSource": "RpmDatabase",
"cveHints": [
"CVE-2025-1234"
],
"evidenceSource": "RpmDatabase",
"cveHints": [
"CVE-2025-1234",
"CVE-2025-9999"
],
"provides": [
"libcrypto.so.3()(64bit)",
"openssl-libs"
@@ -48,10 +49,12 @@
}
}
],
"vendorMetadata": {
"buildTime": null,
"description": null,
"installTime": null,
"vendorMetadata": {
"buildTime": null,
"changelogBugRefs": "debian:#102030,debian:#102031,launchpad:#456789,rhbz:#220001",
"changelogBugToCves": "debian:#102030=\u003ECVE-2025-9999;debian:#102031=\u003ECVE-2025-9999;launchpad:#456789=\u003ECVE-2025-9999;rhbz:#220001=\u003ECVE-2025-1234",
"description": null,
"installTime": null,
"rpm:summary": "TLS toolkit",
"sourceRpm": "openssl-3.2.1-8.el9.src.rpm",
"summary": "TLS toolkit",

View File

@@ -0,0 +1,48 @@
using StellaOps.Scanner.Analyzers.OS.Helpers;
using Xunit;
namespace StellaOps.Scanner.Analyzers.OS.Tests.Helpers;
public sealed class ChangelogBugReferenceExtractorTests
{
[Fact]
public void Extract_MapsDebianRhbzAndLaunchpadReferencesToCves()
{
var result = ChangelogBugReferenceExtractor.Extract(
"Backport parser fix. Closes: #123456, #123457; CVE-2026-0001",
"Fix OpenSSL issue RHBZ#998877 with CVE-2026-0002.",
"Ubuntu patch for LP: #445566 and CVE-2026-0003.");
Assert.Equal(
new[]
{
"debian:#123456",
"debian:#123457",
"launchpad:#445566",
"rhbz:#998877"
},
result.BugReferences);
Assert.Equal(
"debian:#123456=>CVE-2026-0001;debian:#123457=>CVE-2026-0001;launchpad:#445566=>CVE-2026-0003;rhbz:#998877=>CVE-2026-0002",
result.ToBugToCvesMetadataValue());
}
[Fact]
public void Extract_DoesNotEmitMappingsWithoutCveInSameEntry()
{
var result = ChangelogBugReferenceExtractor.Extract(
"Closes: #222222; housekeeping only",
"LP: #777777 without security identifier");
Assert.Equal(
new[]
{
"debian:#222222",
"launchpad:#777777"
},
result.BugReferences);
Assert.Empty(result.BugToCves);
Assert.Equal(string.Empty, result.ToBugToCvesMetadataValue());
}
}

View File

@@ -65,7 +65,11 @@ public sealed class OsAnalyzerDeterminismTests
new RpmFileEntry("/usr/lib64/libcrypto.so.3", false, new Dictionary<string, string> { ["sha256"] = "abc123" }),
new RpmFileEntry("/etc/pki/tls/openssl.cnf", true, new Dictionary<string, string> { ["md5"] = "c0ffee" })
},
changeLogs: new[] { "Resolves: CVE-2025-1234" },
changeLogs: new[]
{
"Resolves: CVE-2025-1234 (RHBZ#220001)",
"Fix startup regression. Closes: #102030, #102031; LP: #456789; CVE-2025-9999"
},
metadata: new Dictionary<string, string?> { ["summary"] = "TLS toolkit" })
};

View File

@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| QA-SCANNER-VERIFY-010 | DONE | Added behavioral coverage for changelog bug-reference extraction and bug-to-CVE mapping evidence in OS analyzer outputs. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.OS.Tests/StellaOps.Scanner.Analyzers.OS.Tests.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -0,0 +1,71 @@
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Scanner.CallGraph.Bun;
using StellaOps.Scanner.Contracts;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Scanner.CallGraph.Tests;
public sealed class BunCallGraphExtractorTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_FromSource_DetectsBunEntrypointsAndSinks()
{
var root = Path.Combine(Path.GetTempPath(), "stella-bun-callgraph-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
var sourcePath = Path.Combine(root, "app.ts");
await File.WriteAllTextAsync(
sourcePath,
"""
import { Elysia } from "elysia";
import { Hono } from "hono";
const app = new Elysia().get("/health", () => "ok");
const hono = new Hono();
hono.get("/v1/ping", (c) => c.text("pong"));
Bun.serve({
fetch(req) {
return new Response("ok");
}
});
const config = Bun.file("/tmp/config.json");
Bun.spawn(["sh", "-c", "echo hello"]);
""");
var extractor = new BunCallGraphExtractor(NullLogger<BunCallGraphExtractor>.Instance);
var snapshot = await extractor.ExtractAsync(new CallGraphExtractionRequest("scan-bun-001", "bun", root));
Assert.Equal("bun", snapshot.Language);
Assert.StartsWith("sha256:", snapshot.GraphDigest, StringComparison.Ordinal);
Assert.Contains(snapshot.Nodes, n => n.IsEntrypoint && n.Symbol == "Bun.serve");
Assert.Contains(snapshot.Nodes, n => n.IsSink && n.SinkCategory == SinkCategory.FileWrite && n.Symbol == "Bun.file");
Assert.Contains(snapshot.Nodes, n => n.IsSink && n.SinkCategory == SinkCategory.CmdExec && n.Symbol == "Bun.spawn");
Assert.NotEmpty(snapshot.EntrypointIds);
Assert.NotEmpty(snapshot.SinkIds);
}
finally
{
if (Directory.Exists(root))
{
Directory.Delete(root, recursive: true);
}
}
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExtractAsync_WithMismatchedLanguage_ThrowsArgumentException()
{
var extractor = new BunCallGraphExtractor(NullLogger<BunCallGraphExtractor>.Instance);
await Assert.ThrowsAsync<ArgumentException>(() =>
extractor.ExtractAsync(new CallGraphExtractionRequest("scan-bun-002", "node", ".")));
}
}

View File

@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| QA-SCANNER-VERIFY-008 | DONE | `SPRINT_20260212_002_Scanner_unchecked_feature_verification_batch1.md`: added/verified binary namespace coverage for entry-trace binary intelligence in run-002 Tier 1/Tier 2 checks (2026-02-12). |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Scanner/__Tests/StellaOps.Scanner.EntryTrace.Tests/StellaOps.Scanner.EntryTrace.Tests.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -93,7 +93,28 @@ public sealed class EntryTraceResultStoreTests
ImmutableArray<EntryTraceEdge>.Empty,
ImmutableArray<EntryTraceDiagnostic>.Empty,
ImmutableArray.Create(plan),
ImmutableArray.Create(terminal));
ImmutableArray.Create(terminal),
new EntryTraceBinaryIntelligence(
ImmutableArray.Create(new EntryTraceBinaryTarget(
"/app/main.py",
"sha256:bin",
"Unknown",
"Raw",
1,
1,
1,
1,
ImmutableArray.Create(new EntryTraceBinaryVulnerability(
"CVE-2024-5678",
"main",
"pkg:generic/demo",
"main",
0.91f,
"High")))),
1,
1,
1,
generatedAt));
var ndjson = EntryTraceNdjsonWriter.Serialize(
graph,
@@ -112,6 +133,9 @@ public sealed class EntryTraceResultStoreTests
Assert.Equal(
EntryTraceGraphSerializer.Serialize(result.Graph),
EntryTraceGraphSerializer.Serialize(stored.Graph));
Assert.NotNull(stored.Graph.BinaryIntelligence);
Assert.Equal(1, stored.Graph.BinaryIntelligence!.TotalTargets);
Assert.Equal(1, stored.Graph.BinaryIntelligence.TotalVulnerableMatches);
Assert.Equal(result.Ndjson.ToArray(), stored.Ndjson.ToArray());
}

View File

@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| QA-SCANNER-VERIFY-008 | DONE | `SPRINT_20260212_002_Scanner_unchecked_feature_verification_batch1.md`: added entry-trace store round-trip coverage for binary intelligence payload; validated in run-002 Tier 2 (2026-02-12). |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Scanner/__Tests/StellaOps.Scanner.Storage.Tests/StellaOps.Scanner.Storage.Tests.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| HOT-002 | DONE | `SPRINT_20260210_001_DOCS_sbom_attestation_hot_lookup_contract.md`: migration coverage for `scanner.artifact_boms` partition/index profile. |

View File

@@ -1,83 +1,166 @@
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Scanner.Storage.ObjectStore;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.TestKit;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class SbomUploadEndpointsTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Upload_validates_cyclonedx_format()
public async Task Upload_accepts_cyclonedx_inline_and_persists_record()
{
// This test validates that CycloneDX format detection works
// Full integration with upload service is tested separately
using var secrets = new TestSurfaceSecretsScope();
await using var factory = await CreateFactoryAsync();
using var client = factory.CreateClient();
var sampleCycloneDx = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"metadata": { "timestamp": "2025-01-15T10:00:00Z" },
"components": []
}
""";
var base64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(sampleCycloneDx));
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"components": [
{
"type": "library",
"name": "left-pad",
"version": "1.3.0",
"purl": "pkg:npm/left-pad@1.3.0",
"licenses": [{ "license": { "id": "MIT" } }]
},
{
"type": "library",
"name": "chalk",
"version": "5.0.0",
"purl": "pkg:npm/chalk@5.0.0"
}
]
}
""";
var request = new SbomUploadRequestDto
{
ArtifactRef = "example.com/app:1.0",
SbomBase64 = base64,
ArtifactRef = "example.com/app:1.0.0",
ArtifactDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Sbom = JsonDocument.Parse(sampleCycloneDx).RootElement.Clone(),
Source = new SbomUploadSourceDto
{
Tool = "syft",
Version = "1.0.0"
Version = "1.0.0",
CiContext = new SbomUploadCiContextDto
{
BuildId = "build-42",
Repository = "example/repo"
}
}
};
// Verify the request is valid and can be serialized
Assert.NotNull(request.ArtifactRef);
Assert.NotEmpty(request.SbomBase64);
Assert.NotNull(request.Source);
Assert.Equal("syft", request.Source.Tool);
var uploadResponse = await client.PostAsJsonAsync("/api/v1/sbom/upload", request);
Assert.Equal(HttpStatusCode.Accepted, uploadResponse.StatusCode);
var payload = await uploadResponse.Content.ReadFromJsonAsync<SbomUploadResponseDto>();
Assert.NotNull(payload);
Assert.False(string.IsNullOrWhiteSpace(payload!.SbomId));
Assert.Equal("cyclonedx", payload.Format);
Assert.Equal("1.6", payload.FormatVersion);
Assert.True(payload.ValidationResult.Valid);
Assert.Equal(2, payload.ValidationResult.ComponentCount);
Assert.Equal(0.85d, payload.ValidationResult.QualityScore);
Assert.StartsWith("sha256:", payload.Digest, StringComparison.Ordinal);
Assert.False(string.IsNullOrWhiteSpace(payload.AnalysisJobId));
var getResponse = await client.GetAsync($"/api/v1/sbom/uploads/{payload.SbomId}");
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
var record = await getResponse.Content.ReadFromJsonAsync<SbomUploadRecordDto>();
Assert.NotNull(record);
Assert.Equal(payload.SbomId, record!.SbomId);
Assert.Equal("example.com/app:1.0.0", record.ArtifactRef);
Assert.Equal("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", record.ArtifactDigest);
Assert.Equal("cyclonedx", record.Format);
Assert.Equal("1.6", record.FormatVersion);
Assert.Equal("build-42", record.Source?.CiContext?.BuildId);
Assert.Equal("example/repo", record.Source?.CiContext?.Repository);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Upload_validates_spdx_format()
public async Task Upload_accepts_spdx_base64_and_tracks_ci_context()
{
// This test validates that SPDX format detection works
using var secrets = new TestSurfaceSecretsScope();
await using var factory = await CreateFactoryAsync();
using var client = factory.CreateClient();
var sampleSpdx = """
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "test-sbom",
"documentNamespace": "https://example.com/test",
"packages": []
}
""";
var base64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(sampleSpdx));
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "test-sbom",
"documentNamespace": "https://example.com/test",
"packages": [
{
"name": "openssl",
"versionInfo": "3.0.0",
"licenseDeclared": "Apache-2.0",
"externalRefs": [
{
"referenceType": "purl",
"referenceLocator": "pkg:generic/openssl@3.0.0"
}
]
}
]
}
""";
var request = new SbomUploadRequestDto
{
ArtifactRef = "example.com/service:2.0",
SbomBase64 = base64
ArtifactRef = "example.com/service:2.0.0",
SbomBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(sampleSpdx)),
Source = new SbomUploadSourceDto
{
Tool = "trivy",
Version = "0.50.0",
CiContext = new SbomUploadCiContextDto
{
BuildId = "build-77",
Repository = "example/service-repo"
}
}
};
// Verify the request is valid
Assert.NotNull(request.ArtifactRef);
Assert.NotEmpty(request.SbomBase64);
var uploadResponse = await client.PostAsJsonAsync("/api/v1/sbom/upload", request);
Assert.Equal(HttpStatusCode.Accepted, uploadResponse.StatusCode);
var payload = await uploadResponse.Content.ReadFromJsonAsync<SbomUploadResponseDto>();
Assert.NotNull(payload);
Assert.Equal("spdx", payload!.Format);
Assert.Equal("2.3", payload.FormatVersion);
Assert.True(payload.ValidationResult.Valid);
Assert.Equal(1, payload.ValidationResult.ComponentCount);
Assert.Equal(1.0d, payload.ValidationResult.QualityScore);
Assert.False(string.IsNullOrWhiteSpace(payload.AnalysisJobId));
Assert.StartsWith("sha256:", payload.Digest, StringComparison.Ordinal);
var getResponse = await client.GetAsync($"/api/v1/sbom/uploads/{payload.SbomId}");
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
var record = await getResponse.Content.ReadFromJsonAsync<SbomUploadRecordDto>();
Assert.NotNull(record);
Assert.Equal("build-77", record!.Source?.CiContext?.BuildId);
Assert.Equal("example/service-repo", record.Source?.CiContext?.Repository);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public async Task Upload_rejects_unknown_format()
{
using var secrets = new TestSurfaceSecretsScope();
@@ -87,7 +170,7 @@ public sealed class SbomUploadEndpointsTests
var invalid = new SbomUploadRequestDto
{
ArtifactRef = "example.com/invalid:1.0",
SbomBase64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{\"name\":\"oops\"}"))
SbomBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"name\":\"oops\"}"))
};
var response = await client.PostAsJsonAsync("/api/v1/sbom/upload", invalid);
@@ -109,38 +192,6 @@ public sealed class SbomUploadEndpointsTests
return factory;
}
private static string LoadFixtureBase64(string fileName)
{
var repoRoot = ResolveRepoRoot();
var path = Path.Combine(
repoRoot,
"src",
"AirGap",
"__Tests",
"StellaOps.AirGap.Importer.Tests",
"Reconciliation",
"Fixtures",
fileName);
Assert.True(File.Exists(path), $"Fixture not found at {path}.");
var bytes = File.ReadAllBytes(path);
return Convert.ToBase64String(bytes);
}
private static string ResolveRepoRoot()
{
var baseDirectory = AppContext.BaseDirectory;
return Path.GetFullPath(Path.Combine(
baseDirectory,
"..",
"..",
"..",
"..",
"..",
"..",
".."));
}
private sealed class InMemoryArtifactObjectStore : IArtifactObjectStore
{
private readonly ConcurrentDictionary<string, byte[]> _objects = new(StringComparer.Ordinal);

View File

@@ -153,7 +153,28 @@ public sealed partial class ScansEndpointsTests
ImmutableArray<EntryTraceEdge>.Empty,
ImmutableArray<EntryTraceDiagnostic>.Empty,
ImmutableArray.Create(plan),
ImmutableArray.Create(terminal));
ImmutableArray.Create(terminal),
new EntryTraceBinaryIntelligence(
ImmutableArray.Create(new EntryTraceBinaryTarget(
"/usr/local/bin/app",
"sha256:abc",
"X64",
"ELF",
3,
2,
1,
1,
ImmutableArray.Create(new EntryTraceBinaryVulnerability(
"CVE-2024-1234",
"SSL_read",
"pkg:generic/openssl",
"SSL_read",
0.98f,
"Critical")))),
1,
1,
1,
generatedAt));
var ndjson = EntryTraceNdjsonWriter.Serialize(graph, new EntryTraceNdjsonMetadata(scanId, "sha256:test", generatedAt));
var storedResult = new EntryTraceResult(scanId, "sha256:test", generatedAt, graph, ndjson);
@@ -173,6 +194,8 @@ public sealed partial class ScansEndpointsTests
Assert.Equal(storedResult.ScanId, payload!.ScanId);
Assert.Equal(storedResult.ImageDigest, payload.ImageDigest);
Assert.Equal(storedResult.Graph.Plans.Length, payload.Graph.Plans.Length);
Assert.NotNull(payload.Graph.BinaryIntelligence);
Assert.Equal(1, payload.Graph.BinaryIntelligence!.TotalVulnerableMatches);
}
[Trait("Category", TestCategories.Unit)]

View File

@@ -4,6 +4,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| QA-SCANNER-VERIFY-008 | DONE | `SPRINT_20260212_002_Scanner_unchecked_feature_verification_batch1.md`: extended entry-trace endpoint contract test assertions for `graph.binaryIntelligence`; verified in run-002 Tier 2 (2026-02-12). |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/StellaOps.Scanner.WebService.Tests.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT-20260208-062-VEXREACH-001 | DONE | Added deterministic unit coverage for VEX+reachability filter matrix and controller endpoint (`6` tests passed on filtered run, 2026-02-08). |

View File

@@ -0,0 +1,197 @@
using System.Collections.Immutable;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.BinaryIndex.Core.Models;
using StellaOps.BinaryIndex.Core.Services;
using StellaOps.Scanner.Analyzers.Native.Index;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.PatchVerification;
using StellaOps.Scanner.PatchVerification.Models;
using StellaOps.Scanner.Worker.Extensions;
using StellaOps.Scanner.Worker.Processing;
using StellaOps.TestKit;
using Xunit;
using BinaryFormat = StellaOps.BinaryIndex.Core.Models.BinaryFormat;
namespace StellaOps.Scanner.Worker.Tests;
public sealed class BinaryLookupStageExecutorTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_WiresBuildIdLookupPatchVerificationAndMappedFindings()
{
const string scanId = "11111111-1111-1111-1111-111111111111";
var identity = new BinaryIdentity
{
BinaryKey = "gnu-build-id:abc123:sha256:deadbeef",
BuildId = "gnu-build-id:abc123",
BuildIdType = "gnu-build-id",
FileSha256 = "sha256:deadbeef",
Format = BinaryFormat.Elf,
Architecture = "x86_64"
};
var vulnMatch = new BinaryVulnMatch
{
CveId = "CVE-2026-1234",
VulnerablePurl = "pkg:deb/debian/libssl@1.1.1",
Method = MatchMethod.BuildIdCatalog,
Confidence = 0.97m,
Evidence = new MatchEvidence { BuildId = "gnu-build-id:abc123" }
};
var vulnService = new Mock<IBinaryVulnerabilityService>(MockBehavior.Strict);
vulnService
.Setup(service => service.LookupBatchAsync(
It.IsAny<IEnumerable<BinaryIdentity>>(),
It.IsAny<LookupOptions?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(ImmutableDictionary<string, ImmutableArray<BinaryVulnMatch>>.Empty.Add(identity.BinaryKey, [vulnMatch]));
vulnService
.Setup(service => service.GetFixStatusBatchAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<IEnumerable<string>>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(ImmutableDictionary<string, FixStatusResult>.Empty);
var extractor = new Mock<IBinaryFeatureExtractor>(MockBehavior.Strict);
extractor
.Setup(e => e.ExtractIdentityAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(identity);
var buildIdIndex = new Mock<IBuildIdIndex>(MockBehavior.Strict);
buildIdIndex.SetupGet(index => index.IsLoaded).Returns(false);
buildIdIndex.SetupGet(index => index.Count).Returns(1);
buildIdIndex
.Setup(index => index.LoadAsync(It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
buildIdIndex
.Setup(index => index.BatchLookupAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(
[
new BuildIdLookupResult(
"gnu-build-id:abc123",
"pkg:deb/debian/libssl@1.1.1",
"1.1.1",
"debian",
BuildIdConfidence.Exact,
DateTimeOffset.UtcNow)
]);
buildIdIndex
.Setup(index => index.LookupAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((BuildIdLookupResult?)null);
var patchVerification = new Mock<IPatchVerificationOrchestrator>(MockBehavior.Strict);
patchVerification
.Setup(orchestrator => orchestrator.VerifyAsync(
It.IsAny<PatchVerificationContext>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new PatchVerificationResult
{
ScanId = scanId,
Evidence = [],
PatchedCves = ImmutableHashSet<string>.Empty,
UnpatchedCves = ImmutableHashSet<string>.Empty,
InconclusiveCves = ImmutableHashSet<string>.Empty,
NoPatchDataCves = ImmutableHashSet<string>.Empty,
VerifiedAt = DateTimeOffset.UtcNow,
VerifierVersion = PatchVerificationOrchestrator.VerifierVersion
});
await using var scopedProvider = new ServiceCollection()
.AddSingleton(patchVerification.Object)
.BuildServiceProvider();
var analyzer = new BinaryVulnerabilityAnalyzer(
vulnService.Object,
extractor.Object,
NullLogger<BinaryVulnerabilityAnalyzer>.Instance);
var findingMapper = new BinaryFindingMapper(
vulnService.Object,
NullLogger<BinaryFindingMapper>.Instance);
var stage = new BinaryLookupStageExecutor(
analyzer,
findingMapper,
buildIdIndex.Object,
scopedProvider.GetRequiredService<IServiceScopeFactory>(),
new BinaryIndexOptions { Enabled = true },
NullLogger<BinaryLookupStageExecutor>.Instance);
var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
{
[ScanMetadataKeys.RootFilesystemPath] = "/tmp/rootfs"
};
var lease = new TestLease(metadata, "job-1", scanId);
var context = new ScanJobContext(lease, TimeProvider.System, TimeProvider.System.GetUtcNow(), TestContext.Current.CancellationToken);
context.Analysis.Set("layers", new List<LayerInfo>
{
new() { Digest = "sha256:layer1", MediaType = "application/vnd.oci.image.layer.v1.tar", Size = 123 }
});
context.Analysis.Set("binary_paths_sha256:layer1", new List<string> { "/usr/lib/libssl.so" });
context.Analysis.Set("detected_distro", "debian");
context.Analysis.Set("detected_release", "12");
context.Analysis.Set<Func<string, string, Stream?>>(
"layer_file_opener",
(_, _) => new MemoryStream([0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01]));
await stage.ExecuteAsync(context, TestContext.Current.CancellationToken);
var rawFindings = context.Analysis.GetBinaryFindings();
Assert.Single(rawFindings);
Assert.Equal("CVE-2026-1234", rawFindings[0].CveId);
Assert.True(context.Analysis.TryGet<IReadOnlyList<object>>(ScanAnalysisKeys.BinaryVulnerabilityFindings, out var mapped));
Assert.Single(mapped);
Assert.True(context.Analysis.TryGet<IReadOnlyDictionary<string, BuildIdLookupResult>>(ScanAnalysisKeys.BinaryBuildIdMappings, out var mappings));
Assert.True(mappings.ContainsKey("gnu-build-id:abc123"));
Assert.True(context.Analysis.TryGet<PatchVerificationResult>(ScanAnalysisKeys.BinaryPatchVerificationResult, out var patchResult));
Assert.Equal(scanId, patchResult.ScanId);
buildIdIndex.Verify(index => index.LoadAsync(It.IsAny<CancellationToken>()), Times.Once);
buildIdIndex.Verify(index => index.BatchLookupAsync(
It.Is<IEnumerable<string>>(ids => ids.Contains("gnu-build-id:abc123")),
It.IsAny<CancellationToken>()), Times.Once);
patchVerification.Verify(orchestrator => orchestrator.VerifyAsync(
It.Is<PatchVerificationContext>(ctx => ctx.CveIds.Contains("CVE-2026-1234")),
It.IsAny<CancellationToken>()), Times.Once);
}
private sealed class TestLease : IScanJobLease
{
public TestLease(IReadOnlyDictionary<string, string> metadata, string jobId, string scanId)
{
Metadata = metadata;
JobId = jobId;
ScanId = scanId;
Attempt = 1;
EnqueuedAtUtc = DateTimeOffset.UtcNow;
LeasedAtUtc = DateTimeOffset.UtcNow;
LeaseDuration = TimeSpan.FromMinutes(1);
}
public string JobId { get; }
public string ScanId { get; }
public int Attempt { get; }
public DateTimeOffset EnqueuedAtUtc { get; }
public DateTimeOffset LeasedAtUtc { get; }
public TimeSpan LeaseDuration { get; }
public IReadOnlyDictionary<string, string> Metadata { get; }
public ValueTask RenewAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask CompleteAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask AbandonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask PoisonAsync(string reason, CancellationToken cancellationToken) => ValueTask.CompletedTask;
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
}

View File

@@ -92,6 +92,58 @@ public sealed class EntryTraceExecutionServiceTests : IDisposable
Assert.Equal(ndjsonPayload, store.LastResult.Ndjson);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ExecuteAsync_AddsBinaryIntelligence_ForNativeTerminal()
{
var metadata = CreateMetadata("PATH=/bin:/usr/bin");
var rootDirectory = metadata[ScanMetadataKeys.RootFilesystemPath];
var binaryPath = Path.Combine(rootDirectory, "usr", "bin", "app");
Directory.CreateDirectory(Path.GetDirectoryName(binaryPath)!);
File.WriteAllBytes(binaryPath, CreateElfPayloadWithMarker("CVE-2024-9999"));
var graph = new EntryTraceGraph(
EntryTraceOutcome.Resolved,
ImmutableArray<EntryTraceNode>.Empty,
ImmutableArray<EntryTraceEdge>.Empty,
ImmutableArray<EntryTraceDiagnostic>.Empty,
ImmutableArray.Create(new EntryTracePlan(
ImmutableArray.Create("/usr/bin/app"),
ImmutableDictionary<string, string>.Empty,
"/workspace",
"scanner",
"/usr/bin/app",
EntryTraceTerminalType.Native,
null,
0.9,
ImmutableDictionary<string, string>.Empty)),
ImmutableArray.Create(new EntryTraceTerminal(
"/usr/bin/app",
EntryTraceTerminalType.Native,
null,
0.9,
ImmutableDictionary<string, string>.Empty,
"scanner",
"/workspace",
ImmutableArray<string>.Empty)));
var analyzer = new CapturingEntryTraceAnalyzer(graph);
var store = new CapturingEntryTraceResultStore();
var service = CreateService(analyzer, store);
await service.ExecuteAsync(CreateContext(metadata), TestContext.Current.CancellationToken);
Assert.True(store.Stored);
Assert.NotNull(store.LastResult);
Assert.NotNull(store.LastResult!.Graph.BinaryIntelligence);
Assert.Equal(1, store.LastResult.Graph.BinaryIntelligence!.TotalTargets);
Assert.Equal(1, store.LastResult.Graph.BinaryIntelligence.AnalyzedTargets);
Assert.Single(store.LastResult.Graph.BinaryIntelligence.Targets);
Assert.Contains(
store.LastResult.Graph.BinaryIntelligence.Targets[0].VulnerableMatches,
match => string.Equals(match.VulnerabilityId, "CVE-2024-9999", StringComparison.Ordinal));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
@@ -273,19 +325,24 @@ public sealed class EntryTraceExecutionServiceTests : IDisposable
private sealed class CapturingEntryTraceAnalyzer : IEntryTraceAnalyzer
{
public CapturingEntryTraceAnalyzer(EntryTraceGraph? graph = null)
{
Graph = graph ?? new EntryTraceGraph(
EntryTraceOutcome.Resolved,
ImmutableArray<EntryTraceNode>.Empty,
ImmutableArray<EntryTraceEdge>.Empty,
ImmutableArray<EntryTraceDiagnostic>.Empty,
ImmutableArray<EntryTracePlan>.Empty,
ImmutableArray<EntryTraceTerminal>.Empty);
}
public bool Invoked { get; private set; }
public EntrypointSpecification? LastEntrypoint { get; private set; }
public EntryTraceContext? LastContext { get; private set; }
public EntryTraceGraph Graph { get; } = new(
EntryTraceOutcome.Resolved,
ImmutableArray<EntryTraceNode>.Empty,
ImmutableArray<EntryTraceEdge>.Empty,
ImmutableArray<EntryTraceDiagnostic>.Empty,
ImmutableArray<EntryTracePlan>.Empty,
ImmutableArray<EntryTraceTerminal>.Empty);
public EntryTraceGraph Graph { get; }
public ValueTask<EntryTraceGraph> ResolveAsync(EntrypointSpecification entrypoint, EntryTraceContext context, CancellationToken cancellationToken = default)
{
@@ -303,6 +360,26 @@ public sealed class EntryTraceExecutionServiceTests : IDisposable
}
}
private static byte[] CreateElfPayloadWithMarker(string marker)
{
var prefix = new byte[]
{
0x7F, 0x45, 0x4C, 0x46, // ELF
0x02, 0x01, 0x01, 0x00, // 64-bit, little-endian
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, // e_type
0x3E, 0x00, // x64 machine
0x01, 0x00, 0x00, 0x00
};
var markerBytes = Encoding.ASCII.GetBytes($"SSL_read\0{marker}\0");
var payload = new byte[512];
Buffer.BlockCopy(prefix, 0, payload, 0, prefix.Length);
Buffer.BlockCopy(markerBytes, 0, payload, 128, markerBytes.Length);
return payload;
}
private sealed class CapturingEntryTraceResultStore : IEntryTraceResultStore
{
public bool Stored { get; private set; }

View File

@@ -4,6 +4,8 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| QA-SCANNER-VERIFY-009 | DONE | `SPRINT_20260212_002_Scanner_unchecked_feature_verification_batch1.md`: added deterministic `BinaryLookupStageExecutorTests` coverage for runtime patch verification, Build-ID mapping, and unified finding publication wiring (run-002, 2026-02-12). |
| QA-SCANNER-VERIFY-008 | DONE | `SPRINT_20260212_002_Scanner_unchecked_feature_verification_batch1.md`: added worker entry-trace execution coverage for binary intelligence graph enrichment and validated run-002 pass (2026-02-12). |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Scanner/__Tests/StellaOps.Scanner.Worker.Tests/StellaOps.Scanner.Worker.Tests.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT-20260208-060-IDEMP-001 | DONE | Implement idempotent verdict attestation submission (idempotency key + dedupe + retry classification + tests). |