Here’s a practical, first‑time‑friendly guide to using VEX in Stella Ops, plus a concrete .NET pattern you can drop in today. --- # VEX in a nutshell * **VEX (Vulnerability Exploitability eXchange)**: a small JSON document that says whether specific CVEs *actually* affect a product/version. * **OpenVEX**: SBOM‑agnostic; references products/components directly (URIs, PURLs, hashes). Great for canonical internal models. * **CycloneDX VEX / SPDX VEX**: tie VEX statements closely to a specific SBOM instance (component BOM ref IDs). Great when the BOM is your source of truth. **Our strategy:** * **Store VEX separately** from SBOMs (deterministic, easier air‑gap bundling). * **Link by strong references** (PURLs + content hashes + optional SBOM component IDs). * **Translate on ingest** between OpenVEX ↔ CycloneDX VEX as needed so downstream tools stay happy. --- # Translation model (OpenVEX ↔ CycloneDX VEX) 1. **Identity mapping** * Prefer **PURL** for packages; fallback to **SHA256 (or SHA512)** of artifact; optionally include **SBOM `bom-ref`** if known. 2. **Product scope** * OpenVEX “product” → CycloneDX `affects` with `bom-ref` (if available) or a synthetic ref derived from PURL/hash. 3. **Status mapping** * `affected | not_affected | under_investigation | fixed` map 1:1. * Keep **timestamps**, **justification**, **impact statement**, and **origin**. 4. **Evidence** * Preserve links to advisories, commits, tests; attach as CycloneDX `analysis/evidence` notes (or OpenVEX `metadata/notes`). **Collision rules (deterministic):** * New statement wins if: * Newer `timestamp` **and** * Higher **provenance trust** (signed by vendor/Authority) or equal with a lexicographic tiebreak (issuer keyID). --- # Storage model (MongoDB‑friendly) * **Collections** * `vex.documents` – one doc per VEX file (OpenVEX or CycloneDX VEX). * `vex.statements` – *flattened*, one per (product/component, vuln). * `artifacts` – canonical component index (PURL, hashes, optional SBOM refs). * **Reference keys** * `artifactKey = purl || sha256 || (groupId:name:version for .NET/NuGet)` * `vulnKey = cveId || ghsaId || internalId` * **Deterministic IDs** * `_id = sha256(canonicalize(statement-json-without-signature))` * **Signatures** * Keep DSSE/Sigstore envelopes in `vex.documents.signatures[]` for audit & replay. --- # Air‑gap bundling Package **SBOMs + VEX + artifacts index + trust roots** as a single tarball: ``` /bundle/ sboms/*.json vex/*.json # OpenVEX & CycloneDX VEX allowed index/artifacts.jsonl # purl, hashes, bom-ref map trust/rekor.merkle.roots trust/fulcio.certs.pem trust/keys/*.pub manifest.json # content list + sha256 + issuedAt ``` * **Deterministic replay:** re‑ingest is pure function of bundle bytes → identical DB state. --- # .NET 10 implementation (C#) – deterministic ingestion ### Core models ```csharp public record ArtifactRef( string? Purl, string? Sha256, string? BomRef); public enum VexStatus { Affected, NotAffected, UnderInvestigation, Fixed } public record VexStatement( string StatementId, // sha256 of canonical payload ArtifactRef Artifact, string VulnId, // e.g., "CVE-2024-1234" VexStatus Status, string? Justification, string? ImpactStatement, DateTimeOffset Timestamp, string IssuerKeyId, // from DSSE/Signing int ProvenanceScore); // Authority policy ``` ### Canonicalizer (stable order, no env fields) ```csharp static string Canonicalize(VexStatement s) { var payload = new { artifact = new { s.Artifact.Purl, s.Artifact.Sha256, s.Artifact.BomRef }, vulnId = s.VulnId, status = s.Status.ToString(), justification = s.Justification, impact = s.ImpactStatement, timestamp = s.Timestamp.UtcDateTime }; // Use System.Text.Json with deterministic ordering var opts = new System.Text.Json.JsonSerializerOptions { WriteIndented = false }; string json = System.Text.Json.JsonSerializer.Serialize(payload, opts); // Normalize unicode + newline json = json.Normalize(NormalizationForm.FormKC).Replace("\r\n","\n"); return json; } static string Sha256(string s) { using var sha = System.Security.Cryptography.SHA256.Create(); var bytes = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(s)); return Convert.ToHexString(bytes).ToLowerInvariant(); } ``` ### Ingest pipeline ```csharp public sealed class VexIngestor { readonly IVexParser _parser; // OpenVEX & CycloneDX adapters readonly IArtifactIndex _artifacts; readonly IVexRepo _repo; // Mongo-backed readonly IPolicy _policy; // tie-break rules public async Task IngestAsync(Stream vexJson, SignatureEnvelope? sig) { var doc = await _parser.ParseAsync(vexJson); // yields normalized statements var issuer = sig?.KeyId ?? "unknown"; foreach (var st in doc.Statements) { var canon = Canonicalize(st); var id = Sha256(canon); var withMeta = st with { StatementId = id, IssuerKeyId = issuer, ProvenanceScore = _policy.Score(sig, st) }; // Upsert artifact (purl/hash/bomRef) await _artifacts.UpsertAsync(withMeta.Artifact); // Deterministic merge var existing = await _repo.GetAsync(id) ?? await _repo.FindByKeysAsync(withMeta.Artifact, st.VulnId); if (existing is null || _policy.IsNewerAndStronger(existing, withMeta)) await _repo.UpsertAsync(withMeta); } if (sig is not null) await _repo.AttachSignatureAsync(doc.DocumentId, sig); } } ``` ### Parsers (adapters) * `OpenVexParser` – reads OpenVEX; emits `VexStatement` with `ArtifactRef(PURL/hash)` * `CycloneDxVexParser` – resolves `bom-ref` → look up PURL/hash via `IArtifactIndex` (if SBOM present); if not, store bom‑ref and mark artifact unresolved for later backfill. --- # Why this works for Stella Ops * **SBOM‑agnostic core** (OpenVEX‑first) maps cleanly to your MongoDB canonical stores and `.NET 10` services. * **SBOM‑aware edges** (CycloneDX VEX) are still supported via adapters and `bom-ref` backfill. * **Deterministic everything**: canonical JSON → SHA256 IDs → reproducible merges → perfect for audits and offline environments. * **Air‑gap ready**: single bundle with trust roots, replayable on any node. --- # Next steps (plug‑and‑play) 1. Implement the two parsers (`OpenVexParser`, `CycloneDxVexParser`). 2. Add the repo/index interfaces to your `StellaOps.Vexer` service: * `IVexRepo` (Mongo collections `vex.documents`, `vex.statements`) * `IArtifactIndex` (your canonical PURL/hash map) 3. Wire `Policy` to Authority to score signatures and apply tie‑breaks. 4. Add a `bundle ingest` CLI: `vexer ingest /bundle/manifest.json`. 5. Expose GraphQL (HotChocolate) queries: * `vexStatements(artifactKey, vulnId)`, `vexStatus(artifactKey)`, `evidence(...)`. If you want, I can generate the exact Mongo schemas, HotChocolate types, and a minimal test bundle to validate the ingest end‑to‑end. Below is a complete, developer-ready implementation plan for the **VEX ingestion, translation, canonicalization, storage, and merge-policy pipeline** inside **Stella Ops.Vexer**, aligned with your architecture, deterministic requirements, MongoDB model, DSSE/Authority workflow, and `.NET 10` standards. This is structured so an average developer can follow it step-by-step without ambiguity. It is broken into phases, each with clear tasks, acceptance criteria, failure modes, interfaces, and code pointers. --- # Stella Ops.Vexer ## Full Implementation Plan (Developer-Executable) --- # 1. Core Objectives Develop a deterministic, replayable, SBOM-agnostic but SBOM-compatible VEX subsystem supporting: * OpenVEX and CycloneDX VEX ingestion. * Canonicalization → SHA-256 identity. * Cross-linking to artifacts (purl, hash, bom-ref). * Merge policies driven by Authority trust/lattice rules. * Complete offline reproducibility. * MongoDB canonical storage. * Exposed through gRPC/REST/GraphQL. --- # 2. Module Structure (to be implemented) ``` src/StellaOps.Vexer/ Application/ Commands/ Queries/ Ingest/ Translation/ Merge/ Policies/ Domain/ Entities/ ValueObjects/ Services/ Infrastructure/ Mongo/ AuthorityClient/ Hashing/ Signature/ BlobStore/ Presentation/ GraphQL/ REST/ gRPC/ ``` Every subfolder must compile in strict mode (treat warnings as errors). --- # 3. Data Model (MongoDB) ## 3.1 `vex.statements` collection Document schema: ```json { "_id": "sha256(canonical-json)", "artifact": { "purl": "pkg:nuget/... or null", "sha256": "hex or null", "bomRef": "optional ref", "resolved": true | false }, "vulnId": "CVE-XXXX-YYYY", "status": "affected | not_affected | under_investigation | fixed", "justification": "...", "impact": "...", "timestamp": "2024-01-01T12:34:56Z", "issuerKeyId": "FULCIO-KEY-ID", "provenanceScore": 0–100, "documentId": "UUID of vex.documents entry", "sourceFormat": "openvex|cyclonedx", "createdAt": "...", "updatedAt": "..." } ``` ## 3.2 `vex.documents` collection ``` { "_id": "", "format": "openvex|cyclonedx", "rawBlobId": "", "signatures": [ { "type": "dsse", "verified": true, "issuerKeyId": "F-123...", "timestamp": "...", "bundleEvidence": {...} } ], "ingestedAt": "...", "statementIds": ["sha256-1", "sha256-2", ...] } ``` --- # 4. Components to Implement ## 4.1 Parsing Layer ### Interfaces ```csharp public interface IVexParser { ValueTask ParseAsync(Stream jsonStream); } public sealed record ParsedVexDocument( string DocumentId, string Format, IReadOnlyList Statements); ``` ### Tasks 1. Implement `OpenVexParser`. * Use System.Text.Json source generators. * Validate OpenVEX schema version. * Extract product → component mapping. * Map to internal `ArtifactRef`. 2. Implement `CycloneDxVexParser`. * Support 1.5+ “vex” extension. * bom-ref resolution through `IArtifactIndex`. * Mark unresolved `bom-ref` but store them. ### Acceptance Criteria * Both parsers produce identical internal representation of statements. * Unknown fields must not corrupt canonicalization. * 100% deterministic mapping for same input. --- ## 4.2 Canonicalizer Implement deterministic ordering, UTF-8 normalization, stable JSON. ### Tasks 1. Create `Canonicalizer` class. 2. Apply: * Property order: artifact, vulnId, status, justification, impact, timestamp. * Remove optional metadata (issuerKeyId, provenance). * Normalize Unicode → NFKC. * Replace CRLF → LF. 3. Generate SHA-256. ### Interface ```csharp public interface IVexCanonicalizer { string Canonicalize(VexStatement s); string ComputeId(string canonicalJson); } ``` ### Acceptance Criteria * Hash identical on all OS, time, locale, machines. * Replaying the same bundle yields same `_id`. --- ## 4.3 Authority / Signature Verification ### Tasks 1. Implement DSSE envelope reader. 2. Integrate Authority client: * Verify certificate chain (Fulcio/GOST/eIDAS etc). * Obtain trust lattice score. * Produce `ProvenanceScore`: int. ### Interface ```csharp public interface ISignatureVerifier { ValueTask VerifyAsync(Stream payload, Stream envelope); } ``` ### Acceptance Criteria * If verification fails → Vexer stores document but flags signature invalid. * Scores map to priority in merge policy. --- ## 4.4 Merge Policies ### Implement Default Policy 1. Newer timestamp wins. 2. If timestamps equal: * Higher provenance score wins. * If both equal, lexicographically smaller issuerKeyId wins. ### Interface ```csharp public interface IVexMergePolicy { bool ShouldReplace(VexStatement existing, VexStatement incoming); } ``` ### Acceptance Criteria * Merge decisions reproducible. * Deterministic ordering even when values equal. --- ## 4.5 Ingestion Pipeline ### Steps 1. Accept `multipart/form-data` or referenced blob ID. 2. Parse via correct parser. 3. Verify signature (optional). 4. For each statement: * Canonicalize. * Compute `_id`. * Upsert artifact into `artifacts` (via `IArtifactIndex`). * Resolve bom-ref (if CycloneDX). * Existing statement? Apply merge policy. * Insert or update. 5. Create `vex.documents` entry. ### Class `VexIngestService` ### Required Methods ```csharp public Task IngestAsync(VexIngestRequest request); ``` ### Acceptance Tests * Idempotent: ingesting same VEX repeated → DB unchanged. * Deterministic under concurrency. * Air-gap replay produces identical DB state. --- ## 4.6 Translation Layer ### Implement two converters: * `OpenVexToCycloneDxTranslator` * `CycloneDxToOpenVexTranslator` ### Rules * Prefer PURL → hash → synthetic bom-ref. * Single VEX statement → one CycloneDX “analysis” entry. * Preserve justification, impact, notes. ### Acceptance Criteria * Round-trip OpenVEX → CycloneDX → OpenVEX produces equal canonical hashes (except format markers). --- ## 4.7 Artifact Index Backfill ### Reason CycloneDX VEX may refer to bom-refs not yet known at ingestion. ### Tasks 1. Store unresolved artifacts. 2. Create background `BackfillWorker`: * Watches `sboms.documents` ingestion events. * Matches bom-refs. * Updates statements with resolved PURL/hashes. * Recomputes canonical JSON + SHA-256 (new version stored as new ID). 3. Marks old unresolved statement as superseded. ### Acceptance Criteria * Backfilling is monotonic: no overwriting original. * Deterministic after backfill: same SBOM yields same final ID. --- ## 4.8 Bundle Ingestion (Air-Gap Mode) ### Structure ``` bundle/ sboms/*.json vex/*.json index/artifacts.jsonl trust/* manifest.json ``` ### Tasks 1. Implement `BundleIngestService`. 2. Stages: * Validate manifest + hashes. * Import trust roots (local only). * Ingest SBOMs first. * Ingest VEX documents. 3. Reproduce same IDs on all nodes. ### Acceptance Criteria * Byte-identical bundle → byte-identical DB. * Works offline completely. --- # 5. Interfaces for GraphQL/REST/gRPC Expose: ## Queries * `vexStatement(id)` * `vexStatementsByArtifact(purl/hash)` * `vexStatus(purl)` → latest merged status * `vexDocument(id)` * `affectedComponents(vulnId)` ## Mutations * `ingestVexDocument` * `translateVex(format)` * `exportVexDocument(id, targetFormat)` * `replayBundle(bundleId)` All responses must include deterministic IDs. --- # 6. Detailed Developer Tasks by Sprint ## Sprint 1: Foundation 1. Create solution structure. 2. Add Mongo DB contexts. 3. Implement data entities. 4. Implement hashing + canonicalizer. 5. Implement IVexParser interface. ## Sprint 2: Parsers 1. Implement OpenVexParser. 2. Implement CycloneDxParser. 3. Develop strong unit tests for JSON normalization. ## Sprint 3: Signature & Authority 1. DSSE envelope reader. 2. Call Authority to verify signatures. 3. Produce provenance scores. ## Sprint 4: Merge Policy Engine 1. Implement deterministic lattice merge. 2. Unit tests: 20+ collision scenarios. ## Sprint 5: Ingestion Pipeline 1. Implement ingest service end-to-end. 2. Insert/update logic. 3. Add GraphQL endpoints. ## Sprint 6: Translation Layer 1. OpenVEX↔CycloneDX converter. 2. Tests for round-trip. ## Sprint 7: Backfill System 1. Bom-ref resolver worker. 2. Rehashing logic for updated artifacts. 3. Events linking SBOM ingestion to backfill. ## Sprint 8: Air-Gap Bundle 1. BundleIngestService. 2. Manifest verification. 3. Trust root local loading. ## Sprint 9: Hardening 1. Fuzz parsers. 2. Deterministic stress tests. 3. Concurrency validation. 4. Storage compaction. --- # 7. Failure Handling Matrix | Failure | Action | Logged? | Retries | | ------------------- | -------------------------------------- | ------- | ------- | | Invalid JSON | Reject document | Yes | 0 | | Invalid schema | Reject | Yes | 0 | | Signature invalid | Store document, mark signature invalid | Yes | 0 | | Artifact unresolved | Store unresolved, enqueue backfill | Yes | 3 | | Merge conflict | Apply policy | Yes | 0 | | Canonical mismatch | Hard fail | Yes | 0 | --- # 8. Developer Unit Test Checklist ### must have tests for: * Canonicalization stability (100 samples). * Identical input twice → identical `_id`. * Parsing OpenVEX with multi-product definitions. * Parsing CycloneDX with missing bom-refs. * Merge policy tie-breakers. * Air-gap replay reproducibility. * Translation equivalence. --- # 9. Deliverables for Developers They must produce: 1. Interfaces + DTOs + document schemas. 2. Canonicalizer with 100% deterministic output. 3. Two production-grade parsers. 4. Signature verification pipeline. 5. Merge policies aligned with Authority trust model. 6. End-to-end ingestion service. 7. Translation layer. 8. Backfill worker. 9. Air-gap bundle script + service. 10. GraphQL APIs. --- If you want, I can next produce: * A full **developer handbook** (60–90 pages). * Full **technical architecture ADRs**. * A concrete **scaffold** with compiles-clean `.NET 10` project. * Complete **test suite specification**. * A **README.md** for new joiners.