old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions
This commit is contained in:
@@ -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 { }
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user