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,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);
}

View File

@@ -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; }
}

View File

@@ -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"
};
}
}

View File

@@ -40,6 +40,7 @@ public static class ServiceCollectionExtensions
services.TryAddSingleton<ConfidenceCalculator>();
services.TryAddSingleton<IReachabilityIndex, ReachabilityIndex>();
services.TryAddSingleton<ILatticeTriageService, LatticeTriageService>();
return services;
}