DBotFindSimilarIncidents
Finds past similar incidents based on incident fields' similarity. Includes an option to also display indicators similarity. Note: For the similarity calculation, at least one field must be provided in one of the "similarTextField", "similarCategoricalField", or "similarJsonField" arguments.
python · Base
Details
| ID | DBotFindSimilarIncidents |
|---|---|
| Language | python |
| From Version | 5.0.0 |
| Docker Image | demisto/sklearn:1.0.0.12545527 |
README
Find past similar incidents based on incident fields’ similarity. Includes an option to also display indicators similarity.
Note: For the similarity calculation, at least one field must be provided in one of the “similarTextField”, “similarCategoricalField”, or “similarJsonField” arguments.
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.
- Cortex XDR incident handling v3
- Dedup - Generic v4
- Endpoint Malware Investigation - Generic V2
Inputs
| Argument Name | Description |
|---|---|
| incidentId | Incident ID to get the prediction of. If empty, predicts the the current incident ID. |
| similarTextField | Comma-separated list of incident text fields to take into account when computing similarity. For example: commandline, URL. Note: In order to calculate similarity, fields must consist of a minimum of 2 letters. |
| similarCategoricalField | Comma-separated list of incident categorical fields to take into account whe computing similarity. For example: IP, URL. Note: In order to calculate similarity, fields must consist of a minimum of 2 letters. |
| similarJsonField | Comma-separated list of incident JSON fields to take into account whe computing similarity. For example: CustomFields. Note: In order to calculate similarity, fields must consist of a minimum of 2 letters. |
| fieldsToDisplay | Comma-separated list of additional incident fields to display, but which will not be taken into account when computing similarity. |
| fieldExactMatch | Comma-separated list of incident fields that have to be equal to the current incident fields. This helps reduce the query size. These fields are not part of the similarity calculation. |
| useAllFields | Whether to use a predefined set of fields and custom fields to compute similarity. If “True”, it will ignore values in similarTextField, similarCategoricalField, similarJsonField. |
| fromDate | The start date by which to filter incidents. Date format will be the same as in the incidents query page, for example, “3 days ago”, ““2019-01-01T00:00:00 +0200”). |
| toDate | The end date by which to filter incidents. Date format will be the same as in the incidents query page, for example, “3 days ago”, ““2019-01-01T00:00:00 +0200”). |
| query | Argument for the query. This helps reduce the query size. |
| limit | The maximum number of incidents to query. |
| aggreagateIncidentsDifferentDate | Whether to aggregate duplicate incidents within diffrerent dates. |
| showIncidentSimilarityForAllFields | Whether to display the similarity score for each of the incident fields. |
| minimunIncidentSimilarity | Retain incidents with similarity score that’s higher than the MinimunIncidentSimilarity. |
| maxIncidentsToDisplay | The maximum number of incidents to display. |
| showCurrentIncident | Whether to display the current incident. |
| includeIndicatorsSimilarity | Whether to include similarity of indicators from DBotFindSimilarIncidentsByIndicators in the final score. |
| minNumberOfIndicators | The minimum number of indicators required related to the incident before running the model. Relevant if includeIndicatorsSimilarity is “True”. |
| indicatorsTypes | Comma-separated list of indicator types to take into account. If empty, uses all indicators types. Relevant if includeIndicatorsSimilarity is “True”. |
| maxIncidentsInIndicatorsForWhiteList | Help to filter out indicators that appear in many incidents. Relevant if includeIndicatorsSimilarity is “True”. |
Outputs
There are no outputs for this script.
from copy import deepcopy import demistomock as demisto import numpy as np import pandas as pd import pytest from CommonServerPython import DemistoException CURRENT_INCIDENT_NOT_EMPTY = [ { "id": "123", "commandline": "powershell IP=1.1.1.1", "CustomFields": {"nested_field": "value_nested_field"}, "empty_current_incident_field": None, "empty_fetched_incident_field": "empty_fetched_incident_field_1", } ] FETCHED_INCIDENT_NOT_EMPTY = [ { "id": "1", "created": "2021-01-30", "commandline": "powershell IP=1.1.1.1", "CustomFields": {"nested_field": "value_nested_field_1"}, "empty_current_incident_field": "empty_current_incident_field_1", "empty_fetched_incident_field": None, "name": "incident_name_1", }, { "id": "2", "created": "2021-01-30", "commandline": "powershell IP=2.2.2.2", "CustomFields": {"nested_field": "value_nested_field_2"}, "empty_current_incident_field": "empty_current_incident_field2", "empty_fetched_incident_field": "", "name": "incident_name_2", }, { "id": "3", "created": "2021-01-30", "commandline": "powershell IP=1.1.1.1", "CustomFields": {"nested_field": "value_nested_field_3"}, "empty_current_incident_field": "empty_current_incident_field_3", "empty_fetched_incident_field": None, "name": "incident_name_3", }, ] FETCHED_INCIDENT_EMPTY = [] SIMILAR_INDICATORS_NOT_EMPTY = [ { "ID": "inc_1", "Identical indicators": "ind_1, ind_2", "created": "2021-01-30", "id": "1", "name": "incident_name_1", "similarity indicators": 0.2, }, { "ID": "inc_3", "Identical indicators": "ind_2", "created": "2021-01-30", "id": "3", "name": "incident_name_3", "similarity indicators": 0.4, }, ] SIMILAR_INDICATORS_EMPTY = [] @pytest.fixture(autouse=True) def mock_demistoVersion(mocker): mocker.patch.object(demisto, "demistoVersion", return_value={"platform": "xsoar"}) def executeCommand(command, args): from DBotFindSimilarIncidents import TAG_SCRIPT_INDICATORS global SIMILAR_INDICATORS, FETCHED_INCIDENT, CURRENT_INCIDENT if command == "DBotFindSimilarIncidentsByIndicators": return [[], {"Contents": SIMILAR_INDICATORS, "Type": "note", "Tags": [TAG_SCRIPT_INDICATORS]}] if command == "getIncidents": if "-id:" in args.get("query"): # query for similar incidents return [{"Contents": {"data": FETCHED_INCIDENT}, "Type": "note"}] else: # query for current incident return [{"Contents": {"data": CURRENT_INCIDENT}, "Type": "note"}] return None def check_exist_dataframe_columns(*fields, df): return all(field in df.columns.tolist() for field in fields) def test_keep_high_level_field(): from DBotFindSimilarIncidents import keep_high_level_field incidents_field = ["xdralerts.comandline", "commandline", "CustomsFields.commandline"] res = ["xdralerts", "commandline", "CustomsFields"] assert keep_high_level_field(incidents_field) == res def test_preprocess_incidents_field(): from DBotFindSimilarIncidents import PREFIXES_TO_REMOVE, preprocess_incidents_field assert preprocess_incidents_field("incident.commandline", PREFIXES_TO_REMOVE) == "commandline" assert preprocess_incidents_field("commandline", PREFIXES_TO_REMOVE) == "commandline" def test_check_list_of_dict(): from DBotFindSimilarIncidents import check_list_of_dict assert check_list_of_dict([{"test": "value_test"}, {"test1": "value_test1"}]) is True assert check_list_of_dict({"test": "value_test"}) is False def test_match_one_regex(): from DBotFindSimilarIncidents import REGEX_IP, match_one_regex assert match_one_regex("123.123.123.123", [REGEX_IP]) is True assert match_one_regex("123.123.123", [REGEX_IP]) is False assert match_one_regex("abc", [REGEX_IP]) is False assert match_one_regex(1, [REGEX_IP]) is False @pytest.mark.parametrize( "command, expected", [ pytest.param("cmd -k IP=1.1.1.1 [1.1.1.1]", "cmd -k ip = IP IP", id="string_with_ip_and_brackets"), pytest.param('powershell "remove_quotes"', "powershell remove_quotes", id="string_with_quotes"), pytest.param(["GET", "POST"], {"get", "post"}, id="list_of_strings"), pytest.param([80, 443, 8080], {"80", "443", "8080"}, id="list_of_integers_regression"), pytest.param(["http", 443], {"http", "443"}, id="mixed_list_str_and_int"), pytest.param("", "", id="empty_string"), pytest.param(None, "", id="none_input"), pytest.param([], "", id="empty_list"), ], ) def test_normalize_command_line(command, expected): from DBotFindSimilarIncidents import normalize_command_line result = normalize_command_line(command) # For list inputs the join order is non-deterministic (set), so compare token sets if isinstance(expected, set): assert set(result.split()) == expected else: assert result == expected def test_euclidian_similarity_capped(): from DBotFindSimilarIncidents import euclidian_similarity_capped x = np.array([[1, 1, 1], [2, 2, 2]]) y = np.array([[2.1, 2.1, 2.1]]) distance = euclidian_similarity_capped(x, y) assert distance[0] == 0 assert distance[1] > 0 def test_main_regular(mocker): from DBotFindSimilarIncidents import COLUMN_ID, COLUMN_TIME, SIMILARITY_COLUNM_NAME, SIMILARITY_COLUNM_NAME_INDICATOR, main global SIMILAR_INDICATORS, FETCHED_INCIDENT, CURRENT_INCIDENT FETCHED_INCIDENT = deepcopy(FETCHED_INCIDENT_NOT_EMPTY) CURRENT_INCIDENT = deepcopy(CURRENT_INCIDENT_NOT_EMPTY) SIMILAR_INDICATORS = deepcopy(SIMILAR_INDICATORS_NOT_EMPTY) mocker.patch.object( demisto, "args", return_value={ "incidentId": 12345, "similarTextField": "incident.commandline, commandline, command, " "empty_current_incident_field, empty_fetched_incident_field", "similarCategoricalField": "signature, filehash, incident.commandline", "similarJsonField": "CustomFields", "limit": 10000, "fieldExactMatch": "", "fieldsToDisplay": "filehash, destinationip, closeNotes, sourceip, alertdescription", "showIncidentSimilarityForAllFields": True, "minimunIncidentSimilarity": 0.2, "maxIncidentsToDisplay": 100, "query": "", "aggreagateIncidentsDifferentDate": "False", "includeIndicatorsSimilarity": "True", }, ) mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand) res, _ = main() assert "empty_current_incident_field" not in res.columns assert res.loc["3", "Identical indicators"] == "ind_2" assert res.loc["2", "Identical indicators"] == "" assert check_exist_dataframe_columns( SIMILARITY_COLUNM_NAME_INDICATOR, SIMILARITY_COLUNM_NAME, COLUMN_ID, COLUMN_TIME, "name", df=res ) assert res.loc["3", "similarity indicators"] == 0.4 assert res.loc["2", "similarity indicators"] == 0.0 def test_main_no_indicators_found(mocker): """ Test if no indicators found :param mocker: :return: """ from DBotFindSimilarIncidents import COLUMN_ID, COLUMN_TIME, SIMILARITY_COLUNM_NAME, SIMILARITY_COLUNM_NAME_INDICATOR, main global SIMILAR_INDICATORS, FETCHED_INCIDENT, CURRENT_INCIDENT FETCHED_INCIDENT = deepcopy(FETCHED_INCIDENT_NOT_EMPTY) CURRENT_INCIDENT = deepcopy(CURRENT_INCIDENT_NOT_EMPTY) SIMILAR_INDICATORS = deepcopy(SIMILAR_INDICATORS_EMPTY) mocker.patch.object( demisto, "args", return_value={ "incidentId": 12345, "similarTextField": "incident.commandline, commandline, command," " empty_current_incident_field, empty_fetched_incident_field", "similarCategoricalField": "signature, filehash", "similarJsonField": "CustomFields", "limit": 10000, "fieldExactMatch": "", "fieldsToDisplay": "filehash, destinationip, closeNotes, sourceip, alertdescription", "showIncidentSimilarityForAllFields": True, "minimunIncidentSimilarity": 0.2, "maxIncidentsToDisplay": 100, "query": "", "aggreagateIncidentsDifferentDate": "False", "includeIndicatorsSimilarity": "True", }, ) mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand) res, _ = main() assert "empty_current_incident_field" not in res.columns assert (res["Identical indicators"] == ["", "", ""]).all() assert check_exist_dataframe_columns( SIMILARITY_COLUNM_NAME_INDICATOR, SIMILARITY_COLUNM_NAME, COLUMN_ID, COLUMN_TIME, "name", df=res ) assert (res["similarity indicators"] == [0.0, 0.0, 0.0]).all() def test_main_no_fetched_incidents_found(mocker): """ Test output if no related incidents found - Should return None and MESSAGE_NO_INCIDENT_FETCHED :param mocker: :return: """ from DBotFindSimilarIncidents import MESSAGE_NO_INCIDENT_FETCHED, main global SIMILAR_INDICATORS, FETCHED_INCIDENT, CURRENT_INCIDENT FETCHED_INCIDENT = deepcopy(FETCHED_INCIDENT_EMPTY) CURRENT_INCIDENT = deepcopy(CURRENT_INCIDENT_NOT_EMPTY) SIMILAR_INDICATORS = deepcopy(SIMILAR_INDICATORS_NOT_EMPTY) mocker.patch.object( demisto, "args", return_value={ "incidentId": 12345, "similarTextField": "incident.commandline, commandline, command, " "empty_current_incident_field, empty_fetched_incident_field", "similarCategoricalField": "signature, filehash", "similarJsonField": "CustomFields", "limit": 10000, "fieldExactMatch": "", "fieldsToDisplay": "filehash, destinationip, closeNotes, sourceip, alertdescription", "showIncidentSimilarityForAllFields": True, "minimunIncidentSimilarity": 0.2, "maxIncidentsToDisplay": 100, "query": "", "aggreagateIncidentsDifferentDate": "False", "includeIndicatorsSimilarity": "True", }, ) mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand) res = main() assert not res[0] assert MESSAGE_NO_INCIDENT_FETCHED in res[1] def test_main_some_incorrect_fields(): from DBotFindSimilarIncidents import find_incorrect_fields wrong_field_1 = "wrong_field_1" wrong_field_2 = "wrong_field_2" correct_field_1 = "empty_fetched_incident_field" current_incident_df = pd.DataFrame(CURRENT_INCIDENT) global_msg, incorrect_fields = find_incorrect_fields([correct_field_1, wrong_field_1, wrong_field_2], current_incident_df, "") assert incorrect_fields == ["wrong_field_1", "wrong_field_2"] assert wrong_field_1 in global_msg assert wrong_field_2 in global_msg assert correct_field_1 not in global_msg def test_main_all_incorrect_field(mocker): """ Test if only incorrect fields - Should return None and MESSAGE_INCORRECT_FIELD message for wrong fields :param mocker: :return: """ from DBotFindSimilarIncidents import MESSAGE_INCORRECT_FIELD, main global SIMILAR_INDICATORS, FETCHED_INCIDENT, CURRENT_INCIDENT FETCHED_INCIDENT = deepcopy(FETCHED_INCIDENT_NOT_EMPTY) CURRENT_INCIDENT = deepcopy(CURRENT_INCIDENT_NOT_EMPTY) SIMILAR_INDICATORS = deepcopy(SIMILAR_INDICATORS_NOT_EMPTY) wrong_field_1 = "wrong_field_1" wrong_field_2 = "wrong_field_2" wrong_field_3 = "wrong_field_3" wrong_field_4 = "wrong_field_4" mocker.patch.object( demisto, "args", return_value={ "incidentId": 12345, "similarTextField": wrong_field_1, "similarCategoricalField": wrong_field_2, "similarJsonField": wrong_field_3, "limit": 10000, "fieldExactMatch": "", "fieldsToDisplay": wrong_field_4, "showIncidentSimilarityForAllFields": True, "minimunIncidentSimilarity": 0.2, "maxIncidentsToDisplay": 100, "query": "", "aggreagateIncidentsDifferentDate": "False", "includeIndicatorsSimilarity": "True", }, ) mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand) df, msg = main() assert not df assert MESSAGE_INCORRECT_FIELD % " , ".join([wrong_field_1, wrong_field_3, wrong_field_2, wrong_field_4]) in msg assert all(field in msg for field in [wrong_field_1, wrong_field_2, wrong_field_3, wrong_field_4]) def test_main_incident_truncated(mocker): """ Test if fetched incident truncated - Should return MESSAGE_WARNING_TRUNCATED in the message :param mocker: :return: """ from DBotFindSimilarIncidents import MESSAGE_WARNING_TRUNCATED, main global SIMILAR_INDICATORS, FETCHED_INCIDENT, CURRENT_INCIDENT FETCHED_INCIDENT = deepcopy(FETCHED_INCIDENT_NOT_EMPTY) CURRENT_INCIDENT = deepcopy(CURRENT_INCIDENT_NOT_EMPTY) SIMILAR_INDICATORS = deepcopy(SIMILAR_INDICATORS_NOT_EMPTY) correct_field_1 = "commandline" wrong_field_2 = "wrong_field_2" wrong_field_3 = "wrong_field_3" wrong_field_4 = "wrong_field_4" mocker.patch.object( demisto, "args", return_value={ "incidentId": 12345, "similarTextField": correct_field_1, "similarCategoricalField": wrong_field_2, "similarJsonField": wrong_field_3, "limit": 3, "fieldExactMatch": "", "fieldsToDisplay": wrong_field_4, "showIncidentSimilarityForAllFields": True, "minimunIncidentSimilarity": 0.2, "maxIncidentsToDisplay": 100, "query": "", "aggreagateIncidentsDifferentDate": "False", "includeIndicatorsSimilarity": "True", }, ) mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand) df, msg = main() limit = demisto.args()["limit"] assert not df.empty assert MESSAGE_WARNING_TRUNCATED % (limit, limit) in msg def test_main_incident_nested(mocker): """ Given: Same test case as in test_main_regular but with a nested field as a similarTextField When: Running main() Then: Ensure the nested field exists in the results """ from DBotFindSimilarIncidents import main global SIMILAR_INDICATORS, FETCHED_INCIDENT, CURRENT_INCIDENT FETCHED_INCIDENT = deepcopy(FETCHED_INCIDENT_NOT_EMPTY) CURRENT_INCIDENT = deepcopy(CURRENT_INCIDENT_NOT_EMPTY) SIMILAR_INDICATORS = deepcopy(SIMILAR_INDICATORS_NOT_EMPTY) nested_field = "CustomFields.nested_field" mocker.patch.object( demisto, "args", return_value={ "incidentId": 12345, "similarTextField": f"{nested_field},incident.commandline, commandline, command, " "empty_current_incident_field, empty_fetched_incident_field", "similarCategoricalField": "signature, filehash, incident.commandline", "similarJsonField": "", "limit": 10000, "fieldExactMatch": "", "fieldsToDisplay": "filehash, destinationip, closeNotes, sourceip, alertdescription", "showIncidentSimilarityForAllFields": True, "minimunIncidentSimilarity": 0.2, "maxIncidentsToDisplay": 100, "query": "", "aggreagateIncidentsDifferentDate": "False", "includeIndicatorsSimilarity": "True", }, ) mocker.patch.object(demisto, "executeCommand", side_effect=executeCommand) df, _ = main() assert not df.empty assert (df[f"similarity {nested_field}"] > 0).all() def test_get_get_data_from_indicators_automation(): from DBotFindSimilarIncidents import TAG_SCRIPT_INDICATORS, get_data_from_indicators_automation res = get_data_from_indicators_automation(None, TAG_SCRIPT_INDICATORS) assert res is None @pytest.fixture def sample_data(): # Create sample data for testing data = { "created": ["2019-02-20T15:47:23.962164+02:00"], "Name": ["t"], "Id": [["123"]], "test": [None], "xdralerts": ["N/A"], "test2": [""], } return pd.DataFrame(data) fields_to_match = ["created", "Name", "test", "Id", "test2", "xdralerts", "hello"] expected_results = ["created"] def test_remove_empty_or_short_fields(sample_data): from DBotFindSimilarIncidents import ( FIELD_SKIP_REASON_DOESNT_EXIST, FIELD_SKIP_REASON_FALSY_VALUE, FIELD_SKIP_REASON_TOO_SHORT, Model, ) """ Given: - sample_data: a dataframe with a column of strings When: - calling remove_empty_or_short_fields function Then: - assert that the function removes empty or short or None or 'N/A' or list objects fields """ # Create an instance of Model my_instance = Model({}) my_instance.incident_to_match = sample_data my_instance.field_for_command_line = fields_to_match my_instance.field_for_potential_exact_match = [] my_instance.field_for_json = [] should_proceed, all_skip_reasons = my_instance.remove_empty_or_short_fields() assert my_instance.field_for_command_line == expected_results assert should_proceed assert all("created" not in reason for reason in all_skip_reasons) assert f' - {FIELD_SKIP_REASON_TOO_SHORT.format(field="Name", val="t", len=1)}' in all_skip_reasons assert f' - {FIELD_SKIP_REASON_TOO_SHORT.format(field="Id", val=["123"], len=1)}' in all_skip_reasons assert f' - {FIELD_SKIP_REASON_FALSY_VALUE.format(field="test", val=None)}' in all_skip_reasons assert f' - {FIELD_SKIP_REASON_FALSY_VALUE.format(field="test2", val="")}' in all_skip_reasons assert f' - {FIELD_SKIP_REASON_FALSY_VALUE.format(field="xdralerts", val="N/A")}' in all_skip_reasons assert f' - {FIELD_SKIP_REASON_DOESNT_EXIST.format(field="hello")}' in all_skip_reasons def test_predict_without_similarity_fields(sample_data): """ Given: - A Model object When: - No similarity fields were provided - Calling Model.predict() Then: - Ensure the correct exception is raised """ from DBotFindSimilarIncidents import Model model = Model({}) model.incident_to_match = sample_data model.field_for_command_line = [] model.field_for_potential_exact_match = [] model.field_for_json = [] with pytest.raises(DemistoException) as e: model.predict() assert "No fields were provided for similarity calculation" in str(e) @pytest.mark.parametrize( "similar_text_field", [ ( "incident.xdralerts.osactorprocesscommandline,incident.xdralerts.actorprocesscommandline,incident.xdralerts." "actionprocessimagecommandline,incident.xdralerts.causalityactorprocesscommandline,incident.xdralerts.host_name," "incident.xdralerts.user_name" ), ( "alert.xdralerts.osactorprocesscommandline,alert.xdralerts.actorprocesscommandline,alert.xdralerts." "actionprocessimagecommandline,alert.xdralerts.causalityactorprocesscommandline,alert.xdralerts.host_name," "alert.xdralerts.user_name" ), ( "issue.xdralerts.osactorprocesscommandline,issue.xdralerts.actorprocesscommandline,issue.xdralerts." "actionprocessimagecommandline,issue.xdralerts.causalityactorprocesscommandline,issue.xdralerts.host_name,incident." "xdralerts.user_name" ), ], ) def test_extract_fields_from_args(similar_text_field): """ Given: - Fields to extract with different prefixes. - Case 1: incident prefix. - Case 2: alert prefix. - Case 3: issue prefix. When: Calling extract_fields_from_args function. Then: - Ensure the fields were extracted correctly. """ from DBotFindSimilarIncidents import extract_fields_from_args results = extract_fields_from_args(similar_text_field) expected_results = [ "xdralerts.osactorprocesscommandline", "xdralerts.actorprocesscommandline", "xdralerts.actionprocessimagecommandline", "xdralerts.causalityactorprocesscommandline", "xdralerts.host_name", "xdralerts.user_name", ] assert results == expected_results @pytest.mark.parametrize( "is_platform, version_ge, incident_id, expected_link", [ (True, True, "43076", "[43076](/issue-view/43076)"), (True, False, "43076", "[43076](/issue-view/43076)"), (False, True, "43076", "[43076](/Details/43076)"), (False, False, "43076", "[43076](#/Details/43076)"), ], ) def test_create_incident_link(mocker, is_platform, version_ge, incident_id, expected_link): """ Given: - An incident ID. - Case 1: Unified Cortex platform (XSIAM v3 / XSOAR on platform) -> issue-view URL. - Case 2: Unified Cortex platform takes precedence regardless of demisto version. - Case 3: Cortex XSOAR 8.x (version >= 8.4.0) -> path-based URL. - Case 4: Cortex XSOAR 6.x (version < 8.4.0) -> legacy hash-based URL. When: Calling the incident link creator. Then: - Ensure the correct link format is produced for each platform. """ import DBotFindSimilarIncidents mocker.patch.object(DBotFindSimilarIncidents, "is_platform", return_value=is_platform) mocker.patch.object(DBotFindSimilarIncidents, "is_demisto_version_ge", return_value=version_ge) link_creator = DBotFindSimilarIncidents.get_incident_link_creator() assert link_creator(incident_id) == expected_link @pytest.mark.parametrize( "value, expected", [ ('Payment Request "0000025803" has been "Approved".', 'Payment Request \\"0000025803\\" has been \\"Approved\\".'), ("no special chars", "no special chars"), ('a "quote"', 'a \\"quote\\"'), ("line1\nline2", "line1\\nline2"), ("carriage\rreturn", "carriage\\rreturn"), ("back\\slash", "back\\\\slash"), (12345, "12345"), ], ) def test_escape_query_value(value, expected): """ Given: - A field value that may contain special characters (double quotes, newlines, backslashes). When: - Escaping the value before embedding it in a getIncidents query. Then: - Ensure special characters are escaped so the resulting query is well-formed. """ from DBotFindSimilarIncidents import escape_query_value assert escape_query_value(value) == expected def test_get_all_incidents_for_time_window_and_exact_match_escapes_special_chars(mocker): """ Given: - An incident whose exact-match field value contains unescaped double quotes. When: - Building the getIncidents query in get_all_incidents_for_time_window_and_exact_match. Then: - Ensure the double quotes in the field value are escaped in the query passed to get_incidents_by_query, so the query is not malformed. """ import DBotFindSimilarIncidents captured = {} def fake_get_incidents_by_query(args): captured["query"] = args["query"] return [{"id": "1"}] mocker.patch.object(DBotFindSimilarIncidents, "get_incidents_by_query", side_effect=fake_get_incidents_by_query) incident = { "id": "173171866", "reportedemailsubject": 'Payment Request "0000025803" has been "Approved".', } DBotFindSimilarIncidents.get_all_incidents_for_time_window_and_exact_match( exact_match_fields=["reportedemailsubject"], populate_fields=["id", "reportedemailsubject"], incident=incident, from_date="7 days ago", to_date="now", query_sup="", limit=1000, ) query = captured["query"] assert 'reportedemailsubject: "Payment Request \\"0000025803\\" has been \\"Approved\\"."' in query # No raw (unescaped) double quote should terminate the value prematurely. assert '"Payment Request "0000025803"' not in query