skip to content

How can a single OWASP ZAP scan rule raise alerts at several different risk and confidence levels?

level: middleimportance: should knowfreq 50%

answer

  1. set per finding, not per rule
  2. the builder decides what is pre-filled
  3. active seeds risk, passive seeds neither
  4. one plugin id, many alert references

basics

~20 s

Both scores are set per finding, on the builder the rule uses to raise each alert. An active rule seeds risk from its own rule-level value and may override it; a passive rule seeds neither, so every raise site supplies both.

solid answer

~30 s

A rule builds each finding through `newAlert()` and calls `setRisk` and `setConfidence` on that builder, so every detection branch can choose its own pair. The two engines differ in what the builder starts with: an active rule's builder seeds risk from the rule's `getRisk()`, while a passive rule's builder seeds neither axis — an unset risk there falls back to `RISK_INFO`. Neither engine seeds confidence, because confidence belongs to the observation, not the rule. One shipped passive rule raises alerts spanning Informational to High and Low to Medium confidence from one plugin id, giving each kind its own `alertRef`.

code

java · 13 lines
java
// AbstractPlugin.AlertBuilder - an ACTIVE rule's builder seeds risk from the rule
setPluginId(plugin.getId());
setName(plugin.getName());
setRisk(plugin.getRisk());          // rule-level; AbstractPlugin.getRisk() returns RISK_MEDIUM
// ...description, solution, reference, classification ids, tags
// note: nothing seeds confidence

// PluginPassiveScanner.AlertBuilder - a PASSIVE rule's builder seeds NEITHER axis
setPluginId(plugin.getPluginId());
setName(plugin.getName());
setMessage(message);
setTags(plugin.getAlertTags());
// an unset risk therefore lands on Alert's own field default, RISK_INFO

go deeper

for a junior

Remember that the numbers belong to the finding, not to the rule that found it. Seeing High and Informational rows carrying the same rule name in one report is normal, not a bug.

for a middle

Explain the builder: each raise site sets risk and confidence itself, and what the builder pre-fills differs between the active and passive engines. Name what an unset value falls back to in each case.

for a senior

Demonstrate that you never summarise findings by rule id alone, because one id spans several alert kinds with different scores. Show how you group by alert reference instead when you report to a team.

for a principal

Decide what the organisation's rollup is keyed on. Choosing rule id over alert reference is a reporting decision that silently merges unlike findings, and it is cheaper to settle once than to correct per team.

## One rule, many findings, two scores set in two different places A scan rule in OWASP ZAP does not hand back a severity for itself and let the engine fill in the rest. It builds each finding through a builder — `newAlert()` on the rule, which returns an `AlertBuilder` — and calls `setRisk` and `setConfidence` on it. Because that happens at the point where the rule decided it had seen something, a rule with several detection branches can give each branch its own pair of scores. The builder constructor is where the two axes stop being symmetrical, and the two engines do it differently. | | active rule (`AbstractPlugin.AlertBuilder`) | passive rule (`PluginPassiveScanner.AlertBuilder`) | |---|---|---| | what the constructor seeds | plugin id, name, **risk from `plugin.getRisk()`**, description, solution, reference, the classification ids, tags | plugin id, name, the message, tags | | risk if the raise site never sets it | the rule's own `getRisk()` — `RISK_MEDIUM` unless the rule overrides it | `RISK_INFO`, the field default on `Alert` itself | | confidence if the raise site never sets it | `CONFIDENCE_MEDIUM`, the field default on `Alert` | `CONFIDENCE_MEDIUM`, the same field default | So an **active** rule has a rule-level risk baked into every alert it builds, which it may override per raise site; a **passive** rule has no rule-level risk at all, and every raise site must supply one or the alert ships as Informational. Neither engine seeds confidence, which is the structural statement that confidence is a property of *this observation*, never of the rule. ## What that looks like in a shipped rule The clearest demonstration is a passive rule in the `pscanrules` add-on that inspects view-state fields. It has one plugin id and builds several different alerts from it, and they span the scales: one at Medium risk with Medium confidence, one at Low risk with Medium confidence, one at **High risk with Low confidence**, one at High risk with Medium confidence, and one at Informational risk with Low confidence. Every one of them comes from the same rule, in the same file, on the same response. That High-at-Low row is the interesting one, and the reason it exists is visible in the branch that raises it: the rule has spotted a pattern that *would* be serious if the field really is what it looks like, and it cannot confirm that from the response alone. The rule is being honest rather than cautious — lowering the risk to match its uncertainty would have thrown away the "if real, this is serious" half of the message, which is the half a reviewer needs. ## Each distinct finding gets its own reference Because one rule can report genuinely different problems, the alert carries an `alertRef` — an identifier for the **kind** of alert, not for the instance and not for the rule. A rule that raises more than one kind sets a reference per kind, conventionally the plugin id followed by a dash and a qualifier. So one plugin id fans out to several references, and the scores belong to the reference rather than to the rule. Two consequences for anyone reading output in a pipeline: 1. **"Rule X is High" is usually a category error.** The same rule id can produce High and Informational rows in the same run. If you summarise by rule id you will flatten that. 2. **The pair travels with the finding, not with the rule.** There is nothing to look up in a rule catalogue that will tell you what risk a given row carried; the row itself is the authority. ## Reading the combination Two rows from one rule, side by side, is the everyday shape of this: - a **high-risk, low-confidence** row — serious if real, and the tool cannot tell. It needs a human and it should not be auto-suppressed, because the cost of being wrong is on the consequence axis. - a **low-risk, high-confidence** row — certainly true and small. It is the safest thing to batch, defer or accept, precisely because there is nothing left to verify. Notice that ordering those two requires a judgment the tool does not make. ZAP publishes both numbers on every alert and defines no ranking between them, so a queue sorted only by risk puts the weak signal above the certain fact, and a queue sorted only by confidence does the reverse. Deciding which way round your pipeline sorts them — and writing that down — is the actual work.

  • A passive rule forgets to call `setRisk`. What risk does the alert carry?
    `RISK_INFO`. The passive builder seeds nothing for risk, so the value falls through to the field initialiser on `Alert`. An active rule in the same situation gets its own `getRisk()` instead, which defaults to `RISK_MEDIUM` — so the same omission produces a different band depending on which engine raised it.
  • Why does neither engine pre-fill confidence?
    Because confidence describes the strength of this particular observation, which the rule only knows at the point it decides to raise. Pre-filling it from the rule would assert a certainty the rule has not evaluated yet. It falls back to `CONFIDENCE_MEDIUM`, the neutral middle of the scale.

saying these in an interview costs you the question

  • Says a rule has one severity for every alert it raises
  • Thinks the engine assigns the scores after the rule returns
  • Expects risk and confidence to move together within a rule
  • Assumes every alert from one plugin id is the same finding