Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -12,6 +12,28 @@ public sealed record PolicyGateContext
|
||||
public bool HasReachabilityProof { get; init; }
|
||||
public string? Severity { get; init; }
|
||||
public IReadOnlyCollection<string> ReasonCodes { get; init; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Subgraph slice for reachability proof.
|
||||
/// Required for high-severity findings when RequireSubgraphProofForHighSeverity is enabled.
|
||||
/// </summary>
|
||||
public SubgraphSlice? SubgraphSlice { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Subject key for Signals lookup.
|
||||
/// </summary>
|
||||
public string? SubjectKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVE ID if applicable.
|
||||
/// </summary>
|
||||
public string? CveId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Mutable metadata for audit trail.
|
||||
/// Gates can add metadata here for later inspection.
|
||||
/// </summary>
|
||||
public Dictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
public sealed record GateResult
|
||||
|
||||
@@ -8,6 +8,12 @@ public sealed record ReachabilityRequirementGateOptions
|
||||
{
|
||||
public bool Enabled { get; init; } = true;
|
||||
public string SeverityThreshold { get; init; } = "CRITICAL";
|
||||
|
||||
/// <summary>
|
||||
/// When true, requires subgraph proof for high-severity findings (CRITICAL, HIGH).
|
||||
/// </summary>
|
||||
public bool RequireSubgraphProofForHighSeverity { get; init; } = true;
|
||||
|
||||
public IReadOnlyCollection<VexStatus> RequiredForStatuses { get; init; } = new[]
|
||||
{
|
||||
VexStatus.NotAffected,
|
||||
@@ -52,6 +58,12 @@ public sealed class ReachabilityRequirementGate : IPolicyGate
|
||||
return Task.FromResult(Pass("bypass_reason"));
|
||||
}
|
||||
|
||||
// Check for subgraph proof for high-severity findings
|
||||
if (_options.RequireSubgraphProofForHighSeverity && severityRank >= 3) // HIGH or CRITICAL
|
||||
{
|
||||
return EvaluateSubgraphProof(context, severityRank);
|
||||
}
|
||||
|
||||
var passed = context.HasReachabilityProof;
|
||||
var details = ImmutableDictionary<string, object>.Empty
|
||||
.Add("severity", context.Severity ?? string.Empty)
|
||||
@@ -67,6 +79,71 @@ public sealed class ReachabilityRequirementGate : IPolicyGate
|
||||
});
|
||||
}
|
||||
|
||||
private Task<GateResult> EvaluateSubgraphProof(PolicyGateContext context, int severityRank)
|
||||
{
|
||||
// Check if subgraph slice is available
|
||||
if (context.SubgraphSlice is null)
|
||||
{
|
||||
var details = ImmutableDictionary<string, object>.Empty
|
||||
.Add("severity", context.Severity ?? string.Empty)
|
||||
.Add("severityRank", severityRank)
|
||||
.Add("reason", "high_severity_requires_subgraph_proof");
|
||||
|
||||
return Task.FromResult(new GateResult
|
||||
{
|
||||
GateName = nameof(ReachabilityRequirementGate),
|
||||
Passed = false,
|
||||
Reason = "subgraph_proof_required_for_high_severity",
|
||||
Details = details,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate that subgraph shows actual reachable path
|
||||
if (!HasReachablePath(context.SubgraphSlice))
|
||||
{
|
||||
// No reachable path in subgraph = can pass with "not reachable" justification
|
||||
var passDetails = ImmutableDictionary<string, object>.Empty
|
||||
.Add("severity", context.Severity ?? string.Empty)
|
||||
.Add("subgraphDigest", context.SubgraphSlice.Digest ?? string.Empty)
|
||||
.Add("reason", "subgraph_shows_no_reachable_path");
|
||||
|
||||
return Task.FromResult(new GateResult
|
||||
{
|
||||
GateName = nameof(ReachabilityRequirementGate),
|
||||
Passed = true,
|
||||
Reason = "not_reachable_per_subgraph",
|
||||
Details = passDetails,
|
||||
});
|
||||
}
|
||||
|
||||
// Subgraph shows reachable path, include digest in audit metadata
|
||||
var detailsWithDigest = ImmutableDictionary<string, object>.Empty
|
||||
.Add("severity", context.Severity ?? string.Empty)
|
||||
.Add("subgraphDigest", context.SubgraphSlice.Digest ?? string.Empty)
|
||||
.Add("reachablePathCount", context.SubgraphSlice.Paths?.Count ?? 0)
|
||||
.Add("hasReachabilityProof", true);
|
||||
|
||||
// Store digest in metadata for audit trail
|
||||
if (context.Metadata is not null && context.SubgraphSlice.Digest is not null)
|
||||
{
|
||||
context.Metadata["reachgraph_digest"] = context.SubgraphSlice.Digest;
|
||||
}
|
||||
|
||||
return Task.FromResult(new GateResult
|
||||
{
|
||||
GateName = nameof(ReachabilityRequirementGate),
|
||||
Passed = true,
|
||||
Reason = "reachable_with_subgraph_proof",
|
||||
Details = detailsWithDigest,
|
||||
});
|
||||
}
|
||||
|
||||
private static bool HasReachablePath(SubgraphSlice slice)
|
||||
{
|
||||
return slice.Paths?.Count > 0 &&
|
||||
slice.Paths.Any(p => p.Hops is { Count: > 0 });
|
||||
}
|
||||
|
||||
private bool HasBypass(IReadOnlyCollection<string> reasons)
|
||||
=> reasons.Any(reason => _options.BypassReasons.Contains(reason, StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
@@ -95,3 +172,22 @@ public sealed class ReachabilityRequirementGate : IPolicyGate
|
||||
Details = ImmutableDictionary<string, object>.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subgraph slice data for policy evaluation.
|
||||
/// </summary>
|
||||
public sealed record SubgraphSlice
|
||||
{
|
||||
public string? Digest { get; init; }
|
||||
public List<SubgraphPath>? Paths { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path in a subgraph slice.
|
||||
/// </summary>
|
||||
public sealed record SubgraphPath
|
||||
{
|
||||
public string? Entrypoint { get; init; }
|
||||
public string? Sink { get; init; }
|
||||
public List<string>? Hops { get; init; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user