A ZAP active scan finished in seconds with no alerts against a live site. What explains it?
answer
- a quiet scan is not a safe one
- the engine never goes looking
- the tree is the target list
- read the node-count line first
basics
~20 sAlmost always an empty target list. HostProcess builds its targets by walking the Sites tree, not by exploring the network; when that list is empty it skips every rule with the reason no nodes to scan and the scan completes normally.
solid answer
~40 sZAP's active-scan engine never goes looking for URLs. `HostProcess` traverses the Sites tree from the start node, keeps the history ids of the nodes it is allowed to attack, and counts them into `nodeInScopeCount`. If that count is zero, `processPlugin` skips **every** rule with the recorded reason *no nodes to scan*, and the scan finishes cleanly — a successful run that attacked nothing. The engine also drops nodes on its own: anything whose recorded history type marks it as the scanner's own earlier traffic, anything with no history reference, and anything outside scope when the run is scope-limited. Read the engine's own log line first: it either says how many nodes it will scan or says there were none and it is skipping all plugins.
code
bash · 5 lines# the fastest diagnostic in the tool: what did the engine think it had to attack?
grep -E 'Scanning [0-9]+ node\(s\)|No nodes to scan' "$ZAP_LOG"
# ... and which rules it skipped, with the reason recorded against each
grep -E 'skipped plugin \[' "$ZAP_LOG"go deeper
Remember that the active scan attacks a list someone else filled in. If no URLs were recorded before it ran, it has nothing to do, and it will say so quietly rather than failing.
Explain how the target list is built: a Sites tree traversal, per-node eligibility checks, deduplication of repeated URLs, and a count that gates every rule. Know the three skip reasons the engine records.
Diagnose in order — node count, tree contents, scope, authentication, then policy — and add a pipeline assertion on work done rather than on the absence of alerts, because the engine will not fail an empty run for you.
Set the standard for what a security gate is allowed to conclude. A scan result that cannot distinguish 'nothing found' from 'nothing attempted' should not be permitted to pass a release on its own.
## The failure that looks like success This is the most common way a ZAP active scan in a pipeline lies to you, and the mechanism is unglamorous: **the engine attacks what is already in the Sites tree, and nothing else.** It does not crawl, it does not guess paths, it does not read a sitemap. If nothing put URLs in the tree before the active scan job ran — the crawl failed, the import failed, authentication failed so every page was a login redirect, or the URL was simply wrong — then the engine has an empty target list, skips every rule, and reports a completed scan with no findings. ## How the target list is built `HostProcess.run` does this before a single rule starts: 1. it traverses the Sites tree from each start node; 2. for every node it calls `canScanNode`, and keeps only those that pass; 3. `GET` nodes are collected in a map keyed on the escaped URI, so the same URL discovered twice is attacked once; other methods are added directly; 4. the collected `GET` nodes are then merged into the list, with directory-like paths pushed toward the front; 5. `nodeInScopeCount` is set to the size of the finished list, and the first entry of that list becomes the single message any host-wide rule will be given. ## What `canScanNode` throws away | dropped node | why | |---|---| | no history reference | there is no recorded request to clone and attack | | history type recorded as the scanner's own traffic | the engine refuses to attack requests it previously generated, which would otherwise compound each run | | out of scope, when the run is scope-limited | the engine asks the scan whether the node's name is in scope and skips it if not | | a node the traversal never reached | the tree, not the network, is the source of truth | The third row is where most surprises live: a scan restricted to a context attacks only what that context includes, and a context whose regex does not match what the crawl actually recorded produces an empty list without any error. ## What the engine does when the list is empty `processPlugin` short-circuits. For every rule it records the skip reason *no nodes to scan*, marks the rule completed, and moves on. There is no error, no non-zero condition inside the engine, and no alert. The scan's own log line is explicit — it reports either how many nodes will be scanned or that there were none and all plugins are being skipped. That line is the single fastest diagnostic in the whole tool. Two other skip reasons appear in the same place and are worth recognising, because they produce the same silent nothing: - *scan rule does not target selected technologies* — the run narrowed the technology set and the rule does not apply; - *exceeded max rule time* — a per-rule duration cap was configured and this rule hit it. Both the per-rule and the whole-scan duration caps default to unlimited, so if you see this one, somebody set it. ## Diagnosing it in order 1. **Read the engine's node-count line.** "No nodes to scan" ends the investigation immediately. 2. **Look at what is in the Sites tree**, not at what you asked for. The tree is the contract between whatever discovered URLs and the engine that attacks them. 3. **Check scope before checking rules.** A context or an in-scope-only restriction can empty the list while everything else looks healthy. 4. **Check authentication.** If every recorded response was a login page, the tree may be full and still worthless — a different failure with the same symptom, distinguishable by node count. 5. **Only then look at the policy.** An all-rules-disabled policy also produces no findings, but it produces them with a non-zero node count, which is how you tell the two apart. ## Why "no findings" can never be a green light on its own An active scan reports the absence of alerts identically whether it attacked ten thousand parameters or none. Any pipeline that treats a quiet scan as evidence of a secure application is reading a signal the engine does not send. The assertion worth making is about **work done** — how many nodes went into the list and how many messages the rules sent — and only then about findings. That is a check you have to add; the engine will not fail for you. ## One nuance worth carrying "Nothing was attacked" is not the same as "nothing was sent". `HostProcess` starts an `Analyser` for each start node, and it requests deliberately non-existent paths to learn how the site answers for a missing resource. That traffic goes out even when the rule list ends up empty. If you are accounting for exactly what your job did to somebody else's environment, that detail belongs in the account.
- How do you tell an empty target list from a disabled rule set?By the node count. An empty list logs *no nodes to scan* and skips every rule with that reason; a policy with everything disabled logs a real node count and simply has no rules to run. Both end with zero alerts, so the count is the discriminator.
- Why does the engine refuse to attack its own earlier traffic?`canScanNode` drops nodes whose recorded history type marks them as scanner-generated. Without that guard each run would add its own attack requests to the tree and the next run would attack those, compounding traffic and findings with every pass.
- What should a pipeline assert instead of just the absence of alerts?Assert on work done: that the node count the engine reported is above zero, and ideally that rules sent messages. Findings are a second question. A scan that attacked nothing produces exactly the same empty finding list as a clean application.
saying these in an interview costs you the question
- Reads no alerts as evidence the application is clean
- Thinks the active-scan engine crawls for URLs itself
- Assumes an empty target list makes the scan fail loudly
- Blames the rule policy before checking the node count
- Says nothing reached the target when no rule ran