DBotUpdateLogoURLPhishing
Add, remove, or modify logos from the URL Phishing model.
python · Phishing URL
Source
import demistomock as demisto from CommonServerPython import * from CommonServerUserPython import * import io import dill from PIL import Image import traceback from typing import Literal from collections.abc import Iterable import numpy as np import cv2 as cv # 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"} URL_PHISHING_MODEL_NAME = "url_phishing_model" MSG_EMPTY_NAME_OR_URL = "Empty logo name or/and logo image ID" MSG_EMPTY_LOGO_NAME = "Empty logo name argument" OOB_VERSION_INFO_KEY = "oob_version" OUT_OF_THE_BOX_MODEL_PATH = "/model/model_docker.pkl" MALICIOUS_VERDICT = "malicious" BENIGN_VERDICT = "benign" SUSPICIOUS_VERDICT = "suspicious" MSG_WRONG_CONFIGURATION = "Wrong configuration of the model" MSG_TRANSFER_LOGO = "Transfer logo from demisto model into new docker model" MSG_ERROR_READING_MODEL = "Error reading model %s from Demisto" MSG_NEED_TO_KNOW_WHICH_ACTION = "Need to choose one of the action: Add logo/ Remove logo/ Modify logo/ Display logos" KEY_ADD_LOGO = "AddLogo" KEY_REMOVE_LOGO = "RemoveLogo" KEY_DISPLAY_LOGOS = "DisplayAllLogos" KEY_MODIFY_LOGO = "ModifiedDomainForLogo" 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 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 b64encode_string(string: str) -> str: return base64.b64encode(string.encode()).decode() def b64decode_string(string: str) -> str: return base64.b64decode(string.encode()).decode() def delete_model(): res = demisto.executeCommand("deleteMLModel", {"modelName": URL_PHISHING_MODEL_NAME}) demisto.debug(f"Deleted model. server response: {res}") def load_old_model_data(encoded_model: str) -> ModelData: # pragma: no cover """Update the model to the new version""" import warnings warnings.filterwarnings("ignore", module="sklearn") try: raw_bytes = base64.b64decode(encoded_model.encode()) 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}") raise DemistoException(f"Security: blocked unsafe model payload: {e}") except Exception as e: demisto.debug(f"Unable to load model data: {e}") raise DemistoException(f"Unable to load model data: {e}") if demisto.getArg("action") != KEY_DISPLAY_LOGOS: delete_model() return model_to_data(model) def save_model_data(model_data: ModelData): """ Encode base 64 the model and call load_oob_model_from_model64 :param model: URL Phishing model :return: msg """ res = demisto.executeCommand( "createMLModel", { "modelData": b64encode_string(json.dumps(model_data)), "modelName": URL_PHISHING_MODEL_NAME, "modelLabels": [MALICIOUS_VERDICT, BENIGN_VERDICT], "modelOverride": "true", "modelHidden": True, "modelType": "url_phishing", }, ) if is_error(res): raise DemistoException(get_error(res)) demisto.debug(f"Saved data: {res}") def model_to_data(model: Model) -> ModelData: return cast( ModelData, { "top_domains": model.top_domains, "logos_dict": model.logos_dict, "custom_logo_associated_domain": model.custom_logo_associated_domain, }, ) def load_data_from_docker(path=OUT_OF_THE_BOX_MODEL_PATH) -> ModelData: """ Load model from docker :param path: path of the model in the docker :return: URL Phishing model """ with open(path, "rb") as f: return model_to_data(cast(Model, dill.load(f))) # guardrails-disable-line def load_data_from_xsoar() -> 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 load_old_model_data(model_data) return cast(ModelData, json.loads(b64decode_string(model_data))) def image_from_base64_to_bytes(base64_message: str): """ Transform image from base64 string into bytes :param base64_message: :return: """ return base64.b64decode(base64_message.encode()) def get_concat_logo_single_image(logo_list: Iterable[str]): byte_images = map(image_from_base64_to_bytes, logo_list) number_of_image_per_row = 5 width_new, height_new = 300, 300 images = [Image.open(io.BytesIO(image_bytes)) for image_bytes in byte_images] total_number_of_images = len(images) total_width = number_of_image_per_row * width_new max_height = (total_number_of_images // number_of_image_per_row + 1) * height_new new_im = Image.new("RGB", (total_width, max_height)) number_image_x = 0 x_offset = 0 y_offset = 0 new_size = (width_new, height_new) for im in images: im = im.resize(new_size) # type:ignore[assignment] if number_image_x >= number_of_image_per_row: x_offset = 0 y_offset += height_new number_image_x = 0 new_im.paste(im, (x_offset, y_offset)) x_offset += width_new number_image_x += 1 buf = io.BytesIO() new_im.save(buf, format="JPEG") return buf.getvalue() def decode_image(base64_message: str) -> np.ndarray: """ Decode image from base64 to numpy array of pixels :param base64_message: str representing the encoded image :return: numpy.narray representing the image """ base64_message = base64.decodebytes(base64_message.encode()) nparr = np.frombuffer(base64_message, np.uint8) return cv.imdecode(nparr, cv.IMREAD_GRAYSCALE) # pylint: disable=E1101 def update_top_domain(top_domains: dict, remove_list, add_list): for item_to_remove in remove_list: if top_domains.get(item_to_remove, -1) == 0: top_domains.pop(item_to_remove, None) for item_to_add in add_list: top_domains.setdefault(item_to_add, 0) def add_new_logo(model_data: ModelData, logo_name, logo_image_id, associated_domains): try: res = demisto.getFilePath(logo_image_id) with open(res["path"], "rb") as file: logo_content = file.read() if logo_name in model_data["logos_dict"]: return_error(f"The logo name {logo_name!r} is already in use. Please use another logo name.") encoded_image = base64.b64encode(logo_content).decode() imm_arr = decode_image(encoded_image) if imm_arr is None: raise DemistoException("The file is not a valid image.") model_data["logos_dict"][logo_name] = encoded_image model_data["custom_logo_associated_domain"][logo_name] = associated_domains update_top_domain(model_data["top_domains"], [], associated_domains) return f"Logo {logo_name!r} successfully added." except (ValueError, KeyError): return_error(f"File not found: {logo_image_id}") except Exception as e: return_error(f"Unable to use image. Error: {e}") def remove_logo(model_data: ModelData, logo_name): if logo_name not in model_data["logos_dict"] or logo_name not in model_data["custom_logo_associated_domain"]: return_error(f"Logo name {logo_name!r} not found.") update_top_domain(model_data["top_domains"], model_data["custom_logo_associated_domain"][logo_name], []) model_data["logos_dict"].pop(logo_name) model_data["custom_logo_associated_domain"].pop(logo_name) return f"Logo {logo_name!r} successfully removed." def update_domain_for_custom_logo(model_data: ModelData, logo_name, associated_domains): if logo_name not in model_data["logos_dict"] or logo_name not in model_data["custom_logo_associated_domain"]: return_error(f"Logo name {logo_name!r} not found.") update_top_domain(model_data["top_domains"], model_data["custom_logo_associated_domain"][logo_name], associated_domains) model_data["custom_logo_associated_domain"][logo_name] = associated_domains return f"Logo {logo_name!r} successfully modified." def display_all_logos(model_data: ModelData): description = [] for name in model_data["logos_dict"]: custom_associated_logo = model_data["custom_logo_associated_domain"].get(name, "") description.append( "{} ({}, {})".format(name, "Custom Logo", ",".join(custom_associated_logo)) if name in model_data["custom_logo_associated_domain"] else "{} ({})".format(name, "Default Logo") ) merged_logos = get_concat_logo_single_image(model_data["logos_dict"].values()) return fileResult(filename=", ".join(description), data=merged_logos, file_type=entryTypes["image"]) def execute_action(model_data: ModelData, action, logo_name, logo_image_id, associated_domains): if action == KEY_ADD_LOGO: return add_new_logo(model_data, logo_name, logo_image_id, associated_domains) elif action == KEY_REMOVE_LOGO: return remove_logo(model_data, logo_name) elif action == KEY_MODIFY_LOGO: return update_domain_for_custom_logo(model_data, logo_name, associated_domains) return display_all_logos(model_data) def verify_args(action, logo_image_id, logo_name): if (action == KEY_ADD_LOGO) and not (logo_image_id or logo_name): return_error(MSG_EMPTY_NAME_OR_URL) elif (action == KEY_REMOVE_LOGO) and (not logo_name): return_error(MSG_EMPTY_LOGO_NAME) elif (action == KEY_MODIFY_LOGO) and (not logo_name): return_error(MSG_EMPTY_LOGO_NAME) def main(): # pragma: no cover try: args = demisto.args() logo_image_id = args.get("logoImageId", "") logo_name = args.get("logoName", "") associated_domains = argToList(args.get("associatedDomains", "")) action = args.get("action") verify_args(action, logo_image_id, logo_name) model_data = load_data_from_xsoar() or load_data_from_docker() res = execute_action(model_data, action, logo_name, logo_image_id, associated_domains) if action != KEY_DISPLAY_LOGOS: save_model_data(model_data) return_results(res) except Exception as ex: demisto.error(traceback.format_exc()) # print the traceback return_error(f"Failed to execute URL Phishing script. Error: {str(ex)}") if __name__ in ["__main__", "__builtin__", "builtins"]: main()
README
Add, remove, or modify logos for the URL Phishing model to compare to suspicious websites.
Script Data
| Name | Description |
|---|---|
| Script Type | python3 |
| Tags | ml |
| Cortex XSOAR Version | 6.0.0 |
Inputs
| Argument Name | Description |
|---|---|
| logoImageId | “Entry ID” of the uploaded logo in Cortex XSOAR. |
| logoName | Name of the logo. The name is the unique identifier for the logo. |
| associatedDomains | Comma-separated list of domains that are associated to the logo defined in the logoName argument. It will only be used if the action argument is AddLogo or ModifiedDomainForLogo. |
| action | Action to execute on the model. |
Outputs
There are no outputs for this script.