I want to mention here not the XML or REST API usage now , but the theory in python how the Panorama works with device-groups for managed firewalls. This post is rather on how to parse a Panorama XML configuration to grab out data we are looking for or just audit the enterprise level configuration.
Device groups in Panorama are used to build configuration blocks that are shared among the managed
firewalls.
Device groups provide a way to organize and reuse your policies by applying the principle of inheritance
and implementing a well-defined device group hierarchy.
Let say we have the following device-group hierarchy:
{
"global": {
"datacenters": {
"apac": {
"tokyo": {}
}
},
"branches": {
"america": {
"newyork": {},
"washington": {}
},
"emea": {
"paris": {}
}
}
}
}
If we have to implement policies that apply to every firewall in all branches only, we add the policy to the branches device-group and that way every firewalls in america (New York, Washington) and in emea (Paris) will inherit that policy and that policy is not redundant since it exist only at one place (in branches device-group).
In the hierarchy the parent of every device-group is the “shared” (device-group) if we dont set parent device-group. While creating the device-group the parent device-group can be configured in the device-group settings:
With defining the child parent relationship for each device-group we can build a hierarchy where we can share for example the same policies to multiple managed firewalls in the same environment or region.
The parent device-group is accessible in the readonly section of the panorama xml configuration:
XPATH for each device-group: “./config/readonly/devices/entry/device-group/entry”
Each entry has an attribute “name” that holds the name of the device-group.
The tag used for setting the parent device-group is “parent-dg”, that holds the name of the parent device-group. See the following example with globla that has no parent and the datacenters where the parent is the global:

Apart from that we can have the same policies for multiple managed firewalls there is another option available for the device-group hierarchy. When device-groups at different levels in the device-group hierarchy have an object with the same name but with different value, for the managed firewall the object value will be used from the last descendant device-group where the same object name has been configured.
{
"global": {
"datacenters": {
"apac": {
"tokyo": {}
}
},
"branches": {
"america": {
"newyork": {},
"washington": {}
},
"emea": {
"paris": {}
}
}
}
}
If we have an address h_testhost with ip-netmask 1.1.1.1 in america device-group and in washington the address h_testhost with ip-netmask 2.2.2.2 exists, the managed firewall for washington device-group will use the 2.2.2.2 address since this is the nearest device-group or highest that has the same object name.
To get this hierarchy we need the readonly section of the panorama configuration in XML to get the .
XPATH: “./config/readonly/”
From that relationship we can generate per device-group the child list:
# Function to find all children to each device-group
def find_children(ro_element):
# we create a dictionary with key as the device-group and with value as a list of direct child device-groups
dg_children = {}
dg_children['shared'] = []
for dg in ro_element.findall('./devices/entry/device-group/entry'):
dg_name = dg.get('name')
if dg.find('./parent-dg') is not None and len(dg.find('./parent-dg').text) > 0:
parent_dg_name = dg.find('./parent-dg').text
else:
parent_dg_name = 'shared'
if parent_dg_name not in dg_children:
dg_children[parent_dg_name] = [dg_name]
else:
dg_children[parent_dg_name].append(dg_name)
return (dg_children)
And from the output of GET CHILDREN we can generate the tree structure of the device-groups:
# Function to build the device-group tree recursively
def build_tree(dg_children, parent):
children = dg_children.get(parent, [])
tree = {}
for child in children:
tree[child] = build_tree(dg_children, child)
return tree
dg_children = find_children(ro_element)
And we cen generate per device-group the descendants:
# Function to find all descendants fora a device-group recursively
def find_descendants(dg_children, parent_dg):
dg_descendants = [] # List to store descendants of a device-group
# Find all children of the given parent
if parent_dg in dg_children:
children = dg_children[parent_dg]
for child in children:
# add a single list element to the descendant list
dg_descendants.append(child)
# extend with multiple list elements the device-group with recursively get descendants
dg_descendants.extend(find_descendants(dg_children, child))
return dg_descendants
Or we can generate per device-group the anchestors:
# Function to find all anchestors for a device-group recursively
def find_ancestors(ro_element, child_dg):
dg = ro_element.find("./devices/entry/device-group/entry[@name='" + child_dg + "']")
if dg.find('./parent-dg') is not None and len(dg.find('./parent-dg').text) > 0:
parent_dg = dg.find('./parent-dg').text
# Recursively collect ancestors from the parent
ancestors = find_ancestors(ro_element, parent_dg)
# Add the current parent to the list of ancestors
ancestors.append(parent_dg)
return ancestors
# Base case: If there is no parent (None or no text in tag parent-dg), return 'shared'
else:
return ['shared']
For example to find all the unused addresses, this is how we can iterate over the device-groups including shared as well. The iteration over device-group script (with shared):
# Iterate over all device-group in the xml including shared
pa_all_dgs = {"shared": "./shared", "default": "./devices/entry/device-group/entry"}
for key in pa_all_dgs:
xpath = pa_all_dgs[key]
for dg in root.findall(xpath):
if key == "default":
dg_name = dg.attrib["name"]
else:
dg_name = key
print(dg_name)
If you want to parse the rulebase objects for a device-group that has an allocated managed device, you can check if the object exists in the current device-group and if not you have to iterate over its anchestors in the reverse order of the list, since the find_anchestors functions delivers a list of device-groups from shared till the parent device-group.
For example for paris device-group the anchestors should be used to get the rulebase of it and its anchestor device-groups extending this with pre and post rules for rulepositions:
# Print anchestors for the given device-group
dg_list = find_ancestors(ro_element, "paris")
print(dg_list)
# Output will be
#['shared', 'global', 'branches', 'emea']
dg_list.append('paris')
for dg in reversed(dg_list):
print(dg)
# Iterate over the device-group policies from paris perspective with rule position, pre and post since both needed.
rule_pos_list: list[str] = ["pre", "post"]
print(f'the right order to check rulebase objects for the device-group {dg_name}:')
dg_list.append(dg_name)
for rule_pos in rule_pos_list:
print(f'rule position {rule_pos}:')
xpath_rulebase = "./" + rule_pos + "-rulebase"
for dg in reversed(dg_list):
print(f'device-group {dg}, with rulebase {rule_pos}-rulebase')
The complete script is available on my github site:
https://github.com/itsecworks/PaloAlto_Auditing/blob/main/scripts/panorama_parser.py
Posted on January 17, 2025
0