Hoxhunt v2

Use the Hoxhunt integration to send feedback to reporters of incidents, set incident sensitivity, and apply SOC classification to incidents.

Email · Hoxhunt

Details

IDHoxhunt v2
ProviderHoxhunt
CategoryEmail
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Use the Hoxhunt integration to send feedback to reporters of incidents, set incident sensitivity, and apply SOC classification to incidents.
This integration was integrated and tested with version August 2024 of Hoxhunt.

Configure Hoxhunt v2 in Cortex

Parameter Description Required
Server URL e.g. https://api.hoxhunt.com/graphql-external True
API Key Input your api key from Hoxhunt True
Fetch incidents   False
Incidents Fetch Interval   False
Incident type   False
First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days). Defaults to 7 days. False
Fetch limit Maxium number of incidents per fetch. Cap is 100. False
Only fetch escalated incidents If enabled, only escalated incidents will be fetched. Set up escalation rules in Hoxhunt Response -> Incident Rules False
Only fetch open incidents If enabled, only open incidents will be fetched. False
Use system proxy settings   False
Incident Mirroring Direction Choose the direction to mirror the incident: Incoming (from Hoxhunt to Cortex XSOAR), Outgoing (from Cortex XSOAR to Hoxhunt), or Incoming and Outgoing (from/to Cortex XSOAR and Hoxhunt). False
Trust any certificate (not secure)   False

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

hoxhunt-current-user-get


Gets the current user information from Hoxhunt.

Base Command

hoxhunt-current-user-get

Input

| Argument Name | Description | Required |
| — | — | — |

Command Example

!hoxhunt-current-user-get

Context Output

Path Type Description
HoxHunt.CurrentUser string Current User information from Hoxhunt.

hoxhunt-incident-threats-get


Gets threats from Hoxhunt.

Base Command

hoxhunt-incident-threats-get

Input

Argument Name Description Required
incident_id The ID of the Hoxhunt incident the threats will be from. Required
sort Sorting strategy to use when returning threats from API. Defaults to createdAt_DESC. See Hoxhunt API documentation for more information. Optional
limit Limit the amount of threats returned. Defaults to 50, maximum is 100. Optional
filter Filter string to use to pick only threats of interest. See Hoxhunt API documentation for more information. Optional

Context Output

Path Type Description
Hoxhunt.Threats string Threats from Hoxhunt.
Command Example

!hoxhunt-incident-threats-get incident_id="12345" limit=5 sort="createdAt_DESC"

hoxhunt-incident-note-add


Add Incident note.

Base Command

hoxhunt-incident-note-add

Input

Argument Name Description Required
incident_id The ID of the Hoxhunt incident to which the note will be added. Required
note The text content of the note to add to the Hoxhunt incident. Required

Context Output

Path Type Description
Hoxhunt.addIncidentNode string Add Incident note.
Command Example

!hoxhunt-incident-note-add incident_id="12345" note="Investigated and escalated to the SOC team."

hoxhunt-incident-threats-remove


Remove all threats that belong to an incident.

Base Command

hoxhunt-incident-threats-remove

Input

Argument Name Description Required
incident_id The ID of the Hoxhunt incident from which threats will be removed. Required

Context Output

Path Type Description
Hoxhunt.removeIncidentThreats number Returns number of removed threats.
Command Example

!hoxhunt-incident-threats-remove incident_id="12345"

hoxhunt-incident-soc-feedback-send


Send feedback to reporters of incident about whether the reported email was safe, spam or malicious.

Base Command

hoxhunt-incident-soc-feedback-send

Input

Argument Name Description Required
incident_id The ID of the Hoxhunt incident for which SOC feedback will be sent. Required
custom_message A custom message to include with the SOC feedback. Required
threat_feedback_reported_at_limit Datetime limit. Accepts (<number> <time unit>), e.g “7 days”, “one month” or a iso string (e.g. “2024-10-30T08:37:42.359Z”) Required

Context Output

Path Type Description
Hoxhunt.sendIncidentSocFeedback string The reporter will be informed the incident has been resolved and that no further actions are required from them.
Command Example

!hoxhunt-incident-soc-feedback-send incident_id="12345" custom_message="User reported a phishing email." threat_feedback_reported_at_limit="2024-09-01T00:00:00Z"

hoxhunt-incident-set-sensitive


Set incident to contain sensitive information.

Base Command

hoxhunt-incident-set-sensitive

Input

Argument Name Description Required
incident_id The ID of the Hoxhunt incident to be marked as sensitive or not sensitive. Required
is_sensitive True or False Boolean for whether the incident contains sensitive information. Possible values are: TRUE, FALSE. Required

Context Output

Path Type Description
Hoxhunt.setIncidentSensitive string Incident sensitivity information.
Command Example

!hoxhunt-incident-set-sensitive incident_id="12345" is_sensitive=true

hoxhunt-incident-set-soc-classification


Set soc classification for an incident.

Base Command

hoxhunt-incident-set-soc-classification

Input

Argument Name Description Required
incident_id The ID of the Hoxhunt incident to classify. Required
classification The SOC classification to apply to the incident. Possible values are: MALICIOUS, SPAM, SAFE. Required

Context Output

Path Type Description
Hoxhunt.setIncidentSocClassification string Incident SOC classification information.
Command Example

!hoxhunt-incident-set-soc-classification incident_id="12345" classification="Malware"

hoxhunt-incident-update-state


Updates Incident state.

Base Command

hoxhunt-incident-update-state

Input

Argument Name Description Required
incident_id The ID of the Hoxhunt incident to update. Required
state The new state of the incident. Possible values are: OPEN, RESOLVED. Required

Context Output

Path Type Description
Hoxhunt.updateIncidentState string Incident state information.
Command Example

!hoxhunt-incident-update-state incident_id="12345" state="RESOLVED"

get-mapping-fields


Get mapping fields from remote incident. Please note that this method will not update the current incident. It’s here for debugging purposes.

Base Command

get-mapping-fields

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

There is no context output for this command.

Incident Mirroring

You can enable incident mirroring between Cortex XSOAR incidents and Hoxhunt v2 corresponding events (available from Cortex XSOAR version 6.0.0).
To set up the mirroring:

  1. Enable Fetching incidents in your instance configuration.
  2. In the Mirroring Direction integration parameter, select in which direction the incidents should be mirrored:

    Option Description
    None Turns off incident mirroring.
    Incoming Any changes in Hoxhunt v2 events (mirroring incoming fields) will be reflected in Cortex XSOAR incidents.
    Outgoing Any changes in Cortex XSOAR incidents will be reflected in Hoxhunt v2 events (outgoing mirrored fields).
    Incoming And Outgoing Changes in Cortex XSOAR incidents and Hoxhunt v2 events will be reflected in both directions.

Newly fetched incidents will be mirrored in the chosen direction. However, this selection does not affect existing incidents.
Important Note: To ensure the mirroring works as expected, mappers are required, both for incoming and outgoing, to map the expected fields in Cortex XSOAR and Hoxhunt v2.

Configuration parameters

  • url — Server URL (required)
  • api_key — API Key (required)
  • mirror_direction — Incident Mirroring Direction
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • incidentFetchInterval — Incidents Fetch Interval
  • incidentType — Incident type
  • first_fetch — First fetch timestamp
  • max_fetch — Fetch limit
  • only_escalated_incidents — Only fetch escalated incidents
  • only_open_incidents — Only fetch open incidents

Commands (9)

  • get-mapping-fields

    Get mapping fields from remote incident. Please note that this method will not update the current incident. It's here for debugging purposes.

  • hoxhunt-current-user-get

    Gets the current user information from Hoxhunt.

  • hoxhunt-incident-note-add

    Add Incident note.

  • hoxhunt-incident-set-sensitive

    Set incident to contain sensitive information.

  • hoxhunt-incident-set-soc-classification

    Set soc classification for an incident.

  • hoxhunt-incident-soc-feedback-send

    Send feedback to reporters of incident about whether the reported email was safe, spam or malicious.

  • hoxhunt-incident-threats-get

    Gets threats from Hoxhunt.

  • hoxhunt-incident-threats-remove

    Remove all threats that belong to an incident.

  • hoxhunt-incident-update-state

    Updates Incident state.

from datetime import datetime
from typing import Any
from unittest.mock import MagicMock

import demistomock as demisto
import pytest
from CommonServerPython import *
from freezegun import freeze_time
from HoxhuntV2 import *
from requests import Response

del globals()["test_module_command"]

FROZEN_DATE = "2024-09-01T00:00:00Z"
FROZEN_DATE_AFTER = "2024-10-01T00:00:00Z"


def form_incidents(incidents: list[dict[str, Any]], now: float) -> list[dict[str, Any]]:
    return [create_incident_from_log(item, now) for item in incidents]


@pytest.fixture
def mock_client():
    client = MagicMock(spec=Client)
    return client


@pytest.fixture
def mock_demisto(mocker):
    incidents_mock = mocker.patch.object(demisto, "incidents")
    set_last_run_mock = mocker.patch.object(demisto, "setLastRun")
    params_mock = mocker.patch.object(demisto, "params")
    args_mock = mocker.patch.object(demisto, "args")
    return {
        "incidents": incidents_mock,
        "setLastRun": set_last_run_mock,
        "params": params_mock,
        "args": args_mock,
    }


def setup_mock_params(
    mock_demisto,
    first_fetch=FROZEN_DATE,
    max_fetch="",
    query_filter="",
    incident_id="",
    last_update=FROZEN_DATE,
    delta={},
    incidentChanged=False,
    status=1,
    data={},
    remoteId="1234",
):
    mock_demisto["args"].return_value = {
        "incident_id": incident_id,
        "lastUpdate": last_update,
        "delta": delta,
        "incidentChanged": incidentChanged,
        "status": status,
        "data": data,
        "remoteId": remoteId,
        "id": incident_id,
    }
    mock_demisto["params"].return_value = {
        "first_fetch": first_fetch,
        "max_fetch": max_fetch,
        "queryfilter": query_filter,
    }


@pytest.fixture
def incident_1():
    return {
        "_id": "1",
        "createdAt": FROZEN_DATE_AFTER,
        "updatedAt": FROZEN_DATE_AFTER,
        "humanReadableId": "incident-1",
        "classification": "malicious",
        "policyName": "BEC",
        "threatCount": 5,
        "globalThreatCount": 10,
        "firstThreat": {"_id": "threat-1", "createdAt": FROZEN_DATE, "classification": "malicious"},
    }


@pytest.fixture
def incident_2():
    return {
        "_id": "2",
        "createdAt": FROZEN_DATE_AFTER,
        "updatedAt": FROZEN_DATE_AFTER,
        "humanReadableId": "incident-2",
        "classification": "malicious",
        "policyName": "BEC",
        "threatCount": 5,
        "globalThreatCount": 10,
        "firstThreat": {"_id": "threat-2", "createdAt": FROZEN_DATE, "classification": "malicious"},
    }


@pytest.fixture
def mock_incidents(incident_1, incident_2):
    return [incident_1, incident_2]


#  GqlResult


def test_gql_result_no_errors():
    """
    Given
        GqlResult initialized without errors
    When
        Creating a new GqlResult instance
    Then
        Check that data is an empty dictionary and errors is an empty list
    """
    gql_result = GqlResult()
    assert gql_result.data == {}
    assert gql_result.errors == []
    assert not gql_result.has_errors()


def test_gql_result_with_errors():
    """
    Given
        GqlResult initialized with errors
    When
        Creating a new GqlResult instance
    Then
        Check that errors are stored and has_errors returns True
    """
    error = "Sample error"
    gql_result = GqlResult(errors=[error])
    assert gql_result.has_errors()
    assert gql_result.errors == [error]


# http_error_handler


def test_http_error_handler_rate_limit():
    """
    Given
        A Response object with a status code of 429 (rate limit)
    When
        Calling http_error_handler with the response
    Then
        Expect an exception with message 'API rate limit'
    """
    res = MagicMock(spec=Response)
    res.status_code = 429

    with pytest.raises(Exception, match="API rate limit"):
        http_error_handler(res)


def test_http_error_handler_json_error():
    """
    Given
        A Response object with a status code and a JSON error message
    When
        Calling http_error_handler with the response
    Then
        Expect a DemistoException with the parsed JSON message
    """
    res = MagicMock(spec=Response)
    res.status_code = 400
    res.reason = "Bad Request"
    res.json.return_value = {"error": "Invalid request"}

    with pytest.raises(DemistoException) as excinfo:
        http_error_handler(res)

    assert "Error in API call [400] - Bad Request" in str(excinfo.value)
    assert '{"error": "Invalid request"}' in str(excinfo.value)


# client.query


@pytest.mark.parametrize(
    "mock_response, expected_data, expected_errors",
    [
        ({"data": {"key": "value"}}, {"key": "value"}, []),
        (
            {
                "errors": [
                    {
                        "message": "An error occurred",
                        "locations": [{"line": 1, "column": 2}],
                        "path": ["query", "field"],
                        "extensions": {"code": "INTERNAL_ERROR"},
                    }
                ]
            },
            {},
            ["An error occurred"],
        ),
        (
            {
                "data": {"key": "value"},
                "errors": [
                    {
                        "message": "Partial error occurred",
                        "locations": [{"line": 2, "column": 3}],
                        "path": ["query", "otherField"],
                        "extensions": {"code": "PARTIAL_ERROR"},
                    }
                ],
            },
            {"key": "value"},
            ["Partial error occurred"],
        ),
    ],
)
def test_query(mock_response, expected_data, expected_errors, mocker):
    client = Client(base_url="https://api.example.com", headers={"Authorization": "Bearer token"})

    with mocker.patch.object(client, "_http_request", return_value=mock_response):
        gql_result = client.query("query { field }")

        assert gql_result.data == expected_data
        assert len(gql_result.errors) == len(expected_errors)

        for actual_error, expected_error in zip(gql_result.errors, expected_errors):
            assert actual_error == expected_error


# fetch_incidents_command


@pytest.mark.parametrize(
    "first_fetch, max_fetch, only_open_incidents, only_escalated_incidents, last_run, "
    "expected_next_run, expected_query_part, raises_exception",
    [
        (FROZEN_DATE, "50", None, None, {}, {"start_time": FROZEN_DATE_AFTER}, "createdAt_gt", False),
        (FROZEN_DATE, "50", False, True, {}, {"start_time": FROZEN_DATE_AFTER}, "escalation__escalatedAt_exists: true", False),
        (
            FROZEN_DATE,
            "50",
            True,
            True,
            {},
            {"start_time": FROZEN_DATE_AFTER},
            "escalation__escalatedAt_exists: true, state_eq: OPEN",
            False,
        ),
        (FROZEN_DATE, "50", True, False, {}, {"start_time": FROZEN_DATE_AFTER}, "state_eq: OPEN", False),
        (FROZEN_DATE, "50", False, False, {}, {"start_time": FROZEN_DATE_AFTER}, "createdAt_gt", False),
        ("invalid-time-format", "50", None, None, {}, None, "", True),
    ],
)
def test_fetch_incidents(
    mock_client,
    mock_incidents,
    first_fetch,
    max_fetch,
    only_open_incidents,
    only_escalated_incidents,
    last_run,
    expected_next_run,
    expected_query_part,
    raises_exception,
):
    mock_client.query = MagicMock(return_value=GqlResult({"incidents": mock_incidents if not raises_exception else []}))
    now = datetime.now().timestamp()

    if raises_exception:
        with pytest.raises(ValueError, match="Invalid first_fetch format"):
            fetch_incidents(mock_client, first_fetch, max_fetch, only_open_incidents, only_escalated_incidents, now, last_run)
    else:
        incidents, next_run = fetch_incidents(
            mock_client, first_fetch, max_fetch, only_open_incidents, only_escalated_incidents, now, last_run
        )

        if expected_next_run:
            expected_incidents = form_incidents(mock_incidents, now)
        else:
            expected_incidents = []

        assert incidents == expected_incidents
        assert next_run == expected_next_run

        if expected_query_part:
            query_call = mock_client.query.call_args[0][0]
            assert expected_query_part in query_call, f"Expected query to contain: {expected_query_part}"


# get_remote_data_command


def test_get_remote_data_command_resolved(mock_client, mock_demisto):
    """
    Given
        An incident with state 'RESOLVED' that has been updated since the last update time
    When
        Calling get_remote_data_command
    Then
        The incident data is returned with an entry to close the incident in Demisto
    """
    incident_id = "incident-1"
    last_update = FROZEN_DATE
    setup_mock_params(mock_demisto, incident_id, last_update)

    incident_data = {
        "_id": incident_id,
        "state": "RESOLVED",
        "updatedAt": FROZEN_DATE,
    }
    mock_client.get_incident_by_id.return_value = incident_data

    response = get_remote_data_command(mock_client, mock_demisto["args"](), mock_demisto["params"]())

    assert response.mirrored_object["id"] == incident_id
    assert response.mirrored_object["state"] == "RESOLVED"

    expected_entry = {
        "Type": EntryType.NOTE,
        "Contents": {"closeReason": "Incident was resolved in Hoxhunt platform", "dbotIncidentClose": True},
        "ContentsFormat": EntryFormat.JSON,
    }
    assert response.entries == [expected_entry]


def test_get_remote_data_command_open(mock_client, mock_demisto):
    """
    Given
        An incident with state 'OPEN' that has been updated since the last update time
    When
        Calling get_remote_data_command
    Then
        The incident data is returned with an entry to reopen the incident in Demisto
    """
    incident_id = "incident-2"
    last_update = FROZEN_DATE
    setup_mock_params(mock_demisto, incident_id, last_update)

    incident_data = {
        "_id": incident_id,
        "state": "OPEN",
        "updatedAt": FROZEN_DATE,
    }
    mock_client.get_incident_by_id.return_value = incident_data

    response = get_remote_data_command(mock_client, mock_demisto["args"](), mock_demisto["params"]())

    assert response.mirrored_object["id"] == incident_id
    assert response.mirrored_object["state"] == "OPEN"

    expected_entry = {
        "Type": EntryType.NOTE,
        "Contents": {"dbotIncidentReopen": True},
        "ContentsFormat": EntryFormat.JSON,
    }
    assert response.entries == [expected_entry]


def test_get_remote_data_command_no_update(mock_client, mock_demisto):
    """
    Given
        An incident that has not been updated since the last update time
    When
        Calling get_remote_data_command
    Then
        No incident data or entries are returned
    """
    incident_id = "incident-3"
    last_update = FROZEN_DATE
    setup_mock_params(mock_demisto, incident_id, last_update)

    mock_client.get_incident_by_id.return_value = {}

    response = get_remote_data_command(mock_client, mock_demisto["args"](), mock_demisto["params"]())

    assert response.mirrored_object == {}
    assert response.entries == []


def test_get_remote_data_command_missing_id(mock_client, mock_demisto):
    """
    Given
        Incident data returned without an '_id' field
    When
        Calling get_remote_data_command
    Then
        The 'id' field is not set in the mirrored object
    """
    incident_id = "incident-4"
    last_update = FROZEN_DATE
    setup_mock_params(mock_demisto, incident_id, last_update)

    incident_data = {
        "state": "OPEN",
        "updatedAt": FROZEN_DATE,
    }
    mock_client.get_incident_by_id.return_value = incident_data

    response = get_remote_data_command(mock_client, mock_demisto["args"](), mock_demisto["params"]())

    assert "id" not in response.mirrored_object
    assert response.mirrored_object["state"] == "OPEN"

    expected_entry = {
        "Type": EntryType.NOTE,
        "Contents": {"dbotIncidentReopen": True},
        "ContentsFormat": EntryFormat.JSON,
    }
    assert response.entries == [expected_entry]


# get_mapping_fields_command


def test_get_mapping_fields_command(mock_client, mocker):
    """
    Given
        The Hoxhunt integration is properly set up
    When
        Calling get_mapping_fields_command
    Then
        A GetMappingFieldsResponse is returned containing the correct mapping fields for each incident type
    """
    mocker.patch("HoxhuntV2.CAMPAIGN_INCIDENT_TYPE_NAME", CAMPAIGN_INCIDENT_TYPE_NAME)
    mocker.patch("HoxhuntV2.USER_ACTED_TYPE_NAME", USER_ACTED_TYPE_NAME)
    mocker.patch("HoxhuntV2.BEC_TYPE_NAME", BEC_TYPE_NAME)
    mocker.patch("HoxhuntV2.INCIDENT_MAPPING_FIELDS", INCIDENT_MAPPING_FIELDS)

    response = get_mapping_fields_command(mock_client, args={}, params={})

    assert isinstance(response, GetMappingFieldsResponse)

    schemes = response.scheme_types_mappings

    assert len(schemes) == 3

    type_names = [scheme.type_name for scheme in schemes]
    expected_type_names = [CAMPAIGN_INCIDENT_TYPE_NAME, USER_ACTED_TYPE_NAME, BEC_TYPE_NAME]
    assert set(type_names) == set(expected_type_names)

    for scheme in schemes:
        fields = scheme.fields
        assert len(fields) == len(INCIDENT_MAPPING_FIELDS)

        for field_name, field in fields.items():
            assert field_name in INCIDENT_MAPPING_FIELDS
            mapping_field = INCIDENT_MAPPING_FIELDS[field_name]
            expected_description = mapping_field["description"]
            assert field == expected_description


# update_remote_system_command


def test_update_remote_system_command_incident_opened(mock_client, mock_demisto, mocker):
    """
    Given
        An incident that was changed in XSOAR with delta changes, and the status is 'open'
    When
        Running update_remote_system_command
    Then
        The client's update_incident_state is called with 'OPEN', and update_changed_incident_fields is called
    """
    setup_mock_params(
        mock_demisto, delta={"field1": "value1"}, incidentChanged=True, status=1, data={"some": "data"}, remoteId="1234"
    )

    update_changed_fields_mock = mocker.patch.object(mock_client, "update_changed_incident_fields")

    incident_id = update_remote_system_command(mock_client, mock_demisto["args"](), mock_demisto["params"]())

    mock_client.update_incident_state.assert_called_with("1234", "OPEN")
    update_changed_fields_mock.assert_called_with("1234", {"field1": "value1"})
    assert incident_id == "1234"


# get_modified_remote_data_command


def test_get_modified_remote_data_command_success(mock_client, mock_demisto):
    """
    Given
        A valid last_update timestamp, and the client returns modified incidents without errors
    When
        Running get_modified_remote_data_command
    Then
        The function returns a GetModifiedRemoteDataResponse with the modified incident IDs
    """

    setup_mock_params(mock_demisto, last_update=FROZEN_DATE)

    mock_result = MagicMock()
    mock_result.has_errors.return_value = False
    mock_result.data = {
        "incidents": [
            {"_id": "inc1"},
            {"_id": "inc2"},
            {"_id": "inc3"},
        ]
    }
    mock_client.get_modified_incidents.return_value = mock_result

    response = get_modified_remote_data_command(mock_client, mock_demisto["args"](), mock_demisto["params"]())

    assert isinstance(response, GetModifiedRemoteDataResponse)
    assert response.modified_incident_ids == ["inc1", "inc2", "inc3"]


def test_get_modified_remote_data_command_client_error(mock_client, mock_demisto):
    """
    Given
        A valid last_update timestamp, but the client returns errors
    When
        Running get_modified_remote_data_command
    Then
        The function raises an Exception with the client's error messages
    """

    setup_mock_params(mock_demisto, last_update=FROZEN_DATE)

    mock_result = MagicMock()
    mock_result.has_errors.return_value = True
    mock_result.errors = "Some error occurred"
    mock_client.get_modified_incidents.return_value = mock_result

    with pytest.raises(Exception, match="Some error occurred"):
        get_modified_remote_data_command(mock_client, mock_demisto["args"](), mock_demisto["params"]())


def test_get_modified_remote_data_command_no_incidents(mock_client, mock_demisto):
    """
    Given
        A valid last_update timestamp, and the client returns no modified incidents
    When
        Running get_modified_remote_data_command
    Then
        The function returns an empty list of modified incident IDs
    """

    setup_mock_params(mock_demisto, last_update=FROZEN_DATE)

    mock_result = MagicMock()
    mock_result.has_errors.return_value = False
    mock_result.data = {"incidents": []}
    mock_client.get_modified_incidents.return_value = mock_result

    response = get_modified_remote_data_command(mock_client, mock_demisto["args"](), mock_demisto["params"]())

    assert isinstance(response, GetModifiedRemoteDataResponse)
    assert response.modified_incident_ids == []


# gql commands


@pytest.mark.parametrize(
    "command_function, args, query_return_value, expected_output",
    [
        (
            hoxhunt_get_current_user_command,
            {},
            {"data": {"currentUser": {"emails": ["user@example.com"]}}},
            ["user@example.com"],
        ),
        (
            hoxhunt_get_current_user_command,
            {},
            {"errors": ["Some error occurred"]},
            None,
        ),
        (
            hoxhunt_get_incident_threats_command,
            {"incident_id": "123"},
            {"data": {"incidents": [{"threats": [{"id": "t1", "name": "Threat 1"}]}]}},
            [{"id": "t1", "name": "Threat 1"}],
        ),
        (
            hoxhunt_get_incident_threats_command,
            {},
            {"errors": ["Some error occurred"]},
            None,
        ),
        (
            hoxhunt_add_incident_note_command,
            {"incident_id": "inc123", "note": "Test note"},
            {
                "data": {
                    "addIncidentNote": {
                        "notes": [{"text": "Previous note", "_id": "note122"}, {"text": "Test note", "_id": "note123"}]
                    }
                }
            },
            {"incident_id": "inc123", "note_id": "note123", "note": "Test note"},
        ),
        (
            hoxhunt_add_incident_note_command,
            {"incident_id": "inc123", "note": "Test note"},
            {"errors": ["Some error occurred"]},
            None,
        ),
        (
            hoxhunt_remove_incident_threats_command,
            {"incident_id": "inc123"},
            {"data": {"removeIncidentThreats": 5}},
            {"incident_id": "inc123", "removed threats number": 5},
        ),
        (
            hoxhunt_remove_incident_threats_command,
            {"incident_id": "inc123"},
            {"errors": ["Some error occurred"]},
            None,
        ),
        (
            hoxhunt_send_incident_soc_feedback_command,
            {"incident_id": "inc123", "custom_message": "Test message", "threat_feedback_reported_at_limit": "2023-10-01"},
            {"data": {}},
            {"_id": "inc123", "custom_message": "Test message", "limit date": "2023-10-01"},
        ),
        (
            hoxhunt_send_incident_soc_feedback_command,
            {"incident_id": "inc123", "custom_message": "Test message", "threat_feedback_reported_at_limit": "2023-10-01"},
            {"errors": ["Some error occurred"]},
            None,
        ),
        (
            hoxhunt_set_incident_sensitive_command,
            {"incident_id": "inc123", "is_sensitive": "true"},
            {"_id": "inc123", "hasSensitiveInformation": True},
            {"incident_id": "inc123", "is_sensitive": "True"},
        ),
        (
            hoxhunt_set_incident_sensitive_command,
            {"incident_id": "inc123", "is_sensitive": "false"},
            {"_id": "inc123", "hasSensitiveInformation": False},
            {"incident_id": "inc123", "is_sensitive": "False"},
        ),
        (
            hoxhunt_set_incident_soc_classification_command,
            {"incident_id": "inc123", "classification": "Malware"},
            {"data": {"setIncidentSocClassification": {"_id": "inc123", "classification": "Malware"}}},
            {"_id": "inc123", "classification": "Malware"},
        ),
        (
            hoxhunt_set_incident_soc_classification_command,
            {"incident_id": "inc123", "classification": "Malware"},
            {"errors": ["Some error occurred"]},
            None,
        ),
        (
            hoxhunt_update_incident_state_command,
            {"incident_id": "inc123", "state": "closed"},
            {"data": {"updateIncidentState": {"_id": "inc123", "state": "closed"}}},
            {"_id": "inc123", "state": "closed"},
        ),
        (
            hoxhunt_update_incident_state_command,
            {"incident_id": "inc123", "state": "closed"},
            {"errors": ["Some error occurred"]},
            None,
        ),
    ],
)
def test_commands(
    mock_client,
    command_function,
    args,
    query_return_value,
    expected_output,
    mocker,
):
    command_function_to_client_method = {
        hoxhunt_get_current_user_command: "get_current_user",
        hoxhunt_add_incident_note_command: "add_incident_note",
        hoxhunt_remove_incident_threats_command: "remove_incident_threats",
        hoxhunt_send_incident_soc_feedback_command: "send_incident_soc_feedback",
        hoxhunt_set_incident_sensitive_command: "set_incident_sensitive",
        hoxhunt_set_incident_soc_classification_command: "set_incident_soc_classification",
        hoxhunt_update_incident_state_command: "update_incident_state",
    }

    # Set up mocks
    if command_function == hoxhunt_set_incident_sensitive_command:
        # For functions where client method returns a dict
        mocker.patch.object(mock_client, command_function_to_client_method[command_function], return_value=query_return_value)
    else:
        mock_result = GqlResult(data=query_return_value.get("data", {}), errors=query_return_value.get("errors", []))
        mocker.patch.object(mock_result, "has_errors", return_value=bool(query_return_value.get("errors", [])))

        mocker.patch.object(mock_client, "query", return_value=mock_result)

        if command_function in command_function_to_client_method:
            client_method_name = command_function_to_client_method[command_function]
            mocker.patch.object(mock_client, client_method_name, return_value=mock_result)
        else:
            pass

    mocker.patch("HoxhuntV2.create_output", side_effect=lambda results, *_: results)

    if query_return_value.get("errors"):
        with pytest.raises(Exception):
            command_function(mock_client, args, params={})
    else:
        output = command_function(mock_client, args, params={})
        assert output == expected_output


@pytest.mark.parametrize(
    "test_date, expected_api_date, should_raise",
    [
        # ISO format - should pass through unchanged
        ("2025-02-25T13:41:10.382Z", "2025-02-25T13:41:10.382Z", False),
        # Relative date - should convert to ISO
        ("1 hour", "2024-01-05T11:00:00.000Z", False),
        # Relative date - should convert to ISO
        ("2 days", "2024-01-03T12:00:00.000Z", False),
        # Relative date - should convert to ISO
        ("6 months", "2023-07-05T12:00:00.000Z", False),
        # Relative date - should convert to ISO
        ("2 years", "2022-01-05T12:00:00.000Z", False),
        # Timezone aware ISO format - should convert to UTC
        (
            "2025-02-25T15:41:10+02:00",  # Helsinki time (UTC+2)
            "2025-02-25T13:41:10.000Z",  # Same time in UTC
            False,
        ),
        # Invalid date - should raise ValueError
        (
            "Skipidi duu",  # Invalid date
            None,
            True,
        ),
    ],
)
@freeze_time("2024-01-05T12:00:00Z")  # Frozen time in UTC
def test_send_incident_soc_feedback_date_parsing(mock_client, mocker, test_date, expected_api_date, should_raise):
    """
    Given
        Different formats of threat_feedback_reported_at_limit dates with various timezone specifications
    When
        Calling hoxhunt_send_incident_soc_feedback_command
    Then
        - ISO dates should pass through unchanged if already in UTC
        - Relative dates should be converted to ISO
        - Timezone-aware dates should be converted to UTC
        - Invalid timezones should raise ValueError
    """
    args = {"incident_id": "inc123", "custom_message": "Test message", "threat_feedback_reported_at_limit": test_date}

    mock_result = GqlResult(data={})
    mocker.patch.object(mock_client, "send_incident_soc_feedback", return_value=mock_result)
    mocker.patch("HoxhuntV2.create_output", side_effect=lambda results, *_: results)

    if should_raise:
        with pytest.raises(ValueError):
            hoxhunt_send_incident_soc_feedback_command(mock_client, args, {})
    else:
        result = hoxhunt_send_incident_soc_feedback_command(mock_client, args, {})

        mock_client.send_incident_soc_feedback.assert_called_once_with("inc123", "Test message", expected_api_date)
        assert result == {"_id": "inc123", "custom_message": "Test message", "limit date": test_date}