Edits history of script submission #24117 for ' Upsert firewall address (fortimanager)'

  • 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 in an ADOM 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.Fortimanager,
      adom: string,
      name: string,
      type: "ipmask" | "iprange" | "fqdn",
      subnet: string | undefined,
      start_ip: string | undefined,
      end_ip: string | undefined,
      fqdn: string | undefined,
      comment: string | undefined,
      dry_run: boolean = true
    ) {
      // The ADOM is a segment of the JSON-RPC url: a "/" would address another table.
      if (adom.includes("/")) throw new Error(`Invalid adom: ${adom}`)
      const table = `/pm/config/adom/${adom}/obj/firewall/address`
    
      const rpc = async (method: string, params: { [key: string]: any }) => {
        const response = await fetch(
          `${auth.base_url.replace(/\/+$/, "")}/jsonrpc`,
          {
            method: "POST",
            headers: {
              Authorization: `Bearer ${auth.token}`,
              "Content-Type": "application/json",
              Accept: "application/json",
            },
            body: JSON.stringify({ id: 1, method, params: [params], verbose: 1 }),
          }
        )
        if (!response.ok) {
          throw new Error(`${response.status} ${await response.text()}`)
        }
        const json = await response.json()
        const result = json.result?.[0]
        if (result?.status?.code !== 0) {
          throw new Error(
            `FortiManager error: ${JSON.stringify(json.result ?? json)}`
          )
        }
        return result
      }
    
      // Intended state. FortiManager 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.
      const found = await rpc("get", { url: table, filter: ["name", "==", name] })
      const current = (found.data ?? [])[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 result
      if (!current) {
        result = await rpc("add", { url: table, data: { name, ...desired } })
      } else {
        // update on the table URL, keyed by name in the data: no escaping of names containing "/".
        const update: { [key: string]: any } = { name }
        for (const key of Object.keys(diff)) update[key] = desired[key]
        result = await rpc("update", { url: table, data: update })
      }
    
      return { changed, operation, diff, applied: true, result }
    }
    
    // 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
    }
    
    function normalize(value: any): string {
      if (value == null) return ""
      if (Array.isArray(value)) return value.map(String).join(" ")
      return String(value).trim()
    }
    

    Submitted by hugo989 6 hours ago