Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View File

@@ -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,
}

View File

@@ -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,

View File

@@ -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>