house keeping work
This commit is contained in:
@@ -0,0 +1,478 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PrReachabilityGate.cs
|
||||
// Sprint: SPRINT_3700_0006_0001_incremental_cache (CACHE-014)
|
||||
// Description: Evaluates incremental reachability results for PR gate decisions.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Cache;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for PR reachability gate evaluation.
|
||||
/// </summary>
|
||||
public sealed class PrReachabilityGateOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Section name for configuration binding.
|
||||
/// </summary>
|
||||
public const string SectionName = "Scanner:Reachability:PrGate";
|
||||
|
||||
/// <summary>
|
||||
/// Whether the PR gate is enabled. Default: true.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to block PRs that introduce new reachable vulnerabilities. Default: true.
|
||||
/// </summary>
|
||||
public bool BlockOnNewReachable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of new reachable paths allowed before blocking. Default: 0.
|
||||
/// </summary>
|
||||
public int MaxNewReachablePaths { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to require a minimum confidence level for blocking decisions. Default: true.
|
||||
/// </summary>
|
||||
public bool RequireMinimumConfidence { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum confidence level (0.0-1.0) for a path to count as blocking. Default: 0.7.
|
||||
/// </summary>
|
||||
public double MinimumConfidenceThreshold { get; set; } = 0.7;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to add annotations to the PR for state flips. Default: true.
|
||||
/// </summary>
|
||||
public bool AddAnnotations { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of annotations to add per PR. Default: 10.
|
||||
/// </summary>
|
||||
public int MaxAnnotations { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include mitigated paths in the summary. Default: true.
|
||||
/// </summary>
|
||||
public bool IncludeMitigatedInSummary { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of PR gate evaluation.
|
||||
/// </summary>
|
||||
public sealed record PrGateResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the PR passed the gate.
|
||||
/// </summary>
|
||||
public required bool Passed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable reason for the decision.
|
||||
/// </summary>
|
||||
public required string Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detailed decision breakdown.
|
||||
/// </summary>
|
||||
public required PrGateDecision Decision { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Annotations to add to the PR.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PrGateAnnotation> Annotations { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Summary markdown for PR comment.
|
||||
/// </summary>
|
||||
public string? SummaryMarkdown { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detailed breakdown of PR gate decision.
|
||||
/// </summary>
|
||||
public sealed record PrGateDecision
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of new reachable vulnerability paths introduced.
|
||||
/// </summary>
|
||||
public int NewReachableCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of vulnerability paths mitigated (became unreachable).
|
||||
/// </summary>
|
||||
public int MitigatedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Net change in reachable vulnerability paths.
|
||||
/// </summary>
|
||||
public int NetChange { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether incremental analysis was used.
|
||||
/// </summary>
|
||||
public bool WasIncremental { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Cache savings ratio (1.0 = 100% cached, 0.0 = full recompute).
|
||||
/// </summary>
|
||||
public double SavingsRatio { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Analysis duration.
|
||||
/// </summary>
|
||||
public TimeSpan Duration { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// State flips that caused blocking.
|
||||
/// </summary>
|
||||
public IReadOnlyList<StateFlip> BlockingFlips { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Annotation to add to a PR.
|
||||
/// </summary>
|
||||
public sealed record PrGateAnnotation
|
||||
{
|
||||
/// <summary>
|
||||
/// Annotation level (error, warning, notice).
|
||||
/// </summary>
|
||||
public required PrAnnotationLevel Level { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Annotation message.
|
||||
/// </summary>
|
||||
public required string Message { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// File path (if applicable).
|
||||
/// </summary>
|
||||
public string? FilePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Start line (if applicable).
|
||||
/// </summary>
|
||||
public int? StartLine { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// End line (if applicable).
|
||||
/// </summary>
|
||||
public int? EndLine { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Annotation title.
|
||||
/// </summary>
|
||||
public string? Title { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PR annotation severity level.
|
||||
/// </summary>
|
||||
public enum PrAnnotationLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Notice level (informational).
|
||||
/// </summary>
|
||||
Notice,
|
||||
|
||||
/// <summary>
|
||||
/// Warning level (non-blocking).
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// Error level (blocking).
|
||||
/// </summary>
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates incremental reachability results for PR gate decisions.
|
||||
/// </summary>
|
||||
public interface IPrReachabilityGate
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates an incremental reachability result for PR gating.
|
||||
/// </summary>
|
||||
/// <param name="result">The incremental reachability result.</param>
|
||||
/// <returns>The PR gate evaluation result.</returns>
|
||||
PrGateResult Evaluate(IncrementalReachabilityResult result);
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates state flips directly for PR gating.
|
||||
/// </summary>
|
||||
/// <param name="stateFlips">The state flip result.</param>
|
||||
/// <param name="wasIncremental">Whether incremental analysis was used.</param>
|
||||
/// <param name="savingsRatio">Cache savings ratio.</param>
|
||||
/// <param name="duration">Analysis duration.</param>
|
||||
/// <returns>The PR gate evaluation result.</returns>
|
||||
PrGateResult EvaluateFlips(
|
||||
StateFlipResult stateFlips,
|
||||
bool wasIncremental,
|
||||
double savingsRatio,
|
||||
TimeSpan duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IPrReachabilityGate"/>.
|
||||
/// </summary>
|
||||
public sealed class PrReachabilityGate : IPrReachabilityGate
|
||||
{
|
||||
private readonly IOptionsMonitor<PrReachabilityGateOptions> _options;
|
||||
private readonly ILogger<PrReachabilityGate> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new PR reachability gate.
|
||||
/// </summary>
|
||||
public PrReachabilityGate(
|
||||
IOptionsMonitor<PrReachabilityGateOptions> options,
|
||||
ILogger<PrReachabilityGate> logger)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public PrGateResult Evaluate(IncrementalReachabilityResult result)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
|
||||
if (result.StateFlips is null)
|
||||
{
|
||||
return CreatePassResult(
|
||||
"No state flip detection performed",
|
||||
wasIncremental: result.WasIncremental,
|
||||
savingsRatio: result.SavingsRatio,
|
||||
duration: result.Duration);
|
||||
}
|
||||
|
||||
return EvaluateFlips(
|
||||
result.StateFlips,
|
||||
result.WasIncremental,
|
||||
result.SavingsRatio,
|
||||
result.Duration);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public PrGateResult EvaluateFlips(
|
||||
StateFlipResult stateFlips,
|
||||
bool wasIncremental,
|
||||
double savingsRatio,
|
||||
TimeSpan duration)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stateFlips);
|
||||
|
||||
var options = _options.CurrentValue;
|
||||
|
||||
// If gate is disabled, always pass
|
||||
if (!options.Enabled)
|
||||
{
|
||||
_logger.LogDebug("PR gate is disabled, passing");
|
||||
return CreatePassResult(
|
||||
"PR gate is disabled",
|
||||
wasIncremental,
|
||||
savingsRatio,
|
||||
duration);
|
||||
}
|
||||
|
||||
// No flips = pass
|
||||
if (!stateFlips.HasFlips)
|
||||
{
|
||||
_logger.LogDebug("No reachability changes detected");
|
||||
return CreatePassResult(
|
||||
"No reachability changes",
|
||||
wasIncremental,
|
||||
savingsRatio,
|
||||
duration);
|
||||
}
|
||||
|
||||
// Filter blocking flips by confidence if required
|
||||
var blockingFlips = options.RequireMinimumConfidence
|
||||
? stateFlips.NewlyReachable
|
||||
.Where(f => f.Confidence >= options.MinimumConfidenceThreshold)
|
||||
.ToList()
|
||||
: stateFlips.NewlyReachable.ToList();
|
||||
|
||||
var blockingCount = blockingFlips.Count;
|
||||
|
||||
// Check if should block
|
||||
var shouldBlock = options.BlockOnNewReachable &&
|
||||
blockingCount > options.MaxNewReachablePaths;
|
||||
|
||||
var decision = new PrGateDecision
|
||||
{
|
||||
NewReachableCount = stateFlips.NewRiskCount,
|
||||
MitigatedCount = stateFlips.MitigatedCount,
|
||||
NetChange = stateFlips.NetChange,
|
||||
WasIncremental = wasIncremental,
|
||||
SavingsRatio = savingsRatio,
|
||||
Duration = duration,
|
||||
BlockingFlips = blockingFlips
|
||||
};
|
||||
|
||||
if (shouldBlock)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"PR gate BLOCKED: {Count} new reachable vulnerability paths introduced",
|
||||
blockingCount);
|
||||
|
||||
PrReachabilityGateMetrics.BlockedPrs.Add(1);
|
||||
|
||||
return new PrGateResult
|
||||
{
|
||||
Passed = false,
|
||||
Reason = $"{blockingCount} vulnerabilities became reachable",
|
||||
Decision = decision,
|
||||
Annotations = BuildAnnotations(blockingFlips, options),
|
||||
SummaryMarkdown = BuildSummaryMarkdown(decision, options, passed: false)
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"PR gate PASSED: {NewCount} new, {MitigatedCount} mitigated (net: {Net})",
|
||||
stateFlips.NewRiskCount, stateFlips.MitigatedCount, stateFlips.NetChange);
|
||||
|
||||
PrReachabilityGateMetrics.PassedPrs.Add(1);
|
||||
|
||||
var reason = stateFlips.MitigatedCount > 0
|
||||
? $"{stateFlips.MitigatedCount} vulnerabilities mitigated"
|
||||
: "Reachability changes within threshold";
|
||||
|
||||
return new PrGateResult
|
||||
{
|
||||
Passed = true,
|
||||
Reason = reason,
|
||||
Decision = decision,
|
||||
Annotations = BuildAnnotations(blockingFlips, options),
|
||||
SummaryMarkdown = BuildSummaryMarkdown(decision, options, passed: true)
|
||||
};
|
||||
}
|
||||
|
||||
private PrGateResult CreatePassResult(
|
||||
string reason,
|
||||
bool wasIncremental,
|
||||
double savingsRatio,
|
||||
TimeSpan duration)
|
||||
{
|
||||
return new PrGateResult
|
||||
{
|
||||
Passed = true,
|
||||
Reason = reason,
|
||||
Decision = new PrGateDecision
|
||||
{
|
||||
NewReachableCount = 0,
|
||||
MitigatedCount = 0,
|
||||
NetChange = 0,
|
||||
WasIncremental = wasIncremental,
|
||||
SavingsRatio = savingsRatio,
|
||||
Duration = duration,
|
||||
BlockingFlips = []
|
||||
},
|
||||
Annotations = [],
|
||||
SummaryMarkdown = null
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<PrGateAnnotation> BuildAnnotations(
|
||||
IReadOnlyList<StateFlip> blockingFlips,
|
||||
PrReachabilityGateOptions options)
|
||||
{
|
||||
if (!options.AddAnnotations || blockingFlips.Count == 0)
|
||||
return [];
|
||||
|
||||
var annotations = new List<PrGateAnnotation>();
|
||||
var flipsToAnnotate = blockingFlips.Take(options.MaxAnnotations);
|
||||
|
||||
foreach (var flip in flipsToAnnotate)
|
||||
{
|
||||
annotations.Add(new PrGateAnnotation
|
||||
{
|
||||
Level = PrAnnotationLevel.Error,
|
||||
Title = "New Reachable Vulnerability Path",
|
||||
Message = $"Vulnerability path became reachable: {flip.EntryMethodKey} → {flip.SinkMethodKey}",
|
||||
FilePath = flip.SourceFile,
|
||||
StartLine = flip.StartLine,
|
||||
EndLine = flip.EndLine
|
||||
});
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
private static string BuildSummaryMarkdown(
|
||||
PrGateDecision decision,
|
||||
PrReachabilityGateOptions options,
|
||||
bool passed)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine(passed
|
||||
? "## ✅ Reachability Gate Passed"
|
||||
: "## ❌ Reachability Gate Blocked");
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Metric | Value |");
|
||||
sb.AppendLine("|--------|-------|");
|
||||
sb.AppendLine($"| New reachable paths | {decision.NewReachableCount} |");
|
||||
|
||||
if (options.IncludeMitigatedInSummary)
|
||||
{
|
||||
sb.AppendLine($"| Mitigated paths | {decision.MitigatedCount} |");
|
||||
sb.AppendLine($"| Net change | {decision.NetChange:+#;-#;0} |");
|
||||
}
|
||||
|
||||
sb.AppendLine($"| Analysis type | {(decision.WasIncremental ? "Incremental" : "Full")} |");
|
||||
sb.AppendLine($"| Cache savings | {decision.SavingsRatio:P0} |");
|
||||
sb.AppendLine($"| Duration | {decision.Duration.TotalMilliseconds:F0}ms |");
|
||||
|
||||
if (!passed && decision.BlockingFlips.Count > 0)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("### Blocking Paths");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var flip in decision.BlockingFlips.Take(10))
|
||||
{
|
||||
sb.AppendLine($"- `{flip.EntryMethodKey}` → `{flip.SinkMethodKey}` (confidence: {flip.Confidence:P0})");
|
||||
}
|
||||
|
||||
if (decision.BlockingFlips.Count > 10)
|
||||
{
|
||||
sb.AppendLine($"- ... and {decision.BlockingFlips.Count - 10} more");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metrics for PR reachability gate.
|
||||
/// </summary>
|
||||
internal static class PrReachabilityGateMetrics
|
||||
{
|
||||
private static readonly string MeterName = "StellaOps.Scanner.Reachability.PrGate";
|
||||
|
||||
/// <summary>
|
||||
/// Counter for passed PRs.
|
||||
/// </summary>
|
||||
public static readonly System.Diagnostics.Metrics.Counter<long> PassedPrs =
|
||||
new System.Diagnostics.Metrics.Meter(MeterName).CreateCounter<long>(
|
||||
"stellaops.reachability_prgate.passed",
|
||||
description: "Number of PRs that passed the reachability gate");
|
||||
|
||||
/// <summary>
|
||||
/// Counter for blocked PRs.
|
||||
/// </summary>
|
||||
public static readonly System.Diagnostics.Metrics.Counter<long> BlockedPrs =
|
||||
new System.Diagnostics.Metrics.Meter(MeterName).CreateCounter<long>(
|
||||
"stellaops.reachability_prgate.blocked",
|
||||
description: "Number of PRs blocked by the reachability gate");
|
||||
}
|
||||
@@ -127,6 +127,26 @@ public sealed record StateFlip
|
||||
/// Package name if applicable.
|
||||
/// </summary>
|
||||
public string? PackageName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence score (0.0-1.0) of the reachability analysis.
|
||||
/// </summary>
|
||||
public double Confidence { get; init; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Source file where the entry point is defined (if available).
|
||||
/// </summary>
|
||||
public string? SourceFile { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Start line number in the source file (if available).
|
||||
/// </summary>
|
||||
public int? StartLine { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// End line number in the source file (if available).
|
||||
/// </summary>
|
||||
public int? EndLine { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user