save progress
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.CallGraph;
|
||||
using StellaOps.Scanner.ReachabilityDrift;
|
||||
using StellaOps.Scanner.ReachabilityDrift.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.ReachabilityDrift.Tests;
|
||||
|
||||
public sealed class DriftCauseExplainerTests
|
||||
{
|
||||
private static readonly DateTimeOffset FixedNow = DateTimeOffset.Parse("2025-12-17T00:00:00Z");
|
||||
|
||||
[Fact]
|
||||
public void ExplainNewlyReachable_NewEntrypoint_ReturnsNewPublicRoute()
|
||||
{
|
||||
var entry = Node("E", "HomeController.Get", Visibility.Public);
|
||||
var sink = Sink("S", "System.Diagnostics.Process.Start");
|
||||
|
||||
var baseGraph = Graph(
|
||||
scanId: "base",
|
||||
entrypointIds: ImmutableArray<string>.Empty,
|
||||
nodes: new[] { entry, sink },
|
||||
edges: Array.Empty<CallGraphEdge>());
|
||||
|
||||
var headGraph = Graph(
|
||||
scanId: "head",
|
||||
entrypointIds: ImmutableArray.Create("E"),
|
||||
nodes: new[] { entry, sink },
|
||||
edges: new[] { new CallGraphEdge("E", "S", CallKind.Direct) });
|
||||
|
||||
var explainer = new DriftCauseExplainer();
|
||||
var cause = explainer.ExplainNewlyReachable(baseGraph, headGraph, "S", ImmutableArray.Create("E", "S"), Array.Empty<CodeChangeFact>());
|
||||
|
||||
Assert.Equal(DriftCauseKind.NewPublicRoute, cause.Kind);
|
||||
Assert.Contains("HomeController.Get", cause.Description, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplainNewlyReachable_VisibilityEscalation_UsesCodeChangeId()
|
||||
{
|
||||
var changed = Node("N1", "ApiController.GetSecret", Visibility.Public);
|
||||
var baseNode = changed with { Visibility = Visibility.Internal };
|
||||
|
||||
var baseGraph = Graph(
|
||||
scanId: "base",
|
||||
entrypointIds: ImmutableArray.Create("N1"),
|
||||
nodes: new[] { baseNode },
|
||||
edges: Array.Empty<CallGraphEdge>());
|
||||
|
||||
var headGraph = Graph(
|
||||
scanId: "head",
|
||||
entrypointIds: ImmutableArray.Create("N1"),
|
||||
nodes: new[] { changed },
|
||||
edges: Array.Empty<CallGraphEdge>());
|
||||
|
||||
var changeId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
|
||||
var changes = new[]
|
||||
{
|
||||
new CodeChangeFact
|
||||
{
|
||||
Id = changeId,
|
||||
ScanId = "head",
|
||||
BaseScanId = "base",
|
||||
Language = "dotnet",
|
||||
NodeId = "N1",
|
||||
File = "api.cs",
|
||||
Symbol = "ApiController.GetSecret",
|
||||
Kind = CodeChangeKind.VisibilityChanged,
|
||||
Details = null,
|
||||
DetectedAt = FixedNow
|
||||
}
|
||||
};
|
||||
|
||||
var explainer = new DriftCauseExplainer();
|
||||
var cause = explainer.ExplainNewlyReachable(baseGraph, headGraph, "N1", ImmutableArray.Create("N1"), changes);
|
||||
|
||||
Assert.Equal(DriftCauseKind.VisibilityEscalated, cause.Kind);
|
||||
Assert.Equal(changeId, cause.CodeChangeId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplainNewlyUnreachable_SinkRemoved_ReturnsSymbolRemoved()
|
||||
{
|
||||
var entry = Node("E", "Entry", Visibility.Public);
|
||||
var sink = Sink("S", "System.Diagnostics.Process.Start");
|
||||
|
||||
var baseGraph = Graph(
|
||||
scanId: "base",
|
||||
entrypointIds: ImmutableArray.Create("E"),
|
||||
nodes: new[] { entry, sink },
|
||||
edges: new[] { new CallGraphEdge("E", "S", CallKind.Direct) });
|
||||
|
||||
var headGraph = Graph(
|
||||
scanId: "head",
|
||||
entrypointIds: ImmutableArray.Create("E"),
|
||||
nodes: new[] { entry },
|
||||
edges: Array.Empty<CallGraphEdge>());
|
||||
|
||||
var explainer = new DriftCauseExplainer();
|
||||
var cause = explainer.ExplainNewlyUnreachable(baseGraph, headGraph, "S", ImmutableArray.Create("E", "S"), Array.Empty<CodeChangeFact>());
|
||||
|
||||
Assert.Equal(DriftCauseKind.SymbolRemoved, cause.Kind);
|
||||
Assert.Contains("System.Diagnostics.Process.Start", cause.Description, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplainNewlyUnreachable_EdgeRemoved_ReturnsGuardAdded()
|
||||
{
|
||||
var entry = Node("E", "Entry", Visibility.Public);
|
||||
var sink = Sink("S", "System.Diagnostics.Process.Start");
|
||||
|
||||
var baseGraph = Graph(
|
||||
scanId: "base",
|
||||
entrypointIds: ImmutableArray.Create("E"),
|
||||
nodes: new[] { entry, sink },
|
||||
edges: new[] { new CallGraphEdge("E", "S", CallKind.Direct) });
|
||||
|
||||
var headGraph = Graph(
|
||||
scanId: "head",
|
||||
entrypointIds: ImmutableArray.Create("E"),
|
||||
nodes: new[] { entry, sink },
|
||||
edges: Array.Empty<CallGraphEdge>());
|
||||
|
||||
var explainer = new DriftCauseExplainer();
|
||||
var cause = explainer.ExplainNewlyUnreachable(baseGraph, headGraph, "S", ImmutableArray.Create("E", "S"), Array.Empty<CodeChangeFact>());
|
||||
|
||||
Assert.Equal(DriftCauseKind.GuardAdded, cause.Kind);
|
||||
Assert.Contains("Entry", cause.Description, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static CallGraphSnapshot Graph(
|
||||
string scanId,
|
||||
ImmutableArray<string> entrypointIds,
|
||||
IEnumerable<CallGraphNode> nodes,
|
||||
IEnumerable<CallGraphEdge> edges)
|
||||
{
|
||||
var nodesArray = nodes.OrderBy(n => n.NodeId, StringComparer.Ordinal).ToImmutableArray();
|
||||
var edgesArray = edges.ToImmutableArray();
|
||||
|
||||
var sinkIds = nodesArray.Where(n => n.IsSink).Select(n => n.NodeId).ToImmutableArray();
|
||||
|
||||
var provisional = new CallGraphSnapshot(
|
||||
ScanId: scanId,
|
||||
GraphDigest: string.Empty,
|
||||
Language: "dotnet",
|
||||
ExtractedAt: FixedNow,
|
||||
Nodes: nodesArray,
|
||||
Edges: edgesArray,
|
||||
EntrypointIds: entrypointIds,
|
||||
SinkIds: sinkIds);
|
||||
|
||||
return provisional with { GraphDigest = CallGraphDigests.ComputeGraphDigest(provisional) };
|
||||
}
|
||||
|
||||
private static CallGraphNode Node(string nodeId, string symbol, Visibility visibility)
|
||||
=> new(
|
||||
NodeId: nodeId,
|
||||
Symbol: symbol,
|
||||
File: $"{nodeId}.cs",
|
||||
Line: 1,
|
||||
Package: "app",
|
||||
Visibility: visibility,
|
||||
IsEntrypoint: true,
|
||||
EntrypointType: EntrypointType.HttpHandler,
|
||||
IsSink: false,
|
||||
SinkCategory: null);
|
||||
|
||||
private static CallGraphNode Sink(string nodeId, string symbol)
|
||||
=> new(
|
||||
NodeId: nodeId,
|
||||
Symbol: symbol,
|
||||
File: $"{nodeId}.cs",
|
||||
Line: 1,
|
||||
Package: "app",
|
||||
Visibility: Visibility.Public,
|
||||
IsEntrypoint: false,
|
||||
EntrypointType: null,
|
||||
IsSink: true,
|
||||
SinkCategory: Reachability.SinkCategory.CmdExec);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user