sprints and audit work
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexGateServiceTests.cs
|
||||
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
|
||||
// Description: Unit tests for VexGateService.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Gate.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="VexGateService"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class VexGateServiceTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly VexGatePolicyEvaluator _policyEvaluator;
|
||||
private readonly Mock<IVexObservationProvider> _vexProviderMock;
|
||||
|
||||
public VexGateServiceTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(
|
||||
new DateTimeOffset(2026, 1, 6, 10, 30, 0, TimeSpan.Zero));
|
||||
_policyEvaluator = new VexGatePolicyEvaluator(
|
||||
NullLogger<VexGatePolicyEvaluator>.Instance);
|
||||
_vexProviderMock = new Mock<IVexObservationProvider>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WithVexNotAffected_ReturnsPass()
|
||||
{
|
||||
_vexProviderMock
|
||||
.Setup(p => p.GetVexStatusAsync("CVE-2025-1234", "pkg:npm/test@1.0.0", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new VexObservationResult
|
||||
{
|
||||
Status = VexStatus.NotAffected,
|
||||
Confidence = 0.95,
|
||||
});
|
||||
|
||||
_vexProviderMock
|
||||
.Setup(p => p.GetStatementsAsync("CVE-2025-1234", "pkg:npm/test@1.0.0", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<VexStatementInfo>
|
||||
{
|
||||
new()
|
||||
{
|
||||
StatementId = "stmt-001",
|
||||
IssuerId = "vendor-a",
|
||||
Status = VexStatus.NotAffected,
|
||||
Timestamp = _timeProvider.GetUtcNow().AddDays(-1),
|
||||
TrustWeight = 0.9,
|
||||
},
|
||||
});
|
||||
|
||||
var service = CreateService();
|
||||
|
||||
var finding = new VexGateFinding
|
||||
{
|
||||
FindingId = "finding-001",
|
||||
VulnerabilityId = "CVE-2025-1234",
|
||||
Purl = "pkg:npm/test@1.0.0",
|
||||
ImageDigest = "sha256:abc123",
|
||||
IsReachable = true,
|
||||
};
|
||||
|
||||
var result = await service.EvaluateAsync(finding);
|
||||
|
||||
Assert.Equal(VexGateDecision.Pass, result.Decision);
|
||||
Assert.Equal("pass-vendor-not-affected", result.PolicyRuleMatched);
|
||||
Assert.Single(result.ContributingStatements);
|
||||
Assert.Equal("stmt-001", result.ContributingStatements[0].StatementId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_ExploitableReachable_ReturnsBlock()
|
||||
{
|
||||
_vexProviderMock
|
||||
.Setup(p => p.GetVexStatusAsync("CVE-2025-5678", "pkg:npm/vuln@2.0.0", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new VexObservationResult
|
||||
{
|
||||
Status = VexStatus.Affected,
|
||||
Confidence = 0.9,
|
||||
});
|
||||
|
||||
_vexProviderMock
|
||||
.Setup(p => p.GetStatementsAsync("CVE-2025-5678", "pkg:npm/vuln@2.0.0", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<VexStatementInfo>());
|
||||
|
||||
var service = CreateService();
|
||||
|
||||
var finding = new VexGateFinding
|
||||
{
|
||||
FindingId = "finding-002",
|
||||
VulnerabilityId = "CVE-2025-5678",
|
||||
Purl = "pkg:npm/vuln@2.0.0",
|
||||
ImageDigest = "sha256:def456",
|
||||
IsReachable = true,
|
||||
IsExploitable = true,
|
||||
HasCompensatingControl = false,
|
||||
SeverityLevel = "critical",
|
||||
};
|
||||
|
||||
var result = await service.EvaluateAsync(finding);
|
||||
|
||||
Assert.Equal(VexGateDecision.Block, result.Decision);
|
||||
Assert.Equal("block-exploitable-reachable", result.PolicyRuleMatched);
|
||||
Assert.True(result.Evidence.IsReachable);
|
||||
Assert.True(result.Evidence.IsExploitable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_NoVexProvider_UsesDefaultEvidence()
|
||||
{
|
||||
var service = new VexGateService(
|
||||
_policyEvaluator,
|
||||
_timeProvider,
|
||||
NullLogger<VexGateService>.Instance,
|
||||
vexProvider: null);
|
||||
|
||||
var finding = new VexGateFinding
|
||||
{
|
||||
FindingId = "finding-003",
|
||||
VulnerabilityId = "CVE-2025-9999",
|
||||
Purl = "pkg:npm/unknown@1.0.0",
|
||||
ImageDigest = "sha256:xyz789",
|
||||
IsReachable = false,
|
||||
SeverityLevel = "high",
|
||||
};
|
||||
|
||||
var result = await service.EvaluateAsync(finding);
|
||||
|
||||
// High severity + not reachable = warn
|
||||
Assert.Equal(VexGateDecision.Warn, result.Decision);
|
||||
Assert.Null(result.Evidence.VendorStatus);
|
||||
Assert.Empty(result.ContributingStatements);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_EvaluatedAtIsSet()
|
||||
{
|
||||
var service = CreateServiceWithoutVex();
|
||||
|
||||
var finding = new VexGateFinding
|
||||
{
|
||||
FindingId = "finding-004",
|
||||
VulnerabilityId = "CVE-2025-1111",
|
||||
Purl = "pkg:npm/pkg@1.0.0",
|
||||
ImageDigest = "sha256:time123",
|
||||
};
|
||||
|
||||
var result = await service.EvaluateAsync(finding);
|
||||
|
||||
Assert.Equal(_timeProvider.GetUtcNow(), result.EvaluatedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateBatchAsync_ProcessesMultipleFindings()
|
||||
{
|
||||
var service = CreateServiceWithoutVex();
|
||||
|
||||
var findings = new List<VexGateFinding>
|
||||
{
|
||||
new()
|
||||
{
|
||||
FindingId = "f1",
|
||||
VulnerabilityId = "CVE-1",
|
||||
Purl = "pkg:npm/a@1.0.0",
|
||||
ImageDigest = "sha256:batch",
|
||||
IsReachable = true,
|
||||
IsExploitable = true,
|
||||
HasCompensatingControl = false,
|
||||
},
|
||||
new()
|
||||
{
|
||||
FindingId = "f2",
|
||||
VulnerabilityId = "CVE-2",
|
||||
Purl = "pkg:npm/b@1.0.0",
|
||||
ImageDigest = "sha256:batch",
|
||||
IsReachable = false,
|
||||
SeverityLevel = "high",
|
||||
},
|
||||
new()
|
||||
{
|
||||
FindingId = "f3",
|
||||
VulnerabilityId = "CVE-3",
|
||||
Purl = "pkg:npm/c@1.0.0",
|
||||
ImageDigest = "sha256:batch",
|
||||
SeverityLevel = "low",
|
||||
},
|
||||
};
|
||||
|
||||
var results = await service.EvaluateBatchAsync(findings);
|
||||
|
||||
Assert.Equal(3, results.Length);
|
||||
Assert.Equal(VexGateDecision.Block, results[0].GateResult.Decision);
|
||||
Assert.Equal(VexGateDecision.Warn, results[1].GateResult.Decision);
|
||||
Assert.Equal(VexGateDecision.Warn, results[2].GateResult.Decision); // Default
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateBatchAsync_EmptyList_ReturnsEmpty()
|
||||
{
|
||||
var service = CreateServiceWithoutVex();
|
||||
|
||||
var results = await service.EvaluateBatchAsync(new List<VexGateFinding>());
|
||||
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateBatchAsync_UsesBatchPrefetch_WhenAvailable()
|
||||
{
|
||||
var batchProviderMock = new Mock<IVexObservationBatchProvider>();
|
||||
var prefetchedKeys = new List<VexLookupKey>();
|
||||
|
||||
batchProviderMock
|
||||
.Setup(p => p.PrefetchAsync(It.IsAny<IReadOnlyList<VexLookupKey>>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<IReadOnlyList<VexLookupKey>, CancellationToken>((keys, _) => prefetchedKeys.AddRange(keys))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
batchProviderMock
|
||||
.Setup(p => p.GetVexStatusAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((VexObservationResult?)null);
|
||||
|
||||
batchProviderMock
|
||||
.Setup(p => p.GetStatementsAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<VexStatementInfo>());
|
||||
|
||||
var service = new VexGateService(
|
||||
_policyEvaluator,
|
||||
_timeProvider,
|
||||
NullLogger<VexGateService>.Instance,
|
||||
batchProviderMock.Object);
|
||||
|
||||
var findings = new List<VexGateFinding>
|
||||
{
|
||||
new()
|
||||
{
|
||||
FindingId = "f1",
|
||||
VulnerabilityId = "CVE-1",
|
||||
Purl = "pkg:npm/a@1.0.0",
|
||||
ImageDigest = "sha256:batch",
|
||||
},
|
||||
new()
|
||||
{
|
||||
FindingId = "f2",
|
||||
VulnerabilityId = "CVE-2",
|
||||
Purl = "pkg:npm/b@1.0.0",
|
||||
ImageDigest = "sha256:batch",
|
||||
},
|
||||
};
|
||||
|
||||
await service.EvaluateBatchAsync(findings);
|
||||
|
||||
batchProviderMock.Verify(
|
||||
p => p.PrefetchAsync(It.IsAny<IReadOnlyList<VexLookupKey>>(), It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
|
||||
Assert.Equal(2, prefetchedKeys.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_VexFixed_ReturnsPass()
|
||||
{
|
||||
_vexProviderMock
|
||||
.Setup(p => p.GetVexStatusAsync("CVE-2025-FIXED", "pkg:deb/fixed@1.0.0", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new VexObservationResult
|
||||
{
|
||||
Status = VexStatus.Fixed,
|
||||
Confidence = 0.85,
|
||||
BackportHints = ImmutableArray.Create("deb:1.0.0-2ubuntu1"),
|
||||
});
|
||||
|
||||
_vexProviderMock
|
||||
.Setup(p => p.GetStatementsAsync("CVE-2025-FIXED", "pkg:deb/fixed@1.0.0", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<VexStatementInfo>
|
||||
{
|
||||
new()
|
||||
{
|
||||
StatementId = "stmt-fixed",
|
||||
IssuerId = "ubuntu",
|
||||
Status = VexStatus.Fixed,
|
||||
Timestamp = _timeProvider.GetUtcNow().AddHours(-6),
|
||||
TrustWeight = 0.95,
|
||||
},
|
||||
});
|
||||
|
||||
var service = CreateService();
|
||||
|
||||
var finding = new VexGateFinding
|
||||
{
|
||||
FindingId = "finding-fixed",
|
||||
VulnerabilityId = "CVE-2025-FIXED",
|
||||
Purl = "pkg:deb/fixed@1.0.0",
|
||||
ImageDigest = "sha256:ubuntu",
|
||||
IsReachable = true,
|
||||
};
|
||||
|
||||
var result = await service.EvaluateAsync(finding);
|
||||
|
||||
Assert.Equal(VexGateDecision.Pass, result.Decision);
|
||||
Assert.Equal("pass-backport-confirmed", result.PolicyRuleMatched);
|
||||
Assert.Single(result.Evidence.BackportHints);
|
||||
}
|
||||
|
||||
private VexGateService CreateService()
|
||||
{
|
||||
return new VexGateService(
|
||||
_policyEvaluator,
|
||||
_timeProvider,
|
||||
NullLogger<VexGateService>.Instance,
|
||||
_vexProviderMock.Object);
|
||||
}
|
||||
|
||||
private VexGateService CreateServiceWithoutVex()
|
||||
{
|
||||
return new VexGateService(
|
||||
_policyEvaluator,
|
||||
_timeProvider,
|
||||
NullLogger<VexGateService>.Instance,
|
||||
vexProvider: null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user