|
| 1 | +import ipaddress |
| 2 | +import json |
| 3 | +from urllib.parse import urlparse |
| 4 | + |
| 5 | +import requests |
| 6 | +from pymisp import MISPAttribute, MISPEvent, MISPObject |
| 7 | + |
| 8 | +from . import check_input_attribute, standard_error_message |
| 9 | + |
| 10 | +mispattributes = { |
| 11 | + "input": ["domain", "hostname", "ip-src", "ip-dst", "url"], |
| 12 | + "format": "misp_standard", |
| 13 | +} |
| 14 | +moduleinfo = { |
| 15 | + "version": 1, |
| 16 | + "author": "Ali Bhutto", |
| 17 | + "description": ( |
| 18 | + "An expansion module to query the public RDAP bootstrap (rdap.org) for" |
| 19 | + " registration data of a domain, hostname, IP address or URL. RDAP" |
| 20 | + " (Registration Data Access Protocol, RFC 9082/9083) is the free," |
| 21 | + " unauthenticated and structured successor to WHOIS." |
| 22 | + ), |
| 23 | + "module-type": ["expansion", "hover"], |
| 24 | + "name": "RDAP Lookup", |
| 25 | + "logo": "", |
| 26 | + "requirements": [], |
| 27 | + "features": ( |
| 28 | + "The module takes a domain, hostname, IP address or URL attribute as" |
| 29 | + " input, resolves a URL to its host, and queries the rdap.org bootstrap" |
| 30 | + " which redirects to the authoritative RDAP server for the object. The" |
| 31 | + " registrar, registration and expiration dates, name servers, status and" |
| 32 | + " registrant information are parsed into a MISP whois object." |
| 33 | + ), |
| 34 | + "references": ["https://about.rdap.org/", "https://rdap.org/"], |
| 35 | + "input": "A domain, hostname, IP address or URL attribute.", |
| 36 | + "output": "A whois object holding the registration data returned by RDAP.", |
| 37 | +} |
| 38 | +moduleconfig = [] |
| 39 | + |
| 40 | +RDAP_URL = "https://rdap.org" |
| 41 | + |
| 42 | +# RDAP event actions (RFC 9083) mapped to whois object relations. |
| 43 | +_EVENT_MAPPING = { |
| 44 | + "registration": "creation-date", |
| 45 | + "expiration": "expiration-date", |
| 46 | + "last changed": "modification-date", |
| 47 | +} |
| 48 | + |
| 49 | + |
| 50 | +def _is_ip(value): |
| 51 | + try: |
| 52 | + ipaddress.ip_address(value) |
| 53 | + except ValueError: |
| 54 | + return False |
| 55 | + return True |
| 56 | + |
| 57 | + |
| 58 | +def _vcard_value(entity, field): |
| 59 | + """Pull a single field (e.g. ``fn``, ``org``, ``email``) out of an RDAP |
| 60 | + entity's jCard ``vcardArray`` (RFC 7095), or ``None``.""" |
| 61 | + vcard = entity.get("vcardArray") |
| 62 | + if not vcard or len(vcard) < 2: |
| 63 | + return None |
| 64 | + for item in vcard[1]: |
| 65 | + # each item is [name, params, type, value] |
| 66 | + if len(item) >= 4 and item[0] == field: |
| 67 | + value = item[3] |
| 68 | + if isinstance(value, list): |
| 69 | + value = " ".join(str(part) for part in value if part) |
| 70 | + return value |
| 71 | + return None |
| 72 | + |
| 73 | + |
| 74 | +def _entity_by_role(entities, role): |
| 75 | + for entity in entities or []: |
| 76 | + if role in entity.get("roles", []): |
| 77 | + return entity |
| 78 | + return None |
| 79 | + |
| 80 | + |
| 81 | +def _add_if(misp_object, relation, value): |
| 82 | + if value: |
| 83 | + misp_object.add_attribute(relation, value) |
| 84 | + |
| 85 | + |
| 86 | +def _parse_rdap(rdap, queried_value, is_ip): |
| 87 | + """Build a MISP whois object from an RDAP response.""" |
| 88 | + whois = MISPObject("whois") |
| 89 | + whois.add_attribute("ip-address" if is_ip else "domain", queried_value) |
| 90 | + |
| 91 | + registrar = _entity_by_role(rdap.get("entities"), "registrar") |
| 92 | + if registrar: |
| 93 | + _add_if(whois, "registrar", _vcard_value(registrar, "fn")) |
| 94 | + |
| 95 | + registrant = _entity_by_role(rdap.get("entities"), "registrant") |
| 96 | + if registrant: |
| 97 | + _add_if(whois, "registrant-name", _vcard_value(registrant, "fn")) |
| 98 | + _add_if(whois, "registrant-org", _vcard_value(registrant, "org")) |
| 99 | + _add_if(whois, "registrant-email", _vcard_value(registrant, "email")) |
| 100 | + |
| 101 | + for event in rdap.get("events", []): |
| 102 | + relation = _EVENT_MAPPING.get(event.get("eventAction")) |
| 103 | + if relation and event.get("eventDate"): |
| 104 | + _add_if(whois, relation, event["eventDate"]) |
| 105 | + |
| 106 | + for nameserver in rdap.get("nameservers", []): |
| 107 | + _add_if(whois, "nameserver", nameserver.get("ldhName")) |
| 108 | + |
| 109 | + if rdap.get("status"): |
| 110 | + whois.add_attribute("text", "status: " + ", ".join(rdap["status"])) |
| 111 | + |
| 112 | + return whois |
| 113 | + |
| 114 | + |
| 115 | +def handler(q=False): |
| 116 | + if q is False: |
| 117 | + return False |
| 118 | + request = json.loads(q) |
| 119 | + if not request.get("attribute") or not check_input_attribute(request["attribute"]): |
| 120 | + return {"error": f"{standard_error_message}, which should contain at least a type, a value and an uuid."} |
| 121 | + attribute = request["attribute"] |
| 122 | + if attribute.get("type") not in mispattributes["input"]: |
| 123 | + return {"error": "Wrong input attribute type."} |
| 124 | + |
| 125 | + value = attribute["value"] |
| 126 | + if attribute["type"] == "url": |
| 127 | + host = urlparse(value).hostname |
| 128 | + if not host: |
| 129 | + return {"error": f"Could not extract a host from URL {value}."} |
| 130 | + value = host |
| 131 | + |
| 132 | + is_ip = _is_ip(value) |
| 133 | + path = f"ip/{value}" if is_ip else f"domain/{value}" |
| 134 | + try: |
| 135 | + response = requests.get( |
| 136 | + f"{RDAP_URL}/{path}", |
| 137 | + headers={"Accept": "application/rdap+json"}, |
| 138 | + timeout=15, |
| 139 | + ) |
| 140 | + except requests.RequestException as e: |
| 141 | + return {"error": f"Error while querying rdap.org: {e}"} |
| 142 | + |
| 143 | + if response.status_code == 404: |
| 144 | + return {"error": f"No RDAP record found for {value}."} |
| 145 | + if response.status_code != 200: |
| 146 | + return {"error": f"Error while querying rdap.org - {response.status_code}: {response.reason}"} |
| 147 | + |
| 148 | + try: |
| 149 | + rdap = response.json() |
| 150 | + except ValueError: |
| 151 | + return {"error": "RDAP server returned a non-JSON response."} |
| 152 | + |
| 153 | + misp_event = MISPEvent() |
| 154 | + input_attribute = MISPAttribute() |
| 155 | + input_attribute.from_dict(**attribute) |
| 156 | + misp_event.add_attribute(**input_attribute) |
| 157 | + |
| 158 | + whois = _parse_rdap(rdap, value, is_ip) |
| 159 | + whois.add_reference(input_attribute.uuid, "related-to") |
| 160 | + misp_event.add_object(whois) |
| 161 | + |
| 162 | + event = json.loads(misp_event.to_json()) |
| 163 | + return {"results": {key: event[key] for key in ("Attribute", "Object")}} |
| 164 | + |
| 165 | + |
| 166 | +def introspection(): |
| 167 | + return mispattributes |
| 168 | + |
| 169 | + |
| 170 | +def version(): |
| 171 | + moduleinfo["config"] = moduleconfig |
| 172 | + return moduleinfo |
0 commit comments