This guide covers the parts of PaloAltoNetworks/pan-os-php that the operator, architect, and contributor care about: the bootstrap order of the framework, the class hierarchy that underpins every UTIL, the XML-API protocol details, the security posture of the API keystore, the deployment patterns that scale, and the extension points for writing new actions, filters, and parsers.
If you are looking for "how do I tag every unused address object" recipes, those live in the User Guide. This document is its companion.
1. Architecture Overview
pan-os-php is a single-process PHP framework with no daemon, no database, and no inter-process state. Each invocation is a complete pipeline:
- The shell alias
pan-os-phpcallsphp -r "require_once 'utils/pan-os-php.php';" $@. utils/pan-os-php.phprequire_once'slib/pan_php_framework.php, which is the actual framework boot sequence — it fixes PHP runtime settings (memory_limit=14512M,set_time_limit(0),error_reporting(E_ALL),gc_enable(),date_default_timezone_set('UTC')), definesSTDIN/STDOUT/STDERRfor non-CLI contexts, registers a custom error handler that re-throws PHP notices and warnings as fatalderr()calls, registers a shutdown function that disables GC, and thenrequire_once's the entire framework (phpseclib,git-php, thelib/misc-classes/,lib/helper-classes/,lib/container-classes/,lib/object-classes/,lib/device-and-system-classes/,lib/network-classes/, andlib/rule-classes/trees) in dependency order.utils/pan-os-php.phpthenrequire_once'sutils/lib/UTIL.php, which in turn pulls in every UTIL subclass (RULEUTIL,STATSUTIL,SECURITYPROFILEUTIL,DEVICEUTIL,NETWORKUTIL,MERGER,RULEMERGER,KEYMANGER,PREDEFINED,UPLOAD,XMLISSUE,DIFF,OVERRIDEFINDER,APPIDENABLER,CONFIGSIZE,BPAGENERATOR,XMLOPJSON,REGISTERIP,USERIDMGR,SOFTWAREREMOVE,TRAFFICLOG,SYSTEMLOG,GARPSEND,CUSTOMREPORT,SOFTWARE_DOWNLOAD,SOFTWARE_PREPARATION__,LICENSE__,CONFIG_DOWNLOAD_ALL__,SPIFFY__,CONFIG_COMMIT__,MAXMIND__,PLAYBOOK__,UTIL_GET_ACTION_FILTER,IRONSKILLET_UPDATE__,PROTOCOLL_NUMBERS__,HTMLmerger__,TSF__,XPATH,SSH_CONNECTOR__,GCP,CONVERTER,PARSER,APPIDTOOLBOX,RULE_COMPARE).PH::processCliArgs()parses thekey=valuepairs from$argvintoPH::$args.- The dispatcher inspects
PH::$args['type']. If absent, it lists every supported type and exits. Otherwise it callsPH::callPANOSPHP($type, $argv, $argc, $PHP_FILE), whichnew's the appropriate UTIL subclass. - The UTIL constructor calls
utilStart()→utilInit()(CLI args, plugin load,arg_validation,listactions/listfilters,init_arguments) →utilActionFilter()(extracting_actions,createRQuery,load_config,location_filter) →location_filter_object()→time_to_process_objects()(the actual per-object iteration that runs eachaction) →GlobalFinishAction()→stats()→save_our_work(TRUE)(writesout=file, pushesoutputformatset=, and on API mode commits viaPanAPIConnector).
Confirmed components
+-----------------------------+
| Shell alias pan-os-php |
| (utils/alias.sh) |
+--------------+--------------+
|
v
+-----------------------------+
| utils/pan-os-php.php |
| (single CLI entry) |
+--------------+--------------+
|
+-----------------------+-----------------------+
| |
v v
+----------------------------+ +-----------------------------+
| lib/pan_php_framework.php | | utils/lib/UTIL.php |
| (framework boot) | | (UTIL dispatcher) |
+-------------+--------------+ +-------------+---------------+
| |
v v
+----------------------------+ +-----------------------------+
| lib/misc-classes | | utils/lib/<TYPE>UTIL.php |
| PH (CLI helper) | | RULEUTIL, STATSUTIL, |
| DH (DOM helper) | | SECURITYPROFILEUTIL, |
| PanAPIConnector | <----. | DEVICEUTIL, NETWORKUTIL, |
| PanSaseAPIConnector | \ | MERGER, RULEMERGER, ... |
| RQuery + RQueryContexts | \ +-------------+---------------+
+-------------+--------------+ \ |
| \ v
v \ +-----------------------------+
+----------------------------+ \ | utils/common/<X>CallContext |
| lib/object-classes | - | + actions-<x>.php |
| App, Address, Service, | | (action implementations) |
| Tag, Schedule, Threat, | +-------------+---------------+
| *Profile, *Store | |
+-------------+--------------+ |
| |
v v
+----------------------------+ +-----------------------------+
| lib/network-classes | | lib/rule-classes |
| Zone, Interface*, | | Rule (base), |
| IKEGateway, IPsecTunnel, | | SecurityRule, NatRule, |
| VirtualRouter, DHCP, ... | | DecryptionRule, PbfRule, |
+-------------+--------------+ | QoSRule, DoSRule, ... |
| +------------+----------------+
v |
+----------------------------+ |
| lib/device-and-system- | |
| classes: PANConf, | <--------------------------+
| PanoramaConf, FawkesConf,|
| BuckbeakConf, DeviceGroup|
| Template, TemplateStack, |
| ManagedDevice, Snippet |
+-------------+--------------+
|
v
+----------------------------+ +-----------------------------+
| XML config (file or live) | <----> | XML API endpoint |
| (DOM tree, in-memory) | | https://<fw>/api/?... |
+----------------------------+ +-----------------------------+
+----------------------------+ +-----------------------------+
| git-php/ (vendored) | | phpseclib (Math, Crypt, Net)|
| for input file Git history | | for SSH/RSA in SSH connector|
+----------------------------+ +-----------------------------+
+----------------------------+ +-----------------------------+
| iron-skillet/panos_v*/ | | migration/parser/<vendor>/ |
| day-1 templates | | per-vendor PARSERs |
+----------------------------+ +-----------------------------+
Component inventory
| Component | Path | Role |
|---|---|---|
| Framework boot | lib/pan_php_framework.php |
Sets PHP runtime, registers handlers, require_once's every framework class |
| Single CLI entry | utils/pan-os-php.php |
Accepts type= and dispatches |
| UTIL base + dispatcher table | utils/lib/UTIL.php, lib/misc-classes/PH.php |
PH::$supportedUTILTypes, PH::callPANOSPHP() |
| CLI helper (PH) | lib/misc-classes/PH.php |
PH::$args, PH::$argv, PH::frameworkVersion(), PH::print_stdout(), PH::processCliArgs() |
| DOM helper (DH) | lib/misc-classes/DH.php |
XML element manipulation, XPath generation |
| Error reporter | lib/ErrorReporter.php |
derr() (fatal + backtrace), mwarning() (non-fatal) |
| RQuery filter engine | lib/misc-classes/RQuery.php + RQueryContext subclasses (Address, Application, Certificate, DHCP, Device, Interface, Routing, Rule, Schedule, SecurityProfile, SecurityProfileGroup, Service, StaticRoute, Tag, Threat, VirtualWire, Zone) |
Parses filter= expressions into a tree, evaluates per-object |
| API connector (XML API) | lib/misc-classes/PanAPIConnector.php |
keygen flow, op/get/set/edit/delete/commit calls, ~/.panconfkeystore persistence |
| API connector (SASE) | lib/misc-classes/PanSaseAPIConnector.php |
Strata Cloud Manager endpoints (partial) |
| Object classes | lib/object-classes/ (49 files) |
App, AppCustom, AppFilter, AppGroup, AppStore; Address, AddressGroup, AddressStore, Region; Service, ServiceGroup, ServiceStore; Tag, TagStore; Schedule, ScheduleStore; Threat, ThreatVulnerability, ThreatSpyware, ThreatStore; *Profile and Profile base + *ProfileStore; ObjStore base |
| Network classes | lib/network-classes/ (37 files) |
Zone, ZoneStore; EthernetInterface + EthernetIfStore; AggregateEthernetInterface + AggregateEthernetIfStore; LoopbackInterface + LoopbackIfStore; TunnelInterface + TunnelIfStore; VlanInterface + VlanIfStore; VirtualWire + VirtualWireStore; VirtualRouter + VirtualRouterStore; StaticRoute; IKEGateway + IKEGatewayStore; IkeCryptoProfil/IPSecCryptoProfil + their stores; IPsecTunnel + IPsecTunnelStore; GreTunnel + GreTunnelStore; DHCP + DHCPStore; Certificate + CertificateStore; TmpInterface + TmpInterfaceStore; SharedGatewayStore; InterfaceContainer, VirtualRouterContainer, NetworkPropertiesContainer |
| Rule classes | lib/rule-classes/ (18 files) |
Rule base; RuleWithUserID, RuleWithGroupTag, RuleWithSchedule; SecurityRule, NatRule, DecryptionRule, AppOverrideRule, CaptivePortalRule, AuthenticationRule, PbfRule, QoSRule, DoSRule, TunnelInspectionRule, SDWanRule, NetworkPacketBrokerRule, DefaultSecurityRule; RuleStore |
| Device/system classes | lib/device-and-system-classes/ (16 files) |
PANConf (firewall), PanoramaConf, FawkesConf, BuckbeakConf; DeviceGroup, Template, TemplateStack, ManagedDevice, ManagedDeviceStore, LogCollectorGroup; Container, DeviceCloud, DeviceOnPrem; Snippet, Sub; VirtualSystem |
| Container classes | lib/container-classes/ (8 files) |
ObjRuleContainer base; AddressRuleContainer, AppRuleContainer, ServiceRuleContainer, TagRuleContainer, GroupTagRuleContainer, UrlCategoryRuleContainer, ZoneRuleContainer |
| Helper classes | lib/helper-classes/ (4 files) |
IP4Map, cidr, ServiceDstPortMapping, ServiceSrcPortMapping |
| UTIL implementations | utils/lib/ (47 files) |
One PHP class per UTIL type — see Section 8 for the full table |
| Action / CallContext | utils/common/ (38 files) |
*CallContext per type + actions-*.php files defining action handlers |
| Vendor migration | migration/parser/ (per-vendor folders) |
CONVERTER → PARSER → <vendor> parser hierarchy |
| AppID Toolbox | appid-toolbox/ |
Self-contained sub-tool; companion type=appid-toolbox UTIL |
| IronSkillet bundle | iron-skillet/panos_v8.1, panos_v9.1, panos_v10.0, panos_v10.1, panos_v10.2, panos_v11.0 |
Vendored PAN-OS day-1 hardening templates |
| git-php | git-php/src/ |
Bundled fork of czproject/git-php for the git= argument |
| API listener | utils/api/v1/tool.php |
Optional HTTP front-end exposing UTILs as REST routes |
| Web UI scaffolding | utils/develop/ui/, utils/common/html/ |
Experimental browser UI |
| Bash autocompletion | utils/bash_autocompletion/pan-os-php.sh (and per-tool pa_*.sh) |
Tab completion for the dispatcher and the legacy aliases |
Technology stack
| Layer | Technology |
|---|---|
| Language | PHP 7.4+ |
| Required PHP extensions | json, curl, dom, mbstring, bcmath, mysqli, yaml |
| Crypto | phpseclib (vendored) — Math/BigInteger, Crypt/RSA, Net/SSH2 |
| Git library | czproject/git-php (vendored under git-php/) — used when git= is passed |
| Auth flow | XML API (/api/?type=keygen then `?type=config |
| Output formats | Plain text, PAN-OS set commands, JSON (BETA via shadow-json), XLSX (via python3-xlsxwriter), HTML (via HTMLmerger__) |
| Optional helpers | Python 3 with pandas, xlsxwriter, netaddr, requests; jq |
| License | ISC for the project; bundled git-php is BSD-3-Clause; PAN-OS schema files (lib/resources/) are © Palo Alto Networks (fair-use copy) |
Data flow — single offline UTIL run
$argv ─────► PH::processCliArgs() ─────► PH::$args
│
▼
PH::callPANOSPHP(type)
│
▼
new <Type>UTIL(...)
│
▼
UTIL::utilInit()
├── arg_validation()
├── listactions/listfilters
└── init_arguments()
│
▼
UTIL::utilActionFilter()
├── extracting_actions()
├── createRQuery()
├── load_config() ──► XML on disk → DOMDocument → PANConf/PanoramaConf
└── location_filter()
│
▼
location_filter_object() ──► scope to vsys/DG/template
│
▼
time_to_process_objects()
│
▼
for each object:
RQuery::test(object)
│ true
▼
for each action:
apply()
│
▼
GlobalFinishAction()
│
▼
save_our_work(TRUE)
├── DOMDocument → out= file
└── outputformatset= → set commands file
Data flow — single API UTIL run
Identical to the offline flow, except:
load_config()callsPanAPIConnector::getRunningConfig()(andgetCandidateConfig()for some operations) over HTTPS, returning XML.save_our_work()invokesPanAPIConnector::edit(),PanAPIConnector::set(), orPanAPIConnector::delete()per modified node.- If
outputformatset=is also passed, thesetcommands are written alongside the API push. - The first API call requires authentication; the connector reads
~/.panconfkeystorefor cachedserial@hostkeys, otherwise prompts for username/password and persists the resulting key (unlessshadow-apikeynosave).
Note
The framework intentionally re-throws every PHP
E_NOTICE/E_WARNINGas a fatal viamyErrorHandler→derr(). This is unconventional but correct for the use case: a config-mutation tool must never silently continue on a typo or NULL-deref. Operators reading the codebase should not "fix" warnings in user code by silencing them; the framework will end the run if the warning is a real bug.
2. System Requirements
Runtime
| Requirement | Minimum | Recommended |
|---|---|---|
| PHP CLI | 7.4 | 8.1 |
PHP memory_limit |
14 GB (set inside framework) | leave default |
| OS | Linux (Ubuntu 20.04+, CentOS 7), macOS, Windows via Docker | Ubuntu 22.04 |
| Bash | 5.0 (for autocompletion) | — |
| Disk | repo ~150 MB; large XML configs and Excel exports add MBs–GBs | 5 GB free |
| Network | HTTPS to firewall(s) and Panorama for API mode; HTTPS to GitHub for update_pan-os-php.sh; optional outbound to MaxMind, Iron-Skillet repo |
— |
PHP extensions
The composer.json require block lists the hard requirements:
{
"require": {
"php": ">=7.4.0",
"ext-json": "*",
"ext-curl": "*",
"ext-dom": "*",
"ext-mbstring": "*",
"ext-bcmath": "*",
"ext-mysqli": "*",
"ext-yaml": "*"
}
}
Note that ext-mysqli is required even for offline-only use because the experimental utils/develop/ui/ and migration/parser/lib/SHAREDNEW.php features assume it is present at autoload time; the framework will error if the extension is missing.
Optional dependencies
| Optional | Reason |
|---|---|
python3 + pandas + xlsxwriter + netaddr + requests |
XLSX export via actions=exportToExcel:..., IP arithmetic helpers |
jq |
Bash-side parsing of shadow-json output |
| Docker | Run via the swaschkut/pan-os-php:latest image instead of installing PHP locally |
Network / firewall
- Outbound HTTPS to each managed device or Panorama (port 443).
- The XML API must be enabled on the device (
Device > Setup > Management > XML APIor its Panorama equivalent). - Time-skew matters: API auth tokens are time-bound — keep the jump host's NTP in sync with the firewall.
3. Configuration Reference
pan-os-php itself reads no configuration files. Configuration is supplied per-invocation via CLI arguments and resolved at runtime by PH::processCliArgs() and the per-UTIL supportedArguments tables.
Configuration files (read by the framework)
| File | Format | Location | Purpose |
|---|---|---|---|
~/.panconfkeystore |
proprietary text | user home | Cached API keys, written by PanAPIConnector after first keygen call. Disable via shadow-apikeynosave. |
utils/lib/util_action_filter.json |
JSON | repo | Generated by type=util_get-action-filter; used by utils/api/v1/tool.php and tooling that needs the action/filter catalogue. |
iron-skillet/panos_v*/templates/panorama/snippets/*.xml |
PAN-OS template XML | repo | Bundled IronSkillet day-1 hardening snippets, one folder per major PAN-OS version. |
lib/resources/schema/*.xml |
PAN-OS schema XML | repo | Schema files copied from the official Palo Alto product (https://[MGMT-IP]/php/utils/schema.php). Used for validation. |
composer.json |
JSON | repo root | Declares PHP and extension requirements. |
git-php/composer.json |
JSON | submodule root | Declares the vendored git-php library's metadata. |
docker-compose.yml, docker-compose2.yml |
YAML | repo root | Optional Docker-based development environment (PHP-Apache + MySQL + phpMyAdmin); used by the experimental utils/develop/ui/. |
Environment variables
| Variable | Read by | Purpose | Default |
|---|---|---|---|
HOME |
utils/develop/grp-static-to-dynamic.php |
Locate user home for the API keystore | system value |
USERPROFILE |
lib/misc-classes/PanAPIConnector.php |
Windows equivalent of HOME | system value |
HOMEDRIVE |
lib/misc-classes/PanAPIConnector.php |
Windows path component | system value |
HOMEPATH |
lib/misc-classes/PanAPIConnector.php |
Windows path component | system value |
TERM |
lib/misc-classes/PH.php |
Decide whether to emit ANSI bold escape codes | system value |
There are no pan-os-php-specific environment variables. The framework deliberately does not hide config in the environment — it must all be in the CLI invocation, so playbooks are self-describing.
Universal CLI flags (all UTILs)
These are wired into UTIL::supportedArguments() and apply to nearly every type. Type describes the value's shape; Default is the value used when the flag is absent.
| Flag | Type | Default | Description |
|---|---|---|---|
in= |
filename or api://... |
— (required for most UTILs) | Source: file or live API endpoint |
out= |
filename | — | Output file (offline mode); ignored in API mode |
location= |
shared, any, vsys name, DG name, <DG>:includechilddgs, <DG>:excludeMaindg |
shared (Panorama), vsys1 (firewall) |
Scope of the operation |
template= |
template name | any |
Limit to a specific Panorama template |
filter= |
RQuery expression | none | Per-object filter (see Section 8 for the engine) |
actions= |
action[:arg1[,arg2]][/action2[:...]] |
none | Slash-separated list of actions to apply |
listactions |
flag | off | Print every supported action and exit |
listfilters |
flag | off | Print every supported filter operator and exit |
stats |
flag | off | Print object/rule counts after run |
actions=display |
special | — | Print matched objects without changing them |
loadplugin= |
filename | — | Load a custom action/filter plugin |
apitimeout= |
seconds | 60 | API timeout |
auditcomment= |
string | PAN-OS-PHP $actions $time |
Audit comment for API commits |
outputformatset[=file] |
flag or filename | off / no file | Emit PAN-OS set commands |
projectfolder= |
dir | — | Output directory root |
loadpanoramapushedconfig |
flag | off | Pull Panorama-pushed objects when loading from a managed firewall |
cycleconnectedfirewalls |
flag | off | Iterate Panorama-managed devices |
git |
flag | off | Wrap input file in a Git repo + auto-commit |
expedition |
flag | off | Internal marker that the run is from the Expedition tool |
debugapi |
flag | off | Print every API call |
debugloadtime |
flag | off | Print phase timings |
help |
flag | off | Print usage and exit |
shadow- flags (advanced behaviours)
| Flag | Default | Description |
|---|---|---|
shadow-disableoutputformatting |
off | Skip DOMDocument output normalisation |
shadow-enablexmlduplicatesdeletion |
off | When duplicate object names exist, keep one and drop the rest |
shadow-ignoreinvalidaddressobjects |
off | Tolerate address objects with no value/type |
shadow-apikeyhidden |
off | POST API key (PAN-OS 9.0+) instead of putting it in URL |
shadow-apikeynohidden |
off | Force API key in URL (PAN-OS < 9.0) |
shadow-apikeynosave |
off | Do not persist API key to ~/.panconfkeystore |
shadow-displaycurlrequest |
off | Print raw curl command for every API call |
shadow-reducexml |
off | Strip whitespace from output XML |
shadow-json |
off | BETA: emit stdout as a single JSON document |
shadow-nojson |
off | Force human text even if shadow-json was set |
shadow-displayxmlnode |
off | Include matched XML elements in actions=display output |
shadow-saseapiqa |
off | Use SASE QA endpoints |
Important
Treat
~/.panconfkeystoreas a secret file. It contains long-lived PAN-OS API keys, each of which carries the full RBAC of the user that rankeygen. Keep its mode at0600, never commit it, and rotate keys (type=key-manager actions=delete:host) when staff leave or when a host is decommissioned. If you cannot accept the cache, run every API invocation withshadow-apikeynosaveand pipe credentials in interactively — but expect each run to require fresh auth.
Per-UTIL specifics
Each UTIL extends UTIL::supportedArguments() with its own flags. To enumerate them:
pan-os-php type=<X> help
A few of the high-impact ones:
| Type | Notable extra arguments |
|---|---|
rule-merger |
panoramaPreRules, panoramaPostRules, mergeAdjacentOnly, dupAlgorithm |
rule |
dryrun, plus all rule-specific filter operators |
address-merger |
mergeByValue, mergeByName, dupAlgorithm |
bpa-generator |
cycleconnectedfirewalls (mandatory for Panorama-driven runs) |
traffic-log, system-log |
Log filter/time-window arguments |
software-download |
PAN-OS version selector |
userid-mgr |
record=user,IP[,VSYS] |
register-ip-mgr |
actions=register/unregister:ip,tag |
xml-issue |
actions=display, actions=fix |
playbook |
playbook=path/to/playbook.json |
vendor-migration |
vendor=ciscoasa|netscreen|sonicwall|sophos|ciscoswitch|ciscoisr|fortinet|srx|cp-r80|cp|cp-beta|huawei|stonesoft|sidewinder|pfsense |
4. Deployment & Scaling
pan-os-php is a per-invocation CLI; "deployment" is shaping how and where it is invoked.
Deployment patterns
| Pattern | Use when |
|---|---|
| Single jump host (Ubuntu 22.04 with the install script) | Small team, ad-hoc runs, full Bash/Tab completion, easy SSH access |
| Per-engineer laptop (macOS install script, or Docker) | Lots of one-off exploration, no shared state needed |
Docker per run (swaschkut/pan-os-php:latest) |
CI pipelines, ephemeral compute, hosts you do not own |
Managed centrally (Ansible / Salt installs the repo into /tools/pan-os-php) |
Compliance-controlled environments where every change must be source-tracked |
REST front (utils/api/v1/tool.php) behind nginx + PHP-FPM |
When other systems need to call UTIL operations as HTTP — exposes /utils/api/v1/tool.php/<type> |
Scaling considerations
- Memory. Loading a multi-thousand-rule, multi-hundred-DG Panorama XML routinely consumes 10–25 GB. The framework hard-codes
memory_limit=14512M(14 GB) — for the largest customers you may need to override. There is no streaming parser; every config is held inDOMDocumentat once. - CPU. Mostly DOM walking, regex, and string compares. Single-threaded. Multi-runs in parallel are fine if each has its own memory headroom.
- API rate. PAN-OS XML API does not impose a rate limit on small calls but commit operations serialise at the device. Avoid calling
config-commitfrom many concurrent runs against the same device. The connector has retry logic for transient HTTPS failures. - Network. API mode pulls the full running config on every invocation (or running + candidate). For very large Panoramas a single
getRunningConfigcan be 50–200 MB.
Update strategy
- Pin via Git.
update_pan-os-php.shpullsorigin mainon its host. For change control, fork the repo and pin a release tag; only runupdate_pan-os-php.shafter CI validation against your config corpus. - Pin the Docker image by digest. Use
swaschkut/pan-os-php@sha256:...rather than:latestin CI. - Watch the upstream
CHANGELOG.txt. Action names occasionally change (replaceWithObject↔replace-With-Objectbetween releases, for example).
CI / automation patterns
Read pan-os-php type=<X> listactions and listfilters for every type at CI start and store the JSON output as build metadata. This catches the "the action you depend on no longer exists" failure mode early.
docker run --rm -v $PWD:/share swaschkut/pan-os-php:latest \
type=util_get-action-filter projectfolder=/share/build-metadata
For idempotent operations, run with actions=display first and assert on the count; only proceed to the destructive action if the count matches expectations.
5. Operations
Day-to-day operator runbooks
Rotate API key cache
pan-os-php type=key-manager actions=display
pan-os-php type=key-manager actions=delete:<hostOrSerial>
type=key-manager is your direct interface to ~/.panconfkeystore. The next API call to that host will re-prompt for credentials.
Validate a config XML before processing
pan-os-php type=xml-issue in=panorama.xml actions=display
Lists empty entries, missing required attributes, and duplicate names. actions=fix repairs many of them in place.
Snapshot a live device to disk
pan-os-php type=stats in=api://10.0.0.5 out=fw-$(date -u +%Y%m%dT%H%M%SZ).xml
stats is read-only; the side effect is out= writes the running config locally. This is the safest base for any subsequent destructive offline work.
Generate a complete change preview
pan-os-php type=rule \
in=api://10.0.0.5 \
'filter=(action eq deny)' \
actions=tag-Add:Reviewed-2026Q2 \
outputformatset=preview-2026Q2.set \
shadow-json
Combines a set-format file (for human review) with JSON output (for tool review).
Bulk download every running and candidate config from a Panorama
pan-os-php type=config-download-all in=api://panorama.local projectfolder=panorama-snapshot/
Useful before a major upgrade or as a pre-change snapshot.
Trigger a commit via API
pan-os-php type=config-commit in=api://panorama.local
type=config-commit issues a commit (/api/?type=commit&cmd=<commit/>) and waits for completion. Combine with cycleconnectedfirewalls to push to managed devices.
Note
config-commithonoursapitimeout=. Panoramas with hundreds of devices can take 20+ minutes to fully push; setapitimeout=1800for those runs. The connector polls the job status until it completes or the timeout expires.
Tip
Wrap the whole change in
git=to get an automatic before/after commit on the input file. The dispatcher initialises a Git repo around the input filename, commits the original, runs the actions, and commits the result with the action and timestamp in the message.
Logging
The framework writes to STDOUT (human report) and STDERR (errors and warnings only). There is no logfile written by default. To capture a full run:
pan-os-php type=rule in=fw.xml ... 2>&1 | tee run.log
When shadow-json is set, stdout is a JSON document. The log field contains the human-text equivalent for archival.
The utils/lib/logWriter.php class is used internally by some long-running UTILs (appid-toolbox, config-download-all) to write structured per-step logs into the project folder.
Health checks
There is no daemon to check, so health is "does a known-good run still work":
pan-os-php version
- PAN-OS-PHP version: 2.1.20 [UNIX]
- /tools/pan-os-php/utils
- PHP version: 8.1.2
A non-zero exit, an empty version line, or a *** ERROR *** indicates a broken install. For an end-to-end smoke test, use a tiny saved XML and a no-op:
pan-os-php type=stats in=tests/input/firewall-test.xml
Backup & restore
pan-os-php itself stores no state apart from ~/.panconfkeystore. To back up:
- Source tree: standard Git pull (or repo mirror).
- Keystore:
cp ~/.panconfkeystore ~/.panconfkeystore.bak(treat as a secret). - Generated artefacts (set files, snapshots, Excel exports): whatever lives under
projectfolder=is yours to back up.
Note
outputformatset=,out=, andprojectfolder=outputs are deterministic snapshots of a moment in time. There is no built-in "rollback to previous run" — keep prior snapshots if you need that, or usegit=to keep history alongside the input file.
6. Performance
Memory
memory_limit is set to 14 GB by lib/pan_php_framework.php. For very large Panoramas this is a soft floor; if you see Allowed memory size of N bytes exhausted, increase via php -d memory_limit=32G ... at the shell, or edit the framework. There is no streaming parser; the full DOM is in memory. memory_and_gc('end') is called by some UTILs and example scripts to display before/after consumption.
CPU profile
Most invocations are CPU-bound on:
- DOM traversal (
DH::dom_to_xml,DH::elementToPanXPath, the per-rule iteration inRuleStore) - Regex evaluation when the filter uses
regexoperators - String compares and array reflows during merger runs (
MERGER,RULEMERGER)
The RULEMERGER algorithm is O(n^2) over the rule count for adjacency-aware merges. On 50k-rule Panoramas, expect tens of minutes. mergeAdjacentOnly reduces this to nearly linear at the cost of merging only adjacent shadow rules.
I/O
- Offline mode: input parsed once via
DOMDocument::load, modifications stay in memory, single write at end. - API mode: one HTTP request per push (
PanAPIConnector::edit,PanAPIConnector::set, etc.) — for a UTIL that mutates 1000 objects, expect 1000+ requests. The connector reuses the underlying TLS connection where possible. outputformatset=writes line-by-line as actions execute, so the file is complete even if the process is killed mid-run.
Tuning knobs
| Flag | Effect |
|---|---|
apitimeout=N |
Per-call API timeout, default 60 — raise for slow Panoramas |
shadow-reducexml |
Strip whitespace from output XML (smaller files, slightly faster I/O) |
shadow-disableoutputformatting |
Skip DOM normalisation on output, save CPU |
Run a bpa-generator outside business hours |
The combined cycleconnectedfirewalls walk hammers the API |
Increase PHP memory_limit for huge Panoramas |
php -d memory_limit=64G |
| Use Docker to keep PHP/extension versions stable | Avoids "works on Ubuntu 22 but not 20" drift |
Benchmarks (qualitative)
type=address actions=display filter=(object is.unused)on a 30k-object Panorama: ~3–8 minutes wall-clock, ~12 GB memory.type=rule-merger actions=mergeAdjacenton a 5k-rule Panorama withpanoramaPreRules: ~5–15 minutes wall-clock, ~6 GB memory.type=stats in=api://...against a Panorama with 30 managed firewalls: 2–5 minutes for the running config download alone.
7. Security
Threat model
pan-os-php is a privileged operations tool: it has read/write access to firewall and Panorama configurations, often via long-lived API keys cached on the operator's host. Treat any host that runs it as a tier-zero administrative system.
Secrets handling
| Secret | Where it lives | Lifetime | How to revoke |
|---|---|---|---|
| PAN-OS API key | ~/.panconfkeystore (user home) |
Until firewall admin revokes it or pan-os-php type=key-manager actions=delete:host is run |
Delete via key-manager or rotate at the device |
| Username/password (during interactive auth) | Process memory, never written | Single invocation | Process exit |
LDAP credentials (if user-check-ldap is used) |
Read from the loaded config; not persisted by pan-os-php |
— | Rotate at the LDAP server / firewall |
PanAPIConnector writes the keystore at file mode 0600 if the umask is reasonable; verify after first run:
ls -l ~/.panconfkeystore
-rw------- 1 alice alice 384 Apr 28 12:00 /home/alice/.panconfkeystore
Caution
A leaked
~/.panconfkeystoreis equivalent to a leaked credential set for every device it covers. Never copy it to a shared host, never include it in tarballs or backups that leave the operator's account, and rotate any key whose host has been suspected of compromise. Useshadow-apikeynosaveon shared workstations to avoid persisting the key at all.
Exported configuration files are sensitive
pan-os-php works on exported configuration XML (in=config.xml). An export, whether copied off a firewall, saved
with show config or taken as a Panorama named snapshot, is a full copy of the device configuration: it can include
administrator password hashes, certificates and keys, and the credentials of downstream services (LDAP, RADIUS,
SNMP, syslog). Handle it as credential material.
| Action | Cadence | Notes |
|---|---|---|
| Keep exports encrypted at rest and limited to the operator role | Continuous | Retain them only as long as change management requires |
Rotate the API keys in ~/.panconfkeystore |
On a defined cadence, and after an export has been handled outside the operator's host | pan-os-php type=key-manager actions=delete:host, then a fresh run that re-prompts for credentials |
| Treat a Panorama configuration backup like the firewall configs it manages | Continuous | It aggregates the configuration of every managed device |
| Remove sensitive values before sharing an export (vendor, support case, migration) | Per share | Follow your data-handling policy |
For how PAN-OS protects stored secrets (including the master key), use the official PAN-OS administrator documentation as the reference, not this guide.
Network security
- TLS verification.
PanAPIConnectordefaults to verifying the device's TLS certificate. The bundled install and update scripts set their own Git options, which are separate from API TLS; review them before running them on your hosts. Do not disable TLS verification for API mode without a deliberate decision. - API mode endpoints. Always direct API connections at the management plane IP. The framework supports
api://[email removed]to tunnel through Panorama, which is the preferred path for firewalls without direct management-plane reachability. - Inbound REST mode. If you deploy
utils/api/v1/tool.phpbehind nginx, put it on a dedicated VLAN with mTLS or an authenticating reverse proxy in front. The endpoint executes UTILs server-side with whatever credentials the PHP process has — there is no built-in auth on the tool endpoint.
Input validation
- The framework refuses to load on PHP < 5.5 (
lib/pan_php_framework.php:22) and historically also imposed an upper bound (<=8.1) — the upper-bound check is currently commented out inpan_php_framework.php. myErrorHandler(lib/pan_php_framework.php:275) re-raises anyE_USER_NOTICE,E_USER_WARNING,E_NOTICE, orE_WARNINGas a fatalderr(); this prevents subtle PHP coercion bugs from corrupting a config silently.xml-issueruns a structural sanity pass; treat XMLs from untrusted sources withxml-issuefirst.
XSS / SQL / deserialization
- No HTML output is generated by the CLI itself. The optional
utils/develop/ui/web UI is explicitly experimental and ships with sample test accounts (seeutils/develop/ui/test/README.md). Do not deploy that UI without replacing those values and disabling sample DBs. mysqliis loaded at framework boot to supportutils/develop/ui/andmigration/parser/lib/SHAREDNEW.php. The CLI flow does not connect to any database.- No
unserialize()of attacker-controlled data; XML is parsed viaDOMDocumentonly.
Audit trail
auditcomment=sets the audit comment Panorama records for every API change. Default isPAN-OS-PHP $actions $time. Set this to a JIRA / change ticket reference for compliance environments.git=produces commits in the wrapping Git repo with the action + timestamp in the message.- Combine with
outputformatset=files retained alongside Git history for a tamper-evident change log.
Warning
Several known issues affect destructive operations. Always preview with
actions=displayandoutputformatset=before running mergers (#793, #798), bulk renames (#764), or zone manipulations (#786). When in doubt, snapshot first (type=stats out=snap.xml) and iterate offline.
Hardening checklist
- [ ]
~/.panconfkeystoremode0600, owned by the operator user - [ ]
shadow-apikeynosaveon shared / multi-user hosts - [ ]
apitimeout=set sensibly to detect dead devices instead of waiting forever - [ ]
auditcomment=integrated into the operator's playbook (mapped to ticket IDs) - [ ] Source tree pinned to a known commit / tag rather than tracking
main - [ ] No public network exposure of
utils/api/v1/tool.phpwithout authenticating reverse proxy - [ ]
utils/develop/ui/disabled in production installs - [ ] Operator hosts on the same NTP source as the firewalls (API tokens are time-bound)
- [ ] Config exports stored under restricted ACLs (treat as secret material, not as backups)
- [ ] Device API keys rotated on a defined post-export cadence (
pan-os-php type=key-manager actions=delete:host+ fresh auth) - [ ] Downstream credentials (LDAP / RADIUS / SNMP / syslog) rotated when reachable from any config export
8. Developer Internals
Repository structure
pan-os-php/
├── composer.json # PHP + extension requirements
├── docker/ # Dockerfile-main_amd, Dockerfile-API_amd, etc.
├── docker-compose.yml # Production image compose
├── docker-compose2.yml # PHP-Apache + MySQL + phpMyAdmin (dev)
├── create_all_docker_container.sh
├── create_docker_manifest.sh
├── install_script_centos7.sh
├── install_script_centos7_old.sh
├── install_script_ubuntu20.sh
├── install_script_ubuntu22.sh
├── mac_set_path_variable.sh
├── remove_path_variable.sh
├── set_alias_usage.sh
├── update_pan-os-php.sh
├── lib/
│ ├── pan_php_framework.php # Framework boot
│ ├── ErrorReporter.php # derr() static
│ ├── autoloader.php # myAutoloader (exists but unused)
│ ├── classes/taskManagement/ # Task reporters (FileTaskReporter, ExpeditionTaskReporter)
│ ├── container-classes/ # ObjRuleContainer + 7 subclasses
│ ├── object-classes/ # 49 object classes (Address, Service, App, *Profile, *Store)
│ ├── network-classes/ # 37 network classes (Zone, *Interface, *IfStore, IKEGateway, ...)
│ ├── rule-classes/ # 18 rule classes (Rule + Security/NAT/Decryption/...)
│ ├── device-and-system-classes/ # 16 (PANConf, PanoramaConf, FawkesConf, BuckbeakConf, DeviceGroup, Template, Snippet, ...)
│ ├── helper-classes/ # IP4Map, cidr, ServiceDstPortMapping, ServiceSrcPortMapping
│ ├── misc-classes/ # PH, DH, RQuery + RQueryContexts, PanAPIConnector, PanSaseAPIConnector, CsvParser
│ └── resources/ # PAN-OS schema XML, IP protocol map, GeoIP data, License notices
├── utils/
│ ├── pan-os-php.php # Single CLI entry
│ ├── alias.sh # Shell aliases
│ ├── lib/ # 47 UTIL implementations
│ ├── common/ # 38 CallContext + actions-*.php files
│ ├── api/v1/ # Optional REST front-end (tool.php)
│ ├── bash_autocompletion/ # pan-os-php.sh + per-tool autocomplete
│ ├── develop/ # In-progress UTILs and the experimental Web UI
│ ├── doc/ # Markdown documentation drafts
│ └── common/html/ # JS/HTML for the experimental UI
├── examples/ # 13 standalone PHP examples + sample-configs/
├── git-php/ # Vendored fork of czproject/git-php
├── iron-skillet/ # Bundled Palo Alto IronSkillet templates per PAN-OS version
├── migration/ # Vendor migration framework + per-vendor parsers
├── appid-toolbox/ # Self-contained App-ID activation sub-tool
├── projects/ # Empty by default; output of `projectfolder=` runs lands here
├── log/ # Runtime log root
├── tests/ # Tests + sample configs
├── doc/ # Auto-generated phpdoc HTML
├── README.md / READMEcustomScripting.md / READMEdocker.md / READMEutil.md
├── CHANGELOG.txt
└── SUPPORT.md
Architecture deep dive
The PH and DH static helpers
PH (lib/misc-classes/PH.php) is the global CLI helper — it owns PH::$args, PH::$argv, PH::$basedir, PH::$useExceptions, PH::$shadow_json, and the JSON output buffers PH::$JSON_OUT / PH::$JSON_OUTlog / PH::$JSON_TMP. Static methods include PH::frameworkVersion() (composes library_version_major.sub.bugfix, currently 2.1.20), PH::frameworkInstalledOS() (returns WIN or UNIX), PH::print_stdout() (the unified print wrapper that respects shadow-json), PH::processCliArgs() (parses argv into args), PH::$supportedUTILTypes (the master list), and PH::callPANOSPHP($type, ...) (the type → class dispatch table).
DH (lib/misc-classes/DH.php) is the DOM helper — DH::elementToPanXPath() produces the canonical PAN-OS XPath for a given element, DH::dom_to_xml() serialises with depth control, DH::firstChildElement() / DH::nextChildElement() walk past whitespace text nodes.
Together these are imported by every class file via require_once's in lib/pan_php_framework.php; you do not need to autoload them yourself.
The RQuery filter engine
lib/misc-classes/RQuery.php is the parser/evaluator for filter= expressions. It tokenises the expression, builds an AST of RQueryNodes, and walks the tree per object during time_to_process_objects(). Each leaf node references a RQueryContext subclass (AddressRQueryContext, RuleRQueryContext, ServiceRQueryContext, ZoneRQueryContext, InterfaceRQueryContext, etc.) that knows the per-type field/operator catalogue.
To add a new filter operator for, say, address:
- Open
lib/misc-classes/AddressRQueryContext.php. - Add an entry to
RQuery::$defaultFilters['address'](kept inRQuery.phpinitialisation):['name' => 'is.in.region', 'operators' => ['Asia' => [...], 'Europe' => [...], ...]]. - Implement the evaluator function the operator points to.
listfilters will pick the new operator up automatically because it iterates RQuery::$defaultFilters[<type>].
The CallContext + Action pattern
Every UTIL type that mutates objects has an associated *CallContext class (AddressCallContext, RuleCallContext, ServiceCallContext, ZoneCallContext, TagCallContext, ScheduleCallContext, SecurityProfileCallContext, SecurityProfileGroupCallContext, ApplicationCallContext, ThreatCallContext, DeviceCallContext, VsysCallContext, VirtualWireCallContext, RoutingCallContext, InterfaceCallContext, DHCPCallContext, CertificateCallContext, StaticRouteCallContext). The CallContext holds the per-action invocation state (the matched object, the supplied arguments, the parent UTIL).
Each context has a $supportedActions static array. Entries look like:
array(
'name' => 'tag-Add',
'MainFunction' => function (RuleCallContext $context) { ... },
'args' => array('tagName' => array('type' => 'string', 'default' => '*nodefault*')),
)
utils/common/actions-rule.php, actions-address.php, actions-service.php, actions-zone.php, actions-routing.php, actions-securityprofile.php, actions-securityprofilegroup.php, actions-tag.php, actions-threat.php, actions-schedule.php, actions-device.php, etc., are the files that add entries to those arrays.
To add a new action:
- Open the relevant
actions-*.phpfile underutils/common/. - Append a new entry to
XCallContext::$supportedActionswithname,MainFunction,GlobalInitFunction(optional),GlobalFinishFunction(optional), andargs(each withtype,default, and optionalchoices). - The
MainFunctionreceives a*CallContext $contextand reads$context->object(the matched object) and$context->arguments(resolved CLI args). - Inside, mutate
$context->objectvia the public methods on the object class. Avoid touching the underlying DOM element directly except viaDH::helpers. pan-os-php type=<X> listactionspicks the new entry up automatically.
Plugin loading via loadplugin=
UTIL::loadplugin() accepts a path and require_once's the file early in utilInit. The plugin is plain PHP; it can append entries to any *CallContext::$supportedActions array or to RQuery::$defaultFilters. See examples/example_plugin_rule_display_source_destination.php for the canonical example.
Object class invariants
- Every object class extends
ReferenceableObject(trait) so that uses are tracked; bidirectional reference graph is essential foraddObjectWhereUsed,removeWhereUsed,replaceWithObject, etc. - Every object lives inside a
*Store(AddressStore,ServiceStore,TagStore,RuleStore,ZoneStore,IKEGatewayStore, ...) which extendsObjStore. findOrCreateis the canonical write path: nevernew Address()directly inside an action — go through the store.name(),setName(),xmlroot,ownerare the universal members.display()prints the object;display_referencedFromText()shows where it is used.
Rule class hierarchy
Rule (base)
├── RuleWithUserID (mixin) → SecurityRule, DecryptionRule, AuthenticationRule, DoSRule, PbfRule, QoSRule, SDWanRule, NetworkPacketBrokerRule, TunnelInspectionRule
├── NatRule
├── AppOverrideRule
├── CaptivePortalRule
└── DefaultSecurityRule
Every rule type has its own subclass under lib/rule-classes/, with the trait-based RulewithLogging, RuleWithSchedule, and NegatableRule mixed in where appropriate. RuleStore is the single owner; it knows whether it is pre-rulebase, post-rulebase, or local.
Device/Panorama configuration classes
PANConf— a single firewall.PanoramaConf— a Panorama, ownsDeviceGroups andTemplates.FawkesConf— Fawkes (early SCM) backend.BuckbeakConf— Buckbeak (another SCM iteration) backend.DeviceCloudandDeviceOnPrem— used for Cloud-Managed (Strata Cloud Manager) topologies.Container,Snippet,Sub— units for the cloud-managed config model.
When loading XML, the framework auto-detects which root element is present and instantiates the matching *Conf. Passing loadpanoramapushedconfig makes the firewall load the inherited template + DG-pushed objects.
Vendor migration framework
UTIL → CONVERTER → PARSER → <vendor> (ASA, NetScreen, SonicWall, ...)
Each <vendor>/<vendor>.php file extends PARSER and implements vendor-specific tokenisation. The CONVERTER coordinates the migration: load source, parse, build a PANConf/PanoramaConf, write XML. migration/parser/lib/SHAREDNEW.php is the reusable cross-vendor logic. The set of supported vendor codes is declared in utils/bash_autocompletion/pa_docker-migration-parser.sh and matches the parsers under migration/parser/.
appid-toolbox
A self-contained Application-ID activation pipeline in appid-toolbox/:
- Entry scripts (
report-generator.php,rule-activation.php,rule-cleaner.php,rule-cloner.php,rule-marker.php). - Library (
appid-toolbox/lib/common.php,timezone.php, plus thelib/old/andlib/trait/sources for the older + newer implementations). - Tests under
appid-toolbox/tests/with sample XML for input, comparison, and preparation stages. - Companion UTIL:
pan-os-php type=appid-toolboxdrives the same logic from the unified entry point (the dispatcher routesappid-toolboxto theAppIDToolboxUTIL class).
Module reference (HIGH-priority)
| Folder | Files | Purpose |
|---|---|---|
lib/object-classes/ |
49 | Every PAN-OS object type (Address, Service, App, Tag, Schedule, Threat, Profile family) and their stores |
lib/network-classes/ |
37 | Zones, interfaces, virtual routers, IKE/IPsec, DHCP, certificates, virtual wires |
lib/rule-classes/ |
18 | Rule base + per-rule-type classes; RuleStore |
lib/device-and-system-classes/ |
16 | PANConf, PanoramaConf, FawkesConf, BuckbeakConf, DeviceGroup, Template, TemplateStack, ManagedDevice, LogCollectorGroup, Container, DeviceCloud, DeviceOnPrem, Snippet, Sub, VirtualSystem |
lib/misc-classes/ |
24 | PH, DH, RQuery + per-type RQueryContexts, PanAPIConnector, PanSaseAPIConnector, CsvParser |
lib/container-classes/ |
8 | ObjRuleContainer and per-field rule containers (Address, App, Service, Tag, GroupTag, UrlCategory, Zone) |
lib/helper-classes/ |
4 | IP arithmetic + service port mappings |
utils/lib/ |
47 | One UTIL class per type= (RULEUTIL, STATSUTIL, MERGER, RULEMERGER, KEYMANGER, BPAGENERATOR, ...) |
utils/common/ |
38 | *CallContext, actions-*.php, CallContext.php, RuleCallContext.php etc. |
utils/develop/ |
21 | Experimental / in-progress UTILs (grp-static-to-dynamic.php, ike.php, wildcard_calculator.php, csv-import.php, checkpoint-exclude.php, reset-config.php, pan_get_user_info.php, migration/*.php for bluecoat/f5/vmware-vrni) |
utils/develop/migration/ |
5 | One-off migration helpers (Bluecoat, F5 BIG-IP, VMware vRNI, ...) |
utils/develop/ui/ |
8 | Experimental browser UI (do not deploy to production) |
utils/api/ |
1 | utils/api/v1/tool.php REST front-end |
utils/bash_autocompletion/ |
3 | Bash completion for pan-os-php and the legacy pa_* aliases |
migration/parser/ |
per-vendor folders | One PARSER subclass per supported vendor |
migration/parser/lib/ |
3 | CONVERTER.php, PARSER.php, SHAREDNEW.php |
appid-toolbox/ |
5 entry scripts + lib + tests | App-ID activation toolset |
examples/ |
13 | End-to-end demonstrations of using the library directly (without the UTIL dispatcher) |
iron-skillet/panos_v8.1 … panos_v11.0 |
snippets | Bundled IronSkillet templates |
git-php/src/ |
9 | Bundled Git wrapper |
Code patterns and conventions
- Static
$supportedXarrays. The convention across the framework is to declare a static array on a class —PH::$supportedUTILTypes,RuleCallContext::$supportedActions,App::$_supportedCharacteristics,RQuery::$defaultFilters— and use it both as a runtime dispatch table and as the source of truth forlistactions/listfilters/helpoutput. derrfor fatal,mwarningfor non-fatal.derralways exits 1 with a backtrace;mwarningprints to STDERR (or the JSON warning array) and continues. Usederrwhenever the program cannot meaningfully proceed.PH::print_stdout()overecho. All textual output goes throughPH::print_stdout(). This routes to STDOUT in human mode or into the JSON buffer whenshadow-jsonis set.require_onceoveruse. The framework predates strict PSR-4 layout; classes are loaded centrally vialib/pan_php_framework.php. The companionlib/autoloader.phpexists but is not active (the centralrequire_onceblock runs first).- Trait-based mixins.
ReferenceableObject,XmlConvertible,ObjectWithDescription,PathableName,NegatableRule,RulewithLogging,RuleWithSchedule,RuleWithUserIDare traits applied where the role is shared. - The
find/findOrCreate/getAlltriplet is implemented across every store. Use them; do not reach into$store->o[]directly.
Testing strategy
- Unit / integration.
tests/test_actions.phpreadsutils/lib/util_action_filter.json, iterates every action, and exercises it againsttests/input/panorama-8.0.xml(or whateverin=overrides). It is essentially a per-action smoke test; failures show which actions regressed. appid-toolbox/tests/. Stage-based test fixtures (stage00.xml…stage20.xml) for the AppID activation pipeline.git-php/tests/. Tests for the bundled Git wrapper (nette/testerbased —git-php/composer.jsondeclaresnette/tester ^2.0as a dev dep).- Sample configs.
examples/sample-configs/containspolicy-best-practices.xml,panorama-example.xml,panorama-8.0.xml. Example scripts underexamples/consume these.
When adding an action, also add a smoke entry to util_action_filter.json (type=util_get-action-filter regenerates the file) and verify tests/test_actions.php covers it.
Potential improvements
- Streaming XML parsing.
DOMDocument-only loading caps the practical config size at host-RAM. A streamingXMLReaderpath for read-only UTILs (stats,xml-issue,display-only filters) would let a 16 GB jump host handle Panoramas it currently cannot. - Concurrent API push.
PanAPIConnectoris single-threaded; large mutate-many runs serialise the HTTP latency. A bounded worker pool (e.g.,pthreadsor curl multi) forset/editcalls would dramatically cut wall-clock for high-N operations. - Action removal of obsolete patterns. Some actions exist twice with
-FastAPIsuffixes (enabled-Setvsenabled-Set-FastAPI,securityProfile-Group-SetvssecurityProfile-Group-Set-FastAPI); document or consolidate. - Better SASE coverage.
PanSaseAPIConnectorreturns "do not yet support this UTIL type" for many UTILs. Tracking a per-UTIL support matrix (and surfacing it inhelpoutput) would set expectations. - Replace
mysqlirequirement with optional load.composer.jsonrequiresext-mysqlifor everyone, even though onlyutils/develop/ui/and one migration helper actually use it. Move to runtime feature detection. - Audit
utils/develop/ui/defaults. The experimental UI ships sample test accounts. Gate it behind a build flag or replace them with placeholders that error on first use. - Issue triage. Several open bugs (
#791,#798,#552,#557,#696,#588) point at filter and action edge cases that would benefit from regression tests added totests/test_actions.php.
Contributing
Contributions are accepted via pull request to main. The repository is licensed under the ISC License (see composer.json and LICENSE); the bundled git-php/ is BSD-3-Clause; bundled PAN-OS schema XML under lib/resources/ is © Palo Alto Networks under fair-use copy. By contributing you agree your changes are released under ISC.
Practical guidance:
- New action: add to the relevant
*CallContext::$supportedActions(utils/common/actions-*.php), regenerateutil_action_filter.jsonwithtype=util_get-action-filter, and add a smoke test totests/test_actions.php. - New filter operator: extend
RQuery::$defaultFilters[<type>]and the correspondingRQueryContextclass. - New UTIL type: add a class under
utils/lib/, add the type name toPH::$supportedUTILTypes, wire it into thePH::callPANOSPHP()switch. - New vendor parser: add
migration/parser/<VENDOR>/<VENDOR>.phpextendingPARSER, register it inutils/bash_autocompletion/pa_docker-migration-parser.sh. - Run
php -l <file>on every modified PHP file before pushing. - The
update_pan-os-php.shflow expects PRs to be mergeable viagit pull origin main; rebase your branch.
See CONTRIBUTING.md (top-level) and SUPPORT.md for the official policy. Note in particular that PAN-OS-PHP is community-supported and is not covered by Palo Alto Networks TAC.
9. Integration Points
pan-os-php is designed to plug into existing operational pipelines. The integration points exposed by the framework:
CLI
The single CLI is pan-os-php (alias) → php utils/pan-os-php.php. Every UTIL is reachable through it. STDOUT is human text by default and JSON when shadow-json is set; STDERR carries warnings and fatal traces. Exit code 0 is success, 1 is derr() exit, 2 is unhandled PHP fatal.
Library mode
Drop into a custom PHP script:
require_once '/tools/pan-os-php/lib/pan_php_framework.php';
$pan = new PanoramaConf();
$pan->load_from_file('panorama.xml');
foreach( $pan->securityRules->rules() as $rule )
{
PH::print_stdout("Rule '" . $rule->name() . "'");
}
$pan->save_to_file('panorama-modified.xml');
Every public method on PANConf, PanoramaConf, FawkesConf, the rule stores, the object stores, and the helper classes is fair game. See examples/ for thirteen worked examples of this style — including example-add-security-profile-to-all-rules-panorama.php, example-basics-panos.php, and example_plugin_rule_display_source_destination.php.
REST front-end
utils/api/v1/tool.php exposes every UTIL as GET /utils/api/v1/tool.php/<type>?.... Mount behind nginx + PHP-FPM. The endpoint validates the <type> against PH::$supportedUTILTypes and returns:
- 400 on unknown endpoint with
["error" => ..., "endpoint" => [<list of supported types>], "example" => ...]. - The UTIL's normal output (JSON if
shadow-jsonis in the query, human text otherwise) on success.
curl 'http://localhost:8082/utils/api/v1/tool.php/address?shadow-json'
This endpoint has no built-in auth. Always front it with an authenticating proxy or a service mesh in any deployment beyond localhost.
Docker image
docker pull swaschkut/pan-os-php:latest
docker pull swaschkut/pan-os-php-cli:latest
docker pull swaschkut/pan-os-php-api:latest
Three image variants exist for amd64 and arm64 (per create_all_docker_container.sh):
swaschkut/pan-os-php— interactive CLIswaschkut/pan-os-php-cli— non-interactive CLI for CIswaschkut/pan-os-php-api— bundled REST front-end
The develop tag tracks the development branch. For production CI, pin by digest.
Plugin system
loadplugin=path/to/file.php require_once's the file during UTIL::utilInit(). The plugin can append to any *CallContext::$supportedActions array, register new RQuery operators, or simply define helper functions used by an existing action. See examples/example_plugin_rule_display_source_destination.php for the working example.
Output integrations
| Output | Format | Consumer |
|---|---|---|
out=after.xml |
PAN-OS XML | Re-import into Panorama / firewall |
outputformatset=changes.set |
PAN-OS CLI set commands |
Paste into firewall CLI; review tool |
actions=exportToExcel:file.xlsx |
XLSX (via python3-xlsxwriter) |
Excel / Google Sheets review |
shadow-json on stdout |
JSON | jq / scripting / ingest into observability |
git= |
Git history around in= file |
Standard Git tooling |
actions=display |
Human text on stdout | Ad-hoc inspection |
type=html-merger |
Merged HTML report | Browser review |
Inputs accepted
- PAN-OS firewall running config XML (
<config>...</config>). - Panorama running config XML.
- Cloud-managed (Fawkes/Buckbeak) config XML.
- Live device via
api://IP,api://serial@panorama, orsase-api://.... - Vendor configs via
type=vendor-migration vendor=<X>for: ciscoasa, ciscoisr, ciscoswitch, netscreen, sonicwall, sophos, fortinet, srx (Juniper SRX), cp-r80 (CheckPoint R80), cp (CheckPoint), cp-beta, huawei, stonesoft, sidewinder, pfsense.
External services
- MaxMind GeoIP.
type=maxmind-updaterefresheslib/resources/geoip/from MaxMind's downloads; requires the operator-supplied license key (set in MaxMind account, passed via the UTIL's command line). - IronSkillet upstream.
type=ironskillet-updatepulls from the public IronSkillet GitHub repository. - MaxMind / IANA for protocol-number refresh via
type=protocoll-number-download. - Expedition integration is supported via the
expeditionflag andlib/classes/taskManagement/ExpeditionTaskReporter.php; pan-os-php was historically the engine inside Palo Alto's Expedition migration tool and retains the hooks. - GCP integration helper at
type=gcpfor Google Cloud-managed Palo Alto deployments. - SSH (
type=ssh-connector) uses bundledphpseclibfor SSH/RSA — the device must allow CLI SSH from the operator host.
Embedding pan-os-php in other tools
Common patterns:
- Ansible. Drop a wrapper module that shells out to
pan-os-php type=...and parsesshadow-jsonoutput. - GitOps. Store firewall configs as XML in a Git repo, run
pan-os-phpin CI to validate (type=xml-issue), generate diffs (type=diff), and push (type=upload) on merge. - Change-management gates. Run
actions=displayinside CI on every PR to surface what each merge would actually change in production. Block merges with unexpected diffs. - Self-service portals. Expose
utils/api/v1/tool.phpbehind an authenticating proxy and offer pre-canned UTIL invocations (display unused objects, list rules with broad source) as ticket-creating buttons.
Note
The framework is intentionally synchronous and CLI-shaped. There is no built-in webhook, no event stream, no scheduler. When something happens in PAN-OS, it is up to the surrounding system (cron, Ansible, your CI, Panorama's own scheduler) to trigger pan-os-php; pan-os-php itself only runs when invoked.
Logging and observability
There is no built-in metrics emission. To get observability:
- Wrap each invocation in a shell that records start/end time, exit code, and stdout to your log aggregator.
- Use
shadow-jsonand parse the resulting structure for action counts, warnings, errors. - Combine
outputformatset=with version-controlled storage so each change is auditable independently of the surrounding pipeline.
Compatibility matrix
| External system | Compatible | Notes |
|---|---|---|
| PAN-OS 8.1 | Yes | IronSkillet 8.1 templates bundled |
| PAN-OS 9.1 | Yes | IronSkillet 9.1 templates bundled |
| PAN-OS 10.0, 10.1, 10.2 | Yes | IronSkillet templates per-version |
| PAN-OS 11.0 | Yes | IronSkillet 11.0 templates bundled |
| PAN-OS 11.1+ / 12.x | Best-effort | Update via update_pan-os-php.sh; new XML elements can break specific actions until the framework is updated |
| Panorama | Yes | First-class — most operations are designed for Panorama scale |
| Cloud-managed (Strata Cloud Manager, formerly Fawkes/Buckbeak) | Partial | Classes exist (FawkesConf, BuckbeakConf, DeviceCloud, DeviceOnPrem); sase-api:// connector is partial |
| Prisma Access | Best-effort | Treated as a Panorama with managed devices; not all features mapped |
| Expedition | Yes | Hooks via expedition flag and ExpeditionTaskReporter |
| Python automation | Indirect | Run via subprocess and parse shadow-json; or use the REST front-end |
| Ansible | Indirect | Wrap as a custom module |