# VEX Lens Contract **Contract ID:** `CONTRACT-VEX-LENS-005` **Version:** 1.0 **Status:** Published **Last Updated:** 2025-12-05 ## Overview This contract defines the VEX Lens (VexLinkset) data model used to correlate multiple VEX observations for a specific vulnerability and product. The VEX Lens captures provider agreement, disagreements, and calculates consensus confidence. ## Implementation Reference **Source:** `src/Excititor/__Libraries/StellaOps.Excititor.Core/Observations/VexLinkset.cs` ## Data Model ### VexLinkset The core VEX Lens structure correlating observations. ```csharp public sealed record VexLinkset { /// /// Unique identifier: SHA256(tenant|vulnerabilityId|productKey) /// public string LinksetId { get; } /// /// Tenant identifier (normalized to lowercase). /// public string Tenant { get; } /// /// The vulnerability identifier (CVE, GHSA, vendor ID). /// public string VulnerabilityId { get; } /// /// Product key (typically a PURL or CPE). /// public string ProductKey { get; } /// /// Canonical scope metadata for the product key. /// public VexProductScope Scope { get; } /// /// References to observations that contribute to this linkset. /// public ImmutableArray Observations { get; } /// /// Conflict annotations capturing disagreements between providers. /// public ImmutableArray Disagreements { get; } /// /// When this linkset was first created. /// public DateTimeOffset CreatedAt { get; } /// /// When this linkset was last updated. /// public DateTimeOffset UpdatedAt { get; } } ``` ### JSON Representation ```json { "linkset_id": "sha256:abc123...", "tenant": "default", "vulnerability_id": "CVE-2024-1234", "product_key": "pkg:npm/lodash@4.17.20", "scope": { "ecosystem": "npm", "namespace": null, "name": "lodash", "version": "4.17.20" }, "observations": [ { "observation_id": "obs-001", "provider_id": "github", "status": "affected", "confidence": 0.9 }, { "observation_id": "obs-002", "provider_id": "redhat", "status": "not_affected", "confidence": 0.85 } ], "disagreements": [ { "provider_id": "github", "status": "affected", "justification": null, "confidence": 0.9 }, { "provider_id": "redhat", "status": "not_affected", "justification": "vulnerable_code_not_in_execute_path", "confidence": 0.85 } ], "created_at": "2025-12-05T10:00:00Z", "updated_at": "2025-12-05T10:00:00Z" } ``` ### VexLinksetObservationRefModel Reference to an observation contributing to the linkset. ```json { "observation_id": "obs-001", "provider_id": "github", "status": "affected", "confidence": 0.9 } ``` | Field | Type | Description | |-------|------|-------------| | `observation_id` | string | Unique observation identifier | | `provider_id` | string | VEX provider identifier | | `status` | string | VEX status claim | | `confidence` | double? | Optional confidence [0.0-1.0] | ### VexObservationDisagreement Captures conflict between providers. ```json { "provider_id": "github", "status": "affected", "justification": null, "confidence": 0.9 } ``` ### VEX Status Values | Status | Description | |--------|-------------| | `affected` | Product is affected by vulnerability | | `not_affected` | Product is not affected | | `fixed` | Vulnerability has been fixed | | `under_investigation` | Status is being determined | ### VEX Justification Codes When `status` is `not_affected`, justification may include: | Code | Description | |------|-------------| | `component_not_present` | Vulnerable component not present | | `vulnerable_code_not_present` | Vulnerable code not present | | `vulnerable_code_not_in_execute_path` | Code present but not reachable | | `vulnerable_code_cannot_be_controlled_by_adversary` | Not exploitable | | `inline_mitigations_already_exist` | Mitigations in place | ## Confidence Levels ### VexLinksetConfidence Computed confidence based on linkset state. | Level | Conditions | |-------|------------| | `Low` | Conflicts exist, or < 1 observation, or multiple distinct statuses | | `Medium` | Single provider, or consistent observations | | `High` | 2+ providers agree on status | ### Confidence Calculation ```csharp public VexLinksetConfidence Confidence { get { if (HasConflicts) return VexLinksetConfidence.Low; if (Observations.Length == 0) return VexLinksetConfidence.Low; if (Statuses.Count > 1) return VexLinksetConfidence.Low; if (ProviderIds.Count >= 2) return VexLinksetConfidence.High; return VexLinksetConfidence.Medium; } } ``` ## Linkset ID Generation Deterministic ID from key components: ```csharp public static string CreateLinksetId(string tenant, string vulnerabilityId, string productKey) { var input = $"{tenant.ToLowerInvariant()}|{vulnerabilityId}|{productKey}"; var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input)); return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}"; } ``` ## API Endpoints ### Resolve VEX for Finding ``` POST /excititor/resolve Content-Type: application/json { "tenant_id": "default", "queries": [ { "vulnerability_id": "CVE-2024-1234", "product_key": "pkg:npm/lodash@4.17.20" } ] } Response: 200 OK { "results": [ { "linkset_id": "sha256:...", "vulnerability_id": "CVE-2024-1234", "product_key": "pkg:npm/lodash@4.17.20", "rollup_status": "affected", "confidence": "medium", "has_conflicts": false, "provider_count": 1 } ] } ``` ### Get Linkset Details ``` GET /excititor/linksets/{linkset_id} Response: 200 OK { "linkset_id": "sha256:...", "vulnerability_id": "CVE-2024-1234", "observations": [...], "disagreements": [...], "confidence": "low" } ``` ## Consensus Algorithm The consensus rollup algorithm: 1. **Filter:** Remove invalid statements by signature policy 2. **Score:** `score = weight(provider) × freshnessFactor(lastObserved)` 3. **Aggregate:** `W(status) = Σ score` per status 4. **Pick:** `rollupStatus = argmax_status W(status)` 5. **Tie-breakers:** - Higher max single provider score - More recent `lastObserved` - Lexicographic order (fixed > not_affected > under_investigation > affected) ### Provider Weights | Provider Type | Default Weight | |---------------|----------------| | Vendor | 1.0 | | Distribution | 0.9 | | Platform | 0.7 | | Attestation | 0.6 | | Hub | 0.5 | ### Freshness Factor ``` freshnessFactor = clamp(0.8, 1.0 - (age_days / 30), 1.0) ``` ## Determinism Guarantees 1. **Stable ID:** LinksetId is deterministic from (tenant, vulnId, productKey) 2. **Sorted observations:** Observations sorted by observationId 3. **Sorted disagreements:** Disagreements sorted by (providerId, status) 4. **Immutable records:** Linksets are immutable; updates create new versions ## Unblocks This contract unblocks the following tasks: - CONCELIER-VEXLENS-30-001 - EXCITITOR-VEXLENS-30-001 ## Related Contracts - [Advisory Key Contract](./advisory-key.md) - Vulnerability ID canonicalization - [Risk Scoring Contract](./risk-scoring.md) - VEX evidence for scoring