Nice, let’s lock this in so your dev can basically copy‑paste and go. I’ll give you: 1. JSON **schemas** (Draft 2020‑12) 2. Example **JSON documents** (edge + manifest + DSSE envelope) 3. C# **interfaces**: `IEdgeExtractor`, `IAttestor`, `IReplayer`, `ITransparencyClient` 4. A quick **ticket-style breakdown** you can drop into Jira/Linear --- ## 1. JSON Schemas ### 1.1 DSSE Envelope schema **File:** `schemas/dsse-envelope-v1.json` ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://stellaops.dev/schemas/dsse-envelope-v1.json", "title": "DSSE Envelope", "type": "object", "required": ["payloadType", "payload", "signatures"], "properties": { "payloadType": { "type": "string", "description": "Type URL of the statement, e.g. stellaops.dev/call-edge/v1" }, "payload": { "type": "string", "description": "Base64-encoded JSON for the statement" }, "signatures": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["keyid", "sig"], "properties": { "keyid": { "type": "string", "description": "Key identifier for the signing key" }, "sig": { "type": "string", "description": "Base64-encoded signature over DSSE PAE(payloadType, payload)" } }, "additionalProperties": false } } }, "additionalProperties": false } ``` --- ### 1.2 EdgeStatement schema **File:** `schemas/call-edge-statement-v1.json` ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://stellaops.dev/schemas/call-edge-statement-v1.json", "title": "Call Edge Statement", "type": "object", "required": ["statementType", "subject", "edge", "provenance", "policyHash"], "properties": { "statementType": { "type": "string", "const": "stellaops.dev/call-edge/v1" }, "subject": { "$ref": "#/$defs/BinarySubject" }, "edge": { "$ref": "#/$defs/CallEdge" }, "provenance": { "$ref": "#/$defs/ToolProvenance" }, "policyHash": { "type": "string", "minLength": 1, "description": "Hash (e.g. sha256) of the policy/config/lattice used" } }, "$defs": { "BinarySubject": { "type": "object", "required": ["type", "name", "digest"], "properties": { "type": { "type": "string", "enum": ["file"] }, "name": { "type": "string", "description": "Human-friendly name, usually the filename" }, "digest": { "type": "object", "description": "Map of algorithm name -> hex digest (e.g. sha256, sha512)", "propertyNames": { "type": "string" }, "additionalProperties": { "type": "string" }, "minProperties": 1 }, "buildId": { "type": "string", "description": "Optional platform-specific build ID (e.g. PE timestamp+size, ELF build-id)" } }, "additionalProperties": false }, "FunctionId": { "type": "object", "required": ["binaryId", "kind", "value"], "properties": { "binaryId": { "type": "string", "description": "Identifier linking back to subject.digest or buildId" }, "kind": { "type": "string", "enum": ["RVA", "Symbol", "Pdb", "Other"] }, "value": { "type": "string", "description": "RVA (e.g. 0x401000) or fully qualified symbol name" } }, "additionalProperties": false }, "CallEdge": { "type": "object", "required": ["edgeId", "caller", "callee", "reason"], "properties": { "edgeId": { "type": "string", "description": "Deterministic ID (e.g. sha256 of canonical edge tuple)" }, "caller": { "$ref": "#/$defs/FunctionId" }, "callee": { "$ref": "#/$defs/FunctionId" }, "reason": { "type": "string", "enum": [ "StaticImportThunk", "StaticDirectCall", "StaticCtorOrInitArray", "ExceptionHandler", "JumpTable", "DynamicTraceWitness" ] }, "evidenceHash": { "type": ["string", "null"], "description": "Optional hash of attached evidence (CFG snippet, trace chunk, etc.)" } }, "additionalProperties": false }, "ToolProvenance": { "type": "object", "required": ["toolName", "toolVersion"], "properties": { "toolName": { "type": "string" }, "toolVersion": { "type": "string" }, "hostOs": { "type": "string" }, "runtime": { "type": "string", "description": ".NET runtime or other execution environment" }, "pipelineRunId": { "type": "string", "description": "CI/CD or local run identifier" } }, "additionalProperties": false } }, "additionalProperties": false } ``` --- ### 1.3 GraphManifest schema **File:** `schemas/call-graph-manifest-v1.json` ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://stellaops.dev/schemas/call-graph-manifest-v1.json", "title": "Call Graph Manifest", "type": "object", "required": ["manifestType", "subject", "edgeEnvelopeDigests", "roots", "provenance", "policyHash"], "properties": { "manifestType": { "type": "string", "const": "stellaops.dev/call-graph-manifest/v1" }, "subject": { "$ref": "#/$defs/BinarySubject" }, "edgeEnvelopeDigests": { "type": "array", "minItems": 1, "items": { "type": "string", "description": "Hex sha256 digest of a DSSE envelope JSON blob" } }, "roots": { "type": "array", "items": { "$ref": "#/$defs/FunctionId" }, "description": "Entrypoints / roots used for reachability" }, "provenance": { "$ref": "#/$defs/ToolProvenance" }, "policyHash": { "type": "string", "description": "Hash (e.g. sha256) of the policy/config/lattice used" } }, "$defs": { "BinarySubject": { "type": "object", "required": ["type", "name", "digest"], "properties": { "type": { "type": "string", "enum": ["file"] }, "name": { "type": "string" }, "digest": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": { "type": "string" }, "minProperties": 1 }, "buildId": { "type": "string" } }, "additionalProperties": false }, "FunctionId": { "type": "object", "required": ["binaryId", "kind", "value"], "properties": { "binaryId": { "type": "string" }, "kind": { "type": "string", "enum": ["RVA", "Symbol", "Pdb", "Other"] }, "value": { "type": "string" } }, "additionalProperties": false }, "ToolProvenance": { "type": "object", "required": ["toolName", "toolVersion"], "properties": { "toolName": { "type": "string" }, "toolVersion": { "type": "string" }, "hostOs": { "type": "string" }, "runtime": { "type": "string" }, "pipelineRunId": { "type": "string" } }, "additionalProperties": false } }, "additionalProperties": false } ``` --- ## 2. Example JSON documents ### 2.1 Example EdgeStatement ```json { "statementType": "stellaops.dev/call-edge/v1", "subject": { "type": "file", "name": "myservice.dll", "digest": { "sha256": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684aa...", "sha512": "0b6bf8fdfa..." }, "buildId": "dotnet:mvid:2e81a930-6d5d-4862-b6fd-b6d2a5a8af93" }, "edge": { "edgeId": "b3b40b0e5cfac0dfe5e04dfe0e53e3fb90b504e0be6e0cf1b79dd6e0db9ab012", "caller": { "binaryId": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684aa...", "kind": "Pdb", "value": "MyCompany.Service.Controllers.UserController::GetUser" }, "callee": { "binaryId": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684aa...", "kind": "Pdb", "value": "MyCompany.Service.Core.UserRepository::GetById" }, "reason": "StaticDirectCall", "evidenceHash": "8af02b0ea518f50b4c4735a3df0e93c7c8d3e0f1d2..." }, "provenance": { "toolName": "StellaOps.CallGraph", "toolVersion": "1.0.0", "hostOs": "Windows 11.0.22631", "runtime": ".NET 9.0.0", "pipelineRunId": "build-12345" }, "policyHash": "e6a9a7b1b909b2ba03b6f71a0d13a5b9b2f3e97832..." } ``` ### 2.2 Example GraphManifest ```json { "manifestType": "stellaops.dev/call-graph-manifest/v1", "subject": { "type": "file", "name": "myservice.dll", "digest": { "sha256": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684aa..." }, "buildId": "dotnet:mvid:2e81a930-6d5d-4862-b6fd-b6d2a5a8af93" }, "edgeEnvelopeDigests": [ "f7b0b1e47f599f8990dd52978a0aee22722e7bcb9e30...", "180a2a6e065d667ea2290da8b7ad7010c0d7af9aec33..." ], "roots": [ { "binaryId": "9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684aa...", "kind": "Pdb", "value": "MyCompany.Service.Program::Main" } ], "provenance": { "toolName": "StellaOps.CallGraph", "toolVersion": "1.0.0", "hostOs": "Windows 11.0.22631", "runtime": ".NET 9.0.0", "pipelineRunId": "build-12345" }, "policyHash": "e6a9a7b1b909b2ba03b6f71a0d13a5b9b2f3e97832..." } ``` ### 2.3 Example DSSE envelope wrapping the EdgeStatement ```json { "payloadType": "stellaops.dev/call-edge/v1", "payload": "eyJzdGF0ZW1lbnRUeXBlIjoi...base64-of-edge-statement-json...IiwicG9saWN5SGFzaCI6Ii4uLiJ9", "signatures": [ { "keyid": "edge-signing-key-1", "sig": "k5p5+1Hz+PZd...base64-signature..." } ] } ``` --- ## 3. C# interfaces Drop these into `StellaOps.CallGraph.Core` (or split into `Core` / `Attestation` / `Transparency` as you prefer). ### 3.1 Shared models (minimal, just to compile the interfaces) ```csharp public sealed record BinarySubject( string Name, string Sha256, string? BuildId ); public sealed record FunctionId( string BinaryId, string Kind, string Value ); public enum EdgeReasonKind { StaticImportThunk, StaticDirectCall, StaticCtorOrInitArray, ExceptionHandler, JumpTable, DynamicTraceWitness } public sealed record CallEdge( string EdgeId, FunctionId Caller, FunctionId Callee, EdgeReasonKind Reason, string? EvidenceHash ); public sealed record EdgeStatement( string StatementType, BinarySubject Subject, CallEdge Edge, ToolProvenance Provenance, string PolicyHash ); public sealed record ToolProvenance( string ToolName, string ToolVersion, string HostOs, string Runtime, string PipelineRunId ); public sealed record GraphManifest( string ManifestType, BinarySubject Subject, IReadOnlyList EdgeEnvelopeDigests, IReadOnlyList Roots, ToolProvenance Provenance, string PolicyHash ); public sealed record DsseSignature(string KeyId, string Sig); public sealed record DsseEnvelope( string PayloadType, string Payload, IReadOnlyList Signatures ); ``` --- ### 3.2 `IEdgeExtractor` One extractor per “strategy” (IL calls, import table, .ctors, traces, etc.) ```csharp public sealed record BinaryContext( string Path, byte[] Bytes, BinarySubject Subject // optional: add parsed PE/ELF metadata, debug info handles, etc. ); public interface IEdgeExtractor { /// Unique ID for this extractor (used in logs / debugging). string Id { get; } /// Human readable description, e.g. "Managed IL direct calls". string Description { get; } /// Extract edges from the binary context. Task> ExtractAsync( BinaryContext context, CancellationToken cancellationToken = default); } ``` You can have `ManagedIlEdgeExtractor`, `PeImportTableEdgeExtractor`, `ElfInitArrayEdgeExtractor`, `TraceLogEdgeExtractor`, etc. --- ### 3.3 `IAttestor` Wraps DSSE signing/verification for both edges and manifests. ```csharp public interface IAttestor { /// Sign a single call edge statement as DSSE. Task SignEdgeAsync( EdgeStatement statement, CancellationToken cancellationToken = default); /// Verify DSSE signature and PAE for an edge envelope. Task VerifyEdgeAsync( DsseEnvelope envelope, CancellationToken cancellationToken = default); /// Sign the call graph manifest as DSSE. Task SignManifestAsync( GraphManifest manifest, CancellationToken cancellationToken = default); /// Verify DSSE signature and PAE for a manifest envelope. Task VerifyManifestAsync( DsseEnvelope envelope, CancellationToken cancellationToken = default); } ``` Implementation `DsseAttestor` plugs in your actual crypto (`ISigningKey` or KMS client). --- ### 3.4 `ITransparencyClient` (Rekor / log abstraction) ```csharp public sealed record TransparencyEntryRef( string EntryId, long? Index, string? LogId ); public sealed record TransparencyInclusionProof( string EntryId, string RootHash, int TreeSize, IReadOnlyList AuditPath ); public interface ITransparencyClient { /// Publish a DSSE envelope to the transparency log. Task PublishAsync( DsseEnvelope envelope, CancellationToken cancellationToken = default); /// Fetch an inclusion proof (if supported). Task GetInclusionProofAsync( string entryId, CancellationToken cancellationToken = default); } ``` For v1 you can stub this out with a filesystem log and later swap in a Rekor client. --- ### 3.5 `IReplayer` (deterministic offline verification) This is the “does reality match the attestation?” API. ```csharp public sealed record ReplayMismatch( string Kind, // "MissingEdge", "ExtraEdge", "RootMismatch", etc. string Details ); public sealed record ReplayResult( bool Success, IReadOnlyList Mismatches ); public interface IReplayer { /// Deterministically recompute call graph and compare with an attested manifest + envelopes. Task ReplayAsync( string binaryPath, GraphManifest manifest, IReadOnlyList edgeEnvelopes, CancellationToken cancellationToken = default); } ``` Implementation `CallGraphReplayer` just: 1. Rebuilds `BinarySubject` from `binaryPath`. 2. Re-runs your configured `IEdgeExtractor` chain. 3. Regenerates EdgeIds. 4. Compares EdgeId set + roots with the manifest and envelopes. --- ## 4. Ticket-style breakdown (you can paste this directly) You can rename IDs to match your project convention. --- **CG-1 – Core data contracts** * Create `StellaOps.CallGraph.Core` project. * Add records: `BinarySubject`, `FunctionId`, `CallEdge`, `ToolProvenance`, `EdgeStatement`, `GraphManifest`, `DsseSignature`, `DsseEnvelope`. * Add `BinaryContext` struct/record. * Add `EdgeReasonKind` enum. * Add `Hashing` helper (SHA‑256 hex). **CG-2 – JSON schema + serialization** * Add JSON schema files: * `dsse-envelope-v1.json` * `call-edge-statement-v1.json` * `call-graph-manifest-v1.json` * Configure tests that serialize sample `EdgeStatement` / `GraphManifest` and validate against schemas (optional but nice). **CG-3 – Edge extractor abstraction** * Add `IEdgeExtractor` interface. * Add `BinaryContext` builder that: * Reads bytes from file path * Computes `BinarySubject` (sha256 + buildId placeholder). **CG-4 – MVP PE managed IL extractor** * Reference `Mono.Cecil` (or chosen IL reader). * Implement `ManagedIlEdgeExtractor : IEdgeExtractor` that: * Iterates IL, emits `StaticDirectCall` edges. * Computes deterministic `EdgeId` using canonical string + SHA‑256. * Unit tests: simple test DLL with one method calling another → expect a single edge. **CG-5 – DSSE attestation** * Add `ISigningKey` abstraction and one file-based implementation (RSA or Ed25519). * Implement `DsseAttestor : IAttestor`: * PAE implementation. * Edge + manifest signing & verification. * Tests: * Round-trip sign/verify for edge and manifest. * Tamper payload → verify fails. **CG-6 – Transparency client stub** * Add `ITransparencyClient` + models `TransparencyEntryRef`, `TransparencyInclusionProof`. * Implement `FileTransparencyClient` that appends entries to a JSONL file, returns synthetic IDs. * Add integration test: publish envelope, read it back. **CG-7 – Manifest builder & orchestration** * Implement `ManifestBuilder` (or a small service class) that: * Takes `BinarySubject`, list of edge envelopes, list of roots, tool provenance, policy hash. * Computes `edgeEnvelopeDigests` sorted deterministically. * Add a `CallGraphAttestationService` that: * Accepts binary path. * Builds `BinaryContext`. * Runs all registered `IEdgeExtractor`s. * Signs each `EdgeStatement` via `IAttestor`. * Builds and signs `GraphManifest`. * Optionally publishes envelopes via `ITransparencyClient`. **CG-8 – Replayer** * Implement `CallGraphReplayer : IReplayer`: * Recompute `BinarySubject` from `binaryPath`. * Run extractors. * Compare recomputed EdgeIds against those inferred from envelopes/manifest. * Populate `ReplayResult` with `ReplayMismatch` entries. * Tests: * Self-consistency test (same config → `Success = true`). * Edge removed/added → `Success = false` with appropriate mismatches. **CG-9 – CLI** * New project `StellaOps.CallGraph.Cli`. * Use `System.CommandLine`. * Commands: * `analyze --out ` * `verify --manifest --edges ` * Wire up DI for `IEdgeExtractor`, `IAttestor`, `ITransparencyClient`, `IReplayer`. * Integration test: run `analyze` on a sample binary, then `verify` → exit code 0. --- If you’d like, next step I can help you **decide concrete PE / ELF libraries** for each platform and sketch one full extractor implementation end‑to‑end (e.g., import-table edges for Windows PE). Alright, let’s turn this into something your devs can literally work through story by story. I’ll structure it as **phases → epics → concrete tasks**, with **what to build**, **implementation hints**, and **acceptance criteria**. You can paste these straight into Jira/Linear. --- ## Phase 0 – Solution scaffolding & baseline ### Epic 0.1 – Solution & projects **Goal:** Have a clean .NET solution with the right projects and references. **Tasks** **T0.1.1 – Create solution & projects** * Create solution: `StellaOps.CallGraph.sln` * Projects: * `StellaOps.CallGraph.Core` (Class Library) * `StellaOps.CallGraph.BinaryParsers` (Class Library) * `StellaOps.CallGraph.EdgeExtraction` (Class Library) * `StellaOps.CallGraph.Attestation` (Class Library) * `StellaOps.CallGraph.Cli` (Console App) * `StellaOps.CallGraph.Tests` (xUnit/NUnit) * Set `LangVersion` to latest and target your `.NET 10` TFM. **Acceptance criteria** * `dotnet build` succeeds. * Projects reference each other as expected (Core ← BinaryParsers/EdgeExtraction/Attestation, Cli references all). **T0.1.2 – Add core dependencies** In relevant projects, add NuGet packages: * Core: * `System.Text.Json` * BinaryParsers: * `System.Reflection.Metadata` * (later) `Mono.Cecil` or separate in EdgeExtraction * EdgeExtraction: * `Mono.Cecil` * CLI: * `System.CommandLine` * Tests: * `xunit` / `NUnit` * `FluentAssertions` (optional, but nice) **Acceptance criteria** * All packages restored successfully. * No unused or redundant packages. --- ## Phase 1 – Core domain models & contracts ### Epic 1.1 – Data contracts **Goal:** Have a shared, stable model for binaries, functions, edges, manifests, DSSE envelopes. **Tasks** **T1.1.1 – Implement core records** In `StellaOps.CallGraph.Core` add: * `BinarySubject` * `FunctionId` * `EdgeReasonKind` (enum) * `CallEdge` * `ToolProvenance` * `EdgeStatement` * `GraphManifest` * `DsseSignature` * `DsseEnvelope` Use auto-properties or records; keep them immutable where possible. **Key details** * `BinarySubject`: * `Name` (file name or logical name) * `Sha256` (hex string) * `BuildId` (nullable) * `FunctionId`: * `BinaryId` pointing at `BinarySubject.Sha256` or `BuildId` * `Kind` string: `"RVA" | "Symbol" | "Pdb" | "Other"` * `Value` string: RVA (hex), symbol name, or metadata token * `CallEdge`: * `EdgeId` (computed deterministic ID) * `Caller`, `Callee` (FunctionId) * `Reason` (EdgeReasonKind) * `EvidenceHash` (nullable) **Acceptance criteria** * Models compile. * Public properties clearly named. * No business logic baked into these types yet. **T1.1.2 – Implement `EdgeReasonKind` enum** Values (at least): ```csharp public enum EdgeReasonKind { StaticImportThunk, StaticDirectCall, StaticCtorOrInitArray, ExceptionHandler, JumpTable, DynamicTraceWitness } ``` **Acceptance criteria** * Enum used by `CallEdge` and anywhere reasons are exposed. * No magic strings for reasons used anywhere else. --- ## Phase 2 – Binary abstraction & managed PE parser ### Epic 2.1 – Binary context & hashing **Goal:** Standard way to represent “this binary we’re analyzing”. **Tasks** **T2.1.1 – Implement hashing helper** In `Core`: ```csharp public static class Hashing { public static string Sha256Hex(Stream stream) { using var sha = SHA256.Create(); var hash = sha.ComputeHash(stream); return Convert.ToHexString(hash).ToLowerInvariant(); } public static string Sha256Hex(byte[] data) { ... } } ``` **Acceptance criteria** * Unit test: Known byte input → expected hex string. * Works on large streams without loading entire file into memory (stream-based). **T2.1.2 – Define `BinaryContext`** ```csharp public sealed record BinaryContext( string Path, BinarySubject Subject, byte[] Bytes // Later: add parsed metadata if needed ); ``` **Acceptance criteria** * `BinaryContext` encapsulates exactly what extractors need now (path + subject + raw bytes). * No parser-specific details leak into it yet. --- ### Epic 2.2 – IBinaryParser and managed implementation **Goal:** For a .NET assembly, identify it and list functions + roots. **Tasks** **T2.2.1 – Define `IBinaryParser`** In Core: ```csharp public interface IBinaryParser { BinaryContext Load(string path); // compute subject + read bytes IReadOnlyList GetAllFunctions(BinaryContext context); IReadOnlyList GetRoots(BinaryContext context); } ``` **Acceptance criteria** * Interface compiles. * No implementation yet. **T2.2.2 – Implement `PeManagedBinaryParser`** In `BinaryParsers`: * Use `FileStream` + `Hashing.Sha256Hex` to compute SHA-256. * Use `PEReader` + `MetadataReader` to: * Confirm this is a managed assembly (has metadata). * Extract MVID & assembly name if needed. * Build `BinarySubject`: * `Name` = filename without path. * `Sha256` = hash. * `BuildId` = `"dotnet:mvid:"`. * `Load(path)`: * Compute hash, read bytes to memory, return `BinaryContext`. **Implementation hints** ```csharp using var stream = File.OpenRead(path); var sha256 = Hashing.Sha256Hex(stream); stream.Position = 0; var peReader = new PEReader(stream); var mdReader = peReader.GetMetadataReader(); var mvid = mdReader.GetGuid(mdReader.GetModuleDefinition().Mvid); var subject = new BinarySubject( Name: Path.GetFileName(path), Sha256: sha256, BuildId: $"dotnet:mvid:{mvid:D}" ); ``` **T2.2.3 – Implement `GetAllFunctions`** * Re-open assembly with `Mono.Cecil` (simpler for IL later): ```csharp var module = ModuleDefinition.ReadModule(context.Path); foreach (var type in module.Types) foreach (var method in type.Methods) { var funcId = new FunctionId( BinaryId: context.Subject.Sha256, Kind: "Pdb", Value: $"{type.FullName}::{method.Name}" ); } ``` **T2.2.4 – Implement `GetRoots`** First pass (simple but deterministic): * Root = `Program.Main`-like method in entry assembly: * Find method(s) with name `"Main"` in public types under namespaces like `*.Program` or `Program`. * Mark them as roots: same `FunctionId` representation. Later you can extend to: * Public API surface (public methods in public types). * Static constructors `.cctor`. **Acceptance criteria (for Epic 2.2)** * Unit test assembly with simple code: * `GetAllFunctions` returns all methods (at least user-defined). * `GetRoots` includes `Program.Main`. * If assembly is not managed (no metadata), `PeManagedBinaryParser` fails gracefully with a clear exception or sentinel result (you can add a `CanParse(path)` later if needed). --- ## Phase 3 – Edge extraction (managed IL) ### Epic 3.1 – Edge extractor abstraction **Goal:** Have a pluggable way to produce edges from a `BinaryContext`. **Tasks** **T3.1.1 – Define `IEdgeExtractor`** ```csharp public interface IEdgeExtractor { string Id { get; } string Description { get; } Task> ExtractAsync( BinaryContext context, CancellationToken cancellationToken = default); } ``` **Acceptance criteria** * Interface compiles. * No dependencies on dsse/transparency yet. --- ### Epic 3.2 – Managed IL direct-call extractor **Goal:** For .NET assemblies, emit `StaticDirectCall` edges using IL. **Tasks** **T3.2.1 – Implement `ManagedIlEdgeExtractor` basics** In `EdgeExtraction`: * Use `Mono.Cecil` to re-open `context.Path`. * Iterate `module.Types` / `type.Methods` where `HasBody`. * For each instruction in `method.Body.Instructions`: * If `instr.OpCode.FlowControl` is `FlowControl.Call` **and** `instr.Operand` is `MethodReference` → create edge. **Caller `FunctionId`:** ```csharp var callerId = new FunctionId( BinaryId: context.Subject.Sha256, Kind: "Pdb", Value: $"{method.DeclaringType.FullName}::{method.Name}" ); ``` **Callee `FunctionId`:** ```csharp var calleeId = new FunctionId( BinaryId: context.Subject.Sha256, // same binary for now Kind: "Pdb", Value: $"{calleeMethod.DeclaringType.FullName}::{calleeMethod.Name}" ); ``` **T3.2.2 – Implement deterministic `EdgeId` generation** Add helper in Core: ```csharp public static class EdgeIdGenerator { public static string Compute(CallEdge edgeWithoutId) { var canonical = string.Join("|", new[] { edgeWithoutId.Caller.BinaryId, edgeWithoutId.Caller.Kind, edgeWithoutId.Caller.Value, edgeWithoutId.Callee.BinaryId, edgeWithoutId.Callee.Kind, edgeWithoutId.Callee.Value, edgeWithoutId.Reason.ToString() }); var bytes = Encoding.UTF8.GetBytes(canonical); return Hashing.Sha256Hex(bytes); } } ``` Then in `ManagedIlEdgeExtractor`, build edge without ID, compute ID, then create the full `CallEdge`. **T3.2.3 – Set reason & evidence** * For now: * `Reason = EdgeReasonKind.StaticDirectCall` * `EvidenceHash = null` (or hash IL snippet later). **T3.2.4 – Unit tests** Create a tiny test assembly (project) like: ```csharp public class A { public void Caller() => Callee(); public void Callee() { } } ``` Then in test: * Run `ManagedIlEdgeExtractor` on that assembly. * Assert: * There is an edge where: * `Caller.Value` contains `"A::Caller"` * `Callee.Value` contains `"A::Callee"` * `Reason == StaticDirectCall` * `EdgeId` is non-empty and stable across runs (run twice, compare). **Acceptance criteria (Epic 3.2)** * For simple assemblies, edges match expected calls. * No unhandled exceptions on normal assemblies. --- ## Phase 4 – DSSE attestation engine ### Epic 4.1 – DSSE PAE & signing abstraction **Goal:** Ability to sign statements (edges, manifest) with DSSE envelopes. **Tasks** **T4.1.1 – Implement PAE helper** In `Attestation`: ```csharp public static class DssePaEncoder { public static byte[] PreAuthEncode(string payloadType, byte[] payload) { static byte[] Utf8(string s) => Encoding.UTF8.GetBytes(s); static byte[] Concat(params byte[][] parts) { ... } var header = Utf8("DSSEv1"); var pt = Utf8(payloadType); var ptLen = Utf8(pt.Length.ToString(CultureInfo.InvariantCulture)); var payloadLen = Utf8(payload.Length.ToString(CultureInfo.InvariantCulture)); var space = Utf8(" "); return Concat(header, space, ptLen, space, pt, space, payloadLen, space, payload); } } ``` **T4.1.2 – Define `ISigningKey` abstraction** ```csharp public interface ISigningKey { string KeyId { get; } Task SignAsync(byte[] data, CancellationToken ct = default); Task VerifyAsync(byte[] data, byte[] signature, CancellationToken ct = default); } ``` Implement a simple file-based Ed25519 or RSA keypair later. **T4.1.3 – Implement `DsseAttestor`** Implements `IAttestor`: * `SignEdgeAsync(EdgeStatement)`: * Serialize `EdgeStatement` to JSON. * Compute `pae = DssePaEncoder.PreAuthEncode(payloadType, payloadBytes)`. * `sigBytes = ISigningKey.SignAsync(pae)`. * Return `DsseEnvelope(payloadType, base64(payloadBytes), [sig])`. * `VerifyEdgeAsync(DsseEnvelope)`: * Decode payload. * Recompute PAE. * Verify signature with `ISigningKey.VerifyAsync`. Do the same for `GraphManifest`. **Serialization settings** Use `System.Text.Json` with: ```csharp new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; ``` **T4.1.4 – Unit tests** * Use a test signing key with deterministic signatures. * Sign + verify: * EdgeStatement → DSSE → verify returns true. * Tamper `payload` and ensure verify returns false. **Acceptance criteria (Epic 4.1)** * DSSE envelopes created and verified locally. * Code is isolated from edge extraction (no cyclical dependencies). --- ## Phase 5 – Call graph pipeline & manifest ### Epic 5.1 – Attestation orchestrator **Goal:** End-to-end: parse binary → extract edges → sign edges → build & sign manifest. **Tasks** **T5.1.1 – Implement `CallGraphAttestationService`** In `Attestation` (or a separate Orchestration project if you prefer): ```csharp public sealed class CallGraphAttestationService { private readonly IBinaryParser _parser; private readonly IEnumerable _extractors; private readonly IAttestor _attestor; private readonly ToolProvenance _provenance; private readonly string _policyHash; public async Task<(DsseEnvelope ManifestEnvelope, IReadOnlyList EdgeEnvelopes, GraphManifest Manifest)> AnalyzeAndAttestAsync(string binaryPath, CancellationToken ct = default) { // 1) Load binary // 2) Run extractors // 3) Build EdgeStatements & DSSE envelopes // 4) Compute manifest & DSSE envelope } } ``` **Implementation detail** 1. Load: ```csharp var context = _parser.Load(binaryPath); ``` 2. Extract edges: ```csharp var edges = new List(); foreach (var extractor in _extractors) { var result = await extractor.ExtractAsync(context, ct); edges.AddRange(result); } ``` 3. Build `EdgeStatement` per edge: ```csharp var stmt = new EdgeStatement( StatementType: "stellaops.dev/call-edge/v1", Subject: context.Subject, Edge: edge, Provenance: _provenance, PolicyHash: _policyHash ); var env = await _attestor.SignEdgeAsync(stmt, ct); ``` 4. Compute `edgeEnvelopeDigests`: ```csharp var envelopeDigests = edgeEnvelopes .Select(e => Hashing.Sha256Hex( Encoding.UTF8.GetBytes(JsonSerializer.Serialize(e, jsonOptions)))) .OrderBy(x => x) // deterministic order .ToList(); ``` 5. Build roots: ```csharp var roots = _parser.GetRoots(context); ``` 6. Build `GraphManifest` & sign. **T5.1.2 – Unit test: vertical slice** * Use the simple test assembly from earlier. * Wire up: * `PeManagedBinaryParser` * `ManagedIlEdgeExtractor` * `DsseAttestor` with test signing key. * Simple `ToolProvenance` & `policyHash` constant. * Assert: * At least one edge envelope produced. * Manifest includes same count of `edgeEnvelopeDigests`. * `VerifyManifestAsync` and `VerifyEdgeAsync` return true. **Acceptance criteria (Epic 5.1)** * You can call one method in code and get a manifest DSSE + edge DSSEs for a binary. * Tests confirm everything compiles and verifies. --- ## Phase 6 – Transparency log client (Rekor or equivalent) ### Epic 6.1 – Abstract transparency client **Goal:** Publish envelopes to a transparency log (real or stub). **Tasks** **T6.1.1 – Implement `ITransparencyClient`** As previously defined: ```csharp public interface ITransparencyClient { Task PublishAsync( DsseEnvelope envelope, CancellationToken cancellationToken = default); Task GetInclusionProofAsync( string entryId, CancellationToken cancellationToken = default); } ``` **T6.1.2 – Implement `FileTransparencyClient` (MVP)** * Writes each envelope as a JSON line into a file: * e.g., `transparency-log.jsonl`. * `EntryId` = SHA-256 of envelope JSON. * `Index` = line number. * `LogId` = fixed string `"local-file-log"`. **Acceptance criteria** * Publishing several envelopes appends lines. * No concurrency issues for single-process usage. **T6.1.3 – Integrate into `CallGraphAttestationService` (optional in v1)** * After signing edges & manifest: * Optionally call `_transparencyClient.PublishAsync(...)` for each envelope. * Return transparency references if you want, or store them alongside. --- ## Phase 7 – Replayer / verifier ### Epic 7.1 – Offline replay **Goal:** Given a binary + manifest + edge envelopes, recompute graph and compare. **Tasks** **T7.1.1 – Implement `IReplayer`** ```csharp public sealed record ReplayMismatch(string Kind, string Details); public sealed record ReplayResult(bool Success, IReadOnlyList Mismatches); public interface IReplayer { Task ReplayAsync( string binaryPath, GraphManifest manifest, IReadOnlyList edgeEnvelopes, CancellationToken cancellationToken = default); } ``` **T7.1.2 – Implement `CallGraphReplayer`** Steps: 1. **Verify manifest subject vs current file:** * Recompute SHA-256 of `binaryPath`. * Compare to `manifest.Subject.digest["sha256"]` (or `Sha256` property). * If mismatch → add `ReplayMismatch("BinaryDigestMismatch", "...")`. 2. **Re-run analysis:** * Use same `IBinaryParser` + `IEdgeExtractor` set as production. * Build set of recomputed `EdgeId`s. 3. **Parse envelopes:** * For each edge envelope: * Verify DSSE signature (using same `IAttestor.VerifyEdgeAsync`). * Decode payload and deserialize `EdgeStatement`. * Collect attested `EdgeId`s into a set. 4. **Compare:** * `MissingEdge`: in attested set but not recomputed. * `ExtraEdge`: recomputed but not in attested. * `RootMismatch`: any difference between recomputed roots and `manifest.Roots`. 5. Set `Success = (no mismatches)`. **T7.1.3 – Tests** * Use the vertical-slice test: * Generate attestation. * Call replayer → expect `Success = true`. * Modify binary (or edges) slightly: * Rebuild binary without re-attesting. * Replay → expect `BinaryDigestMismatch` or edge differences. **Acceptance criteria** * Replayer can detect changed binaries or changed call graph. * No unhandled exceptions on normal flows. --- ## Phase 8 – CLI & developer UX ### Epic 8.1 – CLI commands **Goal:** Simple commands devs can run locally. **Tasks** **T8.1.1 – Implement `analyze` command** In `StellaOps.CallGraph.Cli`: * Command: `analyze --out ` * Behavior: 1. Resolve services via DI: * `IBinaryParser` → `PeManagedBinaryParser` * `IEdgeExtractor` → `ManagedIlEdgeExtractor` * `IAttestor` → `DsseAttestor` with test key (configurable later) * `ITransparencyClient` → `FileTransparencyClient` or no-op 2. Call `CallGraphAttestationService.AnalyzeAndAttestAsync`. 3. Write: * `manifest.dsse.json` * `edge-000001.dsse.json`, etc. **T8.1.2 – Implement `verify` command** * Command: `verify --manifest --edges-dir ` * Behavior: 1. Load manifest DSSE, verify signature, deserialize `GraphManifest`. 2. Load all edge DSSE JSON files from directory. 3. Call `IReplayer.ReplayAsync`. 4. Exit code: * `0` if `Success = true`. * Non-zero if mismatches. **Acceptance criteria** * Running `analyze` on sample binary produces DSSE files. * Running `verify` on same binary + outputs returns success (exit code 0). * Help text is clear: `stella-callgraph --help`. --- ## Phase 9 – Hardening & non-functional aspects ### Epic 9.1 – Logging & error handling **Tasks** * Add minimal logging via `Microsoft.Extensions.Logging`. * Log: * Binary path, subject hash. * Number of edges extracted per extractor. * Errors with context (but no sensitive key material). **Acceptance criteria** * Mis-parsed binaries or invalid IL lead to clear error logs, not silent failures. ### Epic 9.2 – Configuration & keys **Tasks** * Add configuration model for: * DSSE signing key location / KMS. * Policy hash value (string). * Transparency log settings (file path or HTTP endpoint). * Use `IConfiguration` inside CLI. **Acceptance criteria** * You can switch signing keys or policy hash without code change. * No private keys are ever written to logs. --- ## Suggested order for the team If you want a straight, “do this in order” list for devs: 1. **Phase 0** – Projects & deps (T0.1.x, T2.1.1). 2. **Phase 1** – Core models (T1.1.x). 3. **Phase 2** – `IBinaryParser` + `PeManagedBinaryParser` + hashing (T2.2.x). 4. **Phase 3** – `IEdgeExtractor` + `ManagedIlEdgeExtractor` + EdgeId (T3.1.x, T3.2.x). 5. **Phase 4** – DSSE PAE + `ISigningKey` + `DsseAttestor` (T4.1.x). 6. **Phase 5** – `CallGraphAttestationService` vertical slice (T5.1.x). 7. **Phase 6** – Simple `FileTransparencyClient` (T6.1.x). 8. **Phase 7** – `IReplayer` + `CallGraphReplayer` (T7.1.x). 9. **Phase 8** – CLI `analyze` + `verify` commands (T8.1.x). 10. **Phase 9** – Logging, config, hardening (T9.1.x, T9.2.x). If you’d like, I can next expand **any one epic** (e.g. “Edge extraction” or “DSSE attestation”) into even more concrete pseudo-code so a dev can almost just fill in the blanks.