Agari Phishing Defense

Agari Phishing Defense stops phishing, BEC, and other identity deception attacks that trick employees into harming your business.

Data Enrichment & Threat Intelligence · Agari Phishing Defense

Details

IDAgari Phishing Defense
ProviderFortra
CategoryData Enrichment & Threat Intelligence
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM EDR Cortex Cloud Cloud Runtime Security

README

Agari Phishing Defense stops phishing, BEC, and other identity deception attacks that trick employees into harming your business.
This integration was integrated and tested with a standard version of Agari Phishing Defense.

Configure Agari Phishing Defense in Cortex

Parameter Description Required
url URL to connect to Agari True
apikey API Key True
apisecret Secret Key True
max_fetch Maximum number of incidents to fetch every time True
first_fetch First fetch time interval False
fetch_policy_actions Policy Action False
exclude_alert_type Exclude alerts False
policy_filter Fetches policy events to limit the amount of data. Can be applied to specific fields False
incidentType Incident type False
isFetch Fetch incidents False
insecure Trust any certificate (not secure) False
proxy Use system proxy settings False

Commands

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

apd-list-policy-events


Retrieves a list of policy events.

Base Command

apd-list-policy-events

Input

Argument Name Description Required
limit The maximum number of items to be returned in the paged response. Optional
start_date The earliest date time (UTC) a search should target (ISO 8601 format).
Formats accepted: YYYY-MM-dd
YYYY-MM-ddTHH:mm:ss
N days
N hours
Example: 2020-05-01
2020-05-01T00:00:00
2 days
5 hours.
Optional
end_date The latest date time (UTC) a search should target (ISO 8601 format).
Formats accepted: YYYY-MM-dd
YYYY-MM-ddTHH:mm:ss
N days
N hours
Example: 2020-05-01
2020-05-01T00:00:00
2 days
5 hours.
Optional
page_id To page through a collection of policy events. Optional
sort A comma-delimited string that specifies the field ordering to be applied to the response. Example: created_at DESC, id ASC. Optional
add_fields A comma-delimited list of optional fields to add to the default payload. Additional fields would add data in the entry context. Optional
rem_fields A comma-delimited list of fields to remove from the default payload. Limited fields would return limited data in entry context. Optional
fields A comma-delimited list of fields to include in the payload. Limited fields would return limited data in entry context. Optional
filter Search filters that can be applied to the response. Optional
exclude_alert_types Exclude policy types such as ‘MessageAlert’ or ‘SystemAlert’. Optional
policy_name Find by policy name. Optional
policy_action Filter by policy action: ‘deliver’, ‘mark-spam’, ‘move’, ‘inbox’, ‘delete’ and ‘none’. Optional

Context Output

Path Type Description
AgariPhishingDefense.Alert.alert_definition_name String Alert definition name.
AgariPhishingDefense.Alert.id String Unique alert id.
AgariPhishingDefense.Alert.summary String Summary of the alert.
AgariPhishingDefense.Alert.policy_action String Alert policy action.
AgariPhishingDefense.Alert.policy_enabled Boolean Indicates if policy is enabled.
AgariPhishingDefense.Alert.updated_at Date Updated time of the alert. The format is ISO8601.
AgariPhishingDefense.Alert.created_at Date Created time of the alert. The format is ISO8601.
AgariPhishingDefense.Alert.admin_recipients Unknown List of notified admin recipients.
AgariPhishingDefense.Alert.notified_original_recipients Boolean Indicates whether the original recipient was notified.

Command Example

!apd-list-policy-events limit=2

Context Example

{
    "AgariPhishingDefense": {
        "Alert": [
            {
                "alert_definition_name": "Spoof of Partner Domains",
                "created_at": "2020-12-03T04:32:23Z",
                "id": 549904303,
                "notified_original_recipients": false,
                "policy_action": "none",
                "policy_enabled": true,
                "summary": false,
                "updated_at": "2020-12-03T04:32:23Z"
            },
            {
                "alert_definition_name": "Untrusted Messages",
                "created_at": "2020-12-03T04:32:23Z",
                "id": 549904302,
                "notified_original_recipients": false,
                "policy_action": "none",
                "policy_enabled": true,
                "summary": false,
                "updated_at": "2020-12-03T04:32:23Z"
            }
        ]
    }
}

Human Readable Output

Policy Events

Event ID Alert Definition Name Policy Action Notified Original Recipients Created Updated
549904303 Spoof of Partner Domains none false 2020-12-03T04:32:23Z 2020-12-03T04:32:23Z
549904302 Untrusted Messages none false 2020-12-03T04:32:23Z 2020-12-03T04:32:23Z

apd-list-message-data


Retrieves a list of messages.

Base Command

apd-list-message-data

Input

Argument Name Description Required
start_date The earliest date time (UTC) a search should target (ISO 8601 format).
Formats accepted: YYYY-MM-dd
YYYY-MM-ddTHH:mm:ss
N days
N hours
Example: 2020-05-01
2020-05-01T00:00:00
2 days
5 hours.
Optional
end_date The latest date time (UTC) a search should target (ISO 8601 format).
Formats accepted: YYYY-MM-dd
YYYY-MM-ddTHH:mm:ss
N days
N hours
Example: 2020-05-01
2020-05-01T00:00:00
2 days
5 hours.
Optional
add_fields A comma-delimited list of optional fields to add to the default payload. Additional fields would add data in the entry context. Optional
rem_fields A comma-delimited list of fields to remove from the default payload. Limited fields would return limited data in entry context. Optional
fields A comma-delimited list of fields to include in the payload. Limited fields would return limited data in entry context. Optional
limit The maximum number of items to be returned in the paged response. Optional
page_id To page through a collection of message data. Optional
sort A comma-delimited string that specifies the field ordering to be applied to the response. Optional
search Search using advanced search syntax.
Format: field operator operand {and/or field operator operand}
Example: has_attachment=true and ip=’10.0.0.0’
sbrs in [3.5, 2.6]
domain_reputation is not null
sbrs gt 3
sbrs>=3 and domain_tags eq internal
Optional

Context Output

Path Type Description
AgariPhishingDefense.Message.has_attachment Boolean Has attachment.
AgariPhishingDefense.Message.ip String IP address.
AgariPhishingDefense.Message.message_id String The Global message ID.
AgariPhishingDefense.Message.ptr_name String PTR name.
AgariPhishingDefense.Message.sbrs String SBRS.
AgariPhishingDefense.Message.id String The internal message ID.
AgariPhishingDefense.Message.authenticity Number Authenticity score.
AgariPhishingDefense.Message.to String Message recipient.
AgariPhishingDefense.Message.date String Date in ISO format.
AgariPhishingDefense.Message.timestamp_ms Number Timestamp in ms since epoch.
AgariPhishingDefense.Message.from String Sender’s email.
AgariPhishingDefense.Message.from_domain String From domain.
AgariPhishingDefense.Message.subject String Message subject.
AgariPhishingDefense.Message.domain_reputation Number Reputation of sender domain.
AgariPhishingDefense.Message.message_trust_score Number Risk score.
AgariPhishingDefense.Message.message_details_link String Link to message details.
AgariPhishingDefense.Message.domain_tags Unknown List of domain tags.
AgariPhishingDefense.Message.mail_from String Mail from domain.
AgariPhishingDefense.Message.reply_to String Reply-to address.
AgariPhishingDefense.Message.uris Unknown List of URIs.
AgariPhishingDefense.Message.attachment_extensions Unknown List of message attachment extensions.
AgariPhishingDefense.Message.attachment_filenames Unknown List of message attachment filenames.
AgariPhishingDefense.Message.attachment_sha256 Unknown List of message attachment SHA256 hashes.
AgariPhishingDefense.Message.attachment_types Unknown List of message attachment types.
AgariPhishingDefense.Message.attack_types Unknown List of attack type classifications.
AgariPhishingDefense.Message.dkim_result String DKIM result.
AgariPhishingDefense.Message.dmarc_result String DMARC result.
AgariPhishingDefense.Message.domain_dmarc_policy String DMARC policy for domain.
AgariPhishingDefense.Message.enforcement_action String Enforcement action.
AgariPhishingDefense.Message.enforcement_folder String Enforcement folder.
AgariPhishingDefense.Message.enforcement_result String Enforcement result.
AgariPhishingDefense.Message.expanded_from String Expanded from.
AgariPhishingDefense.Message.forwarded_from String Forwarded from.
AgariPhishingDefense.Message.has_malicious_attachment Boolean Has malicious attachment.
AgariPhishingDefense.Message.message_read_status Boolean Message read status.
AgariPhishingDefense.Message.org_domain String Organization domain.
AgariPhishingDefense.Message.policy_ids Unknown List of triggered policy IDs.
AgariPhishingDefense.Message.sender_approval_state String Sender approval state.
AgariPhishingDefense.Message.sender_type String Sender type.
AgariPhishingDefense.Message.spf_result String SPF result.

Command Example

!apd-list-message-data limit=2

Context Example

{
    "AgariPhishingDefense": {
        "Message": [
            {
                "attack_types": [
                    "spoof (Domain spoof)"
                ],
                "authenticity": "0.085819915",
                "date": "2020-12-03T02:07:02+00:00",
                "domain_reputation": "9.0",
                "domain_tags": [
                    "partner"
                ],
                "enforcement_action": "move",
                "enforcement_result": "pending",
                "from": "Accounts@abc.com",
                "from_domain": "abc.com",
                "has_attachment": "false",
                "id": "785d91a8-34fb-11eb-bf90-f6ba445dac4f",
                "ip": "1.2.3.4",
                "message_details_link": "https://apis.com/messages/785d91a8-34fb-11eb-bf90-f6ba445dac4f",
                "message_id": "<facade2c8712345c91c755d17f1134cb@BY2PR12MB0054.abc.com>",
                "message_trust_score": "0.6",
                "policy_ids": [
                    9014,
                    9008,
                    2843446
                ],
                "subject": "Please approve and forward expense report \"December Expenses\"",
                "timestamp_ms": "1606961222000",
                "to": "acoyle@xyz.com"
            },
            {
                "attack_types": [
                    "spoof (Domain spoof)"
                ],
                "authenticity": "0.07902577",
                "date": "2020-12-03T02:07:02+00:00",
                "domain_reputation": "8.6",
                "domain_tags": [
                    "internal"
                ],
                "enforcement_action": "move",
                "enforcement_result": "pending",
                "from": "help@xyz.com",
                "from_domain": "xyz.com",
                "has_attachment": "false",
                "id": "7852dc68-34fb-11eb-bf90-f6ba445dac4f",
                "ip": "2.2.3.4",
                "message_details_link": "https://apis.com/messages/7852dc68-34fb-11eb-bf90-f6ba445dac4f",
                "message_id": "<facade2c12345f9c91c755d17f1134cb@BY2PR12MB0054.xyz.com>",
                "message_trust_score": "0.8",
                "policy_ids": [
                    9014,
                    9009,
                    2843455
                ],
                "subject": "Please approve and forward expense report \"December Expenses\"",
                "timestamp_ms": "1606961222000",
                "to": "aarmstrong@xyz.com"
            }
        ]
    }
}

Human Readable Output

Messages

ID From To Subject Message Trust Score Domain Reputation IP Authenticity Attack Types Date
785d91a8-34fb-11eb-bf90-f6ba445dac4f Accounts@abc.com acoyle@xyz.com Please approve and forward expense report “December Expenses” 0.6 9.0 1.2.3.4 0.085819915 spoof (Domain spoof) 2020-12-03T02:07:02+00:00
7852dc68-34fb-11eb-bf90-f6ba445dac4f help@xyz.com aarmstrong@xyz.com Please approve and forward expense report “December Expenses” 0.8 8.6 2.2.3.4 0.07902577 spoof (Domain spoof) 2020-12-03T02:07:02+00:00

apd-remediate-message


Remediate suspected message.

Base Command

apd-remediate-message

Input

Argument Name Description Required
id The primary identifier to remediate a message (UUID). To retrieve the ID execute the apd-list-message-data command. Required
operation An operation to remediate a message. Remediation operation is either ‘delete’ or ‘move’. Required

Context Output

There is no context output for this command.

Command Example

!apd-remediate-message id="0e43a684-2e0e-11eb-815a-0a8f2da72108" operation="move"

Context Example

{}

Human Readable Output

Message ID - 0e43a684-2e0e-11eb-815a-0a8f2da72108 remediated successfully with operation ‘move’.

Configuration parameters

  • url — URL to connect to Agari (required)
  • apikey — API Key (required)
  • apisecret — Secret Key (required)
  • max_fetch — Maximum number of incidents to fetch every time (required)
  • first_fetch — First fetch time interval
  • fetch_policy_actions — Policy action taken
  • exclude_alert_type — Exclude alerts
  • policy_filter — Fetches policy events as per the filter to limit the amount of data. Can be applied to specific fields.
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • isFetch — Fetch incidents
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (3)

  • apd-list-message-data

    Retrieves a list of messages.

  • apd-list-policy-events

    Retrieves a list of policy events.

  • apd-remediate-message

    Remediate suspected message.

import demistomock as demisto
from CommonServerPython import *
from CommonServerUserPython import *

""" IMPORTS """

import dateparser
from requests import Response
from typing import Any
from requests.exceptions import (
    MissingSchema,
    InvalidSchema,
    InvalidURL,
    SSLError,
)
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
import urllib3

urllib3.disable_warnings()

""" CONSTANTS """

MAX_LIMIT_FOR_EVENT = 200
MAX_LIMIT_FOR_MESSAGE = 1000
DEFAULT_FETCH_LIMIT = "50"
DEFAULT_FIRST_FETCH = "12 hours"
DEFAULT_SESSION_TIMEOUT = 30
TOKEN_EXPIRY_TIMEOUT = 60 * 60 * 4
CONTENT_TYPE_JSON = "application/json"
DATE_FORMAT_OF_YEAR_MONTH_DAY = "%Y-%m-%d"
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
DATE_FORMAT_WITH_MICROSECOND = "%Y-%m-%dT%H:%M:%S.%fZ"
API_SUPPORT_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.000-00:00"
API_VERSION = "v1"
MAX_WORKERS = 5
TOKEN_TIME_DIFF = 60
INTEGRATION_VERSION = "v1.0"

URL_SUFFIX: dict[str, str] = {
    "GET_TOKEN": "/token",
    "GET_EVENTS": "/policy_events",
    "REMEDIATE_MSG": "/messages/{}/remediate",
    "GET_MESSAGES": "/messages",
}

MESSAGES: dict[str, str] = {
    "BAD_REQUEST_ERROR": "An error occurred while fetching the data.",
    "AUTHENTICATION_ERROR": "Unauthenticated. Check the configured API Key and Secret Key.",
    "PROXY_ERROR": "Proxy Error - cannot connect to proxy. Either try clearing the 'Use system proxy' check-box or "
    "check the host, authentication details and connection details for the proxy.",
    "SSL_CERT_ERROR": "SSL Certificate Verification Failed - try selecting 'Trust any certificate' checkbox in the "
    "integration configuration.",
    "INTERNAL_SERVER_ERROR": "The server encountered an internal error for Agari and was unable to complete your request.",
    "MISSING_SCHEMA_ERROR": "Invalid API URL. No schema supplied: http(s).",
    "INVALID_SCHEMA_ERROR": "Invalid API URL. Supplied schema is invalid, supports http(s).",
    "INVALID_API_URL": "Invalid API URL.",
    "CONNECTION_ERROR": "Connectivity failed. Check your internet connection or the API URL.",
    "NO_RECORDS_FOUND": "No {} were found for the given argument(s).",
    "FETCH_LIMIT_VALIDATION": "Value of Fetch Limit must be a positive integer between 1 to 200.",
    "REQUEST_TIMEOUT": "Request timed out. Check the configured HTTP(S) Request Timeout (in seconds) value.",
    "INVALID_TIME_VALIDATION": "The given value for {0} argument is invalid.",
    "INVALID_POLICY_ACTION_TYPE": 'The given value for Policy Actions is invalid. Expected "deliver", "mark-spam", '
    '"move", "inbox", "delete" or "none". ',
    "INVALID_LIMIT": "Argument limit must be a positive integer between 1 to {}.",
    "INVALID_PAGE_ID": "Argument page_id must be a positive integer.",
    "INVALID_REM_FIELDS": 'Cannot pass "id" in rem_fields argument.',
    "MISSING_REMEDIATE_ARGS": 'Invalid argument value. Requires both "id" and "operation" argument.',
    "INVALID_EXCLUDE_ALERT_TYPE": 'The given value for Exclude Alerts is invalid. Expected "System Alert" or '
    '"Message Alert". ',
}

HR_MESSAGES: dict[str, str] = {"REMEDIATE_MSG_SUCCESS": "Message ID - {} remediated successfully with operation '{}'."}


def strip_blank(args: dict) -> dict:
    nargs = {}
    for key, value in args.items():
        if isinstance(value, str):
            value = value.strip()
        nargs[key] = value
    return nargs


""" CLIENT CLASS """


class Client(BaseClient):
    def __init__(
        self,
        base_url: str,
        verify: bool,
        proxy: bool,
        request_timeout: int,
        payload,
    ):
        """
        Initialization of HTTP request timeout, payload(i.e client id and client secret)
        Initialization of lock object and create a token for the first time if not available in context

        :param base_url: Base url of Endpoint
        :param verify: Whether the request should verify the SSL certificate.
        :param proxy: Whether to run the integration using the system proxy.
        :param request_timeout: Time of Request Timeout
        :param payload: Payload data (i.e client id and client secret)
        """
        super().__init__(base_url=base_url, verify=verify, proxy=proxy)
        self.request_timeout = request_timeout
        self.payload = payload
        self.lock = threading.Lock()
        self.api_token, self.api_token_valid_until = self.get_api_token()

    def http_request(
        self,
        method: str,
        url_suffix: str,
        json_data=None,
        params=None,
        headers=None,
        data=None,
    ):
        """
            Override http_request method from BaseClient class. This method will print an error based on status code
            and exceptions.

        :type method: ``str``
        :param method: The HTTP method, for example: GET, POST, and so on.

        :type url_suffix: ``str``
        :param url_suffix: The API endpoint.

        :type json_data: ``dict``
        :param json_data: The dictionary to send in a 'POST' request.

        :type params: ``dict``
        :param params: URL parameters to specify the query.

        :type headers: ``dict``
        :param headers: Headers to send in the request. If None, will use self._headers.

        :return: Depends on the resp_type parameter
        :rtype: ``dict`` or ``str`` or ``requests.Response``
        """

        resp = Response()
        try:
            if url_suffix != URL_SUFFIX["GET_TOKEN"]:
                with self.lock:
                    if int(time.time() + TOKEN_TIME_DIFF) >= self.api_token_valid_until:
                        (
                            self.api_token,
                            self.api_token_valid_until,
                        ) = self.get_api_token()
            # pylint: disable=E1101
            headers["User-Agent"] = (
                "AgariDemisto APDIntegration/" + INTEGRATION_VERSION + " DemistoServer/" + demisto.demistoVersion()["version"]
            )  # type: ignore[attr-defined]
            # pylint: enable=E1101
            resp = super()._http_request(
                method=method,
                url_suffix=url_suffix,
                json_data=json_data,
                params=params,
                headers=headers,
                resp_type="response",
                timeout=self.request_timeout,
                ok_codes=(200, 201),
                error_handler=self.handle_error_response,
                data=data,
            )
        except MissingSchema:
            raise ValueError(MESSAGES["MISSING_SCHEMA_ERROR"])
        except InvalidSchema:
            raise ValueError(MESSAGES["INVALID_SCHEMA_ERROR"])
        except InvalidURL:
            raise ValueError(MESSAGES["INVALID_API_URL"])
        except DemistoException as e:
            self.handle_demisto_exception(e)

        if resp.ok:
            content_type = resp.headers.get("Content-Type", "")
            if content_type.__contains__(CONTENT_TYPE_JSON):
                # Handle empty response
                if resp.text == "":
                    return resp
                else:
                    return resp.json()
        return resp

    @staticmethod
    def handle_demisto_exception(e) -> None:
        """
        Handle Demisto exception based on string.

        :param e: Demisto Exception
        :return: Error message
        """
        if "Proxy Error" in str(e):
            raise ConnectionError(MESSAGES["PROXY_ERROR"])
        elif "ReadTimeoutError" in str(e):
            raise ConnectionError(MESSAGES["REQUEST_TIMEOUT"])
        elif "ConnectionError" in str(e) or "ConnectTimeoutError" in str(e):
            raise ConnectionError(MESSAGES["CONNECTION_ERROR"])
        elif "SSLError" in str(e):
            raise SSLError(MESSAGES["SSL_CERT_ERROR"])
        else:
            raise e

    @staticmethod
    def handle_error_response(resp) -> None:
        """
        Handle error response and display user specific error message based on status code.

        :param resp: response from API.
        :return: raise DemistoException based on status code.
        """

        error_message = ""
        error_message_with_reason = ""
        try:
            json_resp = resp.json()
            error_message = json_resp["error"]
            error_message_with_reason = json_resp["error_description"]
        except Exception:  # ignoring json parsing errors
            pass

        status_code_messages = {
            400: f"{MESSAGES['BAD_REQUEST_ERROR']} {error_message_with_reason}",
            401: MESSAGES["AUTHENTICATION_ERROR"],
            403: error_message,
            404: f"{MESSAGES['BAD_REQUEST_ERROR']} {error_message_with_reason}",
            406: error_message,
            407: MESSAGES["PROXY_ERROR"],
            500: MESSAGES["INTERNAL_SERVER_ERROR"],
            503: MESSAGES["INTERNAL_SERVER_ERROR"],
        }

        if resp.status_code in status_code_messages:
            demisto.debug(f"Response Code: {resp.status_code}, Reason: {status_code_messages[resp.status_code]}")
            raise DemistoException(status_code_messages[resp.status_code])
        else:
            raise DemistoException(resp.raise_for_status())

    @staticmethod
    def set_integration_context(resp) -> dict:
        """
        set api token and expiry time in integration configuration context.
        Will raise value error if api-token is not found.

        :param resp: resp from API.
        :return: integration context
        """

        integration_context = demisto.getIntegrationContext()
        api_token = resp["access_token"]
        if api_token:
            integration_context["api_token"] = "Bearer " + api_token
            integration_context["valid_until"] = int(time.time() + TOKEN_EXPIRY_TIMEOUT)
        else:
            raise ValueError("No api token found. Please try again")
        demisto.setIntegrationContext(integration_context)
        return integration_context

    def get_api_token(self):
        """
        Retrieve new api token and set it to integration context.
        if api token is not not found or expired, making api call to retrieve api token and set it to integration
        context.

        :return: api-token, validity of token
        """
        integration_context = demisto.getIntegrationContext()
        api_token = integration_context.get("api_token")
        valid_until = integration_context.get("valid_until")

        # Return api token from integration context, if found and not expired
        if api_token and valid_until and time.time() + TOKEN_TIME_DIFF < valid_until:
            demisto.debug("Retrieved api-token from integration cache.")
            return api_token, valid_until

        headers = {
            "Accept": CONTENT_TYPE_JSON,
            "Content-Type": "application/x-www-form-urlencoded",
        }

        demisto.debug("Calling authentication API for retrieve api-token")
        resp = self.http_request(
            method="POST",
            url_suffix=URL_SUFFIX["GET_TOKEN"],
            headers=headers,
            data=self.payload,
        )

        integration_context = self.set_integration_context(resp)

        return integration_context.get("api_token"), int(integration_context.get("valid_until", 0))


def get_fetch_limit(fetch_limit) -> int:
    """
    Retrieve fetch limit from demisto arguments and validate it.
    Will raise ValueError if inappropriate input given.

    :param fetch_limit: The maximum number of incident want to fetch.
    :return: fetch limit
    """
    fetch_limit = fetch_limit if fetch_limit else DEFAULT_FETCH_LIMIT
    try:
        fetch_limit_int = int(fetch_limit)
        if not 1 <= fetch_limit_int <= 200:
            raise ValueError
    except ValueError:
        raise ValueError(MESSAGES["FETCH_LIMIT_VALIDATION"])

    return fetch_limit_int


def validate_fetch_policy_action(fetch_policy_action) -> bool:
    """
    Validate fetch policy action.

    :param fetch_policy_action: A list contain policy actions which user want to fetch.
    :return: True if it is valid
    """
    policy_actions = [
        "deliver",
        "mark-spam",
        "move",
        "inbox",
        "delete",
        "none",
    ]
    if fetch_policy_action == "" or fetch_policy_action is None:
        return True

    if fetch_policy_action not in policy_actions:
        raise ValueError(MESSAGES["INVALID_POLICY_ACTION_TYPE"])
    return False


def validate_exclude_alert_type(exclude_alert_type) -> bool:
    """
    Validate exclude alert type.

    :param exclude_alert_type: A list contain exclude alert types which user want to exclude.
    :return: True if it is valid
    """

    exclude_alert_types = ["System Alert", "Message Alert"]
    if exclude_alert_type == "" or exclude_alert_type is None:
        return True

    if exclude_alert_type not in exclude_alert_types:
        raise ValueError(MESSAGES["INVALID_EXCLUDE_ALERT_TYPE"])
    return False


def prepare_hr_for_events(events_info) -> str:
    """
    Prepare the Human readable info for events command.

    :param events_info: The events data.
    :return: Human readable.
    """
    hr_list = []
    for record in events_info:
        hr_record = {
            "Event ID": record.get("id", None),
            "Alert Definition Name": record.get("alert_definition_name", ""),
            "Created": record.get("created_at", ""),
            "Updated": record.get("updated_at", ""),
            "Policy Action": record.get("policy_action", ""),
            "Notified Original Recipients": record.get("notified_original_recipients", ""),
            "Admin Recipients": record.get("admin_recipients", ""),
        }
        hr_list.append(hr_record)

    return tableToMarkdown(
        "Policy Events",
        hr_list,
        [
            "Event ID",
            "Alert Definition Name",
            "Policy Action",
            "Notified Original Recipients",
            "Admin Recipients",
            "Created",
            "Updated",
        ],
        removeNull=True,
    )


def prepare_hr_for_messages(messages_info) -> str:
    """
    Prepare the Human readable info for messages data command.

    :param messages_info: The messages data.
    :return: Human readable.
    """
    hr_list = []
    for record in messages_info:
        hr_record = {
            "ID": record.get("id", ""),
            "From": record.get("from", ""),
            "To": record.get("to", ""),
            "Subject": record.get("subject", ""),
            "Message Trust Score": record.get("message_trust_score", ""),
            "Domain Reputation": record.get("domain_reputation", ""),
            "IP": record.get("ip", ""),
            "Authenticity": record.get("authenticity", ""),
            "Attachment Filenames": record.get("attachment_filenames", ""),
            "Attachment sha256": record.get("attachment_sha256", ""),
            "Attack Types": record.get("attack_types", ""),
            "Date": record.get("date", ""),
        }
        hr_list.append(hr_record)

    return tableToMarkdown(
        "Messages",
        hr_list,
        [
            "ID",
            "From",
            "To",
            "Subject",
            "Message Trust Score",
            "Domain Reputation",
            "IP",
            "Authenticity",
            "Attachment Filenames",
            "Attachment sha256",
            "Attack Types",
            "Date",
        ],
        removeNull=True,
    )


def test_function(**kwargs) -> str:
    """
    Performs test connectivity by valid http response.

    :param kwargs: Contains all required parameters.
    :return: raise ValueError if any error occurred during connection
    """
    headers = {
        "Accept": CONTENT_TYPE_JSON,
        "Content-Type": "application/x-www-form-urlencoded",
    }
    client = kwargs["client"]
    res = client.http_request(
        method="POST",
        url_suffix=URL_SUFFIX["GET_TOKEN"],
        headers=headers,
        data=client.payload,
    )
    if not res["access_token"]:
        return "Invalid API Key, Secret Key or URL"

    if kwargs["is_fetch"]:
        get_fetch_limit(kwargs["fetch_limit"])

        # getting numeric value from string representation
        start_time, _ = parse_date_range(kwargs["first_fetch_time"], date_format=DATE_FORMAT, utc=True)

        # validate policy actions

        validate_fetch_policy_action(kwargs["fetch_policy_actions"])

        # validate exclude alerts

        validate_exclude_alert_type(kwargs["exclude_alert_type"])
        kwargs["exclude_alert_type"] = kwargs["exclude_alert_type"].replace(" ", "")
        fetch_incidents(client, {}, {}, True)

    return "ok"


def get_events_params(args: dict[str, Any], max_record=MAX_LIMIT_FOR_EVENT) -> dict[str, Any]:
    """
    Validates the input arguments of command and returns parameter dictionary
    or raises ValueError in case of validation failed.

    :param args: Input arguments of command
    :param max_record: Maximum fetch limit
    :return: Params dict or error message
    """

    arg_keys = args.keys()

    if "rem_fields" in arg_keys:
        values = args.get("rem_fields", "").split(",")
        if "id" in values:
            raise ValueError(MESSAGES["INVALID_REM_FIELDS"])

    if "start_date" in arg_keys:
        start_date = args.get("start_date", "")
        if start_date.isdigit():
            raise ValueError(MESSAGES["INVALID_TIME_VALIDATION"].format("start_date"))
        date_time = dateparser.parse(start_date, settings={"STRICT_PARSING": True})
        if date_time:
            args["start_date"] = str(date_time.strftime(API_SUPPORT_DATE_FORMAT))
        else:
            raise ValueError(MESSAGES["INVALID_TIME_VALIDATION"].format("start_date"))

    if "end_date" in arg_keys:
        end_date = args.get("end_date", "")
        if end_date.isdigit():
            raise ValueError(MESSAGES["INVALID_TIME_VALIDATION"].format("end_date"))
        date_time = dateparser.parse(end_date, settings={"STRICT_PARSING": True})
        if date_time:
            args["end_date"] = str(date_time.strftime(API_SUPPORT_DATE_FORMAT))
        else:
            raise ValueError(MESSAGES["INVALID_TIME_VALIDATION"].format("end_date"))

    limit = 25
    if "limit" in arg_keys:
        try:
            limit = int(args.get("limit", 25))
            if not (1 <= limit <= max_record):
                raise ValueError(MESSAGES["INVALID_LIMIT"].format(max_record))
        except ValueError:
            raise ValueError(MESSAGES["INVALID_LIMIT"].format(max_record))

    if "page_id" in arg_keys:
        try:
            page_id = int(args.get("page_id", 1))
            page_id -= 1
            if page_id < 0:
                raise ValueError(MESSAGES["INVALID_PAGE_ID"])
        except ValueError:
            raise ValueError(MESSAGES["INVALID_PAGE_ID"])

        offset = page_id * limit
        args["offset"] = offset
        args.__delitem__("page_id")

    return args


def list_policy_events_command(client: Client, args: dict[str, Any]) -> str | CommandResults:
    """
    Retrieve list of events based on various argument(s).
    Will raise an exception if validation fails.

    :param client: Client object
    :param args: The command arguments provided by user.
    :return: Standard command result or no records found message
    """

    # Validate arguments
    params = get_events_params(args, MAX_LIMIT_FOR_EVENT)

    resp = get_list_policies_api_endpoint(client, params)

    total_records = resp["alert_events"]
    if not total_records:
        return MESSAGES["NO_RECORDS_FOUND"].format("event(s)")

    # Creating entry context
    custom_ec_for_event = remove_empty_elements(total_records)

    # Creating human-readable
    hr = prepare_hr_for_events(total_records)

    return CommandResults(
        outputs_prefix="AgariPhishingDefense.Alert",
        outputs_key_field="id",
        outputs=custom_ec_for_event,
        readable_output=hr,
        raw_response=resp,
    )


def list_message_data_command(client: Client, args: dict[str, Any]) -> str | CommandResults:
    """
    Retrieve list of messages based on various argument(s).
    Will raise an exception if validation fails.

    :param client: Client object
    :param args: The command arguments provided by user.
    :return: Standard command result or no records found message
    """

    # Validate arguments
    params = get_events_params(args, MAX_LIMIT_FOR_MESSAGE)

    # Preparing header

    headers = {"Authorization": client.api_token, "Accept": CONTENT_TYPE_JSON}

    # http call
    resp = client.http_request(
        method="GET",
        url_suffix=URL_SUFFIX["GET_MESSAGES"],
        params=params,
        headers=headers,
    )

    total_records = resp["messages"]
    if not total_records:
        return MESSAGES["NO_RECORDS_FOUND"].format("event(s)")

    # Creating entry context
    custom_ec_for_message = remove_empty_elements(total_records)

    # Creating human-readable
    hr = prepare_hr_for_messages(total_records)

    return CommandResults(
        outputs_prefix="AgariPhishingDefense.Message",
        outputs_key_field="id",
        outputs=custom_ec_for_message,
        readable_output=hr,
        raw_response=resp,
    )


def remediate_message_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Remediate a message by applying a remediation operation 'move' or 'delete'.

    :param client: Client object
    :param args: The command arguments provided by user.
    :return: Standard command result or raise Exception
    """
    # Validate arguments
    message_id, action = (
        args.get("id", "").strip(),
        args.get("operation", "").strip(),
    )

    if not message_id or not action:
        raise ValueError(MESSAGES["MISSING_REMEDIATE_ARGS"])

    body = {"operation": action.lower()}

    headers = {"Authorization": client.api_token, "Accept": CONTENT_TYPE_JSON}

    # http call
    client.http_request(
        method="POST",
        url_suffix=URL_SUFFIX["REMEDIATE_MSG"].format(message_id),
        json_data=body,
        headers=headers,
    )

    return CommandResults(readable_output=HR_MESSAGES["REMEDIATE_MSG_SUCCESS"].format(message_id, action))


def get_list_policies_api_endpoint(client: Client, params: dict[str, Any]) -> Any:
    """
    This function gets list of policies

    :param client: Client object
    :param params: Parameters to be passed in API call

    :return response: Response of API call of list all policies
    """
    # Preparing header
    headers = {"Authorization": client.api_token, "Accept": CONTENT_TYPE_JSON}

    # http call
    return client.http_request(
        method="GET",
        url_suffix=URL_SUFFIX["GET_EVENTS"],
        params=params,
        headers=headers,
    )


def fetch_incidents_params(**kwargs) -> dict[str, Any]:
    """
    Validates the input arguments of integration for fetching incidents and returns parameter dictionary
    or raises ValueError in case of validation failed.

    :param kwargs: Input arguments of integration to fetch incidents
    :return: params dict or error message
    """
    params = {
        "start_date": kwargs["start_date"],
        "sort": "created_at ASC,id ASC",
    }

    if kwargs["id"] is not None and kwargs["id"] != "":
        params["filter"] = "id.gt(+" + str(kwargs["id"]) + ")"
        if kwargs["policy_filter"] is not None and kwargs["policy_filter"] != "":
            params["filter"] = kwargs["policy_filter"] + " and " + params["filter"]
    elif kwargs["policy_filter"] is not None and kwargs["policy_filter"] != "":
        params["filter"] = kwargs["policy_filter"]

    if kwargs["fetch_limit"] is not None and kwargs["fetch_limit"] != "":
        params["limit"] = get_fetch_limit(kwargs["fetch_limit"])

    if kwargs["fetch_policy_actions"] is not None and kwargs["fetch_policy_actions"] != "":
        params["policy_action"] = kwargs["fetch_policy_actions"]
    if kwargs["exclude_alert_type"] is not None and kwargs["exclude_alert_type"] != "":
        params["exclude_alert_types"] = kwargs["exclude_alert_type"].replace(" ", "")
    return params


def get_message(client: Client, policy_id: str) -> Any:
    """
    This function is called for getting message for each policy_id
    :param client: Client object
    :param policy_id: Policy id of policy

    :return message_response: Response of message of given policy_id
    """

    headers = {"Authorization": client.api_token, "Accept": CONTENT_TYPE_JSON}

    try:
        policy_response = client.http_request(
            method="GET",
            url_suffix=URL_SUFFIX["GET_EVENTS"] + f"/{policy_id}",
            headers=headers,
        )
        url = policy_response["alert_event"]["collector_message_id"]
        message_response = client.http_request(
            method="GET",
            url_suffix=URL_SUFFIX["GET_MESSAGES"] + f"/{url}",
            headers=headers,
        )
        # attack_class_types Key is added to separate out the attack_class' keys in Dashboard
        message_response["message"]["attack_class_types"] = []
        for attack_class in list(message_response["message"]["attack_class"].keys()):
            message_response["message"]["attack_class_types"].append({"Types": attack_class})
        return message_response["message"]
    except Exception as ex:
        demisto.debug(str(ex))


def fetch_incidents(
    client: Client,
    last_run: dict[str, Any],
    args: dict[str, Any],
    call_from_test=False,
) -> tuple[dict, list]:
    """
    This function is called for fetching incidents.
    This function gets all policies, then after using ThreadPoolExecutor, for each policy in all policies, get each
    policy and get message from message id obtained from each policy
    This function will execute each interval (default is 1 minute).

    :param client: Client object
    :param last_run: The greatest incident created_time we fetched from last fetch
    :param args: The command arguments provided by user.
    :param call_from_test: Whether calling from test module

    :return next_run: This will be last_run in the next fetch-incidents
    :return incidents: Incidents that will be created in Cortex XSOAR
    """

    # Get the last fetch time and id, if exists
    last_fetch = last_run.get("last_fetch")
    id = last_run.get("id", "")

    first_fetch = args.get("first_fetch")
    # Set first fetch time as default if user leave empty
    first_fetch = first_fetch if first_fetch else DEFAULT_FIRST_FETCH

    fetch_limit = args.get("max_fetch", DEFAULT_FETCH_LIMIT)

    fetch_policy_actions = args.get("fetch_policy_actions", "")

    exclude_alert_type = args.get("exclude_alert_type", "")

    policy_filter = args.get("policy_filter", "")

    # Handle first time fetch
    if last_fetch is None:
        latest_created_time_date = dateparser.parse(first_fetch)
    else:
        latest_created_time_date = dateparser.parse(last_fetch)
    assert latest_created_time_date is not None, f"could not parse date {first_fetch}"
    latest_created_time_date_str = latest_created_time_date.strftime(DATE_FORMAT)

    params = fetch_incidents_params(
        start_date=latest_created_time_date_str,
        fetch_limit=fetch_limit,
        fetch_policy_actions=fetch_policy_actions,
        exclude_alert_type=exclude_alert_type,
        policy_filter=policy_filter,
        id=id,
    )

    resp = get_list_policies_api_endpoint(client, params)

    total_records = resp["alert_events"]
    if not total_records:
        return last_run, []

    policy_ids = []
    policy_data = {}
    for alert_event in total_records:
        policy_data[alert_event["id"]] = alert_event
        policy_ids.append(alert_event["id"])

    items = []
    incidents = []
    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        future_to_url = {executor.submit(get_message, client, policy_id): policy_id for policy_id in policy_ids}
        for future in as_completed(future_to_url):
            try:
                items.append([future.result(), future_to_url[future]])
            except Exception as ex:
                demisto.debug(str(ex))
    for item in items:
        if item[0] is None:
            continue
        result = {"message": item[0], "policy": policy_data[item[1]]}
        incident = {"name": str(item[1]), "rawJSON": json.dumps(result)}
        incidents.append(incident)

    # Update last run and add incident if the incident is newer than last fetch
    latest_created_time_date = dateparser.parse(total_records[-1]["created_at"])
    assert latest_created_time_date is not None, f"failed parsing created_at date: {total_records[-1]['created_at']}"
    latest_created_time_date_str = latest_created_time_date.strftime(DATE_FORMAT)
    next_run = {
        "last_fetch": latest_created_time_date_str,
        "id": total_records[-1]["id"],
    }
    if call_from_test:
        # Returning None
        return {}, []
    return next_run, incidents


""" MAIN FUNCTION """


def main() -> None:
    """
    PARSE AND VALIDATE INTEGRATION PARAMS
    """

    command = demisto.command()
    demisto.info(f"[Agari] Command being called is {command}")

    try:
        url = demisto.params().get("url")
        client_id = demisto.params().get("apikey")
        client_secret = demisto.params().get("apisecret")

        verify_certificate = not demisto.params().get("insecure", False)
        proxy = demisto.params().get("proxy", False)

        # prepare payload
        payload = "client_id=" + client_id + "&client_secret=" + client_secret

        # prepare client class object
        client = Client(
            base_url=url,
            verify=verify_certificate,
            proxy=proxy,
            request_timeout=DEFAULT_SESSION_TIMEOUT,
            payload=payload,
        )

        # Trim the arguments
        args = strip_blank(demisto.args())

        # This is the call made when pressing the integration Test button.
        if demisto.command() == "test-module":
            is_fetch = demisto.params().get("isFetch")

            first_fetch_time = demisto.params().get("first_fetch")

            # Set first fetch time as default if user leave empty
            first_fetch_time = first_fetch_time if first_fetch_time else DEFAULT_FIRST_FETCH

            incident_type = demisto.params().get("incidentType")

            fetch_limit = demisto.params().get("max_fetch")

            fetch_policy_actions = demisto.params().get("fetch_policy_actions")

            policy_filter = demisto.params().get("policy_filter")

            exclude_alert_type = demisto.params().get("exclude_alert_type")

            result = test_function(
                client=client,
                first_fetch_time=first_fetch_time,
                fetch_limit=fetch_limit,
                is_fetch=is_fetch,
                incident_type=incident_type,
                fetch_policy_actions=fetch_policy_actions,
                policy_filter=policy_filter,
                exclude_alert_type=exclude_alert_type,
            )
            demisto.results(result)
        elif demisto.command() == "apd-list-policy-events":
            return_results(list_policy_events_command(client, args))
        elif demisto.command() == "apd-list-message-data":
            return_results(list_message_data_command(client, args))
        elif demisto.command() == "fetch-incidents":
            next_run, incidents = fetch_incidents(client, demisto.getLastRun(), demisto.params())

            demisto.setLastRun(next_run)
            demisto.incidents(incidents)
        elif demisto.command() == "apd-remediate-message":
            return_results(remediate_message_command(client, args))

    # Log exceptions
    except Exception as e:
        demisto.error(traceback.format_exc())  # print the traceback
        return_error(f"Failed to execute {demisto.command()} command.\nError:\n{str(e)}")


""" ENTRY POINT """

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