In OWASP ZAP, what does a saved scan policy file hold, and how does a run select one?
answer
- XML on disk, not a runtime flag
- it lives under ZAP home
- core lists the policies folder
- the filename is the name you type
basics
~20 sA ZAP scan policy is an XML file with the .policy extension holding a default attack strength, a default alert threshold and optional per-rule overrides. Core's PolicyManager lists the policies folder under ZAP home and loads one by filename.
solid answer
~40 sA saved scan policy is an XML file named `<name>.policy`, and it holds very little: a `<scanner>` block carrying the policy-wide default `<level>` (the alert threshold) and `<strength>` (the attack strength), plus an optional `<plugins>` block of per-rule overrides. The reader is **core**, not an add-on — `org.zaproxy.zap.extension.ascan.PolicyManager` lists every `*.policy` file in the `policies` directory under ZAP home and strips the extension to get the name. Selection is by **that filename stem**: `PolicyManager.getPolicy(name)` builds the path directly. The `<policy>` element inside the file is display text and often differs from the filename. The named policies you can pick out of the box arrive with the `scanpolicies` add-on, which ships no Java or Kotlin at all — only `.policy` files installed into ZAP home for core's reader to find.
code
xml · 9 lines<configuration>
<policy>St-Low-Th-High</policy>
<statsId>dock-low-high</statsId>
<readonly>true</readonly>
<scanner>
<level>HIGH</level>
<strength>LOW</strength>
</scanner>
</configuration>go deeper
Recall the shape: a .policy file is XML under ZAP home, it carries a default strength and a default threshold, and you pick it by filename.
Be able to walk the read path: PolicyManager lists the policies directory, strips the extension for the name, and getPolicy rebuilds that exact path to load the file.
Show you know the two shipped sets differ on opposite axes — the named tiers vary their rule allow-list at fixed Medium dials, the container set varies the dials with no rule list.
The angle to own is reproducibility: a policy with no plugins block means the effective rule set is whatever add-ons that machine has installed, so pin the image rather than the policy name alone.
## What a saved scan policy actually is In OWASP ZAP a **scan policy** is a file on disk, not a runtime switch. It is XML, it carries the `.policy` extension, and its whole job is to say two things: what the **defaults** are for every active scan rule, and which individual rules **deviate** from those defaults. The root element is `<configuration>`. Inside it: - `<policy>` — a display name. It is the text shown in the UI, and it is *not* the name you select by. - `<statsId>` — a short key the run uses when it emits statistics. - `<readonly>` and `<locked>` — whether the file may be edited, and whether it behaves as an allow-list (a locked policy sets every rule it does not mention to `OFF`). - `<scanner>` — the policy-wide defaults. `<level>` is the default **alert threshold**; `<strength>` is the default **attack strength**. - `<plugins>` — optional. One element per rule, keyed by that rule's own id, carrying its `<enabled>` flag and its own `<level>` or `<strength>` where it deviates. A policy can be very small. Several shipped ones carry no `<plugins>` block at all: they set the two dials and nothing else, and so they apply to whatever rules happen to be installed on that machine. ## Who reads it, and where it lives The reader is **core**: `org.zaproxy.zap.extension.ascan.PolicyManager`. Its `getAllPolicyNames()` lists the `policies` directory under **ZAP home** (`Constant.getPoliciesDir()`), keeps every entry ending in `.policy`, and strips that suffix to produce the name. If the directory is missing or holds nothing, it does not fail: it builds a policy from the existing scanner configuration, names it the default, and saves it back — so the list is never empty and an absent directory is not an error. Loading is just as literal. `getPolicy(name)` builds the path `<policies dir>/<name>.policy`, parses it into a `ScanPolicy`, applies `<level>` and `<strength>` to every rule in a fresh `PluginFactory`, and then layers the `<plugins>` overrides on top. ## The name is the filename This is the detail that costs people a pipeline run. Selection is by the **filename stem**. The `<policy>` element inside the file is display text, and across the shipped set it frequently differs: the file `Pen Test.policy` announces itself as *Penetration Tester*, and `Dev CICD.policy` as *Developer CI/CD*. Ask for the name written inside the file and the load fails, because no file by that name exists. ## Two shipped sets, and each varies a different axis ZAP ships two separate collections of `.policy` files, and reading them side by side is the fastest way to see what a policy can express. | collection | where it comes from | what varies between its files | what is identical in all of them | |---|---|---|---| | the named tiers (Default, API, the Dev and QA tiers, Pen Test) | the **`scanpolicies` add-on** | the `<plugins>` allow-list — which rules are named in it | both dials: every file ships a Medium threshold and Medium strength | | the container image's set | the core repository's `docker/policies` directory | the two dials — the filenames are literally `St-<strength>-Th-<threshold>` | the rule set: these files carry no `<plugins>` block at all | So the named tiers do **not** differ in how hard they attack or how sure a rule must be. *Dev CI/CD* and *Pen Test* both run Medium/Medium; what separates them is the length of the allow-list. The dial grid is the mirror image — it restricts no rules at all and applies one strength/threshold pair to everything installed. ## The add-on that ships no code Worth stating plainly, because it inverts the usual expectation: the add-on that gives you the named policies contains **no Java and no Kotlin whatsoever**. It is `.policy` XML plus help pages, installed into ZAP home. Installing it does not add an engine — it adds files a core engine already knew how to read. Most ZAP subsystems arrive the other way round, with the add-on carrying the implementation and core keeping a deprecated shim, so this shape is worth recognising for what it is. ## What a policy does not decide A policy is about **active scan rules and their two dials**. It does not decide which parts of a request are injectable, it does not set scope, and it does not choose a crawler; those are separate configuration surfaces. A reader who goes looking in the policy file for a switch that turns on, say, cookie testing will not find one, because that switch is not a policy concern.
- What happens if the policies directory under ZAP home is empty or missing?`PolicyManager.getAllPolicyNames()` finds no `*.policy` entry, builds a `ScanPolicy` from the existing scanner configuration, names it the default and saves it into that directory. You always end up with at least one selectable policy rather than an empty list or an error.
- Why can a policy you can see listed still fail to load when a job names it?`PolicyManager.getPolicy(name)` builds `<policies dir>/<name>.policy` directly, and the listing shows filename stems. The usual cause is typing the display name from the `<policy>` element instead — `Penetration Tester` has no file, `Pen Test` does.
- What does `<locked>true</locked>` change about how the file is read?It turns the file into an allow-list. `PluginFactory` checks each installed rule for a matching entry in the file and, finding none, sets that rule's alert threshold to `OFF`. An unlocked policy leaves unmentioned rules at the policy default instead.
saying these in an interview costs you the question
- Calls the policy file YAML because automation plans are YAML
- Thinks the scanpolicies add-on brings its own scanning engine
- Selects a policy by the display name written inside the file
- Expects the policy file to choose which request parts get attacked
- Assumes a missing policies directory makes the run fail