Skip to content

Commit

Permalink
Merge pull request #26 from splunk-soar-connectors/next
Browse files Browse the repository at this point in the history
Merging next to main for release 2.7.0
  • Loading branch information
ishans-crest authored Aug 23, 2022
2 parents b119097 + 540d0f8 commit 67fb484
Show file tree
Hide file tree
Showing 10 changed files with 1,278 additions and 238 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/review-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Review Release
concurrency:
group: app-release
cancel-in-progress: true
permissions:
contents: read
id-token: write
statuses: write
on:
workflow_dispatch:
inputs:
task_token:
description: 'StepFunction task token'
required: true

jobs:
review:
uses: 'phantomcyber/dev-cicd-tools/.github/workflows/review-release.yml@main'
with:
task_token: ${{ inputs.task_token }}
secrets:
resume_release_role_arn: ${{ secrets.RESUME_RELEASE_ROLE_ARN }}
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ repos:
- id: org-hook
- id: package-app-dependencies
- repo: https://github.com/Yelp/detect-secrets
rev: v1.2.0
rev: v1.3.0
hooks:
- id: detect-secrets
args: ['--no-verify', '--exclude-files', '^office365.json$']
168 changes: 147 additions & 21 deletions README.md

Large diffs are not rendered by default.

1,138 changes: 960 additions & 178 deletions office365.json

Large diffs are not rendered by default.

116 changes: 93 additions & 23 deletions office365_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ def _process_response(self, r, action_result):
return self._process_json_response(r, action_result)

# Process an HTML response, Do this no matter what the api talks.
# There is a high chance of a PROXY in between phantom and the rest of
# There is a high chance of a PROXY in between Splunk SOAR and the rest of
# world, in case of errors, PROXY's return HTML, this function parses
# the error and adds it to the action_result.
if 'html' in r.headers.get('Content-Type', ''):
Expand Down Expand Up @@ -569,17 +569,22 @@ def _make_rest_call(self, action_result, url, verify=True, headers={}, params=No
except AttributeError:
return RetVal(action_result.set_status(phantom.APP_ERROR, "Invalid method: {0}".format(method)), resp_json)

try:
r = request_func(
url,
data=data,
headers=headers,
verify=verify,
params=params,
timeout=MSGOFFICE365_DEFAULT_REQUEST_TIMEOUT)
except Exception as e:
error_msg = _get_error_message_from_exception(e)
return RetVal(action_result.set_status(phantom.APP_ERROR, "Error connecting to server. {0}".format(error_msg)), resp_json)
for _ in range(self._number_of_retries):
try:
r = request_func(
url,
data=data,
headers=headers,
verify=verify,
params=params,
timeout=MSGOFFICE365_DEFAULT_REQUEST_TIMEOUT)
except Exception as e:
error_msg = _get_error_message_from_exception(e)
return RetVal(action_result.set_status(phantom.APP_ERROR, "Error connecting to server. {0}".format(error_msg)), resp_json)
if r.status_code != 502:
break
self.debug_print("Received 502 status code from the server")
time.sleep(self._retry_wait_time)

if download:
if 200 <= r.status_code < 399:
Expand All @@ -591,7 +596,7 @@ def _make_rest_call(self, action_result, url, verify=True, headers={}, params=No

def _get_asset_name(self, action_result):

rest_endpoint = PHANTOM_ASSET_INFO_URL.format(url=self.get_phantom_base_url(), asset_id=self._asset_id)
rest_endpoint = SPLUNK_SOAR_ASSET_INFO_URL.format(url=self.get_phantom_base_url(), asset_id=self._asset_id)

ret_val, resp_json = self._make_rest_call(action_result, rest_endpoint, False)

Expand All @@ -614,7 +619,7 @@ def _update_container(self, action_result, container_id, container):
:param container: container's payload to update
:return: status phantom.APP_ERROR/phantom.APP_SUCCESS with status message
"""
rest_endpoint = PHANTOM_CONTAINER_INFO_URL.format(url=self.get_phantom_base_url(), container_id=container_id)
rest_endpoint = SPLUNK_SOAR_CONTAINER_INFO_URL.format(url=self.get_phantom_base_url(), container_id=container_id)

try:
data = json.dumps(container)
Expand All @@ -635,7 +640,7 @@ def _update_container(self, action_result, container_id, container):

def _get_phantom_base_url(self, action_result):

ret_val, resp_json = self._make_rest_call(action_result, PHANTOM_SYS_INFO_URL.format(url=self.get_phantom_base_url()), False)
ret_val, resp_json = self._make_rest_call(action_result, SPLUNK_SOAR_SYS_INFO_URL.format(url=self.get_phantom_base_url()), False)

if phantom.is_fail(ret_val):
return (ret_val, None)
Expand All @@ -644,7 +649,7 @@ def _get_phantom_base_url(self, action_result):

if not phantom_base_url:
return (action_result.set_status(phantom.APP_ERROR,
"Phantom Base URL not found in System Settings. Please specify this value in System Settings"), None)
"Splunk SOAR Base URL not found in System Settings. Please specify this value in System Settings"), None)

return (phantom.APP_SUCCESS, phantom_base_url)

Expand All @@ -653,7 +658,7 @@ def _get_url_to_app_rest(self, action_result=None):
if not action_result:
action_result = ActionResult()

# get the phantom ip to redirect to
# get the Splunk SOAR ip to redirect to
ret_val, phantom_base_url = self._get_phantom_base_url(action_result)

if phantom.is_fail(ret_val):
Expand All @@ -665,7 +670,7 @@ def _get_url_to_app_rest(self, action_result=None):
if phantom.is_fail(ret_val):
return (action_result.get_status(), None)

self.save_progress('Using Phantom base URL as: {0}'.format(phantom_base_url))
self.save_progress('Using Splunk SOAR base URL as: {0}'.format(phantom_base_url))

app_json = self.get_app_json()

Expand Down Expand Up @@ -699,7 +704,8 @@ def _make_rest_call_helper(self, action_result, endpoint, verify=True, headers=N
msg = action_result.get_message()

if msg and 'token is invalid' in msg or ('Access token has expired' in
msg) or ('ExpiredAuthenticationToken' in msg) or ('AuthenticationFailed' in msg):
msg) or ('ExpiredAuthenticationToken' in msg) or ('AuthenticationFailed' in msg) or ('TokenExpired' in
msg) or ('InvalidAuthenticationToken' in msg):

self.debug_print("Token is invalid/expired. Hence, generating a new token.")
ret_val = self._get_token(action_result)
Expand Down Expand Up @@ -1132,6 +1138,18 @@ def _process_email_data(self, config, action_result, endpoint, email):

return phantom.APP_SUCCESS

def _remove_tokens(self, action_result):
# checks whether the message includes any of the known error codes
if len(list(filter(lambda x: x in action_result.get_message(), MSGOFFICE365_ASSET_PARAM_CHECK_LIST_ERRORS))) > 0:
if not self._admin_access:
if self._state.get('non_admin_auth', {}).get('access_token'):
self._state['non_admin_auth'].pop('access_token')
if self._state.get('non_admin_auth', {}).get('refresh_token'):
self._state['non_admin_auth'].pop('refresh_token')
else:
if self._state.get('admin_auth', {}).get('access_token'):
self._state['admin_auth'].pop('access_token')

def _handle_test_connectivity(self, param):
""" Function that handles the test connectivity action, it is much simpler than other action handlers."""

Expand Down Expand Up @@ -1242,6 +1260,7 @@ def _handle_test_connectivity(self, param):
ret_val = self._get_token(action_result)

if phantom.is_fail(ret_val):
self._remove_tokens(action_result)
return action_result.get_status()

params = {'$top': '1'}
Expand Down Expand Up @@ -1355,6 +1374,28 @@ def _handle_delete_email(self, param):

return action_result.set_status(phantom.APP_SUCCESS, "Successfully deleted email")

def _handle_delete_event(self, param):

self.save_progress("In action handler for: {0}".format(self.get_action_identifier()))
action_result = self.add_action_result(ActionResult(dict(param)))

email_addr = param['email_address']
message_id = param['id']
send_decline_response = param.get('send_decline_response')
endpoint = "/users/{0}/events/{1}".format(email_addr, message_id)
method = "delete"
data = None
if send_decline_response:
method = "post"
endpoint += '/decline'
data = json.dumps({'sendResponse': True})

ret_val, _ = self._make_rest_call_helper(action_result, endpoint, method=method, data=data)
if phantom.is_fail(ret_val):
return action_result.get_status()

return action_result.set_status(phantom.APP_SUCCESS, "Successfully deleted event")

def _handle_oof_check(self, param):
self.save_progress('In action handler for: {0}'.format(self.get_action_identifier()))
action_result = self.add_action_result(ActionResult(dict(param)))
Expand Down Expand Up @@ -1671,10 +1712,10 @@ def _handle_get_email(self, param):
response['internetMessageHeaders'] = header_response.get('internetMessageHeaders')

if param.get('download_attachments', False) and response.get('hasAttachments'):

endpoint += '/attachments'
attachment_endpoint = '{}?$expand=microsoft.graph.itemattachment/item'.format(endpoint)
ret_val, attach_resp = self._make_rest_call_helper(action_result, attachment_endpoint)

if phantom.is_fail(ret_val):
return action_result.get_status()

Expand All @@ -1689,6 +1730,16 @@ def _handle_get_email(self, param):

response['attachments'] = attach_resp['value']

if response.get('@odata.type') in ["#microsoft.graph.eventMessage", "#microsoft.graph.eventMessageRequest",
"#microsoft.graph.eventMessageResponse"]:

event_endpoint = '{}/?$expand=Microsoft.Graph.EventMessage/Event'.format(endpoint)
ret_val, event_resp = self._make_rest_call_helper(action_result, event_endpoint)
if phantom.is_fail(ret_val):
return action_result.get_status()

response['event'] = event_resp['event']

if 'internetMessageHeaders' in response:
response['internetMessageHeaders'] = self._flatten_headers(response['internetMessageHeaders'])

Expand All @@ -1703,6 +1754,13 @@ def _handle_get_email(self, param):
if attachment_type == '#microsoft.graph.itemAttachment':
attachment['itemType'] = attachment.get('item', {}).get('@odata.type', '')

if param.get('download_email'):
subject = response.get('subject')
email_message = {'id': message_id, 'name': subject if subject else "email_message_{}".format(message_id)}
if not self._handle_item_attachment(email_message, self.get_container_id(), '/users/{0}/messages'.format(email_addr), action_result):
return action_result.set_status(phantom.APP_ERROR, 'Could not download the email. See logs for details')
response['vaultId'] = email_message['vaultId']

action_result.add_data(response)

return action_result.set_status(phantom.APP_SUCCESS, "Successfully fetched email")
Expand Down Expand Up @@ -2348,6 +2406,9 @@ def handle_action(self, param):
elif action_id == 'delete_email':
ret_val = self._handle_delete_email(param)

elif action_id == 'delete_event':
ret_val = self._handle_delete_event(param)

elif action_id == 'get_email':
ret_val = self._handle_get_email(param)

Expand Down Expand Up @@ -2491,6 +2552,18 @@ def initialize(self):
self._admin_consent = config.get('admin_consent')
self._scope = config.get('scope') if config.get('scope') else None

self._number_of_retries = config.get("retry_count", MSGOFFICE365_DEFAULT_NUMBER_OF_RETRIES)
ret_val, self._number_of_retries = _validate_integer(self, self._number_of_retries,
"'Maximum attempts to retry the API call' asset configuration")
if phantom.is_fail(ret_val):
return self.get_status()

self._retry_wait_time = config.get("retry_wait_time", MSGOFFICE365_DEFAULT_RETRY_WAIT_TIME)
ret_val, self._retry_wait_time = _validate_integer(self, self._retry_wait_time,
"'Delay in seconds between retries' asset configuration")
if phantom.is_fail(ret_val):
return self.get_status()

if not self._admin_access:
if not self._scope:
return self.set_status(phantom.APP_ERROR, "Please provide scope for non-admin access in the asset configuration")
Expand All @@ -2512,9 +2585,6 @@ def initialize(self):
if not admin_consent and action_id != 'test_connectivity':
return self.set_status(phantom.APP_ERROR, MSGOFFICE365_RUN_CONNECTIVITY_MSG)

if not self._access_token:
return self.set_status(phantom.APP_ERROR, MSGOFFICE365_UNEXPECTED_ACCESS_TOKEN_ERR)

if not self._admin_access and action_id != 'test_connectivity' and (not self._access_token or not self._refresh_token):
ret_val = self._get_token(action_result)

Expand Down
20 changes: 14 additions & 6 deletions office365_consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
# and limitations under the License.
TC_STATUS_SLEEP = 2
MSGOFFICE365_PER_PAGE_COUNT = 999
PHANTOM_SYS_INFO_URL = "{url}rest/system_info"
PHANTOM_ASSET_INFO_URL = "{url}rest/asset/{asset_id}"
PHANTOM_CONTAINER_INFO_URL = "{url}rest/container/{container_id}"
SPLUNK_SOAR_SYS_INFO_URL = "{url}rest/system_info"
SPLUNK_SOAR_ASSET_INFO_URL = "{url}rest/asset/{asset_id}"
SPLUNK_SOAR_CONTAINER_INFO_URL = "{url}rest/container/{container_id}"
O365_TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
MSGOFFICE365_RUN_CONNECTIVITY_MSG = "Please run test connectivity first to complete authorization flow and "\
"generate a token that the app can use to make calls to the server "
Expand All @@ -28,20 +28,28 @@
"displayName eq 'sync issues'"
MSGOFFICE365_STATE_FILE_CORRUPT_ERR = "Error occurred while loading the state file. " \
"Resetting the state file with the default format. Please test the connectivity."
MSGOFFICE365_AUTHORIZE_TROUBLESHOOT_MSG = 'If authorization URL fails to communicate with your Phantom instance, check whether you have: '\
MSGOFFICE365_AUTHORIZE_TROUBLESHOOT_MSG = 'If authorization URL fails to communicate with your Splunk SOAR instance, check whether you have: '\
' 1. Specified the Web Redirect URL of your App -- The Redirect URL should be <POST URL>/result . '\
' 2. Configured the base URL of your Phantom Instance at Administration -> Company Settings -> Info'
' 2. Configured the base URL of your Splunk SOAR Instance at Administration -> Company Settings -> Info'
MSGOFFICE365_INVALID_PERMISSION_ERR = "Error occurred while saving the newly generated access token "\
"(in place of the expired token) in the state file."
MSGOFFICE365_INVALID_PERMISSION_ERR += " Please check the owner, owner group, and the permissions of the state file. The Phantom "
MSGOFFICE365_INVALID_PERMISSION_ERR += " Please check the owner, owner group, and the permissions of the state file. The Splunk SOAR "
MSGOFFICE365_INVALID_PERMISSION_ERR += "user should have the correct access rights and ownership for the corresponding state file "\
"(refer to readme file for more information)."
MSGOFFICE365_NO_DATA_FOUND = "No data found"
MSGOFFICE365_DUPLICATE_CONTAINER_FOUND_MSG = "duplicate container found"
MSGOFFICE365_ERR_EMPTY_RESPONSE = "Status Code {code}. Empty response and no information in the header."

MSGOFFICE365_DEFAULT_REQUEST_TIMEOUT = 30 # in seconds
MSGOFFICE365_DEFAULT_NUMBER_OF_RETRIES = 3
MSGOFFICE365_DEFAULT_RETRY_WAIT_TIME = 60 # in seconds
MSGOFFICE365_CONTAINER_DESCRIPTION = 'Email ingested using MS Graph API - {last_modified_time}'
MSGOFFICE365_HTTP_401_STATUS_CODE = '401'
MSGOFFICE365_INVALID_CLIENT_ID_ERROR_CODE = 'AADSTS700016'
MSGOFFICE365_INVALID_TENANT_ID_FORMAT_ERROR_CODE = 'AADSTS900023'
MSGOFFICE365_INVALID_TENANT_ID_NOT_FOUND_ERROR_CODE = 'AADSTS90002'
MSGOFFICE365_ASSET_PARAM_CHECK_LIST_ERRORS = [MSGOFFICE365_HTTP_401_STATUS_CODE, MSGOFFICE365_INVALID_CLIENT_ID_ERROR_CODE,
MSGOFFICE365_INVALID_TENANT_ID_FORMAT_ERROR_CODE, MSGOFFICE365_INVALID_TENANT_ID_NOT_FOUND_ERROR_CODE]

# Constants relating to '_get_error_message_from_exception'
ERR_MSG_UNAVAILABLE = "Error message unavailable. Please check the asset configuration and|or action parameters"
Expand Down
13 changes: 13 additions & 0 deletions office365_get_email.html
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ <h4 class="wf-h4-style">Data</h4>
<th class="widget-th">To</th>
<th class="widget-th">Has Attachments?</th>
<th class="widget-th">Internet Message ID</th>
<th class="widget-th">Event ID</th>
</thead>
{% for data in result.data %}
<tr>
Expand Down Expand Up @@ -199,6 +200,18 @@ <h4 class="wf-h4-style">Data</h4>
<span class="fa fa-caret-down" style="font-size: smaller;"></span>
</a>
</td>
{% if data.event.id %}
<td>
<a href="javascript:;" onclick="context_menu(this, [{'contains': ['msgoffice365 event id'],
'value': '{{ data.event.id }}' }], 0, {{ container.id }}, null, false);">
{{ data.event.id }}
&nbsp;
<span class="fa fa-caret-down" style="font-size: smaller;"></span>
</a>
</td>
{% else %}
<td>None</td>
{% endif %}
</tr>
{% endfor %}
</table>
Expand Down
13 changes: 13 additions & 0 deletions office365_list_events.html
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,26 @@ <h4 class="wf-h4-style">Info</h4>
<h4 class="wf-h4-style">Data</h4>
<table class="phantom-table dataTable">
<thead>
<th class="widget-th">Event ID</th>
<th class="widget-th">Subject</th>
<th class="widget-th">Start Time</th>
<th class="widget-th">End Time</th>
<th class="widget-th">Attendee(s)</th>
</thead>
{% for data in result.data %}
<tr>
{% if data.id %}
<td>
<a href="javascript:;" onclick="context_menu(this, [{'contains': ['msgoffice365 event id'],
'value': '{{ data.id }}' }], 0, {{ container.id }}, null, false);">
{{ data.id }}
&nbsp;
<span class="fa fa-caret-down" style="font-size: smaller;"></span>
</a>
</td>
{% else %}
<td>None</td>
{% endif %}
<td>{{ data.subject }}</td>
<td>{{ data.start.dateTime }}</td>
<td>{{ data.end.dateTime }}</td>
Expand Down
Loading

0 comments on commit 67fb484

Please sign in to comment.