skip to content

In ZAP, what is a passive scan rule handed to work with, and what stops it sending a request?

level: juniorimportance: must knowfreq 70%

answer

  1. it reads, it never asks
  2. the contract offers no transport
  3. a stored message and its history id
  4. raiseAlert and addHistoryTag, nothing else
  5. a passive alert cannot carry an attack

basics

~20 s

A ZAP passive scan rule implements the core PassiveScanner interface, which hands it a recorded HTTP message and that message's history id. The interface declares no send method, so a rule can only read traffic already captured.

solid answer

~30 s

A passive scan rule in ZAP implements `PassiveScanner`, or extends `PluginPassiveScanner`, and the engine calls it on each stored record: `scanHttpRequestSend(msg, id)` with the recorded message, and `scanHttpResponseReceive(msg, id, source)` with the same message plus a pre-parsed HTML `Source`. Both are `default` no-op methods, so a rule overrides only the half it needs. Nothing on that interface reaches the network — the engine injects a `PassiveScanActions` handle whose whole surface is `raiseAlert` and `addHistoryTag`. The guarantee is reinforced on the alert itself: `PluginPassiveScanner`'s `AlertBuilder` overrides `setAttack` to throw `IllegalStateException("Passive alerts should not have an attack.")`, because nothing was attacked.

go deeper

for a junior

Remember the shape: a passive rule is given a message that was already recorded and is asked what it notices. Nothing in what it is given lets it fetch anything.

for a middle

Name the callbacks and say both fire: scanHttpRequestSend and scanHttpResponseReceive. Then explain that the injected PassiveScanActions handle offers only raiseAlert and addHistoryTag, which is where the guarantee lives.

for a senior

Use the contract to explain report quality. Passive coverage is capped by what the crawl recorded, findings are correlational rather than confirmed, and a rule that throws is swallowed per record, so a thin passive report needs the capture checked before the rules are blamed.

for a principal

Decide what weight a build gate may put on evidence nobody confirmed. A finding a rule could not test is a signal, not a verdict, and the honest design keeps unconfirmable classes out of the blocking set while still surfacing them.

## What a passive scan rule is, in ZAP's own types A **passive scan rule** is a Java class implementing the `PassiveScanner` interface, or — far more commonly — extending the abstract `PluginPassiveScanner`. Both types live in ZAP's **core**, in the package `org.zaproxy.zap.extension.pscan`, and neither is deprecated. The thing that *calls* them does not live in core. The live passive-scan engine ships as the **`pscan` add-on**, and core keeps deprecated shims of the engine classes so older add-ons still compile. Hold those halves apart, because the names collide: there is a `PassiveScanController` and a `PassiveScanTask` in core *and* in the add-on, and it is the **core** pair that carries `@Deprecated`. The sentence that survives review is *"the rule contract is core, the engine that drives it is the `pscan` add-on"* — not *"passive scanning is an add-on"*. ## What the interface actually hands a rule `PassiveScanner` declares both entry points below as `default` methods with empty bodies, so a rule overrides only the half it cares about. | entry point | what the engine passes | when the engine calls it | |---|---|---| | `scanHttpRequestSend(HttpMessage msg, int id)` | the stored message and its History-table id | for each queued record the rule is enabled and applicable for | | `scanHttpResponseReceive(HttpMessage msg, int id, Source source)` | the same message, its id, and a parsed HTML `Source` | only when `msg.isResponseFromTargetHost()` holds | | `setPassiveScanActions(PassiveScanActions actions)` | the handle a rule acts *through* | once, before the callbacks, on each per-message copy | Consequences people miss: - **A passive rule sees the request, not only the response.** "Passive means response-only" is a common and wrong summary. A rule that flags a credential sitting in a query string is reading the request half. - **The HTML `Source` is parsed once per record by the engine**, then shared with every rule for that record, rather than re-parsed per rule. ## Why it cannot send, structurally There is no method anywhere on `PassiveScanner` or `PluginPassiveScanner` that reaches the network. That is the whole mechanism — not a setting, not a policy, not a mode. The only capability the engine injects is `PassiveScanActions`, and its entire surface is: 1. `raiseAlert(HistoryReference, Alert)` — record a finding against the stored record; 2. `addHistoryTag(HistoryReference, String)` — tag that record in the history table. Neither opens a connection. A rule therefore cannot re-fetch a page, cannot flip one bit of a parameter and compare, and cannot confirm anything by observing a different response — because it can only ever observe the one response that was already stored. The second half of the guarantee sits on the alert. A passive rule builds its finding with `newAlert()`, and that builder is `PluginPassiveScanner.AlertBuilder`, which overrides `setAttack` to throw `IllegalStateException("Passive alerts should not have an attack.")`. So a passive finding may carry **evidence** — a substring of what was actually captured — but never an attack string, because no payload was ever sent. ## What the rule does get to decide A rule still has real control over whether it runs at all: - `isEnabled()` / `setEnabled(boolean)` — the engine skips a disabled rule, and the engine can switch a rule off mid-run. - `appliesToHistoryType(int)` — a rule declares which kinds of history record it cares about (proxied traffic, spider traffic, and so on), and the engine also keeps a set of opted-in history types that widens the answer. - Each `PluginPassiveScanner` is `copy()`-ed by the engine for every record before its callbacks fire, and the copy is given a `PassiveScanData` helper bound to that one message. A rule instance therefore cannot accumulate state across messages through its own fields. ## What this means when you are the one running the scan For anyone driving ZAP from a pipeline, the contract is the explanation for most surprises in the report: - **Coverage is bounded by capture.** A passive rule has nothing to say about a page nobody fetched, so the crawl's reach is the ceiling on passive findings, and a thin report is at least as likely to be a thin crawl. - **Passive findings are correlational.** The rule saw a header, a cookie flag, a comment, a body pattern. It did not prove exploitability, because proving it would require sending something. - **A passive rule cannot be "turned up" into an attacking one.** If someone proposes raising a dial so that a passive rule confirms its finding, the answer is that there is no dial: the type it implements has no transport. - **One broken rule is not a broken run.** The engine catches an exception per rule, logs it against the record id and URL, and carries on with the next rule — so a rule can fail quietly and cost you only its own findings. That last point is the practical shape of the whole design: a passive rule is cheap, unprivileged and unable to disturb the target, and in exchange it is entirely at the mercy of what the rest of the run happened to record.

  • Why does ZAP's passive engine copy each rule before running it against a record?
    The engine calls `copy()` on every `PluginPassiveScanner` per record and binds a `PassiveScanData` helper for that one message to the copy. The rule instance the scan actually runs is therefore fresh, so a rule cannot leak state from one message into the next through its own fields, and the pooled worker threads can run different records concurrently without sharing a rule object.
  • If a passive scan rule throws an exception, what happens to the rest of the sweep?
    The engine catches it inside the per-record task, logs the rule name, the record id, the method and the URL at error level, and moves on to the next rule. The record is still marked complete and the queue keeps draining. So a broken rule costs you that rule's findings silently, not the run — which is why a passive report can be thin with a clean exit.

saying these in an interview costs you the question

  • Says a passive rule re-requests the page to confirm what it found.
  • Claims a passive rule can be made to send by turning a setting up.
  • Thinks a passive rule only ever looks at the response, never the request.
  • Assumes a passive finding carries an attack string like any other alert.
  • Believes the passive rule base class ships in the pscan add-on, not in core.