GetFailedTasks

Gets failed tasks details for incidents based on a query. Limited to 1000 incidents. 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

python · Integrations & Incidents Health Check

Details

IDGetFailedTasks
Languagepython
From Version6.0.0
Docker Imagedemisto/python3:3.12.13.10116658

README

Gets failed tasks details for incidents based on a query. Limited to 1000 incidents

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: 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.

Script Data


Name Description
Script Type python3
Tags  
Cortex XSOAR Version 6.0.0

Used In


This script is used in the following playbooks and scripts.

  • Integrations and Playbooks Health Check - Running Scripts

Inputs


Argument Name Description
query The query by which to retrieve failed tasks. Optional. The default value is “-status:closed”
rest_api_instance The Rest API instance to use.
max_incidents Maximum number of incidents to query. Maximum is 1000.

Outputs


There are no outputs for this script.

Troubleshooting

Multi-tenant environments should be configured with the Cortex Rest API instance when using this
automation. Make sure the Use tenant parameter (in the Cortex Rest API integration) is checked
to ensure that API calls are made to the current tenant instead of the master tenant.

from pytest_mock import MockerFixture
import demistomock as demisto
import pytest
import json
from GetFailedTasks import (
    main,
    get_failed_tasks_output,
    get_incident_tasks_using_internal_request,
    get_incident_data,
    get_custom_scripts_map_id_and_name,
    get_rest_api_instance,
    filter_playbooks_failures,
)
from test_data.constants import INCIDENTS_RESULT, RESTAPI_TAS_RESULT, INTERNAL_TASKS_RESULT


def mock_execute_command(command_name, _):
    if command_name == "getIncidents":
        return INCIDENTS_RESULT
    elif command_name == "core-api-post":
        return RESTAPI_TAS_RESULT
    return None


@pytest.mark.parametrize("rest_api_instacne", ["rest_api_instacne", None])
def test_get_failed_tasks(mocker, rest_api_instacne):
    mocker.patch.object(demisto, "args", return_value={"rest_api_instance": rest_api_instacne})
    mocker.patch.object(demisto, "executeCommand", side_effect=mock_execute_command)
    mocker.patch.object(demisto, "internalHttpRequest", return_value=INTERNAL_TASKS_RESULT)
    mocker.patch.object(demisto, "results")
    mocker.patch("GetFailedTasks.get_tenant_name", return_value="test")

    main()

    human_readable = demisto.results.call_args[0][0].get("HumanReadable")
    assert (
        "|Incident Created Date|Incident ID|Task Name|Task ID|Playbook Name|Command Name|"
        "Brand Name|Error Entry ID|Error Handling|"
    ) in human_readable
    assert human_readable.count("AutoFocusPolling") == 3

    entry_context = demisto.results.call_args[0][0].get("EntryContext")
    assert entry_context.get("NumberofFailedIncidents", [])[0].get("Number of total errors") == 6
    assert entry_context.get("NumberofFailedIncidents", [])[0].get("total of failed incidents") == 3

    assert entry_context.get("GetFailedTasks", [])[1] == {
        "Command Name": "",
        "Brand Name": None,
        "Error Entry ID": ["8@3", "9@3"],
        "Incident Created Date": "2020-09-29T14:02:45.82647067Z",
        "Incident ID": "3",
        "Incident Owner": "admin",
        "Number of Errors": 2,
        "Playbook Name": "AutoFocusPolling",
        "Task ID": "3",
        "Task Name": "Fail",
        "Command Description": "command desc",
        "Error Handling": "Error Path",
    }


@pytest.mark.parametrize(
    "tasks,expected_outputs",
    [
        ([], ([], 0)),
        (
            json.loads(INTERNAL_TASKS_RESULT.get("body")),
            (
                [
                    {
                        "Command Name": "",
                        "Brand Name": None,
                        "Error Entry ID": ["8@3", "9@3"],
                        "Incident Created Date": "2020-09-29T14:02:45.82647067Z",
                        "Incident ID": "3",
                        "Incident Owner": "admin",
                        "Number of Errors": 2,
                        "Playbook Name": "AutoFocusPolling",
                        "Task ID": "3",
                        "Task Name": "Fail",
                        "Command Description": "command desc",
                        "Error Handling": "Error Path",
                    }
                ],
                2,
            ),
        ),
    ],
)
def test_get_failed_tasks_output(tasks, expected_outputs):
    """
    Given:
        - A list of tasks

    When:
        - Running the get_failed_tasks_output function

    Then:
        - Validates that the response is as expected.
    """
    tasks_res, num_tasks = get_failed_tasks_output(tasks, INCIDENTS_RESULT[0].get("Contents").get("data")[1])

    assert tasks_res == expected_outputs[0]
    assert num_tasks == expected_outputs[1]


def test_get_failed_tasks_output_with_tasks(mocker):
    """
    Given: A list of failing tasks and an incident object.
    When: Calling get_failed_tasks_output function.
    Then: It should return the expected task outputs and the correct number of error entries.
    """
    # Mock data
    tasks = [
        {
            "entries": ["error1", "error2"],
            "task": {"scriptId": "brand1|||script1", "name": "Task 1", "description": "Description 1"},
            "ancestors": ["Playbook 1"],
            "id": "task1",
        },
        {"entries": ["error3"], "task": {"scriptId": "script2", "name": "Task 2"}, "ancestors": ["Playbook 2"], "id": "task2"},
    ]
    incident = {"id": "incident1", "created": "2023-01-01", "owner": "admin"}
    custom_scripts_map = {"script1": "Custom Script 1", "script2": "Custom Script 2"}

    # Expected output
    expected_outputs = [
        {
            "Incident ID": "incident1",
            "Playbook Name": "Playbook 1",
            "Task Name": "Task 1",
            "Error Entry ID": ["error1", "error2"],
            "Number of Errors": 2,
            "Task ID": "task1",
            "Incident Created Date": "2023-01-01",
            "Command Name": "Custom Script 1",
            "Brand Name": "brand1",
            "Incident Owner": "admin",
            "Command Description": "Description 1",
            "Error Handling": "Stop Playbook",
        },
        {
            "Incident ID": "incident1",
            "Playbook Name": "Playbook 2",
            "Task Name": "Task 2",
            "Error Entry ID": ["error3"],
            "Number of Errors": 1,
            "Task ID": "task2",
            "Incident Created Date": "2023-01-01",
            "Command Name": "Custom Script 2",
            "Brand Name": None,
            "Incident Owner": "admin",
            "Error Handling": "Stop Playbook",
        },
    ]

    # Call the function
    result, total_errors = get_failed_tasks_output(tasks, incident, custom_scripts_map)

    # Assert the results
    assert result == expected_outputs
    assert total_errors == 3


@pytest.mark.parametrize(
    "response,expected_result", [(INTERNAL_TASKS_RESULT, json.loads(INTERNAL_TASKS_RESULT.get("body"))), ({}, [])]
)
def test_get_incident_tasks_using_internal_request(mocker, response, expected_result):
    """
    Given:
        - A response of an internal request to get failed tasks of an incident.

    When:
        - Running the get_incident_tasks_using_internal_request function

    Then:
        - Validates that the returned tasks is as expected.
    """
    mocker.patch.object(demisto, "internalHttpRequest", return_value=response)
    mocker.patch.object(demisto, "error")

    result = get_incident_tasks_using_internal_request(RESTAPI_TAS_RESULT[0]["Contents"]["response"][0])
    assert result == expected_result


def test_get_incident_data_using_rest_api_instance(mocker):
    """
    Given:
        - An incident and an Core REST API instance.

    When:
        - Running the get_incident_data function.

    Then:
        - Validates that get_incident_tasks_using_rest_api_instance was called.
    """
    mock_res = mocker.patch("GetFailedTasks.get_incident_tasks_using_rest_api_instance", return_value=[])
    mocker.patch("GetFailedTasks.get_tenant_name", return_value="test")
    result = get_incident_data(INCIDENTS_RESULT[0].get("Contents").get("data")[1], {}, "instance_mock")
    assert mock_res.call_count == 1
    assert result[0] == []


def test_get_incident_data_using_internal_http_request(mocker):
    """
    Given:
        - An incident and no Core REST API instance.

    When:
        - Running the get_incident_data function.

    Then:
        - Validates that get_incident_tasks_using_internal_request was called.
    """
    mock_res = mocker.patch("GetFailedTasks.get_incident_tasks_using_internal_request", return_value=[])
    result = get_incident_data(INCIDENTS_RESULT[0].get("Contents").get("data")[1], {}, None)
    assert mock_res.call_count == 1
    assert result[0] == []


def test_get_rest_api_instance_with_provided_instance(mocker):
    """
    Given:
        A REST API instance is provided as an argument.
    When:
        The get_rest_api_instance function is called.
    Then:
        It should return the provided REST API instance and log a debug message.
    """
    mock_debug = mocker.patch("GetFailedTasks.demisto.debug")

    result = get_rest_api_instance("test_instance")

    assert result == "test_instance"
    mock_debug.assert_called_once_with("Using REST API instance: test_instance, that provided as an argument.")


def test_get_rest_api_instance_without_provided_instance_using_internal_request(mocker):
    """
    Given:
        No REST API instance is provided and the internal HTTP request succeeds.
    When:
        The get_rest_api_instance function is called.
    Then:
        It should return None and log a debug message about using internal HTTP request.
    """
    mocker.patch("GetFailedTasks.get_incident_tasks_using_internal_request")
    mock_debug = mocker.patch("GetFailedTasks.demisto.debug")

    result = get_rest_api_instance(None)

    assert result is None
    mock_debug.assert_called_once_with("Using internal HTTP request to retrieve incident tasks.")


def test_get_rest_api_instance_without_provided_instance_using_rest_api(mocker):
    """
    Given:
        No REST API instance is provided and the internal HTTP request fails.
    When:
        The get_rest_api_instance function is called.
    Then:
        It should return a REST API instance from get_rest_api_instance_to_use and log appropriate debug messages.
    """
    mocker.patch("GetFailedTasks.get_incident_tasks_using_internal_request", side_effect=ValueError)
    mocker.patch("GetFailedTasks.get_rest_api_instance_to_use", return_value="auto_instance")
    mock_debug = mocker.patch("GetFailedTasks.demisto.debug")

    result = get_rest_api_instance(None)

    assert result == "auto_instance"
    mock_debug.assert_called_with("Using REST API instance: auto_instance to retrieve incident tasks.")


def test_get_custom_scripts_map_id_and_name_with_rest_api(mocker: MockerFixture):
    """
    Given:
        A REST API instance is provided.
    When:
        The get_custom_scripts_map_id_and_name function is called.
    Then:
        It should use the core-api-post command and return the correct script map.
    """
    mock_execute_command = mocker.patch.object(demisto, "executeCommand")
    mock_execute_command.return_value = [
        {
            "Contents": {
                "response": {"scripts": [{"id": "script1", "name": "Script One"}, {"id": "script2", "name": "Script Two"}]}
            },
            "Type": 1,
        }
    ]

    result = get_custom_scripts_map_id_and_name("rest_api_instance")

    assert result == {"script1": "Script One", "script2": "Script Two"}
    mock_execute_command.assert_called_once_with(
        "core-api-post", {"uri": "automation/search", "body": {"query": "system:F"}, "using": "rest_api_instance"}
    )


def test_get_custom_scripts_map_id_and_name_without_rest_api(mocker):
    """
    Given:
        No REST API instance is provided.
    When:
        The get_custom_scripts_map_id_and_name function is called.
    Then:
        It should use the internalHttpRequest and return the correct script map.
    """
    mock_internal_request = mocker.patch("GetFailedTasks.demisto.internalHttpRequest")
    mock_internal_request.return_value = {
        "statusCode": 200,
        "body": json.dumps({"scripts": [{"id": "script3", "name": "Script Three"}, {"id": "script4", "name": "Script Four"}]}),
    }

    result = get_custom_scripts_map_id_and_name()

    assert result == {"script3": "Script Three", "script4": "Script Four"}
    mock_internal_request.assert_called_once_with(method="POST", uri="automation/search", body={"query": "system:F"})


def test_filter_playbooks_failures():
    """
    Given:
        - A list of tasks, some of which are of type "playbook" and appear in the ancestors of other tasks.

    When:
        - Running the filter_playbooks_failures function.

    Then:
        - Ensure that tasks of type "playbook" whose names appear in the ancestors of other tasks are removed.
        - Ensure that tasks not matching this condition remain in the output.
    """
    tasks = [
        {"id": "1", "type": "playbook", "task": {"name": "Playbook1"}, "ancestors": []},
        {"id": "2", "type": "regular", "task": {"name": "Task1"}, "ancestors": ["Playbook1"]},
        {"id": "3", "type": "playbook", "task": {"name": "Playbook2"}, "ancestors": ["Playbook1"]},
    ]

    # Expected output: Playbook1 should be removed because it is in the ancestors of Task1.
    expected_filtered_tasks = [
        {"id": "2", "type": "regular", "task": {"name": "Task1"}, "ancestors": ["Playbook1"]},
        {"id": "3", "type": "playbook", "task": {"name": "Playbook2"}, "ancestors": ["Playbook1"]},
    ]

    filtered_tasks = filter_playbooks_failures(tasks)
    assert filtered_tasks == expected_filtered_tasks