pan-os-php is a PHP framework and command-line toolset published by Palo Alto Networks for working with PAN-OS firewall and Panorama configurations at scale. It loads an offline XML configuration file (or pulls one live over the XML API), exposes every object — addresses, services, rules, zones, security profiles, interfaces, routing, schedules, certificates — as native PHP classes, and ships a single front-end utility (pan-os-php) that dispatches forty-plus subcommands ("UTIL types") covering object editing, rule editing, mergers, BPA generation, software downloads, log retrieval, configuration diffs, IronSkillet pushes, and dozens of other day-to-day Palo Alto admin tasks.
This guide is for the engineers who run those subcommands from a terminal: how to install the framework, how to point it at a Panorama or firewall, how to pick the right type= and actions=, and how to recover when things go wrong.
1. Quick Start
pan-os-php is invoked as pan-os-php type=<subcommand> (or via Docker as docker run ... type=<subcommand>) followed by query arguments and actions. The fastest way to be running an action against a sample config in under thirty seconds is Docker — no host PHP, no install scripts, no shell rc edits.
Try it in 30 seconds with Docker
docker run --rm -v "$PWD":/share swaschkut/pan-os-php:latest \
type=address \
in=/share/panorama-config.xml \
'filter=(object is.unused.recursive)' \
actions=display
***********************************************
*********** pan-os-php.php UTILITY **************
- PAN-OS-PHP version: 2.1.20 [UNIX] [8.1.2]
***********************************************
*********** Reading configuration from file ****
***********************************************
- object 'lab-mgmt-host' is unused (recursive)
- object 'old-vpn-peer' is unused (recursive)
- object 'decommissioned-printer' is unused (recursive)
**** PROCESSING OF 3 OBJECTS DONE ****
That single command pulls the published image, mounts your current directory at /share, runs the dispatcher inside the container, finds every never-referenced address object, and prints them. No file is written; nothing is changed.
Tip
If you do not have a Panorama config handy, the repo ships sample configs under
examples/sample-configs/— clone the repo,cdinto it, and re-run within=/share/examples/sample-configs/<file>.xml. Once you are comfortable, alias the command permanently: the shippedutils/alias.shdefinespa_docker-panosphpasdocker run --name panosphp --rm -v $PWD:/share -it swaschkut/pan-os-php:latest, sopa_docker-panosphp type=address listactionsbecomes the entire invocation.
Key features
- Single dispatcher (
utils/pan-os-php.php) with 47 supported UTIL types includingaddress,service,tag,rule,zone,interface,routing,schedule,application,threat,securityprofile,securityprofilegroup,device,dhcp,certificate,static-route,address-merger,addressgroup-merger,service-merger,servicegroup-merger,tag-merger,rule-merger,custom-url-category-merger,override-finder,diff,upload,xml-issue,appid-enabler,config-size,download-predefined,register-ip-mgr,userid-mgr,xml-op-json,bpa-generator,playbook,ironskillet-update,maxmind-update,software-remove,software-download,software-preparation,traffic-log,system-log,gratuitous-arp,license,config-download-all,spiffy,config-commit,protocoll-number-download,html-merger,tsf,xpath,key-manager,ssh-connector,custom-report,gcp,vendor-migration,appid-toolbox,rule-compare,stats, andutil_get-action-filter. - 220+ named actions across object and rule types — every rename, tag manipulation, IP swap, profile assignment, and bulk merge is a one-liner.
- Filter language (
filter=) that reads like English —'(from has external) and (to has Trust)'— to scope which objects an action touches. - Works offline against an exported XML config (
in=config.xml) or live over the API (in=api://10.0.0.2orin=api://[email removed]). API keys are cached in~/.panconfkeystoreafter the first login. - Generates set-format command output for change reviews (
outputformatset=changes.txt) and supports a preview model that loads, evaluates, and reports without writing anything back. - Ships Docker images for amd64 and arm64 (
swaschkut/pan-os-php:latest), plus install scripts for Ubuntu 20.04, Ubuntu 22.04, CentOS 7, and macOS.
Prerequisites
PHP 7.4 or later (8.1 is the upper bound the project tests against), with the json, curl, dom, mbstring, bcmath, mysqli, and yaml extensions available; git; bash 5; and a Palo Alto Networks firewall or Panorama configuration (XML file or live device). Docker users skip every PHP requirement — the image bundles everything.
What to try next
- Section 2 (Installation) — pick the install method that matches your OS. Docker is recommended for everyone trying
pan-os-phpfor the first time. - Section 4 (Core Usage) — the
type=+filter=+actions=model explained from scratch. - Section 5 (Common Tasks) — copy-paste recipes for the discovery and reduction work that drives most real-world use.
2. Installation
pan-os-php is not a Composer-installable library in the conventional sense; it is a fixed-path source tree that PHP loads from include_path. The supported native install methods all converge on the same outcome: the repo lives at /tools/pan-os-php, that path is on PHP's include_path, and the alias pan-os-php evaluates to php -r "require_once 'utils/pan-os-php.php';" $@. Docker bypasses all of that — the image already has the right layout baked in.
System requirements
| Component | Minimum | Tested upper bound |
|---|---|---|
| PHP | 7.4.0 | 8.1 (composer.json), 8.x runtime |
| PHP extensions | json, curl, dom, mbstring, bcmath, mysqli, yaml |
— |
| Bash | 5.0 (for pan-os-php autocompletion) |
— |
| OS | Ubuntu 20.04 / 22.04, CentOS 7, macOS, Windows via Docker | — |
| Memory | 14 GB PHP memory_limit is set inside the framework (ini_set("memory_limit", "14512M")) |
— |
| Git | Any 2.x | — |
| Python 3 (optional, used by some helper scripts) | 3.x with pandas, xlsxwriter, netaddr, requests |
— |
Important
The framework calls
ini_set("memory_limit", "14512M")andset_time_limit(0)at boot. Loading a multi-thousand-rule Panorama config can genuinely consume tens of GB of resident PHP memory; size your jump host accordingly. The framework also callsdate_default_timezone_set('UTC')unconditionally — if you read timestamps off a generated report, treat them as UTC.
2.1 Quick install (recommended for trying it)
The published Docker image is the lowest-friction path. Zero PHP setup on your host, identical behaviour on Linux / macOS / Windows / WSL2 / a CI runner.
docker run --rm -v "$PWD":/share -it swaschkut/pan-os-php:latest type=address listactions
The image entrypoint (docker/entrypoint.sh) is literally:
exec php -r "require_once('/tools/pan-os-php/utils/pan-os-php.php');" "$@"
so anything you would pass to pan-os-php you pass after the image name. Mount your XML config or working directory at /share and reference it with in=/share/panorama-config.xml; outputs written to /share/... end up in your host directory.
Make the alias permanent. The shipped utils/alias.sh defines a convenience alias:
alias pa_docker-panosphp='docker run --name panosphp --rm -v $PWD:/share -it swaschkut/pan-os-php:latest'
Drop that line into ~/.bashrc (or source it from the cloned repo with source utils/alias.sh), then every invocation simplifies to:
pa_docker-panosphp type=address in=/share/panorama-config.xml \
'filter=(object is.unused.recursive)' actions=display
Tip
The
--name panosphpflag means a stuck or crashed container leaves a dead container behind under that fixed name, which the--rmcleans up — safe to re-run. If you want concurrent invocations, drop--name(Docker will auto-name them) and keep--rmfor cleanup.
Both swaschkut/pan-os-php-amd:latest and swaschkut/pan-os-php-arm:latest are published; the manifest swaschkut/pan-os-php:latest selects the right architecture automatically. A :develop tag tracks the development branch.
2.2 Native install on Linux (production)
For a long-lived jump host running scheduled jobs, the native install is faster per-invocation (no container start cost) and lets you keep your editor / IDE / git working in the same shell.
Ubuntu 22.04 (preferred native target)
sudo bash /path/to/repo/install_script_ubuntu22.sh
The script installs php8.1, php8.1-curl, php8.1-dom, php8.1-mbstring, php8.1-bcmath, php8.1-yaml, python3 with pandas/xlsxwriter/netaddr/requests, jq, and git; clones the repo to /tools/pan-os-php; appends include_path = '/tools/pan-os-php' to /etc/php/8.1/cli/php.ini; and symlinks the bash autocompletion script into /usr/share/bash-completion/completions/pan-os-php. After it finishes, run the per-user step exactly once for each user account that will use the tool:
sh /tools/pan-os-php/set_alias_usage.sh
That appends source /tools/pan-os-php/utils/alias.sh and the autocompletion stub to your ~/.bashrc, then verifies the install with the standard one-liner:
php -r "require('/tools/pan-os-php/lib/pan_php_framework.php');print 'PAN-OS-PHP LIBRARY - OK INSTALL SUCCESSFUL\n';"
PAN-OS-PHP LIBRARY - OK INSTALL SUCCESSFUL
Ubuntu 20.04
install_script_ubuntu20.sh is identical except it pulls php7.4 and writes its include_path line to /etc/php/7.4/cli/php.ini. Use it on hosts you cannot upgrade to 22.04.
2.3 macOS
cd /path/to/pan-os-php
bash mac_set_path_variable.sh
The macOS script reads the Loaded Configuration File path out of php -i, appends include_path = '<repo>' to that file, then registers source <repo>/utils/alias.sh into ~/.profile, ~/.bash_profile, and ~/.zshrc. After it finishes, open a new shell and run the verification one-liner from section 2.5.
2.4 CentOS 7
sudo bash /path/to/repo/install_script_centos7.sh
The CentOS path is more involved — it adds epel-release, the Remi repo (remi-php74), builds bash 5 from source (CentOS 7 ships bash 4), and changes the user's login shell with chsh -s /usr/local/bin/bash. After the script finishes, run chsh -s /usr/local/bin/bash for your own user before opening a new shell.
Warning
The CentOS 7 installer rewrites
/etc/shellsand changes the login shell. Read the script before running it on a server with strict change control. There is also an older companion script (install_script_centos7_old.sh) that targets PHP 5.6 via theremi-php56repo — it is retained for legacy use only and should not be used on new installs.
2.5 Verify the install
After a native install, this single line proves the autoloader, the framework, the API connector, and phpseclib are all on the include path and parseable:
php -r "require('/tools/pan-os-php/lib/pan_php_framework.php');print 'PAN-OS-PHP LIBRARY - OK INSTALL SUCCESSFUL\n';"
PAN-OS-PHP LIBRARY - OK INSTALL SUCCESSFUL
Then check the dispatcher itself:
pan-os-php version
- PAN-OS-PHP version: 2.1.20 [UNIX]
- /tools/pan-os-php/utils
- PHP version: 8.1.2
For the Docker path, the equivalent verification is:
docker run --rm swaschkut/pan-os-php:latest version
Updating
bash /tools/pan-os-php/update_pan-os-php.sh
The updater stashes any local edits, runs git clean -f, pulls origin main over HTTPS with TLS verification disabled (because the script is meant to be runnable on locked-down jump hosts behind MITM proxies), and re-runs git submodule update --remote. Docker users update with docker pull swaschkut/pan-os-php:latest.
Removing the path/alias
bash /path/to/pan-os-php/remove_path_variable.sh
That script removes the include_path line from your php.ini and the source .../alias.sh line from ~/.profile, ~/.bash_profile, and ~/.zshrc. It does not delete the source tree — rm -rf /tools/pan-os-php if you want it gone for good.
Install troubleshooting
Error: *** ERROR **** PAN-OS-PHP requires PHP version >= 5.5
The framework refuses to load on PHP < 5.5. In practice, raise the floor: install PHP 7.4 or later via your distro's package manager, or use the Docker image (which already has 8.1).
pan-os-php: command not found after install
Open a new shell (the alias is added to ~/.bashrc), or source ~/.bashrc in the current one. The alias itself is alias pan-os-php='php -r "require_once 'utils/pan-os-php.php';" $@'. If you skipped set_alias_usage.sh, run it now.
Warning: include(...): failed to open stream from any UTIL
PHP cannot find the framework files. Check php -i | grep include_path. If it does not contain the install path (default /tools/pan-os-php), append include_path = '/tools/pan-os-php' to the php.ini reported by php -i | grep "Loaded Configuration File".
3. Configuration
pan-os-php does not read a config file. It is configured by:
- CLI arguments parsed by
PH::processCliArgs()— everykey=valuepair on the command line becomes an entry inPH::$args. - Aliases in
utils/alias.shthat rebind shorthand command names (pa_docker-panosphp,pa_ckp-exclude,pa_csv-import,pa_ike,pa_config-reset,pa_get-system-user-info). - Environment variables read by the framework:
HOME,USERPROFILE,HOMEDRIVE,HOMEPATH(used to locate~/.panconfkeystore) andTERM(used to enable bold ANSI output). - The hidden API keystore at
$HOME/.panconfkeystore— a file that mapsserial@hostto API key once you have logged in once. Disable withshadow-apikeynosave.
Standard arguments accepted by every UTIL
These are wired up in UTIL::supportedArguments() and apply to nearly every type= invocation:
| Argument | Purpose | Example |
|---|---|---|
in=<file|api> |
Source config: filename, api://IP, or api://serial@panorama |
in=api://10.0.0.5 |
out=<file> |
Output file when in is a file |
out=after.xml |
location=<scope> |
VSYS or DG to scope the run; defaults to shared (Panorama) or vsys1 (firewall) |
location=any, location=DG-Edge:includechilddgs |
template=<name> |
Limit Panorama processing to a specific template | template=Edge-Stack |
filter=<RQuery> |
Filter expression (see Section 4) | 'filter=(object is.unused.recursive)' |
actions=<action>[:arg1[,arg2]] |
What to do per matched object — slash separates multiple | actions=tag-Add:RetiredQ4 |
listactions |
Print every supported action and its argument schema for this type |
pan-os-php type=rule listactions |
listfilters |
Print every supported filter operator for this type |
pan-os-php type=address listfilters |
stats |
Print object/rule counts after the run | stats |
loadplugin=<file> |
Load a custom action/filter plugin | loadplugin=examples/example_plugin_rule_display_source_destination.php |
apitimeout=<seconds> |
API call timeout, default 60 | apitimeout=300 |
auditcomment=<text> |
Override the default audit comment when committing via API | auditcomment="JIRA-1234" |
outputformatset[=file] |
Emit PAN-OS set commands for the change |
outputformatset=changes.set |
projectfolder=<dir> |
Place all generated outputs in this folder | projectfolder=runs/2026-04-28/ |
loadpanoramapushedconfig |
When loading from a managed firewall, also pull in Panorama-pushed objects | loadpanoramapushedconfig |
cycleconnectedfirewalls |
For Panorama API: iterate every connected firewall (used by bpa-generator, override-finder) |
cycleconnectedfirewalls |
git |
Initialise a Git repo around the input file and commit before/after | git |
debugapi |
Print every API call as it happens | debugapi |
debugloadtime |
Print the time each load phase takes | debugloadtime |
help |
Show usage for the current type= and exit |
help |
shadow- flags (advanced)
These toggle protocol- and output-level behaviours. They are real flags, exposed by listactions/help, but you rarely need them day-to-day.
| Flag | Purpose |
|---|---|
shadow-disableoutputformatting |
Skip DOMDocument output cleanup; keep raw XML |
shadow-enablexmlduplicatesdeletion |
If duplicate objects are seen, keep only one |
shadow-ignoreinvalidaddressobjects |
Tolerate address objects with no value/type |
shadow-apikeyhidden |
POST the API key (PAN-OS 9.0+) instead of putting it in the URL |
shadow-apikeynohidden |
Force API key in URL (PAN-OS < 9.0) |
shadow-apikeynosave |
Do not persist the API key to ~/.panconfkeystore |
shadow-displaycurlrequest |
Print the raw curl invocation for every API call |
shadow-reducexml |
Strip whitespace from output XML |
shadow-json |
Emit stdout as a JSON document instead of human text (BETA) |
shadow-nojson |
Force human text output even if shadow-json was set elsewhere |
shadow-displayxmlnode |
Include matched XML element content in actions=display output |
shadow-saseapiqa |
Use SASE QA endpoints instead of production |
Note
The
outputformatsetargument is the safest way to preview a destructive change. It loads the input, evaluates every action, and writes the equivalent PAN-OSsetcommands to a file — paste those commands into the firewall CLI yourself if the diff looks right.
Where things live on disk
| Path | Purpose |
|---|---|
/tools/pan-os-php/utils/pan-os-php.php |
Single CLI entry point |
/tools/pan-os-php/utils/lib/*.php |
UTIL implementations (RULEUTIL.php, STATSUTIL.php, MERGER.php, RULEMERGER.php, KEYMANGER.php, BPAGENERATOR.php, OVERRIDEFINDER.php, etc.) |
/tools/pan-os-php/utils/common/*.php |
CallContext + Action implementations |
/tools/pan-os-php/lib/ |
Framework library (pan_php_framework.php is the entry the framework loads) |
/tools/pan-os-php/iron-skillet/ |
Bundled Palo Alto IronSkillet day-1 templates (PAN-OS 8.1, 9.1, 10.0, 10.1, 10.2, 11.0) |
/tools/pan-os-php/migration/parser/ |
Vendor migration parsers (Cisco ASA, Cisco ISR, Cisco Switch, NetScreen, SonicWall, Sophos, Fortinet, Juniper SRX, CheckPoint, CheckPoint R80, Huawei, StoneSoft, Sidewinder, pfSense) |
~/.panconfkeystore |
Cached API keys (one per device) |
For the full list of every config file, every supported argument with type and default, and the deep XML-API protocol details, see the Administrator & Developer Guide.
4. Core Usage
Every pan-os-php run follows the same five-stage pipeline, regardless of which type= you choose:
- Parse arguments (
PH::processCliArgs()). - Load config —
in=<file>reads XML off disk;in=api://...pulls it via the XML API and caches the API key. - Filter — apply
filter=<RQuery>to narrow the working set. - Act — run each
actions=<name>in order./separates multiple actions (actions=tag-Add:Old/disabled-Set). - Save — write changes back (
out=<file>for offline mode, push via API for online mode), and optionally emitsetcommands or stats.
Picking a type=
The type= argument selects the UTIL class. Group them by job:
Object editors (use type=<object>)
| Type | What it edits |
|---|---|
address |
Address objects and address groups |
service |
Service objects and service groups |
tag |
Tags |
schedule |
Schedule objects |
application |
Application objects (custom apps, app filters, app groups) |
threat |
Threat exceptions (vulnerability, anti-spyware) |
securityprofile |
URL filtering, AV, anti-spyware, vulnerability, file blocking, wildfire profiles |
securityprofilegroup |
Security profile groups |
Network editors
| Type | What it edits |
|---|---|
zone |
Zones |
interface |
Ethernet, aggregate-ethernet, loopback, tunnel, vlan, virtual-wire interfaces |
virtualwire |
Virtual wires |
routing |
Virtual routers and static routes |
static-route |
Just static routes |
dhcp |
DHCP servers / relays |
certificate |
Certificates |
Rule editors
| Type | What it edits |
|---|---|
rule |
All rule types: security, NAT, decryption, app-override, captive-portal, authentication, PBF, QoS, DoS, tunnel-inspection, SD-WAN, packet-broker, default-security |
rule-merger |
Merges adjacent / shadowed rules |
rule-compare |
Diffs two rule sets |
Object/group mergers
| Type | What it does |
|---|---|
address-merger |
Collapses duplicate address objects |
addressgroup-merger |
Collapses duplicate address groups |
service-merger |
Collapses duplicate service objects |
servicegroup-merger |
Collapses duplicate service groups |
tag-merger |
Collapses duplicate tags |
custom-url-category-merger |
Collapses duplicate custom URL categories |
Operations
| Type | What it does |
|---|---|
device |
Manages Panorama-managed devices |
bpa-generator |
Generates Palo Alto Best Practice Assessment input |
config-size |
Reports size/depth metrics for the loaded config |
config-commit |
Commits changes via API |
config-download-all |
Downloads every running/candidate config from a Panorama |
diff |
Diffs two configs |
override-finder |
Finds firewalls whose candidate config has drifted from the Panorama template |
xml-issue |
Reports XML structure issues |
xml-op-json |
Runs an XML-API op command and returns JSON |
xpath |
Runs an XPath query against a config |
upload |
Uploads a config to a device |
key-manager |
Manages cached API keys (~/.panconfkeystore) |
register-ip-mgr |
Registered-IP/dynamic address group manager |
userid-mgr |
User-ID mapping injector |
appid-enabler |
Promotes app-ID-aware versions of app-default services |
appid-toolbox |
Long-running App-ID activation pipeline |
playbook |
Runs an ordered playbook of UTIL invocations |
software-download / software-remove / software-preparation |
PAN-OS software image lifecycle |
traffic-log / system-log |
Log retrieval |
gratuitous-arp |
Triggers gratuitous ARP from a firewall |
license |
License operations |
spiffy |
SPIFFY policy migration helper |
gcp |
GCP integration helper |
ironskillet-update |
Refreshes the bundled IronSkillet templates |
maxmind-update |
Refreshes the bundled MaxMind GeoIP database |
protocoll-number-download |
Refreshes the IANA protocol number list |
download-predefined |
Downloads PAN-OS predefined objects |
html-merger |
Merges HTML reports |
tsf |
Tech Support File helper |
ssh-connector |
SSH connector for non-XML-API operations |
custom-report |
Custom-report exporter |
vendor-migration |
Vendor migration runner (drives the migration/parser/<vendor>/ modules) |
stats |
Pure read-only — print statistics about the loaded config |
util_get-action-filter |
Dumps every supported action and filter as JSON |
Picking a filter=
filter= accepts an RQuery expression — boolean logic over per-type operators. Run pan-os-php type=<X> listfilters to print every supported field/operator combo. Common idioms:
# All address objects whose name contains "lab"
'filter=(name regex /lab/)'
# Unused address objects (recursive: includes objects only used by other unused groups)
'filter=(object is.unused.recursive)'
# Security rules from any external zone to the Trust zone, disabled
'filter=((from has external) and (to has Trust) and (rule is.disabled))'
# Address objects whose IP value is fully or partially inside an aggregate
'filter=(value is.partially.or.fully.included.in.list,10.0.0.0/8)'
Filters compose with and, or, ! (NOT), and parentheses — and they always quote the whole expression to keep the shell out of it.
Picking actions=
actions= runs after the filter. Multiple actions chain with /. Examples:
# Tag every unused address object as Retired
pan-os-php type=address \
in=panorama.xml \
'filter=(object is.unused)' \
actions=tag-Add:Retired
# Rename every address object that starts with "OLD-" by stripping the prefix
pan-os-php type=address \
in=panorama.xml \
'filter=(name regex /^OLD-/)' \
actions=name-removePrefix:OLD-
# Disable every never-hit rule and append a description
pan-os-php type=rule \
in=api://10.0.0.5 \
'filter=(rule is.unused.fast)' \
actions=disabled-Set/description-Append:" - never used"
Loading from disk vs. from a live device
| Source | in= value |
What happens |
|---|---|---|
| Saved Panorama running config XML | in=panorama.xml |
XML loaded directly. out= required to save changes. |
| Firewall running config XML | in=firewall.xml |
XML loaded directly. out= required. |
| Live firewall (key flow) | in=api://10.0.0.2 |
Prompts for username/password the first time, requests an API key, stores it in ~/.panconfkeystore, then reuses it. Changes pushed straight to candidate config. |
| Live firewall (key already cached) | in=api://10.0.0.2 |
Reuses cached key. |
| Specific firewall managed by a Panorama | in=api://[email removed] |
Tunnels through Panorama to that serial. |
| SASE | in=sase-api://... |
Uses the SASE API connector (limited UTIL coverage; see key-manager for setup) |
Tip
Before running anything destructive against a live device, save a snapshot offline first (
in=api://... out=snapshot.xml) and iterate on the snapshot. Combine that withoutputformatset=preview.setto see exactly which CLI commands would have been issued.
Important
Quote your filter expressions. The shell will eat parentheses, pipes, and spaces otherwise. Wrap the whole
filter=...argument in single quotes:'filter=(from has external)'.
Output channels
| Argument | Effect |
|---|---|
out=after.xml |
Save edited config (offline mode only) |
outputformatset=changes.set |
Write equivalent PAN-OS set commands |
projectfolder=runs/today/ |
Place every generated file under this folder |
stats |
Print before/after object/rule counts |
shadow-json |
Emit machine-readable JSON instead of a human report |
Discovering what a UTIL can do
listactions and listfilters are your friends:
pan-os-php type=rule listactions
* delete | | |
==================================================================================================
* tag-Add | #1 tagName:string | <empty> |
==================================================================================================
* tag-Add-Force | #1 tagName:string | <empty> |
==================================================================================================
* enabled-Set | | |
==================================================================================================
* disabled-Set | | |
... [hundreds more rows]
pan-os-php type=rule listfilters
* name
- regex : matches name against a PCRE regex
- eq : exact name match
- contains : substring match
* from
- has : contains a zone with this name
- has.regex : zone name matches regex
... [dozens more]
The util_get-action-filter UTIL writes both lists for every type to utils/lib/util_action_filter.json — useful when you want to grep across types instead of running listactions for each one.
5. Common Tasks
This section is a copy-paste cookbook for the two jobs that fill most of an admin's week with pan-os-php: discovery (find what is there, what is unused, what shadows what, what overrides what) and reduction (merge duplicates, drop unused, normalize naming, move objects to the right scope). Each recipe follows the same structure: Goal · Prerequisites · Steps · Verify · If it fails.
Two safety patterns repeat throughout. Burn them into muscle memory before any destructive run:
- Dry-run first. For object and rule UTILs that accept
actions=, replace the destructive action withactions=displayto see what the filter matches. For mergers, passactions=display(the merger framework treatsdisplayas "compute the merge plan, print it, write nothing"). Then re-run with the destructive action only after the diff looks right. - Snapshot live devices. When
in=api://..., immediately stop and re-run within=api://... out=snap.xmlso you have a known-good offline copy. Iterate againstsnap.xmluntil you trust the change, then push it.
Caution
Mergers and bulk renames rewrite references everywhere — security rules, NAT rules, address groups, IKE gateway peer addresses, route-map next-hops, and any other place an object is mentioned by name. Always preview with
actions=displayand (where supported)outputformatset=preview.setbefore the real run. See GitHub issue #793 for an example of how merger-related filter behaviour can drift between releases.
5.1 — Discovery: find every unused address object on a firewall
Goal: flag dead addresses on a single firewall so they can be retired.
Prerequisites: A saved firewall config XML, or API access to the live device.
Steps:
pan-os-php type=address \
in=firewall-running.xml \
location=any \
'filter=(object is.unused)' \
actions=display
For a CSV-friendly export instead of human text, swap actions=display for actions=exportToExcel:unused-addresses.xlsx,WhereUsed|UsedInLocation.
Verify: every line in stdout corresponds to an address object name. The trailing summary reads **** PROCESSING OF N OBJECTS DONE ****. Cross-check N against pan-os-php type=address in=firewall-running.xml stats to know what fraction of the address store is dead.
If it fails: if you get cannot find vsys/device-group <name>, the input is from a different scope than location= expects. Add location=any so every VSYS / DG is scanned. If the count looks suspiciously low, switch to recipe 5.2 (is.unused.recursive) — non-recursive misses objects that are referenced only by another unused group.
5.2 — Discovery: find every unused address object on a Panorama (recursive)
Goal: flag dead addresses across an entire Panorama, including ones whose only reference is another unused group.
Prerequisites: A saved Panorama config XML.
Steps:
pan-os-php type=address \
in=panorama-running.xml \
location=any \
'filter=(object is.unused.recursive)' \
actions=display
The recursive matters because of how Panorama device-group inheritance works: an address object can sit in shared while its only reference is addr-group-A (also in shared), which is itself only referenced by addr-group-B (in a child DG), which is itself unused. Plain is.unused only flags addr-group-B; is.unused.recursive walks the chain and flags all three.
Verify: spot-check a few of the flagged objects in the Panorama GUI under Objects > Addresses — the Used in column should be empty.
If it fails: see the open is.unused.recursive correctness issue (#791 in the project tracker). For high-stakes deletions, run both is.unused and is.unused.recursive, diff the results, and review anything that appears in only one set.
5.3 — Discovery: find every unused service object
Goal: the same dead-object hunt, but for service objects.
Steps:
pan-os-php type=service \
in=panorama.xml \
location=any \
'filter=(object is.unused)' \
actions=display
pan-os-php type=service \
in=panorama.xml \
location=any \
'filter=(object is.unused)' \
actions=exportToExcel:unused-services.xlsx,WhereUsed
Verify: the WhereUsed column on the Excel export is empty for every flagged row.
If it fails: services with the same port/protocol but different names sometimes appear unused on one side and used on the other. Run recipe 5.12 (service-merger) afterwards to consolidate name-different / value-identical services first, then re-discover.
5.4 — Discovery: find every unused tag
Goal: flag tags nobody is referencing so they can be deleted from the legend.
Steps:
pan-os-php type=tag \
in=panorama.xml \
location=any \
'filter=(object is.unused)' \
actions=display
Verify: the count printed at the end matches the count you see in Objects > Tags > Unused in the Panorama GUI (refresh required).
If it fails: is.unused for tags ignores rule group-tag references on some PAN-OS versions. Cross-check with a type=rule pass: pan-os-php type=rule in=panorama.xml 'filter=(group-tag is.set)' actions=display — any group-tag listed there should NOT appear in the unused-tag set.
5.5 — Discovery: find every rule that has never been hit
Goal: identify dead security rules — rules with zero traffic since the device started counting.
Prerequisites: API access to the firewall or Panorama (hit-count data only exists on live devices). For Panorama: identify whether the rules of interest are pre-rules or post-rules.
Steps:
pan-os-php type=rule \
in=api://10.0.0.5 \
'filter=(rule is.unused.fast)' \
actions=display
For Panorama:
pan-os-php type=rule \
in=api://panorama.local \
panoramaPreRules \
'filter=(rule is.unused.fast)' \
actions=display
(Repeat with panoramaPostRules for the post-rulebase.)
To get hit-count detail with the rule list, add actions=rule-hit-count-show:
pan-os-php type=rule \
in=api://10.0.0.5 \
'filter=(rule is.unused.fast)' \
actions=display/rule-hit-count-show
Verify: the rule-hit-count-show output prints hit-count: 0 for every flagged rule, plus last-hit-timestamp and rule-creation-timestamp. A rule created yesterday with hit-count 0 is not a great candidate for deletion; one created two years ago with hit-count 0 is.
If it fails: fresh installs and devices that recently rebooted have empty hit counters; the filter will correctly match every rule until traffic catches up. Wait at least one full business cycle (typically a week) after a reboot before treating is.unused.fast as authoritative.
5.6 — Discovery: find rules unused beyond a date threshold
Goal: narrow the "never hit" set to "not hit in N days" by combining the rule filter with the hit-count operator.
Steps:
# Rules whose hit count is < 1 (i.e., zero hits in the API window)
pan-os-php type=rule \
in=api://10.0.0.5 \
'filter=(hit-count.fast < 1)' \
actions=display
For a more targeted "haven't been hit in 90 days" approach, follow the hit-count export pattern — run rule-hit-count-show for every rule in scope, capture the last-hit-timestamp field, and post-filter externally. The exportToExcel action with additionalFields=HitCount gives you a spreadsheet you can sort:
pan-os-php type=rule \
in=api://10.0.0.5 \
'filter=(rule is.enabled)' \
actions=exportToExcel:hit-counts.xlsx,HitCount
Verify: open the spreadsheet, sort by last-hit, and review the rules whose last hit predates your cutoff date.
If it fails: see the comment under recipe 5.5 about reboots. Also note that hit-count behaviour changes with PAN-OS rule-modification events: editing any field (description, tag, action) on some PAN-OS versions resets the rule-creation-timestamp and may reset the hit counter.
5.7 — Discovery: who references this object?
Goal: answer the perennial question — "if I delete addr-prod-db, what breaks?"
Steps:
pan-os-php type=address \
in=panorama.xml \
location=any \
'filter=(name eq addr-prod-db)' \
actions=displayReferences
The displayReferences action lists every place in the config — every rule (security, NAT, decryption, etc.), every group, every interface, every IKE gateway — that mentions the object by name.
For a tag-based cohort, swap the filter for the broader cohort and the action stays the same:
pan-os-php type=address \
in=panorama.xml \
location=any \
'filter=(tag has Production)' \
actions=displayReferences
Verify: the count of references printed equals what the Panorama GUI shows in the Used In column for that object.
If it fails: if displayReferences returns nothing for an object you can clearly see in a rule, the object name is being matched case-sensitively — confirm with 'filter=(name eq.nocase addr-prod-db)' (operator eq.nocase does case-insensitive match for address objects).
5.8 — Discovery: address objects scoped by tag, group, or supernet
Goal: assemble lists of objects matching real-world facets — "all production-tagged hosts", "all members of the DMZ-Hosts group", "everything in the 10.0.0.0/8 supernet".
Steps:
# Everything tagged Production
pan-os-php type=address \
in=panorama.xml location=any \
'filter=(tag has Production)' \
actions=display
# Everything inside an address group (recursive — picks up nested groups)
pan-os-php type=address \
in=panorama.xml location=any \
'filter=(object is.recursive.member.of,DMZ-Hosts)' \
actions=display
# Everything inside a /8 supernet (only matches ip-netmask and ip-range objects)
pan-os-php type=address \
in=panorama.xml location=any \
'filter=(value is.partially.or.fully.included.in.list,10.0.0.0/8)' \
actions=display
# Everything that is an IPv6 address
pan-os-php type=address \
in=panorama.xml location=any \
'filter=(object is.ipv6)' \
actions=display
Verify: the count of matched objects matches what you would manually count from the GUI filter.
If it fails: if is.partially.or.fully.included.in.list returns nothing or the wrong set, check whether you are running into the regression tracked in #793 — fall back to running is.partially.included.in.list and is.fully.included.in.list separately and combining the results with or.
5.9 — Discovery: rules + zones + actions to a CSV/Excel for review
Goal: dump every security rule with its zones and action into a spreadsheet for offline review (audit, peer review, change-control attachment).
Steps:
pan-os-php type=rule \
in=panorama.xml \
location=any \
panoramaPreRules \
actions=exportToExcel:rules-prerule.xlsx,ResolveAddressSummary|ResolveServiceSummary|HitCount
The additionalFields after the filename are pipe-separated and supported choices include ResolveAddressSummary, ResolveServiceSummary, ResolveServiceAppDefaultSummary, ResolveApplicationSummary, ResolveScheduleSummary, ApplicationSeen, and HitCount.
Verify: open the resulting .xlsx. Every row should have populated from, to, source, destination, service, and action columns plus the resolved-summary columns you asked for.
If it fails: the exporter relies on python3-xlsxwriter (installed by the Ubuntu install scripts). If you used Docker, the image already has it; if you did a custom install, pip3 install xlsxwriter pandas.
5.10 — Discovery: find Panorama-template overrides on managed firewalls
Goal: find every managed firewall whose candidate config has drifted from the Panorama template.
Steps:
pan-os-php type=override-finder \
in=api://panorama.local \
cycleconnectedfirewalls
The override-finder UTIL is API-only — it pulls the connected-firewalls list from Panorama, then for each firewall walks its candidate config and reports every XPath where the firewall-local value differs from what the template defines. Source: utils/lib/OVERRIDEFINDER.php.
Verify: every line printed is a fully-qualified XPath into /config/devices/... along with which value won.
If it fails: override-finder requires the Panorama API; it will refuse to run against an offline file. If you only have a Panorama XML, you cannot use this UTIL — you have to either temporarily get API access or use type=diff between the template and a snapshot of each firewall.
5.11 — Reduction: merge duplicate address objects across a Panorama hierarchy
Goal: collapse address objects that have identical values but different names so rules / groups / NAT rules all point at one canonical name.
Prerequisites: A snapshot of the Panorama config (pan-os-php type=address in=api://panorama.local out=before.xml).
Steps:
# Step 1 - DRY RUN. The merger treats actions=display as "compute the plan, write nothing"
pan-os-php type=address-merger \
in=before.xml out=after.xml \
location=any \
allowMergingWithUpperLevel \
actions=display
# Step 2 - REAL RUN once the dry-run output looks correct
pan-os-php type=address-merger \
in=before.xml out=after.xml \
location=any \
allowMergingWithUpperLevel
allowMergingWithUpperLevel is the key argument: with it, an address object in a child device-group that has an identical-value sibling in shared (or any ancestor DG) is replaced by the upper-level one and deleted from the child. Without it, mergers run only inside each DG independently.
Verify:
pan-os-php type=address in=after.xml stats
pan-os-php type=xml-issue in=after.xml
The post-merge address count should be lower; xml-issue should report a clean tree.
If it fails: if you see there is a conflict with an object of same name and type. Please use address-merger.php script with argument 'allowmergingwithupperlevel', you forgot the upper-level flag. If the merger candidate set looks wrong, see GitHub issues #778 and #779 (both closed) for tag-related edge cases — and update via update_pan-os-php.sh before reporting a new bug.
5.12 — Reduction: merge duplicate service objects
Goal: collapse services with identical port/protocol but different names. Same dry-run-then-commit pattern as 5.11.
Steps:
# DRY RUN
pan-os-php type=service-merger \
in=before.xml out=after.xml \
location=any \
allowMergingWithUpperLevel \
DupAlgorithm=SamePorts \
actions=display
# REAL RUN
pan-os-php type=service-merger \
in=before.xml out=after.xml \
location=any \
allowMergingWithUpperLevel \
DupAlgorithm=SamePorts
DupAlgorithm selects how duplicates are detected. For services the supported values are SamePorts (port-and-protocol match — the default and most common) and Whereused (objects referenced from the same set of rules).
Verify: pan-os-php type=service in=after.xml stats shows fewer services; pan-os-php type=xml-issue in=after.xml is clean.
If it fails: see unsupported value for dupAlgorithm: <name> in section 6 — DupAlgorithm is case-sensitive on input.
5.13 — Reduction: merge duplicate tags
Goal: collapse tag duplicates (e.g. Production and production and PROD) into one canonical tag.
Steps:
# DRY RUN
pan-os-php type=tag-merger \
in=before.xml out=after.xml \
location=any \
allowMergingWithUpperLevel \
actions=display
# REAL RUN
pan-os-php type=tag-merger \
in=before.xml out=after.xml \
location=any \
allowMergingWithUpperLevel
tag-merger rewrites every reference to the merged-away tag — on rules, on objects, on groups — to the surviving tag. If you have a curated tag taxonomy, run a name-Replace-Character pass (recipe 5.15) on the survivors first to enforce naming consistency, then merge.
Verify: pan-os-php type=tag in=after.xml stats; the unused-tag query (recipe 5.4) against after.xml returns the orphaned tags from the merge, ready for actions=delete.
If it fails: if a tag survives merging that you expected to disappear, check whether it has tag-on-tag references — colour assignments and parent-tag relationships — that a merger may treat as a pinning reference. Re-run with actions=display and read the per-tag plan output.
5.14 — Reduction: merge duplicate address groups and service groups
Goal: consolidate redundant groups whose membership lists are identical.
Steps:
# DRY RUN address groups
pan-os-php type=addressgroup-merger \
in=before.xml out=after.xml \
location=any \
allowMergingWithUpperLevel \
DupAlgorithm=SameMembers \
actions=display
# REAL RUN
pan-os-php type=addressgroup-merger \
in=before.xml out=after.xml \
location=any \
allowMergingWithUpperLevel \
DupAlgorithm=SameMembers
For address groups, DupAlgorithm supports SameMembers (literal member-list equality), SameIP4Mapping (groups whose resolved IP set is identical even when membership lists differ), and Whereused (groups referenced from the same rules). For service groups, the same pattern with DupAlgorithm=SameMembers or SamePortMapping.
Verify: before-and-after group counts; spot-check a few previously-duplicate groups in the GUI.
If it fails: see the open issue #798 for known address-group-merger edge cases — fall back to addressgroup-merger with a tighter location=DG-Name scope and run iteratively per device-group rather than location=any if the candidate set looks wrong.
5.15 — Reduction: bulk rename to normalize naming
Goal: apply a naming convention — prefix, suffix, character replacement, regex rewrite — across hundreds of objects in one pass.
Steps:
# Add a prefix to every object whose name starts with "dev-"
pan-os-php type=address \
in=before.xml out=after.xml \
'filter=(name regex /^dev-/)' \
actions=name-addPrefix:LAB-
# Strip an obsolete prefix
pan-os-php type=address \
in=before.xml out=after.xml \
'filter=(name regex /^OLD-/)' \
actions=name-removePrefix:OLD-
# Append a suffix
pan-os-php type=address \
in=before.xml out=after.xml \
'filter=(tag has Decommissioning)' \
actions=name-addSuffix:_RETIRED
# Replace a forbidden character (spaces -> underscore)
pan-os-php type=address \
in=before.xml out=after.xml \
'filter=(name regex / /)' \
actions=name-Replace-Character:" ",_
# Direct rename of one specific object
pan-os-php type=address \
in=before.xml out=after.xml \
'filter=(name eq h-10.0.0.5)' \
actions=name-Rename:srv-prod-db
The name-Replace-Character:<search>,<replace> syntax accepts the helper aliases $$comma$$, $$forwardslash$$, $$colon$$, and $$pipe$$ to escape characters that the shell or argument parser would otherwise eat.
Verify:
grep '<entry name="LAB-' after.xml | wc -l
should match the change count printed at the end of the run.
If it fails: the action is global within the matched filter set, so a wrong filter rewrites the wrong objects. Always pair the rename with outputformatset=rename.set so the equivalent CLI commands are emitted to a file you can review:
pan-os-php type=address \
in=before.xml \
'filter=(name regex /^dev-/)' \
actions=name-addPrefix:LAB- \
outputformatset=rename-preview.set
Note the closed issue #764: when renaming an address object, references inside interface configs (e.g. IKE gateway peer-address set to an address object name) were not always rewritten on older builds. After a rename, run actions=display against type=interface to confirm no orphan references remain.
5.16 — Reduction: quarantine-then-delete unused rules
Goal: retire dead security rules without deleting them on day one. Tag-and-disable now, audit for a watch period, delete only after no one complains.
Phase 1 (today): tag and disable.
pan-os-php type=rule \
in=api://10.0.0.5 \
'filter=(rule is.unused.fast)' \
actions=tag-Add-Force:Quarantine-2026Q2/disabled-Set \
outputformatset=quarantine.set
Review quarantine.set to make sure the rule list looks right, then re-run without outputformatset to push to the device. tag-Add-Force is used here instead of plain tag-Add so the tag is created on first reference if it does not already exist.
Phase 2 (after the watch period — typically 30 days): delete.
pan-os-php type=rule \
in=api://10.0.0.5 \
'filter=((tag has Quarantine-2026Q2) and (rule is.disabled))' \
actions=display
# After confirming the displayed list is what you expect:
pan-os-php type=rule \
in=api://10.0.0.5 \
'filter=((tag has Quarantine-2026Q2) and (rule is.disabled))' \
actions=delete
Verify: between phase 1 and phase 2, monitor the Monitor > Logs GUI for any traffic that gets dropped because of the disabled-but-still-named rules (PAN-OS will log "denied: implicit deny" at the end of the rulebase). Any such traffic means a rule was wrongly quarantined — re-enable just that rule.
If it fails: see closed issue #786 for the historical workaround — older builds required a tag (or zone) to already be present in the rulebase before bulk-add could reference it. If you hit that on an older install, update via update_pan-os-php.sh.
5.17 — Reduction: merge shadowed and adjacent security rules
Goal: find groups of security rules that match identical traffic ("matchFromToSrcDstApp" — same from-zones, to-zones, source, destination, app) and collapse them into one.
Prerequisites: API or saved Panorama config; decision on whether to merge across the entire rulebase (mergeAdjacentOnly=no) or only consecutive duplicates (mergeAdjacentOnly=yes).
Steps:
# DRY RUN - preview the merge plan
pan-os-php type=rule-merger \
in=panorama.xml out=after.xml \
location=any \
panoramaPreRules \
method=matchFromToSrcDstApp \
actions=display
# REAL RUN
pan-os-php type=rule-merger \
in=panorama.xml out=after.xml \
location=any \
panoramaPreRules \
method=matchFromToSrcDstApp
The method= argument selects what counts as "the same rule". The supported values are:
| Method | Considers identical when these fields match |
|---|---|
matchFromToSrcDstApp |
from, to, source, destination, application |
matchFromToSrcDstSvc |
from, to, source, destination, service |
matchFromToSrcSvcApp |
from, to, source, service, application |
matchFromToDstSvcApp |
from, to, destination, service, application |
matchFromSrcDstSvcApp |
from, source, destination, service, application |
matchToSrcDstSvcApp |
to, source, destination, service, application |
matchToDstSvcApp |
to, destination, service, application |
matchFromSrcSvcApp |
from, source, service, application |
identical |
every rule field |
Optional refinements: additionalMatch=tag,secprof,user,urlcategory,target,logsetting,logprof to extend the equality check; mergeAdjacentOnly=yes to only collapse runs of consecutive duplicates; mergeDenyRules=yes to allow merging across deny rules (default no — deny-then-allow patterns matter); stopMergingIfDenySeen=no to keep going past the first deny.
Verify: before-and-after rule counts; for a Panorama, run a type=diff in=panorama.xml in2=after.xml to inspect every difference.
If it fails: Panorama was detected but no Pre or Post rules were selected, use CLI argument 'panoramaPreRules' or 'panoramaPostRules' means exactly that — pick one. See the open issue #572 for known cases where not all shadow rules get merged on a single pass; running the merger twice with different method= values often catches the rest.
5.18 — Reduction: move objects to a parent device-group to deduplicate across child DGs
Goal: if addr-prod-db lives identically in DG-East, DG-West, and DG-Central, move it to a parent DG (or shared) so all three children inherit one canonical copy.
Steps:
# DRY RUN
pan-os-php type=address \
in=before.xml out=after.xml \
'filter=(name eq addr-prod-db)' \
actions=display
# REAL RUN
pan-os-php type=address \
in=before.xml out=after.xml \
'filter=(name eq addr-prod-db)' \
actions=move:shared,skipIfConflict
The move action takes two arguments: a target location (shared or a DG/VSYS name) and a conflict-handling mode. The supported modes are skipIfConflict (don't move if an object of the same name already exists at the target — the safe default), removeIfMatch (move and silently remove the duplicate if its value also matches), and removeIfNumericalMatch (move and remove the duplicate if its numerical value is identical, even if the textual representation differs).
For a bulk move based on tag:
pan-os-php type=address \
in=before.xml out=after.xml \
'filter=(tag has CanonicalShared)' \
actions=move:shared,skipIfConflict
Verify: in the Panorama GUI, the moved object now appears under shared; child DGs show the inherited object (greyed-out edit pencil).
If it fails: if the target location already has an object with the same name but different value, skipIfConflict simply leaves the source alone — no error, no move. Switch to actions=display first and look for the lines reading there is a conflict with an object of same name and type. If you genuinely want to overwrite, use address-merger with allowMergingWithUpperLevel instead (recipe 5.11) — that handles the conflict resolution properly.
5.19 — Reduction: replace, decommission, or delete a specific object
Goal: stop using an object, replacing references where possible, then remove it.
Steps:
# Replace every reference to OLD-DB with NEW-DB across the entire config
pan-os-php type=address \
in=before.xml out=after.xml \
'filter=(name eq OLD-DB)' \
actions=replaceWithObject:NEW-DB
# Or "decommission" (a soft-delete pattern that flags + renames the object for audit)
pan-os-php type=address \
in=before.xml out=after.xml \
'filter=(name eq OLD-DB)' \
actions=decommission
# Or hard delete after confirming zero references with displayReferences (recipe 5.7)
pan-os-php type=address \
in=before.xml out=after.xml \
'filter=(name eq OLD-DB)' \
actions=delete
actions=delete refuses to delete an object that still has references; that is deliberate. actions=delete-Force overrides the safety check — only use it when you have already audited every reference with displayReferences. actions=removeWhereUsed walks every rule and group, strips the object from each member list, and then deletes — useful when the object's "real" delete blocks because of a stale group.
Verify: after replaceWithObject, pan-os-php type=address in=after.xml 'filter=(name eq OLD-DB)' actions=displayReferences should print zero references.
If it fails: see the closed issue #771 if removeWhereUsed:delete returns an error — update via update_pan-os-php.sh. If delete complains about references that the GUI says don't exist, run pan-os-php type=xml-issue in=before.xml first; orphaned XML nodes can show up as references the renderer doesn't display.
6. Troubleshooting
Each entry uses Error / Cause / Solution / Prevention format. Real GitHub issue numbers are referenced where they fit.
Error: *** ERROR **** PAN-OS-PHP requires PHP version >= 5.5
Cause: The framework's first runtime check refuses PHP 5.4 or older.
Solution: Install PHP 7.4+ via your distro's package manager (apt install php8.1, yum install php74, or use the Remi repo on CentOS), or skip the host PHP entirely with the Docker image.
Prevention: Use the bundled install scripts (install_script_ubuntu22.sh, install_script_centos7.sh) — they install the supported PHP version automatically.
Error: unsupported argument provided: <name>
Cause: You passed an argument= that is not in the UTIL's supportedArguments table. Common typos: inn= instead of in=, action= (singular) instead of actions=. Source: utils/lib/UTIL.php:640, utils/develop/ike.php:106, utils/develop/wildcard_calculator.php:71.
Solution: Run pan-os-php type=<X> help to see the exact argument list.
Prevention: Enable bash completion (set_alias_usage.sh does this for you) — Tab will only offer real argument names.
Error: unsupported Action: <name>
Cause: The action name is misspelled, or the action exists for a different type=. Action names are case-sensitive. Source: utils/lib/UTIL.php:1171.
Solution: pan-os-php type=<X> listactions lists every action and its argument schema.
Prevention: Look the action up in utils/lib/util_action_filter.json (regenerate with type=util_get-action-filter).
Error: argument actions only support value: merge / display / mergenoghost
Cause: You passed an action other than merge, display, or mergenoghost to a merger UTIL (address-merger, service-merger, tag-merger, etc). Mergers do not accept the standard object-action vocabulary.
Solution: Use actions=display to dry-run, actions=merge (or no actions at all — that is the default) for the real run, or actions=mergenoghost to skip the ghost-mode safety pass.
Error: Could not connect to LDAP server.
Cause: The user-check-ldap action needs a reachable LDAP server defined in the input config. Source: utils/common/actions-rule.php:5137.
Solution: Verify connectivity from your jump host (ldapsearch -H ldap://...) and that the Panorama config has an LDAP server profile referenced from the rule's source-user list.
Prevention: Run actions=display first to preview which rules would have their users validated; address any missing LDAP-server-profile entries before retrying.
Error: Panorama was detected but no Pre or Post rules were selected, use CLI argument
Cause: Source: utils/lib/RULEMERGER.php:839. When loading a Panorama config you must scope rule operations to either pre-rulebase or post-rulebase. Both at once is also rejected.
Solution: Add exactly one of panoramaPreRules or panoramaPostRules to the command.
Prevention: Save the chosen direction into your playbook so the same scope is used every run.
Error: cannot find vsys/device-group <name>
Cause: location= references a name that does not exist in the loaded config. Source: utils/develop/grp-static-to-dynamic.php:573.
Solution: Run pan-os-php type=device in=... actions=display first to list the actual VSYS/DG names; copy the exact name (case-sensitive) into location=.
Error: Bug-08062022-2 "create-address-from-file" with force group = yes and rewrite = yes
Cause: Reproducible glitch in the create-address-from-file action — see GitHub issue #588.
Solution: Avoid force group = yes together with rewrite = yes in the same invocation; split into two passes.
Error: removeWhereUsed:delete failing / description-append is failing
Cause: Known bugs in specific actions, see GitHub issues #771 and #772 (both closed).
Solution: Update to the current main (bash update_pan-os-php.sh) — both fixes have landed since report.
Error: interfaces using address-objects do not get updated after an address-object rename
Cause: GitHub issue #764 (closed). When renaming an address object via name-Rename, references inside interface configurations (e.g., IKE gateway peer addresses, interface IP overrides) were not always rewritten.
Solution: After the rename, run actions=display with a filter that scans both rules and interfaces; manually patch any interfaces still pointing at the old name. The replace-IP-by-MT-like-Object action is a more robust alternative for some cases.
Error: New zone can't be added to rules unless it already exists in a rule
Cause: GitHub issue #786 (closed) — the zone has to be present somewhere in the rulebase before from-Add/to-Add accepts it.
Solution: Update via update_pan-os-php.sh, or manually create a throwaway rule that already references the new zone before re-running the bulk add. The *-Add-Force family of actions also bypasses the existence check.
Error: is.partially.or.fully.included.in.list filter does not work as expected
Cause: GitHub issue #793 (closed) — Bug-11122024-1.
Solution: Update to current main. If still reproducible, fall back to is.partially.included.in.list and is.fully.included.in.list separately and combine with or.
Error: Address Unused Recursive Filter inconsistencies
Cause: GitHub issue #791 (open) — is.unused.recursive was over- or under-counting in some hierarchical-DG scenarios.
Solution: Use both is.unused and is.unused.recursive and compare results; manually validate flagged objects against the GUI before deletion. Track issue #791 for status.
Error: error when running addressgroup-merger
Cause: GitHub issue #798 (open). Edge cases in tag matching during address-group merging.
Solution: Run with actions=display and inspect which groups would be merged; if the candidate set looks wrong, fall back to manual merging via repeated address-merger runs scoped by tag.
Error: outputformatset doesn't take into account multi-vsys firewall command format
Cause: GitHub issue #677 (closed). Set-format command output did not always include the set vsys vsysN prefix on multi-vsys firewalls.
Solution: Update via update_pan-os-php.sh.
Error: PHP Warning: include(...): failed to open stream
Cause: PHP cannot find framework files because include_path is not set or because the working directory is not the install dir.
Solution: php -i | grep include_path — if the install path is missing, append it to the right php.ini (/etc/php/8.1/cli/php.ini on Ubuntu 22.04).
Prevention: Always invoke via the pan-os-php alias rather than running php utils/pan-os-php.php from arbitrary directories.
Error: Unable to write to file!
Cause: From utils/lib/logWriter.php:75, utils/lib/SOFTWARE_DOWNLOAD.php:1132, utils/lib/LICENSE__.php:450, and the migration helpers (utils/develop/migration/bluecoat.php:116, etc.) — the process cannot write the output. Usually a permission issue or a missing parent directory.
Solution: Check write permission on the target directory (projectfolder= location, current directory if out= is relative). Create it if missing.
Caution
Do not run
pan-os-phpas root unless required — many UTILs write into the current working directory by default, and root-owned outputs are a recurring support headache. Run as your own user, withprojectfolder=pointing somewhere you own.
Error: invalid JSON in util_action_filter.json
Cause: The file is regenerated by type=util_get-action-filter. If a previous run was interrupted, the file may be truncated.
Solution: Delete utils/lib/util_action_filter.json and re-run pan-os-php type=util_get-action-filter.
Error: Running PAN-OS-PHP on Panorama File (no API)
Cause: GitHub issue #792 (open). Running purely against a Panorama-exported XML file (no API) hits edge cases in some UTILs that assume API access.
Solution: For affected UTILs, snapshot the device's current running config to disk via in=api://... out=snap.xml, then iterate offline against snap.xml. Track issue #792 for full no-API support.
Error: unsupported value for dupAlgorithm: <name>
Cause: Source: utils/lib/MERGER.php:628. DupAlgorithm= was passed an algorithm name the merger does not implement. The supported values per merger type are SamePorts / Whereused (service-merger), SameMembers / SameIP4Mapping / Whereused (addressgroup-merger), SameMembers / SamePortMapping / Whereused (servicegroup-merger).
Solution: Run type=address-merger help (or any other merger) to list valid algorithms, or simply omit DupAlgorithm and let the merger pick.
Diagnostic decision tree
When something fails, work down this list:
- Did the framework even load? Run
pan-os-php version— if the version line prints, PHP and the autoloader are fine; the problem is in your invocation. - Is the right type? Run
pan-os-php type=<your-type> help. If you get usage rather than your error, the framework is happy with the type. - Is the filter valid? Replace
actions=...withactions=display. If it still fails, the filter or input is at fault. - Is the action valid? Run
pan-os-php type=<your-type> listactionsand confirm the spelling and argument count. - API issues? Append
debugapiandshadow-displaycurlrequestto see exactly what is being called. - Memory / timeouts?
debugloadtimeshows where time is spent; the framework already raisesmemory_limitto 14 GB but very large Panoramas can still exceed it.
7. FAQ
Q: What versions of PAN-OS does pan-os-php support?
The bundled IronSkillet templates cover PAN-OS 8.1, 9.1, 10.0, 10.1, 10.2, and 11.0, and the framework parses configurations from those versions out of the box. The XML schema is validated against lib/resources/ schema files copied from the official Palo Alto product. If you load a config from a newer PAN-OS than the framework knows, you will see warnings like *** Unsupported PANOS version: ... from utils/develop/grp-static-to-dynamic.php (and similar) — update via update_pan-os-php.sh.
Q: Does pan-os-php support Strata Cloud Manager / SASE?
Partially. There is a PanSaseAPIConnector and a sase-api:// URL form, but the framework explicitly errors with PAN-OS-PHP connection method 'sase-api://' - do not yet support this UTIL type for many UTILs. Check key-manager and the SASE-specific operators before relying on it.
Q: How do I avoid passing the API key on the command line?
Run any UTIL with in=api://<host> interactively the first time. The framework prompts for username/password, requests an API key from PAN-OS, and writes it to ~/.panconfkeystore. Subsequent runs reuse the key. Disable persistence with shadow-apikeynosave.
Q: Where do I configure the API timeout?
apitimeout=<seconds> per invocation. Default is 60 seconds. Long-running operations like bpa-generator cycleconnectedfirewalls against a Panorama with many devices may need 300–600.
Q: Why does my Panorama config require location=?
By default the framework runs against shared for Panorama and vsys1 for firewalls. To act on a specific device-group, set location=<DGname>; to act everywhere, location=any; to include child device-groups, location=DGname:includechilddgs; to act only on the immediate children, location=DGname:excludeMaindg.
Q: Can I write my own action?
Yes. Drop a PHP file under utils/common/ that extends the relevant *CallContext (e.g., RuleCallContext) and adds an entry to its $supportedActions array. Load it at runtime with loadplugin=path/to/myplugin.php. The example examples/example_plugin_rule_display_source_destination.php shows the pattern.
Q: Does outputformatset= cover every action?
No — some destructive or vendor-specific actions emit no set command equivalent (the change is purely API-driven). For those, the file will simply not include a line for that action; the human report still describes what happened.
Q: How do I handle a malformed input config?
pan-os-php type=xml-issue in=fw.xml walks the tree and reports structural problems — empty entries, missing required attributes, duplicate names. Fix those first; most UTILs will refuse to operate on a config that fails this check.
Q: How can I make the output JSON-friendly for piping into another tool?
Append shadow-json to any UTIL invocation. The output goes from plain text + tables to a single JSON document on stdout. This is documented as BETA inside UTIL.php; adopt it for tooling but expect occasional schema changes.
Q: Can I run pan-os-php in CI?
Yes — the Docker image swaschkut/pan-os-php:latest is the simplest path. Mount your repo at /share, point in= at /share/<config>.xml, and write outputs to /share/out/. Use shadow-json for machine-readable results.
Q: What is iron-skillet/?
Vendored copies of Palo Alto's IronSkillet day-1 hardening templates — one folder per major PAN-OS version — used by the ironskillet-update UTIL. You normally do not need to touch them directly.
Q: What is migration/parser/<vendor>/?
Per-vendor parsers used by type=vendor-migration to convert non-Palo configurations (Cisco ASA, Cisco ISR, Cisco Switch, NetScreen, SonicWall, Sophos, Fortinet, Juniper SRX, CheckPoint R80, Huawei, StoneSoft, Sidewinder, pfSense) into PAN-OS XML. Each parser is a PARSER subclass that extends CONVERTER (which extends UTIL).
Q: What is appid-toolbox/?
A self-contained sub-toolset (with its own lib/, tests/, and report-generator.php / rule-cleaner.php / rule-cloner.php / rule-marker.php / rule-activation.php entry scripts) for the multi-week App-ID activation workflow. The companion UTIL type=appid-toolbox drives it from pan-os-php itself.
Q: How do I report a bug?
Open an issue at https://github.com/PaloAltoNetworks/pan-os-php/issues. Include your pan-os-php version output, the exact command line, a redacted snippet of the input XML around the affected element, and the full error stack (the framework prints a backtrace on derr()). See SUPPORT.md in the repo for the official policy: pan-os-php is community-supported and not covered by Palo Alto Networks TAC.
Q: How do I retire pan-os-php cleanly?
bash remove_path_variable.sh undoes the php.ini and shell rc changes; rm -rf /tools/pan-os-php removes the source; rm ~/.panconfkeystore removes cached keys.
Q: What does the version number 2.1.20 mean?
Source lib/misc-classes/PH.php defines library_version_major=2, library_version_sub=1, library_version_bugfix=20. Major bumps reflect breaking framework changes (new class hierarchies, changed action signatures); sub bumps add new UTILs/actions; bugfix bumps fix specific reported bugs. Pin a major version in CI; expect new actions inside a sub series.