A CLI toolkit to manage FortiGate via REST API (policies, addresses, addrgrps, VIPs, etc.)
Project description
FortiGate Policy & Address Toolkit — README (v1.1.3)
A single-file Python CLI that talks to FortiGate’s REST API to manage policies, addresses, address groups, and virtual IPs (VIPs) — plus a few productivity helpers (smart grouping, bulk attach, quick search).
Tested against FortiOS 7.4.4 and 7.2.7. The script includes compatibility shims for common response/payload differences between these versions.
✨ Highlights
- Policies: inspect by ID/name, search, create, update, mutate
srcaddr/dstaddrquickly, attach address-group families. - Addresses: full CRUD, JSON and no-JSON shortcuts, resilient subnet handling.
- Address Groups: full CRUD, validate members, add/remove members, “find groups containing X”.
- Smart chunking: keep each group ≤ 255 members; auto-create
-2,-3, … suffix groups. - VIPs (Virtual IPs): list/search/get/create/update/delete. Supports 1:1 static NAT and port forward (tcp/udp/sctp). FortiOS 7.2/7.4 payload compatibility built in.
- Robustness: VDOM fallback for
424, graceful retries for common 5xx validation shapes, “removeallwhen mixed” safeguard.
🧩 Requirements
-
Python 3.8+
-
Packages:
requests,urllib3pip install requests urllib3
No other third-party deps. ipaddress is stdlib.
🔐 Authentication & Connectivity
- Pass your API token with both headers and query: the tool sets
Authorization: Bearer,X-Auth-Token, and?access_token=...automatically. It's recommended to grand the API administrator permission. - Self-signed certs: use
--insecure(or import the FortiGate CA into your OS trust store).
❗ Place
--insecurebefore the subcommand, otherwise it won’t apply.
▶️ Quick Start
# Show version
python <SCRIPT>.py --version
# Inspect a policy by ID (with VDOM)
python <SCRIPT>.py --host https://10.1.1.1:10443 --token <TOKEN> --vdom test-vdom --insecure \
policy-id --policy-id 209
# Search address groups by substring
python <SCRIPT>.py --host https://10.1.1.1:10443 --token <TOKEN> --vdom test-vdom --insecure \
search-addrgrps --q Black
Replace
<SCRIPT>.pywith your filename. Windows CMD line continuation: use^; PowerShell: use backtick ``` or split lines.
🧭 Global CLI Syntax
python <SCRIPT>.py --host https://<ip>:<port> --token <TOKEN> [--vdom <VDOM>] [--insecure] <SUBCOMMAND> [OPTIONS]
Global options
--host(required) – e.g.https://10.1.1.1:10443--token(required) – FortiGate API token--vdom(optional) – VDOM name (e.g.test-vdom)--insecure– disable SSL verification (self-signed)--version– print tool version (v1.1.0)
Output: JSON to stdout.
Exit codes: 0 success, 1 runtime/API/SSL errors, 2 usage/help.
📚 Subcommands & Examples
1) Policies
Inspect / Search
# By ID
policy-id --policy-id 231
# By name
policy-name --policy-name "From-internet"
# Search by name substring
search-policies --q "blacklist"
# Find policies referencing a given object (addr/addrgrp/vip name)
find-policies-with --object-name "Blacklist"
Create
# JSON payload
create-policy --data @policy.json
# No-JSON “simple” creation
create-policy-simple \
--name "From-internet-副本" \
--srcintf "any" \
--dstintf "any" \
--srcaddr "blacklist-must-deny,blacklist-auto,Blacklist" \
--dstaddr "all" \
--service "ALL" \
--action deny \
--schedule always \
--status enable \
--logtraffic all \
--comment "created by script"
create-policy-simple wraps comma-lists into FortiGate’s [{ "name": ... }] form automatically.
Update
# Full JSON replace/merge (FortiGate semantics)
update-policy --policy-id 263 --data @update.json
# Field-level updates (no JSON)
update-policy-fields --policy-id 263 \
--srcaddr "Blacklist,Code_Group_BlackList" \
--dstaddr "all" \
--logtraffic all \
--comment "updated by script"
# Enable/Disable (note spelling)
update-policy-fields --policy-id 263 --status disable
Quickly mutate src/dst address lists
# Add/remove names (addresses or groups)
policy-set-addrs --policy-id 263 --add-dst "Blacklist" --remove-src "OldGroup"
Safeguard: if a list contains
alland other members, the tool removesallto avoid FortiOS500 -7. VIP notice: VIPs may be used only in dstaddr (destination). Using a VIP insrcaddrwill fail (500 -3).
Attach an address-group family to a policy
# Will attach base and its -2/-3/... siblings (if present)
attach-grps-to-policy --policy-id 263 --base-name "Blacklist" --direction dst
2) Addresses
# List (brief)
list-addresses
# Search by substring
search-addresses --q "BAN-"
# Get one (full)
get-address --name "crl.pki.goog"
# Create (JSON)
create-address --data '{"name":"A-10.0.0.0_24","type":"ipmask","subnet":"10.0.0.0 255.255.255.0","comment":"demo"}'
# Create ipmask (no JSON)
create-address-ipmask --name "A-10.0.0.0_24" --cidr 10.0.0.0/24
# Update (JSON)
update-address --name "BAN-77.90.151.5" --data '{"comment":"updated"}'
# Update comment (no JSON)
update-address-comment --name "BAN-77.90.151.5" --comment "updated"
# Delete
delete-address --name "*.microsoft.com.akadns.net"
Address creation auto-converts
subnetbetween"IP MASK"and["IP","MASK"]to bypass500 -5.
3) Address Groups
# List
list-addrgrps
# Search by substring
search-addrgrps --q "Black"
# Get one (shows members & count)
get-addrgrp --name "Blacklist"
# Create (validate members by default)
create-addrgrp --name "HQ-NETWORKS" --members "A1,A2,A3" --comment "demo"
# Replace fields/members (full replacement)
update-addrgrp --name "HQ-NETWORKS" --members "A-10.0.0.0_24,Another-Addr" --comment "updated"
# Rename
update-addrgrp --name "HQ-NETWORKS" --new-name "HQ-NETWORKS-NEW"
# Add members (idempotent)
add-to-addrgrp --name "HQ-NETWORKS" --members "10.1.6.0/23"
# Remove members
remove-from-addrgrp --name "HQ-NETWORKS" --members "10.1.6.0/23"
# Delete
delete-addrgrp --name "HQ-NETWORKS"
# Find groups that contain a given object
find-groups-with-member --name "BAN-131.226.102.110"
Skip member validation (when you’re sure members exist or are created out of band): add --skip-validate-members.
4) Smart chunking for groups (≤255 members each)
# Provide members inline and/or via file (one per line)
smart-fill-addrgrps \
--base-name "Blacklist" \
--members "A-10.0.0.0_24,HQ-NETWORKS" \
--members-file C:\path\ips.txt \
--chunk-size 255 \
--comment "auto-chunked"
# Then attach all -N groups to a policy (dst or src)
attach-grps-to-policy --policy-id 263 --base-name "Blacklist" --direction dst
The tool:
- reads existing
baseandbase-2/-3/...groups, - fills up to
chunk-size, - creates new groups as needed,
- skips members already present.
5) VIPs (Virtual IPs)
VIPs belong in
dstaddrof policies. They represent “what external users hit”. For DNAT/port-forward scenarios, policynatis usually disable (VIP does the translation); useserviceto restrict ports.
List / Search / Get
list-vips
search-vips --q "openvpn"
get-vip --name "to-internet"
Create (JSON)
# 1:1 static NAT
create-vip --data '{
"name": "Pub_1.1.1.1_1to1_10.1.1.1",
"extip": "1.1.1.1",
"mappedip": [{"range": "10.1.1.1"}], // can also be "10.1.1.1"
"extintf": "any",
"type": "static-nat",
"comment": "by API"
}'
# Port forward (tcp/udp/sctp)
create-vip --data '{
"name": "Pub_1.1.1.1_1to1_10.1.1.1",
"extip": "1.1.1.1_1",
"mappedip": "10.1.1.1",
"portforward": "enable",
"protocol": "tcp",
"extport": "943",
"mappedport": "943",
"extintf": "any",
"type": "static-nat"
}'
Create (no JSON)
# Static 1:1
create-vip-simple \
--name "Pub_1.1.1.1_1to1_10.1.1.1" \
--extip 1.1.1.1 \
--mappedip 10.1.1.1 \
--extintf any \
--comment "by API"
# Port forward
create-vip-portforward \
--name "Pub_1.1.1.1_943_10.1.1.1_943" \
--extip 1.1.1.1 \
--mappedip 10.1.1.1 \
--protocol tcp \
--extport 943 \
--mappedport 943 \
--extintf any \
--comment "by API"
create-vip-simpleis for 1:1 static NAT only (no port args). For port forwarding, usecreate-vip-portforwardorupdate-vip-fields.
Update
# JSON
update-vip --name "Pub_..." --data @vip-update.json
# Field-level (no JSON)
update-vip-fields --name "Pub_..." \
--portforward true \
--protocol tcp \
--extport 943 \
--mappedport 943 \
--mappedip 10.1.1.1 \
--extintf any \
--comment "443 only"
Delete
delete-vip --name "Pub_..."
Attach VIP to policy (destination)
# Add VIP to dstaddr; remove old internal object if present
policy-set-addrs --policy-id 209 --add-dst "Pub_1.1.1.1_943_10.1.1.1_943" --remove-dst "demo"
Version compatibility built in
extipmay be given with/32— the tool strips CIDR.mappedipaccepts"x.x.x.x",["x.x.x.x"], or[{"range":"x.x.x.x"}]— normalized internally.- Ports coerced to strings to satisfy both 7.2/7.4 JSON validators.
🧪 End-to-End Example: Expose OpenVPN on 943
# 1) Create VIP (port forward 943 → 10.1.1.1:943)
create-vip-portforward \
--name "Pub_1.1.1.1_943_10.1.1.1_943" \
--extip 1.1.1.1 --mappedip 10.1.1.1 \
--protocol tcp --extport 943 --mappedport 943 --extintf any
# 2) Attach it to policy 209 (dstaddr), drop old target
policy-set-addrs --policy-id 209 \
--add-dst "Pub_1.1.1.1_943_10.1.1.1_943" \
--remove-dst "demo"
# 3) Verify
policy-id --policy-id 209
🛠️ Troubleshooting
-
SSL:
CERTIFICATE_VERIFY_FAILEDPut--insecurebefore the subcommand:python <SCRIPT>.py --host ... --token ... --vdom test-vdom --insecure list-vipsOr import the FortiGate CA into your system trust store.
-
424with VDOM Usually wrong/absent VDOM or per-endpoint limitation. The tool auto-retries list calls without VDOM; ensure your--vdomis correct (e.g.,test-vdom). -
500 -7on policy update Mixingallwith other members is invalid. The tool auto-removesallwhen you add other names. -
500 -5creating addresses Subnet shape mismatch. The tool retries between"IP MASK"and["IP","MASK"]. Usecreate-address-ipmaskto avoid format issues. -
500 -3when mutating policy addrs Typically caused by putting a VIP intosrcaddr. VIPs belong in dstaddr only. -
-8creating VIP (e.g., “Mapped-ip range is not specified”) Provide--mappedipor correct JSON shape; the tool normalizes but requires an actual value. -
“unrecognized arguments” Ensure global flags (
--host --token [--vdom] [--insecure]) come before the subcommand.
🔒 Best Practices
- Treat API tokens like passwords; avoid shell history leaks.
- Prefer port-forward VIPs if you only need specific ports; otherwise use 1:1 and restrict by
service. - For DNAT policies, keep
natdisable and rely on VIP; useserviceto allow intended ports only. - Use
--skip-validate-membersonly when you’re sure objects exist.
🔁 Changelog (SemVer)
v1.1.0
- New: VIP CRUD/search, simple creators (1:1 & port-forward), field-level updates.
- Compat: Normalize
extip,mappedip,portforward, and port types across 7.2/7.4. - Polish: Stronger policy update fallback (light PUT → full merge PUT), auto-remove
allwhen mixed. - UX:
--version, clearer SSL error guidance, better “not found” suggestions.
v1.0.0
- Policies, addresses, address groups CRUD.
- Smart chunking (≤255 per group) and attach-groups-to-policy.
📦 Files & JSON Payload Examples
policy.json
{
"name": "Example-Policy",
"srcintf": [{"name": "any"}],
"dstintf": [{"name": "any"}],
"srcaddr": [{"name": "all"}],
"dstaddr": [{"name": "all"}],
"service": [{"name": "ALL"}],
"action": "accept",
"schedule": "always",
"status": "enable",
"logtraffic": "all",
"comments": "created by API"
}
vip-update.json
{
"portforward": "enable",
"protocol": "tcp",
"extport": "443",
"mappedport": "443",
"mappedip": [{"range": "10.146.42.239"}],
"extintf": "any",
"comment": "443 only"
}
🧭 Roadmap (optional)
--cafile <path>to validate against a custom CA (safer than--insecure).--dry-runto preview changes without applying.- Retry/backoff knobs and
--timeoutoverride. - VIP groups (
vipgrp) if needed.
📄 License
Add your preferred license here (e.g., MIT/Apache-2.0). If omitted, assume “all rights reserved”.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file netknife-1.1.3.tar.gz.
File metadata
- Download URL: netknife-1.1.3.tar.gz
- Upload date:
- Size: 22.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a23f7aa5dfabe9936c8c2bac62c1fd66966d21dce918f1f4526741195b7012db
|
|
| MD5 |
7541b7e891300dbba1b481c8ae390f47
|
|
| BLAKE2b-256 |
3a858cd8225abba328390a8a48ec0b81c8e66d57b8f7b6f4fb1c42c70e3c807d
|
File details
Details for the file netknife-1.1.3-py3-none-any.whl.
File metadata
- Download URL: netknife-1.1.3-py3-none-any.whl
- Upload date:
- Size: 29.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25a72dd88534f377c6892317846b6a747ca5bab8922fca54540f93f3a18d5aee
|
|
| MD5 |
07bfec507616ae4b31d3b161da51f019
|
|
| BLAKE2b-256 |
19341fcf325e8d053f97b22d47bf0f1c69dd9cf72601ed8c54838b3857bbf5df
|