Refactor code structure for improved readability and maintainability; optimize performance in key functions.

This commit is contained in:
master
2025-12-22 19:06:31 +02:00
parent dfaa2079aa
commit 4602ccc3a3
1444 changed files with 109919 additions and 8058 deletions

View File

@@ -0,0 +1,395 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using StellaOps.Scanner.Reachability.MiniMap;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests.MiniMap;
public class MiniMapExtractorTests
{
private readonly MiniMapExtractor _extractor = new();
[Fact]
public void Extract_ReachableComponent_ReturnsPaths()
{
var graph = CreateGraphWithPaths();
var result = _extractor.Extract(graph, "pkg:npm/vulnerable@1.0.0");
result.State.Should().Be(ReachabilityState.StaticReachable);
result.Paths.Should().NotBeEmpty();
result.Entrypoints.Should().NotBeEmpty();
result.Confidence.Should().BeGreaterThan(0.5m);
}
[Fact]
public void Extract_UnreachableComponent_ReturnsEmptyPaths()
{
var graph = CreateGraphWithoutPaths();
var result = _extractor.Extract(graph, "pkg:npm/isolated@1.0.0");
result.State.Should().Be(ReachabilityState.StaticUnreachable);
result.Paths.Should().BeEmpty();
result.Confidence.Should().Be(0.9m); // High confidence in unreachability
}
[Fact]
public void Extract_WithRuntimeEvidence_ReturnsConfirmedReachable()
{
var graph = CreateGraphWithRuntimeEvidence();
var result = _extractor.Extract(graph, "pkg:npm/vulnerable@1.0.0");
result.State.Should().Be(ReachabilityState.ConfirmedReachable);
result.Paths.Should().Contain(p => p.HasRuntimeEvidence);
result.Confidence.Should().BeGreaterThan(0.8m);
}
[Fact]
public void Extract_NonExistentComponent_ReturnsNotFoundMap()
{
var graph = CreateGraphWithPaths();
var result = _extractor.Extract(graph, "pkg:npm/nonexistent@1.0.0");
result.State.Should().Be(ReachabilityState.Unknown);
result.Confidence.Should().Be(0m);
result.VulnerableComponent.Id.Should().Be("pkg:npm/nonexistent@1.0.0");
}
[Fact]
public void Extract_RespectMaxPaths_LimitsResults()
{
var graph = CreateGraphWithManyPaths();
var result = _extractor.Extract(graph, "pkg:npm/vulnerable@1.0.0", maxPaths: 5);
result.Paths.Count.Should().BeLessOrEqualTo(5);
}
[Fact]
public void Extract_ClassifiesEntrypointKinds_Correctly()
{
var graph = CreateGraphWithDifferentEntrypoints();
var result = _extractor.Extract(graph, "pkg:npm/vulnerable@1.0.0");
result.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.HttpEndpoint);
result.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.MainFunction);
}
private static RichGraph CreateGraphWithPaths()
{
var nodes = new List<RichGraphNode>
{
new(
Id: "entrypoint:main",
SymbolId: "main",
CodeId: null,
Purl: null,
Lang: "javascript",
Kind: "main",
Display: "main()",
BuildId: null,
Evidence: null,
Attributes: null,
SymbolDigest: null),
new(
Id: "function:process",
SymbolId: "process",
CodeId: null,
Purl: null,
Lang: "javascript",
Kind: "function",
Display: "process()",
BuildId: null,
Evidence: null,
Attributes: null,
SymbolDigest: null),
new(
Id: "vuln:component",
SymbolId: "vulnerable",
CodeId: null,
Purl: "pkg:npm/vulnerable@1.0.0",
Lang: "javascript",
Kind: "function",
Display: "vulnerable()",
BuildId: null,
Evidence: null,
Attributes: null,
SymbolDigest: null)
};
var edges = new List<RichGraphEdge>
{
new(
From: "entrypoint:main",
To: "function:process",
Kind: "call",
Purl: null,
SymbolDigest: null,
Evidence: null,
Confidence: 0.9,
Candidates: null),
new(
From: "function:process",
To: "vuln:component",
Kind: "call",
Purl: "pkg:npm/vulnerable@1.0.0",
SymbolDigest: null,
Evidence: null,
Confidence: 0.9,
Candidates: null)
};
return new RichGraph(
Nodes: nodes,
Edges: edges,
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null));
}
private static RichGraph CreateGraphWithoutPaths()
{
var nodes = new List<RichGraphNode>
{
new(
Id: "entrypoint:main",
SymbolId: "main",
CodeId: null,
Purl: null,
Lang: "javascript",
Kind: "main",
Display: "main()",
BuildId: null,
Evidence: null,
Attributes: null,
SymbolDigest: null),
new(
Id: "isolated:component",
SymbolId: "isolated",
CodeId: null,
Purl: "pkg:npm/isolated@1.0.0",
Lang: "javascript",
Kind: "function",
Display: "isolated()",
BuildId: null,
Evidence: null,
Attributes: null,
SymbolDigest: null)
};
// No edges - isolated component
var edges = new List<RichGraphEdge>();
return new RichGraph(
Nodes: nodes,
Edges: edges,
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null));
}
private static RichGraph CreateGraphWithRuntimeEvidence()
{
var nodes = new List<RichGraphNode>
{
new(
Id: "entrypoint:main",
SymbolId: "main",
CodeId: null,
Purl: null,
Lang: "javascript",
Kind: "main",
Display: "main()",
BuildId: null,
Evidence: null,
Attributes: null,
SymbolDigest: null),
new(
Id: "vuln:component",
SymbolId: "vulnerable",
CodeId: null,
Purl: "pkg:npm/vulnerable@1.0.0",
Lang: "javascript",
Kind: "function",
Display: "vulnerable()",
BuildId: null,
Evidence: null,
Attributes: null,
SymbolDigest: null)
};
var edges = new List<RichGraphEdge>
{
new(
From: "entrypoint:main",
To: "vuln:component",
Kind: "call",
Purl: "pkg:npm/vulnerable@1.0.0",
SymbolDigest: null,
Evidence: new[] { "runtime", "static" },
Confidence: 0.95,
Candidates: null)
};
return new RichGraph(
Nodes: nodes,
Edges: edges,
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null));
}
private static RichGraph CreateGraphWithManyPaths()
{
var nodes = new List<RichGraphNode>
{
new(
Id: "entrypoint:main",
SymbolId: "main",
CodeId: null,
Purl: null,
Lang: "javascript",
Kind: "main",
Display: "main()",
BuildId: null,
Evidence: null,
Attributes: null,
SymbolDigest: null),
new(
Id: "vuln:component",
SymbolId: "vulnerable",
CodeId: null,
Purl: "pkg:npm/vulnerable@1.0.0",
Lang: "javascript",
Kind: "function",
Display: "vulnerable()",
BuildId: null,
Evidence: null,
Attributes: null,
SymbolDigest: null)
};
// Add intermediate nodes to create multiple paths
for (int i = 1; i <= 10; i++)
{
nodes.Add(new RichGraphNode(
Id: $"function:intermediate{i}",
SymbolId: $"intermediate{i}",
CodeId: null,
Purl: null,
Lang: "javascript",
Kind: "function",
Display: $"intermediate{i}()",
BuildId: null,
Evidence: null,
Attributes: null,
SymbolDigest: null));
}
var edges = new List<RichGraphEdge>();
// Create multiple paths from main to vuln through different intermediates
for (int i = 1; i <= 10; i++)
{
edges.Add(new RichGraphEdge(
From: "entrypoint:main",
To: $"function:intermediate{i}",
Kind: "call",
Purl: null,
SymbolDigest: null,
Evidence: null,
Confidence: 0.9,
Candidates: null));
edges.Add(new RichGraphEdge(
From: $"function:intermediate{i}",
To: "vuln:component",
Kind: "call",
Purl: "pkg:npm/vulnerable@1.0.0",
SymbolDigest: null,
Evidence: null,
Confidence: 0.9,
Candidates: null));
}
return new RichGraph(
Nodes: nodes,
Edges: edges,
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null));
}
private static RichGraph CreateGraphWithDifferentEntrypoints()
{
var nodes = new List<RichGraphNode>
{
new(
Id: "entrypoint:http",
SymbolId: "handleRequest",
CodeId: null,
Purl: null,
Lang: "javascript",
Kind: "entrypoint",
Display: "handleRequest()",
BuildId: null,
Evidence: null,
Attributes: new Dictionary<string, string> { ["http_method"] = "POST" },
SymbolDigest: null),
new(
Id: "entrypoint:main",
SymbolId: "main",
CodeId: null,
Purl: null,
Lang: "javascript",
Kind: "main",
Display: "main()",
BuildId: null,
Evidence: null,
Attributes: null,
SymbolDigest: null),
new(
Id: "vuln:component",
SymbolId: "vulnerable",
CodeId: null,
Purl: "pkg:npm/vulnerable@1.0.0",
Lang: "javascript",
Kind: "function",
Display: "vulnerable()",
BuildId: null,
Evidence: null,
Attributes: null,
SymbolDigest: null)
};
var edges = new List<RichGraphEdge>
{
new(
From: "entrypoint:http",
To: "vuln:component",
Kind: "call",
Purl: "pkg:npm/vulnerable@1.0.0",
SymbolDigest: null,
Evidence: null,
Confidence: 0.9,
Candidates: null),
new(
From: "entrypoint:main",
To: "vuln:component",
Kind: "call",
Purl: "pkg:npm/vulnerable@1.0.0",
SymbolDigest: null,
Evidence: null,
Confidence: 0.9,
Candidates: null)
};
return new RichGraph(
Nodes: nodes,
Edges: edges,
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null));
}
}

View File

@@ -0,0 +1,97 @@
using System.Collections.Immutable;
using StellaOps.Scanner.Reachability.Gates;
using StellaOps.Scanner.Reachability.Subgraph;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests;
public sealed class ReachabilitySubgraphExtractorTests
{
[Fact]
public void Extract_BuildsSubgraphWithEntrypointAndVulnerable()
{
var graph = CreateGraph();
var request = new ReachabilitySubgraphRequest(
Graph: graph,
FindingKeys: ["CVE-2025-1234@pkg:npm/demo@1.0.0"],
TargetSymbols: ["sink"],
Entrypoints: []);
var extractor = new ReachabilitySubgraphExtractor();
var subgraph = extractor.Extract(request);
Assert.Equal(3, subgraph.Nodes.Length);
Assert.Equal(2, subgraph.Edges.Length);
Assert.Contains(subgraph.Nodes, n => n.Id == "root" && n.Type == ReachabilitySubgraphNodeType.Entrypoint);
Assert.Contains(subgraph.Nodes, n => n.Id == "sink" && n.Type == ReachabilitySubgraphNodeType.Vulnerable);
Assert.Contains(subgraph.Nodes, n => n.Id == "call" && n.Type == ReachabilitySubgraphNodeType.Call);
}
[Fact]
public void Extract_MapsGateMetadata()
{
var graph = CreateGraph(withGate: true);
var request = new ReachabilitySubgraphRequest(
Graph: graph,
FindingKeys: ["CVE-2025-1234@pkg:npm/demo@1.0.0"],
TargetSymbols: ["sink"],
Entrypoints: []);
var extractor = new ReachabilitySubgraphExtractor();
var subgraph = extractor.Extract(request);
var gatedEdge = subgraph.Edges.First(e => e.To == "sink");
Assert.NotNull(gatedEdge.Gate);
Assert.Equal("auth", gatedEdge.Gate!.GateType);
Assert.Equal("auth.check", gatedEdge.Gate.GuardSymbol);
}
[Fact]
public void Extract_WithNoTargets_ReturnsEmptySubgraph()
{
var graph = CreateGraph();
var request = new ReachabilitySubgraphRequest(
Graph: graph,
FindingKeys: [],
TargetSymbols: [],
Entrypoints: []);
var extractor = new ReachabilitySubgraphExtractor();
var subgraph = extractor.Extract(request);
Assert.Empty(subgraph.Nodes);
Assert.Empty(subgraph.Edges);
Assert.NotNull(subgraph.AnalysisMetadata);
}
private static RichGraph CreateGraph(bool withGate = false)
{
var nodes = new List<RichGraphNode>
{
new("root", "root", null, null, "csharp", "entrypoint", "root", null, null, null, null),
new("call", "call", null, null, "csharp", "call", "call", null, null, null, null),
new("sink", "sink", null, "pkg:npm/demo@1.0.0", "csharp", "sink", "sink", null, null, null, null)
};
var edges = new List<RichGraphEdge>
{
new("root", "call", "call", null, null, null, 0.9, null),
new("call", "sink", "call", null, null, null, 0.8, null,
withGate ? new[]
{
new DetectedGate
{
Type = GateType.AuthRequired,
Detail = "auth",
GuardSymbol = "auth.check",
Confidence = 0.9,
DetectionMethod = "static"
}
} : null)
};
var roots = new List<RichGraphRoot> { new("root", "runtime", null) };
var analyzer = new RichGraphAnalyzer("reachability", "1.0.0", null);
return new RichGraph(nodes, edges, roots, analyzer).Trimmed();
}
}

View File

@@ -0,0 +1,54 @@
using System.Collections.Immutable;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography;
using StellaOps.Scanner.Reachability.Attestation;
using StellaOps.Scanner.Reachability.Subgraph;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests;
public sealed class ReachabilitySubgraphPublisherTests
{
[Fact]
public async Task PublishAsync_BuildsDigestAndStoresInCas()
{
var subgraph = new ReachabilitySubgraph
{
FindingKeys = ["CVE-2025-1234@pkg:npm/demo@1.0.0"],
Nodes =
[
new ReachabilitySubgraphNode { Id = "root", Type = ReachabilitySubgraphNodeType.Entrypoint, Symbol = "root" },
new ReachabilitySubgraphNode { Id = "sink", Type = ReachabilitySubgraphNodeType.Vulnerable, Symbol = "sink" }
],
Edges =
[
new ReachabilitySubgraphEdge { From = "root", To = "sink", Type = "call", Confidence = 0.9 }
],
AnalysisMetadata = new ReachabilitySubgraphMetadata
{
Analyzer = "reachability",
AnalyzerVersion = "1.0.0",
Confidence = 0.9,
Completeness = "partial",
GeneratedAt = new DateTimeOffset(2025, 12, 22, 0, 0, 0, TimeSpan.Zero)
}
};
var options = Options.Create(new ReachabilitySubgraphOptions { Enabled = true, StoreInCas = true });
var cas = new FakeFileContentAddressableStore();
var publisher = new ReachabilitySubgraphPublisher(
options,
CryptoHashFactory.CreateDefault(),
NullLogger<ReachabilitySubgraphPublisher>.Instance,
cas: cas);
var result = await publisher.PublishAsync(subgraph, "sha256:subject");
Assert.False(string.IsNullOrWhiteSpace(result.SubgraphDigest));
Assert.False(string.IsNullOrWhiteSpace(result.AttestationDigest));
Assert.NotNull(result.CasUri);
Assert.NotEmpty(result.DsseEnvelopeBytes);
Assert.NotNull(cas.GetBytes(result.SubgraphDigest.Split(':')[1]));
}
}

View File

@@ -0,0 +1,69 @@
using StellaOps.Scanner.Reachability.Tests;
using StellaOps.Scanner.Reachability.Slices;
using System;
using StellaOps.Cryptography;
using StellaOps.Replay.Core;
using StellaOps.Scanner.ProofSpine;
using StellaOps.Scanner.ProofSpine.Options;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests.Slices;
[Trait("Category", "Slice")]
[Trait("Sprint", "3810")]
public sealed class SliceCasStorageTests
{
[Fact(DisplayName = "SliceCasStorage stores slice and DSSE envelope in CAS")]
public async Task StoreAsync_WritesSliceAndDsseToCas()
{
var cryptoHash = DefaultCryptoHash.CreateForTests();
var signer = CreateDeterministicSigner("slice-test-key");
var cryptoProfile = new TestCryptoProfile("slice-test-key", "hs256");
var hasher = new SliceHasher(cryptoHash);
var dsseSigner = new SliceDsseSigner(signer, cryptoProfile, hasher, new FixedTimeProvider());
var storage = new SliceCasStorage(hasher, dsseSigner, cryptoHash);
var cas = new FakeFileContentAddressableStore();
var slice = SliceTestData.CreateSlice();
var result = await storage.StoreAsync(slice, cas);
var key = ExtractDigestHex(result.SliceDigest);
Assert.NotNull(cas.GetBytes(key));
Assert.NotNull(cas.GetBytes(key + ".dsse"));
Assert.StartsWith("cas://slices/", result.SliceCasUri, StringComparison.Ordinal);
Assert.EndsWith(".dsse", result.DsseCasUri, StringComparison.Ordinal);
Assert.Equal(SliceSchema.DssePayloadType, result.SignedSlice.Envelope.PayloadType);
}
private static IDsseSigningService CreateDeterministicSigner(string keyId)
{
var options = Microsoft.Extensions.Options.Options.Create(new ProofSpineDsseSigningOptions
{
Mode = "hash",
KeyId = keyId,
Algorithm = "hs256",
AllowDeterministicFallback = true,
});
return new HmacDsseSigningService(
options,
DefaultCryptoHmac.CreateForTests(),
DefaultCryptoHash.CreateForTests());
}
private static string ExtractDigestHex(string prefixed)
{
var index = prefixed.IndexOf(':');
return index >= 0 ? prefixed[(index + 1)..] : prefixed;
}
private sealed record TestCryptoProfile(string KeyId, string Algorithm) : ICryptoProfile;
private sealed class FixedTimeProvider : TimeProvider
{
public override DateTimeOffset GetUtcNow() => new(2025, 12, 22, 10, 0, 0, TimeSpan.Zero);
}
}

View File

@@ -0,0 +1,54 @@
using StellaOps.Scanner.Reachability.Slices;
using System.Collections.Immutable;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests.Slices;
[Trait("Category", "Slice")]
[Trait("Sprint", "3810")]
public sealed class SliceExtractorTests
{
[Fact(DisplayName = "SliceExtractor returns reachable slice for entrypoint -> target path")]
public void Extract_WithPath_ReturnsReachableSlice()
{
var graph = SliceTestData.CreateGraph();
var request = new SliceExtractionRequest(
Graph: graph,
Inputs: SliceTestData.CreateInputs(),
Query: SliceTestData.CreateQuery(
targets: ImmutableArray.Create("target"),
entrypoints: ImmutableArray.Create("entry")),
Manifest: SliceTestData.CreateManifest());
var extractor = new SliceExtractor(new VerdictComputer());
var slice = extractor.Extract(request);
Assert.Equal(SliceVerdictStatus.Reachable, slice.Verdict.Status);
Assert.Contains(slice.Subgraph.Nodes, n => n.Id == "entry" && n.Kind == SliceNodeKind.Entrypoint);
Assert.Contains(slice.Subgraph.Nodes, n => n.Id == "target" && n.Kind == SliceNodeKind.Target);
Assert.DoesNotContain(slice.Subgraph.Nodes, n => n.Id == "other");
Assert.Equal(2, slice.Subgraph.Edges.Length);
Assert.Contains(slice.Verdict.PathWitnesses, witness => witness.Contains("entry", StringComparison.Ordinal));
}
[Fact(DisplayName = "SliceExtractor returns unknown verdict when entrypoints are missing")]
public void Extract_MissingEntrypoints_ReturnsUnknown()
{
var graph = SliceTestData.CreateGraph();
var request = new SliceExtractionRequest(
Graph: graph,
Inputs: SliceTestData.CreateInputs(),
Query: SliceTestData.CreateQuery(
targets: ImmutableArray.Create("target"),
entrypoints: ImmutableArray.Create("missing")),
Manifest: SliceTestData.CreateManifest());
var extractor = new SliceExtractor(new VerdictComputer());
var slice = extractor.Extract(request);
Assert.Equal(SliceVerdictStatus.Unknown, slice.Verdict.Status);
Assert.Contains("missing_entrypoints", slice.Verdict.Reasons);
}
}

View File

@@ -0,0 +1,45 @@
using StellaOps.Scanner.Reachability.Slices;
using System.Collections.Immutable;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests.Slices;
[Trait("Category", "Determinism")]
[Trait("Sprint", "3810")]
public sealed class SliceHasherTests
{
[Fact(DisplayName = "SliceHasher produces deterministic bytes across ordering differences")]
public void ComputeDigest_IsDeterministicAcrossOrdering()
{
var nodesA = ImmutableArray.Create(
new SliceNode { Id = "node:2", Symbol = "b", Kind = SliceNodeKind.Intermediate },
new SliceNode { Id = "node:1", Symbol = "a", Kind = SliceNodeKind.Entrypoint },
new SliceNode { Id = "node:3", Symbol = "c", Kind = SliceNodeKind.Target });
var nodesB = ImmutableArray.Create(
new SliceNode { Id = "node:3", Symbol = "c", Kind = SliceNodeKind.Target },
new SliceNode { Id = "node:1", Symbol = "a", Kind = SliceNodeKind.Entrypoint },
new SliceNode { Id = "node:2", Symbol = "b", Kind = SliceNodeKind.Intermediate });
var edgesA = ImmutableArray.Create(
new SliceEdge { From = "node:2", To = "node:3", Kind = SliceEdgeKind.Direct, Confidence = 0.9 },
new SliceEdge { From = "node:1", To = "node:2", Kind = SliceEdgeKind.Direct, Confidence = 1.0 });
var edgesB = ImmutableArray.Create(
new SliceEdge { From = "node:1", To = "node:2", Kind = SliceEdgeKind.Direct, Confidence = 1.0 },
new SliceEdge { From = "node:2", To = "node:3", Kind = SliceEdgeKind.Direct, Confidence = 0.9 });
var sliceA = SliceTestData.CreateSlice(nodesA, edgesA);
var sliceB = SliceTestData.CreateSlice(nodesB, edgesB);
var hasher = new SliceHasher(DefaultCryptoHash.CreateForTests());
var digestA = hasher.ComputeDigest(sliceA);
var digestB = hasher.ComputeDigest(sliceB);
Assert.Equal(digestA.Digest, digestB.Digest);
Assert.Equal(digestA.CanonicalBytes, digestB.CanonicalBytes);
}
}

View File

@@ -0,0 +1,105 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using FluentAssertions;
using Json.Schema;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests.Slices;
[Trait("Category", "Schema")]
[Trait("Sprint", "3810")]
public sealed class SliceSchemaValidationTests
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false
};
[Fact(DisplayName = "Valid ReachabilitySlice passes schema validation")]
public void ValidSlice_PassesValidation()
{
var schema = LoadSchema();
var slice = SliceTestData.CreateSlice();
var json = JsonSerializer.Serialize(slice, JsonOptions);
var node = JsonDocument.Parse(json).RootElement;
var result = schema.Evaluate(node);
result.IsValid.Should().BeTrue("valid slices should pass schema validation");
}
[Fact(DisplayName = "Slice missing required fields fails validation")]
public void MissingRequiredField_FailsValidation()
{
var schema = LoadSchema();
var json = """
{
"_type": "stellaops.dev/predicates/reachability-slice@v1",
"inputs": { "graphDigest": "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd" }
}
""";
var node = JsonDocument.Parse(json).RootElement;
var result = schema.Evaluate(node);
result.IsValid.Should().BeFalse("missing required subgraph/verdict/manifest should fail validation");
}
[Fact(DisplayName = "Slice with invalid verdict status fails validation")]
public void InvalidVerdictStatus_FailsValidation()
{
var schema = LoadSchema();
var json = """
{
"_type": "stellaops.dev/predicates/reachability-slice@v1",
"inputs": { "graphDigest": "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd" },
"query": { "cveId": "CVE-2024-1234" },
"subgraph": { "nodes": [], "edges": [] },
"verdict": { "status": "invalid", "confidence": 0.5 },
"manifest": {
"scanId": "scan-1",
"createdAtUtc": "2025-12-22T10:00:00Z",
"artifactDigest": "sha256:00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
"scannerVersion": "scanner.native:1.2.0",
"workerVersion": "scanner.worker:1.2.0",
"concelierSnapshotHash": "sha256:1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff",
"excititorSnapshotHash": "sha256:2222333344445555666677778888999900001111aaaabbbbccccddddeeeeffff",
"latticePolicyHash": "sha256:3333444455556666777788889999000011112222aaaabbbbccccddddeeeeffff",
"deterministic": true,
"seed": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"knobs": { "maxDepth": "20" }
}
}
""";
var node = JsonDocument.Parse(json).RootElement;
var result = schema.Evaluate(node);
result.IsValid.Should().BeFalse("invalid verdict status should fail validation");
}
private static JsonSchema LoadSchema()
{
var schemaPath = FindSchemaPath();
var json = File.ReadAllText(schemaPath);
return JsonSchema.FromText(json);
}
private static string FindSchemaPath()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null)
{
var candidate = Path.Combine(dir.FullName, "docs", "schemas", "stellaops-slice.v1.schema.json");
if (File.Exists(candidate))
{
return candidate;
}
dir = dir.Parent;
}
throw new FileNotFoundException("Could not locate stellaops-slice.v1.schema.json from test directory.");
}
}

View File

@@ -0,0 +1,109 @@
using System.Collections.Immutable;
using StellaOps.Scanner.Reachability;
using StellaOps.Scanner.Reachability.Slices;
using StellaOps.Scanner.Core;
namespace StellaOps.Scanner.Reachability.Tests.Slices;
internal static class SliceTestData
{
private static readonly DateTimeOffset FixedTimestamp = new(2025, 12, 22, 10, 0, 0, TimeSpan.Zero);
public static ScanManifest CreateManifest(
string scanId = "scan-1",
string artifactDigest = "sha256:00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
{
var seed = new byte[32];
var builder = ScanManifest.CreateBuilder(scanId, artifactDigest)
.WithCreatedAt(FixedTimestamp)
.WithArtifactPurl("pkg:generic/app@1.0.0")
.WithScannerVersion("scanner.native:1.2.0")
.WithWorkerVersion("scanner.worker:1.2.0")
.WithConcelierSnapshot("sha256:1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff")
.WithExcititorSnapshot("sha256:2222333344445555666677778888999900001111aaaabbbbccccddddeeeeffff")
.WithLatticePolicyHash("sha256:3333444455556666777788889999000011112222aaaabbbbccccddddeeeeffff")
.WithDeterministic(true)
.WithSeed(seed)
.WithKnob("maxDepth", "20");
return builder.Build();
}
public static SliceInputs CreateInputs()
{
return new SliceInputs
{
GraphDigest = "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
BinaryDigests = ImmutableArray.Create(
"sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
SbomDigest = "sha256:cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe"
};
}
public static SliceQuery CreateQuery(
ImmutableArray<string>? targets = null,
ImmutableArray<string>? entrypoints = null)
{
return new SliceQuery
{
CveId = "CVE-2024-1234",
TargetSymbols = targets ?? ImmutableArray.Create("openssl:EVP_PKEY_decrypt"),
Entrypoints = entrypoints ?? ImmutableArray.Create("main")
};
}
public static ReachabilitySlice CreateSlice(
ImmutableArray<SliceNode>? nodes = null,
ImmutableArray<SliceEdge>? edges = null)
{
return new ReachabilitySlice
{
Inputs = CreateInputs(),
Query = CreateQuery(),
Subgraph = new SliceSubgraph
{
Nodes = nodes ?? ImmutableArray.Create(
new SliceNode { Id = "node:1", Symbol = "main", Kind = SliceNodeKind.Entrypoint },
new SliceNode { Id = "node:2", Symbol = "decrypt_data", Kind = SliceNodeKind.Intermediate },
new SliceNode { Id = "node:3", Symbol = "EVP_PKEY_decrypt", Kind = SliceNodeKind.Target }
),
Edges = edges ?? ImmutableArray.Create(
new SliceEdge { From = "node:1", To = "node:2", Kind = SliceEdgeKind.Direct, Confidence = 1.0 },
new SliceEdge { From = "node:2", To = "node:3", Kind = SliceEdgeKind.Plt, Confidence = 0.9 }
)
},
Verdict = new SliceVerdict
{
Status = SliceVerdictStatus.Reachable,
Confidence = 0.9,
Reasons = ImmutableArray.Create("path_exists_high_confidence"),
PathWitnesses = ImmutableArray.Create("main -> decrypt_data -> EVP_PKEY_decrypt"),
UnknownCount = 0
},
Manifest = CreateManifest()
};
}
public static RichGraph CreateGraph()
{
var nodes = new[]
{
new RichGraphNode("entry", "entry", null, null, "native", "method", "entry", null, null, null, null),
new RichGraphNode("mid", "mid", null, null, "native", "method", "mid", null, null, null, null),
new RichGraphNode("target", "target", null, null, "native", "method", "target", null, null, null, null),
new RichGraphNode("other", "other", null, null, "native", "method", "other", null, null, null, null)
};
var edges = new[]
{
new RichGraphEdge("entry", "mid", "call", null, null, null, 0.95, null),
new RichGraphEdge("mid", "target", "call", null, null, null, 0.9, null),
new RichGraphEdge("other", "mid", "call", null, null, null, 0.5, null)
};
var roots = new[] { new RichGraphRoot("entry", "runtime", null) };
return new RichGraph(nodes, edges, roots, new RichGraphAnalyzer("slice-test", "1.0.0", null));
}
}

View File

@@ -0,0 +1,40 @@
using StellaOps.Scanner.Reachability.Slices;
using Xunit;
namespace StellaOps.Scanner.Reachability.Tests.Slices;
[Trait("Category", "Slice")]
[Trait("Sprint", "3810")]
public sealed class SliceVerdictComputerTests
{
[Fact(DisplayName = "VerdictComputer returns reachable when path is strong and no unknowns")]
public void Compute_ReturnsReachable()
{
var paths = new[] { new SlicePathSummary("path-1", 0.85, "entry -> target") };
var verdict = new VerdictComputer().Compute(paths, unknownEdgeCount: 0);
Assert.Equal(SliceVerdictStatus.Reachable, verdict.Status);
Assert.Contains("path_exists_high_confidence", verdict.Reasons);
}
[Fact(DisplayName = "VerdictComputer returns unreachable when no paths and no unknowns")]
public void Compute_ReturnsUnreachable()
{
var verdict = new VerdictComputer().Compute(Array.Empty<SlicePathSummary>(), unknownEdgeCount: 0);
Assert.Equal(SliceVerdictStatus.Unreachable, verdict.Status);
Assert.Contains("no_paths_found", verdict.Reasons);
}
[Fact(DisplayName = "VerdictComputer returns unknown when unknown edges exist")]
public void Compute_ReturnsUnknownWhenUnknownEdgesPresent()
{
var paths = new[] { new SlicePathSummary("path-1", 0.9, "entry -> target") };
var verdict = new VerdictComputer().Compute(paths, unknownEdgeCount: 2);
Assert.Equal(SliceVerdictStatus.Unknown, verdict.Status);
Assert.Contains("unknown_edges:2", verdict.Reasons);
}
}

View File

@@ -9,6 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="JsonSchema.Net" Version="7.3.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />