475 lines
15 KiB
C#
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; } = [];
|
|
}
|