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.


Hi
Great post. This command
“pan-os-php in=pa-test1.xml type=address ‘filter=(object is.unused.recursive)’ actions=delete out=pa-test1-out.xml” gives first all the “delete shared address “name_of_object”, then a bunch of virus, spyware and vulnerability related “set” commands that has nothing to do with type=address. I have tested it against two different Panorama XML files.
This gives a clean output with just the “delete shared address…” commands:
pan-os-php in=pa-test1-out.xml type=address ‘filter=(object is.unused.recursive)’ actions=delete out=pa-test1-out2.xml
While this also includes the virus, spyware and vulnerability set commands:
pan-os-php type=diff file1=pa-test1.xml file2=pa-test1-out2.xml outputformatset
Very confusing and would like to know if it happens to others too.
by the first usage of pan-os-php, it must be validated, if all Palo Alto Networks [PANW] default config behaviour is visible inside the XML file.
if not, a file compare does not make sense in specific if you are working on best-practice assessments.
PANW is NOT showing all default config within the XML file, so pan-os-php must bring in the default PAN-OS config part.
As soon as this is done, all other parts like object cleanup, merging aso. can be directly done.
Hi Sven
Great tool you’ve created! So we have just to ignore the other “set” commands output that are generated and only use the “delete” commands? This is regarding decommission/deleting of unused objects.
if you like to use the “set commands” for deleting objects, then of course you only need to focus on the “set” information.
Instead of going through the whole diff process, as nicely described in this blog:
pan-os-php type=diff file1=pa-test1.xml file2=pa-test1-out2.xml outputformatset
Why not just use the decommission action?:
pan-os-php in=pa-test1.xml type=address ‘filter=(object is.unused.recursive)’ actions=decommission outputformatset
Decommission does even a better job by removing unused nested groups. Example:
ObjectA is a member of GroupA and GroupA is a member of GroupB
● ObjectA and GroupA and GroupB are not referenced anywhere else in the configuration
I tested with a service object and it removed first groupB, groupA and at the end ObjectA.
Below is from original documentation
● GroupB will be removed (from documentation, outdated?)
● ObjectA and GroupA will remain (from documentation, outdated?)
indeed, actions=decommission can be also used.
if you are referencing this User Guide:
https://github.com/swaschkut/pan-os-php/blob/main/PAN-OS-PHP%20User%20Guide%20-%20shareable%20outside%20Palo.pdf
this is in specific working for actions=delete;
actions=decommission is also removing ‘used’ objects, which is the case in your example.
used, inside a different Group