Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/DependencyReachabilityReporterTests.cs
2026-01-22 19:08:46 +02:00

135 lines
5.4 KiB
C#

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<string, ReachabilityStatus>(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, string?>
{
[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, DependencyReachabilityAdvisorySummary>
{
[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();
}
}