28 KiB
Here’s a crisp idea I think you’ll like: attested, offline‑verifiable call graphs for binaries.
The gist
-
Goal: Make binary reachability (who calls whom) something an auditor can replay deterministically, even air‑gapped.
-
How:
- Build the call graph for ELF/PE/Mach‑O.
- Seal each edge (caller → callee) as its own artifact and sign it in a DSSE (in‑toto envelope).
- Bundle a reachability graph manifest listing all edge‑artifacts + hashes of the inputs (binary, debug info, decompiler version, lattice/policy config).
- Upload edge‑attestations to a transparency log (e.g., Rekor v2).
- Anyone can later fetch/verifiy the envelopes and replay the analysis identically (same inputs ⇒ same graph).
Why this matters
- Deterministic audits: “Prove this edge existed at analysis time.” No hand‑wavy “our tool said so last week.”
- Granular trust: You can quarantine or dispute just one edge without invalidating the whole graph.
- Supply‑chain fit: Edge‑artifacts compose nicely with SBOM/VEX; you can say “CVE‑123 is reachable via these signed edges.”
Minimal vocabulary
- DSSE: A standard envelope that signs the statement (here: an edge) and its subject (binary, build‑ID, PURLs).
- Rekor (v2): An append‑only public log for attestations. Inclusion proofs = tamper‑evidence.
- Reachability graph: Nodes are functions/symbols; edges are possible calls; roots are entrypoints (exports, handlers, ctors, etc.).
What “best‑in‑class” looks like in Stella Ops
-
Edge schema (per envelope):
subject: binary digest + build‑id, container image digest (if relevant)caller: {binary‑offset | symbol | demangled | PURL, version}callee: same structurereason: static pattern (PLT/JMP, thunk), init_array/ctors, EH frames, import table, or dynamic witness (trace sample ID)provenance: tool name + version, pipeline run ID, OS, container digestpolicy-hash: hash of lattice/policy/rules usedevidence: (optional) byte slice, CFG snippet hash, or trace excerpt hash
-
Graph manifest (DSSE too):
- list of edge envelope digests, roots set, toolchain hashes, input feeds, PURL map (component/function ↔ PURL).
-
Verification flow:
- Verify envelopes → verify Rekor inclusion → recompute edges from inputs (or check cached proofs) → compare manifest hash.
-
Roots you must include: exports, syscalls, signal handlers, .init_array / .ctors, TLS callbacks, exception trampolines, plugin entrypoints, registered callbacks.
Quick implementation plan (C#/.NET 10, fits your stack)
-
Parsers: ELF/PE/Mach‑O loaders (SymbolTable, DynSym, Reloc/Relr, Import/Export, Sections, Build‑ID), plus DWARF/PDB stubs when present.
-
Normalizer: stable symbol IDs (image base + RVA) and PURL resolver (package → function namespace).
-
Edge extractors (pluggable):
- Static: import thunks, PLT/JMP, reloc‑targets, vtable patterns, .init_array, EH tables, jump tables.
- Dynamic (optional): eBPF/ETW/Perf trace ingester → produce witness edges.
-
Edge attestation: one DSSE per edge + signer (FIPS/SM/GOST/EIDAS as needed).
-
Manifest builder: emit graph manifest + policy/lattice hash; store in your Ledger.
-
Transparency client: Rekor v2 submit/query; cache inclusion proofs for offline bundles.
-
Verifier: deterministic replay runner; diff engine (edge‑set, roots, policy changes).
-
UI: “Edge provenance” panel; click an edge → see DSSE, Rekor proof, extraction reason.
Practical guardrails
- Idempotence: Edge IDs =
hash(callerID, calleeID, reason, tool-version). Re‑runs don’t duplicate. - Explainability: Every edge must say why it exists (pattern or witness).
- Stripped binaries: fall back to pattern heuristics + patch oracles; mark edges probabilistic with separate attestation type.
- Hybrid truth: Keep static and dynamic edges distinct; policies can require both for “reachable”.
How this helps your day‑to‑day
- Compliance: Ship an SBOM/VEX plus a proof pack; auditors can verify offline.
- Triage: For a CVE, show the exact signed path from entrypoint → vulnerable function; suppresses noisy “maybe‑reachable” claims.
- Vendor claims: Accept third‑party edges only if they come with DSSE + Rekor inclusion.
If you want, I can draft the DSSE edge schema (JSON), the manifest format, and the .NET 10 interfaces (IEdgeExtractor, IAttestor, IReplayer, ITransparencyClient) so your mid‑level dev can start coding today.
Here’s a concrete, “give this to a mid‑level .NET dev” implementation plan for the attested, offline‑verifiable call graph.
I’ll assume:
- Recent .NET (your “.NET 10”)
- C#
- You can add NuGet packages
- You already have (or will have) an “Authority Signer” for DSSE signatures (file key, KMS, etc.)
0. Solution layout (what projects to create)
Create a new solution, e.g. StellaOps.CallGraph.sln with:
-
StellaOps.CallGraph.Core(Class Library)- Domain models (functions, edges, manifests)
- Interfaces (
IBinaryParser,IEdgeExtractor,IAttestor,IRekorClient, etc.) - DSSE envelope and helpers
-
StellaOps.CallGraph.BinaryParsers(Class Library) -
StellaOps.CallGraph.EdgeExtraction(Class Library)- Call‑graph builder / edge extractors (import table, IL call instructions, .ctors, etc.)
-
StellaOps.CallGraph.Attestation(Class Library)- DSSE helpers
- Attestation logic for edges + graph manifest
- Transparency log (Rekor) client
-
StellaOps.CallGraph.Cli(Console app)-
Developer entrypoint:
callgraph analyze <binary> -
Outputs:
- Edge DSSE envelopes (one per edge, or batched)
- Graph manifest DSSE
- Human‑readable summary
-
-
StellaOps.CallGraph.Tests(xUnit / NUnit)- Unit tests per layer
1. Define the core domain (Core project)
1.1 Records and enums
Create these in StellaOps.CallGraph.Core:
public sealed record BinaryIdentity(
string LogicalId, // e.g. build-id or image digest
string Path, // local path used during analysis
string? BuildId,
string? ImageDigest, // e.g. OCI digest
IReadOnlyDictionary<string, string> Digests // sha256, sha512, etc.
);
public sealed record FunctionRef(
string BinaryLogicalId, // link to BinaryIdentity.LogicalId
ulong Rva, // Relative virtual address (for native) or metadata token for managed
string? SymbolName, // raw symbol if available
string? DisplayName, // demangled, user-facing
string? Purl // optional: pkg/function mapping
);
public enum EdgeReasonKind
{
ImportTable,
StaticCall, // direct call instruction
VirtualDispatch, // via vtable / callvirt
InitArrayOrCtor,
ExceptionHandler,
DynamicWitness // from traces
}
public sealed record EdgeReason(
EdgeReasonKind Kind,
string Detail // e.g. ".text: call 0x401234", "import: kernel32!CreateFileW"
);
public sealed record ReachabilityEdge(
FunctionRef Caller,
FunctionRef Callee,
EdgeReason Reason,
string ToolVersion,
string PolicyHash, // hash of lattice/policy
string EvidenceHash // hash of raw evidence blob (CFG snippet, trace, etc.)
);
Graph manifest:
public sealed record CallGraphManifest(
string SchemaVersion,
BinaryIdentity Binary,
IReadOnlyList<FunctionRef> Roots,
IReadOnlyList<string> EdgeEnvelopeDigests, // sha256 of DSSE envelopes
string PolicyHash,
IReadOnlyDictionary<string, string> ToolMetadata
);
1.2 Core interfaces
public interface IBinaryParser
{
BinaryIdentity Identify(string path);
IReadOnlyList<FunctionRef> GetFunctions(BinaryIdentity binary);
IReadOnlyList<FunctionRef> GetRoots(BinaryIdentity binary); // exports, entrypoint, handlers, etc.
BinaryCodeRegion GetCodeRegion(BinaryIdentity binary); // raw bytes + mappings, see below
}
public sealed record BinaryCodeRegion(
byte[] Bytes,
ulong ImageBase,
IReadOnlyList<SectionInfo> Sections
);
public sealed record SectionInfo(
string Name,
ulong Rva,
uint Size
);
public interface IEdgeExtractor
{
IReadOnlyList<ReachabilityEdge> Extract(
BinaryIdentity binary,
IReadOnlyList<FunctionRef> functions,
BinaryCodeRegion code);
}
public interface IAttestor
{
Task<DsseEnvelope> SignEdgeAsync(
ReachabilityEdge edge,
BinaryIdentity binary,
CancellationToken ct = default);
Task<DsseEnvelope> SignManifestAsync(
CallGraphManifest manifest,
CancellationToken ct = default);
}
public interface IRekorClient
{
Task<RekorEntryRef> UploadAsync(DsseEnvelope envelope, CancellationToken ct = default);
}
public sealed record RekorEntryRef(string LogId, long Index, string Uuid);
(We’ll define DsseEnvelope in section 3.)
2. Implement minimal PE parser (BinaryParsers project)
Start with PE/.NET only; expand later.
2.1 Add NuGet packages
System.Reflection.Metadata(if you’re not already on a shared framework that has it)(NuGet)- Optionally
Microsoft.Binary.Parsersfor native PE & ELF; it already knows how to parse PE headers and ELF.(NuGet)
2.2 Implement PeBinaryParser (managed assemblies)
In StellaOps.CallGraph.BinaryParsers:
-
BinaryIdentity Identify(string path)-
Open file, compute SHA‑256 (streaming).
-
Use
PEReaderandMetadataReaderto pull:- MVID (
ModuleDefinition). - Assembly name, version.
- MVID (
-
Derive
LogicalId, e.g."dotnet:<AssemblyName>/<Mvid>".
-
-
IReadOnlyList<FunctionRef> GetFunctions(...)-
Use
PEReader→GetMetadataReader()to enumerate methods:-
reader.TypeDefinitions→ methods in each type. -
For each
MethodDefinition, compute:BinaryLogicalId = binary.LogicalIdRva = methodDef.RelativeVirtualAddressSymbolName = reader.GetString(methodDef.Name)DisplayName = typeFullName + "::" + methodName + signaturePurloptional mapping (you can fill later from SBOM).
-
-
-
IReadOnlyList<FunctionRef> GetRoots(...)-
Roots for .NET:
Mainmethods in entry assembly.- Public exported API if you want (public methods in public types).
- Static constructors (.cctor) for public types (init roots).
-
Keep it simple for v1: treat
Mainas only root.
-
-
BinaryCodeRegion GetCodeRegion(...)-
For managed assemblies, you only need IL for now:
- Use
PEReader.GetMethodBody(rva)to getMethodBodyBlock.(Microsoft Learn) - For v1, you can assemble per‑method IL as you go in the extractor instead of pre‑building a whole region.
- Use
-
Implementation trick: have PeBinaryParser expose a helper:
public MethodBodyBlock? TryGetMethodBody(BinaryIdentity binary, uint rva);
You’ll pass this down to the edge extractor.
2.3 (Optional) native PE/ELF
Once managed assemblies work:
You can then:
- Parse import table → edges from “import stub” → imported function.
- Parse export table → roots (exports).
- Parse
.pdata,.xdata→ exception handlers. - Parse
.init_array(ELF) / TLS callbacks, C runtime init functions.
For an “average dev” first iteration, you can skip native and get a lot of value from .NET assemblies only.
3. DSSE attestation primitives (Attestation project)
You already use DSSE elsewhere, but here’s a self‑contained minimal version.
3.1 Envelope models
public sealed record DsseSignature(
string KeyId,
string Sig // base64 signature
);
public sealed record DsseEnvelope(
string PayloadType, // e.g. "application/vnd.stella.call-edge+json"
string Payload, // base64-encoded JSON statement
IReadOnlyList<DsseSignature> Signatures
);
Statement for a single edge:
public sealed record EdgeStatement(
string _type, // e.g. "https://stella.ops/Statement/CallEdge/v1"
object subject, // Binary info + maybe PURLs
ReachabilityEdge edge
);
You can loosely follow the DSSE / in‑toto style: Google’s Grafeas Envelope type also matches DSSE’s envelope.proto.(Google Cloud)
3.2 Pre‑authentication encoding (PAE)
Implement DSSE PAE once:
public static class Dsse
{
public static byte[] PreAuthEncode(string payloadType, byte[] payload)
{
static byte[] Cat(params byte[][] parts)
{
var total = parts.Sum(p => p.Length);
var buf = new byte[total];
var offset = 0;
foreach (var part in parts)
{
Buffer.BlockCopy(part, 0, buf, offset, part.Length);
offset += part.Length;
}
return buf;
}
static byte[] Utf8(string s) => Encoding.UTF8.GetBytes(s);
var header = Utf8("DSSEv1");
var pt = Utf8(payloadType);
var lenPt = Utf8(pt.Length.ToString(CultureInfo.InvariantCulture));
var lenPayload = Utf8(payload.Length.ToString(CultureInfo.InvariantCulture));
var space = Utf8(" ");
return Cat(header, space, lenPt, space, pt, space, lenPayload, space, payload);
}
}
3.3 Implement IAttestor
Assume you already have some IAuthoritySigner that can sign arbitrary byte arrays (Ed25519, RSA, etc.).
public sealed class DsseAttestor : IAttestor
{
private readonly IAuthoritySigner _signer;
private readonly JsonSerializerOptions _jsonOptions = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
public DsseAttestor(IAuthoritySigner signer) => _signer = signer;
public async Task<DsseEnvelope> SignEdgeAsync(
ReachabilityEdge edge,
BinaryIdentity binary,
CancellationToken ct = default)
{
var stmt = new EdgeStatement(
_type: "https://stella.ops/Statement/CallEdge/v1",
subject: new
{
type = "file",
name = binary.Path,
digest = binary.Digests
},
edge: edge
);
return await SignStatementAsync(
stmt,
payloadType: "application/vnd.stella.call-edge+json",
ct);
}
public async Task<DsseEnvelope> SignManifestAsync(
CallGraphManifest manifest,
CancellationToken ct = default)
{
var stmt = new
{
_type = "https://stella.ops/Statement/CallGraphManifest/v1",
subject = new
{
type = "file",
name = manifest.Binary.Path,
digest = manifest.Binary.Digests
},
manifest
};
return await SignStatementAsync(
stmt,
payloadType: "application/vnd.stella.call-manifest+json",
ct);
}
private async Task<DsseEnvelope> SignStatementAsync(
object statement,
string payloadType,
CancellationToken ct)
{
var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(statement, _jsonOptions);
var pae = Dsse.PreAuthEncode(payloadType, payloadBytes);
var signatureBytes = await _signer.SignAsync(pae, ct).ConfigureAwait(false);
var keyId = await _signer.GetKeyIdAsync(ct).ConfigureAwait(false);
return new DsseEnvelope(
PayloadType: payloadType,
Payload: Convert.ToBase64String(payloadBytes),
Signatures: new[]
{
new DsseSignature(keyId, Convert.ToBase64String(signatureBytes))
});
}
}
You can plug in:
IAuthoritySignerusingSystem.Security.Cryptography.Ed25519on .NET (or BouncyCastle) for signatures.(Stack Overflow)
4. Edge extraction (EdgeExtraction project)
4.1 Choose strategy per binary type
For managed .NET assemblies the easiest route is to use Mono.Cecil to read IL opcodes.(NuGet)
Add package: Mono.Cecil.
public sealed class ManagedIlEdgeExtractor : IEdgeExtractor
{
public IReadOnlyList<ReachabilityEdge> Extract(
BinaryIdentity binary,
IReadOnlyList<FunctionRef> functions,
BinaryCodeRegion code)
{
// For managed we won't use BinaryCodeRegion; we’ll re-open file with Cecil.
var result = new List<ReachabilityEdge>();
var filePath = binary.Path;
var module = ModuleDefinition.ReadModule(filePath, new ReaderParameters
{
ReadSymbols = false
});
foreach (var type in module.Types)
foreach (var method in type.Methods.Where(m => m.HasBody))
{
var callerRef = ToFunctionRef(binary, method);
foreach (var instr in method.Body.Instructions)
{
if (instr.OpCode.FlowControl != FlowControl.Call)
continue;
if (instr.Operand is not MethodReference calleeMethod)
continue;
var calleeRef = ToFunctionRef(binary, calleeMethod);
var edge = new ReachabilityEdge(
Caller: callerRef,
Callee: calleeRef,
Reason: new EdgeReason(
EdgeReasonKind.StaticCall,
Detail: $"IL {instr.OpCode} {calleeMethod.FullName}"
),
ToolVersion: "stella-callgraph/0.1.0",
PolicyHash: "TODO",
EvidenceHash: "TODO" // later: hash of snippet
);
result.Add(edge);
}
}
return result;
}
private static FunctionRef ToFunctionRef(BinaryIdentity binary, MethodReference method)
{
var displayName = $"{method.DeclaringType.FullName}::{method.Name}";
return new FunctionRef(
BinaryLogicalId: binary.LogicalId,
Rva: (ulong)method.MetadataToken.ToInt32(),
SymbolName: method.FullName,
DisplayName: displayName,
Purl: null
);
}
}
Later, you can add:
- Import table edges (
EdgeReasonKind.ImportTable). - Virtual dispatch edges, heuristics, etc.
- Dynamic edges from trace logs (
EdgeReasonKind.DynamicWitness).
4.2 Call‑graph builder
Add a thin orchestration service:
public sealed class CallGraphBuilder
{
private readonly IBinaryParser _parser;
private readonly IReadOnlyList<IEdgeExtractor> _extractors;
public CallGraphBuilder(
IBinaryParser parser,
IEnumerable<IEdgeExtractor> extractors)
{
_parser = parser;
_extractors = extractors.ToList();
}
public (BinaryIdentity binary,
IReadOnlyList<FunctionRef> functions,
IReadOnlyList<FunctionRef> roots,
IReadOnlyList<ReachabilityEdge> edges) Build(string path)
{
var binary = _parser.Identify(path);
var functions = _parser.GetFunctions(binary);
var roots = _parser.GetRoots(binary);
// Optionally, pack code region if needed
var code = new BinaryCodeRegion(Array.Empty<byte>(), 0, Array.Empty<SectionInfo>());
var edges = _extractors
.SelectMany(e => e.Extract(binary, functions, code))
.ToList();
return (binary, functions, roots, edges);
}
}
5. Edge→DSSE and manifest→DSSE wiring
In StellaOps.CallGraph.Attestation, create a coordinator:
public sealed class CallGraphAttestationService
{
private readonly CallGraphBuilder _builder;
private readonly IAttestor _attestor;
private readonly IRekorClient _rekor;
public CallGraphAttestationService(
CallGraphBuilder builder,
IAttestor attestor,
IRekorClient rekor)
{
_builder = builder;
_attestor = attestor;
_rekor = rekor;
}
public async Task<CallGraphAttestationResult> AnalyzeAndAttestAsync(
string path,
CancellationToken ct = default)
{
var (binary, functions, roots, edges) = _builder.Build(path);
// 1) Sign each edge
var edgeEnvelopes = new List<DsseEnvelope>();
foreach (var edge in edges)
{
var env = await _attestor.SignEdgeAsync(edge, binary, ct);
edgeEnvelopes.Add(env);
}
// 2) Compute digests for manifest
var edgeEnvelopeDigests = edgeEnvelopes
.Select(e => Crypto.HashSha256(JsonSerializer.SerializeToUtf8Bytes(e)))
.ToList();
var manifest = new CallGraphManifest(
SchemaVersion: "1.0",
Binary: binary,
Roots: roots,
EdgeEnvelopeDigests: edgeEnvelopeDigests,
PolicyHash: edges.FirstOrDefault()?.PolicyHash ?? "",
ToolMetadata: new Dictionary<string, string>
{
["builder"] = "stella-callgraph/0.1.0",
["created-at"] = DateTimeOffset.UtcNow.ToString("O")
});
var manifestEnvelope = await _attestor.SignManifestAsync(manifest, ct);
// 3) Publish DSSE envelopes to Rekor (if configured)
var rekorRefs = new List<RekorEntryRef>();
foreach (var env in edgeEnvelopes.Append(manifestEnvelope))
{
var entry = await _rekor.UploadAsync(env, ct);
rekorRefs.Add(entry);
}
return new CallGraphAttestationResult(
Manifest: manifest,
ManifestEnvelope: manifestEnvelope,
EdgeEnvelopes: edgeEnvelopes,
RekorEntries: rekorRefs);
}
}
public sealed record CallGraphAttestationResult(
CallGraphManifest Manifest,
DsseEnvelope ManifestEnvelope,
IReadOnlyList<DsseEnvelope> EdgeEnvelopes,
IReadOnlyList<RekorEntryRef> RekorEntries);
6. Rekor v2 client (transparency log)
Rekor is a REST‑based transparency log (part of Sigstore).(Sigstore)
For an average dev, keep it simple:
-
Add
HttpClient‑basedRekorClient:-
UploadAsync(DsseEnvelope):- POST to your Rekor server’s
/api/v1/log/entries(v1 today; v2 is under active development, but the pattern is similar). - Store returned
logID,logIndex,uuidinRekorEntryRef.
- POST to your Rekor server’s
-
-
For offline replay you’ll want to store:
- The DSSE envelopes.
- Rekor entry references (and ideally inclusion proofs, but that can come later).
You don’t need to fully implement Merkle tree verification in v1; you can add that when you harden the verifier.
7. CLI for developers (Cli project)
A simple console app gives you fast feedback:
stella-callgraph analyze myapp.dll \
--output-dir artifacts/callgraph
Implementation sketch:
static async Task<int> Main(string[] args)
{
var input = args[1]; // TODO: proper parser
var services = Bootstrap(); // DI container
var svc = services.GetRequiredService<CallGraphAttestationService>();
var result = await svc.AnalyzeAndAttestAsync(input);
// Write DSSE envelopes & manifest as JSON files
var outDir = Path.Combine("artifacts", "callgraph");
Directory.CreateDirectory(outDir);
await File.WriteAllTextAsync(
Path.Combine(outDir, "manifest.dsse.json"),
JsonSerializer.Serialize(result.ManifestEnvelope, new JsonSerializerOptions { WriteIndented = true }));
for (var i = 0; i < result.EdgeEnvelopes.Count; i++)
{
var path = Path.Combine(outDir, $"edge-{i:D6}.dsse.json");
await File.WriteAllTextAsync(path,
JsonSerializer.Serialize(result.EdgeEnvelopes[i], new JsonSerializerOptions { WriteIndented = true }));
}
return 0;
}
8. Verifier (same libraries, different flow)
Later (or in parallel), add a verification mode:
-
Inputs:
- Binary file.
- Manifest DSSE file.
- Edge DSSE files.
- (Optionally) Rekor log inclusion proof bundle.
-
Steps (same dev can implement):
-
Verify DSSE signatures for manifest and edges (using
IAuthoritySigner.VerifyAsync). -
Check:
- Manifest’s binary digest matches the current file.
- Manifest’s edge‑envelope digests match hashes of the provided DSSE edge files.
-
Rebuild call graph using the same tool & policy version and diff against attested edges:
- For deterministic replay, their differences should be zero.
-
Optionally:
- Ask Rekor for current log info and verify inclusion proof (advanced).
-
9. Order of work for a mid‑level .NET dev
If you hand this as a sequence of tasks:
-
Core models & interfaces
- Add domain records (
BinaryIdentity,FunctionRef,ReachabilityEdge,CallGraphManifest). - Add
IBinaryParser,IEdgeExtractor,IAttestor,IRekorClient.
- Add domain records (
-
Managed PE parser
- Implement
PeBinaryParserusingSystem.Reflection.Metadata(PEReader,MetadataReader).(NuGet) - Return
BinaryIdentity, a list of methods asFunctionRef, and roots (Main).
- Implement
-
IL edge extractor
-
Add
Mono.Cecil. -
Implement
ManagedIlEdgeExtractorthat:- Iterates methods and IL instructions.
- Emits edges for
callandcallvirt.
-
-
CallGraphBuilder
- Wire
.Build(path)to usePeBinaryParser+ManagedIlEdgeExtractor.
- Wire
-
DSSE library
- Add
DsseEnvelope,DsseSignature,Dsse.PreAuthEncode. - Implement
DsseAttestorthat wrapsReachabilityEdgeandCallGraphManifestinto DSSE envelopes using anIAuthoritySigner.
- Add
-
Rekor client (stub, then real)
- First:
DummyRekorClientthat just returns fake IDs. - Then:
HttpRekorClientthat POSTs to your Rekor server.
- First:
-
CallGraphAttestationService + CLI
-
Implement
CallGraphAttestationService. -
CLI command to:
- Run analysis.
- Write DSSE files + a human readable summary.
-
-
Verifier
-
Implement basic “offline verify” command:
- Verify DSSE signatures on manifest + edges.
- Verify manifest ↔ edge digest linkage.
- (Later) compare re‑analyzed graph with attested one.
-
If you want, I can next:
- Propose the exact JSON schema for
EdgeStatementandCallGraphManifest(with sample instances). - Or help turn this into a Jira/Linear ticket breakdown ready for your team.