diff --git a/.travis.yml b/.travis.yml index 0a3a912..d7f452e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,11 +6,11 @@ services: cache: pip python: - - "3.4" - "3.5" - "3.5-dev" - "3.6" - "3.6-dev" + - "3.7-dev" install: - pip install -U nose codecov pytest diff --git a/README.md b/README.md index 8b96814..f284e68 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [countrycode](misp_modules/modules/expansion/countrycode.py) - a hover module to tell you what country a URL belongs to. * [CrowdStrike Falcon](misp_modules/modules/expansion/crowdstrike_falcon.py) - an expansion module to expand using CrowdStrike Falcon Intel Indicator API. * [CVE](misp_modules/modules/expansion/cve.py) - a hover module to give more information about a vulnerability (CVE). +* [DBL Spamhaus](misp_modules/modules/expansion/dbl_spamhaus.py) - a hover module to check Spamhaus DBL for a domain name. * [DNS](misp_modules/modules/expansion/dns.py) - a simple module to resolve MISP attributes like hostname and domain to expand IP addresses attributes. * [DomainTools](misp_modules/modules/expansion/domaintools.py) - a hover and expansion module to get information from [DomainTools](http://www.domaintools.com/) whois. * [EUPI](misp_modules/modules/expansion/eupi.py) - a hover and expansion module to get information about an URL from the [Phishing Initiative project](https://phishing-initiative.eu/?lang=en). @@ -30,15 +31,26 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/ * [GeoIP](misp_modules/modules/expansion/geoip_country.py) - a hover and expansion module to get GeoIP information from geolite/maxmind. * [hashdd](misp_modules/modules/expansion/hashdd.py) - a hover module to check file hashes against [hashdd.com](http://www.hashdd.com) including NSLR dataset. * [IPASN](misp_modules/modules/expansion/ipasn.py) - a hover and expansion to get the BGP ASN of an IP address. -* [iprep](misp-modules/modules/expansion/iprep.py) - an expansion module to get IP reputation from packetmail.net. +* [iprep](misp_modules/modules/expansion/iprep.py) - an expansion module to get IP reputation from packetmail.net. +* [macaddress.io](misp_modules/modules/expansion/macaddress_io.py) - a hover module to retrieve vendor details and other information regarding a given MAC address or an OUI from [MAC address Vendor Lookup](https://macaddress.io). See [integration tutorial here](https://macaddress.io/integrations/MISP-module). +* [onyphe](misp_modules/modules/expansion/onyphe.py) - a modules to process queries on Onyphe. +* [onyphe_full](misp_modules/modules/expansion/onyphe_full.py) - a modules to process full queries on Onyphe. * [OTX](misp_modules/modules/expansion/otx.py) - an expansion module for [OTX](https://otx.alienvault.com/). * [passivetotal](misp_modules/modules/expansion/passivetotal.py) - a [passivetotal](https://www.passivetotal.org/) module that queries a number of different PassiveTotal datasets. * [rbl](misp_modules/modules/expansion/rbl.py) - a module to get RBL (Real-Time Blackhost List) values from an attribute. +* [reversedns](misp_modules/modules/expansion/reversedns.py) - Simple Reverse DNS expansion service to resolve reverse DNS from MISP attributes. +* [securitytrails](misp_modules/modules/expansion/securitytrails.py) - an expansion module for [securitytrails](https://securitytrails.com/). * [shodan](misp_modules/modules/expansion/shodan.py) - a minimal [shodan](https://www.shodan.io/) expansion module. +* [Sigma queries](misp_modules/modules/expansion/sigma_queries.py) - Experimental expansion module querying a sigma rule to convert it into all the available SIEM signatures. +* [Sigma syntax validator](misp_modules/modules/expansion/sigma_syntax_validator.py) - Sigma syntax validator. * [sourcecache](misp_modules/modules/expansion/sourcecache.py) - a module to cache a specific link from a MISP instance. +* [STIX2 pattern syntax validator](misp_modules/modules/expansion/stix2_pattern_syntax_validator.py) - a module to check a STIX2 pattern syntax. * [ThreatCrowd](misp_modules/modules/expansion/threatcrowd.py) - an expansion module for [ThreatCrowd](https://www.threatcrowd.org/). * [threatminer](misp_modules/modules/expansion/threatminer.py) - an expansion module to expand from [ThreatMiner](https://www.threatminer.org/). * [virustotal](misp_modules/modules/expansion/virustotal.py) - an expansion module to pull known resolutions and malware samples related with an IP/Domain from virusTotal (this modules require a VirusTotal private API key) +* [VMray](misp_modules/modules/expansion/vmray_submit.py) - a module to submit a sample to VMray. +* [VulnDB](misp_modules/modules/expansion/vulndb.py) - a module to query [VulnDB](https://www.riskbasedsecurity.com/). +* [whois](misp_modules/modules/expansion) - a module to query a local instance of [uwhois](https://github.com/rafiot/uwhoisd). * [wikidata](misp_modules/modules/expansion/wiki.py) - a [wikidata](https://www.wikidata.org) expansion module. * [xforce](misp_modules/modules/expansion/xforceexchange.py) - an IBM X-Force Exchange expansion module. * [YARA syntax validator](misp_modules/modules/expansion/yara_syntax_validator.py) - YARA syntax validator. @@ -373,7 +385,7 @@ Recommended Plugin.Import_ocr_enabled true Enable or disable the ocr In this same menu set any other plugin settings that are required for testing. ## Install misp-module on an offline instance. -First, you need to grab all necessery packages for example like this : +First, you need to grab all necessary packages for example like this : Use pip wheel to create an archive ~~~ @@ -381,7 +393,7 @@ mkdir misp-modules-offline pip3 wheel -r REQUIREMENTS shodan --wheel-dir=./misp-modules-offline tar -cjvf misp-module-bundeled.tar.bz2 ./misp-modules-offline/* ~~~ -On offline machine : +On offline machine : ~~~ mkdir misp-modules-bundle tar xvf misp-module-bundeled.tar.bz2 -C misp-modules-bundle @@ -440,3 +452,14 @@ cd tests/ curl -s http://127.0.0.1:6666/query -H "Content-Type: application/json" --data @MY_TEST_FILE.json -X POST cd ../ ~~~ + +## Documentation + +In order to provide documentation about some modules that require specific input / output / configuration, the [doc](doc) directory contains detailed information about the general purpose, requirements, features, input and ouput of each of these modules: + +- ***description** - quick description of the general purpose of the module, as the one given by the moduleinfo +- **requirements** - special libraries needed to make the module work +- **features** - description of the way to use the module, with the required MISP features to make the module give the intended result +- **references** - link(s) giving additional information about the format concerned in the module +- **input** - description of the format of data used in input +- **output** - description of the format given as the result of the module execution diff --git a/REQUIREMENTS b/REQUIREMENTS index 9404855..6ab46cc 100644 --- a/REQUIREMENTS +++ b/REQUIREMENTS @@ -12,11 +12,17 @@ asnhistory git+https://github.com/Rafiot/uwhoisd.git@testing#egg=uwhois&subdirectory=client git+https://github.com/MISP/PyMISP.git#egg=pymisp git+https://github.com/sebdraven/pyonyphe#egg=pyonyphe +git+https://github.com/sebdraven/pydnstrails#egg=pydnstrails pillow pytesseract +wand SPARQLWrapper domaintools_api pygeoip bs4 oauth2 yara +sigmatools +stix2-patterns +maclookup +vulners \ No newline at end of file diff --git a/doc/documentation.md b/doc/documentation.md new file mode 100644 index 0000000..7be5f29 --- /dev/null +++ b/doc/documentation.md @@ -0,0 +1,508 @@ +# MISP modules documentation + +## Expansion Modules + +#### [asn_history](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/asn_history.py) + +Query an ASN description history service (https://github.com/CIRCL/ASN-Description-History.git). +- **requirements**: +>asnhistory + +----- + +#### [circl_passivedns](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/circl_passivedns.py) + + + +Module to access CIRCL Passive DNS. + +----- + +#### [circl_passivessl](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/circl_passivessl.py) + + + +Modules to access CIRCL Passive SSL. + +----- + +#### [countrycode](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/countrycode.py) + +Module to expand country codes. + +----- + +#### [crowdstrike_falcon](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/crowdstrike_falcon.py) + + + +Module to query Crowdstrike Falcon. + +----- + +#### [cve](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/cve.py) + +An expansion hover module to expand information about CVE id. + +----- + +#### [dbl_spamhaus](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/dbl_spamhaus.py) + + + +Module to check Spamhaus DBL for a domain name. + +----- + +#### [dns](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/dns.py) + +A simple DNS expansion service to resolve IP address from MISP attributes. + +----- + +#### [domaintools](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/domaintools.py) + + + +DomainTools MISP expansion module. + +----- + +#### [eupi](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/eupi.py) + + + +A module to query the Phishing Initiative service (https://phishing-initiative.lu). + +----- + +#### [farsight_passivedns](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/farsight_passivedns.py) + + + +Module to access Farsight DNSDB Passive DNS. + +----- + +#### [geoip_country](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/geoip_country.py) + +Module to query a local copy of Maxminds Geolite database. + +----- + +#### [intelmq_eventdb](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/intelmq_eventdb.py) + +Module to access intelmqs eventdb. + +----- + +#### [ipasn](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/ipasn.py) + +Module to query an IP ASN history service (https://github.com/CIRCL/IP-ASN-history.git). + +----- + +#### [iprep](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/iprep.py) + +Module to query IPRep data for IP addresses. + +----- + +#### [onyphe](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/onyphe.py) + + + +Module to process a query on Onyphe. + +----- + +#### [onyphe_full](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/onyphe_full.py) + + + +Module to process a full query on Onyphe. + +----- + +#### [otx](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/otx.py) + + + +Module to get information from AlienVault OTX. + +----- + +#### [passivetotal](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/passivetotal.py) + + + +The PassiveTotal MISP expansion module brings the datasets derived from Internet scanning directly into your MISP instance. This module supports passive DNS, historic SSL, WHOIS, and host attributes. In order to use the module, you must have a valid PassiveTotal account username and API key. Registration is free and can be done by visiting https://www.passivetotal.org/register + +----- + +#### [rbl](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/rbl.py) + +Module to check an IPv4 address against known RBLs. +- **requirements**: +>dnspython3 + +----- + +#### [reversedns](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/reversedns.py) + +Simple Reverse DNS expansion service to resolve reverse DNS from MISP attributes. + +----- + +#### [shodan](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/shodan.py) + + + +Module to query on Shodan. + +----- + +#### [sourcecache](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/sourcecache.py) + +Module to cache web pages of analysis reports, OSINT sources. The module returns a link of the cached page. + +----- + +#### [threatcrowd](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/threatcrowd.py) + + + +Module to get information from ThreatCrowd. + +----- + +#### [threatminer](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/threatminer.py) + + + +Module to get information from ThreatMiner. + +----- + +#### [virustotal](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/virustotal.py) + + + +Module to get information from virustotal. + +----- + +#### [vmray_submit](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/vmray_submit.py) + + + +Module to submit a sample to VMRay. + +----- + +#### [vulndb](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/vulndb.py) + + + +Module to query VulnDB (RiskBasedSecurity.com). + +----- + +#### [whois](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/whois.py) + +Module to query a local instance of uwhois (https://github.com/rafiot/uwhoisd). +- **requirements**: +>uwhois + +----- + +#### [wiki](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/wiki.py) + + + +An expansion hover module to extract information from Wikidata to have additional information about particular term for analysis. + +----- + +#### [xforceexchange](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/xforceexchange.py) + + + +An expansion module for IBM X-Force Exchange. + +----- + +#### [yara_syntax_validator](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/expansion/yara_syntax_validator.py) + + + +An expansion hover module to perform a syntax check on if yara rules are valid or not. + +----- + +## Export Modules + +#### [cef_export](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/cef_export.py) + +Module to export a MISP event in CEF format. +- **features**: +>The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in Common Event Format. +>Thus, there is no particular feature concerning MISP Events since any event can be exported. However, 4 configuration parameters recognized by CEF format are required and should be provided by users before exporting data: the device vendor, product and version, as well as the default severity of data. +- **references**: +>https://community.softwaregrp.com/t5/ArcSight-Connectors/ArcSight-Common-Event-Format-CEF-Guide/ta-p/1589306?attachment-id=65537 +- **input**: +>MISP Event attributes +- **output**: +>Common Event Format file + +----- + +#### [goamlexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/goamlexport.py) + + + +This module is used to export MISP events containing transaction objects into GoAML format. +- **requirements**: +>PyMISP, MISP objects +- **features**: +>The module works as long as there is at least one transaction object in the Event. +> +>Then in order to have a valid GoAML document, please follow these guidelines: +>- For each transaction object, use either a bank-account, person, or legal-entity object to describe the origin of the transaction, and again one of them to describe the target of the transaction. +>- Create an object reference for both origin and target objects of the transaction. +>- A bank-account object needs a signatory, which is a person object, put as object reference of the bank-account. +>- A person can have an address, which is a geolocation object, put as object reference of the person. +> +>Supported relation types for object references that are recommended for each object are the folowing: +>- transaction: +> - 'from', 'from_my_client': Origin of the transaction - at least one of them is required. +> - 'to', 'to_my_client': Target of the transaction - at least one of them is required. +> - 'address': Location of the transaction - optional. +>- bank-account: +> - 'signatory': Signatory of a bank-account - the reference from bank-account to a signatory is required, but the relation-type is optional at the moment since this reference will always describe a signatory. +> - 'entity': Entity owning the bank account - optional. +>- person: +> - 'address': Address of a person - optional. +- **references**: +>http://goaml.unodc.org/ +- **input**: +>MISP objects (transaction, bank-account, person, legal-entity, geolocation), with references, describing financial transactions and their origin and target. +- **output**: +>GoAML format file, describing financial transactions, with their origin and target (bank accounts, persons or entities). + +----- + +#### [liteexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/liteexport.py) + +Lite export of a MISP event. +- **features**: +>This module is simply producing a json MISP event format file, but exporting only Attributes from the Event. Thus, MISP Events exported with this module should have attributes that are not internal references, otherwise the resulting event would be empty. +- **input**: +>MISP Event attributes +- **output**: +>Lite MISP Event + +----- + +#### [pdfexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/pdfexport.py) + +Simple export of a MISP event to PDF. +- **requirements**: +>PyMISP, asciidoctor +- **features**: +>The module takes care of the PDF file building, and work with any MISP Event. Except the requirement of asciidoctor, used to create the file, there is no special feature concerning the Event. +- **references**: +>https://acrobat.adobe.com/us/en/acrobat/about-adobe-pdf.html +- **input**: +>MISP Event +- **output**: +>MISP Event in a PDF file. + +----- + +#### [testexport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/testexport.py) + +Skeleton export module. + +----- + +#### [threatStream_misp_export](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/threatStream_misp_export.py) + + + +Module to export a structured CSV file for uploading to threatStream. +- **requirements**: +>csv +- **features**: +>The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in a CSV format recognized by ThreatStream. +- **references**: +>https://www.anomali.com/platform/threatstream, https://github.com/threatstream +- **input**: +>MISP Event attributes +- **output**: +>ThreatStream CSV format file + +----- + +#### [threat_connect_export](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/export_mod/threat_connect_export.py) + + + +Module to export a structured CSV file for uploading to ThreatConnect. +- **requirements**: +>csv +- **features**: +>The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in a CSV format recognized by ThreatConnect. +>Users should then provide, as module configuration, the source of data they export, because it is required by the output format. +- **references**: +>https://www.threatconnect.com +- **input**: +>MISP Event attributes +- **output**: +>ThreatConnect CSV format file + +----- + +## Import Modules + +#### [csvimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/csvimport.py) + +Module to import MISP attributes from a csv file. +- **requirements**: +>PyMISP +- **features**: +>In order to parse data from a csv file, a header is required to let the module know which column is matching with known attribute fields / MISP types. +>This header is part of the configuration of the module and should be filled out in MISP plugin settings, each field separated by COMMAS. Fields that do not match with any type known in MISP can be ignored in import, using a space or simply nothing between two separators (example: 'ip-src, , comment, '). +>There is also one type that is confused and can be either a MISP attribute type or an attribute field: 'comment'. In this case, using 'attrComment' specifies that the attribute field 'comment' should be considered, otherwise it will be considered as the MISP attribute type. +> +>For each MISP attribute type, an attribute is created. +>Attribute fields that are imported are the following: value, type, category, to-ids, distribution, comment, tag. +- **references**: +>https://tools.ietf.org/html/rfc4180, https://tools.ietf.org/html/rfc7111 +- **input**: +>CSV format file. +- **output**: +>MISP Event attributes + +----- + +#### [cuckooimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/cuckooimport.py) + + + +Module to import Cuckoo JSON. +- **features**: +>The module simply imports MISP Attributes from a Cuckoo JSON format file. There is thus no special feature to make it work. +- **references**: +>https://cuckoosandbox.org/, https://github.com/cuckoosandbox/cuckoo +- **input**: +>Cuckoo JSON file +- **output**: +>MISP Event attributes + +----- + +#### [email_import](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/email_import.py) + +Module to import emails in MISP. +- **features**: +>This module can be used to import e-mail text as well as attachments and urls. +>3 configuration parameters are then used to unzip attachments, guess zip attachment passwords, and extract urls: set each one of them to True or False to process or not the respective corresponding actions. +- **input**: +>E-mail file +- **output**: +>MISP Event attributes + +----- + +#### [goamlimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/goamlimport.py) + + + +Module to import MISP objects about financial transactions from GoAML files. +- **requirements**: +>PyMISP +- **features**: +>Unlike the GoAML export module, there is here no special feature to import data from GoAML external files, since the module will import MISP Objects with their References on its own, as it is required for the export module to rebuild a valid GoAML document. +- **references**: +>http://goaml.unodc.org/ +- **input**: +>GoAML format file, describing financial transactions, with their origin and target (bank accounts, persons or entities). +- **output**: +>MISP objects (transaction, bank-account, person, legal-entity, geolocation), with references, describing financial transactions and their origin and target. + +----- + +#### [mispjson](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/mispjson.py) + +Module to import MISP JSON format for merging MISP events. +- **features**: +>The module simply imports MISP Attributes from an other MISP Event in order to merge events together. There is thus no special feature to make it work. +- **input**: +>MISP Event +- **output**: +>MISP Event attributes + +----- + +#### [ocr](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/ocr.py) + +Optical Character Recognition (OCR) module for MISP. +- **features**: +>The module tries to recognize some text from an image and import the result as a freetext attribute, there is then no special feature asked to users to make it work. +- **input**: +>Image +- **output**: +>freetext MISP attribute + +----- + +#### [openiocimport](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/openiocimport.py) + +Module to import OpenIOC packages. +- **requirements**: +>PyMISP +- **features**: +>The module imports MISP Attributes from OpenIOC packages, there is then no special feature for users to make it work. +- **references**: +>https://www.fireeye.com/blog/threat-research/2013/10/openioc-basics.html +- **input**: +>OpenIOC packages +- **output**: +>MISP Event attributes + +----- + +#### [threatanalyzer_import](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/threatanalyzer_import.py) + +Module to import ThreatAnalyzer archive.zip / analysis.json files. +- **features**: +>The module imports MISP Attributes from a ThreatAnalyzer format file. This file can be either ZIP, or JSON format. +>There is by the way no special feature for users to make the module work. +- **references**: +>https://www.threattrack.com/malware-analysis.aspx +- **input**: +>ThreatAnalyzer format file +- **output**: +>MISP Event attributes + +----- + +#### [vmray_import](https://github.com/MISP/misp-modules/tree/master/misp_modules/modules/import_mod/vmray_import.py) + + + +Module to import VMRay (VTI) results. +- **requirements**: +>vmray_rest_api +- **features**: +>The module imports MISP Attributes from VMRay format, using the VMRay api. +>Users should then provide as the module configuration the API Key as well as the server url in order to fetch their data to import. +- **references**: +>https://www.vmray.com/ +- **input**: +>VMRay format +- **output**: +>MISP Event attributes + +----- diff --git a/doc/expansion/asn_history.json b/doc/expansion/asn_history.json new file mode 100644 index 0000000..936feba --- /dev/null +++ b/doc/expansion/asn_history.json @@ -0,0 +1,4 @@ +{ + "description": "Query an ASN description history service (https://github.com/CIRCL/ASN-Description-History.git).", + "requirements": ["asnhistory"] +} diff --git a/doc/expansion/circl_passivedns.json b/doc/expansion/circl_passivedns.json new file mode 100644 index 0000000..664ca77 --- /dev/null +++ b/doc/expansion/circl_passivedns.json @@ -0,0 +1,4 @@ +{ + "description": "Module to access CIRCL Passive DNS.", + "logo": "logos/passivedns.png" +} diff --git a/doc/expansion/circl_passivessl.json b/doc/expansion/circl_passivessl.json new file mode 100644 index 0000000..2015b59 --- /dev/null +++ b/doc/expansion/circl_passivessl.json @@ -0,0 +1,4 @@ +{ + "description": "Modules to access CIRCL Passive SSL.", + "logo": "logos/passivessl.png" +} diff --git a/doc/expansion/countrycode.json b/doc/expansion/countrycode.json new file mode 100644 index 0000000..367c14b --- /dev/null +++ b/doc/expansion/countrycode.json @@ -0,0 +1,3 @@ +{ + "description": "Module to expand country codes." +} diff --git a/doc/expansion/crowdstrike_falcon.json b/doc/expansion/crowdstrike_falcon.json new file mode 100644 index 0000000..0faa6c0 --- /dev/null +++ b/doc/expansion/crowdstrike_falcon.json @@ -0,0 +1,4 @@ +{ + "description": "Module to query Crowdstrike Falcon.", + "logo": "logos/crowdstrike.png" +} diff --git a/doc/expansion/cve.json b/doc/expansion/cve.json new file mode 100644 index 0000000..afc4c33 --- /dev/null +++ b/doc/expansion/cve.json @@ -0,0 +1,3 @@ +{ + "description": "An expansion hover module to expand information about CVE id." +} diff --git a/doc/expansion/dbl_spamhaus.json b/doc/expansion/dbl_spamhaus.json new file mode 100644 index 0000000..b691007 --- /dev/null +++ b/doc/expansion/dbl_spamhaus.json @@ -0,0 +1,4 @@ +{ + "description": "Module to check Spamhaus DBL for a domain name.", + "logo": "logos/spamhaus.jpg" +} diff --git a/doc/expansion/dns.json b/doc/expansion/dns.json new file mode 100644 index 0000000..2ca7e42 --- /dev/null +++ b/doc/expansion/dns.json @@ -0,0 +1,3 @@ +{ + "description": "A simple DNS expansion service to resolve IP address from MISP attributes." +} diff --git a/doc/expansion/domaintools.json b/doc/expansion/domaintools.json new file mode 100644 index 0000000..5ed0cb2 --- /dev/null +++ b/doc/expansion/domaintools.json @@ -0,0 +1,4 @@ +{ + "description": "DomainTools MISP expansion module.", + "logo": "logos/domaintools.png" +} diff --git a/doc/expansion/eupi.json b/doc/expansion/eupi.json new file mode 100644 index 0000000..42da8aa --- /dev/null +++ b/doc/expansion/eupi.json @@ -0,0 +1,4 @@ +{ + "description": "A module to query the Phishing Initiative service (https://phishing-initiative.lu).", + "logo": "logos/eupi.png" +} diff --git a/doc/expansion/farsight_passivedns.json b/doc/expansion/farsight_passivedns.json new file mode 100644 index 0000000..6fd038b --- /dev/null +++ b/doc/expansion/farsight_passivedns.json @@ -0,0 +1,4 @@ +{ + "description": "Module to access Farsight DNSDB Passive DNS.", + "logo": "logos/farsight.png" +} diff --git a/doc/expansion/geoip_country.json b/doc/expansion/geoip_country.json new file mode 100644 index 0000000..fb3bf33 --- /dev/null +++ b/doc/expansion/geoip_country.json @@ -0,0 +1,3 @@ +{ + "description": "Module to query a local copy of Maxminds Geolite database." +} diff --git a/doc/expansion/intelmq_eventdb.json b/doc/expansion/intelmq_eventdb.json new file mode 100644 index 0000000..7746551 --- /dev/null +++ b/doc/expansion/intelmq_eventdb.json @@ -0,0 +1,3 @@ +{ + "description": "Module to access intelmqs eventdb." +} diff --git a/doc/expansion/ipasn.json b/doc/expansion/ipasn.json new file mode 100644 index 0000000..1ab9cdd --- /dev/null +++ b/doc/expansion/ipasn.json @@ -0,0 +1,3 @@ +{ + "description": "Module to query an IP ASN history service (https://github.com/CIRCL/IP-ASN-history.git)." +} diff --git a/doc/expansion/iprep.json b/doc/expansion/iprep.json new file mode 100644 index 0000000..343ce4d --- /dev/null +++ b/doc/expansion/iprep.json @@ -0,0 +1,3 @@ +{ + "description": "Module to query IPRep data for IP addresses." +} diff --git a/doc/expansion/onyphe.json b/doc/expansion/onyphe.json new file mode 100644 index 0000000..4c00866 --- /dev/null +++ b/doc/expansion/onyphe.json @@ -0,0 +1,4 @@ +{ + "description": "Module to process a query on Onyphe.", + "logo": "logos/onyphe.jpg" +} diff --git a/doc/expansion/onyphe_full.json b/doc/expansion/onyphe_full.json new file mode 100644 index 0000000..15f07f1 --- /dev/null +++ b/doc/expansion/onyphe_full.json @@ -0,0 +1,4 @@ +{ + "description": "Module to process a full query on Onyphe.", + "logo": "logos/onyphe.jpg" +} diff --git a/doc/expansion/otx.json b/doc/expansion/otx.json new file mode 100644 index 0000000..16ee6d6 --- /dev/null +++ b/doc/expansion/otx.json @@ -0,0 +1,4 @@ +{ + "description": "Module to get information from AlienVault OTX.", + "logo": "logos/otx.png" +} diff --git a/doc/expansion/passivetotal.json b/doc/expansion/passivetotal.json new file mode 100644 index 0000000..5b09f56 --- /dev/null +++ b/doc/expansion/passivetotal.json @@ -0,0 +1,4 @@ +{ + "description": "The PassiveTotal MISP expansion module brings the datasets derived from Internet scanning directly into your MISP instance. This module supports passive DNS, historic SSL, WHOIS, and host attributes. In order to use the module, you must have a valid PassiveTotal account username and API key. Registration is free and can be done by visiting https://www.passivetotal.org/register", + "logo": "logos/passivetotal.png" +} diff --git a/doc/expansion/rbl.json b/doc/expansion/rbl.json new file mode 100644 index 0000000..0f67c2c --- /dev/null +++ b/doc/expansion/rbl.json @@ -0,0 +1,4 @@ +{ + "description": "Module to check an IPv4 address against known RBLs.", + "requirements": ["dnspython3"] +} diff --git a/doc/expansion/reversedns.json b/doc/expansion/reversedns.json new file mode 100644 index 0000000..96773ac --- /dev/null +++ b/doc/expansion/reversedns.json @@ -0,0 +1,3 @@ +{ + "description": "Simple Reverse DNS expansion service to resolve reverse DNS from MISP attributes." +} diff --git a/doc/expansion/shodan.json b/doc/expansion/shodan.json new file mode 100644 index 0000000..734d768 --- /dev/null +++ b/doc/expansion/shodan.json @@ -0,0 +1,4 @@ +{ + "description": "Module to query on Shodan.", + "logo": "logos/shodan.png" +} diff --git a/doc/expansion/sourcecache.json b/doc/expansion/sourcecache.json new file mode 100644 index 0000000..13c2a03 --- /dev/null +++ b/doc/expansion/sourcecache.json @@ -0,0 +1,3 @@ +{ + "description": "Module to cache web pages of analysis reports, OSINT sources. The module returns a link of the cached page." +} diff --git a/doc/expansion/threatcrowd.json b/doc/expansion/threatcrowd.json new file mode 100644 index 0000000..83af5fd --- /dev/null +++ b/doc/expansion/threatcrowd.json @@ -0,0 +1,4 @@ +{ + "description": "Module to get information from ThreatCrowd.", + "logo": "logos/threatcrowd.png" +} diff --git a/doc/expansion/threatminer.json b/doc/expansion/threatminer.json new file mode 100644 index 0000000..da75784 --- /dev/null +++ b/doc/expansion/threatminer.json @@ -0,0 +1,4 @@ +{ + "description": "Module to get information from ThreatMiner.", + "logo": "logos/threatminer.png" +} diff --git a/doc/expansion/virustotal.json b/doc/expansion/virustotal.json new file mode 100644 index 0000000..8c203eb --- /dev/null +++ b/doc/expansion/virustotal.json @@ -0,0 +1,4 @@ +{ + "description": "Module to get information from virustotal.", + "logo": "logos/virustotal.png" +} diff --git a/doc/expansion/vmray_submit.json b/doc/expansion/vmray_submit.json new file mode 100644 index 0000000..b977203 --- /dev/null +++ b/doc/expansion/vmray_submit.json @@ -0,0 +1,4 @@ +{ + "description": "Module to submit a sample to VMRay.", + "logo": "logos/vmray.png" +} diff --git a/doc/expansion/vulndb.json b/doc/expansion/vulndb.json new file mode 100644 index 0000000..a4fec3b --- /dev/null +++ b/doc/expansion/vulndb.json @@ -0,0 +1,4 @@ +{ + "description": "Module to query VulnDB (RiskBasedSecurity.com).", + "logo": "logos/vulndb.png" +} diff --git a/doc/expansion/whois.json b/doc/expansion/whois.json new file mode 100644 index 0000000..7c5c119 --- /dev/null +++ b/doc/expansion/whois.json @@ -0,0 +1,4 @@ +{ + "description": "Module to query a local instance of uwhois (https://github.com/rafiot/uwhoisd).", + "requirements": ["uwhois"] +} diff --git a/doc/expansion/wiki.json b/doc/expansion/wiki.json new file mode 100644 index 0000000..14c4451 --- /dev/null +++ b/doc/expansion/wiki.json @@ -0,0 +1,4 @@ +{ + "description": "An expansion hover module to extract information from Wikidata to have additional information about particular term for analysis.", + "logo": "logos/wikidata.png" +} diff --git a/doc/expansion/xforceexchange.json b/doc/expansion/xforceexchange.json new file mode 100644 index 0000000..13d3622 --- /dev/null +++ b/doc/expansion/xforceexchange.json @@ -0,0 +1,4 @@ +{ + "description": "An expansion module for IBM X-Force Exchange.", + "logo": "logos/xforce.png" +} diff --git a/doc/expansion/yara_syntax_validator.json b/doc/expansion/yara_syntax_validator.json new file mode 100644 index 0000000..891aa5a --- /dev/null +++ b/doc/expansion/yara_syntax_validator.json @@ -0,0 +1,4 @@ +{ + "description": "An expansion hover module to perform a syntax check on if yara rules are valid or not.", + "logo": "logos/yara.png" +} diff --git a/doc/export_mod/cef_export.json b/doc/export_mod/cef_export.json new file mode 100644 index 0000000..84bba8e --- /dev/null +++ b/doc/export_mod/cef_export.json @@ -0,0 +1,8 @@ +{ + "description": "Module to export a MISP event in CEF format.", + "requirements": [], + "features": "The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in Common Event Format.\nThus, there is no particular feature concerning MISP Events since any event can be exported. However, 4 configuration parameters recognized by CEF format are required and should be provided by users before exporting data: the device vendor, product and version, as well as the default severity of data.", + "references": ["https://community.softwaregrp.com/t5/ArcSight-Connectors/ArcSight-Common-Event-Format-CEF-Guide/ta-p/1589306?attachment-id=65537"], + "input": "MISP Event attributes", + "output": "Common Event Format file" +} diff --git a/doc/export_mod/goamlexport.json b/doc/export_mod/goamlexport.json new file mode 100644 index 0000000..57a1587 --- /dev/null +++ b/doc/export_mod/goamlexport.json @@ -0,0 +1,9 @@ +{ + "description": "This module is used to export MISP events containing transaction objects into GoAML format.", + "logo": "logos/goAML.jpg", + "requirements": ["PyMISP","MISP objects"], + "features": "The module works as long as there is at least one transaction object in the Event.\n\nThen in order to have a valid GoAML document, please follow these guidelines:\n- For each transaction object, use either a bank-account, person, or legal-entity object to describe the origin of the transaction, and again one of them to describe the target of the transaction.\n- Create an object reference for both origin and target objects of the transaction.\n- A bank-account object needs a signatory, which is a person object, put as object reference of the bank-account.\n- A person can have an address, which is a geolocation object, put as object reference of the person.\n\nSupported relation types for object references that are recommended for each object are the folowing:\n- transaction:\n\t- 'from', 'from_my_client': Origin of the transaction - at least one of them is required.\n\t- 'to', 'to_my_client': Target of the transaction - at least one of them is required.\n\t- 'address': Location of the transaction - optional.\n- bank-account:\n\t- 'signatory': Signatory of a bank-account - the reference from bank-account to a signatory is required, but the relation-type is optional at the moment since this reference will always describe a signatory.\n\t- 'entity': Entity owning the bank account - optional.\n- person:\n\t- 'address': Address of a person - optional.", + "references": ["http://goaml.unodc.org/"], + "input": "MISP objects (transaction, bank-account, person, legal-entity, geolocation), with references, describing financial transactions and their origin and target.", + "output": "GoAML format file, describing financial transactions, with their origin and target (bank accounts, persons or entities)." +} diff --git a/doc/export_mod/liteexport.json b/doc/export_mod/liteexport.json new file mode 100644 index 0000000..110577c --- /dev/null +++ b/doc/export_mod/liteexport.json @@ -0,0 +1,8 @@ +{ + "description": "Lite export of a MISP event.", + "requirements": [], + "features": "This module is simply producing a json MISP event format file, but exporting only Attributes from the Event. Thus, MISP Events exported with this module should have attributes that are not internal references, otherwise the resulting event would be empty.", + "references": [], + "input": "MISP Event attributes", + "output": "Lite MISP Event" +} diff --git a/doc/export_mod/pdfexport.json b/doc/export_mod/pdfexport.json new file mode 100644 index 0000000..9803c77 --- /dev/null +++ b/doc/export_mod/pdfexport.json @@ -0,0 +1,8 @@ +{ + "description": "Simple export of a MISP event to PDF.", + "requirements": ["PyMISP", "asciidoctor"], + "features": "The module takes care of the PDF file building, and work with any MISP Event. Except the requirement of asciidoctor, used to create the file, there is no special feature concerning the Event.", + "references": ["https://acrobat.adobe.com/us/en/acrobat/about-adobe-pdf.html"], + "input": "MISP Event", + "output": "MISP Event in a PDF file." +} diff --git a/doc/export_mod/testexport.json b/doc/export_mod/testexport.json new file mode 100644 index 0000000..213ea92 --- /dev/null +++ b/doc/export_mod/testexport.json @@ -0,0 +1,3 @@ +{ + "description": "Skeleton export module." +} diff --git a/doc/export_mod/threatStream_misp_export.json b/doc/export_mod/threatStream_misp_export.json new file mode 100644 index 0000000..3fdc50a --- /dev/null +++ b/doc/export_mod/threatStream_misp_export.json @@ -0,0 +1,9 @@ +{ + "description": "Module to export a structured CSV file for uploading to threatStream.", + "logo": "logos/threatstream.png", + "requirements": ["csv"], + "features": "The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in a CSV format recognized by ThreatStream.", + "references": ["https://www.anomali.com/platform/threatstream", "https://github.com/threatstream"], + "input": "MISP Event attributes", + "output": "ThreatStream CSV format file" +} diff --git a/doc/export_mod/threat_connect_export.json b/doc/export_mod/threat_connect_export.json new file mode 100644 index 0000000..8d19572 --- /dev/null +++ b/doc/export_mod/threat_connect_export.json @@ -0,0 +1,9 @@ +{ + "description": "Module to export a structured CSV file for uploading to ThreatConnect.", + "logo": "logos/threatconnect.png", + "requirements": ["csv"], + "features": "The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in a CSV format recognized by ThreatConnect.\nUsers should then provide, as module configuration, the source of data they export, because it is required by the output format.", + "references": ["https://www.threatconnect.com"], + "input": "MISP Event attributes", + "output": "ThreatConnect CSV format file" +} diff --git a/doc/generate_documentation.py b/doc/generate_documentation.py new file mode 100644 index 0000000..5a59814 --- /dev/null +++ b/doc/generate_documentation.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +import os +import json + +module_types = ['expansion', 'export_mod', 'import_mod'] +titles = ['Expansion Modules', 'Export Modules', 'Import Modules'] +markdown= ["# MISP modules documentation\n"] +githublink = 'https://github.com/MISP/misp-modules/tree/master/misp_modules/modules' + +def generate_doc(root_path): + for _path, title in zip(module_types, titles): + markdown.append('\n## {}\n'.format(title)) + current_path = os.path.join(root_path, _path) + files = sorted(os.listdir(current_path)) + githubpath = '{}/{}'.format(githublink, _path) + for _file in files: + modulename = _file.split('.json')[0] + githubref = '{}/{}.py'.format(githubpath, modulename) + markdown.append('\n#### [{}]({})\n'.format(modulename, githubref)) + filename = os.path.join(current_path, _file) + with open(filename, 'rt', encoding='utf-8') as f: + definition = json.loads(f.read()) + if 'logo' in definition: + markdown.append('\n\n'.format(definition.pop('logo'))) + if 'description' in definition: + markdown.append('\n{}\n'.format(definition.pop('description'))) + for field, value in definition.items(): + if value: + value = ', '.join(value) if isinstance(value, list) else '{}'.format(value.replace('\n', '\n>')) + markdown.append('- **{}**:\n>{}\n'.format(field, value)) + markdown.append('\n-----\n') + with open('documentation.md', 'w') as w: + w.write(''.join(markdown)) + +if __name__ == '__main__': + root_path = os.path.dirname(os.path.realpath(__file__)) + generate_doc(root_path) diff --git a/doc/import_mod/csvimport.json b/doc/import_mod/csvimport.json new file mode 100644 index 0000000..6dc6182 --- /dev/null +++ b/doc/import_mod/csvimport.json @@ -0,0 +1,8 @@ +{ + "description": "Module to import MISP attributes from a csv file.", + "requirements": ["PyMISP"], + "features": "In order to parse data from a csv file, a header is required to let the module know which column is matching with known attribute fields / MISP types.\nThis header is part of the configuration of the module and should be filled out in MISP plugin settings, each field separated by COMMAS. Fields that do not match with any type known in MISP can be ignored in import, using a space or simply nothing between two separators (example: 'ip-src, , comment, ').\nThere is also one type that is confused and can be either a MISP attribute type or an attribute field: 'comment'. In this case, using 'attrComment' specifies that the attribute field 'comment' should be considered, otherwise it will be considered as the MISP attribute type.\n\nFor each MISP attribute type, an attribute is created.\nAttribute fields that are imported are the following: value, type, category, to-ids, distribution, comment, tag.", + "references": ["https://tools.ietf.org/html/rfc4180", "https://tools.ietf.org/html/rfc7111"], + "input": "CSV format file.", + "output": "MISP Event attributes" +} diff --git a/doc/import_mod/cuckooimport.json b/doc/import_mod/cuckooimport.json new file mode 100644 index 0000000..8091d07 --- /dev/null +++ b/doc/import_mod/cuckooimport.json @@ -0,0 +1,9 @@ +{ + "description": "Module to import Cuckoo JSON.", + "logo": "logos/cuckoo.png", + "requirements": [], + "features": "The module simply imports MISP Attributes from a Cuckoo JSON format file. There is thus no special feature to make it work.", + "references": ["https://cuckoosandbox.org/", "https://github.com/cuckoosandbox/cuckoo"], + "input": "Cuckoo JSON file", + "output": "MISP Event attributes" +} diff --git a/doc/import_mod/email_import.json b/doc/import_mod/email_import.json new file mode 100644 index 0000000..1f53852 --- /dev/null +++ b/doc/import_mod/email_import.json @@ -0,0 +1,8 @@ +{ + "description": "Module to import emails in MISP.", + "requirements": [], + "features": "This module can be used to import e-mail text as well as attachments and urls.\n3 configuration parameters are then used to unzip attachments, guess zip attachment passwords, and extract urls: set each one of them to True or False to process or not the respective corresponding actions.", + "references": [], + "input": "E-mail file", + "output": "MISP Event attributes" +} diff --git a/doc/import_mod/goamlimport.json b/doc/import_mod/goamlimport.json new file mode 100644 index 0000000..f2a1ec2 --- /dev/null +++ b/doc/import_mod/goamlimport.json @@ -0,0 +1,9 @@ +{ + "description": "Module to import MISP objects about financial transactions from GoAML files.", + "logo": "logos/goAML.jpg", + "requirements": ["PyMISP"], + "features": "Unlike the GoAML export module, there is here no special feature to import data from GoAML external files, since the module will import MISP Objects with their References on its own, as it is required for the export module to rebuild a valid GoAML document.", + "references": "http://goaml.unodc.org/", + "input": "GoAML format file, describing financial transactions, with their origin and target (bank accounts, persons or entities).", + "output": "MISP objects (transaction, bank-account, person, legal-entity, geolocation), with references, describing financial transactions and their origin and target." +} diff --git a/doc/import_mod/mispjson.json b/doc/import_mod/mispjson.json new file mode 100644 index 0000000..dd11405 --- /dev/null +++ b/doc/import_mod/mispjson.json @@ -0,0 +1,8 @@ +{ + "description": "Module to import MISP JSON format for merging MISP events.", + "requirements": [], + "features": "The module simply imports MISP Attributes from an other MISP Event in order to merge events together. There is thus no special feature to make it work.", + "references": [], + "input": "MISP Event", + "output": "MISP Event attributes" +} diff --git a/doc/import_mod/ocr.json b/doc/import_mod/ocr.json new file mode 100644 index 0000000..14bbf0b --- /dev/null +++ b/doc/import_mod/ocr.json @@ -0,0 +1,8 @@ +{ + "description": "Optical Character Recognition (OCR) module for MISP.", + "requirements": [], + "features": "The module tries to recognize some text from an image and import the result as a freetext attribute, there is then no special feature asked to users to make it work.", + "references": [], + "input": "Image", + "output": "freetext MISP attribute" +} diff --git a/doc/import_mod/openiocimport.json b/doc/import_mod/openiocimport.json new file mode 100644 index 0000000..e173392 --- /dev/null +++ b/doc/import_mod/openiocimport.json @@ -0,0 +1,8 @@ +{ + "description": "Module to import OpenIOC packages.", + "requirements": ["PyMISP"], + "features": "The module imports MISP Attributes from OpenIOC packages, there is then no special feature for users to make it work.", + "references": ["https://www.fireeye.com/blog/threat-research/2013/10/openioc-basics.html"], + "input": "OpenIOC packages", + "output": "MISP Event attributes" +} diff --git a/doc/import_mod/threatanalyzer_import.json b/doc/import_mod/threatanalyzer_import.json new file mode 100644 index 0000000..40e4436 --- /dev/null +++ b/doc/import_mod/threatanalyzer_import.json @@ -0,0 +1,8 @@ +{ + "description": "Module to import ThreatAnalyzer archive.zip / analysis.json files.", + "requirements": [], + "features": "The module imports MISP Attributes from a ThreatAnalyzer format file. This file can be either ZIP, or JSON format.\nThere is by the way no special feature for users to make the module work.", + "references": ["https://www.threattrack.com/malware-analysis.aspx"], + "input": "ThreatAnalyzer format file", + "output": "MISP Event attributes" +} diff --git a/doc/import_mod/vmray_import.json b/doc/import_mod/vmray_import.json new file mode 100644 index 0000000..b7c0dad --- /dev/null +++ b/doc/import_mod/vmray_import.json @@ -0,0 +1,9 @@ +{ + "description": "Module to import VMRay (VTI) results.", + "logo": "logos/vmray.png", + "requirements": ["vmray_rest_api"], + "features": "The module imports MISP Attributes from VMRay format, using the VMRay api.\nUsers should then provide as the module configuration the API Key as well as the server url in order to fetch their data to import.", + "references": ["https://www.vmray.com/"], + "input": "VMRay format", + "output": "MISP Event attributes" +} diff --git a/doc/logos/Sigma.png b/doc/logos/Sigma.png new file mode 100644 index 0000000..0bd0db1 Binary files /dev/null and b/doc/logos/Sigma.png differ diff --git a/doc/logos/crowdstrike.png b/doc/logos/crowdstrike.png new file mode 100644 index 0000000..359cb01 Binary files /dev/null and b/doc/logos/crowdstrike.png differ diff --git a/doc/logos/cuckoo.png b/doc/logos/cuckoo.png new file mode 100644 index 0000000..57cf35a Binary files /dev/null and b/doc/logos/cuckoo.png differ diff --git a/doc/logos/domaintools.png b/doc/logos/domaintools.png new file mode 100644 index 0000000..69965e1 Binary files /dev/null and b/doc/logos/domaintools.png differ diff --git a/doc/logos/eupi.png b/doc/logos/eupi.png new file mode 100644 index 0000000..1800657 Binary files /dev/null and b/doc/logos/eupi.png differ diff --git a/doc/logos/farsight.png b/doc/logos/farsight.png new file mode 100644 index 0000000..31a73c1 Binary files /dev/null and b/doc/logos/farsight.png differ diff --git a/doc/logos/goAML.jpg b/doc/logos/goAML.jpg new file mode 100644 index 0000000..4e938ee Binary files /dev/null and b/doc/logos/goAML.jpg differ diff --git a/doc/logos/onyphe.jpg b/doc/logos/onyphe.jpg new file mode 100644 index 0000000..cd16f76 Binary files /dev/null and b/doc/logos/onyphe.jpg differ diff --git a/doc/logos/otx.png b/doc/logos/otx.png new file mode 100644 index 0000000..eae32c1 Binary files /dev/null and b/doc/logos/otx.png differ diff --git a/doc/logos/passivedns.png b/doc/logos/passivedns.png new file mode 100644 index 0000000..4959a84 Binary files /dev/null and b/doc/logos/passivedns.png differ diff --git a/doc/logos/passivessl.png b/doc/logos/passivessl.png new file mode 100644 index 0000000..e92c87d Binary files /dev/null and b/doc/logos/passivessl.png differ diff --git a/doc/logos/passivetotal.png b/doc/logos/passivetotal.png new file mode 100644 index 0000000..87cef69 Binary files /dev/null and b/doc/logos/passivetotal.png differ diff --git a/doc/logos/securitytrails.png b/doc/logos/securitytrails.png new file mode 100644 index 0000000..072dac5 Binary files /dev/null and b/doc/logos/securitytrails.png differ diff --git a/doc/logos/shodan.png b/doc/logos/shodan.png new file mode 100644 index 0000000..7de068e Binary files /dev/null and b/doc/logos/shodan.png differ diff --git a/doc/logos/spamhaus.jpg b/doc/logos/spamhaus.jpg new file mode 100644 index 0000000..4c868e4 Binary files /dev/null and b/doc/logos/spamhaus.jpg differ diff --git a/doc/logos/stix.png b/doc/logos/stix.png new file mode 100644 index 0000000..e8b8241 Binary files /dev/null and b/doc/logos/stix.png differ diff --git a/doc/logos/threatconnect.png b/doc/logos/threatconnect.png new file mode 100644 index 0000000..4c8a5b1 Binary files /dev/null and b/doc/logos/threatconnect.png differ diff --git a/doc/logos/threatcrowd.png b/doc/logos/threatcrowd.png new file mode 100644 index 0000000..94eacfc Binary files /dev/null and b/doc/logos/threatcrowd.png differ diff --git a/doc/logos/threatminer.png b/doc/logos/threatminer.png new file mode 100644 index 0000000..d7ac96e Binary files /dev/null and b/doc/logos/threatminer.png differ diff --git a/doc/logos/threatstream.png b/doc/logos/threatstream.png new file mode 100644 index 0000000..eb3837e Binary files /dev/null and b/doc/logos/threatstream.png differ diff --git a/doc/logos/virustotal.png b/doc/logos/virustotal.png new file mode 100644 index 0000000..935c5cc Binary files /dev/null and b/doc/logos/virustotal.png differ diff --git a/doc/logos/vmray.png b/doc/logos/vmray.png new file mode 100644 index 0000000..e2e9fa1 Binary files /dev/null and b/doc/logos/vmray.png differ diff --git a/doc/logos/vulndb.png b/doc/logos/vulndb.png new file mode 100644 index 0000000..bfaf40f Binary files /dev/null and b/doc/logos/vulndb.png differ diff --git a/doc/logos/wikidata.png b/doc/logos/wikidata.png new file mode 100644 index 0000000..0ffb4b1 Binary files /dev/null and b/doc/logos/wikidata.png differ diff --git a/doc/logos/xforce.png b/doc/logos/xforce.png new file mode 100644 index 0000000..96db659 Binary files /dev/null and b/doc/logos/xforce.png differ diff --git a/doc/logos/yara.png b/doc/logos/yara.png new file mode 100644 index 0000000..c74c314 Binary files /dev/null and b/doc/logos/yara.png differ diff --git a/misp_modules/__init__.py b/misp_modules/__init__.py index 1c1713b..3bb7253 100644 --- a/misp_modules/__init__.py +++ b/misp_modules/__init__.py @@ -193,7 +193,7 @@ class QueryModule(tornado.web.RequestHandler): if dict_payload.get('timeout'): timeout = datetime.timedelta(seconds=int(dict_payload.get('timeout'))) else: - timeout = datetime.timedelta(seconds=30) + timeout = datetime.timedelta(seconds=300) response = yield tornado.gen.with_timeout(timeout, self.run_request(jsonpayload)) self.write(response) except tornado.gen.TimeoutError: diff --git a/misp_modules/modules/expansion/__init__.py b/misp_modules/modules/expansion/__init__.py index a9389e0..fce9343 100644 --- a/misp_modules/modules/expansion/__init__.py +++ b/misp_modules/modules/expansion/__init__.py @@ -1,3 +1,3 @@ from . import _vmray -__all__ = ['vmray_submit', 'asn_history', 'circl_passivedns', 'circl_passivessl', 'countrycode', 'cve', 'dns', 'domaintools', 'eupi', 'farsight_passivedns', 'ipasn', 'passivetotal', 'sourcecache', 'virustotal', 'whois', 'shodan', 'reversedns', 'geoip_country', 'wiki', 'iprep', 'threatminer', 'otx', 'threatcrowd', 'vulndb', 'crowdstrike_falcon', 'yara_syntax_validator', 'hashdd'] +__all__ = ['vmray_submit', 'asn_history', 'circl_passivedns', 'circl_passivessl', 'countrycode', 'cve', 'dns', 'domaintools', 'eupi', 'farsight_passivedns', 'ipasn', 'passivetotal', 'sourcecache', 'virustotal', 'whois', 'shodan', 'reversedns', 'geoip_country', 'wiki', 'iprep', 'threatminer', 'otx', 'threatcrowd', 'vulndb', 'crowdstrike_falcon', 'yara_syntax_validator', 'hashdd', 'onyphe', 'onyphe_full', 'rbl', 'xforceexchange', 'sigma_syntax_validator', 'stix2_pattern_syntax_validator', 'sigma_queries', 'dbl_spamhaus', 'vulners'] diff --git a/misp_modules/modules/expansion/dbl_spamhaus.py b/misp_modules/modules/expansion/dbl_spamhaus.py new file mode 100644 index 0000000..306ea21 --- /dev/null +++ b/misp_modules/modules/expansion/dbl_spamhaus.py @@ -0,0 +1,60 @@ +import json +import datetime +from collections import defaultdict + +try: + import dns.resolver + resolver = dns.resolver.Resolver() + resolver.timeout = 0.2 + resolver.lifetime = 0.2 +except ImportError: + print("dnspython3 is missing, use 'pip install dnspython3' to install it.") + sys.exit(0) + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['domain', 'domain|ip', 'hostname', 'hostname|port'], 'output': ['text']} +moduleinfo = {'version': '0.1', 'author': 'Christian Studer', + 'description': 'Checks Spamhaus DBL for a domain name.', + 'module-type': ['expansion', 'hover']} +moduleconfig = [] + +dbl = 'dbl.spamhaus.org' +dbl_mapping = {'127.0.1.2': 'spam domain', + '127.0.1.4': 'phish domain', + '127.0.1.5': 'malware domain', + '127.0.1.6': 'botnet C&C domain', + '127.0.1.102': 'abused legit spam', + '127.0.1.103': 'abused spammed redirector domain', + '127.0.1.104': 'abused legit phish', + '127.0.1.105': 'abused legit malware', + '127.0.1.106': 'abused legit botnet C&C', + '127.0.1.255': 'IP queries prohibited!'} + +def fetch_requested_value(request): + for attribute_type in mispattributes['input']: + if request.get(attribute_type): + return request[attribute_type].split('|')[0] + return None + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + requested_value = fetch_requested_value(request) + if requested_value is None: + misperrors['error'] = "Unsupported attributes type" + return misperrors + query = "{}.{}".format(requested_value, dbl) + try: + query_result = resolver.query(query, 'A')[0] + result = "{} - {}".format(requested_value, dbl_mapping[str(query_result)]) + except Exception as e: + result = e + return {'results': [{'types': mispattributes.get('output'), 'values': result}]} + +def introspection(): + return mispattributes + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/misp_modules/modules/expansion/macaddress_io.py b/misp_modules/modules/expansion/macaddress_io.py new file mode 100644 index 0000000..e735f39 --- /dev/null +++ b/misp_modules/modules/expansion/macaddress_io.py @@ -0,0 +1,126 @@ +import json + +from maclookup import ApiClient, exceptions + +misperrors = { + 'error': 'Error' +} + +mispattributes = { + 'input': ['mac-address'], +} + +moduleinfo = { + 'version': '1.0', + 'author': 'CodeLine OY - macaddress.io', + 'description': 'MISP hover module for macaddress.io', + 'module-type': ['hover'] +} + +moduleconfig = ['api_key'] + + +def handler(q=False): + if q is False: + return False + + request = json.loads(q) + + if request.get('mac-address'): + mac_address = request['mac-address'] + else: + return False + + if request['config'].get('api_key'): + api_key = request['config'].get('api_key') + + else: + misperrors['error'] = 'Authorization required' + return misperrors + + api_client = ApiClient(api_key) + + try: + response = api_client.get(mac_address) + + except exceptions.EmptyResponseException: + misperrors['error'] = 'Empty response' + return misperrors + + except exceptions.UnparsableResponseException: + misperrors['error'] = 'Unparsable response' + return misperrors + + except exceptions.ServerErrorException: + misperrors['error'] = 'Internal server error' + return misperrors + + except exceptions.UnknownOutputFormatException: + misperrors['error'] = 'Unknown output' + return misperrors + + except exceptions.AuthorizationRequiredException: + misperrors['error'] = 'Authorization required' + return misperrors + + except exceptions.AccessDeniedException: + misperrors['error'] = 'Access denied' + return misperrors + + except exceptions.InvalidMacOrOuiException: + misperrors['error'] = 'Invalid MAC or OUI' + return misperrors + + except exceptions.NotEnoughCreditsException: + misperrors['error'] = 'Not enough credits' + return misperrors + + except Exception: + misperrors['error'] = 'Unknown error' + return misperrors + + date_created = \ + response.block_details.date_created.strftime('%d %B %Y') if response.block_details.date_created else None + + date_updated = \ + response.block_details.date_updated.strftime('%d %B %Y') if response.block_details.date_updated else None + + results = { + 'results': [ + {'types': ['text'], 'values': + { + # Mac address details + 'Valid MAC address': "True" if response.mac_address_details.is_valid else "False", + 'Transmission type': response.mac_address_details.transmission_type, + 'Administration type': response.mac_address_details.administration_type, + + # Vendor details + 'OUI': response.vendor_details.oui, + 'Vendor details are hidden': "True" if response.vendor_details.is_private else "False", + 'Company name': response.vendor_details.company_name, + 'Company\'s address': response.vendor_details.company_address, + 'County code': response.vendor_details.country_code, + + # Block details + 'Block found': "True" if response.block_details.block_found else "False", + 'The left border of the range': response.block_details.border_left, + 'The right border of the range': response.block_details.border_right, + 'The total number of MAC addresses in this range': response.block_details.block_size, + 'Assignment block size': response.block_details.assignment_block_size, + 'Date when the range was allocated': date_created, + 'Date when the range was last updated': date_updated + } + } + ] + } + + return results + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/modules/expansion/module.py.skeleton b/misp_modules/modules/expansion/module.py.skeleton similarity index 100% rename from modules/expansion/module.py.skeleton rename to misp_modules/modules/expansion/module.py.skeleton diff --git a/misp_modules/modules/expansion/otx.py b/misp_modules/modules/expansion/otx.py index 214e7f0..86685eb 100755 --- a/misp_modules/modules/expansion/otx.py +++ b/misp_modules/modules/expansion/otx.py @@ -32,16 +32,15 @@ def valid_ip(ip): def findAll(data, keys): a = [] if isinstance(data, dict): - for key in data.keys(): + for key, value in data.items(): if key == keys: - a.append(data[key]) + a.append(value) else: - if isinstance(data[key], (dict, list)): - a += findAll(data[key], keys) + if isinstance(value, (dict, list)): + a.extend(findAll(value, keys)) if isinstance(data, list): for i in data: - a += findAll(i, keys) - + a.extend(findAll(i, keys)) return a def valid_email(email): @@ -82,10 +81,10 @@ def handler(q=False): return r -def getHash(hash, key): +def getHash(_hash, key): ret = [] - req = json.loads(requests.get("https://otx.alienvault.com/otxapi/indicator/file/analysis/" + hash).text) + req = json.loads(requests.get("https://otx.alienvault.com/otxapi/indicator/file/analysis/" + _hash).text) for ip in findAll(req, "dst"): if not isBlacklisted(ip) and valid_ip(ip): @@ -102,8 +101,8 @@ def getIP(ip, key): ret = [] req = json.loads( requests.get("https://otx.alienvault.com/otxapi/indicator/ip/malware/" + ip + "?limit=1000").text ) - for hash in findAll(req, "hash"): - ret.append({"types": ["sha256"], "values": [hash]}) + for _hash in findAll(req, "hash"): + ret.append({"types": ["sha256"], "values": [_hash]}) req = json.loads( requests.get("https://otx.alienvault.com/otxapi/indicator/ip/passive_dns/" + ip).text ) @@ -122,21 +121,21 @@ def getDomain(domain, key): req = json.loads( requests.get("https://otx.alienvault.com/otxapi/indicator/domain/malware/" + domain + "?limit=1000").text ) - for hash in findAll(req, "hash"): - ret.append({"types": ["sha256"], "values": [hash]}) + for _hash in findAll(req, "hash"): + ret.append({"types": ["sha256"], "values": [_hash]}) req = json.loads(requests.get("https://otx.alienvault.com/otxapi/indicator/domain/whois/" + domain).text) - for domain in findAll(req, "domain"): - ret.append({"types": ["hostname"], "values": [domain]}) + for _domain in findAll(req, "domain"): + ret.append({"types": ["hostname"], "values": [_domain]}) for email in findAll(req, "value"): if valid_email(email): - ret.append({"types": ["email"], "values": [domain]}) + ret.append({"types": ["email"], "values": [email]}) - for domain in findAll(req, "hostname"): - if "." in domain and not isBlacklisted(domain): - ret.append({"types": ["hostname"], "values": [domain]}) + for _domain in findAll(req, "hostname"): + if "." in _domain and not isBlacklisted(_domain): + ret.append({"types": ["hostname"], "values": [_domain]}) req = json.loads(requests.get("https://otx.alienvault.com/otxapi/indicator/hostname/passive_dns/" + domain).text) for ip in findAll(req, "address"): diff --git a/misp_modules/modules/expansion/rbl.py b/misp_modules/modules/expansion/rbl.py index da8c5fb..6626760 100644 --- a/misp_modules/modules/expansion/rbl.py +++ b/misp_modules/modules/expansion/rbl.py @@ -6,7 +6,7 @@ try: resolver = dns.resolver.Resolver() resolver.timeout = 0.2 resolver.lifetime = 0.2 -except: +except ImportError: print("dnspython3 is missing, use 'pip install dnspython3' to install it.") sys.exit(0) @@ -96,13 +96,12 @@ def handler(q=False): txt = resolver.query(query,'TXT') listed.append(query) info.append(str(txt[0])) - except: + except Exception: continue result = {} for l, i in zip(listed, info): result[l] = i - r = {'results': [{'types': mispattributes.get('output'), 'values': json.dumps(result)}]} - return r + return {'results': [{'types': mispattributes.get('output'), 'values': json.dumps(result)}]} def introspection(): return mispattributes diff --git a/misp_modules/modules/expansion/securitytrails.py b/misp_modules/modules/expansion/securitytrails.py new file mode 100644 index 0000000..325fa13 --- /dev/null +++ b/misp_modules/modules/expansion/securitytrails.py @@ -0,0 +1,566 @@ +import json +import logging +import sys +import time + +from dnstrails import APIError +from dnstrails import DnsTrails + +log = logging.getLogger('dnstrails') +log.setLevel(logging.DEBUG) +ch = logging.StreamHandler(sys.stdout) +ch.setLevel(logging.DEBUG) +formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s') +ch.setFormatter(formatter) +log.addHandler(ch) + +misperrors = {'error': 'Error'} +mispattributes = { + 'input': ['hostname', 'domain', 'ip-src', 'ip-dst'], + 'output': ['hostname', 'domain', 'ip-src', 'ip-dst', 'dns-soa-email', + 'whois-registrant-email', 'whois-registrant-phone', + 'whois-registrant-name', + 'whois-registrar', 'whois-creation-date', 'domain'] +} + +moduleinfo = {'version': '1', 'author': 'Sebastien Larinier @sebdraven', + 'description': 'Query on securitytrails.com', + 'module-type': ['expansion', 'hover']} + +# config fields that your code expects from the site admin +moduleconfig = ['apikey'] + + +def handler(q=False): + if q: + + request = json.loads(q) + + if not request.get('config') and not (request['config'].get('apikey')): + misperrors['error'] = 'DNS authentication is missing' + return misperrors + + api = DnsTrails(request['config'].get('apikey')) + + if not api: + misperrors['error'] = 'Onyphe Error instance api' + + ip = "" + dns_name = "" + + ip = '' + if request.get('ip-src'): + ip = request['ip-src'] + return handle_ip(api, ip, misperrors) + elif request.get('ip-dst'): + ip = request['ip-dst'] + return handle_ip(api, ip, misperrors) + elif request.get('domain'): + domain = request['domain'] + return handle_domain(api, domain, misperrors) + elif request.get('hostname'): + hostname = request['hostname'] + return handle_domain(api, hostname, misperrors) + else: + misperrors['error'] = "Unsupported attributes types" + return misperrors + else: + return False + + +def handle_domain(api, domain, misperrors): + result_filtered = {"results": []} + + r, status_ok = expand_domain_info(api, misperrors, domain) + + if status_ok: + if r: + result_filtered['results'].extend(r) + else: + misperrors['error'] = misperrors['error'] + ' Error DNS result' + return misperrors + + time.sleep(1) + r, status_ok = expand_subdomains(api, domain) + + if status_ok: + if r: + result_filtered['results'].extend(r) + else: + misperrors['error'] = misperrors['error'] + ' Error subdomains result' + return misperrors + + time.sleep(1) + r, status_ok = expand_whois(api, domain) + + if status_ok: + if r: + result_filtered['results'].extend(r) + else: + misperrors['error'] = misperrors['error'] + ' Error whois result' + return misperrors + + time.sleep(1) + r, status_ok = expand_history_ipv4_ipv6(api, domain) + + if status_ok: + if r: + result_filtered['results'].extend(r) + else: + misperrors['error'] = misperrors['error'] + ' Error history ipv4' + return misperrors + + time.sleep(1) + + r, status_ok = expand_history_dns(api, domain) + + if status_ok: + if r: + result_filtered['results'].extend(r) + else: + misperrors['error'] = misperrors[ + 'error'] + ' Error in expand History DNS' + return misperrors + + r, status_ok = expand_history_whois(api, domain) + + if status_ok: + if r: + result_filtered['results'].extend(r) + else: + misperrors['error'] = misperrors['error'] + \ + ' Error in expand History Whois' + return misperrors + + return result_filtered + + +def handle_ip(api, ip, misperrors): + result_filtered = {"results": []} + + r, status_ok = expand_searching_domain(api, ip) + + if status_ok: + if r: + result_filtered['results'].extend(r) + else: + misperrors['error'] += ' Error in expand searching domain' + return misperrors + + return result_filtered + + +def expand_domain_info(api, misperror, domain): + r = [] + status_ok = False + ns_servers = [] + list_ipv4 = [] + list_ipv6 = [] + servers_mx = [] + soa_hostnames = [] + + results = api.domain(domain) + + if results: + status_ok = True + if 'current_dns' in results: + if 'values' in results['current_dns']['ns']: + ns_servers = [ns_entry['nameserver'] for ns_entry in + results['current_dns']['ns']['values'] + if 'nameserver' in ns_entry] + if 'values' in results['current_dns']['a']: + list_ipv4 = [a_entry['ip'] for a_entry in + results['current_dns']['a']['values'] if + 'ip' in a_entry] + + if 'values' in results['current_dns']['aaaa']: + list_ipv6 = [ipv6_entry['ipv6'] for ipv6_entry in + results['current_dns']['aaaa']['values'] if + 'ipv6' in ipv6_entry] + + if 'values' in results['current_dns']['mx']: + servers_mx = [mx_entry['hostname'] for mx_entry in + results['current_dns']['mx']['values'] if + 'hostname' in mx_entry] + if 'values' in results['current_dns']['soa']: + soa_hostnames = [soa_entry['email'] for soa_entry in + results['current_dns']['soa']['values'] if + 'email' in soa_entry] + + if ns_servers: + r.append({'types': ['domain'], + 'values': ns_servers, + 'categories': ['Network activity'], + 'comment': 'List of name servers of %s first seen %s ' % + (domain, + results['current_dns']['ns']['first_seen']) + }) + + if list_ipv4: + r.append({'types': ['domain|ip'], + 'values': ['%s|%s' % (domain, ipv4) for ipv4 in + list_ipv4], + 'categories': ['Network activity'], + + 'comment': ' List ipv4 of %s first seen %s' % + (domain, + results['current_dns']['a']['first_seen']) + + }) + if list_ipv6: + r.append({'types': ['domain|ip'], + 'values': ['%s|%s' % (domain, ipv6) for ipv6 in + list_ipv6], + 'categories': ['Network activity'], + 'comment': ' List ipv6 of %s first seen %s' % + (domain, + results['current_dns']['aaaa']['first_seen']) + + }) + + if servers_mx: + r.append({'types': ['domain'], + 'values': servers_mx, + 'categories': ['Network activity'], + 'comment': ' List mx of %s first seen %s' % + (domain, + results['current_dns']['mx']['first_seen']) + + }) + if soa_hostnames: + r.append({'types': ['domain'], + 'values': soa_hostnames, + 'categories': ['Network activity'], + 'comment': ' List soa of %s first seen %s' % + (domain, + results['current_dns']['soa']['first_seen']) + }) + + return r, status_ok + + +def expand_subdomains(api, domain): + r = [] + status_ok = False + + try: + results = api.subdomains(domain) + + if results: + status_ok = True + if 'subdomains' in results: + r.append({ + 'types': ['domain'], + 'values': ['%s.%s' % (sub, domain) + for sub in results['subdomains']], + 'categories': ['Network activity'], + 'comment': 'subdomains of %s' % domain + } + + ) + except APIError as e: + misperrors['error'] = e.value + return [], False + + return r, status_ok + + +def expand_whois(api, domain): + r = [] + status_ok = False + + try: + results = api.whois(domain) + + if results: + status_ok = True + item_registrant = __select_registrant_item(results) + if item_registrant: + + if 'email' in item_registrant[0]: + r.append( + { + 'types': ['whois-registrant-email'], + 'values': [item_registrant[0]['email']], + 'categories': ['Attribution'], + 'comment': 'Whois information of %s by securitytrails' + % domain + } + ) + + if 'telephone' in item_registrant[0]: + r.append( + { + 'types': ['whois-registrant-phone'], + 'values': [item_registrant[0]['telephone']], + 'categories': ['Attribution'], + 'comment': 'Whois information of %s by securitytrails' + % domain + } + ) + + if 'name' in item_registrant[0]: + r.append( + { + 'types': ['whois-registrant-name'], + 'values': [item_registrant[0]['name']], + 'categories': ['Attribution'], + 'comment': 'Whois information of %s by securitytrails' + % domain + } + ) + + if 'registrarName' in item_registrant[0]: + r.append( + { + 'types': ['whois-registrar'], + 'values': [item_registrant[0]['registrarName']], + 'categories': ['Attribution'], + 'comment': 'Whois information of %s by securitytrails' + % domain + } + ) + + if 'createdDate' in item_registrant[0]: + r.append( + { + 'types': ['whois-creation-date'], + 'values': [item_registrant[0]['createdDate']], + 'categories': ['Attribution'], + 'comment': 'Whois information of %s by securitytrails' + % domain + } + ) + + except APIError as e: + misperrors['error'] = e.value + return [], False + + return r, status_ok + + +def expand_history_ipv4_ipv6(api, domain): + r = [] + status_ok = False + + try: + results = api.history_dns_ipv4(domain) + + if results: + status_ok = True + r.extend(__history_ip(results, domain)) + + time.sleep(1) + results = api.history_dns_aaaa(domain) + + if results: + status_ok = True + r.extend(__history_ip(results, domain, type_ip='ipv6')) + + except APIError as e: + misperrors['error'] = e.value + return [], False + + return r, status_ok + + +def expand_history_dns(api, domain): + r = [] + status_ok = False + + try: + + results = api.history_dns_ns(domain) + if results: + r.extend(__history_dns(results, domain, 'nameserver', 'ns')) + + time.sleep(1) + + results = api.history_dns_soa(domain) + + if results: + r.extend(__history_dns(results, domain, 'email', 'soa')) + + time.sleep(1) + + results = api.history_dns_mx(domain) + + if results: + status_ok = True + r.extend(__history_dns(results, domain, 'host', 'mx')) + + except APIError as e: + misperrors['error'] = e.value + return [], False + + status_ok = True + + return r, status_ok + + +def expand_history_whois(api, domain): + r = [] + status_ok = False + try: + results = api.history_whois(domain) + + if results: + + if 'items' in results['result']: + for item in results['result']['items']: + item_registrant = __select_registrant_item(item) + + r.append( + { + 'types': ['domain'], + 'values': item['nameServers'], + 'categories': ['Network activity'], + 'comment': 'Whois history Name Servers of %s ' + 'Status: %s ' % ( + domain, ' '.join(item['status'])) + + } + ) + if item_registrant: + + if 'email' in item_registrant[0]: + r.append( + { + 'types': ['whois-registrant-email'], + 'values': [item_registrant[0]['email']], + 'categories': ['Attribution'], + 'comment': 'Whois history registrant email of %s' + 'Status: %s' % ( + domain, + ' '.join(item['status'])) + } + ) + + if 'telephone' in item_registrant[0]: + r.append( + { + 'types': ['whois-registrant-phone'], + 'values': [item_registrant[0]['telephone']], + 'categories': ['Attribution'], + 'comment': 'Whois history registrant phone of %s' + 'Status: %s' % ( + domain, + ' '.join(item['status'])) + } + ) + + except APIError as e: + misperrors['error'] = e.value + return [], False + status_ok = True + + return r, status_ok + + +def __history_ip(results, domain, type_ip='ip'): + r = [] + if 'records' in results: + for record in results['records']: + if 'values' in record: + for item in record['values']: + r.append( + {'types': ['domain|ip'], + 'values': ['%s|%s' % (domain, item[type_ip])], + 'categories': ['Network activity'], + 'comment': 'History IP on securitytrails %s ' + 'last seen: %s first seen: %s' % + (domain, record['last_seen'], + record['first_seen']) + } + ) + + return r + + +def __history_dns(results, domain, type_serv, service): + r = [] + + if 'records' in results: + for record in results['records']: + if 'values' in record: + values = record['values'] + if type(values) is list: + + for item in record['values']: + r.append( + {'types': ['domain|ip'], + 'values': [item[type_serv]], + 'categories': ['Network activity'], + 'comment': 'history %s of %s last seen: %s first seen: %s' % + (service, domain, record['last_seen'], + record['first_seen']) + } + ) + else: + r.append( + {'types': ['domain|ip'], + 'values': [values[type_serv]], + 'categories': ['Network activity'], + 'comment': 'history %s of %s last seen: %s first seen: %s' % + (service, domain, record['last_seen'], + record['first_seen']) + } + ) + return r + + +def expand_searching_domain(api, ip): + r = [] + status_ok = False + + try: + results = api.searching_domains(ipv4=ip) + + if results: + if 'records' in results: + res = [(r['host_provider'], r['hostname'], r['whois']) + for r in results['records']] + + for host_provider, hostname, whois in res: + comment = 'domain for %s by %s' % (ip, host_provider[0]) + if whois['registrar']: + comment = comment + ' registrar %s' % whois['registrar'] + + r.append( + { + 'types': ['domain'], + 'category': ['Network activity'], + 'values': [hostname], + 'comment': comment + + } + ) + status_ok = True + except APIError as e: + misperrors['error'] = e.value + return [], False + + return r, status_ok + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo + + +def __select_registrant_item(entry): + res = None + if 'contacts' in entry: + res = list(filter(lambda x: x['type'] == 'registrant', + entry['contacts'])) + + if 'contact' in entry: + res = list(filter(lambda x: x['type'] == 'registrant', + entry['contact'])) + + return res diff --git a/misp_modules/modules/expansion/sigma_queries.py b/misp_modules/modules/expansion/sigma_queries.py new file mode 100644 index 0000000..d263245 --- /dev/null +++ b/misp_modules/modules/expansion/sigma_queries.py @@ -0,0 +1,50 @@ +import sys, os, io, json +try: + from sigma.parser import SigmaCollectionParser + from sigma.config import SigmaConfiguration + from sigma.backends import getBackend, BackendOptions +except ImportError: + print("sigma or yaml is missing, use 'pip3 install sigmatools' to install it.") + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['sigma'], 'output': ['text']} +moduleinfo = {'version': '0.1', 'author': 'Christian Studer', 'module-type': ['expansion', 'hover'], + 'description': 'An expansion hover module to display the result of sigma queries.'} +moduleconfig = [] +sigma_targets = ('es-dsl', 'es-qs', 'graylog', 'kibana', 'xpack-watcher', 'logpoint', 'splunk', 'grep', 'wdatp', 'splunkxml', 'arcsight', 'qualys') + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + if not request.get('sigma'): + misperrors['error'] = 'Sigma rule missing' + return misperrors + config = SigmaConfiguration() + backend_options = BackendOptions(None) + f = io.TextIOWrapper(io.BytesIO(request.get('sigma').encode()), encoding='utf-8') + parser = SigmaCollectionParser(f, config, None) + targets = [] + old_stdout = sys.stdout + result = io.StringIO() + sys.stdout = result + for t in sigma_targets: + backend = getBackend(t)(config, backend_options, None) + try: + parser.generate(backend) + backend.finalize() + print("#NEXT") + targets.append(t) + except: + continue + sys.stdout = old_stdout + results = result.getvalue()[:-5].split('#NEXT') + d_result = {t: r.strip() for t,r in zip(targets, results)} + return {'results': [{'types': mispattributes['output'], 'values': d_result}]} + +def introspection(): + return mispattributes + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/misp_modules/modules/expansion/sigma_syntax_validator.py b/misp_modules/modules/expansion/sigma_syntax_validator.py new file mode 100644 index 0000000..e5cc335 --- /dev/null +++ b/misp_modules/modules/expansion/sigma_syntax_validator.py @@ -0,0 +1,35 @@ +import json +try: + import yaml + from sigma.parser import SigmaParser + from sigma.config import SigmaConfiguration +except ImportError: + print("sigma or yaml is missing, use 'pip3 install sigmatools' to install it.") + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['sigma'], 'output': ['text']} +moduleinfo = {'version': '0.1', 'author': 'Christian Studer', 'module-type': ['expansion', 'hover'], + 'description': 'An expansion hover module to perform a syntax check on sigma rules'} +moduleconfig = [] + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + if not request.get('sigma'): + misperrors['error'] = 'Sigma rule missing' + return misperrors + config = SigmaConfiguration() + try: + parser = SigmaParser(yaml.safe_load(request.get('sigma')), config) + result = ("Syntax valid: {}".format(parser.values)) + except Exception as e: + result = ("Syntax error: {}".format(str(e))) + return {'results': [{'types': mispattributes['output'], 'values': result}]} + +def introspection(): + return mispattributes + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/misp_modules/modules/expansion/stix2_pattern_syntax_validator.py b/misp_modules/modules/expansion/stix2_pattern_syntax_validator.py new file mode 100644 index 0000000..78307a4 --- /dev/null +++ b/misp_modules/modules/expansion/stix2_pattern_syntax_validator.py @@ -0,0 +1,42 @@ +import json +try: + from stix2patterns.validator import run_validator +except ImportError: + print("stix2 patterns python library is missing, use 'pip3 install stix2-patterns' to install it.") + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['stix2-pattern'], 'output': ['text']} +moduleinfo = {'version': '0.1', 'author': 'Christian Studer', 'module-type': ['expansion', 'hover'], + 'description': 'An expansion hover module to perform a syntax check on stix2 patterns.'} +moduleconfig = [] + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + if not request.get('stix2-pattern'): + misperrors['error'] = 'STIX2 pattern missing' + return misperrors + pattern = request.get('stix2-pattern') + syntax_errors = [] + for p in pattern[2:-2].split(' AND '): + syntax_validator = run_validator("[{}]".format(p)) + if syntax_validator: + for error in syntax_validator: + syntax_errors.append(error) + if syntax_errors: + s = 's' if len(syntax_errors) > 1 else '' + s_errors = "" + for error in syntax_errors: + s_errors += "{}\n".format(error[6:]) + result = "Syntax error{}: \n{}".format(s, s_errors[:-1]) + else: + result = "Syntax valid" + return {'results': [{'types': mispattributes['output'], 'values': result}]} + +def introspection(): + return mispattributes + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/misp_modules/modules/expansion/urlscan.py b/misp_modules/modules/expansion/urlscan.py new file mode 100644 index 0000000..31c9230 --- /dev/null +++ b/misp_modules/modules/expansion/urlscan.py @@ -0,0 +1,265 @@ +import json +import requests +import logging +import sys +import time + +log = logging.getLogger('urlscan') +log.setLevel(logging.DEBUG) +ch = logging.StreamHandler(sys.stdout) +ch.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +ch.setFormatter(formatter) +log.addHandler(ch) + +moduleinfo = { + 'version': '0.1', + 'author': 'Dave Johnson', + 'description': 'Module to query urlscan.io', + 'module-type': ['expansion'] +} + +moduleconfig = ['apikey'] +misperrors = {'error': 'Error'} +mispattributes = { + 'input': ['hostname', 'domain', 'url', 'hash'], + 'output': ['hostname', 'domain', 'ip-src', 'ip-dst', 'url', 'text', 'link', 'hash'] +} + + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + if (request.get('config')): + if (request['config'].get('apikey') is None): + misperrors['error'] = 'urlscan apikey is missing' + return misperrors + client = urlscanAPI(request['config']['apikey']) + + r = {'results': []} + + if 'ip-src' in request: + r['results'] += lookup_indicator(client, request['ip-src']) + if 'ip-dst' in request: + r['results'] += lookup_indicator(client, request['ip-dst']) + if 'domain' in request: + r['results'] += lookup_indicator(client, request['domain']) + if 'hostname' in request: + r['results'] += lookup_indicator(client, request['hostname']) + if 'url' in request: + r['results'] += lookup_indicator(client, request['url']) + if 'hash' in request: + r['results'] += lookup_indicator(client, request['hash']) + + # Return any errors generated from lookup to the UI and remove duplicates + + uniq = [] + log.debug(r['results']) + for item in r['results']: + log.debug(item) + if 'error' in item: + misperrors['error'] = item['error'] + return misperrors + if item not in uniq: + uniq.append(item) + r['results'] = uniq + return r + + +def lookup_indicator(client, query): + result = client.search_url(query) + log.debug('RESULTS: ' + json.dumps(result)) + r = [] + misp_comment = "{}: Enriched via the urlscan module".format(query) + + # Determine if the page is reachable + for request in result['data']['requests']: + if request['response'].get('failed'): + if request['response']['failed']['errorText']: + log.debug('The page could not load') + r.append( + {'error': 'Domain could not be resolved: {}'.format(request['response']['failed']['errorText'])}) + + if result.get('page'): + if result['page'].get('domain'): + misp_val = result['page']['domain'] + r.append({'types': 'domain', + 'categories': ['Network activity'], + 'values': misp_val, + 'comment': misp_comment}) + + if result['page'].get('ip'): + misp_val = result['page']['ip'] + r.append({'types': 'ip-dst', + 'categories': ['Network activity'], + 'values': misp_val, + 'comment': misp_comment}) + + if result['page'].get('country'): + misp_val = 'country: ' + result['page']['country'] + if result['page'].get('city'): + misp_val += ', city: ' + result['page']['city'] + r.append({'types': 'text', + 'categories': ['External analysis'], + 'values': misp_val, + 'comment': misp_comment}) + + if result['page'].get('asn'): + misp_val = result['page']['asn'] + r.append({'types': 'AS', 'categories': ['External analysis'], 'values': misp_val, 'comment': misp_comment}) + + if result['page'].get('asnname'): + misp_val = result['page']['asnname'] + r.append({'types': 'text', + 'categories': ['External analysis'], + 'values': misp_val, + 'comment': misp_comment}) + + if result.get('stats'): + if result['stats'].get('malicious'): + log.debug('There is something in results > stats > malicious') + threat_list = set() + + if 'matches' in result['meta']['processors']['gsb']['data']: + for item in result['meta']['processors']['gsb']['data']['matches']: + if item['threatType']: + threat_list.add(item['threatType']) + + threat_list = ', '.join(threat_list) + log.debug('threat_list values are: \'' + threat_list + '\'') + + if threat_list: + misp_val = '{} threat(s) detected'.format(threat_list) + r.append({'types': 'text', + 'categories': ['External analysis'], + 'values': misp_val, + 'comment': misp_comment}) + + if result.get('lists'): + if result['lists'].get('urls'): + for url in result['lists']['urls']: + url = url.lower() + if 'office' in url: + misp_val = "Possible Office-themed phishing" + elif 'o365' in url or '0365' in url: + misp_val = "Possible O365-themed phishing" + elif 'microsoft' in url: + misp_val = "Possible Microsoft-themed phishing" + elif 'paypal' in url: + misp_val = "Possible PayPal-themed phishing" + elif 'onedrive' in url: + misp_val = "Possible OneDrive-themed phishing" + elif 'docusign' in url: + misp_val = "Possible DocuSign-themed phishing" + r.append({'types': 'text', + 'categories': ['External analysis'], + 'values': misp_val, + 'comment': misp_comment}) + + if result.get('task'): + if result['task'].get('reportURL'): + misp_val = result['task']['reportURL'] + r.append({'types': 'link', + 'categories': ['External analysis'], + 'values': misp_val, + 'comment': misp_comment}) + + if result['task'].get('screenshotURL'): + image_url = result['task']['screenshotURL'] + r.append({'types': 'link', + 'categories': ['External analysis'], + 'values': image_url, + 'comment': misp_comment}) + ### TO DO ### + ### Add ability to add an in-line screenshot of the target website into an attribute + # screenshot = requests.get(image_url).content + # r.append({'types': ['attachment'], + # 'categories': ['External analysis'], + # 'values': image_url, + # 'image': str(base64.b64encode(screenshot), 'utf-8'), + # 'comment': 'Screenshot of website'}) + + return r + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo + + +class urlscanAPI(): + def __init__(self, apikey=None, uuid=None): + self.key = apikey + self.uuid = uuid + + def request(self, query): + log.debug('From request function with the parameter: ' + query) + payload = {'url': query} + headers = {'API-Key': self.key, + 'Content-Type': "application/json", + 'Cache-Control': "no-cache"} + + # Troubleshooting problems with initial search request + log.debug('PAYLOAD: ' + json.dumps(payload)) + log.debug('HEADERS: ' + json.dumps(headers)) + + search_url_string = "https://urlscan.io/api/v1/scan/" + response = requests.request("POST", + search_url_string, + data=json.dumps(payload), + headers=headers) + + # HTTP 400 - Bad Request + if response.status_code == 400: + raise Exception('HTTP Error 400 - Bad Request') + + # HTTP 404 - Not found + if response.status_code == 404: + raise Exception('HTTP Error 404 - These are not the droids you\'re looking for') + + # Any other status code + if response.status_code != 200: + raise Exception('HTTP Error ' + str(response.status_code)) + + if response.text: + response = json.loads(response.content.decode("utf-8")) + time.sleep(3) + self.uuid = response['uuid'] + + # Strings for to check for errors on the results page + # Null response string for any unavailable resources + null_response_string = '"status": 404' + # Redirect string accounting for 301/302/303/307/308 status codes + redirect_string = '"status": 30' + # Normal response string with 200 status code + normal_response_string = '"status": 200' + + results_url_string = "https://urlscan.io/api/v1/result/" + self.uuid + log.debug('Results URL: ' + results_url_string) + + # Need to wait for results to process and check if they are valid + tries = 10 + while tries >= 0: + results = requests.request("GET", results_url_string) + log.debug('Made a GET request') + results = results.content.decode("utf-8") + # checking if there is a 404 status code and no available resources + if null_response_string in results and \ + redirect_string not in results and \ + normal_response_string not in results: + log.debug('Results not processed. Please check again later.') + time.sleep(3) + tries -= 1 + else: + return json.loads(results) + + raise Exception('Results contained a 404 status error and could not be processed.') + + def search_url(self, query): + log.debug('From search_url with parameter: ' + query) + return self.request(query) diff --git a/misp_modules/modules/expansion/virustotal.py b/misp_modules/modules/expansion/virustotal.py old mode 100755 new mode 100644 index 3997ee6..d92ede1 --- a/misp_modules/modules/expansion/virustotal.py +++ b/misp_modules/modules/expansion/virustotal.py @@ -2,201 +2,168 @@ import json import requests from requests import HTTPError import base64 +from collections import defaultdict misperrors = {'error': 'Error'} mispattributes = {'input': ['hostname', 'domain', "ip-src", "ip-dst", "md5", "sha1", "sha256", "sha512"], 'output': ['domain', "ip-src", "ip-dst", "text", "md5", "sha1", "sha256", "sha512", "ssdeep", - "authentihash", "filename"] - } + "authentihash", "filename"]} # possible module-types: 'expansion', 'hover' or both -moduleinfo = {'version': '2', 'author': 'Hannah Ward', +moduleinfo = {'version': '3', 'author': 'Hannah Ward', 'description': 'Get information from virustotal', 'module-type': ['expansion']} # config fields that your code expects from the site admin moduleconfig = ["apikey", "event_limit"] -limit = 5 # Default -comment = '%s: Enriched via VT' +comment = '{}: Enriched via VirusTotal' +hash_types = ["md5", "sha1", "sha256", "sha512"] +class VirusTotalRequest(object): + def __init__(self, config): + self.apikey = config['apikey'] + self.limit = int(config.get('event_limit', 5)) + self.base_url = "https://www.virustotal.com/vtapi/v2/{}/report" + self.results = defaultdict(set) + self.to_return = [] + self.input_types_mapping = {'ip-src': self.get_ip, 'ip-dst': self.get_ip, + 'domain': self.get_domain, 'hostname': self.get_domain, + 'md5': self.get_hash, 'sha1': self.get_hash, + 'sha256': self.get_hash, 'sha512': self.get_hash} + self.output_types_mapping = {'submission_names': 'filename', 'ssdeep': 'ssdeep', + 'authentihash': 'authentihash', 'ITW_urls': 'url'} + + def parse_request(self, q): + req_values = set() + for attribute_type, attribute_value in q.items(): + req_values.add(attribute_value) + try: + error = self.input_types_mapping[attribute_type](attribute_value) + except KeyError: + continue + if error is not None: + return error + for key, values in self.results.items(): + values = values.difference(req_values) + if values: + if isinstance(key, tuple): + types, comment = key + self.to_return.append({'types': list(types), 'values': list(values), 'comment': comment}) + else: + self.to_return.append({'types': key, 'values': list(values)}) + return self.to_return + + def get_domain(self, domain, do_not_recurse=False): + req = requests.get(self.base_url.format('domain'), params={'domain': domain, 'apikey': self.apikey}) + try: + req.raise_for_status() + req = req.json() + except HTTPError as e: + return str(e) + if req["response_code"] == 0: + # Nothing found + return [] + if "resolutions" in req: + for res in req["resolutions"][:self.limit]: + ip_address = res["ip_address"] + self.results[(("ip-dst", "ip-src"), comment.format(domain))].add(ip_address) + # Pivot from here to find all domain info + if not do_not_recurse: + error = self.get_ip(ip_address, True) + if error is not None: + return error + self.get_more_info(req) + + def get_hash(self, _hash): + req = requests.get(self.base_url.format('file'), params={'resource': _hash, 'apikey': self.apikey, 'allinfo': 1}) + try: + req.raise_for_status() + req = req.json() + except HTTPError as e: + return str(e) + if req["response_code"] == 0: + # Nothing found + return [] + self.get_more_info(req) + + def get_ip(self, ip, do_not_recurse=False): + req = requests.get(self.base_url.format('ip-address'), params={'ip': ip, 'apikey': self.apikey}) + try: + req.raise_for_status() + req = req.json() + except HTTPError as e: + return str(e) + if req["response_code"] == 0: + # Nothing found + return [] + if "resolutions" in req: + for res in req["resolutions"][:self.limit]: + hostname = res["hostname"] + self.results[(("domain",), comment.format(ip))].add(hostname) + # Pivot from here to find all domain info + if not do_not_recurse: + error = self.get_domain(hostname, True) + if error is not None: + return error + self.get_more_info(req) + + def find_all(self, data): + hashes = [] + if isinstance(data, dict): + for key, value in data.items(): + if key in hash_types: + self.results[key].add(value) + hashes.append(value) + else: + if isinstance(value, (dict, list)): + hashes.extend(self.find_all(value)) + elif isinstance(data, list): + for d in data: + hashes.extend(self.find_all(d)) + return hashes + + def get_more_info(self, req): + # Get all hashes first + hashes = self.find_all(req) + for h in hashes[:self.limit]: + # Search VT for some juicy info + try: + data = requests.get(self.base_url.format('file'), params={'resource': h, 'apikey': self.apikey, 'allinfo': 1}).json() + except Exception: + continue + # Go through euch key and check if it exists + for VT_type, MISP_type in self.output_types_mapping.items(): + if VT_type in data: + try: + self.results[((MISP_type,), comment.format(h))].add(data[VT_type]) + except TypeError: + self.results[((MISP_type,), comment.format(h))].update(data[VT_type]) + # Get the malware sample + sample = requests.get(self.base_url[:-6].format('file/download'), params={'hash': h, 'apikey': self.apikey}) + malsample = sample.content + # It is possible for VT to not give us any submission names + if "submission_names" in data: + self.to_return.append({"types": ["malware-sample"], "categories": ["Payload delivery"], + "values": data["submimssion_names"], "data": str(base64.b64encore(malsample), 'utf-8')}) def handler(q=False): - global limit if q is False: return False - q = json.loads(q) - - key = q["config"]["apikey"] - limit = int(q["config"].get("event_limit", 5)) - - r = {"results": []} - - if "ip-src" in q: - r["results"] += getIP(q["ip-src"], key) - if "ip-dst" in q: - r["results"] += getIP(q["ip-dst"], key) - if "domain" in q: - r["results"] += getDomain(q["domain"], key) - if 'hostname' in q: - r["results"] += getDomain(q['hostname'], key) - if 'md5' in q: - r["results"] += getHash(q['md5'], key) - if 'sha1' in q: - r["results"] += getHash(q['sha1'], key) - if 'sha256' in q: - r["results"] += getHash(q['sha256'], key) - if 'sha512' in q: - r["results"] += getHash(q['sha512'], key) - - uniq = [] - for res in r["results"]: - if res not in uniq: - uniq.append(res) - r["results"] = uniq - return r - - -def getHash(hash, key, do_not_recurse=False): - req = requests.get("https://www.virustotal.com/vtapi/v2/file/report", - params={"allinfo": 1, "apikey": key, 'resource': hash}) - try: - req.raise_for_status() - req = req.json() - except HTTPError as e: - misperrors['error'] = str(e) + if not q.get('config') or not q['config'].get('apikey'): + misperrors['error'] = "A VirusTotal api key is required for this module." return misperrors - - if req["response_code"] == 0: - # Nothing found - return [] - - return getMoreInfo(req, key) - - -def getIP(ip, key, do_not_recurse=False): - global limit - toReturn = [] - req = requests.get("https://www.virustotal.com/vtapi/v2/ip-address/report", - params={"ip": ip, "apikey": key}) - try: - req.raise_for_status() - req = req.json() - except HTTPError as e: - misperrors['error'] = str(e) + del q['module'] + query = VirusTotalRequest(q.pop('config')) + r = query.parse_request(q) + if isinstance(r, str): + misperrors['error'] = r return misperrors - - if req["response_code"] == 0: - # Nothing found - return [] - - if "resolutions" in req: - for res in req["resolutions"][:limit]: - toReturn.append({"types": ["domain"], "values": [res["hostname"]], "comment": comment % ip}) - # Pivot from here to find all domain info - if not do_not_recurse: - toReturn += getDomain(res["hostname"], key, True) - - toReturn += getMoreInfo(req, key) - return toReturn - - -def getDomain(domain, key, do_not_recurse=False): - global limit - toReturn = [] - req = requests.get("https://www.virustotal.com/vtapi/v2/domain/report", - params={"domain": domain, "apikey": key}) - try: - req.raise_for_status() - req = req.json() - except HTTPError as e: - misperrors['error'] = str(e) - return misperrors - - if req["response_code"] == 0: - # Nothing found - return [] - - if "resolutions" in req: - for res in req["resolutions"][:limit]: - toReturn.append({"types": ["ip-dst", "ip-src"], "values": [res["ip_address"]], "comment": comment % domain}) - # Pivot from here to find all info on IPs - if not do_not_recurse: - toReturn += getIP(res["ip_address"], key, True) - if "subdomains" in req: - for subd in req["subdomains"]: - toReturn.append({"types": ["domain"], "values": [subd], "comment": comment % domain}) - toReturn += getMoreInfo(req, key) - return toReturn - - -def findAll(data, keys): - a = [] - if isinstance(data, dict): - for key in data.keys(): - if key in keys: - a.append(data[key]) - else: - if isinstance(data[key], (dict, list)): - a += findAll(data[key], keys) - if isinstance(data, list): - for i in data: - a += findAll(i, keys) - - return a - - -def getMoreInfo(req, key): - global limit - r = [] - # Get all hashes first - hashes = [] - hashes = findAll(req, ["md5", "sha1", "sha256", "sha512"]) - r.append({"types": ["freetext"], "values": hashes}) - for hsh in hashes[:limit]: - # Search VT for some juicy info - try: - data = requests.get("http://www.virustotal.com/vtapi/v2/file/report", - params={"allinfo": 1, "apikey": key, "resource": hsh} - ).json() - except: - continue - - # Go through each key and check if it exists - if "submission_names" in data: - r.append({'types': ["filename"], "values": data["submission_names"], "comment": comment % hsh}) - - if "ssdeep" in data: - r.append({'types': ["ssdeep"], "values": [data["ssdeep"]], "comment": comment % hsh}) - - if "authentihash" in data: - r.append({"types": ["authentihash"], "values": [data["authentihash"]], "comment": comment % hsh}) - - if "ITW_urls" in data: - r.append({"types": ["url"], "values": data["ITW_urls"], "comment": comment % hsh}) - - # Get the malware sample - sample = requests.get("https://www.virustotal.com/vtapi/v2/file/download", - params={"hash": hsh, "apikey": key}) - - malsample = sample.content - - # It is possible for VT to not give us any submission names - if "submission_names" in data: - r.append({"types": ["malware-sample"], - "categories": ["Payload delivery"], - "values": data["submission_names"], - "data": str(base64.b64encode(malsample), 'utf-8') - } - ) - - return r - + return {'results': r} def introspection(): return mispattributes - def version(): moduleinfo['config'] = moduleconfig return moduleinfo diff --git a/misp_modules/modules/expansion/vulners.py b/misp_modules/modules/expansion/vulners.py new file mode 100644 index 0000000..49cb9aa --- /dev/null +++ b/misp_modules/modules/expansion/vulners.py @@ -0,0 +1,65 @@ +import json +import requests +import vulners + +misperrors = {'error': 'Error'} +mispattributes = {'input': ['vulnerability'], 'output': ['text']} +moduleinfo = {'version': '0.1', 'author': 'Igor Ivanov', 'description': 'An expansion hover module to expand information about CVE id using Vulners API.', 'module-type': ['hover']} + +# Get API key from https://vulners.com/userinfo +moduleconfig = ["apikey"] + + +def handler(q=False): + if q is False: + return False + request = json.loads(q) + if not request.get('vulnerability'): + misperrors['error'] = 'Vulnerability id missing' + return misperrors + + ai_summary = '' + exploit_summary = '' + vuln_summary = '' + + key = request['config'].get('apikey') + vulners_api = vulners.Vulners(api_key=key) + vulnerability = request.get('vulnerability') + vulners_document = vulners_api.document(vulnerability) + + # Get AI scoring from the document if it's already calculated + # There is no need to call AI Scoring method + if 'score' in vulners_document.get('enchantments', {}): + vulners_ai_score = vulners_document['enchantments']['score']['value'] + else: + vulners_ai_score = None + + vulners_exploits = vulners_api.searchExploit(vulnerability) + + if vulners_document: + vuln_summary += vulners_document.get('description') + else: + vuln_summary += 'Non existing CVE' + + if vulners_ai_score: + ai_summary += 'Vulners AI Score is ' + str(vulners_ai_score[0]) + " " + + if vulners_exploits: + exploit_summary += " || " + str(len(vulners_exploits[0])) + " Public exploits available:\n " + for exploit in vulners_exploits[0]: + exploit_summary += exploit['title'] + " " + exploit['href'] + "\n " + exploit_summary += "|| Vulnerability Description: " + vuln_summary + + summary = ai_summary + exploit_summary + vuln_summary + + r = {'results': [{'types': mispattributes['output'], 'values': summary}]} + return r + + +def introspection(): + return mispattributes + + +def version(): + moduleinfo['config'] = moduleconfig + return moduleinfo diff --git a/misp_modules/modules/expansion/yara_syntax_validator.py b/misp_modules/modules/expansion/yara_syntax_validator.py index 4953c41..c68d934 100644 --- a/misp_modules/modules/expansion/yara_syntax_validator.py +++ b/misp_modules/modules/expansion/yara_syntax_validator.py @@ -2,7 +2,7 @@ import json import requests try: import yara -except: +except (OSError, ImportError): print("yara is missing, use 'pip3 install yara' to install it.") misperrors = {'error': 'Error'} diff --git a/misp_modules/modules/export_mod/__init__.py b/misp_modules/modules/export_mod/__init__.py index d2407a4..7e546cd 100644 --- a/misp_modules/modules/export_mod/__init__.py +++ b/misp_modules/modules/export_mod/__init__.py @@ -1 +1 @@ -__all__ = ['testexport','cef_export','liteexport','goamlexport','threat_connect_export','pdfexport','threatStream_misp_export', 'osqueryexport'] +__all__ = ['cef_export','liteexport','goamlexport','threat_connect_export','pdfexport','threatStream_misp_export', 'osqueryexport'] \ No newline at end of file diff --git a/misp_modules/modules/export_mod/goamlexport.py b/misp_modules/modules/export_mod/goamlexport.py index c277640..f09f2e6 100644 --- a/misp_modules/modules/export_mod/goamlexport.py +++ b/misp_modules/modules/export_mod/goamlexport.py @@ -1,4 +1,5 @@ -import json, datetime, base64 +import json +import base64 from pymisp import MISPEvent from collections import defaultdict, Counter @@ -67,7 +68,7 @@ class GoAmlGeneration(object): try: report_code.append(obj.get_attributes_by_relation('report-code')[0].value.split(' ')[0]) currency_code.append(obj.get_attributes_by_relation('currency-code')[0].value) - except: + except IndexError: print('report_code or currency_code error') self.uuids, self.report_codes, self.currency_codes = uuids, report_code, currency_code @@ -87,9 +88,12 @@ class GoAmlGeneration(object): person_to_parse = [person_uuid for person_uuid in self.uuids.get('person') if person_uuid not in self.parsed_uuids.get('person')] if len(person_to_parse) == 1: self.itterate('person', 'reporting_person', person_to_parse[0], 'header') - location_to_parse = [location_uuid for location_uuid in self.uuids.get('geolocation') if location_uuid not in self.parsed_uuids.get('geolocation')] - if len(location_to_parse) == 1: - self.itterate('geolocation', 'location', location_to_parse[0], 'header') + try: + location_to_parse = [location_uuid for location_uuid in self.uuids.get('geolocation') if location_uuid not in self.parsed_uuids.get('geolocation')] + if len(location_to_parse) == 1: + self.itterate('geolocation', 'location', location_to_parse[0], 'header') + except TypeError: + pass self.xml['data'] += "" def itterate(self, object_type, aml_type, uuid, xml_part): diff --git a/misp_modules/modules/import_mod/__init__.py b/misp_modules/modules/import_mod/__init__.py index 886eaf7..5190abb 100644 --- a/misp_modules/modules/import_mod/__init__.py +++ b/misp_modules/modules/import_mod/__init__.py @@ -1,4 +1,3 @@ from . import _vmray -__all__ = ['vmray_import', 'testimport', 'ocr', 'stiximport', 'cuckooimport', 'goamlimport', - 'email_import', 'mispjson', 'openiocimport', 'threatanalyzer_import', 'csvimport'] +__all__ = ['vmray_import', 'ocr', 'cuckooimport', 'goamlimport', 'email_import', 'mispjson', 'openiocimport', 'threatanalyzer_import', 'csvimport'] diff --git a/misp_modules/modules/import_mod/ocr.py b/misp_modules/modules/import_mod/ocr.py index aafe653..f14212b 100755 --- a/misp_modules/modules/import_mod/ocr.py +++ b/misp_modules/modules/import_mod/ocr.py @@ -1,16 +1,24 @@ +import sys import json import base64 - -from PIL import Image - -from pytesseract import image_to_string from io import BytesIO + +import logging + +log = logging.getLogger('ocr') +log.setLevel(logging.DEBUG) +ch = logging.StreamHandler(sys.stdout) +ch.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +ch.setFormatter(formatter) +log.addHandler(ch) + misperrors = {'error': 'Error'} -userConfig = { }; +userConfig = {}; inputSource = ['file'] -moduleinfo = {'version': '0.1', 'author': 'Alexandre Dulaunoy', +moduleinfo = {'version': '0.2', 'author': 'Alexandre Dulaunoy', 'description': 'Optical Character Recognition (OCR) module for MISP', 'module-type': ['import']} @@ -18,14 +26,61 @@ moduleconfig = [] def handler(q=False): + # try to import modules and return errors if module not found + try: + from PIL import Image + except ImportError: + misperrors['error'] = "Please pip(3) install pillow" + return misperrors + + try: + # Official ImageMagick module + from wand.image import Image as WImage + except ImportError: + misperrors['error'] = "Please pip(3) install wand" + return misperrors + + try: + from pytesseract import image_to_string + except ImportError: + misperrors['error'] = "Please pip(3) install pytesseract" + return misperrors + if q is False: return False r = {'results': []} request = json.loads(q) - image = base64.b64decode(request["data"]) + document = base64.b64decode(request["data"]) + document = WImage(blob=document) + if document.format == 'PDF': + with document as pdf: + # Get number of pages + pages=len(pdf.sequence) + log.debug("PDF with {} page(s) detected".format(pages)) + # Create new image object where the height will be the number of pages. With huge PDFs this will overflow, break, consume silly memory etc… + img = WImage(width=pdf.width, height=pdf.height * pages) + # Cycle through pages and stitch it together to one big file + for p in range(pages): + log.debug("Stitching page {}".format(p+1)) + image = img.composite(pdf.sequence[p], top=pdf.height * p, left=0) + # Create a png blob + image = img.make_blob('png') + log.debug("Final image size is {}x{}".format(pdf.width, pdf.height*(p+1))) + else: + image = document + image_file = BytesIO(image) image_file.seek(0) - ocrized = image_to_string(Image.open(image_file)) + + try: + im = Image.open(image_file) + except IOError: + misperrors['error'] = "Corrupt or not an image file." + return misperrors + + + ocrized = image_to_string(im) + freetext = {} freetext['values'] = ocrized freetext['types'] = ['freetext'] diff --git a/misp_modules/modules/import_mod/threatanalyzer_import.py b/misp_modules/modules/import_mod/threatanalyzer_import.py index 757f849..2e3a507 100755 --- a/misp_modules/modules/import_mod/threatanalyzer_import.py +++ b/misp_modules/modules/import_mod/threatanalyzer_import.py @@ -15,7 +15,7 @@ misperrors = {'error': 'Error'} userConfig = {} inputSource = ['file'] -moduleinfo = {'version': '0.7', 'author': 'Christophe Vandeplas', +moduleinfo = {'version': '0.9', 'author': 'Christophe Vandeplas', 'description': 'Import for ThreatAnalyzer archive.zip/analysis.json files', 'module-type': ['import']} @@ -45,16 +45,21 @@ def handler(q=False): if re.match(r"Analysis/proc_\d+/modified_files/mapping\.log", zip_file_name): with zf.open(zip_file_name, mode='r', pwd=None) as fp: file_data = fp.read() - for line in file_data.decode().split('\n'): - if line: + for line in file_data.decode("utf-8", 'ignore').split('\n'): + if not line: + continue + if line.count('|') == 3: l_fname, l_size, l_md5, l_created = line.split('|') - l_fname = cleanup_filepath(l_fname) - if l_fname: - if l_size == 0: - pass # FIXME create an attribute for the filename/path - else: - # file is a non empty sample, upload the sample later - modified_files_mapping[l_md5] = l_fname + if line.count('|') == 4: + l_fname, l_size, l_md5, l_sha256, l_created = line.split('|') + l_fname = cleanup_filepath(l_fname) + if l_fname: + if l_size == 0: + results.append({'values': l_fname, 'type': 'filename', 'to_ids': True, + 'categories': ['Artifacts dropped', 'Payload delivery'], 'comment': ''}) + else: + # file is a non empty sample, upload the sample later + modified_files_mapping[l_md5] = l_fname # now really process the data for zip_file_name in zf.namelist(): # Get all files in the zip file @@ -84,7 +89,7 @@ def handler(q=False): results.append({ 'values': sample_filename, 'data': base64.b64encode(file_data).decode(), - 'type': 'malware-sample', 'categories': ['Artifacts dropped', 'Payload delivery'], 'to_ids': True, 'comment': ''}) + 'type': 'malware-sample', 'categories': ['Payload delivery', 'Artifacts dropped'], 'to_ids': True, 'comment': ''}) except Exception as e: # no 'sample' in archive, might be an url analysis, just ignore pass @@ -109,7 +114,15 @@ def process_analysis_json(analysis_json): for process in analysis_json['analysis']['processes']['process']: # print_json(process) if 'connection_section' in process and 'connection' in process['connection_section']: + # compensate for absurd behavior of the data format: if one entry = immediately the dict, if multiple entries = list containing dicts + # this will always create a list, even with only one item + if isinstance(process['connection_section']['connection'], dict): + process['connection_section']['connection'] = [process['connection_section']['connection']] + # iterate over each entry for connection_section_connection in process['connection_section']['connection']: + if 'name_to_ip' in connection_section_connection: # TA 6.1 data format + connection_section_connection['@remote_ip'] = connection_section_connection['name_to_ip']['@result_addresses'] + connection_section_connection['@remote_hostname'] = connection_section_connection['name_to_ip']['@request_name'] connection_section_connection['@remote_ip'] = cleanup_ip(connection_section_connection['@remote_ip']) connection_section_connection['@remote_hostname'] = cleanup_hostname(connection_section_connection['@remote_hostname']) @@ -120,7 +133,7 @@ def process_analysis_json(analysis_json): # connection_section_connection['@remote_hostname'], # connection_section_connection['@remote_ip']) # ) - yield({'values': val, 'type': 'domain|ip', 'categories': 'Network activity', 'to_ids': True, 'comment': ''}) + yield({'values': val, 'type': 'domain|ip', 'categories': ['Network activity'], 'to_ids': True, 'comment': ''}) elif connection_section_connection['@remote_ip']: # print("connection_section_connection ip-dst: {} IDS:yes".format( # connection_section_connection['@remote_ip']) @@ -134,18 +147,18 @@ def process_analysis_json(analysis_json): if 'http_command' in connection_section_connection: for http_command in connection_section_connection['http_command']: # print('connection_section_connection HTTP COMMAND: {}\t{}'.format( - # http_command['@method'], # comment - # http_command['@url']) # url + # connection_section_connection['http_command']['@method'], # comment + # connection_section_connection['http_command']['@url']) # url # ) val = cleanup_url(http_command['@url']) if val: - yield({'values': val, 'type': 'url', 'categories': 'Network activity', 'to_ids': True, 'comment': http_command['@method']}) + yield({'values': val, 'type': 'url', 'categories': ['Network activity'], 'to_ids': True, 'comment': http_command['@method']}) if 'http_header' in connection_section_connection: for http_header in connection_section_connection['http_header']: if 'User-Agent:' in http_header['@header']: val = http_header['@header'][len('User-Agent: '):] - yield({'values': val, 'type': 'user-agent', 'categories': 'Network activity', 'to_ids': False, 'comment': ''}) + yield({'values': val, 'type': 'user-agent', 'categories': ['Network activity'], 'to_ids': False, 'comment': ''}) elif 'Host:' in http_header['@header']: val = http_header['@header'][len('Host: '):] if ':' in val: @@ -158,7 +171,7 @@ def process_analysis_json(analysis_json): if val_hostname and val_port: val_combined = '{}|{}'.format(val_hostname, val_port) # print({'values': val_combined, 'type': 'hostname|port', 'to_ids': True, 'comment': ''}) - yield({'values': val_combined, 'type': 'hostname|port', 'to_ids': True, 'comment': ''}) + yield({'values': val_combined, 'type': 'hostname|port', 'categories': ['Network activity'], 'to_ids': True, 'comment': ''}) elif val_ip and val_port: val_combined = '{}|{}'.format(val_ip, val_port) # print({'values': val_combined, 'type': 'ip-dst|port', 'to_ids': True, 'comment': ''}) @@ -203,7 +216,7 @@ def process_analysis_json(analysis_json): # networkoperation_section_dns_request_by_name['@request_name'], # networkoperation_section_dns_request_by_name['@result_addresses']) # ) - yield({'values': val, 'type': 'domain|ip', 'categories': 'Network activity', 'to_ids': True, 'comment': ''}) + yield({'values': val, 'type': 'domain|ip', 'categories': ['Network activity'], 'to_ids': True, 'comment': ''}) elif networkoperation_section_dns_request_by_name['@request_name']: # print("networkoperation_section_dns_request_by_name hostname: {} IDS:yes".format( # networkoperation_section_dns_request_by_name['@request_name']) @@ -227,14 +240,14 @@ def process_analysis_json(analysis_json): # networkpacket_section_connect_to_computer['@remote_port']) # ) val_combined = "{}|{}".format(networkpacket_section_connect_to_computer['@remote_hostname'], networkpacket_section_connect_to_computer['@remote_ip']) - yield({'values': val_combined, 'type': 'hostname|ip', 'to_ids': True, 'comment': ''}) + yield({'values': val_combined, 'type': 'domain|ip', 'to_ids': True, 'comment': ''}) elif networkpacket_section_connect_to_computer['@remote_hostname']: # print("networkpacket_section_connect_to_computer hostname: {} IDS:yes COMMENT:port {}".format( # networkpacket_section_connect_to_computer['@remote_hostname'], # networkpacket_section_connect_to_computer['@remote_port']) # ) val_combined = "{}|{}".format(networkpacket_section_connect_to_computer['@remote_hostname'], networkpacket_section_connect_to_computer['@remote_port']) - yield({'values': val_combined, 'type': 'hostname|port', 'to_ids': True, 'comment': ''}) + yield({'values': val_combined, 'type': 'hostname|port', 'categories': ['Network activity'], 'to_ids': True, 'comment': ''}) elif networkpacket_section_connect_to_computer['@remote_ip']: # print("networkpacket_section_connect_to_computer ip-dst: {} IDS:yes COMMENT:port {}".format( # networkpacket_section_connect_to_computer['@remote_ip'], @@ -442,9 +455,9 @@ def cleanup_filepath(item): '\\AppData\\Roaming\\Adobe\\Acrobat\\9.0\\UserCache.bin', '\\AppData\\Roaming\\Macromedia\\Flash Player\\macromedia.com\\support\\flashplayer\\sys\\settings.sol', - '\\AppData\\Roaming\Adobe\\Flash Player\\NativeCache\\', + '\\AppData\\Roaming\\Adobe\\Flash Player\\NativeCache\\', 'C:\\Windows\\AppCompat\\Programs\\', - 'C:\~' # caused by temp file created by MS Office when opening malicious doc/xls/... + 'C:\\~' # caused by temp file created by MS Office when opening malicious doc/xls/... } if list_in_string(noise_substrings, item): return None