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: matchesPattern and matchesPurl — 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:

FieldTypeDescription
vuln.idstringThe finding's own identifier.
vuln.cveIdstringThe advisory id, e.g. "CVE-2021-1234". Empty for findings without a CVE.
vuln.cvemapThe nested CVE object — type . again for its fields.
vuln.componentPurlstringPackage URL of the vulnerable component, e.g. "pkg:npm/lodash@4.17.21".
vuln.rawRiskAssessmentnumberDevGuard's risk score for the finding.
vuln.assetVersionNamestringThe branch or tag the finding belongs to.

CVE details: vuln.cve

Type vuln.cve. for these:

FieldTypeDescription
vuln.cve.cvestringThe CVE identifier, e.g. "CVE-2021-1234".
vuln.cve.cvssnumberCVSS base score.
vuln.cve.vectorstringThe CVSS vector string.
vuln.cve.epssnumberEPSS 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.

TokenMeaning
*Zero or more path elements — any ancestors above the next named element.
ROOTAnchors 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:

ConstraintMatches
pkg:npm/undici@>=6.0.06.0.0 and above
pkg:npm/undici@6.26.*any 6.26.z
pkg:npm/undici@6.xany 6.y.z
pkg:npm/undici@~6.26.06.26.0 up to but excluding 6.27.0
pkg:npm/undici@^6.0.06.0.0 up to but excluding 7.0.0
pkg:npm/undici@>=6.0.0 <6.27.0a 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

Have feedback? We want to hear from you!

Fields marked with * are required