- Introduced `VexStatusChipComponent` to display VEX status with color coding and tooltips. - Implemented integration tests for reachability drift detection, covering various scenarios including drift detection, determinism, and error handling. - Enhanced `ScannerToSignalsReachabilityTests` with a null implementation of `ICallGraphSyncService` for better test isolation. - Updated project references to include the new Reachability Drift library.
614 lines
24 KiB
C#
614 lines
24 KiB
C#
using System.Collections.Immutable;
|
|
using FluentAssertions;
|
|
using StellaOps.Scanner.CallGraph;
|
|
using StellaOps.Scanner.Reachability;
|
|
using StellaOps.Scanner.ReachabilityDrift;
|
|
using StellaOps.Scanner.ReachabilityDrift.Services;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.ScannerSignals.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Integration tests for the Reachability Drift Detection pipeline.
|
|
/// Tests the end-to-end flow from call graph extraction through drift detection.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Task: RDRIFT-MASTER-0002
|
|
/// Sprint: SPRINT_3600_0001_0001_reachability_drift_master
|
|
/// </remarks>
|
|
public sealed class ReachabilityDriftIntegrationTests
|
|
{
|
|
private readonly TimeProvider _fixedTime = new FakeTimeProvider(
|
|
new DateTimeOffset(2025, 12, 19, 12, 0, 0, TimeSpan.Zero));
|
|
|
|
#region Drift Detection Tests
|
|
|
|
[Fact]
|
|
public void DetectDrift_WhenPathBecomesReachable_ReportsNewlyReachableSink()
|
|
{
|
|
// Arrange: unreachable -> reachable (guard removed)
|
|
var baseGraph = CreateUnreachableGraph("scan-v1");
|
|
var headGraph = CreateReachableGraph("scan-v2");
|
|
|
|
var extractor = new CodeChangeFactExtractor(_fixedTime);
|
|
var codeChanges = extractor.Extract(baseGraph, headGraph);
|
|
var detector = new ReachabilityDriftDetector(_fixedTime);
|
|
|
|
// Act
|
|
var drift = detector.Detect(baseGraph, headGraph, codeChanges, includeFullPath: true);
|
|
|
|
// Assert
|
|
drift.Should().NotBeNull();
|
|
drift.BaseScanId.Should().Be("scan-v1");
|
|
drift.HeadScanId.Should().Be("scan-v2");
|
|
drift.Language.Should().Be("java");
|
|
drift.HasMaterialDrift.Should().BeTrue();
|
|
|
|
drift.NewlyReachable.Should().HaveCount(1);
|
|
drift.NewlyUnreachable.Should().BeEmpty();
|
|
|
|
var sink = drift.NewlyReachable[0];
|
|
sink.Direction.Should().Be(DriftDirection.BecameReachable);
|
|
sink.SinkNodeId.Should().Be("jndi-lookup-sink");
|
|
sink.SinkCategory.Should().Be(SinkCategory.CmdExec);
|
|
sink.Cause.Kind.Should().Be(DriftCauseKind.GuardRemoved);
|
|
sink.Path.Entrypoint.NodeId.Should().Be("http-handler-entry");
|
|
sink.Path.Sink.NodeId.Should().Be("jndi-lookup-sink");
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectDrift_WhenPathBecomesUnreachable_ReportsNewlyUnreachableSink()
|
|
{
|
|
// Arrange: reachable -> unreachable (guard added)
|
|
var baseGraph = CreateReachableGraph("scan-v1");
|
|
var headGraph = CreateUnreachableGraph("scan-v2");
|
|
|
|
var extractor = new CodeChangeFactExtractor(_fixedTime);
|
|
var codeChanges = extractor.Extract(baseGraph, headGraph);
|
|
var detector = new ReachabilityDriftDetector(_fixedTime);
|
|
|
|
// Act
|
|
var drift = detector.Detect(baseGraph, headGraph, codeChanges, includeFullPath: true);
|
|
|
|
// Assert
|
|
drift.Should().NotBeNull();
|
|
drift.BaseScanId.Should().Be("scan-v1");
|
|
drift.HeadScanId.Should().Be("scan-v2");
|
|
drift.HasMaterialDrift.Should().BeFalse();
|
|
|
|
drift.NewlyReachable.Should().BeEmpty();
|
|
drift.NewlyUnreachable.Should().HaveCount(1);
|
|
|
|
var sink = drift.NewlyUnreachable[0];
|
|
sink.Direction.Should().Be(DriftDirection.BecameUnreachable);
|
|
sink.SinkNodeId.Should().Be("jndi-lookup-sink");
|
|
sink.Cause.Kind.Should().Be(DriftCauseKind.GuardAdded);
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectDrift_WhenNoChange_ReportsNoDrift()
|
|
{
|
|
// Arrange: same graph, no changes
|
|
var baseGraph = CreateReachableGraph("scan-v1");
|
|
var headGraph = CreateReachableGraph("scan-v2");
|
|
|
|
var extractor = new CodeChangeFactExtractor(_fixedTime);
|
|
var codeChanges = extractor.Extract(baseGraph, headGraph);
|
|
var detector = new ReachabilityDriftDetector(_fixedTime);
|
|
|
|
// Act
|
|
var drift = detector.Detect(baseGraph, headGraph, codeChanges, includeFullPath: false);
|
|
|
|
// Assert
|
|
drift.Should().NotBeNull();
|
|
drift.HasMaterialDrift.Should().BeFalse();
|
|
drift.NewlyReachable.Should().BeEmpty();
|
|
drift.NewlyUnreachable.Should().BeEmpty();
|
|
drift.TotalDriftCount.Should().Be(0);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Determinism Tests
|
|
|
|
[Fact]
|
|
public void DetectDrift_IsDeterministic_SameInputsProduceSameOutputs()
|
|
{
|
|
// Arrange
|
|
var baseGraph = CreateUnreachableGraph("scan-v1");
|
|
var headGraph = CreateReachableGraph("scan-v2");
|
|
|
|
var extractor = new CodeChangeFactExtractor(_fixedTime);
|
|
var codeChanges = extractor.Extract(baseGraph, headGraph);
|
|
var detector = new ReachabilityDriftDetector(_fixedTime);
|
|
|
|
// Act
|
|
var drift1 = detector.Detect(baseGraph, headGraph, codeChanges, includeFullPath: true);
|
|
var drift2 = detector.Detect(baseGraph, headGraph, codeChanges, includeFullPath: true);
|
|
|
|
// Assert
|
|
drift1.Id.Should().Be(drift2.Id);
|
|
drift1.ResultDigest.Should().Be(drift2.ResultDigest);
|
|
drift1.DetectedAt.Should().Be(drift2.DetectedAt);
|
|
drift1.NewlyReachable.Length.Should().Be(drift2.NewlyReachable.Length);
|
|
|
|
for (var i = 0; i < drift1.NewlyReachable.Length; i++)
|
|
{
|
|
drift1.NewlyReachable[i].Id.Should().Be(drift2.NewlyReachable[i].Id);
|
|
drift1.NewlyReachable[i].SinkNodeId.Should().Be(drift2.NewlyReachable[i].SinkNodeId);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectDrift_ResultDigest_IsStableAcrossRuns()
|
|
{
|
|
// Arrange
|
|
var baseGraph = CreateUnreachableGraph("scan-v1");
|
|
var headGraph = CreateReachableGraph("scan-v2");
|
|
|
|
var extractor = new CodeChangeFactExtractor(_fixedTime);
|
|
var codeChanges = extractor.Extract(baseGraph, headGraph);
|
|
|
|
// Act: Create multiple detectors and run independently
|
|
var detector1 = new ReachabilityDriftDetector(_fixedTime);
|
|
var detector2 = new ReachabilityDriftDetector(_fixedTime);
|
|
|
|
var drift1 = detector1.Detect(baseGraph, headGraph, codeChanges, includeFullPath: false);
|
|
var drift2 = detector2.Detect(baseGraph, headGraph, codeChanges, includeFullPath: false);
|
|
|
|
// Assert
|
|
drift1.ResultDigest.Should().NotBeNullOrWhiteSpace();
|
|
drift1.ResultDigest.Should().Be(drift2.ResultDigest);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CodeChangeFact Extraction Tests
|
|
|
|
[Fact]
|
|
public void CodeChangeFactExtractor_DetectsAddedEdge()
|
|
{
|
|
// Arrange
|
|
var baseGraph = CreateUnreachableGraph("scan-v1");
|
|
var headGraph = CreateReachableGraph("scan-v2");
|
|
|
|
var extractor = new CodeChangeFactExtractor(_fixedTime);
|
|
|
|
// Act
|
|
var codeChanges = extractor.Extract(baseGraph, headGraph);
|
|
|
|
// Assert - The extractor reports edge changes as GuardChanged with details
|
|
codeChanges.Should().NotBeEmpty();
|
|
codeChanges.Should().Contain(c =>
|
|
c.Kind == CodeChangeKind.GuardChanged &&
|
|
c.Details.HasValue &&
|
|
c.Details.Value.GetRawText().Contains("edge_added", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void CodeChangeFactExtractor_DetectsRemovedEdge()
|
|
{
|
|
// Arrange
|
|
var baseGraph = CreateReachableGraph("scan-v1");
|
|
var headGraph = CreateUnreachableGraph("scan-v2");
|
|
|
|
var extractor = new CodeChangeFactExtractor(_fixedTime);
|
|
|
|
// Act
|
|
var codeChanges = extractor.Extract(baseGraph, headGraph);
|
|
|
|
// Assert - The extractor reports edge changes as GuardChanged with details
|
|
codeChanges.Should().NotBeEmpty();
|
|
codeChanges.Should().Contain(c =>
|
|
c.Kind == CodeChangeKind.GuardChanged &&
|
|
c.Details.HasValue &&
|
|
c.Details.Value.GetRawText().Contains("edge_removed", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Multi-Sink Tests
|
|
|
|
[Fact]
|
|
public void DetectDrift_WithMultipleSinks_ReportsAllDriftedSinks()
|
|
{
|
|
// Arrange: Multiple sinks become reachable
|
|
var baseGraph = CreateMultiSinkUnreachableGraph("scan-v1");
|
|
var headGraph = CreateMultiSinkReachableGraph("scan-v2");
|
|
|
|
var extractor = new CodeChangeFactExtractor(_fixedTime);
|
|
var codeChanges = extractor.Extract(baseGraph, headGraph);
|
|
var detector = new ReachabilityDriftDetector(_fixedTime);
|
|
|
|
// Act
|
|
var drift = detector.Detect(baseGraph, headGraph, codeChanges, includeFullPath: true);
|
|
|
|
// Assert
|
|
drift.Should().NotBeNull();
|
|
drift.HasMaterialDrift.Should().BeTrue();
|
|
drift.NewlyReachable.Should().HaveCount(2);
|
|
drift.NewlyUnreachable.Should().BeEmpty();
|
|
|
|
var sinkIds = drift.NewlyReachable.Select(s => s.SinkNodeId).OrderBy(s => s).ToList();
|
|
sinkIds.Should().Contain("jndi-lookup-sink");
|
|
sinkIds.Should().Contain("file-write-sink");
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectDrift_OrderingSinks_IsStableAndDeterministic()
|
|
{
|
|
// Arrange
|
|
var baseGraph = CreateMultiSinkUnreachableGraph("scan-v1");
|
|
var headGraph = CreateMultiSinkReachableGraph("scan-v2");
|
|
|
|
var extractor = new CodeChangeFactExtractor(_fixedTime);
|
|
var codeChanges = extractor.Extract(baseGraph, headGraph);
|
|
var detector = new ReachabilityDriftDetector(_fixedTime);
|
|
|
|
// Act: Run multiple times
|
|
var results = Enumerable.Range(0, 5)
|
|
.Select(_ => detector.Detect(baseGraph, headGraph, codeChanges, includeFullPath: false))
|
|
.ToList();
|
|
|
|
// Assert: All results should have same ordering
|
|
var expectedOrder = results[0].NewlyReachable.Select(s => s.SinkNodeId).ToList();
|
|
foreach (var result in results.Skip(1))
|
|
{
|
|
var actualOrder = result.NewlyReachable.Select(s => s.SinkNodeId).ToList();
|
|
actualOrder.Should().Equal(expectedOrder);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Path Compression Tests
|
|
|
|
[Fact]
|
|
public void DetectDrift_WithFullPath_IncludesIntermediateNodes()
|
|
{
|
|
// Arrange
|
|
var baseGraph = CreateUnreachableGraph("scan-v1");
|
|
var headGraph = CreateReachableGraphWithIntermediates("scan-v2");
|
|
|
|
var extractor = new CodeChangeFactExtractor(_fixedTime);
|
|
var codeChanges = extractor.Extract(baseGraph, headGraph);
|
|
var detector = new ReachabilityDriftDetector(_fixedTime);
|
|
|
|
// Act
|
|
var drift = detector.Detect(baseGraph, headGraph, codeChanges, includeFullPath: true);
|
|
|
|
// Assert
|
|
drift.NewlyReachable.Should().HaveCount(1);
|
|
var sink = drift.NewlyReachable[0];
|
|
|
|
sink.Path.Should().NotBeNull();
|
|
sink.Path.Entrypoint.NodeId.Should().Be("http-handler-entry");
|
|
sink.Path.Sink.NodeId.Should().Be("jndi-lookup-sink");
|
|
sink.Path.FullPath.Should().NotBeNullOrEmpty();
|
|
sink.Path.FullPath!.Value.Length.Should().BeGreaterThan(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectDrift_WithoutFullPath_OmitsIntermediateNodes()
|
|
{
|
|
// Arrange
|
|
var baseGraph = CreateUnreachableGraph("scan-v1");
|
|
var headGraph = CreateReachableGraphWithIntermediates("scan-v2");
|
|
|
|
var extractor = new CodeChangeFactExtractor(_fixedTime);
|
|
var codeChanges = extractor.Extract(baseGraph, headGraph);
|
|
var detector = new ReachabilityDriftDetector(_fixedTime);
|
|
|
|
// Act
|
|
var drift = detector.Detect(baseGraph, headGraph, codeChanges, includeFullPath: false);
|
|
|
|
// Assert
|
|
drift.NewlyReachable.Should().HaveCount(1);
|
|
var sink = drift.NewlyReachable[0];
|
|
|
|
sink.Path.Should().NotBeNull();
|
|
sink.Path.Entrypoint.NodeId.Should().Be("http-handler-entry");
|
|
sink.Path.Sink.NodeId.Should().Be("jndi-lookup-sink");
|
|
sink.Path.FullPath.Should().BeNullOrEmpty();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Error Handling Tests
|
|
|
|
[Fact]
|
|
public void DetectDrift_WithLanguageMismatch_ThrowsArgumentException()
|
|
{
|
|
// Arrange
|
|
var baseGraph = CreateGraph("scan-v1", "java", ImmutableArray<CallGraphEdge>.Empty);
|
|
var headGraph = CreateGraph("scan-v2", "dotnet", ImmutableArray<CallGraphEdge>.Empty);
|
|
|
|
var detector = new ReachabilityDriftDetector(_fixedTime);
|
|
|
|
// Act & Assert
|
|
var act = () => detector.Detect(baseGraph, headGraph, ImmutableArray<CodeChangeFact>.Empty.ToList(), includeFullPath: false);
|
|
act.Should().Throw<ArgumentException>().WithMessage("*Language mismatch*");
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectDrift_WithNullBaseGraph_ThrowsArgumentNullException()
|
|
{
|
|
// Arrange
|
|
var headGraph = CreateReachableGraph("scan-v2");
|
|
var detector = new ReachabilityDriftDetector(_fixedTime);
|
|
|
|
// Act & Assert
|
|
var act = () => detector.Detect(null!, headGraph, Array.Empty<CodeChangeFact>(), includeFullPath: false);
|
|
act.Should().Throw<ArgumentNullException>();
|
|
}
|
|
|
|
[Fact]
|
|
public void DetectDrift_WithNullHeadGraph_ThrowsArgumentNullException()
|
|
{
|
|
// Arrange
|
|
var baseGraph = CreateUnreachableGraph("scan-v1");
|
|
var detector = new ReachabilityDriftDetector(_fixedTime);
|
|
|
|
// Act & Assert
|
|
var act = () => detector.Detect(baseGraph, null!, Array.Empty<CodeChangeFact>(), includeFullPath: false);
|
|
act.Should().Throw<ArgumentNullException>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private static CallGraphSnapshot CreateUnreachableGraph(string scanId)
|
|
{
|
|
// Graph with no edges - sink is unreachable
|
|
return CreateGraph(scanId, "java", ImmutableArray<CallGraphEdge>.Empty);
|
|
}
|
|
|
|
private static CallGraphSnapshot CreateReachableGraph(string scanId)
|
|
{
|
|
// Graph with edge from entry to sink - sink is reachable
|
|
var edges = ImmutableArray.Create(
|
|
new CallGraphEdge("http-handler-entry", "jndi-lookup-sink", CallKind.Direct, "Logger.java:42"));
|
|
return CreateGraph(scanId, "java", edges);
|
|
}
|
|
|
|
private static CallGraphSnapshot CreateReachableGraphWithIntermediates(string scanId)
|
|
{
|
|
// Graph with intermediate nodes: entry -> logger -> substitutor -> sink
|
|
var edges = ImmutableArray.Create(
|
|
new CallGraphEdge("http-handler-entry", "logger-method", CallKind.Direct, "App.java:10"),
|
|
new CallGraphEdge("logger-method", "pattern-converter", CallKind.Direct, "Logger.java:15"),
|
|
new CallGraphEdge("pattern-converter", "str-substitutor", CallKind.Direct, "PatternConverter.java:20"),
|
|
new CallGraphEdge("str-substitutor", "jndi-lookup-sink", CallKind.Direct, "StrSubstitutor.java:25"));
|
|
|
|
var nodes = ImmutableArray.Create(
|
|
new CallGraphNode(
|
|
NodeId: "http-handler-entry",
|
|
Symbol: "com.example.App.handleRequest",
|
|
File: "App.java",
|
|
Line: 10,
|
|
Package: "pkg:maven/com.example/app@1.0.0",
|
|
Visibility: Visibility.Public,
|
|
IsEntrypoint: true,
|
|
EntrypointType: EntrypointType.HttpHandler,
|
|
IsSink: false,
|
|
SinkCategory: null),
|
|
new CallGraphNode(
|
|
NodeId: "logger-method",
|
|
Symbol: "org.apache.logging.log4j.Logger.info",
|
|
File: "Logger.java",
|
|
Line: 15,
|
|
Package: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.0",
|
|
Visibility: Visibility.Public,
|
|
IsEntrypoint: false,
|
|
EntrypointType: null,
|
|
IsSink: false,
|
|
SinkCategory: null),
|
|
new CallGraphNode(
|
|
NodeId: "pattern-converter",
|
|
Symbol: "org.apache.logging.log4j.core.pattern.MessagePatternConverter.format",
|
|
File: "PatternConverter.java",
|
|
Line: 20,
|
|
Package: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.0",
|
|
Visibility: Visibility.Internal,
|
|
IsEntrypoint: false,
|
|
EntrypointType: null,
|
|
IsSink: false,
|
|
SinkCategory: null),
|
|
new CallGraphNode(
|
|
NodeId: "str-substitutor",
|
|
Symbol: "org.apache.logging.log4j.core.lookup.StrSubstitutor.replace",
|
|
File: "StrSubstitutor.java",
|
|
Line: 25,
|
|
Package: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.0",
|
|
Visibility: Visibility.Internal,
|
|
IsEntrypoint: false,
|
|
EntrypointType: null,
|
|
IsSink: false,
|
|
SinkCategory: null),
|
|
new CallGraphNode(
|
|
NodeId: "jndi-lookup-sink",
|
|
Symbol: "org.apache.logging.log4j.core.lookup.JndiLookup.lookup",
|
|
File: "JndiLookup.java",
|
|
Line: 30,
|
|
Package: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.0",
|
|
Visibility: Visibility.Public,
|
|
IsEntrypoint: false,
|
|
EntrypointType: null,
|
|
IsSink: true,
|
|
SinkCategory: SinkCategory.CmdExec));
|
|
|
|
var provisional = new CallGraphSnapshot(
|
|
ScanId: scanId,
|
|
GraphDigest: string.Empty,
|
|
Language: "java",
|
|
ExtractedAt: DateTimeOffset.UnixEpoch,
|
|
Nodes: nodes,
|
|
Edges: edges,
|
|
EntrypointIds: ImmutableArray.Create("http-handler-entry"),
|
|
SinkIds: ImmutableArray.Create("jndi-lookup-sink"));
|
|
|
|
return provisional with { GraphDigest = CallGraphDigests.ComputeGraphDigest(provisional) };
|
|
}
|
|
|
|
private static CallGraphSnapshot CreateMultiSinkUnreachableGraph(string scanId)
|
|
{
|
|
// Graph with multiple sinks, none reachable
|
|
var nodes = ImmutableArray.Create(
|
|
new CallGraphNode(
|
|
NodeId: "http-handler-entry",
|
|
Symbol: "com.example.App.handleRequest",
|
|
File: "App.java",
|
|
Line: 10,
|
|
Package: "pkg:maven/com.example/app@1.0.0",
|
|
Visibility: Visibility.Public,
|
|
IsEntrypoint: true,
|
|
EntrypointType: EntrypointType.HttpHandler,
|
|
IsSink: false,
|
|
SinkCategory: null),
|
|
new CallGraphNode(
|
|
NodeId: "jndi-lookup-sink",
|
|
Symbol: "org.apache.logging.log4j.core.lookup.JndiLookup.lookup",
|
|
File: "JndiLookup.java",
|
|
Line: 30,
|
|
Package: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.0",
|
|
Visibility: Visibility.Public,
|
|
IsEntrypoint: false,
|
|
EntrypointType: null,
|
|
IsSink: true,
|
|
SinkCategory: SinkCategory.CmdExec),
|
|
new CallGraphNode(
|
|
NodeId: "file-write-sink",
|
|
Symbol: "java.io.FileOutputStream.write",
|
|
File: "FileOutputStream.java",
|
|
Line: 100,
|
|
Package: "pkg:maven/java/jdk@17",
|
|
Visibility: Visibility.Public,
|
|
IsEntrypoint: false,
|
|
EntrypointType: null,
|
|
IsSink: true,
|
|
SinkCategory: SinkCategory.FileWrite));
|
|
|
|
var provisional = new CallGraphSnapshot(
|
|
ScanId: scanId,
|
|
GraphDigest: string.Empty,
|
|
Language: "java",
|
|
ExtractedAt: DateTimeOffset.UnixEpoch,
|
|
Nodes: nodes,
|
|
Edges: ImmutableArray<CallGraphEdge>.Empty,
|
|
EntrypointIds: ImmutableArray.Create("http-handler-entry"),
|
|
SinkIds: ImmutableArray.Create("jndi-lookup-sink", "file-write-sink"));
|
|
|
|
return provisional with { GraphDigest = CallGraphDigests.ComputeGraphDigest(provisional) };
|
|
}
|
|
|
|
private static CallGraphSnapshot CreateMultiSinkReachableGraph(string scanId)
|
|
{
|
|
// Graph with multiple sinks, all reachable
|
|
var nodes = ImmutableArray.Create(
|
|
new CallGraphNode(
|
|
NodeId: "http-handler-entry",
|
|
Symbol: "com.example.App.handleRequest",
|
|
File: "App.java",
|
|
Line: 10,
|
|
Package: "pkg:maven/com.example/app@1.0.0",
|
|
Visibility: Visibility.Public,
|
|
IsEntrypoint: true,
|
|
EntrypointType: EntrypointType.HttpHandler,
|
|
IsSink: false,
|
|
SinkCategory: null),
|
|
new CallGraphNode(
|
|
NodeId: "jndi-lookup-sink",
|
|
Symbol: "org.apache.logging.log4j.core.lookup.JndiLookup.lookup",
|
|
File: "JndiLookup.java",
|
|
Line: 30,
|
|
Package: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.0",
|
|
Visibility: Visibility.Public,
|
|
IsEntrypoint: false,
|
|
EntrypointType: null,
|
|
IsSink: true,
|
|
SinkCategory: SinkCategory.CmdExec),
|
|
new CallGraphNode(
|
|
NodeId: "file-write-sink",
|
|
Symbol: "java.io.FileOutputStream.write",
|
|
File: "FileOutputStream.java",
|
|
Line: 100,
|
|
Package: "pkg:maven/java/jdk@17",
|
|
Visibility: Visibility.Public,
|
|
IsEntrypoint: false,
|
|
EntrypointType: null,
|
|
IsSink: true,
|
|
SinkCategory: SinkCategory.FileWrite));
|
|
|
|
var edges = ImmutableArray.Create(
|
|
new CallGraphEdge("http-handler-entry", "jndi-lookup-sink", CallKind.Direct, "App.java:15"),
|
|
new CallGraphEdge("http-handler-entry", "file-write-sink", CallKind.Direct, "App.java:20"));
|
|
|
|
var provisional = new CallGraphSnapshot(
|
|
ScanId: scanId,
|
|
GraphDigest: string.Empty,
|
|
Language: "java",
|
|
ExtractedAt: DateTimeOffset.UnixEpoch,
|
|
Nodes: nodes,
|
|
Edges: edges,
|
|
EntrypointIds: ImmutableArray.Create("http-handler-entry"),
|
|
SinkIds: ImmutableArray.Create("jndi-lookup-sink", "file-write-sink"));
|
|
|
|
return provisional with { GraphDigest = CallGraphDigests.ComputeGraphDigest(provisional) };
|
|
}
|
|
|
|
private static CallGraphSnapshot CreateGraph(string scanId, string language, ImmutableArray<CallGraphEdge> edges)
|
|
{
|
|
var nodes = ImmutableArray.Create(
|
|
new CallGraphNode(
|
|
NodeId: "http-handler-entry",
|
|
Symbol: "com.example.App.handleRequest",
|
|
File: "App.java",
|
|
Line: 10,
|
|
Package: "pkg:maven/com.example/app@1.0.0",
|
|
Visibility: Visibility.Public,
|
|
IsEntrypoint: true,
|
|
EntrypointType: EntrypointType.HttpHandler,
|
|
IsSink: false,
|
|
SinkCategory: null),
|
|
new CallGraphNode(
|
|
NodeId: "jndi-lookup-sink",
|
|
Symbol: "org.apache.logging.log4j.core.lookup.JndiLookup.lookup",
|
|
File: "JndiLookup.java",
|
|
Line: 30,
|
|
Package: "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.0",
|
|
Visibility: Visibility.Public,
|
|
IsEntrypoint: false,
|
|
EntrypointType: null,
|
|
IsSink: true,
|
|
SinkCategory: SinkCategory.CmdExec));
|
|
|
|
var provisional = new CallGraphSnapshot(
|
|
ScanId: scanId,
|
|
GraphDigest: string.Empty,
|
|
Language: language,
|
|
ExtractedAt: DateTimeOffset.UnixEpoch,
|
|
Nodes: nodes,
|
|
Edges: edges,
|
|
EntrypointIds: ImmutableArray.Create("http-handler-entry"),
|
|
SinkIds: ImmutableArray.Create("jndi-lookup-sink"));
|
|
|
|
return provisional with { GraphDigest = CallGraphDigests.ComputeGraphDigest(provisional) };
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region FakeTimeProvider
|
|
|
|
private sealed class FakeTimeProvider : TimeProvider
|
|
{
|
|
private readonly DateTimeOffset _fixedTime;
|
|
|
|
public FakeTimeProvider(DateTimeOffset fixedTime) => _fixedTime = fixedTime;
|
|
|
|
public override DateTimeOffset GetUtcNow() => _fixedTime;
|
|
}
|
|
|
|
#endregion
|
|
}
|