skip to content

Alert Filters

A filter matches raised alerts on rule, URL, parameter or evidence and rewrites their score, so recurring noise stops driving a build. Interviewers probe whether you suppress deliberately or blindly.

on this pageshow

questions

5

Which fields does a ZAP alertFilter job match an alert on, and how is each compared?

level: middleimportance: must knowfreq 55%

answer

  1. two identifiers, one equality check
  2. four optional string clauses
  3. each clause has its own regex flag
  4. an empty clause matches anything
  5. the regex must match end to end

basics

~20 s

The ruleId is compared for equality against the alert's scan-rule id and against its alertRef. The url, parameter, attack and evidence clauses are exact strings unless their own regex flag is set, and methods is a set. Any clause you omit matches everything.

solid answer

~40 s

A filter first checks `ruleId`, which is plain string equality tried against two things: the alert's numeric scan-rule id and the alert's `alertRef`. It is never a regex and there is no wildcard. Then come four optional clauses — `url`, `parameter`, `attack` and `evidence` — each compared as a literal string unless its partner boolean (`urlRegex`, `parameterRegex`, `attackRegex`, `evidenceRegex`) is true, in which case it is a **full match** against the whole value, not a search. Last is `methods`, a list that is uppercased and, if non-empty, must contain the alert's method. Every clause you leave out or leave empty matches anything, so a filter carrying only a `ruleId` silences that rule everywhere.

code

yaml · 8 lines
yaml
- type: alertFilter
  alertFilters:
    - ruleId: '<scan-rule-id>'      # equality, against the rule id OR the alertRef
      newRisk: 'False Positive'
      url: 'https://example\.com/products.*'   # full match, so the trailing .* is required
      urlRegex: true
      parameter: lang               # plain string: parameterRegex is not set
      methods: [GET]                # uppercased; omit it and every method matches

go deeper

for a junior

Recall the clause names — rule id, url, parameter, attack, evidence, methods — and that each string clause has its own boolean saying whether it is a regex.

for a middle

Explain that the rule id is an equality check tried against both the scan-rule id and the alertRef, that a regex clause is a full match rather than a search, and that an omitted clause matches anything.

for a senior

Diagnose the silent case: a filter that validates, loads and never fires because its URL pattern was written for a path while the comparison runs against the whole URI. Say how you would confirm it before shipping.

for a principal

Argue for a house style on how tightly filters must be written, since the cost of a loose clause is findings nobody ever sees rather than a failure anyone notices.

## The order the checks run in When ZAP's `alertFilters` add-on decides whether a filter applies to an alert, it runs a fixed sequence and bails out at the first clause that does not match: 1. Is the filter enabled? A disabled filter never applies. 2. Does `ruleId` equal the alert's scan-rule id, **or** the alert's `alertRef`? If neither, stop. 3. For a context filter, the alert's URL must be inside that context. 4. `url`, then `parameter`, then `attack`, then `evidence` — each skipped if the filter left it empty. 5. `methods` — skipped if the list is empty. Only if everything survives does the filter rewrite the alert. ## `ruleId` is equality, and it is tried twice This is the clause people get wrong. `ruleId` is a string compared with equality — no regex, no `*`, no comma-separated list. What makes it useful is that it is compared against **two** values on the alert. One is the numeric id of the scan rule that raised it. The other is the alert's `alertRef`, the finer identifier a rule uses when it can raise several distinct alert types. So: - Put the bare scan-rule id in `ruleId` and the filter matches every alert that rule can raise. - Put a specific `alertRef` in `ruleId` and the filter matches only that one variant, leaving the rule's other findings alone. That is the single most important precision dial the add-on has, and the plan schema accepts either — the job only rejects a `ruleId` that is blank or a negative integer, so a non-numeric `alertRef` passes validation. ## The four string clauses | clause | compared against | regex flag | |---|---|---| | `url` | the alert's full URI, query string included | `urlRegex` | | `parameter` | the alert's parameter field | `parameterRegex` | | `attack` | the payload the rule sent | `attackRegex` | | `evidence` | the snippet the rule quoted from the response | `evidenceRegex` | Two behaviours run through all four. **Empty means "anything".** A clause that is absent or an empty string is skipped, not treated as "must be empty". This is why a filter with only a `ruleId` is so blunt — the other four clauses are all silently wide open. **Regex here is a full match.** With the flag set, the pattern must match the value end to end. It is not a search and there is no implicit anchoring to relax: `https://example.com/products` as a URL regex will not match an alert raised on `https://example.com/products?lang=fr`, because the query string is part of the value being matched. You need a trailing `.*`. Getting this wrong produces a filter that validates cleanly, loads without a warning, and matches nothing — a common silent failure on this add-on. The plan only checks that a pattern compiles, and only when its flag is true. ## `methods` `methods` is a list of HTTP methods, normalised to upper case when the filter is built and when it is compared, so case in the plan does not matter. An empty list means every method. It is the clause most often forgotten, and the cheapest way to stop a filter written for a noisy `GET` from also covering the `POST` on the same path. ## One clause that can never match a passive finding `attack` holds the payload a rule sent. Passive rules do not send anything — the passive base class refuses outright to set an attack on an alert it builds. So an `attack` clause naming a payload can never match an alert a passive rule raised. If the noise you are chasing came from passive scanning, use `evidence` instead. ## What this adds up to Every clause you add narrows the set of future findings the filter can swallow, and every clause you leave out widens it. A filter that names a rule and nothing else will quietly absorb the same rule firing on a page nobody has looked at yet. A filter that names the alertRef, the parameter, an anchored URL pattern and the method will not.

  • Why does a `ruleId` holding an alertRef rather than a bare scan-rule id give you a narrower filter?
    Because `ruleId` is compared for equality against both the alert's scan-rule id and its `alertRef`. The bare id therefore matches every alert type that rule can raise, while the alertRef matches only the one variant. The job accepts either — it only rejects a blank or negative value.
  • A filter has `urlRegex: true` and a pattern that looks right, but nothing is ever rewritten. What is the first thing to check?
    Whether the pattern matches the alert's URI end to end. The comparison is a full match against the whole URI including the query string, so a pattern written for the path alone fails on any alert that carries a query. Adding a trailing `.*` usually fixes it. A pattern that merely compiles passes the plan's validation.
  • Can a filter's `attack` clause ever match an alert raised by passive scanning?
    Not one naming a payload. The attack field holds what a rule sent, and passive rules send nothing — the passive alert builder refuses to set an attack at all, so the field is empty. Use `evidence` to pin down a passive finding instead.
  • What does the plan actually validate about an alertFilter entry before the run starts?
    That `ruleId` is present and is not a negative integer, that `newRisk` is one of the accepted names, and that each pattern compiles — but only for the clauses whose regex flag is true. It cannot check that the pattern will ever match anything, so a filter that silently matches nothing is a clean plan.

saying these in an interview costs you the question

  • Thinks ruleId accepts a regex or a wildcard
  • Expects a url regex to match on a substring rather than end to end
  • Believes an omitted clause means the alert's field must be empty
  • Writes an attack clause for a finding raised by passive scanning
  • Assumes the plan validating cleanly means the filter will match something
  • Thinks methods is case-sensitive and must be spelled exactly as in the plan
open as a page

In ZAP's alertFilters add-on, how does a global alert filter differ from a context one?

level: juniorimportance: should knowfreq 45%

basics

~20 s

A global alert filter is tested against every alert and is stored in the add-on's own options, so it survives the next session. A context filter applies only inside its named context and is saved with that context.

open as a page

Where must the alertFilter job sit in a ZAP automation plan, and why does its position matter?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Before every job that can raise an alert. Filters are applied as each alert is added, not swept over alerts already recorded, and a plan runs its jobs in the order they appear in the file — nothing reorders them for you.

open as a page

A low-value ZAP alert recurs on one query parameter. How do you scope an alertFilter to just it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Name the alertRef rather than the bare scan-rule id, add the parameter's exact name, anchor the URL clause with a trailing wildcard, and restrict the method. Then use the add-on's test action to count what the filter would match before you commit it.

open as a page

Why does a ZAP alert filter's newRisk of 'False Positive' leave the alert's risk unchanged?

level: middleimportance: nice to knowfreq 32%

basics

~20 s

Because 'False Positive' is not a risk level. The job maps it to a sentinel of -1, and when the add-on meets that sentinel it writes the alert's confidence field instead, passing the existing risk straight through.

open as a page