|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# encoding: utf-8 |
| 3 | + |
| 4 | +from domaintools.exceptions import NotFoundException |
| 5 | +from domaintools.exceptions import NotAuthorizedException |
| 6 | +from domaintools.exceptions import ServiceUnavailableException |
| 7 | + |
| 8 | +from domaintools import API |
| 9 | + |
| 10 | +from cortexutils.analyzer import Analyzer |
| 11 | +from datetime import datetime |
| 12 | + |
| 13 | + |
| 14 | +class DomainToolsAnalyzer(Analyzer): |
| 15 | + def __init__(self): |
| 16 | + Analyzer.__init__(self) |
| 17 | + self.service = self.get_param( |
| 18 | + "config.service", None, "Service parameter is missing" |
| 19 | + ) |
| 20 | + self.raw = "" |
| 21 | + self.pivot_count_threshold = int(self.get_param("config.pivot_count_threshold")) |
| 22 | + |
| 23 | + @staticmethod |
| 24 | + def get_domain_age(create_date): |
| 25 | + """ |
| 26 | + Finds how many days old a domain is given a start date. |
| 27 | + Args: |
| 28 | + create_date: Date in the form of %Y-%m-%d' |
| 29 | +
|
| 30 | + Returns: Number of days |
| 31 | + """ |
| 32 | + time_diff = datetime.now() - datetime.strptime(create_date, "%Y-%m-%d") |
| 33 | + return time_diff.days |
| 34 | + |
| 35 | + @staticmethod |
| 36 | + def get_threat_level(risk_score): |
| 37 | + level = "info" |
| 38 | + if risk_score <= 65: |
| 39 | + level = "safe" |
| 40 | + elif 65 < risk_score <= 80: |
| 41 | + level = "suspicious" |
| 42 | + elif risk_score > 80: |
| 43 | + level = "malicious" |
| 44 | + return level |
| 45 | + |
| 46 | + @staticmethod |
| 47 | + def get_threat_level_class(risk_score): |
| 48 | + level = "" |
| 49 | + if risk_score <= 65: |
| 50 | + level = "label-success" |
| 51 | + elif 65 < risk_score <= 80: |
| 52 | + level = "label-warning" |
| 53 | + elif risk_score > 80: |
| 54 | + level = "label-danger" |
| 55 | + return level |
| 56 | + |
| 57 | + def add_pivot_class(self, data_obj): |
| 58 | + """ |
| 59 | + Does a deep dive through a data object to check count vs pivot threshold to add class to DOM element. |
| 60 | + Args: |
| 61 | + data_obj: Either a list or dict that needs to check pivot count |
| 62 | + """ |
| 63 | + if isinstance(data_obj, dict) and len(data_obj): |
| 64 | + for k, v in data_obj.items(): |
| 65 | + if isinstance(data_obj[k], dict) or isinstance(data_obj[k], list): |
| 66 | + self.add_pivot_class(data_obj[k]) |
| 67 | + if "count" in data_obj and ( |
| 68 | + 0 < data_obj["count"] < self.pivot_count_threshold |
| 69 | + ): |
| 70 | + data_obj["class"] = "label-danger" |
| 71 | + elif "count" in data_obj and data_obj["count"] == 0: |
| 72 | + del data_obj["count"] |
| 73 | + elif ( |
| 74 | + "count" in data_obj |
| 75 | + and "class" not in data_obj |
| 76 | + and data_obj["count"] != 0 |
| 77 | + ): |
| 78 | + data_obj["class"] = "label-info" |
| 79 | + |
| 80 | + elif isinstance(data_obj, list) and len(data_obj): |
| 81 | + for index, item in enumerate(data_obj): |
| 82 | + self.add_pivot_class(item) |
| 83 | + |
| 84 | + @staticmethod |
| 85 | + def get_threat_component(components, threat_type): |
| 86 | + """ |
| 87 | + Gets a certain threat component out a list of components |
| 88 | + Args: |
| 89 | + components: List of threat components |
| 90 | + threat_type: Type of threat we are looking for |
| 91 | +
|
| 92 | + Returns: Either the component that we asked for or None |
| 93 | + """ |
| 94 | + for component in components: |
| 95 | + if component.get("name") == threat_type: |
| 96 | + return component |
| 97 | + else: |
| 98 | + return None |
| 99 | + |
| 100 | + def format_single_domain(self, domain_data): |
| 101 | + domain_data["last_enriched"] = datetime.now().date().strftime("%m-%d-%Y") |
| 102 | + if isinstance(domain_data["website_response"], dict): |
| 103 | + domain_data["website_response"] = domain_data["website_response"].get( |
| 104 | + "value", "" |
| 105 | + ) |
| 106 | + # Threat Components Flatten |
| 107 | + domain_risk = domain_data.get("domain_risk", {}) |
| 108 | + |
| 109 | + overall_risk_score = domain_risk.get("risk_score", 0) |
| 110 | + domain_risk["overall"] = { |
| 111 | + "value": overall_risk_score, |
| 112 | + "class": DomainToolsAnalyzer.get_threat_level_class(overall_risk_score), |
| 113 | + } |
| 114 | + risk_components = domain_risk.get("components", {}) |
| 115 | + if risk_components: |
| 116 | + proximity_data = DomainToolsAnalyzer.get_threat_component( |
| 117 | + risk_components, "proximity" |
| 118 | + ) |
| 119 | + blacklist_data = DomainToolsAnalyzer.get_threat_component( |
| 120 | + risk_components, "blacklist" |
| 121 | + ) |
| 122 | + domain_risk["proximity"] = {"value": 0} |
| 123 | + if proximity_data: |
| 124 | + domain_risk["proximity"]["value"] = proximity_data.get("risk_score", 0) |
| 125 | + elif blacklist_data: |
| 126 | + domain_risk["proximity"]["value"] = blacklist_data.get("risk_score", 0) |
| 127 | + domain_risk["proximity"][ |
| 128 | + "class" |
| 129 | + ] = DomainToolsAnalyzer.get_threat_level_class( |
| 130 | + domain_risk["proximity"]["value"] |
| 131 | + ) |
| 132 | + threat_profile_data = DomainToolsAnalyzer.get_threat_component( |
| 133 | + risk_components, "threat_profile" |
| 134 | + ) |
| 135 | + if threat_profile_data: |
| 136 | + domain_risk["tp"] = {} |
| 137 | + domain_risk["tp"]["value"] = threat_profile_data.get("risk_score", 0) |
| 138 | + domain_risk["tp"]["class"] = DomainToolsAnalyzer.get_threat_level_class( |
| 139 | + domain_risk["tp"]["value"] |
| 140 | + ) |
| 141 | + domain_risk["tp"]["threats"] = threat_profile_data.get("threats", []) |
| 142 | + domain_risk["tp"]["evidence"] = threat_profile_data.get("evidence", []) |
| 143 | + threat_profile_malware_data = DomainToolsAnalyzer.get_threat_component( |
| 144 | + risk_components, "threat_profile_malware" |
| 145 | + ) |
| 146 | + if threat_profile_malware_data: |
| 147 | + domain_risk["tpm"] = {} |
| 148 | + domain_risk["tpm"]["value"] = threat_profile_malware_data.get( |
| 149 | + "risk_score", 0 |
| 150 | + ) |
| 151 | + domain_risk["tpm"][ |
| 152 | + "class" |
| 153 | + ] = DomainToolsAnalyzer.get_threat_level_class( |
| 154 | + domain_risk["tpm"]["value"] |
| 155 | + ) |
| 156 | + threat_profile_phshing_data = DomainToolsAnalyzer.get_threat_component( |
| 157 | + risk_components, "threat_profile_phishing" |
| 158 | + ) |
| 159 | + if threat_profile_phshing_data: |
| 160 | + domain_risk["tpp"] = {} |
| 161 | + domain_risk["tpp"]["value"] = threat_profile_malware_data.get( |
| 162 | + "risk_score", 0 |
| 163 | + ) |
| 164 | + domain_risk["tpp"][ |
| 165 | + "class" |
| 166 | + ] = DomainToolsAnalyzer.get_threat_level_class( |
| 167 | + domain_risk["tpp"]["value"] |
| 168 | + ) |
| 169 | + threat_profile_spam_data = DomainToolsAnalyzer.get_threat_component( |
| 170 | + risk_components, "threat_profile_spam" |
| 171 | + ) |
| 172 | + if threat_profile_spam_data: |
| 173 | + domain_risk["tps"] = {} |
| 174 | + domain_risk["tps"]["value"] = threat_profile_malware_data.get( |
| 175 | + "risk_score", 0 |
| 176 | + ) |
| 177 | + domain_risk["tps"][ |
| 178 | + "class" |
| 179 | + ] = DomainToolsAnalyzer.get_threat_level_class( |
| 180 | + domain_risk["tps"]["value"] |
| 181 | + ) |
| 182 | + |
| 183 | + # Contacts Flatten |
| 184 | + domain_data["types"] = [ |
| 185 | + "registrant_contact", |
| 186 | + "admin_contact", |
| 187 | + "technical_contact", |
| 188 | + "billing_contact", |
| 189 | + ] |
| 190 | + domain_data["contacts"] = [] |
| 191 | + for c in domain_data["types"]: |
| 192 | + split_type = c.split("_") |
| 193 | + domain_data[c]["type"] = "{} Contact".format(split_type[0].capitalize()) |
| 194 | + domain_data["contacts"].append(domain_data[c]) |
| 195 | + |
| 196 | + self.add_pivot_class(domain_data) |
| 197 | + return domain_data |
| 198 | + |
| 199 | + @staticmethod |
| 200 | + def format_pivot_domains(domains, artifact_type, artifact_data): |
| 201 | + result = { |
| 202 | + "last_enriched": datetime.now().date().strftime("%m-%d-%Y"), |
| 203 | + "pivot_artifact": "{} = {}".format(artifact_type.upper(), artifact_data), |
| 204 | + "average_risk_score": 0, |
| 205 | + } |
| 206 | + total_risk_score = 0 |
| 207 | + sorted_domains = sorted( |
| 208 | + domains, |
| 209 | + key=lambda d: ( |
| 210 | + d.get("domain_risk", 0).get("risk_score", 0), |
| 211 | + d.get("domain"), |
| 212 | + ), |
| 213 | + reverse=True, |
| 214 | + ) |
| 215 | + result_domains = [] |
| 216 | + for domain in sorted_domains: |
| 217 | + total_risk_score += domain.get("domain_risk", 0).get("risk_score", 0) |
| 218 | + temp_dict = {"domain": domain.get("domain")} |
| 219 | + risk_score = domain.get("domain_risk", 0).get("risk_score", 0) |
| 220 | + temp_dict["domain_risk"] = { |
| 221 | + "class": DomainToolsAnalyzer.get_threat_level_class(risk_score), |
| 222 | + "risk_score": risk_score, |
| 223 | + } |
| 224 | + create_date = domain.get("create_date", {}).get("value", "") |
| 225 | + if create_date: |
| 226 | + temp_dict["domain_age"] = DomainToolsAnalyzer.get_domain_age( |
| 227 | + create_date |
| 228 | + ) |
| 229 | + else: |
| 230 | + temp_dict["domain_age"] = 0 |
| 231 | + result_domains.append(temp_dict) |
| 232 | + result["results"] = result_domains |
| 233 | + if len(result_domains): |
| 234 | + result["average_risk_score"] = total_risk_score // len(result_domains) |
| 235 | + return result |
| 236 | + |
| 237 | + def domaintools(self, data): |
| 238 | + response = None |
| 239 | + api = API(self.get_param("config.username"), self.get_param("config.key")) |
| 240 | + |
| 241 | + APP_PARAMETERS = {"app_partner": "cortex", "app_name": "Iris", "app_version": 1} |
| 242 | + |
| 243 | + if self.service == "investigate-domain" and self.data_type in ["domain"]: |
| 244 | + response = api.iris_investigate(data, **APP_PARAMETERS).response() |
| 245 | + if response["results_count"]: |
| 246 | + response = self.format_single_domain(response.get("results")[0]) |
| 247 | + |
| 248 | + elif self.service == "pivot" and self.data_type in ["hash", "ip", "mail"]: |
| 249 | + iris_investigate_args_map = { |
| 250 | + "ip": "ip", |
| 251 | + "mail": "email", |
| 252 | + "hash": "ssl_hash", |
| 253 | + } |
| 254 | + APP_PARAMETERS[iris_investigate_args_map[self.data_type]] = data |
| 255 | + response = api.iris_investigate(**APP_PARAMETERS).response() |
| 256 | + response = DomainToolsAnalyzer.format_pivot_domains( |
| 257 | + response.get("results"), iris_investigate_args_map[self.data_type], data |
| 258 | + ) |
| 259 | + |
| 260 | + return response |
| 261 | + |
| 262 | + def summary(self, raw): |
| 263 | + self.raw = raw |
| 264 | + r = {"service": self.service, "dataType": self.data_type} |
| 265 | + |
| 266 | + taxonomies = [] |
| 267 | + |
| 268 | + # Prepare predicate and value for each service |
| 269 | + if r["service"] == "investigate-domain": |
| 270 | + if "risk_score" in raw["domain_risk"]: |
| 271 | + risk_score = raw["domain_risk"]["overall"]["value"] |
| 272 | + level = self.get_threat_level(risk_score) |
| 273 | + taxonomies.append( |
| 274 | + self.build_taxonomy( |
| 275 | + level, "DT", "Risk Score", "{}".format(risk_score) |
| 276 | + ) |
| 277 | + ) |
| 278 | + else: |
| 279 | + taxonomies.append( |
| 280 | + self.build_taxonomy("info", "DT", "Risk Score", "No Risk Score") |
| 281 | + ) |
| 282 | + |
| 283 | + if "tp" in raw["domain_risk"]: |
| 284 | + risk_score = raw["domain_risk"]["tp"]["value"] |
| 285 | + level = self.get_threat_level(risk_score) |
| 286 | + taxonomies.append( |
| 287 | + self.build_taxonomy( |
| 288 | + level, "DT", "Threat Profile Score", "{}".format(risk_score) |
| 289 | + ) |
| 290 | + ) |
| 291 | + evidence = ",".join(raw["domain_risk"]["tp"]["evidence"]) |
| 292 | + if evidence: |
| 293 | + taxonomies.append( |
| 294 | + self.build_taxonomy( |
| 295 | + "info", "DT", "Evidence", "{}".format(evidence) |
| 296 | + ) |
| 297 | + ) |
| 298 | + else: |
| 299 | + taxonomies.append( |
| 300 | + self.build_taxonomy( |
| 301 | + "info", "DT", "Threat Profile Score", "No Threat Profile Score" |
| 302 | + ) |
| 303 | + ) |
| 304 | + tags = ",".join([t["label"] for t in raw.get("tags", [])]) |
| 305 | + if tags: |
| 306 | + taxonomies.append( |
| 307 | + self.build_taxonomy("info", "DT", "IrisTags", "{}".format(tags)) |
| 308 | + ) |
| 309 | + |
| 310 | + elif r["service"] == "pivot": |
| 311 | + taxonomies.append(self.build_taxonomy("info", "DT", "Pivots", "Pivots")) |
| 312 | + |
| 313 | + result = {"taxonomies": taxonomies} |
| 314 | + return result |
| 315 | + |
| 316 | + def run(self): |
| 317 | + data = self.get_data() |
| 318 | + |
| 319 | + try: |
| 320 | + r = self.domaintools(data) |
| 321 | + |
| 322 | + if "error" in r and "message" in r["error"]: |
| 323 | + # noinspection PyTypeChecker |
| 324 | + self.error(r["error"]["message"]) |
| 325 | + else: |
| 326 | + self.report(r) |
| 327 | + |
| 328 | + except NotFoundException: |
| 329 | + self.error(self.data_type.capitalize() + " not found") |
| 330 | + except NotAuthorizedException: |
| 331 | + self.error("An authorization error occurred") |
| 332 | + except ServiceUnavailableException: |
| 333 | + self.error("DomainTools Service is currenlty unavailable") |
| 334 | + except Exception as e: |
| 335 | + self.unexpectedError(e) |
| 336 | + |
| 337 | + |
| 338 | +if __name__ == "__main__": |
| 339 | + DomainToolsAnalyzer().run() |
0 commit comments