old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions

This commit is contained in:
master
2026-01-15 18:37:59 +02:00
parent c631bacee2
commit 88a85cdd92
208 changed files with 32271 additions and 2287 deletions

View File

@@ -0,0 +1,316 @@
// <copyright file="DeterminizationConfigEndpoints.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-004)
// </copyright>
using System.Security.Claims;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using StellaOps.Policy.Determinization;
namespace StellaOps.Policy.Engine.Endpoints;
/// <summary>
/// API endpoints for determinization configuration.
/// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-004)
/// </summary>
public static class DeterminizationConfigEndpoints
{
/// <summary>
/// Maps determinization config endpoints.
/// </summary>
public static IEndpointRouteBuilder MapDeterminizationConfigEndpoints(this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/api/v1/policy/config/determinization")
.WithTags("Determinization Configuration");
// Read endpoints (policy viewer access)
group.MapGet("", GetEffectiveConfig)
.WithName("GetEffectiveDeterminizationConfig")
.WithSummary("Get effective determinization configuration for the current tenant")
.Produces<EffectiveConfigResponse>(StatusCodes.Status200OK)
.RequireAuthorization("PolicyViewer");
group.MapGet("/defaults", GetDefaultConfig)
.WithName("GetDefaultDeterminizationConfig")
.WithSummary("Get default determinization configuration")
.Produces<DeterminizationOptions>(StatusCodes.Status200OK)
.RequireAuthorization("PolicyViewer");
group.MapGet("/audit", GetAuditHistory)
.WithName("GetDeterminizationConfigAuditHistory")
.WithSummary("Get audit history for determinization configuration changes")
.Produces<AuditHistoryResponse>(StatusCodes.Status200OK)
.RequireAuthorization("PolicyViewer");
// Write endpoints (policy admin access)
group.MapPut("", UpdateConfig)
.WithName("UpdateDeterminizationConfig")
.WithSummary("Update determinization configuration for the current tenant")
.Produces<EffectiveConfigResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization("PolicyAdmin");
group.MapPost("/validate", ValidateConfig)
.WithName("ValidateDeterminizationConfig")
.WithSummary("Validate determinization configuration without saving")
.Produces<ValidationResponse>(StatusCodes.Status200OK)
.RequireAuthorization("PolicyViewer");
return endpoints;
}
private static async Task<IResult> GetEffectiveConfig(
HttpContext context,
IDeterminizationConfigStore configStore,
ILogger<DeterminizationConfigEndpoints> logger,
CancellationToken ct)
{
var tenantId = GetTenantId(context);
logger.LogDebug("Getting effective determinization config for tenant {TenantId}", tenantId);
var config = await configStore.GetEffectiveConfigAsync(tenantId, ct);
return Results.Ok(new EffectiveConfigResponse
{
Config = config.Config,
IsDefault = config.IsDefault,
TenantId = config.TenantId,
LastUpdatedAt = config.LastUpdatedAt,
LastUpdatedBy = config.LastUpdatedBy,
Version = config.Version
});
}
private static IResult GetDefaultConfig(
ILogger<DeterminizationConfigEndpoints> logger)
{
logger.LogDebug("Getting default determinization config");
return Results.Ok(new DeterminizationOptions());
}
private static async Task<IResult> GetAuditHistory(
HttpContext context,
IDeterminizationConfigStore configStore,
ILogger<DeterminizationConfigEndpoints> logger,
int limit = 50,
CancellationToken ct = default)
{
var tenantId = GetTenantId(context);
logger.LogDebug("Getting audit history for tenant {TenantId}", tenantId);
var entries = await configStore.GetAuditHistoryAsync(tenantId, limit, ct);
return Results.Ok(new AuditHistoryResponse
{
Entries = entries.Select(e => new AuditEntryDto
{
Id = e.Id,
ChangedAt = e.ChangedAt,
Actor = e.Actor,
Reason = e.Reason,
Source = e.Source,
Summary = e.Summary
}).ToList()
});
}
private static async Task<IResult> UpdateConfig(
HttpContext context,
IDeterminizationConfigStore configStore,
ILogger<DeterminizationConfigEndpoints> logger,
UpdateConfigRequest request,
CancellationToken ct)
{
var tenantId = GetTenantId(context);
var actor = GetActorId(context);
logger.LogInformation(
"Updating determinization config for tenant {TenantId} by {Actor}: {Reason}",
tenantId,
actor,
request.Reason);
// Validate config
var validation = ValidateConfigInternal(request.Config);
if (!validation.IsValid)
{
return Results.BadRequest(new { errors = validation.Errors });
}
// Save with audit
await configStore.SaveConfigAsync(
tenantId,
request.Config,
new ConfigAuditInfo
{
Actor = actor,
Reason = request.Reason,
Source = "API",
CorrelationId = context.TraceIdentifier
},
ct);
// Return updated config
var updated = await configStore.GetEffectiveConfigAsync(tenantId, ct);
return Results.Ok(new EffectiveConfigResponse
{
Config = updated.Config,
IsDefault = updated.IsDefault,
TenantId = updated.TenantId,
LastUpdatedAt = updated.LastUpdatedAt,
LastUpdatedBy = updated.LastUpdatedBy,
Version = updated.Version
});
}
private static IResult ValidateConfig(
ValidateConfigRequest request,
ILogger<DeterminizationConfigEndpoints> logger)
{
logger.LogDebug("Validating determinization config");
var validation = ValidateConfigInternal(request.Config);
return Results.Ok(new ValidationResponse
{
IsValid = validation.IsValid,
Errors = validation.Errors,
Warnings = validation.Warnings
});
}
private static (bool IsValid, List<string> Errors, List<string> Warnings) ValidateConfigInternal(
DeterminizationOptions config)
{
var errors = new List<string>();
var warnings = new List<string>();
// Validate trigger config
if (config.Triggers.EpssDeltaThreshold < 0 || config.Triggers.EpssDeltaThreshold > 1)
{
errors.Add("EpssDeltaThreshold must be between 0 and 1");
}
if (config.Triggers.EpssDeltaThreshold < 0.1)
{
warnings.Add("EpssDeltaThreshold below 0.1 may cause excessive reanalysis");
}
// Validate conflict policy
if (config.Conflicts.EscalationSeverityThreshold < 0 || config.Conflicts.EscalationSeverityThreshold > 1)
{
errors.Add("EscalationSeverityThreshold must be between 0 and 1");
}
if (config.Conflicts.ConflictTtlHours < 1)
{
errors.Add("ConflictTtlHours must be at least 1");
}
// Validate environment thresholds
ValidateThresholds(config.Thresholds.Development, "Development", errors, warnings);
ValidateThresholds(config.Thresholds.Staging, "Staging", errors, warnings);
ValidateThresholds(config.Thresholds.Production, "Production", errors, warnings);
return (errors.Count == 0, errors, warnings);
}
private static void ValidateThresholds(
EnvironmentThreshold threshold,
string envName,
List<string> errors,
List<string> warnings)
{
if (threshold.EpssThreshold < 0 || threshold.EpssThreshold > 1)
{
errors.Add($"{envName}.EpssThreshold must be between 0 and 1");
}
if (threshold.UncertaintyFactor < 0 || threshold.UncertaintyFactor > 1)
{
errors.Add($"{envName}.UncertaintyFactor must be between 0 and 1");
}
if (threshold.MinScore < 0 || threshold.MinScore > 100)
{
errors.Add($"{envName}.MinScore must be between 0 and 100");
}
if (threshold.MaxScore < threshold.MinScore)
{
errors.Add($"{envName}.MaxScore must be >= MinScore");
}
}
private static string GetTenantId(HttpContext context)
{
return context.User.FindFirstValue("tenant_id") ?? "default";
}
private static string GetActorId(HttpContext context)
{
return context.User.FindFirstValue(ClaimTypes.NameIdentifier)
?? context.User.FindFirstValue("sub")
?? "system";
}
}
// DTOs
/// <summary>Effective config response.</summary>
public sealed record EffectiveConfigResponse
{
public required DeterminizationOptions Config { get; init; }
public required bool IsDefault { get; init; }
public string? TenantId { get; init; }
public DateTimeOffset? LastUpdatedAt { get; init; }
public string? LastUpdatedBy { get; init; }
public int Version { get; init; }
}
/// <summary>Update config request.</summary>
public sealed record UpdateConfigRequest
{
public required DeterminizationOptions Config { get; init; }
public required string Reason { get; init; }
}
/// <summary>Validate config request.</summary>
public sealed record ValidateConfigRequest
{
public required DeterminizationOptions Config { get; init; }
}
/// <summary>Validation response.</summary>
public sealed record ValidationResponse
{
public required bool IsValid { get; init; }
public required List<string> Errors { get; init; }
public required List<string> Warnings { get; init; }
}
/// <summary>Audit history response.</summary>
public sealed record AuditHistoryResponse
{
public required List<AuditEntryDto> Entries { get; init; }
}
/// <summary>Audit entry DTO.</summary>
public sealed record AuditEntryDto
{
public required Guid Id { get; init; }
public required DateTimeOffset ChangedAt { get; init; }
public required string Actor { get; init; }
public required string Reason { get; init; }
public string? Source { get; init; }
public string? Summary { get; init; }
}
/// <summary>Logger wrapper for DI.</summary>
file class DeterminizationConfigEndpoints { }

View File

@@ -211,6 +211,29 @@ internal static class UnknownsEndpoints
var hint = hintsRegistry.GetHint(u.ReasonCode);
var shortCode = ShortCodes.TryGetValue(u.ReasonCode, out var code) ? code : "U-RCH";
// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-003)
var triggersDto = u.Triggers.Count > 0
? u.Triggers.Select(t => new UnknownTriggerDto(
t.EventType,
t.EventVersion,
t.Source,
t.ReceivedAt,
t.CorrelationId)).ToList()
: null;
var conflictDto = u.ConflictInfo is { } ci
? new UnknownConflictInfoDto(
ci.HasConflict,
ci.Severity,
ci.SuggestedPath,
ci.Conflicts.Select(c => new UnknownConflictDetailDto(
c.Signal1,
c.Signal2,
c.Type,
c.Description,
c.Severity)).ToList())
: null;
return new UnknownDto(
u.Id,
u.PackageId,
@@ -228,7 +251,12 @@ internal static class UnknownsEndpoints
u.RemediationHint ?? hint.ShortHint,
hint.DetailedHint,
hint.AutomationRef,
u.EvidenceRefs.Select(e => new EvidenceRefDto(e.Type, e.Uri, e.Digest)).ToList());
u.EvidenceRefs.Select(e => new EvidenceRefDto(e.Type, e.Uri, e.Digest)).ToList(),
u.FingerprintId,
triggersDto,
u.NextActions.Count > 0 ? u.NextActions.ToList() : null,
conflictDto,
u.ObservationState);
}
private static readonly IReadOnlyDictionary<UnknownReasonCode, string> ShortCodes =
@@ -264,13 +292,50 @@ public sealed record UnknownDto(
string? RemediationHint,
string? DetailedHint,
string? AutomationCommand,
IReadOnlyList<EvidenceRefDto> EvidenceRefs);
IReadOnlyList<EvidenceRefDto> EvidenceRefs,
// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-003)
string? FingerprintId = null,
IReadOnlyList<UnknownTriggerDto>? Triggers = null,
IReadOnlyList<string>? NextActions = null,
UnknownConflictInfoDto? ConflictInfo = null,
string? ObservationState = null);
public sealed record EvidenceRefDto(
string Type,
string Uri,
string? Digest);
/// <summary>
/// Trigger that caused a reanalysis.
/// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-003)
/// </summary>
public sealed record UnknownTriggerDto(
string EventType,
int EventVersion,
string? Source,
DateTimeOffset ReceivedAt,
string? CorrelationId);
/// <summary>
/// Conflict information for an unknown.
/// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-003)
/// </summary>
public sealed record UnknownConflictInfoDto(
bool HasConflict,
double Severity,
string SuggestedPath,
IReadOnlyList<UnknownConflictDetailDto> Conflicts);
/// <summary>
/// Detail of a specific conflict.
/// </summary>
public sealed record UnknownConflictDetailDto(
string Signal1,
string Signal2,
string Type,
string Description,
double Severity);
/// <summary>Response containing a list of unknowns.</summary>
public sealed record UnknownsListResponse(IReadOnlyList<UnknownDto> Items, int TotalCount);

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using StellaOps.Policy.Determinization.Evidence;
using StellaOps.Policy.Determinization.Models;
namespace StellaOps.Policy.Engine.Gates.Determinization;
@@ -62,14 +63,91 @@ public sealed class SignalSnapshotBuilder : ISignalSnapshotBuilder
private static string BuildSubjectKey(string cveId, string componentPurl)
=> $"{cveId}::{componentPurl}";
// Sprint: SPRINT_20260112_004_BE_policy_determinization_attested_rules (DET-ATT-002)
// Map signals to snapshot with anchor metadata support
private SignalSnapshot ApplySignal(SignalSnapshot snapshot, Signal signal)
{
// This is a placeholder implementation
// In a real implementation, this would map Signal objects to SignalState<T> instances
// based on signal type and update the appropriate field in the snapshot
var queriedAt = signal.ObservedAt;
return signal.Type.ToUpperInvariant() switch
{
"VEX" => snapshot with
{
Vex = MapSignalState<VexClaimSummary>(signal, queriedAt)
},
"EPSS" => snapshot with
{
Epss = MapSignalState<EpssEvidence>(signal, queriedAt)
},
"REACHABILITY" => snapshot with
{
Reachability = MapSignalState<ReachabilityEvidence>(signal, queriedAt)
},
"RUNTIME" => snapshot with
{
Runtime = MapSignalState<RuntimeEvidence>(signal, queriedAt)
},
"BACKPORT" => snapshot with
{
Backport = MapSignalState<BackportEvidence>(signal, queriedAt)
},
"SBOM" => snapshot with
{
Sbom = MapSignalState<SbomLineageEvidence>(signal, queriedAt)
},
"CVSS" => snapshot with
{
Cvss = MapSignalState<CvssEvidence>(signal, queriedAt)
},
_ => HandleUnknownSignalType(snapshot, signal.Type)
};
}
private SignalSnapshot HandleUnknownSignalType(SignalSnapshot snapshot, string signalType)
{
_logger.LogWarning("Unknown signal type: {Type}", signalType);
return snapshot;
}
/// <summary>
/// Maps a raw signal to a typed SignalState with proper evidence casting.
/// Handles anchor metadata propagation from stored evidence.
/// </summary>
private static SignalState<T> MapSignalState<T>(Signal signal, DateTimeOffset queriedAt)
{
if (signal.Evidence is null)
{
return SignalState<T>.Queried(default, queriedAt);
}
// Handle direct type match
if (signal.Evidence is T typedEvidence)
{
return SignalState<T>.Queried(typedEvidence, queriedAt);
}
// Handle JSON element deserialization (common when evidence comes from storage)
if (signal.Evidence is System.Text.Json.JsonElement jsonElement)
{
try
{
var deserialized = System.Text.Json.JsonSerializer.Deserialize<T>(
jsonElement.GetRawText(),
new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return SignalState<T>.Queried(deserialized, queriedAt);
}
catch (System.Text.Json.JsonException)
{
return SignalState<T>.Failed($"Failed to deserialize {typeof(T).Name}", queriedAt);
}
}
// Cannot convert
return SignalState<T>.Failed($"Cannot convert {signal.Evidence.GetType().Name} to {typeof(T).Name}", queriedAt);
}
}
/// <summary>

View File

@@ -23,6 +23,69 @@ public sealed class DeterminizationRuleSet
public static DeterminizationRuleSet Default(DeterminizationOptions options) =>
new(new List<DeterminizationRule>
{
// Sprint: SPRINT_20260112_004_BE_policy_determinization_attested_rules (DET-ATT-003)
// Anchored rules have highest priority to short-circuit evaluation
// Rule 0a: Hard-fail if anchored VEX affected + anchored runtime telemetry confirms
new DeterminizationRule
{
Name = "AnchoredAffectedWithRuntimeHardFail",
Priority = 1,
Condition = (ctx, _) =>
ctx.SignalSnapshot.Vex.HasValue &&
ctx.SignalSnapshot.Vex.Value!.IsAnchored &&
string.Equals(ctx.SignalSnapshot.Vex.Value.Status, "affected", StringComparison.OrdinalIgnoreCase) &&
ctx.SignalSnapshot.Runtime.HasValue &&
ctx.SignalSnapshot.Runtime.Value!.IsAnchored &&
ctx.SignalSnapshot.Runtime.Value.Detected,
Action = (ctx, _) =>
DeterminizationResult.Blocked(
"Anchored VEX affected status combined with anchored runtime telemetry confirms active vulnerability - hard fail")
},
// Rule 0b: Allow if anchored VEX not_affected
new DeterminizationRule
{
Name = "AnchoredVexNotAffectedAllow",
Priority = 2,
Condition = (ctx, _) =>
ctx.SignalSnapshot.Vex.HasValue &&
ctx.SignalSnapshot.Vex.Value!.IsAnchored &&
(string.Equals(ctx.SignalSnapshot.Vex.Value.Status, "not_affected", StringComparison.OrdinalIgnoreCase) ||
string.Equals(ctx.SignalSnapshot.Vex.Value.Status, "fixed", StringComparison.OrdinalIgnoreCase)),
Action = (ctx, _) =>
DeterminizationResult.Allowed(
$"Anchored VEX statement indicates {ctx.SignalSnapshot.Vex.Value!.Status} - short-circuit allow")
},
// Rule 0c: Allow if anchored backport proof
new DeterminizationRule
{
Name = "AnchoredBackportProofAllow",
Priority = 3,
Condition = (ctx, _) =>
ctx.SignalSnapshot.Backport.HasValue &&
ctx.SignalSnapshot.Backport.Value!.IsAnchored &&
ctx.SignalSnapshot.Backport.Value.Detected,
Action = (ctx, _) =>
DeterminizationResult.Allowed(
$"Anchored backport proof confirms patch applied (source: {ctx.SignalSnapshot.Backport.Value!.Source}) - short-circuit allow")
},
// Rule 0d: Allow if anchored reachability not_reachable
new DeterminizationRule
{
Name = "AnchoredUnreachableAllow",
Priority = 4,
Condition = (ctx, _) =>
ctx.SignalSnapshot.Reachability.HasValue &&
ctx.SignalSnapshot.Reachability.Value!.IsAnchored &&
!ctx.SignalSnapshot.Reachability.Value.IsReachable,
Action = (ctx, _) =>
DeterminizationResult.Allowed(
"Anchored reachability analysis confirms code is unreachable - short-circuit allow")
},
// Rule 1: Escalate if runtime evidence shows vulnerable code loaded
new DeterminizationRule
{

View File

@@ -45,6 +45,11 @@ public sealed record DeterminizationResult
public static DeterminizationResult Quarantined(string reason, PolicyVerdictStatus status = PolicyVerdictStatus.Blocked) =>
new() { Status = status, Reason = reason };
// Sprint: SPRINT_20260112_004_BE_policy_determinization_attested_rules (DET-ATT-003)
/// <summary>Creates a hard-fail blocked result for anchored evidence confirming active vulnerability.</summary>
public static DeterminizationResult Blocked(string reason) =>
new() { Status = PolicyVerdictStatus.Blocked, Reason = reason, SuggestedState = ObservationState.Disputed };
public static DeterminizationResult Escalated(string reason, PolicyVerdictStatus status = PolicyVerdictStatus.Escalated) =>
new() { Status = status, Reason = reason };

View File

@@ -4,6 +4,7 @@ namespace StellaOps.Policy.Engine.Subscriptions;
/// <summary>
/// Events for signal updates that trigger re-evaluation.
/// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-005)
/// </summary>
public static class DeterminizationEventTypes
{
@@ -13,20 +14,39 @@ public static class DeterminizationEventTypes
public const string RuntimeUpdated = "runtime.updated";
public const string BackportUpdated = "backport.updated";
public const string ObservationStateChanged = "observation.state_changed";
// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-005)
// Additional event types for reanalysis triggers
public const string SbomUpdated = "sbom.updated";
public const string DsseValidationChanged = "dsse.validation_changed";
public const string RekorEntryAdded = "rekor.entry_added";
public const string PatchProofAdded = "patch.proof_added";
public const string ToolVersionChanged = "tool.version_changed";
}
/// <summary>
/// Event published when a signal is updated.
/// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-005)
/// </summary>
public sealed record SignalUpdatedEvent
{
public required string EventType { get; init; }
/// <summary>Event schema version (default: 1).</summary>
public int EventVersion { get; init; } = 1;
public required string CveId { get; init; }
public required string Purl { get; init; }
public required DateTimeOffset UpdatedAt { get; init; }
public required string Source { get; init; }
public object? NewValue { get; init; }
public object? PreviousValue { get; init; }
/// <summary>Correlation ID for tracing event chains.</summary>
public string? CorrelationId { get; init; }
/// <summary>Additional metadata for event processing.</summary>
public IReadOnlyDictionary<string, object>? Metadata { get; init; }
}
/// <summary>

View File

@@ -1,36 +1,86 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Determinization;
using StellaOps.Policy.Determinization.Models;
using StellaOps.Policy.Engine.Gates;
namespace StellaOps.Policy.Engine.Subscriptions;
/// <summary>
/// Implementation of signal update handling.
/// Implementation of signal update handling with versioned event mapping.
/// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-005)
/// </summary>
public sealed class SignalUpdateHandler : ISignalUpdateSubscription
{
private readonly IObservationRepository _observations;
private readonly IDeterminizationGate _gate;
private readonly IEventPublisher _eventPublisher;
private readonly DeterminizationOptions _options;
private readonly TimeProvider _timeProvider;
private readonly ILogger<SignalUpdateHandler> _logger;
// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-005)
// Event version registry for compatibility
private static readonly IReadOnlyDictionary<string, int> CurrentEventVersions = new Dictionary<string, int>
{
[DeterminizationEventTypes.EpssUpdated] = 1,
[DeterminizationEventTypes.VexUpdated] = 1,
[DeterminizationEventTypes.ReachabilityUpdated] = 1,
[DeterminizationEventTypes.RuntimeUpdated] = 1,
[DeterminizationEventTypes.BackportUpdated] = 1,
[DeterminizationEventTypes.SbomUpdated] = 1,
[DeterminizationEventTypes.DsseValidationChanged] = 1,
[DeterminizationEventTypes.RekorEntryAdded] = 1,
[DeterminizationEventTypes.PatchProofAdded] = 1,
[DeterminizationEventTypes.ToolVersionChanged] = 1
};
public SignalUpdateHandler(
IObservationRepository observations,
IDeterminizationGate gate,
IEventPublisher eventPublisher,
IOptions<DeterminizationOptions> options,
TimeProvider timeProvider,
ILogger<SignalUpdateHandler> logger)
{
_observations = observations;
_gate = gate;
_eventPublisher = eventPublisher;
_options = options.Value;
_timeProvider = timeProvider;
_logger = logger;
}
// Legacy constructor for backward compatibility
public SignalUpdateHandler(
IObservationRepository observations,
IDeterminizationGate gate,
IEventPublisher eventPublisher,
ILogger<SignalUpdateHandler> logger)
: this(observations, gate, eventPublisher,
Options.Create(new DeterminizationOptions()),
TimeProvider.System,
logger)
{
}
public async Task HandleAsync(SignalUpdatedEvent evt, CancellationToken ct = default)
{
// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-005)
// Check if this event type should trigger reanalysis
if (!ShouldTriggerReanalysis(evt))
{
_logger.LogDebug(
"Event {EventType}@{EventVersion} does not trigger reanalysis per config",
evt.EventType,
evt.EventVersion);
return;
}
_logger.LogInformation(
"Processing signal update: {EventType} for CVE {CveId} on {Purl}",
"Processing signal update: {EventType}@{EventVersion} for CVE {CveId} on {Purl}",
evt.EventType,
evt.EventVersion,
evt.CveId,
evt.Purl);
@@ -52,23 +102,107 @@ public sealed class SignalUpdateHandler : ISignalUpdateSubscription
}
}
/// <summary>
/// Determines if an event should trigger reanalysis based on config.
/// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-005)
/// </summary>
private bool ShouldTriggerReanalysis(SignalUpdatedEvent evt)
{
var triggers = _options.Triggers;
return evt.EventType switch
{
DeterminizationEventTypes.EpssUpdated =>
triggers.TriggerOnThresholdCrossing && MeetsEpssDeltaThreshold(evt),
DeterminizationEventTypes.VexUpdated =>
triggers.TriggerOnVexStatusChange,
DeterminizationEventTypes.ReachabilityUpdated or
DeterminizationEventTypes.RuntimeUpdated =>
triggers.TriggerOnRuntimeTelemetryChange,
DeterminizationEventTypes.BackportUpdated or
DeterminizationEventTypes.PatchProofAdded =>
triggers.TriggerOnPatchProofAdded,
DeterminizationEventTypes.DsseValidationChanged =>
triggers.TriggerOnDsseValidationChange,
DeterminizationEventTypes.RekorEntryAdded =>
triggers.TriggerOnRekorEntry,
DeterminizationEventTypes.ToolVersionChanged =>
triggers.TriggerOnToolVersionChange,
DeterminizationEventTypes.SbomUpdated =>
true, // Always trigger for SBOM changes
_ => true // Unknown events default to trigger
};
}
/// <summary>
/// Check if EPSS delta meets threshold.
/// </summary>
private bool MeetsEpssDeltaThreshold(SignalUpdatedEvent evt)
{
if (evt.Metadata is null ||
!evt.Metadata.TryGetValue("delta", out var deltaObj) ||
deltaObj is not double delta)
{
return true; // If no delta info, trigger anyway
}
return Math.Abs(delta) >= _options.Triggers.EpssDeltaThreshold;
}
/// <summary>
/// Gets the current version for an event type.
/// </summary>
public static int GetCurrentEventVersion(string eventType) =>
CurrentEventVersions.TryGetValue(eventType, out var version) ? version : 1;
/// <summary>
/// Checks if an event version is supported.
/// </summary>
public static bool IsVersionSupported(string eventType, int version)
{
if (!CurrentEventVersions.TryGetValue(eventType, out var currentVersion))
{
return true; // Unknown events are allowed
}
return version <= currentVersion;
}
private async Task ReEvaluateObservationAsync(
CveObservation obs,
SignalUpdatedEvent trigger,
CancellationToken ct)
{
// This is a placeholder for re-evaluation logic
// In a full implementation, this would:
// 1. Build PolicyGateContext from observation
// 2. Call gate.EvaluateDeterminizationAsync()
// 3. Compare new verdict with old verdict
// 4. Publish ObservationStateChangedEvent if state changed
// 5. Update observation in repository
_logger.LogDebug(
"Re-evaluating observation {ObservationId} after {EventType}",
"Re-evaluating observation {ObservationId} after {EventType}@{EventVersion}",
obs.Id,
trigger.EventType);
trigger.EventType,
trigger.EventVersion);
// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-005)
// Build reanalysis trigger for fingerprint
var reanalysisTrigger = new ReanalysisTrigger
{
EventType = trigger.EventType,
EventVersion = trigger.EventVersion,
Source = trigger.Source,
ReceivedAt = _timeProvider.GetUtcNow(),
CorrelationId = trigger.CorrelationId
};
// TODO: Full implementation would:
// 1. Build PolicyGateContext from observation
// 2. Call gate.EvaluateDeterminizationAsync() with trigger info
// 3. Compare new verdict with old verdict
// 4. If state changed, publish ObservationStateChangedEvent
// 5. Update observation in repository with new fingerprint
await Task.CompletedTask;
}