using System.Collections.Immutable; using FluentAssertions; using StellaOps.Concelier.SbomIntegration.Models; using StellaOps.Scanner.Reachability.Dependencies; using StellaOps.Scanner.Reachability.Dependencies.Reporting; using StellaOps.Scanner.Sarif; using StellaOps.Scanner.Sarif.Fingerprints; using StellaOps.Scanner.Sarif.Rules; using StellaOps.TestKit; using Xunit; using static StellaOps.Scanner.Reachability.Tests.DependencyTestData; using ReachabilityStatus = StellaOps.Scanner.Reachability.Dependencies.ReachabilityStatus; namespace StellaOps.Scanner.Reachability.Tests; public sealed class DependencyReachabilityReporterTests { [Trait("Category", TestCategories.Unit)] [Fact] public async Task BuildReport_EmitsFilteredFindingsAndSarif() { var sbom = BuildSbom( components: [ Component("app", type: "application", purl: "pkg:npm/app@1.0.0"), Component("lib-a", purl: "pkg:npm/lib-a@1.0.0"), Component("lib-b", purl: "pkg:npm/lib-b@1.0.0") ], dependencies: [ Dependency("app", ["lib-a"], DependencyScope.Runtime), Dependency("lib-a", ["lib-b"], DependencyScope.Runtime) ], rootRef: "app"); var policy = new ReachabilityPolicy { Reporting = new ReachabilityReportingPolicy { ShowFilteredVulnerabilities = true, IncludeReachabilityPaths = true } }; var combiner = new ReachGraphReachabilityCombiner(); var reachabilityReport = combiner.Analyze(sbom, callGraph: null, policy); var matchedAt = new DateTimeOffset(2025, 1, 2, 3, 4, 5, TimeSpan.Zero); var matches = new[] { new SbomAdvisoryMatch { Id = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), SbomId = Guid.Empty, SbomDigest = "sha256:deadbeef", CanonicalId = Guid.Parse("11111111-1111-1111-1111-111111111111"), Purl = "pkg:npm/lib-a@1.0.0", Method = MatchMethod.ExactPurl, IsReachable = true, IsDeployed = false, MatchedAt = matchedAt }, new SbomAdvisoryMatch { Id = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), SbomId = Guid.Empty, SbomDigest = "sha256:deadbeef", CanonicalId = Guid.Parse("22222222-2222-2222-2222-222222222222"), Purl = "pkg:npm/lib-b@1.0.0", Method = MatchMethod.ExactPurl, IsReachable = false, IsDeployed = false, MatchedAt = matchedAt } }; var reachabilityMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["pkg:npm/lib-a@1.0.0"] = ReachabilityStatus.Reachable, ["pkg:npm/lib-b@1.0.0"] = ReachabilityStatus.Unreachable }; var severityMap = new Dictionary { [Guid.Parse("11111111-1111-1111-1111-111111111111")] = "high", [Guid.Parse("22222222-2222-2222-2222-222222222222")] = "medium" }; var filter = new VulnerabilityReachabilityFilter(); var filterResult = filter.Apply(matches, reachabilityMap, policy, severityMap); var advisorySummaries = new Dictionary { [Guid.Parse("11111111-1111-1111-1111-111111111111")] = new DependencyReachabilityAdvisorySummary { CanonicalId = Guid.Parse("11111111-1111-1111-1111-111111111111"), VulnerabilityId = "CVE-2025-0001", Severity = "high", Title = "lib-a issue" }, [Guid.Parse("22222222-2222-2222-2222-222222222222")] = new DependencyReachabilityAdvisorySummary { CanonicalId = Guid.Parse("22222222-2222-2222-2222-222222222222"), VulnerabilityId = "CVE-2025-0002", Severity = "medium", Title = "lib-b issue" } }; var ruleRegistry = new SarifRuleRegistry(); var fingerprintGenerator = new FingerprintGenerator(ruleRegistry); var reporter = new DependencyReachabilityReporter(new SarifExportService( ruleRegistry, fingerprintGenerator)); var report = reporter.BuildReport(sbom, reachabilityReport, filterResult, advisorySummaries, policy); report.Vulnerabilities.Should().ContainSingle(); report.FilteredVulnerabilities.Should().ContainSingle(); report.Summary.VulnerabilityStatistics.FilteredVulnerabilities.Should().Be(1); var purlLookup = sbom.Components .Where(component => !string.IsNullOrWhiteSpace(component.BomRef)) .ToDictionary(component => component.BomRef!, component => component.Purl, StringComparer.Ordinal); var dot = reporter.ExportGraphViz( reachabilityReport.Graph, reachabilityReport.ComponentReachability, purlLookup); dot.Should().Contain("digraph"); dot.Should().Contain("\"app\""); var sarif = await reporter.ExportSarifAsync(report, "1.2.3", includeFiltered: true); sarif.Runs.Should().NotBeEmpty(); } }