Skip to content

Commit c49356d

Browse files
committed
Improve verdict speed in domain command
1 parent c24403a commit c49356d

File tree

3 files changed

+210
-46
lines changed

3 files changed

+210
-46
lines changed

Packs/DomainTools_Iris/Integrations/DomainTools_Iris/DomainTools_Iris.py

Lines changed: 192 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
""" HELPER FUNCTIONS """
8585

8686

87-
def get_client(proxy_url: Optional[str] = None):
87+
def get_client(proxy_url: str | None = None):
8888
return API(
8989
USERNAME,
9090
API_KEY,
@@ -108,6 +108,7 @@ def http_request(method: str, params: dict = {}):
108108
Returns: request result
109109
110110
"""
111+
response = None
111112
proxy_url = PROXIES.get("https") if PROXIES.get("https") != "" else PROXIES.get("http")
112113
if not (USERNAME and API_KEY):
113114
raise DemistoException("The 'API Username' and 'API Key' parameters are required.")
@@ -134,32 +135,53 @@ def http_request(method: str, params: dict = {}):
134135
response = api.reverse_ip(domain=params.get("domain"), limit=params.get("limit")).response()
135136
elif method == "host-domains":
136137
response = api.host_domains(ip=params.get("ip"), limit=params.get("limit")).response()
138+
elif method == "risk":
139+
response = api.risk(domain=params.get("domain")).response()
137140
else:
138141
response = api.iris_investigate(**params).response()
139142
except Exception as e:
140-
demisto.error(str(e))
141-
raise
143+
# exclude for risk endpoint as it throws 404 status by default if domain was not found.
144+
if method != "risk":
145+
demisto.error(str(e))
146+
raise
142147

143148
return response
144149

145150

146-
def get_dbot_score(proximity_score, age, threat_profile_score):
151+
def get_dbot_score(proximity_score, age=None, threat_profile_score=None, overall_risk_score=None):
147152
"""
148153
Gets the DBot score
154+
info:
155+
GOOD = 1
156+
SUSPICIOUS = 2
157+
BAD = 3
149158
Args:
150159
proximity_score: The proximity threat score deals with closeness to other malicious domains.
151160
age: The age of the domain.
152161
threat_profile_score: The threat profile score looking at things like phishing and spam.
162+
overall_risk_score: The overall riskscore. Defaults to None.
153163
154164
Returns: DBot Score
155165
156166
"""
157-
if proximity_score >= RISK_THRESHOLD or threat_profile_score >= RISK_THRESHOLD:
158-
return 3
159-
elif age < YOUNG_DOMAIN_TIMEFRAME and (proximity_score < RISK_THRESHOLD or threat_profile_score < RISK_THRESHOLD):
160-
return 2
161-
else:
162-
return 1
167+
168+
# check for the 'BAD' condition then return.
169+
if proximity_score and proximity_score >= RISK_THRESHOLD or (threat_profile_score and threat_profile_score >= RISK_THRESHOLD):
170+
return Common.DBotScore.BAD
171+
172+
# check for 'SUSPICIOUS' conditions as we know both scores will be lower.
173+
# this means we used the risk score endpoint which we don't check for the domain age
174+
# and look for the overall risk score instead.
175+
if overall_risk_score is not None:
176+
if 50 <= overall_risk_score <= 69:
177+
return Common.DBotScore.SUSPICIOUS
178+
179+
else: # otherwise, we look for the domain age.
180+
if not age or age < YOUNG_DOMAIN_TIMEFRAME:
181+
return Common.DBotScore.SUSPICIOUS
182+
183+
# If the domain is not BAD and not SUSPICIOUS, then return GOOD.
184+
return Common.DBotScore.GOOD
163185

164186

165187
def prune_context_data(data_obj):
@@ -459,6 +481,89 @@ def create_results(domain_result):
459481
return outputs
460482

461483

484+
def create_domain_risk_results(domain_result: dict):
485+
"""
486+
Creates all the context data necessary given a domain result
487+
Args:
488+
domain_result (dict): DomainTools domain data
489+
490+
Returns: dict {
491+
domain: <Common.Domain> - Domain indicator object with Iris results that map to Domain context
492+
domaintools: <dict> - DomainTools context with all Iris results
493+
}
494+
495+
"""
496+
domain = f"{domain_result.get('domain')}"
497+
498+
domain_risk_score_details = {
499+
"overall_risk_score": domain_result.get("risk_score"),
500+
"proximity": "",
501+
"threat_profile": "",
502+
"threat_profile_phishing": "",
503+
"threat_profile_malware": "",
504+
"threat_profile_spam": "",
505+
}
506+
507+
for rc in domain_result.get("components") or []:
508+
component_name = rc.get("name")
509+
if component_name == "zerolist": # ignore zero list
510+
continue
511+
component_riskscore = rc.get("risk_score") or ""
512+
domain_risk_score_details[component_name] = component_riskscore
513+
514+
domaintools_risk_context = {
515+
"Name": domain,
516+
"LastEnriched": datetime.now().strftime("%Y-%m-%d"),
517+
"Analytics": {
518+
"OverallRiskScore": domain_risk_score_details["overall_risk_score"],
519+
"ProximityRiskScore": domain_risk_score_details["proximity"],
520+
"MalwareRiskScGore": domain_risk_score_details["threat_profile_malware"],
521+
"PhishingRiskScore": domain_risk_score_details["threat_profile_phishing"],
522+
"SpamRiskScore": domain_risk_score_details["threat_profile_spam"],
523+
"ThreatProfileRiskScore": {
524+
"RiskScore": domain_risk_score_details["threat_profile"],
525+
},
526+
},
527+
}
528+
529+
# get the db score
530+
reliability = demisto.params().get("integrationReliability")
531+
if domain_risk_score_details["overall_risk_score"] is None: # Score is Unknown
532+
dbot_score = Common.DBotScore(
533+
indicator=domain,
534+
indicator_type=DBotScoreType.DOMAIN,
535+
integration_name="DomainTools Iris",
536+
score=Common.DBotScore.NONE,
537+
reliability=reliability,
538+
message="No current risk score found for this domain.",
539+
)
540+
else:
541+
dbot_score_value = get_dbot_score(
542+
proximity_score=domain_risk_score_details["proximity"],
543+
age=None,
544+
threat_profile_score=domain_risk_score_details["threat_profile"],
545+
overall_risk_score=domain_risk_score_details["overall_risk_score"],
546+
)
547+
548+
malicious_description = None
549+
if dbot_score_value == Common.DBotScore.BAD:
550+
malicious_description = "This domain has been profiled as a threat."
551+
552+
dbot_score = Common.DBotScore(
553+
indicator=domain,
554+
indicator_type=DBotScoreType.DOMAIN,
555+
integration_name="DomainTools Iris",
556+
score=dbot_score_value,
557+
reliability=reliability,
558+
malicious_description=malicious_description,
559+
)
560+
561+
domain_indicator = Common.Domain(domain, dbot_score=dbot_score)
562+
563+
outputs = {"domain": domain_indicator, "domaintools": domaintools_risk_context}
564+
return outputs
565+
566+
462567
def domain_investigate(domain):
463568
"""
464569
Profiles domain and gives back all relevant domain data
@@ -538,6 +643,13 @@ def host_domains(ip: str, limit: int | None = None) -> dict:
538643
return http_request("host-domains", params={"ip": ip, "limit": limit})
539644

540645

646+
def get_domain_risk_score(domain: str) -> dict | None:
647+
"""
648+
Returns the domain risk score using /v1/risk endpoint instead using iris
649+
"""
650+
return http_request("risk", params={"domain": domain})
651+
652+
541653
def add_key_to_json(cur, to_add):
542654
if not cur:
543655
return to_add
@@ -806,6 +918,34 @@ def format_investigate_output(result):
806918
return (human_readable, indicators)
807919

808920

921+
def format_risk_score_output(result):
922+
RISK_SCORE_HEADERS = [
923+
"Name",
924+
"Last Enriched",
925+
"Overall Risk Score",
926+
"Proximity Risk Score",
927+
"Threat Profile Risk Score",
928+
]
929+
domain = result.get("domain")
930+
result_copy = copy.deepcopy(result)
931+
indicators = create_domain_risk_results(result_copy)
932+
933+
domaintools_analytics_data = indicators.get("domaintools", {}).get("Analytics", {})
934+
935+
human_readable_data = {
936+
"Name": f"[{domain}](https://domaintools.com)",
937+
"Last Enriched": datetime.now().strftime("%Y-%m-%d"),
938+
"Overall Risk Score": domaintools_analytics_data.get("OverallRiskScore", ""),
939+
"Proximity Risk Score": domaintools_analytics_data.get("ProximityRiskScore", ""),
940+
"Threat Profile Risk Score": domaintools_analytics_data.get("ThreatProfileRiskScore", {}).get("RiskScore", ""),
941+
}
942+
943+
demisto_title = f"DomainTools Risk Score for {domain}."
944+
human_readable = tableToMarkdown(f"{demisto_title}", human_readable_data, headers=RISK_SCORE_HEADERS)
945+
946+
return human_readable, indicators
947+
948+
809949
def get_domain_risk_score_details(domain_risk: dict[str, Any]) -> dict[str, Any]:
810950
"""Get the domain risk score details on a given domain risk
811951
@@ -1073,34 +1213,57 @@ def domain_command():
10731213
e.g. !domain domain=domaintools.com
10741214
"""
10751215
domain = demisto.args()["domain"]
1216+
domain_result_type = demisto.params().get("domain_result_type") or "Iris"
10761217
domain_list = domain.split(",")
1077-
domain_chunks = chunks(domain_list, 100)
1078-
include_context = argToBoolean(demisto.args().get("include_context", True))
1079-
command_results_list: list[CommandResults] = []
1080-
1081-
for chunk in domain_chunks:
1082-
response = domain_investigate(",".join(chunk))
1083-
missing_domains = response.get("missing_domains")
1084-
for result in response.get("results", []):
1085-
human_readable_output, indicators = format_investigate_output(result)
1086-
1087-
if len(missing_domains) > 0:
1088-
human_readable_output += f"Missing Domains: {','.join(missing_domains)}"
10891218

1090-
domain_indicator = indicators.get("domain") if include_context else None
1091-
domaintools_context = indicators.get("domaintools") if include_context else None
1219+
command_results_list: list[CommandResults] = []
10921220

1221+
human_readable_output = ""
1222+
if domain_result_type.lower() == "verdict":
1223+
# get the risk score using DT risk endpoint
1224+
for domain in domain_list:
1225+
risk_score_result = get_domain_risk_score(domain=domain)
1226+
if risk_score_result is None:
1227+
risk_score_result = {"domain": domain}
1228+
1229+
human_readable_output, indicators = format_risk_score_output(risk_score_result)
1230+
domain_indicator = indicators.get("domain")
1231+
# for risk result (Verdict result type), create a indicator and add the verdict, no context needed.
10931232
command_results_list.append(
10941233
CommandResults(
1095-
outputs_prefix="DomainTools",
1096-
outputs_key_field="Name",
10971234
indicator=domain_indicator,
1098-
outputs=domaintools_context,
1235+
outputs=None,
10991236
readable_output=human_readable_output,
1100-
raw_response=result,
1101-
ignore_auto_extract=True,
1237+
raw_response=risk_score_result,
1238+
ignore_auto_extract=False,
11021239
)
11031240
)
1241+
else:
1242+
include_context = argToBoolean(demisto.args().get("include_context", True))
1243+
domain_chunks = chunks(domain_list, 100)
1244+
for chunk in domain_chunks:
1245+
response = domain_investigate(",".join(chunk))
1246+
missing_domains = response.get("missing_domains")
1247+
for result in response.get("results", []):
1248+
human_readable_output, indicators = format_investigate_output(result)
1249+
1250+
if len(missing_domains) > 0:
1251+
human_readable_output += f"Missing Domains: {','.join(missing_domains)}"
1252+
1253+
domain_indicator = indicators.get("domain") if include_context else None
1254+
domaintools_context = indicators.get("domaintools") if include_context else None
1255+
1256+
command_results_list.append(
1257+
CommandResults(
1258+
outputs_prefix="DomainTools",
1259+
outputs_key_field="Name",
1260+
indicator=domain_indicator,
1261+
outputs=domaintools_context,
1262+
readable_output=human_readable_output,
1263+
raw_response=result,
1264+
ignore_auto_extract=True,
1265+
)
1266+
)
11041267

11051268
if not command_results_list:
11061269
return_warning("No results.", exit=True)

Packs/DomainTools_Iris/Integrations/DomainTools_Iris/DomainTools_Iris.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ configuration:
4343
type: 8
4444
required: false
4545
section: Connect
46+
- display: Domain Result Type
47+
name: domain_result_type
48+
type: 15
49+
required: false
50+
additionalinfo: "Result type of the domain command: Iris returns full investigate results; Verdict returns only the domain risk score"
51+
defaultvalue: Iris
52+
options:
53+
- Iris
54+
- Verdict
55+
section: Collect
4656
- display: Source Reliability
4757
name: integrationReliability
4858
type: 15
@@ -2472,6 +2482,9 @@ script:
24722482
outputs:
24732483
- contextPath: Domain.Name
24742484
description: Name of the domain returned by the query.
2485+
# outputs:
2486+
# - contextPath: Domain.Name
2487+
# description: Name of the domain returned by the query.
24752488
dockerimage: demisto/vendors-sdk:1.0.0.3242986
24762489
runonce: false
24772490
script: '-'

Packs/DomainTools_Iris/pack_metadata.json

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,16 @@
22
"name": "DomainTools Iris Investigate",
33
"description": "Facilitates automation of key infrastructure characterization and hunting portions of the incident response process. Organizations will have access to essential domain profile, web crawl, SSL, and infrastructure data from within Cortex XSOAR. Requires a DomainTools Iris Investigate API key.",
44
"support": "partner",
5-
"currentVersion": "2.2.3",
5+
"currentVersion": "2.2.4",
66
"author": "DomainTools",
77
"url": "https://www.domaintools.com/support/",
88
"email": "memberservices@domaintools.com",
99
"created": "2020-04-14T00:00:00Z",
10-
"categories": [
11-
"Data Enrichment & Threat Intelligence"
12-
],
10+
"categories": ["Data Enrichment & Threat Intelligence"],
1311
"tags": [],
1412
"useCases": [],
1513
"keywords": [],
1614
"certification": "certified",
17-
"marketplaces": [
18-
"xsoar",
19-
"marketplacev2",
20-
"platform"
21-
],
22-
"supportedModules": [
23-
"X1",
24-
"X3",
25-
"X5",
26-
"ENT_PLUS",
27-
"agentix"
28-
]
29-
}
15+
"marketplaces": ["xsoar", "marketplacev2", "platform"],
16+
"supportedModules": ["X1", "X3", "X5", "ENT_PLUS", "agentix"]
17+
}

0 commit comments

Comments
 (0)