135 lines
5.4 KiB
C#
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();
|
|
}
|
|
}
|