There are recurring cases where tasks cannot be edited quickly and easily using the classic Palo Alto Networks GUI or Panorama. For example, editing multiple policies at once, such as during a zone migration. Or checking which policies haven’t log forwarding enabled, hence enabling it directly. Or finding unused objects, including deleting them.
For these situations (and many more!), there’s a tool with a wealth of predefined scripts: pan-os-php. This first blog post covers installation and some initial use cases.
Installation
pan-os-php is based on Docker and is maintained by Sven Waschkut. (Note that it was formerly under the hood of Palo Alto Networks itself, but is not updated anymore there.)
If you are using Windows, you can use the Windows Subsystem for Linux (WSL) as the easiest way to get it working:
1 |
wsl --install Ubuntu-24.04 --web-download |
Mac users can use Colima.
For the Docker part, you can follow the official documentation, which is:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
sudo apt-get update sudo apt-get install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin |
Adding the current user to the Docker group to be able to start Docker without root privileges: (logout required after the following command to get it working)
1 |
sudo usermod -aG docker $USER |
By the way: It seems that Docker does not offer IPv6 support by default. What a shame! Come on, it’s 2025! Some help is here.
The installation of pan-os-php is simple: (Use the same command to update it later on.)
1 |
docker pull swaschkut/pan-os-php:latest |
1 |
docker run --name panosphp --rm -v ${PWD}:/share -it swaschkut/pan-os-php:latest |
Basic Steps
Being in the Docker instance, everything starts with “pan-os-php” again. Press Tab two times to get some inline information. Quite useful helping keywords are: listfilters, listactions, and help, which work almost everywhere.
A very basic command uses the following three options:
- in=filename.xml
- type=<various-types-see-examples-below-or-inline-help>
- out=filename-out.xml <- if not used, the default output is /dev/null ;)
1) Getting the Configuration
You can either manually download an XML file from a Palo/Panorama and place it into your working directory (which is automatically listed as the “/share” folder), or you can download it via the API from the device itself. This requires the type=upload:
1 |
pan-os-php in=api://192.168.21.2 type=upload out=pa-test1.xml |
Here’s a sample run:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
ubuntu@ef7a914fc696:/share$ pan-os-php in=api://192.168.21.2 type=upload out=pa-test1.xml *********************************************** *********** pan-os-php.php type=upload UTILITY ************** - PAN-OS-PHP version: 2.1.36 [UNIX] [8.4.5] ** Request API access to host '192.168.21.2' but API was not found in cache. ** Please enter API key or username [or ldap password] below and hit enter: pan-os-php-user ** you input user 'pan-os-php' , please enter password now, password is hidden : ******************** * Now generating an API key from '192.168.21.2'... OK, key is e6bc89fb5328...GIvhwcOsg-Mw - Downloading config from API... - Detected platform type is 'panos' - Opening/downloading original configuration... - Now saving configuration to - pa-test1.xml... ************* END OF SCRIPT pan-os-php.php type=upload ************ |
After that, you can use this local XML rather than the API for every step you’re doing.
2) Do Something
Now you can work with this freshly downloaded XML file in various ways. Please have a look at the “First Use Cases” below.
A few basic keywords, though:
- location=vysyX <- Select the vsys for a firewall (default: vsys1) or the device group for a Panorama (default: shared).
- template=any <- Select the template if you’re within Panorama.
As a best practice, you should always do a first run with only the filter criteria (in order to see which objects will be touched), followed by a second run with the actual requested action.
3) Get the Changes
I personally prefer to extract “set” commands that I can use on the CLI on a Palo/Panorama rather than uploading a complete XML to the device itself. Fortunately, this can be done in the following way, which compares the input and output XMLs and displays appropriate “set” commands:
pan-os-php type=diff file1=pa-test1.xml file2=pa-test1-out.xml outputformatsetAlternatively, you can omit step 3 by specifying the following “outputformatset” directly within the “Do Something” part, which is: outputformatset=textfile.txt.
First Use Cases
Finding (and deleting) unused objects
For sure, everyone has unused objects such as addresses, groups, services, etc. Use pan-os-php in the following way to get rid of those objects. Hint: Yes, pan-os-php works recursively with this cool object filter: “is.unused.recursive”.
First run, just to get some information on which objects are unused, looking at the “address” and “service” objects here:
1 2 |
pan-os-php in=pa-test1.xml type=address 'filter=(object is.unused.recursive)' pan-os-php in=pa-test1.xml type=service 'filter=(object is.unused.recursive)' |
Second run to delete those objects. Note that you need the “out=” parameter for this while the 1st out is the 2nd in:
1 2 |
pan-os-php in=pa-test1.xml type=address 'filter=(object is.unused.recursive)' actions=delete out=pa-test1-out.xml pan-os-php in=pa-test1-out.xml type=address 'filter=(object is.unused.recursive)' actions=delete out=pa-test1-out2.xml |
Finally, generating the “set” commands:
1 |
pan-os-php type=diff file1=pa-test1.xml file2=pa-test1-out2.xml outputformatset |
Now I can use those commands within the CLI to get rid of those unused objects. Just for reference, here are a few commands out of my test run:
1 2 3 4 |
delete address-group "g_thisgroupnousage" delete address "n_sp-71" delete address "n_sp-71_v6" delete address "h_just-an-object" |
✅
Activating log and log forwarding on all policies
If you really want to be sure that you’re logging every policy hit, just do this. First run: on which policies is either the “log at session end” not set, or no log forwarding profile specified:
1 |
pan-os-php in=pa-test1.xml type=rule 'filter=!(log at.end) or !(logprof is.set)' |
Second run, enabling “log at session end” on all of those policies and setting the log forwarding to the profile called “default”. Multiple actions in the same run can be done by separating them with a slash. This time, I’m using the appendix “outputformatset=textfile.txt” to get the required commands directly rather than setting the “out=…” parameter:
1 |
pan-os-php in=pa-test1.xml type=rule 'filter=!(log at.end) or !(logprof is.set)' actions=logEnd-Enable/logSetting-set:default outputformatset=logging-enabled.txt |
Here are a few of such commands, just for reference again. Note that only the required commands are listed by pan-os-php, that is: rules that had either the logging enabled or the log forwarding set already will only get the other required command (last two lines):
1 2 3 4 5 6 7 8 |
set rulebase security rules "ntp2 aka pi06-dach" log-end yes set rulebase security rules "ntp2 aka pi06-dach" log-setting default set rulebase security rules "ns2 aka pi06-dach" log-end yes set rulebase security rules "ns2 aka pi06-dach" log-setting default set rulebase security rules "ntp4" log-end yes set rulebase security rules "ntp4" log-setting default set rulebase security rules "ad-clients to ad" log-end yes set rulebase security rules "ad-clients to Stg raus" log-setting default |
✅
Migrating a zone to another
A slightly more complex scenario: In case you are migrating some networks from one zone to another, you want to add a second src|dst zone to all rules that currently have another zone as their src|dst.
In this example, I’m using pan-os-php with a panorama.xml file, hence specifying a location. The zone “S2S-VPN” shall get a sibling of “Transfer”. I’m doing the “from” in the first run, and the “to” in a second, while extracting the set commands from a diff between the original input and the 2nd output:
1 2 3 |
pan-os-php in=panorama.xml type=rule location=Segmentation 'filter=(from has S2S-VPN)' actions=from-add-force:Transfer out=panorama-out1.xml pan-os-php in=panorama-out1.xml type=rule location=Segmentation 'filter=(to has S2S-VPN)' actions=to-add-force:Transfer out=panorama-out2.xml pan-os-php type=diff file1=panorama.xml file2=panorama-out2.xml outputformatset |
Yet another example. Replacing (rather than adding) one zone to another, in this case: “WAN” becomes “Transfer”, both for “from” and “to”. No filter needed, since only policies are involved where the current WAN zone is present. Both actions at one. One-liner:
1 |
pan-os-php in=panorama.xml type=rule location=Segmentation actions=from-replace:WAN,Transfer,true/to-replace:WAN,Transfer,true outputformatset=zone-replacement.txt |
✅
Outlook
Some more advanced use cases are:
- rule analyses concerning best practices (formerly Iron Skillet)
- firewall migration from 3rd party devices, e.g. Sophos (formerly Expedition)
- correction of misconfigured objects
Some of those will be covered in upcoming blog posts. Stay tuned.
Photo by Mohamed Munawwar Luthfee on Unsplash.