save progress

This commit is contained in:
StellaOps Bot
2025-12-18 09:10:36 +02:00
parent b4235c134c
commit 28823a8960
169 changed files with 11995 additions and 449 deletions

View File

@@ -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);
}