feat: add security sink detection patterns for JavaScript/TypeScript
- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations). - Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns. - Added `package-lock.json` for dependency management.
This commit is contained in:
244
src/Policy/StellaOps.Policy.Engine/Gates/DriftGateContext.cs
Normal file
244
src/Policy/StellaOps.Policy.Engine/Gates/DriftGateContext.cs
Normal file
@@ -0,0 +1,244 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DriftGateContext.cs
|
||||
// Sprint: SPRINT_3600_0005_0001_policy_ci_gate_integration
|
||||
// Description: Context for drift gate evaluation containing delta metrics.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Context for evaluating drift gates in policy evaluation.
|
||||
/// Contains delta metrics from reachability drift analysis.
|
||||
/// </summary>
|
||||
public sealed record DriftGateContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of newly reachable paths (positive delta).
|
||||
/// </summary>
|
||||
public required int DeltaReachable { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of newly unreachable paths (negative delta, mitigation).
|
||||
/// </summary>
|
||||
public required int DeltaUnreachable { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether any KEV (Known Exploited Vulnerability) is now reachable.
|
||||
/// </summary>
|
||||
public required bool HasKevReachable { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// VEX statuses of newly reachable vulnerabilities.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> NewlyReachableVexStatuses { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Maximum CVSS score among newly reachable vulnerabilities.
|
||||
/// </summary>
|
||||
public double? MaxCvss { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum EPSS score among newly reachable vulnerabilities.
|
||||
/// </summary>
|
||||
public double? MaxEpss { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Scan ID of the base (before) snapshot.
|
||||
/// </summary>
|
||||
public string? BaseScanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Scan ID of the head (after) snapshot.
|
||||
/// </summary>
|
||||
public string? HeadScanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Newly reachable sink IDs (for VEX candidate emission).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> NewlyReachableSinkIds { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Newly unreachable sink IDs (for VEX auto-mitigation).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> NewlyUnreachableSinkIds { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if there is any material drift.
|
||||
/// </summary>
|
||||
public bool HasMaterialDrift => DeltaReachable > 0 || DeltaUnreachable > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if drift represents a security regression.
|
||||
/// </summary>
|
||||
public bool IsRegression => DeltaReachable > 0 &&
|
||||
(HasKevReachable || NewlyReachableVexStatuses.Any(s =>
|
||||
s.Equals("affected", StringComparison.OrdinalIgnoreCase) ||
|
||||
s.Equals("under_investigation", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if drift represents hardening (mitigation).
|
||||
/// </summary>
|
||||
public bool IsHardening => DeltaUnreachable > 0 && DeltaReachable == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request for drift gate evaluation.
|
||||
/// </summary>
|
||||
public sealed record DriftGateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The drift context containing delta metrics.
|
||||
/// </summary>
|
||||
public required DriftGateContext Context { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy configuration ID to use for evaluation.
|
||||
/// </summary>
|
||||
public string? PolicyId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to allow override of blocking gates.
|
||||
/// </summary>
|
||||
public bool AllowOverride { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Justification for override (if AllowOverride is true).
|
||||
/// </summary>
|
||||
public string? OverrideJustification { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of drift gate evaluation.
|
||||
/// </summary>
|
||||
public sealed record DriftGateDecision
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique decision ID.
|
||||
/// </summary>
|
||||
public required string DecisionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Overall decision.
|
||||
/// </summary>
|
||||
public required DriftGateDecisionType Decision { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// List of gate results.
|
||||
/// </summary>
|
||||
public ImmutableArray<DriftGateResult> Gates { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Advisory message.
|
||||
/// </summary>
|
||||
public string? Advisory { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gate that blocked (if blocked).
|
||||
/// </summary>
|
||||
public string? BlockedBy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for blocking (if blocked).
|
||||
/// </summary>
|
||||
public string? BlockReason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Suggestion for resolving the block.
|
||||
/// </summary>
|
||||
public string? Suggestion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the decision was made.
|
||||
/// </summary>
|
||||
public required DateTimeOffset DecidedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Context that was evaluated.
|
||||
/// </summary>
|
||||
public required DriftGateContext Context { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a single drift gate.
|
||||
/// </summary>
|
||||
public sealed record DriftGateResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gate name/ID.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gate result type.
|
||||
/// </summary>
|
||||
public required DriftGateResultType Result { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for the result.
|
||||
/// </summary>
|
||||
public required string Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional note (for warnings/passes with notes).
|
||||
/// </summary>
|
||||
public string? Note { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Condition expression that was evaluated.
|
||||
/// </summary>
|
||||
public string? Condition { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Types of drift gate results.
|
||||
/// </summary>
|
||||
public enum DriftGateResultType
|
||||
{
|
||||
/// <summary>
|
||||
/// Gate passed.
|
||||
/// </summary>
|
||||
Pass,
|
||||
|
||||
/// <summary>
|
||||
/// Gate passed with a note.
|
||||
/// </summary>
|
||||
PassWithNote,
|
||||
|
||||
/// <summary>
|
||||
/// Gate produced a warning.
|
||||
/// </summary>
|
||||
Warn,
|
||||
|
||||
/// <summary>
|
||||
/// Gate blocked the drift.
|
||||
/// </summary>
|
||||
Block,
|
||||
|
||||
/// <summary>
|
||||
/// Gate was skipped.
|
||||
/// </summary>
|
||||
Skip
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Types of drift gate decisions.
|
||||
/// </summary>
|
||||
public enum DriftGateDecisionType
|
||||
{
|
||||
/// <summary>
|
||||
/// Drift is allowed to proceed.
|
||||
/// </summary>
|
||||
Allow,
|
||||
|
||||
/// <summary>
|
||||
/// Drift is allowed with warnings.
|
||||
/// </summary>
|
||||
Warn,
|
||||
|
||||
/// <summary>
|
||||
/// Drift is blocked by policy.
|
||||
/// </summary>
|
||||
Block
|
||||
}
|
||||
463
src/Policy/StellaOps.Policy.Engine/Gates/DriftGateEvaluator.cs
Normal file
463
src/Policy/StellaOps.Policy.Engine/Gates/DriftGateEvaluator.cs
Normal file
@@ -0,0 +1,463 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DriftGateEvaluator.cs
|
||||
// Sprint: SPRINT_3600_0005_0001_policy_ci_gate_integration
|
||||
// Description: Evaluates drift gates for CI/CD pipeline gating.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates drift gates for reachability drift analysis.
|
||||
/// </summary>
|
||||
public interface IDriftGateEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates all drift gates for a drift analysis result.
|
||||
/// </summary>
|
||||
/// <param name="request">The drift gate evaluation request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The drift gate decision.</returns>
|
||||
Task<DriftGateDecision> EvaluateAsync(DriftGateRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="IDriftGateEvaluator"/>.
|
||||
/// </summary>
|
||||
public sealed class DriftGateEvaluator : IDriftGateEvaluator
|
||||
{
|
||||
private readonly IOptionsMonitor<DriftGateOptions> _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<DriftGateEvaluator> _logger;
|
||||
|
||||
public DriftGateEvaluator(
|
||||
IOptionsMonitor<DriftGateOptions> options,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<DriftGateEvaluator> logger)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<DriftGateDecision> EvaluateAsync(DriftGateRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var options = _options.CurrentValue;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var context = request.Context;
|
||||
|
||||
var decisionId = $"drift-gate:{now:yyyyMMddHHmmss}:{Guid.NewGuid():N}";
|
||||
var gateResults = new List<DriftGateResult>();
|
||||
|
||||
string? blockedBy = null;
|
||||
string? blockReason = null;
|
||||
string? suggestion = null;
|
||||
var warnings = new List<string>();
|
||||
|
||||
// If gates are disabled, allow everything
|
||||
if (!options.Enabled)
|
||||
{
|
||||
return Task.FromResult(CreateAllowDecision(decisionId, context, now, "Drift gates disabled"));
|
||||
}
|
||||
|
||||
// If no material drift, allow
|
||||
if (!context.HasMaterialDrift)
|
||||
{
|
||||
return Task.FromResult(CreateAllowDecision(decisionId, context, now, "No material drift detected"));
|
||||
}
|
||||
|
||||
// 1. Evaluate built-in KEV gate
|
||||
if (options.BlockOnKev)
|
||||
{
|
||||
var kevResult = EvaluateKevGate(context);
|
||||
gateResults.Add(kevResult);
|
||||
if (kevResult.Result == DriftGateResultType.Block)
|
||||
{
|
||||
blockedBy = kevResult.Name;
|
||||
blockReason = kevResult.Reason;
|
||||
suggestion = "Review KEV exposure and mitigate before proceeding";
|
||||
}
|
||||
else if (kevResult.Result == DriftGateResultType.Warn)
|
||||
{
|
||||
warnings.Add(kevResult.Reason);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Evaluate built-in affected reachable gate
|
||||
if (blockedBy is null && options.BlockOnAffectedReachable)
|
||||
{
|
||||
var affectedResult = EvaluateAffectedReachableGate(context);
|
||||
gateResults.Add(affectedResult);
|
||||
if (affectedResult.Result == DriftGateResultType.Block)
|
||||
{
|
||||
blockedBy = affectedResult.Name;
|
||||
blockReason = affectedResult.Reason;
|
||||
suggestion = "Triage new reachable affected vulnerabilities";
|
||||
}
|
||||
else if (affectedResult.Result == DriftGateResultType.Warn)
|
||||
{
|
||||
warnings.Add(affectedResult.Reason);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Evaluate CVSS threshold gate
|
||||
if (blockedBy is null && options.CvssBlockThreshold.HasValue)
|
||||
{
|
||||
var cvssResult = EvaluateCvssGate(context, options.CvssBlockThreshold.Value);
|
||||
gateResults.Add(cvssResult);
|
||||
if (cvssResult.Result == DriftGateResultType.Block)
|
||||
{
|
||||
blockedBy = cvssResult.Name;
|
||||
blockReason = cvssResult.Reason;
|
||||
suggestion = $"Address vulnerabilities with CVSS >= {options.CvssBlockThreshold:F1}";
|
||||
}
|
||||
else if (cvssResult.Result == DriftGateResultType.Warn)
|
||||
{
|
||||
warnings.Add(cvssResult.Reason);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Evaluate EPSS threshold gate
|
||||
if (blockedBy is null && options.EpssBlockThreshold.HasValue)
|
||||
{
|
||||
var epssResult = EvaluateEpssGate(context, options.EpssBlockThreshold.Value);
|
||||
gateResults.Add(epssResult);
|
||||
if (epssResult.Result == DriftGateResultType.Block)
|
||||
{
|
||||
blockedBy = epssResult.Name;
|
||||
blockReason = epssResult.Reason;
|
||||
suggestion = $"Review high-probability exploit vulnerabilities (EPSS >= {options.EpssBlockThreshold:P0})";
|
||||
}
|
||||
else if (epssResult.Result == DriftGateResultType.Warn)
|
||||
{
|
||||
warnings.Add(epssResult.Reason);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Evaluate custom gates from configuration
|
||||
foreach (var gate in options.Gates)
|
||||
{
|
||||
if (blockedBy is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var customResult = EvaluateCustomGate(context, gate);
|
||||
gateResults.Add(customResult);
|
||||
|
||||
if (customResult.Result == DriftGateResultType.Block)
|
||||
{
|
||||
blockedBy = customResult.Name;
|
||||
blockReason = customResult.Reason;
|
||||
suggestion = gate.Message;
|
||||
}
|
||||
else if (customResult.Result == DriftGateResultType.Warn)
|
||||
{
|
||||
warnings.Add(customResult.Reason);
|
||||
}
|
||||
}
|
||||
|
||||
// Build final decision
|
||||
DriftGateDecisionType decision;
|
||||
string? advisory = null;
|
||||
|
||||
if (blockedBy is not null)
|
||||
{
|
||||
if (request.AllowOverride && CanOverride(request))
|
||||
{
|
||||
decision = DriftGateDecisionType.Warn;
|
||||
advisory = $"Override accepted: {request.OverrideJustification}";
|
||||
_logger.LogInformation(
|
||||
"Drift gate {Gate} overridden: {Justification}",
|
||||
blockedBy, request.OverrideJustification);
|
||||
}
|
||||
else
|
||||
{
|
||||
decision = DriftGateDecisionType.Block;
|
||||
_logger.LogInformation(
|
||||
"Drift gate {Gate} blocked drift: {Reason}",
|
||||
blockedBy, blockReason);
|
||||
}
|
||||
}
|
||||
else if (warnings.Count > 0)
|
||||
{
|
||||
decision = DriftGateDecisionType.Warn;
|
||||
advisory = string.Join("; ", warnings);
|
||||
}
|
||||
else
|
||||
{
|
||||
decision = DriftGateDecisionType.Allow;
|
||||
}
|
||||
|
||||
return Task.FromResult(new DriftGateDecision
|
||||
{
|
||||
DecisionId = decisionId,
|
||||
Decision = decision,
|
||||
Gates = gateResults.ToImmutableArray(),
|
||||
Advisory = advisory,
|
||||
BlockedBy = blockedBy,
|
||||
BlockReason = blockReason,
|
||||
Suggestion = suggestion,
|
||||
DecidedAt = now,
|
||||
Context = context
|
||||
});
|
||||
}
|
||||
|
||||
private static DriftGateResult EvaluateKevGate(DriftGateContext context)
|
||||
{
|
||||
if (context.HasKevReachable && context.DeltaReachable > 0)
|
||||
{
|
||||
return new DriftGateResult
|
||||
{
|
||||
Name = "KevReachable",
|
||||
Result = DriftGateResultType.Block,
|
||||
Reason = "Known Exploited Vulnerability (KEV) now reachable",
|
||||
Condition = "is_kev = true AND delta_reachable > 0"
|
||||
};
|
||||
}
|
||||
|
||||
return new DriftGateResult
|
||||
{
|
||||
Name = "KevReachable",
|
||||
Result = DriftGateResultType.Pass,
|
||||
Reason = "No KEV in newly reachable paths",
|
||||
Condition = "is_kev = true AND delta_reachable > 0"
|
||||
};
|
||||
}
|
||||
|
||||
private static DriftGateResult EvaluateAffectedReachableGate(DriftGateContext context)
|
||||
{
|
||||
var hasAffected = context.NewlyReachableVexStatuses.Any(s =>
|
||||
s.Equals("affected", StringComparison.OrdinalIgnoreCase) ||
|
||||
s.Equals("under_investigation", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (hasAffected && context.DeltaReachable > 0)
|
||||
{
|
||||
return new DriftGateResult
|
||||
{
|
||||
Name = "AffectedReachable",
|
||||
Result = DriftGateResultType.Block,
|
||||
Reason = $"New paths to affected vulnerabilities detected ({context.DeltaReachable} newly reachable)",
|
||||
Condition = "delta_reachable > 0 AND vex_status IN ['affected', 'under_investigation']"
|
||||
};
|
||||
}
|
||||
|
||||
if (context.DeltaReachable > 0)
|
||||
{
|
||||
return new DriftGateResult
|
||||
{
|
||||
Name = "AffectedReachable",
|
||||
Result = DriftGateResultType.Warn,
|
||||
Reason = $"New reachable paths detected ({context.DeltaReachable}) - review recommended",
|
||||
Condition = "delta_reachable > 0"
|
||||
};
|
||||
}
|
||||
|
||||
return new DriftGateResult
|
||||
{
|
||||
Name = "AffectedReachable",
|
||||
Result = DriftGateResultType.Pass,
|
||||
Reason = "No new paths to affected vulnerabilities",
|
||||
Condition = "delta_reachable > 0 AND vex_status IN ['affected', 'under_investigation']"
|
||||
};
|
||||
}
|
||||
|
||||
private static DriftGateResult EvaluateCvssGate(DriftGateContext context, double threshold)
|
||||
{
|
||||
if (context.MaxCvss.HasValue && context.MaxCvss.Value >= threshold && context.DeltaReachable > 0)
|
||||
{
|
||||
return new DriftGateResult
|
||||
{
|
||||
Name = "CvssThreshold",
|
||||
Result = DriftGateResultType.Block,
|
||||
Reason = $"High-severity vulnerability (CVSS {context.MaxCvss.Value:F1}) now reachable",
|
||||
Condition = $"max_cvss >= {threshold:F1} AND delta_reachable > 0"
|
||||
};
|
||||
}
|
||||
|
||||
return new DriftGateResult
|
||||
{
|
||||
Name = "CvssThreshold",
|
||||
Result = DriftGateResultType.Pass,
|
||||
Reason = $"No newly reachable vulnerabilities exceed CVSS {threshold:F1}",
|
||||
Condition = $"max_cvss >= {threshold:F1} AND delta_reachable > 0"
|
||||
};
|
||||
}
|
||||
|
||||
private static DriftGateResult EvaluateEpssGate(DriftGateContext context, double threshold)
|
||||
{
|
||||
if (context.MaxEpss.HasValue && context.MaxEpss.Value >= threshold && context.DeltaReachable > 0)
|
||||
{
|
||||
return new DriftGateResult
|
||||
{
|
||||
Name = "EpssThreshold",
|
||||
Result = DriftGateResultType.Block,
|
||||
Reason = $"High-probability exploit (EPSS {context.MaxEpss.Value:P0}) now reachable",
|
||||
Condition = $"max_epss >= {threshold:P0} AND delta_reachable > 0"
|
||||
};
|
||||
}
|
||||
|
||||
return new DriftGateResult
|
||||
{
|
||||
Name = "EpssThreshold",
|
||||
Result = DriftGateResultType.Pass,
|
||||
Reason = $"No newly reachable vulnerabilities exceed EPSS {threshold:P0}",
|
||||
Condition = $"max_epss >= {threshold:P0} AND delta_reachable > 0"
|
||||
};
|
||||
}
|
||||
|
||||
private static DriftGateResult EvaluateCustomGate(DriftGateContext context, DriftGateDefinition gate)
|
||||
{
|
||||
// Simple condition parser for common patterns
|
||||
var matches = EvaluateCondition(context, gate.Condition);
|
||||
|
||||
if (matches)
|
||||
{
|
||||
var resultType = gate.Action switch
|
||||
{
|
||||
DriftGateAction.Block => DriftGateResultType.Block,
|
||||
DriftGateAction.Warn => DriftGateResultType.Warn,
|
||||
DriftGateAction.Allow => DriftGateResultType.Pass,
|
||||
_ => DriftGateResultType.Pass
|
||||
};
|
||||
|
||||
return new DriftGateResult
|
||||
{
|
||||
Name = gate.Id,
|
||||
Result = resultType,
|
||||
Reason = string.IsNullOrEmpty(gate.Message) ? $"Custom gate '{gate.Id}' triggered" : gate.Message,
|
||||
Condition = gate.Condition
|
||||
};
|
||||
}
|
||||
|
||||
return new DriftGateResult
|
||||
{
|
||||
Name = gate.Id,
|
||||
Result = DriftGateResultType.Pass,
|
||||
Reason = $"Custom gate '{gate.Id}' condition not met",
|
||||
Condition = gate.Condition
|
||||
};
|
||||
}
|
||||
|
||||
private static bool EvaluateCondition(DriftGateContext context, string condition)
|
||||
{
|
||||
// Simple condition evaluator for common patterns
|
||||
// Supports: delta_reachable, delta_unreachable, is_kev, max_cvss, max_epss
|
||||
// Operators: >, <, >=, <=, =, AND, OR
|
||||
|
||||
var normalized = condition.ToUpperInvariant().Trim();
|
||||
|
||||
// Handle AND conditions
|
||||
if (normalized.Contains(" AND "))
|
||||
{
|
||||
var parts = normalized.Split(new[] { " AND " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
return parts.All(p => EvaluateCondition(context, p));
|
||||
}
|
||||
|
||||
// Handle OR conditions
|
||||
if (normalized.Contains(" OR "))
|
||||
{
|
||||
var parts = normalized.Split(new[] { " OR " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
return parts.Any(p => EvaluateCondition(context, p));
|
||||
}
|
||||
|
||||
// Handle simple comparisons
|
||||
return normalized switch
|
||||
{
|
||||
var c when c.StartsWith("DELTA_REACHABLE") => EvaluateNumericCondition(context.DeltaReachable, c["DELTA_REACHABLE".Length..]),
|
||||
var c when c.StartsWith("DELTA_UNREACHABLE") => EvaluateNumericCondition(context.DeltaUnreachable, c["DELTA_UNREACHABLE".Length..]),
|
||||
var c when c.StartsWith("IS_KEV") => c.Contains("TRUE") ? context.HasKevReachable : !context.HasKevReachable,
|
||||
var c when c.StartsWith("MAX_CVSS") && context.MaxCvss.HasValue => EvaluateNumericCondition(context.MaxCvss.Value, c["MAX_CVSS".Length..]),
|
||||
var c when c.StartsWith("MAX_EPSS") && context.MaxEpss.HasValue => EvaluateNumericCondition(context.MaxEpss.Value, c["MAX_EPSS".Length..]),
|
||||
var c when c.Contains("VEX_STATUS") => EvaluateVexStatusCondition(context.NewlyReachableVexStatuses, c),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static bool EvaluateNumericCondition(double value, string remainder)
|
||||
{
|
||||
remainder = remainder.Trim();
|
||||
|
||||
if (remainder.StartsWith(">="))
|
||||
{
|
||||
return double.TryParse(remainder[2..].Trim(), out var threshold) && value >= threshold;
|
||||
}
|
||||
if (remainder.StartsWith("<="))
|
||||
{
|
||||
return double.TryParse(remainder[2..].Trim(), out var threshold) && value <= threshold;
|
||||
}
|
||||
if (remainder.StartsWith(">"))
|
||||
{
|
||||
return double.TryParse(remainder[1..].Trim(), out var threshold) && value > threshold;
|
||||
}
|
||||
if (remainder.StartsWith("<"))
|
||||
{
|
||||
return double.TryParse(remainder[1..].Trim(), out var threshold) && value < threshold;
|
||||
}
|
||||
if (remainder.StartsWith("="))
|
||||
{
|
||||
return double.TryParse(remainder[1..].Trim(), out var threshold) && Math.Abs(value - threshold) < 0.001;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool EvaluateVexStatusCondition(IReadOnlyList<string> statuses, string condition)
|
||||
{
|
||||
// Handle VEX_STATUS IN ['affected', 'under_investigation']
|
||||
var inMatch = condition.IndexOf("IN", StringComparison.OrdinalIgnoreCase);
|
||||
if (inMatch < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var listPart = condition[(inMatch + 2)..].Trim();
|
||||
if (!listPart.StartsWith("[") || !listPart.Contains(']'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var values = listPart
|
||||
.Trim('[', ']', ' ')
|
||||
.Split(',')
|
||||
.Select(v => v.Trim().Trim('\'', '"').ToUpperInvariant())
|
||||
.ToHashSet();
|
||||
|
||||
return statuses.Any(s => values.Contains(s.ToUpperInvariant()));
|
||||
}
|
||||
|
||||
private static bool CanOverride(DriftGateRequest request)
|
||||
{
|
||||
return request.AllowOverride &&
|
||||
!string.IsNullOrWhiteSpace(request.OverrideJustification) &&
|
||||
request.OverrideJustification.Length >= 10;
|
||||
}
|
||||
|
||||
private static DriftGateDecision CreateAllowDecision(
|
||||
string decisionId,
|
||||
DriftGateContext context,
|
||||
DateTimeOffset decidedAt,
|
||||
string reason)
|
||||
{
|
||||
return new DriftGateDecision
|
||||
{
|
||||
DecisionId = decisionId,
|
||||
Decision = DriftGateDecisionType.Allow,
|
||||
Gates = ImmutableArray.Create(new DriftGateResult
|
||||
{
|
||||
Name = "Bypass",
|
||||
Result = DriftGateResultType.Pass,
|
||||
Reason = reason
|
||||
}),
|
||||
Advisory = reason,
|
||||
DecidedAt = decidedAt,
|
||||
Context = context
|
||||
};
|
||||
}
|
||||
}
|
||||
151
src/Policy/StellaOps.Policy.Engine/Gates/DriftGateOptions.cs
Normal file
151
src/Policy/StellaOps.Policy.Engine/Gates/DriftGateOptions.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DriftGateOptions.cs
|
||||
// Sprint: SPRINT_3600_0005_0001_policy_ci_gate_integration
|
||||
// Description: Configuration options for drift gate evaluation.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for drift gate evaluation.
|
||||
/// </summary>
|
||||
public sealed class DriftGateOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration section name.
|
||||
/// </summary>
|
||||
public const string SectionName = "SmartDiff:Gates";
|
||||
|
||||
/// <summary>
|
||||
/// Whether drift gates are enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Custom gate definitions.
|
||||
/// </summary>
|
||||
public List<DriftGateDefinition> Gates { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Default action when no gate matches.
|
||||
/// </summary>
|
||||
public DriftGateAction DefaultAction { get; set; } = DriftGateAction.Warn;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to block on KEV reachable by default.
|
||||
/// </summary>
|
||||
public bool BlockOnKev { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to block when affected vulnerabilities become reachable.
|
||||
/// </summary>
|
||||
public bool BlockOnAffectedReachable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to auto-emit VEX candidates for unreachable sinks.
|
||||
/// </summary>
|
||||
public bool AutoEmitVexForUnreachable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum CVSS score to trigger block action.
|
||||
/// </summary>
|
||||
public double? CvssBlockThreshold { get; set; } = 9.0;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum EPSS score to trigger block action.
|
||||
/// </summary>
|
||||
public double? EpssBlockThreshold { get; set; } = 0.5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom gate definition from policy configuration.
|
||||
/// </summary>
|
||||
public sealed class DriftGateDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Gate identifier.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Condition expression (e.g., "delta_reachable > 0 AND is_kev = true").
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Condition { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Action to take when condition matches.
|
||||
/// </summary>
|
||||
public DriftGateAction Action { get; set; } = DriftGateAction.Warn;
|
||||
|
||||
/// <summary>
|
||||
/// Message to display when gate triggers.
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Severity level.
|
||||
/// </summary>
|
||||
public DriftGateSeverity Severity { get; set; } = DriftGateSeverity.Medium;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to auto-mitigate (emit VEX) when condition matches.
|
||||
/// </summary>
|
||||
public bool AutoMitigate { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Actions that can be taken by drift gates.
|
||||
/// </summary>
|
||||
public enum DriftGateAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Allow the drift to proceed.
|
||||
/// </summary>
|
||||
Allow,
|
||||
|
||||
/// <summary>
|
||||
/// Allow with a warning.
|
||||
/// </summary>
|
||||
Warn,
|
||||
|
||||
/// <summary>
|
||||
/// Block the drift.
|
||||
/// </summary>
|
||||
Block
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Severity levels for drift gates.
|
||||
/// </summary>
|
||||
public enum DriftGateSeverity
|
||||
{
|
||||
/// <summary>
|
||||
/// Informational.
|
||||
/// </summary>
|
||||
Info,
|
||||
|
||||
/// <summary>
|
||||
/// Low severity.
|
||||
/// </summary>
|
||||
Low,
|
||||
|
||||
/// <summary>
|
||||
/// Medium severity.
|
||||
/// </summary>
|
||||
Medium,
|
||||
|
||||
/// <summary>
|
||||
/// High severity.
|
||||
/// </summary>
|
||||
High,
|
||||
|
||||
/// <summary>
|
||||
/// Critical severity.
|
||||
/// </summary>
|
||||
Critical
|
||||
}
|
||||
Reference in New Issue
Block a user