Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
@@ -0,0 +1,696 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// AutoVexDowngradeServiceTests.cs
|
||||
// Sprint: SPRINT_20251226_011_BE_auto_vex_downgrade
|
||||
// Task: AUTOVEX-16 — Integration tests for auto-VEX downgrade
|
||||
// Description: Unit and integration tests for AutoVexDowngradeService.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Excititor.Core.AutoVex.Tests;
|
||||
|
||||
public class AutoVexDowngradeServiceTests
|
||||
{
|
||||
private readonly TestHotSymbolQueryService _hotSymbolService;
|
||||
private readonly TestVulnerableSymbolCorrelator _correlator;
|
||||
private readonly AutoVexDowngradeOptions _options;
|
||||
private readonly AutoVexDowngradeService _sut;
|
||||
|
||||
public AutoVexDowngradeServiceTests()
|
||||
{
|
||||
_hotSymbolService = new TestHotSymbolQueryService();
|
||||
_correlator = new TestVulnerableSymbolCorrelator();
|
||||
_options = new AutoVexDowngradeOptions
|
||||
{
|
||||
MinObservationCount = 5,
|
||||
MinCpuPercentage = 1.0,
|
||||
MinConfidenceThreshold = 0.7
|
||||
};
|
||||
|
||||
_sut = new AutoVexDowngradeService(
|
||||
NullLogger<AutoVexDowngradeService>.Instance,
|
||||
Options.Create(_options),
|
||||
_hotSymbolService,
|
||||
_correlator);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectHotVulnerableSymbols_ReturnsEmptyWhenNoHotSymbols()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:abc123";
|
||||
var window = TimeWindow.FromDuration(TimeSpan.FromHours(1));
|
||||
_hotSymbolService.SetHotSymbols([]);
|
||||
|
||||
// Act
|
||||
var result = await _sut.DetectHotVulnerableSymbolsAsync(imageDigest, window);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectHotVulnerableSymbols_FiltersOutNonVulnerable()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:abc123";
|
||||
var window = TimeWindow.FromDuration(TimeSpan.FromHours(1));
|
||||
|
||||
_hotSymbolService.SetHotSymbols(
|
||||
[
|
||||
new HotSymbolEntry
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
BuildId = "build-001",
|
||||
SymbolId = "sym-001",
|
||||
Symbol = "libfoo::safe_function",
|
||||
ObservationCount = 100,
|
||||
CpuPercentage = 15.0
|
||||
}
|
||||
]);
|
||||
|
||||
_correlator.SetCorrelations([]); // No CVE correlation
|
||||
|
||||
// Act
|
||||
var result = await _sut.DetectHotVulnerableSymbolsAsync(imageDigest, window);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectHotVulnerableSymbols_ReturnsVulnerableSymbols()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:abc123";
|
||||
var window = TimeWindow.FromDuration(TimeSpan.FromHours(1));
|
||||
|
||||
_hotSymbolService.SetHotSymbols(
|
||||
[
|
||||
new HotSymbolEntry
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
BuildId = "build-001",
|
||||
SymbolId = "sym-001",
|
||||
Symbol = "libfoo::parse_header",
|
||||
ObservationCount = 100,
|
||||
CpuPercentage = 15.0
|
||||
}
|
||||
]);
|
||||
|
||||
_correlator.SetCorrelations(
|
||||
[
|
||||
new VulnerableSymbolCorrelation
|
||||
{
|
||||
SymbolId = "sym-001",
|
||||
CveId = "CVE-2024-1234",
|
||||
PackagePath = "libfoo",
|
||||
Confidence = 0.95
|
||||
}
|
||||
]);
|
||||
|
||||
// Act
|
||||
var result = await _sut.DetectHotVulnerableSymbolsAsync(imageDigest, window);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal("CVE-2024-1234", result[0].CveId);
|
||||
Assert.Equal("libfoo::parse_header", result[0].Symbol);
|
||||
Assert.Equal(15.0, result[0].CpuPercentage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectHotVulnerableSymbols_FiltersOutBelowThreshold()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:abc123";
|
||||
var window = TimeWindow.FromDuration(TimeSpan.FromHours(1));
|
||||
|
||||
_hotSymbolService.SetHotSymbols(
|
||||
[
|
||||
new HotSymbolEntry
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
BuildId = "build-001",
|
||||
SymbolId = "sym-001",
|
||||
Symbol = "libfoo::parse_header",
|
||||
ObservationCount = 3, // Below threshold of 5
|
||||
CpuPercentage = 0.5 // Below threshold of 1.0
|
||||
}
|
||||
]);
|
||||
|
||||
_correlator.SetCorrelations(
|
||||
[
|
||||
new VulnerableSymbolCorrelation
|
||||
{
|
||||
SymbolId = "sym-001",
|
||||
CveId = "CVE-2024-1234",
|
||||
PackagePath = "libfoo",
|
||||
Confidence = 0.95
|
||||
}
|
||||
]);
|
||||
|
||||
// Act
|
||||
var result = await _sut.DetectHotVulnerableSymbolsAsync(imageDigest, window);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result); // Filtered out due to thresholds
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectHotVulnerableSymbols_CalculatesConfidenceCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:abc123";
|
||||
var window = TimeWindow.FromDuration(TimeSpan.FromHours(1));
|
||||
|
||||
_hotSymbolService.SetHotSymbols(
|
||||
[
|
||||
new HotSymbolEntry
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
BuildId = "build-001",
|
||||
SymbolId = "sym-001",
|
||||
Symbol = "libfoo::parse_header",
|
||||
ObservationCount = 1000, // High observation count
|
||||
CpuPercentage = 25.0 // High CPU
|
||||
}
|
||||
]);
|
||||
|
||||
_correlator.SetCorrelations(
|
||||
[
|
||||
new VulnerableSymbolCorrelation
|
||||
{
|
||||
SymbolId = "sym-001",
|
||||
CveId = "CVE-2024-1234",
|
||||
PackagePath = "libfoo",
|
||||
Confidence = 0.95
|
||||
}
|
||||
]);
|
||||
|
||||
// Act
|
||||
var result = await _sut.DetectHotVulnerableSymbolsAsync(imageDigest, window);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.True(result[0].Confidence > 0.9); // High confidence expected
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessImageAsync_CompletePipeline()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:abc123";
|
||||
var window = TimeWindow.FromDuration(TimeSpan.FromHours(1));
|
||||
|
||||
_hotSymbolService.SetHotSymbols(
|
||||
[
|
||||
new HotSymbolEntry
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
BuildId = "build-001",
|
||||
SymbolId = "sym-001",
|
||||
Symbol = "libssl::ssl3_get_record",
|
||||
ObservationCount = 500,
|
||||
CpuPercentage = 12.5
|
||||
}
|
||||
]);
|
||||
|
||||
_correlator.SetCorrelations(
|
||||
[
|
||||
new VulnerableSymbolCorrelation
|
||||
{
|
||||
SymbolId = "sym-001",
|
||||
CveId = "CVE-2024-5678",
|
||||
PackagePath = "openssl",
|
||||
Confidence = 0.92
|
||||
}
|
||||
]);
|
||||
|
||||
var generator = new TestVexDowngradeGenerator();
|
||||
var service = new AutoVexDowngradeService(
|
||||
NullLogger<AutoVexDowngradeService>.Instance,
|
||||
Options.Create(_options),
|
||||
_hotSymbolService,
|
||||
_correlator);
|
||||
|
||||
// Act
|
||||
var detections = await service.DetectHotVulnerableSymbolsAsync(imageDigest, window);
|
||||
|
||||
// Assert
|
||||
Assert.Single(detections);
|
||||
var detection = detections[0];
|
||||
Assert.Equal("CVE-2024-5678", detection.CveId);
|
||||
Assert.Equal("openssl", detection.PackagePath);
|
||||
Assert.Equal(500, detection.ObservationCount);
|
||||
}
|
||||
|
||||
#region Test Doubles
|
||||
|
||||
private class TestHotSymbolQueryService : IHotSymbolQueryService
|
||||
{
|
||||
private List<HotSymbolEntry> _hotSymbols = [];
|
||||
|
||||
public void SetHotSymbols(List<HotSymbolEntry> symbols) => _hotSymbols = symbols;
|
||||
|
||||
public Task<IReadOnlyList<HotSymbolEntry>> GetHotSymbolsAsync(
|
||||
string imageDigest,
|
||||
TimeWindow window,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = _hotSymbols
|
||||
.Where(s => s.ImageDigest == imageDigest)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<HotSymbolEntry>>(result);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestVulnerableSymbolCorrelator : IVulnerableSymbolCorrelator
|
||||
{
|
||||
private List<VulnerableSymbolCorrelation> _correlations = [];
|
||||
|
||||
public void SetCorrelations(List<VulnerableSymbolCorrelation> correlations)
|
||||
=> _correlations = correlations;
|
||||
|
||||
public Task<IReadOnlyList<VulnerableSymbolCorrelation>> CorrelateAsync(
|
||||
IReadOnlyList<HotSymbolEntry> hotSymbols,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var symbolIds = hotSymbols.Select(s => s.SymbolId).ToHashSet();
|
||||
var result = _correlations
|
||||
.Where(c => symbolIds.Contains(c.SymbolId))
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<VulnerableSymbolCorrelation>>(result);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestVexDowngradeGenerator : IVexDowngradeGenerator
|
||||
{
|
||||
public Task<VexDowngradeResult> GenerateAsync(
|
||||
HotVulnerableSymbol detection,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var statement = new VexDowngradeStatement
|
||||
{
|
||||
StatementId = $"vex-{Guid.NewGuid():N}",
|
||||
VulnerabilityId = detection.CveId,
|
||||
ProductId = detection.ProductId,
|
||||
ComponentPath = detection.PackagePath,
|
||||
Symbol = detection.Symbol,
|
||||
OriginalStatus = "not_affected",
|
||||
NewStatus = "affected",
|
||||
Justification = "vulnerable_code_in_execute_path",
|
||||
RuntimeScore = detection.Confidence,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
DssePayload = null,
|
||||
RekorLogIndex = null
|
||||
};
|
||||
|
||||
return Task.FromResult(new VexDowngradeResult
|
||||
{
|
||||
Success = true,
|
||||
Source = detection,
|
||||
Statement = statement
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class TimeBoxedConfidenceManagerTests
|
||||
{
|
||||
private readonly InMemoryConfidenceRepository _repository;
|
||||
private readonly TimeBoxedConfidenceOptions _options;
|
||||
private readonly TimeBoxedConfidenceManager _sut;
|
||||
|
||||
public TimeBoxedConfidenceManagerTests()
|
||||
{
|
||||
_repository = new InMemoryConfidenceRepository();
|
||||
_options = new TimeBoxedConfidenceOptions
|
||||
{
|
||||
DefaultTtl = TimeSpan.FromHours(24),
|
||||
MaxTtl = TimeSpan.FromDays(7),
|
||||
MinTtl = TimeSpan.FromHours(1),
|
||||
RefreshExtension = TimeSpan.FromHours(12),
|
||||
ConfirmationThreshold = 3,
|
||||
DecayRatePerHour = 0.1
|
||||
};
|
||||
|
||||
_sut = new TimeBoxedConfidenceManager(
|
||||
NullLogger<TimeBoxedConfidenceManager>.Instance,
|
||||
Options.Create(_options),
|
||||
_repository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_CreatesProvisionalConfidence()
|
||||
{
|
||||
// Arrange
|
||||
var statement = new VexDowngradeStatement
|
||||
{
|
||||
StatementId = "stmt-001",
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
ProductId = "product-001",
|
||||
ComponentPath = "libfoo",
|
||||
Symbol = "libfoo::parse",
|
||||
OriginalStatus = "not_affected",
|
||||
NewStatus = "affected",
|
||||
Justification = "runtime_observed",
|
||||
RuntimeScore = 0.85,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _sut.CreateAsync(statement, TimeSpan.FromHours(24));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("CVE-2024-1234", result.CveId);
|
||||
Assert.Equal("product-001", result.ProductId);
|
||||
Assert.Equal(ConfidenceState.Provisional, result.State);
|
||||
Assert.Equal(0, result.RefreshCount);
|
||||
Assert.False(result.IsExpired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RefreshAsync_UpdatesStateAndExtendsTtl()
|
||||
{
|
||||
// Arrange
|
||||
var statement = new VexDowngradeStatement
|
||||
{
|
||||
StatementId = "stmt-001",
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
ProductId = "product-001",
|
||||
ComponentPath = "libfoo",
|
||||
Symbol = "libfoo::parse",
|
||||
OriginalStatus = "not_affected",
|
||||
NewStatus = "affected",
|
||||
Justification = "runtime_observed",
|
||||
RuntimeScore = 0.85,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
var created = await _sut.CreateAsync(statement, TimeSpan.FromHours(24));
|
||||
var originalExpiry = created.ExpiresAt;
|
||||
|
||||
var evidence = new RuntimeObservationEvidence
|
||||
{
|
||||
BuildId = "build-001",
|
||||
ObservationCount = 50,
|
||||
AverageCpuPercentage = 5.0,
|
||||
Score = 0.9,
|
||||
Window = new TimeWindow
|
||||
{
|
||||
Start = DateTimeOffset.UtcNow.AddHours(-1),
|
||||
End = DateTimeOffset.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var refreshed = await _sut.RefreshAsync("CVE-2024-1234", "product-001", evidence);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ConfidenceState.Refreshed, refreshed.State);
|
||||
Assert.Equal(1, refreshed.RefreshCount);
|
||||
Assert.True(refreshed.ExpiresAt >= originalExpiry);
|
||||
Assert.Equal(2, refreshed.EvidenceHistory.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RefreshAsync_BecomesConfirmedAfterThreshold()
|
||||
{
|
||||
// Arrange
|
||||
var statement = new VexDowngradeStatement
|
||||
{
|
||||
StatementId = "stmt-001",
|
||||
VulnerabilityId = "CVE-2024-1234",
|
||||
ProductId = "product-001",
|
||||
ComponentPath = "libfoo",
|
||||
Symbol = "libfoo::parse",
|
||||
OriginalStatus = "not_affected",
|
||||
NewStatus = "affected",
|
||||
Justification = "runtime_observed",
|
||||
RuntimeScore = 0.85,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
await _sut.CreateAsync(statement, TimeSpan.FromHours(24));
|
||||
|
||||
var evidence = new RuntimeObservationEvidence
|
||||
{
|
||||
BuildId = "build-001",
|
||||
ObservationCount = 50,
|
||||
AverageCpuPercentage = 5.0,
|
||||
Score = 0.9,
|
||||
Window = new TimeWindow
|
||||
{
|
||||
Start = DateTimeOffset.UtcNow.AddHours(-1),
|
||||
End = DateTimeOffset.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
// Act - refresh 3 times (confirmation threshold)
|
||||
await _sut.RefreshAsync("CVE-2024-1234", "product-001", evidence);
|
||||
await _sut.RefreshAsync("CVE-2024-1234", "product-001", evidence);
|
||||
var final = await _sut.RefreshAsync("CVE-2024-1234", "product-001", evidence);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(ConfidenceState.Confirmed, final.State);
|
||||
Assert.Equal(3, final.RefreshCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAsync_ReturnsNullForNonExistent()
|
||||
{
|
||||
// Act
|
||||
var result = await _sut.GetAsync("CVE-NONEXISTENT", "product-000");
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
}
|
||||
|
||||
public class ReachabilityLatticeUpdaterTests
|
||||
{
|
||||
[Fact]
|
||||
public void UpdateState_UnknownToRuntimeObserved()
|
||||
{
|
||||
// Arrange
|
||||
var current = LatticeState.Unknown;
|
||||
var evidence = new RuntimeObservationEvidence
|
||||
{
|
||||
BuildId = "build-001",
|
||||
ObservationCount = 10,
|
||||
AverageCpuPercentage = 5.0,
|
||||
Score = 0.8,
|
||||
Window = new TimeWindow
|
||||
{
|
||||
Start = DateTimeOffset.UtcNow.AddHours(-1),
|
||||
End = DateTimeOffset.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ReachabilityLatticeUpdater.ComputeTransition(current, evidence);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(LatticeState.RuntimeObserved, result.NewState);
|
||||
Assert.True(result.Changed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateState_StaticallyReachableToConfirmedReachable()
|
||||
{
|
||||
// Arrange
|
||||
var current = LatticeState.StaticallyReachable;
|
||||
var evidence = new RuntimeObservationEvidence
|
||||
{
|
||||
BuildId = "build-001",
|
||||
ObservationCount = 100,
|
||||
AverageCpuPercentage = 15.0,
|
||||
Score = 0.95,
|
||||
Window = new TimeWindow
|
||||
{
|
||||
Start = DateTimeOffset.UtcNow.AddHours(-1),
|
||||
End = DateTimeOffset.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ReachabilityLatticeUpdater.ComputeTransition(current, evidence);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(LatticeState.ConfirmedReachable, result.NewState);
|
||||
Assert.True(result.Changed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateState_EntryPointRemains()
|
||||
{
|
||||
// Arrange - EntryPoint is maximum state, should not change
|
||||
var current = LatticeState.EntryPoint;
|
||||
var evidence = new RuntimeObservationEvidence
|
||||
{
|
||||
BuildId = "build-001",
|
||||
ObservationCount = 10,
|
||||
AverageCpuPercentage = 5.0,
|
||||
Score = 0.8,
|
||||
Window = new TimeWindow
|
||||
{
|
||||
Start = DateTimeOffset.UtcNow.AddHours(-1),
|
||||
End = DateTimeOffset.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ReachabilityLatticeUpdater.ComputeTransition(current, evidence);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(LatticeState.EntryPoint, result.NewState);
|
||||
Assert.False(result.Changed);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(LatticeState.Unknown, 0.0)]
|
||||
[InlineData(LatticeState.NotPresent, 0.0)]
|
||||
[InlineData(LatticeState.PresentUnreachable, 0.1)]
|
||||
[InlineData(LatticeState.StaticallyReachable, 0.4)]
|
||||
[InlineData(LatticeState.RuntimeObserved, 0.7)]
|
||||
[InlineData(LatticeState.ConfirmedReachable, 0.9)]
|
||||
[InlineData(LatticeState.EntryPoint, 1.0)]
|
||||
[InlineData(LatticeState.Sink, 1.0)]
|
||||
public void GetRtsWeight_ReturnsCorrectWeight(LatticeState state, double expectedWeight)
|
||||
{
|
||||
// Act
|
||||
var weight = ReachabilityLatticeUpdater.GetRtsWeight(state);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedWeight, weight, precision: 2);
|
||||
}
|
||||
}
|
||||
|
||||
public class DriftGateIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public void GateVerdict_BlockTriggersCorrectActions()
|
||||
{
|
||||
// Arrange
|
||||
var action = new PolicyGateAction
|
||||
{
|
||||
ActionId = "release-block",
|
||||
Type = PolicyGateActionType.ReleaseBlock,
|
||||
Description = "Block release pipeline"
|
||||
};
|
||||
|
||||
// Act - using reflection or internal testing
|
||||
var shouldTrigger = ShouldTriggerAction(action, GateVerdict.Block);
|
||||
|
||||
// Assert
|
||||
Assert.True(shouldTrigger);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GateVerdict_PassTriggersNoActions()
|
||||
{
|
||||
// Arrange
|
||||
var action = new PolicyGateAction
|
||||
{
|
||||
ActionId = "release-block",
|
||||
Type = PolicyGateActionType.ReleaseBlock,
|
||||
Description = "Block release pipeline"
|
||||
};
|
||||
|
||||
// Act
|
||||
var shouldTrigger = ShouldTriggerAction(action, GateVerdict.Pass);
|
||||
|
||||
// Assert
|
||||
Assert.False(shouldTrigger);
|
||||
}
|
||||
|
||||
// Helper to test action triggering logic
|
||||
private static bool ShouldTriggerAction(PolicyGateAction action, GateVerdict verdict)
|
||||
{
|
||||
return verdict switch
|
||||
{
|
||||
GateVerdict.Block => action.Type is PolicyGateActionType.ReleaseBlock
|
||||
or PolicyGateActionType.CanaryFreeze
|
||||
or PolicyGateActionType.NotifyOnly
|
||||
or PolicyGateActionType.CreateTicket,
|
||||
|
||||
GateVerdict.Quarantine => action.Type is PolicyGateActionType.Quarantine
|
||||
or PolicyGateActionType.NotifyOnly
|
||||
or PolicyGateActionType.CreateTicket,
|
||||
|
||||
GateVerdict.Warn => action.Type is PolicyGateActionType.NotifyOnly
|
||||
or PolicyGateActionType.CreateTicket,
|
||||
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#region Test Models
|
||||
|
||||
internal sealed record HotSymbolEntry
|
||||
{
|
||||
public required string ImageDigest { get; init; }
|
||||
public required string BuildId { get; init; }
|
||||
public required string SymbolId { get; init; }
|
||||
public required string Symbol { get; init; }
|
||||
public required int ObservationCount { get; init; }
|
||||
public required double CpuPercentage { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record VulnerableSymbolCorrelation
|
||||
{
|
||||
public required string SymbolId { get; init; }
|
||||
public required string CveId { get; init; }
|
||||
public required string PackagePath { get; init; }
|
||||
public required double Confidence { get; init; }
|
||||
}
|
||||
|
||||
internal interface IHotSymbolQueryService
|
||||
{
|
||||
Task<IReadOnlyList<HotSymbolEntry>> GetHotSymbolsAsync(
|
||||
string imageDigest,
|
||||
TimeWindow window,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
internal interface IVulnerableSymbolCorrelator
|
||||
{
|
||||
Task<IReadOnlyList<VulnerableSymbolCorrelation>> CorrelateAsync(
|
||||
IReadOnlyList<HotSymbolEntry> hotSymbols,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
internal interface IVexDowngradeGenerator
|
||||
{
|
||||
Task<VexDowngradeResult> GenerateAsync(
|
||||
HotVulnerableSymbol detection,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
internal sealed record TimeWindow
|
||||
{
|
||||
public required DateTimeOffset Start { get; init; }
|
||||
public required DateTimeOffset End { get; init; }
|
||||
|
||||
public static TimeWindow FromDuration(TimeSpan duration)
|
||||
{
|
||||
var end = DateTimeOffset.UtcNow;
|
||||
return new TimeWindow
|
||||
{
|
||||
Start = end.Subtract(duration),
|
||||
End = end
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user