This is the third publication in the technical series about the engineering decisions behind Quor's images. In the previous article, we explored how compiling directly from source code eliminates supply chain dependencies and allows precise control over what goes into the image. In this article, we reach a different layer of the problem: even after drastically reducing the number of packages and CVEs, a question still remains that no scanner answers by default: is this vulnerability exploitable in this image, in this context? The answer to this question is VEX.

Security Researcher
Heitor Gouvêa

Grype, Trivy, and Docker Scout do what they are designed for very well: scanning the packages of an image against databases of known vulnerabilities (NVD, OSV, GitHub Advisory Database) and reporting matches. The model is simple:
package X in version Y has associated CVE Z → report.
What this model does not capture is context.
A CVE is associated with a component, not a specific use of it. When the database records that libssl 3.0.7 has CVE-2023-0286, it does not know (and has no way of knowing) whether the vulnerable code within libssl is actually reachable by your application. It doesn't know if the affected function is called in any execution path. It doesn't know if the attack vector assumes access to an interface that does not exist in the image.
The practical result: scanners report vulnerabilities that, technically, exist in the component, but are structurally unexploitable in the specific context of the image. This is not a failure of the scanners, it is an inherent limitation of the database matching approach. The problem is that without an additional layer of contextualization, every reported CVE seems equally urgent.
For an engineer, CVEs without context have a direct cost. Pipelines configured with blocking policies on HIGH or CRITICAL severity stop. Someone needs to manually investigate each reported CVE, understand if it is exploitable, and decide if the block is justified. In images with dozens of packages, this can mean hours per sprint dedicated to triaging vulnerabilities that, at the end of the investigation, are dismissed as non-applicable.
This time is taken away from features, infrastructure, and technical debt, and goes into triaging false positives. VEX was created to eliminate this noise with an auditable technical justification.
What is VEX (Vulnerability Exploitability eXchange)
VEX stands for Vulnerability Exploitability eXchange. It is a metadata standard that allows the producer of an artifact (a container image, a binary, a package) to formally declare, for each CVE, whether that vulnerability is exploitable in that specific artifact and why.
The concept was initiated by CISA (Cybersecurity and Infrastructure Security Agency) as part of the SBOM maturity effort. While the SBOM lists the components contained in the image, VEX indicates which of those components represent actual risk.
The four VEX statuses: not_affected, affected, fixed, under_investigation
VEX models the lifecycle of a vulnerability in relation to an artifact as a four-state machine:
not_affected: the component with the CVE is present in the image, but the vulnerability is not exploitable in that context. It requires an obligatory technical justification.
affected: the vulnerability is confirmed to be exploitable. It must include information about expected impact and recommended actions.
fixed: the vulnerability existed and has been fixed. Useful when the image already applies a patch before it is reflected in the official database.
under_investigation: the status is still being analyzed. Indicates that the producer is aware and working on the evaluation.

At Quor, the statuses that dominate our operation are not_affected and fixed, each for a different reason that we will explore in detail.
What is OpenVEX and why we chose this format
There are three main VEX formats: OpenVEX (CISA/OpenSSF), CycloneDX VEX, and CSAF VEX. At Quor, we adopted OpenVEX, the leanest format, specifically designed for the context of containers and software artifacts.
OpenVEX is a JSON-LD document with a minimal, well-defined structure. Let us break down each relevant field. Structure of an OpenVEX document:
Fields that deserve specific attention:
vulnerability.@id: the canonical reference to the CVE, using the official URL from NVD or CVE.org. Not a freeform string, but a resolvable identifier.
products: the evaluated artifact, identified by the SHA256 digest of the image. This is key: the VEX statement is bound to a specific and immutable version of the image, not to a generic name.
subcomponents: the specific component within the artifact that contains the CVE, identified by PURL (Package URL). This allows tools to understand exactly which package is being evaluated.
justification: the most critical field in the document. OpenVEX defines a controlled vocabulary of justifications for the not_affected status:
Justification | Meaning |
component_not_present | The component is not present in the image (scanner false positive) |
vulnerable_code_not_present | The component is present, but the vulnerable code has been removed (e.g., partial patch, build with feature flags) |
vulnerable_code_not_in_execute_path | The vulnerable code exists, but it cannot be reached by any execution path |
vulnerable_code_cannot_be_controlled_by_adversary | The execution path exists, but the input required for exploitability cannot be controlled externally |
inline_mitigations_already_exist | Configuration or runtime-level mitigations block exploitation |
impact_statement: | The technical justification in natural language. This field is where the actual analysis is documented. It must be specific enough so that another engineer can verify the finding independently. |
The controlled vocabulary is intentional. It is not free text, but rather a taxonomy that security tools can process automatically (enabling parsing).
Examples of VEX statements in real cases
Case 1: CVE in unreachable code (CVE-2023-0286 in libssl)
CVE-2023-0286 affects the X509_NAME_oneline() function in OpenSSL, in processing GeneralNames of type ASN1_STRING in X.509 certificates. The vulnerability is exploitable when an application processes arbitrary certificates provided by external clients.
What the scanner sees: libssl with CVE of HIGH severity. What the VEX declares: the attack vector assumes arbitrary certificate processing, a feature that does not exist in this image.
Case 2: component not present, scanner false positive (CVE-2022-37434 in zlib)
Some scanners associate CVEs with packages based on name without verifying the actual presence of the binary or library in the image. This is a classic case of a structural false positive. CVE-2022-37434 affects the inflate() function in zlib, exploitable via specific malformed input. Trivy and Grype frequently report this CVE in Alpine images that list zlib as a transitive dependency, even when the installed version already includes the patch or when the vulnerable binary is not present.
This case exposes an important limitation of scanners: the NVD database can record a CVE as affecting a range of versions without capturing that individual distributions (Alpine, Debian, Red Hat) apply patches within the same version number. The VEX closes this gap with direct traceability to the distribution advisory and the patch commit.
Case 3: early patch with 'fixed' status (CVE-2024-41110 in Docker Engine)
This is one of the most valuable cases from an operational standpoint. When we compile directly from source code (as described in article #2), we have the ability to apply security patches before the official database records the fix, and before the distribution releases a version with the fix. CVE-2024-41110 is a critical vulnerability in Docker Engine (AuthZ plugin bypass). For images that include the Docker CLI as an auxiliary tool, the scanner will report the CVE as long as the advisory is not resolved in the databases.
Without fixed VEX, the scanner will continue reporting the CVE until the database reflects the corrected version. With VEX, the CI pipeline knows that the image is already patched, with traceability down to the specific commit.
Case 4: externally uncontrollable attack vector (CVE-2023-45853 in zlib/minizip)
Some CVEs are exploitable only under very specific conditions, conditions that the application architecture makes externally impossible. CVE-2023-45853 affects minizip within zlib, exploitable via ZIP files with excessively long filename fields. This is only applicable in contexts where the application processes ZIP files from untrusted sources.
How VEX is consumed by scanners
The practical question for a platform engineer: how does this work in integration with Grype, Trivy, and Docker Scout?
Grype supports VEX natively via the --vex flag:
With the VEX document loaded, Grype automatically filters CVEs with status not_affected or fixed. The result is a list of vulnerabilities that represents only actual risk — not contextual false positives.
For CI/CD integration:
Trivy supports VEX starting with version 0.46.0, via the --vex flag:
In Trivy's output, CVEs with a VEX statement appear with the VEX Status column populated, and can be filtered with --ignore-status not_affected.
Docker Scout supports VEX via attestations on the image. The VEX document is attached as an OCI attestation during push:
With the attestation present, docker scout cves automatically filters vulnerabilities declared as not_affected. VEX becomes an inseparable part of the image, and any scanner that extracts OCI attestations receives the context along with it.
This is the preferred approach: by embedding VEX as an attestation in the image, the document follows the image to any registry or execution environment, without the need for manual distribution of external files.
The operational cost of producing and maintaining VEX
Before closing, a point that is often left out: producing quality VEX has a cost. Every not_affected statement requires technical analysis, reading the advisory, verifying if the vulnerable code path is reachable in the image, and mapping the attack vector against the architecture. Without this rigor, the statement becomes false security.
For a platform team to maintain VEX on their own images, the process involves:
For each new CVE reported: reading the complete advisory, understanding the attack vector, verifying the code path in the image, writing the technical justification, committing the updated document, and ensuring that the image version is updated with the new attestation.
For each new version of the image: reviewing all existing VEX statements to ensure they are still valid for the new version—something is missing to connect here, isn't there? A patch could introduce a new component that activates a previously inert code path.
For each advisory update: monitoring changes in the scope of already declared CVEs; a CVE declared not_affected today might have its attack vector expanded by a supplemental advisory, requiring a new analysis.
This is exactly the work that Quor does for the customer.
Each image in the catalog is distributed with a VEX document maintained by our team, linked to the specific image digest by attestation. When a customer pulls quor/node:22-lts, they don't just receive the image; they receive the exploitability intelligence that goes with it: which CVEs reported by scanners are structurally unexploitable, with the technical justification supporting each statement.
The practical effect on the CI pipeline: scanners stop blocking builds for CVEs already evaluated as unexploitable, and the engineering team stops spending hours per sprint on manual triaging.
Limitations of VEX: what it doesn't solve
VEX solves a specific problem: separating exploitable CVEs from alerts that do not apply to the analyzed context. The standard helps reduce scanner noise but does not eliminate the need for ongoing technical analysis.
Analysis supports the document. An inadequately justified not_affected statement can suppress an alert without sufficient evidence. Therefore, each justification must consider the advisory, the affected package, the context of use, and the vulnerable code path.
Maintenance is also continuous. A VEX document applies to the version of the image where the analysis was done. Linking it to the SHA256 digest ensures that the statement applies to a specific and immutable artifact. New versions require review, and an already analyzed CVE may change context if supplemental advisories expand the attack vector.
The scope of VEX in the context of container images also has limits. It does not cover vulnerabilities in application code, business logic flaws, runtime configurations, or dependencies added by the application, such as npm packages, JARs, and external libraries. These risks require other layers of analysis.
Support across tools still varies. Grype and Trivy already have consistent implementations, but some vulnerability management platforms may require additional configuration to consume OpenVEX documents and apply the automatic suppression of the corresponding alerts.
Check out the other contents in the series:
→ How we are building images with zero CVEs #1: reducing the attack surface (Alpine and distroless)
→ How we are building images with zero CVEs #2: compiling from source
Operating Kubernetes in production for more than 13 years. With Quor, this experience extends to software supply chain security as well.
GET UP
© Getup · 2026
