// ----------------------------------------------------------------------------- // UnknownsWorkflowTests.cs // Sprint: SPRINT_3500_0004_0003_integration_tests_corpus // Task: T3 - Unknowns Workflow Tests // Description: Integration tests for unknowns lifecycle: // detection → ranking → escalation → resolution // ----------------------------------------------------------------------------- using FluentAssertions; using Xunit; namespace StellaOps.Integration.Unknowns; /// /// Integration tests for the unknowns registry workflow. /// Tests the complete lifecycle: detection → ranking → band assignment /// → escalation → resolution. /// public class UnknownsWorkflowTests { #region T3-AC1: Test unknown detection during scan [Fact] public void UnknownDetection_CreatesEntry_ForUnmatchedVulnerability() { // Arrange var ranker = new UnknownRanker(); var unknown = new UnknownEntry { CveId = "CVE-2024-UNKNOWN-001", Package = "mystery-package@1.0.0", DetectedAt = DateTimeOffset.UtcNow, ExploitPressure = 0.5, Uncertainty = 0.8 }; // Act var ranked = ranker.Rank(unknown); // Assert ranked.Should().NotBeNull(); ranked.Score.Should().BeGreaterThan(0); ranked.Band.Should().NotBeNullOrEmpty(); } [Fact] public void UnknownDetection_CapturesMetadata_FromScan() { // Arrange var unknown = new UnknownEntry { CveId = "CVE-2024-SCAN-001", Package = "scanned-package@2.0.0", DetectedAt = DateTimeOffset.UtcNow, ScanId = Guid.NewGuid().ToString(), SourceFeed = "nvd", ExploitPressure = 0.3, Uncertainty = 0.6 }; // Assert unknown.ScanId.Should().NotBeNullOrEmpty(); unknown.SourceFeed.Should().Be("nvd"); unknown.DetectedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5)); } #endregion #region T3-AC2: Test ranking determinism [Fact] public void UnknownRanking_IsDeterministic_WithSameInputs() { // Arrange var ranker = new UnknownRanker(); var unknown = new UnknownEntry { CveId = "CVE-2024-DETERM-001", Package = "det-package@1.0.0", DetectedAt = DateTimeOffset.Parse("2024-01-01T00:00:00Z"), ExploitPressure = 0.7, Uncertainty = 0.4 }; // Act - Rank the same entry multiple times var rank1 = ranker.Rank(unknown); var rank2 = ranker.Rank(unknown); var rank3 = ranker.Rank(unknown); // Assert - All rankings should be identical rank1.Score.Should().Be(rank2.Score); rank2.Score.Should().Be(rank3.Score); rank1.Band.Should().Be(rank2.Band); rank2.Band.Should().Be(rank3.Band); } [Fact] public void UnknownRanking_UsesSimplifiedTwoFactorModel() { // Arrange - Per advisory: 2-factor model (uncertainty + exploit pressure) var ranker = new UnknownRanker(); var highPressureHighUncertainty = new UnknownEntry { CveId = "CVE-HIGH-HIGH", ExploitPressure = 0.9, Uncertainty = 0.9, DetectedAt = DateTimeOffset.UtcNow }; var lowPressureLowUncertainty = new UnknownEntry { CveId = "CVE-LOW-LOW", ExploitPressure = 0.1, Uncertainty = 0.1, DetectedAt = DateTimeOffset.UtcNow }; // Act var highRank = ranker.Rank(highPressureHighUncertainty); var lowRank = ranker.Rank(lowPressureLowUncertainty); // Assert highRank.Score.Should().BeGreaterThan(lowRank.Score, "High pressure + high uncertainty should rank higher"); } #endregion #region T3-AC3: Test band assignment [Theory] [InlineData(0.9, 0.9, "HOT")] [InlineData(0.5, 0.5, "WARM")] [InlineData(0.1, 0.1, "COLD")] public void BandAssignment_MapsCorrectly_BasedOnScore( double exploitPressure, double uncertainty, string expectedBand) { // Arrange var ranker = new UnknownRanker(); var unknown = new UnknownEntry { CveId = $"CVE-BAND-{expectedBand}", ExploitPressure = exploitPressure, Uncertainty = uncertainty, DetectedAt = DateTimeOffset.UtcNow }; // Act var ranked = ranker.Rank(unknown); // Assert ranked.Band.Should().Be(expectedBand); } [Fact] public void BandThresholds_AreWellDefined() { // Arrange - Verify thresholds per sprint spec var ranker = new UnknownRanker(); // Act & Assert // HOT: score >= 0.7 var hotEntry = new UnknownEntry { CveId = "CVE-HOT", ExploitPressure = 0.85, Uncertainty = 0.85, DetectedAt = DateTimeOffset.UtcNow }; ranker.Rank(hotEntry).Band.Should().Be("HOT"); // WARM: 0.3 <= score < 0.7 var warmEntry = new UnknownEntry { CveId = "CVE-WARM", ExploitPressure = 0.5, Uncertainty = 0.5, DetectedAt = DateTimeOffset.UtcNow }; ranker.Rank(warmEntry).Band.Should().Be("WARM"); // COLD: score < 0.3 var coldEntry = new UnknownEntry { CveId = "CVE-COLD", ExploitPressure = 0.15, Uncertainty = 0.15, DetectedAt = DateTimeOffset.UtcNow }; ranker.Rank(coldEntry).Band.Should().Be("COLD"); } #endregion #region T3-AC4: Test escalation triggers rescan [Fact] public void Escalation_MovesBandToHot() { // Arrange var unknown = new UnknownEntry { CveId = "CVE-ESCALATE-001", ExploitPressure = 0.3, Uncertainty = 0.3, DetectedAt = DateTimeOffset.UtcNow, Band = "WARM" }; // Act var escalated = unknown.Escalate("Urgent customer request"); // Assert escalated.Band.Should().Be("HOT"); escalated.EscalatedAt.Should().NotBeNull(); escalated.EscalationReason.Should().Be("Urgent customer request"); } [Fact] public void Escalation_SetsRescanFlag() { // Arrange var unknown = new UnknownEntry { CveId = "CVE-RESCAN-001", Band = "COLD", DetectedAt = DateTimeOffset.UtcNow }; // Act var escalated = unknown.Escalate("New exploit discovered"); // Assert escalated.RequiresRescan.Should().BeTrue(); } #endregion #region T3-AC5: Test resolution updates status [Theory] [InlineData("matched", "RESOLVED")] [InlineData("not_applicable", "RESOLVED")] [InlineData("deferred", "DEFERRED")] public void Resolution_UpdatesStatus_Correctly(string resolution, string expectedStatus) { // Arrange var unknown = new UnknownEntry { CveId = "CVE-RESOLVE-001", Band = "HOT", DetectedAt = DateTimeOffset.UtcNow, Status = "OPEN" }; // Act var resolved = unknown.Resolve(resolution, "Test resolution"); // Assert resolved.Status.Should().Be(expectedStatus); resolved.ResolvedAt.Should().NotBeNull(); resolved.ResolutionNote.Should().Be("Test resolution"); } [Fact] public void Resolution_RecordsResolutionType() { // Arrange var unknown = new UnknownEntry { CveId = "CVE-RESOLUTION-TYPE", Band = "WARM", DetectedAt = DateTimeOffset.UtcNow, Status = "OPEN" }; // Act var resolved = unknown.Resolve("matched", "Found in OSV feed"); // Assert resolved.ResolutionType.Should().Be("matched"); } #endregion #region T3-AC6: Test band transitions [Fact] public void BandTransition_IsTracked_OnRerank() { // Arrange var ranker = new UnknownRanker(); var unknown = new UnknownEntry { CveId = "CVE-TRANSITION-001", ExploitPressure = 0.3, Uncertainty = 0.3, DetectedAt = DateTimeOffset.UtcNow.AddDays(-7), Band = "COLD" }; // Update pressure (simulating new exploit info) unknown = unknown with { ExploitPressure = 0.9 }; // Act var reranked = ranker.Rank(unknown); // Assert reranked.Band.Should().NotBe("COLD"); reranked.PreviousBand.Should().Be("COLD"); reranked.BandTransitionAt.Should().NotBeNull(); } [Fact] public void BandTransition_RecordsHistory() { // Arrange var unknown = new UnknownEntry { CveId = "CVE-HISTORY-001", Band = "COLD", DetectedAt = DateTimeOffset.UtcNow.AddDays(-30), BandHistory = new List() }; // Act - Simulate transition unknown = unknown.RecordBandTransition("COLD", "WARM", "Score increased"); unknown = unknown.RecordBandTransition("WARM", "HOT", "Escalated"); // Assert unknown.BandHistory.Should().HaveCount(2); unknown.BandHistory[0].FromBand.Should().Be("COLD"); unknown.BandHistory[0].ToBand.Should().Be("WARM"); unknown.BandHistory[1].FromBand.Should().Be("WARM"); unknown.BandHistory[1].ToBand.Should().Be("HOT"); } #endregion #region Helper Classes /// /// Unknown entry model for tests. /// public sealed record UnknownEntry { public string CveId { get; init; } = string.Empty; public string? Package { get; init; } public DateTimeOffset DetectedAt { get; init; } public string? ScanId { get; init; } public string? SourceFeed { get; init; } public double ExploitPressure { get; init; } public double Uncertainty { get; init; } public string Band { get; init; } = "COLD"; public string Status { get; init; } = "OPEN"; public DateTimeOffset? EscalatedAt { get; init; } public string? EscalationReason { get; init; } public bool RequiresRescan { get; init; } public DateTimeOffset? ResolvedAt { get; init; } public string? ResolutionType { get; init; } public string? ResolutionNote { get; init; } public string? PreviousBand { get; init; } public DateTimeOffset? BandTransitionAt { get; init; } public List BandHistory { get; init; } = new(); public UnknownEntry Escalate(string reason) { return this with { Band = "HOT", EscalatedAt = DateTimeOffset.UtcNow, EscalationReason = reason, RequiresRescan = true, PreviousBand = Band, BandTransitionAt = DateTimeOffset.UtcNow }; } public UnknownEntry Resolve(string resolution, string note) { var status = resolution == "deferred" ? "DEFERRED" : "RESOLVED"; return this with { Status = status, ResolvedAt = DateTimeOffset.UtcNow, ResolutionType = resolution, ResolutionNote = note }; } public UnknownEntry RecordBandTransition(string fromBand, string toBand, string reason) { var history = new List(BandHistory) { new(fromBand, toBand, DateTimeOffset.UtcNow, reason) }; return this with { Band = toBand, PreviousBand = fromBand, BandTransitionAt = DateTimeOffset.UtcNow, BandHistory = history }; } } public sealed record BandHistoryEntry( string FromBand, string ToBand, DateTimeOffset TransitionAt, string Reason); /// /// Ranked unknown result. /// public sealed record RankedUnknown( string CveId, double Score, string Band, string? PreviousBand = null, DateTimeOffset? BandTransitionAt = null); /// /// Simple 2-factor ranker for unknowns. /// Uses: Uncertainty + Exploit Pressure (per advisory spec) /// public sealed class UnknownRanker { private const double HotThreshold = 0.7; private const double WarmThreshold = 0.3; public RankedUnknown Rank(UnknownEntry entry) { // 2-factor model: simple average of uncertainty and exploit pressure var score = (entry.Uncertainty + entry.ExploitPressure) / 2.0; var band = score switch { >= HotThreshold => "HOT", >= WarmThreshold => "WARM", _ => "COLD" }; var previousBand = entry.Band != band ? entry.Band : null; var transitionAt = previousBand != null ? DateTimeOffset.UtcNow : (DateTimeOffset?)null; return new RankedUnknown( entry.CveId, score, band, previousBand, transitionAt); } } #endregion }