FindDuplicateEmailIncidents

Can be used to find duplicate emails for incidents of type phishing, including malicious, spam, and legitimate emails.

python · Phishing

Source

import multiprocessing

# set omp
import os

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401

from CommonServerUserPython import *

os.environ["OMP_NUM_THREADS"] = str(multiprocessing.cpu_count())  # noqa
demisto.debug(f"Set OMP_NUM_THREADS to {os.environ['OMP_NUM_THREADS']}")

import re
from email.utils import parseaddr
from urllib.parse import urlparse

import dateutil  # type: ignore
import pandas as pd
import tldextract
from bs4 import BeautifulSoup
from FormatURLApiModule import *  # noqa: E402
from numpy import dot
from numpy.linalg import norm
from sklearn.feature_extraction.text import CountVectorizer

no_fetch_extract = tldextract.TLDExtract(suffix_list_urls=[], cache_dir=None)
pd.options.mode.chained_assignment = None  # default='warn'

SIMILARITY_THRESHOLD = float(demisto.args().get("threshold", 0.97))
CLOSE_TO_SIMILAR_DISTANCE = 0.2

EMAIL_BODY_FIELD = "emailbody"
EMAIL_SUBJECT_FIELD = "emailsubject"
EMAIL_HTML_FIELD = "emailbodyhtml"
FROM_FIELD = "emailfrom"
FROM_DOMAIN_FIELD = "fromdomain"
PREPROCESSED_EMAIL_BODY = "preprocessedemailbody"
PREPROCESSED_EMAIL_SUBJECT = "preprocessedemailsubject"
MERGED_TEXT_FIELD = "mereged_text"
MIN_TEXT_LENGTH = 50
DEFAULT_ARGS = {
    "limit": "1000",
    "incidentTypes": "Phishing",
    "existingIncidentsLookback": "100 days ago",
}
FROM_POLICY_TEXT_ONLY = "TextOnly"
FROM_POLICY_EXACT = "Exact"
FROM_POLICY_DOMAIN = "Domain"

FROM_POLICY = FROM_POLICY_TEXT_ONLY
URL_REGEX = (
    r"(?:(?:https?|ftp|hxxps?):\/\/|www\[?\.\]?|ftp\[?\.\]?)(?:[-\w\d]+\[?\.\]?)+[-\w\d]+(?::\d+)?"
    r"(?:(?:\/|\?)[-\w\d+&@#\/%=~_$?!\-:,.\(\);]*[\w\d+&@#\/%=~_$\(\);])?"
)

IGNORE_INCIDENT_TYPE_VALUE = "None"


def get_existing_incidents(input_args, current_incident_type):
    global DEFAULT_ARGS
    demisto.debug("Entering get_existing_incidents")
    get_incidents_args = {}
    get_incidents_args["limit"] = input_args.get("limit", DEFAULT_ARGS["limit"])
    if "existingIncidentsLookback" in input_args:
        get_incidents_args["fromDate"] = input_args["existingIncidentsLookback"]
    elif "existingIncidentsLookback" in DEFAULT_ARGS:
        get_incidents_args["fromDate"] = DEFAULT_ARGS["existingIncidentsLookback"]
    status_scope = input_args.get("statusScope", "All")
    query_components = []
    if input_args.get("query"):
        query_components.append(input_args["query"])
    if status_scope == "ClosedOnly":
        query_components.append("status:closed")
    elif status_scope == "NonClosedOnly":
        query_components.append("-status:closed")
    elif status_scope == "All":
        pass
    else:
        return_error(f"Unsupported statusScope: {status_scope}")
    type_values = input_args.get("incidentTypes", current_incident_type)
    if type_values != IGNORE_INCIDENT_TYPE_VALUE:
        type_field = input_args.get("incidentTypeFieldName", "type")
        type_query = generate_incident_type_query_component(type_field, type_values)
        query_components.append(type_query)
    if len(query_components) > 0:
        get_incidents_args["query"] = " and ".join(f"({c})" for c in query_components)

    fields = [
        EMAIL_BODY_FIELD,
        EMAIL_SUBJECT_FIELD,
        EMAIL_HTML_FIELD,
        FROM_FIELD,
        FROM_DOMAIN_FIELD,
        "created",
        "id",
        "name",
        "status",
        "emailto",
        "emailcc",
        "emailbcc",
        "removedfromcampaigns",
    ]

    if "populateFields" in input_args and input_args["populateFields"] is not None:
        get_incidents_args["populateFields"] = ",".join([",".join(fields), input_args["populateFields"]])
    else:
        get_incidents_args["populateFields"] = ",".join(fields)

    demisto.debug(f"Calling GetIncidentsByQuery with {get_incidents_args=}")
    incidents_query_res = demisto.executeCommand("GetIncidentsByQuery", get_incidents_args)
    if is_error(incidents_query_res):
        return_error(get_error(incidents_query_res))
    incidents_query_contents = "{}"

    for res in incidents_query_res:
        if res["Contents"]:
            incidents_query_contents = res["Contents"]
    incidents = json.loads(incidents_query_contents)
    demisto.info(f"Retrieved {len(incidents)} existing incidents for comparison.")
    return incidents


def generate_incident_type_query_component(type_field_arg, type_values_arg):
    demisto.debug(f"Generating incident type query for field '{type_field_arg}' with values '{type_values_arg}'")
    type_field = type_field_arg.strip()
    type_values = [x.strip() for x in type_values_arg.split(",")]
    types_unions = " ".join(f'"{t}"' for t in type_values)
    return f"{type_field}:({types_unions})"


def extract_domain(address):
    global no_fetch_extract
    if address == "":
        return ""
    demisto.debug(f"Going to extract domain from {address=}")
    email_address = parseaddr(address)[1]
    ext = no_fetch_extract(email_address)
    return f"{ext.domain}.{ext.suffix}"


def get_text_from_html(html):
    demisto.debug("Entering get_text_from_html")
    soup = BeautifulSoup(html, features="html.parser")
    # kill all script and style elements
    for script in soup(["script", "style"]):
        script.extract()  # rip it out
    # get text
    text = soup.get_text()
    # break into lines and remove leading and trailing space on each
    lines = (line.strip() for line in text.splitlines())
    # break multi-headlines into a line each
    chunks = (phrase.strip() for line in lines for phrase in line.split("  "))
    # drop blank lines
    text = "\n".join(chunk for chunk in chunks if chunk)
    demisto.debug("Successfully extracted text from HTML.")
    return text


def eliminate_urls_extensions(text):
    demisto.debug("Entering eliminate_urls_extensions")
    urls_list = re.findall(URL_REGEX, text)
    if len(urls_list) == 0:
        demisto.debug("No URLs found to eliminate.")
        return text
    demisto.debug(f"Found {len(urls_list)} URLs to neutralize.")
    formatted_urls_list = format_urls(urls_list)
    for url, formatted_url in zip(urls_list, formatted_urls_list):
        parsed_uri = urlparse(formatted_url)
        url_with_no_path = f"{parsed_uri.scheme}://{parsed_uri.netloc}/"
        text = text.replace(url, url_with_no_path)
    return text


def preprocess_email_body(incident):
    demisto.debug(f"Entering preprocess_email_body for incident {incident.get('id')}")
    email_body = email_html = ""
    if EMAIL_BODY_FIELD in incident:
        email_body = incident[EMAIL_BODY_FIELD]
    if EMAIL_HTML_FIELD in incident:
        email_html = incident[EMAIL_HTML_FIELD]
    if isinstance(email_html, float):
        email_html = ""
    if email_body is None or isinstance(email_body, float) or email_body.strip() == "":
        demisto.debug("Email body is empty or None, falling back to HTML extraction.")
        email_body = get_text_from_html(email_html)
    return eliminate_urls_extensions(email_body)


def preprocess_email_subject(incident):
    demisto.debug(f"Entering preprocess_email_subject for incident {incident.get('id')}")
    email_subject = ""
    if EMAIL_SUBJECT_FIELD in incident:
        email_subject = incident[EMAIL_SUBJECT_FIELD]
    if isinstance(email_subject, float):
        email_subject = ""
    return eliminate_urls_extensions(email_subject)


def concatenate_subject_body(row):
    return f"{row[PREPROCESSED_EMAIL_SUBJECT]}\n{row[PREPROCESSED_EMAIL_BODY]}"


def preprocess_incidents_df(existing_incidents):
    global MERGED_TEXT_FIELD, FROM_FIELD, FROM_DOMAIN_FIELD
    demisto.debug(f"Entering preprocess_incidents_df for {len(existing_incidents)} incidents.")
    incidents_df = pd.DataFrame(existing_incidents)
    if "CustomFields" in incidents_df.columns:
        demisto.debug("Found 'CustomFields', expanding them into DataFrame columns.")
        incidents_df["CustomFields"] = incidents_df["CustomFields"].fillna(value={})
        custom_fields_df = incidents_df["CustomFields"].apply(pd.Series)
        unique_keys = [k for k in custom_fields_df if k not in incidents_df]
        custom_fields_df = custom_fields_df[unique_keys]
        incidents_df = pd.concat([incidents_df.drop("CustomFields", axis=1), custom_fields_df], axis=1).reset_index()
    incidents_df[PREPROCESSED_EMAIL_SUBJECT] = incidents_df.apply(lambda x: preprocess_email_subject(x), axis=1)
    incidents_df[PREPROCESSED_EMAIL_BODY] = incidents_df.apply(lambda x: preprocess_email_body(x), axis=1)
    incidents_df[MERGED_TEXT_FIELD] = incidents_df.apply(concatenate_subject_body, axis=1)
    incidents_df = incidents_df[incidents_df[MERGED_TEXT_FIELD].str.len() >= MIN_TEXT_LENGTH]
    demisto.debug(f"Preprocessing complete. {len(incidents_df)} incidents remain after filtering by text length.")
    incidents_df = incidents_df.reset_index()
    if FROM_FIELD in incidents_df:
        incidents_df[FROM_FIELD] = incidents_df[FROM_FIELD].fillna(value="")
    else:
        incidents_df[FROM_FIELD] = ""
    incidents_df[FROM_FIELD] = incidents_df[FROM_FIELD].apply(lambda x: x.strip())
    incidents_df[FROM_DOMAIN_FIELD] = incidents_df[FROM_FIELD].apply(lambda address: extract_domain(address))
    incidents_df["created"] = incidents_df["created"].apply(lambda x: dateutil.parser.parse(x))  # type: ignore
    return incidents_df


def incident_has_text_fields(incident):
    demisto.debug(f"Checking for text fields in incident {incident.get('id')}")
    text_fields = [EMAIL_SUBJECT_FIELD, EMAIL_HTML_FIELD, EMAIL_BODY_FIELD]
    custom_fields = incident.get("CustomFields", []) or []
    if any(field in incident for field in text_fields):
        return True
    elif "CustomFields" in incident and any(field in custom_fields for field in text_fields):
        return True
    return False


def filter_out_same_incident(existing_incidents_df, new_incident):
    demisto.debug(f"Filtering out current incident {new_incident['id']} from DataFrame.")
    same_id_mask = existing_incidents_df["id"] == new_incident["id"]
    existing_incidents_df = existing_incidents_df[~same_id_mask]
    return existing_incidents_df


def filter_newer_incidents(existing_incidents_df, new_incident):
    demisto.debug("Filtering out incidents newer than the current incident.")
    new_incident_datetime = dateutil.parser.parse(new_incident["created"])  # type: ignore
    earlier_incidents_mask = existing_incidents_df["created"] < new_incident_datetime
    filtered_df = existing_incidents_df[earlier_incidents_mask]
    demisto.debug(f"{len(filtered_df)} incidents remain after filtering newer incidents.")
    return filtered_df


def vectorize(text, vectorizer):
    return vectorizer.transform([text]).toarray()[0]


def cosine_sim(a, b):
    return dot(a, b) / (norm(a) * norm(b))


def find_duplicate_incidents(new_incident, existing_incidents_df, max_incidents_to_return):
    global MERGED_TEXT_FIELD, FROM_POLICY
    demisto.debug(f"Entering find_duplicate_incidents. Comparing 1 new incident against {len(existing_incidents_df)} existing.")
    new_incident_text = new_incident[MERGED_TEXT_FIELD]
    text = [new_incident_text] + existing_incidents_df[MERGED_TEXT_FIELD].tolist()
    demisto.debug("Vectorizing text corpus...")
    vectorizer = CountVectorizer(token_pattern=r"(?u)\b\w\w+\b|!|\?|\"|\'").fit(text)
    new_incident_vector = vectorize(new_incident_text, vectorizer)
    existing_incidents_df["vector"] = existing_incidents_df[MERGED_TEXT_FIELD].apply(lambda x: vectorize(x, vectorizer))
    existing_incidents_df["similarity"] = existing_incidents_df["vector"].apply(lambda x: cosine_sim(x, new_incident_vector))
    if FROM_POLICY == FROM_POLICY_DOMAIN:
        demisto.debug(f"Applying FROM_POLICY_DOMAIN. Filtering by domain: {new_incident[FROM_DOMAIN_FIELD]}")
        mask = (existing_incidents_df[FROM_DOMAIN_FIELD] != "") & (
            existing_incidents_df[FROM_DOMAIN_FIELD] == new_incident[FROM_DOMAIN_FIELD]
        )
        existing_incidents_df = existing_incidents_df[mask]
    elif FROM_POLICY == FROM_POLICY_EXACT:
        demisto.debug(f"Applying FROM_POLICY_EXACT. Filtering by sender: {new_incident[FROM_FIELD]}")
        mask = (existing_incidents_df[FROM_FIELD] != "") & (existing_incidents_df[FROM_FIELD] == new_incident[FROM_FIELD])
        existing_incidents_df = existing_incidents_df[mask]
    else:
        demisto.debug("Applying FROM_POLICY_TEXT_ONLY. No sender/domain filtering.")
    existing_incidents_df["distance"] = existing_incidents_df["similarity"].apply(lambda x: 1 - x)
    tie_breaker_col = "id"
    try:
        existing_incidents_df["int_id"] = existing_incidents_df["id"].astype(int)
        tie_breaker_col = "int_id"
    except Exception:
        pass
    existing_incidents_df = existing_incidents_df.sort_values(by=["distance", "created", tie_breaker_col])
    demisto.debug(f"Found {len(existing_incidents_df)} potential duplicates after filtering and sorting.")
    return existing_incidents_df.head(max_incidents_to_return)


def return_entry(message, duplicate_incidents_df=None, new_incident=None):
    demisto.debug("Entering return_entry to format and return results.")
    if duplicate_incidents_df is None:
        demisto.debug("No duplicate incidents DataFrame provided.")
        duplicate_incident = {}
        all_duplicate_incidents = []
        full_incidents = []
    else:
        demisto.debug(f"Formatting {len(duplicate_incidents_df)} duplicate incidents for output.")
        most_similar_incident = duplicate_incidents_df.iloc[0]
        duplicate_incident = format_incident_context(most_similar_incident)
        all_duplicate_incidents = [format_incident_context(row) for _, row in duplicate_incidents_df.iterrows()]
        new_incident["created"] = new_incident["created"].astype(str)
        duplicate_incidents_df["created"] = duplicate_incidents_df["created"].astype(str)
        duplicate_incidents_df = duplicate_incidents_df.drop("vector", axis=1)
        full_incidents = new_incident.to_dict(orient="records") + duplicate_incidents_df.to_dict(orient="records")
    outputs = {
        "duplicateIncident": duplicate_incident,
        "isDuplicateIncidentFound": duplicate_incidents_df is not None,
        "allDuplicateIncidents": all_duplicate_incidents,
    }
    demisto.info(f"Setting isDuplicateIncidentFound: {duplicate_incidents_df is not None}")
    return_outputs(message, outputs, raw_response=json.dumps(full_incidents))


def format_incident_context(df_row):
    duplicate_incident = {
        "rawId": df_row["id"],
        "id": df_row["id"],
        "name": df_row.get("name"),
        "similarity": df_row.get("similarity"),
    }
    return duplicate_incident


def close_new_incident_and_link_to_existing(new_incident, duplicate_incidents_df):
    demisto.info(f"Found {len(duplicate_incidents_df)} duplicates above threshold. Closing and linking.")
    mask = duplicate_incidents_df["similarity"] >= SIMILARITY_THRESHOLD
    duplicate_incidents_df = duplicate_incidents_df[mask]
    most_similar_incident = duplicate_incidents_df.iloc[0]
    max_similarity = duplicate_incidents_df.iloc[0]["similarity"]
    min_similarity = duplicate_incidents_df.iloc[-1]["similarity"]
    formatted_incident, headers = format_incident_hr(duplicate_incidents_df)
    incident = "incidents" if len(duplicate_incidents_df) > 1 else "incident"

    if max_similarity > min_similarity:
        title = f"Duplicate {incident} found with similarity {min_similarity * 100:.1f}%-{max_similarity * 100:.1f}%"
    else:
        title = f"Duplicate {incident} found with similarity {max_similarity * 100:.1f}%"
    message = tableToMarkdown(title, formatted_incident, headers)
    if demisto.args().get("closeAsDuplicate", "true") == "true":
        demisto.debug("closeAsDuplicate is 'true'. Executing CloseInvestigationAsDuplicate.")
        res = demisto.executeCommand("CloseInvestigationAsDuplicate", {"duplicateId": most_similar_incident["id"]})
        if is_error(res):
            return_error(res)
        message += "This incident (#{}) will be closed and linked to #{}.".format(
            new_incident.iloc[0]["id"], most_similar_incident["id"]
        )
    else:
        demisto.debug("closeAsDuplicate is 'false'. Will not close incident, just linking.")
    return_entry(message, duplicate_incidents_df, new_incident)


def create_new_incident():
    demisto.info("No duplicates found. This is a new incident.")
    return_entry("This incident is not a duplicate of an existing incident.")


def format_incident_hr(duplicate_incidents_df):
    demisto.debug(f"Entering format_incident_hr for {len(duplicate_incidents_df)} incidents.")
    incidents_list = duplicate_incidents_df.to_dict("records")
    json_lists = []
    status_map = {"0": "Pending", "1": "Active", "2": "Closed", "3": "Archive"}
    for incident in incidents_list:
        json_lists.append(
            {
                "Id": "[{}](#/Details/{})".format(incident["id"], incident["id"]),
                "Name": incident["name"],
                "Status": status_map[str(incident.get("status"))],
                "Time": str(incident["created"]),
                "Email From": incident.get(demisto.args().get(FROM_FIELD)),
                "Text Similarity": "{:.1f}%".format(incident["similarity"] * 100),
            }
        )
    headers = ["Id", "Name", "Status", "Time", "Email From", "Text Similarity"]
    return json_lists, headers


def create_new_incident_low_similarity(duplicate_incidents_df):
    demisto.info("No duplicates found above similarity threshold.")
    message = "## This incident is not a duplicate of an existing incident.\n"
    similarity = duplicate_incidents_df.iloc[0]["similarity"]
    if similarity > SIMILARITY_THRESHOLD - CLOSE_TO_SIMILAR_DISTANCE:
        demisto.debug(f"Found {len(duplicate_incidents_df)} incidents with similarity close to threshold. Will report them.")
        mask = duplicate_incidents_df["similarity"] >= SIMILARITY_THRESHOLD - CLOSE_TO_SIMILAR_DISTANCE
        duplicate_incidents_df = duplicate_incidents_df[mask]
        formatted_incident, headers = format_incident_hr(duplicate_incidents_df)
        message += tableToMarkdown("Most similar incidents found", formatted_incident, headers=headers)
        message += (
            f"The threshold for considering 2 incidents as duplicate is a similarity of {SIMILARITY_THRESHOLD * 100:.1f}%.\n"
        )
        message += (
            "Therefore these 2 incidents will not be considered as duplicate and the current incident will remain active.\n"
        )
    else:
        demisto.debug("No incidents were close to the similarity threshold.")
    return_entry(message)


def create_new_incident_no_text_fields():
    demisto.info("Incident has no text fields to compare. Treating as new.")
    text_fields = [EMAIL_BODY_FIELD, EMAIL_HTML_FIELD, EMAIL_SUBJECT_FIELD]
    message = "No text fields were found within this incident: {}.\n".format(",".join(text_fields))
    message += "Incident will remain active."
    return_entry(message)


def create_new_incident_too_short():
    demisto.info(f"Incident text is too short (less than {MIN_TEXT_LENGTH} chars) after preprocessing. Treating as new.")
    return_entry("Incident text after preprocessing is too short for deduplication. Incident will remain active.")


def main():
    global EMAIL_BODY_FIELD, EMAIL_SUBJECT_FIELD, EMAIL_HTML_FIELD, FROM_FIELD, MIN_TEXT_LENGTH, FROM_POLICY
    demisto.debug("Starting FindDuplicateEmailIncidents script.")
    input_args = demisto.args()
    demisto.debug(f"Script arguments: {input_args}")
    EMAIL_BODY_FIELD = input_args.get("emailBody", EMAIL_BODY_FIELD)
    EMAIL_SUBJECT_FIELD = input_args.get("emailSubject", EMAIL_SUBJECT_FIELD)
    EMAIL_HTML_FIELD = input_args.get("emailBodyHTML", EMAIL_HTML_FIELD)
    FROM_FIELD = input_args.get("emailFrom", FROM_FIELD)
    FROM_POLICY = input_args.get("fromPolicy", FROM_POLICY)
    max_incidents_to_return = input_args.get("maxIncidentsToReturn", "20")

    try:
        max_incidents_to_return = int(max_incidents_to_return)
    except Exception:
        return_error(f'Illegal value of arguement "maxIncidentsToReturn": {max_incidents_to_return}. Value should be an integer')
    new_incident = demisto.incidents()[0]
    demisto.debug(f"Current incident ID: {new_incident.get('id')}")
    type_field = input_args.get("incidentTypeFieldName", "type")
    existing_incidents = get_existing_incidents(input_args, new_incident.get(type_field, IGNORE_INCIDENT_TYPE_VALUE))
    demisto.debug(f"found {len(existing_incidents)} incidents by query")
    if len(existing_incidents) == 0:
        demisto.debug("No existing incidents found matching query. Calling create_new_incident.")
        create_new_incident()
        return None
    if not incident_has_text_fields(new_incident):
        demisto.debug("Current incident has no text fields. Calling create_new_incident_no_text_fields.")
        create_new_incident_no_text_fields()
        return None
    demisto.debug("Preprocessing new incident.")
    new_incident_df = preprocess_incidents_df([new_incident])
    if len(new_incident_df) == 0:  # len(new_incident_df)==0 means new incident is too short
        demisto.debug("New incident is too short after preprocessing. Calling create_new_incident_too_short.")
        create_new_incident_too_short()
        return None
    demisto.debug("Preprocessing existing incidents.")
    existing_incidents_df = preprocess_incidents_df(existing_incidents)
    demisto.debug("Filtering same incident ID.")
    existing_incidents_df = filter_out_same_incident(existing_incidents_df, new_incident)
    demisto.debug("Filtering newer incidents.")
    existing_incidents_df = filter_newer_incidents(existing_incidents_df, new_incident)
    if len(existing_incidents_df) == 0:
        demisto.debug("No existing incidents left after filtering. Calling create_new_incident.")
        create_new_incident()
        return None
    new_incident_preprocessed = new_incident_df.iloc[0].to_dict()
    demisto.debug("Finding duplicate incidents...")
    duplicate_incidents_df = find_duplicate_incidents(new_incident_preprocessed, existing_incidents_df, max_incidents_to_return)
    if len(duplicate_incidents_df) == 0:
        demisto.debug("No duplicates found by find_duplicate_incidents. Calling create_new_incident.")
        create_new_incident()
        return None
    if duplicate_incidents_df.iloc[0]["similarity"] < SIMILARITY_THRESHOLD:
        demisto.debug("Highest similarity is below threshold. Calling create_new_incident_low_similarity.")
        create_new_incident_low_similarity(duplicate_incidents_df)
        return None
    else:
        demisto.debug("Duplicates found above threshold. Calling close_new_incident_and_link_to_existing.")
        return close_new_incident_and_link_to_existing(new_incident_df, duplicate_incidents_df)


if __name__ in ["__main__", "__builtin__", "builtins"]:
    main()

README

Can be used to find duplicate emails for incidents of type phishing, including malicious, spam, and legitimate emails.

Script Data


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

Inputs


Argument Name Description
incidentTypeFieldName The name of the incident field where its type is stored. Default is “type”. Change this argument only in case you use a custom field for specifying incident type.
incidentTypes A comma-separated list of incident types by which to filter. The default is the current incident type. Specify “None” to ignore incident type from deduplication logic.
existingIncidentsLookback The start date by which to search for duplicated existing incidents. Date format is the same as in the incidents query page. For example, “3 days ago”, “2019-01-01T00:00:00 +0200”).
query Additional text by which to query incidents.
limit The maximum number of incidents to fetch.
emailSubject Subject of the email.
emailBody Body of the email.
emailBodyHTML HTML body of the email.
emailFrom Incident fields contains the email from value.
fromPolicy Whether to take into account the email from field for deduplication. “TextOnly” - incidents will be considered as duplicated based on test similarity only, ignoring the sender’s address. “Exact” - incidents will be considered as duplicated if their text is similar and their sender is the same. “Domain” - incidents will be considered as duplicated if their text is similar and their senders’ address has the same domain. Default is “Domain”.
statusScope Whether to compare the new incident to past closed or non closed incidents only.
closeAsDuplicate Whether to close the current incident if a duplicate incident is found. Only supported in Cortex XSOAR.
threshold Threshold to consider incident as duplication, number between 0-1
maxIncidentsToReturn Maximum number of duplicate incidents IDs to return.
populateFields A comma-separated list of incident fields to populate.
exsitingIncidentsLookback Deprecated. Use the *existingIncidentsLookback* argument instead.

Outputs


Path Description Type
duplicateIncident The oldest duplicate incident found with the highest similarity to the current incident. unknown
duplicateIncident.id Duplicate incident ID. string
duplicateIncident.rawId Duplicate incident ID. Unknown
duplicateIncident.name Duplicate incident name. Unknown
duplicateIncident.similarity Number in range 0-1 which describe the similarity between the existing incident and the new incident. Unknown
isDuplicateIncidentFound Whether a duplicate incident was found (“true” or “false”). boolean
allDuplicateIncidents All duplicate incidents found where their similarity with the new incident exceeds the threshold. Unknown
allDuplicateIncidents.id A list of all duplicate incidents IDs found. Unknown
allDuplicateIncidents.rawId A list of all duplicate incidents IDs found. Unknown
allDuplicateIncidents.name A list of all duplicate incidents names found. Unknown
allDuplicateIncidents.similarity A list of the similarity between duplicate incidents and new the incident of all duplicate incidents names found. Unknown