diff --git a/docs/usage/notifications/filter-expressions.md b/docs/usage/notifications/filter-expressions.md new file mode 100644 index 000000000..eb7c10d9c --- /dev/null +++ b/docs/usage/notifications/filter-expressions.md @@ -0,0 +1,181 @@ +## Introduction + +Alerts can include a filter expression to control which notifications are dispatched +based on their content. Filter expressions are written in [CEL] (Common Expression Language), +the same language used for [policy compliance expressions](../policy-compliance/expressions.md). + +Without a filter expression, an alert matches all notifications that satisfy its scope, group, +level, and project or tag restrictions. A filter expression adds a further condition: the +notification is only dispatched when the expression evaluates to `true`. + +![Filter expression field in the alert editor](./images/filter-expression-editor.png) + +## Syntax + +Filter expressions use the same [CEL syntax](../policy-compliance/expressions.md#syntax) as +policy compliance expressions. CEL is not [Turing-complete] and does not support constructs +like `if` statements or loops. It compensates for this with [macros] like `all`, `exists`, +`exists_one`, `map`, and `filter`. + +Refer to the official [language definition] for a thorough description of the syntax. + +## Evaluation Context + +The context in which filter expressions are evaluated contains the following variables: + +| Variable | Type | Description | +|:------------|:-------------------------------------------------------------|:---------------------------------------------------------------------------------------| +| `level` | [`Level`](../../reference/schemas/notification.md#level) | The notification level, as an integer enum value. Use named constants (see below). | +| `scope` | [`Scope`](../../reference/schemas/notification.md#scope) | The notification scope, as an integer enum value. Use named constants (see below). | +| `group` | [`Group`](../../reference/schemas/notification.md#group) | The notification group, as an integer enum value. Use named constants (see below). | +| `title` | `string` | The notification title. | +| `content` | `string` | The notification content. | +| `timestamp` | [`google.protobuf.Timestamp`][protobuf-ts-docs] | The time at which the notification was created. | +| `subject` | dynamic | The notification subject, typed according to the notification group (see below). | + +### Enum Constants + +The `level`, `scope`, and `group` variables hold integer values. To compare them in a readable way, +use the named constants from the [notification schema](../../reference/schemas/notification.md#enums): + +```js +level == Level.LEVEL_INFORMATIONAL +``` + +```js +group == Group.GROUP_NEW_VULNERABILITY +``` + +```js +scope == Scope.SCOPE_PORTFOLIO +``` + +### Subject Types + +The `subject` variable holds the notification's subject, which varies depending on the notification +group. Refer to the [notification schema reference](../../reference/schemas/notification.md#subjects) +for full details of each subject type and its fields. + +| Group | Subject Type | +|:--------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------| +| `BOM_CONSUMED`, `BOM_PROCESSED` | [BomConsumedOrProcessedSubject](../../reference/schemas/notification.md#bomconsumedorprocessedsubject) | +| `BOM_PROCESSING_FAILED` | [BomProcessingFailedSubject](../../reference/schemas/notification.md#bomprocessingfailedsubject) | +| `BOM_VALIDATION_FAILED` | [BomValidationFailedSubject](../../reference/schemas/notification.md#bomvalidationfailedsubject) | +| `NEW_VULNERABILITY` | [NewVulnerabilitySubject](../../reference/schemas/notification.md#newvulnerabilitysubject) | +| `NEW_VULNERABLE_DEPENDENCY` | [NewVulnerableDependencySubject](../../reference/schemas/notification.md#newvulnerabledependencysubject) | +| `POLICY_VIOLATION` | [PolicyViolationSubject](../../reference/schemas/notification.md#policyviolationsubject) | +| `PROJECT_AUDIT_CHANGE` | [VulnerabilityAnalysisDecisionChangeSubject](../../reference/schemas/notification.md#vulnerabilityanalysisdecisionchangesubject) or [PolicyViolationAnalysisDecisionChangeSubject](../../reference/schemas/notification.md#policyviolationanalysisdecisionchangesubject) | +| `PROJECT_VULN_ANALYSIS_COMPLETE` | [ProjectVulnAnalysisCompleteSubject](../../reference/schemas/notification.md#projectvulnanalysiscompletesubject) | +| `VEX_CONSUMED`, `VEX_PROCESSED` | [VexConsumedOrProcessedSubject](../../reference/schemas/notification.md#vexconsumedorprocessedsubject) | +| `USER_CREATED`, `USER_DELETED` | [UserSubject](../../reference/schemas/notification.md#usersubject) | + +## Validation + +Filter expressions are validated when an alert is saved. If the expression contains syntax errors +or references types that do not exist, the save operation will fail and the errors will be reported +with their exact location (line and column). + +![Validation error for an invalid filter expression](./images/filter-expression-editor-error.png) + +The maximum length of a filter expression is 2048 characters. + +## Failure Behaviour + +If a filter expression fails to evaluate at dispatch time (for example due to accessing a field +that does not exist on the subject), the alert matches the notification regardless. +This "fail-open" strategy ensures that a broken expression causes over-notification rather than +silently suppressing notifications. Evaluation failures are logged as warnings. + +!!! warning + An alert with an expression that consistently fails will behave as though + it has no filter expression at all. Check the application logs for evaluation warnings + if an alert appears to match more broadly than expected. + +## Filtering Order + +When a notification is dispatched, Dependency-Track evaluates the following filters in order: + +1. **Scope, group, and level** matching (configured on the alert). +2. **Project and tag restrictions** (if the alert is limited to specific projects or tags). +3. **Filter expression** (if the alert has one). + +The filter expression is only evaluated when the notification has already passed the preceding +checks. This means that project and tag restrictions are always enforced, regardless of what the +expression contains. + +## Examples + +### Only critical and high severity vulnerabilities + +The following expression matches `NEW_VULNERABILITY` notifications where the vulnerability +severity is `CRITICAL` or `HIGH`: + +```js linenums="1" +subject.vulnerability.severity in ["CRITICAL", "HIGH"] +``` + +### Vulnerabilities with a CVSS v3 score above a threshold + +```js linenums="1" +subject.vulnerability.cvss_v3 >= 7.0 +``` + +### Notifications for projects matching a name prefix + +The following expression matches notifications whose subject contains a project with a name +starting with `acme-`: + +```js linenums="1" +subject.project.name.startsWith("acme-") +``` + +!!! tip + For simple project-based filtering, consider using project and tag restrictions on the + alert instead. Filter expressions are more useful for content-based conditions + that cannot be expressed through project or tag restrictions alone. + +### Vulnerabilities with a specific CWE + +The following expression matches `NEW_VULNERABILITY` notifications where the vulnerability +has CWE-79 (Cross-site Scripting) among its CWEs: + +```js linenums="1" +subject.vulnerability.cwes.exists(cwe, cwe.cwe_id == 79) +``` + +### Combining multiple conditions + +The following expression matches `NEW_VULNERABILITY` notifications for `CRITICAL` vulnerabilities +with a network attack vector in CVSSv3: + +```js linenums="1" +subject.vulnerability.severity == "CRITICAL" + && subject.vulnerability.cvss_v3_vector.matches(".*/AV:N/.*") +``` + +### Scheduled alerts: only when critical vulnerabilities were found + +For scheduled alerts that produce vulnerability summaries, the subject contains +an overview with vulnerability counts grouped by severity. The following expression matches +only when at least one `CRITICAL` vulnerability was found in the reporting period: + +```js linenums="1" +"CRITICAL" in subject.overview.new_vulnerabilities_count_by_severity +``` + +### 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), which can lead to misleading matches. +Use the `has()` macro to check for field presence before accessing it: + +```js linenums="1" +has(subject.vulnerability.cvss_v3_vector) + && subject.vulnerability.cvss_v3_vector.matches(".*/AV:N/.*") +``` + +[CEL]: https://cel.dev/ +[Turing-complete]: https://en.wikipedia.org/wiki/Turing_completeness +[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 \ No newline at end of file diff --git a/docs/usage/notifications/images/filter-expression-editor-error.png b/docs/usage/notifications/images/filter-expression-editor-error.png new file mode 100644 index 000000000..1625e1e34 Binary files /dev/null and b/docs/usage/notifications/images/filter-expression-editor-error.png differ diff --git a/docs/usage/notifications/images/filter-expression-editor.png b/docs/usage/notifications/images/filter-expression-editor.png new file mode 100644 index 000000000..229c318a7 Binary files /dev/null and b/docs/usage/notifications/images/filter-expression-editor.png differ diff --git a/docs/usage/notifications/overview.md b/docs/usage/notifications/overview.md index b9148a6e1..28bd32272 100644 --- a/docs/usage/notifications/overview.md +++ b/docs/usage/notifications/overview.md @@ -8,7 +8,14 @@ in the platform. ### Alerts -Alerts, a.k.a. *notification rules*, are configurations that specify +Alerts, a.k.a. *notification rules*, are configurations that specify which notifications +are sent to which destinations. An alert defines the scope, groups, and level of notifications +it is interested in, and optionally restricts matching to specific projects or tags. + +Alerts can further be refined with a *filter expression*, written in [CEL], that evaluates +against the content of each notification. This allows filtering by properties such as +vulnerability severity, CVSS score, or component name, without requiring dedicated UI controls +for each filter criterion. Refer to [Filter Expressions](filter-expressions.md) for details. ### Publishers @@ -69,3 +76,5 @@ A group is a granular classification of notification subjects within a [scope](# | PORTFOLIO | BOM_PROCESSING_FAILED | Error | Notifications generated whenever a BOM upload process fails | | PORTFOLIO | BOM_VALIDATION_FAILED | Error | Notifications generated whenever an invalid BOM is uploaded | | PORTFOLIO | POLICY_VIOLATION | Informational | Notifications generated whenever a policy violation is identified | + +[CEL]: https://cel.dev/ diff --git a/mkdocs.yml b/mkdocs.yml index 7252cdf0e..fd16dbdfe 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -93,6 +93,7 @@ nav: - Notifications: - Overview: usage/notifications/overview.md - Publishers: usage/notifications/publishers.md + - Filter Expressions: usage/notifications/filter-expressions.md - Templating: usage/notifications/templating.md - Architecture: - Overview: architecture/index.md diff --git a/proto/src/main/proto/org/dependencytrack/notification/v1/notification.proto b/proto/src/main/proto/org/dependencytrack/notification/v1/notification.proto index 2ba275581..d6e45a954 100644 --- a/proto/src/main/proto/org/dependencytrack/notification/v1/notification.proto +++ b/proto/src/main/proto/org/dependencytrack/notification/v1/notification.proto @@ -6,7 +6,7 @@ import "google/protobuf/any.proto"; import "google/protobuf/timestamp.proto"; option java_multiple_files = true; -option java_package = "org.dependencytrack.proto.notification.v1"; +option java_package = "org.dependencytrack.notification.proto.v1"; message Notification { Level level = 1; @@ -57,6 +57,16 @@ enum Group { GROUP_USER_DELETED = 20; GROUP_BOM_VALIDATION_FAILED = 21; + // A previously identified vulnerability is no longer applicable, + // e.g. due to upstream sources correcting their data. + GROUP_VULNERABILITY_RETRACTED = 22; + + // Scheduled summary of new vulnerabilities across projects. + GROUP_NEW_VULNERABILITIES_SUMMARY = 23; + + // Scheduled summary of new policy violations across projects. + GROUP_NEW_POLICY_VIOLATIONS_SUMMARY = 24; + // Indexing service has been removed as of // https://github.com/DependencyTrack/hyades/issues/661 reserved 5; @@ -101,13 +111,16 @@ message NewVulnerabilitySubject { Project project = 2; Vulnerability vulnerability = 3; BackReference affected_projects_reference = 4; - optional string vulnerability_analysis_level = 5; + optional string vulnerability_analysis_level = 5 [deprecated = true]; // List of projects affected by the vulnerability. // DEPRECATED: This list only holds one item, and it is identical to the one in the project field. // The field is kept for backward compatibility of JSON notifications, but consumers should not expect multiple projects here. // Transmitting all affected projects in one notification is not feasible for large portfolios, // see https://github.com/DependencyTrack/hyades/issues/467 for details. repeated Project affected_projects = 6 [deprecated = true]; + + // The trigger of the analysis that identified the vulnerability. + AnalysisTrigger analysis_trigger = 7; } message NewVulnerableDependencySubject { @@ -257,3 +270,106 @@ message PolicyViolationAnalysis { optional string state = 4; optional bool suppressed = 5; } + +enum AnalysisTrigger { + // No trigger specified. + ANALYSIS_TRIGGER_UNSPECIFIED = 0; + + // The analysis was triggered by a BOM upload. + ANALYSIS_TRIGGER_BOM_UPLOAD = 1; + + // The analysis was triggered by a schedule. + ANALYSIS_TRIGGER_SCHEDULE = 2; + + // The analysis was triggered manually. + ANALYSIS_TRIGGER_MANUAL = 3; +} + +message VulnerabilityRetractedSubject { + // The component for which the vulnerability was previously reported. + Component component = 1; + + // The project for which the vulnerability was previously reported. + Project project = 2; + + // The previously reported vulnerability. + Vulnerability vulnerability = 3; +} + +// Subject for GROUP_NEW_VULNERABILITIES_SUMMARY notifications. +message NewVulnerabilitiesSummarySubject { + Overview overview = 1; + repeated ProjectSummaryEntry project_summaries = 2; + repeated ProjectFindingsEntry findings_by_project = 3; + google.protobuf.Timestamp since = 4; + + message Overview { + int32 affected_projects_count = 1; + int32 affected_components_count = 2; + int32 new_vulnerabilities_count = 3; + map new_vulnerabilities_count_by_severity = 4; + int32 suppressed_new_vulnerabilities_count = 5; + int32 total_new_vulnerabilities_count = 6; + } + + message ProjectSummaryEntry { + Project project = 1; + map new_vulnerabilities_count_by_severity = 2; + map suppressed_new_vulnerabilities_count_by_severity = 3; + map total_new_vulnerabilities_count_by_severity = 4; + } + + message ProjectFindingsEntry { + Project project = 1; + repeated Finding findings = 2; + } + + message Finding { + Component component = 1; + Vulnerability vulnerability = 2; + optional string analyzer_identity = 3; + google.protobuf.Timestamp attributed_on = 4; + optional string reference_url = 5; + optional string analysis_state = 6; + bool suppressed = 7; + } +} + +// Subject for GROUP_NEW_POLICY_VIOLATIONS_SUMMARY notifications. +message NewPolicyViolationsSummarySubject { + Overview overview = 1; + repeated ProjectSummaryEntry project_summaries = 2; + repeated ProjectViolationsEntry violations_by_project = 3; + google.protobuf.Timestamp since = 4; + + message Overview { + int32 affected_projects_count = 1; + int32 affected_components_count = 2; + int32 new_violations_count = 3; + map new_violations_count_by_type = 4; + int32 suppressed_new_violations_count = 5; + int32 total_new_violations_count = 6; + } + + message ProjectSummaryEntry { + Project project = 1; + map new_violations_count_by_type = 2; + map suppressed_new_violations_count_by_type = 3; + map total_new_violations_count_by_type = 4; + } + + message ProjectViolationsEntry { + Project project = 1; + repeated Violation violations = 2; + } + + message Violation { + string uuid = 1; + Component component = 2; + PolicyCondition policy_condition = 3; + string type = 4; + google.protobuf.Timestamp timestamp = 5; + optional string analysis_state = 6; + bool suppressed = 7; + } +} \ No newline at end of file