diff --git a/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists.py b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists.py new file mode 100644 index 000000000000..f535dc561bf3 --- /dev/null +++ b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists.py @@ -0,0 +1,491 @@ +import demistomock as demisto # noqa: F401 +from CommonServerPython import * # noqa: F401 + + +FEED_STR = { + 'cryptominer': 'Cryptominer', + 'first-stage-delivery-vectors': 'First stage delivery vectors', + 'infostealer': 'Infostealer', + 'iot': 'IoT', + 'linux': 'Linux', + 'malicious-network-infrastructure': 'Malicious network infrastructure', + 'malware': 'Malware', + 'mobile': 'Mobile', + 'osx': 'OSX', + 'phishing': 'Phishing', + 'ransomware': 'Ransomware', + 'threat-actor': 'Threat actor', + 'trending': 'Trending', + 'vulnerability-weaponization': 'Vulnerability weaponization', +} + + +def _get_current_package(): + """Gets current package for Threat Lists.""" + time_obj = datetime.utcnow() - timedelta(hours=2) + package = time_obj.strftime('%Y%m%d%H') + return package + + +class DetectionRatio: + """Class for detections.""" + malicious = 0 + total = 0 + + def __init__(self, last_analysis_stats: dict): + self.malicious = last_analysis_stats.get('malicious', 0) + self.total = sum(last_analysis_stats.values()) + + def __repr__(self): + return f'{self.malicious}/{self.total}' + + +class Client(BaseClient): + """Client for Google Threat Intelligence API.""" + + def get_threat_list(self, + feed_type: str, + package: str, + filter_query: str = None, + limit: int = 10) -> dict: + """Get indicators from GTI API.""" + return self._http_request( + 'GET', + f'threat_lists/{feed_type}/{package}', + params=assign_params( + query=filter_query, + limit=limit, + ) + ) + + def fetch_indicators(self, + feed_type: str, + package: str = None, + filter_query: str = None, + limit: int = 10, + fetch_command: bool = False) -> list: + """Retrieves matches for a given feed type.""" + package = package or _get_current_package() + filter_query = filter_query or '' + + if fetch_command: + if self.get_last_run() == package: + return [] + + response = self.get_threat_list(feed_type, package, filter_query.strip(), limit) + + if fetch_command: + self.set_last_run(package) + + return response.get('iocs', []) + + @staticmethod + def set_last_run(package: str = None): + """Sets last threat feed.""" + current_package = package or _get_current_package() + demisto.setLastRun({'last_threat_feed': current_package}) + + @staticmethod + def get_last_run() -> str: + """Gets last threat feed, or '' if no last run.""" + return (demisto.getLastRun() or {}).get('last_threat_feed', '') + + +def test_module(client: Client, args: dict) -> str: + """Tests module.""" + try: + client.fetch_indicators('malware') + except Exception: + raise Exception("Could not fetch Google Threat Intelligence IoC Threat Lists\n" + "\nCheck your API key and your connection to Google Threat Intelligence.") + return 'ok' + + +def _gti_verdict_to_dbot_score(gti_verdict: str): + """Parses GTI verdict to DBotScore.""" + return { + 'VERDICT_BENIGN': 1, + 'VERDICT_SUSPICIOUS': 2, + 'VERDICT_MALICIOUS': 3, + }.get(gti_verdict, 0) + + +def _add_gti_attributes(indicator_obj: dict, item: dict): + """Addes GTI attributes.""" + + # GTI assessment + attributes = item.get('attributes', {}) + gti_assessment = attributes.get('gti_assessment', {}) + gti_threat_score = gti_assessment.get('threat_score', {}).get('value') + gti_severity = gti_assessment.get('severity', {}).get('value') + gti_verdict = gti_assessment.get('verdict', {}).get('value') + + # Relationships + relationships = item.get('relationships', {}) + malware_families: list[str] = [ + x.get('attributes', {}).get('name') + for x in relationships.get('malware_families', {}).get('data', []) + ] + malware_families = list(set(malware_families)) + threat_actors: list[str] = [ + x.get('attributes', {}).get('name') + for x in relationships.get('threat_actors', {}).get('data', []) + ] + threat_actors = list(set(threat_actors)) + + indicator_obj['fields'].update({ + 'gtithreatscore': gti_threat_score, + 'gtiseverity': gti_severity, + 'gtiverdict': gti_verdict, + 'malwarefamily': malware_families, + 'actor': threat_actors, + }) + indicator_obj.update({ + 'score': _gti_verdict_to_dbot_score(gti_verdict), + 'gti_threat_score': gti_threat_score, + 'gti_severity': gti_severity, + 'gti_verdict': gti_verdict, + 'malware_families': malware_families, + 'threat_actors': threat_actors, + 'relationships': [ + EntityRelationship( + name=EntityRelationship.Relationships.PART_OF, + entity_a=indicator_obj['value'], + entity_a_type=indicator_obj['type'], + entity_b=malware_family.title(), + entity_b_type=ThreatIntel.ObjectsNames.MALWARE, + reverse_name=EntityRelationship.Relationships.CONTAINS, + ).to_indicator() for malware_family in malware_families + ] + [ + EntityRelationship( + name=EntityRelationship.Relationships.ATTRIBUTED_BY, + entity_a=indicator_obj['value'], + entity_a_type=indicator_obj['type'], + entity_b=threat_actor.title(), + entity_b_type=ThreatIntel.ObjectsNames.THREAT_ACTOR, + reverse_name=EntityRelationship.Relationships.ATTRIBUTED_TO, + ).to_indicator() for threat_actor in threat_actors + ], + }) + + return indicator_obj + + +def _get_indicator_type(item: dict): + """Gets indicator type.""" + if item.get('type') == 'file': + return FeedIndicatorType.File + if item.get('type') == 'domain': + return FeedIndicatorType.Domain + if item.get('type') == 'url': + return FeedIndicatorType.URL + if item.get('type') == 'ip_address': + return FeedIndicatorType.IP + raise ValueError(f'Unknown type: {item.get("type")}. ID: {item.get("id")}') + + +def _get_indicator_id(item: dict): + """Gets indicator ID.""" + if item.get('type') == 'url': + return item.get('attributes', {}).get('url') or item.get('id') + return item.get('id') + + +def _add_file_attributes(indicator_obj: dict, attributes: dict) -> dict: + """Adds file attributes.""" + indicator_obj['fields'].update({ + 'md5': attributes.get('md5'), + 'sha1': attributes.get('sha1'), + 'sha256': attributes.get('sha256'), + 'ssdeep': attributes.get('ssdeep'), + 'fileextension': attributes.get('type_extension'), + 'filetype': attributes.get('type_tag'), + 'imphash': attributes.get('pe_info', {}).get('imphash'), + 'displayname': attributes.get('meaningful_name'), + 'name': attributes.get('meaningful_name'), + 'size': attributes.get('size'), + 'creationdate': attributes.get('creation_date'), + 'firstseenbysource': attributes.get('first_submission_date'), + 'lastseenbysource': attributes.get('last_submission_date'), + }) + + return indicator_obj + + +def _add_domain_attributes(indicator_obj: dict, attributes: dict) -> dict: + """Adds domain attributes.""" + whois: str = attributes.get('whois', '') + + admin_country = re.search(r'Admin Country:\s*([^\n]+)', whois) + admin_email = re.search(r'Admin Email:\s*([^\n]+)', whois) + admin_name = re.search(r'Admin Name:\s*([^\n]+)', whois) + admin_phone = re.search(r'Admin Phone:\s*([^\n]+)', whois) + + registrant_country = re.search(r'Registrant Country:\s*([^\n]+)', whois) + registrant_email = re.search(r'Registrant Email:\s*([^\n]+)', whois) + registrant_name = re.search(r'Registrant Name:\s*([^\n]+)', whois) + registrant_phone = re.search(r'Registrant Phone:\s*([^\n]+)', whois) + + registrar_abuse_email = re.search(r'Registrar Abuse Contact Email:\s*([^\n]+)', whois) + registrar_abuse_phone = re.search(r'Registrar Abuse Contact Phone:\s*([^\n]+)', whois) + + indicator_obj['fields'].update({ + 'creationdate': attributes.get('creation_date'), + 'admincountry': admin_country.group(1) if admin_country else None, + 'adminemail': admin_email.group(1) if admin_email else None, + 'adminname': admin_name.group(1) if admin_name else None, + 'adminphone': admin_phone.group(1) if admin_phone else None, + 'registrantcountry': registrant_country.group(1) if registrant_country else None, + 'registrantemail': registrant_email.group(1) if registrant_email else None, + 'registrantname': registrant_name.group(1) if registrant_name else None, + 'registrantphone': registrant_phone.group(1) if registrant_phone else None, + 'registrarabuseemail': registrar_abuse_email.group(1) if registrar_abuse_email else None, + 'registrarabusephone': registrar_abuse_phone.group(1) if registrar_abuse_phone else None, + 'registrarname': attributes.get('registrar'), + 'firstseenbysource': attributes.get('first_seen_itw_date'), + 'lastseenbysource': attributes.get('last_seen_itw_date'), + }) + + return indicator_obj + + +def _add_url_attributes(indicator_obj: dict, attributes: dict) -> dict: + """Adds URL attributes.""" + indicator_obj['fields'].update({ + 'firstseenbysource': attributes.get('first_submission_date'), + 'lastseenbysource': attributes.get('last_submission_date'), + }) + + return indicator_obj + + +def _add_ip_attributes(indicator_obj: dict, attributes: dict) -> dict: + """Adds IP attributes.""" + indicator_obj['fields'].update({ + 'countrycode': attributes.get('country'), + 'firstseenbysource': attributes.get('first_seen_itw_date'), + 'lastseenbysource': attributes.get('last_seen_itw_date'), + }) + + return indicator_obj + + +def _add_dedicated_attributes(indicator_obj: dict, attributes: dict) -> dict: + """Adds dedicated attributes to indicator object.""" + if indicator_obj['type'] == FeedIndicatorType.File: + return _add_file_attributes(indicator_obj, attributes) + if indicator_obj['type'] == FeedIndicatorType.Domain: + return _add_domain_attributes(indicator_obj, attributes) + if indicator_obj['type'] == FeedIndicatorType.URL: + return _add_url_attributes(indicator_obj, attributes) + if indicator_obj['type'] == FeedIndicatorType.IP: + return _add_ip_attributes(indicator_obj, attributes) + raise ValueError(f'Unknown type: {indicator_obj["type"]}. ID: {indicator_obj["id"]}') + + +def _create_indicator(item: dict) -> dict: + """Creates indicator object.""" + indicator_type = _get_indicator_type(item) + indicator_id = _get_indicator_id(item) + + attributes: dict = item.get('attributes', {}) + + detection_ratio = DetectionRatio(attributes.get('last_analysis_stats', {})) + + indicator_obj = { + 'type': indicator_type, + 'value': indicator_id, + 'service': 'Google Threat Intelligence', + 'fields': { + 'tags': attributes.get('tags') or None, + 'updateddate': attributes.get('last_modification_date'), + 'detectionengines': detection_ratio.total, + 'positivedetections': detection_ratio.malicious, + }, + 'rawJSON': { + 'type': indicator_type, + 'value': indicator_id, + 'attributes': attributes, + 'relationships': item.get('relationships', {}), + }, + 'id': indicator_id, + 'detections': str(detection_ratio), + } + + indicator_obj = _add_gti_attributes(indicator_obj, item) + indicator_obj = _add_dedicated_attributes(indicator_obj, attributes) + + return indicator_obj + + +def fetch_indicators_command(client: Client, + feed_type: str, + package: str = None, + filter_query: str = None, + limit: int = 10, + tlp_color: str = None, + feed_tags: list = None, + fetch_command: bool = False) -> list[dict]: + """Retrieves indicators from the feed. + Args: + client (Client): Client object with request + feed_type (str): Feed type + package (string): Package in '%Y%m%d%H' format + filter_query (string): filter query + limit (int): Limit the results + tlp_color (str): Traffic Light Protocol color + feed_tags (list): Tags to assign fetched indicators + fetch_command (bool): Whether command is used as fetch command. + Returns: + Indicators. + """ + indicators = [] + + raw_indicators = client.fetch_indicators(feed_type, + package=package, + filter_query=filter_query, + limit=limit, + fetch_command=fetch_command) + + # extract values from iterator + for item in raw_indicators: + try: + indicator_obj = _create_indicator(item.get('data', {})) + except ValueError as exc: + demisto.info(str(exc)) + continue + + if feed_tags: + indicator_obj['fields']['tags'] = feed_tags + + if tlp_color: + indicator_obj['fields']['trafficlightprotocol'] = tlp_color + + indicators.append(indicator_obj) + + return indicators + + +def get_indicators_command(client: Client, + params: dict[str, str], + args: dict[str, str]) -> CommandResults: + """Wrapper for retrieving indicators from the feed to the war-room. + Args: + client: Client object with request + params: demisto.params() + args: demisto.args() + Returns: + Outputs. + """ + tlp_color = params.get('tlp_color') + feed_tags = argToList(params.get('feedTags', '')) + feed_type = args.get('feed_type', 'malware') + package = args.get('package') + filter_query = args.get('filter') + limit = int(args.get('limit', 10)) + + indicators = fetch_indicators_command( + client, + feed_type, + package=package, + filter_query=filter_query, + limit=limit, + tlp_color=tlp_color, + feed_tags=feed_tags, + ) + + human_readable = tableToMarkdown( + f'Indicators from Google Threat Intelligence {FEED_STR.get(feed_type, feed_type)} Threat List:', + indicators, + headers=[ + 'id', + 'detections', + 'gti_threat_score', + 'gti_severity', + 'gti_verdict', + 'malware_families', + 'threat_actors', + ], + headerTransform=string_to_table_header, + removeNull=True, + ) + + return CommandResults( + readable_output=human_readable, + outputs_prefix='', + outputs_key_field='', + raw_response=indicators, + outputs={}, + ) + + +def main(): + """main function, parses params and runs command functions.""" + params = demisto.params() + + feed_type = params.get('feed_type', 'malware') + filter_query = params.get('filter') + limit = int(params.get('limit', 10)) + tlp_color = params.get('tlp_color') + feed_tags = argToList(params.get('feedTags', '')) + + # If your Client class inherits from BaseClient, SSL verification is + # handled out of the box by it, just pass ``verify_certificate`` to + # the Client constructor + secure = not params.get('insecure', False) + + # If your Client class inherits from BaseClient, system proxy is handled + # out of the box by it, just pass ``proxy`` to the Client constructor + proxy = params.get('proxy', False) + + command = demisto.command() + + demisto.debug(f'Command being called is {command}') + + try: + client = Client( + base_url='https://www.virustotal.com/api/v3/', + verify=secure, + proxy=proxy, + headers={ + 'x-apikey': params.get('credentials', {}).get('password'), + 'x-tool': 'CortexGTIFeeds', + } + ) + + if command == 'test-module': + # This is the call made when pressing the integration Test button. + return_results(test_module(client, {})) + + elif command == 'gti-threatlists-get-indicators': + # This is the command that fetches a limited number of indicators + # from the feed source and displays them in the war room. + return_results(get_indicators_command(client, params, demisto.args())) + + elif command == 'fetch-indicators': + # This is the command that initiates a request to the feed endpoint + # and create new indicators objects from the data fetched. If the + # integration instance is configured to fetch indicators, then this + # is the command that will be executed at the specified feed fetch + # interval. + indicators = fetch_indicators_command(client, + feed_type, + filter_query=filter_query, + limit=limit, + tlp_color=tlp_color, + feed_tags=feed_tags, + fetch_command=True) + for iter_ in batch(indicators, batch_size=2000): + demisto.createIndicators(iter_) + + else: + raise NotImplementedError(f'Command {command} is not implemented.') + + # Log exceptions and return errors + except Exception as e: + demisto.error(traceback.format_exc()) # Print the traceback + return_error(f'Failed to execute {command} command.\nError:\n{str(e)}') + + +if __name__ in ['__main__', 'builtin', 'builtins']: + main() diff --git a/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists.yml b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists.yml new file mode 100644 index 000000000000..618057ad5b9a --- /dev/null +++ b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists.yml @@ -0,0 +1,157 @@ +category: Data Enrichment & Threat Intelligence +commonfields: + id: Google Threat Intelligence Threat Lists + version: -1 +sectionOrder: +- Connect +- Collect +configuration: +- display: API Key (leave empty. Fill in the API key in the password field.) + displaypassword: API Key + name: credentials + type: 9 + required: true + hiddenusername: true + section: Connect +- display: Feed type. + name: feed_type + defaultvalue: malware + type: 15 + options: + - cryptominer + - first-stage-delivery-vectors + - infostealer + - iot + - linux + - malicious-network-infrastructure + - malware + - mobile + - osx + - phishing + - ransomware + - threat-actor + - trending + - vulnerability-weaponization + section: Connect +- name: filter + display: Filter + type: 0 + additionalinfo: Filter your Threat Lists (e.g., "gti_score:70+ positives:10- has:campaigns"). Leave empty to receive all. + required: false + advanced: true + section: Connect +- display: The maximum number of results to return. If 0 all results will be returned. + name: limit + defaultvalue: 10 + type: 0 + required: false + section: Collect +- display: Fetch indicators + name: feed + defaultvalue: "true" + type: 8 + required: false + section: Collect +- display: Indicator Reputation + name: feedReputation + defaultvalue: feedInstanceReputationNotSet + type: 18 + options: + - None + - Good + - Suspicious + - Bad + additionalinfo: Indicators from this integration instance will be marked with this reputation. + required: false + section: Collect +- display: Source Reliability + name: feedReliability + defaultvalue: F - Reliability cannot be judged + type: 15 + required: true + options: + - A - Completely reliable + - B - Usually reliable + - C - Fairly reliable + - D - Not usually reliable + - E - Unreliable + - F - Reliability cannot be judged + additionalinfo: Reliability of the source providing the intelligence data. + section: Collect +- display: "" + name: feedExpirationPolicy + defaultvalue: indicatorType + type: 17 + options: + - never + - interval + - indicatorType + - suddenDeath + required: false + section: Collect + advanced: true +- display: "" + name: feedExpirationInterval + defaultvalue: "20160" + type: 1 + required: false + section: Collect + advanced: true +- display: Feed Fetch Interval + name: feedFetchInterval + defaultvalue: "60" + type: 19 + required: false + section: Collect + advanced: true +- display: Bypass exclusion list + name: feedBypassExclusionList + type: 8 + additionalinfo: When selected, the exclusion list is ignored for indicators from this feed. This means that if an indicator from this feed is on the exclusion list, the indicator might still be added to the system. + required: false + section: Collect + advanced: true +- name: feedTags + display: Tags + type: 0 + additionalinfo: Supports CSV values. + required: false + section: Collect + advanced: true +- name: tlp_color + display: Traffic Light Protocol Color + options: + - RED + - AMBER + - GREEN + - WHITE + type: 15 + additionalinfo: The Traffic Light Protocol (TLP) designation to apply to indicators fetched from the feed. + required: false + section: Collect +description: Use this feed integration to fetch Google Threat Intelligence Threat Lists matches as indicators. +display: Google Threat Intelligence Threat Lists +name: Google Threat Intelligence Threat Lists +script: + commands: + - arguments: + - name: feed_type + description: Feed type. + - name: package + description: Package in '%Y%m%d%H' format. If not given, the latest package is taken. + - description: Filter your Threat Lists (e.g., "gti_score:70+ positives:10- has:campaigns"). Leave empty to receive all. + name: filter + - name: limit + defaultValue: '10' + description: The maximum number of results to return. If 0 all results will be returned. + description: Gets the matches from the latest Feed. + name: gti-threatlists-get-indicators + dockerimage: demisto/python3:3.12.8.1983910 + feed: true + runonce: false + script: "-" + subtype: python3 + type: python +fromversion: 5.5.0 +tests: +- No tests (auto formatted) diff --git a/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists_description.md b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists_description.md new file mode 100644 index 000000000000..fbb52f333dac --- /dev/null +++ b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists_description.md @@ -0,0 +1,9 @@ +### Authorization: +Your API key can be found in your Google Threat Intelligence account user menu. +Your API key carries all your privileges, so keep it secure and don't share it with anyone. + +### Feed types: +Currently there are 14 feed types: cryptominer, first-stage-delivery-vectors, infostealer, iot, linux, malicious-network-infrastructure, malware, mobile, osx, phishing, ransomware, threat-actor, trending and vulnerability-weaponization. + +### Cortex XSOAR version 6.0.0 and below: +Fill anything in the "Username" fill box. Use you API key in the password fill box. \ No newline at end of file diff --git a/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists_image.png b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists_image.png new file mode 100644 index 000000000000..950d9124d7b2 Binary files /dev/null and b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists_image.png differ diff --git a/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists_test.py b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists_test.py new file mode 100644 index 000000000000..cbebcda3becd --- /dev/null +++ b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/FeedThreatLists_test.py @@ -0,0 +1,229 @@ +"""Tests for Google Threat Intelligence IoC Stream Feed integration.""" +import demistomock as demisto # noqa: F401 +from CommonServerPython import FeedIndicatorType # noqa: F401 + +import json +from unittest import mock + +from FeedThreatLists import Client, fetch_indicators_command, get_indicators_command, main + + +def _mock_indicator(indicator_type, gti_score=None): + """Mocks indicator.""" + with open(f'./test_data/{indicator_type}.json', encoding='utf-8') as f: + indicator_mock = json.load(f) + + if gti_score is not None: + indicator_mock['attributes']['gti_assessment']['threat_score']['value'] = gti_score + + return indicator_mock + + +def _mock_file(gti_score=None): + """Mocks file.""" + return _mock_indicator('file', gti_score) + + +def _mock_domain(gti_score=None): + """Mocks domain.""" + return _mock_indicator('domain', gti_score) + + +def _mock_url(gti_score=None): + """Mocks URL.""" + return _mock_indicator('url', gti_score) + + +def _mock_ip(gti_score=None): + """Mocks IP address.""" + return _mock_indicator('ip', gti_score) + + +def test_fetch_indicators_command(mocker): + """Tests fetch indicators command.""" + client = Client('https://fake') + + mocker.patch.object( + client, + 'get_threat_list', + return_value={ + 'iocs': [ + _mock_file(), + _mock_domain(), + _mock_url(), + _mock_ip(), + ], + }, + ) + + indicators = fetch_indicators_command(client, 'malware', limit=10) + + assert len(indicators) == 4 + + for indicator in indicators: + if indicator['type'] == FeedIndicatorType.File: + assert set(indicator['fields'].keys()) == { + 'md5', 'sha1', 'sha256', 'ssdeep', 'fileextension', 'filetype', 'imphash', + 'tags', 'firstseenbysource', 'lastseenbysource', 'creationdate', 'updateddate', + 'detectionengines', 'positivedetections', 'displayname', 'name', 'size', + 'gtithreatscore', 'gtiseverity', 'gtiverdict', 'actor', 'malwarefamily', + } + assert indicator['value'] == '' + assert indicator['value'] == indicator['fields']['sha256'] + assert indicator['fields']['gtiverdict'] == 'VERDICT_MALICIOUS' + assert indicator['score'] == 3 + elif indicator['type'] == FeedIndicatorType.Domain: + assert set(indicator['fields'].keys()) == { + 'admincountry', 'adminname', 'adminemail', 'adminphone', 'registrantcountry', + 'registrantemail', 'registrantname', 'registrantphone', 'registrarabusephone', + 'registrarabuseemail', 'registrarname', 'firstseenbysource', 'lastseenbysource', + 'tags', 'creationdate', 'updateddate', 'detectionengines', 'positivedetections', + 'gtithreatscore', 'gtiseverity', 'gtiverdict', 'actor', 'malwarefamily', + } + assert indicator['value'] == '' + assert indicator['fields']['adminemail'] == '@google.com' + assert indicator['fields']['registrantcountry'] == 'US' + assert indicator['fields']['registrarabusephone'] == '+34 600 000 000' + assert indicator['fields']['gtiverdict'] == 'VERDICT_MALICIOUS' + assert indicator['score'] == 3 + elif indicator['type'] == FeedIndicatorType.URL: + assert set(indicator['fields'].keys()) == { + 'tags', 'firstseenbysource', 'lastseenbysource', 'updateddate', + 'detectionengines', 'positivedetections', + 'gtithreatscore', 'gtiseverity', 'gtiverdict', 'actor', 'malwarefamily', + } + assert indicator['value'] == '' + assert indicator['fields']['firstseenbysource'] == 1722360511 + assert indicator['fields']['gtiverdict'] == 'VERDICT_UNDETECTED' + assert indicator['score'] == 0 + elif indicator['type'] == FeedIndicatorType.IP: + assert set(indicator['fields'].keys()) == { + 'tags', 'firstseenbysource', 'lastseenbysource', 'updateddate', + 'detectionengines', 'positivedetections', 'countrycode', + 'gtithreatscore', 'gtiseverity', 'gtiverdict', 'actor', 'malwarefamily', + } + assert indicator['value'] == 'X.X.X.X' + assert indicator['fields']['countrycode'] == 'US' + assert indicator['fields']['gtiverdict'] == 'VERDICT_BENIGN' + assert indicator['score'] == 1 + else: + raise ValueError(f'Unknown type: {indicator["type"]}') + + +def test_get_indicators_command(mocker): + """Tests get indicators command.""" + client = Client('https://fake') + + mocker.patch.object( + client, + 'get_threat_list', + return_value={ + 'iocs': [ + _mock_file(), + _mock_domain(), + _mock_url(), + _mock_ip(), + ], + }, + ) + params = { + 'tlp_color': None, + 'feedTags': [], + } + + result = get_indicators_command(client, params, {}) + + assert len(result.raw_response) == 4 + + +def test_main_manual_command(mocker): + """Tests main manual.""" + params = { + 'tlp_color': None, + 'feedTags': [], + 'credentials': {'password': 'xxx'}, + } + + args = { + 'limit': 7, + 'feed_type': 'malware', + 'filter': 'gti_score:95+', + } + + mocker.patch.object(demisto, 'params', return_value=params) + mocker.patch.object(demisto, 'command', return_value='gti-threatlists-get-indicators') + mocker.patch.object(demisto, 'args', return_value=args) + get_threat_list_mock = mocker.patch.object( + Client, + 'get_threat_list', + return_value={ + 'iocs': [ + _mock_file(), + _mock_domain(), + _mock_url(), + _mock_ip(), + ], + }, + ) + return_results_mock = mocker.patch.object(demisto, 'results') + + main() + + assert get_threat_list_mock.call_args == mock.call('malware', mock.ANY, 'gti_score:95+', 7) + assert len(return_results_mock.call_args[0][0]['Contents']) == 4 + + +def test_main_default_command(mocker): + """Tests main default.""" + params = { + 'tlp_color': None, + 'feedTags': [], + 'credentials': {'password': 'xxx'}, + 'limit': 7, + 'feed_type': 'malware', + 'filter': 'gti_score:1+', + } + + mocker.patch.object(demisto, 'params', return_value=params) + mocker.patch.object(demisto, 'command', return_value='fetch-indicators') + get_threat_list_mock = mocker.patch.object( + Client, + 'get_threat_list', + return_value={ + 'iocs': [ + _mock_file(), + _mock_domain(), + _mock_url(), + _mock_ip(), + ], + }, + ) + create_indicators_mock = mocker.patch.object(demisto, 'createIndicators') + + main() + + assert get_threat_list_mock.call_args == mock.call('malware', mock.ANY, 'gti_score:1+', 7) + assert len(create_indicators_mock.call_args[0][0]) == 4 + + +def test_main_test_command(mocker): + """Tests main test.""" + params = { + 'credentials': {'password': 'xxx'} + } + + mocker.patch.object(demisto, 'params', return_value=params) + mocker.patch.object(demisto, 'command', return_value='test-module') + get_threat_list_mock = mocker.patch.object( + Client, + 'get_threat_list', + return_value={ + 'iocs': [ + _mock_file(), + ], + }, + ) + + main() + + assert get_threat_list_mock.call_count == 1 diff --git a/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/README.md b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/README.md new file mode 100644 index 000000000000..57f53e50038e --- /dev/null +++ b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/README.md @@ -0,0 +1,59 @@ +Use this feed integration to fetch Google Threat Intelligence Threat Lists matches as indicators. It processes the latest finished job retrieving its matches based on the limit parameter (10 by default) in every fetch until there are no more matches for that job. + +## Configure Google Threat Intelligence Threat Lists on Cortex XSOAR + +1. Navigate to **Settings** > **Integrations** > **Servers & Services**. +2. Search for Google Threat Intelligence Threat Lists. +3. Click **Add instance** to create and configure a new integration instance. + +| **Parameter** | **Description** | **Required** | +| --- | --- | --- | +| feed | The fetch indicators. | False | +| credentials | API Key. | True | +| feed_type | Feed type. | True | +| filter | Filter your Threat Lists (e.g., "gti_score:70+ positives:10- has:campaigns"). Leave empty to receive all. | False | +| limit | The maximum number of results to return. Default is 10. | False | +| feedReputation | The indicator reputation. | False | +| feedReliability | The source's reliability. | True | +| tlp_color | The Traffic Light Protocol (TLP) designation to apply to indicators fetched from the feed. More information about the protocol can be found at https://us-cert.cisa.gov/tlp | False | +| feedExpirationPolicy | The feed's expiration policy. | False | +| feedFetchInterval | The feed fetch interval. | False | +| feedBypassExclusionList | Whether to bypass exclusion list. | False | + +4. Click **Test** to validate the URLs, token, and connection. + +## Commands +You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook. +After you successfully execute a command, a DBot message appears in the War Room with the command details. + +### gti-threatlists-get-indicators +*** +Gets the matches from Google Threat Intelligence Threat Lists. + +#### Base Command + +`gti-threatlists-get-indicators` +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| feed_type | Feed type. | Required | +| package | Package in '%Y%m%d%H' format. If not given, the latest package is taken. | Optional | +| filter | Filter your Threat Lists (e.g., "gti_score:70+ positives:10- has:campaigns"). Leave empty to receive all. | Optional | +| limit | The maximum number of results to return. Default is 10. | Optional | + + +#### Context Output + +There is no context output for this command. + +#### Command Example +```!gti-threatlists-get-indicators``` +```!gti-threatlists-get-indicators feed=malware package=2025021910 filter="gti_score:70+" limit=10``` + +#### Human Readable Output + +### Indicators from Google Threat Intelligence Threat Lists: +| Id | Detections | Gti Threat Score | Gti Severity | Gti Verdict | Malware Families | Threat Actors | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| f221425286c9073cbb2168f73120b6... | 59/69 | 80 | SEVERITY_LOW | VERDICT_MALICIOUS | beacon | SWEED | diff --git a/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/test_data/domain.json b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/test_data/domain.json new file mode 100644 index 000000000000..a3974621bb68 --- /dev/null +++ b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/test_data/domain.json @@ -0,0 +1,102 @@ +{ + "data": { + "id": "", + "type": "domain", + "attributes": { + "last_modification_date": 1726690020, + "reputation": 0, + "tld": "com", + "first_seen_itw_date": 1315830193, + "mandiant_ic_score": 84, + "registrar": "Amazon Registrar, Inc.", + "last_dns_records_date": 1726675835, + "whois": "Admin City: Clearwater\nAdmin Country: US\nAdmin Email: @google.com\nAdmin Organization: \nAdmin Postal Code: 33755\nAdmin State/Province: FL\nCreation Date: 2020-12-07T20:18:23Z\nDNSSEC: unsigned\nDomain Name: \nDomain Status: OK\nName Server: \nRegistrant City: 9f17c16e0cbd11e5\nRegistrant Country: US\nRegistrant Email: @google.com\nRegistrant Fax Ext: 3432650ec337c945\nRegistrant Fax: 3432650ec337c945\nRegistrant Name: 24277ff58446df8f\nRegistrant Organization: c25274193b9137ef\nRegistrant Phone Ext: 3432650ec337c945\nRegistrant Phone: 6099a769d1923d4a\nRegistrant Postal Code: f28adb1ee249d449\nRegistrant State/Province: 6eb233f5a5adbed8\nRegistrant Street: 7d57c43d3cfd7338\nRegistrar Abuse Contact Email: @google.com\nRegistrar Abuse Contact Phone: +34 600 000 000\nRegistrar IANA ID: \nRegistrar Registration Expiration Date: 2024-12-07T20:18:23Z\nRegistrar URL: \nRegistrar URL: \nRegistrar WHOIS Server: \nRegistrar: \nRegistry Admin ID: Not Available From Registry\nRegistry Domain ID: \nRegistry Expiry Date: 2024-12-07T20:18:23Z\nRegistry Registrant ID: Not Available From Registry\nRegistry Tech ID: Not Available From Registry\nTech City: Clearwater\nTech Country: US\nTech Email: @google.com\nTech Organization: \nTech Postal Code: 33755\nTech State/Province: FL\nUpdated Date: 2023-11-03T22:57:14Z", + "last_https_certificate_date": 1593568953, + "threat_verdict": "VERDICT_UNDETECTED", + "whois_date": 1724217310, + "categories": {}, + "last_analysis_stats": { + "malicious": 11, + "suspicious": 1, + "undetected": 31, + "harmless": 51, + "timeout": 0 + }, + "last_analysis_date": 1726675631, + "last_seen_itw_date": 1658186104, + "popularity_ranks": {}, + "creation_date": 1607372303, + "attribution": { + "detailed_threat_actors": [ + { + "name": "", + "source": "Mandiant", + "id": "" + } + ], + "threat_actors": [ + "" + ] + }, + "total_votes": { + "harmless": 0, + "malicious": 0 + }, + "last_update_date": 1699052234, + "threat_severity": { + "version": "D3", + "threat_severity_level": "SEVERITY_NONE", + "threat_severity_data": { + "num_detections": 11, + "belongs_to_bad_collection": true, + "belongs_to_threat_actor": true + }, + "last_analysis_date": "1726675845", + "level_description": "Severity NONE because it has less than 2 detections." + }, + "tags": [], + "gti_assessment": { + "verdict": { + "value": "VERDICT_MALICIOUS" + }, + "contributing_factors": { + "associated_actor": true, + "normalised_categories": [ + "malware" + ], + "mandiant_confidence_score": 84, + "safebrowsing_verdict": "harmless", + "mandiant_association_actor": true, + "mandiant_association_report": true + }, + "threat_score": { + "value": 90 + }, + "severity": { + "value": "SEVERITY_HIGH" + }, + "description": "This indicator is malicious (high severity) with high impact. It is associated with a Mandiant Intelligence Report, Mandiant's scoring pipeline identified this indicator as malicious and it is associated with a tracked Mandiant threat actor. Analysts should prioritize investigation." + } + }, + "context_attributes": { + "notification_id": "19104706970", + "origin": "hunting", + "notification_date": 1726761663, + "sources": [ + { + "id": "19072418300", + "type": "hunting_ruleset", + "label": "Malware Families YARA ruleset" + } + ], + "tags": [ + "malware_families_yara_ruleset", + "malwarefamilyagenttesla" + ], + "hunting_info": { + "rule_name": "MalwareFamilyAgentTesla", + "match_source": "ORIGINAL_FILE" + } + } + } +} diff --git a/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/test_data/file.json b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/test_data/file.json new file mode 100644 index 000000000000..fe38dfbb55e8 --- /dev/null +++ b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/test_data/file.json @@ -0,0 +1,141 @@ +{ + "data": { + "id": "", + "type": "file", + "attributes": { + "type_description": "Win32 EXE", + "tlsh": "", + "vhash": "", + "exiftool": { + "MIMEType": "application/octet-stream", + "Subsystem": "Windows GUI", + "MachineType": "Intel 386 or later, and compatibles", + "TimeStamp": "2010:11:20 09:03:08+00:00", + "FileType": "Win32 EXE", + "PEType": "PE32", + "CodeSize": "36864", + "LinkerVersion": "6.0", + "ImageFileCharacteristics": "No relocs, Executable, No line numbers, No symbols, 32-bit", + "FileTypeExtension": "exe", + "InitializedDataSize": "3682304", + "SubsystemVersion": "4.0", + "ImageVersion": "0.0", + "OSVersion": "4.0", + "EntryPoint": "0x9a16", + "UninitializedDataSize": "0" + }, + "trid": [ + { + "file_type": "Win32 Executable MS Visual C++ (generic)", + "probability": 38.8 + }, + { + "file_type": "Microsoft Visual C++ compiled executable (generic)", + "probability": 20.5 + }, + { + "file_type": "Win64 Executable (generic)", + "probability": 13.0 + }, + { + "file_type": "Win32 Dynamic Link Library (generic)", + "probability": 8.1 + }, + { + "file_type": "Win16 NE executable (generic)", + "probability": 6.2 + } + ], + "creation_date": 1290243788, + "names": [ + ".virus" + ], + "last_modification_date": 1635959808, + "type_tag": "peexe", + "times_submitted": 1, + "total_votes": { + "harmless": 0, + "malicious": 0 + }, + "size": 3723264, + "authentihash": "", + "last_submission_date": 1635952526, + "meaningful_name": ".virus", + "downloadable": true, + "sha256": "", + "type_extension": "exe", + "tags": [ + "peexe", + "cve-2017-0147", + "exploit" + ], + "last_analysis_date": 1635952526, + "unique_sources": 1, + "first_submission_date": 1635952526, + "sha1": "", + "ssdeep": "", + "bloom": "", + "packers": { + "PEiD": "Microsoft Visual C++" + }, + "md5": "", + "pe_info": { + "imphash": "" + }, + "magic": "PE32 executable for MS Windows (GUI) Intel 80386 32-bit", + "last_analysis_stats": { + "harmless": 0, + "type-unsupported": 5, + "suspicious": 0, + "confirmed-timeout": 0, + "timeout": 0, + "failure": 0, + "malicious": 60, + "undetected": 9 + }, + "reputation": 0, + "gti_assessment": { + "verdict": { + "value": "VERDICT_MALICIOUS" + }, + "contributing_factors": { + "associated_actor": true, + "normalised_categories": [ + "malware" + ], + "mandiant_confidence_score": 84, + "safebrowsing_verdict": "harmless", + "mandiant_association_actor": true, + "mandiant_association_report": true + }, + "threat_score": { + "value": 95 + }, + "severity": { + "value": "SEVERITY_HIGH" + }, + "description": "This indicator is malicious (high severity) with high impact. It is associated with a Mandiant Intelligence Report, Mandiant's scoring pipeline identified this indicator as malicious and it is associated with a tracked Mandiant threat actor. Analysts should prioritize investigation." + } + }, + "context_attributes": { + "notification_id": "19104706970", + "origin": "hunting", + "notification_date": 1726761663, + "sources": [ + { + "id": "19072418300", + "type": "hunting_ruleset", + "label": "Malware Families YARA ruleset" + } + ], + "tags": [ + "malware_families_yara_ruleset", + "malwarefamilyagenttesla" + ], + "hunting_info": { + "rule_name": "MalwareFamilyAgentTesla", + "match_source": "ORIGINAL_FILE" + } + } + } +} diff --git a/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/test_data/ip.json b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/test_data/ip.json new file mode 100644 index 000000000000..db1f87dd351a --- /dev/null +++ b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/test_data/ip.json @@ -0,0 +1,113 @@ +{ + "data": { + "id": "X.X.X.X", + "type": "ip_address", + "attributes": { + "last_modification_date": 1726781178, + "total_votes": { + "harmless": 203, + "malicious": 32 + }, + "last_analysis_stats": { + "malicious": 1, + "suspicious": 0, + "undetected": 31, + "harmless": 62, + "timeout": 0 + }, + "last_analysis_date": 1726758865, + "first_seen_itw_date": 1409607591, + "reputation": 535, + "tags": [], + "whois": "NetRange: X.X.X.0 - X.X.X.255\nCIDR: X.X.X.0/24\n", + "continent": "NA", + "network": "X.X.X.0/24", + "threat_verdict": "VERDICT_UNDETECTED", + "country": "US", + "regional_internet_registry": "ARIN", + "asn": 15169, + "whois_date": 1726150884, + "as_owner": "GOOGLE", + "last_https_certificate_date": 1726758877, + "mandiant_ic_score": 0, + "attribution": { + "detailed_threat_actors": [ + { + "source": "", + "name": "", + "id": "" + }, + { + "source": "", + "name": "", + "id": "" + } + ], + "malware_families": [ + { + "source": "", + "family": "" + }, + { + "source": "", + "family": "" + } + ], + "threat_actors": [ + "", + "" + ], + "family": "", + "family_pivot": "collection:None" + }, + "last_seen_itw_date": 1726766855, + "jarm": "", + "gti_assessment": { + "contributing_factors": { + "normalised_categories": [ + "infostealer", + "malware", + "control-server", + "phishing", + "malware", + "phishing" + ], + "pervasive_indicator": true, + "mandiant_confidence_score": 0, + "mandiant_analyst_benign": true, + "google_malware_analysis": true + }, + "severity": { + "value": "SEVERITY_NONE" + }, + "threat_score": { + "value": 0 + }, + "verdict": { + "value": "VERDICT_BENIGN" + }, + "description": "This indicator was determined as benign by a Mandiant analyst and likely poses no threat." + } + }, + "context_attributes": { + "notification_id": "19104706970", + "origin": "hunting", + "notification_date": 1726761663, + "sources": [ + { + "id": "19072418300", + "type": "hunting_ruleset", + "label": "Malware Families YARA ruleset" + } + ], + "tags": [ + "malware_families_yara_ruleset", + "malwarefamilyagenttesla" + ], + "hunting_info": { + "rule_name": "MalwareFamilyAgentTesla", + "match_source": "ORIGINAL_FILE" + } + } + } +} diff --git a/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/test_data/url.json b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/test_data/url.json new file mode 100644 index 000000000000..439cf34810f7 --- /dev/null +++ b/Packs/GoogleThreatIntelligence/Integrations/FeedThreatLists/test_data/url.json @@ -0,0 +1,81 @@ +{ + "data": { + "id": "", + "type": "url", + "attributes": { + "last_final_url": "", + "tags": [], + "categories": { + "Forcepoint ThreatSeeker": "elevated exposure" + }, + "private": false, + "last_analysis_stats": { + "malicious": 1, + "suspicious": 1, + "undetected": 25, + "harmless": 67, + "timeout": 0 + }, + "url": "", + "threat_verdict": "VERDICT_UNDETECTED", + "reputation": 0, + "has_content": false, + "last_analysis_date": 1722360511, + "threat_names": [], + "title": "", + "total_votes": { + "harmless": 0, + "malicious": 0 + }, + "tld": "wf", + "threat_severity": { + "version": "U3", + "threat_severity_level": "SEVERITY_NONE", + "threat_severity_data": { + "num_detections": 1 + }, + "last_analysis_date": "1722360525", + "level_description": "Severity NONE because it has less than 2 detections." + }, + "last_submission_date": 1722360511, + "times_submitted": 1, + "last_modification_date": 1722360527, + "first_submission_date": 1722360511, + "gti_assessment": { + "threat_score": { + "value": 1 + }, + "severity": { + "value": "SEVERITY_NONE" + }, + "contributing_factors": { + "safebrowsing_verdict": "harmless" + }, + "verdict": { + "value": "VERDICT_UNDETECTED" + }, + "description": "This indicator did not match our detection criteria and there is currently no evidence of malicious activity." + } + }, + "context_attributes": { + "notification_id": "19104706970", + "origin": "hunting", + "notification_date": 1726761663, + "sources": [ + { + "id": "19072418300", + "type": "hunting_ruleset", + "label": "Malware Families YARA ruleset" + } + ], + "tags": [ + "malware_families_yara_ruleset", + "malwarefamilyagenttesla" + ], + "hunting_info": { + "rule_name": "MalwareFamilyAgentTesla", + "match_source": "ORIGINAL_FILE" + } + } + } +} diff --git a/Packs/GoogleThreatIntelligence/ReleaseNotes/1_3_0.md b/Packs/GoogleThreatIntelligence/ReleaseNotes/1_3_0.md new file mode 100644 index 000000000000..7f04fbb1cd7c --- /dev/null +++ b/Packs/GoogleThreatIntelligence/ReleaseNotes/1_3_0.md @@ -0,0 +1,6 @@ + +#### Integrations + +##### New: Google Threat Intelligence Threat Lists + +- New: Use this feed integration to fetch Google Threat Intelligence Threat Lists matches as indicators. diff --git a/Packs/GoogleThreatIntelligence/pack_metadata.json b/Packs/GoogleThreatIntelligence/pack_metadata.json index 7faea84e7d53..eddc6f6fd917 100644 --- a/Packs/GoogleThreatIntelligence/pack_metadata.json +++ b/Packs/GoogleThreatIntelligence/pack_metadata.json @@ -2,7 +2,7 @@ "name": "GoogleThreatIntelligence", "description": "Analyze suspicious hashes, URLs, domains and IP addresses", "support": "partner", - "currentVersion": "1.2.0", + "currentVersion": "1.3.0", "author": "Google Threat Intelligence", "url": "https://www.virustotal.com", "email": "contact@virustotal.com",