// ----------------------------------------------------------------------------- // SurfaceAwareReachabilityAnalyzer.cs // Sprint: SPRINT_3700_0004_0001_reachability_integration (REACH-005, REACH-006, REACH-009) // Description: Reachability analyzer that uses vulnerability surfaces for precise sink resolution. // ----------------------------------------------------------------------------- using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace StellaOps.Scanner.Reachability.Surfaces; /// /// Reachability analyzer that integrates with vulnerability surfaces /// for precise trigger-based sink resolution. /// public sealed class SurfaceAwareReachabilityAnalyzer : ISurfaceAwareReachabilityAnalyzer { private readonly ISurfaceQueryService _surfaceQuery; private readonly IReachabilityGraphService _graphService; private readonly ILogger _logger; public SurfaceAwareReachabilityAnalyzer( ISurfaceQueryService surfaceQuery, IReachabilityGraphService graphService, ILogger logger) { _surfaceQuery = surfaceQuery ?? throw new ArgumentNullException(nameof(surfaceQuery)); _graphService = graphService ?? throw new ArgumentNullException(nameof(graphService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// public async Task AnalyzeAsync( SurfaceAwareReachabilityRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); var sw = Stopwatch.StartNew(); var findings = new List(); // Query surfaces for all vulnerabilities var surfaceRequests = request.Vulnerabilities .Select(v => new SurfaceQueryRequest { CveId = v.CveId, Ecosystem = v.Ecosystem, PackageName = v.PackageName, Version = v.Version, IncludePaths = true }) .ToList(); var surfaceResults = await _surfaceQuery.QueryBulkAsync(surfaceRequests, cancellationToken); foreach (var vuln in request.Vulnerabilities) { cancellationToken.ThrowIfCancellationRequested(); var queryKey = $"{vuln.CveId}|{vuln.Ecosystem}|{vuln.PackageName}|{vuln.Version}"; if (!surfaceResults.TryGetValue(queryKey, out var surface)) { // No surface result - should not happen but handle gracefully findings.Add(CreateUnknownFinding(vuln, "No surface query result")); continue; } var finding = await AnalyzeVulnerabilityAsync(vuln, surface, request.CallGraph, cancellationToken); findings.Add(finding); } sw.Stop(); // Compute summary statistics var confirmedCount = findings.Count(f => f.ConfidenceTier == ReachabilityConfidenceTier.Confirmed); var likelyCount = findings.Count(f => f.ConfidenceTier == ReachabilityConfidenceTier.Likely); var unreachableCount = findings.Count(f => f.ConfidenceTier == ReachabilityConfidenceTier.Unreachable); _logger.LogInformation( "Surface-aware reachability analysis complete: {Total} vulns, {Confirmed} confirmed, {Likely} likely, {Unreachable} unreachable in {Duration}ms", findings.Count, confirmedCount, likelyCount, unreachableCount, sw.ElapsedMilliseconds); return new SurfaceAwareReachabilityResult { Findings = findings, TotalVulnerabilities = findings.Count, ConfirmedReachable = confirmedCount, LikelyReachable = likelyCount, Unreachable = unreachableCount, AnalysisDuration = sw.Elapsed }; } private async Task AnalyzeVulnerabilityAsync( VulnerabilityInfo vuln, SurfaceQueryResult surface, ICallGraphAccessor? callGraph, CancellationToken cancellationToken) { // Determine sink source and resolve sinks IReadOnlyList sinks; SinkSource sinkSource; if (surface.SurfaceFound && surface.Triggers.Count > 0) { // Use trigger methods as sinks (highest precision) sinks = surface.Triggers.Select(t => t.MethodKey).ToList(); sinkSource = SinkSource.Surface; _logger.LogDebug( "{CveId}/{PackageName}: Using {TriggerCount} trigger methods from surface", vuln.CveId, vuln.PackageName, sinks.Count); } else if (surface.Source == SinkSource.PackageApi) { // Fallback to package API methods sinks = await ResolvePackageApiMethodsAsync(vuln, cancellationToken); sinkSource = SinkSource.PackageApi; _logger.LogDebug( "{CveId}/{PackageName}: Using {SinkCount} package API methods as fallback", vuln.CveId, vuln.PackageName, sinks.Count); } else { // Ultimate fallback - no sink resolution possible return CreatePresentFinding(vuln, surface); } // If no call graph, we can't determine reachability if (callGraph is null) { return CreatePresentFinding(vuln, surface); } // Perform reachability analysis from entrypoints to sinks var reachablePaths = await _graphService.FindPathsToSinksAsync( callGraph, sinks, cancellationToken); if (reachablePaths.Count == 0) { // No paths found - unreachable return new SurfaceReachabilityFinding { CveId = vuln.CveId, PackageName = vuln.PackageName, Version = vuln.Version, ConfidenceTier = ReachabilityConfidenceTier.Unreachable, SinkSource = sinkSource, SurfaceId = surface.SurfaceId, Message = "No execution path to vulnerable code found", ReachableTriggers = [], Witnesses = [] }; } // Paths found - determine confidence tier var tier = sinkSource == SinkSource.Surface ? ReachabilityConfidenceTier.Confirmed : ReachabilityConfidenceTier.Likely; var reachableTriggers = reachablePaths .Select(p => p.SinkMethodKey) .Distinct() .ToList(); return new SurfaceReachabilityFinding { CveId = vuln.CveId, PackageName = vuln.PackageName, Version = vuln.Version, ConfidenceTier = tier, SinkSource = sinkSource, SurfaceId = surface.SurfaceId, Message = $"{tier.GetDescription()}: {reachablePaths.Count} paths to {reachableTriggers.Count} triggers", ReachableTriggers = reachableTriggers, Witnesses = reachablePaths.Select(p => new PathWitness { EntrypointMethodKey = p.EntrypointMethodKey, SinkMethodKey = p.SinkMethodKey, PathLength = p.PathLength, PathMethodKeys = p.PathMethodKeys }).ToList() }; } private async Task> ResolvePackageApiMethodsAsync( VulnerabilityInfo vuln, CancellationToken cancellationToken) { // TODO: Implement package API method resolution // This would query the package's public API methods as fallback sinks await Task.CompletedTask; return []; } private static SurfaceReachabilityFinding CreatePresentFinding( VulnerabilityInfo vuln, SurfaceQueryResult surface) { return new SurfaceReachabilityFinding { CveId = vuln.CveId, PackageName = vuln.PackageName, Version = vuln.Version, ConfidenceTier = ReachabilityConfidenceTier.Present, SinkSource = surface.Source, SurfaceId = surface.SurfaceId, Message = "Package present; reachability undetermined", ReachableTriggers = [], Witnesses = [] }; } private static SurfaceReachabilityFinding CreateUnknownFinding( VulnerabilityInfo vuln, string reason) { return new SurfaceReachabilityFinding { CveId = vuln.CveId, PackageName = vuln.PackageName, Version = vuln.Version, ConfidenceTier = ReachabilityConfidenceTier.Unknown, SinkSource = SinkSource.FallbackAll, Message = reason, ReachableTriggers = [], Witnesses = [] }; } } /// /// Interface for surface-aware reachability analysis. /// public interface ISurfaceAwareReachabilityAnalyzer { /// /// Analyzes reachability for vulnerabilities using surface data. /// Task AnalyzeAsync( SurfaceAwareReachabilityRequest request, CancellationToken cancellationToken = default); } /// /// Request for surface-aware reachability analysis. /// public sealed record SurfaceAwareReachabilityRequest { /// /// Vulnerabilities to analyze. /// public required IReadOnlyList Vulnerabilities { get; init; } /// /// Call graph accessor for the analyzed codebase. /// public ICallGraphAccessor? CallGraph { get; init; } /// /// Maximum depth for path finding. /// public int MaxPathDepth { get; init; } = 20; } /// /// Result of surface-aware reachability analysis. /// public sealed record SurfaceAwareReachabilityResult { /// /// Individual findings for each vulnerability. /// public IReadOnlyList Findings { get; init; } = []; /// /// Total vulnerabilities analyzed. /// public int TotalVulnerabilities { get; init; } /// /// Count of confirmed reachable vulnerabilities. /// public int ConfirmedReachable { get; init; } /// /// Count of likely reachable vulnerabilities. /// public int LikelyReachable { get; init; } /// /// Count of unreachable vulnerabilities. /// public int Unreachable { get; init; } /// /// Analysis duration. /// public TimeSpan AnalysisDuration { get; init; } } /// /// Reachability finding for a single vulnerability. /// public sealed record SurfaceReachabilityFinding { /// /// CVE identifier. /// public required string CveId { get; init; } /// /// Package name. /// public required string PackageName { get; init; } /// /// Package version. /// public required string Version { get; init; } /// /// Confidence tier for this finding. /// public ReachabilityConfidenceTier ConfidenceTier { get; init; } /// /// Source of sink methods used. /// public SinkSource SinkSource { get; init; } /// /// Surface ID if available. /// public Guid? SurfaceId { get; init; } /// /// Human-readable message. /// public required string Message { get; init; } /// /// Trigger methods that are reachable. /// public IReadOnlyList ReachableTriggers { get; init; } = []; /// /// Path witnesses from entrypoints to triggers. /// public IReadOnlyList Witnesses { get; init; } = []; } /// /// Vulnerability information for analysis. /// public sealed record VulnerabilityInfo { /// /// CVE identifier. /// public required string CveId { get; init; } /// /// Package ecosystem. /// public required string Ecosystem { get; init; } /// /// Package name. /// public required string PackageName { get; init; } /// /// Package version. /// public required string Version { get; init; } } /// /// Path witness from entrypoint to sink. /// public sealed record PathWitness { /// /// Entrypoint method key. /// public required string EntrypointMethodKey { get; init; } /// /// Sink (trigger) method key. /// public required string SinkMethodKey { get; init; } /// /// Number of hops in path. /// public int PathLength { get; init; } /// /// Ordered method keys in path. /// public IReadOnlyList PathMethodKeys { get; init; } = []; } /// /// Interface for call graph access. /// public interface ICallGraphAccessor { /// /// Gets entrypoint method keys. /// IReadOnlyList GetEntrypoints(); /// /// Gets callees of a method. /// IReadOnlyList GetCallees(string methodKey); /// /// Checks if a method exists. /// bool ContainsMethod(string methodKey); } /// /// Interface for reachability graph operations. /// public interface IReachabilityGraphService { /// /// Finds paths from entrypoints to any of the specified sinks. /// Task> FindPathsToSinksAsync( ICallGraphAccessor callGraph, IReadOnlyList sinkMethodKeys, CancellationToken cancellationToken = default); } /// /// A reachable path from entrypoint to sink. /// public sealed record ReachablePath { /// /// Entrypoint method key. /// public required string EntrypointMethodKey { get; init; } /// /// Sink method key. /// public required string SinkMethodKey { get; init; } /// /// Path length. /// public int PathLength { get; init; } /// /// Ordered method keys in path. /// public IReadOnlyList PathMethodKeys { get; init; } = []; }