diff --git a/playbooks/CrowdStrike_OAuth_API_Endpoint_Analysis.json b/playbooks/CrowdStrike_OAuth_API_Endpoint_Analysis.json
new file mode 100644
index 0000000000..efa5626ad0
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Endpoint_Analysis.json
@@ -0,0 +1,795 @@
+{
+ "blockly": false,
+ "blockly_xml": "",
+ "category": "Enrichment",
+ "coa": {
+ "data": {
+ "description": "Accepts a hostname or device id as input and collects running processes, network connections and various system information from the device via Crowdstrike. We then generate an observable report for each. This can be customized based on user preference.",
+ "edges": [
+ {
+ "id": "port_0_to_port_2",
+ "sourceNode": "0",
+ "sourcePort": "0_out",
+ "targetNode": "2",
+ "targetPort": "2_in"
+ },
+ {
+ "conditions": [
+ {
+ "index": 0
+ }
+ ],
+ "id": "port_2_to_port_7",
+ "sourceNode": "2",
+ "sourcePort": "2_out",
+ "targetNode": "7",
+ "targetPort": "7_in"
+ },
+ {
+ "id": "port_7_to_port_8",
+ "sourceNode": "7",
+ "sourcePort": "7_out",
+ "targetNode": "8",
+ "targetPort": "8_in"
+ },
+ {
+ "id": "port_8_to_port_11",
+ "sourceNode": "8",
+ "sourcePort": "8_out",
+ "targetNode": "11",
+ "targetPort": "11_in"
+ },
+ {
+ "id": "port_11_to_port_12",
+ "sourceNode": "11",
+ "sourcePort": "11_out",
+ "targetNode": "12",
+ "targetPort": "12_in"
+ },
+ {
+ "id": "port_12_to_port_17",
+ "sourceNode": "12",
+ "sourcePort": "12_out",
+ "targetNode": "17",
+ "targetPort": "17_in"
+ },
+ {
+ "id": "port_11_to_port_18",
+ "sourceNode": "11",
+ "sourcePort": "11_out",
+ "targetNode": "18",
+ "targetPort": "18_in"
+ },
+ {
+ "id": "port_18_to_port_19",
+ "sourceNode": "18",
+ "sourcePort": "18_out",
+ "targetNode": "19",
+ "targetPort": "19_in"
+ },
+ {
+ "id": "port_8_to_port_20",
+ "sourceNode": "8",
+ "sourcePort": "8_out",
+ "targetNode": "20",
+ "targetPort": "20_in"
+ },
+ {
+ "id": "port_20_to_port_21",
+ "sourceNode": "20",
+ "sourcePort": "20_out",
+ "targetNode": "21",
+ "targetPort": "21_in"
+ },
+ {
+ "id": "port_21_to_port_1",
+ "sourceNode": "21",
+ "sourcePort": "21_out",
+ "targetNode": "1",
+ "targetPort": "1_in"
+ },
+ {
+ "id": "port_19_to_port_23",
+ "sourceNode": "19",
+ "sourcePort": "19_out",
+ "targetNode": "23",
+ "targetPort": "23_in"
+ },
+ {
+ "id": "port_17_to_port_23",
+ "sourceNode": "17",
+ "sourcePort": "17_out",
+ "targetNode": "23",
+ "targetPort": "23_in"
+ },
+ {
+ "id": "port_23_to_port_1",
+ "sourceNode": "23",
+ "sourcePort": "23_out",
+ "targetNode": "1",
+ "targetPort": "1_in"
+ },
+ {
+ "id": "port_11_to_port_24",
+ "sourceNode": "11",
+ "sourcePort": "11_out",
+ "targetNode": "24",
+ "targetPort": "24_in"
+ },
+ {
+ "id": "port_24_to_port_25",
+ "sourceNode": "24",
+ "sourcePort": "24_out",
+ "targetNode": "25",
+ "targetPort": "25_in"
+ },
+ {
+ "id": "port_25_to_port_23",
+ "sourceNode": "25",
+ "sourcePort": "25_out",
+ "targetNode": "23",
+ "targetPort": "23_in"
+ }
+ ],
+ "hash": "b056d8c5001ac05e31fe03ff61e1acb72c4b9281",
+ "nodes": {
+ "0": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_start",
+ "id": "0",
+ "type": "start"
+ },
+ "errors": {},
+ "id": "0",
+ "type": "start",
+ "warnings": {},
+ "x": 360,
+ "y": -3.197442310920451e-13
+ },
+ "1": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_finish",
+ "id": "1",
+ "type": "end"
+ },
+ "errors": {},
+ "id": "1",
+ "type": "end",
+ "warnings": {},
+ "x": 440,
+ "y": 1460
+ },
+ "11": {
+ "data": {
+ "action": "create session",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "create session",
+ "customNameId": 0,
+ "description": "Create a Real Time Response (RTR) session on CS Falcon to interact with the endpoint.",
+ "join": [],
+ "note": "Create a Real Time Response (RTR) session on CS Falcon to interact with the endpoint."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "create_session",
+ "id": "11",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "device_id": "query_device:action_result.data.*.device_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "device_id"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "11",
+ "type": "action",
+ "warnings": {},
+ "x": 170,
+ "y": 680
+ },
+ "12": {
+ "data": {
+ "action": "run admin command",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "run admin command ps",
+ "customNameId": 0,
+ "description": "Gets a list of running processes on the specified endpoint.",
+ "join": [],
+ "note": "Gets a list of running processes on the specified endpoint."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "run_admin_command_ps",
+ "id": "12",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "command": "ps",
+ "device_id": "query_device:action_result.data.*.device_id",
+ "session_id": "create_session:action_result.data.*.resources.*.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "command"
+ },
+ {
+ "data_type": "string",
+ "field": "device_id"
+ },
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "12",
+ "type": "action",
+ "warnings": {},
+ "x": 340,
+ "y": 840
+ },
+ "17": {
+ "data": {
+ "advanced": {
+ "customName": "process process observables",
+ "customNameId": 0,
+ "description": "Format a normalized output for each process",
+ "join": [],
+ "note": "Format a normalized output for each process"
+ },
+ "functionId": 3,
+ "functionName": "process_process_observables",
+ "id": "17",
+ "inputParameters": [
+ "query_device:action_result.data.*.hostname",
+ "run_admin_command_ps:action_result.parameter",
+ "run_admin_command_ps:action_result.data",
+ "run_admin_command_ps:action_result.status",
+ "run_admin_command_ps:action_result.message"
+ ],
+ "outputVariables": [
+ "observable_array"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "17",
+ "type": "code",
+ "userCode": "\n import re\n process_process_observables__observable_array = []\n i = 0\n \n ################################################################################\n # This returns process information from stdout as text, with lines separated by newlines (\\n).\n # For example:\n #\n # Name Id Start Time (UTC-7) WorkingMemory(kb) CPU(s) HandleCount Path \n # ---- -- ------------------ ----------------- ------ ----------- ---- \n # conhost 2216 4/8/2025 12:14:21 PM 5,612 0.05 93 C:\\Windows\\system32\\conhost.exe \n # conhost 4192 4/8/2025 12:14:21 PM 5,620 0.03 93 C:\\Windows\\system32\\conhost.exe \n # CSFalconService 488 3/27/2025 9:05:13 AM 60,196 312.31 671 \n # csrss 672 3/11/2025 4:21:37 AM 4,476 667.27 500\n # powershell 3768 4/8/2025 12:14:21 PM 60,892 0.66 511 C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe \n # VGAuthService 2412 3/11/2025 4:21:42 AM 10,920 0.05 149 C:\\Program Files\\VMware\\VMware Tools\\VMware \n # VGAuth\\VGAuthService.exe \n # vm3dservice 2440 3/11/2025 4:21:42 AM 6,096 0.02 123 C:\\Windows\\system32\\vm3dservice.exe \n # vm3dservice 2568 3/11/2025 4:21:42 AM 6,564 0.03 110 C:\\Windows\\system32\\vm3dservice.exe \n # vm3dservice 4248 3/11/2025 4:25:31 AM 6,564 0.08 111 C:\\Windows\\system32\\vm3dservice.exe \n \n for system in run_admin_command_ps_result_item_1:\n device_id = run_admin_command_ps_result_item_0[i].get(\"device_id\")\n hostname = query_device_result_item_0[i]\n \n observable = {\n \"source\": \"Crowdstrike OAuth API\",\n \"type\": \"Endpoint\",\n \"activity_name\": \"Process Collection\",\n \"uid\": device_id,\n \"hostname\": hostname,\n \"status\": run_admin_command_ps_result_item_2[i],\n \"status_detail\": run_admin_command_ps_result_message[i],\n \"process_artifacts\": []\n }\n \n stdout = system[0][\"resources\"][0][\"stdout\"]\n \n # Skip header lines and read each one using REGEX (for lack of a better way).\n for line in stdout.split('\\n')[3:]:\n pattern = re.compile('^(?P\\w+)\\s+(?P\\d+)\\s+(?P[\\d\\/:\\s]+(AM|PM))\\s+(?P[\\d,]+)\\s+(?P[\\d,\\.]+)\\s+(?P\\d+)\\s(?P.*)$')\n matches = pattern.match(line)\n orphan_matches = pattern.match(line)\n \n if matches:\n name = matches.group('name')\n pid = matches.group('pid')\n start_time = matches.group('start_time')\n working_memory = matches.group('working_memory')\n cpu = matches.group('cpu')\n handle_count = matches.group('handle_count')\n path = matches.group('path') \n \n # Skip lines we don't care about.\n if line.strip() and name not in ['Idle','System']:\n observable['process_artifacts'].append(\n {\n \"name\": name,\n \"pid\": pid,\n \"path\": path.strip(),\n \"created_time\": start_time,\n \"xattributes\": [\n {\n \"working_memory\": working_memory,\n \"handle_count\": handle_count,\n \"cpu\": cpu\n }\n ]\n }\n )\n else:\n # Some paths are continued on the next line... fold them back into the correct observable.\n orphan_pattern = re.compile('^\\s+(?P.*?)\\s+$')\n orphan_matches = orphan_pattern.match(line)\n \n if orphan_matches:\n orphaned_path = orphan_matches.group('orphaned_path')\n previous_observable = observable['process_artifacts'][-1]\n if previous_observable:\n previous_observable[\"path\"] = previous_observable[\"path\"] + \" \" + orphaned_path\n \n \n #phantom.debug(f\"observable = {observable}\")\n process_process_observables__observable_array.append(observable)\n i+=1\n \n",
+ "warnings": {},
+ "x": 340,
+ "y": 1020
+ },
+ "18": {
+ "data": {
+ "action": "run admin command",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "run admin command netstat",
+ "customNameId": 0,
+ "description": "List the open sockets & network ports on the specified endpoint.",
+ "join": [],
+ "note": "List the open sockets & network ports on the specified endpoint."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 2,
+ "functionName": "run_admin_command_netstat",
+ "id": "18",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "command": "netstat",
+ "device_id": "query_device:action_result.data.*.device_id",
+ "session_id": "create_session:action_result.data.*.resources.*.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "command"
+ },
+ {
+ "data_type": "string",
+ "field": "device_id"
+ },
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "18",
+ "type": "action",
+ "warnings": {},
+ "x": 0,
+ "y": 840
+ },
+ "19": {
+ "customCode": null,
+ "data": {
+ "advanced": {
+ "customName": "process network observables",
+ "customNameId": 0,
+ "description": "Format a normalized output for each network connection",
+ "join": [],
+ "note": "Format a normalized output for each network connection"
+ },
+ "functionId": 4,
+ "functionName": "process_network_observables",
+ "id": "19",
+ "inputParameters": [
+ "run_admin_command_netstat:action_result.parameter.device_id",
+ "query_device:action_result.data.*.hostname",
+ "run_admin_command_netstat:action_result.status",
+ "run_admin_command_netstat:action_result.message",
+ "run_admin_command_netstat:action_result.data"
+ ],
+ "outputVariables": [
+ "observable_array"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "19",
+ "type": "code",
+ "userCode": "\n import re\n process_network_observables__observable_array = []\n i = 0\n \n ################################################################################\n # This returns network connection information from stdout as text, with lines separated by newlines (\\n).\n # For example:\n #\n # Proto Local Address Foreign Address State Owning Process Id Owning Process Name Process Start Time (UTC-8)\n # ----- ------------- --------------- ----- ----------------- ------------------- --------------------------\n # TCP :::49689 :::0 Listen 920 C:\\Windows\\system32\\lsass.exe 10/25/2024 10:28:22 AM \n # TCP :::49675 :::0 Listen 2060 C:\\Windows\\system32\\svchost.exe 10/25/2024 10:28:13 AM \n # TCP :::49669 :::0 Listen 2164 C:\\Windows\\System32\\spoolsv.exe 10/25/2024 10:28:13 AM \n # TCP :::49666 :::0 Listen 1056 C:\\Windows\\system32\\svchost.exe 10/25/2024 10:28:13 AM \n # TCP :::49665 :::0 Listen 1116 C:\\Windows\\System32\\svchost.exe 10/25/2024 10:28:12 AM \n # TCP :::49664 :::0 Listen 768 10/25/2024 10:28:12 AM \n # TCP :::3389 :::0 Listen 1048 C:\\Windows\\System32\\svchost.exe 10/25/2024 10:28:13 AM \n # TCP :::445 :::0 Listen 4 10/25/2024 10:28:13 AM \n # TCP :::135 :::0 Listen 352 C:\\Windows\\system32\\svchost.exe 10/25/2024 10:28:12 AM \n # TCP 0.0.0.0:58808 0.0.0.0:0 Bound 1056 C:\\Windows\\system32\\svchost.exe 11/23/2024 9:31:16 PM \n # TCP 10.1.19.4:59929 54.241.197.58:443 Established 4 2/24/2025 2:34:34 PM \n # TCP 10.1.19.4:58808 13.64.180.106:443 Established 1056 C:\\Windows\\system32\\svchost.exe 11/23/2024 9:31:16 PM \n # TCP 0.0.0.0:49689 0.0.0.0:0 Listen 920 C:\\Windows\\system32\\lsass.exe 10/25/2024 10:28:22 AM \n # TCP 0.0.0.0:49679 0.0.0.0:0 Listen 904 10/25/2024 10:28:13 AM \n # TCP 0.0.0.0:49675 0.0.0.0:0 Listen 2060 C:\\Windows\\system32\\svchost.exe 10/25/2024 10:28:13 AM \n # TCP 0.0.0.0:49669 0.0.0.0:0 Listen 2164 C:\\Windows\\System32\\spoolsv.exe 10/25/2024 10:28:13 AM \n # UDP ::1:60661 *:* Listen 5472 C:\\Windows\\system32\\svchost.exe 10/26/2024 9:03:09 AM \n # UDP fe80::9c8b:6e28:cc9:6e2e%2:60660 *:* Listen 5472 C:\\Windows\\system32\\svchost.exe 10/26/2024 9:03:09 AM \n # UDP :::5355 *:* Listen 1488 C:\\Windows\\System32\\svchost.exe 2/25/2025 6:52:43 AM \n # UDP :::5353 *:* Listen 1488 C:\\Windows\\System32\\svchost.exe 2/25/2025 6:52:43 AM \n\n for system in run_admin_command_netstat_result_item_3:\n device_id = run_admin_command_netstat_parameter_device_id[i]\n hostname = query_device_result_item_0[i]\n \n observable = {\n \"source\": \"Crowdstrike OAuth API\",\n \"type\": \"Endpoint\",\n \"activity_name\": \"Network Connections Collection\",\n \"uid\": device_id,\n \"hostname\": hostname,\n \"status\": run_admin_command_netstat_result_item_1[i],\n \"status_detail\": run_admin_command_netstat_result_message[i],\n \"network_artifacts\": []\n }\n\n phantom.debug(f\"system -> {system}\")\n \n stdout = system[0][\"resources\"][0][\"stdout\"]\n if stdout:\n # Skip header lines and read each one, extracting fields via REGEX (for lack of a better way).\n for line in stdout.split('\\n')[3:]: \n line_groups = re.search('^(\\w+)\\s+(.*?)\\s+(.*?)\\s+(\\w+)\\s+(\\d+)\\s(.*?)\\s(\\d.*?)\\s+$', line)\n \n if line_groups:\n proto = line_groups.group(1)\n local_address = line_groups.group(2)\n foreign_address = line_groups.group(3)\n state = line_groups.group(4)\n process_id = line_groups.group(5)\n process_path = line_groups.group(6)\n start_time = line_groups.group(7)\n \n observable['network_artifacts'].append(\n {\n \"protocol_name\": proto,\n \"local_address\": local_address,\n \"foreign_address\": foreign_address,\n \"state\": state,\n \"start_time\": start_time,\n \"process\": [\n {\n \"pid\": process_id,\n \"path\": process_path\n }\n ]\n }\n ) \n\n #phantom.debug(observable)\n process_network_observables__observable_array.append(observable)\n i+=1\n \n",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1020
+ },
+ "2": {
+ "data": {
+ "advanced": {
+ "customName": "input filter",
+ "customNameId": 0,
+ "description": "Determines if the provided inputs are present in the dataset.",
+ "join": [],
+ "note": "Determines if the provided inputs are present in the dataset."
+ },
+ "conditions": [
+ {
+ "comparisons": [
+ {
+ "conditionIndex": 0,
+ "op": "!=",
+ "param": "playbook_input:device",
+ "value": ""
+ }
+ ],
+ "conditionIndex": 0,
+ "customName": "input device present",
+ "logic": "and"
+ }
+ ],
+ "functionId": 1,
+ "functionName": "input_filter",
+ "id": "2",
+ "type": "filter"
+ },
+ "errors": {},
+ "id": "2",
+ "type": "filter",
+ "warnings": {},
+ "x": 400,
+ "y": 148
+ },
+ "20": {
+ "data": {
+ "action": "get system info",
+ "actionType": "investigate",
+ "advanced": {
+ "customName": "get system info",
+ "customNameId": 0,
+ "description": "Get information from the endpoint.",
+ "join": [],
+ "note": "Get information from the endpoint."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "get_system_info",
+ "id": "20",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "id": "query_device:action_result.data.*.device_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "id"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "20",
+ "type": "action",
+ "warnings": {},
+ "x": 680,
+ "y": 680
+ },
+ "21": {
+ "data": {
+ "advanced": {
+ "customName": "process system observables",
+ "customNameId": 0,
+ "description": "Format a normalized output for the endpoint.",
+ "join": [],
+ "note": "Format a normalized output for the endpoint."
+ },
+ "functionId": 2,
+ "functionName": "process_system_observables",
+ "id": "21",
+ "inputParameters": [
+ "get_system_info:action_result.status",
+ "get_system_info:action_result.message",
+ "get_system_info:action_result.data"
+ ],
+ "outputVariables": [
+ "observable_array"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "21",
+ "type": "code",
+ "userCode": " process_system_observables__observable_array = []\n i = 0\n \n for system in get_system_info_result_item_2:\n data = system[0]\n if data:\n observable = {\n \"source\": \"Crowdstrike OAuth API\", \n \"type\": \"Endpoint\", \n \"activity_name\": \"System Collection\",\n \"uid\": data['device_id'],\n \"hostname\": data['hostname'],\n \"status\": get_system_info_result_item_0[i],\n \"status_detail\": get_system_info_result_message[i],\n \"endpoint_artifacts\": [\n {\n \"agents\": [\n {\n \"type\": \"Endpoint Detection and Response\",\n \"type_id\": 1,\n \"uid\": data['cid'],\n \"vendor_name\": \"Crowdstrike\",\n \"version\": data['agent_version'] \n }\n ],\n \"ip\": data['local_ip'],\n \"external_ip\": data['external_ip'],\n \"mac\": data['mac_address'],\n \"domain\": data['machine_domain'],\n \"type\": data['product_type_desc'],\n \"last_seen\": data['last_seen'],\n \"last_reboot\": data['last_reboot'],\n \"last_login_user_sid\": data.get('last_login_user_sid', ''),\n \"last_login_timestamp\": data.get('last_login_timestamp', ''),\n \"operating_system\": {\n \"build:\": data['os_build'],\n \"kernel_release\": data['kernel_version'],\n \"name\": data['os_product_name'],\n \"type\": data['platform_name'],\n \"version\": data['os_version']\n },\n \"hw_info\": {\n \"bios_manufacturer\": data['bios_manufacturer'],\n \"bios_version\": data['bios_version'],\n \"serial_number\": data['serial_number'],\n \"chassis\": data['chassis_type_desc']\n }\n }\n ]\n }\n \n policies = []\n for policy in data['policies']:\n policies.append(\n {\n \"name\": policy['policy_type'],\n \"uid\": policy['policy_id'],\n \"is_applied\": policy['applied']\n }\n )\n observable['endpoint_artifacts'][0]['agents'][0]['policies'] = policies\n \n #phantom.debug(f\"---> OBSERVABLE: {observable}\")\n process_system_observables__observable_array.append(observable)\n i+=1\n\n",
+ "warnings": {},
+ "x": 680,
+ "y": 1020
+ },
+ "23": {
+ "data": {
+ "action": "delete session",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "delete session",
+ "customNameId": 0,
+ "description": "Deletes the Real Time Response (RTR) session on CS Falcon.",
+ "join": [],
+ "note": "Deletes the Real Time Response (RTR) session on CS Falcon."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "delete_session",
+ "id": "23",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "session_id": "create_session:action_result.summary.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "23",
+ "type": "action",
+ "warnings": {},
+ "x": 180,
+ "y": 1260
+ },
+ "24": {
+ "data": {
+ "action": "run admin command",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "run admin command services",
+ "customNameId": 0,
+ "description": "Runs a set of Powershell commands to gather details about the services on the endpoint. This requires that the host response policy permits it on the CrowdStrike side.",
+ "join": [],
+ "note": "Runs a set of Powershell commands to gather details about the services on the endpoint. This requires that the host response policy permits it on the CrowdStrike side."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 3,
+ "functionName": "run_admin_command_services",
+ "id": "24",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "command": "runscript",
+ "data": "-Raw=```Get-Service | Select-Object -Property Name, DisplayName, Status, ServiceType, StartType | ConvertTo-Json```",
+ "device_id": "query_device:action_result.data.*.device_id",
+ "session_id": "create_session:action_result.data.*.resources.*.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "command"
+ },
+ {
+ "data_type": "string",
+ "field": "device_id"
+ },
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "24",
+ "type": "action",
+ "warnings": {},
+ "x": -320,
+ "y": 840
+ },
+ "25": {
+ "data": {
+ "advanced": {
+ "customName": "process service observables",
+ "customNameId": 0,
+ "description": "Format a normalized output for each service",
+ "join": [],
+ "note": "Format a normalized output for each service"
+ },
+ "functionId": 1,
+ "functionName": "process_service_observables",
+ "id": "25",
+ "inputParameters": [
+ "query_device:action_result.data.*.device_id",
+ "query_device:action_result.data.*.hostname",
+ "run_admin_command_services:action_result.data.*.resources",
+ "run_admin_command_services:action_result.status",
+ "run_admin_command_services:action_result.message"
+ ],
+ "outputVariables": [
+ "observable_array"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "25",
+ "type": "code",
+ "userCode": "\n import json\n \n # Ref: https://learn.microsoft.com/en-us/dotnet/api/system.serviceprocess.servicestartmode?view=net-9.0-pp\n ocsf_start_type_mapping = {\n \"0\": { \"id\": \"1\", \"type\": \"Boot\" }, # Boot -> Boot\n \"1\": { \"id\": \"2\", \"type\": \"System\" }, # System -> System\n \"2\": { \"id\": \"3\", \"type\": \"Auto\" }, # Automatic -> Auto\n \"3\": { \"id\": \"4\", \"type\": \"Demand\" }, # Manual -> Demand\n \"4\": { \"id\": \"5\", \"type\": \"Disabled\" } # Disabled -> Disabled\n }\n \n # Ref: https://learn.microsoft.com/en-us/dotnet/api/system.serviceprocess.servicetype?view=net-9.0-pp\n ocsf_service_type_mapping = {\n # Unused for now. OCSF doesn't seem to properly support BITWISE ENUMs yet.\n # See \"service_type_id\" at https://schema.ocsf.io/1.4.0/objects/win/win_service\n }\n \n process_service_observables__observable_array = []\n for device_id, hostname, services, status, messsage in zip(query_device_result_item_0, query_device_result_item_1, run_admin_command_services_result_item_0, run_admin_command_services_result_item_1, run_admin_command_services_result_message):\n \n observable = {\n \"source\": \"Crowdstrike OAuth API\",\n \"type\": \"Endpoint\",\n \"activity_name\": \"Services Collection\",\n \"uid\": device_id,\n \"hostname\": hostname,\n \"status\": status,\n \"status_detail\": messsage,\n \"service_artifacts\": []\n }\n \n if services[0] and services[0][\"stdout\"]:\n services_json = json.loads(services[0][\"stdout\"])\n for service in services_json:\n start_type = ocsf_start_type_mapping.get(str(service[\"StartType\"]))\n \n observable[\"service_artifacts\"].append({\n \"type_id\": 3, \n \"type\": \"Service\",\n \"name\": service[\"Name\"],\n \"display_name\": service[\"DisplayName\"],\n \"service_start_type_id\": start_type[\"id\"],\n \"service_start_type\": start_type[\"type\"], \n \"service_type_id\": service[\"ServiceType\"], # No mapping to OCSF yet, see comment above.\n })\n \n process_service_observables__observable_array.append(observable)\n \n #phantom.debug(process_service_observables__observable_array)\n \n",
+ "warnings": {},
+ "x": -320,
+ "y": 1020
+ },
+ "7": {
+ "data": {
+ "advanced": {
+ "customName": "format fql",
+ "customNameId": 0,
+ "description": "Format the FQL query to get the input device information using its ID or hostname.",
+ "join": [],
+ "note": "Format the FQL query to get the input device information using its ID or hostname."
+ },
+ "functionId": 2,
+ "functionName": "format_fql",
+ "id": "7",
+ "parameters": [
+ "playbook_input:device"
+ ],
+ "template": "%%\nhostname:['{0}'],device_id:['{0}']\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "7",
+ "type": "format",
+ "warnings": {},
+ "x": 340,
+ "y": 328
+ },
+ "8": {
+ "data": {
+ "action": "query device",
+ "actionType": "investigate",
+ "advanced": {
+ "customName": "query device",
+ "customNameId": 0,
+ "description": "Get information about the device to unquarantine using its hostname or device id.",
+ "join": [],
+ "note": "Get information about the device to unquarantine using its hostname or device id."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "query_device",
+ "id": "8",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "filter": "format_fql:formatted_data.*",
+ "limit": 50
+ },
+ "requiredParameters": [
+ {
+ "data_type": "numeric",
+ "default": 50,
+ "field": "limit"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "8",
+ "type": "action",
+ "warnings": {},
+ "x": 340,
+ "y": 504
+ }
+ },
+ "notes": "Inputs: device (CrowdStrike Device ID or Hostname)\nInteractions: CrowdStrike OAuth API\nActions: unquarantine device\nOutputs: observables, markdown report"
+ },
+ "input_spec": [
+ {
+ "contains": [
+ "host name"
+ ],
+ "description": "Device ID or hostname of the host to quarantine",
+ "name": "device"
+ }
+ ],
+ "output_spec": [
+ {
+ "contains": [],
+ "datapaths": [
+ "process_process_observables:custom_function:observable_array"
+ ],
+ "deduplicate": false,
+ "description": "An array of process observable dictionaries",
+ "metadata": {},
+ "name": "process_observable"
+ },
+ {
+ "contains": [],
+ "datapaths": [
+ "process_network_observables:custom_function:observable_array"
+ ],
+ "deduplicate": false,
+ "description": "An array of network connection observable dictionaries",
+ "metadata": {},
+ "name": "network_observable"
+ },
+ {
+ "contains": [],
+ "datapaths": [
+ "process_system_observables:custom_function:observable_array"
+ ],
+ "deduplicate": false,
+ "description": "An array of endpoint observable dictionaries",
+ "metadata": {},
+ "name": "endpoint_observable"
+ },
+ {
+ "contains": [],
+ "datapaths": [
+ "process_service_observables:custom_function:observable_array"
+ ],
+ "deduplicate": false,
+ "description": "An array of service observable dictionaries",
+ "metadata": {},
+ "name": "service_observable"
+ }
+ ],
+ "playbook_trigger": "artifact_created",
+ "playbook_type": "data",
+ "python_version": "3",
+ "schema": "5.0.15",
+ "version": "6.3.1.178"
+ },
+ "create_time": "2025-05-01T17:39:10.288392+00:00",
+ "draft_mode": false,
+ "labels": [
+ "*"
+ ],
+ "tags": [
+ "CrowdStrike_OAuth_API",
+ "host name",
+ "enrichment",
+ "D3-NTA",
+ "D3-PA",
+ "D3-AI"
+ ]
+}
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_Endpoint_Analysis.png b/playbooks/CrowdStrike_OAuth_API_Endpoint_Analysis.png
new file mode 100644
index 0000000000..2ec126e9db
Binary files /dev/null and b/playbooks/CrowdStrike_OAuth_API_Endpoint_Analysis.png differ
diff --git a/playbooks/CrowdStrike_OAuth_API_Endpoint_Analysis.py b/playbooks/CrowdStrike_OAuth_API_Endpoint_Analysis.py
new file mode 100644
index 0000000000..0dfc82c0c9
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Endpoint_Analysis.py
@@ -0,0 +1,830 @@
+"""
+Accepts a hostname or device id as input and collects running processes, network connections and various system information from the device via Crowdstrike. We then generate an observable report for each. This can be customized based on user preference.
+"""
+
+
+import phantom.rules as phantom
+import json
+from datetime import datetime, timedelta
+
+
+@phantom.playbook_block()
+def on_start(container):
+ phantom.debug('on_start() called')
+
+ # call 'input_filter' block
+ input_filter(container=container)
+
+ return
+
+@phantom.playbook_block()
+def input_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("input_filter() called")
+
+ ################################################################################
+ # Determines if the provided inputs are present in the dataset.
+ ################################################################################
+
+ # collect filtered artifact ids and results for 'if' condition 1
+ matched_artifacts_1, matched_results_1 = phantom.condition(
+ container=container,
+ conditions=[
+ ["playbook_input:device", "!=", ""]
+ ],
+ name="input_filter:condition_1",
+ delimiter=None)
+
+ # call connected blocks if filtered artifacts or results
+ if matched_artifacts_1 or matched_results_1:
+ format_fql(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_1, filtered_results=matched_results_1)
+
+ return
+
+
+@phantom.playbook_block()
+def format_fql(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_fql() called")
+
+ ################################################################################
+ # Format the FQL query to get the input device information using its ID or hostname.
+ ################################################################################
+
+ template = """%%\nhostname:['{0}'],device_id:['{0}']\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "playbook_input:device"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_fql")
+
+ query_device(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def query_device(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("query_device() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Get information about the device to unquarantine using its hostname or device
+ # id.
+ ################################################################################
+
+ format_fql__as_list = phantom.get_format_data(name="format_fql__as_list")
+
+ parameters = []
+
+ # build parameters list for 'query_device' call
+ for format_fql__item in format_fql__as_list:
+ parameters.append({
+ "limit": 50,
+ "filter": format_fql__item,
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("query device", parameters=parameters, name="query_device", assets=["crowdstrike_oauth_api"], callback=query_device_callback)
+
+ return
+
+
+@phantom.playbook_block()
+def query_device_callback(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("query_device_callback() called")
+
+
+ create_session(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=filtered_artifacts, filtered_results=filtered_results)
+ get_system_info(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=filtered_artifacts, filtered_results=filtered_results)
+
+
+ return
+
+
+@phantom.playbook_block()
+def create_session(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("create_session() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Create a Real Time Response (RTR) session on CS Falcon to interact with the
+ # endpoint.
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'create_session' call
+ for query_device_result_item in query_device_result_data:
+ if query_device_result_item[0] is not None:
+ parameters.append({
+ "device_id": query_device_result_item[0],
+ "context": {'artifact_id': query_device_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("create session", parameters=parameters, name="create_session", assets=["crowdstrike_oauth_api"], callback=create_session_callback)
+
+ return
+
+
+@phantom.playbook_block()
+def create_session_callback(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("create_session_callback() called")
+
+
+ run_admin_command_ps(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=filtered_artifacts, filtered_results=filtered_results)
+ run_admin_command_netstat(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=filtered_artifacts, filtered_results=filtered_results)
+ run_admin_command_services(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=filtered_artifacts, filtered_results=filtered_results)
+
+
+ return
+
+
+@phantom.playbook_block()
+def run_admin_command_ps(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("run_admin_command_ps() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Gets a list of running processes on the specified endpoint.
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.data.*.resources.*.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'run_admin_command_ps' call
+ for query_device_result_item in query_device_result_data:
+ for create_session_result_item in create_session_result_data:
+ if query_device_result_item[0] is not None and create_session_result_item[0] is not None:
+ parameters.append({
+ "command": "ps",
+ "device_id": query_device_result_item[0],
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("run admin command", parameters=parameters, name="run_admin_command_ps", assets=["crowdstrike_oauth_api"], callback=process_process_observables)
+
+ return
+
+
+@phantom.playbook_block()
+def process_process_observables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("process_process_observables() called")
+
+ ################################################################################
+ # Format a normalized output for each process
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.hostname"], action_results=results)
+ run_admin_command_ps_result_data = phantom.collect2(container=container, datapath=["run_admin_command_ps:action_result.parameter","run_admin_command_ps:action_result.data","run_admin_command_ps:action_result.status","run_admin_command_ps:action_result.message"], action_results=results)
+
+ query_device_result_item_0 = [item[0] for item in query_device_result_data]
+ run_admin_command_ps_result_item_0 = [item[0] for item in run_admin_command_ps_result_data]
+ run_admin_command_ps_result_item_1 = [item[1] for item in run_admin_command_ps_result_data]
+ run_admin_command_ps_result_item_2 = [item[2] for item in run_admin_command_ps_result_data]
+ run_admin_command_ps_result_message = [item[3] for item in run_admin_command_ps_result_data]
+
+ process_process_observables__observable_array = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ import re
+ process_process_observables__observable_array = []
+ i = 0
+
+ ################################################################################
+ # This returns process information from stdout as text, with lines separated by newlines (\n).
+ # For example:
+ #
+ # Name Id Start Time (UTC-7) WorkingMemory(kb) CPU(s) HandleCount Path
+ # ---- -- ------------------ ----------------- ------ ----------- ----
+ # conhost 2216 4/8/2025 12:14:21 PM 5,612 0.05 93 C:\Windows\system32\conhost.exe
+ # conhost 4192 4/8/2025 12:14:21 PM 5,620 0.03 93 C:\Windows\system32\conhost.exe
+ # CSFalconService 488 3/27/2025 9:05:13 AM 60,196 312.31 671
+ # csrss 672 3/11/2025 4:21:37 AM 4,476 667.27 500
+ # powershell 3768 4/8/2025 12:14:21 PM 60,892 0.66 511 C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
+ # VGAuthService 2412 3/11/2025 4:21:42 AM 10,920 0.05 149 C:\Program Files\VMware\VMware Tools\VMware
+ # VGAuth\VGAuthService.exe
+ # vm3dservice 2440 3/11/2025 4:21:42 AM 6,096 0.02 123 C:\Windows\system32\vm3dservice.exe
+ # vm3dservice 2568 3/11/2025 4:21:42 AM 6,564 0.03 110 C:\Windows\system32\vm3dservice.exe
+ # vm3dservice 4248 3/11/2025 4:25:31 AM 6,564 0.08 111 C:\Windows\system32\vm3dservice.exe
+
+ for system in run_admin_command_ps_result_item_1:
+ device_id = run_admin_command_ps_result_item_0[i].get("device_id")
+ hostname = query_device_result_item_0[i]
+
+ observable = {
+ "source": "Crowdstrike OAuth API",
+ "type": "Endpoint",
+ "activity_name": "Process Collection",
+ "uid": device_id,
+ "hostname": hostname,
+ "status": run_admin_command_ps_result_item_2[i],
+ "status_detail": run_admin_command_ps_result_message[i],
+ "process_artifacts": []
+ }
+
+ stdout = system[0]["resources"][0]["stdout"]
+
+ # Skip header lines and read each one using REGEX (for lack of a better way).
+ for line in stdout.split('\n')[3:]:
+ pattern = re.compile('^(?P\w+)\s+(?P\d+)\s+(?P[\d\/:\s]+(AM|PM))\s+(?P[\d,]+)\s+(?P[\d,\.]+)\s+(?P\d+)\s(?P.*)$')
+ matches = pattern.match(line)
+ orphan_matches = pattern.match(line)
+
+ if matches:
+ name = matches.group('name')
+ pid = matches.group('pid')
+ start_time = matches.group('start_time')
+ working_memory = matches.group('working_memory')
+ cpu = matches.group('cpu')
+ handle_count = matches.group('handle_count')
+ path = matches.group('path')
+
+ # Skip lines we don't care about.
+ if line.strip() and name not in ['Idle','System']:
+ observable['process_artifacts'].append(
+ {
+ "name": name,
+ "pid": pid,
+ "path": path.strip(),
+ "created_time": start_time,
+ "xattributes": [
+ {
+ "working_memory": working_memory,
+ "handle_count": handle_count,
+ "cpu": cpu
+ }
+ ]
+ }
+ )
+ else:
+ # Some paths are continued on the next line... fold them back into the correct observable.
+ orphan_pattern = re.compile('^\s+(?P.*?)\s+$')
+ orphan_matches = orphan_pattern.match(line)
+
+ if orphan_matches:
+ orphaned_path = orphan_matches.group('orphaned_path')
+ previous_observable = observable['process_artifacts'][-1]
+ if previous_observable:
+ previous_observable["path"] = previous_observable["path"] + " " + orphaned_path
+
+
+ #phantom.debug(f"observable = {observable}")
+ process_process_observables__observable_array.append(observable)
+ i+=1
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="process_process_observables:observable_array", value=json.dumps(process_process_observables__observable_array))
+
+ join_delete_session(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def run_admin_command_netstat(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("run_admin_command_netstat() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # List the open sockets & network ports on the specified endpoint.
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.data.*.resources.*.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'run_admin_command_netstat' call
+ for query_device_result_item in query_device_result_data:
+ for create_session_result_item in create_session_result_data:
+ if query_device_result_item[0] is not None and create_session_result_item[0] is not None:
+ parameters.append({
+ "command": "netstat",
+ "device_id": query_device_result_item[0],
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("run admin command", parameters=parameters, name="run_admin_command_netstat", assets=["crowdstrike_oauth_api"], callback=process_network_observables)
+
+ return
+
+
+@phantom.playbook_block()
+def process_network_observables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("process_network_observables() called")
+
+ ################################################################################
+ # Format a normalized output for each network connection
+ ################################################################################
+
+ run_admin_command_netstat_result_data = phantom.collect2(container=container, datapath=["run_admin_command_netstat:action_result.parameter.device_id","run_admin_command_netstat:action_result.status","run_admin_command_netstat:action_result.message","run_admin_command_netstat:action_result.data"], action_results=results)
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.hostname"], action_results=results)
+
+ run_admin_command_netstat_parameter_device_id = [item[0] for item in run_admin_command_netstat_result_data]
+ run_admin_command_netstat_result_item_1 = [item[1] for item in run_admin_command_netstat_result_data]
+ run_admin_command_netstat_result_message = [item[2] for item in run_admin_command_netstat_result_data]
+ run_admin_command_netstat_result_item_3 = [item[3] for item in run_admin_command_netstat_result_data]
+ query_device_result_item_0 = [item[0] for item in query_device_result_data]
+
+ process_network_observables__observable_array = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ import re
+ process_network_observables__observable_array = []
+ i = 0
+
+ ################################################################################
+ # This returns network connection information from stdout as text, with lines separated by newlines (\n).
+ # For example:
+ #
+ # Proto Local Address Foreign Address State Owning Process Id Owning Process Name Process Start Time (UTC-8)
+ # ----- ------------- --------------- ----- ----------------- ------------------- --------------------------
+ # TCP :::49689 :::0 Listen 920 C:\Windows\system32\lsass.exe 10/25/2024 10:28:22 AM
+ # TCP :::49675 :::0 Listen 2060 C:\Windows\system32\svchost.exe 10/25/2024 10:28:13 AM
+ # TCP :::49669 :::0 Listen 2164 C:\Windows\System32\spoolsv.exe 10/25/2024 10:28:13 AM
+ # TCP :::49666 :::0 Listen 1056 C:\Windows\system32\svchost.exe 10/25/2024 10:28:13 AM
+ # TCP :::49665 :::0 Listen 1116 C:\Windows\System32\svchost.exe 10/25/2024 10:28:12 AM
+ # TCP :::49664 :::0 Listen 768 10/25/2024 10:28:12 AM
+ # TCP :::3389 :::0 Listen 1048 C:\Windows\System32\svchost.exe 10/25/2024 10:28:13 AM
+ # TCP :::445 :::0 Listen 4 10/25/2024 10:28:13 AM
+ # TCP :::135 :::0 Listen 352 C:\Windows\system32\svchost.exe 10/25/2024 10:28:12 AM
+ # TCP 0.0.0.0:58808 0.0.0.0:0 Bound 1056 C:\Windows\system32\svchost.exe 11/23/2024 9:31:16 PM
+ # TCP 10.1.19.4:59929 54.241.197.58:443 Established 4 2/24/2025 2:34:34 PM
+ # TCP 10.1.19.4:58808 13.64.180.106:443 Established 1056 C:\Windows\system32\svchost.exe 11/23/2024 9:31:16 PM
+ # TCP 0.0.0.0:49689 0.0.0.0:0 Listen 920 C:\Windows\system32\lsass.exe 10/25/2024 10:28:22 AM
+ # TCP 0.0.0.0:49679 0.0.0.0:0 Listen 904 10/25/2024 10:28:13 AM
+ # TCP 0.0.0.0:49675 0.0.0.0:0 Listen 2060 C:\Windows\system32\svchost.exe 10/25/2024 10:28:13 AM
+ # TCP 0.0.0.0:49669 0.0.0.0:0 Listen 2164 C:\Windows\System32\spoolsv.exe 10/25/2024 10:28:13 AM
+ # UDP ::1:60661 *:* Listen 5472 C:\Windows\system32\svchost.exe 10/26/2024 9:03:09 AM
+ # UDP fe80::9c8b:6e28:cc9:6e2e%2:60660 *:* Listen 5472 C:\Windows\system32\svchost.exe 10/26/2024 9:03:09 AM
+ # UDP :::5355 *:* Listen 1488 C:\Windows\System32\svchost.exe 2/25/2025 6:52:43 AM
+ # UDP :::5353 *:* Listen 1488 C:\Windows\System32\svchost.exe 2/25/2025 6:52:43 AM
+
+ for system in run_admin_command_netstat_result_item_3:
+ device_id = run_admin_command_netstat_parameter_device_id[i]
+ hostname = query_device_result_item_0[i]
+
+ observable = {
+ "source": "Crowdstrike OAuth API",
+ "type": "Endpoint",
+ "activity_name": "Network Connections Collection",
+ "uid": device_id,
+ "hostname": hostname,
+ "status": run_admin_command_netstat_result_item_1[i],
+ "status_detail": run_admin_command_netstat_result_message[i],
+ "network_artifacts": []
+ }
+
+ phantom.debug(f"system -> {system}")
+
+ stdout = system[0]["resources"][0]["stdout"]
+ if stdout:
+ # Skip header lines and read each one, extracting fields via REGEX (for lack of a better way).
+ for line in stdout.split('\n')[3:]:
+ line_groups = re.search('^(\w+)\s+(.*?)\s+(.*?)\s+(\w+)\s+(\d+)\s(.*?)\s(\d.*?)\s+$', line)
+
+ if line_groups:
+ proto = line_groups.group(1)
+ local_address = line_groups.group(2)
+ foreign_address = line_groups.group(3)
+ state = line_groups.group(4)
+ process_id = line_groups.group(5)
+ process_path = line_groups.group(6)
+ start_time = line_groups.group(7)
+
+ observable['network_artifacts'].append(
+ {
+ "protocol_name": proto,
+ "local_address": local_address,
+ "foreign_address": foreign_address,
+ "state": state,
+ "start_time": start_time,
+ "process": [
+ {
+ "pid": process_id,
+ "path": process_path
+ }
+ ]
+ }
+ )
+
+ #phantom.debug(observable)
+ process_network_observables__observable_array.append(observable)
+ i+=1
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="process_network_observables:observable_array", value=json.dumps(process_network_observables__observable_array))
+
+ join_delete_session(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def get_system_info(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("get_system_info() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Get information from the endpoint.
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'get_system_info' call
+ for query_device_result_item in query_device_result_data:
+ if query_device_result_item[0] is not None:
+ parameters.append({
+ "id": query_device_result_item[0],
+ "context": {'artifact_id': query_device_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("get system info", parameters=parameters, name="get_system_info", assets=["crowdstrike_oauth_api"], callback=process_system_observables)
+
+ return
+
+
+@phantom.playbook_block()
+def process_system_observables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("process_system_observables() called")
+
+ ################################################################################
+ # Format a normalized output for the endpoint.
+ ################################################################################
+
+ get_system_info_result_data = phantom.collect2(container=container, datapath=["get_system_info:action_result.status","get_system_info:action_result.message","get_system_info:action_result.data"], action_results=results)
+
+ get_system_info_result_item_0 = [item[0] for item in get_system_info_result_data]
+ get_system_info_result_message = [item[1] for item in get_system_info_result_data]
+ get_system_info_result_item_2 = [item[2] for item in get_system_info_result_data]
+
+ process_system_observables__observable_array = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+ process_system_observables__observable_array = []
+ i = 0
+
+ for system in get_system_info_result_item_2:
+ data = system[0]
+ if data:
+ observable = {
+ "source": "Crowdstrike OAuth API",
+ "type": "Endpoint",
+ "activity_name": "System Collection",
+ "uid": data['device_id'],
+ "hostname": data['hostname'],
+ "status": get_system_info_result_item_0[i],
+ "status_detail": get_system_info_result_message[i],
+ "endpoint_artifacts": [
+ {
+ "agents": [
+ {
+ "type": "Endpoint Detection and Response",
+ "type_id": 1,
+ "uid": data['cid'],
+ "vendor_name": "Crowdstrike",
+ "version": data['agent_version']
+ }
+ ],
+ "ip": data['local_ip'],
+ "external_ip": data['external_ip'],
+ "mac": data['mac_address'],
+ "domain": data['machine_domain'],
+ "type": data['product_type_desc'],
+ "last_seen": data['last_seen'],
+ "last_reboot": data['last_reboot'],
+ "last_login_user_sid": data.get('last_login_user_sid', ''),
+ "last_login_timestamp": data.get('last_login_timestamp', ''),
+ "operating_system": {
+ "build:": data['os_build'],
+ "kernel_release": data['kernel_version'],
+ "name": data['os_product_name'],
+ "type": data['platform_name'],
+ "version": data['os_version']
+ },
+ "hw_info": {
+ "bios_manufacturer": data['bios_manufacturer'],
+ "bios_version": data['bios_version'],
+ "serial_number": data['serial_number'],
+ "chassis": data['chassis_type_desc']
+ }
+ }
+ ]
+ }
+
+ policies = []
+ for policy in data['policies']:
+ policies.append(
+ {
+ "name": policy['policy_type'],
+ "uid": policy['policy_id'],
+ "is_applied": policy['applied']
+ }
+ )
+ observable['endpoint_artifacts'][0]['agents'][0]['policies'] = policies
+
+ #phantom.debug(f"---> OBSERVABLE: {observable}")
+ process_system_observables__observable_array.append(observable)
+ i+=1
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="process_system_observables:observable_array", value=json.dumps(process_system_observables__observable_array))
+
+ return
+
+
+@phantom.playbook_block()
+def join_delete_session(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("join_delete_session() called")
+
+ if phantom.completed(action_names=["run_admin_command_ps", "run_admin_command_netstat", "run_admin_command_services"]):
+ # call connected block "delete_session"
+ delete_session(container=container, handle=handle)
+
+ return
+
+
+@phantom.playbook_block()
+def delete_session(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("delete_session() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Deletes the Real Time Response (RTR) session on CS Falcon.
+ ################################################################################
+
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.summary.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'delete_session' call
+ for create_session_result_item in create_session_result_data:
+ if create_session_result_item[0] is not None:
+ parameters.append({
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("delete session", parameters=parameters, name="delete_session", assets=["crowdstrike_oauth_api"])
+
+ return
+
+
+@phantom.playbook_block()
+def run_admin_command_services(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("run_admin_command_services() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Runs a set of Powershell commands to gather details about the services on the
+ # endpoint. This requires that the host response policy permits it on the CrowdStrike
+ # side.
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.data.*.resources.*.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'run_admin_command_services' call
+ for query_device_result_item in query_device_result_data:
+ for create_session_result_item in create_session_result_data:
+ if query_device_result_item[0] is not None and create_session_result_item[0] is not None:
+ parameters.append({
+ "data": "-Raw=```Get-Service | Select-Object -Property Name, DisplayName, Status, ServiceType, StartType | ConvertTo-Json```",
+ "command": "runscript",
+ "device_id": query_device_result_item[0],
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("run admin command", parameters=parameters, name="run_admin_command_services", assets=["crowdstrike_oauth_api"], callback=process_service_observables)
+
+ return
+
+
+@phantom.playbook_block()
+def process_service_observables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("process_service_observables() called")
+
+ ################################################################################
+ # Format a normalized output for each service
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.data.*.hostname"], action_results=results)
+ run_admin_command_services_result_data = phantom.collect2(container=container, datapath=["run_admin_command_services:action_result.data.*.resources","run_admin_command_services:action_result.status","run_admin_command_services:action_result.message"], action_results=results)
+
+ query_device_result_item_0 = [item[0] for item in query_device_result_data]
+ query_device_result_item_1 = [item[1] for item in query_device_result_data]
+ run_admin_command_services_result_item_0 = [item[0] for item in run_admin_command_services_result_data]
+ run_admin_command_services_result_item_1 = [item[1] for item in run_admin_command_services_result_data]
+ run_admin_command_services_result_message = [item[2] for item in run_admin_command_services_result_data]
+
+ process_service_observables__observable_array = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ import json
+
+ # Ref: https://learn.microsoft.com/en-us/dotnet/api/system.serviceprocess.servicestartmode?view=net-9.0-pp
+ ocsf_start_type_mapping = {
+ "0": { "id": "1", "type": "Boot" }, # Boot -> Boot
+ "1": { "id": "2", "type": "System" }, # System -> System
+ "2": { "id": "3", "type": "Auto" }, # Automatic -> Auto
+ "3": { "id": "4", "type": "Demand" }, # Manual -> Demand
+ "4": { "id": "5", "type": "Disabled" } # Disabled -> Disabled
+ }
+
+ # Ref: https://learn.microsoft.com/en-us/dotnet/api/system.serviceprocess.servicetype?view=net-9.0-pp
+ ocsf_service_type_mapping = {
+ # Unused for now. OCSF doesn't seem to properly support BITWISE ENUMs yet.
+ # See "service_type_id" at https://schema.ocsf.io/1.4.0/objects/win/win_service
+ }
+
+ process_service_observables__observable_array = []
+ for device_id, hostname, services, status, messsage in zip(query_device_result_item_0, query_device_result_item_1, run_admin_command_services_result_item_0, run_admin_command_services_result_item_1, run_admin_command_services_result_message):
+
+ observable = {
+ "source": "Crowdstrike OAuth API",
+ "type": "Endpoint",
+ "activity_name": "Services Collection",
+ "uid": device_id,
+ "hostname": hostname,
+ "status": status,
+ "status_detail": messsage,
+ "service_artifacts": []
+ }
+
+ if services[0] and services[0]["stdout"]:
+ services_json = json.loads(services[0]["stdout"])
+ for service in services_json:
+ start_type = ocsf_start_type_mapping.get(str(service["StartType"]))
+
+ observable["service_artifacts"].append({
+ "type_id": 3,
+ "type": "Service",
+ "name": service["Name"],
+ "display_name": service["DisplayName"],
+ "service_start_type_id": start_type["id"],
+ "service_start_type": start_type["type"],
+ "service_type_id": service["ServiceType"], # No mapping to OCSF yet, see comment above.
+ })
+
+ process_service_observables__observable_array.append(observable)
+
+ #phantom.debug(process_service_observables__observable_array)
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="process_service_observables:observable_array", value=json.dumps(process_service_observables__observable_array))
+
+ join_delete_session(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def on_finish(container, summary):
+ phantom.debug("on_finish() called")
+
+ process_process_observables__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="process_process_observables:observable_array")) != "" else "null") # pylint: disable=used-before-assignment
+ process_network_observables__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="process_network_observables:observable_array")) != "" else "null") # pylint: disable=used-before-assignment
+ process_system_observables__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="process_system_observables:observable_array")) != "" else "null") # pylint: disable=used-before-assignment
+ process_service_observables__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="process_service_observables:observable_array")) != "" else "null") # pylint: disable=used-before-assignment
+
+ output = {
+ "process_observable": process_process_observables__observable_array,
+ "network_observable": process_network_observables__observable_array,
+ "endpoint_observable": process_system_observables__observable_array,
+ "service_observable": process_service_observables__observable_array,
+ }
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_playbook_output_data(output=output)
+
+ return
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_Endpoint_Analysis.yml b/playbooks/CrowdStrike_OAuth_API_Endpoint_Analysis.yml
new file mode 100644
index 0000000000..c1adce850f
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Endpoint_Analysis.yml
@@ -0,0 +1,34 @@
+name: CrowdStrike OAuth API Endpoint Analysis
+id: 1356baeb-9ad4-4d2c-b6ae-55dda6bd9db5
+version: 1
+date: '2025-06-09'
+author: Christian Cloutier, Splunk
+type: Investigation
+description: "Accepts a hostname or device id as input and collects running processes, network connections and various system information from the device via Crowdstrike. We then generate an observable report for each. This can be customized based on user preference."
+playbook: CrowdStrike_OAuth_API_Endpoint_Analysis
+how_to_implement: This input playbook requires the CrowdStrike OAuth API connector to be configured. It is designed to work with an endpoint hostname or device id and collect key information about the system, network connections and running processes for use in automation playbooks.
+references: []
+app_list:
+ - CrowdStrike OAuth API
+tags:
+ platform_tags:
+ - "host name"
+ - "device id"
+ - "enrichment"
+ - "D3-NTA"
+ - "D3-PA"
+ - "D3-AI"
+ - "CrowdStrike_OAuth_API"
+ playbook_type: Input
+ vpe_type: Modern
+ playbook_fields: [device]
+ product:
+ - Splunk SOAR
+ use_cases:
+ - Enrichment
+ - Malware
+ - Endpoint
+ defend_technique_id:
+ - D3-NTA
+ - D3-PA
+ - D3-AI
diff --git a/playbooks/CrowdStrike_OAuth_API_Executable_Denylisting.json b/playbooks/CrowdStrike_OAuth_API_Executable_Denylisting.json
new file mode 100644
index 0000000000..9e94bdc1ae
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Executable_Denylisting.json
@@ -0,0 +1,407 @@
+{
+ "blockly": false,
+ "blockly_xml": "",
+ "category": "Executable Denylisting",
+ "coa": {
+ "data": {
+ "description": "Accepts a hostname or device id as well as a file hash as input and add an indicator (IOC) for a device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference.",
+ "edges": [
+ {
+ "id": "port_0_to_port_2",
+ "sourceNode": "0",
+ "sourcePort": "0_out",
+ "targetNode": "2",
+ "targetPort": "2_in"
+ },
+ {
+ "id": "port_5_to_port_1",
+ "sourceNode": "5",
+ "sourcePort": "5_out",
+ "targetNode": "1",
+ "targetPort": "1_in"
+ },
+ {
+ "conditions": [
+ {
+ "index": 0
+ }
+ ],
+ "id": "port_2_to_port_7",
+ "sourceNode": "2",
+ "sourcePort": "2_out",
+ "targetNode": "7",
+ "targetPort": "7_in"
+ },
+ {
+ "id": "port_7_to_port_8",
+ "sourceNode": "7",
+ "sourcePort": "7_out",
+ "targetNode": "8",
+ "targetPort": "8_in"
+ },
+ {
+ "id": "port_9_to_port_5",
+ "sourceNode": "9",
+ "sourcePort": "9_out",
+ "targetNode": "5",
+ "targetPort": "5_in"
+ },
+ {
+ "id": "port_8_to_port_18",
+ "sourceNode": "8",
+ "sourcePort": "8_out",
+ "targetNode": "18",
+ "targetPort": "18_in"
+ },
+ {
+ "id": "port_18_to_port_9",
+ "sourceNode": "18",
+ "sourcePort": "18_out",
+ "targetNode": "9",
+ "targetPort": "9_in"
+ }
+ ],
+ "hash": "7505d40f9e5d889d81302b51e4d2f10f2d245c5f",
+ "nodes": {
+ "0": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_start",
+ "id": "0",
+ "type": "start"
+ },
+ "errors": {},
+ "id": "0",
+ "type": "start",
+ "warnings": {},
+ "x": 19.999999999999986,
+ "y": -1.9184653865522705e-13
+ },
+ "1": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_finish",
+ "id": "1",
+ "type": "end"
+ },
+ "errors": {},
+ "id": "1",
+ "type": "end",
+ "warnings": {},
+ "x": 19.999999999999986,
+ "y": 1196
+ },
+ "18": {
+ "data": {
+ "action": "upload indicator",
+ "actionType": "contain",
+ "advanced": {
+ "customName": "upload indicator",
+ "customNameId": 0,
+ "description": "Upload indicator that we want CrowdStrike to prevent and watch for all platforms.",
+ "join": [],
+ "note": "Upload indicator that we want CrowdStrike to prevent and watch for all platforms."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "upload_indicator",
+ "id": "18",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "action": "prevent",
+ "description": "File Indicator blocked from Splunk SOAR",
+ "ioc": "filtered-data:input_filter:condition_1:playbook_input:hash",
+ "platforms": "linux,mac,windows",
+ "severity": "MEDIUM",
+ "source": "IOC uploaded via Splunk SOAR"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "ioc"
+ },
+ {
+ "data_type": "string",
+ "field": "action"
+ },
+ {
+ "data_type": "string",
+ "default": "IOC uploaded via Splunk SOAR",
+ "field": "source"
+ },
+ {
+ "data_type": "string",
+ "field": "platforms"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "18",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 680
+ },
+ "2": {
+ "data": {
+ "advanced": {
+ "customName": "input filter",
+ "customNameId": 0,
+ "description": "Determines if the provided inputs are present in the dataset.",
+ "join": [],
+ "note": "Determines if the provided inputs are present in the dataset."
+ },
+ "conditions": [
+ {
+ "comparisons": [
+ {
+ "conditionIndex": 0,
+ "op": "!=",
+ "param": "playbook_input:device",
+ "value": ""
+ },
+ {
+ "conditionIndex": 0,
+ "op": "!=",
+ "param": "playbook_input:hash",
+ "value": ""
+ }
+ ],
+ "conditionIndex": 0,
+ "customName": "input device and hash present",
+ "logic": "and"
+ }
+ ],
+ "functionId": 1,
+ "functionName": "input_filter",
+ "id": "2",
+ "type": "filter"
+ },
+ "errors": {},
+ "id": "2",
+ "type": "filter",
+ "warnings": {},
+ "x": 60,
+ "y": 148
+ },
+ "5": {
+ "customCode": null,
+ "data": {
+ "advanced": {
+ "customName": "file observables",
+ "customNameId": 0,
+ "description": "Format a normalized output for each host",
+ "join": [],
+ "note": "Format a normalized output for each host."
+ },
+ "functionId": 1,
+ "functionName": "file_observables",
+ "id": "5",
+ "inputParameters": [
+ "query_device:action_result.data.*.device_id",
+ "query_device:action_result.data.*.hostname",
+ "filtered-data:input_filter:condition_1:playbook_input:hash",
+ "upload_indicator:action_result.status",
+ "upload_indicator:action_result.message"
+ ],
+ "outputVariables": [
+ "observable_array"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "5",
+ "type": "code",
+ "userCode": " \n file_observables__observable_array = []\n \n for device_id, hostname, file_hash, status, status_message in zip(query_device_result_item_0, query_device_result_item_1, filtered_input_0_hash_values, upload_indicator_result_item_0, upload_indicator_result_message):\n # Initialize the observable dictionary\n observable = {\n \"source\": \"Crowdstrike OAuth API\",\n \"type\": \"Endpoint\",\n \"activity_name\": \"File Execution Prevention\",\n \"uid\": device_id,\n \"hostname\": hostname,\n \"status\": status,\n \"status_detail\": status_message,\n \"file\": {\n \"hashes\": [\n {\n \"algorithm\": \"SHA-256\",\n \"algorithm_id\": 3,\n \"value\": file_hash \n }\n ]\n },\n \"d3fend\": {\n \"d3f_tactic\": \"Isolate\",\n \"d3f_technique\": \"D3-EDL\",\n \"version\": \"1.0.0\"\n }\n } \n\n # Add the observable to the array\n file_observables__observable_array.append(observable)\n \n # Debug output for verification\n phantom.debug(file_observables__observable_array)\n \n",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1020
+ },
+ "7": {
+ "data": {
+ "advanced": {
+ "customName": "format fql",
+ "customNameId": 0,
+ "description": "Format the FQL query to get the input device information using its ID or hostname.",
+ "join": [],
+ "note": "Format the FQL query to get the input device information using its ID or hostname."
+ },
+ "functionId": 2,
+ "functionName": "format_fql",
+ "id": "7",
+ "parameters": [
+ "playbook_input:device"
+ ],
+ "template": "%%\nhostname:['{0}'],device_id:['{0}']\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "7",
+ "type": "format",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 320
+ },
+ "8": {
+ "data": {
+ "action": "query device",
+ "actionType": "investigate",
+ "advanced": {
+ "customName": "query device",
+ "customNameId": 0,
+ "description": "Get information about the device to unquarantine using its hostname or device id.",
+ "join": [],
+ "note": "Get information about the device to unquarantine using its hostname or device id."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "query_device",
+ "id": "8",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "filter": "format_fql:formatted_data.*",
+ "limit": 50
+ },
+ "requiredParameters": [
+ {
+ "data_type": "numeric",
+ "default": 50,
+ "field": "limit"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "8",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 504
+ },
+ "9": {
+ "data": {
+ "advanced": {
+ "customName": "format executable denylisting report",
+ "customNameId": 0,
+ "description": "Format a summary table with the information gathered from the playbook.",
+ "join": [],
+ "note": "Format a summary table with the information gathered from the playbook."
+ },
+ "functionId": 3,
+ "functionName": "format_executable_denylisting_report",
+ "id": "9",
+ "parameters": [
+ "query_device:action_result.data.*.device_id",
+ "filtered-data:input_filter:condition_1:playbook_input:hash",
+ "upload_indicator:action_result.parameter.action",
+ "upload_indicator:action_result.status",
+ "upload_indicator:action_result.message"
+ ],
+ "template": "Endpoint Files were denylisted by Splunk SOAR. The table below summarizes the information gathered.\n\n| Device ID | Executable Hash | Action | Denylisting Status | Message |\n| --- | --- | --- | --- | --- |\n%%\n| {0} | {1} | {2} | {3} | {4} |\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "9",
+ "type": "format",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 828
+ }
+ },
+ "notes": "Inputs: \ndevice (CrowdStrike Device ID or Hostname)\nhash (SHA-256 File hash)\nInteractions: CrowdStrike OAuth API\nActions: query device, upload indicator\nOutputs: observables, markdown report",
+ "origin": {
+ "playbook_id": 232,
+ "playbook_name": "CrowdStrike_OAuth_API_File_Eviction",
+ "playbook_repo_id": 2,
+ "playbook_repo_name": "local"
+ }
+ },
+ "input_spec": [
+ {
+ "contains": [
+ "host name"
+ ],
+ "description": "Device ID or hostname of the host to deny a file on",
+ "name": "device"
+ },
+ {
+ "contains": [
+ "sha256"
+ ],
+ "description": "Hash of the executable file on the endpoint to deny",
+ "name": "hash"
+ }
+ ],
+ "output_spec": [
+ {
+ "contains": [],
+ "datapaths": [
+ "file_observables:custom_function:observable_array"
+ ],
+ "deduplicate": false,
+ "description": "An array of observable dictionaries",
+ "metadata": {},
+ "name": "observable"
+ },
+ {
+ "contains": [],
+ "datapaths": [
+ "format_executable_denylisting_report:formatted_data"
+ ],
+ "deduplicate": false,
+ "description": "A report of the devices that were isolated via Splunk SOAR.",
+ "metadata": {},
+ "name": "markdown_report"
+ }
+ ],
+ "playbook_trigger": "artifact_created",
+ "playbook_type": "data",
+ "python_version": "3",
+ "schema": "5.0.15",
+ "version": "6.3.1.178"
+ },
+ "create_time": "2025-04-09T12:54:45.902287+00:00",
+ "draft_mode": false,
+ "labels": [
+ "*"
+ ],
+ "tags": [
+ "CrowdStrike_OAuth_API",
+ "host name",
+ "D3-EDL",
+ "file_hash",
+ "response_option"
+ ]
+}
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_Executable_Denylisting.png b/playbooks/CrowdStrike_OAuth_API_Executable_Denylisting.png
new file mode 100644
index 0000000000..95a089c0ac
Binary files /dev/null and b/playbooks/CrowdStrike_OAuth_API_Executable_Denylisting.png differ
diff --git a/playbooks/CrowdStrike_OAuth_API_Executable_Denylisting.py b/playbooks/CrowdStrike_OAuth_API_Executable_Denylisting.py
new file mode 100644
index 0000000000..0b1757d3fa
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Executable_Denylisting.py
@@ -0,0 +1,283 @@
+"""
+Accepts a hostname or device id as well as a file hash as input and add an indicator (IOC) for a device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference.
+"""
+
+
+import phantom.rules as phantom
+import json
+from datetime import datetime, timedelta
+
+
+@phantom.playbook_block()
+def on_start(container):
+ phantom.debug('on_start() called')
+
+ # call 'input_filter' block
+ input_filter(container=container)
+
+ return
+
+@phantom.playbook_block()
+def input_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("input_filter() called")
+
+ ################################################################################
+ # Determines if the provided inputs are present in the dataset.
+ ################################################################################
+
+ # collect filtered artifact ids and results for 'if' condition 1
+ matched_artifacts_1, matched_results_1 = phantom.condition(
+ container=container,
+ logical_operator="and",
+ conditions=[
+ ["playbook_input:device", "!=", ""],
+ ["playbook_input:hash", "!=", ""]
+ ],
+ name="input_filter:condition_1",
+ delimiter=None)
+
+ # call connected blocks if filtered artifacts or results
+ if matched_artifacts_1 or matched_results_1:
+ format_fql(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_1, filtered_results=matched_results_1)
+
+ return
+
+
+@phantom.playbook_block()
+def file_observables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("file_observables() called")
+
+ ################################################################################
+ # Format a normalized output for each host
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.data.*.hostname"], action_results=results)
+ filtered_input_0_hash = phantom.collect2(container=container, datapath=["filtered-data:input_filter:condition_1:playbook_input:hash"])
+ upload_indicator_result_data = phantom.collect2(container=container, datapath=["upload_indicator:action_result.status","upload_indicator:action_result.message"], action_results=results)
+
+ query_device_result_item_0 = [item[0] for item in query_device_result_data]
+ query_device_result_item_1 = [item[1] for item in query_device_result_data]
+ filtered_input_0_hash_values = [item[0] for item in filtered_input_0_hash]
+ upload_indicator_result_item_0 = [item[0] for item in upload_indicator_result_data]
+ upload_indicator_result_message = [item[1] for item in upload_indicator_result_data]
+
+ file_observables__observable_array = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ file_observables__observable_array = []
+
+ for device_id, hostname, file_hash, status, status_message in zip(query_device_result_item_0, query_device_result_item_1, filtered_input_0_hash_values, upload_indicator_result_item_0, upload_indicator_result_message):
+ # Initialize the observable dictionary
+ observable = {
+ "source": "Crowdstrike OAuth API",
+ "type": "Endpoint",
+ "activity_name": "File Execution Prevention",
+ "uid": device_id,
+ "hostname": hostname,
+ "status": status,
+ "status_detail": status_message,
+ "file": {
+ "hashes": [
+ {
+ "algorithm": "SHA-256",
+ "algorithm_id": 3,
+ "value": file_hash
+ }
+ ]
+ },
+ "d3fend": {
+ "d3f_tactic": "Isolate",
+ "d3f_technique": "D3-EDL",
+ "version": "1.0.0"
+ }
+ }
+
+ # Add the observable to the array
+ file_observables__observable_array.append(observable)
+
+ # Debug output for verification
+ phantom.debug(file_observables__observable_array)
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="file_observables:observable_array", value=json.dumps(file_observables__observable_array))
+
+ return
+
+
+@phantom.playbook_block()
+def format_fql(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_fql() called")
+
+ ################################################################################
+ # Format the FQL query to get the input device information using its ID or hostname.
+ ################################################################################
+
+ template = """%%\nhostname:['{0}'],device_id:['{0}']\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "playbook_input:device"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_fql")
+
+ query_device(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def query_device(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("query_device() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Get information about the device to unquarantine using its hostname or device
+ # id.
+ ################################################################################
+
+ format_fql__as_list = phantom.get_format_data(name="format_fql__as_list")
+
+ parameters = []
+
+ # build parameters list for 'query_device' call
+ for format_fql__item in format_fql__as_list:
+ parameters.append({
+ "limit": 50,
+ "filter": format_fql__item,
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("query device", parameters=parameters, name="query_device", assets=["crowdstrike_oauth_api"], callback=upload_indicator)
+
+ return
+
+
+@phantom.playbook_block()
+def format_executable_denylisting_report(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_executable_denylisting_report() called")
+
+ ################################################################################
+ # Format a summary table with the information gathered from the playbook.
+ ################################################################################
+
+ template = """Endpoint Files were denylisted by Splunk SOAR. The table below summarizes the information gathered.\n\n| Device ID | Executable Hash | Action | Denylisting Status | Message |\n| --- | --- | --- | --- | --- |\n%%\n| {0} | {1} | {2} | {3} | {4} |\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "query_device:action_result.data.*.device_id",
+ "filtered-data:input_filter:condition_1:playbook_input:hash",
+ "upload_indicator:action_result.parameter.action",
+ "upload_indicator:action_result.status",
+ "upload_indicator:action_result.message"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_executable_denylisting_report")
+
+ file_observables(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def upload_indicator(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("upload_indicator() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Upload indicator that we want CrowdStrike to prevent and watch for all platforms.
+ ################################################################################
+
+ filtered_input_0_hash = phantom.collect2(container=container, datapath=["filtered-data:input_filter:condition_1:playbook_input:hash"])
+
+ parameters = []
+
+ # build parameters list for 'upload_indicator' call
+ for filtered_input_0_hash_item in filtered_input_0_hash:
+ if filtered_input_0_hash_item[0] is not None:
+ parameters.append({
+ "ioc": filtered_input_0_hash_item[0],
+ "action": "prevent",
+ "source": "IOC uploaded via Splunk SOAR",
+ "severity": "MEDIUM",
+ "platforms": "linux,mac,windows",
+ "description": "File Indicator blocked from Splunk SOAR",
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("upload indicator", parameters=parameters, name="upload_indicator", assets=["crowdstrike_oauth_api"], callback=format_executable_denylisting_report)
+
+ return
+
+
+@phantom.playbook_block()
+def on_finish(container, summary):
+ phantom.debug("on_finish() called")
+
+ format_executable_denylisting_report = phantom.get_format_data(name="format_executable_denylisting_report")
+ file_observables__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="file_observables:observable_array")) != "" else "null") # pylint: disable=used-before-assignment
+
+ output = {
+ "observable": file_observables__observable_array,
+ "markdown_report": format_executable_denylisting_report,
+ }
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_playbook_output_data(output=output)
+
+ return
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_Executable_Denylisting.yml b/playbooks/CrowdStrike_OAuth_API_Executable_Denylisting.yml
new file mode 100644
index 0000000000..0a985541e8
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Executable_Denylisting.yml
@@ -0,0 +1,31 @@
+name: CrowdStrike OAuth API Executable Denylisting
+id: 9d87f2d5-2578-4f39-9eee-c1a88af658bb
+version: 1
+date: '2025-06-09'
+author: Christian Cloutier, Splunk
+type: Response
+description: "Accepts a hostname or device id as well as a file hash as input and add an indicator (IOC) for a device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference."
+playbook: CrowdStrike_OAuth_API_Executable_Denylisting
+how_to_implement: This input playbook requires the CrowdStrike OAuth API connector to be configured. It is designed to work with an endpoint hostname or device id and create an indicator in CrowdStrike Falcon based on the malicious process hash value (preventing it from running on other endpoints) for use in automation playbooks.
+references: []
+app_list:
+ - CrowdStrike OAuth API
+tags:
+ platform_tags:
+ - "host name"
+ - "device id"
+ - "file_hash"
+ - "Executable Denylisting"
+ - "D3-EDL"
+ - "CrowdStrike_OAuth_API"
+ playbook_type: Input
+ vpe_type: Modern
+ playbook_fields: [device,file_hash]
+ product:
+ - Splunk SOAR
+ use_cases:
+ - Response
+ - Malware
+ - Endpoint
+ defend_technique_id:
+ - D3-EDL
diff --git a/playbooks/CrowdStrike_OAuth_API_File_Collection.json b/playbooks/CrowdStrike_OAuth_API_File_Collection.json
new file mode 100644
index 0000000000..b14acec4d3
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_File_Collection.json
@@ -0,0 +1,880 @@
+{
+ "blockly": false,
+ "blockly_xml": "",
+ "category": "File Collection",
+ "coa": {
+ "data": {
+ "description": "Accepts a hostname or device id as well as a file path as input and collects the file to the event File Vault from a device in Crowdstrike. An artifact is created from the collected file. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference.",
+ "edges": [
+ {
+ "id": "port_0_to_port_2",
+ "sourceNode": "0",
+ "sourcePort": "0_out",
+ "targetNode": "2",
+ "targetPort": "2_in"
+ },
+ {
+ "id": "port_5_to_port_1",
+ "sourceNode": "5",
+ "sourcePort": "5_out",
+ "targetNode": "1",
+ "targetPort": "1_in"
+ },
+ {
+ "conditions": [
+ {
+ "index": 0
+ }
+ ],
+ "id": "port_2_to_port_7",
+ "sourceNode": "2",
+ "sourcePort": "2_out",
+ "targetNode": "7",
+ "targetPort": "7_in"
+ },
+ {
+ "id": "port_7_to_port_8",
+ "sourceNode": "7",
+ "sourcePort": "7_out",
+ "targetNode": "8",
+ "targetPort": "8_in"
+ },
+ {
+ "id": "port_9_to_port_5",
+ "sourceNode": "9",
+ "sourcePort": "9_out",
+ "targetNode": "5",
+ "targetPort": "5_in"
+ },
+ {
+ "id": "port_8_to_port_11",
+ "sourceNode": "8",
+ "sourcePort": "8_out",
+ "targetNode": "11",
+ "targetPort": "11_in"
+ },
+ {
+ "id": "port_11_to_port_12",
+ "sourceNode": "11",
+ "sourcePort": "11_out",
+ "targetNode": "12",
+ "targetPort": "12_in"
+ },
+ {
+ "id": "port_13_to_port_14",
+ "sourceNode": "13",
+ "sourcePort": "13_out",
+ "targetNode": "14",
+ "targetPort": "14_in"
+ },
+ {
+ "id": "port_15_to_port_9",
+ "sourceNode": "15",
+ "sourcePort": "15_out",
+ "targetNode": "9",
+ "targetPort": "9_in"
+ },
+ {
+ "id": "port_14_to_port_18",
+ "sourceNode": "14",
+ "sourcePort": "14_out",
+ "targetNode": "18",
+ "targetPort": "18_in"
+ },
+ {
+ "id": "port_18_to_port_19",
+ "sourceNode": "18",
+ "sourcePort": "18_out",
+ "targetNode": "19",
+ "targetPort": "19_in"
+ },
+ {
+ "id": "port_19_to_port_15",
+ "sourceNode": "19",
+ "sourcePort": "19_out",
+ "targetNode": "15",
+ "targetPort": "15_in"
+ },
+ {
+ "id": "port_12_to_port_21",
+ "sourceNode": "12",
+ "sourcePort": "12_out",
+ "targetNode": "21",
+ "targetPort": "21_in"
+ },
+ {
+ "id": "port_21_to_port_13",
+ "sourceNode": "21",
+ "sourcePort": "21_out",
+ "targetNode": "13",
+ "targetPort": "13_in"
+ }
+ ],
+ "hash": "73a7f39d0ed6036fcc911933d66ca06fd300d55b",
+ "nodes": {
+ "0": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_start",
+ "id": "0",
+ "type": "start"
+ },
+ "errors": {},
+ "id": "0",
+ "type": "start",
+ "warnings": {},
+ "x": 19.999999999999986,
+ "y": -1.2789769243681803e-13
+ },
+ "1": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_finish",
+ "id": "1",
+ "type": "end"
+ },
+ "errors": {},
+ "id": "1",
+ "type": "end",
+ "warnings": {},
+ "x": 19.999999999999986,
+ "y": 2456
+ },
+ "11": {
+ "data": {
+ "action": "create session",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "create session",
+ "customNameId": 0,
+ "description": "Creates a Real Time Response (RTR) session to interact with the endpoint",
+ "join": [],
+ "note": "Creates a Real Time Response (RTR) session to interact with the endpoint"
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "create_session",
+ "id": "11",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "device_id": "query_device:action_result.data.*.device_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "device_id"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "11",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 680
+ },
+ "12": {
+ "data": {
+ "action": "run admin command",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "run admin command get",
+ "customNameId": 0,
+ "description": "Gets the specified file based on the playbook input path",
+ "join": [],
+ "note": "Gets the specified file based on the playbook input path"
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "run_admin_command_get",
+ "id": "12",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "command": "get",
+ "data": "playbook_input:path",
+ "device_id": "query_device:action_result.data.*.device_id",
+ "session_id": "create_session:action_result.summary.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "command"
+ },
+ {
+ "data_type": "string",
+ "field": "device_id"
+ },
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "12",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 860
+ },
+ "13": {
+ "data": {
+ "action": "list session files",
+ "actionType": "investigate",
+ "advanced": {
+ "customName": "list session files",
+ "customNameId": 0,
+ "description": "Lists the previous file we ran a get command on, which returns its SHA256 hash",
+ "join": [],
+ "note": "Lists the previous file we ran a get command on, which returns its SHA256 hash"
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "list_session_files",
+ "id": "13",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "session_id": "create_session:action_result.summary.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "13",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1208
+ },
+ "14": {
+ "data": {
+ "action": "get session file",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "get session file",
+ "customNameId": 0,
+ "description": "Downloads the RTR session file from the endpoint to the SOAR File Vault",
+ "join": [],
+ "note": "Downloads the RTR session file from the endpoint to the SOAR File Vault"
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "get_session_file",
+ "id": "14",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "file_hash": "list_session_files:action_result.data.*.resources.*.sha256",
+ "file_name": "playbook_input:path",
+ "session_id": "create_session:action_result.summary.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "file_hash"
+ },
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "14",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1384
+ },
+ "15": {
+ "data": {
+ "action": "delete session",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "delete session",
+ "customNameId": 0,
+ "description": "Closes a Real Time Response (RTR) session with the endpoint",
+ "join": [],
+ "notRequiredJoins": [],
+ "note": "Closes a Real Time Response (RTR) session with the endpoint"
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "delete_session",
+ "id": "15",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "session_id": "create_session:action_result.summary.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "15",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1912
+ },
+ "18": {
+ "data": {
+ "advanced": {
+ "customName": "create file artifact",
+ "customNameId": 0,
+ "description": "Create the JSON structure from the collected file metadata to create a SOAR artifact.",
+ "join": [],
+ "note": "Create the JSON structure from the collected file metadata to create a SOAR artifact."
+ },
+ "functionId": 2,
+ "functionName": "create_file_artifact",
+ "id": "18",
+ "inputParameters": [
+ "query_device:action_result.data.*.device_id",
+ "query_device:action_result.data.*.hostname",
+ "list_session_files:action_result.data.*.resources.*.name",
+ "list_session_files:action_result.data.*.resources.*.sha256",
+ "get_session_file:action_result.data.*.vault_id",
+ "run_admin_command_get:action_result.parameter.data"
+ ],
+ "outputVariables": [
+ "data"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "18",
+ "type": "code",
+ "userCode": " import os\n \n create_file_artifact__data = []\n for device_id, hostname, origFilepath, filehash, vault_id, fullpath in zip(query_device_result_item_0, query_device_result_item_1, list_session_files_result_item_0, list_session_files_result_item_1, get_session_file_result_item_0, run_admin_command_get_parameter_data):\n filename = os.path.basename(fullpath.replace('\\\\','/'))\n filepath = os.path.dirname(fullpath.replace('\\\\','/'))\n create_file_artifact__data = {\n \"cef\": {\n \"deviceExternalId\": device_id,\n \"hostname\": hostname,\n \"origFilePath\": origFilepath,\n \"fileName\": filename,\n \"filePath\": os.path.join(filepath.replace('/','\\\\'), ''),\n \"fileHashSha256\": filehash,\n \"vaultId\": vault_id\n }\n }\n\n #phantom.debug(f\"File artifact data: {create_file_artifact__data}\")\n \n",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1560
+ },
+ "19": {
+ "data": {
+ "advanced": {
+ "customName": "artifact create",
+ "customNameId": 0,
+ "description": "Creates a new artifact to store information about the file collected from Crowdstrike.",
+ "join": [],
+ "note": "Creates a new artifact to store information about the file collected from Crowdstrike."
+ },
+ "customFunction": {
+ "draftMode": false,
+ "name": "artifact_create",
+ "repoName": "community"
+ },
+ "functionId": 2,
+ "functionName": "artifact_create",
+ "id": "19",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "selectMore": false,
+ "type": "utility",
+ "utilities": {
+ "artifact_create": {
+ "description": "Create a new artifact with the specified attributes. Supports all fields available in /rest/artifact. Add any unlisted inputs as dictionary keys in input_json. Unsupported keys will automatically be dropped.",
+ "fields": [
+ {
+ "dataTypes": [
+ "phantom container id"
+ ],
+ "description": "Container which the artifact will be added to.",
+ "inputType": "item",
+ "label": "container",
+ "name": "container",
+ "placeholder": "container:id",
+ "renderType": "datapath",
+ "required": false
+ },
+ {
+ "dataTypes": [],
+ "description": "The name of the new artifact, which is optional and defaults to \"artifact\".",
+ "inputType": "item",
+ "label": "name",
+ "name": "name",
+ "placeholder": "artifact",
+ "renderType": "datapath",
+ "required": false
+ },
+ {
+ "dataTypes": [],
+ "description": "The label of the new artifact, which is optional and defaults to \"events\"",
+ "inputType": "item",
+ "label": "label",
+ "name": "label",
+ "placeholder": "events",
+ "renderType": "datapath",
+ "required": false
+ },
+ {
+ "dataTypes": [
+ ""
+ ],
+ "description": "The severity of the new artifact, which is optional and defaults to \"Medium\". Typically this is either \"High\", \"Medium\", or \"Low\".",
+ "inputType": "item",
+ "label": "severity",
+ "name": "severity",
+ "placeholder": "Medium",
+ "renderType": "datapath",
+ "required": false
+ },
+ {
+ "dataTypes": [],
+ "description": "The name of the CEF field to populate in the artifact, such as \"destinationAddress\" or \"sourceDnsDomain\". Required only if cef_value is provided.",
+ "inputType": "item",
+ "label": "cef_field",
+ "name": "cef_field",
+ "placeholder": "destinationAddress",
+ "renderType": "datapath",
+ "required": false
+ },
+ {
+ "dataTypes": [
+ "*"
+ ],
+ "description": "The value of the CEF field to populate in the artifact, such as the IP address, domain name, or file hash. Required only if cef_field is provided.",
+ "inputType": "item",
+ "label": "cef_value",
+ "name": "cef_value",
+ "placeholder": "192.0.2.192",
+ "renderType": "datapath",
+ "required": false
+ },
+ {
+ "dataTypes": [],
+ "description": "The CEF data type of the data in cef_value. For example, this could be \"ip\", \"hash\", or \"domain\". Optional.",
+ "inputType": "item",
+ "label": "cef_data_type",
+ "name": "cef_data_type",
+ "placeholder": "ip",
+ "renderType": "datapath",
+ "required": false
+ },
+ {
+ "dataTypes": [],
+ "description": "A comma-separated list of tags to apply to the created artifact, which is optional.",
+ "inputType": "item",
+ "label": "tags",
+ "name": "tags",
+ "placeholder": "tag1, tag2, tag3",
+ "renderType": "datapath",
+ "required": false
+ },
+ {
+ "dataTypes": [],
+ "description": "Either \"true\" or \"false\", depending on whether or not the new artifact should trigger the execution of any playbooks that are set to active on the label of the container the artifact will be added to. Optional and defaults to \"false\".",
+ "inputType": "item",
+ "label": "run_automation",
+ "name": "run_automation",
+ "placeholder": "false",
+ "renderType": "datapath",
+ "required": false
+ },
+ {
+ "dataTypes": [],
+ "description": "Optional parameter to modify any extra attributes of the artifact. Input_json will be merged with other inputs. In the event of a conflict, input_json will take precedence.",
+ "inputType": "item",
+ "label": "input_json",
+ "name": "input_json",
+ "placeholder": "{\"source_data_identifier\": \"1234\", \"data\": \"5678\"}",
+ "renderType": "datapath",
+ "required": false
+ }
+ ],
+ "label": "artifact_create",
+ "name": "artifact_create"
+ }
+ },
+ "utilityType": "custom_function",
+ "values": {
+ "artifact_create": {
+ "cef_data_type": null,
+ "cef_field": null,
+ "cef_value": null,
+ "container": "container:id",
+ "input_json": "create_file_artifact:custom_function:data",
+ "label": "collected_file",
+ "name": "Collected File Artifact",
+ "run_automation": null,
+ "severity": null,
+ "tags": null
+ }
+ }
+ },
+ "errors": {},
+ "id": "19",
+ "type": "utility",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1736
+ },
+ "2": {
+ "data": {
+ "advanced": {
+ "customName": "input filter",
+ "customNameId": 0,
+ "description": "Determines if the provided inputs are present in the dataset.",
+ "join": [],
+ "note": "Determines if the provided inputs are present in the dataset."
+ },
+ "conditions": [
+ {
+ "comparisons": [
+ {
+ "conditionIndex": 0,
+ "op": "!=",
+ "param": "playbook_input:device",
+ "value": ""
+ },
+ {
+ "conditionIndex": 0,
+ "op": "!=",
+ "param": "playbook_input:path",
+ "value": ""
+ }
+ ],
+ "conditionIndex": 0,
+ "customName": "input device and path present",
+ "logic": "and"
+ }
+ ],
+ "functionId": 1,
+ "functionName": "input_filter",
+ "id": "2",
+ "type": "filter"
+ },
+ "errors": {},
+ "id": "2",
+ "type": "filter",
+ "warnings": {},
+ "x": 60,
+ "y": 148
+ },
+ "21": {
+ "data": {
+ "advanced": {
+ "customName": "determine get status",
+ "customNameId": 0,
+ "description": "Check the results of the RTR get command to ensure it was indeed successful. RTR commands are notably unreliable in how they report status, instead proving stdout and stderr information.",
+ "join": [],
+ "note": "Check the results of the RTR get command to ensure it was indeed successful. RTR commands are notably unreliable in how they report status, instead proving stdout and stderr information."
+ },
+ "functionId": 3,
+ "functionName": "determine_get_status",
+ "id": "21",
+ "inputParameters": [
+ "run_admin_command_get:action_result.status",
+ "run_admin_command_get:action_result.data.*.resources.*.stdout",
+ "run_admin_command_get:action_result.data.*.resources.*.stderr",
+ "run_admin_command_get:action_result.parameter.data",
+ "run_admin_command_get:action_result.message"
+ ],
+ "outputVariables": [
+ "file_name",
+ "status",
+ "status_detail"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "21",
+ "type": "code",
+ "userCode": " \n determine_get_status__file_name = []\n determine_get_status__status = []\n determine_get_status__status_detail = []\n \n for file_name, status, stdout, stderr, message in zip(run_admin_command_get_parameter_data, run_admin_command_get_result_item_0, run_admin_command_get_result_item_1, run_admin_command_get_result_item_2,run_admin_command_get_result_message):\n #phantom.debug(f\"file_name: {file_name}, status: {status}, stdout: {stdout}, stderr: {stderr}\")\n determine_get_status__file_name.append(file_name)\n determine_get_status__status.append(status)\n if status == \"success\" and stdout.strip() and not stderr.strip():\n determine_get_status__status_detail.append(f\"File collected: {stdout.strip()}\")\n else:\n determine_get_status__status_detail.append(stderr.strip() if stderr else message)\n\n",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1032
+ },
+ "5": {
+ "customCode": null,
+ "data": {
+ "advanced": {
+ "customName": "host observables",
+ "customNameId": 0,
+ "description": "Format a normalized output for each host",
+ "join": [],
+ "note": "Format a normalized output for each host."
+ },
+ "functionId": 1,
+ "functionName": "host_observables",
+ "id": "5",
+ "inputParameters": [
+ "create_session:action_result.parameter.device_id",
+ "query_device:action_result.data.*.hostname",
+ "determine_get_status:custom_function:file_name",
+ "list_session_files:action_result.data.*.resources.*.sha256",
+ "list_session_files:action_result.data.*.resources.*.size",
+ "get_session_file:action_result.data.*.vault_document",
+ "get_session_file:action_result.summary.vault_id",
+ "determine_get_status:custom_function:status",
+ "determine_get_status:custom_function:status_detail"
+ ],
+ "outputVariables": [
+ "observable_array"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "5",
+ "type": "code",
+ "userCode": " \n host_observables__observable_array = []\n \n for device_id, hostname, file_hash, file_size, vault_document, vault_id in zip(create_session_parameter_device_id, query_device_result_item_0, list_session_files_result_item_0, list_session_files_result_item_1, get_session_file_result_item_0, get_session_file_summary_vault_id):\n file_artifacts = []\n for file_name, status, status_message in zip(determine_get_status__file_name, determine_get_status__status, determine_get_status__status_detail):\n # Initialize the observable dictionary \n if status == \"success\":\n file_artifacts.append({\n \"name\": file_name,\n \"status\": status,\n \"status_detail\": status_message,\n \"hashes\": [\n {\n \"value\": file_hash,\n \"algorithm\": \"SHA-256\",\n \"algorithm_id\": 3\n }\n ],\n \"size\": file_size,\n \"vault_document\": vault_document,\n \"vault_id\": vault_id\n })\n else:\n file_artifacts.append({\n \"name\": file_name,\n \"status\": status,\n \"status_detail\": status_message\n })\n observable = {\n \"source\": \"Crowdstrike OAuth API\", \n \"type\": \"Endpoint\", \n \"activity_name\": \"File Collection\",\n \"uid\": device_id,\n \"hostname\": hostname,\n \"file_artifacts\": file_artifacts\n }\n \n # Add the observable to the array\n host_observables__observable_array.append(observable)\n \n # Debug output for verification\n #phantom.debug(host_observables__observable_array)\n \n",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 2280
+ },
+ "7": {
+ "data": {
+ "advanced": {
+ "customName": "format fql",
+ "customNameId": 0,
+ "description": "Format the FQL query to get the input device information using its ID or hostname.",
+ "join": [],
+ "note": "Format the FQL query to get the input device information using its ID or hostname."
+ },
+ "functionId": 2,
+ "functionName": "format_fql",
+ "id": "7",
+ "parameters": [
+ "playbook_input:device"
+ ],
+ "template": "%%\nhostname:['{0}'],device_id:['{0}']\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "7",
+ "type": "format",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 328
+ },
+ "8": {
+ "data": {
+ "action": "query device",
+ "actionType": "investigate",
+ "advanced": {
+ "customName": "query device",
+ "customNameId": 0,
+ "description": "Get information about the device using its hostname or device id.",
+ "join": [],
+ "note": "Get information about the device using its hostname or device id."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "query_device",
+ "id": "8",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "filter": "format_fql:formatted_data.*",
+ "limit": 50
+ },
+ "requiredParameters": [
+ {
+ "data_type": "numeric",
+ "default": 50,
+ "field": "limit"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "8",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 500
+ },
+ "9": {
+ "data": {
+ "advanced": {
+ "customName": "format file collection report",
+ "customNameId": 0,
+ "description": "Format a summary table with the information gathered from the playbook.",
+ "join": [],
+ "note": "Format a summary table with the information gathered from the playbook."
+ },
+ "functionId": 3,
+ "functionName": "format_file_collection_report",
+ "id": "9",
+ "parameters": [
+ "create_session:action_result.parameter.device_id",
+ "query_device:action_result.data.*.hostname",
+ "list_session_files:action_result.data.*.resources.*.name",
+ "list_session_files:action_result.data.*.resources.*.sha256",
+ "get_session_file:action_result.summary.vault_id",
+ "artifact_create:custom_function_result.data.artifact_id",
+ "determine_get_status:custom_function:status",
+ "determine_get_status:custom_function:status_detail"
+ ],
+ "template": "Endpoint Files were collected by Splunk SOAR and an artifact was created. The table below summarizes the information gathered.\n\n| Device ID | Hostname | File Path | File Hash | Vault ID | Artifact ID | Status | Message |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n%%\n| {0} | {1} | `{2}` | {3} | {4} | {5} | {6} | `{7}` |\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "9",
+ "type": "format",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 2088
+ }
+ },
+ "notes": "Inputs: \ndevice (CrowdStrike Device ID or Hostname)\npath (File path)\nInteractions: CrowdStrike OAuth API\nActions: create session, run admin command, list session file, get session file\nOutputs: observables, markdown report"
+ },
+ "input_spec": [
+ {
+ "contains": [
+ "host name"
+ ],
+ "description": "Device ID or hostname of the host from which to collect files",
+ "name": "device"
+ },
+ {
+ "contains": [
+ "file path"
+ ],
+ "description": "Path of the file on the endpoint to collect to a new artifact",
+ "name": "path"
+ }
+ ],
+ "output_spec": [
+ {
+ "contains": [],
+ "datapaths": [
+ "host_observables:custom_function:observable_array"
+ ],
+ "deduplicate": false,
+ "description": "An array of observable dictionaries",
+ "metadata": {},
+ "name": "observable"
+ },
+ {
+ "contains": [],
+ "datapaths": [
+ "format_file_collection_report:formatted_data"
+ ],
+ "deduplicate": false,
+ "description": "A report of the devices that were isolated via Splunk SOAR.",
+ "metadata": {},
+ "name": "markdown_report"
+ }
+ ],
+ "playbook_trigger": "artifact_created",
+ "playbook_type": "data",
+ "python_version": "3",
+ "schema": "5.0.15",
+ "version": "6.3.1.178"
+ },
+ "create_time": "2025-04-30T17:42:24.612005+00:00",
+ "draft_mode": false,
+ "labels": [
+ "*"
+ ],
+ "tags": [
+ "CrowdStrike_OAuth_API",
+ "host name",
+ "enrichment",
+ "D3-FA"
+ ]
+}
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_File_Collection.png b/playbooks/CrowdStrike_OAuth_API_File_Collection.png
new file mode 100644
index 0000000000..19831ceff7
Binary files /dev/null and b/playbooks/CrowdStrike_OAuth_API_File_Collection.png differ
diff --git a/playbooks/CrowdStrike_OAuth_API_File_Collection.py b/playbooks/CrowdStrike_OAuth_API_File_Collection.py
new file mode 100644
index 0000000000..996f27f0d1
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_File_Collection.py
@@ -0,0 +1,606 @@
+"""
+Accepts a hostname or device id as well as a file path as input and collects the file to the event File Vault from a device in Crowdstrike. An artifact is created from the collected file. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference.
+"""
+
+
+import phantom.rules as phantom
+import json
+from datetime import datetime, timedelta
+
+
+@phantom.playbook_block()
+def on_start(container):
+ phantom.debug('on_start() called')
+
+ # call 'input_filter' block
+ input_filter(container=container)
+
+ return
+
+@phantom.playbook_block()
+def input_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("input_filter() called")
+
+ ################################################################################
+ # Determines if the provided inputs are present in the dataset.
+ ################################################################################
+
+ # collect filtered artifact ids and results for 'if' condition 1
+ matched_artifacts_1, matched_results_1 = phantom.condition(
+ container=container,
+ logical_operator="and",
+ conditions=[
+ ["playbook_input:device", "!=", ""],
+ ["playbook_input:path", "!=", ""]
+ ],
+ name="input_filter:condition_1",
+ delimiter=None)
+
+ # call connected blocks if filtered artifacts or results
+ if matched_artifacts_1 or matched_results_1:
+ format_fql(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_1, filtered_results=matched_results_1)
+
+ return
+
+
+@phantom.playbook_block()
+def host_observables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("host_observables() called")
+
+ ################################################################################
+ # Format a normalized output for each host
+ ################################################################################
+
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.parameter.device_id"], action_results=results)
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.hostname"], action_results=results)
+ list_session_files_result_data = phantom.collect2(container=container, datapath=["list_session_files:action_result.data.*.resources.*.sha256","list_session_files:action_result.data.*.resources.*.size"], action_results=results)
+ get_session_file_result_data = phantom.collect2(container=container, datapath=["get_session_file:action_result.data.*.vault_document","get_session_file:action_result.summary.vault_id"], action_results=results)
+ determine_get_status__file_name = json.loads(_ if (_ := phantom.get_run_data(key="determine_get_status:file_name")) != "" else "null") # pylint: disable=used-before-assignment
+ determine_get_status__status = json.loads(_ if (_ := phantom.get_run_data(key="determine_get_status:status")) != "" else "null") # pylint: disable=used-before-assignment
+ determine_get_status__status_detail = json.loads(_ if (_ := phantom.get_run_data(key="determine_get_status:status_detail")) != "" else "null") # pylint: disable=used-before-assignment
+
+ create_session_parameter_device_id = [item[0] for item in create_session_result_data]
+ query_device_result_item_0 = [item[0] for item in query_device_result_data]
+ list_session_files_result_item_0 = [item[0] for item in list_session_files_result_data]
+ list_session_files_result_item_1 = [item[1] for item in list_session_files_result_data]
+ get_session_file_result_item_0 = [item[0] for item in get_session_file_result_data]
+ get_session_file_summary_vault_id = [item[1] for item in get_session_file_result_data]
+
+ host_observables__observable_array = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ host_observables__observable_array = []
+
+ for device_id, hostname, file_hash, file_size, vault_document, vault_id in zip(create_session_parameter_device_id, query_device_result_item_0, list_session_files_result_item_0, list_session_files_result_item_1, get_session_file_result_item_0, get_session_file_summary_vault_id):
+ file_artifacts = []
+ for file_name, status, status_message in zip(determine_get_status__file_name, determine_get_status__status, determine_get_status__status_detail):
+ # Initialize the observable dictionary
+ if status == "success":
+ file_artifacts.append({
+ "name": file_name,
+ "status": status,
+ "status_detail": status_message,
+ "hashes": [
+ {
+ "value": file_hash,
+ "algorithm": "SHA-256",
+ "algorithm_id": 3
+ }
+ ],
+ "size": file_size,
+ "vault_document": vault_document,
+ "vault_id": vault_id
+ })
+ else:
+ file_artifacts.append({
+ "name": file_name,
+ "status": status,
+ "status_detail": status_message
+ })
+ observable = {
+ "source": "Crowdstrike OAuth API",
+ "type": "Endpoint",
+ "activity_name": "File Collection",
+ "uid": device_id,
+ "hostname": hostname,
+ "file_artifacts": file_artifacts
+ }
+
+ # Add the observable to the array
+ host_observables__observable_array.append(observable)
+
+ # Debug output for verification
+ #phantom.debug(host_observables__observable_array)
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="host_observables:observable_array", value=json.dumps(host_observables__observable_array))
+
+ return
+
+
+@phantom.playbook_block()
+def format_fql(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_fql() called")
+
+ ################################################################################
+ # Format the FQL query to get the input device information using its ID or hostname.
+ ################################################################################
+
+ template = """%%\nhostname:['{0}'],device_id:['{0}']\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "playbook_input:device"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_fql")
+
+ query_device(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def query_device(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("query_device() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Get information about the device using its hostname or device id.
+ ################################################################################
+
+ format_fql__as_list = phantom.get_format_data(name="format_fql__as_list")
+
+ parameters = []
+
+ # build parameters list for 'query_device' call
+ for format_fql__item in format_fql__as_list:
+ parameters.append({
+ "limit": 50,
+ "filter": format_fql__item,
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("query device", parameters=parameters, name="query_device", assets=["crowdstrike_oauth_api"], callback=create_session)
+
+ return
+
+
+@phantom.playbook_block()
+def format_file_collection_report(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_file_collection_report() called")
+
+ ################################################################################
+ # Format a summary table with the information gathered from the playbook.
+ ################################################################################
+
+ template = """Endpoint Files were collected by Splunk SOAR and an artifact was created. The table below summarizes the information gathered.\n\n| Device ID | Hostname | File Path | File Hash | Vault ID | Artifact ID | Status | Message |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n%%\n| {0} | {1} | `{2}` | {3} | {4} | {5} | {6} | `{7}` |\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "create_session:action_result.parameter.device_id",
+ "query_device:action_result.data.*.hostname",
+ "list_session_files:action_result.data.*.resources.*.name",
+ "list_session_files:action_result.data.*.resources.*.sha256",
+ "get_session_file:action_result.summary.vault_id",
+ "artifact_create:custom_function_result.data.artifact_id",
+ "determine_get_status:custom_function:status",
+ "determine_get_status:custom_function:status_detail"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_file_collection_report")
+
+ host_observables(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def create_session(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("create_session() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Creates a Real Time Response (RTR) session to interact with the endpoint
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'create_session' call
+ for query_device_result_item in query_device_result_data:
+ if query_device_result_item[0] is not None:
+ parameters.append({
+ "device_id": query_device_result_item[0],
+ "context": {'artifact_id': query_device_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("create session", parameters=parameters, name="create_session", assets=["crowdstrike_oauth_api"], callback=run_admin_command_get)
+
+ return
+
+
+@phantom.playbook_block()
+def run_admin_command_get(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("run_admin_command_get() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Gets the specified file based on the playbook input path
+ ################################################################################
+
+ playbook_input_path = phantom.collect2(container=container, datapath=["playbook_input:path"])
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.summary.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'run_admin_command_get' call
+ for playbook_input_path_item in playbook_input_path:
+ for query_device_result_item in query_device_result_data:
+ for create_session_result_item in create_session_result_data:
+ if query_device_result_item[0] is not None and create_session_result_item[0] is not None:
+ parameters.append({
+ "data": playbook_input_path_item[0],
+ "command": "get",
+ "device_id": query_device_result_item[0],
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("run admin command", parameters=parameters, name="run_admin_command_get", assets=["crowdstrike_oauth_api"], callback=determine_get_status)
+
+ return
+
+
+@phantom.playbook_block()
+def list_session_files(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("list_session_files() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Lists the previous file we ran a get command on, which returns its SHA256 hash
+ ################################################################################
+
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.summary.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'list_session_files' call
+ for create_session_result_item in create_session_result_data:
+ if create_session_result_item[0] is not None:
+ parameters.append({
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("list session files", parameters=parameters, name="list_session_files", assets=["crowdstrike_oauth_api"], callback=get_session_file)
+
+ return
+
+
+@phantom.playbook_block()
+def get_session_file(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("get_session_file() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Downloads the RTR session file from the endpoint to the SOAR File Vault
+ ################################################################################
+
+ list_session_files_result_data = phantom.collect2(container=container, datapath=["list_session_files:action_result.data.*.resources.*.sha256","list_session_files:action_result.parameter.context.artifact_id"], action_results=results)
+ playbook_input_path = phantom.collect2(container=container, datapath=["playbook_input:path"])
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.summary.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'get_session_file' call
+ for list_session_files_result_item in list_session_files_result_data:
+ for playbook_input_path_item in playbook_input_path:
+ for create_session_result_item in create_session_result_data:
+ if list_session_files_result_item[0] is not None and create_session_result_item[0] is not None:
+ parameters.append({
+ "file_hash": list_session_files_result_item[0],
+ "file_name": playbook_input_path_item[0],
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("get session file", parameters=parameters, name="get_session_file", assets=["crowdstrike_oauth_api"], callback=create_file_artifact)
+
+ return
+
+
+@phantom.playbook_block()
+def delete_session(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("delete_session() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Closes a Real Time Response (RTR) session with the endpoint
+ ################################################################################
+
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.summary.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'delete_session' call
+ for create_session_result_item in create_session_result_data:
+ if create_session_result_item[0] is not None:
+ parameters.append({
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("delete session", parameters=parameters, name="delete_session", assets=["crowdstrike_oauth_api"], callback=format_file_collection_report)
+
+ return
+
+
+@phantom.playbook_block()
+def create_file_artifact(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("create_file_artifact() called")
+
+ ################################################################################
+ # Create the JSON structure from the collected file metadata to create a SOAR
+ # artifact.
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.data.*.hostname"], action_results=results)
+ list_session_files_result_data = phantom.collect2(container=container, datapath=["list_session_files:action_result.data.*.resources.*.name","list_session_files:action_result.data.*.resources.*.sha256"], action_results=results)
+ get_session_file_result_data = phantom.collect2(container=container, datapath=["get_session_file:action_result.data.*.vault_id"], action_results=results)
+ run_admin_command_get_result_data = phantom.collect2(container=container, datapath=["run_admin_command_get:action_result.parameter.data"], action_results=results)
+
+ query_device_result_item_0 = [item[0] for item in query_device_result_data]
+ query_device_result_item_1 = [item[1] for item in query_device_result_data]
+ list_session_files_result_item_0 = [item[0] for item in list_session_files_result_data]
+ list_session_files_result_item_1 = [item[1] for item in list_session_files_result_data]
+ get_session_file_result_item_0 = [item[0] for item in get_session_file_result_data]
+ run_admin_command_get_parameter_data = [item[0] for item in run_admin_command_get_result_data]
+
+ create_file_artifact__data = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+ import os
+
+ create_file_artifact__data = []
+ for device_id, hostname, origFilepath, filehash, vault_id, fullpath in zip(query_device_result_item_0, query_device_result_item_1, list_session_files_result_item_0, list_session_files_result_item_1, get_session_file_result_item_0, run_admin_command_get_parameter_data):
+ filename = os.path.basename(fullpath.replace('\\','/'))
+ filepath = os.path.dirname(fullpath.replace('\\','/'))
+ create_file_artifact__data = {
+ "cef": {
+ "deviceExternalId": device_id,
+ "hostname": hostname,
+ "origFilePath": origFilepath,
+ "fileName": filename,
+ "filePath": os.path.join(filepath.replace('/','\\'), ''),
+ "fileHashSha256": filehash,
+ "vaultId": vault_id
+ }
+ }
+
+ #phantom.debug(f"File artifact data: {create_file_artifact__data}")
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="create_file_artifact:data", value=json.dumps(create_file_artifact__data))
+
+ artifact_create(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def artifact_create(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("artifact_create() called")
+
+ ################################################################################
+ # Creates a new artifact to store information about the file collected from Crowdstrike.
+ ################################################################################
+
+ id_value = container.get("id", None)
+ create_file_artifact__data = json.loads(_ if (_ := phantom.get_run_data(key="create_file_artifact:data")) != "" else "null") # pylint: disable=used-before-assignment
+
+ parameters = []
+
+ parameters.append({
+ "name": "Collected File Artifact",
+ "tags": None,
+ "label": "collected_file",
+ "severity": None,
+ "cef_field": None,
+ "cef_value": None,
+ "container": id_value,
+ "input_json": create_file_artifact__data,
+ "cef_data_type": None,
+ "run_automation": None,
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.custom_function(custom_function="community/artifact_create", parameters=parameters, name="artifact_create", callback=delete_session)
+
+ return
+
+
+@phantom.playbook_block()
+def determine_get_status(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("determine_get_status() called")
+
+ ################################################################################
+ # Check the results of the RTR get command to ensure it was indeed successful.
+ # RTR commands are notably unreliable in how they report status, instead proving
+ # stdout and stderr information.
+ ################################################################################
+
+ run_admin_command_get_result_data = phantom.collect2(container=container, datapath=["run_admin_command_get:action_result.status","run_admin_command_get:action_result.data.*.resources.*.stdout","run_admin_command_get:action_result.data.*.resources.*.stderr","run_admin_command_get:action_result.parameter.data","run_admin_command_get:action_result.message"], action_results=results)
+
+ run_admin_command_get_result_item_0 = [item[0] for item in run_admin_command_get_result_data]
+ run_admin_command_get_result_item_1 = [item[1] for item in run_admin_command_get_result_data]
+ run_admin_command_get_result_item_2 = [item[2] for item in run_admin_command_get_result_data]
+ run_admin_command_get_parameter_data = [item[3] for item in run_admin_command_get_result_data]
+ run_admin_command_get_result_message = [item[4] for item in run_admin_command_get_result_data]
+
+ determine_get_status__file_name = None
+ determine_get_status__status = None
+ determine_get_status__status_detail = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ determine_get_status__file_name = []
+ determine_get_status__status = []
+ determine_get_status__status_detail = []
+
+ for file_name, status, stdout, stderr, message in zip(run_admin_command_get_parameter_data, run_admin_command_get_result_item_0, run_admin_command_get_result_item_1, run_admin_command_get_result_item_2,run_admin_command_get_result_message):
+ #phantom.debug(f"file_name: {file_name}, status: {status}, stdout: {stdout}, stderr: {stderr}")
+ determine_get_status__file_name.append(file_name)
+ determine_get_status__status.append(status)
+ if status == "success" and stdout.strip() and not stderr.strip():
+ determine_get_status__status_detail.append(f"File collected: {stdout.strip()}")
+ else:
+ determine_get_status__status_detail.append(stderr.strip() if stderr else message)
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="determine_get_status:file_name", value=json.dumps(determine_get_status__file_name))
+ phantom.save_run_data(key="determine_get_status:status", value=json.dumps(determine_get_status__status))
+ phantom.save_run_data(key="determine_get_status:status_detail", value=json.dumps(determine_get_status__status_detail))
+
+ list_session_files(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def on_finish(container, summary):
+ phantom.debug("on_finish() called")
+
+ format_file_collection_report = phantom.get_format_data(name="format_file_collection_report")
+ host_observables__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="host_observables:observable_array")) != "" else "null") # pylint: disable=used-before-assignment
+
+ output = {
+ "observable": host_observables__observable_array,
+ "markdown_report": format_file_collection_report,
+ }
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_playbook_output_data(output=output)
+
+ return
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_File_Collection.yml b/playbooks/CrowdStrike_OAuth_API_File_Collection.yml
new file mode 100644
index 0000000000..4094ab98e0
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_File_Collection.yml
@@ -0,0 +1,31 @@
+name: CrowdStrike OAuth API File Collection
+id: 2296ce3f-171f-467f-8025-f046f5d59133
+version: 1
+date: '2025-06-09'
+author: Christian Cloutier, Splunk
+type: Investigation
+description: "Accepts a hostname or device id as well as a file path as input and collects the file to the event File Vault from a device in Crowdstrike. An artifact is created from the collected file. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference."
+playbook: CrowdStrike_OAuth_API_File_Collection
+how_to_implement: This input playbook requires the CrowdStrike OAuth API connector to be configured. It is designed to work with an endpoint hostname or agent id and collect a specific file from the endpoint (using an absolute path) for forensics or later use in automation playbooks.
+references: []
+app_list:
+ - CrowdStrike OAuth API
+tags:
+ platform_tags:
+ - "host name"
+ - "device id"
+ - "path"
+ - "File Collection"
+ - "D3-FA"
+ - "CrowdStrike_OAuth_API"
+ playbook_type: Input
+ vpe_type: Modern
+ playbook_fields: [device,path]
+ product:
+ - Splunk SOAR
+ use_cases:
+ - Collection
+ - Malware
+ - Endpoint
+ defend_technique_id:
+ - D3-FA
diff --git a/playbooks/CrowdStrike_OAuth_API_File_Eviction.json b/playbooks/CrowdStrike_OAuth_API_File_Eviction.json
new file mode 100644
index 0000000000..e30f1efa49
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_File_Eviction.json
@@ -0,0 +1,499 @@
+{
+ "blockly": false,
+ "blockly_xml": "",
+ "category": "File Eviction",
+ "coa": {
+ "data": {
+ "description": "Accepts a hostname or device id as well as a file path as input and deletes the file from a device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference.",
+ "edges": [
+ {
+ "id": "port_0_to_port_2",
+ "sourceNode": "0",
+ "sourcePort": "0_out",
+ "targetNode": "2",
+ "targetPort": "2_in"
+ },
+ {
+ "id": "port_5_to_port_1",
+ "sourceNode": "5",
+ "sourcePort": "5_out",
+ "targetNode": "1",
+ "targetPort": "1_in"
+ },
+ {
+ "conditions": [
+ {
+ "index": 0
+ }
+ ],
+ "id": "port_2_to_port_7",
+ "sourceNode": "2",
+ "sourcePort": "2_out",
+ "targetNode": "7",
+ "targetPort": "7_in"
+ },
+ {
+ "id": "port_7_to_port_8",
+ "sourceNode": "7",
+ "sourcePort": "7_out",
+ "targetNode": "8",
+ "targetPort": "8_in"
+ },
+ {
+ "id": "port_9_to_port_5",
+ "sourceNode": "9",
+ "sourcePort": "9_out",
+ "targetNode": "5",
+ "targetPort": "5_in"
+ },
+ {
+ "id": "port_8_to_port_11",
+ "sourceNode": "8",
+ "sourcePort": "8_out",
+ "targetNode": "11",
+ "targetPort": "11_in"
+ },
+ {
+ "id": "port_15_to_port_9",
+ "sourceNode": "15",
+ "sourcePort": "15_out",
+ "targetNode": "9",
+ "targetPort": "9_in"
+ },
+ {
+ "id": "port_11_to_port_16",
+ "sourceNode": "11",
+ "sourcePort": "11_out",
+ "targetNode": "16",
+ "targetPort": "16_in"
+ },
+ {
+ "id": "port_16_to_port_15",
+ "sourceNode": "16",
+ "sourcePort": "16_out",
+ "targetNode": "15",
+ "targetPort": "15_in"
+ }
+ ],
+ "hash": "5a2079268f4ee2268aa332c6f8af2f2b76274fba",
+ "nodes": {
+ "0": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_start",
+ "id": "0",
+ "type": "start"
+ },
+ "errors": {},
+ "id": "0",
+ "type": "start",
+ "warnings": {},
+ "x": 19.999999999999986,
+ "y": -2.5579538487363607e-13
+ },
+ "1": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_finish",
+ "id": "1",
+ "type": "end"
+ },
+ "errors": {},
+ "id": "1",
+ "type": "end",
+ "warnings": {},
+ "x": 19.999999999999986,
+ "y": 1576
+ },
+ "11": {
+ "data": {
+ "action": "create session",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "create session",
+ "customNameId": 0,
+ "description": "Creates a Real Time Response (RTR) session to interact with the endpoint",
+ "join": [],
+ "note": "Creates a Real Time Response (RTR) session to interact with the endpoint"
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "create_session",
+ "id": "11",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "device_id": "query_device:action_result.data.*.device_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "device_id"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "11",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 680
+ },
+ "15": {
+ "data": {
+ "action": "delete session",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "delete session",
+ "customNameId": 0,
+ "description": "Closes a Real Time Response (RTR) session to interact with the endpoint",
+ "join": [],
+ "note": "Closes a Real Time Response (RTR) session to interact with the endpoint"
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "delete_session",
+ "id": "15",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "session_id": "create_session:action_result.summary.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "15",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1032
+ },
+ "16": {
+ "data": {
+ "action": "run admin command",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "run admin command rm",
+ "customNameId": 0,
+ "description": "Run the rm admin command via Real Time Response (RTR)",
+ "join": [],
+ "note": "Run the rm admin command via Real Time Response (RTR)"
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "run_admin_command_rm",
+ "id": "16",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "command": "rm",
+ "data": "playbook_input:path",
+ "device_id": "query_device:action_result.data.*.device_id",
+ "session_id": "create_session:action_result.summary.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "command"
+ },
+ {
+ "data_type": "string",
+ "field": "device_id"
+ },
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "16",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 856
+ },
+ "2": {
+ "data": {
+ "advanced": {
+ "customName": "input filter",
+ "customNameId": 0,
+ "description": "Determines if the provided inputs are present in the dataset.",
+ "join": [],
+ "note": "Determines if the provided inputs are present in the dataset."
+ },
+ "conditions": [
+ {
+ "comparisons": [
+ {
+ "conditionIndex": 0,
+ "op": "!=",
+ "param": "playbook_input:device",
+ "value": ""
+ },
+ {
+ "conditionIndex": 0,
+ "op": "!=",
+ "param": "playbook_input:path",
+ "value": ""
+ }
+ ],
+ "conditionIndex": 0,
+ "customName": "input device and path present",
+ "logic": "and"
+ }
+ ],
+ "functionId": 1,
+ "functionName": "input_filter",
+ "id": "2",
+ "type": "filter"
+ },
+ "errors": {},
+ "id": "2",
+ "type": "filter",
+ "warnings": {},
+ "x": 60,
+ "y": 148
+ },
+ "5": {
+ "customCode": null,
+ "data": {
+ "advanced": {
+ "customName": "host observables",
+ "customNameId": 0,
+ "description": "Format a normalized output for each host",
+ "join": [],
+ "note": "Format a normalized output for each host."
+ },
+ "functionId": 1,
+ "functionName": "host_observables",
+ "id": "5",
+ "inputParameters": [
+ "create_session:action_result.parameter.device_id",
+ "query_device:action_result.data.*.hostname",
+ "run_admin_command_rm:action_result.parameter.data",
+ "run_admin_command_rm:action_result.data"
+ ],
+ "outputVariables": [
+ "observable_array"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "5",
+ "type": "code",
+ "userCode": " \n host_observables__observable_array = []\n i = 0\n \n for system in run_admin_command_rm_result_item_1:\n # Handle case where multiple files are listed on the same host.\n device_id = create_session_parameter_device_id[i] if i < len(create_session_parameter_device_id) else create_session_parameter_device_id[0]\n hostname = query_device_result_item_0[i] if i < len(query_device_result_item_0) else query_device_result_item_0[0]\n \n # Initialize the observable dictionary\n observable = {\n \"source\": \"Crowdstrike OAuth API\",\n \"type\": \"Endpoint\",\n \"activity_name\": \"File Eviction\",\n \"uid\": device_id,\n \"hostname\": hostname,\n \"file_artifacts\": [],\n \"d3fend\": {\n \"d3f_tactic\": \"Evict\",\n \"d3f_technique\": \"D3-FEV\",\n \"version\": \"1.0.0\"\n }\n } \n \n for file in system[0][\"resources\"]:\n file_name = run_admin_command_rm_parameter_data[i]\n status = \"success\" if file[\"stdout\"] and not file[\"stderr\"] else \"failed\"\n status_message = file[\"stderr\"] if file[\"stderr\"] else file[\"stdout\"]\n observable[\"file_artifacts\"].append(\n {\n \"name\": file_name,\n \"status\": status,\n \"status_detail\": status_message\n }\n )\n \n # Add the observable to the array\n host_observables__observable_array.append(observable)\n i+=1\n \n # Debug output for verification\n phantom.debug(host_observables__observable_array)\n \n",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1400
+ },
+ "7": {
+ "data": {
+ "advanced": {
+ "customName": "format fql",
+ "customNameId": 0,
+ "description": "Format the FQL query to get the input device information using its ID or hostname.",
+ "join": [],
+ "note": "Format the FQL query to get the input device information using its ID or hostname."
+ },
+ "functionId": 2,
+ "functionName": "format_fql",
+ "id": "7",
+ "parameters": [
+ "playbook_input:device"
+ ],
+ "template": "%%\nhostname:['{0}'],device_id:['{0}']\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "7",
+ "type": "format",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 328
+ },
+ "8": {
+ "data": {
+ "action": "query device",
+ "actionType": "investigate",
+ "advanced": {
+ "customName": "query device",
+ "customNameId": 0,
+ "description": "Get information about the device using its hostname or device id.",
+ "join": [],
+ "note": "Get information about the device using its hostname or device id."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "query_device",
+ "id": "8",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "filter": "format_fql:formatted_data.*",
+ "limit": 50
+ },
+ "requiredParameters": [
+ {
+ "data_type": "numeric",
+ "default": 50,
+ "field": "limit"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "8",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 504
+ },
+ "9": {
+ "data": {
+ "advanced": {
+ "customName": "format file eviction report",
+ "customNameId": 0,
+ "description": "Format a summary table with the information gathered from the playbook.",
+ "join": [],
+ "note": "Format a summary table with the information gathered from the playbook."
+ },
+ "functionId": 3,
+ "functionName": "format_file_eviction_report",
+ "id": "9",
+ "parameters": [
+ "query_device:action_result.data.*.device_id",
+ "run_admin_command_rm:action_result.parameter.data",
+ "run_admin_command_rm:action_result.data.*.resources.*.stdout",
+ "run_admin_command_rm:action_result.data.*.resources.*.stderr"
+ ],
+ "template": "Endpoint Files were removed by Splunk SOAR. The table below summarizes the information gathered.\n\n| Device ID | File Path | Eviction Status |\n| --- | --- | --- |\n%%\n| {0} | {1} | {2}{3} |\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "9",
+ "type": "format",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1208
+ }
+ },
+ "notes": "Inputs: \ndevice (CrowdStrike Device ID or Hostname)\npath (File path)\nInteractions: CrowdStrike OAuth API\nActions: create session, run admin command, delete session\nOutputs: observables, markdown report"
+ },
+ "input_spec": [
+ {
+ "contains": [
+ "host name"
+ ],
+ "description": "Device ID or hostname of the host to delete a file from",
+ "name": "device"
+ },
+ {
+ "contains": [
+ "file path"
+ ],
+ "description": "Path of the file on the endpoint to remove",
+ "name": "path"
+ }
+ ],
+ "output_spec": [
+ {
+ "contains": [],
+ "datapaths": [
+ "host_observables:custom_function:observable_array"
+ ],
+ "deduplicate": false,
+ "description": "An array of observable dictionaries",
+ "metadata": {},
+ "name": "observable"
+ },
+ {
+ "contains": [],
+ "datapaths": [
+ "format_file_eviction_report:formatted_data"
+ ],
+ "deduplicate": false,
+ "description": "A report of the devices that were isolated via Splunk SOAR.",
+ "metadata": {},
+ "name": "markdown_report"
+ }
+ ],
+ "playbook_trigger": "artifact_created",
+ "playbook_type": "data",
+ "python_version": "3",
+ "schema": "5.0.15",
+ "version": "6.3.1.178"
+ },
+ "create_time": "2025-04-09T16:26:45.445744+00:00",
+ "draft_mode": false,
+ "labels": [
+ "*"
+ ],
+ "tags": [
+ "CrowdStrike_OAuth_API",
+ "host name",
+ "D3-FEV"
+ ]
+}
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_File_Eviction.png b/playbooks/CrowdStrike_OAuth_API_File_Eviction.png
new file mode 100644
index 0000000000..e46ebdc072
Binary files /dev/null and b/playbooks/CrowdStrike_OAuth_API_File_Eviction.png differ
diff --git a/playbooks/CrowdStrike_OAuth_API_File_Eviction.py b/playbooks/CrowdStrike_OAuth_API_File_Eviction.py
new file mode 100644
index 0000000000..ee89b9207a
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_File_Eviction.py
@@ -0,0 +1,365 @@
+"""
+Accepts a hostname or device id as well as a file path as input and deletes the file from a device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference.
+"""
+
+
+import phantom.rules as phantom
+import json
+from datetime import datetime, timedelta
+
+
+@phantom.playbook_block()
+def on_start(container):
+ phantom.debug('on_start() called')
+
+ # call 'input_filter' block
+ input_filter(container=container)
+
+ return
+
+@phantom.playbook_block()
+def input_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("input_filter() called")
+
+ ################################################################################
+ # Determines if the provided inputs are present in the dataset.
+ ################################################################################
+
+ # collect filtered artifact ids and results for 'if' condition 1
+ matched_artifacts_1, matched_results_1 = phantom.condition(
+ container=container,
+ logical_operator="and",
+ conditions=[
+ ["playbook_input:device", "!=", ""],
+ ["playbook_input:path", "!=", ""]
+ ],
+ name="input_filter:condition_1",
+ delimiter=None)
+
+ # call connected blocks if filtered artifacts or results
+ if matched_artifacts_1 or matched_results_1:
+ format_fql(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_1, filtered_results=matched_results_1)
+
+ return
+
+
+@phantom.playbook_block()
+def host_observables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("host_observables() called")
+
+ ################################################################################
+ # Format a normalized output for each host
+ ################################################################################
+
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.parameter.device_id"], action_results=results)
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.hostname"], action_results=results)
+ run_admin_command_rm_result_data = phantom.collect2(container=container, datapath=["run_admin_command_rm:action_result.parameter.data","run_admin_command_rm:action_result.data"], action_results=results)
+
+ create_session_parameter_device_id = [item[0] for item in create_session_result_data]
+ query_device_result_item_0 = [item[0] for item in query_device_result_data]
+ run_admin_command_rm_parameter_data = [item[0] for item in run_admin_command_rm_result_data]
+ run_admin_command_rm_result_item_1 = [item[1] for item in run_admin_command_rm_result_data]
+
+ host_observables__observable_array = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ host_observables__observable_array = []
+ i = 0
+
+ for system in run_admin_command_rm_result_item_1:
+ # Handle case where multiple files are listed on the same host.
+ device_id = create_session_parameter_device_id[i] if i < len(create_session_parameter_device_id) else create_session_parameter_device_id[0]
+ hostname = query_device_result_item_0[i] if i < len(query_device_result_item_0) else query_device_result_item_0[0]
+
+ # Initialize the observable dictionary
+ observable = {
+ "source": "Crowdstrike OAuth API",
+ "type": "Endpoint",
+ "activity_name": "File Eviction",
+ "uid": device_id,
+ "hostname": hostname,
+ "file_artifacts": [],
+ "d3fend": {
+ "d3f_tactic": "Evict",
+ "d3f_technique": "D3-FEV",
+ "version": "1.0.0"
+ }
+ }
+
+ for file in system[0]["resources"]:
+ file_name = run_admin_command_rm_parameter_data[i]
+ status = "success" if file["stdout"] and not file["stderr"] else "failed"
+ status_message = file["stderr"] if file["stderr"] else file["stdout"]
+ observable["file_artifacts"].append(
+ {
+ "name": file_name,
+ "status": status,
+ "status_detail": status_message
+ }
+ )
+
+ # Add the observable to the array
+ host_observables__observable_array.append(observable)
+ i+=1
+
+ # Debug output for verification
+ phantom.debug(host_observables__observable_array)
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="host_observables:observable_array", value=json.dumps(host_observables__observable_array))
+
+ return
+
+
+@phantom.playbook_block()
+def format_fql(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_fql() called")
+
+ ################################################################################
+ # Format the FQL query to get the input device information using its ID or hostname.
+ ################################################################################
+
+ template = """%%\nhostname:['{0}'],device_id:['{0}']\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "playbook_input:device"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_fql")
+
+ query_device(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def query_device(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("query_device() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Get information about the device using its hostname or device id.
+ ################################################################################
+
+ format_fql__as_list = phantom.get_format_data(name="format_fql__as_list")
+
+ parameters = []
+
+ # build parameters list for 'query_device' call
+ for format_fql__item in format_fql__as_list:
+ parameters.append({
+ "limit": 50,
+ "filter": format_fql__item,
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("query device", parameters=parameters, name="query_device", assets=["crowdstrike_oauth_api"], callback=create_session)
+
+ return
+
+
+@phantom.playbook_block()
+def format_file_eviction_report(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_file_eviction_report() called")
+
+ ################################################################################
+ # Format a summary table with the information gathered from the playbook.
+ ################################################################################
+
+ template = """Endpoint Files were removed by Splunk SOAR. The table below summarizes the information gathered.\n\n| Device ID | File Path | Eviction Status |\n| --- | --- | --- |\n%%\n| {0} | {1} | {2}{3} |\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "query_device:action_result.data.*.device_id",
+ "run_admin_command_rm:action_result.parameter.data",
+ "run_admin_command_rm:action_result.data.*.resources.*.stdout",
+ "run_admin_command_rm:action_result.data.*.resources.*.stderr"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_file_eviction_report")
+
+ host_observables(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def create_session(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("create_session() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Creates a Real Time Response (RTR) session to interact with the endpoint
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'create_session' call
+ for query_device_result_item in query_device_result_data:
+ if query_device_result_item[0] is not None:
+ parameters.append({
+ "device_id": query_device_result_item[0],
+ "context": {'artifact_id': query_device_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("create session", parameters=parameters, name="create_session", assets=["crowdstrike_oauth_api"], callback=run_admin_command_rm)
+
+ return
+
+
+@phantom.playbook_block()
+def delete_session(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("delete_session() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Closes a Real Time Response (RTR) session to interact with the endpoint
+ ################################################################################
+
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.summary.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'delete_session' call
+ for create_session_result_item in create_session_result_data:
+ if create_session_result_item[0] is not None:
+ parameters.append({
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("delete session", parameters=parameters, name="delete_session", assets=["crowdstrike_oauth_api"], callback=format_file_eviction_report)
+
+ return
+
+
+@phantom.playbook_block()
+def run_admin_command_rm(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("run_admin_command_rm() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Run the rm admin command via Real Time Response (RTR)
+ ################################################################################
+
+ playbook_input_path = phantom.collect2(container=container, datapath=["playbook_input:path"])
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.summary.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'run_admin_command_rm' call
+ for playbook_input_path_item in playbook_input_path:
+ for query_device_result_item in query_device_result_data:
+ for create_session_result_item in create_session_result_data:
+ if query_device_result_item[0] is not None and create_session_result_item[0] is not None:
+ parameters.append({
+ "data": playbook_input_path_item[0],
+ "command": "rm",
+ "device_id": query_device_result_item[0],
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("run admin command", parameters=parameters, name="run_admin_command_rm", assets=["crowdstrike_oauth_api"], callback=delete_session)
+
+ return
+
+
+@phantom.playbook_block()
+def on_finish(container, summary):
+ phantom.debug("on_finish() called")
+
+ format_file_eviction_report = phantom.get_format_data(name="format_file_eviction_report")
+ host_observables__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="host_observables:observable_array")) != "" else "null") # pylint: disable=used-before-assignment
+
+ output = {
+ "observable": host_observables__observable_array,
+ "markdown_report": format_file_eviction_report,
+ }
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_playbook_output_data(output=output)
+
+ return
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_File_Eviction.yml b/playbooks/CrowdStrike_OAuth_API_File_Eviction.yml
new file mode 100644
index 0000000000..2c8456a6d1
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_File_Eviction.yml
@@ -0,0 +1,31 @@
+name: CrowdStrike OAuth API File Eviction
+id: 4750935c-0105-416f-aca4-0d7a4207666d
+version: 1
+date: '2025-06-09'
+author: Christian Cloutier, Splunk
+type: Response
+description: "Accepts a hostname or device id as well as a file path as input and deletes the file from a device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference."
+playbook: CrowdStrike_OAuth_API_File_Eviction
+how_to_implement: This input playbook requires the CrowdStrike OAuth API connector to be configured. It is designed to work with an endpoint hostname or device id and delete a specific file from the endpoint (using an absolute path) for use in automation playbooks.
+references: []
+app_list:
+ - CrowdStrike OAuth API
+tags:
+ platform_tags:
+ - "host name"
+ - "device id"
+ - "path"
+ - "File Eviction"
+ - "D3-FEV"
+ - "CrowdStrike_OAuth_API"
+ playbook_type: Input
+ vpe_type: Modern
+ playbook_fields: [device,path]
+ product:
+ - Splunk SOAR
+ use_cases:
+ - Response
+ - Malware
+ - Endpoint
+ defend_technique_id:
+ - D3-FEV
diff --git a/playbooks/CrowdStrike_OAuth_API_File_Restore.json b/playbooks/CrowdStrike_OAuth_API_File_Restore.json
new file mode 100644
index 0000000000..92f2b9543a
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_File_Restore.json
@@ -0,0 +1,697 @@
+{
+ "blockly": false,
+ "blockly_xml": "",
+ "category": "File Restore",
+ "coa": {
+ "data": {
+ "description": "Accepts a hostname or device id as well as a file path as input and restores the file from the File Vault to a device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference.",
+ "edges": [
+ {
+ "id": "port_0_to_port_2",
+ "sourceNode": "0",
+ "sourcePort": "0_out",
+ "targetNode": "2",
+ "targetPort": "2_in"
+ },
+ {
+ "id": "port_5_to_port_1",
+ "sourceNode": "5",
+ "sourcePort": "5_out",
+ "targetNode": "1",
+ "targetPort": "1_in"
+ },
+ {
+ "conditions": [
+ {
+ "index": 0
+ }
+ ],
+ "id": "port_2_to_port_7",
+ "sourceNode": "2",
+ "sourcePort": "2_out",
+ "targetNode": "7",
+ "targetPort": "7_in"
+ },
+ {
+ "id": "port_7_to_port_8",
+ "sourceNode": "7",
+ "sourcePort": "7_out",
+ "targetNode": "8",
+ "targetPort": "8_in"
+ },
+ {
+ "id": "port_9_to_port_5",
+ "sourceNode": "9",
+ "sourcePort": "9_out",
+ "targetNode": "5",
+ "targetPort": "5_in"
+ },
+ {
+ "id": "port_15_to_port_9",
+ "sourceNode": "15",
+ "sourcePort": "15_out",
+ "targetNode": "9",
+ "targetPort": "9_in"
+ },
+ {
+ "id": "port_16_to_port_15",
+ "sourceNode": "16",
+ "sourcePort": "16_out",
+ "targetNode": "15",
+ "targetPort": "15_in"
+ },
+ {
+ "id": "port_19_to_port_11",
+ "sourceNode": "19",
+ "sourcePort": "19_out",
+ "targetNode": "11",
+ "targetPort": "11_in"
+ },
+ {
+ "conditions": [
+ {
+ "index": 0
+ }
+ ],
+ "id": "port_18_to_port_19",
+ "sourceNode": "18",
+ "sourcePort": "18_out",
+ "targetNode": "19",
+ "targetPort": "19_in"
+ },
+ {
+ "id": "port_8_to_port_18",
+ "sourceNode": "8",
+ "sourcePort": "8_out",
+ "targetNode": "18",
+ "targetPort": "18_in"
+ },
+ {
+ "id": "port_11_to_port_29",
+ "sourceNode": "11",
+ "sourcePort": "11_out",
+ "targetNode": "29",
+ "targetPort": "29_in"
+ },
+ {
+ "id": "port_29_to_port_16",
+ "sourceNode": "29",
+ "sourcePort": "29_out",
+ "targetNode": "16",
+ "targetPort": "16_in"
+ }
+ ],
+ "hash": "3339fe23d90ca191d7151a1b509f9bc5879ea2da",
+ "nodes": {
+ "0": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_start",
+ "id": "0",
+ "type": "start"
+ },
+ "errors": {},
+ "id": "0",
+ "type": "start",
+ "warnings": {},
+ "x": 19.999999999999986,
+ "y": -6.394884621840902e-14
+ },
+ "1": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_finish",
+ "id": "1",
+ "type": "end"
+ },
+ "errors": {},
+ "id": "1",
+ "type": "end",
+ "warnings": {},
+ "x": 19.999999999999986,
+ "y": 2080
+ },
+ "11": {
+ "data": {
+ "action": "create session",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "create session",
+ "customNameId": 0,
+ "description": "Creates a Real Time Response (RTR) session to interact with the endpoint",
+ "join": [],
+ "note": "Creates a Real Time Response (RTR) session to interact with the endpoint"
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "create_session",
+ "id": "11",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "device_id": "query_device:action_result.data.*.device_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "device_id"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "11",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1036
+ },
+ "15": {
+ "data": {
+ "action": "delete session",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "delete session",
+ "customNameId": 0,
+ "description": "Closes a Real Time Response (RTR) session to interact with the endpoint",
+ "join": [],
+ "note": "Closes a Real Time Response (RTR) session to interact with the endpoint"
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "delete_session",
+ "id": "15",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "session_id": "create_session:action_result.summary.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "15",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1536
+ },
+ "16": {
+ "data": {
+ "action": "run admin command",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "run admin command put",
+ "customNameId": 0,
+ "description": "Run the put admin command via Real Time Response (RTR)",
+ "join": [],
+ "note": "Run the put admin command via Real Time Response (RTR)"
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "run_admin_command_put",
+ "id": "16",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "command": "put",
+ "data": "artifact:*.cef.fileName",
+ "device_id": "query_device:action_result.data.*.device_id",
+ "session_id": "create_session:action_result.summary.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "command"
+ },
+ {
+ "data_type": "string",
+ "field": "device_id"
+ },
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "16",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1360
+ },
+ "18": {
+ "data": {
+ "advanced": {
+ "customName": "filter file artifacts",
+ "customNameId": 0,
+ "description": "Filter to ensure the requested file to restore was found in the vent artifacts. This playbook assumes the file to restore has been previously collected by the CrowdStrike_OAuth_API_File_Collection input playbook.\n",
+ "join": [],
+ "note": "Filter to ensure the requested file to restore was found in the vent artifacts. This playbook assumes the file to restore has been previously collected by the CrowdStrike_OAuth_API_File_Collection input playbook.\n"
+ },
+ "conditions": [
+ {
+ "comparisons": [
+ {
+ "conditionIndex": 0,
+ "op": "==",
+ "param": "artifact:*.label",
+ "value": "collected_file"
+ },
+ {
+ "conditionIndex": 0,
+ "op": "in",
+ "param": "playbook_input:file",
+ "value": "artifact:*.cef.fileName"
+ }
+ ],
+ "conditionIndex": 0,
+ "customName": "file artifact exists",
+ "logic": "and"
+ }
+ ],
+ "functionId": 2,
+ "functionName": "filter_file_artifacts_0",
+ "id": "18",
+ "type": "filter"
+ },
+ "errors": {},
+ "id": "18",
+ "type": "filter",
+ "warnings": {},
+ "x": 60,
+ "y": 680
+ },
+ "19": {
+ "data": {
+ "action": "upload put file",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "upload put file",
+ "customNameId": 0,
+ "description": "Upload the file to restore from the SOAR Vault to Crowdstrike.",
+ "join": [],
+ "note": "Upload the file to restore from the SOAR Vault to Crowdstrike."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "upload_put_file",
+ "id": "19",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "comment": "File restored from Splunk SOAR",
+ "description": "File restored from Splunk SOAR via the CrowdStrike_OAuth_API_File_Restore playbook.",
+ "file_name": "filtered-data:filter_file_artifacts_0:condition_1:artifact:*.cef.fileName",
+ "vault_id": "filtered-data:filter_file_artifacts_0:condition_1:artifact:*.cef.vaultId"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "vault_id"
+ },
+ {
+ "data_type": "string",
+ "field": "description"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "19",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 860
+ },
+ "2": {
+ "data": {
+ "advanced": {
+ "customName": "input filter",
+ "customNameId": 0,
+ "description": "Determines if the provided inputs are present in the dataset.",
+ "join": [],
+ "note": "Determines if the provided inputs are present in the dataset."
+ },
+ "conditions": [
+ {
+ "comparisons": [
+ {
+ "conditionIndex": 0,
+ "op": "!=",
+ "param": "playbook_input:device",
+ "value": ""
+ },
+ {
+ "conditionIndex": 0,
+ "op": "!=",
+ "param": "playbook_input:file",
+ "value": ""
+ }
+ ],
+ "conditionIndex": 0,
+ "customName": "input device and file present",
+ "logic": "and"
+ }
+ ],
+ "functionId": 1,
+ "functionName": "input_filter",
+ "id": "2",
+ "type": "filter"
+ },
+ "errors": {},
+ "id": "2",
+ "type": "filter",
+ "warnings": {},
+ "x": 60,
+ "y": 148
+ },
+ "29": {
+ "data": {
+ "action": "run admin command",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "run admin command cd",
+ "customNameId": 0,
+ "description": "Run the cd admin command via Real Time Response (RTR) to set the working directory properly for the following put command.",
+ "join": [],
+ "note": "Run the cd admin command via Real Time Response (RTR) to set the working directory properly for the following put command."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 2,
+ "functionName": "run_admin_command_cd",
+ "id": "29",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "command": "cd",
+ "data": "filtered-data:filter_file_artifacts_0:condition_1:artifact:*.cef.filePath",
+ "device_id": "query_device:action_result.data.*.device_id",
+ "session_id": "create_session:action_result.summary.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "command"
+ },
+ {
+ "data_type": "string",
+ "field": "device_id"
+ },
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "29",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1212
+ },
+ "5": {
+ "customCode": null,
+ "data": {
+ "advanced": {
+ "customName": "host observables",
+ "customNameId": 0,
+ "description": "Format a normalized output for each host",
+ "join": [],
+ "note": "Format a normalized output for each host."
+ },
+ "functionId": 1,
+ "functionName": "host_observables",
+ "id": "5",
+ "inputParameters": [
+ "create_session:action_result.parameter.device_id",
+ "query_device:action_result.data.*.hostname",
+ "run_admin_command_put:action_result.parameter.data",
+ "run_admin_command_cd:action_result.parameter.data",
+ "run_admin_command_put:action_result.status",
+ "run_admin_command_put:action_result.message"
+ ],
+ "outputVariables": [
+ "observable_array"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "5",
+ "type": "code",
+ "userCode": " \n host_observables__observable_array = []\n \n for device_id, hostname, file_name, file_path, status, status_message in zip(create_session_parameter_device_id, query_device_result_item_0, run_admin_command_put_parameter_data, run_admin_command_cd_parameter_data, run_admin_command_put_result_item_1, run_admin_command_put_result_message):\n # Initialize the observable dictionary\n observable = {\n \"source\": \"Crowdstrike OAuth API\", \n \"type\": \"Endpoint\", \n \"activity_name\": \"File Restore\",\n \"uid\": device_id,\n \"hostname\": hostname,\n \"status\": status,\n \"status_detail\": status_message,\n \"file_artifacts\": {\n \"name\": file_name,\n \"path\": file_path\n },\n \"d3fend\": {\n \"d3f_tactic\": \"Restore\",\n \"d3f_technique\": \"D3-RF\",\n \"version\": \"1.0.0\"\n }\n }\n\n # Add the observable to the array\n host_observables__observable_array.append(observable)\n \n # Debug output for verification\n phantom.debug(host_observables__observable_array)\n \n",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1900
+ },
+ "7": {
+ "data": {
+ "advanced": {
+ "customName": "format fql",
+ "customNameId": 0,
+ "description": "Format the FQL query to get the input device information using its ID or hostname.",
+ "join": [],
+ "note": "Format the FQL query to get the input device information using its ID or hostname."
+ },
+ "functionId": 2,
+ "functionName": "format_fql",
+ "id": "7",
+ "parameters": [
+ "playbook_input:device"
+ ],
+ "template": "%%\nhostname:['{0}'],device_id:['{0}']\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "7",
+ "type": "format",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 328
+ },
+ "8": {
+ "data": {
+ "action": "query device",
+ "actionType": "investigate",
+ "advanced": {
+ "customName": "query device",
+ "customNameId": 0,
+ "description": "Get information about the device using its hostname or device id.",
+ "join": [],
+ "note": "Get information about the device using its hostname or device id."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "query_device",
+ "id": "8",
+ "loop": {
+ "conditions": [
+ {
+ "comparisons": [
+ {
+ "conditionIndex": 0,
+ "op": "==",
+ "param": "",
+ "value": ""
+ }
+ ],
+ "conditionIndex": 0,
+ "display": "If",
+ "logic": "and",
+ "type": "if"
+ }
+ ],
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "filter": "format_fql:formatted_data.*",
+ "limit": 50
+ },
+ "requiredParameters": [
+ {
+ "data_type": "numeric",
+ "default": 50,
+ "field": "limit"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "8",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 504
+ },
+ "9": {
+ "data": {
+ "advanced": {
+ "customName": "format file restore report",
+ "customNameId": 0,
+ "description": "Format a summary table with the information gathered from the playbook.",
+ "join": [],
+ "note": "Format a summary table with the information gathered from the playbook."
+ },
+ "functionId": 3,
+ "functionName": "format_file_restore_report",
+ "id": "9",
+ "parameters": [
+ "query_device:action_result.data.*.device_id",
+ "run_admin_command_put:action_result.parameter.data",
+ "run_admin_command_put:action_result.status"
+ ],
+ "template": "Endpoint Files were restored by Splunk SOAR. The table below summarizes the information gathered.\n\n| Device ID | File Path | Restore Status |\n| --- | --- | --- |\n%%\n| {0} | {1} | {2} |\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "9",
+ "type": "format",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1712
+ }
+ },
+ "notes": "Inputs: \ndevice (CrowdStrike Device ID or Hostname)\npath (File path)\nInteractions: CrowdStrike OAuth API\nActions: create session, run admin command, delete session\nOutputs: observables, markdown report"
+ },
+ "input_spec": [
+ {
+ "contains": [
+ "host name"
+ ],
+ "description": "Device ID or hostname of the host where the file should be restored.",
+ "name": "device"
+ },
+ {
+ "contains": [
+ "file name"
+ ],
+ "description": "Name of the file on the endpoint to restore. Assumes a file is present in the File Vault and a matching file_collected artifact exists with the same name from a previous File Collection run.",
+ "name": "file"
+ }
+ ],
+ "output_spec": [
+ {
+ "contains": [],
+ "datapaths": [
+ "host_observables:custom_function:observable_array"
+ ],
+ "deduplicate": false,
+ "description": "An array of observable dictionaries",
+ "metadata": {},
+ "name": "observable"
+ },
+ {
+ "contains": [],
+ "datapaths": [
+ "format_file_restore_report:formatted_data"
+ ],
+ "deduplicate": false,
+ "description": "A report of the devices that were isolated via Splunk SOAR.",
+ "metadata": {},
+ "name": "markdown_report"
+ }
+ ],
+ "playbook_trigger": "artifact_created",
+ "playbook_type": "data",
+ "python_version": "3",
+ "schema": "5.0.15",
+ "version": "6.3.1.178"
+ },
+ "create_time": "2025-04-09T16:44:19.014187+00:00",
+ "draft_mode": false,
+ "labels": [
+ "*"
+ ],
+ "tags": [
+ "CrowdStrike_OAuth_API",
+ "host name",
+ "D3-RF"
+ ]
+}
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_File_Restore.png b/playbooks/CrowdStrike_OAuth_API_File_Restore.png
new file mode 100644
index 0000000000..c897194be8
Binary files /dev/null and b/playbooks/CrowdStrike_OAuth_API_File_Restore.png differ
diff --git a/playbooks/CrowdStrike_OAuth_API_File_Restore.py b/playbooks/CrowdStrike_OAuth_API_File_Restore.py
new file mode 100644
index 0000000000..ad9da8bffe
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_File_Restore.py
@@ -0,0 +1,468 @@
+"""
+Accepts a hostname or device id as well as a file path as input and restores the file from the File Vault to a device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference.
+"""
+
+
+import phantom.rules as phantom
+import json
+from datetime import datetime, timedelta
+
+
+@phantom.playbook_block()
+def on_start(container):
+ phantom.debug('on_start() called')
+
+ # call 'input_filter' block
+ input_filter(container=container)
+
+ return
+
+@phantom.playbook_block()
+def input_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("input_filter() called")
+
+ ################################################################################
+ # Determines if the provided inputs are present in the dataset.
+ ################################################################################
+
+ # collect filtered artifact ids and results for 'if' condition 1
+ matched_artifacts_1, matched_results_1 = phantom.condition(
+ container=container,
+ logical_operator="and",
+ conditions=[
+ ["playbook_input:device", "!=", ""],
+ ["playbook_input:file", "!=", ""]
+ ],
+ name="input_filter:condition_1",
+ delimiter=None)
+
+ # call connected blocks if filtered artifacts or results
+ if matched_artifacts_1 or matched_results_1:
+ format_fql(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_1, filtered_results=matched_results_1)
+
+ return
+
+
+@phantom.playbook_block()
+def host_observables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("host_observables() called")
+
+ ################################################################################
+ # Format a normalized output for each host
+ ################################################################################
+
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.parameter.device_id"], action_results=results)
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.hostname"], action_results=results)
+ run_admin_command_put_result_data = phantom.collect2(container=container, datapath=["run_admin_command_put:action_result.parameter.data","run_admin_command_put:action_result.status","run_admin_command_put:action_result.message"], action_results=results)
+ run_admin_command_cd_result_data = phantom.collect2(container=container, datapath=["run_admin_command_cd:action_result.parameter.data"], action_results=results)
+
+ create_session_parameter_device_id = [item[0] for item in create_session_result_data]
+ query_device_result_item_0 = [item[0] for item in query_device_result_data]
+ run_admin_command_put_parameter_data = [item[0] for item in run_admin_command_put_result_data]
+ run_admin_command_put_result_item_1 = [item[1] for item in run_admin_command_put_result_data]
+ run_admin_command_put_result_message = [item[2] for item in run_admin_command_put_result_data]
+ run_admin_command_cd_parameter_data = [item[0] for item in run_admin_command_cd_result_data]
+
+ host_observables__observable_array = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ host_observables__observable_array = []
+
+ for device_id, hostname, file_name, file_path, status, status_message in zip(create_session_parameter_device_id, query_device_result_item_0, run_admin_command_put_parameter_data, run_admin_command_cd_parameter_data, run_admin_command_put_result_item_1, run_admin_command_put_result_message):
+ # Initialize the observable dictionary
+ observable = {
+ "source": "Crowdstrike OAuth API",
+ "type": "Endpoint",
+ "activity_name": "File Restore",
+ "uid": device_id,
+ "hostname": hostname,
+ "status": status,
+ "status_detail": status_message,
+ "file_artifacts": {
+ "name": file_name,
+ "path": file_path
+ },
+ "d3fend": {
+ "d3f_tactic": "Restore",
+ "d3f_technique": "D3-RF",
+ "version": "1.0.0"
+ }
+ }
+
+ # Add the observable to the array
+ host_observables__observable_array.append(observable)
+
+ # Debug output for verification
+ phantom.debug(host_observables__observable_array)
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="host_observables:observable_array", value=json.dumps(host_observables__observable_array))
+
+ return
+
+
+@phantom.playbook_block()
+def format_fql(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_fql() called")
+
+ ################################################################################
+ # Format the FQL query to get the input device information using its ID or hostname.
+ ################################################################################
+
+ template = """%%\nhostname:['{0}'],device_id:['{0}']\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "playbook_input:device"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_fql")
+
+ query_device(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def query_device(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("query_device() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Get information about the device using its hostname or device id.
+ ################################################################################
+
+ format_fql__as_list = phantom.get_format_data(name="format_fql__as_list")
+
+ parameters = []
+
+ # build parameters list for 'query_device' call
+ for format_fql__item in format_fql__as_list:
+ parameters.append({
+ "limit": 50,
+ "filter": format_fql__item,
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("query device", parameters=parameters, name="query_device", assets=["crowdstrike_oauth_api"], callback=filter_file_artifacts_0)
+
+ return
+
+
+@phantom.playbook_block()
+def format_file_restore_report(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_file_restore_report() called")
+
+ ################################################################################
+ # Format a summary table with the information gathered from the playbook.
+ ################################################################################
+
+ template = """Endpoint Files were restored by Splunk SOAR. The table below summarizes the information gathered.\n\n| Device ID | File Path | Restore Status |\n| --- | --- | --- |\n%%\n| {0} | {1} | {2} |\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "query_device:action_result.data.*.device_id",
+ "run_admin_command_put:action_result.parameter.data",
+ "run_admin_command_put:action_result.status"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_file_restore_report")
+
+ host_observables(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def create_session(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("create_session() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Creates a Real Time Response (RTR) session to interact with the endpoint
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'create_session' call
+ for query_device_result_item in query_device_result_data:
+ if query_device_result_item[0] is not None:
+ parameters.append({
+ "device_id": query_device_result_item[0],
+ "context": {'artifact_id': query_device_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("create session", parameters=parameters, name="create_session", assets=["crowdstrike_oauth_api"], callback=run_admin_command_cd)
+
+ return
+
+
+@phantom.playbook_block()
+def delete_session(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("delete_session() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Closes a Real Time Response (RTR) session to interact with the endpoint
+ ################################################################################
+
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.summary.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'delete_session' call
+ for create_session_result_item in create_session_result_data:
+ if create_session_result_item[0] is not None:
+ parameters.append({
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("delete session", parameters=parameters, name="delete_session", assets=["crowdstrike_oauth_api"], callback=format_file_restore_report)
+
+ return
+
+
+@phantom.playbook_block()
+def run_admin_command_put(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("run_admin_command_put() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Run the put admin command via Real Time Response (RTR)
+ ################################################################################
+
+ container_artifact_data = phantom.collect2(container=container, datapath=["artifact:*.cef.fileName","artifact:*.id"])
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.summary.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'run_admin_command_put' call
+ for container_artifact_item in container_artifact_data:
+ for query_device_result_item in query_device_result_data:
+ for create_session_result_item in create_session_result_data:
+ if query_device_result_item[0] is not None and create_session_result_item[0] is not None:
+ parameters.append({
+ "data": container_artifact_item[0],
+ "command": "put",
+ "device_id": query_device_result_item[0],
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("run admin command", parameters=parameters, name="run_admin_command_put", assets=["crowdstrike_oauth_api"], callback=delete_session)
+
+ return
+
+
+@phantom.playbook_block()
+def filter_file_artifacts_0(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("filter_file_artifacts_0() called")
+
+ ################################################################################
+ # Filter to ensure the requested file to restore was found in the vent artifacts.
+ # This playbook assumes the file to restore has been previously collected by
+ # the CrowdStrike_OAuth_API_File_Collection input playbook.
+ #
+ ################################################################################
+
+ # collect filtered artifact ids and results for 'if' condition 1
+ matched_artifacts_1, matched_results_1 = phantom.condition(
+ container=container,
+ logical_operator="and",
+ conditions=[
+ ["artifact:*.label", "==", "collected_file"],
+ ["playbook_input:file", "in", "artifact:*.cef.fileName"]
+ ],
+ name="filter_file_artifacts_0:condition_1",
+ delimiter=None)
+
+ # call connected blocks if filtered artifacts or results
+ if matched_artifacts_1 or matched_results_1:
+ upload_put_file(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_1, filtered_results=matched_results_1)
+
+ return
+
+
+@phantom.playbook_block()
+def upload_put_file(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("upload_put_file() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Upload the file to restore from the SOAR Vault to Crowdstrike.
+ ################################################################################
+
+ filtered_artifact_0_data_filter_file_artifacts_0 = phantom.collect2(container=container, datapath=["filtered-data:filter_file_artifacts_0:condition_1:artifact:*.cef.vaultId","filtered-data:filter_file_artifacts_0:condition_1:artifact:*.cef.fileName","filtered-data:filter_file_artifacts_0:condition_1:artifact:*.id"])
+
+ parameters = []
+
+ # build parameters list for 'upload_put_file' call
+ for filtered_artifact_0_item_filter_file_artifacts_0 in filtered_artifact_0_data_filter_file_artifacts_0:
+ if filtered_artifact_0_item_filter_file_artifacts_0[0] is not None:
+ parameters.append({
+ "comment": "File restored from Splunk SOAR",
+ "vault_id": filtered_artifact_0_item_filter_file_artifacts_0[0],
+ "file_name": filtered_artifact_0_item_filter_file_artifacts_0[1],
+ "description": "File restored from Splunk SOAR via the CrowdStrike_OAuth_API_File_Restore playbook.",
+ "context": {'artifact_id': filtered_artifact_0_item_filter_file_artifacts_0[2]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("upload put file", parameters=parameters, name="upload_put_file", assets=["crowdstrike_oauth_api"], callback=create_session)
+
+ return
+
+
+@phantom.playbook_block()
+def run_admin_command_cd(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("run_admin_command_cd() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Run the cd admin command via Real Time Response (RTR) to set the working directory
+ # properly for the following put command.
+ ################################################################################
+
+ filtered_artifact_0_data_filter_file_artifacts_0 = phantom.collect2(container=container, datapath=["filtered-data:filter_file_artifacts_0:condition_1:artifact:*.cef.filePath","filtered-data:filter_file_artifacts_0:condition_1:artifact:*.id"])
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.summary.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'run_admin_command_cd' call
+ for filtered_artifact_0_item_filter_file_artifacts_0 in filtered_artifact_0_data_filter_file_artifacts_0:
+ for query_device_result_item in query_device_result_data:
+ for create_session_result_item in create_session_result_data:
+ if query_device_result_item[0] is not None and create_session_result_item[0] is not None:
+ parameters.append({
+ "data": filtered_artifact_0_item_filter_file_artifacts_0[0],
+ "command": "cd",
+ "device_id": query_device_result_item[0],
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("run admin command", parameters=parameters, name="run_admin_command_cd", assets=["crowdstrike_oauth_api"], callback=run_admin_command_put)
+
+ return
+
+
+@phantom.playbook_block()
+def on_finish(container, summary):
+ phantom.debug("on_finish() called")
+
+ format_file_restore_report = phantom.get_format_data(name="format_file_restore_report")
+ host_observables__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="host_observables:observable_array")) != "" else "null") # pylint: disable=used-before-assignment
+
+ output = {
+ "observable": host_observables__observable_array,
+ "markdown_report": format_file_restore_report,
+ }
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_playbook_output_data(output=output)
+
+ return
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_File_Restore.yml b/playbooks/CrowdStrike_OAuth_API_File_Restore.yml
new file mode 100644
index 0000000000..f6d8db71a8
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_File_Restore.yml
@@ -0,0 +1,31 @@
+name: CrowdStrike OAuth API File Restore
+id: 8afd7816-bab2-41f6-a848-395115c46d1c
+version: 1
+date: '2025-06-09'
+author: Christian Cloutier, Splunk
+type: Response
+description: "Accepts a hostname or device id as well as a file path as input and restores the file from the File Vault to a device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference."
+playbook: CrowdStrike_OAuth_API_File_Restore
+how_to_implement: This input playbook requires the CrowdStrike OAuth API connector to be configured. It is designed to work with an endpoint hostname or device id and restore a specific file to the endpoint (based on a previous run of the CrowdStrike_OAuth_API_File_Collection playbook) for use in automation playbooks.
+references: []
+app_list:
+ - CrowdStrike OAuth API
+tags:
+ platform_tags:
+ - "host name"
+ - "device id"
+ - "file name"
+ - "File Restore"
+ - "D3-RF"
+ - "CrowdStrike_OAuth_API"
+ playbook_type: Input
+ vpe_type: Modern
+ playbook_fields: [device,file]
+ product:
+ - Splunk SOAR
+ use_cases:
+ - Response
+ - Malware
+ - Endpoint
+ defend_technique_id:
+ - D3-RF
diff --git a/playbooks/CrowdStrike_OAuth_API_Get_Device_Info.json b/playbooks/CrowdStrike_OAuth_API_Get_Device_Info.json
new file mode 100644
index 0000000000..7671c0bd6f
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Get_Device_Info.json
@@ -0,0 +1,300 @@
+{
+ "blockly": false,
+ "blockly_xml": "",
+ "category": "Attribute Lookup",
+ "coa": {
+ "data": {
+ "description": "Given either a CrowdStrike device id (agentId) or a hostname, will query the device to get the other missing attribute. This enables finding the hostname from a device id or the device id from a hostname and can be used in front of other CrowdStrike custom playbooks for added flexibility.",
+ "edges": [
+ {
+ "id": "port_0_to_port_3",
+ "sourceNode": "0",
+ "sourcePort": "0_out",
+ "targetNode": "3",
+ "targetPort": "3_in"
+ },
+ {
+ "conditions": [
+ {
+ "index": 0
+ }
+ ],
+ "id": "port_3_to_port_2",
+ "sourceNode": "3",
+ "sourcePort": "3_out",
+ "targetNode": "2",
+ "targetPort": "2_in"
+ },
+ {
+ "id": "port_2_to_port_4",
+ "sourceNode": "2",
+ "sourcePort": "2_out",
+ "targetNode": "4",
+ "targetPort": "4_in"
+ },
+ {
+ "id": "port_4_to_port_6",
+ "sourceNode": "4",
+ "sourcePort": "4_out",
+ "targetNode": "6",
+ "targetPort": "6_in"
+ },
+ {
+ "id": "port_6_to_port_1",
+ "sourceNode": "6",
+ "sourcePort": "6_out",
+ "targetNode": "1",
+ "targetPort": "1_in"
+ }
+ ],
+ "hash": "71712736c224e12b9baf5c81a571a973fa95b728",
+ "nodes": {
+ "0": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_start",
+ "id": "0",
+ "type": "start"
+ },
+ "errors": {},
+ "id": "0",
+ "type": "start",
+ "warnings": {},
+ "x": 19.999999999999986,
+ "y": -6.394884621840902e-14
+ },
+ "1": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_finish",
+ "id": "1",
+ "type": "end"
+ },
+ "errors": {},
+ "id": "1",
+ "type": "end",
+ "warnings": {},
+ "x": 20,
+ "y": 800
+ },
+ "2": {
+ "data": {
+ "advanced": {
+ "customName": "format fql",
+ "customNameId": 0,
+ "description": "Format the FQL query to get the input device information using its Device ID or hostname.",
+ "join": [],
+ "note": "Format the FQL query to get the input device information using its Device ID or hostname."
+ },
+ "functionId": 1,
+ "functionName": "format_fql",
+ "id": "2",
+ "parameters": [
+ "playbook_input:device"
+ ],
+ "template": "%%\nhostname:['{0}'],device_id:['{0}']\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "2",
+ "type": "format",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 328
+ },
+ "3": {
+ "data": {
+ "advanced": {
+ "customName": "input filter",
+ "customNameId": 0,
+ "description": "Determines if the provided inputs are present in the dataset.",
+ "join": [],
+ "note": "Determines if the provided inputs are present in the dataset."
+ },
+ "conditions": [
+ {
+ "comparisons": [
+ {
+ "conditionIndex": 0,
+ "op": "!=",
+ "param": "playbook_input:device",
+ "value": ""
+ }
+ ],
+ "conditionIndex": 0,
+ "customName": "playbook inputs are present",
+ "logic": "and"
+ }
+ ],
+ "functionId": 1,
+ "functionName": "input_filter",
+ "id": "3",
+ "type": "filter"
+ },
+ "errors": {},
+ "id": "3",
+ "type": "filter",
+ "warnings": {},
+ "x": 60,
+ "y": 148
+ },
+ "4": {
+ "data": {
+ "action": "query device",
+ "actionType": "investigate",
+ "advanced": {
+ "customName": "query device",
+ "customNameId": 0,
+ "description": "Get information about the CrowdStrike device using its hostname or device id.",
+ "join": [],
+ "note": "Get information about the CrowdStrike device using its hostname or device id."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "query_device",
+ "id": "4",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "filter": "format_fql:formatted_data.*",
+ "limit": 50
+ },
+ "requiredParameters": [
+ {
+ "data_type": "numeric",
+ "default": 50,
+ "field": "limit"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "4",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 504
+ },
+ "6": {
+ "data": {
+ "advanced": {
+ "customName": "format output data",
+ "customNameId": 0,
+ "description": "This code block removes null values returned in some cases for device_id or hostname values. It also provides a default value of \"No results found\" when nothing is returned.",
+ "join": [],
+ "note": "This code block removes null values returned in some cases for device_id or hostname values. It also provides a default value of \"No results found\" when nothing is returned."
+ },
+ "functionId": 1,
+ "functionName": "format_output_data",
+ "id": "6",
+ "inputParameters": [
+ "query_device:action_result.data.*.device_id",
+ "query_device:action_result.data.*.hostname"
+ ],
+ "outputVariables": [
+ "device_ids",
+ "hostnames"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "6",
+ "type": "code",
+ "userCode": "\n format_output_data__device_ids = [x for x in query_device_result_item_0 if x is not None] \n format_output_data__hostnames = [x for x in query_device_result_item_1 if x is not None] \n if not format_output_data__device_ids:\n format_output_data__device_ids = [\"No result found\"]\n if not format_output_data__hostnames:\n format_output_data__hostnames = [\"No result found\"]\n \n",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 660
+ }
+ },
+ "notes": "This playbook can be customized to return other key attributes as required by customers in the End block."
+ },
+ "input_spec": [
+ {
+ "contains": [
+ "host name"
+ ],
+ "description": "Hostname or Device ID of the Crowdstrike Endpoint to query",
+ "name": "device"
+ }
+ ],
+ "output_spec": [
+ {
+ "contains": [
+ "crowdstrike device id"
+ ],
+ "datapaths": [
+ "format_output_data:custom_function:device_ids"
+ ],
+ "deduplicate": true,
+ "description": "The device_id of the CrowdStrike Endpoint",
+ "metadata": {
+ "device_id_drop_none:custom_function_result.data.*.item": {
+ "contains": [
+ "*"
+ ]
+ },
+ "query_device:action_result.data.*.device_id": {
+ "contains": [
+ "crowdstrike device id"
+ ]
+ }
+ },
+ "name": "device_id"
+ },
+ {
+ "contains": [
+ "host name"
+ ],
+ "datapaths": [
+ "format_output_data:custom_function:hostnames"
+ ],
+ "deduplicate": true,
+ "description": "The hostname of the CrowdStrike Endpoint",
+ "metadata": {
+ "hostname_drop_none:custom_function_result.data.*.item": {
+ "contains": [
+ "*"
+ ]
+ },
+ "query_device:action_result.data.*.hostname": {
+ "contains": [
+ "host name"
+ ]
+ }
+ },
+ "name": "hostname"
+ }
+ ],
+ "playbook_trigger": "artifact_created",
+ "playbook_type": "data",
+ "python_version": "3",
+ "schema": "5.0.15",
+ "version": "6.3.1.178"
+ },
+ "create_time": "2025-05-02T17:31:33.721278+00:00",
+ "draft_mode": false,
+ "labels": [
+ "*"
+ ],
+ "tags": [
+ "host name",
+ "device id",
+ "CrowdStrike_OAuth_API"
+ ]
+}
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_Get_Device_Info.png b/playbooks/CrowdStrike_OAuth_API_Get_Device_Info.png
new file mode 100644
index 0000000000..a30c75369f
Binary files /dev/null and b/playbooks/CrowdStrike_OAuth_API_Get_Device_Info.png differ
diff --git a/playbooks/CrowdStrike_OAuth_API_Get_Device_Info.py b/playbooks/CrowdStrike_OAuth_API_Get_Device_Info.py
new file mode 100644
index 0000000000..57bbfbfe67
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Get_Device_Info.py
@@ -0,0 +1,179 @@
+"""
+Given either a CrowdStrike device id (agentId) or a hostname, will query the device to get the other missing attribute. This enables finding the hostname from a device id or the device id from a hostname and can be used in front of other CrowdStrike custom playbooks for added flexibility.
+"""
+
+
+import phantom.rules as phantom
+import json
+from datetime import datetime, timedelta
+
+
+@phantom.playbook_block()
+def on_start(container):
+ phantom.debug('on_start() called')
+
+ # call 'input_filter' block
+ input_filter(container=container)
+
+ return
+
+@phantom.playbook_block()
+def format_fql(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_fql() called")
+
+ ################################################################################
+ # Format the FQL query to get the input device information using its Device ID
+ # or hostname.
+ ################################################################################
+
+ template = """%%\nhostname:['{0}'],device_id:['{0}']\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "playbook_input:device"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_fql")
+
+ query_device(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def input_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("input_filter() called")
+
+ ################################################################################
+ # Determines if the provided inputs are present in the dataset.
+ ################################################################################
+
+ # collect filtered artifact ids and results for 'if' condition 1
+ matched_artifacts_1, matched_results_1 = phantom.condition(
+ container=container,
+ conditions=[
+ ["playbook_input:device", "!=", ""]
+ ],
+ name="input_filter:condition_1",
+ delimiter=None)
+
+ # call connected blocks if filtered artifacts or results
+ if matched_artifacts_1 or matched_results_1:
+ format_fql(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_1, filtered_results=matched_results_1)
+
+ return
+
+
+@phantom.playbook_block()
+def query_device(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("query_device() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Get information about the CrowdStrike device using its hostname or device id.
+ ################################################################################
+
+ format_fql__as_list = phantom.get_format_data(name="format_fql__as_list")
+
+ parameters = []
+
+ # build parameters list for 'query_device' call
+ for format_fql__item in format_fql__as_list:
+ parameters.append({
+ "limit": 50,
+ "filter": format_fql__item,
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("query device", parameters=parameters, name="query_device", assets=["crowdstrike_oauth_api"], callback=format_output_data)
+
+ return
+
+
+@phantom.playbook_block()
+def format_output_data(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_output_data() called")
+
+ ################################################################################
+ # This code block removes null values returned in some cases for device_id or
+ # hostname values. It also provides a default value of "No results found" when
+ # nothing is returned.
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.data.*.hostname"], action_results=results)
+
+ query_device_result_item_0 = [item[0] for item in query_device_result_data]
+ query_device_result_item_1 = [item[1] for item in query_device_result_data]
+
+ format_output_data__device_ids = None
+ format_output_data__hostnames = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ format_output_data__device_ids = [x for x in query_device_result_item_0 if x is not None]
+ format_output_data__hostnames = [x for x in query_device_result_item_1 if x is not None]
+ if not format_output_data__device_ids:
+ format_output_data__device_ids = ["No result found"]
+ if not format_output_data__hostnames:
+ format_output_data__hostnames = ["No result found"]
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="format_output_data:device_ids", value=json.dumps(format_output_data__device_ids))
+ phantom.save_run_data(key="format_output_data:hostnames", value=json.dumps(format_output_data__hostnames))
+
+ return
+
+
+@phantom.playbook_block()
+def on_finish(container, summary):
+ phantom.debug("on_finish() called")
+
+ format_output_data__device_ids = json.loads(_ if (_ := phantom.get_run_data(key="format_output_data:device_ids")) != "" else "null") # pylint: disable=used-before-assignment
+ format_output_data__hostnames = json.loads(_ if (_ := phantom.get_run_data(key="format_output_data:hostnames")) != "" else "null") # pylint: disable=used-before-assignment
+
+ device_id_combined_value = phantom.concatenate(format_output_data__device_ids, dedup=True)
+ hostname_combined_value = phantom.concatenate(format_output_data__hostnames, dedup=True)
+
+ output = {
+ "device_id": device_id_combined_value,
+ "hostname": hostname_combined_value,
+ }
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_playbook_output_data(output=output)
+
+ return
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_Get_Device_Info.yml b/playbooks/CrowdStrike_OAuth_API_Get_Device_Info.yml
new file mode 100644
index 0000000000..ba182e9291
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Get_Device_Info.yml
@@ -0,0 +1,26 @@
+name: CrowdStrike OAuth API File Restore
+id: d97f8a59-fbb0-40db-a4ca-a681000c3b6d
+version: 1
+date: '2025-06-09'
+author: Christian Cloutier, Splunk
+type: Investigation
+description: "Given either a CrowdStrike device id (agentId) or a hostname, will query the device to get the other missing attribute. This enables finding the hostname from a device id or the device id from a hostname and can be used in front of other CrowdStrike custom playbooks for added flexibility."
+playbook: CrowdStrike_OAuth_API_Get_Device_Info
+how_to_implement: This input playbook requires the CrowdStrike OAuth API connector to be configured. It is designed to work with an endpoint hostname or device id and will provide the correspoding information (hostname for a device id, and vice versa) for use in automation playbooks.
+references: []
+app_list:
+ - CrowdStrike OAuth API
+tags:
+ platform_tags:
+ - "host name"
+ - "device id"
+ - "CrowdStrike_OAuth_API"
+ playbook_type: Input
+ vpe_type: Modern
+ playbook_fields: [device]
+ product:
+ - Splunk SOAR
+ use_cases:
+ - Utility
+ - Endpoint
+ defend_technique_id:
diff --git a/playbooks/CrowdStrike_OAuth_API_Network_Isolation.json b/playbooks/CrowdStrike_OAuth_API_Network_Isolation.json
new file mode 100644
index 0000000000..d07f406503
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Network_Isolation.json
@@ -0,0 +1,362 @@
+{
+ "blockly": false,
+ "blockly_xml": "",
+ "category": "Containment",
+ "coa": {
+ "data": {
+ "description": "Accepts a hostname or device id as input and attempts to isolate (quarantine) the device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report from the results. Both reports can be customized based on user preference.",
+ "edges": [
+ {
+ "id": "port_0_to_port_2",
+ "sourceNode": "0",
+ "sourcePort": "0_out",
+ "targetNode": "2",
+ "targetPort": "2_in"
+ },
+ {
+ "id": "port_5_to_port_1",
+ "sourceNode": "5",
+ "sourcePort": "5_out",
+ "targetNode": "1",
+ "targetPort": "1_in"
+ },
+ {
+ "conditions": [
+ {
+ "index": 0
+ }
+ ],
+ "id": "port_2_to_port_7",
+ "sourceNode": "2",
+ "sourcePort": "2_out",
+ "targetNode": "7",
+ "targetPort": "7_in"
+ },
+ {
+ "id": "port_7_to_port_8",
+ "sourceNode": "7",
+ "sourcePort": "7_out",
+ "targetNode": "8",
+ "targetPort": "8_in"
+ },
+ {
+ "id": "port_8_to_port_3",
+ "sourceNode": "8",
+ "sourcePort": "8_out",
+ "targetNode": "3",
+ "targetPort": "3_in"
+ },
+ {
+ "id": "port_9_to_port_5",
+ "sourceNode": "9",
+ "sourcePort": "9_out",
+ "targetNode": "5",
+ "targetPort": "5_in"
+ },
+ {
+ "id": "port_3_to_port_9",
+ "sourceNode": "3",
+ "sourcePort": "3_out",
+ "targetNode": "9",
+ "targetPort": "9_in"
+ }
+ ],
+ "hash": "81702a597569cf52a31eedffe0193bbff4825326",
+ "nodes": {
+ "0": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_start",
+ "id": "0",
+ "type": "start"
+ },
+ "errors": {},
+ "id": "0",
+ "type": "start",
+ "warnings": {},
+ "x": 19.999999999999986,
+ "y": -2.5579538487363607e-13
+ },
+ "1": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_finish",
+ "id": "1",
+ "type": "end"
+ },
+ "errors": {},
+ "id": "1",
+ "type": "end",
+ "warnings": {},
+ "x": 19.999999999999986,
+ "y": 1224
+ },
+ "2": {
+ "data": {
+ "advanced": {
+ "customName": "input filter",
+ "customNameId": 0,
+ "description": "Determines if the provided inputs are present in the dataset.",
+ "join": [],
+ "note": "Determines if the provided inputs are present in the dataset."
+ },
+ "conditions": [
+ {
+ "comparisons": [
+ {
+ "conditionIndex": 0,
+ "op": "!=",
+ "param": "playbook_input:device",
+ "value": ""
+ }
+ ],
+ "conditionIndex": 0,
+ "customName": "input device present",
+ "logic": "and"
+ }
+ ],
+ "functionId": 1,
+ "functionName": "input_filter",
+ "id": "2",
+ "type": "filter"
+ },
+ "errors": {},
+ "id": "2",
+ "type": "filter",
+ "warnings": {},
+ "x": 60,
+ "y": 148
+ },
+ "3": {
+ "data": {
+ "action": "quarantine device",
+ "actionType": "contain",
+ "advanced": {
+ "customName": "quarantine device",
+ "customNameId": 0,
+ "description": "Quarantines device in Crowdstrike given either a Sensor ID (device_id) or hostname.",
+ "join": [],
+ "note": "Quarantines device in Crowdstrike given either a Sensor ID (device_id) or hostname."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "quarantine_device",
+ "id": "3",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "device_id": "query_device:action_result.data.*.device_id",
+ "hostname": ""
+ },
+ "requiredParameters": [],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "3",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 680
+ },
+ "5": {
+ "customCode": null,
+ "data": {
+ "advanced": {
+ "customName": "host observables",
+ "customNameId": 0,
+ "description": "Format a normalized output for each host",
+ "join": [],
+ "note": "Format a normalized output for each host."
+ },
+ "functionId": 1,
+ "functionName": "host_observables",
+ "id": "5",
+ "inputParameters": [
+ "quarantine_device:action_result.parameter.device_id",
+ "query_device:action_result.data.*.hostname",
+ "quarantine_device:action_result.status",
+ "quarantine_device:action_result.message"
+ ],
+ "outputVariables": [
+ "observable_array"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "5",
+ "type": "code",
+ "userCode": " \n host_observables__observable_array = []\n \n for device_id, hostname, status, status_message in zip(quarantine_device_parameter_device_id, query_device_result_item_0, quarantine_device_result_item_1, quarantine_device_result_message):\n # Initialize the observable dictionary\n observable = {\n \"source\": \"Crowdstrike OAuth API\", \n \"type\": \"Endpoint\", \n \"activity_name\": \"Network Isolation\",\n \"uid\": device_id,\n \"hostname\": hostname,\n \"status\": status,\n \"status_detail\": status_message,\n \"d3fend\": {\n \"d3f_tactic\": \"Isolate\",\n \"d3f_technique\": \"D3-NAM\",\n \"version\": \"1.0.0\"\n }\n }\n\n # Add the observable to the array\n host_observables__observable_array.append(observable)\n \n # Debug output for verification\n phantom.debug(host_observables__observable_array)\n \n",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1048
+ },
+ "7": {
+ "data": {
+ "advanced": {
+ "customName": "format fql",
+ "customNameId": 0,
+ "description": "Format the FQL query to get the input device information using its ID or hostname.",
+ "join": [],
+ "note": "Format the FQL query to get the input device information using its ID or hostname."
+ },
+ "functionId": 2,
+ "functionName": "format_fql",
+ "id": "7",
+ "parameters": [
+ "playbook_input:device"
+ ],
+ "template": "%%\nhostname:['{0}'],device_id:['{0}']\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "7",
+ "type": "format",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 328
+ },
+ "8": {
+ "data": {
+ "action": "query device",
+ "actionType": "investigate",
+ "advanced": {
+ "customName": "query device",
+ "customNameId": 0,
+ "description": "Get information about the device to quarantine using its hostname or device id.",
+ "join": [],
+ "note": "Get information about the device to quarantine using its hostname or device id."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "query_device",
+ "id": "8",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "filter": "format_fql:formatted_data.*",
+ "limit": 50
+ },
+ "requiredParameters": [
+ {
+ "data_type": "numeric",
+ "default": 50,
+ "field": "limit"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "8",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 504
+ },
+ "9": {
+ "data": {
+ "advanced": {
+ "customName": "format quarantine report",
+ "customNameId": 0,
+ "description": "Format a summary table with the information gathered from the playbook.",
+ "join": [],
+ "note": "Format a summary table with the information gathered from the playbook."
+ },
+ "functionId": 3,
+ "functionName": "format_quarantine_report",
+ "id": "9",
+ "parameters": [
+ "quarantine_device:action_result.data.*.id",
+ "quarantine_device:action_result.parameter.hostname",
+ "quarantine_device:action_result.data.*.path",
+ "quarantine_device:action_result.status"
+ ],
+ "template": "An attempt to isolate device(s) was performed via Splunk SOAR. The table below summarizes the information gathered.\n\n| Device ID | DNS Name | Device Path | Quarantine Status |\n| --- | --- | --- | --- |\n%%\n| {0} | {1} | {2} | {3} |\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "9",
+ "type": "format",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 856
+ }
+ },
+ "notes": "Inputs: device (CrowdStrike Device ID or Hostname)\nInteractions: CrowdStrike OAuth API\nActions: quarantine device\nOutputs: observables, markdown report"
+ },
+ "input_spec": [
+ {
+ "contains": [
+ "host name"
+ ],
+ "description": "Device ID or hostname of the host to quarantine",
+ "name": "device"
+ }
+ ],
+ "output_spec": [
+ {
+ "contains": [],
+ "datapaths": [
+ "host_observables:custom_function:observable_array"
+ ],
+ "deduplicate": false,
+ "description": "An array of observable dictionaries",
+ "metadata": {},
+ "name": "observable"
+ },
+ {
+ "contains": [],
+ "datapaths": [
+ "format_quarantine_report:formatted_data"
+ ],
+ "deduplicate": false,
+ "description": "A report of the devices that were isolated via Splunk SOAR.",
+ "metadata": {},
+ "name": "markdown_report"
+ }
+ ],
+ "playbook_trigger": "artifact_created",
+ "playbook_type": "data",
+ "python_version": "3",
+ "schema": "5.0.15",
+ "version": "6.3.1.178"
+ },
+ "create_time": "2025-04-08T14:45:48.561243+00:00",
+ "draft_mode": false,
+ "labels": [
+ "*"
+ ],
+ "tags": [
+ "CrowdStrike_OAuth_API",
+ "host name",
+ "containment",
+ "D3-NAM"
+ ]
+}
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_Network_Isolation.png b/playbooks/CrowdStrike_OAuth_API_Network_Isolation.png
new file mode 100644
index 0000000000..23833d36de
Binary files /dev/null and b/playbooks/CrowdStrike_OAuth_API_Network_Isolation.png differ
diff --git a/playbooks/CrowdStrike_OAuth_API_Network_Isolation.py b/playbooks/CrowdStrike_OAuth_API_Network_Isolation.py
new file mode 100644
index 0000000000..233abbe29f
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Network_Isolation.py
@@ -0,0 +1,265 @@
+"""
+Accepts a hostname or device id as input and attempts to isolate (quarantine) the device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report from the results. Both reports can be customized based on user preference.
+"""
+
+
+import phantom.rules as phantom
+import json
+from datetime import datetime, timedelta
+
+
+@phantom.playbook_block()
+def on_start(container):
+ phantom.debug('on_start() called')
+
+ # call 'input_filter' block
+ input_filter(container=container)
+
+ return
+
+@phantom.playbook_block()
+def input_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("input_filter() called")
+
+ ################################################################################
+ # Determines if the provided inputs are present in the dataset.
+ ################################################################################
+
+ # collect filtered artifact ids and results for 'if' condition 1
+ matched_artifacts_1, matched_results_1 = phantom.condition(
+ container=container,
+ conditions=[
+ ["playbook_input:device", "!=", ""]
+ ],
+ name="input_filter:condition_1",
+ delimiter=None)
+
+ # call connected blocks if filtered artifacts or results
+ if matched_artifacts_1 or matched_results_1:
+ format_fql(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_1, filtered_results=matched_results_1)
+
+ return
+
+
+@phantom.playbook_block()
+def quarantine_device(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("quarantine_device() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Quarantines device in Crowdstrike given either a Sensor ID (device_id) or hostname.
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'quarantine_device' call
+ for query_device_result_item in query_device_result_data:
+ parameters.append({
+ "hostname": "",
+ "device_id": query_device_result_item[0],
+ "context": {'artifact_id': query_device_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("quarantine device", parameters=parameters, name="quarantine_device", assets=["crowdstrike_oauth_api"], callback=format_quarantine_report)
+
+ return
+
+
+@phantom.playbook_block()
+def host_observables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("host_observables() called")
+
+ ################################################################################
+ # Format a normalized output for each host
+ ################################################################################
+
+ quarantine_device_result_data = phantom.collect2(container=container, datapath=["quarantine_device:action_result.parameter.device_id","quarantine_device:action_result.status","quarantine_device:action_result.message"], action_results=results)
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.hostname"], action_results=results)
+
+ quarantine_device_parameter_device_id = [item[0] for item in quarantine_device_result_data]
+ quarantine_device_result_item_1 = [item[1] for item in quarantine_device_result_data]
+ quarantine_device_result_message = [item[2] for item in quarantine_device_result_data]
+ query_device_result_item_0 = [item[0] for item in query_device_result_data]
+
+ host_observables__observable_array = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ host_observables__observable_array = []
+
+ for device_id, hostname, status, status_message in zip(quarantine_device_parameter_device_id, query_device_result_item_0, quarantine_device_result_item_1, quarantine_device_result_message):
+ # Initialize the observable dictionary
+ observable = {
+ "source": "Crowdstrike OAuth API",
+ "type": "Endpoint",
+ "activity_name": "Network Isolation",
+ "uid": device_id,
+ "hostname": hostname,
+ "status": status,
+ "status_detail": status_message,
+ "d3fend": {
+ "d3f_tactic": "Isolate",
+ "d3f_technique": "D3-NAM",
+ "version": "1.0.0"
+ }
+ }
+
+ # Add the observable to the array
+ host_observables__observable_array.append(observable)
+
+ # Debug output for verification
+ phantom.debug(host_observables__observable_array)
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="host_observables:observable_array", value=json.dumps(host_observables__observable_array))
+
+ return
+
+
+@phantom.playbook_block()
+def format_fql(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_fql() called")
+
+ ################################################################################
+ # Format the FQL query to get the input device information using its ID or hostname.
+ ################################################################################
+
+ template = """%%\nhostname:['{0}'],device_id:['{0}']\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "playbook_input:device"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_fql")
+
+ query_device(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def query_device(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("query_device() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Get information about the device to quarantine using its hostname or device
+ # id.
+ ################################################################################
+
+ format_fql__as_list = phantom.get_format_data(name="format_fql__as_list")
+
+ parameters = []
+
+ # build parameters list for 'query_device' call
+ for format_fql__item in format_fql__as_list:
+ parameters.append({
+ "limit": 50,
+ "filter": format_fql__item,
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("query device", parameters=parameters, name="query_device", assets=["crowdstrike_oauth_api"], callback=quarantine_device)
+
+ return
+
+
+@phantom.playbook_block()
+def format_quarantine_report(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_quarantine_report() called")
+
+ ################################################################################
+ # Format a summary table with the information gathered from the playbook.
+ ################################################################################
+
+ template = """An attempt to isolate device(s) was performed via Splunk SOAR. The table below summarizes the information gathered.\n\n| Device ID | DNS Name | Device Path | Quarantine Status |\n| --- | --- | --- | --- |\n%%\n| {0} | {1} | {2} | {3} |\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "quarantine_device:action_result.data.*.id",
+ "quarantine_device:action_result.parameter.hostname",
+ "quarantine_device:action_result.data.*.path",
+ "quarantine_device:action_result.status"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_quarantine_report")
+
+ host_observables(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def on_finish(container, summary):
+ phantom.debug("on_finish() called")
+
+ format_quarantine_report = phantom.get_format_data(name="format_quarantine_report")
+ host_observables__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="host_observables:observable_array")) != "" else "null") # pylint: disable=used-before-assignment
+
+ output = {
+ "observable": host_observables__observable_array,
+ "markdown_report": format_quarantine_report,
+ }
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_playbook_output_data(output=output)
+
+ return
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_Network_Isolation.yml b/playbooks/CrowdStrike_OAuth_API_Network_Isolation.yml
new file mode 100644
index 0000000000..198489ecda
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Network_Isolation.yml
@@ -0,0 +1,30 @@
+name: CrowdStrike OAuth API Network Isolation
+id: dd7ef79b-2bfd-4844-821d-a9e8db570d0a
+version: 1
+date: '2025-06-09'
+author: Christian Cloutier, Splunk
+type: Response
+description: "Accepts a hostname or device id as input and attempts to isolate (quarantine) the device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report from the results. Both reports can be customized based on user preference."
+playbook: CrowdStrike_OAuth_API_Network_Isolation
+how_to_implement: This input playbook requires the CrowdStrike OAuth API connector to be configured. It is designed to work with an endpoint hostname or device id and contain the corresponding endpoint for use in automation playbooks.
+references: []
+app_list:
+ - CrowdStrike OAuth API
+tags:
+ platform_tags:
+ - "host name"
+ - "device id"
+ - "Network Isolation"
+ - "D3-NAM"
+ - "CrowdStrike_OAuth_API"
+ playbook_type: Input
+ vpe_type: Modern
+ playbook_fields: [device]
+ product:
+ - Splunk SOAR
+ use_cases:
+ - Response
+ - Malware
+ - Endpoint
+ defend_technique_id:
+ - D3-NAM
diff --git a/playbooks/CrowdStrike_OAuth_API_Network_Restore.json b/playbooks/CrowdStrike_OAuth_API_Network_Restore.json
new file mode 100644
index 0000000000..bd5a7121d0
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Network_Restore.json
@@ -0,0 +1,361 @@
+{
+ "blockly": false,
+ "blockly_xml": "",
+ "category": "Network Restore",
+ "coa": {
+ "data": {
+ "description": "Accepts a hostname or device id as input and tries to restore access for (unquarantines) the device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report of the results. Both can be customized based on user preference.",
+ "edges": [
+ {
+ "id": "port_0_to_port_2",
+ "sourceNode": "0",
+ "sourcePort": "0_out",
+ "targetNode": "2",
+ "targetPort": "2_in"
+ },
+ {
+ "id": "port_5_to_port_1",
+ "sourceNode": "5",
+ "sourcePort": "5_out",
+ "targetNode": "1",
+ "targetPort": "1_in"
+ },
+ {
+ "conditions": [
+ {
+ "index": 0
+ }
+ ],
+ "id": "port_2_to_port_7",
+ "sourceNode": "2",
+ "sourcePort": "2_out",
+ "targetNode": "7",
+ "targetPort": "7_in"
+ },
+ {
+ "id": "port_7_to_port_8",
+ "sourceNode": "7",
+ "sourcePort": "7_out",
+ "targetNode": "8",
+ "targetPort": "8_in"
+ },
+ {
+ "id": "port_9_to_port_5",
+ "sourceNode": "9",
+ "sourcePort": "9_out",
+ "targetNode": "5",
+ "targetPort": "5_in"
+ },
+ {
+ "id": "port_8_to_port_10",
+ "sourceNode": "8",
+ "sourcePort": "8_out",
+ "targetNode": "10",
+ "targetPort": "10_in"
+ },
+ {
+ "id": "port_10_to_port_9",
+ "sourceNode": "10",
+ "sourcePort": "10_out",
+ "targetNode": "9",
+ "targetPort": "9_in"
+ }
+ ],
+ "hash": "45988d55dd87c4bf4f04e4989535ed09243af0a4",
+ "nodes": {
+ "0": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_start",
+ "id": "0",
+ "type": "start"
+ },
+ "errors": {},
+ "id": "0",
+ "type": "start",
+ "warnings": {},
+ "x": 19.999999999999986,
+ "y": -2.5579538487363607e-13
+ },
+ "1": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_finish",
+ "id": "1",
+ "type": "end"
+ },
+ "errors": {},
+ "id": "1",
+ "type": "end",
+ "warnings": {},
+ "x": 19.999999999999986,
+ "y": 1224
+ },
+ "10": {
+ "data": {
+ "action": "unquarantine device",
+ "actionType": "correct",
+ "advanced": {
+ "customName": "unquarantine device",
+ "customNameId": 0,
+ "description": "Remove network isolation from a device in CrowdStrike.",
+ "join": [],
+ "note": "Remove network isolation from a device in CrowdStrike."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "unquarantine_device",
+ "id": "10",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "device_id": "query_device:action_result.data.*.device_id",
+ "hostname": ""
+ },
+ "requiredParameters": [],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "10",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 680
+ },
+ "2": {
+ "data": {
+ "advanced": {
+ "customName": "input filter",
+ "customNameId": 0,
+ "description": "Determines if the provided inputs are present in the dataset.",
+ "join": [],
+ "note": "Determines if the provided inputs are present in the dataset."
+ },
+ "conditions": [
+ {
+ "comparisons": [
+ {
+ "conditionIndex": 0,
+ "op": "!=",
+ "param": "playbook_input:device",
+ "value": ""
+ }
+ ],
+ "conditionIndex": 0,
+ "customName": "input device present",
+ "logic": "and"
+ }
+ ],
+ "functionId": 1,
+ "functionName": "input_filter",
+ "id": "2",
+ "type": "filter"
+ },
+ "errors": {},
+ "id": "2",
+ "type": "filter",
+ "warnings": {},
+ "x": 60,
+ "y": 148
+ },
+ "5": {
+ "customCode": null,
+ "data": {
+ "advanced": {
+ "customName": "host observables",
+ "customNameId": 0,
+ "description": "Format a normalized output for each host",
+ "join": [],
+ "note": "Format a normalized output for each host."
+ },
+ "functionId": 1,
+ "functionName": "host_observables",
+ "id": "5",
+ "inputParameters": [
+ "unquarantine_device:action_result.parameter.device_id",
+ "query_device:action_result.data.*.hostname",
+ "unquarantine_device:action_result.status",
+ "unquarantine_device:action_result.message"
+ ],
+ "outputVariables": [
+ "observable_array"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "5",
+ "type": "code",
+ "userCode": " \n host_observables__observable_array = []\n \n for device_id, hostname, status, status_message in zip(unquarantine_device_parameter_device_id, query_device_result_item_0, unquarantine_device_result_item_1, unquarantine_device_result_message):\n # Initialize the observable dictionary\n observable = {\n \"source\": \"Crowdstrike OAuth API\", \n \"type\": \"Endpoint\",\n \"activity_name\": \"Network Restore\",\n \"uid\": device_id,\n \"hostname\": hostname,\n \"status\": status,\n \"status_detail\": status_message,\n \"d3fend\": {\n \"d3f_tactic\": \"Restore\",\n \"d3f_technique\": \"D3-RNA\",\n \"version\": \"1.0.0\"\n }\n }\n\n # Add the observable to the array\n host_observables__observable_array.append(observable)\n \n # Debug output for verification\n phantom.debug(host_observables__observable_array)\n \n",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 1040
+ },
+ "7": {
+ "data": {
+ "advanced": {
+ "customName": "format fql",
+ "customNameId": 0,
+ "description": "Format the FQL query to get the input device information using its ID or hostname.",
+ "join": [],
+ "note": "Format the FQL query to get the input device information using its ID or hostname."
+ },
+ "functionId": 2,
+ "functionName": "format_fql",
+ "id": "7",
+ "parameters": [
+ "playbook_input:device"
+ ],
+ "template": "%%\nhostname:['{0}'],device_id:['{0}']\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "7",
+ "type": "format",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 328
+ },
+ "8": {
+ "data": {
+ "action": "query device",
+ "actionType": "investigate",
+ "advanced": {
+ "customName": "query device",
+ "customNameId": 0,
+ "description": "Get information about the device to unquarantine using its hostname or device id.",
+ "join": [],
+ "note": "Get information about the device to unquarantine using its hostname or device id."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "query_device",
+ "id": "8",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "filter": "format_fql:formatted_data.*",
+ "limit": 50
+ },
+ "requiredParameters": [
+ {
+ "data_type": "numeric",
+ "default": 50,
+ "field": "limit"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "8",
+ "type": "action",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 504
+ },
+ "9": {
+ "data": {
+ "advanced": {
+ "customName": "format unquarantine report",
+ "customNameId": 0,
+ "description": "Format a summary table with the information gathered from the playbook.",
+ "join": [],
+ "note": "Format a summary table with the information gathered from the playbook."
+ },
+ "functionId": 3,
+ "functionName": "format_unquarantine_report",
+ "id": "9",
+ "parameters": [
+ "unquarantine_device:action_result.parameter.device_id",
+ "unquarantine_device:action_result.parameter.hostname",
+ "unquarantine_device:action_result.data.*.path",
+ "unquarantine_device:action_result.status"
+ ],
+ "template": "An attempt to remove device(s) from isolation was performed via Splunk SOAR. The table below summarizes the information gathered.\n\n| Device ID | DNS Name | Device Path | Unquarantine Status |\n| --- | --- | --- | --- |\n%%\n| {0} | {1} | {2} | {3} |\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "9",
+ "type": "format",
+ "warnings": {},
+ "x": 1.4210854715202004e-14,
+ "y": 856
+ }
+ },
+ "notes": "Inputs: device (CrowdStrike Device ID or Hostname)\nInteractions: CrowdStrike OAuth API\nActions: unquarantine device\nOutputs: observables, markdown report"
+ },
+ "input_spec": [
+ {
+ "contains": [],
+ "description": "Device ID or hostname of the host to quarantine",
+ "name": "device"
+ }
+ ],
+ "output_spec": [
+ {
+ "contains": [],
+ "datapaths": [
+ "host_observables:custom_function:observable_array"
+ ],
+ "deduplicate": false,
+ "description": "An array of observable dictionaries",
+ "metadata": {},
+ "name": "observable"
+ },
+ {
+ "contains": [],
+ "datapaths": [
+ "format_unquarantine_report:formatted_data"
+ ],
+ "deduplicate": false,
+ "description": "A report of the devices that were isolated via Splunk SOAR.",
+ "metadata": {},
+ "name": "markdown_report"
+ }
+ ],
+ "playbook_trigger": "artifact_created",
+ "playbook_type": "data",
+ "python_version": "3",
+ "schema": "5.0.15",
+ "version": "6.3.1.178"
+ },
+ "create_time": "2025-04-08T13:59:03.337026+00:00",
+ "draft_mode": false,
+ "labels": [
+ "*"
+ ],
+ "tags": [
+ "CrowdStrike_OAuth_API",
+ "host name",
+ "restore",
+ "D3-RNA"
+ ]
+}
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_Network_Restore.png b/playbooks/CrowdStrike_OAuth_API_Network_Restore.png
new file mode 100644
index 0000000000..a51676b87f
Binary files /dev/null and b/playbooks/CrowdStrike_OAuth_API_Network_Restore.png differ
diff --git a/playbooks/CrowdStrike_OAuth_API_Network_Restore.py b/playbooks/CrowdStrike_OAuth_API_Network_Restore.py
new file mode 100644
index 0000000000..3d303d59d5
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Network_Restore.py
@@ -0,0 +1,265 @@
+"""
+Accepts a hostname or device id as input and tries to restore access for (unquarantines) the device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report of the results. Both can be customized based on user preference.
+"""
+
+
+import phantom.rules as phantom
+import json
+from datetime import datetime, timedelta
+
+
+@phantom.playbook_block()
+def on_start(container):
+ phantom.debug('on_start() called')
+
+ # call 'input_filter' block
+ input_filter(container=container)
+
+ return
+
+@phantom.playbook_block()
+def input_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("input_filter() called")
+
+ ################################################################################
+ # Determines if the provided inputs are present in the dataset.
+ ################################################################################
+
+ # collect filtered artifact ids and results for 'if' condition 1
+ matched_artifacts_1, matched_results_1 = phantom.condition(
+ container=container,
+ conditions=[
+ ["playbook_input:device", "!=", ""]
+ ],
+ name="input_filter:condition_1",
+ delimiter=None)
+
+ # call connected blocks if filtered artifacts or results
+ if matched_artifacts_1 or matched_results_1:
+ format_fql(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_1, filtered_results=matched_results_1)
+
+ return
+
+
+@phantom.playbook_block()
+def host_observables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("host_observables() called")
+
+ ################################################################################
+ # Format a normalized output for each host
+ ################################################################################
+
+ unquarantine_device_result_data = phantom.collect2(container=container, datapath=["unquarantine_device:action_result.parameter.device_id","unquarantine_device:action_result.status","unquarantine_device:action_result.message"], action_results=results)
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.hostname"], action_results=results)
+
+ unquarantine_device_parameter_device_id = [item[0] for item in unquarantine_device_result_data]
+ unquarantine_device_result_item_1 = [item[1] for item in unquarantine_device_result_data]
+ unquarantine_device_result_message = [item[2] for item in unquarantine_device_result_data]
+ query_device_result_item_0 = [item[0] for item in query_device_result_data]
+
+ host_observables__observable_array = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ host_observables__observable_array = []
+
+ for device_id, hostname, status, status_message in zip(unquarantine_device_parameter_device_id, query_device_result_item_0, unquarantine_device_result_item_1, unquarantine_device_result_message):
+ # Initialize the observable dictionary
+ observable = {
+ "source": "Crowdstrike OAuth API",
+ "type": "Endpoint",
+ "activity_name": "Network Restore",
+ "uid": device_id,
+ "hostname": hostname,
+ "status": status,
+ "status_detail": status_message,
+ "d3fend": {
+ "d3f_tactic": "Restore",
+ "d3f_technique": "D3-RNA",
+ "version": "1.0.0"
+ }
+ }
+
+ # Add the observable to the array
+ host_observables__observable_array.append(observable)
+
+ # Debug output for verification
+ phantom.debug(host_observables__observable_array)
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="host_observables:observable_array", value=json.dumps(host_observables__observable_array))
+
+ return
+
+
+@phantom.playbook_block()
+def format_fql(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_fql() called")
+
+ ################################################################################
+ # Format the FQL query to get the input device information using its ID or hostname.
+ ################################################################################
+
+ template = """%%\nhostname:['{0}'],device_id:['{0}']\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "playbook_input:device"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_fql")
+
+ query_device(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def query_device(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("query_device() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Get information about the device to unquarantine using its hostname or device
+ # id.
+ ################################################################################
+
+ format_fql__as_list = phantom.get_format_data(name="format_fql__as_list")
+
+ parameters = []
+
+ # build parameters list for 'query_device' call
+ for format_fql__item in format_fql__as_list:
+ parameters.append({
+ "limit": 50,
+ "filter": format_fql__item,
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("query device", parameters=parameters, name="query_device", assets=["crowdstrike_oauth_api"], callback=unquarantine_device)
+
+ return
+
+
+@phantom.playbook_block()
+def format_unquarantine_report(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_unquarantine_report() called")
+
+ ################################################################################
+ # Format a summary table with the information gathered from the playbook.
+ ################################################################################
+
+ template = """An attempt to remove device(s) from isolation was performed via Splunk SOAR. The table below summarizes the information gathered.\n\n| Device ID | DNS Name | Device Path | Unquarantine Status |\n| --- | --- | --- | --- |\n%%\n| {0} | {1} | {2} | {3} |\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "unquarantine_device:action_result.parameter.device_id",
+ "unquarantine_device:action_result.parameter.hostname",
+ "unquarantine_device:action_result.data.*.path",
+ "unquarantine_device:action_result.status"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_unquarantine_report")
+
+ host_observables(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def unquarantine_device(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("unquarantine_device() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Remove network isolation from a device in CrowdStrike.
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'unquarantine_device' call
+ for query_device_result_item in query_device_result_data:
+ parameters.append({
+ "hostname": "",
+ "device_id": query_device_result_item[0],
+ "context": {'artifact_id': query_device_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("unquarantine device", parameters=parameters, name="unquarantine_device", assets=["crowdstrike_oauth_api"], callback=format_unquarantine_report)
+
+ return
+
+
+@phantom.playbook_block()
+def on_finish(container, summary):
+ phantom.debug("on_finish() called")
+
+ format_unquarantine_report = phantom.get_format_data(name="format_unquarantine_report")
+ host_observables__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="host_observables:observable_array")) != "" else "null") # pylint: disable=used-before-assignment
+
+ output = {
+ "observable": host_observables__observable_array,
+ "markdown_report": format_unquarantine_report,
+ }
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_playbook_output_data(output=output)
+
+ return
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_Network_Restore.yml b/playbooks/CrowdStrike_OAuth_API_Network_Restore.yml
new file mode 100644
index 0000000000..0d5a0d1da5
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Network_Restore.yml
@@ -0,0 +1,30 @@
+name: CrowdStrike OAuth API Network Restore
+id: 92ddd3da-02ac-40d1-84fb-1bc8c9fdc1da
+version: 1
+date: '2025-06-09'
+author: Christian Cloutier, Splunk
+type: Response
+description: "Accepts a hostname or device id as input and tries to restore access for (unquarantines) the device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report of the results. Both can be customized based on user preference."
+playbook: CrowdStrike_OAuth_API_Network_Restore
+how_to_implement: This input playbook requires the CrowdStrike OAuth API connector to be configured. It is designed to work with an endpoint hostname or device id and remove the corresponding endpoint from containment for use in automation playbooks.
+references: []
+app_list:
+ - CrowdStrike OAuth API
+tags:
+ platform_tags:
+ - "host name"
+ - "device id"
+ - "Network Restore"
+ - "D3-RNA"
+ - "CrowdStrike_OAuth_API"
+ playbook_type: Input
+ vpe_type: Modern
+ playbook_fields: [device]
+ product:
+ - Splunk SOAR
+ use_cases:
+ - Response
+ - Malware
+ - Endpoint
+ defend_technique_id:
+ - D3-RNA
diff --git a/playbooks/CrowdStrike_OAuth_API_Process_Termination.json b/playbooks/CrowdStrike_OAuth_API_Process_Termination.json
new file mode 100644
index 0000000000..762a7ce35d
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Process_Termination.json
@@ -0,0 +1,497 @@
+{
+ "blockly": false,
+ "blockly_xml": "",
+ "category": "Process Termination",
+ "coa": {
+ "data": {
+ "description": "Accepts a hostname or device id as well as one or more process IDs as input and terminates those process(es) on a device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference. Note that the Markdown report can report a status of \"success\" even when a particular PID is not actually killed. Rely on the observable output if you need to reliably check that.",
+ "edges": [
+ {
+ "id": "port_0_to_port_2",
+ "sourceNode": "0",
+ "sourcePort": "0_out",
+ "targetNode": "2",
+ "targetPort": "2_in"
+ },
+ {
+ "id": "port_5_to_port_1",
+ "sourceNode": "5",
+ "sourcePort": "5_out",
+ "targetNode": "1",
+ "targetPort": "1_in"
+ },
+ {
+ "conditions": [
+ {
+ "index": 0
+ }
+ ],
+ "id": "port_2_to_port_7",
+ "sourceNode": "2",
+ "sourcePort": "2_out",
+ "targetNode": "7",
+ "targetPort": "7_in"
+ },
+ {
+ "id": "port_7_to_port_8",
+ "sourceNode": "7",
+ "sourcePort": "7_out",
+ "targetNode": "8",
+ "targetPort": "8_in"
+ },
+ {
+ "id": "port_9_to_port_5",
+ "sourceNode": "9",
+ "sourcePort": "9_out",
+ "targetNode": "5",
+ "targetPort": "5_in"
+ },
+ {
+ "id": "port_8_to_port_14",
+ "sourceNode": "8",
+ "sourcePort": "8_out",
+ "targetNode": "14",
+ "targetPort": "14_in"
+ },
+ {
+ "id": "port_14_to_port_15",
+ "sourceNode": "14",
+ "sourcePort": "14_out",
+ "targetNode": "15",
+ "targetPort": "15_in"
+ },
+ {
+ "id": "port_16_to_port_9",
+ "sourceNode": "16",
+ "sourcePort": "16_out",
+ "targetNode": "9",
+ "targetPort": "9_in"
+ },
+ {
+ "id": "port_15_to_port_16",
+ "sourceNode": "15",
+ "sourcePort": "15_out",
+ "targetNode": "16",
+ "targetPort": "16_in"
+ }
+ ],
+ "hash": "efab1b66882b7dade8150195f014248b9575d250",
+ "nodes": {
+ "0": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_start",
+ "id": "0",
+ "type": "start"
+ },
+ "errors": {},
+ "id": "0",
+ "type": "start",
+ "warnings": {},
+ "x": 20,
+ "y": 0
+ },
+ "1": {
+ "data": {
+ "advanced": {
+ "join": []
+ },
+ "functionName": "on_finish",
+ "id": "1",
+ "type": "end"
+ },
+ "errors": {},
+ "id": "1",
+ "type": "end",
+ "warnings": {},
+ "x": 20,
+ "y": 1576
+ },
+ "14": {
+ "data": {
+ "action": "create session",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "create session",
+ "customNameId": 0,
+ "description": "Create an RTR session on the selected Crowdstrike endpoint.",
+ "join": [],
+ "note": "Create an RTR session on the selected Crowdstrike endpoint."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "create_session",
+ "id": "14",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "device_id": "query_device:action_result.data.*.device_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "device_id"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "14",
+ "type": "action",
+ "warnings": {},
+ "x": 0,
+ "y": 680
+ },
+ "15": {
+ "data": {
+ "action": "run admin command",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "run admin command",
+ "customNameId": 0,
+ "description": "Runs the kill command against a Crowdstrike endpoint via an RTR session to terminate the selected process PID.",
+ "join": [],
+ "note": "Runs the kill command against a Crowdstrike endpoint via an RTR session to terminate the selected process PID."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "run_admin_command",
+ "id": "15",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "command": "kill",
+ "data": "playbook_input:pid",
+ "device_id": "query_device:action_result.data.*.device_id",
+ "session_id": "create_session:action_result.data.*.resources.*.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "command"
+ },
+ {
+ "data_type": "string",
+ "field": "device_id"
+ },
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "tab": "byAction",
+ "type": "action"
+ },
+ "errors": {},
+ "id": "15",
+ "type": "action",
+ "warnings": {},
+ "x": 0,
+ "y": 856
+ },
+ "16": {
+ "data": {
+ "action": "delete session",
+ "actionType": "generic",
+ "advanced": {
+ "customName": "delete session",
+ "customNameId": 0,
+ "description": "Deletes an RTR session on the selected Crowdstrike endpoint.",
+ "join": [],
+ "note": "Deletes an RTR session on the selected Crowdstrike endpoint."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "delete_session",
+ "id": "16",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "session_id": "create_session:action_result.summary.session_id"
+ },
+ "requiredParameters": [
+ {
+ "data_type": "string",
+ "field": "session_id"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "16",
+ "type": "action",
+ "warnings": {},
+ "x": 0,
+ "y": 1032
+ },
+ "2": {
+ "data": {
+ "advanced": {
+ "customName": "input filter",
+ "customNameId": 0,
+ "description": "Determines if the provided inputs are present in the dataset.",
+ "join": [],
+ "note": "Determines if the provided inputs are present in the dataset."
+ },
+ "conditions": [
+ {
+ "comparisons": [
+ {
+ "conditionIndex": 0,
+ "op": "!=",
+ "param": "playbook_input:device",
+ "value": ""
+ },
+ {
+ "op": "!=",
+ "param": "playbook_input:pid",
+ "value": ""
+ }
+ ],
+ "conditionIndex": 0,
+ "customName": "input device present",
+ "logic": "and"
+ }
+ ],
+ "functionId": 1,
+ "functionName": "input_filter",
+ "id": "2",
+ "type": "filter"
+ },
+ "errors": {},
+ "id": "2",
+ "type": "filter",
+ "warnings": {},
+ "x": 60,
+ "y": 148
+ },
+ "5": {
+ "customCode": null,
+ "data": {
+ "advanced": {
+ "customName": "host observables",
+ "customNameId": 0,
+ "description": "Format a normalized output for each host",
+ "join": [],
+ "note": "Format a normalized output for each host."
+ },
+ "functionId": 1,
+ "functionName": "host_observables",
+ "id": "5",
+ "inputParameters": [
+ "query_device:action_result.data.*.device_id",
+ "query_device:action_result.data.*.hostname",
+ "run_admin_command:action_result.parameter.data",
+ "run_admin_command:action_result.data"
+ ],
+ "outputVariables": [
+ "observable_array"
+ ],
+ "type": "code"
+ },
+ "errors": {},
+ "id": "5",
+ "type": "code",
+ "userCode": " \n host_observables__observable_array = []\n i = 0\n \n for system in run_admin_command_result_item_1:\n # Handle case where a single host is passed with multiple PIDs to kill.\n device_id = query_device_result_item_0[i] if i < len(query_device_result_item_0) else query_device_result_item_0[0]\n hostname = query_device_result_item_1[i] if i < len(query_device_result_item_1) else query_device_result_item_1[0]\n\n # Initialize the observable dictionary\n observable = {\n \"source\": \"Crowdstrike OAuth API\",\n \"type\": \"Endpoint\",\n \"activity_name\": \"Process Termination\",\n \"uid\": device_id,\n \"hostname\": hostname, \n \"processes\": [],\n \"d3fend\": {\n \"d3f_tactic\": \"Evict\",\n \"d3f_technique\": \"D3-PT\",\n \"version\": \"1.0.0\"\n }\n }\n\n for process in system[0][\"resources\"]:\n pid = run_admin_command_parameter_data[i]\n status = \"success\" if process[\"stdout\"] and not process[\"stderr\"] else \"failed\"\n message = process[\"stdout\"].strip() if process[\"stdout\"] else process[\"stderr\"].rstrip()\n observable[\"processes\"].append(\n {\n \"pid\": pid,\n \"status\": status,\n \"message\": message\n }\n )\n \n # Add the observable to the array\n host_observables__observable_array.append(observable)\n i+=1\n \n # Debug output for verification\n #phantom.debug(host_observables__observable_array)\n \n",
+ "warnings": {},
+ "x": 0,
+ "y": 1400
+ },
+ "7": {
+ "data": {
+ "advanced": {
+ "customName": "format fql",
+ "customNameId": 0,
+ "description": "Format the FQL query to get the input device information using its ID or hostname.",
+ "join": [],
+ "note": "Format the FQL query to get the input device information using its ID or hostname."
+ },
+ "functionId": 2,
+ "functionName": "format_fql",
+ "id": "7",
+ "parameters": [
+ "playbook_input:device"
+ ],
+ "template": "%%\nhostname:['{0}'],device_id:['{0}']\n%%",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "7",
+ "type": "format",
+ "warnings": {},
+ "x": 0,
+ "y": 340
+ },
+ "8": {
+ "data": {
+ "action": "query device",
+ "actionType": "investigate",
+ "advanced": {
+ "customName": "query device",
+ "customNameId": 0,
+ "description": "Get information about the device where the process should be terminated using its hostname or device id.",
+ "join": [],
+ "note": "Get information about the device where the process should be terminated using its hostname or device id."
+ },
+ "connector": "CrowdStrike OAuth API",
+ "connectorConfigs": [
+ "crowdstrike_oauth_api"
+ ],
+ "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232",
+ "connectorVersion": "v1",
+ "functionId": 1,
+ "functionName": "query_device",
+ "id": "8",
+ "loop": {
+ "enabled": false,
+ "exitAfterUnit": "m",
+ "exitAfterValue": 10,
+ "exitConditionEnabled": false,
+ "exitLoopAfter": 2,
+ "pauseUnit": "m",
+ "pauseValue": 2
+ },
+ "parameters": {
+ "filter": "format_fql:formatted_data.*",
+ "limit": 50
+ },
+ "requiredParameters": [
+ {
+ "data_type": "numeric",
+ "default": 50,
+ "field": "limit"
+ }
+ ],
+ "type": "action"
+ },
+ "errors": {},
+ "id": "8",
+ "type": "action",
+ "warnings": {},
+ "x": 0,
+ "y": 504
+ },
+ "9": {
+ "data": {
+ "advanced": {
+ "customName": "format process termination report",
+ "customNameId": 0,
+ "description": "Format a summary table with the information gathered from the playbook.",
+ "join": [],
+ "note": "Format a summary table with the information gathered from the playbook."
+ },
+ "functionId": 3,
+ "functionName": "format_process_termination_report",
+ "id": "9",
+ "parameters": [
+ "query_device:action_result.data.*.device_id",
+ "query_device:action_result.data.*.hostname",
+ "filtered-data:input_filter:condition_1:playbook_input:pid",
+ "run_admin_command:action_result.status",
+ "run_admin_command:action_result.data.*.resources.*.stdout",
+ "run_admin_command:action_result.data.*.resources.*.stderr"
+ ],
+ "template": "Devices processes were terminated via Splunk SOAR. The table below summarizes the information gathered.\n\n%%\n| Device ID | DNS Name | Process ID | Request Status |\n| --- | --- | --- | --- |\n| {0} | {1} | {2} | {3} |\n\n```\n{4}{5}\n```\n%%\n",
+ "type": "format"
+ },
+ "errors": {},
+ "id": "9",
+ "type": "format",
+ "warnings": {},
+ "x": 0,
+ "y": 1208
+ }
+ },
+ "notes": "Inputs: device (CrowdStrike Device ID or Hostname)\npid (one or more process IDs to terminate)\nInteractions: CrowdStrike OAuth API\nActions: run admin command (kill)\nOutputs: observables, markdown report"
+ },
+ "input_spec": [
+ {
+ "contains": [
+ "host name"
+ ],
+ "description": "Device ID or hostname of the host to quarantine",
+ "name": "device"
+ },
+ {
+ "contains": [],
+ "description": "Process ID of the process to terminate",
+ "name": "pid"
+ }
+ ],
+ "output_spec": [
+ {
+ "contains": [],
+ "datapaths": [
+ "host_observables:custom_function:observable_array"
+ ],
+ "deduplicate": false,
+ "description": "An array of observable dictionaries",
+ "metadata": {},
+ "name": "observable"
+ },
+ {
+ "contains": [],
+ "datapaths": [
+ "format_process_termination_report:formatted_data"
+ ],
+ "deduplicate": false,
+ "description": "A report of the devices that were isolated via Splunk SOAR.",
+ "metadata": {},
+ "name": "markdown_report"
+ }
+ ],
+ "playbook_trigger": "artifact_created",
+ "playbook_type": "data",
+ "python_version": "3",
+ "schema": "5.0.15",
+ "version": "6.3.1.178"
+ },
+ "create_time": "2025-04-09T14:46:26.643171+00:00",
+ "draft_mode": false,
+ "labels": [
+ "*"
+ ],
+ "tags": [
+ "CrowdStrike_OAuth_API",
+ "host name",
+ "D3-PT"
+ ]
+}
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_Process_Termination.png b/playbooks/CrowdStrike_OAuth_API_Process_Termination.png
new file mode 100644
index 0000000000..4d9a556ab2
Binary files /dev/null and b/playbooks/CrowdStrike_OAuth_API_Process_Termination.png differ
diff --git a/playbooks/CrowdStrike_OAuth_API_Process_Termination.py b/playbooks/CrowdStrike_OAuth_API_Process_Termination.py
new file mode 100644
index 0000000000..e5819616de
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Process_Termination.py
@@ -0,0 +1,368 @@
+"""
+Accepts a hostname or device id as well as one or more process IDs as input and terminates those process(es) on a device in Crowdstrike. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference. Note that the Markdown report can report a status of "success" even when a particular PID is not actually killed. Rely on the observable output if you need to reliably check that.
+"""
+
+
+import phantom.rules as phantom
+import json
+from datetime import datetime, timedelta
+
+
+@phantom.playbook_block()
+def on_start(container):
+ phantom.debug('on_start() called')
+
+ # call 'input_filter' block
+ input_filter(container=container)
+
+ return
+
+@phantom.playbook_block()
+def input_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("input_filter() called")
+
+ ################################################################################
+ # Determines if the provided inputs are present in the dataset.
+ ################################################################################
+
+ # collect filtered artifact ids and results for 'if' condition 1
+ matched_artifacts_1, matched_results_1 = phantom.condition(
+ container=container,
+ logical_operator="and",
+ conditions=[
+ ["playbook_input:device", "!=", ""],
+ ["playbook_input:pid", "!=", ""]
+ ],
+ name="input_filter:condition_1",
+ delimiter=None)
+
+ # call connected blocks if filtered artifacts or results
+ if matched_artifacts_1 or matched_results_1:
+ format_fql(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_1, filtered_results=matched_results_1)
+
+ return
+
+
+@phantom.playbook_block()
+def host_observables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("host_observables() called")
+
+ ################################################################################
+ # Format a normalized output for each host
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.data.*.hostname"], action_results=results)
+ run_admin_command_result_data = phantom.collect2(container=container, datapath=["run_admin_command:action_result.parameter.data","run_admin_command:action_result.data"], action_results=results)
+
+ query_device_result_item_0 = [item[0] for item in query_device_result_data]
+ query_device_result_item_1 = [item[1] for item in query_device_result_data]
+ run_admin_command_parameter_data = [item[0] for item in run_admin_command_result_data]
+ run_admin_command_result_item_1 = [item[1] for item in run_admin_command_result_data]
+
+ host_observables__observable_array = None
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ host_observables__observable_array = []
+ i = 0
+
+ for system in run_admin_command_result_item_1:
+ # Handle case where a single host is passed with multiple PIDs to kill.
+ device_id = query_device_result_item_0[i] if i < len(query_device_result_item_0) else query_device_result_item_0[0]
+ hostname = query_device_result_item_1[i] if i < len(query_device_result_item_1) else query_device_result_item_1[0]
+
+ # Initialize the observable dictionary
+ observable = {
+ "source": "Crowdstrike OAuth API",
+ "type": "Endpoint",
+ "activity_name": "Process Termination",
+ "uid": device_id,
+ "hostname": hostname,
+ "processes": [],
+ "d3fend": {
+ "d3f_tactic": "Evict",
+ "d3f_technique": "D3-PT",
+ "version": "1.0.0"
+ }
+ }
+
+ for process in system[0]["resources"]:
+ pid = run_admin_command_parameter_data[i]
+ status = "success" if process["stdout"] and not process["stderr"] else "failed"
+ message = process["stdout"].strip() if process["stdout"] else process["stderr"].rstrip()
+ observable["processes"].append(
+ {
+ "pid": pid,
+ "status": status,
+ "message": message
+ }
+ )
+
+ # Add the observable to the array
+ host_observables__observable_array.append(observable)
+ i+=1
+
+ # Debug output for verification
+ #phantom.debug(host_observables__observable_array)
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_run_data(key="host_observables:observable_array", value=json.dumps(host_observables__observable_array))
+
+ return
+
+
+@phantom.playbook_block()
+def format_fql(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_fql() called")
+
+ ################################################################################
+ # Format the FQL query to get the input device information using its ID or hostname.
+ ################################################################################
+
+ template = """%%\nhostname:['{0}'],device_id:['{0}']\n%%"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "playbook_input:device"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_fql")
+
+ query_device(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def query_device(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("query_device() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Get information about the device where the process should be terminated using
+ # its hostname or device id.
+ ################################################################################
+
+ format_fql__as_list = phantom.get_format_data(name="format_fql__as_list")
+
+ parameters = []
+
+ # build parameters list for 'query_device' call
+ for format_fql__item in format_fql__as_list:
+ parameters.append({
+ "limit": 50,
+ "filter": format_fql__item,
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("query device", parameters=parameters, name="query_device", assets=["crowdstrike_oauth_api"], callback=create_session)
+
+ return
+
+
+@phantom.playbook_block()
+def format_process_termination_report(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("format_process_termination_report() called")
+
+ ################################################################################
+ # Format a summary table with the information gathered from the playbook.
+ ################################################################################
+
+ template = """Devices processes were terminated via Splunk SOAR. The table below summarizes the information gathered.\n\n%%\n| Device ID | DNS Name | Process ID | Request Status |\n| --- | --- | --- | --- |\n| {0} | {1} | {2} | {3} |\n\n```\n{4}{5}\n```\n%%\n"""
+
+ # parameter list for template variable replacement
+ parameters = [
+ "query_device:action_result.data.*.device_id",
+ "query_device:action_result.data.*.hostname",
+ "filtered-data:input_filter:condition_1:playbook_input:pid",
+ "run_admin_command:action_result.status",
+ "run_admin_command:action_result.data.*.resources.*.stdout",
+ "run_admin_command:action_result.data.*.resources.*.stderr"
+ ]
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.format(container=container, template=template, parameters=parameters, name="format_process_termination_report")
+
+ host_observables(container=container)
+
+ return
+
+
+@phantom.playbook_block()
+def create_session(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("create_session() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Create an RTR session on the selected Crowdstrike endpoint.
+ ################################################################################
+
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'create_session' call
+ for query_device_result_item in query_device_result_data:
+ if query_device_result_item[0] is not None:
+ parameters.append({
+ "device_id": query_device_result_item[0],
+ "context": {'artifact_id': query_device_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("create session", parameters=parameters, name="create_session", assets=["crowdstrike_oauth_api"], callback=run_admin_command)
+
+ return
+
+
+@phantom.playbook_block()
+def run_admin_command(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("run_admin_command() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Runs the kill command against a Crowdstrike endpoint via an RTR session to terminate
+ # the selected process PID.
+ ################################################################################
+
+ playbook_input_pid = phantom.collect2(container=container, datapath=["playbook_input:pid"])
+ query_device_result_data = phantom.collect2(container=container, datapath=["query_device:action_result.data.*.device_id","query_device:action_result.parameter.context.artifact_id"], action_results=results)
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.data.*.resources.*.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'run_admin_command' call
+ for playbook_input_pid_item in playbook_input_pid:
+ for query_device_result_item in query_device_result_data:
+ for create_session_result_item in create_session_result_data:
+ if query_device_result_item[0] is not None and create_session_result_item[0] is not None:
+ parameters.append({
+ "data": playbook_input_pid_item[0],
+ "command": "kill",
+ "device_id": query_device_result_item[0],
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("run admin command", parameters=parameters, name="run_admin_command", assets=["crowdstrike_oauth_api"], callback=delete_session)
+
+ return
+
+
+@phantom.playbook_block()
+def delete_session(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, loop_state_json=None, **kwargs):
+ phantom.debug("delete_session() called")
+
+ # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
+
+ ################################################################################
+ # Deletes an RTR session on the selected Crowdstrike endpoint.
+ ################################################################################
+
+ create_session_result_data = phantom.collect2(container=container, datapath=["create_session:action_result.summary.session_id","create_session:action_result.parameter.context.artifact_id"], action_results=results)
+
+ parameters = []
+
+ # build parameters list for 'delete_session' call
+ for create_session_result_item in create_session_result_data:
+ if create_session_result_item[0] is not None:
+ parameters.append({
+ "session_id": create_session_result_item[0],
+ "context": {'artifact_id': create_session_result_item[1]},
+ })
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.act("delete session", parameters=parameters, name="delete_session", assets=["crowdstrike_oauth_api"], callback=format_process_termination_report)
+
+ return
+
+
+@phantom.playbook_block()
+def on_finish(container, summary):
+ phantom.debug("on_finish() called")
+
+ format_process_termination_report = phantom.get_format_data(name="format_process_termination_report")
+ host_observables__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="host_observables:observable_array")) != "" else "null") # pylint: disable=used-before-assignment
+
+ output = {
+ "observable": host_observables__observable_array,
+ "markdown_report": format_process_termination_report,
+ }
+
+ ################################################################################
+ ## Custom Code Start
+ ################################################################################
+
+ # Write your custom code here...
+
+ ################################################################################
+ ## Custom Code End
+ ################################################################################
+
+ phantom.save_playbook_output_data(output=output)
+
+ return
\ No newline at end of file
diff --git a/playbooks/CrowdStrike_OAuth_API_Process_Termination.yml b/playbooks/CrowdStrike_OAuth_API_Process_Termination.yml
new file mode 100644
index 0000000000..c9f78aa193
--- /dev/null
+++ b/playbooks/CrowdStrike_OAuth_API_Process_Termination.yml
@@ -0,0 +1,31 @@
+name: CrowdStrike OAuth API Process Termination
+id: 4a64ee9b-09fa-42fa-8b43-0de75908ff08
+version: 1
+date: '2025-06-09'
+author: Christian Cloutier, Splunk
+type: Response
+description: "Accepts a hostname or device id as well as one or more process IDs as input and terminates those process(es) on a device in CrowdStrike. We then generate an observable report as well as a Markdown formatted report. Both reports can be customized based on user preference. Note that the Markdown report can report a status of "success" even when a particular PID is not actually killed. Rely on the observable output if you need to reliably check that."
+playbook: CrowdStrike_OAuth_API_Process_Termination
+how_to_implement: This input playbook requires the CrowdStrike OAuth API connector to be configured. It is designed to work with an endpoint hostname or device id and terminate the corresponding process on the endpoint for use in automation playbooks.
+references: []
+app_list:
+ - CrowdStrike OAuth API
+tags:
+ platform_tags:
+ - "host name"
+ - "device id"
+ - "pid"
+ - "Process Termination"
+ - "D3-PT"
+ - "CrowdStrike_OAuth_API"
+ playbook_type: Input
+ vpe_type: Modern
+ playbook_fields: [device,pid]
+ product:
+ - Splunk SOAR
+ use_cases:
+ - Response
+ - Malware
+ - Endpoint
+ defend_technique_id:
+ - D3-PT