The controller said zero firewall rules. The dashboard showed a policy table with 109 entries. Only one of those was true, and the API response was the wrong one.

That is the argument for auditing by API rather than by screenshot: the API is the source of truth, but only if you ask the right endpoint and check the answer against reality.

Read-only first, and only

An audit account should have the smallest permission that produces the data:

Permission Why
Read network configuration Networks, WLANs, policies, devices
Read site statistics Clients, health, events
No write scope A script with write access will eventually write

If the platform cannot express read-only at the API level, do not automate with that account. The first time a loop runs twice, you will learn why.

Ask the endpoint that answers

Every controller has legacy endpoints that return plausible, empty results. UniFi Network 9 is the example in front of us:

Question Endpoint Reality
Firewall rules /rest/firewallrule Returns [] — legacy
Firewall policies /proxy/network/v2/api/site/default/firewall-policies 109 policies
Networks /rest/networkconf Correct
Devices /stat/device Correct

The lesson generalizes: a 200 response with an empty list is not proof of an empty configuration. Check one known object before trusting any endpoint.

The snapshot pattern

1. Authenticate (cookie + CSRF token).
2. GET each endpoint, save the raw JSON under a timestamped directory.
3. Compute a summary: counts, isolated networks, policy hits, device firmware.
4. Diff against the previous snapshot.
5. Store 90 days; alert on unexpected differences.

A snapshot is not a backup. It is evidence: what the network looked like at a moment, in a form you can compare and search.

Read-only audit: collect, summarise, diff, and alert on what changed.

Worked example: the minimum useful script

import json, requests

BASE = "https://10.0.0.46"
s = requests.Session()
s.post(f"{BASE}/api/auth/login", json={"username": USER, "password": PASS}, verify=False)
s.headers["X-CSRF-Token"] = s.cookies.get("csrf_token", "")

endpoints = {
    "networks": "/proxy/network/api/s/default/rest/networkconf",
    "devices": "/proxy/network/api/s/default/stat/device",
    "wlans": "/proxy/network/api/s/default/rest/wlanconf",
    "policies": "/proxy/network/v2/api/site/default/firewall-policies",
}
out = {}
for name, path in endpoints.items():
    r = s.get(f"{BASE}{path}", verify=False)
    out[name] = r.json()
json.dump(out, open("snapshot.json", "w"), indent=1)

Then assert on the result: len(out["policies"]) > 0, or the script has failed silently and should say so rather than producing an empty report.

What to diff

Change Why it matters
New network or VLAN Undocumented address plan growth
Policy added/removed/reordered The most consequential change in the system
WLAN security change PSK rotation or a downgrade
Firmware version change Explains behavior changes after an auto-update
Device offline for >24h Inventory drift

A weekly diff of those five items is a security control and a documentation check at the same time.

Never point an audit script at production with write scope

The account is the safety mechanism. Keep it read-only at the controller, not by convention in the script, and never store its credentials in the repository.

What breaks in production

  • Trusting a 200 with an empty body. The endpoint changed, or you are calling the legacy path.
  • Pagination. Some endpoints return the first page and a cursor; the rest is silently missing.
  • API version drift after a controller upgrade. Fields move; the script keeps running and produces wrong summaries.
  • Credentials in code. Once committed, rotate.
  • No assertion on the output. An empty snapshot looks like a healthy, quiet network.

Checklist

  1. The audit account is read-only at the controller.
  2. Every endpoint used has been validated against a known object.
  3. Snapshots are timestamped, retained, and diffed.
  4. The script asserts on expected counts before reporting.
  5. Credentials live outside the repository and are rotated on a schedule.

Next

Auditing tells you what changed. Continue with Change Windows and Blast Radius to decide how to change it safely.

  • API
  • Auditing
  • UniFi
  • Automation
  • Drift