mirror of
https://github.com/splunk/security_content
synced 2026-06-08 17:32:49 +00:00
Merge pull request #1876 from splunk/lateralmovement_playbooks
Adding custom functions & playbooks
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 279 KiB |
@@ -0,0 +1,214 @@
|
||||
"""
|
||||
This playbook resets the password of a potentially compromised user account. First, an analyst is prompted to evaluate the situation and choose whether to reset the account. If they approve, a strong password is generated and the password is reset.
|
||||
"""
|
||||
|
||||
import phantom.rules as phantom
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
##############################
|
||||
# Start - Global Code Block
|
||||
|
||||
from random import randint
|
||||
from random import shuffle
|
||||
|
||||
# End - Global Code block
|
||||
##############################
|
||||
|
||||
def on_start(container):
|
||||
phantom.debug('on_start() called')
|
||||
|
||||
reset_password(container=container)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Custom code block that generates a strong random password
|
||||
"""
|
||||
def generate_password(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('generate_password() called')
|
||||
|
||||
input_parameter_0 = ""
|
||||
|
||||
generate_password__strong_password = None
|
||||
|
||||
################################################################################
|
||||
## Custom Code Start
|
||||
################################################################################
|
||||
|
||||
alpha = 'abcdefghijklmnopqrstuvwxyz'
|
||||
num = '0123456789'
|
||||
special = '!@#$%^&*('
|
||||
|
||||
pwd = ''
|
||||
for i in range(5):
|
||||
pwd += alpha[randint(0, len(alpha)-1)]
|
||||
pwd += (alpha[randint(0, len(alpha)-1)]).upper()
|
||||
pwd += num[randint(0, len(num)-1)]
|
||||
pwd += special[randint(0, len(special)-1)]
|
||||
r = list(pwd)
|
||||
shuffle(r)
|
||||
generate_password__strong_password = ''.join(r)
|
||||
|
||||
################################################################################
|
||||
## Custom Code End
|
||||
################################################################################
|
||||
|
||||
phantom.save_run_data(key='generate_password:strong_password', value=json.dumps(generate_password__strong_password))
|
||||
reset_ad_password(container=container)
|
||||
format_pwd_message(container=container)
|
||||
|
||||
return
|
||||
|
||||
def reset_password(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('reset_password() called')
|
||||
|
||||
# set user and message variables for phantom.prompt call
|
||||
user = "admin"
|
||||
message = """Found the account \"{0}\" has a compromised credential! Would you like to automatically reset the password?"""
|
||||
|
||||
# parameter list for template variable replacement
|
||||
parameters = [
|
||||
"artifact:*.cef.compromisedUserName",
|
||||
]
|
||||
|
||||
#responses:
|
||||
response_types = [
|
||||
{
|
||||
"prompt": "",
|
||||
"options": {
|
||||
"type": "list",
|
||||
"choices": [
|
||||
"Yes",
|
||||
"No",
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
phantom.prompt2(container=container, user=user, message=message, respond_in_mins=30, name="reset_password", parameters=parameters, response_types=response_types, callback=reset_option)
|
||||
|
||||
return
|
||||
|
||||
def reset_option(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('reset_option() called')
|
||||
|
||||
# check for 'if' condition 1
|
||||
matched = phantom.decision(
|
||||
container=container,
|
||||
action_results=results,
|
||||
conditions=[
|
||||
["reset_password:action_result.summary.responses.0", "==", "Yes"],
|
||||
])
|
||||
|
||||
# call connected blocks if condition 1 matched
|
||||
if matched:
|
||||
generate_password(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
return
|
||||
|
||||
# call connected blocks for 'else' condition 2
|
||||
format_decline_msg(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Reset the Active Directory password of the user to the generated password
|
||||
"""
|
||||
def reset_ad_password(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('reset_ad_password() called')
|
||||
|
||||
#phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
|
||||
|
||||
generate_password__strong_password = json.loads(phantom.get_run_data(key='generate_password:strong_password'))
|
||||
# collect data for 'reset_ad_password' call
|
||||
container_data = phantom.collect2(container=container, datapath=['artifact:*.cef.compromisedUserName', 'artifact:*.id'])
|
||||
|
||||
parameters = []
|
||||
|
||||
# build parameters list for 'reset_ad_password' call
|
||||
for container_item in container_data:
|
||||
if container_item[0]:
|
||||
parameters.append({
|
||||
'username': container_item[0],
|
||||
'new_password': generate_password__strong_password,
|
||||
# context (artifact id) is added to associate results with the artifact
|
||||
'context': {'artifact_id': container_item[1]},
|
||||
})
|
||||
|
||||
phantom.act(action="set password", parameters=parameters, assets=['active directory'], name="reset_ad_password")
|
||||
|
||||
return
|
||||
|
||||
def add_comment_pwd_reset(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('add_comment_pwd_reset() called')
|
||||
|
||||
formatted_data_1 = phantom.get_format_data(name='format_pwd_message')
|
||||
|
||||
phantom.comment(container=container, comment=formatted_data_1)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Formats a message about the password reset to provide in the comments
|
||||
"""
|
||||
def format_pwd_message(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('format_pwd_message() called')
|
||||
|
||||
template = """Reset user {0} password to {1}"""
|
||||
|
||||
# parameter list for template variable replacement
|
||||
parameters = [
|
||||
"artifact:*.cef.compromisedUserName",
|
||||
"generate_password:custom_function:strong_password",
|
||||
]
|
||||
|
||||
phantom.format(container=container, template=template, parameters=parameters, name="format_pwd_message")
|
||||
|
||||
add_comment_pwd_reset(container=container)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Formats a message stating the user declined to reset the password
|
||||
"""
|
||||
def format_decline_msg(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('format_decline_msg() called')
|
||||
|
||||
template = """Analyst declined to reset password for user: {0}"""
|
||||
|
||||
# parameter list for template variable replacement
|
||||
parameters = [
|
||||
"artifact:*.cef.compromisedUserName",
|
||||
]
|
||||
|
||||
phantom.format(container=container, template=template, parameters=parameters, name="format_decline_msg")
|
||||
|
||||
add_comment_no_reset(container=container)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Add the comment notifying the reader that the password reset was declined
|
||||
"""
|
||||
def add_comment_no_reset(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('add_comment_no_reset() called')
|
||||
|
||||
formatted_data_1 = phantom.get_format_data(name='format_decline_msg')
|
||||
|
||||
phantom.comment(container=container, comment=formatted_data_1)
|
||||
|
||||
return
|
||||
|
||||
def on_finish(container, summary):
|
||||
phantom.debug('on_finish() called')
|
||||
# This function is called after all actions are completed.
|
||||
# summary of all the action and/or all details of actions
|
||||
# can be collected here.
|
||||
|
||||
# summary_json = phantom.get_summary()
|
||||
# if 'result' in summary_json:
|
||||
# for action_result in summary_json['result']:
|
||||
# if 'action_run_id' in action_result:
|
||||
# action_results = phantom.get_action_results(action_run_id=action_result['action_run_id'], result_data=False, flatten=False)
|
||||
# phantom.debug(action_results)
|
||||
|
||||
return
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Active Directory Reset password
|
||||
id: fc0edc96-ff2b-48b0-9f6f-63da6783fd63
|
||||
version: 1
|
||||
date: '2020-12-08'
|
||||
author: Philip Royer, Splunk
|
||||
type: Response
|
||||
description: This playbook resets the password of a potentially compromised user account. First, an analyst is prompted to evaluate the situation and choose whether to reset the account. If they approve, a strong password is generated and the password is reset.
|
||||
playbook: activedirectory_reset_password
|
||||
how_to_implement: This playbook works on artifacts with artifact:*.cef.compromisedUserName which can be created as shown in the playbook "recorded_future_handle_leaked_credentials" - The prompt is hard-coded to use "admin" as the user, so change it to the correct user or role
|
||||
references: []
|
||||
app_list:
|
||||
- "LDAP"
|
||||
tags:
|
||||
platform_tags:
|
||||
- Response
|
||||
playbook_fields:
|
||||
- compromisedUserName
|
||||
product:
|
||||
- Splunk SOAR
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 573 KiB |
@@ -0,0 +1,792 @@
|
||||
"""
|
||||
Enrich and respond to a CrowdStrike Falcon detection involving a potentially malicious executable on an endpoint. Check for previous sightings of the same executable, hunt across other endpoints for the file, gather details about all processes associated with the file, and collect all the gathered information into a prompt for an analyst to review. Based on the analyst's choice, the file can be added to the custom indicators list in CrowdStrike with a detection policy of "detect" or "none", and the endpoint can be optionally quarantined from the network.
|
||||
"""
|
||||
|
||||
import phantom.rules as phantom
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
def on_start(container):
|
||||
phantom.debug('on_start() called')
|
||||
|
||||
# call 'if_sha256_exists' block
|
||||
if_sha256_exists(container=container)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
List all machines where the file hash has been seen.
|
||||
"""
|
||||
def hunt_file_1(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('hunt_file_1() called')
|
||||
|
||||
#phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
|
||||
|
||||
# collect data for 'hunt_file_1' call
|
||||
filtered_artifacts_data_1 = phantom.collect2(container=container, datapath=['filtered-data:filter_main_artifact:condition_1:artifact:*.cef.fileHashSha256', 'filtered-data:filter_main_artifact:condition_1:artifact:*.id'])
|
||||
|
||||
parameters = []
|
||||
|
||||
# build parameters list for 'hunt_file_1' call
|
||||
for filtered_artifacts_item_1 in filtered_artifacts_data_1:
|
||||
if filtered_artifacts_item_1[0]:
|
||||
parameters.append({
|
||||
'hash': filtered_artifacts_item_1[0],
|
||||
'count_only': False,
|
||||
# context (artifact id) is added to associate results with the artifact
|
||||
'context': {'artifact_id': filtered_artifacts_item_1[1]},
|
||||
})
|
||||
|
||||
phantom.act(action="hunt file", parameters=parameters, assets=['crowdstrike_oauth'], callback=get_system_info_1, name="hunt_file_1")
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Ensure that the event has at least one artifact with a SHA256 file hash before attempting to process the event.
|
||||
"""
|
||||
def if_sha256_exists(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('if_sha256_exists() called')
|
||||
|
||||
# check for 'if' condition 1
|
||||
matched = phantom.decision(
|
||||
container=container,
|
||||
conditions=[
|
||||
["artifact:*.cef.fileHashSha256", "!=", ""],
|
||||
])
|
||||
|
||||
# call connected blocks if condition 1 matched
|
||||
if matched:
|
||||
filter_main_artifact(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
return
|
||||
|
||||
# call connected blocks for 'else' condition 2
|
||||
ignore_if_no_sha256(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
End the playbook if no SHA256 file hash is found in any of the artifacts.
|
||||
"""
|
||||
def ignore_if_no_sha256(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('ignore_if_no_sha256() called')
|
||||
|
||||
phantom.comment(container=container, comment="Ignoring alert because no SHA256 file hash was found")
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Fetch the CrowdStrike indicator for the SHA256 file hash, if there is one. This action will fail if there is no matching indicator in CrowdStrike, but the playbook will check for the failure and continue.
|
||||
"""
|
||||
def get_indicator_2(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('get_indicator_2() called')
|
||||
|
||||
# collect data for 'get_indicator_2' call
|
||||
filtered_artifacts_data_1 = phantom.collect2(container=container, datapath=['filtered-data:filter_main_artifact:condition_1:artifact:*.cef.fileHashSha256', 'filtered-data:filter_main_artifact:condition_1:artifact:*.id'])
|
||||
|
||||
parameters = []
|
||||
|
||||
# build parameters list for 'get_indicator_2' call
|
||||
for filtered_artifacts_item_1 in filtered_artifacts_data_1:
|
||||
if filtered_artifacts_item_1[0]:
|
||||
parameters.append({
|
||||
'indicator_type': "sha256",
|
||||
'indicator_value': filtered_artifacts_item_1[0],
|
||||
# context (artifact id) is added to associate results with the artifact
|
||||
'context': {'artifact_id': filtered_artifacts_item_1[1]},
|
||||
})
|
||||
|
||||
phantom.act(action="get indicator", parameters=parameters, assets=['crowdstrike_oauth'], callback=if_indicator_exists, name="get_indicator_2")
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Determine which response to take based on whether an Indicator exists in CrowdStrike for the SHA256 file hash.
|
||||
"""
|
||||
def if_indicator_exists(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('if_indicator_exists() called')
|
||||
|
||||
# check for 'if' condition 1
|
||||
matched = phantom.decision(
|
||||
container=container,
|
||||
action_results=results,
|
||||
conditions=[
|
||||
["Resource Not Found", "in", "get_indicator_2:action_result.message"],
|
||||
])
|
||||
|
||||
# call connected blocks if condition 1 matched
|
||||
if matched:
|
||||
hunt_file_1(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
list_processes_with_hash(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
return
|
||||
|
||||
# call connected blocks for 'else' condition 2
|
||||
indicator_policy_decision(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Escalate the event because the Indicator policy is "detect", meaning the event is a true positive.
|
||||
"""
|
||||
def escalate_severity_to_high(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('escalate_severity_to_high() called')
|
||||
|
||||
phantom.set_severity(container=container, severity="High")
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Format a note to summarize all known information about the event.
|
||||
"""
|
||||
def format_repeat_note(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('format_repeat_note() called')
|
||||
|
||||
template = """CrowdStrike detected a file on an endpoint which matched a previously detected file hash:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Host | {0} |
|
||||
| Command Line | {1} |
|
||||
| SHA 256 | {2} |
|
||||
| File Path | {3}\\\\{4} |
|
||||
| CrowdStrike Detection Link | {5} |
|
||||
|
||||
---
|
||||
|
||||
This event will have the severity escalated to high, and should be investigated further."""
|
||||
|
||||
# parameter list for template variable replacement
|
||||
parameters = [
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.sourceHostName",
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.cmdLine",
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.fileHashSha256",
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.filePath",
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.fileName",
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.falconHostLink",
|
||||
]
|
||||
|
||||
phantom.format(container=container, template=template, parameters=parameters, name="format_repeat_note")
|
||||
|
||||
add_repeat_note(container=container)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Add a note to summarize the event information.
|
||||
"""
|
||||
def add_repeat_note(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('add_repeat_note() called')
|
||||
|
||||
formatted_data_1 = phantom.get_format_data(name='format_repeat_note')
|
||||
|
||||
note_title = "Known Malicious File"
|
||||
note_content = formatted_data_1
|
||||
note_format = "markdown"
|
||||
phantom.add_note(container=container, note_type="general", title=note_title, content=note_content, note_format=note_format)
|
||||
crowdstrike_known_file_quarantine(container=container)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Only process the main detection artifact, not any sub event artifacts.
|
||||
"""
|
||||
def filter_main_artifact(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('filter_main_artifact() called')
|
||||
|
||||
# collect filtered artifact ids for 'if' condition 1
|
||||
matched_artifacts_1, matched_results_1 = phantom.condition(
|
||||
container=container,
|
||||
conditions=[
|
||||
["artifact:*.label", "==", "event"],
|
||||
],
|
||||
name="filter_main_artifact:condition_1")
|
||||
|
||||
# call connected blocks if filtered artifacts or results
|
||||
if matched_artifacts_1 or matched_results_1:
|
||||
get_indicator_2(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function, filtered_artifacts=matched_artifacts_1, filtered_results=matched_results_1)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Handle the Indicator differently if the policy is "detect", "none", or other.
|
||||
"""
|
||||
def indicator_policy_decision(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('indicator_policy_decision() called')
|
||||
|
||||
# check for 'if' condition 1
|
||||
matched = phantom.decision(
|
||||
container=container,
|
||||
action_results=results,
|
||||
conditions=[
|
||||
["get_indicator_2:action_result.data.*.resources.*.policy", "==", "none"],
|
||||
])
|
||||
|
||||
# call connected blocks if condition 1 matched
|
||||
if matched:
|
||||
detection_policy_none(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
return
|
||||
|
||||
# check for 'elif' condition 2
|
||||
matched = phantom.decision(
|
||||
container=container,
|
||||
action_results=results,
|
||||
conditions=[
|
||||
["get_indicator_2:action_result.data.*.resources.*.policy", "==", "detect"],
|
||||
])
|
||||
|
||||
# call connected blocks if condition 2 matched
|
||||
if matched:
|
||||
escalate_severity_to_high(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
format_repeat_note(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
return
|
||||
|
||||
# call connected blocks for 'else' condition 3
|
||||
comment_unexpected_policy(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
End processing because this playbook only expects "none" or "detect" as the Indicator policy.
|
||||
"""
|
||||
def comment_unexpected_policy(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('comment_unexpected_policy() called')
|
||||
|
||||
phantom.comment(container=container, comment="The playbook received an unexpected indicator policy and needs to be extended to handle this situation.")
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Add a comment to explain why the event is being closed.
|
||||
"""
|
||||
def detection_policy_none(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('detection_policy_none() called')
|
||||
|
||||
phantom.comment(container=container, comment="The file hash indicator has a detection policy of none, so previous investigations have found that the file is not harmful. This playbook will take no further action and the event will be closed.")
|
||||
close_event(container=container)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Fetch additional information about each machine listed in the previous step.
|
||||
"""
|
||||
def get_system_info_1(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('get_system_info_1() called')
|
||||
|
||||
#phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
|
||||
|
||||
# collect data for 'get_system_info_1' call
|
||||
results_data_1 = phantom.collect2(container=container, datapath=['hunt_file_1:action_result.data.*.device_id', 'hunt_file_1:action_result.parameter.context.artifact_id'], action_results=results)
|
||||
|
||||
parameters = []
|
||||
|
||||
# build parameters list for 'get_system_info_1' call
|
||||
for results_item_1 in results_data_1:
|
||||
if results_item_1[0]:
|
||||
parameters.append({
|
||||
'id': results_item_1[0],
|
||||
# context (artifact id) is added to associate results with the artifact
|
||||
'context': {'artifact_id': results_item_1[1]},
|
||||
})
|
||||
|
||||
phantom.act(action="get system info", parameters=parameters, assets=['crowdstrike_oauth'], callback=join_format_prompt, name="get_system_info_1", parent_action=action)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Summarize all the gathered information to help the analyst decide a response in the prompt.
|
||||
"""
|
||||
def format_prompt(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('format_prompt() called')
|
||||
|
||||
template = """CrowdStrike detected the following suspicious activity on an endpoint:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Host | {0} |
|
||||
| Command Line | {1} |
|
||||
| SHA 256 | {2} |
|
||||
| File Path | {3}\\\\{4}
|
||||
| CrowdStrike Detection Link | {5} |
|
||||
| Details of processes associated with the file hash | <see \"get process details\" action results> |
|
||||
| Count of machines that have the file on disk | {6} |
|
||||
| System information of machines that have the file on disk | <see \"get system info\" action results> |"""
|
||||
|
||||
# parameter list for template variable replacement
|
||||
parameters = [
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.sourceHostName",
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.cmdLine",
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.fileHashSha256",
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.filePath",
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.fileName",
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.falconHostLink",
|
||||
"hunt_file_1:action_result.summary.device_count",
|
||||
]
|
||||
|
||||
phantom.format(container=container, template=template, parameters=parameters, name="format_prompt")
|
||||
|
||||
crowdstrike_new_file_detection(container=container)
|
||||
|
||||
return
|
||||
|
||||
def join_format_prompt(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None):
|
||||
phantom.debug('join_format_prompt() called')
|
||||
|
||||
# if the joined function has already been called, do nothing
|
||||
if phantom.get_run_data(key='join_format_prompt_called'):
|
||||
return
|
||||
|
||||
# check if all connected incoming playbooks, actions, or custom functions are done i.e. have succeeded or failed
|
||||
if phantom.completed(action_names=['get_process_details']):
|
||||
|
||||
# save the state that the joined function has now been called
|
||||
phantom.save_run_data(key='join_format_prompt_called', value='format_prompt')
|
||||
|
||||
# call connected block "format_prompt"
|
||||
format_prompt(container=container, handle=handle)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Close the event because the Indicator policy is "none", meaning the detection is a false positive.
|
||||
"""
|
||||
def close_event(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('close_event() called')
|
||||
|
||||
phantom.set_status(container=container, status="Closed")
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
List all processes seen on this host associated with this file hash.
|
||||
"""
|
||||
def list_processes_with_hash(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('list_processes_with_hash() called')
|
||||
|
||||
#phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
|
||||
|
||||
# collect data for 'list_processes_with_hash' call
|
||||
filtered_artifacts_data_1 = phantom.collect2(container=container, datapath=['filtered-data:filter_main_artifact:condition_1:artifact:*.cef.sensorId', 'filtered-data:filter_main_artifact:condition_1:artifact:*.cef.fileHashSha256', 'filtered-data:filter_main_artifact:condition_1:artifact:*.id'])
|
||||
|
||||
parameters = []
|
||||
|
||||
# build parameters list for 'list_processes_with_hash' call
|
||||
for filtered_artifacts_item_1 in filtered_artifacts_data_1:
|
||||
if filtered_artifacts_item_1[0] and filtered_artifacts_item_1[1]:
|
||||
parameters.append({
|
||||
'id': filtered_artifacts_item_1[0],
|
||||
'ioc': filtered_artifacts_item_1[1],
|
||||
# context (artifact id) is added to associate results with the artifact
|
||||
'context': {'artifact_id': filtered_artifacts_item_1[2]},
|
||||
})
|
||||
|
||||
phantom.act(action="list processes", parameters=parameters, assets=['crowdstrike_oauth'], callback=get_process_details, name="list_processes_with_hash")
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Fetch additional information about each process listed in the previous step.
|
||||
"""
|
||||
def get_process_details(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('get_process_details() called')
|
||||
|
||||
#phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
|
||||
|
||||
# collect data for 'get_process_details' call
|
||||
results_data_1 = phantom.collect2(container=container, datapath=['list_processes_with_hash:action_result.data.*.falcon_process_id', 'list_processes_with_hash:action_result.parameter.context.artifact_id'], action_results=results)
|
||||
|
||||
parameters = []
|
||||
|
||||
# build parameters list for 'get_process_details' call
|
||||
for results_item_1 in results_data_1:
|
||||
if results_item_1[0]:
|
||||
parameters.append({
|
||||
'falcon_process_id': results_item_1[0],
|
||||
# context (artifact id) is added to associate results with the artifact
|
||||
'context': {'artifact_id': results_item_1[1]},
|
||||
})
|
||||
|
||||
phantom.act(action="get process detail", parameters=parameters, assets=['crowdstrike_oauth'], callback=join_format_prompt, name="get_process_details", parent_action=action)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Prompt the user to determine whether or not to create an Indicator for the file hash and whether or not to quarantine the endpoint.
|
||||
"""
|
||||
def crowdstrike_new_file_detection(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('crowdstrike_new_file_detection() called')
|
||||
|
||||
# set user and message variables for phantom.prompt call
|
||||
user = "admin"
|
||||
message = """{0}"""
|
||||
|
||||
# parameter list for template variable replacement
|
||||
parameters = [
|
||||
"format_prompt:formatted_data",
|
||||
]
|
||||
|
||||
#responses:
|
||||
response_types = [
|
||||
{
|
||||
"prompt": "Should Phantom create an Indicator in CrowdStrike to track this file hash from now on?",
|
||||
"options": {
|
||||
"type": "list",
|
||||
"choices": [
|
||||
"No, do not create an Indicator in CrowdStrike at this time.",
|
||||
"Yes, create a CrowdStrike Indicator to detect and block this file hash from now on. (True Positive)",
|
||||
"Yes, create a CrowdStrike Indicator to ignore this file hash from now on. (False Positive)",
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
"prompt": "Should Phantom quarantine the endpoint?",
|
||||
"options": {
|
||||
"type": "list",
|
||||
"choices": [
|
||||
"Yes",
|
||||
"No",
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
phantom.prompt2(container=container, user=user, message=message, respond_in_mins=30, name="crowdstrike_new_file_detection", parameters=parameters, response_types=response_types, callback=crowdstrike_new_file_detection_callback)
|
||||
|
||||
return
|
||||
|
||||
def crowdstrike_new_file_detection_callback(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None):
|
||||
phantom.debug('crowdstrike_new_file_detection_callback() called')
|
||||
|
||||
indicator_decision(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
quarantine_decision_1(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Parse the prompt response to determine how to handle the indicator.
|
||||
"""
|
||||
def indicator_decision(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('indicator_decision() called')
|
||||
|
||||
# check for 'if' condition 1
|
||||
matched = phantom.decision(
|
||||
container=container,
|
||||
action_results=results,
|
||||
conditions=[
|
||||
["No, do not create an Indicator in CrowdStrike at this time.", "==", "crowdstrike_new_file_detection:action_result.summary.responses.0"],
|
||||
])
|
||||
|
||||
# call connected blocks if condition 1 matched
|
||||
if matched:
|
||||
comment_no_indicator(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
return
|
||||
|
||||
# check for 'elif' condition 2
|
||||
matched = phantom.decision(
|
||||
container=container,
|
||||
action_results=results,
|
||||
conditions=[
|
||||
["Yes, create a CrowdStrike Indicator to detect and block this file hash from now on. (True Positive)", "==", "crowdstrike_new_file_detection:action_result.summary.responses.0"],
|
||||
])
|
||||
|
||||
# call connected blocks if condition 2 matched
|
||||
if matched:
|
||||
format_detect_description(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
return
|
||||
|
||||
# check for 'elif' condition 3
|
||||
matched = phantom.decision(
|
||||
container=container,
|
||||
action_results=results,
|
||||
conditions=[
|
||||
["Yes, create a CrowdStrike Indicator to ignore this file hash going forward (False Positive)", "==", "crowdstrike_new_file_detection:action_result.summary.responses.0"],
|
||||
])
|
||||
|
||||
# call connected blocks if condition 3 matched
|
||||
if matched:
|
||||
format_ignore_description(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
return
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Check the quarantine device prompt response.
|
||||
"""
|
||||
def quarantine_decision_1(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('quarantine_decision_1() called')
|
||||
|
||||
# check for 'if' condition 1
|
||||
matched = phantom.decision(
|
||||
container=container,
|
||||
action_results=results,
|
||||
conditions=[
|
||||
["crowdstrike_new_file_detection:action_result.summary.responses.1", "==", "Yes"],
|
||||
])
|
||||
|
||||
# call connected blocks if condition 1 matched
|
||||
if matched:
|
||||
quarantine_device_1(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
return
|
||||
|
||||
# call connected blocks for 'else' condition 2
|
||||
comment_no_quarantine_1(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Create an Indicator in CrowdStrike with a policy of "none" to ignore detections based on this file hash in the future.
|
||||
"""
|
||||
def create_ignore_indicator(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('create_ignore_indicator() called')
|
||||
|
||||
#phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
|
||||
|
||||
# collect data for 'create_ignore_indicator' call
|
||||
filtered_artifacts_data_1 = phantom.collect2(container=container, datapath=['filtered-data:filter_main_artifact:condition_1:artifact:*.cef.fileHashSha256', 'filtered-data:filter_main_artifact:condition_1:artifact:*.id'])
|
||||
formatted_data_1 = phantom.get_format_data(name='format_ignore_description')
|
||||
|
||||
parameters = []
|
||||
|
||||
# build parameters list for 'create_ignore_indicator' call
|
||||
for filtered_artifacts_item_1 in filtered_artifacts_data_1:
|
||||
if filtered_artifacts_item_1[0]:
|
||||
parameters.append({
|
||||
'ioc': filtered_artifacts_item_1[0],
|
||||
'policy': "none",
|
||||
'source': "Phantom Playbook crowdstrike_malware_triage",
|
||||
'expiration': "",
|
||||
'description': formatted_data_1,
|
||||
'share_level': "red",
|
||||
# context (artifact id) is added to associate results with the artifact
|
||||
'context': {'artifact_id': filtered_artifacts_item_1[1]},
|
||||
})
|
||||
|
||||
phantom.act(action="upload indicator", parameters=parameters, assets=['crowdstrike_oauth'], name="create_ignore_indicator")
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Format a description to provide when creating an Indicator with a policy of "none".
|
||||
"""
|
||||
def format_ignore_description(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('format_ignore_description() called')
|
||||
|
||||
template = """This indicator was created by Phantom in the playbook crowdstrike_malware_triage to ignore CrowdStrike detections based on the file hash first seen in {0} and processed in Phantom as {1}"""
|
||||
|
||||
# parameter list for template variable replacement
|
||||
parameters = [
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.falconHostLink",
|
||||
"container:url",
|
||||
]
|
||||
|
||||
phantom.format(container=container, template=template, parameters=parameters, name="format_ignore_description")
|
||||
|
||||
create_ignore_indicator(container=container)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Explain in a comment that no Indicator will be created.
|
||||
"""
|
||||
def comment_no_indicator(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('comment_no_indicator() called')
|
||||
|
||||
phantom.comment(container=container, comment="The analyst decided not to create a custom indicator for the file hash.")
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Format a description to provide when creating an Indicator with a policy of "detect".
|
||||
"""
|
||||
def format_detect_description(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('format_detect_description() called')
|
||||
|
||||
template = """This indicator was created by Phantom in the playbook crowdstrike_malware_triage to detect and block process executions based on the file hash first seen in {0} and processed in Phantom as {1}"""
|
||||
|
||||
# parameter list for template variable replacement
|
||||
parameters = [
|
||||
"filtered-data:filter_main_artifact:condition_1:artifact:*.cef.falconHostLink",
|
||||
"container:url",
|
||||
]
|
||||
|
||||
phantom.format(container=container, template=template, parameters=parameters, name="format_detect_description")
|
||||
|
||||
create_detect_indicator(container=container)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Create an Indicator to detect and block this file hash.
|
||||
"""
|
||||
def create_detect_indicator(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('create_detect_indicator() called')
|
||||
|
||||
#phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
|
||||
|
||||
# collect data for 'create_detect_indicator' call
|
||||
filtered_artifacts_data_1 = phantom.collect2(container=container, datapath=['filtered-data:filter_main_artifact:condition_1:artifact:*.cef.fileHashSha256', 'filtered-data:filter_main_artifact:condition_1:artifact:*.id'])
|
||||
|
||||
parameters = []
|
||||
|
||||
# build parameters list for 'create_detect_indicator' call
|
||||
for filtered_artifacts_item_1 in filtered_artifacts_data_1:
|
||||
if filtered_artifacts_item_1[0]:
|
||||
parameters.append({
|
||||
'ioc': filtered_artifacts_item_1[0],
|
||||
'policy': "detect",
|
||||
'source': "",
|
||||
'expiration': "",
|
||||
'description': "",
|
||||
'share_level': "red",
|
||||
# context (artifact id) is added to associate results with the artifact
|
||||
'context': {'artifact_id': filtered_artifacts_item_1[1]},
|
||||
})
|
||||
|
||||
phantom.act(action="upload indicator", parameters=parameters, assets=['crowdstrike_oauth'], name="create_detect_indicator")
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Do not quarantine the endpoint because the analyst responded No in the prompt.
|
||||
"""
|
||||
def comment_no_quarantine_1(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('comment_no_quarantine_1() called')
|
||||
|
||||
phantom.comment(container=container, comment="The analyst decided not to quarantine the endpoint.")
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Block the endpoint from everything but the configured allowlist of network addresses while the investigation is ongoing.
|
||||
"""
|
||||
def quarantine_device_1(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('quarantine_device_1() called')
|
||||
|
||||
#phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
|
||||
|
||||
# collect data for 'quarantine_device_1' call
|
||||
filtered_artifacts_data_1 = phantom.collect2(container=container, datapath=['filtered-data:filter_main_artifact:condition_1:artifact:*.cef.sensorId', 'filtered-data:filter_main_artifact:condition_1:artifact:*.id'])
|
||||
|
||||
parameters = []
|
||||
|
||||
# build parameters list for 'quarantine_device_1' call
|
||||
for filtered_artifacts_item_1 in filtered_artifacts_data_1:
|
||||
parameters.append({
|
||||
'hostname': "",
|
||||
'device_id': filtered_artifacts_item_1[0],
|
||||
# context (artifact id) is added to associate results with the artifact
|
||||
'context': {'artifact_id': filtered_artifacts_item_1[1]},
|
||||
})
|
||||
|
||||
phantom.act(action="quarantine device", parameters=parameters, assets=['crowdstrike_oauth'], name="quarantine_device_1")
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Ask the analyst if the endpoint should be quarantined.
|
||||
"""
|
||||
def crowdstrike_known_file_quarantine(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('crowdstrike_known_file_quarantine() called')
|
||||
|
||||
# set user and message variables for phantom.prompt call
|
||||
user = "admin"
|
||||
message = """{0}
|
||||
|
||||
---
|
||||
|
||||
Should Phantom quarantine the device?"""
|
||||
|
||||
# parameter list for template variable replacement
|
||||
parameters = [
|
||||
"format_repeat_note:formatted_data",
|
||||
]
|
||||
|
||||
#responses:
|
||||
response_types = [
|
||||
{
|
||||
"prompt": "",
|
||||
"options": {
|
||||
"type": "list",
|
||||
"choices": [
|
||||
"Yes",
|
||||
"No",
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
phantom.prompt2(container=container, user=user, message=message, respond_in_mins=30, name="crowdstrike_known_file_quarantine", parameters=parameters, response_types=response_types, callback=quarantine_decision_2)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Check if the analyst responded Yes or No to the quarantine.
|
||||
"""
|
||||
def quarantine_decision_2(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('quarantine_decision_2() called')
|
||||
|
||||
# check for 'if' condition 1
|
||||
matched = phantom.decision(
|
||||
container=container,
|
||||
action_results=results,
|
||||
conditions=[
|
||||
["crowdstrike_known_file_quarantine:action_result.summary.responses.0", "==", "Yes"],
|
||||
])
|
||||
|
||||
# call connected blocks if condition 1 matched
|
||||
if matched:
|
||||
quarantine_device_2(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
return
|
||||
|
||||
# call connected blocks for 'else' condition 2
|
||||
comment_no_quarantine_2(action=action, success=success, container=container, results=results, handle=handle, custom_function=custom_function)
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Block the endpoint from everything but the configured allowlist of network addresses while the investigation is ongoing.
|
||||
"""
|
||||
def quarantine_device_2(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('quarantine_device_2() called')
|
||||
|
||||
#phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
|
||||
|
||||
# collect data for 'quarantine_device_2' call
|
||||
filtered_artifacts_data_1 = phantom.collect2(container=container, datapath=['filtered-data:filter_main_artifact:condition_1:artifact:*.cef.sensorId', 'filtered-data:filter_main_artifact:condition_1:artifact:*.id'])
|
||||
|
||||
parameters = []
|
||||
|
||||
# build parameters list for 'quarantine_device_2' call
|
||||
for filtered_artifacts_item_1 in filtered_artifacts_data_1:
|
||||
parameters.append({
|
||||
'hostname': "",
|
||||
'device_id': filtered_artifacts_item_1[0],
|
||||
# context (artifact id) is added to associate results with the artifact
|
||||
'context': {'artifact_id': filtered_artifacts_item_1[1]},
|
||||
})
|
||||
|
||||
phantom.act(action="quarantine device", parameters=parameters, assets=['crowdstrike_oauth'], name="quarantine_device_2")
|
||||
|
||||
return
|
||||
|
||||
"""
|
||||
Do not quarantine the endpoint because the analyst responded No in the prompt.
|
||||
"""
|
||||
def comment_no_quarantine_2(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('comment_no_quarantine_2() called')
|
||||
|
||||
phantom.comment(container=container, comment="The analyst decided not to quarantine the endpoint.")
|
||||
|
||||
return
|
||||
|
||||
def on_finish(container, summary):
|
||||
phantom.debug('on_finish() called')
|
||||
# This function is called after all actions are completed.
|
||||
# summary of all the action and/or all details of actions
|
||||
# can be collected here.
|
||||
|
||||
# summary_json = phantom.get_summary()
|
||||
# if 'result' in summary_json:
|
||||
# for action_result in summary_json['result']:
|
||||
# if 'action_run_id' in action_result:
|
||||
# action_results = phantom.get_action_results(action_run_id=action_result['action_run_id'], result_data=False, flatten=False)
|
||||
# phantom.debug(action_results)
|
||||
|
||||
return
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Crowdstrike Malware Triage
|
||||
id: fc0edc96-fa2b-48b0-9a6f-63da6783fd63
|
||||
version: 1
|
||||
date: '2021-02-25'
|
||||
author: Philip Royer, Splunk
|
||||
type: Response
|
||||
description: This playbook is used to enrich and respond to a CrowdStrike Falcon detection involving a potentially malicious executable on an endpoint. Check for previous sightings of the same executable, hunt across other endpoints for the file, gather details about all processes associated with the file, and collect all the gathered information into a prompt for an analyst to review. Based on the analyst's choice, the file can be added to the custom indicators list in CrowdStrike with a detection policy of "detect" or "none", and the endpoint can be optionally quarantined from the network.
|
||||
playbook: crowdstrike_malware_triage
|
||||
how_to_implement: This playbook uses the Crowdstrike OAuth app. Change the target user of the prompt from admin to the appropriate user or role.
|
||||
references: []
|
||||
app_list:
|
||||
- "Crowdstrike OAuth"
|
||||
tags:
|
||||
platform_tags:
|
||||
- Response
|
||||
playbook_fields:
|
||||
- filePath
|
||||
- destinationAddress
|
||||
product:
|
||||
- Splunk SOAR
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"create_time": "2021-08-13T13:55:18.025884+00:00",
|
||||
"custom_function_id": "d4bcb95cc227e78a6e6985e2400015a14ada3056",
|
||||
"description": "Create a new artifact with the specified attributes.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom container id"
|
||||
],
|
||||
"description": "Container which the artifact will be added to.",
|
||||
"input_type": "item",
|
||||
"name": "container",
|
||||
"placeholder": "container:id"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "The name of the new artifact, which is optional and defaults to \"artifact\".",
|
||||
"input_type": "item",
|
||||
"name": "name",
|
||||
"placeholder": "artifact"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "The label of the new artifact, which is optional and defaults to \"events\"",
|
||||
"input_type": "item",
|
||||
"name": "label",
|
||||
"placeholder": "events"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"description": "The severity of the new artifact, which is optional and defaults to \"Medium\". Typically this is either \"High\", \"Medium\", or \"Low\".",
|
||||
"input_type": "item",
|
||||
"name": "severity",
|
||||
"placeholder": "Medium"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "The name of the CEF field to populate in the artifact, such as \"destinationAddress\" or \"sourceDnsDomain\". Required only if cef_value is provided.",
|
||||
"input_type": "item",
|
||||
"name": "cef_field",
|
||||
"placeholder": "destinationAddress"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "The value of the CEF field to populate in the artifact, such as the IP address, domain name, or file hash. Required only if cef_field is provided.",
|
||||
"input_type": "item",
|
||||
"name": "cef_value",
|
||||
"placeholder": "192.0.2.192"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "The CEF data type of the data in cef_value. For example, this could be \"ip\", \"hash\", or \"domain\". Optional.",
|
||||
"input_type": "item",
|
||||
"name": "cef_data_type",
|
||||
"placeholder": "ip"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "A comma-separated list of tags to apply to the created artifact, which is optional.",
|
||||
"input_type": "item",
|
||||
"name": "tags",
|
||||
"placeholder": "tag1, tag2, tag3"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Either \"true\" or \"false\", depending on whether or not the new artifact should trigger the execution of any playbooks that are set to active on the label of the container the artifact will be added to. Optional and defaults to \"false\".",
|
||||
"input_type": "item",
|
||||
"name": "run_automation",
|
||||
"placeholder": "false"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional parameter to modify any extra attributes of the artifact. Input_json will be merged with other inputs. In the event of a conflict, input_json will take precedence.",
|
||||
"input_type": "item",
|
||||
"name": "input_json",
|
||||
"placeholder": "{\"source_data_identifier\": \"1234\", \"data\": \"5678\"}"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom artifact id"
|
||||
],
|
||||
"data_path": "artifact_id",
|
||||
"description": "The ID of the created artifact."
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.4.56260",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
def artifact_create(container=None, name=None, label=None, severity=None, cef_field=None, cef_value=None, cef_data_type=None, tags=None, run_automation=None, input_json=None, **kwargs):
|
||||
"""
|
||||
Create a new artifact with the specified attributes.
|
||||
|
||||
Args:
|
||||
container (CEF type: phantom container id): Container which the artifact will be added to.
|
||||
name: The name of the new artifact, which is optional and defaults to "artifact".
|
||||
label: The label of the new artifact, which is optional and defaults to "events"
|
||||
severity: The severity of the new artifact, which is optional and defaults to "Medium". Typically this is either "High", "Medium", or "Low".
|
||||
cef_field: The name of the CEF field to populate in the artifact, such as "destinationAddress" or "sourceDnsDomain". Required only if cef_value is provided.
|
||||
cef_value (CEF type: *): The value of the CEF field to populate in the artifact, such as the IP address, domain name, or file hash. Required only if cef_field is provided.
|
||||
cef_data_type: The CEF data type of the data in cef_value. For example, this could be "ip", "hash", or "domain". Optional.
|
||||
tags: A comma-separated list of tags to apply to the created artifact, which is optional.
|
||||
run_automation: Either "true" or "false", depending on whether or not the new artifact should trigger the execution of any playbooks that are set to active on the label of the container the artifact will be added to. Optional and defaults to "false".
|
||||
input_json: Optional parameter to modify any extra attributes of the artifact. Input_json will be merged with other inputs. In the event of a conflict, input_json will take precedence.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
artifact_id (CEF type: phantom artifact id): The ID of the created artifact.
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
new_artifact = {}
|
||||
json_dict = None
|
||||
|
||||
if isinstance(container, int):
|
||||
container_id = container
|
||||
elif isinstance(container, dict):
|
||||
container_id = container['id']
|
||||
else:
|
||||
raise TypeError("container is neither an int nor a dictionary")
|
||||
|
||||
if name:
|
||||
new_artifact['name'] = name
|
||||
else:
|
||||
new_artifact['name'] = 'artifact'
|
||||
if label:
|
||||
new_artifact['label'] = label
|
||||
else:
|
||||
new_artifact['label'] = 'events'
|
||||
if severity:
|
||||
new_artifact['severity'] = severity
|
||||
else:
|
||||
new_artifact['severity'] = 'Medium'
|
||||
|
||||
# validate that if cef_field or cef_value is provided, the other is also provided
|
||||
if (cef_field and not cef_value) or (cef_value and not cef_field):
|
||||
raise ValueError("only one of cef_field and cef_value was provided")
|
||||
|
||||
# cef_data should be formatted {cef_field: cef_value}
|
||||
if cef_field:
|
||||
new_artifact['cef_data'] = {cef_field: cef_value}
|
||||
if cef_data_type and isinstance(cef_data_type, str):
|
||||
new_artifact['field_mapping'] = {cef_field: [cef_data_type]}
|
||||
|
||||
# run_automation must be "true" or "false" and defaults to "false"
|
||||
if run_automation:
|
||||
if not isinstance(run_automation, str):
|
||||
raise TypeError("run automation must be a string")
|
||||
if run_automation.lower() == 'true':
|
||||
new_artifact['run_automation'] = True
|
||||
elif run_automation.lower() == 'false':
|
||||
new_artifact['run_automation'] = False
|
||||
else:
|
||||
raise ValueError("run_automation must be either 'true' or 'false'")
|
||||
else:
|
||||
new_artifact['run_automation'] = False
|
||||
|
||||
if input_json:
|
||||
# ensure valid input_json
|
||||
if isinstance(input_json, dict):
|
||||
json_dict = input_json
|
||||
elif isinstance(input_json, str):
|
||||
json_dict = json.loads(input_json)
|
||||
else:
|
||||
raise ValueError("input_json must be either 'dict' or valid json 'string'")
|
||||
|
||||
if json_dict:
|
||||
# Merge dictionaries, using the value from json_dict if there are any conflicting keys
|
||||
for json_key in json_dict:
|
||||
# extract tags from json_dict since it is not a valid parameter for phantom.add_artifact()
|
||||
if json_key == 'tags':
|
||||
tags = json_dict[json_key]
|
||||
else:
|
||||
new_artifact[json_key] = json_dict[json_key]
|
||||
|
||||
# now actually create the artifact
|
||||
phantom.debug('creating a new artifact with the following attributes:\n{}'.format(new_artifact))
|
||||
success, message, artifact_id = phantom.add_artifact(**new_artifact)
|
||||
|
||||
phantom.debug('add_artifact() returned the following:\nsuccess: {}\nmessage: {}\nartifact_id: {}'.format(success, message, artifact_id))
|
||||
if not success:
|
||||
raise RuntimeError("add_artifact() failed")
|
||||
|
||||
# add the tags in a separate REST call because there is no tags parameter in add_artifact()
|
||||
if tags:
|
||||
tags = tags.replace(" ", "").split(",")
|
||||
url = phantom.build_phantom_rest_url('artifact', artifact_id)
|
||||
response = phantom.requests.post(uri=url, json={'tags': tags}, verify=False).json()
|
||||
phantom.debug('response from POST request to add tags:\n{}'.format(response))
|
||||
|
||||
# Return the id of the created artifact
|
||||
return {'artifact_id': artifact_id}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"create_time": "2021-07-24T01:17:48.431013+00:00",
|
||||
"custom_function_id": "1d358be9992079dad6d3313d465e65318930cf70",
|
||||
"description": "Update an artifact with the specified attributes. All parameters are optional, except that cef_field and cef_value must both be provided if one is provided.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom artifact id"
|
||||
],
|
||||
"description": "ID of the artifact to update, which is required.",
|
||||
"input_type": "item",
|
||||
"name": "artifact_id",
|
||||
"placeholder": "1234"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Change the name of the artifact.",
|
||||
"input_type": "item",
|
||||
"name": "name",
|
||||
"placeholder": "artifact"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Change the label of the artifact.",
|
||||
"input_type": "item",
|
||||
"name": "label",
|
||||
"placeholder": "events"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"description": "Change the severity of the artifact. Typically this is either \"High\", \"Medium\", or \"Low\".",
|
||||
"input_type": "item",
|
||||
"name": "severity",
|
||||
"placeholder": "Medium"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "The name of the CEF field to populate in the artifact, such as \"destinationAddress\" or \"sourceDnsDomain\". Required only if cef_value is provided.",
|
||||
"input_type": "item",
|
||||
"name": "cef_field",
|
||||
"placeholder": "destinationAddress"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "The value of the CEF field to populate in the artifact, such as the IP address, domain name, or file hash. Required only if cef_field is provided.",
|
||||
"input_type": "item",
|
||||
"name": "cef_value",
|
||||
"placeholder": "192.0.2.192"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "The CEF data type of the data in cef_value. For example, this could be \"ip\", \"hash\", or \"domain\". Optional, but only operational if cef_field is provided.",
|
||||
"input_type": "item",
|
||||
"name": "cef_data_type",
|
||||
"placeholder": "ip"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "A comma-separated list of tags to apply to the artifact, which is optional.",
|
||||
"input_type": "item",
|
||||
"name": "tags",
|
||||
"placeholder": "tag1, tag2, tag3"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional parameter to modify any extra attributes of the artifact. Input_json will be merged with other inputs. In the event of a conflict, input_json will take precedence.",
|
||||
"input_type": "item",
|
||||
"name": "input_json",
|
||||
"placeholder": "{\"source_data_identifier\": \"1234\", \"data\": \"5678\"}"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"platform_version": "4.10.4.56260",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
def artifact_update(artifact_id=None, name=None, label=None, severity=None, cef_field=None, cef_value=None, cef_data_type=None, tags=None, input_json=None, **kwargs):
|
||||
"""
|
||||
Update an artifact with the specified attributes. All parameters are optional, except that cef_field and cef_value must both be provided if one is provided.
|
||||
|
||||
Args:
|
||||
artifact_id (CEF type: phantom artifact id): ID of the artifact to update, which is required.
|
||||
name: Change the name of the artifact.
|
||||
label: Change the label of the artifact.
|
||||
severity: Change the severity of the artifact. Typically this is either "High", "Medium", or "Low".
|
||||
cef_field: The name of the CEF field to populate in the artifact, such as "destinationAddress" or "sourceDnsDomain". Required only if cef_value is provided.
|
||||
cef_value (CEF type: *): The value of the CEF field to populate in the artifact, such as the IP address, domain name, or file hash. Required only if cef_field is provided.
|
||||
cef_data_type: The CEF data type of the data in cef_value. For example, this could be "ip", "hash", or "domain". Optional, but only operational if cef_field is provided.
|
||||
tags: A comma-separated list of tags to apply to the artifact, which is optional.
|
||||
input_json: Optional parameter to modify any extra attributes of the artifact. Input_json will be merged with other inputs. In the event of a conflict, input_json will take precedence.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
updated_artifact = {}
|
||||
|
||||
if not isinstance(artifact_id, int):
|
||||
raise TypeError("artifact_id is required")
|
||||
|
||||
if name:
|
||||
updated_artifact['name'] = name
|
||||
if label:
|
||||
updated_artifact['label'] = label
|
||||
if severity:
|
||||
updated_artifact['severity'] = severity
|
||||
|
||||
# validate that if cef_field or cef_value is provided, the other is also provided
|
||||
if (cef_field and not cef_value) or (cef_value and not cef_field):
|
||||
raise ValueError("only one of cef_field and cef_value was provided")
|
||||
|
||||
# cef_data should be formatted {cef_field: cef_value}
|
||||
if cef_field:
|
||||
updated_artifact['cef'] = {cef_field: cef_value}
|
||||
if cef_data_type and isinstance(cef_data_type, str):
|
||||
updated_artifact['cef_types'] = {cef_field: [cef_data_type]}
|
||||
|
||||
# separate tags by comma
|
||||
if tags:
|
||||
tags = tags.replace(" ", "").split(",")
|
||||
updated_artifact['tags'] = tags
|
||||
|
||||
if input_json:
|
||||
json_dict = json.loads(input_json)
|
||||
# Merge dictionaries, using the value from json_dict if there are any conflicting keys
|
||||
for json_key in json_dict:
|
||||
updated_artifact[json_key] = json_dict[json_key]
|
||||
|
||||
# now actually update the artifact
|
||||
phantom.debug('updating artifact {} with the following attributes:\n{}'.format(artifact_id, updated_artifact))
|
||||
url = phantom.build_phantom_rest_url('artifact', artifact_id)
|
||||
response = phantom.requests.post(url, json=updated_artifact, verify=False).json()
|
||||
|
||||
phantom.debug('POST /rest/artifact returned the following response:\n{}'.format(response))
|
||||
if 'success' not in response or response['success'] != True:
|
||||
raise RuntimeError("POST /rest/artifact failed")
|
||||
|
||||
return
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"create_time": "2021-08-24T13:51:40.605448+00:00",
|
||||
"custom_function_id": "5ca49f921dfa723aff4e340671d610634f89e262",
|
||||
"description": "Allows the retrieval of an attribute from an asset configuration for access in a playbook. This can be valuable in instances such as a dynamic note that references the Asset hostname. Must provide asset name or id.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"description": "Asset numeric ID or asset name.",
|
||||
"input_type": "item",
|
||||
"name": "asset",
|
||||
"placeholder": "splunk_es"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "id",
|
||||
"description": "Unique asset id"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"data_path": "name",
|
||||
"description": "Unique asset name"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "configuration",
|
||||
"description": "Access individual configuration attributes by appending \".<keyname>\"\nExample: configuration.device"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "tags",
|
||||
"description": "Asset tags"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "description",
|
||||
"description": "Asset description"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "product_name",
|
||||
"description": "Asset product_name"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "product_vendor",
|
||||
"description": "Asset product_vendor"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"data_path": "product_version",
|
||||
"description": "Asset product_version"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"data_path": "type",
|
||||
"description": "Asset type"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "version",
|
||||
"description": "Asset version"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.6.61906",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
def asset_get_attributes(asset=None, **kwargs):
|
||||
"""
|
||||
Allows the retrieval of an attribute from an asset configuration for access in a playbook. This can be valuable in instances such as a dynamic note that references the Asset hostname. Must provide asset name or id.
|
||||
|
||||
Args:
|
||||
asset: Asset numeric ID or asset name.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
id: Unique asset id
|
||||
name: Unique asset name
|
||||
configuration: Access individual configuration attributes by appending ".<keyname>"
|
||||
Example: configuration.device
|
||||
tags: Asset tags
|
||||
description: Asset description
|
||||
product_name: Asset product_name
|
||||
product_vendor: Asset product_vendor
|
||||
product_version: Asset product_version
|
||||
type: Asset type
|
||||
version: Asset version
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = {}
|
||||
url = phantom.build_phantom_rest_url('asset')
|
||||
|
||||
if isinstance(asset, int):
|
||||
url += '/{}'.format(asset)
|
||||
|
||||
# Attempt to translate asset_name to asset_id
|
||||
elif isinstance(asset, str):
|
||||
params = {'_filter_name': '"{}"'.format(asset)}
|
||||
response = phantom.requests.get(uri=url, params=params, verify=False).json()
|
||||
if response['count'] == 1:
|
||||
url += '/{}'.format(response['data'][0]['id'])
|
||||
else:
|
||||
raise RuntimeError("No valid asset id found for provided asset name: {}".format(asset))
|
||||
else:
|
||||
raise TypeError("No valid asset id or name provided.")
|
||||
|
||||
response = phantom.requests.get(uri=url, verify=False).json()
|
||||
if response.get('id'):
|
||||
outputs = response
|
||||
else:
|
||||
raise RuntimeError("No valid asset id found.")
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"create_time": "2021-10-18T17:15:17.576903+00:00",
|
||||
"custom_function_id": "a5663cbe44479126d9fdff5818c8500011abc53e",
|
||||
"description": "Decode one or more strings encoded with base64. The input can be a single chunk of base64 or a list of strings separated by a delimiter.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "Y2FsYy5leGU=",
|
||||
"input_type": "item",
|
||||
"name": "input_string",
|
||||
"placeholder": "base64 string to decode"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"description": "Defaults to False. If True, use the delimiter to split the input string and decode each of the components separately if it is base64.",
|
||||
"input_type": "item",
|
||||
"name": "split_input",
|
||||
"placeholder": "True or False"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "The character to use as a delimiter if split_input is True. Defaults to a comma. The special option \"space\" can be used to split on a single space character (\" \").",
|
||||
"input_type": "item",
|
||||
"name": "delimiter",
|
||||
"placeholder": ","
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.input_string",
|
||||
"description": "Base64 string before being decoded"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.output_string",
|
||||
"description": "Resulting string after decoding from base64"
|
||||
}
|
||||
],
|
||||
"platform_version": "5.0.1.66250",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
def base64_decode(input_string=None, split_input=None, delimiter=None, **kwargs):
|
||||
"""
|
||||
Decode one or more strings encoded with base64. The input can be a single chunk of base64 or a list of strings separated by a delimiter.
|
||||
|
||||
Args:
|
||||
input_string (CEF type: *): Y2FsYy5leGU=
|
||||
split_input: Defaults to False. If True, use the delimiter to split the input string and decode each of the components separately if it is base64.
|
||||
delimiter: The character to use as a delimiter if split_input is True. Defaults to a comma. The special option "space" can be used to split on a single space character (" ").
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.input_string (CEF type: *): Base64 string before being decoded
|
||||
*.output_string (CEF type: *): Resulting string after decoding from base64
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
import base64
|
||||
|
||||
if not input_string or not isinstance(input_string, str):
|
||||
raise ValueError('input_string must be a string')
|
||||
|
||||
def isBase64(sb):
|
||||
try:
|
||||
if isinstance(sb, str):
|
||||
# If there's any unicode here, an exception will be thrown and the function will return false
|
||||
sb_bytes = bytes(sb, 'ascii')
|
||||
elif isinstance(sb, bytes):
|
||||
sb_bytes = sb
|
||||
else:
|
||||
raise ValueError("Argument must be string or bytes")
|
||||
return base64.b64encode(base64.b64decode(sb_bytes)) == sb_bytes
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
outputs = []
|
||||
|
||||
# split_input defaults to false
|
||||
if split_input == True or (isinstance(split_input, str) and split_input.lower() == 'true'):
|
||||
split_input = True
|
||||
else:
|
||||
split_input = False
|
||||
|
||||
# create the list of inputs, whether it be the single input or a delimiter-separated list
|
||||
if not split_input:
|
||||
input_list = [input_string]
|
||||
else:
|
||||
if not isinstance(delimiter, str):
|
||||
delimiter = ','
|
||||
if delimiter == 'space':
|
||||
delimiter = ' '
|
||||
input_list = input_string.split(delimiter)
|
||||
|
||||
# now that input_list is set up, perform the base64 decode on each item that is valid base64
|
||||
for index, value in enumerate(input_list):
|
||||
if isBase64(value):
|
||||
try:
|
||||
value_bytes = value.encode('ascii')
|
||||
data = base64.b64decode(value_bytes, validate=True)
|
||||
if data:
|
||||
outputs.append({'input_string': value, 'output_string': data.decode('ascii').replace('\x00','')})
|
||||
|
||||
except Exception as e:
|
||||
phantom.error(f'Unable to decode string: {e}')
|
||||
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"create_time": "2021-08-24T16:17:12.241297+00:00",
|
||||
"custom_function_id": "98612a9a22a18dff43b6644ed00c2c523d348d79",
|
||||
"description": "Collect all artifact values that match the desired CEF data types, such as \"ip\", \"url\", \"sha1\", or \"all\". Optionally also filter for artifacts that have the specified tags.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom container id"
|
||||
],
|
||||
"description": "Container ID or container object.",
|
||||
"input_type": "item",
|
||||
"name": "container",
|
||||
"placeholder": "container:id"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "The CEF data type to collect values for. This could be a single string or a comma separated list such as \"hash,filehash,file_hash\". The special value \"all\" can also be used to collect all field values from all artifacts.",
|
||||
"input_type": "item",
|
||||
"name": "data_types",
|
||||
"placeholder": "data_type1, data_type2, data_type3"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "If tags are provided, only return fields from artifacts that have all of the provided tags. This could be an individual tag or a comma separated list.",
|
||||
"input_type": "item",
|
||||
"name": "tags",
|
||||
"placeholder": "tag1,tag2,tag3"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Defaults to 'new'. Define custom scope. Advanced Settings Scope is not passed to a custom function. Options are 'all' or 'new'.",
|
||||
"input_type": "item",
|
||||
"name": "scope",
|
||||
"placeholder": "new"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.artifact_value",
|
||||
"description": "The value of the field with the matching CEF data type."
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom artifact id"
|
||||
],
|
||||
"data_path": "*.artifact_id",
|
||||
"description": "ID of the artifact that contains the value."
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.6.61906",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
def collect_by_cef_type(container=None, data_types=None, tags=None, scope=None, **kwargs):
|
||||
"""
|
||||
Collect all artifact values that match the desired CEF data types, such as "ip", "url", "sha1", or "all". Optionally also filter for artifacts that have the specified tags.
|
||||
|
||||
Args:
|
||||
container (CEF type: phantom container id): Container ID or container object.
|
||||
data_types: The CEF data type to collect values for. This could be a single string or a comma separated list such as "hash,filehash,file_hash". The special value "all" can also be used to collect all field values from all artifacts.
|
||||
tags: If tags are provided, only return fields from artifacts that have all of the provided tags. This could be an individual tag or a comma separated list.
|
||||
scope: Defaults to 'new'. Define custom scope. Advanced Settings Scope is not passed to a custom function. Options are 'all' or 'new'.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.artifact_value (CEF type: *): The value of the field with the matching CEF data type.
|
||||
*.artifact_id (CEF type: phantom artifact id): ID of the artifact that contains the value.
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
import traceback
|
||||
|
||||
# validate container and get ID
|
||||
if isinstance(container, dict) and container['id']:
|
||||
container_dict = container
|
||||
container_id = container['id']
|
||||
elif isinstance(container, int):
|
||||
rest_container = phantom.requests.get(uri=phantom.build_phantom_rest_url('container', container), verify=False).json()
|
||||
if 'id' not in rest_container:
|
||||
raise ValueError('Failed to find container with id {container}')
|
||||
container_dict = rest_container
|
||||
container_id = container
|
||||
else:
|
||||
raise TypeError("The input 'container' is neither a container dictionary nor an int, so it cannot be used")
|
||||
|
||||
# validate the data_types input
|
||||
if not data_types or not isinstance(data_types, str):
|
||||
raise ValueError("The input 'data_types' must exist and must be a string")
|
||||
# if data_types has a comma, split it and treat it as a list
|
||||
elif "," in data_types:
|
||||
data_types = [item.strip() for item in data_types.split(",")]
|
||||
# else it must be a single data type
|
||||
else:
|
||||
data_types = [data_types]
|
||||
|
||||
# validate scope input
|
||||
if isinstance(scope, str) and scope.lower() in ['new', 'all']:
|
||||
scope = scope.lower()
|
||||
elif not scope:
|
||||
scope = None
|
||||
else:
|
||||
raise ValueError("The input 'scope' is not one of 'new' or 'all'")
|
||||
|
||||
# split tags if it contains commas or use as-is
|
||||
if not tags:
|
||||
tags = []
|
||||
# if tags has a comma, split it and treat it as a list
|
||||
elif tags and "," in tags:
|
||||
tags = [item.strip() for item in tags.split(",")]
|
||||
# if there is no comma, treat it as a single tag
|
||||
else:
|
||||
tags = [tags]
|
||||
|
||||
# collect all values matching the cef type (which was previously called "contains")
|
||||
collected_field_values = phantom.collect_from_contains(container=container_dict, action_results=None, contains=data_types, scope=scope)
|
||||
phantom.debug(f'found the following field values: {collected_field_values}')
|
||||
|
||||
# collect all the artifacts in the container to get the artifact IDs
|
||||
artifacts = phantom.requests.get(uri=phantom.build_phantom_rest_url('container', container_id, 'artifacts'), params={'page_size': 0}, verify=False).json()['data']
|
||||
|
||||
# build the output list from artifacts with the collected field values
|
||||
outputs = []
|
||||
for artifact in artifacts:
|
||||
# if any tags are provided, make sure each provided tag is in the artifact's tags
|
||||
if tags:
|
||||
if not set(tags).issubset(set(artifact['tags'])):
|
||||
continue
|
||||
# "all" is a special value to collect every value from every artifact
|
||||
if data_types == ['all']:
|
||||
for cef_key in artifact['cef']:
|
||||
new_output = {'artifact_value': artifact['cef'][cef_key], 'artifact_id': artifact['id']}
|
||||
if new_output not in outputs:
|
||||
outputs.append(new_output)
|
||||
continue
|
||||
for cef_key in artifact['cef']:
|
||||
if artifact['cef'][cef_key] in collected_field_values:
|
||||
new_output = {'artifact_value': artifact['cef'][cef_key], 'artifact_id': artifact['id']}
|
||||
if new_output not in outputs:
|
||||
outputs.append(new_output)
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"create_time": "2021-10-18T12:31:32.500833+00:00",
|
||||
"custom_function_id": "83776ecf4dd52c71d8497cb500dd332780eb9c72",
|
||||
"description": "An alternative to the add-to-case API call. This function will copy all artifacts, automation, notes and comments over from every container within the container_list into the target_container. The target_container will be upgraded to a case.\n\nThe notes will be copied over with references to the child containers from where they came. A note will be left in the child containers with a link to the target container. The child containers will be marked as evidence within the target container. \n\nAny notes left as a consequence of the merge process will be skipped in subsequent merges.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom container id"
|
||||
],
|
||||
"description": "The target container to copy the information over. Supports container dictionary or container id.",
|
||||
"input_type": "item",
|
||||
"name": "target_container",
|
||||
"placeholder": "container:id"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "A list of container IDs to copy into the target container.",
|
||||
"input_type": "list",
|
||||
"name": "container_list",
|
||||
"placeholder": "[1, 5, 10]"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Name or ID of the workbook to add if the container does not have a workbook yet. If no workbook is provided, the system default workbook will be added.",
|
||||
"input_type": "item",
|
||||
"name": "workbook",
|
||||
"placeholder": "My Workbook"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "True or False to close the child containers in the container_list after merge. Defaults to False.",
|
||||
"input_type": "item",
|
||||
"name": "close_containers",
|
||||
"placeholder": "True or False"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"platform_version": "5.0.1.66250",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
def container_merge(target_container=None, container_list=None, workbook=None, close_containers=None, **kwargs):
|
||||
"""
|
||||
An alternative to the add-to-case API call. This function will copy all artifacts, automation, notes and comments over from every container within the container_list into the target_container. The target_container will be upgraded to a case.
|
||||
|
||||
The notes will be copied over with references to the child containers from where they came. A note will be left in the child containers with a link to the target container. The child containers will be marked as evidence within the target container.
|
||||
|
||||
Any notes left as a consequence of the merge process will be skipped in subsequent merges.
|
||||
|
||||
Args:
|
||||
target_container (CEF type: phantom container id): The target container to copy the information over. Supports container dictionary or container id.
|
||||
container_list: A list of container IDs to copy into the target container.
|
||||
workbook: Name or ID of the workbook to add if the container does not have a workbook yet. If no workbook is provided, the system default workbook will be added.
|
||||
close_containers: True or False to close the child containers in the container_list after merge. Defaults to False.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = {}
|
||||
|
||||
# Check if valid target_container input was provided
|
||||
if isinstance(target_container, int):
|
||||
container = phantom.get_container(target_container)
|
||||
elif isinstance(target_container, dict):
|
||||
container = target_container
|
||||
else:
|
||||
raise TypeError(f"target_container '{target_container}' is neither a int or a dictionary")
|
||||
|
||||
container_url = phantom.build_phantom_rest_url('container', container['id'])
|
||||
|
||||
# Check if container_list input is a list of IDs
|
||||
if isinstance(container_list, list) and (all(isinstance(x, int) for x in container_list) or all(x.isnumeric() for x in container_list)):
|
||||
pass
|
||||
else:
|
||||
raise TypeError(f"container_list '{container_list}' is not a list of integers")
|
||||
|
||||
## Prep parent container as case with workbook ##
|
||||
workbook_name = phantom.requests.get(container_url, verify=False).json().get('workflow_name')
|
||||
# If workbook already exists, proceed to promote to case
|
||||
if workbook_name:
|
||||
phantom.debug("workbook already exists. adding [Parent] to container name and promoting to case")
|
||||
update_data = {'container_type': 'case'}
|
||||
if not '[Parent]' in container['name']:
|
||||
update_data['name'] = "[Parent] {}".format(container['name'])
|
||||
phantom.update(container, update_data)
|
||||
else:
|
||||
phantom.update(container, update_data)
|
||||
# If no workbook exists, add one
|
||||
else:
|
||||
phantom.debug("no workbook in container. adding one by name or using the default")
|
||||
# If workbook ID was provided, add it
|
||||
if isinstance(workbook, int):
|
||||
workbook_id = workbook
|
||||
phantom.add_workbook(container=container['id'], workbook_id=workbook_id)
|
||||
# elif workbook name was provided, attempt to translate it to an id
|
||||
elif isinstance(workbook, str):
|
||||
workbook_url = phantom.build_phantom_rest_url('workbook_template') + '?_filter_name="{}"'.format(workbook)
|
||||
response = phantom.requests.get(workbook_url, verify=False).json()
|
||||
if response['count'] > 1:
|
||||
raise RuntimeError('Unable to add workbook - more than one ID matches workbook name')
|
||||
elif response['data'][0]['id']:
|
||||
workbook_id = response['data'][0]['id']
|
||||
phantom.add_workbook(container=container['id'], workbook_id=workbook_id)
|
||||
else:
|
||||
# Adding default workbook
|
||||
phantom.promote(container=container['id'])
|
||||
# Check again to see if a workbook now exists
|
||||
workbook_name = phantom.requests.get(container_url, verify=False).json().get('workflow_name')
|
||||
# If workbook is now present, promote to case
|
||||
if workbook_name:
|
||||
update_data = {'container_type': 'case'}
|
||||
if not '[Parent]' in container['name']:
|
||||
update_data['name'] = "[Parent] {}".format(container['name'])
|
||||
phantom.update(container, update_data)
|
||||
else:
|
||||
phantom.update(container, update_data)
|
||||
else:
|
||||
raise RuntimeError(f"Error occurred during workbook add for workbook '{workbook_name}'")
|
||||
|
||||
## Check if current phase is set. If not, set the current phase to the first available phase to avoid artifact merge error ##
|
||||
if not container.get('current_phase_id'):
|
||||
phantom.debug("no current phase, so setting first available phase to current")
|
||||
workbook_phase_url = phantom.build_phantom_rest_url('workbook_phase') + "?_filter_container={}".format(container['id'])
|
||||
request_json = phantom.requests.get(workbook_phase_url, verify=False).json()
|
||||
update_data = {'current_phase_id': request_json['data'][0]['id']}
|
||||
phantom.update(container, update_data)
|
||||
|
||||
child_container_list = []
|
||||
child_container_name_list = []
|
||||
# Iterate through child containers
|
||||
for child_container_id in container_list:
|
||||
|
||||
### Begin child container processing ###
|
||||
phantom.debug("Processing Child Container ID: {}".format(child_container_id))
|
||||
|
||||
child_container = phantom.get_container(child_container_id)
|
||||
child_container_list.append(child_container_id)
|
||||
child_container_name_list.append(child_container['name'])
|
||||
child_container_url = phantom.build_phantom_rest_url('container', child_container_id)
|
||||
|
||||
## Update container name with parent relationship
|
||||
if not "[Parent:" in child_container['name']:
|
||||
update_data = {'name': "[Parent: {0}] {1}".format(container['id'], child_container['name'])}
|
||||
phantom.update(child_container, update_data)
|
||||
|
||||
## Gather and add notes ##
|
||||
for note in phantom.get_notes(container=child_container_id):
|
||||
# Avoid copying any notes related to the merge process.
|
||||
if note['success'] and not note['data']['title'] in ('[Auto-Generated] Related Containers',
|
||||
'[Auto-Generated] Parent Container',
|
||||
'[Auto-Generated] Child Containers'):
|
||||
phantom.add_note(container=container['id'],
|
||||
note_type='general',
|
||||
note_format=note['data']['note_format'],
|
||||
title="[From Event {0}] {1}".format(note['data']['container'], note['data']['title']),
|
||||
content=note['data']['content'])
|
||||
|
||||
## Copy information and add to case
|
||||
data = {'add_to_case': True,
|
||||
'container_id': child_container_id,
|
||||
'copy_artifacts': True,
|
||||
'copy_automation': True,
|
||||
'copy_files': True,
|
||||
'copy_comments': True
|
||||
}
|
||||
phantom.requests.post(container_url, json=data, verify=False)
|
||||
|
||||
## Leave a note with a link to the parent container
|
||||
phantom.debug("Adding parent relationship note to child container '{}'".format(child_container_id))
|
||||
data_row = "{0} | [{1}]({2}/mission/{0}) |".format(container['id'], container['name'], phantom.get_base_url())
|
||||
phantom.add_note(container=child_container_id,
|
||||
note_type="general",
|
||||
note_format="markdown",
|
||||
title="[Auto-Generated] Parent Container",
|
||||
content="| Container_ID | Container_Name |\n| --- | --- |\n| {}".format(data_row))
|
||||
|
||||
## Mark child container as evidence in target_container
|
||||
data = {
|
||||
"container_id": container['id'],
|
||||
"object_id": child_container_id,
|
||||
"content_type": "container"
|
||||
}
|
||||
evidence_url = phantom.build_phantom_rest_url('evidence')
|
||||
response = phantom.requests.post(evidence_url, json=data, verify=False).json()
|
||||
|
||||
## Close child container
|
||||
if isinstance(close_containers, str) and close_containers.lower() == 'true':
|
||||
phantom.set_status(container=child_container_id, status="closed")
|
||||
|
||||
### End child container processing ###
|
||||
|
||||
## Format and add note for link back to child_containers in parent_container
|
||||
note_title = "[Auto-Generated] Child Containers"
|
||||
note_format = "markdown"
|
||||
format_list = []
|
||||
# Build new note
|
||||
for child_container_id,child_container_name in zip(child_container_list,child_container_name_list):
|
||||
format_list.append("| {0} | [{1}]({2}/mission/{0}) |\n".format(child_container_id, child_container_name, phantom.get_base_url()))
|
||||
# Fetch any previous merge note
|
||||
params = {'_filter_container': '"{}"'.format(container['id']), '_filter_title': '"[Auto-Generated] Child Containers"'}
|
||||
note_url = phantom.build_phantom_rest_url('note')
|
||||
response_data = phantom.requests.get(note_url, verify=False).json()
|
||||
# If an old note was found, proceed to overwrite it
|
||||
if response_data['count'] > 0:
|
||||
note_item = response_data['data'][0]
|
||||
note_content = note_item['content']
|
||||
# Append new information to existing note
|
||||
for c_note in format_list:
|
||||
note_content += c_note
|
||||
data = {"note_type": "general",
|
||||
"title": note_title,
|
||||
"content": note_content,
|
||||
"note_format": note_format}
|
||||
# Overwrite note
|
||||
response_data = phantom.requests.post(note_url + "/{}".format(note_item['id']), json=data, verify=False).json()
|
||||
# If no old note was found, add new with header
|
||||
else:
|
||||
template = "| Container ID | Container Name |\n| --- | --- |\n"
|
||||
for c_note in format_list:
|
||||
template += c_note
|
||||
success, message, process_container_merge__note_id = phantom.add_note(container=container,
|
||||
note_type="general",
|
||||
title=note_title,
|
||||
content=template,
|
||||
note_format=note_format)
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"create_time": "2021-07-19T18:11:14.706144+00:00",
|
||||
"custom_function_id": "7272e46db8e97248abb1584c72c0734ff9e303dc",
|
||||
"description": "Allows updating various attributes of a container in a single custom function. Any attributes of a container not listed can be updated via the input_json parameter. ",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom container id"
|
||||
],
|
||||
"description": "Supports a container id or container dictionary",
|
||||
"input_type": "item",
|
||||
"name": "container_input",
|
||||
"placeholder": "container:id"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional parameter to change container name",
|
||||
"input_type": "item",
|
||||
"name": "name",
|
||||
"placeholder": "My Container Name"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional parameter to change the container description",
|
||||
"input_type": "item",
|
||||
"name": "description",
|
||||
"placeholder": "My Container Description"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom container label"
|
||||
],
|
||||
"description": "Optional parameter to change the container label",
|
||||
"input_type": "item",
|
||||
"name": "label",
|
||||
"placeholder": "my_label"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional parameter to change the container owner. Accepts a username or role name or keyword \"current\" to set the currently running playbook user as the owner.",
|
||||
"input_type": "item",
|
||||
"name": "owner",
|
||||
"placeholder": "admin"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional parameter to change the container sensitivity. ",
|
||||
"input_type": "item",
|
||||
"name": "sensitivity",
|
||||
"placeholder": "amber"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional parameter to change the container severity.",
|
||||
"input_type": "item",
|
||||
"name": "severity",
|
||||
"placeholder": "medium"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional parameter to change the container status.",
|
||||
"input_type": "item",
|
||||
"name": "status",
|
||||
"placeholder": "open"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional parameter to change the container tags. Must be in the format of a comma separated list.",
|
||||
"input_type": "item",
|
||||
"name": "tags",
|
||||
"placeholder": "tag1, tag2"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional parameter to modify any extra attributes of a container. Input_json will be merged with other inputs. In the event of a conflict, input_json will take precedence.",
|
||||
"input_type": "item",
|
||||
"name": "input_json",
|
||||
"placeholder": "{\"custom_fields\": {\"field_name\": \"field_value\"}}"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"platform_version": "4.10.4.56260",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
def container_update(container_input=None, name=None, description=None, label=None, owner=None, sensitivity=None, severity=None, status=None, tags=None, input_json=None, **kwargs):
|
||||
"""
|
||||
Allows updating various attributes of a container in a single custom function. Any attributes of a container not listed can be updated via the input_json parameter.
|
||||
|
||||
Args:
|
||||
container_input (CEF type: phantom container id): Supports a container id or container dictionary
|
||||
name: Optional parameter to change container name
|
||||
description: Optional parameter to change the container description
|
||||
label (CEF type: phantom container label): Optional parameter to change the container label
|
||||
owner: Optional parameter to change the container owner. Accepts a username or role name or keyword "current" to set the currently running playbook user as the owner.
|
||||
sensitivity: Optional parameter to change the container sensitivity.
|
||||
severity: Optional parameter to change the container severity.
|
||||
status: Optional parameter to change the container status.
|
||||
tags: Optional parameter to change the container tags. Must be in the format of a comma separated list.
|
||||
input_json: Optional parameter to modify any extra attributes of a container. Input_json will be merged with other inputs. In the event of a conflict, input_json will take precedence.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = {}
|
||||
update_dict = {}
|
||||
|
||||
if isinstance(container_input, int):
|
||||
container = phantom.get_container(container_input)
|
||||
elif isinstance(container_input, dict):
|
||||
container = container_input
|
||||
else:
|
||||
raise TypeError("container_input is neither a int or a dictionary")
|
||||
|
||||
if name:
|
||||
update_dict['name'] = name
|
||||
if description:
|
||||
update_dict['description'] = description
|
||||
if label:
|
||||
update_dict['label'] = label
|
||||
if owner:
|
||||
# If keyword 'current' entered then translate effective_user id to a username
|
||||
if owner.lower() == 'current':
|
||||
update_dict['owner_id'] = phantom.get_effective_user()
|
||||
else:
|
||||
# Attempt to translate name to owner_id
|
||||
url = phantom.build_phantom_rest_url('ph_user') + f'?_filter_username="{owner}"'
|
||||
data = phantom.requests.get(url, verify=False).json().get('data')
|
||||
if data and len(data) == 1:
|
||||
update_dict['owner_id'] = data[0]['id']
|
||||
elif data and len(data) > 1:
|
||||
phantom.error(f'Multiple matches for owner "{owner}"')
|
||||
else:
|
||||
# Attempt to translate name to role_id
|
||||
url = phantom.build_phantom_rest_url('role') + f'?_filter_name="{owner}"'
|
||||
data = phantom.requests.get(url, verify=False).json().get('data')
|
||||
if data and len(data) == 1:
|
||||
update_dict['role_id'] = data[0]['id']
|
||||
elif data and len(data) > 1:
|
||||
phantom.error(f'Multiple matches for role "{owner}"')
|
||||
else:
|
||||
phantom.error(f'"{owner}" is not a valid username or role')
|
||||
if sensitivity:
|
||||
update_dict['sensitivity'] = sensitivity
|
||||
if severity:
|
||||
update_dict['severity'] = severity
|
||||
if status:
|
||||
update_dict['status'] = status
|
||||
if tags:
|
||||
tags = tags.replace(" ", "").split(",")
|
||||
update_dict['tags'] = tags
|
||||
if input_json:
|
||||
json_dict = json.loads(input_json)
|
||||
# Merge dictionaries together. The second argument, "**json_dict" will take precedence and overwrite any duplicate parameters.
|
||||
update_dict = {**update_dict, **json_dict}
|
||||
|
||||
if update_dict:
|
||||
phantom.debug('Updating container {0} with the following information: "{1}"'.format(container['id'], update_dict))
|
||||
phantom.update(container, update_dict)
|
||||
else:
|
||||
phantom.debug("Valid container entered but no valid container changes provided.")
|
||||
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"create_time": "2021-03-24T13:38:50.951017+00:00",
|
||||
"custom_function_id": "b7a89f44958aee7bbdb3054d43c06df9a2026370",
|
||||
"description": "Fetch a custom list and iterate through the rows, producing a dictionary output for each row with the row number and the value for each column.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "the name or ID of a custom list",
|
||||
"input_type": "item",
|
||||
"name": "custom_list",
|
||||
"placeholder": "my_custom_list"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.row_num",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.column_0",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"data_path": "*.column_1",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.column_2",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.column_3",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.column_4",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.column_5",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.column_6",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.column_7",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.column_8",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.2.47587",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
def custom_list_enumerate(custom_list=None, **kwargs):
|
||||
"""
|
||||
Fetch a custom list and iterate through the rows, producing a dictionary output for each row with the row number and the value for each column.
|
||||
|
||||
Args:
|
||||
custom_list: the name or ID of a custom list
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.row_num
|
||||
*.column_0
|
||||
*.column_1
|
||||
*.column_2
|
||||
*.column_3
|
||||
*.column_4
|
||||
*.column_5
|
||||
*.column_6
|
||||
*.column_7
|
||||
*.column_8
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
if not custom_list:
|
||||
raise ValueError('list_name_or_num parameter is required')
|
||||
|
||||
outputs = []
|
||||
|
||||
# Use REST to get the custom list
|
||||
custom_list_request = phantom.requests.get(
|
||||
phantom.build_phantom_rest_url('decided_list', custom_list),
|
||||
verify=False
|
||||
)
|
||||
|
||||
# Raise error if unsuccessful
|
||||
custom_list_request.raise_for_status()
|
||||
|
||||
# Get the list content
|
||||
custom_list = custom_list_request.json().get('content', [])
|
||||
|
||||
# Iterate through all rows and save to a list of dicts
|
||||
for row_num, row in enumerate(custom_list):
|
||||
row_dict = {'column_{}'.format(col): val for col, val in enumerate(row)}
|
||||
row_dict['row_num'] = row_num
|
||||
outputs.append(row_dict)
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"create_time": "2021-09-20T17:42:46.572482+00:00",
|
||||
"custom_function_id": "2ec78e71ee35dce744423a989e7efcaa0b17f51f",
|
||||
"description": "Iterates through all items of a custom list to see if any list value (i.e. \"sample.com\") exists in the input you are comparing it to (i.e \"findme.sample.com\"). Returns a list of matches, a list of misses, a count of matches, and a count of misses.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"description": "Name of the custom list. Every string in this list will be compared to see if it is a substring of any of the comparison_strings",
|
||||
"input_type": "item",
|
||||
"name": "custom_list",
|
||||
"placeholder": "custom_list_name"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "String to use for comparison.",
|
||||
"input_type": "list",
|
||||
"name": "comparison_strings",
|
||||
"placeholder": "comparison_strings"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "matches.*.match",
|
||||
"description": "List of all items from the list that are substrings of any of the comparison strings"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"data_path": "match_count",
|
||||
"description": "Number of matches"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "misses.*.miss",
|
||||
"description": "List of all items from the list that are not substrings of any of the comparison strings"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "miss_count",
|
||||
"description": "Number of misses"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.7.63984",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
def custom_list_value_in_strings(custom_list=None, comparison_strings=None, **kwargs):
|
||||
"""
|
||||
Iterates through all items of a custom list to see if any list value (i.e. "sample.com") exists in the input you are comparing it to (i.e "findme.sample.com"). Returns a list of matches, a list of misses, a count of matches, and a count of misses.
|
||||
|
||||
Args:
|
||||
custom_list: Name of the custom list. Every string in this list will be compared to see if it is a substring of any of the comparison_strings
|
||||
comparison_strings (CEF type: *): String to use for comparison.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
matches.*.match (CEF type: *): List of all items from the list that are substrings of any of the comparison strings
|
||||
match_count: Number of matches
|
||||
misses.*.miss (CEF type: *): List of all items from the list that are not substrings of any of the comparison strings
|
||||
miss_count: Number of misses
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
# Get the custom list
|
||||
success, message, this_list = phantom.get_list(list_name=custom_list)
|
||||
|
||||
# Create the lists to store matches and misses
|
||||
matches = []
|
||||
misses = []
|
||||
|
||||
# Loop through each comparison string
|
||||
for comparison_string in comparison_strings:
|
||||
|
||||
# Loop through the custom list to see if any list value is found in the comparison string
|
||||
for row in this_list:
|
||||
for cell in row:
|
||||
if comparison_string.find(cell) != -1:
|
||||
matches.append({"match": cell})
|
||||
else:
|
||||
misses.append({"miss": cell})
|
||||
|
||||
# Prepare the outputs
|
||||
match_count = len(matches)
|
||||
miss_count = len(misses)
|
||||
outputs = {
|
||||
'matches': matches,
|
||||
'match_count': match_count,
|
||||
'misses': misses,
|
||||
'miss_count': miss_count,
|
||||
}
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"create_time": "2021-08-20T19:37:15.192987+00:00",
|
||||
"custom_function_id": "1df6dfb4792ebd6ffca642caf7056300a16ce635",
|
||||
"description": "Change a timestamp by adding or subtracting minutes, hours, or days.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"description": "The datetime to modify, which should be provided in a string format determined by input_format_string",
|
||||
"input_type": "item",
|
||||
"name": "input_datetime",
|
||||
"placeholder": "2020-06-27T14:53:08.219016Z"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"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.",
|
||||
"input_type": "item",
|
||||
"name": "input_format_string",
|
||||
"placeholder": "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"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'",
|
||||
"input_type": "item",
|
||||
"name": "modification_unit",
|
||||
"placeholder": "minutes"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"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.",
|
||||
"input_type": "item",
|
||||
"name": "amount_to_modify",
|
||||
"placeholder": "0"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"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'.",
|
||||
"input_type": "item",
|
||||
"name": "output_format_string",
|
||||
"placeholder": "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "datetime_string",
|
||||
"description": "The output datetime as formatted by the given output_format_string using Python's datetime.strftime()"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "epoch_time",
|
||||
"description": "An integer representing the output time as a number of seconds since January 1 1970 assuming a naive UTC timezone. This is easier to use for comparisons to other epoch timestamps."
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "seconds_modified",
|
||||
"description": "The number of seconds (positive or negative) by which the input was modified"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.6.61906",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
def datetime_modify(input_datetime=None, input_format_string=None, modification_unit=None, amount_to_modify=None, output_format_string=None, **kwargs):
|
||||
"""
|
||||
Change a timestamp by adding or subtracting minutes, hours, or days.
|
||||
|
||||
Args:
|
||||
input_datetime: The datetime to modify, which should be provided in a string format determined by input_format_string
|
||||
input_format_string: 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.
|
||||
modification_unit: 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'
|
||||
amount_to_modify: 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.
|
||||
output_format_string: 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'.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
datetime_string: The output datetime as formatted by the given output_format_string using Python's datetime.strftime()
|
||||
epoch_time: An integer representing the output time as a number of seconds since January 1 1970 assuming a naive UTC timezone. This is easier to use for comparisons to other epoch timestamps.
|
||||
seconds_modified: The number of seconds (positive or negative) by which the input was modified
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
import datetime
|
||||
|
||||
outputs = {}
|
||||
|
||||
# set the input format string to the phantom default if none is provided
|
||||
if not input_format_string:
|
||||
input_format_string = "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
|
||||
# set the date to the default, which is the current time if none is provided
|
||||
if not input_datetime:
|
||||
input_datetime = datetime.datetime.now().strftime(input_format_string)
|
||||
|
||||
# use the phantom default as the output format string if none is provided
|
||||
if not output_format_string:
|
||||
output_format_string = "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
|
||||
if input_format_string.lower() == 'epoch':
|
||||
parsed_input = datetime.datetime.utcfromtimestamp(int(input_datetime))
|
||||
else:
|
||||
parsed_input = datetime.datetime.strptime(input_datetime, input_format_string)
|
||||
phantom.debug("parsed the input datetime as: {}".format(parsed_input))
|
||||
|
||||
# validate the modification_unit parameter, which must be a unit of time
|
||||
if modification_unit == None:
|
||||
modification_unit = 'minutes'
|
||||
if modification_unit not in ['seconds', 'minutes', 'hours', 'days']:
|
||||
raise ValueError('invalid modification_unit. must be either seconds, minutes, hours, or days.')
|
||||
|
||||
# amount_to_modify defaults to zero
|
||||
if not amount_to_modify:
|
||||
amount_to_modify = 0
|
||||
|
||||
# validate that amount_to_modify is an int or float (booleans will work as 0 or 1, but should not be used)
|
||||
if not isinstance(amount_to_modify, int) and not isinstance(amount_to_modify, float):
|
||||
raise ValueError('invalid amount_to_modify. must be an int or float')
|
||||
|
||||
# convert all time units to seconds
|
||||
conversions = {
|
||||
"seconds": 1,
|
||||
"minutes": 60,
|
||||
"hours": 60*60,
|
||||
"days": 60*60*24
|
||||
}
|
||||
conversion_multiplier = conversions.get(modification_unit, None)
|
||||
if not conversion_multiplier:
|
||||
raise KeyError("failed to convert modification_unit to seconds")
|
||||
|
||||
seconds_to_modify = amount_to_modify * conversion_multiplier
|
||||
if seconds_to_modify < 0:
|
||||
phantom.debug("subtracting {} {} which is {} seconds".format(amount_to_modify * -1, modification_unit, seconds_to_modify * -1))
|
||||
else:
|
||||
phantom.debug("adding {} {} which is {} seconds".format(amount_to_modify, modification_unit, seconds_to_modify))
|
||||
|
||||
outputs['seconds_modified'] = seconds_to_modify
|
||||
seconds_to_modify = datetime.timedelta(seconds=seconds_to_modify)
|
||||
|
||||
# do the actual modification
|
||||
phantom.debug("adding {} plus {}".format(parsed_input, seconds_to_modify))
|
||||
result_time = parsed_input + seconds_to_modify
|
||||
phantom.debug("the unformatted result is: {}".format(result_time))
|
||||
|
||||
# use the provided output_format_string to turn the output into a string
|
||||
string_output = result_time.strftime(output_format_string)
|
||||
phantom.debug("the formatted result is: {}".format(string_output))
|
||||
outputs['datetime_string'] = string_output
|
||||
|
||||
# also return an epoch time (seconds since Jan 1 1970) which assumes the input is a naive UTC datetime for time zone purposes
|
||||
epoch_time = (result_time - datetime.datetime.utcfromtimestamp(0)).total_seconds()
|
||||
outputs['epoch_time'] = epoch_time
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"create_time": "2021-04-28T19:54:35.225927+00:00",
|
||||
"custom_function_id": "537aa035a6106bc6aeba14414631e2f17b7bc8bd",
|
||||
"description": "Print debug messages with the type and value of 0-10 different inputs. This is useful for checking the values of input data or the outputs of other playbook blocks.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_1",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_2",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_3",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_4",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_5",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_6",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_7",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_8",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_9",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_10",
|
||||
"placeholder": ""
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.input_name",
|
||||
"description": "The variable name used for this input, such as input_1 or input_7"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.value",
|
||||
"description": "The string representation of the value of this input"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"data_path": "*.types",
|
||||
"description": "The string representation of the type of this input, such as \"<type 'list'>\""
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.3.51237",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
def debug(input_1=None, input_2=None, input_3=None, input_4=None, input_5=None, input_6=None, input_7=None, input_8=None, input_9=None, input_10=None, **kwargs):
|
||||
"""
|
||||
Print debug messages with the type and value of 0-10 different inputs. This is useful for checking the values of input data or the outputs of other playbook blocks.
|
||||
|
||||
Args:
|
||||
input_1 (CEF type: *)
|
||||
input_2 (CEF type: *)
|
||||
input_3 (CEF type: *)
|
||||
input_4 (CEF type: *)
|
||||
input_5 (CEF type: *)
|
||||
input_6 (CEF type: *)
|
||||
input_7 (CEF type: *)
|
||||
input_8 (CEF type: *)
|
||||
input_9 (CEF type: *)
|
||||
input_10 (CEF type: *)
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.input_name: The variable name used for this input, such as input_1 or input_7
|
||||
*.value (CEF type: *): The string representation of the value of this input
|
||||
*.types: The string representation of the type of this input, such as "<type 'list'>"
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
output = []
|
||||
for index, input_value in enumerate([input_1, input_2, input_3, input_4, input_5, input_6, input_7, input_8, input_9, input_10]):
|
||||
this_output = {}
|
||||
phantom.debug("input_{}:".format(index+1))
|
||||
this_output['input_name'] = "input_{}".format(index+1)
|
||||
phantom.debug(" value: " + str(input_value))
|
||||
this_output['value'] = str(input_value)
|
||||
if isinstance(input_value, list):
|
||||
list_item_types = str([type(list_item) for list_item in input_value])
|
||||
phantom.debug(" types: " + list_item_types)
|
||||
this_output['types'] = list_item_types
|
||||
output.append(this_output)
|
||||
|
||||
assert json.dumps(output) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return output
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"create_time": "2021-10-07T15:52:23.940165+00:00",
|
||||
"custom_function_id": "24c4ef5ecd259674a07cd3c747f4223f09b5dd8f",
|
||||
"description": "Takes a provided list of indicator values to search for and finds all related containers. It will produce a list of the related container details.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "An indicator value to search on, such as a file hash or IP address. To search on all indicator values in the container, use \"*\".",
|
||||
"input_type": "list",
|
||||
"name": "value_list",
|
||||
"placeholder": "*"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "The minimum number of similar indicator records that a container must have to be considered \"related.\" If no match count provided, this will default to 1.",
|
||||
"input_type": "item",
|
||||
"name": "minimum_match_count",
|
||||
"placeholder": "1-100"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom container id"
|
||||
],
|
||||
"description": "The container to run indicator analysis against. Supports container object or container_id. This container will also be excluded from the results for related_containers.",
|
||||
"input_type": "item",
|
||||
"name": "container",
|
||||
"placeholder": "container:id"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional modifier to only consider related containers within a time window. Default is -30d. Supports year (y), month (m), day (d), hour (h), or minute (m) Custom function will always set the earliest container window based on the input container \"create_time\".",
|
||||
"input_type": "item",
|
||||
"name": "earliest_time",
|
||||
"placeholder": "-30d"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional comma-separated list of statuses to filter on. Only containers that have statuses matching an item in this list will be included.",
|
||||
"input_type": "item",
|
||||
"name": "filter_status",
|
||||
"placeholder": "open"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional comma-separated list of labels to filter on. Only containers that have labels matching an item in this list will be included.",
|
||||
"input_type": "item",
|
||||
"name": "filter_label",
|
||||
"placeholder": "events"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional comma-separated list of severities to filter on. Only containers that have severities matching an item in this list will be included.",
|
||||
"input_type": "item",
|
||||
"name": "filter_severity",
|
||||
"placeholder": "medium"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional parameter to filter containers that are in a case or not. Defaults to True (drop containers that are already in a case).",
|
||||
"input_type": "item",
|
||||
"name": "filter_in_case",
|
||||
"placeholder": "True or False"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.container_id",
|
||||
"description": "The unique id of the related container"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.container_indicator_match_count",
|
||||
"description": "The number of indicators matched to the related container"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.container_status",
|
||||
"description": "The status of the related container e.g. new, open, closed"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.container_type",
|
||||
"description": "The type of the related container, e.g. default or case"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.container_name",
|
||||
"description": "The name of the related container"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.in_case",
|
||||
"description": "True or False if the related container is already included in a case"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.indicator_ids",
|
||||
"description": "Indicator ID that matched"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"url"
|
||||
],
|
||||
"data_path": "*.container_url",
|
||||
"description": "Link to container"
|
||||
}
|
||||
],
|
||||
"platform_version": "5.0.1.66250",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
def find_related_containers(value_list=None, minimum_match_count=None, container=None, earliest_time=None, filter_status=None, filter_label=None, filter_severity=None, filter_in_case=None, **kwargs):
|
||||
"""
|
||||
Takes a provided list of indicator values to search for and finds all related containers. It will produce a list of the related container details.
|
||||
|
||||
Args:
|
||||
value_list (CEF type: *): An indicator value to search on, such as a file hash or IP address. To search on all indicator values in the container, use "*".
|
||||
minimum_match_count (CEF type: *): The minimum number of similar indicator records that a container must have to be considered "related." If no match count provided, this will default to 1.
|
||||
container (CEF type: phantom container id): The container to run indicator analysis against. Supports container object or container_id. This container will also be excluded from the results for related_containers.
|
||||
earliest_time: Optional modifier to only consider related containers within a time window. Default is -30d. Supports year (y), month (m), day (d), hour (h), or minute (m) Custom function will always set the earliest container window based on the input container "create_time".
|
||||
filter_status: Optional comma-separated list of statuses to filter on. Only containers that have statuses matching an item in this list will be included.
|
||||
filter_label: Optional comma-separated list of labels to filter on. Only containers that have labels matching an item in this list will be included.
|
||||
filter_severity: Optional comma-separated list of severities to filter on. Only containers that have severities matching an item in this list will be included.
|
||||
filter_in_case: Optional parameter to filter containers that are in a case or not. Defaults to True (drop containers that are already in a case).
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.container_id (CEF type: *): The unique id of the related container
|
||||
*.container_indicator_match_count: The number of indicators matched to the related container
|
||||
*.container_status: The status of the related container e.g. new, open, closed
|
||||
*.container_type: The type of the related container, e.g. default or case
|
||||
*.container_name: The name of the related container
|
||||
*.in_case: True or False if the related container is already included in a case
|
||||
*.indicator_ids: Indicator ID that matched
|
||||
*.container_url (CEF type: url): Link to container
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from urllib import parse
|
||||
|
||||
outputs = []
|
||||
related_containers = []
|
||||
indicator_id_dictionary = {}
|
||||
container_dictionary = {}
|
||||
offset_time = None
|
||||
|
||||
base_url = phantom.get_base_url()
|
||||
indicator_by_value_url = phantom.build_phantom_rest_url('indicator_by_value')
|
||||
indicator_common_container_url = phantom.build_phantom_rest_url('indicator_common_container')
|
||||
container_url = phantom.build_phantom_rest_url('container')
|
||||
|
||||
# Get indicator ids based on value_list
|
||||
def format_offset_time(seconds):
|
||||
datetime_obj = datetime.now() - timedelta(seconds=seconds)
|
||||
formatted_time = datetime_obj.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
|
||||
return formatted_time
|
||||
|
||||
def fetch_indicator_ids(value_list):
|
||||
indicator_id_list = []
|
||||
for value in value_list:
|
||||
params = {'indicator_value': f'{value}', 'timerange': 'all'}
|
||||
indicator_id = phantom.requests.get(indicator_by_value_url, params=params, verify=False).json().get('id')
|
||||
if indicator_id:
|
||||
indicator_id_list.append(indicator_id)
|
||||
return indicator_id_list
|
||||
|
||||
# Ensure valid time modifier
|
||||
if earliest_time:
|
||||
# convert user-provided input to seconds
|
||||
char_lookup = {'y': 31557600, 'mon': 2592000, 'w': 604800, 'd': 86400, 'h': 3600, 'm': 60}
|
||||
pattern = re.compile(r'-(\d+)([mM][oO][nN]|[yYwWdDhHmM]{1})$')
|
||||
if re.search(pattern, earliest_time):
|
||||
integer, char = (re.findall(pattern, earliest_time)[0])
|
||||
time_in_seconds = int(integer) * char_lookup[char.lower()]
|
||||
else:
|
||||
raise RuntimeError(f'earliest_time string "{earliest_time}" is incorrectly formatted. Format is -<int><time> where <int> is an integer and <time> is y, mon, w, d, h, or m. Example: "-1h"')
|
||||
else:
|
||||
# default 30 days in seconds
|
||||
time_in_seconds = 2592000
|
||||
|
||||
# Ensure valid container input
|
||||
if isinstance(container, dict) and container.get('id'):
|
||||
current_container = container['id']
|
||||
elif isinstance(container, int):
|
||||
current_container = container
|
||||
else:
|
||||
raise TypeError("The input 'container' is neither a container dictionary nor an int, so it cannot be used")
|
||||
|
||||
if minimum_match_count and not isinstance(minimum_match_count, int):
|
||||
raise TypeError(f"Invalid type for 'minimum_match_count', {type(minimum_match_count)}, must be 'int'")
|
||||
elif not minimum_match_count:
|
||||
minimum_match_count = 1
|
||||
|
||||
# Ensure valid filter inputs
|
||||
status_list, label_list, severity_list = [], [], []
|
||||
if isinstance(filter_status, str):
|
||||
status_list = [item.strip().lower() for item in filter_status.split(',')]
|
||||
if isinstance(filter_label, str):
|
||||
label_list = [item.strip().lower() for item in filter_label.split(',')]
|
||||
if isinstance(filter_severity, str):
|
||||
severity_list = [item.strip().lower() for item in filter_severity.split(',')]
|
||||
if isinstance(filter_in_case, str) and filter_in_case.lower() == 'false':
|
||||
filter_in_case = False
|
||||
else:
|
||||
filter_in_case = True
|
||||
|
||||
# If value list is equal to * then proceed to grab all indicator records for the current container
|
||||
if isinstance(value_list, list) and value_list[0] == "*":
|
||||
new_value_list = []
|
||||
url = phantom.build_phantom_rest_url('container', current_container, 'artifacts') + '?page_size=0'
|
||||
response_data = phantom.requests.get(uri=url, verify=False).json().get('data')
|
||||
if response_data:
|
||||
for data in response_data:
|
||||
for k,v in data['cef'].items():
|
||||
if isinstance(v, list):
|
||||
for item in v:
|
||||
new_value_list.append(item)
|
||||
else:
|
||||
new_value_list.append(v)
|
||||
new_value_list = list(set(new_value_list))
|
||||
indicator_id_list = fetch_indicator_ids(new_value_list)
|
||||
elif isinstance(value_list, list):
|
||||
# dedup value_list
|
||||
value_list = list(set(value_list))
|
||||
indicator_id_list = fetch_indicator_ids(value_list)
|
||||
else:
|
||||
raise TypeError(f"Invalid input for value_list: '{value_list}'")
|
||||
|
||||
# Quit early if no indicator_ids were found
|
||||
if not indicator_id_list:
|
||||
phantom.debug(f"No indicators IDs found for provided values: '{value_list}'")
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
|
||||
# Get list of related containers
|
||||
for indicator_id in list(set(indicator_id_list)):
|
||||
params = {'indicator_ids': indicator_id}
|
||||
response_data = phantom.requests.get(indicator_common_container_url, params=params, verify=False).json()
|
||||
# Populate an indicator dictionary where the original ids are the dictionary keys and the
|
||||
# associated continers are the values
|
||||
if response_data:
|
||||
# Quit early if no related containers were found
|
||||
if len(response_data) == 1 and response_data[0].get('container_id') == current_container:
|
||||
phantom.debug(f"No related containers found for provided values: '{value_list}'")
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
|
||||
indicator_id_dictionary[str(indicator_id)] = []
|
||||
for item in response_data:
|
||||
# Append all related containers except for current container
|
||||
if item['container_id'] != current_container:
|
||||
indicator_id_dictionary[str(indicator_id)].append(item['container_id'])
|
||||
|
||||
# Iterate through the newly created indicator id dictionary and create a dictionary where
|
||||
# the keys are related containers and the values are the associated indicator ids
|
||||
for k,v in indicator_id_dictionary.items():
|
||||
for item in v:
|
||||
if str(item) not in container_dictionary.keys():
|
||||
container_dictionary[str(item)] = [str(k)]
|
||||
else:
|
||||
container_dictionary[str(item)].append(str(k))
|
||||
|
||||
# Iterate through the newly created container dictionary
|
||||
if container_dictionary:
|
||||
|
||||
container_number = 0
|
||||
# Dedupe the number of indicators
|
||||
for k,v in container_dictionary.items():
|
||||
container_dictionary[str(k)] = list(set(v))
|
||||
# Count how many containers are actually going to be queried based on minimum_match_count
|
||||
if len(container_dictionary[str(k)]) >= minimum_match_count:
|
||||
container_number += 1
|
||||
|
||||
# If the container number is greater than 600, then its faster to grab all containers
|
||||
if container_number >= 600:
|
||||
|
||||
# Gather container data
|
||||
params = {'page_size': 0}
|
||||
if offset_time:
|
||||
params['_filter__create_time__gt'] = f'"{format_offset_time(time_in_seconds)}"'
|
||||
containers_response = phantom.requests.get(uri=container_url, params=params, verify=False).json()
|
||||
all_container_dictionary = {}
|
||||
if containers_response['count'] > 0:
|
||||
|
||||
# Build repository of available container data
|
||||
for data in containers_response['data']:
|
||||
all_container_dictionary[str(data['id'])] = data
|
||||
|
||||
for k,v in container_dictionary.items():
|
||||
|
||||
# Omit any containers that have less than the minimum match count
|
||||
if len(container_dictionary[str(k)]) >= minimum_match_count:
|
||||
valid_container = True
|
||||
# Grab container details if its a valid container based on previous filtering.
|
||||
if str(k) in all_container_dictionary.keys():
|
||||
container_data = all_container_dictionary[str(k)]
|
||||
|
||||
# Omit any containers that don't meet the specified criteria
|
||||
if container_data['create_time'] < format_offset_time(time_in_seconds):
|
||||
valid_container = False
|
||||
if status_list and container_data['status'].lower() not in status_list:
|
||||
valid_container = False
|
||||
if label_list and container_data['label'].lower() not in label_list:
|
||||
valid_container = False
|
||||
if severity_list and container_data['severity'].lower() not in severity_list:
|
||||
valid_container = False
|
||||
if response_data['in_case'] and filter_in_case:
|
||||
valid_container = False
|
||||
|
||||
# Build outputs if checks are passed
|
||||
if valid_container:
|
||||
outputs.append({
|
||||
'container_id': str(k),
|
||||
'container_indicator_match_count': len(container_dictionary[str(k)]),
|
||||
'container_status': container_data['status'],
|
||||
'container_type': container_data['container_type'],
|
||||
'container_name': container_data['name'],
|
||||
'container_url': base_url.rstrip('/') + '/mission/{}'.format(str(k)),
|
||||
'in_case': container_data['in_case'],
|
||||
'indicator_id': container_dictionary[str(k)]
|
||||
})
|
||||
|
||||
else:
|
||||
raise RuntimeError(f"'Unable to find any valid containers at url: '{url}'")
|
||||
|
||||
elif container_number < 600 and container_number > 0:
|
||||
# if the container number is smaller than 600, its faster to grab each container individiually
|
||||
for k,v in container_dictionary.items():
|
||||
# Dedupe the number of indicators
|
||||
container_dictionary[str(k)] = list(set(v))
|
||||
|
||||
# If any of the containers contain more than the minimum match count request that container detail.
|
||||
if len(container_dictionary[str(k)]) >= minimum_match_count:
|
||||
|
||||
valid_container = True
|
||||
|
||||
# Grab container details
|
||||
url = phantom.build_phantom_rest_url('container', k)
|
||||
response_data = phantom.requests.get(url, verify=False).json()
|
||||
|
||||
# Omit any containers that don't meet the specified criteria
|
||||
if response_data['create_time'] < format_offset_time(time_in_seconds):
|
||||
valid_container = False
|
||||
if status_list and response_data['status'].lower() not in status_list:
|
||||
valid_container = False
|
||||
if label_list and response_data['label'].lower() not in label_list:
|
||||
valid_container = False
|
||||
if severity_list and response_data['severity'].lower() not in severity_list:
|
||||
valid_container = False
|
||||
if response_data['in_case'] and filter_in_case:
|
||||
valid_container = False
|
||||
|
||||
# Build outputs if checks are passed and valid_container is still true
|
||||
if valid_container:
|
||||
outputs.append({
|
||||
'container_id': str(k),
|
||||
'container_indicator_match_count': len(container_dictionary[str(k)]),
|
||||
'container_status': response_data['status'],
|
||||
'container_severity': response_data['severity'],
|
||||
'container_type': response_data['container_type'],
|
||||
'container_name': response_data['name'],
|
||||
'container_url': base_url.rstrip('/') + '/mission/{}'.format(str(k)),
|
||||
'in_case': response_data['in_case'],
|
||||
'indicator_ids': container_dictionary[str(k)]
|
||||
})
|
||||
|
||||
|
||||
else:
|
||||
raise RuntimeError('Unable to create container_dictionary')
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"create_time": "2021-11-30T14:20:41.840902+00:00",
|
||||
"custom_function_id": "5febf154c78c6815119c08f9dfaba9a661a992d6",
|
||||
"description": "Collect all indicators in a container and separate them by data type. Additional output data paths are created for each data type. Artifact scope is ignored. ",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom container id"
|
||||
],
|
||||
"description": "The current container",
|
||||
"input_type": "item",
|
||||
"name": "container",
|
||||
"placeholder": "container:id"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "all_indicators.*.cef_key",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "all_indicators.*.cef_value",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "all_indicators.*.data_types",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "all_indicators.*.artifact_id",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "domain.*.cef_key",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"domain"
|
||||
],
|
||||
"data_path": "domain.*.cef_value",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "domain.*.artifact_id",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"file name"
|
||||
],
|
||||
"data_path": "file_name.*.cef_key",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"file name"
|
||||
],
|
||||
"data_path": "file_name.*.cef_value",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "file_name.*.artifact_id",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"platform_version": "5.1.0.70187",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
def indicator_collect(container=None, **kwargs):
|
||||
"""
|
||||
Collect all indicators in a container and separate them by data type. Additional output data paths are created for each data type. Artifact scope is ignored.
|
||||
|
||||
Args:
|
||||
container (CEF type: phantom container id): The current container
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
all_indicators.*.cef_key
|
||||
all_indicators.*.cef_value
|
||||
all_indicators.*.data_types
|
||||
all_indicators.*.artifact_id
|
||||
domain.*.cef_key
|
||||
domain.*.cef_value (CEF type: domain)
|
||||
domain.*.artifact_id
|
||||
file_name.*.cef_key (CEF type: file name)
|
||||
file_name.*.cef_value (CEF type: file name)
|
||||
file_name.*.artifact_id
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = {'all_indicators': []}
|
||||
data_types = [
|
||||
"domain",
|
||||
"file name",
|
||||
"file path",
|
||||
"hash",
|
||||
"host name",
|
||||
"ip",
|
||||
"mac address",
|
||||
"md5",
|
||||
"port",
|
||||
"process name",
|
||||
"sha1",
|
||||
"sha256",
|
||||
"sha512",
|
||||
"url",
|
||||
"user name",
|
||||
"vault id"
|
||||
]
|
||||
|
||||
for data_type in data_types:
|
||||
data_type_escaped = data_type.replace(' ', '_')
|
||||
outputs[data_type_escaped] = []
|
||||
|
||||
# validate container and get ID
|
||||
if isinstance(container, dict) and container['id']:
|
||||
container_dict = container
|
||||
container_id = container['id']
|
||||
elif isinstance(container, int):
|
||||
rest_container = phantom.requests.get(uri=phantom.build_phantom_rest_url('container', container), verify=False).json()
|
||||
if 'id' not in rest_container:
|
||||
raise ValueError('Failed to find container with id {container}')
|
||||
container_dict = rest_container
|
||||
container_id = container
|
||||
else:
|
||||
raise TypeError("The input 'container' is neither a container dictionary nor an int, so it cannot be used")
|
||||
|
||||
# fetch all artifacts in the container
|
||||
artifacts = phantom.requests.get(uri=phantom.build_phantom_rest_url('container', container_id, 'artifacts'), params={'page_size': 0}, verify=False).json()['data']
|
||||
|
||||
for artifact in artifacts:
|
||||
artifact_id = artifact['id']
|
||||
for cef_key in artifact['cef']:
|
||||
cef_value = artifact['cef'][cef_key]
|
||||
params = {'indicator_value': cef_value, "_special_contains": True, 'page_size': 1}
|
||||
indicator_data = phantom.requests.get(uri=phantom.build_phantom_rest_url('indicator_by_value'), params=params, verify=False)
|
||||
if indicator_data.status_code == 200:
|
||||
indicator_json = indicator_data.json()
|
||||
data_types = []
|
||||
if indicator_json.get('id'):
|
||||
data_types = indicator_json['_special_contains']
|
||||
# drop none
|
||||
data_types = [item for item in data_types if item]
|
||||
|
||||
# store the value in all_indicators and a list of values for each data type
|
||||
outputs['all_indicators'].append({'cef_key': cef_key, 'cef_value': cef_value, 'artifact_id': artifact_id, 'data_types': data_types})
|
||||
for data_type in data_types:
|
||||
# outputs will have underscores instead of spaces
|
||||
data_type_escaped = data_type.replace(' ', '_')
|
||||
if data_type_escaped not in outputs:
|
||||
outputs[data_type_escaped] = []
|
||||
outputs[data_type_escaped].append({'cef_key': cef_key, 'cef_value': cef_value, 'artifact_id': artifact_id})
|
||||
|
||||
# sort the all_indicators outputs to make them more consistent
|
||||
outputs['all_indicators'].sort(key=lambda indicator: str(indicator['cef_value']))
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"create_time": "2021-08-27T14:42:51.824426+00:00",
|
||||
"custom_function_id": "7e304fa9f4afcb82df42669646baa18fb0afb890",
|
||||
"description": "Get indicator(s) by tags.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Comma separated list of tags. Tags will be OR'd together: e.g. tag1 OR tag2 OR tag3. Tags do not support whitespace and whitespace will be automatically removed.",
|
||||
"input_type": "item",
|
||||
"name": "tags_or",
|
||||
"placeholder": "tag1,tag2,...tagK"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Comma separated list of tags. Tags will be AND'd together: e.g. tag1 AND tag2 AND tag3. Tags do not support whitespace and whitespace will be automatically removed.",
|
||||
"input_type": "item",
|
||||
"name": "tags_and",
|
||||
"placeholder": "tag1,tag2,...tagK"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Defaults to last_30_days\noptions:\ntoday\nyesterday\nthis_week\nthis_month\nlast_7_days\nlast_30_days\nlast_week\nlast_month",
|
||||
"input_type": "item",
|
||||
"name": "indicator_timerange",
|
||||
"placeholder": "last_30_days"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional parameter to ensure the fetched indicator exists in the supplied container.",
|
||||
"input_type": "item",
|
||||
"name": "container",
|
||||
"placeholder": "container:id"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Comma separated list of tags to filter out. If the indicator's tags contain any of the values in this list, they will be omitted from the output.",
|
||||
"input_type": "item",
|
||||
"name": "tags_exclude",
|
||||
"placeholder": "tag1, tag2"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.indicator_id",
|
||||
"description": "A matching indicator id record"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.indicator_value",
|
||||
"description": "A matching indicator value"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.indicator_tags",
|
||||
"description": "List of tags associated with the indicator record"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.indicator_cef_type",
|
||||
"description": "List of cef types associated with the indicator record"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.6.61906",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
def indicator_get_by_tag(tags_or=None, tags_and=None, indicator_timerange=None, container=None, tags_exclude=None, **kwargs):
|
||||
"""
|
||||
Get indicator(s) by tags.
|
||||
|
||||
Args:
|
||||
tags_or: Comma separated list of tags. Tags will be OR'd together: e.g. tag1 OR tag2 OR tag3. Tags do not support whitespace and whitespace will be automatically removed.
|
||||
tags_and: Comma separated list of tags. Tags will be AND'd together: e.g. tag1 AND tag2 AND tag3. Tags do not support whitespace and whitespace will be automatically removed.
|
||||
indicator_timerange: Defaults to last_30_days
|
||||
options:
|
||||
today
|
||||
yesterday
|
||||
this_week
|
||||
this_month
|
||||
last_7_days
|
||||
last_30_days
|
||||
last_week
|
||||
last_month
|
||||
container: Optional parameter to ensure the fetched indicator exists in the supplied container.
|
||||
tags_exclude: Comma separated list of tags to filter out. If the indicator's tags contain any of the values in this list, they will be omitted from the output.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.indicator_id (CEF type: *): A matching indicator id record
|
||||
*.indicator_value (CEF type: *): A matching indicator value
|
||||
*.indicator_tags (CEF type: *): List of tags associated with the indicator record
|
||||
*.indicator_cef_type: List of cef types associated with the indicator record
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
outputs = []
|
||||
indicator_record = {}
|
||||
container_id = None
|
||||
allowed_timeranges = ['today', 'yesterday', 'this_week', 'this_month', 'this_year', 'last_7_days',
|
||||
'last_30_days', 'last_week', 'last_month', 'last_year']
|
||||
|
||||
# Helper function to translate timeranges to relative datetime.
|
||||
# Uses filter_earliest / filter_later for anything 30 days and under as it is quicker.
|
||||
# Uses summary timeranges for items greater than 30 days.
|
||||
def translate_relative_input(relative_time):
|
||||
now = datetime.utcnow()
|
||||
relative_time = relative_time.lower()
|
||||
time_format = "%Y-%m-%dT%H:%M:%S.%fZ"
|
||||
if relative_time == 'today':
|
||||
earliest = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
params = {"_filter_earliest_time__gt": '"{}"'.format(earliest.strftime(time_format))}
|
||||
elif relative_time == 'yesterday':
|
||||
earliest = now.replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=1)
|
||||
latest = earliest.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
params = {"_filter_earliest_time__gt": '"{}"'.format(earliest.strftime(time_format)),
|
||||
"_filter_latest_time__lt": '"{}"'.format(latest.strftime(time_format))}
|
||||
elif relative_time == 'this_week':
|
||||
earliest = now.replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=now.weekday())
|
||||
params = {"_filter_earliest_time__gt": '"{}"'.format(earliest.strftime(time_format))}
|
||||
elif relative_time == 'this_month':
|
||||
earliest = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
params = {"_filter_earliest_time__gt": '"{}"'.format(earliest.strftime(time_format))}
|
||||
elif relative_time == 'last_7_days':
|
||||
earliest = now - timedelta(days=7)
|
||||
params = {"_filter_earliest_time__gt": '"{}"'.format(earliest.strftime(time_format))}
|
||||
elif relative_time == 'last_30_days':
|
||||
params = {}
|
||||
elif relative_time == 'last_week':
|
||||
latest = now.replace(hour=23, minute=59, second=59, microsecond=0) - timedelta(days=now.weekday() + 1)
|
||||
earliest = latest.replace(hour=0, minute=0, second=0) - timedelta(days=8)
|
||||
params = {"_filter_earliest_time__gt": '"{}"'.format(earliest.strftime(time_format)),
|
||||
"_filter_latest_time__lt": '"{}"'.format(latest.strftime(time_format))}
|
||||
else:
|
||||
params = {'timerange': relative_time}
|
||||
|
||||
return params
|
||||
|
||||
if indicator_timerange and isinstance(indicator_timerange, str) and indicator_timerange.lower() in allowed_timeranges:
|
||||
time_params = translate_relative_input(indicator_timerange)
|
||||
elif not indicator_timerange:
|
||||
time_params = {}
|
||||
else:
|
||||
raise ValueError(f"invalid indicator_timerange: '{indicator_timerange}'")
|
||||
|
||||
|
||||
if isinstance(container, int):
|
||||
container_id = container
|
||||
elif isinstance(container, dict):
|
||||
container_id = container['id']
|
||||
elif container:
|
||||
raise TypeError("container_input is neither a int or a dictionary")
|
||||
|
||||
url = phantom.build_phantom_rest_url('indicator')
|
||||
if tags_or:
|
||||
tags_or = tags_or.replace(' ','')
|
||||
for tag in tags_or.split(','):
|
||||
params = {'_filter_tags__contains': f'"{tag}"', "_special_contains": True, 'page_size': 0, **time_params}
|
||||
response = phantom.requests.get(url, params=params, verify=False).json()
|
||||
if response['count'] > 0:
|
||||
for data in response['data']:
|
||||
indicator_record[data['id']] = {'indicator_value': data['value'], 'indicator_tags': data['tags'], 'indicator_cef_type': data['_special_contains']}
|
||||
if tags_and:
|
||||
tags = tags_and.replace(' ','').split(',')
|
||||
params = {'_filter_tags__contains': f'{json.dumps(tags)}', "_special_contains": True, 'page_size': 0, **time_params}
|
||||
response = phantom.requests.get(url, params=params, verify=False).json()
|
||||
if response['count'] > 0:
|
||||
for data in response['data']:
|
||||
indicator_record[data['id']] = {'indicator_value': data['value'], 'indicator_tags': data['tags'], 'indicator_cef_type': data['_special_contains']}
|
||||
|
||||
if tags_exclude:
|
||||
tags_exclude = [item.strip() for item in tags_exclude.split(',')]
|
||||
|
||||
if indicator_record:
|
||||
for i_id, i_data in indicator_record.items():
|
||||
skip_indicator = False
|
||||
|
||||
# Skip indicators that contain an excluded tag
|
||||
if tags_exclude:
|
||||
for item in tags_exclude:
|
||||
if item in i_data['indicator_tags']:
|
||||
skip_indicator = True
|
||||
|
||||
if container_id and not skip_indicator:
|
||||
url = phantom.build_phantom_rest_url('indicator_common_container')
|
||||
params = {'indicator_ids': i_id}
|
||||
response = phantom.requests.get(url, params=params, verify=False).json()
|
||||
if response:
|
||||
for container_item in response:
|
||||
# Only add to outputs if the supplied container_id shows in the common_container results
|
||||
if container_item['container_id'] == container_id:
|
||||
outputs.append({'indicator_id': i_id, **indicator_record[i_id]})
|
||||
else:
|
||||
phantom.debug("No indicators found for provided tags and container")
|
||||
|
||||
elif not skip_indicator:
|
||||
|
||||
outputs.append({'indicator_id': i_id, **indicator_record[i_id]})
|
||||
else:
|
||||
phantom.debug("No indicators found for provided tags")
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"create_time": "2021-10-29T21:39:29.397862+00:00",
|
||||
"custom_function_id": "e7c38086f9c212dc5db9f6198f5e03be8158c2b2",
|
||||
"description": "Tag an existing indicator record. Tags can be overwritten or appended.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "Specifies the indicator which the tag will be added to. Supports a string indicator value or an indicator id.",
|
||||
"input_type": "item",
|
||||
"name": "indicator",
|
||||
"placeholder": "my_indicator"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "Comma separated list of tags. Tags should only contain characters Aa-Zz, 0-9, '-', and '_'.",
|
||||
"input_type": "item",
|
||||
"name": "tags",
|
||||
"placeholder": "tag1,tag2,...,tagk"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Optional input. Either \"true\" or \"false\" with default as \"false\". If set to \"true\", existing tags on the indicator record will be replaced by the provided input. If set to \"false\", the new tags will be appended to the existing indicator tags.",
|
||||
"input_type": "item",
|
||||
"name": "overwrite",
|
||||
"placeholder": "false"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "indicator_id",
|
||||
"description": "The indicator id that was tagged."
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "indicator_tags",
|
||||
"description": "The new tags for the indicator"
|
||||
}
|
||||
],
|
||||
"platform_version": "5.0.1.66250",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
def indicator_tag(indicator=None, tags=None, overwrite=None, **kwargs):
|
||||
"""
|
||||
Tag an existing indicator record. Tags can be overwritten or appended.
|
||||
|
||||
Args:
|
||||
indicator (CEF type: *): Specifies the indicator which the tag will be added to. Supports a string indicator value or an indicator id.
|
||||
tags (CEF type: *): Comma separated list of tags. Tags should only contain characters Aa-Zz, 0-9, '-', and '_'.
|
||||
overwrite: Optional input. Either "true" or "false" with default as "false". If set to "true", existing tags on the indicator record will be replaced by the provided input. If set to "false", the new tags will be appended to the existing indicator tags.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
indicator_id: The indicator id that was tagged.
|
||||
indicator_tags: The new tags for the indicator
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
import string
|
||||
|
||||
outputs = {}
|
||||
|
||||
# remove whitespace from tags and convert to a list
|
||||
tags = tags.replace(' ','').split(',')
|
||||
allowed_characters = string.ascii_lowercase + string.ascii_uppercase + string.digits + '_' + '-'
|
||||
for tag in tags:
|
||||
if any(c not in allowed_characters for c in tag):
|
||||
raise ValueError("Tags should only contain characters Aa-Zz, 0-9, '-', and '_'")
|
||||
|
||||
# overwrite must be "true" or "false" and defaults to "false"
|
||||
if overwrite:
|
||||
if not isinstance(overwrite, str):
|
||||
raise TypeError("overwrite must be a string")
|
||||
if overwrite.lower() == 'true':
|
||||
overwrite = True
|
||||
elif overwrite.lower() == 'false':
|
||||
overwrite = False
|
||||
else:
|
||||
raise ValueError("overwrite must be either 'true' or 'false'")
|
||||
else:
|
||||
overwrite = False
|
||||
|
||||
url = phantom.build_phantom_rest_url('indicator')
|
||||
|
||||
# if indicator is an int, treat it as an indicator id
|
||||
if isinstance(indicator, int):
|
||||
indicator_id = indicator
|
||||
url += f'/{indicator_id}'
|
||||
response = phantom.requests.get(url, verify=False).json()
|
||||
if response.get('id'):
|
||||
existing_tags = response['tags']
|
||||
else:
|
||||
raise RuntimeError(f"No indicator record found for indicator with id: {indicator}")
|
||||
|
||||
# attempt to translate indicator string value to a indicator id
|
||||
elif isinstance(indicator, str):
|
||||
params = {'_filter_value__iexact': f'"{indicator}"'}
|
||||
response = phantom.requests.get(url, params=params, verify=False).json()
|
||||
if response['count'] == 1:
|
||||
indicator_id = response['data'][0]['id']
|
||||
url += f'/{indicator_id}'
|
||||
existing_tags = response['data'][0]['tags']
|
||||
elif response['count'] > 1:
|
||||
raise RuntimeError("Located more than 1 indicator record")
|
||||
else:
|
||||
raise RuntimeError(f"Unable to locate any indicator record for value: {indicator}")
|
||||
else:
|
||||
raise ValueError("Indicator must be a string or integer")
|
||||
|
||||
# if overwrite is set to false, then start with existing tags and append new tags to them
|
||||
if not overwrite:
|
||||
tags = existing_tags + tags
|
||||
|
||||
# deduplicate before POSTing
|
||||
tags = list(set(tags))
|
||||
|
||||
data = {"tags": tags}
|
||||
response = phantom.requests.post(url, json=data, verify=False).json()
|
||||
if response.get('success'):
|
||||
outputs = {'indicator_id': indicator_id, 'indicator_tags': tags}
|
||||
else:
|
||||
raise RuntimeError(f"Failed to update tags for indicator with id: {indicator_id}")
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"create_time": "2021-03-24T13:43:27.570789+00:00",
|
||||
"custom_function_id": "033ea99767cb63402a75971532c551c1be247075",
|
||||
"description": "Load JSON string in a non-strict mode to allow unescaped control characters to be correctly escaped before passing them on to actions that require it.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "String in JSON format with possible unescaped control characters.",
|
||||
"input_type": "item",
|
||||
"name": "json_input",
|
||||
"placeholder": ""
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "json_output",
|
||||
"description": "JSON-serializable string with correctly escaped control characters."
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.2.47587",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
def json_safe_format(json_input=None, **kwargs):
|
||||
"""
|
||||
Load JSON string in a non-strict mode to allow unescaped control characters to be correctly escaped before passing them on to actions that require it.
|
||||
|
||||
Args:
|
||||
json_input: String in JSON format with possible unescaped control characters.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
json_output: JSON-serializable string with correctly escaped control characters.
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = {}
|
||||
|
||||
safe_json = json.dumps(json.loads(json_input, strict=False))
|
||||
|
||||
outputs['json_output'] = safe_json
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"create_time": "2021-04-28T19:54:24.181595+00:00",
|
||||
"custom_function_id": "5afb07e68d980dda3bf45c8852bfa744e0e5d4b2",
|
||||
"description": "Remove non-unique items from a list.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "A list of items to deduplicate",
|
||||
"input_type": "list",
|
||||
"name": "input_list",
|
||||
"placeholder": ""
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.item",
|
||||
"description": "A deduplicated list with all the unique items in input_list"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.3.51237",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
def list_deduplicate(input_list=None, **kwargs):
|
||||
"""
|
||||
Remove non-unique items from a list.
|
||||
|
||||
Args:
|
||||
input_list (CEF type: *): A list of items to deduplicate
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.item (CEF type: *): A deduplicated list with all the unique items in input_list
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
# this only works on lists, so print a warning and return None if the input is not a list
|
||||
if not isinstance(input_list, list):
|
||||
phantom.debug("unable to deduplicate because the input is not a list")
|
||||
return
|
||||
|
||||
# deduplicate the list by converting it to a set. this will fail if items are not hashable
|
||||
unique_set = set(input_list)
|
||||
|
||||
# iterate through the unique items in the set and append each one as its own dictionary
|
||||
outputs = []
|
||||
for item in unique_set:
|
||||
outputs.append({"item": item})
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"create_time": "2021-04-28T19:54:08.906345+00:00",
|
||||
"custom_function_id": "24133bfc9a05a6834e2b30348e9364d62f00413d",
|
||||
"description": "Filter out all values from a list where the value evaluates to False in Python (such as None, \"\", or [])",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "a list of items to filter",
|
||||
"input_type": "list",
|
||||
"name": "input_list",
|
||||
"placeholder": ""
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.item",
|
||||
"description": "a return item for each value that did not evaluate to False"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.3.51237",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
def list_drop_none(input_list=None, **kwargs):
|
||||
"""
|
||||
Filter out all values from a list where the value evaluates to False in Python (such as None, "", or [])
|
||||
|
||||
Args:
|
||||
input_list (CEF type: *): a list of items to filter
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.item (CEF type: *): a return item for each value that did not evaluate to False
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
# this only works on lists, so print a warning and return None if the input is not a list
|
||||
if not isinstance(input_list, list):
|
||||
phantom.debug("unable to process because the input is not a list")
|
||||
return
|
||||
|
||||
# iterate through the items in the list and append each non-falsy one as its own dictionary
|
||||
outputs = []
|
||||
for item in input_list:
|
||||
if item:
|
||||
outputs.append({"item": item})
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"create_time": "2021-04-28T19:53:33.624071+00:00",
|
||||
"custom_function_id": "b524eca7afdfcb2a4714529f28e5f0142b12d459",
|
||||
"description": "Merge 2-10 different data paths into a single output data path. For example, if IP addresses are stored in the fields sourceAddress, destinationAddress, and deviceAddress, then those three fields could be merged together to form a single list of IP addresses.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_1",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_2",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_3",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_4",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_5",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_6",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_7",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_8",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_9",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_10",
|
||||
"placeholder": ""
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.item",
|
||||
"description": "A combined list of all the values from all the input lists"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.3.51237",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
def list_merge(input_1=None, input_2=None, input_3=None, input_4=None, input_5=None, input_6=None, input_7=None, input_8=None, input_9=None, input_10=None, **kwargs):
|
||||
"""
|
||||
Merge 2-10 different data paths into a single output data path. For example, if IP addresses are stored in the fields sourceAddress, destinationAddress, and deviceAddress, then those three fields could be merged together to form a single list of IP addresses.
|
||||
|
||||
Args:
|
||||
input_1 (CEF type: *)
|
||||
input_2 (CEF type: *)
|
||||
input_3 (CEF type: *)
|
||||
input_4 (CEF type: *)
|
||||
input_5 (CEF type: *)
|
||||
input_6 (CEF type: *)
|
||||
input_7 (CEF type: *)
|
||||
input_8 (CEF type: *)
|
||||
input_9 (CEF type: *)
|
||||
input_10 (CEF type: *)
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.item (CEF type: *): A combined list of all the values from all the input lists
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = []
|
||||
|
||||
# loop through all the inputs and use the index to track which input is being processed
|
||||
for index, input_value in enumerate([input_1, input_2, input_3, input_4, input_5, input_6, input_7, input_8, input_9, input_10]):
|
||||
|
||||
# skip the input if no datapath is provided or the datapath does not resolve to anything
|
||||
if not input_value:
|
||||
phantom.debug("skipping input_{} because it is falsy".format(index+1))
|
||||
continue
|
||||
|
||||
# if the input is not a list just append the single item
|
||||
if not isinstance(input_value, list):
|
||||
outputs.append({"item": input_value})
|
||||
phantom.debug("merged 1 items from input_{}".format(index+1))
|
||||
continue
|
||||
|
||||
# keep track of how many items were merged from each input
|
||||
item_count = 0
|
||||
|
||||
# iterate through the list and append each item in its own dictionary
|
||||
for item in input_value:
|
||||
if item:
|
||||
phantom.debug("input_value is {} and item is {}".format(input_value, item))
|
||||
outputs.append({"item": item})
|
||||
item_count += 1
|
||||
phantom.debug("merged {} items from input_{}".format(item_count, index+1))
|
||||
|
||||
phantom.debug("merged results: {}".format(outputs))
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"create_time": "2021-04-27T14:52:21.197821+00:00",
|
||||
"custom_function_id": "4fd5ececa630e09aa1451c8eab283ee4ceae1211",
|
||||
"description": "Mark an object as Evidence in a container",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom container id"
|
||||
],
|
||||
"description": "Container ID or Container Object",
|
||||
"input_type": "item",
|
||||
"name": "container",
|
||||
"placeholder": "container:id"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "The object to mark as evidence. This could be a vault_id, artifact_id, note_id, container_id, or action_run_id. If the previous playbook block is an action then \"keyword_argument:results\" can be used for the action_run_id with the content_type \"action_run_id\". Vault_id can be an ID or a vault hash.",
|
||||
"input_type": "item",
|
||||
"name": "input_object",
|
||||
"placeholder": "artifact id, note id, vault_id, etc."
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "The content type of the object to add as evidence which must be one of the following:\n \n vault_id\n artifact_id\n container_id\n note_id\n action_run_id",
|
||||
"input_type": "item",
|
||||
"name": "content_type",
|
||||
"placeholder": "See help text for supported types"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.id",
|
||||
"description": "ID of the evidence item"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.2.47587",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
def mark_evidence(container=None, input_object=None, content_type=None, **kwargs):
|
||||
"""
|
||||
Mark an object as Evidence in a container
|
||||
|
||||
Args:
|
||||
container (CEF type: phantom container id): Container ID or Container Object
|
||||
input_object (CEF type: *): The object to mark as evidence. This could be a vault_id, artifact_id, note_id, container_id, or action_run_id. If the previous playbook block is an action then "keyword_argument:results" can be used for the action_run_id with the content_type "action_run_id". Vault_id can be an ID or a vault hash.
|
||||
content_type (CEF type: *): The content type of the object to add as evidence which must be one of the following:
|
||||
|
||||
vault_id
|
||||
artifact_id
|
||||
container_id
|
||||
note_id
|
||||
action_run_id
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.id (CEF type: *): ID of the evidence item
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = []
|
||||
container_id = None
|
||||
data = []
|
||||
valid_types = ['vault_id','artifact_id','container_id', 'note_id','action_run_id']
|
||||
|
||||
# Ensure valid content_type:
|
||||
if content_type.lower() not in valid_types:
|
||||
raise TypeError(f"The content_type '{content_type}' is not a valid content_type")
|
||||
|
||||
# Ensure valid container input
|
||||
if isinstance(container, dict) and container.get('id'):
|
||||
container_id = container['id']
|
||||
elif isinstance(container, int) or (isinstance(container, str) and container.isdigit()):
|
||||
container_id = container
|
||||
else:
|
||||
raise TypeError("The input 'container' is neither a container dictionary nor an int, so it cannot be used")
|
||||
|
||||
# If content added is type 'action_run_id',
|
||||
# then iterate through an input object that is a results object,
|
||||
# and append the action_run_id's to data
|
||||
if isinstance(input_object, list) and content_type.lower() == 'action_run_id':
|
||||
for action_result in input_object:
|
||||
if action_result.get('action_run_id'):
|
||||
data.append({
|
||||
"container_id": container_id,
|
||||
"object_id": action_result['action_run_id'],
|
||||
"content_type": 'actionrun',
|
||||
})
|
||||
# If data is still an empty list after for loop,
|
||||
# it indicates that the input_object was not a valid results object
|
||||
if not data:
|
||||
raise TypeError("The input for 'input_object' is not a valid integer or supported object.")
|
||||
|
||||
# If 'input_object' is already an action_run_id, no need to translate it.
|
||||
elif (isinstance(input_object, int) or (isinstance(input_object, str) and input_object.isdigit())) and content_type.lower() == 'action_run_id':
|
||||
data = [{
|
||||
"container_id": container_id,
|
||||
"object_id": input_object,
|
||||
"content_type": 'actionrun',
|
||||
}]
|
||||
|
||||
# If vault_id was entered, check to see if user already entered a vault integer
|
||||
# else if user entered a hash vault_id, attempt to translate to a vault integer
|
||||
elif input_object and content_type.lower() == 'vault_id':
|
||||
if isinstance(input_object, int) or (isinstance(input_object, str) and input_object.isdigit()):
|
||||
content_type = "containerattachment"
|
||||
else:
|
||||
success, message, info = phantom.vault_info(vault_id=input_object)
|
||||
if success == False:
|
||||
raise RuntimeError(f"Invalid vault_id: {message}")
|
||||
else:
|
||||
input_object = info[0]['id']
|
||||
content_type = "containerattachment"
|
||||
data = [{
|
||||
"container_id": container_id,
|
||||
"object_id": input_object,
|
||||
"content_type": content_type,
|
||||
}]
|
||||
|
||||
# If 'container_id' was entered, the content_type needs to be set to 'container'.
|
||||
# Phantom does not allow a literal input of 'container' so thus 'container_id is used.
|
||||
elif (isinstance(input_object, int) or (isinstance(input_object, str) and input_object.isdigit())) and content_type.lower() == 'container_id':
|
||||
data = [{
|
||||
"container_id": container_id,
|
||||
"object_id": input_object,
|
||||
"content_type": 'container',
|
||||
}]
|
||||
|
||||
# If 'artifact_id' was entered, the content_type needs to be set to 'artifact'
|
||||
elif (isinstance(input_object, int) or (isinstance(input_object, str) and input_object.isdigit())) and content_type.lower() == 'artifact_id':
|
||||
data = [{
|
||||
"container_id": container_id,
|
||||
"object_id": input_object,
|
||||
"content_type": 'artifact',
|
||||
}]
|
||||
# If 'note_id' was entered, the content_type needs to be set to 'note'
|
||||
elif (isinstance(input_object, int) or (isinstance(input_object, str) and input_object.isdigit())) and content_type.lower() == 'note_id':
|
||||
data = [{
|
||||
"container_id": container_id,
|
||||
"object_id": input_object,
|
||||
"content_type": 'note',
|
||||
}]
|
||||
else:
|
||||
raise TypeError(f"The input_object is not a valid integer or supported object. Type '{type(input_object)}'")
|
||||
|
||||
# Build url for evidence endpoint
|
||||
url = phantom.build_phantom_rest_url('evidence')
|
||||
|
||||
# Post data to evidence endpoint
|
||||
for item in data:
|
||||
response = phantom.requests.post(uri=url, json=item, verify=False).json()
|
||||
|
||||
# If successful add evidence id to outputs
|
||||
# elif evidence already exists print to debug
|
||||
# else error out
|
||||
if response.get('success'):
|
||||
outputs.append({'id': response['id']})
|
||||
elif response.get('failed') and response.get('message') == 'Already added to Evidence.':
|
||||
phantom.debug(f"{content_type} \'{container_id}\' {response['message']}")
|
||||
else:
|
||||
raise RuntimeError(f"Unable to add evidence: {response}")
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"create_time": "2021-04-28T19:53:21.159704+00:00",
|
||||
"custom_function_id": "561cadef222a920bb91c236c6e9eb7604f537369",
|
||||
"description": "Do nothing and return nothing. Use this if you want to do something in a custom function setup section or leave a placeholder block in a playbook. This does not sleep or wait and will return as soon as possible.",
|
||||
"draft_mode": false,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"platform_version": "4.10.3.51237",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
def noop(**kwargs):
|
||||
"""
|
||||
Do nothing and return nothing. Use this if you want to do something in a custom function setup section or leave a placeholder block in a playbook. This does not sleep or wait and will return as soon as possible.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = {}
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"create_time": "2021-04-16T19:17:01.589064+00:00",
|
||||
"custom_function_id": "e1210d7e75e7046880321061008a70eda2159831",
|
||||
"description": "Return the inputs as outputs. This is useful for publishing pieces of data for other blocks in the playbook to use.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_1",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_2",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_3",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_4",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_5",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_6",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_7",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_8",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_9",
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "",
|
||||
"input_type": "list",
|
||||
"name": "input_10",
|
||||
"placeholder": ""
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.item",
|
||||
"description": "The output item for each input"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.input_name",
|
||||
"description": "The corresponding input name for each output"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.3.51237",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
def passthrough(input_1=None, input_2=None, input_3=None, input_4=None, input_5=None, input_6=None, input_7=None, input_8=None, input_9=None, input_10=None, **kwargs):
|
||||
"""
|
||||
Return the inputs as outputs. This is useful for publishing pieces of data for other blocks in the playbook to use.
|
||||
|
||||
Args:
|
||||
input_1 (CEF type: *)
|
||||
input_2 (CEF type: *)
|
||||
input_3 (CEF type: *)
|
||||
input_4 (CEF type: *)
|
||||
input_5 (CEF type: *)
|
||||
input_6 (CEF type: *)
|
||||
input_7 (CEF type: *)
|
||||
input_8 (CEF type: *)
|
||||
input_9 (CEF type: *)
|
||||
input_10 (CEF type: *)
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.item (CEF type: *): The output item for each input
|
||||
*.input_name: The corresponding input name for each output
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = []
|
||||
for index, input_value in enumerate([input_1, input_2, input_3, input_4, input_5, input_6, input_7, input_8, input_9, input_10]):
|
||||
if input_value:
|
||||
if isinstance(input_value, list):
|
||||
for input_item in input_value:
|
||||
this_output = {}
|
||||
this_output['item'] = input_item
|
||||
this_output['input_name'] = "input_{}".format(index+1)
|
||||
outputs.append(this_output)
|
||||
|
||||
else:
|
||||
this_output = {}
|
||||
this_output['item'] = input_value
|
||||
this_output['input_name'] = "input_{}".format(index+1)
|
||||
outputs.append(this_output)
|
||||
|
||||
phantom.debug('outputs of passthrough:\n{}'.format(outputs))
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"create_time": "2021-09-17T17:19:24.149079+00:00",
|
||||
"custom_function_id": "4581ae2a513db9be2faa490bfe585aa1defdc089",
|
||||
"description": "List all playbooks matching the provided name, category, and tags. If no filters are provided, list all playbooks.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Only return playbooks with the provided name.",
|
||||
"input_type": "item",
|
||||
"name": "name",
|
||||
"placeholder": "Playbook Name"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Only returns playbooks that match the provided category.",
|
||||
"input_type": "item",
|
||||
"name": "category",
|
||||
"placeholder": "Playbook Category"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Only return playbooks that contain ALL the provided tags. Multiple tags must be a comma-separated list.",
|
||||
"input_type": "item",
|
||||
"name": "tags",
|
||||
"placeholder": "tag1,tag2,tag3"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Only return playbooks that exist in this repo.",
|
||||
"input_type": "item",
|
||||
"name": "repo",
|
||||
"placeholder": "local"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Only return playbooks that match the provided type. Accepts 'automation', 'input' or 'data.'",
|
||||
"input_type": "item",
|
||||
"name": "playbook_type",
|
||||
"placeholder": "automation"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"data_path": "*.id",
|
||||
"description": "Playbook ID:\ne.g. 1234"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.full_name",
|
||||
"description": "Playbook full name with repo, e.g.:\nlocal/playbook_name"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.name",
|
||||
"description": "Playbook Name:\ne.g. My Playbook"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.category",
|
||||
"description": "Playbook category:\ne.g. Uncategorized"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.tags",
|
||||
"description": "List of tags:\ne.g. [ tag1, tag2, tag3 ]"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.active",
|
||||
"description": "Playbook automation status:\ne.g. True or False"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.disabled",
|
||||
"description": "Playbook enabled / disabled status:\ne.g. True or False"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.playbook_type",
|
||||
"description": "Playbook type: 'automation' or 'data'"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.input_spec",
|
||||
"description": "If the playbook type is 'data,' this will be a list of dictionaries for the accepted inputs."
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.6.61906",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
def playbooks_list(name=None, category=None, tags=None, repo=None, playbook_type=None, **kwargs):
|
||||
"""
|
||||
List all playbooks matching the provided name, category, and tags. If no filters are provided, list all playbooks.
|
||||
|
||||
Args:
|
||||
name: Only return playbooks with the provided name.
|
||||
category: Only returns playbooks that match the provided category.
|
||||
tags: Only return playbooks that contain ALL the provided tags. Multiple tags must be a comma-separated list.
|
||||
repo: Only return playbooks that exist in this repo.
|
||||
playbook_type: Only return playbooks that match the provided type. Accepts 'automation', 'input' or 'data.'
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.id: Playbook ID:
|
||||
e.g. 1234
|
||||
*.full_name: Playbook full name with repo, e.g.:
|
||||
local/playbook_name
|
||||
*.name: Playbook Name:
|
||||
e.g. My Playbook
|
||||
*.category: Playbook category:
|
||||
e.g. Uncategorized
|
||||
*.tags: List of tags:
|
||||
e.g. [ tag1, tag2, tag3 ]
|
||||
*.active: Playbook automation status:
|
||||
e.g. True or False
|
||||
*.disabled: Playbook enabled / disabled status:
|
||||
e.g. True or False
|
||||
*.playbook_type: Playbook type: 'automation' or 'data'
|
||||
*.input_spec: If the playbook type is 'data,' this will be a list of dictionaries for the accepted inputs.
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = []
|
||||
|
||||
url = phantom.build_phantom_rest_url('playbook')
|
||||
params = {'pretty' : True, 'page_size': 0}
|
||||
|
||||
# Add Name
|
||||
if name:
|
||||
params['_filter_name'] = f'"{name}"'
|
||||
# Add Category
|
||||
if category:
|
||||
params['_filter_category'] = f'"{category}"'
|
||||
|
||||
# Create list of tags and add tags minus whitespace
|
||||
if tags:
|
||||
tags = [item.replace(' ','') for item in tags.split(',')]
|
||||
params['_filter_tags__contains'] = f'{json.dumps(tags)}'
|
||||
|
||||
# Add Repo
|
||||
if isinstance(repo, int):
|
||||
params['_filter_scm'] = f'{repo}'
|
||||
# Translate string to id
|
||||
elif isinstance(repo, str):
|
||||
scm_params = {'_filter_name': f'"{repo}"'}
|
||||
response = phantom.requests.get(uri=phantom.build_phantom_rest_url('scm'), params=scm_params, verify=False).json()
|
||||
if response['count'] == 1:
|
||||
params['_filter_scm'] = '{}'.format(response['data'][0]['id'])
|
||||
else:
|
||||
raise RuntimeError(f"Invalid repo specified: '{repo}'")
|
||||
|
||||
# Add type
|
||||
if isinstance(playbook_type, str) and playbook_type.lower() in ['automation', 'input', 'data']:
|
||||
# Alias 'input' to 'data'
|
||||
if playbook_type.lower() == 'input':
|
||||
playbook_type = 'data'
|
||||
playbook_type = playbook_type.lower()
|
||||
elif playbook_type:
|
||||
raise TypeError(f"Invalid playbook type specified - '{playbook_type}' - must be one of: 'automation', 'input', 'data'")
|
||||
|
||||
# Fetch playbook data
|
||||
response = phantom.requests.get(uri=url, params=params, verify=False).json()
|
||||
# If playbooks were found generate output
|
||||
if response['count'] > 0:
|
||||
for data in response['data']:
|
||||
|
||||
valid_playbook = False
|
||||
# SOAR < 5.0 does not have playbook_type so providing a playbook type will raise an error
|
||||
if not data.get('playbook_type') and playbook_type:
|
||||
raise TypeError("playbook_type filter not valid on SOAR prior to 5.0")
|
||||
# If no playbook type exists user does not want to filter on playbook types
|
||||
elif not playbook_type:
|
||||
valid_playbook = True
|
||||
# If user provided a playbook type then only output playbooks that match that provided type
|
||||
elif data.get('playbook_type') == playbook_type:
|
||||
valid_playbook = True
|
||||
|
||||
if valid_playbook:
|
||||
outputs.append({'id': data['id'],
|
||||
'full_name': f"{data['_pretty_scm']}/{data['name']}",
|
||||
'name': data['name'],
|
||||
'category': data['category'],
|
||||
'tags': data['tags'],
|
||||
'active': data['active'],
|
||||
'disabled': data['disabled'],
|
||||
'playbook_type': data.get('playbook_type'),
|
||||
'input_spec': data.get('input_spec')
|
||||
})
|
||||
else:
|
||||
phantom.debug("No playbook found for supplied filter")
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"create_time": "2021-04-13T21:08:20.394899+00:00",
|
||||
"custom_function_id": "41fbb093035db6dff20778327e42682cd7a33f0c",
|
||||
"description": "Provide a string with one or more email addresses in it to be extracted.\nCan be helpful with strings from the To or CC fields of an email: \"<other_email@domain.com>, 'Name' <e-mail@domain.com>\"",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "String containing email addresses",
|
||||
"input_type": "item",
|
||||
"name": "input_string",
|
||||
"placeholder": ""
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"email"
|
||||
],
|
||||
"data_path": "*.email_address",
|
||||
"description": "Parsed email addresses"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"domain"
|
||||
],
|
||||
"data_path": "*.domain",
|
||||
"description": "Domain names of the parsed email addresses (everything after the \"@\")"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.3.51237",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
def regex_extract_email(input_string=None, **kwargs):
|
||||
"""
|
||||
Provide a string with one or more email addresses in it to be extracted.
|
||||
Can be helpful with strings from the To or CC fields of an email: "<other_email@domain.com>, 'Name' <e-mail@domain.com>"
|
||||
|
||||
Args:
|
||||
input_string (CEF type: *): String containing email addresses
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.email_address (CEF type: email): Parsed email addresses
|
||||
*.domain (CEF type: domain): Domain names of the parsed email addresses (everything after the "@")
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
|
||||
if not input_string:
|
||||
raise ValueError('Missing input_string to process.')
|
||||
|
||||
import re
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = []
|
||||
|
||||
email_regex = r'[a-z0-9.!#$%&\'*+/=?^_`{|}~-]+@[a-z0-9.-]+\.[a-z]{2,}'
|
||||
|
||||
for email in re.findall(email_regex, input_string, re.IGNORECASE):
|
||||
phantom.debug('found email address: {}'.format(email))
|
||||
outputs.append({
|
||||
'email_address': email,
|
||||
'domain': email.split('@')[-1]}
|
||||
)
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"create_time": "2021-03-23T19:52:27.702665+00:00",
|
||||
"custom_function_id": "031e0d2e7d63647b12b99750a49038e9df5a0f3c",
|
||||
"description": "Takes a single input and extracts all IPv4 addresses from it using regex.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "An input string that may contain an arbitrary number of ipv4 addresses",
|
||||
"input_type": "list",
|
||||
"name": "input_string",
|
||||
"placeholder": "192.0.2.1"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"ip"
|
||||
],
|
||||
"data_path": "*.ipv4",
|
||||
"description": "Extracted ipv4 address"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.0.40961",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
def regex_extract_ipv4(input_string=None, **kwargs):
|
||||
"""
|
||||
Takes a single input and extracts all IPv4 addresses from it using regex.
|
||||
|
||||
Args:
|
||||
input_string (CEF type: *): An input string that may contain an arbitrary number of ipv4 addresses
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.ipv4 (CEF type: ip): Extracted ipv4 address
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
import re
|
||||
|
||||
outputs = []
|
||||
ip_list = []
|
||||
for ip in input_string:
|
||||
if ip:
|
||||
ip_rex = re.findall('(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)',ip)
|
||||
for ip in set(ip_rex):
|
||||
ip_list.append(ip)
|
||||
|
||||
for ip in set(ip_list):
|
||||
outputs.append({"ipv4": ip})
|
||||
|
||||
phantom.debug("Extracted ips: {}".format(outputs))
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"create_time": "2021-04-19T20:11:08.343196+00:00",
|
||||
"custom_function_id": "e77ff64b3274a644f799579652c234f9a7f2de28",
|
||||
"description": "Filter values in a list using a regex and either keep or drop values that match, depending on the action parameter.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "The list of items to filter using a regex",
|
||||
"input_type": "list",
|
||||
"name": "input_list",
|
||||
"placeholder": "list to filter"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "The regular expression to use to filter the list",
|
||||
"input_type": "item",
|
||||
"name": "regex",
|
||||
"placeholder": "\\w+\\.exe"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Either 'keep' or 'drop' to specify what to do with the items that match the regular expression. The default is 'keep'.",
|
||||
"input_type": "list",
|
||||
"name": "action",
|
||||
"placeholder": "'keep' or 'drop'"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.item",
|
||||
"description": "List of output items"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.3.51237",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
def regex_filter_list(input_list=None, regex=None, action=None, **kwargs):
|
||||
"""
|
||||
Filter values in a list using a regex and either keep or drop values that match, depending on the action parameter.
|
||||
|
||||
Args:
|
||||
input_list (CEF type: *): The list of items to filter using a regex
|
||||
regex: The regular expression to use to filter the list
|
||||
action: Either 'keep' or 'drop' to specify what to do with the items that match the regular expression. The default is 'keep'.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.item (CEF type: *): List of output items
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
import re
|
||||
|
||||
# this only works on lists, so print a warning and return None if the input is not a list
|
||||
if not isinstance(input_list, list):
|
||||
raise ValueError('input_list is not a list')
|
||||
|
||||
action = action.lower()
|
||||
if action not in ('keep', 'drop'):
|
||||
raise ValueError("action is not 'keep' or 'drop'")
|
||||
|
||||
# iterate through the items in the list and append each non-falsy one as its own dictionary
|
||||
outputs = []
|
||||
for item in input_list:
|
||||
if item:
|
||||
if re.match(str(regex), str(item)):
|
||||
if action == 'keep':
|
||||
outputs.append({"item": item})
|
||||
else:
|
||||
if action == 'drop':
|
||||
outputs.append({"item": item})
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"create_time": "2021-04-14T20:55:22.433977+00:00",
|
||||
"custom_function_id": "b09e6a45f7ced6a7fd17b98264b078ec1996e27d",
|
||||
"description": "Use a regular expression to split an input_string into multiple items.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "The input string to split.",
|
||||
"input_type": "item",
|
||||
"name": "input_string",
|
||||
"placeholder": "string to split"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "The regular expression to use to split the string. Reserved regular expression characters should be escaped with a backslash, so '\\.' will match '.' and '\\\\\\\\' will match '\\'.",
|
||||
"input_type": "item",
|
||||
"name": "regex",
|
||||
"placeholder": "[\\s.,;]+"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Either True or False to indicate whether or not to remove whitespace before and after each item. Defaults to True",
|
||||
"input_type": "item",
|
||||
"name": "strip_whitespace",
|
||||
"placeholder": "True"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.item",
|
||||
"description": "A list of items created by splitting the input string."
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.3.51237",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
def regex_split(input_string=None, regex=None, strip_whitespace=None, **kwargs):
|
||||
"""
|
||||
Use a regular expression to split an input_string into multiple items.
|
||||
|
||||
Args:
|
||||
input_string (CEF type: *): The input string to split.
|
||||
regex: The regular expression to use to split the string. Reserved regular expression characters should be escaped with a backslash, so '\.' will match '.' and '\\\\' will match '\'.
|
||||
strip_whitespace: Either True or False to indicate whether or not to remove whitespace before and after each item. Defaults to True
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.item (CEF type: *): A list of items created by splitting the input string.
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
import re
|
||||
|
||||
outputs = []
|
||||
|
||||
# strip_whitespace defaults to True, but if any value besides "True" is provided, it will be set to False
|
||||
if strip_whitespace == None or strip_whitespace.lower() == 'true':
|
||||
strip_whitespace = True
|
||||
else:
|
||||
strip_whitespace = False
|
||||
|
||||
regex = regex.replace('\\\\','\\')
|
||||
results = re.split(regex, input_string)
|
||||
|
||||
if strip_whitespace:
|
||||
results = [result.strip() for result in results]
|
||||
|
||||
phantom.debug("the input string {} was split into {}".format(input_string, results))
|
||||
|
||||
for result in results:
|
||||
outputs.append({'item': result})
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"create_time": "2021-04-28T19:53:57.385922+00:00",
|
||||
"custom_function_id": "3df37bde81cd69c94bd5d77ea8d8f8cee0214fd5",
|
||||
"description": "Return a list of the components of input_string when split using the specified delimiter. If strip_whitespace is not specified or is \"True\", strip all whitespace from the beginning and end of each resulting component.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "The string to split",
|
||||
"input_type": "item",
|
||||
"name": "input_string",
|
||||
"placeholder": "item_1, item_2, item_3"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "The delimiter to split by, which defaults to a comma",
|
||||
"input_type": "item",
|
||||
"name": "delimiter",
|
||||
"placeholder": ","
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Either True or False to indicate whether or not to remove whitespace before and after each item. Defaults to True",
|
||||
"input_type": "item",
|
||||
"name": "strip_whitespace",
|
||||
"placeholder": "True"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "*.item",
|
||||
"description": "One result for each output item"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.3.51237",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
def string_split(input_string=None, delimiter=None, strip_whitespace=None, **kwargs):
|
||||
"""
|
||||
Return a list of the components of input_string when split using the specified delimiter. If strip_whitespace is not specified or is "True", strip all whitespace from the beginning and end of each resulting component.
|
||||
|
||||
Args:
|
||||
input_string (CEF type: *): The string to split
|
||||
delimiter: The delimiter to split by, which defaults to a comma
|
||||
strip_whitespace: Either True or False to indicate whether or not to remove whitespace before and after each item. Defaults to True
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.item (CEF type: *): One result for each output item
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
if not delimiter:
|
||||
delimiter = ","
|
||||
|
||||
# strip_whitespace defaults to True, but if any value besides "True" is provided, it will be set to False
|
||||
if strip_whitespace == "True" or strip_whitespace == True or strip_whitespace == None:
|
||||
strip_whitespace = True
|
||||
else:
|
||||
strip_whitespace = False
|
||||
|
||||
output_list = input_string.split(delimiter)
|
||||
|
||||
outputs = []
|
||||
for item in output_list:
|
||||
if strip_whitespace:
|
||||
item = item.strip()
|
||||
outputs.append({"item": item})
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"create_time": "2021-04-28T19:53:45.712824+00:00",
|
||||
"custom_function_id": "5b89b2189e3615c380ffdc039992ce8b209f5aa6",
|
||||
"description": "Convert the provided string to all lowercase characters",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "The string to convert to lowercase",
|
||||
"input_type": "item",
|
||||
"name": "input_string",
|
||||
"placeholder": "string to convert"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "lowercase_string",
|
||||
"description": "The lowercase string after conversion"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.3.51237",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
def string_to_lowercase(input_string=None, **kwargs):
|
||||
"""
|
||||
Convert the provided string to all lowercase characters
|
||||
|
||||
Args:
|
||||
input_string (CEF type: *): The string to convert to lowercase
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
lowercase_string (CEF type: *): The lowercase string after conversion
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
try:
|
||||
lowercase_string = input_string.lower()
|
||||
except AttributeError:
|
||||
raise ValueError('input_string must be a string or unicode')
|
||||
|
||||
outputs = {"lowercase_string": lowercase_string}
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"create_time": "2021-04-28T18:59:13.280589+00:00",
|
||||
"custom_function_id": "62a7fbbc463ca097bdb7c7f2cf84d5f7e071cbd8",
|
||||
"description": "Convert the provided string to all uppercase characters.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "The string to convert to uppercase",
|
||||
"input_type": "item",
|
||||
"name": "input_string",
|
||||
"placeholder": ""
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"data_path": "uppercase_string",
|
||||
"description": "The string after converting to upper case"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.3.51237",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
def string_to_uppercase(input_string=None, **kwargs):
|
||||
"""
|
||||
Convert the provided string to all uppercase characters.
|
||||
|
||||
Args:
|
||||
input_string (CEF type: *): The string to convert to uppercase
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
uppercase_string (CEF type: *): The string after converting to upper case
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
try:
|
||||
uppercase_string = input_string.upper()
|
||||
except AttributeError:
|
||||
raise ValueError('input_string must be a string or unicode')
|
||||
|
||||
outputs = {"uppercase_string": uppercase_string}
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"create_time": "2021-02-02T15:10:10.119581+00:00",
|
||||
"custom_function_id": "afbe8e52b8fbd17c6282cfd1165242298655c038",
|
||||
"description": "Separate a URL into its components using urlparse() from the urllib module of Python 3.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"url"
|
||||
],
|
||||
"description": "The URL to parse",
|
||||
"input_type": "item",
|
||||
"name": "input_url",
|
||||
"placeholder": "artifact:*.cef.requestUrl"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "scheme",
|
||||
"description": "The scheme of the URL, such as HTTP, HTTPS, or FTP."
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"domain"
|
||||
],
|
||||
"data_path": "netloc",
|
||||
"description": "The network location of the URL, which is typically the hostname."
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "path",
|
||||
"description": "The path to the resource after the first slash in the URL, such as \"en_us/software/splunk-security-orchestration-and-automation.html\"."
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "params",
|
||||
"description": "The parameters in the URL after the semicolon."
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "query",
|
||||
"description": "The query string of the URL after the question mark. Multiple parameters are not separated from each other."
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "fragment",
|
||||
"description": "The subcomponent of the resource which is identified after the hash sign."
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"url"
|
||||
],
|
||||
"data_path": "output_url",
|
||||
"description": "Passthrough of the original url"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.0.40961",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
def url_parse(input_url=None, **kwargs):
|
||||
"""
|
||||
Separate a URL into its components using urlparse() from the urllib module of Python 3.
|
||||
|
||||
Args:
|
||||
input_url (CEF type: url): The URL to parse
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
scheme: The scheme of the URL, such as HTTP, HTTPS, or FTP.
|
||||
netloc (CEF type: domain): The network location of the URL, which is typically the hostname.
|
||||
path: The path to the resource after the first slash in the URL, such as "en_us/software/splunk-security-orchestration-and-automation.html".
|
||||
params: The parameters in the URL after the semicolon.
|
||||
query: The query string of the URL after the question mark. Multiple parameters are not separated from each other.
|
||||
fragment: The subcomponent of the resource which is identified after the hash sign.
|
||||
output_url (CEF type: url): Passthrough of the original url
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
from urllib.parse import urlparse
|
||||
|
||||
outputs = {}
|
||||
if input_url:
|
||||
parsed = urlparse(input_url)
|
||||
outputs = {'scheme': parsed.scheme, 'netloc': parsed.netloc, 'path': parsed.path, 'params': parsed.params, 'query': parsed.query, 'fragment': parsed.fragment, 'output_url': input_url}
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"create_time": "2021-08-24T14:42:23.906804+00:00",
|
||||
"custom_function_id": "138036ba78ec7b362b5bcd909048b07f0dc0e6f4",
|
||||
"description": "Add a workbook to a container. Provide a container id and a workbook name or id",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom container id"
|
||||
],
|
||||
"description": "A phantom container id",
|
||||
"input_type": "item",
|
||||
"name": "container",
|
||||
"placeholder": "container:id"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "A workbook name or id",
|
||||
"input_type": "item",
|
||||
"name": "workbook",
|
||||
"placeholder": "my_workbook"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Defaults to True. Check to see if workbook already exists in container before adding.",
|
||||
"input_type": "item",
|
||||
"name": "check_for_existing_workbook",
|
||||
"placeholder": "True or False"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "Defaults to True. Sets the added workbook to the current phase.",
|
||||
"input_type": "item",
|
||||
"name": "start_workbook",
|
||||
"placeholder": "True or False"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "workbook_id",
|
||||
"description": "ID of the workbook that was added"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "current_phase_id",
|
||||
"description": "ID of the current phase if start_workbook set to True."
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.6.61906",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
def workbook_add(container=None, workbook=None, check_for_existing_workbook=None, start_workbook=None, **kwargs):
|
||||
"""
|
||||
Add a workbook to a container. Provide a container id and a workbook name or id
|
||||
|
||||
Args:
|
||||
container (CEF type: phantom container id): A phantom container id
|
||||
workbook (CEF type: *): A workbook name or id
|
||||
check_for_existing_workbook: Defaults to True. Check to see if workbook already exists in container before adding.
|
||||
start_workbook: Defaults to True. Sets the added workbook to the current phase.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
workbook_id: ID of the workbook that was added
|
||||
current_phase_id: ID of the current phase if start_workbook set to True.
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = {}
|
||||
existing_templates = []
|
||||
container_id = None
|
||||
|
||||
# Ensure valid container input
|
||||
if isinstance(container, dict) and container.get('id'):
|
||||
container_id = container['id']
|
||||
elif isinstance(container, int):
|
||||
container_id = container
|
||||
else:
|
||||
raise TypeError("The input 'container' is neither a container dictionary nor an int, so it cannot be used")
|
||||
|
||||
# Determine if check_for_existing_workbook should be overwritten by function input
|
||||
if isinstance(check_for_existing_workbook, str) and check_for_existing_workbook.lower() == 'false':
|
||||
check_for_existing_workbook = False
|
||||
else:
|
||||
check_for_existing_workbook = True
|
||||
|
||||
# Determine if start_workbook should be overwritten by function input
|
||||
if isinstance(start_workbook, str) and start_workbook.lower() == 'false':
|
||||
start_workbook = False
|
||||
else:
|
||||
start_workbook = True
|
||||
|
||||
if check_for_existing_workbook:
|
||||
#phantom.debug('Checking for existing workbook')
|
||||
url = phantom.build_phantom_rest_url('container', container_id, 'phases')
|
||||
container_data = phantom.requests.get(url, verify=False).json()
|
||||
if container_data['count'] > 0:
|
||||
phase_names = set([phase_id['name'] for phase_id in container_data['data']])
|
||||
existing_templates = []
|
||||
for name in phase_names:
|
||||
url = phantom.build_phantom_rest_url('workbook_phase_template') + '?_filter_name="{}"'.format(name)
|
||||
phase_template_response = phantom.requests.get(url, verify=False).json()
|
||||
if phase_template_response['count'] > 0:
|
||||
for phase in phase_template_response['data']:
|
||||
existing_templates.append(phase['template'])
|
||||
existing_templates = set(existing_templates)
|
||||
|
||||
if isinstance(workbook,int):
|
||||
workbook_id = workbook
|
||||
if workbook_id in existing_templates:
|
||||
phantom.debug("Workbook already added to container. Skipping")
|
||||
else:
|
||||
phantom.add_workbook(container=container_id, workbook_id=workbook_id)
|
||||
|
||||
|
||||
elif isinstance(workbook, str):
|
||||
url = phantom.build_phantom_rest_url('workbook_template') + '?_filter_name="{}"'.format(workbook)
|
||||
response = phantom.requests.get(url, verify=False).json()
|
||||
if response['count'] > 1:
|
||||
raise RuntimeError('Unable to add workbook - more than one ID matches workbook name')
|
||||
elif response['data'][0]['id']:
|
||||
workbook_id = response['data'][0]['id']
|
||||
|
||||
if workbook_id in existing_templates:
|
||||
phantom.debug("Workbook already added to container. Skipping")
|
||||
else:
|
||||
phantom.add_workbook(container=container_id, workbook_id=workbook_id)
|
||||
|
||||
outputs['workbook_id'] = workbook_id
|
||||
|
||||
if start_workbook:
|
||||
url = phantom.build_phantom_rest_url('workbook_phase_template') + '?_filter_template="{}"'.format(workbook_id)
|
||||
first_phase = phantom.requests.get(url, verify=False).json()['data'][0]['name']
|
||||
url = phantom.build_phantom_rest_url('container', container_id, 'phases') + '?_filter_name="{}"'.format(first_phase)
|
||||
existing_phases = phantom.requests.get(url, verify=False).json()
|
||||
if existing_phases['count'] > 1:
|
||||
raise RuntimeError('Cannot set current phase - duplicate phase names exist in container')
|
||||
else:
|
||||
phantom.set_phase(container=container_id, phase=existing_phases['data'][0]['id'], trace=False)
|
||||
outputs['current_phase_id'] = existing_phases['data'][0]['id']
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"create_time": "2021-02-01T20:09:54.597377+00:00",
|
||||
"custom_function_id": "f6015f324c26982d963e40b33a344f558b0c168e",
|
||||
"description": "Return a list of all the workbooks on this Phantom instance. This might be useful to display possible options for workbooks to add to this event.",
|
||||
"draft_mode": false,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.id",
|
||||
"description": "Unique workbook ID"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.name",
|
||||
"description": "Workbook name"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
""
|
||||
],
|
||||
"data_path": "*.description",
|
||||
"description": "Workbook description"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.status",
|
||||
"description": "Status of the workbook, e.g. published"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.is_default",
|
||||
"description": "True or False if it is the default workbook"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.is_note_required",
|
||||
"description": "True or False if a note is required to finish each task in the workbook"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.creator",
|
||||
"description": "Unique ID of the user that created the workbook"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.create_time",
|
||||
"description": "Timestamp when the workbook was created"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "*.modified_time",
|
||||
"description": "Timestamp when the workbook was last modified"
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.0.40961",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
def workbook_list(**kwargs):
|
||||
"""
|
||||
Return a list of all the workbooks on this Phantom instance. This might be useful to display possible options for workbooks to add to this event.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
*.id: Unique workbook ID
|
||||
*.name: Workbook name
|
||||
*.description: Workbook description
|
||||
*.status: Status of the workbook, e.g. published
|
||||
*.is_default: True or False if it is the default workbook
|
||||
*.is_note_required: True or False if a note is required to finish each task in the workbook
|
||||
*.creator: Unique ID of the user that created the workbook
|
||||
*.create_time: Timestamp when the workbook was created
|
||||
*.modified_time: Timestamp when the workbook was last modified
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = []
|
||||
url = phantom.build_phantom_rest_url('workbook_template') + '?page_size=0'
|
||||
phantom.debug(f"Querying for workbooks using URL: '{url}'")
|
||||
|
||||
response = phantom.requests.get(uri=url, verify=False).json()
|
||||
if response and response['count'] > 0:
|
||||
for data in response['data']:
|
||||
outputs.append({"id": data['id'],
|
||||
"name": data['name'],
|
||||
"description": data['description'],
|
||||
"status": data['status'],
|
||||
"is_default": data['is_default'],
|
||||
"is_note_required": data['is_note_required'],
|
||||
"creator": data['creator'],
|
||||
"create_time": data['create_time'],
|
||||
"modified_time": data['modified_time']})
|
||||
else:
|
||||
raise RuntimeError(f"Error getting workbook data: {response}")
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"create_time": "2021-10-08T21:01:32.942530+00:00",
|
||||
"custom_function_id": "71ecfbca11ceb4ee5cf40d6307e00cf9505bac12",
|
||||
"description": "Update a workbook task by task name",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "Name of a workbook task (Required)",
|
||||
"input_type": "item",
|
||||
"name": "task_name",
|
||||
"placeholder": "my_task"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "Note title goes here (Optional)",
|
||||
"input_type": "item",
|
||||
"name": "note_title",
|
||||
"placeholder": "My Title"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "Body of note goes here (Optional)",
|
||||
"input_type": "item",
|
||||
"name": "note_content",
|
||||
"placeholder": "My notes"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "One of: incomplete, in_progress, complete (Optional)",
|
||||
"input_type": "item",
|
||||
"name": "status",
|
||||
"placeholder": "in_progress"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"*"
|
||||
],
|
||||
"description": "Assigns task to provided owner. Accepts keyword 'current\" to assign task to currently running playbook user. (Optional)",
|
||||
"input_type": "item",
|
||||
"name": "owner",
|
||||
"placeholder": "username"
|
||||
},
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom container id"
|
||||
],
|
||||
"description": "ID of Phantom Container (Required)",
|
||||
"input_type": "item",
|
||||
"name": "container",
|
||||
"placeholder": "container:id"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "note_id",
|
||||
"description": "Returns note_id if a note was added"
|
||||
}
|
||||
],
|
||||
"platform_version": "5.0.1.66250",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
def workbook_task_update(task_name=None, note_title=None, note_content=None, status=None, owner=None, container=None, **kwargs):
|
||||
"""
|
||||
Update a workbook task by task name
|
||||
|
||||
Args:
|
||||
task_name (CEF type: *): Name of a workbook task (Required)
|
||||
note_title (CEF type: *): Note title goes here (Optional)
|
||||
note_content (CEF type: *): Body of note goes here (Optional)
|
||||
status (CEF type: *): One of: incomplete, in_progress, complete (Optional)
|
||||
owner (CEF type: *): Assigns task to provided owner. Accepts keyword 'current" to assign task to currently running playbook user. (Optional)
|
||||
container (CEF type: phantom container id): ID of Phantom Container (Required)
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
note_id: Returns note_id if a note was added
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
outputs = {}
|
||||
|
||||
# Ensure valid container input
|
||||
if isinstance(container, dict) and container.get('id'):
|
||||
container_id = container['id']
|
||||
elif isinstance(container, int):
|
||||
container_id = container
|
||||
else:
|
||||
raise TypeError("The input 'container' is neither a container dictionary nor an int, so it cannot be used")
|
||||
|
||||
if task_name:
|
||||
task_list = phantom.get_tasks(container_id)
|
||||
task_count = 0
|
||||
for task in task_list:
|
||||
if task_name == task['data']['name']:
|
||||
task_count += 1
|
||||
if task_count > 1:
|
||||
raise RuntimeError(f'Unable to update workbook task - multiple tasks match criteria: {task_count}')
|
||||
task_id = task['data']['id']
|
||||
task_is_note_required = task['data']['is_note_required']
|
||||
task_count += 1
|
||||
task_status = task['data']['status']
|
||||
task_notes = task['data']['notes']
|
||||
task_owner = task['data']['owner']
|
||||
|
||||
if task_count == 0:
|
||||
raise RuntimeError(f"No task name matches input task_name: '{task_name}'")
|
||||
|
||||
if task_is_note_required and (not note_content or not note_title) and status == 'complete' and task_status != 1:
|
||||
raise RuntimeError('Unable to update workbook task - The task requires a closing note and a closing title')
|
||||
else:
|
||||
# Add Note
|
||||
if note_content:
|
||||
success, message, note_id = phantom.add_note(container=container_id, note_type='task',
|
||||
task_id=task_id, title=note_title,
|
||||
content=note_content, note_format='markdown')
|
||||
outputs['note_id'] = str(note_id)
|
||||
|
||||
# Set owner
|
||||
if owner:
|
||||
owner_dict = {}
|
||||
# If keyword 'current' entered then translate effective_user id to a username
|
||||
if owner.lower() == 'current':
|
||||
owner_dict['owner_id'] = phantom.get_effective_user()
|
||||
else:
|
||||
# Attempt to translate name to owner_id
|
||||
url = phantom.build_phantom_rest_url('ph_user') + f'?_filter_username="{owner}"'
|
||||
data = phantom.requests.get(url, verify=False).json().get('data')
|
||||
if data and len(data) == 1:
|
||||
owner_dict['owner_id'] = data[0]['id']
|
||||
elif data and len(data) > 1:
|
||||
raise RuntimeError(f'Multiple matches for owner "{owner}"')
|
||||
else:
|
||||
# Attempt to translate name to role_id
|
||||
url = phantom.build_phantom_rest_url('role') + f'?_filter_name="{owner}"'
|
||||
data = phantom.requests.get(url, verify=False).json().get('data')
|
||||
if data and len(data) == 1:
|
||||
owner_dict['role_id'] = data[0]['id']
|
||||
elif data and len(data) > 1:
|
||||
raise RuntimeError(f'Multiple matches for owner "{owner}"')
|
||||
else:
|
||||
raise RuntimeError(f'"{owner}" is not a valid username or role')
|
||||
|
||||
url = phantom.build_phantom_rest_url('workbook_task') + '/{}'.format(task_id)
|
||||
response = phantom.requests.post(url, data=json.dumps(owner_dict), verify=False).json()
|
||||
if not response.get('success'):
|
||||
raise RuntimeError(f'Error setting "{owner}" - {response}')
|
||||
|
||||
# Set Status
|
||||
if isinstance(status, str):
|
||||
status = status.lower()
|
||||
url = phantom.build_phantom_rest_url('workbook_task') + '/{}'.format(task_id)
|
||||
if status == 'complete' and task_status == 0:
|
||||
# Move to in progress
|
||||
data = {'status': 2}
|
||||
response = phantom.requests.post(url, data=json.dumps(data), verify=False).json()
|
||||
if not response.get('success'):
|
||||
raise RuntimeError(f'Error setting status "{status}" - {response}')
|
||||
# Then move to close
|
||||
data = {'status': 1}
|
||||
if task_is_note_required and note_content:
|
||||
data['note'] = note_content
|
||||
data['title'] = note_title
|
||||
data['note_format'] = 'markdown'
|
||||
response = phantom.requests.post(url, data=json.dumps(data), verify=False).json()
|
||||
if not response.get('success'):
|
||||
raise RuntimeError(f'Error setting status "{status}" - {response}')
|
||||
elif (status == 'in progress' or status == 'in_progress') and task_status != 2:
|
||||
data = {'status': 2}
|
||||
# Move to in progress
|
||||
response = phantom.requests.post(url, data=json.dumps(data), verify=False).json()
|
||||
if not response.get('success'):
|
||||
raise RuntimeError(f'Error setting status "{status}" - {response}')
|
||||
elif status == 'incomplete' and task_status != 0:
|
||||
data = {'status': 0}
|
||||
# Move to incomplete
|
||||
response = phantom.requests.post(url, data=json.dumps(data), verify=False).json()
|
||||
if not response.get('success'):
|
||||
raise RuntimeError(f'Error setting status "{status}" - {response}')
|
||||
elif status == 'complete' and task_status != 1:
|
||||
data = {'status': 1}
|
||||
# Move to complete
|
||||
if task_is_note_required and note_content:
|
||||
data['note'] = note_content
|
||||
data['title'] = note_title
|
||||
data['note_format'] = 'markdown'
|
||||
response = phantom.requests.post(url, data=json.dumps(data), verify=False).json()
|
||||
if not response.get('success'):
|
||||
raise RuntimeError(f'Error setting status "{status}" - {response}')
|
||||
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"create_time": "2021-09-21T15:42:41.329482+00:00",
|
||||
"custom_function_id": "88e76e61451c3d3d0b1d2537a8bf7f9b3bf5bc80",
|
||||
"description": "Extract all files recursively from a .zip archive. Add the extracted files to the vault and return the vault IDs of the extracted files. Provide a password if needed to decrypt.",
|
||||
"draft_mode": false,
|
||||
"inputs": [
|
||||
{
|
||||
"contains_type": [
|
||||
"phantom container id"
|
||||
],
|
||||
"description": "The container that extracted files will be added to. Should be a container ID or a container dictionary.",
|
||||
"input_type": "item",
|
||||
"name": "container",
|
||||
"placeholder": "container:id"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "The vault ID of the zip archive to be unzipped.",
|
||||
"input_type": "item",
|
||||
"name": "vault_id",
|
||||
"placeholder": "artifact:*.cef.vaultId"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"description": "The password to use for decryption of the zip archive if necessary.",
|
||||
"input_type": "item",
|
||||
"name": "password",
|
||||
"placeholder": "infected"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "zip_file_info.name",
|
||||
"description": "File name of the zip file in the vault"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "zip_file_info.user",
|
||||
"description": "User who added the zip file to the vault"
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "output_files.*.file_name",
|
||||
"description": "The names of the files extracted from the zip archive."
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "output_files.*.file_path",
|
||||
"description": "The file paths of the files extracted from the zip archive."
|
||||
},
|
||||
{
|
||||
"contains_type": [],
|
||||
"data_path": "output_files.*.vault_id",
|
||||
"description": "The vault IDs of the files extracted from the zip archive."
|
||||
}
|
||||
],
|
||||
"platform_version": "4.10.7.63984",
|
||||
"python_version": "3"
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
def zip_extract(container=None, vault_id=None, password=None, **kwargs):
|
||||
"""
|
||||
Extract all files recursively from a .zip archive. Add the extracted files to the vault and return the vault IDs of the extracted files. Provide a password if needed to decrypt.
|
||||
|
||||
Args:
|
||||
container (CEF type: phantom container id): The container that extracted files will be added to. Should be a container ID or a container dictionary.
|
||||
vault_id: The vault ID of the zip archive to be unzipped.
|
||||
password: The password to use for decryption of the zip archive if necessary.
|
||||
|
||||
Returns a JSON-serializable object that implements the configured data paths:
|
||||
zip_file_info.name: File name of the zip file in the vault
|
||||
zip_file_info.user: User who added the zip file to the vault
|
||||
output_files.*.file_name: The names of the files extracted from the zip archive.
|
||||
output_files.*.file_path: The file paths of the files extracted from the zip archive.
|
||||
output_files.*.vault_id: The vault IDs of the files extracted from the zip archive.
|
||||
"""
|
||||
############################ Custom Code Goes Below This Line #################################
|
||||
import json
|
||||
import phantom.rules as phantom
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
outputs = {'output_files': []}
|
||||
|
||||
# Ensure valid container input
|
||||
if isinstance(container, dict) and container.get('id'):
|
||||
container_id = container['id']
|
||||
elif isinstance(container, int):
|
||||
container_id = container
|
||||
else:
|
||||
raise TypeError("The input 'container' is neither a container dictionary nor an int, so it cannot be used")
|
||||
|
||||
# check the vault_id input
|
||||
success, message, info = phantom.vault_info(
|
||||
vault_id=vault_id,
|
||||
container_id=container_id
|
||||
)
|
||||
if not success:
|
||||
raise ValueError("Could not find file in vault")
|
||||
outputs['zip_file_info'] = info[0]
|
||||
|
||||
if password and not isinstance(password, str):
|
||||
raise TypeError("password must be a string")
|
||||
|
||||
# create a directory to store the extracted files before adding to the vault
|
||||
extract_path = Path("/opt/phantom/vault/tmp/") / vault_id
|
||||
extract_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# extract the files with ZipFile
|
||||
with zipfile.ZipFile(info[0]["path"]) as f_zip:
|
||||
if password:
|
||||
f_zip.extractall(str(extract_path), pwd=password.encode())
|
||||
else:
|
||||
f_zip.extractall(str(extract_path))
|
||||
|
||||
# add each extracted file to the vault and the output
|
||||
for p in extract_path.rglob("*"):
|
||||
if p.is_file():
|
||||
success, message, vault_id = phantom.vault_add(container=container_id, file_location=str(p), file_name=p.name)
|
||||
if not success:
|
||||
raise RuntimeError('failed to add file to vault with path {}'.format(str(p)))
|
||||
outputs['output_files'].append({'file_path': str(p), 'file_name': p.name, 'vault_id': vault_id})
|
||||
|
||||
# Return a JSON-serializable object
|
||||
assert json.dumps(outputs) # Will raise an exception if the :outputs: object is not JSON-serializable
|
||||
return outputs
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
This playbook acts upon events where a file has been determined to be malicious (ie webshells being dropped on an end host).
|
||||
|
||||
Before deleting the file, we run a "more' command on the file in question to extract its contents.
|
||||
|
||||
We then run a delete on the file in question.
|
||||
"""
|
||||
|
||||
import phantom.rules as phantom
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
def on_start(container):
|
||||
phantom.debug('on_start() called')
|
||||
|
||||
# call 'Format_More_Command' block
|
||||
Format_More_Command(container=container)
|
||||
|
||||
return
|
||||
|
||||
def Format_Del_Command(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('Format_Del_Command() called')
|
||||
|
||||
template = """del \"{0}\""""
|
||||
|
||||
# parameter list for template variable replacement
|
||||
parameters = [
|
||||
"artifact:*.cef.filePath",
|
||||
]
|
||||
|
||||
phantom.format(container=container, template=template, parameters=parameters, name="Format_Del_Command")
|
||||
|
||||
Delete_File(container=container)
|
||||
|
||||
return
|
||||
|
||||
def Format_More_Command(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('Format_More_Command() called')
|
||||
|
||||
template = """more \"{0}\""""
|
||||
|
||||
# parameter list for template variable replacement
|
||||
parameters = [
|
||||
"artifact:*.cef.filePath",
|
||||
]
|
||||
|
||||
phantom.format(container=container, template=template, parameters=parameters, name="Format_More_Command")
|
||||
|
||||
Gather_File_Contents(container=container)
|
||||
|
||||
return
|
||||
|
||||
def Gather_File_Contents(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('Gather_File_Contents() called')
|
||||
|
||||
# collect data for 'Gather_File_Contents' call
|
||||
container_data = phantom.collect2(container=container, datapath=['artifact:*.cef.destinationAddress', 'artifact:*.id'])
|
||||
formatted_data_1 = phantom.get_format_data(name='Format_More_Command')
|
||||
|
||||
parameters = []
|
||||
|
||||
# build parameters list for 'Gather_File_Contents' call
|
||||
for container_item in container_data:
|
||||
parameters.append({
|
||||
'ip_hostname': container_item[0],
|
||||
'command': formatted_data_1,
|
||||
'arguments': "",
|
||||
'parser': "",
|
||||
'async': "",
|
||||
'command_id': "",
|
||||
'shell_id': "",
|
||||
# context (artifact id) is added to associate results with the artifact
|
||||
'context': {'artifact_id': container_item[1]},
|
||||
})
|
||||
|
||||
phantom.act(action="run command", parameters=parameters, assets=['winrm'], callback=Format_Del_Command, name="Gather_File_Contents")
|
||||
|
||||
return
|
||||
|
||||
def Delete_File(action=None, success=None, container=None, results=None, handle=None, filtered_artifacts=None, filtered_results=None, custom_function=None, **kwargs):
|
||||
phantom.debug('Delete_File() called')
|
||||
|
||||
#phantom.debug('Action: {0} {1}'.format(action['name'], ('SUCCEEDED' if success else 'FAILED')))
|
||||
|
||||
# collect data for 'Delete_File' call
|
||||
container_data = phantom.collect2(container=container, datapath=['artifact:*.cef.destinationAddress', 'artifact:*.id'])
|
||||
formatted_data_1 = phantom.get_format_data(name='Format_Del_Command')
|
||||
|
||||
parameters = []
|
||||
|
||||
# build parameters list for 'Delete_File' call
|
||||
for container_item in container_data:
|
||||
parameters.append({
|
||||
'ip_hostname': container_item[0],
|
||||
'command': formatted_data_1,
|
||||
'arguments': "",
|
||||
'parser': "",
|
||||
'async': "",
|
||||
'command_id': "",
|
||||
'shell_id': "",
|
||||
# context (artifact id) is added to associate results with the artifact
|
||||
'context': {'artifact_id': container_item[1]},
|
||||
})
|
||||
|
||||
phantom.act(action="run command", parameters=parameters, assets=['winrm'], name="Delete_File")
|
||||
|
||||
return
|
||||
|
||||
def on_finish(container, summary):
|
||||
phantom.debug('on_finish() called')
|
||||
# This function is called after all actions are completed.
|
||||
# summary of all the action and/or all details of actions
|
||||
# can be collected here.
|
||||
|
||||
# summary_json = phantom.get_summary()
|
||||
# if 'result' in summary_json:
|
||||
# for action_result in summary_json['result']:
|
||||
# if 'action_run_id' in action_result:
|
||||
# action_results = phantom.get_action_results(action_run_id=action_result['action_run_id'], result_data=False, flatten=False)
|
||||
# phantom.debug(action_results)
|
||||
|
||||
return
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Delete Detected Files
|
||||
id: fc0edc96-ff2b-48b0-9a6f-63da6783fd63
|
||||
version: 1
|
||||
date: '2021-03-29'
|
||||
author: Philip Royer, Splunk
|
||||
type: Response
|
||||
description: This playbook acts upon events where a file has been determined to be malicious (ie webshells being dropped on an end host). Before deleting the file, we run a "more" command on the file in question to extract its contents. We then run a delete on the file in question.
|
||||
playbook: delete_detected_files
|
||||
how_to_implement: This playbook reads and then deletes files stored with artifact:*.cef.filePath from hosts stored in artifact:*.cef.destinationAddress. Windows Remote Management must be enabled on the remote computer.
|
||||
references: []
|
||||
app_list:
|
||||
- "Windows Remote Management"
|
||||
tags:
|
||||
analytic_story:
|
||||
- Active Directory Lateral Movement
|
||||
detections:
|
||||
- Executable File Written in Administrative SMB Share
|
||||
platform_tags:
|
||||
- Response
|
||||
playbook_fields:
|
||||
- filePath
|
||||
- destinationAddress
|
||||
product:
|
||||
- Splunk SOAR
|
||||
Reference in New Issue
Block a user