From b95f095897901f836a390e45612971337ca21c8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 19:11:56 +0000 Subject: [PATCH 01/95] build(deps): bump werkzeug from 2.3.8 to 3.0.3 in /website Bumps [werkzeug](https://github.com/pallets/werkzeug) from 2.3.8 to 3.0.3. - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/werkzeug/compare/2.3.8...3.0.3) --- updated-dependencies: - dependency-name: werkzeug dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- website/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/requirements.txt b/website/requirements.txt index d87ce1b2..f83c7813 100644 --- a/website/requirements.txt +++ b/website/requirements.txt @@ -6,7 +6,7 @@ Flask-WTF Flask-Migrate Flask-Login WTForms -Werkzeug==2.3.8 +Werkzeug==3.0.3 flask-restx python-dateutil schedule From bb42e5d9c19d4765186bcc6a12813efe45d15175 Mon Sep 17 00:00:00 2001 From: Daniel Pascual Date: Mon, 13 May 2024 10:59:21 +0200 Subject: [PATCH 02/95] Google Threat Intelligence MISP module --- misp_modules/modules/expansion/__init__.py | 3 +- .../expansion/google_threat_intelligence.py | 330 ++++++++++++++++++ tests/test_expansions.py | 2 +- 3 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 misp_modules/modules/expansion/google_threat_intelligence.py diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index 9b5150cb..baacbe9d 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -20,7 +20,8 @@ __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'c 'trustar_enrich', 'recordedfuture', 'html_to_markdown', 'socialscan', 'passive-ssh', 'qintel_qsentry', 'mwdb', 'hashlookup', 'mmdb_lookup', 'ipqs_fraud_and_risk_scoring', 'clamav', 'jinja_template_rendering','hyasinsight', 'variotdbs', 'crowdsec', - 'extract_url_components', 'ipinfo', 'whoisfreaks', 'ip2locationio', 'vysion'] + 'extract_url_components', 'ipinfo', 'whoisfreaks', 'ip2locationio', 'vysion', + 'google_threat_intelligence'] minimum_required_fields = ('type', 'uuid', 'value') diff --git a/misp_modules/modules/expansion/google_threat_intelligence.py b/misp_modules/modules/expansion/google_threat_intelligence.py new file mode 100644 index 00000000..b8ab05b9 --- /dev/null +++ b/misp_modules/modules/expansion/google_threat_intelligence.py @@ -0,0 +1,330 @@ +#!/usr/local/bin/python +# Copyright © 2024 The Google Threat Intelligence authors. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Google Threat Intelligence MISP expansion module.""" + +from urllib import parse +import vt +import pymisp +import logging + + +MISP_ATTRIBUTES = { + 'input': [ + 'hostname', + 'domain', + 'ip-src', + 'ip-dst', + 'md5', + 'sha1', + 'sha256', + 'url', + ], + 'format': 'misp_standard', +} + +MODULE_INFO = { + 'version': '1', + 'author': 'Google Threat Intelligence team', + 'description': '', + 'module-type': ['expansion'], + 'config': [ + 'apikey', + 'event_limit', + 'proxy_host', + 'proxy_port', + 'proxy_username', + 'proxy_password' + ] +} + +DEFAULT_RESULTS_LIMIT = 10 + + + + +class GoogleThreatIntelligenceParser: + """Main parser class to create the MISP event.""" + def __init__(self, client: vt.Client, limit: int) -> None: + with open('/tmp/vtlog.txt','wb') as f: + f.write('XXXLOG2') + self.client = client + self.limit = limit or DEFAULT_RESULTS_LIMIT + self.misp_event = pymisp.MISPEvent() + self.attribute = pymisp.MISPAttribute() + self.parsed_objects = {} + self.input_types_mapping = { + 'ip-src': self.parse_ip, + 'ip-dst': self.parse_ip, + 'domain': self.parse_domain, + 'hostname': self.parse_domain, + 'md5': self.parse_hash, + 'sha1': self.parse_hash, + 'sha256': self.parse_hash, + 'url': self.parse_url + } + self.proxies = None + + def query_api(self, attribute: dict) -> None: + """Get data from the API and parse it.""" + self.attribute.from_dict(**attribute) + self.input_types_mapping[self.attribute.type](self.attribute.value) + + def get_results(self) -> dict: + """Serialize the MISP event.""" + event = self.misp_event.to_dict() + results = { + key: event[key] for key in ('Attribute', 'Object') \ + if (key in event and event[key]) + } + return {'results': results} + + def create_gti_report_object(self, report): + """Create GTI report object.""" + report = report.to_dict() + permalink = ('https://www.virustotal.com/gui/' + f"{report['type']}/{report['id']}") + report_object = pymisp.MISPObject('Google-Threat-Intel-report') + report_object.add_attribute('permalink', type='link', value=permalink) + report_object.add_attribute( + 'Threat Score', type='text', + value=get_key( + report, 'attributes.gti_assessment.threat_score.value')) + report_object.add_attribute( + 'Verdict', type='text', + value=get_key( + report, 'attributes.gti_assessment.verdict.value').replace( + 'VERDICT_', '')) + report_object.add_attribute( + 'Severity', type='text', + value=get_key( + report, 'attributes.gti_assessment.severity.value').replace( + 'SEVERITY_', '')) + report_object.add_attribute( + 'Threat Label', type='text', + value=get_key( + report, ('attributes.popular_threat_classification' + '.suggested_threat_label'))) + self.misp_event.add_object(**report_object) + return report_object.uuid + + def parse_domain(self, domain: str) -> str: + """Create domain MISP object.""" + domain_report = self.client.get_object(f'/domains/{domain}') + + # DOMAIN + domain_object = pymisp.MISPObject('domain-ip') + domain_object.add_attribute( + 'domain', type='domain', value=domain_report.id) + + report_uuid = self.create_gti_report_object(domain_report) + domain_object.add_reference(report_uuid, 'analyzed-with') + self.misp_event.add_object(**domain_object) + return domain_object.uuid + + def parse_hash(self, file_hash: str) -> str: + """Create hash MISP object.""" + file_report = self.client.get_object(f'/files/{file_hash}') + file_object = pymisp.MISPObject('file') + for hash_type in ('md5', 'sha1', 'sha256'): + file_object.add_attribute( + hash_type, + **{'type': hash_type, 'value': file_report.get(hash_type)}) + + report_uuid = self.create_gti_report_object(file_report) + file_object.add_reference(report_uuid, 'analyzed-with') + self.misp_event.add_object(**file_object) + return file_object.uuid + + def parse_ip(self, ip: str) -> str: + """Create ip MISP object.""" + ip_report = self.client.get_object(f'/ip_addresses/{ip}') + + # IP + ip_object = pymisp.MISPObject('domain-ip') + ip_object.add_attribute('ip', type='ip-dst', value=ip_report.id) + + report_uuid = self.create_gti_report_object(ip_report) + ip_object.add_reference(report_uuid, 'analyzed-with') + self.misp_event.add_object(**ip_object) + return ip_object.uuid + + def parse_url(self, url: str) -> str: + """Create URL MISP object.""" + url_id = vt.url_id(url) + url_report = self.client.get_object(f'/urls/{url_id}') + + url_object = pymisp.MISPObject('url') + url_object.add_attribute('url', type='url', value=url_report.url) + + report_uuid = self.create_gti_report_object(url_report) + url_object.add_reference(report_uuid, 'analyzed-with') + self.misp_event.add_object(**url_object) + return url_object.uuid + + +def get_key(dictionary, key, default_value=''): + """Get value from nested dictionaries.""" + dictionary = dictionary or {} + keys = key.split('.') + field_name = keys.pop() + for k in keys: + if k not in dictionary: + return default_value + dictionary = dictionary[k] + return dictionary.get(field_name, default_value) + + +def get_proxy_settings(config: dict) -> dict: + """Returns proxy settings in the requests format or None if not set up.""" + proxies = None + host = config.get('proxy_host') + port = config.get('proxy_port') + username = config.get('proxy_username') + password = config.get('proxy_password') + + if host: + if not port: + raise KeyError( + ('The google_threat_intelligence_proxy_host config is set, ' + 'please also set the virustotal_proxy_port.')) + parsed = parse.urlparse(host) + if 'http' in parsed.scheme: + scheme = 'http' + else: + scheme = parsed.scheme + netloc = parsed.netloc + host = f'{netloc}:{port}' + + if username: + if not password: + raise KeyError(('The google_threat_intelligence_' + ' proxy_host config is set, please also' + ' set the virustotal_proxy_password.')) + auth = f'{username}:{password}' + host = auth + '@' + host + + proxies = { + 'http': f'{scheme}://{host}', + 'https': f'{scheme}://{host}' + } + return proxies + + +def dict_handler(request: dict): + """MISP entry point fo the module.""" + with open('/tmp/vtlog.txt','wb') as f: + f.write('XXXLOG1') + if not request.get('config') or not request['config'].get('apikey'): + return { + 'error': ('A Google Threat Intelligence api ' + 'key is required for this module.') + } + + if not request.get('attribute'): + return { + 'error': ('This module requires an "attribute" field as input,' + ' which should contain at least a type, a value and an' + ' uuid.') + } + + if request['attribute']['type'] not in MISP_ATTRIBUTES['input']: + return {'error': 'Unsupported attribute type.'} + + event_limit = request['config'].get('event_limit') + attribute = request['attribute'] + + try: + proxy_settings = get_proxy_settings(request.get('config')) + client = vt.Client( + request['config']['apikey'], + headers={ + 'x-tool': 'MISPModuleGTIExpansion', + }, + proxy=proxy_settings['http'] if proxy_settings else None) + parser = GoogleThreatIntelligenceParser( + client, int(event_limit) if event_limit else None) + parser.query_api(attribute) + except vt.APIError as ex: + return {'error': ex.message} + except KeyError as ex: + return {'error': str(ex)} + with open('/tmp/vtlog.txt','wb') as f: + f.write('XXXLOG2') + + return parser.get_results() + + +def introspection(): + """Returns the module input attributes required.""" + return MISP_ATTRIBUTES + + +def version(): + """Returns the module metadata.""" + return MODULE_INFO + + +if __name__ == '__main__': + # Testing/debug calls. + import os + api_key = os.getenv('GTI_API_KEY') + # File + request_data = { + 'config': {'apikey': api_key}, + 'attribute': { + 'type': 'sha256', + 'value': ('ed01ebfbc9eb5bbea545af4d01bf5f10' + '71661840480439c6e5babe8e080e41aa') + } + } + response = dict_handler(request_data) + report_obj = response['results']['Object'][0] + print(report_obj.to_dict()) + + # URL + request_data = { + 'config': {'apikey': api_key}, + 'attribute': { + 'type': 'url', + 'value': 'http://47.21.48.182:60813/Mozi.a' + } + } + response = dict_handler(request_data) + report_obj = response['results']['Object'][0] + print(report_obj.to_dict()) + + # Ip + request_data = { + 'config': {'apikey': api_key}, + 'attribute': { + 'type': 'ip-src', + 'value': '180.72.148.38' + } + } + response = dict_handler(request_data) + report_obj = response['results']['Object'][0] + print(report_obj.to_dict()) + + # Domain + request_data = { + 'config': {'apikey': api_key}, + 'attribute': { + 'type': 'domain', + 'value': 'qexyhuv.com' + } + } + response = dict_handler(request_data) + report_obj = response['results']['Object'][0] + print(report_obj.to_dict()) diff --git a/tests/test_expansions.py b/tests/test_expansions.py index 96017249..6e17e619 100644 --- a/tests/test_expansions.py +++ b/tests/test_expansions.py @@ -92,7 +92,7 @@ class TestExpansions(unittest.TestCase): query = {'module': 'apiosintds', 'ip-dst': '10.10.10.10'} response = self.misp_modules_post(query) - + try: self.assertTrue(self.get_values(response).startswith('IoC 10.10.10.10')) except AssertionError: From da072cc38a6cce5514aa5e493c58375c523c32c9 Mon Sep 17 00:00:00 2001 From: Daniel Pascual Date: Mon, 13 May 2024 19:50:46 +0200 Subject: [PATCH 03/95] Remove debug traces --- .../modules/expansion/google_threat_intelligence.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/misp_modules/modules/expansion/google_threat_intelligence.py b/misp_modules/modules/expansion/google_threat_intelligence.py index b8ab05b9..fd6db0c5 100644 --- a/misp_modules/modules/expansion/google_threat_intelligence.py +++ b/misp_modules/modules/expansion/google_threat_intelligence.py @@ -17,7 +17,6 @@ from urllib import parse import vt import pymisp -import logging MISP_ATTRIBUTES = { @@ -57,8 +56,6 @@ DEFAULT_RESULTS_LIMIT = 10 class GoogleThreatIntelligenceParser: """Main parser class to create the MISP event.""" def __init__(self, client: vt.Client, limit: int) -> None: - with open('/tmp/vtlog.txt','wb') as f: - f.write('XXXLOG2') self.client = client self.limit = limit or DEFAULT_RESULTS_LIMIT self.misp_event = pymisp.MISPEvent() @@ -224,8 +221,6 @@ def get_proxy_settings(config: dict) -> dict: def dict_handler(request: dict): """MISP entry point fo the module.""" - with open('/tmp/vtlog.txt','wb') as f: - f.write('XXXLOG1') if not request.get('config') or not request['config'].get('apikey'): return { 'error': ('A Google Threat Intelligence api ' @@ -260,8 +255,6 @@ def dict_handler(request: dict): return {'error': ex.message} except KeyError as ex: return {'error': str(ex)} - with open('/tmp/vtlog.txt','wb') as f: - f.write('XXXLOG2') return parser.get_results() From 3af14a7f6ede8ec4e45f3575d5e9f1182f2206eb Mon Sep 17 00:00:00 2001 From: Daniel Pascual Date: Mon, 13 May 2024 20:00:14 +0200 Subject: [PATCH 04/95] Logo and desc --- misp_modules/modules/expansion/google_threat_intelligence.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/misp_modules/modules/expansion/google_threat_intelligence.py b/misp_modules/modules/expansion/google_threat_intelligence.py index fd6db0c5..3b767034 100644 --- a/misp_modules/modules/expansion/google_threat_intelligence.py +++ b/misp_modules/modules/expansion/google_threat_intelligence.py @@ -36,7 +36,8 @@ MISP_ATTRIBUTES = { MODULE_INFO = { 'version': '1', 'author': 'Google Threat Intelligence team', - 'description': '', + 'description': ('An expansion module to have the observable\'s threat' + ' score assessed by Google Threat Intelligence.'), 'module-type': ['expansion'], 'config': [ 'apikey', From bd9316b3131c8567de619e0a937d3682865f1f2d Mon Sep 17 00:00:00 2001 From: Daniel Pascual Date: Mon, 13 May 2024 20:15:39 +0200 Subject: [PATCH 05/95] doc --- docs/index.md | 1 + docs/logos/google_threat_intelligence.png | Bin 0 -> 4748 bytes documentation/README.md | 44 ++++++++++++++++------ 3 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 docs/logos/google_threat_intelligence.png diff --git a/docs/index.md b/docs/index.md index b3c588f9..817f9c49 100644 --- a/docs/index.md +++ b/docs/index.md @@ -38,6 +38,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [EQL](misp_modules/modules/expansion/eql.py) - an expansion module to generate event query language (EQL) from an attribute. [Event Query Language](https://eql.readthedocs.io/en/latest/) * [Farsight DNSDB Passive DNS](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/farsight_passivedns.py) - a hover and expansion module to expand hostname and IP addresses with passive DNS information. * [GeoIP](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/geoip_country.py) - a hover and expansion module to get GeoIP information from geolite/maxmind. +* [Google Threat Intelligence] (https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/google_threat_intelligence.py) - An expansion module to have the observable's threat score assessed by Google Threat Intelligence. * [Greynoise](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/greynoise.py) - a hover to get information from greynoise. * [hashdd](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/hashdd.py) - a hover module to check file hashes against [hashdd.com](http://www.hashdd.com) including NSLR dataset. * [hibp](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/hibp.py) - a hover module to lookup against Have I Been Pwned? diff --git a/docs/logos/google_threat_intelligence.png b/docs/logos/google_threat_intelligence.png new file mode 100644 index 0000000000000000000000000000000000000000..d0aa76df55c37557113b9f2a6a4b3dd4a5bc34cf GIT binary patch literal 4748 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 module takes a cpe attribute as input and queries the CVE search API to get its related vulnerabilities. +>The module takes a cpe attribute as input and queries the CVE search API to get its related vulnerabilities. >The list of vulnerabilities is then parsed and returned as vulnerability objects. > >Users can use their own CVE search API url by defining a value to the custom_API_URL parameter. If no custom API url is given, the default cve.circl.lu api url is used. @@ -640,6 +640,7 @@ Module to query a local copy of Maxmind's Geolite database. #### [google_search](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/google_search.py) + - **descrption**: >A hover module to get information about an url using a Google search. - **features**: @@ -655,6 +656,27 @@ Module to query a local copy of Maxmind's Geolite database. ----- +#### [google_threat_intelligence](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/google_threat_intelligence.py) + + + +- **description**: +An expansion module to have the observable's threat score assessed by Google Threat Intelligence. +- **features**: +>The module gives the Google Threat Intelligence assessment including a verdict for the given obsevable. [Example screeshot](https://github.com/MISP/MISP/assets/4747608/e275db2f-bb1e-4413-8cc0-ec3cb05e0414) +] +- **input**: +>'hostname', 'domain', 'ip-src', 'ip-dst', 'md5', 'sha1', 'sha256', 'url'. +- **output**: +>Text fields containing the threat score, the severity, the verdict and the threat label of the observable inspected. +- **references**: +>https://gtidocs.virustotal.com/reference +- **requirements**: +>- pymisp +>- vt + +----- + #### [greynoise](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/greynoise.py) @@ -745,7 +767,7 @@ Expansion module to fetch the html content from an url and convert it into markd HYAS Insight integration to MISP provides direct, high volume access to HYAS Insight data. It enables investigators and analysts to understand and defend against cyber adversaries and their infrastructure. - **features**: >This Module takes the IP Address, Domain, URL, Email, Phone Number, MD5, SHA1, Sha256, SHA512 MISP Attributes as input to query the HYAS Insight API. -> The results of the HYAS Insight API are than are then returned and parsed into Hyas Insight Objects. +> The results of the HYAS Insight API are than are then returned and parsed into Hyas Insight Objects. > >An API key is required to submit queries to the HYAS Insight API. > @@ -819,9 +841,9 @@ Module to access intelmqs eventdb. An expansion module to query IP2Location.io to gather more information on a given IP address. - **features**: ->The module takes an IP address attribute as input and queries the IP2Location.io API. ->Free plan user will get the basic geolocation informaiton, and different subsription plan will get more information on the IP address. -> Refer to [pricing page](https://www.ip2location.io/pricing) for more information on data available for each plan. +>The module takes an IP address attribute as input and queries the IP2Location.io API. +>Free plan user will get the basic geolocation informaiton, and different subsription plan will get more information on the IP address. +> Refer to [pricing page](https://www.ip2location.io/pricing) for more information on data available for each plan. > >More information on the responses content is available in the [documentation](https://www.ip2location.io/ip2location-documentation). - **input**: @@ -857,7 +879,7 @@ Module to query an IP ASN history service (https://github.com/D4-project/IPASN-H An expansion module to query ipinfo.io to gather more information on a given IP address. - **features**: ->The module takes an IP address attribute as input and queries the ipinfo.io API. +>The module takes an IP address attribute as input and queries the ipinfo.io API. >The geolocation information on the IP address is always returned. > >Depending on the subscription plan, the API returns different pieces of information then: @@ -883,7 +905,7 @@ An expansion module to query ipinfo.io to gather more information on a given IP IPQualityScore MISP Expansion Module for IP reputation, Email Validation, Phone Number Validation, Malicious Domain and Malicious URL Scanner. - **features**: >This Module takes the IP Address, Domain, URL, Email and Phone Number MISP Attributes as input to query the IPQualityScore API. -> The results of the IPQualityScore API are than returned as IPQS Fraud and Risk Scoring Object. +> The results of the IPQualityScore API are than returned as IPQS Fraud and Risk Scoring Object. > The object contains a copy of the enriched attribute with added tags presenting the verdict based on fraud score,risk score and other attributes from IPQualityScore. - **input**: >A MISP attribute of type IP Address(ip-src, ip-dst), Domain(hostname, domain), URL(url, uri), Email Address(email, email-src, email-dst, target-email, whois-registrant-email) and Phone Number(phone-number, whois-registrant-phone). @@ -1222,7 +1244,7 @@ Module to get information from AlienVault OTX. An expansion module to query the CIRCL Passive SSH. - **features**: >The module queries the Passive SSH service from CIRCL. -> +> > The module can be used an hover module but also an expansion model to add related MISP objects. > - **input**: @@ -1945,7 +1967,7 @@ Module to query a local instance of uwhois (https://github.com/rafiot/uwhoisd). An expansion module for https://whoisfreaks.com/ that will provide an enriched analysis of the provided domain, including WHOIS and DNS information. -Our Whois service, DNS Lookup API, and SSL analysis, equips organizations with comprehensive threat intelligence and attack surface analysis capabilities for enhanced security. +Our Whois service, DNS Lookup API, and SSL analysis, equips organizations with comprehensive threat intelligence and attack surface analysis capabilities for enhanced security. Explore our website's product section at https://whoisfreaks.com/ for a wide range of additional services catering to threat intelligence and attack surface analysis needs. - **features**: >The module takes a domain as input and queries the Whoisfreaks API with it. @@ -2084,7 +2106,7 @@ Module to process a query on Yeti. > - https://github.com/sebdraven/pyeti - **requirements**: > - pyeti -> - API key +> - API key ----- @@ -2241,7 +2263,7 @@ Simple export of a MISP event to PDF. > 'Activate_galaxy_description' is a boolean (True or void) to activate the description of event related galaxies. > 'Activate_related_events' is a boolean (True or void) to activate the description of related event. Be aware this might leak information on confidential events linked to the current event ! > 'Activate_internationalization_fonts' is a boolean (True or void) to activate Noto fonts instead of default fonts (Helvetica). This allows the support of CJK alphabet. Be sure to have followed the procedure to download Noto fonts (~70Mo) in the right place (/tools/pdf_fonts/Noto_TTF), to allow PyMisp to find and use them during PDF generation. -> 'Custom_fonts_path' is a text (path or void) to the TTF file of your choice, to create the PDF with it. Be aware the PDF won't support bold/italic/special style anymore with this option +> 'Custom_fonts_path' is a text (path or void) to the TTF file of your choice, to create the PDF with it. Be aware the PDF won't support bold/italic/special style anymore with this option - **input**: >MISP Event - **output**: From 6b7260229b5bf64c4ffc94422b9d66040560b602 Mon Sep 17 00:00:00 2001 From: Daniel Pascual Date: Mon, 13 May 2024 20:24:13 +0200 Subject: [PATCH 06/95] fix hedight --- documentation/README.md | 2 +- tests/test_expansions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/README.md b/documentation/README.md index 208317e2..11838066 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -658,7 +658,7 @@ Module to query a local copy of Maxmind's Geolite database. #### [google_threat_intelligence](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/google_threat_intelligence.py) - + - **description**: An expansion module to have the observable's threat score assessed by Google Threat Intelligence. diff --git a/tests/test_expansions.py b/tests/test_expansions.py index 6e17e619..96017249 100644 --- a/tests/test_expansions.py +++ b/tests/test_expansions.py @@ -92,7 +92,7 @@ class TestExpansions(unittest.TestCase): query = {'module': 'apiosintds', 'ip-dst': '10.10.10.10'} response = self.misp_modules_post(query) - + try: self.assertTrue(self.get_values(response).startswith('IoC 10.10.10.10')) except AssertionError: From a9dda347bb14dfcc9a01c70e3316cfb5f0cac260 Mon Sep 17 00:00:00 2001 From: Daniel Pascual Date: Tue, 14 May 2024 12:47:20 +0200 Subject: [PATCH 07/95] Add web doc and fix logo for the Google Threat Intelligence module --- docs/logos/google_threat_intelligence.png | Bin 4748 -> 18501 bytes .../expansion/google_threat_intelligence.json | 14 ++++++++++++++ .../expansion/google_threat_intelligence.py | 2 -- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 documentation/website/expansion/google_threat_intelligence.json diff --git a/docs/logos/google_threat_intelligence.png b/docs/logos/google_threat_intelligence.png index d0aa76df55c37557113b9f2a6a4b3dd4a5bc34cf..9a2067b2de250980676dd3ae6478d1f3f74e7b1e 100644 GIT binary patch literal 18501 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 B#;z*`NmqMA;HW3w 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 literal 4748 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 None: From 1457575dda5a846a702199bc02f6d516f8cd0627 Mon Sep 17 00:00:00 2001 From: David Cruciani Date: Thu, 16 May 2024 14:32:49 +0200 Subject: [PATCH 08/95] new: [functionality] flowintel + multiple entry --- website/app/__init__.py | 1 + website/app/db_class/db.py | 4 +- website/app/history/history_core.py | 8 +- website/app/home.py | 51 ++- website/app/home_core.py | 2 +- website/app/session_class.py | 51 +-- .../static/js/history/history_tree_query.js | 2 +- website/app/static/js/history/history_view.js | 4 +- website/app/static/js/mispParser.js | 30 +- website/app/templates/history.html | 2 +- website/app/templates/history_session.html | 4 +- website/app/templates/home.html | 62 +++- website/app/templates/query.html | 310 ++++++++++-------- website/app/utils/utils.py | 1 + website/conf/config.py | 1 + 15 files changed, 316 insertions(+), 217 deletions(-) diff --git a/website/app/__init__.py b/website/app/__init__.py index 6cf0a3cf..29967261 100644 --- a/website/app/__init__.py +++ b/website/app/__init__.py @@ -37,6 +37,7 @@ def create_app(): app.register_blueprint(home_blueprint, url_prefix="/") app.register_blueprint(history_blueprint, url_prefix="/") app.register_blueprint(account_blueprint, url_prefix="/") + csrf.exempt(home_blueprint) return app diff --git a/website/app/db_class/db.py b/website/app/db_class/db.py index 924b0fc6..89ec6ee2 100644 --- a/website/app/db_class/db.py +++ b/website/app/db_class/db.py @@ -38,7 +38,7 @@ class Session_db(db.Model): "id": self.id, "uuid": self.uuid, "modules": json.loads(self.modules_list), - "query_enter": self.query_enter, + "query_enter": json.loads(self.query_enter), "input_query": self.input_query, "config_module": json.loads(self.config_module), "result": json.loads(self.result), @@ -51,7 +51,7 @@ class Session_db(db.Model): json_dict = { "uuid": self.uuid, "modules": json.loads(self.modules_list), - "query": self.query_enter, + "query": json.loads(self.query_enter), "input": self.input_query, "query_date": self.query_date.strftime('%Y-%m-%d %H:%M') } diff --git a/website/app/history/history_core.py b/website/app/history/history_core.py index baa2b1ef..200a96b1 100644 --- a/website/app/history/history_core.py +++ b/website/app/history/history_core.py @@ -146,8 +146,8 @@ def util_remove_node_session(node_uuid, parent, parent_path): child = parent["children"][i] if child["uuid"] == node_uuid: del parent_path["children"][i] - return - elif child["children"]: + return True + elif "children" in child and child["children"]: return util_remove_node_session(node_uuid, child, parent_path["children"][i]) def remove_node_session(node_uuid): @@ -160,7 +160,9 @@ def remove_node_session(node_uuid): loc = i break elif q_value["children"]: - return util_remove_node_session(node_uuid, q_value, sess[keys_list[i]]) + if util_remove_node_session(node_uuid, q_value, sess[keys_list[i]]): + loc = i + break if loc: del sess[keys_list[i]] diff --git a/website/app/home.py b/website/app/home.py index 6c4e669d..7a675cb1 100644 --- a/website/app/home.py +++ b/website/app/home.py @@ -1,9 +1,10 @@ +import ast import json -from flask import Blueprint, render_template, request, jsonify, session as sess +from flask import Blueprint, redirect, render_template, request, jsonify, session as sess from flask_login import current_user from . import session_class as SessionModel from . import home_core as HomeModel -from .utils.utils import admin_user_active +from .utils.utils import admin_user_active, FLOWINTEL_URL home_blueprint = Blueprint( 'home', @@ -13,18 +14,35 @@ home_blueprint = Blueprint( ) -@home_blueprint.route("/") +@home_blueprint.route("/", methods=["GET", "POST"]) def home(): + try: + del sess["query"] + except: + pass sess["admin_user"] = bool(admin_user_active()) if "query" in request.args: - return render_template("home.html", query=request.args.get("query")) + sess["query"] = ast.literal_eval(request.args.get("query")) + if "query" in request.form: + sess["query"] = json.loads(request.form.get("query")) return render_template("home.html") +@home_blueprint.route("/get_query", methods=['GET', 'POST']) +def get_query(): + """Get result from flowintel""" + if "query" in sess: + return {"query": sess.get("query")} + return {"message": "No query"} + @home_blueprint.route("/home/", methods=["GET", "POST"]) def home_query(sid): + try: + del sess["query"] + except: + pass sess["admin_user"] = admin_user_active() if "query" in request.args: - query = request.args.get("query") + sess["query"] = [request.args.get("query")] return render_template("home.html", query=query, sid=sid) return render_template("404.html") @@ -33,21 +51,28 @@ def query(sid): sess["admin_user"] = admin_user_active() session = HomeModel.get_session(sid) flag=False + modules_list = [] if session: flag = True - query_loc = session.query_enter + query_loc = json.loads(session.query_enter) + modules_list = json.loads(session.modules_list) else: for s in SessionModel.sessions: if s.uuid == sid: flag = True query_loc = s.query session=s + modules_list = session.modules_list + query_str = ", ".join(query_loc) + if len(query_str) > 40: + query_str = query_str[0:40] + "..." if flag: return render_template("query.html", query=query_loc, + query_str=query_str, sid=sid, input_query=session.input_query, - modules=json.loads(session.modules_list), + modules=modules_list, query_date=session.query_date.strftime('%Y-%m-%d %H:%M')) return render_template("404.html") @@ -60,18 +85,20 @@ def get_query_info(sid): flag=False if session: flag = True - query_loc = session.query_enter + query_loc = json.loads(session.query_enter) + modules_list = json.loads(session.modules_list) else: for s in SessionModel.sessions: if s.uuid == sid: flag = True query_loc = s.query + modules_list = s.modules_list session=s if flag: loc_dict = { "query": query_loc, "input_query": session.input_query, - "modules": json.loads(session.modules_list), + "modules": modules_list, "query_date": session.query_date.strftime('%Y-%m-%d %H:%M') } return loc_dict @@ -227,3 +254,9 @@ def change_status(): return {'message': 'Something went wrong', 'toast_class': "danger-subtle"}, 400 return {'message': 'Need to pass "module_id"', 'toast_class': "warning-subtle"}, 400 return {'message': 'Permission denied', 'toast_class': "danger-subtle"}, 403 + + +@home_blueprint.route("/flowintel_url") +def flowintel_url(): + """send result to flowintel-cm""" + return {"url": f"{FLOWINTEL_URL}/analyzer/recieve_result"}, 200 diff --git a/website/app/home_core.py b/website/app/home_core.py index 1221fe54..2b13088b 100644 --- a/website/app/home_core.py +++ b/website/app/home_core.py @@ -163,7 +163,7 @@ def create_new_session_tree(current_session, parent_id): loc_json = { "uuid": loc_session.uuid, "modules": json.loads(loc_session.modules_list), - "query": loc_session.query_enter, + "query": json.loads(loc_session.query_enter), "input": loc_session.input_query, "query_date": loc_session.query_date.strftime('%Y-%m-%d %H:%M'), "config": json.loads(loc_session.config_module), diff --git a/website/app/session_class.py b/website/app/session_class.py index 2e32cab2..10b07f25 100644 --- a/website/app/session_class.py +++ b/website/app/session_class.py @@ -64,9 +64,12 @@ class Session_class: def start(self): """Start all worker""" - for i in range(len(self.modules_list)): - #need the index and the url in each queue item. - self.jobs.put((i, self.modules_list[i])) + cp = 0 + for i in self.query: + for j in self.modules_list: + self.jobs.put((cp, i, j)) + cp += 1 + #need the index and the url in each queue item. for _ in range(self.thread_count): worker = Thread(target=self.process) worker.daemon = True @@ -111,44 +114,44 @@ class Session_class: modules = query_get_module() loc_query = {} + self.result[work[1]] = dict() # If Misp format for module in modules: - if module["name"] == work[1]: + if module["name"] == work[2]: if "format" in module["mispattributes"]: loc_query = { "type": self.input_query, - "value": self.query, + "value": work[1], "uuid": str(uuid.uuid4()) } break loc_config = {} - if work[1] in self.config_module: - loc_config = self.config_module[work[1]] + if work[2] in self.config_module: + loc_config = self.config_module[work[2]] if loc_query: - send_to = {"module": work[1], "attribute": loc_query, "config": loc_config} + send_to = {"module": work[2], "attribute": loc_query, "config": loc_config} else: - send_to = {"module": work[1], self.input_query: self.query, "config": loc_config} + send_to = {"module": work[2], self.input_query: work[1], "config": loc_config} res = query_post_query(send_to) ## Sort attr in object by ui-priority - if "results" in res: - if "Object" in res["results"]: - for obj in res["results"]["Object"]: - loc_obj = get_object(obj["name"]) - if loc_obj: - for attr in obj["Attribute"]: - attr["ui-priority"] = loc_obj["attributes"][attr["object_relation"]]["ui-priority"] - - # After adding 'ui-priority' - obj["Attribute"].sort(key=lambda x: x["ui-priority"], reverse=True) + if res: + if "results" in res: + if "Object" in res["results"]: + for obj in res["results"]["Object"]: + loc_obj = get_object(obj["name"]) + if loc_obj: + for attr in obj["Attribute"]: + attr["ui-priority"] = loc_obj["attributes"][attr["object_relation"]]["ui-priority"] + + # After adding 'ui-priority' + obj["Attribute"].sort(key=lambda x: x["ui-priority"], reverse=True) - - # print(res) - if "error" in res: + if res and "error" in res: self.nb_errors += 1 - self.result[work[1]] = res + self.result[work[1]][work[2]] = res self.jobs.task_done() return True @@ -161,7 +164,7 @@ class Session_class: s = Session_db( uuid=str(self.uuid), modules_list=json.dumps(self.modules_list), - query_enter=self.query, + query_enter=json.dumps(self.query), input_query=self.input_query, config_module=json.dumps(self.config_module), result=json.dumps(self.result), diff --git a/website/app/static/js/history/history_tree_query.js b/website/app/static/js/history/history_tree_query.js index 022a3b56..8140c0c5 100644 --- a/website/app/static/js/history/history_tree_query.js +++ b/website/app/static/js/history/history_tree_query.js @@ -6,7 +6,7 @@ export default { }, template: ` -
  • [[history.query]]
  • +
  • [[history.query.join(", ")]]