From 334ecac83eb91ac1c7e2f77b12c2cc051a23de67 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Sat, 11 Jul 2026 22:22:46 +0200 Subject: [PATCH] feat(microsoft-sentinel): new Microsoft Sentinel integration 19 commands (Azure Resource Manager API): incident ingestion + CRUD, comments, related alerts/entities/relations, watchlists, and threat indicators. Azure AD OAuth 2.0 client-credentials, stdlib-only. Co-Authored-By: Claude Fable 5 --- .../microsoft-sentinel-incident.yaml | 3 + integrations/microsoft-sentinel/manifest.yaml | 248 ++++++++++++++++++ .../mappers/get_incidents.yaml | 18 ++ .../microsoft-sentinel/scripts/add_comment.py | 80 ++++++ .../scripts/create_incident.py | 86 ++++++ .../scripts/create_threat_indicator.py | 102 +++++++ .../scripts/delete_incident.py | 74 ++++++ .../scripts/delete_threat_indicator.py | 74 ++++++ .../scripts/get_incident.py | 74 ++++++ .../scripts/get_incidents.py | 105 ++++++++ .../scripts/list_comments.py | 75 ++++++ .../scripts/list_incident_alerts.py | 74 ++++++ .../scripts/list_incident_entities.py | 74 ++++++ .../scripts/list_incident_relations.py | 74 ++++++ .../scripts/list_incidents.py | 72 +++++ .../scripts/list_threat_indicators.py | 70 +++++ .../scripts/list_watchlist_items.py | 75 ++++++ .../scripts/list_watchlists.py | 69 +++++ .../scripts/test_connection.py | 69 +++++ .../scripts/update_incident.py | 94 +++++++ .../scripts/update_threat_indicator.py | 88 +++++++ .../scripts/upsert_watchlist_item.py | 83 ++++++ 22 files changed, 1781 insertions(+) create mode 100644 integrations/microsoft-sentinel/incident-types/microsoft-sentinel-incident.yaml create mode 100644 integrations/microsoft-sentinel/manifest.yaml create mode 100644 integrations/microsoft-sentinel/mappers/get_incidents.yaml create mode 100644 integrations/microsoft-sentinel/scripts/add_comment.py create mode 100644 integrations/microsoft-sentinel/scripts/create_incident.py create mode 100644 integrations/microsoft-sentinel/scripts/create_threat_indicator.py create mode 100644 integrations/microsoft-sentinel/scripts/delete_incident.py create mode 100644 integrations/microsoft-sentinel/scripts/delete_threat_indicator.py create mode 100644 integrations/microsoft-sentinel/scripts/get_incident.py create mode 100644 integrations/microsoft-sentinel/scripts/get_incidents.py create mode 100644 integrations/microsoft-sentinel/scripts/list_comments.py create mode 100644 integrations/microsoft-sentinel/scripts/list_incident_alerts.py create mode 100644 integrations/microsoft-sentinel/scripts/list_incident_entities.py create mode 100644 integrations/microsoft-sentinel/scripts/list_incident_relations.py create mode 100644 integrations/microsoft-sentinel/scripts/list_incidents.py create mode 100644 integrations/microsoft-sentinel/scripts/list_threat_indicators.py create mode 100644 integrations/microsoft-sentinel/scripts/list_watchlist_items.py create mode 100644 integrations/microsoft-sentinel/scripts/list_watchlists.py create mode 100644 integrations/microsoft-sentinel/scripts/test_connection.py create mode 100644 integrations/microsoft-sentinel/scripts/update_incident.py create mode 100644 integrations/microsoft-sentinel/scripts/update_threat_indicator.py create mode 100644 integrations/microsoft-sentinel/scripts/upsert_watchlist_item.py diff --git a/integrations/microsoft-sentinel/incident-types/microsoft-sentinel-incident.yaml b/integrations/microsoft-sentinel/incident-types/microsoft-sentinel-incident.yaml new file mode 100644 index 0000000..ec3dbfd --- /dev/null +++ b/integrations/microsoft-sentinel/incident-types/microsoft-sentinel-incident.yaml @@ -0,0 +1,3 @@ +name: "Microsoft Sentinel Incident" +color: "#0078d4" +icon: "alert" diff --git a/integrations/microsoft-sentinel/manifest.yaml b/integrations/microsoft-sentinel/manifest.yaml new file mode 100644 index 0000000..3ca30bb --- /dev/null +++ b/integrations/microsoft-sentinel/manifest.yaml @@ -0,0 +1,248 @@ +id: microsoft_sentinel +name: Microsoft Sentinel +version: 1.0.0 +description: "Microsoft Sentinel (Azure Resource Manager API) — incident management (list/get/create/update/delete, comments, related alerts/entities), watchlists, threat indicators, and incident ingestion (get_incidents) with an OCSF mapper. Azure AD OAuth 2.0 client-credentials authentication. Stdlib-only, no extra Python dependencies." +changelog: "1.0.0 — Initial release: incident CRUD + comments + related data, watchlist listing/upsert, threat indicator CRUD, and incident ingestion with an OCSF mapper." +category: siem + +# Per-instance configuration. Register an Azure AD application, grant it the +# 'Microsoft Sentinel Contributor' (or Responder) role on the workspace's +# resource group, and admin-consent it. The scripts request an Azure Resource +# Manager token (client-credentials) and call https://management.azure.com. +config_schema: + properties: + tenant_id: + type: string + description: "Azure AD tenant (directory) ID" + client_id: + type: string + description: "Application (client) ID" + client_secret: + type: string + description: "Client secret" + x-soar-sensitive: true + subscription_id: + type: string + description: "Azure subscription ID that holds the Sentinel workspace" + resource_group: + type: string + description: "Resource group of the Sentinel workspace" + workspace_name: + type: string + description: "Log Analytics workspace name backing Sentinel" + required: + - tenant_id + - client_id + - client_secret + - subscription_id + - resource_group + - workspace_name + +commands: + # ── Ingestion / incidents ───────────────────────────────────────────────── + - id: get_incidents + name: microsoft-sentinel-get-incidents + description: "Fetch Sentinel incidents for ingestion. Returns {result:[...]}; use result as the alert rule results path." + risk: read + inputs_schema: + properties: + filter: { type: string, description: "OData $filter (e.g. properties/status eq 'New')" } + created_after: { type: string, description: "Lower bound on createdTimeUtc, ISO8601 (incremental fetch watermark, applied via $filter)" } + limit: { type: number, description: "Maximum incidents (default 50)" } + required: [] + outputs_schema: { properties: {} } + ingest: + results_path: result + dedup_key: name + incremental_field: created_after + - id: list_incidents + name: microsoft-sentinel-list-incidents + description: "List incidents with optional OData filter and ordering." + risk: read + inputs_schema: + properties: + filter: { type: string, description: "OData $filter expression" } + orderby: { type: string, description: "OData $orderby (e.g. properties/createdTimeUtc desc)" } + limit: { type: number, description: "Maximum incidents (default 50)" } + required: [] + outputs_schema: { properties: {} } + - id: get_incident + name: microsoft-sentinel-get-incident + description: "Get a single incident by its name (GUID)." + risk: read + inputs_schema: + properties: + incident_id: { type: string, description: "Incident name/ID (GUID)" } + required: [incident_id] + outputs_schema: { properties: {} } + - id: create_incident + name: microsoft-sentinel-create-incident + description: "Create an incident." + inputs_schema: + properties: + title: { type: string, description: "Incident title" } + severity: { type: string, description: "Informational, Low, Medium or High" } + status: { type: string, description: "New, Active or Closed (default New)" } + description: { type: string, description: "Description" } + assigned_to: { type: string, description: "Owner UPN/email" } + required: [title, severity] + outputs_schema: { properties: {} } + - id: update_incident + name: microsoft-sentinel-update-incident + description: "Update an incident's title, status, severity, classification or owner." + inputs_schema: + properties: + incident_id: { type: string, description: "Incident name/ID (GUID)" } + title: { type: string, description: "New title" } + status: { type: string, description: "New, Active or Closed" } + severity: { type: string, description: "Informational, Low, Medium or High" } + classification: { type: string, description: "Closing classification (BenignPositive, FalsePositive, TruePositive, Undetermined)" } + classification_reason: { type: string, description: "Closing reason" } + assigned_to: { type: string, description: "Owner UPN/email" } + description: { type: string, description: "Description" } + required: [incident_id] + outputs_schema: { properties: {} } + - id: delete_incident + name: microsoft-sentinel-delete-incident + description: "Delete an incident." + inputs_schema: + properties: + incident_id: { type: string, description: "Incident name/ID (GUID)" } + required: [incident_id] + outputs_schema: { properties: {} } + - id: add_comment + name: microsoft-sentinel-add-comment + description: "Add a comment to an incident." + inputs_schema: + properties: + incident_id: { type: string, description: "Incident name/ID (GUID)" } + comment: { type: string, description: "Comment message" } + required: [incident_id, comment] + outputs_schema: { properties: {} } + - id: list_comments + name: microsoft-sentinel-list-comments + description: "List an incident's comments." + risk: read + inputs_schema: + properties: + incident_id: { type: string, description: "Incident name/ID (GUID)" } + limit: { type: number, description: "Maximum comments (default 50)" } + required: [incident_id] + outputs_schema: { properties: {} } + - id: list_incident_alerts + name: microsoft-sentinel-list-incident-alerts + description: "List the alerts associated with an incident." + risk: read + inputs_schema: + properties: + incident_id: { type: string, description: "Incident name/ID (GUID)" } + required: [incident_id] + outputs_schema: { properties: {} } + - id: list_incident_entities + name: microsoft-sentinel-list-incident-entities + description: "List the entities (accounts, hosts, IPs, …) of an incident." + risk: read + inputs_schema: + properties: + incident_id: { type: string, description: "Incident name/ID (GUID)" } + required: [incident_id] + outputs_schema: { properties: {} } + - id: list_incident_relations + name: microsoft-sentinel-list-incident-relations + description: "List an incident's relations (linked bookmarks and alerts)." + risk: read + inputs_schema: + properties: + incident_id: { type: string, description: "Incident name/ID (GUID)" } + required: [incident_id] + outputs_schema: { properties: {} } + + # ── Watchlists ──────────────────────────────────────────────────────────── + - id: list_watchlists + name: microsoft-sentinel-list-watchlists + description: "List watchlists in the workspace." + risk: read + inputs_schema: + properties: + limit: { type: number, description: "Maximum watchlists (default 50)" } + required: [] + outputs_schema: { properties: {} } + - id: list_watchlist_items + name: microsoft-sentinel-list-watchlist-items + description: "List the items of a watchlist." + risk: read + inputs_schema: + properties: + watchlist_alias: { type: string, description: "Watchlist alias" } + limit: { type: number, description: "Maximum items (default 50)" } + required: [watchlist_alias] + outputs_schema: { properties: {} } + - id: upsert_watchlist_item + name: microsoft-sentinel-upsert-watchlist-item + description: "Create or update a watchlist item (properties as a JSON object)." + inputs_schema: + properties: + watchlist_alias: { type: string, description: "Watchlist alias" } + item_id: { type: string, description: "Watchlist item ID (GUID; generated when omitted)" } + item_json: { type: string, description: "Item key/value properties as a JSON object" } + required: [watchlist_alias, item_json] + outputs_schema: { properties: {} } + + # ── Threat indicators ───────────────────────────────────────────────────── + - id: list_threat_indicators + name: microsoft-sentinel-list-threat-indicators + description: "List threat intelligence indicators." + risk: read + inputs_schema: + properties: + filter: { type: string, description: "OData $filter expression" } + limit: { type: number, description: "Maximum indicators (default 50)" } + required: [] + outputs_schema: { properties: {} } + - id: create_threat_indicator + name: microsoft-sentinel-create-threat-indicator + description: "Create a threat intelligence indicator." + inputs_schema: + properties: + value: { type: string, description: "Indicator value" } + pattern_type: { type: string, description: "Type: ipv4-addr, domain-name, url, file:hashes.'SHA-256', …" } + display_name: { type: string, description: "Display name" } + threat_types: { type: string, description: "Comma-separated threat types (e.g. malicious-activity)" } + confidence: { type: number, description: "Confidence 0-100" } + valid_until: { type: string, description: "Expiration, ISO8601" } + required: [value, pattern_type] + outputs_schema: { properties: {} } + - id: update_threat_indicator + name: microsoft-sentinel-update-threat-indicator + description: "Update a threat intelligence indicator (display name, confidence, expiry, threat types)." + inputs_schema: + properties: + indicator_name: { type: string, description: "Indicator name (from list_threat_indicators)" } + display_name: { type: string, description: "Display name" } + confidence: { type: number, description: "Confidence 0-100" } + valid_until: { type: string, description: "Expiration, ISO8601" } + threat_types: { type: string, description: "Comma-separated threat types" } + required: [indicator_name] + outputs_schema: { properties: {} } + - id: delete_threat_indicator + name: microsoft-sentinel-delete-threat-indicator + description: "Delete a threat intelligence indicator by name." + inputs_schema: + properties: + indicator_name: { type: string, description: "Indicator name (from list_threat_indicators)" } + required: [indicator_name] + outputs_schema: { properties: {} } + + - id: test_connection + name: microsoft-sentinel-test-connection + description: "Verify connectivity and credentials (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + +ingestion: + command: get_incidents + mapper: get_incidents + default_incident_type: "Microsoft Sentinel Incident" diff --git a/integrations/microsoft-sentinel/mappers/get_incidents.yaml b/integrations/microsoft-sentinel/mappers/get_incidents.yaml new file mode 100644 index 0000000..798ee66 --- /dev/null +++ b/integrations/microsoft-sentinel/mappers/get_incidents.yaml @@ -0,0 +1,18 @@ +name: "Microsoft Sentinel Incidents → OCSF" +description: "Maps a Sentinel incident (get_incidents, results_path = result) to OCSF finding fields. The fetch script lifts the ARM 'properties' object to the top level and keeps the incident 'name' (GUID) as the id." +field_mappings: + title: "title" + severity: "severity = 'High' ? 4 : (severity = 'Medium' ? 3 : (severity = 'Low' ? 2 : 1))" + description: "description" +ocsf: + - { source_path: "name", ocsf_field: "finding_info.uid" } + - { source_path: "incidentNumber", ocsf_field: "finding_info.uid_alt" } + - { source_path: "title", ocsf_field: "finding_info.title" } + - { source_path: "description", ocsf_field: "finding_info.desc" } + - { source_path: "createdTimeUtc", ocsf_field: "finding_info.created_time" } + - { source_path: "lastModifiedTimeUtc", ocsf_field: "finding_info.modified_time" } + - { source_path: "incidentUrl", ocsf_field: "finding_info.src_url" } + - { source_path: "status", ocsf_field: "status" } + - { source_path: "classification", ocsf_field: "disposition" } + - { source_path: "owner.assignedTo", ocsf_field: "assignee.name" } + - { source_path: "labels[0].labelName", ocsf_field: "finding_info.types" } diff --git a/integrations/microsoft-sentinel/scripts/add_comment.py b/integrations/microsoft-sentinel/scripts/add_comment.py new file mode 100644 index 0000000..fe838d1 --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/add_comment.py @@ -0,0 +1,80 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error +import uuid + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + incident_id = inputs.get("incident_id") + comment = inputs.get("comment") + if not incident_id: + raise Exception("incident_id is required") + if not comment: + raise Exception("comment is required") + comment_id = str(uuid.uuid4()) + res = request("PUT", "/incidents/" + q(incident_id) + "/comments/" + comment_id, + body={"properties": {"message": comment}}) + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/create_incident.py b/integrations/microsoft-sentinel/scripts/create_incident.py new file mode 100644 index 0000000..2972bff --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/create_incident.py @@ -0,0 +1,86 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error +import uuid + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + title = inputs.get("title") + severity = inputs.get("severity") + if not title: + raise Exception("title is required") + if not severity: + raise Exception("severity is required") + status = inputs.get("status") or "New" + description = inputs.get("description") + assigned_to = inputs.get("assigned_to") + + props = {"title": title, "severity": severity, "status": status} + if description: + props["description"] = description + if assigned_to: + props["owner"] = {"assignedTo": assigned_to} + + new_guid = str(uuid.uuid4()) + res = request("PUT", "/incidents/" + new_guid, body={"properties": props}) + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/create_threat_indicator.py b/integrations/microsoft-sentinel/scripts/create_threat_indicator.py new file mode 100644 index 0000000..bb4a85f --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/create_threat_indicator.py @@ -0,0 +1,102 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + value = inputs.get("value") + pattern_type = inputs.get("pattern_type") + if not value: + raise Exception("value is required") + if not pattern_type: + raise Exception("pattern_type is required") + + display_name = inputs.get("display_name") or value + threat_types_raw = inputs.get("threat_types") + if threat_types_raw: + threat_types = [t.strip() for t in str(threat_types_raw).split(",") if t.strip()] + else: + threat_types = ["malicious-activity"] + confidence = inputs.get("confidence") + valid_until = inputs.get("valid_until") + + if str(pattern_type).startswith("file"): + pattern = "[" + str(pattern_type) + " = '" + str(value) + "']" + else: + pattern = "[" + str(pattern_type) + ":value = '" + str(value) + "']" + + props = { + "pattern": pattern, + "patternType": pattern_type, + "displayName": display_name, + "threatTypes": threat_types, + "source": "Riposte", + } + if confidence is not None and confidence != "": + props["confidence"] = int(confidence) + if valid_until: + props["validUntil"] = valid_until + + body = {"kind": "indicator", "properties": props} + res = request("POST", "/threatIntelligence/main/createIndicator", body=body) + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/delete_incident.py b/integrations/microsoft-sentinel/scripts/delete_incident.py new file mode 100644 index 0000000..eb40652 --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/delete_incident.py @@ -0,0 +1,74 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + incident_id = inputs.get("incident_id") + if not incident_id: + raise Exception("incident_id is required") + request("DELETE", "/incidents/" + q(incident_id)) + print(json.dumps({"ok": True, "incident_id": incident_id})) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/delete_threat_indicator.py b/integrations/microsoft-sentinel/scripts/delete_threat_indicator.py new file mode 100644 index 0000000..74b7ae1 --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/delete_threat_indicator.py @@ -0,0 +1,74 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + indicator_name = inputs.get("indicator_name") + if not indicator_name: + raise Exception("indicator_name is required") + request("DELETE", "/threatIntelligence/main/indicators/" + q(indicator_name)) + print(json.dumps({"ok": True, "indicator_name": indicator_name})) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/get_incident.py b/integrations/microsoft-sentinel/scripts/get_incident.py new file mode 100644 index 0000000..6deef2a --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/get_incident.py @@ -0,0 +1,74 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + incident_id = inputs.get("incident_id") + if not incident_id: + raise Exception("incident_id is required") + res = request("GET", "/incidents/" + q(incident_id)) + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/get_incidents.py b/integrations/microsoft-sentinel/scripts/get_incidents.py new file mode 100644 index 0000000..0a431ae --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/get_incidents.py @@ -0,0 +1,105 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error +from datetime import datetime, timezone + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _normalize_iso(s): + s = str(s) + if s.isdigit(): + ts = int(s) + if ts > 10 ** 12: + ts = ts / 1000.0 + return datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + try: + dt = datetime.fromisoformat(s.replace("Z", "+00:00")).astimezone(timezone.utc) + return dt.strftime("%Y-%m-%dT%H:%M:%SZ") + except Exception: + return s + + +def _flatten(item): + flat = dict(item.get("properties", {})) + flat["name"] = item.get("name") + flat["id"] = item.get("id") + return flat + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + filt = inputs.get("filter") + created_after = inputs.get("created_after") + limit = inputs.get("limit") or 50 + + filter_parts = [] + if filt: + filter_parts.append(str(filt)) + if created_after: + filter_parts.append("properties/createdTimeUtc ge " + _normalize_iso(created_after)) + filter_str = " and ".join(filter_parts) if filter_parts else None + + res = request("GET", "/incidents", params={ + "$filter": filter_str, + "$top": limit, + "$orderby": "properties/createdTimeUtc asc", + }) + print(json.dumps({"result": [_flatten(i) for i in res.get("value", [])]})) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/list_comments.py b/integrations/microsoft-sentinel/scripts/list_comments.py new file mode 100644 index 0000000..1245f2d --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/list_comments.py @@ -0,0 +1,75 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + incident_id = inputs.get("incident_id") + if not incident_id: + raise Exception("incident_id is required") + limit = inputs.get("limit") or 50 + res = request("GET", "/incidents/" + q(incident_id) + "/comments", params={"$top": limit}) + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/list_incident_alerts.py b/integrations/microsoft-sentinel/scripts/list_incident_alerts.py new file mode 100644 index 0000000..09fcff1 --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/list_incident_alerts.py @@ -0,0 +1,74 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + incident_id = inputs.get("incident_id") + if not incident_id: + raise Exception("incident_id is required") + res = request("POST", "/incidents/" + q(incident_id) + "/alerts") + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/list_incident_entities.py b/integrations/microsoft-sentinel/scripts/list_incident_entities.py new file mode 100644 index 0000000..32f5a45 --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/list_incident_entities.py @@ -0,0 +1,74 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + incident_id = inputs.get("incident_id") + if not incident_id: + raise Exception("incident_id is required") + res = request("POST", "/incidents/" + q(incident_id) + "/entities") + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/list_incident_relations.py b/integrations/microsoft-sentinel/scripts/list_incident_relations.py new file mode 100644 index 0000000..7176d3a --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/list_incident_relations.py @@ -0,0 +1,74 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + incident_id = inputs.get("incident_id") + if not incident_id: + raise Exception("incident_id is required") + res = request("GET", "/incidents/" + q(incident_id) + "/relations") + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/list_incidents.py b/integrations/microsoft-sentinel/scripts/list_incidents.py new file mode 100644 index 0000000..06e883b --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/list_incidents.py @@ -0,0 +1,72 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + res = request("GET", "/incidents", params={ + "$filter": inputs.get("filter"), + "$orderby": inputs.get("orderby"), + "$top": inputs.get("limit") or 50, + }) + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/list_threat_indicators.py b/integrations/microsoft-sentinel/scripts/list_threat_indicators.py new file mode 100644 index 0000000..b29837d --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/list_threat_indicators.py @@ -0,0 +1,70 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + filt = inputs.get("filter") + limit = inputs.get("limit") or 50 + res = request("GET", "/threatIntelligence/main/indicators", params={"$filter": filt or None, "$top": limit}) + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/list_watchlist_items.py b/integrations/microsoft-sentinel/scripts/list_watchlist_items.py new file mode 100644 index 0000000..813c787 --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/list_watchlist_items.py @@ -0,0 +1,75 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + watchlist_alias = inputs.get("watchlist_alias") + if not watchlist_alias: + raise Exception("watchlist_alias is required") + limit = inputs.get("limit") or 50 + res = request("GET", "/watchlists/" + q(watchlist_alias) + "/watchlistItems", params={"$top": limit}) + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/list_watchlists.py b/integrations/microsoft-sentinel/scripts/list_watchlists.py new file mode 100644 index 0000000..c1a69fb --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/list_watchlists.py @@ -0,0 +1,69 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + limit = inputs.get("limit") or 50 + res = request("GET", "/watchlists", params={"$top": limit}) + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/test_connection.py b/integrations/microsoft-sentinel/scripts/test_connection.py new file mode 100644 index 0000000..3f9ab9a --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/test_connection.py @@ -0,0 +1,69 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + res = request("GET", "/incidents", params={"$top": 1}) + if "value" not in res: + raise Exception("unexpected response") + print(json.dumps({"ok": True})) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/update_incident.py b/integrations/microsoft-sentinel/scripts/update_incident.py new file mode 100644 index 0000000..6111fd1 --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/update_incident.py @@ -0,0 +1,94 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + incident_id = inputs.get("incident_id") + if not incident_id: + raise Exception("incident_id is required") + + existing = request("GET", "/incidents/" + q(incident_id)) + props = dict(existing.get("properties", {})) + + if inputs.get("title"): + props["title"] = inputs["title"] + if inputs.get("status"): + props["status"] = inputs["status"] + if inputs.get("severity"): + props["severity"] = inputs["severity"] + if inputs.get("description"): + props["description"] = inputs["description"] + if inputs.get("classification"): + props["classification"] = inputs["classification"] + if inputs.get("classification_reason"): + props["classificationReason"] = inputs["classification_reason"] + if inputs.get("assigned_to"): + props["owner"] = {"assignedTo": inputs["assigned_to"]} + + body = {"properties": props, "etag": existing.get("etag")} + res = request("PUT", "/incidents/" + q(incident_id), body=body) + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/update_threat_indicator.py b/integrations/microsoft-sentinel/scripts/update_threat_indicator.py new file mode 100644 index 0000000..e3311b3 --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/update_threat_indicator.py @@ -0,0 +1,88 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + indicator_name = inputs.get("indicator_name") + if not indicator_name: + raise Exception("indicator_name is required") + + existing = request("GET", "/threatIntelligence/main/indicators/" + q(indicator_name)) + props = dict(existing.get("properties", {})) + + if inputs.get("display_name"): + props["displayName"] = inputs["display_name"] + if inputs.get("confidence") is not None and inputs.get("confidence") != "": + props["confidence"] = int(inputs["confidence"]) + if inputs.get("valid_until"): + props["validUntil"] = inputs["valid_until"] + if inputs.get("threat_types"): + props["threatTypes"] = [t.strip() for t in str(inputs["threat_types"]).split(",") if t.strip()] + + body = {"kind": "indicator", "properties": props, "etag": existing.get("etag")} + res = request("PUT", "/threatIntelligence/main/indicators/" + q(indicator_name), body=body) + print(json.dumps(res)) + + +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) diff --git a/integrations/microsoft-sentinel/scripts/upsert_watchlist_item.py b/integrations/microsoft-sentinel/scripts/upsert_watchlist_item.py new file mode 100644 index 0000000..3942461 --- /dev/null +++ b/integrations/microsoft-sentinel/scripts/upsert_watchlist_item.py @@ -0,0 +1,83 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error +import uuid + +API_VERSION = "2023-11-01" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _token(): + cfg = _cfg() + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", + "client_id": str(cfg.get("client_id") or ""), + "client_secret": str(cfg.get("client_secret") or ""), + "scope": "https://management.azure.com/.default", + }).encode("utf-8") + url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id") or "") + "/oauth2/v2.0/token" + req = urllib.request.Request(url, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, + method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + tok = json.loads(r.read()) + if not tok.get("access_token"): + raise Exception("Token request failed: " + json.dumps(tok)) + return tok["access_token"] + + +def _si_base(): + cfg = _cfg() + for key in ("subscription_id", "resource_group", "workspace_name"): + if not str(cfg.get(key) or ""): + raise Exception(key + " is not set") + return ("https://management.azure.com/subscriptions/" + str(cfg["subscription_id"]) + + "/resourceGroups/" + str(cfg["resource_group"]) + + "/providers/Microsoft.OperationalInsights/workspaces/" + str(cfg["workspace_name"]) + + "/providers/Microsoft.SecurityInsights") + + +def request(method, path, params=None, body=None, token=None): + url = _si_base() + path + q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + q["api-version"] = API_VERSION + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Accept": "application/json", "Authorization": "Bearer " + (token or _token())} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + watchlist_alias = inputs.get("watchlist_alias") + item_json = inputs.get("item_json") + if not watchlist_alias: + raise Exception("watchlist_alias is required") + if not item_json: + raise Exception("item_json is required") + item = json.loads(item_json) + if not isinstance(item, dict): + raise Exception("item_json must be a JSON object") + item_id = inputs.get("item_id") or str(uuid.uuid4()) + res = request("PUT", "/watchlists/" + q(watchlist_alias) + "/watchlistItems/" + q(item_id), + body={"properties": {"itemsKeyValue": item}}) + print(json.dumps(res)) + + +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)