save progress

This commit is contained in:
StellaOps Bot
2026-01-03 12:41:57 +02:00
parent 83c37243e0
commit d486d41a48
48 changed files with 7174 additions and 1086 deletions

View File

@@ -1,851 +0,0 @@
// -----------------------------------------------------------------------------
// VexLensTruthTableTests.cs
// Sprint: SPRINT_20251229_004_003_BE_vexlens_truth_tables
// Tasks: VTT-001 through VTT-009
// Comprehensive truth table tests for VexLens lattice merge operations
// -----------------------------------------------------------------------------
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace StellaOps.VexLens.Tests.Consensus;
/// <summary>
/// Systematic truth table tests for VexLens consensus engine.
/// Verifies lattice merge correctness, conflict detection, and determinism.
///
/// VEX Status Lattice:
/// ┌─────────┐
/// │ fixed │ (terminal)
/// └────▲────┘
/// │
/// ┌───────────────┼───────────────┐
/// │ │ │
/// ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
/// │not_affected│ │ affected │ │ (tie) │
/// └─────▲─────┘ └─────▲─────┘ └───────────┘
/// │ │
/// └───────┬───────┘
/// │
/// ┌───────▼───────┐
/// │under_investigation│
/// └───────▲───────┘
/// │
/// ┌───────▼───────┐
/// │ unknown │ (bottom)
/// └───────────────┘
/// </summary>
[Trait("Category", "Determinism")]
[Trait("Category", "Golden")]
public class VexLensTruthTableTests
{
private static readonly JsonSerializerOptions CanonicalOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
#region Single Issuer Identity Tests (VTT-001 to VTT-005)
/// <summary>
/// Test data for single issuer identity cases.
/// A single VEX statement should return its status unchanged.
/// </summary>
public static TheoryData<string, VexStatus, VexStatus> SingleIssuerCases => new()
{
{ "TT-001", VexStatus.Unknown, VexStatus.Unknown },
{ "TT-002", VexStatus.UnderInvestigation, VexStatus.UnderInvestigation },
{ "TT-003", VexStatus.Affected, VexStatus.Affected },
{ "TT-004", VexStatus.NotAffected, VexStatus.NotAffected },
{ "TT-005", VexStatus.Fixed, VexStatus.Fixed }
};
[Theory]
[MemberData(nameof(SingleIssuerCases))]
public void SingleIssuer_ReturnsIdentity(string testId, VexStatus input, VexStatus expected)
{
// Arrange
var statement = CreateStatement("issuer-a", input);
var statements = new[] { statement };
// Act
var result = ComputeConsensus(statements);
// Assert
result.Status.Should().Be(expected, because: $"{testId}: single issuer should return identity");
result.Conflicts.Should().BeEmpty(because: "single issuer cannot have conflicts");
result.StatementCount.Should().Be(1);
result.ConfidenceScore.Should().BeGreaterOrEqualTo(0.8m);
}
#endregion
#region Two Issuer Merge Tests (VTT-010 to VTT-019)
/// <summary>
/// Test data for two issuers at the same trust tier.
/// Tests lattice join operation and conflict detection.
///
/// EDGE CASE: Affected and NotAffected are at the SAME lattice level.
/// When both appear at the same trust tier, this creates a conflict.
/// The system conservatively chooses 'affected' and records the conflict.
///
/// EDGE CASE: Fixed is lattice terminal (top).
/// Any statement with 'fixed' status will win, regardless of other statuses.
///
/// EDGE CASE: Unknown is lattice bottom.
/// Unknown never wins when merged with any other status.
/// </summary>
public static TheoryData<string, VexStatus, VexStatus, VexStatus, bool> TwoIssuerMergeCases => new()
{
// Both unknown → unknown (lattice bottom)
{ "TT-010", VexStatus.Unknown, VexStatus.Unknown, VexStatus.Unknown, false },
// Unknown merges up the lattice
{ "TT-011", VexStatus.Unknown, VexStatus.Affected, VexStatus.Affected, false },
{ "TT-012", VexStatus.Unknown, VexStatus.NotAffected, VexStatus.NotAffected, false },
// CONFLICT: Affected vs NotAffected at same level (must record)
{ "TT-013", VexStatus.Affected, VexStatus.NotAffected, VexStatus.Affected, true },
// Fixed wins (lattice top)
{ "TT-014", VexStatus.Affected, VexStatus.Fixed, VexStatus.Fixed, false },
{ "TT-015", VexStatus.NotAffected, VexStatus.Fixed, VexStatus.Fixed, false },
// Under investigation merges up
{ "TT-016", VexStatus.UnderInvestigation, VexStatus.Affected, VexStatus.Affected, false },
{ "TT-017", VexStatus.UnderInvestigation, VexStatus.NotAffected, VexStatus.NotAffected, false },
// Same status → same status
{ "TT-018", VexStatus.Affected, VexStatus.Affected, VexStatus.Affected, false },
{ "TT-019", VexStatus.NotAffected, VexStatus.NotAffected, VexStatus.NotAffected, false }
};
[Theory]
[MemberData(nameof(TwoIssuerMergeCases))]
public void TwoIssuers_SameTier_MergesCorrectly(
string testId,
VexStatus statusA,
VexStatus statusB,
VexStatus expected,
bool expectConflict)
{
// Arrange
var statementA = CreateStatement("issuer-a", statusA, trustTier: 90);
var statementB = CreateStatement("issuer-b", statusB, trustTier: 90);
var statements = new[] { statementA, statementB };
// Act
var result = ComputeConsensus(statements);
// Assert
result.Status.Should().Be(expected, because: $"{testId}: lattice merge should produce expected status");
result.Conflicts.Any().Should().Be(expectConflict, because: $"{testId}: conflict detection must be accurate");
result.StatementCount.Should().Be(2);
if (expectConflict)
{
result.Conflicts.Should().HaveCount(1, because: "should record the conflict");
result.ConflictCount.Should().Be(1);
}
}
#endregion
#region Trust Tier Precedence Tests (VTT-020 to VTT-022)
/// <summary>
/// Test data for trust tier precedence.
/// Higher tier statements should take precedence over lower tier.
///
/// EDGE CASE: Trust tier filtering happens BEFORE lattice merge.
/// Only the highest tier statements are considered for merging.
/// Lower tier statements are completely ignored, even if they would
/// produce a different result via lattice merge.
///
/// EDGE CASE: Trust tier hierarchy (Distro=100, Vendor=90, Community=50).
/// Distro-level security trackers have absolute authority over vendor advisories.
/// This ensures that distribution-specific backports and patches are respected.
///
/// EDGE CASE: When high tier says 'unknown', low tier can provide information.
/// If the highest tier has no data (unknown), the next tier is consulted.
/// This cascading behavior prevents data loss when authoritative sources
/// haven't analyzed a CVE yet.
/// </summary>
public static TheoryData<string, VexStatus, int, VexStatus, int, VexStatus> TrustTierCases => new()
{
// High tier (100) beats low tier (50)
{ "TT-020", VexStatus.Affected, 100, VexStatus.NotAffected, 50, VexStatus.Affected },
{ "TT-021", VexStatus.NotAffected, 100, VexStatus.Affected, 50, VexStatus.NotAffected },
// Low tier fills in when high tier is unknown
{ "TT-022", VexStatus.Unknown, 100, VexStatus.Affected, 50, VexStatus.Affected }
};
[Theory]
[MemberData(nameof(TrustTierCases))]
public void TrustTier_HigherPrecedence_WinsConflicts(
string testId,
VexStatus highStatus,
int highTier,
VexStatus lowStatus,
int lowTier,
VexStatus expected)
{
// Arrange
var highTierStmt = CreateStatement("high-tier-issuer", highStatus, trustTier: highTier);
var lowTierStmt = CreateStatement("low-tier-issuer", lowStatus, trustTier: lowTier);
var statements = new[] { highTierStmt, lowTierStmt };
// Act
var result = ComputeConsensus(statements);
// Assert
result.Status.Should().Be(expected, because: $"{testId}: higher trust tier should win");
result.StatementCount.Should().Be(2);
}
#endregion
#region Justification Impact Tests (VTT-030 to VTT-033)
/// <summary>
/// Test data for justification impact on confidence scores.
/// Justifications affect confidence but not status.
///
/// EDGE CASE: Justifications NEVER change the consensus status.
/// They only modulate the confidence score. A well-justified 'not_affected'
/// is still 'not_affected', just with higher confidence.
///
/// EDGE CASE: Justification hierarchy for not_affected:
/// 1. component_not_present (0.95+) - strongest, binary condition
/// 2. vulnerable_code_not_in_execute_path (0.90+) - requires code analysis
/// 3. inline_mitigations_already_exist (0.85+) - requires verification
///
/// EDGE CASE: Missing justification still has good confidence.
/// An explicit 'affected' statement without justification is still 0.80+
/// because the issuer made a clear determination.
///
/// EDGE CASE: Multiple justifications (future).
/// If multiple statements have different justifications, the strongest
/// justification determines the final confidence score.
/// </summary>
public static TheoryData<string, VexStatus, string?, decimal> JustificationConfidenceCases => new()
{
// Strong justifications → high confidence
{ "TT-030", VexStatus.NotAffected, "component_not_present", 0.95m },
{ "TT-031", VexStatus.NotAffected, "vulnerable_code_not_in_execute_path", 0.90m },
{ "TT-032", VexStatus.NotAffected, "inline_mitigations_already_exist", 0.85m },
// No justification → still high confidence (explicit statement)
{ "TT-033", VexStatus.Affected, null, 0.80m }
};
[Theory]
[MemberData(nameof(JustificationConfidenceCases))]
public void Justification_AffectsConfidence_NotStatus(
string testId,
VexStatus status,
string? justification,
decimal minConfidence)
{
// Arrange
var statement = CreateStatement("issuer-a", status, justification: justification);
var statements = new[] { statement };
// Act
var result = ComputeConsensus(statements);
// Assert
result.Status.Should().Be(status, because: $"{testId}: justification should not change status");
result.ConfidenceScore.Should().BeGreaterOrEqualTo(minConfidence, because: $"{testId}: justification impacts confidence");
}
#endregion
#region Determinism Tests (VTT-006)
/// <summary>
/// EDGE CASE: Determinism is CRITICAL for reproducible vulnerability assessment.
/// Same inputs must ALWAYS produce byte-for-byte identical outputs.
/// Any non-determinism breaks audit trails and makes replay impossible.
///
/// EDGE CASE: Statement order independence.
/// The consensus algorithm must be commutative. Processing statements
/// in different orders must yield the same result. This is tested by
/// shuffling statement arrays and verifying identical consensus.
///
/// EDGE CASE: Floating point determinism.
/// Confidence scores use decimal (not double/float) to ensure
/// bit-exact reproducibility across platforms and CPU architectures.
///
/// EDGE CASE: Hash-based conflict detection must be stable.
/// When recording conflicts, issuer IDs are sorted lexicographically
/// to ensure deterministic JSON serialization.
///
/// EDGE CASE: Timestamp normalization.
/// All timestamps are normalized to UTC ISO-8601 format to prevent
/// timezone-related non-determinism in serialized output.
/// </summary>
[Fact]
public void SameInputs_ProducesIdenticalOutput_Across10Iterations()
{
// Arrange: Create conflicting statements
var statements = new[]
{
CreateStatement("vendor-a", VexStatus.Affected, trustTier: 90),
CreateStatement("vendor-b", VexStatus.NotAffected, trustTier: 90),
CreateStatement("distro-security", VexStatus.Fixed, trustTier: 100)
};
var results = new List<string>();
// Act: Compute consensus 10 times
for (int i = 0; i < 10; i++)
{
var result = ComputeConsensus(statements);
var canonical = JsonSerializer.Serialize(result, CanonicalOptions);
results.Add(canonical);
}
// Assert: All results should be byte-for-byte identical
results.Distinct().Should().HaveCount(1, because: "determinism: all iterations must produce identical JSON");
// Verify the result is fixed (highest tier + lattice top)
var finalResult = ComputeConsensus(statements);
finalResult.Status.Should().Be(VexStatus.Fixed, because: "fixed wins at lattice top");
}
[Fact]
public void StatementOrder_DoesNotAffect_ConsensusOutcome()
{
// Arrange: Same statements in different orders
var stmt1 = CreateStatement("issuer-1", VexStatus.Affected, trustTier: 90);
var stmt2 = CreateStatement("issuer-2", VexStatus.NotAffected, trustTier: 90);
var stmt3 = CreateStatement("issuer-3", VexStatus.UnderInvestigation, trustTier: 80);
var order1 = new[] { stmt1, stmt2, stmt3 };
var order2 = new[] { stmt3, stmt1, stmt2 };
var order3 = new[] { stmt2, stmt3, stmt1 };
// Act
var result1 = ComputeConsensus(order1);
var result2 = ComputeConsensus(order2);
var result3 = ComputeConsensus(order3);
// Assert: All should produce identical results
var json1 = JsonSerializer.Serialize(result1, CanonicalOptions);
var json2 = JsonSerializer.Serialize(result2, CanonicalOptions);
var json3 = JsonSerializer.Serialize(result3, CanonicalOptions);
json1.Should().Be(json2).And.Be(json3, because: "statement order must not affect consensus");
}
#endregion
#region Conflict Detection Tests (VTT-004)
/// <summary>
/// EDGE CASE: Conflict detection is not the same as disagreement.
/// A conflict occurs when same-tier issuers provide statuses at the SAME lattice level.
/// Example: Affected vs NotAffected = conflict (same level).
/// Example: UnderInvestigation vs Affected = no conflict (hierarchical).
///
/// EDGE CASE: Conflicts must be recorded with ALL participating issuers.
/// The consensus engine must track which issuers contributed to the conflict,
/// not just the ones that "lost" the merge. This is critical for audit trails.
///
/// EDGE CASE: N-way conflicts (3+ issuers with different views).
/// When three or more issuers at the same tier have different statuses,
/// the system uses lattice merge (affected wins) and records all conflicts.
///
/// EDGE CASE: Unanimous agreement = zero conflicts.
/// When all same-tier issuers agree, confidence increases to 0.95+
/// and the conflict array remains empty.
/// </summary>
[Fact]
public void ThreeWayConflict_RecordsAllDisagreements()
{
// Arrange: Three issuers at same tier with different assessments
var statements = new[]
{
CreateStatement("issuer-a", VexStatus.Affected, trustTier: 90),
CreateStatement("issuer-b", VexStatus.NotAffected, trustTier: 90),
CreateStatement("issuer-c", VexStatus.UnderInvestigation, trustTier: 90)
};
// Act
var result = ComputeConsensus(statements);
// Assert: Should record conflicts and use lattice merge
result.Status.Should().Be(VexStatus.Affected, because: "affected wins in lattice");
result.ConflictCount.Should().BeGreaterThan(0, because: "should detect conflicts");
result.Conflicts.Should().NotBeEmpty(because: "should record conflicting issuers");
}
[Fact]
public void NoConflict_WhenStatementsAgree()
{
// Arrange: All issuers agree
var statements = new[]
{
CreateStatement("issuer-a", VexStatus.NotAffected, trustTier: 90),
CreateStatement("issuer-b", VexStatus.NotAffected, trustTier: 90),
CreateStatement("issuer-c", VexStatus.NotAffected, trustTier: 90)
};
// Act
var result = ComputeConsensus(statements);
// Assert
result.Status.Should().Be(VexStatus.NotAffected);
result.Conflicts.Should().BeEmpty(because: "all issuers agree");
result.ConflictCount.Should().Be(0);
result.ConfidenceScore.Should().BeGreaterOrEqualTo(0.95m, because: "unanimous agreement increases confidence");
}
#endregion
#region Recorded Replay Tests (VTT-008)
/// <summary>
/// Seed cases for deterministic replay verification.
/// Each seed represents a real-world scenario that must produce stable results.
/// </summary>
public static TheoryData<string, VexStatement[], VexStatus> ReplaySeedCases => new()
{
// Seed 1: Distro disagrees with upstream (high tier wins)
{
"SEED-001",
new[]
{
CreateStatement("debian-security", VexStatus.Affected, trustTier: 100),
CreateStatement("npm-advisory", VexStatus.NotAffected, trustTier: 80)
},
VexStatus.Affected
},
// Seed 2: Three vendors agree on fix
{
"SEED-002",
new[]
{
CreateStatement("vendor-redhat", VexStatus.Fixed, trustTier: 90),
CreateStatement("vendor-ubuntu", VexStatus.Fixed, trustTier: 90),
CreateStatement("vendor-debian", VexStatus.Fixed, trustTier: 90)
},
VexStatus.Fixed
},
// Seed 3: Mixed signals (under investigation + affected → affected wins)
{
"SEED-003",
new[]
{
CreateStatement("researcher-a", VexStatus.UnderInvestigation, trustTier: 70),
CreateStatement("researcher-b", VexStatus.Affected, trustTier: 70),
CreateStatement("researcher-c", VexStatus.UnderInvestigation, trustTier: 70)
},
VexStatus.Affected
},
// Seed 4: Conflict between two high-tier vendors
{
"SEED-004",
new[]
{
CreateStatement("vendor-a", VexStatus.Affected, trustTier: 100),
CreateStatement("vendor-b", VexStatus.NotAffected, trustTier: 100)
},
VexStatus.Affected // Conservative: affected wins in conflict
},
// Seed 5: Low confidence unknown statements
{
"SEED-005",
new[]
{
CreateStatement("issuer-1", VexStatus.Unknown, trustTier: 50),
CreateStatement("issuer-2", VexStatus.Unknown, trustTier: 50),
CreateStatement("issuer-3", VexStatus.Unknown, trustTier: 50)
},
VexStatus.Unknown
},
// Seed 6: Fixed status overrides all lower statuses
{
"SEED-006",
new[]
{
CreateStatement("vendor-a", VexStatus.Affected, trustTier: 90),
CreateStatement("vendor-b", VexStatus.NotAffected, trustTier: 90),
CreateStatement("vendor-c", VexStatus.Fixed, trustTier: 90)
},
VexStatus.Fixed
},
// Seed 7: Single high-tier not_affected
{
"SEED-007",
new[]
{
CreateStatement("distro-maintainer", VexStatus.NotAffected, trustTier: 100, justification: "component_not_present")
},
VexStatus.NotAffected
},
// Seed 8: Investigation escalates to affected
{
"SEED-008",
new[]
{
CreateStatement("issuer-early", VexStatus.UnderInvestigation, trustTier: 90),
CreateStatement("issuer-update", VexStatus.Affected, trustTier: 90)
},
VexStatus.Affected
},
// Seed 9: All tiers present (distro > vendor > community)
{
"SEED-009",
new[]
{
CreateStatement("community", VexStatus.Affected, trustTier: 50),
CreateStatement("vendor", VexStatus.NotAffected, trustTier: 80),
CreateStatement("distro", VexStatus.Fixed, trustTier: 100)
},
VexStatus.Fixed
},
// Seed 10: Multiple affected statements (unanimous)
{
"SEED-010",
new[]
{
CreateStatement("nvd", VexStatus.Affected, trustTier: 85),
CreateStatement("github-advisory", VexStatus.Affected, trustTier: 85),
CreateStatement("snyk", VexStatus.Affected, trustTier: 85)
},
VexStatus.Affected
}
};
[Theory]
[MemberData(nameof(ReplaySeedCases))]
public void ReplaySeed_ProducesStableOutput_Across10Runs(
string seedId,
VexStatement[] statements,
VexStatus expectedStatus)
{
// Act: Run consensus 10 times
var results = new List<string>();
for (int i = 0; i < 10; i++)
{
var result = ComputeConsensus(statements);
var canonical = JsonSerializer.Serialize(result, CanonicalOptions);
results.Add(canonical);
}
// Assert: All 10 runs must produce byte-identical output
results.Distinct().Should().HaveCount(1, because: $"{seedId}: replay must be deterministic");
// Verify expected status
var finalResult = ComputeConsensus(statements);
finalResult.Status.Should().Be(expectedStatus, because: $"{seedId}: status regression check");
}
[Fact]
public void AllReplaySeeds_ExecuteWithinTimeLimit()
{
// Arrange: Collect all seed cases
var allSeeds = ReplaySeedCases.Select(data => (VexStatement[])data[1]).ToList();
// Act: Measure execution time
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
foreach (var statements in allSeeds)
{
_ = ComputeConsensus(statements);
}
stopwatch.Stop();
// Assert: All 10 seeds should complete in under 100ms
stopwatch.ElapsedMilliseconds.Should().BeLessThan(100, because: "replay tests must be fast");
}
#endregion
#region Golden Output Snapshot Tests (VTT-007)
/// <summary>
/// Test cases that have golden output snapshots for regression testing.
/// </summary>
public static TheoryData<string> GoldenSnapshotCases => new()
{
{ "tt-001" }, // Single issuer unknown
{ "tt-013" }, // Two issuer conflict
{ "tt-014" }, // Two issuer merge (affected + fixed)
{ "tt-020" } // Trust tier precedence
};
[Theory]
[MemberData(nameof(GoldenSnapshotCases))]
public void GoldenSnapshot_MatchesExpectedOutput(string testId)
{
// Arrange: Load test scenario and expected golden output
var (statements, expected) = LoadGoldenTestCase(testId);
// Act: Compute consensus
var actual = ComputeConsensus(statements);
// Assert: Compare against golden snapshot
var actualJson = JsonSerializer.Serialize(actual, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
var expectedJson = JsonSerializer.Serialize(expected, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
actualJson.Should().Be(expectedJson, because: $"golden snapshot {testId} must match exactly");
// Verify key fields individually for better diagnostics
actual.Status.Should().Be(expected.Status, because: $"{testId}: status mismatch");
actual.ConflictCount.Should().Be(expected.ConflictCount, because: $"{testId}: conflict count mismatch");
actual.StatementCount.Should().Be(expected.StatementCount, because: $"{testId}: statement count mismatch");
}
/// <summary>
/// Load a golden test case from fixtures.
/// </summary>
private static (VexStatement[] Statements, GoldenConsensusResult Expected) LoadGoldenTestCase(string testId)
{
var basePath = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "fixtures", "truth-tables", "expected");
var goldenPath = Path.Combine(basePath, $"{testId}.consensus.json");
if (!File.Exists(goldenPath))
{
throw new FileNotFoundException($"Golden file not found: {goldenPath}");
}
var goldenJson = File.ReadAllText(goldenPath);
var golden = JsonSerializer.Deserialize<GoldenConsensusResult>(goldenJson, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}) ?? throw new InvalidOperationException($"Failed to deserialize {goldenPath}");
// Reconstruct statements from golden file
var statements = golden.AppliedStatements.Select(s => new VexStatement
{
IssuerId = s.IssuerId,
Status = ParseVexStatus(s.Status),
TrustTier = ParseTrustTier(s.TrustTier),
Justification = null,
Timestamp = DateTimeOffset.Parse(s.Timestamp),
VulnerabilityId = golden.VulnerabilityId,
ProductKey = golden.ProductKey
}).ToArray();
return (statements, golden);
}
private static VexStatus ParseVexStatus(string status) => status.ToLowerInvariant() switch
{
"unknown" => VexStatus.Unknown,
"under_investigation" => VexStatus.UnderInvestigation,
"not_affected" => VexStatus.NotAffected,
"affected" => VexStatus.Affected,
"fixed" => VexStatus.Fixed,
_ => throw new ArgumentException($"Unknown VEX status: {status}")
};
private static int ParseTrustTier(string tier) => tier.ToLowerInvariant() switch
{
"distro" => 100,
"vendor" => 90,
"community" => 50,
_ => 80
};
#endregion
#region Helper Methods
/// <summary>
/// Create a normalized VEX statement for testing.
/// </summary>
private static VexStatement CreateStatement(
string issuerId,
VexStatus status,
int trustTier = 90,
string? justification = null)
{
return new VexStatement
{
IssuerId = issuerId,
Status = status,
TrustTier = trustTier,
Justification = justification,
Timestamp = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
VulnerabilityId = "CVE-2024-1234",
ProductKey = "pkg:npm/lodash@4.17.21"
};
}
/// <summary>
/// Compute consensus from statements.
/// This is a simplified mock - in real tests this would call VexConsensusEngine.
/// </summary>
private static ConsensusResult ComputeConsensus(VexStatement[] statements)
{
// Simple lattice merge implementation for tests
var orderedByTier = statements.OrderByDescending(s => s.TrustTier).ToList();
var highestTier = orderedByTier[0].TrustTier;
var topTierStatements = orderedByTier.Where(s => s.TrustTier == highestTier).ToList();
// Lattice merge logic
var status = MergeLattice(topTierStatements.Select(s => s.Status));
// Conflict detection
var distinctStatuses = topTierStatements.Select(s => s.Status).Distinct().ToList();
var hasConflict = distinctStatuses.Count > 1 && !IsHierarchical(distinctStatuses);
var conflicts = hasConflict
? topTierStatements.Where(s => s.Status != status).Select(s => s.IssuerId).ToList()
: new List<string>();
// Confidence calculation
var baseConfidence = 0.85m;
if (topTierStatements.Count == 1 || distinctStatuses.Count == 1)
baseConfidence = 0.95m; // Unanimous or single source
if (topTierStatements.Any(s => s.Justification == "component_not_present"))
baseConfidence = 0.95m;
else if (topTierStatements.Any(s => s.Justification == "vulnerable_code_not_in_execute_path"))
baseConfidence = 0.90m;
return new ConsensusResult
{
Status = status,
StatementCount = statements.Length,
ConflictCount = conflicts.Count,
Conflicts = conflicts,
ConfidenceScore = baseConfidence
};
}
/// <summary>
/// Merge statuses according to lattice rules.
/// </summary>
private static VexStatus MergeLattice(IEnumerable<VexStatus> statuses)
{
var statusList = statuses.ToList();
// Fixed is lattice top (terminal)
if (statusList.Contains(VexStatus.Fixed))
return VexStatus.Fixed;
// Affected and NotAffected at same level
if (statusList.Contains(VexStatus.Affected))
return VexStatus.Affected; // Conservative choice in conflict
if (statusList.Contains(VexStatus.NotAffected))
return VexStatus.NotAffected;
if (statusList.Contains(VexStatus.UnderInvestigation))
return VexStatus.UnderInvestigation;
return VexStatus.Unknown; // Lattice bottom
}
/// <summary>
/// Check if statuses are hierarchical (no conflict).
/// </summary>
private static bool IsHierarchical(List<VexStatus> statuses)
{
// Affected and NotAffected are at same level (conflict)
if (statuses.Contains(VexStatus.Affected) && statuses.Contains(VexStatus.NotAffected))
return false;
return true;
}
#endregion
#region Test Models
private class VexStatement
{
public required string IssuerId { get; init; }
public required VexStatus Status { get; init; }
public required int TrustTier { get; init; }
public string? Justification { get; init; }
public required DateTimeOffset Timestamp { get; init; }
public required string VulnerabilityId { get; init; }
public required string ProductKey { get; init; }
}
private class ConsensusResult
{
public required VexStatus Status { get; init; }
public required int StatementCount { get; init; }
public required int ConflictCount { get; init; }
public required IReadOnlyList<string> Conflicts { get; init; }
public required decimal ConfidenceScore { get; init; }
}
private enum VexStatus
{
Unknown,
UnderInvestigation,
NotAffected,
Affected,
Fixed
}
/// <summary>
/// Golden file format for consensus results (matches expected/*.consensus.json).
/// </summary>
private class GoldenConsensusResult
{
public required string VulnerabilityId { get; init; }
public required string ProductKey { get; init; }
public required string Status { get; init; }
public required decimal Confidence { get; init; }
public required int StatementCount { get; init; }
public required int ConflictCount { get; init; }
public required List<GoldenConflict> Conflicts { get; init; }
public required List<GoldenStatement> AppliedStatements { get; init; }
public required string ComputedAt { get; init; }
}
private class GoldenConflict
{
public required string Reason { get; init; }
public required List<GoldenIssuer> Issuers { get; init; }
}
private class GoldenIssuer
{
public required string IssuerId { get; init; }
public required string Status { get; init; }
public required string TrustTier { get; init; }
}
private class GoldenStatement
{
public required string IssuerId { get; init; }
public required string Status { get; init; }
public required string TrustTier { get; init; }
public required string Timestamp { get; init; }
}
#endregion
}