Here’s a clear, SBOM‑first blueprint you can drop into Stella Ops without extra context. --- # SBOM‑first spine (with attestations) — the short, practical version ![High-level flow](https://dummyimage.com/1200x300/ffffff/000000.png\&text=Scanner+→+Sbomer+→+Authority+→+Graphs+/%20APIs) ## Why this matters (plain English) * **SBOMs** (CycloneDX/SPDX) = a complete parts list of your software. * **Attestations** (in‑toto + DSSE) = tamper‑evident receipts proving *who did what, to which artifact, when, and how*. * **Determinism** = if you re‑scan tomorrow, you get the same result for the same inputs. * **Explainability** = every risk decision links back to evidence you can show to auditors/customers. --- ## Core pipeline (modules & responsibilities) 1. **Scan (Scanner)** * Inputs: container image / dir / repo. * Outputs: raw facts (packages, files, symbols), and a **Scan‑Evidence** attestation (DSSE‑wrapped in‑toto statement). * Must support offline feeds (bundle CVE/NVD/OSV/vendor advisories). 2. **Sbomer** * Normalizes raw facts → **canonical SBOM** (CycloneDX or SPDX) with: * PURLs, license info, checksums, build‑IDs (ELF/PE/Mach‑O), source locations. * Emits **SBOM‑Produced** attestation linking SBOM ↔ image digest. 3. **Authority** * Verifies every attestation chain (Sigstore/keys; PQ-ready option later). * Stamps **Policy‑Verified** attestation (who approved, policy hash, inputs). * Persists **trust‑log**: signatures, cert chains, Rekor‑like index (mirrorable offline). 4. **Graph Store (Canonical Graph)** * Ingests SBOM, vulnerabilities, reachability facts, VEX statements. * Preserves **evidence links** (edge predicates: “found‑by”, “reachable‑via”, “proven‑by”). * Enables **deterministic replay** (snapshot manifests: feeds+rules+hashes). --- ## Stable APIs (keep these boundaries sharp) * **/scan** → start scan; returns Evidence ID + attestation ref. * **/sbom** → get canonical SBOM (by image digest or Evidence ID). * **/attest** → submit/fetch attestations; verify chain; returns trust‑proof. * **/vex‑gate** → policy decision: *allow / warn / block* with proof bundle. * **/diff** → SBOM↔SBOM + SBOM↔runtime diffs (see below). * **/unknowns** → create/list/resolve Unknowns (signals needing human/vendor input). Design notes: * All responses include `decision`, `explanation`, `evidence[]`, `hashes`, `clock`. * Support **air‑gap**: all endpoints operate on local bundles (ZIP/TAR with SBOM+attestations+feeds). --- ## Determinism & “Unknowns” (noise‑killer loop) **Smart diffs** * **SBOM↔SBOM**: detect added/removed/changed components (by PURL+version+hash). * **SBOM↔runtime**: prove reachability (e.g., symbol/function use, loaded libs, process maps). * Score only on **provable** paths; gate on **VEX** (vendor/exploitability statements). **Unknowns handler** * Any unresolved signal (ambiguous CVE mapping, stripped binary, unverified vendor VEX) → **Unknowns** queue: * SLA, owner, evidence snapshot, audit trail. * State machine: `new → triage → vendor‑query → verified → closed`. * Every VEX or vendor reply becomes an attestation; decisions re‑evaluated deterministically. --- ## What to store (so you can explain every decision) * **Artifacts**: image digest, SBOM hash, feed versions, rule set hash. * **Proofs**: DSSE envelopes, signatures, certs, inclusion proofs (Rekor‑style). * **Predicates (edges)**: * `contains(component)`, `vulnerable_to(cve)`, `reachable_via(callgraph|runtime)`, * `overridden_by(vex)`, `verified_by(authority)`, `derived_from(scan-evidence)`. * **Why‑strings**: human‑readable proof trails (1–3 sentences) output with every decision. --- ## Minimal policies that work on day 1 * **Block** only when: `vuln.severity ≥ High` AND `reachable == true` AND `no VEX allows`. * **Warn** when: `High/Critical` but `reachable == unknown` → route to Unknowns with SLA. * **Allow** when: `Low/Medium` OR VEX says `not_affected` (trusted signer + policy). --- ## Offline/air‑gap bundle format (zip) ``` /bundle/ feeds/ (NVD, OSV, vendor) + manifest.json (hashes, timestamps) sboms/ imageDigest.json attestations/ *.jsonl (DSSE) proofs/ rekor/ merkle.json policy/ lattice.json replay/ inputs.lock (content‑hashes of everything above) ``` * Every API accepts `?bundle=/path/to/bundle.zip`. * **Replay**: `inputs.lock` guarantees deterministic re‑evaluation. --- ## .NET 10 implementation sketch (pragmatic) * **Contracts**: `StellaOps.Contracts.*` (Scan, Attest, VexGate, Diff, Unknowns). * **Attestations**: `StellaOps.Attest.Dsse` (IEnvelope, IStatement); pluggable crypto (FIPS/GOST/SM/PQ). * **SBOM**: `StellaOps.Sbom` (CycloneDX/SPDX models + mappers; PURL utilities). * **Graph**: `StellaOps.Graph` (EF Core 9/10 over Mongo/Postgres; edge predicates as enums + JSON evidence). * **Policy/Lattice**: `StellaOps.Policy.Lattice` (pure functions over graph snapshots; produce Decision+Why). * **Unknowns**: `StellaOps.Unknowns` (aggregate root; SLA timers; audit events). * **CLI**: `stella scan|sbom|attest|vex-gate|diff|unknowns` with `--bundle`. Key patterns: * All public methods return `(Result value, Proof proof)`. * Hash everything (inputs, rules, feeds) and bake into `Proof`. * Emit DSSE envelopes at each hop; verify on ingest; never trust unauthenticated input. --- ## Next steps (week‑one checklist) * Define JSON schemas for: **Decision** (with Why+Proof), **Unknown**, **DiffResult**. * Stand up **/scan**, **/sbom**, **/attest**, **/vex‑gate** skeletons returning mock Proofs. * Implement **SBOM↔SBOM diff** (PURL+version+hash) and wire to **/diff**. * Create **bundle.zip** reader/writer + `inputs.lock`. * Add **Authority.Verify()** with DSSE verification + trust‑store. * Ship a basic **policy.lattice.json** (3 rules above) and e2e test: image → allow/block with proofs. If you want, I can generate: * The exact C# interfaces/classes (namespaces + DTOs), * JSON Schemas for Decision/Proof/Unknowns, * A seed `bundle.zip` layout with fake data for local e2e. Cool, let’s turn that blueprint into something your devs can actually build from. Below is a detailed, opinionated implementation plan you can paste into an engineering doc and assign as epics / tickets. --- ## 0. Solution layout (concrete) **Repo structure** ```text /stella-ops /src StellaOps.Contracts // DTOs, API contracts, JSON schemas StellaOps.Domain // Core domain types (ArtifactId, Proof, Decision, etc.) StellaOps.Attest // DSSE envelopes, in-toto statements, signing/verification StellaOps.Sbom // SBOM models + normalization StellaOps.Graph // Graph store, entities, queries StellaOps.Policy // Policy engine (lattice evaluation) StellaOps.WebApi // HTTP APIs: /scan, /sbom, /attest, /vex-gate, /diff, /unknowns StellaOps.Cli // `stella` CLI, offline bundles /tests StellaOps.Tests.Unit StellaOps.Tests.Integration StellaOps.Tests.E2E ``` **Baseline tech assumptions** * Runtime: .NET (8+; you can call it “.NET 10” in your roadmap). * API: ASP.NET Core minimal APIs. * DB: Postgres (via EF Core) for graph + unknowns + metadata. * Storage: local filesystem / S3-compatible for bundle zips, scanner DB caches. * External scanners: Trivy / Grype / Syft (invoked via CLI with deterministic config). --- ## 1. Core domain & shared contracts (Phase 1) **Goal:** Have a stable core domain + contracts that all teams can build against. ### 1.1 Core domain types (`StellaOps.Domain`) Implement: ```csharp public readonly record struct Digest(string Algorithm, string Value); // e.g. ("sha256", "abcd...") public readonly record struct ArtifactRef(string Kind, string Value); // Kind: "container-image", "file", "package", "sbom", etc. public readonly record struct EvidenceId(Guid Value); public readonly record struct AttestationId(Guid Value); public enum PredicateType { ScanEvidence, SbomProduced, PolicyVerified, VulnerabilityFinding, ReachabilityFinding, VexStatement } public sealed class Proof { public string ProofId { get; init; } = default!; public Digest InputsLock { get; init; } = default!; // hash of feeds+rules+sbom bundle public DateTimeOffset EvaluatedAt { get; init; } public IReadOnlyList EvidenceIds { get; init; } = Array.Empty(); public IReadOnlyDictionary Meta { get; init; } = new Dictionary(); } ``` ### 1.2 Attestation model (`StellaOps.Attest`) Implement DSSE + in‑toto abstractions: ```csharp public sealed class DsseEnvelope { public string PayloadType { get; init; } = default!; public string Payload { get; init; } = default!; // base64url(JSON) public IReadOnlyList Signatures { get; init; } = Array.Empty(); } public sealed class DsseSignature { public string KeyId { get; init; } = default!; public string Sig { get; init; } = default!; // base64url } public interface IStatement { string Type { get; } // in-toto type URI string PredicateType { get; } // URI or enum -> string TPredicate Predicate { get; } string Subject { get; } // e.g., image digest } ``` Attestation services: ```csharp public interface IAttestationSigner { Task SignAsync(IStatement statement, CancellationToken ct); } public interface IAttestationVerifier { Task VerifyAsync(DsseEnvelope envelope, CancellationToken ct); } ``` ### 1.3 Decision & VEX-gate contracts (`StellaOps.Contracts`) ```csharp public enum GateDecisionKind { Allow, Warn, Block } public sealed class GateDecision { public GateDecisionKind Decision { get; init; } public string Reason { get; init; } = default!; // short human-readable public Proof Proof { get; init; } = default!; public IReadOnlyList Evidence { get; init; } = Array.Empty(); // EvidenceIds / AttestationIds } public sealed class VexGateRequest { public ArtifactRef Artifact { get; init; } public string? Environment { get; init; } // "prod", "staging", cluster id, etc. public string? BundlePath { get; init; } // optional offline bundle path } ``` **Acceptance criteria** * Shared projects compile. * No service references each other directly (only via Contracts + Domain). * Example test that serializes/deserializes GateDecision and DsseEnvelope using System.Text.Json. --- ## 2. SBOM pipeline (Scanner → Sbomer) (Phase 2) **Goal:** For a container image, produce a canonical SBOM + attestation deterministically. ### 2.1 Scanner integration (`StellaOps.WebApi` + `StellaOps.Cli`) #### API contract (`/scan`) ```csharp public sealed class ScanRequest { public string SourceType { get; init; } = default!; // "container-image" | "directory" | "git-repo" public string Locator { get; init; } = default!; // e.g. "registry/myapp:1.2.3" public bool IncludeFiles { get; init; } = true; public bool IncludeLicenses { get; init; } = true; public string? BundlePath { get; init; } // for offline data } public sealed class ScanResponse { public EvidenceId EvidenceId { get; init; } public AttestationId AttestationId { get; init; } public Digest ArtifactDigest { get; init; } = default!; } ``` #### Implementation steps 1. **Scanner abstraction** ```csharp public interface IArtifactScanner { Task ScanAsync(ScanRequest request, CancellationToken ct); } public sealed class ScanResult { public ArtifactRef Artifact { get; init; } = default!; public Digest ArtifactDigest { get; init; } = default!; public IReadOnlyList Packages { get; init; } = Array.Empty(); public IReadOnlyList Files { get; init; } = Array.Empty(); } ``` 2. **CLI wrapper** (Trivy/Grype/Syft): * Implement `SyftScanner : IArtifactScanner`: * Invoke external CLI with fixed flags. * Use JSON output mode. * Resolve CLI path from config. * Ensure deterministic: * Disable auto-updating DB. * Use a local DB path versioned and optionally included into bundle. * Write parsing code Syft → `ScanResult`. * Add retry & clear error mapping (timeout, auth error, network error). 3. **/scan endpoint** * Validate request. * Call `IArtifactScanner.ScanAsync`. * Build a `ScanEvidence` predicate: ```csharp public sealed class ScanEvidencePredicate { public ArtifactRef Artifact { get; init; } = default!; public Digest ArtifactDigest { get; init; } = default!; public DateTimeOffset ScannedAt { get; init; } public string ScannerName { get; init; } = default!; public string ScannerVersion { get; init; } = default!; public IReadOnlyList Packages { get; init; } = Array.Empty(); } ``` * Build in‑toto statement for predicate. * Call `IAttestationSigner.SignAsync`, persist: * Raw envelope to `attestations` table. * Map to `EvidenceId` + `AttestationId`. **Acceptance criteria** * Given a fixed image and fixed scanner DB, repeated `/scan` calls produce identical: * `ScanResult` (up to ordering). * `ScanEvidence` payload. * `InputsLock` proof hash (once implemented). * E2E test: run scan on a small public image in CI using a pre-bundled scanner DB. --- ### 2.2 Sbomer (`StellaOps.Sbom` + `/sbom`) **Goal:** Normalize `ScanResult` into a canonical SBOM (CycloneDX/SPDX) + emit SBOM attestation. #### Models Create neutral SBOM model (internal): ```csharp public sealed class CanonicalComponent { public string Name { get; init; } = default!; public string Version { get; init; } = default!; public string Purl { get; init; } = default!; public string? License { get; init; } public Digest Digest { get; init; } = default!; public string? SourceLocation { get; init; } // file path, layer info } public sealed class CanonicalSbom { public string SbomId { get; init; } = default!; public ArtifactRef Artifact { get; init; } = default!; public Digest ArtifactDigest { get; init; } = default!; public IReadOnlyList Components { get; init; } = Array.Empty(); public DateTimeOffset CreatedAt { get; init; } public string Format { get; init; } = "CycloneDX-JSON-1.5"; // default } ``` #### Sbomer service ```csharp public interface ISbomer { CanonicalSbom FromScan(ScanResult scan); string ToCycloneDxJson(CanonicalSbom sbom); string ToSpdxJson(CanonicalSbom sbom); } ``` Implementation details: * Map OS/deps to PURLs (use existing PURL libs or implement minimal helpers). * Stable ordering: * Sort components by `Purl` then `Version` before serialization. * Hash the SBOM JSON → `Digest` (e.g., `Digest("sha256", "...")`). #### SBOM attestation & `/sbom` endpoint * For an `ArtifactRef` (or `ScanEvidence` EvidenceId): 1. Fetch latest `ScanResult` from DB. 2. Call `ISbomer.FromScan`. 3. Serialize to CycloneDX. 4. Emit `SbomProduced` predicate & DSSE envelope. 5. Persist SBOM JSON blob & link to artifact. **Acceptance criteria** * Same `ScanResult` always produces bit-identical SBOM JSON. * Unit tests verifying: * PURL mapping correctness. * Stable ordering. * `/sbom` endpoint can: * Build SBOM from scan. * Return existing SBOM if already generated (idempotence). --- ## 3. Attestation Authority & trust log (Phase 3) **Goal:** Verify all attestations, store them with a trust log, and produce `PolicyVerified` attestations. ### 3.1 Authority service (`StellaOps.Attest` + `StellaOps.WebApi`) Key interfaces: ```csharp public interface IAuthority { Task RecordAsync(DsseEnvelope envelope, CancellationToken ct); Task VerifyChainAsync(ArtifactRef artifact, CancellationToken ct); } ``` Implementation steps: 1. **Attestations store** * Table `attestations`: * `id` (AttestationId, PK) * `artifact_kind` / `artifact_value` * `predicate_type` (enum) * `payload_type` * `payload_hash` * `envelope_json` * `created_at` * `signer_keyid` * Table `trust_log`: * `id` * `attestation_id` * `status` (verified / failed / pending) * `reason` * `verified_at` * `verification_data_json` (cert chain, Rekor log index, etc.) 2. **Verification pipeline** * Implement `IAttestationVerifier.VerifyAsync`: * Check envelope integrity (no duplicate signatures, required fields). * Verify crypto signature (keys from configuration store or Sigstore if you integrate later). * `IAuthority.RecordAsync`: * Verify envelope. * Save to `attestations`. * Add entry to `trust_log`. * `VerifyChainAsync`: * For a given `ArtifactRef`: * Load all attestations for that artifact. * Ensure each is `status=verified`. * Compute `InputsLock` = hash of: * Sorted predicate payloads. * Feeds manifest. * Policy rules. * Return `Proof`. ### 3.2 `/attest` API * **POST /attest**: submit DSSE envelope (for external tools). * **GET /attest?artifact=`...`**: list attestations + trust status. * **GET /attest/{id}/proof**: return verification proof (including InputsLock). **Acceptance criteria** * Invalid signatures rejected. * Tampering test: alter a byte in envelope JSON → verification fails. * `VerifyChainAsync` returns same `Proof.InputsLock` for identical sets of inputs. --- ## 4. Graph Store & Policy engine (Phase 4) **Goal:** Store SBOM, vulnerabilities, reachability, VEX, and query them to make deterministic VEX-gate decisions. ### 4.1 Graph model (`StellaOps.Graph`) Tables (simplified): * `artifacts`: * `id` (PK), `kind`, `value`, `digest_algorithm`, `digest_value` * `components`: * `id`, `purl`, `name`, `version`, `license`, `digest_algorithm`, `digest_value` * `vulnerabilities`: * `id`, `cve_id`, `severity`, `source` (NVD/OSV/vendor), `data_json` * `vex_statements`: * `id`, `cve_id`, `component_purl`, `status` (`not_affected`, `affected`, etc.), `source`, `data_json` * `edges`: * `id`, `from_kind`, `from_id`, `to_kind`, `to_id`, `relation` (enum), `evidence_id`, `data_json` Example `relation` values: * `artifact_contains_component` * `component_vulnerable_to` * `component_reachable_via` * `vulnerability_overridden_by_vex` * `artifact_scanned_by` * `decision_verified_by` Graph access abstraction: ```csharp public interface IGraphRepository { Task UpsertSbomAsync(CanonicalSbom sbom, EvidenceId evidenceId, CancellationToken ct); Task ApplyVulnerabilityFactsAsync(IEnumerable facts, CancellationToken ct); Task ApplyReachabilityFactsAsync(IEnumerable facts, CancellationToken ct); Task ApplyVexStatementsAsync(IEnumerable vexStatements, CancellationToken ct); Task GetSnapshotAsync(ArtifactRef artifact, CancellationToken ct); } ``` `ArtifactGraphSnapshot` is an in-memory projection used by the policy engine. ### 4.2 Policy engine (`StellaOps.Policy`) Policy lattice (minimal version): ```csharp public enum RiskState { Clean, VulnerableNotReachable, VulnerableReachable, Unknown } public sealed class PolicyEvaluationContext { public ArtifactRef Artifact { get; init; } = default!; public ArtifactGraphSnapshot Snapshot { get; init; } = default!; public IReadOnlyDictionary? Environment { get; init; } } public interface IPolicyEngine { GateDecision Evaluate(PolicyEvaluationContext context); } ``` Default policy logic: 1. For each vulnerability affecting a component in the artifact: * Check for VEX: * If trusted VEX says `not_affected` → ignore. * Check reachability: * If proven reachable → mark as `VulnerableReachable`. * If proven not reachable → `VulnerableNotReachable`. * If unknown → `Unknown`. 2. Aggregate: * If any `Critical/High` in `VulnerableReachable` → `Block`. * Else if any `Critical/High` in `Unknown` → `Warn` and log Unknowns. * Else → `Allow`. ### 4.3 `/vex-gate` endpoint Implementation: * Resolve `ArtifactRef`. * Build `ArtifactGraphSnapshot` using `IGraphRepository.GetSnapshotAsync`. * Call `IPolicyEngine.Evaluate`. * Request `IAuthority.VerifyChainAsync` → `Proof`. * Emit `PolicyVerified` attestation for this decision. * Return `GateDecision` + `Proof`. **Acceptance criteria** * Given a fixture DB snapshot, calling `/vex-gate` twice yields identical decisions & proof IDs. * Policy behavior matches the rule text: * Regression test that modifies severity or reachability → correct decision changes. --- ## 5. Diffs & Unknowns workflow (Phase 5) ### 5.1 Diff engine (`/diff`) Contracts: ```csharp public sealed class DiffRequest { public string Kind { get; init; } = default!; // "sbom-sbom" | "sbom-runtime" public string LeftId { get; init; } = default!; public string RightId { get; init; } = default!; } public sealed class DiffComponentChange { public string Purl { get; init; } = default!; public string ChangeType { get; init; } = default!; // "added" | "removed" | "changed" public string? OldVersion { get; init; } public string? NewVersion { get; init; } } public sealed class DiffResponse { public IReadOnlyList Components { get; init; } = Array.Empty(); } ``` Implementation: * SBOM↔SBOM: compare `CanonicalSbom.Components` by PURL (+ version). * SBOM↔runtime: * Input runtime snapshot (`process maps`, `loaded libs`, etc.) from agents. * Map runtime libs to PURLs. * Determine reachable components from runtime usage → `ReachabilityFact`s into graph. ### 5.2 Unknowns module (`/unknowns`) Data model: ```csharp public enum UnknownState { New, Triage, VendorQuery, Verified, Closed } public sealed class Unknown { public Guid Id { get; init; } public ArtifactRef Artifact { get; init; } = default!; public string Type { get; init; } = default!; // "vuln-mapping", "reachability", "vex-trust" public string Subject { get; init; } = default!; // e.g., "CVE-2024-XXXX / purl:pkg:..." public UnknownState State { get; set; } public DateTimeOffset CreatedAt { get; init; } public DateTimeOffset? SlaDeadline { get; set; } public string? Owner { get; set; } public string EvidenceJson { get; init; } = default!; // serialized proof / edges public string? ResolutionNotes { get; set; } } ``` API: * `GET /unknowns`: filter by state, artifact, owner. * `POST /unknowns`: create manual unknown. * `PATCH /unknowns/{id}`: update state, owner, notes. Integration: * Policy engine: * For any `Unknown` risk state, auto-create Unknown with SLA if not already present. * When Unknown resolves (e.g., vendor VEX added), re-run policy evaluation for affected artifact(s). **Acceptance criteria** * When `VulnerableReachability` is `Unknown`, `/vex-gate` both: * Returns `Warn`. * Creates an Unknown row. * Transitioning Unknown to `Verified` triggers re-evaluation (integration test). --- ## 6. Offline / air‑gapped bundles (Phase 6) **Goal:** Everything works on a single machine with no network. ### 6.1 Bundle format & IO (`StellaOps.Cli` + `StellaOps.WebApi`) Directory structure inside ZIP: ```text /bundle/ feeds/ manifest.json // hashes, timestamps for NVD, OSV, vendor feeds nvd.json osv.json vendor-*.json sboms/ {artifactDigest}.json attestations/ *.jsonl // one DSSE envelope per line proofs/ rekor/ merkle.json policy/ lattice.json // serialized rules / thresholds replay/ inputs.lock // hash & metadata of all of the above ``` Implement: ```csharp public interface IBundleReader { Task ReadAsync(string path, CancellationToken ct); } public interface IBundleWriter { Task WriteAsync(Bundle bundle, string path, CancellationToken ct); } ``` `Bundle` holds strongly-typed representations of the manifest, SBOMs, attestations, proofs, etc. ### 6.2 CLI commands * `stella scan --image registry/app:1.2.3 --out bundle.zip` * Runs scan + sbom locally. * Writes bundle with: * SBOM. * Scan + Sbom attestations. * Feeds manifest. * `stella vex-gate --bundle bundle.zip` * Loads bundle. * Runs policy engine locally. * Prints `Allow/Warn/Block` + proof summary. **Acceptance criteria** * Given the same `bundle.zip`, `stella vex-gate` on different machines produces identical decisions and proof hashes. * `/vex-gate?bundle=/path/to/bundle.zip` in API uses same BundleReader and yields same output as CLI. --- ## 7. Testing & quality plan ### 7.1 Unit tests * Domain & Contracts: * Serialization roundtrip for all DTOs. * Attest: * DSSE encode/decode. * Signature verification with test key pair. * Sbom: * Known `ScanResult` → expected SBOM JSON snapshot. * Policy: * Table-driven tests: * Cases: {severity, reachable, hasVex} → {Allow/Warn/Block}. ### 7.2 Integration tests * Scanner: * Use a tiny test image with known components. * Graph + Policy: * Seed DB with: * 1 artifact, 2 components, 1 vuln, 1 VEX, 1 reachability fact. * Assert that `/vex-gate` returns expected decision. ### 7.3 E2E scenario Single test flow: 1. `POST /scan` → EvidenceId. 2. `POST /sbom` → SBOM + SbomProduced attestation. 3. Load dummy vulnerability feed → `ApplyVulnerabilityFactsAsync`. 4. `POST /vex-gate` → Block (no VEX). 5. Add VEX statement → `ApplyVexStatementsAsync`. 6. `POST /vex-gate` → Allow. Assertions: * All decisions contain `Proof` with non-empty `InputsLock`. * `InputsLock` is identical between runs with unchanged inputs. --- ## 8. Concrete backlog (you can paste into Jira) ### Epic 1 – Foundations * Task: Create solution & project skeleton. * Task: Implement core domain types (`Digest`, `ArtifactRef`, `EvidenceId`, `Proof`). * Task: Implement DSSE envelope + JSON serialization. * Task: Implement basic `IAttestationSigner` with local key pair. * Task: Define `GateDecision` & `VexGateRequest` contracts. ### Epic 2 – Scanner & Sbomer * Task: Implement `IArtifactScanner` + `SyftScanner`. * Task: Implement `/scan` endpoint + attestation. * Task: Implement `ISbomer` & canonical SBOM model. * Task: Implement `/sbom` endpoint + SbomProduced attestation. * Task: Snapshot tests for SBOM determinism. ### Epic 3 – Authority & Trust log * Task: Design `attestations` & `trust_log` tables (EF Core migrations). * Task: Implement `IAuthority.RecordAsync` + `VerifyChainAsync`. * Task: Implement `/attest` endpoints. * Task: Add proof generation (`InputsLock` hashing). ### Epic 4 – Graph & Policy * Task: Create graph schema (`artifacts`, `components`, `vulnerabilities`, `edges`, `vex_statements`). * Task: Implement `IGraphRepository.UpsertSbomAsync`. * Task: Ingest vulnerability feed (NVD/OSV) into graph facts. * Task: Implement minimal `IPolicyEngine` with rules. * Task: Implement `/vex-gate` endpoint. ### Epic 5 – Diff & Unknowns * Task: Implement SBOM↔SBOM diff logic + `/diff`. * Task: Create `unknowns` table + API. * Task: Wire policy engine to auto-create Unknowns. * Task: Add re-evaluation when Unknown state changes. ### Epic 6 – Offline bundles & CLI * Task: Implement `BundleReader` / `BundleWriter`. * Task: Implement `stella scan` and `stella vex-gate`. * Task: Add `?bundle=` parameter support in APIs. --- If you’d like, I can next: * Turn this into actual C# interface files (ready to drop into your repo), or * Produce a JSON OpenAPI sketch for `/scan`, `/sbom`, `/attest`, `/vex-gate`, `/diff`, `/unknowns`.