diff --git a/integrations/cortex-xdr/manifest.yaml b/integrations/cortex-xdr/manifest.yaml index fe48fb4..1aeae6e 100644 --- a/integrations/cortex-xdr/manifest.yaml +++ b/integrations/cortex-xdr/manifest.yaml @@ -1,8 +1,8 @@ id: 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." -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 # Per-instance configuration. The base URL is the tenant API root, e.g. @@ -11,7 +11,7 @@ config_schema: properties: url: type: string - description: "Cortex XDR API base URL, e.g. https://api-.xdr..paloaltonetworks.com" + description: "Tenant API host — Settings → Configurations → API Keys → Copy URL, e.g. https://api-.xdr..paloaltonetworks.com. Not the console URL you log into (that one answers with HTML, not JSON)." api_key_id: type: string 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 auth_type: 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 required: - url diff --git a/integrations/cortex-xdr/scripts/abort_scan.py b/integrations/cortex-xdr/scripts/abort_scan.py index c9784c5..3972402 100644 --- a/integrations/cortex-xdr/scripts/abort_scan.py +++ b/integrations/cortex-xdr/scripts/abort_scan.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/action_status.py b/integrations/cortex-xdr/scripts/action_status.py index 8a50e80..c1cf753 100644 --- a/integrations/cortex-xdr/scripts/action_status.py +++ b/integrations/cortex-xdr/scripts/action_status.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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(): diff --git a/integrations/cortex-xdr/scripts/add_exclusion.py b/integrations/cortex-xdr/scripts/add_exclusion.py index 7e7faab..345b9b5 100644 --- a/integrations/cortex-xdr/scripts/add_exclusion.py +++ b/integrations/cortex-xdr/scripts/add_exclusion.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/add_tag_endpoint.py b/integrations/cortex-xdr/scripts/add_tag_endpoint.py index 7956fc8..2d09752 100644 --- a/integrations/cortex-xdr/scripts/add_tag_endpoint.py +++ b/integrations/cortex-xdr/scripts/add_tag_endpoint.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/allowlist_files.py b/integrations/cortex-xdr/scripts/allowlist_files.py index 959e8fa..21a54ff 100644 --- a/integrations/cortex-xdr/scripts/allowlist_files.py +++ b/integrations/cortex-xdr/scripts/allowlist_files.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/audit_management_logs.py b/integrations/cortex-xdr/scripts/audit_management_logs.py index 7d36a56..28747c6 100644 --- a/integrations/cortex-xdr/scripts/audit_management_logs.py +++ b/integrations/cortex-xdr/scripts/audit_management_logs.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/blocklist_files.py b/integrations/cortex-xdr/scripts/blocklist_files.py index f64469c..1567491 100644 --- a/integrations/cortex-xdr/scripts/blocklist_files.py +++ b/integrations/cortex-xdr/scripts/blocklist_files.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/create_distribution.py b/integrations/cortex-xdr/scripts/create_distribution.py index 472414c..553b73d 100644 --- a/integrations/cortex-xdr/scripts/create_distribution.py +++ b/integrations/cortex-xdr/scripts/create_distribution.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/delete_endpoints.py b/integrations/cortex-xdr/scripts/delete_endpoints.py index d89ae96..a2368bb 100644 --- a/integrations/cortex-xdr/scripts/delete_endpoints.py +++ b/integrations/cortex-xdr/scripts/delete_endpoints.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/delete_exclusion.py b/integrations/cortex-xdr/scripts/delete_exclusion.py index a0736d1..6cadbc8 100644 --- a/integrations/cortex-xdr/scripts/delete_exclusion.py +++ b/integrations/cortex-xdr/scripts/delete_exclusion.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_alerts.py b/integrations/cortex-xdr/scripts/get_alerts.py index c8fc9b2..e9ff5fb 100644 --- a/integrations/cortex-xdr/scripts/get_alerts.py +++ b/integrations/cortex-xdr/scripts/get_alerts.py @@ -3,29 +3,61 @@ from datetime import datetime def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_audit_agent_reports.py b/integrations/cortex-xdr/scripts/get_audit_agent_reports.py index 3c50e60..23eb5a1 100644 --- a/integrations/cortex-xdr/scripts/get_audit_agent_reports.py +++ b/integrations/cortex-xdr/scripts/get_audit_agent_reports.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_device_control_violations.py b/integrations/cortex-xdr/scripts/get_device_control_violations.py index d530229..abec9a8 100644 --- a/integrations/cortex-xdr/scripts/get_device_control_violations.py +++ b/integrations/cortex-xdr/scripts/get_device_control_violations.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_distribution_status.py b/integrations/cortex-xdr/scripts/get_distribution_status.py index 817b73c..0ef64d9 100644 --- a/integrations/cortex-xdr/scripts/get_distribution_status.py +++ b/integrations/cortex-xdr/scripts/get_distribution_status.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_distribution_url.py b/integrations/cortex-xdr/scripts/get_distribution_url.py index ff720e0..3dc0519 100644 --- a/integrations/cortex-xdr/scripts/get_distribution_url.py +++ b/integrations/cortex-xdr/scripts/get_distribution_url.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_distribution_versions.py b/integrations/cortex-xdr/scripts/get_distribution_versions.py index e4fdb4a..5b5eb19 100644 --- a/integrations/cortex-xdr/scripts/get_distribution_versions.py +++ b/integrations/cortex-xdr/scripts/get_distribution_versions.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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(): diff --git a/integrations/cortex-xdr/scripts/get_endpoints.py b/integrations/cortex-xdr/scripts/get_endpoints.py index 87ac1b9..93e7795 100644 --- a/integrations/cortex-xdr/scripts/get_endpoints.py +++ b/integrations/cortex-xdr/scripts/get_endpoints.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_exclusion.py b/integrations/cortex-xdr/scripts/get_exclusion.py index bb10c74..ebff00c 100644 --- a/integrations/cortex-xdr/scripts/get_exclusion.py +++ b/integrations/cortex-xdr/scripts/get_exclusion.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_incident_extra_data.py b/integrations/cortex-xdr/scripts/get_incident_extra_data.py index 301a1fb..4a3ca10 100644 --- a/integrations/cortex-xdr/scripts/get_incident_extra_data.py +++ b/integrations/cortex-xdr/scripts/get_incident_extra_data.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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(): diff --git a/integrations/cortex-xdr/scripts/get_incidents.py b/integrations/cortex-xdr/scripts/get_incidents.py index 8789ec9..0be322e 100644 --- a/integrations/cortex-xdr/scripts/get_incidents.py +++ b/integrations/cortex-xdr/scripts/get_incidents.py @@ -3,29 +3,61 @@ from datetime import datetime def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_original_alerts.py b/integrations/cortex-xdr/scripts/get_original_alerts.py index 9d002ff..3c94130 100644 --- a/integrations/cortex-xdr/scripts/get_original_alerts.py +++ b/integrations/cortex-xdr/scripts/get_original_alerts.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_policy.py b/integrations/cortex-xdr/scripts/get_policy.py index 32ec446..94bd540 100644 --- a/integrations/cortex-xdr/scripts/get_policy.py +++ b/integrations/cortex-xdr/scripts/get_policy.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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(): diff --git a/integrations/cortex-xdr/scripts/get_quarantine_status.py b/integrations/cortex-xdr/scripts/get_quarantine_status.py index 083a362..e132ac5 100644 --- a/integrations/cortex-xdr/scripts/get_quarantine_status.py +++ b/integrations/cortex-xdr/scripts/get_quarantine_status.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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(): diff --git a/integrations/cortex-xdr/scripts/get_risk_score.py b/integrations/cortex-xdr/scripts/get_risk_score.py index a854296..bb9497e 100644 --- a/integrations/cortex-xdr/scripts/get_risk_score.py +++ b/integrations/cortex-xdr/scripts/get_risk_score.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_script_code.py b/integrations/cortex-xdr/scripts/get_script_code.py index 271ffaa..04e67d3 100644 --- a/integrations/cortex-xdr/scripts/get_script_code.py +++ b/integrations/cortex-xdr/scripts/get_script_code.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_script_execution_results.py b/integrations/cortex-xdr/scripts/get_script_execution_results.py index 457d376..8407128 100644 --- a/integrations/cortex-xdr/scripts/get_script_execution_results.py +++ b/integrations/cortex-xdr/scripts/get_script_execution_results.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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(): diff --git a/integrations/cortex-xdr/scripts/get_script_execution_status.py b/integrations/cortex-xdr/scripts/get_script_execution_status.py index 440d0d7..d700510 100644 --- a/integrations/cortex-xdr/scripts/get_script_execution_status.py +++ b/integrations/cortex-xdr/scripts/get_script_execution_status.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_script_metadata.py b/integrations/cortex-xdr/scripts/get_script_metadata.py index ff14f07..2027865 100644 --- a/integrations/cortex-xdr/scripts/get_script_metadata.py +++ b/integrations/cortex-xdr/scripts/get_script_metadata.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/get_scripts.py b/integrations/cortex-xdr/scripts/get_scripts.py index cff3417..c7cb26d 100644 --- a/integrations/cortex-xdr/scripts/get_scripts.py +++ b/integrations/cortex-xdr/scripts/get_scripts.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/insert_cef_alerts.py b/integrations/cortex-xdr/scripts/insert_cef_alerts.py index 324b68b..7388744 100644 --- a/integrations/cortex-xdr/scripts/insert_cef_alerts.py +++ b/integrations/cortex-xdr/scripts/insert_cef_alerts.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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(): diff --git a/integrations/cortex-xdr/scripts/insert_parsed_alerts.py b/integrations/cortex-xdr/scripts/insert_parsed_alerts.py index 891ba97..a026baa 100644 --- a/integrations/cortex-xdr/scripts/insert_parsed_alerts.py +++ b/integrations/cortex-xdr/scripts/insert_parsed_alerts.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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(): diff --git a/integrations/cortex-xdr/scripts/isolate_endpoint.py b/integrations/cortex-xdr/scripts/isolate_endpoint.py index 9d49b2d..0e9ce5a 100644 --- a/integrations/cortex-xdr/scripts/isolate_endpoint.py +++ b/integrations/cortex-xdr/scripts/isolate_endpoint.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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(): diff --git a/integrations/cortex-xdr/scripts/list_risky_hosts.py b/integrations/cortex-xdr/scripts/list_risky_hosts.py index 1d7e924..07ba42e 100644 --- a/integrations/cortex-xdr/scripts/list_risky_hosts.py +++ b/integrations/cortex-xdr/scripts/list_risky_hosts.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/list_risky_users.py b/integrations/cortex-xdr/scripts/list_risky_users.py index 773f792..471ec5e 100644 --- a/integrations/cortex-xdr/scripts/list_risky_users.py +++ b/integrations/cortex-xdr/scripts/list_risky_users.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/list_roles.py b/integrations/cortex-xdr/scripts/list_roles.py index 0dd92f9..e372da7 100644 --- a/integrations/cortex-xdr/scripts/list_roles.py +++ b/integrations/cortex-xdr/scripts/list_roles.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/list_user_groups.py b/integrations/cortex-xdr/scripts/list_user_groups.py index 3f3961a..e3f22e7 100644 --- a/integrations/cortex-xdr/scripts/list_user_groups.py +++ b/integrations/cortex-xdr/scripts/list_user_groups.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/list_users.py b/integrations/cortex-xdr/scripts/list_users.py index 43cd549..a9ce4bf 100644 --- a/integrations/cortex-xdr/scripts/list_users.py +++ b/integrations/cortex-xdr/scripts/list_users.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/quarantine_files.py b/integrations/cortex-xdr/scripts/quarantine_files.py index b94045f..ee2ac4c 100644 --- a/integrations/cortex-xdr/scripts/quarantine_files.py +++ b/integrations/cortex-xdr/scripts/quarantine_files.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/remove_allowlist_files.py b/integrations/cortex-xdr/scripts/remove_allowlist_files.py index 11c0fe2..aa09b89 100644 --- a/integrations/cortex-xdr/scripts/remove_allowlist_files.py +++ b/integrations/cortex-xdr/scripts/remove_allowlist_files.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/remove_blocklist_files.py b/integrations/cortex-xdr/scripts/remove_blocklist_files.py index e0f34c4..9c4caa4 100644 --- a/integrations/cortex-xdr/scripts/remove_blocklist_files.py +++ b/integrations/cortex-xdr/scripts/remove_blocklist_files.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/remove_tag_endpoint.py b/integrations/cortex-xdr/scripts/remove_tag_endpoint.py index 6995ee9..0765c66 100644 --- a/integrations/cortex-xdr/scripts/remove_tag_endpoint.py +++ b/integrations/cortex-xdr/scripts/remove_tag_endpoint.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/remove_user_role.py b/integrations/cortex-xdr/scripts/remove_user_role.py index 5420513..1135054 100644 --- a/integrations/cortex-xdr/scripts/remove_user_role.py +++ b/integrations/cortex-xdr/scripts/remove_user_role.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/restore_file.py b/integrations/cortex-xdr/scripts/restore_file.py index 01133f4..32931b7 100644 --- a/integrations/cortex-xdr/scripts/restore_file.py +++ b/integrations/cortex-xdr/scripts/restore_file.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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(): diff --git a/integrations/cortex-xdr/scripts/retrieve_file.py b/integrations/cortex-xdr/scripts/retrieve_file.py index d9265e2..d46048c 100644 --- a/integrations/cortex-xdr/scripts/retrieve_file.py +++ b/integrations/cortex-xdr/scripts/retrieve_file.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/retrieve_file_details.py b/integrations/cortex-xdr/scripts/retrieve_file_details.py index c2c66e3..b9de6fe 100644 --- a/integrations/cortex-xdr/scripts/retrieve_file_details.py +++ b/integrations/cortex-xdr/scripts/retrieve_file_details.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/run_script.py b/integrations/cortex-xdr/scripts/run_script.py index 214a705..36e484a 100644 --- a/integrations/cortex-xdr/scripts/run_script.py +++ b/integrations/cortex-xdr/scripts/run_script.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/run_snippet_code_script.py b/integrations/cortex-xdr/scripts/run_snippet_code_script.py index e3c4ed8..262039f 100644 --- a/integrations/cortex-xdr/scripts/run_snippet_code_script.py +++ b/integrations/cortex-xdr/scripts/run_snippet_code_script.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/scan_endpoints.py b/integrations/cortex-xdr/scripts/scan_endpoints.py index d8826f2..57178a2 100644 --- a/integrations/cortex-xdr/scripts/scan_endpoints.py +++ b/integrations/cortex-xdr/scripts/scan_endpoints.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/set_endpoint_alias.py b/integrations/cortex-xdr/scripts/set_endpoint_alias.py index a839571..74617e3 100644 --- a/integrations/cortex-xdr/scripts/set_endpoint_alias.py +++ b/integrations/cortex-xdr/scripts/set_endpoint_alias.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/set_user_role.py b/integrations/cortex-xdr/scripts/set_user_role.py index 9e1d13d..1d477ca 100644 --- a/integrations/cortex-xdr/scripts/set_user_role.py +++ b/integrations/cortex-xdr/scripts/set_user_role.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post_raw(path, body): base, headers = _client() 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") with urllib.request.urlopen(req, timeout=90) as r: 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): diff --git a/integrations/cortex-xdr/scripts/test_connection.py b/integrations/cortex-xdr/scripts/test_connection.py index e4fdb4a..39b707f 100644 --- a/integrations/cortex-xdr/scripts/test_connection.py +++ b/integrations/cortex-xdr/scripts/test_connection.py @@ -2,39 +2,83 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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(): - 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: main() 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) except Exception as e: print(json.dumps({"error": str(e)})) diff --git a/integrations/cortex-xdr/scripts/unisolate_endpoint.py b/integrations/cortex-xdr/scripts/unisolate_endpoint.py index ccd76ce..52b70c2 100644 --- a/integrations/cortex-xdr/scripts/unisolate_endpoint.py +++ b/integrations/cortex-xdr/scripts/unisolate_endpoint.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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(): diff --git a/integrations/cortex-xdr/scripts/update_incident.py b/integrations/cortex-xdr/scripts/update_incident.py index eb2f547..bd97d37 100644 --- a/integrations/cortex-xdr/scripts/update_incident.py +++ b/integrations/cortex-xdr/scripts/update_incident.py @@ -2,29 +2,61 @@ import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.err def _client(): - s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - base = s.get("url", "").rstrip("/") + "/public_api/v1" + s = json.loads(os.environ.get("INTEGRATION_SECRETS") or "{}") + 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", "") 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: + 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)) + ts = str(int(time.time()) * 1000) + headers = { + "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": - nonce = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64)) - ts = str(int(time.time()) * 1000) - headers["x-xdr-nonce"] = nonce - headers["x-xdr-timestamp"] = ts headers["Authorization"] = hashlib.sha256((key + nonce + ts).encode("utf-8")).hexdigest() else: headers["Authorization"] = key 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-.xdr..paloaltonetworks.com), not the console URL" + ) + + def post(path, request_data): base, headers = _client() data = json.dumps({"request_data": request_data}).encode("utf-8") req = urllib.request.Request(base + path, data=data, headers=headers, method="POST") with urllib.request.urlopen(req, timeout=90) as r: 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():