panhygiene
Offline hygiene checker for Palo Alto Panorama and PAN-OS configurations: unused objects, duplicates, shadowed rules, and risky policy — from a config export, with zero network access.
$ panhygiene running-config.xml
panhygiene — Panorama config, 3 device group(s) + shared
Summary
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┓
┃ Location ┃ Score ┃ Rules ┃ Objects ┃ Unused ┃ Dupes ┃ Hazards ┃ Cleanup ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━┩
│ shared │ 78 │ 1 │ 15 │ 8 (53%) │ 2 (13%) │ 0 │ 11 │
│ Branch-Firewalls │ 50 │ 2 │ 3 │ 2 (67%) │ 1 (33%) │ 3 │ 4 │
│ Corp │ 92 │ 4 │ 2 │ 0 (0%) │ 0 (0%) │ 4 │ 2 │
│ DC-Firewalls │ 67 │ 3 │ 3 │ 1 (33%) │ 0 (0%) │ 2 │ 1 │
└──────────────────┴───────┴───────┴─────────┴─────────┴─────────┴─────────┴─────────┘
Hazards (9)
error B2 Corp / temp-allow-all Allow rule permits any source to any
destination on any service and any application.
error B7 Branch-FW / branch-ghost References 'ghost-object', which does not exist.
warning A7 Corp / srv-dns Device-group object shadows the shared object
of the same name.
warning B1 DC-FW / dc-db-in-legacy Fully covered by earlier rule 'dc-allow-broad';
it is redundant.
...
Cleanup (18)
warning A1 shared / srv-dns-old Address object (10.0.0.54/32) is not referenced
by any rule or group.
...
Findings: 3 error(s), 18 warning(s), 6 info(s).
Why this exists
Palo Alto Networks has announced end-of-life for Expedition, the free tool many engineers relied on for config cleanup. The built-in Policy Optimizer remains excellent for what it covers — unused rules and unused apps — but nothing free covers unused objects, duplicate objects, empty groups, or device-group objects shadowing shared ones. Those are exactly the things that accumulate over years of firewall operations and make every change review slower.
panhygiene fills that gap: a small, sharp, offline CLI that reads a config export and tells you what to clean up and what to worry about.
v0.1.0 status: tested against synthetic configurations and the public config fixtures from Palo Alto's own tooling repos — not yet against a large production Panorama config, because we don't keep any. If the parser stumbles on yours, please run
panhygiene config.xml --diagnosticsand open an issue with the output — it contains only counts and XML tag names, nothing identifying. Parser reports are the single most valuable contribution right now.
Your config never leaves your machine
A firewall configuration is one of the most sensitive artifacts your organisation has. panhygiene is built around that fact:
- No network code. The tool reads one local XML file and writes a report. There are no API clients, no credentials, no telemetry, no crash reporting, no update checker. This is a permanent design guarantee, not a version note.
- Verifiable. The codebase is a few small modules — grep it for
socket,http,urllib, orrequestsand you'll find nothing. Run it in a network namespace or an air-gapped host; nothing changes. Dependencies arerich(terminal rendering) and optionallylxml(faster XML parsing).
Install
pip install panhygiene
# or, isolated:
pipx install panhygiene
Python 3.9+ — the floor is 3.9 deliberately, because that is the system Python on RHEL 9 and its derivatives, which is what actually runs on many network-ops jump hosts. No compiled dependencies required.
Quickstart
- Export your config from Panorama:
- GUI: Panorama > Setup > Operations > Save named Panorama configuration snapshot, then Export named Panorama configuration snapshot.
- CLI over scp:
scp export configuration from running-config.xml to user@host:/path/ - Standalone firewall exports and
<shared>-only configs work the same way.
- Run:
panhygiene running-config.xml
Useful variants:
panhygiene config.xml --format html -o report.html # self-contained report to email
panhygiene config.xml --format json | jq .summary # automation
panhygiene config.xml --names-only > flagged.csv # just the names, as CSV
panhygiene config.xml --plain # unstyled text for tickets
panhygiene config.xml --only A --strict # object checks, list soft-kept objects
panhygiene config.xml --skip B6,B5 # not everything matters everywhere
panhygiene config.xml --fail-on error # CI: fail only on errors
Exit codes: 0 no findings at/above --fail-on (default info), 1 findings,
2 tool error. Summary entries (group C) never affect the exit code.
Checks
Findings are grouped into Hazards (can change what traffic is allowed, or hide it) and Cleanup (dead weight, safe to tidy once confirmed).
| ID | Category | Severity | Detects | Why it matters |
|---|---|---|---|---|
| A1 | cleanup | warning | Unused address objects (transitively — an object referenced only by an unused group is unused) | Dead objects slow reviews and hide mistakes |
| A2 | cleanup | warning | Unused service objects | Same |
| A3 | cleanup | warning | Unused address/service groups, including chains of groups only referencing each other | Same |
| A4 | cleanup | info | Duplicate address objects (identical value, different names), as clusters | Consolidation targets; edits in one copy miss the others |
| A5 | cleanup | info | Duplicate service objects (identical protocol/port) | Same |
| A6 | cleanup | warning | Empty static groups | Rules using them match nothing — usually broken intent |
| A7 | hazard | warning | Device-group object with the same name as a shared object | The DG object silently wins in that DG; a classic Panorama footgun |
| B1 | hazard | warning | Shadowed rules — a later rule fully covered by an earlier one in the same rulebase (exact-superset only, see limitations) | The later rule never matches; a shadowed deny is a hole |
| B2 | hazard | error | Allow rules with any source + any destination + any service + any application | The rule that ate the firewall |
| B3 | cleanup | warning | Allow rules with no security profiles/group | Permitted traffic bypasses threat inspection |
| B4 | hazard | warning/info | Rules with logging fully disabled (warning) or no log forwarding profile (info) | Invisible traffic, logs that never reach the SIEM |
| B5 | cleanup | info | Disabled rules | Long-disabled rules are usually dead |
| B6 | cleanup | info | Rules with no description | Intent evaporates when the author leaves |
| B7 | hazard | warning | References to objects that don't exist in any visible scope | Broken config; commits will fail. Warning (not error) in v0.1.0 — see limitations |
| C1 | summary | info | Per-location counts: rules, objects, unused %, duplicate % | Orientation |
| C2 | summary | info | Hygiene score per location | Track cleanup progress over time |
Every finding carries: severity, device group, object/rule name, full XPath
(--verbose / always present in JSON and HTML), a one-line explanation, and a
suggested remediation. Remediations are always advisory text — panhygiene never
emits executable config or delete commands.
The hygiene score
No magic weighting — the score is the average of three ratios, mapped to 0–100:
unused_ratio = unused objects / total objects
dup_ratio = redundant duplicate copies / total objects
rule_ratio = rules with error- or warning-level findings / total rules
score = 100 × (1 − (unused_ratio + dup_ratio + rule_ratio) / 3)
Scores reflect the checks you ran: --skip A1 and unused addresses stop counting.
The score is designed for tracking one config over time — run it before and after cleanup sprints and watch it move. It is not meaningful for comparing different organisations or even different device groups with very different sizes and purposes.
Design principle: false positives are the cardinal sin
A cleanup tool you can't trust is worse than no tool. Wherever panhygiene cannot be certain, it stays silent and tells you why:
- Unmodelled rulebases. References from policy types the tool doesn't fully
parse (PBF, QoS, decryption, authentication, DoS, SD-WAN…) still mark objects
as "in use", so they are never falsely reported unused. The report prints how
many objects were kept this way;
--strictlists them for manual review. - Disabled rules count as usage — deleting an object referenced by a disabled rule breaks the config.
- Device-group hierarchy. Parent-DG objects referenced by child DGs resolve
correctly when the export contains the hierarchy (
<readonly>) section. If it doesn't, panhygiene warns prominently and resolves cross-DG references permissively rather than guessing. - Literal values. Raw IPs/CIDRs/ranges in rule fields, predefined services, region codes, and EDL names are never flagged as broken references.
Known limitations (read this)
- B1 shadowing is deliberately conservative. It detects exact-superset shadowing only, on literal member names — it does not expand groups or compute CIDR containment, and a rule matched on users, URL categories, schedules, or negated fields is never treated as covering another. This means B1 misses real shadowing (e.g. an earlier rule using a group that contains the later rule's specific object). It also means that when B1 does flag a rule, it's worth your attention. Full set-theoretic overlap analysis is a possible future direction; it is not in v1 because its false positives destroy trust.
- Unused-object analysis considers security policy, NAT policy, group membership, and (conservatively) all other rulebase types. It does not parse templates/network config, so an address object referenced only from e.g. an IKE gateway inside a template would be a false positive — rare for device-group objects, but verify with Where Used before deleting, always.
- Dynamic address groups match by tag; membership is not resolved. A DAG is only reported unused if no rule references the DAG itself.
- The predefined-object knowledge is hand-maintained, not extracted from a
PAN-OS
predefined.xml. The tool knowsservice-http/service-https, theany/application-defaultkeywords, and treats 2-letter uppercase names as built-in region codes. PAN-OS versions ship predefined objects this list doesn't know, which could make B7 flag a valid reference. That is why B7 is a warning (not an error) in v0.1.0, and why--predefined FILEexists: put any additional predefined names in a file (one per line) and B7 will treat them as always existing. If you hit one, please open an issue naming it — the built-in list only improves through reports. - B7 does not validate application names.
- Duplicate clusters spanning locations are attributed to the first member's location in the per-DG summary counts.
- The tool analyses the exported XML as-is. Export the running config if you want findings about what is actually enforced.
Performance
The parser streams (via iterparse) and frees each element after converting it,
so peak memory is bounded by the largest single rule or object entry — not the
file size. A 100 MB+ Panorama export parses comfortably; installing the lxml
extra (pip install "panhygiene[lxml]") speeds it up further. B1 is quadratic
per rulebase in the worst case but bails early on the first field mismatch;
rulebases in the tens of thousands of rules may take a couple of minutes.
CI usage
# fail the pipeline if a config export contains error-level findings
- name: Config hygiene
run: |
pip install panhygiene
panhygiene exported/running-config.xml --fail-on error --format json -o hygiene.json
Contributing
See CONTRIBUTING.md. Two rules are non-negotiable: no real firewall configs in the repo (test fixtures are synthetic, RFC 5737/RFC 1918 only), and no network access in the tool, ever.
License
MIT. Not affiliated with or endorsed by Palo Alto Networks. "Palo Alto Networks", "Panorama", and "PAN-OS" are trademarks of Palo Alto Networks, Inc.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file panhygiene-0.1.0.tar.gz.
File metadata
- Download URL: panhygiene-0.1.0.tar.gz
- Upload date:
- Size: 42.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b7049e9e3aa03937ba429eeec817c98c74b79f337d72dadb10efadc52acc6cbe
|
|
| MD5 |
b52fd64e104964471b99b9dff52735b9
|
|
| BLAKE2b-256 |
6d02b22b678d2da0db389c35ca8911a16ead56d45aeb05587f49452a86ff63fb
|
Provenance
The following attestation bundles were made for panhygiene-0.1.0.tar.gz:
Publisher:
publish.yml on BelalMou/panhygiene
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
panhygiene-0.1.0.tar.gz -
Subject digest:
b7049e9e3aa03937ba429eeec817c98c74b79f337d72dadb10efadc52acc6cbe - Sigstore transparency entry: 2287455652
- Sigstore integration time:
-
Permalink:
BelalMou/panhygiene@e65c41e70f631d721916e1ba057f7c214361de9e -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/BelalMou
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e65c41e70f631d721916e1ba057f7c214361de9e -
Trigger Event:
push
-
Statement type:
File details
Details for the file panhygiene-0.1.0-py3-none-any.whl.
File metadata
- Download URL: panhygiene-0.1.0-py3-none-any.whl
- Upload date:
- Size: 37.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
616ac49b65f59b89c227200de755d96389f862de2e1b4d9aa031a3420d7f793b
|
|
| MD5 |
0f539fc9495ae143a0e39b45e60a67b9
|
|
| BLAKE2b-256 |
5e263c0eb3a549c36dca30ade305901f010312331b280faaee74c4174f423d35
|
Provenance
The following attestation bundles were made for panhygiene-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on BelalMou/panhygiene
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
panhygiene-0.1.0-py3-none-any.whl -
Subject digest:
616ac49b65f59b89c227200de755d96389f862de2e1b4d9aa031a3420d7f793b - Sigstore transparency entry: 2287455719
- Sigstore integration time:
-
Permalink:
BelalMou/panhygiene@e65c41e70f631d721916e1ba057f7c214361de9e -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/BelalMou
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e65c41e70f631d721916e1ba057f7c214361de9e -
Trigger Event:
push
-
Statement type: