// -----------------------------------------------------------------------------
// 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
}