import demistomock as demisto from CommonServerPython import * from CommonServerUserPython import * ''' IMPORTS ''' import json import requests import urllib3 from requests.exceptions import MissingSchema, ConnectionError # Disable insecure warnings urllib3.disable_warnings() ''' GLOBALS/PARAMS ''' VENDOR_NAME = 'ThreatStream' HEADERS = { 'Content-Type': 'application/json' } CREDENTIALS: dict = {} DBOT_SCORE = { 'low': 2, 'medium': 2, 'high': 3, 'very-high': 3 } SEVERITY_SCORE = { 'low': 0, 'medium': 1, 'high': 2, 'very-high': 3 } DBOT_MAPPING = { 'value': 'Indicator', 'type': 'Type', 'source': 'Vendor', } DEFAULT_INDICATOR_MAPPING = { 'asn': 'ASN', 'value': 'Address', 'country': 'Country', 'type': 'Type', 'modified_ts': 'Modified', 'confidence': 'Confidence', 'status': 'Status', 'org': 'Organization', 'source': 'Source', 'tags': 'Tags', } INDICATOR_EXTENDED_MAPPING = { 'value': 'Value', 'id': 'ID', 'itype': 'IType', 'meta': 'Meta', 'confidence': 'Confidence', 'country': 'Country', 'org': 'Organization', 'asn': 'ASN', 'status': 'Status', 'tags': 'Tags', 'modified_ts': 'Modified', 'source': 'Source', 'type': 'Type', } THREAT_MODEL_MAPPING = { 'name': 'Name', 'id': 'ID', 'created_ts': 'CreatedTime', } ''' HELPER FUNCTIONS ''' class Client: def __init__(self, base_url, use_ssl, default_threshold, reliability): self.base_url = base_url self.use_ssl = use_ssl self.default_threshold = default_threshold self.reliability = reliability def http_request(self, method, url_suffix, params=None, data=None, headers=None, files=None, json=None, text_response=None): """ A wrapper for requests lib to send our requests and handle requests and responses better. """ res = requests.request( method, self.base_url + url_suffix, verify=self.use_ssl, params=params, data=data, headers=headers, files=files, json=json ) # Handle error responses gracefully if res.status_code in {401}: raise Exception(f"{VENDOR_NAME} - Got unauthorized from the server. Check the credentials.") elif res.status_code in {404}: command = demisto.command() if command in ['threatstream-get-model-description', 'threatstream-get-indicators-by-model', 'threatstream-get-analysis-status', 'threatstream-analysis-report']: # in order to prevent raising en error in case model/indicator/report was not found return {} else: raise Exception(f"{VENDOR_NAME} - The resource not found. Check the endpoint.") elif res.status_code not in {200, 201, 202}: raise Exception(F"{VENDOR_NAME} - Error in API call to ThreatStream {res.status_code} - {res.text}") if text_response: return res.text return res.json() def get_dbot_context(self, indicator, threshold, mapping=None): """ Builds and returns dictionary with Indicator, Type, Vendor and Score keys and values from the indicator that will be returned to context. """ if not mapping: mapping = DBOT_MAPPING dbot_context = {mapping[k]: v for (k, v) in indicator.items() if k in mapping.keys()} indicator_score = DBOT_SCORE[indicator.get('meta', {}).get('severity', 'low')] # the indicator will be considered as malicious in case it's score is greater or equal to threshold dbot_context['Score'] = 3 if indicator_score >= DBOT_SCORE[threshold] else indicator_score dbot_context['Vendor'] = VENDOR_NAME dbot_context['Reliability'] = self.reliability return dbot_context def find_worst_indicator(indicators): """ Sorts list of indicators by severity score and returns one indicator with the highest severity. In case the indicator has no severity value, the indicator severity score is set to 0 (low). """ indicators.sort(key=lambda ioc: SEVERITY_SCORE[ioc.get('meta', {}).get('severity', 'low')], reverse=True) return indicators[0] def prepare_args(args): # removing empty keys that can be passed from playbook input args = {k: v for (k, v) in args.items() if v} if 'include_inactive' in args: # special handling for ip, domain, file, url and threatstream-email-reputation commands args['status'] = "active,inactive" if args.pop('include_inactive') == 'True' else "active" if 'indicator_severity' in args: # special handling for threatstream-get-indicators args['meta.severity'] = args.pop('indicator_severity', None) if 'tags_name' in args: # special handling for threatstream-get-indicators args['tags.name'] = args.pop('tags_name', None) if 'indicator_value' in args: # special handling for threatstream-get-indicators args['value'] = args.pop('indicator_value', None) return args def build_params(**params): """ Builds query string from key word arguments and appends to it username and api key. """ params.update(CREDENTIALS) return params def mark_as_malicious(indicator, threshold, context): """ Marks indicator as malicious if severity of indicator is greater/equals to threshold and adds Malicious key to returned dictionary (context) in such case. """ severity = indicator.get('meta', {}).get('severity', 'low') if SEVERITY_SCORE[severity] >= SEVERITY_SCORE[threshold]: context['Malicious'] = { 'Vendor': 'ThreatStream' } def search_indicator_by_params(client: Client, params, searchable_value): """ Generic function that searches for indicators from ThreatStream by given query string. Returns indicator with the highest severity score. """ indicators_data = client.http_request("Get", "v2/intelligence/", params=params, headers=HEADERS) if not indicators_data['objects']: demisto.results(F"No intelligence has been found for {searchable_value}") return None return find_worst_indicator(indicators_data['objects']) def get_ip_context(indicator, threshold): """ Builds and returns dictionary that will be set to IP generic context. """ ip_context = {} ip_context['ASN'] = indicator.get('asn', '') ip_context['Address'] = indicator.get('value', '') ip_context['Geo'] = { 'Country': indicator.get('country', ''), 'Location': F"{indicator.get('latitude', '')},{indicator.get('longitude', '')}" } indicator_tags = indicator.get('tags', []) if indicator_tags: ip_context['Tags'] = [str(tag.get('name', '')) for tag in indicator_tags] mark_as_malicious(indicator, threshold, ip_context) return ip_context def get_domain_context(indicator, threshold): """ Builds and returns dictionary that will be set to Domain generic context. """ domain_context = {} whois_context = {} domain_context['Name'] = indicator.get('value', '') domain_context['DNS'] = indicator.get('ip', '') whois_context['CreationDate'] = indicator.get('created_ts', '') whois_context['UpdatedDate'] = indicator.get('modified_ts', '') meta = indicator.get('meta', None) if meta: registrant = {} registrant['Name'] = meta.get('registrant_name', '') registrant['Email'] = meta.get('registrant_email', '') registrant['Phone'] = meta.get('registrant_phone', '') whois_context['Registrant'] = registrant domain_context['WHOIS'] = whois_context indicator_tags = indicator.get('tags', []) if indicator_tags: domain_context['Tags'] = [str(tag.get('name', '')) for tag in indicator_tags] mark_as_malicious(indicator, threshold, domain_context) return domain_context def get_file_type(file_indicator): """ The function gets a file indicator data and returns it's subtype. """ indicator_type = file_indicator.get('subtype', '') return indicator_type def get_file_context(indicator, threshold): """ Builds and returns dictionary that will be set to File generic context. """ indicator_type = get_file_type(indicator) indicator_value = indicator.get('value', '') file_context = {} if indicator_type: file_context = {indicator_type: indicator_value} indicator_tags = indicator.get('tags', []) if indicator_tags: file_context['Tags'] = [str(tag.get('name', '')) for tag in indicator_tags] mark_as_malicious(indicator, threshold, file_context) return file_context def get_url_context(indicator, threshold): """ Builds and returns dictionary that will be set to URL generic context. """ url_context = {'Data': indicator.get('value', '')} indicator_tags = indicator.get('tags', []) if indicator_tags: url_context['Tags'] = [str(tag.get('name', '')) for tag in indicator_tags] mark_as_malicious(indicator, threshold, url_context) return url_context def get_threat_generic_context(indicator, indicator_mapping=None): """ Receives indicator and builds new dictionary from values that were defined in DEFAULT_INDICATOR_MAPPING keys and adds the Severity key with indicator severity value. """ # True when the indicator isn't a file (file indicator has a modified indicator_mapping). if not indicator_mapping: indicator_mapping = DEFAULT_INDICATOR_MAPPING threat_ip_context = {indicator_mapping[k]: v for (k, v) in indicator.items() if k in indicator_mapping.keys()} try: threat_ip_context['Severity'] = indicator['meta']['severity'] except KeyError: threat_ip_context['Severity'] = 'low' finally: return threat_ip_context def parse_network_elem(element_list, context_prefix): """ Parses the network elements list and returns a new dictionary. """ return list(map(lambda e: { F'{context_prefix}Source': e.get('src', ''), F'{context_prefix}Destination': e.get('dst', ''), F'{context_prefix}Port': e.get('dport', ''), }, element_list)) def parse_network_lists(network): """ Parses the network part that was received from sandbox report json. In each list, only sublist of 10 elements is taken. """ hosts = [{'Hosts': h} for h in network.get('hosts', [])[:10]] if 'packets' in network: network = network['packets'] udp_list = parse_network_elem(network.get('udp', [])[:10], 'Udp') icmp_list = parse_network_elem(network.get('icmp', [])[:10], 'Icmp') tcp_list = parse_network_elem(network.get('tcp', [])[:10], 'Tcp') http_list = parse_network_elem(network.get('http', [])[:10], 'Http') https_list = parse_network_elem(network.get('https', [])[:10], 'Https') network_result = udp_list + icmp_list + tcp_list + http_list + https_list + hosts return network_result def parse_info(info): """ Parses the info part that was received from sandbox report json """ info.update(info.pop('machine', {})) parsed_info = { 'Category': info.get('category', '').title(), 'Started': info.get('started', ''), 'Completed': info.get('ended', ''), 'Duration': info.get('duration', ''), 'VmName': info.get('name', ''), 'VmID': info.get('id', '') } return parsed_info def get_report_outputs(report, report_id): """ Returns human readable and entry context of the sandbox report """ info = parse_info(report.get('info', {})) info['ReportID'] = report_id _, info['Verdict'] = get_submission_status(report_id, False) network = parse_network_lists(report.get('network', {})) hm = tableToMarkdown(F"Report {report_id} analysis results", info) ec = { 'ThreatStream.Analysis': info, 'ThreatStream.Analysis.Network': network } return hm, ec def parse_indicators_list(iocs_list): """ Parses the indicator list and returns dictionary that will be set to context. """ for indicator in iocs_list: if indicator.get('type', '') == 'md5': indicator['type'] = indicator.get('subtype', '') iocs_context = list(map(lambda i: {INDICATOR_EXTENDED_MAPPING[k]: v for (k, v) in i.items() if k in INDICATOR_EXTENDED_MAPPING.keys()}, iocs_list)) for indicator in iocs_context: meta = indicator.pop('Meta', None) if meta: indicator['Severity'] = meta.get('severity', 'low') tags = indicator.get('Tags', []) if isinstance(tags, list): indicator['Tags'] = ",".join(list(map(lambda t: t.get('name', ''), tags))) return iocs_context def build_model_data(model, name, is_public, tlp, tags, intelligence, description): """ Builds data dictionary that is used in Threat Model creation/update request. """ if model == 'tipreport': description_field_name = 'body' else: description_field_name = 'description' data = {k: v for (k, v) in (('name', name), ('is_public', is_public), ('tlp', tlp), (description_field_name, description)) if v} if tags: data['tags'] = tags if isinstance(tags, list) else [t.strip() for t in tags.split(',')] if intelligence: data['intelligence'] = intelligence if isinstance(intelligence, list) else [i.strip() for i in intelligence.split(',')] return data def get_file_mapping(): """ Returns the file indicator mapping after changing it's type field to subtype. """ file_indicator_mapping = DEFAULT_INDICATOR_MAPPING.copy() # The real type of the hash is in subtype field. file_indicator_mapping.pop('type', '') file_indicator_mapping['subtype'] = 'Type' return file_indicator_mapping ''' COMMANDS + REQUESTS FUNCTIONS ''' def test_module(client: Client): """ Performs basic get request to get item samples """ params = build_params(limit=1) client.http_request('GET', 'v2/intelligence/', params=params) demisto.results('ok') def ips_reputation_command(client: Client, ip, threshold=None, status="active,inactive"): ips = argToList(ip, ',') for single_ip in ips: if is_ip_address_internal(single_ip): demisto.info(f"Skipping internal IP: {single_ip}") continue get_ip_reputation(client, single_ip, threshold, status) def get_ip_reputation(client: Client, ip, threshold=None, status="active,inactive"): """ Checks the reputation of given ip from ThreatStream and returns the indicator with highest severity score. """ params = build_params(value=ip, type="ip", status=status, limit=0) indicator = search_indicator_by_params(client, params, ip) if not indicator: return_results(create_indicator_result_with_dbotscore_unknown(indicator=ip, indicator_type=DBotScoreType.IP, reliability=client.reliability)) return threshold = threshold or client.default_threshold dbot_context = client.get_dbot_context(indicator, threshold) ip_context = get_ip_context(indicator, threshold) threat_ip_context = get_threat_generic_context(indicator) threat_ip_tags = threat_ip_context.pop('Tags', []) if threat_ip_tags: # Convert the tags objects into s string for the human readable and then override it with the original objects # for the context. threat_ip_context['Tags'] = ', '.join(str(tag.get('name', '')) for tag in threat_ip_tags) human_readable = tableToMarkdown(F"IP reputation for: {ip}", threat_ip_context) threat_ip_context['Tags'] = threat_ip_tags ec = { 'DBotScore': dbot_context, 'IP(val.Address == obj.Address)': ip_context, 'ThreatStream.IP(val.Address == obj.Address)': threat_ip_context } return_outputs(human_readable, ec, indicator) def domains_reputation_command(client: Client, domain, threshold=None, status="active,inactive"): """ Wrapper function for get_domain_reputation. """ domains = argToList(domain, ',') for single_domain in domains: get_domain_reputation(client, single_domain, threshold, status) def get_domain_reputation(client: Client, domain, threshold=None, status="active,inactive"): """ Checks the reputation of given domain from ThreatStream and returns the indicator with highest severity score. """ params = build_params(value=domain, type="domain", status=status, limit=0) indicator = search_indicator_by_params(client, params, domain) if not indicator: return_results(create_indicator_result_with_dbotscore_unknown(indicator=domain, indicator_type=DBotScoreType.DOMAIN, reliability=client.reliability)) return threshold = threshold or client.default_threshold dbot_context = client.get_dbot_context(indicator, threshold) domain_context = get_domain_context(indicator, threshold) threat_domain_context = get_threat_generic_context(indicator) threat_domain_tags = threat_domain_context.pop('Tags', []) if threat_domain_tags: # Convert the tags objects into s string for the human readable and then override it with the original objects # for the context. threat_domain_context['Tags'] = ', '.join(str(tag.get('name', '')) for tag in threat_domain_tags) human_readable = tableToMarkdown(F"Domain reputation for: {domain}", threat_domain_context) threat_domain_context['Tags'] = threat_domain_tags ec = { 'DBotScore': dbot_context, 'Domain(val.Name == obj.Name)': domain_context, 'ThreatStream.Domain(val.Address == obj.Address)': threat_domain_context } return_outputs(human_readable, ec, indicator) def files_reputation_command(client: Client, file, threshold=None, status="active,inactive"): """ Wrapper function for get_file_reputation. """ files = argToList(file, ',') for single_file in files: get_file_reputation(client, single_file, threshold, status) def get_file_reputation(client: Client, file, threshold=None, status="active,inactive"): """ Checks the reputation of given hash of the file from ThreatStream and returns the indicator with highest severity score. """ params = build_params(value=file, type="md5", status=status, limit=0) indicator = search_indicator_by_params(client, params, file) if not indicator: return_results(create_indicator_result_with_dbotscore_unknown(indicator=file, indicator_type=DBotScoreType.FILE, reliability=client.reliability)) return threshold = threshold or client.default_threshold file_dbot_mapping = { 'value': 'Indicator', 'subtype': 'Type', 'source': 'Vendor', } dbot_context = client.get_dbot_context(indicator, threshold, file_dbot_mapping) file_type = get_file_type(indicator) file_context = get_file_context(indicator, threshold) file_indicator_mapping = get_file_mapping() threat_file_context = get_threat_generic_context(indicator, file_indicator_mapping) threat_file_context[file_type] = threat_file_context.pop('Address') threat_file_context.pop("ASN", None) threat_file_context.pop("Organization", None) threat_file_context.pop("Country", None) threat_file_tags = threat_file_context.pop('Tags', []) if threat_file_tags: # Convert the tags objects into s string for the human readable and then override it with the original objects # for the context. threat_file_context['Tags'] = ', '.join(str(tag.get('name', '')) for tag in threat_file_tags) human_readable = tableToMarkdown(F"{file_type} reputation for: {file}", threat_file_context) threat_file_context['Tags'] = threat_file_tags ec = { 'DBotScore': dbot_context, Common.File.CONTEXT_PATH: file_context, f'ThreatStream.{Common.File.CONTEXT_PATH}': threat_file_context } return_outputs(human_readable, ec, indicator) def urls_reputation_command(client: Client, url, threshold=None, status="active,inactive"): """ Wrapper function for get_url_reputation. """ urls = argToList(url, ',') for single_url in urls: get_url_reputation(client, single_url, threshold, status) def get_url_reputation(client: Client, url, threshold=None, status="active,inactive"): """ Checks the reputation of given url address from ThreatStream and returns the indicator with highest severity score. """ params = build_params(value=url, type="url", status=status, limit=0) indicator = search_indicator_by_params(client, params, url) if not indicator: return_results(create_indicator_result_with_dbotscore_unknown(indicator=url, indicator_type=DBotScoreType.URL, reliability=client.reliability)) return threshold = threshold or client.default_threshold dbot_context = client.get_dbot_context(indicator, threshold) domain_context = get_url_context(indicator, threshold) threat_url_context = get_threat_generic_context(indicator) del threat_url_context['ASN'] threat_url_tags = threat_url_context.pop('Tags', []) if threat_url_tags: # Convert the tags objects into s string for the human readable and then override it with the original objects # for the context. threat_url_context['Tags'] = ', '.join(str(tag.get('name', '')) for tag in threat_url_tags) human_readable = tableToMarkdown(F"URL reputation for: {url}", threat_url_context) threat_url_context['Tags'] = threat_url_tags ec = { 'DBotScore': dbot_context, 'URL(val.Data == obj.Data)': domain_context, 'ThreatStream.URL(val.Address == obj.Address)': threat_url_context } return_outputs(human_readable, ec, indicator) def get_email_reputation(client: Client, email, threshold=None, status="active,inactive"): """ Checks the reputation of given email address from ThreatStream and returns the indicator with highest severity score. """ params = build_params(value=email, type="email", status=status, limit=0) indicator = search_indicator_by_params(client, params, email) if not indicator: return threshold = threshold or client.default_threshold dbot_context = client.get_dbot_context(indicator, threshold) threat_email_context = get_threat_generic_context(indicator) threat_email_context['Email'] = threat_email_context.pop('Address') threat_email_context.pop("ASN", None) threat_email_context.pop("Organization", None) threat_email_context.pop("Country", None) threat_email_tags = threat_email_context.pop('Tags', []) if threat_email_tags: # Convert the tags objects into s string for the human readable and then override it with the original objects # for the context. threat_email_context['Tags'] = ', '.join(str(tag.get('name', '')) for tag in threat_email_tags) human_readable = tableToMarkdown(F"Email reputation for: {email}", threat_email_context) threat_email_context['Tags'] = threat_email_tags ec = { 'DBotScore': dbot_context, 'ThreatStream.EmailReputation(val.Email == obj.Email)': threat_email_context } return_outputs(human_readable, ec, indicator) def get_passive_dns(client: Client, value, type="ip", limit=50): """ Receives value and type of indicator and returns enrichment data for domain or ip. """ dns_results = client.http_request("GET", F"v1/pdns/{type}/{value}/", params=CREDENTIALS).get('results', None) if not dns_results: demisto.results(F"No Passive DNS enrichment data found for {value}") sys.exit() dns_results = dns_results[:int(limit)] output = camelize(dns_results, delim='_') ec = ({ 'ThreatStream.PassiveDNS': output }) human_readable = tableToMarkdown(F"Passive DNS enrichment data for: {value}", output) return_outputs(human_readable, ec, dns_results) def import_ioc_with_approval(client: Client, import_type, import_value, confidence="50", classification="Private", threat_type="exploit", severity="low", ip_mapping=None, domain_mapping=None, url_mapping=None, email_mapping=None, md5_mapping=None): """ Imports indicators data to ThreatStream. The data can be imported using one of three import_types: data-text (plain-text), file-id of uploaded file to war room or URL. """ files = None uploaded_file = None data = assign_params( classification=classification, confidence=int(confidence), ip_mapping=ip_mapping, domain_mapping=domain_mapping, url_mapping=url_mapping, email_mapping=email_mapping, md5_mapping=md5_mapping, threat_type=threat_type, severity=severity, ) if import_type == 'file-id': try: # import_value will be entry id of uploaded file to war room file_info = demisto.getFilePath(import_value) except Exception: raise DemistoException(f"Entry {import_value} does not contain a file.") uploaded_file = open(file_info['path'], 'rb') files = {'file': (file_info['name'], uploaded_file)} elif import_type == 'url': data['url'] = import_value else: data['datatext'] = import_value params = build_params() # in case import_type is not file-id, http_requests will receive None as files res = client.http_request("POST", "v1/intelligence/import/", params=params, data=data, files=files) # closing the opened file if exist if uploaded_file: uploaded_file.close() # checking that response contains success key if res.get('success', False): imported_id = res.get('import_session_id', '') ec = {'ThreatStream.Import.ImportID': imported_id} return_outputs(F"The data was imported successfully. The ID of imported job is: {imported_id}", ec, res) else: return_outputs("The data was not imported. Check if valid arguments were passed", None) def import_ioc_without_approval(client: Client, file_id, classification, confidence=None, allow_unresolved=None, source_confidence_weight=None, expiration_ts=None, severity=None, tags=None, trustedcircles=None): """ Imports indicators data to ThreatStream. file_id of uploaded file to war room or URL. Other fields are """ if allow_unresolved: allow_unresolved = allow_unresolved == 'yes' if tags: tags = argToList(tags) if trustedcircles: trustedcircles = argToList(trustedcircles) try: # entry id of uploaded file to war room file_info = demisto.getFilePath(file_id) with open(file_info['path'], 'rb') as uploaded_file: ioc_to_import = json.load(uploaded_file) except json.JSONDecodeError: raise DemistoException(F"Entry {file_id} does not contain a valid json file.") except Exception: raise DemistoException(F"Entry {file_id} does not contain a file.") ioc_to_import.update({'meta': assign_params( classification=classification, confidence=confidence, allow_unresolved=allow_unresolved, source_confidence_weight=source_confidence_weight, expiration_ts=expiration_ts, severity=severity, tags=tags, trustedcircles=trustedcircles )}) params = build_params() res = client.http_request("PATCH", "v1/intelligence/", params=params, json=ioc_to_import, text_response=True) return_outputs("The data was imported successfully.", {}, res) def get_model_list(client: Client, model, limit="50"): """ Returns list of Threat Model that was specified. By default limit is set to 50 results. Possible values for model are : actor, campaign, incident, signature, ttp, vulnerability, tipreport """ # if limit=0 don't put to context params = build_params(limit=limit, skip_intelligence="true", skip_associations="true") model_list = client.http_request("GET", F"v1/{model}/", params=params).get('objects', None) if not model_list: demisto.results(F"No Threat Model {model.title()} found.") sys.exit() threat_list_context = list(map(lambda m: {THREAT_MODEL_MAPPING[k]: v for (k, v) in m.items() if k in THREAT_MODEL_MAPPING.keys()}, model_list)) for m in threat_list_context: m['Type'] = model.title() # in case that no limit was passed, the stage of set to context is skipped ec = {'ThreatStream.List': threat_list_context} if limit != '0' else None human_readable = tableToMarkdown(F"List of {model.title()}s", threat_list_context) return_outputs(human_readable, ec, model_list) def get_model_description(client: Client, model, id): """ Returns a description of Threat Model as html file to the war room. """ params = build_params(skip_intelligence="true", skip_associations="true") description = client.http_request("GET", F"v1/{model}/{id}", params=params) if model == 'signature': description = description.get('notes', None) elif model == 'tipreport': description = description.get('body', None) else: description = description.get('description', None) if not description: demisto.results(F"No description found for Threat Model {model.title()} with id {id}") sys.exit() demisto.results(fileResult(F"{model}_{id}.html", description.encode(encoding='UTF-8'))) def get_iocs_by_model(client: Client, model, id, limit="20"): """ Returns list of indicators associated with specific Threat Model by model id. """ params = build_params(limit=limit) iocs_list = client.http_request("GET", F"v1/{model}/{id}/intelligence/", params=params).get('objects', None) if not iocs_list: demisto.results(F"No indicators found for Threat Model {model.title()} with id {id}") sys.exit() iocs_context = parse_indicators_list(iocs_list) ec = { 'ThreatStream.Model(val.ModelID == obj.ModelID && val.ModelType == obj.ModelType)': {'ModelType': model.title(), 'ModelID': id, 'Indicators': iocs_context}, } human_readable = tableToMarkdown(F"Indicators list for Threat Model {model.title()} with id {id}", iocs_context) return_outputs(human_readable, ec, iocs_list) def create_model(client: Client, model, name, is_public="false", tlp=None, tags=None, intelligence=None, description=None): """ Creates Threat Model with basic parameters. """ data = build_model_data(model, name, is_public, tlp, tags, intelligence, description) model_id = client.http_request("POST", F"v1/{model}/", data=json.dumps(data), params=CREDENTIALS).get('id', None) if model_id: get_iocs_by_model(client, model, model_id, limit="50") else: demisto.results(F"{model.title()} Threat Model was not created. Check the input parameters") def update_model(client: Client, model, model_id, name=None, is_public="false", tlp=None, tags=None, intelligence=None, description=None): """ Updates a ThreatStream model with parameters. In case one or more optional parameters are defined, the previous data is overridden. """ data = build_model_data(model, name, is_public, tlp, tags, intelligence, description) client.http_request("PATCH", F"v1/{model}/{model_id}/", data=json.dumps(data), params=CREDENTIALS) get_iocs_by_model(client, model, model_id, limit="50") def supported_platforms(client: Client, sandbox_type="default"): """ Returns list of supported platforms for premium sandbox or default sandbox. """ platform_data = client.http_request("GET", "v1/submit/parameters/", params=CREDENTIALS) available_platforms = platform_data.get('platform_choices', []) if sandbox_type == 'default' else platform_data.get( 'premium_platform_choices', []) if available_platforms: supported_output = camelize(available_platforms) context_path = "ThreatStream.DefaultPlatforms" if sandbox_type == 'default' else "ThreatStream.PremiumPlatforms" return_outputs(tableToMarkdown(F"Supported platforms for {sandbox_type} sandbox", supported_output), {context_path: supported_output}, platform_data) else: demisto.results(F"No supported platforms found for {sandbox_type} sandbox") def get_submission_status(client: Client, report_id, output=True): """ Returns the sandbox submission status. If status is not received in report_info then status is set to done. Receives output boolean that prints the result to the war room. By default the output boolean is set to True. """ report_info = client.http_request("GET", F"v1/submit/{report_id}/", params=CREDENTIALS, headers=HEADERS) if not report_info: demisto.results(F"No report found with id {report_id}") sys.exit() status = report_info.get('status', "done") verdict = report_info.get('verdict', '').title() platform = report_info.get('platform', '') if output: report_outputs = {'ReportID': report_id, 'Status': status, 'Platform': platform, 'Verdict': verdict} ec = {'ThreatStream.Analysis(val.ReportID == obj.ReportID)': report_outputs} return_outputs(tableToMarkdown(F"The analysis status for id {report_id}", report_outputs), ec, report_info) return status, verdict def file_name_to_valid_string(file_name): try: # In case the user uses Demisto version < 5.0 and the new docker image will not be automatically changed import emoji if emoji.emoji_count(file_name): # type: ignore return emoji.demojize(file_name) # type: ignore except Exception: pass return file_name def submit_report(client: Client, submission_type, submission_value, submission_classification="private", report_platform="WINDOWS7", premium_sandbox="false", detail=None): """ Detonates URL or file that was uploaded to war room to ThreatStream sandbox. """ uploaded_file = None files = None data = { 'report_radio-classification': submission_classification, 'report_radio-platform': report_platform, 'use_premium_sandbox': premium_sandbox, } if detail: data['detail'] = detail if submission_type == 'file': try: # submission_value will be entry id of uploaded file to war room file_info = demisto.getFilePath(submission_value) except Exception: raise Exception(F"{VENDOR_NAME} - Entry {submission_value} does not contain a file.") uploaded_file = open(file_info['path'], 'rb') file_name = file_name_to_valid_string(file_info.get('name')) files = {'report_radio-file': (file_name, uploaded_file)} else: data['report_radio-url'] = submission_value res = client.http_request("POST", "v1/submit/new/", params=CREDENTIALS, data=data, files=files) # closing the opened file if exist if uploaded_file: uploaded_file.close() if str(res.get('success', '')).lower() == 'true': report_info = res['reports'][report_platform] report_id = report_info['id'] report_status, _ = get_submission_status(client, report_id, False) report_outputs = {'ReportID': report_id, 'Status': report_status, 'Platform': report_platform} ec = {'ThreatStream.Analysis': report_outputs} return_outputs(tableToMarkdown(F"The submission info for {submission_value}", report_outputs), ec, report_info) else: demisto.results(F"The submission of {submission_value} failed") def get_report(client: Client, report_id): """ Returns the report from ThreatStream sandbox by id. """ report = client.http_request("GET", F"v1/submit/{report_id}/report", params=CREDENTIALS, headers=HEADERS) if not report: demisto.results(F"No report found with id {report_id}") sys.exit() hm, ec = get_report_outputs(report.get('results', {}), report_id) return_outputs(hm, ec, report) def add_tag_to_model(client: Client, model_id, tags, model="intelligence"): """ Adds tag to specific Threat Model. By default is set to intelligence (indicators). """ tags = tags if isinstance(tags, list) else tags.split(',') data = { 'tags': [{'name': t, 'tlp': 'red'} for t in tags] } res = client.http_request("POST", F"v1/{model}/{model_id}/tag/", params=CREDENTIALS, data=json.dumps(data)) if str(res.get('success', '')).lower() == 'true': return_outputs(F"Added successfully tags: {tags} to {model} with {model_id}", None, res) else: return_outputs(F"Failed to add {tags} to {model} with {model_id}", None, res) def get_indicators(client: Client, **kwargs): """ Returns filtered indicators by parameters from ThreatStream. By default the limit of indicators result is set to 20. """ limit = kwargs['limit'] = int(kwargs.get('limit', 20)) offset = kwargs['offset'] = 0 if 'query' in kwargs: kwargs['q'] = kwargs['query'] kwargs.pop('query', None) params = build_params(**kwargs) iocs_list = client.http_request("GET", "v2/intelligence/", params=params).get('objects', None) if not iocs_list: demisto.results('No indicators found from ThreatStream') sys.exit() iocs_context = parse_indicators_list(iocs_list) # handle the issue that the API does not return more than 1000 indicators. if limit > 1000: while len(iocs_context) < limit: offset += len(iocs_list) kwargs['limit'] = limit kwargs['offset'] = offset params = build_params(**kwargs) iocs_list = client.http_request("GET", "v2/intelligence/", params=params).get('objects', None) if iocs_list: iocs_context.extend(parse_indicators_list(iocs_list)) else: break ec = {'ThreatStream.Indicators': iocs_context} return_outputs(tableToMarkdown("The indicators results", iocs_context), ec, iocs_list) def main(): """ Initiate integration command """ command = demisto.command() LOG(f'Command being called is {command}') params = demisto.params() user_name = params.get('credentials', {}).get('identifier', '') or params.get('username', '') api_key = params.get('credentials', {}).get('password', '') or params.get('apikey', '') server_url = params.get('url', '').strip('/') CREDENTIALS['username'] = user_name CREDENTIALS['api_key'] = api_key reliability = params.get('integrationReliability') reliability = reliability if reliability else DBotScoreReliability.B if DBotScoreReliability.is_valid_type(reliability): reliability = DBotScoreReliability.get_dbot_score_reliability_from_str(reliability) else: Exception("Please provide a valid value for the Source Reliability parameter.") try: client = Client( base_url=f'{server_url}/api/', use_ssl=not params.get('insecure', False), default_threshold=params.get('default_threshold', 'high'), reliability=reliability ) handle_proxy() args = prepare_args(demisto.args()) if command == 'test-module': test_module(client) elif command == 'ip': ips_reputation_command(client, **args) elif command == 'domain': domains_reputation_command(client, **args) elif command == 'file': files_reputation_command(client, **args) elif command == 'url': urls_reputation_command(client, **args) elif command == 'threatstream-email-reputation': get_email_reputation(client, **args) elif command == 'threatstream-get-passive-dns': get_passive_dns(client, **args) elif command == 'threatstream-import-indicator-with-approval': import_ioc_with_approval(client, **args) elif command == 'threatstream-import-indicator-without-approval': import_ioc_without_approval(client, **args) elif command == 'threatstream-get-model-list': get_model_list(client, **args) elif command == 'threatstream-get-model-description': get_model_description(client, **args) elif command == 'threatstream-get-indicators-by-model': get_iocs_by_model(client, **args) elif command == 'threatstream-create-model': create_model(client, **args) elif command == 'threatstream-update-model': update_model(client, **args) elif command == 'threatstream-submit-to-sandbox': submit_report(client, **args) elif command == 'threatstream-get-analysis-status': get_submission_status(client, **args) elif command == 'threatstream-analysis-report': get_report(client, **args) elif command == 'threatstream-supported-platforms': supported_platforms(client, **args) elif command == 'threatstream-get-indicators': get_indicators(client, **args) elif command == 'threatstream-add-tag-to-model': add_tag_to_model(client, **args) except Exception as err: if isinstance(err, MissingSchema): return_error("Not valid server url. Check url format") elif isinstance(err, ConnectionError): return_error("The server is not reachable.") else: return_error(err) # python2 uses __builtin__ python3 uses builtins if __name__ in ("builtins", "__builtin__", "__main__"): main()