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

FormatVersionParser
OpenVEX0.2.0+OpenVexParser
CycloneDX VEX1.5+CycloneDxVexParser
CSAF VEX2.0CsafVexParser

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 FormatSource ValueNormalized Status
OpenVEXnot_affectedNotAffected
OpenVEXaffectedAffected
OpenVEXfixedFixed
OpenVEXunder_investigationUnderInvestigation
CycloneDXnotAffectedNotAffected
CycloneDXaffectedAffected
CycloneDXresolvedFixed
CycloneDXinTriageUnderInvestigation
CSAFnot_affectedNotAffected
CSAFknown_affectedAffected
CSAFfixedFixed
CSAFunder_investigationUnderInvestigation

4.2 Justification Mapping

Source FormatSource ValueNormalized Justification
OpenVEXcomponent_not_presentComponentNotPresent
OpenVEXvulnerable_code_not_presentVulnerableCodeNotPresent
OpenVEXvulnerable_code_not_in_execute_pathVulnerableCodeNotInExecutePath
OpenVEXvulnerable_code_cannot_be_controlled_by_adversaryVulnerableCodeCannotBeControlledByAdversary
OpenVEXinline_mitigations_already_existInlineMitigationsAlreadyExist
CycloneDXSame as OpenVEX (camelCase)Same mapping
CSAFcomponent_not_presentComponentNotPresent
CSAFvulnerable_code_not_presentVulnerableCodeNotPresent
CSAFvulnerable_code_not_in_execute_pathVulnerableCodeNotInExecutePath
CSAFvulnerable_code_cannot_be_controlled_by_adversaryVulnerableCodeCannotBeControlledByAdversary
CSAFinline_mitigations_already_existInlineMitigationsAlreadyExist

4.3 Product Identifier Normalization

Products are normalized to PURL (Package URL) format:

pkg:{ecosystem}/{namespace}/{name}@{version}?{qualifiers}#{subpath}
SourceExtraction Method
OpenVEXDirect from product.id if PURL, else construct from product.identifiers
CycloneDXFrom bom-ref PURL or construct from component.purl
CSAFFrom product_idproduct_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

EndpointMethodDescription
/api/v1/vex/statementsGETQuery normalized statements
/api/v1/vex/statements/{id}GETGet specific statement
/api/v1/vex/normalizePOSTNormalize a VEX document
/api/v1/vex/issuersGETList known issuers
/api/v1/vex/issuers/{id}GETGet issuer details

6.2 Query Parameters

ParameterTypeDescription
vulnerabilitystringFilter by CVE/vulnerability ID
productstringFilter by PURL (URL-encoded)
statusenumFilter by VEX status
issuerstringFilter by issuer ID
sincedatetimeStatements after timestamp
limitintMax results (default: 100, max: 1000)
cursorstringPagination 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:

  1. Timestamp: Later statements supersede earlier ones
  2. Trust Tier: Higher trust tiers take precedence (Authoritative > Trusted > Community > Unknown)
  3. Specificity: More specific product matches win (exact version > version range > package)

8. Validation

All normalized statements must pass:

  1. vulnerabilityId matches CVE/GHSA/vendor pattern
  2. status is a valid enum value
  3. products contains at least one valid PURL
  4. timestamp is valid ISO-8601 UTC
  5. issuer.issuerId exists in Issuer Directory or is marked Unknown

Changelog

VersionDateChanges
1.0.02025-12-19Initial release