diff --git a/talosintelligence.json b/talosintelligence.json index 2e317e2..e69c54d 100644 --- a/talosintelligence.json +++ b/talosintelligence.json @@ -11,12 +11,22 @@ "product_version_regex": ".*", "publisher": "Splunk Community", "license": "Copyright (c) Splunk Community, 2024", - "app_version": "1.0.1", + "app_version": "1.0.0", "utctime_updated": "2024-06-21T18:40:03.685771Z", "package_name": "phantom_talosintelligence", "main_module": "talosintelligence_connector.py", "min_phantom_version": "6.2.1.305", "app_wizard_version": "1.0.0", + "pip39_dependencies": { + "pypi": [ + { + "module": "httpx[http2]" + }, + { + "module": "pydantic==1.10.13" + } + ] + }, "configuration": { "base_url": { "data_type": "string", @@ -68,7 +78,7 @@ "required": true, "primary": true, "contains": [ - "ip" + "ips" ], "value_list": [], "default": "", @@ -82,9 +92,7 @@ "data_type": "string", "contains": [ "ip" - ], - "column_name": "ip", - "column_order": 0 + ] }, { "data_path": "action_result.status", @@ -105,26 +113,40 @@ "data_type": "numeric" }, { - "data_path": "action_result.data.0.Threat Level", + "data_path": "action_result.data.*.Observable", + "data_type": "string", + "column_name": "Observable", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.Threat_Level", "data_type": "string", "column_name": "threat level", "column_order": 2 }, { - "data_path": "action_result.data.1.Threat Categories", + "data_path": "action_result.data.*.Threat_Categories", "data_type": "string", "column_name": "threat categories", "column_order": 3 }, { - "data_path": "action_result.data.2.Acceptable Use Policy Categories", + "data_path": "action_result.data.*.AUP", "data_type": "string", "column_name": "Acceptable Use Policy Categories", "column_order": 4 + }, + { + "data_path": "action_result.summary.message", + "data_type": "string", + "example_values": [ + "IP successfully queried" + ] } ], "render": { - "type": "table" + "type": "table", + "title": "IP Reputation Results" }, "versions": "EQ(*)" }, @@ -149,6 +171,18 @@ "default": "", "order": 0, "name": "domain" + }, + "ips": { + "description": "Corresponding IPs to the domain. A domain may have a different reputation based on the IP it resolves to. Passing an IP can improve the accuracy of the response", + "data_type": "string", + "primary": true, + "contains": [ + "ips" + ], + "value_list": [], + "default": "", + "order": 1, + "name": "ips" } }, "output": [ @@ -158,9 +192,14 @@ "contains": [ "domain", "url" - ], - "column_name": "domain", - "column_order": 0 + ] + }, + { + "data_path": "action_result.parameter.ips", + "data_type": "string", + "contains": [ + "ips" + ] }, { "data_path": "action_result.status", @@ -181,25 +220,39 @@ "data_type": "numeric" }, { - "data_path": "action_result.data.0.Threat Level", + "data_path": "action_result.data.*.Observable", + "data_type": "string", + "column_name": "Observable", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.Threat_Level", "data_type": "string", "column_name": "threat level", "column_order": 2 }, { - "data_path": "action_result.data.1.Threat Categories", + "data_path": "action_result.data.*.Threat_Categories", "data_type": "string", "column_name": "threat categories", "column_order": 3 }, { - "data_path": "action_result.data.2.Acceptable Use Policy Categories", + "data_path": "action_result.data.*.AUP", "data_type": "string", "column_name": "Acceptable Use Policy Categories", "column_order": 4 + }, + { + "data_path": "action_result.summary.message", + "data_type": "string", + "example_values": [ + "Domain successfully queried" + ] } ], "render": { + "title": "Domain Reputation Results", "type": "table" }, "versions": "EQ(*)" @@ -224,6 +277,18 @@ "default": "", "order": 0, "name": "url" + }, + "ips": { + "description": "Corresponding IPs to the url. A domain may have a different reputation based on the IP it resolves to. Passing an IP can improve the accuracy of the response", + "data_type": "string", + "primary": true, + "contains": [ + "ip" + ], + "value_list": [], + "default": "", + "order": 1, + "name": "ips" } }, "output": [ @@ -232,9 +297,14 @@ "data_type": "string", "contains": [ "url" - ], - "column_name": "url", - "column_order": 0 + ] + }, + { + "data_path": "action_result.parameter.ips", + "data_type": "string", + "contains": [ + "ips" + ] }, { "data_path": "action_result.status", @@ -255,25 +325,39 @@ "data_type": "numeric" }, { - "data_path": "action_result.data.0.Threat Level", + "data_path": "action_result.data.*.Observable", + "data_type": "string", + "column_name": "Observable", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.Threat_Level", "data_type": "string", "column_name": "threat level", "column_order": 2 }, { - "data_path": "action_result.data.1.Threat Categories", + "data_path": "action_result.data.*.Threat_Categories", "data_type": "string", "column_name": "threat categories", "column_order": 3 }, { - "data_path": "action_result.data.2.Acceptable Use Policy Categories", + "data_path": "action_result.data.*.AUP", "data_type": "string", "column_name": "Acceptable Use Policy Categories", "column_order": 4 + }, + { + "data_path": "action_result.summary.message", + "data_type": "string", + "example_values": [ + "URL successfully queried" + ] } ], "render": { + "title": "URL Reputation Results", "type": "table" }, "versions": "EQ(*)" @@ -286,53 +370,5 @@ "appname": "-", "executable": "spawn3", "disabled": false, - "custom_made": true, - "pip39_dependencies": { - "wheel": [ - { - "module": "anyio", - "input_file": "wheels/py3/anyio-4.4.0-py3-none-any.whl" - }, - { - "module": "exceptiongroup", - "input_file": "wheels/py3/exceptiongroup-1.2.2-py3-none-any.whl" - }, - { - "module": "h11", - "input_file": "wheels/py3/h11-0.14.0-py3-none-any.whl" - }, - { - "module": "h2", - "input_file": "wheels/py3/h2-4.1.0-py3-none-any.whl" - }, - { - "module": "hpack", - "input_file": "wheels/py3/hpack-4.0.0-py3-none-any.whl" - }, - { - "module": "httpcore", - "input_file": "wheels/py3/httpcore-1.0.5-py3-none-any.whl" - }, - { - "module": "httpx", - "input_file": "wheels/py3/httpx-0.27.2-py3-none-any.whl" - }, - { - "module": "hyperframe", - "input_file": "wheels/py3/hyperframe-6.0.1-py3-none-any.whl" - }, - { - "module": "pydantic", - "input_file": "wheels/py39/pydantic-1.10.13-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" - }, - { - "module": "sniffio", - "input_file": "wheels/py3/sniffio-1.3.1-py3-none-any.whl" - }, - { - "module": "typing_extensions", - "input_file": "wheels/py3/typing_extensions-4.12.2-py3-none-any.whl" - } - ] - } -} + "custom_made": true +} \ No newline at end of file diff --git a/talosintelligence_connector.py b/talosintelligence_connector.py index f31a918..5f169cb 100644 --- a/talosintelligence_connector.py +++ b/talosintelligence_connector.py @@ -6,22 +6,25 @@ # Python 3 Compatibility imports from __future__ import print_function, unicode_literals - -import ipaddress -import json import os -import tempfile -import httpx # Phantom App imports import phantom.app as phantom -import requests -from bs4 import BeautifulSoup -from phantom.action_result import ActionResult from phantom.base_connector import BaseConnector +from phantom.action_result import ActionResult +from phantom_common.install_info import is_dev_env from talosintelligence_consts import * +import requests +import json +from bs4 import BeautifulSoup +import tempfile +import httpx +import ipaddress +import time +import random + class RetVal(tuple): def __new__(cls, val1, val2=None): @@ -29,13 +32,14 @@ def __new__(cls, val1, val2=None): class TalosIntelligenceConnector(BaseConnector): + def __init__(self): super(TalosIntelligenceConnector, self).__init__() self._state = None self._base_url = None - self._certificate = None + self._cert = None self._key = None self._appinfo = None @@ -48,8 +52,7 @@ def _process_empty_response(self, response, action_result): return RetVal( action_result.set_status( phantom.APP_ERROR, "Empty response and no information in the header" - ), - None, + ), None ) def _process_html_response(self, response, action_result): @@ -59,17 +62,15 @@ def _process_html_response(self, response, action_result): try: soup = BeautifulSoup(response.text, "html.parser") error_text = soup.text - split_lines = error_text.split("\n") + split_lines = error_text.split('\n') split_lines = [x.strip() for x in split_lines if x.strip()] - error_text = "\n".join(split_lines) + error_text = '\n'.join(split_lines) except: error_text = "Cannot parse error details" - message = "Status Code: {0}. Data from server:\n{1}\n".format( - status_code, error_text - ) + message = "Status Code: {0}. Data from server:\n{1}\n".format(status_code, error_text) - message = message.replace("{", "{{").replace("}", "}}") + message = message.replace(u'{', '{{').replace(u'}', '}}') return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) def _process_json_response(self, r, action_result): @@ -79,10 +80,8 @@ def _process_json_response(self, r, action_result): except Exception as e: return RetVal( action_result.set_status( - phantom.APP_ERROR, - "Unable to parse JSON response. Error: {0}".format(str(e)), - ), - None, + phantom.APP_ERROR, "Unable to parse JSON response. Error: {0}".format(str(e)) + ), None ) # Please specify the status codes here @@ -91,29 +90,43 @@ def _process_json_response(self, r, action_result): # You should process the error returned in the json message = "Error from server. Status Code: {0} Data from server: {1}".format( - r.status_code, r.text.replace("{", "{{").replace("}", "}}") + r.status_code, + r.text.replace(u'{', '{{').replace(u'}', '}}') ) return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) - def _process_response(self, r, action_result): + def _process_response(self, r, action_result, retry=3): # store the r_text in debug data, it will get dumped in the logs if the action fails - if hasattr(action_result, "add_debug_data"): - action_result.add_debug_data({"r_status_code": r.status_code}) - action_result.add_debug_data({"r_text": r.text}) - action_result.add_debug_data({"r_headers": r.headers}) + if hasattr(action_result, 'add_debug_data'): + action_result.add_debug_data({'r_status_code': r.status_code}) + action_result.add_debug_data({'r_text': r.text}) + action_result.add_debug_data({'r_headers': r.headers}) + + retryable_error_codes = {2, 4, 8, 9, 13, 14} + + if retry < MAX_REQUEST_RETRIES: + if r.headers.get('grpc-status', 0) in retryable_error_codes: + return action_result.set_status( + phantom.APP_ERROR, "Got retryable grpc-status of {0} with message {1}".format(r.headers['grpc-status'], r.headers.get('grpc-message', "Error")) + ), r + + if r.status_code == 503: + return action_result.set_status( + phantom.APP_ERROR, "Got retryable http status code {0}".format(r.status_code) + ), r # Process each 'Content-Type' of response separately # Process a json response - if "json" in r.headers.get("Content-Type", ""): + if 'json' in r.headers.get('Content-Type', ''): 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 # 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", ""): + if 'html' in r.headers.get('Content-Type', ''): return self._process_html_response(r, action_result) # it's not content-type that is to be parsed, handle an empty response @@ -122,12 +135,13 @@ def _process_response(self, r, action_result): # everything else is actually an error at this point message = "Can't process response from server. Status Code: {0} Data from server: {1}".format( - r.status_code, r.text.replace("{", "{{").replace("}", "}}") + r.status_code, + r.text.replace('{', '{{').replace('}', '}}') ) return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) - def _make_rest_call(self, endpoint, action_result, method="get", **kwargs): + def _make_rest_call(self, retry, endpoint, action_result, method="get", **kwargs): # **kwargs can be any additional parameters that requests.request accepts config = self.get_config() @@ -137,57 +151,58 @@ def _make_rest_call(self, endpoint, action_result, method="get", **kwargs): # Create a URL to connect to url = self._base_url + endpoint - with tempfile.NamedTemporaryFile( - mode="w+", delete=False, suffix="test" - ) as temp_file: - combined_file = ( - "-----BEGIN CERTIFICATE-----\n" - f"{self._certificate}\n" - "-----END CERTIFICATE-----\n" - "-----BEGIN RSA PRIVATE KEY-----\n" # pragma: allowlist secret - f"{self._key}\n" - "-----END RSA PRIVATE KEY-----\n" - ) - - temp_file.write(combined_file) - temp_file.seek(0) # Move the file pointer to the beginning for reading - temp_file_path = temp_file.name # Get the name of the temporary file - try: - client = httpx.Client( - http2=True, - verify=config.get("verify_server_cert", False), - cert=temp_file_path, - ) - request_func = getattr(client, method) + delay = 0.25 + for i in range(MAX_CONNECTION_RETIRIES): + try: + request_func = getattr(self.client, method) + + r = request_func( + url, + **kwargs + ) + except Exception as e: + self.debug_print(f"Retrying to establish connection to the server for the {i + 1} time") + jittered_delay = random.randint(delay * 0.9, delay * 1.1) + time.sleep(jittered_delay) + delay = max(delay * 2, 256) + + with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix="test") as temp_file: + cert = f"-----BEGIN CERTIFICATE-----\n{self._cert}\n-----END CERTIFICATE-----\n-----BEGIN RSA PRIVATE KEY-----\n{self._key}\n-----END RSA PRIVATE KEY-----\n" + temp_file.write(cert) + temp_file.seek(0) # Move the file pointer to the beginning for reading + temp_file_path = temp_file.name # Get the name of the temporary file + self.client = httpx.Client(http2=True, verify=config.get('verify_server_cert', False), cert=temp_file_path) + + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + + if i == MAX_CONNECTION_RETIRIES - 1: + return RetVal( + action_result.set_status( + phantom.APP_ERROR, "Error Connecting to server. Details: {0}".format(str(e)) + ), resp_json + ) - r = request_func(url, **kwargs) + return self._process_response(r, action_result, retry) - except Exception as e: - if os.path.exists(temp_file_path): - os.remove(temp_file_path) + def _make_rest_call_helper(self, *args, **kwargs): + request_delay = 0.25 + for i in range(MAX_REQUEST_RETRIES + 1): + ret_val, response = self._make_rest_call(i, *args, **kwargs) + if phantom.is_fail(ret_val) and response: + time.sleep(request_delay) + request_delay *= 2 + else: + break - return RetVal( - action_result.set_status( - phantom.APP_ERROR, - "Error Connecting to server. Details: {0}".format(str(e)), - ), - resp_json, - ) - - if os.path.exists(temp_file_path): - os.remove(temp_file_path) - - return self._process_response(r, action_result) + return ret_val, response def _handle_test_connectivity(self, param): action_result = self.add_action_result(ActionResult(dict(param))) self.save_progress("Connecting to endpoint") - ret_val, response = self._make_rest_call( - ENDPOINT_QUERY_AUP_CAT_MAP, - action_result, - "post", - json={"app_info": self._appinfo}, + ret_val, response = self._make_rest_call_helper( + ENDPOINT_QUERY_AUP_CAT_MAP, action_result, "post", json={"app_info": self._appinfo} ) if phantom.is_fail(ret_val): @@ -201,81 +216,124 @@ def _handle_test_connectivity(self, param): return action_result.set_status(phantom.APP_SUCCESS) def _handle_ip_reputation(self, param): - self.save_progress( - "In action handler for: {0}".format(self.get_action_identifier()) - ) + self.save_progress("In action handler for: {0}".format(self.get_action_identifier())) action_result = self.add_action_result(ActionResult(dict(param))) - ip = param["ip"] + ip = param['ip'] try: ip_addr = ipaddress.ip_address(ip) big_endian = int(ip_addr) - - except Exception: - return action_result.set_status( - phantom.APP_ERROR, "Please provide a valid IP Address" - ) + except Exception as exc: + return action_result.set_status(phantom.APP_ERROR, f"Please provide a valid IP Address. Error: {exc}") payload = { - "urls": {"endpoint": [{"ipv4_addr": big_endian}]}, - "app_info": self._appinfo, + "urls": { "endpoint": [{"ipv4_addr": big_endian}]}, + "app_info": self._appinfo } - self._query_reputation(action_result, payload) + ret_val = self._query_reputation(action_result, payload, ip) + if phantom.is_fail(ret_val): + return action_result.get_status() + summary = action_result.update_summary({}) + summary["Message"] = "IP successfully queried" return action_result.set_status(phantom.APP_SUCCESS) def _handle_domain_reputation(self, param): - self.save_progress( - "In action handler for: {0}".format(self.get_action_identifier()) - ) + self.save_progress("In action handler for: {0}".format(self.get_action_identifier())) action_result = self.add_action_result(ActionResult(dict(param))) - domain = param["domain"] + domain = param['domain'] + ips = param.get("ips", "") + ips_list = [item.strip() for item in ips.split(',') if item.strip()] + url_entry = {"raw_url": domain} - payload = {"urls": [{"raw_url": domain}], "app_info": self._appinfo} + endpoints = [] + for ip in ips_list: + try: + ip_addr = ipaddress.ip_address(ip) + big_endian = int(ip_addr) + endpoints.append({"ipv4_addr": big_endian}) + except Exception as exc: + self.debug_print(f"{ip} is not a valid ip address got. Error: {exc}") - self._query_reputation(action_result, payload) + if endpoints: + url_entry["endpoint"] = endpoints + payload = { + "urls": [], + "app_info": self._appinfo + } + payload["urls"].append(url_entry) + + ret_val = self._query_reputation(action_result, payload, domain) + if phantom.is_fail(ret_val): + return action_result.get_status() + + summary = action_result.update_summary({}) + summary["Message"] = "Domain successfully queried" return action_result.set_status(phantom.APP_SUCCESS) def _handle_url_reputation(self, param): - self.save_progress( - "In action handler for: {0}".format(self.get_action_identifier()) - ) + self.save_progress("In action handler for: {0}".format(self.get_action_identifier())) action_result = self.add_action_result(ActionResult(dict(param))) - url = param["url"] + url = param['url'] + ips = param.get("ips", "") + ips_list = [item.strip() for item in ips.split(',') if item.strip()] + url_entry = {"raw_url": url} + + endpoints = [] + for ip in ips_list: + try: + ip_addr = ipaddress.ip_address(ip) + big_endian = int(ip_addr) + endpoints.append({"ipv4_addr": big_endian}) + except Exception as exc: + self.debug_print(f"{ip} is not a valid ip address. Error: {exc}") - payload = {"urls": [{"raw_url": url}], "app_info": self._appinfo} + if endpoints: + url_entry["endpoint"] = endpoints - self._query_reputation(action_result, payload) + payload = { + "urls": [], + "app_info": self._appinfo + } + payload["urls"].append(url_entry) + ret_val = self._query_reputation(action_result, payload, url) + if phantom.is_fail(ret_val): + return action_result.get_status() + + summary = action_result.update_summary({}) + summary["Message"] = "URL successfully queried" return action_result.set_status(phantom.APP_SUCCESS) - def _query_reputation(self, action_result, payload): + def _query_reputation(self, action_result, payload, observable=None): + new_taxonomy_fetched = False + taxonomy_ret_val, taxonomy = self._fetch_taxonomy(action_result) if phantom.is_fail(taxonomy_ret_val): return action_result.get_status() # make rest call - ret_val, response = self._make_rest_call( + ret_val, response = self._make_rest_call_helper( ENDPOINT_QUERY_REPUTATION_V3, action_result, method="post", json=payload ) + if phantom.is_fail(ret_val): + return action_result.get_status() + response_taxonomy_map_version = response["taxonomy_map_version"] if response_taxonomy_map_version > self._state["taxonomy_version"]: - taxonomy_ret_val, taxonomy = self._fetch_taxonomy( - action_result, allow_cache=False - ) + new_taxonomy_fetched = True + taxonomy_ret_val, taxonomy = self._fetch_taxonomy(action_result, allow_cache=False) if phantom.is_fail(ret_val) or "results" not in response: return action_result.get_status() - summary = action_result.update_summary({}) - threat_level = "" threat_categories = {} aup_categories = {} @@ -289,6 +347,16 @@ def _query_reputation(self, action_result, payload): if tax_id not in taxonomy["taxonomies"]: continue + if not taxonomy["taxonomies"][tax_id]["is_avail"]: + if taxonomy["taxonomies"][tax_id]["vers_avail"]["starting"] > taxonomy["taxonomies"][tax_id]["vers_avail"]["ending"] and not new_taxonomy_fetched: + taxonomy_ret_val, taxonomy = self._fetch_taxonomy(action_result, allow_cache=False) + new_taxonomy_fetched = True + if not taxonomy["taxonomies"][tax_id]["is_avail"]: + # even after fetching the taxonomy we're looking for isn't available + continue + else: + continue + category = taxonomy["taxonomies"][tax_id]["name"]["en-us"]["text"] name = taxonomy["taxonomies"][tax_id]["entries"][entry_id]["name"][ "en-us" @@ -304,32 +372,32 @@ def _query_reputation(self, action_result, payload): elif category == "Acceptable Use Policy Categories": aup_categories[name] = description - summary["Threat Levels"] = threat_level - action_result.add_data({"Threat Level": threat_level}) + output = {} + output["Observable"] = observable + output["Threat_Level"] = threat_level + output["Threat_Categories"] = ", ".join(list(threat_categories.keys())) + output["AUP"] = ", ".join(list(aup_categories.keys())) - summary["Threat Categories"] = threat_categories - action_result.add_data( - {"Threat Categories": ", ".join(list(threat_categories.keys()))} - ) + action_result.add_data(output) - summary["Acceptable Use Policy Categories"] = aup_categories - action_result.add_data( - { - "Acceptable Use Policy Categories": ", ".join( - list(aup_categories.keys()) - ) - } - ) + return phantom.APP_SUCCESS def _fetch_taxonomy(self, action_result, allow_cache=True): - payload = {"app_info": self._appinfo} + + payload = { + "app_info": self._appinfo + } if "taxonomy" in self._state and allow_cache: return 1, self._state["taxonomy"] - ret_val, response = self._make_rest_call( + ret_val, response = self._make_rest_call_helper( ENDPOINT_QUERY_TAXONOMIES, action_result, method="post", json=payload ) + self.debug_print("fetching taxonomyyy") + if phantom.is_fail(ret_val): + return action_result.get_status() + taxonomy = response["catalogs"][str(self._catalog_id)] self._state = {"taxonomy": taxonomy, "taxonomy_version": response["version"]} @@ -343,16 +411,16 @@ def handle_action(self, param): self.debug_print("action_id", self.get_action_identifier()) - if action_id == "ip_reputation": + if action_id == 'ip_reputation': ret_val = self._handle_ip_reputation(param) - if action_id == "domain_reputation": + if action_id == 'domain_reputation': ret_val = self._handle_domain_reputation(param) - if action_id == "url_reputation": + if action_id == 'url_reputation': ret_val = self._handle_url_reputation(param) - if action_id == "test_connectivity": + if action_id == 'test_connectivity': ret_val = self._handle_test_connectivity(param) return ret_val @@ -366,12 +434,10 @@ def initialize(self): config = self.get_config() def insert_newlines(string, every=64): - return "\n".join( - string[i: i + every] for i in range(0, len(string), every) - ) + return '\n'.join(string[i:i + every] for i in range(0, len(string), every)) - self._base_url = config["base_url"] - self._certificate = insert_newlines(config["certificate"]) + self._base_url = config['base_url'] + self._cert = insert_newlines(config["certificate"]) self._key = insert_newlines(config["key"]) self._appinfo = { @@ -379,8 +445,29 @@ def insert_newlines(string, every=64): "product_id": "soar", "device_id": self.get_product_installation_id(), "product_version": self.get_product_version(), + "perf_testing": True, } + if is_dev_env: + self._appinfo["perf_testing"] = True + + with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix="test") as temp_file: + cert = f"-----BEGIN CERTIFICATE-----\n{self._cert}\n-----END CERTIFICATE-----\n-----BEGIN RSA PRIVATE KEY-----\n{self._key}\n-----END RSA PRIVATE KEY-----\n" + + temp_file.write(cert) + temp_file.seek(0) # Move the file pointer to the beginning for reading + temp_file_path = temp_file.name # Get the name of the temporary file + # exceptions shouldn't really be thrown here because most network related disconnections will happen when a request is sent + try: + self.client = httpx.Client(http2=True, verify=config.get('verify_server_cert', False), cert=temp_file_path) + except Exception as e: + self.debug_print(f"Could not connect to server because of {e}") + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + return phantom.APP_ERROR + + if os.path.exists(temp_file_path): + os.remove(temp_file_path) return phantom.APP_SUCCESS def finalize(self): @@ -393,9 +480,9 @@ def main(): argparser = argparse.ArgumentParser() - argparser.add_argument("input_test_json", help="Input Test JSON file") - argparser.add_argument("-u", "--username", help="username", required=False) - argparser.add_argument("-p", "--password", help="password", required=False) + argparser.add_argument('input_test_json', help='Input Test JSON file') + argparser.add_argument('-u', '--username', help='username', required=False) + argparser.add_argument('-p', '--password', help='password', required=False) args = argparser.parse_args() session_id = None @@ -404,31 +491,31 @@ def main(): password = args.password if username is not None and password is None: + # User specified a username but not a password, so ask import getpass - password = getpass.getpass("Password: ") if username and password: try: - login_url = TalosIntelligenceConnector._get_phantom_base_url() + "/login" + login_url = TalosIntelligenceConnector._get_phantom_base_url() + '/login' print("Accessing the Login page") r = requests.get(login_url, verify=False) - csrftoken = r.cookies["csrftoken"] + csrftoken = r.cookies['csrftoken'] data = dict() - data["username"] = username - data["password"] = password - data["csrfmiddlewaretoken"] = csrftoken + data['username'] = username + data['password'] = password + data['csrfmiddlewaretoken'] = csrftoken headers = dict() - headers["Cookie"] = "csrftoken=" + csrftoken - headers["Referer"] = login_url + headers['Cookie'] = 'csrftoken=' + csrftoken + headers['Referer'] = login_url print("Logging into Platform to get the session id") r2 = requests.post(login_url, verify=False, data=data, headers=headers) - session_id = r2.cookies["sessionid"] + session_id = r2.cookies['sessionid'] except Exception as e: print("Unable to get session id from the platform. Error: " + str(e)) exit(1) @@ -442,8 +529,8 @@ def main(): connector.print_progress_message = True if session_id is not None: - in_json["user_session_token"] = session_id - connector._set_csrf_info(csrftoken, headers["Referer"]) + in_json['user_session_token'] = session_id + connector._set_csrf_info(csrftoken, headers['Referer']) ret_val = connector._handle_action(json.dumps(in_json), None) print(json.dumps(json.loads(ret_val), indent=4)) @@ -451,5 +538,5 @@ def main(): exit(0) -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/talosintelligence_consts.py b/talosintelligence_consts.py index ffc19d2..a14202b 100644 --- a/talosintelligence_consts.py +++ b/talosintelligence_consts.py @@ -1,6 +1,7 @@ # Define your constants here - ENDPOINT_QUERY_REPUTATION_V3 = "/Talos.Service.URS/QueryReputationV3" ENDPOINT_QUERY_AUP_CAT_MAP = "/Talos.Service.URS/QueryAUPCatMap" ENDPOINT_QUERY_TAXONOMIES = "/Talos.Service.TTS/QueryTaxonomyCatalogs" +MAX_CONNECTION_RETIRIES = 10 +MAX_REQUEST_RETRIES = 2