mirror of https://github.com/MISP/misp-modules
				
				
				
			
		
			
				
	
	
		
			235 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
			
		
		
	
	
			235 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
# -*- coding: utf-8 -*-
 | 
						|
 | 
						|
import json
 | 
						|
try:
 | 
						|
    import shodan
 | 
						|
except ImportError:
 | 
						|
    print("shodan module not installed.")
 | 
						|
from . import check_input_attribute, standard_error_message
 | 
						|
from datetime import datetime
 | 
						|
from pymisp import MISPAttribute, MISPEvent, MISPObject
 | 
						|
 | 
						|
misperrors = {'error': 'Error'}
 | 
						|
mispattributes = {'input': ['ip-src', 'ip-dst'],
 | 
						|
                  'format': 'misp_standard'}
 | 
						|
moduleinfo = {'version': '0.2', 'author': 'Raphaël Vinot',
 | 
						|
              'description': 'Query on Shodan',
 | 
						|
              'module-type': ['expansion']}
 | 
						|
 | 
						|
moduleconfig = ['apikey']
 | 
						|
 | 
						|
 | 
						|
class ShodanParser():
 | 
						|
    def __init__(self, attribute):
 | 
						|
        self.misp_event = MISPEvent()
 | 
						|
        self.attribute = MISPAttribute()
 | 
						|
        self.attribute.from_dict(**attribute)
 | 
						|
        self.misp_event.add_attribute(**self.attribute)
 | 
						|
        self.ip_address_mapping = {
 | 
						|
            'asn': {'type': 'AS', 'object_relation': 'asn'},
 | 
						|
            'city': {'type': 'text', 'object_relation': 'city'},
 | 
						|
            'country_code': {'type': 'text', 'object_relation': 'country-code'},
 | 
						|
            'country_name': {'type': 'text', 'object_relation': 'country'},
 | 
						|
            'isp': {'type': 'text', 'object_relation': 'ISP'},
 | 
						|
            'latitude': {'type': 'float', 'object_relation': 'latitude'},
 | 
						|
            'longitude': {'type': 'float', 'object_relation': 'longitude'},
 | 
						|
            'org': {'type': 'text', 'object_relation': 'organization'},
 | 
						|
            'postal_code': {'type': 'text', 'object_relation': 'zipcode'},
 | 
						|
            'region_code': {'type': 'text', 'object_relation': 'region-code'}
 | 
						|
        }
 | 
						|
        self.ip_port_mapping = {
 | 
						|
            'domains': {'type': 'domain', 'object_relation': 'domain'},
 | 
						|
            'hostnames': {'type': 'hostname', 'object_relation': 'hostname'}
 | 
						|
        }
 | 
						|
        self.vulnerability_mapping = {
 | 
						|
            'cvss': {'type': 'float', 'object_relation': 'cvss-score'},
 | 
						|
            'summary': {'type': 'text', 'object_relation': 'summary'}
 | 
						|
        }
 | 
						|
        self.x509_mapping = {
 | 
						|
            'bits': {'type': 'text', 'object_relation': 'pubkey-info-size'},
 | 
						|
            'expires': {'type': 'datetime', 'object_relation': 'validity-not-after'},
 | 
						|
            'issued': {'type': 'datetime', 'object_relation': 'validity-not-before'},
 | 
						|
            'issuer': {'type': 'text', 'object_relation': 'issuer'},
 | 
						|
            'serial': {'type': 'text', 'object_relation': 'serial-number'},
 | 
						|
            'sig_alg': {'type': 'text', 'object_relation': 'signature_algorithm'},
 | 
						|
            'subject': {'type': 'text', 'object_relation': 'subject'},
 | 
						|
            'type': {'type': 'text', 'object_relation': 'pubkey-info-algorithm'},
 | 
						|
            'version': {'type': 'text', 'object_relation': 'version'}
 | 
						|
        }
 | 
						|
 | 
						|
    def query_shodan(self, apikey):
 | 
						|
        # Query Shodan and get the results in a json blob
 | 
						|
        api = shodan.Shodan(apikey)
 | 
						|
        query_results = api.host(self.attribute.value)
 | 
						|
 | 
						|
        # Parse the information about the IP address used as input
 | 
						|
        ip_address_attributes = []
 | 
						|
        for feature, mapping in self.ip_address_mapping.items():
 | 
						|
            if query_results.get(feature):
 | 
						|
                attribute = {'value': query_results[feature]}
 | 
						|
                attribute.update(mapping)
 | 
						|
                ip_address_attributes.append(attribute)
 | 
						|
        if ip_address_attributes:
 | 
						|
            ip_address_object = MISPObject('ip-api-address')
 | 
						|
            for attribute in ip_address_attributes:
 | 
						|
                ip_address_object.add_attribute(**attribute)
 | 
						|
            ip_address_object.add_reference(self.attribute.uuid, 'describes')
 | 
						|
            self.misp_event.add_object(ip_address_object)
 | 
						|
 | 
						|
        # Parse the hostnames / domains and ports associated with the IP address
 | 
						|
        if query_results.get('ports'):
 | 
						|
            ip_port_object = MISPObject('ip-port')
 | 
						|
            ip_port_object.add_attribute(**self._get_source_attribute())
 | 
						|
            feature = self.attribute.type.split('-')[1]
 | 
						|
            for port in query_results['ports']:
 | 
						|
                attribute = {
 | 
						|
                    'type': 'port',
 | 
						|
                    'object_relation': f'{feature}-port',
 | 
						|
                    'value': port
 | 
						|
                }
 | 
						|
                ip_port_object.add_attribute(**attribute)
 | 
						|
            for feature, mapping in self.ip_port_mapping.items():
 | 
						|
                for value in query_results.get(feature, []):
 | 
						|
                    attribute = {'value': value}
 | 
						|
                    attribute.update(mapping)
 | 
						|
                    ip_port_object.add_attribute(**attribute)
 | 
						|
            ip_port_object.add_reference(self.attribute.uuid, 'extends')
 | 
						|
            self.misp_event.add_object(ip_port_object)
 | 
						|
        else:
 | 
						|
            if any(query_results.get(feature) for feature in ('domains', 'hostnames')):
 | 
						|
                domain_ip_object = MISPObject('domain-ip')
 | 
						|
                domain_ip_object.add_attribute(**self._get_source_attribute())
 | 
						|
                for feature in ('domains', 'hostnames'):
 | 
						|
                    for value in query_results[feature]:
 | 
						|
                        attribute = {
 | 
						|
                            'type': 'domain',
 | 
						|
                            'object_relation': 'domain',
 | 
						|
                            'value': value
 | 
						|
                        }
 | 
						|
                        domain_ip_object.add_attribute(**attribute)
 | 
						|
                domain_ip_object.add_reference(self.attribute.uuid, 'extends')
 | 
						|
                self.misp_event.add_object(domain_ip_object)
 | 
						|
 | 
						|
        # Parse data within the "data" field
 | 
						|
        if query_results.get('vulns'):
 | 
						|
            vulnerabilities = {}
 | 
						|
            for data in query_results['data']:
 | 
						|
                # Parse vulnerabilities
 | 
						|
                if data.get('vulns'):
 | 
						|
                    for cve, vulnerability in data['vulns'].items():
 | 
						|
                        if cve not in vulnerabilities:
 | 
						|
                            vulnerabilities[cve] = vulnerability
 | 
						|
                # Also parse the certificates
 | 
						|
                if data.get('ssl'):
 | 
						|
                    self._parse_cert(data['ssl'])
 | 
						|
            for cve, vulnerability in vulnerabilities.items():
 | 
						|
                vulnerability_object = MISPObject('vulnerability')
 | 
						|
                vulnerability_object.add_attribute(**{
 | 
						|
                    'type': 'vulnerability',
 | 
						|
                    'object_relation': 'id',
 | 
						|
                    'value': cve
 | 
						|
                })
 | 
						|
                for feature, mapping in self.vulnerability_mapping.items():
 | 
						|
                    if vulnerability.get(feature):
 | 
						|
                        attribute = {'value': vulnerability[feature]}
 | 
						|
                        attribute.update(mapping)
 | 
						|
                        vulnerability_object.add_attribute(**attribute)
 | 
						|
                if vulnerability.get('references'):
 | 
						|
                    for reference in vulnerability['references']:
 | 
						|
                        vulnerability_object.add_attribute(**{
 | 
						|
                            'type': 'link',
 | 
						|
                            'object_relation': 'references',
 | 
						|
                            'value': reference
 | 
						|
                        })
 | 
						|
                vulnerability_object.add_reference(self.attribute.uuid, 'vulnerability-of')
 | 
						|
                self.misp_event.add_object(vulnerability_object)
 | 
						|
            for cve_id in query_results['vulns']:
 | 
						|
                if cve_id not in vulnerabilities:
 | 
						|
                    attribute = {
 | 
						|
                        'type': 'vulnerability',
 | 
						|
                        'value': cve_id
 | 
						|
                    }
 | 
						|
                    self.misp_event.add_attribute(**attribute)
 | 
						|
        else:
 | 
						|
            # We have no vulnerability data, we only check if we have
 | 
						|
            # certificates within the "data" field
 | 
						|
            for data in query_results['data']:
 | 
						|
                if data.get('ssl'):
 | 
						|
                    self._parse_cert(data['ssl']['cert'])
 | 
						|
 | 
						|
    def get_result(self):
 | 
						|
        event = json.loads(self.misp_event.to_json())
 | 
						|
        results = {key: event[key] for key in ('Attribute', 'Object') if (key in event and event[key])}
 | 
						|
        return {'results': results}
 | 
						|
 | 
						|
    # When we want to add the IP address information in objects such as the
 | 
						|
    # domain-ip or ip-port objects referencing the input IP address attribute
 | 
						|
    def _get_source_attribute(self):
 | 
						|
        return {
 | 
						|
            'type': self.attribute.type,
 | 
						|
            'object_relation': self.attribute.type,
 | 
						|
            'value': self.attribute.value
 | 
						|
        }
 | 
						|
 | 
						|
    def _parse_cert(self, certificate):
 | 
						|
        x509_object = MISPObject('x509')
 | 
						|
        for feature in ('serial', 'sig_alg', 'version'):
 | 
						|
            if certificate.get(feature):
 | 
						|
                attribute = {'value': certificate[feature]}
 | 
						|
                attribute.update(self.x509_mapping[feature])
 | 
						|
                x509_object.add_attribute(**attribute)
 | 
						|
        # Parse issuer and subject value
 | 
						|
        for feature in ('issuer', 'subject'):
 | 
						|
            if certificate.get(feature):
 | 
						|
                attribute_value = (f'{identifier}={value}' for identifier, value in certificate[feature].items())
 | 
						|
                attribute = {'value': f'/{"/".join(attribute_value)}'}
 | 
						|
                attribute.update(self.x509_mapping[feature])
 | 
						|
                x509_object.add_attribute(**attribute)
 | 
						|
        # Parse datetime attributes
 | 
						|
        for feature in ('expires', 'issued'):
 | 
						|
            if certificate.get(feature):
 | 
						|
                attribute = {'value': datetime.strptime(certificate[feature], '%Y%m%d%H%M%SZ')}
 | 
						|
                attribute.update(self.x509_mapping[feature])
 | 
						|
                x509_object.add_attribute(**attribute)
 | 
						|
        # Parse fingerprints
 | 
						|
        if certificate.get('fingerprint'):
 | 
						|
            for hash_type, hash_value in certificate['fingerprint'].items():
 | 
						|
                x509_object.add_attribute(**{
 | 
						|
                    'type': f'x509-fingerprint-{hash_type}',
 | 
						|
                    'object_relation': f'x509-fingerprint-{hash_type}',
 | 
						|
                    'value': hash_value
 | 
						|
                })
 | 
						|
        # Parse public key related info
 | 
						|
        if certificate.get('pubkey'):
 | 
						|
            for feature, value in certificate['pubkey'].items():
 | 
						|
                attribute = {'value': value}
 | 
						|
                attribute.update(self.x509_mapping[feature])
 | 
						|
                x509_object.add_attribute(**attribute)
 | 
						|
        x509_object.add_reference(self.attribute.uuid, 'identifies')
 | 
						|
        self.misp_event.add_object(x509_object)
 | 
						|
 | 
						|
 | 
						|
def handler(q=False):
 | 
						|
    if q is False:
 | 
						|
        return False
 | 
						|
    request = json.loads(q)
 | 
						|
    if not request.get('config', {}).get('apikey'):
 | 
						|
        return {'error': 'Shodan authentication is missing'}
 | 
						|
    if not request.get('attribute') or not check_input_attribute(request['attribute']):
 | 
						|
        return {'error': f'{standard_error_message}, which should contain at least a type, a value and an uuid.'}
 | 
						|
    attribute = request['attribute']
 | 
						|
    if attribute['type'] not in mispattributes['input']:
 | 
						|
        return {'error': 'Unsupported attribute type.'}
 | 
						|
    shodan_parser = ShodanParser(attribute)
 | 
						|
    shodan_parser.query_shodan(request['config']['apikey'])
 | 
						|
    return shodan_parser.get_result()
 | 
						|
 | 
						|
 | 
						|
def introspection():
 | 
						|
    return mispattributes
 | 
						|
 | 
						|
 | 
						|
def version():
 | 
						|
    moduleinfo['config'] = moduleconfig
 | 
						|
    return moduleinfo
 |