panorama-super-cli 0.2.6__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/CHANGELOG.md +24 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/PKG-INFO +1 -1
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/getting-started/concepts.md +10 -6
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/guides/duplicates-and-merging.md +5 -1
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/guides/references-and-audit.md +11 -5
- panorama_super_cli-0.3.0/docs/superpowers/specs/2026-06-04-where-used-all-rulebases-design.md +227 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/_version.py +1 -1
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/apply_live.py +6 -6
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/apply_xml.py +9 -10
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/changeset.py +17 -10
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/dedup.py +29 -4
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/models.py +60 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/parse.py +37 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/refs.py +64 -5
- panorama_super_cli-0.3.0/psc/core/rulebases.py +55 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/setcmd.py +9 -8
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/pyproject.toml +1 -1
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/skills/panorama-super-cli/SKILL.md +6 -1
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/conftest.py +16 -0
- panorama_super_cli-0.3.0/tests/fixtures/all-rulebases.xml +153 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_apply_xml.py +16 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_dedup.py +32 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_live_apply.py +42 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_naming.py +61 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_parse.py +47 -1
- panorama_super_cli-0.3.0/tests/test_refs.py +100 -0
- panorama_super_cli-0.3.0/tests/test_rulebases.py +35 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_setcmd.py +36 -1
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/uv.lock +577 -577
- panorama_super_cli-0.2.6/tests/test_refs.py +0 -42
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/.github/CODEOWNERS +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/.github/dependabot.yml +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/.github/workflows/docs.yml +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/.github/workflows/lint.yml +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/.github/workflows/release.yml +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/.github/workflows/test.yml +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/.gitignore +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/.pre-commit-config.yaml +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/.python-version +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/AGENTS.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/CLAUDE.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/LICENSE +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/README.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/contributing/branching.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/contributing/development.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/contributing/release-process.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/getting-started/first-run.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/getting-started/install.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/guides/finding-objects.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/guides/live-vs-offline.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/guides/naming.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/guides/output-formats.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/guides/safety.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/guides/using-with-ai-agents.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/index.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/reference/cli.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/reference/config.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/docs/reference/exit-codes.md +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/justfile +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/mkdocs.yml +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/__init__.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/__main__.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/cli/__init__.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/cli/_plan.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/cli/app.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/cli/auth_cmds.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/cli/dedup_cmds.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/cli/find_cmds.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/cli/name_cmds.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/cli/profile_cmds.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/cli/refs_cmds.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/cli/runtime.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/cli/version_cmds.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/config/__init__.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/config/loader.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/config/models.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/__init__.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/naming.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/normalize.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/resolve.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/source.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/core/version_check.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/output/__init__.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/output/errors.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/psc/output/format.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/scripts/sync_agents_md.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/__init__.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/fixtures/nested-device-groups.xml +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/fixtures/panorama-config.xml +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_auth.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_changeset.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_cli.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_cli_auth.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_cli_profile.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_hardening.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_nested_dg.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_normalize.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_output.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_resolve.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_source.py +0 -0
- {panorama_super_cli-0.2.6 → panorama_super_cli-0.3.0}/tests/test_version_check.py +0 -0
|
@@ -7,6 +7,30 @@ project will follow [Semantic Versioning](https://semver.org/). While on
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## v0.3.0 — 2026-06-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Where-used across every object-referencing rulebase** (#2). The reference
|
|
15
|
+
graph previously covered only `security` and `nat`; it now also scans PBF,
|
|
16
|
+
decryption, authentication, QoS, application-override, DoS, SD-WAN,
|
|
17
|
+
tunnel-inspect, and network-packet-broker. `refs used`/`unused`/`dangling`
|
|
18
|
+
and the merge/rename repointing all account for them, so an object referenced
|
|
19
|
+
only by, say, a QoS or PBF rule is no longer reported unused (and can no
|
|
20
|
+
longer be deleted out from under a live rule). A PBF forwarding next-hop that
|
|
21
|
+
names an address object is shown in where-used and **blocks** a merge/rename
|
|
22
|
+
that would strand it (it has no flat member list to rewrite — edit it by hand,
|
|
23
|
+
then re-run). The `referrer_kind` in the output (`qos-rule`, `pbf-rule`, …)
|
|
24
|
+
names the exact rulebase.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Tag rename/merge no longer wipes a rule's other tags** (#2). `field_members`
|
|
29
|
+
read the wrong attribute name for a rule's `tag` field, so renaming or merging
|
|
30
|
+
a tag referenced by a security rule rewrote the rule's tag list to an empty (or
|
|
31
|
+
single-entry) value, destroying its co-tags. All rule kinds now resolve the
|
|
32
|
+
field uniformly.
|
|
33
|
+
|
|
10
34
|
## v0.2.6 — 2026-06-04
|
|
11
35
|
|
|
12
36
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: panorama-super-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Agent-friendly CLI for Palo Alto Panorama object management: find, dedup/merge, rename, and audit address/service objects safely.
|
|
5
5
|
Project-URL: Homepage, https://github.com/thomaschristory/panorama-super-cli
|
|
6
6
|
Project-URL: Documentation, https://thomaschristory.github.io/panorama-super-cli/
|
|
@@ -5,9 +5,11 @@ A small mental model goes a long way.
|
|
|
5
5
|
## Snapshot
|
|
6
6
|
|
|
7
7
|
`psc` reads a config into an immutable **snapshot**: every address, address
|
|
8
|
-
group, service, service group, tag,
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
group, service, service group, tag, and rule it understands — security, NAT,
|
|
9
|
+
and the nine other rulebases that reference objects (PBF, decryption,
|
|
10
|
+
authentication, QoS, application-override, DoS, SD-WAN, tunnel-inspect,
|
|
11
|
+
network-packet-broker) — across `shared` and every device-group. Read commands
|
|
12
|
+
query the snapshot; they never mutate it.
|
|
11
13
|
|
|
12
14
|
## Locations and inheritance
|
|
13
15
|
|
|
@@ -38,9 +40,11 @@ it. A change-set with **blockers** is unsafe and is refused — even with
|
|
|
38
40
|
|
|
39
41
|
## Reference graph
|
|
40
42
|
|
|
41
|
-
The **reference graph** answers "who points at this object?" across groups
|
|
42
|
-
security
|
|
43
|
-
|
|
43
|
+
The **reference graph** answers "who points at this object?" across groups and
|
|
44
|
+
every object-referencing rulebase — security, NAT (match *and* translation
|
|
45
|
+
fields), and PBF, decryption, authentication, QoS, application-override, DoS,
|
|
46
|
+
SD-WAN, tunnel-inspect, and network-packet-broker. It powers where-used, unused
|
|
47
|
+
detection, and the safe repointing that merge and rename rely on.
|
|
44
48
|
|
|
45
49
|
## Sources
|
|
46
50
|
|
|
@@ -58,7 +58,11 @@ Every reference the [reference graph](references-and-audit.md) knows about:
|
|
|
58
58
|
|
|
59
59
|
- static address-group membership,
|
|
60
60
|
- security rule `source` / `destination`,
|
|
61
|
-
- NAT rule `source` / `destination` (translation fields are flagged for review)
|
|
61
|
+
- NAT rule `source` / `destination` (translation fields are flagged for review),
|
|
62
|
+
- every other rulebase's `source` / `destination` / `service` / `tag` (PBF,
|
|
63
|
+
decryption, authentication, QoS, application-override, DoS, SD-WAN,
|
|
64
|
+
tunnel-inspect, network-packet-broker). A PBF next-hop object has no flat
|
|
65
|
+
member list, so a merge that would strand it is **blocked** for manual review.
|
|
62
66
|
|
|
63
67
|
References are rewritten **before** the object is deleted, and duplicate members
|
|
64
68
|
are collapsed (a group that listed both names ends up with just the survivor).
|
|
@@ -13,7 +13,10 @@ psc -c panorama.xml refs used h-web1
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Lists every reference that *resolves* to that object — group memberships,
|
|
16
|
-
security rule `source`/`destination`, NAT `source`/`destination`/translation
|
|
16
|
+
security rule `source`/`destination`, NAT `source`/`destination`/translation,
|
|
17
|
+
and the `source`/`destination`/`service`/`tag` fields of every other rulebase
|
|
18
|
+
(PBF, decryption, authentication, QoS, application-override, DoS, SD-WAN,
|
|
19
|
+
tunnel-inspect, network-packet-broker), plus a PBF forwarding next-hop object.
|
|
17
20
|
|
|
18
21
|
If a name is ambiguous (exists in multiple kinds/locations), pass `--kind` and
|
|
19
22
|
`--location`:
|
|
@@ -59,7 +62,10 @@ if `refs dangling` finds anything).
|
|
|
59
62
|
psc -c panorama.xml --strict refs dangling || echo "config has dangling refs"
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
!!! note "
|
|
63
|
-
The reference graph
|
|
64
|
-
security
|
|
65
|
-
QoS,
|
|
65
|
+
!!! note "Rulebase coverage"
|
|
66
|
+
The reference graph covers address-groups, service-groups, and **every**
|
|
67
|
+
object-referencing rulebase: security, NAT, PBF, decryption, authentication,
|
|
68
|
+
QoS, application-override, DoS, SD-WAN, tunnel-inspect, and
|
|
69
|
+
network-packet-broker. A PBF forwarding next-hop that names an address
|
|
70
|
+
object is shown in where-used and blocks a merge/rename that would strand it
|
|
71
|
+
(it has no flat member list to rewrite — edit it by hand, then re-run).
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# where-used across ALL rulebases — design (issue #2)
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
`psc`'s reference graph only scans two rulebases: `security` and `nat`. PAN-OS
|
|
6
|
+
carries object references in **nine more** rulebases — PBF, decryption,
|
|
7
|
+
authentication, QoS, application-override, DoS, SD-WAN, tunnel-inspect, and
|
|
8
|
+
network-packet-broker. Until these are scanned:
|
|
9
|
+
|
|
10
|
+
- **`unused` lies.** An address referenced *only* by a QoS or PBF rule is
|
|
11
|
+
reported as unused — and could then be deleted. `reachable_targets()` seeds
|
|
12
|
+
only from `security-rule`/`nat-rule` references, so every other rulebase is
|
|
13
|
+
invisible to reachability.
|
|
14
|
+
- **`merge`/`rename`/`delete` are unsafe.** They repoint *known* references
|
|
15
|
+
before removing an object. A reference in an unscanned rulebase is silently
|
|
16
|
+
left dangling — the exact failure repoint-before-delete exists to prevent.
|
|
17
|
+
- **`where-used` is incomplete** as a delete/rename pre-flight.
|
|
18
|
+
|
|
19
|
+
Scope decisions (confirmed with the user):
|
|
20
|
+
|
|
21
|
+
1. **All nine** object-referencing rulebases, not just the seven in the issue
|
|
22
|
+
(tunnel-inspect + network-packet-broker added — the registry makes them
|
|
23
|
+
nearly free, and omitting them leaves the same safety hole the issue exists
|
|
24
|
+
to close).
|
|
25
|
+
2. **PBF nexthop** address-object references are modelled as a *review-gated*
|
|
26
|
+
reference: shown in `where-used`, but a hard **blocker** on merge/rename
|
|
27
|
+
(the appliers can't rewrite a nested single-value field), exactly like NAT
|
|
28
|
+
translation fields.
|
|
29
|
+
3. **Additive** model: `SecurityRule`/`NatRule` are left untouched (zero
|
|
30
|
+
regression risk on the safety-critical paths). One new generic `PolicyRule`
|
|
31
|
+
+ a tiny rulebase registry covers the nine new rulebases.
|
|
32
|
+
4. **Reference surface** per rulebase: address objects in `source`/`destination`,
|
|
33
|
+
service objects in `service`, and rule `tag`s. Applications, zones, profiles,
|
|
34
|
+
URL categories, and schedules are out of scope (none are psc-managed object
|
|
35
|
+
kinds).
|
|
36
|
+
|
|
37
|
+
## Key insight
|
|
38
|
+
|
|
39
|
+
`referrer_kind` is always `"{rule_type}-rule"`, and the XML container tag, the
|
|
40
|
+
`set` keyword, and the live xpath segment are all just `rule_type`:
|
|
41
|
+
`security-rule → security`, `pbf-rule → pbf`, even `nat-rule → nat`. So a single
|
|
42
|
+
helper — `rule_container(referrer_kind) -> str | None` — collapses the four
|
|
43
|
+
hardcoded switch sites (`setcmd`, `apply_xml`, `apply_live`,
|
|
44
|
+
`changeset.reference_edit_is_mappable`) into table-driven code. Adding the
|
|
45
|
+
twelfth rulebase later becomes a one-line registry entry.
|
|
46
|
+
|
|
47
|
+
## Architecture
|
|
48
|
+
|
|
49
|
+
A new `psc/core/rulebases.py` is the single source of truth for *which*
|
|
50
|
+
rulebases carry references and *which* of their fields are flat (repointable)
|
|
51
|
+
vs nested (review-gated). Everything else consults it.
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
parse.py ──► PolicyRule (new model) ──► Snapshot.policy_rules
|
|
55
|
+
│
|
|
56
|
+
refs.py._walk ──────────────────────────────►│ emits References (addr/svc/tag + pbf nexthop)
|
|
57
|
+
refs.py._rule_seeded_targets ────────────────►│ seeds reachability from ALL rulebases ← unused fix
|
|
58
|
+
│
|
|
59
|
+
dedup.field_members ─────────────────────────►│ resolves current member list for any *-rule
|
|
60
|
+
changeset.reference_edit_is_mappable ─────────►│ uses rule_container() + FLAT_RULE_FIELDS
|
|
61
|
+
setcmd / apply_xml / apply_live ─────────────►│ build path from rule_container()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### New module: `psc/core/rulebases.py`
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# rule_type value == XML container tag == `set` keyword == xpath segment.
|
|
68
|
+
POLICY_RULE_TYPES = (
|
|
69
|
+
"pbf", "decryption", "authentication", "qos", "application-override",
|
|
70
|
+
"dos", "sdwan", "tunnel-inspect", "network-packet-broker",
|
|
71
|
+
)
|
|
72
|
+
# Every referrer_kind that maps to a `{rb}-rulebase/{container}/rules` entry.
|
|
73
|
+
_RULE_CONTAINERS = {"security", "nat", *POLICY_RULE_TYPES}
|
|
74
|
+
# Flat <member> fields an applier can rewrite in place.
|
|
75
|
+
FLAT_RULE_FIELDS = frozenset({"source", "destination", "service", "tag"})
|
|
76
|
+
|
|
77
|
+
def rule_container(referrer_kind: str) -> str | None:
|
|
78
|
+
"""'pbf-rule' -> 'pbf'; None if not a rulebase referrer (group/object)."""
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`FLAT_RULE_FIELDS` deliberately excludes `application`/`source-user` (never
|
|
82
|
+
emitted as object reference-edits) and PBF `nexthop` (nested → review-gated).
|
|
83
|
+
NAT keeps its existing `field in ("source","destination")` narrowing in the
|
|
84
|
+
appliers, since its translation fields are nested.
|
|
85
|
+
|
|
86
|
+
### New model: `psc/core/models.py`
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
class RuleType(str, Enum):
|
|
90
|
+
PBF = "pbf"; DECRYPTION = "decryption"; AUTHENTICATION = "authentication"
|
|
91
|
+
QOS = "qos"; APPLICATION_OVERRIDE = "application-override"; DOS = "dos"
|
|
92
|
+
SDWAN = "sdwan"; TUNNEL_INSPECT = "tunnel-inspect"
|
|
93
|
+
NETWORK_PACKET_BROKER = "network-packet-broker"
|
|
94
|
+
|
|
95
|
+
class PolicyRule(BaseModel):
|
|
96
|
+
"""Reference surface of a 'security-shaped' rulebase (no NAT translation).
|
|
97
|
+
|
|
98
|
+
One model for nine rulebases: they share source/destination (address),
|
|
99
|
+
an optional service list, rule tags, and — for PBF only — a forwarding
|
|
100
|
+
nexthop address object. Mirrors SecurityRule's 'only the reference surface'
|
|
101
|
+
philosophy; application/source-user are omitted because they name no
|
|
102
|
+
psc-managed object.
|
|
103
|
+
"""
|
|
104
|
+
name: str
|
|
105
|
+
location: Location = SHARED
|
|
106
|
+
rulebase: Rulebase = Rulebase.PRE
|
|
107
|
+
rule_type: RuleType
|
|
108
|
+
source: list[str] = Field(default_factory=lambda: ["any"])
|
|
109
|
+
destination: list[str] = Field(default_factory=lambda: ["any"])
|
|
110
|
+
service: list[str] = Field(default_factory=list) # empty for app-override
|
|
111
|
+
nexthop: str | None = None # PBF forwarding object
|
|
112
|
+
disabled: bool = False
|
|
113
|
+
tags: list[str] = Field(default_factory=list)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def referrer_kind(self) -> str: return f"{self.rule_type.value}-rule"
|
|
117
|
+
@property
|
|
118
|
+
def key(self) -> tuple[str, str, str, str]:
|
|
119
|
+
return (self.location.name, self.rule_type.value, self.rulebase.value, self.name)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`Snapshot` gains `policy_rules: list[PolicyRule] = Field(default_factory=list)`.
|
|
123
|
+
|
|
124
|
+
### Parsing: `psc/core/parse.py`
|
|
125
|
+
|
|
126
|
+
`_collect` already loops `(pre-rulebase, post-rulebase)`. Add one call per
|
|
127
|
+
rulebase tag:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
for rt in RuleType: # drives off the registry
|
|
131
|
+
snap.policy_rules.extend(_parse_policy_rules(rb_el, loc, rt))
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`_parse_policy_rules` finds `./{rt.value}/rules/entry` and reads `source`,
|
|
135
|
+
`destination`, `service`, `tag` with the existing `_members` helper (absent →
|
|
136
|
+
empty, so app-override naturally has no service). For PBF it additionally reads
|
|
137
|
+
`action/forward/nexthop/fqdn` (the object-capable nexthop variant; a literal
|
|
138
|
+
`ip-address` nexthop is *not* an object and is ignored).
|
|
139
|
+
|
|
140
|
+
### Reference walk: `psc/core/refs.py`
|
|
141
|
+
|
|
142
|
+
- `_walk` gains one loop over `snap.policy_rules` emitting `source`/`destination`
|
|
143
|
+
(address), `service` (service), `tag` (tag), and — when `rule_type is PBF and
|
|
144
|
+
nexthop` — a `nexthop` (address) reference.
|
|
145
|
+
- `_rule_seeded_targets`: replace the `referrer_kind in ("security-rule",
|
|
146
|
+
"nat-rule")` test with `rule_container(referrer_kind) is not None`. **This is
|
|
147
|
+
the `unused` safety fix** — reachability now seeds from every rulebase.
|
|
148
|
+
- `dangling()`: exclude `field == "nexthop"` unresolved refs. A PBF nexthop is
|
|
149
|
+
often a literal FQDN/IP, not an object; flagging those as dangling would be
|
|
150
|
+
noise. (When it *does* resolve to an object, it appears in `where_used` and is
|
|
151
|
+
gated on merge/rename — the safety win is preserved.)
|
|
152
|
+
|
|
153
|
+
### Repoint resolution: `psc/core/dedup.py::field_members`
|
|
154
|
+
|
|
155
|
+
Add an `elif rule_container(ref.referrer_kind) ...` branch that looks the rule
|
|
156
|
+
up in `snap.policy_rules` by `(rule_type, name, location, rulebase)` and returns
|
|
157
|
+
`getattr(rule, field)`. The existing security/nat branches stay.
|
|
158
|
+
|
|
159
|
+
### Safety gate + appliers (the four switch sites)
|
|
160
|
+
|
|
161
|
+
All four now compute the container via `rule_container()`:
|
|
162
|
+
|
|
163
|
+
- **`changeset.reference_edit_is_mappable`**: `address-group`/`service-group` →
|
|
164
|
+
True; else `container = rule_container(kind)`; None or no `rulebase` → False;
|
|
165
|
+
`nat` → `field in (source,destination)`; otherwise `field in FLAT_RULE_FIELDS`.
|
|
166
|
+
PBF `nexthop` (not in the set) → False → `gate_unmappable_reference_edits`
|
|
167
|
+
promotes it to a blocker when a delete/rename is present. **No new gating code
|
|
168
|
+
is needed — the existing gate already does the right thing** once
|
|
169
|
+
`reference_edit_is_mappable` returns False for nexthop.
|
|
170
|
+
- **`apply_xml._referrer_field_element`**, **`apply_live._referrer_field_xpath`**,
|
|
171
|
+
**`setcmd._referrer_path`**: groups unchanged; rules build
|
|
172
|
+
`{rb}-rulebase/{container}/rules/entry/{field}` (XML / xpath) or
|
|
173
|
+
`{scope} {rb}-rulebase {container} rules {name} {field}` (set) from
|
|
174
|
+
`rule_container()`. NAT translation + PBF nexthop fall through to the existing
|
|
175
|
+
`None`/`# REVIEW` path.
|
|
176
|
+
|
|
177
|
+
The security and NAT behaviours are byte-for-byte identical (container values
|
|
178
|
+
`security`/`nat`), so the existing applier/setcmd/round-trip tests are the
|
|
179
|
+
regression guard.
|
|
180
|
+
|
|
181
|
+
### CLI / output
|
|
182
|
+
|
|
183
|
+
No changes. `refs_cmds` passes `referrer_kind` through as a string, so the new
|
|
184
|
+
kinds (`pbf-rule`, `qos-rule`, …) surface in `where-used`/`dangling` tables
|
|
185
|
+
automatically with distinct, self-describing labels.
|
|
186
|
+
|
|
187
|
+
## Testing (TDD — failing test first)
|
|
188
|
+
|
|
189
|
+
New fixture `tests/fixtures/all-rulebases.xml`: one rule per new rulebase across
|
|
190
|
+
`shared` + a device-group, pre and post, referencing shared and DG-local
|
|
191
|
+
addresses/services/tags, plus a PBF rule whose nexthop names an address object.
|
|
192
|
+
|
|
193
|
+
- **test_parse**: all nine rule_types parsed; fields correct; app-override has
|
|
194
|
+
empty service; PBF nexthop captured.
|
|
195
|
+
- **test_refs**: `where_used` on an address used only by a QoS rule finds it;
|
|
196
|
+
`unused` does **not** report an address used only by a DoS/PBF/SD-WAN rule
|
|
197
|
+
(seeding fix); a bad service name in a decryption rule shows in `dangling`;
|
|
198
|
+
a PBF nexthop object shows in `where_used`; an unresolved nexthop does **not**
|
|
199
|
+
show in `dangling`.
|
|
200
|
+
- **test_dedup**: merging an address referenced by SD-WAN + authentication rules
|
|
201
|
+
repoints both before delete; merging an address that is a PBF nexthop →
|
|
202
|
+
**blocker** (review-gated), zero ops.
|
|
203
|
+
- **test_naming**: renaming an address referenced by a tunnel-inspect rule
|
|
204
|
+
repoints it.
|
|
205
|
+
- **test_apply_xml**: reference edit into a decryption/QoS rule rewrites the
|
|
206
|
+
member list and round-trips.
|
|
207
|
+
- **test_live_apply**: xpath for a network-packet-broker rule field is correct;
|
|
208
|
+
a PBF nexthop edit is skipped (`None`), not mis-addressed.
|
|
209
|
+
- **test_setcmd**: `set`/`delete` lines for an SD-WAN reference edit; PBF nexthop
|
|
210
|
+
edit renders `# REVIEW`.
|
|
211
|
+
|
|
212
|
+
## Docs to update
|
|
213
|
+
|
|
214
|
+
- `docs/getting-started/concepts.md` (reference-graph coverage sentence).
|
|
215
|
+
- `docs/guides/references-and-audit.md` (lines 16, 63-64 — drop the "other
|
|
216
|
+
rulebases not yet covered" note).
|
|
217
|
+
- `docs/guides/duplicates-and-merging.md` (the repointed-fields list).
|
|
218
|
+
- `skills/panorama-super-cli/SKILL.md` (refs coverage).
|
|
219
|
+
- `refs.py` / `models.py` module docstrings.
|
|
220
|
+
|
|
221
|
+
## Out of scope / follow-ups
|
|
222
|
+
|
|
223
|
+
- Applications, zones, profiles, URL categories, schedules as reference kinds
|
|
224
|
+
(would need new object models).
|
|
225
|
+
- Live read-modify-write for in-place updates (pre-existing limitation).
|
|
226
|
+
- Literal-IP PBF nexthop (only the object-capable `fqdn` variant is modelled).
|
|
227
|
+
```
|
|
@@ -23,7 +23,8 @@ from typing import Literal
|
|
|
23
23
|
|
|
24
24
|
from pydantic import BaseModel
|
|
25
25
|
|
|
26
|
-
from psc.core.changeset import ChangeSet, ObjectUpsert, ReferenceEdit
|
|
26
|
+
from psc.core.changeset import ChangeSet, ObjectUpsert, ReferenceEdit, reference_edit_is_mappable
|
|
27
|
+
from psc.core.rulebases import rule_container
|
|
27
28
|
from psc.output.errors import ErrorType, PscError
|
|
28
29
|
|
|
29
30
|
# The device entry under which Panorama keeps its device-groups. Fixed on
|
|
@@ -84,11 +85,10 @@ def _referrer_field_xpath(edit: ReferenceEdit) -> tuple[str, str] | None:
|
|
|
84
85
|
return f"{base}/address-group/entry[@name='{name}']/static", "static"
|
|
85
86
|
if edit.referrer_kind == "service-group":
|
|
86
87
|
return f"{base}/service-group/entry[@name='{name}']/members", "members"
|
|
87
|
-
if edit
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
path = f"{base}/{rb}-rulebase/nat/rules/entry[@name='{name}']/{edit.field}"
|
|
88
|
+
if reference_edit_is_mappable(edit):
|
|
89
|
+
# Any rulebase flat member field: container derives from referrer_kind.
|
|
90
|
+
container = rule_container(edit.referrer_kind)
|
|
91
|
+
path = f"{base}/{rb}-rulebase/{container}/rules/entry[@name='{name}']/{edit.field}"
|
|
92
92
|
return path, edit.field
|
|
93
93
|
return None
|
|
94
94
|
|
|
@@ -17,7 +17,8 @@ import xml.etree.ElementTree as ET
|
|
|
17
17
|
|
|
18
18
|
from defusedxml.ElementTree import fromstring as _safe_fromstring
|
|
19
19
|
|
|
20
|
-
from psc.core.changeset import ChangeSet, ObjectUpsert, ReferenceEdit
|
|
20
|
+
from psc.core.changeset import ChangeSet, ObjectUpsert, ReferenceEdit, reference_edit_is_mappable
|
|
21
|
+
from psc.core.rulebases import rule_container
|
|
21
22
|
from psc.output.errors import ErrorType, PscError
|
|
22
23
|
|
|
23
24
|
|
|
@@ -68,15 +69,13 @@ def _referrer_field_element(scope: ET.Element, edit: ReferenceEdit) -> ET.Elemen
|
|
|
68
69
|
elif edit.referrer_kind == "service-group":
|
|
69
70
|
entry = _find_named(scope.find("service-group"), "entry", name)
|
|
70
71
|
leaf = "members"
|
|
71
|
-
elif edit
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
):
|
|
79
|
-
entry = _find_named(scope.find(f"./{edit.rulebase}-rulebase/nat/rules"), "entry", name)
|
|
72
|
+
elif reference_edit_is_mappable(edit):
|
|
73
|
+
# Any rulebase whose field is a flat member list: container == the
|
|
74
|
+
# tag derived from referrer_kind (security/nat/pbf/qos/…).
|
|
75
|
+
container = rule_container(edit.referrer_kind)
|
|
76
|
+
entry = _find_named(
|
|
77
|
+
scope.find(f"./{edit.rulebase}-rulebase/{container}/rules"), "entry", name
|
|
78
|
+
)
|
|
80
79
|
leaf = edit.field
|
|
81
80
|
else:
|
|
82
81
|
return None
|
|
@@ -17,6 +17,8 @@ from enum import Enum
|
|
|
17
17
|
|
|
18
18
|
from pydantic import BaseModel, Field
|
|
19
19
|
|
|
20
|
+
from psc.core.rulebases import FLAT_RULE_FIELDS, rule_container
|
|
21
|
+
|
|
20
22
|
|
|
21
23
|
class ObjectKind(str, Enum):
|
|
22
24
|
ADDRESS = "address"
|
|
@@ -132,20 +134,25 @@ def reference_edit_is_mappable(edit: ReferenceEdit) -> bool:
|
|
|
132
134
|
|
|
133
135
|
The single source of truth behind `apply_xml._referrer_field_element` and
|
|
134
136
|
`apply_live._referrer_field_xpath`: a member field they know how to address
|
|
135
|
-
(a group's `static`/`members`, a security-rule field, a NAT src/dst list
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
(a group's `static`/`members`, a security-rule field, a NAT src/dst list, or
|
|
138
|
+
any other rulebase's source/destination/service/tag — see
|
|
139
|
+
`psc.core.rulebases`). A NAT *translation* field and a PBF `nexthop` are
|
|
140
|
+
nested with no flat list, and a rule edit with no `rulebase` can't be
|
|
141
|
+
located — all unmappable, and an applier silently skips them. The planner
|
|
142
|
+
uses this to refuse such a skip when the same plan tears the target down,
|
|
143
|
+
instead of leaving a dangling reference (#28).
|
|
140
144
|
"""
|
|
141
145
|
kind = edit.referrer_kind
|
|
142
146
|
if kind in ("address-group", "service-group"):
|
|
143
147
|
return True
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
container = rule_container(kind)
|
|
149
|
+
if container is None or edit.rulebase is None:
|
|
150
|
+
return False
|
|
151
|
+
if container == "nat":
|
|
152
|
+
# NAT keeps only flat match fields; its translation fields are nested.
|
|
153
|
+
return edit.field in ("source", "destination")
|
|
154
|
+
# security + every policy rulebase: the shared flat member fields.
|
|
155
|
+
return edit.field in FLAT_RULE_FIELDS
|
|
149
156
|
|
|
150
157
|
|
|
151
158
|
def gate_unmappable_reference_edits(cs: ChangeSet) -> None:
|
|
@@ -24,6 +24,7 @@ from psc.core.changeset import (
|
|
|
24
24
|
from psc.core.models import Location, Snapshot
|
|
25
25
|
from psc.core.normalize import normalize_address, service_key
|
|
26
26
|
from psc.core.refs import Reference, ReferenceGraph
|
|
27
|
+
from psc.core.rulebases import rule_container
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
class ObjectRef(BaseModel):
|
|
@@ -188,6 +189,23 @@ def plan_merge(
|
|
|
188
189
|
return cs
|
|
189
190
|
|
|
190
191
|
|
|
192
|
+
def _field_attr(field: str) -> str:
|
|
193
|
+
"""The model attribute holding a reference field's member list.
|
|
194
|
+
|
|
195
|
+
The reference `field` is the PAN-OS *element* name (`tag`, `source`,
|
|
196
|
+
`destination-translation`); the model attribute is its Python form. The one
|
|
197
|
+
irregular case is `tag` → `tags` — getting this wrong returns an empty list,
|
|
198
|
+
so a tag rename/merge would wipe the field instead of rewriting one member.
|
|
199
|
+
"""
|
|
200
|
+
return "tags" if field == "tag" else field.replace("-", "_")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _attr_as_members(obj: object, field: str) -> list[str]:
|
|
204
|
+
"""Read a rule's reference field as a member list (a scalar wraps to one)."""
|
|
205
|
+
val = getattr(obj, _field_attr(field), [])
|
|
206
|
+
return list(val) if isinstance(val, list) else [val]
|
|
207
|
+
|
|
208
|
+
|
|
191
209
|
def field_members(snapshot: Snapshot, ref: Reference) -> list[str]:
|
|
192
210
|
"""Current member list of the field a reference points at."""
|
|
193
211
|
loc = ref.referrer_location
|
|
@@ -202,7 +220,7 @@ def field_members(snapshot: Snapshot, ref: Reference) -> list[str]:
|
|
|
202
220
|
and r.location == loc
|
|
203
221
|
and (ref.rulebase is None or r.rulebase == ref.rulebase)
|
|
204
222
|
):
|
|
205
|
-
return
|
|
223
|
+
return _attr_as_members(r, ref.field)
|
|
206
224
|
elif ref.referrer_kind == "nat-rule":
|
|
207
225
|
for n in snapshot.nat_rules:
|
|
208
226
|
if (
|
|
@@ -210,7 +228,14 @@ def field_members(snapshot: Snapshot, ref: Reference) -> list[str]:
|
|
|
210
228
|
and n.location == loc
|
|
211
229
|
and (ref.rulebase is None or n.rulebase == ref.rulebase)
|
|
212
230
|
):
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
231
|
+
return _attr_as_members(n, ref.field)
|
|
232
|
+
elif rule_container(ref.referrer_kind) is not None:
|
|
233
|
+
for p in snapshot.policy_rules:
|
|
234
|
+
if (
|
|
235
|
+
p.referrer_kind == ref.referrer_kind
|
|
236
|
+
and p.name == ref.referrer_name
|
|
237
|
+
and p.location == loc
|
|
238
|
+
and (ref.rulebase is None or p.rulebase == ref.rulebase)
|
|
239
|
+
):
|
|
240
|
+
return _attr_as_members(p, ref.field)
|
|
216
241
|
return [ref.target_name]
|
|
@@ -32,6 +32,24 @@ class Rulebase(str, Enum):
|
|
|
32
32
|
POST = "post"
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
class RuleType(str, Enum):
|
|
36
|
+
"""The "security-shaped" rulebases beyond `security`/`nat` that carry object
|
|
37
|
+
references. Each value is also the PAN-OS XML container tag / `set` keyword
|
|
38
|
+
(see `psc.core.rulebases`), so the reference graph and the appliers stay
|
|
39
|
+
table-driven rather than hard-coding one branch per rulebase.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
PBF = "pbf"
|
|
43
|
+
DECRYPTION = "decryption"
|
|
44
|
+
AUTHENTICATION = "authentication"
|
|
45
|
+
QOS = "qos"
|
|
46
|
+
APPLICATION_OVERRIDE = "application-override"
|
|
47
|
+
DOS = "dos"
|
|
48
|
+
SDWAN = "sdwan"
|
|
49
|
+
TUNNEL_INSPECT = "tunnel-inspect"
|
|
50
|
+
NETWORK_PACKET_BROKER = "network-packet-broker"
|
|
51
|
+
|
|
52
|
+
|
|
35
53
|
class Location(BaseModel):
|
|
36
54
|
"""Where an object lives: Panorama `shared`, or a named device-group.
|
|
37
55
|
|
|
@@ -184,6 +202,46 @@ class NatRule(BaseModel):
|
|
|
184
202
|
return (self.location.name, self.rulebase.value, self.name)
|
|
185
203
|
|
|
186
204
|
|
|
205
|
+
class PolicyRule(BaseModel):
|
|
206
|
+
"""The object-reference surface of one "security-shaped" rulebase rule.
|
|
207
|
+
|
|
208
|
+
A single model spans the nine rulebases in `RuleType` (PBF, decryption,
|
|
209
|
+
authentication, QoS, application-override, DoS, SD-WAN, tunnel-inspect,
|
|
210
|
+
network-packet-broker): they all reference addresses in `source`/
|
|
211
|
+
`destination`, an optional `service` list, and carry rule `tags`. PBF adds a
|
|
212
|
+
forwarding `nexthop` that can name an address object. Like `SecurityRule`,
|
|
213
|
+
this models only the reference surface — `application`/`source-user` are
|
|
214
|
+
omitted because they name no psc-managed object.
|
|
215
|
+
|
|
216
|
+
`SecurityRule`/`NatRule` predate this model and stay separate (NAT's nested
|
|
217
|
+
translation fields don't fit); `rule_type` is what distinguishes the rest.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
name: str
|
|
221
|
+
location: Location = SHARED
|
|
222
|
+
rulebase: Rulebase = Rulebase.PRE
|
|
223
|
+
rule_type: RuleType
|
|
224
|
+
source: list[str] = Field(default_factory=lambda: ["any"])
|
|
225
|
+
destination: list[str] = Field(default_factory=lambda: ["any"])
|
|
226
|
+
service: list[str] = Field(default_factory=list)
|
|
227
|
+
"""Empty when the rulebase has no service field (e.g. application-override)."""
|
|
228
|
+
nexthop: str | None = None
|
|
229
|
+
"""PBF forwarding next-hop address object, if the rule names one."""
|
|
230
|
+
disabled: bool = False
|
|
231
|
+
tags: list[str] = Field(default_factory=list)
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def referrer_kind(self) -> str:
|
|
235
|
+
"""`"qos-rule"`, `"pbf-rule"`, … — the edge label in the reference graph
|
|
236
|
+
and the key the appliers resolve back to a container via
|
|
237
|
+
`rulebases.rule_container`."""
|
|
238
|
+
return f"{self.rule_type.value}-rule"
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def key(self) -> tuple[str, str, str, str]:
|
|
242
|
+
return (self.location.name, self.rule_type.value, self.rulebase.value, self.name)
|
|
243
|
+
|
|
244
|
+
|
|
187
245
|
class Snapshot(BaseModel):
|
|
188
246
|
"""An immutable point-in-time view of the parts of a Panorama config
|
|
189
247
|
`psc` understands. Built by the XML parser or the live client; consumed
|
|
@@ -198,6 +256,8 @@ class Snapshot(BaseModel):
|
|
|
198
256
|
tags: list[Tag] = Field(default_factory=list)
|
|
199
257
|
security_rules: list[SecurityRule] = Field(default_factory=list)
|
|
200
258
|
nat_rules: list[NatRule] = Field(default_factory=list)
|
|
259
|
+
policy_rules: list[PolicyRule] = Field(default_factory=list)
|
|
260
|
+
"""Rules from the nine non-security/nat rulebases (see `RuleType`)."""
|
|
201
261
|
device_groups: list[str] = Field(default_factory=list)
|
|
202
262
|
device_group_parents: dict[str, str] = Field(default_factory=dict)
|
|
203
263
|
"""Child device-group name → its parent device-group name. A device-group
|
|
@@ -24,7 +24,9 @@ from psc.core.models import (
|
|
|
24
24
|
AddressType,
|
|
25
25
|
Location,
|
|
26
26
|
NatRule,
|
|
27
|
+
PolicyRule,
|
|
27
28
|
Rulebase,
|
|
29
|
+
RuleType,
|
|
28
30
|
SecurityRule,
|
|
29
31
|
Service,
|
|
30
32
|
ServiceGroup,
|
|
@@ -224,6 +226,40 @@ def _parse_nat_rules(parent: ET.Element, loc: Location, rb: Rulebase) -> list[Na
|
|
|
224
226
|
return out
|
|
225
227
|
|
|
226
228
|
|
|
229
|
+
def _parse_policy_rules(parent: ET.Element, loc: Location, rb: Rulebase) -> list[PolicyRule]:
|
|
230
|
+
"""Parse every "security-shaped" rulebase under one `<*-rulebase>` element.
|
|
231
|
+
|
|
232
|
+
Drives off `RuleType` (whose values are the container tags), so a new
|
|
233
|
+
rulebase needs only a new enum member. Reads the shared reference surface
|
|
234
|
+
(source/destination/service/tag); `service` stays empty when the rulebase
|
|
235
|
+
omits it (application-override). PBF additionally captures an object-named
|
|
236
|
+
forwarding next-hop (`action/forward/nexthop/fqdn`); a literal `ip-address`
|
|
237
|
+
next-hop names no object and is skipped.
|
|
238
|
+
"""
|
|
239
|
+
out: list[PolicyRule] = []
|
|
240
|
+
for rt in RuleType:
|
|
241
|
+
for entry in parent.findall(f"./{rt.value}/rules/entry"):
|
|
242
|
+
name = entry.get("name")
|
|
243
|
+
if not name:
|
|
244
|
+
continue
|
|
245
|
+
nexthop = _text(entry, "action/forward/nexthop/fqdn") if rt is RuleType.PBF else None
|
|
246
|
+
out.append(
|
|
247
|
+
PolicyRule(
|
|
248
|
+
name=name,
|
|
249
|
+
location=loc,
|
|
250
|
+
rulebase=rb,
|
|
251
|
+
rule_type=rt,
|
|
252
|
+
source=_member_list_or_any(entry, "source"),
|
|
253
|
+
destination=_member_list_or_any(entry, "destination"),
|
|
254
|
+
service=_members(entry, "service"),
|
|
255
|
+
nexthop=nexthop,
|
|
256
|
+
disabled=_text(entry, "disabled") == "yes",
|
|
257
|
+
tags=_members(entry, "tag"),
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
return out
|
|
261
|
+
|
|
262
|
+
|
|
227
263
|
def _collect(snap: Snapshot, parent: ET.Element, loc: Location) -> None:
|
|
228
264
|
snap.addresses.extend(_parse_addresses(parent, loc))
|
|
229
265
|
snap.address_groups.extend(_parse_address_groups(parent, loc))
|
|
@@ -235,6 +271,7 @@ def _collect(snap: Snapshot, parent: ET.Element, loc: Location) -> None:
|
|
|
235
271
|
if rb_el is not None:
|
|
236
272
|
snap.security_rules.extend(_parse_security_rules(rb_el, loc, rb))
|
|
237
273
|
snap.nat_rules.extend(_parse_nat_rules(rb_el, loc, rb))
|
|
274
|
+
snap.policy_rules.extend(_parse_policy_rules(rb_el, loc, rb))
|
|
238
275
|
|
|
239
276
|
|
|
240
277
|
def _find_config_root(root: ET.Element) -> ET.Element:
|