fix(cortex-xdr): say what went wrong when the API answers something other than JSON (v1.2.1)
A tenant URL that is not the API host answers 200 with the console's HTML shell, and the client turned that into "Expecting value: line 1 column 1 (char 0)" — a JSON parse error that names neither the URL nor the response. The shared client now reports the status, content type and first bytes of any non-JSON reply, and points at the API host the operator was meant to copy. Around it, the URL is reduced to scheme + host so a pasted /public_api/v1 or console path cannot double the API root, a missing key or key ID is refused before the request, and 401/403 recalls the three usual causes (auth mode, revoked key, clock skew). Nonce and timestamp now ride along in standard mode too, matching the reference client, and test_connection probes get_incidents — the read that ingestion actually depends on — instead of the distribution versions.
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
id: cortex_xdr
|
id: cortex_xdr
|
||||||
name: Cortex XDR
|
name: Cortex XDR
|
||||||
version: 1.2.0
|
version: 1.2.1
|
||||||
description: "Palo Alto Cortex XDR (public API v1) — incident ingestion + write-back, endpoint isolation/scan/delete/tagging, RTR scripts, hash block/allow lists, file quarantine/restore/retrieval, alert exclusions, external alert push (parsed/CEF), device-control violations, audits, distributions and RBAC/risk."
|
description: "Palo Alto Cortex XDR (public API v1) — incident ingestion + write-back, endpoint isolation/scan/delete/tagging, RTR scripts, hash block/allow lists, file quarantine/restore/retrieval, alert exclusions, external alert push (parsed/CEF), device-control violations, audits, distributions and RBAC/risk."
|
||||||
changelog: "1.2.0 — Incident write-back (update_incident: status/severity/assignment/resolve comment) and external alert push (insert_parsed_alerts, insert_cef_alerts). 1.1.0 — Full command coverage: added delete/alias/tag endpoints, abort scan, original alerts, script metadata/code/snippet/exec-status, file retrieval (+details), alert exclusions, device-control violations, audits, distribution url/status/create and RBAC (users, roles, groups, risk score, risky users/hosts). 1.0.0 — Initial release: incident ingestion (get_incidents) with OCSF mapper, endpoints, isolate/unisolate, scan, hash blocklist/allowlist, quarantine/restore, run script + results, alerts retrieval, distributions and action status. Standard or Advanced API authentication."
|
changelog: "1.2.1 — Connection troubleshooting: the URL is normalised to the tenant host (a pasted /public_api/v1 or console path no longer breaks the call), a non-JSON reply reports the status, content type and body instead of a bare JSON parse error, missing key/key ID is caught up front, nonce and timestamp are sent in both auth modes as the reference client does, and test_connection now probes get_incidents. 1.2.0 — Incident write-back (update_incident: status/severity/assignment/resolve comment) and external alert push (insert_parsed_alerts, insert_cef_alerts). 1.1.0 — Full command coverage: added delete/alias/tag endpoints, abort scan, original alerts, script metadata/code/snippet/exec-status, file retrieval (+details), alert exclusions, device-control violations, audits, distribution url/status/create and RBAC (users, roles, groups, risk score, risky users/hosts). 1.0.0 — Initial release: incident ingestion (get_incidents) with OCSF mapper, endpoints, isolate/unisolate, scan, hash blocklist/allowlist, quarantine/restore, run script + results, alerts retrieval, distributions and action status. Standard or Advanced API authentication."
|
||||||
category: endpoint
|
category: endpoint
|
||||||
|
|
||||||
# Per-instance configuration. The base URL is the tenant API root, e.g.
|
# Per-instance configuration. The base URL is the tenant API root, e.g.
|
||||||
@@ -11,7 +11,7 @@ config_schema:
|
|||||||
properties:
|
properties:
|
||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
description: "Cortex XDR API base URL, e.g. https://api-<tenant>.xdr.<region>.paloaltonetworks.com"
|
description: "Tenant API host — Settings → Configurations → API Keys → Copy URL, e.g. https://api-<tenant>.xdr.<region>.paloaltonetworks.com. Not the console URL you log into (that one answers with HTML, not JSON)."
|
||||||
api_key_id:
|
api_key_id:
|
||||||
type: string
|
type: string
|
||||||
description: "API Key ID (the integer ID shown next to the API key in Settings → API Keys)"
|
description: "API Key ID (the integer ID shown next to the API key in Settings → API Keys)"
|
||||||
@@ -21,7 +21,7 @@ config_schema:
|
|||||||
x-soar-sensitive: true
|
x-soar-sensitive: true
|
||||||
auth_type:
|
auth_type:
|
||||||
type: string
|
type: string
|
||||||
description: "Authentication method: standard (key sent as-is) or advanced (nonce + timestamp SHA-256 hash)"
|
description: "How the key was created in Cortex XDR: standard (key sent as-is) or advanced (nonce + timestamp SHA-256 hash). A mismatch here is rejected with 401."
|
||||||
default: standard
|
default: standard
|
||||||
required:
|
required:
|
||||||
- url
|
- url
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def csv(v):
|
def csv(v):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def csv(v):
|
def csv(v):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -3,29 +3,61 @@ from datetime import datetime
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def csv(v):
|
def csv(v):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def csv(v):
|
def csv(v):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -3,29 +3,61 @@ from datetime import datetime
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def to_ms(v):
|
def to_ms(v):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def csv(v):
|
def csv(v):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def csv(v):
|
def csv(v):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def csv(v):
|
def csv(v):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def csv(v):
|
def csv(v):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def csv(v):
|
def csv(v):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def csv(v):
|
def csv(v):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_raw(path, body):
|
def post_raw(path, body):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data=None):
|
def post(path, request_data=None):
|
||||||
|
|||||||
@@ -2,39 +2,83 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print(json.dumps(post("/distributions/get_versions/", {})))
|
# Same probe as the reference client: reading one incident proves the URL,
|
||||||
|
# the key pair and the "View Cases and Issues" right ingestion depends on.
|
||||||
|
reply = post("/incidents/get_incidents/", {"search_from": 0, "search_to": 1}).get("reply", {})
|
||||||
|
print(json.dumps({"ok": True, "total_count": reply.get("total_count")}))
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
main()
|
main()
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
detail = e.read().decode("utf-8", "replace")
|
||||||
|
if e.code in (401, 403):
|
||||||
|
# The two ways a valid-looking key still gets rejected: the wrong auth
|
||||||
|
# mode for the key, or a host clock too far from the tenant's.
|
||||||
|
detail += (
|
||||||
|
" — check auth_type matches how the key was created in Cortex XDR"
|
||||||
|
" (Standard vs Advanced), that the key has not been revoked, and that"
|
||||||
|
" this host's clock is in sync"
|
||||||
|
)
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": detail}))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(json.dumps({"error": str(e)}))
|
print(json.dumps({"error": str(e)}))
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err
|
|||||||
|
|
||||||
|
|
||||||
def _client():
|
def _client():
|
||||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}")
|
||||||
base = s.get("url", "").rstrip("/") + "/public_api/v1"
|
raw = str(s.get("url") or "").strip().rstrip("/")
|
||||||
|
if not raw:
|
||||||
|
raise ValueError("no url configured — paste the tenant API URL (Cortex XDR > Settings > Configurations > API Keys > Copy URL)")
|
||||||
|
if "://" not in raw:
|
||||||
|
raw = "https://" + raw
|
||||||
|
# The tenant URL is a bare host. Drop whatever was pasted after it (a stray
|
||||||
|
# /public_api/v1, a console path) so the API root is built exactly once.
|
||||||
|
scheme, _, rest = raw.partition("://")
|
||||||
|
base = scheme + "://" + rest.split("/")[0] + "/public_api/v1"
|
||||||
key = s.get("api_key", "")
|
key = s.get("api_key", "")
|
||||||
kid = str(s.get("api_key_id", ""))
|
kid = str(s.get("api_key_id", ""))
|
||||||
headers = {"x-xdr-auth-id": kid, "Content-Type": "application/json", "Accept": "application/json"}
|
if not key or not kid:
|
||||||
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
raise ValueError("api_key and api_key_id are both required")
|
||||||
|
# Nonce and timestamp ride along in both modes, as the reference client does.
|
||||||
|
# A standard key travels as-is; an advanced one as sha256(key + nonce + ts).
|
||||||
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||||
ts = str(int(time.time()) * 1000)
|
ts = str(int(time.time()) * 1000)
|
||||||
headers["x-xdr-nonce"] = nonce
|
headers = {
|
||||||
headers["x-xdr-timestamp"] = ts
|
"x-xdr-auth-id": kid,
|
||||||
|
"x-xdr-nonce": nonce,
|
||||||
|
"x-xdr-timestamp": ts,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if str(s.get("auth_type") or "standard").lower() == "advanced":
|
||||||
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest()
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = key
|
headers["Authorization"] = key
|
||||||
return base, headers
|
return base, headers
|
||||||
|
|
||||||
|
|
||||||
|
def _not_json(r, raw):
|
||||||
|
"""A 2xx that is not JSON means we are not talking to the XDR API at all."""
|
||||||
|
ctype = (r.headers.get("Content-Type") or "unknown").split(";")[0].strip()
|
||||||
|
head = raw[:160].decode("utf-8", "replace").replace("\n", " ").strip()
|
||||||
|
return (
|
||||||
|
"expected JSON from " + r.geturl() + ", got " + ctype + " (HTTP " + str(r.status) + "): " + head
|
||||||
|
+ " — check the configured url is the tenant API host"
|
||||||
|
+ " (https://api-<tenant>.xdr.<region>.paloaltonetworks.com), not the console URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def post(path, request_data):
|
def post(path, request_data):
|
||||||
base, headers = _client()
|
base, headers = _client()
|
||||||
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
data = json.dumps({"request_data": request_data}).encode("utf-8")
|
||||||
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
req = urllib.request.Request(base + path, data=data, headers=headers, method="POST")
|
||||||
with urllib.request.urlopen(req, timeout=90) as r:
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
raw = r.read()
|
raw = r.read()
|
||||||
return json.loads(raw) if raw else {}
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(_not_json(r, raw))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
Reference in New Issue
Block a user