DBotFindSimilarIncidentsByIndicators
Finds similar incidents based on indicators' similarity. Indicators' contribution to the final score is based on their scarcity.
python · Base
Details
| ID | DBotFindSimilarIncidentsByIndicators |
|---|---|
| Language | python |
| From Version | 5.0.0 |
| Docker Image | demisto/sklearn:1.0.0.12545527 |
README
Finds similar incidents based on indicators’ similarity. Indicators’ contribution to the final score is based on their scarcity.
Script Data
| Name | Description |
|---|---|
| Script Type | python3 |
| Cortex XSOAR Version | 5.0.0 |
Used In
This script is used in the following playbooks and scripts.
- Dedup - Generic v4
Inputs
| Argument Name | Description |
|---|---|
| incidentId | Incident ID to get the prediction of. If empty, predicts the current incident ID. |
| maxIncidentsInIndicatorsForWhiteList | The maximum number of incidents that an indicator can be associated with to be retained. This helps to filter out indicators that appear in many incidents |
| minNumberOfIndicators | The minimum number of indicators related to the incident required before running the model. |
| threshold | Threshold to similarity value which is between 0 and 1. |
| indicatorsTypes | Type of indicators to take into account. If empty, uses all indicators types. |
| showActualIncident | Whether to show the incident you are investigating. |
| maxIncidentsToDisplay | The maximum number of incidents to display. |
| fieldsIncidentToDisplay | Fields to add in the table of incident |
| fromDate | The start date by which we retrieve information on incidents. |
| query | Argument for the query of similar incidents. |
Outputs
There are no outputs for this script.
import math import re from collections import Counter import demistomock as demisto import numpy as np import pandas as pd from CommonServerPython import * from GetIncidentsApiModule import * # noqa: E402 from sklearn.base import BaseEstimator, TransformerMixin from CommonServerUserPython import * SEARCH_INDICATORS_LIMIT = 10000 SEARCH_INDICATORS_PAGE_SIZE = 500 ROUND_SCORING = 2 PLAYGROUND_PATTERN = "[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}" # Mutual indicators fields/columns INDICATOR_ID_FIELD = "id" VALUE_FIELD = "value" VALUE_FIELD_FOR_COMPATIBILITY = "name" # value field can't be used in demisto.searchIndicators on v6.9.0 SCORE_FIELD = "score" INVESTIGATION_IDS_FIELD = "investigationIDs" INDICATOR_TYPE_FIELD = "indicator_type" INDICATOR_LINK_COLUMN = "indicatorID" TYPE_COLUMN = "type" REPUTATION_COLUMN = "Reputation" INVOLVED_INCIDENTS_COUNT_COLUMN = "involvedIncidentsCount" INDICATOR_FIELDS_TO_POPULATE_FROM_QUERY = [ INDICATOR_ID_FIELD, INDICATOR_TYPE_FIELD, INVESTIGATION_IDS_FIELD, SCORE_FIELD, VALUE_FIELD_FOR_COMPATIBILITY, ] MUTUAL_INDICATORS_HEADERS = [INDICATOR_LINK_COLUMN, VALUE_FIELD, TYPE_COLUMN, REPUTATION_COLUMN, INVOLVED_INCIDENTS_COUNT_COLUMN] # Similar incidents fields/columns INCIDENT_ID_FIELD = "id" CREATED_FIELD = "created" NAME_FIELD = "name" STATUS_FIELD = "status" INCIDENT_LINK_COLUMN = "incident ID" INDICATORS_COLUMN = "indicators" SIMILARITY_SCORE_COLUMN = "similarity indicators" IDENTICAL_INDICATORS_COLUMN = "Identical indicators" FIRST_COLUMNS_INCIDENTS_DISPLAY = [INCIDENT_LINK_COLUMN, CREATED_FIELD, NAME_FIELD] FIELDS_TO_EXCLUDE_FROM_DISPLAY = [INCIDENT_ID_FIELD] STATUS_DICT = { 0: "Pending", 1: "Active", 2: "Closed", 3: "Archive", } INDICATOR_LINK_FORMAT = "[{0}](#/indicator/{0})" INCIDENT_LINK_FORMAT = "[{0}](#/Details/{0})" DATE_FORMAT = "%Y-%m-%d" def flatten_list(my_list: list[list]) -> list: """ Flatten a list of list :param l: list of list :return: list """ return [item for sublist in my_list for item in sublist] class FrequencyIndicators(BaseEstimator, TransformerMixin): """ FrequencyIndicators class for indicator frequencies computation """ def __init__(self, incident_field: str, actual_incident: pd.DataFrame) -> None: self.column_name = incident_field self.frequency: dict = {} self.vocabulary = actual_incident[self.column_name].iloc[0].split(" ") def fit(self, x: pd.DataFrame) -> "FrequencyIndicators": x = x[self.column_name] size = len(x) + 1 frequencies = Counter(flatten_list([t.split(" ") for t in x.values])) frequencies.update(Counter(self.vocabulary)) self.frequency = {k: math.log(1 + size / v) for k, v in frequencies.items()} return self def transform(self, x: pd.DataFrame) -> pd.DataFrame: return x[self.column_name].apply(self.compute_term_score) def compute_term_score(self, indicators_values_string: str) -> float: x = indicators_values_string.split(" ") return sum([1 * self.frequency[word] for word in self.vocabulary if word in x]) / sum( [self.frequency[word] for word in self.vocabulary] ) class FrequencyIndicatorsTransformer: def __init__(self, incidents_df: pd.DataFrame, actual_incident: pd.DataFrame): """ :param incidents_df: DataFrame of related incidents :param actual_incident: DataFrame of the actual incident """ self.incidents_df = incidents_df self.actual_incident = actual_incident self.transformed_column = INDICATORS_COLUMN self.scoring_function = lambda x: x def fit_transform(self): transformer = FrequencyIndicators( self.transformed_column, self.actual_incident, ) x_vector = transformer.fit_transform(self.incidents_df) incident_vect = transformer.transform(self.actual_incident) return x_vector, incident_vect def get_score(self): x_vector, _ = self.fit_transform() distance = self.scoring_function(x_vector) self.incidents_df[SIMILARITY_SCORE_COLUMN] = np.round(distance, ROUND_SCORING) return self.incidents_df class Model: def __init__( self, incident_to_match: pd.DataFrame, incidents_df: pd.DataFrame, similarity_threshold: float, max_incidents: int, ) -> None: """ :param incident_to_match: Dataframe with one incident :param incidents_df: Dataframe with all the incidents :param similarity_threshold: The similarity threshold :param max_incidents: Maximum number of incidents to return """ self.incident_to_match = incident_to_match self.incidents_df = incidents_df self.threshold = similarity_threshold self.max_incidents = max_incidents def predict(self) -> pd.DataFrame: self.get_score() self.filter_results() self.prepare_for_display() return self.incidents_df def get_score(self) -> None: t = FrequencyIndicatorsTransformer( self.incidents_df, self.incident_to_match, ) t.get_score() def filter_results(self) -> None: self.incidents_df = self.incidents_df[self.incidents_df[SIMILARITY_SCORE_COLUMN] > self.threshold] self.incidents_df = self.incidents_df.sort_values([SIMILARITY_SCORE_COLUMN], ascending=False) self.incidents_df = self.incidents_df.head(self.max_incidents) def prepare_for_display(self) -> None: vocabulary = self.incident_to_match[INDICATORS_COLUMN].iloc[0].split(" ") self.incidents_df[IDENTICAL_INDICATORS_COLUMN] = self.incidents_df[INDICATORS_COLUMN].apply( lambda x: ",".join([id for id in x.split(" ") if id in vocabulary]) ) def search_indicators( query: str, fields_to_populate: list | None = None, limit: int = SEARCH_INDICATORS_LIMIT, page_size: int = SEARCH_INDICATORS_PAGE_SIZE, ) -> list: demisto.debug(f"Searching indicators with {query=}") search_indicators = IndicatorsSearcher( query=query, limit=limit, size=page_size, filter_fields=",".join(fields_to_populate) if fields_to_populate else None ) return flatten_list([ioc_res.get("iocs") or [] for ioc_res in search_indicators]) def get_indicators_of_actual_incident( incident_id: str, indicator_types: list[str], min_number_of_indicators: int, max_incidents_per_indicator: int, ) -> dict[str, dict]: """Given an incident ID, returns a map between IDs of its related indicators and their data :param incident_id: ID of actual incident :param indicators_types: list of indicators type accepted :param min_number_of_indicators: Min number of indicators in the actual incident :param max_incidents_per_indicator: Max incidents in indicators for white list :return: a map from indicator ids of the actual incident to their data """ indicators = search_indicators(query=f"investigationIDs:({incident_id})") if not indicators: return {} indicators = [i for i in indicators if len(i.get("investigationIDs") or []) <= max_incidents_per_indicator] if indicator_types: indicators = [x for x in indicators if x[INDICATOR_TYPE_FIELD].lower() in indicator_types] if len(indicators) < min_number_of_indicators: return {} indicators_data = {ind[INDICATOR_ID_FIELD]: ind for ind in indicators} demisto.debug(f"Found {len(indicators_data)} indicators for incident {incident_id}: {list(indicators_data.keys())}") return indicators_data def get_related_incidents( indicators: dict[str, dict], query: str, from_date: str | None, ) -> list[str]: """Given indicators data including their related incidents, filters their related incidents by query and date and returns a list of the incident IDs. :param indicators: List of indicators :param query: A query to filter the related incidents by :param from_date: A created date to filter the related incidents by :return: The list of the related incident IDs """ incident_ids = flatten_list([i.get("investigationIDs") or [] for i in indicators.values()]) incident_ids = list({x for x in incident_ids if not re.match(PLAYGROUND_PATTERN, x)}) if not (query or from_date) or not incident_ids: demisto.debug(f"Found {len(incident_ids)} related incidents: {incident_ids}") return incident_ids args = { "query": f"{query + ' AND ' if query else ''}incident.id:({' '.join(incident_ids)})", "populateFields": INCIDENT_ID_FIELD, "fromDate": from_date, } demisto.debug(f"Executing GetIncidentsByQuery with {args=}") incidents = get_incidents_by_query(args) incident_ids = [incident[INCIDENT_ID_FIELD] for incident in incidents] demisto.debug(f"Found {len(incident_ids)} related incidents: {incident_ids}") return incident_ids def get_indicators_of_related_incidents( incident_ids: list[str], max_incidents_per_indicator: int, ) -> list[dict]: if not incident_ids: demisto.debug("No mutual indicators were found.") return [] indicators = search_indicators( query=f"investigationIDs:({' '.join(incident_ids)})", fields_to_populate=INDICATOR_FIELDS_TO_POPULATE_FROM_QUERY, ) indicators = [i for i in indicators if len(i.get("investigationIDs") or []) <= max_incidents_per_indicator] indicators_ids = [ind[INDICATOR_ID_FIELD] for ind in indicators] demisto.debug(f"Found {len(indicators_ids)} related indicators: {indicators_ids}") return indicators def get_mutual_indicators( related_indicators: list[dict], indicators_of_actual_incident: dict[str, dict], ) -> list[dict]: mutual_indicators = [ind for ind in related_indicators if ind[INDICATOR_ID_FIELD] in indicators_of_actual_incident] mutual_indicators_ids = [ind[INDICATOR_ID_FIELD] for ind in mutual_indicators] demisto.debug(f"Found {len(mutual_indicators_ids)} mutual indicators: {mutual_indicators_ids}") return mutual_indicators def get_mutual_indicators_df( indicators: list[dict], incident_ids: list[str], ) -> pd.DataFrame: indicators_df = pd.DataFrame(indicators) if not indicators_df.empty: indicators_df[INVOLVED_INCIDENTS_COUNT_COLUMN] = indicators_df[INVESTIGATION_IDS_FIELD].apply( lambda inv_ids: sum(id_ in incident_ids for id_ in inv_ids), ) indicators_df[INDICATOR_LINK_COLUMN] = indicators_df[INDICATOR_ID_FIELD].apply(lambda x: INDICATOR_LINK_FORMAT.format(x)) indicators_df = indicators_df.sort_values( [SCORE_FIELD, INVOLVED_INCIDENTS_COUNT_COLUMN], ascending=False, ) indicators_df[REPUTATION_COLUMN] = indicators_df[SCORE_FIELD].apply(scoreToReputation) # pylint: disable=E1137 indicators_df = indicators_df.rename({INDICATOR_TYPE_FIELD: TYPE_COLUMN}, axis=1) indicators_df = indicators_df.rename({VALUE_FIELD_FOR_COMPATIBILITY: VALUE_FIELD}, axis=1) return indicators_df def mutual_indicators_results(mutual_indicators: list[dict], incident_ids: list[str]): indicators_df = get_mutual_indicators_df(mutual_indicators, incident_ids) outputs = [] if indicators_df.empty else indicators_df[[INDICATOR_ID_FIELD, VALUE_FIELD]].to_dict(orient="records") readable_output = tableToMarkdown( "Mutual Indicators", indicators_df.to_dict(orient="records"), headers=MUTUAL_INDICATORS_HEADERS, headerTransform=pascalToSpace, ) return CommandResults( outputs=outputs, outputs_prefix="MutualIndicators.indicators", readable_output=readable_output, ) def create_actual_incident_df(indicators_of_actual_incident: dict[str, dict]) -> pd.DataFrame: return pd.DataFrame( data=[" ".join(indicators_of_actual_incident.keys())], columns=[INDICATORS_COLUMN], ) def create_related_incidents_df( indicators: list[dict], incident_ids: list[str], actual_incident_id: str, ) -> dict[str, list]: """ :param indicators: list of dict representing indicators :param incident_ids: list of incident ids :return: dict of {incident id : list of indicators ids related to this incident) """ incidents_to_indicators = { inc_id: [ indicator[INDICATOR_ID_FIELD] for indicator in indicators if inc_id in (indicator.get(INVESTIGATION_IDS_FIELD) or []) ] for inc_id in incident_ids } return pd.DataFrame.from_dict( data={k: " ".join(v) for k, v in incidents_to_indicators.items() if k != actual_incident_id}, orient="index", columns=[INDICATORS_COLUMN], ) def enrich_incidents( incidents: pd.DataFrame, fields_to_display: list, ) -> pd.DataFrame: """ Enriches a DataFrame of incidents with the given fields to display. :param similar_incidents: Incidents dataFrame :param fields_to_display: Fields selected for enrichement :return: Incidents dataFrame enriched """ if incidents.empty: return incidents incident_ids = incidents.id.tolist() if INCIDENT_ID_FIELD in incidents.columns else incidents.index args = { "query": f"incident.id:({' '.join(incident_ids)})", "populateFields": ",".join(fields_to_display), } demisto.debug(f"Executing GetIncidentsByQuery with {args=}") res = get_incidents_by_query(args) incidents_map: dict[str, dict] = {incident[INCIDENT_ID_FIELD]: incident for incident in res} if CREATED_FIELD in fields_to_display: incidents[CREATED_FIELD] = [ dateparser.parse(incidents_map[inc_id][CREATED_FIELD]).strftime(DATE_FORMAT) # type: ignore for inc_id in incident_ids ] if STATUS_FIELD in fields_to_display: incidents[STATUS_FIELD] = [STATUS_DICT.get(incidents_map[inc_id][STATUS_FIELD]) or " " for inc_id in incident_ids] for field in fields_to_display: if field not in [CREATED_FIELD, STATUS_FIELD]: incidents[field] = [incidents_map[inc_id].get(field) or "" for inc_id in incident_ids] return incidents def replace_indicator_ids_with_values( inc_ids: str, indicators_data: dict[str, dict], ) -> str: return "\n".join([indicators_data.get(x, {}).get(VALUE_FIELD_FOR_COMPATIBILITY) or " " for x in inc_ids.split(" ")]) def format_similar_incidents( similar_incidents: pd.DataFrame, indicators_data: dict[str, dict], fields_to_display: list[str], ) -> pd.DataFrame: """Formats the similar incidents DataFrame. :param indicators_data: a mapping between IDs and the mutual indicators data :param fields_to_display: Fields selected for enrichement :return: a formatted, enriched DataFrame of the similar incidents. """ if similar_incidents.empty: demisto.debug("No similar incidents found.") return similar_incidents # format and enrich DataFrame similar_incidents = similar_incidents.reset_index().rename(columns={"index": INCIDENT_ID_FIELD}) similar_incidents[INCIDENT_LINK_COLUMN] = similar_incidents[INCIDENT_ID_FIELD].apply( lambda _id: INCIDENT_LINK_FORMAT.format(_id) ) similar_incidents[IDENTICAL_INDICATORS_COLUMN] = similar_incidents[IDENTICAL_INDICATORS_COLUMN].apply( lambda inc_ids: replace_indicator_ids_with_values(inc_ids, indicators_data) ) similar_incidents = similar_incidents[ [INCIDENT_LINK_COLUMN, INCIDENT_ID_FIELD, IDENTICAL_INDICATORS_COLUMN, SIMILARITY_SCORE_COLUMN] ] return enrich_incidents(similar_incidents, fields_to_display) def similar_incidents_results( similar_incidents: pd.DataFrame, indicators_of_actual_incident: dict, fields_to_display: list, ): similar_incidents = format_similar_incidents(similar_incidents, indicators_of_actual_incident, fields_to_display) outputs = similar_incidents.to_dict(orient="records") additional_headers = [ x for x in similar_incidents.columns.tolist() if x not in FIRST_COLUMNS_INCIDENTS_DISPLAY + FIELDS_TO_EXCLUDE_FROM_DISPLAY ] return CommandResults( outputs={"similarIncident": outputs, "isSimilarIncidentFound": len(outputs) > 0}, outputs_prefix="DBotFindSimilarIncidentsByIndicators", raw_response=outputs, readable_output=tableToMarkdown( "Similar Incidents", outputs, headers=FIRST_COLUMNS_INCIDENTS_DISPLAY + additional_headers, headerTransform=str.title, ), tags=["similarIncidents"], # type: ignore ) def actual_incident_results( incident_df: pd.DataFrame, incident_id: str, indicators_data: dict, fields_to_display: list[str], ) -> CommandResults: """ Formats the given DataFrame, and returns a CommandResults object of the actual incident data :param incident_df: a DataFrame of actual incident :param incident_id: the incident ID :param indicators_data: indicators data of the actual incident :param fields_to_display: A list of fields to display :return: a CommandResults obj """ incident_df[INCIDENT_ID_FIELD] = [incident_id] incident_df[INCIDENT_LINK_COLUMN] = incident_df[INCIDENT_ID_FIELD].apply(lambda _id: INCIDENT_LINK_FORMAT.format(_id)) incident_df[INDICATORS_COLUMN] = incident_df[INDICATORS_COLUMN].apply( lambda inc_ids: replace_indicator_ids_with_values(inc_ids, indicators_data) ) incident_df = enrich_incidents(incident_df, fields_to_display) additional_headers = [ x for x in incident_df.columns.tolist() if x not in FIRST_COLUMNS_INCIDENTS_DISPLAY + FIELDS_TO_EXCLUDE_FROM_DISPLAY ] return CommandResults( readable_output=tableToMarkdown( "Actual Incident", incident_df.to_dict(orient="records"), headers=FIRST_COLUMNS_INCIDENTS_DISPLAY + additional_headers, headerTransform=pascalToSpace, ), ) def find_similar_incidents_by_indicators(incident_id: str, args: dict) -> list[CommandResults]: # get_indicators_of_actual_incident() args indicators_types = argToList(args.get("indicatorsTypes"), transform=str.lower) min_number_of_indicators = int(args["minNumberOfIndicators"]) max_incidents_per_indicator = int(args["maxIncidentsInIndicatorsForWhiteList"]) # get_related_incidents() args query = args.get("query") or "" from_date = args.get("fromDate") # get_similar_incidents() args similarity_threshold = float(args["threshold"]) max_incidents_to_display = int(args["maxIncidentsToDisplay"]) # outputs formatting args show_actual_incident = argToBoolean(args.get("showActualIncident")) fields_to_display = list(set(argToList(args["fieldsIncidentToDisplay"])) | {CREATED_FIELD, NAME_FIELD}) command_results_list: list[CommandResults] = [] indicators_of_actual_incident = get_indicators_of_actual_incident( incident_id, indicators_types, min_number_of_indicators, max_incidents_per_indicator, ) incident_ids = get_related_incidents(indicators_of_actual_incident, query, from_date) related_indicators = get_indicators_of_related_incidents(incident_ids, max_incidents_per_indicator) mutual_indicators = get_mutual_indicators(related_indicators, indicators_of_actual_incident) actual_incident_df = create_actual_incident_df(indicators_of_actual_incident) related_incidents_df = create_related_incidents_df(related_indicators, incident_ids, incident_id) similar_incidents = Model( actual_incident_df, related_incidents_df, similarity_threshold, max_incidents_to_display, ).predict() if show_actual_incident: command_results_list.append( actual_incident_results(actual_incident_df, incident_id, indicators_of_actual_incident, fields_to_display) ) command_results_list.extend( [ mutual_indicators_results(mutual_indicators, incident_ids), similar_incidents_results(similar_incidents, indicators_of_actual_incident, fields_to_display), ] ) return command_results_list def main(): # pragma: no cover try: args = demisto.args() incident_id = args.get("incidentId") or demisto.incidents()[0]["id"] return_results(find_similar_incidents_by_indicators(incident_id, args)) except Exception as e: return_error(f"Failed to execute DBotFindSimilarIncidentsByIndicators. Error: {e}") if __name__ in ["__main__", "__builtin__", "builtins"]: main()