up
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ public sealed class ScannerToSignalsReachabilityTests
|
||||
parserResolver,
|
||||
artifactStore,
|
||||
callgraphRepo,
|
||||
new CallgraphNormalizationService(),
|
||||
Options.Create(new SignalsOptions()),
|
||||
TimeProvider.System,
|
||||
NullLogger<CallgraphIngestionService>.Instance);
|
||||
@@ -65,10 +66,15 @@ public sealed class ScannerToSignalsReachabilityTests
|
||||
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 truth = JsonDocument.Parse(File.ReadAllText(Path.Combine(variantPath, "reachgraph.truth.json"))).RootElement;
|
||||
@@ -180,6 +186,46 @@ public sealed class ScannerToSignalsReachabilityTests
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private sealed class NullEventsPublisher : IEventsPublisher
|
||||
{
|
||||
public Task PublishFactUpdatedAsync(ReachabilityFactDocument fact, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class InMemoryCallgraphArtifactStore : ICallgraphArtifactStore
|
||||
{
|
||||
public async Task<StoredCallgraphArtifact> SaveAsync(CallgraphArtifactSaveRequest request, Stream content, CancellationToken cancellationToken)
|
||||
|
||||
@@ -7,8 +7,10 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Signals.Models;
|
||||
using StellaOps.Signals.Options;
|
||||
using StellaOps.Signals.Parsing;
|
||||
using StellaOps.Signals.Persistence;
|
||||
using StellaOps.Signals.Services;
|
||||
@@ -56,7 +58,19 @@ public sealed class ReachabilityScoringTests
|
||||
var callgraph = await LoadCallgraphAsync(caseId, variant, variantPath);
|
||||
var callgraphRepo = new InMemoryCallgraphRepository(callgraph);
|
||||
var factRepo = new InMemoryReachabilityFactRepository();
|
||||
var scoringService = new ReachabilityScoringService(callgraphRepo, factRepo, TimeProvider.System, NullLogger<ReachabilityScoringService>.Instance);
|
||||
var options = new SignalsOptions();
|
||||
var cache = new InMemoryReachabilityCache();
|
||||
var eventsPublisher = new NullEventsPublisher();
|
||||
var unknowns = new InMemoryUnknownsRepository();
|
||||
var scoringService = new ReachabilityScoringService(
|
||||
callgraphRepo,
|
||||
factRepo,
|
||||
TimeProvider.System,
|
||||
Options.Create(options),
|
||||
cache,
|
||||
unknowns,
|
||||
eventsPublisher,
|
||||
NullLogger<ReachabilityScoringService>.Instance);
|
||||
|
||||
var request = BuildRequest(casePath, variant, sinks, entryPoints);
|
||||
request.CallgraphId = callgraph.Id;
|
||||
@@ -218,6 +232,47 @@ public sealed class ReachabilityScoringTests
|
||||
return Task.FromResult(document);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private sealed class NullEventsPublisher : IEventsPublisher
|
||||
{
|
||||
public Task PublishFactUpdatedAsync(ReachabilityFactDocument fact, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string LocateRepoRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
|
||||
15
tests/reachability/samples-public/README.md
Normal file
15
tests/reachability/samples-public/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Public Reachability Samples (offline fixtures)
|
||||
|
||||
This folder contains a small, public-friendly reachability mini-dataset intended for:
|
||||
|
||||
- deterministic fixture validation in CI (hash manifests),
|
||||
- offline demos and ingestion tests (Signals callgraph/runtime facts),
|
||||
- documentation examples without pulling external repos.
|
||||
|
||||
Layout (mirrors `docs/reachability/corpus-plan.md`):
|
||||
|
||||
- `schema/ground-truth.schema.json` — JSON schema for `ground-truth.json`.
|
||||
- `scripts/update_manifest.py` — deterministic manifest generator.
|
||||
- `manifest.json` — hashes for required files in each sample directory.
|
||||
- `samples/<lang>/<case-id>/` — per-sample code + callgraph + SBOM + VEX + ground truth.
|
||||
|
||||
35
tests/reachability/samples-public/manifest.json
Normal file
35
tests/reachability/samples-public/manifest.json
Normal file
@@ -0,0 +1,35 @@
|
||||
[
|
||||
{
|
||||
"files": {
|
||||
"callgraph.static.json": "1492f2cd51647c6c483f4d8a169f4b0c2ef5b3cc73f280aacd46035793fb07e2",
|
||||
"ground-truth.json": "34b5500cfb9f14e4d423a3021372286520826e79a29f5405e3b5b4fd7473686f",
|
||||
"repro.sh": "6be2324a06a4873c80ada305b60602e3f1ca134a39d021f663694974e8e7b20b",
|
||||
"sbom.cdx.json": "999333d7e6b1c2f96833d532ae9a247cec6589ae936a16e8e8db21bf6885267a",
|
||||
"vex.openvex.json": "447fa7bf849d61fc59d8892c37918714c7cb878658db27d709001560a173fe0b"
|
||||
},
|
||||
"id": "cs-001-binaryformatter-deserialize",
|
||||
"language": "csharp"
|
||||
},
|
||||
{
|
||||
"files": {
|
||||
"callgraph.static.json": "32de821ce640554c4cdeac9251c3314e30c934c17783a11b999c54af03f9321c",
|
||||
"ground-truth.json": "d9493688ae781f32628476b1bf06a45501e08a260f65ae68fd4d3722e01acc46",
|
||||
"repro.sh": "6be2324a06a4873c80ada305b60602e3f1ca134a39d021f663694974e8e7b20b",
|
||||
"sbom.cdx.json": "035a9c241e287ff4a52d2e702735649b96ddfec1ffb1ce8de980634b44906694",
|
||||
"vex.openvex.json": "c1a62ab9bde5a1e60ac48f1fb8e275fa75caeadb29177972808a9abaee2f17f5"
|
||||
},
|
||||
"id": "js-002-yaml-unsafe-load",
|
||||
"language": "js"
|
||||
},
|
||||
{
|
||||
"files": {
|
||||
"callgraph.static.json": "e55c78a1ea4c3615477bdabe2b069e3d756be2b21c4b359186612818a7213470",
|
||||
"ground-truth.json": "de5b129c315abb4eae3dabe76c0961fe5f97dff3024805c6705ebb45c809f22c",
|
||||
"repro.sh": "6be2324a06a4873c80ada305b60602e3f1ca134a39d021f663694974e8e7b20b",
|
||||
"sbom.cdx.json": "d4b18c13d2d7e7cda0ec7a8f6355e4dc52f0c30eaa8b1eddaea2c0da21620455",
|
||||
"vex.openvex.json": "df1c795978893c01d64831ff2232aebd66efe9cfe418f461ac125aece1906778"
|
||||
},
|
||||
"id": "php-001-phar-deserialize",
|
||||
"language": "php"
|
||||
}
|
||||
]
|
||||
5
tests/reachability/samples-public/runners/run_all.ps1
Normal file
5
tests/reachability/samples-public/runners/run_all.ps1
Normal file
@@ -0,0 +1,5 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
python (Join-Path $PSScriptRoot "..\\scripts\\update_manifest.py") | Out-Null
|
||||
Write-Host "samples-public: manifest regenerated"
|
||||
|
||||
6
tests/reachability/samples-public/runners/run_all.sh
Normal file
6
tests/reachability/samples-public/runners/run_all.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
python3 "$(dirname "$0")/../scripts/update_manifest.py" >/dev/null
|
||||
echo "samples-public: manifest regenerated"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
|
||||
// Fixture-only sample: demonstrates a BinaryFormatter deserialize-style sink.
|
||||
// Do not deploy.
|
||||
|
||||
var payload = Environment.GetEnvironmentVariable("PAYLOAD") ?? string.Empty;
|
||||
var bytes = Convert.FromBase64String(payload);
|
||||
using var ms = new MemoryStream(bytes);
|
||||
var formatter = new BinaryFormatter();
|
||||
_ = formatter.Deserialize(ms);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# cs-001-binaryformatter-deserialize
|
||||
|
||||
Minimal C# sample used as a public reachability fixture.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"roots": [
|
||||
{ "id": "sym://dotnet:Program#Main", "phase": "runtime", "source": "static" }
|
||||
],
|
||||
"nodes": [
|
||||
{ "id": "sym://dotnet:Program#Main", "name": "Main", "kind": "function", "language": "dotnet" },
|
||||
{ "id": "sym://dotnet:System.Runtime.Serialization.Formatters.Binary.BinaryFormatter#Deserialize", "name": "Deserialize", "kind": "function", "language": "dotnet" }
|
||||
],
|
||||
"edges": [
|
||||
{ "from": "sym://dotnet:Program#Main", "to": "sym://dotnet:System.Runtime.Serialization.Formatters.Binary.BinaryFormatter#Deserialize", "kind": "call" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"case_id": "cs-001-binaryformatter-deserialize",
|
||||
"paths": [
|
||||
[
|
||||
"sym://dotnet:Program#Main",
|
||||
"sym://dotnet:System.Runtime.Serialization.Formatters.Binary.BinaryFormatter#Deserialize"
|
||||
]
|
||||
],
|
||||
"schema_version": "reachbench.reachgraph.truth/v1",
|
||||
"variant": "reachable"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "Fixture-only sample: no live repro; use callgraph.static.json + ground-truth.json for ingestion/tests."
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"component": {
|
||||
"type": "application",
|
||||
"name": "cs-001-binaryformatter-deserialize",
|
||||
"version": "0.0.0",
|
||||
"purl": "pkg:nuget/cs-001-binaryformatter-deserialize@0.0.0"
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"type": "library",
|
||||
"name": "System.Runtime.Serialization.Formatters",
|
||||
"version": "4.3.0",
|
||||
"purl": "pkg:nuget/System.Runtime.Serialization.Formatters@4.3.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"@context": "https://openvex.dev/ns/v0.2.0",
|
||||
"@id": "urn:stellaops:vex:cs-001-binaryformatter-deserialize",
|
||||
"author": "StellaOps",
|
||||
"timestamp": "2025-12-12T00:00:00Z",
|
||||
"version": 1,
|
||||
"statements": [
|
||||
{
|
||||
"vulnerability": {
|
||||
"name": "CVE-TEST-0003"
|
||||
},
|
||||
"products": [
|
||||
{
|
||||
"@id": "pkg:nuget/System.Runtime.Serialization.Formatters@4.3.0"
|
||||
}
|
||||
],
|
||||
"status": "under_investigation"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# js-002-yaml-unsafe-load
|
||||
|
||||
Minimal JavaScript sample used as a public reachability fixture.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"roots": [
|
||||
{ "id": "sym://js:src/index.js#main", "phase": "runtime", "source": "static" }
|
||||
],
|
||||
"nodes": [
|
||||
{ "id": "sym://js:src/index.js#main", "name": "main", "kind": "function", "file": "src/index.js", "line": 1, "language": "nodejs" },
|
||||
{ "id": "sym://js:node_modules/js-yaml#load", "name": "load", "kind": "function", "namespace": "js-yaml", "language": "nodejs" }
|
||||
],
|
||||
"edges": [
|
||||
{ "from": "sym://js:src/index.js#main", "to": "sym://js:node_modules/js-yaml#load", "kind": "call" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"case_id": "js-002-yaml-unsafe-load",
|
||||
"paths": [
|
||||
[
|
||||
"sym://js:src/index.js#main",
|
||||
"sym://js:node_modules/js-yaml#load"
|
||||
]
|
||||
],
|
||||
"schema_version": "reachbench.reachgraph.truth/v1",
|
||||
"variant": "reachable"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// Fixture-only sample: demonstrates an unsafe YAML load-style sink.
|
||||
// Do not deploy.
|
||||
|
||||
const yaml = require("js-yaml");
|
||||
yaml.load(process.env.PAYLOAD || "");
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "Fixture-only sample: no live repro; use callgraph.static.json + ground-truth.json for ingestion/tests."
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"component": {
|
||||
"type": "application",
|
||||
"name": "js-002-yaml-unsafe-load",
|
||||
"version": "0.0.0",
|
||||
"purl": "pkg:npm/js-002-yaml-unsafe-load@0.0.0"
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"type": "library",
|
||||
"name": "js-yaml",
|
||||
"version": "4.1.0",
|
||||
"purl": "pkg:npm/js-yaml@4.1.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"@context": "https://openvex.dev/ns/v0.2.0",
|
||||
"@id": "urn:stellaops:vex:js-002-yaml-unsafe-load",
|
||||
"author": "StellaOps",
|
||||
"timestamp": "2025-12-12T00:00:00Z",
|
||||
"version": 1,
|
||||
"statements": [
|
||||
{
|
||||
"vulnerability": {
|
||||
"name": "CVE-TEST-0002"
|
||||
},
|
||||
"products": [
|
||||
{
|
||||
"@id": "pkg:npm/js-yaml@4.1.0"
|
||||
}
|
||||
],
|
||||
"status": "under_investigation"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# php-001-phar-deserialize
|
||||
|
||||
Minimal PHP sample used as a public reachability fixture.
|
||||
|
||||
This is a fixture only: it is not intended to be deployed.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// Fixture-only sample: demonstrates a deserialize-style sink.
|
||||
// Do not deploy.
|
||||
|
||||
$payload = $_GET["payload"] ?? "";
|
||||
unserialize($payload);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"roots": [
|
||||
{ "id": "sym://php:public/index.php#main", "phase": "runtime", "source": "static" }
|
||||
],
|
||||
"nodes": [
|
||||
{ "id": "sym://php:public/index.php#main", "name": "main", "kind": "function", "file": "public/index.php", "line": 1, "language": "php" },
|
||||
{ "id": "sym://php:app/UploadController.php#handle", "name": "handle", "kind": "function", "file": "app/UploadController.php", "line": 1, "language": "php" },
|
||||
{ "id": "sym://php:php.net#unserialize", "name": "unserialize", "kind": "function", "namespace": "php", "language": "php" }
|
||||
],
|
||||
"edges": [
|
||||
{ "from": "sym://php:public/index.php#main", "to": "sym://php:app/UploadController.php#handle", "kind": "call" },
|
||||
{ "from": "sym://php:app/UploadController.php#handle", "to": "sym://php:php.net#unserialize", "kind": "call" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"case_id": "php-001-phar-deserialize",
|
||||
"paths": [
|
||||
[
|
||||
"sym://php:public/index.php#main",
|
||||
"sym://php:app/UploadController.php#handle",
|
||||
"sym://php:php.net#unserialize"
|
||||
]
|
||||
],
|
||||
"schema_version": "reachbench.reachgraph.truth/v1",
|
||||
"variant": "reachable"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "Fixture-only sample: no live repro; use callgraph.static.json + ground-truth.json for ingestion/tests."
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"component": {
|
||||
"type": "application",
|
||||
"name": "php-001-phar-deserialize",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"type": "library",
|
||||
"name": "php",
|
||||
"version": "8.x",
|
||||
"purl": "pkg:generic/php@8"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"@context": "https://openvex.dev/ns/v0.2.0",
|
||||
"@id": "urn:stellaops:vex:php-001-phar-deserialize",
|
||||
"author": "StellaOps",
|
||||
"timestamp": "2025-12-12T00:00:00Z",
|
||||
"version": 1,
|
||||
"statements": [
|
||||
{
|
||||
"vulnerability": {
|
||||
"name": "CVE-TEST-0001"
|
||||
},
|
||||
"products": [
|
||||
{
|
||||
"@id": "pkg:generic/php@8"
|
||||
}
|
||||
],
|
||||
"status": "under_investigation"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "stellaops.reachability.ground-truth.schema.json",
|
||||
"title": "StellaOps Reachability Ground Truth (public samples)",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "case_id", "variant", "paths"],
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"type": "string",
|
||||
"const": "reachbench.reachgraph.truth/v1"
|
||||
},
|
||||
"case_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"enum": ["reachable", "unreachable"]
|
||||
},
|
||||
"paths": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
|
||||
53
tests/reachability/samples-public/scripts/update_manifest.py
Normal file
53
tests/reachability/samples-public/scripts/update_manifest.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Regenerate the public samples manifest deterministically.
|
||||
Usage: python tests/reachability/samples-public/scripts/update_manifest.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SAMPLES_ROOT = ROOT / "samples"
|
||||
FILE_LIST = [
|
||||
"callgraph.static.json",
|
||||
"ground-truth.json",
|
||||
"sbom.cdx.json",
|
||||
"vex.openvex.json",
|
||||
"repro.sh",
|
||||
]
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
entries: list[dict] = []
|
||||
for lang_dir in sorted(p for p in SAMPLES_ROOT.iterdir() if p.is_dir()):
|
||||
for case_dir in sorted(p for p in lang_dir.iterdir() if p.is_dir()):
|
||||
files: dict[str, str] = {}
|
||||
for name in FILE_LIST:
|
||||
path = case_dir / name
|
||||
if not path.exists():
|
||||
raise SystemExit(f"missing {path}")
|
||||
files[name] = sha256(path)
|
||||
entries.append(
|
||||
{
|
||||
"id": case_dir.name,
|
||||
"language": lang_dir.name,
|
||||
"files": files,
|
||||
}
|
||||
)
|
||||
|
||||
manifest_path = ROOT / "manifest.json"
|
||||
manifest_path.write_text(json.dumps(entries, indent=2, sort_keys=True) + "\n")
|
||||
print(f"wrote {manifest_path} ({len(entries)} entries)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
Reference in New Issue
Block a user