// -----------------------------------------------------------------------------
// 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; } = [];
}