Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -34,6 +34,138 @@ public interface IReachabilityFactsSignalsClient
|
||||
Task<bool> TriggerRecomputeAsync(
|
||||
SignalsRecomputeRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a reachability fact with its associated subgraph slice.
|
||||
/// Fetches from Signals for the fact and ReachGraph Store for the subgraph.
|
||||
/// </summary>
|
||||
/// <param name="subjectKey">Subject key (scan ID or component key).</param>
|
||||
/// <param name="cveId">Optional CVE ID to slice by.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The reachability fact with subgraph, or null if not found.</returns>
|
||||
Task<ReachabilityFactWithSubgraph?> GetWithSubgraphAsync(
|
||||
string subjectKey,
|
||||
string? cveId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response containing both the reachability fact and its subgraph slice.
|
||||
/// </summary>
|
||||
public sealed record ReachabilityFactWithSubgraph(
|
||||
SignalsReachabilityFactResponse Fact,
|
||||
ReachGraphSlice? Subgraph);
|
||||
|
||||
/// <summary>
|
||||
/// Represents a slice of the reachability graph for a specific query.
|
||||
/// </summary>
|
||||
public sealed record ReachGraphSlice
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema version.
|
||||
/// </summary>
|
||||
public string? SchemaVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Slice query information.
|
||||
/// </summary>
|
||||
public ReachGraphSliceQuery? SliceQuery { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Parent graph digest.
|
||||
/// </summary>
|
||||
public string? ParentDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// BLAKE3 digest of this slice.
|
||||
/// </summary>
|
||||
public string? Digest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Nodes in the slice.
|
||||
/// </summary>
|
||||
public List<ReachGraphSliceNode>? Nodes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Edges in the slice.
|
||||
/// </summary>
|
||||
public List<ReachGraphSliceEdge>? Edges { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of nodes.
|
||||
/// </summary>
|
||||
public int NodeCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of edges.
|
||||
/// </summary>
|
||||
public int EdgeCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Sink node IDs.
|
||||
/// </summary>
|
||||
public List<string>? Sinks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Paths from entrypoints to sinks.
|
||||
/// </summary>
|
||||
public List<ReachGraphPath>? Paths { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slice query information.
|
||||
/// </summary>
|
||||
public sealed record ReachGraphSliceQuery
|
||||
{
|
||||
public string? Type { get; init; }
|
||||
public string? Query { get; init; }
|
||||
public string? Cve { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Node in a reachability graph slice.
|
||||
/// </summary>
|
||||
public sealed record ReachGraphSliceNode
|
||||
{
|
||||
public string? Id { get; init; }
|
||||
public string? Kind { get; init; }
|
||||
public string? Ref { get; init; }
|
||||
public string? File { get; init; }
|
||||
public int? Line { get; init; }
|
||||
public bool IsEntrypoint { get; init; }
|
||||
public bool IsSink { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Edge in a reachability graph slice.
|
||||
/// </summary>
|
||||
public sealed record ReachGraphSliceEdge
|
||||
{
|
||||
public string? From { get; init; }
|
||||
public string? To { get; init; }
|
||||
public ReachGraphEdgeExplanation? Why { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Edge explanation in a reachability graph.
|
||||
/// </summary>
|
||||
public sealed record ReachGraphEdgeExplanation
|
||||
{
|
||||
public string? Type { get; init; }
|
||||
public string? Loc { get; init; }
|
||||
public string? Guard { get; init; }
|
||||
public double Confidence { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path from entrypoint to sink.
|
||||
/// </summary>
|
||||
public sealed record ReachGraphPath
|
||||
{
|
||||
public string? Entrypoint { get; init; }
|
||||
public string? Sink { get; init; }
|
||||
public List<string>? Hops { get; init; }
|
||||
public List<ReachGraphSliceEdge>? Edges { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -190,6 +190,83 @@ public sealed class ReachabilityFactsSignalsClient : IReachabilityFactsSignalsCl
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ReachabilityFactWithSubgraph?> GetWithSubgraphAsync(
|
||||
string subjectKey,
|
||||
string? cveId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(subjectKey);
|
||||
|
||||
using var activity = PolicyEngineTelemetry.ActivitySource.StartActivity(
|
||||
"signals_client.get_fact_with_subgraph",
|
||||
ActivityKind.Client);
|
||||
activity?.SetTag("signals.subject_key", subjectKey);
|
||||
if (cveId is not null)
|
||||
{
|
||||
activity?.SetTag("signals.cve_id", cveId);
|
||||
}
|
||||
|
||||
// Get base reachability fact from Signals
|
||||
var fact = await GetBySubjectAsync(subjectKey, cancellationToken).ConfigureAwait(false);
|
||||
if (fact is null)
|
||||
{
|
||||
_logger.LogDebug("No reachability fact found for subject {SubjectKey}", subjectKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(fact.CallgraphId))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Reachability fact for subject {SubjectKey} has no callgraph ID",
|
||||
subjectKey);
|
||||
return new ReachabilityFactWithSubgraph(fact, null);
|
||||
}
|
||||
|
||||
// Fetch subgraph slice from ReachGraph Store
|
||||
var sliceQuery = cveId is not null
|
||||
? $"?cve={Uri.EscapeDataString(cveId)}"
|
||||
: "";
|
||||
|
||||
try
|
||||
{
|
||||
var slicePath = _options.ReachGraphStoreBaseUri is not null
|
||||
? $"v1/reachgraphs/{Uri.EscapeDataString(fact.CallgraphId)}/slice{sliceQuery}"
|
||||
: $"reachgraph/v1/reachgraphs/{Uri.EscapeDataString(fact.CallgraphId)}/slice{sliceQuery}";
|
||||
|
||||
var response = await _httpClient.GetAsync(slicePath, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Failed to fetch subgraph slice for callgraph {CallgraphId}: {StatusCode}",
|
||||
fact.CallgraphId,
|
||||
response.StatusCode);
|
||||
return new ReachabilityFactWithSubgraph(fact, null);
|
||||
}
|
||||
|
||||
var slice = await response.Content
|
||||
.ReadFromJsonAsync<ReachGraphSlice>(SerializerOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Fetched subgraph slice for callgraph {CallgraphId}: {NodeCount} nodes, {PathCount} paths",
|
||||
fact.CallgraphId,
|
||||
slice?.NodeCount ?? 0,
|
||||
slice?.Paths?.Count ?? 0);
|
||||
|
||||
return new ReachabilityFactWithSubgraph(fact, slice);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Error fetching subgraph slice for callgraph {CallgraphId}",
|
||||
fact.CallgraphId);
|
||||
return new ReachabilityFactWithSubgraph(fact, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -207,6 +284,12 @@ public sealed class ReachabilityFactsSignalsClientOptions
|
||||
/// </summary>
|
||||
public Uri? BaseUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Base URI for the ReachGraph Store service.
|
||||
/// If null, uses the same base URI as Signals.
|
||||
/// </summary>
|
||||
public Uri? ReachGraphStoreBaseUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum concurrent requests for batch operations.
|
||||
/// Default: 10.
|
||||
|
||||
Reference in New Issue
Block a user