Add integration tests for Proof Chain and Reachability workflows
- Implement ProofChainTestFixture for PostgreSQL-backed integration tests. - Create StellaOps.Integration.ProofChain project with necessary dependencies. - Add ReachabilityIntegrationTests to validate call graph extraction and reachability analysis. - Introduce ReachabilityTestFixture for managing corpus and fixture paths. - Establish StellaOps.Integration.Reachability project with required references. - Develop UnknownsWorkflowTests to cover the unknowns lifecycle: detection, ranking, escalation, and resolution. - Create StellaOps.Integration.Unknowns project with dependencies for unknowns workflow.
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the unknowns registry workflow.
|
||||
/// Tests the complete lifecycle: detection → ranking → band assignment
|
||||
/// → escalation → resolution.
|
||||
/// </summary>
|
||||
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<BandHistoryEntry>()
|
||||
};
|
||||
|
||||
// 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
|
||||
|
||||
/// <summary>
|
||||
/// Unknown entry model for tests.
|
||||
/// </summary>
|
||||
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<BandHistoryEntry> 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<BandHistoryEntry>(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);
|
||||
|
||||
/// <summary>
|
||||
/// Ranked unknown result.
|
||||
/// </summary>
|
||||
public sealed record RankedUnknown(
|
||||
string CveId,
|
||||
double Score,
|
||||
string Band,
|
||||
string? PreviousBand = null,
|
||||
DateTimeOffset? BandTransitionAt = null);
|
||||
|
||||
/// <summary>
|
||||
/// Simple 2-factor ranker for unknowns.
|
||||
/// Uses: Uncertainty + Exploit Pressure (per advisory spec)
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user