save progress
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Secrets;
|
||||
|
||||
/// <summary>
|
||||
/// A single secret detection rule defining patterns and metadata for identifying secrets.
|
||||
/// </summary>
|
||||
public sealed record SecretRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique rule identifier (e.g., "stellaops.secrets.aws-access-key").
|
||||
/// </summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rule version in SemVer format (e.g., "1.0.0").
|
||||
/// </summary>
|
||||
public required string Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable rule name.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Detailed description of what this rule detects.
|
||||
/// </summary>
|
||||
public required string Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The detection strategy type.
|
||||
/// </summary>
|
||||
public required SecretRuleType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The detection pattern (regex pattern for Regex type, entropy config for Entropy type).
|
||||
/// </summary>
|
||||
public required string Pattern { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Default severity for findings from this rule.
|
||||
/// </summary>
|
||||
public required SecretSeverity Severity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Default confidence level for findings from this rule.
|
||||
/// </summary>
|
||||
public required SecretConfidence Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional masking hint (e.g., "prefix:4,suffix:2") for payload masking.
|
||||
/// </summary>
|
||||
public string? MaskingHint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pre-filter keywords for fast rejection of non-matching content.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> Keywords { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Glob patterns for files this rule should be applied to.
|
||||
/// Empty means all text files.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> FilePatterns { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether this rule is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum entropy threshold for entropy-based detection.
|
||||
/// Only used when Type is Entropy or Composite.
|
||||
/// </summary>
|
||||
public double EntropyThreshold { get; init; } = 4.5;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum string length for entropy-based detection.
|
||||
/// </summary>
|
||||
public int MinLength { get; init; } = 16;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum string length for detection (prevents matching entire files).
|
||||
/// </summary>
|
||||
public int MaxLength { get; init; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Optional metadata for the rule.
|
||||
/// </summary>
|
||||
public ImmutableDictionary<string, string> Metadata { get; init; } =
|
||||
ImmutableDictionary<string, string>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The compiled regex pattern, created lazily.
|
||||
/// </summary>
|
||||
private Regex? _compiledPattern;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the compiled regex for this rule. Returns null if the pattern is invalid.
|
||||
/// </summary>
|
||||
public Regex? GetCompiledPattern()
|
||||
{
|
||||
if (Type == SecretRuleType.Entropy)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_compiledPattern is not null)
|
||||
{
|
||||
return _compiledPattern;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_compiledPattern = new Regex(
|
||||
Pattern,
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant,
|
||||
TimeSpan.FromSeconds(5));
|
||||
return _compiledPattern;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the content might match this rule based on keywords.
|
||||
/// Returns true if no keywords are defined or if any keyword is found.
|
||||
/// </summary>
|
||||
public bool MightMatch(ReadOnlySpan<char> content)
|
||||
{
|
||||
if (Keywords.IsDefaultOrEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var keyword in Keywords)
|
||||
{
|
||||
if (content.Contains(keyword.AsSpan(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this rule should be applied to the given file path.
|
||||
/// </summary>
|
||||
public bool AppliesToFile(string filePath)
|
||||
{
|
||||
if (FilePatterns.IsDefaultOrEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
foreach (var pattern in FilePatterns)
|
||||
{
|
||||
if (MatchesGlob(fileName, pattern) || MatchesGlob(filePath, pattern))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool MatchesGlob(string path, string pattern)
|
||||
{
|
||||
// Simple glob matching for common patterns
|
||||
if (pattern.StartsWith("**", StringComparison.Ordinal))
|
||||
{
|
||||
var suffix = pattern[2..].TrimStart('/').TrimStart('\\');
|
||||
if (suffix.StartsWith("*.", StringComparison.Ordinal))
|
||||
{
|
||||
var extension = suffix[1..];
|
||||
return path.EndsWith(extension, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
return path.Contains(suffix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (pattern.StartsWith("*.", StringComparison.Ordinal))
|
||||
{
|
||||
var extension = pattern[1..];
|
||||
return path.EndsWith(extension, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return path.Equals(pattern, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user