Add Canonical JSON serialization library with tests and documentation
- Implemented CanonJson class for deterministic JSON serialization and hashing. - Added unit tests for CanonJson functionality, covering various scenarios including key sorting, handling of nested objects, arrays, and special characters. - Created project files for the Canonical JSON library and its tests, including necessary package references. - Added README.md for library usage and API reference. - Introduced RabbitMqIntegrationFactAttribute for conditional RabbitMQ integration tests.
This commit is contained in:
@@ -0,0 +1,473 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
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; } = [];
|
||||
}
|
||||
Reference in New Issue
Block a user