If you work with Palo Alto firewalls, you might know there's no straightforward way to find and remove unused address objects. When I googled for solutions, I found that others suggested using Expedition or some kind of automation. In this blog post, I'll show you a very simple script to find these objects and remove them if needed.
If you are looking for a more sophisticated solution, consider using 'pan-os-php'. Here is a blog post if you are interested.
Expedition or Automation
I tried using Expedition a few years back, but it required a dedicated VM, and I struggled to wrap my head around how to use it. I just needed a simple solution. While I could also use Palo Alto's REST API or even the Python SDK, setting everything up takes a bit more time.
Palo Alto 'Set' Commands
Then it occurred to me that Palo Alto provides 'set' commands, and you can use the 'delete' version of those commands to remove something. With that in mind, I thought, "Hmm, what if I get the whole config from either the firewall or Panorama in the 'set' format, run it through a regex, and extract all the object names?" Once I have the object names, I can go through the configuration line by line to check if the objects are referenced anywhere.
To make this work, I needed to ensure the script ignores the lines where the objects are initially defined. This is important because we don't want to mistakenly count the creation line as a reference. The goal is to find actual references elsewhere in the configuration.
By identifying which objects are not referenced at all, we can confidently generate 'delete' commands for those unused objects.
For this example, I'm using a simple example where two of the address objects are not referenced anywhere. When I run the script, it should find the objects and provide the 'delete' commands.
Here is the Script
Here's an example of how an address object could be defined in both Panorama and the Firewall using the 'set' commands. If you want to view the configs as set commands, run the following on the CLI.
set cli config-output-format set
configure
show
{{ TRUNCATED }}
set rulebase security rules allow_dns to any
set rulebase security rules allow_dns from any
set rulebase security rules allow_dns source any
set rulebase security rules allow_dns destination dns_servers
set rulebase security rules allow_dns source-user any
set rulebase security rules allow_dns category any
set rulebase security rules allow_dns application dns
set rulebase security rules allow_dns service application-default
set rulebase security rules allow_dns source-hip any
set rulebase security rules allow_dns destination-hip any
set rulebase security rules allow_dns action allow
set rulebase security rules ssh_access to any
set rulebase security rules ssh_access from any
set rulebase security rules ssh_access source app_server_1
set rulebase security rules ssh_access destination any
set rulebase security rules ssh_access source-user any
set rulebase security rules ssh_access category any
set rulebase security rules ssh_access application any
set rulebase security rules ssh_access service application-default
set rulebase security rules ssh_access source-hip any
set rulebase security rules ssh_access destination-hip any
set rulebase security rules ssh_access action allow
set application-group
set application
set address-group dns_servers static [ cloud_flare google_dns ]
set address-group google_dns_servers static [ google_dns google_dns_2 ]
set address google_dns ip-netmask 8.8.8.8/32
set address cloud_flare ip-netmask 1.1.1.1
set address app_server_1 ip-netmask 192.168.10.10
set address app_server_2 ip-netmask 192.168.10.11
set address app_server_subnet ip-netmask 192.168.10.0/24
set address google_dns_2 ip-netmask 8.8.4.4
set mgt-config users admin phash $5$gevustrd$unupSG0YZvZ/NUdMlmmP.mLjKnfGuUdmrWgYb1HoNp9
set mgt-config users admin permissions role-based superuser yes
set mgt-config password-complexity enabled yes
set mgt-config password-complexity minimum-length 8
In these lines, the name of the object comes right after the word 'address'. We need this for our regex to correctly identify and extract the object names.
- Get the 'set' commands output from the CLI, and save it to a file called
set_commands.txt
and keep it in the same directory as the script. - Run the script
import re
with open('set_commands.txt', 'r') as file:
config_lines = file.read().splitlines()
object_name_pattern = re.compile(r' address (\S+)')
def extract_object_names(config_lines):
object_names = set()
for line in config_lines:
match = object_name_pattern.search(line)
if match:
object_names.add(match.group(1))
return object_names
def check_object_references(config_lines, object_names):
references = {name: False for name in object_names}
for line in config_lines:
for object_name in object_names:
if object_name in line and not object_name_pattern.search(line):
references[object_name] = True
return references
object_names = extract_object_names(config_lines)
references = check_object_references(config_lines, object_names)
for object_name, is_referenced in references.items():
if not is_referenced:
for line in config_lines:
match = object_name_pattern.search(line)
if match and object_name == match.group(1):
delete_command = line.split(object_name)[0] + object_name
delete_command = delete_command.replace('set', 'delete')
print(delete_command)
break
#output
delete address app_server_2
delete address app_server_subnet
First, we import the re
module, which provides support for regular expressions in Python. We then open the configuration file, panorama_config.txt
, and read its contents, splitting the file into individual lines and storing them in config_lines
.
We define a regex pattern object_name_pattern
to match address object definitions. This pattern looks for lines containing the word 'address' followed by a non-whitespace sequence, which represents the object name.
Next, we define the extract_object_names
function. This function iterates through each line in the configuration file, applying the regex pattern to find and extract address object names. It collects these names into a set called object_names
, ensuring each name is unique.
The check_object_references
function is then defined. It takes the configuration lines and the set of object names as input. It initializes a dictionary, references
, where each key is an object name, and each value is a boolean indicating whether the object is referenced elsewhere. The function iterates through the configuration lines and checks if each object name appears in the line but isn't part of a line that defines the object itself (using the regex pattern to exclude those lines). If an object name is found in any other line, the corresponding value in the references
dictionary is set to True
.
After defining these functions, we extract the object names from the configuration lines and check their references using the functions. Finally, the script iterates through the references
dictionary. For each object that is not referenced (is_referenced
is False
), it searches for the line defining the object. When it finds this line, it replaces 'set' with 'delete' and prints the modified line, providing the command needed to remove the unused address object.
Closing Up
If there is a better way of doing this, please let me know in the comments.