|
| 1 | +"""Module to retrieve active DNS resolution |
| 2 | +""" |
| 3 | + |
| 4 | +import traceback |
| 5 | +import requests |
| 6 | +import ipaddress |
| 7 | +import socket |
| 8 | + |
| 9 | +from api_app.exceptions import AnalyzerConfigurationException, AnalyzerRunException |
| 10 | +from api_app.script_analyzers import general |
| 11 | + |
| 12 | +import logging |
| 13 | +logger = logging.getLogger(__name__) |
| 14 | + |
| 15 | + |
| 16 | +def run(analyzer_name, job_id, observable_name, observable_classification, additional_config_params): |
| 17 | + """Run ActiveDNS analyzer |
| 18 | +
|
| 19 | + Admit: |
| 20 | + * additional_config_params[service]: google - Google DoH (DNS over HTTPS) |
| 21 | + * additional_config_params[service]: cloudflare - CloudFlare DoH (DNS over HTTPS) |
| 22 | + * additional_config_params[service]: classic - classic DNS query |
| 23 | +
|
| 24 | + Google and CloudFlare return an IP (or NXDOMAIN) from a domain. |
| 25 | + Classic support also reverse lookup (domain from IP) |
| 26 | +
|
| 27 | + :param analyzer_name: Analyzer configuration in analyzer_config.json |
| 28 | + :type analyzer_name: str |
| 29 | + :param job_id: job identifier |
| 30 | + :type job_id: str |
| 31 | + :param observable_name: analyzed observable |
| 32 | + :type observable_name: str |
| 33 | + :param observable_classification: observable classification (allow: ip or domain) ip only classic |
| 34 | + :type observable_classification: str |
| 35 | + :param additional_config_params: params service to select the service |
| 36 | + :type additional_config_params: dict |
| 37 | + :return: report: name: observable_name, resolution: ip,NXDOMAIN, '' |
| 38 | + :rtype: report: dict |
| 39 | + """ |
| 40 | + logger.info(f"started analyzer {analyzer_name} job_id {job_id} observable {observable_name}") |
| 41 | + report = general.get_basic_report_template(analyzer_name) |
| 42 | + |
| 43 | + try: |
| 44 | + dns_type = additional_config_params.get('service', '') |
| 45 | + if dns_type == 'google': |
| 46 | + _doh_google(job_id, analyzer_name, observable_classification, observable_name, report) |
| 47 | + elif dns_type == 'cloudflare': |
| 48 | + _doh_cloudflare(job_id, analyzer_name, observable_classification, observable_name, |
| 49 | + report) |
| 50 | + elif dns_type == 'classic': |
| 51 | + _classic_dns(job_id, analyzer_name, observable_classification, observable_name, report) |
| 52 | + else: |
| 53 | + raise AnalyzerConfigurationException(f'Service selected: {dns_type} is not available') |
| 54 | + |
| 55 | + except (AnalyzerConfigurationException, AnalyzerRunException) as e: |
| 56 | + error_message = f"job_id:{job_id} analyzer:{analyzer_name} " \ |
| 57 | + f"observable_name:{observable_name} Analyzer error {e}" |
| 58 | + logger.error(error_message) |
| 59 | + report['errors'].append(error_message) |
| 60 | + report['success'] = False |
| 61 | + except Exception as e: |
| 62 | + traceback.print_exc() |
| 63 | + error_message = f"job_id:{job_id} analyzer:{analyzer_name} " \ |
| 64 | + f"observable_name:{observable_name} Unexpected error {e}" |
| 65 | + logger.exception(error_message) |
| 66 | + report['errors'].append(str(e)) |
| 67 | + report['success'] = False |
| 68 | + |
| 69 | + general.set_report_and_cleanup(job_id, report) |
| 70 | + |
| 71 | + logger.info(f"ended analyzer {analyzer_name} job_id {job_id} observable {observable_name}") |
| 72 | + |
| 73 | + return report |
| 74 | + |
| 75 | + |
| 76 | +def _doh_google(job_id, analyzer_name, observable_classification, observable_name, report): |
| 77 | + if observable_classification == 'domain': |
| 78 | + try: |
| 79 | + params = { |
| 80 | + "name": observable_name, |
| 81 | + # this filter should work but it is not |
| 82 | + "type": 1 |
| 83 | + } |
| 84 | + response = requests.get('https://dns.google.com/resolve', params=params) |
| 85 | + response.raise_for_status() |
| 86 | + data = response.json() |
| 87 | + ip = '' |
| 88 | + answers = data.get("Answer", []) |
| 89 | + for answer in answers: |
| 90 | + if answer.get('type', 1) == 1: |
| 91 | + ip = answer.get('data', 'NXDOMAIN') |
| 92 | + break |
| 93 | + if not ip: |
| 94 | + logger.error(f"observable {observable_name} active_dns query retrieved no valid A answer: {answers}") |
| 95 | + report['report'] = {'name': observable_name, 'resolution': ip} |
| 96 | + report['success'] = True |
| 97 | + except requests.exceptions.RequestException as error: |
| 98 | + error_message = f"job_id:{job_id}, analyzer:{analyzer_name}, " \ |
| 99 | + f"observable_classification:{observable_classification}, " \ |
| 100 | + f"observable_name:{observable_name}, RequestException {error}" |
| 101 | + logger.error(error_message) |
| 102 | + report['errors'].append(error_message) |
| 103 | + report['success'] = False |
| 104 | + else: |
| 105 | + error_message = f"job_id:{job_id}, analyzer:{analyzer_name}, " \ |
| 106 | + f"observable_classification:{observable_classification}, " \ |
| 107 | + f"observable_name:{observable_name}, " \ |
| 108 | + f"cannot analyze something different from domain" |
| 109 | + logger.error(error_message) |
| 110 | + report['errors'].append(error_message) |
| 111 | + report['success'] = False |
| 112 | + |
| 113 | + |
| 114 | +def _doh_cloudflare(job_id, analyzer_name, observable_classification, observable_name, report): |
| 115 | + if observable_classification == 'domain': |
| 116 | + try: |
| 117 | + client = requests.session() |
| 118 | + params = { |
| 119 | + 'name': observable_name, |
| 120 | + 'type': 'A', |
| 121 | + 'ct': 'application/dns-json', |
| 122 | + } |
| 123 | + response = client.get('https://cloudflare-dns.com/dns-query', params=params) |
| 124 | + response.raise_for_status() |
| 125 | + response_dict = response.json() |
| 126 | + response_answer = response_dict.get('Answer', []) |
| 127 | + # first resolution or NXDOMAIN if domain does not exist |
| 128 | + result_data = response_answer[0].get('data', 'NXDOMAIN') if response_answer else 'NXDOMAIN' |
| 129 | + report['report'] = {'name': observable_name, 'resolution': result_data} |
| 130 | + report['success'] = True |
| 131 | + except requests.exceptions.RequestException as error: |
| 132 | + error_message = f"job_id:{job_id}, analyzer:{analyzer_name}, " \ |
| 133 | + f"observable_classification:{observable_classification}, " \ |
| 134 | + f"observable_name:{observable_name}, RequestException {error}" |
| 135 | + logger.error(error_message) |
| 136 | + report['errors'].append(error_message) |
| 137 | + report['success'] = False |
| 138 | + else: |
| 139 | + error_message = f"job_id:{job_id}, analyzer:{analyzer_name}, " \ |
| 140 | + f"observable_classification:{observable_classification}, " \ |
| 141 | + f"observable_name:{observable_name}, " \ |
| 142 | + f"cannot analyze something different from domain" |
| 143 | + logger.error(error_message) |
| 144 | + report['errors'].append(error_message) |
| 145 | + report['success'] = False |
| 146 | + |
| 147 | + |
| 148 | +def _classic_dns(job_id, analyzer_name, observable_classification, observable_name, report): |
| 149 | + result = {} |
| 150 | + if observable_classification == 'ip': |
| 151 | + ipaddress.ip_address(observable_name) |
| 152 | + try: |
| 153 | + domains = socket.gethostbyaddr(observable_name) |
| 154 | + # return a tuple (hostname, aliaslist, ipaddrlist), select hostname |
| 155 | + # if does not exist return socket.herror |
| 156 | + if domains: |
| 157 | + resolution = domains[0] |
| 158 | + except socket.herror: |
| 159 | + resolution = '' |
| 160 | + result = {'name': observable_name, 'resolution': resolution} |
| 161 | + elif observable_classification == 'domain': |
| 162 | + try: |
| 163 | + resolution = socket.gethostbyname(observable_name) |
| 164 | + except socket.gaierror: |
| 165 | + resolution = 'NXDOMAIN' |
| 166 | + result = {'name': observable_name, 'resolution': resolution} |
| 167 | + else: |
| 168 | + error_message = f"job_id:{job_id}, analyzer:{analyzer_name}, " \ |
| 169 | + f"observable_classification: {observable_classification}, " \ |
| 170 | + f"observable_name:{observable_name}, not analyzable" |
| 171 | + logger.error(error_message) |
| 172 | + report['errors'].append(error_message) |
| 173 | + report['success'] = False |
| 174 | + |
| 175 | + report['report'] = result |
| 176 | + report['success'] = True |
0 commit comments