VEX Normalization Contract v1.0.0
Status: APPROVED Version: 1.0.0 Effective: 2025-12-19 Owner: VEX Lens Guild Sprint: SPRINT_0129_0001_0001 (unblocks VEXLENS-30-001 through 30-011)
1. Purpose
This contract defines the normalization rules for VEX (Vulnerability Exploitability eXchange) documents from multiple sources into a canonical StellaOps internal representation.
2. Supported Input Formats
| Format | Version | Parser |
|---|---|---|
| OpenVEX | 0.2.0+ | OpenVexParser |
| CycloneDX VEX | 1.5+ | CycloneDxVexParser |
| CSAF VEX | 2.0 | CsafVexParser |
3. Canonical Representation
3.1 NormalizedVexStatement
public sealed record NormalizedVexStatement
{
/// <summary>Unique statement identifier (deterministic hash).</summary>
public required string StatementId { get; init; }
/// <summary>CVE or vulnerability identifier.</summary>
public required string VulnerabilityId { get; init; }
/// <summary>Normalized status (not_affected, affected, fixed, under_investigation).</summary>
public required VexStatus Status { get; init; }
/// <summary>Justification code (when status = not_affected).</summary>
public VexJustification? Justification { get; init; }
/// <summary>Human-readable impact statement.</summary>
public string? ImpactStatement { get; init; }
/// <summary>Action statement for remediation.</summary>
public string? ActionStatement { get; init; }
/// <summary>Products affected by this statement.</summary>
public required ImmutableArray<ProductIdentifier> Products { get; init; }
/// <summary>Source document metadata.</summary>
public required VexSourceMetadata Source { get; init; }
/// <summary>Statement timestamp (UTC, ISO-8601).</summary>
public required DateTimeOffset Timestamp { get; init; }
/// <summary>Issuer information.</summary>
public required IssuerInfo Issuer { get; init; }
}
3.2 VexStatus Enum
public enum VexStatus
{
/// <summary>Product is not affected by the vulnerability.</summary>
NotAffected = 0,
/// <summary>Product is affected and vulnerable.</summary>
Affected = 1,
/// <summary>Product was affected but is now fixed.</summary>
Fixed = 2,
/// <summary>Impact is being investigated.</summary>
UnderInvestigation = 3
}
3.3 VexJustification Enum
public enum VexJustification
{
/// <summary>Component is not present.</summary>
ComponentNotPresent = 0,
/// <summary>Vulnerable code is not present.</summary>
VulnerableCodeNotPresent = 1,
/// <summary>Vulnerable code is not in execute path.</summary>
VulnerableCodeNotInExecutePath = 2,
/// <summary>Vulnerable code cannot be controlled by adversary.</summary>
VulnerableCodeCannotBeControlledByAdversary = 3,
/// <summary>Inline mitigations exist.</summary>
InlineMitigationsAlreadyExist = 4
}
4. Normalization Rules
4.1 Status Mapping
| Source Format | Source Value | Normalized Status |
|---|---|---|
| OpenVEX | not_affected | NotAffected |
| OpenVEX | affected | Affected |
| OpenVEX | fixed | Fixed |
| OpenVEX | under_investigation | UnderInvestigation |
| CycloneDX | notAffected | NotAffected |
| CycloneDX | affected | Affected |
| CycloneDX | resolved | Fixed |
| CycloneDX | inTriage | UnderInvestigation |
| CSAF | not_affected | NotAffected |
| CSAF | known_affected | Affected |
| CSAF | fixed | Fixed |
| CSAF | under_investigation | UnderInvestigation |
4.2 Justification Mapping
| Source Format | Source Value | Normalized Justification |
|---|---|---|
| OpenVEX | component_not_present | ComponentNotPresent |
| OpenVEX | vulnerable_code_not_present | VulnerableCodeNotPresent |
| OpenVEX | vulnerable_code_not_in_execute_path | VulnerableCodeNotInExecutePath |
| OpenVEX | vulnerable_code_cannot_be_controlled_by_adversary | VulnerableCodeCannotBeControlledByAdversary |
| OpenVEX | inline_mitigations_already_exist | InlineMitigationsAlreadyExist |
| CycloneDX | Same as OpenVEX (camelCase) | Same mapping |
| CSAF | component_not_present | ComponentNotPresent |
| CSAF | vulnerable_code_not_present | VulnerableCodeNotPresent |
| CSAF | vulnerable_code_not_in_execute_path | VulnerableCodeNotInExecutePath |
| CSAF | vulnerable_code_cannot_be_controlled_by_adversary | VulnerableCodeCannotBeControlledByAdversary |
| CSAF | inline_mitigations_already_exist | InlineMitigationsAlreadyExist |
4.3 Product Identifier Normalization
Products are normalized to PURL (Package URL) format:
pkg:{ecosystem}/{namespace}/{name}@{version}?{qualifiers}#{subpath}
| Source | Extraction Method |
|---|---|
| OpenVEX | Direct from product.id if PURL, else construct from product.identifiers |
| CycloneDX | From bom-ref PURL or construct from component.purl |
| CSAF | From product_id → product_identification_helper.purl |
4.4 Statement ID Generation
Statement IDs are deterministic SHA-256 hashes:
public static string GenerateStatementId(
string vulnerabilityId,
VexStatus status,
IEnumerable<string> productPurls,
string issuerId,
DateTimeOffset timestamp)
{
var input = $"{vulnerabilityId}|{status}|{string.Join(",", productPurls.OrderBy(p => p))}|{issuerId}|{timestamp:O}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"stmt:{Convert.ToHexString(hash).ToLowerInvariant()[..32]}";
}
5. Issuer Directory Integration
Normalized statements include issuer information from the Issuer Directory:
public sealed record IssuerInfo
{
/// <summary>Issuer identifier (e.g., "vendor:redhat", "vendor:canonical").</summary>
public required string IssuerId { get; init; }
/// <summary>Display name.</summary>
public required string DisplayName { get; init; }
/// <summary>Trust tier (authoritative, trusted, community, unknown).</summary>
public required IssuerTrustTier TrustTier { get; init; }
/// <summary>Issuer's signing key fingerprints (if signed).</summary>
public ImmutableArray<string> SigningKeyFingerprints { get; init; }
}
public enum IssuerTrustTier
{
Authoritative = 0, // Vendor/maintainer of the product
Trusted = 1, // Known security research org
Community = 2, // Community contributor
Unknown = 3 // Unverified source
}
6. API Governance
6.1 Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/v1/vex/statements | GET | Query normalized statements |
/api/v1/vex/statements/{id} | GET | Get specific statement |
/api/v1/vex/normalize | POST | Normalize a VEX document |
/api/v1/vex/issuers | GET | List known issuers |
/api/v1/vex/issuers/{id} | GET | Get issuer details |
6.2 Query Parameters
| Parameter | Type | Description |
|---|---|---|
vulnerability | string | Filter by CVE/vulnerability ID |
product | string | Filter by PURL (URL-encoded) |
status | enum | Filter by VEX status |
issuer | string | Filter by issuer ID |
since | datetime | Statements after timestamp |
limit | int | Max results (default: 100, max: 1000) |
cursor | string | Pagination cursor |
6.3 Response Format
{
"statements": [
{
"statementId": "stmt:a1b2c3d4e5f6...",
"vulnerabilityId": "CVE-2024-1234",
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"products": ["pkg:npm/lodash@4.17.21"],
"issuer": {
"issuerId": "vendor:lodash",
"displayName": "Lodash Maintainers",
"trustTier": "authoritative"
},
"timestamp": "2024-12-19T10:30:00Z"
}
],
"cursor": "next_page_token",
"total": 42
}
7. Precedence Rules
When multiple statements exist for the same vulnerability+product:
- Timestamp: Later statements supersede earlier ones
- Trust Tier: Higher trust tiers take precedence (Authoritative > Trusted > Community > Unknown)
- Specificity: More specific product matches win (exact version > version range > package)
8. Validation
All normalized statements must pass:
vulnerabilityIdmatches CVE/GHSA/vendor patternstatusis a valid enum valueproductscontains at least one valid PURLtimestampis valid ISO-8601 UTCissuer.issuerIdexists in Issuer Directory or is marked Unknown
Changelog
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2025-12-19 | Initial release |
