feat(vex): native discovery of VEX documents stored as OCI artifacts#10807
feat(vex): native discovery of VEX documents stored as OCI artifacts#10807mvanhorn wants to merge 26 commits into
Conversation
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
left a comment
There was a problem hiding this comment.
Hello @mvanhorn
Thanks for your work!
I left a comments, can you take a look?
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.
|
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).
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).
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.
…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.
|
Hello @nikpivkin
Please take a look when you have time. |
|
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! |
|
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. |
Description
This migrates OCI VEX discovery (
--vex oci) off the externalopenvex/discoverylibrary 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 attestdefaults to the new Sigstore bundle stored as an OCI 1.1 referrer, so attestations produced by a defaultcosign atteston Cosign v3 were not discovered and users gotNo VEX attestations found(reported in #10656).Discovery now lives in a dedicated in-tree package (
pkg/vex/oci) and supports both layouts:.atttag (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/attestationhelpers, and the OCI media-type constants are shared frompkg/oci.Hardening
The attestation fetch is bounded against hostile registries: at most 100 attestations are processed per image (referrers and legacy
.attlayers alike), and each layer is read with a 50 MiB limit to guard against decompression bombs.Dependency impact
Dropping
openvex/discoveryalso 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 nocosign/sigstore-godependency is reintroduced.go.mod/go.sumare tidied accordingly.Behavior notes
Registry authentication is not yet wired through.
The attestation fetch still uses the default keychain, so
docker login/trivy registry loginkeep 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/discoverybehavior.TODO
--insecureand TLS settings through to the VEX fetch (bug(vex): Trivy doesn't support auth for OCI images from private registries #8916).Related issues
openvex/discovery(support Cosign v3 / OCI 1.1 referrers) #10750Checklist