317 lines
10 KiB
C#
317 lines
10 KiB
C#
// <copyright file="EbpfSignalMergerTests.cs" company="StellaOps">
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
// </copyright>
|
|
|
|
namespace StellaOps.Signals.Ebpf.Tests;
|
|
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using StellaOps.Scanner.Reachability;
|
|
using StellaOps.Scanner.Reachability.Runtime;
|
|
using StellaOps.Scanner.Reachability.Slices;
|
|
using StellaOps.Signals.Ebpf.Schema;
|
|
using Xunit;
|
|
|
|
/// <summary>
|
|
/// Tests for <see cref="EbpfSignalMerger"/>.
|
|
/// </summary>
|
|
public sealed class EbpfSignalMergerTests
|
|
{
|
|
private readonly EbpfSignalMerger _merger;
|
|
private readonly RuntimeStaticMerger _baseMerger;
|
|
|
|
public EbpfSignalMergerTests()
|
|
{
|
|
_baseMerger = new RuntimeStaticMerger();
|
|
_merger = new EbpfSignalMerger(
|
|
_baseMerger,
|
|
NullLogger<EbpfSignalMerger>.Instance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Merge_WithNoSignals_ReturnsSameGraph()
|
|
{
|
|
var graph = CreateTestGraph();
|
|
|
|
var result = _merger.Merge(graph, null);
|
|
|
|
Assert.Same(graph, result.MergedGraph);
|
|
Assert.Empty(result.Evidence);
|
|
Assert.Equal(2, result.Statistics.StaticEdgeCount);
|
|
Assert.Equal(0, result.Statistics.RuntimeEventCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Merge_WithEmptySignals_ReturnsSameGraph()
|
|
{
|
|
var graph = CreateTestGraph();
|
|
var signals = new RuntimeSignalSummary
|
|
{
|
|
ContainerId = "container-123",
|
|
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
StoppedAt = DateTimeOffset.UtcNow,
|
|
TotalEvents = 0,
|
|
CallPaths = [],
|
|
ObservedSymbols = [],
|
|
};
|
|
|
|
var result = _merger.Merge(graph, signals);
|
|
|
|
Assert.Same(graph, result.MergedGraph);
|
|
Assert.Empty(result.Evidence);
|
|
}
|
|
|
|
[Fact]
|
|
public void Merge_WithMatchingSignals_CreatesConfirmedEvidence()
|
|
{
|
|
var graph = CreateTestGraph();
|
|
var signals = new RuntimeSignalSummary
|
|
{
|
|
ContainerId = "container-123",
|
|
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
StoppedAt = DateTimeOffset.UtcNow,
|
|
TotalEvents = 100,
|
|
CallPaths = new List<ObservedCallPath>
|
|
{
|
|
new()
|
|
{
|
|
Symbols = ["main", "processRequest"],
|
|
ObservationCount = 50,
|
|
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
LastObservedAt = DateTimeOffset.UtcNow,
|
|
},
|
|
},
|
|
ObservedSymbols = ["main", "processRequest"],
|
|
};
|
|
|
|
var result = _merger.Merge(graph, signals);
|
|
|
|
Assert.NotEmpty(result.Evidence);
|
|
Assert.Contains(result.Evidence, e =>
|
|
e.Type == RuntimeEvidenceType.RuntimeConfirmed);
|
|
}
|
|
|
|
[Fact]
|
|
public void Merge_WithRuntimeOnlyPath_CreatesRuntimeOnlyEvidence()
|
|
{
|
|
var graph = CreateTestGraph();
|
|
var signals = new RuntimeSignalSummary
|
|
{
|
|
ContainerId = "container-123",
|
|
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
StoppedAt = DateTimeOffset.UtcNow,
|
|
TotalEvents = 100,
|
|
CallPaths = new List<ObservedCallPath>
|
|
{
|
|
new()
|
|
{
|
|
// Path not in static graph
|
|
Symbols = ["dynamic_dispatch", "hidden_method"],
|
|
ObservationCount = 20,
|
|
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
LastObservedAt = DateTimeOffset.UtcNow,
|
|
},
|
|
},
|
|
ObservedSymbols = ["dynamic_dispatch", "hidden_method"],
|
|
};
|
|
|
|
var result = _merger.Merge(graph, signals);
|
|
|
|
Assert.Contains(result.Evidence, e =>
|
|
e.Type == RuntimeEvidenceType.RuntimeOnly);
|
|
Assert.True(result.Statistics.RuntimeOnlyPathCount > 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Merge_WithDetectedRuntimes_CreatesRuntimeDetectedEvidence()
|
|
{
|
|
var graph = CreateTestGraph();
|
|
var signals = new RuntimeSignalSummary
|
|
{
|
|
ContainerId = "container-123",
|
|
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
StoppedAt = DateTimeOffset.UtcNow,
|
|
TotalEvents = 100,
|
|
CallPaths = [],
|
|
ObservedSymbols = [],
|
|
DetectedRuntimes = [RuntimeType.Node, RuntimeType.Python],
|
|
};
|
|
|
|
var result = _merger.Merge(graph, signals);
|
|
|
|
Assert.Contains(result.Evidence, e =>
|
|
e.Type == RuntimeEvidenceType.RuntimeDetected &&
|
|
e.RuntimeType == "Node");
|
|
Assert.Contains(result.Evidence, e =>
|
|
e.Type == RuntimeEvidenceType.RuntimeDetected &&
|
|
e.RuntimeType == "Python");
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidatePath_WithValidPath_ReturnsConfirmed()
|
|
{
|
|
var graph = CreateTestGraph();
|
|
var path = new ObservedCallPath
|
|
{
|
|
Symbols = ["main", "processRequest"],
|
|
ObservationCount = 10,
|
|
FirstObservedAt = DateTimeOffset.UtcNow,
|
|
LastObservedAt = DateTimeOffset.UtcNow,
|
|
};
|
|
|
|
var result = _merger.ValidatePath(graph, path);
|
|
|
|
Assert.True(result.IsValid);
|
|
Assert.Equal(PathType.Confirmed, result.PathType);
|
|
Assert.Equal(1.0, result.MatchRatio);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidatePath_WithUnknownPath_ReturnsRuntimeOnly()
|
|
{
|
|
var graph = CreateTestGraph();
|
|
var path = new ObservedCallPath
|
|
{
|
|
Symbols = ["unknown", "method"],
|
|
ObservationCount = 10,
|
|
FirstObservedAt = DateTimeOffset.UtcNow,
|
|
LastObservedAt = DateTimeOffset.UtcNow,
|
|
};
|
|
|
|
var result = _merger.ValidatePath(graph, path);
|
|
|
|
Assert.True(result.IsValid);
|
|
Assert.Equal(PathType.RuntimeOnly, result.PathType);
|
|
Assert.Equal(0.0, result.MatchRatio);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidatePath_WithShortPath_ReturnsInvalid()
|
|
{
|
|
var graph = CreateTestGraph();
|
|
var path = new ObservedCallPath
|
|
{
|
|
Symbols = ["single"],
|
|
ObservationCount = 10,
|
|
FirstObservedAt = DateTimeOffset.UtcNow,
|
|
LastObservedAt = DateTimeOffset.UtcNow,
|
|
};
|
|
|
|
var result = _merger.ValidatePath(graph, path);
|
|
|
|
Assert.False(result.IsValid);
|
|
Assert.Equal(PathType.Invalid, result.PathType);
|
|
}
|
|
|
|
[Fact]
|
|
public void Merge_StatisticsAreAccurate()
|
|
{
|
|
var graph = CreateTestGraph();
|
|
var signals = new RuntimeSignalSummary
|
|
{
|
|
ContainerId = "container-123",
|
|
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
StoppedAt = DateTimeOffset.UtcNow,
|
|
TotalEvents = 500,
|
|
CallPaths = new List<ObservedCallPath>
|
|
{
|
|
new()
|
|
{
|
|
Symbols = ["main", "processRequest"],
|
|
ObservationCount = 100,
|
|
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
LastObservedAt = DateTimeOffset.UtcNow,
|
|
},
|
|
},
|
|
ObservedSymbols = ["main", "processRequest"],
|
|
DroppedEvents = 10,
|
|
};
|
|
|
|
var result = _merger.Merge(graph, signals);
|
|
|
|
Assert.Equal(2, result.Statistics.StaticEdgeCount);
|
|
Assert.Equal(500, result.Statistics.RuntimeEventCount);
|
|
Assert.Equal(1, result.Statistics.CallPathCount);
|
|
Assert.Equal(10, result.Statistics.DroppedEventCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void RuntimeEvidence_ContainsContainerId()
|
|
{
|
|
var graph = CreateTestGraph();
|
|
var signals = new RuntimeSignalSummary
|
|
{
|
|
ContainerId = "my-container-id",
|
|
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
StoppedAt = DateTimeOffset.UtcNow,
|
|
TotalEvents = 100,
|
|
CallPaths = new List<ObservedCallPath>
|
|
{
|
|
new()
|
|
{
|
|
Symbols = ["main", "processRequest"],
|
|
ObservationCount = 10,
|
|
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
LastObservedAt = DateTimeOffset.UtcNow,
|
|
},
|
|
},
|
|
ObservedSymbols = [],
|
|
};
|
|
|
|
var result = _merger.Merge(graph, signals);
|
|
|
|
Assert.All(result.Evidence, e =>
|
|
Assert.Equal("my-container-id", e.ContainerId));
|
|
}
|
|
|
|
[Fact]
|
|
public void EvidenceSource_IsEbpf()
|
|
{
|
|
var graph = CreateTestGraph();
|
|
var signals = new RuntimeSignalSummary
|
|
{
|
|
ContainerId = "container-123",
|
|
StartedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
StoppedAt = DateTimeOffset.UtcNow,
|
|
TotalEvents = 100,
|
|
CallPaths = new List<ObservedCallPath>
|
|
{
|
|
new()
|
|
{
|
|
Symbols = ["main", "processRequest"],
|
|
ObservationCount = 10,
|
|
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
|
|
LastObservedAt = DateTimeOffset.UtcNow,
|
|
},
|
|
},
|
|
ObservedSymbols = [],
|
|
};
|
|
|
|
var result = _merger.Merge(graph, signals);
|
|
|
|
Assert.All(result.Evidence, e =>
|
|
Assert.Equal(EvidenceSource.Ebpf, e.Source));
|
|
}
|
|
|
|
private static RichGraph CreateTestGraph()
|
|
{
|
|
return new RichGraph(
|
|
Nodes: new List<RichGraphNode>
|
|
{
|
|
new("main", "main", null, null, "native", "entrypoint", null, null, null, null, null),
|
|
new("processRequest", "processRequest", null, null, "native", "function", null, null, null, null, null),
|
|
new("handleError", "handleError", null, null, "native", "function", null, null, null, null, null),
|
|
},
|
|
Edges: new List<RichGraphEdge>
|
|
{
|
|
new("main", "processRequest", "call", null, null, null, 1.0, null),
|
|
new("processRequest", "handleError", "call", null, null, null, 0.8, null),
|
|
},
|
|
Roots: new List<RichGraphRoot>
|
|
{
|
|
new("main", "main", "entrypoint")
|
|
},
|
|
Analyzer: new RichGraphAnalyzer("test-analyzer", "1.0.0", null)
|
|
);
|
|
}
|
|
}
|