DBotPredictPhishingWords

Predict text label using a pre-trained machine learning phishing model, and get the most important words used in the classification decision.

python · Base

Details

IDDBotPredictPhishingWords
Languagepython
From Version5.0.0
Docker Imagedemisto/ml:1.0.0.9793547
Tagsml phishing

README

Predict text label using a pre-trained machine learning phishing model, and get the most important words used in the classification decision.

Script Data


Name Description
Script Type python3
Tags ml, phishing
Cortex XSOAR Version 5.0.0

Used In


This script is used in the following playbooks and scripts.

  • Phishing Investigation - Generic v2

Inputs


Argument Name Description
modelName The model name (or list name) in Demisto.
hashSeed Seed for the hash function, at the pre-process stage.
emailSubject Subject of the email.
emailBody Body of the email.
emailBodyHTML HTML body of the email. Only use this field if the emailBody argument is empty.
topWordsLimit Maximum number of positive/negative words to return for the model decision. Default is 20.
wordThreshold Threshold to determine word importance (range 0-1). Default is 0.05.
modelStoreType How the model is stored in Demisto. Can be “list” or “mlModel”. Default is “list”.
minTextLength Minimum number of characters for the prediction.
labelProbabilityThreshold The label probability threshold. Default is 0.8.
confidenceThreshold The confidence threshold. The model will provide predictions only if their confidence is above this threshold.
returnError Whether to return an error when there is no prediction. Default is “true”.
setIncidentFields Whether to set Demisto out-of-the-box DBot fields.
language The language of the input text. Default is “Any”. Can be “Any”, “English”, “German”, “French”, “Spanish”, “Portuguese”, “Italian”, “Dutch”, or “Other”. If “Any” or “Other” is selected, the script preprocess the entire input, no matter what its acutual language is. If a specific language is selected, the script filters out any other language from the output text.
tokenizationMethod Tokenization method for text. Only required when the language argument is set to “Other”. Can be “tokenizer”, “byWords”, or “byLetters”.

Outputs


Path Description Type
DBotPredictPhishingWords.Label The predicted label. String
DBotPredictPhishingWords.Probability The predicted probability (range 0-1). Number
DBotPredictPhishingWords.PositiveWords A list of words in the input text that supports the model decision. Unknown
DBotPredictPhishingWords.NegativeWords A list of words in the input text that do not support the model decision. These words better support a different classification class. Unknown
DBotPredictPhishingWords.TextTokensHighlighted The input text (after pre-processing) with the positive words that support the model decision. String
from collections import defaultdict

import pytest
from CommonServerPython import *
from DBotPredictPhishingWords import get_model_data, main, predict_phishing_words

TOKENIZATION_RESULT = None


class PhishingModelMock:
    def __init__(self, filter_words_res=None, explain_model_words_res=None):
        self.filter_words_res = filter_words_res
        self.explain_model_words_res = explain_model_words_res

    def filter_model_words(self):
        return self.filter_words_res

    def explain_model_words(self, a, b, c, d):
        return self.explain_model_words_res


def get_args():
    args = defaultdict(lambda: "yes")
    args["encoding"] = "utf8"
    args["encoding"] = "utf8"
    args["removeNonEnglishWords"] = "no"
    return args


def bold(word):
    return f"**{word}**"


def executeCommand(command, args=None):
    global TOKENIZATION_RESULT
    if command == "getList":
        return [{"Contents": "ModelDataList", "Type": "note"}]
    elif command == "getMLModel":
        return [{"Contents": {"modelData": "ModelDataML", "model": {"type": {"type": ""}}}, "Type": "note"}]
    elif command == "DBotPreProcessTextData":
        TOKENIZATION_RESULT["originalText"] = args["input"]
        TOKENIZATION_RESULT["tokenizedText"] = args["input"]
        return [{"Contents": TOKENIZATION_RESULT, "Type": "note"}]
    elif command == "HighlightWords":
        text = args["text"]
        terms = set(args["terms"].split(","))
        words = text.split()
        for i, w in enumerate(words):
            if w in terms:
                words[i] = bold(w)
        return [{"Contents": " ".join(words), "Type": "note"}]
    return None


def test_get_model_data(mocker):
    mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand)
    assert get_model_data("test", "list", True)[0] == "ModelDataList"
    assert get_model_data("test", "mlModel", True)[0] == "ModelDataML"


def test_predict_phishing_words(mocker):
    global TOKENIZATION_RESULT
    d = {"Label": "Valid", "Probability": 0.7, "PositiveWords": ["word1"], "NegativeWords": ["word2"]}
    phishing_mock = PhishingModelMock()
    mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand)
    mocker.patch.object(demisto, "args", return_value={"topWordsLimit": 10})
    mocker.patch("demisto_ml.phishing_model_loads_handler", return_value=phishing_mock, create=True)
    mocker.patch.object(demisto, "incidents", return_value=[{"isPlayground": True}])
    mocker.patch.object(phishing_mock, "filter_model_words", return_value=("text", 2), create=True)
    mocker.patch.object(phishing_mock, "explain_model_words", return_value=d, create=True)

    TOKENIZATION_RESULT = {
        "originalText": "word1 word2 word3",
        "tokenizedText": "word1 word2 word3",
        "originalWordsToTokens": {"word1": ["word1"], "word2": ["word2"], "word3": ["word3"]},
    }

    email_subject = "word1"
    email_body = "word2 word3"
    res = predict_phishing_words("modelName", "list", email_subject, email_body, 0, 0, 0, 10, True)
    correct_res = {
        "OriginalText": concatenate_subject_body(email_subject, email_body),
        "Probability": 0.7,
        "NegativeWords": ["word2"],
        "TextTokensHighlighted": concatenate_subject_body(f"**{email_subject}**", email_body),
        "PositiveWords": ["word1"],
        "Label": "Valid",
    }
    assert res["Contents"] == correct_res


def concatenate_subject_body(email_subject, email_body):
    return f"{email_subject} \n{email_body}"


def test_predict_phishing_words_low_threshold(mocker):
    global TOKENIZATION_RESULT
    d = {"Label": "Valid", "Probability": 0.7, "PositiveWords": ["word1"], "NegativeWords": ["word2"]}
    phishing_mock = PhishingModelMock()
    mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand)
    mocker.patch.object(demisto, "args", return_value={"topWordsLimit": 10})
    mocker.patch("demisto_ml.phishing_model_loads_handler", return_value=phishing_mock, create=True)
    mocker.patch.object(phishing_mock, "filter_model_words", return_value=("text", 2), create=True)
    mocker.patch.object(phishing_mock, "explain_model_words", return_value=d, create=True)
    TOKENIZATION_RESULT = {
        "originalText": "word1 word2 word3",
        "tokenizedText": "word1 word2 word3",
        "hashedTokenizedText": "23423 432432 12321",
        "originalWordsToTokens": {"word1": ["word1"], "word2": ["word2"], "word3": ["word3"]},
        "wordsToHashedTokens": {"word1": ["23423"], "word2": ["432432"], "word3": ["12321"]},
    }
    with pytest.raises(SystemExit):
        predict_phishing_words("modelName", "list", "subject", "body", 0, 0.8, 0, 10, True)


def test_predict_phishing_words_no_words(mocker):
    global TOKENIZATION_RESULT
    phishing_mock = PhishingModelMock()
    mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand)
    mocker.patch.object(demisto, "args", return_value={"topWordsLimit": 10})
    mocker.patch("demisto_ml.phishing_model_loads_handler", return_value=phishing_mock, create=True)
    mocker.patch.object(phishing_mock, "filter_model_words", return_value=("", 0), create=True)
    TOKENIZATION_RESULT = {
        "originalText": "word1 word2 word3",
        "tokenizedText": "word1 word2 word3",
        "hashedTokenizedText": "23423 432432 12321",
        "originalWordsToTokens": {"word1": ["word1"], "word2": ["word2"], "word3": ["word3"]},
        "wordsToHashedTokens": {"word1": ["23423"], "word2": ["432432"], "word3": ["12321"]},
    }
    with pytest.raises(SystemExit):
        predict_phishing_words("modelName", "list", "subject", "body", 0, 0, 0, 10, True)
    mocker.patch.object(phishing_mock, "filter_model_words", return_value=("", 10), create=True)
    with pytest.raises(SystemExit):
        predict_phishing_words("modelName", "list", "subject", "body", 20, 0, 0, 10, True)


def test_predict_phishing_words_hashed(mocker):
    global TOKENIZATION_RESULT
    d = {"Label": "Valid", "Probability": 0.7, "PositiveWords": ["23423"], "NegativeWords": ["432432"]}
    phishing_mock = PhishingModelMock()
    mocker.patch.object(demisto, "incidents", return_value=[{"isPlayground": True}])

    mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand)
    mocker.patch.object(demisto, "args", return_value={"topWordsLimit": 10, "hashSeed": 10})
    mocker.patch("demisto_ml.phishing_model_loads_handler", return_value=phishing_mock, create=True)
    mocker.patch.object(phishing_mock, "filter_model_words", return_value=("text", 2), create=True)
    mocker.patch.object(phishing_mock, "explain_model_words", return_value=d, create=True)
    TOKENIZATION_RESULT = {
        "originalText": "word1 word2 word3",
        "tokenizedText": "word1 word2 word3",
        "hashedTokenizedText": "23423 432432 12321",
        "originalWordsToTokens": {"word1": ["word1"], "word2": ["word2"], "word3": ["word3"]},
        "wordsToHashedTokens": {"word1": ["23423"], "word2": ["432432"], "word3": ["12321"]},
    }
    email_subject = "word1"
    email_body = "word2 word3"
    res = predict_phishing_words("modelName", "list", email_subject, email_body, 0, 0, 0, 10, True)
    assert res["Contents"] == {
        "OriginalText": concatenate_subject_body(email_subject, email_body),
        "Probability": 0.7,
        "NegativeWords": ["word2"],
        "TextTokensHighlighted": concatenate_subject_body(f"**{email_subject}**", email_body),
        "PositiveWords": ["word1"],
        "Label": "Valid",
    }


def test_predict_phishing_words_tokenization_by_character(mocker):
    global TOKENIZATION_RESULT
    phishing_mock = PhishingModelMock()
    mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand)
    mocker.patch.object(demisto, "args", return_value={"topWordsLimit": 10, "hashSeed": 10})
    mocker.patch("demisto_ml.phishing_model_loads_handler", return_value=phishing_mock, create=True)
    mocker.patch.object(phishing_mock, "filter_model_words", return_value=("text", 2), create=True)
    mocker.patch.object(demisto, "incidents", return_value=[{"isPlayground": True}])
    original_text = "this is a test"
    tokenized_text = " ".join(c for c in original_text if c != " ")
    original_words_to_tokes = {w: list(w) for w in original_text.split()}
    TOKENIZATION_RESULT = {
        "originalText": original_text,
        "tokenizedText": tokenized_text,
        "originalWordsToTokens": original_words_to_tokes,
    }
    positive_tokens = ["t", "i"]
    negative_tokens = []
    d = {"Label": "Valid", "Probability": 0.7, "PositiveWords": positive_tokens, "NegativeWords": negative_tokens}
    mocker.patch.object(phishing_mock, "explain_model_words", return_value=d, create=True)
    res = predict_phishing_words("modelName", "list", original_text, "", 0, 0, 0, 10, True)
    correct_highlighted = " ".join(
        bold(w) if any(pos_token in w for pos_token in positive_tokens) else w for w in original_text.split()
    )
    assert res["Contents"] == {
        "OriginalText": original_text,
        "Probability": 0.7,
        "NegativeWords": negative_tokens,
        "TextTokensHighlighted": correct_highlighted,
        "PositiveWords": [w for w in original_text.split() if any(pos_token in w for pos_token in positive_tokens)],
        "Label": "Valid",
    }


def test_predict_phishing_words_tokenization_by_character_hashed(mocker):
    global TOKENIZATION_RESULT

    def hash_token(t):
        return str(ord(t))

    def unhash_token(t):
        return chr(int(t))

    phishing_mock = PhishingModelMock()
    mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand)
    mocker.patch.object(demisto, "args", return_value={"topWordsLimit": 10, "hashSeed": 10})
    mocker.patch("demisto_ml.phishing_model_loads_handler", return_value=phishing_mock, create=True)
    mocker.patch.object(phishing_mock, "filter_model_words", return_value=("text", 2), create=True)
    mocker.patch.object(demisto, "incidents", return_value=[{"isPlayground": True}])
    original_text = "this is a test"
    tokenized_text = " ".join(c for c in original_text if c != " ")
    original_words_to_tokes = {w: list(w) for w in original_text.split()}
    TOKENIZATION_RESULT = {
        "originalText": original_text,
        "tokenizedText": tokenized_text,
        "originalWordsToTokens": original_words_to_tokes,
        "hashedTokenizedText": "".join(hash_token(t) if t != " " else t for t in tokenized_text),
        "wordsToHashedTokens": {w: [hash_token(t) for t in token_list] for w, token_list in original_words_to_tokes.items()},
    }
    positive_tokens = [hash_token("t"), hash_token("i")]
    negative_tokens = []
    d = {"Label": "Valid", "Probability": 0.7, "PositiveWords": positive_tokens, "NegativeWords": negative_tokens}
    mocker.patch.object(phishing_mock, "explain_model_words", return_value=d, create=True)
    res = predict_phishing_words("modelName", "list", original_text, "", 0, 0, 0, 10, True)
    correct_highlighted = " ".join(
        bold(w) if any(unhash_token(pos_token) in w for pos_token in positive_tokens) else w for w in original_text.split()
    )
    assert res["Contents"] == {
        "OriginalText": original_text,
        "Probability": 0.7,
        "NegativeWords": negative_tokens,
        "TextTokensHighlighted": correct_highlighted,
        "PositiveWords": [w for w in original_text.split() if any(unhash_token(pos_token) in w for pos_token in positive_tokens)],
        "Label": "Valid",
    }


def test_main(mocker):
    global TOKENIZATION_RESULT
    phishing_mock = PhishingModelMock()
    d = {"Label": "Valid", "Probability": 0.7, "PositiveWords": ["word1"], "NegativeWords": ["word2"]}
    args = {
        "modelName": "modelName",
        "modelStoreType": "list",
        "emailSubject": "word1",
        "emailBody": "word2 word3",
        "minTextLength": "0",
        "labelProbabilityThreshold": "0",
        "wordThreshold": "0",
        "topWordsLimit": "10",
        "returnError": "true",
    }
    mocker.patch.object(demisto, "args", return_value=args)
    mocker.patch.object(demisto, "incidents", return_value=[{"isPlayground": True}])
    mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand)
    mocker.patch("demisto_ml.phishing_model_loads_handler", return_value=phishing_mock, create=True)
    mocker.patch.object(phishing_mock, "filter_model_words", return_value=("text", 2), create=True)
    mocker.patch.object(phishing_mock, "explain_model_words", return_value=d, create=True)

    TOKENIZATION_RESULT = {
        "originalText": "{} {}".format(args["emailSubject"], args["emailBody"]),
        "tokenizedText": "{} {}".format(args["emailSubject"], args["emailBody"]),
        "originalWordsToTokens": {"word1": ["word1"], "word2": ["word2"], "word3": ["word3"]},
    }

    res = main()
    correct_res = {
        "OriginalText": concatenate_subject_body(args["emailSubject"], args["emailBody"]),
        "Probability": 0.7,
        "NegativeWords": ["word2"],
        "TextTokensHighlighted": concatenate_subject_body(bold(args["emailSubject"]), args["emailBody"]),
        "PositiveWords": ["word1"],
        "Label": "Valid",
    }
    assert res["Contents"] == correct_res

    args["emailBodyHTML"] = args.pop("emailBody")
    TOKENIZATION_RESULT = {
        "originalText": concatenate_subject_body(args["emailSubject"], args["emailBodyHTML"]),
        "tokenizedText": concatenate_subject_body(args["emailSubject"], args["emailBodyHTML"]),
        "originalWordsToTokens": {"word1": ["word1"], "word2": ["word2"], "word3": ["word3"]},
    }
    main()
    assert res["Contents"] == correct_res


def test_no_positive_words(mocker):
    # make sure that if no positive words were found, TextTokensHighlighted output is equivalent to original text
    global TOKENIZATION_RESULT
    phishing_mock = PhishingModelMock()
    d = {"Label": "Valid", "Probability": 0.7, "PositiveWords": [], "NegativeWords": ["word2"]}
    args = {
        "modelName": "modelName",
        "modelStoreType": "list",
        "emailSubject": "word1",
        "emailBody": "word2 word3",
        "minTextLength": "0",
        "labelProbabilityThreshold": "0",
        "wordThreshold": "0",
        "topWordsLimit": "10",
        "returnError": "true",
    }
    mocker.patch.object(demisto, "args", return_value=args)
    mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand)
    phishing_mock = PhishingModelMock(("text", 2))
    mocker.patch("demisto_ml.phishing_model_loads_handler", return_value=phishing_mock, create=True)
    mocker.patch.object(demisto, "incidents", return_value=[{"isPlayground": True}])
    mocker.patch.object(phishing_mock, "filter_model_words", return_value=("text", 2), create=True)
    mocker.patch.object(phishing_mock, "explain_model_words", return_value=d, create=True)

    TOKENIZATION_RESULT = {
        "originalText": "{} {}".format(args["emailSubject"], args["emailBody"]),
        "tokenizedText": "{} {}".format(args["emailSubject"], args["emailBody"]),
        "originalWordsToTokens": {"word1": ["word1"], "word2": ["word2"], "word3": ["word3"]},
    }

    res = main()
    assert res["Contents"]["TextTokensHighlighted"] == TOKENIZATION_RESULT["originalText"]