Implement VEX document verification system with issuer management and signature verification
- Added IIssuerDirectory interface for managing VEX document issuers, including methods for registration, revocation, and trust validation. - Created InMemoryIssuerDirectory class as an in-memory implementation of IIssuerDirectory for testing and single-instance deployments. - Introduced ISignatureVerifier interface for verifying signatures on VEX documents, with support for multiple signature formats. - Developed SignatureVerifier class as the default implementation of ISignatureVerifier, allowing extensibility for different signature formats. - Implemented handlers for DSSE and JWS signature formats, including methods for verification and signature extraction. - Defined various records and enums for issuer and signature metadata, enhancing the structure and clarity of the verification process.
This commit is contained in:
476
src/VexLens/StellaOps.VexLens/Testing/VexLensTestHarness.cs
Normal file
476
src/VexLens/StellaOps.VexLens/Testing/VexLensTestHarness.cs
Normal file
@@ -0,0 +1,476 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.VexLens.Consensus;
|
||||
using StellaOps.VexLens.Models;
|
||||
using StellaOps.VexLens.Normalization;
|
||||
using StellaOps.VexLens.Storage;
|
||||
using StellaOps.VexLens.Trust;
|
||||
using StellaOps.VexLens.Verification;
|
||||
|
||||
namespace StellaOps.VexLens.Testing;
|
||||
|
||||
/// <summary>
|
||||
/// Test harness for VexLens operations with determinism verification.
|
||||
/// </summary>
|
||||
public sealed class VexLensTestHarness : IDisposable
|
||||
{
|
||||
private readonly VexNormalizerRegistry _normalizerRegistry;
|
||||
private readonly InMemoryIssuerDirectory _issuerDirectory;
|
||||
private readonly InMemoryConsensusEventEmitter _eventEmitter;
|
||||
private readonly InMemoryConsensusProjectionStore _projectionStore;
|
||||
private readonly TrustWeightEngine _trustWeightEngine;
|
||||
private readonly VexConsensusEngine _consensusEngine;
|
||||
|
||||
public VexLensTestHarness()
|
||||
{
|
||||
_normalizerRegistry = new VexNormalizerRegistry();
|
||||
_normalizerRegistry.Register(new OpenVexNormalizer());
|
||||
_normalizerRegistry.Register(new CsafVexNormalizer());
|
||||
_normalizerRegistry.Register(new CycloneDxVexNormalizer());
|
||||
|
||||
_issuerDirectory = new InMemoryIssuerDirectory();
|
||||
_eventEmitter = new InMemoryConsensusEventEmitter();
|
||||
_projectionStore = new InMemoryConsensusProjectionStore(_eventEmitter);
|
||||
_trustWeightEngine = new TrustWeightEngine();
|
||||
_consensusEngine = new VexConsensusEngine();
|
||||
}
|
||||
|
||||
public IVexNormalizerRegistry NormalizerRegistry => _normalizerRegistry;
|
||||
public IIssuerDirectory IssuerDirectory => _issuerDirectory;
|
||||
public IConsensusEventEmitter EventEmitter => _eventEmitter;
|
||||
public InMemoryConsensusEventEmitter TestEventEmitter => _eventEmitter;
|
||||
public IConsensusProjectionStore ProjectionStore => _projectionStore;
|
||||
public ITrustWeightEngine TrustWeightEngine => _trustWeightEngine;
|
||||
public IVexConsensusEngine ConsensusEngine => _consensusEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes VEX content and returns the result.
|
||||
/// </summary>
|
||||
public async Task<NormalizationResult> NormalizeAsync(
|
||||
string content,
|
||||
string? sourceUri = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizer = _normalizerRegistry.DetectNormalizer(content);
|
||||
if (normalizer == null)
|
||||
{
|
||||
throw new InvalidOperationException("No normalizer found for content");
|
||||
}
|
||||
|
||||
var context = new NormalizationContext(
|
||||
SourceUri: sourceUri,
|
||||
NormalizedAt: DateTimeOffset.UtcNow,
|
||||
Normalizer: "VexLensTestHarness",
|
||||
Options: null);
|
||||
|
||||
return await normalizer.NormalizeAsync(content, context, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes trust weight for a statement.
|
||||
/// </summary>
|
||||
public async Task<TrustWeightResult> ComputeTrustWeightAsync(
|
||||
NormalizedStatement statement,
|
||||
VexIssuer? issuer = null,
|
||||
DateTimeOffset? documentIssuedAt = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new TrustWeightRequest(
|
||||
Statement: statement,
|
||||
Issuer: issuer,
|
||||
SignatureVerification: null,
|
||||
DocumentIssuedAt: documentIssuedAt,
|
||||
Context: new TrustWeightContext(
|
||||
TenantId: null,
|
||||
EvaluationTime: DateTimeOffset.UtcNow,
|
||||
CustomFactors: null));
|
||||
|
||||
return await _trustWeightEngine.ComputeWeightAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes consensus from weighted statements.
|
||||
/// </summary>
|
||||
public async Task<VexConsensusResult> ComputeConsensusAsync(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
IEnumerable<WeightedStatement> statements,
|
||||
ConsensusMode mode = ConsensusMode.WeightedVote,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new VexConsensusRequest(
|
||||
VulnerabilityId: vulnerabilityId,
|
||||
ProductKey: productKey,
|
||||
Statements: statements.ToList(),
|
||||
Context: new ConsensusContext(
|
||||
TenantId: null,
|
||||
EvaluationTime: DateTimeOffset.UtcNow,
|
||||
Policy: new ConsensusPolicy(
|
||||
Mode: mode,
|
||||
MinimumWeightThreshold: 0.1,
|
||||
ConflictThreshold: 0.3,
|
||||
RequireJustificationForNotAffected: false,
|
||||
PreferredIssuers: null)));
|
||||
|
||||
return await _consensusEngine.ComputeConsensusAsync(request, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a test issuer.
|
||||
/// </summary>
|
||||
public async Task<IssuerRecord> RegisterTestIssuerAsync(
|
||||
string issuerId,
|
||||
string name,
|
||||
IssuerCategory category = IssuerCategory.Vendor,
|
||||
TrustTier trustTier = TrustTier.Trusted,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var registration = new IssuerRegistration(
|
||||
IssuerId: issuerId,
|
||||
Name: name,
|
||||
Category: category,
|
||||
TrustTier: trustTier,
|
||||
InitialKeys: null,
|
||||
Metadata: null);
|
||||
|
||||
return await _issuerDirectory.RegisterIssuerAsync(registration, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a test statement.
|
||||
/// </summary>
|
||||
public static NormalizedStatement CreateTestStatement(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
VexStatus status,
|
||||
VexJustification? justification = null,
|
||||
string? statementId = null)
|
||||
{
|
||||
return new NormalizedStatement(
|
||||
StatementId: statementId ?? $"stmt-{Guid.NewGuid():N}",
|
||||
VulnerabilityId: vulnerabilityId,
|
||||
VulnerabilityAliases: null,
|
||||
Product: new NormalizedProduct(
|
||||
Key: productKey,
|
||||
Name: null,
|
||||
Version: null,
|
||||
Purl: productKey.StartsWith("pkg:") ? productKey : null,
|
||||
Cpe: null,
|
||||
Hashes: null),
|
||||
Status: status,
|
||||
StatusNotes: null,
|
||||
Justification: justification,
|
||||
ImpactStatement: null,
|
||||
ActionStatement: null,
|
||||
ActionStatementTimestamp: null,
|
||||
Versions: null,
|
||||
Subcomponents: null,
|
||||
FirstSeen: DateTimeOffset.UtcNow,
|
||||
LastSeen: DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a test issuer.
|
||||
/// </summary>
|
||||
public static VexIssuer CreateTestIssuer(
|
||||
string id,
|
||||
string name,
|
||||
IssuerCategory category = IssuerCategory.Vendor,
|
||||
TrustTier trustTier = TrustTier.Trusted)
|
||||
{
|
||||
return new VexIssuer(
|
||||
Id: id,
|
||||
Name: name,
|
||||
Category: category,
|
||||
TrustTier: trustTier,
|
||||
KeyFingerprints: null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all test data.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_eventEmitter.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup if needed
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determinism verification harness for VexLens operations.
|
||||
/// </summary>
|
||||
public sealed class DeterminismHarness
|
||||
{
|
||||
private readonly VexLensTestHarness _harness;
|
||||
|
||||
public DeterminismHarness()
|
||||
{
|
||||
_harness = new VexLensTestHarness();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that normalization produces deterministic results.
|
||||
/// </summary>
|
||||
public async Task<DeterminismResult> VerifyNormalizationDeterminismAsync(
|
||||
string content,
|
||||
int iterations = 3,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var results = new List<string>();
|
||||
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
var result = await _harness.NormalizeAsync(content, cancellationToken: cancellationToken);
|
||||
if (result.Success && result.Document != null)
|
||||
{
|
||||
var hash = ComputeDocumentHash(result.Document);
|
||||
results.Add(hash);
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add($"error:{result.Errors.FirstOrDefault()?.Code}");
|
||||
}
|
||||
}
|
||||
|
||||
var isEqual = results.Distinct().Count() == 1;
|
||||
return new DeterminismResult(
|
||||
Operation: "normalization",
|
||||
IsDeterministic: isEqual,
|
||||
Iterations: iterations,
|
||||
DistinctResults: results.Distinct().Count(),
|
||||
FirstResult: results.FirstOrDefault(),
|
||||
Discrepancies: isEqual ? null : results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that consensus produces deterministic results.
|
||||
/// </summary>
|
||||
public async Task<DeterminismResult> VerifyConsensusDeterminismAsync(
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
IEnumerable<(NormalizedStatement Statement, VexIssuer? Issuer)> statements,
|
||||
int iterations = 3,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var results = new List<string>();
|
||||
var stmtList = statements.ToList();
|
||||
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
var weighted = new List<WeightedStatement>();
|
||||
|
||||
foreach (var (stmt, issuer) in stmtList)
|
||||
{
|
||||
var weight = await _harness.ComputeTrustWeightAsync(stmt, issuer, cancellationToken: cancellationToken);
|
||||
weighted.Add(new WeightedStatement(stmt, weight, issuer, null));
|
||||
}
|
||||
|
||||
var result = await _harness.ComputeConsensusAsync(
|
||||
vulnerabilityId,
|
||||
productKey,
|
||||
weighted,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
var hash = ComputeConsensusHash(result);
|
||||
results.Add(hash);
|
||||
}
|
||||
|
||||
var isEqual = results.Distinct().Count() == 1;
|
||||
return new DeterminismResult(
|
||||
Operation: "consensus",
|
||||
IsDeterministic: isEqual,
|
||||
Iterations: iterations,
|
||||
DistinctResults: results.Distinct().Count(),
|
||||
FirstResult: results.FirstOrDefault(),
|
||||
Discrepancies: isEqual ? null : results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that trust weight computation produces deterministic results.
|
||||
/// </summary>
|
||||
public async Task<DeterminismResult> VerifyTrustWeightDeterminismAsync(
|
||||
NormalizedStatement statement,
|
||||
VexIssuer? issuer,
|
||||
int iterations = 3,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var results = new List<string>();
|
||||
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
var result = await _harness.ComputeTrustWeightAsync(statement, issuer, cancellationToken: cancellationToken);
|
||||
var hash = $"{result.Weight:F10}";
|
||||
results.Add(hash);
|
||||
}
|
||||
|
||||
var isEqual = results.Distinct().Count() == 1;
|
||||
return new DeterminismResult(
|
||||
Operation: "trust_weight",
|
||||
IsDeterministic: isEqual,
|
||||
Iterations: iterations,
|
||||
DistinctResults: results.Distinct().Count(),
|
||||
FirstResult: results.FirstOrDefault(),
|
||||
Discrepancies: isEqual ? null : results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs all determinism checks.
|
||||
/// </summary>
|
||||
public async Task<DeterminismReport> RunFullDeterminismCheckAsync(
|
||||
string vexContent,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var results = new List<DeterminismResult>();
|
||||
|
||||
// Normalization
|
||||
var normResult = await VerifyNormalizationDeterminismAsync(vexContent, cancellationToken: cancellationToken);
|
||||
results.Add(normResult);
|
||||
|
||||
// If normalization succeeded, test downstream operations
|
||||
if (normResult.IsDeterministic)
|
||||
{
|
||||
var normalizeResult = await _harness.NormalizeAsync(vexContent, cancellationToken: cancellationToken);
|
||||
if (normalizeResult.Success && normalizeResult.Document != null && normalizeResult.Document.Statements.Count > 0)
|
||||
{
|
||||
var statement = normalizeResult.Document.Statements[0];
|
||||
var issuer = normalizeResult.Document.Issuer;
|
||||
|
||||
// Trust weight
|
||||
var trustResult = await VerifyTrustWeightDeterminismAsync(statement, issuer, cancellationToken: cancellationToken);
|
||||
results.Add(trustResult);
|
||||
|
||||
// Consensus
|
||||
var consensusResult = await VerifyConsensusDeterminismAsync(
|
||||
statement.VulnerabilityId,
|
||||
statement.Product.Key,
|
||||
[(statement, issuer)],
|
||||
cancellationToken: cancellationToken);
|
||||
results.Add(consensusResult);
|
||||
}
|
||||
}
|
||||
|
||||
return new DeterminismReport(
|
||||
Results: results,
|
||||
AllDeterministic: results.All(r => r.IsDeterministic),
|
||||
GeneratedAt: DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
private static string ComputeDocumentHash(NormalizedVexDocument doc)
|
||||
{
|
||||
// Create a stable representation for hashing
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(doc.DocumentId);
|
||||
sb.Append(doc.SourceFormat);
|
||||
sb.Append(doc.Issuer?.Id ?? "null");
|
||||
|
||||
foreach (var stmt in doc.Statements.OrderBy(s => s.StatementId))
|
||||
{
|
||||
sb.Append(stmt.VulnerabilityId);
|
||||
sb.Append(stmt.Product.Key);
|
||||
sb.Append(stmt.Status);
|
||||
sb.Append(stmt.Justification?.ToString() ?? "null");
|
||||
}
|
||||
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sb.ToString()));
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string ComputeConsensusHash(VexConsensusResult result)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(result.ConsensusStatus);
|
||||
sb.Append(result.ConsensusJustification?.ToString() ?? "null");
|
||||
sb.Append($"{result.ConfidenceScore:F10}");
|
||||
sb.Append(result.Outcome);
|
||||
|
||||
foreach (var contrib in result.Contributions.OrderBy(c => c.StatementId))
|
||||
{
|
||||
sb.Append(contrib.StatementId);
|
||||
sb.Append($"{contrib.Weight:F10}");
|
||||
sb.Append(contrib.IsWinner);
|
||||
}
|
||||
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sb.ToString()));
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a determinism check.
|
||||
/// </summary>
|
||||
public sealed record DeterminismResult(
|
||||
string Operation,
|
||||
bool IsDeterministic,
|
||||
int Iterations,
|
||||
int DistinctResults,
|
||||
string? FirstResult,
|
||||
IReadOnlyList<string>? Discrepancies);
|
||||
|
||||
/// <summary>
|
||||
/// Report of determinism checks.
|
||||
/// </summary>
|
||||
public sealed record DeterminismReport(
|
||||
IReadOnlyList<DeterminismResult> Results,
|
||||
bool AllDeterministic,
|
||||
DateTimeOffset GeneratedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Test data generators for VexLens.
|
||||
/// </summary>
|
||||
public static class VexLensTestData
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a sample OpenVEX document.
|
||||
/// </summary>
|
||||
public static string GenerateOpenVexDocument(
|
||||
string vulnerabilityId,
|
||||
string productPurl,
|
||||
VexStatus status,
|
||||
VexJustification? justification = null)
|
||||
{
|
||||
var doc = new
|
||||
{
|
||||
@context = "https://openvex.dev/ns/v0.2.0",
|
||||
@id = $"urn:uuid:{Guid.NewGuid()}",
|
||||
author = new { @id = "test-vendor", name = "Test Vendor" },
|
||||
timestamp = DateTimeOffset.UtcNow.ToString("O"),
|
||||
statements = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
vulnerability = vulnerabilityId,
|
||||
products = new[] { productPurl },
|
||||
status = status.ToString().ToLowerInvariant().Replace("notaffected", "not_affected").Replace("underinvestigation", "under_investigation"),
|
||||
justification = justification?.ToString().ToLowerInvariant()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates sample statements for consensus testing.
|
||||
/// </summary>
|
||||
public static IEnumerable<(NormalizedStatement Statement, VexIssuer Issuer)> GenerateConflictingStatements(
|
||||
string vulnerabilityId,
|
||||
string productKey)
|
||||
{
|
||||
yield return (
|
||||
VexLensTestHarness.CreateTestStatement(vulnerabilityId, productKey, VexStatus.NotAffected, VexJustification.ComponentNotPresent, "stmt-1"),
|
||||
VexLensTestHarness.CreateTestIssuer("vendor-1", "Vendor A", IssuerCategory.Vendor, TrustTier.Authoritative));
|
||||
|
||||
yield return (
|
||||
VexLensTestHarness.CreateTestStatement(vulnerabilityId, productKey, VexStatus.Affected, null, "stmt-2"),
|
||||
VexLensTestHarness.CreateTestIssuer("researcher-1", "Security Researcher", IssuerCategory.Community, TrustTier.Trusted));
|
||||
|
||||
yield return (
|
||||
VexLensTestHarness.CreateTestStatement(vulnerabilityId, productKey, VexStatus.UnderInvestigation, null, "stmt-3"),
|
||||
VexLensTestHarness.CreateTestIssuer("aggregator-1", "VEX Aggregator", IssuerCategory.Aggregator, TrustTier.Unknown));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user