7.4 KiB
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)
{
"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)
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.