Checking before you delete something
Every configuration change on a firewall is preceded by the same question: what
breaks if I touch this. find_references answers it in one call.
Is the address object DMZ-SERVERS safe to delete?
The answer comes from the appliance’s own reference lookup — the same one the web UI’s reference counter uses — rather than from guessing at a handful of tables. That matters more than it sounds: on FortiOS 7.0.14 there are 234 tables that can hold a reference to an interface and 74 for a firewall address, so any hand-rolled scan is checking a rounding error’s worth of them.
Alongside the authoritative answer, the tool still reads policies, groups, virtual IPs, and static routes directly, because those carry readable detail the lookup does not — a policy’s name, whether it is on, whether it accepts or denies.
Read the verdict, not the count
Section titled “Read the verdict, not the count”{ "object": "DMZ-SERVERS", "verdict": "referenced", "resolved_as": ["address"], "total_references": 3, "candidate_tables": 74, "references": [ { "table": "firewall.policy", "object": "9", "attribute": "dstaddr" }, { "table": "firewall.addrgrp", "object": "ALL-SERVERS", "attribute": "member" } ], "sources_checked": { "object_usage": "ok", "policies": "ok", "address_groups": "ok", "service_groups": "ok", "vips": "ok", "routes": "ok" }, "safe_to_delete": false, "policies": [ { "id": 9, "name": "block-legacy-smb", "enabled": true, "action": "deny", "referenced_as": ["destination"] }, { "id": 12, "name": "dmz-mgmt", "enabled": false, "action": "accept", "referenced_as": ["destination", "source"] } ], "groups": [{ "name": "ALL-SERVERS", "kind": "address_group" }], "vips": [], "routes": []}verdict has five values and it is the field to read. safe_to_delete is
present only on referenced and no_references, because a lookup that could
not run cannot support a conclusion either way. The
other three are below.
references is the authoritative half and policies / groups / vips /
routes are the readable half, so the same reference can legitimately appear
in both — once as a row naming the table and attribute, once with the policy’s
name and action attached.
referenced_as names the field the object turned up in, so a policy listing it
as both source and destination reports both rather than being counted twice.
Disabled policies count. Policy 12 above is off, and it is still a reference. A disabled policy is one somebody intends to turn back on, and deleting the object underneath it turns that into a surprise at the worst possible moment.
Group membership is not expanded. DMZ-SERVERS is in ALL-SERVERS.
Anything referencing ALL-SERVERS therefore depends on DMZ-SERVERS
indirectly, and none of those policies appear in this result — they reference
the group, not the object. The tool does not follow the chain for you. When
groups is non-empty, run find_references again on each group name, and
check that the model actually did rather than assuming it would.
The three answers that are not answers
Section titled “The three answers that are not answers”safe_to_delete appears on referenced and no_references only. The other
three verdicts omit it, because each one means the question was not actually
settled — and omitting the key forces you to read the verdict, where a false
would invite you to stop.
object_not_found
Section titled “object_not_found”No address, group, service, virtual IP, or interface carries that name.
{ "object": "DMZ-SERVRES", "verdict": "object_not_found", "resolved_as": [], "total_references": 0, "note": "No address, group, service, virtual IP, or interface named 'DMZ-SERVRES' exists on this appliance, so this answer is about a name rather than an object. Check the spelling."}Look closely at the object name. Without this verdict, a typo reports zero
references and safe_to_delete: true — a confident yes to a question nobody
actually asked. This is the cheapest possible bug to hit and one of the more
expensive ones to act on.
no_references_in_checked_scopes
Section titled “no_references_in_checked_scopes”The appliance’s own lookup was unavailable, the fallback scan of four tables ran, and it found nothing.
{ "object": "DMZ-SERVERS", "verdict": "no_references_in_checked_scopes", "total_references": 0, "sources_checked": { "object_usage": "denied: http=403", "policies": "ok" }, "note": "The appliance's own reference lookup was unavailable, so this covers only policies, groups, virtual IPs, and static routes. FortiOS reports many more tables that can hold a reference, and they were not consulted."}This is a fact about four tables, not about the firewall. Of the 74 tables that can reference an address, four came back clean. Treating that as nothing references this is how an object in a web-proxy profile gets deleted.
indeterminate
Section titled “indeterminate”Something the tool needed could not be read at all.
total_references here is a lower bound: some references were found and an
unknown number were never looked at.
Two ways forward, either grant the profile read on the source that failed and ask again, or check that source by hand. What you cannot do is treat a lower bound as a count.
Without these distinctions the failure is silent and severe: an empty list from a refused read looks exactly like an empty list from a clean table. How the denial case is handled throughout.
What safe_to_delete: true actually means
Section titled “What safe_to_delete: true actually means”It means the appliance’s own reference lookup answered, and said nothing points at this object. That is a far stronger claim than it used to be, and it is worth knowing exactly how strong.
It still does not mean deleting it is a good idea.
The honest framing: this tool now answers what does this appliance say points at this object, which is the right question and the one the web UI answers too. It does not replace knowing your own configuration.
The wider gap
Section titled “The wider gap”There is a second kind of reference that no configuration read can see: things outside the firewall that depend on the object existing.
A monitoring system polling a VIP by name. A runbook that says add the host to DMZ-SERVERS. A FortiManager policy package that will push the object back, or flag the appliance as out of sync when it disappears. A colleague’s half-finished change.
safe_to_delete is a statement about the configuration, and the configuration
is not the whole system.
A workflow that holds up
Section titled “A workflow that holds up”Ask in this order, and the answer at each step tells you whether the next one matters.
Start with the object itself. find_references on the name, and read the
verdict before anything else. object_not_found means fix the spelling and
start over. indeterminate or no_references_in_checked_scopes mean the rest
of this workflow is premature, because you do not yet know what you are looking
at. referenced gives you both the answer and the list of what to fix first.
Follow the groups. Every name under groups gets its own
find_references call, because membership is not expanded transitively. This
is where indirect dependencies live, and it is the step most often skipped.
Check what else shares the value. search_config on the object’s IP or
subnet finds other objects covering the same addresses, which is the common
case where a duplicate was created years ago and half the policies use each.
Deleting one and leaving the other looks clean and leaves the configuration
inconsistent.
Check it is not doing something live. If the object is a subnet, ask
get_arp_table or list_dhcp_leases whether anything is actually on it. An
object with no references and no live hosts is genuinely dead. An object with
no references and forty active devices behind it is a documentation gap, not
cruft.
And then
Section titled “And then”This server cannot make the change. It reads, and that is all it does — so the deletion happens in the web UI or the CLI, by a person, with the reference list in front of them.
That division is the point rather than a limitation. Why read-only is structural here.