sprints work

This commit is contained in:
StellaOps Bot
2025-12-25 12:19:12 +02:00
parent 223843f1d1
commit 2a06f780cf
224 changed files with 41796 additions and 1515 deletions

View File

@@ -7,6 +7,7 @@ using StellaOps.Policy.Confidence.Models;
using StellaOps.Policy.Exceptions.Models;
using StellaOps.Policy.Unknowns.Models;
using StellaOps.PolicyDsl;
using StellaOps.Signals.EvidenceWeightedScore;
namespace StellaOps.Policy.Engine.Evaluation;
@@ -128,7 +129,8 @@ internal sealed record PolicyEvaluationResult(
ConfidenceScore? Confidence,
PolicyFailureReason? FailureReason = null,
string? FailureMessage = null,
BudgetStatusSummary? UnknownBudgetStatus = null)
BudgetStatusSummary? UnknownBudgetStatus = null,
EvidenceWeightedScoreResult? EvidenceWeightedScore = null)
{
public static PolicyEvaluationResult CreateDefault(string? severity) => new(
Matched: false,
@@ -139,7 +141,8 @@ internal sealed record PolicyEvaluationResult(
Annotations: ImmutableDictionary<string, string>.Empty,
Warnings: ImmutableArray<string>.Empty,
AppliedException: null,
Confidence: null);
Confidence: null,
EvidenceWeightedScore: null);
}
internal enum PolicyFailureReason

View File

@@ -10,10 +10,15 @@ using StellaOps.Policy;
using StellaOps.Policy.Confidence.Configuration;
using StellaOps.Policy.Confidence.Models;
using StellaOps.Policy.Confidence.Services;
using StellaOps.Policy.Engine.Scoring.EvidenceWeightedScore;
using StellaOps.Policy.Unknowns.Models;
using StellaOps.Policy.Unknowns.Services;
using StellaOps.PolicyDsl;
// Alias Confidence types to avoid ambiguity with EWS types
using ConfidenceReachabilityState = StellaOps.Policy.Confidence.Models.ReachabilityState;
using ConfidenceRuntimePosture = StellaOps.Policy.Confidence.Models.RuntimePosture;
namespace StellaOps.Policy.Engine.Evaluation;
/// <summary>
@@ -23,15 +28,18 @@ internal sealed class PolicyEvaluator
{
private readonly IConfidenceCalculator _confidenceCalculator;
private readonly IUnknownBudgetService? _budgetService;
private readonly IFindingScoreEnricher? _scoreEnricher;
public PolicyEvaluator(
IConfidenceCalculator? confidenceCalculator = null,
IUnknownBudgetService? budgetService = null)
IUnknownBudgetService? budgetService = null,
IFindingScoreEnricher? scoreEnricher = null)
{
_confidenceCalculator = confidenceCalculator
?? new ConfidenceCalculator(
new StaticOptionsMonitor<ConfidenceWeightOptions>(new ConfidenceWeightOptions()));
_budgetService = budgetService;
_scoreEnricher = scoreEnricher;
}
public PolicyEvaluationResult Evaluate(PolicyEvaluationRequest request)
@@ -46,7 +54,10 @@ internal sealed class PolicyEvaluator
throw new ArgumentNullException(nameof(request.Document));
}
var evaluator = new PolicyExpressionEvaluator(request.Context);
// Pre-compute EWS so it's available during rule evaluation for score-based rules
var precomputedScore = PrecomputeEvidenceWeightedScore(request.Context);
var evaluator = new PolicyExpressionEvaluator(request.Context, precomputedScore);
var orderedRules = request.Document.Rules
.Select(static (rule, index) => new { rule, index })
.OrderBy(x => x.rule.Priority)
@@ -85,13 +96,15 @@ internal sealed class PolicyEvaluator
var result = ApplyExceptions(request, baseResult);
var budgeted = ApplyUnknownBudget(request.Context, result);
return ApplyConfidence(request.Context, budgeted);
var withConfidence = ApplyConfidence(request.Context, budgeted);
return ApplyEvidenceWeightedScore(request.Context, withConfidence, precomputedScore);
}
var defaultResult = PolicyEvaluationResult.CreateDefault(request.Context.Severity.Normalized);
var defaultWithExceptions = ApplyExceptions(request, defaultResult);
var budgetedDefault = ApplyUnknownBudget(request.Context, defaultWithExceptions);
return ApplyConfidence(request.Context, budgetedDefault);
var defaultWithConfidence = ApplyConfidence(request.Context, budgetedDefault);
return ApplyEvidenceWeightedScore(request.Context, defaultWithConfidence, precomputedScore);
}
private static void ApplyAction(
@@ -513,6 +526,139 @@ internal sealed class PolicyEvaluator
return baseResult with { Confidence = confidence };
}
/// <summary>
/// Pre-computes the Evidence-Weighted Score before rule evaluation so it's available
/// for score-based policy rules (e.g., "when score >= 80 then block").
/// </summary>
private global::StellaOps.Signals.EvidenceWeightedScore.EvidenceWeightedScoreResult? PrecomputeEvidenceWeightedScore(
PolicyEvaluationContext context)
{
// Skip if no enricher configured
if (_scoreEnricher is null || !_scoreEnricher.IsEnabled)
{
return null;
}
try
{
// Generate finding ID from context
var findingId = GenerateFindingIdFromContext(context);
// Extract evidence from context
var evidence = context.ExtractEwsEvidence(
findingId,
epssScore: context.Advisory.Metadata.TryGetValue("epss.score", out var epssStr)
? double.TryParse(epssStr, out var epss) ? epss : null
: null,
epssPercentile: context.Advisory.Metadata.TryGetValue("epss.percentile", out var epssPercStr)
? double.TryParse(epssPercStr, out var epssPerc) ? epssPerc : null
: null,
isInKev: context.Advisory.Metadata.TryGetValue("kev.status", out var kevStatus)
&& kevStatus.Equals("true", StringComparison.OrdinalIgnoreCase),
kevAddedDate: context.Advisory.Metadata.TryGetValue("kev.added", out var kevAddedStr)
? DateTimeOffset.TryParse(kevAddedStr, out var kevAdded) ? kevAdded : null
: null);
// Calculate score synchronously
var enrichmentResult = _scoreEnricher.Enrich(evidence);
return enrichmentResult.IsSuccess ? enrichmentResult.Score : null;
}
catch
{
// Pre-computation should not fail the evaluation
return null;
}
}
/// <summary>
/// Generates a deterministic finding ID from context (without requiring result).
/// </summary>
private static string GenerateFindingIdFromContext(PolicyEvaluationContext context)
{
var source = context.Advisory.Source ?? "unknown";
var severity = context.Severity.Normalized ?? "unknown";
// Use advisory metadata CVE ID if available
if (context.Advisory.Metadata.TryGetValue("cve", out var cve) && !string.IsNullOrEmpty(cve))
{
return $"finding:{cve}:{source}";
}
// Fall back to deterministic hash
var input = $"{source}|{severity}|{context.Now:O}";
Span<byte> hash = stackalloc byte[32];
SHA256.HashData(Encoding.UTF8.GetBytes(input), hash);
return $"finding:sha256:{Convert.ToHexString(hash).ToLowerInvariant()[..16]}";
}
/// <summary>
/// Applies Evidence-Weighted Score enrichment if the enricher is available and enabled.
/// Uses pre-computed score if available to avoid recalculation.
/// </summary>
private PolicyEvaluationResult ApplyEvidenceWeightedScore(
PolicyEvaluationContext context,
PolicyEvaluationResult baseResult,
global::StellaOps.Signals.EvidenceWeightedScore.EvidenceWeightedScoreResult? precomputedScore = null)
{
// Use precomputed score if available
var score = precomputedScore;
// If no precomputed score and enricher is enabled, compute now
if (score is null && _scoreEnricher is not null && _scoreEnricher.IsEnabled)
{
score = PrecomputeEvidenceWeightedScore(context);
}
// Skip if no score available
if (score is null)
{
return baseResult;
}
try
{
// Add score to annotations for DSL access
var annotations = baseResult.Annotations.ToBuilder();
annotations["ews.score"] = score.Score.ToString("F2", CultureInfo.InvariantCulture);
annotations["ews.bucket"] = score.Bucket.ToString();
return baseResult with
{
EvidenceWeightedScore = score,
Annotations = annotations.ToImmutable()
};
}
catch
{
// Score enrichment should not fail the evaluation
// Return base result unchanged
return baseResult;
}
}
/// <summary>
/// Generates a deterministic finding ID from evaluation context.
/// </summary>
private static string GenerateFindingId(PolicyEvaluationContext context, PolicyEvaluationResult result)
{
var source = context.Advisory.Source ?? "unknown";
var severity = context.Severity.Normalized ?? "unknown";
var ruleName = result.RuleName ?? "default";
// Use advisory metadata CVE ID if available
if (context.Advisory.Metadata.TryGetValue("cve", out var cve) && !string.IsNullOrEmpty(cve))
{
return $"finding:{cve}:{source}";
}
// Fall back to deterministic hash
var input = $"{source}|{severity}|{ruleName}|{context.Now:O}";
Span<byte> hash = stackalloc byte[32];
SHA256.HashData(Encoding.UTF8.GetBytes(input), hash);
return $"finding:sha256:{Convert.ToHexString(hash).ToLowerInvariant()[..16]}";
}
private static ConfidenceInput BuildConfidenceInput(PolicyEvaluationContext context, PolicyEvaluationResult result)
{
return new ConfidenceInput
@@ -535,10 +681,10 @@ internal sealed class PolicyEvaluator
}
var state = reachability.IsReachable
? (reachability.HasRuntimeEvidence ? ReachabilityState.ConfirmedReachable : ReachabilityState.StaticReachable)
? (reachability.HasRuntimeEvidence ? ConfidenceReachabilityState.ConfirmedReachable : ConfidenceReachabilityState.StaticReachable)
: reachability.IsUnreachable
? (reachability.HasRuntimeEvidence ? ReachabilityState.ConfirmedUnreachable : ReachabilityState.StaticUnreachable)
: ReachabilityState.Unknown;
? (reachability.HasRuntimeEvidence ? ConfidenceReachabilityState.ConfirmedUnreachable : ConfidenceReachabilityState.StaticUnreachable)
: ConfidenceReachabilityState.Unknown;
var digests = string.IsNullOrWhiteSpace(reachability.EvidenceRef)
? Array.Empty<string>()
@@ -560,8 +706,8 @@ internal sealed class PolicyEvaluator
}
var posture = context.Reachability.IsReachable || context.Reachability.IsUnreachable
? RuntimePosture.Supports
: RuntimePosture.Unknown;
? ConfidenceRuntimePosture.Supports
: ConfidenceRuntimePosture.Unknown;
return new RuntimeEvidence
{

View File

@@ -4,6 +4,7 @@ using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using StellaOps.PolicyDsl;
using StellaOps.Signals.EvidenceWeightedScore;
namespace StellaOps.Policy.Engine.Evaluation;
@@ -23,10 +24,14 @@ internal sealed class PolicyExpressionEvaluator
};
private readonly PolicyEvaluationContext context;
private readonly EvidenceWeightedScoreResult? _evidenceWeightedScore;
public PolicyExpressionEvaluator(PolicyEvaluationContext context)
public PolicyExpressionEvaluator(
PolicyEvaluationContext context,
EvidenceWeightedScoreResult? evidenceWeightedScore = null)
{
this.context = context ?? throw new ArgumentNullException(nameof(context));
_evidenceWeightedScore = evidenceWeightedScore;
}
public EvaluationValue Evaluate(PolicyExpression expression, EvaluationScope? scope = null)
@@ -65,6 +70,9 @@ internal sealed class PolicyExpressionEvaluator
"sbom" => new EvaluationValue(new SbomScope(context.Sbom)),
"reachability" => new EvaluationValue(new ReachabilityScope(context.Reachability)),
"entropy" => new EvaluationValue(new EntropyScope(context.Entropy)),
"score" => _evidenceWeightedScore is not null
? new EvaluationValue(new ScoreScope(_evidenceWeightedScore))
: EvaluationValue.Null,
"now" => new EvaluationValue(context.Now),
"true" => EvaluationValue.True,
"false" => EvaluationValue.False,
@@ -111,6 +119,11 @@ internal sealed class PolicyExpressionEvaluator
return entropy.Get(member.Member);
}
if (raw is ScoreScope scoreScope)
{
return scoreScope.Get(member.Member);
}
if (raw is ComponentScope componentScope)
{
return componentScope.Get(member.Member);
@@ -202,6 +215,22 @@ internal sealed class PolicyExpressionEvaluator
{
return advisoryScope.Invoke(member.Member, invocation.Arguments, scope, this);
}
if (root.Name == "score" && targetRaw is ScoreScope scoreScope)
{
return member.Member.ToLowerInvariant() switch
{
"has_flag" or "hasflag" => invocation.Arguments.Length > 0
? scoreScope.HasFlag(Evaluate(invocation.Arguments[0], scope).AsString() ?? "")
: EvaluationValue.False,
"between" => invocation.Arguments.Length >= 2
? scoreScope.Between(
Evaluate(invocation.Arguments[0], scope).AsDecimal() ?? 0m,
Evaluate(invocation.Arguments[1], scope).AsDecimal() ?? 100m)
: EvaluationValue.False,
_ => EvaluationValue.Null,
};
}
}
}
@@ -915,6 +944,94 @@ internal sealed class PolicyExpressionEvaluator
};
}
/// <summary>
/// SPL scope for Evidence-Weighted Score predicates.
/// Provides access to score value, bucket, flags, and individual dimensions.
/// </summary>
/// <example>
/// SPL predicates supported:
/// - score >= 80
/// - score.value >= 80
/// - score.bucket == "ActNow"
/// - score.is_act_now == true
/// - score.rch > 0.8
/// - score.runt > 0.5
/// - score.has_flag("live-signal")
/// - score.flags contains "kev"
/// </example>
private sealed class ScoreScope
{
private readonly EvidenceWeightedScoreResult score;
public ScoreScope(EvidenceWeightedScoreResult score)
{
this.score = score;
}
public EvaluationValue Get(string member) => member.ToLowerInvariant() switch
{
// Core score value (allows direct comparison: score >= 80)
"value" => new EvaluationValue(score.Score),
// Bucket access
"bucket" => new EvaluationValue(score.Bucket.ToString()),
"is_act_now" or "isactnow" => new EvaluationValue(score.Bucket == ScoreBucket.ActNow),
"is_schedule_next" or "isschedulenext" => new EvaluationValue(score.Bucket == ScoreBucket.ScheduleNext),
"is_investigate" or "isinvestigate" => new EvaluationValue(score.Bucket == ScoreBucket.Investigate),
"is_watchlist" or "iswatchlist" => new EvaluationValue(score.Bucket == ScoreBucket.Watchlist),
// Individual dimension scores (0-1 normalized) - using Breakdown
"rch" or "reachability" => new EvaluationValue(GetDimensionInput("RCH")),
"rts" or "runtime" => new EvaluationValue(GetDimensionInput("RTS")),
"bkp" or "backport" => new EvaluationValue(GetDimensionInput("BKP")),
"xpl" or "exploit" => new EvaluationValue(GetDimensionInput("XPL")),
"src" or "source_trust" => new EvaluationValue(GetDimensionInput("SRC")),
"mit" or "mitigation" => new EvaluationValue(GetDimensionInput("MIT")),
// Flags as array
"flags" => new EvaluationValue(score.Flags.Select(f => (object?)f).ToImmutableArray()),
// Policy info
"policy_digest" or "policydigest" => new EvaluationValue(score.PolicyDigest),
// Calculation metadata
"calculated_at" or "calculatedat" => new EvaluationValue(score.CalculatedAt),
// Explanations
"explanations" => new EvaluationValue(score.Explanations.Select(e => (object?)e).ToImmutableArray()),
_ => EvaluationValue.Null,
};
private double GetDimensionInput(string symbol)
{
var contribution = score.Breakdown.FirstOrDefault(c =>
c.Symbol.Equals(symbol, StringComparison.OrdinalIgnoreCase));
return contribution?.InputValue ?? 0.0;
}
/// <summary>
/// Check if score has a specific flag.
/// </summary>
public EvaluationValue HasFlag(string flagName)
{
if (string.IsNullOrWhiteSpace(flagName))
{
return EvaluationValue.False;
}
return new EvaluationValue(score.Flags.Contains(flagName, StringComparer.OrdinalIgnoreCase));
}
/// <summary>
/// Check if score is between min and max (inclusive).
/// </summary>
public EvaluationValue Between(decimal min, decimal max)
{
return new EvaluationValue(score.Score >= min && score.Score <= max);
}
}
/// <summary>
/// SPL scope for macOS component predicates.
/// Provides access to bundle signing, entitlements, sandboxing, and package receipt information.

View File

@@ -0,0 +1,323 @@
// -----------------------------------------------------------------------------
// VerdictSummary.cs
// Sprint: SPRINT_8200_0012_0003_policy_engine_integration
// Task: PINT-8200-024
// Description: VerdictSummary extension for including EWS bucket and top factors
// -----------------------------------------------------------------------------
using System.Collections.Immutable;
using StellaOps.Signals.EvidenceWeightedScore;
namespace StellaOps.Policy.Engine.Evaluation;
/// <summary>
/// A summarized view of a policy evaluation result, including evidence-weighted
/// score bucket and top contributing factors for quick triage visualization.
/// </summary>
public sealed record VerdictSummary
{
/// <summary>The overall verdict status (e.g., "affected", "not_affected").</summary>
public required string Status { get; init; }
/// <summary>The severity level (Critical, High, Medium, Low, Info).</summary>
public string? Severity { get; init; }
/// <summary>Whether a rule matched this finding.</summary>
public bool RuleMatched { get; init; }
/// <summary>Name of the matching rule, if any.</summary>
public string? RuleName { get; init; }
/// <summary>Rule priority, if applicable.</summary>
public int? Priority { get; init; }
/// <summary>Evidence-weighted score bucket for quick triage.</summary>
public string? ScoreBucket { get; init; }
/// <summary>Numeric score (0-100) from evidence-weighted scoring.</summary>
public int? Score { get; init; }
/// <summary>
/// Top contributing factors from EWS breakdown, ordered by contribution magnitude.
/// Each entry contains the dimension name and its contribution.
/// </summary>
public ImmutableArray<VerdictFactor> TopFactors { get; init; } = [];
/// <summary>Active flags from EWS (e.g., "live-signal", "kev", "vendor-na").</summary>
public ImmutableArray<string> Flags { get; init; } = [];
/// <summary>Human-readable explanations for the score.</summary>
public ImmutableArray<string> Explanations { get; init; } = [];
/// <summary>Whether guardrails (caps/floors) were applied to the score.</summary>
public bool GuardrailsApplied { get; init; }
/// <summary>Warnings emitted during evaluation.</summary>
public ImmutableArray<string> Warnings { get; init; } = [];
/// <summary>Whether an exception was applied to this finding.</summary>
public bool ExceptionApplied { get; init; }
/// <summary>Legacy confidence score, if available.</summary>
public decimal? ConfidenceScore { get; init; }
/// <summary>Legacy confidence band, if available.</summary>
public string? ConfidenceBand { get; init; }
}
/// <summary>
/// A single contributing factor to the evidence-weighted score.
/// </summary>
public sealed record VerdictFactor
{
/// <summary>Full dimension name (e.g., "Reachability", "Runtime Signal").</summary>
public required string Dimension { get; init; }
/// <summary>Short symbol (e.g., "RCH", "RTS", "XPL").</summary>
public required string Symbol { get; init; }
/// <summary>Contribution to the score (positive for additive, negative for subtractive).</summary>
public required double Contribution { get; init; }
/// <summary>Weight applied to this dimension.</summary>
public required double Weight { get; init; }
/// <summary>Normalized input value [0, 1].</summary>
public required double InputValue { get; init; }
/// <summary>Whether this is a subtractive factor (like Mitigation).</summary>
public bool IsSubtractive { get; init; }
}
/// <summary>
/// Extension methods for creating <see cref="VerdictSummary"/> from evaluation results.
/// </summary>
internal static class VerdictSummaryExtensions
{
/// <summary>
/// Maximum number of top factors to include in the summary.
/// </summary>
private const int MaxTopFactors = 5;
/// <summary>
/// Creates a <see cref="VerdictSummary"/> from a <see cref="PolicyEvaluationResult"/>.
/// </summary>
/// <param name="result">The policy evaluation result.</param>
/// <returns>A summarized view of the verdict including EWS bucket and top factors.</returns>
internal static VerdictSummary ToSummary(this PolicyEvaluationResult result)
{
ArgumentNullException.ThrowIfNull(result);
var ews = result.EvidenceWeightedScore;
return new VerdictSummary
{
Status = result.Status,
Severity = result.Severity,
RuleMatched = result.Matched,
RuleName = result.RuleName,
Priority = result.Priority,
ScoreBucket = ews?.Bucket.ToString(),
Score = ews?.Score,
TopFactors = ExtractTopFactors(ews),
Flags = ews?.Flags.ToImmutableArray() ?? [],
Explanations = ews?.Explanations.ToImmutableArray() ?? [],
GuardrailsApplied = ews?.Caps.AnyApplied ?? false,
Warnings = result.Warnings,
ExceptionApplied = result.AppliedException is not null,
ConfidenceScore = result.Confidence?.Value,
ConfidenceBand = result.Confidence?.Tier.ToString(),
};
}
/// <summary>
/// Creates a minimal <see cref="VerdictSummary"/> with only status and rule info.
/// Use this for quick serialization when EWS details are not needed.
/// </summary>
/// <param name="result">The policy evaluation result.</param>
/// <returns>A minimal summarized view.</returns>
internal static VerdictSummary ToMinimalSummary(this PolicyEvaluationResult result)
{
ArgumentNullException.ThrowIfNull(result);
return new VerdictSummary
{
Status = result.Status,
Severity = result.Severity,
RuleMatched = result.Matched,
RuleName = result.RuleName,
Priority = result.Priority,
ScoreBucket = result.EvidenceWeightedScore?.Bucket.ToString(),
Score = result.EvidenceWeightedScore?.Score,
Warnings = result.Warnings,
ExceptionApplied = result.AppliedException is not null,
};
}
/// <summary>
/// Extracts the top contributing factors from the EWS breakdown,
/// ordered by absolute contribution magnitude (descending).
/// </summary>
private static ImmutableArray<VerdictFactor> ExtractTopFactors(EvidenceWeightedScoreResult? ews)
{
if (ews?.Breakdown is null || ews.Breakdown.Count == 0)
{
return [];
}
return ews.Breakdown
.OrderByDescending(d => Math.Abs(d.Contribution))
.Take(MaxTopFactors)
.Select(d => new VerdictFactor
{
Dimension = d.Dimension,
Symbol = d.Symbol,
Contribution = d.Contribution,
Weight = d.Weight,
InputValue = d.InputValue,
IsSubtractive = d.IsSubtractive,
})
.ToImmutableArray();
}
/// <summary>
/// Gets the primary contributing factor from the EWS breakdown.
/// Returns null if no breakdown is available.
/// </summary>
/// <param name="ews">The evidence-weighted score result.</param>
/// <returns>The highest-contributing factor, or null.</returns>
public static VerdictFactor? GetPrimaryFactor(this EvidenceWeightedScoreResult? ews)
{
if (ews?.Breakdown is null || ews.Breakdown.Count == 0)
{
return null;
}
var primary = ews.Breakdown
.OrderByDescending(d => Math.Abs(d.Contribution))
.FirstOrDefault();
if (primary is null)
{
return null;
}
return new VerdictFactor
{
Dimension = primary.Dimension,
Symbol = primary.Symbol,
Contribution = primary.Contribution,
Weight = primary.Weight,
InputValue = primary.InputValue,
IsSubtractive = primary.IsSubtractive,
};
}
/// <summary>
/// Formats the verdict summary as a single-line triage string.
/// Example: "[ActNow 92] CVE-2024-1234: RCH(+35), XPL(+28), RTS(+20) | live-signal"
/// </summary>
/// <param name="summary">The verdict summary.</param>
/// <param name="findingId">Optional finding ID to include.</param>
/// <returns>A formatted triage string.</returns>
public static string FormatTriageLine(this VerdictSummary summary, string? findingId = null)
{
ArgumentNullException.ThrowIfNull(summary);
var parts = new List<string>();
// Score bucket and value
if (summary.Score.HasValue)
{
parts.Add($"[{summary.ScoreBucket ?? "?"} {summary.Score}]");
}
// Finding ID if provided
if (!string.IsNullOrEmpty(findingId))
{
parts.Add($"{findingId}:");
}
// Top factors
if (summary.TopFactors.Length > 0)
{
var factors = summary.TopFactors
.Take(3)
.Select(f => $"{f.Symbol}({(f.Contribution >= 0 ? "+" : "")}{f.Contribution:F0})")
.ToArray();
parts.Add(string.Join(", ", factors));
}
// Flags
if (summary.Flags.Length > 0)
{
parts.Add($"| {string.Join(", ", summary.Flags.Take(3))}");
}
return string.Join(" ", parts);
}
/// <summary>
/// Gets a brief explanation of why this verdict received its score bucket.
/// </summary>
/// <param name="summary">The verdict summary.</param>
/// <returns>A human-readable explanation.</returns>
public static string GetBucketExplanation(this VerdictSummary summary)
{
ArgumentNullException.ThrowIfNull(summary);
if (!summary.Score.HasValue)
{
return "No evidence-weighted score available.";
}
var bucket = summary.ScoreBucket;
var score = summary.Score.Value;
var explanation = bucket switch
{
"ActNow" => $"Score {score}/100: Strong evidence of exploitable risk. Immediate action recommended.",
"ScheduleNext" => $"Score {score}/100: Likely real risk. Schedule remediation for next sprint.",
"Investigate" => $"Score {score}/100: Moderate evidence. Investigate when working on this component.",
"Watchlist" => $"Score {score}/100: Insufficient evidence. Monitor for changes.",
_ => $"Score {score}/100."
};
// Add primary factor context
if (summary.TopFactors.Length > 0)
{
var primary = summary.TopFactors[0];
var factorContext = primary.Symbol switch
{
"RCH" => "Reachability analysis is the primary driver.",
"RTS" => "Runtime signals detected exploitation activity.",
"XPL" => "Known exploit evidence is significant.",
"BKP" => "Backport information affects the score.",
"SRC" => "Source trust levels impact the assessment.",
"MIT" => "Mitigations reduce the effective risk.",
_ => null
};
if (factorContext is not null)
{
explanation = $"{explanation} {factorContext}";
}
}
// Add flag context
if (summary.Flags.Contains("live-signal"))
{
explanation = $"{explanation} ALERT: Live exploitation signal detected!";
}
else if (summary.Flags.Contains("kev"))
{
explanation = $"{explanation} This is a Known Exploited Vulnerability (KEV).";
}
else if (summary.Flags.Contains("vendor-na"))
{
explanation = $"{explanation} Vendor has confirmed not affected.";
}
return explanation;
}
}