Here's a compact, practical way to make VEX trust decisions explainable and replayable across vendor, distro, and internal sources—without adding human-in-the-loop friction. --- # VEX Trust Lattice (compact) **Goal:** turn messy/contradictory VEX claims into a single, signed, reproducible verdict with a numeric confidence and an audit trail. ## 1) Trust vector per source Each VEX source S gets a 3‑component trust vector scored in [0..1]: * **Provenance (P):** cryptographic & process integrity * 1.00 = DSSE‑signed, timestamped, Rekor/Git tag anchored, org DKIM/Sigstore OIDC, key in allow‑list, rotation policy OK * 0.75 = DSSE‑signed + public key known, but no transparency log * 0.40 = unsigned but retrieved via authenticated, immutable artifact repo * 0.10 = opaque/CSV/email/manual import * **Coverage (C):** how well the statement's scope maps to your asset * 1.00 = exact package + version/build digest + feature/flag context matched * 0.75 = exact pkg + version range matched; partial feature context * 0.50 = product‑level only; maps via CPE/PURL family * 0.25 = family‑level heuristics; no version proof * **Replayability (R):** can we deterministically re‑derive the claim? * 1.00 = all inputs pinned (feeds, SBOM hash, ruleset hash, lattice version); replays byte‑identical * 0.60 = inputs mostly pinned; non‑deterministic ordering tolerated but stable outcome * 0.20 = ephemeral APIs; no snapshot **BaseTrust(S) = wP·P + wC·C + wR·R** (defaults: wP=0.45, wC=0.35, wR=0.20; tunable per policy). ## 2) Claim strength & freshness Every individual VEX claim `S asserts X` carries multipliers: * **Strength (M):** *not*‑affected‑because‑{reason} * Exploitability analysis + reachability proof subgraph provided → 1.00 * Config/feature‑flag reason with evidence → 0.80 * Vendor blanket statement → 0.60 * "Under investigation" → 0.40 * **Freshness (F):** time‑decay curve; default half‑life 90 days * `F = exp(- ln(2) · age_days / 90)`; floor at 0.35 unless revoked **ClaimScore = BaseTrust(S) · M · F.** ## 3) Lattice ordering & merge Define a **partial order** on claims by (scope specificity, ClaimScore). More specific scope wins ties. For a given CVE×Asset, gather all claims `{Ci}`: * If any **revocation/contradiction** exists, keep both and trigger **conflict mode**: require replay proof; otherwise down‑weight older/weaker by Δ=0.25. * Final **verdict** chosen by **argmax(ClaimScore)** after conflict adjustments. Return tuple: ``` Verdict = { status: {affected|not_affected|under_investigation|fixed}, confidence: ClaimScore*, expl: list of (source, reason, P/C/R, M, F), evidence_refs: [attestations, SBOM hash, reachability subgraph id], policy_hash, lattice_version } ``` ## 4) Policy hooks (explainable gates) * **Minimum confidence by environment:** e.g., prod requires ≥0.75 to accept "not_affected". * **Unknowns budget:** fail if (#unknown deps > N) OR (Σ(1–ClaimScore) over unknowns > T). * **Source quotas:** cap influence from any single vendor at 60% unless a second independent source supports within Δ=0.1. * **Reason allow‑list:** forbid blanket vendor claims for criticals unless reachability proof exists. ## 5) Deterministic replay To guarantee "same inputs → same verdict": * Pin: SBOM digest(s), vuln feed snapshot ids, VEX document digests, reachability graph ids, policy file, lattice version, clock cutoff. * Sort: stable topological order on inputs (by `(issuer_did, statement_digest)`). * Serialize verdict + inputs into a **Verdict Manifest** (JSON/CBOR) and sign (DSSE). * Store in **Authority** with index: `(asset_digest, CVE, policy_hash, lattice_version)`. ## 6) Minimal data model (for Vexer/Policy Engine) ```json { "source": { "id": "did:web:vendor.example", "provenance": {"sig_type":"dsse","rekor_log_id":"...","key_alias":"vendor_k1"}, "provenance_score": 0.90, "coverage_score": 0.75, "replay_score": 0.60, "weights": {"wP":0.45,"wC":0.35,"wR":0.20} }, "claim": { "scope": {"purl":"pkg:rpm/openssl@3.0.12-5","digest":"sha256:...","features":{"fips":true}}, "cve": "CVE-2025-12345", "status": "not_affected", "reason": "feature_flag_off", "strength": 0.80, "issued_at": "2025-11-28T10:12:00Z", "evidence": {"reach_subgraph_id":"reg:subg/abcd","attestation":"sha256:..."} }, "policy": {"min_confidence_prod":0.75,"unknown_budget":5,"require_reachability_for_criticals":true}, "lattice_version": "1.2.0" } ``` ## 7) Deterministic evaluation (C# sketch) ```csharp public record TrustWeights(double wP=0.45, double wC=0.35, double wR=0.20); double BaseTrust(double P, double C, double R, TrustWeights W) => W.wP*P + W.wC*C + W.wR*R; double Freshness(DateTime issuedAt, DateTime cutoff, double halfLifeDays=90, double floor=0.35) { var age = (cutoff - issuedAt).TotalDays; var f = Math.Exp(-Math.Log(2) * age / halfLifeDays); return Math.Max(f, floor); } double ClaimScore(Source s, Claim c, TrustWeights W, DateTime cutoffUtc) { var baseTrust = BaseTrust(s.P, s.C, s.R, W); var freshness = Freshness(c.IssuedAt, cutoffUtc); return baseTrust * c.Strength * freshness; } // Merge: pick best score; apply conflict penalty if contradictory present Verdict Merge(IEnumerable<(Source S, Claim C)> claims, Policy policy, DateTime cutoffUtc) { var scored = claims.Select(t => new { t.S, t.C, Score = ClaimScore(t.S, t.C, t.S.Weights, cutoffUtc) }).ToList(); bool contradictory = scored.Select(x=>x.C.Status).Distinct().Count() > 1; if (contradictory) { scored = scored.Select(x => new { x.S, x.C, Score = x.Score * 0.75 // conflict penalty }).ToList(); } var winner = scored.OrderByDescending(x => (x.C.Scope.Specificity, x.Score)).First(); if (policy.RequireReachForCriticals && winner.C.IsCritical && !winner.C.HasReachabilityProof) return Verdict.FailGate("No reachability proof for critical"); if (policy.MinConfidenceProd.HasValue && winner.Score < policy.MinConfidenceProd) return Verdict.FailGate("Below minimum confidence"); return Verdict.Accept(winner.C.Status, winner.Score, AuditTrail.From(scored)); } ``` ## 8) UI: "Trust Algebra" panel (1 screen, no new page) * **Header:** CVE × Asset digest → final status + confidence meter. * **Stacked bars:** P/C/R contributions for the winning claim. * **Claim table:** source, status, reason, P/C/R, strength, freshness, ClaimScore; toggle "show conflicts". * **Policy chips:** which gates applied; click to open policy YAML/JSON (read‑only if in replay). * **Replay button:** "Reproduce verdict" → emits a signed **Verdict Manifest** and logs proof ids. ## 9) Defaults for source classes * **Vendor:** P=0.9, C=0.7 (often coarse), R=0.6 * **Distro:** P=0.8, C=0.85 (build‑aware), R=0.6 * **Internal:** P=0.85 (org‑signed), C=0.95 (exact SBOM+reach), R=0.9 Tune per issuer using rolling calibration: compare past ClaimScores vs. post‑mortem truth; adjust via small learning rate (±0.02/epoch) under a signed **calibration manifest** (also replayable). --- If you want, I can drop this into your Stella Ops modules today as: * **Vexer:** trust‑vector store + claim normalizer * **Policy Engine:** lattice evaluator + gates * **Authority:** verdict manifest signer/indexer * **UI:** single "Trust Algebra" panel wired to evidence ids Say the word and I'll generate the concrete JSON schemas, C# interfaces, and a seed policy file.