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:
StellaOps Bot
2025-12-22 23:21:21 +02:00
parent 3ba7157b00
commit 5146204f1b
529 changed files with 73579 additions and 5985 deletions

View 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
}

View 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
};
}
}

View 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
}