consolidate the tests locations

This commit is contained in:
StellaOps Bot
2025-12-26 01:48:24 +02:00
parent 17613acf57
commit 39359da171
2031 changed files with 2607 additions and 476 deletions

View File

@@ -1,111 +0,0 @@
# PoE Golden Fixtures
This directory contains golden test fixtures for Proof of Exposure (PoE) determinism testing.
## Purpose
Golden fixtures serve as:
1. **Determinism Tests**: Verify that PoE generation produces identical output for identical inputs
2. **Regression Tests**: Detect unintended changes to PoE format or content
3. **Documentation**: Show real-world examples of PoE artifacts
## Fixtures
| Fixture | Description | Size | Paths | Nodes | Edges |
|---------|-------------|------|-------|-------|-------|
| `log4j-cve-2021-44228.poe.golden.json` | Log4j RCE with single path | ~2.5 KB | 1 | 4 | 3 |
| `multi-path-java.poe.golden.json` | Java with 3 alternative paths | ~8 KB | 3 | 12 | 18 |
| `guarded-path-dotnet.poe.golden.json` | .NET with feature flag guards | ~5 KB | 2 | 8 | 10 |
| `stripped-binary-c.poe.golden.json` | C/C++ stripped binary (code_id) | ~6 KB | 1 | 6 | 5 |
## Hash Verification
Each fixture has a known BLAKE3-256 hash for integrity verification:
```
log4j-cve-2021-44228.poe.golden.json:
blake3: 7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b
sha256: abc123def456789012345678901234567890123456789012345678901234567890
```
## Usage in Tests
### Determinism Test
```csharp
[Fact]
public async Task PoEGeneration_WithSameInputs_ProducesSameHash()
{
var goldenPath = "Fixtures/log4j-cve-2021-44228.poe.golden.json";
var goldenBytes = await File.ReadAllBytesAsync(goldenPath);
var goldenHash = ComputeBlake3Hash(goldenBytes);
// Generate PoE from test inputs
var generatedPoe = await GeneratePoE(testInputs);
var generatedHash = ComputeBlake3Hash(generatedPoe);
Assert.Equal(goldenHash, generatedHash);
}
```
### Regression Test
```csharp
[Fact]
public async Task PoEGeneration_Schema_MatchesGolden()
{
var goldenPath = "Fixtures/log4j-cve-2021-44228.poe.golden.json";
var golden = await LoadPoE(goldenPath);
// Generate PoE from test inputs
var generated = await GeneratePoE(testInputs);
// Schema should match (structure, field types)
Assert.Equal(golden.Schema, generated.Schema);
Assert.Equal(golden.Subject.VulnId, generated.Subject.VulnId);
Assert.Equal(golden.Subgraph.Nodes.Count, generated.Subgraph.Nodes.Count);
}
```
## Generating New Fixtures
To create a new golden fixture:
1. **Run scanner on test image:**
```bash
stella scan --image test/log4j:vulnerable --emit-poe --output ./test-output/
```
2. **Extract PoE artifact:**
```bash
cp ./test-output/poe.json ./Fixtures/new-fixture.poe.golden.json
```
3. **Verify determinism:**
```bash
# Run scan again
stella scan --image test/log4j:vulnerable --emit-poe --output ./test-output2/
# Compare hashes
sha256sum ./test-output/poe.json ./test-output2/poe.json
# Hashes MUST match for determinism
```
4. **Commit fixture:**
```bash
git add ./Fixtures/new-fixture.poe.golden.json
git commit -m "Add golden fixture: new-fixture"
```
## Maintenance
- **Update fixtures** when PoE schema version changes (schema field)
- **Regenerate fixtures** when canonical JSON format changes
- **Verify hashes** after any changes to serialization logic
- **Document breaking changes** in fixture commit messages
## Related Documentation
- [POE_PREDICATE_SPEC.md](../../../../src/Attestor/POE_PREDICATE_SPEC.md) - PoE schema specification
- [SUBGRAPH_EXTRACTION.md](../../../../src/Scanner/__Libraries/StellaOps.Scanner.Reachability/SUBGRAPH_EXTRACTION.md) - Extraction algorithm
- [PoEArtifactGeneratorTests.cs](../../../../src/Attestor/__Tests/PoEArtifactGeneratorTests.cs) - Unit tests using fixtures

View File

@@ -1,192 +0,0 @@
{
"@type": "https://stellaops.dev/predicates/proof-of-exposure@v1",
"evidence": {
"graphHash": "blake3:f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5",
"sbomRef": "cas://scanner-artifacts/sbom.cdx.json"
},
"metadata": {
"analyzer": {
"name": "stellaops-scanner",
"toolchainDigest": "sha256:567890123456789012345678901234567890123456789012345678901234",
"version": "1.2.0"
},
"generatedAt": "2025-12-23T12:15:00.000Z",
"policy": {
"evaluatedAt": "2025-12-23T12:13:00.000Z",
"policyDigest": "sha256:890123456789012345678901234567890123456789012345678901234567",
"policyId": "prod-release-v42"
},
"reproSteps": [
"1. Build container image from Dockerfile (commit: ghi789)",
"2. Run scanner with config: etc/scanner.yaml (includeGuards=true)",
"3. Extract reachability graph with maxDepth=10",
"4. Resolve CVE-2024-56789 to vulnerable symbols with guard predicates"
]
},
"schema": "stellaops.dev/poe@v1",
"subject": {
"buildId": "gnu-build-id:9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f",
"componentRef": "pkg:nuget/VulnerableLib@2.3.1",
"imageDigest": "sha256:ghi789012345678901234567890123456789012345678901234567890123",
"vulnId": "CVE-2024-56789"
},
"subgraph": {
"edges": [
{
"confidence": 0.98,
"from": "sym:csharp:WebApi.Controllers.PaymentController.ProcessPayment",
"guards": [],
"to": "sym:csharp:WebApi.Services.PaymentService.Charge"
},
{
"confidence": 0.96,
"from": "sym:csharp:WebApi.Services.PaymentService.Charge",
"guards": [
"FeatureFlags.EnableLegacyPayment"
],
"to": "sym:csharp:WebApi.Legacy.LegacyPaymentAdapter.ProcessLegacy"
},
{
"confidence": 0.94,
"from": "sym:csharp:WebApi.Legacy.LegacyPaymentAdapter.ProcessLegacy",
"guards": [],
"to": "sym:csharp:VulnerableLib.Crypto.InsecureHasher.ComputeHash"
},
{
"confidence": 0.97,
"from": "sym:csharp:WebApi.Controllers.PaymentController.ProcessPayment",
"guards": [],
"to": "sym:csharp:WebApi.Services.PaymentService.Validate"
},
{
"confidence": 0.95,
"from": "sym:csharp:WebApi.Services.PaymentService.Validate",
"guards": [
"RuntimeInformation.IsOSPlatform(OSPlatform.Windows)"
],
"to": "sym:csharp:WebApi.Validation.WindowsValidator.CheckSignature"
},
{
"confidence": 0.93,
"from": "sym:csharp:WebApi.Validation.WindowsValidator.CheckSignature",
"guards": [],
"to": "sym:csharp:VulnerableLib.Crypto.InsecureHasher.ComputeHash"
},
{
"confidence": 0.92,
"from": "sym:csharp:WebApi.Controllers.AdminController.MigrateData",
"guards": [
"Environment.GetEnvironmentVariable(\"ENABLE_MIGRATION\") == \"true\""
],
"to": "sym:csharp:WebApi.Migration.DataMigrator.ExecuteMigration"
},
{
"confidence": 0.90,
"from": "sym:csharp:WebApi.Migration.DataMigrator.ExecuteMigration",
"guards": [],
"to": "sym:csharp:WebApi.Legacy.LegacyDataConverter.ConvertFormat"
},
{
"confidence": 0.88,
"from": "sym:csharp:WebApi.Legacy.LegacyDataConverter.ConvertFormat",
"guards": [],
"to": "sym:csharp:VulnerableLib.Crypto.InsecureHasher.ComputeHash"
},
{
"confidence": 0.87,
"from": "sym:csharp:WebApi.Services.PaymentService.Charge",
"guards": [],
"to": "sym:csharp:WebApi.Logging.AuditLogger.LogTransaction"
}
],
"entryRefs": [
"sym:csharp:WebApi.Controllers.PaymentController.ProcessPayment",
"sym:csharp:WebApi.Controllers.AdminController.MigrateData"
],
"nodes": [
{
"addr": "0x601000",
"file": "PaymentController.cs",
"id": "sym:csharp:WebApi.Controllers.PaymentController.ProcessPayment",
"line": 56,
"moduleHash": "sha256:601234567890123456789012345678901234567890123456789012345678",
"symbol": "WebApi.Controllers.PaymentController.ProcessPayment(PaymentRequest)"
},
{
"addr": "0x602000",
"file": "AdminController.cs",
"id": "sym:csharp:WebApi.Controllers.AdminController.MigrateData",
"line": 89,
"moduleHash": "sha256:601234567890123456789012345678901234567890123456789012345678",
"symbol": "WebApi.Controllers.AdminController.MigrateData()"
},
{
"addr": "0x603000",
"file": "PaymentService.cs",
"id": "sym:csharp:WebApi.Services.PaymentService.Charge",
"line": 123,
"moduleHash": "sha256:612345678901234567890123456789012345678901234567890123456789",
"symbol": "WebApi.Services.PaymentService.Charge(decimal, string)"
},
{
"addr": "0x603100",
"file": "PaymentService.cs",
"id": "sym:csharp:WebApi.Services.PaymentService.Validate",
"line": 167,
"moduleHash": "sha256:612345678901234567890123456789012345678901234567890123456789",
"symbol": "WebApi.Services.PaymentService.Validate(PaymentRequest)"
},
{
"addr": "0x604000",
"file": "LegacyPaymentAdapter.cs",
"id": "sym:csharp:WebApi.Legacy.LegacyPaymentAdapter.ProcessLegacy",
"line": 78,
"moduleHash": "sha256:623456789012345678901234567890123456789012345678901234567890",
"symbol": "WebApi.Legacy.LegacyPaymentAdapter.ProcessLegacy(LegacyPayment)"
},
{
"addr": "0x605000",
"file": "WindowsValidator.cs",
"id": "sym:csharp:WebApi.Validation.WindowsValidator.CheckSignature",
"line": 45,
"moduleHash": "sha256:634567890123456789012345678901234567890123456789012345678901",
"symbol": "WebApi.Validation.WindowsValidator.CheckSignature(byte[])"
},
{
"addr": "0x606000",
"file": "DataMigrator.cs",
"id": "sym:csharp:WebApi.Migration.DataMigrator.ExecuteMigration",
"line": 234,
"moduleHash": "sha256:645678901234567890123456789012345678901234567890123456789012",
"symbol": "WebApi.Migration.DataMigrator.ExecuteMigration(MigrationConfig)"
},
{
"addr": "0x607000",
"file": "LegacyDataConverter.cs",
"id": "sym:csharp:WebApi.Legacy.LegacyDataConverter.ConvertFormat",
"line": 156,
"moduleHash": "sha256:623456789012345678901234567890123456789012345678901234567890",
"symbol": "WebApi.Legacy.LegacyDataConverter.ConvertFormat(byte[])"
},
{
"addr": "0x608000",
"file": "InsecureHasher.cs",
"id": "sym:csharp:VulnerableLib.Crypto.InsecureHasher.ComputeHash",
"line": 67,
"moduleHash": "sha256:656789012345678901234567890123456789012345678901234567890123",
"symbol": "VulnerableLib.Crypto.InsecureHasher.ComputeHash(byte[])"
},
{
"addr": "0x609000",
"file": "AuditLogger.cs",
"id": "sym:csharp:WebApi.Logging.AuditLogger.LogTransaction",
"line": 91,
"moduleHash": "sha256:612345678901234567890123456789012345678901234567890123456789",
"symbol": "WebApi.Logging.AuditLogger.LogTransaction(string, decimal)"
}
],
"sinkRefs": [
"sym:csharp:VulnerableLib.Crypto.InsecureHasher.ComputeHash"
]
}
}

View File

@@ -1,92 +0,0 @@
{
"@type": "https://stellaops.dev/predicates/proof-of-exposure@v1",
"evidence": {
"graphHash": "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234",
"sbomRef": "cas://scanner-artifacts/sbom.cdx.json"
},
"metadata": {
"analyzer": {
"name": "stellaops-scanner",
"toolchainDigest": "sha256:def456789012345678901234567890123456789012345678901234567890",
"version": "1.2.0"
},
"generatedAt": "2025-12-23T10:00:00.000Z",
"policy": {
"evaluatedAt": "2025-12-23T09:58:00.000Z",
"policyDigest": "sha256:abc123456789012345678901234567890123456789012345678901234567",
"policyId": "prod-release-v42"
},
"reproSteps": [
"1. Build container image from Dockerfile (commit: abc123)",
"2. Run scanner with config: etc/scanner.yaml",
"3. Extract reachability graph with maxDepth=10",
"4. Resolve CVE-2021-44228 to vulnerable symbols"
]
},
"schema": "stellaops.dev/poe@v1",
"subject": {
"buildId": "gnu-build-id:5f0c7c3c4d5e6f7a8b9c0d1e2f3a4b5c",
"componentRef": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
"imageDigest": "sha256:abc123def456789012345678901234567890123456789012345678901234",
"vulnId": "CVE-2021-44228"
},
"subgraph": {
"edges": [
{
"confidence": 0.98,
"from": "sym:java:com.example.GreetingService.greet",
"to": "sym:java:com.example.GreetingService.processRequest"
},
{
"confidence": 0.95,
"from": "sym:java:com.example.GreetingService.processRequest",
"to": "sym:java:org.apache.logging.log4j.Logger.error"
},
{
"confidence": 0.92,
"from": "sym:java:org.apache.logging.log4j.Logger.error",
"to": "sym:java:org.apache.logging.log4j.core.lookup.JndiLookup.lookup"
}
],
"entryRefs": [
"sym:java:com.example.GreetingService.greet"
],
"nodes": [
{
"addr": "0x401000",
"file": "GreetingService.java",
"id": "sym:java:com.example.GreetingService.greet",
"line": 42,
"moduleHash": "sha256:abc123456789012345678901234567890123456789012345678901234567",
"symbol": "com.example.GreetingService.greet(String)"
},
{
"addr": "0x401100",
"file": "GreetingService.java",
"id": "sym:java:com.example.GreetingService.processRequest",
"line": 58,
"moduleHash": "sha256:abc123456789012345678901234567890123456789012345678901234567",
"symbol": "com.example.GreetingService.processRequest(String)"
},
{
"addr": "0x402000",
"file": "Logger.java",
"id": "sym:java:org.apache.logging.log4j.Logger.error",
"line": 128,
"moduleHash": "sha256:def456789012345678901234567890123456789012345678901234567890",
"symbol": "org.apache.logging.log4j.Logger.error(String)"
},
{
"addr": "0x403000",
"file": "JndiLookup.java",
"id": "sym:java:org.apache.logging.log4j.core.lookup.JndiLookup.lookup",
"line": 56,
"moduleHash": "sha256:def456789012345678901234567890123456789012345678901234567890",
"symbol": "org.apache.logging.log4j.core.lookup.JndiLookup.lookup(LogEvent, String)"
}
],
"sinkRefs": [
"sym:java:org.apache.logging.log4j.core.lookup.JndiLookup.lookup"
]
}
}

View File

@@ -1,257 +0,0 @@
{
"@type": "https://stellaops.dev/predicates/proof-of-exposure@v1",
"evidence": {
"graphHash": "blake3:e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3",
"sbomRef": "cas://scanner-artifacts/sbom.cdx.json"
},
"metadata": {
"analyzer": {
"name": "stellaops-scanner",
"toolchainDigest": "sha256:456789012345678901234567890123456789012345678901234567890123",
"version": "1.2.0"
},
"generatedAt": "2025-12-23T11:30:00.000Z",
"policy": {
"evaluatedAt": "2025-12-23T11:28:00.000Z",
"policyDigest": "sha256:789012345678901234567890123456789012345678901234567890123456",
"policyId": "prod-release-v42"
},
"reproSteps": [
"1. Build container image from Dockerfile (commit: def456)",
"2. Run scanner with config: etc/scanner.yaml",
"3. Extract reachability graph with maxDepth=10, maxPaths=3",
"4. Resolve CVE-2023-12345 to vulnerable symbols"
]
},
"schema": "stellaops.dev/poe@v1",
"subject": {
"buildId": "gnu-build-id:7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
"componentRef": "pkg:maven/com.example/vulnerable-lib@1.5.0",
"imageDigest": "sha256:def456789012345678901234567890123456789012345678901234567890",
"vulnId": "CVE-2023-12345"
},
"subgraph": {
"edges": [
{
"confidence": 0.98,
"from": "sym:java:com.example.api.UserController.getUser",
"to": "sym:java:com.example.service.UserService.fetchUser"
},
{
"confidence": 0.95,
"from": "sym:java:com.example.service.UserService.fetchUser",
"to": "sym:java:com.example.util.XmlParser.parse"
},
{
"confidence": 0.92,
"from": "sym:java:com.example.util.XmlParser.parse",
"to": "sym:java:com.vulnerable.XXEVulnerableParser.parseXml"
},
{
"confidence": 0.97,
"from": "sym:java:com.example.api.UserController.updateUser",
"to": "sym:java:com.example.service.UserService.updateProfile"
},
{
"confidence": 0.94,
"from": "sym:java:com.example.service.UserService.updateProfile",
"to": "sym:java:com.example.util.XmlParser.parse"
},
{
"confidence": 0.96,
"from": "sym:java:com.example.api.AdminController.importUsers",
"to": "sym:java:com.example.service.ImportService.processXml"
},
{
"confidence": 0.93,
"from": "sym:java:com.example.service.ImportService.processXml",
"to": "sym:java:com.example.util.XmlParser.parseStream"
},
{
"confidence": 0.91,
"from": "sym:java:com.example.util.XmlParser.parseStream",
"to": "sym:java:com.vulnerable.XXEVulnerableParser.parseXml"
},
{
"confidence": 0.89,
"from": "sym:java:com.example.api.UserController.getUser",
"to": "sym:java:com.example.cache.CacheService.getCachedUser"
},
{
"confidence": 0.87,
"from": "sym:java:com.example.cache.CacheService.getCachedUser",
"to": "sym:java:com.example.serialization.Deserializer.fromXml"
},
{
"confidence": 0.85,
"from": "sym:java:com.example.serialization.Deserializer.fromXml",
"to": "sym:java:com.example.util.XmlParser.parse"
},
{
"confidence": 0.88,
"from": "sym:java:com.example.api.AdminController.importUsers",
"to": "sym:java:com.example.validation.XmlValidator.validate"
},
{
"confidence": 0.86,
"from": "sym:java:com.example.validation.XmlValidator.validate",
"to": "sym:java:com.vulnerable.XXEVulnerableParser.parseXml"
},
{
"confidence": 0.90,
"from": "sym:java:com.example.service.UserService.fetchUser",
"to": "sym:java:com.example.logging.AuditLogger.logAccess"
},
{
"confidence": 0.84,
"from": "sym:java:com.example.logging.AuditLogger.logAccess",
"to": "sym:java:com.example.util.XmlParser.parseConfig"
},
{
"confidence": 0.82,
"from": "sym:java:com.example.util.XmlParser.parseConfig",
"to": "sym:java:com.vulnerable.XXEVulnerableParser.parseXml"
},
{
"confidence": 0.95,
"from": "sym:java:com.example.service.ImportService.processXml",
"to": "sym:java:com.example.transform.XsltTransformer.transform"
},
{
"confidence": 0.88,
"from": "sym:java:com.example.transform.XsltTransformer.transform",
"to": "sym:java:com.vulnerable.XXEVulnerableParser.parseXml"
}
],
"entryRefs": [
"sym:java:com.example.api.UserController.getUser",
"sym:java:com.example.api.UserController.updateUser",
"sym:java:com.example.api.AdminController.importUsers"
],
"nodes": [
{
"addr": "0x501000",
"file": "UserController.java",
"id": "sym:java:com.example.api.UserController.getUser",
"line": 45,
"moduleHash": "sha256:123456789012345678901234567890123456789012345678901234567890",
"symbol": "com.example.api.UserController.getUser(String)"
},
{
"addr": "0x501100",
"file": "UserController.java",
"id": "sym:java:com.example.api.UserController.updateUser",
"line": 67,
"moduleHash": "sha256:123456789012345678901234567890123456789012345678901234567890",
"symbol": "com.example.api.UserController.updateUser(String, UserData)"
},
{
"addr": "0x502000",
"file": "AdminController.java",
"id": "sym:java:com.example.api.AdminController.importUsers",
"line": 89,
"moduleHash": "sha256:123456789012345678901234567890123456789012345678901234567890",
"symbol": "com.example.api.AdminController.importUsers(InputStream)"
},
{
"addr": "0x503000",
"file": "UserService.java",
"id": "sym:java:com.example.service.UserService.fetchUser",
"line": 34,
"moduleHash": "sha256:234567890123456789012345678901234567890123456789012345678901",
"symbol": "com.example.service.UserService.fetchUser(String)"
},
{
"addr": "0x503100",
"file": "UserService.java",
"id": "sym:java:com.example.service.UserService.updateProfile",
"line": 78,
"moduleHash": "sha256:234567890123456789012345678901234567890123456789012345678901",
"symbol": "com.example.service.UserService.updateProfile(String, UserData)"
},
{
"addr": "0x504000",
"file": "ImportService.java",
"id": "sym:java:com.example.service.ImportService.processXml",
"line": 56,
"moduleHash": "sha256:234567890123456789012345678901234567890123456789012345678901",
"symbol": "com.example.service.ImportService.processXml(InputStream)"
},
{
"addr": "0x505000",
"file": "XmlParser.java",
"id": "sym:java:com.example.util.XmlParser.parse",
"line": 112,
"moduleHash": "sha256:345678901234567890123456789012345678901234567890123456789012",
"symbol": "com.example.util.XmlParser.parse(String)"
},
{
"addr": "0x505100",
"file": "XmlParser.java",
"id": "sym:java:com.example.util.XmlParser.parseStream",
"line": 145,
"moduleHash": "sha256:345678901234567890123456789012345678901234567890123456789012",
"symbol": "com.example.util.XmlParser.parseStream(InputStream)"
},
{
"addr": "0x505200",
"file": "XmlParser.java",
"id": "sym:java:com.example.util.XmlParser.parseConfig",
"line": 178,
"moduleHash": "sha256:345678901234567890123456789012345678901234567890123456789012",
"symbol": "com.example.util.XmlParser.parseConfig(File)"
},
{
"addr": "0x506000",
"file": "XXEVulnerableParser.java",
"id": "sym:java:com.vulnerable.XXEVulnerableParser.parseXml",
"line": 67,
"moduleHash": "sha256:456789012345678901234567890123456789012345678901234567890123",
"symbol": "com.vulnerable.XXEVulnerableParser.parseXml(InputSource)"
},
{
"addr": "0x507000",
"file": "CacheService.java",
"id": "sym:java:com.example.cache.CacheService.getCachedUser",
"line": 89,
"moduleHash": "sha256:234567890123456789012345678901234567890123456789012345678901",
"symbol": "com.example.cache.CacheService.getCachedUser(String)"
},
{
"addr": "0x508000",
"file": "Deserializer.java",
"id": "sym:java:com.example.serialization.Deserializer.fromXml",
"line": 123,
"moduleHash": "sha256:345678901234567890123456789012345678901234567890123456789012",
"symbol": "com.example.serialization.Deserializer.fromXml(String)"
},
{
"addr": "0x509000",
"file": "XmlValidator.java",
"id": "sym:java:com.example.validation.XmlValidator.validate",
"line": 45,
"moduleHash": "sha256:345678901234567890123456789012345678901234567890123456789012",
"symbol": "com.example.validation.XmlValidator.validate(InputStream)"
},
{
"addr": "0x50A000",
"file": "AuditLogger.java",
"id": "sym:java:com.example.logging.AuditLogger.logAccess",
"line": 78,
"moduleHash": "sha256:234567890123456789012345678901234567890123456789012345678901",
"symbol": "com.example.logging.AuditLogger.logAccess(String, String)"
},
{
"addr": "0x50B000",
"file": "XsltTransformer.java",
"id": "sym:java:com.example.transform.XsltTransformer.transform",
"line": 134,
"moduleHash": "sha256:345678901234567890123456789012345678901234567890123456789012",
"symbol": "com.example.transform.XsltTransformer.transform(Document)"
}
],
"sinkRefs": [
"sym:java:com.vulnerable.XXEVulnerableParser.parseXml"
]
}
}

View File

@@ -1,106 +0,0 @@
{
"@type": "https://stellaops.dev/predicates/proof-of-exposure@v1",
"evidence": {
"graphHash": "blake3:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0",
"sbomRef": "cas://scanner-artifacts/sbom.cdx.json"
},
"metadata": {
"analyzer": {
"name": "stellaops-scanner",
"toolchainDigest": "sha256:678901234567890123456789012345678901234567890123456789012345",
"version": "1.2.0"
},
"generatedAt": "2025-12-23T13:00:00.000Z",
"policy": {
"evaluatedAt": "2025-12-23T12:58:00.000Z",
"policyDigest": "sha256:901234567890123456789012345678901234567890123456789012345678",
"policyId": "prod-release-v42"
},
"reproSteps": [
"1. Build container image from Dockerfile (commit: jkl012)",
"2. Run scanner with config: etc/scanner.yaml",
"3. Extract reachability graph with maxDepth=10 (stripped binary)",
"4. Resolve CVE-2024-99999 to vulnerable code addresses via binary diffing"
]
},
"schema": "stellaops.dev/poe@v1",
"subject": {
"buildId": "gnu-build-id:c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9",
"componentRef": "pkg:generic/libcrypto.so.1.1@1.1.1k",
"imageDigest": "sha256:jkl012345678901234567890123456789012345678901234567890123456",
"vulnId": "CVE-2024-99999"
},
"subgraph": {
"edges": [
{
"confidence": 0.94,
"from": "code_id:0x401530",
"to": "code_id:0x402140"
},
{
"confidence": 0.91,
"from": "code_id:0x402140",
"to": "code_id:0x403880"
},
{
"confidence": 0.89,
"from": "code_id:0x403880",
"to": "code_id:0x405220"
},
{
"confidence": 0.87,
"from": "code_id:0x405220",
"to": "code_id:0x407b40"
},
{
"confidence": 0.85,
"from": "code_id:0x407b40",
"to": "code_id:0x409c60"
}
],
"entryRefs": [
"code_id:0x401530"
],
"nodes": [
{
"addr": "0x401530",
"id": "code_id:0x401530",
"moduleHash": "sha256:701234567890123456789012345678901234567890123456789012345678",
"symbol": "<stripped>"
},
{
"addr": "0x402140",
"id": "code_id:0x402140",
"moduleHash": "sha256:701234567890123456789012345678901234567890123456789012345678",
"symbol": "<stripped>"
},
{
"addr": "0x403880",
"id": "code_id:0x403880",
"moduleHash": "sha256:701234567890123456789012345678901234567890123456789012345678",
"symbol": "<stripped>"
},
{
"addr": "0x405220",
"id": "code_id:0x405220",
"moduleHash": "sha256:712345678901234567890123456789012345678901234567890123456789",
"symbol": "<stripped>"
},
{
"addr": "0x407b40",
"id": "code_id:0x407b40",
"moduleHash": "sha256:712345678901234567890123456789012345678901234567890123456789",
"symbol": "<stripped>"
},
{
"addr": "0x409c60",
"id": "code_id:0x409c60",
"moduleHash": "sha256:723456789012345678901234567890123456789012345678901234567890",
"symbol": "<stripped>"
}
],
"sinkRefs": [
"code_id:0x409c60"
]
}
}

View File

@@ -1,21 +0,0 @@
# Reachability Fixture Harness
This directory carries the reachbench fixture packs used by Sprint 201 to validate reachability explainability.
- `fixtures/reachbench-2025-expanded/` contains 24 multi-language cases with reachable and unreachable variants, SBOMs, callgraphs, runtime traces, and DSSE envelopes. Each variant ships a manifest with SHA-256 hashes to keep runs deterministic.
- `StellaOps.Reachability.FixtureTests` provides guard rails that ensure files exist, hashes match manifests, and ground-truth paths align with reachable/unreachable variants before the Signals/Scanner reachability pipeline consumes them.
## Running the fixture tests
```bash
# From the repo root
DOTNET_CLI_UI_LANGUAGE=en DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 \
dotnet test tests/reachability/StellaOps.Reachability.FixtureTests/StellaOps.Reachability.FixtureTests.csproj
# Focus only the evaluation harness checks
DOTNET_CLI_UI_LANGUAGE=en DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 \
dotnet test tests/reachability/StellaOps.Reachability.FixtureTests/StellaOps.Reachability.FixtureTests.csproj \
--filter ReachbenchEvaluationHarnessTests
```
The tests validate fixture integrity (hashes, schema versions) and now enforce the evaluation harness contract: reachable variants must surface execution paths while unreachable variants must not. Keep the environment overrides above in CI to avoid localization drift during hash comparisons.

View File

@@ -1,100 +0,0 @@
using System.Security.Cryptography;
using System.Text.Json;
using FluentAssertions;
using Xunit;
namespace StellaOps.Reachability.FixtureTests;
public class CorpusFixtureTests
{
private static readonly string RepoRoot = ReachbenchFixtureTests.LocateRepoRoot();
private static readonly string CorpusRoot = Path.Combine(RepoRoot, "tests", "reachability", "corpus");
[Fact]
public void ManifestExistsAndIsDeterministic()
{
var manifestPath = Path.Combine(CorpusRoot, "manifest.json");
File.Exists(manifestPath).Should().BeTrue("corpus manifest should exist");
using var stream = File.OpenRead(manifestPath);
using var doc = JsonDocument.Parse(stream);
doc.RootElement.ValueKind.Should().Be(JsonValueKind.Array);
}
[Fact]
public void CorpusEntriesMatchManifestHashes()
{
var manifestPath = Path.Combine(CorpusRoot, "manifest.json");
var manifest = JsonDocument.Parse(File.ReadAllBytes(manifestPath)).RootElement.EnumerateArray().ToArray();
manifest.Should().NotBeEmpty("corpus manifest must have entries");
foreach (var entry in manifest)
{
var id = entry.GetProperty("id").GetString();
var language = entry.GetProperty("language").GetString();
var files = entry.GetProperty("files");
id.Should().NotBeNullOrEmpty();
language.Should().NotBeNullOrEmpty();
var caseDir = Path.Combine(CorpusRoot, language!, id!);
Directory.Exists(caseDir).Should().BeTrue($"case folder missing: {caseDir}");
foreach (var fileProp in files.EnumerateObject())
{
var filename = fileProp.Name;
var expectedHash = fileProp.Value.GetString();
File.Exists(Path.Combine(caseDir, filename)).Should().BeTrue($"{id} missing {filename}");
var actualHash = BitConverter.ToString(SHA256.HashData(File.ReadAllBytes(Path.Combine(caseDir, filename)))).Replace("-", "").ToLowerInvariant();
actualHash.Should().Be(expectedHash, $"{id} hash mismatch for {filename}");
}
}
}
[Fact]
public void GroundTruthFilesContainRequiredFields()
{
var manifestPath = Path.Combine(CorpusRoot, "manifest.json");
var manifest = JsonDocument.Parse(File.ReadAllBytes(manifestPath)).RootElement.EnumerateArray().ToArray();
const string expectedSchemaVersion = "reachbench.reachgraph.truth/v1";
var allowedVariants = new[] { "reachable", "unreachable" };
foreach (var entry in manifest)
{
var id = entry.GetProperty("id").GetString()!;
var language = entry.GetProperty("language").GetString()!;
var truthPath = Path.Combine(CorpusRoot, language, id, "ground-truth.json");
File.Exists(truthPath).Should().BeTrue($"{id} missing ground-truth.json");
using var truthDoc = JsonDocument.Parse(File.ReadAllBytes(truthPath));
truthDoc.RootElement.GetProperty("schema_version").GetString().Should().Be(expectedSchemaVersion, $"{id} ground-truth schema_version mismatch");
truthDoc.RootElement.GetProperty("case_id").GetString().Should().Be(id, $"{id} ground-truth case_id must match manifest id");
var variant = truthDoc.RootElement.GetProperty("variant").GetString();
variant.Should().NotBeNullOrWhiteSpace($"{id} ground-truth must set variant");
allowedVariants.Should().Contain(variant!, $"{id} variant must be reachable|unreachable");
truthDoc.RootElement.TryGetProperty("paths", out var pathsProp).Should().BeTrue($"{id} ground-truth must include paths");
pathsProp.ValueKind.Should().Be(JsonValueKind.Array, $"{id} paths must be an array");
if (string.Equals(variant, "reachable", StringComparison.Ordinal))
{
pathsProp.GetArrayLength().Should().BeGreaterThan(0, $"{id} reachable ground-truth should include at least one path");
}
foreach (var path in pathsProp.EnumerateArray())
{
path.ValueKind.Should().Be(JsonValueKind.Array, $"{id} each path must be an array");
path.GetArrayLength().Should().BeGreaterThan(0, $"{id} each path must contain at least one symbol");
foreach (var segment in path.EnumerateArray())
{
segment.ValueKind.Should().Be(JsonValueKind.String, $"{id} path segments must be strings");
segment.GetString().Should().NotBeNullOrWhiteSpace($"{id} path segments must be non-empty strings");
}
}
}
}
}

View File

@@ -1,56 +0,0 @@
using System.Text.Json;
using FluentAssertions;
using Xunit;
namespace StellaOps.Reachability.FixtureTests;
public sealed class FixtureCoverageTests
{
private static readonly string RepoRoot = ReachbenchFixtureTests.LocateRepoRoot();
private static readonly string ReachabilityRoot = Path.Combine(RepoRoot, "tests", "reachability");
private static readonly string CorpusRoot = Path.Combine(ReachabilityRoot, "corpus");
private static readonly string SamplesPublicRoot = Path.Combine(ReachabilityRoot, "samples-public");
[Fact]
public void CorpusAndPublicSamplesCoverExpectedLanguageBuckets()
{
var corpusLanguages = ReadManifestLanguages(Path.Combine(CorpusRoot, "manifest.json"));
corpusLanguages.Should().Contain(new[] { "dotnet", "go", "python", "rust" });
var samplesLanguages = ReadManifestLanguages(Path.Combine(SamplesPublicRoot, "manifest.json"));
samplesLanguages.Should().Contain(new[] { "csharp", "js", "php" });
}
[Fact]
public void CorpusManifestIsSorted()
{
var keys = ReadManifestKeys(Path.Combine(CorpusRoot, "manifest.json"));
keys.Should().NotBeEmpty("corpus manifest should have entries");
keys.Should().BeInAscendingOrder(StringComparer.Ordinal);
}
private static string[] ReadManifestLanguages(string manifestPath)
{
File.Exists(manifestPath).Should().BeTrue($"{manifestPath} should exist");
using var doc = JsonDocument.Parse(File.ReadAllBytes(manifestPath));
return doc.RootElement.EnumerateArray()
.Select(entry => entry.GetProperty("language").GetString())
.Where(language => !string.IsNullOrWhiteSpace(language))
.Select(language => language!)
.Distinct(StringComparer.Ordinal)
.OrderBy(language => language, StringComparer.Ordinal)
.ToArray();
}
private static string[] ReadManifestKeys(string manifestPath)
{
File.Exists(manifestPath).Should().BeTrue($"{manifestPath} should exist");
using var doc = JsonDocument.Parse(File.ReadAllBytes(manifestPath));
return doc.RootElement.EnumerateArray()
.Select(entry => $"{entry.GetProperty("language").GetString()}/{entry.GetProperty("id").GetString()}")
.ToArray();
}
}

View File

@@ -1,375 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using StellaOps.Scanner.Reachability;
namespace StellaOps.Reachability.FixtureTests.PatchOracle;
/// <summary>
/// Compares a RichGraph against a patch-oracle definition.
/// Reports missing expected elements and present forbidden elements.
/// </summary>
public sealed class PatchOracleComparer
{
private readonly PatchOracleDefinition _oracle;
private readonly double _defaultMinConfidence;
public PatchOracleComparer(PatchOracleDefinition oracle)
{
_oracle = oracle ?? throw new ArgumentNullException(nameof(oracle));
_defaultMinConfidence = oracle.MinConfidence;
}
/// <summary>
/// Compares the graph against the oracle and returns a result.
/// </summary>
public PatchOracleResult Compare(RichGraph graph)
{
ArgumentNullException.ThrowIfNull(graph);
var violations = new List<PatchOracleViolation>();
// Check expected functions
foreach (var expected in _oracle.ExpectedFunctions.Where(f => f.Required))
{
if (!HasMatchingNode(graph, expected))
{
violations.Add(new PatchOracleViolation(
ViolationType.MissingFunction,
expected.SymbolId,
null,
expected.Reason ?? $"Expected function '{expected.SymbolId}' not found in graph"));
}
}
// Check expected edges
foreach (var expected in _oracle.ExpectedEdges.Where(e => e.Required))
{
var minConf = expected.MinConfidence ?? _defaultMinConfidence;
if (!HasMatchingEdge(graph, expected, minConf))
{
violations.Add(new PatchOracleViolation(
ViolationType.MissingEdge,
expected.From,
expected.To,
expected.Reason ?? $"Expected edge '{expected.From}' -> '{expected.To}' not found in graph"));
}
}
// Check expected roots
foreach (var expected in _oracle.ExpectedRoots.Where(r => r.Required))
{
if (!HasMatchingRoot(graph, expected))
{
violations.Add(new PatchOracleViolation(
ViolationType.MissingRoot,
expected.Id,
null,
expected.Reason ?? $"Expected root '{expected.Id}' not found in graph"));
}
}
// Check forbidden functions
foreach (var forbidden in _oracle.ForbiddenFunctions)
{
if (HasMatchingNode(graph, forbidden))
{
violations.Add(new PatchOracleViolation(
ViolationType.ForbiddenFunctionPresent,
forbidden.SymbolId,
null,
forbidden.Reason ?? $"Forbidden function '{forbidden.SymbolId}' is present in graph"));
}
}
// Check forbidden edges
foreach (var forbidden in _oracle.ForbiddenEdges)
{
if (HasMatchingEdge(graph, forbidden, 0.0))
{
violations.Add(new PatchOracleViolation(
ViolationType.ForbiddenEdgePresent,
forbidden.From,
forbidden.To,
forbidden.Reason ?? $"Forbidden edge '{forbidden.From}' -> '{forbidden.To}' is present in graph"));
}
}
// Strict mode: check for unexpected elements
if (_oracle.StrictMode)
{
var unexpectedNodes = FindUnexpectedNodes(graph);
foreach (var node in unexpectedNodes)
{
violations.Add(new PatchOracleViolation(
ViolationType.UnexpectedFunction,
node.Id,
null,
$"Strict mode: unexpected function '{node.Id}' found in graph"));
}
var unexpectedEdges = FindUnexpectedEdges(graph);
foreach (var edge in unexpectedEdges)
{
violations.Add(new PatchOracleViolation(
ViolationType.UnexpectedEdge,
edge.From,
edge.To,
$"Strict mode: unexpected edge '{edge.From}' -> '{edge.To}' found in graph"));
}
}
return new PatchOracleResult(
OracleId: _oracle.Id,
CaseRef: _oracle.CaseRef,
Variant: _oracle.Variant,
Success: violations.Count == 0,
Violations: violations,
Summary: GenerateSummary(graph, violations));
}
private bool HasMatchingNode(RichGraph graph, ExpectedFunction expected)
{
foreach (var node in graph.Nodes)
{
if (!MatchesPattern(node.Id, expected.SymbolId) &&
!MatchesPattern(node.SymbolId, expected.SymbolId))
{
continue;
}
if (!string.IsNullOrEmpty(expected.Lang) &&
!string.Equals(node.Lang, expected.Lang, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!string.IsNullOrEmpty(expected.Kind) &&
!string.Equals(node.Kind, expected.Kind, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!string.IsNullOrEmpty(expected.PurlPattern) &&
!MatchesPattern(node.Purl ?? string.Empty, expected.PurlPattern))
{
continue;
}
return true;
}
return false;
}
private bool HasMatchingEdge(RichGraph graph, ExpectedEdge expected, double minConfidence)
{
foreach (var edge in graph.Edges)
{
if (!MatchesPattern(edge.From, expected.From))
{
continue;
}
if (!MatchesPattern(edge.To, expected.To))
{
continue;
}
if (!string.IsNullOrEmpty(expected.Kind) &&
!string.Equals(edge.Kind, expected.Kind, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (edge.Confidence < minConfidence)
{
continue;
}
return true;
}
return false;
}
private bool HasMatchingRoot(RichGraph graph, ExpectedRoot expected)
{
foreach (var root in graph.Roots)
{
if (!MatchesPattern(root.Id, expected.Id))
{
continue;
}
if (!string.IsNullOrEmpty(expected.Phase) &&
!string.Equals(root.Phase, expected.Phase, StringComparison.OrdinalIgnoreCase))
{
continue;
}
return true;
}
return false;
}
private IEnumerable<RichGraphNode> FindUnexpectedNodes(RichGraph graph)
{
var allExpected = _oracle.ExpectedFunctions
.Select(f => f.SymbolId)
.ToHashSet(StringComparer.Ordinal);
foreach (var node in graph.Nodes)
{
var isExpected = allExpected.Any(pattern => MatchesPattern(node.Id, pattern) || MatchesPattern(node.SymbolId, pattern));
if (!isExpected)
{
yield return node;
}
}
}
private IEnumerable<RichGraphEdge> FindUnexpectedEdges(RichGraph graph)
{
foreach (var edge in graph.Edges)
{
var isExpected = _oracle.ExpectedEdges.Any(e =>
MatchesPattern(edge.From, e.From) && MatchesPattern(edge.To, e.To));
if (!isExpected)
{
yield return edge;
}
}
}
/// <summary>
/// Matches a value against a pattern supporting '*' wildcards.
/// </summary>
private static bool MatchesPattern(string value, string pattern)
{
if (string.IsNullOrEmpty(pattern))
{
return true;
}
if (string.IsNullOrEmpty(value))
{
return false;
}
// Exact match
if (!pattern.Contains('*'))
{
return string.Equals(value, pattern, StringComparison.Ordinal);
}
// Convert wildcard pattern to regex
var regexPattern = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$";
return Regex.IsMatch(value, regexPattern, RegexOptions.None, TimeSpan.FromMilliseconds(100));
}
private static PatchOracleSummary GenerateSummary(RichGraph graph, List<PatchOracleViolation> violations)
{
return new PatchOracleSummary(
TotalNodes: graph.Nodes.Count,
TotalEdges: graph.Edges.Count,
TotalRoots: graph.Roots.Count,
MissingFunctions: violations.Count(v => v.Type == ViolationType.MissingFunction),
MissingEdges: violations.Count(v => v.Type == ViolationType.MissingEdge),
MissingRoots: violations.Count(v => v.Type == ViolationType.MissingRoot),
ForbiddenFunctionsPresent: violations.Count(v => v.Type == ViolationType.ForbiddenFunctionPresent),
ForbiddenEdgesPresent: violations.Count(v => v.Type == ViolationType.ForbiddenEdgePresent),
UnexpectedFunctions: violations.Count(v => v.Type == ViolationType.UnexpectedFunction),
UnexpectedEdges: violations.Count(v => v.Type == ViolationType.UnexpectedEdge));
}
}
/// <summary>
/// Result of comparing a graph against a patch-oracle.
/// </summary>
public sealed record PatchOracleResult(
string OracleId,
string CaseRef,
string Variant,
bool Success,
IReadOnlyList<PatchOracleViolation> Violations,
PatchOracleSummary Summary)
{
/// <summary>
/// Generates a human-readable report.
/// </summary>
public string ToReport()
{
var lines = new List<string>
{
$"Patch-Oracle Validation Report",
$"==============================",
$"Oracle: {OracleId}",
$"Case: {CaseRef} ({Variant})",
$"Status: {(Success ? "PASS" : "FAIL")}",
string.Empty,
$"Graph Statistics:",
$" Nodes: {Summary.TotalNodes}",
$" Edges: {Summary.TotalEdges}",
$" Roots: {Summary.TotalRoots}",
string.Empty
};
if (Violations.Count > 0)
{
lines.Add($"Violations ({Violations.Count}):");
foreach (var v in Violations)
{
var target = v.To is not null ? $" -> {v.To}" : string.Empty;
lines.Add($" [{v.Type}] {v.From}{target}");
lines.Add($" Reason: {v.Message}");
}
}
else
{
lines.Add("No violations found.");
}
return string.Join(Environment.NewLine, lines);
}
}
/// <summary>
/// A single violation found during oracle comparison.
/// </summary>
public sealed record PatchOracleViolation(
ViolationType Type,
string From,
string? To,
string Message);
/// <summary>
/// Type of oracle violation.
/// </summary>
public enum ViolationType
{
MissingFunction,
MissingEdge,
MissingRoot,
ForbiddenFunctionPresent,
ForbiddenEdgePresent,
UnexpectedFunction,
UnexpectedEdge
}
/// <summary>
/// Summary statistics for oracle comparison.
/// </summary>
public sealed record PatchOracleSummary(
int TotalNodes,
int TotalEdges,
int TotalRoots,
int MissingFunctions,
int MissingEdges,
int MissingRoots,
int ForbiddenFunctionsPresent,
int ForbiddenEdgesPresent,
int UnexpectedFunctions,
int UnexpectedEdges);

View File

@@ -1,112 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace StellaOps.Reachability.FixtureTests.PatchOracle;
/// <summary>
/// Loads patch-oracle definitions from fixture files.
/// </summary>
public sealed class PatchOracleLoader
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true
};
private readonly string _fixtureRoot;
public PatchOracleLoader(string fixtureRoot)
{
_fixtureRoot = fixtureRoot ?? throw new ArgumentNullException(nameof(fixtureRoot));
}
/// <summary>
/// Loads the oracle index from INDEX.json.
/// </summary>
public PatchOracleIndex LoadIndex()
{
var indexPath = Path.Combine(_fixtureRoot, "INDEX.json");
if (!File.Exists(indexPath))
{
throw new FileNotFoundException($"Patch-oracle INDEX.json not found at {indexPath}");
}
var json = File.ReadAllText(indexPath);
return JsonSerializer.Deserialize<PatchOracleIndex>(json, JsonOptions)
?? throw new InvalidOperationException("Failed to deserialize patch-oracle index");
}
/// <summary>
/// Loads an oracle definition by its ID.
/// </summary>
public PatchOracleDefinition LoadOracle(string oracleId)
{
var index = LoadIndex();
var entry = index.Oracles
.FirstOrDefault(o => string.Equals(o.Id, oracleId, StringComparison.Ordinal))
?? throw new KeyNotFoundException($"Oracle '{oracleId}' not found in index");
return LoadOracleFromPath(entry.Path);
}
/// <summary>
/// Loads an oracle definition from a relative path.
/// </summary>
public PatchOracleDefinition LoadOracleFromPath(string relativePath)
{
var fullPath = Path.Combine(_fixtureRoot, relativePath);
if (!File.Exists(fullPath))
{
throw new FileNotFoundException($"Oracle file not found at {fullPath}");
}
var json = File.ReadAllText(fullPath);
return JsonSerializer.Deserialize<PatchOracleDefinition>(json, JsonOptions)
?? throw new InvalidOperationException($"Failed to deserialize oracle from {fullPath}");
}
/// <summary>
/// Loads all oracles for a specific case.
/// </summary>
public IEnumerable<PatchOracleDefinition> LoadOraclesForCase(string caseRef)
{
var index = LoadIndex();
foreach (var entry in index.Oracles.Where(o => string.Equals(o.CaseRef, caseRef, StringComparison.Ordinal)))
{
yield return LoadOracleFromPath(entry.Path);
}
}
/// <summary>
/// Loads all available oracles.
/// </summary>
public IEnumerable<PatchOracleDefinition> LoadAllOracles()
{
var index = LoadIndex();
foreach (var entry in index.Oracles)
{
yield return LoadOracleFromPath(entry.Path);
}
}
/// <summary>
/// Enumerates all oracle entries without loading full definitions.
/// </summary>
public IEnumerable<PatchOracleIndexEntry> EnumerateOracles()
{
var index = LoadIndex();
return index.Oracles;
}
/// <summary>
/// Checks if the oracle index exists.
/// </summary>
public bool IndexExists()
{
return File.Exists(Path.Combine(_fixtureRoot, "INDEX.json"));
}
}

View File

@@ -1,158 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Reachability.FixtureTests.PatchOracle;
/// <summary>
/// Root model for patch-oracle fixture files.
/// </summary>
public sealed record PatchOracleDefinition
{
[JsonPropertyName("schema_version")]
public string SchemaVersion { get; init; } = "patch-oracle/v1";
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("case_ref")]
public required string CaseRef { get; init; }
[JsonPropertyName("variant")]
public required string Variant { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("expected_functions")]
public IReadOnlyList<ExpectedFunction> ExpectedFunctions { get; init; } = Array.Empty<ExpectedFunction>();
[JsonPropertyName("expected_edges")]
public IReadOnlyList<ExpectedEdge> ExpectedEdges { get; init; } = Array.Empty<ExpectedEdge>();
[JsonPropertyName("expected_roots")]
public IReadOnlyList<ExpectedRoot> ExpectedRoots { get; init; } = Array.Empty<ExpectedRoot>();
[JsonPropertyName("forbidden_functions")]
public IReadOnlyList<ExpectedFunction> ForbiddenFunctions { get; init; } = Array.Empty<ExpectedFunction>();
[JsonPropertyName("forbidden_edges")]
public IReadOnlyList<ExpectedEdge> ForbiddenEdges { get; init; } = Array.Empty<ExpectedEdge>();
[JsonPropertyName("min_confidence")]
public double MinConfidence { get; init; } = 0.5;
[JsonPropertyName("strict_mode")]
public bool StrictMode { get; init; } = false;
[JsonPropertyName("created_at")]
public DateTimeOffset? CreatedAt { get; init; }
[JsonPropertyName("updated_at")]
public DateTimeOffset? UpdatedAt { get; init; }
}
/// <summary>
/// Expected function/node in the graph.
/// </summary>
public sealed record ExpectedFunction
{
[JsonPropertyName("symbol_id")]
public required string SymbolId { get; init; }
[JsonPropertyName("lang")]
public string? Lang { get; init; }
[JsonPropertyName("kind")]
public string? Kind { get; init; }
[JsonPropertyName("purl_pattern")]
public string? PurlPattern { get; init; }
[JsonPropertyName("required")]
public bool Required { get; init; } = true;
[JsonPropertyName("reason")]
public string? Reason { get; init; }
}
/// <summary>
/// Expected edge in the graph.
/// </summary>
public sealed record ExpectedEdge
{
[JsonPropertyName("from")]
public required string From { get; init; }
[JsonPropertyName("to")]
public required string To { get; init; }
[JsonPropertyName("kind")]
public string? Kind { get; init; }
[JsonPropertyName("min_confidence")]
public double? MinConfidence { get; init; }
[JsonPropertyName("required")]
public bool Required { get; init; } = true;
[JsonPropertyName("reason")]
public string? Reason { get; init; }
}
/// <summary>
/// Expected root node in the graph.
/// </summary>
public sealed record ExpectedRoot
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("phase")]
public string? Phase { get; init; }
[JsonPropertyName("required")]
public bool Required { get; init; } = true;
[JsonPropertyName("reason")]
public string? Reason { get; init; }
}
/// <summary>
/// Index entry for an oracle.
/// </summary>
public sealed record PatchOracleIndexEntry
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("case_ref")]
public required string CaseRef { get; init; }
[JsonPropertyName("variant")]
public required string Variant { get; init; }
[JsonPropertyName("path")]
public required string Path { get; init; }
}
/// <summary>
/// Root model for patch-oracle INDEX.json.
/// </summary>
public sealed record PatchOracleIndex
{
[JsonPropertyName("version")]
public string Version { get; init; } = "1.0";
[JsonPropertyName("schema")]
public string Schema { get; init; } = "patch-oracle/v1";
[JsonPropertyName("generated_at")]
public DateTimeOffset? GeneratedAt { get; init; }
[JsonPropertyName("description")]
public string? Description { get; init; }
[JsonPropertyName("oracles")]
public IReadOnlyList<PatchOracleIndexEntry> Oracles { get; init; } = Array.Empty<PatchOracleIndexEntry>();
}

View File

@@ -1,494 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FluentAssertions;
using StellaOps.Reachability.FixtureTests.PatchOracle;
using StellaOps.Scanner.Reachability;
using Xunit;
namespace StellaOps.Reachability.FixtureTests;
/// <summary>
/// Tests for the patch-oracle harness infrastructure.
/// Validates that the oracle comparison logic correctly identifies missing and forbidden elements.
/// </summary>
public class PatchOracleHarnessTests
{
private static readonly string RepoRoot = ReachbenchFixtureTests.LocateRepoRoot();
private static readonly string PatchOracleRoot = Path.Combine(
RepoRoot, "tests", "reachability", "fixtures", "patch-oracles");
#region Oracle Loading Tests
[Fact]
public void Loader_IndexExists()
{
var loader = new PatchOracleLoader(PatchOracleRoot);
loader.IndexExists().Should().BeTrue("patch-oracle INDEX.json should exist");
}
[Fact]
public void Loader_IndexLoadsSuccessfully()
{
var loader = new PatchOracleLoader(PatchOracleRoot);
var index = loader.LoadIndex();
index.Should().NotBeNull();
index.Version.Should().Be("1.0");
index.Schema.Should().Be("patch-oracle/v1");
index.Oracles.Should().NotBeEmpty("should have at least one oracle defined");
}
[Fact]
public void Loader_AllOraclesLoadSuccessfully()
{
var loader = new PatchOracleLoader(PatchOracleRoot);
var oracles = loader.LoadAllOracles().ToList();
oracles.Should().NotBeEmpty();
foreach (var oracle in oracles)
{
oracle.SchemaVersion.Should().Be("patch-oracle/v1");
oracle.Id.Should().NotBeNullOrEmpty();
oracle.CaseRef.Should().NotBeNullOrEmpty();
oracle.Variant.Should().BeOneOf("reachable", "unreachable");
}
}
[Fact]
public void Loader_LoadOracleById()
{
var loader = new PatchOracleLoader(PatchOracleRoot);
var oracle = loader.LoadOracle("curl-CVE-2023-38545-socks5-heap-reachable");
oracle.Should().NotBeNull();
oracle.Id.Should().Be("curl-CVE-2023-38545-socks5-heap-reachable");
oracle.CaseRef.Should().Be("curl-CVE-2023-38545-socks5-heap");
oracle.Variant.Should().Be("reachable");
}
#endregion
#region Comparer Tests - Pass Cases
[Fact]
public void Comparer_PassesWhenAllExpectedElementsPresent()
{
var oracle = new PatchOracleDefinition
{
Id = "test-pass",
CaseRef = "test-case",
Variant = "reachable",
ExpectedFunctions = new[]
{
new ExpectedFunction { SymbolId = "sym://test#func1", Required = true },
new ExpectedFunction { SymbolId = "sym://test#func2", Required = true }
},
ExpectedEdges = new[]
{
new ExpectedEdge { From = "sym://test#func1", To = "sym://test#func2", Required = true }
}
};
var graph = new RichGraph(
Nodes: new[]
{
new RichGraphNode("sym://test#func1", "sym://test#func1", null, null, "test", "function", null, null, null, null, null),
new RichGraphNode("sym://test#func2", "sym://test#func2", null, null, "test", "function", null, null, null, null, null)
},
Edges: new[]
{
new RichGraphEdge("sym://test#func1", "sym://test#func2", "call", null, null, null, 0.9, null)
},
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null)
);
var comparer = new PatchOracleComparer(oracle);
var result = comparer.Compare(graph);
result.Success.Should().BeTrue();
result.Violations.Should().BeEmpty();
}
[Fact]
public void Comparer_PassesWithWildcardPatterns()
{
var oracle = new PatchOracleDefinition
{
Id = "test-wildcard",
CaseRef = "test-case",
Variant = "reachable",
ExpectedFunctions = new[]
{
new ExpectedFunction { SymbolId = "sym://test#*", Required = true }
}
};
var graph = new RichGraph(
Nodes: new[]
{
new RichGraphNode("sym://test#anything", "sym://test#anything", null, null, "test", "function", null, null, null, null, null)
},
Edges: Array.Empty<RichGraphEdge>(),
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null)
);
var comparer = new PatchOracleComparer(oracle);
var result = comparer.Compare(graph);
result.Success.Should().BeTrue();
}
#endregion
#region Comparer Tests - Fail Cases
[Fact]
public void Comparer_FailsWhenExpectedFunctionMissing()
{
var oracle = new PatchOracleDefinition
{
Id = "test-missing-func",
CaseRef = "test-case",
Variant = "reachable",
ExpectedFunctions = new[]
{
new ExpectedFunction { SymbolId = "sym://test#missing", Required = true, Reason = "This function is critical" }
}
};
var graph = new RichGraph(
Nodes: Array.Empty<RichGraphNode>(),
Edges: Array.Empty<RichGraphEdge>(),
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null)
);
var comparer = new PatchOracleComparer(oracle);
var result = comparer.Compare(graph);
result.Success.Should().BeFalse();
result.Violations.Should().HaveCount(1);
result.Violations[0].Type.Should().Be(ViolationType.MissingFunction);
result.Violations[0].From.Should().Be("sym://test#missing");
result.Summary.MissingFunctions.Should().Be(1);
}
[Fact]
public void Comparer_FailsWhenExpectedEdgeMissing()
{
var oracle = new PatchOracleDefinition
{
Id = "test-missing-edge",
CaseRef = "test-case",
Variant = "reachable",
ExpectedEdges = new[]
{
new ExpectedEdge { From = "sym://a", To = "sym://b", Required = true }
}
};
var graph = new RichGraph(
Nodes: new[]
{
new RichGraphNode("sym://a", "sym://a", null, null, "test", "function", null, null, null, null, null),
new RichGraphNode("sym://b", "sym://b", null, null, "test", "function", null, null, null, null, null)
},
Edges: Array.Empty<RichGraphEdge>(),
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null)
);
var comparer = new PatchOracleComparer(oracle);
var result = comparer.Compare(graph);
result.Success.Should().BeFalse();
result.Violations.Should().HaveCount(1);
result.Violations[0].Type.Should().Be(ViolationType.MissingEdge);
result.Summary.MissingEdges.Should().Be(1);
}
[Fact]
public void Comparer_FailsWhenExpectedRootMissing()
{
var oracle = new PatchOracleDefinition
{
Id = "test-missing-root",
CaseRef = "test-case",
Variant = "reachable",
ExpectedRoots = new[]
{
new ExpectedRoot { Id = "sym://root#main", Phase = "main", Required = true }
}
};
var graph = new RichGraph(
Nodes: Array.Empty<RichGraphNode>(),
Edges: Array.Empty<RichGraphEdge>(),
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null)
);
var comparer = new PatchOracleComparer(oracle);
var result = comparer.Compare(graph);
result.Success.Should().BeFalse();
result.Violations.Should().HaveCount(1);
result.Violations[0].Type.Should().Be(ViolationType.MissingRoot);
result.Summary.MissingRoots.Should().Be(1);
}
[Fact]
public void Comparer_FailsWhenForbiddenFunctionPresent()
{
var oracle = new PatchOracleDefinition
{
Id = "test-forbidden-func",
CaseRef = "test-case",
Variant = "unreachable",
ForbiddenFunctions = new[]
{
new ExpectedFunction { SymbolId = "sym://dangerous#sink", Reason = "Should not be reachable" }
}
};
var graph = new RichGraph(
Nodes: new[]
{
new RichGraphNode("sym://dangerous#sink", "sym://dangerous#sink", null, null, "test", "function", null, null, null, null, null)
},
Edges: Array.Empty<RichGraphEdge>(),
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null)
);
var comparer = new PatchOracleComparer(oracle);
var result = comparer.Compare(graph);
result.Success.Should().BeFalse();
result.Violations.Should().HaveCount(1);
result.Violations[0].Type.Should().Be(ViolationType.ForbiddenFunctionPresent);
result.Summary.ForbiddenFunctionsPresent.Should().Be(1);
}
[Fact]
public void Comparer_FailsWhenForbiddenEdgePresent()
{
var oracle = new PatchOracleDefinition
{
Id = "test-forbidden-edge",
CaseRef = "test-case",
Variant = "unreachable",
ForbiddenEdges = new[]
{
new ExpectedEdge { From = "sym://entry", To = "sym://sink", Reason = "Path should be blocked" }
}
};
var graph = new RichGraph(
Nodes: Array.Empty<RichGraphNode>(),
Edges: new[]
{
new RichGraphEdge("sym://entry", "sym://sink", "call", null, null, null, 1.0, null)
},
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null)
);
var comparer = new PatchOracleComparer(oracle);
var result = comparer.Compare(graph);
result.Success.Should().BeFalse();
result.Violations.Should().HaveCount(1);
result.Violations[0].Type.Should().Be(ViolationType.ForbiddenEdgePresent);
result.Summary.ForbiddenEdgesPresent.Should().Be(1);
}
#endregion
#region Confidence Threshold Tests
[Fact]
public void Comparer_RespectsMinConfidenceThreshold()
{
var oracle = new PatchOracleDefinition
{
Id = "test-confidence",
CaseRef = "test-case",
Variant = "reachable",
MinConfidence = 0.8,
ExpectedEdges = new[]
{
new ExpectedEdge { From = "sym://a", To = "sym://b", Required = true }
}
};
var lowConfidenceGraph = new RichGraph(
Nodes: Array.Empty<RichGraphNode>(),
Edges: new[]
{
new RichGraphEdge("sym://a", "sym://b", "call", null, null, null, 0.5, null)
},
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null)
);
var comparer = new PatchOracleComparer(oracle);
var result = comparer.Compare(lowConfidenceGraph);
result.Success.Should().BeFalse("edge confidence 0.5 is below threshold 0.8");
result.Summary.MissingEdges.Should().Be(1);
}
[Fact]
public void Comparer_EdgeSpecificConfidenceOverridesDefault()
{
var oracle = new PatchOracleDefinition
{
Id = "test-edge-confidence",
CaseRef = "test-case",
Variant = "reachable",
MinConfidence = 0.8,
ExpectedEdges = new[]
{
new ExpectedEdge { From = "sym://a", To = "sym://b", MinConfidence = 0.3, Required = true }
}
};
var lowConfidenceGraph = new RichGraph(
Nodes: Array.Empty<RichGraphNode>(),
Edges: new[]
{
new RichGraphEdge("sym://a", "sym://b", "call", null, null, null, 0.5, null)
},
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null)
);
var comparer = new PatchOracleComparer(oracle);
var result = comparer.Compare(lowConfidenceGraph);
result.Success.Should().BeTrue("edge-specific threshold 0.3 allows confidence 0.5");
}
#endregion
#region Strict Mode Tests
[Fact]
public void Comparer_StrictModeRejectsUnexpectedNodes()
{
var oracle = new PatchOracleDefinition
{
Id = "test-strict",
CaseRef = "test-case",
Variant = "reachable",
StrictMode = true,
ExpectedFunctions = new[]
{
new ExpectedFunction { SymbolId = "sym://expected", Required = true }
}
};
var graph = new RichGraph(
Nodes: new[]
{
new RichGraphNode("sym://expected", "sym://expected", null, null, "test", "function", null, null, null, null, null),
new RichGraphNode("sym://unexpected", "sym://unexpected", null, null, "test", "function", null, null, null, null, null)
},
Edges: Array.Empty<RichGraphEdge>(),
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null)
);
var comparer = new PatchOracleComparer(oracle);
var result = comparer.Compare(graph);
result.Success.Should().BeFalse();
result.Violations.Should().Contain(v => v.Type == ViolationType.UnexpectedFunction);
result.Summary.UnexpectedFunctions.Should().Be(1);
}
#endregion
#region Report Generation Tests
[Fact]
public void Result_GeneratesReadableReport()
{
var oracle = new PatchOracleDefinition
{
Id = "test-report",
CaseRef = "test-case",
Variant = "reachable",
ExpectedFunctions = new[]
{
new ExpectedFunction { SymbolId = "sym://missing", Required = true, Reason = "Critical sink" }
}
};
var graph = new RichGraph(
Nodes: new[]
{
new RichGraphNode("sym://other", "sym://other", null, null, "test", "function", null, null, null, null, null)
},
Edges: Array.Empty<RichGraphEdge>(),
Roots: Array.Empty<RichGraphRoot>(),
Analyzer: new RichGraphAnalyzer("test", "1.0", null)
);
var comparer = new PatchOracleComparer(oracle);
var result = comparer.Compare(graph);
var report = result.ToReport();
report.Should().Contain("FAIL");
report.Should().Contain("test-report");
report.Should().Contain("MissingFunction");
report.Should().Contain("sym://missing");
}
#endregion
#region Integration with Fixture Data
public static IEnumerable<object[]> AllOracleData()
{
var loader = new PatchOracleLoader(PatchOracleRoot);
if (!loader.IndexExists())
{
yield break;
}
foreach (var entry in loader.EnumerateOracles())
{
yield return new object[] { entry.Id, entry.CaseRef, entry.Variant };
}
}
[Theory]
[MemberData(nameof(AllOracleData))]
public void AllOracles_HaveValidStructure(string oracleId, string caseRef, string variant)
{
var loader = new PatchOracleLoader(PatchOracleRoot);
var oracle = loader.LoadOracle(oracleId);
oracle.Id.Should().Be(oracleId);
oracle.CaseRef.Should().Be(caseRef);
oracle.Variant.Should().Be(variant);
oracle.SchemaVersion.Should().Be("patch-oracle/v1");
// At least one expectation should be defined
var hasExpectations = oracle.ExpectedFunctions.Count > 0
|| oracle.ExpectedEdges.Count > 0
|| oracle.ExpectedRoots.Count > 0
|| oracle.ForbiddenFunctions.Count > 0
|| oracle.ForbiddenEdges.Count > 0;
hasExpectations.Should().BeTrue($"Oracle '{oracleId}' should define at least one expectation");
}
#endregion
}

View File

@@ -1,405 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using StellaOps.Scanner.Reachability;
using StellaOps.Scanner.Reachability.Lifters;
using Xunit;
namespace StellaOps.Reachability.FixtureTests;
public sealed class ReachabilityLifterTests : IDisposable
{
private readonly string _tempDir;
public ReachabilityLifterTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"lifter-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
}
public void Dispose()
{
try
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, true);
}
}
catch
{
// Best effort cleanup
}
}
[Fact]
public async Task NodeLifter_ExtractsPackageInfo()
{
// Arrange
var packageJson = """
{
"name": "my-app",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"express": "^4.18.0",
"lodash": "^4.17.0"
}
}
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), packageJson);
var lifter = new NodeReachabilityLifter();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-001"
};
var builder = new ReachabilityGraphBuilder();
// Act
await lifter.LiftAsync(context, builder, CancellationToken.None);
var graph = builder.ToUnionGraph("node");
// Assert
graph.Nodes.Should().NotBeEmpty();
graph.Nodes.Should().Contain(n => n.Display == "my-app");
graph.Nodes.Should().Contain(n => n.Display == "express");
graph.Nodes.Should().Contain(n => n.Display == "lodash");
graph.Edges.Should().NotBeEmpty();
graph.Edges.Should().Contain(e => e.EdgeType == "import");
}
[Fact]
public async Task NodeLifter_ExtractsEntrypoints()
{
// Arrange
var packageJson = """
{
"name": "my-cli",
"version": "2.0.0",
"main": "lib/index.js",
"module": "lib/index.mjs",
"bin": {
"mycli": "bin/cli.js"
}
}
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), packageJson);
var lifter = new NodeReachabilityLifter();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-002"
};
var builder = new ReachabilityGraphBuilder();
// Act
await lifter.LiftAsync(context, builder, CancellationToken.None);
var graph = builder.ToUnionGraph("node");
// Assert
graph.Nodes.Should().Contain(n => n.Kind == "entrypoint");
graph.Nodes.Should().Contain(n => n.Kind == "binary");
graph.Edges.Should().Contain(e => e.EdgeType == "loads");
graph.Edges.Should().Contain(e => e.EdgeType == "spawn");
}
[Fact]
public async Task NodeLifter_ExtractsImportsFromSource()
{
// Arrange
var packageJson = """
{
"name": "my-app",
"version": "1.0.0"
}
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), packageJson);
var sourceCode = """
import express from 'express';
const lodash = require('lodash');
import('./dynamic-module.js');
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "index.js"), sourceCode);
var lifter = new NodeReachabilityLifter();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-003"
};
var builder = new ReachabilityGraphBuilder();
// Act
await lifter.LiftAsync(context, builder, CancellationToken.None);
var graph = builder.ToUnionGraph("node");
// Assert
graph.Nodes.Should().Contain(n => n.Display == "express");
graph.Nodes.Should().Contain(n => n.Display == "lodash");
graph.Edges.Count(e => e.EdgeType == "import").Should().BeGreaterOrEqualTo(2);
}
[Fact]
public async Task DotNetLifter_ExtractsProjectInfo()
{
// Arrange
var csproj = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>MyApp</AssemblyName>
<RootNamespace>MyCompany.MyApp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="3.1.1" />
</ItemGroup>
</Project>
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "MyApp.csproj"), csproj);
var lifter = new DotNetReachabilityLifter();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-004"
};
var builder = new ReachabilityGraphBuilder();
// Act
await lifter.LiftAsync(context, builder, CancellationToken.None);
var graph = builder.ToUnionGraph("dotnet");
// Assert
graph.Nodes.Should().NotBeEmpty();
graph.Nodes.Should().Contain(n => n.Display == "MyApp");
graph.Nodes.Should().Contain(n => n.Display == "Newtonsoft.Json");
graph.Nodes.Should().Contain(n => n.Display == "Serilog");
graph.Nodes.Should().Contain(n => n.Kind == "namespace" && n.Display == "MyCompany.MyApp");
graph.Edges.Should().NotBeEmpty();
graph.Edges.Count(e => e.EdgeType == "import").Should().BeGreaterOrEqualTo(2);
}
[Fact]
public async Task DotNetLifter_ExtractsProjectReferences()
{
// Arrange
var libDir = Path.Combine(_tempDir, "Lib");
Directory.CreateDirectory(libDir);
var libCsproj = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>MyLib</AssemblyName>
</PropertyGroup>
</Project>
""";
await File.WriteAllTextAsync(Path.Combine(libDir, "MyLib.csproj"), libCsproj);
var appCsproj = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>MyApp</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="Lib/MyLib.csproj" />
</ItemGroup>
</Project>
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "MyApp.csproj"), appCsproj);
var lifter = new DotNetReachabilityLifter();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-005"
};
var builder = new ReachabilityGraphBuilder();
// Act
await lifter.LiftAsync(context, builder, CancellationToken.None);
var graph = builder.ToUnionGraph("dotnet");
// Assert
graph.Nodes.Should().Contain(n => n.Display == "MyApp");
graph.Nodes.Should().Contain(n => n.Display == "MyLib");
graph.Edges.Should().Contain(e => e.EdgeType == "import");
}
[Fact]
public async Task LifterRegistry_CombinesMultipleLanguages()
{
// Arrange
var packageJson = """
{
"name": "hybrid-app",
"version": "1.0.0",
"dependencies": { "express": "^4.0.0" }
}
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), packageJson);
var csproj = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>HybridBackend</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
</ItemGroup>
</Project>
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "Backend.csproj"), csproj);
var registry = new ReachabilityLifterRegistry();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-006"
};
// Act
var graph = await registry.LiftAllAsync(context, CancellationToken.None);
// Assert
registry.Lifters.Should().HaveCountGreaterOrEqualTo(2);
graph.Nodes.Should().Contain(n => n.Lang == "node");
graph.Nodes.Should().Contain(n => n.Lang == "dotnet");
}
[Fact]
public async Task LifterRegistry_SelectsSpecificLanguages()
{
// Arrange
var packageJson = """
{
"name": "node-only",
"version": "1.0.0"
}
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), packageJson);
var registry = new ReachabilityLifterRegistry();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-analysis-007"
};
// Act
var graph = await registry.LiftAsync(context, new[] { "node" }, CancellationToken.None);
// Assert
graph.Nodes.Should().OnlyContain(n => n.Lang == "node");
}
[Fact]
public async Task LifterRegistry_LiftAndWrite_CreatesOutputFiles()
{
// Arrange
var packageJson = """
{
"name": "write-test",
"version": "1.0.0",
"dependencies": { "lodash": "^4.0.0" }
}
""";
await File.WriteAllTextAsync(Path.Combine(_tempDir, "package.json"), packageJson);
var outputDir = Path.Combine(_tempDir, "output");
Directory.CreateDirectory(outputDir);
var registry = new ReachabilityLifterRegistry();
var context = new ReachabilityLifterContext
{
RootPath = _tempDir,
AnalysisId = "test-write-001"
};
// Act
var result = await registry.LiftAndWriteAsync(context, outputDir, CancellationToken.None);
// Assert
result.Should().NotBeNull();
File.Exists(result.MetaPath).Should().BeTrue();
File.Exists(result.Nodes.Path).Should().BeTrue();
File.Exists(result.Edges.Path).Should().BeTrue();
result.Nodes.RecordCount.Should().BeGreaterThan(0);
}
[Fact]
public void GraphBuilder_AddsRichNodes()
{
// Arrange
var builder = new ReachabilityGraphBuilder();
// Act
builder.AddNode(
symbolId: "sym:test:abc123",
lang: "test",
kind: "function",
display: "myFunction",
sourceFile: "src/main.ts",
sourceLine: 42,
attributes: new System.Collections.Generic.Dictionary<string, string>
{
["visibility"] = "public",
["async"] = "true"
});
var graph = builder.ToUnionGraph("test");
// Assert
graph.Nodes.Should().HaveCount(1);
var node = graph.Nodes.First();
node.SymbolId.Should().Be("sym:test:abc123");
node.Lang.Should().Be("test");
node.Kind.Should().Be("function");
node.Display.Should().Be("myFunction");
node.Attributes.Should().ContainKey("visibility");
node.Source.Should().NotBeNull();
node.Source!.Evidence.Should().Contain("src/main.ts:42");
}
[Fact]
public void GraphBuilder_AddsRichEdges()
{
// Arrange
var builder = new ReachabilityGraphBuilder();
// Act
builder.AddEdge(
from: "sym:test:from",
to: "sym:test:to",
edgeType: EdgeTypes.Call,
confidence: EdgeConfidence.High,
origin: "static",
provenance: Provenance.Il,
evidence: "file:src/main.cs:100");
var graph = builder.ToUnionGraph("test");
// Assert
graph.Edges.Should().HaveCount(1);
var edge = graph.Edges.First();
edge.From.Should().Be("sym:test:from");
edge.To.Should().Be("sym:test:to");
edge.EdgeType.Should().Be("call");
edge.Confidence.Should().Be("high");
edge.Source.Should().NotBeNull();
edge.Source!.Origin.Should().Be("static");
edge.Source.Provenance.Should().Be("il");
}
}

View File

@@ -1,63 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using FluentAssertions;
using StellaOps.Replay.Core;
using StellaOps.Scanner.Reachability;
using Xunit;
namespace StellaOps.Reachability.FixtureTests;
public sealed class ReachabilityReplayWriterTests
{
[Fact]
public void AttachEvidence_AppendsGraphsAndTracesDeterministically()
{
var manifest = new ReplayManifest
{
Scan = new ReplayScanMetadata { Id = "scan-123", Time = DateTimeOffset.Parse("2025-10-15T10:00:00Z", CultureInfo.InvariantCulture) }
};
var graphs = new List<ReachabilityReplayGraph>
{
new("static", "cas://graph/B", "ABCDEF", "scanner-jvm", "1.0.0"),
new("framework", "cas://graph/A", "abcdef", "scanner-jvm", "1.0.0"),
new("static", "cas://graph/B", "ABCDEF", "scanner-jvm", "1.0.0") // duplicate
};
var traces = new List<ReachabilityReplayTrace>
{
new("zastava", "cas://trace/1", "FFEE", DateTimeOffset.Parse("2025-10-15T09:00:00+02:00", CultureInfo.InvariantCulture)),
new("zastava", "cas://trace/2", "ffee", DateTimeOffset.Parse("2025-10-15T09:05:00Z", CultureInfo.InvariantCulture)),
new("zastava", "cas://trace/1", "FFEE", DateTimeOffset.Parse("2025-10-15T09:00:00Z", CultureInfo.InvariantCulture)) // duplicate once normalized
};
var writer = new StellaOps.Scanner.Reachability.ReachabilityReplayWriter();
writer.AttachEvidence(manifest, graphs, traces);
manifest.Reachability.Should().NotBeNull();
manifest.Reachability!.Graphs.Should().HaveCount(2);
manifest.Reachability.Graphs[0].CasUri.Should().Be("cas://graph/A");
manifest.Reachability.Graphs[0].Sha256.Should().Be("abcdef");
manifest.Reachability.Graphs[1].CasUri.Should().Be("cas://graph/B");
manifest.Reachability.Graphs[1].Kind.Should().Be("static");
manifest.Reachability.RuntimeTraces.Should().HaveCount(2);
manifest.Reachability.RuntimeTraces[0].RecordedAt.Should().Be(DateTimeOffset.Parse("2025-10-15T07:00:00Z"));
manifest.Reachability.RuntimeTraces[0].Sha256.Should().Be("ffee");
manifest.Reachability.RuntimeTraces[1].CasUri.Should().Be("cas://trace/2");
}
[Fact]
public void AttachEvidence_DoesNotCreateSectionWhenEmpty()
{
var manifest = new ReplayManifest();
var writer = new StellaOps.Scanner.Reachability.ReachabilityReplayWriter();
writer.AttachEvidence(manifest, Array.Empty<ReachabilityReplayGraph>(), Array.Empty<ReachabilityReplayTrace>());
manifest.Reachability.AnalysisId.Should().BeNull();
manifest.Reachability.Graphs.Should().BeEmpty();
manifest.Reachability.RuntimeTraces.Should().BeEmpty();
}
}

View File

@@ -1,85 +0,0 @@
using System.Text.Json;
using FluentAssertions;
using Xunit;
namespace StellaOps.Reachability.FixtureTests;
public class ReachbenchEvaluationHarnessTests
{
private static readonly string RepoRoot = LocateRepoRoot();
private static readonly string CasesRoot = Path.Combine(
RepoRoot,
"tests",
"reachability",
"fixtures",
"reachbench-2025-expanded",
"cases");
public static IEnumerable<object[]> CaseIds()
{
return Directory.EnumerateDirectories(CasesRoot)
.OrderBy(path => path, StringComparer.Ordinal)
.Select(path => new object[] { Path.GetFileName(path)! });
}
[Theory]
[MemberData(nameof(CaseIds))]
public void GroundTruthStatusesMatchVariantIntent(string caseId)
{
var caseJsonPath = Path.Combine(CasesRoot, caseId, "case.json");
File.Exists(caseJsonPath).Should().BeTrue();
using var caseDoc = JsonDocument.Parse(File.ReadAllBytes(caseJsonPath));
var groundTruth = caseDoc.RootElement.GetProperty("ground_truth");
groundTruth.GetProperty("reachable_variant")
.GetProperty("status")
.GetString()
.Should()
.Be("affected", $"{caseId} reachable variant should be marked affected for evaluation harness");
groundTruth.GetProperty("unreachable_variant")
.GetProperty("status")
.GetString()
.Should()
.Be("not_affected", $"{caseId} unreachable variant should be marked not_affected for evaluation harness");
}
[Theory]
[MemberData(nameof(CaseIds))]
public void TruthGraphsAlignWithExpectedReachability(string caseId)
{
var reachablePaths = CountTruthPaths(caseId, "reachable");
reachablePaths.Should().BeGreaterThan(0, $"{caseId} reachable variant should expose at least one execution path");
var unreachablePaths = CountTruthPaths(caseId, "unreachable");
unreachablePaths.Should().Be(0, $"{caseId} unreachable variant should have no execution paths");
}
private static int CountTruthPaths(string caseId, string variant)
{
var truthPath = Path.Combine(CasesRoot, caseId, "images", variant, "reachgraph.truth.json");
File.Exists(truthPath).Should().BeTrue();
using var truthDoc = JsonDocument.Parse(File.ReadAllBytes(truthPath));
var paths = truthDoc.RootElement.GetProperty("paths");
paths.ValueKind.Should().Be(JsonValueKind.Array, $"{caseId}:{variant} should list truth paths as an array");
return paths.GetArrayLength();
}
private static string LocateRepoRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current != null)
{
if (File.Exists(Path.Combine(current.FullName, "Directory.Build.props")))
{
return current.FullName;
}
current = current.Parent;
}
throw new InvalidOperationException("Cannot locate repository root (missing Directory.Build.props).");
}
}

View File

@@ -1,159 +0,0 @@
using System.Text.Json;
using FluentAssertions;
using Xunit;
using System.Security.Cryptography;
using System.Linq;
namespace StellaOps.Reachability.FixtureTests;
public class ReachbenchFixtureTests
{
private static readonly string RepoRoot = LocateRepoRoot();
private static readonly string FixtureRoot = Path.Combine(
RepoRoot, "tests", "reachability", "fixtures", "reachbench-2025-expanded");
private static readonly string CasesRoot = Path.Combine(FixtureRoot, "cases");
[Fact]
public void IndexListsAllCases()
{
Directory.Exists(FixtureRoot).Should().BeTrue("reachbench fixtures should exist under tests/reachability/fixtures");
File.Exists(Path.Combine(FixtureRoot, "INDEX.json")).Should().BeTrue("the reachbench index must be present");
using var indexStream = File.OpenRead(Path.Combine(FixtureRoot, "INDEX.json"));
using var document = JsonDocument.Parse(indexStream);
var names = new List<string>();
var found = false;
JsonElement casesElement = default;
foreach (var property in document.RootElement.EnumerateObject())
{
names.Add(property.Name);
if (property.NameEquals("cases"))
{
casesElement = property.Value;
found = true;
}
}
found.Should().BeTrue($"INDEX.json should contain 'cases'. Properties present: {string.Join(",", names)}");
casesElement.ValueKind.Should().Be(JsonValueKind.Array);
casesElement.GetArrayLength().Should().BeGreaterOrEqualTo(20, "expanded pack should carry broad coverage");
foreach (var entry in casesElement.EnumerateArray())
{
var id = entry.GetProperty("id").GetString();
id.Should().NotBeNullOrEmpty();
var rel = entry.TryGetProperty("path", out var relProp)
? relProp.GetString()
: Path.Combine("cases", id!);
rel.Should().NotBeNullOrEmpty();
var path = Path.Combine(FixtureRoot, rel!);
Directory.Exists(path).Should().BeTrue($"case '{id}' folder '{rel}' should exist");
}
}
public static IEnumerable<object[]> CaseVariantData()
{
foreach (var caseDir in Directory.EnumerateDirectories(CasesRoot))
{
var caseId = Path.GetFileName(caseDir);
yield return new object[] { caseId!, Path.Combine(caseDir, "images", "reachable") };
yield return new object[] { caseId!, Path.Combine(caseDir, "images", "unreachable") };
}
}
[Theory]
[MemberData(nameof(CaseVariantData))]
public void CaseVariantContainsExpectedArtifacts(string caseId, string variantPath)
{
Directory.Exists(variantPath).Should().BeTrue();
var requiredFiles = new[]
{
"manifest.json",
"sbom.cdx.json",
"sbom.spdx.json",
"symbols.json",
"callgraph.static.json",
"callgraph.framework.json",
"reachgraph.truth.json",
"vex.openvex.json",
"attestation.dsse.json"
};
foreach (var file in requiredFiles)
{
File.Exists(Path.Combine(variantPath, file)).Should().BeTrue($"{caseId}:{Path.GetFileName(variantPath)} missing {file}");
}
var truthPath = Path.Combine(variantPath, "reachgraph.truth.json");
using var truthStream = File.OpenRead(truthPath);
using var truthDoc = JsonDocument.Parse(truthStream);
truthDoc.RootElement.GetProperty("schema_version").GetString().Should().NotBeNullOrEmpty();
truthDoc.RootElement.GetProperty("paths").ValueKind.Should().Be(JsonValueKind.Array);
VerifyManifestHashes(caseId, variantPath, requiredFiles);
}
[Theory]
[MemberData(nameof(CaseVariantData))]
public void CaseGroundTruthMatchesVariants(string caseId, string variantPath)
{
var caseJsonPath = Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(variantPath))!, "case.json");
File.Exists(caseJsonPath).Should().BeTrue();
using var caseStream = File.OpenRead(caseJsonPath);
using var caseDoc = JsonDocument.Parse(caseStream);
var groundTruth = caseDoc.RootElement.GetProperty("ground_truth");
var variantKey = variantPath.EndsWith("reachable", StringComparison.OrdinalIgnoreCase)
? "reachable_variant"
: "unreachable_variant";
var variant = groundTruth.GetProperty(variantKey);
variant.GetProperty("status").GetString().Should().NotBeNullOrEmpty($"{caseId}:{variantKey} should set status");
variant.TryGetProperty("evidence", out var evidence).Should().BeTrue($"{caseId}:{variantKey} should define evidence");
evidence.TryGetProperty("paths", out var pathsProp).Should().BeTrue();
pathsProp.ValueKind.Should().Be(JsonValueKind.Array);
var truthPath = Path.Combine(variantPath, "reachgraph.truth.json");
using var truthStream = File.OpenRead(truthPath);
using var truthDoc = JsonDocument.Parse(truthStream);
var paths = truthDoc.RootElement.GetProperty("paths");
paths.ValueKind.Should().Be(JsonValueKind.Array);
}
internal static string LocateRepoRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current != null)
{
if (File.Exists(Path.Combine(current.FullName, "Directory.Build.props")))
{
return current.FullName;
}
current = current.Parent;
}
throw new InvalidOperationException("Cannot locate repository root (missing Directory.Build.props).");
}
private static void VerifyManifestHashes(string caseId, string variantPath, IEnumerable<string> requiredFiles)
{
var manifestPath = Path.Combine(variantPath, "manifest.json");
using var manifestStream = File.OpenRead(manifestPath);
using var manifestDoc = JsonDocument.Parse(manifestStream);
var files = manifestDoc.RootElement.GetProperty("files");
foreach (var file in requiredFiles.Where(f => f != "manifest.json"))
{
files.TryGetProperty(file, out var hashProp).Should().BeTrue($"{caseId}:{variantPath} manifest missing hash for {file}");
var expectedHash = hashProp.GetString();
expectedHash.Should().NotBeNullOrEmpty($"{caseId}:{variantPath} hash missing for {file}");
var path = Path.Combine(variantPath, file);
var actualHash = BitConverter.ToString(SHA256.HashData(File.ReadAllBytes(path))).Replace("-", "").ToLowerInvariant();
actualHash.Should().Be(expectedHash, $"{caseId}:{variantPath} hash mismatch for {file}");
}
}
}

View File

@@ -1,76 +0,0 @@
using System.Security.Cryptography;
using System.Text.Json;
using FluentAssertions;
using Xunit;
namespace StellaOps.Reachability.FixtureTests;
public class SamplesPublicFixtureTests
{
private static readonly string RepoRoot = ReachbenchFixtureTests.LocateRepoRoot();
private static readonly string SamplesPublicRoot = Path.Combine(RepoRoot, "tests", "reachability", "samples-public");
private static readonly string SamplesRoot = Path.Combine(SamplesPublicRoot, "samples");
private static readonly string[] RequiredFiles =
[
"callgraph.static.json",
"ground-truth.json",
"sbom.cdx.json",
"vex.openvex.json",
"repro.sh"
];
[Fact]
public void ManifestExistsAndIsSorted()
{
var manifestPath = Path.Combine(SamplesPublicRoot, "manifest.json");
File.Exists(manifestPath).Should().BeTrue("samples-public manifest should exist");
using var stream = File.OpenRead(manifestPath);
using var doc = JsonDocument.Parse(stream);
doc.RootElement.ValueKind.Should().Be(JsonValueKind.Array);
var keys = doc.RootElement.EnumerateArray()
.Select(entry => $"{entry.GetProperty("language").GetString()}/{entry.GetProperty("id").GetString()}")
.ToArray();
keys.Should().NotBeEmpty("samples-public manifest should have entries");
keys.Should().BeInAscendingOrder(StringComparer.Ordinal);
}
[Fact]
public void SamplesPublicEntriesMatchManifestHashes()
{
var manifestPath = Path.Combine(SamplesPublicRoot, "manifest.json");
var manifest = JsonDocument.Parse(File.ReadAllBytes(manifestPath)).RootElement.EnumerateArray().ToArray();
manifest.Should().NotBeEmpty("samples-public manifest must have entries");
foreach (var entry in manifest)
{
var id = entry.GetProperty("id").GetString();
var language = entry.GetProperty("language").GetString();
var files = entry.GetProperty("files");
id.Should().NotBeNullOrEmpty();
language.Should().NotBeNullOrEmpty();
files.ValueKind.Should().Be(JsonValueKind.Object);
var caseDir = Path.Combine(SamplesRoot, language!, id!);
Directory.Exists(caseDir).Should().BeTrue($"case folder missing: {caseDir}");
foreach (var filename in RequiredFiles)
{
files.TryGetProperty(filename, out var expectedHashProp).Should().BeTrue($"{id} manifest missing {filename}");
var expectedHash = expectedHashProp.GetString();
expectedHash.Should().NotBeNullOrEmpty($"{id} expected hash missing for {filename}");
var filePath = Path.Combine(caseDir, filename);
File.Exists(filePath).Should().BeTrue($"{id} missing {filename}");
var actualHash = BitConverter.ToString(SHA256.HashData(File.ReadAllBytes(filePath))).Replace("-", "").ToLowerInvariant();
actualHash.Should().Be(expectedHash, $"{id} hash mismatch for {filename}");
}
}
}
}

View File

@@ -1,29 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="System.Text.Json" Version="10.0.0-preview.7.24405.7" />
</ItemGroup>
<ItemGroup>
<Content Include="..\\..\\fixtures\\**\\*">
<Link>fixtures\\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Scanner\__Libraries\StellaOps.Scanner.Reachability\StellaOps.Scanner.Reachability.csproj" />
<ProjectReference Include="..\..\..\src\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,186 +0,0 @@
using FluentAssertions;
using StellaOps.Scanner.Reachability;
using Xunit;
namespace StellaOps.Reachability.FixtureTests;
public sealed class SymbolIdTests
{
[Fact]
public void ForJava_CreatesCanonicalSymbolId()
{
var id = SymbolId.ForJava("com.example", "MyClass", "doSomething", "(Ljava/lang/String;)V");
id.Should().StartWith("sym:java:");
id.Should().HaveLength("sym:java:".Length + 43); // Base64url SHA-256 without padding = 43 chars
}
[Fact]
public void ForJava_IsDeterministic()
{
var id1 = SymbolId.ForJava("com.example", "MyClass", "doSomething", "(Ljava/lang/String;)V");
var id2 = SymbolId.ForJava("com.example", "MyClass", "doSomething", "(Ljava/lang/String;)V");
id1.Should().Be(id2);
}
[Fact]
public void ForJava_IsCaseInsensitive()
{
var id1 = SymbolId.ForJava("com.example", "MyClass", "doSomething", "()V");
var id2 = SymbolId.ForJava("COM.EXAMPLE", "MYCLASS", "DOSOMETHING", "()V");
id1.Should().Be(id2);
}
[Fact]
public void ForDotNet_CreatesCanonicalSymbolId()
{
var id = SymbolId.ForDotNet("MyAssembly", "MyNamespace", "MyClass", "MyMethod(System.String)");
id.Should().StartWith("sym:dotnet:");
}
[Fact]
public void ForDotNet_DifferentSignaturesProduceDifferentIds()
{
var id1 = SymbolId.ForDotNet("MyAssembly", "MyNamespace", "MyClass", "Method(String)");
var id2 = SymbolId.ForDotNet("MyAssembly", "MyNamespace", "MyClass", "Method(Int32)");
id1.Should().NotBe(id2);
}
[Fact]
public void ForNode_CreatesCanonicalSymbolId()
{
var id = SymbolId.ForNode("express", "lib/router", "function");
id.Should().StartWith("sym:node:");
}
[Fact]
public void ForNode_HandlesScopedPackages()
{
var id1 = SymbolId.ForNode("@angular/core", "src/render", "function");
var id2 = SymbolId.ForNode("@angular/core", "src/render", "function");
id1.Should().Be(id2);
id1.Should().StartWith("sym:node:");
}
[Fact]
public void ForGo_CreatesCanonicalSymbolId()
{
var id = SymbolId.ForGo("github.com/example/repo", "pkg/http", "Server", "HandleRequest");
id.Should().StartWith("sym:go:");
}
[Fact]
public void ForGo_FunctionWithoutReceiver()
{
var id = SymbolId.ForGo("github.com/example/repo", "pkg/main", "", "main");
id.Should().StartWith("sym:go:");
}
[Fact]
public void ForRust_CreatesCanonicalSymbolId()
{
var id = SymbolId.ForRust("my_crate", "foo::bar", "my_function", "_ZN8my_crate3foo3bar11my_functionE");
id.Should().StartWith("sym:rust:");
}
[Fact]
public void ForSwift_CreatesCanonicalSymbolId()
{
var id = SymbolId.ForSwift("MyModule", "MyClass", "myMethod", null);
id.Should().StartWith("sym:swift:");
}
[Fact]
public void ForShell_CreatesCanonicalSymbolId()
{
var id = SymbolId.ForShell("scripts/deploy.sh", "run_migration");
id.Should().StartWith("sym:shell:");
}
[Fact]
public void ForBinary_CreatesCanonicalSymbolId()
{
var id = SymbolId.ForBinary("7f6e5d4c3b2a1908", ".text", "_start");
id.Should().StartWith("sym:binary:");
}
[Fact]
public void ForPython_CreatesCanonicalSymbolId()
{
var id = SymbolId.ForPython("requests", "requests.api", "get");
id.Should().StartWith("sym:python:");
}
[Fact]
public void ForRuby_CreatesCanonicalSymbolId()
{
var id = SymbolId.ForRuby("rails", "ActiveRecord::Base", "#save");
id.Should().StartWith("sym:ruby:");
}
[Fact]
public void ForPhp_CreatesCanonicalSymbolId()
{
var id = SymbolId.ForPhp("laravel/framework", "Illuminate\\Http", "Request::input");
id.Should().StartWith("sym:php:");
}
[Fact]
public void Parse_ValidSymbolId_ReturnsComponents()
{
var id = SymbolId.ForJava("com.example", "MyClass", "method", "()V");
var result = SymbolId.Parse(id);
result.Should().NotBeNull();
result!.Value.Lang.Should().Be("java");
result.Value.Fragment.Should().HaveLength(43);
}
[Fact]
public void Parse_InvalidSymbolId_ReturnsNull()
{
SymbolId.Parse("invalid").Should().BeNull();
SymbolId.Parse("sym:").Should().BeNull();
SymbolId.Parse("sym:java").Should().BeNull();
SymbolId.Parse("").Should().BeNull();
SymbolId.Parse(null!).Should().BeNull();
}
[Fact]
public void FromTuple_CreatesSymbolIdFromRawTuple()
{
var tuple = "my\0canonical\0tuple";
var id = SymbolId.FromTuple("custom", tuple);
id.Should().StartWith("sym:custom:");
}
[Fact]
public void AllLanguagesAreDifferent()
{
// Same tuple data should produce different IDs for different languages
var java = SymbolId.ForJava("pkg", "cls", "meth", "()V");
var dotnet = SymbolId.ForDotNet("pkg", "cls", "meth", "()V");
var node = SymbolId.ForNode("pkg", "cls", "meth");
java.Should().NotBe(dotnet);
dotnet.Should().NotBe(node);
java.Should().NotBe(node);
}
}

View File

@@ -1,34 +0,0 @@
using System.Text.Json;
using FluentAssertions;
using StellaOps.Replay.Core;
using Xunit;
namespace StellaOps.Replay.Core.Tests;
public sealed class CanonicalJsonTests
{
[Fact]
public void CanonicalJson_OrdersPropertiesLexicographically()
{
var payload = new
{
zeta = 1,
alpha = new { z = 9, m = 7 },
list = new[] { new { y = 2, x = 1 } }
};
var canonical = CanonicalJson.Serialize(payload);
canonical.Should().Be("{\"alpha\":{\"m\":7,\"z\":9},\"list\":[{\"x\":1,\"y\":2}],\"zeta\":1}");
}
[Fact]
public void CanonicalJson_PreservesNumbersAndBooleans()
{
var payload = JsonSerializer.Deserialize<JsonElement>("{\"b\":true,\"a\":1.25}");
var canonical = CanonicalJson.Serialize(payload);
canonical.Should().Be("{\"a\":1.25,\"b\":true}");
}
}

View File

@@ -1,30 +0,0 @@
using System.Text;
using FluentAssertions;
using StellaOps.Replay.Core;
using Xunit;
namespace StellaOps.Replay.Core.Tests;
public sealed class DeterministicHashTests
{
[Fact]
public void Sha256Hex_ComputesLowercaseDigest()
{
var digest = DeterministicHash.Sha256Hex("replay-core");
digest.Should().Be("a914f5ac6a57aab0189bb55bcb0ef6bcdbd86f77198c8669eab5ae38a325e41d");
}
[Fact]
public void MerkleRootHex_IsDeterministic()
{
var leaves = new[] { "alpha", "beta", "gamma" }
.Select(Encoding.UTF8.GetBytes)
.ToList();
var root = DeterministicHash.MerkleRootHex(leaves);
root.Should().Be("50298939464ed02cbf2b587250a55746b3422e133ac4f09b7e2b07869023bc9e");
DeterministicHash.MerkleRootHex(leaves).Should().Be(root);
}
}

View File

@@ -1,34 +0,0 @@
using System;
using System.Text;
using FluentAssertions;
using StellaOps.Replay.Core;
using Xunit;
namespace StellaOps.Replay.Core.Tests;
public sealed class DsseEnvelopeTests
{
[Fact]
public void BuildUnsigned_ProducesCanonicalPayload()
{
var manifest = new ReplayManifest
{
Scan = new ReplayScanMetadata
{
Id = "scan-123",
Time = DateTimeOffset.UnixEpoch
}
};
var envelope = DssePayloadBuilder.BuildUnsigned(manifest);
envelope.PayloadType.Should().Be(DssePayloadBuilder.ReplayPayloadType);
envelope.Signatures.Should().BeEmpty();
var payload = Convert.FromBase64String(envelope.Payload);
var json = Encoding.UTF8.GetString(payload);
json.Should().Be("{\"reachability\":{\"graphs\":[],\"runtimeTraces\":[]},\"scan\":{\"id\":\"scan-123\",\"time\":\"1970-01-01T00:00:00+00:00\"},\"schemaVersion\":\"1.0\"}");
envelope.DigestSha256.Should().Be(DeterministicHash.Sha256Hex(payload));
}
}

View File

@@ -1,64 +0,0 @@
using System.Collections.Generic;
using System.Formats.Tar;
using System.IO;
using FluentAssertions;
using StellaOps.Replay.Core;
using ZstdSharp;
using Xunit;
namespace StellaOps.Replay.Core.Tests;
public sealed class ReplayBundleWriterTests
{
[Fact]
public async Task WriteTarZstAsync_IsDeterministicAndSorted()
{
var entries = new[]
{
new ReplayBundleEntry("b.txt", "beta"u8.ToArray()),
new ReplayBundleEntry("a.txt", "alpha"u8.ToArray())
};
await using var buffer = new MemoryStream();
var first = await ReplayBundleWriter.WriteTarZstAsync(entries, buffer, compressionLevel: 3);
var firstBytes = buffer.ToArray();
await using var buffer2 = new MemoryStream();
var second = await ReplayBundleWriter.WriteTarZstAsync(entries.Reverse(), buffer2, compressionLevel: 3);
first.ZstSha256.Should().Be(second.ZstSha256);
first.TarSha256.Should().Be(second.TarSha256);
firstBytes.Should().Equal(buffer2.ToArray());
// Decompress and verify ordering/content
buffer.Position = 0;
await using var decompressed = new MemoryStream();
await using (var decompress = new DecompressionStream(buffer, 16 * 1024, leaveOpen: true, enableMultiThreaded: false))
{
await decompress.CopyToAsync(decompressed);
}
decompressed.Position = 0;
var reader = new TarReader(decompressed, leaveOpen: true);
var names = new List<string>();
TarEntry? entry;
while ((entry = reader.GetNextEntry()) != null)
{
names.Add(entry.Name);
using var ms = new MemoryStream();
entry.DataStream!.CopyTo(ms);
var text = System.Text.Encoding.UTF8.GetString(ms.ToArray());
text.Should().Be(entry.Name.StartsWith("a") ? "alpha" : "beta");
}
names.Should().BeEquivalentTo(new[] { "a.txt", "b.txt" }, opts => opts.WithStrictOrdering());
}
[Fact]
public void BuildCasUri_UsesPrefixAndShard()
{
ReplayBundleWriter.BuildCasUri("abcdef", null).Should().Be("cas://replay/ab/abcdef.tar.zst");
ReplayBundleWriter.BuildCasUri("1234", "custom").Should().Be("cas://custom/12/1234.tar.zst");
}
}

View File

@@ -1,41 +0,0 @@
using System.Text.Json;
using FluentAssertions;
using StellaOps.Replay.Core;
using Xunit;
namespace StellaOps.Replay.Core.Tests;
public sealed class ReplayManifestExtensionsTests
{
[Fact]
public void AddsReachabilityEvidence()
{
var manifest = new ReplayManifest
{
Scan = new ReplayScanMetadata { Id = "scan-1" }
};
manifest.AddReachabilityGraph(new ReplayReachabilityGraphReference
{
Kind = "static",
Analyzer = "scanner/java",
CasUri = "cas://replay/graph",
Sha256 = "abc",
Version = "1.0"
});
manifest.AddReachabilityTrace(new ReplayReachabilityTraceReference
{
Source = "zastava",
CasUri = "cas://replay/trace",
Sha256 = "def"
});
manifest.Reachability.Should().NotBeNull();
manifest.Reachability!.Graphs.Should().HaveCount(1);
manifest.Reachability.RuntimeTraces.Should().HaveCount(1);
var json = JsonSerializer.Serialize(manifest);
json.Should().Contain("\"reachability\"");
}
}

View File

@@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../src/__Libraries/StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,613 +0,0 @@
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
}

View File

@@ -1,382 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Reachability;
using StellaOps.Signals.Models;
using StellaOps.Signals.Options;
using StellaOps.Signals.Parsing;
using StellaOps.Signals.Persistence;
using StellaOps.Signals.Services;
using StellaOps.Signals.Storage;
using StellaOps.Signals.Storage.Models;
using Xunit;
namespace StellaOps.ScannerSignals.IntegrationTests;
public sealed class ScannerToSignalsReachabilityTests
{
private static readonly string RepoRoot = LocateRepoRoot();
private static readonly string FixtureRoot = Path.Combine(RepoRoot, "tests", "reachability", "fixtures", "reachbench-2025-expanded", "cases");
[Fact]
public async Task ScannerBuilderFeedsSignalsScoringPipeline()
{
var caseId = "java-log4j-CVE-2021-44228-log4shell";
var variant = "reachable";
var variantPath = Path.Combine(FixtureRoot, caseId, "images", variant);
Directory.Exists(variantPath).Should().BeTrue();
var truth = JsonDocument.Parse(File.ReadAllText(Path.Combine(variantPath, "reachgraph.truth.json"))).RootElement;
var paths = truth.GetProperty("paths")
.EnumerateArray()
.Select(path => path.EnumerateArray().Select(x => x.GetString()!).Where(x => !string.IsNullOrWhiteSpace(x)).ToList())
.Where(path => path.Count > 0)
.ToList();
var builder = new ReachabilityGraphBuilder();
foreach (var path in paths)
{
for (var i = 0; i < path.Count; i++)
{
builder.AddNode(path[i]);
if (i + 1 < path.Count)
{
builder.AddEdge(path[i], path[i + 1]);
}
}
}
var artifactJson = builder.BuildJson(indented: false);
var parser = new SimpleJsonCallgraphParser("java");
var parserResolver = new StaticParserResolver(new Dictionary<string, ICallgraphParser>
{
["java"] = parser
});
var artifactStore = new InMemoryCallgraphArtifactStore();
var callgraphRepo = new InMemoryCallgraphRepository();
var reachabilityStore = new InMemoryReachabilityStoreRepository(TimeProvider.System);
var ingestionService = new CallgraphIngestionService(
parserResolver,
artifactStore,
callgraphRepo,
reachabilityStore,
new CallgraphNormalizationService(),
new NullCallGraphSyncService(),
Options.Create(new SignalsOptions()),
TimeProvider.System,
NullLogger<CallgraphIngestionService>.Instance);
var request = new CallgraphIngestRequest(
Language: "java",
Component: caseId,
Version: variant,
ArtifactContentType: "application/json",
ArtifactFileName: "callgraph.static.json",
ArtifactContentBase64: Convert.ToBase64String(Encoding.UTF8.GetBytes(artifactJson)),
Metadata: null);
var ingestResponse = await ingestionService.IngestAsync(request, CancellationToken.None);
ingestResponse.CallgraphId.Should().NotBeNullOrWhiteSpace();
var scoringOptions = new SignalsOptions();
var scoringService = new ReachabilityScoringService(
callgraphRepo,
new InMemoryReachabilityFactRepository(),
TimeProvider.System,
Options.Create(scoringOptions),
new InMemoryReachabilityCache(),
new InMemoryUnknownsRepository(),
new NullEventsPublisher(),
NullLogger<ReachabilityScoringService>.Instance);
var entryPoints = paths
.Select(path => path[0])
.Distinct(StringComparer.Ordinal)
.ToList();
var targets = paths
.Select(path => path[^1])
.Distinct(StringComparer.Ordinal)
.ToList();
var recomputeRequest = new ReachabilityRecomputeRequest
{
CallgraphId = ingestResponse.CallgraphId,
Subject = new ReachabilitySubject
{
ScanId = $"{caseId}:{variant}",
Component = caseId,
Version = variant
},
EntryPoints = entryPoints,
Targets = targets,
RuntimeHits = ReadRuntimeHits(Path.Combine(variantPath, "traces.runtime.jsonl"))
};
var fact = await scoringService.RecomputeAsync(recomputeRequest, CancellationToken.None);
fact.States.Should().ContainSingle(state => state.Target == targets[0] && state.Reachable);
}
private static List<string> ReadRuntimeHits(string tracePath)
{
var hits = new List<string>();
if (!File.Exists(tracePath))
{
return hits;
}
foreach (var line in File.ReadLines(tracePath))
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
using var doc = JsonDocument.Parse(line);
if (doc.RootElement.TryGetProperty("sid", out var sid))
{
hits.Add(sid.GetString()!);
}
}
return hits;
}
private sealed class StaticParserResolver : ICallgraphParserResolver
{
private readonly IReadOnlyDictionary<string, ICallgraphParser> parsers;
public StaticParserResolver(IReadOnlyDictionary<string, ICallgraphParser> parsers)
{
this.parsers = parsers;
}
public ICallgraphParser Resolve(string language)
{
if (parsers.TryGetValue(language, out var parser))
{
return parser;
}
throw new CallgraphParserNotFoundException(language);
}
}
private sealed class InMemoryCallgraphRepository : ICallgraphRepository
{
private readonly Dictionary<string, CallgraphDocument> storage = new(StringComparer.Ordinal);
public Task<CallgraphDocument?> GetByIdAsync(string id, CancellationToken cancellationToken)
{
storage.TryGetValue(id, out var document);
return Task.FromResult(document);
}
public Task<CallgraphDocument> UpsertAsync(CallgraphDocument document, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(document.Id))
{
document.Id = $"cg-{storage.Count + 1}";
}
storage[document.Id] = document;
return Task.FromResult(document);
}
}
private sealed class InMemoryReachabilityFactRepository : IReachabilityFactRepository
{
private readonly Dictionary<string, ReachabilityFactDocument> storage = new(StringComparer.Ordinal);
public Task<ReachabilityFactDocument?> GetBySubjectAsync(string subjectKey, CancellationToken cancellationToken)
{
storage.TryGetValue(subjectKey, out var document);
return Task.FromResult(document);
}
public Task<ReachabilityFactDocument> UpsertAsync(ReachabilityFactDocument document, CancellationToken cancellationToken)
{
storage[document.SubjectKey] = document;
return Task.FromResult(document);
}
public Task<IReadOnlyList<ReachabilityFactDocument>> GetExpiredAsync(DateTimeOffset cutoff, int limit, CancellationToken cancellationToken)
=> Task.FromResult((IReadOnlyList<ReachabilityFactDocument>)Array.Empty<ReachabilityFactDocument>());
public Task<bool> DeleteAsync(string subjectKey, CancellationToken cancellationToken)
{
var removed = storage.Remove(subjectKey);
return Task.FromResult(removed);
}
public Task<int> GetRuntimeFactsCountAsync(string subjectKey, CancellationToken cancellationToken)
=> Task.FromResult(0);
public Task TrimRuntimeFactsAsync(string subjectKey, int maxCount, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
private sealed class InMemoryReachabilityCache : IReachabilityCache
{
private readonly Dictionary<string, ReachabilityFactDocument> storage = new(StringComparer.Ordinal);
public Task<ReachabilityFactDocument?> GetAsync(string subjectKey, CancellationToken cancellationToken)
{
storage.TryGetValue(subjectKey, out var document);
return Task.FromResult(document);
}
public Task SetAsync(ReachabilityFactDocument document, CancellationToken cancellationToken)
{
storage[document.SubjectKey] = document;
return Task.CompletedTask;
}
public Task InvalidateAsync(string subjectKey, CancellationToken cancellationToken)
{
storage.Remove(subjectKey);
return Task.CompletedTask;
}
}
private sealed class InMemoryUnknownsRepository : IUnknownsRepository
{
public Task UpsertAsync(string subjectKey, IEnumerable<UnknownSymbolDocument> items, CancellationToken cancellationToken) =>
Task.CompletedTask;
public Task<IReadOnlyList<UnknownSymbolDocument>> GetBySubjectAsync(string subjectKey, CancellationToken cancellationToken) =>
Task.FromResult((IReadOnlyList<UnknownSymbolDocument>)Array.Empty<UnknownSymbolDocument>());
public Task<int> CountBySubjectAsync(string subjectKey, CancellationToken cancellationToken) =>
Task.FromResult(0);
public Task BulkUpdateAsync(IEnumerable<UnknownSymbolDocument> items, CancellationToken cancellationToken) =>
Task.CompletedTask;
public Task<IReadOnlyList<string>> GetAllSubjectKeysAsync(CancellationToken cancellationToken) =>
Task.FromResult((IReadOnlyList<string>)Array.Empty<string>());
public Task<IReadOnlyList<UnknownSymbolDocument>> GetDueForRescanAsync(
UnknownsBand band,
int limit,
CancellationToken cancellationToken) =>
Task.FromResult((IReadOnlyList<UnknownSymbolDocument>)Array.Empty<UnknownSymbolDocument>());
public Task<IReadOnlyList<UnknownSymbolDocument>> QueryAsync(
UnknownsBand? band,
int limit,
int offset,
CancellationToken cancellationToken) =>
Task.FromResult((IReadOnlyList<UnknownSymbolDocument>)Array.Empty<UnknownSymbolDocument>());
public Task<UnknownSymbolDocument?> GetByIdAsync(string id, CancellationToken cancellationToken) =>
Task.FromResult<UnknownSymbolDocument?>(null);
}
private sealed class NullEventsPublisher : IEventsPublisher
{
public Task PublishFactUpdatedAsync(ReachabilityFactDocument fact, CancellationToken cancellationToken) => Task.CompletedTask;
}
private sealed class NullCallGraphSyncService : ICallGraphSyncService
{
public Task<CallGraphSyncResult> SyncAsync(Guid scanId, string artifactDigest, CallgraphDocument document, CancellationToken cancellationToken)
=> Task.FromResult(new CallGraphSyncResult(scanId, 0, 0, 0, false, 0L));
public Task DeleteByScanAsync(Guid scanId, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
private sealed class InMemoryCallgraphArtifactStore : ICallgraphArtifactStore
{
private readonly Dictionary<string, byte[]> artifacts = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, byte[]> manifests = new(StringComparer.OrdinalIgnoreCase);
public async Task<StoredCallgraphArtifact> SaveAsync(CallgraphArtifactSaveRequest request, Stream content, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(content);
await using var buffer = new MemoryStream();
await content.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
var bytes = buffer.ToArray();
var computedHash = Convert.ToHexString(SHA256.HashData(bytes));
if (content.CanSeek)
{
content.Position = 0;
}
if (!computedHash.Equals(request.Hash, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Hash mismatch for {request.FileName}: expected {request.Hash} but computed {computedHash}.");
}
var casUri = $"cas://fixtures/{request.Component}/{request.Version}/{computedHash}";
var manifestPath = $"cas://fixtures/{request.Component}/{request.Version}/{computedHash}/manifest";
artifacts[computedHash] = bytes;
if (request.ManifestContent is not null)
{
await using var manifestBuffer = new MemoryStream();
await request.ManifestContent.CopyToAsync(manifestBuffer, cancellationToken).ConfigureAwait(false);
manifests[computedHash] = manifestBuffer.ToArray();
}
return new StoredCallgraphArtifact(
Path: $"fixtures/{request.Component}/{request.Version}/{request.FileName}",
Length: bytes.Length,
Hash: computedHash,
ContentType: request.ContentType,
CasUri: casUri,
ManifestPath: manifestPath,
ManifestCasUri: manifestPath);
}
public Task<Stream?> GetAsync(string hash, string? fileName, CancellationToken cancellationToken)
{
if (!artifacts.TryGetValue(hash, out var bytes))
{
return Task.FromResult<Stream?>(null);
}
return Task.FromResult<Stream?>(new MemoryStream(bytes, writable: false));
}
public Task<Stream?> GetManifestAsync(string hash, CancellationToken cancellationToken)
{
if (!manifests.TryGetValue(hash, out var bytes))
{
return Task.FromResult<Stream?>(null);
}
return Task.FromResult<Stream?>(new MemoryStream(bytes, writable: false));
}
public Task<bool> ExistsAsync(string hash, CancellationToken cancellationToken)
=> Task.FromResult(artifacts.ContainsKey(hash));
}
private static string LocateRepoRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current != null)
{
if (File.Exists(Path.Combine(current.FullName, "Directory.Build.props")))
{
return current.FullName;
}
current = current.Parent;
}
throw new InvalidOperationException("Cannot locate repository root (missing Directory.Build.props).");
}
}

View File

@@ -1,30 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../src/Signals/StellaOps.Signals/StellaOps.Signals.csproj" />
<ProjectReference Include="../../../src/Scanner/__Libraries/StellaOps.Scanner.Reachability/StellaOps.Scanner.Reachability.csproj" />
<ProjectReference Include="../../../src/Scanner/__Libraries/StellaOps.Scanner.ReachabilityDrift/StellaOps.Scanner.ReachabilityDrift.csproj" />
<ProjectReference Include="../../../src/Scanner/__Libraries/StellaOps.Scanner.CallGraph/StellaOps.Scanner.CallGraph.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\..\fixtures\**\*">
<Link>fixtures\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -1,732 +0,0 @@
using FluentAssertions;
using StellaOps.Signals.Models;
using StellaOps.Signals.Parsing;
using Xunit;
namespace StellaOps.Signals.Reachability.Tests;
/// <summary>
/// Unit tests for CallgraphSchemaMigrator.
/// Verifies schema migration from legacy format to stella.callgraph.v1.
/// </summary>
public class CallgraphSchemaMigratorTests
{
#region EnsureV1 - Schema Version Tests
[Fact]
public void EnsureV1_SetsSchemaToV1_WhenNotSet()
{
// Arrange
var document = new CallgraphDocument
{
Schema = string.Empty
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Schema.Should().Be(CallgraphSchemaVersions.V1);
}
[Fact]
public void EnsureV1_PreservesV1Schema_WhenAlreadySet()
{
// Arrange
var document = new CallgraphDocument
{
Schema = CallgraphSchemaVersions.V1
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Schema.Should().Be(CallgraphSchemaVersions.V1);
}
[Fact]
public void EnsureV1_UpdatesLegacySchema_ToV1()
{
// Arrange
var document = new CallgraphDocument
{
Schema = "legacy-schema-1.0"
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Schema.Should().Be(CallgraphSchemaVersions.V1);
}
#endregion
#region EnsureV1 - Language Parsing Tests
[Theory]
[InlineData("dotnet", CallgraphLanguage.DotNet)]
[InlineData(".net", CallgraphLanguage.DotNet)]
[InlineData("csharp", CallgraphLanguage.DotNet)]
[InlineData("c#", CallgraphLanguage.DotNet)]
[InlineData("java", CallgraphLanguage.Java)]
[InlineData("node", CallgraphLanguage.Node)]
[InlineData("nodejs", CallgraphLanguage.Node)]
[InlineData("javascript", CallgraphLanguage.Node)]
[InlineData("typescript", CallgraphLanguage.Node)]
[InlineData("python", CallgraphLanguage.Python)]
[InlineData("go", CallgraphLanguage.Go)]
[InlineData("golang", CallgraphLanguage.Go)]
[InlineData("rust", CallgraphLanguage.Rust)]
[InlineData("ruby", CallgraphLanguage.Ruby)]
[InlineData("php", CallgraphLanguage.Php)]
[InlineData("binary", CallgraphLanguage.Binary)]
[InlineData("native", CallgraphLanguage.Binary)]
[InlineData("elf", CallgraphLanguage.Binary)]
[InlineData("swift", CallgraphLanguage.Swift)]
[InlineData("kotlin", CallgraphLanguage.Kotlin)]
public void EnsureV1_ParsesLanguageString_ToEnum(string languageString, CallgraphLanguage expected)
{
// Arrange
var document = new CallgraphDocument
{
Language = languageString,
LanguageType = CallgraphLanguage.Unknown
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.LanguageType.Should().Be(expected);
}
[Fact]
public void EnsureV1_PreservesLanguageType_WhenAlreadySet()
{
// Arrange
var document = new CallgraphDocument
{
Language = "java",
LanguageType = CallgraphLanguage.DotNet // Already set to something different
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.LanguageType.Should().Be(CallgraphLanguage.DotNet);
}
#endregion
#region EnsureV1 - Node Visibility Inference Tests
[Fact]
public void EnsureV1_InfersPublicVisibility_ForStandardNames()
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "node1", Name = "ProcessOrder", Visibility = SymbolVisibility.Unknown }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Should().ContainSingle()
.Which.Visibility.Should().Be(SymbolVisibility.Public);
}
[Fact]
public void EnsureV1_InfersPrivateVisibility_ForUnderscorePrefixed()
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "node1", Name = "_privateMethod", Visibility = SymbolVisibility.Unknown }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Should().ContainSingle()
.Which.Visibility.Should().Be(SymbolVisibility.Private);
}
[Fact]
public void EnsureV1_InfersPrivateVisibility_ForAngleBracketNames()
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "node1", Name = "<Main>$", Visibility = SymbolVisibility.Unknown }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Should().ContainSingle()
.Which.Visibility.Should().Be(SymbolVisibility.Private);
}
[Fact]
public void EnsureV1_InfersInternalVisibility_ForInternalNamespace()
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "node1", Name = "Helper", Namespace = "MyApp.Internal.Utils", Visibility = SymbolVisibility.Unknown }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Should().ContainSingle()
.Which.Visibility.Should().Be(SymbolVisibility.Internal);
}
[Fact]
public void EnsureV1_PreservesVisibility_WhenAlreadySet()
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "node1", Name = "_privateMethod", Visibility = SymbolVisibility.Protected }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Should().ContainSingle()
.Which.Visibility.Should().Be(SymbolVisibility.Protected);
}
#endregion
#region EnsureV1 - Symbol Key Building Tests
[Fact]
public void EnsureV1_BuildsSymbolKey_FromNamespaceAndName()
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "node1", Name = "ProcessOrder", Namespace = "MyApp.Services" }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Should().ContainSingle()
.Which.SymbolKey.Should().Be("MyApp.Services.ProcessOrder");
}
[Fact]
public void EnsureV1_BuildsSymbolKey_FromNameOnly_WhenNoNamespace()
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "node1", Name = "GlobalMethod" }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Should().ContainSingle()
.Which.SymbolKey.Should().Be("GlobalMethod");
}
[Fact]
public void EnsureV1_PreservesSymbolKey_WhenAlreadySet()
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "node1", Name = "Method", Namespace = "Ns", SymbolKey = "Custom.Key" }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Should().ContainSingle()
.Which.SymbolKey.Should().Be("Custom.Key");
}
#endregion
#region EnsureV1 - Entrypoint Candidate Detection Tests
[Theory]
[InlineData("Main")]
[InlineData("main")]
[InlineData("MAIN")]
public void EnsureV1_DetectsEntrypointCandidate_ForMainMethod(string methodName)
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "node1", Name = methodName }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Should().ContainSingle()
.Which.IsEntrypointCandidate.Should().BeTrue();
}
[Theory]
[InlineData("OrdersController")]
[InlineData("UserController")]
public void EnsureV1_DetectsEntrypointCandidate_ForControllerNames(string name)
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "node1", Name = name }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Should().ContainSingle()
.Which.IsEntrypointCandidate.Should().BeTrue();
}
[Theory]
[InlineData("RequestHandler")]
[InlineData("EventHandler")]
public void EnsureV1_DetectsEntrypointCandidate_ForHandlerNames(string name)
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "node1", Name = name }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Should().ContainSingle()
.Which.IsEntrypointCandidate.Should().BeTrue();
}
[Theory]
[InlineData(".cctor")]
[InlineData("ModuleInitializer")]
public void EnsureV1_DetectsEntrypointCandidate_ForModuleInitializers(string name)
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "node1", Name = name }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Should().ContainSingle()
.Which.IsEntrypointCandidate.Should().BeTrue();
}
#endregion
#region EnsureV1 - Edge Reason Inference Tests
[Theory]
[InlineData("call", EdgeReason.DirectCall)]
[InlineData("direct", EdgeReason.DirectCall)]
[InlineData("virtual", EdgeReason.VirtualCall)]
[InlineData("callvirt", EdgeReason.VirtualCall)]
[InlineData("newobj", EdgeReason.NewObj)]
[InlineData("new", EdgeReason.NewObj)]
[InlineData("ldftn", EdgeReason.DelegateCreate)]
[InlineData("delegate", EdgeReason.DelegateCreate)]
[InlineData("reflection", EdgeReason.ReflectionString)]
[InlineData("di", EdgeReason.DiBinding)]
[InlineData("injection", EdgeReason.DiBinding)]
[InlineData("async", EdgeReason.AsyncContinuation)]
[InlineData("continuation", EdgeReason.AsyncContinuation)]
[InlineData("event", EdgeReason.EventHandler)]
[InlineData("generic", EdgeReason.GenericInstantiation)]
[InlineData("native", EdgeReason.NativeInterop)]
[InlineData("pinvoke", EdgeReason.NativeInterop)]
[InlineData("ffi", EdgeReason.NativeInterop)]
public void EnsureV1_InfersEdgeReason_FromLegacyType(string legacyType, EdgeReason expected)
{
// Arrange
var document = new CallgraphDocument
{
Edges = new List<CallgraphEdge>
{
new() { SourceId = "n1", TargetId = "n2", Type = legacyType, Reason = EdgeReason.Unknown }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Edges.Should().ContainSingle()
.Which.Reason.Should().Be(expected);
}
[Fact]
public void EnsureV1_InfersRuntimeMinted_ForRuntimeKind()
{
// Arrange
var document = new CallgraphDocument
{
Edges = new List<CallgraphEdge>
{
new() { SourceId = "n1", TargetId = "n2", Type = "unknown", Kind = EdgeKind.Runtime, Reason = EdgeReason.Unknown }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Edges.Should().ContainSingle()
.Which.Reason.Should().Be(EdgeReason.RuntimeMinted);
}
[Fact]
public void EnsureV1_InfersDynamicImport_ForHeuristicKind()
{
// Arrange
var document = new CallgraphDocument
{
Edges = new List<CallgraphEdge>
{
new() { SourceId = "n1", TargetId = "n2", Type = "unknown", Kind = EdgeKind.Heuristic, Reason = EdgeReason.Unknown }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Edges.Should().ContainSingle()
.Which.Reason.Should().Be(EdgeReason.DynamicImport);
}
[Fact]
public void EnsureV1_PreservesEdgeReason_WhenAlreadySet()
{
// Arrange
var document = new CallgraphDocument
{
Edges = new List<CallgraphEdge>
{
new() { SourceId = "n1", TargetId = "n2", Type = "call", Reason = EdgeReason.VirtualCall }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Edges.Should().ContainSingle()
.Which.Reason.Should().Be(EdgeReason.VirtualCall);
}
#endregion
#region EnsureV1 - Entrypoint Inference Tests
[Fact]
public void EnsureV1_InfersEntrypoints_FromEntrypointCandidateNodes()
{
// Arrange
var document = new CallgraphDocument
{
LanguageType = CallgraphLanguage.DotNet,
Nodes = new List<CallgraphNode>
{
new() { Id = "main", Name = "Main", IsEntrypointCandidate = true },
new() { Id = "helper", Name = "Helper", IsEntrypointCandidate = false }
},
Entrypoints = new List<CallgraphEntrypoint>()
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Entrypoints.Should().ContainSingle()
.Which.NodeId.Should().Be("main");
}
[Fact]
public void EnsureV1_InfersEntrypoints_FromExplicitRoots()
{
// Arrange
var document = new CallgraphDocument
{
LanguageType = CallgraphLanguage.DotNet,
Nodes = new List<CallgraphNode>
{
new() { Id = "init", Name = "Initialize" }
},
Roots = new List<CallgraphRoot>
{
new("init", "init", "module_init")
},
Entrypoints = new List<CallgraphEntrypoint>()
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Entrypoints.Should().ContainSingle()
.Which.NodeId.Should().Be("init");
}
[Fact]
public void EnsureV1_PreservesEntrypoints_WhenAlreadyPresent()
{
// Arrange
var existingEntrypoint = new CallgraphEntrypoint
{
NodeId = "existing",
Kind = EntrypointKind.Http,
Route = "/api/test"
};
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "main", Name = "Main", IsEntrypointCandidate = true }
},
Entrypoints = new List<CallgraphEntrypoint> { existingEntrypoint }
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Entrypoints.Should().ContainSingle()
.Which.NodeId.Should().Be("existing");
}
#endregion
#region EnsureV1 - Ordering Tests
[Fact]
public void EnsureV1_SortsNodes_ByIdAlphabetically()
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>
{
new() { Id = "z-node", Name = "Z" },
new() { Id = "a-node", Name = "A" },
new() { Id = "m-node", Name = "M" }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Select(n => n.Id).Should().BeInAscendingOrder();
}
[Fact]
public void EnsureV1_SortsEdges_BySourceThenTargetThenTypeThenOffset()
{
// Arrange
var document = new CallgraphDocument
{
Edges = new List<CallgraphEdge>
{
new() { SourceId = "b", TargetId = "x", Type = "call", Offset = 10 },
new() { SourceId = "a", TargetId = "y", Type = "call", Offset = 5 },
new() { SourceId = "a", TargetId = "x", Type = "virtual", Offset = 0 },
new() { SourceId = "a", TargetId = "x", Type = "call", Offset = 20 }
}
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
var sortedEdges = result.Edges.ToList();
sortedEdges[0].SourceId.Should().Be("a");
sortedEdges[0].TargetId.Should().Be("x");
sortedEdges[0].Type.Should().Be("call");
}
[Fact]
public void EnsureV1_SortsEntrypoints_ByPhaseThenOrder()
{
// Arrange
var document = new CallgraphDocument
{
LanguageType = CallgraphLanguage.DotNet,
Nodes = new List<CallgraphNode>
{
new() { Id = "main", Name = "Main", IsEntrypointCandidate = true },
new() { Id = "init", Name = ".cctor", IsEntrypointCandidate = true }
},
Entrypoints = new List<CallgraphEntrypoint>()
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Entrypoints.Should().HaveCount(2);
// ModuleInit phase should come before Runtime
result.Entrypoints.First().NodeId.Should().Be("init");
}
#endregion
#region EnsureV1 - Null Handling Tests
[Fact]
public void EnsureV1_ThrowsArgumentNullException_ForNullDocument()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => CallgraphSchemaMigrator.EnsureV1(null!));
}
[Fact]
public void EnsureV1_HandlesEmptyNodes_Gracefully()
{
// Arrange
var document = new CallgraphDocument
{
Nodes = new List<CallgraphNode>()
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Nodes.Should().BeEmpty();
}
[Fact]
public void EnsureV1_HandlesEmptyEdges_Gracefully()
{
// Arrange
var document = new CallgraphDocument
{
Edges = new List<CallgraphEdge>()
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Edges.Should().BeEmpty();
}
#endregion
#region Framework Inference Tests
[Fact]
public void EnsureV1_InfersAspNetCoreFramework_ForDotNetController()
{
// Arrange
var document = new CallgraphDocument
{
LanguageType = CallgraphLanguage.DotNet,
Nodes = new List<CallgraphNode>
{
new() { Id = "ctrl", Name = "OrdersController", IsEntrypointCandidate = true }
},
Entrypoints = new List<CallgraphEntrypoint>()
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Entrypoints.Should().ContainSingle()
.Which.Framework.Should().Be(EntrypointFramework.AspNetCore);
}
[Fact]
public void EnsureV1_InfersSpringFramework_ForJavaController()
{
// Arrange
var document = new CallgraphDocument
{
LanguageType = CallgraphLanguage.Java,
Nodes = new List<CallgraphNode>
{
new() { Id = "ctrl", Name = "OrderController", IsEntrypointCandidate = true }
},
Entrypoints = new List<CallgraphEntrypoint>()
};
// Act
var result = CallgraphSchemaMigrator.EnsureV1(document);
// Assert
result.Entrypoints.Should().ContainSingle()
.Which.Framework.Should().Be(EntrypointFramework.Spring);
}
#endregion
}

View File

@@ -1,396 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using FluentAssertions;
using StellaOps.Signals.Models;
using StellaOps.Signals.Parsing;
using Xunit;
namespace StellaOps.Signals.Reachability.Tests;
/// <summary>
/// Determinism tests for the stella.callgraph.v1 schema.
/// These tests validate:
/// - Round-trip serialization produces identical output
/// - Schema migration from legacy formats
/// - Enum values serialize as expected strings
/// - Arrays maintain stable ordering
/// </summary>
public sealed class CallgraphSchemaV1DeterminismTests
{
private static readonly string RepoRoot = LocateRepoRoot();
private static readonly string FixtureRoot = Path.Combine(RepoRoot, "tests", "reachability", "fixtures", "callgraph-schema-v1");
private static readonly JsonSerializerOptions DeterministicOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
};
public static IEnumerable<object[]> GoldenFixtures()
{
if (!Directory.Exists(FixtureRoot))
{
yield break;
}
foreach (var file in Directory.GetFiles(FixtureRoot, "*.json").OrderBy(f => f, StringComparer.Ordinal))
{
yield return new object[] { Path.GetFileNameWithoutExtension(file) };
}
}
[Theory]
[MemberData(nameof(GoldenFixtures))]
public void GoldenFixture_DeserializesWithoutError(string fixtureName)
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json);
document.Should().NotBeNull();
document!.Id.Should().NotBeNullOrEmpty();
}
[Theory]
[MemberData(nameof(GoldenFixtures))]
public void GoldenFixture_NodesHaveRequiredFields(string fixtureName)
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
foreach (var node in document.Nodes)
{
node.Id.Should().NotBeNullOrEmpty($"Node in {fixtureName} must have Id");
node.Name.Should().NotBeNullOrEmpty($"Node {node.Id} in {fixtureName} must have Name");
}
}
[Theory]
[MemberData(nameof(GoldenFixtures))]
public void GoldenFixture_EdgesReferenceValidNodes(string fixtureName)
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
var nodeIds = document.Nodes.Select(n => n.Id).ToHashSet(StringComparer.Ordinal);
foreach (var edge in document.Edges)
{
nodeIds.Should().Contain(edge.SourceId, $"Edge source {edge.SourceId} in {fixtureName} must reference existing node");
nodeIds.Should().Contain(edge.TargetId, $"Edge target {edge.TargetId} in {fixtureName} must reference existing node");
}
}
[Theory]
[MemberData(nameof(GoldenFixtures))]
public void GoldenFixture_EntrypointsReferenceValidNodes(string fixtureName)
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
var nodeIds = document.Nodes.Select(n => n.Id).ToHashSet(StringComparer.Ordinal);
foreach (var entrypoint in document.Entrypoints)
{
nodeIds.Should().Contain(entrypoint.NodeId, $"Entrypoint {entrypoint.NodeId} in {fixtureName} must reference existing node");
}
}
[Fact]
public void DotNetFixture_HasCorrectLanguageEnum()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "dotnet-aspnetcore-minimal.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
document.LanguageType.Should().Be(CallgraphLanguage.DotNet);
}
[Fact]
public void JavaFixture_HasCorrectLanguageEnum()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "java-spring-boot.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
document.LanguageType.Should().Be(CallgraphLanguage.Java);
}
[Fact]
public void NodeFixture_HasCorrectLanguageEnum()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "node-express-api.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
document.LanguageType.Should().Be(CallgraphLanguage.Node);
}
[Fact]
public void GoFixture_HasCorrectLanguageEnum()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "go-gin-api.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
document.LanguageType.Should().Be(CallgraphLanguage.Go);
}
[Fact]
public void AllEdgeReasonsFixture_ContainsAllEdgeReasons()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-edge-reasons.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
var expectedReasons = Enum.GetValues<EdgeReason>();
var actualReasons = document.Edges.Select(e => e.Reason).Distinct().ToHashSet();
foreach (var expected in expectedReasons)
{
actualReasons.Should().Contain(expected, $"EdgeReason.{expected} should be covered by fixture");
}
}
[Fact]
public void AllEdgeReasonsFixture_ContainsAllEdgeKinds()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-edge-reasons.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
var expectedKinds = Enum.GetValues<EdgeKind>();
var actualKinds = document.Edges.Select(e => e.Kind).Distinct().ToHashSet();
foreach (var expected in expectedKinds)
{
actualKinds.Should().Contain(expected, $"EdgeKind.{expected} should be covered by fixture");
}
}
[Fact]
public void AllVisibilityFixture_ContainsAllVisibilityLevels()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-visibility-levels.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
var expectedVisibilities = Enum.GetValues<SymbolVisibility>();
var actualVisibilities = document.Nodes.Select(n => n.Visibility).Distinct().ToHashSet();
foreach (var expected in expectedVisibilities)
{
actualVisibilities.Should().Contain(expected, $"SymbolVisibility.{expected} should be covered by fixture");
}
}
[Fact]
public void LegacyFixture_HasNoSchemaField()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "legacy-no-schema.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
// Legacy fixture should deserialize but have default schema (v1) due to property default
document.Schema.Should().Be(CallgraphSchemaVersions.V1);
}
[Fact]
public void LegacyFixture_MigratesToV1Schema()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "legacy-no-schema.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
var migrated = CallgraphSchemaMigrator.EnsureV1(document);
migrated.Schema.Should().Be(CallgraphSchemaVersions.V1);
// Verify that nodes have visibility inferred (may be Unknown for some cases)
migrated.Nodes.Should().AllSatisfy(n => Enum.IsDefined(n.Visibility).Should().BeTrue());
// Verify that edges have reason inferred (defaults to DirectCall for legacy 'call' type)
migrated.Edges.Should().AllSatisfy(e => Enum.IsDefined(e.Reason).Should().BeTrue());
}
[Theory]
[InlineData("dotnet-aspnetcore-minimal")]
[InlineData("java-spring-boot")]
[InlineData("node-express-api")]
[InlineData("go-gin-api")]
public void V1Fixture_MigrationIsIdempotent(string fixtureName)
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
var migrated1 = CallgraphSchemaMigrator.EnsureV1(document);
var migrated2 = CallgraphSchemaMigrator.EnsureV1(migrated1);
migrated2.Schema.Should().Be(migrated1.Schema);
migrated2.Nodes.Should().HaveCount(migrated1.Nodes.Count);
migrated2.Edges.Should().HaveCount(migrated1.Edges.Count);
migrated2.Entrypoints.Should().HaveCount(migrated1.Entrypoints.Count);
}
[Fact]
public void EdgeReason_SerializesAsCamelCaseString()
{
var edge = new CallgraphEdge
{
SourceId = "s1",
TargetId = "t1",
Type = "call",
Reason = EdgeReason.DirectCall
};
var json = JsonSerializer.Serialize(edge, DeterministicOptions);
json.Should().Contain("\"reason\": \"directCall\"");
}
[Fact]
public void SymbolVisibility_SerializesAsCamelCaseString()
{
var node = new CallgraphNode
{
Id = "n1",
Name = "Test",
Kind = "method",
Visibility = SymbolVisibility.Public
};
var json = JsonSerializer.Serialize(node, DeterministicOptions);
json.Should().Contain("\"visibility\": \"public\"");
}
[Fact]
public void EntrypointKind_SerializesAsCamelCaseString()
{
var entrypoint = new CallgraphEntrypoint
{
NodeId = "n1",
Kind = EntrypointKind.Http,
Framework = EntrypointFramework.AspNetCore
};
var json = JsonSerializer.Serialize(entrypoint, DeterministicOptions);
json.Should().Contain("\"kind\": \"http\"");
json.Should().Contain("\"framework\": \"aspNetCore\"");
}
[Theory]
[MemberData(nameof(GoldenFixtures))]
public void GoldenFixture_NodesSortedById(string fixtureName)
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
var nodeIds = document.Nodes.Select(n => n.Id).ToList();
var sortedIds = nodeIds.OrderBy(id => id, StringComparer.Ordinal).ToList();
nodeIds.Should().Equal(sortedIds, $"Nodes in {fixtureName} should be sorted by Id for determinism");
}
[Theory]
[MemberData(nameof(GoldenFixtures))]
public void GoldenFixture_EntrypointsSortedByOrder(string fixtureName)
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, $"{fixtureName}.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
var orders = document.Entrypoints.Select(e => e.Order).ToList();
var sortedOrders = orders.OrderBy(o => o).ToList();
orders.Should().Equal(sortedOrders, $"Entrypoints in {fixtureName} should be sorted by Order for determinism");
}
[Fact]
public void DotNetFixture_HasCorrectAspNetCoreEntrypoints()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "dotnet-aspnetcore-minimal.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
document.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.Main && e.Framework == EntrypointFramework.AspNetCore);
document.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.Http && e.Route == "/weatherforecast");
}
[Fact]
public void JavaFixture_HasCorrectSpringEntrypoints()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "java-spring-boot.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
document.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.Main && e.Framework == EntrypointFramework.SpringBoot);
document.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.Http && e.Route == "/owners/{ownerId}");
}
[Fact]
public void GoFixture_HasModuleInitEntrypoint()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "go-gin-api.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
document.Entrypoints.Should().Contain(e => e.Kind == EntrypointKind.ModuleInit && e.Phase == EntrypointPhase.ModuleInit);
}
[Fact]
public void AllEdgeReasonsFixture_ReflectionEdgeIsUnresolved()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-edge-reasons.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
var reflectionEdge = document.Edges.Single(e => e.Reason == EdgeReason.ReflectionString);
reflectionEdge.IsResolved.Should().BeFalse("Reflection edges are typically unresolved");
reflectionEdge.Weight.Should().BeLessThan(1.0, "Reflection edges should have lower confidence");
}
[Fact]
public void AllEdgeReasonsFixture_DiBindingHasProvenance()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "all-edge-reasons.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
var diEdge = document.Edges.Single(e => e.Reason == EdgeReason.DiBinding);
diEdge.Provenance.Should().NotBeNullOrEmpty("DI binding edges should include provenance");
}
[Fact]
public void Artifacts_HaveRequiredFields()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "dotnet-aspnetcore-minimal.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
document.Artifacts.Should().NotBeEmpty();
foreach (var artifact in document.Artifacts)
{
artifact.ArtifactKey.Should().NotBeNullOrEmpty();
artifact.Kind.Should().NotBeNullOrEmpty();
artifact.Sha256.Should().NotBeNullOrEmpty().And.HaveLength(64);
}
}
[Fact]
public void Metadata_HasRequiredToolInfo()
{
var json = File.ReadAllText(Path.Combine(FixtureRoot, "dotnet-aspnetcore-minimal.json"));
var document = JsonSerializer.Deserialize<CallgraphDocument>(json)!;
document.GraphMetadata.Should().NotBeNull();
document.GraphMetadata!.ToolId.Should().NotBeNullOrEmpty();
document.GraphMetadata!.ToolVersion.Should().NotBeNullOrEmpty();
document.GraphMetadata!.AnalysisTimestamp.Should().NotBe(default);
}
private static string LocateRepoRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current != null)
{
if (File.Exists(Path.Combine(current.FullName, "Directory.Build.props")))
{
return current.FullName;
}
current = current.Parent;
}
throw new InvalidOperationException("Cannot locate repository root (missing Directory.Build.props).");
}
}

View File

@@ -1,341 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Signals.Models;
using StellaOps.Signals.Options;
using StellaOps.Signals.Parsing;
using StellaOps.Signals.Persistence;
using StellaOps.Signals.Services;
using Xunit;
namespace StellaOps.Signals.Reachability.Tests;
public sealed class ReachabilityScoringTests
{
private static readonly string RepoRoot = LocateRepoRoot();
private static readonly string FixtureRoot = Path.Combine(RepoRoot, "tests", "reachability", "fixtures", "reachbench-2025-expanded", "cases");
private static readonly (string CaseId, string Variant)[] SampleCases =
{
("java-log4j-CVE-2021-44228-log4shell", "reachable"),
("java-log4j-CVE-2021-44228-log4shell", "unreachable"),
("redis-CVE-2022-0543-lua-sandbox-escape", "reachable")
};
public static IEnumerable<object[]> CaseVariants()
{
foreach (var (caseId, variant) in SampleCases)
{
var path = Path.Combine(FixtureRoot, caseId, "images", variant);
if (Directory.Exists(path))
{
yield return new object[] { caseId, variant };
}
}
}
[Theory]
[MemberData(nameof(CaseVariants))]
public async Task RecomputedFactsMatchTruthFixtures(string caseId, string variant)
{
var casePath = Path.Combine(FixtureRoot, caseId);
var caseJson = JsonDocument.Parse(File.ReadAllText(Path.Combine(casePath, "case.json"))).RootElement;
var reachablePathsNode = caseJson
.GetProperty("ground_truth")
.GetProperty("reachable_variant")
.GetProperty("evidence")
.GetProperty("paths");
var paths = reachablePathsNode.EnumerateArray()
.Select(path => path.EnumerateArray().Select(x => x.GetString()!).Where(x => !string.IsNullOrWhiteSpace(x)).ToList())
.Where(path => path.Count > 0)
.ToList();
var entryPoints = paths
.Select(path => path[0])
.Where(p => !string.IsNullOrWhiteSpace(p))
.Distinct(StringComparer.Ordinal)
.ToList();
var sinks = paths
.Select(path => path[^1])
.Where(p => !string.IsNullOrWhiteSpace(p))
.Distinct(StringComparer.Ordinal)
.ToList();
var callgraph = BuildCallgraphFromPaths(caseId, paths);
var callgraphRepo = new InMemoryCallgraphRepository(callgraph);
var factRepo = new InMemoryReachabilityFactRepository();
var options = new SignalsOptions();
var cache = new InMemoryReachabilityCache();
var eventsPublisher = new NullEventsPublisher();
var unknowns = new InMemoryUnknownsRepository();
var scoringService = new ReachabilityScoringService(
callgraphRepo,
factRepo,
TimeProvider.System,
Microsoft.Extensions.Options.Options.Create(options),
cache,
unknowns,
eventsPublisher,
NullLogger<ReachabilityScoringService>.Instance);
var request = BuildRequest(casePath, variant, sinks, entryPoints);
request.CallgraphId = callgraph.Id;
var fact = await scoringService.RecomputeAsync(request, CancellationToken.None);
fact.States.Should().HaveCount(sinks.Count);
var expectedReachable = variant == "reachable";
foreach (var sink in sinks)
{
var state = fact.States.Single(s => s.Target == sink);
state.Reachable.Should().Be(expectedReachable, $"{caseId}:{variant} expected reachable={expectedReachable}");
if (expectedReachable)
{
state.Path.Should().NotBeEmpty();
state.Evidence.RuntimeHits.Should().NotBeEmpty();
}
else
{
state.Path.Should().BeEmpty();
state.Evidence.BlockedEdges.Should().NotBeNull();
}
}
}
private static ReachabilityRecomputeRequest BuildRequest(string casePath, string variant, List<string> targets, List<string> entryPoints)
{
var caseJson = JsonDocument.Parse(File.ReadAllText(Path.Combine(casePath, "case.json"))).RootElement;
var variantKey = variant == "reachable" ? "reachable_variant" : "unreachable_variant";
var variantNode = caseJson.GetProperty("ground_truth").GetProperty(variantKey);
var blockedEdges = new List<ReachabilityBlockedEdge>();
if (variantNode.TryGetProperty("evidence", out var evidence) && evidence.TryGetProperty("blocked_edges", out var blockedArray))
{
foreach (var item in blockedArray.EnumerateArray())
{
var parts = item.GetString()?.Split("->", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (parts is { Length: 2 })
{
blockedEdges.Add(new ReachabilityBlockedEdge { From = parts[0], To = parts[1] });
}
}
}
var runtimeHits = new List<string>();
var tracePath = Path.Combine(casePath, "images", variant, "traces.runtime.jsonl");
if (File.Exists(tracePath))
{
foreach (var line in File.ReadLines(tracePath))
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
using var doc = JsonDocument.Parse(line);
if (doc.RootElement.TryGetProperty("sid", out var sidProp))
{
runtimeHits.Add(sidProp.GetString()!);
}
}
}
return new ReachabilityRecomputeRequest
{
Subject = new ReachabilitySubject
{
ScanId = $"{Path.GetFileName(casePath)}:{variant}",
Component = Path.GetFileName(casePath),
Version = variant
},
EntryPoints = entryPoints,
Targets = targets,
RuntimeHits = runtimeHits,
BlockedEdges = blockedEdges
};
}
private static CallgraphDocument BuildCallgraphFromPaths(string caseId, IReadOnlyList<IReadOnlyList<string>> paths)
{
var nodes = new Dictionary<string, CallgraphNode>(StringComparer.Ordinal);
var edges = new List<CallgraphEdge>();
foreach (var path in paths)
{
if (path.Count == 0)
{
continue;
}
foreach (var nodeId in path)
{
if (!nodes.ContainsKey(nodeId))
{
nodes[nodeId] = new CallgraphNode(nodeId, nodeId, "function", null, null, null);
}
}
for (var i = 0; i < path.Count - 1; i++)
{
edges.Add(new CallgraphEdge(path[i], path[i + 1], "call"));
}
}
return new CallgraphDocument
{
Id = caseId,
Language = "fixture",
Component = caseId,
Version = "truth",
Nodes = nodes.Values.OrderBy(n => n.Id, StringComparer.Ordinal).ToList(),
Edges = edges
.OrderBy(e => e.SourceId, StringComparer.Ordinal)
.ThenBy(e => e.TargetId, StringComparer.Ordinal)
.ToList(),
Artifact = new CallgraphArtifactMetadata
{
Path = $"cas://fixtures/{caseId}",
Hash = "stub",
ContentType = "application/json",
Length = 0
}
};
}
private sealed class InMemoryCallgraphRepository : ICallgraphRepository
{
private readonly Dictionary<string, CallgraphDocument> storage;
public InMemoryCallgraphRepository(CallgraphDocument document)
{
storage = new Dictionary<string, CallgraphDocument>(StringComparer.Ordinal)
{
[document.Id] = document
};
}
public Task<CallgraphDocument> UpsertAsync(CallgraphDocument document, CancellationToken cancellationToken)
{
storage[document.Id] = document;
return Task.FromResult(document);
}
public Task<CallgraphDocument?> GetByIdAsync(string id, CancellationToken cancellationToken)
{
storage.TryGetValue(id, out var document);
return Task.FromResult(document);
}
}
private sealed class InMemoryReachabilityFactRepository : IReachabilityFactRepository
{
private readonly Dictionary<string, ReachabilityFactDocument> storage = new(StringComparer.Ordinal);
public Task<ReachabilityFactDocument?> GetBySubjectAsync(string subjectKey, CancellationToken cancellationToken)
{
storage.TryGetValue(subjectKey, out var document);
return Task.FromResult(document);
}
public Task<ReachabilityFactDocument> UpsertAsync(ReachabilityFactDocument document, CancellationToken cancellationToken)
{
storage[document.SubjectKey] = document;
return Task.FromResult(document);
}
public Task<IReadOnlyList<ReachabilityFactDocument>> GetExpiredAsync(DateTimeOffset olderThan, int limit, CancellationToken cancellationToken) =>
Task.FromResult<IReadOnlyList<ReachabilityFactDocument>>(Array.Empty<ReachabilityFactDocument>());
public Task<bool> DeleteAsync(string subjectKey, CancellationToken cancellationToken)
{
var removed = storage.Remove(subjectKey);
return Task.FromResult(removed);
}
public Task<int> GetRuntimeFactsCountAsync(string subjectKey, CancellationToken cancellationToken) =>
Task.FromResult(0);
public Task TrimRuntimeFactsAsync(string subjectKey, int maxCount, CancellationToken cancellationToken) =>
Task.CompletedTask;
}
private sealed class InMemoryReachabilityCache : IReachabilityCache
{
private readonly Dictionary<string, ReachabilityFactDocument> storage = new(StringComparer.Ordinal);
public Task<ReachabilityFactDocument?> GetAsync(string subjectKey, CancellationToken cancellationToken)
{
storage.TryGetValue(subjectKey, out var doc);
return Task.FromResult(doc);
}
public Task SetAsync(ReachabilityFactDocument document, CancellationToken cancellationToken)
{
storage[document.SubjectKey] = document;
return Task.CompletedTask;
}
public Task InvalidateAsync(string subjectKey, CancellationToken cancellationToken)
{
storage.Remove(subjectKey);
return Task.CompletedTask;
}
}
private sealed class InMemoryUnknownsRepository : IUnknownsRepository
{
public Task UpsertAsync(string subjectKey, IEnumerable<UnknownSymbolDocument> items, CancellationToken cancellationToken) =>
Task.CompletedTask;
public Task<IReadOnlyList<UnknownSymbolDocument>> GetBySubjectAsync(string subjectKey, CancellationToken cancellationToken) =>
Task.FromResult((IReadOnlyList<UnknownSymbolDocument>)Array.Empty<UnknownSymbolDocument>());
public Task<int> CountBySubjectAsync(string subjectKey, CancellationToken cancellationToken) =>
Task.FromResult(0);
public Task BulkUpdateAsync(IEnumerable<UnknownSymbolDocument> unknowns, CancellationToken cancellationToken) =>
Task.CompletedTask;
public Task<IReadOnlyList<string>> GetAllSubjectKeysAsync(CancellationToken cancellationToken) =>
Task.FromResult((IReadOnlyList<string>)Array.Empty<string>());
public Task<IReadOnlyList<UnknownSymbolDocument>> GetDueForRescanAsync(UnknownsBand band, int limit, CancellationToken cancellationToken) =>
Task.FromResult((IReadOnlyList<UnknownSymbolDocument>)Array.Empty<UnknownSymbolDocument>());
public Task<IReadOnlyList<UnknownSymbolDocument>> QueryAsync(UnknownsBand? band, int limit, int offset, CancellationToken cancellationToken) =>
Task.FromResult((IReadOnlyList<UnknownSymbolDocument>)Array.Empty<UnknownSymbolDocument>());
public Task<UnknownSymbolDocument?> GetByIdAsync(string id, CancellationToken cancellationToken) =>
Task.FromResult<UnknownSymbolDocument?>(null);
}
private sealed class NullEventsPublisher : IEventsPublisher
{
public Task PublishFactUpdatedAsync(ReachabilityFactDocument fact, CancellationToken cancellationToken) => Task.CompletedTask;
}
private static string LocateRepoRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current != null)
{
if (File.Exists(Path.Combine(current.FullName, "Directory.Build.props")))
{
return current.FullName;
}
current = current.Parent;
}
throw new InvalidOperationException("Cannot locate repository root (missing Directory.Build.props).");
}
}

View File

@@ -1,250 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Signals.Models;
using StellaOps.Signals.Persistence;
using StellaOps.Signals.Services;
using Xunit;
namespace StellaOps.Signals.Reachability.Tests;
public sealed class RuntimeFactsIngestionServiceTests
{
private readonly FakeReachabilityFactRepository repository = new();
private readonly FakeReachabilityCache cache = new();
private readonly FakeEventsPublisher eventsPublisher = new();
private readonly FakeScoringService scoringService = new();
private readonly FakeProvenanceNormalizer provenanceNormalizer = new();
private readonly FakeTimeProvider timeProvider = new(DateTimeOffset.Parse("2025-11-09T10:15:00Z", null, System.Globalization.DateTimeStyles.AssumeUniversal));
private readonly RuntimeFactsIngestionService sut;
public RuntimeFactsIngestionServiceTests()
{
sut = new RuntimeFactsIngestionService(
repository,
timeProvider,
cache,
eventsPublisher,
scoringService,
provenanceNormalizer,
NullLogger<RuntimeFactsIngestionService>.Instance);
}
[Fact]
public async Task IngestAsync_InsertsAggregatedFacts()
{
var request = new RuntimeFactsIngestRequest
{
CallgraphId = "cg-123",
Subject = new ReachabilitySubject { ScanId = "scan-1" },
Events = new List<RuntimeFactEvent>
{
new()
{
SymbolId = "symbol::foo",
HitCount = 3,
ProcessId = 100,
ProcessName = "worker",
ContainerId = "ctr-1",
SocketAddress = "10.0.0.5:443",
Metadata = new Dictionary<string, string?> { ["thread"] = "main" }
},
new()
{
SymbolId = "symbol::foo",
HitCount = 2
},
new()
{
SymbolId = "symbol::bar",
CodeId = "elf:abcd",
LoaderBase = "0x4000",
HitCount = 1
}
},
Metadata = new Dictionary<string, string?> { ["source"] = "zastava" }
};
var response = await sut.IngestAsync(request, CancellationToken.None);
response.SubjectKey.Should().Be("scan-1");
response.CallgraphId.Should().Be("cg-123");
response.RuntimeFactCount.Should().Be(2);
response.TotalHitCount.Should().Be(6);
response.StoredAt.Should().Be(timeProvider.GetUtcNow());
repository.LastUpsert.Should().NotBeNull();
repository.LastUpsert!.RuntimeFacts.Should().NotBeNull();
repository.LastUpsert!.RuntimeFacts.Should().HaveCount(2);
repository.LastUpsert!.RuntimeFacts![0].SymbolId.Should().Be("symbol::bar");
repository.LastUpsert!.RuntimeFacts![0].HitCount.Should().Be(1);
repository.LastUpsert!.RuntimeFacts![1].SymbolId.Should().Be("symbol::foo");
repository.LastUpsert!.RuntimeFacts![1].ProcessId.Should().Be(100);
repository.LastUpsert!.RuntimeFacts![1].ContainerId.Should().Be("ctr-1");
repository.LastUpsert!.RuntimeFacts![1].HitCount.Should().Be(5);
repository.LastUpsert!.Metadata.Should().ContainKey("source");
}
[Fact]
public async Task IngestAsync_MergesExistingDocument()
{
var existing = new ReachabilityFactDocument
{
Id = "507f1f77bcf86cd799439011",
Subject = new ReachabilitySubject { ImageDigest = "sha256:abc" },
SubjectKey = "sha256:abc",
CallgraphId = "cg-old",
RuntimeFacts = new List<RuntimeFactDocument>
{
new() { SymbolId = "old::symbol", HitCount = 1, Metadata = new Dictionary<string, string?> { ["thread"] = "bg" } }
}
};
repository.LastUpsert = existing;
var request = new RuntimeFactsIngestRequest
{
Subject = new ReachabilitySubject { ImageDigest = "sha256:abc" },
CallgraphId = "cg-new",
Events = new List<RuntimeFactEvent>
{
new() { SymbolId = "new::symbol", HitCount = 2, ProcessName = "svc" },
new() { SymbolId = "old::symbol", HitCount = 3, ProcessId = 200, Metadata = new Dictionary<string, string?> { ["thread"] = "main" } }
}
};
var response = await sut.IngestAsync(request, CancellationToken.None);
response.FactId.Should().Be(existing.Id);
repository.LastUpsert!.RuntimeFacts.Should().HaveCount(2);
repository.LastUpsert!.RuntimeFacts![0].SymbolId.Should().Be("new::symbol");
repository.LastUpsert!.RuntimeFacts![1].SymbolId.Should().Be("old::symbol");
repository.LastUpsert!.RuntimeFacts![1].HitCount.Should().Be(4);
repository.LastUpsert!.RuntimeFacts![1].ProcessId.Should().Be(200);
repository.LastUpsert!.RuntimeFacts![1].Metadata.Should().ContainKey("thread").WhoseValue.Should().Be("main");
}
[Theory]
[InlineData(null)]
[InlineData("")]
public async Task IngestAsync_ValidatesCallgraphId(string? callgraphId)
{
var request = new RuntimeFactsIngestRequest
{
Subject = new ReachabilitySubject { ScanId = "scan" },
CallgraphId = callgraphId ?? string.Empty,
Events = new List<RuntimeFactEvent> { new() { SymbolId = "foo" } }
};
await Assert.ThrowsAsync<RuntimeFactsValidationException>(() => sut.IngestAsync(request, CancellationToken.None));
}
private sealed class FakeReachabilityFactRepository : IReachabilityFactRepository
{
public ReachabilityFactDocument? LastUpsert { get; set; }
public Task<ReachabilityFactDocument> UpsertAsync(ReachabilityFactDocument document, CancellationToken cancellationToken)
{
LastUpsert = document;
return Task.FromResult(document);
}
public Task<ReachabilityFactDocument?> GetBySubjectAsync(string subjectKey, CancellationToken cancellationToken) =>
Task.FromResult(LastUpsert is { SubjectKey: not null } doc && doc.SubjectKey == subjectKey ? doc : null);
public Task<IReadOnlyList<ReachabilityFactDocument>> GetExpiredAsync(DateTimeOffset olderThan, int limit, CancellationToken cancellationToken) =>
Task.FromResult<IReadOnlyList<ReachabilityFactDocument>>(Array.Empty<ReachabilityFactDocument>());
public Task<bool> DeleteAsync(string subjectKey, CancellationToken cancellationToken) =>
Task.FromResult(true);
public Task<int> GetRuntimeFactsCountAsync(string subjectKey, CancellationToken cancellationToken) =>
Task.FromResult(0);
public Task TrimRuntimeFactsAsync(string subjectKey, int maxCount, CancellationToken cancellationToken) =>
Task.CompletedTask;
}
private sealed class FakeReachabilityCache : IReachabilityCache
{
private readonly Dictionary<string, ReachabilityFactDocument> storage = new(StringComparer.Ordinal);
public Task<ReachabilityFactDocument?> GetAsync(string subjectKey, CancellationToken cancellationToken)
{
storage.TryGetValue(subjectKey, out var document);
return Task.FromResult(document);
}
public Task SetAsync(ReachabilityFactDocument document, CancellationToken cancellationToken)
{
storage[document.SubjectKey] = document;
return Task.CompletedTask;
}
public Task InvalidateAsync(string subjectKey, CancellationToken cancellationToken)
{
storage.Remove(subjectKey);
return Task.CompletedTask;
}
}
private sealed class FakeEventsPublisher : IEventsPublisher
{
public List<ReachabilityFactDocument> Published { get; } = new();
public Task PublishFactUpdatedAsync(ReachabilityFactDocument fact, CancellationToken cancellationToken)
{
Published.Add(fact);
return Task.CompletedTask;
}
}
private sealed class FakeScoringService : IReachabilityScoringService
{
public List<ReachabilityRecomputeRequest> Requests { get; } = new();
public Task<ReachabilityFactDocument> RecomputeAsync(ReachabilityRecomputeRequest request, CancellationToken cancellationToken)
{
Requests.Add(request);
return Task.FromResult(new ReachabilityFactDocument
{
Subject = request.Subject,
SubjectKey = request.Subject.ToSubjectKey(),
CallgraphId = request.CallgraphId,
ComputedAt = TimeProvider.System.GetUtcNow()
});
}
}
private sealed class FakeProvenanceNormalizer : IRuntimeFactsProvenanceNormalizer
{
public ProvenanceFeed NormalizeToFeed(
IEnumerable<RuntimeFactEvent> events,
ReachabilitySubject subject,
string callgraphId,
Dictionary<string, string?>? metadata,
DateTimeOffset generatedAt) => new()
{
FeedId = "fixture",
GeneratedAt = generatedAt,
CorrelationId = callgraphId,
Records = new List<ProvenanceRecord>()
};
public ContextFacts CreateContextFacts(
IEnumerable<RuntimeFactEvent> events,
ReachabilitySubject subject,
string callgraphId,
Dictionary<string, string?>? metadata,
DateTimeOffset timestamp) => new()
{
Provenance = NormalizeToFeed(events, subject, callgraphId, metadata, timestamp),
LastUpdatedAt = timestamp,
RecordCount = events is ICollection<RuntimeFactEvent> collection ? collection.Count : 0
};
}
}

View File

@@ -1,50 +0,0 @@
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using StellaOps.Signals.Parsing;
using Xunit;
namespace StellaOps.Signals.Reachability.Tests;
public sealed class RuntimeFactsNdjsonReaderTests
{
[Fact]
public async Task ReadAsync_ParsesLines()
{
var ndjson = """
{"symbolId":"sym::foo","hitCount":2,"processId":10,"processName":"api"}
{"symbolId":"sym::bar","codeId":"elf:abcd","loaderBase":"0x1000","metadata":{"thread":"bg"}}
""";
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(ndjson));
var events = await RuntimeFactsNdjsonReader.ReadAsync(stream, gzipEncoded: false, CancellationToken.None);
events.Should().HaveCount(2);
events[0].SymbolId.Should().Be("sym::foo");
events[0].ProcessId.Should().Be(10);
events[0].ProcessName.Should().Be("api");
events[1].LoaderBase.Should().Be("0x1000");
}
[Fact]
public async Task ReadAsync_HandlesGzip()
{
var ndjson = """
{"symbolId":"sym::foo"}
""";
await using var compressed = new MemoryStream();
await using (var gzip = new GZipStream(compressed, CompressionLevel.Optimal, leaveOpen: true))
await using (var writer = new StreamWriter(gzip, Encoding.UTF8, leaveOpen: true))
{
await writer.WriteAsync(ndjson);
}
compressed.Position = 0;
var events = await RuntimeFactsNdjsonReader.ReadAsync(compressed, gzipEncoded: true, CancellationToken.None);
events.Should().HaveCount(1);
}
}

View File

@@ -1,100 +0,0 @@
using System;
using System.IO;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Signals.Hosting;
using StellaOps.Signals.Options;
using Xunit;
namespace StellaOps.Signals.Reachability.Tests;
public sealed class SignalsSealedModeMonitorTests : IDisposable
{
private readonly string tempDir = Path.Combine(Path.GetTempPath(), $"signals-sealed-tests-{Guid.NewGuid():N}");
[Fact]
public void IsCompliant_WhenEnforcementDisabled_ReturnsTrue()
{
var options = new SignalsOptions();
options.AirGap.SealedMode.EnforcementEnabled = false;
var monitor = CreateMonitor(options);
monitor.IsCompliant(out _).Should().BeTrue();
}
[Fact]
public void IsCompliant_WhenEvidenceMissing_ReturnsFalse()
{
var options = CreateEnforcedOptions();
options.AirGap.SealedMode.EvidencePath = Path.Combine(tempDir, "missing.json");
var monitor = CreateMonitor(options);
monitor.IsCompliant(out var reason).Should().BeFalse();
reason.Should().Contain("not found");
}
[Fact]
public void IsCompliant_WhenEvidenceFresh_ReturnsTrue()
{
var evidencePath = CreateEvidenceFile(TimeSpan.Zero);
var options = CreateEnforcedOptions();
options.AirGap.SealedMode.EvidencePath = evidencePath;
var monitor = CreateMonitor(options);
monitor.IsCompliant(out _).Should().BeTrue();
}
[Fact]
public void IsCompliant_WhenEvidenceStale_ReturnsFalse()
{
var evidencePath = CreateEvidenceFile(TimeSpan.FromHours(7));
var options = CreateEnforcedOptions();
options.AirGap.SealedMode.EvidencePath = evidencePath;
var monitor = CreateMonitor(options);
monitor.IsCompliant(out _).Should().BeFalse();
}
private SignalsOptions CreateEnforcedOptions()
{
var options = new SignalsOptions();
options.AirGap.SealedMode.EnforcementEnabled = true;
options.AirGap.SealedMode.MaxEvidenceAge = TimeSpan.FromHours(6);
options.AirGap.SealedMode.CacheLifetime = TimeSpan.FromSeconds(1);
return options;
}
private string CreateEvidenceFile(TimeSpan age)
{
Directory.CreateDirectory(tempDir);
var path = Path.Combine(tempDir, $"{Guid.NewGuid():N}.json");
File.WriteAllText(path, "{}");
if (age > TimeSpan.Zero)
{
File.SetLastWriteTimeUtc(path, DateTime.UtcNow - age);
}
return path;
}
private SignalsSealedModeMonitor CreateMonitor(SignalsOptions options)
{
return new SignalsSealedModeMonitor(
options,
new FakeTimeProvider(DateTimeOffset.UtcNow),
NullLogger<SignalsSealedModeMonitor>.Instance);
}
public void Dispose()
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
}
}

View File

@@ -1,28 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.6.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../src/Signals/StellaOps.Signals/StellaOps.Signals.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\..\fixtures\**\*">
<Link>fixtures\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -1,22 +0,0 @@
# Reachability Corpus (QA-CORPUS-401-031)
Layout
- `manifest.json` — deterministic SHA-256 hashes for each case file.
- `<language>/<case>/ground-truth.json` — expected reachability outcome (`reachable|unreachable`) and example path(s) (Reachbench truth schema v1).
- `<language>/<case>/callgraph.static.json` — static call graph sample (stub for MVP).
- `<language>/<case>/vex.openvex.json` — expected VEX slice for the case.
- Legacy `expect.yaml` has been retired; its state/score are preserved under `legacy_expect` in `ground-truth.json`.
Determinism
- JSON files have sorted keys; hashes recorded in `manifest.json`.
- Scores rounded to 2dp; timestamps (if added later) must be UTC ISO-8601.
- No network access required to consume the corpus.
MVP cases (stubs, to be replaced with real artifacts)
- Go: `go-ssh-CVE-2020-9283-keyexchange`
- .NET: `dotnet-kestrel-CVE-2023-44487-http2-rapid-reset`
- Python: `python-django-CVE-2019-19844-sqli-like`
- Rust: `rust-axum-header-parsing-TBD`
CI intent
- `CorpusFixtureTests` validates presence and hashes from the manifest; hook this into CI once repo build stabilises.

View File

@@ -1,5 +0,0 @@
{
"schema_version": "reach-corpus.callgraph/v1",
"nodes": [],
"edges": []
}

View File

@@ -1,16 +0,0 @@
{
"case_id": "dotnet-kestrel-CVE-2023-44487-http2-rapid-reset",
"legacy_expect": {
"schema_version": "reach-corpus.expect/v1",
"score": 0.85,
"state": "reachable"
},
"paths": [
[
"sym://dotnet:entry",
"sym://dotnet:sink"
]
],
"schema_version": "reachbench.reachgraph.truth/v1",
"variant": "reachable"
}

View File

@@ -1,12 +0,0 @@
{
"author": "StellaOps",
"role": "reachability-corpus",
"timestamp": "2025-11-18T00:00:00Z",
"statements": [
{
"vulnerability": "TBD",
"products": ["pkg:demo/demo"],
"status": "affected"
}
]
}

View File

@@ -1,5 +0,0 @@
{
"schema_version": "reach-corpus.callgraph/v1",
"nodes": [],
"edges": []
}

View File

@@ -1,16 +0,0 @@
{
"case_id": "go-ssh-CVE-2020-9283-keyexchange",
"legacy_expect": {
"schema_version": "reach-corpus.expect/v1",
"score": 0.8,
"state": "reachable"
},
"paths": [
[
"sym://go:entry",
"sym://go:sink"
]
],
"schema_version": "reachbench.reachgraph.truth/v1",
"variant": "reachable"
}

View File

@@ -1,12 +0,0 @@
{
"author": "StellaOps",
"role": "reachability-corpus",
"timestamp": "2025-11-18T00:00:00Z",
"statements": [
{
"vulnerability": "TBD",
"products": ["pkg:demo/demo"],
"status": "affected"
}
]
}

View File

@@ -1,38 +0,0 @@
[
{
"files": {
"callgraph.static.json": "7359d8c26f16151a4b05cf0e6675e5c66b5ffb6396b906e74c0d5bb2f290e972",
"ground-truth.json": "5e9fe73eabe607c9912c64d7b3d31b456a2b74631b935ce81f769d4520303c59",
"vex.openvex.json": "c3593790f769974b1b66aa5331f1d3ad4d699f77f198b2e77e78659ee79d3c15"
},
"id": "dotnet-kestrel-CVE-2023-44487-http2-rapid-reset",
"language": "dotnet"
},
{
"files": {
"callgraph.static.json": "7359d8c26f16151a4b05cf0e6675e5c66b5ffb6396b906e74c0d5bb2f290e972",
"ground-truth.json": "430adb2d001b526cff666336689006bad00e27c9f82582795a2d9dd106e1797d",
"vex.openvex.json": "c3593790f769974b1b66aa5331f1d3ad4d699f77f198b2e77e78659ee79d3c15"
},
"id": "go-ssh-CVE-2020-9283-keyexchange",
"language": "go"
},
{
"files": {
"callgraph.static.json": "7359d8c26f16151a4b05cf0e6675e5c66b5ffb6396b906e74c0d5bb2f290e972",
"ground-truth.json": "50538def2e0a8b28134051b52a848eb4b53d43cf7a6eb6d041e8fc9f1d9210f1",
"vex.openvex.json": "c3593790f769974b1b66aa5331f1d3ad4d699f77f198b2e77e78659ee79d3c15"
},
"id": "python-django-CVE-2019-19844-sqli-like",
"language": "python"
},
{
"files": {
"callgraph.static.json": "7359d8c26f16151a4b05cf0e6675e5c66b5ffb6396b906e74c0d5bb2f290e972",
"ground-truth.json": "36312fc03b7f46c8655c21448c9fb7acd6495344896b79010fbd9644a182a865",
"vex.openvex.json": "c3593790f769974b1b66aa5331f1d3ad4d699f77f198b2e77e78659ee79d3c15"
},
"id": "rust-axum-header-parsing-TBD",
"language": "rust"
}
]

View File

@@ -1,5 +0,0 @@
{
"schema_version": "reach-corpus.callgraph/v1",
"nodes": [],
"edges": []
}

View File

@@ -1,16 +0,0 @@
{
"case_id": "python-django-CVE-2019-19844-sqli-like",
"legacy_expect": {
"schema_version": "reach-corpus.expect/v1",
"score": 0.8,
"state": "reachable"
},
"paths": [
[
"sym://python:entry",
"sym://python:sink"
]
],
"schema_version": "reachbench.reachgraph.truth/v1",
"variant": "reachable"
}

View File

@@ -1,12 +0,0 @@
{
"author": "StellaOps",
"role": "reachability-corpus",
"timestamp": "2025-11-18T00:00:00Z",
"statements": [
{
"vulnerability": "TBD",
"products": ["pkg:demo/demo"],
"status": "affected"
}
]
}

View File

@@ -1,5 +0,0 @@
{
"schema_version": "reach-corpus.callgraph/v1",
"nodes": [],
"edges": []
}

View File

@@ -1,11 +0,0 @@
{
"case_id": "rust-axum-header-parsing-TBD",
"legacy_expect": {
"schema_version": "reach-corpus.expect/v1",
"score": 0.6,
"state": "conditional"
},
"paths": [],
"schema_version": "reachbench.reachgraph.truth/v1",
"variant": "unreachable"
}

View File

@@ -1,12 +0,0 @@
{
"author": "StellaOps",
"role": "reachability-corpus",
"timestamp": "2025-11-18T00:00:00Z",
"statements": [
{
"vulnerability": "TBD",
"products": ["pkg:demo/demo"],
"status": "affected"
}
]
}

View File

@@ -1,171 +0,0 @@
{
"schema": "stella.callgraph.v1",
"scanKey": "scan:edge-reasons-test:1.0.0",
"language": "DotNet",
"artifacts": [
{
"artifactKey": "TestAssembly.dll",
"kind": "assembly",
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
],
"nodes": [
{ "id": "async", "name": "AsyncTarget", "kind": "method", "visibility": "Public" },
{ "id": "caller", "name": "Caller", "kind": "method", "visibility": "Public" },
{ "id": "delegate", "name": "DelegateTarget", "kind": "method", "visibility": "Public" },
{ "id": "di", "name": "DiTarget", "kind": "method", "visibility": "Public" },
{ "id": "direct", "name": "DirectTarget", "kind": "method", "visibility": "Public" },
{ "id": "dynamic", "name": "DynamicTarget", "kind": "method", "visibility": "Public" },
{ "id": "event", "name": "EventTarget", "kind": "method", "visibility": "Public" },
{ "id": "generic", "name": "GenericTarget", "kind": "method", "visibility": "Public" },
{ "id": "native", "name": "NativeTarget", "kind": "method", "visibility": "Public" },
{ "id": "newobj", "name": "NewObjTarget", "kind": "method", "visibility": "Public" },
{ "id": "reflection", "name": "ReflectionTarget", "kind": "method", "visibility": "Public" },
{ "id": "runtime", "name": "RuntimeTarget", "kind": "method", "visibility": "Public" },
{ "id": "unknown", "name": "UnknownTarget", "kind": "method", "visibility": "Public" },
{ "id": "virtual", "name": "VirtualTarget", "kind": "method", "visibility": "Public" }
],
"edges": [
{
"sourceId": "caller",
"targetId": "direct",
"type": "call",
"kind": "Static",
"reason": "DirectCall",
"weight": 1.0,
"isResolved": true
},
{
"sourceId": "caller",
"targetId": "virtual",
"type": "callvirt",
"kind": "Static",
"reason": "VirtualCall",
"weight": 1.0,
"isResolved": true,
"candidates": ["impl1", "impl2"]
},
{
"sourceId": "caller",
"targetId": "reflection",
"type": "reflection",
"kind": "Heuristic",
"reason": "ReflectionString",
"weight": 0.5,
"isResolved": false,
"provenance": "Type.GetMethod"
},
{
"sourceId": "caller",
"targetId": "di",
"type": "di-binding",
"kind": "Heuristic",
"reason": "DiBinding",
"weight": 0.9,
"isResolved": true,
"provenance": "Microsoft.Extensions.DependencyInjection"
},
{
"sourceId": "caller",
"targetId": "dynamic",
"type": "dynamic-import",
"kind": "Heuristic",
"reason": "DynamicImport",
"weight": 0.7,
"isResolved": false
},
{
"sourceId": "caller",
"targetId": "newobj",
"type": "newobj",
"kind": "Static",
"reason": "NewObj",
"weight": 1.0,
"isResolved": true
},
{
"sourceId": "caller",
"targetId": "delegate",
"type": "ldftn",
"kind": "Static",
"reason": "DelegateCreate",
"weight": 1.0,
"isResolved": true
},
{
"sourceId": "caller",
"targetId": "async",
"type": "async",
"kind": "Static",
"reason": "AsyncContinuation",
"weight": 1.0,
"isResolved": true
},
{
"sourceId": "caller",
"targetId": "event",
"type": "event",
"kind": "Heuristic",
"reason": "EventHandler",
"weight": 0.85,
"isResolved": true
},
{
"sourceId": "caller",
"targetId": "generic",
"type": "generic",
"kind": "Static",
"reason": "GenericInstantiation",
"weight": 1.0,
"isResolved": true
},
{
"sourceId": "caller",
"targetId": "native",
"type": "pinvoke",
"kind": "Static",
"reason": "NativeInterop",
"weight": 1.0,
"isResolved": false,
"provenance": "kernel32.dll"
},
{
"sourceId": "caller",
"targetId": "runtime",
"type": "runtime",
"kind": "Runtime",
"reason": "RuntimeMinted",
"weight": 1.0,
"isResolved": true
},
{
"sourceId": "caller",
"targetId": "unknown",
"type": "unknown",
"kind": "Heuristic",
"reason": "Unknown",
"weight": 0.3,
"isResolved": false
}
],
"entrypoints": [
{
"nodeId": "caller",
"kind": "Test",
"framework": "Unknown",
"source": "test-runner",
"phase": "Runtime",
"order": 0
}
],
"metadata": {
"toolId": "stellaops.scanner.test",
"toolVersion": "1.0.0",
"analysisTimestamp": "2025-01-15T14:00:00Z"
},
"id": "cg-edge-reasons-001",
"component": "EdgeReasonsTest",
"version": "1.0.0",
"ingestedAt": "2025-01-15T14:00:00Z",
"graphHash": "sha256:edge-reasons"
}

View File

@@ -1,119 +0,0 @@
{
"schema": "stella.callgraph.v1",
"scanKey": "scan:visibility-test:1.0.0",
"language": "DotNet",
"artifacts": [
{
"artifactKey": "VisibilityTest.dll",
"kind": "assembly",
"sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
],
"nodes": [
{
"id": "v001",
"name": "PublicMethod",
"kind": "method",
"namespace": "VisibilityTest",
"symbolKey": "VisibilityTest::PublicMethod()",
"visibility": "Public",
"isEntrypointCandidate": true
},
{
"id": "v002",
"name": "InternalMethod",
"kind": "method",
"namespace": "VisibilityTest.Internal",
"symbolKey": "VisibilityTest.Internal::InternalMethod()",
"visibility": "Internal",
"isEntrypointCandidate": false
},
{
"id": "v003",
"name": "ProtectedMethod",
"kind": "method",
"namespace": "VisibilityTest",
"symbolKey": "VisibilityTest.BaseClass::ProtectedMethod()",
"visibility": "Protected",
"isEntrypointCandidate": false
},
{
"id": "v004",
"name": "PrivateMethod",
"kind": "method",
"namespace": "VisibilityTest",
"symbolKey": "VisibilityTest.SomeClass::PrivateMethod()",
"visibility": "Private",
"isEntrypointCandidate": false
},
{
"id": "v005",
"name": "UnknownMethod",
"kind": "method",
"namespace": "External",
"symbolKey": "External::UnknownMethod()",
"visibility": "Unknown",
"isEntrypointCandidate": false
}
],
"edges": [
{
"sourceId": "v001",
"targetId": "v002",
"type": "call",
"kind": "Static",
"reason": "DirectCall",
"weight": 1.0,
"isResolved": true
},
{
"sourceId": "v002",
"targetId": "v003",
"type": "call",
"kind": "Static",
"reason": "DirectCall",
"weight": 1.0,
"isResolved": true
},
{
"sourceId": "v003",
"targetId": "v004",
"type": "call",
"kind": "Static",
"reason": "DirectCall",
"weight": 1.0,
"isResolved": true
},
{
"sourceId": "v004",
"targetId": "v005",
"type": "external",
"kind": "Static",
"reason": "DirectCall",
"weight": 1.0,
"isResolved": false
}
],
"entrypoints": [
{
"nodeId": "v001",
"kind": "Http",
"route": "/api/visibility",
"httpMethod": "GET",
"framework": "AspNetCore",
"source": "attribute",
"phase": "Runtime",
"order": 0
}
],
"metadata": {
"toolId": "stellaops.scanner.test",
"toolVersion": "1.0.0",
"analysisTimestamp": "2025-01-15T15:00:00Z"
},
"id": "cg-visibility-001",
"component": "VisibilityTest",
"version": "1.0.0",
"ingestedAt": "2025-01-15T15:00:00Z",
"graphHash": "sha256:visibility"
}

View File

@@ -1,155 +0,0 @@
{
"schema": "stella.callgraph.v1",
"scanKey": "scan:dotnet-aspnetcore-minimal:v1.0.0",
"language": "DotNet",
"artifacts": [
{
"artifactKey": "SampleApi.dll",
"kind": "assembly",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"purl": "pkg:nuget/SampleApi@1.0.0",
"buildId": "build-001",
"filePath": "/app/SampleApi.dll",
"sizeBytes": 12345
}
],
"nodes": [
{
"id": "n001",
"nodeId": "n001",
"name": "Main",
"kind": "method",
"namespace": "SampleApi",
"file": "Program.cs",
"line": 1,
"symbolKey": "SampleApi::Main(string[])",
"artifactKey": "SampleApi.dll",
"visibility": "Public",
"isEntrypointCandidate": true,
"attributes": {
"returnType": "void"
},
"flags": 1
},
{
"id": "n002",
"nodeId": "n002",
"name": "GetWeatherForecast",
"kind": "method",
"namespace": "SampleApi.Controllers",
"file": "WeatherForecastController.cs",
"line": 15,
"symbolKey": "SampleApi.Controllers.WeatherForecastController::GetWeatherForecast()",
"artifactKey": "SampleApi.dll",
"visibility": "Public",
"isEntrypointCandidate": true,
"attributes": {
"returnType": "IEnumerable<WeatherForecast>",
"httpMethod": "GET",
"route": "/weatherforecast"
},
"flags": 3
},
{
"id": "n003",
"nodeId": "n003",
"name": "GetRandomSummary",
"kind": "method",
"namespace": "SampleApi.Services",
"file": "WeatherService.cs",
"line": 20,
"symbolKey": "SampleApi.Services.WeatherService::GetRandomSummary()",
"artifactKey": "SampleApi.dll",
"visibility": "Internal",
"isEntrypointCandidate": false,
"attributes": {
"returnType": "string"
},
"flags": 0
},
{
"id": "n004",
"nodeId": "n004",
"name": "CreateLogger",
"kind": "method",
"namespace": "SampleApi.Internal",
"file": "LoggingHelper.cs",
"line": 8,
"symbolKey": "SampleApi.Internal.LoggingHelper::CreateLogger()",
"artifactKey": "SampleApi.dll",
"visibility": "Private",
"isEntrypointCandidate": false,
"flags": 0
}
],
"edges": [
{
"sourceId": "n001",
"targetId": "n002",
"from": "n001",
"to": "n002",
"type": "call",
"kind": "Static",
"reason": "DirectCall",
"weight": 1.0,
"isResolved": true
},
{
"sourceId": "n002",
"targetId": "n003",
"from": "n002",
"to": "n003",
"type": "di",
"kind": "Heuristic",
"reason": "DiBinding",
"weight": 0.9,
"isResolved": true,
"provenance": "Microsoft.Extensions.DependencyInjection"
},
{
"sourceId": "n003",
"targetId": "n004",
"from": "n003",
"to": "n004",
"type": "call",
"kind": "Static",
"reason": "DirectCall",
"weight": 1.0,
"offset": 42,
"isResolved": true
}
],
"entrypoints": [
{
"nodeId": "n001",
"kind": "Main",
"framework": "AspNetCore",
"source": "attribute",
"phase": "AppStart",
"order": 0
},
{
"nodeId": "n002",
"kind": "Http",
"route": "/weatherforecast",
"httpMethod": "GET",
"framework": "AspNetCore",
"source": "attribute",
"phase": "Runtime",
"order": 1
}
],
"metadata": {
"toolId": "stellaops.scanner.dotnet",
"toolVersion": "1.0.0",
"analysisTimestamp": "2025-01-15T10:00:00Z",
"sourceCommit": "abc123def456",
"buildId": "build-001"
},
"id": "cg-dotnet-aspnetcore-minimal-001",
"languageString": "dotnet",
"component": "SampleApi",
"version": "1.0.0",
"ingestedAt": "2025-01-15T10:00:00Z",
"graphHash": "sha256:a1b2c3d4e5f6"
}

View File

@@ -1,155 +0,0 @@
{
"schema": "stella.callgraph.v1",
"scanKey": "scan:gin-api:1.5.0",
"language": "Go",
"artifacts": [
{
"artifactKey": "gin-api",
"kind": "go-binary",
"sha256": "d5e6f78901234567890abcdef0123456789abcdef0123456789abcdef0123456",
"purl": "pkg:golang/github.com/example/gin-api@1.5.0",
"filePath": "/app/gin-api",
"sizeBytes": 15000000
}
],
"nodes": [
{
"id": "g001",
"nodeId": "g001",
"name": "main",
"kind": "function",
"namespace": "main",
"file": "main.go",
"line": 12,
"symbolKey": "main.main",
"artifactKey": "gin-api",
"visibility": "Public",
"isEntrypointCandidate": true,
"flags": 1
},
{
"id": "g002",
"nodeId": "g002",
"name": "GetProduct",
"kind": "function",
"namespace": "handlers",
"file": "product_handler.go",
"line": 28,
"symbolKey": "github.com/example/gin-api/handlers.GetProduct",
"artifactKey": "gin-api",
"visibility": "Public",
"isEntrypointCandidate": true,
"attributes": {
"httpMethod": "GET",
"route": "/api/products/:id"
},
"flags": 3
},
{
"id": "g003",
"nodeId": "g003",
"name": "FindByID",
"kind": "function",
"namespace": "repository",
"file": "product_repo.go",
"line": 45,
"symbolKey": "github.com/example/gin-api/repository.(*ProductRepo).FindByID",
"artifactKey": "gin-api",
"visibility": "Public",
"isEntrypointCandidate": false,
"flags": 0
},
{
"id": "g004",
"nodeId": "g004",
"name": "init",
"kind": "function",
"namespace": "config",
"file": "config.go",
"line": 8,
"symbolKey": "github.com/example/gin-api/config.init",
"artifactKey": "gin-api",
"visibility": "Unknown",
"isEntrypointCandidate": true,
"flags": 2
}
],
"edges": [
{
"sourceId": "g004",
"targetId": "g001",
"from": "g004",
"to": "g001",
"type": "init",
"kind": "Static",
"reason": "DirectCall",
"weight": 1.0,
"isResolved": true,
"provenance": "go-init-order"
},
{
"sourceId": "g001",
"targetId": "g002",
"from": "g001",
"to": "g002",
"type": "router-bind",
"kind": "Heuristic",
"reason": "DelegateCreate",
"weight": 0.9,
"isResolved": true,
"provenance": "gin-router"
},
{
"sourceId": "g002",
"targetId": "g003",
"from": "g002",
"to": "g003",
"type": "interface",
"kind": "Static",
"reason": "VirtualCall",
"weight": 1.0,
"isResolved": true
}
],
"entrypoints": [
{
"nodeId": "g004",
"kind": "ModuleInit",
"framework": "Unknown",
"source": "convention",
"phase": "ModuleInit",
"order": 0
},
{
"nodeId": "g001",
"kind": "Main",
"framework": "Gin",
"source": "convention",
"phase": "AppStart",
"order": 1
},
{
"nodeId": "g002",
"kind": "Http",
"route": "/api/products/:id",
"httpMethod": "GET",
"framework": "Gin",
"source": "code-analysis",
"phase": "Runtime",
"order": 2
}
],
"metadata": {
"toolId": "stellaops.scanner.go",
"toolVersion": "1.0.0",
"analysisTimestamp": "2025-01-15T13:00:00Z",
"sourceCommit": "012def345abc",
"buildId": "build-004"
},
"id": "cg-go-gin-api-001",
"languageString": "go",
"component": "gin-api",
"version": "1.5.0",
"ingestedAt": "2025-01-15T13:00:00Z",
"graphHash": "sha256:d4e5f6a7b8c9"
}

View File

@@ -1,155 +0,0 @@
{
"schema": "stella.callgraph.v1",
"scanKey": "scan:spring-petclinic:3.2.0",
"language": "Java",
"artifacts": [
{
"artifactKey": "spring-petclinic-3.2.0.jar",
"kind": "jar",
"sha256": "f4d3c2b1a0987654321fedcba0987654321fedcba0987654321fedcba098765",
"purl": "pkg:maven/org.springframework.samples/spring-petclinic@3.2.0",
"filePath": "/app/spring-petclinic-3.2.0.jar",
"sizeBytes": 54321000
}
],
"nodes": [
{
"id": "j001",
"nodeId": "j001",
"name": "main",
"kind": "method",
"namespace": "org.springframework.samples.petclinic",
"file": "PetClinicApplication.java",
"line": 25,
"symbolKey": "org.springframework.samples.petclinic.PetClinicApplication::main(String[])",
"artifactKey": "spring-petclinic-3.2.0.jar",
"visibility": "Public",
"isEntrypointCandidate": true,
"attributes": {
"returnType": "void",
"modifiers": "public static"
},
"flags": 1
},
{
"id": "j002",
"nodeId": "j002",
"name": "showOwner",
"kind": "method",
"namespace": "org.springframework.samples.petclinic.owner",
"file": "OwnerController.java",
"line": 87,
"symbolKey": "org.springframework.samples.petclinic.owner.OwnerController::showOwner(int)",
"artifactKey": "spring-petclinic-3.2.0.jar",
"visibility": "Public",
"isEntrypointCandidate": true,
"attributes": {
"returnType": "ModelAndView",
"httpMethod": "GET",
"route": "/owners/{ownerId}"
},
"flags": 3
},
{
"id": "j003",
"nodeId": "j003",
"name": "findById",
"kind": "method",
"namespace": "org.springframework.samples.petclinic.owner",
"file": "OwnerRepository.java",
"line": 42,
"symbolKey": "org.springframework.samples.petclinic.owner.OwnerRepository::findById(Integer)",
"artifactKey": "spring-petclinic-3.2.0.jar",
"visibility": "Public",
"isEntrypointCandidate": false,
"attributes": {
"returnType": "Owner"
},
"flags": 0
},
{
"id": "j004",
"nodeId": "j004",
"name": "validateOwner",
"kind": "method",
"namespace": "org.springframework.samples.petclinic.owner",
"file": "OwnerValidator.java",
"line": 30,
"symbolKey": "org.springframework.samples.petclinic.owner.OwnerValidator::validateOwner(Owner)",
"artifactKey": "spring-petclinic-3.2.0.jar",
"visibility": "Protected",
"isEntrypointCandidate": false,
"flags": 0
}
],
"edges": [
{
"sourceId": "j001",
"targetId": "j002",
"from": "j001",
"to": "j002",
"type": "spring-bean",
"kind": "Heuristic",
"reason": "DiBinding",
"weight": 0.85,
"isResolved": true,
"provenance": "SpringBoot"
},
{
"sourceId": "j002",
"targetId": "j003",
"from": "j002",
"to": "j003",
"type": "virtual",
"kind": "Static",
"reason": "VirtualCall",
"weight": 1.0,
"isResolved": true
},
{
"sourceId": "j002",
"targetId": "j004",
"from": "j002",
"to": "j004",
"type": "call",
"kind": "Static",
"reason": "DirectCall",
"weight": 1.0,
"offset": 156,
"isResolved": true
}
],
"entrypoints": [
{
"nodeId": "j001",
"kind": "Main",
"framework": "SpringBoot",
"source": "annotation",
"phase": "AppStart",
"order": 0
},
{
"nodeId": "j002",
"kind": "Http",
"route": "/owners/{ownerId}",
"httpMethod": "GET",
"framework": "Spring",
"source": "annotation",
"phase": "Runtime",
"order": 1
}
],
"metadata": {
"toolId": "stellaops.scanner.java",
"toolVersion": "1.0.0",
"analysisTimestamp": "2025-01-15T11:00:00Z",
"sourceCommit": "def789abc012",
"buildId": "build-002"
},
"id": "cg-java-spring-petclinic-001",
"languageString": "java",
"component": "spring-petclinic",
"version": "3.2.0",
"ingestedAt": "2025-01-15T11:00:00Z",
"graphHash": "sha256:b2c3d4e5f6a7"
}

View File

@@ -1,47 +0,0 @@
{
"id": "cg-legacy-001",
"languageString": "csharp",
"component": "LegacyApp",
"version": "0.9.0",
"ingestedAt": "2024-06-15T08:00:00Z",
"graphHash": "sha256:legacy123",
"nodes": [
{
"id": "l001",
"name": "Main",
"kind": "method",
"namespace": "LegacyApp"
},
{
"id": "l002",
"name": "ProcessData",
"kind": "method",
"namespace": "LegacyApp.Controllers"
},
{
"id": "l003",
"name": "ValidateInput",
"kind": "method",
"namespace": "LegacyApp.Internal"
}
],
"edges": [
{
"sourceId": "l001",
"targetId": "l002",
"type": "call"
},
{
"sourceId": "l002",
"targetId": "l003",
"type": "call"
}
],
"roots": [
{
"id": "l001",
"phase": "startup",
"source": "convention"
}
]
}

View File

@@ -1,146 +0,0 @@
{
"schema": "stella.callgraph.v1",
"scanKey": "scan:express-api:2.1.0",
"language": "Node",
"artifacts": [
{
"artifactKey": "express-api",
"kind": "npm-package",
"sha256": "c4d5e6f7890123456789abcdef0123456789abcdef0123456789abcdef012345",
"purl": "pkg:npm/express-api@2.1.0",
"filePath": "/app",
"sizeBytes": 2500000
}
],
"nodes": [
{
"id": "e001",
"nodeId": "e001",
"name": "startServer",
"kind": "function",
"namespace": "src",
"file": "index.js",
"line": 15,
"symbolKey": "src/index.js::startServer",
"artifactKey": "express-api",
"visibility": "Public",
"isEntrypointCandidate": true,
"flags": 1
},
{
"id": "e002",
"nodeId": "e002",
"name": "getUserById",
"kind": "function",
"namespace": "src/routes",
"file": "users.js",
"line": 22,
"symbolKey": "src/routes/users.js::getUserById",
"artifactKey": "express-api",
"visibility": "Public",
"isEntrypointCandidate": true,
"attributes": {
"httpMethod": "GET",
"route": "/api/users/:id"
},
"flags": 3
},
{
"id": "e003",
"nodeId": "e003",
"name": "findUser",
"kind": "function",
"namespace": "src/services",
"file": "userService.js",
"line": 45,
"symbolKey": "src/services/userService.js::findUser",
"artifactKey": "express-api",
"visibility": "Public",
"isEntrypointCandidate": false,
"flags": 0
},
{
"id": "e004",
"nodeId": "e004",
"name": "query",
"kind": "function",
"namespace": "src/db",
"file": "connection.js",
"line": 30,
"symbolKey": "src/db/connection.js::query",
"artifactKey": "express-api",
"visibility": "Public",
"isEntrypointCandidate": false,
"flags": 0
}
],
"edges": [
{
"sourceId": "e001",
"targetId": "e002",
"from": "e001",
"to": "e002",
"type": "require",
"kind": "Static",
"reason": "DynamicImport",
"weight": 0.95,
"isResolved": true,
"provenance": "express-router"
},
{
"sourceId": "e002",
"targetId": "e003",
"from": "e002",
"to": "e003",
"type": "call",
"kind": "Static",
"reason": "DirectCall",
"weight": 1.0,
"isResolved": true
},
{
"sourceId": "e003",
"targetId": "e004",
"from": "e003",
"to": "e004",
"type": "async-call",
"kind": "Static",
"reason": "AsyncContinuation",
"weight": 1.0,
"isResolved": true
}
],
"entrypoints": [
{
"nodeId": "e001",
"kind": "Main",
"framework": "Express",
"source": "convention",
"phase": "AppStart",
"order": 0
},
{
"nodeId": "e002",
"kind": "Http",
"route": "/api/users/:id",
"httpMethod": "GET",
"framework": "Express",
"source": "code-analysis",
"phase": "Runtime",
"order": 1
}
],
"metadata": {
"toolId": "stellaops.scanner.node",
"toolVersion": "1.0.0",
"analysisTimestamp": "2025-01-15T12:00:00Z",
"sourceCommit": "789abc012def",
"buildId": "build-003"
},
"id": "cg-node-express-api-001",
"languageString": "javascript",
"component": "express-api",
"version": "2.1.0",
"ingestedAt": "2025-01-15T12:00:00Z",
"graphHash": "sha256:c3d4e5f6a7b8"
}

View File

@@ -1,32 +0,0 @@
{
"version": "1.0",
"schema": "patch-oracle/v1",
"generated_at": "2025-12-13T00:00:00Z",
"description": "Patch-oracle fixtures for CI graph validation. Each oracle defines expected functions/edges that must be present (or absent) in generated reachability graphs.",
"oracles": [
{
"id": "curl-CVE-2023-38545-socks5-heap-reachable",
"case_ref": "curl-CVE-2023-38545-socks5-heap",
"variant": "reachable",
"path": "cases/curl-CVE-2023-38545-socks5-heap/reachable.oracle.json"
},
{
"id": "curl-CVE-2023-38545-socks5-heap-unreachable",
"case_ref": "curl-CVE-2023-38545-socks5-heap",
"variant": "unreachable",
"path": "cases/curl-CVE-2023-38545-socks5-heap/unreachable.oracle.json"
},
{
"id": "java-log4j-CVE-2021-44228-log4shell-reachable",
"case_ref": "java-log4j-CVE-2021-44228-log4shell",
"variant": "reachable",
"path": "cases/java-log4j-CVE-2021-44228-log4shell/reachable.oracle.json"
},
{
"id": "dotnet-kestrel-CVE-2023-44487-http2-rapid-reset-reachable",
"case_ref": "dotnet-kestrel-CVE-2023-44487-http2-rapid-reset",
"variant": "reachable",
"path": "cases/dotnet-kestrel-CVE-2023-44487-http2-rapid-reset/reachable.oracle.json"
}
]
}

View File

@@ -1,56 +0,0 @@
{
"schema_version": "patch-oracle/v1",
"id": "curl-CVE-2023-38545-socks5-heap-reachable",
"case_ref": "curl-CVE-2023-38545-socks5-heap",
"variant": "reachable",
"description": "Validates that the SOCKS5 heap overflow vulnerability path is reachable from network handler to vulnerable sink",
"expected_functions": [
{
"symbol_id": "sym://net:handler#read",
"kind": "entrypoint",
"required": true,
"reason": "Network read handler is the entry point for external data"
},
{
"symbol_id": "sym://curl:curl.c#entry",
"kind": "function",
"required": true,
"reason": "SOCKS5 protocol handling entry point"
},
{
"symbol_id": "sym://curl:curl.c#sink",
"kind": "function",
"required": true,
"reason": "Vulnerable buffer handling function"
}
],
"expected_edges": [
{
"from": "sym://net:handler#read",
"to": "sym://curl:curl.c#entry",
"kind": "call",
"min_confidence": 0.8,
"required": true,
"reason": "Data flows from network handler to SOCKS5 handler"
},
{
"from": "sym://curl:curl.c#entry",
"to": "sym://curl:curl.c#sink",
"kind": "call",
"min_confidence": 0.8,
"required": true,
"reason": "SOCKS5 handler invokes vulnerable buffer function"
}
],
"expected_roots": [
{
"id": "sym://net:handler#read",
"phase": "runtime",
"required": true,
"reason": "Network handler is the runtime entry point"
}
],
"min_confidence": 0.5,
"strict_mode": false,
"created_at": "2025-12-13T00:00:00Z"
}

View File

@@ -1,32 +0,0 @@
{
"schema_version": "patch-oracle/v1",
"id": "curl-CVE-2023-38545-socks5-heap-unreachable",
"case_ref": "curl-CVE-2023-38545-socks5-heap",
"variant": "unreachable",
"description": "Validates that the SOCKS5 heap overflow vulnerability path is NOT reachable when SOCKS5 is disabled",
"expected_functions": [
{
"symbol_id": "sym://net:handler#read",
"kind": "entrypoint",
"required": true,
"reason": "Network read handler still exists but cannot reach vulnerable code"
}
],
"expected_edges": [],
"forbidden_functions": [
{
"symbol_id": "sym://curl:curl.c#sink",
"reason": "Vulnerable sink should not be in call graph when SOCKS5 disabled"
}
],
"forbidden_edges": [
{
"from": "sym://curl:curl.c#entry",
"to": "sym://curl:curl.c#sink",
"reason": "This edge should not exist when SOCKS5 is disabled"
}
],
"min_confidence": 0.5,
"strict_mode": false,
"created_at": "2025-12-13T00:00:00Z"
}

View File

@@ -1,44 +0,0 @@
{
"schema_version": "patch-oracle/v1",
"id": "dotnet-kestrel-CVE-2023-44487-http2-rapid-reset-reachable",
"case_ref": "dotnet-kestrel-CVE-2023-44487-http2-rapid-reset",
"variant": "reachable",
"description": "Validates that the HTTP/2 Rapid Reset DoS vulnerability path is reachable",
"expected_functions": [
{
"symbol_id": "sym://dotnet:Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection#ProcessRequestsAsync",
"lang": "dotnet",
"kind": "method",
"required": true,
"reason": "HTTP/2 connection handler entry point"
},
{
"symbol_id": "sym://dotnet:Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Stream#*",
"lang": "dotnet",
"kind": "method",
"required": true,
"reason": "HTTP/2 stream management affected by rapid reset"
}
],
"expected_edges": [
{
"from": "sym://dotnet:Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection#ProcessRequestsAsync",
"to": "sym://dotnet:Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Stream#*",
"kind": "call",
"min_confidence": 0.7,
"required": true,
"reason": "Connection handler creates/manages streams"
}
],
"expected_roots": [
{
"id": "sym://dotnet:Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2Connection#ProcessRequestsAsync",
"phase": "runtime",
"required": true,
"reason": "HTTP/2 processing is a runtime entry point"
}
],
"min_confidence": 0.5,
"strict_mode": false,
"created_at": "2025-12-13T00:00:00Z"
}

View File

@@ -1,64 +0,0 @@
{
"schema_version": "patch-oracle/v1",
"id": "java-log4j-CVE-2021-44228-log4shell-reachable",
"case_ref": "java-log4j-CVE-2021-44228-log4shell",
"variant": "reachable",
"description": "Validates that the Log4Shell JNDI injection path is reachable from logger to JNDI lookup",
"expected_functions": [
{
"symbol_id": "sym://java:org.apache.logging.log4j.core.Logger#logMessage",
"lang": "java",
"kind": "method",
"required": true,
"reason": "Logger entry point that processes user-controlled format strings"
},
{
"symbol_id": "sym://java:org.apache.logging.log4j.core.pattern.MessagePatternConverter#format",
"lang": "java",
"kind": "method",
"required": true,
"reason": "Pattern converter that triggers lookup substitution"
},
{
"symbol_id": "sym://java:org.apache.logging.log4j.core.lookup.StrSubstitutor#replace",
"lang": "java",
"kind": "method",
"required": true,
"reason": "String substitution that invokes lookups"
},
{
"symbol_id": "sym://java:org.apache.logging.log4j.core.lookup.JndiLookup#lookup",
"lang": "java",
"kind": "method",
"required": true,
"reason": "Vulnerable JNDI lookup method"
}
],
"expected_edges": [
{
"from": "sym://java:org.apache.logging.log4j.core.Logger#logMessage",
"to": "sym://java:org.apache.logging.log4j.core.pattern.MessagePatternConverter#format",
"kind": "call",
"required": true,
"reason": "Logger delegates to pattern converter"
},
{
"from": "sym://java:org.apache.logging.log4j.core.lookup.StrSubstitutor#replace",
"to": "sym://java:org.apache.logging.log4j.core.lookup.JndiLookup#lookup",
"kind": "call",
"required": true,
"reason": "String substitution invokes JNDI lookup"
}
],
"expected_roots": [
{
"id": "sym://java:org.apache.logging.log4j.core.Logger#*",
"phase": "runtime",
"required": true,
"reason": "Logger methods are runtime entry points"
}
],
"min_confidence": 0.6,
"strict_mode": false,
"created_at": "2025-12-13T00:00:00Z"
}

View File

@@ -1,179 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "stellaops:patch-oracle/v1",
"title": "Patch Oracle Schema v1",
"description": "Defines expected functions/edges for reachability graph validation. CI fails when expected elements are missing.",
"type": "object",
"properties": {
"schema_version": {
"type": "string",
"const": "patch-oracle/v1",
"description": "Schema version identifier"
},
"id": {
"type": "string",
"description": "Unique oracle identifier (e.g., 'curl-CVE-2023-38545-socks5-heap-reachable')"
},
"case_ref": {
"type": "string",
"description": "Reference to parent reachbench case (e.g., 'curl-CVE-2023-38545-socks5-heap')"
},
"variant": {
"type": "string",
"enum": ["reachable", "unreachable"],
"description": "Which variant this oracle applies to"
},
"description": {
"type": "string",
"description": "Human-readable description of what this oracle validates"
},
"expected_functions": {
"type": "array",
"description": "Functions that MUST be present in the generated graph",
"items": {
"$ref": "#/definitions/expected_function"
}
},
"expected_edges": {
"type": "array",
"description": "Edges that MUST be present in the generated graph",
"items": {
"$ref": "#/definitions/expected_edge"
}
},
"expected_roots": {
"type": "array",
"description": "Root nodes that MUST be present in the generated graph",
"items": {
"$ref": "#/definitions/expected_root"
}
},
"forbidden_functions": {
"type": "array",
"description": "Functions that MUST NOT be present (for unreachable variants)",
"items": {
"$ref": "#/definitions/expected_function"
}
},
"forbidden_edges": {
"type": "array",
"description": "Edges that MUST NOT be present (for unreachable variants)",
"items": {
"$ref": "#/definitions/expected_edge"
}
},
"min_confidence": {
"type": "number",
"minimum": 0.0,
"maximum": 1.0,
"default": 0.5,
"description": "Minimum confidence threshold for edge matching"
},
"strict_mode": {
"type": "boolean",
"default": false,
"description": "If true, extra functions/edges not in oracle cause failure"
},
"created_at": {
"type": "string",
"format": "date-time",
"description": "When this oracle was created"
},
"updated_at": {
"type": "string",
"format": "date-time",
"description": "When this oracle was last updated"
}
},
"required": ["schema_version", "id", "case_ref", "variant"],
"definitions": {
"expected_function": {
"type": "object",
"properties": {
"symbol_id": {
"type": "string",
"description": "Expected symbol ID (exact match or pattern with '*' wildcards)"
},
"lang": {
"type": "string",
"description": "Expected language (optional, for filtering)"
},
"kind": {
"type": "string",
"description": "Expected node kind (e.g., 'function', 'method', 'entrypoint')"
},
"purl_pattern": {
"type": "string",
"description": "Expected purl pattern (optional, supports wildcards)"
},
"required": {
"type": "boolean",
"default": true,
"description": "If true, missing this function fails CI"
},
"reason": {
"type": "string",
"description": "Why this function is expected (for documentation)"
}
},
"required": ["symbol_id"]
},
"expected_edge": {
"type": "object",
"properties": {
"from": {
"type": "string",
"description": "Source node symbol ID (exact match or pattern)"
},
"to": {
"type": "string",
"description": "Target node symbol ID (exact match or pattern)"
},
"kind": {
"type": "string",
"description": "Expected edge kind (e.g., 'call', 'plt', 'indirect')"
},
"min_confidence": {
"type": "number",
"minimum": 0.0,
"maximum": 1.0,
"description": "Minimum confidence for this specific edge"
},
"required": {
"type": "boolean",
"default": true,
"description": "If true, missing this edge fails CI"
},
"reason": {
"type": "string",
"description": "Why this edge is expected (for documentation)"
}
},
"required": ["from", "to"]
},
"expected_root": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Root node ID (exact match or pattern)"
},
"phase": {
"type": "string",
"enum": ["load", "init", "main", "runtime", "fini"],
"description": "Expected execution phase"
},
"required": {
"type": "boolean",
"default": true,
"description": "If true, missing this root fails CI"
},
"reason": {
"type": "string",
"description": "Why this root is expected"
}
},
"required": ["id"]
}
}
}

View File

@@ -1,444 +0,0 @@
{
"version": "0.1",
"generated_at": "2025-11-07T22:40:04Z",
"cases": [
{
"id": "runc-CVE-2024-21626-symlink-breakout",
"primary_axis": "container-escape",
"tags": [
"symlink",
"filesystem",
"userns"
],
"languages": [
"binary"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 9.0,
"references": [
"cve:CVE-2024-21626"
]
},
{
"id": "linux-cgroups-CVE-2022-0492-release_agent",
"primary_axis": "container-escape",
"tags": [
"cgroups",
"kernel",
"priv-esc"
],
"languages": [
"binary"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 9.0,
"references": [
"cve:CVE-2022-0492"
]
},
{
"id": "glibc-CVE-2023-4911-looney-tunables",
"primary_axis": "binary-hybrid",
"tags": [
"env-vars",
"libc",
"ldso"
],
"languages": [
"c"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2023-4911"
]
},
{
"id": "curl-CVE-2023-38545-socks5-heap",
"primary_axis": "binary-hybrid",
"tags": [
"networking",
"proxy",
"heap"
],
"languages": [
"c"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2023-38545"
]
},
{
"id": "openssl-CVE-2022-3602-x509-name-constraints",
"primary_axis": "binary-hybrid",
"tags": [
"x509",
"parser",
"stack-overflow"
],
"languages": [
"c"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2022-3602"
]
},
{
"id": "openssh-CVE-2024-6387-regreSSHion",
"primary_axis": "binary-hybrid",
"tags": [
"signal-handler",
"daemon"
],
"languages": [
"c"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2024-6387"
]
},
{
"id": "redis-CVE-2022-0543-lua-sandbox-escape",
"primary_axis": "binary-hybrid",
"tags": [
"lua",
"sandbox",
"rce"
],
"languages": [
"c",
"lua"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2022-0543"
]
},
{
"id": "java-log4j-CVE-2021-44228-log4shell",
"primary_axis": "lang-jvm",
"tags": [
"jndi",
"deserialization",
"rce"
],
"languages": [
"java"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 9.8,
"references": [
"cve:CVE-2021-44228"
]
},
{
"id": "java-spring-CVE-2022-22965-spring4shell",
"primary_axis": "lang-jvm",
"tags": [
"binding",
"reflection",
"rce"
],
"languages": [
"java"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 9.8,
"references": [
"cve:CVE-2022-22965"
]
},
{
"id": "java-jackson-CVE-2019-12384-polymorphic-deser",
"primary_axis": "lang-jvm",
"tags": [
"deserialization",
"polymorphism"
],
"languages": [
"java"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2019-12384"
]
},
{
"id": "dotnet-kestrel-CVE-2023-44487-http2-rapid-reset",
"primary_axis": "lang-dotnet",
"tags": [
"protocol",
"http2",
"dos"
],
"languages": [
"dotnet"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2023-44487"
]
},
{
"id": "dotnet-newtonsoft-deser-TBD",
"primary_axis": "lang-dotnet",
"tags": [
"deserialization",
"json",
"polymorphic"
],
"languages": [
"dotnet"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": []
},
{
"id": "go-ssh-CVE-2020-9283-keyexchange",
"primary_axis": "lang-go",
"tags": [
"crypto",
"handshake"
],
"languages": [
"go"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2020-9283"
]
},
{
"id": "go-gateway-reflection-auth-bypass",
"primary_axis": "lang-go",
"tags": [
"grpc",
"reflection",
"authz-gap"
],
"languages": [
"go"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": []
},
{
"id": "node-tar-CVE-2021-37713-path-traversal",
"primary_axis": "lang-node",
"tags": [
"path-traversal",
"archive-extract"
],
"languages": [
"node"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2021-37713"
]
},
{
"id": "node-express-middleware-order-auth-bypass",
"primary_axis": "lang-node",
"tags": [
"middleware-order",
"authz"
],
"languages": [
"node"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": []
},
{
"id": "python-jinja2-CVE-2019-10906-template-injection",
"primary_axis": "lang-python",
"tags": [
"template-injection"
],
"languages": [
"python"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2019-10906"
]
},
{
"id": "python-django-CVE-2019-19844-sqli-like",
"primary_axis": "lang-python",
"tags": [
"sqli",
"orm"
],
"languages": [
"python"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2019-19844"
]
},
{
"id": "python-urllib3-dos-regex-TBD",
"primary_axis": "lang-python",
"tags": [
"regex-dos",
"parser"
],
"languages": [
"python"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": []
},
{
"id": "php-phpmailer-CVE-2016-10033-rce",
"primary_axis": "lang-php",
"tags": [
"rce",
"email"
],
"languages": [
"php"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2016-10033"
]
},
{
"id": "wordpress-core-CVE-2022-21661-sqli",
"primary_axis": "lang-php",
"tags": [
"sqli",
"core"
],
"languages": [
"php"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2022-21661"
]
},
{
"id": "rails-CVE-2019-5418-file-content-disclosure",
"primary_axis": "lang-ruby",
"tags": [
"path-traversal",
"mime"
],
"languages": [
"ruby"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": [
"cve:CVE-2019-5418"
]
},
{
"id": "rust-axum-header-parsing-TBD",
"primary_axis": "lang-rust",
"tags": [
"parser",
"config-sensitive"
],
"languages": [
"rust"
],
"variants": [
"reachable",
"unreachable"
],
"severity_cvss": 7.5,
"references": []
}
]
}

View File

@@ -1,2 +0,0 @@
# ReachBench-2025 Expanded Kit (Skeleton)
This is a scaffold containing diverse cases across languages and reach paths. Replace STUBs with real build configs, symbols, and call graphs.

View File

@@ -1,46 +0,0 @@
{
"id": "curl-CVE-2023-38545-socks5-heap",
"cve": "CVE-2023-38545",
"description": "STUB: Replace with accurate description and threat model for the specific CVE/case.",
"threat_model": {
"entry_points": [
"STUB: define concrete inputs"
],
"preconditions": [
"STUB: feature flags / modules / protocols enabled"
],
"privilege_boundary": [
"STUB: describe boundary (if any)"
]
},
"ground_truth": {
"reachable_variant": {
"status": "affected",
"evidence": {
"symbols": [
"sym://curl:curl.c#sink"
],
"paths": [
[
"sym://net:handler#read",
"sym://curl:curl.c#entry",
"sym://curl:curl.c#sink"
]
],
"runtime_proof": "traces.runtime.jsonl: lines 1-5"
}
},
"unreachable_variant": {
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"evidence": {
"pruning_reason": [
"STUB: feature disabled, module absent, or policy denies"
],
"blocked_edges": [
"sym://curl:curl.c#entry -> sym://curl:curl.c#sink"
]
}
}
}
}

View File

@@ -1,15 +0,0 @@
# curl-CVE-2023-38545-socks5-heap
Primary axis: binary-hybrid
Tags: networking, proxy, heap
Languages: c
## Variants
- reachable: vulnerable function/path is on an executable route.
- unreachable: same base image/config with control toggles that prune the path.
## Entrypoint & Controls (fill in)
- entrypoints: e.g., http:/route, grpc method, tcp port, OCI hook
- flags: e.g., feature_on=true, middleware_order=bad|good, module_loaded=true|false, LSM=enforcing|permissive
## Expected ground-truth path(s)
See `images/*/reachgraph.truth.json`.

View File

@@ -1,5 +0,0 @@
{
"payload": "",
"payloadType": "application/vnd.in-toto+json",
"signatures": []
}

View File

@@ -1,5 +0,0 @@
{
"edges": [],
"nodes": [],
"schema_version": "reachbench.callgraph.framework/v1"
}

View File

@@ -1,5 +0,0 @@
{
"edges": [],
"nodes": [],
"schema_version": "reachbench.callgraph.static/v1"
}

View File

@@ -1,15 +0,0 @@
{
"case_id": "curl-CVE-2023-38545-socks5-heap",
"files": {
"attestation.dsse.json": "12ced21ccc633b0f458df44e276c954ccdbb14c5acd0d234fdf7934eec48696f",
"callgraph.framework.json": "86ebf343e4b684a3bf2b3200e0bd1849397ea69f280330b1095aceefdff799ce",
"callgraph.static.json": "99c850cccba6641635d1c668f831c80667930ddcd1f7acb2fe9c4c7771c63e7e",
"reachgraph.truth.json": "9545261d413f4f85d120ebe8432c32ba97ba3feb2d34075fd689fcb5794f3ab0",
"sbom.cdx.json": "ce41fd9b9edadf94a8cc84a3cce4e175b0602fd2e0d8dcb067273b9584479980",
"sbom.spdx.json": "10d7417961d3cac0f3a5c4b083917fba3dc4f9bd9140d80aad0a873435158482",
"symbols.json": "c5f473aff5b428df5a3f9c3393b7fbceb94214e3c2fd4f547d4f258ca25a3080",
"vex.openvex.json": "0518d09c2ae692b96553feb821ff8138fc0ea6c840d75c1f80149add21127ddd"
},
"schema_version": "reachbench.manifest/v1",
"variant": "reachable"
}

View File

@@ -1,12 +0,0 @@
{
"case_id": "curl-CVE-2023-38545-socks5-heap",
"paths": [
[
"sym://net:handler#read",
"sym://curl:curl.c#entry",
"sym://curl:curl.c#sink"
]
],
"schema_version": "reachbench.reachgraph.truth/v1",
"variant": "reachable"
}

View File

@@ -1,11 +0,0 @@
{
"bomFormat": "CycloneDX",
"components": [],
"metadata": {
"component": {
"name": "curl-CVE-2023-38545-socks5-heap",
"version": "0.0.0"
}
},
"specVersion": "1.5"
}

View File

@@ -1,6 +0,0 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"name": "curl-CVE-2023-38545-socks5-heap",
"packages": [],
"spdxVersion": "SPDX-2.3"
}

View File

@@ -1,8 +0,0 @@
{
"case_id": "curl-CVE-2023-38545-socks5-heap",
"schema_version": "reachbench.symbols/v1",
"symbols": [
"sym://curl:curl.c#sink"
],
"variant": "reachable"
}

View File

@@ -1,2 +0,0 @@
{"ts": 1.001, "event": "call", "sid": "sym://curl:curl.c#entry", "pid": 100}
{"ts": 1.005, "event": "call", "sid": "sym://curl:curl.c#sink", "pid": 100}

View File

@@ -1,15 +0,0 @@
{
"author": "StellaOps",
"role": "reachbench",
"statements": [
{
"products": [
"pkg:curl-CVE-2023-38545-socks5-heap"
],
"status": "affected",
"statusJustification": "component_present",
"vulnerability": "cve:CVE-2023-38545"
}
],
"timestamp": "2025-11-18T00:00:00Z"
}

View File

@@ -1,5 +0,0 @@
{
"payload": "",
"payloadType": "application/vnd.in-toto+json",
"signatures": []
}

View File

@@ -1,5 +0,0 @@
{
"edges": [],
"nodes": [],
"schema_version": "reachbench.callgraph.framework/v1"
}

View File

@@ -1,5 +0,0 @@
{
"edges": [],
"nodes": [],
"schema_version": "reachbench.callgraph.static/v1"
}

View File

@@ -1,15 +0,0 @@
{
"case_id": "curl-CVE-2023-38545-socks5-heap",
"files": {
"attestation.dsse.json": "12ced21ccc633b0f458df44e276c954ccdbb14c5acd0d234fdf7934eec48696f",
"callgraph.framework.json": "86ebf343e4b684a3bf2b3200e0bd1849397ea69f280330b1095aceefdff799ce",
"callgraph.static.json": "99c850cccba6641635d1c668f831c80667930ddcd1f7acb2fe9c4c7771c63e7e",
"reachgraph.truth.json": "490c4175eb06e0c623e60263d2ce029ffa8b236aea5780c448b8180f38a1bf6f",
"sbom.cdx.json": "ce41fd9b9edadf94a8cc84a3cce4e175b0602fd2e0d8dcb067273b9584479980",
"sbom.spdx.json": "10d7417961d3cac0f3a5c4b083917fba3dc4f9bd9140d80aad0a873435158482",
"symbols.json": "1b6a9e5598d2521e0ca55ed0f3f287ef19dc11cb1fb24fe961370c2fa7036214",
"vex.openvex.json": "a9fa7e917601538e17750fb1c25b24e18333c779ec0d5d98d4fbccf84e2f544e"
},
"schema_version": "reachbench.manifest/v1",
"variant": "unreachable"
}

View File

@@ -1,6 +0,0 @@
{
"case_id": "curl-CVE-2023-38545-socks5-heap",
"paths": [],
"schema_version": "reachbench.reachgraph.truth/v1",
"variant": "unreachable"
}

View File

@@ -1,11 +0,0 @@
{
"bomFormat": "CycloneDX",
"components": [],
"metadata": {
"component": {
"name": "curl-CVE-2023-38545-socks5-heap",
"version": "0.0.0"
}
},
"specVersion": "1.5"
}

View File

@@ -1,6 +0,0 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"name": "curl-CVE-2023-38545-socks5-heap",
"packages": [],
"spdxVersion": "SPDX-2.3"
}

View File

@@ -1,6 +0,0 @@
{
"case_id": "curl-CVE-2023-38545-socks5-heap",
"schema_version": "reachbench.symbols/v1",
"symbols": [],
"variant": "unreachable"
}

View File

@@ -1 +0,0 @@
{"ts": 1.001, "event": "call", "sid": "sym://curl:curl.c#entry", "pid": 100}

View File

@@ -1,15 +0,0 @@
{
"author": "StellaOps",
"role": "reachbench",
"statements": [
{
"products": [
"pkg:curl-CVE-2023-38545-socks5-heap"
],
"status": "not_affected",
"statusJustification": "component_not_present",
"vulnerability": "cve:CVE-2023-38545"
}
],
"timestamp": "2025-11-18T00:00:00Z"
}

View File

@@ -1,46 +0,0 @@
{
"id": "dotnet-kestrel-CVE-2023-44487-http2-rapid-reset",
"cve": "CVE-2023-44487",
"description": "STUB: Replace with accurate description and threat model for the specific CVE/case.",
"threat_model": {
"entry_points": [
"STUB: define concrete inputs"
],
"preconditions": [
"STUB: feature flags / modules / protocols enabled"
],
"privilege_boundary": [
"STUB: describe boundary (if any)"
]
},
"ground_truth": {
"reachable_variant": {
"status": "affected",
"evidence": {
"symbols": [
"sym://dotnet:dotnet.c#sink"
],
"paths": [
[
"sym://net:handler#read",
"sym://dotnet:dotnet.c#entry",
"sym://dotnet:dotnet.c#sink"
]
],
"runtime_proof": "traces.runtime.jsonl: lines 1-5"
}
},
"unreachable_variant": {
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"evidence": {
"pruning_reason": [
"STUB: feature disabled, module absent, or policy denies"
],
"blocked_edges": [
"sym://dotnet:dotnet.c#entry -> sym://dotnet:dotnet.c#sink"
]
}
}
}
}

View File

@@ -1,15 +0,0 @@
# dotnet-kestrel-CVE-2023-44487-http2-rapid-reset
Primary axis: lang-dotnet
Tags: protocol, http2, dos
Languages: dotnet
## Variants
- reachable: vulnerable function/path is on an executable route.
- unreachable: same base image/config with control toggles that prune the path.
## Entrypoint & Controls (fill in)
- entrypoints: e.g., http:/route, grpc method, tcp port, OCI hook
- flags: e.g., feature_on=true, middleware_order=bad|good, module_loaded=true|false, LSM=enforcing|permissive
## Expected ground-truth path(s)
See `images/*/reachgraph.truth.json`.

View File

@@ -1,5 +0,0 @@
{
"payload": "",
"payloadType": "application/vnd.in-toto+json",
"signatures": []
}

View File

@@ -1,5 +0,0 @@
{
"edges": [],
"nodes": [],
"schema_version": "reachbench.callgraph.framework/v1"
}

View File

@@ -1,5 +0,0 @@
{
"edges": [],
"nodes": [],
"schema_version": "reachbench.callgraph.static/v1"
}

View File

@@ -1,15 +0,0 @@
{
"case_id": "dotnet-kestrel-CVE-2023-44487-http2-rapid-reset",
"files": {
"attestation.dsse.json": "12ced21ccc633b0f458df44e276c954ccdbb14c5acd0d234fdf7934eec48696f",
"callgraph.framework.json": "86ebf343e4b684a3bf2b3200e0bd1849397ea69f280330b1095aceefdff799ce",
"callgraph.static.json": "99c850cccba6641635d1c668f831c80667930ddcd1f7acb2fe9c4c7771c63e7e",
"reachgraph.truth.json": "5396e1c97612e0963bdaf9d5d3f570f095feaccfd46ed6e96af52a6dc4608608",
"sbom.cdx.json": "8747790b2c9638b08aedca818367852889ee9bb50f1be1212b9c46b27296b8b9",
"sbom.spdx.json": "fd5b8befa1a59f06c315406213426ee516276ad806f4acb1f53472149d97c402",
"symbols.json": "c2bc2c131db1565b272900b2d86733086d601fc05a9072a43b9cd8b89a2e6f95",
"vex.openvex.json": "2bc0466a7b733a0915b6a799e91ec731c0700d5bea8645c0bf983b6da180bc48"
},
"schema_version": "reachbench.manifest/v1",
"variant": "reachable"
}

View File

@@ -1,12 +0,0 @@
{
"case_id": "dotnet-kestrel-CVE-2023-44487-http2-rapid-reset",
"paths": [
[
"sym://net:handler#read",
"sym://dotnet:dotnet.c#entry",
"sym://dotnet:dotnet.c#sink"
]
],
"schema_version": "reachbench.reachgraph.truth/v1",
"variant": "reachable"
}

View File

@@ -1,11 +0,0 @@
{
"bomFormat": "CycloneDX",
"components": [],
"metadata": {
"component": {
"name": "dotnet-kestrel-CVE-2023-44487-http2-rapid-reset",
"version": "0.0.0"
}
},
"specVersion": "1.5"
}

View File

@@ -1,6 +0,0 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"name": "dotnet-kestrel-CVE-2023-44487-http2-rapid-reset",
"packages": [],
"spdxVersion": "SPDX-2.3"
}

View File

@@ -1,8 +0,0 @@
{
"case_id": "dotnet-kestrel-CVE-2023-44487-http2-rapid-reset",
"schema_version": "reachbench.symbols/v1",
"symbols": [
"sym://dotnet:dotnet.c#sink"
],
"variant": "reachable"
}

View File

@@ -1,2 +0,0 @@
{"ts": 1.001, "event": "call", "sid": "sym://dotnet:dotnet.c#entry", "pid": 100}
{"ts": 1.005, "event": "call", "sid": "sym://dotnet:dotnet.c#sink", "pid": 100}

View File

@@ -1,15 +0,0 @@
{
"author": "StellaOps",
"role": "reachbench",
"statements": [
{
"products": [
"pkg:dotnet-kestrel-CVE-2023-44487-http2-rapid-reset"
],
"status": "affected",
"statusJustification": "component_present",
"vulnerability": "cve:CVE-2023-44487"
}
],
"timestamp": "2025-11-18T00:00:00Z"
}

View File

@@ -1,5 +0,0 @@
{
"payload": "",
"payloadType": "application/vnd.in-toto+json",
"signatures": []
}

View File

@@ -1,5 +0,0 @@
{
"edges": [],
"nodes": [],
"schema_version": "reachbench.callgraph.framework/v1"
}

Some files were not shown because too many files have changed in this diff Show More