# 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