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