Edits history of script submission #24111 for ' Upsert firewall address (fortigate)'

  • bunnative
    One script reply has been approved by the moderators
    Ap­pro­ved
    //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