Asimily Insight

Integrate Asimily Insight to ingest security anomalies, CVEs, and leverage detailed asset data for streamlined incident investigation.

Network Security · Asimily Insight

Details

IDAsimily Insight
ProviderAsimily
CategoryNetwork Security
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658

README

Integrate Asimily Insight to ingest security anomalies, CVEs, and leverage detailed asset data for streamlined incident investigation.

Asimily Insight delivers comprehensive IoT/OT security and management by providing deep asset visibility, automated vulnerability prioritization, and actionable mitigation strategies aligned with MITRE ATT&CK. It analyzes device communication, calculates holistic risk scores, and enables targeted network segmentation. The platform detects anomalies, simplifies policy management, and automates forensic packet capture. Beyond security, Asimily optimizes asset utilization, tracks operational usage, and manages configuration drift. Its Risk Simulator improves efficiency, and centralized data streamlines IT/OT convergence while identifying unmanaged devices.

This integration utilizes the Asimily Insight RESTful APIs to provide seamless access to comprehensive device data. Users can retrieve detailed information on IT, medical, and security parameters, as well as view known vulnerabilities (CVEs), detect asset anomalies, and maintain synchronized data—either on a regular schedule or on demand.

What does this pack do?

  • On-Demand Asset Retrieval: Query Asimily Insight for detailed device information using search parameters, such as IP address, MAC address, facility, or device ID.
  • Anomaly Alert Synchronization: Regularly sync alerts generated by Asimily Insight to ensure up-to-date visibility into device-related security and operational events.
  • Vulnerability (CVE) Synchronization: Periodically retrieve and update known device vulnerabilities detected by Asimily Insight to support informed risk management and mitigation.
  • Incident Creation in Cortex XSOAR: All synced data—whether alerts, or vulnerabilities is automatically converted into incidents within the Cortex XSOAR platform for streamlined investigation and response workflows. Asset info can be automatically queried and stored by using Playbook to call the asimily-get-asset-details command.

Configure Asimily Insight on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Asimily Insight.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Description Required/Default Type
    Name XSOAR Integration Instance Name True String
    Asimily Portal URL This will be in the following format: https://customer-portal.asimily.com. True String
    API User Name The Asimily Insight API username True String
    Password The password of the Asimily Insight API user True String
    Trust any certificate (not secure)   Default False Boolean
    Use system proxy settings   Default False Boolean
    Device Family Filter for Fetch Operation Fetch incidents only for assets with selected device families in fetch-incidents “All” Multi-Select
    Device Tags Filter for Fetch Operation Fetch incidents only for assets with selected device tags in fetch-incidents “All” Multi-Select
    Fetch Anomaly Alerts If fetch anomalies incidents in fetch-incidents Default False Boolean
    Fetch Anomaly Criticality Fetch anomaly incidents only for anomalies with selected criticality “High Only” Single-Select
    Fetch Device CVEs If fetch CVEs incidents in fetch-incidents Default False Boolean
    Fetch CVE Score Fetch CVEs incidents only for CVEs with selected scores “High Only” Single-Select
    Incidents Fetch Interval The default 5-minute fetch interval can be adjusted based on observed fetch duration to optimize data freshness and system load. 5 Number
    Fetches incidents If Fetch incidents False Boolean
  4. Click Test to validate the URL, API username, API password and connection.

Resetting the “Last Run” Timestamp

If you modify any of the fetch filters (Fetch Anomaly Alerts, Fetch Device CVEs, Device Family Filter, Device Tags Filter, Fetch Anomaly Criticality, Fetch CVE Score), this means there may be new devices included to fetch incidents or there will be new incidents included for existing devices. It is strongly recommended to Reset the “last run” timestamp.

To reset:

  1. Go to the integration instance configuration.
  2. Navigate to Collect > Advanced Settings.
  3. Click Reset the “last run” timestamp.
  4. Click Reset Now.

This ensures that incidents related to newly included devices and updated filters are correctly fetched.

Reset Fetch Incident Last Run Timestamp

Configure Pre-Process Rules

The integration pulls in assets information, anomaly alerts, CVEs from Asimily Insight based on its updates or upon query, creating the need for a preprocessing rule that drops the incoming incident if it’s a duplication.

Follow the guidelines below to configure the preprocessing rule.

  1. Navigate to Settings > Object Setup > Incidents > Pre-Process Rules.
  2. Click +New Rule on the top right corner.
  3. Name the rule appropriately (e.g. Asimily Incident Dedup).
  4. In the Conditions for Incoming Incident section, enter:
    • Type - Includes - Asimily
  5. In the Action section, select Run a script.
  6. In the Choose a script section, pick from the drop-down list:
    • PreProcessAsimilyDedup

The configuration of the preprocessing rule is optional, but highly recommended to avoid duplication.

Pre-process Rule

The integration includes a preprocessing script (PreProcessAsimilyDedup) that will drop the incoming incident if it’s a duplicate. It will search all past incidents with all status.

Alternative rule settings can be:

  1. In the Action section, select Drop and update.
  2. In the Update section, enter
    • Link to - Oldest incident - Created within the last - Your desired timeframe
    • DbotMirrorId - Is identical (Incoming Incident) - to incoming incident

Commands

You can run these commands from the Cortex XSOAR CLI, as part of an automation, or within a playbook.
Upon successful execution, a DBot message will appear in the War Room with execution details.
If asset data, anomaly alerts, or CVE information is retrieved, corresponding incidents will be automatically created with appropriate incident types.

asimily-get-asset-details


Fetch assets details from Asimily Insight. You can add argument filters.

Base Command

asimily-get-asset-details

Input

Argument Description Required Support List
mac_addr The MAC Address of Asimily Asset Optional No
ip_addr The IP Address of Asimily Asset Optional No
facility The Facility of Asimily Asset Optional No
asimily_device_id Asimily Insight given ID for devices Optional No
limit Maximum amount of items to fetch. Optional No

Context Output

Fetched ${fetch_limit} out of ${device_count} assets matching search. Asset fetch count is limited to avoid server overload.

Path Type Description
AsimilyInsight.Asset.asimilydeviceid number Asimily Device ID
AsimilyInsight.Asset.asimilydeviceipv4address unknown Asimily Device IP Address
AsimilyInsight.Asset.asimilydevicemacaddress string Asimily Device MAC Address
AsimilyInsight.Asset.asimilydeviceipv6address unknown Asimily Device IPV6 Address
AsimilyInsight.Asset.asimilydevicemanufacturer string Asimily Device Manufacturer
AsimilyInsight.Asset.asimilydevicemodel string Asimily Device Model
AsimilyInsight.Asset.asimilydeviceos string Asimily Device OS
AsimilyInsight.Asset.asimilydeviceosversion string Asimily Device OS Version
AsimilyInsight.Asset.asimilydevicetype string Asimily Device Type
AsimilyInsight.Asset.asimilydevicefamilies unknown Asimily Device Families
AsimilyInsight.Asset.asimilydeviceserialnumber string Asimily Device Serial Number
AsimilyInsight.Asset.asimilydevicedepartment string Asimily Device Department
AsimilyInsight.Asset.asimilydevicefacility string Asimily Device Facility
AsimilyInsight.Asset.asimilydevicehardwarearchitecture string Asimily Device Hardware Architecture
AsimilyInsight.Asset.asimilydevicehostname string Asimily Device Host Name
AsimilyInsight.Asset.asimilydevicelocation string Asimily Device Location
AsimilyInsight.Asset.asimilydeviceregion string Asimily Device Region
AsimilyInsight.Asset.asimilydevicesoftwareverison string Asimily Device Software Verison
AsimilyInsight.Asset.asimilydeviceifstoreephi boolean Asimily Device If Store Ephi
AsimilyInsight.Asset.asimilydeviceiftransmitephi boolean Asimily Device If Transmit Ephi
AsimilyInsight.Asset.asimilydeviceriskscore number Asimily Device Risk Score
AsimilyInsight.Asset.asimilydevicelikelihood number Asimily Device Likelihood
AsimilyInsight.Asset.asimilydeviceimpact number Asimily Device Impact
AsimilyInsight.Asset.asimilydeviceaverageutilizationpercent number Asimily Device Average Utilization Percent
AsimilyInsight.Asset.asimilydeviceuptime number Asimily Device Up Time
AsimilyInsight.Asset.asimilydeviceisconnected boolean Asimily Device Is Connected
AsimilyInsight.Asset.asimilydeviceiscurrentlyinuse boolean Asimily Device Is Currently In Use
AsimilyInsight.Asset.asimilydeviceisnetworkingdevice boolean ‘Asimily Device Is Networking Device ‘
AsimilyInsight.Asset.asimilydeviceiswireless boolean Asimily Device Is Wireless
AsimilyInsight.Asset.asimilydeviceclass string Asimily Device Class
AsimilyInsight.Asset.asimilydevicemanagedby string Asimily Device Managed By
AsimilyInsight.Asset.asimilydeviceanomalypresent boolean Asimily Device Anomaly Present
AsimilyInsight.Asset.asimilydevicemds2 string Asimily Device MDS2
AsimilyInsight.Asset.asimilydevicecmmsid string Asimily Device CMMS ID
AsimilyInsight.Asset.asimilydevicelastdiscoveredtime date Asimily Device Last Discovered Time
AsimilyInsight.Asset.asimilydevicetag unknown Asimily Device Tag
AsimilyInsight.Asset.asimilydevicemasterfamily string Asimily Device Master Family
AsimilyInsight.Asset.asimilydevicediscoverysource string Asimily Device Discovery Source
AsimilyInsight.Asset.asimilydeviceapplications unknown Asimily Device Applications
AsimilyInsight.Asset.asimilydeviceurl string Asimily Device URL
AsimilyInsight.Asset.asimilydeviceifusingendpointsecurity boolean Asimily Device If Using Endpoint Security

asimily-get-asset-anomalies


Fetch anomaly alerts from Asimily Insight. You can add argument filters.

Base Command

asimily-get-asset-anomalies

Input

Argument Name Description Required Support List
mac_addr The MAC Address of Asimily Asset Optional No
ip_addr The IP Address of Asimily Asset Optional No
asimily_device_id Asimily Insight given ID for devices Optional No
device_family   Optional Yes
device_tag   Optional Yes
criticality Anomaly Alert Criticality Filter. Options: High Only, Medium and High, All Optional No
limit Maximum amount of items to fetch. (Fetch will stop after device anomalies reached the limit) Optional No

Context Output

Fetched ${fetch_count} anomalies for ${fetch_device_count} devices matching search.
${pending_device_count} devices matching search is not fetched. Fetch count is limited to avoid server overload.

Path Type Description
AsimilyInsight.Anomaly.asimilyanomalyname string Asimily Anomaly Name
AsimilyInsight.Anomaly.asimilyanomalycriticality string Asimily Anomaly Criticality
AsimilyInsight.Anomaly.asimilyanomalyearliesttriggertime date Asimily Anomaly Earliest Trigger Time
AsimilyInsight.Anomaly.asimilyanomalylasttriggertime date Asimily Anomaly Last Trigger Time
AsimilyInsight.Anomaly.asimilyanomalyalertid string Asimily Anomaly Alert ID
AsimilyInsight.Anomaly.asimilyanomalyurls unknown Asimily Anomaly URLs
AsimilyInsight.Anomaly.asimilyanomalyisfixed boolean Asimily Anomaly Is Fixed
AsimilyInsight.Anomaly.asimilyanomalyfixby string Asimily Anomaly Fix By
AsimilyInsight.Anomaly.asimilyanomalycriticalityscore number Asimily Anomaly Criticality Score
AsimilyInsight.Anomaly.asimilyanomalymitretactic string Asimily Anomaly Mitre Tactic
AsimilyInsight.Anomaly.asimilyanomalymitretechnique string Asimily Anomaly Mitre Technique
AsimilyInsight.Anomaly.asimilyanomalycategory string Asimily Anomaly Category
AsimilyInsight.Anomaly.asimilyanomalydescription string Asimily Anomaly Description
AsimilyInsight.Anomaly.asimilyanomalycustomeranomalyid string Asimily Anomaly Customer Anomaly ID. Unique anomaly type identifier assigned by Asimily for a specific customer. This ID can be used in future operations such as invoking anomaly fix actions.
AsimilyInsight.Anomaly.asimilydeviceid number Asimily Device ID
AsimilyInsight.Anomaly.asimilydevicemacaddress string Asimily Device MAC Address
AsimilyInsight.Anomaly.asimilydeviceipv4address string Asimily Device IPV4 Address
AsimilyInsight.Anomaly.asimilydevicehostname string Asimily Device Host Name
AsimilyInsight.Anomaly.asimilydevicetype string Asimily Device Type
AsimilyInsight.Anomaly.asimilydevicemodel string Asimily Device Model
AsimilyInsight.Anomaly.asimilydeviceos string Asimily Device OS
AsimilyInsight.Anomaly.asimilydevicemanufacturer string Asimily Device Manufacturer
AsimilyInsight.Anomaly.asimilydevicefamilies unknown Asimily Device Families

asimily-get-asset-vulnerabilities


Fetch device CVEs from Asimily Insight. You can add argument filters.

Base Command

asimily-get-asset-vulnerabilities

Input

Argument Name Description Required Support List
mac_addr The MAC Address of Asimily Asset Optional No
ip_addr The IP Address of Asimily Asset Optional No
asimily_device_id Asimily Insight given ID for devices Optional No
device_family   Optional Yes
device_tag   Optional Yes
cve_score CVE Score Filter. Options: High Only, Medium and High, All (Score limit: High=7.5, Medium=3.5) Optional No
limit Maximum amount of items to fetch. (Fetch will stop after device CVEs reached the limit) Optional No

Context Output

Fetched ${fetch_count} CVEs for ${fetch_device_count} devices matching search.
${pending_device_count} devices matching search is not fetched. Fetch count is limited to avoid server overload.

Path Type Description
AsimilyInsight.CVE.asimilycvename string Asimily CVE Name
AsimilyInsight.CVE.asimilycvecwetype string Asimily CVE CWE Type
AsimilyInsight.CVE.asimilycveentitytype string Asimily CVE Entity Type
AsimilyInsight.CVE.asimilycveentityname string Asimily CVE Entity Name
AsimilyInsight.CVE.asimilycvescore number Asimily CVE Score
AsimilyInsight.CVE.asimilycvecvss3basescore number Asimily CVE CVSS 3 Base Score
AsimilyInsight.CVE.asimilycvedescripttion string Asimily CVE Descripttion
AsimilyInsight.CVE.asimilycveisfixed boolean Asimily CVE Is Fixed
AsimilyInsight.CVE.asimilycvefixedby string Asimily CVE Fixed By
AsimilyInsight.CVE.asimilycveoempatched boolean Asimily CVE OEM Patched
AsimilyInsight.CVE.asimilycveismuted boolean Asimily CVE Is Muted
AsimilyInsight.CVE.asimilycveexploitableinwild boolean Asimily CVE Exploitable In Wild
AsimilyInsight.CVE.asimilycvepublisheddate date Asimily CVE Published Date
AsimilyInsight.CVE.asimilycveopendate date Asimily CVE Open Date
AsimilyInsight.CVE.asimilycvefixeddate date Asimily CVE Fixed Date
AsimilyInsight.CVE.asimilydeviceid number Asimily Device ID
AsimilyInsight.CVE.asimilydevicemacaddress string Asimily Device MAC Address
AsimilyInsight.CVE.asimilydeviceipv4address string Asimily Device IPV4 Address
AsimilyInsight.CVE.asimilydevicehostname string Asimily Device Host Name
AsimilyInsight.CVE.asimilydevicetype string Asimily Device Type
AsimilyInsight.CVE.asimilydevicemodel string Asimily Device Model
AsimilyInsight.CVE.asimilydeviceos string Asimily Device OS
AsimilyInsight.CVE.asimilydevicemanufacturer string Asimily Device Manufacturer
AsimilyInsight.CVE.asimilydevicefamilies string Asimily Device Families

Additional Information

Incident Search

  1. System searchable fields: macaddress, devicelocalip, devicemodel, hostnames, deviceid
  2. Asimily Insight Custom searchable fields: asimilyanomalyname, asimilycvename

Incident Name

  1. For Asimily Anomaly incident, name will be anomalyname|hostname|ip|mac
  2. For Asimily CVE incident, name will be cvename|hostname|ip|mac

Incident Criticality Mapping

  1. XSOAR incidents have 4 levels of criticality:
    • Informational (0)
    • Low (1)
    • Medium (2)
    • High (3)
    • Critical (4)
  2. Asimily Anomaly Alerts have 3 levels of criticality (Low/Medium/High). When creating incidents, we populate the incident criticality using the respective anomaly alert criticality.
  3. Asimily Asset CVEs have Asimily Calculated CVE score (Range: 0-10). When creating incidents, we populate the incident criticality using the respective CVE scores.
    • Low (CVE score: < 3.5)
    • Medium (CVE score: < 7.5)
    • High (CVE score: < 10)

Default Playbook

To enrich incidents with corresponding asset information, we’ve published a default playbook for two custom incident types. This playbook calls asimily-get-asset-details to retrieve asset information and stores it in Context Data (under Asimily > Asset).

We’ve also added a section to custom incident layouts to display this information.

Users can modify this playbook or create similar ones. We recommend using the device ID or MAC address as a unique identifier when querying asset details.

Configuration parameters

  • url — Asimily Portal URL (required)
  • asimilycred — API User Name (required)
  • isFetch — Fetch incidents
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • fetchonlydevicefamilies — Device Family Filter for Fetch Operation (required)
  • fetchonlydevicetags — Device Tags Filter for Fetch Operation (required)
  • iffetchanomalies — Fetch Anomaly Alerts
  • fetchanomalycriticality — Fetch Anomaly Criticality (required)
  • iffetchcves — Fetch Device CVEs
  • fetchcvescore — Fetch CVE Score (required)
  • incidentFetchInterval — Incidents Fetch Interval
  • max_fetch — Max Incidents
  • first_fetch — First Fetch Time Interval
  • incidentType — Incident type

Commands (3)

  • asimily-get-asset-anomalies

    Fetch Asimily Anomalies.

  • asimily-get-asset-details

    Fetch Asimily Asset information.

  • asimily-get-asset-vulnerabilities

    Fetch Asimily CVEs.

from typing import Any, TypeAlias
from collections.abc import Callable
import urllib3
import copy
import dateparser
import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401
from datetime import datetime

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """
LIMIT = 10
DEFAULT_PAGE_SIZE = 5

DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"  # ISO8601 format with UTC, default in XSOAR

BACKOFF_FACTOR = 7.5
TOTAL_RETRIES = 4
REQUEST_TIMEOUT = 180
STATUS_CODE_TO_RETRY = (429, *(status_code for status_code in requests.status_codes._codes if status_code >= 500))  # type: ignore

# Asimily Constants
INCIDENT_CRITICALITY_INFORMATIONAL = 0.5
ASIMILY_ANOMALY_CRITICALITY_INCIDENT_CRITICALITY_MAPPING = {"High": 3, "Medium": 2, "Low": 1}
HIGH_RISK_CVE_LIMIT = 7.5
MEDIUM_RISK_CVE_LIMIT = 3.5
LOW_RISK_CVE_LIMIT = 0.0

MAX_FETCH_PAGE_LIMIT = 500
MAX_FETCH_TOTAL_LIMIT = 1000

# CLI command fetch limit
MAX_FETCH_TOTAL_LIMIT_ASSETS = 1200
MAX_FETCH_PAGE_LIMIT_ASSETS = 400
MAX_FETCH_TOTAL_LIMIT_ASSETS_WITH_APP = 100
MAX_FETCH_PAGE_LIMIT_ASSETS_WITH_APP = 100

# CLI command fetch limit
MAX_FETCH_TOTAL_LIMIT_ANOMALIES_CLI = 10000
MAX_FETCH_PAGE_LIMIT_ANOMALIES_CLI = 100

MAX_FETCH_TOTAL_LIMIT_CVES_CLI = 10000
MAX_FETCH_PAGE_LIMIT_CVES_CLI = 50

# Internal limit of Anomalies/CVEs incident fetch limit per fetch
MAX_FETCH_TOTAL_LIMIT_ANOMALIES = 200
MAX_FETCH_PAGE_LIMIT_ANOMALIES = 100

MAX_FETCH_TOTAL_LIMIT_CVES = 200
MAX_FETCH_PAGE_LIMIT_CVES = 25

ASIMILY_INSIGHT_FETCH_DEVICE_CVES_API = "/api/extapi/assets/device-cves"
ASIMILY_INSIGHT_FETCH_DEVICE_ANOMALIES_API = "/api/extapi/assets/anomalies"
ASIMILY_INSIGHT_FETCH_ASSETS_API = "/api/extapi/assets"

ANOMALIES_API_SORT_DEVICE_ID = "deviceRangeId"
CVES_API_SORT_DEVICE_ID = "deviceInfoId"

ASIMILY_ASSET_CONTEXT_OUTPUT_KEY_ORDER = [
    "asimilydeviceid",
    "asimilydevicemacaddress",
    "asimilydeviceipv4address",
    "asimilydevicemanufacturer",
    "asimilydevicemodel",
    "asimilydevicehostname",
    "asimilydeviceos",
    "asimilydeviceosversion",
    "asimilydevicetype",
    "asimilydeviceserialnumber",
    "asimilydevicefamilies",
    "asimilydevicetag",
    "asimilydevicedepartment",
    "asimilydevicefacility",
    "asimilydevicehardwarearchitecture",
    "asimilydevicelocation",
    "asimilydeviceregion",
    "asimilydevicesoftwareverison",
    "asimilydeviceifstoreephi",
    "asimilydeviceiftransmitephi",
    "asimilydeviceifusingendpointsecurity",
    "asimilydeviceriskscore",
    "asimilydevicelikelihood",
    "asimilydeviceimpact",
    "asimilydeviceaverageutilizationpercent",
    "asimilydeviceuptime",
    "asimilydeviceisconnected",
    "asimilydeviceiscurrentlyinuse",
    "asimilydeviceisnetworkingdevice",
    "asimilydeviceiswireless",
    "asimilydeviceclass",
    "asimilydevicemanagedby",
    "asimilydeviceanomalypresent",
    "asimilydevicemds2",
    "asimilydevicecmmsid",
    "asimilydevicelastdiscoveredtime",
    "asimilydevicemasterfamily",
    "asimilydevicediscoverysource",
    "asimilydeviceapplications",
    "asimilydeviceurl",
    "asimilydeviceipv6address",
]

ASIMILY_ANOMALY_CONTEXT_OUTPUT_KEY_ORDER = [
    "asimilyanomalyname",
    "asimilyanomalycriticality",
    "asimilyanomalyearliesttriggertime",
    "asimilyanomalylasttriggertime",
    "asimilyanomalyalertid",
    "asimilyanomalyurls",
    "asimilyanomalyisfixed",
    "asimilyanomalyfixby",
    "asimilyanomalycriticalityscore",
    "asimilyanomalymitretactic",
    "asimilyanomalymitretechnique",
    "asimilyanomalycategory",
    "asimilyanomalydescription",
    "asimilyanomalycustomeranomalyid",
    "asimilydeviceid",
    "asimilydevicemacaddress",
    "asimilydeviceipv4address",
    "asimilydevicehostname",
    "asimilydevicetype",
    "asimilydevicemodel",
    "asimilydeviceos",
    "asimilydevicemanufacturer",
    "asimilydevicefamilies",
]

ASIMILY_CVE_CONTEXT_OUTPUT_KEY_ORDER = [
    "asimilycvename",
    "asimilycvecwetype",
    "asimilycveentitytype",
    "asimilycveentityname",
    "asimilycvescore",
    "asimilycvecvss3basescore",
    "asimilycvedescripttion",
    "asimilycveisfixed",
    "asimilycvefixedby",
    "asimilycveoempatched",
    "asimilycveismuted",
    "asimilycveexploitableinwild",
    "asimilycvepublisheddate",
    "asimilycveopendate",
    "asimilycvefixeddate",
    "asimilydeviceid",
    "asimilydevicemacaddress",
    "asimilydeviceipv4address",
    "asimilydevicehostname",
    "asimilydevicetype",
    "asimilydevicemodel",
    "asimilydeviceos",
    "asimilydevicemanufacturer",
    "asimilydevicefamilies",
]

QueryFilterType: TypeAlias = dict[str, Any]


class Client(BaseClient):
    """Client class to interact with the service API

    This Client implements API calls, and does not contain any XSOAR logic.
    Should only do requests and return data.
    It inherits from BaseClient defined in CommonServer Python.
    Most calls use _http_request() that handles proxy, SSL verification, etc.
    For this  implementation, no special attributes defined
    """

    def get_base_url(self):
        return self._base_url

    def _force_get_all_wrapper(
        self,
        paginated_getter_func: Callable,
        filters: QueryFilterType | None = None,
        page_start: int | None = None,
        page_size_limit: int | None = None,
        page_end: int | None = None,
        total_limit: int | None = None,
        args: dict | None = {},
        api: str | None = ASIMILY_INSIGHT_FETCH_ASSETS_API,
    ) -> tuple[int, list[Any]]:
        """
        Wrapper method to do paginated query of Asimily Insight APIs
        Args:
        paginated_getter_func: Callable method that calls specific APIs
        filters: QueryFilterType dict
        page_start: start page index
        page_size_limit: page size limit
        page_end: page count limit for fetch
        total_limit: total count fetch limit
        args: dict of parameters for API qeury filter
        api: str of Asimily Insight API

        Returns:
        total_elements: total count of elements to be fetched with given args as query parameter (can be > len(records))
        records: array of fetched events
        """

        page = page_start or 0
        size = page_size_limit or MAX_FETCH_PAGE_LIMIT
        limit = total_limit or MAX_FETCH_TOTAL_LIMIT
        initial_data = paginated_getter_func(filters=filters, page=page, size=size, args=args)

        records = []
        total_pages = 0
        total_elements = 0
        total_incident_count = 0

        if initial_data:
            total_pages = initial_data.get("totalPages", 0)
            total_elements = initial_data.get("totalElements", 0)
            print_debug_msg(
                f"Total pages to be fetched: {total_pages} with page size: {size}, total elements to process: {total_elements}"
            )
            records = initial_data.get("content", [])

        if len(records) > 0 and "v4IpAddrs" in records[0]:
            total_incident_count += len(initial_data.get("content", []))
        else:
            records = []
            for item in initial_data.get("content", []):
                count = len(item.get("anomalies", [])) + len(item.get("cves", []))
                total_incident_count += count
                records.append(item)
                if total_incident_count >= limit:
                    break

        if total_incident_count >= limit:
            print_debug_msg(f"Stop fetching events as limit exceeded, fetched {total_incident_count} events.")
            return total_elements, records

        if page_end is not None and total_pages > 1:
            total_pages = min(page_end, total_pages)

        for page_num in range(1, total_pages):
            print_debug_msg(
                f"Fetching events for page {page_num} with page size {size} with {total_incident_count} events fetched."
            )
            page_data = paginated_getter_func(filters=filters, page=page_num, size=size, args=args)
            if len(records) > 0 and "v4IpAddrs" in records[0]:
                records.extend(page_data.get("content", []))
                total_incident_count += len(page_data.get("content", []))
            else:
                for item in page_data.get("content", []):
                    count = len(item.get("anomalies", [])) + len(item.get("cves", []))
                    total_incident_count += count
                    records.append(item)
                    if total_incident_count >= limit:
                        break

            if total_incident_count >= limit:
                print_debug_msg(f"Stop fetching events as limit exceeded, fetched {total_incident_count} events.")
                break

        records = records[:limit]
        return total_elements, records

    def get_asset_applications_by_mac_addr(self, mac_addr) -> list[dict]:
        """
        Helper method to query asset applications API given mac address
        """
        print_debug_msg(f"Fetching applications for device with mac {mac_addr}")
        init_data = self._http_request(
            method="GET",
            url_suffix="/api/extapi/assets/application",
            params={"macAddr": mac_addr},
            retries=TOTAL_RETRIES,
            status_list_to_retry=STATUS_CODE_TO_RETRY,
            backoff_factor=BACKOFF_FACTOR,
            timeout=REQUEST_TIMEOUT,
        )
        if not init_data:
            return []
        raw_data = init_data[0]
        if raw_data and raw_data.get("applications"):
            app_list = [item["application"] for item in raw_data.get("applications")]
            return app_list
        return []

    def force_get_asset_details(
        self,
        args: dict | None = {},
        page_size_limit: int | None = MAX_FETCH_PAGE_LIMIT_ASSETS,
        total_limit: int | None = MAX_FETCH_TOTAL_LIMIT_ASSETS,
        api: str | None = ASIMILY_INSIGHT_FETCH_ASSETS_API,
    ) -> tuple[int, list[Any]]:
        return self._force_get_all_wrapper(
            paginated_getter_func=self.get_asset_details_call,
            args=args,
            page_size_limit=page_size_limit,
            total_limit=total_limit,
            api=api,
        )

    def force_get_asset_anomalies(
        self,
        args: dict | None = {},
        page_size_limit: int | None = MAX_FETCH_PAGE_LIMIT_ANOMALIES,
        total_limit: int | None = MAX_FETCH_TOTAL_LIMIT_ANOMALIES,
        api: str | None = ASIMILY_INSIGHT_FETCH_DEVICE_ANOMALIES_API,
    ) -> tuple[int, list[Any]]:
        return self._force_get_all_wrapper(
            paginated_getter_func=self.get_asset_anomalies_call,
            args=args,
            page_size_limit=page_size_limit,
            total_limit=total_limit,
            api=api,
        )

    def force_get_asset_cves(
        self,
        args: dict | None = {},
        page_size_limit: int | None = MAX_FETCH_PAGE_LIMIT_CVES,
        total_limit: int | None = MAX_FETCH_TOTAL_LIMIT_CVES,
        api: str | None = ASIMILY_INSIGHT_FETCH_DEVICE_CVES_API,
    ) -> tuple[int, list[Any]]:
        return self._force_get_all_wrapper(
            paginated_getter_func=self.get_asset_cves_call,
            args=args,
            page_size_limit=page_size_limit,
            total_limit=total_limit,
            api=api,
        )

    def get_asset_details_call(
        self,
        args: dict | None = {},
        page: int | None = 0,
        size: int | None = MAX_FETCH_PAGE_LIMIT,
        filters: QueryFilterType | None = None,
        resp_type: str | None = "json",
        api: str | None = ASIMILY_INSIGHT_FETCH_ASSETS_API,
    ) -> dict:
        result = args.copy() if args is not None else {}
        result["size"] = size if size is not None else MAX_FETCH_PAGE_LIMIT
        result["page"] = page if page is not None else 0
        return self._http_request(
            method="GET",
            url_suffix=api,
            params=result,
            retries=TOTAL_RETRIES,
            status_list_to_retry=STATUS_CODE_TO_RETRY,
            backoff_factor=BACKOFF_FACTOR,
            timeout=REQUEST_TIMEOUT,
            resp_type=resp_type,
        )

    def get_asset_anomalies_call(
        self,
        args: dict | None = {},
        page: int | None = 0,
        size: int | None = MAX_FETCH_PAGE_LIMIT,
        filters: QueryFilterType | None = None,
        resp_type: str | None = "json",
        api: str | None = ASIMILY_INSIGHT_FETCH_DEVICE_ANOMALIES_API,
    ) -> dict:
        filter_json = create_filter_json(args, api)
        print_debug_msg(filter_json)
        result: Dict[str, int | str] = {}
        result["size"] = size if size is not None else MAX_FETCH_PAGE_LIMIT
        result["page"] = page if page is not None else 0
        result["sort"] = ANOMALIES_API_SORT_DEVICE_ID
        print_debug_msg(result)
        return self._http_request(
            method="POST",
            url_suffix=api,
            params=result,
            json_data=filter_json,
            retries=TOTAL_RETRIES,
            status_list_to_retry=STATUS_CODE_TO_RETRY,
            backoff_factor=BACKOFF_FACTOR,
            timeout=REQUEST_TIMEOUT,
        )

    def get_asset_cves_call(
        self,
        args: dict | None = {},
        page: int | None = 0,
        size: int | None = MAX_FETCH_PAGE_LIMIT,
        filters: QueryFilterType | None = None,
        resp_type: str | None = "json",
        api: str | None = ASIMILY_INSIGHT_FETCH_DEVICE_CVES_API,
    ) -> dict:
        filter_json = create_filter_json(args, api)
        add_non_fixed_cve_filter(filter_json)
        print_debug_msg(filter_json)
        result: Dict[str, int | str] = {}
        result["size"] = size if size is not None else MAX_FETCH_PAGE_LIMIT
        result["page"] = page if page is not None else 0
        result["sort"] = CVES_API_SORT_DEVICE_ID
        return self._http_request(
            method="POST",
            url_suffix=api,
            params=result,
            json_data=filter_json,
            retries=TOTAL_RETRIES,
            status_list_to_retry=STATUS_CODE_TO_RETRY,
            backoff_factor=BACKOFF_FACTOR,
            timeout=REQUEST_TIMEOUT,
        )


""" HELPER FUNCTIONS """


def calculate_asimily_cve_incident_criticality(cve_score) -> int:
    """
    Helper method to calculate Asimily CVE incident XSOAR severity using CVE score
    """
    if cve_score:
        if cve_score >= HIGH_RISK_CVE_LIMIT:
            return 3
        if cve_score >= MEDIUM_RISK_CVE_LIMIT:
            return 2
        return 1
    return 0


def populate_asimily_asset_anomaly_incident_system_field(incident, raw_data, device_data):
    if raw_data is None:
        return
    incident["type"] = "Asimily Anomaly"
    incident["severity"] = ASIMILY_ANOMALY_CRITICALITY_INCIDENT_CRITICALITY_MAPPING.get(raw_data.get("criticality"), 0)
    incident["dbotMirrorId"] = str(raw_data.get("alertId")) + (
        ("|" + str(device_data.get("deviceId"))) if device_data.get("deviceId") else ""
    )

    # anomalyname|hostname|ip|mac
    incident["name"] = raw_data.get("anomaly") if raw_data.get("anomaly") else ""
    if device_data.get("hostName"):
        incident["name"] = incident["name"] + "|" + device_data.get("hostName")
    if device_data.get("ipAddr"):
        incident["name"] = incident["name"] + "|" + device_data.get("ipAddr")
    if device_data.get("macAddr"):
        incident["name"] = incident["name"] + "|" + device_data.get("macAddr")
    incident["name"] = incident["name"].lstrip("|")


def populate_asimily_asset_cve_incident_system_field(incident, raw_data, device_data):
    if raw_data is None:
        return
    incident["type"] = "Asimily CVE"
    incident["severity"] = calculate_asimily_cve_incident_criticality(raw_data.get("score"))

    # cvename|hostname|ip|mac
    incident["name"] = raw_data.get("cveName") if raw_data.get("cveName") else ""
    incident["dbotMirrorId"] = incident["name"] + (
        ("|" + str(device_data.get("deviceId"))) if device_data.get("deviceId") else ""
    )

    if device_data.get("hostName"):
        incident["name"] = incident["name"] + "|" + device_data.get("hostName")
    if device_data.get("ipAddr"):
        incident["name"] = incident["name"] + "|" + device_data.get("ipAddr")
    if device_data.get("macAddr"):
        incident["name"] = incident["name"] + "|" + device_data.get("macAddr")
    incident["name"] = incident["name"].lstrip("|")


def create_filter_json(
    input_dict: dict[str, Any] | None = {}, api: str | None = ASIMILY_INSIGHT_FETCH_ASSETS_API
) -> dict[str, dict[str, list[dict[str, Any]]]]:
    """
    Convert an input dictionary to a JSON filter structure based on specified rules.
    Args:
        input_dict: Dictionary with keys like "cveScore", "criticality", etc.

    Returns:
        Dictionary formatted as the requested JSON structure.
    """
    result: dict[str, list[dict[str, Any]]] = {}

    # Define mappings for criticality
    criticality_map = {"High Only": ["HIGH"], "Medium and High": ["MEDIUM", "HIGH"]}

    # Define mappings for cveScore with operator
    cve_score_map = {"High Only": (7.5, "Gte"), "Medium and High": (3.5, "Gte")}

    input_filters = (input_dict or {}).copy()
    if input_filters.get("deviceFamily") and (not isinstance(input_filters["deviceFamily"], list)):
        input_filters["deviceFamily"] = argToList(input_filters["deviceFamily"])
    if input_filters.get("deviceTag") and (not isinstance(input_filters["deviceTag"], list)):
        input_filters["deviceTag"] = argToList(input_filters["deviceTag"])

    for key, value in input_filters.items():
        # Skip if value is "All" or a list containing "All"
        if value == "All" or (isinstance(value, list) and "All" in value):
            continue

        # Skip adding limit as filter
        if key == "limit":
            continue

        # Handle criticality
        if key == "criticality":
            if api != ASIMILY_INSIGHT_FETCH_DEVICE_ANOMALIES_API:
                continue
            if isinstance(value, list):
                print_debug_msg("criticality must be a string, not a list")
                continue
            if value in criticality_map:
                result["anomaliesCriticality"] = [{"operator": ":", "value": crit} for crit in criticality_map[value]]
            elif value != "All":
                print_debug_msg(f"Unsupported criticality value {value}")
                continue
            continue

        # Handle cveScore
        if key == "cveScore":
            if api != ASIMILY_INSIGHT_FETCH_DEVICE_CVES_API:
                continue
            if isinstance(value, list):
                print_debug_msg("cveScore must be a string, not a list")
                continue
            if value in cve_score_map:
                val, op = cve_score_map[value]
                result["cveScore"] = [{"operator": op, "value": val}]
            elif value != "All":
                print_debug_msg(f"Unsupported cveScore value {value}")
                continue
            continue

        if key == "deviceRangeId":
            if value > 0:
                result[key] = [{"operator": "Grt", "value": value}]
            continue

        if key == "cvesLastUpdatedSince" or key == "anomaliesLastUpdatedSince":
            result[key] = [{"operator": ">", "value": value}]
            continue

        # Handle other fields (deviceFamily, deviceTag, ipAddr, macAddr)
        if isinstance(value, list):
            # Create an object for each list item
            result[key] = [{"operator": ":", "value": item} for item in value]
        else:
            # Create a single object for non-list values
            result[key] = [{"operator": ":", "value": value}]

    filter_json: dict[str, dict[str, list[dict[str, Any]]]] = {}
    filter_json["filters"] = result
    print_debug_msg(filter_json)

    return filter_json


def process_params_and_args(params: dict[str, Any], args: dict[str, Any], command: str) -> dict[str, Any]:
    """
    Process and update args based on command and params.

    Logic:
    0. Rename "asimilyDeviceId" to "deviceInfoId" in args.
    1. If command != "fetch-incidents", return updated args.
    2. If command == "fetch-incidents", further process params:
       - If "fetchonlydevicefamilies" in params, add to args["deviceFamily"]
       - If "fetchonlydevicetags" in params, add to args["deviceTag"]
       - If "iffetchanomalies" is True and "fetchanomalycriticality" in params, set args["criticality"]
       - If "iffetchcves" is True and "fetchcvescore" in params, set args["cveScore"]

    Args:
        params (dict): Additional parameters.
        args (dict): Arguments dictionary to process.
        command (str): Command name.

    Returns:
        dict: A new dictionary with the updated keys.
    """
    updated_args = args.copy()

    if "asimily_device_id" in updated_args:
        if command == "asimily-get-asset-details":
            updated_args["deviceId"] = updated_args.pop("asimily_device_id")  # asset API expects different key
        else:
            updated_args["deviceInfoId"] = updated_args.pop("asimily_device_id")

    if command != "fetch-incidents":
        # transform XSOAR parameters into Asimily filter names
        if "mac_addr" in updated_args:
            updated_args["macAddr"] = updated_args.pop("mac_addr")
        if "ip_addr" in updated_args:
            updated_args["ipAddr"] = updated_args.pop("ip_addr")
        if "device_family" in updated_args:
            updated_args["deviceFamily"] = updated_args.pop("device_family")
        if "device_tag" in updated_args:
            updated_args["deviceTag"] = updated_args.pop("device_tag")
        if "cve_score" in updated_args:
            updated_args["cveScore"] = updated_args.pop("cve_score")
        return updated_args

    if "fetchonlydevicefamilies" in params:
        updated_args["deviceFamily"] = params["fetchonlydevicefamilies"]

    if "fetchonlydevicetags" in params:
        updated_args["deviceTag"] = params["fetchonlydevicetags"]

    if params.get("iffetchanomalies") and "fetchanomalycriticality" in params:
        updated_args["criticality"] = params["fetchanomalycriticality"]

    if params.get("iffetchcves") and "fetchcvescore" in params:
        updated_args["cveScore"] = params["fetchcvescore"]

    return updated_args


def add_non_fixed_cve_filter(filter_json: dict[str, Any]):
    if filter_json.get("filters"):
        filter_json["filters"]["isFixed"] = [{"operator": ":", "value": 56}]


""" COMMAND FUNCTIONS """


def test_module(client: Client, params: dict[str, Any]) -> str:
    try:
        response = client.get_asset_details_call(size=1, page=0, resp_type="text")
        if "loginStyle" in response:  # Current Asimily API will direct to login page when creds are incorrect
            return "Authentication Error: The specified API username and password are not correct."
        if params.get("isFetch", False) and params.get("iffetchanomalies", False):
            response = client.get_asset_anomalies_call(size=1, page=0)
            if "content" not in response:
                return "Exception occured when validating fetch anomalies, no content in response."
        if params.get("isFetch", False) and params.get("iffetchcves", False):
            response = client.get_asset_cves_call(size=1, page=0)
            if "content" not in response:
                return "Exception occured when validating fetch cves, no content in response."

    except DemistoException as e:
        if "Forbidden" in str(e):
            return "Authentication Error: The specified API username and password are not correct."
        else:
            return f"Exception occured: {str(e)}"
    return "ok"


def print_debug_msg(msg: Any):
    """
    Prints a message to debug with Asimily-Insight-Msg prefix.
    Args:
        msg (str): Message to be logged.
    """
    demisto.debug(f"Asimily-Insight-Msg - {msg}")


def format_date(date: Union[str, datetime] | None, format: str = DATE_FORMAT) -> str:
    if not date:
        return ""
    dt = date if isinstance(date, datetime) else dateparser.parse(date)
    assert dt is not None
    return dt.strftime(format)


def construct_url(base, *paths, **query_params):
    url = base.rstrip("/")
    for path in paths:
        url += "/" + path.strip("/")
    if query_params:
        url += "?" + urllib.parse.urlencode(query_params, doseq=True)
    return url


def construct_asimily_asset_portal_url(portal_base_url, asimilydeviceid):
    if asimilydeviceid:
        return f"{portal_base_url}/index.html#/asset/1/{asimilydeviceid}"
    return None


def map_asimily_asset_entity_from_asimily_assets_json(client: Client, incident, raw_data, base_url):
    incident["customFields"] = {}
    if raw_data is None:
        return
    incident["customFields"]["asimilydeviceid"] = raw_data.get("deviceID")
    incident["customFields"]["asimilydevicemacaddress"] = raw_data.get("macAddr")
    incident["customFields"]["asimilydeviceipv4address"] = raw_data.get("v4IpAddrs")
    incident["customFields"]["asimilydeviceipv6address"] = raw_data.get("v6IpAddrs")
    incident["customFields"]["asimilydevicemanufacturer"] = raw_data.get("manufacturer")
    incident["customFields"]["asimilydevicemodel"] = raw_data.get("deviceModel")
    incident["customFields"]["asimilydeviceos"] = raw_data.get("os")
    incident["customFields"]["asimilydeviceosversion"] = raw_data.get("osVersion")
    incident["customFields"]["asimilydevicetype"] = raw_data.get("deviceType")
    incident["customFields"]["asimilydevicefamilies"] = raw_data.get("deviceFamilies")
    incident["customFields"]["asimilydeviceserialnumber"] = raw_data.get("serialNumber")
    incident["customFields"]["asimilydevicedepartment"] = raw_data.get("department")
    incident["customFields"]["asimilydevicefacility"] = raw_data.get("facility")
    incident["customFields"]["asimilydevicehardwarearchitecture"] = raw_data.get("hardwareArchitecture")
    incident["customFields"]["asimilydevicehostname"] = raw_data.get("hostName")
    incident["customFields"]["asimilydevicelocation"] = raw_data.get("location")
    incident["customFields"]["asimilydeviceregion"] = raw_data.get("region")
    incident["customFields"]["asimilydevicesoftwareverison"] = raw_data.get("softwareVersion")
    incident["customFields"]["asimilydeviceifstoreephi"] = raw_data.get("storesEphi")
    incident["customFields"]["asimilydeviceiftransmitephi"] = raw_data.get("transmitEphi")
    incident["customFields"]["asimilydeviceriskscore"] = raw_data.get("riskScore")
    incident["customFields"]["asimilydevicelikelihood"] = raw_data.get("likelihood")
    incident["customFields"]["asimilydeviceimpact"] = raw_data.get("impact")
    incident["customFields"]["asimilydeviceaverageutilizationpercent"] = raw_data.get("avgUtilizationPercent")
    incident["customFields"]["asimilydeviceuptime"] = raw_data.get("uptime")
    incident["customFields"]["asimilydeviceisconnected"] = raw_data.get("isConnected")
    incident["customFields"]["asimilydeviceiscurrentlyinuse"] = raw_data.get("isCurrentlyInUse")
    incident["customFields"]["asimilydeviceisnetworkingdevice"] = raw_data.get("isNetworkingDevice")
    incident["customFields"]["asimilydeviceiswireless"] = raw_data.get("isWireless")
    incident["customFields"]["asimilydeviceclass"] = raw_data.get("deviceClass")
    incident["customFields"]["asimilydevicemangedby"] = raw_data.get("managedBy")
    incident["customFields"]["asimilydeviceanomalypresent"] = raw_data.get("anomalyPresent")
    incident["customFields"]["asimilydevicemds2"] = raw_data.get("mds2")
    incident["customFields"]["asimilydevicecmmsid"] = raw_data.get("cmmsId")
    incident["customFields"]["asimilydevicelastdiscoveredtime"] = (
        format_date(dateparser.parse(raw_data.get("lastDiscoveredAt"))) if raw_data.get("lastDiscoveredAt") else None
    )
    incident["customFields"]["asimilydevicetag"] = raw_data.get("deviceTag")
    incident["customFields"]["asimilydevicemasterfamily"] = raw_data.get("deviceMasterFamily")
    incident["customFields"]["asimilydevicediscoverysource"] = raw_data.get("discoverySourceValue")
    incident["customFields"]["asimilydeviceifusingendpointsecurity"] = (
        False if raw_data.get("isUsingEndpointSecurity") is None else raw_data.get("isUsingEndpointSecurity")
    )
    incident["customFields"]["asimilydeviceurl"] = construct_asimily_asset_portal_url(base_url, raw_data.get("deviceID"))

    if raw_data.get("macAddr"):
        incident["customFields"]["asimilydeviceapplications"] = client.get_asset_applications_by_mac_addr(raw_data.get("macAddr"))


def get_asset_anomalies_cves_asimily_device_fields(device_obj):
    device_fields = {}
    device_fields["asimilydeviceid"] = device_obj.get("deviceId")
    device_fields["asimilydevicemacaddress"] = device_obj.get("macAddr")
    device_fields["asimilydeviceipv4address"] = [device_obj.get("ipAddr")]
    device_fields["asimilydevicetype"] = device_obj.get("deviceType")
    device_fields["asimilydevicemodel"] = device_obj.get("deviceModel")
    device_fields["asimilydeviceos"] = device_obj.get("os")
    device_fields["asimilydevicemanufacturer"] = device_obj.get("manufacturer")
    device_fields["asimilydevicehostname"] = device_obj.get("hostName")
    if device_obj.get("deviceFamily"):
        device_family_str = device_obj.get("deviceFamily")
        device_fields["asimilydevicefamilies"] = [s.strip() for s in device_family_str.split(",")]

    # populate built-in incident fields for search
    device_fields["devicelocalip"] = device_obj.get("ipAddr")  # searchable
    device_fields["macaddress"] = device_obj.get("macAddr")  # searchable
    device_fields["devicemodel"] = device_obj.get("deviceModel")  # searchable
    device_fields["hostnames"] = device_obj.get("hostName")  # searchable
    device_fields["deviceid"] = str(device_obj.get("deviceId") or "")  # searchable

    return device_fields


def populate_asset_anomalies_asimily_anomaly_fields(incident, anomaly_obj):
    anomaly_fields = {}
    anomaly_fields["asimilyanomalyname"] = anomaly_obj.get("anomaly")
    anomaly_fields["asimilyanomalydescription"] = anomaly_obj.get("description")
    anomaly_fields["asimilyanomalycriticality"] = anomaly_obj.get("criticality")
    anomaly_fields["asimilyanomalyearliesttriggertime"] = (
        format_date(dateparser.parse(anomaly_obj.get("earliestTriggerTime"))) if anomaly_obj.get("earliestTriggerTime") else None
    )
    anomaly_fields["asimilyanomalylasttriggertime"] = (
        format_date(dateparser.parse(anomaly_obj.get("latestTriggerTime"))) if anomaly_obj.get("latestTriggerTime") else None
    )
    anomaly_fields["asimilyanomalyalertid"] = anomaly_obj.get("alertId")

    value = anomaly_obj.get("domainDeviceOrIpDevice")
    anomaly_fields["asimilyanomalyurls"] = [x.strip() for x in value.split(",")] if value else []
    anomaly_fields["asimilyanomalyisfixed"] = anomaly_obj.get("isFixed") == 55
    anomaly_fields["asimilyanomalyfixby"] = anomaly_obj.get("fixBy")
    anomaly_fields["asimilyanomalycriticalityscore"] = anomaly_obj.get("anomalyScore")
    anomaly_fields["asimilyanomalymitretactic"] = anomaly_obj.get("mitreTactic")
    anomaly_fields["asimilyanomalymitretechnique"] = anomaly_obj.get("mitreTechnique")
    anomaly_fields["asimilyanomalycategory"] = anomaly_obj.get("anomalyCategory")
    anomaly_fields["asimilyanomalycustomeranomalyid"] = anomaly_obj.get("customerAnomalyId")
    if incident.get("customFields") is None:
        incident["customFields"] = anomaly_fields
    else:
        incident.get("customFields").update(anomaly_fields)


def populate_asset_cves_asimily_cve_fields(incident, cve_obj):
    cve_fields = {}
    cve_fields["asimilycvename"] = cve_obj.get("cveName")
    cve_fields["asimilycvecwetype"] = cve_obj.get("cveTitle")
    cve_fields["asimilycveentitytype"] = cve_obj.get("productType")
    cve_fields["asimilycveentityname"] = cve_obj.get("productName")
    cve_fields["asimilycvescore"] = cve_obj.get("score")
    cve_fields["asimilycvecvss3basescore"] = cve_obj.get("cvssBaseScore")
    cve_fields["asimilycvedescription"] = cve_obj.get("description")
    cve_fields["asimilycveisfixed"] = cve_obj.get("isFixed") and cve_obj.get("isFixed") == 55
    cve_fields["asimilycvefixedby"] = cve_obj.get("fixedBy")
    cve_fields["asimilycveoempatched"] = cve_obj.get("oemPatched")
    cve_fields["asimilycveismuted"] = cve_obj.get("isCveMuted")
    cve_fields["asimilycveexploitableinwild"] = cve_obj.get("exploitableInWild")
    cve_fields["asimilycvepublisheddate"] = (
        format_date(dateparser.parse(cve_obj.get("nvdPublishDate"))) if cve_obj.get("nvdPublishDate") else None
    )
    cve_fields["asimilycveopendate"] = format_date(dateparser.parse(cve_obj.get("openDate"))) if cve_obj.get("openDate") else None
    cve_fields["asimilycvefixeddate"] = (
        format_date(dateparser.parse(cve_obj.get("fixedDate"))) if cve_obj.get("fixedDate") else None
    )

    if incident.get("customFields") is None:
        incident["customFields"] = cve_fields
    else:
        incident.get("customFields").update(cve_fields)


def get_asset_anomalies_command(client: Client, args, if_from_fetch_incident):
    """
    Returns:
        CommandResults: when triggered from CLI command
        incidents, last_device_id, total_elements: when triggered from fetch-incident

    """
    try:
        limit = int(args.get("limit", 0))
    except (ValueError, TypeError):
        limit = 0

    page_size_limit = MAX_FETCH_PAGE_LIMIT_ANOMALIES if if_from_fetch_incident else MAX_FETCH_PAGE_LIMIT_ANOMALIES_CLI
    total_limit = MAX_FETCH_TOTAL_LIMIT_ANOMALIES if if_from_fetch_incident else MAX_FETCH_TOTAL_LIMIT_ANOMALIES_CLI
    if limit > 0:
        page_size_limit = min(page_size_limit, limit)
        total_limit = min(limit, total_limit)

    total_elements, init_data = client.force_get_asset_anomalies(
        args=args, page_size_limit=page_size_limit, total_limit=total_limit
    )
    anomaly_incidents = []
    last_device_id = None
    for device_obj in init_data:
        device_anomaly_list = device_obj.get("anomalies", [])
        device_fields_dict = get_asset_anomalies_cves_asimily_device_fields(device_obj)
        last_device_id = device_obj.get("deviceId")
        for anomaly_obj in device_anomaly_list:
            incident = {}
            incident["customFields"] = device_fields_dict.copy()
            populate_asimily_asset_anomaly_incident_system_field(incident, anomaly_obj, device_obj)
            populate_asset_anomalies_asimily_anomaly_fields(incident, anomaly_obj)
            anomaly_incidents.append(incident)
    msg = (
        f"Fetched {len(anomaly_incidents)} anomalies for {len(init_data)} devices "
        f"matching search, total matching device count is {total_elements}"
    )
    print_debug_msg(msg)

    # return CommandResults when calling from CLI command
    if not if_from_fetch_incident:
        msg = f"Fetched {len(anomaly_incidents)} anomalies for {len(init_data)} devices matching search."
        if total_elements and total_elements > len(init_data):
            msg = (
                msg + f"\n{total_elements - len(init_data)} devices matching search is not fetched. "
                f"Fetch count is limited to avoid server overload."
            )

        asimily_anomaly_list = [item["customFields"] for item in anomaly_incidents if "customFields" in item]

        human_readable_output = tableToMarkdown(
            "Asimily Insight Anomalies List", asimily_anomaly_list, headers=ASIMILY_ANOMALY_CONTEXT_OUTPUT_KEY_ORDER
        )
        results = CommandResults(
            readable_output=f"{msg}\n\n{human_readable_output}",
            outputs_prefix="AsimilyInsight.Anomaly",
            outputs_key_field=["asimilydeviceid", "asimilyanomalyname"],
            outputs=asimily_anomaly_list,
            raw_response=init_data,
            ignore_auto_extract=True,
        )
        return results

    # return incident list for incident creation
    return anomaly_incidents, last_device_id, total_elements


def get_asset_cves_command(client: Client, args, if_from_fetch_incident):
    """
    Returns:
        CommandResults: when triggered from CLI command
        incidents, last_device_id, total_elements: when triggered from fetch-incident

    """
    try:
        limit = int(args.get("limit", 0))
    except (ValueError, TypeError):
        limit = 0

    page_size_limit = MAX_FETCH_PAGE_LIMIT_CVES if if_from_fetch_incident else MAX_FETCH_PAGE_LIMIT_CVES_CLI
    total_limit = MAX_FETCH_TOTAL_LIMIT_CVES if if_from_fetch_incident else MAX_FETCH_TOTAL_LIMIT_CVES_CLI
    if limit > 0:
        page_size_limit = min(page_size_limit, limit)
        total_limit = min(limit, total_limit)

    total_elements, init_data = client.force_get_asset_cves(args=args, page_size_limit=page_size_limit, total_limit=total_limit)
    cves_incidents = []
    last_device_id = None
    for device_obj in init_data:
        device_cve_list = device_obj.get("cves", [])
        device_fields_dict = get_asset_anomalies_cves_asimily_device_fields(device_obj)
        last_device_id = device_obj.get("deviceId")
        for cve_obj in device_cve_list:
            incident = {}
            incident["customFields"] = device_fields_dict.copy()
            populate_asimily_asset_cve_incident_system_field(incident, cve_obj, device_obj)
            populate_asset_cves_asimily_cve_fields(incident, cve_obj)
            cves_incidents.append(incident)

    msg = (
        f"Fetched {len(cves_incidents)} CVEs for {len(init_data)} devices "
        f"matching search, total matching device count is {total_elements}"
    )
    print_debug_msg(msg)

    # return CommandResults when calling from CLI command
    if not if_from_fetch_incident:
        msg = f"Fetched {len(cves_incidents)} CVEs for {len(init_data)} devices matching search."
        if total_elements and total_elements > len(init_data):
            msg = (
                msg + f"\n{total_elements - len(init_data)} devices matching search is not fetched. "
                f"Fetch count is limited to avoid server overload."
            )

        asimily_cve_list = [item["customFields"] for item in cves_incidents if "customFields" in item]

        human_readable_output = tableToMarkdown(
            "Asimily Insight CVEs List", asimily_cve_list, headers=ASIMILY_CVE_CONTEXT_OUTPUT_KEY_ORDER
        )
        results = CommandResults(
            readable_output=f"{msg}\n\n{human_readable_output}",
            outputs_prefix="AsimilyInsight.CVE",
            outputs_key_field=["asimilydeviceid", "asimilycvename"],
            outputs=asimily_cve_list,
            raw_response=init_data,
            ignore_auto_extract=True,
        )
        return results

    # return incident list for incident creation
    return cves_incidents, last_device_id, total_elements


def get_asset_details_command(client: Client, args) -> CommandResults:
    if_fetch_applications = True  # For now we need to fetch assets with applications and query apps in separate call always
    init_data: List[Any] = []

    try:
        limit = int(args.get("limit", 0))
    except (ValueError, TypeError):
        limit = 0

    page_size_limit = MAX_FETCH_PAGE_LIMIT_ASSETS_WITH_APP if if_fetch_applications else MAX_FETCH_PAGE_LIMIT_ASSETS
    total_limit = MAX_FETCH_TOTAL_LIMIT_ASSETS_WITH_APP if if_fetch_applications else MAX_FETCH_TOTAL_LIMIT_ASSETS
    if limit > 0:
        page_size_limit = min(page_size_limit, limit)
        total_limit = min(limit, total_limit)

    total_elements, init_data = client.force_get_asset_details(
        args=args, page_size_limit=page_size_limit, total_limit=total_limit
    )

    incidents = []
    if init_data:
        counter = 0
        for raw_data in init_data:
            try:
                counter += 1
                print_debug_msg(f"Processing {counter} device details for incident creation.")
                entity: Dict[str, Any] = {}
                map_asimily_asset_entity_from_asimily_assets_json(client, entity, raw_data, client.get_base_url())
                incidents.append(entity["customFields"])
            except Exception as e:
                print_debug_msg(f"Failure during converting asset incident: {str(e)}")

    msg = (
        f"Fetched {len(incidents)} out of {total_elements} assets matching search. "
        f"Asset fetch count is limited to avoid server overload."
    )

    print_debug_msg(msg)

    asimily_asset_list = incidents
    human_readable_output = tableToMarkdown(
        "Asimily Insight Asset Details List", asimily_asset_list, headers=ASIMILY_ASSET_CONTEXT_OUTPUT_KEY_ORDER
    )
    results = CommandResults(
        readable_output=f"{msg}\n\n{human_readable_output}",
        outputs_prefix="AsimilyInsight.Asset",
        outputs_key_field="asimilydeviceid",
        outputs=asimily_asset_list,
        raw_response=init_data,
        ignore_auto_extract=True,
    )
    return results


def update_next_run(last_run: dict, updated: dict, flags: dict) -> dict:
    """
    LastRun Object key:
        last_type: "anomaly" or "cve". The last fetch-incident run's fetch entity type. Anomalies/CVEs are fetched in turns
        last_anomaly_deviceid: last asimily device id that we fetched the anomalies for.
        last_cve_deviceid: last asimily device id that we fetched the cves for.
        last_anom_syncstart: the last full sync of anomaly start time. Used as anomaliesLastUpdatedSince filter for fetch.
        last_cve_syncstart: the last full sync of cve start time. Used as the cvesLastUpdatedSince filter for fetch.
        cur_anom_syncstart: the current full sync of anomaly start time.
        cur_cve_syncstart: the current full sync of cve start time.
    """
    next_run = {"last_type": "anomaly" if flags.get("trigger_anomalies_fetch") else "cve"}
    if flags.get("last_anomaly_deviceid") is not None:
        next_run["last_anomaly_deviceid"] = str(flags["last_anomaly_deviceid"])
    if flags.get("last_cve_deviceid") is not None:
        next_run["last_cve_deviceid"] = str(flags["last_cve_deviceid"])
    if flags.get("last_anom_syncstart"):
        next_run["last_anom_syncstart"] = format_date(flags["last_anom_syncstart"])
    if flags.get("cur_anom_syncstart"):
        next_run["cur_anom_syncstart"] = format_date(flags["cur_anom_syncstart"])
    if flags.get("last_cve_syncstart"):
        next_run["last_cve_syncstart"] = format_date(flags["last_cve_syncstart"])
    if flags.get("cur_cve_syncstart"):
        next_run["cur_cve_syncstart"] = format_date(flags["cur_cve_syncstart"])

    return {**last_run, **next_run, **updated}


def should_trigger_anomaly(params: dict, last_type: str) -> bool:
    return bool(params.get("iffetchanomalies") and (not params.get("iffetchcves") or last_type != "anomaly"))


def should_trigger_cve(params: dict, last_type: str) -> bool:
    return bool(params.get("iffetchcves") and (not params.get("iffetchanomalies") or last_type == "anomaly"))


def fetch_asimily_incidents(client: Client, args, params):
    last_run = demisto.getLastRun() or {}
    last_type = last_run.get("last_type", "anomaly")
    print_debug_msg(f"Last run type: {last_type}")

    flags = {
        "last_anomaly_deviceid": int(last_run.get("last_anomaly_deviceid", 0)),
        "last_cve_deviceid": int(last_run.get("last_cve_deviceid", 0)),
        "last_anom_syncstart": last_run.get("last_anom_syncstart"),
        "last_cve_syncstart": last_run.get("last_cve_syncstart"),
        "cur_anom_syncstart": last_run.get("cur_anom_syncstart"),
        "cur_cve_syncstart": last_run.get("cur_cve_syncstart"),
    }
    print_debug_msg(f"Initial flags: {flags}")

    flags["trigger_anomalies_fetch"] = should_trigger_anomaly(params, last_type)
    flags["trigger_cves_fetch"] = should_trigger_cve(params, last_type)
    print_debug_msg(f"Trigger flags: Anomaly: {flags['trigger_anomalies_fetch']}, CVE: {flags['trigger_cves_fetch']}")

    updated_args = args.copy()
    updated_args.pop("lastRun", None)

    total_elements = 0
    incidents = []

    if flags["trigger_anomalies_fetch"]:
        updated_args["deviceRangeId"] = flags["last_anomaly_deviceid"]
        if flags["last_anom_syncstart"]:
            updated_args["anomaliesLastUpdatedSince"] = flags["last_anom_syncstart"]

        if flags["cur_anom_syncstart"] is None:  # first full sync
            flags["cur_anom_syncstart"] = datetime.utcnow()

        print_debug_msg(f"Fetching anomalies with args: {updated_args}")
        incidents, flags["last_anomaly_deviceid"], total_elements = get_asset_anomalies_command(client, updated_args, True)
        print_debug_msg(f"Fetched {len(incidents)} anomaly incidents for {total_elements} devices")
        if total_elements == 0:
            flags["last_anomaly_deviceid"] = 0
            flags["last_anom_syncstart"] = flags["cur_anom_syncstart"]
            flags["cur_anom_syncstart"] = datetime.utcnow()
    elif flags["trigger_cves_fetch"]:
        updated_args["deviceRangeId"] = flags["last_cve_deviceid"]
        if flags["last_cve_syncstart"]:
            updated_args["cvesLastUpdatedSince"] = flags["last_cve_syncstart"]

        if flags["cur_cve_syncstart"] is None:  # first full sync
            flags["cur_cve_syncstart"] = datetime.utcnow()

        print_debug_msg(f"Fetching CVEs with args: {updated_args}")
        incidents, flags["last_cve_deviceid"], total_elements = get_asset_cves_command(client, updated_args, True)
        print_debug_msg(f"Fetched {len(incidents)} CVE incidents for {total_elements} devices")
        if total_elements == 0:
            flags["last_cve_deviceid"] = 0
            flags["last_cve_syncstart"] = flags["cur_cve_syncstart"]
            flags["cur_cve_syncstart"] = datetime.utcnow()

    print_debug_msg(f"Pushing {len(incidents)} incidents to XSOAR")
    demisto.incidents(incidents)
    new_last_run = update_next_run(last_run, {}, flags)
    print_debug_msg(f"Setting new lastRun: {new_last_run}")
    demisto.setLastRun(new_last_run)


""" MAIN FUNCTION """


def main() -> None:
    params = demisto.params()
    args = demisto.args()
    command = demisto.command()

    print_params = copy.deepcopy(params)
    print_params.get("asimilycred", {}).pop("password", None)
    print_debug_msg(print_params)

    base_url = params.get("url")
    api_key = params["asimilycred"]["password"]
    user_name = params["asimilycred"]["identifier"]

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

    source = "PAN XSIAM" if is_xsiam() else "PAN XSOAR"

    processed_args = process_params_and_args(params, args, command)

    print_debug_msg(f"Command being called is {command}")
    try:
        client = Client(
            base_url=base_url,
            verify=verify_certificate,
            auth=(user_name, api_key),
            proxy=proxy,
            headers={"source": source},
        )

        if command == "test-module":
            result = test_module(client, params)
            return_results(result)

        elif command == "fetch-incidents":
            fetch_asimily_incidents(client, processed_args, params)

        elif command == "asimily-get-asset-details":
            return_results(get_asset_details_command(client, processed_args))

        elif command == "asimily-get-asset-anomalies":
            return_results(get_asset_anomalies_command(client, processed_args, False))

        elif command == "asimily-get-asset-vulnerabilities":
            return_results(get_asset_cves_command(client, processed_args, False))

        else:
            raise NotImplementedError(f"Command {command} is not implemented")

    # Log exceptions and return errors
    except Exception as e:
        return_error(f"Failed to execute {command} command.\nError:\n{str(e)}")


""" ENTRY POINT """

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