FireEye ETP

Trellix Email Security - Cloud is a cloud-based platform that protects against advanced email attacks.

Network Security · Trellix Email Security - Cloud

Details

IDFireEye ETP
ProviderTrellix
CategoryNetwork Security
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10404775
Supported ModulesAgentix XSIAM

README

Trellix Email Security - Cloud

Overview

Use the Trellix Email Security - Cloud integration to import messages as incidents, search for messages with specific attributes, and retrieve alert data.

Use Cases

  • Search for messages using specific message attributes as indicators.
  • Import messages as Cortex incidents/issues, using the message status as indicator.

Authentication and Authorization

Configuring API Keys

If the IAM domain you use to access the Trellix UI ends in fireeye.com, follow these steps to configure API keys:

  1. Log in to the Email Security — Cloud Web Portal or IAM console.
  2. Click My Settings in the top navigation bar.
  3. Click the API Keys tab in the IAM console.
  4. Click Create API Key.
  5. On the Manage API Key page, specify the following:
  • API key name
  • Expiration time for the API key.
    The expiration time should be set as "100d" for 100 days or "1y" for 1 year, for example.
  • Products
    Select both Email Threat Prevention and Identity Access Management.
  1. Select all required entitlements. For any API access, the following entitlements are mandatory:

    • iam.users.browse
    • iam.orgs.self.read

    Alerts APIs

    For accessing alerts APIs, the following additional entitlements are required:

    • etp.alerts.read

    Trace APIs

    For accessing trace APIs, the following additional entitlements are required:

    • etp.email_trace.read

    Quarantine APIs

    For accessing quarantine APIs, the following additional entitlements are required:

    • etp.quarantine.update
    • etp.quar

Authentication Prerequisites

To ensure a successful connection, select the authentication method that matches the Server URL (Instance URL) you are configuring.

Dual Authentication Methods

We support two different authentication methods depending on the endpoint domain:

Domain Used in Server URL Authentication Method Required Parameters
Ends in trellix.com OAuth 2.0 Client ID, Client Secret, and OAuth Scopes
Ends in fireeye.com API Key API Key (only)

Authentication Setup (Choose One)

You must configure only one of the two authentication approaches below based on your Server URL domain.

  • 1. API Key Method (For fireeye.com URLs):
    • Configure an API key on the ETP Web portal. Select the product as both Email Threat Prevention and Identity Access Management. Select all entitlements.
  • 2. OAuth 2.0 Method (For trellix.com URLs):
    • When creating the Client ID and Client Secret, ensure the corresponding user/role has explicit permission to access the API.
    • Note: If API access permissions are not properly set for the user/role, the authentication attempt will fail with a 400 Client Error: Bad Request even if the Client ID and Secret are correct.
  • Contact Trellix Email Security - Cloud Technical Support to let them know the IP address of your Cortex Server and the URL you are accessing, e.g. https://etp.us.fireeye.com. Trellix will add these details to their Firewall rules so that the bidirectional traffic can be allowed between Cortex and Trellix Email Security - Cloud.

Configure Trellix Email Security - Cloud in Cortex

Parameter Description Required
Server URL Valid URLs (US, EMEA, USGOV): https://us.etp.trellix.com / https://etp.us.fireeye.com, https://eu.etp.trellix.com / https://etp.eu.fireeye.com, https://etp.us.fireeyegov.com True
Client ID (OAuth) Use the Client ID and Client Secret for the Trellix base URL.  
Client Secret (OAuth)    
OAuth Scopes (OAuth) Space-separated list of OAuth scopes. Note: Only include scopes that your application’s Client ID has already been authorized to use. False
Token URL Override the OAuth 2.0 token endpoint base URL. Leave empty to use the default Trellix IAM endpoint. For Trellix GovCloud tenants, set to https://iam.us.trellix-gov.com. False
API Key Use the Api key for the FireEye base URL. False
Trust any certificate (not secure)   False
Use system proxy settings   False
Fetch incidents   False
First fetch timestamp.   False
Max incidents per fetch Input a value between 1 and 59. Values above 59 will be internally capped to avoid exceeding API rate limits. False
Incident type   False
Alerts statuses to import All alerts with a status specified here will be imported as incidents. Valid values are: accepted, deleted, delivered, delivered (retroactive), dropped, dropped oob, dropped (oob retroactive), permanent failure, processing quarantined, rejected, temporary failure False

Fetched Incidents Data

To use Fetch incidents:

  1. Configure a new instance.
  2. Navigate to instance settings, and specify the message status (using the valid values).
  3. Select Fetch incidents option.

The integration will fetch alerts as incidents. It is possible to filter alerts using the specified message status.

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.

fireeye-etp-search-messages

Search for messages using specific message attributes as indicators.


Search for messages that include specified message attributes that are accessible in the ETP portal.

Base Command

fireeye-etp-search-messages

Input

Argument Name Description Required
from_email List of ‘From’ email-addresses, max limit of entries is 10. Optional
from_email_not_in List of ‘From’ email-addresses not to be included, max limit of entries is 10. Optional
recipients List of ‘To’/’Cc’ email-addresses, max limit of entries is 10. Optional
recipients_not_in list of ‘To’/’Cc’ email-addresses not to be included, max limit of entries is 10. Optional
subject List of strings, max limit of entries is 10. Optional
from_accepted_date_time The time stamp of the email-accepted date to specify the beginning of the date range to search, e.g. 2017-10-24T10:48:51.000Z . Specify ‘to_accepted_date_time’ as well to set the complete date range for the search. Optional
to_accepted_date_time The time stamp of the email-accepted date to specify the end of the date range to search, e.g. 2017-10-24T10:48:51.000Z . Specify ‘from_accepted_date_time’ as well to set the complete date range for the search. Optional
rejection_reason List of ETP rejection reason codes ( “ETP102”, “ETP103”, “ETP104”, “ETP200”, “ETP201”, “ETP203”, “ETP204”, “ETP205”, “ETP300”, “ETP301”, “ETP302”, “ETP401”, “ETP402”, “ETP403”, “ETP404”, “ETP405”). Optional
sender_ip List of sender IP addresses, max limit of entries is 10. Optional
status List of email status values( “accepted”, “deleted”, “delivered”, “delivered (retroactive)”, “dropped”, “dropped oob”, “dropped (oob retroactive)”, “permanent failure”, “processing”, “quarantined”, “rejected”, “temporary failure”). Optional
status_not_in List of email status values not to include( “accepted”, “deleted”, “delivered”, “delivered (retroactive)”, “dropped”, “dropped oob”, “dropped (oob retroactive)”, “permanent failure”, “processing”, “quarantined”, “rejected”, “temporary failure”). Optional
last_modified_date_time Date corresponding to last modified date, along with one of the following operators: “>”, “<”, “>=”, “<=”. E.g. use value “<2017-10-24T18:00:00.000Z” to search for messages that were last modified after the specified time stamp. Optional
domain List of domain names. Optional
has_attachments Boolean value to indicate if the message has attachments. Possible values are: true, false. Optional
max_message_size The default value is 20kb and maximum value is 100kb. Optional

Context Output

Path Type Description
FireEyeETP.Message.acceptedDateTime unknown Message accepted date.
FireEyeETP.Message.countryCode unknown Sender country code.
FireEyeETP.Message.domain unknown Domain.
FireEyeETP.Message.emailSize unknown Email size in kb.
FireEyeETP.Message.lastModifiedDateTime unknown Message last modified date.
FireEyeETP.Message.recipientHeader unknown List of message recipients header (includes the display name of the user).
FireEyeETP.Message.recipients unknown List of message recipients.
FireEyeETP.Message.senderHeader unknown Message sender header (includes the display name of the user).
FireEyeETP.Message.sender unknown Message sender address.
FireEyeETP.Message.senderSMTP unknown Message sender SMTP.
FireEyeETP.Message.senderIP unknown Message sender IP.
FireEyeETP.Message.status unknown Message status.
FireEyeETP.Message.subject unknown Message subject.
FireEyeETP.Message.verdicts.AS unknown pass/fail verdict for AS.
FireEyeETP.Message.verdicts.AV unknown pass/fail verdict for AV.
FireEyeETP.Message.verdicts.AT unknown pass/fail verdict for AT.
FireEyeETP.Message.verdicts.PV unknown pass/fail verdict for PV.
FireEyeETP.Message.id unknown Message ID.
Command example 1

!fireeye-etp-search-messages to_accepted_date_time=2017-10-24T10:00:00.000Z from_accepted_date_time=2017-10-24T10:30:00.000Z

Command example 2

!fireeye-etp-search-messages from_email=diana@corp.com,charles@corp.com

Raw Output
{  
   "data": [  
      {  
         "attributes": {  
            "acceptedDateTime": "2018-06-09T10:49:32.000Z",
            "countryCode": "US",
            "domain": "test.com",
            "downStreamMsgID": "250 2.0.0 OK 100041373 d14-v6si970000qtb.70 - gsmtp",
            "emailSize": 9.89,
            "lastModifiedDateTime": "2018-06-09T10:49:33.329Z",
            "recipientHeader": [  
               "Security Operations Center <SOC@corp.com>"
            ],
            "recipientSMTP": [  
               "jason@demisto.com"
            ],
            "senderHeader": "\"soc@demisto.com\" <bot@demisto.com >",
            "senderSMTP": "prvs=691a94fds62a=demisto@demisto.com ",
            "senderIP": "***.***.***.***",
            "status": "delivered",
            "subject": "Attack TCP: SYN Host Sweep (Medium)",
            "verdicts": {  
               "AS": "",
               "AV": "",
               "AT": "pass",
               "PV": ""
            }
         },
         "included": [  
            {  
               "type": "domain",
               "id": 29074,
               "attributes": {  
                  "name": "test.com "
               }
            }
         ],
         "id": "C88B18749AAAAB1B55fc0fa78",
         "type": "trace"
      }
   ],
   "meta": {  
      "total": 85347,
      "copyright": "Copyright 2018 Fireeye Inc",
      "fromLastModifiedOn": {  
         "start": "2018-06-09T10:49:33.329Z",
         "end": "2018-06-09T10:50:59.034Z"
      }
   }
}

fireeye-etp-get-message


Get the data of a specific message.

Base Command

fireeye-etp-get-message

Input

Argument Name Description Required
message_id The message ID. Required

Context Output

Path Type Description
FireEyeETP.Message.acceptedDateTime unknown Message accepted date.
FireEyeETP.Message.countryCode unknown Sender country code.
FireEyeETP.Message.domain unknown Domain.
FireEyeETP.Message.emailSize unknown Email size in kb.
FireEyeETP.Message.lastModifiedDateTime unknown Message last modified date.
FireEyeETP.Message.recipientHeader unknown List of message recipients header (includes the display name of the user).
FireEyeETP.Message.recipients unknown List of message recipients.
FireEyeETP.Message.senderHeader unknown Message sender header (includes the display name of the user).
FireEyeETP.Message.sender unknown Message sender address.
FireEyeETP.Message.senderSMTP unknown Message sender SMTP.
FireEyeETP.Message.senderIP unknown Message sender IP.
FireEyeETP.Message.status unknown Message status.
FireEyeETP.Message.subject unknown Message subject.
FireEyeETP.Message.verdicts.AS unknown pass/fail verdict for AS.
FireEyeETP.Message.verdicts.AV unknown pass/fail verdict for AV.
FireEyeETP.Message.verdicts.AT unknown pass/fail verdict for AT.
FireEyeETP.Message.verdicts.PV unknown pass/fail verdict for PV.
FireEyeETP.Message.id unknown Message ID.
Command example

!fireeye-etp-get-message message_id= C88B18749AAAAB1B55fc0fa78

Raw Output

There is no raw output for this command.

fireeye-etp-list-alerts


Get summary format information about the alerts.

Base Command

fireeye-etp-list-alerts

Input

Argument Name Description Required
alert_id The alert ID. Optional
date_from Supports ISO format (e.g., 2025-09-02T06:45:01Z) or natural language (“7 days ago”, “now”). Optional
date_to Supports ISO format (e.g., 2025-09-02T06:45:01Z) or natural language (“7 days ago”, “now”). Optional
domain List of domain names. Optional
domain_group List of domain groups names. Optional
email_header_subject List of message subject headers. Optional
is_read is_read flag. Optional
is_retro is_retro flag. Optional
malwarename List of malware names. Optional
malwarestype List of malware types. Optional
md5 List of md5. Optional
mta_msg_id List of mta_msg_id. Optional
traffic_type Traffic type defaults to inbound. To handle outbound traffic, set the traffic_type parameter to outbound. Possible values are: inbound, outbound. Optional
verdict List of verdicts. Optional
limit Number of alerts to include in response. Valid range: 1-200. Optional

Context Output

Path Type Description
FireEyeETP.Alerts.domain unknown Domain.
FireEyeETP.Alerts.report_id unknown Report_id.
FireEyeETP.Alerts.id unknown The alert unique ID.
FireEyeETP.Alerts.alert_date unknown The alert date.

fireeye-etp-download-alert-case-files


Downloads all case files for the specified alert ID as a ZIP file. You can obtain the alert ID from the Alert Summary response, for example: “id”: “AV7zzRy7kvIwrKcfu0I”.
The downloaded zip is password protected, and the password is “infected”.

Base Command

fireeye-etp-download-alert-case-files

Input

Argument Name Description Required
alert_id The alert ID. Required

Context Output

There is no context output for this command.

fireeye-etp-list-yara-rulesets


Fetch the list of YARA rulesets and return a list with all the rules.

Base Command

fireeye-etp-list-yara-rulesets

Input

Argument Name Description Required
policy_uuid Universally unique identifier (UUID) of the policy. (Can be found in the URL of the ETP Policies). Required

Context Output

Path Type Description
FireEyeETP.Policy unknown The policy id.

fireeye-etp-download-yara-file


Downloads a YARA file.

Base Command

fireeye-etp-download-yara-file

Input

Argument Name Description Required
policy_uuid Universally unique identifier (UUID) of the policy. (Can be found in the URL of the ETP Policies). Required
ruleset_uuid Universally unique identifier (UUID) of the ruleset. Required

Context Output

There is no context output for this command.

fireeye-etp-upload-yara-file


Update or replace the YARA rule file in the existing ruleset.

Base Command

fireeye-etp-upload-yara-file

Input

Argument Name Description Required
policy_uuid Universally unique identifier (UUID) of the policy. (Can be found in the URL of the ETP Policies). Required
ruleset_uuid Universally unique identifier (UUID) of the ruleset. Required
entryID Entry ID of yara file to upload. Required

Context Output

There is no context output for this command.

fireeye-etp-get-events-data


Returns all events of the alert by the alert ID.

Base Command

fireeye-etp-get-events-data

Input

Argument Name Description Required
message_id Message ID of alert. Required

Context Output

Path Type Description
FireEyeETP.Events unknown The events of the alert.
FireEyeETP.Events.Delivered_msg unknown Display if event is delivered successfully or not.
FireEyeETP.Events.Delivered_status unknown The status of the message.
FireEyeETP.Events.InternetMessageId unknown The internet message ID of the alert.
FireEyeETP.Events.Logs unknown The logs of the alert.

fireeye-etp-quarantine-release


Releases the email file present in the quarantine for the given email. Cloud message ID.

Base Command

fireeye-etp-quarantine-release

Input

Argument Name Description Required
message_id The message ID. Optional

Context Output

There is no context output for this command.

Configuration parameters

  • server — Server URL (required)
  • credentials — Client ID (OAuth)
  • oauth_scopes — OAuth Scopes (OAuth)
  • token_url — Token URL
  • api_key — API key
  • credentials_api_key
  • unsecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • fetch_time — First fetch timestamp.
  • incidentFetchInterval — Incidents Fetch Interval
  • incidents_per_fetch — Max incidents per fetch
  • incidentType — Incident type
  • message_status — Alerts statuses to import

Commands (12)

  • fireeye-etp-download-alert-artifact Deprecated

    Downloads all case files of the alert specified by the alert ID, in a zip file. You can obtain the ID from the Alert Summary response, for example "id": "AV7zzRy7kvIwrKcfu0I". Deprecated. Use the fireeye-etp-download-alert-case-files instead.

  • fireeye-etp-download-alert-case-files

    Downloads all case files of the alert specified by the alert ID, in a zip file. You can obtain the ID from the Alert Summary response, for example "id": "AV7zzRy7kvIwrKcfu0I". The downloaded zip is password protected, and the password is "infected".

  • fireeye-etp-download-yara-file

    Downloads a YARA file.

  • fireeye-etp-get-alert Deprecated

    Detailed information from any particular alert. Alerts more than 90 days old are not available. Deprecated. Use the fireeye-etp-list-alerts instead.

  • fireeye-etp-get-alerts Deprecated

    Get summary format information about the alerts. Deprecated. Use the fireeye-etp-list-alerts instead.

  • fireeye-etp-get-events-data

    Returns all events of the alert by the alert ID.

  • fireeye-etp-get-message

    Get the data of a specific message.

  • fireeye-etp-list-alerts

    Get summary format information about the alerts.

  • fireeye-etp-list-yara-rulesets

    Fetch the list of YARA rulesets and return a list with all the rules.

  • fireeye-etp-quarantine-release

    Releases the email file present in the quarantine for the given email. Cloud message ID.

  • fireeye-etp-search-messages

    Search for messages that include specified message attributes that are accessible in the ETP portal.

  • fireeye-etp-upload-yara-file

    Update or replace the YARA rule file in the existing ruleset.

import json
from unittest.mock import MagicMock, mock_open, patch

import demistomock as demisto
import FireEyeETP
import pytest
from CommonServerPython import CommandResults, EntryType, tableToMarkdown
from FireEyeETP import Client


def test_malware_readable_data():
    """
    Given:
        A dict with only "name" key
    When:
        calling malware_readable_data method on it
    Then:
        Ensure execution does not raise exception on it
    """
    from FireEyeETP import malware_readable_data

    try:
        malware_readable_data({"name": "some-name"})
    except KeyError:
        raise AssertionError("malware_readable_data method should not fail on dict with name key only")


def test_get_alert_command(mocker, requests_mock):
    """
    Given:
        - ID of alert to get
        - The alert object contain unicode

    When:
        - Running get-alert command

    Then:
        - Ensure command runs successfully
        - Ensure results are returned
    """
    import FireEyeETP

    base_url = "https://server_url/api/v1"
    mocker.patch("FireEyeETP.BASE_PATH_V1", base_url)
    mocker.patch("FireEyeETP.API_KEY", "fake-api-key")
    mocker.patch.object(demisto, "args", return_value={"alert_id": "KgBdei7RQS4u4m8Jl7mG"})
    mocker.patch.object(demisto, "results")
    requests_mock.get(
        base_url + "/alerts/KgBdei7RQS4u4m8Jl7mG?",
        json={
            "meta": {"total": 1},
            "data": [
                {
                    "alert": {"explanation": {"malware_detected": {"malware": {}}}},
                    "email": {"headers": {"to": "\u200b"}, "timestamp": {}},
                }
            ],
        },
    )
    FireEyeETP.get_alert_command()
    results = demisto.results.call_args[0][0]
    assert results


def test_fetch_incident_by_status_messages(mocker):
    """
    Given:
        - A status message similar to the alert's status
    When:
        - Running fetch-incidents command (v2 implementation)
    Then:
        - Ensure one incident was fetched as expected
        - Ensure the correct v2 request functions were called
    """
    from FireEyeETP import Client

    mock_client = MagicMock(spec=Client)
    response_1 = {
        "meta": {"search_after": "token123", "size": 1},
        "data": [{"id": "alert1", "email_status": "delivered (retroactive)"}],
    }
    response_2 = {"id": "alert1", "alert": {"occurred": "2023-02-09T19:34:17Z", "severity": "minr"}}

    mocker.patch.object(FireEyeETP, "MESSAGE_STATUS", ["delivered (retroactive)"])
    mocker.patch("FireEyeETP.demisto.getLastRun", return_value={})
    mock_client.get_alerts_request_v2.return_value = response_1
    mock_client.get_alert_request_v2.return_value = response_2
    mocker.patch("FireEyeETP.demisto.setLastRun")

    incidents, last_run = FireEyeETP.fetch_incidents(mock_client)

    assert incidents[0].get("name") == "alert1"
    assert incidents[0].get("rawJSON") == json.dumps(response_2)
    assert last_run.get("pagination_token") == "token123"


def test_fetch_incident_by_status_messages_mismatch_status(mocker):
    """
    Given:
        - A status message differs from the alert's status
    When:
        - Running fetch-incidents command (v2 implementation)
    Then:
        - Ensure no incidents were fetched as expected due to filtering
    """
    from FireEyeETP import Client

    mock_client = MagicMock(spec=Client)
    response_1 = {
        "meta": {"search_after": "token123", "size": 1},
        "data": [{"id": "alert1", "email_status": "deleted"}],
    }

    mocker.patch.object(FireEyeETP, "MESSAGE_STATUS", ["delivered (retroactive)"])
    mocker.patch("FireEyeETP.demisto.getLastRun", return_value={})
    mock_client.get_alerts_request_v2.return_value = response_1
    mocker.patch("FireEyeETP.demisto.setLastRun")

    incidents, _ = FireEyeETP.fetch_incidents(mock_client)

    assert incidents == []


def test_fetch_incident_by_status_messages_with_two_status(mocker):
    """
    Given:
        - A list of status messages matching the alerts' statuses
    When:
        - Running fetch-incidents command (v2 implementation)
    Then:
        - Ensure 2 incidents were fetched as expected
    """
    from FireEyeETP import Client

    mock_client = MagicMock(spec=Client)

    response_1 = {
        "meta": {"search_after": "token456", "size": 2},
        "data": [{"id": "alert1", "email_status": "delivered (retroactive)"}, {"id": "alert2", "email_status": "deleted"}],
    }
    response_alert1 = {"id": "alert1", "alert": {"occurred": "2023-02-09T19:34:17Z", "severity": "minr"}}
    response_alert2 = {"id": "alert2", "alert": {"occurred": "2023-02-09T19:34:18Z", "severity": "majr"}}

    mocker.patch.object(FireEyeETP, "MESSAGE_STATUS", ["delivered (retroactive)", "deleted"])
    mocker.patch("FireEyeETP.demisto.getLastRun", return_value={})
    mock_client.get_alerts_request_v2.return_value = response_1
    mock_client.get_alert_request_v2.side_effect = [response_alert1, response_alert2]
    mocker.patch("FireEyeETP.demisto.setLastRun")

    incidents, _ = FireEyeETP.fetch_incidents(mock_client)

    assert len(incidents) == 2
    assert incidents[0].get("name") == "alert1"
    assert incidents[1].get("name") == "alert2"


@pytest.fixture
def FireEyeETP_client():
    return Client(base_url="https://fireeyeetp", verify=False, headers={}, proxy=False)


@patch("FireEyeETP.fileResult")
def test_download_alert_artifacts_command(mock_file_result):
    """
    Given:
        - ID of alert to get
    When:
        - Running download-alert-artifact command
    Then:
        - Ensure 1 zip file fetched as expected
    """
    from FireEyeETP import Client, download_alert_artifacts_command

    args = {"alert_id": "12345"}
    mock_client = MagicMock(spec=Client)
    mock_response = MagicMock()
    mock_response.content = b"fake_zip_content"
    mock_client.get_artifacts.return_value = mock_response
    mock_file_result.return_value = {"File": "12345.zip", "Type": EntryType.FILE, "Contents": "fake_zip_content"}
    results = download_alert_artifacts_command(mock_client, args)
    mock_client.get_artifacts.assert_called_once_with("12345")
    mock_file_result.assert_called_once_with("12345.zip", data=b"fake_zip_content", file_type=EntryType.FILE)
    assert isinstance(results[0], CommandResults)
    assert results[0].readable_output == "Download alert artifact completed successfully"
    assert results[1] == {"File": "12345.zip", "Type": EntryType.FILE, "Contents": "fake_zip_content"}


def test_list_yara_rulesets_command():
    """
    Given:
        - Policy UUID to get
    When:
        - Running list-yara-rulesets command
    Then:
        - Ensure command runs successfully
        - Ensure results are returned
    """
    from FireEyeETP import Client, list_yara_rulesets_command

    args = {"policy_uuid": "abc-123-uuid"}
    mock_client = MagicMock(spec=Client)
    mock_response = {
        "data": {
            "rulesets": [
                {"name": "Test Ruleset", "description": "Test Description", "uuid": "uuid-123", "yara_file_name": "test.yara"}
            ]
        }
    }
    mock_client.get_yara_rulesets.return_value = mock_response
    result = list_yara_rulesets_command(mock_client, args)
    mock_client.get_yara_rulesets.assert_called_once_with("abc-123-uuid")
    assert isinstance(result, CommandResults)
    assert result.outputs == [
        {"name": "Test Ruleset", "description": "Test Description", "uuid": "uuid-123", "yara_file_name": "test.yara"}
    ]
    assert result.outputs_prefix == "FireEyeETP.Policy.abc-123-uuid"
    assert result.readable_output == (
        "### Rulesets\n|name|description|uuid|yara_file_name|\n|---|---|---|---|\n| Test Ruleset"
        " | Test Description | uuid-123 | test.yara |\n"
    )


@patch("FireEyeETP.fileResult")
def test_download_yara_file_command(mock_file_result):
    """
    Given:
        - Policy UUID to get
        - Ruleset UUID to get
    When:
        - Running download-yara-file command
    Then:
        - Ensure command runs successfully
        - Ensure 1 yara file fetched as expected
    """
    from FireEyeETP import Client, download_yara_file_command

    args = {"policy_uuid": "policy-12345", "ruleset_uuid": "ruleset-67890"}
    mock_client = MagicMock(spec=Client)
    mock_response = MagicMock()
    mock_response.content = b"fake_yara_file_content"
    mock_file_result.return_value = {"File": "original.yara", "Type": EntryType.FILE, "Contents": "fake_yara_file_content"}
    mock_client.get_yara_file.return_value = mock_response
    results = download_yara_file_command(mock_client, args)
    mock_file_result.assert_called_once_with("original.yara", data=b"fake_yara_file_content", file_type=EntryType.FILE)
    assert isinstance(results[0], CommandResults)
    assert results[0].readable_output == "Download yara file completed successfully."
    assert results[1] == {"File": "original.yara", "Type": EntryType.FILE, "Contents": "fake_yara_file_content"}


@patch("FireEyeETP.demisto.getFilePath")
@patch("FireEyeETP.open", new_callable=mock_open, read_data=b"fake_yara_file_content")
def test_upload_yara_file_command_success(mock_open_file, mock_getFilePath):
    """
    Given:
        - Policy UUID to get
        - Ruleset UUID to get
        - EntryID of context file to put
    When:
        - Running upload-yara-file command
    Then:
        - Ensure command runs successfully
        - Ensure 1 yara file uploaded as expected
    """
    from FireEyeETP import Client, upload_yara_file_command

    args = {"entryID": "1", "policy_uuid": "policy-12345", "ruleset_uuid": "ruleset-67890"}
    mock_getFilePath.return_value = {"path": "/path/to/file"}
    mock_response = MagicMock()
    mock_response.status_code = 202
    mock_client = MagicMock(spec=Client)
    mock_client.upload_yara_file.return_value = mock_response
    results = upload_yara_file_command(mock_client, args)
    mock_getFilePath.assert_called_once_with("1")
    mock_open_file.assert_called_once_with("/path/to/file", "rb")
    assert isinstance(results, CommandResults)
    assert results.readable_output == "Upload of Yara file succesfully."


@patch("FireEyeETP.demisto.getFilePath")
@patch("FireEyeETP.open", new_callable=mock_open, read_data=b"fake_yara_file_content")
def test_upload_yara_file_command_failure(mock_open_file, mock_getFilePath):
    """
    Given:
        - Policy UUID to get
        - Ruleset UUID to get
        - EntryID of context file to put
    When:
        - Running upload-yara-file command
    Then:
        - Ensure no yara file uploaded as expected
    """
    from FireEyeETP import Client, upload_yara_file_command

    args = {"entryID": "1", "policy_uuid": "policy-12345", "ruleset_uuid": "ruleset-67890"}
    mock_getFilePath.return_value = {"path": "/path/to/file"}
    mock_response = MagicMock()
    mock_response.status_code = 400
    mock_client = MagicMock(spec=Client)
    mock_client.upload_yara_file.return_value = mock_response
    results = upload_yara_file_command(mock_client, args)
    mock_getFilePath.assert_called_once_with("1")
    mock_open_file.assert_called_once_with("/path/to/file", "rb")
    assert isinstance(results, CommandResults)
    assert results.readable_output == "Upload of Yara file failed."


def test_get_events_data_command_delivered():
    """
    Given:
        - Message ID to get
    When:
        - Running get-events-data command
    Then:
        - Ensure command runs successfully
        - Ensure results are returned
    """
    from FireEyeETP import Client, get_events_data_command

    args = {"message_id": "12345"}
    mock_response = {
        "data": {"12345": [{"action_on_msg": "MTA_RCPT_DELIVERED_OUTBOUND", "display_msg": "Delivered <internetMessageId12345>"}]}
    }
    mock_client = MagicMock(spec=Client)
    mock_client.get_events_data.return_value = mock_response
    result = get_events_data_command(mock_client, args)
    assert isinstance(result, CommandResults)
    expected_output = {
        "Logs": mock_response["data"]["12345"],
        "Delivered_msg": "Delivered <internetMessageId12345>",
        "Delivered_status": "Delivered",
        "InternetMessageId": "internetMessageId12345",
    }
    assert result.outputs == expected_output
    assert result.outputs_prefix == "FireEyeETP.Events"
    expected_md = tableToMarkdown(
        "Events", expected_output, headers=["Logs", "Delivered_msg", "Delivered_status"], is_auto_json_transform=True
    )
    assert result.readable_output == expected_md


def test_get_events_data_command_failed():
    """
    Given:
        - Message ID to get
    When:
        - Running get-events-data command
    Then:
        - Ensure results are not returned
    """
    from FireEyeETP import Client, get_events_data_command

    args = {"message_id": "12345"}
    mock_response = {
        "data": {
            "12345": [
                {
                    "action_on_msg": "MTA_RCPT_DELIVERY_PERM_FAILURE_OUTBOUND",
                    "display_msg": "Failed to deliver <internetMessageId67890>",
                }
            ]
        }
    }
    mock_client = MagicMock(spec=Client)
    mock_client.get_events_data.return_value = mock_response
    result = get_events_data_command(mock_client, args)
    assert isinstance(result, CommandResults)
    expected_output = {
        "Logs": mock_response["data"]["12345"],
        "Delivered_msg": "Failed to deliver <internetMessageId67890>",
        "Delivered_status": "Failed",
    }
    assert result.outputs == expected_output
    assert result.outputs_prefix == "FireEyeETP.Events"
    expected_md = tableToMarkdown(
        "Events", expected_output, headers=["Logs", "Delivered_msg", "Delivered_status"], is_auto_json_transform=True
    )
    assert result.readable_output == expected_md


class MockResponse:
    def __init__(self, data):
        self.data = data

    def json(self):
        return self.data


def test_quarantine_release_command(mocker):
    """
    Given:
        - Message ID to get
    When:
        - Running quarantine-release command
    Then:
        - Ensure command runs successfully
        - Ensure results are returned
        - Ensure message send succesfully to quarantine
    """
    from FireEyeETP import Client, quarantine_release_command

    response_data = {"data": {"type": "some_type", "operation": "some_operation", "successful_message_ids": "1,2,3"}}

    mock_response = MockResponse(response_data)
    args = {"message_id": "12345"}
    mock_client = MagicMock(spec=Client)
    mock_client.quarantine_release.return_value = mock_response
    result = quarantine_release_command(mock_client, args)
    assert isinstance(result, CommandResults)
    assert result.readable_output == (
        "### Quarantine\n|type|operation|successful_message_ids|\n|---|---|---|\n| some_type | some_operation | 1,2,3 |\n"
    )
    mock_client.quarantine_release.assert_called_once_with("12345")


@pytest.mark.parametrize(
    "client_id, client_secret, api_key, scopes, expected_result, expected_exception",
    [
        # Case 1: SUCCESS - Full OAuth2 configuration
        ("id", "secret", "", "scope", "oauth2", None),
        # Case 2: SUCCESS - API Key configuration
        ("", "", "key", "", "api_key", None),
        # Case 3: FAILURE - Ambiguous Over-Configuration (OAuth2 + API Key)
        ("id", "secret", "key", "scope", None, r"Both OAuth2 \(Client ID/Secret\) and API Key were provided\..*"),
        # Case 4: FAILURE - Incomplete OAuth2 (Missing Scopes)
        ("id", "secret", "", "", None, r".*'OAuth Scopes' parameter is missing\..*"),
        # Case 5 (FIXED): FAILURE - Incomplete OAuth2 (Missing Secret)
        # We expect the error message for MISSING SECRET.
        ("id", "", "", "scope", None, r"Client ID provided but Client Secret is missing\..*"),
        # Case 6 (FIXED): FAILURE - Incomplete OAuth2 (Missing ID)
        # We expect the error message for MISSING ID.
        ("", "secret", "", "scope", None, r"Client Secret provided but Client ID is missing\..*"),
        # Case 7: FAILURE - No credentials provided
        ("", "", "", "", None, r"No authentication credentials provided\."),
    ],
)
def test_validate_authentication_params_parametrized(
    client_id, client_secret, api_key, scopes, expected_result, expected_exception, mocker
):
    """
    Given:
        - A set of authentication parameters (client ID, client secret, API key, scopes).
    When:
        - Calling the validate_authentication_params function.
    Then:
        - Ensure the function returns the expected authentication method ('oauth2' or 'api_key'),
          OR
        - Ensure the function raises the expected ValueError for invalid or over-configured parameters.
    """
    mocker.patch("FireEyeETP.CLIENT_ID", client_id)
    mocker.patch("FireEyeETP.CLIENT_SECRET", client_secret)
    mocker.patch("FireEyeETP.API_KEY", api_key)
    mocker.patch("FireEyeETP.SCOPES", scopes)

    from FireEyeETP import validate_authentication_params

    if expected_exception:
        with pytest.raises(ValueError, match=expected_exception):
            validate_authentication_params()
    else:
        assert validate_authentication_params() == expected_result


def test_convert_to_demisto_severity():
    from FireEyeETP import convert_to_demisto_severity
    from CommonServerPython import IncidentSeverity

    assert convert_to_demisto_severity("crit") == IncidentSeverity.CRITICAL
    assert convert_to_demisto_severity("majr") == IncidentSeverity.HIGH
    assert convert_to_demisto_severity("minr") == IncidentSeverity.LOW
    assert convert_to_demisto_severity("unkn") == IncidentSeverity.UNKNOWN


def test_get_search_alert_summary_v2():
    from FireEyeETP import get_search_alert_summary_v2

    alert = {
        "id": "1",
        "sha256": "s",
        "md5": "m",
        "domain": "d",
        "original": "o",
        "report_id": "r",
        "alert_date": "date",
        "malware": [{"name": "mn", "stype": "ms"}, {"name": "mn2", "stype": "ms2"}],
        "email_status": "es",
    }
    res = get_search_alert_summary_v2(alert)
    assert res["Alert ID"] == "1"
    assert res["Malware name"] == ["mn", "mn2"]
    assert res["Malware stype"] == ["ms", "ms2"]


def test_get_single_alert_summary_v2():
    from FireEyeETP import get_single_alert_summary_v2

    alert = {
        "id": "1",
        "domain": "d",
        "msg": "m",
        "traffic_type": "t",
        "verdict": "v",
        "report_id": "r",
        "alert_date": "ad",
        "product": "p",
        "alert": {"occurred": "o", "name": "n", "attack-time": "at", "severity": "s"},
    }
    res = get_single_alert_summary_v2(alert)
    assert res["Alert ID"] == "1"
    assert res["Severity"] == "s"


def test_get_alert_list_with_alert_id(mocker):
    """
    Given:
        - An alert_id argument is provided
    When:
        - Running get_alert_list function
    Then:
        - Ensure get_single_alert_entry is called
        - Ensure the correct CommandResults is returned
    """
    from FireEyeETP import get_alert_list, Client

    mock_client = MagicMock(spec=Client)

    alert_data = {
        "id": "alert123",
        "domain": "example.com",
        "msg": "test message",
        "traffic_type": "email",
        "verdict": "malicious",
        "report_id": "report1",
        "alert_date": "2023-01-01",
        "product": "ETP",
        "alert": {
            "occurred": "2023-01-01T10:00:00Z",
            "name": "Test Alert",
            "attack-time": "2023-01-01T09:00:00Z",
            "severity": "majr",
        },
    }

    mocker.patch("FireEyeETP.demisto.args", return_value={"alert_id": "alert123"})
    mock_client.get_alert_request_v2.return_value = alert_data

    result = get_alert_list(mock_client)

    assert isinstance(result, CommandResults)
    assert result.outputs_prefix == "FireEyeETP.Alerts"
    assert result.outputs_key_field == "id"
    assert result.outputs == alert_data


@pytest.mark.parametrize(
    "token_url_value, expected_url",
    [
        # Default commercial URL when no override is configured (TOKEN_URL stays as DEFAULT_TOKEN_URL)
        (
            "https://auth.trellix.com/auth/realms/IAM/protocol/openid-connect/token",
            "https://auth.trellix.com/auth/realms/IAM/protocol/openid-connect/token",
        ),
        # GovCloud override — user provides base URL, code appends /iam/v1.0/token via urljoin
        (
            "https://iam.us.trellix-gov.com/iam/v1.0/token",
            "https://iam.us.trellix-gov.com/iam/v1.0/token",
        ),
    ],
)
def test_fetch_oauth_token_uses_configured_url(mocker, token_url_value, expected_url):
    """
    Given:
        - A resolved TOKEN_URL (default commercial or GovCloud after urljoin).
    When:
        - Calling fetch_oauth_token.
    Then:
        - Ensure the OAuth token request is sent to the resolved URL.
    """
    mocker.patch("FireEyeETP.TOKEN_URL", token_url_value)
    mocker.patch("FireEyeETP.CLIENT_ID", "test_id")
    mocker.patch("FireEyeETP.CLIENT_SECRET", "test_secret")
    mocker.patch("FireEyeETP.SCOPES", "etp.alrt.ro")
    mocker.patch("FireEyeETP.USE_SSL", True)
    mocker.patch("FireEyeETP.get_integration_context", return_value={})
    mocker.patch("FireEyeETP.set_integration_context")

    mock_response = MagicMock()
    mock_response.json.return_value = {"access_token": "test_token", "expires_in": 600}
    mock_response.raise_for_status.return_value = None
    mock_post = mocker.patch("FireEyeETP.requests.post", return_value=mock_response)

    token = FireEyeETP.fetch_oauth_token()

    assert token == "test_token"
    mock_post.assert_called_once()
    actual_url = mock_post.call_args[0][0]
    assert actual_url == expected_url


def test_get_alert_list_without_alert_id(mocker):
    """
    Given:
        - No alert_id argument is provided (search parameters instead)
    When:
        - Running get_alert_list function
    Then:
        - Ensure get_alerts_entry is called
        - Ensure the correct CommandResults is returned with multiple alerts
    """
    from FireEyeETP import Client, get_alert_list

    mock_client = MagicMock(spec=Client)

    search_response = {
        "data": [
            {
                "id": "alert1",
                "sha256": "sha1",
                "md5": "md5_1",
                "domain": "example1.com",
                "original": "orig1",
                "report_id": "r1",
                "alert_date": "2023-01-01",
                "malware": [{"name": "malware1", "stype": "type1"}],
                "email_status": "delivered",
            },
            {
                "id": "alert2",
                "sha256": "sha2",
                "md5": "md5_2",
                "domain": "example2.com",
                "original": "orig2",
                "report_id": "r2",
                "alert_date": "2023-01-02",
                "malware": [{"name": "malware2", "stype": "type2"}],
                "email_status": "quarantined",
            },
        ]
    }

    mocker.patch("FireEyeETP.demisto.args", return_value={"limit": "10", "domain": "example.com"})
    mock_client.get_alerts_request_v2.return_value = search_response

    result = get_alert_list(mock_client)

    assert isinstance(result, CommandResults)
    assert result.outputs_prefix == "FireEyeETP.Alerts"
    assert result.outputs_key_field == "id"
    assert len(result.outputs) == 2
    assert result.outputs[0]["id"] == "alert1"
    assert result.outputs[1]["id"] == "alert2"