SearchIncidentsV2

Searches Demisto incidents. A summarized version of this scrips is available with the summarizedversion argument. This automation runs using the default Limited User role, unless you explicitly change the permissions. For more information, see the section about permissions here: - For Cortex XSOAR 6 see https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.x/Cortex-XSOAR-Playbook-Design-Guide/Automations - For Cortex XSOAR 8 Cloud see https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8/Cortex-XSOAR-Cloud-Documentation/Create-a-script - For Cortex XSOAR 8.7 On-prem see https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8.7/Cortex-XSOAR-On-prem-Documentation/Create-a-script https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.10/Cortex-XSOAR-Administrator-Guide/Automations

python · Common Scripts

Details

IDSearchIncidentsV2
Languagepython
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10404775
TagsUtility

README

Searches Demisto incidents. A summarized version of this scrips is available with the summarizedversion argument.

Permissions


This automation runs using the default Limited User role, unless you explicitly change the permissions.
For more information, see the section about permissions here: <~XSOAR>For Cortex XSOAR 6, see the https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/6.x/Cortex-XSOAR-Playbook-Design-Guide/Automations for Cortex XSOAR 8 Cloud, see the https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8/Cortex-XSOAR-Cloud-Documentation/Create-a-script for Cortex XSOAR 8 On-prem, see the https://docs-cortex.paloaltonetworks.com/r/Cortex-XSOAR/8.7/Cortex-XSOAR-On-prem-Documentation/Create-a-script.</~XSOAR><~XSIAM>https://docs-cortex.paloaltonetworks.com/r/Cortex-XSIAM/Cortex-XSIAM-Administrator-Guide/Permission-Management</~XSIAM>

Script Data


Name Description
Script Type python3
Tags Utility
Cortex XSOAR Version 5.0.0

Used In


Sample usage of this script can be found in the following playbooks and scripts.

  • Endpoint Investigation Plan
  • ExtraHop - Ticket Tracking
  • Kaseya VSA 0-day - REvil Ransomware Supply Chain Attack
  • MDE - False Positive Incident Handling
  • MDE - True Positive Incident Handling
  • Prisma Cloud Correlate Alerts v2
  • Ransomware Enrich and Contain
  • SafeBreach - Create Incidents per Insight and Associate Indicators
  • SolarStorm and SUNBURST Hunting and Response Playbook
  • Spring Core and Cloud Function SpEL RCEs

Inputs


Argument Name Description
id A comma-separated list of incident IDs by which to filter the results.
name A comma-separated list of incident names by which to filter the results.
status A comma-separated list of incident statuses by which to filter the results. For example: assigned.
notstatus A comma-separated list of incident statuses to exclude from the results. For example: assigned.
reason A comma-separated list of incident close reasons by which to filter the results.
fromdate Filter by from date (e.g. “3 days ago” or 2006-01-02T15:04:05+07:00 or 2006-01-02T15:04:05Z). Default value is “30 days ago”.
todate Filter by to date (e.g. “3 days ago” or 2006-01-02T15:04:05+07:00 or 2006-01-02T15:04:05Z)
fromclosedate Filter by from close date (e.g. 2006-01-02T15:04:05+07:00 or 2006-01-02T15:04:05Z)
toclosedate Filter by to close date (e.g. 2006-01-02T15:04:05+07:00 or 2006-01-02T15:04:05Z)
fromduedate Filter by from due date (e.g. 2006-01-02T15:04:05+07:00 or 2006-01-02T15:04:05Z)
toduedate Filter by to due date (e.g. 2006-01-02T15:04:05+07:00 or 2006-01-02T15:04:05Z)
level Filter by Severity
owner Filter by incident owners
details Filter by incident details
type Filter by incident type
query Use free form query (use Lucene syntax) as filter. All other filters will be ignored when this filter is used.
page Filter by the page number (deprecated)
trimevents The number of events to return from the alert JSON. The default is 0, which returns all events.
Note that the count is from the head of the list, regardless of event time or other properties.
size Number of incidents per page (per fetch) (deprecated)
sort Sort in format of field.asc,field.desc,…
searchresultslabel If provided, the value of this argument will be set under the searchResultsLabel context key for each incident found.
summarizedversion If enabled runs a summarized version of this script. Disables auto-extract, sets fromDate to 30 days, and minimizes the context output. You can add sepcific fields to context using the add_fields_to_summarize_context argument. Default is false.
includeinformational Supported only in XSIAM. When the value is set to ‘True’, informational severity alerts will return as part of the results. The ‘fromdate’ and ‘todate’ arguments must be provided to use this argument. The maximum value currently supported for the ‘fromdate’ argument to retrieve informational incidents is 5 hours. If a value greater than this is provided, it will be adjusted to 5 hours ago. To retrieve only informational incidents, use the query argument and include this limitation within the query. Default is false.
limit The maximum number of incidents to be returned. Default is 100.

Outputs


Path Description Type
foundIncidents.id A list of incident IDs returned from the query. Unknown
foundIncidents.name A list of incident names returned from the query. Unknown
foundIncidents.severity A list of incident severities returned from the query. Unknown
foundIncidents.status A list of incident statuses returned from the query. Unknown
foundIncidents.owner A list of incident owners returned from the query. Unknown
foundIncidents.created A list of the incident create date returned from the query. Unknown
foundIncidents.closed A list of incident close dates returned from the query. Unknown
foundIncidents.labels An array of labels per incident returned from the query. Unknown
foundIncidents.details Details of the incidents returned from the query. Unknown
foundIncidents.dueDate A list of incident due dates returned from the query. Unknown
foundIncidents.phase A list of incident phases returned from the query. Unknown
foundIncidents.searchResultsLabel The value provided in the searchresultslabel argument. String

Script Example

!SearchIncidentsV2 name="Incident to search"

Context Example

{
    "foundIncidents": [
        {
            "CustomFields": {
                "detectionsla": {
                    "accumulatedPause": 0,
                    "breachTriggered": false,
                    "dueDate": "0001-01-01T00:00:00Z",
                    "endDate": "0001-01-01T00:00:00Z",
                    "lastPauseDate": "0001-01-01T00:00:00Z",
                    "runStatus": "idle",
                    "sla": 20,
                    "slaStatus": -1,
                    "startDate": "0001-01-01T00:00:00Z",
                    "totalDuration": 0
                },
                "remediationsla": {
                    "accumulatedPause": 0,
                    "breachTriggered": false,
                    "dueDate": "0001-01-01T00:00:00Z",
                    "endDate": "0001-01-01T00:00:00Z",
                    "lastPauseDate": "0001-01-01T00:00:00Z",
                    "runStatus": "idle",
                    "sla": 7200,
                    "slaStatus": -1,
                    "startDate": "0001-01-01T00:00:00Z",
                    "totalDuration": 0
                },
                "timetoassignment": {
                    "accumulatedPause": 0,
                    "breachTriggered": false,
                    "dueDate": "0001-01-01T00:00:00Z",
                    "endDate": "0001-01-01T00:00:00Z",
                    "lastPauseDate": "0001-01-01T00:00:00Z",
                    "runStatus": "idle",
                    "sla": 0,
                    "slaStatus": -1,
                    "startDate": "0001-01-01T00:00:00Z",
                    "totalDuration": 0
                },
                "urlsslverification": []
            },
            "ShardID": 0,
            "account": "",
            "activated": "0001-01-01T00:00:00Z",
            "allRead": false,
            "allReadWrite": false,
            "attachment": null,
            "autime": 1601389784162034000,
            "canvases": null,
            "category": "",
            "closeNotes": "",
            "closeReason": "",
            "closed": "0001-01-01T00:00:00Z",
            "closingUserId": "",
            "created": "2020-09-29T17:29:44.162034+03:00",
            "dbotCreatedBy": "admin",
            "dbotCurrentDirtyFields": null,
            "dbotDirtyFields": null,
            "dbotMirrorDirection": "",
            "dbotMirrorId": "",
            "dbotMirrorInstance": "",
            "dbotMirrorLastSync": "0001-01-01T00:00:00Z",
            "dbotMirrorTags": null,
            "details": "",
            "droppedCount": 0,
            "dueDate": "2020-10-09T17:29:44.162034+03:00",
            "feedBased": false,
            "hasRole": false,
            "id": "978",
            "investigationId": "",
            "isPlayground": false,
            "labels": [
                {
                    "type": "Instance",
                    "value": "admin"
                },
                {
                    "type": "Brand",
                    "value": "Manual"
                }
            ],
            "lastJobRunTime": "0001-01-01T00:00:00Z",
            "lastOpen": "0001-01-01T00:00:00Z",
            "linkedCount": 0,
            "linkedIncidents": null,
            "modified": "2020-09-29T17:29:44.162202+03:00",
            "name": "Incident to search",
            "notifyTime": "0001-01-01T00:00:00Z",
            "occurred": "2020-09-29T17:29:44.162034+03:00",
            "openDuration": 0,
            "owner": "admin",
            "parent": "",
            "phase": "",
            "playbookId": "",
            "previousAllRead": false,
            "previousAllReadWrite": false,
            "previousRoles": null,
            "rawCategory": "",
            "rawCloseReason": "",
            "rawJSON": "",
            "rawName": "Incident to search",
            "rawPhase": "",
            "rawType": "Unclassified",
            "reason": "",
            "reminder": "0001-01-01T00:00:00Z",
            "roles": null,
            "runStatus": "",
            "severity": 0,
            "sla": 0,
            "sortValues": [
                "_score"
            ],
            "sourceBrand": "Manual",
            "sourceInstance": "admin",
            "status": 0,
            "type": "Unclassified",
            "version": 1
        }
    ]
}

Human Readable Output

Incidents found

id name severity status owner created closed
978 Incident to search 0 0 admin 2020-09-29T17:29:44.162034+03:00 0001-01-01T00:00:00Z
import datetime as dt

import pytest
from SearchIncidentsV2 import *

data_test_check_if_found_incident = [
    ([], "failed to get incidents from xsoar.\nGot: []"),
    (None, "failed to get incidents from xsoar.\nGot: None"),
    ("", "failed to get incidents from xsoar.\nGot: "),
    ([{"Contents": {"data": None}}], False),
    ([{"Contents": {"data": "test"}}], True),
    ([{"Contents": {"test": "test"}}], "{'test': 'test'}"),
]


def create_sample_incidents(start, end, incident_type):
    return [
        {
            "id": f"{i}",
            "type": f"{incident_type}",
            "name": f"incident-{i}",
        }
        for i in range(start, end + 1)
    ]


def execute_get_incidents_command_side_effect(amount_of_mocked_incidents):
    mocked_incidents = []

    default_jump = 100
    counter = 1
    for start in range(1, amount_of_mocked_incidents + 1, default_jump):
        end = min(amount_of_mocked_incidents, default_jump * counter)

        incident_type = "A" if counter % 2 == 0 else "B"
        if counter == 1:
            execute_command_mock = [{"Contents": {"data": create_sample_incidents(start, end, incident_type), "total": 0}}]
        else:
            execute_command_mock = {"data": create_sample_incidents(start, end, incident_type)}

        mocked_incidents.append(execute_command_mock)
        counter += 1

    if mocked_incidents:
        mocked_incidents.append({"data": None})

    return mocked_incidents


@pytest.mark.parametrize("_input, expected_output", data_test_check_if_found_incident)
def test_check_if_found_incident(_input, expected_output):
    try:
        output = check_if_found_incident(_input)
    except DemistoException as error:
        output = str(error)
    assert output == expected_output, f"check_if_found_incident({_input}) returns: {output}. expected: {expected_output}"


data_test_is_valid_args = [
    ("\\", True),
    ("\n", True),
    ("\\n", True),
    ("\\t", True),
    ("\\\\", True),
    ('\\"', True),
    ("\\r", True),
    ("\\7", True),
    ("\\'", True),
]


@pytest.mark.parametrize("_input, expected_output", data_test_is_valid_args)
def test_is_valid_args(_input, expected_output):
    try:
        output = is_valid_args({"test": _input})
    except DemistoException:
        output = False

    assert output == expected_output, f"is_valid_args({_input}) returns: {output}. expected: {expected_output}"


data_test_is_id_valid = [
    (123, True),
    ("123", True),
    (123.3, False),
    ("1,2,3", True),
    ([1, 2, 3], True),
    ("[1,2,3]", True),
]


@pytest.mark.parametrize("id_value, expected_output", data_test_is_id_valid)
def test_is_incident_id_valid(id_value, expected_output):
    """
    Given:
        - an incident id

    When:
        - running the script as a playbook task

    Then:
        - validating that the incident is is a valid input from type int or str

    """
    try:
        is_valid_id = is_valid_args({"id": id_value})
    except DemistoException:
        is_valid_id = False
    assert is_valid_id == expected_output


EXAMPLE_INCIDENTS_RAW_RESPONSE = [
    {"id": "1", "type": "TypeA", "name": "Phishing", "created": "2025-01-01T10:00:00Z"},
    {"id": "2", "type": "Type-A", "name": "Phishing Campaign", "created": "2025-01-01T10:01:00Z"},
    {"id": "3", "type": "SomeType-A", "name": "Go Phish", "created": "2025-01-01T10:02:00Z"},
    {"id": "4", "type": "Another Type-A", "name": "Hello", "created": "2025-01-01T10:03:00Z"},
]

FILTER_TO_MATCHED_INCIDENTS = [
    ({"type": "Type-A"}, ["2"]),
    ({"type": "Type-A, SomeTypeA"}, ["2"]),
    ({"type": ["Type-A", "SomeType-A"]}, ["2", "3"]),
    ({"type": "Another"}, []),
    ({"name": "Phishing"}, ["1"]),
    ({"name": "Phishing,Phishing Campaign"}, ["1", "2"]),
]

INCIDENT = [
    {
        "CustomFields": {
            "hostname": "host_name",  # noqa
            "initiatedby": "initiated_by",
            "targetprocessname": "target_process_name",
            "username": "user_name",
        },
        "status": 0,
        "severity": 1,
    },
]


@pytest.mark.parametrize("args, expected_incident_ids", FILTER_TO_MATCHED_INCIDENTS)
def test_apply_filters(args, expected_incident_ids):
    incidents = apply_filters(EXAMPLE_INCIDENTS_RAW_RESPONSE, args)
    assert [incident["id"] for incident in incidents] == expected_incident_ids


def get_incidents_mock(_, args, extract_contents=True, fail_on_error=True):
    ids = args.get("id", "").split(",")
    incidents_list = [incident for incident in EXAMPLE_INCIDENTS_RAW_RESPONSE if incident["id"] in ids]
    if not extract_contents:
        return [{"Contents": {"data": incidents_list, "total": len(incidents_list)}}]
    return {"data": None}


@pytest.mark.parametrize(
    "args,filtered_args,expected_result",
    [
        # ({}, {}, []),
        ({"trimevents": "0"}, {}, []),
        ({"trimevents": "1"}, {"trimevents": "1"}, []),
        ({"id": 1}, {"id": "1", "todate": "2025-01-01T10:00:00Z"}, [EXAMPLE_INCIDENTS_RAW_RESPONSE[0]]),
        (
            {"id": [1, 2]},
            {"id": "1,2", "todate": "2025-01-01T10:00:00Z"},
            [EXAMPLE_INCIDENTS_RAW_RESPONSE[0], EXAMPLE_INCIDENTS_RAW_RESPONSE[1]],
        ),
        (
            {"id": "1,2"},
            {"id": "1,2", "todate": "2025-01-01T10:00:00Z"},
            [EXAMPLE_INCIDENTS_RAW_RESPONSE[0], EXAMPLE_INCIDENTS_RAW_RESPONSE[1]],
        ),
    ],
)
def test_filter_events(mocker, args, filtered_args, expected_result):
    """
    Given:
        - The script args.

    When:
        - Running the search_incidents function.

    Then:
        - Validating the outputs as expected.
        - Validating the filtered args that was sent to the api is as expected.
    """
    import SearchIncidentsV2

    execute_mock = mocker.patch.object(SearchIncidentsV2, "execute_command", side_effect=get_incidents_mock)
    if "trimevents" in args:
        # trimevents supported only in XSIAM
        mocker.patch.object(demisto, "demistoVersion", return_value={"platform": "xsiam"})
    else:
        mocker.patch.object(demisto, "demistoVersion", return_value={"platform": "xsoar"})
    _, res, _ = SearchIncidentsV2.search_incidents(args)
    assert res == expected_result
    assert execute_mock.call_count == 1
    assert execute_mock.call_args[0][1] == filtered_args


def get_incidents_mock_include_informational(_, args, extract_contents=True, fail_on_error=True):
    incidents = [
        {"id": "1", "informational": False, "created": "2025-01-01T09:59:00Z"},
        {"id": "2", "informational": False, "created": "2025-01-01T10:00:00Z"},
    ]

    includeinformational = args.get("includeinformational", None)

    if includeinformational:
        incidents.extend(
            [
                {"id": "3", "informational": True, "created": "2025-01-01T10:01:00Z"},
                {"id": "4", "informational": True, "created": "2025-01-01T10:02:00Z"},
            ]
        )

    if not extract_contents:
        return [{"Contents": {"data": incidents, "total": len(incidents)}}]

    return {"Contents": {"data": incidents}}


INCLUDE_INFORMATIONAL_FIXED_TIME = dt.datetime(2024, 10, 1, 15, 0, 0)
INCLUDE_INFORMATIONAL_NOW = INCLUDE_INFORMATIONAL_FIXED_TIME.isoformat()
INCLUDE_INFORMATIONAL_5_HOURS_AGO = dt.datetime(2024, 10, 1, 10, 0, 0).isoformat()
INCLUDE_INFORMATIONAL_3_HOURS_AGO = dt.datetime(2024, 10, 1, 12, 0, 0).isoformat()


@pytest.mark.parametrize(
    "args,expected_filtered_args,expected_result",
    [
        (
            {"includeinformational": "true", "fromdate": "3 hours ago", "todate": "now"},
            {"includeinformational": True, "fromdate": INCLUDE_INFORMATIONAL_3_HOURS_AGO, "todate": INCLUDE_INFORMATIONAL_NOW},
            ["1", "2", "3", "4"],
        ),
        (
            {"includeinformational": "true", "fromdate": "6 hours ago", "todate": "now"},
            {"includeinformational": True, "fromdate": INCLUDE_INFORMATIONAL_5_HOURS_AGO, "todate": INCLUDE_INFORMATIONAL_NOW},
            ["1", "2", "3", "4"],
        ),
        ({"includeinformational": "true"}, {}, ValueError),
        ({"includeinformational": "false"}, {"todate": "2025-01-01T09:59:00Z"}, ["1", "2"]),
        ({}, {"todate": "2025-01-01T09:59:00Z"}, ["1", "2"]),
        ({"includeinformational": "true", "todate": "now"}, {}, ValueError),
        ({"includeinformational": "true", "fromdate": "3 hours ago"}, {}, ValueError),
    ],
)
def test_includeinformational_logic(mocker, args, expected_filtered_args, expected_result):
    """
    Given:
    - Case A: includeinformational=True, with relative fromdate and todate provided, both dates within 5 hours from
    current time.
    - Case B: includeinformational=true, fromdate is more than 5 hours ago, requiring an adjustment of fromdate.
    - Case C: includeinformational=true, but fromdate and todate are missing, should raise a ValueError.
    - Case D: includeinformational=false, meaning no informational incidents should be returned.
    - Case E: includeinformational not present, should behave like false and no informational incidents should be returned.
    - Case F: includeinformational=true,but fromdate is missing, should raise a ValueError.
    - Case G: includeinformational=true, but todate is missing, should raise a ValueError.

    When:
       - Running the search_incidents function to process the input args.

    Then:
       - Case A: Ensure both regular and informational incidents are returned when includeinformational=true.
       - Case B: Ensure fromdate is adjusted to 5 hours ago and incidents (regular + informational) are fetched correctly.
       - Case C: Verify that a ValueError is raised due to missing fromdate and todate when includeinformational=true.
       - Case D: Ensure only regular incidents are returned when includeinformational=false.
       - Case E: Ensure only regular incidents are returned when includeinformational=None (default behavior).
       - Case F: Ensure that a ValueError is raised when fromdate is missing but includeinformational=true.
       - Case G: Ensure that a ValueError is raised when todate is missing but includeinformational=true.
    """
    import SearchIncidentsV2

    mocker.patch.object(dt, "datetime", autospec=True)
    mocker.patch.object(SearchIncidentsV2, "is_xsiam", return_value=True)
    dt.datetime.utcnow.return_value = INCLUDE_INFORMATIONAL_FIXED_TIME

    class MockDateTime:
        def utcnow(self):
            return INCLUDE_INFORMATIONAL_FIXED_TIME

    mocker.patch("SearchIncidentsV2.datetime", MockDateTime())

    # mock arg to datetime since internally uses utc which is not the same as the fixed datetime
    mocker.patch.object(
        SearchIncidentsV2,
        "arg_to_datetime",
        side_effect=lambda x: None
        if x is None
        else INCLUDE_INFORMATIONAL_FIXED_TIME - dt.timedelta(hours=int(x.split()[0]))
        if "hours ago" in x
        else INCLUDE_INFORMATIONAL_FIXED_TIME,
    )

    execute_mock = mocker.patch.object(SearchIncidentsV2, "execute_command", side_effect=get_incidents_mock_include_informational)

    if expected_result is ValueError:
        with pytest.raises(ValueError):
            SearchIncidentsV2.search_incidents(args)
    else:
        _, res, _ = SearchIncidentsV2.search_incidents(args)
        assert [incident["id"] for incident in res] == expected_result
        assert execute_mock.call_count == 1
        assert execute_mock.call_args[0][1] == expected_filtered_args


@pytest.mark.parametrize(
    "platform, version, link_type, expected_result",
    [
        ("x2", "", "alertLink", "alerts?action:openAlertDetails="),
        ("platform", "", "alertLink", "alerts?action:openAlertDetails="),
        ("xsoar", "6.10.0", "incidentLink", "#/Details/"),
        ("xsoar", "8.4.0", "incidentLink", "/Details/"),
    ],
)
def test_add_incidents_link(mocker, platform, version, link_type, expected_result):
    mocker.patch.object(demisto, "getLicenseCustomField", return_value="")
    mocker.patch.object(demisto, "demistoUrls", return_value={"server": ""})
    if version:
        mocker.patch.object(demisto, "demistoVersion", return_value={"version": version})
    data = add_incidents_link(EXAMPLE_INCIDENTS_RAW_RESPONSE, platform)
    assert expected_result in data[0][link_type]


def test_transform_to_alert_data():
    incident = transform_to_alert_data(INCIDENT)[0]
    assert incident["hostname"] == "host_name"
    assert incident["status"] == "PENDING"
    assert incident["severity"] == "LOW"


def test_summarize_incidents():
    assert summarize_incidents(
        {"add_fields_to_summarize_context": "test"}, [{"id": "test", "CustomFields": {}}], platform="xsoar"
    ) == [
        {
            "closed": "n/a",
            "created": "n/a",
            "id": "test",
            "incidentLink": "n/a",
            "name": "n/a",
            "owner": "n/a",
            "severity": "n/a",
            "status": "n/a",
            "test": "n/a",
            "type": "n/a",
        }
    ]


@pytest.mark.parametrize(
    "amount_of_mocked_incidents, args, expected_incidents_length",
    [
        (306, {}, 100),
        (306, {"limit": "200"}, 200),
        (105, {"limit": "200"}, 105),
        (1000, {"limit": "100"}, 100),
        (1000, {"limit": "1100"}, 1000),
        (205, {"limit": "105.5"}, 105),
        (700, {"limit": "500", "type": "A"}, 300),
        (1500, {"limit": "250", "type": "A"}, 250),
        (500, {"limit": "100", "name": "incident-8"}, 1),
    ],
)
def test_main_flow_with_limit(mocker, amount_of_mocked_incidents, args, expected_incidents_length):
    """
    Given:
       - Case A: Total of 306 incidents matching in XSOAR and no args
       - Case B: Total of 306 incidents matching in XSOAR and limit = 200
       - Case C: Total of 105 incidents matching in XSOAR and limit = 200
       - Case D: Total of 1000 incidents matching in XSOAR and limit = 100
       - Case E: Total of 1000 incidents matching in XSOAR and limit = 1100
       - Case F: Total of 205 incidents matching in XSOAR and limit = 105.5
       - Case G: Total of 700 incidents and only 300 incidents which match type = 'A' and limit = 500
       - Case H: Total of 1500 incidents and only 700 incidents which match type = 'A' and limit = 250
       - Case I: Total of 500 incidents and only 1 incident that its name = 'incident-8' and limit = 100

    When:
       - Running the main flow

    Then:
       - Case A: Make sure only 100 incidents have been returned (default of the limit if not stated)
       - Case B: Make sure only 200 incidents have been returned.
       - Case C: Make sure only 105 incidents have been returned (cause there are fewer incidents than requested limit)
       - Case D: Make sure only 100 incidents have been returned.
       - Case E: Make sure only 1000 incidents have been returned.
       - Case F: Make sure only 105 (rounded) incidents have been returned.
       - Case G: Make sure only 300 incidents have been returned.
       - Case H: Make sure only 250 incidents have been returned.
       - Case I: Make sure only one incident has been returned.

    """
    import SearchIncidentsV2

    mocker.patch.object(
        SearchIncidentsV2, "execute_command", side_effect=execute_get_incidents_command_side_effect(amount_of_mocked_incidents)
    )

    mocker.patch.object(demisto, "args", return_value=args)
    return_results_mocker = mocker.patch.object(SearchIncidentsV2, "return_results")
    mocker.patch("SearchIncidentsV2.get_demisto_version", return_value={})

    SearchIncidentsV2.main()

    assert return_results_mocker.called
    assert len(return_results_mocker.call_args[0][0].outputs) == expected_incidents_length


def test_query_argument_with_unicode_escape(mocker):
    """
    Given:
       - A query to search incidents with unicode escape

    When:
       - Executing the SearchIncidentsV2 command and check arg validation.

    Then:
       - Make sure the query format is correct and is_valid_args method is not failed.
    """
    import SearchIncidentsV2

    special_chars = ["\n", "\t", "\\", '"', "'", "\7", "\r", "\\x", "\\X", "\\N", "\\u", "\\U"]
    args_array = [
        {"query": f"`(username:'user{special_char}sername') and (name:'name_1' or name:'name_2')`"}
        for special_char in special_chars
    ]
    mocker.patch.object(demisto, "args", side_effect=args_array)
    mocker.patch.object(SearchIncidentsV2, "return_results")
    mocker.patch("SearchIncidentsV2.get_demisto_version", return_value={})
    for _ in special_chars:
        mocker.patch.object(SearchIncidentsV2, "execute_command", side_effect=execute_get_incidents_command_side_effect(1))
        SearchIncidentsV2.main()


def test_todate_set_and_pagination(mocker):
    """
    Given: Duplicated incidents from executing getIncidents command to the platform in 2 different requests in a row.
    When: Running the command with limit that is larger than the page size.
    Then: Validate that the command return incident list without duplications by changing the todate to
     be the first incident time from the first run.
    """
    import SearchIncidentsV2

    # Page 1 returns exactly page_size incidents
    page1_incidents = [{"created": f"2025-01-01T10:0{i}:00Z", "id": i} for i in range(5)]
    # Page 2 returns fewer, triggering end-of-pagination
    page2_incidents = [{"created": "2025-01-01T09:59:00Z", "id": 101}]

    # Mock execute_command behavior
    execute_command_mocker = mocker.patch.object(
        SearchIncidentsV2,
        "execute_command",
        side_effect=[
            # 1st call: initial getIncidents
            [{"Contents": {"data": page1_incidents}}],
            # 2nd call: getIncidents inside loop (page2)
            {"data": page2_incidents},
        ],
    )

    args = {"limit": 10, "size": 5}
    SearchIncidentsV2.search_incidents(args=args)

    # After first page, todate should be set to the very first created timestamp
    expected_todate = page1_incidents[0]["created"]

    # Confirm that execute_command was called a second time with args including todate
    _, second_call_kwargs = execute_command_mocker.call_args_list
    assert second_call_kwargs[0][1]["todate"] == expected_todate