How Trivy VEX Works: Understanding Vulnerability Exploitability Exchange in Container Scanning

Trivy's VEX integration suppresses or modifies vulnerability findings by consuming external attestations that declare vulnerabilities as not-affected or fixed, supporting OpenVEX, CycloneDX VEX, and CSAF formats from file, repository, or OCI sources.

Trivy, the comprehensive security scanner from Aqua Security, implements VEX (Vulnerability Exploitability Exchange) as an experimental layer that refines vulnerability reports based on external exploitability statements. This feature allows security teams to filter out false positives or irrelevant vulnerabilities by consuming machine-readable documents that declare specific vulnerabilities as not affecting particular components.

VEX Source Types and Architecture

According to the Trivy source code in pkg/vex/vex.go, the system recognizes four distinct source types for VEX data:

type SourceType string

const (
    TypeFile          SourceType = "file"
    TypeRepository    SourceType = "repo"
    TypeOCI           SourceType = "oci"
    TypeSBOMReference SourceType = "sbom-ref"
)
  • File sources point to local VEX documents on disk.
  • Repository sources download VEX data from the default VEXHub (https://github.com/aquasecurity/vexhub) or custom repositories, caching files under $CACHE_DIR/vex/repositories/.
  • OCI sources retrieve VEX attestations stored as OCI artifacts in container registries.
  • SBOM-Reference sources resolve VEX documents linked within SBOM attestations.

Loading and Parsing VEX Documents

When users specify the --vex flag, the CLI constructs a vex.Options struct and initializes the VEX client via vex.New() in pkg/vex/vex.go. This function iterates through configured sources and dispatches to appropriate loaders:

func New(ctx context.Context, report *types.Report, opts Options) (*Client, error) {
    var vexes []VEX
    for _, src := range opts.Sources {
        var v VEX
        var err error
        switch src.Type {
        case TypeFile:
            v, err = NewDocument(src.FilePath, report)
        case TypeRepository:
            v, err = repo.NewClient(src, report)
        case TypeOCI:
            v, err = oci.NewClient(src, report)
        case TypeSBOMReference:
            v, err = sbomref.NewClient(src, report)
        }
        // ...
    }
    // ...
}

File-based documents are parsed by NewDocument() in pkg/vex/document.go, which dispatches to format-specific decoders for OpenVEX, CycloneDX VEX, and CSAF. Repository-based sources utilize the manager in pkg/vex/repo/manager.go to download and index remote VEX collections, while OCI attestations are fetched by pkg/vex/oci.go and converted into OpenVEX documents.

Filtering Scan Results with NotAffected

After Trivy generates the SBOM, it encodes the component hierarchy once using sbomio.NewEncoder(), then filters vulnerabilities through vex.Filter() in pkg/vex/vex.go. This function walks each Result and applies the VEX client's NotAffected() method:

func Filter(ctx context.Context, report *types.Report, opts Options) error {
    client, err := New(ctx, report, opts)
    // ...
    bom, err := sbomio.NewEncoder(...).Encode(*report)
    // ...
    for i, result := range report.Results {
        filterVulnerabilities(&report.Results[i], bom, client.NotAffected)
    }
    return nil
}

The OpenVEX implementation in pkg/vex/openvex.go demonstrates the core filtering logic. It matches statements against vulnerabilities, products, and sub-components, selecting the latest statement when multiple exist:

func (v *OpenVEX) NotAffected(vuln types.DetectedVulnerability,
    product, subComponent *core.Component) (types.ModifiedFinding, bool) {

    stmts := v.Matches(vuln, product, subComponent)
    if len(stmts) == 0 {
        return types.ModifiedFinding{}, false
    }
    stmt := stmts[len(stmts)-1] // latest statement wins
    if stmt.Status == openvex.StatusNotAffected || stmt.Status == openvex.StatusFixed {
        modified := types.NewModifiedFinding(vuln,
            findingStatus(stmt.Status), string(stmt.Justification), v.source)
        return modified, true
    }
    return types.ModifiedFinding{}, false
}

When a vulnerability matches a statement with status NotAffected or Fixed, Trivy converts it into a ModifiedFinding rather than removing it entirely, preserving the audit trail while suppressing the alert.

CLI Usage Examples

Apply VEX data from the default repository when scanning an image:

trivy image --vex repo nginx:latest

Use a local OpenVEX or CycloneDX VEX document:

trivy fs --vex file:/path/to/vex-document.json /my/application

Fetch VEX attestations from an OCI registry:

trivy image --vex oci:registry.example.com/vex/attest:latest my-app:latest

Manage VEX repositories with the dedicated sub-command:


# List configured repositories

trivy vex list

# Download or refresh repository data

trivy vex download

# Refresh specific repository only

trivy vex download my-custom-repo

Programmatic Implementation

For custom tooling, interact with the VEX package directly:

import (
    "context"
    "github.com/aquasecurity/trivy/pkg/vex"
    "github.com/aquasecurity/trivy/pkg/types"
)

func applyVEX(report *types.Report) error {
    opts := vex.Options{
        CacheDir: "/tmp/trivy-cache",
        Sources: []vex.Source{
            vex.NewSource("repo"),                      // default VEXHub
            vex.NewSource("/path/to/local.vex.json"),   // local file
        },
    }
    return vex.Filter(context.Background(), report, opts)
}

Summary

  • Trivy implements VEX as an experimental filtering layer that interprets NotAffected and Fixed statements from external documents.
  • Four source types are supported: file, repo, oci, and sbom-ref, defined in pkg/vex/vex.go.
  • The vex.New() constructor in pkg/vex/vex.go dispatches to format-specific loaders including pkg/vex/document.go for local files and pkg/vex/repo/manager.go for remote repositories.
  • Vulnerability filtering occurs post-scan via vex.Filter(), which encodes the SBOM and invokes the NotAffected() interface method for each vulnerability candidate.
  • Filtered vulnerabilities become ModifiedFinding entries rather than disappearing completely, maintaining transparency in security reports.

Frequently Asked Questions

How does Trivy determine which VEX statement to apply when multiple exist?

Trivy selects the latest statement for a given vulnerability-product combination. In pkg/vex/openvex.go, the implementation sorts matching statements and uses stmts[len(stmts)-1] to ensure the most recent exploitability assessment takes precedence.

Can Trivy use VEX documents stored in private OCI registries?

Yes. When specifying --vex oci:registry/repository:tag, Trivy uses the oci.NewClient() implementation in pkg/vex/oci.go to fetch attestation layers from authenticated registries, parsing the payload as OpenVEX data.

Why do suppressed vulnerabilities still appear in JSON output?

Rather than deleting findings, Trivy converts suppressed vulnerabilities into ModifiedFinding structures with status NotAffected or Fixed. This preserves the complete audit trail while clearly indicating which VEX statement affected the result, as implemented in the NotAffected() return logic across all VEX format implementations.

What happens when the VEX repository source is unavailable?

If the repository download fails, pkg/vex/repo/manager.go returns an error during client initialization in vex.New(), causing the scan to fail fast unless the repository is optional. Users can pre-download repositories using trivy vex download to ensure offline availability.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →