Compare commits
48 Commits
7187e7b607
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bf273b959a | |||
| ba68d19e51 | |||
| a6e1245c79 | |||
| d06f8ea413 | |||
| 17318d4225 | |||
| c22982c8af | |||
| 1d475a6cf5 | |||
| 93309ac74d | |||
| a5095d3f0f | |||
| cebb3af796 | |||
| ba6bcd19f4 | |||
| aabe1a6837 | |||
| 50358ef8a1 | |||
| 82f22fdbb3 | |||
| f7c5e99cc5 | |||
| 6343a0c952 | |||
| 7efc873867 | |||
| 144089a099 | |||
| a4b5b44157 | |||
| 4ea3af6c1d | |||
| 952dc7efda | |||
| cba746a9f9 | |||
| 5d6f823189 | |||
| 098b90d6f3 | |||
| db902c35bc | |||
| 59e9211c0d | |||
| fcbd2ff135 | |||
| ef60cec0fa | |||
| 6d993f584f | |||
| 6d311740f9 | |||
| f6b8c84285 | |||
| 5ee7352652 | |||
| 534748f16a | |||
| a2c9f998c3 | |||
| ae7c494658 | |||
| 40252dadcb | |||
| ac2203f9a5 | |||
| 8248c60da8 | |||
| 4206b44e78 | |||
| 9cffefbb36 | |||
| f7791402a3 | |||
| c42a9cef03 | |||
| f6ff018f09 | |||
| c091800434 | |||
| 929b4166d6 | |||
| 381b42b6c7 | |||
| 5715f21603 | |||
| 7543a95711 |
@@ -84,7 +84,7 @@ commands:
|
||||
outputs_schema: { properties: {} }
|
||||
- id: search_alerts
|
||||
name: cbc-search-alerts
|
||||
description: "Search alerts."
|
||||
description: "Search alerts. Used for ingestion: results path = results."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
@@ -92,6 +92,9 @@ commands:
|
||||
rows: { type: number, description: "Max alerts (default 50)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
ingest:
|
||||
results_path: results
|
||||
dedup_key: id
|
||||
- id: ban_hash
|
||||
name: cbc-ban-hash
|
||||
description: "Ban a file by SHA-256 hash (reputation deny-list override)."
|
||||
@@ -119,3 +122,8 @@ commands:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
ingestion:
|
||||
command: search_alerts
|
||||
mapper: search_alerts
|
||||
default_incident_type: "Carbon Black Cloud Alert"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
name: "VMware Carbon Black Cloud Alerts → OCSF"
|
||||
description: "Exhaustive map of a Carbon Black Cloud Alert Search v7 alert (POST /api/alerts/v7/orgs/{org_key}/alerts/_search, results_path = results) to OCSF. Field paths validated against the CBC Platform Alerts v7 API. The alerting process = actor (actor.*); the child/target process spawned as a result = process.*; parent lineage under actor.process.parent_process.*."
|
||||
field_mappings:
|
||||
title: "reason"
|
||||
severity: "severity >= 9 ? 5 : (severity >= 7 ? 4 : (severity >= 5 ? 3 : (severity >= 3 ? 2 : 1)))"
|
||||
description: "type"
|
||||
# results_path = results; source_path is JSONata over ONE alert object.
|
||||
# Paths absent from a given alert (e.g. no childproc.* on a non-process alert type) are
|
||||
# skipped at ingestion, so extra entries are safe.
|
||||
ocsf:
|
||||
# ── Finding / alert identity ───────────────────────────────────────
|
||||
- { source_path: "id", ocsf_field: "finding_info.uid" }
|
||||
- { source_path: "reason", ocsf_field: "finding_info.title" }
|
||||
- { source_path: "type", ocsf_field: "finding_info.desc" }
|
||||
- { source_path: "first_event_timestamp", ocsf_field: "finding_info.created_time" }
|
||||
- { source_path: "backend_update_timestamp", ocsf_field: "finding_info.modified_time" }
|
||||
- { source_path: "alert_url", ocsf_field: "finding_info.src_url" }
|
||||
- { source_path: "workflow.status", ocsf_field: "status" }
|
||||
- { source_path: "determination.value", ocsf_field: "status_detail" }
|
||||
# ── Endpoint / device ──────────────────────────────────────────────
|
||||
- { source_path: "device_id", ocsf_field: "device.uid" }
|
||||
- { source_path: "device_os_version", ocsf_field: "device.os.build" }
|
||||
- { source_path: "device_name", ocsf_field: "src_endpoint.hostname" }
|
||||
- { source_path: "device_os", ocsf_field: "src_endpoint.os.name" }
|
||||
- { source_path: "device_internal_ip", ocsf_field: "src_endpoint.ip" }
|
||||
- { source_path: "device_username", ocsf_field: "user.name" }
|
||||
- { source_path: "org_key", ocsf_field: "cloud.account.uid" }
|
||||
# ── Triggering process — the actor ─────────────────────────────────
|
||||
- { source_path: "process_name", ocsf_field: "actor.process.name" }
|
||||
- { source_path: "process_pid", ocsf_field: "actor.process.pid" }
|
||||
- { source_path: "process_cmdline", ocsf_field: "actor.process.cmd_line" }
|
||||
- { source_path: "process_sha256", ocsf_field: "actor.process.file.hashes.sha256" }
|
||||
- { source_path: "process_md5", ocsf_field: "actor.process.file.hashes.md5" }
|
||||
- { source_path: "process_guid", ocsf_field: "metadata.original_event_uid" }
|
||||
- { source_path: "process_username", ocsf_field: "actor.user.name" }
|
||||
# ── Parent process ──────────────────────────────────────────────────
|
||||
- { source_path: "parent_name", ocsf_field: "actor.process.parent_process.name" }
|
||||
- { source_path: "parent_pid", ocsf_field: "actor.process.parent_process.pid" }
|
||||
- { source_path: "parent_cmdline", ocsf_field: "actor.process.parent_process.cmd_line" }
|
||||
- { source_path: "parent_sha256", ocsf_field: "actor.process.parent_process.file.hashes.sha256" }
|
||||
# ── Child / target process ──────────────────────────────────────────
|
||||
- { source_path: "childproc_name", ocsf_field: "process.name" }
|
||||
- { source_path: "childproc_pid", ocsf_field: "process.pid" }
|
||||
- { source_path: "childproc_cmdline", ocsf_field: "process.cmd_line" }
|
||||
- { source_path: "childproc_guid", ocsf_field: "process.uid" }
|
||||
# ── Detection rule / watchlist / report ─────────────────────────────
|
||||
- { source_path: "watchlists[0].name", ocsf_field: "rule.name" }
|
||||
- { source_path: "watchlists[0].id", ocsf_field: "rule.uid" }
|
||||
- { source_path: "report_name", ocsf_field: "rule.desc" }
|
||||
# ── IOC / observable ──────────────────────────────────────────────
|
||||
- { source_path: "ioc_hit", ocsf_field: "observables.value" }
|
||||
- { source_path: "ioc_field", ocsf_field: "observables.type" }
|
||||
@@ -62,7 +62,7 @@ commands:
|
||||
outputs_schema: { properties: {} }
|
||||
- id: list_events
|
||||
name: cisco-amp-list-events
|
||||
description: "List events."
|
||||
description: "List events. Used for ingestion: results path = data."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
@@ -70,6 +70,9 @@ commands:
|
||||
limit: { type: number, description: "Max events (default 50)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
ingest:
|
||||
results_path: data
|
||||
dedup_key: id
|
||||
- id: get_trajectory
|
||||
name: cisco-amp-get-trajectory
|
||||
description: "Get a computer's device trajectory."
|
||||
@@ -88,3 +91,8 @@ commands:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
ingestion:
|
||||
command: list_events
|
||||
mapper: list_events
|
||||
default_incident_type: "Cisco Secure Endpoint Event"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
name: "Cisco Secure Endpoint Events → OCSF"
|
||||
description: "Maps a Cisco AMP for Endpoints event object (GET /v1/events, results_path = data) to OCSF. The detected file is the subject (file.*/malware.*); the process that dropped/ran it (file.parent.*) is the actor (actor.process.*). Field paths validated against the AMP API v1 event schema (Cisco docs + demisto/content CiscoAMP pack README)."
|
||||
field_mappings:
|
||||
title: "detection ? detection : event_type"
|
||||
severity: "severity = 'Critical' ? 5 : (severity = 'High' ? 4 : (severity = 'Medium' ? 3 : 2))"
|
||||
description: "event_type"
|
||||
# results_path = data; source_path is JSONata over ONE event object.
|
||||
# Paths absent from a given event are skipped at ingestion, so extra entries are safe.
|
||||
ocsf:
|
||||
# ── Endpoint / device ──────────────────────────────────────────────
|
||||
- { source_path: "computer.hostname", ocsf_field: "src_endpoint.hostname" }
|
||||
- { source_path: "computer.external_ip", ocsf_field: "src_endpoint.ip" }
|
||||
- { source_path: "computer.connector_guid", ocsf_field: "device.uid" }
|
||||
- { source_path: "computer.user", ocsf_field: "user.name" }
|
||||
# ── Finding / analytic ─────────────────────────────────────────────
|
||||
- { source_path: "id", ocsf_field: "finding_info.uid" }
|
||||
- { source_path: "timestamp", ocsf_field: "finding_info.created_time" }
|
||||
- { source_path: "event_type", ocsf_field: "finding_info.analytic.name" }
|
||||
- { source_path: "event_type_id", ocsf_field: "finding_info.analytic.uid" }
|
||||
- { source_path: "file.disposition", ocsf_field: "disposition" }
|
||||
# ── Malware / detection ────────────────────────────────────────────
|
||||
- { source_path: "detection", ocsf_field: "malware.name" }
|
||||
- { source_path: "detection_id", ocsf_field: "malware.uid" }
|
||||
# ── Detected file — the subject ────────────────────────────────────
|
||||
- { source_path: "file.file_name", ocsf_field: "file.name" }
|
||||
- { source_path: "file.file_path", ocsf_field: "file.path" }
|
||||
- { source_path: "file.identity.sha256", ocsf_field: "file.hashes.sha256" }
|
||||
- { source_path: "file.identity.sha1", ocsf_field: "file.hashes.sha1" }
|
||||
- { source_path: "file.identity.md5", ocsf_field: "file.hashes.md5" }
|
||||
# ── Parent process (dropped/ran the file) — the actor ──────────────
|
||||
- { source_path: "file.parent.file_name", ocsf_field: "actor.process.name" }
|
||||
- { source_path: "file.parent.process_id", ocsf_field: "actor.process.pid" }
|
||||
- { source_path: "file.parent.identity.sha256", ocsf_field: "actor.process.file.hashes.sha256" }
|
||||
- { source_path: "file.parent.identity.sha1", ocsf_field: "actor.process.file.hashes.sha1" }
|
||||
- { source_path: "file.parent.identity.md5", ocsf_field: "actor.process.file.hashes.md5" }
|
||||
@@ -0,0 +1,3 @@
|
||||
name: "Cortex XDR Alert"
|
||||
color: "#ef8354"
|
||||
icon: "alert"
|
||||
@@ -1,8 +1,8 @@
|
||||
id: cortex_xdr
|
||||
name: Cortex XDR
|
||||
version: 1.2.0
|
||||
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."
|
||||
version: 1.3.1
|
||||
description: "Palo Alto Cortex XDR (public API v1) — incident and alert 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.3.1 — get_incidents_full can watermark on modification_time (modified_after), which is what ingestion wants: an XDR incident keeps growing after creation, and a creation_time watermark fetches it once and never looks again, so every alert that joins it afterwards is lost. The full-incident fetch command is named cortex-xdr-get-incidents-full (id get_incidents_full), not cortex-xdr-fetch-incidents: sitting next to cortex-xdr-get-incidents in the command list, it now reads as what it is — the same call, everything included. A rule created against the old id must be pointed at the new one. 1.3.0 — Richer incident ingestion (get_incidents_full, on get_multiple_incidents_extra_data): incidents now arrive with their alerts in full and their file/network artifacts, where get_incidents only ever answered a 21-field summary carrying neither hosts, users, MITRE nor a single alert. Alert ingestion, and the alerts endpoint answers at last: get_alerts was sending the incidents dialect ({filters, search_from, search_to, sort}) to a grid endpoint that speaks request_data.filter_data (SEARCH_FIELD/SEARCH_TYPE/SEARCH_VALUE blocks, paging.from/to, sort as a list), and every call came back HTTP 500. Body rebuilt from the reference client, rows unwrapped out of their alert_fields envelope, severity code and the dotted status.progress key derived into readable fields. Alert ingestion: get_alerts is now a fetch command (results path reply.alerts, dedup on alert_id, incremental on source_insert_ts) with a bundled OCSF mapper and a Cortex XDR Alert incident type, so detections can be ingested alongside — or instead of — incidents. The incident mapper is fixed on the way past: its severity expression compared strings, which the mapping engine cannot do, so every ingested incident silently took the rule's default severity; it also now carries a detection anchor so MTTD is measurable. It pages past the API's 100-results-per-call ceiling, and sorts oldest-first on an incremental fetch so a truncated window drops the alerts the next poll can still see. 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-<tenant>.xdr.<region>.paloaltonetworks.com"
|
||||
description: "Tenant API host — Settings → Configurations → API Keys → Copy URL, e.g. https://api-<tenant>.xdr.<region>.paloaltonetworks.com. Not the console URL you log into (that one answers with HTML, not JSON)."
|
||||
api_key_id:
|
||||
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
|
||||
@@ -44,7 +44,7 @@ commands:
|
||||
# ── Ingestion ─────────────────────────────────────────────────────────────
|
||||
- id: get_incidents
|
||||
name: cortex-xdr-get-incidents
|
||||
description: "Fetch Cortex XDR incidents for ingestion. Returns {reply:{incidents:[...]}}; use reply.incidents as the alert rule results path."
|
||||
description: "List Cortex XDR incidents as a 21-field summary (no hosts, no users, no MITRE, no alerts). Cheap to poll, but for ingestion prefer cortex-xdr-get-incidents-full, which returns the same incidents with their alerts and artifacts. Returns {reply:{incidents:[...]}}."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
@@ -57,6 +57,23 @@ commands:
|
||||
results_path: reply.incidents
|
||||
dedup_key: incident_id
|
||||
incremental_field: created_after
|
||||
- id: get_incidents_full
|
||||
name: cortex-xdr-get-incidents-full
|
||||
description: "Fetch incidents WITH their alerts and artifacts (get_multiple_incidents_extra_data) — the ingestion command to prefer. get_incidents answers with a 21-field summary carrying no hosts, no users, no MITRE and none of the alerts; this one returns 39 incident fields, every alert in full (156 fields each) and the file/network artifacts. Records are flattened, so mapping expressions written against get_incidents keep working and alerts[], file_artifacts[], network_artifacts[] sit beside them. Returns {reply:{incidents:[...]}}."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
status: { type: string, description: "Comma-separated statuses to keep (new, under_investigation, resolved_threat_handled…)" }
|
||||
created_after: { type: string, description: "Lower bound on creation_time, ISO8601 or epoch ms. Watermarking on this fetches each incident once and never revisits it — alerts joining it later never arrive." }
|
||||
modified_after: { type: string, description: "Lower bound on modification_time, ISO8601 or epoch ms. The watermark to prefer for ingestion: an incident comes back whenever it changes, and dedup on incident_id turns the second visit into an enrichment." }
|
||||
limit: { type: number, description: "Maximum incidents to fetch (default 50, paged 50 at a time). A full incident weighs a few KB and up to a few hundred with its alerts, so raise this knowingly." }
|
||||
exclude_artifacts: { type: boolean, description: "Drop the file and network artifact blocks, keeping the alerts (lighter payload)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
ingest:
|
||||
results_path: reply.incidents
|
||||
dedup_key: incident_id
|
||||
incremental_field: modified_after
|
||||
- id: get_incident_extra_data
|
||||
name: cortex-xdr-get-incident-extra-data
|
||||
description: "Get full incident data including its alerts and network artifacts by incident ID."
|
||||
@@ -83,15 +100,21 @@ commands:
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_alerts
|
||||
name: cortex-xdr-get-alerts
|
||||
description: "Retrieve alerts using a custom filter (get_alerts_by_filter_data). Returns rich alert objects."
|
||||
description: "Fetch Cortex XDR alerts for ingestion (get_alerts_by_filter_data). Returns {reply:{alerts:[...]}}; use reply.alerts as the alert rule results path. Each row is unwrapped out of the API's alert_fields envelope and carries a readable severity_name and status_progress, so alerts-grid field names (internal_id, alert_name, agent_hostname) are what mapping expressions see. Alerts are the detection layer under incidents: ingest them alongside get_incidents when the SOC works detections, not only aggregates."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
severity: { type: string, description: "Comma-separated severities (low, medium, high, critical)" }
|
||||
created_after: { type: string, description: "Lower bound on alert source_insert_ts, epoch ms" }
|
||||
limit: { type: number, description: "Maximum alerts to fetch (default 100)" }
|
||||
severity: { type: string, description: "Comma-separated severities (informational, low, medium, high, critical)" }
|
||||
created_after: { type: string, description: "Lower bound on alert source_insert_ts, ISO8601 or epoch ms (incremental fetch watermark)" }
|
||||
limit: { type: number, description: "Maximum alerts to fetch (default 100). The API serves 100 per call at most; above that the script pages until the limit is reached." }
|
||||
# Left unfiltered, the call is bounded to the last 30 days: the alerts
|
||||
# grid is not meant to be asked for a tenant's whole retention.
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
ingest:
|
||||
results_path: reply.alerts
|
||||
dedup_key: internal_id
|
||||
incremental_field: created_after
|
||||
- id: insert_parsed_alerts
|
||||
name: cortex-xdr-insert-parsed-alerts
|
||||
description: "Push external alerts (parsed JSON objects) into Cortex XDR for correlation."
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
name: "Cortex XDR Alerts → OCSF"
|
||||
description: "Maps one Cortex XDR alert (alerts/get_alerts_by_filter_data/, results_path = reply.alerts) to OCSF Detection Finding fields. Field names are the alerts-grid ones (internal_id, alert_name, agent_hostname…), not the incident ones; the script unwraps the API's alert_fields envelope and derives severity_name and status_progress, which the raw payload cannot express. Alerts are the detection layer under incidents: a tenant ingesting both feeds holds each detection twice, once inside an aggregate and once on its own."
|
||||
field_mappings:
|
||||
title: "alert_name"
|
||||
description: "alert_description"
|
||||
# severity_name, not severity: the API sends an enum code (SEV_040_HIGH) that
|
||||
# no severity scale can read, so the script carries the plain name alongside it.
|
||||
severity: "severity_name"
|
||||
# Which sensor fired: "XDR Agent", "PAN NGFW", "XDR Analytics"…
|
||||
source: "alert_source"
|
||||
# results_path = reply.alerts; source_path is JSONata over ONE alert object.
|
||||
# Paths absent from a given alert are skipped at ingestion, so entries for fields
|
||||
# a tenant never emits are safe. Where two entries target the same OCSF field,
|
||||
# the LAST non-empty one wins — that is how the events[] fallbacks are ordered.
|
||||
ocsf:
|
||||
# ── Finding ───────────────────────────────────────────────────────
|
||||
- { source_path: "internal_id", ocsf_field: "finding_info.uid" }
|
||||
- { source_path: "external_id", ocsf_field: "finding_info.uid_alt" }
|
||||
- { source_path: "alert_name", ocsf_field: "finding_info.title" }
|
||||
- { source_path: "alert_description", ocsf_field: "finding_info.desc" }
|
||||
- { source_path: "source_insert_ts", ocsf_field: "finding_info.created_time" }
|
||||
- { source_path: "local_insert_ts", ocsf_field: "finding_info.modified_time" }
|
||||
- { source_path: "alert_category", ocsf_field: "finding_info.analytic.category" }
|
||||
- { source_path: "alert_name", ocsf_field: "finding_info.analytic.name" }
|
||||
- { source_path: "matching_service_rule_id", ocsf_field: "finding_info.analytic.uid" }
|
||||
# ── Detection time: the MTTD anchor ───────────────────────────────
|
||||
# `time` is what Riposte measures detection-to-ingestion against. The grid
|
||||
# exposes when the tenant took the alert in (source_insert_ts); a sensor-side
|
||||
# detection timestamp, when the tenant sends one, is the better anchor and
|
||||
# comes last so it wins.
|
||||
- { source_path: "source_insert_ts", ocsf_field: "time" }
|
||||
- { source_path: "detection_timestamp", ocsf_field: "time" }
|
||||
# ── Alert state ───────────────────────────────────────────────────
|
||||
- { source_path: "severity_name", ocsf_field: "severity" }
|
||||
- { source_path: "alert_domain", ocsf_field: "activity_name" }
|
||||
- { source_path: "alert_action_status", ocsf_field: "action" }
|
||||
- { source_path: "status_progress", ocsf_field: "status" }
|
||||
- { source_path: "matching_status", ocsf_field: "status_detail" }
|
||||
- { source_path: "events_length", ocsf_field: "count" }
|
||||
# The XDR case this alert was folded into — the join back to the incident feed.
|
||||
- { source_path: "case_id", ocsf_field: "metadata.correlation_uid" }
|
||||
# ── Product identity ──────────────────────────────────────────────
|
||||
- { source_path: "'Cortex XDR'", ocsf_field: "metadata.product.name" }
|
||||
- { source_path: "'Palo Alto Networks'", ocsf_field: "metadata.product.vendor_name" }
|
||||
- { source_path: "alert_source", ocsf_field: "metadata.log_source" }
|
||||
# ── MITRE ATT&CK ──────────────────────────────────────────────────
|
||||
# Both fields arrive as a list on most tenants and as a bare string on some;
|
||||
# [0] reads the first element either way.
|
||||
- { source_path: "mitre_tactic_id_and_name[0]", ocsf_field: "attacks.tactic.name" }
|
||||
- { source_path: "mitre_technique_id_and_name[0]", ocsf_field: "attacks.technique.name" }
|
||||
# ── Affected endpoint ─────────────────────────────────────────────
|
||||
- { source_path: "agent_hostname", ocsf_field: "device.hostname" }
|
||||
- { source_path: "agent_ip_addresses[0]", ocsf_field: "device.ip" }
|
||||
- { source_path: "agent_id", ocsf_field: "device.uid" }
|
||||
- { source_path: "agent_os_type", ocsf_field: "device.os.type" }
|
||||
# Mirrored onto src_endpoint so routers and pre-processing rules written for
|
||||
# the incident feed (which maps hosts there) match alerts unchanged.
|
||||
- { source_path: "agent_hostname", ocsf_field: "src_endpoint.hostname" }
|
||||
- { source_path: "agent_ip_addresses[0]", ocsf_field: "src_endpoint.ip" }
|
||||
- { source_path: "actor_effective_username", ocsf_field: "user.name" }
|
||||
# ── What actually happened ────────────────────────────────────────
|
||||
# Grid columns first, then the same detail read off the first event, which is
|
||||
# where a tenant that does not flatten these columns puts them.
|
||||
- { source_path: "actor_process_image_name", ocsf_field: "process.name" }
|
||||
- { source_path: "actor_process_command_line", ocsf_field: "process.cmd_line" }
|
||||
- { source_path: "actor_process_image_sha256", ocsf_field: "process.file.hashes.sha256" }
|
||||
- { source_path: "causality_actor_process_command_line", ocsf_field: "process.parent_process.cmd_line" }
|
||||
- { source_path: "action_file_path", ocsf_field: "file.path" }
|
||||
- { source_path: "action_file_sha256", ocsf_field: "file.hashes.sha256" }
|
||||
- { source_path: "action_file_md5", ocsf_field: "file.hashes.md5" }
|
||||
- { source_path: "action_registry_key_name", ocsf_field: "reg_key.path" }
|
||||
- { source_path: "action_registry_data", ocsf_field: "reg_value.data" }
|
||||
- { source_path: "action_local_ip", ocsf_field: "src_endpoint.ip" }
|
||||
- { source_path: "action_local_port", ocsf_field: "src_endpoint.port" }
|
||||
- { source_path: "action_remote_ip", ocsf_field: "dst_endpoint.ip" }
|
||||
- { source_path: "action_remote_port", ocsf_field: "dst_endpoint.port" }
|
||||
- { source_path: "dst_action_external_hostname", ocsf_field: "dst_endpoint.hostname" }
|
||||
- { source_path: "events[0].actor_process_image_name", ocsf_field: "process.name" }
|
||||
- { source_path: "events[0].actor_process_command_line", ocsf_field: "process.cmd_line" }
|
||||
- { source_path: "events[0].actor_process_image_path", ocsf_field: "process.path" }
|
||||
- { source_path: "events[0].actor_process_image_sha256", ocsf_field: "process.file.hashes.sha256" }
|
||||
- { source_path: "events[0].causality_actor_process_image_name", ocsf_field: "process.parent_process.name" }
|
||||
- { source_path: "events[0].action_file_path", ocsf_field: "file.path" }
|
||||
- { source_path: "events[0].action_file_sha256", ocsf_field: "file.hashes.sha256" }
|
||||
- { source_path: "events[0].action_remote_ip", ocsf_field: "dst_endpoint.ip" }
|
||||
- { source_path: "events[0].action_remote_port", ocsf_field: "dst_endpoint.port" }
|
||||
- { source_path: "events[0].action_external_hostname", ocsf_field: "dst_endpoint.hostname" }
|
||||
@@ -2,7 +2,10 @@ name: "Cortex XDR Incidents → OCSF"
|
||||
description: "Maps a Cortex XDR incident (incidents/get_incidents/, results_path = reply.incidents) to OCSF finding fields. Incidents are aggregates; use get_incident_extra_data for per-alert detail."
|
||||
field_mappings:
|
||||
title: "incident_name"
|
||||
severity: "severity = 'critical' ? 5 : (severity = 'high' ? 4 : (severity = 'medium' ? 3 : 2))"
|
||||
# The raw string, not a ternary: the mapping engine compares numbers only, so
|
||||
# every string test read as false and every incident landed on the rule's
|
||||
# default severity. Riposte reads critical/high/medium/low onto 1-5 itself.
|
||||
severity: "severity"
|
||||
description: "description"
|
||||
# results_path = reply.incidents; source_path is JSONata over ONE incident object.
|
||||
# Paths absent from a given incident are skipped at ingestion, so extra entries are safe.
|
||||
@@ -15,6 +18,9 @@ ocsf:
|
||||
- { source_path: "modification_time", ocsf_field: "finding_info.modified_time" }
|
||||
- { source_path: "xdr_url", ocsf_field: "finding_info.src_url" }
|
||||
- { source_path: "status", ocsf_field: "status" }
|
||||
# `time` is the MTTD anchor — when XDR opened the incident, as opposed to when
|
||||
# Riposte ingested it. Without it the detection delay column stays empty.
|
||||
- { source_path: "creation_time", ocsf_field: "time" }
|
||||
- { source_path: "alert_count", ocsf_field: "count" }
|
||||
# ── MITRE ATT&CK (first aggregated tactic/technique) ──────────────
|
||||
- { source_path: "mitre_tactics_ids_and_names[0]", ocsf_field: "attacks.tactic.name" }
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
name: "Cortex XDR Incidents (full) → OCSF"
|
||||
description: "Maps one Cortex XDR incident fetched with its alerts and artifacts (incidents/get_multiple_incidents_extra_data/, results_path = reply.incidents) to OCSF finding fields. The script flattens the record, so incident fields sit at the top level — every expression written against get_incidents keeps working — while alerts[], file_artifacts[] and network_artifacts[] are plain lists beside them. Incident-level values are mapped first and the first alert's equivalents last, so the alert wins wherever the aggregate says nothing."
|
||||
field_mappings:
|
||||
# incident_name is null on most tenants (it is only set when someone renames
|
||||
# the incident), and a mapping that resolves to nothing leaves the title to
|
||||
# the incident type's fallback. description is the sentence XDR itself shows.
|
||||
title: "description"
|
||||
description: "description"
|
||||
severity: "severity"
|
||||
# incident_source, not incident_sources[0]: this mapper reads dotted paths and
|
||||
# cannot index a list, so the script lifts the first sensor out for it.
|
||||
source: "incident_source"
|
||||
# results_path = reply.incidents; source_path is JSONata over ONE flattened
|
||||
# incident. Paths absent from a given incident are skipped at ingestion, so
|
||||
# entries for fields a tenant never emits are safe. Where two entries target the
|
||||
# same OCSF field, the LAST non-empty one wins.
|
||||
ocsf:
|
||||
# ── Finding ───────────────────────────────────────────────────────
|
||||
- { source_path: "incident_id", ocsf_field: "finding_info.uid" }
|
||||
- { source_path: "incident_name ? incident_name : description", ocsf_field: "finding_info.title" }
|
||||
- { source_path: "description", ocsf_field: "finding_info.desc" }
|
||||
- { source_path: "creation_time", ocsf_field: "finding_info.created_time" }
|
||||
- { source_path: "modification_time", ocsf_field: "finding_info.modified_time" }
|
||||
- { source_path: "xdr_url", ocsf_field: "finding_info.src_url" }
|
||||
- { source_path: "alert_categories[0]", ocsf_field: "finding_info.analytic.category" }
|
||||
- { source_path: "alerts[0].name", ocsf_field: "finding_info.analytic.name" }
|
||||
# ── Detection time: the MTTD anchor ───────────────────────────────
|
||||
# Weakest first, strongest last. detection_time is often null on an XDR
|
||||
# incident, and then the first alert's own detection timestamp is the honest
|
||||
# anchor; incident creation is the last resort.
|
||||
- { source_path: "creation_time", ocsf_field: "time" }
|
||||
- { source_path: "alerts[0].detection_timestamp", ocsf_field: "time" }
|
||||
- { source_path: "detection_time", ocsf_field: "time" }
|
||||
# ── Incident state ────────────────────────────────────────────────
|
||||
- { source_path: "severity", ocsf_field: "severity" }
|
||||
- { source_path: "status", ocsf_field: "status" }
|
||||
- { source_path: "resolve_comment", ocsf_field: "status_detail" }
|
||||
- { source_path: "alert_count", ocsf_field: "count" }
|
||||
- { source_path: "aggregated_score", ocsf_field: "risk_score" }
|
||||
- { source_path: "tags", ocsf_field: "metadata.labels" }
|
||||
- { source_path: "alerts[0].action_pretty", ocsf_field: "action" }
|
||||
# ── Product identity ──────────────────────────────────────────────
|
||||
- { source_path: "'Cortex XDR'", ocsf_field: "metadata.product.name" }
|
||||
- { source_path: "'Palo Alto Networks'", ocsf_field: "metadata.product.vendor_name" }
|
||||
- { source_path: "incident_sources[0]", ocsf_field: "metadata.log_source" }
|
||||
# ── MITRE ATT&CK: the aggregate, else the first alert ─────────────
|
||||
- { source_path: "mitre_tactics_ids_and_names[0]", ocsf_field: "attacks.tactic.name" }
|
||||
- { source_path: "mitre_techniques_ids_and_names[0]", ocsf_field: "attacks.technique.name" }
|
||||
- { source_path: "alerts[0].mitre_tactic_id_and_name[0]", ocsf_field: "attacks.tactic.name" }
|
||||
- { source_path: "alerts[0].mitre_technique_id_and_name[0]", ocsf_field: "attacks.technique.name" }
|
||||
# ── Affected host / user ──────────────────────────────────────────
|
||||
# An incident's hosts are 'hostname:agent_id' strings; an alert names them plainly.
|
||||
- { source_path: "$split(hosts[0], ':')[0]", ocsf_field: "src_endpoint.hostname" }
|
||||
- { source_path: "$split(hosts[0], ':')[0]", ocsf_field: "device.hostname" }
|
||||
- { source_path: "alerts[0].host_name", ocsf_field: "src_endpoint.hostname" }
|
||||
- { source_path: "alerts[0].host_name", ocsf_field: "device.hostname" }
|
||||
- { source_path: "alerts[0].host_ip[0]", ocsf_field: "device.ip" }
|
||||
- { source_path: "alerts[0].host_ip[0]", ocsf_field: "src_endpoint.ip" }
|
||||
- { source_path: "alerts[0].endpoint_id", ocsf_field: "device.uid" }
|
||||
- { source_path: "alerts[0].agent_os_type", ocsf_field: "device.os.type" }
|
||||
- { source_path: "users[0]", ocsf_field: "user.name" }
|
||||
- { source_path: "alerts[0].user_name", ocsf_field: "user.name" }
|
||||
# ── What the first alert actually saw ─────────────────────────────
|
||||
- { source_path: "alerts[0].actor_process_image_name", ocsf_field: "process.name" }
|
||||
- { source_path: "alerts[0].actor_process_command_line", ocsf_field: "process.cmd_line" }
|
||||
- { source_path: "alerts[0].actor_process_image_path", ocsf_field: "process.path" }
|
||||
- { source_path: "alerts[0].actor_process_image_sha256", ocsf_field: "process.file.hashes.sha256" }
|
||||
- { source_path: "alerts[0].causality_actor_process_image_name", ocsf_field: "process.parent_process.name" }
|
||||
- { source_path: "alerts[0].action_file_path", ocsf_field: "file.path" }
|
||||
- { source_path: "alerts[0].action_file_name", ocsf_field: "file.name" }
|
||||
- { source_path: "alerts[0].action_file_sha256", ocsf_field: "file.hashes.sha256" }
|
||||
- { source_path: "alerts[0].action_file_md5", ocsf_field: "file.hashes.md5" }
|
||||
- { source_path: "alerts[0].action_remote_ip", ocsf_field: "dst_endpoint.ip" }
|
||||
- { source_path: "alerts[0].action_remote_port", ocsf_field: "dst_endpoint.port" }
|
||||
- { source_path: "alerts[0].action_external_hostname", ocsf_field: "dst_endpoint.hostname" }
|
||||
# ── The artifact the incident is really about ─────────────────────
|
||||
# Last, because a file artifact is the incident's verdict on the file, where
|
||||
# the alert only reports what one detection touched.
|
||||
- { source_path: "file_artifacts[0].file_name", ocsf_field: "file.name" }
|
||||
- { source_path: "file_artifacts[0].file_sha256", ocsf_field: "file.hashes.sha256" }
|
||||
- { source_path: "file_artifacts[0].file_wildfire_verdict", ocsf_field: "malware.classifications" }
|
||||
- { source_path: "network_artifacts[0].network_remote_ip", ocsf_field: "dst_endpoint.ip" }
|
||||
- { source_path: "network_artifacts[0].network_domain", ocsf_field: "dst_endpoint.hostname" }
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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():
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
@@ -44,19 +76,121 @@ def to_ms(v):
|
||||
return None
|
||||
|
||||
|
||||
# This endpoint speaks the alerts-grid dialect, NOT the incidents one: a body of
|
||||
# {filters, search_from, search_to, sort} — what incidents/get_incidents/ takes —
|
||||
# is answered with a bare HTTP 500. It wants request_data.filter_data with
|
||||
# SEARCH_FIELD/SEARCH_TYPE/SEARCH_VALUE blocks, paging.from/to and a sort LIST.
|
||||
# Shape taken from the reference client (demisto/content,
|
||||
# Packs/ApiModules/Scripts/CoreIRApiModule — get_alerts_by_filter_command).
|
||||
PAGE = 100
|
||||
|
||||
# Severity travels as an enum code both ways. Riposte reads plain names onto its
|
||||
# 1-5 scale, so alerts carry `severity_name` alongside the raw code.
|
||||
SEVERITY_CODE_TO_NAME = {
|
||||
"SEV_010_INFO": "informational",
|
||||
"SEV_020_LOW": "low",
|
||||
"SEV_030_MEDIUM": "medium",
|
||||
"SEV_040_HIGH": "high",
|
||||
"SEV_050_CRITICAL": "critical",
|
||||
}
|
||||
SEVERITY_NAME_TO_CODE = dict((v, k) for k, v in SEVERITY_CODE_TO_NAME.items())
|
||||
SEVERITY_NAME_TO_CODE["info"] = "SEV_010_INFO"
|
||||
|
||||
# Our clock and the tenant's are not the same clock. A range that ends exactly
|
||||
# now silently drops alerts the tenant stamped a few seconds ahead of us.
|
||||
SKEW_MS = 5 * 60 * 1000
|
||||
|
||||
# Window applied when the caller passes no filter at all — see main().
|
||||
DEFAULT_LOOKBACK_MS = 30 * 24 * 60 * 60 * 1000
|
||||
|
||||
|
||||
def severity_block(value):
|
||||
"""One EQ block per severity, OR'd together (the reference client's array rule)."""
|
||||
blocks = [
|
||||
{"SEARCH_FIELD": "severity", "SEARCH_TYPE": "EQ",
|
||||
"SEARCH_VALUE": SEVERITY_NAME_TO_CODE.get(s.lower(), s.upper())}
|
||||
for s in csv(value)
|
||||
]
|
||||
if not blocks:
|
||||
return None
|
||||
return blocks[0] if len(blocks) == 1 else {"OR": blocks}
|
||||
|
||||
|
||||
def flatten(item):
|
||||
"""One grid row -> one flat alert.
|
||||
|
||||
The API wraps every row as {alert_fields, incident_fields}. Mapping through
|
||||
that wrapper would put an `alert_fields.` prefix on every expression an
|
||||
operator writes, so the row is unwrapped here and the two fields Riposte
|
||||
cannot express are derived: `status.progress` carries a dot INSIDE the key
|
||||
(unusable as a mapping path) and severity is an enum code.
|
||||
"""
|
||||
fields = item.get("alert_fields")
|
||||
alert = dict(fields) if isinstance(fields, dict) else dict(item)
|
||||
alert.pop("incident_fields", None)
|
||||
if "status.progress" in alert:
|
||||
alert["status_progress"] = alert.pop("status.progress")
|
||||
name = SEVERITY_CODE_TO_NAME.get(alert.get("severity"))
|
||||
if name:
|
||||
alert["severity_name"] = name
|
||||
incident = item.get("incident_fields")
|
||||
if isinstance(incident, dict):
|
||||
alert["incident_fields"] = incident
|
||||
return alert
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
limit = int(inputs.get("limit") or 100)
|
||||
filters = []
|
||||
if inputs.get("severity"):
|
||||
filters.append({"field": "severity", "operator": "in", "value": csv(inputs["severity"])})
|
||||
limit = max(1, int(inputs.get("limit") or 100))
|
||||
conditions = []
|
||||
sev = severity_block(inputs.get("severity"))
|
||||
if sev:
|
||||
conditions.append(sev)
|
||||
created_ms = to_ms(inputs.get("created_after"))
|
||||
if created_ms is not None:
|
||||
filters.append({"field": "source_insert_ts", "operator": "gte", "value": created_ms})
|
||||
rd = {"search_from": 0, "search_to": limit, "sort": {"field": "source_insert_ts", "keyword": "desc"}}
|
||||
if filters:
|
||||
rd["filters"] = filters
|
||||
print(json.dumps(post("/alerts/get_alerts_by_filter_data/", rd)))
|
||||
conditions.append({
|
||||
"SEARCH_FIELD": "source_insert_ts",
|
||||
"SEARCH_TYPE": "RANGE",
|
||||
"SEARCH_VALUE": {"from": created_ms, "to": int(time.time() * 1000) + SKEW_MS},
|
||||
})
|
||||
if not conditions:
|
||||
# The reference client refuses a filterless query outright, and an
|
||||
# unbounded scan of the whole alerts grid is not what the API is for.
|
||||
# A recent window is a better default than an empty filter the tenant
|
||||
# may well answer with a 500.
|
||||
now_ms = int(time.time() * 1000)
|
||||
conditions.append({
|
||||
"SEARCH_FIELD": "source_insert_ts",
|
||||
"SEARCH_TYPE": "RANGE",
|
||||
"SEARCH_VALUE": {"from": now_ms - DEFAULT_LOOKBACK_MS, "to": now_ms + SKEW_MS},
|
||||
})
|
||||
# Oldest first on an incremental fetch, so that a window holding more alerts
|
||||
# than `limit` drops its most RECENT ones — the only ones the next poll can
|
||||
# still see. Newest first otherwise, which is what an operator running the
|
||||
# command by hand is asking for.
|
||||
order = "ASC" if created_ms is not None else "DESC"
|
||||
|
||||
alerts, truncated = [], False
|
||||
while len(alerts) < limit:
|
||||
rd = {"filter_data": {
|
||||
"sort": [{"FIELD": "source_insert_ts", "ORDER": order}],
|
||||
"paging": {"from": len(alerts), "to": min(len(alerts) + PAGE, limit)},
|
||||
"filter": {"AND": conditions},
|
||||
}}
|
||||
reply = (post("/alerts/get_alerts_by_filter_data/", rd) or {}).get("reply") or {}
|
||||
page = reply.get("alerts") or []
|
||||
alerts.extend(flatten(a) for a in page)
|
||||
if len(page) < PAGE:
|
||||
break
|
||||
# Stopped on the ceiling rather than on an exhausted window: whatever is
|
||||
# left is not coming back on the next poll, and a silent cap reads like
|
||||
# a quiet feed.
|
||||
truncated = len(alerts) >= limit
|
||||
|
||||
out = {"result_count": len(alerts), "alerts": alerts}
|
||||
if truncated:
|
||||
out["truncated"] = True
|
||||
print(json.dumps({"reply": out}))
|
||||
|
||||
|
||||
try:
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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():
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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():
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import json, os, sys, time, hashlib, secrets, string, urllib.request, urllib.error
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def _client():
|
||||
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", ""))
|
||||
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":
|
||||
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-<tenant>.xdr.<region>.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()
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except ValueError:
|
||||
raise ValueError(_not_json(r, raw))
|
||||
|
||||
|
||||
def to_ms(v):
|
||||
if v in (None, ""):
|
||||
return None
|
||||
s = str(v)
|
||||
if s.isdigit():
|
||||
return int(s)
|
||||
try:
|
||||
return int(datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp() * 1000)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def csv(v):
|
||||
return [x.strip() for x in str(v or "").split(",") if x.strip()]
|
||||
|
||||
|
||||
# incidents/get_incidents/ answers with a 21-field summary: no hosts, no users,
|
||||
# no MITRE, and above all not one of the alerts the incident aggregates. This
|
||||
# endpoint returns the same incident with 39 fields, its alerts in full (156
|
||||
# fields each) and its file/network artifacts — which is why the reference
|
||||
# client fetches through it and not through get_incidents (demisto/content,
|
||||
# CortexXDRIR.get_multiple_incidents_extra_data).
|
||||
PAGE = 50
|
||||
|
||||
# Artifacts are dropped by name, not by omission — the API only understands
|
||||
# being told which blocks to leave out.
|
||||
ARTIFACT_BLOCKS = ["network_artifacts", "file_artifacts"]
|
||||
|
||||
|
||||
def flatten(item):
|
||||
"""One record -> one incident.
|
||||
|
||||
Records arrive as {incident, alerts, network_artifacts, file_artifacts},
|
||||
each nested block wrapped as {total_count, data}. Flattening the incident to
|
||||
the top level keeps every expression written against get_incidents working
|
||||
unchanged — the summary's 21 fields are a subset of these 39 — while the
|
||||
alerts and artifacts land beside them as plain lists.
|
||||
"""
|
||||
incident = dict(item.get("incident") or {})
|
||||
for key in ("alerts", "network_artifacts", "file_artifacts"):
|
||||
block = item.get(key)
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
incident[key] = block.get("data") or []
|
||||
if block.get("total_count") is not None:
|
||||
# The tenant caps alerts per incident (50 by default), so the count
|
||||
# says when the list is a sample rather than the whole set.
|
||||
incident[key + "_total_count"] = block["total_count"]
|
||||
# The producing sensor is a list here, and the incident-field mapper reads
|
||||
# dotted paths only — no array indexing — so the first source is lifted out
|
||||
# for it. The list itself stays, for expressions that can index.
|
||||
sources = incident.get("incident_sources")
|
||||
if isinstance(sources, list) and sources:
|
||||
incident["incident_source"] = sources[0]
|
||||
return incident
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
limit = max(1, int(inputs.get("limit") or 50))
|
||||
filters = []
|
||||
if inputs.get("status"):
|
||||
statuses = csv(inputs["status"])
|
||||
filters.append({"field": "status", "operator": "in", "value": statuses})
|
||||
created_ms = to_ms(inputs.get("created_after"))
|
||||
if created_ms is not None:
|
||||
filters.append({"field": "creation_time", "operator": "gte", "value": created_ms})
|
||||
# An XDR incident keeps growing after it is created: alerts join it, an
|
||||
# analyst changes its status. Watermarking on creation_time fetches it once
|
||||
# and never looks again, so everything that happened afterwards is lost.
|
||||
# Watermarking on modification_time brings it back on every change, where
|
||||
# dedup on incident_id turns the second visit into an enrichment.
|
||||
modified_ms = to_ms(inputs.get("modified_after"))
|
||||
if modified_ms is not None:
|
||||
filters.append({"field": "modification_time", "operator": "gte", "value": modified_ms})
|
||||
incremental = created_ms is not None or modified_ms is not None
|
||||
# Oldest first on an incremental fetch, so that a window holding more
|
||||
# incidents than `limit` drops its most RECENT ones — the only ones the next
|
||||
# poll can still see. Newest first otherwise, for a hand-run command.
|
||||
sort_field = "modification_time" if modified_ms is not None else "creation_time"
|
||||
keyword = "asc" if incremental else "desc"
|
||||
exclude = str(inputs.get("exclude_artifacts") or "").lower() in ("1", "true", "yes")
|
||||
|
||||
incidents, total = [], None
|
||||
while len(incidents) < limit:
|
||||
rd = {
|
||||
"search_from": len(incidents),
|
||||
"search_to": min(len(incidents) + PAGE, limit),
|
||||
"sort": {"field": sort_field, "keyword": keyword},
|
||||
# Without this the nested alerts come back trimmed to a handful of
|
||||
# fields — the very thing this command exists to avoid.
|
||||
"full_alert_fields": True,
|
||||
}
|
||||
if filters:
|
||||
rd["filters"] = filters
|
||||
if exclude:
|
||||
rd["fields_to_exclude"] = ARTIFACT_BLOCKS
|
||||
reply = (post("/incidents/get_multiple_incidents_extra_data/", rd) or {}).get("reply") or {}
|
||||
page = reply.get("incidents") or []
|
||||
if total is None:
|
||||
total = reply.get("total_count")
|
||||
incidents.extend(flatten(i) for i in page)
|
||||
if len(page) < PAGE:
|
||||
break
|
||||
|
||||
out = {"result_count": len(incidents), "incidents": incidents}
|
||||
if total is not None:
|
||||
out["total_count"] = total
|
||||
# Say it when the window held more than the limit: those incidents are
|
||||
# not coming back on the next poll.
|
||||
out["truncated"] = total > len(incidents)
|
||||
print(json.dumps({"reply": out}))
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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():
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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():
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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():
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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():
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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():
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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():
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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():
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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):
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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)}))
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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():
|
||||
|
||||
@@ -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-<tenant>.xdr.<region>.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():
|
||||
|
||||
@@ -31,7 +31,7 @@ config_schema:
|
||||
commands:
|
||||
- id: query_malops
|
||||
name: cybereason-query-malops
|
||||
description: "Query Malops (malicious operations) in a time window."
|
||||
description: "Query Malops (malicious operations) in a time window. Used for ingestion: results path = malops."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
@@ -39,6 +39,10 @@ commands:
|
||||
end_time: { type: number, description: "End time as Unix epoch milliseconds" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
ingest:
|
||||
results_path: malops
|
||||
dedup_key: guid
|
||||
incremental_field: start_time
|
||||
- id: list_sensors
|
||||
name: cybereason-list-sensors
|
||||
description: "List sensors (endpoints)."
|
||||
@@ -93,3 +97,8 @@ commands:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
ingestion:
|
||||
command: query_malops
|
||||
mapper: query_malops
|
||||
default_incident_type: "Cybereason Malop"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
name: "Cybereason Malops → OCSF"
|
||||
description: "Maps the raw items of the 'malops' array returned by POST /rest/detection/inbox (results_path = malops) to OCSF finding fields. Each element is the flat, non-nested Malop-inbox shape (guid, edr, displayName, status, severity, creationTime, machines[], users[]) — not the enriched elementValues/simpleValues shape returned by /rest/crimes/unified."
|
||||
field_mappings:
|
||||
title: "displayName"
|
||||
severity: "severity = 'Critical' ? 5 : (severity = 'High' ? 4 : (severity = 'Medium' ? 3 : (severity = 'Low' ? 2 : 3)))"
|
||||
description: "\"Cybereason Malop — severity: \" & severity & \", status: \" & status"
|
||||
ocsf:
|
||||
- { source_path: "guid", ocsf_field: "finding_info.uid" }
|
||||
- { source_path: "displayName", ocsf_field: "finding_info.title" }
|
||||
- { source_path: "creationTime", ocsf_field: "finding_info.created_time" }
|
||||
- { source_path: "status", ocsf_field: "status" }
|
||||
- { source_path: "machines[0].displayName", ocsf_field: "src_endpoint.hostname" }
|
||||
- { source_path: "$count(machines)", ocsf_field: "count" }
|
||||
@@ -0,0 +1,43 @@
|
||||
id: feed_aws_ipranges
|
||||
name: AWS IP Ranges Feed
|
||||
version: 1.0.0
|
||||
description: "Amazon Web Services IP ranges feed connector — pull the official AWS public IP range list and emit normalized IOCs (CIDR + type, with region/service) for import into the Threat Indicator Manager. Intended as an allowlist / known-infrastructure feed (mark benign in TIM). Optional region/service filters. Free, no authentication required; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: fetch the AWS ip-ranges.json (IPv4 + IPv6), optional region/service filter."
|
||||
category: feed
|
||||
|
||||
# AWS publishes its public IP ranges as a free JSON file over HTTPS.
|
||||
config_schema:
|
||||
properties:
|
||||
region:
|
||||
type: string
|
||||
description: "Optional: only return prefixes for this AWS region (e.g. eu-west-1)"
|
||||
service:
|
||||
type: string
|
||||
description: "Optional: only return prefixes for this AWS service (e.g. EC2, S3, CLOUDFRONT)"
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required: []
|
||||
|
||||
commands:
|
||||
- id: fetch_indicators
|
||||
name: feed-aws-ipranges-fetch-indicators
|
||||
description: "Fetch the AWS IP ranges and return normalized indicators."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
region: { type: string, description: "Filter by AWS region (overrides config)" }
|
||||
service: { type: string, description: "Filter by AWS service (overrides config)" }
|
||||
max_indicators: { type: number, description: "Max indicators to return (0 = no limit, default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: feed-aws-ipranges-test-connection
|
||||
description: "Verify the AWS IP ranges feed is reachable (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,79 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
URL = "https://ip-ranges.amazonaws.com/ip-ranges.json"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(
|
||||
url, headers={"User-Agent": "Riposte-SOAR", "Accept": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
region = inputs.get("region") or cfg.get("region")
|
||||
service = inputs.get("service") or cfg.get("service")
|
||||
raw = _get(URL, cfg)
|
||||
data = json.loads(raw) if raw else {}
|
||||
|
||||
maxn = int(inputs.get("max_indicators") or 0)
|
||||
out = []
|
||||
|
||||
def add(prefixes, field):
|
||||
for p in prefixes:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
cidr = p.get(field)
|
||||
if not cidr:
|
||||
continue
|
||||
if region and p.get("region") != region:
|
||||
continue
|
||||
if service and p.get("service") != service:
|
||||
continue
|
||||
out.append({
|
||||
"value": cidr,
|
||||
"type": "cidr",
|
||||
"region": p.get("region"),
|
||||
"service": p.get("service"),
|
||||
"provider": "aws",
|
||||
})
|
||||
if maxn and len(out) >= maxn:
|
||||
return True
|
||||
return False
|
||||
|
||||
if not add(data.get("prefixes", []), "ip_prefix"):
|
||||
add(data.get("ipv6_prefixes", []), "ipv6_prefix")
|
||||
|
||||
return {"source": "aws:ip-ranges", "count": len(out), "indicators": out}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,49 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
URL = "https://ip-ranges.amazonaws.com/ip-ranges.json"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(
|
||||
url, headers={"User-Agent": "Riposte-SOAR", "Accept": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
raw = _get(URL, cfg)
|
||||
data = json.loads(raw) if raw else {}
|
||||
n = len(data.get("prefixes", [])) if isinstance(data, dict) else 0
|
||||
return {"ok": True, "sample_count": n, "create_date": data.get("createDate")}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,40 @@
|
||||
id: feed_blocklist_de
|
||||
name: Blocklist.de Feed
|
||||
version: 1.0.0
|
||||
description: "Blocklist.de feed connector — pull the community fail2ban attacker lists (all attackers, or per-service subsets: ssh, mail, apache, ftp, sip, bots, strongips, ...) and emit normalized IOCs (IP + type) for import into the Threat Indicator Manager. Free, no authentication required; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: fetch a blocklist.de attacker list."
|
||||
category: feed
|
||||
|
||||
# Blocklist.de publishes free plain-text IP lists over HTTPS. No key needed.
|
||||
config_schema:
|
||||
properties:
|
||||
list:
|
||||
type: string
|
||||
description: "Which list: all, ssh, mail, apache, imap, ftp, sip, bots, strongips, ircbot, bruteforcelogin. Default all"
|
||||
default: "all"
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required: []
|
||||
|
||||
commands:
|
||||
- id: fetch_indicators
|
||||
name: feed-blocklist-de-fetch-indicators
|
||||
description: "Fetch a blocklist.de attacker list and return normalized indicators."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
list: { type: string, description: "all | ssh | mail | apache | imap | ftp | sip | bots | strongips | ircbot | bruteforcelogin (overrides config)" }
|
||||
max_indicators: { type: number, description: "Max indicators to return (0 = no limit, default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: feed-blocklist-de-test-connection
|
||||
description: "Verify the blocklist.de feed is reachable (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,63 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://lists.blocklist.de/lists/"
|
||||
ALLOWED = {
|
||||
"all", "ssh", "mail", "apache", "imap", "ftp", "sip",
|
||||
"bots", "strongips", "ircbot", "bruteforcelogin",
|
||||
}
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
key = str(inputs.get("list") or cfg.get("list") or "all").lower()
|
||||
if key not in ALLOWED:
|
||||
key = "all"
|
||||
raw = _get(BASE + key + ".txt", cfg).decode("utf-8", "replace")
|
||||
|
||||
maxn = int(inputs.get("max_indicators") or 0)
|
||||
out = []
|
||||
for line in raw.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
out.append({"value": line, "type": "ip", "list": key})
|
||||
if maxn and len(out) >= maxn:
|
||||
break
|
||||
|
||||
return {"source": "blocklist.de:" + key, "count": len(out), "indicators": out}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,47 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://lists.blocklist.de/lists/"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
key = str(cfg.get("list") or "all").lower()
|
||||
raw = _get(BASE + key + ".txt", cfg).decode("utf-8", "replace")
|
||||
n = sum(1 for ln in raw.splitlines() if ln.strip() and not ln.startswith("#"))
|
||||
return {"ok": True, "sample_count": n, "list": key}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,40 @@
|
||||
id: feed_botvrij
|
||||
name: Botvrij.eu Feed
|
||||
version: 1.0.0
|
||||
description: "Botvrij.eu OSINT feed connector — pull the free community IOC lists (destination IPs, domains, hostnames, URLs, file hashes, filenames, e-mails) and emit normalized IOCs (value + type) for import into the Threat Indicator Manager. Free, no authentication required; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: fetch a botvrij.eu IOC list by type."
|
||||
category: feed
|
||||
|
||||
# Botvrij.eu publishes free plain-text IOC lists over HTTPS. No key required.
|
||||
config_schema:
|
||||
properties:
|
||||
list:
|
||||
type: string
|
||||
description: "Which list: ip-dst, domain, hostname, url, md5, sha1, sha256, filename, email-src. Default ip-dst"
|
||||
default: "ip-dst"
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required: []
|
||||
|
||||
commands:
|
||||
- id: fetch_indicators
|
||||
name: feed-botvrij-fetch-indicators
|
||||
description: "Fetch a botvrij.eu IOC list and return normalized indicators."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
list: { type: string, description: "ip-dst | domain | hostname | url | md5 | sha1 | sha256 | filename | email-src (overrides config)" }
|
||||
max_indicators: { type: number, description: "Max indicators to return (0 = no limit, default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: feed-botvrij-test-connection
|
||||
description: "Verify the botvrij.eu feed is reachable (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,74 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://www.botvrij.eu/data/ioclist."
|
||||
|
||||
# botvrij list name -> normalized indicator type
|
||||
TYPES = {
|
||||
"ip-dst": "ip",
|
||||
"domain": "domain",
|
||||
"hostname": "domain",
|
||||
"url": "url",
|
||||
"md5": "hash",
|
||||
"sha1": "hash",
|
||||
"sha256": "hash",
|
||||
"filename": "filename",
|
||||
"email-src": "email",
|
||||
}
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
key = str(inputs.get("list") or cfg.get("list") or "ip-dst").lower()
|
||||
if key not in TYPES:
|
||||
key = "ip-dst"
|
||||
typ = TYPES[key]
|
||||
|
||||
raw = _get(BASE + key, cfg).decode("utf-8", "replace")
|
||||
|
||||
maxn = int(inputs.get("max_indicators") or 0)
|
||||
out = []
|
||||
for line in raw.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
out.append({"value": line, "type": typ, "list": key})
|
||||
if maxn and len(out) >= maxn:
|
||||
break
|
||||
|
||||
return {"source": "botvrij:" + key, "count": len(out), "indicators": out}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,47 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://www.botvrij.eu/data/ioclist."
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
key = str(cfg.get("list") or "ip-dst").lower()
|
||||
raw = _get(BASE + key, cfg).decode("utf-8", "replace")
|
||||
n = sum(1 for ln in raw.splitlines() if ln.strip() and not ln.startswith("#"))
|
||||
return {"ok": True, "sample_count": n, "list": key}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,38 @@
|
||||
id: feed_cins_army
|
||||
name: CINS Army Feed
|
||||
version: 1.0.0
|
||||
description: "CINS Score (CI Army) feed connector — pull the free 'CI Army' bad-guys list (poorly-rated IPs from the CINS threat-intel system that have not yet been seen attacking your own network) and emit normalized IOCs (IP + type) for import into the Threat Indicator Manager. Free, no authentication required; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: fetch the CI Army bad-guys list."
|
||||
category: feed
|
||||
|
||||
# The CI Army list is a free CINS Score feed. No key is required.
|
||||
config_schema:
|
||||
properties:
|
||||
feed_url:
|
||||
type: string
|
||||
description: "Override the list URL (default https://cinsscore.com/list/ci-badguys.txt)"
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required: []
|
||||
|
||||
commands:
|
||||
- id: fetch_indicators
|
||||
name: feed-cins-army-fetch-indicators
|
||||
description: "Fetch the CI Army bad-guys list and return normalized indicators."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
max_indicators: { type: number, description: "Max indicators to return (0 = no limit, default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: feed-cins-army-test-connection
|
||||
description: "Verify the CI Army feed is reachable (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,57 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
DEFAULT_URL = "https://cinsscore.com/list/ci-badguys.txt"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
url = cfg.get("feed_url") or DEFAULT_URL
|
||||
raw = _get(url, cfg).decode("utf-8", "replace")
|
||||
|
||||
maxn = int(inputs.get("max_indicators") or 0)
|
||||
out = []
|
||||
for line in raw.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
out.append({"value": line, "type": "ip", "tags": ["ci-army"]})
|
||||
if maxn and len(out) >= maxn:
|
||||
break
|
||||
|
||||
return {"source": "ci-army", "count": len(out), "indicators": out}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,47 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
DEFAULT_URL = "https://cinsscore.com/list/ci-badguys.txt"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
url = cfg.get("feed_url") or DEFAULT_URL
|
||||
raw = _get(url, cfg).decode("utf-8", "replace")
|
||||
n = sum(1 for ln in raw.splitlines() if ln.strip() and not ln.startswith("#"))
|
||||
return {"ok": True, "sample_count": n}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,36 @@
|
||||
id: feed_cisa_kev
|
||||
name: CISA KEV Feed
|
||||
version: 1.0.0
|
||||
description: "CISA Known Exploited Vulnerabilities (KEV) feed connector — pull the authoritative catalog of CVEs known to be actively exploited in the wild and emit normalized IOCs (CVE + type, with vendor/product/due-date/ransomware flag) for import into the Threat Indicator Manager. Free, no authentication required; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: fetch the CISA KEV catalog."
|
||||
category: feed
|
||||
|
||||
# The CISA KEV catalog is a free JSON file served over HTTPS. No key required.
|
||||
config_schema:
|
||||
properties:
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required: []
|
||||
|
||||
commands:
|
||||
- id: fetch_indicators
|
||||
name: feed-cisa-kev-fetch-indicators
|
||||
description: "Fetch the CISA KEV catalog and return normalized CVE indicators."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
ransomware_only: { type: boolean, description: "Only return CVEs linked to known ransomware campaigns (default false)" }
|
||||
max_indicators: { type: number, description: "Max indicators to return (0 = no limit, default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: feed-cisa-kev-test-connection
|
||||
description: "Verify the CISA KEV feed is reachable (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,80 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(
|
||||
url, headers={"User-Agent": "Riposte-SOAR", "Accept": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
ransomware_only = bool(inputs.get("ransomware_only"))
|
||||
raw = _get(URL, cfg)
|
||||
data = json.loads(raw) if raw else {}
|
||||
|
||||
maxn = int(inputs.get("max_indicators") or 0)
|
||||
out = []
|
||||
for v in data.get("vulnerabilities", []):
|
||||
if not isinstance(v, dict):
|
||||
continue
|
||||
cid = v.get("cveID")
|
||||
if not cid:
|
||||
continue
|
||||
ransom = v.get("knownRansomwareCampaignUse")
|
||||
if ransomware_only and str(ransom).lower() != "known":
|
||||
continue
|
||||
out.append({
|
||||
"value": cid,
|
||||
"type": "cve",
|
||||
"vendor": v.get("vendorProject"),
|
||||
"product": v.get("product"),
|
||||
"name": v.get("vulnerabilityName"),
|
||||
"date_added": v.get("dateAdded"),
|
||||
"due_date": v.get("dueDate"),
|
||||
"ransomware": ransom,
|
||||
"tags": ["cisa-kev", "exploited"],
|
||||
})
|
||||
if maxn and len(out) >= maxn:
|
||||
break
|
||||
|
||||
return {
|
||||
"source": "cisa:kev",
|
||||
"count": len(out),
|
||||
"indicators": out,
|
||||
"catalog_version": data.get("catalogVersion"),
|
||||
}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,49 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(
|
||||
url, headers={"User-Agent": "Riposte-SOAR", "Accept": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
raw = _get(URL, cfg)
|
||||
data = json.loads(raw) if raw else {}
|
||||
n = data.get("count") or len(data.get("vulnerabilities", []))
|
||||
return {"ok": True, "sample_count": n, "catalog_version": data.get("catalogVersion")}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,40 @@
|
||||
id: feed_cloudflare_ips
|
||||
name: Cloudflare IPs Feed
|
||||
version: 1.0.0
|
||||
description: "Cloudflare IP ranges feed connector — pull the official Cloudflare edge IP ranges (IPv4 and/or IPv6) and emit normalized IOCs (CIDR + type) for import into the Threat Indicator Manager. Intended as an allowlist / known-infrastructure feed (mark benign in TIM). Free, no authentication required; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: fetch Cloudflare ips-v4 / ips-v6."
|
||||
category: feed
|
||||
|
||||
# Cloudflare publishes its edge IP ranges as free plain-text lists over HTTPS.
|
||||
config_schema:
|
||||
properties:
|
||||
family:
|
||||
type: string
|
||||
description: "Which addresses: v4, v6 or both (default both)"
|
||||
default: "both"
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required: []
|
||||
|
||||
commands:
|
||||
- id: fetch_indicators
|
||||
name: feed-cloudflare-ips-fetch-indicators
|
||||
description: "Fetch the Cloudflare IP ranges and return normalized indicators."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
family: { type: string, description: "v4 | v6 | both (overrides the config default)" }
|
||||
max_indicators: { type: number, description: "Max indicators to return (0 = no limit, default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: feed-cloudflare-ips-test-connection
|
||||
description: "Verify the Cloudflare IPs feed is reachable (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,62 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
URLS = {
|
||||
"v4": "https://www.cloudflare.com/ips-v4",
|
||||
"v6": "https://www.cloudflare.com/ips-v6",
|
||||
}
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
fam = str(inputs.get("family") or cfg.get("family") or "both").lower()
|
||||
families = ["v4", "v6"] if fam == "both" else [fam if fam in URLS else "v4"]
|
||||
|
||||
maxn = int(inputs.get("max_indicators") or 0)
|
||||
out = []
|
||||
for f in families:
|
||||
raw = _get(URLS[f], cfg).decode("utf-8", "replace")
|
||||
for line in raw.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
out.append({"value": line, "type": "cidr", "provider": "cloudflare"})
|
||||
if maxn and len(out) >= maxn:
|
||||
return {"source": "cloudflare:ips", "count": len(out), "indicators": out}
|
||||
|
||||
return {"source": "cloudflare:ips", "count": len(out), "indicators": out}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,49 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
URLS = {
|
||||
"v4": "https://www.cloudflare.com/ips-v4",
|
||||
"v6": "https://www.cloudflare.com/ips-v6",
|
||||
}
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
raw = _get(URLS["v4"], cfg).decode("utf-8", "replace")
|
||||
n = sum(1 for ln in raw.splitlines() if ln.strip() and not ln.startswith("#"))
|
||||
return {"ok": True, "sample_count": n}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,38 @@
|
||||
id: feed_dshield
|
||||
name: DShield Block List Feed
|
||||
version: 1.0.0
|
||||
description: "SANS Internet Storm Center DShield feed connector — pull the recommended block list (networks responsible for the most attacks seen by the DShield sensor network) and emit normalized IOCs (CIDR + type, with country) for import into the Threat Indicator Manager. Free, no authentication required; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: fetch the DShield recommended block list."
|
||||
category: feed
|
||||
|
||||
# The DShield block list is a free SANS ISC feed served over HTTPS. No key needed.
|
||||
config_schema:
|
||||
properties:
|
||||
feed_url:
|
||||
type: string
|
||||
description: "Override the block-list URL (default https://feeds.dshield.org/block.txt)"
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required: []
|
||||
|
||||
commands:
|
||||
- id: fetch_indicators
|
||||
name: feed-dshield-fetch-indicators
|
||||
description: "Fetch the DShield block list and return normalized indicators."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
max_indicators: { type: number, description: "Max indicators to return (0 = no limit, default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: feed-dshield-test-connection
|
||||
description: "Verify the DShield feed is reachable (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,70 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
DEFAULT_URL = "https://feeds.dshield.org/block.txt"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
url = cfg.get("feed_url") or DEFAULT_URL
|
||||
raw = _get(url, cfg).decode("utf-8", "replace")
|
||||
|
||||
maxn = int(inputs.get("max_indicators") or 0)
|
||||
out = []
|
||||
for line in raw.splitlines():
|
||||
line = line.rstrip()
|
||||
if not line or line.lstrip().startswith("#"):
|
||||
continue
|
||||
# tab-separated: Start End Netmask Attacks Name Country Email
|
||||
cols = line.split("\t")
|
||||
if len(cols) < 3:
|
||||
cols = line.split()
|
||||
if len(cols) < 3:
|
||||
continue
|
||||
start = cols[0].strip()
|
||||
netmask = cols[2].strip()
|
||||
if start.count(".") != 3 or not netmask.isdigit():
|
||||
continue
|
||||
item = {"value": start + "/" + netmask, "type": "cidr", "start": start, "netmask": netmask}
|
||||
if len(cols) > 5 and cols[5].strip():
|
||||
item["country"] = cols[5].strip()
|
||||
out.append(item)
|
||||
if maxn and len(out) >= maxn:
|
||||
break
|
||||
|
||||
return {"source": "dshield:block", "count": len(out), "indicators": out}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,47 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
DEFAULT_URL = "https://feeds.dshield.org/block.txt"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
url = cfg.get("feed_url") or DEFAULT_URL
|
||||
raw = _get(url, cfg).decode("utf-8", "replace")
|
||||
n = sum(1 for ln in raw.splitlines() if ln and not ln.lstrip().startswith("#"))
|
||||
return {"ok": True, "sample_lines": n}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,40 @@
|
||||
id: feed_emergingthreats
|
||||
name: Emerging Threats Feed
|
||||
version: 1.0.0
|
||||
description: "Emerging Threats (Proofpoint ET Open) feed connector — pull the free reputation blocklists: known compromised hosts, or the ET firewall block-IP ruleset, and emit normalized IOCs (IP/CIDR + type) for import into the Threat Indicator Manager. Free, no authentication required; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: fetch the ET compromised-IPs or firewall block-IPs list."
|
||||
category: feed
|
||||
|
||||
# ET Open lists are free and served over HTTPS. No key required.
|
||||
config_schema:
|
||||
properties:
|
||||
list:
|
||||
type: string
|
||||
description: "Which list: compromised (compromised-ips) or block (firewall block-IPs). Default compromised"
|
||||
default: "compromised"
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required: []
|
||||
|
||||
commands:
|
||||
- id: fetch_indicators
|
||||
name: feed-emergingthreats-fetch-indicators
|
||||
description: "Fetch an Emerging Threats reputation list and return normalized indicators."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
list: { type: string, description: "compromised | block (overrides the config default)" }
|
||||
max_indicators: { type: number, description: "Max indicators to return (0 = no limit, default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: feed-emergingthreats-test-connection
|
||||
description: "Verify the Emerging Threats feed is reachable (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,62 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
LISTS = {
|
||||
"compromised": "https://rules.emergingthreats.net/blockrules/compromised-ips.txt",
|
||||
"block": "https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt",
|
||||
}
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
key = str(inputs.get("list") or cfg.get("list") or "compromised").lower()
|
||||
url = LISTS.get(key, LISTS["compromised"])
|
||||
raw = _get(url, cfg).decode("utf-8", "replace")
|
||||
|
||||
maxn = int(inputs.get("max_indicators") or 0)
|
||||
out = []
|
||||
for line in raw.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
typ = "cidr" if "/" in line else "ip"
|
||||
out.append({"value": line, "type": typ, "list": key})
|
||||
if maxn and len(out) >= maxn:
|
||||
break
|
||||
|
||||
return {"source": "emergingthreats:" + key, "count": len(out), "indicators": out}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,51 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
LISTS = {
|
||||
"compromised": "https://rules.emergingthreats.net/blockrules/compromised-ips.txt",
|
||||
"block": "https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt",
|
||||
}
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
key = str(cfg.get("list") or "compromised").lower()
|
||||
url = LISTS.get(key, LISTS["compromised"])
|
||||
raw = _get(url, cfg).decode("utf-8", "replace")
|
||||
n = sum(1 for ln in raw.splitlines() if ln.strip() and not ln.startswith("#"))
|
||||
return {"ok": True, "sample_count": n, "list": key}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,39 @@
|
||||
id: feed_first_epss
|
||||
name: FIRST EPSS Feed
|
||||
version: 1.0.0
|
||||
description: "FIRST EPSS (Exploit Prediction Scoring System) feed connector — pull the CVEs with the highest probability of being exploited in the next 30 days and emit normalized IOCs (CVE + type, with EPSS score and percentile) for import into the Threat Indicator Manager. Free public API, no authentication required; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: fetch top-EPSS CVEs from the FIRST EPSS API."
|
||||
category: feed
|
||||
|
||||
# The FIRST EPSS API is free and served over HTTPS. No key required.
|
||||
config_schema:
|
||||
properties:
|
||||
min_epss:
|
||||
type: string
|
||||
description: "Optional: only return CVEs with EPSS score greater than this (0-1, e.g. 0.5)"
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required: []
|
||||
|
||||
commands:
|
||||
- id: fetch_indicators
|
||||
name: feed-first-epss-fetch-indicators
|
||||
description: "Fetch top-EPSS CVEs and return normalized indicators."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
min_epss: { type: string, description: "Only CVEs with EPSS > this value (overrides config)" }
|
||||
max_indicators: { type: number, description: "Max indicators to return (default 100, max 1000)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: feed-first-epss-test-connection
|
||||
description: "Verify the FIRST EPSS API is reachable (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,73 @@
|
||||
import json, os, sys, ssl, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.first.org/data/v1/epss"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(
|
||||
url, headers={"User-Agent": "Riposte-SOAR", "Accept": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
limit = int(inputs.get("max_indicators") or 0) or 100
|
||||
limit = max(1, min(limit, 1000))
|
||||
min_epss = inputs.get("min_epss") or cfg.get("min_epss")
|
||||
|
||||
params = {"order": "!epss", "limit": limit}
|
||||
if min_epss not in (None, ""):
|
||||
params["epss-gt"] = str(min_epss)
|
||||
|
||||
url = BASE + "?" + urllib.parse.urlencode(params)
|
||||
raw = _get(url, cfg)
|
||||
data = json.loads(raw) if raw else {}
|
||||
|
||||
out = []
|
||||
for e in data.get("data", []):
|
||||
if not isinstance(e, dict):
|
||||
continue
|
||||
cve = e.get("cve")
|
||||
if not cve:
|
||||
continue
|
||||
out.append({
|
||||
"value": cve,
|
||||
"type": "cve",
|
||||
"epss": e.get("epss"),
|
||||
"percentile": e.get("percentile"),
|
||||
"date": e.get("date"),
|
||||
})
|
||||
|
||||
return {"source": "first:epss", "count": len(out), "indicators": out}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,49 @@
|
||||
import json, os, sys, ssl, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.first.org/data/v1/epss"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(
|
||||
url, headers={"User-Agent": "Riposte-SOAR", "Accept": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
url = BASE + "?" + urllib.parse.urlencode({"limit": 1})
|
||||
raw = _get(url, cfg)
|
||||
data = json.loads(raw) if raw else {}
|
||||
return {"ok": True, "status": data.get("status"), "total": data.get("total")}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,39 @@
|
||||
id: feed_gcp_ipranges
|
||||
name: GCP IP Ranges Feed
|
||||
version: 1.0.0
|
||||
description: "Google Cloud Platform IP ranges feed connector — pull the official GCP public IP range list and emit normalized IOCs (CIDR + type, with service/scope) for import into the Threat Indicator Manager. Intended as an allowlist / known-infrastructure feed (mark benign in TIM). Free, no authentication required; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: fetch the GCP cloud.json (IPv4 + IPv6)."
|
||||
category: feed
|
||||
|
||||
# GCP publishes its public IP ranges as a free JSON file over HTTPS.
|
||||
config_schema:
|
||||
properties:
|
||||
service:
|
||||
type: string
|
||||
description: "Optional: only return prefixes for this GCP service (e.g. Google Cloud, Google APIs)"
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required: []
|
||||
|
||||
commands:
|
||||
- id: fetch_indicators
|
||||
name: feed-gcp-ipranges-fetch-indicators
|
||||
description: "Fetch the GCP IP ranges and return normalized indicators."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
service: { type: string, description: "Filter by GCP service (overrides config)" }
|
||||
max_indicators: { type: number, description: "Max indicators to return (0 = no limit, default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: feed-gcp-ipranges-test-connection
|
||||
description: "Verify the GCP IP ranges feed is reachable (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,70 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
URL = "https://www.gstatic.com/ipranges/cloud.json"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(
|
||||
url, headers={"User-Agent": "Riposte-SOAR", "Accept": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
service = inputs.get("service") or cfg.get("service")
|
||||
raw = _get(URL, cfg)
|
||||
data = json.loads(raw) if raw else {}
|
||||
|
||||
maxn = int(inputs.get("max_indicators") or 0)
|
||||
out = []
|
||||
for p in data.get("prefixes", []):
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
cidr = p.get("ipv4Prefix") or p.get("ipv6Prefix")
|
||||
if not cidr:
|
||||
continue
|
||||
if service and p.get("service") != service:
|
||||
continue
|
||||
out.append({
|
||||
"value": cidr,
|
||||
"type": "cidr",
|
||||
"service": p.get("service"),
|
||||
"scope": p.get("scope"),
|
||||
"provider": "gcp",
|
||||
})
|
||||
if maxn and len(out) >= maxn:
|
||||
break
|
||||
|
||||
return {"source": "gcp:cloud", "count": len(out), "indicators": out}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,49 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
URL = "https://www.gstatic.com/ipranges/cloud.json"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(
|
||||
url, headers={"User-Agent": "Riposte-SOAR", "Accept": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
raw = _get(URL, cfg)
|
||||
data = json.loads(raw) if raw else {}
|
||||
n = len(data.get("prefixes", [])) if isinstance(data, dict) else 0
|
||||
return {"ok": True, "sample_count": n, "creation_time": data.get("creationTime")}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,59 @@
|
||||
id: feed_json
|
||||
name: JSON Feed
|
||||
version: 1.0.0
|
||||
description: "Generic JSON threat-intel feed connector — fetch any JSON feed from a URL, dig into a configurable array path, pull a value field (and optional type field) and emit normalized IOCs (value + type) for import into the Threat Indicator Manager. One connector, many feeds. Optional bearer token; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: fetch indicators from a JSON feed with configurable field mapping."
|
||||
category: feed
|
||||
|
||||
# The feed is fetched over HTTP(S) from feed_url. array_path digs into nested
|
||||
# objects (dot notation) to reach the list of indicators; value_field/type_field
|
||||
# select the fields on each element.
|
||||
config_schema:
|
||||
properties:
|
||||
feed_url:
|
||||
type: string
|
||||
description: "URL of the JSON feed"
|
||||
api_token:
|
||||
type: string
|
||||
description: "Optional bearer token (if the feed requires auth)"
|
||||
x-soar-sensitive: true
|
||||
array_path:
|
||||
type: string
|
||||
description: "Dot path to the array of indicators (e.g. 'data.indicators'). Empty = the top-level value"
|
||||
value_field:
|
||||
type: string
|
||||
description: "Field holding the IOC value on each element (dot path allowed, default 'value')"
|
||||
default: "value"
|
||||
type_field:
|
||||
type: string
|
||||
description: "Optional field holding the IOC type on each element"
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required:
|
||||
- feed_url
|
||||
|
||||
commands:
|
||||
- id: fetch_indicators
|
||||
name: feed-json-fetch-indicators
|
||||
description: "Fetch and parse the JSON feed, returning normalized indicators."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
array_path: { type: string, description: "Dot path to the array (overrides config)" }
|
||||
value_field: { type: string, description: "Value field / dot path (overrides config)" }
|
||||
type_field: { type: string, description: "Type field (overrides config)" }
|
||||
ioc_type: { type: string, description: "auto | ip | domain | url | hash | email — fallback type when no type_field (default auto)" }
|
||||
max_indicators: { type: number, description: "Max indicators to return (0 = no limit, default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: feed-json-test-connection
|
||||
description: "Verify the feed URL returns valid JSON (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,120 @@
|
||||
import json, os, sys, re, ssl, urllib.request, urllib.error
|
||||
|
||||
_IP = re.compile(r"^(?:\d{1,3}\.){3}\d{1,3}$")
|
||||
_HASH = re.compile(r"^[a-fA-F0-9]{32}$|^[a-fA-F0-9]{40}$|^[a-fA-F0-9]{64}$")
|
||||
_EMAIL = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||
_URL = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.\-]*://")
|
||||
_DOMAIN = re.compile(r"^(?:[a-zA-Z0-9_-]+\.)+[a-zA-Z]{2,}$")
|
||||
|
||||
|
||||
def detect_type(v):
|
||||
v = v.strip()
|
||||
if _IP.match(v):
|
||||
return "ip"
|
||||
if _URL.match(v):
|
||||
return "url"
|
||||
if _EMAIL.match(v):
|
||||
return "email"
|
||||
if _HASH.match(v):
|
||||
return "hash"
|
||||
if _DOMAIN.match(v):
|
||||
return "domain"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _headers(cfg):
|
||||
h = {"User-Agent": "Riposte-SOAR", "Accept": "application/json"}
|
||||
t = cfg.get("api_token")
|
||||
if t:
|
||||
h["Authorization"] = "Bearer " + str(t)
|
||||
return h
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers=_headers(cfg))
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _dig(obj, path):
|
||||
if not path:
|
||||
return obj
|
||||
cur = obj
|
||||
for part in path.split("."):
|
||||
if isinstance(cur, dict):
|
||||
cur = cur.get(part)
|
||||
else:
|
||||
return None
|
||||
return cur
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
url = cfg.get("feed_url")
|
||||
if not url:
|
||||
raise Exception("feed_url is required")
|
||||
|
||||
array_path = inputs.get("array_path") or cfg.get("array_path") or ""
|
||||
value_field = inputs.get("value_field") or cfg.get("value_field") or "value"
|
||||
type_field = inputs.get("type_field") or cfg.get("type_field")
|
||||
ioc_type = str(inputs.get("ioc_type") or "auto").lower()
|
||||
|
||||
raw = _get(url, cfg)
|
||||
data = json.loads(raw) if raw else {}
|
||||
|
||||
arr = _dig(data, array_path) if array_path else data
|
||||
if isinstance(arr, dict):
|
||||
arr = list(arr.values())
|
||||
if not isinstance(arr, list):
|
||||
arr = []
|
||||
|
||||
maxn = int(inputs.get("max_indicators") or 0)
|
||||
out = []
|
||||
for e in arr:
|
||||
if isinstance(e, dict):
|
||||
val = _dig(e, value_field) if "." in value_field else e.get(value_field)
|
||||
t = e.get(type_field) if type_field else None
|
||||
else:
|
||||
val = e
|
||||
t = None
|
||||
if val in (None, ""):
|
||||
continue
|
||||
val = str(val).strip()
|
||||
if not val:
|
||||
continue
|
||||
typ = str(t).lower() if t else (ioc_type if ioc_type != "auto" else detect_type(val))
|
||||
out.append({"value": val, "type": typ})
|
||||
if maxn and len(out) >= maxn:
|
||||
break
|
||||
|
||||
return {"source": url, "count": len(out), "indicators": out}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,56 @@
|
||||
import json, os, sys, ssl, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _headers(cfg):
|
||||
h = {"User-Agent": "Riposte-SOAR", "Accept": "application/json"}
|
||||
t = cfg.get("api_token")
|
||||
if t:
|
||||
h["Authorization"] = "Bearer " + str(t)
|
||||
return h
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers=_headers(cfg))
|
||||
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
url = cfg.get("feed_url")
|
||||
if not url:
|
||||
raise Exception("feed_url is required")
|
||||
raw = _get(url, cfg)
|
||||
data = json.loads(raw) if raw else {}
|
||||
kind = "array" if isinstance(data, list) else type(data).__name__
|
||||
return {"ok": True, "top_level": kind}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,45 @@
|
||||
id: feed_nvd
|
||||
name: NVD CVE Feed
|
||||
version: 1.0.0
|
||||
description: "NIST National Vulnerability Database (NVD) feed connector — pull recently-published CVEs from the NVD 2.0 API and emit normalized IOCs (CVE + type, with description/CVSS/status) for import into the Threat Indicator Manager. Works without a key; an optional free NVD API key raises the rate limit. Stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: fetch recent CVEs from the NVD 2.0 API."
|
||||
category: feed
|
||||
|
||||
# The NVD 2.0 API is free. An optional API key (sent as the apiKey header)
|
||||
# raises the request rate limit.
|
||||
config_schema:
|
||||
properties:
|
||||
api_key:
|
||||
type: string
|
||||
description: "Optional NVD API key (free — raises the rate limit)"
|
||||
x-soar-sensitive: true
|
||||
days:
|
||||
type: number
|
||||
description: "Look-back window in days for recently-published CVEs (default 7, max 120)"
|
||||
default: 7
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required: []
|
||||
|
||||
commands:
|
||||
- id: fetch_indicators
|
||||
name: feed-nvd-fetch-indicators
|
||||
description: "Fetch recently-published CVEs from NVD and return normalized indicators."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
days: { type: number, description: "Look-back window in days (overrides config, max 120)" }
|
||||
max_indicators: { type: number, description: "Max indicators to return (0 = API page size 2000, default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: feed-nvd-test-connection
|
||||
description: "Verify the NVD API is reachable (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,110 @@
|
||||
import json, os, sys, ssl, datetime, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://services.nvd.nist.gov/rest/json/cves/2.0"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _headers(cfg):
|
||||
h = {"User-Agent": "Riposte-SOAR", "Accept": "application/json"}
|
||||
k = cfg.get("api_key")
|
||||
if k:
|
||||
h["apiKey"] = str(k)
|
||||
return h
|
||||
|
||||
|
||||
def _get(url, cfg):
|
||||
req = urllib.request.Request(url, headers=_headers(cfg))
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _cvss(metrics):
|
||||
for key in ("cvssMetricV31", "cvssMetricV30", "cvssMetricV2"):
|
||||
arr = metrics.get(key)
|
||||
if arr and isinstance(arr, list) and isinstance(arr[0], dict):
|
||||
d = arr[0].get("cvssData", {})
|
||||
return d.get("baseScore"), d.get("baseSeverity") or arr[0].get("baseSeverity")
|
||||
return None, None
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
days = int(inputs.get("days") or cfg.get("days") or 7)
|
||||
days = max(1, min(days, 120))
|
||||
|
||||
end = datetime.datetime.now(datetime.timezone.utc)
|
||||
start = end - datetime.timedelta(days=days)
|
||||
fmt = "%Y-%m-%dT%H:%M:%S.000"
|
||||
|
||||
per = int(inputs.get("max_indicators") or 0) or 2000
|
||||
per = max(1, min(per, 2000))
|
||||
|
||||
params = {
|
||||
"pubStartDate": start.strftime(fmt),
|
||||
"pubEndDate": end.strftime(fmt),
|
||||
"resultsPerPage": per,
|
||||
}
|
||||
url = BASE + "?" + urllib.parse.urlencode(params)
|
||||
raw = _get(url, cfg)
|
||||
data = json.loads(raw) if raw else {}
|
||||
|
||||
maxn = int(inputs.get("max_indicators") or 0)
|
||||
out = []
|
||||
for v in data.get("vulnerabilities", []):
|
||||
cve = v.get("cve", {}) if isinstance(v, dict) else {}
|
||||
cid = cve.get("id")
|
||||
if not cid:
|
||||
continue
|
||||
desc = ""
|
||||
for d in cve.get("descriptions", []):
|
||||
if d.get("lang") == "en":
|
||||
desc = d.get("value")
|
||||
break
|
||||
score, severity = _cvss(cve.get("metrics", {}))
|
||||
out.append({
|
||||
"value": cid,
|
||||
"type": "cve",
|
||||
"description": desc,
|
||||
"cvss": score,
|
||||
"severity": severity,
|
||||
"published": cve.get("published"),
|
||||
"status": cve.get("vulnStatus"),
|
||||
})
|
||||
if maxn and len(out) >= maxn:
|
||||
break
|
||||
|
||||
return {
|
||||
"source": "nvd:recent",
|
||||
"count": len(out),
|
||||
"indicators": out,
|
||||
"total_results": data.get("totalResults"),
|
||||
}
|
||||
|
||||
|
||||
_run(main)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user