save progress
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
// Licensed to StellaOps under the AGPL-3.0-or-later license.
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.VexLens.Delta;
|
||||
using StellaOps.VexLens.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.VexLens.Tests.Delta;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="DeltaReportBuilder"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class DeltaReportBuilderTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
|
||||
public DeltaReportBuilderTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 4, 12, 0, 0, TimeSpan.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_EmptyReport_ShouldHaveZeroCounts()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to");
|
||||
|
||||
// Act
|
||||
var report = builder.Build();
|
||||
|
||||
// Assert
|
||||
report.Summary.TotalCount.Should().Be(0);
|
||||
report.Summary.NewCount.Should().Be(0);
|
||||
report.Summary.ResolvedCount.Should().Be(0);
|
||||
report.HasActionableChanges.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddNew_ShouldAddNewEntry()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to");
|
||||
|
||||
// Act
|
||||
builder.AddNew(
|
||||
"CVE-2024-1234",
|
||||
"pkg:npm/lodash@4.17.21",
|
||||
VexStatus.Affected,
|
||||
0.85,
|
||||
"binary",
|
||||
null,
|
||||
["nvd", "github"]);
|
||||
|
||||
var report = builder.Build();
|
||||
|
||||
// Assert
|
||||
report.Summary.NewCount.Should().Be(1);
|
||||
report.HasActionableChanges.Should().BeTrue();
|
||||
report.GetSection(DeltaSection.New).Should().HaveCount(1);
|
||||
|
||||
var entry = report.GetSection(DeltaSection.New)[0];
|
||||
entry.VulnerabilityId.Should().Be("CVE-2024-1234");
|
||||
entry.ProductKey.Should().Be("pkg:npm/lodash@4.17.21");
|
||||
entry.ToStatus.Should().Be(VexStatus.Affected);
|
||||
entry.ContributingSources.Should().Contain("nvd");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddResolved_ShouldAddResolvedEntry()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to");
|
||||
|
||||
// Act
|
||||
builder.AddResolved(
|
||||
"CVE-2024-1234",
|
||||
"pkg:npm/lodash@4.17.21",
|
||||
VexStatus.Affected,
|
||||
VexStatus.NotAffected,
|
||||
0.80,
|
||||
0.95,
|
||||
VexJustification.VulnerableCodeNotPresent);
|
||||
|
||||
var report = builder.Build();
|
||||
|
||||
// Assert
|
||||
report.Summary.ResolvedCount.Should().Be(1);
|
||||
var entry = report.GetSection(DeltaSection.Resolved)[0];
|
||||
entry.FromStatus.Should().Be(VexStatus.Affected);
|
||||
entry.ToStatus.Should().Be(VexStatus.NotAffected);
|
||||
entry.Justification.Should().Be(VexJustification.VulnerableCodeNotPresent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddConfidenceChange_AboveThreshold_ShouldAddEntry()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to")
|
||||
.WithOptions(new DeltaReportOptions { ConfidenceChangeThreshold = 0.15 });
|
||||
|
||||
// Act
|
||||
builder.AddConfidenceChange(
|
||||
"CVE-2024-1234",
|
||||
"pkg:npm/lodash@4.17.21",
|
||||
VexStatus.Affected,
|
||||
0.50,
|
||||
0.90); // 40% increase
|
||||
|
||||
var report = builder.Build();
|
||||
|
||||
// Assert
|
||||
report.Summary.ConfidenceUpCount.Should().Be(1);
|
||||
var entry = report.GetSection(DeltaSection.ConfidenceUp)[0];
|
||||
entry.FromConfidence.Should().Be(0.50);
|
||||
entry.ToConfidence.Should().Be(0.90);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddConfidenceChange_BelowThreshold_ShouldNotAddEntry()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to")
|
||||
.WithOptions(new DeltaReportOptions { ConfidenceChangeThreshold = 0.15 });
|
||||
|
||||
// Act
|
||||
builder.AddConfidenceChange(
|
||||
"CVE-2024-1234",
|
||||
"pkg:npm/lodash@4.17.21",
|
||||
VexStatus.Affected,
|
||||
0.80,
|
||||
0.85); // Only 5% increase
|
||||
|
||||
var report = builder.Build();
|
||||
|
||||
// Assert
|
||||
report.Summary.ConfidenceUpCount.Should().Be(0);
|
||||
report.Entries.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddConfidenceChange_Decrease_ShouldAddConfidenceDownEntry()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to")
|
||||
.WithOptions(new DeltaReportOptions { ConfidenceChangeThreshold = 0.15 });
|
||||
|
||||
// Act
|
||||
builder.AddConfidenceChange(
|
||||
"CVE-2024-1234",
|
||||
"pkg:npm/lodash@4.17.21",
|
||||
VexStatus.Affected,
|
||||
0.90,
|
||||
0.50); // 40% decrease
|
||||
|
||||
var report = builder.Build();
|
||||
|
||||
// Assert
|
||||
report.Summary.ConfidenceDownCount.Should().Be(1);
|
||||
report.GetSection(DeltaSection.ConfidenceDown).Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddPolicyImpact_ShouldAddPolicyImpactEntry()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to");
|
||||
|
||||
// Act
|
||||
builder.AddPolicyImpact(
|
||||
"CVE-2024-1234",
|
||||
"pkg:npm/lodash@4.17.21",
|
||||
VexStatus.Affected,
|
||||
0.85,
|
||||
"Gate decision changed: pass -> fail");
|
||||
|
||||
var report = builder.Build();
|
||||
|
||||
// Assert
|
||||
report.Summary.PolicyImpactCount.Should().Be(1);
|
||||
report.HasActionableChanges.Should().BeTrue();
|
||||
var entry = report.GetSection(DeltaSection.PolicyImpact)[0];
|
||||
entry.Summary.Should().Contain("Gate decision changed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddDamped_WhenExcluded_ShouldNotAddEntry()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to")
|
||||
.WithOptions(new DeltaReportOptions { IncludeDamped = false });
|
||||
|
||||
// Act
|
||||
builder.AddDamped(
|
||||
"CVE-2024-1234",
|
||||
"pkg:npm/lodash@4.17.21",
|
||||
VexStatus.Affected,
|
||||
VexStatus.NotAffected,
|
||||
0.80,
|
||||
0.75,
|
||||
"Duration threshold not met");
|
||||
|
||||
var report = builder.Build();
|
||||
|
||||
// Assert
|
||||
report.Summary.DampedCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddDamped_WhenIncluded_ShouldAddEntry()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to")
|
||||
.WithOptions(new DeltaReportOptions { IncludeDamped = true });
|
||||
|
||||
// Act
|
||||
builder.AddDamped(
|
||||
"CVE-2024-1234",
|
||||
"pkg:npm/lodash@4.17.21",
|
||||
VexStatus.Affected,
|
||||
VexStatus.NotAffected,
|
||||
0.80,
|
||||
0.75,
|
||||
"Duration threshold not met");
|
||||
|
||||
var report = builder.Build();
|
||||
|
||||
// Assert
|
||||
report.Summary.DampedCount.Should().Be(1);
|
||||
report.GetSection(DeltaSection.Damped).Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddEvidenceChange_ShouldAddEvidenceChangedEntry()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to")
|
||||
.WithOptions(new DeltaReportOptions { IncludeEvidenceChanges = true });
|
||||
|
||||
// Act
|
||||
builder.AddEvidenceChange(
|
||||
"CVE-2024-1234",
|
||||
"pkg:npm/lodash@4.17.21",
|
||||
VexStatus.Affected,
|
||||
0.85,
|
||||
"heuristic",
|
||||
"binary");
|
||||
|
||||
var report = builder.Build();
|
||||
|
||||
// Assert
|
||||
report.Summary.EvidenceChangedCount.Should().Be(1);
|
||||
var entry = report.GetSection(DeltaSection.EvidenceChanged)[0];
|
||||
entry.FromRationaleClass.Should().Be("heuristic");
|
||||
entry.ToRationaleClass.Should().Be("binary");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_ShouldSortEntriesDeterministically()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to");
|
||||
|
||||
// Add entries in non-sorted order
|
||||
builder.AddNew("CVE-2024-0002", "pkg:b", VexStatus.Affected, 0.8);
|
||||
builder.AddNew("CVE-2024-0001", "pkg:a", VexStatus.Affected, 0.8);
|
||||
builder.AddResolved("CVE-2024-0003", "pkg:c", VexStatus.Affected, VexStatus.Fixed, 0.8, 0.9);
|
||||
|
||||
// Act
|
||||
var report = builder.Build();
|
||||
|
||||
// Assert - entries should be sorted by section then vuln ID then product key
|
||||
report.Entries[0].Section.Should().Be(DeltaSection.New);
|
||||
report.Entries[0].VulnerabilityId.Should().Be("CVE-2024-0001");
|
||||
report.Entries[1].VulnerabilityId.Should().Be("CVE-2024-0002");
|
||||
report.Entries[2].Section.Should().Be(DeltaSection.Resolved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_ReportId_ShouldBeDeterministic()
|
||||
{
|
||||
// Arrange
|
||||
var builder1 = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to")
|
||||
.AddNew("CVE-2024-1234", "pkg:npm/lodash", VexStatus.Affected, 0.8);
|
||||
|
||||
var builder2 = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to")
|
||||
.AddNew("CVE-2024-1234", "pkg:npm/lodash", VexStatus.Affected, 0.8);
|
||||
|
||||
// Act
|
||||
var report1 = builder1.Build();
|
||||
var report2 = builder2.Build();
|
||||
|
||||
// Assert - same inputs should produce same report ID
|
||||
report1.ReportId.Should().Be(report2.ReportId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToNotificationSummary_WithMultipleChanges_ShouldFormatCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to")
|
||||
.AddNew("CVE-2024-0001", "pkg:a", VexStatus.Affected, 0.8)
|
||||
.AddNew("CVE-2024-0002", "pkg:b", VexStatus.Affected, 0.8)
|
||||
.AddResolved("CVE-2024-0003", "pkg:c", VexStatus.Affected, VexStatus.Fixed, 0.8, 0.9);
|
||||
|
||||
// Act
|
||||
var report = builder.Build();
|
||||
var summary = report.ToNotificationSummary();
|
||||
|
||||
// Assert
|
||||
summary.Should().Contain("2 new");
|
||||
summary.Should().Contain("1 resolved");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToNotificationSummary_NoChanges_ShouldReturnNoSignificantChanges()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to");
|
||||
|
||||
// Act
|
||||
var report = builder.Build();
|
||||
var summary = report.ToNotificationSummary();
|
||||
|
||||
// Assert
|
||||
summary.Should().Be("No significant changes");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BySection_ShouldGroupEntriesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new DeltaReportBuilder(_timeProvider)
|
||||
.WithSnapshots("sha256:from", "sha256:to")
|
||||
.AddNew("CVE-2024-0001", "pkg:a", VexStatus.Affected, 0.8)
|
||||
.AddResolved("CVE-2024-0002", "pkg:b", VexStatus.Affected, VexStatus.Fixed, 0.8, 0.9);
|
||||
|
||||
// Act
|
||||
var report = builder.Build();
|
||||
var bySection = report.BySection;
|
||||
|
||||
// Assert
|
||||
bySection.Should().ContainKey(DeltaSection.New);
|
||||
bySection.Should().ContainKey(DeltaSection.Resolved);
|
||||
bySection[DeltaSection.New].Should().HaveCount(1);
|
||||
bySection[DeltaSection.Resolved].Should().HaveCount(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
// Licensed to StellaOps under the AGPL-3.0-or-later license.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.ReachGraph.Deduplication;
|
||||
using StellaOps.ReachGraph.Schema;
|
||||
using StellaOps.VexLens.Models;
|
||||
using StellaOps.VexLens.NoiseGate;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.VexLens.Tests.NoiseGate;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="NoiseGateService"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class NoiseGateServiceTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly NoiseGateOptions _defaultOptions;
|
||||
|
||||
public NoiseGateServiceTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 4, 12, 0, 0, TimeSpan.Zero));
|
||||
_defaultOptions = new NoiseGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
EdgeDeduplicationEnabled = true,
|
||||
StabilityDampingEnabled = true,
|
||||
MinConfidenceThreshold = 0.0,
|
||||
ConfidenceChangeThreshold = 0.15
|
||||
};
|
||||
}
|
||||
|
||||
private NoiseGateService CreateService(
|
||||
IEdgeDeduplicator? edgeDeduplicator = null,
|
||||
IStabilityDampingGate? dampingGate = null,
|
||||
NoiseGateOptions? options = null)
|
||||
{
|
||||
var opts = options ?? _defaultOptions;
|
||||
var optionsMonitor = new TestOptionsMonitor<NoiseGateOptions>(opts);
|
||||
|
||||
edgeDeduplicator ??= new EdgeDeduplicator();
|
||||
|
||||
if (dampingGate is null)
|
||||
{
|
||||
var dampingOptions = new StabilityDampingOptions { Enabled = true };
|
||||
var dampingOptionsMonitor = new TestOptionsMonitor<StabilityDampingOptions>(dampingOptions);
|
||||
dampingGate = new StabilityDampingGate(
|
||||
dampingOptionsMonitor,
|
||||
_timeProvider,
|
||||
NullLogger<StabilityDampingGate>.Instance);
|
||||
}
|
||||
|
||||
return new NoiseGateService(
|
||||
edgeDeduplicator,
|
||||
dampingGate,
|
||||
optionsMonitor,
|
||||
_timeProvider,
|
||||
NullLogger<NoiseGateService>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DedupeEdgesAsync_WithDuplicateEdges_ShouldDeduplicates()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var edges = new List<ReachGraphEdge>
|
||||
{
|
||||
new()
|
||||
{
|
||||
From = "node-a",
|
||||
To = "node-b",
|
||||
Why = new EdgeExplanation { Type = EdgeExplanationType.DirectCall, Confidence = 0.9, Loc = "file1.cs:10" }
|
||||
},
|
||||
new()
|
||||
{
|
||||
From = "node-a",
|
||||
To = "node-b",
|
||||
Why = new EdgeExplanation { Type = EdgeExplanationType.DirectCall, Confidence = 0.85, Loc = "file2.cs:20" }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.DedupeEdgesAsync(edges);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(1);
|
||||
result[0].EntryPointId.Should().Be("node-a");
|
||||
result[0].SinkId.Should().Be("node-b");
|
||||
result[0].ProvenanceCount.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DedupeEdgesAsync_WhenDisabled_ShouldPassThrough()
|
||||
{
|
||||
// Arrange
|
||||
var options = new NoiseGateOptions { Enabled = false };
|
||||
var service = CreateService(options: options);
|
||||
var edges = new List<ReachGraphEdge>
|
||||
{
|
||||
new()
|
||||
{
|
||||
From = "node-a",
|
||||
To = "node-b",
|
||||
Why = new EdgeExplanation { Type = EdgeExplanationType.DirectCall, Confidence = 0.9 }
|
||||
},
|
||||
new()
|
||||
{
|
||||
From = "node-a",
|
||||
To = "node-b",
|
||||
Why = new EdgeExplanation { Type = EdgeExplanationType.DirectCall, Confidence = 0.85 }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.DedupeEdgesAsync(edges);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2); // Not deduplicated
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveVerdictAsync_NewVerdict_ShouldSurface()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var request = new VerdictResolutionRequest
|
||||
{
|
||||
Key = "artifact:CVE-2024-1234",
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
ProductKey = "pkg:npm/lodash@4.17.21",
|
||||
ProposedStatus = VexStatus.Affected,
|
||||
ProposedConfidence = 0.85
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.ResolveVerdictAsync(request);
|
||||
|
||||
// Assert
|
||||
result.WasSurfaced.Should().BeTrue();
|
||||
result.VulnerabilityId.Should().Be("CVE-2024-1234");
|
||||
result.Status.Should().Be(VexStatus.Affected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveVerdictAsync_WhenDampingDisabled_ShouldAlwaysSurface()
|
||||
{
|
||||
// Arrange
|
||||
var options = new NoiseGateOptions { StabilityDampingEnabled = false };
|
||||
var service = CreateService(options: options);
|
||||
var request = new VerdictResolutionRequest
|
||||
{
|
||||
Key = "artifact:CVE-2024-1234",
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
ProductKey = "pkg:npm/lodash@4.17.21",
|
||||
ProposedStatus = VexStatus.Affected,
|
||||
ProposedConfidence = 0.85
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.ResolveVerdictAsync(request);
|
||||
|
||||
// Assert
|
||||
result.WasSurfaced.Should().BeTrue();
|
||||
result.DampingReason.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GateAsync_ShouldDeduplicateAndResolve()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
var graph = new ReachGraphMinimal
|
||||
{
|
||||
SchemaVersion = "reachgraph.min@v1",
|
||||
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
|
||||
Scope = new ReachGraphScope(["main"], ["*"]),
|
||||
Nodes = [new ReachGraphNode { Id = "node-a" }, new ReachGraphNode { Id = "node-b" }],
|
||||
Edges =
|
||||
[
|
||||
new ReachGraphEdge
|
||||
{
|
||||
From = "node-a",
|
||||
To = "node-b",
|
||||
Why = new EdgeExplanation { Type = EdgeExplanationType.DirectCall, Confidence = 0.9 }
|
||||
},
|
||||
new ReachGraphEdge
|
||||
{
|
||||
From = "node-a",
|
||||
To = "node-b",
|
||||
Why = new EdgeExplanation { Type = EdgeExplanationType.DirectCall, Confidence = 0.85 }
|
||||
}
|
||||
],
|
||||
Provenance = new ReachGraphProvenance("scanner", "1.0", _timeProvider.GetUtcNow())
|
||||
};
|
||||
|
||||
var request = new NoiseGateRequest
|
||||
{
|
||||
Graph = graph,
|
||||
SnapshotId = "snapshot-001",
|
||||
Verdicts =
|
||||
[
|
||||
new VerdictResolutionRequest
|
||||
{
|
||||
Key = "artifact:CVE-2024-1234",
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
ProductKey = "pkg:npm/lodash@4.17.21",
|
||||
ProposedStatus = VexStatus.Affected,
|
||||
ProposedConfidence = 0.85
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.GateAsync(request);
|
||||
|
||||
// Assert
|
||||
result.SnapshotId.Should().Be("snapshot-001");
|
||||
result.Edges.Should().HaveCount(1); // Deduplicated
|
||||
result.Verdicts.Should().HaveCount(1);
|
||||
result.Statistics.EdgeReductionPercent.Should().Be(50.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DiffAsync_ShouldDetectNewFindings()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
|
||||
var fromSnapshot = new GatedGraphSnapshot
|
||||
{
|
||||
SnapshotId = "snapshot-001",
|
||||
Digest = "sha256:from",
|
||||
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
|
||||
Edges = [],
|
||||
Verdicts = [],
|
||||
CreatedAt = _timeProvider.GetUtcNow(),
|
||||
Statistics = new GatingStatistics
|
||||
{
|
||||
OriginalEdgeCount = 0,
|
||||
DeduplicatedEdgeCount = 0,
|
||||
TotalVerdictCount = 0,
|
||||
SurfacedVerdictCount = 0,
|
||||
DampedVerdictCount = 0,
|
||||
Duration = TimeSpan.Zero
|
||||
}
|
||||
};
|
||||
|
||||
var toSnapshot = new GatedGraphSnapshot
|
||||
{
|
||||
SnapshotId = "snapshot-002",
|
||||
Digest = "sha256:to",
|
||||
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
|
||||
Edges = [],
|
||||
Verdicts =
|
||||
[
|
||||
new ResolvedVerdict
|
||||
{
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
ProductKey = "pkg:npm/lodash@4.17.21",
|
||||
Status = VexStatus.Affected,
|
||||
Confidence = 0.85,
|
||||
WasSurfaced = true,
|
||||
ResolvedAt = _timeProvider.GetUtcNow()
|
||||
}
|
||||
],
|
||||
CreatedAt = _timeProvider.GetUtcNow(),
|
||||
Statistics = new GatingStatistics
|
||||
{
|
||||
OriginalEdgeCount = 0,
|
||||
DeduplicatedEdgeCount = 0,
|
||||
TotalVerdictCount = 1,
|
||||
SurfacedVerdictCount = 1,
|
||||
DampedVerdictCount = 0,
|
||||
Duration = TimeSpan.Zero
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var delta = await service.DiffAsync(fromSnapshot, toSnapshot);
|
||||
|
||||
// Assert
|
||||
delta.Summary.NewCount.Should().Be(1);
|
||||
delta.Summary.ResolvedCount.Should().Be(0);
|
||||
delta.Entries.Should().ContainSingle(e => e.Section == Delta.DeltaSection.New);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DiffAsync_ShouldDetectResolvedFindings()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
|
||||
var fromSnapshot = new GatedGraphSnapshot
|
||||
{
|
||||
SnapshotId = "snapshot-001",
|
||||
Digest = "sha256:from",
|
||||
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
|
||||
Edges = [],
|
||||
Verdicts =
|
||||
[
|
||||
new ResolvedVerdict
|
||||
{
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
ProductKey = "pkg:npm/lodash@4.17.21",
|
||||
Status = VexStatus.Affected,
|
||||
Confidence = 0.85,
|
||||
WasSurfaced = true,
|
||||
ResolvedAt = _timeProvider.GetUtcNow()
|
||||
}
|
||||
],
|
||||
CreatedAt = _timeProvider.GetUtcNow(),
|
||||
Statistics = new GatingStatistics
|
||||
{
|
||||
OriginalEdgeCount = 0,
|
||||
DeduplicatedEdgeCount = 0,
|
||||
TotalVerdictCount = 1,
|
||||
SurfacedVerdictCount = 1,
|
||||
DampedVerdictCount = 0,
|
||||
Duration = TimeSpan.Zero
|
||||
}
|
||||
};
|
||||
|
||||
var toSnapshot = new GatedGraphSnapshot
|
||||
{
|
||||
SnapshotId = "snapshot-002",
|
||||
Digest = "sha256:to",
|
||||
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
|
||||
Edges = [],
|
||||
Verdicts =
|
||||
[
|
||||
new ResolvedVerdict
|
||||
{
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
ProductKey = "pkg:npm/lodash@4.17.21",
|
||||
Status = VexStatus.NotAffected,
|
||||
Confidence = 0.95,
|
||||
WasSurfaced = true,
|
||||
Justification = VexJustification.VulnerableCodeNotPresent,
|
||||
ResolvedAt = _timeProvider.GetUtcNow()
|
||||
}
|
||||
],
|
||||
CreatedAt = _timeProvider.GetUtcNow(),
|
||||
Statistics = new GatingStatistics
|
||||
{
|
||||
OriginalEdgeCount = 0,
|
||||
DeduplicatedEdgeCount = 0,
|
||||
TotalVerdictCount = 1,
|
||||
SurfacedVerdictCount = 1,
|
||||
DampedVerdictCount = 0,
|
||||
Duration = TimeSpan.Zero
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var delta = await service.DiffAsync(fromSnapshot, toSnapshot);
|
||||
|
||||
// Assert
|
||||
delta.Summary.ResolvedCount.Should().Be(1);
|
||||
delta.Entries.Should().ContainSingle(e => e.Section == Delta.DeltaSection.Resolved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DiffAsync_ShouldDetectConfidenceChanges()
|
||||
{
|
||||
// Arrange
|
||||
var service = CreateService();
|
||||
|
||||
var fromSnapshot = new GatedGraphSnapshot
|
||||
{
|
||||
SnapshotId = "snapshot-001",
|
||||
Digest = "sha256:from",
|
||||
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
|
||||
Edges = [],
|
||||
Verdicts =
|
||||
[
|
||||
new ResolvedVerdict
|
||||
{
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
ProductKey = "pkg:npm/lodash@4.17.21",
|
||||
Status = VexStatus.Affected,
|
||||
Confidence = 0.50,
|
||||
WasSurfaced = true,
|
||||
ResolvedAt = _timeProvider.GetUtcNow()
|
||||
}
|
||||
],
|
||||
CreatedAt = _timeProvider.GetUtcNow(),
|
||||
Statistics = new GatingStatistics
|
||||
{
|
||||
OriginalEdgeCount = 0,
|
||||
DeduplicatedEdgeCount = 0,
|
||||
TotalVerdictCount = 1,
|
||||
SurfacedVerdictCount = 1,
|
||||
DampedVerdictCount = 0,
|
||||
Duration = TimeSpan.Zero
|
||||
}
|
||||
};
|
||||
|
||||
var toSnapshot = new GatedGraphSnapshot
|
||||
{
|
||||
SnapshotId = "snapshot-002",
|
||||
Digest = "sha256:to",
|
||||
Artifact = new ReachGraphArtifact("test", "sha256:abc123", []),
|
||||
Edges = [],
|
||||
Verdicts =
|
||||
[
|
||||
new ResolvedVerdict
|
||||
{
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
ProductKey = "pkg:npm/lodash@4.17.21",
|
||||
Status = VexStatus.Affected,
|
||||
Confidence = 0.90, // Large increase
|
||||
WasSurfaced = true,
|
||||
ResolvedAt = _timeProvider.GetUtcNow()
|
||||
}
|
||||
],
|
||||
CreatedAt = _timeProvider.GetUtcNow(),
|
||||
Statistics = new GatingStatistics
|
||||
{
|
||||
OriginalEdgeCount = 0,
|
||||
DeduplicatedEdgeCount = 0,
|
||||
TotalVerdictCount = 1,
|
||||
SurfacedVerdictCount = 1,
|
||||
DampedVerdictCount = 0,
|
||||
Duration = TimeSpan.Zero
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var delta = await service.DiffAsync(fromSnapshot, toSnapshot);
|
||||
|
||||
// Assert
|
||||
delta.Summary.ConfidenceUpCount.Should().Be(1);
|
||||
delta.Entries.Should().ContainSingle(e => e.Section == Delta.DeltaSection.ConfidenceUp);
|
||||
}
|
||||
|
||||
private sealed class TestOptionsMonitor<T> : IOptionsMonitor<T>
|
||||
where T : class
|
||||
{
|
||||
public TestOptionsMonitor(T value) => CurrentValue = value;
|
||||
public T CurrentValue { get; }
|
||||
public T Get(string? name) => CurrentValue;
|
||||
public IDisposable? OnChange(Action<T, string?> listener) => null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>StellaOps.VexLens.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.VexLens/StellaOps.VexLens.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user