diff --git a/playbooks/Dynamic_Related_Tickets_Search.json b/playbooks/Dynamic_Related_Tickets_Search.json new file mode 100644 index 0000000000..5c06f3b24e --- /dev/null +++ b/playbooks/Dynamic_Related_Tickets_Search.json @@ -0,0 +1,735 @@ +{ + "blockly": false, + "blockly_xml": "", + "category": "Dynamic Related Ticket Search", + "coa": { + "data": { + "description": "Detects available indicators and routes them to dynamic related ticket search playbooks. The output of the analysis will update any artifacts, tasks, and indicator tags.", + "edges": [ + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_3_to_port_6", + "sourceNode": "3", + "sourcePort": "3_out", + "targetNode": "6", + "targetPort": "6_in" + }, + { + "id": "port_0_to_port_7", + "sourceNode": "0", + "sourcePort": "0_out", + "targetNode": "7", + "targetPort": "7_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_7_to_port_3", + "sourceNode": "7", + "sourcePort": "7_out", + "targetNode": "3", + "targetPort": "3_in" + }, + { + "conditions": [ + { + "index": 1 + } + ], + "id": "port_7_to_port_5", + "sourceNode": "7", + "sourcePort": "7_out", + "targetNode": "5", + "targetPort": "5_in" + }, + { + "id": "port_6_to_port_8", + "sourceNode": "6", + "sourcePort": "6_out", + "targetNode": "8", + "targetPort": "8_in" + }, + { + "conditions": [ + { + "index": 1 + } + ], + "id": "port_8_to_port_9", + "sourceNode": "8", + "sourcePort": "8_out", + "targetNode": "9", + "targetPort": "9_in" + }, + { + "id": "port_10_to_port_11", + "sourceNode": "10", + "sourcePort": "10_out", + "targetNode": "11", + "targetPort": "11_in" + }, + { + "id": "port_11_to_port_1", + "sourceNode": "11", + "sourcePort": "11_out", + "targetNode": "1", + "targetPort": "1_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_8_to_port_14", + "sourceNode": "8", + "sourcePort": "8_out", + "targetNode": "14", + "targetPort": "14_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_14_to_port_10", + "sourceNode": "14", + "sourcePort": "14_out", + "targetNode": "10", + "targetPort": "10_in" + } + ], + "hash": "a6c7d1370562fa8c2c59b56f47c3fd52f27f8b37", + "nodes": { + "0": { + "data": { + "advanced": { + "join": [] + }, + "functionName": "on_start", + "id": "0", + "type": "start" + }, + "errors": {}, + "id": "0", + "type": "start", + "warnings": {}, + "x": 300, + "y": 0 + }, + "1": { + "data": { + "advanced": { + "join": [] + }, + "functionName": "on_finish", + "id": "1", + "type": "end" + }, + "errors": {}, + "id": "1", + "type": "end", + "warnings": {}, + "x": 300, + "y": 1358 + }, + "10": { + "data": { + "advanced": { + "customName": "merge reports", + "customNameId": 0, + "description": "Format a note that merges together normalized data. The data will come from the playbooks launched by the Dispatch Ticketing Playbooks block.", + "join": [], + "note": "Format a note that merges together normalized data. The data will come from the playbooks launched by the Dispatch Ticketing Playbooks block." + }, + "customDatapaths": { + "dispatch_filter_1": { + "condition_1:dispatch_ticketing_playbooks:outputs:observable.matched_fields": { + "contains": [], + "isCustomDatapath": true, + "label": "condition_1:dispatch_ticketing_playbooks:outputs:observable.matched_fields", + "value": "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.matched_fields" + }, + "condition_1:dispatch_ticketing_playbooks:outputs:observable.source": { + "contains": [], + "isCustomDatapath": true, + "label": "condition_1:dispatch_ticketing_playbooks:outputs:observable.source", + "value": "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.source" + }, + "condition_1:dispatch_ticketing_playbooks:outputs:observable.source_link": { + "contains": [], + "isCustomDatapath": true, + "label": "condition_1:dispatch_ticketing_playbooks:outputs:observable.source_link", + "value": "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.source_link" + }, + "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.assignee": { + "contains": [], + "isCustomDatapath": true, + "label": "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.assignee", + "value": "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.assignee" + }, + "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.creator_name": { + "contains": [], + "isCustomDatapath": true, + "label": "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.creator_name", + "value": "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.creator_name" + }, + "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.end_time": { + "contains": [], + "isCustomDatapath": true, + "label": "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.end_time", + "value": "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.end_time" + }, + "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.message": { + "contains": [], + "isCustomDatapath": true, + "label": "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.message", + "value": "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.message" + }, + "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.name": { + "contains": [], + "isCustomDatapath": true, + "label": "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.name", + "value": "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.name" + }, + "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.number": { + "contains": [], + "isCustomDatapath": true, + "label": "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.number", + "value": "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.number" + }, + "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.start_time": { + "contains": [], + "isCustomDatapath": true, + "label": "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.start_time", + "value": "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.start_time" + }, + "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.state": { + "contains": [], + "isCustomDatapath": true, + "label": "condition_1:dispatch_ticketing_playbooks:outputs:observable.ticket.state", + "value": "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.state" + } + } + }, + "functionId": 1, + "functionName": "merge_reports", + "id": "10", + "parameters": [ + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.name", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.number", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.message", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.start_time", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.end_time", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.assignee", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.creator_name", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.state", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.matched_fields", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.source", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.source_link" + ], + "template": "SOAR retrieved tickets from Splunk. The table below shows a summary of the information gathered.\n\n| Name | Number | Message | Start Time | End Time | Assignee | Creator Name | State | Matched Fields | Source | Source Link |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n%%\n| {0} | {1} | {2} | {3} | {4} | {5} | {6} | {7} | {8} | {9} | {10} |\n%%\n", + "type": "format" + }, + "errors": {}, + "id": "10", + "type": "format", + "warnings": {}, + "x": 280, + "y": 1032 + }, + "11": { + "data": { + "advanced": { + "customName": "ticketing update", + "customNameId": 0, + "join": [] + }, + "customFunction": { + "draftMode": false, + "name": "workbook_task_update", + "repoName": "community" + }, + "functionId": 4, + "functionName": "ticketing_update", + "id": "11", + "selectMore": false, + "type": "utility", + "utilities": { + "workbook_task_update": { + "description": "Update a workbook task by task name or the task where the currently running playbook appears. Requires a task_name, container_id, and a note_title, note_content, owner, or status.", + "fields": [ + { + "dataTypes": [ + "*" + ], + "description": "Name of a workbook task or keyword 'playbook' to update the task where the currently running playbook appears. (Required)", + "inputType": "item", + "label": "task_name", + "name": "task_name", + "placeholder": "my_task", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [ + "*" + ], + "description": "Note title. (Optional)", + "inputType": "item", + "label": "note_title", + "name": "note_title", + "placeholder": "My Title", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [ + "*" + ], + "description": "Note content. (Optional)", + "inputType": "item", + "label": "note_content", + "name": "note_content", + "placeholder": "My notes", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [ + "*" + ], + "description": "Accepts 'incomplete', 'in_progress, or 'complete'. (Optional)", + "inputType": "item", + "label": "status", + "name": "status", + "placeholder": "in_progress", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [ + "*" + ], + "description": "A user to assign as the task owner or keyword 'current\" to assign the task to the user that launched the playbook. (Optional)", + "inputType": "item", + "label": "owner", + "name": "owner", + "placeholder": "username", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [ + "phantom container id" + ], + "description": "The ID of a SOAR 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": "merge_reports:formatted_data", + "note_title": "Dynamic Related Ticket Search Report", + "owner": null, + "status": "complete", + "task_name": "playbook" + } + } + }, + "errors": {}, + "id": "11", + "type": "utility", + "warnings": {}, + "x": 280, + "y": 1210 + }, + "14": { + "data": { + "advanced": { + "customName": "dispatch filter", + "customNameId": 1, + "description": "Create a dataset with the output of the dispatch playbooks that is not None", + "join": [], + "note": "Create a dataset with the output of the dispatch playbooks that is not None" + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "dispatch_ticketing_playbooks:playbook_output:observable", + "value": "None" + } + ], + "conditionIndex": 0, + "customName": "outputs exist", + "logic": "and" + } + ], + "functionId": 3, + "functionName": "dispatch_filter_1", + "id": "14", + "type": "filter" + }, + "errors": {}, + "id": "14", + "type": "filter", + "warnings": {}, + "x": 340, + "y": 852 + }, + "3": { + "data": { + "advanced": { + "customName": "filter new artifacts", + "customNameId": 0, + "description": "Only dispatch rplaybooks against new artifacts.", + "join": [], + "note": "Only dispatch rplaybooks against new artifacts.", + "scope": "default" + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "artifact:*.id", + "value": "None" + } + ], + "conditionIndex": 0, + "customName": "artifacts", + "logic": "and" + } + ], + "functionId": 2, + "functionName": "filter_new_artifacts", + "id": "3", + "type": "filter" + }, + "errors": {}, + "id": "3", + "type": "filter", + "warnings": {}, + "x": 200, + "y": 328 + }, + "5": { + "data": { + "advanced": { + "join": [] + }, + "functionId": 2, + "functionName": "add_comment_2", + "id": "5", + "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 artifact data found in event." + } + } + }, + "errors": {}, + "id": "5", + "type": "utility", + "warnings": {}, + "x": 420, + "y": 344 + }, + "6": { + "data": { + "advanced": { + "customName": "Dispatch Ticketing Playbooks", + "customNameId": 0, + "join": [] + }, + "functionId": 1, + "functionName": "dispatch_ticketing_playbooks", + "id": "6", + "inputs": { + "artifact_ids_include": { + "datapaths": [ + "filtered-data:filter_new_artifacts: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": [ + "ticket" + ], + "deduplicate": false + } + }, + "playbookName": "dispatch_input_playbooks", + "playbookRepo": 3, + "playbookRepoName": "community", + "playbookType": "data", + "synchronous": true, + "type": "playbook" + }, + "errors": {}, + "id": "6", + "type": "playbook", + "warnings": {}, + "x": 140, + "y": 508 + }, + "7": { + "data": { + "advanced": { + "customName": "artifact decision", + "customNameId": 0, + "description": "Determine if artifacts exist to run through the playbook.", + "join": [], + "note": "Determine if artifacts exist to run through the playbook." + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "artifact:*.id", + "value": "None" + } + ], + "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": "artifact_decision", + "id": "7", + "type": "decision" + }, + "errors": {}, + "id": "7", + "type": "decision", + "warnings": {}, + "x": 360, + "y": 148 + }, + "8": { + "data": { + "advanced": { + "customName": "outputs decision", + "customNameId": 0, + "description": "Determine if outputs exist.", + "join": [], + "note": "Determine if outputs exist." + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "dispatch_ticketing_playbooks:playbook_output:observable", + "value": "None" + } + ], + "conditionIndex": 0, + "customName": "output exists", + "display": "If", + "logic": "and", + "type": "if" + }, + { + "comparisons": [ + { + "conditionIndex": 1, + "op": "==", + "param": "", + "value": "" + } + ], + "conditionIndex": 1, + "customName": "outputs do not exist", + "display": "Else", + "logic": "and", + "type": "else" + } + ], + "functionId": 2, + "functionName": "outputs_decision", + "id": "8", + "type": "decision" + }, + "errors": {}, + "id": "8", + "type": "decision", + "warnings": {}, + "x": 220, + "y": 672 + }, + "9": { + "data": { + "advanced": { + "join": [] + }, + "functionId": 3, + "functionName": "add_comment_3", + "id": "9", + "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 from dispatched playbooks." + } + } + }, + "errors": {}, + "id": "9", + "type": "utility", + "warnings": {}, + "x": 0, + "y": 868 + } + }, + "notes": "Outputs:\ntags indicators\nadd relevant tickets" + }, + "input_spec": null, + "output_spec": null, + "playbook_type": "automation", + "python_version": "3", + "schema": "5.0.9", + "version": "6.0.0.114895" + }, + "create_time": "2023-02-27T20:44:02.427087+00:00", + "draft_mode": false, + "labels": [ + "*" + ], + "tags": [] +} \ No newline at end of file diff --git a/playbooks/Dynamic_Related_Tickets_Search.png b/playbooks/Dynamic_Related_Tickets_Search.png new file mode 100644 index 0000000000..a884a3caf4 Binary files /dev/null and b/playbooks/Dynamic_Related_Tickets_Search.png differ diff --git a/playbooks/Dynamic_Related_Tickets_Search.py b/playbooks/Dynamic_Related_Tickets_Search.py new file mode 100644 index 0000000000..5a05c4d180 --- /dev/null +++ b/playbooks/Dynamic_Related_Tickets_Search.py @@ -0,0 +1,278 @@ +""" +Detects available indicators and routes them to dynamic related ticket search playbooks. The output of the analysis will update any artifacts, tasks, and indicator tags. +""" + + +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 'artifact_decision' block + artifact_decision(container=container) + + return + +@phantom.playbook_block() +def filter_new_artifacts(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("filter_new_artifacts() called") + + ################################################################################ + # Only dispatch rplaybooks against 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", "!=", None] + ], + name="filter_new_artifacts:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + dispatch_ticketing_playbooks(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_2(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("add_comment_2() called") + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.comment(container=container, comment="No new artifact data found in event.") + + return + + +@phantom.playbook_block() +def dispatch_ticketing_playbooks(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("dispatch_ticketing_playbooks() called") + + filtered_artifact_0_data_filter_new_artifacts = phantom.collect2(container=container, datapath=["filtered-data:filter_new_artifacts:condition_1:artifact:*.id"]) + + filtered_artifact_0__id = [item[0] for item in filtered_artifact_0_data_filter_new_artifacts] + + inputs = { + "playbook_tags": ["ticket"], + "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_ticketing_playbooks", callback=outputs_decision, inputs=inputs) + + return + + +@phantom.playbook_block() +def artifact_decision(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("artifact_decision() called") + + ################################################################################ + # Determine if artifacts exist to run through the playbook. + ################################################################################ + + # check for 'if' condition 1 + found_match_1 = phantom.decision( + container=container, + conditions=[ + ["artifact:*.id", "!=", None] + ]) + + # call connected blocks if condition 1 matched + if found_match_1: + filter_new_artifacts(action=action, success=success, container=container, results=results, handle=handle) + return + + # check for 'else' condition 2 + add_comment_2(action=action, success=success, container=container, results=results, handle=handle) + + return + + +@phantom.playbook_block() +def outputs_decision(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("outputs_decision() called") + + ################################################################################ + # Determine if outputs exist. + ################################################################################ + + # check for 'if' condition 1 + found_match_1 = phantom.decision( + container=container, + conditions=[ + ["dispatch_ticketing_playbooks:playbook_output:observable", "!=", None] + ]) + + # call connected blocks if condition 1 matched + if found_match_1: + dispatch_filter_1(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") + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.comment(container=container, comment="No observable data found from dispatched playbooks.") + + return + + +@phantom.playbook_block() +def merge_reports(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("merge_reports() called") + + ################################################################################ + # Format a note that merges together normalized data. The data will come from + # the playbooks launched by the Dispatch Ticketing Playbooks block. + ################################################################################ + + template = """SOAR retrieved tickets from Splunk. The table below shows a summary of the information gathered.\n\n| Name | Number | Message | Start Time | End Time | Assignee | Creator Name | State | Matched Fields | Source | Source Link |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n%%\n| {0} | {1} | {2} | {3} | {4} | {5} | {6} | {7} | {8} | {9} | {10} |\n%%\n""" + + # parameter list for template variable replacement + parameters = [ + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.name", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.number", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.message", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.start_time", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.end_time", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.assignee", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.creator_name", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.ticket.state", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.matched_fields", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.source", + "filtered-data:dispatch_filter_1:condition_1:dispatch_ticketing_playbooks:playbook_output:observable.source_link" + ] + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.format(container=container, template=template, parameters=parameters, name="merge_reports") + + ticketing_update(container=container) + + return + + +@phantom.playbook_block() +def ticketing_update(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("ticketing_update() called") + + id_value = container.get("id", None) + merge_reports = phantom.get_format_data(name="merge_reports") + + parameters = [] + + parameters.append({ + "owner": None, + "status": "complete", + "container": id_value, + "task_name": "playbook", + "note_title": "Dynamic Related Ticket Search Report", + "note_content": merge_reports, + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.custom_function(custom_function="community/workbook_task_update", parameters=parameters, name="ticketing_update") + + return + + +@phantom.playbook_block() +def dispatch_filter_1(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("dispatch_filter_1() called") + + ################################################################################ + # Create a dataset with the output of the dispatch playbooks that is not None + ################################################################################ + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["dispatch_ticketing_playbooks:playbook_output:observable", "!=", None] + ], + name="dispatch_filter_1:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + merge_reports(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_Related_Tickets_Search.yml b/playbooks/Dynamic_Related_Tickets_Search.yml new file mode 100644 index 0000000000..5ad47b1503 --- /dev/null +++ b/playbooks/Dynamic_Related_Tickets_Search.yml @@ -0,0 +1,19 @@ +name: Dynamic Related Tickets Search +id: fc0edc96-ab1f-48b9-9b4d-63da61bafe74 +version: 1 +date: '2023-02-28' +author: Patrick Bareiss, Splunk +type: Investigation +description: "Detects available indicators and routes them to dynamic related ticket search playbooks. The output of the analysis will update any artifacts, tasks, and indicator tags." +playbook: Dynamic_Related_Tickets_Search +how_to_implement: This playbook looks for artifacts and then dispatches the community Related Tickets playbooks. This playbook takes the output of those playbooks and nicely formats them into notes and tags indicators with their results. +references: + - https://d3fend.mitre.org/technique/d3f:IdentifierReputationAnalysis/ +app_list: [] +tags: + platform_tags: [] + playbook_type: Automation + vpe_type: Modern + playbook_fields: [] + product: + - Splunk SOAR \ No newline at end of file diff --git a/playbooks/ServiceNow_Related_Tickets_Search.json b/playbooks/ServiceNow_Related_Tickets_Search.json new file mode 100644 index 0000000000..f9b8ef0e82 --- /dev/null +++ b/playbooks/ServiceNow_Related_Tickets_Search.json @@ -0,0 +1,576 @@ +{ + "blockly": false, + "blockly_xml": "", + "category": "Dynamic Related Ticket Search", + "coa": { + "data": { + "description": "Accepts a user or device and identifies if related tickets exists in a timeframe of last 30 days. Generates a global report and list of observables.", + "edges": [ + { + "id": "port_6_to_port_8", + "sourceNode": "6", + "sourcePort": "6_out", + "targetNode": "8", + "targetPort": "8_in" + }, + { + "id": "port_0_to_port_6", + "sourceNode": "0", + "sourcePort": "0_out", + "targetNode": "6", + "targetPort": "6_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_10_to_port_9", + "sourceNode": "10", + "sourcePort": "10_out", + "targetNode": "9", + "targetPort": "9_in" + }, + { + "id": "port_8_to_port_17", + "sourceNode": "8", + "sourcePort": "8_out", + "targetNode": "17", + "targetPort": "17_in" + }, + { + "id": "port_17_to_port_10", + "sourceNode": "17", + "sourcePort": "17_out", + "targetNode": "10", + "targetPort": "10_in" + }, + { + "id": "port_9_to_port_19", + "sourceNode": "9", + "sourcePort": "9_out", + "targetNode": "19", + "targetPort": "19_in" + }, + { + "id": "port_19_to_port_14", + "sourceNode": "19", + "sourcePort": "19_out", + "targetNode": "14", + "targetPort": "14_in" + }, + { + "id": "port_14_to_port_16", + "sourceNode": "14", + "sourcePort": "14_out", + "targetNode": "16", + "targetPort": "16_in" + }, + { + "id": "port_16_to_port_25", + "sourceNode": "16", + "sourcePort": "16_out", + "targetNode": "25", + "targetPort": "25_in" + }, + { + "id": "port_25_to_port_1", + "sourceNode": "25", + "sourcePort": "25_out", + "targetNode": "1", + "targetPort": "1_in" + } + ], + "hash": "e7ead2bdc6a802950dd37c47b3f4af40d61c676c", + "nodes": { + "0": { + "data": { + "advanced": { + "join": [] + }, + "functionName": "on_start", + "id": "0", + "type": "start" + }, + "errors": {}, + "id": "0", + "type": "start", + "warnings": {}, + "x": 19.999999999999986, + "y": -6.394884621840902e-14 + }, + "1": { + "data": { + "advanced": { + "join": [] + }, + "functionName": "on_finish", + "id": "1", + "type": "end" + }, + "errors": {}, + "id": "1", + "type": "end", + "warnings": {}, + "x": 19.999999999999986, + "y": 1708 + }, + "10": { + "data": { + "advanced": { + "customName": "input filter", + "customNameId": 0, + "description": "Creates a dataset without None values.", + "join": [], + "note": "Creates a dataset without None values." + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "playbook_input:search_term", + "value": "None" + } + ], + "conditionIndex": 0, + "customName": "search term exists", + "logic": "and" + } + ], + "functionId": 1, + "functionName": "input_filter", + "id": "10", + "type": "filter" + }, + "errors": {}, + "id": "10", + "type": "filter", + "warnings": { + "config": [ + "Reconfigure invalid datapath." + ] + }, + "x": 60, + "y": 622 + }, + "14": { + "data": { + "advanced": { + "customName": "process results", + "customNameId": 0, + "description": "Iterates through the results of the run ticket query to link playbook input search term to their associated tickets.", + "join": [], + "note": "Iterates through the results of the run ticket query to link playbook input search term to their associated tickets." + }, + "functionId": 3, + "functionName": "process_results", + "id": "14", + "inputParameters": [ + "filtered-data:input_filter:condition_1:playbook_input:search_term", + "run_ticket_query:action_result.data", + "run_ticket_query:action_result.parameter.query_table" + ], + "outputVariables": [ + "output" + ], + "type": "code" + }, + "errors": {}, + "id": "14", + "type": "code", + "userCode": " process_results__output = {}\n for search_term in filtered_input_0_search_term_values:\n process_results__output[search_term] = []\n for result, query_table in zip(run_ticket_query_result_item_0, run_ticket_query_parameter_query_table):\n if not isinstance(result, list):\n list_result = [result]\n else:\n list_result = result\n for result_item in list_result:\n result_item_values = [item.lower() for item in result_item.values() if isinstance(item, str)]\n match = False\n for string_value in result_item_values:\n if search_term.lower() in string_value:\n match = True\n break\n if match:\n process_results__output[search_term].append({**result_item, **{\"ticket_type\": query_table}})\n\n", + "warnings": {}, + "x": 0, + "y": 1158 + }, + "16": { + "customCode": null, + "data": { + "advanced": { + "customName": "build output", + "customNameId": 0, + "description": "Extract relevant data and add them to an observable array.", + "join": [], + "note": "Extract relevant data and add them to an observable array." + }, + "functionId": 5, + "functionName": "build_output", + "id": "16", + "inputParameters": [ + "process_results:custom_function:output" + ], + "outputVariables": [ + "observable_array", + "name", + "number", + "message", + "start_time", + "end_time", + "assignee", + "creator_name", + "state", + "matched_fields", + "source_link", + "source" + ], + "type": "code" + }, + "errors": {}, + "id": "16", + "type": "code", + "userCode": " import re \n \n build_output__observable_array = []\n build_output__name = []\n build_output__number = []\n build_output__message = []\n build_output__start_time = []\n build_output__end_time = []\n build_output__assignee = []\n build_output__creator_name = []\n build_output__state = []\n build_output__matched_fields = []\n build_output__source_link = []\n build_output__source = []\n \n def generate_ticket_link(sample_url, ticket_type, sys_id):\n extract_host = re.search(r\"https*:\\/\\/[^\\/]+\", sample_url).group(0)\n extract_host += f\"/nav_to.do?uri={ticket_type}.do?sys_id={sys_id}\"\n return extract_host\n \n for key in process_results__output.keys():\n \n for value in process_results__output[key]:\n assigned_to = None\n caller_id = None\n matched_fields = []\n if value.get(\"assigned_to\"):\n assigned_to = value[\"assigned_to\"][\"display_value\"]\n if value.get(\"caller_id\"):\n caller_id = value[\"caller_id\"][\"display_value\"]\n \n for k, v in value.items():\n # generate matched fields where the searched entity appears\n if isinstance(v, str) and key.lower() in v.lower():\n matched_fields.append(k)\n # search for any link sample:\n if isinstance(v, dict):\n sample_link = v.get('link')\n \n source_link = generate_ticket_link(sample_link, value['ticket_type'], value['sys_id'])\n observable_object = {\n \"value\": key,\n \"ticket\": {\n \"name\": value[\"short_description\"],\n \"id\": value[\"sys_id\"],\n \"number\": value[\"number\"],\n \"message\": json.dumps(value[\"description\"]),\n \"start_time\": value[\"sys_created_on\"],\n \"end_time\": value[\"closed_at\"],\n \"assigned_to\": assigned_to,\n \"creator_name\": caller_id,\n \"state\": value[\"state\"],\n \"notes\": [value[\"work_notes\"]],\n \"comments\": [value[\"comments\"]]\n },\n \"matched_fields\": matched_fields,\n \"source\": \"ServiceNow\",\n \"source_link\": source_link\n }\n build_output__observable_array.append(observable_object)\n build_output__name.append(value[\"short_description\"])\n build_output__number.append(value[\"number\"])\n build_output__message.append(json.dumps(value[\"description\"])) # eliminate new line issues\n build_output__start_time.append(value[\"sys_created_on\"])\n build_output__end_time.append(value[\"closed_at\"])\n build_output__assignee.append(assigned_to)\n build_output__creator_name.append(caller_id)\n build_output__state.append(value[\"state\"])\n build_output__matched_fields.append(matched_fields)\n build_output__source.append(\"ServiceNow\")\n build_output__source_link.append(source_link)\n #phantom.debug(observable_object)\n\n", + "warnings": {}, + "x": 0, + "y": 1352 + }, + "17": { + "data": { + "advanced": { + "customName": "calculate earliest time", + "customNameId": 0, + "join": [] + }, + "customFunction": { + "draftMode": false, + "name": "datetime_modify", + "repoName": "community" + }, + "functionId": 2, + "functionName": "calculate_earliest_time", + "id": "17", + "selectMore": false, + "type": "utility", + "utilities": { + "datetime_modify": { + "description": "Change a timestamp by adding or subtracting minutes, hours, or days.", + "fields": [ + { + "dataTypes": [ + "" + ], + "description": "The datetime to modify, which should be provided in a string format determined by input_format_string", + "inputType": "item", + "label": "input_datetime", + "name": "input_datetime", + "placeholder": "2020-06-27T14:53:08.219016Z", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [], + "description": "The format string to use for the input according to the Python's datetime.strptime() formatting rules. If none is provided the default will be '%Y-%m-%dT%H:%M:%S.%fZ'. In addition to strptime() formats, the special format \"epoch\" can be used to accept unix epoch timestamps.", + "inputType": "item", + "label": "input_format_string", + "name": "input_format_string", + "placeholder": "%Y-%m-%dT%H:%M:%S.%fZ", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [ + "" + ], + "description": "Choose a unit to modify the date by, which must be either seconds, minutes, hours, or days. If none is provided the default will be 'minutes'", + "inputType": "item", + "label": "modification_unit", + "name": "modification_unit", + "placeholder": "minutes", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [], + "description": "The number of seconds, minutes, hours, or days to add or subtract. Use a negative number such as -1.5 to subtract time. Defaults to zero.", + "inputType": "item", + "label": "amount_to_modify", + "name": "amount_to_modify", + "placeholder": "0", + "renderType": "datapath", + "required": false + }, + { + "dataTypes": [], + "description": "The format string to use for the output according to the Python's datetime.strftime() formatting rules. If none is provided the default will be '%Y-%m-%dT%H:%M:%S.%fZ'.", + "inputType": "item", + "label": "output_format_string", + "name": "output_format_string", + "placeholder": "%Y-%m-%dT%H:%M:%S.%fZ", + "renderType": "datapath", + "required": false + } + ], + "label": "datetime_modify", + "name": "datetime_modify" + } + }, + "utilityType": "custom_function", + "values": { + "datetime_modify": { + "amount_to_modify": "-30", + "input_datetime": "container:create_time", + "input_format_string": "%Y-%m-%d %H:%M:%S.%f+00", + "modification_unit": "days", + "output_format_string": "'%Y-%m-%d','%H:%M:%S'" + } + } + }, + "errors": {}, + "id": "17", + "type": "utility", + "warnings": {}, + "x": 0, + "y": 474 + }, + "19": { + "data": { + "action": "run query", + "actionType": "investigate", + "advanced": { + "customName": "run ticket query", + "customNameId": 0, + "description": "Perform a text search match within ServiceNow.", + "join": [], + "note": "Perform a text search match within ServiceNow." + }, + "connector": "ServiceNow", + "connectorConfigs": [ + "servicenow" + ], + "connectorId": "a590c3bc-ca41-4a0e-b063-8066ca868794", + "connectorVersion": "v1", + "functionId": 3, + "functionName": "run_ticket_query", + "id": "19", + "parameters": { + "max_results": "100", + "query": { + "functionId": 3, + "parameters": [ + "space_delimiter_input:formatted_data", + "calculate_earliest_time:custom_function_result.data.datetime_string" + ], + "template": "sysparm_query=active=true^IR_OR_QUERY={0}^opened_at>javascript:gs.dateGenerate({1})&sysparm_display_value=true \n\n" + }, + "query_table": "convert_table_list:custom_function_result.data.output" + }, + "requiredParameters": [ + { + "data_type": "string", + "field": "query" + }, + { + "data_type": "numeric", + "default": 100, + "field": "max_results" + }, + { + "data_type": "string", + "field": "query_table" + } + ], + "type": "action" + }, + "errors": {}, + "id": "19", + "type": "action", + "warnings": {}, + "x": 0, + "y": 980 + }, + "25": { + "data": { + "advanced": { + "customName": "format report", + "customNameId": 0, + "description": "Format a summary table with the information gathered from the playbook.", + "join": [], + "note": "Format a summary table with the information gathered from the playbook." + }, + "functionId": 3, + "functionName": "format_report", + "id": "25", + "parameters": [ + "build_output:custom_function:name", + "build_output:custom_function:number", + "build_output:custom_function:message", + "build_output:custom_function:start_time", + "build_output:custom_function:end_time", + "build_output:custom_function:assignee", + "build_output:custom_function:creator_name", + "build_output:custom_function:state", + "build_output:custom_function:matched_fields", + "build_output:custom_function:source", + "build_output:custom_function:source_link" + ], + "template": "SOAR retrieved tickets from Service Now. The table below shows a summary of the information gathered.\n\n| Name | Number | Message | Start Time | End Time | Assignee | Creator Name | State | Matched Fields | Source | Source Link |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n%%\n| {0} | {1} | {2} | {3} | {4} | {5} | {6} | {7} | {8} | {9} | {10} |\n%%\n", + "type": "format" + }, + "errors": {}, + "id": "25", + "type": "format", + "warnings": {}, + "x": 0, + "y": 1530 + }, + "6": { + "data": { + "advanced": { + "customName": "default table list", + "customNameId": 0, + "description": "Adjust the table list variable to change which tables should be searched.", + "join": [], + "note": "Adjust the table list variable to change which tables should be searched." + }, + "functionId": 2, + "functionName": "default_table_list", + "id": "6", + "inputParameters": [], + "outputVariables": [ + "output" + ], + "type": "code" + }, + "errors": {}, + "id": "6", + "type": "code", + "userCode": " \n # Default tables list to find related tickets. Adjust as needed.\n default_table_list = [\n 'incident', \n 'change_request', \n 'change_task', \n 'problem',\n 'sc_request', \n 'sc_task', \n 'sc_req_item',\n ]\n default_table_list__output = default_table_list\n \n", + "warnings": {}, + "x": 0, + "y": 148 + }, + "8": { + "data": { + "advanced": { + "customName": "convert table list", + "customNameId": 0, + "join": [], + "refreshNotableData": false + }, + "customFunction": { + "draftMode": false, + "name": "list_demux", + "repoName": "community" + }, + "functionId": 1, + "functionName": "convert_table_list", + "id": "8", + "selectMore": false, + "type": "utility", + "utilities": { + "list_demux": { + "description": "Accepts a single list and converts it into multiple custom function output results. All output will be placed in the \"output\" datapath. Sub-items and sub-item variable names are dependent on the input.", + "fields": [ + { + "dataTypes": [ + "*" + ], + "description": "A list of objects. Nested lists are not unpacked.", + "inputType": "item", + "label": "input_list", + "name": "input_list", + "placeholder": "[\"list_item_1\", \"list_item_2\", \"list_item_3\"]", + "renderType": "datapath", + "required": false + } + ], + "label": "list_demux", + "name": "list_demux" + } + }, + "utilityType": "custom_function", + "values": { + "list_demux": { + "input_list": "default_table_list:custom_function:output" + } + } + }, + "errors": {}, + "id": "8", + "type": "utility", + "userCode": "\n", + "warnings": {}, + "x": 0, + "y": 326 + }, + "9": { + "data": { + "advanced": { + "customName": "space delimiter input", + "customNameId": 0, + "description": "Convert playbook input into space delimiter string for ServiceNow query.", + "drop_none": true, + "join": [], + "note": "Convert playbook input into space delimiter string for ServiceNow query.", + "separator": " " + }, + "functionId": 1, + "functionName": "space_delimiter_input", + "id": "9", + "parameters": [ + "filtered-data:input_filter:condition_1:playbook_input:search_term" + ], + "template": "{0}\n", + "type": "format" + }, + "errors": {}, + "id": "9", + "type": "format", + "warnings": {}, + "x": 0, + "y": 802 + } + }, + "notes": "Inputs: user, device\nInteractions: ServiceNow\nActions: run query\nOutputs: report, observables" + }, + "input_spec": [ + { + "contains": [ + "user name", + "host name" + ], + "description": "Find tickets in ServiceNow that have mentioned this search term..", + "name": "search_term" + } + ], + "output_spec": [ + { + "contains": [], + "datapaths": [ + "build_output:custom_function:observable_array" + ], + "deduplicate": false, + "description": "An array of observable dictionaries with value, type, score, score_id, and categories.", + "metadata": {}, + "name": "observable" + }, + { + "contains": [], + "datapaths": [ + "format_report:formatted_data" + ], + "deduplicate": false, + "description": "An array of reports. One report per reputation category.", + "metadata": {}, + "name": "markdown_report" + } + ], + "playbook_type": "data", + "python_version": "3", + "schema": "5.0.9", + "version": "6.0.0.114895" + }, + "create_time": "2023-03-03T21:21:48.729159+00:00", + "draft_mode": false, + "labels": [ + "*" + ], + "tags": [ + "user", + "device", + "ServiceNow", + "ticket" + ] +} \ No newline at end of file diff --git a/playbooks/ServiceNow_Related_Tickets_Search.png b/playbooks/ServiceNow_Related_Tickets_Search.png new file mode 100644 index 0000000000..91b44f9880 Binary files /dev/null and b/playbooks/ServiceNow_Related_Tickets_Search.png differ diff --git a/playbooks/ServiceNow_Related_Tickets_Search.py b/playbooks/ServiceNow_Related_Tickets_Search.py new file mode 100644 index 0000000000..027adeb29b --- /dev/null +++ b/playbooks/ServiceNow_Related_Tickets_Search.py @@ -0,0 +1,452 @@ +""" +Accepts a user or device and identifies if related tickets exists in a timeframe of last 30 days. Generates a global report and list of observables. +""" + + +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 'default_table_list' block + default_table_list(container=container) + + return + +@phantom.playbook_block() +def default_table_list(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("default_table_list() called") + + ################################################################################ + # Adjust the table list variable to change which tables should be searched. + ################################################################################ + + default_table_list__output = None + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Default tables list to find related tickets. Adjust as needed. + default_table_list = [ + 'incident', + 'change_request', + 'change_task', + 'problem', + 'sc_request', + 'sc_task', + 'sc_req_item', + ] + default_table_list__output = default_table_list + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_run_data(key="default_table_list:output", value=json.dumps(default_table_list__output)) + + convert_table_list(container=container) + + return + + +@phantom.playbook_block() +def convert_table_list(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("convert_table_list() called") + + default_table_list__output = json.loads(_ if (_ := phantom.get_run_data(key="default_table_list:output")) != "" else "null") # pylint: disable=used-before-assignment + + parameters = [] + + parameters.append({ + "input_list": default_table_list__output, + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.custom_function(custom_function="community/list_demux", parameters=parameters, name="convert_table_list", callback=calculate_earliest_time) + + return + + +@phantom.playbook_block() +def space_delimiter_input(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("space_delimiter_input() called") + + ################################################################################ + # Convert playbook input into space delimiter string for ServiceNow query. + ################################################################################ + + template = """{0}\n""" + + # parameter list for template variable replacement + parameters = [ + "filtered-data:input_filter:condition_1:playbook_input:search_term" + ] + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.format(container=container, template=template, parameters=parameters, name="space_delimiter_input", separator=" ", drop_none=True) + + run_ticket_query(container=container) + + return + + +@phantom.playbook_block() +def input_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("input_filter() called") + + ################################################################################ + # Creates a dataset without None values. + ################################################################################ + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["playbook_input:search_term", "!=", None] + ], + name="input_filter:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + space_delimiter_input(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 process_results(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("process_results() called") + + ################################################################################ + # Iterates through the results of the run ticket query to link playbook input + # search term to their associated tickets. + ################################################################################ + + filtered_input_0_search_term = phantom.collect2(container=container, datapath=["filtered-data:input_filter:condition_1:playbook_input:search_term"]) + run_ticket_query_result_data = phantom.collect2(container=container, datapath=["run_ticket_query:action_result.data","run_ticket_query:action_result.parameter.query_table"], action_results=results) + + filtered_input_0_search_term_values = [item[0] for item in filtered_input_0_search_term] + run_ticket_query_result_item_0 = [item[0] for item in run_ticket_query_result_data] + run_ticket_query_parameter_query_table = [item[1] for item in run_ticket_query_result_data] + + process_results__output = None + + ################################################################################ + ## Custom Code Start + ################################################################################ + process_results__output = {} + for search_term in filtered_input_0_search_term_values: + process_results__output[search_term] = [] + for result, query_table in zip(run_ticket_query_result_item_0, run_ticket_query_parameter_query_table): + if not isinstance(result, list): + list_result = [result] + else: + list_result = result + for result_item in list_result: + result_item_values = [item.lower() for item in result_item.values() if isinstance(item, str)] + match = False + for string_value in result_item_values: + if search_term.lower() in string_value: + match = True + break + if match: + process_results__output[search_term].append({**result_item, **{"ticket_type": query_table}}) + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_run_data(key="process_results:output", value=json.dumps(process_results__output)) + + build_output(container=container) + + return + + +@phantom.playbook_block() +def build_output(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("build_output() called") + + ################################################################################ + # Extract relevant data and add them to an observable array. + ################################################################################ + + process_results__output = json.loads(_ if (_ := phantom.get_run_data(key="process_results:output")) != "" else "null") # pylint: disable=used-before-assignment + + build_output__observable_array = None + build_output__name = None + build_output__number = None + build_output__message = None + build_output__start_time = None + build_output__end_time = None + build_output__assignee = None + build_output__creator_name = None + build_output__state = None + build_output__matched_fields = None + build_output__source_link = None + build_output__source = None + + ################################################################################ + ## Custom Code Start + ################################################################################ + import re + + build_output__observable_array = [] + build_output__name = [] + build_output__number = [] + build_output__message = [] + build_output__start_time = [] + build_output__end_time = [] + build_output__assignee = [] + build_output__creator_name = [] + build_output__state = [] + build_output__matched_fields = [] + build_output__source_link = [] + build_output__source = [] + + def generate_ticket_link(sample_url, ticket_type, sys_id): + extract_host = re.search(r"https*:\/\/[^\/]+", sample_url).group(0) + extract_host += f"/nav_to.do?uri={ticket_type}.do?sys_id={sys_id}" + return extract_host + + for key in process_results__output.keys(): + + for value in process_results__output[key]: + assigned_to = None + caller_id = None + matched_fields = [] + if value.get("assigned_to"): + assigned_to = value["assigned_to"]["display_value"] + if value.get("caller_id"): + caller_id = value["caller_id"]["display_value"] + + for k, v in value.items(): + # generate matched fields where the searched entity appears + if isinstance(v, str) and key.lower() in v.lower(): + matched_fields.append(k) + # search for any link sample: + if isinstance(v, dict): + sample_link = v.get('link') + + source_link = generate_ticket_link(sample_link, value['ticket_type'], value['sys_id']) + observable_object = { + "value": key, + "ticket": { + "name": value["short_description"], + "id": value["sys_id"], + "number": value["number"], + "message": json.dumps(value["description"]), + "start_time": value["sys_created_on"], + "end_time": value["closed_at"], + "assigned_to": assigned_to, + "creator_name": caller_id, + "state": value["state"], + "notes": [value["work_notes"]], + "comments": [value["comments"]] + }, + "matched_fields": matched_fields, + "source": "ServiceNow", + "source_link": source_link + } + build_output__observable_array.append(observable_object) + build_output__name.append(value["short_description"]) + build_output__number.append(value["number"]) + build_output__message.append(json.dumps(value["description"])) # eliminate new line issues + build_output__start_time.append(value["sys_created_on"]) + build_output__end_time.append(value["closed_at"]) + build_output__assignee.append(assigned_to) + build_output__creator_name.append(caller_id) + build_output__state.append(value["state"]) + build_output__matched_fields.append(matched_fields) + build_output__source.append("ServiceNow") + build_output__source_link.append(source_link) + #phantom.debug(observable_object) + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_run_data(key="build_output:observable_array", value=json.dumps(build_output__observable_array)) + phantom.save_run_data(key="build_output:name", value=json.dumps(build_output__name)) + phantom.save_run_data(key="build_output:number", value=json.dumps(build_output__number)) + phantom.save_run_data(key="build_output:message", value=json.dumps(build_output__message)) + phantom.save_run_data(key="build_output:start_time", value=json.dumps(build_output__start_time)) + phantom.save_run_data(key="build_output:end_time", value=json.dumps(build_output__end_time)) + phantom.save_run_data(key="build_output:assignee", value=json.dumps(build_output__assignee)) + phantom.save_run_data(key="build_output:creator_name", value=json.dumps(build_output__creator_name)) + phantom.save_run_data(key="build_output:state", value=json.dumps(build_output__state)) + phantom.save_run_data(key="build_output:matched_fields", value=json.dumps(build_output__matched_fields)) + phantom.save_run_data(key="build_output:source_link", value=json.dumps(build_output__source_link)) + phantom.save_run_data(key="build_output:source", value=json.dumps(build_output__source)) + + format_report(container=container) + + return + + +@phantom.playbook_block() +def calculate_earliest_time(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("calculate_earliest_time() called") + + create_time_value = container.get("create_time", None) + + parameters = [] + + parameters.append({ + "input_datetime": create_time_value, + "amount_to_modify": -30, + "modification_unit": "days", + "input_format_string": "%Y-%m-%d %H:%M:%S.%f+00", + "output_format_string": "'%Y-%m-%d','%H:%M:%S'", + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.custom_function(custom_function="community/datetime_modify", parameters=parameters, name="calculate_earliest_time", callback=input_filter) + + return + + +@phantom.playbook_block() +def run_ticket_query(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("run_ticket_query() called") + + # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED'))) + + query_formatted_string = phantom.format( + container=container, + template="""sysparm_query=active=true^IR_OR_QUERY={0}^opened_at>javascript:gs.dateGenerate({1})&sysparm_display_value=true \n\n""", + parameters=[ + "space_delimiter_input:formatted_data", + "calculate_earliest_time:custom_function_result.data.datetime_string" + ]) + + ################################################################################ + # Perform a text search match within ServiceNow. + ################################################################################ + + calculate_earliest_time__result = phantom.collect2(container=container, datapath=["calculate_earliest_time:custom_function_result.data.datetime_string"]) + convert_table_list__result = phantom.collect2(container=container, datapath=["convert_table_list:custom_function_result.data.output"]) + space_delimiter_input = phantom.get_format_data(name="space_delimiter_input") + + parameters = [] + + # build parameters list for 'run_ticket_query' call + for calculate_earliest_time__result_item in calculate_earliest_time__result: + for convert_table_list__result_item in convert_table_list__result: + if query_formatted_string is not None and convert_table_list__result_item[0] is not None: + parameters.append({ + "query": query_formatted_string, + "max_results": 100, + "query_table": convert_table_list__result_item[0], + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.act("run query", parameters=parameters, name="run_ticket_query", assets=["servicenow"], callback=process_results) + + return + + +@phantom.playbook_block() +def format_report(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("format_report() called") + + ################################################################################ + # Format a summary table with the information gathered from the playbook. + ################################################################################ + + template = """SOAR retrieved tickets from Service Now. The table below shows a summary of the information gathered.\n\n| Name | Number | Message | Start Time | End Time | Assignee | Creator Name | State | Matched Fields | Source | Source Link |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n%%\n| {0} | {1} | {2} | {3} | {4} | {5} | {6} | {7} | {8} | {9} | {10} |\n%%\n""" + + # parameter list for template variable replacement + parameters = [ + "build_output:custom_function:name", + "build_output:custom_function:number", + "build_output:custom_function:message", + "build_output:custom_function:start_time", + "build_output:custom_function:end_time", + "build_output:custom_function:assignee", + "build_output:custom_function:creator_name", + "build_output:custom_function:state", + "build_output:custom_function:matched_fields", + "build_output:custom_function:source", + "build_output:custom_function:source_link" + ] + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.format(container=container, template=template, parameters=parameters, name="format_report") + + return + + +@phantom.playbook_block() +def on_finish(container, summary): + phantom.debug("on_finish() called") + + format_report = phantom.get_format_data(name="format_report") + build_output__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="build_output:observable_array")) != "" else "null") # pylint: disable=used-before-assignment + + output = { + "observable": build_output__observable_array, + "markdown_report": format_report, + } + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_playbook_output_data(output=output) + + return \ No newline at end of file diff --git a/playbooks/ServiceNow_Related_Tickets_Search.yml b/playbooks/ServiceNow_Related_Tickets_Search.yml new file mode 100644 index 0000000000..5cf7e82961 --- /dev/null +++ b/playbooks/ServiceNow_Related_Tickets_Search.yml @@ -0,0 +1,24 @@ +name: ServiceNow Related Tickets Search +id: fc0edc96-ff2b-48b0-9b4d-63da61bafe74 +version: 1 +date: '2023-02-28' +author: Patrick Bareiss, Splunk +type: Investigation +description: "Accepts a user or device and identifies if related tickets exists in a timeframe of last 30 days. Generates a global report and list of observables." +playbook: ServiceNow_Related_Tickets_Search +how_to_implement: This input playbook requires the ServiceNow connector to be configured. It is designed to work in conjunction with the Dynamic Related Tickets Seach playbook or other playbooks in the same style. +references: + - https://d3fend.mitre.org/technique/d3f:IdentifierReputationAnalysis/ +app_list: + - Splunk +tags: + platform_tags: + - user + - device + - ServiceNow + - ticket + playbook_type: Input + vpe_type: Modern + playbook_fields: [] + product: + - Splunk SOAR \ No newline at end of file diff --git a/playbooks/Splunk_Notable_Related_Tickets_Search.json b/playbooks/Splunk_Notable_Related_Tickets_Search.json new file mode 100644 index 0000000000..d471475d5d --- /dev/null +++ b/playbooks/Splunk_Notable_Related_Tickets_Search.json @@ -0,0 +1,428 @@ +{ + "blockly": false, + "blockly_xml": "", + "category": "Dynamic Related Ticket Search", + "coa": { + "data": { + "description": "Accepts a user or device and identifies if related notables exists in a timeframe of last 24 hours.. Generates a global report and list of observables.", + "edges": [ + { + "id": "port_0_to_port_2", + "sourceNode": "0", + "sourcePort": "0_out", + "targetNode": "2", + "targetPort": "2_in" + }, + { + "id": "port_10_to_port_3", + "sourceNode": "10", + "sourcePort": "10_out", + "targetNode": "3", + "targetPort": "3_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_2_to_port_10", + "sourceNode": "2", + "sourcePort": "2_out", + "targetNode": "10", + "targetPort": "10_in" + }, + { + "id": "port_7_to_port_12", + "sourceNode": "7", + "sourcePort": "7_out", + "targetNode": "12", + "targetPort": "12_in" + }, + { + "id": "port_12_to_port_1", + "sourceNode": "12", + "sourcePort": "12_out", + "targetNode": "1", + "targetPort": "1_in" + }, + { + "id": "port_15_to_port_7", + "sourceNode": "15", + "sourcePort": "15_out", + "targetNode": "7", + "targetPort": "7_in" + }, + { + "id": "port_3_to_port_17", + "sourceNode": "3", + "sourcePort": "3_out", + "targetNode": "17", + "targetPort": "17_in" + }, + { + "conditions": [ + { + "index": 0 + } + ], + "id": "port_17_to_port_15", + "sourceNode": "17", + "sourcePort": "17_out", + "targetNode": "15", + "targetPort": "15_in" + } + ], + "hash": "97d4448db53d4f9f36897f9da6cc35bd6c97e460", + "nodes": { + "0": { + "data": { + "advanced": { + "join": [] + }, + "functionName": "on_start", + "id": "0", + "type": "start" + }, + "errors": {}, + "id": "0", + "type": "start", + "warnings": {}, + "x": 19.999999999999986, + "y": -6.394884621840902e-14 + }, + "1": { + "data": { + "advanced": { + "join": [] + }, + "functionName": "on_finish", + "id": "1", + "type": "end" + }, + "errors": {}, + "id": "1", + "type": "end", + "warnings": {}, + "x": 19.999999999999986, + "y": 1398 + }, + "10": { + "data": { + "advanced": { + "customName": "comma separated user", + "customNameId": 0, + "description": "Convert playbook user input list into comma separated string for Splunk query.", + "drop_none": true, + "join": [], + "note": "Convert playbook user input list into comma separated string for Splunk query.", + "separator": "*,*" + }, + "functionId": 2, + "functionName": "comma_separated_user", + "id": "10", + "parameters": [ + "filtered-data:input_filter:condition_1:playbook_input:search_term" + ], + "template": "*{0}*", + "type": "format" + }, + "errors": {}, + "id": "10", + "type": "format", + "warnings": {}, + "x": 0, + "y": 328 + }, + "12": { + "data": { + "advanced": { + "customName": "format report", + "customNameId": 0, + "description": "Format a summary table with the information gathered from the playbook.", + "join": [], + "note": "Format a summary table with the information gathered from the playbook." + }, + "functionId": 4, + "functionName": "format_report", + "id": "12", + "parameters": [ + "build_output:custom_function:name", + "build_output:custom_function:number", + "build_output:custom_function:message", + "build_output:custom_function:start_time", + "build_output:custom_function:end_time", + "build_output:custom_function:assignee", + "build_output:custom_function:creator_name", + "build_output:custom_function:state" + ], + "template": "SOAR retrieved tickets from Splunk. The table below shows a summary of the information gathered.\n\n| Name | Number | Message | Start Time | End Time | Assignee | Creator Name | State | Source |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n%%\n| {0} | {1} | {2} | {3} | {4} | {5} | {6} | {7} | Splunk Enterprise Security |\n%%\n", + "type": "format" + }, + "errors": {}, + "id": "12", + "type": "format", + "warnings": {}, + "x": 0, + "y": 1220 + }, + "15": { + "data": { + "advanced": { + "customName": "process results", + "customNameId": 0, + "description": "Iterates through the results of the search notable query to link playbook input search term to their associated notables.", + "join": [], + "note": "Iterates through the results of the search notable query to link playbook input search term to their associated notables." + }, + "functionId": 1, + "functionName": "process_results", + "id": "15", + "inputParameters": [ + "filtered-data:input_filter:condition_1:playbook_input:search_term", + "filtered-data:search_results_filter:condition_1:search_notables:action_result.data" + ], + "outputVariables": [ + "output" + ], + "type": "code" + }, + "errors": {}, + "id": "15", + "type": "code", + "userCode": "\n process_results__output = {}\n for search_term in filtered_input_0_search_term_values:\n process_results__output[search_term] = []\n for result in filtered_result_0_data:\n if not isinstance(result, list):\n list_result = [result]\n else:\n list_result = result\n for result_item in list_result:\n result_item_values = [item.lower() for item in result_item.values() if isinstance(item, str)]\n match = False\n for string_value in result_item_values:\n if search_term.lower() in string_value:\n match = True\n break\n if match:\n process_results__output[search_term].append({**result_item})\n\n", + "warnings": {}, + "x": 0, + "y": 864 + }, + "17": { + "data": { + "advanced": { + "customName": "search results filter", + "customNameId": 0, + "description": "Determine if search results exist from the previous query.", + "join": [], + "note": "Determine if search results exist from the previous query." + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": ">", + "param": "search_notables:action_result.summary.total_events", + "value": "0" + } + ], + "conditionIndex": 0, + "customName": "results_exist", + "logic": "and" + } + ], + "functionId": 2, + "functionName": "search_results_filter", + "id": "17", + "type": "filter" + }, + "errors": {}, + "id": "17", + "type": "filter", + "warnings": {}, + "x": 60, + "y": 684 + }, + "2": { + "data": { + "advanced": { + "customName": "input_filter", + "customNameId": 0, + "description": "Creates a dataset without None values.", + "join": [], + "note": "Creates a dataset without None values." + }, + "conditions": [ + { + "comparisons": [ + { + "conditionIndex": 0, + "op": "!=", + "param": "playbook_input:search_term", + "value": "None" + } + ], + "conditionIndex": 0, + "customName": "output_exists", + "logic": "and" + } + ], + "functionId": 1, + "functionName": "input_filter", + "id": "2", + "type": "filter" + }, + "errors": {}, + "id": "2", + "type": "filter", + "warnings": { + "config": [ + "Reconfigure invalid datapath." + ] + }, + "x": 60, + "y": 148 + }, + "3": { + "customCode": null, + "data": { + "action": "run query", + "actionType": "investigate", + "advanced": { + "customName": "search notables", + "customNameId": 0, + "description": "Retrieve a list of notables which matched the given search input.", + "join": [], + "note": "Retrieve a list of notables which matched the given search input." + }, + "connector": "Splunk", + "connectorConfigs": [ + "splunk" + ], + "connectorId": "91883aa8-9c81-470b-97a1-5d8f7995f560", + "connectorVersion": "v1", + "functionId": 1, + "functionName": "search_notables", + "id": "3", + "parameters": { + "attach_result": true, + "command": "search", + "display": "", + "end_time": "now", + "query": { + "functionId": 1, + "parameters": [ + "comma_separated_user:formatted_data" + ], + "template": "`notable` | search _raw IN ({0})\n" + }, + "search_mode": "smart", + "start_time": "-24h" + }, + "requiredParameters": [ + { + "data_type": "string", + "field": "query" + }, + { + "data_type": "string", + "default": "search", + "field": "command" + }, + { + "data_type": "string", + "default": "smart", + "field": "search_mode" + } + ], + "tab": "byConnector", + "type": "action" + }, + "errors": {}, + "id": "3", + "type": "action", + "userCode": null, + "warnings": {}, + "x": 0, + "y": 506 + }, + "7": { + "customCode": null, + "data": { + "advanced": { + "customName": "build output", + "customNameId": 0, + "description": "Extract relevant data and add them to an observable array.", + "join": [], + "note": "Extract relevant data and add them to an observable array." + }, + "functionId": 2, + "functionName": "build_output", + "id": "7", + "inputParameters": [ + "process_results:custom_function:output" + ], + "outputVariables": [ + "observable_array", + "name", + "id", + "number", + "message", + "start_time", + "end_time", + "assignee", + "creator_name", + "state", + "notes", + "comments" + ], + "type": "code" + }, + "errors": {}, + "id": "7", + "type": "code", + "userCode": " build_output__observable_array = []\n build_output__name = []\n build_output__id = []\n build_output__number = []\n build_output__message = []\n build_output__start_time = []\n build_output__end_time = []\n build_output__assignee = []\n build_output__creator_name = []\n build_output__state = []\n build_output__notes = []\n build_output__comments = []\n \n \n for key in process_results__output.keys():\n \n for result in process_results__output[key]:\n if \"comment\" in result:\n if isinstance(result[\"comment\"], str):\n comments = [result[\"comment\"]]\n else:\n comments = result[\"comment\"]\n else:\n comments = []\n \n matched_fields = []\n for k, v in result.items():\n # generate matched fields where the searched entity appears\n if isinstance(v, str) and key.lower() in v.lower():\n matched_fields.append(k)\n\n observable_object = {\n \"value\": key,\n \"ticket\": {\n \"name\": result['rule_title'] if result.get('rule_title') else result.get(\"search_name\"),\n \"id\": result.get(\"event_id\"),\n \"number\": result.get(\"notable_xref_id\"),\n \"message\": result['rule_description'] if result.get(\"rule_description\") else result.get(\"savedsearch_description\"),\n \"start_time\": result.get(\"_time\"),\n \"end_time\": \"\",\n \"assigned_to\": result.get(\"owner\"),\n \"creator_name\": \"\",\n \"state\": result.get(\"status_label\"),\n \"notes\": [],\n \"comments\": comments\n },\n \"matched_fields\": matched_fields,\n \"source\": \"Splunk Enterprise Security\"\n }\n build_output__observable_array.append(observable_object)\n build_output__name.append(result.get(\"search_name\"))\n build_output__id.append(result.get(\"event_id\"))\n build_output__number.append(result.get(\"notable_xref_id\"))\n build_output__message.append(result.get(\"savedsearch_description\"))\n build_output__start_time.append(result.get(\"_time\"))\n build_output__end_time.append(\"\")\n build_output__assignee.append(result.get(\"owner\"))\n build_output__creator_name.append(\"\")\n build_output__state.append(result.get(\"status_label\"))\n build_output__notes.append([])\n build_output__comments.append(comments)\n \n \n", + "warnings": {}, + "x": 0, + "y": 1042 + } + }, + "notes": "Inputs: user, device\nInteractions: Splunk\nActions: run query\nOutputs: report, observables" + }, + "input_spec": [ + { + "contains": [ + "user name", + "host name" + ], + "description": "Find notables in Splunk that contains the given search_term.", + "name": "search_term" + } + ], + "output_spec": [ + { + "contains": [], + "datapaths": [ + "build_output:custom_function:observable_array" + ], + "deduplicate": false, + "description": "An array of observable dictionaries with value, type and information about the retrieved notables.", + "metadata": {}, + "name": "observable" + }, + { + "contains": [], + "datapaths": [ + "format_report:formatted_data" + ], + "deduplicate": false, + "description": "An array of reports.", + "metadata": {}, + "name": "markdown_report" + } + ], + "playbook_type": "data", + "python_version": "3", + "schema": "5.0.9", + "version": "6.0.0.114895" + }, + "create_time": "2023-03-03T21:14:02.937956+00:00", + "draft_mode": false, + "labels": [ + "*" + ], + "tags": [ + "user", + "device", + "splunk", + "ticket" + ] +} \ No newline at end of file diff --git a/playbooks/Splunk_Notable_Related_Tickets_Search.png b/playbooks/Splunk_Notable_Related_Tickets_Search.png new file mode 100644 index 0000000000..5110524360 Binary files /dev/null and b/playbooks/Splunk_Notable_Related_Tickets_Search.png differ diff --git a/playbooks/Splunk_Notable_Related_Tickets_Search.py b/playbooks/Splunk_Notable_Related_Tickets_Search.py new file mode 100644 index 0000000000..07d7f0061c --- /dev/null +++ b/playbooks/Splunk_Notable_Related_Tickets_Search.py @@ -0,0 +1,367 @@ +""" +Accepts a user or device and identifies if related notables exists in a timeframe of last 24 hours.. Generates a global report and list of observables. +""" + + +import phantom.rules as phantom +import json +from datetime import datetime, timedelta + + +@phantom.playbook_block() +def on_start(container): + phantom.debug('on_start() called') + + # call 'input_filter' block + input_filter(container=container) + + return + +@phantom.playbook_block() +def input_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("input_filter() called") + + ################################################################################ + # Creates a dataset without None values. + ################################################################################ + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["playbook_input:search_term", "!=", None] + ], + name="input_filter:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + comma_separated_user(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 search_notables(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("search_notables() called") + + # phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED'))) + + query_formatted_string = phantom.format( + container=container, + template="""`notable` | search _raw IN ({0})\n""", + parameters=[ + "comma_separated_user:formatted_data" + ]) + + ################################################################################ + # Retrieve a list of notables which matched the given search input. + ################################################################################ + + comma_separated_user = phantom.get_format_data(name="comma_separated_user") + + parameters = [] + + if query_formatted_string is not None: + parameters.append({ + "query": query_formatted_string, + "command": "search", + "display": "", + "end_time": "now", + "start_time": "-24h", + "search_mode": "smart", + "attach_result": True, + }) + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.act("run query", parameters=parameters, name="search_notables", assets=["splunk"], callback=search_results_filter) + + return + + +@phantom.playbook_block() +def build_output(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("build_output() called") + + ################################################################################ + # Extract relevant data and add them to an observable array. + ################################################################################ + + process_results__output = json.loads(_ if (_ := phantom.get_run_data(key="process_results:output")) != "" else "null") # pylint: disable=used-before-assignment + + build_output__observable_array = None + build_output__name = None + build_output__id = None + build_output__number = None + build_output__message = None + build_output__start_time = None + build_output__end_time = None + build_output__assignee = None + build_output__creator_name = None + build_output__state = None + build_output__notes = None + build_output__comments = None + + ################################################################################ + ## Custom Code Start + ################################################################################ + build_output__observable_array = [] + build_output__name = [] + build_output__id = [] + build_output__number = [] + build_output__message = [] + build_output__start_time = [] + build_output__end_time = [] + build_output__assignee = [] + build_output__creator_name = [] + build_output__state = [] + build_output__notes = [] + build_output__comments = [] + + + for key in process_results__output.keys(): + + for result in process_results__output[key]: + if "comment" in result: + if isinstance(result["comment"], str): + comments = [result["comment"]] + else: + comments = result["comment"] + else: + comments = [] + + matched_fields = [] + for k, v in result.items(): + # generate matched fields where the searched entity appears + if isinstance(v, str) and key.lower() in v.lower(): + matched_fields.append(k) + + observable_object = { + "value": key, + "ticket": { + "name": result['rule_title'] if result.get('rule_title') else result.get("search_name"), + "id": result.get("event_id"), + "number": result.get("notable_xref_id"), + "message": result['rule_description'] if result.get("rule_description") else result.get("savedsearch_description"), + "start_time": result.get("_time"), + "end_time": "", + "assigned_to": result.get("owner"), + "creator_name": "", + "state": result.get("status_label"), + "notes": [], + "comments": comments + }, + "matched_fields": matched_fields, + "source": "Splunk Enterprise Security" + } + build_output__observable_array.append(observable_object) + build_output__name.append(result.get("search_name")) + build_output__id.append(result.get("event_id")) + build_output__number.append(result.get("notable_xref_id")) + build_output__message.append(result.get("savedsearch_description")) + build_output__start_time.append(result.get("_time")) + build_output__end_time.append("") + build_output__assignee.append(result.get("owner")) + build_output__creator_name.append("") + build_output__state.append(result.get("status_label")) + build_output__notes.append([]) + build_output__comments.append(comments) + + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_run_data(key="build_output:observable_array", value=json.dumps(build_output__observable_array)) + phantom.save_run_data(key="build_output:name", value=json.dumps(build_output__name)) + phantom.save_run_data(key="build_output:id", value=json.dumps(build_output__id)) + phantom.save_run_data(key="build_output:number", value=json.dumps(build_output__number)) + phantom.save_run_data(key="build_output:message", value=json.dumps(build_output__message)) + phantom.save_run_data(key="build_output:start_time", value=json.dumps(build_output__start_time)) + phantom.save_run_data(key="build_output:end_time", value=json.dumps(build_output__end_time)) + phantom.save_run_data(key="build_output:assignee", value=json.dumps(build_output__assignee)) + phantom.save_run_data(key="build_output:creator_name", value=json.dumps(build_output__creator_name)) + phantom.save_run_data(key="build_output:state", value=json.dumps(build_output__state)) + phantom.save_run_data(key="build_output:notes", value=json.dumps(build_output__notes)) + phantom.save_run_data(key="build_output:comments", value=json.dumps(build_output__comments)) + + format_report(container=container) + + return + + +@phantom.playbook_block() +def comma_separated_user(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("comma_separated_user() called") + + ################################################################################ + # Convert playbook user input list into comma separated string for Splunk query. + ################################################################################ + + template = """*{0}*""" + + # parameter list for template variable replacement + parameters = [ + "filtered-data:input_filter:condition_1:playbook_input:search_term" + ] + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.format(container=container, template=template, parameters=parameters, name="comma_separated_user", separator="*,*", drop_none=True) + + search_notables(container=container) + + return + + +@phantom.playbook_block() +def format_report(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("format_report() called") + + ################################################################################ + # Format a summary table with the information gathered from the playbook. + ################################################################################ + + template = """SOAR retrieved tickets from Splunk. The table below shows a summary of the information gathered.\n\n| Name | Number | Message | Start Time | End Time | Assignee | Creator Name | State | Source |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n%%\n| {0} | {1} | {2} | {3} | {4} | {5} | {6} | {7} | Splunk Enterprise Security |\n%%\n""" + + # parameter list for template variable replacement + parameters = [ + "build_output:custom_function:name", + "build_output:custom_function:number", + "build_output:custom_function:message", + "build_output:custom_function:start_time", + "build_output:custom_function:end_time", + "build_output:custom_function:assignee", + "build_output:custom_function:creator_name", + "build_output:custom_function:state" + ] + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.format(container=container, template=template, parameters=parameters, name="format_report") + + return + + +@phantom.playbook_block() +def process_results(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("process_results() called") + + ################################################################################ + # Iterates through the results of the search notable query to link playbook input + # search term to their associated notables. + ################################################################################ + + filtered_input_0_search_term = phantom.collect2(container=container, datapath=["filtered-data:input_filter:condition_1:playbook_input:search_term"]) + filtered_result_0_data_search_results_filter = phantom.collect2(container=container, datapath=["filtered-data:search_results_filter:condition_1:search_notables:action_result.data"]) + + filtered_input_0_search_term_values = [item[0] for item in filtered_input_0_search_term] + filtered_result_0_data = [item[0] for item in filtered_result_0_data_search_results_filter] + + process_results__output = None + + ################################################################################ + ## Custom Code Start + ################################################################################ + + process_results__output = {} + for search_term in filtered_input_0_search_term_values: + process_results__output[search_term] = [] + for result in filtered_result_0_data: + if not isinstance(result, list): + list_result = [result] + else: + list_result = result + for result_item in list_result: + result_item_values = [item.lower() for item in result_item.values() if isinstance(item, str)] + match = False + for string_value in result_item_values: + if search_term.lower() in string_value: + match = True + break + if match: + process_results__output[search_term].append({**result_item}) + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_run_data(key="process_results:output", value=json.dumps(process_results__output)) + + build_output(container=container) + + return + + +@phantom.playbook_block() +def search_results_filter(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs): + phantom.debug("search_results_filter() called") + + ################################################################################ + # Determine if search results exist from the previous query. + ################################################################################ + + # collect filtered artifact ids and results for 'if' condition 1 + matched_artifacts_1, matched_results_1 = phantom.condition( + container=container, + conditions=[ + ["search_notables:action_result.summary.total_events", ">", 0] + ], + name="search_results_filter:condition_1") + + # call connected blocks if filtered artifacts or results + if matched_artifacts_1 or matched_results_1: + process_results(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_report = phantom.get_format_data(name="format_report") + build_output__observable_array = json.loads(_ if (_ := phantom.get_run_data(key="build_output:observable_array")) != "" else "null") # pylint: disable=used-before-assignment + + output = { + "observable": build_output__observable_array, + "markdown_report": format_report, + } + + ################################################################################ + ## Custom Code Start + ################################################################################ + + # Write your custom code here... + + ################################################################################ + ## Custom Code End + ################################################################################ + + phantom.save_playbook_output_data(output=output) + + return \ No newline at end of file diff --git a/playbooks/Splunk_Notable_Related_Tickets_Search.yml b/playbooks/Splunk_Notable_Related_Tickets_Search.yml new file mode 100644 index 0000000000..c94b5a58c2 --- /dev/null +++ b/playbooks/Splunk_Notable_Related_Tickets_Search.yml @@ -0,0 +1,24 @@ +name: Splunk Notable Related Tickets Search +id: fc0edc96-ff2b-58b0-9b4d-43bc61bafe74 +version: 1 +date: '2023-02-28' +author: Patrick Bareiss, Splunk +type: Investigation +description: "Accepts a user or device and identifies if related notables exists in a timeframe of last 24 hours. Generates a global report and list of observables." +playbook: Splunk_Notable_Related_Tickets_Search +how_to_implement: This input playbook requires the Splunk connector to be configured. It is designed to work in conjunction with the Dynamic Related Tickets Seach playbook or other playbooks in the same style. +references: + - https://d3fend.mitre.org/technique/d3f:IdentifierReputationAnalysis/ +app_list: + - Splunk +tags: + platform_tags: + - user + - device + - splunk + - ticket + playbook_type: Input + vpe_type: Modern + playbook_fields: [] + product: + - Splunk SOAR \ No newline at end of file