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 demistomock as demisto
from CommonServerPython import *

from CommonServerUserPython import *

"""
IMPORTS
"""

import base64
import copy
import json
import os
import re
import time
from datetime import datetime, UTC
import requests
import urllib3

urllib3.disable_warnings()

"""
GLOBAL VARS
"""

PARAMS = demisto.params()

ALERT_INCIDENT_TYPE_NAME = "trellix_alerts"
"""
Cloud REST APIs are rate-limited to 60 requests per minute per API route
(e.g., /trace, /alert, /quarantine) per customer.
The fetch command uses:
    - 1 request to retrieve the list of alerts
    - 1 additional request per alert to retrieve its severity
As a result, a maximum of 59 alerts can be fetched per minute.
"""
MAX_FETCHED_ALERT = min(int(PARAMS.get("incidents_per_fetch", 59)), 59)
FETCH_TIME = PARAMS.get("fetch_time", "1 minutes")

CLIENT_ID = PARAMS.get("credentials", {}).get("identifier", "")
CLIENT_SECRET = PARAMS.get("credentials", {}).get("password", "")
SCOPES = PARAMS.get(
    "oauth_scopes",
    (
        "etp.conf.ro etp.trce.rw etp.admn.ro etp.domn.ro etp.accs.rw etp.quar.rw "
        "etp.domn.rw etp.rprt.rw etp.accs.ro etp.quar.ro etp.alrt.rw etp.rprt.ro "
        "etp.conf.rw etp.trce.ro etp.alrt.ro etp.admn.rw"
    ),
).strip()
API_KEY = PARAMS.get("credentials_api_key", {}).get("password") or PARAMS.get("api_key")

DEFAULT_TOKEN_URL = "https://auth.trellix.com/auth/realms/IAM/protocol/openid-connect/token"
TOKEN_URL_SUFFIX = "/iam/v1.0/token"
_token_url_override = PARAMS.get("token_url", "").strip()
TOKEN_URL = urljoin(_token_url_override, TOKEN_URL_SUFFIX) if _token_url_override else DEFAULT_TOKEN_URL

BASE_PATH_V1 = "{}/api/v1".format(PARAMS.get("server"))  # Deprecated endpoint
BASE_PATH_V2 = "{}/api/v2".format(PARAMS.get("server"))


HTTP_HEADERS = {"Content-Type": "application/json"}
USE_SSL = not PARAMS.get("unsecure")
MESSAGE_STATUS = argToList(PARAMS.get("message_status"))

# OAuth2 token constants
OAUTH_TOKEN_KEY = "oauth_access_token"
OAUTH_EXPIRES_KEY = "oauth_token_expires_at"

"""
SEARCH ATTRIBUTES VALID VALUES
"""

REJECTION_REASONS = [
    "ETP102",
    "ETP103",
    "ETP104",
    "ETP200",
    "ETP201",
    "ETP203",
    "ETP204",
    "ETP205",
    "ETP300",
    "ETP301",
    "ETP302",
    "ETP401",
    "ETP402",
    "ETP403",
    "ETP404",
    "ETP405",
]

STATUS_VALUES = [
    "accepted",
    "deleted",
    "delivered",
    "delivered (retroactive)",
    "dropped",
    "dropped oob",
    "dropped (oob retroactive)",
    "permanent failure",
    "processing",
    "quarantined",
    "rejected",
    "temporary failure",
]

ISO_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
"""
BASIC FUNCTIONS
"""


def is_iso_utc(date_str) -> bool:
    if not date_str:
        return False
    try:
        datetime.strptime(date_str, ISO_FORMAT)
        return True
    except ValueError:
        return False


def fetch_oauth_token():
    """
    Fetch OAuth 2.0 access token
    """
    token_url = TOKEN_URL
    credentials = f"{CLIENT_ID}:{CLIENT_SECRET}"
    encoded_credentials = base64.b64encode(credentials.encode()).decode()

    headers = {"Content-Type": "application/x-www-form-urlencoded", "Authorization": f"Basic {encoded_credentials}"}

    data = {"grant_type": "client_credentials", "scope": SCOPES}

    try:
        response = requests.post(token_url, headers=headers, data=data, verify=USE_SSL)
        response.raise_for_status()

        result = response.json()
        access_token = result.get("access_token")
        expires_in = result.get("expires_in", 600)  # Default is 10 minutes

        if not access_token:
            raise DemistoException("Failed to retrieve access token from OAuth response")

        # Store token and expiration time in integration context
        expires_at = time.time() + expires_in
        ctx = get_integration_context() or {}
        ctx[OAUTH_TOKEN_KEY] = access_token
        ctx[OAUTH_EXPIRES_KEY] = expires_at
        set_integration_context(ctx)

        demisto.debug(f"OAuth token fetched successfully, expires in {expires_in} seconds")
        return access_token

    except Exception as e:
        raise DemistoException(f"OAuth authentication failed: {str(e)}")


def is_oauth_token_expired(ctx):
    """
    Check if the current OAuth token is expired or about to expire (within 60 seconds)
    """
    expires_at = ctx.get(OAUTH_EXPIRES_KEY, 0)
    current_time = time.time()

    # Consider token expired if it expires within the next 60 seconds
    is_expired = current_time >= (expires_at - 60)
    return is_expired


def get_valid_oauth_token():
    """
    Get a valid OAuth token, refreshing if necessary
    """
    ctx = get_integration_context() or {}
    access_token = ctx.get(OAUTH_TOKEN_KEY)

    if not access_token:
        demisto.debug("No OAuth token found, fetching new one")
    elif is_oauth_token_expired(ctx):
        demisto.debug("OAuth token expired, fetching new one")
    else:
        demisto.debug("Using existing valid OAuth token")
        return access_token

    return fetch_oauth_token()


def validate_authentication_params():
    """
    Validate authentication parameters and determine which method to use.
    The SCOPES parameter is required only for OAuth2.
    Returns: 'oauth2' for Client ID/Secret, 'api_key' for API Key
    Raises: ValueError if authentication configuration is invalid or over-configured.
    """
    has_client_id = bool(CLIENT_ID)
    has_client_secret = bool(CLIENT_SECRET)
    has_api_key = bool(API_KEY)
    has_scopes = bool(SCOPES)

    # 1. CHECK FOR AMBIGUOUS OVER-CONFIGURATION
    if has_client_id and has_client_secret and has_api_key:
        raise ValueError(
            "Both OAuth2 (Client ID/Secret) and API Key were provided. " "Please configure only one authentication method."
        )

    # 2. OAUTH2 VALIDATION
    if has_client_id and has_client_secret:
        # Check for required SCOPES when using OAuth2
        if not has_scopes:
            raise ValueError(
                "Client ID and Client Secret provided, but the 'OAuth Scopes' parameter is missing. "
                "Scopes are required for OAuth2 authentication."
            )

        demisto.info("Authentication: Using OAuth2 (Client ID/Secret)")
        return "oauth2"

    # 3. API KEY VALIDATION
    if has_api_key and not has_client_id and not has_client_secret:
        demisto.info("Authentication: Using API Key")
        return "api_key"

    # 4. INCOMPLETE OAUTH2 CONFIGURATION
    if has_client_id and not has_client_secret:
        raise ValueError(
            "Client ID provided but Client Secret is missing. "
            "Both Client ID and Client Secret are required for OAuth2 authentication."
        )

    if has_client_secret and not has_client_id:
        raise ValueError(
            "Client Secret provided but Client ID is missing. "
            "Both Client ID and Client Secret are required for OAuth2 authentication."
        )

    # 5. NO AUTHENTICATION METHOD PROVIDED
    raise ValueError("No authentication credentials provided.")


def get_auth_headers():
    """Get authentication headers based on available authentication method."""
    auth_method = validate_authentication_params()

    if auth_method == "oauth2":
        access_token = get_valid_oauth_token()
        return {"Content-Type": "application/json", "Authorization": f"Bearer {access_token}"}
    elif auth_method == "api_key":
        return {"Content-Type": "application/json", "x-fireeye-api-key": API_KEY}
    else:
        raise ValueError("Unknown authentication method")


class Client(BaseClient):
    def get_alerts_request_v2(self, size=None, start_time=None, end_time=None, pagination_token=None, body={}):
        """
        size (int, optional): Maximum number of alerts to return.
        start_time (str, optional): Start time in ISO 8601 UTC format (e.g., 2025-01-18T14:34:59Z).
        end_time (str, optional): End time in ISO 8601 UTC format (e.g., 2025-01-19T14:34:59Z).
        pagination_token (str, optional): Token used for pagination.
        body (dict, optional): Additional request body parameters.

        """
        url = "/public/alerts/search"
        body["sort"] = {"order": "asc"}
        if size:
            body["size"] = int(size)
        if start_time and end_time:
            body["date_range"] = {"from": start_time, "to": end_time}
        if pagination_token:
            body["sort"]["search_after"] = pagination_token

        response = self._http_request(method="POST", url_suffix=url, json_data=body)
        return response

    def get_alert_request_v2(self, alert_id):
        url = f"/public/alerts/{alert_id}"
        response = self._http_request(method="GET", url_suffix=url)
        return response

    def get_artifacts_v2(self, alert_id):
        url = f"/public/alerts/{alert_id}/casefile"
        response = self._http_request(method="GET", url_suffix=url, resp_type="response")
        return response

    def get_artifacts(self, alert_id):
        url = f"/alerts/{alert_id}/downloadzip"
        response = self._http_request(method="POST", url_suffix=url, resp_type="response")
        return response

    def get_yara_rulesets(self, policy_uuid):
        url = f"/policies/{policy_uuid}/configuration/rules/yara/rulesets"
        response = self._http_request(method="GET", url_suffix=url)
        return response

    def get_yara_file(self, policy_uuid, ruleset_uuid):
        url = f"/policies/{policy_uuid}/configuration/rules/yara/rulesets/{ruleset_uuid}/file"
        response = self._http_request(method="GET", url_suffix=url, resp_type="response")
        return response

    def upload_yara_file(self, policy_uuid, ruleset_uuid, files):
        url = f"/policies/{policy_uuid}/configuration/rules/yara/rulesets/{ruleset_uuid}/file"
        response = self._http_request(method="PUT", url_suffix=url, files=files, resp_type="response")
        return response

    def get_events_data(self, message_id):
        url = f"/events/{message_id}"

        response = self._http_request(method="GET", url_suffix=url, resp_type="json")
        return response

    def quarantine_release(self, message_id):
        url = f"/quarantine/release/{message_id}"

        response = self._http_request(method="POST", url_suffix=url, resp_type="response")
        return response


def set_proxies():
    if not PARAMS.get("proxy", False):
        # Remove proxy environment variables if they exist
        for proxy_var in ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"]:
            os.environ.pop(proxy_var, None)


def listify(comma_separated_list):
    if isinstance(comma_separated_list, list):
        return comma_separated_list
    return comma_separated_list.split(",")


def http_request(method, url, body=None, headers={}, url_params=None):
    """
    returns the http response
    """

    # Use proper authentication method
    auth_headers = get_auth_headers()
    demisto.debug(f"Headers before update: {headers}")
    headers.update(auth_headers)
    demisto.debug(f"Headers after update: {headers}")

    request_kwargs = {"headers": headers, "verify": USE_SSL}

    # add optional arguments if specified
    if body is not None:
        request_kwargs["data"] = json.dumps(body)
    if url_params is not None:
        request_kwargs["params"] = json.dumps(url_params)

    LOG(f"attempting {method} request sent to {url} with body:\n{json.dumps(body, indent=4)}")
    response = requests.request(method, url, **request_kwargs)
    # handle request failure
    if response.status_code not in range(200, 205):
        raise ValueError(f"Request failed with status code {response.status_code}\n{response.text}")
    return response.json()


def return_error_entry(message):
    entry = {
        "Type": entryTypes["error"],
        "Contents": str(message),
        "ContentsFormat": formats["text"],
    }
    demisto.results(entry)


def to_search_attribute_object(value, filter=None, is_list=False, valid_values=None):
    values = listify(value) if is_list else value
    if valid_values:
        for val in values:
            if val not in valid_values:
                raise ValueError(f"{val} is not a valid value")

    attribute = {"value": values, "includes": ["SMTP", "HEADER"]}
    if filter:
        attribute["filter"] = filter

    return attribute


def format_search_attributes(
    from_email=None,
    from_email_not_in=None,
    recipients=None,
    recipients_not_in=None,
    subject=None,
    from_accepted_date_time=None,
    to_accepted_date_time=None,
    rejection_reason=None,
    sender_ip=None,
    status=None,
    status_not_in=None,
    last_modified_date_time=None,
    domains=None,
):
    search_attributes = {}  # type: Dict

    # handle from_email attribute
    if from_email and from_email_not_in:
        raise ValueError("Only one of the followings can be specified: from_email, from_email_not_in")
    if from_email:
        search_attributes["fromEmail"] = to_search_attribute_object(from_email, filter="in", is_list=True)
    elif from_email_not_in:
        search_attributes["fromEmail"] = to_search_attribute_object(from_email_not_in, filter="not in", is_list=True)

    # handle recipients attributes
    if recipients and recipients_not_in:
        raise ValueError("Only one of the followings can be specified: recipients, recipients_not_in")
    if recipients:
        search_attributes["recipients"] = to_search_attribute_object(recipients, filter="in", is_list=True)
    elif recipients_not_in:
        search_attributes["recipients"] = to_search_attribute_object(recipients_not_in, filter="not in", is_list=True)

    # handle status attributes
    if status and status_not_in:
        raise ValueError("Only one of the followings can be specified: status, status_not_in")
    if status:
        search_attributes["status"] = to_search_attribute_object(status, filter="in", is_list=True, valid_values=STATUS_VALUES)
    elif status_not_in:
        search_attributes["status"] = to_search_attribute_object(status, filter="in", is_list=True, valid_values=STATUS_VALUES)

    if subject:
        search_attributes["subject"] = to_search_attribute_object(subject, filter="in", is_list=True)
    if rejection_reason:
        search_attributes["rejectionReason"] = to_search_attribute_object(
            rejection_reason, is_list=True, valid_values=REJECTION_REASONS
        )
    if sender_ip:
        search_attributes["senderIP"] = to_search_attribute_object(sender_ip, filter="in", is_list=True)
    if domains:
        search_attributes["domains"] = to_search_attribute_object(domains, is_list=True)
    if from_accepted_date_time and to_accepted_date_time:
        search_attributes["period"] = {
            "range": {"fromAcceptedDateTime": from_accepted_date_time, "toAcceptedDateTime": to_accepted_date_time}
        }
    if last_modified_date_time:
        # try to parse '>timestamp' | '>=timestamp' | '<timestamp' | '<=timestamp'
        operator_ends_at = 0 if last_modified_date_time.find("=") == 1 else 1
        search_attributes["lastModifiedDateTime"] = {
            "value": last_modified_date_time[operator_ends_at:],
            "filter": last_modified_date_time[:operator_ends_at],
        }
    return search_attributes


def readable_message_data(message):
    return {
        "Message ID": message["id"],
        "Accepted Time": message["acceptedDateTime"],
        "From": message["from"],
        "Recipients": message.get("recipients"),
        "Subject": message["subject"],
        "Message Status": message["status"],
    }


def message_context_data(message):
    context_data = copy.deepcopy(message)

    # remove 'attributes' level
    context_data.update(context_data.pop("attributes", {}))

    # parse email sddresses
    match = re.search("<(.*)>", context_data["senderHeader"].replace('\\"', ""))
    context_data["from"] = match.group() if match else context_data["senderHeader"]

    if context_data.get("recipientHeader") is None:
        context_data["recipients"] = []
        return context_data

    recipients = []
    for recipient_header in context_data.get("recipientHeader", []):
        match = re.search("<(.*)>", recipient_header)
        recipient_address = match.group() if match else recipient_header
        recipients.append(recipient_address)
    context_data["recipients"] = ",".join(recipients)

    return context_data


def search_messages_request(attributes={}, has_attachments=None, max_message_size=None):
    url = f"{BASE_PATH_V1}/messages/trace"
    body = {"attributes": attributes, "type": "MessageAttributes", "size": max_message_size or 20}
    if has_attachments is not None:
        body["hasAttachments"] = has_attachments
    response = http_request("POST", url, body=body, headers=HTTP_HEADERS)
    # no results
    if response["meta"]["total"] == 0:
        return []
    return response["data"]


def search_messages_command():
    args = demisto.args()
    if "size" in args:
        # parse to int
        args["size"] = int(args["size"])
    if args.get("has_attachments") is not None:
        # parse to boolean
        args["hasAttachments"] = args["hasAttachments"] == "true"

    search_attributes = format_search_attributes(
        from_email=args.get("from_email"),
        from_email_not_in=args.get("from_email_not_in"),
        recipients=args.get("recipients"),
        recipients_not_in=args.get("recipients_not_in"),
        subject=args.get("subject"),
        from_accepted_date_time=args.get("from_accepted_date_time"),
        to_accepted_date_time=args.get("to_accepted_date_time"),
        rejection_reason=args.get("rejection_reason"),
        sender_ip=args.get("sender_ip"),
        status=args.get("status"),
        status_not_in=args.get("status_not_in"),
        last_modified_date_time=args.get("last_modified_date_time"),
        domains=args.get("domains"),
    )

    # raw data
    messages_raw = search_messages_request(search_attributes, args.get("hasAttachments"), args.get("size"))

    # create context data
    messages_context = [message_context_data(message) for message in messages_raw]

    # create readable data
    messages_readable_data = [readable_message_data(message) for message in messages_context]
    md_table = tableToMarkdown("Trellix Email Security - Cloud - Search Messages", messages_readable_data, removeNull=True)

    entry = {
        "Type": entryTypes["note"],
        "Contents": messages_raw,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": md_table,
        "EntryContext": {"FireEyeETP.Messages(obj.id==val.id)": messages_context},
    }
    demisto.results(entry)


def get_message_request(message_id):
    url = f"{BASE_PATH_V1}/messages/{message_id}"
    response = http_request("GET", url)
    if response["meta"]["total"] == 0:
        return {}
    return response["data"][0]


def get_message_command():
    # get raw data
    raw_message = get_message_request(demisto.args()["message_id"])

    if raw_message:
        # create context data
        context_data = message_context_data(raw_message)

        # create readable data
        message_readable_data = readable_message_data(context_data)
        messages_md_headers = ["Message ID", "Accepted Time", "From", "Recipients", "Subject", "Message Status"]
        md_table = tableToMarkdown(
            "Trellix Email Security - Cloud - Get Message", message_readable_data, headers=messages_md_headers
        )

        entry = {
            "Type": entryTypes["note"],
            "Contents": raw_message,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": md_table,
            "EntryContext": {"FireEyeETP.Messages(obj.id==val.id)": context_data},
        }
        demisto.results(entry)
    # no results
    else:
        entry = {
            "Type": entryTypes["note"],
            "Contents": {},
            "ContentsFormat": formats["text"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": "### Trellix Email Security - Cloud - Get Message \n no results",
        }
        demisto.results(entry)


def get_search_alert_summary_v2(alert):
    return {
        "Alert ID": alert.get("id"),
        "Sha256": alert.get("sha256"),
        "md5": alert.get("md5"),
        "Domain": alert.get("domain"),
        "Original": alert.get("original"),
        "Report id": alert.get("report_id"),
        "Alert date": alert.get("alert_date"),
        "Malware name": [item.get("name") for item in alert.get("malware")],
        "Malware stype": [item.get("stype") for item in alert.get("malware")],
        "Email status": alert.get("email_status"),
    }


def alert_readable_data_summery(alert):
    return {
        "Alert ID": alert.get("id"),
        "Alert Timestamp": alert.get("alert").get("timestamp"),
        "From": alert.get("email").get("headers").get("from"),
        "Recipients": "{}|{}".format(alert.get("email").get("headers").get("to"), alert.get("email").get("headers").get("cc")),
        "Subject": alert.get("email").get("headers").get("subject"),
        "MD5": alert.get("alert").get("malware_md5"),
        "URL/Attachment": alert.get("email").get("attachment"),
        "Email Status": alert.get("email").get("status"),
        "Email Accepted": alert.get("email").get("timestamp").get("accepted"),
        "Threat Intel": alert.get("ati"),
    }


def alert_readable_data(alert):
    return {
        "Alert ID": alert.get("id"),
        "Alert Timestamp": alert.get("alert").get("timestamp"),
        "From": alert.get("email").get("headers").get("from"),
        "Recipients": "{}|{}".format(alert.get("email").get("headers").get("to"), alert.get("email").get("headers").get("cc")),
        "Subject": alert.get("email").get("headers").get("subject"),
        "MD5": alert.get("alert").get("malware_md5"),
        "URL/Attachment": alert.get("email").get("attachment"),
        "Email Status": alert.get("email").get("status"),
        "Email Accepted": alert.get("email").get("timestamp").get("accepted"),
        "Sevirity": alert.get("alert").get("severity"),
    }


def get_single_alert_summary_v2(alert):
    return {
        "Alert ID": alert.get("id"),
        "Domain": alert.get("domain"),
        "Msg": alert.get("msg"),
        "Traffic type": alert.get("traffic_type"),
        "Verdict": alert.get("verdict"),
        "Report id": alert.get("report_id"),
        "Alert date": alert.get("alert_date"),
        "Product": alert.get("product"),
        "Occurred": alert.get("alert").get("occurred"),
        "Name": alert.get("alert").get("name"),
        "Attack time": alert.get("alert").get("attack-time"),
        "Severity": alert.get("alert").get("severity"),
    }


def malware_readable_data_v2(malware):
    return {
        "Name": malware.get("name"),
        "Domain": malware.get("domain"),
        "Downloaded At": malware.get("downloaded-at"),
        "Executed At": malware.get("executed-at"),
        "Type": malware.get("stype"),
        "Submitted At": malware.get("submitted-at"),
    }


def malware_readable_data(malware):
    return {
        "Name": malware.get("name"),
        "Domain": malware.get("domain"),
        "Downloaded At": malware.get("downloaded_at"),
        "Executed At": malware.get("executed_at"),
        "Type": malware.get("stype"),
        "Submitted At": malware.get("submitted_at"),
        "SID": malware.get("sid"),
    }


def alert_context_data(alert):
    context_data = copy.deepcopy(alert)
    # remove 'attributes' level
    context_data.update(context_data.pop("attributes", {}))
    return context_data


# Deprecated endpoint
def get_alerts_request(legacy_id=None, from_last_modified_on=None, etp_message_id=None, size=None, raw_response=False):
    url = f"{BASE_PATH_V1}/alerts"

    # constract the body for the request
    body = {}
    attributes = {}
    if legacy_id:
        attributes["legacy_id"] = legacy_id
    if etp_message_id:
        attributes["etp_message_id"] = etp_message_id
    if attributes:
        body["attribute"] = attributes
    if size:
        body["size"] = size
    if from_last_modified_on:
        body["fromLastModifiedOn"] = from_last_modified_on

    response = http_request("POST", url, body=body, headers=HTTP_HEADERS)
    if raw_response:
        return response
    if response["meta"]["total"] == 0:
        return []
    return response["data"]


# Deprecated command
def get_alerts_command():
    args = demisto.args()

    if "size" in args:
        args["size"] = int(args["size"])

    if "legacy_id" in args:
        args["legacy_id"] = int(args["legacy_id"])

    # get raw data
    alerts_raw = get_alerts_request(
        legacy_id=args.get("legacy_id"),
        from_last_modified_on=args.get("from_last_modified_on"),
        etp_message_id=args.get("etp_message_id"),
        size=args.get("size"),
    )

    # create context data
    alerts_context = [alert_context_data(alert) for alert in alerts_raw]

    # create readable data
    alerts_readable_data = [alert_readable_data_summery(alert) for alert in alerts_context]
    alerts_summery_headers = [
        "Alert ID",
        "Alert Timestamp",
        "Email Accepted",
        "From",
        "Recipients",
        "Subject",
        "MD5",
        "URL/Attachment",
        "Email Status",
        "Threat Intel",
    ]
    md_table = tableToMarkdown(
        "Trellix Email Security - Cloud - Get Alerts", alerts_readable_data, headers=alerts_summery_headers
    )
    entry = {
        "Type": entryTypes["note"],
        "Contents": alerts_raw,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": md_table,
        "EntryContext": {"FireEyeETP.Alerts(obj.id==val.id)": alerts_context},
    }
    demisto.results(entry)


def upload_yara_file_command(client, args):
    entry_id = args.get("entryID")
    policy_uuid = args.get("policy_uuid")
    ruleset_uuid = args.get("ruleset_uuid")

    file_obj = demisto.getFilePath(entry_id)
    file_path = file_obj["path"]

    with open(file_path, "rb") as file:
        data = file.read()
        files = {"file": ("new.yara", data)}
        response = client.upload_yara_file(policy_uuid, ruleset_uuid, files)
        if response.status_code == 202:
            return CommandResults(readable_output="Upload of Yara file succesfully.")
        else:
            return CommandResults(readable_output="Upload of Yara file failed.")


def get_events_data_command(client, args):
    message_id = args.get("message_id")

    response = client.get_events_data(message_id)

    result_output = {}
    result_output["Logs"] = response["data"][message_id]

    for log in result_output["Logs"]:
        if log["action_on_msg"] == "MTA_RCPT_DELIVERED_OUTBOUND":
            result_output["Delivered_msg"] = log["display_msg"]
            result_output["Delivered_status"] = "Delivered"
            result_output["InternetMessageId"] = result_output["Delivered_msg"].split("<")[1].split(">")[0]
        if log["action_on_msg"] == "MTA_RCPT_DELIVERY_PERM_FAILURE_OUTBOUND":
            result_output["Delivered_msg"] = log["display_msg"]
            result_output["Delivered_status"] = "Failed"

    command_results = CommandResults(
        outputs=result_output,
        readable_output=tableToMarkdown(
            "Events", result_output, headers=["Logs", "Delivered_msg", "Delivered_status"], is_auto_json_transform=True
        ),
        outputs_prefix="FireEyeETP.Events",
    )
    return command_results


# Deprecated command
def download_alert_artifacts_command(client, args):
    alert_id = args.get("alert_id")

    response = client.get_artifacts(alert_id)
    file_entry = fileResult(alert_id + ".zip", data=response.content, file_type=EntryType.FILE)

    return [CommandResults(readable_output="Download alert artifact completed successfully"), file_entry]


def download_alert_case_files(client: Client, args):
    alert_id = args.get("alert_id")

    response = client.get_artifacts_v2(alert_id)
    file_entry = fileResult(alert_id + ".zip", data=response.content, file_type=EntryType.FILE)
    return [CommandResults(readable_output="Download alert artifact completed successfully"), file_entry]


def list_yara_rulesets_command(client, args):
    policy_uuid = args.get("policy_uuid")

    response = client.get_yara_rulesets(policy_uuid)

    command_results = CommandResults(
        outputs=response["data"]["rulesets"],
        readable_output=tableToMarkdown(
            "Rulesets", response["data"]["rulesets"], headers=["name", "description", "uuid", "yara_file_name"]
        ),
        outputs_prefix=f"FireEyeETP.Policy.{policy_uuid}",
    )

    return command_results


def download_yara_file_command(client, args):
    policy_uuid = args.get("policy_uuid")
    ruleset_uuid = args.get("ruleset_uuid")

    response = client.get_yara_file(policy_uuid, ruleset_uuid)

    file_entry = fileResult("original.yara", data=response.content, file_type=EntryType.FILE)

    return [CommandResults(readable_output="Download yara file completed successfully."), file_entry]


# Deprecated endpoint
def get_alert_request(alert_id):
    url = f"{BASE_PATH_V1}/alerts/{alert_id}"
    response = http_request("GET", url)
    if response["meta"]["total"] == 0:
        return {}
    return response["data"][0]


def create_request_body_alert_search_endpoint(args):
    body = assign_params(
        domain=argToList(args.get("domain")),
        domain_group=argToList(args.get("domain_group")),
        is_read=argToBoolean(args.get("is_read")) if args.get("is_read") else None,
        is_retro=argToBoolean(args.get("is_retro")) if args.get("is_retro") else None,
        malwarename=argToList(args.get("malwarename")),
        malwarestype=argToList(args.get("malwarestype")),
        md5=argToList(args.get("md5")),
        mta_msg_id=argToList(args.get("mta_msg_id")),
        traffic_type=args.get("traffic_type"),
        verdict=argToList(args.get("verdict")),
    )

    email_header_subject = argToList(args.get("email_header_subject"))
    if email_header_subject:
        body["email-header"] = {"subject": email_header_subject}

    return body


def quarantine_release_command(client, args):
    message_id = args.get("message_id")

    response = client.quarantine_release(message_id)

    command_results = CommandResults(
        readable_output=tableToMarkdown(
            "Quarantine", response.json()["data"], headers=["type", "operation", "successful_message_ids"]
        )
    )

    return command_results


def get_single_alert_entry(alert_id, client: Client):
    alert = client.get_alert_request_v2(alert_id)
    alert_summary = get_single_alert_summary_v2(alert)
    readable_output = tableToMarkdown("Alert Details", alert_summary)

    return CommandResults(
        readable_output=readable_output, outputs_prefix="FireEyeETP.Alerts", outputs_key_field="id", outputs=alert
    )


def get_alerts_entry(args, client: Client):
    body = create_request_body_alert_search_endpoint(args)
    size = args.get("limit")
    start_time = args.get("date_from")
    end_time = args.get("date_to")
    if start_time and not is_iso_utc(start_time):
        start_time = parse_date_range(start_time)[0].strftime("%Y-%m-%dT%H:%M:%SZ")
    if end_time and not is_iso_utc(end_time):
        end_time = parse_date_range(end_time)[0].strftime("%Y-%m-%dT%H:%M:%SZ")
    if start_time and not end_time:
        end_time = datetime.now(UTC).strftime(ISO_FORMAT)

    response = client.get_alerts_request_v2(size=size, start_time=start_time, end_time=end_time, body=body)
    alerts = response.get("data") or []
    alerts_summaries = [get_search_alert_summary_v2(alert) for alert in alerts]
    readable_output = tableToMarkdown("Trellix Email Security - Cloud - Get Alerts", alerts_summaries)

    return CommandResults(
        readable_output=readable_output, outputs_prefix="FireEyeETP.Alerts", outputs_key_field="id", outputs=alerts
    )


def get_alert_list(client: Client):
    args = demisto.args()
    alert_id = args.get("alert_id")

    # In case alert_id is provided, calling: GET /api/v2/public/alerts/<alert_id>
    if alert_id:
        command_result = get_single_alert_entry(alert_id, client)

    # alert_id is not provided, POST calling: /api/v2/public/alerts/search
    else:
        command_result = get_alerts_entry(args, client)

    return command_result


# Deprecated command
def get_alert_command():
    # get raw data
    alert_raw = get_alert_request(demisto.args()["alert_id"])
    if alert_raw:
        # create context data
        alert_context = alert_context_data(alert_raw)

        # create readable data
        readable_data = alert_readable_data(alert_context)
        alert_md_table = tableToMarkdown("Alert Details", readable_data)
        data = alert_context["alert"]["explanation"]["malware_detected"]["malware"]
        malware_data = [malware_readable_data(malware) for malware in data]
        malware_md_table = tableToMarkdown("Malware Details", malware_data)

        entry = {
            "Type": entryTypes["note"],
            "Contents": alert_raw,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": f"## Trellix Email Security - Cloud - Get Alert\n{alert_md_table}\n{malware_md_table}",
            "EntryContext": {"FireEyeETP.Alerts(obj.id==val.id)": alert_context},
        }
        demisto.results(entry)
    # no results
    else:
        entry = {
            "Type": entryTypes["note"],
            "Contents": {},
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": "### Trellix Email Security - Cloud - Get Alert\nno results",
        }
        demisto.results(entry)


def parse_string_in_iso_format_to_datetime(iso_format_string):
    alert_last_modified = None
    try:
        alert_last_modified = datetime.strptime(iso_format_string, "%Y-%m-%dT%H:%M:%S.%f")
    except ValueError:
        try:
            alert_last_modified = datetime.strptime(iso_format_string, "%Y-%m-%dT%H:%M:%S")
        except ValueError:
            alert_last_modified = datetime.strptime(iso_format_string, "%Y-%m-%dT%H:%M")
    return alert_last_modified


def convert_to_demisto_severity(severity: str) -> int:
    """
    Converts the FireEyeETP alert severity level ('Low', 'Medium', 'High', 'Critical') to Cortex XSOAR alert
    severity (1 to 4).

    Args:
        severity (str): severity as returned from the FireEyeETP API.

    Returns:
        int: Cortex XSOAR Severity (1 to 4)
    """

    return {
        "crit": IncidentSeverity.CRITICAL,
        "majr": IncidentSeverity.HIGH,
        "minr": IncidentSeverity.LOW,
        "unkn": IncidentSeverity.UNKNOWN,
    }.get(severity, IncidentSeverity.UNKNOWN)


def parse_alert_to_incident(response):
    """
    Creates an incident from an alert with proper field mapping.

    Args:
        alert: The alert from the search API (first API call)
        detailed_alert: The detailed alert from the get alert API (second API call)

    Returns:
        Incident dictionary with mapped fields
    """
    occurred = arg_to_datetime(arg=response.get("alert", {}).get("occurred", ""))
    occurred = occurred.strftime(ISO_FORMAT) if occurred else None

    severity = response.get("alert", {}).get("severity")
    return {
        "name": response.get("id", ""),
        "occurred": occurred,
        "severity": convert_to_demisto_severity(severity),
        "rawJSON": json.dumps(response),
        "incident_type": ALERT_INCIDENT_TYPE_NAME,
    }


def fetch_incidents(client: Client):
    last_run = demisto.getLastRun()
    demisto.debug(f"[FireEyeETP - fetch_incidents] last_run before fetching:\n{last_run}")

    # The start time does not change, progress happens using the pagination token.
    start_time_dt = arg_to_datetime(FETCH_TIME)
    start_time = start_time_dt.strftime("%Y-%m-%dT%H:%M:%SZ") if start_time_dt else None
    now_utc = datetime.now(UTC).strftime(ISO_FORMAT)
    pagination_token = last_run.get("pagination_token")

    demisto.debug("[FireEyeETP - fetch_incidents] calling /api/v2/public/alerts/search")
    alerts_response = client.get_alerts_request_v2(
        size=MAX_FETCHED_ALERT, start_time=start_time, end_time=now_utc, pagination_token=pagination_token
    )

    # When no alerts are found, there is no search_after token in the response.
    if not alerts_response or not alerts_response.get("data"):
        demisto.debug("[FireEyeETP - fetch_incidents] No incident found.")
        return [], last_run

    pagination_token = alerts_response.get("meta", {}).get("search_after")
    if pagination_token:
        demisto.debug(f"[FireEyeETP - fetch_incidents] response includes a pagination token.\n{pagination_token}")
        last_run["pagination_token"] = pagination_token

    total_alert_fetched = alerts_response.get("meta", {}).get("size")
    demisto.debug(
        f"[FireEyeETP - fetch_incidents] Total incidents fetched from /api/v2/public/alerts/search: {total_alert_fetched}."
    )

    alerts = alerts_response.get("data", [])
    incidents = []
    for alert in alerts:
        alert_id = alert.get("id")
        email_status = alert.get("email_status")

        if MESSAGE_STATUS and email_status not in MESSAGE_STATUS:
            demisto.debug(f"[FireEyeETP - fetch_incidents] alert: {alert_id} with status {email_status} filtered out.")
            continue

        demisto.debug(f"[FireEyeETP - fetch_incidents] calling /api/v2/public/alerts/{alert_id}")
        alert_info = client.get_alert_request_v2(alert_id)
        incidents.append(parse_alert_to_incident(alert_info))

    if not incidents:
        demisto.debug("[FireEyeETP - fetch_incidents] all alerts filtered out, 0 alerts left.")
        return [], last_run

    demisto.debug(f"[FireEyeETP - fetch_incidents] Total incidents left after filtering: {len(incidents)}.")
    return incidents, last_run


def test_module(client: Client):
    params = demisto.params()
    try:
        if params.get("isFetch"):
            fetch_incidents(client)
        else:
            client.get_alerts_request_v2(size=1)

    except Exception as e:
        return f"test-module failed: {str(e)}"
    return "ok"


def main():
    """
    main function, parses params and runs command functions
    """

    params = demisto.params()
    args = demisto.args()
    proxy = params.get("proxy", False)

    verify_certificate = not params.get("unsecure", False)

    set_proxies()

    try:
        # Validate authentication configuration
        validate_authentication_params()
        headers = get_auth_headers()

        command = demisto.command()

        if command == "test-module":
            client = Client(base_url=BASE_PATH_V2, verify=verify_certificate, headers=headers, proxy=proxy)
            result = test_module(client)
            return_results(result)
        elif command == "fetch-incidents":
            client = Client(base_url=BASE_PATH_V2, verify=verify_certificate, headers=headers, proxy=proxy)
            incidents, last_run = fetch_incidents(client)
            demisto.setLastRun(last_run)
            demisto.incidents(incidents)
        elif command == "fireeye-etp-search-messages":
            search_messages_command()
        elif command == "fireeye-etp-get-message":
            get_message_command()
        elif command == "fireeye-etp-get-alerts":
            get_alerts_command()
        elif command == "fireeye-etp-get-alert":
            get_alert_command()
        elif command == "fireeye-etp-list-alerts":
            client = Client(base_url=BASE_PATH_V2, verify=verify_certificate, headers=headers, proxy=proxy)
            return_results(get_alert_list(client))
        elif command == "fireeye-etp-download-alert-artifact":
            client = Client(base_url=BASE_PATH_V1, verify=verify_certificate, headers=headers, proxy=proxy)
            return_results(download_alert_artifacts_command(client, args))
        elif command == "fireeye-etp-download-alert-case-files":
            client = Client(base_url=BASE_PATH_V2, verify=verify_certificate, headers=headers, proxy=proxy)
            return_results(download_alert_case_files(client, args))
        elif command == "fireeye-etp-list-yara-rulesets":
            client = Client(base_url=BASE_PATH_V1, verify=verify_certificate, headers=headers, proxy=proxy)
            return_results(list_yara_rulesets_command(client, args))
        elif command == "fireeye-etp-download-yara-file":
            client = Client(base_url=BASE_PATH_V1, verify=verify_certificate, headers=headers, proxy=proxy)
            return_results(download_yara_file_command(client, args))
        elif command == "fireeye-etp-upload-yara-file":
            client = Client(base_url=BASE_PATH_V1, verify=verify_certificate, headers=headers, proxy=proxy)
            return_results(upload_yara_file_command(client, args))
        elif command == "fireeye-etp-get-events-data":
            client = Client(base_url=BASE_PATH_V1, verify=verify_certificate, headers=headers, proxy=proxy)
            return_results(get_events_data_command(client, args))
        elif command == "fireeye-etp-quarantine-release":
            client = Client(base_url=BASE_PATH_V1, verify=verify_certificate, headers=headers, proxy=proxy)
            return_results(quarantine_release_command(client, args))
    except ValueError as e:
        LOG(e)
        LOG.print_log()
        return_error_entry(e)


if __name__ in ("__main__", "__builtin__", "builtins"):
    main()