ExtraHop v2

ExtraHop Reveal(x) for Cortex XSOAR is a network detection and response solution that provides complete visibility of network communications at enterprise scale, real-time threat detections backed by machine learning, and guided investigation workflows that simplify response.

Network Security · ExtraHop Reveal(x)

Details

IDExtraHop v2
ProviderBain Capital Private Equity
CategoryNetwork Security
From Version6.5.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM EDR Cortex Cloud Cloud Runtime Security

README

ExtraHop Reveal(x) for Cortex XSOAR is a network detection and response solution that provides complete visibility of network communications at enterprise scale, real-time threat detections backed by machine learning, and guided investigation workflows that simplify response.

Configure ExtraHop Reveal(x) in Cortex

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for ExtraHop Reveal(x).
  3. Click Add instance to create and configure a new integration instance.
Parameter Description Required
Name The name of the instance. True
Fetches incidents Select to enable this instance to fetch detection events. Otherwise, select Do not fetch. Each API call fetches a maximum of 200 detection events. True
Classifier Specifies the type of incident to be created for detection events ingested by this instance. False
Incident type Specifies the type of incident to be created for detection events ingested by this instance if a Classifier is not specified. False
Mapper Specifies how detection events ingested by this instance are mapped to Cortex XSOAR incident fields. False
On Cloud The type of ExtraHop system the integration will connect to. Select if connecting to ExtraHop Reveal(x) 360. Leave unselected if connecting to Reveal(x) Enterprise. False
URL The URL of the ExtraHop system this integration will connect to. True
API Key The API key required for authentication if connecting to ExtraHop Reveal(x) Enterprise. The API key is generated on your ExtraHop system. False
Client ID and Client Secret The credential pair required for authentication if connecting to ExtraHop Reveal(x) 360. The client ID and secret are generated on your ExtraHop system. False
Trust any certificate (not secure) Specifies whether to allow connections without verifying SSL certificate’s validity. False
Use system proxy settings Specifies whether to use XSOAR system proxy settings to connect to the API. False
First fetch time Specifies the beginning timestamp from which to start fetching detection events. False
Incidents Fetch Interval Specifies how often the instance fetches detection events. Because each API call fetches a maximum of 200 detection events, we recommend specifying one minute intervals to fetch all detection events. False
Advanced Filter Applies a filter to the list of detections based on a JSON-specific query.

Example for detections:
{
“categories”: [“sec.attack”],
“risk_score_min”: 51
}

If the categories and category are not specified, then categories will be set to [“sec.attack”]. The category field is deprecated by the API, so please use the categories field instead.
For a complete reference to the Extrahop detections filter fields, please refer to the ExtraHop REST API documentation at
https://docs.extrahop.com/current/rest-api-guide/
False
Do not use by default Select to disable running commands through the Cortex XSOAR CLI on this instance of the integration. False
Log Level Specifies the level of logging to enable for this instance of the integration. False
Run on Specifies whether to run the instance of the integration on a single engine. False
  1. Click Test to validate the URL, credentials, and connection.

Commands

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

  • Get detections from ExtraHop Reveal(x): extrahop-detections-list
  • Link an ExtraHop Reveal(x) detection to a Cortex XSOAR incident: extrahop-ticket-track
  • Search for devices in ExtraHop Reveal(x): extrahop-devices-search
  • Get all active network protocols for a device from ExtraHop Reveal(x): extrahop-protocols-get
  • Get all peers for a device from ExtraHop Reveal(x): extrahop-peers-get
  • Get a link to a live activity map in ExtraHop Reveal(x): extrahop-activity-map-get
  • Get all devices on the Advanced Analysis watchlist in ExtraHop Reveal(x): extrahop-watchlist-get
  • Add or remove devices from the Advanced Analysis watchlist in ExtraHop Reveal(x): extrahop-watchlist-edit
  • Add or remove a tag from devices in ExtraHop Reveal(x): extrahop-devices-tag
  • Get all alert rules from ExtraHop Reveal(x): extrahop-alert-rules-get
  • Create a new alert rule in ExtraHop Reveal(x): extrahop-alert-rule-create
  • Modify an alert rule in ExtraHop Reveal(x): extrahop-alert-rule-edit
  • Get metrics for specified objects from ExtraHop Reveal(x): extrahop-metrics-list
  • Search for specific packets in ExtraHop Reveal(x): extrahop-packets-search

extrahop-watchlist-get


Get all devices on the advanced analysis watchlist in ExtraHop Reveal(x).

Base Command

extrahop-watchlist-get

Input

There are no input arguments for this command.

Context Output

Path Type Description
ExtraHop.Device.Macaddr String The MAC Address of the device.
ExtraHop.Device.DeviceClass String The class of this device.
ExtraHop.Device.UserModTime Number The time of the most recent update, expressed in milliseconds since the epoch.
ExtraHop.Device.AutoRole String The role automatically detected by the ExtraHop system.
ExtraHop.Device.ParentId Number The ID of the parent device.
ExtraHop.Device.Vendor String The device vendor.
ExtraHop.Device.Analysis string The level of analysis received by the device.
ExtraHop.Device.DiscoveryId String The UUID for this device.
ExtraHop.Device.DefaultName String The default name for this device.
ExtraHop.Device.DisplayName String The display name of device.
ExtraHop.Device.OnWatchlist Boolean Whether the device is on the advanced analysis watch list.
ExtraHop.Device.ModTime Number The time of the most recent update, expressed in milliseconds since the epoch.
ExtraHop.Device.IsL3 Boolean Indicates whether the device is a layer 3 device.
ExtraHop.Device.Role String The role of the device.
ExtraHop.Device.DiscoverTime Number The time that the device was discovered.
ExtraHop.Device.Id Number The ID of the device.
ExtraHop.Device.Ipaddr4 String The IPv4 address for this device.
ExtraHop.Device.Vlanid Number The unique identifier for the VLAN associated with the device.
ExtraHop.Device.Ipaddr6 string The IPv6 address of the device.
ExtraHop.Device.NodeId number The node ID of the sensor associated with this device.
ExtraHop.Device.Description string A user customizable description of the device.
ExtraHop.Device.DnsName string The DNS name associated with the device.
ExtraHop.Device.DhcpName string The DHCP name associated with the device.
ExtraHop.Device.CdpName string The Cisco Discovery Protocol name associated with the device.
ExtraHop.Device.NetbiosName string The NetBIOS name associated with the device.
ExtraHop.Device.Url string Link to the device details page in ExtraHop Reveal(x).

Command example


#### Context Example

```json
{
    "ExtraHop": {
        "Device": [
            {
                "analysis": "advanced",
                "analysis_level": 2,
                "auto_role": "other",
                "critical": false,
                "default_name": "VM9",
                "device_class": "node",
                "dhcp_name": "test",
                "discover_time": 1635499650000,
                "discovery_id": "0000000000000000",
                "display_name": "test",
                "extrahop_id": "0000000000000000",
                "id": 25769803982,
                "ipaddr4": "0.0.0.0",
                "is_l3": false,
                "macaddr": "00:00:00:00:00:00",
                "mod_time": 1676638611398,
                "model": "vmware_vm",
                "node_id": 6,
                "on_watchlist": true,
                "role": "other",
                "url": "https://dummy_url/extrahop/#/metrics/devices/overview/",
                "user_mod_time": 1676290306316,
                "vendor": "VMware",
                "vlanid": 0
            },
            {
                "analysis": "advanced",
                "analysis_level": 2,
                "auto_role": "other",
                "critical": false,
                "default_name": "VM8",
                "device_class": "node",
                "discover_time": 1675318050000,
                "discovery_id": "0000000000000000",
                "display_name": "VM8",
                "extrahop_id": "0000000000000000",
                "id": 25769808133,
                "ipaddr4": "0.0.0.0",
                "is_l3": false,
                "last_seen_time": 1675319010000,
                "macaddr": "00:00:00:00:00:00",
                "mod_time": 1675425919964,
                "model": "vmware_vm",
                "node_id": 6,
                "on_watchlist": true,
                "role": "other",
                "url": "https://dummy_url/extrahop/#/metrics/devices/00000000000000000000000000000000.0000000000000000/overview/",
                "user_mod_time": 0,
                "vendor": "VMware",
                "vlanid": 0
            }
        ]
    }
}

Human Readable Output

Device Details

Display Name IP Address MAC Address Role Vendor URL
test 0.0.0.0 00:00:00:00:00:00 other VMware View Device in ExtraHop
VM 8 0.0.0.0 00:00:00:00:00:00 other VMware View Device in ExtraHop

extrahop-peers-get


Get all peers for a device from ExtraHop Reveal(x).

Base Command

extrahop-peers-get

Input

Argument Name Description Required
ip_or_id The IP address or ExtraHop API ID of the source device to get peer devices. Required
query_from The beginning timestamp of the time range the query will search, expressed in milliseconds since the epoch. A negative value is evaluated relative to the current time. The default unit for a negative value is milliseconds, but other units can be specified with one of the following unit suffixes: ms, s, m, h, d, w, M, y. See https://docs.extrahop.com/current/rest-api-guide/#supported-time-units- for more details on supported time units and suffixes. Default is -30m. Optional
query_until The ending timestamp of the time range the query will search, expressed in milliseconds since the epoch. 0 indicates the time of the request. A negative value is evaluated relative to the current time. The default unit for a negative value is milliseconds, but other units can be specified with one of the following unit suffixes: ms, s, m, h, d, w, M, y. See https://docs.extrahop.com/current/rest-api-guide/#supported-time-units- for more details on supported time units and suffixes. Optional
peer_role The role of the peer device in relation to the origin device. Possible values are: any, client, server. Default is any. Optional
protocol A filter to only return peers that the source device has communicated with over this protocol. If no value is set, the object includes any protocol. Possible values are: any, AAA, ActiveMQ, AJP, amf, CIFS, DB, DHCP, DICOM, DNS, FIX, FTP, HL7, HTTP, IBMMQ, ICA, IKE/ISAKMP, IMAP, IPFIX, IPsec NAT-T, IRC, iSCSI, Kerberos, L2TP, LDAP, lync-compress, memcache, Modbus, MongoDB, MSMQ, MSN, MSRPC, NetFlow, NFS, NTP, OpenVPN, PCoIP, Perforce, POP3, RDP, Redis, RFB, RTCP, RTP, sFlow, SIP, SMPP, SMTP, SNMP, SSH, SSL, Syslog, TCP, telnet, UDP, WebSocket. Default is any. Optional

Context Output

Path Type Description
ExtraHop.Device.Macaddr String The MAC address of the device.
ExtraHop.Device.DeviceClass String The class of the device.
ExtraHop.Device.UserModTime Number The time of the most recent update, expressed in milliseconds since the epoch.
ExtraHop.Device.AutoRole String The role automatically detected by the ExtraHop system.
ExtraHop.Device.ParentId Number The ID of the parent device.
ExtraHop.Device.Vendor String The device vendor.
ExtraHop.Device.Analysis string The level of analysis received by the device.
ExtraHop.Device.DiscoveryId String The UUID given of the device.
ExtraHop.Device.DefaultName String The default name for this device.
ExtraHop.Device.DisplayName String The display name of device.
ExtraHop.Device.OnWatchlist Boolean Whether the device is on the advanced analysis watch list.
ExtraHop.Device.ModTime Number The time of the most recent update, expressed in milliseconds since the epoch.
ExtraHop.Device.IsL3 Boolean Indicates whether the device is a layer 3 device.
ExtraHop.Device.Role String The role of the device.
ExtraHop.Device.DiscoverTime Number The time that the device was discovered.
ExtraHop.Device.Id Number The ID of the device.
ExtraHop.Device.Ipaddr4 String The IPv4 address for this device.
ExtraHop.Device.Vlanid Number The unique identifier for the VLAN associated with the device.
ExtraHop.Device.Ipaddr6 string The IPv6 address of the device.
ExtraHop.Device.NodeId number The node ID of the sensor associated with the device.
ExtraHop.Device.Description string A user customizable description of the device.
ExtraHop.Device.DnsName string The DNS name associated with the device.
ExtraHop.Device.DhcpName string The DHCP name associated with the device.
ExtraHop.Device.CdpName string The Cisco Discovery Protocol name associated with the device.
ExtraHop.Device.NetbiosName string The NetBIOS name associated with the device.
ExtraHop.Device.Url string Link to the device details page in ExtraHop Reveal(x).

Command example

!extrahop-peers-get ip_or_id=0.0.0.0 peer_role=server protocol=any query_from=-60m query_until=0

Context Example

{
    "ExtraHop": {
        "Device": {
            "analysis": "advanced",
            "analysis_level": 1,
            "auto_role": "gateway",
            "critical": true,
            "default_name": "Cisco Meraki 23D27A",
            "device_class": "gateway",
            "discover_time": 1655102100000,
            "discovery_id": "0000000000000000",
            "display_name": "Cisco Meraki 23D27A",
            "extrahop_id": "0000000000000000",
            "id": 25769805776,
            "ipaddr4": "0.0.0.0",
            "is_l3": false,
            "macaddr": "00:00:00:00:00:00",
            "mod_time": 1676638911830,
            "node_id": 6,
            "on_watchlist": false,
            "role": "gateway",
            "server_protocols": [
                "UDP:NTP"
            ],
            "url": "https://dummy_url/extrahop/#/metrics/devices/overview/",
            "user_mod_time": 0,
            "vendor": "Cisco Meraki",
            "vlanid": 0
        }
    }
}

Human Readable Output

Device Details

Display Name IP Address MAC Address Role Protocols URL Vendor
Cisco Meraki 23D27A 0.0.0.0 00:00:00:00:00:00 gateway Server: UDP:NTP View Device in ExtraHop Cisco Meraki

extrahop-devices-search


Search for devices in ExtraHop Reveal(x).

Base Command

extrahop-devices-search

Input

Argument Name Description Required
name The name of the device. This searches for matches on all ExtraHop Reveal(x) name fields (DHCP, DNS, NetBIOS, Cisco Discovery Protocol, etc). Optional
ip The IP address of the device. Optional
mac The MAC address of the device. Optional
role The role of the device. Possible values are: db_server, dhcp_server, dns_server, file_server, firewall, gateway, http_server, domain_controller, web_proxy, load_balancer, pc, medical_device, mobile_device, printer, scanner, custom, voip_phone, other. Optional
software The OS of the device. Possible values are: android, apple_ios, arista_eos, cisco_ios, cisco_nx-os, chrome_os, linux, mac_os, windows, windows_server, windows_server_2008, windows_server_2008_r2, windows_server_2012, windows_server_2012_r2, windows_server_2016, windows_vista, windows_7, windows_8, windows_8.1, windows_10. Optional
tag A tag present on the device. Optional
vendor The vendor of the device, based on MAC address via OUI lookup. Possible values are: alcatel-lucent, apple, arista, asus, brother, canon, cisco, cisco-linksys, citrix, dell, dellemc, d-link, emc, f5, google, hp, htc, huawei, ibm, juniper, kyocera, microsoft, netapp, netgear, nokia, nortel, oracle, paloalto, samsung, 3com, toshiba, virtualbox, vmware, zte. Optional
discover_time The time that device was first seen by the ExtraHop system, expressed in milliseconds since the epoch. A negative value is evaluated relative to the current time. The default unit for a negative value is milliseconds, but other units can be specified with the following unit suffixes: ms, s, m, h, d, w, M, y. For example, to look one day back enter -1d or -24h. See https://docs.extrahop.com/current/rest-api-guide/#supported-time-units- for more details on supported time units and suffixes. Optional
vlan The VLAN ID of the Virtual LAN that the device is on. Optional
activity The activity of the device. Possible values are: aaa_client, aaa_server, ajp_client, ajp_server, amf_client, amf_server, cifs_client, cifs_server, db_client, db_server, dhcp_client, dhcp_server, dicom_client, dicom_server, dns_client, dns_server, fix_client, fix_server, ftp_client, ftp_server, hl7_client, hl7_server, http_client, http_server, ibmmq_client, ibmmq_server, ica_client, ica_server, icmp, iscsi_client, iscsi_server, kerberos_client, kerberos_server, ldap_client, ldap_server, llmnr_client, llmnr_server, memcache_client, memcache_server, modbus_client, modbus_server, mongo_client, mongo_server, msmq, nbns_client, nbns_server, nfs_client, nfs_server, pcoip_client, pcoip_server, pop3_client, pop3_server, rdp_client, rdp_server, redis_client, redis_server, rfb_client, rfb_server, rpc_client, rpc_server, rtcp, rtp, scanner, sip_client, sip_server, smpp_client, smpp_server, smtp_client, smtp_server, ssh_client, ssh_server, ssl_client, ssl_server, tcp, telnet_client, telnet_server, udp, websocket_client, websocket_server, wsman_client, wsman_server. Optional
operator The compare method applied when matching the fields against their values. For example, to find devices with names that begin with ‘SEA1’ (set name=SEA1, operator=startswith). Possible values are: >, <, <=, >=, =, !=, startswith, exists, not_exists, ~, !~. Default is =. Optional
match_type The match operator to use when chaining the search fields together. For example, to find all HTTP servers running Windows on the network (set match_type=and, role=http_server, software=windows). Possible values are: and, or, not. Default is and. Optional
active_from The beginning timestamp for the request. Return only devices active after this time. Time is expressed in milliseconds since the epoch. 0 indicates the time of the request. A negative value is evaluated relative to the current time. The default unit for a negative value is milliseconds, but other units can be specified with one of the following unit suffixes: ms, s, m, h, d, w, M, y. See https://docs.extrahop.com/current/rest-api-guide/#supported-time-units- for more details on supported time units and suffixes. Optional
active_until The ending timestamp for the request. Return only devices active before this time. Time is expressed in milliseconds since the epoch. 0 indicates the time of the request. A negative value is evaluated relative to the current time. The default unit for a negative value is milliseconds, but other units can be specified with one of the following unit suffixes: ms, s, m, h, d, w, M, y. See https://docs.extrahop.com/current/rest-api-guide/#supported-time-units- for more details on supported time units and suffixes. Optional
limit The maximum number of devices to return. Default is 10. Optional
l3_only Only returns layer 3 devices by filtering out any layer 2 parent devices. Possible values are: true, false. Default is true. Optional

Context Output

Path Type Description
ExtraHop.Device.Macaddr String The MAC address of the device.
ExtraHop.Device.DeviceClass String The class of the device.
ExtraHop.Device.UserModTime Number The time of the most recent update, expressed in milliseconds since the epoch.
ExtraHop.Device.AutoRole String The role automatically detected by the ExtraHop system.
ExtraHop.Device.ParentId Number The ID of the parent device.
ExtraHop.Device.Vendor String The device vendor.
ExtraHop.Device.Analysis string The level of analysis received by the device.
ExtraHop.Device.DiscoveryId String The UUID of the device.
ExtraHop.Device.DefaultName String The default name of the device.
ExtraHop.Device.DisplayName String The display name of device.
ExtraHop.Device.OnWatchlist Boolean Whether the device is on the advanced analysis watch list.
ExtraHop.Device.ModTime Number The time of the most recent update, expressed in milliseconds since the epoch.
ExtraHop.Device.IsL3 Boolean Indicates whether the device is a layer 3 device.
ExtraHop.Device.Role String The role of the device.
ExtraHop.Device.DiscoverTime Number The time that the device was discovered.
ExtraHop.Device.Id Number The ID of the device.
ExtraHop.Device.Ipaddr4 String The IPv4 address of the device.
ExtraHop.Device.Vlanid Number The ID of the VLAN associated with the device.
ExtraHop.Device.Ipaddr6 String The IPv6 address of the device.
ExtraHop.Device.NodeId Number The node ID of the sensor associated with the device.
ExtraHop.Device.Description String A user customizable description of the device.
ExtraHop.Device.DnsName String The DNS name associated with the device.
ExtraHop.Device.DhcpName String The DHCP name associated with the device.
ExtraHop.Device.CdpName String The Cisco Discovery Protocol name associated with the device.
ExtraHop.Device.NetbiosName String The NetBIOS name associated with the device.
ExtraHop.Device.Url String Link to the device details page in ExtraHop Reveal(x).

Command example

!extrahop-devices-search activity=aaa_client discover_time=-10m ip=0.0.0.0 l3_only=true limit=2 mac=00:00:00:00:00:00 match_type=or name=DNS operator=!= role=file_server software=linux tag=tag1 vendor=cisco

Context Example

{
    "ExtraHop": {
        "Device": [
            {
                "analysis": "advanced",
                "analysis_level": 1,
                "auto_role": "other",
                "critical": false,
                "default_name": "VMware 8",
                "device_class": "node",
                "discover_time": 1676633640000,
                "discovery_id": "0000000000000000",
                "display_name": "VMware 8",
                "extrahop_id": "0000000000000000",
                "id": 25769808421,
                "ipaddr4": "0.0.0.0",
                "is_l3": false,
                "last_seen_time": 1676634840000,
                "macaddr": "00:00:00:00:00:00",
                "mod_time": 1676634890174,
                "model": "vmware_vm",
                "node_id": 6,
                "on_watchlist": false,
                "role": "other",
                "url": "https://dummy_url/extrahop/#/metrics/devices/overview/",
                "user_mod_time": 0,
                "vendor": "VMware",
                "vlanid": 0
            },
            {
                "analysis": "advanced",
                "analysis_level": 1,
                "auto_role": "other",
                "critical": false,
                "default_name": "VMware 3",
                "device_class": "node",
                "discover_time": 1676614620000,
                "discovery_id": "0000000000000000",
                "display_name": "VMware 3",
                "extrahop_id": "0000000000000000",
                "id": 25769808417,
                "ipaddr4": "0.0.0.0",
                "is_l3": false,
                "last_seen_time": 1676616960000,
                "macaddr": "00:00:00:00:00:00",
                "mod_time": 1676616977189,
                "model": "vmware_vm",
                "node_id": 6,
                "on_watchlist": false,
                "role": "other",
                "url": "https://dummy_url/extrahop/#/metrics/devices/overview/",
                "user_mod_time": 0,
                "vendor": "VMware",
                "vlanid": 0
            }
        ]
    }
}

Human Readable Output

Device Details

Display Name IP Address MAC Address Role Vendor URL
VMware 8 0.0.0.0 00:00:00:00:00:00 other VMware View Device in ExtraHop
VMware 3 0.0.0.0 00:00:00:00:00:00 other VMware View Device in ExtraHop

extrahop-protocols-get


Get all active network protocols for a device from ExtraHop Reveal(x).

Base Command

extrahop-protocols-get

Input

Argument Name Description Required
ip_or_id The IP address or ExtraHop API ID of the device to get all active network protocols. Required
query_from The beginning timestamp of the time range the query will search, expressed in milliseconds since the epoch. A negative value is evaluated relative to the current time. The default unit for a negative value is milliseconds, but other units can be specified with one of the following unit suffixes: ms, s, m, h, d, w, M, y. See https://docs.extrahop.com/current/rest-api-guide/#supported-time-units- for more details on supported time units and suffixes. Default is -30m. Optional
query_until The ending timestamp of the time range the query will search, expressed in milliseconds since the epoch. 0 indicates the time of the request. A negative value is evaluated relative to the current time. The default unit for a negative value is milliseconds, but other units can be specified with one of the following unit suffixes: ms, s, m, h, d, w, M, y. See https://docs.extrahop.com/current/rest-api-guide/#supported-time-units- for more details on supported time units and suffixes. Optional

Context Output

Path Type Description
ExtraHop.Device.Macaddr String The MAC address of the device.
ExtraHop.Device.DeviceClass String The class of the device.
ExtraHop.Device.UserModTime Number The time of the most recent update, expressed in milliseconds since the epoch.
ExtraHop.Device.AutoRole String The role automatically detected by the ExtraHop system.
ExtraHop.Device.ParentId Number The ID of the parent device.
ExtraHop.Device.Vendor String The device vendor.
ExtraHop.Device.Analysis String The level of analysis received by the device.
ExtraHop.Device.DiscoveryId String The UUID of the device.
ExtraHop.Device.DefaultName String The default name of the device.
ExtraHop.Device.DisplayName String The display name of device.
ExtraHop.Device.OnWatchlist Boolean Whether the device is on the advanced analysis watch list.
ExtraHop.Device.ModTime Number The time of the most recent update, expressed in milliseconds since the epoch.
ExtraHop.Device.IsL3 Boolean Indicates whether the device is a layer 3 device.
ExtraHop.Device.Role String The role of the device.
ExtraHop.Device.DiscoverTime Number The time that the device was discovered.
ExtraHop.Device.Id Number The ID of the device.
ExtraHop.Device.Ipaddr4 String The IPv4 address of the device.
ExtraHop.Device.Vlanid Number The ID of the VLAN associated with the device.
ExtraHop.Device.Ipaddr6 String The IPv6 address of the device.
ExtraHop.Device.NodeId Number The node ID of the sensor associated with the device.
ExtraHop.Device.Description String A user customizable description of the device.
ExtraHop.Device.DnsName String The DNS name associated with the device.
ExtraHop.Device.DhcpName String The DHCP name associated with the device.
ExtraHop.Device.CdpName String The Cisco Discovery Protocol name associated with the device.
ExtraHop.Device.NetbiosName String The NetBIOS name associated with the device.
ExtraHop.Device.Url String Link to the device details page in ExtraHop Reveal(x).
ExtraHop.Device.ClientProtocols String The list of protocols the peer device is communicating on as a client.
ExtraHop.Device.ServerProtocols String The list of protocols the peer device is communicating on as a server.

Command example

!extrahop-protocols-get ip_or_id=0.0.0.0 query_from=-20m query_until=0

Context Example

{
    "ExtraHop": {
        "Device": {
            "analysis": "advanced",
            "analysis_level": 2,
            "auto_role": "other",
            "client_protocols": [
                "UDP:NTP"
            ],
            "critical": false,
            "default_name": "VMware 9",
            "device_class": "node",
            "dhcp_name": "test",
            "discover_time": 1635499650000,
            "discovery_id": "0000000000000000",
            "display_name": "test",
            "extrahop_id": "0000000000000000",
            "id": 10000000000,
            "ipaddr4": "0.0.0.0",
            "is_l3": false,
            "macaddr": "00:00:00:00:00:000",
            "mod_time": 1676638611398,
            "model": "vmware_vm",
            "node_id": 6,
            "on_watchlist": true,
            "role": "other",
            "url": "https://dummy_url/extrahop/#/metrics/devices/overview/",
            "user_mod_time": 1676290306316,
            "vendor": "VMware",
            "vlanid": 0
        }
    }
}

Human Readable Output

Device Activity Found

Display Name IP Address MAC Address Protocols (Client) Role Vendor URL
test 0.0.0.0 00:00:00:00:00:000 UDP:NTP other VMware View Device in ExtraHop

extrahop-activity-map-get


Get a link to a live activity map in ExtraHop Reveal(x).

Base Command

extrahop-activity-map-get

Input

Argument Name Description Required
ip_or_id The IP address or ExtraHop API ID of the source device to get an activity map. Required
time_interval The time interval of the live activity map, expressed as the “Last” 30 minutes. For example, specify a value of 30 minutes to get an activity map showing the time range of the last 30 minutes. This field is ignored if from_time and until_time are provided. Possible values are: 30 minutes, 6 hours, 1 day, 1 week. Default is 30 minutes. Optional
from_time The beginning timestamp of a fixed time range the activity map will display, expressed in seconds since the epoch. Optional
until_time The ending timestamp of a fixed time range the activity map will display, expressed in seconds since the epoch. Optional
peer_role The role of the peer devices in relation to the source device. For example, specifying a peer_role of client will show All Clients communicating with the source device. Additionally specifying a protocol of HTTP will result in further filtering and only showing HTTP Clients communicating with the source device. Possible values are: any, client, server. Default is any. Optional
protocol The protocol over which the source device is communicating. For example, specifying a protocol of HTTP show only HTTP Clients and HTTP Servers communicating with the source device. Additionally specifying a peer_role of client will result in further filtering and only showing HTTP Clients communicating with the source device. Possible values are: any, AAA, ActiveMQ, AJP, amf, CIFS, DB, DHCP, DICOM, DNS, FIX, FTP, HL7, HTTP, IBMMQ, ICA, IKE/ISAKMP, IMAP, IPFIX, IPsec NAT-T, IRC, iSCSI, Kerberos, L2TP, LDAP, lync-compress, memcache, Modbus, MongoDB, MSMQ, MSN, MSRPC, NetFlow, NFS, NTP, OpenVPN, PCoIP, Perforce, POP3, RDP, Redis, RFB, RTCP, RTP, sFlow, SIP, SMPP, SMTP, SNMP, SSH, SSL, Syslog, TCP, telnet, UDP, WebSocket. Default is any. Optional

Context Output

Path Type Description
ExtraHop.ActivityMap.url String The link to a visual activity map in ExtraHop Reveal(x).

Command example

!extrahop-activity-map-get ip_or_id=0.0.0.0 peer_role=server protocol=any time_interval="30 minutes"

Context Example

{
    "ExtraHop": {
        "ActivityMap": {
            "url": "https://dummy_url/extrahop/#/activitymaps?appliance_id=00000000000000000000000000000000&discovery_id=0000000000000000&from=30&interval_type=MIN&object_type=device&protocol=any&role=server&until=0"
        }
    }
}

Human Readable Output

View Live Activity Map in ExtraHop

extrahop-alert-rules-get


Get all alert rules from ExtraHop Reveal(x).

Base Command

extrahop-alert-rules-get

Input

There are no input arguments for this command.

Context Output

Path Type Description
ExtraHop.Alert.Operator String The logical operator applied when comparing the value of the operand field to alert conditions.
ExtraHop.Alert.FieldName String The name of the monitored metric.
ExtraHop.Alert.NotifySnmp Boolean Indicates whether to send an SNMP trap when an alert is generated.
ExtraHop.Alert.Operand String The value to compare against alert conditions.
ExtraHop.Alert.IntervalLength Number The length of the alert interval, expressed in seconds.
ExtraHop.Alert.Author String The name of the user that created the alert.
ExtraHop.Alert.Name String The unique, friendly name for the alert.
ExtraHop.Alert.FieldName2 String The second monitored metric when applying a ratio.
ExtraHop.Alert.RefireInterval Number The time interval in which alert conditions are monitored, expressed in seconds.
ExtraHop.Alert.ModTime Number The time of the most recent update, expressed in milliseconds since the epoch.
ExtraHop.Alert.Units String The interval in which to evaluate the alert condition.
ExtraHop.Alert.ApplyAll Boolean Indicates whether the alert is assigned to all available data sources.
ExtraHop.Alert.Type String The type of alert.
ExtraHop.Alert.FieldOp String The type of comparison between the “field_name” and “field_name2” fields when applying a ratio.
ExtraHop.Alert.Id Number The unique identifier for the alert.
ExtraHop.Alert.Disabled Boolean Indicates whether the alert is disabled.
ExtraHop.Alert.Description String An optional description for the alert.
ExtraHop.Alert.Severity Number The severity level of the alert.
ExtraHop.Alert.StatName String The statistic name for the alert.

Command example


#### Context Example

```json
{
    "ExtraHop": {
        "Alert": [
            {
                "apply_all": false,
                "author": "ExtraHop",
                "description": "Alert triggered when ratio of DB errors is greater than 1%.",
                "disabled": false,
                "field_name": "rsp_error",
                "field_name2": "rsp",
                "field_op": "/",
                "id": 15,
                "interval_length": 30,
                "mod_time": 1617887147538,
                "name": "DB Error Ratio - Orange",
                "notify_snmp": false,
                "operand": "0.01",
                "operator": ">",
                "refire_interval": 300,
                "severity": 3,
                "stat_name": "extrahop.application.db",
                "type": "threshold",
                "units": "none"
            },
            {
                "apply_all": false,
                "author": "ExtraHop",
                "description": "Alert triggered when ratio of DB errors is greater than 5%.",
                "disabled": false,
                "field_name": "rsp_error",
                "field_name2": "rsp",
                "field_op": "/",
                "id": 14,
                "interval_length": 30,
                "mod_time": 1617887147615,
                "name": "DB Error Ratio - Red",
                "notify_snmp": false,
                "operand": "0.05",
                "operator": ">",
                "refire_interval": 300,
                "severity": 1,
                "stat_name": "extrahop.application.db",
                "type": "threshold",
                "units": "none"
            },
            {
                "apply_all": false,
                "author": "ExtraHop",
                "description": "Alert triggered when ratio of DNS errors is greater than 0.1%.",
                "disabled": false,
                "field_name": "rsp_error",
                "field_name2": "rsp",
                "field_op": "/",
                "id": 19,
                "interval_length": 30,
                "mod_time": 1617887147785,
                "name": "DNS Error Ratio - Yellow",
                "notify_snmp": false,
                "operand": "0.001",
                "operator": ">",
                "refire_interval": 300,
                "severity": 5,
                "stat_name": "extrahop.application.dns",
                "type": "threshold",
                "units": "none"
            }
        ]
    }
}

Human Readable Output

Found 3 Alert(s)

Apply All Author Description Disabled Field Name Field Name2 Field Op Id Interval Length Mod Time Name Notify Snmp Operand Operator Refire Interval Severity Stat Name Type Units
false ExtraHop Alert triggered when ratio of DB errors is greater than 1%. false rsp_error rsp / 15 30 1617887147538 DB Error Ratio - Orange false 0.01 > 300 3 extrahop.application.db threshold none
false ExtraHop Alert triggered when ratio of DB errors is greater than 5%. false rsp_error rsp / 14 30 1617887147615 DB Error Ratio - Red false 0.05 > 300 1 extrahop.application.db threshold none
false ExtraHop Alert triggered when ratio of DNS errors is greater than 0.1%. false rsp_error rsp / 19 30 1617887147785 DNS Error Ratio - Yellow false 0.001 > 300 5 extrahop.application.dns threshold none

extrahop-packets-search


Search for specific packets in ExtraHop Reveal(x).

Base Command

extrahop-packets-search

Input

Argument Name Description Required
output The output format. A pcap file, A keylog.txt file that can be loaded in wireshark to decode ssl packets, or a zip file containing both a packets.pcap and keylog.txt. Possible values are: pcap, keylog_txt, zip. Default is pcap. Optional
limit_bytes The maximum number of bytes to return. Default is 10MB. Optional
limit_search_duration The maximum amount of time to run the packet search. The default unit is milliseconds, but other units can be specified with a unit suffix. Default is 5m. Optional
query_from The beginning timestamp of the time range the search will include, expressed in milliseconds since the epoch. A negative value specifies that the search will begin with packets captured at a time in the past relative to the current time. For example, specify -10m to begin the search with packets captured 10 minutes before the time of the request. The default unit for a negative value is milliseconds, but other units can be specified with one of the following unit suffixes: ms, s, m, h, d, w, M, y. See https://docs.extrahop.com/current/rest-api-guide/#supported-time-units- for more details on supported time units and suffixes. Default is -10m. Optional
query_until The ending timestamp of the time range the search will include, expressed in milliseconds since the epoch. A 0 value specifies that the search will end with packets captured at the time of the search. A negative value specifies that the search will end with packets captured at a time in the past relative to the current time. For example, specify -5m to end the search with packets captured 5 minutes before the time of the request. The default unit for a negative value is milliseconds, but other units can be specified with one of the following unit suffixes: ms, s, m, h, d, w, M, y. See https://docs.extrahop.com/current/rest-api-guide/#supported-time-units- for more details on supported time units and suffixes. Optional
bpf The Berkeley Packet Filter (BPF) syntax for the packet search. Optional
ip1 Returns packets sent to or received by the specified IP address. Optional
port1 Returns packets sent from or received on the specified port. Optional
ip2 Returns packets sent to or received by the specified IP address. Optional
port2 Returns packets sent from or received on the specified port. Optional

Context Output

Path Type Description
File.Size Number The size of the file.
File.SHA1 String The SHA1 hash of the file.
File.SHA256 String The SHA256 hash of the file.
File.Name String The name of the file.
File.SSDeep String The SSDeep hash of the file.
File.EntryID String The entry ID of the file.
File.Info String File information.
File.Type String The file type.
File.MD5 String The MD5 hash of the file.
File.Extension String The file extension.

Command example

!extrahop-packets-search ip1=0.0.0.0 ip2=0.0.0.0 limit_bytes=10MB limit_search_duration=10m output=pcap port1=8000 port2=8000 query_from=-15m query_until=0

Human Readable Output

Uploaded file: extrahop 2022-12-15 21.12.29 to 21.27.29 IST.pcapDownload.

Property Value
Type pcap
Size 1,122,020 bytes
Info data
MD5 710737f2d9874690f130da14da38e7cb
SHA1 a89d4696c11ee0a8890d8f4effba8fad891cf05d
SHA256 433f238d350d8eb19979f0f513974d97b9e9f3445f99deb75c0a1f46e54de111
SHA512 fbb914a425d324e4d50bdcf15fc31499720e48d9242005c796d91c345dcb44e1f2fb1435d6bf44c89e0f8256dbae43638f5d8175872bcd29e5bf4fbcba4124cb
SSDeep 12288:WzC9IOFcF8jgBXx00uMOsOFtKu1R4mF48f6G2GeXCuX:Wgo8cNx3QsODKugmnfjcPX

extrahop-devices-tag


Add or remove a tag from devices in ExtraHop Reveal(x).

Base Command

extrahop-devices-tag

Input

Argument Name Description Required
tag The case-sensitive value of the tag. Required
add The list of IP addresses or ExtraHop API IDs of the devices to tag, comma separated. Optional
remove The list of IP addresses or ExtraHop API IDs of the devices to remove the tag from, comma separated. Optional

Context Output

There is no context output for this command.

Command example

!extrahop-devices-tag tag=MyTag add=0.0.0.0 remove=0.0.0.0

Human Readable Output

Successfully tagged untagged the device/s.

extrahop-alert-rule-create


Create a new alert rule in ExtraHop Reveal(x).

Base Command

extrahop-alert-rule-create

Input

Argument Name Description Required
apply_all Indicates whether the alert is assigned to all available data sources. Possible values are: true, false. Required
disabled Indicates whether the alert is disabled. Possible values are: true, false. Required
field_name The name of the monitored metric. Only applicable to threshold alerts. Optional
field_name2 The second monitored metric when applying a ratio. Only applicable to threshold alerts. Optional
field_op The type of comparison between the field_name and field_name2 fields when applying a ratio. Only applicable to threshold alerts. Possible values are: /, null. Optional
interval_length The length of the alert interval, expressed in seconds. Only applicable to threshold alerts. Possible values are: 30, 60, 120, 300, 600, 900, 1200, 1800. Default is 30. Optional
name The unique, friendly name for the alert. Required
notify_snmp Indicates whether to send an SNMP trap when an alert is generated. Possible values are: true, false. Required
object_type The type of metric source monitored by the alert configuration. Only applicable to detection alerts. Possible values are: application, device. Optional
operand The value to compare against alert conditions. The compare method is specified by the value of the operator field. Only applicable to threshold alerts. Optional
operator The logical operator applied when comparing the value of the operand field to alert conditions. Only applicable to threshold alerts. Possible values are: ==, >, <, >=, <=. Optional
param The first alert parameter, which is either a key pattern or a data point. Only applicable to threshold alerts. Optional
param2 The second alert parameter, which is either a key pattern or a data point. Only applicable to threshold alerts. Optional
protocols The list of monitored protocols. Only applicable to detection alerts. Optional
refire_interval The time interval in which alert conditions are monitored, expressed in seconds. Possible values are: 300, 600, 900, 1800, 3600, 7200, 14400. Required
severity The severity level of the alert, which is displayed in the Alert History, email notifications, and SNMP traps. Possible values are: 0, 1, 2, 3, 4, 5, 6, 7. Required
stat_name The statistic name for the alert. Only applicable to threshold alerts. Optional
type The type of alert. Possible values are: detection, threshold. Required
units The interval in which to evaluate the alert condition. Only applicable to threshold alerts. Possible values are: none, period, 1 sec, 1 min, 1 hr. Optional

Context Output

There is no context output for this command.

Command example

!extrahop-alert-rule-create apply_all=true interval_length=30 disabled=false name="test10" notify_snmp=false refire_interval=300 severity=4 type=detection object_type=device protocols="udp"

Human Readable Output

Successfully created alert rule.

extrahop-ticket-track


Link an ExtraHop Reveal(x) detection to a Cortex XSOAR incident.

Base Command

extrahop-ticket-track

Input

Argument Name Description Required
incident_id The ID of the Cortex XSOAR incident to track. Required
detection_id The ID of the ExtraHop Reveal(x) detection to track. Required
incident_owner Owner of the incident. Optional
incident_status Status of the incident. Possible values are: 0, 1, 2, 3. Optional
incident_close_reason Reason the incident was closed. Optional

Context Output

Path Type Description
ExtraHop.TicketId String Cortex XSOAR incident ID successfully tracked to the ExtraHop Reveal(x) detection.

Command example

!extrahop-ticket-track detection_id=1234 incident_id=1 incident_owner=John incident_status=1

Context Example

{
    "ExtraHop": {
        "ExtraHop": {
            "TicketId": "1"
        }
    }
}

Human Readable Output

Successfully linked detection(1234) with incident(1)

extrahop-alert-rule-edit


Modify an alert rule in ExtraHop Reveal(x).

Base Command

extrahop-alert-rule-edit

Input

Argument Name Description Required
alert_id The unique identifier for the alert. Required
apply_all Indicates whether the alert is assigned to all available data sources. Possible values are: true, false. Required
disabled Indicates whether the alert is disabled. Possible values are: true, false. Required
field_name The name of the monitored metric. Only applicable to threshold alerts. Optional
field_name2 The second monitored metric when applying a ratio. Only applicable to threshold alerts. Optional
field_op The type of comparison between the field_name and field_name2 fields when applying a ratio. Only applicable to threshold alerts. Possible values are: /, null. Optional
interval_length The length of the alert interval, expressed in seconds. Only applicable to threshold alerts. Possible values are: 30, 60, 120, 300, 600, 900, 1200, 1800. Default is 30. Optional
name The unique, friendly name for the alert. Required
notify_snmp Indicates whether to send an SNMP trap when an alert is generated. Possible values are: true, false. Required
object_type The type of metric source monitored by the alert configuration. Only applicable to detection alerts. Possible values are: application, device. Optional
operand The value to compare against alert conditions. The compare method is specified by the value of the operator field. Only applicable to threshold alerts. Optional
operator The logical operator applied when comparing the value of the operand field to alert conditions. Only applicable to threshold alerts. Possible values are: ==, >, <, >=, <=. Optional
param The first alert parameter, which is either a key pattern or a data point. Only applicable to threshold alerts. Optional
param2 The second alert parameter, which is either a key pattern or a data point. Only applicable to threshold alerts. Optional
protocols The list of monitored protocols. Only applicable to detection alerts. Optional
refire_interval The time interval in which alert conditions are monitored, expressed in seconds. Possible values are: 300, 600, 900, 1800, 3600, 7200, 14400. Required
severity The severity level of the alert, which is displayed in the Alert History, email notifications, and SNMP traps. Possible values are: 0, 1, 2, 3, 4, 5, 6, 7. Required
stat_name The statistic name for the alert. Only applicable to threshold alerts. Optional
type The type of alert. Possible values are: detection, threshold. Required
units The interval in which to evaluate the alert condition. Only applicable to threshold alerts. Possible values are: none, period, 1 sec, 1 min, 1 hr. Optional

Context Output

There is no context output for this command.

Command example

!extrahop-alert-rule-edit interval_length=30 alert_id=36 apply_all=true disabled=false name="t127" notify_snmp=false refire_interval=300 severity=4 type=detection protocols="udp" object_type=device

Human Readable Output

Successfully updated alert rule.

extrahop-watchlist-edit


Add or remove devices from the advanced analysis watchlist in ExtraHop Reveal(x).

Base Command

extrahop-watchlist-edit

Input

Argument Name Description Required
add The list of IP addresses or ExtraHop API IDs of the devices to add, comma separated. Optional
remove The list of IP addresses or ExtraHop API IDs of the devices to remove, comma separated. Optional

Context Output

There is no context output for this command.

Command example

!extrahop-watchlist-edit add=0.0.0.0 remove=0.0.0.0

Human Readable Output

Successfully added new devices(0.0.0.0) in the watchlist
Successfully removed devices(0.0.0.0) from the watchlist

extrahop-metrics-list


Get metrics for specified objects from ExtraHop Reveal(x).

Base Command

extrahop-metrics-list

Input

Argument Name Description Required
cycle The aggregation period for metrics.
Supported values: “auto”, “1sec”, “30sec”, “5min”, “1hr”, “24hr”. Possible values are: auto, 1sec, 30sec, 5min, 1hr, 24hr.
Required
from_time The beginning timestamp for the request. Return only metrics collected after this time. Time is expressed in milliseconds since the epoch. 0 indicates the time of the request. A negative value is evaluated relative to the current time. The default unit for a negative value is milliseconds, but other units can be specified with a unit suffix.
For example, to request devices active in the last 30 minutes, specify the following parameter value: “-30m”.
Required
metric_category The group of metrics that are searchable in the metric catalog. Required
object_ids The list of numeric values that represent unique identifiers. Unique identifiers can be retrieved through the /networks, /devices, /applications, /vlans, /devicegroups, /activitygroups, and /appliances resources. For system health metrics, specify the ID of the sensor or console and set the object_type parameter to “system”. Required
object_type Indicates the object type of unique identifiers specified in the object_ids property.
Supported values: “network”, “device”, “application”, “vlan”, “device_group”, “system”. Possible values are: network, device, application, vlan, device_group, system.
Required
until_time The ending timestamp for the request. Return only metrics collected before this time. Time is expressed in milliseconds since the epoch. 0 indicates the time of the request. A negative value is evaluated relative to the current time. The default unit for a negative value is milliseconds, but other units can be specified with a unit suffix.
For example, to request devices active in the last 30 minutes, specify the following parameter value: “-30m”.
Required
metric_specs An array of metric specification objects.
Refer to the ExtraHop REST API Guide at https://docs.extrahop.com/current/rest-api-guide/.
Required

Context Output

Path Type Description
ExtraHop.Metrics.cycle String The aggregation period for metrics.
ExtraHop.Metrics.node_id Number Node ID of the sensor associated with the object.
ExtraHop.Metrics.clock Number The current time.
ExtraHop.Metrics.from Number The beginning time from which metrics were collected.
ExtraHop.Metrics.until Number The ending time that metrics were collected.
ExtraHop.Metrics.stats.oid Number The ID of the object.
ExtraHop.Metrics.stats.time Number The time for which metrics were collected.
ExtraHop.Metrics.stats.duration Number The duration that metrics were collected.
ExtraHop.Metrics.stats.values Unknown The count value of the metrics that were collected.

Command example

!extrahop-metrics-list cycle=auto from_time=0 metric_category=http object_ids=0 object_type=application until_time=0 metric_specs="[{\"name\": \"req\", \"key\": \"/GET/\"}]"

Context Example

{
    "ExtraHop": {
        "Metrics": {
            "clock": 1676883600000,
            "cycle": "1hr",
            "from": 0,
            "node_id": 0,
            "stats": [
                {
                    "duration": 3600000,
                    "oid": 0,
                    "time": 1637740800000,
                    "values": [
                        345
                    ]
                },
                {
                    "duration": 3600000,
                    "oid": 0,
                    "time": 1637744400000,
                    "values": [
                        178
                    ]
                },
                {
                    "duration": 3600000,
                    "oid": 0,
                    "time": 1637751600000,
                    "values": [
                        744
                    ]
                }
            ],
            "until": 1676883600000
        }
    }
}

Human Readable Output

Metrics Found:

Cycle 30 sec
Node Id 0
Clock 1676873250000
From Time 1676871390000
Until Time 1676871990000
Stats {‘oid’: 0, ‘time’: 1637740800000, ‘duration’: 30000, ‘values’: [4]},
{‘oid’: 0, ‘time’: 1676871420000, ‘duration’: 30000, ‘values’: [9]},
{‘oid’: 0, ‘time’: 1676871450000, ‘duration’: 30000, ‘values’: [4]},

extrahop-detections-list


Get detections from ExtraHop Reveal(x).

Base Command

extrahop-detections-list

Input

Argument Name Description Required
filter Detection-specific filters.
For eg:
{
“categories”: [“sec.attack”],
“risk_score_min”: 51
}

If the categories and category are not specified, then categories will be set to [“sec.attack”]. The category field is deprecated by the API, so please use the categories field instead.
Refer to the ExtraHop REST API guide at https://docs.extrahop.com/current/rest-api-guide/.
Optional
from Returns detections that occurred after the specified date, expressed in milliseconds since the epoch. Detections that started before the specified date are returned if the detection was ongoing at that time.

For eg:
from=1673508360001.
Optional
limit Returns no more than the specified number of detections.

For eg:
limit=10. Default is 200.
Optional
offset The number of detections to skip for pagination.

For eg:
offset=100.
Optional
sort Sorts returned detections by the specified fields.
Comma separated “field” “direction” is the accepted format.
By default, detections are sorted by most recent update time and then id in ascending order.

For eg:
sort=”end_time asc,id desc”.
Optional
until Return detections that ended before the specified date, expressed in milliseconds since the epoch.

For eg:
until=1673509360001.
Optional
mod_time Return detections that were modified on or after the specified date, expressed in milliseconds since the epoch.

For eg: 1675416916102 .
Optional

Context Output

Path Type Description
ExtraHop.Detections.id Number The unique detection ID of the detection.
ExtraHop.Detections.start_time Number The timestamp when the detection was identified.
ExtraHop.Detections.mod_time Number The timestamp when the detection was last modified.
ExtraHop.Detections.end_time Number The timestamp when the detection was completed.
ExtraHop.Detections.title String The title of the detection.
ExtraHop.Detections.description String The description of the event for which the detection was created.
ExtraHop.Detections.categories Unknown The categories associated with the detection.
ExtraHop.Detections.risk_score Number The risk level of the event.
ExtraHop.Detections.type String The detection type.
ExtraHop.Detections.properties Unknown The detection properties.
ExtraHop.Detections.participants Unknown The participants involved in the event.
ExtraHop.Detections.ticket_id String The unique ticket ID for the detection that is being tracked.
ExtraHop.Detections.assignee String The user assigned to the detection.
ExtraHop.Detections.status String The status of the detection.
ExtraHop.Detections.resolution String The resolution status of the detection.
ExtraHop.Detections.mitre_tactics Unknown The MITRE tactics associated with the attack.
ExtraHop.Detections.mitre_techniques Unknown The MITRE techniques associated with the attack.
ExtraHop.Detections.appliance_id Number The unique identifier of the sensor on which the attack was detected.
ExtraHop.Detections.is_user_created Boolean Indicates whether the detection is user-created.

Command example

!extrahop-detections-list limit=3

Context Example

{
    "ExtraHop": {
        "Detections": [
            {
                "appliance_id": 0,
                "categories": [
                    "sec",
                    "sec.exploit"
                ],
                "description": "The offender was recently observed carrying out a TCP SYN Scan and has now made a successful TCP 3-way handshake to the victim device. Investigate to determine if this is the result of the SYN Scan.",
                "end_time": 1676895361452,
                "id": 1110161,
                "is_user_created": true,
                "participants": [
                    {
                        "external": false,
                        "id": 2187135,
                        "object_type": "ipaddr",
                        "object_value": "0.0.0.0",
                        "role": "offender"
                    },
                    {
                        "external": true,
                        "id": 2187136,
                        "object_type": "ipaddr",
                        "object_value": "0.0.0.2",
                        "role": "victim"
                    }
                ],
                "risk_score": 50,
                "start_time": 1676895361452,
                "title": "Test_Detection_1_1676895361452",
                "type": "Test_Detection_1_1676895361452",
                "mod_time": 1676895361452
            },
            {
                "appliance_id": 0,
                "categories": [
                    "sec",
                    "sec.exploit"
                ],
                "description": "The offender was recently observed carrying out a TCP SYN Scan and has now made a successful TCP 3-way handshake to the victim device. Investigate to determine if this is the result of the SYN Scan.",
                "end_time": 1676895331451,
                "id": 1110160,
                "is_user_created": true,
                "participants": [
                    {
                        "external": false,
                        "id": 2187133,
                        "object_type": "ipaddr",
                        "object_value": "0.0.0.0",
                        "role": "offender"
                    },
                    {
                        "external": true,
                        "id": 2187134,
                        "object_type": "ipaddr",
                        "object_value": "0.0.0.2",
                        "role": "victim"
                    }
                ],
                "risk_score": 50,
                "start_time": 1676895331451,
                "title": "Test_Detection_1_1676895331451",
                "type": "Test_Detection_1_1676895331451",
                "mod_time": 1676895331451
            },
            {
                "appliance_id": 0,
                "categories": [
                    "sec",
                    "sec.exploit"
                ],
                "description": "The offender was recently observed carrying out a TCP SYN Scan and has now made a successful TCP 3-way handshake to the victim device. Investigate to determine if this is the result of the SYN Scan.",
                "end_time": 1676895301451,
                "id": 1110159,
                "is_user_created": true,
                "participants": [
                    {
                        "external": false,
                        "id": 2187131,
                        "object_type": "ipaddr",
                        "object_value": "0.0.0.0",
                        "role": "offender"
                    },
                    {
                        "external": true,
                        "id": 2187132,
                        "object_type": "ipaddr",
                        "object_value": "0.0.0.2",
                        "role": "victim"
                    }
                ],
                "risk_score": 50,
                "start_time": 1676895301451,
                "title": "Test_Detection_1_1676895301451",
                "type": "Test_Detection_1_1676895301451",
                "mod_time": 1676895301451
            }
        ]
    }
}

Human Readable Output

Found 3 Detection(s)

Detection ID Risk Score Description Categories Start Time
1110161 50 The offender was recently observed carrying out a TCP SYN Scan and has now made a successful TCP 3-way handshake to the victim device. Investigate to determine if this is the result of the SYN Scan. sec,
sec.exploit
1676895361452
1110160 50 The offender was recently observed carrying out a TCP SYN Scan and has now made a successful TCP 3-way handshake to the victim device. Investigate to determine if this is the result of the SYN Scan. sec,
sec.exploit
1676895331451
1110159 50 The offender was recently observed carrying out a TCP SYN Scan and has now made a successful TCP 3-way handshake to the victim device. Investigate to determine if this is the result of the SYN Scan. sec,
sec.exploit
1676895301451

Additional Information

ExtraHop Reveal(x) Playbooks

  • ExtraHop - Default
  • ExtraHop - CVE-2019-0708 (BlueKeep)
  • ExtraHop - Ticket Tracking
  • ExtraHop - Get Peers by Host

Use Cases

  • Create incidents for every detection that ExtraHop Reveal(x) surfaces in real-time.
  • Enable guided investigation and response through playbooks and automation scripts.
  • Query the ExtraHop Reveal(x) REST API using the simple and powerful Cortex XSOAR CLI.

Configuration parameters

  • isFetch — Fetch incidents
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • on_cloud — On Cloud
  • url — URL (required)
  • apikey — API Key
  • client_id — Client ID
  • client_secret — Client Secret
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • first_fetch — First fetch time
  • max_fetch — How many incidents to fetch each time
  • advanced_filter — Advanced Filter

Commands (30)

  • extrahop-activity-map-get

    Get a link to a live activity map in ExtraHop Reveal(x).

  • extrahop-alert-rule-create

    Create a new alert rule in ExtraHop Reveal(x).

  • extrahop-alert-rule-edit

    Modify an alert rule in ExtraHop Reveal(x).

  • extrahop-alert-rules-get

    Get all alert rules from ExtraHop Reveal(x).

  • extrahop-create-alert Deprecated

    Use extrahop-alert-rule-create instead. Create a new alert rule in Reveal(x).

  • extrahop-create-alert-rule Deprecated

    Use extrahop-alert-rule-create instead. Create a new alert rule in Reveal(x).

  • extrahop-detections-list

    Get detections from ExtraHop Reveal(x).

  • extrahop-device-search Deprecated

    Use extrahop-devices-search instead. Search for devices in Reveal(x).

  • extrahop-devices-search

    Search for devices in ExtraHop Reveal(x).

  • extrahop-devices-tag

    Add or remove a tag from devices in ExtraHop Reveal(x).

  • extrahop-edit-alert Deprecated

    Use extrahop-alert-rule-edit instead. Modify an alert rule in Reveal(x).

  • extrahop-edit-alert-rule Deprecated

    Use extrahop-alert-rule-edit instead. Modify an alert rule in Reveal(x).

  • extrahop-edit-watchlist Deprecated

    Use extrahop-watchlist-edit instead. Add or remove devices from the watchlist in Reveal(x).

  • extrahop-get-activity-map Deprecated

    Use extrahop-activity-map-get instead. Get a link to a live activity map in Reveal(x).

  • extrahop-get-alert-rules Deprecated

    Use extrahop-alert-rules-get instead. Get all alert rules from Reveal(x).

  • extrahop-get-alerts Deprecated

    Use extrahop-alert-rules-get instead. Get all alert rules from Reveal(x).

  • extrahop-get-peers Deprecated

    Use extrahop-peers-get instead. Get all peers for a device from Reveal(x).

  • extrahop-get-protocols Deprecated

    Use extrahop-protocols-get instead. Get all active network protocols for a device from Reveal(x).

  • extrahop-get-watchlist Deprecated

    Use extrahop-watchlist-get instead. Get all devices on the watchlist in Reveal(x).

  • extrahop-metrics-list

    Get metrics for specified objects from ExtraHop Reveal(x).

  • extrahop-packets-search

    Search for specific packets in ExtraHop Reveal(x).

  • extrahop-peers-get

    Get all peers for a device from ExtraHop Reveal(x).

  • extrahop-protocols-get

    Get all active network protocols for a device from ExtraHop Reveal(x).

  • extrahop-query-records Deprecated

    No available replacement. Query records from Reveal(x).

  • extrahop-search-packets Deprecated

    Use extrahop-packets-search instead. Search for specific packets in Reveal(x).

  • extrahop-tag-devices Deprecated

    Use extrahop-devices-tag instead. Add or remove a tag from devices in Reveal(x).

  • extrahop-ticket-track

    Link an ExtraHop Reveal(x) detection to a Cortex XSOAR incident.

  • extrahop-track-ticket Deprecated

    Use extrahop-ticket-track instead. Link a Reveal(x) detection to a Demisto Investigation.

  • extrahop-watchlist-edit

    Add or remove devices from the advanced analysis watchlist in ExtraHop Reveal(x).

  • extrahop-watchlist-get

    Get all devices on the advanced analysis watchlist in ExtraHop Reveal(x).

import json
import traceback
from collections import defaultdict
from ipaddress import ip_address

import demistomock as demisto  # noqa: F401
import urllib3
from CommonServerPython import *  # noqa: F401
from requests import Response

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """

VALID_PEER_ROLES = ["any", "client", "server"]

VALID_PROTOCOLS = [
    "any",
    "AAA",
    "ActiveMQ",
    "AJP",
    "amf",
    "CIFS",
    "DB",
    "DHCP",
    "DICOM",
    "DNS",
    "FIX",
    "FTP",
    "HL7",
    "HTTP",
    "IBMMQ",
    "ICA",
    "IKE/ISAKMP",
    "IMAP",
    "IPFIX",
    "IPsec NAT-T",
    "IRC",
    "iSCSI",
    "Kerberos",
    "L2TP",
    "LDAP",
    "lync-compress",
    "memcache",
    "Modbus",
    "MongoDB",
    "MSMQ",
    "MSN",
    "MSRPC",
    "NetFlow",
    "NFS",
    "NTP",
    "OpenVPN",
    "PCoIP",
    "Perforce",
    "POP3",
    "RDP",
    "Redis",
    "RFB",
    "RTCP",
    "RTP",
    "sFlow",
    "SIP",
    "SMPP",
    "SMTP",
    "SNMP",
    "SSH",
    "SSL",
    "Syslog",
    "TCP",
    "telnet",
    "UDP",
    "WebSocket",
]

VALID_DEVICE_ROLES = [
    "db_server",
    "dhcp_server",
    "dns_server",
    "file_server",
    "firewall",
    "gateway",
    "http_server",
    "domain_controller",
    "web_proxy",
    "load_balancer",
    "pc",
    "medical_device",
    "mobile_device",
    "printer",
    "scanner",
    "custom",
    "voip_phone",
    "other",
]

VALID_DEVICE_SOFTWARES = [
    "android",
    "apple_ios",
    "arista_eos",
    "cisco_ios",
    "cisco_nx-os",
    "chrome_os",
    "linux",
    "mac_os",
    "windows",
    "windows_server",
    "windows_server_2008",
    "windows_server_2008_r2",
    "windows_server_2012",
    "windows_server_2012_r2",
    "windows_server_2016",
    "windows_vista",
    "windows_7",
    "windows_8",
    "windows_8.1",
    "windows_10",
]

VALID_DEVICE_VENDORS = [
    "alcatel-lucent",
    "apple",
    "arista",
    "asus",
    "brother",
    "canon",
    "cisco",
    "cisco-linksys",
    "citrix",
    "dell",
    "dellemc",
    "d-link",
    "emc",
    "f5",
    "google",
    "hp",
    "htc",
    "huawei",
    "ibm",
    "juniper",
    "kyocera",
    "microsoft",
    "netapp",
    "netgear",
    "nokia",
    "nortel",
    "oracle",
    "paloalto",
    "samsung",
    "3com",
    "toshiba",
    "virtualbox",
    "vmware",
    "zte",
]

VALID_DEVICE_ACTIVITIES = [
    "aaa_client",
    "aaa_server",
    "ajp_client",
    "ajp_server",
    "amf_client",
    "amf_server",
    "cifs_client",
    "cifs_server",
    "db_client",
    "db_server",
    "dhcp_client",
    "dhcp_server",
    "dicom_client",
    "dicom_server",
    "dns_client",
    "dns_server",
    "fix_client",
    "fix_server",
    "ftp_client",
    "ftp_server",
    "hl7_client",
    "hl7_server",
    "http_client",
    "http_server",
    "ibmmq_client",
    "ibmmq_server",
    "ica_client",
    "ica_server",
    "icmp",
    "iscsi_client",
    "iscsi_server",
    "kerberos_client",
    "kerberos_server",
    "ldap_client",
    "ldap_server",
    "llmnr_client",
    "llmnr_server",
    "memcache_client",
    "memcache_server",
    "modbus_client",
    "modbus_server",
    "mongo_client",
    "mongo_server",
    "msmq",
    "nbns_client",
    "nbns_server",
    "nfs_client",
    "nfs_server",
    "pcoip_client",
    "pcoip_server",
    "pop3_client",
    "pop3_server",
    "rdp_client",
    "rdp_server",
    "redis_client",
    "redis_server",
    "rfb_client",
    "rfb_server",
    "rpc_client",
    "rpc_server",
    "rtcp",
    "rtp",
    "scanner",
    "sip_client",
    "sip_server",
    "smpp_client",
    "smpp_server",
    "smtp_client",
    "smtp_server",
    "ssh_client",
    "ssh_server",
    "ssl_client",
    "ssl_server",
    "tcp",
    "telnet_client",
    "telnet_server",
    "udp",
    "websocket_client",
    "websocket_server",
    "wsman_client",
    "wsman_server",
]

VALID_DEVICE_OPERATORS = [">", "<", "<=", ">=", "=", "!=", "startswith", "exists", "not_exists", "~", "!~"]

VALID_DEVICE_MATCH_TYPES = ["and", "or", "not"]

VALID_TIME_INTERVALS = ["30 minutes", "6 hours", "1 day", "1 week"]

VALID_FILE_FORMATS = ["pcap", "keylog_txt", "zip"]

VALID_INCIDENT_STATUS = ["0", "1", "2", "3"]

TICKET_STATUS_MAP = {
    "0": "new",  # pending
    "1": "in_progress",  # active
    "2": "closed",  # done
    "3": "acknowledged",  # archived
}

TICKET_SEVERITY = {
    "0-39": 1,  # low
    "40-69": 2,  # medium
    "70-89": 3,  # high
    "90-100": 4,  # critical
}

VALID_ALERT_RULE_REFIRE_INTERVALS = ["300", "600", "900", "1800", "3600", "7200", "14400"]

VALID_ALERT_RULE_TYPE = ["threshold", "detection"]

VALID_ALERT_RULE_SEVERITY = ["0", "1", "2", "3", "4", "5", "6", "7"]

VALID_ALERT_RULE_INTERVAL_LENGTH = ["30", "60", "120", "300", "600", "900", "1200", "1800"]

SORT_DIRECTION = ["asc", "desc"]

VALID_ALERT_RULE_OPERATOR = ["==", ">", "<", ">=", "<="]

VALID_ALERT_RULE_UNITS = ["none", "period", "1 sec", "1 min", "1 hr"]

VALID_ALERT_RULE_OBJECT_TYPES = ["application", "device"]

VALID_CYCLES = ["auto", "1sec", "30sec", "5min", "1hr", "24hr"]

VALID_OBJECT_TYPES = ["device", "network", "application", "vlan", "device_group", "system"]

VALID_METRICS_KEYS = ["cycle", "from", "metric_category", "metric_specs", "object_ids", "object_type", "until"]

VALID_DETECTION_KEYS = ["filter", "limit", "offset", "from", "until", "sort", "mod_time"]

VALID_FILTER_KEYS = ["assignee", "categories", "category", "resolution", "risk_score_min", "status", "ticket_id", "types"]

DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"

DEFAULT_FETCH_TYPE = "ExtraHop Detections"

FIRST_FETCH = "3 days"

MAX_FETCH = 200

EXTRAHOP_MARKDOWN_REGEX = r"(\[[^\]]+\]\(\#\/[^\)]+\))+"

XSOAR_VERSION = get_demisto_version_as_str() or "6.5.0"

PACK_VERSION = get_pack_version() or "2.3.1"

MINIMUM_FIRMWARE_VERSION = "9.3.0"

""" CLIENT CLASS """


class ExtraHopClient(BaseClient):
    def __init__(
        self,
        base_url: str,
        api_key: str,
        client_id: str,
        client_secret: str,
        verify: bool,
        use_proxy: bool,
        ok_codes: tuple,
        on_cloud: bool,
    ) -> None:
        """
        Prepare constructor for Client class.

        Calls the constructor of BaseClient class and updates the header with the authentication token.

        Args:
            base_url: The url of ExtraHop instance.
            api_key: The api key to use in header.
            client_id: The Client ID to use for authentication.
            client_secret: The Client Secret to use for authentication.
            verify: True if verify SSL certificate is checked in integration configuration, False otherwise.
            use_proxy: True if the proxy server needs to be used, False otherwise.
        """

        super().__init__(base_url=base_url, verify=verify, ok_codes=ok_codes, proxy=use_proxy)

        # Setting up access token in headers.
        if on_cloud:
            self._headers: Dict[str, Any] = {
                "Authorization": f"Bearer {self.get_access_token(client_id=client_id, client_secret=client_secret)}",
                "ExtraHop-Integration": f"XSOAR-{XSOAR_VERSION}-ExtraHop-{PACK_VERSION}",
            }
        else:
            self._headers = {
                "Accept": "application/json",
                "Authorization": f"ExtraHop apikey={api_key}",
                "ExtraHop-Integration": f"XSOAR-{XSOAR_VERSION}-ExtraHop-{PACK_VERSION}",
            }

    def get_access_token(self, client_id: str, client_secret: str) -> str:
        """Return the token stored in integration context.

        If the token has expired or is not present in the integration context
        (in the first case), it calls the Authentication function, which
        generates a new token and stores it in the integration context.

        Args:
            client_id: The Client ID to use for authentication.
            client_secret: The Client Secret to use for authentication.

        Returns:
            str: Authentication token stored in integration context.
        """
        integration_context = get_integration_context()
        token = integration_context.get("access_token")
        valid_until = integration_context.get("valid_until")
        time_now = int(time.time())

        # If token exists and is valid, then return it.
        if (token and valid_until) and (time_now < valid_until):
            demisto.info("Extrahop token returned from integration context.")
            return token

        # Otherwise, generate a new token and store it.
        token, expires_in = self.authenticate(client_id=client_id, client_secret=client_secret)
        integration_context = {
            "access_token": token,
            "valid_until": time_now + expires_in,  # Token expiration time - 30 mins
        }
        set_integration_context(integration_context)

        return token

    def authenticate(self, client_id: str, client_secret: str) -> tuple[str, int]:
        """
        Get the access token from the ExtraHop API.

        Args:
            client_id: The Client ID to use for authentication.
            client_secret: The Client Secret to use for authentication.

        Returns:
            tuple[str,int]: The token and its expiration time in seconds received from the API.
        """
        demisto.info("Generating new authentication token.")

        req_headers = {
            "cache-control": "no-cache",
            "content-type": "application/x-www-form-urlencoded",
        }
        req_body = {
            "grant_type": "client_credentials",
        }
        response = self._http_request(
            method="POST", url_suffix="/oauth2/token", data=req_body, headers=req_headers, auth=(client_id, client_secret)
        )
        token = response.get("access_token")
        expires_in = response.get("expires_in")

        return token, expires_in

    def test_connection(self):
        """Test the authentication."""
        return self._http_request(method="GET", url_suffix="/api/v1/extrahop", error_handler=authentication_error_handler)

    def get_extrahop_version(self):
        """Retrieve the ExtraHop version."""
        return self._http_request(method="GET", url_suffix="/api/v1/extrahop/version")

    def get_appliance_uuids(self):
        """Retrieve the appliance IDs.

        Returns:
            Response from the API.
        """
        networks = self._http_request("GET", url_suffix="/api/v1/networks")
        uuid_lookup = {}
        for network in networks:
            uuid_lookup[network["node_id"]] = network.get("appliance_uuid")
        return uuid_lookup

    def get_device_by_id(self, device_id: str, ok_codes: tuple = None):
        """Retrieve the device from the Reveal(X).

        Args:
            device_id: Unique ID of the device to be retrieved.
            ok_codes: The status code to not raise error for while fetching detections.

        Returns:
            Response from the API.
        """
        return self._http_request("GET", url_suffix=f"/api/v1/devices/{device_id}", ok_codes=ok_codes)

    def device_search(
        self,
        name: Optional[str],
        ip: Optional[str],
        mac: Optional[str],
        role: Optional[str],
        software: Optional[str],
        vendor: Optional[str],
        tag: Optional[str],
        discover_time: Optional[str],
        vlan: Optional[str],
        activity: Optional[str],
        operator: Optional[str],
        match_type: Optional[str],
        active_from: Optional[str],
        active_until: Optional[str],
        limit: Optional[int],
        l3_only: Optional[bool],
    ):
        """Searches for a device from the Reveal(X).

        Args:
            name: Name of the device.
            ip: IP address of the device.
            mac: Mac address of the device.
            role: Role of the device.
            software: Software used in the device.
            vendor: Vendor of the device.
            tag: Tags of the device.
            discover_time: The time device was discovered.
            vlan: vlan of the device.
            activity: activity information of the device.
            operator: Operator of the device.
            match_type: Match type of the device.
            active_from: Date the device is active from.
            active_until: Date the device was last active.
            limit: Limit of the response
            l3_only: Whether the device is a layer 3 device.

        Returns:
            Response from the API.
        """
        fields = {
            "name": name,
            "ipaddr": ip,
            "macaddr": mac,
            "role": role,
            "software": software,
            "vendor": vendor,
            "tag": tag,
            "discover_time": discover_time,
            "vlan": vlan,
            "activity": activity,
        }

        data: Dict[str, Any] = {}
        if active_from:
            data["active_from"] = int(active_from)
        if active_until:
            data["active_until"] = int(active_until)
        if limit:
            data["limit"] = int(limit)
        if any(val is not None for val in fields.values()):
            data["filter"] = {"operator": match_type, "rules": []}
            rules_list = data["filter"]["rules"]

            if l3_only:
                rules_list.append({"field": "ipaddr", "operator": "exists"})
                if match_type != "and":
                    data["filter"]["operator"] = "and"
                    rules_list.append({"operator": match_type, "rules": []})
                    rules_list = data["filter"]["rules"][1]["rules"]

            for field in fields.items():
                if field[1]:
                    search_filter = {"field": field[0], "operator": operator, "operand": field[1]}
                    rules_list.append(search_filter)
        return self._http_request("POST", url_suffix="/api/v1/devices/search", json_data=data)

    def get_watchlist(self):
        """Retrieve all the devices on the watchlist in Reveal(X).

        Returns:
            Response from the API.
        """
        return self._http_request("GET", url_suffix="/api/v1/watchlist/devices")

    def edit_watchlist(self, body: Dict):
        """Retrieve all devices that are in the watchlist.

        Args:
            body: Json payload to pass with the API.
        """

        self._http_request("POST", url_suffix="/api/v1/watchlist/devices", json_data=body, resp_type="response")

    def get_peers(self, body: Dict):
        """Retrieve the peers of a device from Reveal(X).

        Returns:
            Response from the API.
        """
        return self._http_request("POST", url_suffix="/api/v1/activitymaps/query", json_data=body)

    def get_alert_rules(self):
        """Retrieve the alert rules from Reveal(X).

        Returns:
            Response from the API.
        """
        return self._http_request("GET", url_suffix="/api/v1/alerts")

    def patch_detections(self, detection_id: str, body: Dict):
        """Patch a detection with provided fields.

        Args:
            detection_id: The unique identifier for the detection.
            body: Json payload to pass with the API.
        Returns:
            Response from the API.
        """
        return self._http_request("PATCH", url_suffix=f"/api/v1/detections/{detection_id}", json_data=body, resp_type="response")

    def packets_search(
        self,
        output: Optional[str],
        limit_bytes: Optional[str],
        limit_search_duration: Optional[str],
        query_from: Optional[str],
        query_until: Optional[str],
        bpf: Optional[str],
        ip1: Optional[str],
        port1: Optional[str],
        ip2: Optional[str],
        port2: Optional[str],
    ):
        """Retrieve the specific packets from Reveal(X).

        Args:
            output: The output format
            limit_bytes: The maximum number of bytes to return.
            limit_search_duration: The maximum amount of time to run the packet search.
            query_from: The beginning timestamp of the time range the search will include.
            query_until: The ending timestamp of the time range the search will include.
            bpf: The Berkeley Packet Filter (BPF) syntax for the packet search.
            ip1: Returns packets sent to or received by the specified IP address.
            port1: Returns packets sent from or received on the specified port.
            ip2: Returns packets sent to or received by the specified IP address.
            port2: Returns packets sent from or received on the specified port.

        Returns:
            Response from the API.
        """
        body = {
            "output": output,
            "limit_bytes": limit_bytes,
            "limit_search_duration": limit_search_duration,
            "always_return_body": False,
            "from": query_from,
            "until": query_until,
            "bpf": bpf,
            "ip1": ip1,
            "port1": port1,
            "ip2": ip2,
            "port2": port2,
        }

        return self._http_request("GET", url_suffix="/api/v1/packets/search", json_data=body, resp_type="response")

    def get_all_tags(self):
        """Retrieve all available tags from Reveal(X).

        Returns:
            Response from the API.
        """
        return self._http_request("GET", url_suffix="/api/v1/tags")

    def create_new_tag(self, data: Dict):
        """Create a new tag.

        Returns:
            Response from the API.
        """
        return self._http_request("POST", url_suffix="/api/v1/tags", json_data=data, resp_type="response")

    def tag_untag_devices(self, tag_id: str, data: Dict):
        """Tag and untag devices for the given tag id.

        Returns:
            Response from the API.
        """
        return self._http_request("POST", url_suffix=f"/api/v1/tags/{tag_id}/devices", json_data=data, resp_type="response")

    def create_alert_rule(self, body: Dict):
        """Create a new alert rule with specified value.

        Args:
            body: Request body of alert rule.

        Returns:
            Response from the API.
        """
        return self._http_request(
            "POST", url_suffix="/api/v1/alerts", json_data=body, empty_valid_codes=[201], return_empty_response=True
        )

    def update_alert_rule(self, alert_id, body):
        """Update alert rule with specified value.

        Args:
            alert_id: The unique identifier of the alert.
            body: Json payload to pass with the API.

        Returns:
            Response from the API.
        """
        return self._http_request("PATCH", url_suffix=f"/api/v1/alerts/{alert_id}", json_data=body, return_empty_response=True)

    def metrics_list(self, body: Dict):
        """Retrieves metric information collected about every object from the Reveal(X).
        Args:
            body: Request body of metric list.

        Returns:
            Response from the API.
        """
        return self._http_request("POST", url_suffix="/api/v1/metrics", json_data=body)

    def detections_list(self, body):
        """Retrieve the detections from Reveal(X).

        Returns:
            Response from the API.
        """
        return self._http_request("POST", url_suffix="/api/v1/detections/search", json_data=body)

    def get_detections_by_id(self, detection_id):
        """Retrieve the detections from Reveal(X).

        Returns:
            Response from the API.
        """
        return self._http_request("GET", url_suffix=f"/api/v1/detections/{detection_id}")

    def get_query_records(self, body: Dict):
        """Get query records for specified filter.
        Args:
            body: JSON payload to pass with the API.
        """
        return self._http_request("POST", url_suffix="/api/v1/records/search", json_data=body)

    def get_next_page_records(self, cursor):
        """Get next page records from specified cursor value.
        Args:
            cursor: Cursor value to get records.
        """
        body = {"cursor": cursor}
        params = {"context_ttl": 30000}
        return self._http_request("POST", url_suffix="/api/v1/records/cursor", json_data=body, params=params)


""" EXCEPTION CLASS """


class InvalidValueError(Exception):
    """Custom exception class for invalid values."""

    def __init__(self, arg_name="", arg_value="", arg_list=[], message=""):
        if not message:
            message = f"{arg_value} is an invalid value for {arg_name}. Possible values are: {arg_list}"
        super().__init__(message)


""" HELPER FUNCTIONS """


def modify_description(base_url, description):
    """Modify descriptions of the detections list.

    Args:
        base_url: Base URL of the instance.
        description: Detection description.

    Returns:
        Updated description.
    """
    new_link = f"{base_url}/extrahop/#"

    markdown_data = re.findall(EXTRAHOP_MARKDOWN_REGEX, description)

    for markdown in markdown_data:
        # Replacing the '#' to the extrahop platform url
        if "/extrahop/#" in markdown:
            new_markdown = markdown.replace("#/extrahop", base_url)
        else:
            new_markdown = markdown.replace("#", new_link)
        description = description.replace(markdown, new_markdown)
    return description


def get_extrahop_server_version(client: ExtraHopClient):
    """Retrieve and parse the extrahop server version.

    Args:
        client: ExtraHop client to be used.

    Returns:
        The parsed version of the current extrahop server.

    """
    version = client.get_extrahop_version().get("version")
    temp = version.split(".")
    version = ".".join(temp[:3])
    return version


def remove_empty_elements_from_response(data: Union[Dict, List, None, str]) -> Union[Dict, List, str, None]:
    """Recursively remove empty lists, empty dicts, or None elements from a dictionary.

    Args:
        data: Data from which empty elements are to be removes

    Returns:
        Data with no empty fields.
    """

    def empty(x):
        return x is None or x == {} or x == [] or x == ""

    if not isinstance(data, dict | list):
        return data
    elif isinstance(data, list):
        return [v for v in (remove_empty_elements_from_response(v) for v in data) if not empty(v)]
    else:
        return {k: v for k, v in ((k, remove_empty_elements_from_response(v)) for k, v in data.items()) if not empty(v)}


def authentication_error_handler(res: Response) -> None:
    """Handle 400, 401, 403, 404, 5XX error.

    Args:
        res: The response object obtained from API.

    Raises: ValueError if the status code is 401 or 400.
    """
    if res.status_code == 400:
        raise ValueError("Error code 400: Attempt to bad request.")

    elif res.status_code == 401:
        raise ValueError("Error code 401: Invalid credentials provided.")

    elif res.status_code == 403:
        raise ValueError("Error code 403: Attempt to access forbidden resource.")

    elif res.status_code == 404:
        raise ValueError("Error code 404: The requested resource cannot be found.")

    else:
        raise ValueError("Internal server error.")


def trim_spaces_from_args(args: Dict) -> Dict:
    """Trim spaces from values of the args dict.

    Args:
        args: Dict to trim spaces from.

    Returns:
        Arguments after trim spaces.
    """
    for key, val in args.items():
        if isinstance(val, str):
            args[key] = val.strip()

    return args


def remove_api_from_base_url(url: str) -> str:
    """Prepare URL from base URL required for human-readable.

    Args:
        url: Base URL of the cloud instance.

    Returns:
        ExtraHop cloud instance URL.
    """
    url = url.split(".")
    url.pop(1)
    return ".".join(url)


def validate_peers_get_arguments(peer_role: Optional[Any], protocol: Optional[Any]) -> None:
    """Validate arguments for peers-get command.

    Args:
        peer_role: The role of the peer device in relation to the origin device.
        protocol: The protocol over which the source device is communicating.
    """

    if peer_role and peer_role not in VALID_PEER_ROLES:
        raise InvalidValueError("peer_role", peer_role, VALID_PEER_ROLES)

    if protocol and protocol not in VALID_PROTOCOLS:
        raise InvalidValueError("protocol", protocol, VALID_PROTOCOLS)


def validate_device_search_arguments(
    role: Optional[str],
    software,
    vendor: Optional[str],
    activity: Optional[str],
    operator: Optional[str],
    match_type: Optional[str],
    l3_only: Optional[bool],
) -> None:
    """Validate arguments for peers-get command.

    Args:
        role: The role of the device.
        software: The OS of the device.
        vendor: The vendor of the device, based on MAC address via OUI lookup.
        activity: The activity of the device.
        operator: The compare method applied when matching the fields against their values
        match_type: The match operator to use when chaining the search fields together.
        l3_only: Only returns layer 3 devices by filtering out any layer 2 parent devices.
    """
    if role and role not in VALID_DEVICE_ROLES:
        raise InvalidValueError("role", role, VALID_DEVICE_ROLES)

    if software and software not in VALID_DEVICE_SOFTWARES:
        raise InvalidValueError("software", software, VALID_DEVICE_SOFTWARES)

    if vendor and vendor not in VALID_DEVICE_VENDORS:
        raise InvalidValueError("vendor", vendor, VALID_DEVICE_VENDORS)

    if activity and activity not in VALID_DEVICE_ACTIVITIES:
        raise InvalidValueError("activity", activity, VALID_DEVICE_ACTIVITIES)

    if operator and operator not in VALID_DEVICE_OPERATORS:
        raise InvalidValueError("operator", operator, VALID_DEVICE_OPERATORS)

    if match_type and match_type not in VALID_DEVICE_MATCH_TYPES:
        raise InvalidValueError("match_type", match_type, VALID_DEVICE_MATCH_TYPES)

    if l3_only:
        argToBoolean(l3_only)


def validate_activity_map_get_arguments(
    ip_or_id: Optional[str],
    time_interval: str,
    from_time: Optional[str],
    until_time: Optional[str],
    peer_role: str,
    protocol: str,
) -> None:
    """Validate arguments for peers-get command.

    Args:
        ip_or_id: IP address or unique ID of the device.
        time_interval: Time interval of the live activity map.
        from_time: The beginning timestamp of a fixed time range.
        until_time: The ending timestamp of a fixed time range.
        peer_role: The role of the peer devices in relation to the source device.
        protocol: The protocol over which the source device is communicating.
    """
    if not ip_or_id.isdigit():  # type: ignore
        ip_address(ip_or_id)  # type: ignore

    if time_interval and time_interval not in VALID_TIME_INTERVALS:
        raise InvalidValueError("time_interval", time_interval, VALID_TIME_INTERVALS)

    if from_time:
        arg_to_number(from_time, "from_time")

    if until_time:
        arg_to_number(until_time, "until_time")

    if peer_role and peer_role not in VALID_PEER_ROLES:
        raise InvalidValueError("peer_role", peer_role, VALID_PEER_ROLES)

    if protocol and protocol not in VALID_PROTOCOLS:
        raise InvalidValueError("protocol", protocol, VALID_PROTOCOLS)


def validate_ticket_track_arguments(incident_status: str):
    """Validate arguments for ticket-track command.

    Args:
        incident_status: The status of the incident.
    """
    if incident_status and incident_status not in VALID_INCIDENT_STATUS:
        raise InvalidValueError("incident_status", incident_status, VALID_INCIDENT_STATUS)


def validate_packets_search_arguments(output: str) -> None:
    """Validate arguments for peers-get command.

    Args:
        output: The output format.
    """
    if output and output not in VALID_FILE_FORMATS:
        raise InvalidValueError("output", output, VALID_FILE_FORMATS)


def validate_add_and_remove_arguments(add: str, remove: str) -> None:
    """Validate add and remove arguments for command.

    Args:
        add: The list of IP or device ID to add tag.
        remove: The list of IP or device ID to remove tag.
    """
    if not add and not remove:
        raise DemistoException("No device id provided to add or remove arguments.")


def validate_create_or_update_alert_rule_arguments(
    refire_interval: Optional[str], severity: Optional[str], alert_type: Optional[str]
) -> None:
    """Validate arguments for extrahop-alert-rule-create and extrahop-alert-rule-update commands.

    Args:
        refire_interval: The time interval in which alert conditions are monitored, expressed in seconds.
        severity: The severity level of the alert, which is displayed in the Alert History,
                    email notifications, and SNMP traps.
        alert_type: The type of alert.
    """
    if refire_interval and refire_interval not in VALID_ALERT_RULE_REFIRE_INTERVALS:
        raise InvalidValueError("refire_interval", refire_interval, VALID_ALERT_RULE_REFIRE_INTERVALS)
    if severity and severity not in VALID_ALERT_RULE_SEVERITY:
        raise InvalidValueError("severity", severity, VALID_ALERT_RULE_SEVERITY)
    if alert_type and alert_type not in VALID_ALERT_RULE_TYPE:
        raise InvalidValueError("type", alert_type, VALID_ALERT_RULE_TYPE)


def validate_threshold_alert_rule_arguments(
    interval_length: Optional[str], operator: Optional[str], units: Optional[str]
) -> None:
    """Validate arguments for extrahop-alert-rule-create and extrahop-alert-rule-update commands for alert_type:
    threshold.

    Args:
        interval_length: The length of the alert interval, expressed in seconds.
        operator: The logical operator applied when comparing the value of the operand field to alert conditions
        units: The interval in which to evaluate the alert condition.
    """
    if interval_length and interval_length not in VALID_ALERT_RULE_INTERVAL_LENGTH:
        raise InvalidValueError("interval_length", interval_length, VALID_ALERT_RULE_INTERVAL_LENGTH)
    if operator and operator not in VALID_ALERT_RULE_OPERATOR:
        raise InvalidValueError("operator", operator, VALID_ALERT_RULE_OPERATOR)
    if units and units not in VALID_ALERT_RULE_UNITS:
        raise InvalidValueError("units", units, VALID_ALERT_RULE_UNITS)


def validate_metrics_list_arguments(body: Dict) -> None:
    """Validate arguments for metrics-list command.

    Args:
        body: Payload of the API request.
    """
    keys = body.keys()

    for key in keys:
        if key not in VALID_METRICS_KEYS:
            raise InvalidValueError("keys", key, VALID_METRICS_KEYS)

    if body.get("cycle") not in VALID_CYCLES:
        raise InvalidValueError("cycle", body["cycle"], VALID_CYCLES)

    if body.get("object_type") not in VALID_OBJECT_TYPES:
        raise InvalidValueError("object_type", body["object_type"], VALID_OBJECT_TYPES)


def validate_detections_list_arguments(body: Dict) -> None:
    """Validate arguments for list-detections command.

    Args:
        body: The payload of the API request.

    Raises:
        DemistoException if invalid input given for an argument.
    """
    body = trim_spaces_from_args(body)
    for key in body:
        if key not in VALID_DETECTION_KEYS:
            raise InvalidValueError("key", key, VALID_DETECTION_KEYS)

    if body.get("filter"):
        for key in body["filter"]:
            if key not in VALID_FILTER_KEYS:
                raise InvalidValueError("key", key, VALID_FILTER_KEYS)

    if body.get("from") and body.get("until") and body["from"] > body["until"]:
        raise DemistoException('Input for "from" should always be less than that of "until".')

    if isinstance(body.get("limit"), int):
        if body["limit"] <= 0:
            raise DemistoException("Invalid input for field limit. It should have numeric value greater than zero.")

        body["limit"] = min(body["limit"], 200)
    else:
        body["limit"] = 200

    if isinstance(body.get("offset"), int):
        if body["offset"] < 0:
            raise DemistoException("Invalid input for field offset. It should have numeric value greater than or equal to zero.")
    else:
        body["offset"] = 0


def add_default_category_for_filter_of_detection_list(_filter: Dict) -> None:
    """Set a default category for filter argument.

    Args:
        _filter: Filter argument for detection list command.
    """
    if "category" not in _filter:
        if "categories" not in _filter:
            _filter["categories"] = ["sec.attack"]
        elif isinstance(_filter.get("categories"), list):
            valid_categories = []
            for category in _filter.get("categories", []):
                if isinstance(category, str):
                    category = category.strip()
                    if category:
                        valid_categories.append(category)
            _filter["categories"] = valid_categories if valid_categories else ["sec.attack"]


def format_protocol_stack(protocol_list: List) -> str:
    """Formats the protocol stack.

    Args:
        protocol_list: List of protocols.

    Returns:
        String of formatted protocols.
    """
    if len(protocol_list) > 1:
        protos = protocol_list[1:]
    else:
        protos = protocol_list

    return ":".join(protos)


def sort_protocols(protos_by_weight: Dict) -> List:
    """Sort protocols by weight.

    Args:
        protos_by_weight: Weighted dictionary of protocols.

    Returns:
        Sorted List fo protocols.
    """
    sorted_protos = sorted(protos_by_weight.items(), key=lambda x: x[1], reverse=True)
    return [proto_tuple[0] for proto_tuple in sorted_protos]


def get_device_by_ip(client: ExtraHopClient, ip, active_from: str = None, active_until: str = None, limit: int = None):
    """Retrieve the device by IP address.

    Args:
        client: ExtraHop client to be used.
        ip: IP address of the device.
        active_from: Time the device was active from.
        active_until: Time the device was last active.
        limit: Number of devices to retrieve.

    Returns:
        Devices.
    """
    devices = client.device_search(
        name=None,
        ip=ip,
        mac=None,
        role=None,
        software=None,
        vendor=None,
        tag=None,
        discover_time=None,
        vlan=None,
        activity=None,
        operator="=",
        match_type="and",
        active_from=active_from,
        active_until=active_until,
        limit=limit,
        l3_only=True,
    )
    if devices:
        return devices[0]
    else:
        raise DemistoException(f"Error the IP Address {ip} was not found in ExtraHop.")  # noqa: E713


def get_devices_by_ip_or_id(
    client: ExtraHopClient,
    devices_str,
    active_from: str = None,
    active_until: str = None,
    limit: int = None,
    id_only: bool = False,
) -> List:
    """Retrieve the devices by IP address or ID.

    Args:
        client: Extrahop client to be used.
        devices_str: String os devices.
        active_from: Time the device was active from.
        active_until: Time the device was last active.
        limit: Number of devices to retrieve.
        id_only: Whether to retrieve devices by id only.

    Returns:
        List of devices.
    """
    devices = []
    for item in str(devices_str).split(","):
        if item.isdigit():
            if id_only:
                devices.append(int(item))
            else:
                device = client.get_device_by_id(item)
                devices.append(device)
        else:
            try:
                ip_address(item)
            except ValueError:
                raise DemistoException(f"Error parsing IP Address {item}")
            device = get_device_by_ip(client, item, active_from, active_until, limit)

            if id_only:
                devices.append(int(device["id"]))
            else:
                devices.append(device)

    return devices


def get_protocols(client: ExtraHopClient, ip_or_id, query_from, query_until) -> Dict:
    """Retrieve all the protocols for a device from the Reveal(X) in the given time range.

    Args:
        client: ExtraHop client to be used.
        ip_or_id: IP or ID of the object to get protocols for.
        query_from: Time since epoch to fetch the protocols.
        query_until: Time until epoch to fetch the protocols.

    Returns:
        Dictionory of client and server protocols.
    """
    device = get_devices_by_ip_or_id(client, ip_or_id)[0]
    api_id = int(device["id"])

    if device.get("analysis") == "discovery":
        demisto.results(
            {
                "Type": entryTypes["note"],
                "ContentsFormat": formats["markdown"],
                "Contents": (
                    f"This Device is in Discovery Mode. Configure your [Analysis Priorities]"
                    f"(https://docs.extrahop.com/current/analysis_priorities/) or add this device to the "  # noqa: E231
                    f"[Watchlist](https://docs.extrahop.com/current/analysis-priorities-faq/"  # noqa: E231
                    f"#what-is-the-watchlist) manually with: `!extrahop-edit-watchlist add={api_id}`"
                ),
            }
        )

    body = {
        "edge_annotations": ["protocols"],
        "from": query_from,
        "walks": [{"origins": [{"object_id": api_id, "object_type": "device"}], "steps": [{}]}],
    }
    if query_until:
        body["until"] = query_until

    activitymap = client.get_peers(body)

    client_protocols: defaultdict[str, int] = defaultdict(int)
    server_protocols: defaultdict[str, int] = defaultdict(int)
    for edge in activitymap["edges"]:
        if "annotations" in edge and "protocols" in edge.get("annotations"):
            for protocol_list in edge.get("annotations", {}).get("protocols"):
                proto_stack = format_protocol_stack(protocol_list["protocol"])
                if edge.get("from") == api_id:
                    client_protocols[proto_stack] += protocol_list["weight"]
                elif edge.get("to") == api_id:
                    server_protocols[proto_stack] += protocol_list["weight"]

    device["client_protocols"] = sort_protocols(client_protocols)
    device["server_protocols"] = sort_protocols(server_protocols)

    return device


def peers_get(
    client: ExtraHopClient,
    ip_or_id: Optional[Any],
    query_from: Optional[Any],
    query_until: Optional[Any],
    peer_role: Optional[Any],
    protocol: Optional[Any],
) -> List:
    """Retrieve peers of a device from Reveal(X).

    Args:
        client: ExtraHop client to be used.
        ip_or_id: IP address or ID of the device.
        query_from: Beginning timestamp of the range.
        query_until: Ending timestamp of the range.
        peer_role: The role of the peer device in relation to the origin device.
        protocol: Communication protocol.

    Returns:
        List of peers of the device.
    """
    device = get_devices_by_ip_or_id(client, ip_or_id)[0]
    api_id = int(device["id"])

    if device["analysis"] == "discovery":
        demisto.results(
            {
                "Type": entryTypes["note"],
                "ContentsFormat": formats["markdown"],
                "Contents": (
                    f"This Device is in Discovery Mode. Configure your [Analysis Priorities]"
                    f"(https://docs.extrahop.com/current/analysis_priorities/) or add this device to the "  # noqa: E231
                    f"[Watchlist](https://docs.extrahop.com/current/analysis-priorities-faq/"  # noqa: E231
                    f"#what-is-the-watchlist) manually with: `!extrahop-edit-watchlist add={api_id}`"
                ),
            }
        )

    body = {
        "edge_annotations": ["protocols"],
        "from": query_from,
        "walks": [
            {
                "origins": [{"object_id": api_id, "object_type": "device"}],
                "steps": [{"relationships": [{"protocol": protocol, "role": peer_role}]}],
            }
        ],
    }
    if query_until:
        body["until"] = query_until

    activitymap = client.get_peers(body)

    peers: defaultdict[str, dict] = defaultdict(
        lambda: {"weight": 0, "client_protocols": defaultdict(int), "server_protocols": defaultdict(int)}
    )

    for edge in activitymap["edges"]:
        if edge["to"] == api_id:
            peer_id = edge["from"]
            role_key = "client_protocols"
        else:
            peer_id = edge["to"]
            role_key = "server_protocols"

        peers[peer_id]["weight"] += edge["weight"]

        # add protocols
        if "annotations" in edge and "protocols" in edge["annotations"]:
            for protocol_list in edge["annotations"]["protocols"]:
                proto_stack = format_protocol_stack(protocol_list["protocol"])
                peers[peer_id][role_key][proto_stack] += protocol_list["weight"]

    peer_devices = []
    peer_ids_by_weight = [peer[0] for peer in sorted(peers.items(), key=lambda x: x[1]["weight"], reverse=True)]
    # Lookup each peer device by id
    for peer_id in peer_ids_by_weight:
        device = client.get_device_by_id(peer_id)
        if peer_role in ("any", "client"):
            device["client_protocols"] = sort_protocols(peers[peer_id]["client_protocols"])
        if peer_role in ("any", "server"):
            device["server_protocols"] = sort_protocols(peers[peer_id]["server_protocols"])
        peer_devices.append(device)
    return peer_devices


def get_activity_map(
    client: ExtraHopClient,
    ip_or_id: Optional[str],
    time_interval: Optional[Any],
    from_time: Optional[Any],
    until_time: Optional[Any],
    peer_role: str,
    protocol: str,
    server_url: str,
):
    """Retrieve the activity map link for a device from Reveal(X).

    Args:
        client: ExtraHop client to be used.
        ip_or_id: The IP Address or ExtraHop API ID of the source device to get an activity map.
        time_interval: The time interval of the live activity map.
        from_time: The beginning timestamp of a fixed time range.
        until_time: The ending timestamp of a fixed time range.
        peer_role: The role of the peer devices in relation to the source device.
        protocol: The protocol over which the source device is communicating.
        server_url: Server URL.

    Returns:
        Activity map link.
    """
    device = get_devices_by_ip_or_id(client, ip_or_id)[0]

    time_intervals = {"30 minutes": (30, "MIN"), "6 hours": (6, "HR"), "1 day": (1, "DAY"), "1 week": (1, "WK")}

    if from_time or until_time:
        if from_time and until_time:
            interval = "DT"
            start = from_time
            end = until_time
        else:
            raise ValueError("When using a fixed time range both from_time and until_time timestamps need to be provided.")
    else:
        start, interval = time_intervals.get(time_interval, (30, "MIN"))  # type: ignore
        end = 0

    activity_map_params = {
        "server": server_url,
        "app_id": client.get_appliance_uuids()[device.get("node_id")],
        "disc_id": device.get("discovery_id"),
        "start": start,
        "interval": interval,
        "obj": "device",
        "proto": protocol,
        "role": peer_role,
        "end": end,
    }

    activity_map_link_format = (
        "{server}/extrahop/#/activitymaps"
        "?appliance_id={app_id}"
        "&discovery_id={disc_id}"
        "&from={start}"
        "&interval_type={interval}"
        "&object_type={obj}"
        "&protocol={proto}"
        "&role={role}"
        "&until={end}"
    )

    return activity_map_link_format.format(**activity_map_params)


def prepare_devices_get_output(response: List, appliance_uuids: List, server_url: str) -> str:
    """Prepare human-readable output for watchlist-get command.

    Args:
        appliance_uuids: UUID of the appliance.
        response: Response from the API.
        server_url: Server URL.

    Returns:
        markdown string to be displayed in the war room.
    """
    hr_outputs = []
    headers = ["Display Name", "IP Address", "MAC Address", "Role", "Vendor", "URL"]
    for device in response:
        hr = {
            "Display Name": device.get("display_name"),
            "IP Address": device.get("ipaddr4", device.get("ipaddr6")),
            "MAC Address": device.get("macaddr"),
            "Role": device.get("role"),
            "Vendor": device.get("vendor"),
        }

        device_url = (
            f'{server_url}/extrahop/#/metrics/devices/{appliance_uuids[device.get("node_id")]}.'
            f'{device.get("discovery_id")}/overview/'
        )
        hr["URL"] = f"[View Device in ExtraHop]({device_url})"
        device["url"] = device_url

        if "client_protocols" in device or "server_protocols" in device:
            hr["Protocols"] = {}

            # re-arrange headers to add protocol information
            headers = ["Display Name", "IP Address", "MAC Address", "Role", "Protocols", "URL", "Vendor"]
            if "client_protocols" in device:
                hr["Protocols"]["Client"] = ", ".join(device.get("client_protocols", []))
            if "server_protocols" in device:
                hr["Protocols"]["Server"] = ", ".join(device.get("server_protocols", []))
        hr_outputs.append(hr)
    if not len(response):
        return "No Devices found"

    return tableToMarkdown("Device Details:\n", hr_outputs, headers=headers, removeNull=True)


def prepare_protocol_get_output(device, appliance_uuids, server_url) -> str:
    """Prepare human-readable output for protocol-get command.

    Args:
        device: The device data received from API.
        appliance_uuids: UUID of the appliance.
        server_url: Server URL.

    Returns:
        Markdown string to be displayed in the war room.
    """
    if not device.get("client_protocols") and not device.get("server_protocols"):
        return "No Protocol activity found"
    device_url = (
        f'{server_url}/extrahop/#/metrics/devices/{appliance_uuids[device.get("node_id")]}.'
        f'{device.get("discovery_id")}/overview/'
    )
    hr_outputs = {
        "Display Name": device.get("display_name"),
        "IP Address": device.get("ipaddr4", device.get("ipaddr6")),
        "MAC Address": device.get("macaddr"),
        "Protocols (Client)": ", ".join(device.get("client_protocols", [])),
        "Protocols (Server)": ", ".join(device.get("server_protocols", [])),
        "Role": device.get("role"),
        "Vendor": device.get("vendor"),
        "URL": f"[View Device in ExtraHop]({device_url})",
    }
    device["url"] = device_url

    headers = ["Display Name", "IP Address", "MAC Address", "Protocols (Client)", "Protocols (Server)", "Role", "Vendor", "URL"]

    return tableToMarkdown("Device Activity Found:\n", hr_outputs, headers=headers, removeNull=True)


def prepare_activity_map_link_output(activity_map_link: str) -> str:
    """Prepare human-readable output for activity-map-get command.

    Args:
        activity_map_link: Live link of an activity map.

    Returns:
        markdown string to be displayed in the war room.
    """
    return f"[View Live Activity Map in ExtraHop]({activity_map_link})"


def prepare_alert_rules_get_output(alerts: Dict) -> str:
    """Prepare human-readable output for get-alert-rules command.

    Args:
        alerts: List of alert response from the API.

    Returns:
        markdown string to be displayed in the war room.
    """
    if len(alerts) == 0:
        return "No Alerts were found."

    return tableToMarkdown(f"Found {len(alerts)} Alert(s)", alerts, headerTransform=string_to_table_header, removeNull=True)


def prepare_metrics_list_output(response: Dict) -> str:
    """Prepare human-readable output for metrics-list command.

    Args:
        response: List of metrics response from the API.

    Returns:
        markdown string to be displayed in the war room.
    """
    hr_output = {
        "Cycle": response.get("cycle"),
        "Node ID": response.get("node_id"),
        "Clock": response.get("clock"),
        "From Time": response.get("from"),
        "Until Time": response.get("until"),
        "Stats": response.get("stats"),
    }

    headers = ["Cycle", "Node ID", "Clock", "From Time", "Until Time", "Stats"]

    return tableToMarkdown("Metrics Found:\n", hr_output, headers=headers, removeNull=True)


def parse_location_header(location: str) -> str:
    """Retrieve the tag_id from the location field in header.

    Args:
        location: The location field in the header of the response received.

    Returns:
        The tag ID of the new created tag.
    """
    if location:
        last_slash_index = location.rindex("/") + 1
        tag_id = location[last_slash_index:]
        if tag_id.isdigit():
            return tag_id
    # return error in any other case
    raise DemistoException("Error unable to parse ExtraHop API response location header.")


def devices_tag(client: ExtraHopClient, tag: str, add: str, remove: str) -> None:
    """Tag or untag devices with the given tag name.

    Args:
        client: ExtraHop client to be used.
        tag: The tag which is added or removed from the devices.
        add: The list of IP or device ID to add tag.
        remove: The list of IP or device ID to remove tag.
    """
    body = {}
    if add:
        body["assign"] = get_devices_by_ip_or_id(client, add, id_only=True)
    if remove:
        body["unassign"] = get_devices_by_ip_or_id(client, remove, id_only=True)

    all_tags = client.get_all_tags()
    for current_tag in all_tags:
        if current_tag["name"] == tag:
            tag_id = current_tag["id"]
            break
    else:
        if remove and not add:
            raise DemistoException(f"The tag {tag} does not exist, nothing to remove.")

        tag_create_res = client.create_new_tag(data={"name": tag})
        tag_location = tag_create_res.headers.get("location")
        tag_id = parse_location_header(tag_location)

    client.tag_untag_devices(tag_id, data=body)


def prepare_list_detections_output(detections) -> str:
    """Prepare human-readable output for list-detections command.

    Args:
        detections: List of detection response from the API.

    Returns:
        markdown string to be displayed in the war room.
    """
    hr_outputs = []
    headers = ["Detection ID", "Risk Score", "Description", "Categories", "Status", "Resolution", "Start Time"]
    for detection in detections:
        hr_output = {
            "Detection ID": detection.get("id"),
            "Risk Score": detection.get("risk_score"),
            "Description": detection.get("description"),
            "Categories": detection.get("categories"),
            "Status": detection.get("status"),
            "Resolution": detection.get("resolution"),
            "Start Time": detection.get("start_time"),
        }
        hr_outputs.append(hr_output)

    return tableToMarkdown(f"Found {len(hr_outputs)} Detection(s)", hr_outputs, headers=headers, removeNull=True)


def is_version_greater_or_equal(version1, version2):
    """
    Returns True if version1 >= version2, else False.

    Args:
        version1: The first version to compare.
        version2: The second version to compare.

    Returns:
        True if version1 >= version2, else False
    """
    v1_parts = list(map(int, version1.split(".")))
    v2_parts = list(map(int, version2.split(".")))

    length_diff = len(v1_parts) - len(v2_parts)

    if length_diff > 0:
        v2_parts.extend([0] * length_diff)
    elif length_diff < 0:
        v1_parts.extend([0] * abs(length_diff))

    for a, b in zip(v1_parts, v2_parts):
        if a > b:
            return True
        elif a < b:
            return False
    return True


""" COMMAND FUNCTIONS """


def validate_fetch_incidents_params(params: dict, last_run: dict) -> Dict:
    """
    Validate the parameter list for fetch incidents.

    Args:
        params: Dictionary containing demisto configuration parameters
        last_run: last run returned by function demisto.getLastRun

    Returns:
        Dictionary containing validated configuration parameters in proper format.
    """

    first_fetch = arg_to_datetime(params.get("first_fetch", FIRST_FETCH))
    detection_start_time = int(first_fetch.timestamp() * 1000)  # type: ignore

    if last_run and "detection_start_time" in last_run:
        detection_start_time = last_run.get("detection_start_time")  # type: ignore

    offset = 0
    if last_run and "offset" in last_run:
        offset = last_run.get("offset")  # type: ignore

    return {"detection_start_time": detection_start_time, "offset": offset}


def append_participant_device_data(client: ExtraHopClient, detections: CommandResults) -> CommandResults:
    """Append the device data of the participants present in the detection.

    Args:
        client: ExtraHop client to be used.
        detections: The command result object of detection data fetched from ExtraHop.

    Returns:
        CommandResult object with device data of the participants.
    """
    for detection in detections.outputs:  # type: ignore
        detection["device_data"] = []
        for participant in detection.get("participants", []):
            if participant.get("object_type") == "device":
                if not participant.get("object_id"):
                    continue
                object_id = participant.get("object_id")
                device_data = client.get_device_by_id(object_id, (404, 200, 204, 201))

            else:
                if not participant.get("object_value"):
                    continue
                ip = participant.get("object_value")
                device_data = client.device_search(
                    name=None,
                    ip=ip,
                    mac=None,
                    role=None,
                    software=None,
                    vendor=None,
                    tag=None,
                    discover_time=None,
                    vlan=None,
                    activity=None,
                    operator="=",
                    match_type="and",
                    active_from=None,
                    active_until=None,
                    limit=None,
                    l3_only=True,
                )
            if device_data:
                detection["device_data"].append(device_data)
    return detections


def fetch_extrahop_detections(client: ExtraHopClient, advanced_filter: Dict, last_run: Dict, on_cloud: bool) -> tuple[List, Dict]:
    """Fetch detections from ExtraHop according to the given filter.

    Args:
        client:ExtraHop client to be used.
        advanced_filter: The advanced_filter given by the user to filter out the required detections.
        last_run: Last run returned by function demisto.getLastRun
        on_cloud: Indicator for the instance hosted on cloud.

    Returns:
        List of incidents to be pushed into XSOAR.
    """
    try:
        already_fetched: List[str] = last_run.get("already_fetched", [])
        incidents: List[Dict] = []
        detection_start_time = advanced_filter["mod_time"]

        detections = detections_list_command(client, {}, on_cloud=on_cloud, advanced_filter=advanced_filter)

        if detections.outputs:
            detections = append_participant_device_data(client, detections)

            for detection in detections.outputs:  # type: ignore
                detection_id = detection.get("id")
                if detection_id not in already_fetched:
                    detection.update(get_mirroring())
                    incident = {
                        "name": str(detection.get("type", "")),
                        "occurred": datetime.utcfromtimestamp(detection["start_time"] / 1000).strftime(DATE_FORMAT),
                        "severity": next(
                            (
                                severity
                                for range_str, severity in TICKET_SEVERITY.items()
                                if detection.get("risk_score") in range(*map(int, range_str.split("-")))
                            ),
                            None,
                        ),
                        "rawJSON": json.dumps(detection),
                    }
                    incidents.append(incident)
                    already_fetched.append(detection_id)

                else:
                    demisto.info(f"Extrahop already fetched detection with id: {detection_id}")

        if len(incidents) < advanced_filter["limit"]:
            offset = 0
            detection_start_time = detections.outputs[-1]["mod_time"] + 1 if incidents else detection_start_time  # type: ignore
        else:
            offset = advanced_filter["offset"] + len(incidents)

    except Exception as error:
        raise DemistoException(f"extrahop: exception occurred {error!s}")

    demisto.info(f"Extrahop fetched {len(incidents)} incidents where the advanced filter is {advanced_filter}")

    last_run["detection_start_time"] = int(detection_start_time)
    last_run["offset"] = offset
    last_run["already_fetched"] = already_fetched
    return incidents, last_run


def fetch_incidents(client: ExtraHopClient, params: Dict, last_run: Dict, on_cloud: bool):
    """Fetch the specified ExtraHop entity and push into XSOAR.

    Args:
       client: ExtraHop client to be used.
       params: Integration configuration parameters.
       last_run: The last_run dictionary having the state of previous cycle.
       on_cloud: Indicator for the instance hosted on cloud.
    """
    demisto.info(
        f"Extrahop fetch_incidents invoked with advanced_filter: {params.get('advanced_filter', '')}, "
        f"first_fetch: {params.get('first_fetch', '')} and last_run: {last_run}"
    )
    fetch_params = validate_fetch_incidents_params(params, last_run)

    now = datetime.now()
    next_day = now + timedelta(days=1)
    if last_run.get("version_recheck_time", 1581852287000) < int(now.timestamp() * 1000):
        version = get_extrahop_server_version(client)
        last_run["version_recheck_time"] = int(next_day.timestamp() * 1000)
        if not is_version_greater_or_equal(version, MINIMUM_FIRMWARE_VERSION):
            raise DemistoException(
                "This integration works with ExtraHop firmware version greater than or equal to "
                f"{MINIMUM_FIRMWARE_VERSION} and current firmware version being used by you is {version}"
            )

    advanced_filter = params.get("advanced_filter")
    if advanced_filter and advanced_filter.strip():
        try:
            _filter = json.loads(advanced_filter)
            add_default_category_for_filter_of_detection_list(_filter)
        except json.JSONDecodeError as error:
            raise ValueError("Invalid JSON string provided for advanced filter.") from error
    else:
        _filter = {"categories": ["sec.attack"]}

    advanced_filter = {
        "filter": _filter,
        "mod_time": fetch_params["detection_start_time"],
        "until": 0,
        "limit": MAX_FETCH,
        "offset": fetch_params["offset"],
        "sort": [{"direction": "asc", "field": "mod_time"}],
    }

    incidents, next_run = fetch_extrahop_detections(client, advanced_filter, last_run, on_cloud)
    demisto.info(f"Extrahop next_run is {next_run}")
    return incidents, next_run


def watchlist_get_command(client: ExtraHopClient, on_cloud: bool) -> CommandResults:
    """Retrieve all the devices on the watchlist in Reveal(X).

    Args:
        client: ExtraHop client to be used.
        on_cloud: Check if ExtraHop instance is on cloud.

    Returns:
        CommandResult object
    """
    response = client.get_watchlist()
    appliance_uuids = client.get_appliance_uuids()

    server_url = client._base_url

    if on_cloud:
        server_url = remove_api_from_base_url(server_url)

    readable_output = prepare_devices_get_output(response, appliance_uuids, server_url)

    return CommandResults(
        outputs_prefix="ExtraHop.Device",
        outputs_key_field="id",
        outputs=remove_empty_elements_from_response(response),
        readable_output=readable_output,
        raw_response=response,
    )


def peers_get_command(client: ExtraHopClient, args: Dict[str, Any], on_cloud: bool) -> CommandResults:
    """Retrieve all the peers for a device from the Reveal(X).

    Args:
        client: ExtraHop client to be used.
        args: arguments obtained from demisto.args().
        on_cloud: Check if ExtraHop instance is on cloud.

    Returns:
        CommandResult object
    """
    ip_or_id = args.get("ip_or_id")
    query_from = args.get("query_from", "-30m")
    query_until = args.get("query_until")
    peer_role = args.get("peer_role")
    protocol = args.get("protocol")

    validate_peers_get_arguments(peer_role, protocol)

    peer_devices = peers_get(client, ip_or_id, query_from, query_until, peer_role, protocol)
    appliance_uuids = client.get_appliance_uuids()

    server_url = client._base_url

    if on_cloud:
        server_url = remove_api_from_base_url(server_url)

    readable_output = prepare_devices_get_output(peer_devices, appliance_uuids, server_url)

    return CommandResults(
        outputs_prefix="ExtraHop.Device",
        outputs_key_field="id",
        outputs=remove_empty_elements_from_response(peer_devices),
        readable_output=readable_output,
        raw_response=peer_devices,
    )


def devices_search_command(client: ExtraHopClient, args: Dict[str, Any], on_cloud: bool) -> CommandResults:
    """Retrieve the devices from Reveal(X).

    Args:
        client: ExtraHop client to be used:
        args: Arguments obtained from demisto.args().
        on_cloud: Check if ExtraHop instance is on cloud.

    Returns:
        CommandResult object
    """
    name = args.get("name")
    ip = args.get("ip")
    mac = args.get("mac")
    role = args.get("role")
    software = args.get("software")
    vendor = args.get("vendor")
    tag = args.get("tag")
    discover_time = args.get("discover_time")
    vlan = args.get("vlan")
    activity = args.get("activity")
    operator = args.get("operator")
    match_type = args.get("match_type")
    active_from = args.get("active_from")
    active_until = args.get("active_until")
    limit = args.get("limit", 10)
    l3_only = args.get("l3_only", True)

    validate_device_search_arguments(role, software, vendor, activity, operator, match_type, l3_only)

    devices = client.device_search(
        name,
        ip,
        mac,
        role,
        software,
        vendor,
        tag,
        discover_time,
        vlan,
        activity,
        operator,
        match_type,
        active_from,
        active_until,
        limit,
        l3_only,
    )
    appliance_uuids = client.get_appliance_uuids()

    server_url = client._base_url

    if on_cloud:
        server_url = remove_api_from_base_url(server_url)

    readable_output = prepare_devices_get_output(devices, appliance_uuids, server_url)
    return CommandResults(
        outputs_prefix="ExtraHop.Device",
        outputs_key_field="id",
        outputs=remove_empty_elements_from_response(devices),
        readable_output=readable_output,
        raw_response=devices,
    )


def protocols_get_command(client: ExtraHopClient, args: Dict[str, Any], on_cloud: bool) -> CommandResults:
    """Retrieve all active network protocols for a device from Reveal(x).

    Args:
        client: ExtraHop client to be used.
        args: Arguments obtained from demisto.args()
        on_cloud: Check if ExtraHop instance is on cloud.

    Returns:
        CommandResult object
    """
    ip_or_id = args.get("ip_or_id")
    query_from = args.get("query_from")
    query_until = args.get("query_until")

    device = get_protocols(client, ip_or_id, query_from, query_until)

    server_url = client._base_url
    if on_cloud:
        server_url = remove_api_from_base_url(server_url)

    appliance_uuids = client.get_appliance_uuids()
    readable_output = prepare_protocol_get_output(device, appliance_uuids, server_url)

    return CommandResults(
        outputs_prefix="ExtraHop.Device",
        outputs_key_field="id",
        outputs=remove_empty_elements_from_response(device),
        readable_output=readable_output,
        raw_response=device,
    )


def activity_map_get_command(client: ExtraHopClient, args: Dict[str, Any], on_cloud: bool) -> CommandResults:
    """Retrieve the activity of a device from Reveal(x).

    Args:
        client: ExtraHop client to be used.
        args: Arguments obtained from demisto.args()
        on_cloud: Check if ExtraHop instance is on cloud.

    Returns:
        CommandResult object
    """
    ip_or_id = args.get("ip_or_id")
    time_interval = args.get("time_interval", "30 minutes")
    from_time = args.get("from_time")
    until_time = args.get("until_time")
    peer_role = args.get("peer_role", "any")
    protocol = args.get("protocol", "any")

    validate_activity_map_get_arguments(ip_or_id, time_interval, from_time, until_time, peer_role, protocol)

    server_url = client._base_url

    if on_cloud:
        server_url = remove_api_from_base_url(server_url)

    activity_map_link = get_activity_map(client, ip_or_id, time_interval, from_time, until_time, peer_role, protocol, server_url)

    context = {"url": activity_map_link}

    readable_output = prepare_activity_map_link_output(activity_map_link)

    return CommandResults(
        outputs_prefix="ExtraHop.ActivityMap",
        outputs_key_field="url",
        outputs=remove_empty_elements_from_response(context),
        readable_output=readable_output,
        raw_response=context,
    )


def alerts_rules_get_command(client: ExtraHopClient) -> CommandResults:
    """Retrieve all the available alerts from the ExtraHop Reveal(X).

    Args:
        client: ExtraHop client to be used.

    Returns:
        CommandResult object
    """
    result = client.get_alert_rules()
    readable_output = prepare_alert_rules_get_output(result)
    return CommandResults(
        outputs_prefix="ExtraHop.Alert",
        outputs_key_field="id",
        outputs=remove_empty_elements_from_response(result),
        readable_output=readable_output,
        raw_response=result,
    )


def packets_search_command(client: ExtraHopClient, args: Dict[str, Any]) -> Union[str, Dict]:
    """Retrieve the specific packets from Reveal(X).

    Args:
        client: ExtraHop client to be used.
        args: Arguments obtained from demisto.args().

    Returns:
        File containing the packets.
    """
    output = args.get("output", "pcap")
    limit_bytes = args.get("limit_bytes", "10MB")
    limit_search_duration = args.get("limit_search_duration", "5m")
    query_from = args.get("query_from", "-10m")
    query_until = args.get("query_until")
    bpf = args.get("bpf")
    ip1 = args.get("ip1")
    port1 = args.get("port1")
    ip2 = args.get("ip2")
    port2 = args.get("port2")

    validate_packets_search_arguments(output)

    response = client.packets_search(
        output, limit_bytes, limit_search_duration, query_from, query_until, bpf, ip1, port1, ip2, port2
    )

    if response.status_code == 204:
        return "Search matched no packets."
    filename_header = response.headers.get("content-disposition")
    f_attr = "filename="
    if filename_header and f_attr in filename_header:
        quoted_filename = filename_header[filename_header.index(f_attr) + len(f_attr) :]
        filename = quoted_filename.replace('"', "")
    else:
        raise DemistoException("Error filename could not be found in response header.")

    return fileResult(filename, response.content)


def ticket_track_command(client: ExtraHopClient, args: Dict[str, Any]) -> CommandResults:
    """Link a Reveal(x) detection to a Demisto Investigation.

    Args:
        client: ExtraHop client to be used.
        args: Arguments received for the command.
    Returns:
        CommandResult object
    """
    incident_id = args.get("incident_id")
    detection_id = args.get("detection_id", "")
    incident_owner = args.get("incident_owner")
    incident_status = args.get("incident_status", "")
    incident_close_reason = args.get("incident_close_reason")

    validate_ticket_track_arguments(incident_status)

    detection_status = {
        "ticket_id": incident_id,
        "status": TICKET_STATUS_MAP.get(incident_status),
        "assignee": incident_owner or None,
    }

    # Only set Resolution if the incident is closed
    if detection_status["status"] == "closed" and incident_close_reason:
        if incident_close_reason == "Resolved":
            detection_status["resolution"] = "action_taken"
        elif incident_close_reason in {"False Positive", "Duplicate"}:
            detection_status["resolution"] = "no_action_taken"

    client.patch_detections(detection_id, detection_status)

    readable_output = f"Successfully linked detection({detection_id}) with incident({incident_id})"
    output = {"TicketId": incident_id}  # type: dict

    return CommandResults(
        outputs_prefix="ExtraHop",
        outputs_key_field="TicketId",
        outputs=remove_empty_elements(output),
        readable_output=readable_output,
        raw_response=output,
    )


def devices_tag_command(client: ExtraHopClient, args: Dict[str, Any]) -> str:
    """Add or remove a tag from devices in Reveal(x).

    Args:
        client: ExtraHop client to be used.
        args: Arguments received for the command.

    Returns:
        CommandResult object
    """
    tag = args.get("tag", "")
    add = args.get("add", "")
    remove = args.get("remove", "")

    validate_add_and_remove_arguments(add, remove)

    devices_tag(client, tag, add, remove)

    return "Successfully tagged untagged the device/s."


def create_or_edit_alert_rule_command(client: ExtraHopClient, args: Dict[str, Any]) -> CommandResults:
    """Create or update alert rule from Reveal(x).

    Args:
        client: ExtraHop client to be used.
        args: Arguments received for the command.

    Returns:
        CommandResult object.
    """
    apply_all = argToBoolean(args.get("apply_all", False))
    disabled = argToBoolean(args.get("disabled", False))
    name = args.get("name")
    notify_snmp = argToBoolean(args.get("notify_snmp", False))
    refire_interval = args.get("refire_interval", 0)
    severity = args.get("severity", 0)
    alert_type = args.get("type")
    alert_id = args.get("alert_id")

    validate_create_or_update_alert_rule_arguments(refire_interval, severity, alert_type)

    # Prepare request body for alert-rule.
    data = {
        "apply_all": apply_all,
        "disabled": disabled,
        "name": name,
        "notify_snmp": notify_snmp,
        "refire_interval": int(refire_interval),
        "severity": int(severity),
        "type": alert_type,
    }

    # For alertType = threshold.
    if alert_type == "threshold":
        field_name = args.get("field_name")
        field_name2 = args.get("field_name2")
        field_op = args.get("field_op")
        interval_length = args.get("interval_length", 0)
        operand = args.get("operand")
        operator = args.get("operator")
        param = args.get("param")
        param2 = args.get("param2")
        stat_name = args.get("stat_name")
        units = args.get("units")
        validate_threshold_alert_rule_arguments(interval_length, operator, units)
        data["interval_length"] = int(interval_length)
        data["operator"] = operator
        data["units"] = units

        if field_name:
            data["field_name"] = field_name
        if field_name2:
            data["field_name2"] = field_name2
        if field_op:
            data["field_op"] = field_op
        if operand:
            data["operand"] = operand
        if param:
            data["param"] = param
        if param2:
            data["param2"] = param2
        if stat_name:
            data["stat_name"] = stat_name

    elif alert_type == "detection":
        if object_type := args.get("object_type"):
            if object_type not in VALID_ALERT_RULE_OBJECT_TYPES:
                raise InvalidValueError("object_type", object_type, VALID_ALERT_RULE_OBJECT_TYPES)
            data["object_type"] = object_type
        if protocols := argToList(args.get("protocols")):
            data["protocols"] = protocols

    if alert_id:
        client.update_alert_rule(alert_id, data)
        return CommandResults(readable_output="Successfully updated alert rule.")
    else:
        client.create_alert_rule(data)
        return CommandResults(readable_output="Successfully created alert rule.")


def watchlist_edit_command(client: ExtraHopClient, args: Dict[str, Any]) -> CommandResults:
    """Add or remove devices from the watchlist in Reveal(x).
    Args:
        client: ExtraHop client to be used.
        args: Arguments obtained from demisto.args().
    Returns:
        CommandResults object.
    """
    add = args.get("add", "")
    remove = args.get("remove", "")
    validate_add_and_remove_arguments(add, remove)
    body = {}
    hr_outputs = ""

    if add:
        body["assign"] = get_devices_by_ip_or_id(client, add, id_only=True)
        hr_outputs += f"Successfully added new devices({add}) in the watchlist \n"
    if remove:
        hr_outputs += f"Successfully removed devices({remove}) from the watchlist"
        body["unassign"] = get_devices_by_ip_or_id(client, remove, id_only=True)

    client.edit_watchlist(body)

    return CommandResults(readable_output=hr_outputs)


def metrics_list_command(client: ExtraHopClient, args=None, advanced_filter: str = None) -> CommandResults:
    """Retrieve metric information collected about every object from the Reveal(X).

    Args:
        client: ExtraHop client to be used.
        args: Arguments obtained from demisto.args()
        advanced_filter: Advance filter to be used for fetching incidents.

    Returns:
        CommandResult object
    """

    # This snipper will work in case of the fetch incidents for metrics, when user provides advance filter.
    if args is None:
        args = {}
    if advanced_filter:
        try:
            body = json.loads(advanced_filter)  # type: ignore
        except json.JSONDecodeError:
            raise ValueError("Invalid json string provided for advanced filter.")
        validate_metrics_list_arguments(body)
    else:
        cycle = args.get("cycle")
        from_time = args.get("from_time")
        metric_category = args.get("metric_category")
        object_ids = argToList(args.get("object_ids"))
        object_ids = [arg_to_number(object_id) for object_id in object_ids]
        object_type = args.get("object_type")
        until_time = args.get("until_time")
        metric_specs = args.get("metric_specs")

        try:
            metric_specs_json = json.loads(metric_specs)  # type: ignore
        except json.JSONDecodeError:
            raise ValueError("Invalid JSON string provided for metric specs.")

        body = {
            "cycle": cycle,
            "from": from_time,
            "until": until_time,
            "metric_category": metric_category,
            "metric_specs": metric_specs_json,
            "object_ids": object_ids,
            "object_type": object_type,
        }
        validate_metrics_list_arguments(body)

    response = client.metrics_list(body)
    readable_output = prepare_metrics_list_output(response)

    return CommandResults(
        outputs_prefix="ExtraHop.Metrics",
        outputs=remove_empty_elements_from_response(response),
        readable_output=readable_output,
        raw_response=response,
    )


def detections_list_command(client: ExtraHopClient, args: Dict[str, Any], on_cloud=False, advanced_filter=None) -> CommandResults:
    """Retrieve the detections from Reveal(X).

    Args:
        client: ExtraHop client to be used.
        args: Arguments obtained from demisto.args().
        advanced_filter: The advanced filter provided by user to fetch detections.
        on_cloud: Check if ExtraHop instance is on cloud.

    Returns:
        CommandResults object.
    """
    version = get_extrahop_server_version(client)
    if not is_version_greater_or_equal(version, MINIMUM_FIRMWARE_VERSION):
        raise DemistoException(
            "This integration works with ExtraHop firmware version greater than or equal to "
            f"{MINIMUM_FIRMWARE_VERSION} and current firmware version being used by you is {version}"
        )

    body = {}
    if advanced_filter:
        body = advanced_filter

    else:
        filter_query = args.get("filter")
        from_time = arg_to_number(args.get("from"))
        limit = arg_to_number(args.get("limit"), "200")
        offset = arg_to_number(args.get("offset"))
        sort = args.get("sort")
        until_time = arg_to_number(args.get("until"))
        mod_time = arg_to_number(args.get("mod_time"))
        if filter_query and filter_query.strip():
            try:
                filter_query = json.loads(filter_query)
                add_default_category_for_filter_of_detection_list(filter_query)
                body["filter"] = filter_query
            except json.JSONDecodeError:
                raise ValueError("Invalid json string provided for filter.")
        else:
            body["filter"] = {"categories": ["sec.attack"]}

        if isinstance(from_time, int):
            body["from"] = from_time

        if isinstance(limit, int):
            body["limit"] = limit

        if isinstance(offset, int):
            body["offset"] = offset

        if sort:
            sort_list = []
            sort_on_field = sort.split(",")

            for sort in sort_on_field:
                try:
                    field, direction = sort.split(" ")
                except ValueError:
                    raise DemistoException(
                        'Incorrect input provided for argument "sort". Please follow the format mentioned in description.'
                    )

                if direction not in SORT_DIRECTION:
                    raise DemistoException(
                        'Incorrect input provided for argument "sort". Allowed values for '
                        "direction are: " + ", ".join(SORT_DIRECTION)
                    )

                prepared_sort_dict = {"direction": direction, "field": field}
                sort_list.append(prepared_sort_dict)

            body["sort"] = sort_list

        if isinstance(until_time, int):
            body["until"] = until_time

        if isinstance(mod_time, int):
            body["mod_time"] = mod_time

    validate_detections_list_arguments(body)

    detections = client.detections_list(body)

    base_url = client._base_url
    if on_cloud:
        base_url = remove_api_from_base_url(base_url)
    for detection in detections:
        if detection.get("description"):
            detection["description"] = modify_description(base_url, detection.get("description"))
    readable_output = prepare_list_detections_output(detections)

    return CommandResults(
        outputs_prefix="ExtraHop.Detections",
        outputs_key_field="id",
        outputs=remove_empty_elements(detections),
        readable_output=readable_output,
        raw_response=detections,
    )


def get_mirroring() -> Dict:
    """Add mirroring related keys in an incident.

    Returns:
        A dictionary containing required key-value pairs for mirroring.
    """
    return {
        "mirror_direction": "In",
        "mirror_instance": demisto.integrationInstance(),
    }


def get_modified_remote_data_command(client, args: Dict[str, Any], params: Dict) -> GetModifiedRemoteDataResponse:
    """Retrieve the IDs of the incidents which are updated since the last updated.

    Args:
        client: XSOAR client to use.
        args:
            lastUpdate: When was the last time we retrieved data.
        params: The integration configuration parameters.

    Returns:
        GetModifiedRemoteDataResponse: List of incidents IDs which are modified since the last update.
    """
    # Retrieve the arguments passed with the command.
    command_args = GetModifiedRemoteDataArgs(args)

    # Parse the last update date got from the command arguments.
    command_last_run_date = dateparser.parse(command_args.last_update, settings={"TIMEZONE": "UTC"})

    demisto.debug(f"Last update date of get-modified-remote-data command is {command_last_run_date}.")

    # Convert the datetime object to epoch as the API requires the time in epoch format.
    body = {
        "mod_time": date_to_timestamp(command_last_run_date),
        # End time for the API call will be current time.
        "until": 0,
        "offset": 0,
        "sort": [{"direction": "asc", "field": "mod_time"}],
        "limit": MAX_FETCH,
    }

    advanced_filter = params.get("advanced_filter")
    try:
        advanced_filter = json.loads(advanced_filter)  # type: ignore
    except json.JSONDecodeError as error:
        raise ValueError("Invalid JSON string provided for advanced filter.") from error
    body["filter"] = advanced_filter

    len_of_incidents = 0
    updated_incident_ids = []

    while True:
        body["offset"] += len_of_incidents

        list_incidents_resp = client.detections_list(body)
        len_of_incidents = len(list_incidents_resp)

        if len_of_incidents == 0:
            break

        # Extract the IDs of the incidents.
        updated_incident_ids.extend([str(inc.get("id")) for inc in list_incidents_resp])

        if len(updated_incident_ids) >= 10000:
            break

    # Filter out None values if there are any.
    updated_incident_ids: List[str] = list(filter(None, updated_incident_ids))

    # Filter out any duplicate incident IDs.
    updated_incident_ids = list(set(updated_incident_ids))

    # At max 10,000 incidents should be updated.
    updated_incident_ids = updated_incident_ids[:10000]

    demisto.info(
        f'Extrahop Number of incidents modified between {body["mod_time"]} to {body["until"]} are '
        f'{len(updated_incident_ids)}.'
    )
    demisto.info(
        f'Extrahop List of modified incident ids between {body["mod_time"]} to {body["until"]} is {updated_incident_ids}.'
    )

    return GetModifiedRemoteDataResponse(updated_incident_ids)


def get_remote_data_command(client, args: Dict[str, Any]) -> Union[str, GetRemoteDataResponse]:
    """Return the updated incident and updated entries.

    Args:
        client: XSOAR client to use.
        args:
            id: Incident ID to retrieve.

    Returns:
        First entry is the incident (which can be completely empty) and the new entries.
    """
    parsed_args = GetRemoteDataArgs(args)
    demisto.info(f"Extrahop get_remote_data_command invoked for {parsed_args.remote_incident_id}")
    detection = client.get_detections_by_id(parsed_args.remote_incident_id)
    return GetRemoteDataResponse(detection, [])


def test_module(client: ExtraHopClient) -> str:
    """Tests API connectivity and authentication.

    Returning "ok" indicates that the integration works like it is supposed to.
    Connection to the service is successful.

    Args:
        client: ExtraHop client to be used.

    Returns:
        "ok" if test passed, anything else will fail the test.
    """
    response = client.test_connection()
    version = get_extrahop_server_version(client)
    if not is_version_greater_or_equal(version, MINIMUM_FIRMWARE_VERSION):
        raise DemistoException(
            "This integration works with ExtraHop firmware version greater than or equal to "
            f"{MINIMUM_FIRMWARE_VERSION} and current firmware version being used by you is {version}"
        )
    if response:
        return "ok"
    raise ValueError("Failed to establish connection with provided credentials.")


def prepare_query_records_output(response: Dict) -> str:
    """
        prepare human-readable output for query-records command
    Args:
        response: List of records response from the API
    Returns:
        markdown string to be displayed in the war room
    """
    records = response.get("records", [])
    hr_table = []
    extrahop_context: Dict[str, Any] = {"ExtraHop": {"Record": []}}
    for record in records:
        hr_table.append(record["_source"])
        extrahop_context["ExtraHop"]["Record"].append(createContext(record, keyTransform=string_to_context_key, removeNull=True))
    if len(records) == 0:
        return "No records were found"
    return tableToMarkdown(
        "showing {results} out of {total} Record(s) Found.".format(total=response.get("total", 0), results=len(records)), hr_table
    )


def query_records_command(client: ExtraHopClient, args: Dict[str, Any]) -> CommandResults:
    """Query records command is fetched records in the given timeline from the ExtraHop instance.
    Args:
        client: ExtraHop client to be used.
        args: Arguments received for the command.
    Returns:
        Command Result object.
    """
    query_from = args.get("query_from")
    query_until = args.get("query_until")
    limit = args.get("limit")
    offset = args.get("offset")
    field1 = args.get("field1")
    operator1 = args.get("operator1")
    value1 = args.get("value1")
    field2 = args.get("field2")
    operator2 = args.get("operator2")
    value2 = args.get("value2")
    match_type = args.get("match_type", "")
    specific_types = args.get("types")
    data: Dict = {}
    if specific_types:
        try:
            data["types"] = [f"~{rec_type.strip()}" for rec_type in specific_types.split(",")]
        except Exception:
            raise DemistoException("Error parsing the types argument, expected a comma separated list of types.")
    if query_from:
        data["from"] = query_from
    if query_until:
        data["until"] = query_until
    if limit:
        if int(limit) > 1000:
            data["limit"] = 1000
            data["context_ttl"] = "30s"
        else:
            data["limit"] = int(limit)
    if offset:
        data["offset"] = int(offset)
    data["filter"] = {"operator": match_type, "rules": []}
    if field1:
        rule = {"field": field1, "operator": operator1, "operand": value1 or ""}
        data["filter"]["rules"].append(rule)
    if field2:
        rule = {"field": field2, "operator": operator2, "operand": value2 or ""}
        data["filter"]["rules"].append(rule)
    result = client.get_query_records(body=data)
    cursor = result.get("cursor")
    if cursor and result.get("total", 0) > data["limit"]:
        additional_records = client.get_next_page_records(cursor).get("records", [])
        while (len(additional_records)) > 0:
            result["records"].extend(additional_records)
            additional_records = client.get_next_page_records(cursor).get("records", [])
    readable_output = prepare_query_records_output(result)
    return CommandResults(
        outputs_prefix="ExtraHop.Record",
        outputs_key_field="id",
        outputs=remove_empty_elements_from_response(result),
        readable_output=readable_output,
        raw_response=result,
    )


def main():
    """Parse params and runs command functions."""
    command = demisto.command()
    params = demisto.params()
    args = demisto.args()
    try:
        on_cloud = params.get("on_cloud", False)
        api_key = params.get("apikey")
        base_url = params.get("url").strip("/")
        client_id = params.get("client_id", "")
        client_secret = params.get("client_secret", "")
        verify_certificate = not params.get("insecure", False)
        use_proxy: bool = params.get("proxy", False)

        if on_cloud and (not client_id or not client_secret):
            raise DemistoException("If On Cloud is marked true, Client ID and Client Secret is required field.")

        client = ExtraHopClient(
            base_url=base_url,
            api_key=api_key,
            client_id=client_id,
            client_secret=client_secret,
            verify=verify_certificate,
            use_proxy=use_proxy,
            ok_codes=(200, 201, 204),
            on_cloud=on_cloud,
        )

        remove_nulls_from_dictionary(trim_spaces_from_args(args))

        demisto.info(f"Extrahop command being called is {command}")
        if command == "test-module":
            return_results(test_module(client))
        elif command == "fetch-incidents":
            last_run = demisto.getLastRun()
            incidents, next_run = fetch_incidents(client, params, last_run, on_cloud)
            demisto.setLastRun(next_run)
            demisto.incidents(incidents)
        elif command == "extrahop-watchlist-get":
            return_results(watchlist_get_command(client, on_cloud))
        elif command == "extrahop-peers-get":
            return_results(peers_get_command(client, args, on_cloud))
        elif command == "extrahop-devices-search":
            return_results(devices_search_command(client, args, on_cloud))
        elif command == "extrahop-protocols-get":
            return_results(protocols_get_command(client, args, on_cloud))
        elif command == "extrahop-activity-map-get":
            return_results(activity_map_get_command(client, args, on_cloud))
        elif command == "extrahop-alert-rules-get":
            return_results(alerts_rules_get_command(client))
        elif command == "extrahop-packets-search":
            return_results(packets_search_command(client, args))
        elif command == "extrahop-ticket-track":
            return_results(ticket_track_command(client, args))
        elif command == "extrahop-devices-tag":
            return_results(devices_tag_command(client, args))
        elif command in ("extrahop-alert-rule-create", "extrahop-alert-rule-edit"):
            return_results(create_or_edit_alert_rule_command(client, args))
        elif command == "extrahop-watchlist-edit":
            return_results(watchlist_edit_command(client, args))
        elif command == "extrahop-metrics-list":
            return_results(metrics_list_command(client, args))
        elif command == "extrahop-detections-list":
            return_results(detections_list_command(client, args, on_cloud))
        elif command == "get-remote-data":
            return_results(get_remote_data_command(client, args))
        elif command == "get-modified-remote-data":
            return_results(get_modified_remote_data_command(client, args, params))

        # Deprecated commands.
        elif demisto.command() in ("extrahop-get-alert-rules", "extrahop-get-alerts"):
            return_results(alerts_rules_get_command(client))
        elif demisto.command() == "extrahop-query-records":  # Removed this command.
            return_results(query_records_command(client, args))
        elif demisto.command() == "extrahop-device-search":
            return_results(devices_search_command(client, args, on_cloud))
        elif demisto.command() == "extrahop-edit-watchlist":
            return_results(watchlist_edit_command(client, args))
        elif demisto.command() == "extrahop-get-watchlist":
            return_results(watchlist_get_command(client, on_cloud))
        elif demisto.command() in ("extrahop-create-alert-rule", "extrahop-create-alert"):
            return_results(create_or_edit_alert_rule_command(client, args))
        elif demisto.command() in ("extrahop-edit-alert-rule", "extrahop-edit-alert"):
            return_results(create_or_edit_alert_rule_command(client, args))
        elif demisto.command() == "extrahop-track-ticket":
            return_results(ticket_track_command(client, args))
        elif demisto.command() == "extrahop-get-peers":
            return_results(peers_get_command(client, args, on_cloud))
        elif demisto.command() == "extrahop-get-protocols":
            return_results(protocols_get_command(client, args, on_cloud))
        elif demisto.command() == "extrahop-tag-devices":
            return_results(devices_tag_command(client, args))
        elif demisto.command() == "extrahop-get-activity-map":
            return_results(activity_map_get_command(client, args, on_cloud))
        elif demisto.command() == "extrahop-search-packets":
            return_results(packets_search_command(client, args))
        else:
            raise NotImplementedError(f"Command {command} is not implemented.")
    except Exception as error:
        demisto.error(traceback.format_exc())
        return_error(f"Failed to execute {command} command.\nError:\n{error!s}")  # noqa: E231


if __name__ in ("__main__", "__builtin__", "builtins"):  # pragma: no cover
    main()