feat: Add VEX Status Chip component and integration tests for reachability drift detection

- Introduced `VexStatusChipComponent` to display VEX status with color coding and tooltips.
- Implemented integration tests for reachability drift detection, covering various scenarios including drift detection, determinism, and error handling.
- Enhanced `ScannerToSignalsReachabilityTests` with a null implementation of `ICallGraphSyncService` for better test isolation.
- Updated project references to include the new Reachability Drift library.
This commit is contained in:
StellaOps Bot
2025-12-20 01:26:42 +02:00
parent edc91ea96f
commit 5fc469ad98
159 changed files with 41116 additions and 2305 deletions

View File

@@ -0,0 +1,635 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.EntryTrace.FileSystem;
namespace StellaOps.Scanner.EntryTrace.Baseline;
/// <summary>
/// Context for baseline analysis.
/// </summary>
public sealed record BaselineAnalysisContext
{
/// <summary>Scan identifier.</summary>
public required string ScanId { get; init; }
/// <summary>Root path for scanning.</summary>
public required string RootPath { get; init; }
/// <summary>Configuration to use.</summary>
public required EntryTraceBaselineConfig Config { get; init; }
/// <summary>File system abstraction.</summary>
public IRootFileSystem? FileSystem { get; init; }
/// <summary>Known vulnerabilities for reachability analysis.</summary>
public IReadOnlyList<string>? KnownVulnerabilities { get; init; }
}
/// <summary>
/// Interface for baseline entry point analysis.
/// </summary>
public interface IBaselineAnalyzer
{
/// <summary>
/// Performs baseline entry point analysis.
/// </summary>
Task<BaselineReport> AnalyzeAsync(
BaselineAnalysisContext context,
CancellationToken cancellationToken = default);
/// <summary>
/// Streams detected entry points for large codebases.
/// </summary>
IAsyncEnumerable<DetectedEntryPoint> StreamEntryPointsAsync(
BaselineAnalysisContext context,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Pattern-based baseline analyzer for entry point detection.
/// </summary>
/// <remarks>
/// Implements SCANNER-ENTRYTRACE-18-508: EntryTrace baseline analysis.
/// </remarks>
public sealed class BaselineAnalyzer : IBaselineAnalyzer
{
private readonly ILogger<BaselineAnalyzer> _logger;
private readonly Dictionary<string, Regex> _compiledPatterns = new();
public BaselineAnalyzer(ILogger<BaselineAnalyzer> logger)
{
_logger = logger;
}
public async Task<BaselineReport> AnalyzeAsync(
BaselineAnalysisContext context,
CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
var entryPoints = new List<DetectedEntryPoint>();
var frameworksDetected = new HashSet<string>();
var filesAnalyzed = 0;
var filesSkipped = 0;
_logger.LogInformation("Starting baseline analysis for scan {ScanId}", context.ScanId);
await foreach (var entryPoint in StreamEntryPointsAsync(context, cancellationToken))
{
entryPoints.Add(entryPoint);
if (entryPoint.Framework is not null)
{
frameworksDetected.Add(entryPoint.Framework);
}
}
// Count files (simplified - would need proper tracking in production)
filesAnalyzed = await CountFilesAsync(context, cancellationToken);
stopwatch.Stop();
var statistics = ComputeStatistics(entryPoints, filesAnalyzed, filesSkipped);
var digest = BaselineReport.ComputeDigest(entryPoints);
var report = new BaselineReport
{
ReportId = Guid.NewGuid(),
ScanId = context.ScanId,
GeneratedAt = DateTimeOffset.UtcNow,
ConfigUsed = context.Config.ConfigId,
EntryPoints = entryPoints.ToImmutableArray(),
Statistics = statistics,
FrameworksDetected = frameworksDetected.OrderBy(f => f).ToImmutableArray(),
AnalysisDurationMs = stopwatch.ElapsedMilliseconds,
Digest = digest
};
_logger.LogInformation(
"Baseline analysis complete: {EntryPointCount} entry points in {Duration}ms",
entryPoints.Count, stopwatch.ElapsedMilliseconds);
return report;
}
public async IAsyncEnumerable<DetectedEntryPoint> StreamEntryPointsAsync(
BaselineAnalysisContext context,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var config = context.Config;
var fileExtensions = GetFileExtensions(config.Language);
var excludePatterns = BuildExcludePatterns(config.Exclusions);
await foreach (var filePath in EnumerateFilesAsync(context.RootPath, fileExtensions, cancellationToken))
{
if (ShouldExclude(filePath, excludePatterns, config.Exclusions))
{
continue;
}
string content;
try
{
content = await File.ReadAllTextAsync(filePath, cancellationToken);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to read file {FilePath}", filePath);
continue;
}
var relativePath = Path.GetRelativePath(context.RootPath, filePath);
var lines = content.Split('\n');
var detectedFramework = DetectFramework(content, config.FrameworkConfigs);
foreach (var pattern in config.EntryPointPatterns)
{
// Skip patterns not for this framework
if (pattern.Framework is not null && detectedFramework is not null &&
!pattern.Framework.Equals(detectedFramework, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var matches = FindMatches(content, lines, pattern, relativePath);
foreach (var match in matches)
{
if (match.Confidence >= config.Heuristics.ConfidenceThreshold)
{
var entryPoint = CreateEntryPoint(match, pattern, detectedFramework);
yield return entryPoint;
}
}
}
}
}
private IEnumerable<PatternMatch> FindMatches(
string content,
string[] lines,
EntryPointPattern pattern,
string filePath)
{
var regex = GetCompiledPattern(pattern);
if (regex is null)
yield break;
var matches = regex.Matches(content);
foreach (Match match in matches)
{
var (line, column) = GetLineAndColumn(content, match.Index);
var functionName = ExtractFunctionName(lines, line);
var confidence = CalculateConfidence(pattern, match, lines, line);
yield return new PatternMatch
{
FilePath = filePath,
Line = line,
Column = column,
MatchedText = match.Value,
FunctionName = functionName,
Pattern = pattern,
Confidence = confidence,
Groups = match.Groups.Cast<Group>()
.Where(g => g.Success && !string.IsNullOrEmpty(g.Name) && !int.TryParse(g.Name, out _))
.ToImmutableDictionary(g => g.Name, g => g.Value)
};
}
}
private Regex? GetCompiledPattern(EntryPointPattern pattern)
{
if (_compiledPatterns.TryGetValue(pattern.PatternId, out var cached))
return cached;
try
{
var regex = new Regex(
pattern.Pattern,
RegexOptions.Compiled | RegexOptions.Multiline,
TimeSpan.FromSeconds(5));
_compiledPatterns[pattern.PatternId] = regex;
return regex;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to compile pattern {PatternId}: {Pattern}",
pattern.PatternId, pattern.Pattern);
return null;
}
}
private string? DetectFramework(string content, ImmutableArray<FrameworkConfig> frameworks)
{
foreach (var framework in frameworks)
{
foreach (var detection in framework.DetectionPatterns)
{
if (content.Contains(detection, StringComparison.OrdinalIgnoreCase))
{
return framework.FrameworkId;
}
}
}
return null;
}
private static (int line, int column) GetLineAndColumn(string content, int index)
{
var line = 1;
var lastNewline = -1;
for (var i = 0; i < index && i < content.Length; i++)
{
if (content[i] == '\n')
{
line++;
lastNewline = i;
}
}
var column = index - lastNewline;
return (line, column);
}
private static string? ExtractFunctionName(string[] lines, int lineNumber)
{
if (lineNumber < 1 || lineNumber > lines.Length)
return null;
var line = lines[lineNumber - 1];
// Try common function/method patterns
var patterns = new[]
{
@"(?:def|function|func)\s+(\w+)", // Python, JS, Go
@"(?:public|private|protected)?\s*(?:static)?\s*\w+\s+(\w+)\s*\(", // Java/C#
@"(\w+)\s*[=:]\s*(?:async\s+)?(?:function|\()", // JS arrow/named
};
foreach (var pattern in patterns)
{
var match = Regex.Match(line, pattern);
if (match.Success && match.Groups.Count > 1)
{
return match.Groups[1].Value;
}
}
return null;
}
private double CalculateConfidence(
EntryPointPattern pattern,
Match match,
string[] lines,
int lineNumber)
{
var baseConfidence = pattern.Confidence;
// Boost for annotation patterns (highest reliability)
if (pattern.Type == PatternType.Annotation || pattern.Type == PatternType.Decorator)
{
baseConfidence = Math.Min(1.0, baseConfidence * 1.1);
}
// Check surrounding context for additional confidence
if (lineNumber > 0 && lineNumber <= lines.Length)
{
var line = lines[lineNumber - 1];
// Boost if line contains routing keywords
if (Regex.IsMatch(line, @"\b(route|path|endpoint|api|handler)\b", RegexOptions.IgnoreCase))
{
baseConfidence = Math.Min(1.0, baseConfidence + 0.05);
}
// Reduce for test files (if not already excluded)
if (Regex.IsMatch(line, @"\b(test|spec|mock)\b", RegexOptions.IgnoreCase))
{
baseConfidence *= 0.5;
}
}
return Math.Round(baseConfidence, 3);
}
private DetectedEntryPoint CreateEntryPoint(
PatternMatch match,
EntryPointPattern pattern,
string? framework)
{
var entryId = DetectedEntryPoint.GenerateEntryId(
match.FilePath,
match.FunctionName ?? "anonymous",
match.Line,
pattern.EntryType);
var httpMetadata = ExtractHttpMetadata(match, pattern);
var parameters = ExtractParameters(match, pattern);
return new DetectedEntryPoint
{
EntryId = entryId,
Type = pattern.EntryType,
Name = match.FunctionName ?? "anonymous",
Location = new CodeLocation
{
FilePath = match.FilePath,
LineStart = match.Line,
LineEnd = match.Line,
ColumnStart = match.Column,
ColumnEnd = match.Column + match.MatchedText.Length,
FunctionName = match.FunctionName
},
Confidence = match.Confidence,
Framework = framework ?? pattern.Framework,
HttpMetadata = httpMetadata,
Parameters = parameters,
DetectionMethod = pattern.PatternId
};
}
private HttpMetadata? ExtractHttpMetadata(PatternMatch match, EntryPointPattern pattern)
{
if (pattern.EntryType != EntryPointType.HttpEndpoint)
return null;
// Try to extract HTTP method and path from match groups
var method = HttpMethod.GET;
var path = "/";
if (match.Groups.TryGetValue("method", out var methodStr))
{
method = ParseHttpMethod(methodStr);
}
else if (pattern.PatternId.Contains("get", StringComparison.OrdinalIgnoreCase))
{
method = HttpMethod.GET;
}
else if (pattern.PatternId.Contains("post", StringComparison.OrdinalIgnoreCase))
{
method = HttpMethod.POST;
}
else if (pattern.PatternId.Contains("put", StringComparison.OrdinalIgnoreCase))
{
method = HttpMethod.PUT;
}
else if (pattern.PatternId.Contains("delete", StringComparison.OrdinalIgnoreCase))
{
method = HttpMethod.DELETE;
}
if (match.Groups.TryGetValue("path", out var pathStr))
{
path = pathStr;
}
else
{
// Try to extract path from matched text
var pathMatch = Regex.Match(match.MatchedText, @"['""]([^'""]+)['""]");
if (pathMatch.Success)
{
path = pathMatch.Groups[1].Value;
}
}
// Extract path parameters
var pathParams = Regex.Matches(path, @":(\w+)|{(\w+)}")
.Cast<Match>()
.Select(m => m.Groups[1].Success ? m.Groups[1].Value : m.Groups[2].Value)
.ToImmutableArray();
return new HttpMetadata
{
Method = method,
Path = path,
PathParameters = pathParams
};
}
private static HttpMethod ParseHttpMethod(string method)
{
return method.ToUpperInvariant() switch
{
"GET" => HttpMethod.GET,
"POST" => HttpMethod.POST,
"PUT" => HttpMethod.PUT,
"PATCH" => HttpMethod.PATCH,
"DELETE" => HttpMethod.DELETE,
"HEAD" => HttpMethod.HEAD,
"OPTIONS" => HttpMethod.OPTIONS,
_ => HttpMethod.GET
};
}
private static ImmutableArray<ParameterInfo> ExtractParameters(PatternMatch match, EntryPointPattern pattern)
{
var parameters = new List<ParameterInfo>();
// Extract path parameters from HTTP metadata
if (pattern.EntryType == EntryPointType.HttpEndpoint)
{
var pathMatch = Regex.Match(match.MatchedText, @"['""]([^'""]+)['""]");
if (pathMatch.Success)
{
var path = pathMatch.Groups[1].Value;
var pathParams = Regex.Matches(path, @":(\w+)|{(\w+)}");
foreach (Match pm in pathParams)
{
var name = pm.Groups[1].Success ? pm.Groups[1].Value : pm.Groups[2].Value;
parameters.Add(new ParameterInfo
{
Name = name,
Source = ParameterSource.Path,
Required = true,
Tainted = true
});
}
}
}
return parameters.ToImmutableArray();
}
private static IEnumerable<string> GetFileExtensions(EntryTraceLanguage language)
{
return language switch
{
EntryTraceLanguage.Java => new[] { ".java" },
EntryTraceLanguage.Python => new[] { ".py" },
EntryTraceLanguage.JavaScript => new[] { ".js", ".mjs", ".cjs" },
EntryTraceLanguage.TypeScript => new[] { ".ts", ".tsx", ".mts", ".cts" },
EntryTraceLanguage.Go => new[] { ".go" },
EntryTraceLanguage.Ruby => new[] { ".rb" },
EntryTraceLanguage.Php => new[] { ".php" },
EntryTraceLanguage.CSharp => new[] { ".cs" },
EntryTraceLanguage.Rust => new[] { ".rs" },
_ => Array.Empty<string>()
};
}
private static IReadOnlyList<Regex> BuildExcludePatterns(ExclusionConfig exclusions)
{
var patterns = new List<Regex>();
foreach (var glob in exclusions.ExcludePaths)
{
try
{
// Convert glob to regex
var pattern = "^" + Regex.Escape(glob)
.Replace(@"\*\*", ".*")
.Replace(@"\*", "[^/\\\\]*")
.Replace(@"\?", ".") + "$";
patterns.Add(new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase));
}
catch
{
// Skip invalid patterns
}
}
return patterns;
}
private static bool ShouldExclude(string filePath, IReadOnlyList<Regex> excludePatterns, ExclusionConfig config)
{
var fileName = Path.GetFileName(filePath);
var normalizedPath = filePath.Replace('\\', '/');
// Check test file exclusion
if (config.ExcludeTestFiles)
{
if (Regex.IsMatch(fileName, @"[._-]?(test|spec|tests|specs)[._-]?", RegexOptions.IgnoreCase) ||
normalizedPath.Contains("/test/", StringComparison.OrdinalIgnoreCase) ||
normalizedPath.Contains("/tests/", StringComparison.OrdinalIgnoreCase) ||
normalizedPath.Contains("/__tests__/", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
// Check generated file exclusion
if (config.ExcludeGenerated)
{
if (normalizedPath.Contains("/generated/", StringComparison.OrdinalIgnoreCase) ||
normalizedPath.Contains("/gen/", StringComparison.OrdinalIgnoreCase) ||
fileName.EndsWith(".generated.cs", StringComparison.OrdinalIgnoreCase) ||
fileName.EndsWith(".g.cs", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
// Check glob patterns
foreach (var pattern in excludePatterns)
{
if (pattern.IsMatch(normalizedPath))
{
return true;
}
}
return false;
}
private static async IAsyncEnumerable<string> EnumerateFilesAsync(
string rootPath,
IEnumerable<string> extensions,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var extensionSet = extensions.ToHashSet(StringComparer.OrdinalIgnoreCase);
IEnumerable<string> files;
try
{
files = Directory.EnumerateFiles(rootPath, "*", SearchOption.AllDirectories);
}
catch (Exception)
{
yield break;
}
foreach (var file in files)
{
cancellationToken.ThrowIfCancellationRequested();
var ext = Path.GetExtension(file);
if (extensionSet.Contains(ext))
{
yield return file;
}
}
await Task.CompletedTask;
}
private static async Task<int> CountFilesAsync(BaselineAnalysisContext context, CancellationToken cancellationToken)
{
var extensions = GetFileExtensions(context.Config.Language);
var count = 0;
await foreach (var _ in EnumerateFilesAsync(context.RootPath, extensions, cancellationToken))
{
count++;
}
return count;
}
private static BaselineStatistics ComputeStatistics(
List<DetectedEntryPoint> entryPoints,
int filesAnalyzed,
int filesSkipped)
{
var byType = entryPoints
.GroupBy(e => e.Type)
.ToImmutableDictionary(g => g.Key, g => g.Count());
var byFramework = entryPoints
.Where(e => e.Framework is not null)
.GroupBy(e => e.Framework!)
.ToImmutableDictionary(g => g.Key, g => g.Count());
var highConfidence = entryPoints.Count(e => e.Confidence >= 0.8);
var mediumConfidence = entryPoints.Count(e => e.Confidence >= 0.5 && e.Confidence < 0.8);
var lowConfidence = entryPoints.Count(e => e.Confidence < 0.5);
var reachableVulns = entryPoints
.SelectMany(e => e.ReachableVulnerabilities)
.Distinct()
.Count();
return new BaselineStatistics
{
TotalEntryPoints = entryPoints.Count,
ByType = byType,
ByFramework = byFramework,
HighConfidenceCount = highConfidence,
MediumConfidenceCount = mediumConfidence,
LowConfidenceCount = lowConfidence,
FilesAnalyzed = filesAnalyzed,
FilesSkipped = filesSkipped,
ReachableVulnerabilities = reachableVulns
};
}
private sealed record PatternMatch
{
public required string FilePath { get; init; }
public required int Line { get; init; }
public required int Column { get; init; }
public required string MatchedText { get; init; }
public string? FunctionName { get; init; }
public required EntryPointPattern Pattern { get; init; }
public required double Confidence { get; init; }
public ImmutableDictionary<string, string> Groups { get; init; } =
ImmutableDictionary<string, string>.Empty;
}
}

View File

@@ -0,0 +1,540 @@
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.EntryTrace.Baseline;
/// <summary>
/// Configuration for entry trace baseline analysis.
/// </summary>
/// <remarks>
/// Implements SCANNER-ENTRYTRACE-18-508: EntryTrace baseline schema per
/// docs/schemas/scanner-entrytrace-baseline.schema.json
/// </remarks>
public sealed record EntryTraceBaselineConfig
{
/// <summary>Unique configuration identifier.</summary>
public required string ConfigId { get; init; }
/// <summary>Target language for this configuration.</summary>
public required EntryTraceLanguage Language { get; init; }
/// <summary>Configuration version.</summary>
public string? Version { get; init; }
/// <summary>Entry point detection patterns.</summary>
public ImmutableArray<EntryPointPattern> EntryPointPatterns { get; init; } = ImmutableArray<EntryPointPattern>.Empty;
/// <summary>Framework-specific configurations.</summary>
public ImmutableArray<FrameworkConfig> FrameworkConfigs { get; init; } = ImmutableArray<FrameworkConfig>.Empty;
/// <summary>Heuristics configuration.</summary>
public HeuristicsConfig Heuristics { get; init; } = HeuristicsConfig.Default;
/// <summary>Exclusion rules.</summary>
public ExclusionConfig Exclusions { get; init; } = ExclusionConfig.Default;
}
/// <summary>
/// Supported languages for entry trace analysis.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum EntryTraceLanguage
{
Java,
Python,
JavaScript,
TypeScript,
Go,
Ruby,
Php,
CSharp,
Rust
}
/// <summary>
/// Types of entry points that can be detected.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum EntryPointType
{
/// <summary>HTTP/REST endpoint.</summary>
HttpEndpoint,
/// <summary>gRPC method.</summary>
GrpcMethod,
/// <summary>CLI command handler.</summary>
CliCommand,
/// <summary>Event handler (Kafka, RabbitMQ, etc.).</summary>
EventHandler,
/// <summary>Scheduled job (cron, timer).</summary>
ScheduledJob,
/// <summary>Message queue consumer.</summary>
MessageConsumer,
/// <summary>Test method (for test coverage).</summary>
TestMethod
}
/// <summary>
/// Pattern types for detecting entry points.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum PatternType
{
/// <summary>Annotation/attribute match (e.g., @GetMapping).</summary>
Annotation,
/// <summary>Decorator match (e.g., @app.route).</summary>
Decorator,
/// <summary>Function name pattern.</summary>
FunctionName,
/// <summary>Class name pattern.</summary>
ClassName,
/// <summary>File path pattern.</summary>
FilePattern,
/// <summary>Import statement pattern.</summary>
ImportPattern,
/// <summary>AST pattern for complex matching.</summary>
AstPattern
}
/// <summary>
/// Pattern for detecting entry points.
/// </summary>
public sealed record EntryPointPattern
{
/// <summary>Unique pattern identifier.</summary>
public required string PatternId { get; init; }
/// <summary>Type of pattern matching to use.</summary>
public required PatternType Type { get; init; }
/// <summary>Regex or AST pattern string.</summary>
public required string Pattern { get; init; }
/// <summary>Confidence level for matches (0.0-1.0).</summary>
public double Confidence { get; init; } = 0.7;
/// <summary>Type of entry point this pattern detects.</summary>
public EntryPointType EntryType { get; init; } = EntryPointType.HttpEndpoint;
/// <summary>Associated framework name.</summary>
public string? Framework { get; init; }
/// <summary>Rules for extracting metadata from matches.</summary>
public MetadataExtractionRules? MetadataExtraction { get; init; }
}
/// <summary>
/// Rules for extracting metadata from entry point matches.
/// </summary>
public sealed record MetadataExtractionRules
{
/// <summary>Expression to extract HTTP method.</summary>
public string? HttpMethod { get; init; }
/// <summary>Expression to extract route path.</summary>
public string? RoutePath { get; init; }
/// <summary>Expression to extract parameters.</summary>
public string? Parameters { get; init; }
/// <summary>Expression to detect auth requirements.</summary>
public string? AuthRequired { get; init; }
}
/// <summary>
/// Framework-specific configuration.
/// </summary>
public sealed record FrameworkConfig
{
/// <summary>Unique framework identifier.</summary>
public required string FrameworkId { get; init; }
/// <summary>Display name.</summary>
public required string Name { get; init; }
/// <summary>Supported version range (semver).</summary>
public string? VersionRange { get; init; }
/// <summary>Patterns to detect framework usage.</summary>
public ImmutableArray<string> DetectionPatterns { get; init; } = ImmutableArray<string>.Empty;
/// <summary>Entry point pattern IDs applicable to this framework.</summary>
public ImmutableArray<string> EntryPatterns { get; init; } = ImmutableArray<string>.Empty;
/// <summary>Glob patterns for router/route files.</summary>
public ImmutableArray<string> RouterFilePatterns { get; init; } = ImmutableArray<string>.Empty;
/// <summary>Patterns to identify controller classes.</summary>
public ImmutableArray<string> ControllerPatterns { get; init; } = ImmutableArray<string>.Empty;
}
/// <summary>
/// Heuristics configuration for entry point detection.
/// </summary>
public sealed record HeuristicsConfig
{
/// <summary>Enable static code analysis.</summary>
public bool EnableStaticAnalysis { get; init; } = true;
/// <summary>Use runtime hints if available.</summary>
public bool EnableDynamicHints { get; init; } = false;
/// <summary>Minimum confidence to report entry point.</summary>
public double ConfidenceThreshold { get; init; } = 0.7;
/// <summary>Maximum call graph depth to analyze.</summary>
public int MaxDepth { get; init; } = 10;
/// <summary>Analysis timeout per file in seconds.</summary>
public int TimeoutSeconds { get; init; } = 300;
/// <summary>Scoring weights for confidence calculation.</summary>
public ScoringWeights Weights { get; init; } = ScoringWeights.Default;
public static HeuristicsConfig Default => new();
}
/// <summary>
/// Weights for confidence scoring.
/// </summary>
public sealed record ScoringWeights
{
/// <summary>Weight for annotation/decorator matches.</summary>
public double AnnotationMatch { get; init; } = 0.9;
/// <summary>Weight for naming convention matches.</summary>
public double NamingConvention { get; init; } = 0.6;
/// <summary>Weight for file location patterns.</summary>
public double FileLocation { get; init; } = 0.5;
/// <summary>Weight for import analysis.</summary>
public double ImportAnalysis { get; init; } = 0.7;
/// <summary>Weight for call graph centrality.</summary>
public double CallGraphCentrality { get; init; } = 0.4;
public static ScoringWeights Default => new();
}
/// <summary>
/// Exclusion rules for analysis.
/// </summary>
public sealed record ExclusionConfig
{
/// <summary>Glob patterns for paths to exclude.</summary>
public ImmutableArray<string> ExcludePaths { get; init; } = ImmutableArray<string>.Empty;
/// <summary>Package names to exclude.</summary>
public ImmutableArray<string> ExcludePackages { get; init; } = ImmutableArray<string>.Empty;
/// <summary>Exclude test files from analysis.</summary>
public bool ExcludeTestFiles { get; init; } = true;
/// <summary>Exclude generated files from analysis.</summary>
public bool ExcludeGenerated { get; init; } = true;
public static ExclusionConfig Default => new();
}
/// <summary>
/// Source code location.
/// </summary>
public sealed record CodeLocation
{
/// <summary>File path relative to scan root.</summary>
public required string FilePath { get; init; }
/// <summary>Starting line number (1-indexed).</summary>
public int LineStart { get; init; }
/// <summary>Ending line number.</summary>
public int LineEnd { get; init; }
/// <summary>Starting column.</summary>
public int ColumnStart { get; init; }
/// <summary>Ending column.</summary>
public int ColumnEnd { get; init; }
/// <summary>Containing function name.</summary>
public string? FunctionName { get; init; }
/// <summary>Containing class name.</summary>
public string? ClassName { get; init; }
/// <summary>Package/namespace name.</summary>
public string? PackageName { get; init; }
}
/// <summary>
/// HTTP endpoint metadata.
/// </summary>
public sealed record HttpMetadata
{
/// <summary>HTTP method.</summary>
public HttpMethod Method { get; init; } = HttpMethod.GET;
/// <summary>Route path.</summary>
public required string Path { get; init; }
/// <summary>Path parameters.</summary>
public ImmutableArray<string> PathParameters { get; init; } = ImmutableArray<string>.Empty;
/// <summary>Query parameters.</summary>
public ImmutableArray<string> QueryParameters { get; init; } = ImmutableArray<string>.Empty;
/// <summary>Consumed content types.</summary>
public ImmutableArray<string> Consumes { get; init; } = ImmutableArray<string>.Empty;
/// <summary>Produced content types.</summary>
public ImmutableArray<string> Produces { get; init; } = ImmutableArray<string>.Empty;
/// <summary>Whether authentication is required.</summary>
public bool AuthRequired { get; init; }
/// <summary>Required auth scopes.</summary>
public ImmutableArray<string> AuthScopes { get; init; } = ImmutableArray<string>.Empty;
}
/// <summary>
/// HTTP methods.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum HttpMethod
{
GET,
POST,
PUT,
PATCH,
DELETE,
HEAD,
OPTIONS
}
/// <summary>
/// Parameter source types.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ParameterSource
{
Path,
Query,
Header,
Body,
Form,
Cookie
}
/// <summary>
/// Entry point parameter information.
/// </summary>
public sealed record ParameterInfo
{
/// <summary>Parameter name.</summary>
public required string Name { get; init; }
/// <summary>Parameter type.</summary>
public string? Type { get; init; }
/// <summary>Source of the parameter value.</summary>
public ParameterSource Source { get; init; } = ParameterSource.Query;
/// <summary>Whether the parameter is required.</summary>
public bool Required { get; init; }
/// <summary>Whether this is a potential taint source.</summary>
public bool Tainted { get; init; }
}
/// <summary>
/// Call type in call graph.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CallType
{
Direct,
Virtual,
Interface,
Reflection,
Lambda
}
/// <summary>
/// Individual call site in a call path.
/// </summary>
public sealed record CallSite
{
/// <summary>Caller function/method.</summary>
public required string Caller { get; init; }
/// <summary>Callee function/method.</summary>
public required string Callee { get; init; }
/// <summary>Source location.</summary>
public CodeLocation? Location { get; init; }
/// <summary>Type of call.</summary>
public CallType CallType { get; init; } = CallType.Direct;
}
/// <summary>
/// Call path from entry point to vulnerability.
/// </summary>
public sealed record CallPath
{
/// <summary>Target CVE or vulnerability identifier.</summary>
public required string TargetVulnerability { get; init; }
/// <summary>Number of calls in the path.</summary>
public int PathLength { get; init; }
/// <summary>Call sites along the path.</summary>
public ImmutableArray<CallSite> Calls { get; init; } = ImmutableArray<CallSite>.Empty;
/// <summary>Confidence in the path (0.0-1.0).</summary>
public double Confidence { get; init; }
}
/// <summary>
/// Detected entry point.
/// </summary>
public sealed record DetectedEntryPoint
{
/// <summary>Unique entry point identifier (deterministic).</summary>
public required string EntryId { get; init; }
/// <summary>Type of entry point.</summary>
public required EntryPointType Type { get; init; }
/// <summary>Entry point name (function/method name).</summary>
public required string Name { get; init; }
/// <summary>Source code location.</summary>
public required CodeLocation Location { get; init; }
/// <summary>Detection confidence (0.0-1.0).</summary>
public double Confidence { get; init; }
/// <summary>Detected framework.</summary>
public string? Framework { get; init; }
/// <summary>HTTP-specific metadata (if applicable).</summary>
public HttpMetadata? HttpMetadata { get; init; }
/// <summary>Parameters of the entry point.</summary>
public ImmutableArray<ParameterInfo> Parameters { get; init; } = ImmutableArray<ParameterInfo>.Empty;
/// <summary>CVE IDs reachable from this entry point.</summary>
public ImmutableArray<string> ReachableVulnerabilities { get; init; } = ImmutableArray<string>.Empty;
/// <summary>Call paths to vulnerabilities.</summary>
public ImmutableArray<CallPath> CallPaths { get; init; } = ImmutableArray<CallPath>.Empty;
/// <summary>Pattern ID that detected this entry point.</summary>
public string? DetectionMethod { get; init; }
/// <summary>
/// Generates a deterministic entry ID.
/// </summary>
public static string GenerateEntryId(string filePath, string name, int line, EntryPointType type)
{
var input = $"{filePath}|{name}|{line}|{type}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"ep:{Convert.ToHexString(hash).ToLowerInvariant()[..16]}";
}
}
/// <summary>
/// Baseline analysis statistics.
/// </summary>
public sealed record BaselineStatistics
{
/// <summary>Total number of entry points detected.</summary>
public int TotalEntryPoints { get; init; }
/// <summary>Entry points by type.</summary>
public ImmutableDictionary<EntryPointType, int> ByType { get; init; } =
ImmutableDictionary<EntryPointType, int>.Empty;
/// <summary>Entry points by framework.</summary>
public ImmutableDictionary<string, int> ByFramework { get; init; } =
ImmutableDictionary<string, int>.Empty;
/// <summary>Entry points by confidence level.</summary>
public int HighConfidenceCount { get; init; }
public int MediumConfidenceCount { get; init; }
public int LowConfidenceCount { get; init; }
/// <summary>Number of files analyzed.</summary>
public int FilesAnalyzed { get; init; }
/// <summary>Number of files skipped.</summary>
public int FilesSkipped { get; init; }
/// <summary>Number of reachable vulnerabilities.</summary>
public int ReachableVulnerabilities { get; init; }
}
/// <summary>
/// Entry trace baseline analysis report.
/// </summary>
public sealed record BaselineReport
{
/// <summary>Unique report identifier.</summary>
public required Guid ReportId { get; init; }
/// <summary>Scan identifier.</summary>
public required string ScanId { get; init; }
/// <summary>Report generation timestamp (UTC ISO-8601).</summary>
public required DateTimeOffset GeneratedAt { get; init; }
/// <summary>Configuration ID used for analysis.</summary>
public string? ConfigUsed { get; init; }
/// <summary>Detected entry points.</summary>
public ImmutableArray<DetectedEntryPoint> EntryPoints { get; init; } =
ImmutableArray<DetectedEntryPoint>.Empty;
/// <summary>Analysis statistics.</summary>
public BaselineStatistics Statistics { get; init; } = new();
/// <summary>Detected frameworks.</summary>
public ImmutableArray<string> FrameworksDetected { get; init; } =
ImmutableArray<string>.Empty;
/// <summary>Analysis duration in milliseconds.</summary>
public long AnalysisDurationMs { get; init; }
/// <summary>Report digest (sha256:...).</summary>
public required string Digest { get; init; }
/// <summary>
/// Computes the digest for this report.
/// </summary>
public static string ComputeDigest(IEnumerable<DetectedEntryPoint> entryPoints)
{
var sb = new StringBuilder();
foreach (var ep in entryPoints.OrderBy(e => e.EntryId))
{
sb.Append(ep.EntryId);
sb.Append('|');
}
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sb.ToString()));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
}

View File

@@ -0,0 +1,112 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace StellaOps.Scanner.EntryTrace.Baseline;
/// <summary>
/// Extension methods for registering baseline analysis services.
/// </summary>
public static class BaselineServiceCollectionExtensions
{
/// <summary>
/// Adds baseline entry point analysis services to the service collection.
/// </summary>
public static IServiceCollection AddEntryTraceBaseline(this IServiceCollection services)
{
services.TryAddSingleton<IBaselineAnalyzer, BaselineAnalyzer>();
services.TryAddSingleton<IBaselineConfigProvider, DefaultBaselineConfigProvider>();
return services;
}
/// <summary>
/// Adds baseline entry point analysis with custom configurations.
/// </summary>
public static IServiceCollection AddEntryTraceBaseline(
this IServiceCollection services,
Action<BaselineAnalyzerOptions> configure)
{
services.Configure(configure);
services.TryAddSingleton<IBaselineAnalyzer, BaselineAnalyzer>();
services.TryAddSingleton<IBaselineConfigProvider, DefaultBaselineConfigProvider>();
return services;
}
}
/// <summary>
/// Options for baseline analyzer.
/// </summary>
public sealed class BaselineAnalyzerOptions
{
/// <summary>
/// Additional custom configurations to register.
/// </summary>
public List<EntryTraceBaselineConfig> CustomConfigurations { get; } = new();
/// <summary>
/// Whether to include default configurations.
/// </summary>
public bool IncludeDefaults { get; set; } = true;
/// <summary>
/// Global confidence threshold override.
/// </summary>
public double? GlobalConfidenceThreshold { get; set; }
/// <summary>
/// Global timeout in seconds.
/// </summary>
public int? GlobalTimeoutSeconds { get; set; }
}
/// <summary>
/// Provides baseline configurations.
/// </summary>
public interface IBaselineConfigProvider
{
/// <summary>
/// Gets configuration for the specified language.
/// </summary>
EntryTraceBaselineConfig? GetConfiguration(EntryTraceLanguage language);
/// <summary>
/// Gets configuration by ID.
/// </summary>
EntryTraceBaselineConfig? GetConfiguration(string configId);
/// <summary>
/// Gets all available configurations.
/// </summary>
IReadOnlyList<EntryTraceBaselineConfig> GetAllConfigurations();
}
/// <summary>
/// Default baseline configuration provider.
/// </summary>
public sealed class DefaultBaselineConfigProvider : IBaselineConfigProvider
{
private readonly Dictionary<string, EntryTraceBaselineConfig> _configsById;
private readonly Dictionary<EntryTraceLanguage, EntryTraceBaselineConfig> _configsByLanguage;
public DefaultBaselineConfigProvider()
{
var configs = DefaultConfigurations.All;
_configsById = configs.ToDictionary(c => c.ConfigId, StringComparer.OrdinalIgnoreCase);
_configsByLanguage = configs.ToDictionary(c => c.Language);
}
public EntryTraceBaselineConfig? GetConfiguration(EntryTraceLanguage language)
{
return _configsByLanguage.TryGetValue(language, out var config) ? config : null;
}
public EntryTraceBaselineConfig? GetConfiguration(string configId)
{
return _configsById.TryGetValue(configId, out var config) ? config : null;
}
public IReadOnlyList<EntryTraceBaselineConfig> GetAllConfigurations()
{
return _configsById.Values.ToList();
}
}

View File

@@ -0,0 +1,630 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.EntryTrace.Baseline;
/// <summary>
/// Provides default baseline configurations for common languages and frameworks.
/// </summary>
/// <remarks>
/// Implements SCANNER-ENTRYTRACE-18-508: Default entry point detection patterns.
/// </remarks>
public static class DefaultConfigurations
{
/// <summary>
/// Gets all default configurations.
/// </summary>
public static IReadOnlyList<EntryTraceBaselineConfig> All => new[]
{
JavaSpring,
PythonFlaskDjango,
NodeExpress,
TypeScriptNestJs,
DotNetAspNetCore,
GoGin
};
/// <summary>
/// Java Spring Boot configuration.
/// </summary>
public static EntryTraceBaselineConfig JavaSpring => new()
{
ConfigId = "java-spring-baseline",
Language = EntryTraceLanguage.Java,
Version = "1.0.0",
EntryPointPatterns = ImmutableArray.Create(
new EntryPointPattern
{
PatternId = "spring-get-mapping",
Type = PatternType.Annotation,
Pattern = @"@GetMapping\s*\(\s*[""']?(?<path>[^""'\)]+)[""']?\s*\)",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "spring"
},
new EntryPointPattern
{
PatternId = "spring-post-mapping",
Type = PatternType.Annotation,
Pattern = @"@PostMapping\s*\(\s*[""']?(?<path>[^""'\)]+)[""']?\s*\)",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "spring"
},
new EntryPointPattern
{
PatternId = "spring-put-mapping",
Type = PatternType.Annotation,
Pattern = @"@PutMapping\s*\(\s*[""']?(?<path>[^""'\)]+)[""']?\s*\)",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "spring"
},
new EntryPointPattern
{
PatternId = "spring-delete-mapping",
Type = PatternType.Annotation,
Pattern = @"@DeleteMapping\s*\(\s*[""']?(?<path>[^""'\)]+)[""']?\s*\)",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "spring"
},
new EntryPointPattern
{
PatternId = "spring-request-mapping",
Type = PatternType.Annotation,
Pattern = @"@RequestMapping\s*\([^)]*value\s*=\s*[""'](?<path>[^""']+)[""']",
Confidence = 0.9,
EntryType = EntryPointType.HttpEndpoint,
Framework = "spring"
},
new EntryPointPattern
{
PatternId = "spring-scheduled",
Type = PatternType.Annotation,
Pattern = @"@Scheduled\s*\(",
Confidence = 0.95,
EntryType = EntryPointType.ScheduledJob,
Framework = "spring"
},
new EntryPointPattern
{
PatternId = "spring-kafka-listener",
Type = PatternType.Annotation,
Pattern = @"@KafkaListener\s*\(",
Confidence = 0.95,
EntryType = EntryPointType.MessageConsumer,
Framework = "spring"
},
new EntryPointPattern
{
PatternId = "spring-grpc-service",
Type = PatternType.Annotation,
Pattern = @"@GrpcService",
Confidence = 0.9,
EntryType = EntryPointType.GrpcMethod,
Framework = "spring"
}
),
FrameworkConfigs = ImmutableArray.Create(
new FrameworkConfig
{
FrameworkId = "spring-boot",
Name = "Spring Boot",
VersionRange = ">=2.0.0",
DetectionPatterns = ImmutableArray.Create(
"org.springframework.boot",
"@SpringBootApplication",
"spring-boot-starter"
),
EntryPatterns = ImmutableArray.Create(
"spring-get-mapping",
"spring-post-mapping",
"spring-put-mapping",
"spring-delete-mapping",
"spring-request-mapping",
"spring-scheduled"
),
RouterFilePatterns = ImmutableArray.Create(
"**/controller/**/*.java",
"**/rest/**/*.java",
"**/api/**/*.java"
),
ControllerPatterns = ImmutableArray.Create(
".*Controller$",
".*Resource$"
)
}
),
Exclusions = new ExclusionConfig
{
ExcludePaths = ImmutableArray.Create("**/test/**", "**/generated/**"),
ExcludePackages = ImmutableArray.Create("org.springframework.test"),
ExcludeTestFiles = true,
ExcludeGenerated = true
}
};
/// <summary>
/// Python Flask/Django configuration.
/// </summary>
public static EntryTraceBaselineConfig PythonFlaskDjango => new()
{
ConfigId = "python-web-baseline",
Language = EntryTraceLanguage.Python,
Version = "1.0.0",
EntryPointPatterns = ImmutableArray.Create(
new EntryPointPattern
{
PatternId = "flask-route",
Type = PatternType.Decorator,
Pattern = @"@(?:app|blueprint|bp)\.route\s*\(\s*[""'](?<path>[^""']+)[""']",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "flask"
},
new EntryPointPattern
{
PatternId = "flask-get",
Type = PatternType.Decorator,
Pattern = @"@(?:app|blueprint|bp)\.get\s*\(\s*[""'](?<path>[^""']+)[""']",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "flask"
},
new EntryPointPattern
{
PatternId = "flask-post",
Type = PatternType.Decorator,
Pattern = @"@(?:app|blueprint|bp)\.post\s*\(\s*[""'](?<path>[^""']+)[""']",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "flask"
},
new EntryPointPattern
{
PatternId = "django-path",
Type = PatternType.FunctionName,
Pattern = @"path\s*\(\s*[""'](?<path>[^""']+)[""']\s*,",
Confidence = 0.85,
EntryType = EntryPointType.HttpEndpoint,
Framework = "django"
},
new EntryPointPattern
{
PatternId = "fastapi-route",
Type = PatternType.Decorator,
Pattern = @"@(?:app|router)\.(?<method>get|post|put|delete|patch)\s*\(\s*[""'](?<path>[^""']+)[""']",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "fastapi"
},
new EntryPointPattern
{
PatternId = "celery-task",
Type = PatternType.Decorator,
Pattern = @"@(?:celery\.)?task\s*\(",
Confidence = 0.9,
EntryType = EntryPointType.ScheduledJob,
Framework = "celery"
}
),
FrameworkConfigs = ImmutableArray.Create(
new FrameworkConfig
{
FrameworkId = "flask",
Name = "Flask",
DetectionPatterns = ImmutableArray.Create("from flask import", "Flask(__name__)"),
EntryPatterns = ImmutableArray.Create("flask-route", "flask-get", "flask-post"),
RouterFilePatterns = ImmutableArray.Create("**/routes.py", "**/views.py", "**/api/**/*.py")
},
new FrameworkConfig
{
FrameworkId = "django",
Name = "Django",
DetectionPatterns = ImmutableArray.Create("from django", "django.conf.urls"),
EntryPatterns = ImmutableArray.Create("django-path"),
RouterFilePatterns = ImmutableArray.Create("**/urls.py", "**/views.py")
},
new FrameworkConfig
{
FrameworkId = "fastapi",
Name = "FastAPI",
DetectionPatterns = ImmutableArray.Create("from fastapi import", "FastAPI()"),
EntryPatterns = ImmutableArray.Create("fastapi-route"),
RouterFilePatterns = ImmutableArray.Create("**/routers/**/*.py", "**/api/**/*.py")
}
),
Exclusions = new ExclusionConfig
{
ExcludePaths = ImmutableArray.Create("**/test*/**", "**/migrations/**"),
ExcludeTestFiles = true,
ExcludeGenerated = true
}
};
/// <summary>
/// Node.js Express configuration.
/// </summary>
public static EntryTraceBaselineConfig NodeExpress => new()
{
ConfigId = "node-express-baseline",
Language = EntryTraceLanguage.JavaScript,
Version = "1.0.0",
EntryPointPatterns = ImmutableArray.Create(
new EntryPointPattern
{
PatternId = "express-get",
Type = PatternType.FunctionName,
Pattern = @"(?:app|router)\.get\s*\(\s*['""](?<path>[^'""]+)['""]",
Confidence = 0.9,
EntryType = EntryPointType.HttpEndpoint,
Framework = "express"
},
new EntryPointPattern
{
PatternId = "express-post",
Type = PatternType.FunctionName,
Pattern = @"(?:app|router)\.post\s*\(\s*['""](?<path>[^'""]+)['""]",
Confidence = 0.9,
EntryType = EntryPointType.HttpEndpoint,
Framework = "express"
},
new EntryPointPattern
{
PatternId = "express-put",
Type = PatternType.FunctionName,
Pattern = @"(?:app|router)\.put\s*\(\s*['""](?<path>[^'""]+)['""]",
Confidence = 0.9,
EntryType = EntryPointType.HttpEndpoint,
Framework = "express"
},
new EntryPointPattern
{
PatternId = "express-delete",
Type = PatternType.FunctionName,
Pattern = @"(?:app|router)\.delete\s*\(\s*['""](?<path>[^'""]+)['""]",
Confidence = 0.9,
EntryType = EntryPointType.HttpEndpoint,
Framework = "express"
},
new EntryPointPattern
{
PatternId = "fastify-route",
Type = PatternType.FunctionName,
Pattern = @"fastify\.(?<method>get|post|put|delete|patch)\s*\(\s*['""](?<path>[^'""]+)['""]",
Confidence = 0.9,
EntryType = EntryPointType.HttpEndpoint,
Framework = "fastify"
},
new EntryPointPattern
{
PatternId = "koa-router",
Type = PatternType.FunctionName,
Pattern = @"router\.(?<method>get|post|put|delete|patch)\s*\(\s*['""](?<path>[^'""]+)['""]",
Confidence = 0.85,
EntryType = EntryPointType.HttpEndpoint,
Framework = "koa"
}
),
FrameworkConfigs = ImmutableArray.Create(
new FrameworkConfig
{
FrameworkId = "express",
Name = "Express.js",
DetectionPatterns = ImmutableArray.Create("require('express')", "from 'express'", "express()"),
EntryPatterns = ImmutableArray.Create("express-get", "express-post", "express-put", "express-delete"),
RouterFilePatterns = ImmutableArray.Create("**/routes/**/*.js", "**/api/**/*.js", "**/controllers/**/*.js")
},
new FrameworkConfig
{
FrameworkId = "fastify",
Name = "Fastify",
DetectionPatterns = ImmutableArray.Create("require('fastify')", "from 'fastify'"),
EntryPatterns = ImmutableArray.Create("fastify-route")
}
),
Exclusions = new ExclusionConfig
{
ExcludePaths = ImmutableArray.Create("**/node_modules/**", "**/dist/**", "**/build/**"),
ExcludeTestFiles = true,
ExcludeGenerated = true
}
};
/// <summary>
/// TypeScript NestJS configuration.
/// </summary>
public static EntryTraceBaselineConfig TypeScriptNestJs => new()
{
ConfigId = "typescript-nestjs-baseline",
Language = EntryTraceLanguage.TypeScript,
Version = "1.0.0",
EntryPointPatterns = ImmutableArray.Create(
new EntryPointPattern
{
PatternId = "nestjs-get",
Type = PatternType.Decorator,
Pattern = @"@Get\s*\(\s*['""]?(?<path>[^'"")\s]*)['""]?\s*\)",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "nestjs"
},
new EntryPointPattern
{
PatternId = "nestjs-post",
Type = PatternType.Decorator,
Pattern = @"@Post\s*\(\s*['""]?(?<path>[^'"")\s]*)['""]?\s*\)",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "nestjs"
},
new EntryPointPattern
{
PatternId = "nestjs-put",
Type = PatternType.Decorator,
Pattern = @"@Put\s*\(\s*['""]?(?<path>[^'"")\s]*)['""]?\s*\)",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "nestjs"
},
new EntryPointPattern
{
PatternId = "nestjs-delete",
Type = PatternType.Decorator,
Pattern = @"@Delete\s*\(\s*['""]?(?<path>[^'"")\s]*)['""]?\s*\)",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "nestjs"
},
new EntryPointPattern
{
PatternId = "nestjs-message-pattern",
Type = PatternType.Decorator,
Pattern = @"@MessagePattern\s*\(",
Confidence = 0.9,
EntryType = EntryPointType.MessageConsumer,
Framework = "nestjs"
},
new EntryPointPattern
{
PatternId = "nestjs-event-pattern",
Type = PatternType.Decorator,
Pattern = @"@EventPattern\s*\(",
Confidence = 0.9,
EntryType = EntryPointType.EventHandler,
Framework = "nestjs"
},
new EntryPointPattern
{
PatternId = "nestjs-grpc-method",
Type = PatternType.Decorator,
Pattern = @"@GrpcMethod\s*\(",
Confidence = 0.95,
EntryType = EntryPointType.GrpcMethod,
Framework = "nestjs"
}
),
FrameworkConfigs = ImmutableArray.Create(
new FrameworkConfig
{
FrameworkId = "nestjs",
Name = "NestJS",
DetectionPatterns = ImmutableArray.Create("@nestjs/common", "@Controller", "@Injectable"),
EntryPatterns = ImmutableArray.Create(
"nestjs-get", "nestjs-post", "nestjs-put", "nestjs-delete",
"nestjs-message-pattern", "nestjs-event-pattern", "nestjs-grpc-method"
),
RouterFilePatterns = ImmutableArray.Create("**/*.controller.ts"),
ControllerPatterns = ImmutableArray.Create(".*Controller$")
}
),
Exclusions = new ExclusionConfig
{
ExcludePaths = ImmutableArray.Create("**/node_modules/**", "**/dist/**"),
ExcludeTestFiles = true,
ExcludeGenerated = true
}
};
/// <summary>
/// .NET ASP.NET Core configuration.
/// </summary>
public static EntryTraceBaselineConfig DotNetAspNetCore => new()
{
ConfigId = "dotnet-aspnet-baseline",
Language = EntryTraceLanguage.CSharp,
Version = "1.0.0",
EntryPointPatterns = ImmutableArray.Create(
new EntryPointPattern
{
PatternId = "aspnet-httpget",
Type = PatternType.Annotation,
Pattern = @"\[HttpGet\s*\(\s*[""']?(?<path>[^""'\]]*)[""']?\s*\)\]",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "aspnet"
},
new EntryPointPattern
{
PatternId = "aspnet-httppost",
Type = PatternType.Annotation,
Pattern = @"\[HttpPost\s*\(\s*[""']?(?<path>[^""'\]]*)[""']?\s*\)\]",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "aspnet"
},
new EntryPointPattern
{
PatternId = "aspnet-httpput",
Type = PatternType.Annotation,
Pattern = @"\[HttpPut\s*\(\s*[""']?(?<path>[^""'\]]*)[""']?\s*\)\]",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "aspnet"
},
new EntryPointPattern
{
PatternId = "aspnet-httpdelete",
Type = PatternType.Annotation,
Pattern = @"\[HttpDelete\s*\(\s*[""']?(?<path>[^""'\]]*)[""']?\s*\)\]",
Confidence = 0.95,
EntryType = EntryPointType.HttpEndpoint,
Framework = "aspnet"
},
new EntryPointPattern
{
PatternId = "aspnet-route",
Type = PatternType.Annotation,
Pattern = @"\[Route\s*\(\s*[""'](?<path>[^""']+)[""']\s*\)\]",
Confidence = 0.85,
EntryType = EntryPointType.HttpEndpoint,
Framework = "aspnet"
},
new EntryPointPattern
{
PatternId = "aspnet-minimal-map",
Type = PatternType.FunctionName,
Pattern = @"(?:app|endpoints)\.Map(?<method>Get|Post|Put|Delete|Patch)\s*\(\s*[""'](?<path>[^""']+)[""']",
Confidence = 0.9,
EntryType = EntryPointType.HttpEndpoint,
Framework = "aspnet-minimal"
},
new EntryPointPattern
{
PatternId = "grpc-service",
Type = PatternType.ClassName,
Pattern = @"class\s+\w+\s*:\s*\w+\.(\w+)Base\b",
Confidence = 0.85,
EntryType = EntryPointType.GrpcMethod,
Framework = "grpc"
}
),
FrameworkConfigs = ImmutableArray.Create(
new FrameworkConfig
{
FrameworkId = "aspnet",
Name = "ASP.NET Core",
DetectionPatterns = ImmutableArray.Create(
"Microsoft.AspNetCore",
"ControllerBase",
"[ApiController]"
),
EntryPatterns = ImmutableArray.Create(
"aspnet-httpget", "aspnet-httppost", "aspnet-httpput",
"aspnet-httpdelete", "aspnet-route", "aspnet-minimal-map"
),
RouterFilePatterns = ImmutableArray.Create("**/*Controller.cs", "**/Controllers/**/*.cs"),
ControllerPatterns = ImmutableArray.Create(".*Controller$")
}
),
Exclusions = new ExclusionConfig
{
ExcludePaths = ImmutableArray.Create("**/bin/**", "**/obj/**", "**/Migrations/**"),
ExcludeTestFiles = true,
ExcludeGenerated = true
}
};
/// <summary>
/// Go Gin/Echo configuration.
/// </summary>
public static EntryTraceBaselineConfig GoGin => new()
{
ConfigId = "go-web-baseline",
Language = EntryTraceLanguage.Go,
Version = "1.0.0",
EntryPointPatterns = ImmutableArray.Create(
new EntryPointPattern
{
PatternId = "gin-route",
Type = PatternType.FunctionName,
Pattern = @"(?:r|router|g|group)\.(?<method>GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*\(\s*[""'](?<path>[^""']+)[""']",
Confidence = 0.9,
EntryType = EntryPointType.HttpEndpoint,
Framework = "gin"
},
new EntryPointPattern
{
PatternId = "echo-route",
Type = PatternType.FunctionName,
Pattern = @"e\.(?<method>GET|POST|PUT|DELETE|PATCH)\s*\(\s*[""'](?<path>[^""']+)[""']",
Confidence = 0.9,
EntryType = EntryPointType.HttpEndpoint,
Framework = "echo"
},
new EntryPointPattern
{
PatternId = "chi-route",
Type = PatternType.FunctionName,
Pattern = @"r\.(?<method>Get|Post|Put|Delete|Patch)\s*\(\s*[""'](?<path>[^""']+)[""']",
Confidence = 0.9,
EntryType = EntryPointType.HttpEndpoint,
Framework = "chi"
},
new EntryPointPattern
{
PatternId = "http-handle",
Type = PatternType.FunctionName,
Pattern = @"http\.Handle(?:Func)?\s*\(\s*[""'](?<path>[^""']+)[""']",
Confidence = 0.8,
EntryType = EntryPointType.HttpEndpoint,
Framework = "net/http"
},
new EntryPointPattern
{
PatternId = "grpc-register",
Type = PatternType.FunctionName,
Pattern = @"Register\w+Server\s*\(",
Confidence = 0.85,
EntryType = EntryPointType.GrpcMethod,
Framework = "grpc"
}
),
FrameworkConfigs = ImmutableArray.Create(
new FrameworkConfig
{
FrameworkId = "gin",
Name = "Gin",
DetectionPatterns = ImmutableArray.Create("github.com/gin-gonic/gin", "gin.Default()", "gin.New()"),
EntryPatterns = ImmutableArray.Create("gin-route")
},
new FrameworkConfig
{
FrameworkId = "echo",
Name = "Echo",
DetectionPatterns = ImmutableArray.Create("github.com/labstack/echo", "echo.New()"),
EntryPatterns = ImmutableArray.Create("echo-route")
},
new FrameworkConfig
{
FrameworkId = "chi",
Name = "Chi",
DetectionPatterns = ImmutableArray.Create("github.com/go-chi/chi"),
EntryPatterns = ImmutableArray.Create("chi-route")
}
),
Exclusions = new ExclusionConfig
{
ExcludePaths = ImmutableArray.Create("**/vendor/**", "**/testdata/**"),
ExcludeTestFiles = true,
ExcludeGenerated = true
}
};
/// <summary>
/// Gets configuration for a specific language.
/// </summary>
public static EntryTraceBaselineConfig? GetForLanguage(EntryTraceLanguage language)
{
return language switch
{
EntryTraceLanguage.Java => JavaSpring,
EntryTraceLanguage.Python => PythonFlaskDjango,
EntryTraceLanguage.JavaScript => NodeExpress,
EntryTraceLanguage.TypeScript => TypeScriptNestJs,
EntryTraceLanguage.CSharp => DotNetAspNetCore,
EntryTraceLanguage.Go => GoGin,
_ => null
};
}
}