DBotPredictURLPhishing
Predict phishing URLs using a pre-trained model.
- Type
- python
- Pack
- PhishingURL
Source
import demistomock as demisto
from CommonServerPython import *
from CommonServerUserPython import *
import urllib
import pandas as pd
import base64
import dill
import copy
from tldextract import TLDExtract
from bs4 import BeautifulSoup
from typing import Literal
dill.settings["recurse"] = True
# Site-specific allowlist for safe pickle loading — extends the shared base with classes this site needs.
_ALLOWED_CLASSES: set[tuple[str, str]] = BASE_PICKLE_ALLOWED_CLASSES | {
# The Model class (defined in this script)
("__main__", "Model"),
# Scikit-learn pipeline and estimators
("sklearn.pipeline", "Pipeline"),
("sklearn.linear_model._logistic", "LogisticRegression"),
("sklearn.feature_extraction.text", "TfidfVectorizer"),
("sklearn.feature_extraction.text", "CountVectorizer"),
("sklearn.compose._column_transformer", "ColumnTransformer"),
("sklearn.preprocessing._data", "StandardScaler"),
("sklearn.preprocessing._label", "LabelEncoder"),
("sklearn.base", "BaseEstimator"),
("sklearn.base", "TransformerMixin"),
("sklearn.utils.deprecation", "DeprecationDict"),
# Dill internals (for legacy dill-serialized models)
("dill._dill", "_create_function"),
("dill._dill", "_create_code"),
("dill._dill", "_load_type"),
}
# Safe top-level modules whose internal submodules are all data-science code.
_SAFE_MODULE_PREFIXES = {"sklearn", "numpy", "pandas", "scipy"}
no_fetch_extract = TLDExtract(suffix_list_urls=None, cache_dir=False) # type: ignore
KEY_IMAGE_RASTERIZE = "image_b64"
KEY_IMAGE_HTML = "html"
MSG_SOMETHING_WRONG_IN_RASTERIZE = "Something went wrong with rasterize"
MSG_ENABLE_WHOIS = "Please enable whois integration for more accurate prediction"
MSG_MODEL_VERSION_IN_DEMISTO = "Model version in demisto: {}.{}"
MSG_NO_MODEL_IN_DEMISTO = "There is no existing model version in demisto"
MSG_FAILED_RASTERIZE = "Rasterize error: ERR_NAME_NOT_RESOLVED"
MSG_FAILED_RASTERIZE_TIMEOUT = "Timeout rasterize"
MSG_IMPOSSIBLE_CONNECTION = "Failed to establish a new connection - Name or service not known"
MSG_UPDATE_MODEL = "Update demisto model from docker model version {}.{}"
MSG_UPDATE_LOGO = "Update demisto model from docker model version {}.{} and transfering logos from demisto version {}.{}"
MSG_WRONG_CONFIG_MODEL = "Wrong configuration of the model"
MSG_NO_ACTION_ON_MODEL = "Use current model"
MSG_WHITE_LIST = "White List"
MSG_NEED_TO_UPDATE_RASTERIZE = "Please install and/or update rasterize pack"
URL_PHISHING_MODEL_NAME = "url_phishing_model"
OUT_OF_THE_BOX_MODEL_PATH = "/model/model_docker.pkl"
UNKNOWN_MODEL_TYPE = "UNKNOWN_MODEL_TYPE"
THRESHOLD_NEW_DOMAIN_MONTHS = 6
DOMAIN_AGE_KEY = f"New domain (less than {THRESHOLD_NEW_DOMAIN_MONTHS} months)"
MALICIOUS_VERDICT = "Malicious"
BENIGN_VERDICT = "Benign"
SUSPICIOUS_VERDICT = "Suspicious"
BENIGN_VERDICT_WHITELIST = "Benign - Top domains from Majestic"
UNKNOWN = "Unknown"
BENIGN_THRESHOLD = 0.5
SUSPICIOUS_THRESHOLD = 0.7
SCORE_INVALID_URL = -1.0
SCORE_BENIGN = 0.0 # type: float
GREEN_COLOR = RED_COLOR = VERDICT_MALICIOUS_COLOR = VERDICT_SUSPICIOUS_COLOR = VERDICT_BENIGN_COLOR = VERDICT_ERROR_COLOR = (
"**{}**"
)
MAPPING_VERDICT_COLOR = {
MALICIOUS_VERDICT: VERDICT_MALICIOUS_COLOR,
BENIGN_VERDICT: VERDICT_BENIGN_COLOR,
SUSPICIOUS_VERDICT: VERDICT_SUSPICIOUS_COLOR,
BENIGN_VERDICT_WHITELIST: VERDICT_BENIGN_COLOR,
}
SCORE_THRESHOLD = 0.6 # type: float
MODEL_KEY_URL_SCORE = "url_score"
MODEL_KEY_LOGO_FOUND = "logo_found"
MODEL_KEY_SEO = "seo"
MODEL_KEY_LOGO_IMAGE_BYTES = "image_bytes"
MODEL_KEY_LOGIN_FORM = "login_form"
KEY_CONTENT_DOMAIN = "Domain"
KEY_CONTENT_URL = "URL"
KEY_CONTENT_LOGO = "UseOfSuspiciousLogo"
KEY_CONTENT_LOGIN = "HasLoginForm"
KEY_CONTENT_URL_SCORE = "URLStaticScore"
KEY_CONTENT_SEO = "BadSEOQuality"
KEY_CONTENT_AGE = "NewDomain"
KEY_CONTENT_VERDICT = "FinalVerdict"
KEY_CONTENT_IS_WHITELISTED = "TopMajesticDomain"
KEY_CONTENT_DBOT_SCORE = "DBotScore"
KEY_HR_DOMAIN = "Domain"
KEY_HR_URL = "Url"
KEY_HR_SEO = "Search engine optimization"
KEY_HR_LOGIN = "Is there a Login form?"
KEY_HR_LOGO = "Suspicious use of company logo"
KEY_HR_URL_SCORE = "URL severity score (from 0 to 1)"
KEY_CONTENT_SUMMARY_URL = "URL"
KEY_CONTENT_SUMMARY_FINAL_VERDICT = "FinalVerdict"
KEY_FINAL_VERDICT = "Final Verdict"
WEIGHT_HEURISTIC = {DOMAIN_AGE_KEY: 3, MODEL_KEY_LOGIN_FORM: 1, MODEL_KEY_SEO: 1, MODEL_KEY_URL_SCORE: 2, MODEL_KEY_LOGO_FOUND: 1}
MAPPING_VERDICT_TO_DISPLAY_VERDICT = {
MODEL_KEY_SEO: {True: RED_COLOR.format("Bad"), False: GREEN_COLOR.format("Good")},
MODEL_KEY_LOGO_FOUND: {True: RED_COLOR.format("Suspicious"), False: GREEN_COLOR.format("Not Suspicious")},
MODEL_KEY_LOGIN_FORM: {True: RED_COLOR.format("Yes"), False: GREEN_COLOR.format("No")},
DOMAIN_AGE_KEY: {
True: RED_COLOR.format("Less than 6 months ago"),
False: GREEN_COLOR.format("More than 6 months ago"),
None: None,
},
} # type: Dict
VERDICT_TO_INT = {MALICIOUS_VERDICT: 3, BENIGN_VERDICT: 1, BENIGN_VERDICT_WHITELIST: 1, SUSPICIOUS_VERDICT: 2}
WAIT_TIME_RASTERIZE = 5
TIMEOUT_RASTERIZE = 120
class Model:
"""Abstract class that represents the class of the built-in phishing model."""
clf: Any # sklearn.pipeline.Pipeline
df_voc: dict
top_domains: dict
logos_dict: dict
custom_logo_associated_domain: dict
def predict(self, x_pred: pd.DataFrame) -> dict: # type: ignore[empty-body]
...
def update_model(
self,
top_domains: dict,
logos_dict: dict,
custom_logo_associated_domain: dict,
): ...
class ModelData(dict[Literal["top_domains", "logos_dict", "custom_logo_associated_domain"], dict]):
"""Abstract class that represents the format of the data stored in the server."""
def delete_model():
res = demisto.executeCommand("deleteMLModel", {"modelName": URL_PHISHING_MODEL_NAME})
demisto.debug(f"Deleted model. server response: {res}")
def save_model_data(model_data: ModelData):
"""
Load and save model from the model in the docker
:return: None
"""
res = demisto.executeCommand(
"createMLModel",
{
"modelData": b64encode_string(json.dumps(model_data)),
"modelName": URL_PHISHING_MODEL_NAME,
"modelLabels": [MALICIOUS_VERDICT, BENIGN_VERDICT, SUSPICIOUS_VERDICT],
"modelOverride": "true",
"modelHidden": True,
"modelType": "url_phishing",
},
)
if is_error(res):
raise DemistoException(get_error(res))
def extract_and_save_old_model_data(model_data: str, minor_version: int) -> Optional[ModelData]: # pragma: no cover
"""Update the model to the new version. This will be eventually deleted."""
delete_model()
if minor_version == 0: # no changes were made to the model by the user
demisto.debug("Old version is unchanged")
return None
import warnings
warnings.filterwarnings("ignore", module="sklearn")
try:
raw_bytes = base64_to_bytes(model_data)
model = cast(Model, safe_pickle_loads(raw_bytes, _ALLOWED_CLASSES, _SAFE_MODULE_PREFIXES))
except UnsafePickleError as e:
demisto.error(f"Security: blocked unsafe model payload: {e}")
return None
except Exception as e:
demisto.debug(f"Unable to load model data: {e}")
return None
model_data = cast(
ModelData,
{
"top_domains": model.top_domains,
"logos_dict": model.logos_dict,
"custom_logo_associated_domain": model.custom_logo_associated_domain,
},
)
save_model_data(model_data)
return model_data
def get_model_data() -> Optional[ModelData]:
res = demisto.executeCommand("getMLModel", {"modelName": URL_PHISHING_MODEL_NAME})[0]
if is_error(res):
demisto.debug(f"Model not found: {get_error(res)}")
return None
extra_data = dict_safe_get(res, ("Contents", "model", "extra"))
model_data = dict_safe_get(res, ("Contents", "modelData"))
if isinstance(extra_data, dict) and "minor" in extra_data: # this means the old model exists as a pickled object
demisto.debug(f"Old model found. {extra_data=}")
return extract_and_save_old_model_data(model_data, extra_data["minor"])
return cast(ModelData, json.loads(b64decode_string(model_data)))
def load_model_from_docker(path: str = OUT_OF_THE_BOX_MODEL_PATH) -> Model:
with open(path, "rb") as f:
return cast(Model, dill.load(f)) # guardrails-disable-line
def load_model() -> Model:
model = load_model_from_docker()
model_data = get_model_data()
if model_data:
model.update_model(**model_data) # type: ignore[misc]
return model
def b64encode_string(string: str) -> str:
return base64.b64encode(string.encode()).decode()
def b64decode_string(string: str) -> str:
return base64_to_bytes(string).decode()
def base64_to_bytes(base64_string: str) -> bytes:
"""
Transform a base64 string into bytes
:param base64_string:
:return:
"""
return base64.b64decode(base64_string.encode())
def extract_domainv2(url: str) -> str:
ext = no_fetch_extract(url)
return ext.domain + "." + ext.suffix
def in_white_list(model: Model, url: str) -> bool:
"""
Check if url belongs to the Model whitelist
:param model: model which contains top_domains attribute
:param url: url to check
:return:
"""
return extract_domainv2(url) in model.top_domains
def get_colored_pred_json(pred_json: dict) -> dict:
"""
Create copy and color json values according to their values.
:param pred_json: json to color
:return: json
"""
return copy.deepcopy(pred_json) | {
MODEL_KEY_SEO: MAPPING_VERDICT_TO_DISPLAY_VERDICT[MODEL_KEY_SEO][pred_json[MODEL_KEY_SEO]],
MODEL_KEY_LOGO_FOUND: MAPPING_VERDICT_TO_DISPLAY_VERDICT[MODEL_KEY_LOGO_FOUND][pred_json[MODEL_KEY_LOGO_FOUND]],
MODEL_KEY_LOGIN_FORM: MAPPING_VERDICT_TO_DISPLAY_VERDICT[MODEL_KEY_LOGIN_FORM][pred_json[MODEL_KEY_LOGIN_FORM]],
DOMAIN_AGE_KEY: MAPPING_VERDICT_TO_DISPLAY_VERDICT[DOMAIN_AGE_KEY][pred_json[DOMAIN_AGE_KEY]],
}
def create_x_pred(output_rasterize: dict, url: str) -> pd.DataFrame:
"""
Create dataframe to predict from the rasterize output
:param output_rasterize: Dict from the output of rasterize command
:param url: url to examine
:return: pd.DataFrame
"""
website64 = output_rasterize.get(KEY_IMAGE_RASTERIZE, None)
html = output_rasterize.get(KEY_IMAGE_HTML, None)
X_pred = pd.DataFrame(columns=["name", "image", "html"])
X_pred.loc[0] = [url, website64, html]
return X_pred
def prepend_protocol(url: str, protocol: str, www: bool = True) -> str:
"""forceModel
Append a protocol name (usually http or https) and www to a url
:param url: url
:param protocol: protocol we want to add (usually http or https)
:return: str
"""
p = urllib.parse.urlparse(url, protocol) # type: ignore
netloc = p.netloc or p.path
path = p.path if p.netloc else ""
if not netloc.startswith("www.") and www:
netloc = "www." + netloc
p = urllib.parse.ParseResult(protocol, netloc, path, *p[3:]) # type: ignore
return p.geturl()
def return_entry_summary(
pred_json: dict,
url: str,
is_white_listed: bool,
output_rasterize: dict,
verdict: str,
reliability: str = DBotScoreReliability.A_PLUS,
**_,
) -> Optional[dict[str, Any]]:
"""
Return entry to demisto
:param pred_json: json with output of the model
:param url: url
:param whitelist: if url belongs to whitelist of the model
:param reliability: reliability of the source providing the intelligence data.
:return: entry to demisto
"""
if is_white_listed:
return None
if verdict == BENIGN_VERDICT_WHITELIST:
verdict = BENIGN_VERDICT
if not pred_json:
url_score = SCORE_BENIGN
url_score_colored = (GREEN_COLOR if url_score < SCORE_THRESHOLD else RED_COLOR).format(url_score)
else:
url_score = round(pred_json[MODEL_KEY_URL_SCORE], 2)
url_score_colored = (GREEN_COLOR if url_score < SCORE_THRESHOLD else RED_COLOR).format(url_score)
pred_json_colored = get_colored_pred_json(pred_json) if pred_json else {}
domain = extract_domainv2(url)
explain = {
KEY_CONTENT_DOMAIN: domain,
KEY_CONTENT_URL: url,
KEY_CONTENT_LOGO: str(pred_json.get(MODEL_KEY_LOGO_FOUND, UNKNOWN)),
KEY_CONTENT_LOGIN: str(pred_json.get(MODEL_KEY_LOGIN_FORM, UNKNOWN)),
KEY_CONTENT_URL_SCORE: url_score,
KEY_CONTENT_SEO: str(pred_json.get(MODEL_KEY_SEO, UNKNOWN)),
KEY_CONTENT_VERDICT: verdict,
KEY_CONTENT_IS_WHITELISTED: str(is_white_listed),
}
dbot_score = Common.DBotScore(
indicator=url,
indicator_type=DBotScoreType.URL,
integration_name="DBotPhishingURL",
score=VERDICT_TO_INT.get(verdict),
reliability=reliability,
)
context_DBot_score = dbot_score.to_context().get(dbot_score.get_context_path())
if pred_json and pred_json[DOMAIN_AGE_KEY] is not None:
explain[KEY_CONTENT_AGE] = str(pred_json[DOMAIN_AGE_KEY])
explain_hr = {
KEY_HR_URL: url,
KEY_HR_SEO: str(pred_json_colored.get(MODEL_KEY_SEO, UNKNOWN)),
KEY_HR_LOGIN: str(pred_json_colored.get(MODEL_KEY_LOGIN_FORM, UNKNOWN)),
KEY_HR_LOGO: str(pred_json_colored.get(MODEL_KEY_LOGO_FOUND, UNKNOWN)),
KEY_HR_URL_SCORE: url_score_colored,
}
if pred_json and pred_json[DOMAIN_AGE_KEY] is not None:
explain_hr[DOMAIN_AGE_KEY] = str(pred_json_colored[DOMAIN_AGE_KEY])
if verdict == BENIGN_VERDICT:
return_entry = {
"Type": entryTypes["note"],
"ContentsFormat": formats["json"],
"HumanReadable": tableToMarkdown(f"Phishing prediction evidence | {domain}", explain_hr),
"Contents": explain,
"EntryContext": {"DBotPredictURLPhishing": explain},
}
else:
return_entry = {
"Type": entryTypes["note"],
"ContentsFormat": formats["json"],
"HumanReadable": tableToMarkdown(f"Phishing prediction evidence | {domain}", explain_hr),
"Contents": explain,
"EntryContext": {"DBotPredictURLPhishing": explain, KEY_CONTENT_DBOT_SCORE: context_DBot_score},
"Tags": ["DBOT_URL_PHISHING_MALICIOUS"],
}
return_results(return_entry)
if pred_json:
image = pred_json[MODEL_KEY_LOGO_IMAGE_BYTES]
if not image:
image = base64_to_bytes(output_rasterize.get(KEY_IMAGE_RASTERIZE)) # type: ignore[arg-type]
res = fileResult(filename="Logo detection engine", data=image)
res["Type"] = entryTypes["image"]
if pred_json[MODEL_KEY_LOGO_FOUND]:
res["Tags"] = ["DBOT_URL_PHISHING_MALICIOUS"]
return_results(res)
return explain
def return_entry_white_list(url: str):
"""
Create syntethci entry when url belongs to whitelist
:param url: url
:return:
"""
explain = {
KEY_CONTENT_DOMAIN: extract_domainv2(url),
KEY_CONTENT_URL: url,
KEY_CONTENT_AGE: MSG_WHITE_LIST,
KEY_CONTENT_LOGO: MSG_WHITE_LIST,
KEY_CONTENT_LOGIN: MSG_WHITE_LIST,
KEY_CONTENT_URL_SCORE: MSG_WHITE_LIST,
KEY_CONTENT_SEO: MSG_WHITE_LIST,
}
explain_hr = {
KEY_HR_URL: url,
KEY_HR_SEO: MSG_WHITE_LIST,
DOMAIN_AGE_KEY: MSG_WHITE_LIST,
KEY_HR_LOGIN: MSG_WHITE_LIST,
KEY_HR_LOGO: MSG_WHITE_LIST,
KEY_HR_URL_SCORE: MSG_WHITE_LIST,
}
verdict_hr = {"Verdict": BENIGN_VERDICT, "URL": url}
return_entry = {
"Type": entryTypes["note"],
"ContentsFormat": formats["json"],
"HumanReadable": tableToMarkdown("Verdict", verdict_hr) + tableToMarkdown("Report", explain_hr),
"Contents": explain,
"EntryContext": {"DBotPredictURLPhishing": explain},
}
return_results(return_entry)
def get_score(pred_json: dict) -> int:
use_age = False
use_logo = False
if pred_json[DOMAIN_AGE_KEY]:
use_age = True
if pred_json[MODEL_KEY_LOGO_FOUND]:
use_logo = True
domain_age_key = 0 if pred_json[DOMAIN_AGE_KEY] is None else pred_json[DOMAIN_AGE_KEY]
total_weight_used = (
WEIGHT_HEURISTIC[DOMAIN_AGE_KEY] * use_age
+ WEIGHT_HEURISTIC[MODEL_KEY_LOGIN_FORM]
+ WEIGHT_HEURISTIC[MODEL_KEY_SEO]
+ WEIGHT_HEURISTIC[MODEL_KEY_URL_SCORE]
+ WEIGHT_HEURISTIC[MODEL_KEY_LOGO_FOUND] * use_logo
)
score = (
use_age * WEIGHT_HEURISTIC[DOMAIN_AGE_KEY] * domain_age_key
+ WEIGHT_HEURISTIC[MODEL_KEY_LOGIN_FORM] * pred_json[MODEL_KEY_LOGIN_FORM]
+ WEIGHT_HEURISTIC[MODEL_KEY_SEO] * pred_json[MODEL_KEY_SEO]
+ WEIGHT_HEURISTIC[MODEL_KEY_URL_SCORE] * pred_json[MODEL_KEY_URL_SCORE]
+ use_logo * WEIGHT_HEURISTIC[MODEL_KEY_LOGO_FOUND] * pred_json[MODEL_KEY_LOGO_FOUND]
) / total_weight_used
return score
def get_verdict(pred_json: dict, is_white_listed: bool) -> tuple[float, str]:
"""
Return verdict of the url based on the output of the model
:param pred_json: output from the model
:return:
"""
if is_white_listed:
return SCORE_BENIGN, BENIGN_VERDICT
score = get_score(pred_json)
if pred_json[MODEL_KEY_LOGO_FOUND]:
return score, MALICIOUS_VERDICT
if score < BENIGN_THRESHOLD:
return score, BENIGN_VERDICT
if score < SUSPICIOUS_THRESHOLD:
return score, SUSPICIOUS_VERDICT
return score, MALICIOUS_VERDICT
def create_dict_context(url, verdict, pred_json, score, is_white_listed, output_rasterize) -> dict:
return {
"url_redirect": url,
"url": url,
"verdict": verdict,
"pred_json": pred_json,
"score": score,
"is_white_listed": is_white_listed,
"output_rasterize": output_rasterize,
}
def extract_created_date(entry: dict) -> Union[bool, None]:
"""
Check if domain age is younger than THRESHOLD_NEW_DOMAIN_YEAR year
:param entry_list: output of the whois command
:return: bool
"""
if not is_error(entry):
date_str = dict_safe_get(entry, ("EntryContext", "Domain(val.Name && val.Name == obj.Name)", "WHOIS", "CreationDate"))
if date_str:
date = datetime.strptime(date_str, "%d-%m-%Y")
threshold_date = datetime.now() - timedelta(days=THRESHOLD_NEW_DOMAIN_MONTHS * 30)
return date > threshold_date
return None
def return_and_remove_additional_results(results: list, from_index: int):
"""Return and remove the extra unneeded results returned from a command call.
In XSOAR 8 log results are usually returned with sub-commands if debug-mode=true"""
if results[from_index:]:
return_results(results[from_index:])
del results[from_index:]
demisto.debug(f"removed and returned {from_index} outputs")
def weed_rasterize_errors(urls: list[str], res_rasterize: list[Union[dict, str]]):
"""Remove the URLs that failed rasterization and return them."""
error_idx = [i for (i, res) in enumerate(res_rasterize) if not isinstance(res, dict)][
::-1
] # reverse the list as it will be used to remove elements.
if error_idx:
return_results(
CommandResults(
readable_output=tableToMarkdown(
"The following URLs failed rasterize and were skipped:",
[{"URL": urls.pop(i), "Message": res_rasterize.pop(i)} for i in error_idx],
["URL", "Message"],
)
)
)
def rasterize_command(urls: Union[list[str], str], rasterize_timeout: int) -> list[Union[dict, str]]:
res_rasterize: list[dict] = demisto.executeCommand( # type: ignore
"rasterize", {"type": "json", "url": urls, "wait_time": WAIT_TIME_RASTERIZE, "execution-timeout": rasterize_timeout}
)
demisto.debug(f"Rasterize Data: {res_rasterize}")
return_and_remove_additional_results(res_rasterize, len(urls) if isinstance(urls, list) else 1)
return [res["Contents"] or res["HumanReadable"] for res in res_rasterize]
def rasterize_urls(urls: list[str], rasterize_timeout: int) -> list[dict]:
urls = [url.removeprefix("http://") for url in urls]
res_rasterize = rasterize_command(urls, rasterize_timeout)
if len(res_rasterize) < len(urls): # check for errors in the response
demisto.info(f"Rasterize response is too short, running command for each URL\n{res_rasterize=}\n{urls=}")
rasterize_runs = map(rasterize_command, urls, [rasterize_timeout] * len(urls))
res_rasterize = sum(rasterize_runs, [])
weed_rasterize_errors(urls, res_rasterize)
return cast(list[dict], res_rasterize)
def get_whois_verdict(domains: list[str]) -> list:
"""Check domain age from WHOIS command"""
default = [None] * len(domains)
if isCommandAvailable("whois"):
try:
res = demisto.executeCommand("whois", {"query": domains, "execution-timeout": 5})
return_and_remove_additional_results(res, len(domains))
return res or default
except Exception as e:
demisto.debug(str(e))
else:
return_results(MSG_ENABLE_WHOIS)
return default
def get_predictions_for_urls(
model: Model,
urls: list[str],
requested_urls: list[str],
force_model: bool,
debug: bool,
rasterize_timeout: int,
protocol: str,
) -> Optional[list[dict]]:
"""Generate predictions for the given URL list
:param model: Prediction model to use.
:param urls: List of URLs to generate predictions for.
:param requested_urls: List of URLs requested to be checked, in their original form.
:param force_model: When set, the model will be run even if the URL is whitelisted.
:param debug: Enable debug output messages.
:param rasterize_timeout: Timeout for the rasterize call.
:param protocol: Protocol override to be used in the URLs (e.g. "http", "https")
:return: A list of dictionaries containing the prediction results for each URL, or None if no results were generated.
"""
domains = list(map(extract_domainv2, urls))
rasterize_outputs = rasterize_urls(urls, rasterize_timeout)
if not rasterize_outputs:
return_results("All URLs failed to be rasterized. Skipping prediction.")
return None
whois_results = get_whois_verdict(domains)
results = []
for url, requested_url, res_whois, output_rasterize in zip(urls, requested_urls, whois_results, rasterize_outputs):
# Check is domain in white list - If yes we don't run the model
if in_white_list(model, url):
is_white_listed = True
if not force_model:
results.append(
create_dict_context(requested_url, BENIGN_VERDICT_WHITELIST, {}, SCORE_BENIGN, is_white_listed, {})
)
continue
else:
is_white_listed = False
x_pred = create_x_pred(output_rasterize, prepend_protocol(url, protocol))
pred_json = model.predict(x_pred)
if debug:
return_results(pred_json["debug_top_words"])
return_results(pred_json["debug_found_domains_list"])
return_results(pred_json["seo"])
return_results(pred_json["debug_image"])
pred_json[DOMAIN_AGE_KEY] = extract_created_date(res_whois)
score, verdict = get_verdict(pred_json, is_white_listed)
results.append(create_dict_context(requested_url, verdict, pred_json, score, is_white_listed, output_rasterize))
return results
def return_general_summary(results: list[dict], tag: str = "Summary") -> list[dict]:
df_summary = pd.DataFrame()
df_summary["URL"] = [x.get("url_redirect") for x in results]
df_summary[KEY_FINAL_VERDICT] = [
MAPPING_VERDICT_COLOR.get(x.get("verdict"), VERDICT_ERROR_COLOR).format(x.get("verdict")) # type: ignore
for x in results # type: ignore
]
summary_context = [
{
KEY_CONTENT_SUMMARY_URL: x.get("url_redirect"),
KEY_CONTENT_SUMMARY_FINAL_VERDICT: BENIGN_VERDICT,
KEY_CONTENT_IS_WHITELISTED: "True",
}
for x in results
if x.get("is_white_listed")
]
df_summary_json = df_summary.to_dict(orient="records")
return_entry = {
"Type": entryTypes["note"],
"ContentsFormat": formats["json"],
"HumanReadable": tableToMarkdown(
"Phishing prediction summary for URLs", df_summary_json, headers=["URL", KEY_FINAL_VERDICT]
),
"Contents": summary_context,
"EntryContext": {"DBotPredictURLPhishing": summary_context},
}
if tag is not None:
return_entry["Tags"] = [f"DBOT_URL_PHISHING_{tag}"]
return_results(return_entry)
return df_summary_json
def return_detailed_summary(results: list, reliability: str) -> list[dict[str, str]]:
outputs = []
results.sort(key=lambda x: x["score"])
for result in results:
if result.get("score") == SCORE_INVALID_URL:
continue
summary_json = return_entry_summary(**result, reliability=reliability)
if summary_json:
outputs.append(summary_json)
return outputs
def extract_urls(text: str) -> list[str]:
res = demisto.executeCommand("extractIndicators", {"text": text})
if is_error(res):
raise DemistoException(get_error(res))
return list(set(json.loads(res[0]["Contents"]).get("URL", [])))
def get_final_urls(urls: list[str], max_urls: int, model: Model) -> list[str]:
final_url = []
seen = []
low_priority_urls = []
i = 0
for url in urls:
if i < max_urls:
if extract_domainv2(url) in seen or extract_domainv2(url) in model.top_domains:
low_priority_urls.append(url)
else:
final_url.append(url)
seen.append(extract_domainv2(url))
i += 1
if len(final_url) < max_urls:
final_url += low_priority_urls[: min(len(low_priority_urls), max_urls - len(final_url))]
return final_url
def extract_embedded_urls_from_html(html: str) -> list[str]:
return [a.get("href") for a in BeautifulSoup(html).find_all("a") if a.get("href")] # type: ignore
def get_urls_to_run(
email_body: str,
email_html: str,
urls_argument: Union[list, str],
max_urls: int,
model: Model,
msg_list: list[str],
debug: bool,
) -> tuple[list[str], list[str], list[str]]:
if email_body:
urls_email_body = extract_urls(email_body)
else:
urls_email_body = extract_urls(BeautifulSoup(email_html).get_text()) if email_html else []
if email_html:
urls_email_html = extract_embedded_urls_from_html(email_html)
else:
urls_email_html = []
urls_only = argToList(urls_argument)
urls = list(set(urls_email_body + urls_only + urls_email_html))
mailto_urls = [url for url in urls if url.startswith("mailto:")]
urls = [item for item in urls if item not in mailto_urls]
if mailto_urls:
return_results(
CommandResults(readable_output=f'URLs that start with "mailto:" cannot be rasterized.\nURL: {mailto_urls}')
)
if not urls:
return_results("No URLs for prediction.")
return [], [], msg_list
urls = get_final_urls(urls, max_urls, model)
unescaped_urls = demisto.executeCommand("UnEscapeURLs", {"input": urls}) or []
unescaped_urls = [res["Contents"] for res in unescaped_urls] # type: ignore
if debug:
return_results(unescaped_urls)
return unescaped_urls, urls, msg_list
def main():
msg_list: list = []
try:
args = demisto.args()
reset_model = args.get("resetModel") == "True"
debug = args.get("debug") == "True"
force_model = args.get("forceModel") == "True"
email_body = args.get("emailBody", "")
email_html = args.get("emailHTML", "")
max_urls = cast(int, arg_to_number(args.get("maxNumberOfURL", 5), "maxNumberOfURL", required=True))
urls_argument = args.get("urls", "")
rasterize_timeout = arg_to_number(args.get("rasterize_timeout", TIMEOUT_RASTERIZE)) or 0
reliability = DBotScoreReliability.get_dbot_score_reliability_from_str(
args.get("reliability", DBotScoreReliability.A_PLUS)
)
protocol = demisto.args().get("defaultRequestProtocol", "HTTP").lower()
if reset_model:
delete_model()
model = load_model()
urls, requested_urls, msg_list = get_urls_to_run(email_body, email_html, urls_argument, max_urls, model, msg_list, debug)
if urls:
results = get_predictions_for_urls(model, urls, requested_urls, force_model, debug, rasterize_timeout, protocol)
if results:
general_summary = return_general_summary(results)
detailed_summary = return_detailed_summary(results, reliability)
if debug:
return_results(msg_list)
return general_summary, detailed_summary, msg_list
except Exception as e:
return_error(f"Failed to execute URL Phishing script. Error: {e}")
finally:
demisto.debug(f"{msg_list=}")
if __name__ in ["__main__", "__builtin__", "builtins"]:
main()
README
Predict phishing URLs using a pre-trained model.
Security Recommendations
This script uses the Rasterize integration. If this script is used to rasterize untrusted URLs, we strongly recommend following the security recommendations included at the Rasterize Documentation.
Script Data
| Name | Description |
|---|---|
| Script Type | python3 |
| Tags | ml |
| Cortex XSOAR Version | 6.0.0 |
Used In
This script is used in the following playbooks and scripts.
Phishing - Machine Learning Analysis
Inputs
| Argument Name | Description |
|---|---|
| urls | Space-separated list of URLs. |
| emailBody | Body of the email for URL extraction. |
| emailHTML | HTML of the email for URL extraction. |
| maxNumberOfURL | Maximum number of extracted URLs on which to run the model. |
| forceModel | Whether to force the model to run if the URL belongs to the whitelist. If True, the model will run in every case. If False, the model will run only if the URL does not belong to the whitelist. |
| resetModel | Whether to reset the model to the model existing in Docker. |
| defaultRequestProtocol | The protocol to use when calling the URLs. This argument effects the calls sent by the model only and has no effect on the rasterize or whois commands. |
| debug | Whether to enter debug mode. |
| reliability | Reliability of the source providing the intelligence data. |
Outputs
| Path | Description | Type |
|---|---|---|
| DBotPredictURLPhishing.URL | URL on which the model ran. | String |
| DBotPredictURLPhishing.FinalVerdict | Final verdict of the URL. | String |
| DBotPredictURLPhishing.UseOfSuspiciousLogo | Whether a logo (from our list of top most use company for phishing) has been fraudulently used. Our predefined list of logos is: Paypal, Instagram, Gmail, Outlook, Linkedin, Facebook, Ebay, amazon, Google, Microsoft. | String |
| DBotPredictURLPhishing.HasLoginForm | Whether there is a login form in the HTML. Usually phishing attacks aim to steal credentials from the victim and attackers using login forms to retrieve this information. | String |
| DBotPredictURLPhishing.URLStaticScore | Probability for the URL to be malicious based only on the URL syntax. | Number |
| DBotPredictURLPhishing.BadSEOQuality | Whether the domain has a good search engine optimization. Malicious domains tend to have a poor SEO. | String |
| DBotPredictURLPhishing.NewDomain | Whether the domain is younger than 6 months. New domains tend to be malicious. | String |
| DBotPredictURLPhishing.TopMajesticDomain | Whether the domain belongs to the top Majestic domain list. If it does, we will always consider this domain as benign. | String |
| DBotScore.Score | Severity score. | Number |
Script Examples
Example command
!DBotPredictURLPhishing urls="http://google.com"
Context Example
{
"DBotPredictURLPhishing": [
{
"FinalVerdict": "Benign",
"TopMajesticDomain": "True",
"URL": "http://google.com"
}
]
}
Human Readable Output
Phishing prediction summary for URLs
URL Final Verdict http://google.com Benign - whitelisted