In ZAP's active-scan engine, why does one scan rule fire once per site and another once per parameter?
answer
- the rule chooses, not the operator
- a superclass decides the multiplier
- host once, node many, parameter more
- the scheduler branches only twice
basics
~10 sThe base class the rule's author chose decides it. HostProcess invokes an AbstractHostPlugin once, on a single message, and an AbstractAppPlugin once per in-scope node. AbstractAppParamPlugin then loops over that node's parameters itself.
solid answer
~40 sFire count is a property of the rule, not a setting you turn. `HostProcess.processPlugin` looks at the rule's base class: an `AbstractHostPlugin` is dispatched **once**, against a single message, because it tests something about the host itself; an `AbstractAppPlugin` is dispatched **once per node** in the in-scope list the engine built from the Sites tree. The third name in the family is not a third branch — `AbstractAppParamPlugin` *extends* `AbstractAppPlugin`, so the scheduler still treats it as a per-node rule, and the per-parameter multiplication happens inside that class's own `scan()`, which walks the message's parameters and calls the rule once for each. So the count is fixed in two places by two mechanisms: an `instanceof` branch in the engine, and a loop in a base class.
code
java · 11 lines// HostProcess.processPlugin, condensed
if (plugin instanceof AbstractHostPlugin) {
scanMessage(plugin, messageIdToHostScan); // one message, once
} else if (plugin instanceof AbstractAppPlugin) {
for (int messageId : messagesIdsToAppScan) { // every in-scope node
scanMessage(plugin, messageId);
}
threadPool.waitAllThreadComplete(...); // and wait for them all
}
// AbstractAppParamPlugin extends AbstractAppPlugin, so it takes the
// second branch; its own scan() adds the per-parameter loop.go deeper
Learn the three names and what each multiplies over: a host rule once for the site, an app rule once for each URL, a parameter rule once for each input on each URL. That ordering explains most of a scan's runtime.
Explain that HostProcess branches on the base class with instanceof, and that the parameter class is a subclass of the app class, so the per-parameter loop lives in the rule hierarchy rather than in the scheduler.
Use the shapes to predict cost before a scan runs, and to read a finished one: a rule with a very low message count against a large site is a host rule behaving normally, not a rule that failed.
Own the tradeoff between coverage and blast radius. Because per-parameter rules multiply over everything discovered, the size of the target list your pipeline feeds the engine is a bigger policy decision than any per-rule setting.
## The question behind the question Two rules, same scan, same target. One of them appears in the log having sent a handful of requests; the other sent thousands. Nothing in the run configuration distinguishes them. The difference is structural: **an active rule declares how often it wants to be run by choosing which core base class it extends**, and the engine obeys that choice. An operator cannot change it, and no scan setting overrides it. ## The three shapes | base class | how often the engine runs it | what it is for | |---|---|---| | `AbstractHostPlugin` | once per `HostProcess` — that is, once per site in the scan | facts about the server or the site as a whole | | `AbstractAppPlugin` | once per in-scope node | facts about one URL or one endpoint | | `AbstractAppParamPlugin` | once per in-scope node, then once per parameter inside that node | facts about one input on one endpoint | `AbstractHostPlugin`'s own Javadoc calls it "called just once per scan". Read that as *once per host process*: `Scanner` creates one `HostProcess` per site, each with its own cloned rule set, so a scan that covers two sites runs a host rule twice — once inside each process. ## The correction most people need It reads like a three-way split, and it is not. `AbstractAppParamPlugin extends AbstractAppPlugin`, and `HostProcess.processPlugin` contains exactly two branches: 1. `if (plugin instanceof AbstractHostPlugin)` — dispatch one message and move on; 2. `else if (plugin instanceof AbstractAppPlugin)` — loop over every message id in the in-scope list, dispatch each, then wait for them all. A per-parameter rule matches the **second** branch, because it is an `AbstractAppPlugin`. The engine hands it one node, exactly as it hands one to any other app rule. The multiplication by parameter happens one level down, inside `AbstractAppParamPlugin.scan()`: that method asks the engine's variant factory for the message's parameter list and then calls the rule's own `scan(msg, param)` once per entry. So the fire count is set in **two** places by **two** different mechanisms — a scheduler branch and a base-class loop — not in one three-way switch. That matters when you are reading a stack trace or writing a rule, and it is the kind of detail a summary flattens. ## Which single message a host rule gets An `AbstractHostPlugin` still receives an `HttpMessage`; it is not handed a bare hostname. The engine picks `messageIdToHostScan`, which is the **first entry of the same in-scope list the per-node rules walk**, and the rule pulls domain and port out of it. Two consequences follow: - if the in-scope list is empty there is nothing for a host rule either, and it is skipped with every other rule; - the message a host rule sees is an ordinary recorded request, so anything host-wide it concludes is concluded from one sample. ## A rule instance does not survive between messages `HostProcess` does not reuse the rule object registered in the policy. For every message it dispatches, it constructs a **fresh instance** of the rule class through its no-argument constructor, copies the configuration onto it, calls `init(msg, this)` and hands it to a worker thread. The registered object is a template. Practical consequences: - a rule cannot accumulate state across nodes in an instance field — there is no shared instance to accumulate into; - an active rule class must have a usable no-argument constructor; - per-rule totals you see in the log (messages sent, alerts raised) are aggregated by the engine in `PluginStats`, keyed on the rule id, not held by any one rule object. ## Why this is worth a pipeline owner's time The fire count is the dominant term in how much traffic your scan generates and how long it takes: 1. **Cost scales with the crawl, not with the rule list.** Adding one per-parameter rule multiplies out over every node and every input you discovered. Adding one host rule adds roughly one round of requests for the whole site. 2. **You cannot tune it away per rule.** The dials a policy exposes change how hard a rule works once it is running; they do not change how many times the engine starts it. If a rule's shape is wrong for your environment, the lever is whether it runs at all. 3. **A smaller in-scope list shrinks everything at once.** Because the per-node and per-parameter shapes both multiply over the same list, trimming what the engine was given is the change with the largest effect on scan duration. 4. **Reading the log tells you which shape you are looking at.** The engine logs each rule's completion with the number of messages it sent; a rule with a tiny count against a large site is almost certainly a host rule, not a broken one.
- Can an operator change how often a given rule fires?No. The base class is compiled into the rule, and `HostProcess` branches on it with `instanceof`. What an operator controls is whether the rule runs at all and how large the in-scope node list is. Both of those change the total, neither changes the rule's shape.
- Does the same rule object run against every node?No. For each message the engine constructs a fresh instance of the rule class through its no-argument constructor, copies the configuration onto it and runs that. The object registered in the policy is a template, so a rule cannot carry state from one node to the next.
- Which message does a host-wide rule actually receive?The first entry of the same in-scope list the per-node rules walk, held as `messageIdToHostScan`. The rule reads the domain and port from it. If that list is empty there is no message, and the rule is skipped along with all the others.
Posting one notice on the building's front door, one on every apartment door, and one in every pigeonhole inside each apartment: the same notice, three very different distribution sizes. The building manager only ever chooses between the first two — the pigeonhole round is something the apartment-door courier does for itself once it is inside.
saying these in an interview costs you the question
- Says a scan setting controls how often a rule fires
- Treats AbstractAppParamPlugin as a sibling of AbstractAppPlugin
- Thinks the engine branches three ways on the base class
- Assumes one rule instance is reused across every node
- Says a host rule is given a hostname rather than a message