Skip to content

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:

  • shared and 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 entry row 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.)
  • dangling noise from built-ins. Only any, application-default, service-http, service-https, and service-dns are whitelisted. Other built-in services, regions, or EDL names in a rule will be reported dangling even though they are valid. Skim dangling output for these before treating it as an error list.
  • find ip is literal, not DNS-aware. It won't match an FQDN object that resolves to the target IP. dedup only compares addresses and services — not groups or tags.

Rule of thumb

  1. 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.
  2. refs unused is a candidate list, not a kill list, especially for shared objects. 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-dag answers the registered-IP part of that question for you.
  3. The safe operations are the ones psc can fully model and block when it can't — merge, rename, decommission and delete. Use delete to 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 an unused list, 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.