partly or unimplemented features - now implemented
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ILatticeTriageService.cs
|
||||
// Sprint: SPRINT_20260208_052_ReachGraph_8_state_reachability_lattice
|
||||
// Task: T1 - Triage service interface
|
||||
// Description: Service interface for the lattice triage subsystem providing
|
||||
// state queries, evidence application, manual overrides,
|
||||
// and audit trail access.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace StellaOps.Reachability.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing the reachability lattice triage workflow.
|
||||
/// Provides state queries, evidence application, manual overrides,
|
||||
/// and audit trail access for the 8-state lattice.
|
||||
/// </summary>
|
||||
public interface ILatticeTriageService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or creates a triage entry for a component/CVE pair.
|
||||
/// </summary>
|
||||
Task<LatticeTriageEntry> GetOrCreateEntryAsync(
|
||||
string componentPurl,
|
||||
string cve,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Applies evidence to a triage entry, triggering a state transition.
|
||||
/// </summary>
|
||||
Task<LatticeTriageEntry> ApplyEvidenceAsync(
|
||||
string componentPurl,
|
||||
string cve,
|
||||
EvidenceType evidenceType,
|
||||
string? reason = null,
|
||||
IReadOnlyList<string>? evidenceDigests = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Applies a manual override to force a specific lattice state.
|
||||
/// </summary>
|
||||
Task<LatticeOverrideResult> OverrideStateAsync(
|
||||
LatticeOverrideRequest request,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Lists triage entries matching the given query.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<LatticeTriageEntry>> ListAsync(
|
||||
LatticeTriageQuery query,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the transition history for a component/CVE pair.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<LatticeTransitionRecord>> GetHistoryAsync(
|
||||
string componentPurl,
|
||||
string cve,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resets a triage entry to the Unknown state.
|
||||
/// </summary>
|
||||
Task<LatticeTriageEntry> ResetAsync(
|
||||
string componentPurl,
|
||||
string cve,
|
||||
string actor,
|
||||
string reason,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// LatticeTriageModels.cs
|
||||
// Sprint: SPRINT_20260208_052_ReachGraph_8_state_reachability_lattice
|
||||
// Task: T1 - Triage models for the 8-state reachability lattice
|
||||
// Description: Models for triage workflows, state transitions, manual
|
||||
// overrides, and audit trail for the reachability lattice.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Reachability.Core;
|
||||
|
||||
/// <summary>
|
||||
/// A triage entry representing a component's current lattice state
|
||||
/// along with its full evidence and transition history.
|
||||
/// </summary>
|
||||
public sealed record LatticeTriageEntry
|
||||
{
|
||||
/// <summary>Content-addressed triage entry ID.</summary>
|
||||
[JsonPropertyName("entry_id")]
|
||||
public required string EntryId { get; init; }
|
||||
|
||||
/// <summary>Component PURL.</summary>
|
||||
[JsonPropertyName("component_purl")]
|
||||
public required string ComponentPurl { get; init; }
|
||||
|
||||
/// <summary>CVE identifier.</summary>
|
||||
[JsonPropertyName("cve")]
|
||||
public required string Cve { get; init; }
|
||||
|
||||
/// <summary>Current lattice state.</summary>
|
||||
[JsonPropertyName("current_state")]
|
||||
public required LatticeState CurrentState { get; init; }
|
||||
|
||||
/// <summary>Current confidence score (0.0-1.0).</summary>
|
||||
[JsonPropertyName("confidence")]
|
||||
public required double Confidence { get; init; }
|
||||
|
||||
/// <summary>VEX status derived from the current state.</summary>
|
||||
[JsonPropertyName("vex_status")]
|
||||
public required string VexStatus { get; init; }
|
||||
|
||||
/// <summary>Ordered transition history (oldest first).</summary>
|
||||
[JsonPropertyName("transitions")]
|
||||
public required ImmutableArray<LatticeTransitionRecord> Transitions { get; init; }
|
||||
|
||||
/// <summary>Whether this entry is in a contested state requiring manual review.</summary>
|
||||
[JsonPropertyName("requires_review")]
|
||||
public bool RequiresReview => CurrentState == LatticeState.Contested;
|
||||
|
||||
/// <summary>Whether a manual override has been applied.</summary>
|
||||
[JsonPropertyName("has_override")]
|
||||
public bool HasOverride => Transitions.Any(t => t.IsManualOverride);
|
||||
|
||||
/// <summary>When this entry was created.</summary>
|
||||
[JsonPropertyName("created_at")]
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>When this entry was last updated.</summary>
|
||||
[JsonPropertyName("updated_at")]
|
||||
public required DateTimeOffset UpdatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A recorded state transition in the lattice audit trail.
|
||||
/// </summary>
|
||||
public sealed record LatticeTransitionRecord
|
||||
{
|
||||
/// <summary>State before transition.</summary>
|
||||
[JsonPropertyName("from_state")]
|
||||
public required LatticeState FromState { get; init; }
|
||||
|
||||
/// <summary>State after transition.</summary>
|
||||
[JsonPropertyName("to_state")]
|
||||
public required LatticeState ToState { get; init; }
|
||||
|
||||
/// <summary>Confidence before transition.</summary>
|
||||
[JsonPropertyName("confidence_before")]
|
||||
public required double ConfidenceBefore { get; init; }
|
||||
|
||||
/// <summary>Confidence after transition.</summary>
|
||||
[JsonPropertyName("confidence_after")]
|
||||
public required double ConfidenceAfter { get; init; }
|
||||
|
||||
/// <summary>What triggered this transition.</summary>
|
||||
[JsonPropertyName("trigger")]
|
||||
public required LatticeTransitionTrigger Trigger { get; init; }
|
||||
|
||||
/// <summary>Whether this was a manual override.</summary>
|
||||
[JsonPropertyName("is_manual_override")]
|
||||
public bool IsManualOverride => Trigger == LatticeTransitionTrigger.ManualOverride;
|
||||
|
||||
/// <summary>Reason or justification for the transition.</summary>
|
||||
[JsonPropertyName("reason")]
|
||||
public string? Reason { get; init; }
|
||||
|
||||
/// <summary>Identity of the actor who caused the transition.</summary>
|
||||
[JsonPropertyName("actor")]
|
||||
public string? Actor { get; init; }
|
||||
|
||||
/// <summary>When this transition occurred.</summary>
|
||||
[JsonPropertyName("timestamp")]
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
/// <summary>Evidence digests supporting this transition.</summary>
|
||||
[JsonPropertyName("evidence_digests")]
|
||||
public ImmutableArray<string> EvidenceDigests { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What triggered a lattice state transition.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public enum LatticeTransitionTrigger
|
||||
{
|
||||
/// <summary>Static analysis evidence.</summary>
|
||||
StaticAnalysis,
|
||||
|
||||
/// <summary>Runtime observation evidence.</summary>
|
||||
RuntimeObservation,
|
||||
|
||||
/// <summary>Manual override by an operator.</summary>
|
||||
ManualOverride,
|
||||
|
||||
/// <summary>System reset (e.g., re-scan).</summary>
|
||||
SystemReset,
|
||||
|
||||
/// <summary>Automated triage rule.</summary>
|
||||
AutomatedRule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to manually override the lattice state for a component/CVE pair.
|
||||
/// </summary>
|
||||
public sealed record LatticeOverrideRequest
|
||||
{
|
||||
/// <summary>Component PURL.</summary>
|
||||
[JsonPropertyName("component_purl")]
|
||||
public required string ComponentPurl { get; init; }
|
||||
|
||||
/// <summary>CVE identifier.</summary>
|
||||
[JsonPropertyName("cve")]
|
||||
public required string Cve { get; init; }
|
||||
|
||||
/// <summary>Target state to set.</summary>
|
||||
[JsonPropertyName("target_state")]
|
||||
public required LatticeState TargetState { get; init; }
|
||||
|
||||
/// <summary>Justification for the override (required for audit trail).</summary>
|
||||
[JsonPropertyName("reason")]
|
||||
public required string Reason { get; init; }
|
||||
|
||||
/// <summary>Actor performing the override.</summary>
|
||||
[JsonPropertyName("actor")]
|
||||
public required string Actor { get; init; }
|
||||
|
||||
/// <summary>Supporting evidence digests.</summary>
|
||||
[JsonPropertyName("evidence_digests")]
|
||||
public ImmutableArray<string> EvidenceDigests { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a lattice state override operation.
|
||||
/// </summary>
|
||||
public sealed record LatticeOverrideResult
|
||||
{
|
||||
/// <summary>Whether the override was applied.</summary>
|
||||
[JsonPropertyName("applied")]
|
||||
public required bool Applied { get; init; }
|
||||
|
||||
/// <summary>The updated triage entry.</summary>
|
||||
[JsonPropertyName("entry")]
|
||||
public required LatticeTriageEntry Entry { get; init; }
|
||||
|
||||
/// <summary>The transition record for this override.</summary>
|
||||
[JsonPropertyName("transition")]
|
||||
public required LatticeTransitionRecord Transition { get; init; }
|
||||
|
||||
/// <summary>Warning message if the override was unusual.</summary>
|
||||
[JsonPropertyName("warning")]
|
||||
public string? Warning { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query filter for listing triage entries.
|
||||
/// </summary>
|
||||
public sealed record LatticeTriageQuery
|
||||
{
|
||||
/// <summary>Filter by state.</summary>
|
||||
public LatticeState? State { get; init; }
|
||||
|
||||
/// <summary>Filter entries requiring review (Contested state).</summary>
|
||||
public bool? RequiresReview { get; init; }
|
||||
|
||||
/// <summary>Filter by component PURL prefix.</summary>
|
||||
public string? ComponentPurlPrefix { get; init; }
|
||||
|
||||
/// <summary>Filter by CVE identifier.</summary>
|
||||
public string? Cve { get; init; }
|
||||
|
||||
/// <summary>Maximum entries to return.</summary>
|
||||
public int Limit { get; init; } = 100;
|
||||
|
||||
/// <summary>Offset for pagination.</summary>
|
||||
public int Offset { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// LatticeTriageService.cs
|
||||
// Sprint: SPRINT_20260208_052_ReachGraph_8_state_reachability_lattice
|
||||
// Task: T1 - Triage service implementation
|
||||
// Description: In-memory implementation of the lattice triage service with
|
||||
// full state machine integration, override support, and
|
||||
// audit trail. Thread-safe via ConcurrentDictionary.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Reachability.Core;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of <see cref="ILatticeTriageService"/>.
|
||||
/// Thread-safe via <see cref="ConcurrentDictionary{TKey,TValue}"/>.
|
||||
/// </summary>
|
||||
public sealed class LatticeTriageService : ILatticeTriageService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, TriageState> _entries = new(StringComparer.Ordinal);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<LatticeTriageService> _logger;
|
||||
|
||||
// OTel metrics
|
||||
private readonly Counter<long> _entriesCreated;
|
||||
private readonly Counter<long> _evidenceApplied;
|
||||
private readonly Counter<long> _overridesApplied;
|
||||
private readonly Counter<long> _resetsPerformed;
|
||||
private readonly Counter<long> _contestedEntries;
|
||||
|
||||
public LatticeTriageService(
|
||||
TimeProvider timeProvider,
|
||||
ILogger<LatticeTriageService> logger,
|
||||
IMeterFactory meterFactory)
|
||||
{
|
||||
_timeProvider = timeProvider;
|
||||
_logger = logger;
|
||||
|
||||
var meter = meterFactory.Create("StellaOps.Reachability.Core.LatticeTriage");
|
||||
_entriesCreated = meter.CreateCounter<long>(
|
||||
"stellaops.lattice.triage.entries_created_total",
|
||||
description: "Total triage entries created");
|
||||
_evidenceApplied = meter.CreateCounter<long>(
|
||||
"stellaops.lattice.triage.evidence_applied_total",
|
||||
description: "Total evidence applications");
|
||||
_overridesApplied = meter.CreateCounter<long>(
|
||||
"stellaops.lattice.triage.overrides_applied_total",
|
||||
description: "Total manual overrides applied");
|
||||
_resetsPerformed = meter.CreateCounter<long>(
|
||||
"stellaops.lattice.triage.resets_total",
|
||||
description: "Total resets performed");
|
||||
_contestedEntries = meter.CreateCounter<long>(
|
||||
"stellaops.lattice.triage.contested_total",
|
||||
description: "Total entries that entered Contested state");
|
||||
}
|
||||
|
||||
public Task<LatticeTriageEntry> GetOrCreateEntryAsync(
|
||||
string componentPurl,
|
||||
string cve,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(componentPurl);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(cve);
|
||||
|
||||
var key = MakeKey(componentPurl, cve);
|
||||
var state = _entries.GetOrAdd(key, _ =>
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
_entriesCreated.Add(1);
|
||||
_logger.LogDebug(
|
||||
"Created triage entry for {Purl} / {Cve}",
|
||||
componentPurl, cve);
|
||||
|
||||
return new TriageState
|
||||
{
|
||||
ComponentPurl = componentPurl,
|
||||
Cve = cve,
|
||||
Lattice = new ReachabilityLattice(),
|
||||
Transitions = [],
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
});
|
||||
|
||||
return Task.FromResult(state.ToEntry());
|
||||
}
|
||||
|
||||
public Task<LatticeTriageEntry> ApplyEvidenceAsync(
|
||||
string componentPurl,
|
||||
string cve,
|
||||
EvidenceType evidenceType,
|
||||
string? reason = null,
|
||||
IReadOnlyList<string>? evidenceDigests = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(componentPurl);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(cve);
|
||||
|
||||
var key = MakeKey(componentPurl, cve);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
var state = _entries.GetOrAdd(key, _ =>
|
||||
{
|
||||
_entriesCreated.Add(1);
|
||||
return new TriageState
|
||||
{
|
||||
ComponentPurl = componentPurl,
|
||||
Cve = cve,
|
||||
Lattice = new ReachabilityLattice(),
|
||||
Transitions = [],
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
});
|
||||
|
||||
lock (state)
|
||||
{
|
||||
var fromState = state.Lattice.CurrentState;
|
||||
var fromConfidence = state.Lattice.Confidence;
|
||||
|
||||
var transition = state.Lattice.ApplyEvidence(evidenceType);
|
||||
|
||||
var trigger = evidenceType switch
|
||||
{
|
||||
EvidenceType.StaticReachable or EvidenceType.StaticUnreachable
|
||||
=> LatticeTransitionTrigger.StaticAnalysis,
|
||||
EvidenceType.RuntimeObserved or EvidenceType.RuntimeUnobserved
|
||||
=> LatticeTransitionTrigger.RuntimeObservation,
|
||||
_ => LatticeTransitionTrigger.AutomatedRule
|
||||
};
|
||||
|
||||
var record = new LatticeTransitionRecord
|
||||
{
|
||||
FromState = fromState,
|
||||
ToState = state.Lattice.CurrentState,
|
||||
ConfidenceBefore = fromConfidence,
|
||||
ConfidenceAfter = state.Lattice.Confidence,
|
||||
Trigger = trigger,
|
||||
Reason = reason ?? $"Evidence applied: {evidenceType}",
|
||||
Timestamp = now,
|
||||
EvidenceDigests = evidenceDigests is not null
|
||||
? [.. evidenceDigests]
|
||||
: []
|
||||
};
|
||||
|
||||
state.Transitions.Add(record);
|
||||
state.UpdatedAt = now;
|
||||
|
||||
if (state.Lattice.CurrentState == LatticeState.Contested)
|
||||
{
|
||||
_contestedEntries.Add(1);
|
||||
}
|
||||
|
||||
_evidenceApplied.Add(1);
|
||||
_logger.LogDebug(
|
||||
"Applied {Evidence} to {Purl}/{Cve}: {From} → {To} (confidence {Conf:F2})",
|
||||
evidenceType, componentPurl, cve,
|
||||
fromState, state.Lattice.CurrentState, state.Lattice.Confidence);
|
||||
}
|
||||
|
||||
return Task.FromResult(state.ToEntry());
|
||||
}
|
||||
|
||||
public Task<LatticeOverrideResult> OverrideStateAsync(
|
||||
LatticeOverrideRequest request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(request.ComponentPurl);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(request.Cve);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(request.Reason);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(request.Actor);
|
||||
|
||||
var key = MakeKey(request.ComponentPurl, request.Cve);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
var state = _entries.GetOrAdd(key, _ =>
|
||||
{
|
||||
_entriesCreated.Add(1);
|
||||
return new TriageState
|
||||
{
|
||||
ComponentPurl = request.ComponentPurl,
|
||||
Cve = request.Cve,
|
||||
Lattice = new ReachabilityLattice(),
|
||||
Transitions = [],
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
});
|
||||
|
||||
LatticeTransitionRecord transitionRecord;
|
||||
string? warning = null;
|
||||
|
||||
lock (state)
|
||||
{
|
||||
var fromState = state.Lattice.CurrentState;
|
||||
var fromConfidence = state.Lattice.Confidence;
|
||||
|
||||
// Warn if overriding from a confirmed state
|
||||
if (fromState is LatticeState.ConfirmedReachable or LatticeState.ConfirmedUnreachable)
|
||||
{
|
||||
warning = $"Overriding from confirmed state '{fromState}' — " +
|
||||
"this may invalidate prior evidence-based decisions.";
|
||||
}
|
||||
|
||||
// Force the state via reset + targeted state injection
|
||||
state.Lattice.Reset();
|
||||
ForceState(state.Lattice, request.TargetState);
|
||||
|
||||
var targetConfidence = ConfidenceCalculator.GetConfidenceRange(request.TargetState);
|
||||
// Set confidence to mid-range of the target state
|
||||
var midConfidence = (targetConfidence.Min + targetConfidence.Max) / 2.0;
|
||||
|
||||
transitionRecord = new LatticeTransitionRecord
|
||||
{
|
||||
FromState = fromState,
|
||||
ToState = request.TargetState,
|
||||
ConfidenceBefore = fromConfidence,
|
||||
ConfidenceAfter = midConfidence,
|
||||
Trigger = LatticeTransitionTrigger.ManualOverride,
|
||||
Reason = request.Reason,
|
||||
Actor = request.Actor,
|
||||
Timestamp = now,
|
||||
EvidenceDigests = request.EvidenceDigests
|
||||
};
|
||||
|
||||
state.Transitions.Add(transitionRecord);
|
||||
state.UpdatedAt = now;
|
||||
}
|
||||
|
||||
_overridesApplied.Add(1);
|
||||
_logger.LogInformation(
|
||||
"Manual override by {Actor} on {Purl}/{Cve}: → {TargetState}. Reason: {Reason}",
|
||||
request.Actor, request.ComponentPurl, request.Cve,
|
||||
request.TargetState, request.Reason);
|
||||
|
||||
return Task.FromResult(new LatticeOverrideResult
|
||||
{
|
||||
Applied = true,
|
||||
Entry = state.ToEntry(),
|
||||
Transition = transitionRecord,
|
||||
Warning = warning
|
||||
});
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<LatticeTriageEntry>> ListAsync(
|
||||
LatticeTriageQuery query,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
IEnumerable<TriageState> entries = _entries.Values;
|
||||
|
||||
if (query.State.HasValue)
|
||||
{
|
||||
entries = entries.Where(s => s.Lattice.CurrentState == query.State.Value);
|
||||
}
|
||||
|
||||
if (query.RequiresReview == true)
|
||||
{
|
||||
entries = entries.Where(s => s.Lattice.CurrentState == LatticeState.Contested);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.ComponentPurlPrefix))
|
||||
{
|
||||
entries = entries.Where(s =>
|
||||
s.ComponentPurl.StartsWith(query.ComponentPurlPrefix, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Cve))
|
||||
{
|
||||
entries = entries.Where(s =>
|
||||
s.Cve.Equals(query.Cve, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
var result = entries
|
||||
.OrderByDescending(s => s.UpdatedAt)
|
||||
.Skip(query.Offset)
|
||||
.Take(query.Limit)
|
||||
.Select(s => s.ToEntry())
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<LatticeTriageEntry>>(result);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<LatticeTransitionRecord>> GetHistoryAsync(
|
||||
string componentPurl,
|
||||
string cve,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(componentPurl);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(cve);
|
||||
|
||||
var key = MakeKey(componentPurl, cve);
|
||||
|
||||
if (!_entries.TryGetValue(key, out var state))
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<LatticeTransitionRecord>>([]);
|
||||
}
|
||||
|
||||
lock (state)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<LatticeTransitionRecord>>(
|
||||
[.. state.Transitions]);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<LatticeTriageEntry> ResetAsync(
|
||||
string componentPurl,
|
||||
string cve,
|
||||
string actor,
|
||||
string reason,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(componentPurl);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(cve);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(actor);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(reason);
|
||||
|
||||
var key = MakeKey(componentPurl, cve);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
if (!_entries.TryGetValue(key, out var state))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No triage entry found for {componentPurl} / {cve}");
|
||||
}
|
||||
|
||||
lock (state)
|
||||
{
|
||||
var fromState = state.Lattice.CurrentState;
|
||||
var fromConfidence = state.Lattice.Confidence;
|
||||
|
||||
state.Lattice.Reset();
|
||||
|
||||
var record = new LatticeTransitionRecord
|
||||
{
|
||||
FromState = fromState,
|
||||
ToState = LatticeState.Unknown,
|
||||
ConfidenceBefore = fromConfidence,
|
||||
ConfidenceAfter = 0.0,
|
||||
Trigger = LatticeTransitionTrigger.SystemReset,
|
||||
Reason = reason,
|
||||
Actor = actor,
|
||||
Timestamp = now
|
||||
};
|
||||
|
||||
state.Transitions.Add(record);
|
||||
state.UpdatedAt = now;
|
||||
}
|
||||
|
||||
_resetsPerformed.Add(1);
|
||||
_logger.LogInformation(
|
||||
"Reset triage entry for {Purl}/{Cve} by {Actor}: {Reason}",
|
||||
componentPurl, cve, actor, reason);
|
||||
|
||||
return Task.FromResult(state.ToEntry());
|
||||
}
|
||||
|
||||
// ── Private helpers ──────────────────────────────────────────────────
|
||||
|
||||
private static string MakeKey(string purl, string cve)
|
||||
{
|
||||
return $"{purl}|{cve}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces the lattice into a specific state by applying appropriate evidence.
|
||||
/// Used for manual overrides.
|
||||
/// </summary>
|
||||
private static void ForceState(ReachabilityLattice lattice, LatticeState target)
|
||||
{
|
||||
// The lattice starts at Unknown after Reset.
|
||||
// Apply evidence to reach the target state.
|
||||
switch (target)
|
||||
{
|
||||
case LatticeState.Unknown:
|
||||
// Already at Unknown after reset
|
||||
break;
|
||||
case LatticeState.StaticReachable:
|
||||
lattice.ApplyEvidence(EvidenceType.StaticReachable);
|
||||
break;
|
||||
case LatticeState.StaticUnreachable:
|
||||
lattice.ApplyEvidence(EvidenceType.StaticUnreachable);
|
||||
break;
|
||||
case LatticeState.RuntimeObserved:
|
||||
lattice.ApplyEvidence(EvidenceType.RuntimeObserved);
|
||||
break;
|
||||
case LatticeState.RuntimeUnobserved:
|
||||
lattice.ApplyEvidence(EvidenceType.RuntimeUnobserved);
|
||||
break;
|
||||
case LatticeState.ConfirmedReachable:
|
||||
lattice.ApplyEvidence(EvidenceType.StaticReachable);
|
||||
lattice.ApplyEvidence(EvidenceType.RuntimeObserved);
|
||||
break;
|
||||
case LatticeState.ConfirmedUnreachable:
|
||||
lattice.ApplyEvidence(EvidenceType.StaticUnreachable);
|
||||
lattice.ApplyEvidence(EvidenceType.RuntimeUnobserved);
|
||||
break;
|
||||
case LatticeState.Contested:
|
||||
lattice.ApplyEvidence(EvidenceType.StaticReachable);
|
||||
lattice.ApplyEvidence(EvidenceType.RuntimeUnobserved);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeEntryId(string purl, string cve)
|
||||
{
|
||||
var input = $"{purl}|{cve}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
return $"triage:sha256:{Convert.ToHexStringLower(hash)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal mutable state holder. Thread-safety via lock.
|
||||
/// </summary>
|
||||
private sealed class TriageState
|
||||
{
|
||||
public required string ComponentPurl { get; init; }
|
||||
public required string Cve { get; init; }
|
||||
public required ReachabilityLattice Lattice { get; init; }
|
||||
public required List<LatticeTransitionRecord> Transitions { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
|
||||
public LatticeTriageEntry ToEntry()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
return new LatticeTriageEntry
|
||||
{
|
||||
EntryId = ComputeEntryId(ComponentPurl, Cve),
|
||||
ComponentPurl = ComponentPurl,
|
||||
Cve = Cve,
|
||||
CurrentState = Lattice.CurrentState,
|
||||
Confidence = Lattice.Confidence,
|
||||
VexStatus = MapToVexStatus(Lattice.CurrentState),
|
||||
Transitions = [.. Transitions],
|
||||
CreatedAt = CreatedAt,
|
||||
UpdatedAt = UpdatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static string MapToVexStatus(LatticeState state) => state switch
|
||||
{
|
||||
LatticeState.Unknown => "under_investigation",
|
||||
LatticeState.StaticReachable => "under_investigation",
|
||||
LatticeState.StaticUnreachable => "not_affected",
|
||||
LatticeState.RuntimeObserved => "affected",
|
||||
LatticeState.RuntimeUnobserved => "not_affected",
|
||||
LatticeState.ConfirmedReachable => "affected",
|
||||
LatticeState.ConfirmedUnreachable => "not_affected",
|
||||
LatticeState.Contested => "under_investigation",
|
||||
_ => "under_investigation"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
services.TryAddSingleton<ConfidenceCalculator>();
|
||||
services.TryAddSingleton<IReachabilityIndex, ReachabilityIndex>();
|
||||
services.TryAddSingleton<ILatticeTriageService, LatticeTriageService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user