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:
StellaOps Bot
2025-12-26 15:17:15 +02:00
parent 7792749bb4
commit 907783f625
354 changed files with 79727 additions and 1346 deletions

View File

@@ -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