Skip to content

Tool reference

Seventeen tools, in four groups. Every one of them takes an optional target argument naming which appliance to query, which can be omitted entirely when only one is configured. When several are configured and target is left out, the error names the valid aliases so the model can correct itself on the next call rather than guessing.

Every tool issues GET requests only. There is no write path anywhere in the codebase, and every tool carries the MCP readOnlyHint annotation so a client knows that before it calls one.

Responses name both the appliance and the VDOM they were read from, since every call is scoped to a single VDOM and an answer about the wrong one is otherwise indistinguishable from an answer about yours.

Which FortiGate appliances this server can reach. Call it first when several may be configured. Credentials never appear in the response.

Takes no arguments.

{
"count": 2,
"default_target": null,
"targets": [
{
"name": "branch",
"url": "https://fgt-br2.example.com",
"vdom": "root",
"auth_mode": "token",
"verify_ssl": false,
"timeout": 30
},
{
"name": "edge",
"url": "https://fgt-edge1.example.com",
"vdom": "root",
"auth_mode": "token",
"verify_ssl": true,
"timeout": 30
}
]
}

default_target is populated only when exactly one appliance is configured. When it is null, every other tool requires an explicit target.

Appliance identity and health: model, serial, firmware version, build, hostname, and current load.

ArgumentTypeDefaultMeaning
targetstringonly applianceWhich FortiGate to query
{
"target": "lab",
"url": "https://fgt-edge1.example.com",
"hostname": "fgt-edge1",
"model": "FortiWiFi 61E",
"serial": "FWF61ETK00000000",
"version": "v7.0.14",
"build": 601,
"vdom": "root",
"runtime_status": "ok",
"model_number": "61E",
"log_disk": "available",
"cpu_percent": 4,
"memory_percent": 41,
"sessions": 87
}
FieldAlwaysMeaning
target, url, vdomyesWhich appliance answered, and the scope
hostnameyesFrom the monitor tree, falling back to the configuration
modelyesmodel_name, falling back to model
serial, version, buildyesFrom the response envelope
runtime_statusyesStatus of the monitor/system/status read
alias, timezone, model_number, log_disknoPresent only when reported
cpu_percent, memory_percent, sessionsnoCurrent load, present only when reported
uptime_secondsnoPresent only on firmware that carries it
load_statusnoPresent only when the load read failed

The serial, version, and build come from the envelope of the cmdb response rather than from results. FortiOS puts them as siblings of results on every cmdb call, and any helper that unwraps straight to results throws them away, which is why the serial looks absent from the API until you read the raw response. See identity lives in the envelope.

An absent key means the appliance did not offer the value, rather than the value being zero or unknown-but-present.

That distinction was settled the expensive way. uptime_seconds was documented and returned for weeks as a permanent null, which read like a parsing bug. Probing a FortiWiFi-61E on FortiOS 7.0.14 found it is not a bug at all — that firmware reports no uptime anywhere:

EndpointWhat it returnsUptime?
monitor/system/statushostname, model, model_name, model_number, log_disk_statusno
monitor/system/resource/usagecpu, mem, disk, session, setuprate, lograte countersno
monitor/system/timetime, as epoch secondsno, that is wall clock

The lookup is kept because newer firmware does carry it, so the key appears where it exists and is absent where it does not.

There is one trap in reading this shape, and it is worth stating because it is easy to reintroduce: filter on is not None, never on truthiness. A genuinely idle appliance reports cpu_percent: 0, and a truthiness filter discards that as though the appliance never answered.

runtime_status and load_status exist to separate them. A firmware that does not implement the endpoint and a token that is not allowed to read it both produce no numbers, and they call for completely different responses from you. load_status appears only in the second case, carrying the reason — denied: http=403, say. See fail soft on absence, never on denial.

Where a term appears, across every object type at once. This is the tool for an open question when you do not yet know which kind of object holds the answer.

ArgumentTypeDefaultMeaning
termstringrequiredCase-insensitive substring to look for
targetstringonly applianceWhich FortiGate to query
include_policiesbooleantrueSearch policy names, comments, and member lists

It looks through address objects, address groups, services, interfaces, static routes, and optionally policies, matching against names, values, comments, and member lists. Only the categories that matched appear in the response, so an empty category is absent rather than present and empty.

{
"target": "edge",
"term": "10.20.30.0",
"total_matches": 3,
"matches": {
"addresses": [
{ "name": "LAN-USERS", "type": "ipmask", "value": "10.20.30.0/24" }
],
"address_groups": [
{ "name": "INTERNAL", "members": ["LAN-USERS", "LAN-SERVERS"] }
],
"routes": [
{ "seq_num": 3, "destination": "10.20.30.0/24", "gateway": "192.0.2.1", "interface": "wan1", "enabled": true }
]
}
}

Policies are the largest table on most appliances, so include_policies: false is worth passing when only object definitions matter.

Named addresses that policies reference, each with one readable value regardless of its type.

ArgumentTypeDefaultMeaning
targetstringonly applianceWhich FortiGate to query
name_containsstringnoneCase-insensitive substring filter on the name
address_typestringnoneExact FortiOS type: ipmask, fqdn, iprange, geography, mac

Each FortiOS address type keeps its value in a differently named field. A subnet object holds subnet, an FQDN object holds fqdn, a range holds start-ip and end-ip. The type discriminator is resolved once here so every object reports a single value string.

{
"target": "edge",
"count": 2,
"addresses": [
{ "name": "LAN-USERS", "type": "ipmask", "value": "10.20.30.0/24" },
{ "name": "UPDATE-SERVER", "type": "fqdn", "value": "updates.example.com", "comment": "vendor patch mirror" }
]
}

Groups and their members.

ArgumentTypeDefaultMeaning
targetstringonly applianceWhich FortiGate to query
name_containsstringnoneCase-insensitive substring filter on the group name
{
"target": "edge",
"count": 1,
"groups": [
{ "name": "INTERNAL", "members": ["LAN-USERS", "LAN-SERVERS", "MGMT"] }
]
}

Service objects with their protocols and ports.

ArgumentTypeDefaultMeaning
targetstringonly applianceWhich FortiGate to query
name_containsstringnoneCase-insensitive substring filter on the service name

FortiOS scatters the port range across tcp-portrange, udp-portrange, and sctp-portrange, populating only the ones that apply, while protocol itself may read TCP/UDP/SCTP meaning any of the populated ones. Multi-port values are space-separated, which is how Kerberos arrives as 88 464.

{
"target": "edge",
"count": 3,
"services": [
{ "name": "HTTPS", "protocol": "TCP", "ports": { "tcp": "443" } },
{ "name": "KERBEROS", "protocol": "TCP/UDP", "ports": { "tcp": "88,464", "udp": "88,464" } },
{ "name": "PING", "protocol": "ICMP", "icmp_type": 8 }
]
}

Services defined as protocol: IP report a named protocol where one is known, so GRE comes back as "protocol": "GRE", "protocol_number": 47. Protocol number 0 reports as any, because FortiOS uses IP with no protocol number to mean any IP protocol in its built-in ALL service, and reading it as IANA’s HOPOPT would be technically defensible and operationally wrong.

Firewall rules in evaluation order, with filters.

ArgumentTypeDefaultMeaning
targetstringonly applianceWhich FortiGate to query
enabled_onlybooleanfalseDrop policies whose status is disabled
interfacestringnoneExact match on source or destination interface
addressstringnoneExact match on an address object or group, either side
servicestringnoneExact match on a service object name

Policies come back in the order FortiOS evaluates them, which is configuration order rather than sorted by ID. That order is the meaning of a ruleset, so it is preserved rather than normalized away.

Each policy also carries an explicit order index, starting at zero. A list position is not something downstream is obliged to preserve — a filter, a re-serialization, or a model rewriting the answer into a table can all lose it — so the index travels with the record rather than being implied by it.

total_policies reports the size of the unfiltered ruleset alongside count, so a filtered answer says what it was filtered from.

{
"target": "edge",
"vdom": "root",
"count": 2,
"total_policies": 37,
"policies": [
{
"id": 4,
"name": "guest-to-internet",
"enabled": true,
"action": "accept",
"from": ["guest"],
"to": ["wan1"],
"source": ["GUEST-NET"],
"destination": ["all"],
"service": ["HTTP", "HTTPS", "DNS"],
"nat": true,
"log": "all",
"order": 3
},
{
"id": 9,
"name": "block-legacy-smb",
"enabled": true,
"action": "deny",
"from": ["lan"],
"to": ["dmz"],
"source": ["all"],
"destination": ["DMZ-SERVERS"],
"service": ["SMB"],
"order": 8
}
]
}

An unnamed policy reports as policy-<id>, which is what a FortiGate with one default rule looks like: {"id": 1, "name": "policy-1", "order": 0}.

Virtual IPs, which are FortiOS’s destination NAT rules.

ArgumentTypeDefaultMeaning
targetstringonly applianceWhich FortiGate to query

A VIP maps an external address, optionally with a port, to an internal one. FortiOS stores the addresses inline on the VIP rather than as references to address objects, so nothing here needs a second lookup.

{
"target": "edge",
"count": 1,
"vips": [
{
"name": "web-in",
"external_ip": ["198.51.100.20"],
"mapped_ip": ["10.20.40.10"],
"interface": ["wan1"],
"port_forward": { "protocol": "tcp", "external_port": "443", "mapped_port": "8443" }
}
]
}

port_forward appears only when port forwarding is enabled on the VIP. Its absence means the whole address is mapped.

What points at an address, group, service, virtual IP, or interface, and whether that can be answered at all.

ArgumentTypeDefaultMeaning
object_namestringrequiredExact name of the object. Matching is exact, not a search
targetstringonly applianceWhich FortiGate to query

This is the question that precedes every configuration change on a firewall, and the authority for it is the appliance itself. FortiOS exposes the same reference lookup its web UI uses, and this tool asks that endpoint rather than inferring an answer from a handful of tables.

It also scans policies, address groups, service groups, virtual IPs, and static routes directly, because those yield readable detail the lookup does not — a policy’s name, whether it is enabled, whether it accepts or denies.

So the response has two halves. references is what the appliance says, and is authoritative. policies, groups, vips, and routes are the readable detail, and they cover five tables out of many.

On FortiOS 7.0.14, the number of tables that can hold a reference to an object:

Object kindTables that can reference it
System interface234
Firewall address74
Service17

A five-table scan covers five of 234 for an interface. An address used only by a web-proxy profile came back clean, and safe_to_delete: true on an object that is very much in use is the worst answer this tool can give.

A real case off the lab appliance, asking about wan1, abridged:

{
"object": "wan1",
"verdict": "referenced",
"resolved_as": ["interface"],
"total_references": 2,
"candidate_tables": 234,
"references": [
{ "table": "system.interface", "object": "ssot_test_vlan1", "looked_up_as": "interface", "attribute": "name" },
{ "table": "firewall.policy", "object": "1", "looked_up_as": "interface", "attribute": "dstintf" }
],
"policies": [
{ "id": 1, "name": "policy-1", "enabled": true, "action": "accept", "referenced_as": ["to_interface"] }
],
"safe_to_delete": false
}

The second row a scan would have found — it is the same policy that shows up in policies with its name and action attached. The first it would not. ssot_test_vlan1 is a VLAN sub-interface parented to wan1, and no amount of policy, group, VIP, or route scanning reaches it. Deleting wan1 would have taken the VLAN with it.

Five values, and safe_to_delete is present for only two of them.

verdictMeaningsafe_to_delete
referencedSomething points at itfalse
no_referencesThe appliance confirmed nothing doestrue
object_not_foundNo object of any kind carries this name, so probably a typoabsent
no_references_in_checked_scopesThe authoritative lookup was unavailable and the partial scan found nothingabsent
indeterminateSomething needed could not be readabsent

The two middle values are the ones worth slowing down for.

object_not_found means the question was about a name rather than an object. Without it, a misspelling reports zero references and safe_to_delete: true — a confident yes to a question containing a typo.

no_references_in_checked_scopes is a fact about four tables rather than about the appliance. The authoritative lookup was unavailable and the fallback scan found nothing, which is a much weaker claim than nothing references this. The note field says so in as many words.

FieldAlwaysMeaning
verdictyesThe five values above. Read this, not the count
total_referencesyesAuthoritative row count when the lookup answered, otherwise the scan’s count
resolved_asyesKinds the name matched, as a list: address, interface, service, and so on. Empty on object_not_found
referencesyesAuthoritative rows: table, object, looked_up_as (the kind), attribute (the field holding the reference)
sources_checkedyesPer-source status, including object_usage and the three kind-resolution reads
policies, groups, vips, routesyesReadable detail from the direct scan
candidate_tablesnoHow many tables could reference this kind. Present only when the kind was identified
safe_to_deletenoOnly on referenced and no_references
notenoPresent on the other three verdicts, explaining the limit

sources_checked carries nine keys. Five are the detail scan — policies, address_groups, service_groups, vips, routes. Three are the reads that work out what kind of object the name is — addresses, services, interfaces — which has to happen before the usage lookup can be asked correctly. The last is object_usage, the authoritative lookup itself, and it is the one whose failure downgrades the verdict.

referenced_as inside policies names the field the object appeared in, so a policy listing it as both source and destination reports both rather than being counted twice.

Group membership is not expanded transitively. An object inside a group that a policy uses is reported as referenced by the group, not by the policy.

Both silent, both the kind that produce a confident wrong answer.

Every row reports reference_count: 0, including rows that are real references. Counting that field reports zero for an object with two of them. The row’s existence is the signal; its count is not.

Asking the wrong table succeeds. Query the address table about a name that is actually an interface, and FortiOS answers HTTP 200 with an empty list rather than an error. An interface with two references reports zero, and nothing anywhere indicates the question was malformed. That is why the tool resolves the object’s kind from its defining cmdb table before querying, and why resolved_as is in the response at all.

Interfaces with addresses, VLAN tags, and link state.

ArgumentTypeDefaultMeaning
targetstringonly applianceWhich FortiGate to query
include_internalbooleanfalseInclude FortiOS-generated interfaces
interface_typestringnoneExact type: physical, vlan, aggregate, hard-switch, switch, vap-switch, tunnel
with_ip_onlybooleanfalseKeep only interfaces carrying a static IP

FortiOS creates bookkeeping interfaces alongside real ones — a quarantine interface accompanies every wireless VAP, and SSL-VPN and tunnel roots appear the same way. Those are hidden by default because nobody configured them and nobody can meaningfully act on them.

hidden_internal names what was omitted rather than counting it, so the omission is auditable rather than merely acknowledged.

Abridged from a FortiWiFi-61E on FortiOS 7.0.14, which reports 22 interfaces and hides 7:

{
"target": "lab",
"vdom": "root",
"count": 22,
"interfaces": [
{
"name": "dmz", "type": "physical", "status": "up", "vdom": "root",
"ip": "10.10.10.1/24",
"management_access": "ping https fgfm fabric", "mtu": 1500
},
{
"name": "lan", "type": "switch", "status": "up", "vdom": "root",
"ip": "192.168.99.99/24",
"management_access": "ping https ssh fgfm fabric", "mtu": 1500
},
{
"name": "wan2", "type": "physical", "status": "up", "vdom": "root",
"addressing": "dhcp",
"note": "address assigned by dhcp; see get_routing_table for the runtime value",
"management_access": "ping fgfm", "mtu": 1500
},
{
"name": "modem", "type": "physical", "status": "down", "vdom": "root",
"addressing": "pppoe",
"note": "address assigned by pppoe; see get_routing_table for the runtime value",
"mtu": 1500
},
{
"name": "ssot_test_vlan1", "type": "vlan", "status": "up", "vdom": "root",
"ip": "198.51.100.1/24", "vlan_id": 100, "parent": "wan1",
"description": "[Synced from Nautobot] v3.3 push test",
"management_access": "ping", "mtu": 1500
},
{
"name": "wifi", "type": "vap-switch", "status": "up", "vdom": "root",
"mtu": 1500
}
],
"hidden_internal": [
"naf.root", "ssl.root", "wqtn.16.wifi", "wqtn.21.e2e-vap",
"wqtn.23.e2e-vap", "wqtn.25.xxxxxxx", "wqtn.27.xxxxxxx"
]
}

Two things in that output are worth noticing because they are easy to get wrong. wqt.root — the quarantine soft switch, without the n — is a real interface and is not hidden, while wqtn.* are. And a vap-switch carries no address of its own, so an SSID shows up here as an interface with nothing but a name and an MTU.

The ip field keeps the host address rather than collapsing to the network, which is the opposite of what an address object needs from the same dotted-mask format. That distinction has its own entry in the quirks list.

An interface addressed by DHCP or PPPoE reports no ip. The configuration genuinely holds no address for it, so instead it carries addressing naming the method and a note pointing at get_routing_table for the runtime value. Reporting only the absence of ip would answer what is my WAN address with it has none, which is the wrong kind of true.

VLAN sub-interfaces with their tags and parents, sorted by tag.

ArgumentTypeDefaultMeaning
targetstringonly applianceWhich FortiGate to query

A focused view of the VLAN subset of the interface table, because what VLANs exist and what are they attached to gets asked far more often than the full interface list. FortiOS quarantine VLANs are excluded, since they are type: vlan too and are otherwise indistinguishable from a real one by anything except the name prefix.

Routes an operator configured, sorted by sequence number.

ArgumentTypeDefaultMeaning
targetstringonly applianceWhich FortiGate to query
{
"target": "edge",
"count": 2,
"routes": [
{ "seq_num": 1, "destination": "0.0.0.0/0", "distance": 10, "priority": 0, "enabled": true, "gateway": "198.51.100.1", "interface": "wan1" },
{ "seq_num": 2, "destination": "10.20.50.0/24", "distance": 10, "priority": 0, "enabled": true, "destination_address_object": "REMOTE-SITE", "gateway": "10.20.30.254", "interface": "vlan30" }
]
}

When a route’s destination is a named address object, FortiOS writes the all-zeros sentinel into dst and the real destination lives in dstaddr. Reading dst first turns every named-destination route into a bogus default route, so dstaddr wins wherever it is populated. This tool resolves the name back to CIDR with a second lookup where it can, keeping the object name alongside it under destination_address_object.

A route with "blackhole": true reports no gateway or interface, because it has neither.

Routes the appliance is forwarding on right now.

ArgumentTypeDefaultMeaning
targetstringonly applianceWhich FortiGate to query
protocolstringnoneFilter by route type: static, connect, dhcp

This is live state, so it includes connected routes, dynamically learned routes, and routes handed over by DHCP, none of which appear in the static route configuration. A DHCP-assigned default route is the clearest case: present here, absent from list_static_routes, and not a bug in either.

{
"target": "edge",
"count": 3,
"routes": [
{ "destination": "0.0.0.0/0", "gateway": "198.51.100.1", "interface": "wan1", "type": "static", "distance": 10, "metric": 0 },
{ "destination": "10.20.30.0/24", "gateway": "0.0.0.0", "interface": "vlan30", "type": "connect", "distance": 0, "metric": 0 },
{ "destination": "203.0.113.0/24", "gateway": "192.0.2.1", "interface": "wan2", "type": "dhcp", "distance": 5, "metric": 0 }
]
}

Everything in this group reads the FortiOS monitor tree, which reports what the appliance currently observes rather than what it was configured to do. None of it is persisted anywhere, so these answers are true only at the moment of the call.

Wireless clients currently associated, enriched with DHCP and ARP.

ArgumentTypeDefaultMeaning
targetstringonly applianceWhich FortiGate to query
ssidstringnoneKeep only clients on this SSID

Each client is joined against the DHCP lease table and the ARP table by MAC, which is what turns an anonymous MAC into a recognizable device. The hostname comes from the DHCP lease, falling back to the vendor class identifier when the client did not send one.

{
"target": "edge",
"count": 1,
"clients": [
{
"mac": "aa:bb:cc:dd:ee:01",
"hostname": "kevins-laptop",
"ip": "10.20.30.84",
"ssid": "office",
"signal_dbm": -54,
"data_rate_mbps": 433.3,
"authenticated": true,
"interface": "vlan30"
}
]
}

On an appliance with no wireless hardware there is no monitor/wifi/client endpoint at all, and an absent endpoint yields no rows rather than an error — otherwise the joins in find_device would break entirely on a wired-only box.

An endpoint that refused the read is a different matter and is never treated as emptiness. Fail soft on absence, never on denial.

Leases the appliance is currently handing out.

ArgumentTypeDefaultMeaning
targetstringonly applianceWhich FortiGate to query
interfacestringnoneKeep only leases issued on this interface
hostname_containsstringnoneCase-insensitive substring filter on the hostname
{
"target": "edge",
"count": 1,
"leases": [
{
"mac": "aa:bb:cc:dd:ee:01",
"ip": "10.20.30.84",
"hostname": "kevins-laptop",
"interface": "vlan30",
"expires": 1789000000,
"reserved": false
}
]
}

IP-to-MAC bindings the appliance can see.

ArgumentTypeDefaultMeaning
targetstringonly applianceWhich FortiGate to query
interfacestringnoneKeep only entries learned on this interface

ARP catches devices that DHCP does not, meaning anything with a statically configured address. It is the fallback when a device is demonstrably on the network but holds no lease.

Who a MAC, IP, or hostname fragment belongs to.

ArgumentTypeDefaultMeaning
querystringrequiredA MAC, an IP, or part of a hostname
targetstringonly applianceWhich FortiGate to query

Searches the wireless client list, the DHCP lease table, and the ARP table together, then merges everything known about each matching device into one record. A device seen in all three places produces one result rather than three partial ones, and seen_in says which sources contributed.

{
"target": "edge",
"query": "10.20.30.84",
"count": 1,
"devices": [
{
"mac": "aa:bb:cc:dd:ee:01",
"seen_in": ["dhcp", "arp", "wifi"],
"ip": "10.20.30.84",
"hostname": "kevins-laptop",
"interface": "vlan30",
"lease_expires": 1789000000,
"ssid": "office",
"signal_dbm": -54,
"wireless": true
}
]
}

Matching is case-insensitive and substring-based, so a partial MAC, a bare hostname prefix, or a full IP all work. Once a MAC has matched in any one source, the other two contribute their fields for that MAC whether or not the query itself matched there — which is the entire point, since the wifi endpoint frequently knows a MAC and nothing else useful about it.

The three endpoints disagree about MAC casing, so every MAC is lowercased before the join. Skip that and the join silently matches nothing, producing three partial records that look like three devices.