feat: Add VEX Status Chip component and integration tests for reachability drift detection
- Introduced `VexStatusChipComponent` to display VEX status with color coding and tooltips. - Implemented integration tests for reachability drift detection, covering various scenarios including drift detection, determinism, and error handling. - Enhanced `ScannerToSignalsReachabilityTests` with a null implementation of `ICallGraphSyncService` for better test isolation. - Updated project references to include the new Reachability Drift library.
This commit is contained in:
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* TrustLatticeEngine - Orchestrates the complete trust evaluation pipeline.
|
||||
* Sprint: SPRINT_3600_0001_0001 (Trust Algebra and Lattice Engine)
|
||||
* Task: TRUST-016
|
||||
*
|
||||
* The engine coordinates:
|
||||
* 1. VEX normalization from multiple formats
|
||||
* 2. Claim ingestion and aggregation
|
||||
* 3. K4 lattice evaluation
|
||||
* 4. Disposition selection
|
||||
* 5. Proof bundle generation
|
||||
*/
|
||||
|
||||
namespace StellaOps.Policy.TrustLattice;
|
||||
|
||||
/// <summary>
|
||||
/// Result of processing a batch of inputs.
|
||||
/// </summary>
|
||||
public sealed record EvaluationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the evaluation completed successfully.
|
||||
/// </summary>
|
||||
public required bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if failed.
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The proof bundle containing all evidence.
|
||||
/// </summary>
|
||||
public ProofBundle? ProofBundle { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Quick access to disposition results by subject.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, DispositionResult> Dispositions { get; init; } =
|
||||
new Dictionary<string, DispositionResult>();
|
||||
|
||||
/// <summary>
|
||||
/// Warnings generated during evaluation.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Warnings { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for trust lattice evaluation.
|
||||
/// </summary>
|
||||
public sealed record EvaluationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to generate a proof bundle.
|
||||
/// </summary>
|
||||
public bool GenerateProofBundle { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include full decision traces in the proof bundle.
|
||||
/// </summary>
|
||||
public bool IncludeDecisionTraces { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to validate claim signatures.
|
||||
/// </summary>
|
||||
public bool ValidateSignatures { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp for claim validity evaluation (null = now).
|
||||
/// </summary>
|
||||
public DateTimeOffset? EvaluationTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Filter to specific subjects (null = all).
|
||||
/// </summary>
|
||||
public IReadOnlySet<string>? SubjectFilter { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The trust lattice engine orchestrates the complete evaluation pipeline.
|
||||
/// </summary>
|
||||
public sealed class TrustLatticeEngine
|
||||
{
|
||||
private readonly PolicyBundle _policy;
|
||||
private readonly LatticeStore _store;
|
||||
private readonly DispositionSelector _selector;
|
||||
private readonly Dictionary<string, IVexNormalizer> _normalizers;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new trust lattice engine.
|
||||
/// </summary>
|
||||
/// <param name="policy">The policy bundle to use.</param>
|
||||
public TrustLatticeEngine(PolicyBundle? policy = null)
|
||||
{
|
||||
_policy = policy ?? PolicyBundle.Default;
|
||||
_store = new LatticeStore();
|
||||
_selector = new DispositionSelector(_policy.GetEffectiveRules());
|
||||
|
||||
// Register default normalizers
|
||||
_normalizers = new Dictionary<string, IVexNormalizer>(StringComparer.OrdinalIgnoreCase);
|
||||
RegisterNormalizer(new CycloneDxVexNormalizer());
|
||||
RegisterNormalizer(new OpenVexNormalizer());
|
||||
RegisterNormalizer(new CsafVexNormalizer());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the policy bundle.
|
||||
/// </summary>
|
||||
public PolicyBundle Policy => _policy;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lattice store.
|
||||
/// </summary>
|
||||
public LatticeStore Store => _store;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a VEX normalizer.
|
||||
/// </summary>
|
||||
public void RegisterNormalizer(IVexNormalizer normalizer)
|
||||
{
|
||||
_normalizers[normalizer.Format] = normalizer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ingests a claim directly.
|
||||
/// </summary>
|
||||
public Claim IngestClaim(Claim claim)
|
||||
{
|
||||
return _store.IngestClaim(claim);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ingests multiple claims.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Claim> IngestClaims(IEnumerable<Claim> claims)
|
||||
{
|
||||
return claims.Select(c => _store.IngestClaim(c)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ingests a VEX document.
|
||||
/// </summary>
|
||||
/// <param name="document">The VEX document content.</param>
|
||||
/// <param name="format">The VEX format (CycloneDX/ECMA-424, OpenVEX, CSAF).</param>
|
||||
/// <param name="principal">The principal making the assertions.</param>
|
||||
/// <param name="trustLabel">Default trust label for generated claims.</param>
|
||||
public IReadOnlyList<Claim> IngestVex(
|
||||
string document,
|
||||
string format,
|
||||
Principal principal,
|
||||
TrustLabel? trustLabel = null)
|
||||
{
|
||||
if (!_normalizers.TryGetValue(format, out var normalizer))
|
||||
{
|
||||
throw new ArgumentException($"Unknown VEX format: {format}", nameof(format));
|
||||
}
|
||||
|
||||
var claims = normalizer.Normalize(document, principal, trustLabel).ToList();
|
||||
return IngestClaims(claims);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the disposition for a subject.
|
||||
/// </summary>
|
||||
public DispositionResult GetDisposition(Subject subject)
|
||||
{
|
||||
var state = _store.GetOrCreateSubject(subject);
|
||||
return _selector.Select(state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the disposition for a subject by digest.
|
||||
/// </summary>
|
||||
public DispositionResult? GetDisposition(string subjectDigest)
|
||||
{
|
||||
var state = _store.GetSubjectState(subjectDigest);
|
||||
if (state is null) return null;
|
||||
return _selector.Select(state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates all subjects and produces dispositions.
|
||||
/// </summary>
|
||||
public EvaluationResult Evaluate(EvaluationOptions? options = null)
|
||||
{
|
||||
options ??= new EvaluationOptions();
|
||||
var warnings = new List<string>();
|
||||
var dispositions = new Dictionary<string, DispositionResult>();
|
||||
|
||||
try
|
||||
{
|
||||
var subjects = _store.GetAllSubjects();
|
||||
|
||||
// Apply subject filter if specified
|
||||
if (options.SubjectFilter is not null)
|
||||
{
|
||||
subjects = subjects.Where(s => options.SubjectFilter.Contains(s.SubjectDigest));
|
||||
}
|
||||
|
||||
// Evaluate each subject
|
||||
foreach (var state in subjects)
|
||||
{
|
||||
var result = _selector.Select(state);
|
||||
dispositions[state.SubjectDigest] = result;
|
||||
}
|
||||
|
||||
// Generate proof bundle if requested
|
||||
ProofBundle? proofBundle = null;
|
||||
if (options.GenerateProofBundle)
|
||||
{
|
||||
proofBundle = GenerateProofBundle(dispositions, options);
|
||||
}
|
||||
|
||||
return new EvaluationResult
|
||||
{
|
||||
Success = true,
|
||||
ProofBundle = proofBundle,
|
||||
Dispositions = dispositions,
|
||||
Warnings = warnings,
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new EvaluationResult
|
||||
{
|
||||
Success = false,
|
||||
Error = ex.Message,
|
||||
Dispositions = dispositions,
|
||||
Warnings = warnings,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a proof bundle for the current evaluation state.
|
||||
/// </summary>
|
||||
private ProofBundle GenerateProofBundle(
|
||||
Dictionary<string, DispositionResult> dispositions,
|
||||
EvaluationOptions options)
|
||||
{
|
||||
var builder = new ProofBundleBuilder()
|
||||
.WithPolicyBundle(_policy);
|
||||
|
||||
// Add all claims
|
||||
foreach (var claim in _store.GetAllClaims())
|
||||
{
|
||||
builder.AddClaim(claim);
|
||||
}
|
||||
|
||||
// Add atom tables and decisions for each subject
|
||||
foreach (var state in _store.GetAllSubjects())
|
||||
{
|
||||
builder.AddAtomTable(state);
|
||||
|
||||
if (dispositions.TryGetValue(state.SubjectDigest, out var result))
|
||||
{
|
||||
builder.AddDecision(state.SubjectDigest, result);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all state from the engine.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_store.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets statistics about the current state.
|
||||
/// </summary>
|
||||
public LatticeStoreStats GetStats() => _store.GetStats();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a builder for claims.
|
||||
/// </summary>
|
||||
public ClaimBuilder CreateClaim() => new(this);
|
||||
|
||||
/// <summary>
|
||||
/// Fluent builder for creating and ingesting claims.
|
||||
/// </summary>
|
||||
public sealed class ClaimBuilder
|
||||
{
|
||||
private readonly TrustLatticeEngine _engine;
|
||||
private Subject? _subject;
|
||||
private Principal _principal = Principal.Unknown;
|
||||
private TrustLabel? _trustLabel;
|
||||
private readonly List<AtomAssertion> _assertions = [];
|
||||
private readonly List<string> _evidenceRefs = [];
|
||||
|
||||
internal ClaimBuilder(TrustLatticeEngine engine)
|
||||
{
|
||||
_engine = engine;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the subject.
|
||||
/// </summary>
|
||||
public ClaimBuilder ForSubject(Subject subject)
|
||||
{
|
||||
_subject = subject;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the principal.
|
||||
/// </summary>
|
||||
public ClaimBuilder FromPrincipal(Principal principal)
|
||||
{
|
||||
_principal = principal;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the trust label.
|
||||
/// </summary>
|
||||
public ClaimBuilder WithTrust(TrustLabel label)
|
||||
{
|
||||
_trustLabel = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts an atom value.
|
||||
/// </summary>
|
||||
public ClaimBuilder Assert(SecurityAtom atom, bool value, string? justification = null)
|
||||
{
|
||||
_assertions.Add(new AtomAssertion
|
||||
{
|
||||
Atom = atom,
|
||||
Value = value,
|
||||
Justification = justification,
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts PRESENT = true.
|
||||
/// </summary>
|
||||
public ClaimBuilder Present(bool value = true, string? justification = null)
|
||||
=> Assert(SecurityAtom.Present, value, justification);
|
||||
|
||||
/// <summary>
|
||||
/// Asserts APPLIES = true.
|
||||
/// </summary>
|
||||
public ClaimBuilder Applies(bool value = true, string? justification = null)
|
||||
=> Assert(SecurityAtom.Applies, value, justification);
|
||||
|
||||
/// <summary>
|
||||
/// Asserts REACHABLE = true.
|
||||
/// </summary>
|
||||
public ClaimBuilder Reachable(bool value = true, string? justification = null)
|
||||
=> Assert(SecurityAtom.Reachable, value, justification);
|
||||
|
||||
/// <summary>
|
||||
/// Asserts MITIGATED = true.
|
||||
/// </summary>
|
||||
public ClaimBuilder Mitigated(bool value = true, string? justification = null)
|
||||
=> Assert(SecurityAtom.Mitigated, value, justification);
|
||||
|
||||
/// <summary>
|
||||
/// Asserts FIXED = true.
|
||||
/// </summary>
|
||||
public ClaimBuilder Fixed(bool value = true, string? justification = null)
|
||||
=> Assert(SecurityAtom.Fixed, value, justification);
|
||||
|
||||
/// <summary>
|
||||
/// Asserts MISATTRIBUTED = true.
|
||||
/// </summary>
|
||||
public ClaimBuilder Misattributed(bool value = true, string? justification = null)
|
||||
=> Assert(SecurityAtom.Misattributed, value, justification);
|
||||
|
||||
/// <summary>
|
||||
/// References evidence by digest.
|
||||
/// </summary>
|
||||
public ClaimBuilder WithEvidence(string digest)
|
||||
{
|
||||
_evidenceRefs.Add(digest);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds and ingests the claim.
|
||||
/// </summary>
|
||||
public Claim Build()
|
||||
{
|
||||
if (_subject is null)
|
||||
throw new InvalidOperationException("Subject is required.");
|
||||
|
||||
var claim = new Claim
|
||||
{
|
||||
Subject = _subject,
|
||||
Principal = _principal,
|
||||
TrustLabel = _trustLabel,
|
||||
Assertions = _assertions,
|
||||
EvidenceRefs = _evidenceRefs,
|
||||
TimeInfo = new ClaimTimeInfo { IssuedAt = DateTimeOffset.UtcNow },
|
||||
};
|
||||
|
||||
return _engine.IngestClaim(claim);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user