# pan-os-php Administrator & Developer Guide

This guide covers the parts of `PaloAltoNetworks/pan-os-php` that the **operator, architect, and contributor** care about: the bootstrap order of the framework, the class hierarchy that underpins every UTIL, the XML-API protocol details, the security posture of the API keystore, the deployment patterns that scale, and the extension points for writing new actions, filters, and parsers.

If you are looking for "how do I tag every unused address object" recipes, those live in the User Guide. This document is its companion.

---

## 1. Architecture Overview

`pan-os-php` is a single-process PHP framework with no daemon, no database, and no inter-process state. Each invocation is a complete pipeline:

1. The shell alias `pan-os-php` calls `php -r "require_once 'utils/pan-os-php.php';" $@`.
2. `utils/pan-os-php.php` `require_once`'s `lib/pan_php_framework.php`, which is the actual framework boot sequence — it fixes PHP runtime settings (`memory_limit=14512M`, `set_time_limit(0)`, `error_reporting(E_ALL)`, `gc_enable()`, `date_default_timezone_set('UTC')`), defines `STDIN`/`STDOUT`/`STDERR` for non-CLI contexts, registers a custom error handler that re-throws PHP notices and warnings as fatal `derr()` calls, registers a shutdown function that disables GC, and then `require_once`'s the entire framework (`phpseclib`, `git-php`, the `lib/misc-classes/`, `lib/helper-classes/`, `lib/container-classes/`, `lib/object-classes/`, `lib/device-and-system-classes/`, `lib/network-classes/`, and `lib/rule-classes/` trees) in dependency order.
3. `utils/pan-os-php.php` then `require_once`'s `utils/lib/UTIL.php`, which in turn pulls in every UTIL subclass (`RULEUTIL`, `STATSUTIL`, `SECURITYPROFILEUTIL`, `DEVICEUTIL`, `NETWORKUTIL`, `MERGER`, `RULEMERGER`, `KEYMANGER`, `PREDEFINED`, `UPLOAD`, `XMLISSUE`, `DIFF`, `OVERRIDEFINDER`, `APPIDENABLER`, `CONFIGSIZE`, `BPAGENERATOR`, `XMLOPJSON`, `REGISTERIP`, `USERIDMGR`, `SOFTWAREREMOVE`, `TRAFFICLOG`, `SYSTEMLOG`, `GARPSEND`, `CUSTOMREPORT`, `SOFTWARE_DOWNLOAD`, `SOFTWARE_PREPARATION__`, `LICENSE__`, `CONFIG_DOWNLOAD_ALL__`, `SPIFFY__`, `CONFIG_COMMIT__`, `MAXMIND__`, `PLAYBOOK__`, `UTIL_GET_ACTION_FILTER`, `IRONSKILLET_UPDATE__`, `PROTOCOLL_NUMBERS__`, `HTMLmerger__`, `TSF__`, `XPATH`, `SSH_CONNECTOR__`, `GCP`, `CONVERTER`, `PARSER`, `APPIDTOOLBOX`, `RULE_COMPARE`).
4. `PH::processCliArgs()` parses the `key=value` pairs from `$argv` into `PH::$args`.
5. The dispatcher inspects `PH::$args['type']`. If absent, it lists every supported type and exits. Otherwise it calls `PH::callPANOSPHP($type, $argv, $argc, $PHP_FILE)`, which `new`'s the appropriate UTIL subclass.
6. The UTIL constructor calls `utilStart()` → `utilInit()` (CLI args, plugin load, `arg_validation`, `listactions`/`listfilters`, `init_arguments`) → `utilActionFilter()` (`extracting_actions`, `createRQuery`, `load_config`, `location_filter`) → `location_filter_object()` → `time_to_process_objects()` (the actual per-object iteration that runs each `action`) → `GlobalFinishAction()` → `stats()` → `save_our_work(TRUE)` (writes `out=` file, pushes `outputformatset=`, and on API mode commits via `PanAPIConnector`).

### Confirmed components

```
                         +-----------------------------+
                         | Shell alias  pan-os-php     |
                         | (utils/alias.sh)            |
                         +--------------+--------------+
                                        |
                                        v
                         +-----------------------------+
                         | utils/pan-os-php.php        |
                         | (single CLI entry)          |
                         +--------------+--------------+
                                        |
                +-----------------------+-----------------------+
                |                                               |
                v                                               v
   +----------------------------+                +-----------------------------+
   | lib/pan_php_framework.php  |                | utils/lib/UTIL.php          |
   | (framework boot)           |                | (UTIL dispatcher)           |
   +-------------+--------------+                +-------------+---------------+
                 |                                             |
                 v                                             v
   +----------------------------+                +-----------------------------+
   | lib/misc-classes           |                | utils/lib/<TYPE>UTIL.php    |
   |   PH (CLI helper)          |                | RULEUTIL, STATSUTIL,        |
   |   DH (DOM helper)          |                | SECURITYPROFILEUTIL,        |
   |   PanAPIConnector          | <----.         | DEVICEUTIL, NETWORKUTIL,    |
   |   PanSaseAPIConnector      |       \        | MERGER, RULEMERGER, ...     |
   |   RQuery + RQueryContexts  |        \       +-------------+---------------+
   +-------------+--------------+         \                    |
                 |                         \                   v
                 v                          \   +-----------------------------+
   +----------------------------+            \  | utils/common/<X>CallContext |
   | lib/object-classes         |             - | + actions-<x>.php           |
   |   App, Address, Service,   |               | (action implementations)    |
   |   Tag, Schedule, Threat,   |               +-------------+---------------+
   |   *Profile, *Store         |                             |
   +-------------+--------------+                             |
                 |                                            |
                 v                                            v
   +----------------------------+              +-----------------------------+
   | lib/network-classes        |              | lib/rule-classes            |
   |   Zone, Interface*,        |              |   Rule (base),              |
   |   IKEGateway, IPsecTunnel, |              |   SecurityRule, NatRule,    |
   |   VirtualRouter, DHCP, ... |              |   DecryptionRule, PbfRule,  |
   +-------------+--------------+              |   QoSRule, DoSRule, ...     |
                 |                              +------------+----------------+
                 v                                           |
   +----------------------------+                            |
   | lib/device-and-system-     |                            |
   |   classes:  PANConf,       | <--------------------------+
   |   PanoramaConf, FawkesConf,|
   |   BuckbeakConf, DeviceGroup|
   |   Template, TemplateStack, |
   |   ManagedDevice, Snippet   |
   +-------------+--------------+
                 |
                 v
   +----------------------------+        +-----------------------------+
   | XML config (file or live)  | <----> | XML API endpoint            |
   | (DOM tree, in-memory)      |        | https://<fw>/api/?...       |
   +----------------------------+        +-----------------------------+

   +----------------------------+        +-----------------------------+
   | git-php/ (vendored)        |        | phpseclib (Math, Crypt, Net)|
   | for input file Git history |        | for SSH/RSA in SSH connector|
   +----------------------------+        +-----------------------------+

   +----------------------------+        +-----------------------------+
   | iron-skillet/panos_v*/     |        | migration/parser/<vendor>/  |
   | day-1 templates            |        | per-vendor PARSERs          |
   +----------------------------+        +-----------------------------+
```

### Component inventory

| Component | Path | Role |
|---|---|---|
| Framework boot | `lib/pan_php_framework.php` | Sets PHP runtime, registers handlers, `require_once`'s every framework class |
| Single CLI entry | `utils/pan-os-php.php` | Accepts `type=` and dispatches |
| UTIL base + dispatcher table | `utils/lib/UTIL.php`, `lib/misc-classes/PH.php` | `PH::$supportedUTILTypes`, `PH::callPANOSPHP()` |
| CLI helper (PH) | `lib/misc-classes/PH.php` | `PH::$args`, `PH::$argv`, `PH::frameworkVersion()`, `PH::print_stdout()`, `PH::processCliArgs()` |
| DOM helper (DH) | `lib/misc-classes/DH.php` | XML element manipulation, XPath generation |
| Error reporter | `lib/ErrorReporter.php` | `derr()` (fatal + backtrace), `mwarning()` (non-fatal) |
| RQuery filter engine | `lib/misc-classes/RQuery.php` + `RQueryContext` subclasses (Address, Application, Certificate, DHCP, Device, Interface, Routing, Rule, Schedule, SecurityProfile, SecurityProfileGroup, Service, StaticRoute, Tag, Threat, VirtualWire, Zone) | Parses `filter=` expressions into a tree, evaluates per-object |
| API connector (XML API) | `lib/misc-classes/PanAPIConnector.php` | `keygen` flow, `op`/`get`/`set`/`edit`/`delete`/`commit` calls, `~/.panconfkeystore` persistence |
| API connector (SASE) | `lib/misc-classes/PanSaseAPIConnector.php` | Strata Cloud Manager endpoints (partial) |
| Object classes | `lib/object-classes/` (49 files) | `App`, `AppCustom`, `AppFilter`, `AppGroup`, `AppStore`; `Address`, `AddressGroup`, `AddressStore`, `Region`; `Service`, `ServiceGroup`, `ServiceStore`; `Tag`, `TagStore`; `Schedule`, `ScheduleStore`; `Threat`, `ThreatVulnerability`, `ThreatSpyware`, `ThreatStore`; `*Profile` and `Profile` base + `*ProfileStore`; `ObjStore` base |
| Network classes | `lib/network-classes/` (37 files) | `Zone`, `ZoneStore`; `EthernetInterface` + `EthernetIfStore`; `AggregateEthernetInterface` + `AggregateEthernetIfStore`; `LoopbackInterface` + `LoopbackIfStore`; `TunnelInterface` + `TunnelIfStore`; `VlanInterface` + `VlanIfStore`; `VirtualWire` + `VirtualWireStore`; `VirtualRouter` + `VirtualRouterStore`; `StaticRoute`; `IKEGateway` + `IKEGatewayStore`; `IkeCryptoProfil`/`IPSecCryptoProfil` + their stores; `IPsecTunnel` + `IPsecTunnelStore`; `GreTunnel` + `GreTunnelStore`; `DHCP` + `DHCPStore`; `Certificate` + `CertificateStore`; `TmpInterface` + `TmpInterfaceStore`; `SharedGatewayStore`; `InterfaceContainer`, `VirtualRouterContainer`, `NetworkPropertiesContainer` |
| Rule classes | `lib/rule-classes/` (18 files) | `Rule` base; `RuleWithUserID`, `RuleWithGroupTag`, `RuleWithSchedule`; `SecurityRule`, `NatRule`, `DecryptionRule`, `AppOverrideRule`, `CaptivePortalRule`, `AuthenticationRule`, `PbfRule`, `QoSRule`, `DoSRule`, `TunnelInspectionRule`, `SDWanRule`, `NetworkPacketBrokerRule`, `DefaultSecurityRule`; `RuleStore` |
| Device/system classes | `lib/device-and-system-classes/` (16 files) | `PANConf` (firewall), `PanoramaConf`, `FawkesConf`, `BuckbeakConf`; `DeviceGroup`, `Template`, `TemplateStack`, `ManagedDevice`, `ManagedDeviceStore`, `LogCollectorGroup`; `Container`, `DeviceCloud`, `DeviceOnPrem`; `Snippet`, `Sub`; `VirtualSystem` |
| Container classes | `lib/container-classes/` (8 files) | `ObjRuleContainer` base; `AddressRuleContainer`, `AppRuleContainer`, `ServiceRuleContainer`, `TagRuleContainer`, `GroupTagRuleContainer`, `UrlCategoryRuleContainer`, `ZoneRuleContainer` |
| Helper classes | `lib/helper-classes/` (4 files) | `IP4Map`, `cidr`, `ServiceDstPortMapping`, `ServiceSrcPortMapping` |
| UTIL implementations | `utils/lib/` (47 files) | One PHP class per UTIL type — see Section 8 for the full table |
| Action / CallContext | `utils/common/` (38 files) | `*CallContext` per type + `actions-*.php` files defining action handlers |
| Vendor migration | `migration/parser/` (per-vendor folders) | `CONVERTER` → `PARSER` → `<vendor>` parser hierarchy |
| AppID Toolbox | `appid-toolbox/` | Self-contained sub-tool; companion `type=appid-toolbox` UTIL |
| IronSkillet bundle | `iron-skillet/panos_v8.1`, `panos_v9.1`, `panos_v10.0`, `panos_v10.1`, `panos_v10.2`, `panos_v11.0` | Vendored PAN-OS day-1 hardening templates |
| git-php | `git-php/src/` | Bundled fork of `czproject/git-php` for the `git=` argument |
| API listener | `utils/api/v1/tool.php` | Optional HTTP front-end exposing UTILs as REST routes |
| Web UI scaffolding | `utils/develop/ui/`, `utils/common/html/` | Experimental browser UI |
| Bash autocompletion | `utils/bash_autocompletion/pan-os-php.sh` (and per-tool `pa_*.sh`) | Tab completion for the dispatcher and the legacy aliases |

### Technology stack

| Layer | Technology |
|---|---|
| Language | PHP 7.4+ |
| Required PHP extensions | `json`, `curl`, `dom`, `mbstring`, `bcmath`, `mysqli`, `yaml` |
| Crypto | `phpseclib` (vendored) — `Math/BigInteger`, `Crypt/RSA`, `Net/SSH2` |
| Git library | `czproject/git-php` (vendored under `git-php/`) — used when `git=` is passed |
| Auth flow | XML API (`/api/?type=keygen` then `?type=config|op&action=...&key=...`) |
| Output formats | Plain text, PAN-OS `set` commands, JSON (BETA via `shadow-json`), XLSX (via `python3-xlsxwriter`), HTML (via `HTMLmerger__`) |
| Optional helpers | Python 3 with `pandas`, `xlsxwriter`, `netaddr`, `requests`; `jq` |
| License | ISC for the project; bundled `git-php` is BSD-3-Clause; PAN-OS schema files (`lib/resources/`) are © Palo Alto Networks (fair-use copy) |

### Data flow — single offline UTIL run

```
$argv  ─────►  PH::processCliArgs() ─────►  PH::$args
                                                  │
                                                  ▼
                                       PH::callPANOSPHP(type)
                                                  │
                                                  ▼
                                       new <Type>UTIL(...)
                                                  │
                                                  ▼
                                       UTIL::utilInit()
                                            ├── arg_validation()
                                            ├── listactions/listfilters
                                            └── init_arguments()
                                                  │
                                                  ▼
                                       UTIL::utilActionFilter()
                                            ├── extracting_actions()
                                            ├── createRQuery()
                                            ├── load_config()  ──►  XML on disk → DOMDocument → PANConf/PanoramaConf
                                            └── location_filter()
                                                  │
                                                  ▼
                                       location_filter_object()  ──►  scope to vsys/DG/template
                                                  │
                                                  ▼
                                       time_to_process_objects()
                                                       │
                                                       ▼
                                            for each object:
                                                  RQuery::test(object)
                                                        │ true
                                                        ▼
                                                  for each action:
                                                        apply()
                                                  │
                                                  ▼
                                       GlobalFinishAction()
                                                  │
                                                  ▼
                                       save_our_work(TRUE)
                                            ├── DOMDocument → out= file
                                            └── outputformatset=  → set commands file
```

### Data flow — single API UTIL run

Identical to the offline flow, except:

- `load_config()` calls `PanAPIConnector::getRunningConfig()` (and `getCandidateConfig()` for some operations) over HTTPS, returning XML.
- `save_our_work()` invokes `PanAPIConnector::edit()`, `PanAPIConnector::set()`, or `PanAPIConnector::delete()` per modified node.
- If `outputformatset=` is also passed, the `set` commands are written **alongside** the API push.
- The first API call requires authentication; the connector reads `~/.panconfkeystore` for cached `serial@host` keys, otherwise prompts for username/password and persists the resulting key (unless `shadow-apikeynosave`).

> [!NOTE]
> The framework intentionally re-throws every PHP `E_NOTICE`/`E_WARNING` as a fatal via `myErrorHandler` → `derr()`. This is unconventional but correct for the use case: a config-mutation tool must never silently continue on a typo or NULL-deref. Operators reading the codebase should not "fix" warnings in user code by silencing them; the framework will end the run if the warning is a real bug.

---

## 2. System Requirements

### Runtime

| Requirement | Minimum | Recommended |
|---|---|---|
| PHP CLI | 7.4 | 8.1 |
| PHP `memory_limit` | 14 GB (set inside framework) | leave default |
| OS | Linux (Ubuntu 20.04+, CentOS 7), macOS, Windows via Docker | Ubuntu 22.04 |
| Bash | 5.0 (for autocompletion) | — |
| Disk | repo ~150 MB; large XML configs and Excel exports add MBs–GBs | 5 GB free |
| Network | HTTPS to firewall(s) and Panorama for API mode; HTTPS to GitHub for `update_pan-os-php.sh`; optional outbound to MaxMind, Iron-Skillet repo | — |

### PHP extensions

The `composer.json` `require` block lists the hard requirements:

```json
{
  "require": {
    "php": ">=7.4.0",
    "ext-json": "*",
    "ext-curl": "*",
    "ext-dom": "*",
    "ext-mbstring": "*",
    "ext-bcmath": "*",
    "ext-mysqli": "*",
    "ext-yaml": "*"
  }
}
```

Note that `ext-mysqli` is required even for offline-only use because the experimental `utils/develop/ui/` and `migration/parser/lib/SHAREDNEW.php` features assume it is present at autoload time; the framework will error if the extension is missing.

### Optional dependencies

| Optional | Reason |
|---|---|
| `python3` + `pandas` + `xlsxwriter` + `netaddr` + `requests` | XLSX export via `actions=exportToExcel:...`, IP arithmetic helpers |
| `jq` | Bash-side parsing of `shadow-json` output |
| Docker | Run via the `swaschkut/pan-os-php:latest` image instead of installing PHP locally |

### Network / firewall

- Outbound HTTPS to each managed device or Panorama (port 443).
- The XML API must be enabled on the device (`Device > Setup > Management > XML API` or its Panorama equivalent).
- Time-skew matters: API auth tokens are time-bound — keep the jump host's NTP in sync with the firewall.

---

## 3. Configuration Reference

`pan-os-php` itself reads no configuration files. Configuration is supplied per-invocation via CLI arguments and resolved at runtime by `PH::processCliArgs()` and the per-UTIL `supportedArguments` tables.

### Configuration files (read by the framework)

| File | Format | Location | Purpose |
|---|---|---|---|
| `~/.panconfkeystore` | proprietary text | user home | Cached API keys, written by `PanAPIConnector` after first `keygen` call. Disable via `shadow-apikeynosave`. |
| `utils/lib/util_action_filter.json` | JSON | repo | Generated by `type=util_get-action-filter`; used by `utils/api/v1/tool.php` and tooling that needs the action/filter catalogue. |
| `iron-skillet/panos_v*/templates/panorama/snippets/*.xml` | PAN-OS template XML | repo | Bundled IronSkillet day-1 hardening snippets, one folder per major PAN-OS version. |
| `lib/resources/schema/*.xml` | PAN-OS schema XML | repo | Schema files copied from the official Palo Alto product (`https://[MGMT-IP]/php/utils/schema.php`). Used for validation. |
| `composer.json` | JSON | repo root | Declares PHP and extension requirements. |
| `git-php/composer.json` | JSON | submodule root | Declares the vendored `git-php` library's metadata. |
| `docker-compose.yml`, `docker-compose2.yml` | YAML | repo root | Optional Docker-based development environment (PHP-Apache + MySQL + phpMyAdmin); used by the experimental `utils/develop/ui/`. |

### Environment variables

| Variable | Read by | Purpose | Default |
|---|---|---|---|
| `HOME` | `utils/develop/grp-static-to-dynamic.php` | Locate user home for the API keystore | system value |
| `USERPROFILE` | `lib/misc-classes/PanAPIConnector.php` | Windows equivalent of HOME | system value |
| `HOMEDRIVE` | `lib/misc-classes/PanAPIConnector.php` | Windows path component | system value |
| `HOMEPATH` | `lib/misc-classes/PanAPIConnector.php` | Windows path component | system value |
| `TERM` | `lib/misc-classes/PH.php` | Decide whether to emit ANSI bold escape codes | system value |

There are no `pan-os-php`-specific environment variables. The framework deliberately does not hide config in the environment — it must all be in the CLI invocation, so playbooks are self-describing.

### Universal CLI flags (all UTILs)

These are wired into `UTIL::supportedArguments()` and apply to nearly every type. `Type` describes the value's shape; `Default` is the value used when the flag is absent.

| Flag | Type | Default | Description |
|---|---|---|---|
| `in=` | filename or `api://...` | — (required for most UTILs) | Source: file or live API endpoint |
| `out=` | filename | — | Output file (offline mode); ignored in API mode |
| `location=` | `shared`, `any`, vsys name, DG name, `<DG>:includechilddgs`, `<DG>:excludeMaindg` | `shared` (Panorama), `vsys1` (firewall) | Scope of the operation |
| `template=` | template name | `any` | Limit to a specific Panorama template |
| `filter=` | RQuery expression | none | Per-object filter (see Section 8 for the engine) |
| `actions=` | `action[:arg1[,arg2]][/action2[:...]]` | none | Slash-separated list of actions to apply |
| `listactions` | flag | off | Print every supported action and exit |
| `listfilters` | flag | off | Print every supported filter operator and exit |
| `stats` | flag | off | Print object/rule counts after run |
| `actions=display` | special | — | Print matched objects without changing them |
| `loadplugin=` | filename | — | Load a custom action/filter plugin |
| `apitimeout=` | seconds | 60 | API timeout |
| `auditcomment=` | string | `PAN-OS-PHP $actions $time` | Audit comment for API commits |
| `outputformatset[=file]` | flag or filename | off / no file | Emit PAN-OS `set` commands |
| `projectfolder=` | dir | — | Output directory root |
| `loadpanoramapushedconfig` | flag | off | Pull Panorama-pushed objects when loading from a managed firewall |
| `cycleconnectedfirewalls` | flag | off | Iterate Panorama-managed devices |
| `git` | flag | off | Wrap input file in a Git repo + auto-commit |
| `expedition` | flag | off | Internal marker that the run is from the Expedition tool |
| `debugapi` | flag | off | Print every API call |
| `debugloadtime` | flag | off | Print phase timings |
| `help` | flag | off | Print usage and exit |

### `shadow-` flags (advanced behaviours)

| Flag | Default | Description |
|---|---|---|
| `shadow-disableoutputformatting` | off | Skip DOMDocument output normalisation |
| `shadow-enablexmlduplicatesdeletion` | off | When duplicate object names exist, keep one and drop the rest |
| `shadow-ignoreinvalidaddressobjects` | off | Tolerate address objects with no value/type |
| `shadow-apikeyhidden` | off | POST API key (PAN-OS 9.0+) instead of putting it in URL |
| `shadow-apikeynohidden` | off | Force API key in URL (PAN-OS < 9.0) |
| `shadow-apikeynosave` | off | Do not persist API key to `~/.panconfkeystore` |
| `shadow-displaycurlrequest` | off | Print raw curl command for every API call |
| `shadow-reducexml` | off | Strip whitespace from output XML |
| `shadow-json` | off | BETA: emit stdout as a single JSON document |
| `shadow-nojson` | off | Force human text even if `shadow-json` was set |
| `shadow-displayxmlnode` | off | Include matched XML elements in `actions=display` output |
| `shadow-saseapiqa` | off | Use SASE QA endpoints |

> [!IMPORTANT]
> Treat `~/.panconfkeystore` as a secret file. It contains long-lived PAN-OS API keys, each of which carries the full RBAC of the user that ran `keygen`. Keep its mode at `0600`, never commit it, and rotate keys (`type=key-manager actions=delete:host`) when staff leave or when a host is decommissioned. If you cannot accept the cache, run every API invocation with `shadow-apikeynosave` and pipe credentials in interactively — but expect each run to require fresh auth.

### Per-UTIL specifics

Each UTIL extends `UTIL::supportedArguments()` with its own flags. To enumerate them:

```bash
pan-os-php type=<X> help
```

A few of the high-impact ones:

| Type | Notable extra arguments |
|---|---|
| `rule-merger` | `panoramaPreRules`, `panoramaPostRules`, `mergeAdjacentOnly`, `dupAlgorithm` |
| `rule` | `dryrun`, plus all rule-specific filter operators |
| `address-merger` | `mergeByValue`, `mergeByName`, `dupAlgorithm` |
| `bpa-generator` | `cycleconnectedfirewalls` (mandatory for Panorama-driven runs) |
| `traffic-log`, `system-log` | Log filter/time-window arguments |
| `software-download` | PAN-OS version selector |
| `userid-mgr` | `record=user,IP[,VSYS]` |
| `register-ip-mgr` | `actions=register/unregister:ip,tag` |
| `xml-issue` | `actions=display`, `actions=fix` |
| `playbook` | `playbook=path/to/playbook.json` |
| `vendor-migration` | `vendor=ciscoasa\|netscreen\|sonicwall\|sophos\|ciscoswitch\|ciscoisr\|fortinet\|srx\|cp-r80\|cp\|cp-beta\|huawei\|stonesoft\|sidewinder\|pfsense` |

---

## 4. Deployment & Scaling

`pan-os-php` is a per-invocation CLI; "deployment" is shaping how and where it is invoked.

### Deployment patterns

| Pattern | Use when |
|---|---|
| **Single jump host** (Ubuntu 22.04 with the install script) | Small team, ad-hoc runs, full Bash/Tab completion, easy SSH access |
| **Per-engineer laptop** (macOS install script, or Docker) | Lots of one-off exploration, no shared state needed |
| **Docker per run** (`swaschkut/pan-os-php:latest`) | CI pipelines, ephemeral compute, hosts you do not own |
| **Managed centrally** (Ansible / Salt installs the repo into `/tools/pan-os-php`) | Compliance-controlled environments where every change must be source-tracked |
| **REST front (`utils/api/v1/tool.php`) behind nginx + PHP-FPM** | When other systems need to call UTIL operations as HTTP — exposes `/utils/api/v1/tool.php/<type>` |

### Scaling considerations

- **Memory.** Loading a multi-thousand-rule, multi-hundred-DG Panorama XML routinely consumes 10–25 GB. The framework hard-codes `memory_limit=14512M` (14 GB) — for the largest customers you may need to override. There is no streaming parser; every config is held in `DOMDocument` at once.
- **CPU.** Mostly DOM walking, regex, and string compares. Single-threaded. Multi-runs in parallel are fine if each has its own memory headroom.
- **API rate.** PAN-OS XML API does not impose a rate limit on small calls but commit operations serialise at the device. Avoid calling `config-commit` from many concurrent runs against the same device. The connector has retry logic for transient HTTPS failures.
- **Network.** API mode pulls the full running config on every invocation (or running + candidate). For very large Panoramas a single `getRunningConfig` can be 50–200 MB.

### Update strategy

- **Pin via Git.** `update_pan-os-php.sh` pulls `origin main` on its host. For change control, fork the repo and pin a release tag; only run `update_pan-os-php.sh` after CI validation against your config corpus.
- **Pin the Docker image by digest.** Use `swaschkut/pan-os-php@sha256:...` rather than `:latest` in CI.
- **Watch the upstream `CHANGELOG.txt`.** Action names occasionally change (`replaceWithObject` ↔ `replace-With-Object` between releases, for example).

### CI / automation patterns

Read `pan-os-php type=<X> listactions` and `listfilters` for every type at CI start and store the JSON output as build metadata. This catches the "the action you depend on no longer exists" failure mode early.

```bash
docker run --rm -v $PWD:/share swaschkut/pan-os-php:latest \
  type=util_get-action-filter projectfolder=/share/build-metadata
```

For idempotent operations, run with `actions=display` first and assert on the count; only proceed to the destructive action if the count matches expectations.

---

## 5. Operations

### Day-to-day operator runbooks

#### Rotate API key cache

```bash
pan-os-php type=key-manager actions=display
pan-os-php type=key-manager actions=delete:<hostOrSerial>
```

`type=key-manager` is your direct interface to `~/.panconfkeystore`. The next API call to that host will re-prompt for credentials.

#### Validate a config XML before processing

```bash
pan-os-php type=xml-issue in=panorama.xml actions=display
```

Lists empty entries, missing required attributes, and duplicate names. `actions=fix` repairs many of them in place.

#### Snapshot a live device to disk

```bash
pan-os-php type=stats in=api://10.0.0.5 out=fw-$(date -u +%Y%m%dT%H%M%SZ).xml
```

`stats` is read-only; the side effect is `out=` writes the running config locally. This is the safest base for any subsequent destructive offline work.

#### Generate a complete change preview

```bash
pan-os-php type=rule \
  in=api://10.0.0.5 \
  'filter=(action eq deny)' \
  actions=tag-Add:Reviewed-2026Q2 \
  outputformatset=preview-2026Q2.set \
  shadow-json
```

Combines a `set`-format file (for human review) with JSON output (for tool review).

#### Bulk download every running and candidate config from a Panorama

```bash
pan-os-php type=config-download-all in=api://panorama.local projectfolder=panorama-snapshot/
```

Useful before a major upgrade or as a pre-change snapshot.

#### Trigger a commit via API

```bash
pan-os-php type=config-commit in=api://panorama.local
```

`type=config-commit` issues a commit (`/api/?type=commit&cmd=<commit/>`) and waits for completion. Combine with `cycleconnectedfirewalls` to push to managed devices.

> [!NOTE]
> `config-commit` honours `apitimeout=`. Panoramas with hundreds of devices can take 20+ minutes to fully push; set `apitimeout=1800` for those runs. The connector polls the job status until it completes or the timeout expires.

> [!TIP]
> Wrap the whole change in `git=` to get an automatic before/after commit on the input file. The dispatcher initialises a Git repo around the input filename, commits the original, runs the actions, and commits the result with the action and timestamp in the message.

### Logging

The framework writes to STDOUT (human report) and STDERR (errors and warnings only). There is no logfile written by default. To capture a full run:

```bash
pan-os-php type=rule in=fw.xml ... 2>&1 | tee run.log
```

When `shadow-json` is set, stdout is a JSON document. The `log` field contains the human-text equivalent for archival.

The `utils/lib/logWriter.php` class is used internally by some long-running UTILs (`appid-toolbox`, `config-download-all`) to write structured per-step logs into the project folder.

### Health checks

There is no daemon to check, so health is "does a known-good run still work":

```bash
pan-os-php version
```

```text
 - PAN-OS-PHP version: 2.1.20 [UNIX]
 - /tools/pan-os-php/utils
 - PHP version: 8.1.2
```

A non-zero exit, an empty version line, or a `*** ERROR ***` indicates a broken install. For an end-to-end smoke test, use a tiny saved XML and a no-op:

```bash
pan-os-php type=stats in=tests/input/firewall-test.xml
```

### Backup & restore

`pan-os-php` itself stores no state apart from `~/.panconfkeystore`. To back up:

- Source tree: standard Git pull (or repo mirror).
- Keystore: `cp ~/.panconfkeystore ~/.panconfkeystore.bak` (treat as a secret).
- Generated artefacts (set files, snapshots, Excel exports): whatever lives under `projectfolder=` is yours to back up.

> [!NOTE]
> `outputformatset=`, `out=`, and `projectfolder=` outputs are deterministic snapshots of a moment in time. There is no built-in "rollback to previous run" — keep prior snapshots if you need that, or use `git=` to keep history alongside the input file.

---

## 6. Performance

### Memory

`memory_limit` is set to 14 GB by `lib/pan_php_framework.php`. For very large Panoramas this is a soft floor; if you see `Allowed memory size of N bytes exhausted`, increase via `php -d memory_limit=32G ...` at the shell, or edit the framework. There is no streaming parser; the full DOM is in memory. `memory_and_gc('end')` is called by some UTILs and example scripts to display before/after consumption.

### CPU profile

Most invocations are CPU-bound on:

- DOM traversal (`DH::dom_to_xml`, `DH::elementToPanXPath`, the per-rule iteration in `RuleStore`)
- Regex evaluation when the filter uses `regex` operators
- String compares and array reflows during merger runs (`MERGER`, `RULEMERGER`)

The `RULEMERGER` algorithm is `O(n^2)` over the rule count for adjacency-aware merges. On 50k-rule Panoramas, expect tens of minutes. `mergeAdjacentOnly` reduces this to nearly linear at the cost of merging only adjacent shadow rules.

### I/O

- Offline mode: input parsed once via `DOMDocument::load`, modifications stay in memory, single write at end.
- API mode: one HTTP request per push (`PanAPIConnector::edit`, `PanAPIConnector::set`, etc.) — for a UTIL that mutates 1000 objects, expect 1000+ requests. The connector reuses the underlying TLS connection where possible.
- `outputformatset=` writes line-by-line as actions execute, so the file is complete even if the process is killed mid-run.

### Tuning knobs

| Flag | Effect |
|---|---|
| `apitimeout=N` | Per-call API timeout, default 60 — raise for slow Panoramas |
| `shadow-reducexml` | Strip whitespace from output XML (smaller files, slightly faster I/O) |
| `shadow-disableoutputformatting` | Skip DOM normalisation on output, save CPU |
| Run a `bpa-generator` outside business hours | The combined `cycleconnectedfirewalls` walk hammers the API |
| Increase PHP `memory_limit` for huge Panoramas | `php -d memory_limit=64G` |
| Use Docker to keep PHP/extension versions stable | Avoids "works on Ubuntu 22 but not 20" drift |

### Benchmarks (qualitative)

- `type=address actions=display filter=(object is.unused)` on a 30k-object Panorama: ~3–8 minutes wall-clock, ~12 GB memory.
- `type=rule-merger actions=mergeAdjacent` on a 5k-rule Panorama with `panoramaPreRules`: ~5–15 minutes wall-clock, ~6 GB memory.
- `type=stats in=api://...` against a Panorama with 30 managed firewalls: 2–5 minutes for the running config download alone.

---

## 7. Security

### Threat model

`pan-os-php` is a privileged operations tool: it has read/write access to firewall and Panorama configurations, often via long-lived API keys cached on the operator's host. Treat any host that runs it as a tier-zero administrative system.

### Secrets handling

| Secret | Where it lives | Lifetime | How to revoke |
|---|---|---|---|
| PAN-OS API key | `~/.panconfkeystore` (user home) | Until firewall admin revokes it or `pan-os-php type=key-manager actions=delete:host` is run | Delete via `key-manager` or rotate at the device |
| Username/password (during interactive auth) | Process memory, never written | Single invocation | Process exit |
| LDAP credentials (if `user-check-ldap` is used) | Read from the loaded config; not persisted by `pan-os-php` | — | Rotate at the LDAP server / firewall |

`PanAPIConnector` writes the keystore at file mode `0600` if the umask is reasonable; verify after first run:

```bash
ls -l ~/.panconfkeystore
```

```text
-rw------- 1 alice alice 384 Apr 28 12:00 /home/alice/.panconfkeystore
```

> [!CAUTION]
> A leaked `~/.panconfkeystore` is equivalent to a leaked credential set for every device it covers. Never copy it to a shared host, never include it in tarballs or backups that leave the operator's account, and rotate any key whose host has been suspected of compromise. Use `shadow-apikeynosave` on shared workstations to avoid persisting the key at all.

### Exported configuration files are sensitive

`pan-os-php` works on exported configuration XML (`in=config.xml`). An export, whether copied off a firewall, saved
with `show config` or taken as a Panorama named snapshot, is a full copy of the device configuration: it can include
administrator password hashes, certificates and keys, and the credentials of downstream services (LDAP, RADIUS,
SNMP, syslog). Handle it as credential material.

| Action | Cadence | Notes |
|---|---|---|
| Keep exports encrypted at rest and limited to the operator role | Continuous | Retain them only as long as change management requires |
| Rotate the API keys in `~/.panconfkeystore` | On a defined cadence, and after an export has been handled outside the operator's host | `pan-os-php type=key-manager actions=delete:host`, then a fresh run that re-prompts for credentials |
| Treat a Panorama configuration backup like the firewall configs it manages | Continuous | It aggregates the configuration of every managed device |
| Remove sensitive values before sharing an export (vendor, support case, migration) | Per share | Follow your data-handling policy |

For how PAN-OS protects stored secrets (including the master key), use the official PAN-OS administrator documentation as the reference, not this guide.

### Network security

- **TLS verification.** `PanAPIConnector` defaults to verifying the device's TLS certificate. The bundled install and update scripts set their own Git options, which are separate from API TLS; review them before running them on your hosts. Do not disable TLS verification for API mode without a deliberate decision.
- **API mode endpoints.** Always direct API connections at the management plane IP. The framework supports `api://[email removed]` to tunnel through Panorama, which is the preferred path for firewalls without direct management-plane reachability.
- **Inbound REST mode.** If you deploy `utils/api/v1/tool.php` behind nginx, put it on a dedicated VLAN with mTLS or an authenticating reverse proxy in front. The endpoint executes UTILs server-side with whatever credentials the PHP process has — there is no built-in auth on the tool endpoint.

### Input validation

- The framework refuses to load on PHP < 5.5 (`lib/pan_php_framework.php:22`) and historically also imposed an upper bound (`<=8.1`) — the upper-bound check is currently commented out in `pan_php_framework.php`.
- `myErrorHandler` (`lib/pan_php_framework.php:275`) re-raises any `E_USER_NOTICE`, `E_USER_WARNING`, `E_NOTICE`, or `E_WARNING` as a fatal `derr()`; this prevents subtle PHP coercion bugs from corrupting a config silently.
- `xml-issue` runs a structural sanity pass; treat XMLs from untrusted sources with `xml-issue` first.

### XSS / SQL / deserialization

- No HTML output is generated by the CLI itself. The optional `utils/develop/ui/` web UI is **explicitly experimental** and ships with sample test accounts (see `utils/develop/ui/test/README.md`). Do not deploy that UI without replacing those values and disabling sample DBs.
- `mysqli` is loaded at framework boot to support `utils/develop/ui/` and `migration/parser/lib/SHAREDNEW.php`. The CLI flow does not connect to any database.
- No `unserialize()` of attacker-controlled data; XML is parsed via `DOMDocument` only.

### Audit trail

- `auditcomment=` sets the audit comment Panorama records for every API change. Default is `PAN-OS-PHP $actions $time`. Set this to a JIRA / change ticket reference for compliance environments.
- `git=` produces commits in the wrapping Git repo with the action + timestamp in the message.
- Combine with `outputformatset=` files retained alongside Git history for a tamper-evident change log.

> [!WARNING]
> Several known issues affect destructive operations. Always preview with `actions=display` and `outputformatset=` before running mergers (#793, #798), bulk renames (#764), or zone manipulations (#786). When in doubt, snapshot first (`type=stats out=snap.xml`) and iterate offline.

### Hardening checklist

- [ ] `~/.panconfkeystore` mode `0600`, owned by the operator user
- [ ] `shadow-apikeynosave` on shared / multi-user hosts
- [ ] `apitimeout=` set sensibly to detect dead devices instead of waiting forever
- [ ] `auditcomment=` integrated into the operator's playbook (mapped to ticket IDs)
- [ ] Source tree pinned to a known commit / tag rather than tracking `main`
- [ ] No public network exposure of `utils/api/v1/tool.php` without authenticating reverse proxy
- [ ] `utils/develop/ui/` disabled in production installs
- [ ] Operator hosts on the same NTP source as the firewalls (API tokens are time-bound)
- [ ] Config exports stored under restricted ACLs (treat as secret material, not as backups)
- [ ] Device API keys rotated on a defined post-export cadence (`pan-os-php type=key-manager actions=delete:host` + fresh auth)
- [ ] Downstream credentials (LDAP / RADIUS / SNMP / syslog) rotated when reachable from any config export

---

## 8. Developer Internals

### Repository structure

```
pan-os-php/
├── composer.json                # PHP + extension requirements
├── docker/                      # Dockerfile-main_amd, Dockerfile-API_amd, etc.
├── docker-compose.yml           # Production image compose
├── docker-compose2.yml          # PHP-Apache + MySQL + phpMyAdmin (dev)
├── create_all_docker_container.sh
├── create_docker_manifest.sh
├── install_script_centos7.sh
├── install_script_centos7_old.sh
├── install_script_ubuntu20.sh
├── install_script_ubuntu22.sh
├── mac_set_path_variable.sh
├── remove_path_variable.sh
├── set_alias_usage.sh
├── update_pan-os-php.sh
├── lib/
│   ├── pan_php_framework.php    # Framework boot
│   ├── ErrorReporter.php        # derr() static
│   ├── autoloader.php           # myAutoloader (exists but unused)
│   ├── classes/taskManagement/  # Task reporters (FileTaskReporter, ExpeditionTaskReporter)
│   ├── container-classes/       # ObjRuleContainer + 7 subclasses
│   ├── object-classes/          # 49 object classes (Address, Service, App, *Profile, *Store)
│   ├── network-classes/         # 37 network classes (Zone, *Interface, *IfStore, IKEGateway, ...)
│   ├── rule-classes/            # 18 rule classes (Rule + Security/NAT/Decryption/...)
│   ├── device-and-system-classes/  # 16 (PANConf, PanoramaConf, FawkesConf, BuckbeakConf, DeviceGroup, Template, Snippet, ...)
│   ├── helper-classes/          # IP4Map, cidr, ServiceDstPortMapping, ServiceSrcPortMapping
│   ├── misc-classes/            # PH, DH, RQuery + RQueryContexts, PanAPIConnector, PanSaseAPIConnector, CsvParser
│   └── resources/               # PAN-OS schema XML, IP protocol map, GeoIP data, License notices
├── utils/
│   ├── pan-os-php.php           # Single CLI entry
│   ├── alias.sh                 # Shell aliases
│   ├── lib/                     # 47 UTIL implementations
│   ├── common/                  # 38 CallContext + actions-*.php files
│   ├── api/v1/                  # Optional REST front-end (tool.php)
│   ├── bash_autocompletion/     # pan-os-php.sh + per-tool autocomplete
│   ├── develop/                 # In-progress UTILs and the experimental Web UI
│   ├── doc/                     # Markdown documentation drafts
│   └── common/html/             # JS/HTML for the experimental UI
├── examples/                    # 13 standalone PHP examples + sample-configs/
├── git-php/                     # Vendored fork of czproject/git-php
├── iron-skillet/                # Bundled Palo Alto IronSkillet templates per PAN-OS version
├── migration/                   # Vendor migration framework + per-vendor parsers
├── appid-toolbox/               # Self-contained App-ID activation sub-tool
├── projects/                    # Empty by default; output of `projectfolder=` runs lands here
├── log/                         # Runtime log root
├── tests/                       # Tests + sample configs
├── doc/                         # Auto-generated phpdoc HTML
├── README.md / READMEcustomScripting.md / READMEdocker.md / READMEutil.md
├── CHANGELOG.txt
└── SUPPORT.md
```

### Architecture deep dive

#### The PH and DH static helpers

`PH` (`lib/misc-classes/PH.php`) is the global CLI helper — it owns `PH::$args`, `PH::$argv`, `PH::$basedir`, `PH::$useExceptions`, `PH::$shadow_json`, and the JSON output buffers `PH::$JSON_OUT` / `PH::$JSON_OUTlog` / `PH::$JSON_TMP`. Static methods include `PH::frameworkVersion()` (composes `library_version_major.sub.bugfix`, currently `2.1.20`), `PH::frameworkInstalledOS()` (returns `WIN` or `UNIX`), `PH::print_stdout()` (the unified print wrapper that respects `shadow-json`), `PH::processCliArgs()` (parses `argv` into `args`), `PH::$supportedUTILTypes` (the master list), and `PH::callPANOSPHP($type, ...)` (the type → class dispatch table).

`DH` (`lib/misc-classes/DH.php`) is the DOM helper — `DH::elementToPanXPath()` produces the canonical PAN-OS XPath for a given element, `DH::dom_to_xml()` serialises with depth control, `DH::firstChildElement()` / `DH::nextChildElement()` walk past whitespace text nodes.

Together these are imported by every class file via `require_once`'s in `lib/pan_php_framework.php`; you do not need to autoload them yourself.

#### The RQuery filter engine

`lib/misc-classes/RQuery.php` is the parser/evaluator for `filter=` expressions. It tokenises the expression, builds an AST of `RQueryNode`s, and walks the tree per object during `time_to_process_objects()`. Each leaf node references a `RQueryContext` subclass (`AddressRQueryContext`, `RuleRQueryContext`, `ServiceRQueryContext`, `ZoneRQueryContext`, `InterfaceRQueryContext`, etc.) that knows the per-type field/operator catalogue.

To add a new filter operator for, say, `address`:

1. Open `lib/misc-classes/AddressRQueryContext.php`.
2. Add an entry to `RQuery::$defaultFilters['address']` (kept in `RQuery.php` initialisation): `['name' => 'is.in.region', 'operators' => ['Asia' => [...], 'Europe' => [...], ...]]`.
3. Implement the evaluator function the operator points to.

`listfilters` will pick the new operator up automatically because it iterates `RQuery::$defaultFilters[<type>]`.

#### The CallContext + Action pattern

Every UTIL type that mutates objects has an associated `*CallContext` class (`AddressCallContext`, `RuleCallContext`, `ServiceCallContext`, `ZoneCallContext`, `TagCallContext`, `ScheduleCallContext`, `SecurityProfileCallContext`, `SecurityProfileGroupCallContext`, `ApplicationCallContext`, `ThreatCallContext`, `DeviceCallContext`, `VsysCallContext`, `VirtualWireCallContext`, `RoutingCallContext`, `InterfaceCallContext`, `DHCPCallContext`, `CertificateCallContext`, `StaticRouteCallContext`). The CallContext holds the per-action invocation state (the matched object, the supplied arguments, the parent UTIL).

Each context has a `$supportedActions` static array. Entries look like:

```php
array(
    'name' => 'tag-Add',
    'MainFunction' => function (RuleCallContext $context) { ... },
    'args' => array('tagName' => array('type' => 'string', 'default' => '*nodefault*')),
)
```

`utils/common/actions-rule.php`, `actions-address.php`, `actions-service.php`, `actions-zone.php`, `actions-routing.php`, `actions-securityprofile.php`, `actions-securityprofilegroup.php`, `actions-tag.php`, `actions-threat.php`, `actions-schedule.php`, `actions-device.php`, etc., are the files that add entries to those arrays.

To add a new action:

1. Open the relevant `actions-*.php` file under `utils/common/`.
2. Append a new entry to `XCallContext::$supportedActions` with `name`, `MainFunction`, `GlobalInitFunction` (optional), `GlobalFinishFunction` (optional), and `args` (each with `type`, `default`, and optional `choices`).
3. The `MainFunction` receives a `*CallContext $context` and reads `$context->object` (the matched object) and `$context->arguments` (resolved CLI args).
4. Inside, mutate `$context->object` via the public methods on the object class. Avoid touching the underlying DOM element directly except via `DH::` helpers.
5. `pan-os-php type=<X> listactions` picks the new entry up automatically.

#### Plugin loading via `loadplugin=`

`UTIL::loadplugin()` accepts a path and `require_once`'s the file early in `utilInit`. The plugin is plain PHP; it can append entries to any `*CallContext::$supportedActions` array or to `RQuery::$defaultFilters`. See `examples/example_plugin_rule_display_source_destination.php` for the canonical example.

#### Object class invariants

- Every object class extends `ReferenceableObject` (trait) so that uses are tracked; bidirectional reference graph is essential for `addObjectWhereUsed`, `removeWhereUsed`, `replaceWithObject`, etc.
- Every object lives inside a `*Store` (`AddressStore`, `ServiceStore`, `TagStore`, `RuleStore`, `ZoneStore`, `IKEGatewayStore`, ...) which extends `ObjStore`.
- `findOrCreate` is the canonical write path: never `new Address()` directly inside an action — go through the store.
- `name()`, `setName()`, `xmlroot`, `owner` are the universal members.
- `display()` prints the object; `display_referencedFromText()` shows where it is used.

#### Rule class hierarchy

```
Rule (base)
├── RuleWithUserID (mixin) → SecurityRule, DecryptionRule, AuthenticationRule, DoSRule, PbfRule, QoSRule, SDWanRule, NetworkPacketBrokerRule, TunnelInspectionRule
├── NatRule
├── AppOverrideRule
├── CaptivePortalRule
└── DefaultSecurityRule
```

Every rule type has its own subclass under `lib/rule-classes/`, with the trait-based `RulewithLogging`, `RuleWithSchedule`, and `NegatableRule` mixed in where appropriate. `RuleStore` is the single owner; it knows whether it is pre-rulebase, post-rulebase, or local.

#### Device/Panorama configuration classes

- `PANConf` — a single firewall.
- `PanoramaConf` — a Panorama, owns `DeviceGroup`s and `Template`s.
- `FawkesConf` — Fawkes (early SCM) backend.
- `BuckbeakConf` — Buckbeak (another SCM iteration) backend.
- `DeviceCloud` and `DeviceOnPrem` — used for Cloud-Managed (`Strata Cloud Manager`) topologies.
- `Container`, `Snippet`, `Sub` — units for the cloud-managed config model.

When loading XML, the framework auto-detects which root element is present and instantiates the matching `*Conf`. Passing `loadpanoramapushedconfig` makes the firewall load the inherited template + DG-pushed objects.

#### Vendor migration framework

```
UTIL → CONVERTER → PARSER → <vendor> (ASA, NetScreen, SonicWall, ...)
```

Each `<vendor>/<vendor>.php` file extends `PARSER` and implements vendor-specific tokenisation. The CONVERTER coordinates the migration: load source, parse, build a `PANConf`/`PanoramaConf`, write XML. `migration/parser/lib/SHAREDNEW.php` is the reusable cross-vendor logic. The set of supported vendor codes is declared in `utils/bash_autocompletion/pa_docker-migration-parser.sh` and matches the parsers under `migration/parser/`.

#### appid-toolbox

A self-contained Application-ID activation pipeline in `appid-toolbox/`:

- Entry scripts (`report-generator.php`, `rule-activation.php`, `rule-cleaner.php`, `rule-cloner.php`, `rule-marker.php`).
- Library (`appid-toolbox/lib/common.php`, `timezone.php`, plus the `lib/old/` and `lib/trait/` sources for the older + newer implementations).
- Tests under `appid-toolbox/tests/` with sample XML for input, comparison, and preparation stages.
- Companion UTIL: `pan-os-php type=appid-toolbox` drives the same logic from the unified entry point (the dispatcher routes `appid-toolbox` to the `AppIDToolbox` UTIL class).

### Module reference (HIGH-priority)

| Folder | Files | Purpose |
|---|---|---|
| `lib/object-classes/` | 49 | Every PAN-OS object type (Address, Service, App, Tag, Schedule, Threat, Profile family) and their stores |
| `lib/network-classes/` | 37 | Zones, interfaces, virtual routers, IKE/IPsec, DHCP, certificates, virtual wires |
| `lib/rule-classes/` | 18 | Rule base + per-rule-type classes; `RuleStore` |
| `lib/device-and-system-classes/` | 16 | `PANConf`, `PanoramaConf`, `FawkesConf`, `BuckbeakConf`, `DeviceGroup`, `Template`, `TemplateStack`, `ManagedDevice`, `LogCollectorGroup`, `Container`, `DeviceCloud`, `DeviceOnPrem`, `Snippet`, `Sub`, `VirtualSystem` |
| `lib/misc-classes/` | 24 | `PH`, `DH`, `RQuery` + per-type RQueryContexts, `PanAPIConnector`, `PanSaseAPIConnector`, `CsvParser` |
| `lib/container-classes/` | 8 | `ObjRuleContainer` and per-field rule containers (Address, App, Service, Tag, GroupTag, UrlCategory, Zone) |
| `lib/helper-classes/` | 4 | IP arithmetic + service port mappings |
| `utils/lib/` | 47 | One UTIL class per `type=` (RULEUTIL, STATSUTIL, MERGER, RULEMERGER, KEYMANGER, BPAGENERATOR, ...) |
| `utils/common/` | 38 | `*CallContext`, `actions-*.php`, `CallContext.php`, `RuleCallContext.php` etc. |
| `utils/develop/` | 21 | Experimental / in-progress UTILs (`grp-static-to-dynamic.php`, `ike.php`, `wildcard_calculator.php`, `csv-import.php`, `checkpoint-exclude.php`, `reset-config.php`, `pan_get_user_info.php`, `migration/*.php` for bluecoat/f5/vmware-vrni) |
| `utils/develop/migration/` | 5 | One-off migration helpers (Bluecoat, F5 BIG-IP, VMware vRNI, ...) |
| `utils/develop/ui/` | 8 | Experimental browser UI (do not deploy to production) |
| `utils/api/` | 1 | `utils/api/v1/tool.php` REST front-end |
| `utils/bash_autocompletion/` | 3 | Bash completion for `pan-os-php` and the legacy `pa_*` aliases |
| `migration/parser/` | per-vendor folders | One PARSER subclass per supported vendor |
| `migration/parser/lib/` | 3 | `CONVERTER.php`, `PARSER.php`, `SHAREDNEW.php` |
| `appid-toolbox/` | 5 entry scripts + lib + tests | App-ID activation toolset |
| `examples/` | 13 | End-to-end demonstrations of using the library directly (without the UTIL dispatcher) |
| `iron-skillet/panos_v8.1` … `panos_v11.0` | snippets | Bundled IronSkillet templates |
| `git-php/src/` | 9 | Bundled Git wrapper |

### Code patterns and conventions

- **Static `$supportedX` arrays.** The convention across the framework is to declare a static array on a class — `PH::$supportedUTILTypes`, `RuleCallContext::$supportedActions`, `App::$_supportedCharacteristics`, `RQuery::$defaultFilters` — and use it both as a runtime dispatch table and as the source of truth for `listactions`/`listfilters` / `help` output.
- **`derr` for fatal, `mwarning` for non-fatal.** `derr` always exits 1 with a backtrace; `mwarning` prints to STDERR (or the JSON warning array) and continues. Use `derr` whenever the program cannot meaningfully proceed.
- **`PH::print_stdout()` over `echo`.** All textual output goes through `PH::print_stdout()`. This routes to STDOUT in human mode or into the JSON buffer when `shadow-json` is set.
- **`require_once` over `use`.** The framework predates strict PSR-4 layout; classes are loaded centrally via `lib/pan_php_framework.php`. The companion `lib/autoloader.php` exists but is not active (the central `require_once` block runs first).
- **Trait-based mixins.** `ReferenceableObject`, `XmlConvertible`, `ObjectWithDescription`, `PathableName`, `NegatableRule`, `RulewithLogging`, `RuleWithSchedule`, `RuleWithUserID` are traits applied where the role is shared.
- **The `find` / `findOrCreate` / `getAll` triplet** is implemented across every store. Use them; do not reach into `$store->o[]` directly.

### Testing strategy

- **Unit / integration.** `tests/test_actions.php` reads `utils/lib/util_action_filter.json`, iterates every action, and exercises it against `tests/input/panorama-8.0.xml` (or whatever `in=` overrides). It is essentially a per-action smoke test; failures show which actions regressed.
- **`appid-toolbox/tests/`.** Stage-based test fixtures (`stage00.xml` … `stage20.xml`) for the AppID activation pipeline.
- **`git-php/tests/`.** Tests for the bundled Git wrapper (`nette/tester` based — `git-php/composer.json` declares `nette/tester ^2.0` as a dev dep).
- **Sample configs.** `examples/sample-configs/` contains `policy-best-practices.xml`, `panorama-example.xml`, `panorama-8.0.xml`. Example scripts under `examples/` consume these.

When adding an action, also add a smoke entry to `util_action_filter.json` (`type=util_get-action-filter` regenerates the file) and verify `tests/test_actions.php` covers it.

### Potential improvements

- **Streaming XML parsing.** `DOMDocument`-only loading caps the practical config size at host-RAM. A streaming `XMLReader` path for read-only UTILs (`stats`, `xml-issue`, `display`-only filters) would let a 16 GB jump host handle Panoramas it currently cannot.
- **Concurrent API push.** `PanAPIConnector` is single-threaded; large mutate-many runs serialise the HTTP latency. A bounded worker pool (e.g., `pthreads` or curl multi) for `set`/`edit` calls would dramatically cut wall-clock for high-N operations.
- **Action removal of obsolete patterns.** Some actions exist twice with `-FastAPI` suffixes (`enabled-Set` vs `enabled-Set-FastAPI`, `securityProfile-Group-Set` vs `securityProfile-Group-Set-FastAPI`); document or consolidate.
- **Better SASE coverage.** `PanSaseAPIConnector` returns "do not yet support this UTIL type" for many UTILs. Tracking a per-UTIL support matrix (and surfacing it in `help` output) would set expectations.
- **Replace `mysqli` requirement with optional load.** `composer.json` requires `ext-mysqli` for everyone, even though only `utils/develop/ui/` and one migration helper actually use it. Move to runtime feature detection.
- **Audit `utils/develop/ui/` defaults.** The experimental UI ships sample test accounts. Gate it behind a build flag or replace them with placeholders that error on first use.
- **Issue triage.** Several open bugs (`#791`, `#798`, `#552`, `#557`, `#696`, `#588`) point at filter and action edge cases that would benefit from regression tests added to `tests/test_actions.php`.

### Contributing

Contributions are accepted via pull request to `main`. The repository is licensed under the **ISC License** (see `composer.json` and `LICENSE`); the bundled `git-php/` is **BSD-3-Clause**; bundled PAN-OS schema XML under `lib/resources/` is © Palo Alto Networks under fair-use copy. By contributing you agree your changes are released under ISC.

Practical guidance:

- New action: add to the relevant `*CallContext::$supportedActions` (`utils/common/actions-*.php`), regenerate `util_action_filter.json` with `type=util_get-action-filter`, and add a smoke test to `tests/test_actions.php`.
- New filter operator: extend `RQuery::$defaultFilters[<type>]` and the corresponding `RQueryContext` class.
- New UTIL type: add a class under `utils/lib/`, add the type name to `PH::$supportedUTILTypes`, wire it into the `PH::callPANOSPHP()` switch.
- New vendor parser: add `migration/parser/<VENDOR>/<VENDOR>.php` extending `PARSER`, register it in `utils/bash_autocompletion/pa_docker-migration-parser.sh`.
- Run `php -l <file>` on every modified PHP file before pushing.
- The `update_pan-os-php.sh` flow expects PRs to be mergeable via `git pull origin main`; rebase your branch.

See `CONTRIBUTING.md` (top-level) and `SUPPORT.md` for the official policy. Note in particular that PAN-OS-PHP is **community-supported** and is not covered by Palo Alto Networks TAC.

---

## 9. Integration Points

`pan-os-php` is designed to plug into existing operational pipelines. The integration points exposed by the framework:

### CLI

The single CLI is `pan-os-php` (alias) → `php utils/pan-os-php.php`. Every UTIL is reachable through it. STDOUT is human text by default and JSON when `shadow-json` is set; STDERR carries warnings and fatal traces. Exit code 0 is success, 1 is `derr()` exit, 2 is unhandled PHP fatal.

### Library mode

Drop into a custom PHP script:

```php
require_once '/tools/pan-os-php/lib/pan_php_framework.php';

$pan = new PanoramaConf();
$pan->load_from_file('panorama.xml');

foreach( $pan->securityRules->rules() as $rule )
{
    PH::print_stdout("Rule '" . $rule->name() . "'");
}

$pan->save_to_file('panorama-modified.xml');
```

Every public method on `PANConf`, `PanoramaConf`, `FawkesConf`, the rule stores, the object stores, and the helper classes is fair game. See `examples/` for thirteen worked examples of this style — including `example-add-security-profile-to-all-rules-panorama.php`, `example-basics-panos.php`, and `example_plugin_rule_display_source_destination.php`.

### REST front-end

`utils/api/v1/tool.php` exposes every UTIL as `GET /utils/api/v1/tool.php/<type>?...`. Mount behind nginx + PHP-FPM. The endpoint validates the `<type>` against `PH::$supportedUTILTypes` and returns:

- 400 on unknown endpoint with `["error" => ..., "endpoint" => [<list of supported types>], "example" => ...]`.
- The UTIL's normal output (JSON if `shadow-json` is in the query, human text otherwise) on success.

```bash
curl 'http://localhost:8082/utils/api/v1/tool.php/address?shadow-json'
```

This endpoint has **no built-in auth**. Always front it with an authenticating proxy or a service mesh in any deployment beyond `localhost`.

### Docker image

```bash
docker pull swaschkut/pan-os-php:latest
docker pull swaschkut/pan-os-php-cli:latest
docker pull swaschkut/pan-os-php-api:latest
```

Three image variants exist for amd64 and arm64 (per `create_all_docker_container.sh`):

- `swaschkut/pan-os-php` — interactive CLI
- `swaschkut/pan-os-php-cli` — non-interactive CLI for CI
- `swaschkut/pan-os-php-api` — bundled REST front-end

The `develop` tag tracks the development branch. For production CI, pin by digest.

### Plugin system

`loadplugin=path/to/file.php` `require_once`'s the file during `UTIL::utilInit()`. The plugin can append to any `*CallContext::$supportedActions` array, register new RQuery operators, or simply define helper functions used by an existing action. See `examples/example_plugin_rule_display_source_destination.php` for the working example.

### Output integrations

| Output | Format | Consumer |
|---|---|---|
| `out=after.xml` | PAN-OS XML | Re-import into Panorama / firewall |
| `outputformatset=changes.set` | PAN-OS CLI `set` commands | Paste into firewall CLI; review tool |
| `actions=exportToExcel:file.xlsx` | XLSX (via `python3-xlsxwriter`) | Excel / Google Sheets review |
| `shadow-json` on stdout | JSON | jq / scripting / ingest into observability |
| `git=` | Git history around `in=` file | Standard Git tooling |
| `actions=display` | Human text on stdout | Ad-hoc inspection |
| `type=html-merger` | Merged HTML report | Browser review |

### Inputs accepted

- PAN-OS firewall running config XML (`<config>...</config>`).
- Panorama running config XML.
- Cloud-managed (Fawkes/Buckbeak) config XML.
- Live device via `api://IP`, `api://serial@panorama`, or `sase-api://...`.
- Vendor configs via `type=vendor-migration vendor=<X>` for: ciscoasa, ciscoisr, ciscoswitch, netscreen, sonicwall, sophos, fortinet, srx (Juniper SRX), cp-r80 (CheckPoint R80), cp (CheckPoint), cp-beta, huawei, stonesoft, sidewinder, pfsense.

### External services

- **MaxMind GeoIP.** `type=maxmind-update` refreshes `lib/resources/geoip/` from MaxMind's downloads; requires the operator-supplied license key (set in MaxMind account, passed via the UTIL's command line).
- **IronSkillet upstream.** `type=ironskillet-update` pulls from the public IronSkillet GitHub repository.
- **MaxMind / IANA** for protocol-number refresh via `type=protocoll-number-download`.
- **Expedition** integration is supported via the `expedition` flag and `lib/classes/taskManagement/ExpeditionTaskReporter.php`; pan-os-php was historically the engine inside Palo Alto's Expedition migration tool and retains the hooks.
- **GCP** integration helper at `type=gcp` for Google Cloud-managed Palo Alto deployments.
- **SSH (`type=ssh-connector`)** uses bundled `phpseclib` for SSH/RSA — the device must allow CLI SSH from the operator host.

### Embedding pan-os-php in other tools

Common patterns:

- **Ansible.** Drop a wrapper module that shells out to `pan-os-php type=...` and parses `shadow-json` output.
- **GitOps.** Store firewall configs as XML in a Git repo, run `pan-os-php` in CI to validate (`type=xml-issue`), generate diffs (`type=diff`), and push (`type=upload`) on merge.
- **Change-management gates.** Run `actions=display` inside CI on every PR to surface what each merge would actually change in production. Block merges with unexpected diffs.
- **Self-service portals.** Expose `utils/api/v1/tool.php` behind an authenticating proxy and offer pre-canned UTIL invocations (display unused objects, list rules with broad source) as ticket-creating buttons.

> [!NOTE]
> The framework is intentionally synchronous and CLI-shaped. There is no built-in webhook, no event stream, no scheduler. When something happens in PAN-OS, it is up to the surrounding system (cron, Ansible, your CI, Panorama's own scheduler) to trigger pan-os-php; pan-os-php itself only runs when invoked.

### Logging and observability

There is no built-in metrics emission. To get observability:

- Wrap each invocation in a shell that records start/end time, exit code, and stdout to your log aggregator.
- Use `shadow-json` and parse the resulting structure for action counts, warnings, errors.
- Combine `outputformatset=` with version-controlled storage so each change is auditable independently of the surrounding pipeline.

### Compatibility matrix

| External system | Compatible | Notes |
|---|---|---|
| PAN-OS 8.1 | Yes | IronSkillet 8.1 templates bundled |
| PAN-OS 9.1 | Yes | IronSkillet 9.1 templates bundled |
| PAN-OS 10.0, 10.1, 10.2 | Yes | IronSkillet templates per-version |
| PAN-OS 11.0 | Yes | IronSkillet 11.0 templates bundled |
| PAN-OS 11.1+ / 12.x | Best-effort | Update via `update_pan-os-php.sh`; new XML elements can break specific actions until the framework is updated |
| Panorama | Yes | First-class — most operations are designed for Panorama scale |
| Cloud-managed (Strata Cloud Manager, formerly Fawkes/Buckbeak) | Partial | Classes exist (`FawkesConf`, `BuckbeakConf`, `DeviceCloud`, `DeviceOnPrem`); `sase-api://` connector is partial |
| Prisma Access | Best-effort | Treated as a Panorama with managed devices; not all features mapped |
| Expedition | Yes | Hooks via `expedition` flag and `ExpeditionTaskReporter` |
| Python automation | Indirect | Run via `subprocess` and parse `shadow-json`; or use the REST front-end |
| Ansible | Indirect | Wrap as a custom module |

---

_Generated April 2026 from PaloAltoNetworks/pan-os-php at d87de87. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial, AI-generated guide for a Palo Alto Networks open-source repository; not reviewed by Palo Alto Networks._
