partly or unimplemented features - now implemented

This commit is contained in:
master
2026-02-09 08:53:51 +02:00
parent 1bf6bbf395
commit 4bdc298ec1
674 changed files with 90194 additions and 2271 deletions

View File

@@ -0,0 +1,535 @@
// Licensed to StellaOps under the BUSL-1.1 license.
using FluentAssertions;
using StellaOps.VexLens.Consensus;
using StellaOps.VexLens.Models;
using StellaOps.VexLens.Trust;
using Xunit;
namespace StellaOps.VexLens.Tests.Consensus;
/// <summary>
/// Truth table tests for VEX lattice merge correctness.
/// Validates all combinations of VEX status pairs produce correct merge results.
/// </summary>
[Trait("Category", "Unit")]
[Trait("Feature", "VexLattice")]
public class VexLatticeTruthTableTests
{
private readonly VexConsensusEngine _engine;
private readonly ConsensusConfiguration _config;
public VexLatticeTruthTableTests()
{
_config = VexConsensusEngine.CreateDefaultConfiguration();
_engine = new VexConsensusEngine(_config);
}
#region Lattice Order Truth Table
/// <summary>
/// Verifies the lattice order: Affected < UnderInvestigation < Fixed < NotAffected
/// </summary>
[Theory]
[InlineData(VexStatus.Affected, 0)]
[InlineData(VexStatus.UnderInvestigation, 1)]
[InlineData(VexStatus.Fixed, 2)]
[InlineData(VexStatus.NotAffected, 3)]
public void StatusLattice_OrderIsCorrect(VexStatus status, int expectedOrder)
{
// Act
var order = _config.StatusLattice.StatusOrder[status];
// Assert
order.Should().Be(expectedOrder);
}
[Fact]
public void StatusLattice_BottomIsAffected()
{
_config.StatusLattice.BottomStatus.Should().Be(VexStatus.Affected);
}
[Fact]
public void StatusLattice_TopIsNotAffected()
{
_config.StatusLattice.TopStatus.Should().Be(VexStatus.NotAffected);
}
#endregion
#region Two-Statement Lattice Merge Truth Table
/// <summary>
/// Complete truth table for lattice consensus with two statements.
/// Expected behavior: lattice consensus selects the most conservative (lowest) status.
/// </summary>
[Theory]
// Same status pairs - should return that status
[InlineData(VexStatus.Affected, VexStatus.Affected, VexStatus.Affected)]
[InlineData(VexStatus.UnderInvestigation, VexStatus.UnderInvestigation, VexStatus.UnderInvestigation)]
[InlineData(VexStatus.Fixed, VexStatus.Fixed, VexStatus.Fixed)]
[InlineData(VexStatus.NotAffected, VexStatus.NotAffected, VexStatus.NotAffected)]
// Affected vs others - Affected always wins (most conservative)
[InlineData(VexStatus.Affected, VexStatus.UnderInvestigation, VexStatus.Affected)]
[InlineData(VexStatus.Affected, VexStatus.Fixed, VexStatus.Affected)]
[InlineData(VexStatus.Affected, VexStatus.NotAffected, VexStatus.Affected)]
// UnderInvestigation vs Fixed/NotAffected - UnderInvestigation wins
[InlineData(VexStatus.UnderInvestigation, VexStatus.Fixed, VexStatus.UnderInvestigation)]
[InlineData(VexStatus.UnderInvestigation, VexStatus.NotAffected, VexStatus.UnderInvestigation)]
// Fixed vs NotAffected - Fixed wins (more conservative)
[InlineData(VexStatus.Fixed, VexStatus.NotAffected, VexStatus.Fixed)]
// Reverse order to verify commutativity
[InlineData(VexStatus.UnderInvestigation, VexStatus.Affected, VexStatus.Affected)]
[InlineData(VexStatus.Fixed, VexStatus.Affected, VexStatus.Affected)]
[InlineData(VexStatus.NotAffected, VexStatus.Affected, VexStatus.Affected)]
[InlineData(VexStatus.Fixed, VexStatus.UnderInvestigation, VexStatus.UnderInvestigation)]
[InlineData(VexStatus.NotAffected, VexStatus.UnderInvestigation, VexStatus.UnderInvestigation)]
[InlineData(VexStatus.NotAffected, VexStatus.Fixed, VexStatus.Fixed)]
public async Task LatticeConsensus_TwoStatements_SelectsMostConservative(
VexStatus status1,
VexStatus status2,
VexStatus expectedConsensus)
{
// Arrange
var now = DateTimeOffset.UtcNow;
var statements = new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", status1, 0.5, now),
CreateWeightedStatement("stmt-2", status2, 0.5, now)
};
var request = CreateRequest("CVE-2024-1234", "pkg:test@1.0", statements, ConsensusMode.Lattice);
// Act
var result = await _engine.ComputeConsensusAsync(request);
// Assert
result.ConsensusStatus.Should().Be(expectedConsensus,
because: $"lattice merge of {status1} and {status2} should yield {expectedConsensus}");
}
/// <summary>
/// Verifies lattice consensus is commutative: merge(A, B) == merge(B, A)
/// </summary>
[Theory]
[InlineData(VexStatus.Affected, VexStatus.NotAffected)]
[InlineData(VexStatus.UnderInvestigation, VexStatus.Fixed)]
[InlineData(VexStatus.Fixed, VexStatus.Affected)]
[InlineData(VexStatus.NotAffected, VexStatus.UnderInvestigation)]
public async Task LatticeConsensus_IsCommutative(VexStatus status1, VexStatus status2)
{
// Arrange
var now = DateTimeOffset.UtcNow;
var request1 = CreateRequest("CVE-2024-1234", "pkg:test@1.0", new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", status1, 0.5, now),
CreateWeightedStatement("stmt-2", status2, 0.5, now)
}, ConsensusMode.Lattice);
var request2 = CreateRequest("CVE-2024-1234", "pkg:test@1.0", new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", status2, 0.5, now),
CreateWeightedStatement("stmt-2", status1, 0.5, now)
}, ConsensusMode.Lattice);
// Act
var result1 = await _engine.ComputeConsensusAsync(request1);
var result2 = await _engine.ComputeConsensusAsync(request2);
// Assert
result1.ConsensusStatus.Should().Be(result2.ConsensusStatus,
because: "lattice merge should be commutative");
}
/// <summary>
/// Verifies lattice consensus is associative: merge(merge(A, B), C) == merge(A, merge(B, C))
/// </summary>
[Theory]
[InlineData(VexStatus.Affected, VexStatus.Fixed, VexStatus.NotAffected, VexStatus.Affected)]
[InlineData(VexStatus.UnderInvestigation, VexStatus.Fixed, VexStatus.NotAffected, VexStatus.UnderInvestigation)]
[InlineData(VexStatus.Fixed, VexStatus.NotAffected, VexStatus.NotAffected, VexStatus.Fixed)]
public async Task LatticeConsensus_IsAssociative(
VexStatus status1,
VexStatus status2,
VexStatus status3,
VexStatus expected)
{
// Arrange
var now = DateTimeOffset.UtcNow;
var request = CreateRequest("CVE-2024-1234", "pkg:test@1.0", new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", status1, 0.33, now),
CreateWeightedStatement("stmt-2", status2, 0.33, now),
CreateWeightedStatement("stmt-3", status3, 0.34, now)
}, ConsensusMode.Lattice);
// Act
var result = await _engine.ComputeConsensusAsync(request);
// Assert
result.ConsensusStatus.Should().Be(expected,
because: "lattice merge should be associative");
}
/// <summary>
/// Verifies lattice consensus is idempotent: merge(A, A) == A
/// </summary>
[Theory]
[InlineData(VexStatus.Affected)]
[InlineData(VexStatus.UnderInvestigation)]
[InlineData(VexStatus.Fixed)]
[InlineData(VexStatus.NotAffected)]
public async Task LatticeConsensus_IsIdempotent(VexStatus status)
{
// Arrange
var now = DateTimeOffset.UtcNow;
var request = CreateRequest("CVE-2024-1234", "pkg:test@1.0", new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", status, 0.5, now),
CreateWeightedStatement("stmt-2", status, 0.5, now)
}, ConsensusMode.Lattice);
// Act
var result = await _engine.ComputeConsensusAsync(request);
// Assert
result.ConsensusStatus.Should().Be(status,
because: "lattice merge should be idempotent");
}
#endregion
#region Weighted Vote Truth Table
/// <summary>
/// Truth table for weighted vote consensus - majority wins.
/// </summary>
[Theory]
// Clear majorities
[InlineData(0.7, VexStatus.Affected, 0.3, VexStatus.NotAffected, VexStatus.Affected)]
[InlineData(0.3, VexStatus.Affected, 0.7, VexStatus.NotAffected, VexStatus.NotAffected)]
[InlineData(0.6, VexStatus.Fixed, 0.4, VexStatus.UnderInvestigation, VexStatus.Fixed)]
// Ties resolved by weight order
[InlineData(0.5, VexStatus.Affected, 0.5, VexStatus.NotAffected, VexStatus.Affected)]
public async Task WeightedVote_SelectsStatusWithHighestTotalWeight(
double weight1,
VexStatus status1,
double weight2,
VexStatus status2,
VexStatus expected)
{
// Arrange
var now = DateTimeOffset.UtcNow;
var request = CreateRequest("CVE-2024-1234", "pkg:test@1.0", new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", status1, weight1, now),
CreateWeightedStatement("stmt-2", status2, weight2, now)
}, ConsensusMode.WeightedVote);
// Act
var result = await _engine.ComputeConsensusAsync(request);
// Assert
result.ConsensusStatus.Should().Be(expected);
}
/// <summary>
/// Weighted vote with multiple statements per status.
/// </summary>
[Theory]
// Two affected (0.3+0.3=0.6) vs one not_affected (0.4) -> affected wins
[InlineData(VexStatus.Affected)]
public async Task WeightedVote_AggregatesWeightsByStatus(VexStatus expected)
{
// Arrange
var now = DateTimeOffset.UtcNow;
var request = CreateRequest("CVE-2024-1234", "pkg:test@1.0", new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", VexStatus.Affected, 0.3, now),
CreateWeightedStatement("stmt-2", VexStatus.Affected, 0.3, now),
CreateWeightedStatement("stmt-3", VexStatus.NotAffected, 0.4, now)
}, ConsensusMode.WeightedVote);
// Act
var result = await _engine.ComputeConsensusAsync(request);
// Assert
result.ConsensusStatus.Should().Be(expected);
}
#endregion
#region Highest Weight Truth Table
/// <summary>
/// Truth table for highest weight consensus - single highest weight wins.
/// </summary>
[Theory]
[InlineData(0.9, VexStatus.NotAffected, 0.1, VexStatus.Affected, VexStatus.NotAffected)]
[InlineData(0.1, VexStatus.NotAffected, 0.9, VexStatus.Affected, VexStatus.Affected)]
[InlineData(0.5, VexStatus.Fixed, 0.4, VexStatus.UnderInvestigation, VexStatus.Fixed)]
public async Task HighestWeight_SelectsStatementWithMaxWeight(
double weight1,
VexStatus status1,
double weight2,
VexStatus status2,
VexStatus expected)
{
// Arrange
var now = DateTimeOffset.UtcNow;
var request = CreateRequest("CVE-2024-1234", "pkg:test@1.0", new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", status1, weight1, now),
CreateWeightedStatement("stmt-2", status2, weight2, now)
}, ConsensusMode.HighestWeight);
// Act
var result = await _engine.ComputeConsensusAsync(request);
// Assert
result.ConsensusStatus.Should().Be(expected);
}
#endregion
#region Conflict Detection Truth Table
/// <summary>
/// Verifies conflicts are detected when statements disagree.
/// </summary>
[Theory]
[InlineData(VexStatus.Affected, VexStatus.NotAffected, true)]
[InlineData(VexStatus.Affected, VexStatus.Affected, false)]
[InlineData(VexStatus.Fixed, VexStatus.UnderInvestigation, true)]
[InlineData(VexStatus.NotAffected, VexStatus.NotAffected, false)]
public async Task ConflictDetection_IdentifiesDisagreements(
VexStatus status1,
VexStatus status2,
bool expectConflict)
{
// Arrange
var now = DateTimeOffset.UtcNow;
var request = CreateRequest("CVE-2024-1234", "pkg:test@1.0", new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", status1, 0.5, now),
CreateWeightedStatement("stmt-2", status2, 0.5, now)
}, ConsensusMode.Lattice);
// Act
var result = await _engine.ComputeConsensusAsync(request);
// Assert
if (expectConflict)
{
result.Conflicts.Should().NotBeNullOrEmpty("conflicts should be detected");
}
else
{
(result.Conflicts ?? Array.Empty<ConsensusConflict>()).Should().BeEmpty("no conflicts expected");
}
}
#endregion
#region Outcome Classification Truth Table
/// <summary>
/// Verifies outcome is classified correctly based on agreement.
/// </summary>
[Theory]
[InlineData(VexStatus.Affected, VexStatus.Affected, ConsensusOutcome.Unanimous)]
[InlineData(VexStatus.NotAffected, VexStatus.NotAffected, ConsensusOutcome.Unanimous)]
public async Task OutcomeClassification_UnanimousWhenAllAgree(
VexStatus status1,
VexStatus status2,
ConsensusOutcome expected)
{
// Arrange
var now = DateTimeOffset.UtcNow;
var request = CreateRequest("CVE-2024-1234", "pkg:test@1.0", new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", status1, 0.5, now),
CreateWeightedStatement("stmt-2", status2, 0.5, now)
}, ConsensusMode.Lattice);
// Act
var result = await _engine.ComputeConsensusAsync(request);
// Assert
result.Outcome.Should().Be(expected);
}
[Fact]
public async Task OutcomeClassification_ConflictResolvedWhenDisagree()
{
// Arrange
var now = DateTimeOffset.UtcNow;
var request = CreateRequest("CVE-2024-1234", "pkg:test@1.0", new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", VexStatus.Affected, 0.5, now),
CreateWeightedStatement("stmt-2", VexStatus.NotAffected, 0.5, now)
}, ConsensusMode.Lattice);
// Act
var result = await _engine.ComputeConsensusAsync(request);
// Assert
result.Outcome.Should().Be(ConsensusOutcome.ConflictResolved);
}
#endregion
#region Edge Cases
[Fact]
public async Task SingleStatement_ReturnsItsStatus()
{
// Arrange
var now = DateTimeOffset.UtcNow;
var request = CreateRequest("CVE-2024-1234", "pkg:test@1.0", new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", VexStatus.Fixed, 0.8, now)
}, ConsensusMode.Lattice);
// Act
var result = await _engine.ComputeConsensusAsync(request);
// Assert
result.ConsensusStatus.Should().Be(VexStatus.Fixed);
result.Outcome.Should().Be(ConsensusOutcome.Unanimous);
}
[Fact]
public async Task EmptyStatements_ReturnsNoDataOutcome()
{
// Arrange
var request = CreateRequest("CVE-2024-1234", "pkg:test@1.0",
new List<WeightedStatement>(), ConsensusMode.Lattice);
// Act
var result = await _engine.ComputeConsensusAsync(request);
// Assert
result.Outcome.Should().Be(ConsensusOutcome.NoData);
}
[Fact]
public async Task AllBelowThreshold_ReturnsNoData()
{
// Arrange
var now = DateTimeOffset.UtcNow;
var request = CreateRequest("CVE-2024-1234", "pkg:test@1.0", new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", VexStatus.Affected, 0.05, now), // Below 0.1 threshold
CreateWeightedStatement("stmt-2", VexStatus.Fixed, 0.05, now)
}, ConsensusMode.Lattice);
// Act
var result = await _engine.ComputeConsensusAsync(request);
// Assert
result.Outcome.Should().Be(ConsensusOutcome.NoData);
}
#endregion
#region Determinism Tests
[Fact]
public async Task Consensus_IsDeterministic_SameInputSameOutput()
{
// Arrange
var now = DateTimeOffset.UtcNow;
var request = CreateRequest("CVE-2024-1234", "pkg:test@1.0", new List<WeightedStatement>
{
CreateWeightedStatement("stmt-1", VexStatus.Affected, 0.6, now),
CreateWeightedStatement("stmt-2", VexStatus.NotAffected, 0.4, now)
}, ConsensusMode.Lattice);
// Act
var result1 = await _engine.ComputeConsensusAsync(request);
var result2 = await _engine.ComputeConsensusAsync(request);
// Assert
result1.ConsensusStatus.Should().Be(result2.ConsensusStatus);
result1.ConfidenceScore.Should().Be(result2.ConfidenceScore);
result1.Outcome.Should().Be(result2.Outcome);
}
#endregion
#region Helpers
private static WeightedStatement CreateWeightedStatement(
string id,
VexStatus status,
double weight,
DateTimeOffset timestamp)
{
var statement = new NormalizedStatement(
StatementId: id,
VulnerabilityId: "CVE-2024-1234",
VulnerabilityAliases: null,
Product: new NormalizedProduct("pkg:test@1.0", "Test", "1.0", "pkg:test@1.0", null, null),
Status: status,
StatusNotes: null,
Justification: status == VexStatus.NotAffected ? VexJustification.VulnerableCodeNotPresent : null,
ImpactStatement: null,
ActionStatement: null,
ActionStatementTimestamp: null,
Versions: null,
Subcomponents: null,
FirstSeen: timestamp,
LastSeen: timestamp);
var breakdown = new TrustWeightBreakdown(
IssuerWeight: weight * 0.4,
SignatureWeight: weight * 0.2,
FreshnessWeight: weight * 0.2,
SourceFormatWeight: weight * 0.1,
StatusSpecificityWeight: weight * 0.1,
CustomWeight: 0.0);
var trustWeight = new TrustWeightResult(
Statement: statement,
Weight: weight,
Breakdown: breakdown,
Factors: new List<TrustWeightFactor>(),
Warnings: new List<string>());
return new WeightedStatement(
Statement: statement,
Weight: trustWeight,
Issuer: new VexIssuer("issuer-1", "Test Issuer", IssuerCategory.Community, TrustTier.Trusted, null),
SourceDocumentId: "doc-1");
}
private static VexConsensusRequest CreateRequest(
string vulnerabilityId,
string productKey,
IReadOnlyList<WeightedStatement> statements,
ConsensusMode mode)
{
var policy = new ConsensusPolicy(
Mode: mode,
MinimumWeightThreshold: 0.1,
ConflictThreshold: 0.3,
RequireJustificationForNotAffected: false,
PreferredIssuers: null);
var context = new ConsensusContext(
TenantId: "test-tenant",
EvaluationTime: DateTimeOffset.UtcNow,
Policy: policy);
return new VexConsensusRequest(
VulnerabilityId: vulnerabilityId,
ProductKey: productKey,
Statements: statements,
Context: context);
}
#endregion
}