VectraRUXSyncDetectionSummary

Sync the detection summary from the Vectra platform.

python · Vectra RUX

Details

IDVectraRUXSyncDetectionSummary
Languagepython
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Tagsincident-action-button

README

Sync the detection summary from the Vectra platform.

Script Data


Name Description
Script Type python3
Tags incident-action-button
Cortex XSOAR Version 6.10.0

Inputs


There are no inputs for this script.

Outputs


There are no outputs for this script.

from unittest.mock import patch
from VectraRUXSyncDetectionSummary import handle_error, main, MIRRORING_TAG


class TestHandleError:
    """Test cases for handle_error function."""

    @patch("VectraRUXSyncDetectionSummary.isError")
    @patch("VectraRUXSyncDetectionSummary.return_error")
    def test_handle_error_with_error(self, mock_return_error, mock_is_error):
        """Test handle_error when command results contain an error."""
        mock_is_error.return_value = True
        command_results = [{"Contents": "Error message"}]

        handle_error(command_results)

        mock_is_error.assert_called_once_with(command_results)
        mock_return_error.assert_called_once_with("Error message")

    @patch("VectraRUXSyncDetectionSummary.isError")
    @patch("VectraRUXSyncDetectionSummary.return_error")
    def test_handle_error_without_error(self, mock_return_error, mock_is_error):
        """Test handle_error when command results do not contain an error."""
        mock_is_error.return_value = False
        command_results = [{"Contents": "Success"}]

        handle_error(command_results)

        mock_is_error.assert_called_once_with(command_results)
        mock_return_error.assert_not_called()


class TestMain:
    """Test cases for main function."""

    @patch("VectraRUXSyncDetectionSummary.return_results")
    @patch("VectraRUXSyncDetectionSummary.demisto")
    @patch("VectraRUXSyncDetectionSummary.handle_error")
    @patch("VectraRUXSyncDetectionSummary.CommandResults")
    def test_main_tag_not_present_adds_tag(self, mock_command_results, mock_handle_error, mock_demisto, mock_return_results):
        """Test that tag is added (without remove) when Trigger_XSOAR_Mirroring is not present."""
        detection_id = "123"
        summary_data = {"description": "Test detection", "severity": "high"}

        mock_demisto.incident.return_value = {"CustomFields": {"vectraruxdetectionid": detection_id}}
        mock_demisto.mapObject.return_value = {}
        mock_demisto.executeCommand.side_effect = [
            [{"Contents": {"results": [{"summary": summary_data, "process_context_data": {}}]}}],  # vectra-detection-describe
            [{"Contents": {"tags": ["some_other_tag"]}}],  # vectra-detection-tag-list
            [{"Contents": "Tag added"}],  # vectra-detection-tag-add
            [{"Contents": "Success"}],  # setIncident
        ]
        mock_handle_error.return_value = None

        main()

        mock_demisto.executeCommand.assert_any_call(
            "vectra-detection-tag-add", {"detection_id": detection_id, "tags": MIRRORING_TAG}
        )
        remove_calls = [c for c in mock_demisto.executeCommand.call_args_list if c[0][0] == "vectra-detection-tag-remove"]
        assert len(remove_calls) == 0
        assert mock_demisto.executeCommand.call_count == 4

    @patch("VectraRUXSyncDetectionSummary.return_results")
    @patch("VectraRUXSyncDetectionSummary.demisto")
    @patch("VectraRUXSyncDetectionSummary.handle_error")
    @patch("VectraRUXSyncDetectionSummary.CommandResults")
    def test_main_tag_present_removes_and_readds_tag(
        self, mock_command_results, mock_handle_error, mock_demisto, mock_return_results
    ):
        """Test that tag is removed then re-added when Trigger_XSOAR_Mirroring is already present."""
        detection_id = "123"
        summary_data = {"description": "Test detection", "severity": "high"}

        mock_demisto.incident.return_value = {"CustomFields": {"vectraruxdetectionid": detection_id}}
        mock_demisto.mapObject.return_value = {}
        mock_demisto.executeCommand.side_effect = [
            [{"Contents": {"results": [{"summary": summary_data, "process_context_data": {}}]}}],  # vectra-detection-describe
            [{"Contents": {"tags": [MIRRORING_TAG, "other_tag"]}}],  # vectra-detection-tag-list
            [{"Contents": "Tag removed"}],  # vectra-detection-tag-remove
            [{"Contents": "Tag added"}],  # vectra-detection-tag-add
            [{"Contents": "Success"}],  # setIncident
        ]
        mock_handle_error.return_value = None

        main()

        mock_demisto.executeCommand.assert_any_call(
            "vectra-detection-tag-remove", {"detection_id": detection_id, "tags": MIRRORING_TAG}
        )
        mock_demisto.executeCommand.assert_any_call(
            "vectra-detection-tag-add", {"detection_id": detection_id, "tags": MIRRORING_TAG}
        )
        assert mock_demisto.executeCommand.call_count == 5

    @patch("VectraRUXSyncDetectionSummary.return_results")
    @patch("VectraRUXSyncDetectionSummary.demisto")
    @patch("VectraRUXSyncDetectionSummary.handle_error")
    @patch("VectraRUXSyncDetectionSummary.CommandResults")
    def test_main_tag_present_remove_called_before_add(
        self, mock_command_results, mock_handle_error, mock_demisto, mock_return_results
    ):
        """Test that remove is called before add when tag is already present."""
        detection_id = "123"

        mock_demisto.incident.return_value = {"CustomFields": {"vectraruxdetectionid": detection_id}}
        mock_demisto.mapObject.return_value = {}
        mock_demisto.executeCommand.side_effect = [
            [{"Contents": {"results": [{"summary": {}, "process_context_data": {}}]}}],
            [{"Contents": {"tags": [MIRRORING_TAG]}}],
            [{"Contents": "Tag removed"}],
            [{"Contents": "Tag added"}],
            [{"Contents": "Success"}],
        ]
        mock_handle_error.return_value = None

        main()

        command_names = [c[0][0] for c in mock_demisto.executeCommand.call_args_list]
        remove_idx = command_names.index("vectra-detection-tag-remove")
        add_idx = command_names.index("vectra-detection-tag-add")
        assert remove_idx < add_idx

    @patch("VectraRUXSyncDetectionSummary.return_results")
    @patch("VectraRUXSyncDetectionSummary.demisto")
    @patch("VectraRUXSyncDetectionSummary.handle_error")
    @patch("VectraRUXSyncDetectionSummary.CommandResults")
    def test_main_empty_tag_list_adds_tag(self, mock_command_results, mock_handle_error, mock_demisto, mock_return_results):
        """Test that tag is added when detection has no existing tags."""
        detection_id = "456"

        mock_demisto.incident.return_value = {"CustomFields": {"vectraruxdetectionid": detection_id}}
        mock_demisto.mapObject.return_value = {}
        mock_demisto.executeCommand.side_effect = [
            [{"Contents": {"results": []}}],
            [{"Contents": {"tags": []}}],
            [{"Contents": "Tag added"}],
            [{"Contents": "Success"}],
        ]
        mock_handle_error.return_value = None

        main()

        mock_demisto.executeCommand.assert_any_call(
            "vectra-detection-tag-add", {"detection_id": detection_id, "tags": MIRRORING_TAG}
        )
        remove_calls = [c for c in mock_demisto.executeCommand.call_args_list if c[0][0] == "vectra-detection-tag-remove"]
        assert len(remove_calls) == 0

    @patch("VectraRUXSyncDetectionSummary.return_results")
    @patch("VectraRUXSyncDetectionSummary.demisto")
    @patch("VectraRUXSyncDetectionSummary.handle_error")
    @patch("VectraRUXSyncDetectionSummary.CommandResults")
    def test_main_success_with_detection_summary(
        self, mock_command_results, mock_handle_error, mock_demisto, mock_return_results
    ):
        """Test main function successfully retrieves and sets detection summary."""
        detection_id = "123"
        summary_data = {"description": "Test detection", "severity": "high"}

        mock_demisto.incident.return_value = {"CustomFields": {"vectraruxdetectionid": detection_id}}
        mock_demisto.mapObject.return_value = {}
        mock_demisto.executeCommand.side_effect = [
            [{"Contents": {"results": [{"summary": summary_data, "process_context_data": {}}]}}],
            [{"Contents": {"tags": []}}],
            [{"Contents": "Tag added"}],
            [{"Contents": "Success"}],
        ]
        mock_handle_error.return_value = None

        main()

        mock_demisto.incident.assert_called_once()
        mock_demisto.executeCommand.assert_any_call("vectra-detection-describe", {"detection_ids": detection_id})
        mock_demisto.executeCommand.assert_any_call("vectra-detection-tag-list", {"detection_id": detection_id})
        mock_demisto.executeCommand.assert_any_call(
            "vectra-detection-tag-add", {"detection_id": detection_id, "tags": MIRRORING_TAG}
        )
        mock_demisto.executeCommand.assert_any_call("setIncident", {"details": summary_data})
        mock_command_results.assert_called_once_with(
            readable_output="Detection has been synchronized successfully with EDR process context and summary."
        )

    @patch("VectraRUXSyncDetectionSummary.return_results")
    @patch("VectraRUXSyncDetectionSummary.demisto")
    @patch("VectraRUXSyncDetectionSummary.handle_error")
    @patch("VectraRUXSyncDetectionSummary.CommandResults")
    def test_main_success_with_empty_result(self, mock_command_results, mock_handle_error, mock_demisto, mock_return_results):
        """Test main function when detection result is empty."""
        detection_id = "456"

        mock_demisto.incident.return_value = {"CustomFields": {"vectraruxdetectionid": detection_id}}
        mock_demisto.mapObject.return_value = {}
        mock_demisto.executeCommand.side_effect = [
            [{"Contents": {"results": []}}],
            [{"Contents": {"tags": []}}],
            [{"Contents": "Tag added"}],
            [{"Contents": "Success"}],
        ]
        mock_handle_error.return_value = None

        main()

        mock_demisto.executeCommand.assert_any_call("setIncident", {"details": {}})
        mock_command_results.assert_called_once_with(
            readable_output="Detection has been synchronized successfully with EDR process context and summary."
        )

    @patch("VectraRUXSyncDetectionSummary.return_results")
    @patch("VectraRUXSyncDetectionSummary.demisto")
    @patch("VectraRUXSyncDetectionSummary.handle_error")
    @patch("VectraRUXSyncDetectionSummary.CommandResults")
    def test_main_success_with_non_dict_contents(
        self, mock_command_results, mock_handle_error, mock_demisto, mock_return_results
    ):
        """Test main function when vectra-detection-describe returns non-dict contents."""
        detection_id = "789"

        mock_demisto.incident.return_value = {"CustomFields": {"vectraruxdetectionid": detection_id}}
        mock_demisto.mapObject.return_value = {}
        mock_demisto.executeCommand.side_effect = [
            [{"Contents": "error string"}],
            [{"Contents": {"tags": []}}],
            [{"Contents": "Tag added"}],
            [{"Contents": "Success"}],
        ]
        mock_handle_error.return_value = None

        main()

        mock_demisto.executeCommand.assert_any_call("setIncident", {"details": {}})
        mock_command_results.assert_called_once_with(
            readable_output="Detection has been synchronized successfully with EDR process context and summary."
        )

    @patch("VectraRUXSyncDetectionSummary.return_results")
    @patch("VectraRUXSyncDetectionSummary.demisto")
    @patch("VectraRUXSyncDetectionSummary.handle_error")
    @patch("VectraRUXSyncDetectionSummary.CommandResults")
    def test_main_success_with_detection_without_summary(
        self, mock_command_results, mock_handle_error, mock_demisto, mock_return_results
    ):
        """Test main function when detection does not have a summary field."""
        detection_id = "101"

        mock_demisto.incident.return_value = {"CustomFields": {"vectraruxdetectionid": detection_id}}
        mock_demisto.mapObject.return_value = {}
        mock_demisto.executeCommand.side_effect = [
            [{"Contents": {"results": [{"id": "101", "name": "Test Detection"}]}}],
            [{"Contents": {"tags": []}}],
            [{"Contents": "Tag added"}],
            [{"Contents": "Success"}],
        ]
        mock_handle_error.return_value = None

        main()

        mock_demisto.executeCommand.assert_any_call("setIncident", {"details": {}})
        mock_command_results.assert_called_once_with(
            readable_output="Detection has been synchronized successfully with EDR process context and summary."
        )

    @patch("VectraRUXSyncDetectionSummary.demisto")
    @patch("VectraRUXSyncDetectionSummary.handle_error")
    @patch("VectraRUXSyncDetectionSummary.return_error")
    def test_main_handles_command_error(self, mock_return_error, mock_handle_error, mock_demisto):
        """Test main function handles errors raised by handle_error during command execution."""
        detection_id = "999"

        mock_demisto.incident.return_value = {"CustomFields": {"vectraruxdetectionid": detection_id}}
        mock_demisto.executeCommand.return_value = [{"Type": 4, "Contents": "Command execution failed"}]
        mock_handle_error.side_effect = Exception("Command error")

        main()

        mock_return_error.assert_called_once()
        assert "Failed to execute VectraRUXSyncDetectionSummary" in str(mock_return_error.call_args)

    @patch("VectraRUXSyncDetectionSummary.demisto")
    @patch("VectraRUXSyncDetectionSummary.return_error")
    def test_main_handles_missing_detection_id(self, mock_return_error, mock_demisto):
        """Test main function handles missing detection ID in custom fields."""
        mock_demisto.incident.return_value = {"CustomFields": {}}
        mock_demisto.mapObject.return_value = {}
        mock_demisto.executeCommand.side_effect = [
            [{"Contents": {"results": []}}],
            [{"Contents": {"tags": []}}],
            [{"Contents": "Tag added"}],
            [{"Contents": "Success"}],
        ]

        main()

        assert mock_demisto.executeCommand.call_count >= 1

    @patch("VectraRUXSyncDetectionSummary.demisto")
    @patch("VectraRUXSyncDetectionSummary.return_error")
    @patch("VectraRUXSyncDetectionSummary.traceback")
    def test_main_handles_exception(self, mock_traceback, mock_return_error, mock_demisto):
        """Test main function handles unexpected exceptions."""
        mock_demisto.incident.side_effect = Exception("Unexpected error")
        mock_traceback.format_exc.return_value = "Traceback details"

        main()

        mock_demisto.error.assert_called_once_with("Traceback details")
        mock_return_error.assert_called_once()
        assert "Failed to execute VectraRUXSyncDetectionSummary" in str(mock_return_error.call_args)