f5audit 0.1.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.
- f5audit-0.1.0/LICENSE +21 -0
- f5audit-0.1.0/MANIFEST.in +2 -0
- f5audit-0.1.0/PKG-INFO +184 -0
- f5audit-0.1.0/README.md +151 -0
- f5audit-0.1.0/f5audit/__init__.py +8 -0
- f5audit-0.1.0/f5audit/__main__.py +5 -0
- f5audit-0.1.0/f5audit/analyzer.py +301 -0
- f5audit-0.1.0/f5audit/cli.py +338 -0
- f5audit-0.1.0/f5audit/client.py +221 -0
- f5audit-0.1.0/f5audit/collector.py +304 -0
- f5audit-0.1.0/f5audit/correlator.py +108 -0
- f5audit-0.1.0/f5audit/models.py +104 -0
- f5audit-0.1.0/f5audit/parsing.py +385 -0
- f5audit-0.1.0/f5audit/report.py +407 -0
- f5audit-0.1.0/f5audit.egg-info/PKG-INFO +184 -0
- f5audit-0.1.0/f5audit.egg-info/SOURCES.txt +43 -0
- f5audit-0.1.0/f5audit.egg-info/dependency_links.txt +1 -0
- f5audit-0.1.0/f5audit.egg-info/entry_points.txt +2 -0
- f5audit-0.1.0/f5audit.egg-info/requires.txt +6 -0
- f5audit-0.1.0/f5audit.egg-info/top_level.txt +1 -0
- f5audit-0.1.0/pyproject.toml +60 -0
- f5audit-0.1.0/setup.cfg +4 -0
- f5audit-0.1.0/tests/__init__.py +0 -0
- f5audit-0.1.0/tests/conftest.py +63 -0
- f5audit-0.1.0/tests/fixtures/auth_partition.json +3 -0
- f5audit-0.1.0/tests/fixtures/cm_device.json +18 -0
- f5audit-0.1.0/tests/fixtures/monitors_http.json +5 -0
- f5audit-0.1.0/tests/fixtures/node_stats.json +24 -0
- f5audit-0.1.0/tests/fixtures/nodes.json +22 -0
- f5audit-0.1.0/tests/fixtures/pool_member_stats_pool-web.json +13 -0
- f5audit-0.1.0/tests/fixtures/pool_members_pool-web.json +12 -0
- f5audit-0.1.0/tests/fixtures/pools.json +32 -0
- f5audit-0.1.0/tests/fixtures/rules.json +9 -0
- f5audit-0.1.0/tests/fixtures/sys_failover_active.json +4 -0
- f5audit-0.1.0/tests/fixtures/sys_failover_standby.json +4 -0
- f5audit-0.1.0/tests/fixtures/sys_version.json +13 -0
- f5audit-0.1.0/tests/fixtures/virtual_stats.json +40 -0
- f5audit-0.1.0/tests/fixtures/virtual_stats_zero.json +37 -0
- f5audit-0.1.0/tests/fixtures/virtuals.json +30 -0
- f5audit-0.1.0/tests/test_analyzer.py +186 -0
- f5audit-0.1.0/tests/test_client.py +149 -0
- f5audit-0.1.0/tests/test_collector.py +137 -0
- f5audit-0.1.0/tests/test_correlator.py +71 -0
- f5audit-0.1.0/tests/test_parsing.py +158 -0
- f5audit-0.1.0/tests/test_report.py +124 -0
f5audit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Israel Fernandez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
f5audit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: f5audit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read-only audit tool for F5 BIG-IP LTM: finds unused nodes, pools, virtual servers and monitors via iControl REST and produces an Excel report.
|
|
5
|
+
Author-email: Israel Fernandez <israelfdzhdz@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/netcraftworks/f5audit
|
|
8
|
+
Project-URL: Repository, https://github.com/netcraftworks/f5audit
|
|
9
|
+
Project-URL: Issues, https://github.com/netcraftworks/f5audit/issues
|
|
10
|
+
Keywords: f5,big-ip,ltm,audit,icontrol-rest,network-automation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: System :: Networking
|
|
23
|
+
Classifier: Topic :: System :: Systems Administration
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: requests>=2.25
|
|
28
|
+
Requires-Dist: openpyxl>=3.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# f5audit
|
|
35
|
+
|
|
36
|
+
[](https://github.com/netcraftworks/f5audit/actions/workflows/ci.yml)
|
|
37
|
+
[](https://pypi.org/project/f5audit/)
|
|
38
|
+
[](https://pypi.org/project/f5audit/)
|
|
39
|
+
|
|
40
|
+
Read-only audit tool for **F5 BIG-IP LTM**. It collects configuration and
|
|
41
|
+
statistics via iControl REST (GET only), correlates object references, and
|
|
42
|
+
produces a multi-sheet **Excel report** identifying unused objects (nodes,
|
|
43
|
+
pools, virtual servers, monitors) as input for a human-driven, change-
|
|
44
|
+
controlled cleanup.
|
|
45
|
+
|
|
46
|
+
**This tool never modifies the device.** The "suggested command" columns in
|
|
47
|
+
the report are informational text only; nothing is ever executed.
|
|
48
|
+
|
|
49
|
+
## Safety design
|
|
50
|
+
|
|
51
|
+
- The HTTP client (`F5ReadOnlyClient`) exposes a single `get()` method.
|
|
52
|
+
There are no `post`/`patch`/`put`/`delete` methods. The only internal
|
|
53
|
+
write is the token login, hardcoded to `/mgmt/shared/authn/login`.
|
|
54
|
+
This is enforced by structural tests.
|
|
55
|
+
- No `tmsh`/bash execution endpoints are used or referenced anywhere.
|
|
56
|
+
- No explicit logout (deleting the token would be a write); tokens expire
|
|
57
|
+
on their own (~20 minutes).
|
|
58
|
+
- Passwords are read from an interactive prompt (`getpass`) or the
|
|
59
|
+
`F5_PASS` environment variable — never from a CLI argument, and never
|
|
60
|
+
written to disk or logs.
|
|
61
|
+
- Management-plane friendly: sequential requests only, `$top`/`$skip`
|
|
62
|
+
pagination, per-request delay (`--delay`, default 0.1 s), 30 s timeout,
|
|
63
|
+
max 2 retries with exponential backoff.
|
|
64
|
+
|
|
65
|
+
## Requirements
|
|
66
|
+
|
|
67
|
+
- Python >= 3.9, `requests`, `openpyxl`
|
|
68
|
+
- An account with a **read-only role** (Auditor/Guest) and iControl REST
|
|
69
|
+
access on the BIG-IP
|
|
70
|
+
- Network access to the management interface (TCP 443)
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
pip install f5audit
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
From source: `pip install .` — or without installing, from the project
|
|
79
|
+
directory: `python -m f5audit ...`
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
### 1. Validate access first
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
f5audit validate --host 192.0.2.1 --user auditor --insecure
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Probes login plus the key GET endpoints and prints a diagnosis per
|
|
90
|
+
response code (bad credentials, missing REST access, denied endpoints,
|
|
91
|
+
old BIG-IP versions, network timeouts).
|
|
92
|
+
|
|
93
|
+
### 2. Collect once, analyze offline N times
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
f5audit collect --host 192.0.2.1 --user auditor --insecure --save-raw ./raw/
|
|
97
|
+
f5audit analyze --from-raw ./raw/ --out report.xlsx
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`collect` saves every raw JSON response (with timestamps) to disk;
|
|
101
|
+
`analyze --from-raw` re-analyzes from that cache **without touching the
|
|
102
|
+
F5 again**. This is the recommended workflow: one collection per session,
|
|
103
|
+
all further analysis offline.
|
|
104
|
+
|
|
105
|
+
### One-step alternative
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
f5audit analyze --host 192.0.2.1 --user auditor --insecure --save-raw ./raw/ --out report.xlsx
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Options
|
|
112
|
+
|
|
113
|
+
| Flag | Meaning |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `--user` / `F5_USER` | Username (password via prompt or `F5_PASS`) |
|
|
116
|
+
| `--login-provider` | Token auth provider (default `tmos`; set for TACACS+/RADIUS) |
|
|
117
|
+
| `--insecure` | Skip TLS verification (self-signed mgmt certs); prints a warning |
|
|
118
|
+
| `--delay` | Seconds between requests (default 0.1) |
|
|
119
|
+
| `--top` | Pagination page size (default 100) |
|
|
120
|
+
| `--format xlsx\|csv` | Excel workbook or one CSV per sheet |
|
|
121
|
+
| `--allow-standby` | On a standby unit, emit traffic verdicts marked `UNRELIABLE (standby)` instead of skipping them |
|
|
122
|
+
|
|
123
|
+
Exit codes: `0` OK · `1` connection/auth error · `2` analysis completed
|
|
124
|
+
with warnings (standby device, denied partitions, missing endpoints).
|
|
125
|
+
|
|
126
|
+
## Verdicts
|
|
127
|
+
|
|
128
|
+
| Verdict | Meaning |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `ORPHAN` | Not referenced by anything (node: no pool membership; pool: no VS/iRule/policy reference; monitor: no user). Only issued when the inventory is complete and no dynamic iRules are active. |
|
|
131
|
+
| `MANUAL REVIEW` | A dynamic iRule (`pool $var`, `pool [...]`, datagroups) or a missing `ltm/rule` endpoint means the object *could* be referenced at runtime. Never auto-cleanup these. |
|
|
132
|
+
| `INACTIVE` | Configured and referenced, but disabled or zero total connections since the last counter reset. |
|
|
133
|
+
| `UNRELIABLE (standby)` | Traffic-based verdict computed on a standby unit (only with `--allow-standby`). |
|
|
134
|
+
| `UNRELIABLE (incomplete inventory)` | Some partitions were not readable; a reference could exist in an invisible partition. |
|
|
135
|
+
| `IN USE` | Everything else. |
|
|
136
|
+
|
|
137
|
+
## Operational warning
|
|
138
|
+
|
|
139
|
+
- **Collect on the ACTIVE unit** of the HA pair. On a standby unit traffic
|
|
140
|
+
counters are zeros and the tool will skip traffic analysis (or mark it
|
|
141
|
+
`UNRELIABLE` with `--allow-standby`).
|
|
142
|
+
- Traffic counters reset on reboot / stats reset. Ideally collect after
|
|
143
|
+
**several weeks of uptime**; the report includes the failover-state age
|
|
144
|
+
as context.
|
|
145
|
+
- `INACTIVE` means "no traffic since the counters started", not "safe to
|
|
146
|
+
delete". Use the planned `compare` workflow (v2) — two collections some
|
|
147
|
+
weeks apart — to distinguish real zero traffic from a recent reset.
|
|
148
|
+
The raw cache already stores per-file timestamps to enable this.
|
|
149
|
+
|
|
150
|
+
## Report sheets
|
|
151
|
+
|
|
152
|
+
1. **Summary** — hostname, version, HA state, uptime context, partitions,
|
|
153
|
+
verdict counts, active warnings.
|
|
154
|
+
2. **Inventory** — one row per pool member (plus rows for pool-less
|
|
155
|
+
nodes), fully correlated: node ↔ pool ↔ virtual server ↔ monitor ↔
|
|
156
|
+
iRule/policy references, statuses, traffic and verdict.
|
|
157
|
+
3. **Orphan Nodes** · 4. **Orphan-Inactive Pools** · 5. **Inactive
|
|
158
|
+
Virtual Servers** · 6. **Orphan Monitors** — filtered views with
|
|
159
|
+
informational `tmsh` commands for the change request.
|
|
160
|
+
7. **Manual Review** — objects touched by dynamic logic, with the
|
|
161
|
+
iRule/policy that causes the doubt.
|
|
162
|
+
|
|
163
|
+
Color coding: red = ORPHAN · yellow = MANUAL REVIEW / UNRELIABLE ·
|
|
164
|
+
orange = INACTIVE · green = IN USE.
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
pip install -e ".[dev]"
|
|
170
|
+
ruff check f5audit tests && ruff format --check f5audit tests
|
|
171
|
+
pytest
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
No test touches the network; everything runs from anonymized JSON
|
|
175
|
+
fixtures and mocked HTTP sessions. Structural tests assert the client
|
|
176
|
+
exposes no write verbs and that no forbidden endpoint appears in the
|
|
177
|
+
source. CI runs lint plus the test suite on Python 3.9 through 3.14;
|
|
178
|
+
all checks must pass before a PR can merge.
|
|
179
|
+
|
|
180
|
+
Releases are published to PyPI automatically: bump `__version__` in
|
|
181
|
+
`f5audit/__init__.py`, merge, and create a GitHub release tagged
|
|
182
|
+
`v<version>`. The release workflow verifies the tag matches the package
|
|
183
|
+
version, builds, and publishes via PyPI Trusted Publishing (no stored
|
|
184
|
+
API tokens).
|
f5audit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# f5audit
|
|
2
|
+
|
|
3
|
+
[](https://github.com/netcraftworks/f5audit/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/f5audit/)
|
|
5
|
+
[](https://pypi.org/project/f5audit/)
|
|
6
|
+
|
|
7
|
+
Read-only audit tool for **F5 BIG-IP LTM**. It collects configuration and
|
|
8
|
+
statistics via iControl REST (GET only), correlates object references, and
|
|
9
|
+
produces a multi-sheet **Excel report** identifying unused objects (nodes,
|
|
10
|
+
pools, virtual servers, monitors) as input for a human-driven, change-
|
|
11
|
+
controlled cleanup.
|
|
12
|
+
|
|
13
|
+
**This tool never modifies the device.** The "suggested command" columns in
|
|
14
|
+
the report are informational text only; nothing is ever executed.
|
|
15
|
+
|
|
16
|
+
## Safety design
|
|
17
|
+
|
|
18
|
+
- The HTTP client (`F5ReadOnlyClient`) exposes a single `get()` method.
|
|
19
|
+
There are no `post`/`patch`/`put`/`delete` methods. The only internal
|
|
20
|
+
write is the token login, hardcoded to `/mgmt/shared/authn/login`.
|
|
21
|
+
This is enforced by structural tests.
|
|
22
|
+
- No `tmsh`/bash execution endpoints are used or referenced anywhere.
|
|
23
|
+
- No explicit logout (deleting the token would be a write); tokens expire
|
|
24
|
+
on their own (~20 minutes).
|
|
25
|
+
- Passwords are read from an interactive prompt (`getpass`) or the
|
|
26
|
+
`F5_PASS` environment variable — never from a CLI argument, and never
|
|
27
|
+
written to disk or logs.
|
|
28
|
+
- Management-plane friendly: sequential requests only, `$top`/`$skip`
|
|
29
|
+
pagination, per-request delay (`--delay`, default 0.1 s), 30 s timeout,
|
|
30
|
+
max 2 retries with exponential backoff.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Python >= 3.9, `requests`, `openpyxl`
|
|
35
|
+
- An account with a **read-only role** (Auditor/Guest) and iControl REST
|
|
36
|
+
access on the BIG-IP
|
|
37
|
+
- Network access to the management interface (TCP 443)
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
pip install f5audit
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
From source: `pip install .` — or without installing, from the project
|
|
46
|
+
directory: `python -m f5audit ...`
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### 1. Validate access first
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
f5audit validate --host 192.0.2.1 --user auditor --insecure
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Probes login plus the key GET endpoints and prints a diagnosis per
|
|
57
|
+
response code (bad credentials, missing REST access, denied endpoints,
|
|
58
|
+
old BIG-IP versions, network timeouts).
|
|
59
|
+
|
|
60
|
+
### 2. Collect once, analyze offline N times
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
f5audit collect --host 192.0.2.1 --user auditor --insecure --save-raw ./raw/
|
|
64
|
+
f5audit analyze --from-raw ./raw/ --out report.xlsx
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`collect` saves every raw JSON response (with timestamps) to disk;
|
|
68
|
+
`analyze --from-raw` re-analyzes from that cache **without touching the
|
|
69
|
+
F5 again**. This is the recommended workflow: one collection per session,
|
|
70
|
+
all further analysis offline.
|
|
71
|
+
|
|
72
|
+
### One-step alternative
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
f5audit analyze --host 192.0.2.1 --user auditor --insecure --save-raw ./raw/ --out report.xlsx
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Options
|
|
79
|
+
|
|
80
|
+
| Flag | Meaning |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `--user` / `F5_USER` | Username (password via prompt or `F5_PASS`) |
|
|
83
|
+
| `--login-provider` | Token auth provider (default `tmos`; set for TACACS+/RADIUS) |
|
|
84
|
+
| `--insecure` | Skip TLS verification (self-signed mgmt certs); prints a warning |
|
|
85
|
+
| `--delay` | Seconds between requests (default 0.1) |
|
|
86
|
+
| `--top` | Pagination page size (default 100) |
|
|
87
|
+
| `--format xlsx\|csv` | Excel workbook or one CSV per sheet |
|
|
88
|
+
| `--allow-standby` | On a standby unit, emit traffic verdicts marked `UNRELIABLE (standby)` instead of skipping them |
|
|
89
|
+
|
|
90
|
+
Exit codes: `0` OK · `1` connection/auth error · `2` analysis completed
|
|
91
|
+
with warnings (standby device, denied partitions, missing endpoints).
|
|
92
|
+
|
|
93
|
+
## Verdicts
|
|
94
|
+
|
|
95
|
+
| Verdict | Meaning |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `ORPHAN` | Not referenced by anything (node: no pool membership; pool: no VS/iRule/policy reference; monitor: no user). Only issued when the inventory is complete and no dynamic iRules are active. |
|
|
98
|
+
| `MANUAL REVIEW` | A dynamic iRule (`pool $var`, `pool [...]`, datagroups) or a missing `ltm/rule` endpoint means the object *could* be referenced at runtime. Never auto-cleanup these. |
|
|
99
|
+
| `INACTIVE` | Configured and referenced, but disabled or zero total connections since the last counter reset. |
|
|
100
|
+
| `UNRELIABLE (standby)` | Traffic-based verdict computed on a standby unit (only with `--allow-standby`). |
|
|
101
|
+
| `UNRELIABLE (incomplete inventory)` | Some partitions were not readable; a reference could exist in an invisible partition. |
|
|
102
|
+
| `IN USE` | Everything else. |
|
|
103
|
+
|
|
104
|
+
## Operational warning
|
|
105
|
+
|
|
106
|
+
- **Collect on the ACTIVE unit** of the HA pair. On a standby unit traffic
|
|
107
|
+
counters are zeros and the tool will skip traffic analysis (or mark it
|
|
108
|
+
`UNRELIABLE` with `--allow-standby`).
|
|
109
|
+
- Traffic counters reset on reboot / stats reset. Ideally collect after
|
|
110
|
+
**several weeks of uptime**; the report includes the failover-state age
|
|
111
|
+
as context.
|
|
112
|
+
- `INACTIVE` means "no traffic since the counters started", not "safe to
|
|
113
|
+
delete". Use the planned `compare` workflow (v2) — two collections some
|
|
114
|
+
weeks apart — to distinguish real zero traffic from a recent reset.
|
|
115
|
+
The raw cache already stores per-file timestamps to enable this.
|
|
116
|
+
|
|
117
|
+
## Report sheets
|
|
118
|
+
|
|
119
|
+
1. **Summary** — hostname, version, HA state, uptime context, partitions,
|
|
120
|
+
verdict counts, active warnings.
|
|
121
|
+
2. **Inventory** — one row per pool member (plus rows for pool-less
|
|
122
|
+
nodes), fully correlated: node ↔ pool ↔ virtual server ↔ monitor ↔
|
|
123
|
+
iRule/policy references, statuses, traffic and verdict.
|
|
124
|
+
3. **Orphan Nodes** · 4. **Orphan-Inactive Pools** · 5. **Inactive
|
|
125
|
+
Virtual Servers** · 6. **Orphan Monitors** — filtered views with
|
|
126
|
+
informational `tmsh` commands for the change request.
|
|
127
|
+
7. **Manual Review** — objects touched by dynamic logic, with the
|
|
128
|
+
iRule/policy that causes the doubt.
|
|
129
|
+
|
|
130
|
+
Color coding: red = ORPHAN · yellow = MANUAL REVIEW / UNRELIABLE ·
|
|
131
|
+
orange = INACTIVE · green = IN USE.
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
pip install -e ".[dev]"
|
|
137
|
+
ruff check f5audit tests && ruff format --check f5audit tests
|
|
138
|
+
pytest
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
No test touches the network; everything runs from anonymized JSON
|
|
142
|
+
fixtures and mocked HTTP sessions. Structural tests assert the client
|
|
143
|
+
exposes no write verbs and that no forbidden endpoint appears in the
|
|
144
|
+
source. CI runs lint plus the test suite on Python 3.9 through 3.14;
|
|
145
|
+
all checks must pass before a PR can merge.
|
|
146
|
+
|
|
147
|
+
Releases are published to PyPI automatically: bump `__version__` in
|
|
148
|
+
`f5audit/__init__.py`, merge, and create a GitHub release tagged
|
|
149
|
+
`v<version>`. The release workflow verifies the tag matches the package
|
|
150
|
+
version, builds, and publishes via PyPI Trusted Publishing (no stored
|
|
151
|
+
API tokens).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""f5audit: read-only audit tool for F5 BIG-IP LTM configurations.
|
|
2
|
+
|
|
3
|
+
Collects configuration and statistics via iControl REST (GET only),
|
|
4
|
+
correlates object references, and reports orphaned / inactive objects
|
|
5
|
+
as input for a human-driven, change-controlled cleanup.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Verdict rules.
|
|
2
|
+
|
|
3
|
+
Cross-cutting rules (spec section 8):
|
|
4
|
+
- No object touched by dynamic pool-selection logic can ever be ORPHAN;
|
|
5
|
+
the ceiling is MANUAL REVIEW.
|
|
6
|
+
- Traffic-based verdicts are only trusted when the device is ACTIVE;
|
|
7
|
+
on a standby unit they are either skipped (default) or emitted as
|
|
8
|
+
UNRELIABLE (standby) with --allow-standby.
|
|
9
|
+
- Incomplete inventory (denied partitions/endpoints) degrades orphan
|
|
10
|
+
verdicts, because a reference could live in an invisible partition.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
from .correlator import Correlation, is_builtin_monitor
|
|
18
|
+
from .parsing import ParsedData
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Verdict:
|
|
22
|
+
ORPHAN = "ORPHAN"
|
|
23
|
+
MANUAL_REVIEW = "MANUAL REVIEW"
|
|
24
|
+
INACTIVE = "INACTIVE"
|
|
25
|
+
UNRELIABLE_STANDBY = "UNRELIABLE (standby)"
|
|
26
|
+
UNRELIABLE_INVENTORY = "UNRELIABLE (incomplete inventory)"
|
|
27
|
+
IN_USE = "IN USE"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ObjectVerdict:
|
|
32
|
+
verdict: str
|
|
33
|
+
notes: str = ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ManualReviewItem:
|
|
38
|
+
object_type: str
|
|
39
|
+
full_path: str
|
|
40
|
+
reason: str
|
|
41
|
+
caused_by: str = ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class AnalysisResult:
|
|
46
|
+
node_verdicts: dict[str, ObjectVerdict] = field(default_factory=dict)
|
|
47
|
+
pool_verdicts: dict[str, ObjectVerdict] = field(default_factory=dict)
|
|
48
|
+
virtual_verdicts: dict[str, ObjectVerdict] = field(default_factory=dict)
|
|
49
|
+
monitor_verdicts: dict[str, ObjectVerdict] = field(default_factory=dict)
|
|
50
|
+
manual_review: list[ManualReviewItem] = field(default_factory=list)
|
|
51
|
+
warnings: list[str] = field(default_factory=list)
|
|
52
|
+
stats_analysis_skipped: bool = False
|
|
53
|
+
|
|
54
|
+
def verdict_counts(self) -> dict[str, int]:
|
|
55
|
+
counts: dict[str, int] = {}
|
|
56
|
+
for verdicts in (
|
|
57
|
+
self.node_verdicts,
|
|
58
|
+
self.pool_verdicts,
|
|
59
|
+
self.virtual_verdicts,
|
|
60
|
+
self.monitor_verdicts,
|
|
61
|
+
):
|
|
62
|
+
for object_verdict in verdicts.values():
|
|
63
|
+
counts[object_verdict.verdict] = counts.get(object_verdict.verdict, 0) + 1
|
|
64
|
+
return counts
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _conns_zero(total_conns: int | None) -> bool:
|
|
68
|
+
return total_conns is not None and int(total_conns) == 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Analyzer:
|
|
72
|
+
def __init__(
|
|
73
|
+
self, parsed: ParsedData, correlation: Correlation, *, allow_standby: bool = False
|
|
74
|
+
):
|
|
75
|
+
self.parsed = parsed
|
|
76
|
+
self.correlation = correlation
|
|
77
|
+
self.allow_standby = allow_standby
|
|
78
|
+
|
|
79
|
+
system = parsed.system
|
|
80
|
+
self.is_standby = system.failover_state == "standby"
|
|
81
|
+
self.inventory_complete = not system.partitions_denied and not any(
|
|
82
|
+
"/ltm/node" in ep or "/ltm/pool" in ep or "/ltm/virtual" in ep
|
|
83
|
+
for ep in system.missing_endpoints
|
|
84
|
+
)
|
|
85
|
+
self.irules_available = not any("/ltm/rule" in ep for ep in system.missing_endpoints)
|
|
86
|
+
# Uptime context: traffic counters reset on reboot / stats reset.
|
|
87
|
+
self.traffic_note = (
|
|
88
|
+
"Traffic counters reset on reboot/stats-reset; "
|
|
89
|
+
f"{system.uptime or 'device uptime unknown'}."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def run(self) -> AnalysisResult:
|
|
95
|
+
result = AnalysisResult()
|
|
96
|
+
self._collect_warnings(result)
|
|
97
|
+
self._analyze_nodes(result)
|
|
98
|
+
self._analyze_virtuals(result)
|
|
99
|
+
self._analyze_pools(result)
|
|
100
|
+
self._analyze_monitors(result)
|
|
101
|
+
self._analyze_irules(result)
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
def _collect_warnings(self, result: AnalysisResult) -> None:
|
|
105
|
+
system = self.parsed.system
|
|
106
|
+
if self.is_standby:
|
|
107
|
+
if self.allow_standby:
|
|
108
|
+
result.warnings.append(
|
|
109
|
+
"Device is STANDBY: traffic statistics are not "
|
|
110
|
+
"representative. Traffic-based verdicts are marked "
|
|
111
|
+
"UNRELIABLE (standby). Re-run against the ACTIVE unit."
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
result.stats_analysis_skipped = True
|
|
115
|
+
result.warnings.append(
|
|
116
|
+
"Device is STANDBY: traffic-based analysis was SKIPPED "
|
|
117
|
+
"(configuration-orphan analysis still ran). Re-run "
|
|
118
|
+
"against the ACTIVE unit, or use --allow-standby to "
|
|
119
|
+
"force traffic verdicts marked as UNRELIABLE."
|
|
120
|
+
)
|
|
121
|
+
if system.partitions_denied:
|
|
122
|
+
result.warnings.append(
|
|
123
|
+
"Access denied to partition(s): "
|
|
124
|
+
+ ", ".join(system.partitions_denied)
|
|
125
|
+
+ ". Orphan verdicts are degraded to UNRELIABLE (incomplete "
|
|
126
|
+
"inventory) because objects could be referenced from an "
|
|
127
|
+
"invisible partition."
|
|
128
|
+
)
|
|
129
|
+
if not self.irules_available:
|
|
130
|
+
result.warnings.append(
|
|
131
|
+
"ltm/rule was not readable: pool orphan verdicts are capped "
|
|
132
|
+
"at MANUAL REVIEW (an iRule could reference them)."
|
|
133
|
+
)
|
|
134
|
+
for endpoint in system.missing_endpoints:
|
|
135
|
+
if "/ltm/rule" not in endpoint:
|
|
136
|
+
result.warnings.append(f"Endpoint not collected: {endpoint}")
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def _analyze_nodes(self, result: AnalysisResult) -> None:
|
|
141
|
+
for path, _node in self.parsed.nodes.items():
|
|
142
|
+
pools = self.correlation.node_to_pools.get(path)
|
|
143
|
+
if pools:
|
|
144
|
+
result.node_verdicts[path] = ObjectVerdict(Verdict.IN_USE)
|
|
145
|
+
elif not self.inventory_complete:
|
|
146
|
+
result.node_verdicts[path] = ObjectVerdict(
|
|
147
|
+
Verdict.UNRELIABLE_INVENTORY,
|
|
148
|
+
"Not a member of any visible pool, but some partitions were not readable.",
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
result.node_verdicts[path] = ObjectVerdict(
|
|
152
|
+
Verdict.ORPHAN, "Not a member of any pool."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _analyze_virtuals(self, result: AnalysisResult) -> None:
|
|
156
|
+
for path, virtual in self.parsed.virtuals.items():
|
|
157
|
+
if virtual.admin_state == "disabled":
|
|
158
|
+
result.virtual_verdicts[path] = ObjectVerdict(
|
|
159
|
+
Verdict.INACTIVE, "Administratively disabled."
|
|
160
|
+
)
|
|
161
|
+
continue
|
|
162
|
+
if not virtual.default_pool and (virtual.irules or virtual.policies):
|
|
163
|
+
result.virtual_verdicts[path] = ObjectVerdict(
|
|
164
|
+
Verdict.MANUAL_REVIEW,
|
|
165
|
+
"No default pool; traffic is steered by iRules/policies.",
|
|
166
|
+
)
|
|
167
|
+
result.manual_review.append(
|
|
168
|
+
ManualReviewItem(
|
|
169
|
+
"virtual_server",
|
|
170
|
+
path,
|
|
171
|
+
"No default pool but has iRules/policies attached",
|
|
172
|
+
", ".join(virtual.irules + virtual.policies),
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
continue
|
|
176
|
+
if _conns_zero(virtual.total_conns):
|
|
177
|
+
self._traffic_verdict(
|
|
178
|
+
result.virtual_verdicts,
|
|
179
|
+
path,
|
|
180
|
+
"Enabled but zero total connections. " + self.traffic_note,
|
|
181
|
+
)
|
|
182
|
+
continue
|
|
183
|
+
result.virtual_verdicts[path] = ObjectVerdict(Verdict.IN_USE)
|
|
184
|
+
|
|
185
|
+
def _traffic_verdict(self, verdicts: dict[str, ObjectVerdict], path: str, note: str) -> None:
|
|
186
|
+
"""Emit a traffic-based INACTIVE verdict, degraded on standby."""
|
|
187
|
+
if self.is_standby:
|
|
188
|
+
if self.allow_standby:
|
|
189
|
+
verdicts[path] = ObjectVerdict(
|
|
190
|
+
Verdict.UNRELIABLE_STANDBY,
|
|
191
|
+
"Zero traffic, but this device is standby. " + note,
|
|
192
|
+
)
|
|
193
|
+
else:
|
|
194
|
+
verdicts[path] = ObjectVerdict(
|
|
195
|
+
Verdict.IN_USE,
|
|
196
|
+
"Traffic analysis skipped (standby device).",
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
verdicts[path] = ObjectVerdict(Verdict.INACTIVE, note)
|
|
200
|
+
|
|
201
|
+
def _analyze_pools(self, result: AnalysisResult) -> None:
|
|
202
|
+
for path, _pool in self.parsed.pools.items():
|
|
203
|
+
virtuals = self.correlation.pool_to_virtuals.get(path, set())
|
|
204
|
+
irule_refs = self.correlation.pool_to_irules.get(path, set())
|
|
205
|
+
policy_refs = self.correlation.pool_to_policies.get(path, set())
|
|
206
|
+
|
|
207
|
+
if not virtuals and not irule_refs and not policy_refs:
|
|
208
|
+
self._unreferenced_pool_verdict(result, path)
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
# Referenced pool: inactive if every attached VS has zero traffic.
|
|
212
|
+
if (
|
|
213
|
+
virtuals
|
|
214
|
+
and all(
|
|
215
|
+
_conns_zero(self.parsed.virtuals[v].total_conns)
|
|
216
|
+
for v in virtuals
|
|
217
|
+
if v in self.parsed.virtuals
|
|
218
|
+
)
|
|
219
|
+
and any(v in self.parsed.virtuals for v in virtuals)
|
|
220
|
+
):
|
|
221
|
+
self._traffic_verdict(
|
|
222
|
+
result.pool_verdicts,
|
|
223
|
+
path,
|
|
224
|
+
"All attached virtual servers have zero total "
|
|
225
|
+
"connections. " + self.traffic_note,
|
|
226
|
+
)
|
|
227
|
+
continue
|
|
228
|
+
result.pool_verdicts[path] = ObjectVerdict(Verdict.IN_USE)
|
|
229
|
+
|
|
230
|
+
def _unreferenced_pool_verdict(self, result: AnalysisResult, path: str) -> None:
|
|
231
|
+
if not self.inventory_complete:
|
|
232
|
+
result.pool_verdicts[path] = ObjectVerdict(
|
|
233
|
+
Verdict.UNRELIABLE_INVENTORY,
|
|
234
|
+
"No visible references, but some partitions were not readable.",
|
|
235
|
+
)
|
|
236
|
+
elif not self.irules_available:
|
|
237
|
+
result.pool_verdicts[path] = ObjectVerdict(
|
|
238
|
+
Verdict.MANUAL_REVIEW,
|
|
239
|
+
"No VS/policy references; iRules could not be read "
|
|
240
|
+
"(ltm/rule denied), so an iRule reference cannot be ruled out.",
|
|
241
|
+
)
|
|
242
|
+
result.manual_review.append(
|
|
243
|
+
ManualReviewItem(
|
|
244
|
+
"pool",
|
|
245
|
+
path,
|
|
246
|
+
"Unreferenced, but ltm/rule was not readable",
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
elif self.correlation.has_attached_dynamic_irules:
|
|
250
|
+
dynamic = ", ".join(self.correlation.attached_dynamic_irules)
|
|
251
|
+
result.pool_verdicts[path] = ObjectVerdict(
|
|
252
|
+
Verdict.MANUAL_REVIEW,
|
|
253
|
+
"No static references, but dynamic pool-selection iRules "
|
|
254
|
+
f"are active ({dynamic}); the pool could be selected at "
|
|
255
|
+
"runtime.",
|
|
256
|
+
)
|
|
257
|
+
result.manual_review.append(
|
|
258
|
+
ManualReviewItem(
|
|
259
|
+
"pool",
|
|
260
|
+
path,
|
|
261
|
+
"No static references but dynamic iRules are active",
|
|
262
|
+
dynamic,
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
result.pool_verdicts[path] = ObjectVerdict(
|
|
267
|
+
Verdict.ORPHAN,
|
|
268
|
+
"Not referenced by any virtual server, iRule or policy.",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def _analyze_monitors(self, result: AnalysisResult) -> None:
|
|
272
|
+
for path, _monitor in self.parsed.monitors.items():
|
|
273
|
+
if is_builtin_monitor(path):
|
|
274
|
+
result.monitor_verdicts[path] = ObjectVerdict(
|
|
275
|
+
Verdict.IN_USE, "F5 built-in monitor (excluded from orphan analysis)."
|
|
276
|
+
)
|
|
277
|
+
continue
|
|
278
|
+
users = self.correlation.monitor_users.get(path)
|
|
279
|
+
if users:
|
|
280
|
+
result.monitor_verdicts[path] = ObjectVerdict(Verdict.IN_USE)
|
|
281
|
+
elif not self.inventory_complete:
|
|
282
|
+
result.monitor_verdicts[path] = ObjectVerdict(
|
|
283
|
+
Verdict.UNRELIABLE_INVENTORY,
|
|
284
|
+
"Unused in visible partitions, but some partitions were not readable.",
|
|
285
|
+
)
|
|
286
|
+
else:
|
|
287
|
+
result.monitor_verdicts[path] = ObjectVerdict(
|
|
288
|
+
Verdict.ORPHAN, "Not used by any node or pool."
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def _analyze_irules(self, result: AnalysisResult) -> None:
|
|
292
|
+
for path, irule in self.parsed.irules.items():
|
|
293
|
+
if irule.has_dynamic_pool_selection:
|
|
294
|
+
result.manual_review.append(
|
|
295
|
+
ManualReviewItem(
|
|
296
|
+
"irule",
|
|
297
|
+
path,
|
|
298
|
+
"Selects pools dynamically ($variable, [command] or "
|
|
299
|
+
"datagroup); static analysis cannot resolve its targets",
|
|
300
|
+
)
|
|
301
|
+
)
|