diff --git a/docs/usage/policy-compliance/expressions.md b/docs/usage/policy-compliance/expressions.md
index d63cc4d00..27e98ec37 100644
--- a/docs/usage/policy-compliance/expressions.md
+++ b/docs/usage/policy-compliance/expressions.md
@@ -25,22 +25,28 @@ CEL syntax is described thoroughly in the official [language definition].
## Evaluation Context
-Conditions are scoped to individual components.
+Conditions are scoped to individual components.
Each condition is evaluated for every single component in a project.
The context in which expressions are evaluated in contains the following variables:
-| Variable | Type | Description |
-|:------------|:-----------------------------------|:---------------------------------------------|
-| `component` | [Component] | The component being evaluated |
-| `project` | [Project] | The project the component is part of |
-| `vulns` | list([Vulnerability]) | Vulnerabilities the component is affected by |
+| Variable | Type | Description |
+|:------------|:------------------------------------------------|:---------------------------------------------|
+| `component` | [Component] | The component being evaluated |
+| `project` | [Project] | The project the component is part of |
+| `vulns` | list([Vulnerability]) | Vulnerabilities the component is affected by |
+| `now` | [`google.protobuf.Timestamp`][protobuf-ts-docs] | The current time at the start of evaluation |
+
+!!! note
+ Many fields on [Component], [Project], and [Vulnerability] are optional.
+ Use the `has()` macro to [check for presence of optional fields](#optional-field-checking)
+ before accessing them.
## Best Practices
1. **Keep expressions simple and concise**. The more complex an expression becomes, the harder it gets to determine why
it did or did not match. Use policy operators (`Any`, `All`) to chain multiple expressions if practical.
-2. **Call functions last**. [Custom functions](#function-definitions) involve additional computation that is more
+2. **Call functions last**. [Custom functions](#function-reference) involve additional computation that is more
expensive than simple field accesses. Performing any checks on fields first, and calling functions last, oftentimes
allows evaluation to short-circuit.
3. **Remove conditions that are no longer needed**. Dependency-Track analyzes the configured expressions to determine
@@ -52,22 +58,21 @@ the more data has to be loaded. Removal of outdated conditions thus has a direct
### Component age
Besides out-of-date versions, component age is another indicator of potential risk. Components may be on the latest
-available version, but still be 20 years old.
+available version, but still be 20 years old.
-Component age can be evaluated using the `compare_age` [function](#function-definitions). The first function argument
-is a numeric comparator (`<`, `<=`, `=`, `!=`, `>`, `>=`), and the second is a [duration in ISO8601 notation](https://en.wikipedia.org/wiki/ISO_8601#Durations).
-
-The following expression matches [Component]s that are two years old, or even older:
+The following expression matches [Component]s that are two years old, or even older, using the `now`
+variable and timestamp arithmetic:
```js linenums="1"
-component.compare_age(">=", "P2Y")
+has(component.published_at)
+ && component.published_at < now - duration("17520h") // ~2 years
```
### Component blacklist
The following expression matches on the [Component]'s [Package URL], using a regular expression in [RE2] syntax.
-Additionally, it checks whether the [Component]'s version falls into a given [vers] range, consisting of multiple
-constraints.
+Additionally, it checks whether the [Component]'s version falls into a given [vers] range using
+[`matches_range`](#matches_range), consisting of multiple constraints.
```js linenums="1"
component.purl.matches("^pkg:maven/com.acme/acme-lib\\b.*")
@@ -84,22 +89,6 @@ but not:
* `pkg:maven/com.acme/acme-library@0.1.0`
* `pkg:maven/com.acme/acme-lib@0.2.4`
-`matches_range` currently supports the following versioning schemes:
-
-| Versioning Scheme | Ecosystem |
-|:------------------|:---------------------------------|
-| `deb` | Debian / Ubuntu |
-| `generic` | Generic / Any |
-| `golang` | Go |
-| `maven` | Java / Maven |
-| `npm` | JavaScript / NodeJS |
-| `rpm` | CentOS / Fedora / Red Hat / SUSE |
-
-!!! note
- If the ecosystem of the component(s) to match against is known upfront, it's good practice to use the according
- versioning scheme in `matches_range`. This helps with accuracy, as versioning schemes have different nuances
- across ecosystems, which makes comparisons error-prone.
-
### Dependency graph traversal
The following expression matches [Component]s that are a (possibly transitive) dependency of a [Component]
@@ -110,20 +99,47 @@ component.is_dependency_of(v1.Component{name: "foo"})
&& project.depends_on(v1.Component{name: "bar"})
```
-`is_dependency_of` and `depends_on` lookups currently support the following [Component] fields:
+To check whether a component is a *direct* (i.e. non-transitive) dependency:
+
+```js linenums="1"
+component.is_direct_dependency_of(v1.Component{name: "foo"})
+```
+
+To check whether a component is *exclusively* introduced through another component
+(i.e. no other path in the dependency graph leads to it):
+
+```js linenums="1"
+component.is_exclusive_dependency_of(v1.Component{name: "foo"})
+```
+
+Dependency graph functions support the following [Component] fields for matching:
+
+| Field | `re:` prefix | `vers:` prefix |
+|:--------------|:------------:|:--------------:|
+| `uuid` | | |
+| `group` | ✅ | |
+| `name` | ✅ | |
+| `version` | ✅ | ✅ |
+| `classifier` | | |
+| `cpe` | ✅ | |
+| `purl` | ✅ | |
+| `swid_tag_id` | ✅ | |
+| `is_internal` | | |
+
+The `re:` prefix enables [RE2] regex matching on the field value:
+
+```js linenums="1"
+project.depends_on(v1.Component{name: "re:^acme-.*"})
+```
-* `uuid`
-* `group`
-* `name`
-* `version`
-* `classifier`
-* `cpe`
-* `purl`
-* `swid_tag_id`
-* `internal`
+The `vers:` prefix enables [vers] range matching on the `version` field:
-Initially, only exact matches on those fields are supported. In the future, more sophisticated matching options
-will be added.
+```js linenums="1"
+project.depends_on(v1.Component{
+ name: "acme-lib",
+ version: "vers:maven/>1.0.0|<2.0.0"
+})
+```
!!! note
When constructing objects like [Component] on-the-fly, it is necessary to use their version namespace,
@@ -140,7 +156,7 @@ and have either:
```js linenums="1"
!component.is_internal && (
!has(component.resolved_license)
- || component.resolved_license.groups.exisits(licenseGroup,
+ || !component.resolved_license.groups.exists(licenseGroup,
licenseGroup.name == "Permissive")
)
```
@@ -173,164 +189,236 @@ or `CRITICAL`
)
```
-## Reference
-
-### Types
-
-#### `Component`
-
-| Field | Type | Description |
-|:---------------------|:----------------------------|:---------------------------------|
-| `uuid` | `string` | Internal [UUID] |
-| `group` | `string` | Group / namespace |
-| `name` | `string` | Name |
-| `version` | `string` | Version |
-| `classifier` | `string` | Classifier / type |
-| `cpe` | `string` | [CPE] |
-| `purl` | `string` | [Package URL] |
-| `swid_tag_id` | `string` | [SWID] Tag ID |
-| `is_internal` | `bool` | Is internal? |
-| `md5` | `string` | MD5 hash |
-| `sha1` | `string` | SHA1 hash |
-| `sha256` | `string` | SHA256 hash |
-| `sha384` | `string` | SHA384 hash |
-| `sha512` | `string` | SHA512 hash |
-| `sha3_256` | `string` | SHA3-256 hash |
-| `sha3_384` | `string` | SHA3-384 hash |
-| `sha3_512` | `string` | SHA3-512 hash |
-| `blake2b_256` | `string` | BLAKE2b-256 hash |
-| `blake2b_384` | `string` | BLAKE2b-384 hash |
-| `blake2b_512` | `string` | BLAKE2b-512 hash |
-| `blake3` | `string` | BLAKE3 hash |
-| `license_name` | `string` | License name (if unresolved) |
-| `license_expression` | `string` | [SPDX license expression] |
-| `resolved_license` | [License] | Resolved license |
-| `published_at` | `google.protobuf.Timestamp` | When the component was published |
-| `latest_version` | `string` | Latest known version |
-
-#### `License`
-
-| Field | Type | Description |
-|:-------------------|:-----------------------------------|:-------------------------------------------------|
-| `uuid` | `string` | Internal [UUID] |
-| `id` | `string` | SPDX license ID |
-| `name` | `string` | License name |
-| `groups` | list([License.Group]) | Groups this license is included in |
-| `is_osi_approved` | `bool` | Is [OSI-approved]? |
-| `is_fsf_libre` | `bool` | Is included in [FSF license list]? |
-| `is_deprecated_id` | `bool` | Uses a deprecated SPDX license ID? |
-| `is_custom` | `bool` | Is custom / not included in [SPDX license list]? |
-
-#### `License.Group`
-
-| Field | Type | Description |
-|:-------|:---------|:----------------|
-| `uuid` | `string` | Internal [UUID] |
-| `name` | `string` | Group name |
-
-#### `Project`
-
-| Field | Type | Description |
-|:------------------|:--------------------------------------|:------------------|
-| `uuid` | `string` | Internal [UUID] |
-| `group` | `string` | Group / namespace |
-| `name` | `string` | Name |
-| `version` | `string` | Version |
-| `classifier` | `string` | Classifier / type |
-| `is_active` | `bool` | Is active? |
-| `tags` | `list(string)` | Tags |
-| `properties` | list([Project.Property]) | Properties |
-| `cpe` | `string` | [CPE] |
-| `purl` | `string` | [Package URL] |
-| `swid_tag_id` | `string` | [SWID] Tag ID |
-| `last_bom_import` | `google.protobuf.Timestamp` | |
-
-#### `Project.Property`
-
-| Field | Type | Description |
-|:--------|:---------|:------------|
-| `group` | `string` | |
-| `name` | `string` | |
-| `value` | `string` | |
-| `type` | `string` | |
-
-#### `Vulnerability`
-
-| Field | Type | Description |
-|:----------------------------------|:-----------------------------------------|:-------------------------------------------|
-| `uuid` | `string` | Internal [UUID] |
-| `id` | `string` | ID of the vulnerability (e.g. `CVE-123`) |
-| `source` | `string` | Authoritative source (e.g. `NVD`) |
-| `aliases` | list([Vulnerability.Alias]) | Known aliases |
-| `cwes` | `list(int)` | [CWE] IDs |
-| `created` | `google.protobuf.Timestamp` | When the vulnerability was created |
-| `published` | `google.protobuf.Timestamp` | When the vulnerability was published |
-| `updated` | `google.protobuf.Timestamp` | Then the vulnerability was updated |
-| `severity` | `string` | |
-| `cvssv2_base_score` | `double` | [CVSSv2] base score |
-| `cvssv2_impact_subscore` | `double` | [CVSSv2] impact sub score |
-| `cvssv2_exploitability_subscore` | `double` | [CVSSv2] exploitability sub score |
-| `cvssv2_vector` | `string` | [CVSSv2] vector |
-| `cvssv3_base_score` | `double` | [CVSSv3] base score |
-| `cvssv3_impact_subscore` | `double` | [CVSSv3] impact sub score |
-| `cvssv3_exploitability_subscore` | `double` | [CVSSv3] exploitability sub score |
-| `cvssv3_vector` | `string` | [CVSSv3] vector |
-| `cvssv4_score` | `double` | [CVSSv4] score |
-| `cvssv4_vector` | `string` | [CVSSv4] vector |
-| `owasp_rr_likelihood_score` | `double` | [OWASP Risk Rating] likelihood score |
-| `owasp_rr_technical_impact_score` | `double` | [OWASP Risk Rating] technical impact score |
-| `owasp_rr_business_impact_score` | `double` | [OWASP Risk Rating] business impact score |
-| `owasp_rr_vector` | `string` | [OWASP Risk Rating] vector |
-| `epss_score` | `double` | [EPSS] score |
-| `epss_percentile` | `double` | [EPSS] percentile |
-
-#### `Vulnerability.Alias`
-
-| Field | Type | Description |
-|:---------|:---------|:------------------------------------------|
-| `id` | `string` | ID of the vulnerability (e.g. `GHSA-123`) |
-| `source` | `string` | Authoritative source (e.g. `GITHUB`) |
-
-### Function Definitions
-
-In addition to the [standard definitions] of the CEL specification, Dependency-Track offers additional functions
-to unlock even more use cases:
-
-| Symbol | Type | Description |
-|:---------------------------|:--------------------------------------------------------------------------------------------|:--------------------------------------------------------------|
-| `depends_on` | ([Project], [Component]) -> `bool` | Check if `Project` depends on `Component` |
-| `compare_age` | ([Component], string, string) -> `bool` | Check if a `Component`'s age matches a given duration |
-| `is_dependency_of` | ([Component], [Component]) -> `bool` | Check if a `Component` is a dependency of another `Component` |
-| `matches_range` | ([Project], string) -> `bool`
([Component], string) -> `bool` | Check if a `Project` or `Component` matches a [vers] range |
-| `matches_version_distance` | ([Component], string, string) -> `bool` | Check if a `Component`'s version matches a given distance |
+### Version distance
+
+The [`version_distance`](#version_distance) function allows matching based on how far behind a component's version
+is from the latest known version. The distance is specified using a [VersionDistance] object with `epoch`, `major`,
+`minor`, and `patch` fields.
+
+The following expression matches components that are more than one major version behind:
+
+```js linenums="1"
+component.version_distance(">=", v1.VersionDistance{major: 1})
+```
+
+### Optional field checking
+
+CEL does not have a concept of `null`. Accessing a field that is not set
+returns its default value (e.g. `""` for strings, `0` for numbers, `false` for booleans), which can
+lead to misleading matches. Use the `has()` macro to check for field presence before accessing it:
+
+```js linenums="1"
+has(component.published_at)
+ && component.published_at < now - duration("8760h")
+```
+
+```js linenums="1"
+has(project.metadata) && has(project.metadata.tools)
+```
+
+## Function Reference
+
+For type definitions, refer to the [schema reference](../../reference/schemas/policy.md).
+
+In addition to the [standard definitions] of the CEL specification, Dependency-Track offers the following functions.
+
+### `depends_on`
+
+Checks whether a [Project] contains a [Component] matching the given criteria.
+Useful for enforcing policies only when specific components are present in a project.
+
+| Name | Type | Description |
+|:------------|:------------|:----------------------------------------------------------------|
+| *receiver* | [Project] | The project to check |
+| `component` | [Component] | Criteria to match against. Supports `re:` and `vers:` prefixes. |
+
+**Returns:** `true` if a matching component exists in the project.
+
+```js linenums="1"
+project.depends_on(v1.Component{name: "baz"})
+```
+
+```mermaid
+graph TD
+ P["Project"]:::match --> A["foo"]
+ P --> B["bar"]
+ A --> C["baz"]:::criteria
+ A --> D["qux"]
+ B --> D
+ classDef match stroke:#22c55e,stroke-width:3
+ classDef criteria fill:#22c55e,color:#fff
+```
+
+!!! tip
+ `project` matches because `baz` exists in its dependency graph.
+
+### `is_dependency_of`
+
+Checks whether a [Component] is a (possibly transitive) dependency of another [Component].
+
+| Name | Type | Description |
+|:------------|:------------|:---------------------------------------------------------------------------|
+| *receiver* | [Component] | The component being evaluated |
+| `component` | [Component] | Criteria to match the parent against. Supports `re:` and `vers:` prefixes. |
+
+**Returns:** `true` if the receiver is a dependency (direct or transitive) of a matching component.
+
+```js linenums="1"
+component.is_dependency_of(v1.Component{name: "foo"})
+```
+
+```mermaid
+graph TD
+ P["Project"] --> A["foo"]:::criteria
+ P --> B["bar"]
+ A --> C["baz"]:::match
+ A --> D["qux"]:::match
+ B --> D
+ classDef match stroke:#22c55e,stroke-width:3
+ classDef criteria fill:#22c55e,color:#fff
+```
+
+!!! tip
+ `baz` and `qux` match since both are dependencies of `foo`.
+ `qux` matches even though it's also reachable through `bar`.
+
+### `is_direct_dependency_of`
+
+Checks whether a [Component] is a *direct* (non-transitive) dependency of another [Component].
+
+| Name | Type | Description |
+|:------------|:------------|:---------------------------------------------------------------------------|
+| *receiver* | [Component] | The component being evaluated |
+| `component` | [Component] | Criteria to match the parent against. Supports `re:` and `vers:` prefixes. |
+
+**Returns:** `true` if the receiver is a direct dependency of a matching component.
+
+```js linenums="1"
+component.is_direct_dependency_of(v1.Component{name: "foo"})
+```
+
+```mermaid
+graph TD
+ P["Project"] --> A["foo"]:::criteria
+ P --> B["bar"]
+ A --> C["baz"]:::match
+ A --> D["qux"]:::match
+ B --> D
+ classDef match stroke:#22c55e,stroke-width:3
+ classDef criteria fill:#22c55e,color:#fff
+```
+
+!!! tip
+ `baz` and `qux` match since both are direct children of `foo`.
+
+With a deeper graph, transitive dependencies do **not** match:
+
+```mermaid
+graph TD
+ A["foo"]:::criteria --> C["baz"]:::match
+ C --> E["deep"]:::nomatch
+ classDef match stroke:#22c55e,stroke-width:3
+ classDef criteria fill:#22c55e,color:#fff
+ classDef nomatch stroke:#ef4444,stroke-width:3,stroke-dasharray:5
+```
+
+!!! tip
+ `baz` matches since it's a direct child of `foo`.
+ `deep` does **not** match, as it's a transitive dependency.
+
+### `is_exclusive_dependency_of`
+
+Checks whether a [Component] is *exclusively* introduced through another [Component].
+Returns `true` only if every path from the project root to the receiver passes through a matching component.
+
+| Name | Type | Description |
+|:------------|:------------|:---------------------------------------------------------------------------|
+| *receiver* | [Component] | The component being evaluated |
+| `component` | [Component] | Criteria to match the parent against. Supports `re:` and `vers:` prefixes. |
+
+**Returns:** `true` if the receiver is exclusively introduced through a matching component.
+
+```js linenums="1"
+component.is_exclusive_dependency_of(v1.Component{name: "foo"})
+```
+
+```mermaid
+graph TD
+ P["Project"] --> A["foo"]:::criteria
+ P --> B["bar"]
+ A --> C["baz"]:::match
+ A --> D["qux"]:::nomatch
+ B --> D
+ classDef match stroke:#22c55e,stroke-width:3
+ classDef criteria fill:#22c55e,color:#fff
+ classDef nomatch stroke:#ef4444,stroke-width:3,stroke-dasharray:5
+```
+
+!!! tip
+ `baz` matches since it's only reachable through `foo`.
+ `qux` does **not** match, as it's also reachable through `bar`.
+
+### `matches_range`
+
+Checks whether a [Component]'s or [Project]'s version falls within a [vers] range.
+
+| Name | Type | Description |
+|:-----------|:-------------------------|:-----------------------------------------------------------|
+| *receiver* | [Component] or [Project] | The component or project to check |
+| `range` | `string` | A [vers] range string (e.g. `"vers:maven/>1.0.0\|<2.0.0"`) |
+
+**Returns:** `true` if the version is within the specified range.
+
+```js linenums="1"
+component.matches_range("vers:maven/>0|<1|!=0.2.4")
+```
+
+Currently supported versioning schemes:
+
+| Versioning Scheme | Ecosystem |
+|:------------------|:---------------------------------|
+| `deb` | Debian / Ubuntu |
+| `generic` | Generic / Any |
+| `golang` | Go |
+| `maven` | Java / Maven |
+| `npm` | JavaScript / NodeJS |
+| `rpm` | CentOS / Fedora / Red Hat / SUSE |
+
+!!! note
+ If the ecosystem of the component(s) to match against is known upfront, it's good practice to use the according
+ versioning scheme in `matches_range`. This helps with accuracy, as versioning schemes have different nuances
+ across ecosystems, which makes comparisons error-prone.
+
+### `version_distance`
+
+Checks whether the distance between a [Component]'s current version and its latest known version
+matches a given [VersionDistance].
+
+| Name | Type | Description |
+|:-----------|:------------------|:----------------------------------------------------|
+| *receiver* | [Component] | The component to check |
+| `operator` | `string` | Numeric comparator: `<`, `<=`, `=`, `!=`, `>`, `>=` |
+| `distance` | [VersionDistance] | The version distance to compare against |
+
+**Returns:** `true` if the version distance satisfies the comparison.
+
+```js linenums="1"
+component.version_distance(">=", v1.VersionDistance{major: 1})
+```
[C-style languages]: https://en.wikipedia.org/wiki/List_of_C-family_programming_languages
-[CVSSv2]: https://www.first.org/cvss/v2/guide
[CVSSv3]: https://www.first.org/cvss/v3.0/specification-document
-[CVSSv4]: https://www.first.org/cvss/v4.0/specification-document
-[CPE]: https://csrc.nist.gov/projects/security-content-automation-protocol/specifications/cpe
-[CWE]: https://cwe.mitre.org/
[Common Expression Language]: https://cel.dev/
-[Component]: #component
-[EPSS]: https://www.first.org/epss/
-[FSF license list]: https://www.gnu.org/licenses/license-list.en.html
-[License.Group]: #licensegroup
-[License]: #license
-[OSI-approved]: https://opensource.org/licenses
-[OWASP Risk Rating]: https://owasp.org/www-community/OWASP_Risk_Rating_Methodology
+[Component]: ../../reference/schemas/policy.md#component
+[License]: ../../reference/schemas/policy.md#license
[Package URL]: https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst
-[Project.Property]: #projectproperty
-[Project]: #project
+[Project]: ../../reference/schemas/policy.md#project
[RE2]: https://github.com/google/re2/wiki/Syntax
-[SPDX license expression]: https://spdx.github.io/spdx-spec/v2-draft/SPDX-license-expressions/
-[SPDX license list]: https://spdx.org/licenses/
[Turing-complete]: https://en.wikipedia.org/wiki/Turing_completeness
-[SWID]: https://csrc.nist.gov/projects/Software-Identification-SWID
-[UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier
-[Vulnerability.Alias]: #vulnerabilityalias
-[Vulnerability]: #vulnerability
-[introduction]: https://github.com/google/cel-spec/blob/v0.13.0/doc/intro.md
+[VersionDistance]: ../../reference/schemas/policy.md#versiondistance
+[Vulnerability]: ../../reference/schemas/policy.md#vulnerability
[language definition]: https://github.com/google/cel-spec/blob/v0.13.0/doc/langdef.md#language-definition
[macros]: https://github.com/google/cel-spec/blob/v0.13.0/doc/langdef.md#macros
+[protobuf-ts-docs]: https://protobuf.dev/reference/protobuf/google.protobuf/#timestamp
[standard definitions]: https://github.com/google/cel-spec/blob/v0.13.0/doc/langdef.md#list-of-standard-definitions
-[vers]: https://github.com/package-url/purl-spec/blob/version-range-spec/VERSION-RANGE-SPEC.rst
\ No newline at end of file
+[vers]: https://github.com/package-url/vers-spec
\ No newline at end of file