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

> Discover how Trivy VEX works to streamline container scanning. Learn how it uses vulnerability exploitability exchange to manage findings from multiple sources and formats like OpenVEX, CycloneDX, and CSAF.

- Repository: [Aqua Security/trivy](https://github.com/aquasecurity/trivy)
- Tags: deep-dive
- Published: 2026-03-23

---

**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`](https://github.com/aquasecurity/trivy/blob/main/pkg/vex/vex.go), the system recognizes four distinct source types for VEX data:

```go
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`](https://github.com/aquasecurity/trivy/blob/main/pkg/vex/vex.go). This function iterates through configured sources and dispatches to appropriate loaders:

```go
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`](https://github.com/aquasecurity/trivy/blob/main/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`](https://github.com/aquasecurity/trivy/blob/main/pkg/vex/repo/manager.go) to download and index remote VEX collections, while OCI attestations are fetched by [`pkg/vex/oci.go`](https://github.com/aquasecurity/trivy/blob/main/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`](https://github.com/aquasecurity/trivy/blob/main/pkg/vex/vex.go). This function walks each `Result` and applies the VEX client's `NotAffected()` method:

```go
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`](https://github.com/aquasecurity/trivy/blob/main/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:

```go
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:

```bash
trivy image --vex repo nginx:latest

```

Use a local OpenVEX or CycloneDX VEX document:

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

```

Fetch VEX attestations from an OCI registry:

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

```

Manage VEX repositories with the dedicated sub-command:

```bash

# 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:

```go
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`](https://github.com/aquasecurity/trivy/blob/main/pkg/vex/vex.go).
- The `vex.New()` constructor in [`pkg/vex/vex.go`](https://github.com/aquasecurity/trivy/blob/main/pkg/vex/vex.go) dispatches to format-specific loaders including [`pkg/vex/document.go`](https://github.com/aquasecurity/trivy/blob/main/pkg/vex/document.go) for local files and [`pkg/vex/repo/manager.go`](https://github.com/aquasecurity/trivy/blob/main/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`](https://github.com/aquasecurity/trivy/blob/main/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`](https://github.com/aquasecurity/trivy/blob/main/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`](https://github.com/aquasecurity/trivy/blob/main/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.