Palo Alto Networks IoT

This is the Palo Alto Networks IoT integration (previously Zingbox).

Network Security · IoT by Palo Alto Networks

Details

IDPalo Alto Networks IoT
ProviderPalo Alto Networks
CategoryNetwork Security
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

This is the Palo Alto Networks IoT integration (previously Zingbox).
This integration was integrated and tested with the Banff release of Palo Alto Networks IoT.

Get your Palo Alto Networks IoT Access Keys

This integration requires that API access be configured.
To obtain the Access Key ID and Secret Access Key, refer to the Palo Alto Networks IoT API User Guide.

Configure Palo Alto Networks IoT in Cortex

Parameter Description Required
url Palo Alto Networks IoT Security Portal URL (e.g. https://example.iot.paloaltonetworks.com) True
tenant_id Tenant ID True
access_key_id Access Key ID True
secret_access_key Secret Access Key True
insecure Trust any certificate (not secure) False
proxy Use system proxy settings False
first_fetch First fetch time False
max_fetch Maximum number of incidents per fetch False
fetch_alerts Fetch IoT Alerts False
fetch_vulns Fetch IoT Vulnerabilities False
api_timeout The timeout for querying APIs False
incidentType Incident type False
isFetch Fetch incidents False

Commands

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

iot-security-get-device


IoT get device command - get a single device’s details.

Base Command

iot-security-get-device

Input

Argument Name Description Required
id The device uid (mac address) Required

Context Output

Path Type Description
PaloAltoNetworksIoT.Device unknown Device details.
PaloAltoNetworksIoT.Device.hostname String The hostname of the device.
PaloAltoNetworksIoT.Device.ip_address String The IP address of the device.
PaloAltoNetworksIoT.Device.profile_type String The device profile type: Non_IoT vs IoT.
PaloAltoNetworksIoT.Device.profile_vertical String The device profile vertical.
PaloAltoNetworksIoT.Device.category String The device category
PaloAltoNetworksIoT.Device.profile String The device profile.
PaloAltoNetworksIoT.Device.last_activity Date The last activity timestamp of the device.
PaloAltoNetworksIoT.Device.long_description String The long description of the device.
PaloAltoNetworksIoT.Device.vlan Number The device VLAN ID.
PaloAltoNetworksIoT.Device.site_name String The site which the device is in.
PaloAltoNetworksIoT.Device.risk_score Number The device risk score.
PaloAltoNetworksIoT.Device.risk_level String The device risk level: Low, Medium, High, Critical
PaloAltoNetworksIoT.Device.subnet String The device subnet.
PaloAltoNetworksIoT.Device.first_seen_date Date The first seen date of the device.
PaloAltoNetworksIoT.Device.confidence_score Number The device confidence score.
PaloAltoNetworksIoT.Device.deviceid Date The device ID.
PaloAltoNetworksIoT.Device.location String The device location.
PaloAltoNetworksIoT.Device.vendor String The device vendor.
PaloAltoNetworksIoT.Device.model String The device model.
PaloAltoNetworksIoT.Device.description String The device description.
PaloAltoNetworksIoT.Device.asset_tag String The device asset tag (e.g. a sticky label at the bottom of the device).
PaloAltoNetworksIoT.Device.os_group String The device OS group.
PaloAltoNetworksIoT.Device.Serial_Number String The device serial number.
PaloAltoNetworksIoT.Device.DHCP String Whether the device is in DHCP model: Valid values are Yes or No.
PaloAltoNetworksIoT.Device.wire_or_wireless String Is the device wired or wireless.
PaloAltoNetworksIoT.Device.department String The device department.
PaloAltoNetworksIoT.Device.Switch_Port Number The port of the switch this device is connected to.
PaloAltoNetworksIoT.Device.Switch_Name String The name of the switch this device is connected to.
PaloAltoNetworksIoT.Device.Switch_IP String The IP of the switch this device is connected to.
PaloAltoNetworksIoT.Device.Access_Point_IP String The IP of the access point this device is connected to.
PaloAltoNetworksIoT.Device.Access_Point_Name String The name of the access point this device is connected to.
PaloAltoNetworksIoT.Device.SSID String The SSID of the wireless network this device is connected to.
PaloAltoNetworksIoT.Device.MAC Date The device MAC address.
PaloAltoNetworksIoT.Device.display_tags String The user tags of the device.
PaloAltoNetworksIoT.Device.mac_address String The device MAC address.

Command Example

iot-security-get-device id=00:0f:e5:04:14:4c

Human Readable Output

AD_Domain AD_Username AET Access_Point_IP Access_Point_Name Applications Authentication_Method CMMS_Category CMMS_Source CMMS_State DHCP EAP_Method Encryption_Cipher External_Inventory_Sync_Field MAC NAC_Auth_Info NAC_Auth_State NAC_profile NAC_profile_source NetworkLocation SMB SSID Serial_Number Source Switch_IP Switch_Name Switch_Port Synced_With_Third-Party Time_Synced_With_Third-Party WIFI_Auth_Status WIFI_Auth_Timestamp asset_tag category confidence_score department description deviceid display_tags endpoint_protection endpoint_protection_vendor first_seen_date hostname in_use ip_address is_server last_activity location long_description mac_address model number_of_caution_alerts number_of_critical_alerts number_of_info_alerts number_of_warning_alerts os/firmware_version os_combined os_group parent_mac profile profile_type profile_vertical risk_level risk_score services site_name source subnet vendor vlan wire_or_wireless
                            00:0f:e5:04:14:4c                 Monitored                 Physical Security 94     00:0f:e5:04:14:4c   not_protected   2020-08-13T07:21:02.000Z 00:0f:e5:04:14:4c   10.70.112.20   2020-08-18T19:26:05.000Z     00:0f:e5:04:14:4c   0 0 0 0         Access Control Device IoT Facility Low 10   test-katherine-0821   10.0.0.0/8 HID Global/Mercury Security    

iot-security-list-devices


IoT list devices command

Base Command

iot-security-list-devices

Input

Argument Name Description Required
offset The offset in the pagination. Optional
limit The maximum size of the list of the devices. Optional

Context Output

Path Type Description
PaloAltoNetworksIoT.DeviceList unknown List of devices.

Command Example

iot-security-list-devices offset=0 limit=2

Human Readable Output

AD_Domain AD_Username AET Access_Point_IP Access_Point_Name Applications Authentication_Method CMMS_Category CMMS_Source CMMS_State DHCP EAP_Method Encryption_Cipher External_Inventory_Sync_Field MAC NAC_Auth_Info NAC_Auth_State NAC_profile NAC_profile_source NetworkLocation SMB SSID Serial_Number Source Switch_IP Switch_Name Switch_Port Synced_With_Third-Party Time_Synced_With_Third-Party WIFI_Auth_Status WIFI_Auth_Timestamp asset_tag category confidence_score department description deviceid display_tags endpoint_protection endpoint_protection_vendor first_seen_date hostname in_use ip_address is_server last_activity location long_description mac_address model number_of_caution_alerts number_of_critical_alerts number_of_info_alerts number_of_warning_alerts os/firmware_version os_combined os_group parent_mac profile profile_type profile_vertical risk_level risk_score services site_name source subnet vendor vlan wire_or_wireless
                                              Monitored                 Smartphone 90     356582100001420   not_protected   2020-08-11T01:45:31.000Z 356582100001420   1.0.2.2   2020-08-11T00:09:02.000Z     356582100001420 iPhone 11 (A2223) 0 0 0 0   iOS iOS   Apple iPhone 11 (A2223) IoT Traditional IT Low 21   test   uknown      
                                              Monitored                 Smartphone 90     356582100001430   not_protected   2020-08-11T01:48:05.000Z 356582100001430   1.0.3.2   2020-08-11T00:09:02.000Z     356582100001430 iPhone 11 (A2223) 0 0 0 0   iOS iOS   Apple iPhone 11 (A2223) IoT Traditional IT Low 21   test   uknown      

iot-security-list-alerts


IoT list alerts.

Base Command

iot-security-list-alerts

Input

Argument Name Description Required
start_time The start time in the format of ISO 8601 in UTC, e.g. 2018-11-06T08:56:41Z. Optional
offset The offset in the pagination. Optional
limit The maximum size of the list of the alerts. Optional

Context Output

Path Type Description
PaloAltoNetworksIoT.Alerts unknown List of alerts.

Command Example

iot-security-list-alerts offset=0 limit=2

Human Readable Output

category date description deviceid hostname id inspectorid internal_hostname msg name profile reason_history resolved serviceLevel severity severityNumber siteid tenantid type zb_ticketid
Network Security Equipment 2020-08-26T06:11:04.000Z The usage of an outdated Chrome version has been detected on this device. Using older versions of a web browser can expose your device to security risks. d4:f4:be:b0:c3:10   5f463c8703a2260700a99dbf 012501000732   severity: low<br>taggedBy: PolicyAlert<br>userPolicy: false<br>alertType: security risk<br>localDeviceRole: initiator<br>values: {‘label’: ‘user agent’, ‘value’: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19041’}<br>localProfile: Palo Alto Networks Device<br>description: The usage of an outdated Chrome version has been detected on this device. Using older versions of a web browser can expose your device to security risks.<br>recommendation: {“content”: [“Update the browser to the latest version”, “If browser usage on the device is authorized and essential, use a URL-filtering tool to block connections to known malicious websites or update firewall policy rules to permit connections only to designated websites.”, “Check network traffic coming to and from the device on the device details page and enable trusted behavior by applying an ACL (access control list) to restrict nonessential traffic.”]}<br>alertKey: 24072002d4:f4:be:b0:c3:10analytics-outdated-chrome<br>anomalyMap: {“application”: 1}<br>generationTimestamp: 1598438532757<br>autoPublish: true<br>name: Outdated Chrome version used by IoT device<br>localip: 192.168.58.56<br>fromip: 192.168.58.56<br>id: ObDMsWG0<br>ruleid: analytics-outdated-chrome<br>status: publish<br>toURL: UNKNOWN URL<br>hostname: unknown Outdated Chrome version used by IoT device Palo Alto Networks Device   no   low 2 0   policy_alert alert-ObDMsWG0
IT Server 2020-08-26T02:09:43.000Z This event indicates a brute force attack through multiple login attempts to an SSH server. 00:25:90:92:82:2a   5f45c4a52f31500800a47fc7 012501003437   taggedBy: PolicyAlert<br>values: {‘label’: ‘device profile’, ‘value’: ‘Super Micro Computer’},<br>{‘label’: ‘client port’, ‘value’: 34904},<br>{‘label’: ‘threat ID’, ‘value’: 40015},<br>{‘label’: ‘threat category’, ‘value’: ‘brute-force’},<br>{‘label’: ‘threat type’, ‘value’: ‘vulnerability’},<br>{‘label’: ‘number of occurrences’, ‘value’: 2},<br>{‘label’: ‘alert source’, ‘value’: ‘Firewall’},<br>{‘label’: ‘firewall name’, ‘value’: ‘SJC-Eng-5260-fw1’},<br>{‘label’: ‘firewall action’, ‘value’: ‘Raised an alert’},<br>{‘label’: ‘firewall inbound interface’, ‘value’: ‘vlan’},<br>{‘label’: ‘firewall outbound interface’, ‘value’: ‘vlan’}<br>localProfile: Super Micro Computer<br>localDeviceLabels: Attacker<br>description: This event indicates a brute force attack through multiple login attempts to an SSH server.<br>recommendation: {“content”: [“Enable brute-force login protection by setting a maximum limit for the number of unsuccessful login attempts the device will accept before refusing further attempts.”, “If unauthorized users tried to log in, block the IP addresses from which they made their attempts.”, “Avoid using the manufacturer’s default credentials or the same text string as both the username and password.”, “Strengthen the login username and password for the ssh application.”]}<br>anomalyMap: {“payload”: 2}<br>generationTimestamp: 1598407836500<br>remoteHostMetadata: {‘deviceIds’: [‘10.0.16.245’], ‘ip’: ‘10.0.16.245’, ‘connections’: [{‘app’: ‘ssh’, ‘port’: 22, ‘ipProto’: ‘tcp’}], ‘network’: ‘internal’}<br>toip: 10.0.16.245<br>fromip: 10.0.6.174<br>id: KbYbFjYw<br>severity: medium<br>threatid: 40015<br>userPolicy: false<br>alertType: vulnerability<br>localDeviceRole: initiator<br>appName: ssh<br>alertKey: 2407200200:25:90:92:82:2aanalytics-evt-threat-attacker40015<br>remoteHostLabels: Victim<br>autoPublish: true<br>isAttempt: false<br>forensicData: {“search”: {“iotdevid”: “00:25:90:92:82:2a”, “threatid”: 40015, “remoteIPAddr”: [“10.0.16.245”], “appName”: “ssh”, “tenantid”: “24072002”, “isClient”: “Yes”, “reverse”: true, “timestamp”: 1598407783000, “isLocal”: true, “direction”: “client to server”}, “addFields”: {“rxPkts”: “packets”, “txPkts”: “packets”}}<br>name: SSH User Authentication Brute Force Attempt<br>localip: 10.0.6.174<br>threatCategory: brute-force<br>ruleid: analytics-evt-threat-attacker<br>status: publish<br>toURL: UNKNOWN URL<br>hostname: unknown SSH User Authentication Brute Force Attempt Super Micro Computer   no   medium 3 0   policy_alert alert-KbYbFjYw

iot-security-list-vulns


IoT list Vulnerabilities.

Base Command

iot-security-list-vulns

Input

Argument Name Description Required
start_time The start time in the format of ISO 8601 in UTC, e.g. 2018-11-06T08:56:41Z. Optional
offset The offset in the pagination. Optional
limit The maximum size of the list of the vulnerabilities. Optional

Context Output

Path Type Description
PaloAltoNetworksIoT.Vulns unknown List of vulnerabilities.

Command Example

iot-security-list-vulns limit=2 offset=0

Human Readable Output

asset_tag date detected_date deviceid display_profile_category ip model name os osCombined profile profile_vertical reason_history remediate_checkbox remediate_instruction remediate_workorder risk_level risk_score siteName siteid sn ticketAssignees ticketState vendor vulnerability_name zb_ticketid
  2020-07-16T09:18:21.000Z 2020-08-20T23:59:59.000Z 64:16:7f:77:45:c9 Video Audio Conference 10.72.32.237 Trio8800 Polycom_64167f7745c9 Embedded Embedded Polycom Video Conferencing Device Office         Low 26 test 0       Polycom Vulnerability Test - Medium vuln-65046ad8
  2020-07-22T19:18:32.000Z 2020-08-20T23:59:59.000Z 64:16:7f:76:64:c6 Video Audio Conference 10.72.33.195 Trio8800 Polycom_64167f7664c6 Embedded Embedded Polycom Device Office         Low 26 test 0       Polycom Vulnerability Test - Medium vuln-8cc12cd4

iot-security-resolve-alert


Resolving an IoT alert.

Base Command

iot-security-resolve-alert

Input

Argument Name Description Required
id The alert ID Required
reason The alert resolution reason. Optional
reason_type The alert resolution reason type (No Action Needed, Issue Mitigated). Optional

Context Output

There is no context output for this command.

Command Example

iot-security-resolve-alert id="5e73ecb3eff46f80a7cdc57a" reason=test reason_type="No Action Needed"

iot-security-resolve-vuln


Resolving an IoT vulnerability.

Base Command

iot-security-resolve-vuln

Input

Argument Name Description Required
id The vulnerability ID. Required
full_name The vulnerability full name. Required
reason The vulnerability resolution reason. Optional

Context Output

There is no context output for this command.

Command Example

iot-security-resolve-vuln full_name=CVE-2019-10960 id=vuln-b12d4f0a reason=test

iot-security-get-device-by-ip


IoT get device command - get a single device’s details.

Base Command

iot-security-get-device-by-ip

Input

Argument Name Description Required
ip The device ip (ip address). Required

Context Output

Path Type Description
PaloAltoNetworksIoT.Device unknown Device details.
PaloAltoNetworksIoT.Device.hostname String The hostname of the device.
PaloAltoNetworksIoT.Device.ip_address String The IP address of the device.
PaloAltoNetworksIoT.Device.profile_type String The device profile type: Non_IoT vs IoT.
PaloAltoNetworksIoT.Device.profile_vertical String The device profile vertical.
PaloAltoNetworksIoT.Device.category String The device category
PaloAltoNetworksIoT.Device.profile String The device profile.
PaloAltoNetworksIoT.Device.last_activity Date The last activity timestamp of the device.
PaloAltoNetworksIoT.Device.long_description String The long description of the device.
PaloAltoNetworksIoT.Device.vlan Number The device VLAN ID.
PaloAltoNetworksIoT.Device.site_name String The site which the device is in.
PaloAltoNetworksIoT.Device.risk_score Number The device risk score.
PaloAltoNetworksIoT.Device.risk_level String The device risk level: Low, Medium, High, Critical
PaloAltoNetworksIoT.Device.subnet String The device subnet.
PaloAltoNetworksIoT.Device.first_seen_date Date The first seen date of the device.
PaloAltoNetworksIoT.Device.confidence_score Number The device confidence score.
PaloAltoNetworksIoT.Device.deviceid Date The device ID.
PaloAltoNetworksIoT.Device.location String The device location.
PaloAltoNetworksIoT.Device.vendor String The device vendor.
PaloAltoNetworksIoT.Device.model String The device model.
PaloAltoNetworksIoT.Device.description String The device description.
PaloAltoNetworksIoT.Device.asset_tag String The device asset tag (e.g. a sticky label at the bottom of the device).
PaloAltoNetworksIoT.Device.os_group String The device OS group.
PaloAltoNetworksIoT.Device.Serial_Number String The device serial number.
PaloAltoNetworksIoT.Device.DHCP String Whether the device is in DHCP model: Valid values are Yes or No.
PaloAltoNetworksIoT.Device.wire_or_wireless String Is the device wired or wireless.
PaloAltoNetworksIoT.Device.department String The device department.
PaloAltoNetworksIoT.Device.Switch_Port Number The port of the switch this device is connected to.
PaloAltoNetworksIoT.Device.Switch_Name String The name of the switch this device is connected to.
PaloAltoNetworksIoT.Device.Switch_IP String The IP of the switch this device is connected to.
PaloAltoNetworksIoT.Device.Access_Point_IP String The IP of the access point this device is connected to.
PaloAltoNetworksIoT.Device.Access_Point_Name String The name of the access point this device is connected to.
PaloAltoNetworksIoT.Device.SSID String The SSID of the wireless network this device is connected to.
PaloAltoNetworksIoT.Device.MAC Date The device MAC address.
PaloAltoNetworksIoT.Device.display_tags String The user tags of the device.
PaloAltoNetworksIoT.Device.mac_address String The device MAC address.

Configuration parameters

  • url — Palo Alto Networks IoT Security Portal URL (e.g. https://example.iot.paloaltonetworks.com) (required)
  • tenant_id — Tenant ID (required)
  • credentials — Access Key ID
  • access_key_id — Access Key ID
  • secret_access_key — Secret Access Key
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • first_fetch — First fetch time
  • max_fetch — Maximum number of incidents per fetch
  • fetch_alerts — Fetch IoT Alerts
  • fetch_vulns — Fetch IoT Vulnerabilities
  • api_timeout — The timeout for querying APIs
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • isFetch — Fetch incidents

Commands (7)

  • iot-security-get-device

    IoT get device command - get a single device's details.

  • iot-security-get-device-by-ip

    IoT get device command - get a single device's details.

  • iot-security-list-alerts

    IoT list alerts.

  • iot-security-list-devices

    IoT list devices command.

  • iot-security-list-vulns

    IoT list Vulnerabilities.

  • iot-security-resolve-alert

    Resolving an IoT alert.

  • iot-security-resolve-vuln

    Resolving an IoT vulnerability.

import json
import time
from datetime import UTC, datetime
from typing import Any

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

from CommonServerUserPython import *  # noqa: E402 lgtm [py/polluting-import]

# IMPORTS


# Disable insecure warnings
urllib3.disable_warnings()

# CONSTANTS
# api list size limit
PAGELENGTH = 100


class Client(BaseClient):
    """
    Client will implement the service API, and should not contain any Demisto logic.
    Should only do requests and return data.
    """

    def __init__(
        self,
        base_url,
        tenant_id,
        first_fetch="-1",
        max_fetch=10,
        api_timeout=60,
        verify=True,
        proxy=False,
        ok_codes=(),
        headers=None,
    ):
        super().__init__(base_url, verify=verify, proxy=proxy, ok_codes=ok_codes, headers=headers)
        self.tenant_id = tenant_id
        self.api_timeout = api_timeout
        self.first_fetch = first_fetch
        self.max_fetch = min(max_fetch, PAGELENGTH)

    def _http_request(self, **kwargs):  # type: ignore[override]
        try:
            return super()._http_request(**kwargs)
        except DemistoException as error:
            error_message = error.args[0]
            if "[404]" in error_message:
                ind = error_message.find("Not Found")
                new_message = error_message[:ind] + "\nValidate your server url address"
                raise DemistoException(new_message)
            elif "[403]" in error_message:
                ind = error_message.find("Forbidden")
                new_message = error_message[:ind] + "\nValidate your Tenant ID, Access Key ID or Secret Access Key "
                raise DemistoException(new_message)
            else:
                raise error

    def get_device(self, id):
        """
        Get a device from IoT security portal by device ID
        """
        return self._http_request(
            method="GET", url_suffix="/device", params={"customerid": self.tenant_id, "deviceid": id}, timeout=self.api_timeout
        )

    def get_device_by_ip(self, ip):
        """
        Get a device from IoT security portal by ip
        """
        return self._http_request(
            method="GET", url_suffix="/device/ip", params={"customerid": self.tenant_id, "ip": ip}, timeout=self.api_timeout
        )

    def list_alerts(self, stime="-1", offset=0, pagelength=100, sortdirection="asc"):
        """
        returns alerts inventory list
        """
        data = self._http_request(
            method="GET",
            url_suffix="/alert/list",
            params={
                "customerid": self.tenant_id,
                "offset": offset,
                "pagelength": pagelength,
                "stime": stime,
                "type": "policy_alert",
                "resolved": "no",
                "sortfield": "date",
                "sortdirection": sortdirection,
            },
            timeout=self.api_timeout,
        )
        return data["items"]

    def list_vulns(self, stime="-1", offset=0, pagelength=100):
        """
        returns vulnerability instances
        """
        data = self._http_request(
            method="GET",
            url_suffix="/vulnerability/list",
            params={
                "customerid": self.tenant_id,
                "offset": offset,
                "pagelength": pagelength,
                "stime": stime,
                "type": "vulnerability",
                "status": "Confirmed",
                "groupby": "device",
            },
            timeout=self.api_timeout,
        )
        return data["items"]

    def list_devices(self, offset, pagelength):
        """
        returns a list of devices
        """
        data = self._http_request(
            method="GET",
            url_suffix="/device/list",
            params={
                "customerid": self.tenant_id,
                "filter_monitored": "no",
                "offset": offset,
                "pagelength": pagelength,
                "stime": f"{datetime.utcfromtimestamp(int(time.time()) - 2592000).isoformat()}Z",
                "detail": "true",
                "sortfield": "MAC",
                "sortdirection": "asc",
            },
            timeout=self.api_timeout,
        )
        return data["devices"]

    def resolve_alert(self, alert_id, reason, reason_type="No Action Needed"):
        """
        resolve an IoT alert
        """
        return self._http_request(
            method="PUT",
            url_suffix="/alert/update",
            params={"customerid": self.tenant_id, "id": alert_id},
            json_data={"resolved": "yes", "reason": reason, "reason_type": [reason_type]},
            timeout=self.api_timeout,
        )

    def resolve_vuln(self, vuln_id, full_name, reason):
        """
        resolve an IoT vulnerability
        """
        return self._http_request(
            method="PUT",
            url_suffix="/vulnerability/update",
            params={"customerid": self.tenant_id},
            json_data={"action": "mitigate", "full_name": full_name, "reason": reason, "ticketIdList": [vuln_id]},
            timeout=self.api_timeout,
        )


def arg_to_timestamp(arg: Any, arg_name: str, required: bool = False) -> int | None:
    """Converts an XSOAR argument to a timestamp (seconds from epoch)

    This function is used to quickly validate an argument provided to XSOAR
    via ``demisto.args()`` into an ``int`` containing a timestamp (seconds
    since epoch). It will throw a ValueError if the input is invalid.
    If the input is None, it will throw a ValueError if required is ``True``,
    or ``None`` if required is ``False.

    :type arg: ``Any``
    :param arg: argument to convert

    :type arg_name: ``str``
    :param arg_name: argument name

    :type required: ``bool``
    :param required:
        throws exception if ``True`` and argument provided is None

    :return:
        returns an ``int`` containing a timestamp (seconds from epoch) if conversion works
        returns ``None`` if arg is ``None`` and required is set to ``False``
        otherwise throws an Exception
    :rtype: ``Optional[int]``
    """
    if arg is None:
        if required is True:
            raise ValueError(f'Missing "{arg_name}"')
        return None

    if isinstance(arg, str) and arg.isdigit():
        # timestamp is a str containing digits - we just convert it to int
        return int(arg)
    if isinstance(arg, str):
        # we use dateparser to handle strings either in ISO8601 format, or
        # relative time stamps.
        # For example: format 2019-10-23T00:00:00 or "3 days", etc
        date = dateparser.parse(arg, settings={"TIMEZONE": "UTC"})
        if date is None:
            # if d is None it means dateparser failed to parse it
            raise ValueError(f"Invalid date: {arg}")

        return int(date.replace(tzinfo=UTC).timestamp())
    if isinstance(arg, int | float):
        # Convert to int if the input is a float
        return int(arg)
    raise ValueError(f'Invalid date: "{arg}"')


def test_module(client):
    """
    Returning 'ok' indicates that the integration works like it is supposed to. Connection to the service is successful.

    Args:
        client: IoT client

    Returns:
        'ok' if test passed, anything else will fail the test.
    """
    if demisto.params().get("isFetch"):
        fetch_incidents(client, last_run=demisto.getLastRun(), is_test=True)
    else:
        client.list_devices(0, 1)
    return "ok"


def iot_get_device(client, args):
    """
    Returns an IoT device

    Args:
        client (Client): IoT client.
        args (dict): all command arguments.

    Returns:
        device

        CommandResults
    """
    device_id = args.get("id")

    result = client.get_device(device_id)

    return CommandResults(outputs_prefix="PaloAltoNetworksIoT.Device", outputs_key_field="deviceid", outputs=result)


def iot_get_device_by_ip(client, args):
    """
    Returns an IoT device

    Args:
        client (Client): IoT client.
        args (dict): all command arguments.

    Returns:
        device

        CommandResults
    """
    device_ip = args.get("ip")

    result = client.get_device_by_ip(device_ip)

    return CommandResults(outputs_prefix="PaloAltoNetworksIoT.Device", outputs_key_field="devices", outputs=result["devices"])


def iot_list_devices(client, args):
    """
    Returns a list of IoT devices

    Args:
        client (Client): IoT client.
        args (dict): all command arguments.

    Returns:
        List of devices

        CommandResults
    """
    offset = args.get("offset", "0")
    pagelength = args.get("limit", client.max_fetch)
    result = client.list_devices(offset, pagelength)

    if not result:
        return CommandResults(readable_output="### No devices found")

    return CommandResults(outputs_prefix="PaloAltoNetworksIoT.DeviceList", outputs_key_field="deviceid", outputs=result)


def iot_list_alerts(client, args):
    """
    Returns a list of IoT alerts (max: 1000)

    Args:
        client (Client): IoT client.
        args (dict): all command arguments.

    Returns:
        List of alerts

        CommandResults
    """
    stime = args.get("start_time", "-1")
    offset = args.get("offset", 0)
    pagelength = min(int(args.get("limit", client.max_fetch)), PAGELENGTH)
    result = client.list_alerts(stime, offset, pagelength, "desc")

    if not result:
        return CommandResults(readable_output="### No alerts found")

    return CommandResults(outputs_prefix="PaloAltoNetworksIoT.Alerts", outputs_key_field="id", outputs=result)


def iot_list_vulns(client, args):
    """
    Returns a list of IoT vulnerabilties (max: 1000)

    Args:
        client (Client): IoT client.
        args (dict): all command arguments.

    Returns:
        List of vulnerabilties

        CommandResults
    """
    stime = args.get("start_time", "-1")
    offset = args.get("offset", 0)
    pagelength = min(int(args.get("limit", client.max_fetch)), PAGELENGTH)
    result = client.list_vulns(stime, offset, pagelength)

    if not result:
        return CommandResults(readable_output="### No vulnerabilities found")

    return CommandResults(outputs_prefix="PaloAltoNetworksIoT.Vulns", outputs_key_field="zb_ticketid", outputs=result)


def iot_resolve_alert(client, args):
    """
    Resolve an IoT alert

    Args:
        client (Client): IoT client.
        args (dict): all command arguments.

    Returns:
        None in CommandResults
    """
    alert_id = args.get("id")
    reason = args.get("reason", "resolved by XSOAR")
    reason_type = args.get("reason_type", "No Action Needed")

    client.resolve_alert(alert_id, reason, reason_type)

    return CommandResults(readable_output=f"Alert {alert_id} was resolved successfully")


def iot_resolve_vuln(client, args):
    """
    Resolve an IoT vulnerability

    Args:
        client (Client): IoT client.
        args (dict): all command arguments.

    Returns:
        None in CommandResults
    """
    vuln_id = args.get("id")
    full_name = args.get("full_name")
    reason = args.get("reason", "resolved by XSOAR")

    client.resolve_vuln(vuln_id, full_name, reason)

    return CommandResults(readable_output=f"Vulnerability {vuln_id} was resolved successfully")


def fetch_incidents(client, last_run, is_test=False):
    """
    This function will execute each interval (default is 1 minute).

    Args:
        client (Client): IoT client
        last_run: last_run dict containing the timestamps of the latest incident we fetched from previous fetch

    Returns:
        next_run: This will be last_run in the next fetch-incidents
        incidents: Incidents that will be created in Demisto
    """
    demisto.debug("PaloAltoNetworks_IoT - Start fetching")
    demisto.debug(f"PaloAltoNetworks_IoT - Last run: {json.dumps(last_run)}")
    # Get the last fetch time, if exists
    last_alerts_fetch = last_run.get("last_alerts_fetch")
    last_vulns_fetch = last_run.get("last_vulns_fetch")
    max_fetch = client.max_fetch

    incidents = []

    if demisto.params().get("fetch_alerts", True):
        stime = client.first_fetch
        if last_alerts_fetch is not None:
            # need to add 1ms for the stime
            stime = datetime.utcfromtimestamp(last_alerts_fetch + 0.001).isoformat() + "Z"

        alerts = client.list_alerts(stime, pagelength=max_fetch)
        demisto.debug(f"PaloAltoNetworks_IoT - Number of incidents- alerts before filtering: {len(alerts)}")

        # special handling for the case of having more than the pagelength
        if len(alerts) == max_fetch:
            # get the last date
            last_date = alerts[-1]["date"]
            offset = 0
            done = False
            while not done:
                offset += max_fetch
                others = client.list_alerts(stime, offset, pagelength=max_fetch)
                for alert in others:
                    if alert["date"] == last_date:
                        alerts.append(alert)
                    else:
                        done = True
                        break
                if len(others) != max_fetch:
                    break

        for alert in alerts:
            alert_date_epoch = datetime.strptime(alert["date"], "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=UTC).timestamp()
            alert_id = alert["zb_ticketid"].replace("alert-", "")
            incident = {
                "name": alert["name"],
                "type": "IoT Alert",
                "occurred": alert["date"],
                "rawJSON": json.dumps(alert),
                "details": alert.get("description", ""),
                "CustomFields": {"iotincidenturl": f'{demisto.params()["url"]}/guardian/policies/alert?id={alert_id}'},
            }
            incidents.append(incident)

            # Update last run and add incident if the incident is newer than last fetch
            if last_alerts_fetch is None or alert_date_epoch > last_alerts_fetch:
                last_alerts_fetch = alert_date_epoch

    if demisto.params().get("fetch_vulns", True):
        stime = client.first_fetch
        if last_vulns_fetch is not None:
            # need to add 1ms for the stime
            stime = datetime.utcfromtimestamp(last_vulns_fetch + 0.001).isoformat() + "Z"

        vulns = client.list_vulns(stime, pagelength=max_fetch)

        # special handling for the case of having more than the pagelength
        if len(vulns) == max_fetch:
            # get the last date
            last_date = vulns[-1]["detected_date"]
            if last_date and isinstance(last_date, list):
                last_date = last_date[0]

            offset = 0
            done = False
            while not done:
                offset += max_fetch
                others = client.list_vulns(stime, offset, pagelength=max_fetch)
                for vuln in others:
                    detected_date = vuln["detected_date"]
                    if detected_date and isinstance(detected_date, list):
                        detected_date = detected_date[0]

                    if detected_date == last_date:
                        vulns.append(vuln)
                    else:
                        done = True
                        break
                if len(others) != max_fetch:
                    break
        demisto.debug(f"PaloAltoNetworks_IoT - Number of incidents- vulnerability before filtering: {len(vulns)}")
        for vuln in vulns:
            detected_date = vuln["detected_date"]
            if detected_date and isinstance(detected_date, list):
                detected_date = detected_date[0]

            vuln_date_epoch = datetime.strptime(detected_date, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=UTC).timestamp()
            vuln_name_encoded = vuln["vulnerability_name"].replace(" ", "+")
            incident = {
                "name": vuln["name"],
                "type": "IoT Vulnerability",
                "occurred": detected_date,
                "rawJSON": json.dumps(vuln),
                "details": f'Device {vuln["name"]} at IP {vuln["ip"]}: {vuln["vulnerability_name"]}',
                "CustomFields": {
                    "iotincidenturl": f'{demisto.params()["url"]}/guardian/monitor/inventory/device/'
                    f'{vuln["deviceid"]}?index=0&vuln=true&vulname={vuln_name_encoded}'
                },
            }
            incidents.append(incident)

            if last_vulns_fetch is None or vuln_date_epoch > last_vulns_fetch:
                last_vulns_fetch = vuln_date_epoch

    next_run = {"last_alerts_fetch": last_alerts_fetch, "last_vulns_fetch": last_vulns_fetch}
    demisto.debug(f"PaloAltoNetworks_IoT - Number of incidents (alerts and vulnerability) after filtering : {len(incidents)}")
    demisto.debug(f"PaloAltoNetworks_IoT - Next run after incidents fetching: {json.dumps(next_run)}")

    if is_test:
        return None, None

    return next_run, incidents


def main():
    """
    PARSE AND VALIDATE INTEGRATION PARAMS
    """
    tenant_id = demisto.params()["tenant_id"]
    access_key_id = demisto.params().get("credentials", {}).get("identifier") or demisto.params().get("access_key_id")
    secret_access_key = demisto.params().get("credentials", {}).get("password") or demisto.params().get("secret_access_key")

    api_timeout = 60
    try:
        api_timeout = int(demisto.params().get("api_timeout", "60"))
    except ValueError:
        return_error("API timeout needs to be an integer")

    first_fetch = "-1"
    try:
        ff = arg_to_timestamp(arg=demisto.params().get("first_fetch"), arg_name="First fetch time", required=False)
        if ff:
            first_fetch = datetime.fromtimestamp(ff).astimezone(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
    except ValueError as e:
        return_error(f"First fetch time is in a wrong format. Error: {e!s}")

    max_fetch = 10
    try:
        max_fetch = int(demisto.params().get("max_fetch", "10"))
    except ValueError:
        return_error("Maximum number of incidents per fetch needs to be an integer")

    # get the service API url
    base_url = urljoin(demisto.params()["url"], "/pub/v4.0")

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

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

    demisto.info(f"Command being called is {demisto.command()}")
    try:
        client = Client(
            base_url=base_url,
            tenant_id=tenant_id,
            api_timeout=api_timeout,
            first_fetch=first_fetch,
            max_fetch=max_fetch,
            verify=verify_certificate,
            proxy=proxy,
            ok_codes=(200,),
            headers={"X-Key-Id": access_key_id, "X-Access-Key": secret_access_key},
        )

        if demisto.command() == "test-module":
            # This is the call made when pressing the integration Test button.
            result = test_module(client)
            demisto.results(result)

        elif demisto.command() == "fetch-incidents":
            # Set and define the fetch incidents command to run after activated via integration settings.
            next_run, incidents = fetch_incidents(client=client, last_run=demisto.getLastRun())

            if next_run is not None:
                demisto.setLastRun(next_run)

            if incidents is not None:
                demisto.incidents(incidents)

        elif demisto.command() == "iot-security-get-device":
            return_results(iot_get_device(client, demisto.args()))

        elif demisto.command() == "iot-security-get-device-by-ip":
            return_results(iot_get_device_by_ip(client, demisto.args()))

        elif demisto.command() == "iot-security-list-devices":
            return_results(iot_list_devices(client, demisto.args()))

        elif demisto.command() == "iot-security-list-alerts":
            return_results(iot_list_alerts(client, demisto.args()))

        elif demisto.command() == "iot-security-list-vulns":
            return_results(iot_list_vulns(client, demisto.args()))

        elif demisto.command() == "iot-security-resolve-alert":
            return_results(iot_resolve_alert(client, demisto.args()))

        elif demisto.command() == "iot-security-resolve-vuln":
            return_results(iot_resolve_vuln(client, demisto.args()))

    # Log exceptions
    except Exception as e:
        return_error(f"Failed to execute {demisto.command()} command. Error: {e!s}")


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