Coverage and blind spots¶
Read this before you delete anything based on unused
psc's unused result means "unreferenced by the object groups and
policy rulebases in the config you handed it" — not "safe to delete from
Panorama." Several places that legitimately reference an object are not
scanned at all, so an object that is genuinely in use can be reported
unused. Deleting it then breaks something with no warning.
The merge/rename repointing is trustworthy for the reference sites psc does scan (it repoints them before deleting, and blocks when it can't). The risk is the sites it never sees — listed below.
This page is the authoritative map of what the reference graph (refs used,
refs unused, refs dangling) and the audit/dedup engines do and do not look
at. Keep it open when you act on a finding.
What psc scans¶
psc parses a Panorama config export (or the live running config) and reads
only:
sharedand each device-group's objects: addresses, address-groups, services, service-groups, tags.- Each device-group's policy rulebases, pre- and post-:
security,nat, and (since v0.3.0)pbf,decryption,authentication,qos,application-override,dos,sdwan,tunnel-inspect,network-packet-broker.
Within those rulebases it models exactly this reference surface:
| Field | Namespace | Scanned |
|---|---|---|
source / destination |
address (+ address-group) | ✅ all rulebases |
service |
service (+ service-group) | ✅ all rulebases that have it |
rule tag |
tag | ✅ all rulebases (security, NAT, and the 9) |
NAT source-translation / destination-translation |
address | ✅ where-used; review-gated for repoint |
PBF forwarding nexthop (fqdn variant) |
address | ✅ where-used; review-gated for repoint |
| static address-group / service-group members | address / service | ✅ |
| dynamic address-group (DAG) membership | address | ✅ from config tags; registered IPs with --live-dag on a live source (see below) |
"Review-gated" means psc sees the reference and will block a merge/rename/delete that would strand it (it cannot rewrite a nested, non-member-list field automatically) — a loud refusal, never a silent break.
What psc does NOT scan¶
Ordered by how badly each can hurt you.
1. Templates and network/device config — not parsed at all¶
psc reads device-group objects and policy only. It never opens templates / template-stacks or the network/device config. So an address object referenced by any of these reads as unused:
- IKE / IPSec gateway peer addresses,
- GlobalProtect portal / gateway addresses,
- service routes, DNS proxy, virtual-router static routes, interface addresses,
- log-forwarding / SNMP / syslog server destinations,
- any address used purely in network or device settings.
Deleting such an object because psc called it "unused" can break VPN,
management, routing, or logging — with no warning, because psc literally never
saw the reference. This is the most dangerous gap. Treat every unused
result on a shared object as "unused by policy," and verify in Panorama
before deleting.
2. Dynamic address groups (DAGs): config tags always, registered IPs only with --live-dag¶
A DAG includes addresses by a tag expression (e.g. 'prod' and 'web'), not
a static member list. Since v0.4.3 psc evaluates that filter against the
static tags it already parses, so an address whose only use is being matched
into a rule-referenced DAG is treated as reachable — it is no longer reported
unused, and refs used <addr> shows the DAG (as a dynamic referrer) on the
path to the rule. DAG filters are also still parsed for the unused-tag check.
The other half is runtime, not config. An externally registered IP (XML-API / User-ID / VM-info / cloud plugin) pulls an address into a DAG. That address carries no config tag. The export psc reads cannot show that membership.
On a live source, psc refs unused --live-dag closes this gap. psc reads the
connected firewalls from Panorama (show devices connected). psc then reads the
registered IPs of each firewall (show object registered-ip all). psc adds the
registered tags to the tag set that it matches against the DAG filter. A
rule-referenced live DAG then holds that address, and the address leaves the
unused list. A DAG that no rule references keeps its members off the reachable
set, live data or not. refs used shows the DAG on the path of that address,
with the field dynamic-registered. The field name is a warning. A registered
IP is registered against an IP, not against an address object. A rename cannot
repoint that edge.
The match is deliberately narrow. psc joins a registered value to an address
object only when the two values are identical. A registered host therefore never
marks a larger network object as used. psc does no DNS, so an FQDN object never
matches a registered IP. A registered host does not match an ip-range object of
one address either. psc evaluates the filter once for each firewall that
registered the value, and one firewall is enough for a match. psc never joins
the tags of two firewalls into one set. A filter that negates a tag would then
lose a member that one firewall really holds.
The registered map is estate-wide. A device-group DAG still matches only the
addresses that its own device-group chain can see. A shared DAG matches every
address of the export on the live path, because PAN-OS pushes a shared DAG to
every device group. That direction adds members, which is the safe direction for
unused. The config-tag match keeps the device-group chain in both cases.
psc sends show object registered-ip all without a vsys. psc therefore reads
what the firewall reports for the vsys of the API key. A registration that only
another vsys holds can stay invisible. Give the API key access to every vsys
that you must cover.
--live-dag fails closed. psc refuses an offline source (exit 9). psc refuses
a Panorama that manages no connected firewall (exit 7). psc stops when a
firewall query fails (exit 7). An answer that psc cannot read is a failed
query, and it exits 7 too. Four answers count as unreadable:
- an answer with no
<result>element; - an answer whose
<count>does not agree with the number of rows; - an answer whose
<count>is not a number; - an answer that holds no
entryrow and holds another element instead.
psc never reads such an answer as "this firewall holds no registration".
A firewall that Panorama names and that psc cannot query stops the command in
the same way (exit 7). psc cannot query a firewall that show devices
connected reports as not connected, and it cannot query a device row with no
serial number. Such a firewall still holds its registrations, so it is a gap in
the coverage. psc never drops that row.
Add --live-dag-partial to continue after a firewall that psc cannot read. psc
then reads the firewalls that answer. psc names each firewall that it could not
read on the warning channel, and --no-caveat does not silence that
channel. The stderr caveat names them as well, and it states that the coverage
is partial. refs used --strict refuses to call an object unused while the
coverage is partial (exit 7). A firewall that psc could not read can hold the
one registration that makes the object live.
A registered row with no ip attribute makes the coverage partial too. psc does
not know the subject of that row, so psc cannot rule out that the row holds the
registration of the traced object. A registered value that psc reads and cannot
normalize is a different case. psc names that value on the warning channel, and
the operator can see that it is not the value of the traced object. Such a value
therefore keeps the coverage complete. Without a live source, psc keeps the
config-only behaviour below.
Because runtime DAG membership cannot be computed from the config, refs unused
shows each candidate's tags (a tags column in table/csv, a real list in
json/jsonl/yaml) so you can judge this by hand. A tag-bearing candidate deserves
a closer look: its config tags may tie it to a DAG whose reachability psc cannot
fully resolve — e.g. a DAG referenced only from an unscanned template, or one
that also gathers registered IPs sharing that tag name. The tags are a heuristic
signal, not proof of use: config-tag membership of a rule-referenced DAG is
already resolved (such objects never reach this list), and the purest
registered-IP case above carries no config tag at all, so an empty tags
cell does not clear a candidate.
If a DAG's filter is malformed/unparseable, psc does not guess its
membership (it matches nothing) and prints a warning on stderr naming that DAG,
so you know its coverage is unverified.
3. Whole object categories psc does not model¶
These are not object kinds psc tracks, so it can neither tell you what references them nor follow references to them:
- security / decryption / DoS profiles and profile-groups,
- schedules,
- external dynamic lists (EDLs),
- applications and application-groups (including custom apps),
- custom URL categories, regions, HIP profiles, user-groups.
Two consequences: (a) renaming/deleting one of these is outside psc's safety net
entirely; (b) when one of these names appears in a rule field psc does read
(e.g. an EDL or region in source), it won't resolve to an address/service and
will surface as a dangling false-positive — and it never seeds
reachability, so it can't keep a different object from looking unused.
4. The config is a single snapshot¶
Findings reflect the one export/device you pointed psc at. References from another Panorama, from pushed templates, or from firewall-local config (rules a firewall has that aren't in Panorama) are invisible. A shared object that looks unused here may be referenced by a managed firewall's local config.
Annoying but safe (won't cause a bad delete)¶
- Disabled rules count as "used." psc seeds reachability from disabled rules too, so an object used only by a disabled rule is not reported unused. Safe from deletion — but you also can't easily find such objects to clean up. (Tracked separately.)
danglingnoise from built-ins. Onlyany,application-default,service-http,service-https, andservice-dnsare whitelisted. Other built-in services, regions, or EDL names in a rule will be reporteddanglingeven though they are valid. Skimdanglingoutput for these before treating it as an error list.find ipis literal, not DNS-aware. It won't match an FQDN object that resolves to the target IP.deduponly compares addresses and services — not groups or tags.
Rule of thumb¶
refs used(where-used) is reliable for the rulebases psc scans — it is the right delete/rename pre-flight, and merge/rename repoint those sites or block.refs unusedis a candidate list, not a kill list, especially forsharedobjects. Before deleting, ask: could this live in a template, in network/VPN/management config, in a DAG via an externally registered IP, or on a firewall's local config? If plausibly yes, confirm in Panorama first. On a live source,--live-daganswers the registered-IP part of that question for you.- The safe operations are the ones psc can fully model and block when it
can't —
merge,rename,decommissionanddelete. Usedeleteto remove a verified candidate: it scrubs every reference psc scans and blocks on one it cannot rewrite. It never scrubs a reference psc does not scan, so the risk in this page is unchanged. The risky step is choosing what to delete from anunusedlist, because that is exactly where an unseen reference turns into an outage.
Tracking¶
refs unused prints a one-line caveat to stderr restating these blind spots
at the point of use (stdout stays pure machine output). The caveat text changes
when --live-dag resolved the registered-IP part, and it names the number of
firewalls that psc read. psc resolves registered-IP DAG membership on the live
path only. The offline path keeps the config-tag rule and the tags column. The
workbench (psc workbench) always builds a config-only graph, so its unused spoke
never reads live data. The remaining gaps — parsing template/network references,
modelling more object kinds — are tracked in the issue tracker. See
github.com/thomaschristory/panorama-super-cli/issues.