|
| 1 | +| Status | Date | Author(s) | |
| 2 | +|:---------|:-----------|:-------------------------------------| |
| 3 | +| Accepted | 2026-04-17 | [@nscuro](https://github.com/nscuro) | |
| 4 | + |
| 5 | +## Context |
| 6 | + |
| 7 | +Users have long requested the ability to filter notifications based on their content. For example, an organization |
| 8 | +may only want to receive notifications for newly discovered vulnerabilities of `CRITICAL` or `HIGH` severity, |
| 9 | +or only for components matching a certain name pattern. The existing notification rule model supports filtering |
| 10 | +by project and tags, but has no mechanism for filtering by the properties of the notification itself. |
| 11 | + |
| 12 | +One approach would be to add dedicated filter fields to notification rules, such as a severity threshold dropdown |
| 13 | +or a component name input. This does not scale. Every new filter would require a schema migration, API changes, |
| 14 | +and corresponding UI work. As the number of filterable properties grows, the rule model and UI become increasingly |
| 15 | +complex and rigid. |
| 16 | + |
| 17 | +Another approach would be to extend the existing project and tag filters to cover content-level properties. |
| 18 | +This would conflate two fundamentally different concerns. Project and tag filters control which projects a rule applies |
| 19 | +to. Content filters control which notifications within those projects are relevant. Mixing the two makes the |
| 20 | +filtering logic harder to reason about and harder to extend. |
| 21 | + |
| 22 | +What is needed is a mechanism that is expressive enough to cover arbitrary notification properties, does not |
| 23 | +require code changes when new properties are added, and is safe to evaluate in a high-throughput notification |
| 24 | +pipeline. |
| 25 | + |
| 26 | +## Decision |
| 27 | + |
| 28 | +We will allow users to attach a [CEL] (Common Expression Language) filter expression to notification rules. |
| 29 | +When a notification matches a rule, the filter expression is evaluated against the notification. |
| 30 | +If it evaluates to `true`, the notification is dispatched. If it evaluates to `false`, it is suppressed. |
| 31 | + |
| 32 | +CEL is a natural fit for this use case. Dependency-Track's notifications are defined as [Protocol Buffer] messages, |
| 33 | +and CEL has native support for Protobuf types. This means that all fields of a notification and its subject are |
| 34 | +automatically available in expressions without any additional mapping or configuration. When new fields are added |
| 35 | +to the notification schema, they become available in filter expressions immediately. CEL is already used in the |
| 36 | +project for vulnerability policies, so it is a known quantity in terms of integration and operational behavior. |
| 37 | +The language is designed to be fast, safe (no side effects, no I/O, no unbounded loops), and well-specified. |
| 38 | + |
| 39 | +The following variables are available in filter expressions: |
| 40 | + |
| 41 | +- `level`, `scope`, `group` for the notification's level, scope, and group (as integer enum values, with named constants like `Level.LEVEL_INFORMATIONAL`) |
| 42 | +- `title` and `content` for the notification's title and content strings |
| 43 | +- `timestamp` for the notification's timestamp |
| 44 | +- `subject` for the notification's subject, typed according to the notification group (e.g. `NewVulnerabilitySubject` for `GROUP_NEW_VULNERABILITY`) |
| 45 | + |
| 46 | +Example expressions: |
| 47 | + |
| 48 | +- `subject.vulnerability.severity == "CRITICAL"` to only receive critical vulnerabilities. |
| 49 | +- `subject.project.name.startsWith("acme-")` to filter by project name prefix. |
| 50 | +- `"CRITICAL" in subject.overview.new_vulnerabilities_count_by_severity` to filter scheduled vulnerability summaries. |
| 51 | + |
| 52 | +Filter expressions are optional. Rules without a filter expression behave exactly as before, preserving full |
| 53 | +backward compatibility. |
| 54 | + |
| 55 | +Expressions are validated at save time. When a user creates or updates a notification rule with a filter |
| 56 | +expression, the expression is compiled and checked against the CEL type environment. If the expression is |
| 57 | +invalid, the API returns an [RFC 9457] problem details response with precise error locations (line, column, |
| 58 | +message). This fail-fast approach prevents users from saving expressions that would never evaluate successfully. |
| 59 | + |
| 60 | +At dispatch time, if a filter expression fails to evaluate due to a runtime error, the rule matches the |
| 61 | +notification. This fail-open strategy ensures that a misconfigured expression causes over-notification rather |
| 62 | +than silent suppression. For a security system, failing to deliver a notification is worse than delivering |
| 63 | +one too many. |
| 64 | + |
| 65 | +Project and tag filtering is applied before filter expression evaluation. This ordering ensures that the cheaper |
| 66 | +filter is executed first, and that project-level access restrictions are enforced regardless of what the |
| 67 | +expression says. |
| 68 | + |
| 69 | +Compiled CEL programs are cached in memory (up to 256 entries with a TTL of one hour) to avoid repeated compilation |
| 70 | +of the same expression across notification dispatches. The cache is keyed by expression text, so updating an |
| 71 | +expression naturally invalidates the previous cache entry. |
| 72 | + |
| 73 | +## Consequences |
| 74 | + |
| 75 | +Users can now filter notifications by any property of the notification or its subject without requiring code |
| 76 | +changes, schema migrations, or UI updates. This covers the use cases described in |
| 77 | +[DependencyTrack/dependency-track#3767](https://github.com/DependencyTrack/dependency-track/issues/3767) |
| 78 | +and similar requests. |
| 79 | + |
| 80 | +Filter expressions are a power-user feature. Users need to understand CEL syntax and the structure of |
| 81 | +notification Protobuf messages to write effective expressions. Documentation and example expressions will be |
| 82 | +important for adoption. |
| 83 | + |
| 84 | +The fail-open strategy means that broken expressions will not silently suppress notifications. Operators will |
| 85 | +see warnings in logs when expressions fail to evaluate, giving them the opportunity to fix the expression |
| 86 | +without missing notifications in the meantime. |
| 87 | + |
| 88 | +The expression length is capped at 2048 characters to prevent abuse, which is sufficient for any reasonable |
| 89 | +filter condition. |
| 90 | + |
| 91 | +[CEL]: https://cel.dev |
| 92 | +[Protocol Buffer]: https://protobuf.dev |
| 93 | +[RFC 9457]: https://www.rfc-editor.org/rfc/rfc9457 |
0 commit comments