old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions
This commit is contained in:
@@ -2,6 +2,7 @@ namespace StellaOps.Policy.Determinization;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the Determinization subsystem.
|
||||
/// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-001)
|
||||
/// </summary>
|
||||
public sealed record DeterminizationOptions
|
||||
{
|
||||
@@ -37,4 +38,174 @@ public sealed record DeterminizationOptions
|
||||
|
||||
/// <summary>Maximum retry attempts for failed signal queries (default: 3).</summary>
|
||||
public int MaxSignalQueryRetries { get; init; } = 3;
|
||||
|
||||
// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-001)
|
||||
|
||||
/// <summary>Reanalysis trigger configuration.</summary>
|
||||
public ReanalysisTriggerConfig Triggers { get; init; } = new();
|
||||
|
||||
/// <summary>Conflict handling policy.</summary>
|
||||
public ConflictHandlingPolicy ConflictPolicy { get; init; } = new();
|
||||
|
||||
/// <summary>Per-environment threshold overrides.</summary>
|
||||
public EnvironmentThresholds EnvironmentThresholds { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for reanalysis triggers.
|
||||
/// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-001)
|
||||
/// </summary>
|
||||
public sealed record ReanalysisTriggerConfig
|
||||
{
|
||||
/// <summary>Trigger on EPSS delta >= this value (default: 0.2).</summary>
|
||||
public double EpssDeltaThreshold { get; init; } = 0.2;
|
||||
|
||||
/// <summary>Trigger when entropy crosses threshold (default: true).</summary>
|
||||
public bool TriggerOnThresholdCrossing { get; init; } = true;
|
||||
|
||||
/// <summary>Trigger on new Rekor entry (default: true).</summary>
|
||||
public bool TriggerOnRekorEntry { get; init; } = true;
|
||||
|
||||
/// <summary>Trigger on OpenVEX status change (default: true).</summary>
|
||||
public bool TriggerOnVexStatusChange { get; init; } = true;
|
||||
|
||||
/// <summary>Trigger on runtime telemetry exploit/reachability change (default: true).</summary>
|
||||
public bool TriggerOnRuntimeTelemetryChange { get; init; } = true;
|
||||
|
||||
/// <summary>Trigger on binary patch proof added (default: true).</summary>
|
||||
public bool TriggerOnPatchProofAdded { get; init; } = true;
|
||||
|
||||
/// <summary>Trigger on DSSE validation state change (default: true).</summary>
|
||||
public bool TriggerOnDsseValidationChange { get; init; } = true;
|
||||
|
||||
/// <summary>Trigger on tool version update (default: false).</summary>
|
||||
public bool TriggerOnToolVersionChange { get; init; } = false;
|
||||
|
||||
/// <summary>Minimum interval between reanalyses in minutes (default: 15).</summary>
|
||||
public int MinReanalysisIntervalMinutes { get; init; } = 15;
|
||||
|
||||
/// <summary>Maximum reanalyses per day per CVE (default: 10).</summary>
|
||||
public int MaxReanalysesPerDayPerCve { get; init; } = 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Conflict handling policy configuration.
|
||||
/// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-001)
|
||||
/// </summary>
|
||||
public sealed record ConflictHandlingPolicy
|
||||
{
|
||||
/// <summary>Action to take when VEX/reachability conflict is detected.</summary>
|
||||
public ConflictAction VexReachabilityConflictAction { get; init; } = ConflictAction.RequireManualReview;
|
||||
|
||||
/// <summary>Action to take when static/runtime conflict is detected.</summary>
|
||||
public ConflictAction StaticRuntimeConflictAction { get; init; } = ConflictAction.RequireManualReview;
|
||||
|
||||
/// <summary>Action to take when multiple VEX sources conflict.</summary>
|
||||
public ConflictAction VexStatusConflictAction { get; init; } = ConflictAction.RequestVendorClarification;
|
||||
|
||||
/// <summary>Action to take when backport/status conflict is detected.</summary>
|
||||
public ConflictAction BackportStatusConflictAction { get; init; } = ConflictAction.RequireManualReview;
|
||||
|
||||
/// <summary>Severity threshold above which conflicts require escalation (default: 0.85).</summary>
|
||||
public double EscalationSeverityThreshold { get; init; } = 0.85;
|
||||
|
||||
/// <summary>Time-to-live for conflicts before auto-escalation in hours (default: 48).</summary>
|
||||
public int ConflictTtlHours { get; init; } = 48;
|
||||
|
||||
/// <summary>Enable automatic conflict resolution for low-severity conflicts (default: false).</summary>
|
||||
public bool EnableAutoResolution { get; init; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action to take when a conflict is detected.
|
||||
/// </summary>
|
||||
public enum ConflictAction
|
||||
{
|
||||
/// <summary>Log and continue with existing verdict.</summary>
|
||||
LogAndContinue,
|
||||
|
||||
/// <summary>Require manual security review.</summary>
|
||||
RequireManualReview,
|
||||
|
||||
/// <summary>Request clarification from vendor.</summary>
|
||||
RequestVendorClarification,
|
||||
|
||||
/// <summary>Escalate to security steering committee.</summary>
|
||||
EscalateToCommittee,
|
||||
|
||||
/// <summary>Block release until resolved.</summary>
|
||||
BlockUntilResolved
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-environment threshold configuration.
|
||||
/// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-001)
|
||||
/// </summary>
|
||||
public sealed record EnvironmentThresholds
|
||||
{
|
||||
/// <summary>Development environment thresholds.</summary>
|
||||
public EnvironmentThresholdValues Development { get; init; } = EnvironmentThresholdValues.Relaxed;
|
||||
|
||||
/// <summary>Staging environment thresholds.</summary>
|
||||
public EnvironmentThresholdValues Staging { get; init; } = EnvironmentThresholdValues.Standard;
|
||||
|
||||
/// <summary>Production environment thresholds.</summary>
|
||||
public EnvironmentThresholdValues Production { get; init; } = EnvironmentThresholdValues.Strict;
|
||||
|
||||
/// <summary>Get thresholds for a named environment.</summary>
|
||||
public EnvironmentThresholdValues GetForEnvironment(string environmentName)
|
||||
{
|
||||
return environmentName?.ToUpperInvariant() switch
|
||||
{
|
||||
"DEV" or "DEVELOPMENT" => Development,
|
||||
"STAGE" or "STAGING" or "QA" => Staging,
|
||||
"PROD" or "PRODUCTION" => Production,
|
||||
_ => Staging // Default to staging thresholds
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Threshold values for a specific environment.
|
||||
/// </summary>
|
||||
public sealed record EnvironmentThresholdValues
|
||||
{
|
||||
/// <summary>Maximum entropy allowed for pass verdict.</summary>
|
||||
public double MaxPassEntropy { get; init; }
|
||||
|
||||
/// <summary>Minimum evidence count required for pass verdict.</summary>
|
||||
public int MinEvidenceCount { get; init; }
|
||||
|
||||
/// <summary>Whether DSSE signing is required.</summary>
|
||||
public bool RequireDsseSigning { get; init; }
|
||||
|
||||
/// <summary>Whether Rekor transparency is required.</summary>
|
||||
public bool RequireRekorTransparency { get; init; }
|
||||
|
||||
/// <summary>Standard thresholds for staging-like environments.</summary>
|
||||
public static EnvironmentThresholdValues Standard => new()
|
||||
{
|
||||
MaxPassEntropy = 0.40,
|
||||
MinEvidenceCount = 2,
|
||||
RequireDsseSigning = false,
|
||||
RequireRekorTransparency = false
|
||||
};
|
||||
|
||||
/// <summary>Relaxed thresholds for development environments.</summary>
|
||||
public static EnvironmentThresholdValues Relaxed => new()
|
||||
{
|
||||
MaxPassEntropy = 0.60,
|
||||
MinEvidenceCount = 1,
|
||||
RequireDsseSigning = false,
|
||||
RequireRekorTransparency = false
|
||||
};
|
||||
|
||||
/// <summary>Strict thresholds for production environments.</summary>
|
||||
public static EnvironmentThresholdValues Strict => new()
|
||||
{
|
||||
MaxPassEntropy = 0.25,
|
||||
MinEvidenceCount = 3,
|
||||
RequireDsseSigning = true,
|
||||
RequireRekorTransparency = true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,4 +48,18 @@ public sealed record BackportEvidence
|
||||
/// </summary>
|
||||
[JsonPropertyName("confidence")]
|
||||
public required double Confidence { get; init; }
|
||||
|
||||
// Sprint: SPRINT_20260112_004_BE_policy_determinization_attested_rules (DET-ATT-002)
|
||||
|
||||
/// <summary>
|
||||
/// Anchor metadata for the backport evidence (DSSE envelope, Rekor, etc.).
|
||||
/// </summary>
|
||||
[JsonPropertyName("anchor")]
|
||||
public EvidenceAnchor? Anchor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the backport evidence is anchored (has DSSE/Rekor attestation).
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsAnchored => Anchor?.Anchored == true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) 2025 StellaOps
|
||||
// Sprint: SPRINT_20260112_004_BE_policy_determinization_attested_rules (DET-ATT-002)
|
||||
// Task: Shared anchor metadata for all evidence types
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Shared anchor metadata for cryptographically attested evidence.
|
||||
/// Used across VEX, backport, runtime, and reachability evidence types.
|
||||
/// </summary>
|
||||
public sealed record EvidenceAnchor
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the evidence is anchored with attestation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("anchored")]
|
||||
public required bool Anchored { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// DSSE envelope digest (sha256:hex).
|
||||
/// </summary>
|
||||
[JsonPropertyName("envelope_digest")]
|
||||
public string? EnvelopeDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Predicate type of the attestation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("predicate_type")]
|
||||
public string? PredicateType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor log index if transparency-anchored.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rekor_log_index")]
|
||||
public long? RekorLogIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor entry ID if transparency-anchored.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rekor_entry_id")]
|
||||
public string? RekorEntryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Scope of the attestation (e.g., "finding", "package", "image").
|
||||
/// </summary>
|
||||
[JsonPropertyName("scope")]
|
||||
public string? Scope { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the attestation signature has been verified.
|
||||
/// </summary>
|
||||
[JsonPropertyName("verified")]
|
||||
public bool? Verified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when the attestation was created (UTC).
|
||||
/// </summary>
|
||||
[JsonPropertyName("attested_at")]
|
||||
public DateTimeOffset? AttestedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the evidence is Rekor-anchored (has log index).
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsRekorAnchored => RekorLogIndex.HasValue;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an unanchored evidence anchor.
|
||||
/// </summary>
|
||||
public static EvidenceAnchor Unanchored => new() { Anchored = false };
|
||||
|
||||
/// <summary>
|
||||
/// Creates an anchored evidence anchor with basic info.
|
||||
/// </summary>
|
||||
public static EvidenceAnchor CreateAnchored(
|
||||
string envelopeDigest,
|
||||
string predicateType,
|
||||
long? rekorLogIndex = null,
|
||||
string? rekorEntryId = null,
|
||||
bool? verified = null,
|
||||
DateTimeOffset? attestedAt = null) => new()
|
||||
{
|
||||
Anchored = true,
|
||||
EnvelopeDigest = envelopeDigest,
|
||||
PredicateType = predicateType,
|
||||
RekorLogIndex = rekorLogIndex,
|
||||
RekorEntryId = rekorEntryId,
|
||||
Verified = verified,
|
||||
AttestedAt = attestedAt
|
||||
};
|
||||
}
|
||||
@@ -54,6 +54,20 @@ public sealed record ReachabilityEvidence
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsReachable => Status == ReachabilityStatus.Reachable;
|
||||
|
||||
// Sprint: SPRINT_20260112_004_BE_policy_determinization_attested_rules (DET-ATT-002)
|
||||
|
||||
/// <summary>
|
||||
/// Anchor metadata for the reachability evidence (DSSE envelope, Rekor, etc.).
|
||||
/// </summary>
|
||||
[JsonPropertyName("anchor")]
|
||||
public EvidenceAnchor? Anchor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the reachability evidence is anchored (has DSSE/Rekor attestation).
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsAnchored => Anchor?.Anchored == true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -49,4 +49,18 @@ public sealed record RuntimeEvidence
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool ObservedLoaded => Detected;
|
||||
|
||||
// Sprint: SPRINT_20260112_004_BE_policy_determinization_attested_rules (DET-ATT-002)
|
||||
|
||||
/// <summary>
|
||||
/// Anchor metadata for the runtime evidence (DSSE envelope, Rekor, etc.).
|
||||
/// </summary>
|
||||
[JsonPropertyName("anchor")]
|
||||
public EvidenceAnchor? Anchor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the runtime evidence is anchored (has DSSE/Rekor attestation).
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsAnchored => Anchor?.Anchored == true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
// <copyright file="IDeterminizationConfigStore.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-002)
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Policy.Determinization;
|
||||
|
||||
/// <summary>
|
||||
/// Store for per-tenant determinization configuration with audit trail.
|
||||
/// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-002)
|
||||
/// </summary>
|
||||
public interface IDeterminizationConfigStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the effective configuration for a tenant.
|
||||
/// Returns default config if no tenant-specific config exists.
|
||||
/// </summary>
|
||||
Task<EffectiveDeterminizationConfig> GetEffectiveConfigAsync(
|
||||
string tenantId,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves configuration for a tenant with audit information.
|
||||
/// </summary>
|
||||
Task SaveConfigAsync(
|
||||
string tenantId,
|
||||
DeterminizationOptions config,
|
||||
ConfigAuditInfo auditInfo,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the audit history for a tenant's configuration changes.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<ConfigAuditEntry>> GetAuditHistoryAsync(
|
||||
string tenantId,
|
||||
int limit = 50,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Effective configuration with metadata.
|
||||
/// </summary>
|
||||
public sealed record EffectiveDeterminizationConfig
|
||||
{
|
||||
/// <summary>The active configuration values.</summary>
|
||||
public required DeterminizationOptions Config { get; init; }
|
||||
|
||||
/// <summary>Whether this is the default config or tenant-specific.</summary>
|
||||
public required bool IsDefault { get; init; }
|
||||
|
||||
/// <summary>Tenant ID (null for default).</summary>
|
||||
public string? TenantId { get; init; }
|
||||
|
||||
/// <summary>When the config was last updated.</summary>
|
||||
public DateTimeOffset? LastUpdatedAt { get; init; }
|
||||
|
||||
/// <summary>Who last updated the config.</summary>
|
||||
public string? LastUpdatedBy { get; init; }
|
||||
|
||||
/// <summary>Configuration version for optimistic concurrency.</summary>
|
||||
public int Version { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit information for config changes.
|
||||
/// </summary>
|
||||
public sealed record ConfigAuditInfo
|
||||
{
|
||||
/// <summary>User or system making the change.</summary>
|
||||
public required string Actor { get; init; }
|
||||
|
||||
/// <summary>Reason for the change.</summary>
|
||||
public required string Reason { get; init; }
|
||||
|
||||
/// <summary>Source of the change (UI, API, CLI, etc.).</summary>
|
||||
public string? Source { get; init; }
|
||||
|
||||
/// <summary>Correlation ID for tracing.</summary>
|
||||
public string? CorrelationId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Audit trail entry for config changes.
|
||||
/// </summary>
|
||||
public sealed record ConfigAuditEntry
|
||||
{
|
||||
/// <summary>Unique entry ID.</summary>
|
||||
public required Guid Id { get; init; }
|
||||
|
||||
/// <summary>Tenant ID.</summary>
|
||||
public required string TenantId { get; init; }
|
||||
|
||||
/// <summary>When the change occurred.</summary>
|
||||
public required DateTimeOffset ChangedAt { get; init; }
|
||||
|
||||
/// <summary>User or system making the change.</summary>
|
||||
public required string Actor { get; init; }
|
||||
|
||||
/// <summary>Reason for the change.</summary>
|
||||
public required string Reason { get; init; }
|
||||
|
||||
/// <summary>Source of the change.</summary>
|
||||
public string? Source { get; init; }
|
||||
|
||||
/// <summary>The previous configuration (JSON).</summary>
|
||||
public string? PreviousConfig { get; init; }
|
||||
|
||||
/// <summary>The new configuration (JSON).</summary>
|
||||
public required string NewConfig { get; init; }
|
||||
|
||||
/// <summary>Change summary.</summary>
|
||||
public string? Summary { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of <see cref="IDeterminizationConfigStore"/> for testing.
|
||||
/// Sprint: SPRINT_20260112_012_POLICY_determinization_reanalysis_config (POLICY-CONFIG-002)
|
||||
/// </summary>
|
||||
public sealed class InMemoryDeterminizationConfigStore : IDeterminizationConfigStore
|
||||
{
|
||||
private readonly Dictionary<string, (DeterminizationOptions Config, int Version, DateTimeOffset UpdatedAt, string UpdatedBy)> _configs = new();
|
||||
private readonly List<ConfigAuditEntry> _auditLog = [];
|
||||
private readonly DeterminizationOptions _defaultConfig = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
public Task<EffectiveDeterminizationConfig> GetEffectiveConfigAsync(
|
||||
string tenantId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_configs.TryGetValue(tenantId, out var entry))
|
||||
{
|
||||
return Task.FromResult(new EffectiveDeterminizationConfig
|
||||
{
|
||||
Config = entry.Config,
|
||||
IsDefault = false,
|
||||
TenantId = tenantId,
|
||||
LastUpdatedAt = entry.UpdatedAt,
|
||||
LastUpdatedBy = entry.UpdatedBy,
|
||||
Version = entry.Version
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(new EffectiveDeterminizationConfig
|
||||
{
|
||||
Config = _defaultConfig,
|
||||
IsDefault = true,
|
||||
TenantId = null,
|
||||
LastUpdatedAt = null,
|
||||
LastUpdatedBy = null,
|
||||
Version = 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public Task SaveConfigAsync(
|
||||
string tenantId,
|
||||
DeterminizationOptions config,
|
||||
ConfigAuditInfo auditInfo,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
string? previousConfigJson = null;
|
||||
var version = 1;
|
||||
|
||||
if (_configs.TryGetValue(tenantId, out var existing))
|
||||
{
|
||||
previousConfigJson = System.Text.Json.JsonSerializer.Serialize(existing.Config);
|
||||
version = existing.Version + 1;
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
_configs[tenantId] = (config, version, now, auditInfo.Actor);
|
||||
|
||||
_auditLog.Add(new ConfigAuditEntry
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
ChangedAt = now,
|
||||
Actor = auditInfo.Actor,
|
||||
Reason = auditInfo.Reason,
|
||||
Source = auditInfo.Source,
|
||||
PreviousConfig = previousConfigJson,
|
||||
NewConfig = System.Text.Json.JsonSerializer.Serialize(config),
|
||||
Summary = $"Config updated by {auditInfo.Actor}: {auditInfo.Reason}"
|
||||
});
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<ConfigAuditEntry>> GetAuditHistoryAsync(
|
||||
string tenantId,
|
||||
int limit = 50,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var entries = _auditLog
|
||||
.Where(e => e.TenantId == tenantId)
|
||||
.OrderByDescending(e => e.ChangedAt)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<ConfigAuditEntry>>(entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace StellaOps.Policy.Determinization.Models;
|
||||
/// <summary>
|
||||
/// Result of determinization evaluation.
|
||||
/// Combines observation state, uncertainty score, and guardrails.
|
||||
/// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-001)
|
||||
/// </summary>
|
||||
public sealed record DeterminizationResult
|
||||
{
|
||||
@@ -50,6 +51,13 @@ public sealed record DeterminizationResult
|
||||
[JsonPropertyName("rationale")]
|
||||
public string? Rationale { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reanalysis fingerprint for deterministic replay.
|
||||
/// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-001)
|
||||
/// </summary>
|
||||
[JsonPropertyName("fingerprint")]
|
||||
public ReanalysisFingerprint? Fingerprint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates result for determined observation (low uncertainty).
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
// <copyright file="ReanalysisFingerprint.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-001)
|
||||
// </copyright>
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic fingerprint for reanalysis triggering and replay verification.
|
||||
/// Content-addressed to enable reproducible policy evaluations.
|
||||
/// </summary>
|
||||
public sealed record ReanalysisFingerprint
|
||||
{
|
||||
private static readonly JsonSerializerOptions CanonicalOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Content-addressed fingerprint ID (sha256:...).
|
||||
/// </summary>
|
||||
[JsonPropertyName("fingerprint_id")]
|
||||
public required string FingerprintId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// DSSE bundle digest for evidence provenance.
|
||||
/// </summary>
|
||||
[JsonPropertyName("dsse_bundle_digest")]
|
||||
public string? DsseBundleDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Sorted list of evidence digests contributing to this fingerprint.
|
||||
/// </summary>
|
||||
[JsonPropertyName("evidence_digests")]
|
||||
public IReadOnlyList<string> EvidenceDigests { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Tool versions used for evaluation (deterministic ordering).
|
||||
/// </summary>
|
||||
[JsonPropertyName("tool_versions")]
|
||||
public IReadOnlyDictionary<string, string> ToolVersions { get; init; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>
|
||||
/// Product version under evaluation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("product_version")]
|
||||
public string? ProductVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy configuration hash at evaluation time.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policy_config_hash")]
|
||||
public string? PolicyConfigHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signal weights hash for determinism verification.
|
||||
/// </summary>
|
||||
[JsonPropertyName("signal_weights_hash")]
|
||||
public string? SignalWeightsHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When this fingerprint was computed (UTC ISO-8601).
|
||||
/// </summary>
|
||||
[JsonPropertyName("computed_at")]
|
||||
public required DateTimeOffset ComputedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Triggers that caused this reanalysis.
|
||||
/// </summary>
|
||||
[JsonPropertyName("triggers")]
|
||||
public IReadOnlyList<ReanalysisTrigger> Triggers { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Suggested next actions based on current state.
|
||||
/// </summary>
|
||||
[JsonPropertyName("next_actions")]
|
||||
public IReadOnlyList<string> NextActions { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger that caused a reanalysis.
|
||||
/// </summary>
|
||||
public sealed record ReanalysisTrigger
|
||||
{
|
||||
/// <summary>
|
||||
/// Event type that triggered reanalysis (e.g., epss.updated, vex.changed).
|
||||
/// </summary>
|
||||
[JsonPropertyName("event_type")]
|
||||
public required string EventType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Event version for schema compatibility.
|
||||
/// </summary>
|
||||
[JsonPropertyName("event_version")]
|
||||
public int EventVersion { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Source of the event (e.g., scanner, excititor, signals).
|
||||
/// </summary>
|
||||
[JsonPropertyName("source")]
|
||||
public string? Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the event was received (UTC).
|
||||
/// </summary>
|
||||
[JsonPropertyName("received_at")]
|
||||
public DateTimeOffset ReceivedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Event correlation ID for traceability.
|
||||
/// </summary>
|
||||
[JsonPropertyName("correlation_id")]
|
||||
public string? CorrelationId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder for creating deterministic reanalysis fingerprints.
|
||||
/// </summary>
|
||||
public sealed class ReanalysisFingerprintBuilder
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private string? _dsseBundleDigest;
|
||||
private readonly List<string> _evidenceDigests = [];
|
||||
private readonly SortedDictionary<string, string> _toolVersions = new(StringComparer.Ordinal);
|
||||
private string? _productVersion;
|
||||
private string? _policyConfigHash;
|
||||
private string? _signalWeightsHash;
|
||||
private readonly List<ReanalysisTrigger> _triggers = [];
|
||||
private readonly List<string> _nextActions = [];
|
||||
|
||||
public ReanalysisFingerprintBuilder(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder WithDsseBundleDigest(string? digest)
|
||||
{
|
||||
_dsseBundleDigest = digest;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder AddEvidenceDigest(string digest)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(digest))
|
||||
{
|
||||
_evidenceDigests.Add(digest);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder AddEvidenceDigests(IEnumerable<string> digests)
|
||||
{
|
||||
foreach (var digest in digests)
|
||||
{
|
||||
AddEvidenceDigest(digest);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder WithToolVersion(string tool, string version)
|
||||
{
|
||||
_toolVersions[tool] = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder WithProductVersion(string? version)
|
||||
{
|
||||
_productVersion = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder WithPolicyConfigHash(string? hash)
|
||||
{
|
||||
_policyConfigHash = hash;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder WithSignalWeightsHash(string? hash)
|
||||
{
|
||||
_signalWeightsHash = hash;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder AddTrigger(ReanalysisTrigger trigger)
|
||||
{
|
||||
_triggers.Add(trigger);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder AddTrigger(string eventType, int eventVersion = 1, string? source = null, string? correlationId = null)
|
||||
{
|
||||
_triggers.Add(new ReanalysisTrigger
|
||||
{
|
||||
EventType = eventType,
|
||||
EventVersion = eventVersion,
|
||||
Source = source,
|
||||
ReceivedAt = _timeProvider.GetUtcNow(),
|
||||
CorrelationId = correlationId
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReanalysisFingerprintBuilder AddNextAction(string action)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(action))
|
||||
{
|
||||
_nextActions.Add(action);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the fingerprint with a deterministic content-addressed ID.
|
||||
/// </summary>
|
||||
public ReanalysisFingerprint Build()
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
// Sort evidence digests for determinism
|
||||
var sortedDigests = _evidenceDigests
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(d => d, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
// Sort triggers by event type then received_at for determinism
|
||||
var sortedTriggers = _triggers
|
||||
.OrderBy(t => t.EventType, StringComparer.Ordinal)
|
||||
.ThenBy(t => t.ReceivedAt)
|
||||
.ToList();
|
||||
|
||||
// Sort next actions for determinism
|
||||
var sortedActions = _nextActions
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(a => a, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
// Compute content-addressed fingerprint ID
|
||||
var fingerprintId = ComputeFingerprintId(
|
||||
_dsseBundleDigest,
|
||||
sortedDigests,
|
||||
_toolVersions,
|
||||
_productVersion,
|
||||
_policyConfigHash,
|
||||
_signalWeightsHash);
|
||||
|
||||
return new ReanalysisFingerprint
|
||||
{
|
||||
FingerprintId = fingerprintId,
|
||||
DsseBundleDigest = _dsseBundleDigest,
|
||||
EvidenceDigests = sortedDigests,
|
||||
ToolVersions = new Dictionary<string, string>(_toolVersions),
|
||||
ProductVersion = _productVersion,
|
||||
PolicyConfigHash = _policyConfigHash,
|
||||
SignalWeightsHash = _signalWeightsHash,
|
||||
ComputedAt = now,
|
||||
Triggers = sortedTriggers,
|
||||
NextActions = sortedActions
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeFingerprintId(
|
||||
string? dsseBundleDigest,
|
||||
IReadOnlyList<string> evidenceDigests,
|
||||
IReadOnlyDictionary<string, string> toolVersions,
|
||||
string? productVersion,
|
||||
string? policyConfigHash,
|
||||
string? signalWeightsHash)
|
||||
{
|
||||
// Create canonical representation for hashing
|
||||
var canonical = new
|
||||
{
|
||||
dsse = dsseBundleDigest,
|
||||
evidence = evidenceDigests,
|
||||
tools = toolVersions,
|
||||
product = productVersion,
|
||||
policy = policyConfigHash,
|
||||
weights = signalWeightsHash
|
||||
};
|
||||
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(canonical, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
});
|
||||
|
||||
var hash = SHA256.HashData(json);
|
||||
return "sha256:" + Convert.ToHexStringLower(hash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// <copyright file="SignalConflictExtensions.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-002)
|
||||
// </copyright>
|
||||
|
||||
using StellaOps.Policy.Determinization.Evidence;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for signal conflict detection.
|
||||
/// </summary>
|
||||
public static class SignalConflictExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if VEX status is "not_affected".
|
||||
/// </summary>
|
||||
public static bool IsNotAffected(this SignalState<VexClaimSummary> vex)
|
||||
{
|
||||
return vex.HasValue && vex.Value!.IsNotAffected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if VEX status is "affected".
|
||||
/// </summary>
|
||||
public static bool IsAffected(this SignalState<VexClaimSummary> vex)
|
||||
{
|
||||
return vex.HasValue && string.Equals(vex.Value!.Status, "affected", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if reachability shows exploitable path.
|
||||
/// </summary>
|
||||
public static bool IsExploitable(this SignalState<ReachabilityEvidence> reachability)
|
||||
{
|
||||
return reachability.HasValue && reachability.Value!.IsReachable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if static analysis shows unreachable.
|
||||
/// </summary>
|
||||
public static bool IsStaticUnreachable(this SignalState<ReachabilityEvidence> reachability)
|
||||
{
|
||||
return reachability.HasValue && reachability.Value!.Status == ReachabilityStatus.Unreachable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if runtime telemetry detected execution.
|
||||
/// </summary>
|
||||
public static bool HasExecution(this SignalState<RuntimeEvidence> runtime)
|
||||
{
|
||||
return runtime.HasValue && runtime.Value!.Detected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if multiple VEX sources exist.
|
||||
/// </summary>
|
||||
public static bool HasMultipleSources(this SignalState<VexClaimSummary> vex)
|
||||
{
|
||||
return vex.HasValue && vex.Value!.StatementCount > 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if VEX sources have conflicting status.
|
||||
/// This is determined by low confidence when multiple sources exist.
|
||||
/// </summary>
|
||||
public static bool HasConflictingStatus(this SignalState<VexClaimSummary> vex)
|
||||
{
|
||||
// If there are multiple sources and confidence is below 0.7, they likely conflict
|
||||
return vex.HasValue && vex.Value!.StatementCount > 1 && vex.Value!.Confidence < 0.7;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if backport evidence indicates fix is applied.
|
||||
/// </summary>
|
||||
public static bool IsBackported(this SignalState<BackportEvidence> backport)
|
||||
{
|
||||
return backport.HasValue && backport.Value!.Detected;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
// <copyright file="ConflictDetector.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Sprint: SPRINT_20260112_004_POLICY_unknowns_determinization_greyqueue (POLICY-UNK-002)
|
||||
// </copyright>
|
||||
|
||||
using StellaOps.Policy.Determinization.Evidence;
|
||||
using StellaOps.Policy.Determinization.Models;
|
||||
|
||||
namespace StellaOps.Policy.Determinization.Scoring;
|
||||
|
||||
/// <summary>
|
||||
/// Detects conflicting evidence signals that require manual adjudication.
|
||||
/// </summary>
|
||||
public interface IConflictDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects conflicts in the signal snapshot.
|
||||
/// </summary>
|
||||
ConflictDetectionResult Detect(SignalSnapshot snapshot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of conflict detection.
|
||||
/// </summary>
|
||||
public sealed record ConflictDetectionResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether any conflicts were detected.
|
||||
/// </summary>
|
||||
public bool HasConflict { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// List of detected conflicts.
|
||||
/// </summary>
|
||||
public IReadOnlyList<SignalConflict> Conflicts { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Overall conflict severity (0.0 = none, 1.0 = critical).
|
||||
/// </summary>
|
||||
public double Severity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Suggested adjudication path.
|
||||
/// </summary>
|
||||
public AdjudicationPath SuggestedPath { get; init; } = AdjudicationPath.None;
|
||||
|
||||
public static ConflictDetectionResult NoConflict() => new()
|
||||
{
|
||||
HasConflict = false,
|
||||
Conflicts = [],
|
||||
Severity = 0.0,
|
||||
SuggestedPath = AdjudicationPath.None
|
||||
};
|
||||
|
||||
public static ConflictDetectionResult WithConflicts(
|
||||
IReadOnlyList<SignalConflict> conflicts,
|
||||
double severity,
|
||||
AdjudicationPath suggestedPath) => new()
|
||||
{
|
||||
HasConflict = conflicts.Count > 0,
|
||||
Conflicts = conflicts,
|
||||
Severity = Math.Clamp(severity, 0.0, 1.0),
|
||||
SuggestedPath = suggestedPath
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A detected conflict between signals.
|
||||
/// </summary>
|
||||
public sealed record SignalConflict
|
||||
{
|
||||
/// <summary>
|
||||
/// First signal in the conflict.
|
||||
/// </summary>
|
||||
public required string Signal1 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Second signal in the conflict.
|
||||
/// </summary>
|
||||
public required string Signal2 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of conflict.
|
||||
/// </summary>
|
||||
public required ConflictType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable description.
|
||||
/// </summary>
|
||||
public required string Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Conflict severity (0.0 = minor, 1.0 = critical).
|
||||
/// </summary>
|
||||
public double Severity { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of signal conflict.
|
||||
/// </summary>
|
||||
public enum ConflictType
|
||||
{
|
||||
/// <summary>
|
||||
/// VEX says not_affected but reachability shows exploitable path.
|
||||
/// </summary>
|
||||
VexReachabilityContradiction,
|
||||
|
||||
/// <summary>
|
||||
/// Static analysis says unreachable but runtime telemetry shows execution.
|
||||
/// </summary>
|
||||
StaticRuntimeContradiction,
|
||||
|
||||
/// <summary>
|
||||
/// Multiple VEX statements with conflicting status.
|
||||
/// </summary>
|
||||
VexStatusConflict,
|
||||
|
||||
/// <summary>
|
||||
/// Backport evidence conflicts with vulnerability status.
|
||||
/// </summary>
|
||||
BackportStatusConflict,
|
||||
|
||||
/// <summary>
|
||||
/// EPSS score conflicts with other risk indicators.
|
||||
/// </summary>
|
||||
EpssRiskContradiction,
|
||||
|
||||
/// <summary>
|
||||
/// Other conflict type.
|
||||
/// </summary>
|
||||
Other
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suggested adjudication path for conflicts.
|
||||
/// </summary>
|
||||
public enum AdjudicationPath
|
||||
{
|
||||
/// <summary>
|
||||
/// No adjudication needed.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Automatic resolution possible with additional evidence.
|
||||
/// </summary>
|
||||
AutoResolvable,
|
||||
|
||||
/// <summary>
|
||||
/// Requires human review by security team.
|
||||
/// </summary>
|
||||
SecurityTeamReview,
|
||||
|
||||
/// <summary>
|
||||
/// Requires vendor clarification.
|
||||
/// </summary>
|
||||
VendorClarification,
|
||||
|
||||
/// <summary>
|
||||
/// Escalate to security steering committee.
|
||||
/// </summary>
|
||||
SteeringCommittee
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of conflict detection.
|
||||
/// </summary>
|
||||
public sealed class ConflictDetector : IConflictDetector
|
||||
{
|
||||
private readonly ILogger<ConflictDetector> _logger;
|
||||
|
||||
public ConflictDetector(ILogger<ConflictDetector> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ConflictDetectionResult Detect(SignalSnapshot snapshot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
|
||||
var conflicts = new List<SignalConflict>();
|
||||
|
||||
// Check VEX vs Reachability contradiction
|
||||
CheckVexReachabilityConflict(snapshot, conflicts);
|
||||
|
||||
// Check Static vs Runtime contradiction
|
||||
CheckStaticRuntimeConflict(snapshot, conflicts);
|
||||
|
||||
// Check multiple VEX statements
|
||||
CheckVexStatusConflict(snapshot, conflicts);
|
||||
|
||||
// Check Backport vs Status conflict
|
||||
CheckBackportStatusConflict(snapshot, conflicts);
|
||||
|
||||
if (conflicts.Count == 0)
|
||||
{
|
||||
return ConflictDetectionResult.NoConflict();
|
||||
}
|
||||
|
||||
// Calculate overall severity (max of all conflicts)
|
||||
var severity = conflicts.Max(c => c.Severity);
|
||||
|
||||
// Determine adjudication path based on conflict types and severity
|
||||
var suggestedPath = DetermineAdjudicationPath(conflicts, severity);
|
||||
|
||||
_logger.LogWarning(
|
||||
"Detected {ConflictCount} signal conflicts for CVE {Cve} / PURL {Purl} with severity {Severity:F2}",
|
||||
conflicts.Count,
|
||||
snapshot.Cve,
|
||||
snapshot.Purl,
|
||||
severity);
|
||||
|
||||
return ConflictDetectionResult.WithConflicts(
|
||||
conflicts.OrderBy(c => c.Type).ThenByDescending(c => c.Severity).ToList(),
|
||||
severity,
|
||||
suggestedPath);
|
||||
}
|
||||
|
||||
private static void CheckVexReachabilityConflict(SignalSnapshot snapshot, List<SignalConflict> conflicts)
|
||||
{
|
||||
// VEX says not_affected but reachability shows exploitable
|
||||
if (snapshot.Vex.IsNotAffected && snapshot.Reachability.IsExploitable)
|
||||
{
|
||||
conflicts.Add(new SignalConflict
|
||||
{
|
||||
Signal1 = "VEX",
|
||||
Signal2 = "Reachability",
|
||||
Type = ConflictType.VexReachabilityContradiction,
|
||||
Description = "VEX status is not_affected but reachability analysis shows exploitable path",
|
||||
Severity = 0.9 // High severity - needs resolution
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckStaticRuntimeConflict(SignalSnapshot snapshot, List<SignalConflict> conflicts)
|
||||
{
|
||||
// Static says unreachable but runtime shows execution
|
||||
if (snapshot.Reachability.IsStaticUnreachable && snapshot.Runtime.HasExecution)
|
||||
{
|
||||
conflicts.Add(new SignalConflict
|
||||
{
|
||||
Signal1 = "StaticReachability",
|
||||
Signal2 = "RuntimeTelemetry",
|
||||
Type = ConflictType.StaticRuntimeContradiction,
|
||||
Description = "Static analysis shows unreachable but runtime telemetry detected execution",
|
||||
Severity = 0.85 // High severity - static analysis may be incomplete
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckVexStatusConflict(SignalSnapshot snapshot, List<SignalConflict> conflicts)
|
||||
{
|
||||
// Multiple VEX sources with conflicting status
|
||||
if (snapshot.Vex.HasMultipleSources && snapshot.Vex.HasConflictingStatus)
|
||||
{
|
||||
conflicts.Add(new SignalConflict
|
||||
{
|
||||
Signal1 = "VEX:Source1",
|
||||
Signal2 = "VEX:Source2",
|
||||
Type = ConflictType.VexStatusConflict,
|
||||
Description = "Multiple VEX statements with conflicting status",
|
||||
Severity = 0.7 // Medium-high - needs vendor clarification
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckBackportStatusConflict(SignalSnapshot snapshot, List<SignalConflict> conflicts)
|
||||
{
|
||||
// Backport says fixed but vulnerability still active
|
||||
if (snapshot.Backport.IsBackported && snapshot.Vex.IsAffected)
|
||||
{
|
||||
conflicts.Add(new SignalConflict
|
||||
{
|
||||
Signal1 = "Backport",
|
||||
Signal2 = "VEX",
|
||||
Type = ConflictType.BackportStatusConflict,
|
||||
Description = "Backport evidence indicates fix applied but VEX status shows affected",
|
||||
Severity = 0.6 // Medium - may be version mismatch
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static AdjudicationPath DetermineAdjudicationPath(IReadOnlyList<SignalConflict> conflicts, double severity)
|
||||
{
|
||||
// Critical conflicts go to steering committee
|
||||
if (severity >= 0.95)
|
||||
{
|
||||
return AdjudicationPath.SteeringCommittee;
|
||||
}
|
||||
|
||||
// VEX conflicts need vendor clarification
|
||||
if (conflicts.Any(c => c.Type == ConflictType.VexStatusConflict))
|
||||
{
|
||||
return AdjudicationPath.VendorClarification;
|
||||
}
|
||||
|
||||
// High severity needs security team review
|
||||
if (severity >= 0.7)
|
||||
{
|
||||
return AdjudicationPath.SecurityTeamReview;
|
||||
}
|
||||
|
||||
// Lower severity may be auto-resolvable with more evidence
|
||||
return AdjudicationPath.AutoResolvable;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user