Skip to content

feat(vex): native discovery of VEX documents stored as OCI artifacts#10807

Closed
mvanhorn wants to merge 26 commits into
aquasecurity:mainfrom
mvanhorn:fix/10750-vex-oci-native-discovery
Closed

feat(vex): native discovery of VEX documents stored as OCI artifacts#10807
mvanhorn wants to merge 26 commits into
aquasecurity:mainfrom
mvanhorn:fix/10750-vex-oci-native-discovery

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Jun 7, 2026

Copy link
Copy Markdown

Description

This migrates OCI VEX discovery (--vex oci) off the external openvex/discovery library to a native in-tree implementation, and extends it to support Cosign v3 attestations.

Previously Trivy discovered VEX attestations through openvex/discovery, which only resolves legacy .att-tag attestations (the Cosign v2 default) and is effectively unmaintained.
Starting with Cosign v3, cosign attest defaults to the new Sigstore bundle stored as an OCI 1.1 referrer, so attestations produced by a default cosign attest on Cosign v3 were not discovered and users got No VEX attestations found (reported in #10656).

Discovery now lives in a dedicated in-tree package (pkg/vex/oci) and supports both layouts:

  • OCI 1.1 referrer + Sigstore bundle (Cosign v3 default): list referrers, filter by artifact type, decode the bundle → DSSE → in-toto Statement → OpenVEX predicate.
  • Legacy .att tag (Cosign v2): kept as a fallback; when both are present the referrer result is preferred.

A referrer that carries a supported artifact type but is not OpenVEX (e.g. an SBOM or SLSA provenance attestation, which share the same media type) is skipped rather than treated as an error, and discovery falls back to the legacy tag.

Decoding reuses the existing pkg/attestation helpers, and the OCI media-type constants are shared from pkg/oci.

Hardening

The attestation fetch is bounded against hostile registries: at most 100 attestations are processed per image (referrers and legacy .att layers alike), and each layer is read with a 50 MiB limit to guard against decompression bombs.

Dependency impact

Dropping openvex/discovery also removes its heavy transitive supply chain from the module graph: sigstore/cosign/v2, sigstore/sigstore-go, the TUF stack (theupdateframework/*) and related packages.
The Sigstore bundle is decoded directly via pkg/attestation, so no cosign/sigstore-go dependency is reintroduced.
go.mod/go.sum are tidied accordingly.

Behavior notes

Registry authentication is not yet wired through.
The attestation fetch still uses the default keychain, so docker login / trivy registry login keep working, but credentials passed via Trivy's own flags/env (TRIVY_USERNAME/TRIVY_PASSWORD, --username/--password, --registry-token, --insecure) do not yet reach the VEX fetch.
This matches the previous openvex/discovery behavior.

TODO

Related issues

Checklist

  • I've read the guidelines for contributing to this repository.
  • I've followed the conventions in the PR title.
  • I've added tests that prove my fix is effective or that my feature works.
  • I've updated the documentation with the relevant information (if needed).
  • I've added usage information (if the PR introduces new options)
  • I've included a "before" and "after" example to the description (if the PR is a user interface change).

Resolve and pull a VEX document associated with an image directly from the
registry, so VEX-based filtering works with OCI-distributed VEX instead of
requiring the document out-of-band.

Closes aquasecurity#10750

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>

@DmitriyLewen DmitriyLewen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @mvanhorn
Thanks for your work!

I left a comments, can you take a look?

Comment thread go.mod
Comment thread pkg/vex/oci.go Outdated
Comment thread pkg/vex/oci.go Outdated
Comment thread pkg/vex/oci.go Outdated
Comment thread pkg/vex/oci.go Outdated
Comment thread pkg/vex/oci/oci.go Outdated
The repository_url qualifier comes from untrusted image metadata. The
original code parsed it as a URL and, for an http:// scheme, silently
downgraded the connection to an insecure (plaintext) transport, leaking
ambient registry credentials.

The previous openvex/discovery-based implementation never did this: it
passed repository_url to name.ParseReference verbatim, with no transport
options. Restore that behavior - drop the URL parsing and the (currently
unused) insecure name option. Registry auth and insecure/TLS wiring are
deferred to a follow-up (ref aquasecurity#8916).
pkg/attestation already provides Statement and SigstoreBundle types that
decode a DSSE envelope (bare or wrapped in a Cosign v3+ bundle) into a typed
in-toto predicate. Reuse them instead of hand-rolling the DSSE/base64/in-toto
decode here, and unmarshal the predicate straight into the OpenVEX struct.
A legacy cosign `.att` tag accumulates one layer per `cosign attest` call,
so an image attested with both an SBOM and an OpenVEX document carries a
multi-layer `.att`. Requiring exactly one layer made `--vex oci` fail hard
on such images instead of finding the VEX, a regression versus the previous
openvex/discovery-based implementation.

Iterate every layer and return the first one that decodes to an OpenVEX
document, skipping the rest, mirroring openvex/discovery. The referrer path
is unaffected: each referrer is still a single-layer artifact.
stdlib errors.As already unwraps a hashicorp/go-multierror (its Unwrap
returns a chain implementing As/Is), so the custom errorsAs helper that
walked the multierror by hand is redundant. Drop it together with the
go-multierror import, and collapse the duplicated transport.Error
error-code scanning into a small hasErrorCode helper.
RetrieveVEXAttestation currently calls into the fetch path with an empty
RegistryOptions, so registry credentials, --insecure and TLS settings never
reach the VEX attestation lookup. Document this gap (ref aquasecurity#8916) until the
options are threaded through.
pkg/oci already defines SigstoreBundleArtifactType. Add the DSSE envelope
artifact type next to it (DSSEEnvelopeArtifactType) and use both shared
constants in the VEX OCI discovery instead of redeclaring the media-type
literals locally.
Merge the separate referrer/legacy scenario tests into a single
table-driven TestRetrieveVEXAttestation, where each case sets up its own
fixtures and asserts matches/errors via data fields. The registry-auth
test keeps its own private registry and stays separate.
@DmitriyLewen

Copy link
Copy Markdown
Contributor

Hi @mvanhorn , we want to include these changes in the next release.

I updated the PR. If you have time — please review the changes.

…ex-oci-native-discovery

# Conflicts:
#	go.mod
#	go.sum
A hostile registry could stack an unbounded number of layers in a legacy
`.att` tag, and the legacy path reads layers until it finds an OpenVEX one,
so each extra layer is a blob fetch. Cap the count at 100, mirroring
cosign's per-image attestation limit. The referrer path needs no such cap:
it fetches at most one candidate (it returns on the first supported referrer).
… cases

Add unit tests for the registry error-code helpers (isReferrersUnsupported /
isNotFound / hasErrorCode) via export_test.go, including the multierror case
that drives the fallback from referrers to the legacy `.att` tag when a
registry does not support the referrers API. Also cover resolveDigest's
invalid-purl branches and the legacy edge cases (no OpenVEX layer -> nil,
too many layers -> error).
@DmitriyLewen DmitriyLewen requested a review from nikpivkin June 24, 2026 15:56
Replace []byte(fmt.Sprintf...) with fmt.Appendf (modernize), use %q for the
quoted strings in the docker config template (gocritic), and rename the
unused test-setup parameter to _ (revive).
Comment thread pkg/vex/oci.go Outdated
Comment thread pkg/vex/oci.go Outdated
Comment thread pkg/vex/oci.go Outdated
Comment thread pkg/vex/oci.go Outdated
resolveDigest already rejects a nil package URL, so guard it once at the top
of retrieveVEXAttestation instead of conditionally building the log field and
re-checking deeper down.
The caller already has a context (vex.New -> NewOCI), so thread it into
NewOCI and RetrieveVEXAttestation instead of starting from
context.Background() inside the fetch. Registry credential/options wiring
remains a separate follow-up (TODO aquasecurity#8916).
Membership testing over the supported artifact types reads more idiomatically
with pkg/set than slices.Contains over a slice.
readLayer read an attestation layer with an unbounded io.ReadAll, so a
hostile registry could serve a small compressed layer that expands to
gigabytes (CWE-409). Cap the uncompressed read at 50 MiB and reject larger
layers; OpenVEX documents are far smaller.
…ic API

Move OCI VEX discovery and decoding out of package vex into a dedicated
pkg/vex/oci package and expose Discover(ctx, purl, RegistryOptions), returning
the raw OpenVEX document. This lets tools embedding Trivy fetch a VEX
attestation from the image-pull side (with their own registry credentials) and
stage it, instead of only filtering at scan time.

pkg/vex.NewOCI now wraps oci.Discover. Returning openvex.VEX rather than the
internal *OpenVEX keeps the new package free of an import cycle. The discovery
tests move with the package; TestFilter keeps a lightweight NewOCI error-path check.
A VEX artifact type (Sigstore bundle / DSSE envelope) is shared by every
Cosign attestation, so an image may expose SBOM or provenance referrers too.
Discovery aborted the scan with "unsupported predicate type" on the first
non-OpenVEX referrer instead of skipping it and falling back to legacy `.att`.

Skip non-OpenVEX attestations in both paths, and cap the referrers processed
per image (like the legacy-layer limit) to avoid unbounded fetches.
Add a test that discovers VEX for an OCI purl whose version is the image
digest (the common scan path), and a referrer case using a versioned
OpenVEX predicate namespace, so both the digest branch of resolveDigest and
the prefix branch of isOpenVEXPredicateType are exercised.
@DmitriyLewen DmitriyLewen marked this pull request as draft June 30, 2026 14:54
…n helpers

Add NewServerWithAuth, PushImage and PushLegacyAttestation, and accept
remote.Option on the push helpers, so registry-based tests can cover
authenticated pulls and legacy cosign `.att` attestations. Existing callers
are unaffected (the options are variadic).
isReferrersUnavailable better reflects that it also covers the "not found"
case, not only an unsupported referrers API.
Move the tests to package oci_test, exposing the few internals they need
(readLayer, isReferrersUnavailable, the size limit) through export_test.go,
delegate all registry setup and pushes to internal/registrytest, and collapse
the want-match/want-nil table fields into a single map.
Push a legacy VEX attestation to a test registry and run the full
`--vex oci` filter, asserting the finding is suppressed with the
"VEX attestation in OCI registry (...)" source. This covers the success
path through NewOCI/Discover, which the unit tests do not exercise end to end.
Describe the two attestation layouts Trivy discovers (Cosign v3 OCI 1.1
referrer / Sigstore bundle and the legacy Cosign v2 `.att` tag) and note
that attestation signatures are not verified.
@DmitriyLewen DmitriyLewen marked this pull request as ready for review July 1, 2026 10:26
@DmitriyLewen

Copy link
Copy Markdown
Contributor

Hello @nikpivkin
I've addressed your comments.
I also did some refactoring:

  • added a new Discover function and moved the necessary functions into the pkg/vex/oci subpackage.
  • refactored the tests.

Please take a look when you have time.

@DmitriyLewen

Copy link
Copy Markdown
Contributor

Hi @mvanhorn, thank you very much for your work on this — it was a great foundation for native OCI VEX discovery.

Since the PR has been inactive for a while and I'm not able to rebase or push to this branch myself, I've opened a follow-up PR that builds directly on your commits and carries the feature the rest of the way: #10932. Your original commits are preserved there, so the credit stays with you.

I'll close this one in favor of #10932. Thanks again for getting this started!

@mvanhorn

Copy link
Copy Markdown
Author

Thanks for carrying it forward and for preserving the commits - much appreciated. #10932 looks like the right home for it; glad the groundwork was useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(vex): migrate OCI VEX discovery off openvex/discovery (support Cosign v3 / OCI 1.1 referrers)

3 participants