Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
// <copyright file="EbpfSignalMerger.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Runtime;
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Reachability.Slices;
|
||||
using StellaOps.Signals.Ebpf.Schema;
|
||||
|
||||
/// <summary>
|
||||
/// Merges eBPF runtime signals with static reachability analysis.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This extends the existing RuntimeStaticMerger with specific support for
|
||||
/// eBPF-collected call paths from the Signals module.
|
||||
/// </remarks>
|
||||
public sealed class EbpfSignalMerger
|
||||
{
|
||||
private readonly RuntimeStaticMerger _baseMerger;
|
||||
private readonly ILogger<EbpfSignalMerger> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public EbpfSignalMerger(
|
||||
RuntimeStaticMerger baseMerger,
|
||||
ILogger<EbpfSignalMerger> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_baseMerger = baseMerger;
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges eBPF signal summary with static reachability graph.
|
||||
/// </summary>
|
||||
/// <param name="staticGraph">Static call graph from analysis.</param>
|
||||
/// <param name="runtimeSignals">Runtime signals from eBPF collection.</param>
|
||||
/// <param name="options">Merge options.</param>
|
||||
/// <returns>Merged graph with runtime evidence annotations.</returns>
|
||||
public EbpfMergeResult Merge(
|
||||
RichGraph staticGraph,
|
||||
RuntimeSignalSummary? runtimeSignals,
|
||||
EbpfMergeOptions? options = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(staticGraph);
|
||||
|
||||
options ??= new EbpfMergeOptions();
|
||||
|
||||
if (runtimeSignals is null || runtimeSignals.TotalEvents == 0)
|
||||
{
|
||||
_logger.LogDebug("No runtime signals to merge");
|
||||
return new EbpfMergeResult
|
||||
{
|
||||
MergedGraph = staticGraph,
|
||||
Evidence = ImmutableArray<RuntimeEvidence>.Empty,
|
||||
Statistics = new EbpfMergeStatistics
|
||||
{
|
||||
StaticEdgeCount = staticGraph.Edges.Count,
|
||||
RuntimeEventCount = 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Merging {EventCount} eBPF events, {CallPathCount} call paths with static graph ({EdgeCount} edges)",
|
||||
runtimeSignals.TotalEvents,
|
||||
runtimeSignals.CallPaths.Count,
|
||||
staticGraph.Edges.Count);
|
||||
|
||||
// Convert eBPF call paths to RuntimeCallEvents for base merger
|
||||
var runtimeEvents = ConvertToRuntimeEvents(runtimeSignals);
|
||||
|
||||
// Use base merger for graph merging
|
||||
var baseResult = _baseMerger.Merge(staticGraph, runtimeEvents);
|
||||
|
||||
// Build runtime evidence annotations
|
||||
var evidence = BuildRuntimeEvidence(runtimeSignals, baseResult, options);
|
||||
|
||||
// Calculate statistics
|
||||
var statistics = new EbpfMergeStatistics
|
||||
{
|
||||
StaticEdgeCount = staticGraph.Edges.Count,
|
||||
RuntimeEventCount = (int)runtimeSignals.TotalEvents,
|
||||
CallPathCount = runtimeSignals.CallPaths.Count,
|
||||
ConfirmedPathCount = baseResult.ObservedEdges.Length,
|
||||
RuntimeOnlyPathCount = baseResult.RuntimeOnlyEdges.Length,
|
||||
UnreachedStaticCount = baseResult.Statistics.UnmatchedStaticEdgeCount,
|
||||
DroppedEventCount = runtimeSignals.DroppedEvents,
|
||||
CoverageRatio = baseResult.Statistics.CoverageRatio,
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"eBPF merge complete: {Confirmed}/{Static} paths confirmed ({Coverage:P1}), {RuntimeOnly} runtime-only",
|
||||
statistics.ConfirmedPathCount,
|
||||
statistics.StaticEdgeCount,
|
||||
statistics.CoverageRatio,
|
||||
statistics.RuntimeOnlyPathCount);
|
||||
|
||||
return new EbpfMergeResult
|
||||
{
|
||||
MergedGraph = baseResult.MergedGraph,
|
||||
Evidence = evidence,
|
||||
Statistics = statistics,
|
||||
ObservedEdges = baseResult.ObservedEdges,
|
||||
RuntimeOnlyEdges = baseResult.RuntimeOnlyEdges,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a specific call path against the static graph.
|
||||
/// </summary>
|
||||
public PathValidationResult ValidatePath(
|
||||
RichGraph staticGraph,
|
||||
ObservedCallPath callPath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(staticGraph);
|
||||
ArgumentNullException.ThrowIfNull(callPath);
|
||||
|
||||
if (callPath.Symbols.Count < 2)
|
||||
{
|
||||
return new PathValidationResult
|
||||
{
|
||||
IsValid = false,
|
||||
Reason = "Call path must have at least 2 symbols",
|
||||
PathType = PathType.Invalid,
|
||||
};
|
||||
}
|
||||
|
||||
var edgeIndex = BuildEdgeIndex(staticGraph);
|
||||
var matchedEdges = 0;
|
||||
var missingEdges = new List<(string From, string To)>();
|
||||
|
||||
for (var i = 0; i < callPath.Symbols.Count - 1; i++)
|
||||
{
|
||||
var from = callPath.Symbols[i];
|
||||
var to = callPath.Symbols[i + 1];
|
||||
var key = $"{from}->{to}";
|
||||
|
||||
if (edgeIndex.Contains(key))
|
||||
{
|
||||
matchedEdges++;
|
||||
}
|
||||
else
|
||||
{
|
||||
missingEdges.Add((from, to));
|
||||
}
|
||||
}
|
||||
|
||||
var totalEdges = callPath.Symbols.Count - 1;
|
||||
|
||||
if (matchedEdges == totalEdges)
|
||||
{
|
||||
return new PathValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
PathType = PathType.Confirmed,
|
||||
MatchRatio = 1.0,
|
||||
MatchedEdgeCount = matchedEdges,
|
||||
};
|
||||
}
|
||||
else if (matchedEdges > 0)
|
||||
{
|
||||
return new PathValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
PathType = PathType.Partial,
|
||||
MatchRatio = (double)matchedEdges / totalEdges,
|
||||
MatchedEdgeCount = matchedEdges,
|
||||
MissingEdges = missingEdges,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new PathValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
PathType = PathType.RuntimeOnly,
|
||||
MatchRatio = 0.0,
|
||||
MissingEdges = missingEdges,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<RuntimeCallEvent> ConvertToRuntimeEvents(
|
||||
RuntimeSignalSummary signals)
|
||||
{
|
||||
foreach (var path in signals.CallPaths)
|
||||
{
|
||||
if (path.Symbols.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create events for each edge in the path
|
||||
for (var i = 0; i < path.Symbols.Count - 1; i++)
|
||||
{
|
||||
for (var count = 0; count < path.ObservationCount; count++)
|
||||
{
|
||||
yield return new RuntimeCallEvent
|
||||
{
|
||||
Timestamp = (ulong)path.FirstObservedAt.ToUnixTimeMilliseconds() * 1_000_000,
|
||||
Pid = 0,
|
||||
Tid = 0,
|
||||
CallerSymbol = path.Symbols[i],
|
||||
CalleeSymbol = path.Symbols[i + 1],
|
||||
BinaryPath = path.Purl ?? "unknown",
|
||||
TraceDigest = null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ImmutableArray<RuntimeEvidence> BuildRuntimeEvidence(
|
||||
RuntimeSignalSummary signals,
|
||||
RuntimeStaticMergeResult mergeResult,
|
||||
EbpfMergeOptions options)
|
||||
{
|
||||
var evidence = new List<RuntimeEvidence>();
|
||||
|
||||
// Add evidence for confirmed paths
|
||||
foreach (var observed in mergeResult.ObservedEdges)
|
||||
{
|
||||
evidence.Add(new RuntimeEvidence
|
||||
{
|
||||
Type = RuntimeEvidenceType.RuntimeConfirmed,
|
||||
SourceSymbol = observed.From,
|
||||
TargetSymbol = observed.To,
|
||||
Confidence = 1.0,
|
||||
ObservationCount = observed.ObservationCount,
|
||||
FirstObservedAt = observed.FirstObserved,
|
||||
LastObservedAt = observed.LastObserved,
|
||||
Source = EvidenceSource.Ebpf,
|
||||
ContainerId = signals.ContainerId,
|
||||
});
|
||||
}
|
||||
|
||||
// Add evidence for runtime-only paths
|
||||
foreach (var runtimeOnly in mergeResult.RuntimeOnlyEdges)
|
||||
{
|
||||
evidence.Add(new RuntimeEvidence
|
||||
{
|
||||
Type = RuntimeEvidenceType.RuntimeOnly,
|
||||
SourceSymbol = runtimeOnly.From,
|
||||
TargetSymbol = runtimeOnly.To,
|
||||
Confidence = options.RuntimeOnlyConfidence,
|
||||
ObservationCount = runtimeOnly.ObservationCount,
|
||||
FirstObservedAt = runtimeOnly.FirstObserved,
|
||||
LastObservedAt = runtimeOnly.LastObserved,
|
||||
Source = EvidenceSource.Ebpf,
|
||||
ContainerId = signals.ContainerId,
|
||||
});
|
||||
}
|
||||
|
||||
// Add evidence for detected runtimes
|
||||
foreach (var runtime in signals.DetectedRuntimes)
|
||||
{
|
||||
evidence.Add(new RuntimeEvidence
|
||||
{
|
||||
Type = RuntimeEvidenceType.RuntimeDetected,
|
||||
RuntimeType = runtime.ToString(),
|
||||
Confidence = 1.0,
|
||||
ObservationCount = 1,
|
||||
FirstObservedAt = signals.StartedAt,
|
||||
LastObservedAt = signals.StoppedAt,
|
||||
Source = EvidenceSource.Ebpf,
|
||||
ContainerId = signals.ContainerId,
|
||||
});
|
||||
}
|
||||
|
||||
return evidence.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static HashSet<string> BuildEdgeIndex(RichGraph graph)
|
||||
{
|
||||
var index = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var edge in graph.Edges)
|
||||
{
|
||||
index.Add($"{edge.From}->{edge.To}");
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for eBPF signal merging.
|
||||
/// </summary>
|
||||
public sealed record EbpfMergeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Confidence score for runtime-only edges.
|
||||
/// </summary>
|
||||
public double RuntimeOnlyConfidence { get; init; } = 0.9;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum observation count to include a path.
|
||||
/// </summary>
|
||||
public int MinObservationCount { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to add runtime-only edges to the graph.
|
||||
/// </summary>
|
||||
public bool AddRuntimeOnlyEdges { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum age of observations to consider.
|
||||
/// </summary>
|
||||
public TimeSpan FreshnessWindow { get; init; } = TimeSpan.FromDays(7);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of eBPF signal merging.
|
||||
/// </summary>
|
||||
public sealed record EbpfMergeResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Merged graph with runtime annotations.
|
||||
/// </summary>
|
||||
public required RichGraph MergedGraph { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime evidence items.
|
||||
/// </summary>
|
||||
public required ImmutableArray<RuntimeEvidence> Evidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Merge statistics.
|
||||
/// </summary>
|
||||
public required EbpfMergeStatistics Statistics { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Edges that were observed at runtime.
|
||||
/// </summary>
|
||||
public ImmutableArray<ObservedEdge> ObservedEdges { get; init; } = ImmutableArray<ObservedEdge>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Edges only found at runtime.
|
||||
/// </summary>
|
||||
public ImmutableArray<RuntimeOnlyEdge> RuntimeOnlyEdges { get; init; } = ImmutableArray<RuntimeOnlyEdge>.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Statistics from eBPF signal merging.
|
||||
/// </summary>
|
||||
public sealed record EbpfMergeStatistics
|
||||
{
|
||||
public int StaticEdgeCount { get; init; }
|
||||
public int RuntimeEventCount { get; init; }
|
||||
public int CallPathCount { get; init; }
|
||||
public int ConfirmedPathCount { get; init; }
|
||||
public int RuntimeOnlyPathCount { get; init; }
|
||||
public int UnreachedStaticCount { get; init; }
|
||||
public long DroppedEventCount { get; init; }
|
||||
public double CoverageRatio { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of path validation.
|
||||
/// </summary>
|
||||
public sealed record PathValidationResult
|
||||
{
|
||||
public required bool IsValid { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
public required PathType PathType { get; init; }
|
||||
public double MatchRatio { get; init; }
|
||||
public int MatchedEdgeCount { get; init; }
|
||||
public IReadOnlyList<(string From, string To)>? MissingEdges { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of call path.
|
||||
/// </summary>
|
||||
public enum PathType
|
||||
{
|
||||
/// <summary>Invalid path (too short, etc).</summary>
|
||||
Invalid,
|
||||
|
||||
/// <summary>All edges confirmed in static graph.</summary>
|
||||
Confirmed,
|
||||
|
||||
/// <summary>Some edges in static graph.</summary>
|
||||
Partial,
|
||||
|
||||
/// <summary>No edges in static graph (runtime-only).</summary>
|
||||
RuntimeOnly,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime evidence from eBPF signals.
|
||||
/// </summary>
|
||||
public sealed record RuntimeEvidence
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of evidence.
|
||||
/// </summary>
|
||||
public required RuntimeEvidenceType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source symbol (for edge evidence).
|
||||
/// </summary>
|
||||
public string? SourceSymbol { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Target symbol (for edge evidence).
|
||||
/// </summary>
|
||||
public string? TargetSymbol { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Runtime type (for runtime detection evidence).
|
||||
/// </summary>
|
||||
public string? RuntimeType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence score (0.0-1.0).
|
||||
/// </summary>
|
||||
public required double Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of observations.
|
||||
/// </summary>
|
||||
public required int ObservationCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// First observation timestamp.
|
||||
/// </summary>
|
||||
public required DateTimeOffset FirstObservedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Last observation timestamp.
|
||||
/// </summary>
|
||||
public required DateTimeOffset LastObservedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source of evidence.
|
||||
/// </summary>
|
||||
public required EvidenceSource Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Container ID where observed.
|
||||
/// </summary>
|
||||
public string? ContainerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// PURL of the package (if known).
|
||||
/// </summary>
|
||||
public string? Purl { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of runtime evidence.
|
||||
/// </summary>
|
||||
public enum RuntimeEvidenceType
|
||||
{
|
||||
/// <summary>Function call observed via eBPF.</summary>
|
||||
RuntimeObserved,
|
||||
|
||||
/// <summary>Static path confirmed by runtime.</summary>
|
||||
RuntimeConfirmed,
|
||||
|
||||
/// <summary>Path discovered only at runtime.</summary>
|
||||
RuntimeOnly,
|
||||
|
||||
/// <summary>Runtime type detected.</summary>
|
||||
RuntimeDetected,
|
||||
|
||||
/// <summary>Path marked as unreachable at runtime.</summary>
|
||||
RuntimeUnreached,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source of evidence.
|
||||
/// </summary>
|
||||
public enum EvidenceSource
|
||||
{
|
||||
/// <summary>Static analysis.</summary>
|
||||
Static,
|
||||
|
||||
/// <summary>eBPF runtime probes.</summary>
|
||||
Ebpf,
|
||||
|
||||
/// <summary>ETW on Windows.</summary>
|
||||
Etw,
|
||||
|
||||
/// <summary>DTrace on Solaris/BSD.</summary>
|
||||
DTrace,
|
||||
|
||||
/// <summary>Manual observation.</summary>
|
||||
Manual,
|
||||
}
|
||||
@@ -22,6 +22,10 @@ public enum SinkCategory
|
||||
[JsonStringEnumMemberName("SQL_RAW")]
|
||||
SqlRaw,
|
||||
|
||||
/// <summary>SQL injection (e.g., unparameterized queries with user input)</summary>
|
||||
[JsonStringEnumMemberName("SQL_INJECTION")]
|
||||
SqlInjection,
|
||||
|
||||
/// <summary>Server-side request forgery (e.g., HttpClient with user input)</summary>
|
||||
[JsonStringEnumMemberName("SSRF")]
|
||||
Ssrf,
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Npgsql" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj" />
|
||||
@@ -23,5 +23,6 @@
|
||||
<ProjectReference Include="..\..\..\Attestor\__Libraries\StellaOps.Attestor.GraphRoot\StellaOps.Attestor.GraphRoot.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\..\..\Signals\__Libraries\StellaOps.Signals.Ebpf\StellaOps.Signals.Ebpf.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user