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.