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
| ID | Agari Phishing Defense |
|---|---|
| Provider | Fortra |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 5.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix 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 intervalfetch_policy_actions— Policy action takenexclude_alert_type— Exclude alertspolicy_filter— Fetches policy events as per the filter to limit the amount of data. Can be applied to specific fields.incidentType— Incident typeincidentFetchInterval— Incidents Fetch IntervalisFetch— Fetch incidentsinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (3)
-
apd-list-message-dataRetrieves a list of messages.
-
apd-list-policy-eventsRetrieves a list of policy events.
-
apd-remediate-messageRemediate 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()