//native
/**
* Upsert firewall address
* Declaratively ensure an IPv4 firewall address object exists with the given values. Reads the current object, returns the field-level diff and, unless dry_run is true (the default), creates the object or updates only the fields that differ. Fields left empty are not managed.
*/
export async function main(
auth: RT.Fortigate,
name: string,
type: "ipmask" | "iprange" | "fqdn",
subnet: string | undefined,
start_ip: string | undefined,
end_ip: string | undefined,
fqdn: string | undefined,
comment: string | undefined,
vdom: string | undefined,
dry_run: boolean = true
) {
const base = `${auth.base_url.replace(/\/+$/, "")}/api/v2/cmdb/firewall/address`
const withVdom = (path: string) => {
const url = new URL(path)
if (vdom != null && vdom !== "") {
url.searchParams.append("vdom", vdom)
}
return url
}
const headers = {
Authorization: `Bearer ${auth.token}`,
"Content-Type": "application/json",
Accept: "application/json",
}
// Intended state. FortiOS stores subnet as "address netmask".
const desired: { [key: string]: any } = { type }
if (type === "ipmask") {
if (!subnet) throw new Error("subnet is required when type is ipmask")
desired.subnet = toAddressAndMask(subnet)
} else if (type === "iprange") {
if (!start_ip || !end_ip)
throw new Error("start_ip and end_ip are required when type is iprange")
desired["start-ip"] = start_ip
desired["end-ip"] = end_ip
} else {
if (!fqdn) throw new Error("fqdn is required when type is fqdn")
desired.fqdn = fqdn
}
if (comment) desired.comment = comment
// Current state: 404 means the object does not exist yet.
const objectUrl = withVdom(`${base}/${encodeURIComponent(name)}`)
const getResponse = await fetch(objectUrl, { headers })
let current: { [key: string]: any } | undefined
if (getResponse.status !== 404) {
if (!getResponse.ok) {
throw new Error(`${getResponse.status} ${await getResponse.text()}`)
}
current = ((await getResponse.json()).results ?? [])[0]
}
const diff: { [key: string]: { current: any; desired: any } } = {}
for (const [key, value] of Object.entries(desired)) {
const now = current?.[key]
if (normalize(now) !== normalize(value)) {
diff[key] = { current: now ?? null, desired: value }
}
}
const changed = Object.keys(diff).length > 0
const operation = !current ? "create" : changed ? "update" : "none"
if (dry_run !== false || !changed) {
return { changed, operation, diff, applied: false }
}
let response
if (!current) {
response = await fetch(withVdom(base), {
method: "POST",
headers,
body: JSON.stringify({ name, ...desired }),
})
} else {
const update: { [key: string]: any } = {}
for (const key of Object.keys(diff)) update[key] = desired[key]
response = await fetch(objectUrl, {
method: "PUT",
headers,
body: JSON.stringify(update),
})
}
if (!response.ok) {
throw new Error(`${response.status} ${await response.text()}`)
}
return {
changed,
operation,
diff,
applied: true,
result: await response.json(),
}
}
// Accepts "10.0.0.0/24" or "10.0.0.0 255.255.255.0"; returns "10.0.0.0 255.255.255.0".
function toAddressAndMask(value: string): string {
const trimmed = value.trim()
if (trimmed.includes("/")) {
const [address, bits] = trimmed.split("/")
const prefix = Number(bits)
if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) {
throw new Error(`Invalid prefix length in subnet: ${value}`)
}
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0
return `${address} ${[24, 16, 8, 0].map((s) => (mask >>> s) & 255).join(".")}`
}
const parts = trimmed.split(/\s+/)
if (parts.length !== 2) {
throw new Error(
`subnet must be "address/prefix" or "address netmask": ${value}`
)
}
return parts.join(" ")
}
function normalize(value: any): string {
if (value == null) return ""
return String(value).trim().split(/\s+/).join(" ")
}
Submitted by hugo989 6 hours ago