Add comprehensive security tests for OWASP A03 (Injection) and A10 (SSRF)
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled

- Implemented InjectionTests.cs to cover various injection vulnerabilities including SQL, NoSQL, Command, LDAP, and XPath injections.
- Created SsrfTests.cs to test for Server-Side Request Forgery (SSRF) vulnerabilities, including internal URL access, cloud metadata access, and URL allowlist bypass attempts.
- Introduced MaliciousPayloads.cs to store a collection of malicious payloads for testing various security vulnerabilities.
- Added SecurityAssertions.cs for common security-specific assertion helpers.
- Established SecurityTestBase.cs as a base class for security tests, providing common infrastructure and mocking utilities.
- Configured the test project StellaOps.Security.Tests.csproj with necessary dependencies for testing.
This commit is contained in:
master
2025-12-16 13:11:57 +02:00
parent 5a480a3c2a
commit b55d9fa68d
72 changed files with 8051 additions and 71 deletions

View File

@@ -0,0 +1,47 @@
{
"$schema": "https://raw.githubusercontent.com/stryker-mutator/stryker-net/master/src/Stryker.Core/Stryker.Core/config-schema.json",
"stryker-config": {
"project-info": {
"name": "StellaOps.Scanner",
"module": "Scanner.Core",
"version": "0.0.1"
},
"solution": "../../../StellaOps.Router.slnx",
"project": "StellaOps.Scanner.Core.csproj",
"test-projects": [
"../__Tests/StellaOps.Scanner.Core.Tests/StellaOps.Scanner.Core.Tests.csproj"
],
"reporters": [
"html",
"json",
"progress"
],
"thresholds": {
"high": 85,
"low": 70,
"break": 60
},
"mutation-level": "Standard",
"mutators": {
"included": [
"Arithmetic",
"Boolean",
"Comparison",
"Conditional",
"Equality",
"Logical",
"NullCoalescing",
"String"
]
},
"coverage-analysis": "perTest",
"excluded-files": [
"**/Generated/**/*",
"**/Models/**/*Dto.cs"
],
"excluded-mutations": {
"ignoreBlockRemovalMutations": true
},
"output-path": "../../../.stryker/output/scanner-core"
}
}

View File

@@ -0,0 +1,134 @@
using System.Text.RegularExpressions;
namespace StellaOps.Scanner.Reachability.Gates.Detectors;
/// <summary>
/// Detects admin/role-based gates in code.
/// </summary>
public sealed class AdminOnlyDetector : IGateDetector
{
/// <inheritdoc />
public GateType GateType => GateType.AdminOnly;
/// <inheritdoc />
public async Task<IReadOnlyList<DetectedGate>> DetectAsync(
RichGraphNode node,
IReadOnlyList<RichGraphEdge> incomingEdges,
ICodeContentProvider codeProvider,
string language,
CancellationToken ct = default)
{
var gates = new List<DetectedGate>();
var normalizedLanguage = NormalizeLanguage(language);
if (!GatePatterns.AdminPatterns.TryGetValue(normalizedLanguage, out var patterns))
return gates;
// Check node annotations (attributes, decorators)
if (node.Annotations is { Count: > 0 })
{
foreach (var pattern in patterns)
{
var regex = CreateRegex(pattern.Pattern);
foreach (var annotation in node.Annotations)
{
if (regex.IsMatch(annotation))
{
gates.Add(CreateGate(
node,
pattern,
$"Admin/role required: {pattern.Description}",
$"annotation:{pattern.Pattern}"));
}
}
}
}
// Check source code content
if (node.SourceFile is not null && node.LineNumber is > 0)
{
var startLine = Math.Max(1, node.LineNumber.Value - 5);
var endLine = node.EndLineNumber ?? (node.LineNumber.Value + 15);
var lines = await codeProvider.GetLinesAsync(node.SourceFile, startLine, endLine, ct);
if (lines is { Count: > 0 })
{
var content = string.Join("\n", lines);
foreach (var pattern in patterns)
{
var regex = CreateRegex(pattern.Pattern);
if (regex.IsMatch(content))
{
// Avoid duplicate detection
if (!gates.Any(g => g.DetectionMethod.Contains(pattern.Pattern)))
{
gates.Add(CreateGate(
node,
pattern,
$"Admin/role required: {pattern.Description}",
$"source:{pattern.Pattern}"));
}
}
}
}
}
// Check for role-related metadata
if (node.Metadata is not null)
{
foreach (var (key, value) in node.Metadata)
{
if (key.Contains("role", StringComparison.OrdinalIgnoreCase) ||
key.Contains("admin", StringComparison.OrdinalIgnoreCase))
{
if (value.Contains("admin", StringComparison.OrdinalIgnoreCase) ||
value.Contains("superuser", StringComparison.OrdinalIgnoreCase) ||
value.Contains("elevated", StringComparison.OrdinalIgnoreCase))
{
gates.Add(new DetectedGate
{
Type = GateType.AdminOnly,
Detail = $"Admin/role required: metadata {key}={value}",
GuardSymbol = node.Symbol,
SourceFile = node.SourceFile,
LineNumber = node.LineNumber,
Confidence = 0.70,
DetectionMethod = $"metadata:{key}"
});
}
}
}
}
return gates;
}
private static DetectedGate CreateGate(
RichGraphNode node,
GatePattern pattern,
string detail,
string detectionMethod) => new()
{
Type = GateType.AdminOnly,
Detail = detail,
GuardSymbol = node.Symbol,
SourceFile = node.SourceFile,
LineNumber = node.LineNumber,
Confidence = pattern.DefaultConfidence,
DetectionMethod = detectionMethod
};
private static string NormalizeLanguage(string language) =>
language.ToLowerInvariant() switch
{
"c#" or "cs" => "csharp",
"js" => "javascript",
"ts" => "typescript",
"py" => "python",
"rb" => "ruby",
_ => language.ToLowerInvariant()
};
private static Regex CreateRegex(string pattern) =>
new(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1));
}

View File

@@ -0,0 +1,107 @@
using System.Text.RegularExpressions;
namespace StellaOps.Scanner.Reachability.Gates.Detectors;
/// <summary>
/// Detects authentication gates in code.
/// </summary>
public sealed class AuthGateDetector : IGateDetector
{
/// <inheritdoc />
public GateType GateType => GateType.AuthRequired;
/// <inheritdoc />
public async Task<IReadOnlyList<DetectedGate>> DetectAsync(
RichGraphNode node,
IReadOnlyList<RichGraphEdge> incomingEdges,
ICodeContentProvider codeProvider,
string language,
CancellationToken ct = default)
{
var gates = new List<DetectedGate>();
var normalizedLanguage = NormalizeLanguage(language);
if (!GatePatterns.AuthPatterns.TryGetValue(normalizedLanguage, out var patterns))
return gates;
// Check node annotations (e.g., attributes, decorators)
if (node.Annotations is { Count: > 0 })
{
foreach (var pattern in patterns)
{
var regex = CreateRegex(pattern.Pattern);
foreach (var annotation in node.Annotations)
{
if (regex.IsMatch(annotation))
{
gates.Add(CreateGate(
node,
pattern,
$"Auth required: {pattern.Description}",
$"annotation:{pattern.Pattern}"));
}
}
}
}
// Check source code content if available
if (node.SourceFile is not null && node.LineNumber is > 0)
{
var startLine = Math.Max(1, node.LineNumber.Value - 5);
var endLine = node.EndLineNumber ?? (node.LineNumber.Value + 10);
var lines = await codeProvider.GetLinesAsync(node.SourceFile, startLine, endLine, ct);
if (lines is { Count: > 0 })
{
var content = string.Join("\n", lines);
foreach (var pattern in patterns)
{
var regex = CreateRegex(pattern.Pattern);
if (regex.IsMatch(content))
{
// Avoid duplicate detection
if (!gates.Any(g => g.DetectionMethod.Contains(pattern.Pattern)))
{
gates.Add(CreateGate(
node,
pattern,
$"Auth required: {pattern.Description}",
$"source:{pattern.Pattern}"));
}
}
}
}
}
return gates;
}
private static DetectedGate CreateGate(
RichGraphNode node,
GatePattern pattern,
string detail,
string detectionMethod) => new()
{
Type = GateType.AuthRequired,
Detail = detail,
GuardSymbol = node.Symbol,
SourceFile = node.SourceFile,
LineNumber = node.LineNumber,
Confidence = pattern.DefaultConfidence,
DetectionMethod = detectionMethod
};
private static string NormalizeLanguage(string language) =>
language.ToLowerInvariant() switch
{
"c#" or "cs" => "csharp",
"js" => "javascript",
"ts" => "typescript",
"py" => "python",
"rb" => "ruby",
_ => language.ToLowerInvariant()
};
private static Regex CreateRegex(string pattern) =>
new(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1));
}

View File

@@ -0,0 +1,119 @@
using System.Text.RegularExpressions;
namespace StellaOps.Scanner.Reachability.Gates.Detectors;
/// <summary>
/// Detects feature flag gates in code.
/// </summary>
public sealed class FeatureFlagDetector : IGateDetector
{
/// <inheritdoc />
public GateType GateType => GateType.FeatureFlag;
/// <inheritdoc />
public async Task<IReadOnlyList<DetectedGate>> DetectAsync(
RichGraphNode node,
IReadOnlyList<RichGraphEdge> incomingEdges,
ICodeContentProvider codeProvider,
string language,
CancellationToken ct = default)
{
var gates = new List<DetectedGate>();
var normalizedLanguage = NormalizeLanguage(language);
if (!GatePatterns.FeatureFlagPatterns.TryGetValue(normalizedLanguage, out var patterns))
return gates;
// Check node annotations
if (node.Annotations is { Count: > 0 })
{
foreach (var pattern in patterns)
{
var regex = CreateRegex(pattern.Pattern);
foreach (var annotation in node.Annotations)
{
if (regex.IsMatch(annotation))
{
gates.Add(CreateGate(
node,
pattern,
$"Feature flag: {pattern.Description}",
$"annotation:{pattern.Pattern}"));
}
}
}
}
// Check source code content
if (node.SourceFile is not null && node.LineNumber is > 0)
{
var startLine = Math.Max(1, node.LineNumber.Value - 10);
var endLine = node.EndLineNumber ?? (node.LineNumber.Value + 20);
var lines = await codeProvider.GetLinesAsync(node.SourceFile, startLine, endLine, ct);
if (lines is { Count: > 0 })
{
var content = string.Join("\n", lines);
foreach (var pattern in patterns)
{
var regex = CreateRegex(pattern.Pattern);
var matches = regex.Matches(content);
if (matches.Count > 0)
{
// Avoid duplicate detection
if (!gates.Any(g => g.DetectionMethod.Contains(pattern.Pattern)))
{
// Extract flag name if possible
var flagName = ExtractFlagName(matches[0].Value);
gates.Add(CreateGate(
node,
pattern,
$"Feature flag: {pattern.Description}" +
(flagName != null ? $" ({flagName})" : ""),
$"source:{pattern.Pattern}"));
}
}
}
}
}
return gates;
}
private static DetectedGate CreateGate(
RichGraphNode node,
GatePattern pattern,
string detail,
string detectionMethod) => new()
{
Type = GateType.FeatureFlag,
Detail = detail,
GuardSymbol = node.Symbol,
SourceFile = node.SourceFile,
LineNumber = node.LineNumber,
Confidence = pattern.DefaultConfidence,
DetectionMethod = detectionMethod
};
private static string? ExtractFlagName(string matchValue)
{
// Try to extract flag name from common patterns
var flagPattern = new Regex(@"[""']([^""']+)[""']", RegexOptions.None, TimeSpan.FromSeconds(1));
var match = flagPattern.Match(matchValue);
return match.Success ? match.Groups[1].Value : null;
}
private static string NormalizeLanguage(string language) =>
language.ToLowerInvariant() switch
{
"c#" or "cs" => "csharp",
"js" => "javascript",
"ts" => "typescript",
"py" => "python",
"rb" => "ruby",
_ => language.ToLowerInvariant()
};
private static Regex CreateRegex(string pattern) =>
new(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1));
}

View File

@@ -0,0 +1,98 @@
namespace StellaOps.Scanner.Reachability.Gates.Detectors;
/// <summary>
/// Interface for gate detectors.
/// </summary>
public interface IGateDetector
{
/// <summary>
/// The type of gate this detector identifies.
/// </summary>
GateType GateType { get; }
/// <summary>
/// Detects gates in the given code node and its incoming edges.
/// </summary>
/// <param name="node">The RichGraph node to analyze.</param>
/// <param name="incomingEdges">Edges leading to this node.</param>
/// <param name="codeProvider">Provider for source code content.</param>
/// <param name="language">Programming language of the code.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of detected gates.</returns>
Task<IReadOnlyList<DetectedGate>> DetectAsync(
RichGraphNode node,
IReadOnlyList<RichGraphEdge> incomingEdges,
ICodeContentProvider codeProvider,
string language,
CancellationToken ct = default);
}
/// <summary>
/// Provider for accessing source code content.
/// </summary>
public interface ICodeContentProvider
{
/// <summary>
/// Gets the source code content for a file.
/// </summary>
/// <param name="filePath">Path to the source file.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Source code content, or null if not available.</returns>
Task<string?> GetContentAsync(string filePath, CancellationToken ct = default);
/// <summary>
/// Gets a range of lines from a source file.
/// </summary>
/// <param name="filePath">Path to the source file.</param>
/// <param name="startLine">Starting line (1-based).</param>
/// <param name="endLine">Ending line (1-based, inclusive).</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Lines of code, or null if not available.</returns>
Task<IReadOnlyList<string>?> GetLinesAsync(
string filePath,
int startLine,
int endLine,
CancellationToken ct = default);
}
/// <summary>
/// Minimal RichGraph node representation for gate detection.
/// </summary>
public sealed record RichGraphNode
{
/// <summary>Unique symbol identifier</summary>
public required string Symbol { get; init; }
/// <summary>Source file path</summary>
public string? SourceFile { get; init; }
/// <summary>Line number in source</summary>
public int? LineNumber { get; init; }
/// <summary>End line number in source</summary>
public int? EndLineNumber { get; init; }
/// <summary>Code annotations (attributes, decorators)</summary>
public IReadOnlyList<string>? Annotations { get; init; }
/// <summary>Node metadata</summary>
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
}
/// <summary>
/// Minimal RichGraph edge representation for gate detection.
/// </summary>
public sealed record RichGraphEdge
{
/// <summary>Source symbol</summary>
public required string FromSymbol { get; init; }
/// <summary>Target symbol</summary>
public required string ToSymbol { get; init; }
/// <summary>Edge type (call, reference, etc.)</summary>
public string? EdgeType { get; init; }
/// <summary>Detected gates on this edge</summary>
public IReadOnlyList<DetectedGate> Gates { get; init; } = [];
}

View File

@@ -0,0 +1,147 @@
using System.Text.RegularExpressions;
namespace StellaOps.Scanner.Reachability.Gates.Detectors;
/// <summary>
/// Detects non-default configuration gates in code.
/// </summary>
public sealed class NonDefaultConfigDetector : IGateDetector
{
/// <inheritdoc />
public GateType GateType => GateType.NonDefaultConfig;
/// <inheritdoc />
public async Task<IReadOnlyList<DetectedGate>> DetectAsync(
RichGraphNode node,
IReadOnlyList<RichGraphEdge> incomingEdges,
ICodeContentProvider codeProvider,
string language,
CancellationToken ct = default)
{
var gates = new List<DetectedGate>();
var normalizedLanguage = NormalizeLanguage(language);
if (!GatePatterns.ConfigPatterns.TryGetValue(normalizedLanguage, out var patterns))
return gates;
// Check node annotations
if (node.Annotations is { Count: > 0 })
{
foreach (var pattern in patterns)
{
var regex = CreateRegex(pattern.Pattern);
foreach (var annotation in node.Annotations)
{
if (regex.IsMatch(annotation))
{
gates.Add(CreateGate(
node,
pattern,
$"Non-default config: {pattern.Description}",
$"annotation:{pattern.Pattern}"));
}
}
}
}
// Check source code content
if (node.SourceFile is not null && node.LineNumber is > 0)
{
var startLine = Math.Max(1, node.LineNumber.Value - 10);
var endLine = node.EndLineNumber ?? (node.LineNumber.Value + 25);
var lines = await codeProvider.GetLinesAsync(node.SourceFile, startLine, endLine, ct);
if (lines is { Count: > 0 })
{
var content = string.Join("\n", lines);
foreach (var pattern in patterns)
{
var regex = CreateRegex(pattern.Pattern);
var matches = regex.Matches(content);
if (matches.Count > 0)
{
// Avoid duplicate detection
if (!gates.Any(g => g.DetectionMethod.Contains(pattern.Pattern)))
{
var configName = ExtractConfigName(matches[0].Value);
gates.Add(CreateGate(
node,
pattern,
$"Non-default config: {pattern.Description}" +
(configName != null ? $" ({configName})" : ""),
$"source:{pattern.Pattern}"));
}
}
}
}
}
// Check metadata for configuration hints
if (node.Metadata is not null)
{
foreach (var (key, value) in node.Metadata)
{
if (key.Contains("config", StringComparison.OrdinalIgnoreCase) ||
key.Contains("setting", StringComparison.OrdinalIgnoreCase) ||
key.Contains("option", StringComparison.OrdinalIgnoreCase))
{
if (value.Contains("enabled", StringComparison.OrdinalIgnoreCase) ||
value.Contains("disabled", StringComparison.OrdinalIgnoreCase) ||
value.Contains("true", StringComparison.OrdinalIgnoreCase) ||
value.Contains("false", StringComparison.OrdinalIgnoreCase))
{
gates.Add(new DetectedGate
{
Type = GateType.NonDefaultConfig,
Detail = $"Non-default config: metadata {key}={value}",
GuardSymbol = node.Symbol,
SourceFile = node.SourceFile,
LineNumber = node.LineNumber,
Confidence = 0.65,
DetectionMethod = $"metadata:{key}"
});
}
}
}
}
return gates;
}
private static DetectedGate CreateGate(
RichGraphNode node,
GatePattern pattern,
string detail,
string detectionMethod) => new()
{
Type = GateType.NonDefaultConfig,
Detail = detail,
GuardSymbol = node.Symbol,
SourceFile = node.SourceFile,
LineNumber = node.LineNumber,
Confidence = pattern.DefaultConfidence,
DetectionMethod = detectionMethod
};
private static string? ExtractConfigName(string matchValue)
{
// Try to extract config key from common patterns
var configPattern = new Regex(@"[""']([^""']+)[""']", RegexOptions.None, TimeSpan.FromSeconds(1));
var match = configPattern.Match(matchValue);
return match.Success ? match.Groups[1].Value : null;
}
private static string NormalizeLanguage(string language) =>
language.ToLowerInvariant() switch
{
"c#" or "cs" => "csharp",
"js" => "javascript",
"ts" => "typescript",
"py" => "python",
"rb" => "ruby",
_ => language.ToLowerInvariant()
};
private static Regex CreateRegex(string pattern) =>
new(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1));
}

View File

@@ -0,0 +1,116 @@
namespace StellaOps.Scanner.Reachability.Gates;
/// <summary>
/// Types of gates that can protect code paths.
/// </summary>
public enum GateType
{
/// <summary>Requires authentication (e.g., JWT, session, API key)</summary>
AuthRequired,
/// <summary>Behind a feature flag</summary>
FeatureFlag,
/// <summary>Requires admin or elevated role</summary>
AdminOnly,
/// <summary>Requires non-default configuration</summary>
NonDefaultConfig
}
/// <summary>
/// A detected gate protecting a code path.
/// </summary>
public sealed record DetectedGate
{
/// <summary>Type of gate</summary>
public required GateType Type { get; init; }
/// <summary>Human-readable description</summary>
public required string Detail { get; init; }
/// <summary>Symbol where gate was detected</summary>
public required string GuardSymbol { get; init; }
/// <summary>Source file (if available)</summary>
public string? SourceFile { get; init; }
/// <summary>Line number (if available)</summary>
public int? LineNumber { get; init; }
/// <summary>Confidence score (0.0-1.0)</summary>
public required double Confidence { get; init; }
/// <summary>Detection method used</summary>
public required string DetectionMethod { get; init; }
}
/// <summary>
/// Result of gate detection on a call path.
/// </summary>
public sealed record GateDetectionResult
{
/// <summary>Empty result with no gates</summary>
public static readonly GateDetectionResult Empty = new() { Gates = [] };
/// <summary>All gates detected on the path</summary>
public required IReadOnlyList<DetectedGate> Gates { get; init; }
/// <summary>Whether any gates were detected</summary>
public bool HasGates => Gates.Count > 0;
/// <summary>Highest-confidence gate (if any)</summary>
public DetectedGate? PrimaryGate => Gates
.OrderByDescending(g => g.Confidence)
.FirstOrDefault();
/// <summary>Combined multiplier in basis points (10000 = 100%)</summary>
public int CombinedMultiplierBps { get; init; } = 10000;
}
/// <summary>
/// Multiplier configuration for different gate types.
/// </summary>
public sealed record GateMultiplierConfig
{
/// <summary>Default configuration with standard multipliers.</summary>
public static GateMultiplierConfig Default { get; } = new()
{
AuthRequiredMultiplierBps = 3000, // 30%
FeatureFlagMultiplierBps = 2000, // 20%
AdminOnlyMultiplierBps = 1500, // 15%
NonDefaultConfigMultiplierBps = 5000, // 50%
MinimumMultiplierBps = 500, // 5% floor
MaxMultipliersBps = 10000 // 100% cap
};
/// <summary>Multiplier for auth-required gates (basis points)</summary>
public int AuthRequiredMultiplierBps { get; init; } = 3000;
/// <summary>Multiplier for feature flag gates (basis points)</summary>
public int FeatureFlagMultiplierBps { get; init; } = 2000;
/// <summary>Multiplier for admin-only gates (basis points)</summary>
public int AdminOnlyMultiplierBps { get; init; } = 1500;
/// <summary>Multiplier for non-default config gates (basis points)</summary>
public int NonDefaultConfigMultiplierBps { get; init; } = 5000;
/// <summary>Minimum multiplier floor (basis points)</summary>
public int MinimumMultiplierBps { get; init; } = 500;
/// <summary>Maximum combined multiplier (basis points)</summary>
public int MaxMultipliersBps { get; init; } = 10000;
/// <summary>
/// Gets the multiplier for a specific gate type.
/// </summary>
public int GetMultiplierBps(GateType type) => type switch
{
GateType.AuthRequired => AuthRequiredMultiplierBps,
GateType.FeatureFlag => FeatureFlagMultiplierBps,
GateType.AdminOnly => AdminOnlyMultiplierBps,
GateType.NonDefaultConfig => NonDefaultConfigMultiplierBps,
_ => MaxMultipliersBps
};
}

View File

@@ -0,0 +1,140 @@
namespace StellaOps.Scanner.Reachability.Gates;
/// <summary>
/// Calculates gate multipliers for vulnerability scoring.
/// </summary>
public sealed class GateMultiplierCalculator
{
private readonly GateMultiplierConfig _config;
/// <summary>
/// Creates a new calculator with the specified configuration.
/// </summary>
public GateMultiplierCalculator(GateMultiplierConfig? config = null)
{
_config = config ?? GateMultiplierConfig.Default;
}
/// <summary>
/// Calculates the combined multiplier for a set of detected gates.
/// Uses product reduction: each gate compounds with others.
/// </summary>
/// <param name="gates">The detected gates.</param>
/// <returns>Combined multiplier in basis points (10000 = 100%).</returns>
public int CalculateCombinedMultiplierBps(IReadOnlyList<DetectedGate> gates)
{
if (gates.Count == 0)
return 10000; // 100% - no reduction
// Group gates by type and take highest confidence per type
var gatesByType = gates
.GroupBy(g => g.Type)
.Select(g => new
{
Type = g.Key,
MaxConfidence = g.Max(x => x.Confidence)
})
.ToList();
// Calculate compound multiplier using product reduction
// Each gate multiplier is confidence-weighted
double multiplier = 1.0;
foreach (var gate in gatesByType)
{
var baseMultiplierBps = _config.GetMultiplierBps(gate.Type);
// Scale multiplier by confidence
// Low confidence = less reduction, high confidence = more reduction
var effectiveMultiplierBps = InterpolateMultiplier(
baseMultiplierBps,
10000, // No reduction at 0 confidence
gate.MaxConfidence);
multiplier *= effectiveMultiplierBps / 10000.0;
}
// Apply floor
var result = (int)(multiplier * 10000);
return Math.Max(result, _config.MinimumMultiplierBps);
}
/// <summary>
/// Calculates the multiplier for a single gate.
/// </summary>
/// <param name="gate">The detected gate.</param>
/// <returns>Multiplier in basis points (10000 = 100%).</returns>
public int CalculateSingleMultiplierBps(DetectedGate gate)
{
var baseMultiplierBps = _config.GetMultiplierBps(gate.Type);
return InterpolateMultiplier(baseMultiplierBps, 10000, gate.Confidence);
}
/// <summary>
/// Creates a gate detection result with calculated multiplier.
/// </summary>
/// <param name="gates">The detected gates.</param>
/// <returns>Gate detection result with combined multiplier.</returns>
public GateDetectionResult CreateResult(IReadOnlyList<DetectedGate> gates)
{
return new GateDetectionResult
{
Gates = gates,
CombinedMultiplierBps = CalculateCombinedMultiplierBps(gates)
};
}
/// <summary>
/// Applies the multiplier to a base score.
/// </summary>
/// <param name="baseScore">The base score (e.g., CVSS).</param>
/// <param name="multiplierBps">Multiplier in basis points.</param>
/// <returns>Adjusted score.</returns>
public static double ApplyMultiplier(double baseScore, int multiplierBps)
{
return baseScore * multiplierBps / 10000.0;
}
private static int InterpolateMultiplier(int minBps, int maxBps, double confidence)
{
// Linear interpolation: higher confidence = lower multiplier (closer to minBps)
var range = maxBps - minBps;
var reduction = (int)(range * confidence);
return maxBps - reduction;
}
}
/// <summary>
/// Extension methods for gate detection results.
/// </summary>
public static class GateDetectionResultExtensions
{
/// <summary>
/// Applies the gate multiplier to a CVSS score.
/// </summary>
/// <param name="result">The gate detection result.</param>
/// <param name="cvssScore">Base CVSS score (0.0-10.0).</param>
/// <returns>Adjusted CVSS score.</returns>
public static double ApplyToCvss(this GateDetectionResult result, double cvssScore)
{
return Math.Round(cvssScore * result.CombinedMultiplierBps / 10000.0, 1);
}
/// <summary>
/// Gets a human-readable summary of the gate effects.
/// </summary>
/// <param name="result">The gate detection result.</param>
/// <returns>Summary string.</returns>
public static string GetSummary(this GateDetectionResult result)
{
if (!result.HasGates)
return "No gates detected";
var percentage = result.CombinedMultiplierBps / 100.0;
var gateTypes = result.Gates
.Select(g => g.Type)
.Distinct()
.Select(t => t.ToString());
return $"Gates: {string.Join(", ", gateTypes)} -> {percentage:F1}% severity";
}
}

View File

@@ -0,0 +1,217 @@
namespace StellaOps.Scanner.Reachability.Gates;
/// <summary>
/// Gate detection patterns for various languages and frameworks.
/// </summary>
public static class GatePatterns
{
/// <summary>
/// Authentication gate patterns by language/framework.
/// </summary>
public static readonly IReadOnlyDictionary<string, IReadOnlyList<GatePattern>> AuthPatterns = new Dictionary<string, IReadOnlyList<GatePattern>>
{
["csharp"] =
[
new GatePattern(@"\[Authorize\]", "ASP.NET Core Authorize attribute", 0.95),
new GatePattern(@"\[Authorize\(.*Roles.*\)\]", "ASP.NET Core Role-based auth", 0.95),
new GatePattern(@"\.RequireAuthorization\(\)", "Minimal API authorization", 0.90),
new GatePattern(@"User\.Identity\.IsAuthenticated", "Identity check", 0.85),
new GatePattern(@"ClaimsPrincipal", "Claims-based auth", 0.80)
],
["java"] =
[
new GatePattern(@"@PreAuthorize", "Spring Security PreAuthorize", 0.95),
new GatePattern(@"@Secured", "Spring Security Secured", 0.95),
new GatePattern(@"@RolesAllowed", "JAX-RS RolesAllowed", 0.90),
new GatePattern(@"SecurityContextHolder\.getContext\(\)", "Spring Security context", 0.85),
new GatePattern(@"HttpServletRequest\.getUserPrincipal\(\)", "Servlet principal", 0.80)
],
["javascript"] =
[
new GatePattern(@"passport\.authenticate", "Passport.js auth", 0.90),
new GatePattern(@"jwt\.verify", "JWT verification", 0.90),
new GatePattern(@"req\.isAuthenticated\(\)", "Passport isAuthenticated", 0.85),
new GatePattern(@"\.use\(.*auth.*middleware", "Auth middleware", 0.80)
],
["typescript"] =
[
new GatePattern(@"passport\.authenticate", "Passport.js auth", 0.90),
new GatePattern(@"jwt\.verify", "JWT verification", 0.90),
new GatePattern(@"@UseGuards\(.*AuthGuard", "NestJS AuthGuard", 0.95),
new GatePattern(@"req\.isAuthenticated\(\)", "Passport isAuthenticated", 0.85)
],
["python"] =
[
new GatePattern(@"@login_required", "Flask/Django login required", 0.95),
new GatePattern(@"@permission_required", "Django permission required", 0.90),
new GatePattern(@"request\.user\.is_authenticated", "Django auth check", 0.85),
new GatePattern(@"jwt\.decode", "PyJWT decode", 0.85)
],
["go"] =
[
new GatePattern(@"\.Use\(.*[Aa]uth", "Auth middleware", 0.85),
new GatePattern(@"jwt\.Parse", "JWT parsing", 0.90),
new GatePattern(@"context\.Value\(.*[Uu]ser", "User context", 0.75)
],
["ruby"] =
[
new GatePattern(@"before_action :authenticate", "Rails authentication", 0.90),
new GatePattern(@"authenticate_user!", "Devise authentication", 0.95),
new GatePattern(@"current_user\.present\?", "User presence check", 0.80)
]
};
/// <summary>
/// Feature flag patterns.
/// </summary>
public static readonly IReadOnlyDictionary<string, IReadOnlyList<GatePattern>> FeatureFlagPatterns = new Dictionary<string, IReadOnlyList<GatePattern>>
{
["csharp"] =
[
new GatePattern(@"IFeatureManager\.IsEnabled", "ASP.NET Feature Management", 0.95),
new GatePattern(@"\.IsFeatureEnabled\(", "Generic feature flag", 0.85),
new GatePattern(@"LaunchDarkly.*Variation", "LaunchDarkly SDK", 0.95),
new GatePattern(@"Flipper\.IsEnabled", "Flipper feature flags", 0.90)
],
["java"] =
[
new GatePattern(@"@FeatureToggle", "Feature toggle annotation", 0.90),
new GatePattern(@"UnleashClient\.isEnabled", "Unleash SDK", 0.95),
new GatePattern(@"LaunchDarklyClient\.boolVariation", "LaunchDarkly SDK", 0.95),
new GatePattern(@"FF4j\.check", "FF4J feature flags", 0.90)
],
["javascript"] =
[
new GatePattern(@"ldClient\.variation", "LaunchDarkly JS SDK", 0.95),
new GatePattern(@"unleash\.isEnabled", "Unleash JS SDK", 0.95),
new GatePattern(@"process\.env\.FEATURE_", "Environment feature flag", 0.70),
new GatePattern(@"flagsmith\.hasFeature", "Flagsmith SDK", 0.90)
],
["typescript"] =
[
new GatePattern(@"ldClient\.variation", "LaunchDarkly JS SDK", 0.95),
new GatePattern(@"unleash\.isEnabled", "Unleash JS SDK", 0.95),
new GatePattern(@"process\.env\.FEATURE_", "Environment feature flag", 0.70)
],
["python"] =
[
new GatePattern(@"@feature_flag", "Feature flag decorator", 0.90),
new GatePattern(@"ldclient\.variation", "LaunchDarkly Python", 0.95),
new GatePattern(@"os\.environ\.get\(['\"]FEATURE_", "Env feature flag", 0.70),
new GatePattern(@"waffle\.flag_is_active", "Django Waffle", 0.90)
],
["go"] =
[
new GatePattern(@"unleash\.IsEnabled", "Unleash Go SDK", 0.95),
new GatePattern(@"ldclient\.BoolVariation", "LaunchDarkly Go", 0.95),
new GatePattern(@"os\.Getenv\(\"FEATURE_", "Env feature flag", 0.70)
],
["ruby"] =
[
new GatePattern(@"Flipper\.enabled\?", "Flipper feature flags", 0.95),
new GatePattern(@"Feature\.active\?", "Generic feature check", 0.85)
]
};
/// <summary>
/// Admin/role check patterns.
/// </summary>
public static readonly IReadOnlyDictionary<string, IReadOnlyList<GatePattern>> AdminPatterns = new Dictionary<string, IReadOnlyList<GatePattern>>
{
["csharp"] =
[
new GatePattern(@"\[Authorize\(Roles\s*=\s*[""']Admin", "Admin role check", 0.95),
new GatePattern(@"\.IsInRole\([""'][Aa]dmin", "IsInRole admin", 0.90),
new GatePattern(@"Policy\s*=\s*[""']Admin", "Admin policy", 0.90),
new GatePattern(@"\[Authorize\(Roles\s*=\s*[""'].*[Ss]uperuser", "Superuser role", 0.95)
],
["java"] =
[
new GatePattern(@"hasRole\([""']ADMIN", "Spring hasRole ADMIN", 0.95),
new GatePattern(@"@RolesAllowed\([""']admin", "Admin role allowed", 0.95),
new GatePattern(@"hasAuthority\([""']ROLE_ADMIN", "Spring authority admin", 0.95)
],
["javascript"] =
[
new GatePattern(@"req\.user\.role\s*===?\s*[""']admin", "Admin role check", 0.85),
new GatePattern(@"isAdmin\(\)", "isAdmin function", 0.80),
new GatePattern(@"user\.roles\.includes\([""']admin", "Admin roles check", 0.85)
],
["typescript"] =
[
new GatePattern(@"req\.user\.role\s*===?\s*[""']admin", "Admin role check", 0.85),
new GatePattern(@"@Roles\([""']admin", "NestJS Roles decorator", 0.95),
new GatePattern(@"user\.roles\.includes\([""']admin", "Admin roles check", 0.85)
],
["python"] =
[
new GatePattern(@"@user_passes_test\(.*is_superuser", "Django superuser", 0.95),
new GatePattern(@"@permission_required\([""']admin", "Admin permission", 0.90),
new GatePattern(@"request\.user\.is_staff", "Django staff check", 0.85)
],
["go"] =
[
new GatePattern(@"\.HasRole\([""'][Aa]dmin", "Admin role check", 0.90),
new GatePattern(@"isAdmin\(", "Admin function call", 0.80)
],
["ruby"] =
[
new GatePattern(@"current_user\.admin\?", "Admin user check", 0.90),
new GatePattern(@"authorize! :manage", "CanCanCan manage", 0.90)
]
};
/// <summary>
/// Non-default configuration patterns.
/// </summary>
public static readonly IReadOnlyDictionary<string, IReadOnlyList<GatePattern>> ConfigPatterns = new Dictionary<string, IReadOnlyList<GatePattern>>
{
["csharp"] =
[
new GatePattern(@"IConfiguration\[.*\]\s*==\s*[""']true", "Config-gated feature", 0.75),
new GatePattern(@"options\.Value\.[A-Z].*Enabled", "Options pattern enabled", 0.80),
new GatePattern(@"configuration\.GetValue<bool>", "Config bool value", 0.75)
],
["java"] =
[
new GatePattern(@"@ConditionalOnProperty", "Spring conditional property", 0.90),
new GatePattern(@"@Value\([""']\$\{.*enabled", "Spring property enabled", 0.80),
new GatePattern(@"\.getProperty\([""'].*\.enabled", "Property enabled check", 0.75)
],
["javascript"] =
[
new GatePattern(@"config\.[a-z]+\.enabled", "Config enabled check", 0.75),
new GatePattern(@"process\.env\.[A-Z_]+_ENABLED", "Env enabled flag", 0.70),
new GatePattern(@"settings\.[a-z]+\.enabled", "Settings enabled", 0.75)
],
["typescript"] =
[
new GatePattern(@"config\.[a-z]+\.enabled", "Config enabled check", 0.75),
new GatePattern(@"process\.env\.[A-Z_]+_ENABLED", "Env enabled flag", 0.70)
],
["python"] =
[
new GatePattern(@"settings\.[A-Z_]+_ENABLED", "Django settings enabled", 0.75),
new GatePattern(@"os\.getenv\([""'][A-Z_]+_ENABLED", "Env enabled check", 0.70),
new GatePattern(@"config\.get\([""'].*enabled", "Config enabled", 0.75)
],
["go"] =
[
new GatePattern(@"viper\.GetBool\([""'].*enabled", "Viper bool config", 0.80),
new GatePattern(@"os\.Getenv\([""'][A-Z_]+_ENABLED", "Env enabled", 0.70)
],
["ruby"] =
[
new GatePattern(@"Rails\.configuration\.[a-z_]+_enabled", "Rails config enabled", 0.75),
new GatePattern(@"ENV\[[""'][A-Z_]+_ENABLED", "Env enabled", 0.70)
]
};
}
/// <summary>
/// A regex pattern for gate detection.
/// </summary>
/// <param name="Pattern">Regex pattern string</param>
/// <param name="Description">Human-readable description</param>
/// <param name="DefaultConfidence">Default confidence score (0.0-1.0)</param>
public sealed record GatePattern(string Pattern, string Description, double DefaultConfidence);