DBotFindSimilarIncidentsByIndicators
Finds similar incidents based on indicators' similarity. Indicators' contribution to the final score is based on their scarcity.
python · Base
Source
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()
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.