diff --git a/phutils.json b/phutils.json index 828f0d2..566c3b9 100644 --- a/phutils.json +++ b/phutils.json @@ -5,7 +5,7 @@ "publisher": "Phantom", "type": "Generic", "main_module": "phutils_connector.pyc", - "app_version": "1.0.1", + "app_version": "1.0.2", "utctime_updated": "2018-03-09T00:17:26.000000Z", "package_name": "phantom_utilities", "product_vendor": "Phantom", @@ -20,7 +20,7 @@ "data_type": "string", "order": 0, "description": "Phantom IP/Hostname", - "required": true + "required": false }, "auth_token": { "data_type": "password", @@ -42,6 +42,19 @@ "order": 4, "description": "Verify Server Certificate", "default": true + }, + "url_prompt_handler": { + "data_type": "string", + "description": "URL Prompt Handler", + "verbose":"URL prompt Handler. For example: http://phantom-url-prompt.splunk.link:80/", + "default":"http://phantom-url-prompt.splunk.link:80/", + "order": 5 + }, + "placeholder_domain": { + "data_type": "string", + "description": "Placeholder URL (end-user URL)", + "verbose":"Same as URL Prompt Handler unless you want App to connect to localhost domain and show different Domain while accessing Prompt.", + "order": 6 } }, "pip_dependencies": { @@ -1448,5 +1461,137 @@ ], "versions": "EQ(*)" } + , + { + "action": "url prompt", + "description": "URL Prompt", + "verbose": "Note that the URL Prompt is Cloud based function. You can either use unauthenticated or authenticated prompt.", + "type": "generic", + "identifier": "url_prompt", + "read_only": true, + "lock" : { + "enabled": false + }, + "parameters": { + "mode": { + "description": "Mode", + "data_type": "string", + "default": "Generate URL", + "required": true, + "order": 0, + "value_list": [ + "Generate url", + "Wait for Response" + ] + }, + "message": { + "description": "Message for the User", + "default": "Let's block this. Do you concur?", + "data_type": "string", + "required": false, + "order": 1 + }, + "options": { + "description": "Comma-separated list of options to be presented to end-user", + "value_list": [ + "Yes,No", + "Block,Unblock", + "1,2,3,4,5", + "Low,Medium,High,Severe", + "Custom Message" + ], + "default": "Block,Unblock,Maybe", + "data_type": "string", + "required": false, + "order": 2 + }, + "response_url": { + "description": "Input generated url to check response", + "default": ".", + "data_type": "string", + "required": false, + "order": 3 + }, + "interval": { + "description": "retry interval", + "data_type": "numeric", + "required": false, + "order": 6 + }, + "banner": { + "description": "Banner", + "default": "blizzard Security", + "data_type": "string", + "required": false, + "order": 7 + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "column_name": "Status", + "column_order": 1 + }, + { + "data_path": "action_result.parameter.mode", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.message", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.header", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.message", + "data_type": "string", + "column_order": 2, + "column_name": "Message / Comment" + }, + { + "data_path": "action_result.data.*.comment", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.selection_string", + "data_type": "string", + "column_order": 3, + "column_name": "Selection String" + }, + { + "data_path": "action_result.data.*.selection_list", + "data_type": "string", + "column_order": 4, + "column_name": "Selection List" + }, + { + "data_path": "action_result.data.*.placeholder", + "data_type": "string", + "column_order": 5, + "column_name": "Ext. URL" + }, + { + "data_path": "action_result.data.*.url", + "data_type": "string", + "column_order": 6, + "column_name": "URL" + }, + { + "data_path": "action_result.summary", + "data_type": "string" + }, + { + "data_path": "action_result.message", + "data_type": "string" + } + ], + "render": { + "type": "table" + }, + "versions": "EQ(*)" + } ] } \ No newline at end of file diff --git a/phutils_connector.py b/phutils_connector.py index 0532d97..2e781e0 100644 --- a/phutils_connector.py +++ b/phutils_connector.py @@ -66,7 +66,8 @@ def handle_action(self, param): 'add_indicator_tag': self._add_ioc_tag, 'update_artifact': self._update_artifact, 'modify_string': self._modify_string, - 'update_container': self._update_container + 'update_container': self._update_container, + 'url_prompt': self._url_prompt } run_action = supported_actions[action_id] @@ -76,7 +77,18 @@ def handle_action(self, param): def _test_connectivity(self, param, action_id): config = self.get_config() + self.save_progress("=== Test Connectivity to URL Prompt Handler ===") + url = config.get('url_prompt_handler', "https://phantom-url-prompt.splunk.link/") + resp = requests.get(url) + if resp.status_code == 200 and resp.text == "root": + self.save_progress("✅ Test Connectivity for URL Prompt passed.") + else: + self.save_progress("⚠️ Test Connectivity for Prompt failed. ") + self.save_progress("Status Code: {} - Response: {}".format(resp.status_code, resp.text)) + self.save_progress(".") + self.save_progress(".") + self.save_progress("=== Test Connectivity to Phantom API for actions other than 'url prompt' ===") try: self._send_request(config, '/rest/cef_metadata', 'get') except Exception as err: @@ -98,7 +110,7 @@ def _test_connectivity(self, param, action_id): def _modify_string(self, param, action_id): action_result = self.add_action_result(ActionResult(dict(param))) - + output = '' try: @@ -111,9 +123,9 @@ def _modify_string(self, param, action_id): phantom.APP_ERROR, 'Unable to parse "data" field - ' + err.message ) - + action_result.add_data({'modified_string': output}) - + return action_result.set_status( phantom.APP_SUCCESS, 'Successfully {}\'d string ({})'.format(param['action'], param['string']) @@ -127,7 +139,7 @@ def _get_pin(self, param, action_id): query = param.get('query', '') endpoint = ( - '/rest/container_pin?_filter_container_id=' + '/rest/container_pin?_filter_container_id=' + str(container_id) + ('&' + query if query else '') ) @@ -159,7 +171,7 @@ def _get_pin(self, param, action_id): action_result.update_summary({ 'pins_found': resp_data['count'] }) - + return action_result.set_status( phantom.APP_SUCCESS, 'Successfully retrieved pins' @@ -190,7 +202,7 @@ def _field_updater(self, data, update_data, overwrite): else: if (overwrite and data) or not(data): return update_data - + return data def _update_container(self, param, action_id): @@ -207,7 +219,7 @@ def _update_container(self, param, action_id): phantom.APP_ERROR, 'Unable to parse "data" field - ' + err.message ) - + try: post_data = self._send_request(config, '/rest/container/{}'.format(container_id), 'POST', payload=json.dumps(update_data)) except Exception as err: @@ -227,6 +239,151 @@ def _update_container(self, param, action_id): 'Successfully updated container (ID: {})'.format(container_id) ) + def _url_prompt(self, param, action_id): + config = self.get_config() + + action_result = self.add_action_result(ActionResult(dict(param))) + + mode = param['mode'] + mode = mode.lower().strip() + message = param.get('message', "Default - Do you concur? ") + options = param.get('options', "Default, Yes, No") + response_url = param.get('response_url', None) + banner = param.get('banner', 'Phantom Prompt') + # header = param.get('header', None) + email = param.get('email', None) + interval = int(param.get('interval', 10)) + + placeholder_domain = config.get('placeholder_domain', 'http://dev98-splunk-phantom01.dev.cloud.blizzard.net:81/') + url = config.get('url_prompt_handler', "https://phantom-url-prompt.splunk.link/") + self.save_progress("[-] Generating URL for Prompt via {}".format(url)) + + payload = {} + payload['data'] = { "mode": mode, + "message": message, + "options": options, + "response_url": response_url, + "email": email, + "banner": banner} + self.save_progress("[-] Payload: {}".format(payload)) + + if mode == "generate url": + # Get URL + + url = "{}/prompt?action=from_phantom".format(url) + result = self.__generate_url(url, payload, action_result) + + self.save_progress("result: {}".format(str(result))) + if result == False: + return action_result.set_status( + phantom.APP_ERROR, + 'Unable to get url for prompt - ' + str(response.text) + ) + # action_result.update_summary(summary) + try: + pid = result.get('url').split("pid=")[1] + result['placeholder'] = "{}/prompt?pid={}".format(placeholder_domain, pid) + self.save_progress("\n\n\n----- Placeholder URL -----\n\n\n\n[-] Prompt Link: {}\n\n\n\n".format(result['placeholder'])) + except Exception as e: + self.save_progress("Something's Phishy: {}".fromat(e)) + action_result.add_data(result) + if email is not None: + self.send_progress("Email") + + elif mode == "wait for response": + + # Get Response + # interval = 1 + tries = 2880 + message = "" + # Check if response_url is None + url = "{}&get_response=yes".format(response_url) + self.save_progress("[-] We shall get response at {}".format(url)) + self.save_progress("[-] Waiting for response") + if response_url is not None: + try: + while tries != 0: + tries = tries - 1 + self.send_progress(".") + result = self.__wait_for_response(url, action_result) + # self.save_progress("result: {}".format(result)) + if result.get('user_response', 'No') == "yes": + result['data']['message'] = result['data']['comment'] + result['data']['url'] = url + break + else: + time.sleep(interval) + + except Exception as e: + self.save_progress("Error while waiting for response: {}".format(e)) + + selection_list = result.get('data').get('selection_list') + summary = { + 'message': "Number of options selected {}".format(selection_list), + 'count': "{}".format(len(selection_list)) + } + action_result.update_summary(summary) + action_result.add_data(result.get('data')) + + self.save_progress(".") + return action_result.set_status( + phantom.APP_SUCCESS, + 'Successfully performed "{}".'.format(mode) + ) + + def __generate_url(self, url, payload, action_result): + # Generate URL + result = {} + try: + # payload = {'prompt': 'value1', 'header': 'value2'} + # headers = {'x-request-from': 'phantom-prompt'} + # resp = requests.post(url, data=payload, headers=headers) + headers = {'Content-Type': 'application/json'} + resp = requests.post(url, json=payload, headers=headers) + if resp.status_code != 200: + self.send_progress("[-] Status Code: {}".format(resp.status_code)) + self.send_progress("[-] Error Code: {}".format(resp.text)) + return action_result.set_status( + phantom.APP_ERROR, + 'Unable to get url for prompt - ' + str(response.texts) + ) + # self.save_progress("[-] Result - {}".format(resp.text)) + result = resp.json() + + except Exception as err: + return action_result.set_status( + phantom.APP_ERROR, + 'Unable to get url for prompt - ' + err.message + ) + + return result + + def __wait_for_response(self, url, action_result): + # Check if User replied + # self.save_progress("[-] In __wait_for_response") + result = {} + try: + # payload = {'prompt': 'value1', 'header': 'value2'} + # headers = {'x-request-from': 'phantom-prompt'} + headers = {'Content-Type': 'application/json'} + resp = requests.get(url, headers=headers) + if resp.status_code != 200: + self.send_progress("[-] Status Code: {}".format(resp.status_code)) + return action_result.set_status( + phantom.APP_ERROR, + 'Unable to get url for prompt - ' + str(response.texts) + ) + result = resp.json() + + except Exception as err: + self.save_progress("Error: {}".format(err)) + return action_result.set_status( + phantom.APP_ERROR, + 'Unable to get url for prompt - ' + err.message + ) + + return result + def _update_artifact(self, param, action_id): config = self.get_config() action_result = self.add_action_result(ActionResult(dict(param))) @@ -242,7 +399,7 @@ def _update_artifact(self, param, action_id): phantom.APP_ERROR, 'Unable to parse "data" field - ' + err.message ) - + try: artifact_data = self._send_request(config, '/rest/artifact/{}'.format(artifact_id), 'GET') except Exception as err: @@ -256,12 +413,12 @@ def _update_artifact(self, param, action_id): phantom.APP_ERROR, 'Artifact not found with id {} - {}'.format(artifact_id) ) - + update_data = {} for key in data.keys(): update_data[key] = self._field_updater(artifact_data.get(key, {}), data[key], overwrite) - + self.debug_print("artifacto", update_data) try: @@ -295,7 +452,7 @@ def _get_ioc(self, config, ioc_value, ioc_id): resp_data = self._send_request(config, endpoint, 'get', params=params) return(resp_data) - + def _get_artifact_data_with_ioc(self, config, page_size, order, ioc_id): params = { 'indicator_id': ioc_id, @@ -373,7 +530,7 @@ def _add_ioc_tag(self, param, action_id): phantom.APP_ERROR, 'Either an ioc_value or ioc_id must be provided' ) - + if not(tags_to_add or tags_to_remove): return action_result.set_status( phantom.APP_ERROR, @@ -405,7 +562,7 @@ def _add_ioc_tag(self, param, action_id): phantom.APP_ERROR, 'Unable to add tag: ' + str(tag_resp_data) ) - + summary = { 'ioc_id': resp_data['id'], 'ioc_value': resp_data['value'],