//native
/**
* Upsert security rule
* Declaratively create or update a security policy rule in a firewall vsys, or a pre or post rule in a Panorama device group or the shared location (PAN-OS REST API). Reads the current rule, returns the difference and, unless dry_run is true (the default), applies only that difference to the candidate configuration. Empty inputs leave the current values unchanged; on creation empty lists default to any (service to application-default) and action is required. New rules are added at the bottom of the rulebase. Changes take effect after a commit.
*/
export async function main(
auth: RT.PaloAltoPanos,
name: string,
location: "vsys" | "device-group" | "shared" | undefined,
vsys: string | undefined,
device_group: string | undefined,
rulebase: "pre" | "post" | undefined,
action:
| "allow"
| "deny"
| "drop"
| "reset-client"
| "reset-server"
| "reset-both"
| undefined,
from_zones: string[] | undefined,
to_zones: string[] | undefined,
source: string[] | undefined,
destination: string[] | undefined,
source_user: string[] | undefined,
application: string[] | undefined,
service: string[] | undefined,
category: string[] | undefined,
tags: string[] | undefined,
description: string | undefined,
disabled: "keep" | "yes" | "no" | undefined,
log_setting: string | undefined,
profile_group: string | undefined,
dry_run: boolean = true
) {
const version = await restApiVersion(auth)
const url = new URL(
`/restapi/${version}/Policies/${ruleResource(location, rulebase)}`,
auth.base_url
)
const attrs = setLocation(url, location, vsys, device_group)
// Read the current state: list the rulebase and find the rule by name.
const current = ((await rest(auth, url, "GET")).result?.entry ?? []).find(
(e: any) => e["@name"] === name
)
const desired: { [key: string]: any } = {}
const lists: { [key: string]: string[] | undefined } = {
from: from_zones,
to: to_zones,
source,
destination,
"source-user": source_user,
application,
service,
category,
tag: tags,
}
for (const [key, members] of Object.entries(lists)) {
if (members?.length) desired[key] = { member: members }
}
if (action) desired.action = action
if (description) desired.description = description
if (disabled === "yes" || disabled === "no") desired.disabled = disabled
if (log_setting) desired["log-setting"] = log_setting
if (profile_group) {
desired["profile-setting"] = { group: { member: [profile_group] } }
}
if (!current) {
if (!desired.action) throw new Error("action is required to create a rule")
for (const key of REQUIRED_LISTS) {
desired[key] ??= {
member: [key === "service" ? "application-default" : "any"],
}
}
}
const diff: { [key: string]: { from: any; to: any } } = {}
for (const [key, to] of Object.entries(desired)) {
const from = current?.[key] ?? (current && key === "disabled" ? "no" : null)
if (canon(from) !== canon(to)) diff[key] = { from, to }
}
const changed = Object.keys(diff).length > 0
const operation = !current ? "create" : changed ? "update" : "none"
if (!changed || dry_run !== false) {
return {
changed,
operation,
diff,
applied: false,
dry_run: dry_run !== false,
}
}
// The docs build a PUT body from the GET response, modified as needed: keep
// everything the action does not manage, attributes (e.g. @uuid) included.
const entry: { [key: string]: any } = { ...(current ?? {}) }
Object.assign(entry, { "@name": name, ...attrs }, desired)
url.searchParams.set("name", name)
await rest(auth, url, current ? "PUT" : "POST", { entry: [entry] })
return { changed, operation, diff, applied: true, dry_run: false }
}
const REQUIRED_LISTS = [
"from",
"to",
"source",
"destination",
"source-user",
"application",
"service",
"category",
]
// A firewall vsys has one rulebase (SecurityRules); Panorama has pre and post rulebases.
function ruleResource(
location: string | undefined,
rulebase: string | undefined
) {
if ((location || "vsys") === "vsys") return "SecurityRules"
return rulebase === "post" ? "SecurityPostRules" : "SecurityPreRules"
}
// Order-insensitive comparison of member lists (a single member may come back as a string).
function canon(v: any): string {
if (Array.isArray(v)) return JSON.stringify(v.map(canon).sort())
if (v && typeof v === "object") {
return JSON.stringify(
Object.keys(v)
.sort()
.map((k) => [
k,
canon(k === "member" && !Array.isArray(v[k]) ? [v[k]] : v[k]),
])
)
}
return JSON.stringify(v ?? null)
}
async function rest(auth: RT.PaloAltoPanos, url: URL, method: string, body?: any) {
const response = await fetch(url, {
method,
headers: {
"X-PAN-KEY": auth.api_key,
...(body ? { "Content-Type": "application/json" } : {}),
Accept: "application/json",
},
body: body ? JSON.stringify(body) : undefined,
})
const text = await response.text()
if (!response.ok) {
throw new Error(`${response.status} ${text}`)
}
const data = JSON.parse(text)
if (data["@status"] !== "success") {
throw new Error(`${response.status} ${text}`)
}
return data
}
function setLocation(
url: URL,
location: string | undefined,
vsys: string | undefined,
device_group: string | undefined
) {
const loc = location || "vsys"
url.searchParams.set("location", loc)
if (loc === "vsys") {
url.searchParams.set("vsys", vsys || "vsys1")
return { "@location": loc, "@vsys": vsys || "vsys1" }
}
if (loc === "device-group") {
if (!device_group) {
throw new Error("device_group is required when location is device-group")
}
url.searchParams.set("device-group", device_group)
return { "@location": loc, "@device-group": device_group }
}
return { "@location": loc }
}
async function restApiVersion(auth: RT.PaloAltoPanos) {
if (auth.rest_api_version) return auth.rest_api_version
const url = new URL("/api/", auth.base_url)
url.searchParams.set("type", "op")
url.searchParams.set("cmd", "<show><system><info></info></system></show>")
const response = await fetch(url, { headers: { "X-PAN-KEY": auth.api_key } })
const text = await response.text()
if (!response.ok || text.includes('status="error"')) {
throw new Error(`${response.status} ${text}`)
}
const match = text.match(/<sw-version>(\d+)\.(\d+)/)
if (!match) {
throw new Error(
`Could not read sw-version from show system info; set rest_api_version on the resource. ${text}`
)
}
return `v${match[1]}.${match[2]}`
}
Submitted by hugo989 7 hours ago