Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.Reachability/Surfaces/SurfaceAwareReachabilityAnalyzer.cs
2026-02-01 21:37:40 +02:00

475 lines
15 KiB
C#

// -----------------------------------------------------------------------------
// 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;
/// <summary>
/// Reachability analyzer that integrates with vulnerability surfaces
/// for precise trigger-based sink resolution.
/// </summary>
public sealed class SurfaceAwareReachabilityAnalyzer : ISurfaceAwareReachabilityAnalyzer
{
private readonly ISurfaceQueryService _surfaceQuery;
private readonly IReachabilityGraphService _graphService;
private readonly ILogger<SurfaceAwareReachabilityAnalyzer> _logger;
public SurfaceAwareReachabilityAnalyzer(
ISurfaceQueryService surfaceQuery,
IReachabilityGraphService graphService,
ILogger<SurfaceAwareReachabilityAnalyzer> logger)
{
_surfaceQuery = surfaceQuery ?? throw new ArgumentNullException(nameof(surfaceQuery));
_graphService = graphService ?? throw new ArgumentNullException(nameof(graphService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task<SurfaceAwareReachabilityResult> AnalyzeAsync(
SurfaceAwareReachabilityRequest request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
var sw = Stopwatch.StartNew();
var findings = new List<SurfaceReachabilityFinding>();
// 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<SurfaceReachabilityFinding> AnalyzeVulnerabilityAsync(
VulnerabilityInfo vuln,
SurfaceQueryResult surface,
ICallGraphAccessor? callGraph,
CancellationToken cancellationToken)
{
// Determine sink source and resolve sinks
IReadOnlyList<string> 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<IReadOnlyList<string>> 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 = []
};
}
}
/// <summary>
/// Interface for surface-aware reachability analysis.
/// </summary>
public interface ISurfaceAwareReachabilityAnalyzer
{
/// <summary>
/// Analyzes reachability for vulnerabilities using surface data.
/// </summary>
Task<SurfaceAwareReachabilityResult> AnalyzeAsync(
SurfaceAwareReachabilityRequest request,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Request for surface-aware reachability analysis.
/// </summary>
public sealed record SurfaceAwareReachabilityRequest
{
/// <summary>
/// Vulnerabilities to analyze.
/// </summary>
public required IReadOnlyList<VulnerabilityInfo> Vulnerabilities { get; init; }
/// <summary>
/// Call graph accessor for the analyzed codebase.
/// </summary>
public ICallGraphAccessor? CallGraph { get; init; }
/// <summary>
/// Maximum depth for path finding.
/// </summary>
public int MaxPathDepth { get; init; } = 20;
}
/// <summary>
/// Result of surface-aware reachability analysis.
/// </summary>
public sealed record SurfaceAwareReachabilityResult
{
/// <summary>
/// Individual findings for each vulnerability.
/// </summary>
public IReadOnlyList<SurfaceReachabilityFinding> Findings { get; init; } = [];
/// <summary>
/// Total vulnerabilities analyzed.
/// </summary>
public int TotalVulnerabilities { get; init; }
/// <summary>
/// Count of confirmed reachable vulnerabilities.
/// </summary>
public int ConfirmedReachable { get; init; }
/// <summary>
/// Count of likely reachable vulnerabilities.
/// </summary>
public int LikelyReachable { get; init; }
/// <summary>
/// Count of unreachable vulnerabilities.
/// </summary>
public int Unreachable { get; init; }
/// <summary>
/// Analysis duration.
/// </summary>
public TimeSpan AnalysisDuration { get; init; }
}
/// <summary>
/// Reachability finding for a single vulnerability.
/// </summary>
public sealed record SurfaceReachabilityFinding
{
/// <summary>
/// CVE identifier.
/// </summary>
public required string CveId { get; init; }
/// <summary>
/// Package name.
/// </summary>
public required string PackageName { get; init; }
/// <summary>
/// Package version.
/// </summary>
public required string Version { get; init; }
/// <summary>
/// Confidence tier for this finding.
/// </summary>
public ReachabilityConfidenceTier ConfidenceTier { get; init; }
/// <summary>
/// Source of sink methods used.
/// </summary>
public SinkSource SinkSource { get; init; }
/// <summary>
/// Surface ID if available.
/// </summary>
public Guid? SurfaceId { get; init; }
/// <summary>
/// Human-readable message.
/// </summary>
public required string Message { get; init; }
/// <summary>
/// Trigger methods that are reachable.
/// </summary>
public IReadOnlyList<string> ReachableTriggers { get; init; } = [];
/// <summary>
/// Path witnesses from entrypoints to triggers.
/// </summary>
public IReadOnlyList<PathWitness> Witnesses { get; init; } = [];
}
/// <summary>
/// Vulnerability information for analysis.
/// </summary>
public sealed record VulnerabilityInfo
{
/// <summary>
/// CVE identifier.
/// </summary>
public required string CveId { get; init; }
/// <summary>
/// Package ecosystem.
/// </summary>
public required string Ecosystem { get; init; }
/// <summary>
/// Package name.
/// </summary>
public required string PackageName { get; init; }
/// <summary>
/// Package version.
/// </summary>
public required string Version { get; init; }
}
/// <summary>
/// Path witness from entrypoint to sink.
/// </summary>
public sealed record PathWitness
{
/// <summary>
/// Entrypoint method key.
/// </summary>
public required string EntrypointMethodKey { get; init; }
/// <summary>
/// Sink (trigger) method key.
/// </summary>
public required string SinkMethodKey { get; init; }
/// <summary>
/// Number of hops in path.
/// </summary>
public int PathLength { get; init; }
/// <summary>
/// Ordered method keys in path.
/// </summary>
public IReadOnlyList<string> PathMethodKeys { get; init; } = [];
}
/// <summary>
/// Interface for call graph access.
/// </summary>
public interface ICallGraphAccessor
{
/// <summary>
/// Gets entrypoint method keys.
/// </summary>
IReadOnlyList<string> GetEntrypoints();
/// <summary>
/// Gets callees of a method.
/// </summary>
IReadOnlyList<string> GetCallees(string methodKey);
/// <summary>
/// Checks if a method exists.
/// </summary>
bool ContainsMethod(string methodKey);
}
/// <summary>
/// Interface for reachability graph operations.
/// </summary>
public interface IReachabilityGraphService
{
/// <summary>
/// Finds paths from entrypoints to any of the specified sinks.
/// </summary>
Task<IReadOnlyList<ReachablePath>> FindPathsToSinksAsync(
ICallGraphAccessor callGraph,
IReadOnlyList<string> sinkMethodKeys,
CancellationToken cancellationToken = default);
}
/// <summary>
/// A reachable path from entrypoint to sink.
/// </summary>
public sealed record ReachablePath
{
/// <summary>
/// Entrypoint method key.
/// </summary>
public required string EntrypointMethodKey { get; init; }
/// <summary>
/// Sink method key.
/// </summary>
public required string SinkMethodKey { get; init; }
/// <summary>
/// Path length.
/// </summary>
public int PathLength { get; init; }
/// <summary>
/// Ordered method keys in path.
/// </summary>
public IReadOnlyList<string> PathMethodKeys { get; init; } = [];
}