Add comprehensive security tests for OWASP A03 (Injection) and A10 (SSRF)
- 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:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user