sprints and audit work

This commit is contained in:
StellaOps Bot
2026-01-07 09:36:16 +02:00
parent 05833e0af2
commit ab364c6032
377 changed files with 64534 additions and 1627 deletions

View File

@@ -0,0 +1,230 @@
// -----------------------------------------------------------------------------
// CachingVexObservationProviderTests.cs
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
// Description: Unit tests for CachingVexObservationProvider.
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace StellaOps.Scanner.Gate.Tests;
/// <summary>
/// Unit tests for <see cref="CachingVexObservationProvider"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class CachingVexObservationProviderTests : IDisposable
{
private readonly Mock<IVexObservationQuery> _queryMock;
private readonly CachingVexObservationProvider _provider;
public CachingVexObservationProviderTests()
{
_queryMock = new Mock<IVexObservationQuery>();
_provider = new CachingVexObservationProvider(
_queryMock.Object,
"test-tenant",
NullLogger<CachingVexObservationProvider>.Instance,
TimeSpan.FromMinutes(5),
1000);
}
public void Dispose()
{
_provider.Dispose();
}
[Fact]
public async Task GetVexStatusAsync_CachesMissResult()
{
_queryMock
.Setup(q => q.GetEffectiveStatusAsync(
"test-tenant", "CVE-2025-1234", "pkg:npm/test@1.0.0", It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexObservationQueryResult
{
Status = VexStatus.NotAffected,
Confidence = 0.9,
LastUpdated = DateTimeOffset.UtcNow,
});
// First call - cache miss
var result1 = await _provider.GetVexStatusAsync("CVE-2025-1234", "pkg:npm/test@1.0.0");
Assert.NotNull(result1);
Assert.Equal(VexStatus.NotAffected, result1.Status);
// Second call - should be cache hit
var result2 = await _provider.GetVexStatusAsync("CVE-2025-1234", "pkg:npm/test@1.0.0");
Assert.NotNull(result2);
Assert.Equal(VexStatus.NotAffected, result2.Status);
// Query should only be called once
_queryMock.Verify(
q => q.GetEffectiveStatusAsync(
"test-tenant", "CVE-2025-1234", "pkg:npm/test@1.0.0", It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task GetVexStatusAsync_ReturnsNull_WhenQueryReturnsNull()
{
_queryMock
.Setup(q => q.GetEffectiveStatusAsync(
"test-tenant", "CVE-2025-UNKNOWN", "pkg:npm/unknown@1.0.0", It.IsAny<CancellationToken>()))
.ReturnsAsync((VexObservationQueryResult?)null);
var result = await _provider.GetVexStatusAsync("CVE-2025-UNKNOWN", "pkg:npm/unknown@1.0.0");
Assert.Null(result);
}
[Fact]
public async Task GetStatementsAsync_CallsQueryDirectly()
{
var statements = new List<VexStatementQueryResult>
{
new()
{
StatementId = "stmt-1",
IssuerId = "vendor",
Status = VexStatus.NotAffected,
Timestamp = DateTimeOffset.UtcNow,
},
};
_queryMock
.Setup(q => q.GetStatementsAsync(
"test-tenant", "CVE-2025-1234", "pkg:npm/test@1.0.0", It.IsAny<CancellationToken>()))
.ReturnsAsync(statements);
var result = await _provider.GetStatementsAsync("CVE-2025-1234", "pkg:npm/test@1.0.0");
Assert.Single(result);
Assert.Equal("stmt-1", result[0].StatementId);
}
[Fact]
public async Task PrefetchAsync_PopulatesCache()
{
var batchResults = new Dictionary<VexQueryKey, VexObservationQueryResult>
{
[new VexQueryKey("CVE-1", "pkg:npm/a@1.0.0")] = new VexObservationQueryResult
{
Status = VexStatus.NotAffected,
Confidence = 0.9,
LastUpdated = DateTimeOffset.UtcNow,
},
[new VexQueryKey("CVE-2", "pkg:npm/b@1.0.0")] = new VexObservationQueryResult
{
Status = VexStatus.Fixed,
Confidence = 0.85,
BackportHints = ImmutableArray.Create("backport-1"),
LastUpdated = DateTimeOffset.UtcNow,
},
};
_queryMock
.Setup(q => q.BatchLookupAsync(
"test-tenant", It.IsAny<IReadOnlyList<VexQueryKey>>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(batchResults);
var keys = new List<VexLookupKey>
{
new("CVE-1", "pkg:npm/a@1.0.0"),
new("CVE-2", "pkg:npm/b@1.0.0"),
};
await _provider.PrefetchAsync(keys);
// Now lookups should be cache hits
var result1 = await _provider.GetVexStatusAsync("CVE-1", "pkg:npm/a@1.0.0");
var result2 = await _provider.GetVexStatusAsync("CVE-2", "pkg:npm/b@1.0.0");
Assert.NotNull(result1);
Assert.Equal(VexStatus.NotAffected, result1.Status);
Assert.NotNull(result2);
Assert.Equal(VexStatus.Fixed, result2.Status);
Assert.Single(result2.BackportHints);
// GetEffectiveStatusAsync should not be called since we prefetched
_queryMock.Verify(
q => q.GetEffectiveStatusAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task PrefetchAsync_SkipsAlreadyCachedKeys()
{
// Pre-populate cache
_queryMock
.Setup(q => q.GetEffectiveStatusAsync(
"test-tenant", "CVE-CACHED", "pkg:npm/cached@1.0.0", It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexObservationQueryResult
{
Status = VexStatus.NotAffected,
Confidence = 0.9,
LastUpdated = DateTimeOffset.UtcNow,
});
await _provider.GetVexStatusAsync("CVE-CACHED", "pkg:npm/cached@1.0.0");
// Now prefetch with the same key
var keys = new List<VexLookupKey>
{
new("CVE-CACHED", "pkg:npm/cached@1.0.0"),
};
await _provider.PrefetchAsync(keys);
// BatchLookupAsync should not be called since key is already cached
_queryMock.Verify(
q => q.BatchLookupAsync(
It.IsAny<string>(), It.IsAny<IReadOnlyList<VexQueryKey>>(), It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task PrefetchAsync_EmptyList_DoesNothing()
{
await _provider.PrefetchAsync(new List<VexLookupKey>());
_queryMock.Verify(
q => q.BatchLookupAsync(
It.IsAny<string>(), It.IsAny<IReadOnlyList<VexQueryKey>>(), It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public void GetStatistics_ReturnsCurrentCount()
{
var stats = _provider.GetStatistics();
Assert.Equal(0, stats.CurrentEntryCount);
}
[Fact]
public async Task Cache_IsCaseInsensitive_ForVulnerabilityId()
{
_queryMock
.Setup(q => q.GetEffectiveStatusAsync(
"test-tenant", It.IsAny<string>(), "pkg:npm/test@1.0.0", It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexObservationQueryResult
{
Status = VexStatus.Fixed,
Confidence = 0.8,
LastUpdated = DateTimeOffset.UtcNow,
});
await _provider.GetVexStatusAsync("cve-2025-1234", "pkg:npm/test@1.0.0");
await _provider.GetVexStatusAsync("CVE-2025-1234", "pkg:npm/test@1.0.0");
// Should be treated as the same key
_queryMock.Verify(
q => q.GetEffectiveStatusAsync(
"test-tenant", It.IsAny<string>(), "pkg:npm/test@1.0.0", It.IsAny<CancellationToken>()),
Times.Once);
}
}

View File

@@ -0,0 +1,256 @@
// -----------------------------------------------------------------------------
// VexGatePolicyEvaluatorTests.cs
// Sprint: SPRINT_20260106_003_002_SCANNER_vex_gate_service
// Description: Unit tests for VexGatePolicyEvaluator.
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace StellaOps.Scanner.Gate.Tests;
/// <summary>
/// Unit tests for <see cref="VexGatePolicyEvaluator"/>.
/// </summary>
[Trait("Category", "Unit")]
public sealed class VexGatePolicyEvaluatorTests
{
private readonly VexGatePolicyEvaluator _evaluator;
public VexGatePolicyEvaluatorTests()
{
_evaluator = new VexGatePolicyEvaluator(NullLogger<VexGatePolicyEvaluator>.Instance);
}
[Fact]
public void Evaluate_ExploitableAndReachable_ReturnsBlock()
{
var evidence = new VexGateEvidence
{
IsExploitable = true,
IsReachable = true,
HasCompensatingControl = false,
ConfidenceScore = 0.95,
SeverityLevel = "critical",
};
var (decision, ruleId, rationale) = _evaluator.Evaluate(evidence);
Assert.Equal(VexGateDecision.Block, decision);
Assert.Equal("block-exploitable-reachable", ruleId);
Assert.Contains("Exploitable", rationale);
}
[Fact]
public void Evaluate_ExploitableAndReachableWithControl_ReturnsDefault()
{
var evidence = new VexGateEvidence
{
IsExploitable = true,
IsReachable = true,
HasCompensatingControl = true, // Has control, so block rule doesn't match
ConfidenceScore = 0.95,
SeverityLevel = "critical",
};
var (decision, ruleId, _) = _evaluator.Evaluate(evidence);
// With compensating control, the block rule doesn't match
// Next matching rule or default applies
Assert.NotEqual("block-exploitable-reachable", ruleId);
}
[Fact]
public void Evaluate_HighSeverityNotReachable_ReturnsWarn()
{
var evidence = new VexGateEvidence
{
IsExploitable = true,
IsReachable = false,
HasCompensatingControl = false,
ConfidenceScore = 0.8,
SeverityLevel = "high",
};
var (decision, ruleId, rationale) = _evaluator.Evaluate(evidence);
Assert.Equal(VexGateDecision.Warn, decision);
Assert.Equal("warn-high-not-reachable", ruleId);
Assert.Contains("not reachable", rationale, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Evaluate_CriticalSeverityNotReachable_ReturnsWarn()
{
var evidence = new VexGateEvidence
{
IsExploitable = true,
IsReachable = false,
HasCompensatingControl = false,
ConfidenceScore = 0.8,
SeverityLevel = "critical",
};
var (decision, ruleId, _) = _evaluator.Evaluate(evidence);
Assert.Equal(VexGateDecision.Warn, decision);
Assert.Equal("warn-high-not-reachable", ruleId);
}
[Fact]
public void Evaluate_VendorNotAffected_ReturnsPass()
{
var evidence = new VexGateEvidence
{
VendorStatus = VexStatus.NotAffected,
IsExploitable = false,
IsReachable = true,
HasCompensatingControl = false,
ConfidenceScore = 0.9,
};
var (decision, ruleId, rationale) = _evaluator.Evaluate(evidence);
Assert.Equal(VexGateDecision.Pass, decision);
Assert.Equal("pass-vendor-not-affected", ruleId);
Assert.Contains("not_affected", rationale);
}
[Fact]
public void Evaluate_VendorFixed_ReturnsPass()
{
var evidence = new VexGateEvidence
{
VendorStatus = VexStatus.Fixed,
IsExploitable = false,
IsReachable = true,
HasCompensatingControl = false,
ConfidenceScore = 0.85,
};
var (decision, ruleId, rationale) = _evaluator.Evaluate(evidence);
Assert.Equal(VexGateDecision.Pass, decision);
Assert.Equal("pass-backport-confirmed", ruleId);
Assert.Contains("backport", rationale, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Evaluate_NoMatchingRules_ReturnsDefaultWarn()
{
var evidence = new VexGateEvidence
{
VendorStatus = VexStatus.UnderInvestigation,
IsExploitable = false,
IsReachable = true,
HasCompensatingControl = false,
ConfidenceScore = 0.5,
SeverityLevel = "low",
};
var (decision, ruleId, rationale) = _evaluator.Evaluate(evidence);
Assert.Equal(VexGateDecision.Warn, decision);
Assert.Equal("default", ruleId);
Assert.Contains("default", rationale, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Evaluate_RulesAreEvaluatedInPriorityOrder()
{
// Evidence matches both block and pass-vendor-not-affected rules
// Block has higher priority (100) than pass (80), so block should win
var evidence = new VexGateEvidence
{
VendorStatus = VexStatus.NotAffected, // Would match pass rule
IsExploitable = true,
IsReachable = true,
HasCompensatingControl = false, // Would match block rule
ConfidenceScore = 0.9,
};
var (decision, ruleId, _) = _evaluator.Evaluate(evidence);
// Block rule has higher priority
Assert.Equal(VexGateDecision.Block, decision);
Assert.Equal("block-exploitable-reachable", ruleId);
}
[Fact]
public void DefaultPolicy_HasExpectedRules()
{
var policy = VexGatePolicy.Default;
Assert.Equal(VexGateDecision.Warn, policy.DefaultDecision);
Assert.Equal(4, policy.Rules.Length);
var ruleIds = policy.Rules.Select(r => r.RuleId).ToList();
Assert.Contains("block-exploitable-reachable", ruleIds);
Assert.Contains("warn-high-not-reachable", ruleIds);
Assert.Contains("pass-vendor-not-affected", ruleIds);
Assert.Contains("pass-backport-confirmed", ruleIds);
}
[Fact]
public void PolicyCondition_Matches_AllConditionsMustMatch()
{
var condition = new VexGatePolicyCondition
{
IsExploitable = true,
IsReachable = true,
HasCompensatingControl = false,
};
// All conditions match
var matchingEvidence = new VexGateEvidence
{
IsExploitable = true,
IsReachable = true,
HasCompensatingControl = false,
};
Assert.True(condition.Matches(matchingEvidence));
// One condition doesn't match
var nonMatchingEvidence = new VexGateEvidence
{
IsExploitable = true,
IsReachable = false, // Different
HasCompensatingControl = false,
};
Assert.False(condition.Matches(nonMatchingEvidence));
}
[Fact]
public void PolicyCondition_SeverityLevels_MatchesAny()
{
var condition = new VexGatePolicyCondition
{
SeverityLevels = ["critical", "high"],
};
var criticalEvidence = new VexGateEvidence { SeverityLevel = "critical" };
var highEvidence = new VexGateEvidence { SeverityLevel = "high" };
var mediumEvidence = new VexGateEvidence { SeverityLevel = "medium" };
var noSeverityEvidence = new VexGateEvidence();
Assert.True(condition.Matches(criticalEvidence));
Assert.True(condition.Matches(highEvidence));
Assert.False(condition.Matches(mediumEvidence));
Assert.False(condition.Matches(noSeverityEvidence));
}
[Fact]
public void PolicyCondition_NullConditionsMatch_AnyEvidence()
{
var condition = new VexGatePolicyCondition(); // All null
var anyEvidence = new VexGateEvidence
{
IsExploitable = true,
IsReachable = false,
SeverityLevel = "low",
};
Assert.True(condition.Matches(anyEvidence));
}
}

View File

@@ -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);
}
}