diff --git a/playbooks/AD_LDAP_Entity_Attribute_Lookup.json b/playbooks/AD_LDAP_Entity_Attribute_Lookup.json new file mode 100644 index 0000000000..b7b18a7fcc --- /dev/null +++ b/playbooks/AD_LDAP_Entity_Attribute_Lookup.json @@ -0,0 +1,682 @@ +{ + "blockly": false, + "blockly_xml": "", + "category": "Attribute Lookup", + "coa": { + "data": { + "description": "Accepts a user or device and looks up the most recent attributes and groups for that user or device. This playbook produces a normalized output for each user and device.", + "edges": [ + { + "id": "port_3_to_port_1", + "sourceNode": "3", + "sourcePort": "3_out", + "targetNode": "1", + "targetPort": "1_in" + }, + { + "id": "port_10_to_port_3", + "sourceNode": "10", + "sourcePort": "10_out", + "targetNode": "3", + "targetPort": "3_in" + }, + { + "id": "port_8_to_port_10", + "sourceNode": "8", + "sourcePort": "8_out", + "targetNode": "10", + "targetPort": "10_in" + }, + { + "id": "port_0_to_port_14", + "sourceNode": "0", + "sourcePort": "0_out", + "targetNode": "14", + "targetPort": "14_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_14_to_port_2", + "sourceNode": "14", + "sourcePort": "14_out", + "targetNode": "2", + "targetPort": "2_in" + }, + { + "id": "port_15_to_port_16", + "sourceNode": "15", + "sourcePort": "15_out", + "targetNode": "16", + "targetPort": "16_in" + }, + { + "id": "port_16_to_port_17", + "sourceNode": "16", + "sourcePort": "16_out", + "targetNode": "17", + "targetPort": "17_in" + }, + { + "id": "port_17_to_port_1", + "sourceNode": "17", + "sourcePort": "17_out", + "targetNode": "1", + "targetPort": "1_in" + }, + { + "id": "port_2_to_port_18", + "sourceNode": "2", + "sourcePort": "2_out", + "targetNode": "18", + "targetPort": "18_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_18_to_port_8", + "sourceNode": "18", + "sourcePort": "18_out", + "targetNode": "8", + "targetPort": "8_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_19_to_port_15", + "sourceNode": "19", + "sourcePort": "19_out", + "targetNode": "15", + "targetPort": "15_in" + }, + { + "conditions": [ + { + "index": 1 + } + ], + "id": "port_14_to_port_20", + "sourceNode": "14", + "sourcePort": "14_out", + "targetNode": "20", + "targetPort": "20_in" + }, + { + "id": "port_20_to_port_11", + "sourceNode": "20", + "sourcePort": "20_out", + "targetNode": "11", + "targetPort": "11_in" + }, + { + "id": "port_11_to_port_19", + "sourceNode": "11", + "sourcePort": "11_out", + "targetNode": "19", + "targetPort": "19_in" + } + ], + "hash": "1161e9894971caa9d512c12b7eb803d24bf949bf", + "nodes": { + "0": { + "data": { + "advanced": { + "join": [] + }, + "functionName": "on_start", + "id": "0", + "type": "start" + }, + "errors": {}, + "id": "0", + "type": "start", + "warnings": {}, + "x": 190, + "y": 0 + }, + "1": { + "data": { + "advanced": { + "join": [] + }, + "functionId": 1, + "functionName": "on_finish", + "id": "1", + "type": "end" + }, + "errors": {}, + "id": "1", + "type": "end", + "userCode": "\n # This function is called after all actions are completed.\n # summary of all the action and/or all details of actions\n # can be collected here.\n\n # summary_json = phantom.get_summary()\n # if 'result' in summary_json:\n # for action_result in summary_json['result']:\n # if 'action_run_id' in action_result:\n # action_results = phantom.get_action_results(action_run_id=action_result['action_run_id'], result_data=False, flatten=False)\n # phantom.debug(action_results)\n\n", + "warnings": {}, + "x": 190, + "y": 1414 + }, + "10": { + "data": { + "action": "run query", + "actionType": "investigate", + "advanced": { + "customName": "get user group attributes", + "customNameId": 0, + "description": "Get details for the groups to which the user belongs.", + "join": [], + "note": "Get details for the groups to which the user belongs." + }, + "connector": "AD LDAP", + "connectorConfigs": [ + "ad_ldap" + ], + "connectorId": "a5730e5d-a396-4695-92c2-35ff391aaf45", + "connectorVersion": "v1", + "functionId": 1, + "functionName": "get_user_group_attributes", + "id": "10", + "parameters": { + "attributes": "description;name;memberOf;distinguishedName", + "filter": { + "functionId": 1, + "parameters": [ + "merge_user_groups:custom_function:list" + ], + "template": "(|\n%%\n(distinguishedName={0})\n%%\n)\n" + } + }, + "requiredParameters": [ + { + "data_type": "string", + "field": "filter" + }, + { + "data_type": "string", + "default": "sAMAccountName", + "field": "attributes" + } + ], + "type": "action" + }, + "errors": {}, + "id": "10", + "type": "action", + "warnings": {}, + "x": 0, + "y": 1042 + }, + "11": { + "data": { + "action": "run query", + "actionType": "investigate", + "advanced": { + "customName": "get device attributes", + "customNameId": 0, + "description": "Query for the device attributes for filtered playbook inputs.", + "join": [], + "note": "Query for the device attributes for filtered playbook inputs." + }, + "connector": "AD LDAP", + "connectorConfigs": [ + "ad_ldap" + ], + "connectorId": "a5730e5d-a396-4695-92c2-35ff391aaf45", + "connectorVersion": "v1", + "functionId": 3, + "functionName": "get_device_attributes", + "id": "11", + "parameters": { + "attributes": "description;distinguishedName;objectSid;memberOf;name;sAMAccountName", + "filter": "format_device_query:formatted_data.*" + }, + "requiredParameters": [ + { + "data_type": "string", + "field": "filter" + }, + { + "data_type": "string", + "default": "sAMAccountName", + "field": "attributes" + } + ], + "type": "action" + }, + "errors": {}, + "id": "11", + "type": "action", + "warnings": {}, + "x": 340, + "y": 506 + }, + "14": { + "data": { + "advanced": { + "customName": "playbook inputs filter", + "customNameId": 0, + "description": "Filter inputs to route inputs to appropriate actions.", + "join": [], + "note": "Filter inputs to route inputs to appropriate actions." + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "playbook_input:user", + "value": "None" + } + ], + "conditionIndex": 0, + "customName": "user", + "logic": "and" + }, + { + "comparisons": [ + { + "conditionIndex": 1, + "op": "!=", + "param": "playbook_input:device", + "value": "None" + } + ], + "conditionIndex": 1, + "customName": "device", + "logic": "and" + } + ], + "functionId": 1, + "functionName": "playbook_inputs_filter", + "id": "14", + "type": "filter" + }, + "errors": {}, + "id": "14", + "type": "filter", + "warnings": { + "config": [ + "Reconfigure invalid datapath." + ] + }, + "x": 230, + "y": 148 + }, + "15": { + "data": { + "advanced": { + "customName": "merge device groups", + "customNameId": 0, + "description": "Merge device groups, from preceding action block results, into a single deduplicated list.", + "join": [], + "note": "Merge device groups, from preceding action block results, into a single deduplicated list." + }, + "customDatapaths": { + "filter_device_results": { + "condition_1:get_device_attributes:action_result.data.*.entries.*.attributes.memberOf": { + "contains": [], + "isCustomDatapath": true, + "label": "condition_1:get_device_attributes:action_result.data.*.entries.*.attributes.memberOf", + "value": "filtered-data:filter_device_results:condition_1:get_device_attributes:action_result.data.*.entries.*.attributes.memberOf" + } + } + }, + "functionId": 3, + "functionName": "merge_device_groups", + "id": "15", + "inputParameters": [ + "filtered-data:filter_device_results:condition_1:get_device_attributes:action_result.data.*.entries.*.attributes.memberof" + ], + "outputVariables": [ + "list" + ], + "type": "code" + }, + "errors": {}, + "id": "15", + "type": "code", + "userCode": "\n group_list = []\n for group in filtered_result_0_data___entries___attributes_memberof:\n group_list.extend(group)\n \n merge_device_groups__list = list(set(group_list))\n\n", + "warnings": {}, + "x": 340, + "y": 864 + }, + "16": { + "data": { + "action": "run query", + "actionType": "investigate", + "advanced": { + "customName": "get device group attributes", + "customNameId": 0, + "description": "Get details for the groups to which the device belongs.", + "join": [], + "note": "Get details for the groups to which the device belongs." + }, + "connector": "AD LDAP", + "connectorConfigs": [ + "ad_ldap" + ], + "connectorId": "a5730e5d-a396-4695-92c2-35ff391aaf45", + "connectorVersion": "v1", + "functionId": 2, + "functionName": "get_device_group_attributes", + "id": "16", + "parameters": { + "attributes": "description;name;memberOf;distinguishedName", + "filter": { + "functionId": 2, + "parameters": [ + "merge_device_groups:custom_function:list" + ], + "template": "(|\n%%\n(distinguishedName={0})\n%%\n)" + } + }, + "requiredParameters": [ + { + "data_type": "string", + "field": "filter" + }, + { + "data_type": "string", + "default": "sAMAccountName", + "field": "attributes" + } + ], + "type": "action" + }, + "errors": {}, + "id": "16", + "type": "action", + "warnings": {}, + "x": 340, + "y": 1042 + }, + "17": { + "data": { + "advanced": { + "customName": "format device outputs", + "customNameId": 0, + "description": "Format a normalized output for each device.", + "join": [], + "note": "Format a normalized output for each device." + }, + "functionId": 4, + "functionName": "format_device_outputs", + "id": "17", + "inputParameters": [ + "filtered-data:filter_device_results:condition_1:get_device_attributes:action_result.data.*.entries.*.attributes", + "filtered-data:filter_device_results:condition_1:get_device_attributes:action_result.parameter.filter", + "get_device_group_attributes:action_result.data.*.entries.*.attributes" + ], + "outputVariables": [ + "observable_array" + ], + "type": "code" + }, + "errors": {}, + "id": "17", + "type": "code", + "userCode": " import re\n \n format_device_outputs__observable_array = []\n \n def extract_domain_name(dn):\n dn_rex = re.search(r'(DC=.+)', dn)\n if dn_rex:\n dc_list = dn_rex.group(1).replace('DC=', '').split(',')\n return '.'.join(dc_list)\n \n def extract_ou(dn):\n dn_rex = re.search(r'(OU=.+?),DC', dn)\n if dn_rex:\n ou_list = dn_rex.group(1).replace('OU=', '').split(',')\n return ou_list\n \n def extract_pb_input_from_filter(filter_query_list):\n pb_input_list = []\n for query in filter_query_list:\n query_rex = re.search(r'sAMAccountName=([^\\)]+)', query)\n hostname = query_rex.group(1).replace('*', '')\n pb_input_list.append(hostname)\n return pb_input_list\n \n # create dictionary of group attributes\n group_dict = {}\n for group in get_device_group_attributes_result_item_0:\n group_dict[group['distinguishedname']] = group\n \n pb_input_hostname_list = extract_pb_input_from_filter(filtered_result_0_parameter_filter)\n # create normalized output dictionary\n for pb_input, device in zip(pb_input_hostname_list, filtered_result_0_data___entries___attributes):\n device_dict = {\n \"desc\": device['description'],\n \"domain\": extract_domain_name(device['distinguishedname']),\n \"hostname\": device['name'],\n \"name\": device['samaccountname'],\n \"type\": \"Unknown\",\n \"type_id\": 0,\n \"uid\": device['objectsid'] \n }\n \n # use OUs as labels\n ou_list = extract_ou(device['distinguishedname'])\n if ou_list:\n device_dict['labels'] = ou_list\n \n # lookup groups\n group_list = []\n for group in device['memberof']:\n if group_dict.get(group):\n group_details = group_dict[group]\n if not group_details['description']:\n group_details['description'].append(\"None\")\n temp_group_dict = {\n \"type\": \"Domain\",\n \"desc\": group_details['description'][0],\n \"name\": group_details['name'],\n \"privileges\": group_details['memberof'],\n \"uid\": group_details['distinguishedname']\n }\n # drop values with []\n for k,v in temp_group_dict.copy().items():\n if v == []:\n temp_group_dict.pop(k)\n group_list.append(temp_group_dict)\n if group_list:\n device_dict['groups'] = group_list\n\n \n final_output = {\n \"type\": \"host name\",\n \"value\": pb_input,\n \"attributes\": device_dict,\n \"source\": \"AD LDAP\"\n }\n format_device_outputs__observable_array.append(final_output)\n \n # phantom.debug(format_device_outputs__observable_array)\n\n", + "warnings": {}, + "x": 340, + "y": 1236 + }, + "18": { + "data": { + "advanced": { + "customName": "filter user results", + "customNameId": 0, + "join": [] + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": ">", + "param": "get_user_attributes:action_result.summary.total_objects", + "value": "0" + } + ], + "conditionIndex": 0, + "customName": "user results exist", + "logic": "and" + } + ], + "functionId": 2, + "functionName": "filter_user_results", + "id": "18", + "type": "filter" + }, + "errors": {}, + "id": "18", + "type": "filter", + "warnings": {}, + "x": 60, + "y": 684 + }, + "19": { + "data": { + "advanced": { + "customName": "filter device results", + "customNameId": 0, + "join": [] + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": ">", + "param": "get_device_attributes:action_result.summary.total_objects", + "value": "0" + } + ], + "conditionIndex": 0, + "customName": "device results exist", + "logic": "and" + } + ], + "functionId": 3, + "functionName": "filter_device_results", + "id": "19", + "type": "filter" + }, + "errors": {}, + "id": "19", + "type": "filter", + "warnings": {}, + "x": 400, + "y": 684 + }, + "2": { + "data": { + "action": "get attributes", + "actionType": "investigate", + "advanced": { + "customName": "get user attributes", + "customNameId": 0, + "description": "Query for the user attributes for filtered playbook inputs.", + "join": [], + "note": "Query for the user attributes for filtered playbook inputs." + }, + "connector": "AD LDAP", + "connectorConfigs": [ + "ad_ldap" + ], + "connectorId": "a5730e5d-a396-4695-92c2-35ff391aaf45", + "connectorVersion": "v1", + "functionId": 1, + "functionName": "get_user_attributes", + "id": "2", + "parameters": { + "attributes": "userPrincipalName;mail;accountExpires;name;memberOf;title;department;manager;sAMAccountName;distinguishedName;mobile;ipPhone;homePhone;telephoneNumber;otherMobile;otherIpPhone;otherHomePhone;otherTelephone;whenCreated;objectSid;objectGUID", + "principals": "filtered-data:playbook_inputs_filter:condition_1:playbook_input:user" + }, + "requiredParameters": [ + { + "data_type": "string", + "default": "sAMAccountName", + "field": "attributes" + }, + { + "data_type": "string", + "field": "principals" + } + ], + "type": "action" + }, + "errors": {}, + "id": "2", + "type": "action", + "warnings": {}, + "x": 0, + "y": 506 + }, + "20": { + "data": { + "advanced": { + "customName": "format device query", + "customNameId": 0, + "description": "Iterate through the playbook inputs and format a list of queries.", + "join": [], + "note": "Iterate through the playbook inputs and format a list of queries." + }, + "functionId": 1, + "functionName": "format_device_query", + "id": "20", + "parameters": [ + "filtered-data:playbook_inputs_filter:condition_2:playbook_input:device" + ], + "template": "%%\n(&(objectCategory=computer)(|(sAMAccountName={0}*)(distinguishedName={0}*)(userPrincipalName={0}*)))\n%%", + "type": "format" + }, + "errors": {}, + "id": "20", + "type": "format", + "warnings": {}, + "x": 340, + "y": 328 + }, + "3": { + "data": { + "advanced": { + "customName": "format user outputs", + "customNameId": 0, + "description": "Format a normalized output for each user.", + "join": [], + "note": "Format a normalized output for each user." + }, + "functionId": 1, + "functionName": "format_user_outputs", + "id": "3", + "inputParameters": [ + "filtered-data:filter_user_results:condition_1:get_user_attributes:action_result.data.*.entries.*.attributes", + "filtered-data:filter_user_results:condition_1:get_user_attributes:action_result.parameter.principals", + "get_user_group_attributes:action_result.data.*.entries.*.attributes" + ], + "outputVariables": [ + "observable_array" + ], + "type": "code" + }, + "errors": {}, + "id": "3", + "type": "code", + "userCode": " \n format_user_outputs__observable_array = []\n \n # create dictionary of group attributes\n group_dict = {}\n for group in get_user_group_attributes_result_item_0:\n group_dict[group['distinguishedname']] = group\n \n # create normalized output dictionary\n for pb_input, user in zip(filtered_result_0_parameter_principals, filtered_result_0_data___entries___attributes):\n user_dict = {\n \"account_type\": \"LDAP Account\",\n \"account_type_id\": 1,\n \"account_uid\": user['userPrincipalName'],\n \"domain\": user['userPrincipalName'].split('@')[1],\n \"email_addr\": user['mail'],\n \"end_date\": user['accountExpires'],\n \"full_name\": user['name'],\n \"labels\": [\n user['title'],\n user['department']\n ],\n \"managed_by\": user['manager'],\n \"name\": user['sAMAccountName'],\n \"start_date\": user['whenCreated'],\n \"type\": \"User\",\n \"type_id\": 1,\n \"uid\": user['distinguishedName'],\n \"uuid\": user['objectGUID']\n }\n phones = [\n user['mobile'],\n user['ipPhone'],\n user['homePhone'],\n user['telephoneNumber'],\n user['otherMobile'],\n user['otherIpPhone'],\n user['otherHomePhone'],\n user['otherTelephone']\n ]\n \n # lookup groups\n group_list = []\n for group in user['memberOf']:\n if group_dict.get(group):\n group_details = group_dict[group]\n if not group_details['description']:\n group_details['description'].append(\"None\")\n temp_group_dict = {\n \"type\": \"Domain\",\n \"desc\": group_details['description'][0],\n \"name\": group_details['name'],\n \"privileges\": group_details['memberof'],\n \"uid\": group_details['distinguishedname']\n }\n # drop values with []\n for k,v in temp_group_dict.copy().items():\n if v == []:\n temp_group_dict.pop(k)\n group_list.append(temp_group_dict)\n \n \n if group_list:\n user_dict['groups'] = group_list\n \n # clean up phone numbers\n phones = [phone for phone in phones if phone != []]\n if phones:\n user_dict['phones'] = phones\n \n \n final_output = {\n \"type\": \"user\",\n \"value\": pb_input,\n \"attributes\": user_dict,\n \"soure\": \"AD LDAP\"\n }\n format_user_outputs__observable_array.append(final_output)\n \n # phantom.debug(format_user_outputs__observable_array)\n \n\n", + "warnings": {}, + "x": 0, + "y": 1236 + }, + "8": { + "data": { + "advanced": { + "customName": "merge user groups", + "customNameId": 0, + "description": "Merge user groups, from preceding action block results, into a single deduplicated list.", + "join": [], + "note": "Merge user groups, from preceding action block results, into a single deduplicated list." + }, + "customDatapaths": { + "filter_user_results": { + "condition_1:get_user_attributes:action_result.data.*.entries.*.attributes.memberOf": { + "contains": [], + "isCustomDatapath": true, + "label": "condition_1:get_user_attributes:action_result.data.*.entries.*.attributes.memberOf", + "value": "filtered-data:filter_user_results:condition_1:get_user_attributes:action_result.data.*.entries.*.attributes.memberOf" + } + } + }, + "functionId": 2, + "functionName": "merge_user_groups", + "id": "8", + "inputParameters": [ + "filtered-data:filter_user_results:condition_1:get_user_attributes:action_result.data.*.entries.*.attributes.memberOf" + ], + "outputVariables": [ + "list" + ], + "type": "code" + }, + "errors": {}, + "id": "8", + "type": "code", + "userCode": "\n \n group_list = []\n for group in filtered_result_0_data___entries___attributes_memberof:\n group_list.extend(group)\n \n merge_user_groups__list = list(set(group_list))\n \n", + "warnings": {}, + "x": 0, + "y": 864 + } + }, + "notes": "Inputs: user, device\nInteractions: AD LDAP\nActions: get attributes, run query\nOutputs: report, observables" + }, + "input_spec": [ + { + "contains": [ + "user name" + ], + "description": "A user identity in Active Directory such as sAMAccountName, userPrincipalName, or distinguishedName", + "name": "user" + }, + { + "contains": [ + "host name" + ], + "description": "A device in Active Directory.", + "name": "device" + } + ], + "output_spec": [ + { + "contains": [], + "datapaths": [ + "format_user_outputs:custom_function:observable_array", + "format_device_outputs:custom_function:observable_array" + ], + "deduplicate": false, + "description": "An array of observable dictionaries ", + "metadata": {}, + "name": "observable" + } + ], + "playbook_type": "data", + "python_version": "3", + "schema": "5.0.9", + "version": "6.0.0.114895" + }, + "create_time": "2023-03-03T18:13:51.717485+00:00", + "draft_mode": false, + "labels": [ + "*" + ], + "tags": [ + "attributes", + "user", + "device", + "ad_ldap" + ] +} \ No newline at end of file diff --git a/playbooks/AD_LDAP_Entity_Attribute_Lookup.png b/playbooks/AD_LDAP_Entity_Attribute_Lookup.png new file mode 100644 index 0000000000..fbf3e74588 Binary files /dev/null and b/playbooks/AD_LDAP_Entity_Attribute_Lookup.png differ diff --git a/playbooks/AD_LDAP_Entity_Attribute_Lookup.py b/playbooks/AD_LDAP_Entity_Attribute_Lookup.py new file mode 100644 index 0000000000..635072270d --- /dev/null +++ b/playbooks/AD_LDAP_Entity_Attribute_Lookup.py @@ -0,0 +1,610 @@ +""" +Accepts a user or device and looks up the most recent attributes and groups for that user or device. This playbook produces a normalized output for each user and device. +""" + + +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 'playbook_inputs_filter' block + playbook_inputs_filter(container=container) + + return + +@phantom.playbook_block() +def get_user_attributes(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("get_user_attributes() called") + + # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED'))) + + ################################################################################ + # Query for the user attributes for filtered playbook inputs. + ################################################################################ + + filtered_input_0_user = phantom.collect2(container=container, datapath=["filtered-data:playbook_inputs_filter:condition_1:playbook_input:user"]) + + parameters = [] + + # build parameters list for 'get_user_attributes' call + for filtered_input_0_user_item in filtered_input_0_user: + if filtered_input_0_user_item[0] is not None: + parameters.append({ + "attributes": "userPrincipalName;mail;accountExpires;name;memberOf;title;department;manager;sAMAccountName;distinguishedName;mobile;ipPhone;homePhone;telephoneNumber;otherMobile;otherIpPhone;otherHomePhone;otherTelephone;whenCreated;objectSid;objectGUID", + "principals": filtered_input_0_user_item[0], + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.act("get attributes", parameters=parameters, name="get_user_attributes", assets=["ad_ldap"], callback=filter_user_results) + + return + + +@phantom.playbook_block() +def format_user_outputs(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("format_user_outputs() called") + + ################################################################################ + # Format a normalized output for each user. + ################################################################################ + + filtered_result_0_data_filter_user_results = phantom.collect2(container=container, datapath=["filtered-data:filter_user_results:condition_1:get_user_attributes:action_result.data.*.entries.*.attributes","filtered-data:filter_user_results:condition_1:get_user_attributes:action_result.parameter.principals"]) + get_user_group_attributes_result_data = phantom.collect2(container=container, datapath=["get_user_group_attributes:action_result.data.*.entries.*.attributes"], action_results=results) + + filtered_result_0_data___entries___attributes = [item[0] for item in filtered_result_0_data_filter_user_results] + filtered_result_0_parameter_principals = [item[1] for item in filtered_result_0_data_filter_user_results] + get_user_group_attributes_result_item_0 = [item[0] for item in get_user_group_attributes_result_data] + + format_user_outputs__observable_array = None + + ################################################################################ + ## Custom Code Start + ################################################################################ + + format_user_outputs__observable_array = [] + + # create dictionary of group attributes + group_dict = {} + for group in get_user_group_attributes_result_item_0: + group_dict[group['distinguishedname']] = group + + # create normalized output dictionary + for pb_input, user in zip(filtered_result_0_parameter_principals, filtered_result_0_data___entries___attributes): + user_dict = { + "account_type": "LDAP Account", + "account_type_id": 1, + "account_uid": user['userPrincipalName'], + "domain": user['userPrincipalName'].split('@')[1], + "email_addr": user['mail'], + "end_date": user['accountExpires'], + "full_name": user['name'], + "labels": [ + user['title'], + user['department'] + ], + "managed_by": user['manager'], + "name": user['sAMAccountName'], + "start_date": user['whenCreated'], + "type": "User", + "type_id": 1, + "uid": user['distinguishedName'], + "uuid": user['objectGUID'] + } + phones = [ + user['mobile'], + user['ipPhone'], + user['homePhone'], + user['telephoneNumber'], + user['otherMobile'], + user['otherIpPhone'], + user['otherHomePhone'], + user['otherTelephone'] + ] + + # lookup groups + group_list = [] + for group in user['memberOf']: + if group_dict.get(group): + group_details = group_dict[group] + if not group_details['description']: + group_details['description'].append("None") + temp_group_dict = { + "type": "Domain", + "desc": group_details['description'][0], + "name": group_details['name'], + "privileges": group_details['memberof'], + "uid": group_details['distinguishedname'] + } + # drop values with [] + for k,v in temp_group_dict.copy().items(): + if v == []: + temp_group_dict.pop(k) + group_list.append(temp_group_dict) + + + if group_list: + user_dict['groups'] = group_list + + # clean up phone numbers + phones = [phone for phone in phones if phone != []] + if phones: + user_dict['phones'] = phones + + + final_output = { + "type": "user", + "value": pb_input, + "attributes": user_dict, + "soure": "AD LDAP" + } + format_user_outputs__observable_array.append(final_output) + + # phantom.debug(format_user_outputs__observable_array) + + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_run_data(key="format_user_outputs:observable_array", value=json.dumps(format_user_outputs__observable_array)) + + return + + +@phantom.playbook_block() +def merge_user_groups(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("merge_user_groups() called") + + ################################################################################ + # Merge user groups, from preceding action block results, into a single deduplicated + # list. + ################################################################################ + + filtered_result_0_data_filter_user_results = phantom.collect2(container=container, datapath=["filtered-data:filter_user_results:condition_1:get_user_attributes:action_result.data.*.entries.*.attributes.memberOf"]) + + filtered_result_0_data___entries___attributes_memberof = [item[0] for item in filtered_result_0_data_filter_user_results] + + merge_user_groups__list = None + + ################################################################################ + ## Custom Code Start + ################################################################################ + + + group_list = [] + for group in filtered_result_0_data___entries___attributes_memberof: + group_list.extend(group) + + merge_user_groups__list = list(set(group_list)) + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_run_data(key="merge_user_groups:list", value=json.dumps(merge_user_groups__list)) + + get_user_group_attributes(container=container) + + return + + +@phantom.playbook_block() +def get_user_group_attributes(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("get_user_group_attributes() called") + + # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED'))) + + filter_formatted_string = phantom.format( + container=container, + template="""(|\n%%\n(distinguishedName={0})\n%%\n)\n""", + parameters=[ + "merge_user_groups:custom_function:list" + ]) + + ################################################################################ + # Get details for the groups to which the user belongs. + ################################################################################ + + merge_user_groups__list = json.loads(_ if (_ := phantom.get_run_data(key="merge_user_groups:list")) != "" else "null") # pylint: disable=used-before-assignment + + parameters = [] + + if filter_formatted_string is not None: + parameters.append({ + "filter": filter_formatted_string, + "attributes": "description;name;memberOf;distinguishedName", + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.act("run query", parameters=parameters, name="get_user_group_attributes", assets=["ad_ldap"], callback=format_user_outputs) + + return + + +@phantom.playbook_block() +def get_device_attributes(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("get_device_attributes() called") + + # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED'))) + + ################################################################################ + # Query for the device attributes for filtered playbook inputs. + ################################################################################ + + format_device_query__as_list = phantom.get_format_data(name="format_device_query__as_list") + + parameters = [] + + # build parameters list for 'get_device_attributes' call + for format_device_query__item in format_device_query__as_list: + if format_device_query__item is not None: + parameters.append({ + "filter": format_device_query__item, + "attributes": "description;distinguishedName;objectSid;memberOf;name;sAMAccountName", + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.act("run query", parameters=parameters, name="get_device_attributes", assets=["ad_ldap"], callback=filter_device_results) + + return + + +@phantom.playbook_block() +def playbook_inputs_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("playbook_inputs_filter() called") + + ################################################################################ + # Filter inputs to route inputs to appropriate actions. + ################################################################################ + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["playbook_input:user", "!=", None] + ], + name="playbook_inputs_filter:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + get_user_attributes(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_1, filtered_results=matched_results_1) + + # collect filtered artifact ids and results for 'if' condition 2 + matched_artifacts_2, matched_results_2 = phantom.condition( + container=container, + conditions=[ + ["playbook_input:device", "!=", None] + ], + name="playbook_inputs_filter:condition_2") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_2 or matched_results_2: + format_device_query(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_2, filtered_results=matched_results_2) + + return + + +@phantom.playbook_block() +def merge_device_groups(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("merge_device_groups() called") + + ################################################################################ + # Merge device groups, from preceding action block results, into a single deduplicated + # list. + ################################################################################ + + filtered_result_0_data_filter_device_results = phantom.collect2(container=container, datapath=["filtered-data:filter_device_results:condition_1:get_device_attributes:action_result.data.*.entries.*.attributes.memberof"]) + + filtered_result_0_data___entries___attributes_memberof = [item[0] for item in filtered_result_0_data_filter_device_results] + + merge_device_groups__list = None + + ################################################################################ + ## Custom Code Start + ################################################################################ + + group_list = [] + for group in filtered_result_0_data___entries___attributes_memberof: + group_list.extend(group) + + merge_device_groups__list = list(set(group_list)) + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_run_data(key="merge_device_groups:list", value=json.dumps(merge_device_groups__list)) + + get_device_group_attributes(container=container) + + return + + +@phantom.playbook_block() +def get_device_group_attributes(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("get_device_group_attributes() called") + + # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED'))) + + filter_formatted_string = phantom.format( + container=container, + template="""(|\n%%\n(distinguishedName={0})\n%%\n)""", + parameters=[ + "merge_device_groups:custom_function:list" + ]) + + ################################################################################ + # Get details for the groups to which the device belongs. + ################################################################################ + + merge_device_groups__list = json.loads(_ if (_ := phantom.get_run_data(key="merge_device_groups:list")) != "" else "null") # pylint: disable=used-before-assignment + + parameters = [] + + if filter_formatted_string is not None: + parameters.append({ + "filter": filter_formatted_string, + "attributes": "description;name;memberOf;distinguishedName", + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.act("run query", parameters=parameters, name="get_device_group_attributes", assets=["ad_ldap"], callback=format_device_outputs) + + return + + +@phantom.playbook_block() +def format_device_outputs(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("format_device_outputs() called") + + ################################################################################ + # Format a normalized output for each device. + ################################################################################ + + filtered_result_0_data_filter_device_results = phantom.collect2(container=container, datapath=["filtered-data:filter_device_results:condition_1:get_device_attributes:action_result.data.*.entries.*.attributes","filtered-data:filter_device_results:condition_1:get_device_attributes:action_result.parameter.filter"]) + get_device_group_attributes_result_data = phantom.collect2(container=container, datapath=["get_device_group_attributes:action_result.data.*.entries.*.attributes"], action_results=results) + + filtered_result_0_data___entries___attributes = [item[0] for item in filtered_result_0_data_filter_device_results] + filtered_result_0_parameter_filter = [item[1] for item in filtered_result_0_data_filter_device_results] + get_device_group_attributes_result_item_0 = [item[0] for item in get_device_group_attributes_result_data] + + format_device_outputs__observable_array = None + + ################################################################################ + ## Custom Code Start + ################################################################################ + import re + + format_device_outputs__observable_array = [] + + def extract_domain_name(dn): + dn_rex = re.search(r'(DC=.+)', dn) + if dn_rex: + dc_list = dn_rex.group(1).replace('DC=', '').split(',') + return '.'.join(dc_list) + + def extract_ou(dn): + dn_rex = re.search(r'(OU=.+?),DC', dn) + if dn_rex: + ou_list = dn_rex.group(1).replace('OU=', '').split(',') + return ou_list + + def extract_pb_input_from_filter(filter_query_list): + pb_input_list = [] + for query in filter_query_list: + query_rex = re.search(r'sAMAccountName=([^\)]+)', query) + hostname = query_rex.group(1).replace('*', '') + pb_input_list.append(hostname) + return pb_input_list + + # create dictionary of group attributes + group_dict = {} + for group in get_device_group_attributes_result_item_0: + group_dict[group['distinguishedname']] = group + + pb_input_hostname_list = extract_pb_input_from_filter(filtered_result_0_parameter_filter) + # create normalized output dictionary + for pb_input, device in zip(pb_input_hostname_list, filtered_result_0_data___entries___attributes): + device_dict = { + "desc": device['description'], + "domain": extract_domain_name(device['distinguishedname']), + "hostname": device['name'], + "name": device['samaccountname'], + "type": "Unknown", + "type_id": 0, + "uid": device['objectsid'] + } + + # use OUs as labels + ou_list = extract_ou(device['distinguishedname']) + if ou_list: + device_dict['labels'] = ou_list + + # lookup groups + group_list = [] + for group in device['memberof']: + if group_dict.get(group): + group_details = group_dict[group] + if not group_details['description']: + group_details['description'].append("None") + temp_group_dict = { + "type": "Domain", + "desc": group_details['description'][0], + "name": group_details['name'], + "privileges": group_details['memberof'], + "uid": group_details['distinguishedname'] + } + # drop values with [] + for k,v in temp_group_dict.copy().items(): + if v == []: + temp_group_dict.pop(k) + group_list.append(temp_group_dict) + if group_list: + device_dict['groups'] = group_list + + + final_output = { + "type": "host name", + "value": pb_input, + "attributes": device_dict, + "source": "AD LDAP" + } + format_device_outputs__observable_array.append(final_output) + + # phantom.debug(format_device_outputs__observable_array) + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_run_data(key="format_device_outputs:observable_array", value=json.dumps(format_device_outputs__observable_array)) + + return + + +@phantom.playbook_block() +def filter_user_results(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("filter_user_results() called") + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["get_user_attributes:action_result.summary.total_objects", ">", 0] + ], + name="filter_user_results:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + merge_user_groups(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 filter_device_results(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("filter_device_results() called") + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["get_device_attributes:action_result.summary.total_objects", ">", 0] + ], + name="filter_device_results:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + merge_device_groups(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_device_query(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("format_device_query() called") + + ################################################################################ + # Iterate through the playbook inputs and format a list of queries. + ################################################################################ + + template = """%%\n(&(objectCategory=computer)(|(sAMAccountName={0}*)(distinguishedName={0}*)(userPrincipalName={0}*)))\n%%""" + + # parameter list for template variable replacement + parameters = [ + "filtered-data:playbook_inputs_filter:condition_2:playbook_input:device" + ] + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.format(container=container, template=template, parameters=parameters, name="format_device_query") + + get_device_attributes(container=container) + + return + + +@phantom.playbook_block() +def on_finish(container, summary): + phantom.debug("on_finish() called") + + format_user_outputs__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="format_user_outputs:observable_array")) != "" else "null") # pylint: disable=used-before-assignment + format_device_outputs__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="format_device_outputs:observable_array")) != "" else "null") # pylint: disable=used-before-assignment + + observable_combined_value = phantom.concatenate(format_user_outputs__observable_array, format_device_outputs__observable_array) + + output = { + "observable": observable_combined_value, + } + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # This function is called after all actions are completed. + # summary of all the action and/or all details of actions + # can be collected here. + + # summary_json = phantom.get_summary() + # if 'result' in summary_json: + # for action_result in summary_json['result']: + # if 'action_run_id' in action_result: + # action_results = phantom.get_action_results(action_run_id=action_result['action_run_id'], result_data=False, flatten=False) + # phantom.debug(action_results) + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_playbook_output_data(output=output) + + return \ No newline at end of file diff --git a/playbooks/AD_LDAP_Entity_Attribute_Lookup.yml b/playbooks/AD_LDAP_Entity_Attribute_Lookup.yml new file mode 100644 index 0000000000..9b98ce7457 --- /dev/null +++ b/playbooks/AD_LDAP_Entity_Attribute_Lookup.yml @@ -0,0 +1,23 @@ +name: AD LDAP Entity Attribute Lookup +id: fc0edc96-aa2b-4cb0-7b4d-63da67d3fe74 +version: 1 +date: '2023-01-11' +author: Kelby Shelton, Lou Stella, Splunk +type: Investigation +description: "Accepts a user or device and looks up the most recent attributes and groups for that user or device. This playbook produces a normalized output for each user and device." +playbook: AD_LDAP_Entity_Attribute_Lookup +how_to_implement: This input playbook requires the AD LDAP connector to be configured. It is designed to work in conjunction with the Dynamic Attribute Lookup playbook or other playbooks in the same style. +references: [] +app_list: + - AD LDAP +tags: + platform_tags: + - attributes + - user + - device + - ad_ldap + playbook_type: Input + vpe_type: Modern + playbook_fields: [] + product: + - Splunk SOAR \ No newline at end of file diff --git a/playbooks/Azure_AD_Graph_User_Attribute_Lookup.json b/playbooks/Azure_AD_Graph_User_Attribute_Lookup.json new file mode 100644 index 0000000000..d3d34d968d --- /dev/null +++ b/playbooks/Azure_AD_Graph_User_Attribute_Lookup.json @@ -0,0 +1,277 @@ +{ + "blockly": false, + "blockly_xml": "", + "category": "Attribute Lookup", + "coa": { + "data": { + "description": "Accepts a user or device and looks up the most recent attributes and groups for that user or device. This playbook produces a normalized output for each user and device.", + "edges": [ + { + "id": "port_3_to_port_1", + "sourceNode": "3", + "sourcePort": "3_out", + "targetNode": "1", + "targetPort": "1_in" + }, + { + "id": "port_0_to_port_14", + "sourceNode": "0", + "sourcePort": "0_out", + "targetNode": "14", + "targetPort": "14_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_14_to_port_2", + "sourceNode": "14", + "sourcePort": "14_out", + "targetNode": "2", + "targetPort": "2_in" + }, + { + "id": "port_2_to_port_15", + "sourceNode": "2", + "sourcePort": "2_out", + "targetNode": "15", + "targetPort": "15_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_15_to_port_3", + "sourceNode": "15", + "sourcePort": "15_out", + "targetNode": "3", + "targetPort": "3_in" + } + ], + "hash": "be0b1202e1571a6a8a4d345c6d14d30afcef8422", + "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": [] + }, + "functionId": 1, + "functionName": "on_finish", + "id": "1", + "type": "end" + }, + "errors": {}, + "id": "1", + "type": "end", + "userCode": "\n # This function is called after all actions are completed.\n # summary of all the action and/or all details of actions\n # can be collected here.\n\n # summary_json = phantom.get_summary()\n # if 'result' in summary_json:\n # for action_result in summary_json['result']:\n # if 'action_run_id' in action_result:\n # action_results = phantom.get_action_results(action_run_id=action_result['action_run_id'], result_data=False, flatten=False)\n # phantom.debug(action_results)\n\n", + "warnings": {}, + "x": 20, + "y": 864 + }, + "14": { + "data": { + "advanced": { + "customName": "playbook inputs filter", + "customNameId": 0, + "description": "Filter inputs to route inputs to appropriate actions.", + "join": [], + "note": "Filter inputs to route inputs to appropriate actions." + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "playbook_input:user", + "value": "None" + } + ], + "conditionIndex": 0, + "customName": "user", + "logic": "and" + } + ], + "functionId": 1, + "functionName": "playbook_inputs_filter", + "id": "14", + "type": "filter" + }, + "errors": {}, + "id": "14", + "type": "filter", + "warnings": { + "config": [ + "Reconfigure invalid datapath." + ] + }, + "x": 60, + "y": 148 + }, + "15": { + "data": { + "advanced": { + "customName": "user results filter", + "customNameId": 0, + "description": "Determine if a user was found from the preceding action", + "join": [], + "note": "Determine if a user was found from the preceding action" + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "==", + "param": "get_user_attributes:action_result.status", + "value": "success" + } + ], + "conditionIndex": 0, + "customName": "user exists", + "logic": "and" + } + ], + "functionId": 2, + "functionName": "user_results_filter", + "id": "15", + "type": "filter" + }, + "errors": {}, + "id": "15", + "type": "filter", + "warnings": {}, + "x": 60, + "y": 506 + }, + "2": { + "data": { + "action": "list user attributes", + "actionType": "investigate", + "advanced": { + "customName": "get user attributes", + "customNameId": 0, + "description": "Query for the user attributes for filtered playbook inputs.", + "join": [], + "note": "Query for the user attributes for filtered playbook inputs." + }, + "connector": "Azure AD Graph", + "connectorConfigs": [ + "azure_ad_graph" + ], + "connectorId": "c6d3b801-5c26-4abd-9e89-6d8007e2778f", + "connectorVersion": "v1", + "functionId": 1, + "functionName": "get_user_attributes", + "id": "2", + "parameters": { + "user_id": "filtered-data:playbook_inputs_filter:condition_1:playbook_input:user" + }, + "requiredParameters": [], + "type": "action" + }, + "errors": {}, + "id": "2", + "type": "action", + "warnings": {}, + "x": 0, + "y": 328 + }, + "3": { + "data": { + "advanced": { + "customName": "format user outputs", + "customNameId": 0, + "description": "Format a normalized output for each user.", + "join": [], + "note": "Format a normalized output for each user." + }, + "functionId": 1, + "functionName": "format_user_outputs", + "id": "3", + "inputParameters": [ + "filtered-data:user_results_filter:condition_1:get_user_attributes:action_result.data.0", + "filtered-data:user_results_filter:condition_1:get_user_attributes:action_result.parameter.user_id" + ], + "outputVariables": [ + "observable_array" + ], + "type": "code" + }, + "errors": {}, + "id": "3", + "type": "code", + "userCode": " \n format_user_outputs__observable_array = []\n \n # create normalized output dictionary\n for pb_input, user in zip(filtered_result_0_parameter_user_id, filtered_result_0_data_0):\n user_dict = {\n \"account_type\": \"Azure AD Account\",\n \"account_type_id\": 6,\n \"account_uid\": user['userPrincipalName'],\n \"domain\": user['userPrincipalName'].split('@')[1],\n \"email_addr\": user['mail'],\n \"full_name\": user['displayName'],\n \"name\": user['userPrincipalName'].split('@')[0],\n \"start_date\": user['createdDateTime'],\n \"type\": \"User\",\n \"type_id\": 1,\n \"uid\": user['objectId']\n }\n phones = [\n user['telephoneNumber'],\n user['facsimileTelephoneNumber']\n ]\n labels = [\n user['jobTitle'],\n user['department']\n ]\n \n \n # clean up phone numbers\n phones = [phone for phone in phones if phone]\n user_dict['phones'] = phones\n \n # clean up labels\n labels = [label for label in labels if label]\n user_dict['labels'] = labels\n \n final_output = {\n \"type\": \"user\",\n \"value\": pb_input,\n \"attributes\": user_dict,\n \"source\": \"Azure AD Graph\"\n }\n format_user_outputs__observable_array.append(final_output)\n \n # phantom.debug(format_user_outputs__observable_array)\n \n\n", + "warnings": {}, + "x": 0, + "y": 686 + } + }, + "notes": "Inputs: user, device\nInteractions: AD LDAP\nActions: get attributes, run query\nOutputs: report, observables" + }, + "input_spec": [ + { + "contains": [ + "user name" + ], + "description": "A user identity in Active Directory such as sAMAccountName, userPrincipalName, or distinguishedName", + "name": "user" + }, + { + "contains": [ + "host name" + ], + "description": "A device in Active Directory.", + "name": "device" + } + ], + "output_spec": [ + { + "contains": [], + "datapaths": [ + "format_user_outputs:custom_function:observable_array", + "format_device_outputs:custom_function:observable_array" + ], + "deduplicate": false, + "description": "An array of observable dictionaries ", + "metadata": {}, + "name": "observable" + } + ], + "playbook_type": "data", + "python_version": "3", + "schema": "5.0.9", + "version": "6.0.0.114895" + }, + "create_time": "2023-03-02T14:17:17.726698+00:00", + "draft_mode": false, + "labels": [ + "*" + ], + "tags": [ + "attributes", + "user", + "device", + "azure_ad_graph" + ] +} \ No newline at end of file diff --git a/playbooks/Azure_AD_Graph_User_Attribute_Lookup.png b/playbooks/Azure_AD_Graph_User_Attribute_Lookup.png new file mode 100644 index 0000000000..a08ffda9ce Binary files /dev/null and b/playbooks/Azure_AD_Graph_User_Attribute_Lookup.png differ diff --git a/playbooks/Azure_AD_Graph_User_Attribute_Lookup.py b/playbooks/Azure_AD_Graph_User_Attribute_Lookup.py new file mode 100644 index 0000000000..5c6b930498 --- /dev/null +++ b/playbooks/Azure_AD_Graph_User_Attribute_Lookup.py @@ -0,0 +1,209 @@ +""" +Accepts a user or device and looks up the most recent attributes and groups for that user or device. This playbook produces a normalized output for each user and device. +""" + + +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 'playbook_inputs_filter' block + playbook_inputs_filter(container=container) + + return + +@phantom.playbook_block() +def get_user_attributes(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("get_user_attributes() called") + + # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED'))) + + ################################################################################ + # Query for the user attributes for filtered playbook inputs. + ################################################################################ + + filtered_input_0_user = phantom.collect2(container=container, datapath=["filtered-data:playbook_inputs_filter:condition_1:playbook_input:user"]) + + parameters = [] + + # build parameters list for 'get_user_attributes' call + for filtered_input_0_user_item in filtered_input_0_user: + parameters.append({ + "user_id": filtered_input_0_user_item[0], + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.act("list user attributes", parameters=parameters, name="get_user_attributes", assets=["azure_ad_graph"], callback=user_results_filter) + + return + + +@phantom.playbook_block() +def format_user_outputs(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("format_user_outputs() called") + + ################################################################################ + # Format a normalized output for each user. + ################################################################################ + + filtered_result_0_data_user_results_filter = phantom.collect2(container=container, datapath=["filtered-data:user_results_filter:condition_1:get_user_attributes:action_result.data.0","filtered-data:user_results_filter:condition_1:get_user_attributes:action_result.parameter.user_id"]) + + filtered_result_0_data_0 = [item[0] for item in filtered_result_0_data_user_results_filter] + filtered_result_0_parameter_user_id = [item[1] for item in filtered_result_0_data_user_results_filter] + + format_user_outputs__observable_array = None + + ################################################################################ + ## Custom Code Start + ################################################################################ + + format_user_outputs__observable_array = [] + + # create normalized output dictionary + for pb_input, user in zip(filtered_result_0_parameter_user_id, filtered_result_0_data_0): + user_dict = { + "account_type": "Azure AD Account", + "account_type_id": 6, + "account_uid": user['userPrincipalName'], + "domain": user['userPrincipalName'].split('@')[1], + "email_addr": user['mail'], + "full_name": user['displayName'], + "name": user['userPrincipalName'].split('@')[0], + "start_date": user['createdDateTime'], + "type": "User", + "type_id": 1, + "uid": user['objectId'] + } + phones = [ + user['telephoneNumber'], + user['facsimileTelephoneNumber'] + ] + labels = [ + user['jobTitle'], + user['department'] + ] + + + # clean up phone numbers + phones = [phone for phone in phones if phone] + user_dict['phones'] = phones + + # clean up labels + labels = [label for label in labels if label] + user_dict['labels'] = labels + + final_output = { + "type": "user", + "value": pb_input, + "attributes": user_dict, + "source": "Azure AD Graph" + } + format_user_outputs__observable_array.append(final_output) + + # phantom.debug(format_user_outputs__observable_array) + + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_run_data(key="format_user_outputs:observable_array", value=json.dumps(format_user_outputs__observable_array)) + + return + + +@phantom.playbook_block() +def playbook_inputs_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("playbook_inputs_filter() called") + + ################################################################################ + # Filter inputs to route inputs to appropriate actions. + ################################################################################ + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["playbook_input:user", "!=", None] + ], + name="playbook_inputs_filter:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + get_user_attributes(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 user_results_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("user_results_filter() called") + + ################################################################################ + # Determine if a user was found from the preceding action + ################################################################################ + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["get_user_attributes:action_result.status", "==", "success"] + ], + name="user_results_filter:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + format_user_outputs(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 on_finish(container, summary): + phantom.debug("on_finish() called") + + format_user_outputs__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="format_user_outputs:observable_array")) != "" else "null") # pylint: disable=used-before-assignment + format_device_outputs__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="format_device_outputs:observable_array")) != "" else "null") # pylint: disable=used-before-assignment + + observable_combined_value = phantom.concatenate(format_user_outputs__observable_array, format_device_outputs__observable_array) + + output = { + "observable": observable_combined_value, + } + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # This function is called after all actions are completed. + # summary of all the action and/or all details of actions + # can be collected here. + + # summary_json = phantom.get_summary() + # if 'result' in summary_json: + # for action_result in summary_json['result']: + # if 'action_run_id' in action_result: + # action_results = phantom.get_action_results(action_run_id=action_result['action_run_id'], result_data=False, flatten=False) + # phantom.debug(action_results) + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_playbook_output_data(output=output) + + return \ No newline at end of file diff --git a/playbooks/Azure_AD_Graph_User_Attribute_Lookup.yml b/playbooks/Azure_AD_Graph_User_Attribute_Lookup.yml new file mode 100644 index 0000000000..a5a3d4abfa --- /dev/null +++ b/playbooks/Azure_AD_Graph_User_Attribute_Lookup.yml @@ -0,0 +1,23 @@ +name: Azure AD Graph User Attribute Lookup +id: fc0edc96-aa2b-4cb0-7b4d-63da67e71d74 +version: 1 +date: '2023-01-11' +author: Kelby Shelton, Splunk +type: Investigation +description: "Accepts a user or device and looks up the most recent attributes and groups for that user or device. This playbook produces a normalized output for each user and device." +playbook: Azure_AD_Graph_User_Attribute_Lookup +how_to_implement: This input playbook requires the Azure AD Graph connector to be configured. It is designed to work in conjunction with the Dynamic Attribute Lookup playbook or other playbooks in the same style. +references: [] +app_list: + - Azure AD Graph +tags: + platform_tags: + - attributes + - user + - device + - azure_ad_graph + playbook_type: Input + vpe_type: Modern + playbook_fields: [] + product: + - Splunk SOAR \ No newline at end of file diff --git a/playbooks/CrowdStrike_OAuth_API_Device_Attribute_Lookup.json b/playbooks/CrowdStrike_OAuth_API_Device_Attribute_Lookup.json new file mode 100644 index 0000000000..36d273e094 --- /dev/null +++ b/playbooks/CrowdStrike_OAuth_API_Device_Attribute_Lookup.json @@ -0,0 +1,606 @@ +{ + "blockly": false, + "blockly_xml": "", + "category": "Attribute Lookup", + "coa": { + "data": { + "description": "Accepts device and looks up the most recent attributes for that hostname or IP. This playbook produces a normalized output for each device.", + "edges": [ + { + "id": "port_0_to_port_2", + "sourceNode": "0", + "sourcePort": "0_out", + "targetNode": "2", + "targetPort": "2_in" + }, + { + "id": "port_17_to_port_1", + "sourceNode": "17", + "sourcePort": "17_out", + "targetNode": "1", + "targetPort": "1_in" + }, + { + "id": "port_18_to_port_11", + "sourceNode": "18", + "sourcePort": "18_out", + "targetNode": "11", + "targetPort": "11_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_2_to_port_19", + "sourceNode": "2", + "sourcePort": "2_out", + "targetNode": "19", + "targetPort": "19_in" + }, + { + "id": "port_19_to_port_20", + "sourceNode": "19", + "sourcePort": "19_out", + "targetNode": "20", + "targetPort": "20_in" + }, + { + "conditions": [ + { + "index": 1 + } + ], + "id": "port_20_to_port_18", + "sourceNode": "20", + "sourcePort": "20_out", + "targetNode": "18", + "targetPort": "18_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_20_to_port_21", + "sourceNode": "20", + "sourcePort": "20_out", + "targetNode": "21", + "targetPort": "21_in" + }, + { + "id": "port_21_to_port_22", + "sourceNode": "21", + "sourcePort": "21_out", + "targetNode": "22", + "targetPort": "22_in" + }, + { + "id": "port_24_to_port_1", + "sourceNode": "24", + "sourcePort": "24_out", + "targetNode": "1", + "targetPort": "1_in" + }, + { + "id": "port_11_to_port_25", + "sourceNode": "11", + "sourcePort": "11_out", + "targetNode": "25", + "targetPort": "25_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_25_to_port_17", + "sourceNode": "25", + "sourcePort": "25_out", + "targetNode": "17", + "targetPort": "17_in" + }, + { + "id": "port_22_to_port_26", + "sourceNode": "22", + "sourcePort": "22_out", + "targetNode": "26", + "targetPort": "26_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_26_to_port_24", + "sourceNode": "26", + "sourcePort": "26_out", + "targetNode": "24", + "targetPort": "24_in" + } + ], + "hash": "1c6c95ac6900f02a350245c2f84d9e5b4252dad1", + "nodes": { + "0": { + "data": { + "advanced": { + "join": [] + }, + "functionName": "on_start", + "id": "0", + "type": "start" + }, + "errors": {}, + "id": "0", + "type": "start", + "warnings": {}, + "x": 190, + "y": -6.394884621840902e-14 + }, + "1": { + "data": { + "advanced": { + "join": [] + }, + "functionName": "on_finish", + "id": "1", + "type": "end" + }, + "errors": {}, + "id": "1", + "type": "end", + "warnings": {}, + "x": 190, + "y": 1416 + }, + "11": { + "data": { + "action": "query device", + "actionType": "investigate", + "advanced": { + "customName": "query hostname", + "customNameId": 0, + "description": "Search for devices that have the hostname given to playbook input.", + "join": [], + "note": "Search for devices that have the hostname given to playbook input." + }, + "connector": "CrowdStrike OAuth API", + "connectorConfigs": [ + "crowdstrike" + ], + "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232", + "connectorVersion": "v1", + "functionId": 1, + "functionName": "query_hostname", + "id": "11", + "parameters": { + "filter": "format_hostname_filter:formatted_data.*", + "limit": "10" + }, + "requiredParameters": [ + { + "data_type": "numeric", + "default": 50, + "field": "limit" + } + ], + "type": "action" + }, + "errors": {}, + "id": "11", + "type": "action", + "warnings": {}, + "x": 0, + "y": 864 + }, + "17": { + "data": { + "advanced": { + "customName": "build hostname observables", + "customNameId": 0, + "description": "Generate an observable dictionary to output into the observables data path.", + "join": [], + "note": "Generate an observable dictionary to output into the observables data path." + }, + "functionId": 2, + "functionName": "build_hostname_observables", + "id": "17", + "inputParameters": [ + "filtered-data:filter_hostname_results:condition_1:query_hostname:action_result.data", + "filtered-data:filter_hostname_results:condition_1:query_hostname:action_result.parameter.filter" + ], + "outputVariables": [ + "observable_array" + ], + "type": "code" + }, + "errors": {}, + "id": "17", + "type": "code", + "userCode": " build_hostname_observables__observable_array = []\n device_type_dict = {\n 'server': 1,\n 'desktop': 2,\n 'laptop': 3,\n 'tablet': 4,\n 'mobile': 5,\n 'virtual': 6,\n 'iot': 7,\n 'browser': 8\n }\n for item, input_string in zip(filtered_result_0_data, filtered_result_0_parameter_filter):\n device = item[0]\n device_dict = {\n \"type\": \"host name\",\n \"value\": input_string.replace(\"hostname:\", \"\").replace('\"', \"\"),\n \"source\": \"Crowdstrike OAuth API\",\n \"attributes\": {\n \"desc\": f\"{device.get('system_product_name', 'No product name')} - {device.get('os_product_name', 'No os name')}\",\n \"groups\": device.get('groups'),\n \"hostname\": device.get('hostname'),\n \"ip\": device.get('connection_ip'),\n \"mac\": device.get('mac_address'),\n \"name\": device.get('hostname'),\n \"os\": device.get('os_version'),\n \"hw_info\": {\n \"bios_manufacturer\": device.get('bios_manufacturer'),\n \"bios_ver\": device.get('bios_version'),\n \"serial_number\": device.get('serial_number')\n },\n \"type\": \"Unknown\",\n \"type_id\": 0,\n \"uid\": device.get('device_id') \n }\n }\n if device.get('product_type_desc'):\n if device_type_dict.get(device['product_type_desc'].lower()):\n device_dict['attributes']['type'] = device['product_type_desc']\n device_dict['attributes']['type_id'] = device_type_dict[device['product_type_desc'].lower()]\n else:\n device_dict['attributes']['type'] = device['product_type_desc']\n device_dict['attributes']['type_id'] = 99\n\n build_hostname_observables__observable_array.append(device_dict)\n", + "warnings": {}, + "x": 0, + "y": 1222 + }, + "18": { + "data": { + "advanced": { + "customName": "format hostname filter", + "customNameId": 0, + "description": "Format a filter for the query device, while looping through playbook inputs.", + "join": [], + "note": "Format a filter for the query device, while looping through playbook inputs." + }, + "functionId": 3, + "functionName": "format_hostname_filter", + "id": "18", + "parameters": [ + "filtered-data:ip_filter:condition_2:extract_ipv4:custom_function_result.data.input_value" + ], + "template": "%%\nhostname:\"{0}\"\n%%", + "type": "format" + }, + "errors": {}, + "id": "18", + "type": "format", + "warnings": {}, + "x": 0, + "y": 686 + }, + "19": { + "data": { + "advanced": { + "customName": "extract ipv4", + "customNameId": 0, + "description": "Extract IP address from input device", + "join": [], + "note": "Extract IP address from input device" + }, + "customFunction": { + "draftMode": false, + "name": "regex_extract_ipv4", + "repoName": "community" + }, + "functionId": 1, + "functionName": "extract_ipv4", + "id": "19", + "selectMore": false, + "type": "utility", + "utilities": { + "regex_extract_ipv4": { + "description": "Takes a single input and extracts all IPv4 addresses from it using regex.", + "fields": [ + { + "dataTypes": [ + "*" + ], + "description": "An input string that may contain an arbitrary number of ipv4 addresses", + "inputType": "item", + "label": "input_string", + "name": "input_string", + "placeholder": "192.0.2.1", + "renderType": "datapath", + "required": false + } + ], + "label": "regex_extract_ipv4", + "name": "regex_extract_ipv4" + } + }, + "utilityType": "custom_function", + "values": { + "regex_extract_ipv4": { + "input_string": "filtered-data:device_id_input_filter:condition_1:playbook_input:device" + } + } + }, + "errors": {}, + "id": "19", + "type": "utility", + "warnings": {}, + "x": 170, + "y": 328 + }, + "2": { + "data": { + "advanced": { + "customName": "device id input filter", + "customNameId": 0, + "description": "Ensure that device input exists", + "join": [], + "note": "Ensure that device input exists" + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "playbook_input:device", + "value": "None" + } + ], + "conditionIndex": 0, + "customName": "device_id", + "logic": "and" + } + ], + "functionId": 1, + "functionName": "device_id_input_filter", + "id": "2", + "type": "filter" + }, + "errors": {}, + "id": "2", + "type": "filter", + "warnings": { + "config": [ + "Reconfigure invalid datapath." + ] + }, + "x": 230, + "y": 148 + }, + "20": { + "data": { + "advanced": { + "customName": "ip filter", + "customNameId": 0, + "description": "Determine which of the playbook inputs are devices or hostnames", + "join": [], + "note": "Determine which of the playbook inputs are devices or hostnames" + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "extract_ipv4:custom_function_result.data.extracted_ipv4", + "value": "None" + } + ], + "conditionIndex": 0, + "customName": "ip exists", + "logic": "and" + }, + { + "comparisons": [ + { + "conditionIndex": 1, + "op": "==", + "param": "extract_ipv4:custom_function_result.data.extracted_ipv4", + "value": "None" + } + ], + "conditionIndex": 1, + "customName": "ip does not exist", + "logic": "and" + } + ], + "functionId": 2, + "functionName": "ip_filter", + "id": "20", + "type": "filter" + }, + "errors": {}, + "id": "20", + "type": "filter", + "warnings": {}, + "x": 230, + "y": 506 + }, + "21": { + "data": { + "advanced": { + "customName": "format ip filter", + "customNameId": 0, + "description": "Format a filter for the query device, while looping through playbook inputs.", + "join": [], + "note": "Format a filter for the query device, while looping through playbook inputs." + }, + "functionId": 1, + "functionName": "format_ip_filter", + "id": "21", + "parameters": [ + "filtered-data:ip_filter:condition_1:extract_ipv4:custom_function_result.data.extracted_ipv4" + ], + "template": "%%\nlocal_ip:\"{0}\",external_ip:\"{0}\"\n%%", + "type": "format" + }, + "errors": {}, + "id": "21", + "type": "format", + "warnings": {}, + "x": 340, + "y": 686 + }, + "22": { + "data": { + "action": "query device", + "actionType": "investigate", + "advanced": { + "customName": "query ip", + "customNameId": 0, + "description": "Search for devices that have the IP given to playbook input.", + "join": [], + "note": "Search for devices that have the IP given to playbook input." + }, + "connector": "CrowdStrike OAuth API", + "connectorConfigs": [ + "crowdstrike" + ], + "connectorId": "ae971ba5-3117-444a-8ac5-6ce779f3a232", + "connectorVersion": "v1", + "functionId": 2, + "functionName": "query_ip", + "id": "22", + "parameters": { + "filter": "format_ip_filter:formatted_data.*", + "limit": "10" + }, + "requiredParameters": [ + { + "data_type": "numeric", + "default": 50, + "field": "limit" + } + ], + "type": "action" + }, + "errors": {}, + "id": "22", + "type": "action", + "warnings": {}, + "x": 340, + "y": 864 + }, + "24": { + "data": { + "advanced": { + "customName": "build ip observables", + "customNameId": 0, + "description": "Generate an observable dictionary to output into the observables data path.", + "join": [], + "note": "Generate an observable dictionary to output into the observables data path." + }, + "functionId": 1, + "functionName": "build_ip_observables", + "id": "24", + "inputParameters": [ + "filtered-data:filter_ip_results:condition_1:query_ip:action_result.data", + "filtered-data:filter_ip_results:condition_1:query_ip:action_result.parameter.filter" + ], + "outputVariables": [ + "observable_array" + ], + "type": "code" + }, + "errors": {}, + "id": "24", + "type": "code", + "userCode": "\n build_ip_observables__observable_array = []\n device_type_dict = {\n 'server': 1,\n 'desktop': 2,\n 'laptop': 3,\n 'tablet': 4,\n 'mobile': 5,\n 'virtual': 6,\n 'iot': 7,\n 'browser': 8\n }\n for item, input_string in zip(filtered_result_0_data, filtered_result_0_parameter_filter):\n device = item[0]\n value = input_string.replace(\"local_ip:\", \"\").replace('external_ip', \"\").replace('\"',\"\")\n value = value.split(',')[0]\n device_dict = {\n \"type\": \"ip\",\n \"value\": value,\n \"source\": \"Crowdstrike OAuth API\",\n \"attributes\": {\n \"desc\": f\"{device.get('system_product_name', 'No product name')} - {device.get('os_product_name', 'No os name')}\",\n \"groups\": device.get('groups'),\n \"hostname\": device.get('hostname'),\n \"ip\": device.get('connection_ip'),\n \"mac\": device.get('mac_address'),\n \"name\": device.get('hostname'),\n \"os\": device.get('os_version'),\n \"hw_info\": {\n \"bios_manufacturer\": device.get('bios_manufacturer'),\n \"bios_ver\": device.get('bios_version'),\n \"serial_number\": device.get('serial_number')\n },\n \"type\": \"Unknown\",\n \"type_id\": 0,\n \"uid\": device.get('device_id') \n }\n }\n if device.get('product_type_desc'):\n if device_type_dict.get(device['product_type_desc'].lower()):\n device_dict['attributes']['type'] = device['product_type_desc']\n device_dict['attributes']['type_id'] = device_type_dict[device['product_type_desc'].lower()]\n else:\n device_dict['attributes']['type'] = device['product_type_desc']\n device_dict['attributes']['type_id'] = 99\n\n build_ip_observables__observable_array.append(device_dict)\n\n", + "warnings": {}, + "x": 340, + "y": 1230 + }, + "25": { + "data": { + "advanced": { + "customName": "filter hostname results", + "customNameId": 0, + "description": "Filter on hostnames that returned results.", + "join": [], + "note": "Filter on hostnames that returned results." + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": ">", + "param": "query_hostname:action_result.summary.total_devices", + "value": "0" + } + ], + "conditionIndex": 0, + "customName": "hostname exists", + "logic": "and" + } + ], + "functionId": 3, + "functionName": "filter_hostname_results", + "id": "25", + "type": "filter" + }, + "errors": {}, + "id": "25", + "type": "filter", + "warnings": {}, + "x": 60, + "y": 1042 + }, + "26": { + "data": { + "advanced": { + "customName": "filter ip results", + "customNameId": 0, + "description": "Filter on IPs that returned results.", + "join": [], + "note": "Filter on IPs that returned results." + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": ">", + "param": "query_ip:action_result.summary.total_devices", + "value": "0" + } + ], + "conditionIndex": 0, + "customName": "ip exists", + "logic": "and" + } + ], + "functionId": 4, + "functionName": "filter_ip_results", + "id": "26", + "type": "filter" + }, + "errors": {}, + "id": "26", + "type": "filter", + "warnings": {}, + "x": 400, + "y": 1042 + } + }, + "notes": "Inputs: device\nInteractions: CrowdStrike OAuth API\nActions: query device\nOutputs: observables" + }, + "input_spec": [ + { + "contains": [ + "host name", + "ip" + ], + "description": "A hostname that has a Crowdstrike agent.", + "name": "device" + } + ], + "output_spec": [ + { + "contains": [], + "datapaths": [ + "build_ip_observables:custom_function:observable_array", + "build_hostname_observables:custom_function:observable_array" + ], + "deduplicate": false, + "description": "An array of observable dictionaries ", + "metadata": {}, + "name": "observable" + } + ], + "playbook_type": "data", + "python_version": "3", + "schema": "5.0.9", + "version": "6.0.0.114895" + }, + "create_time": "2023-03-02T13:59:05.442614+00:00", + "draft_mode": false, + "labels": [ + "*" + ], + "tags": [ + "attributes", + "device", + "host name", + "ip", + "crowdstrike_oauth_api" + ] +} \ No newline at end of file diff --git a/playbooks/CrowdStrike_OAuth_API_Device_Attribute_Lookup.png b/playbooks/CrowdStrike_OAuth_API_Device_Attribute_Lookup.png new file mode 100644 index 0000000000..d8b602e91a Binary files /dev/null and b/playbooks/CrowdStrike_OAuth_API_Device_Attribute_Lookup.png differ diff --git a/playbooks/CrowdStrike_OAuth_API_Device_Attribute_Lookup.py b/playbooks/CrowdStrike_OAuth_API_Device_Attribute_Lookup.py new file mode 100644 index 0000000000..546de5c058 --- /dev/null +++ b/playbooks/CrowdStrike_OAuth_API_Device_Attribute_Lookup.py @@ -0,0 +1,464 @@ +""" +Accepts device and looks up the most recent attributes for that hostname or IP. This playbook produces a normalized output for each device. +""" + + +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 'device_id_input_filter' block + device_id_input_filter(container=container) + + return + +@phantom.playbook_block() +def device_id_input_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("device_id_input_filter() called") + + ################################################################################ + # Ensure that device input exists + ################################################################################ + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["playbook_input:device", "!=", None] + ], + name="device_id_input_filter:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + extract_ipv4(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_hostname(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("query_hostname() called") + + # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED'))) + + ################################################################################ + # Search for devices that have the hostname given to playbook input. + ################################################################################ + + format_hostname_filter__as_list = phantom.get_format_data(name="format_hostname_filter__as_list") + + parameters = [] + + # build parameters list for 'query_hostname' call + for format_hostname_filter__item in format_hostname_filter__as_list: + parameters.append({ + "limit": 10, + "filter": format_hostname_filter__item, + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.act("query device", parameters=parameters, name="query_hostname", assets=["crowdstrike"], callback=filter_hostname_results) + + return + + +@phantom.playbook_block() +def build_hostname_observables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("build_hostname_observables() called") + + ################################################################################ + # Generate an observable dictionary to output into the observables data path. + ################################################################################ + + filtered_result_0_data_filter_hostname_results = phantom.collect2(container=container, datapath=["filtered-data:filter_hostname_results:condition_1:query_hostname:action_result.data","filtered-data:filter_hostname_results:condition_1:query_hostname:action_result.parameter.filter"]) + + filtered_result_0_data = [item[0] for item in filtered_result_0_data_filter_hostname_results] + filtered_result_0_parameter_filter = [item[1] for item in filtered_result_0_data_filter_hostname_results] + + build_hostname_observables__observable_array = None + + ################################################################################ + ## Custom Code Start + ################################################################################ + build_hostname_observables__observable_array = [] + device_type_dict = { + 'server': 1, + 'desktop': 2, + 'laptop': 3, + 'tablet': 4, + 'mobile': 5, + 'virtual': 6, + 'iot': 7, + 'browser': 8 + } + for item, input_string in zip(filtered_result_0_data, filtered_result_0_parameter_filter): + device = item[0] + device_dict = { + "type": "host name", + "value": input_string.replace("hostname:", "").replace('"', ""), + "source": "Crowdstrike OAuth API", + "attributes": { + "desc": f"{device.get('system_product_name', 'No product name')} - {device.get('os_product_name', 'No os name')}", + "groups": device.get('groups'), + "hostname": device.get('hostname'), + "ip": device.get('connection_ip'), + "mac": device.get('mac_address'), + "name": device.get('hostname'), + "os": device.get('os_version'), + "hw_info": { + "bios_manufacturer": device.get('bios_manufacturer'), + "bios_ver": device.get('bios_version'), + "serial_number": device.get('serial_number') + }, + "type": "Unknown", + "type_id": 0, + "uid": device.get('device_id') + } + } + if device.get('product_type_desc'): + if device_type_dict.get(device['product_type_desc'].lower()): + device_dict['attributes']['type'] = device['product_type_desc'] + device_dict['attributes']['type_id'] = device_type_dict[device['product_type_desc'].lower()] + else: + device_dict['attributes']['type'] = device['product_type_desc'] + device_dict['attributes']['type_id'] = 99 + + build_hostname_observables__observable_array.append(device_dict) + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_run_data(key="build_hostname_observables:observable_array", value=json.dumps(build_hostname_observables__observable_array)) + + return + + +@phantom.playbook_block() +def format_hostname_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("format_hostname_filter() called") + + ################################################################################ + # Format a filter for the query device, while looping through playbook inputs. + ################################################################################ + + template = """%%\nhostname:\"{0}\"\n%%""" + + # parameter list for template variable replacement + parameters = [ + "filtered-data:ip_filter:condition_2:extract_ipv4:custom_function_result.data.input_value" + ] + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.format(container=container, template=template, parameters=parameters, name="format_hostname_filter") + + query_hostname(container=container) + + return + + +@phantom.playbook_block() +def extract_ipv4(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("extract_ipv4() called") + + ################################################################################ + # Extract IP address from input device + ################################################################################ + + filtered_input_0_device = phantom.collect2(container=container, datapath=["filtered-data:device_id_input_filter:condition_1:playbook_input:device"]) + + parameters = [] + + # build parameters list for 'extract_ipv4' call + for filtered_input_0_device_item in filtered_input_0_device: + parameters.append({ + "input_string": filtered_input_0_device_item[0], + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.custom_function(custom_function="community/regex_extract_ipv4", parameters=parameters, name="extract_ipv4", callback=ip_filter) + + return + + +@phantom.playbook_block() +def ip_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("ip_filter() called") + + ################################################################################ + # Determine which of the playbook inputs are devices or hostnames + ################################################################################ + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["extract_ipv4:custom_function_result.data.extracted_ipv4", "!=", None] + ], + name="ip_filter:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + format_ip_filter(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_1, filtered_results=matched_results_1) + + # collect filtered artifact ids and results for 'if' condition 2 + matched_artifacts_2, matched_results_2 = phantom.condition( + container=container, + conditions=[ + ["extract_ipv4:custom_function_result.data.extracted_ipv4", "==", None] + ], + name="ip_filter:condition_2") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_2 or matched_results_2: + format_hostname_filter(action=action, success=success, container=container, results=results, handle=handle, filtered_artifacts=matched_artifacts_2, filtered_results=matched_results_2) + + return + + +@phantom.playbook_block() +def format_ip_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("format_ip_filter() called") + + ################################################################################ + # Format a filter for the query device, while looping through playbook inputs. + ################################################################################ + + template = """%%\nlocal_ip:\"{0}\",external_ip:\"{0}\"\n%%""" + + # parameter list for template variable replacement + parameters = [ + "filtered-data:ip_filter:condition_1:extract_ipv4:custom_function_result.data.extracted_ipv4" + ] + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.format(container=container, template=template, parameters=parameters, name="format_ip_filter") + + query_ip(container=container) + + return + + +@phantom.playbook_block() +def query_ip(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("query_ip() called") + + # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED'))) + + ################################################################################ + # Search for devices that have the IP given to playbook input. + ################################################################################ + + format_ip_filter__as_list = phantom.get_format_data(name="format_ip_filter__as_list") + + parameters = [] + + # build parameters list for 'query_ip' call + for format_ip_filter__item in format_ip_filter__as_list: + parameters.append({ + "limit": 10, + "filter": format_ip_filter__item, + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.act("query device", parameters=parameters, name="query_ip", assets=["crowdstrike"], callback=filter_ip_results) + + return + + +@phantom.playbook_block() +def build_ip_observables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("build_ip_observables() called") + + ################################################################################ + # Generate an observable dictionary to output into the observables data path. + ################################################################################ + + filtered_result_0_data_filter_ip_results = phantom.collect2(container=container, datapath=["filtered-data:filter_ip_results:condition_1:query_ip:action_result.data","filtered-data:filter_ip_results:condition_1:query_ip:action_result.parameter.filter"]) + + filtered_result_0_data = [item[0] for item in filtered_result_0_data_filter_ip_results] + filtered_result_0_parameter_filter = [item[1] for item in filtered_result_0_data_filter_ip_results] + + build_ip_observables__observable_array = None + + ################################################################################ + ## Custom Code Start + ################################################################################ + + build_ip_observables__observable_array = [] + device_type_dict = { + 'server': 1, + 'desktop': 2, + 'laptop': 3, + 'tablet': 4, + 'mobile': 5, + 'virtual': 6, + 'iot': 7, + 'browser': 8 + } + for item, input_string in zip(filtered_result_0_data, filtered_result_0_parameter_filter): + device = item[0] + value = input_string.replace("local_ip:", "").replace('external_ip', "").replace('"',"") + value = value.split(',')[0] + device_dict = { + "type": "ip", + "value": value, + "source": "Crowdstrike OAuth API", + "attributes": { + "desc": f"{device.get('system_product_name', 'No product name')} - {device.get('os_product_name', 'No os name')}", + "groups": device.get('groups'), + "hostname": device.get('hostname'), + "ip": device.get('connection_ip'), + "mac": device.get('mac_address'), + "name": device.get('hostname'), + "os": device.get('os_version'), + "hw_info": { + "bios_manufacturer": device.get('bios_manufacturer'), + "bios_ver": device.get('bios_version'), + "serial_number": device.get('serial_number') + }, + "type": "Unknown", + "type_id": 0, + "uid": device.get('device_id') + } + } + if device.get('product_type_desc'): + if device_type_dict.get(device['product_type_desc'].lower()): + device_dict['attributes']['type'] = device['product_type_desc'] + device_dict['attributes']['type_id'] = device_type_dict[device['product_type_desc'].lower()] + else: + device_dict['attributes']['type'] = device['product_type_desc'] + device_dict['attributes']['type_id'] = 99 + + build_ip_observables__observable_array.append(device_dict) + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_run_data(key="build_ip_observables:observable_array", value=json.dumps(build_ip_observables__observable_array)) + + return + + +@phantom.playbook_block() +def filter_hostname_results(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("filter_hostname_results() called") + + ################################################################################ + # Filter on hostnames that returned results. + ################################################################################ + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["query_hostname:action_result.summary.total_devices", ">", 0] + ], + name="filter_hostname_results:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + build_hostname_observables(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 filter_ip_results(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("filter_ip_results() called") + + ################################################################################ + # Filter on IPs that returned results. + ################################################################################ + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["query_ip:action_result.summary.total_devices", ">", 0] + ], + name="filter_ip_results:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + build_ip_observables(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 on_finish(container, summary): + phantom.debug("on_finish() called") + + build_ip_observables__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="build_ip_observables:observable_array")) != "" else "null") # pylint: disable=used-before-assignment + build_hostname_observables__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="build_hostname_observables:observable_array")) != "" else "null") # pylint: disable=used-before-assignment + + observable_combined_value = phantom.concatenate(build_ip_observables__observable_array, build_hostname_observables__observable_array) + + output = { + "observable": observable_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_Device_Attribute_Lookup.yml b/playbooks/CrowdStrike_OAuth_API_Device_Attribute_Lookup.yml new file mode 100644 index 0000000000..6cbad29ae4 --- /dev/null +++ b/playbooks/CrowdStrike_OAuth_API_Device_Attribute_Lookup.yml @@ -0,0 +1,25 @@ +name: Crowdstrike OAuth API Device Attribute Lookup +id: fc0eac01-af2b-4cb0-7b4d-63da67d3fe74 +version: 1 +date: '2023-01-11' +author: Teoderick Contreras, Splunk +type: Investigation +description: "Accepts a user or device and looks up the most recent attributes and groups for that user or device. This playbook produces a normalized output for each user and device." +playbook: Crowdstrike_OAuth_API_Device_Attribute_Lookup +how_to_implement: This input playbook requires the Crowdstrike OAuth API connector to be configured. It is designed to work in conjunction with the Dynamic Attribute Lookup playbook or other playbooks in the same style. +references: [] +app_list: + - Crowdstrike OAuth API +tags: + platform_tags: + - attributes + - user + - device + - host name + - ip + - crowdstrike_oauth_api + playbook_type: Input + vpe_type: Modern + playbook_fields: [] + product: + - Splunk SOAR \ No newline at end of file diff --git a/playbooks/Dynamic_Attribute_Lookup.json b/playbooks/Dynamic_Attribute_Lookup.json new file mode 100644 index 0000000000..0e8492709f --- /dev/null +++ b/playbooks/Dynamic_Attribute_Lookup.json @@ -0,0 +1,791 @@ +{ + "blockly": false, + "blockly_xml": "", + "category": "Attribute Lookup", + "coa": { + "data": { + "description": "Detects available entities and routes them to attribute lookup playbooks. The output of the playbooks will create new artifacts for any technologies that returned information.", + "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_3", + "sourceNode": "2", + "sourcePort": "2_out", + "targetNode": "3", + "targetPort": "3_in" + }, + { + "conditions": [ + { + "index": 1 + } + ], + "id": "port_2_to_port_4", + "sourceNode": "2", + "sourcePort": "2_out", + "targetNode": "4", + "targetPort": "4_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_3_to_port_5", + "sourceNode": "3", + "sourcePort": "3_out", + "targetNode": "5", + "targetPort": "5_in" + }, + { + "id": "port_5_to_port_7", + "sourceNode": "5", + "sourcePort": "5_out", + "targetNode": "7", + "targetPort": "7_in" + }, + { + "conditions": [ + { + "index": 1 + } + ], + "id": "port_7_to_port_8", + "sourceNode": "7", + "sourcePort": "7_out", + "targetNode": "8", + "targetPort": "8_in" + }, + { + "id": "port_6_to_port_9", + "sourceNode": "6", + "sourcePort": "6_out", + "targetNode": "9", + "targetPort": "9_in" + }, + { + "id": "port_9_to_port_1", + "sourceNode": "9", + "sourcePort": "9_out", + "targetNode": "1", + "targetPort": "1_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_7_to_port_10", + "sourceNode": "7", + "sourcePort": "7_out", + "targetNode": "10", + "targetPort": "10_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_10_to_port_6", + "sourceNode": "10", + "sourcePort": "10_out", + "targetNode": "6", + "targetPort": "6_in" + } + ], + "hash": "da5ffd389f3e278ac939229d65f40ab122d422fe", + "nodes": { + "0": { + "data": { + "advanced": { + "join": [] + }, + "functionName": "on_start", + "id": "0", + "type": "start" + }, + "errors": {}, + "id": "0", + "type": "start", + "warnings": {}, + "x": 300, + "y": -6.394884621840902e-14 + }, + "1": { + "data": { + "advanced": { + "join": [] + }, + "functionName": "on_finish", + "id": "1", + "type": "end" + }, + "errors": {}, + "id": "1", + "type": "end", + "warnings": {}, + "x": 300, + "y": 1536 + }, + "10": { + "data": { + "advanced": { + "customName": "observable filter", + "customNameId": 0, + "description": "Exclude Null playbook outputs", + "join": [], + "note": "Exclude Null playbook outputs" + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "dispatch_attribute_lookup:playbook_output:observable", + "value": "None" + } + ], + "conditionIndex": 0, + "customName": "observable not none", + "logic": "and" + } + ], + "functionId": 2, + "functionName": "observable_filter", + "id": "10", + "type": "filter" + }, + "errors": {}, + "id": "10", + "type": "filter", + "warnings": {}, + "x": 340, + "y": 852 + }, + "2": { + "data": { + "advanced": { + "join": [] + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "artifact:*.id", + "value": "" + } + ], + "conditionIndex": 0, + "customName": "artifact exists", + "display": "If", + "logic": "and", + "type": "if" + }, + { + "comparisons": [ + { + "conditionIndex": 1, + "op": "==", + "param": "", + "value": "" + } + ], + "conditionIndex": 1, + "customName": "artifact does not exist", + "display": "Else", + "logic": "and", + "type": "else" + } + ], + "functionId": 1, + "functionName": "decision_1", + "id": "2", + "type": "decision" + }, + "errors": {}, + "id": "2", + "type": "decision", + "warnings": {}, + "x": 360, + "y": 148 + }, + "3": { + "data": { + "advanced": { + "customName": "new artifact filter", + "customNameId": 0, + "description": "Only include new artifacts", + "join": [], + "note": "Only include new artifacts" + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "artifact:*.id", + "value": "" + } + ], + "conditionIndex": 0, + "customName": "artifacts", + "logic": "and" + } + ], + "functionId": 1, + "functionName": "new_artifact_filter", + "id": "3", + "type": "filter" + }, + "errors": {}, + "id": "3", + "type": "filter", + "warnings": {}, + "x": 200, + "y": 328 + }, + "4": { + "data": { + "advanced": { + "description": "Leave a comment indicating no new artifacts were found", + "join": [], + "note": "Leave a comment indicating no new artifacts were found" + }, + "functionId": 1, + "functionName": "add_comment_1", + "id": "4", + "selectMore": false, + "tab": "apis", + "type": "utility", + "utilities": { + "comment": { + "description": "", + "fields": [ + { + "description": "", + "label": "comment", + "name": "comment", + "placeholder": "Enter a comment", + "renderType": "datapath", + "required": true + }, + { + "hidden": true, + "name": "container", + "required": false + }, + { + "hidden": true, + "name": "author", + "required": false + }, + { + "hidden": true, + "name": "trace", + "required": false + } + ], + "label": "add comment", + "name": "comment" + } + }, + "utilityType": "api", + "values": { + "comment": { + "_internal": [ + "container", + "author", + "trace" + ], + "comment": "No new artifacts found." + } + } + }, + "errors": {}, + "id": "4", + "type": "utility", + "warnings": {}, + "x": 420, + "y": 329 + }, + "5": { + "data": { + "advanced": { + "customName": "Dispatch Attribute Lookup", + "customNameId": 0, + "join": [] + }, + "functionId": 1, + "functionName": "dispatch_attribute_lookup", + "id": "5", + "inputs": { + "artifact_ids_include": { + "datapaths": [ + "filtered-data:new_artifact_filter:condition_1:artifact:*.id" + ], + "deduplicate": false + }, + "indicator_tags_exclude": { + "datapaths": [], + "deduplicate": false + }, + "indicator_tags_include": { + "datapaths": [], + "deduplicate": false + }, + "playbook_repo": { + "datapaths": [ + "" + ], + "deduplicate": false + }, + "playbook_tags": { + "datapaths": [ + "attributes" + ], + "deduplicate": false + } + }, + "playbookName": "dispatch_input_playbooks", + "playbookRepo": 3, + "playbookRepoName": "community", + "playbookType": "data", + "synchronous": true, + "type": "playbook" + }, + "errors": {}, + "id": "5", + "type": "playbook", + "userCode": "\n # Write your custom code here...\n\n", + "warnings": {}, + "x": 140, + "y": 508 + }, + "6": { + "customCode": null, + "data": { + "advanced": { + "customName": "create entity artifact", + "customNameId": 0, + "description": "Create new artifacts with the outputs of the dispatch lookup (Contains custom code)", + "join": [], + "note": "Create new artifacts with the outputs of the dispatch lookup (Contains custom code)" + }, + "customFunction": { + "draftMode": false, + "name": "artifact_create", + "repoName": "community" + }, + "functionId": 2, + "functionName": "create_entity_artifact", + "id": "6", + "selectMore": false, + "tab": "customFunctions", + "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": "", + "label": "attribute_lookup", + "name": "filtered-data:observable_filter:condition_1:dispatch_attribute_lookup:playbook_output:observable", + "run_automation": null, + "severity": null, + "tags": null + } + } + }, + "errors": {}, + "id": "6", + "type": "utility", + "userCode": "\n parameters = []\n \n for filtered_output_0_dispatch_attribute_lookup_output_observable_item in filtered_output_0_dispatch_attribute_lookup_output_observable:\n name = (\n f\"{filtered_output_0_dispatch_attribute_lookup_output_observable_item[0]['type'].capitalize()} \"\n f\"\\'{filtered_output_0_dispatch_attribute_lookup_output_observable_item[0]['value']}\\' \"\n f\"details from \"\n f\"{filtered_output_0_dispatch_attribute_lookup_output_observable_item[0]['source']}\"\n )\n cef_dict = {\n \"cef\": filtered_output_0_dispatch_attribute_lookup_output_observable_item[0]['attributes']\n }\n parameters.append({\n \"name\": name,\n \"tags\": None,\n \"label\": \"attribute_lookup\",\n \"severity\": None,\n \"cef_field\": None,\n \"cef_value\": None,\n \"container\": id_value,\n \"input_json\": json.dumps(cef_dict),\n \"cef_data_type\": None,\n \"run_automation\": None,\n })\n", + "warnings": {}, + "x": 280, + "y": 1220 + }, + "7": { + "data": { + "advanced": { + "customName": "observable decision", + "customNameId": 0, + "description": "Determine if there are any results from the dispatch playbook", + "join": [], + "note": "Determine if there are any results from the dispatch playbook" + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "dispatch_attribute_lookup:playbook_output:observable", + "value": "None" + } + ], + "conditionIndex": 0, + "customName": "observables exist", + "display": "If", + "logic": "and", + "type": "if" + }, + { + "comparisons": [ + { + "conditionIndex": 1, + "op": "==", + "param": "", + "value": "" + } + ], + "conditionIndex": 1, + "customName": "observables do not exist", + "display": "Else", + "logic": "and", + "type": "else" + } + ], + "functionId": 2, + "functionName": "observable_decision", + "id": "7", + "type": "decision" + }, + "errors": {}, + "id": "7", + "type": "decision", + "warnings": {}, + "x": 220, + "y": 672 + }, + "8": { + "data": { + "advanced": { + "description": "Leave a comment indicating the playbooks did not have results.", + "join": [], + "note": "Leave a comment indicating the playbooks did not have results." + }, + "functionId": 3, + "functionName": "add_comment_3", + "id": "8", + "selectMore": false, + "tab": "apis", + "type": "utility", + "utilities": { + "comment": { + "description": "", + "fields": [ + { + "description": "", + "label": "comment", + "name": "comment", + "placeholder": "Enter a comment", + "renderType": "datapath", + "required": true + }, + { + "hidden": true, + "name": "container", + "required": false + }, + { + "hidden": true, + "name": "author", + "required": false + }, + { + "hidden": true, + "name": "trace", + "required": false + } + ], + "label": "add comment", + "name": "comment" + } + }, + "utilityType": "api", + "values": { + "comment": { + "_internal": [ + "container", + "author", + "trace" + ], + "comment": "No observable data found." + } + } + }, + "errors": {}, + "id": "8", + "type": "utility", + "warnings": {}, + "x": 0, + "y": 853 + }, + "9": { + "data": { + "advanced": { + "join": [] + }, + "customFunction": { + "draftMode": false, + "name": "workbook_task_update", + "repoName": "community" + }, + "functionId": 4, + "functionName": "workbook_task_update_4", + "id": "9", + "selectMore": false, + "tab": "customFunctions", + "type": "utility", + "utilities": { + "workbook_task_update": { + "description": "Update a workbook task by task name", + "fields": [ + { + "dataTypes": [ + "*" + ], + "description": "Name of a workbook task (Required)", + "inputType": "item", + "label": "task_name", + "name": "task_name", + "placeholder": "my_task", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [ + "*" + ], + "description": "Note title goes here (Optional)", + "inputType": "item", + "label": "note_title", + "name": "note_title", + "placeholder": "My Title", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [ + "*" + ], + "description": "Body of note goes here (Optional)", + "inputType": "item", + "label": "note_content", + "name": "note_content", + "placeholder": "My notes", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [ + "*" + ], + "description": "One of: incomplete, in_progress, complete (Optional)", + "inputType": "item", + "label": "status", + "name": "status", + "placeholder": "in_progress", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [ + "*" + ], + "description": "Assigns task to provided owner. Accepts keyword 'current\" to assign task to currently running playbook user. (Optional)", + "inputType": "item", + "label": "owner", + "name": "owner", + "placeholder": "username", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [ + "phantom container id" + ], + "description": "ID of Phantom Container (Required)", + "inputType": "item", + "label": "container", + "name": "container", + "placeholder": "container:id", + "renderType": "datapath", + "required": false + } + ], + "label": "workbook_task_update", + "name": "workbook_task_update" + } + }, + "utilityType": "custom_function", + "values": { + "workbook_task_update": { + "container": "container:id", + "note_content": null, + "note_title": null, + "owner": null, + "status": "complete", + "task_name": "playbook" + } + } + }, + "errors": {}, + "id": "9", + "type": "utility", + "warnings": {}, + "x": 280, + "y": 1388 + } + }, + "notes": "Outputs: \n- Creates artifacts for attribute details\n- Concludes task" + }, + "input_spec": null, + "output_spec": null, + "playbook_type": "automation", + "python_version": "3", + "schema": "5.0.9", + "version": "6.0.0.114895" + }, + "create_time": "2023-03-02T14:39:31.825576+00:00", + "draft_mode": false, + "labels": [ + "*" + ], + "tags": [] +} \ No newline at end of file diff --git a/playbooks/Dynamic_Attribute_Lookup.png b/playbooks/Dynamic_Attribute_Lookup.png new file mode 100644 index 0000000000..245b94ad6b Binary files /dev/null and b/playbooks/Dynamic_Attribute_Lookup.png differ diff --git a/playbooks/Dynamic_Attribute_Lookup.py b/playbooks/Dynamic_Attribute_Lookup.py new file mode 100644 index 0000000000..56e69e5734 --- /dev/null +++ b/playbooks/Dynamic_Attribute_Lookup.py @@ -0,0 +1,304 @@ +""" +Detects available entities and routes them to attribute lookup playbooks. The output of the playbooks will create new artifacts for any technologies that returned information. +""" + + +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 'decision_1' block + decision_1(container=container) + + return + +@phantom.playbook_block() +def decision_1(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("decision_1() called") + + # check for 'if' condition 1 + found_match_1 = phantom.decision( + container=container, + conditions=[ + ["artifact:*.id", "!=", ""] + ]) + + # call connected blocks if condition 1 matched + if found_match_1: + new_artifact_filter(action=action, success=success, container=container, results=results, handle=handle) + return + + # check for 'else' condition 2 + add_comment_1(action=action, success=success, container=container, results=results, handle=handle) + + return + + +@phantom.playbook_block() +def new_artifact_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("new_artifact_filter() called") + + ################################################################################ + # Only include new artifacts + ################################################################################ + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["artifact:*.id", "!=", ""] + ], + name="new_artifact_filter:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + dispatch_attribute_lookup(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 add_comment_1(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("add_comment_1() called") + + ################################################################################ + # Leave a comment indicating no new artifacts were found + ################################################################################ + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.comment(container=container, comment="No new artifacts found.") + + return + + +@phantom.playbook_block() +def dispatch_attribute_lookup(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("dispatch_attribute_lookup() called") + + filtered_artifact_0_data_new_artifact_filter = phantom.collect2(container=container, datapath=["filtered-data:new_artifact_filter:condition_1:artifact:*.id"]) + + filtered_artifact_0__id = [item[0] for item in filtered_artifact_0_data_new_artifact_filter] + + inputs = { + "playbook_tags": ["attributes"], + "playbook_repo": [""], + "indicator_tags_include": [], + "indicator_tags_exclude": [], + "artifact_ids_include": filtered_artifact_0__id, + } + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + # call playbook "community/dispatch_input_playbooks", returns the playbook_run_id + playbook_run_id = phantom.playbook("community/dispatch_input_playbooks", container=container, name="dispatch_attribute_lookup", callback=observable_decision, inputs=inputs) + + return + + +@phantom.playbook_block() +def create_entity_artifact(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("create_entity_artifact() called") + + ################################################################################ + # Create new artifacts with the outputs of the dispatch lookup (Contains custom + # code) + ################################################################################ + + id_value = container.get("id", None) + filtered_output_0_dispatch_attribute_lookup_output_observable = phantom.collect2(container=container, datapath=["filtered-data:observable_filter:condition_1:dispatch_attribute_lookup:playbook_output:observable"]) + + parameters = [] + + # build parameters list for 'create_entity_artifact' call + for filtered_output_0_dispatch_attribute_lookup_output_observable_item in filtered_output_0_dispatch_attribute_lookup_output_observable: + parameters.append({ + "name": filtered_output_0_dispatch_attribute_lookup_output_observable_item[0], + "tags": None, + "label": "attribute_lookup", + "severity": None, + "cef_field": None, + "cef_value": None, + "container": id_value, + "input_json": None, + "cef_data_type": None, + "run_automation": None, + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + parameters = [] + + for filtered_output_0_dispatch_attribute_lookup_output_observable_item in filtered_output_0_dispatch_attribute_lookup_output_observable: + name = ( + f"{filtered_output_0_dispatch_attribute_lookup_output_observable_item[0]['type'].capitalize()} " + f"\'{filtered_output_0_dispatch_attribute_lookup_output_observable_item[0]['value']}\' " + f"details from " + f"{filtered_output_0_dispatch_attribute_lookup_output_observable_item[0]['source']}" + ) + cef_dict = { + "cef": filtered_output_0_dispatch_attribute_lookup_output_observable_item[0]['attributes'] + } + parameters.append({ + "name": name, + "tags": None, + "label": "attribute_lookup", + "severity": None, + "cef_field": None, + "cef_value": None, + "container": id_value, + "input_json": json.dumps(cef_dict), + "cef_data_type": None, + "run_automation": None, + }) + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.custom_function(custom_function="community/artifact_create", parameters=parameters, name="create_entity_artifact", callback=workbook_task_update_4) + + return + + +@phantom.playbook_block() +def observable_decision(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("observable_decision() called") + + ################################################################################ + # Determine if there are any results from the dispatch playbook + ################################################################################ + + # check for 'if' condition 1 + found_match_1 = phantom.decision( + container=container, + conditions=[ + ["dispatch_attribute_lookup:playbook_output:observable", "!=", None] + ]) + + # call connected blocks if condition 1 matched + if found_match_1: + observable_filter(action=action, success=success, container=container, results=results, handle=handle) + return + + # check for 'else' condition 2 + add_comment_3(action=action, success=success, container=container, results=results, handle=handle) + + return + + +@phantom.playbook_block() +def add_comment_3(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("add_comment_3() called") + + ################################################################################ + # Leave a comment indicating the playbooks did not have results. + ################################################################################ + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.comment(container=container, comment="No observable data found.") + + return + + +@phantom.playbook_block() +def workbook_task_update_4(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("workbook_task_update_4() called") + + id_value = container.get("id", None) + + parameters = [] + + parameters.append({ + "owner": None, + "status": "complete", + "container": id_value, + "task_name": "playbook", + "note_title": None, + "note_content": None, + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.custom_function(custom_function="community/workbook_task_update", parameters=parameters, name="workbook_task_update_4") + + return + + +@phantom.playbook_block() +def observable_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("observable_filter() called") + + ################################################################################ + # Exclude Null playbook outputs + ################################################################################ + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["dispatch_attribute_lookup:playbook_output:observable", "!=", None] + ], + name="observable_filter:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + create_entity_artifact(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 on_finish(container, summary): + phantom.debug("on_finish() called") + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + return \ No newline at end of file diff --git a/playbooks/Dynamic_Attribute_Lookup.yml b/playbooks/Dynamic_Attribute_Lookup.yml new file mode 100644 index 0000000000..b7832662c5 --- /dev/null +++ b/playbooks/Dynamic_Attribute_Lookup.yml @@ -0,0 +1,18 @@ +name: Dynamic Attribute Lookup +id: fc0edc96-ff2b-68d0-9a4d-63da6783fd64 +version: 1 +date: '2023-03-06' +author: Lou Stella, Splunk +type: Investigation +description: "Detects available entities and routes them to attribute lookup playbooks. The output of the playbooks will create new artifacts for any technologies that returned information." +playbook: Dynamic_Attribute_Lookup +how_to_implement: This playbook looks for artifacts and then dispatches the community Attribute Lookup playbooks. This playbook takes the output of those playbooks and nicely formats them into new artifacts with their results. +references: [] +app_list: [] +tags: + platform_tags: [] + playbook_type: Automation + vpe_type: Modern + playbook_fields: [] + product: + - Splunk SOAR