CEL Expression Reference for VEX Rules
A VEX rule matches vulnerabilities using
CEL (Common Expression Language). An expression must evaluate to a
boolean: true means the rule applies to that vulnerability. This page documents everything the
expression environment provides.
The environment at a glance
The environment is deliberately small:
- exactly one variable:
vuln - exactly two DevGuard-specific functions:
matchesPatternandmatchesPurl— the only two that return a boolean on their own - the standard CEL operators:
==,!=,<,<=,>,>=,&&,||,!,in,?:
The editor checks two things as you type. First, that the expression parses. Second, that it evaluates to a
boolean — a bare field access like vuln.cveId is a string and is rejected with Expression must evaluate to
a bool, e.g. via ==, !=, matchesPattern(...) or matchesPurl(...). Wrap it in a comparison.
Lines beginning with // are comments and are skipped. Every other line is checked as an expression in its own
right, which is why the rules DevGuard pre-fills for you put their explanatory comment on a separate line above
the expression.
The vuln variable
vuln is the dependency vulnerability being evaluated. The editor's autocomplete offers these fields — type
vuln. to see them:
| Field | Type | Description |
|---|---|---|
vuln.id | string | The finding's own identifier. |
vuln.cveId | string | The advisory id, e.g. "CVE-2021-1234". Empty for findings without a CVE. |
vuln.cve | map | The nested CVE object — type . again for its fields. |
vuln.componentPurl | string | Package URL of the vulnerable component, e.g. "pkg:npm/lodash@4.17.21". |
vuln.rawRiskAssessment | number | DevGuard's risk score for the finding. |
vuln.assetVersionName | string | The branch or tag the finding belongs to. |
CVE details: vuln.cve
Type vuln.cve. for these:
| Field | Type | Description |
|---|---|---|
vuln.cve.cve | string | The CVE identifier, e.g. "CVE-2021-1234". |
vuln.cve.cvss | number | CVSS base score. |
vuln.cve.vector | string | The CVSS vector string. |
vuln.cve.epss | number | EPSS exploitation probability. |
matchesPattern(vuln, pattern)
Matches the vulnerability's dependency path. This is the function you want whenever the decision depends on how a package enters your product rather than which package it is.
The first argument is always vuln itself (the function reads vulnerabilityPath and artifactPurls from
it). The second is a list of strings, each being either a purl or one of two special tokens.
| Token | Meaning |
|---|---|
* | Zero or more path elements — any ancestors above the next named element. |
ROOT | Anchors the pattern at the repository root, i.e. the element after it must be a direct dependency. Matches all artifacts and branches. |
Matching semantics
By default a pattern is matched as a suffix of the dependency path. The pattern
["pkg:npm/a@1.0.0", "pkg:npm/b@2.0.0"] matches any path that ends with a → b, regardless of what comes
before.
If the pattern's first element identifies one of the repository's artifacts, that element is consumed and the remainder is matched anchored from the start of the path instead. This is what lets DevGuard's own CSAF and CycloneDX exports round-trip cleanly back into rules.
Individual elements match by exact purl string first; failing that, the pattern element's version is interpreted as a semver constraint (see the caveat below).
matchesPurl(purl, constraint)
Matches a purl against a version constraint — for dismissing a whole version range rather than one exact version.
The type, namespace and name must be exactly equal. Only the version part is treated as a constraint, using Masterminds semver syntax:
| Constraint | Matches |
|---|---|
pkg:npm/undici@>=6.0.0 | 6.0.0 and above |
pkg:npm/undici@6.26.* | any 6.26.z |
pkg:npm/undici@6.x | any 6.y.z |
pkg:npm/undici@~6.26.0 | 6.26.0 up to but excluding 6.27.0 |
pkg:npm/undici@^6.0.0 | 6.0.0 up to but excluding 7.0.0 |
pkg:npm/undici@>=6.0.0 <6.27.0 | a bounded range |
Cookbook
Dismiss one advisory everywhere
Dismiss one exact component version
Use this form — not matchesPurl — for Debian, Alpine and other non-semver ecosystems:
Dismiss a version range
Dismiss one advisory only along one dependency path
The most precise form, and the one the path graph generates for you:
The leading "*" is here only for readability — it makes the pattern read as "any ancestors, then this
path". Since matching is suffix-based by default, omitting it matches exactly the same vulnerabilities.
Dismiss an advisory only when it arrives through a direct dependency
Accept everything below a CVSS threshold
Accept low-severity findings that are also unlikely to be exploited
DevGuard only re-evaluates rules against open vulnerabilities. A rule like this dismisses a finding once, but it will not reopen it later if the EPSS score subsequently rises above the threshold. This is a current limitation of DevGuard's rule engine, and is why the "accept" rules are less common than "dismiss" rules.
Scope a rule to one branch
Dismiss any of several advisories in the same component
Related Documentation
- Create & Manage VEX Rules — where these expressions are used
- Choose a Mechanical Justification — the decision that accompanies the match
- Transitive Dependencies — how DevGuard builds the dependency paths that
matchesPatternmatches - Vulnerability Matching — how purls are normalized before matching
- Risk Assessment Methodology — what the risk and priority fields mean