Claroty

Use the Claroty CTD integration to manage assets and alerts.

Network Security · Claroty

Details

IDClaroty
ProviderClaroty
CategoryNetwork Security
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Use the Claroty CTD integration to manage assets and alerts.
This integration was integrated and tested with version 4.0.1 of Claroty

Claroty Playbook

Playbook 1: OT Asset Discovery
Maintaining an accurate enterprise asset database is extremely difficult,
but without it effective security is near impossible. This playbook automates the population and maintenance of the
enterprise’s configuration management database (CMDB) with OT asset information.
The rich contextual data provided for each asset makes it realistic to prioritize security processes and actions
based on the CMDB.
Proactive vulnerability management is a fundamental control because it hardens assets against the most common exploits
seen in the wild. This playbook automates OT vulnerability management: it identifies high-severity vulnerabilities on
OT assets, and creates context-rich tickets in the enterprise service manager for action. Crucially, it focuses on
high-risk issues on truly important assets, so that non-critical issues do not overwhelm the vulnerability management
process and obfuscate the issues that demand immediate attention.

Playbook 3: OT Threat Detection Alerts
In order to scale, enterprises must centralize and automate the processing of alerts that are indicators of risk or
compromise. This playbook automates the passing of OT threat detection alerts from the Claroty CTD system to the
enterprise SIEM and ticketing system. CTD correlates the alert with asset and flow information observed in the OT
environment and passes it upstream via the integration, allowing security analysts to quickly evaluate the alert and
take corrective action if necessary.

Use Cases

Retrieve and resolve alerts related to OT devices (communicating outside of the network,
policy violations such as active outside working hours, etc.)

Insights: get information about vulnerable assets in the network (retrieve vulnerabilities and CVEs per asset,
divide according to CVE risk and mitigate accordingly (high will have different mitigation steps)

Configure Claroty on Cortex XSOAR


  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Claroty.
  3. Click Add instance to create and configure a new integration instance.
    • Name: a textual name for the integration instance.
    • CTD Server URL (e.g. https://<IP>:5000)
    • Username
    • Trust any certificate (not secure)
    • Use system proxy settings
    • Incident type
    • Fetch incidents
    • The initial time to fetch from
    • Minimal severity to fetch by
    • Site ID to fetch by
    • Fetch by alert type
    • Exclude resolved alerts
    • Include only ARR completed alerts
  4. Click Test to validate the URLs, token, and connection.

Fetched Incidents Data


Commands


You can execute these commands from the Cortex XSOAR 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.

  1. claroty-get-assets
  2. claroty-query-alerts
  3. claroty-resolve-alert
  4. claroty-get-single-alert

1. claroty-get-assets


Gets all assets from CTD. You can apply one or more filters.

Required Permissions

Admin user.

Base Command

claroty-get-assets

Input
Argument Name Description Required
fields Asset fields to return. The default value is “all”. Optional
criticality Returns assets with this criticality. Can be “Low”, “Medium”, or “High”. Optional
insight_name Get assets with that include the given insight name Optional
should_enrich_assets Add aditional value for the asset CVEs. Optional
asset_limit Maximal value of assets to query at once. Optional
assets_last_seen Get all assets seen last from the given date. Format - YYYY-MM-DDThh:mm:ssZ. Example - 2020-02-02T01:02:03Z Optional
Context Output
Path Type Description
Claroty.Asset.AssetID Number The ID of the asset.
Claroty.Asset.AssetType String The asset type.
Claroty.Asset.CVE.CVSS String CVE Score.
Claroty.Asset.CVE.Description String CVE Description.
Claroty.Asset.CVE.ID String CVE ID.
Claroty.Asset.CVE.Modified Date CVE modification date.
Claroty.Asset.CVE.Published Date CVE publish date.
CVE.CVSS String CVE Score.
CVE.Description String CVE Description.
CVE.ID String CVE ID.
CVE.Modified Date CVE modification date.
CVE.Published Date CVE publish date.
Claroty.Asset.ClassType String The OT/IT class type.
Claroty.Asset.Criticality String The criticality of the asset, according to the Purdue model.
Claroty.Asset.FirmwareVersion String The FM version of the asset.
Claroty.Asset.HighestCVEScore Number Highest CVE Score for the Asset.
Claroty.Asset.IP String The IPv4 address of the asset.
Claroty.Asset.InsightName String The asset insight names generated by CTD.
Claroty.Asset.LastSeen Date The date the asset was last seen.
Claroty.Asset.MAC String The MAC address of the asset.
Claroty.Asset.Name String The asset name.
Claroty.Asset.ResourceID String The asset RID (AssetID-SiteID).
Claroty.Asset.RiskLevel Number The risk indicator.
Claroty.Asset.SiteID Number The site ID of the asset.
Claroty.Asset.SiteName String The site name of the asset.
Claroty.Asset.Vendor String The vendor of the asset.
Claroty.Asset.VirtualZone String The virtual zone of the asset.
Claroty.Asset.WasParsed String Whether the project was parsed.
Command Example

!claroty-get-assets asset_limit=1 criticality=High should_enrich_assets=True

Context Example
{
    "CVE": [
        [
            {
                "ID": "RA-470154-1", 
                "Published": "2012-01-19", 
                "CVSS": "8.8", 
                "Modified": "2018-01-11", 
                "Description": "Denial of Service by receiving valid CIP message"
            }, 
            {
                "ID": "RA-470154-3", 
                "Published": "2012-01-19", 
                "CVSS": "8.8", 
                "Modified": "2018-01-11", 
                "Description": "Denial of Service (reset the product) by receiving valid CIP message"
            }, 
            {
                "ID": "RA-470155-1", 
                "Published": "2012-01-19", 
                "CVSS": "8.8", 
                "Modified": "2018-01-11", 
                "Description": "Denial of Service by receiving malformed CIP packet"
            }
        ]
    ], 
    "Claroty.Asset": [
        {
            "ResourceID": "9-1", 
            "AssetType": "PLC", 
            "Vendor": "Rockwell Automation", 
            "Name": "10.1.0.10", 
            "Criticality": "High", 
            "AssetID": 9, 
            "ClassType": "OT", 
            "SiteName": "site-1", 
            "InsightName": [
                "Full Match CVEs", 
                "Open Ports"
            ], 
            "VirtualZone": "PLC: Rockwell", 
            "RiskLevel": 1, 
            "MAC": [
                "E4:90:69:A7:70:0F"
            ], 
            "SiteID": 1, 
            "HighestCVEScore": 8.8, 
            "IP": [
                "10.1.0.10"
            ], 
            "WasParsed": null, 
            "CVE": [
                {
                    "ID": "RA-470154-1", 
                    "Published": "2012-01-19", 
                    "CVSS": "8.8", 
                    "Modified": "2018-01-11", 
                    "Description": "Denial of Service by receiving valid CIP message"
                }, 
                {
                    "ID": "RA-470154-3", 
                    "Published": "2012-01-19", 
                    "CVSS": "8.8", 
                    "Modified": "2018-01-11", 
                    "Description": "Denial of Service (reset the product) by receiving valid CIP message"
                }, 
                {
                    "ID": "RA-470155-1", 
                    "Published": "2012-01-19", 
                    "CVSS": "8.8", 
                    "Modified": "2018-01-11", 
                    "Description": "Denial of Service by receiving malformed CIP packet"
                }
            ], 
            "FirmwareVersion": "V4.003", 
            "LastSeen": "2020-02-19T07:42:16+00:00"
        }
    ]
}
Human Readable Output

Claroty Asset List

AssetID AssetType CVE ClassType Criticality FirmwareVersion HighestCVEScore IP InsightName LastSeen MAC Name ResourceID RiskLevel SiteID SiteName Vendor VirtualZone WasParsed
9 PLC {‘ID’: ‘RA-470154-1’, ‘CVSS’: ‘8.8’, ‘Published’: ‘2012-01-19’, ‘Modified’: ‘2018-01-11’, ‘Description’: ‘Denial of Service by receiving valid CIP message’},
{‘ID’: ‘RA-470154-3’, ‘CVSS’: ‘8.8’, ‘Published’: ‘2012-01-19’, ‘Modified’: ‘2018-01-11’, ‘Description’: ‘Denial of Service (reset the product) by receiving valid CIP message’},
{‘ID’: ‘RA-470155-1’, ‘CVSS’: ‘8.8’, ‘Published’: ‘2012-01-19’, ‘Modified’: ‘2018-01-11’, ‘Description’: ‘Denial of Service by receiving malformed CIP packet’}
OT High V4.003 8.8 10.1.0.10 Full Match CVEs,
Open Ports
2020-02-19T07:42:16+00:00 E4:90:69:A7:70:0F 10.1.0.10 9-1 1 1 site-1 Rockwell Automation PLC: Rockwell  

2. claroty-query-alerts


Gets alerts from CTD.

Required Permissions

Admin user.

Base Command

claroty-query-alerts

Input
Argument Name Description Required
fields Alert fields to return. Optional
sort_by The field by which to sort the results. The default value is “timestamp”.
Default sort order is ascending
Optional
type Returns alerts that match this alert type. Optional
date_from The start date from which to get alerts. Format - YYYY-MM-DDThh:mm:ssZ. Example - 2020-02-02T01:02:03Z Optional
sort_order The sorting order of the alerts - descending or ascending Optional
alert_limit The maximum number of alerts to query. Optional
minimal_severity Set minimal severity to query by. Optional
exclude_resolved_alerts Returns only unresloved alerts. Optional
include_only_arr_completed_alerts Includes only alerts that have completed their initial Automated Resolution Rules flow. Default is False. Optional
Context Output
Path Type Description
Claroty.Alert.AlertType String The alert type.
Claroty.Alert.AlertTypeID Number The alert type int value
Claroty.Alert.Description String The alert description.
Claroty.Alert.Indicator String The alert indicators.
Claroty.Alert.NetworkID Number The network ID.
Claroty.Alert.RelatedAssets String Assets related to the alert.
Claroty.Alert.RelatedAssets.AssetID Number The ID of the asset.
Claroty.Alert.RelatedAssets.AssetType String The asset type.
Claroty.Alert.RelatedAssets.ClassType String The OT/IT class type.
Claroty.Alert.RelatedAssets.Criticality String The criticality of the asset, according to the Purdue model.
Claroty.Alert.RelatedAssets.FirmwareVersion String The FM version of the asset.
Claroty.Alert.RelatedAssets.IP String The IPv4 address of the asset.
Claroty.Alert.RelatedAssets.InsightName String The asset insight names generated by CTD.
Claroty.Alert.RelatedAssets.LastSeen Date The date the asset was last seen.
Claroty.Alert.RelatedAssets.MAC String The MAC address of the asset.
Claroty.Alert.RelatedAssets.Name String The asset name.
Claroty.Alert.RelatedAssets.ResourceID String The asset RID (AssetID-SiteID).
Claroty.Alert.RelatedAssets.RiskLevel Number The risk indicator.
Claroty.Alert.RelatedAssets.SiteID Number The site ID of the asset.
Claroty.Alert.RelatedAssets.SiteName String The site name of the asset.
Claroty.Alert.RelatedAssets.Vendor String The vendor of the asset.
Claroty.Alert.RelatedAssets.VirtualZone String The virtual zone of the asset.
Claroty.Alert.RelatedAssets.WasParsed String Whether the project was parsed.
Claroty.Alert.Resolved Number The resolve status of the alert.
Claroty.Alert.ResourceID String The alert resource ID (AlertID-SiteID).
Claroty.Alert.Severity String The alert severity.
Claroty.Alert.Category String The alert category.
Claroty.Alert.InitialArrFlowCompleted Number Whether the alert has completed its initial Automated Resolution Rules flow.
Command Example

!claroty-query-alerts alert_limit=1 type=`Known Threat Alert`

Context Example
{
    "Claroty.Alert": [
        {
            "Category": "Security", 
            "NetworkID": 1, 
            "Indicator": "Alert ID - 14\r\nDescription - Event occurred out of working hours\r\nPoints - 10\r\n\nAlert ID - 14\r\nDescription - First time over the past 30 days, this Threat Signature is seen in the network\r\nPoints - 100\r\n\n", 
            "AlertType": "KnownThreatAlert", 
            "Description": "Known Threat: Threat ET TROJAN Conficker.b Shellcode was detected from 192.168.0.121 to 192.168.0.100", 
            "ResourceID": "14-1", 
            "AlertTypeID": 23, 
            "RelatedAssets": [
                {
                    "ResourceID": "15-1", 
                    "AssetType": "Endpoint", 
                    "Vendor": "Advantech Technology", 
                    "Name": "GTWB", 
                    "Criticality": null, 
                    "AssetID": 15, 
                    "IP": null, 
                    "SiteName": null, 
                    "VirtualZone": null, 
                    "RiskLevel": null, 
                    "MAC": [
                        "00:0B:AB:1A:DD:DD"
                    ], 
                    "SiteID": 1, 
                    "InsightName": null, 
                    "ClassType": null, 
                    "WasParsed": null, 
                    "FirmwareVersion": null, 
                    "LastSeen": null
                }, 
                {
                    "ResourceID": "33-1", 
                    "AssetType": "Endpoint", 
                    "Vendor": "Advantech Technology", 
                    "Name": "OISERVM", 
                    "Criticality": null, 
                    "AssetID": 33, 
                    "IP": null, 
                    "SiteName": null, 
                    "VirtualZone": null, 
                    "RiskLevel": null, 
                    "MAC": [
                        "00:0B:AB:1A:DE:BE", 
                        "00:0B:AB:1A:DE:BF"
                    ], 
                    "SiteID": 1, 
                    "InsightName": null, 
                    "ClassType": null, 
                    "WasParsed": null, 
                    "FirmwareVersion": null, 
                    "LastSeen": null
                }, 
                {
                    "ResourceID": "19-1", 
                    "AssetType": "Endpoint", 
                    "Vendor": "Advantech Technology", 
                    "Name": "GTWA", 
                    "Criticality": null, 
                    "AssetID": 19, 
                    "IP": null, 
                    "SiteName": null, 
                    "VirtualZone": null, 
                    "RiskLevel": null, 
                    "MAC": [
                        "00:0B:AB:1A:DD:F8"
                    ], 
                    "SiteID": 1, 
                    "InsightName": null, 
                    "ClassType": null, 
                    "WasParsed": null, 
                    "FirmwareVersion": null, 
                    "LastSeen": null
                }, 
                {
                    "ResourceID": "18-1", 
                    "AssetType": "Endpoint", 
                    "Vendor": "Advantech Technology", 
                    "Name": "DRWSTN", 
                    "Criticality": null, 
                    "AssetID": 18, 
                    "IP": null, 
                    "SiteName": null, 
                    "VirtualZone": null, 
                    "RiskLevel": null, 
                    "MAC": [
                        "00:0B:AB:1A:DE:BC"
                    ], 
                    "SiteID": 1, 
                    "InsightName": null, 
                    "ClassType": null, 
                    "WasParsed": null, 
                    "FirmwareVersion": null, 
                    "LastSeen": null
                }, 
                {
                    "ResourceID": "17-1", 
                    "AssetType": "Endpoint", 
                    "Vendor": "Dell", 
                    "Name": "OISERVR", 
                    "Criticality": null, 
                    "AssetID": 17, 
                    "IP": null, 
                    "SiteName": null, 
                    "VirtualZone": null, 
                    "RiskLevel": null, 
                    "MAC": [
                        "F0:4D:A2:EF:FF:11"
                    ], 
                    "SiteID": 1, 
                    "InsightName": null, 
                    "ClassType": null, 
                    "WasParsed": null, 
                    "FirmwareVersion": null, 
                    "LastSeen": null
                }
            ], 
            "Resolved": true, 
            "Severity": "Critical"
        }
    ]
}
Human Readable Output

Claroty Alert List

AlertType AlertTypeID Category Description Indicator NetworkID RelatedAssets Resolved ResourceID Severity
KnownThreatAlert 23 Security Known Threat: Threat ET TROJAN Conficker.b Shellcode was detected from 192.168.0.121 to 192.168.0.100 Alert ID - 14
Description - Event occurred out of working hours
Points - 10

Alert ID - 14
Description - First time over the past 30 days, this Threat Signature is seen in the network
Points - 100

1 {‘AssetID’: 15, ‘Name’: ‘GTWB’, ‘InsightName’: None, ‘Vendor’: ‘Advantech Technology’, ‘Criticality’: None, ‘AssetType’: ‘Endpoint’, ‘LastSeen’: None, ‘IP’: None, ‘MAC’: [‘00:0B:AB:1A:DD:DD’], ‘VirtualZone’: None, ‘ClassType’: None, ‘SiteName’: None, ‘SiteID’: 1, ‘WasParsed’: None, ‘RiskLevel’: None, ‘FirmwareVersion’: None, ‘ResourceID’: ‘15-1’},
{‘AssetID’: 33, ‘Name’: ‘OISERVM’, ‘InsightName’: None, ‘Vendor’: ‘Advantech Technology’, ‘Criticality’: None, ‘AssetType’: ‘Endpoint’, ‘LastSeen’: None, ‘IP’: None, ‘MAC’: [‘00:0B:AB:1A:DE:BE’, ‘00:0B:AB:1A:DE:BF’], ‘VirtualZone’: None, ‘ClassType’: None, ‘SiteName’: None, ‘SiteID’: 1, ‘WasParsed’: None, ‘RiskLevel’: None, ‘FirmwareVersion’: None, ‘ResourceID’: ‘33-1’},
{‘AssetID’: 19, ‘Name’: ‘GTWA’, ‘InsightName’: None, ‘Vendor’: ‘Advantech Technology’, ‘Criticality’: None, ‘AssetType’: ‘Endpoint’, ‘LastSeen’: None, ‘IP’: None, ‘MAC’: [‘00:0B:AB:1A:DD:F8’], ‘VirtualZone’: None, ‘ClassType’: None, ‘SiteName’: None, ‘SiteID’: 1, ‘WasParsed’: None, ‘RiskLevel’: None, ‘FirmwareVersion’: None, ‘ResourceID’: ‘19-1’},
{‘AssetID’: 18, ‘Name’: ‘DRWSTN’, ‘InsightName’: None, ‘Vendor’: ‘Advantech Technology’, ‘Criticality’: None, ‘AssetType’: ‘Endpoint’, ‘LastSeen’: None, ‘IP’: None, ‘MAC’: [‘00:0B:AB:1A:DE:BC’], ‘VirtualZone’: None, ‘ClassType’: None, ‘SiteName’: None, ‘SiteID’: 1, ‘WasParsed’: None, ‘RiskLevel’: None, ‘FirmwareVersion’: None, ‘ResourceID’: ‘18-1’},
{‘AssetID’: 17, ‘Name’: ‘OISERVR’, ‘InsightName’: None, ‘Vendor’: ‘Dell’, ‘Criticality’: None, ‘AssetType’: ‘Endpoint’, ‘LastSeen’: None, ‘IP’: None, ‘MAC’: [‘F0:4D:A2:EF:FF:11’], ‘VirtualZone’: None, ‘ClassType’: None, ‘SiteName’: None, ‘SiteID’: 1, ‘WasParsed’: None, ‘RiskLevel’: None, ‘FirmwareVersion’: None, ‘ResourceID’: ‘17-1’}
true 14-1 Critical

3. claroty-resolve-alert


Resolves alerts.

Required Permissions

Admin user.

Base Command

claroty-resolve-alert

Input
Argument Name Description Required
selected_alerts The ResourceId of the Alerts to resolve (in - format) Required
resolve_as How to resolve the alert. Can be “archive” or “resolve”. The default value is “resolve”. Optional
resolve_comment A comment to add when resolving an alert. Optional
Context Output
Path Type Description
Claroty.Resolve_out.success String Success output of alert resolving.
Command Example

!claroty-resolve-alert selected_alerts="75-1" resolve_as=archive resolve_comment="Claroty is much wow!"

Context Example
{
    "Claroty.Resolve_out": {
        "success": true
    }
}
Human Readable Output

Alert was resolved successfully

4. claroty-get-single-alert


Get a single alert from CTD.

Required Permissions

Admin user.

Base Command

claroty-get-single-alert

Input
Argument Name Description Required
fields Asset fields to return. The default value is “all”. Optional
alert_rid Resource ID of the desired alert. Expected value - - Required
Context Output
Path Type Description
Claroty.Alert.AlertType String The alert type.
Claroty.Alert.AlertTypeID Number The alert type int value
Claroty.Alert.Description String The alert description.
Claroty.Alert.Indicator String The alert indicators.
Claroty.Alert.NetworkID Number The network ID.
Claroty.Alert.RelatedAssets String Assets related to the alert.
Claroty.Alert.Resolved Number The resolve status of the alert.
Claroty.Alert.ResourceID String The alert resource ID (AlertID-SiteID).
Claroty.Alert.Severity String The alert severity.
Command Example

!claroty-get-single-alert alert_rid="75-1"

Context Example
{
    "Claroty.Alert": {
        "Category": "Integrity", 
        "NetworkID": 1, 
        "Indicator": "Alert ID - 75\r\nDescription - This Event does not currently support Alert Indicators\r\nPoints - 100\r\n\n", 
        "AlertType": "PortScan", 
        "Description": "UDP Port scan: Asset 192.168.1.10 sent probe packets to 192.168.1.25 IP address on different ports", 
        "ResourceID": "75-1", 
        "AlertTypeID": 28, 
        "RelatedAssets": [
            {
                "ResourceID": "47-1", 
                "AssetType": "Endpoint", 
                "Vendor": "Hewlett Packard", 
                "Name": "192.168.1.10", 
                "Criticality": "Low", 
                "AssetID": 47, 
                "IP": null, 
                "SiteName": "site-1", 
                "VirtualZone": "Endpoint: Other", 
                "RiskLevel": 0, 
                "MAC": [
                    "00:1A:4B:6A:CE:FE"
                ], 
                "SiteID": 1, 
                "InsightName": null, 
                "ClassType": "IT", 
                "WasParsed": null, 
                "FirmwareVersion": null, 
                "LastSeen": null
            }, 
            {
                "ResourceID": "48-1", 
                "AssetType": "Endpoint", 
                "Vendor": "VMware", 
                "Name": "192.168.1.25", 
                "Criticality": "Low", 
                "AssetID": 48, 
                "IP": null, 
                "SiteName": "site-1", 
                "VirtualZone": "Endpoint: Other", 
                "RiskLevel": 0, 
                "MAC": [
                    "00:0C:29:86:C8:36"
                ], 
                "SiteID": 1, 
                "InsightName": null, 
                "ClassType": "IT", 
                "WasParsed": null, 
                "FirmwareVersion": null, 
                "LastSeen": null
            }
        ], 
        "Resolved": false, 
        "Severity": "Critical"
    }
}
Human Readable Output

Claroty Alert List

AlertType AlertTypeID Category Description Indicator NetworkID RelatedAssets Resolved ResourceID Severity
PortScan 28 Integrity UDP Port scan: Asset 192.168.1.10 sent probe packets to 192.168.1.25 IP address on different ports Alert ID - 75
Description - This Event does not currently support Alert Indicators
Points - 100

1 {‘AssetID’: 47, ‘Name’: ‘192.168.1.10’, ‘InsightName’: None, ‘Vendor’: ‘Hewlett Packard’, ‘Criticality’: ‘Low’, ‘AssetType’: ‘Endpoint’, ‘LastSeen’: None, ‘IP’: None, ‘MAC’: [‘00:1A:4B:6A:CE:FE’], ‘VirtualZone’: ‘Endpoint: Other’, ‘ClassType’: ‘IT’, ‘SiteName’: ‘site-1’, ‘SiteID’: 1, ‘WasParsed’: None, ‘RiskLevel’: 0, ‘FirmwareVersion’: None, ‘ResourceID’: ‘47-1’},
{‘AssetID’: 48, ‘Name’: ‘192.168.1.25’, ‘InsightName’: None, ‘Vendor’: ‘VMware’, ‘Criticality’: ‘Low’, ‘AssetType’: ‘Endpoint’, ‘LastSeen’: None, ‘IP’: None, ‘MAC’: [‘00:0C:29:86:C8:36’], ‘VirtualZone’: ‘Endpoint: Other’, ‘ClassType’: ‘IT’, ‘SiteName’: ‘site-1’, ‘SiteID’: 1, ‘WasParsed’: None, ‘RiskLevel’: 0, ‘FirmwareVersion’: None, ‘ResourceID’: ‘48-1’}
false 75-1 Critical

Configuration parameters

  • url — CTD Server URL (e.g. https://<IP>:5000) (required)
  • credentials — Username (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • isFetch — Fetch incidents
  • fetch_time — The initial time to fetch from
  • max_fetch — Max per page
  • severity — Minimal severity to fetch by
  • site_id — Site ID to fetch by
  • alert_type — Fetch by alert type
  • exclude_resolved_alerts — Exclude resolved alerts
  • include_only_arr_completed_alerts — Include only ARR completed alerts

Commands (4)

  • claroty-get-assets

    Gets all assets from CTD. You can apply one or more filters.

  • claroty-get-single-alert

    Get a single alert from CTD.

  • claroty-query-alerts

    Gets alerts from CTD.

  • claroty-resolve-alert

    Resolves alerts.

import demistomock as demisto
from CommonServerPython import *

from CommonServerUserPython import *

""" IMPORTS """
import json

from typing import Any

import dateparser
import requests


class Filter:
    def __init__(self, filter: str, value: Any, lookup: str = "exact"):
        self.filter = filter
        self.value = value
        self.lookup = lookup

    def build_filter(self):
        return f"{self.filter}__{self.lookup}={self.value}"


""" CONSTANTS """
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
DEFAULT_HEADERS = {"content-type": "application/json"}
CTD_TO_DEMISTO_SEVERITY = {"Low": 1, "Medium": 2, "High": 3, "Critical": 4}
ALERT_CTD_FIELD_TO_DEMISTO_FIELD = {
    "resource_id": "ResourceID",
    "type": "AlertTypeID",
    "type__": "AlertType",
    "severity__": "Severity",
    "network_id": "NetworkID",
    "resolved": "Resolved",
    "description": "Description",
    "actionable_assets": "RelatedAssets",
    "alert_indicators": "Indicator",
    "category__": "Category",
    "timestamp": "Timestamp",
    "initial_arr_flow_completed": "InitialArrFlowCompleted",
}
ASSET_CTD_FIELD_TO_DEMISTO_FIELD = {
    "id": "AssetID",
    "name": "Name",
    "insight_names": "InsightName",
    "vendor": "Vendor",
    "criticality__": "Criticality",
    "asset_type__": "AssetType",
    "last_seen": "LastSeen",
    "ipv4": "IP",
    "mac": "MAC",
    "virtual_zone_name": "VirtualZone",
    "class_type": "ClassType",
    "site_name": "SiteName",
    "project_parsed": "WasParsed",
    "risk_level": "RiskLevel",
    "firmware": "FirmwareVersion",
    "resource_id": "ResourceID",
    "site_id": "SiteID",
    "insights": "Insights",
}
RESOLVE_STRING_TO_TYPE = {"resolve": 1, "archive": 2}
DEFAULT_ALERT_FIELD_LIST = [
    "resource_id",
    "type",
    "severity",
    "network_id",
    "resolved",
    "description",
    "alert_indicators",
    "actionable_assets",
    "category",
    "initial_arr_flow_completed",
]
DEFAULT_ASSET_FIELD_LIST = [
    "id",
    "name",
    "insight_names",
    "vendor",
    "criticality",
    "asset_type",
    "last_seen",
    "ipv4",
    "mac",
    "virtual_zone_name",
    "class_type",
    "site_name",
    "site_id",
    "project_parsed",
    "risk_level",
    "firmware",
    "resource_id",
]
WINDOWS_CVE_BASE_URL = (
    "ranger/insight_details/Windows%20CVEs?&format=asset_page&sort=-Score%20(CVSS)&per_page=1000&page=1&id__exact="
)
FULL_MATCH_BASE_URL = (
    "ranger/insight_details/Full%20Match%20CVEs?&format=asset_page&sort=-Score%20(CVSS)&per_page=1000&page=1&id__exact="
)
DEFAULT_RESOLVE_ALERT_COMMENT = "Resolved by Demisto"
# MAX_ASSET_LIMIT = 75
# MAX_ALERT_LIMIT = 75
MAX_PER_PAGE = 200
DEFAULT_PER_PAGE = 10
DEFAULT_ALERTS_FILTERS = [Filter("is_qualified", "true", "exact")]
DEFAUL_ASSETS_FILTERS = [Filter("valid", "true", "exact"), Filter("approved", "true", "exact"), Filter("ghost", "false", "exact")]


class Client(BaseClient):
    def __init__(self, **kwargs):
        self._credentials = kwargs.pop("credentials", (None, None))
        super().__init__(**kwargs)
        self._headers = DEFAULT_HEADERS
        self._generate_token()
        self._list_to_filters: dict = {"alerts": [], "assets": []}

    def _request_with_token(self, url_suffix: str, method: str = "GET", data=None):
        try:
            return self._http_request(method, url_suffix=url_suffix, data=data)
        except DemistoException:
            demisto.setIntegrationContext({"jwt_token": None})
            self._headers.pop("Authorization", None)
            # assuming it was just the token that expired, retrying to send the request with the new token
            self._generate_token()

            return self._http_request(method, url_suffix=url_suffix, data=data)

    def _generate_token(self):
        if not demisto.getIntegrationContext().get("jwt_token"):
            res = self._http_request(
                "POST",
                url_suffix="auth/authenticate",
                data=json.dumps({"username": self._credentials[0], "password": self._credentials[1]}),
            )

            if res.get("password_expired", None):
                raise DemistoException("Password expired, please update credentials")

            demisto.setIntegrationContext({"jwt_token": res["token"]})
            self._headers["Authorization"] = demisto.getIntegrationContext()["jwt_token"]
            return self._headers["Authorization"]
        else:
            return demisto.getIntegrationContext()

    def list_incidents(self, fields: list, sort_by: dict, fetch_from_date: str, page_number: int, **extra_filters) -> dict:
        extra_filters_list = [Filter("timestamp", fetch_from_date, "gte")]
        extra_filters_list += DEFAULT_ALERTS_FILTERS

        for extra_filter in extra_filters:
            if extra_filter == "severity":
                extra_filters_list.append(Filter(extra_filter, extra_filters[extra_filter], "gte"))
            else:
                extra_filters_list.append(Filter(extra_filter, extra_filters[extra_filter]))

        if bool(demisto.params().get("exclude_resolved_alerts", False)):
            extra_filters_list = _add_exclude_resolved_alerts_filters(extra_filters_list)

        if argToBoolean(demisto.params().get("include_only_arr_completed_alerts", False)):
            extra_filters_list = _add_include_only_arr_completed_alerts_filters(extra_filters_list)

        return self.get_alerts(fields=fields, sort_by=sort_by, filters=extra_filters_list, page_number=page_number)

    def get_assets(self, fields: list, sort_by: dict, filters: list, limit: int = 10):
        url_suffix = self._add_extra_params_to_url("ranger/assets", fields, sort_by, filters, limit)
        return self._request_with_token(url_suffix, "GET")

    def get_alerts(self, fields: list, sort_by: dict, filters: list, limit: int = 10, page_number: int = 1):
        url_suffix = self._add_extra_params_to_url("ranger/alerts", fields, sort_by, filters, limit, page_number)
        return self._request_with_token(url_suffix, "GET")

    def get_alert(self, rid: str) -> dict | str | requests.Response:
        return self._request_with_token(f"ranger/alerts/{rid}", "GET")

    def get_ranger_table_filters(self, table: str) -> dict:
        if not self._list_to_filters[table]:
            self._list_to_filters[table] = self._request_with_token(f"ranger/{table}/filters", "GET")["filters"]
        return self._list_to_filters[table]

    def resolve_alert(self, selected_alerts: list, filters: dict, resolve_type: int, resolve_comment: str):
        return self._request_with_token(
            "ranger/ranger_api/resolve_alerts",
            "POST",
            data=json.dumps(
                {
                    "selection_params": {"select_all": False, "selected": selected_alerts, "excluded": [], "filters": filters},
                    "resolved_as": resolve_type,
                    "comment": resolve_comment,
                }
            ),
        )

    @staticmethod
    def _add_extra_params_to_url(
        url_suffix: str, fields: list, sort_by: dict, filters: list[Filter], limit: int = 10, page_number: int = 1
    ) -> str:
        url_suffix += "?fields=" + ",;$".join(fields)
        url_suffix += f"&page={page_number}&per_page={limit}"

        if sort_by:
            url_suffix += f"&sort={sort_by['order']}{sort_by['field']}"

        for query_filter in filters:
            url_suffix += f"&{query_filter.build_filter()}"
        return url_suffix

    def enrich_asset_results(self, assets: dict) -> dict:
        for asset in assets["objects"]:
            full_match_cves = self._request_with_token(f"{FULL_MATCH_BASE_URL}{asset['resource_id']}", "GET")
            windows_cves = self._request_with_token(f"{WINDOWS_CVE_BASE_URL}{asset['resource_id']}", "GET")
            assets_cves = [*full_match_cves["rows"], *windows_cves["rows"]]
            asset["insights"] = [
                {
                    "CVE-ID": cve["cells"][0],
                    "Score": cve["cells"][1],
                    "Description": cve["cells"][2],
                    "Published": cve["cells"][3],
                    "Modified": cve["cells"][4],
                }
                for cve in assets_cves
            ]
        return assets


def test_module(client: Client):
    authentication_result = client._generate_token()
    if not authentication_result.get("jwt_token", False):
        return f"Token getter failed, adding result - {authentication_result}"

    query_alerts_result = client.get_alerts(DEFAULT_ALERT_FIELD_LIST, get_sort("timestamp"), [], limit=1)
    if query_alerts_result.get("count_total", "Failed") == "Failed":
        return f"Failed getting alerts, json result - {query_alerts_result}"

    return "ok"


def get_assets_command(client: Client, args: dict) -> tuple:
    relevant_fields, sort_by, limit = _init_request_values("asset", "id", "asset_limit", args)
    filters = []

    filters += DEFAUL_ASSETS_FILTERS

    criticality_str = args.get("criticality", None)
    criticality_int = CTD_TO_DEMISTO_SEVERITY.get(criticality_str, None)
    if criticality_int:
        filters.append(Filter("criticality", criticality_int - 1))

    insight_name = args.get("insight_name", None)
    if insight_name:
        filters.extend([Filter("insight_name", insight_name), Filter("insight_status", 0)])

    assets_last_seen = args.get("assets_last_seen", None)
    if assets_last_seen:
        filters.append(Filter("last_seen", assets_last_seen, "gte"))

    site_id = demisto.params().get("site_id", None)
    if site_id:
        filters.append(Filter("site_id", site_id, "exact"))

    result = client.get_assets(relevant_fields, sort_by, filters, limit)

    should_enrich_assets = argToBoolean(args.get("should_enrich_assets", "False"))
    if should_enrich_assets:
        result = client.enrich_asset_results(result)
        relevant_fields.append("insights")

    parsed_results_assets, parsed_cves = _parse_assets_result(result, relevant_fields)
    outputs = {"Claroty.Asset(val.AssetID == obj.AssetID)": parsed_results_assets}

    if parsed_cves and len(parsed_cves) > 0:
        outputs["CVE(val.ID == obj.ID)"] = parsed_cves

    readable_output = tableToMarkdown("Claroty Asset List", parsed_results_assets)
    return (readable_output, outputs, result)


def resolve_alert_command(client: Client, args: dict) -> tuple:
    bad_input = False
    selected_alerts_arg = args.get("selected_alerts", [])
    selected_alert_list = selected_alerts_arg.split(",") if isinstance(selected_alerts_arg, str) else selected_alerts_arg
    for alert in selected_alert_list:
        split_alert = alert.split("-")
        if len(split_alert) != 2 or not split_alert[0].isnumeric() or not split_alert[1].isnumeric():
            bad_input = True

    resolve_type = RESOLVE_STRING_TO_TYPE[args.get("resolve_as", "resolve")]

    resolve_comment = args.get("resolve_comment", DEFAULT_RESOLVE_ALERT_COMMENT)

    if not bad_input:
        result = client.resolve_alert(selected_alert_list, args.get("filters", {}), resolve_type, resolve_comment)

        outputs = {"Claroty.Resolve_out": result}
        if result["success"]:
            readable_output = "## Alert was resolved successfully"
        else:
            readable_output = "## Alert was not resolved"
    else:
        result = {}
        outputs = {}
        readable_output = "## Bad input"

    return (readable_output, outputs, result)


def get_single_alert_command(client: Client, args: dict) -> tuple:
    relevant_fields = get_fields("alert", args.get("fields", "").split(","))
    alert_rid = args.get("alert_rid", None)
    result = client.get_alert(alert_rid)
    parsed_results = _parse_single_alert(result, relevant_fields)

    outputs = {"Claroty.Alert(val.ResourceID == obj.ResourceID)": parsed_results}
    readable_output = tableToMarkdown("Claroty Alert List", parsed_results)
    return (readable_output, outputs, result)


def query_alerts_command(client: Client, args: dict) -> tuple:
    relevant_fields, sort_by, limit = _init_request_values("alert", "timestamp", "alert_limit", args, True)
    filters = []

    filters += DEFAULT_ALERTS_FILTERS
    alert_type = args.get("type", "").lower().replace(" ", "")
    alert_type_exists = False
    if alert_type:
        alert_filters = client.get_ranger_table_filters("alerts")
        filters_url_suffix = transform_filters_labels_to_values(alert_filters, "type", alert_type)
        if filters_url_suffix:
            for filter_type in filters_url_suffix:
                filters.append(Filter(filter_type[0], filter_type[1]))
                alert_type_exists = True

    alert_time = args.get("date_from", None)
    if alert_time:
        filters.append(Filter("timestamp", alert_time, "gte"))

    alert_severity = args.get("minimal_severity", None)
    if alert_severity:
        filters.append(Filter("severity", get_severity_filter(alert_severity), "gte"))

    if argToBoolean(args.get("exclude_resolved_alerts", "False")):
        filters = _add_exclude_resolved_alerts_filters(filters)

    if argToBoolean(args.get("include_only_arr_completed_alerts", "False")):
        filters = _add_include_only_arr_completed_alerts_filters(filters)

    site_id = demisto.params().get("site_id", None)
    if site_id:
        filters.append(Filter("site_id", site_id, "exact"))

    if bool(alert_type) == alert_type_exists:
        result = client.get_alerts(relevant_fields, sort_by, filters, limit)
        parsed_results = _parse_alerts_result(result, relevant_fields)
    else:
        result = {}
        parsed_results = []

    outputs = {"Claroty.Alert(val.ResourceID == obj.ResourceID)": parsed_results}
    readable_output = tableToMarkdown("Claroty Alert List", parsed_results)
    return (readable_output, outputs, result)


def _add_exclude_resolved_alerts_filters(filters: list[Filter]):
    if not filters:
        return [Filter("resolved", "false", "exact")]

    filters += [Filter("resolved", "false", "exact")]
    return filters


def _add_include_only_arr_completed_alerts_filters(filters: list[Filter]):
    if not filters:
        return [Filter("initial_arr_flow_completed", "true", "exact")]

    filters.append(Filter("initial_arr_flow_completed", "true", "exact"))
    return filters


def _init_request_values(
    obj_name: str, sort_by_default_value: str, limit_arg: str, args: dict, get_sort_order_arg: bool = False
) -> tuple[list, dict, int]:
    relevant_fields = get_fields(obj_name, args.get("fields", "").split(","))

    sort_order = False
    if get_sort_order_arg:
        sort_order = get_sort_order(args.get("sort_order", "asc"))

    sort_by = get_sort(args.get("sort_by", sort_by_default_value), sort_order)
    limit = demisto.params().get("per_page", str(DEFAULT_PER_PAGE)) or args.get(limit_arg, str(DEFAULT_PER_PAGE))

    if limit.isdigit() and int(limit) <= MAX_PER_PAGE:
        limit = int(limit)
    else:
        limit = DEFAULT_PER_PAGE

    return relevant_fields, sort_by, limit


def _parse_alerts_result(alert_result: dict, fields: list) -> list[dict]:
    if "objects" not in alert_result:
        return []
    obj = alert_result.get("objects", [])
    alerts = []

    for obj_fields in obj:
        alert = _parse_single_alert(obj_fields, fields)
        alerts.append(alert)
    return alerts


def _parse_single_alert(alert_obj, fields: list):
    parsed_alert_result = {}
    if alert_obj:
        for field in fields:
            if field == "type":
                parsed_alert_result[ALERT_CTD_FIELD_TO_DEMISTO_FIELD[field]] = alert_obj.get(field)
                alert_type_value = alert_obj.get("type__", [])
                parsed_alert_result[ALERT_CTD_FIELD_TO_DEMISTO_FIELD["type__"]] = (
                    alert_type_value[1:] if alert_type_value else None
                )

            elif field == "alert_indicators":
                indicator_str_result = ""
                for indicator in alert_obj.get(field, []):
                    indicator_str_result += f"Alert ID - {indicator['alert_id']}\r\n"
                    indicator_str_result += f"Description - {indicator['indicator_info']['description']}\r\n"
                    indicator_str_result += f"Points - {indicator['indicator_info']['points']}\r\n\n"
                parsed_alert_result[ALERT_CTD_FIELD_TO_DEMISTO_FIELD[field]] = indicator_str_result

            elif field == "severity":
                alert_severity_value = alert_obj.get("severity__")
                parsed_alert_result[ALERT_CTD_FIELD_TO_DEMISTO_FIELD["severity__"]] = (
                    alert_severity_value[1:] if alert_severity_value else None
                )

            elif field == "category":
                alert_category_value = alert_obj.get("category__")
                parsed_alert_result[ALERT_CTD_FIELD_TO_DEMISTO_FIELD["category__"]] = (
                    alert_category_value[1:] if alert_category_value else None
                )

            elif field == "actionable_assets":
                assets = alert_obj.get(field, [])
                parsed_assets = []

                for asset in assets:
                    parsed_assets.append(_parse_single_asset(asset["asset"], DEFAULT_ASSET_FIELD_LIST))
                parsed_alert_result[ALERT_CTD_FIELD_TO_DEMISTO_FIELD[field]] = parsed_assets
            else:
                parsed_alert_result[ALERT_CTD_FIELD_TO_DEMISTO_FIELD[field]] = alert_obj.get(field)

    return parsed_alert_result


def _parse_assets_result(assets_result: dict, fields: list) -> tuple:
    if "objects" not in assets_result:
        return [], []
    obj = assets_result.get("objects", [])
    assets = []
    cves = []

    for obj_fields in obj:
        asset = _parse_single_asset(obj_fields, fields)
        assets.append(asset)
        if asset.get("CVE", None):
            cves.append(asset.get("CVE"))
    return assets, cves


def _parse_single_asset(asset_obj: dict, fields: list) -> dict:
    parsed_asset_result = {}
    if asset_obj:
        for field in fields:
            if field == "asset_type":
                asset_type_value = asset_obj.get("asset_type__")
                parsed_asset_result[ASSET_CTD_FIELD_TO_DEMISTO_FIELD["asset_type__"]] = (
                    asset_type_value[1:] if asset_type_value else None
                )

            elif field == "criticality":
                asset_criticality_value = asset_obj.get("criticality__")
                parsed_asset_result[ASSET_CTD_FIELD_TO_DEMISTO_FIELD["criticality__"]] = (
                    asset_criticality_value[1:] if asset_criticality_value else None
                )

            elif field == "insights":
                cves = []
                highest_cve_score = 0.0
                for insight in asset_obj.get(field, []):
                    cve = {
                        "ID": insight["CVE-ID"],
                        "CVSS": insight["Score"],
                        "Published": insight["Published"],
                        "Modified": insight["Modified"],
                        "Description": insight["Description"],
                    }
                    if float(insight["Score"]) > highest_cve_score:
                        highest_cve_score = float(insight["Score"])
                    cves.append(cve)
                parsed_asset_result["CVE"] = cves
                parsed_asset_result["HighestCVEScore"] = highest_cve_score

            else:
                parsed_asset_result[ASSET_CTD_FIELD_TO_DEMISTO_FIELD[field]] = asset_obj.get(field)

    return parsed_asset_result


def get_sort(field_to_sort_by: str, order_by_desc: bool = False) -> dict:
    order_by_direction = "-" if order_by_desc else ""
    return {"field": field_to_sort_by, "order": order_by_direction}


def get_sort_order(sort_order: str) -> bool:
    return sort_order != "asc"


def get_fields(obj_name: str, fields: list[str]) -> list:
    if obj_name == "alert":
        fields.append("resource_id")
        if "all" in fields:
            fields.pop(fields.index("all"))
            fields.extend(DEFAULT_ALERT_FIELD_LIST)

    elif obj_name == "asset":
        fields.extend(["id", "resource_id", "site_id"])
        if "all" in fields:
            fields.pop(fields.index("all"))
            fields.extend(DEFAULT_ASSET_FIELD_LIST)

    fields = set(fields)
    return list(fields)


def add_filter(filter_name: str, filter_value: Any, filter_operation: str = "exact"):
    return {
        "field": filter_name,
        "value": filter_value,
        "operator": filter_operation,
    }


def transform_filters_labels_to_values(table_filters, filter_name: str, filter_val: str):
    chosen_filters = []
    for table_filter in table_filters:
        if table_filter["name"].lower() == filter_name:
            table_filter_value = next(
                (
                    table_filter_value["value"]
                    for table_filter_value in table_filter["values"]
                    if filter_val == table_filter_value["label"].lower().replace(" ", "")
                ),
                None,
            )
            if table_filter_value:
                chosen_filters.append((table_filter["name"], table_filter_value))

    return chosen_filters


def get_severity_filter(severity: str) -> str:
    severity_values = []
    for _severity_key, severity_value in CTD_TO_DEMISTO_SEVERITY.items():
        if severity_value >= CTD_TO_DEMISTO_SEVERITY.get(severity, 0):
            severity_values.append(str(severity_value))
    return ",;$".join(severity_values)


def get_list_incidents(client: Client, latest_created_time: str, page_number: int):
    field_list = DEFAULT_ALERT_FIELD_LIST + ["timestamp"]
    extra_filters = {}

    severity = demisto.params().get("severity", None)
    if severity:
        extra_filters["severity"] = get_severity_filter("".join(severity))

    site_id = demisto.params().get("site_id", None)
    if site_id:
        extra_filters["site_id"] = site_id

    alert_type = demisto.params().get("alert_type", None)
    alert_type_exists = False
    if alert_type:
        alert_filters = client.get_ranger_table_filters("alerts")
        filters_url_suffix = transform_filters_labels_to_values(alert_filters, "type", alert_type.lower().replace(" ", ""))
        if filters_url_suffix:
            for filter_type in filters_url_suffix:
                extra_filters["type"] = filter_type[1]
                alert_type_exists = True

    if bool(alert_type) == alert_type_exists:
        response = client.list_incidents(field_list, get_sort("timestamp"), latest_created_time, page_number, **extra_filters)
    else:
        response = {}

    return response, field_list


def fetch_incidents(client: Client, last_run, first_fetch_time):
    """
    This function will execute each interval (default is 1 minute).
    """
    last_fetch = last_run.get("last_fetch", None)
    last_run_rids = last_run.get("last_run_rids", {})
    page_to_query = last_run.get("page_to_query", 1)

    if not last_fetch:
        last_fetch, _ = parse_date_range(first_fetch_time, date_format=DATE_FORMAT, utc=True)

    current_rids = []
    incidents = []
    response, field_list = get_list_incidents(client, last_fetch, page_to_query)
    items = _parse_alerts_result(response, field_list)

    # Check last queried item's timestamp
    latest_created_time = None
    if items:
        parsed_date = dateparser.parse(items[-1]["Timestamp"])
        assert parsed_date is not None, f"failed parsing {items[-1]['Timestamp']}"
        latest_created_time = parsed_date.replace(tzinfo=None).strftime(DATE_FORMAT)

    # If timestamp stayed the same than get next 10
    if last_fetch == latest_created_time:
        page_to_query += 1
    else:
        page_to_query = 1

    for item in items:
        # Don't add duplicated incidents
        rid = item["ResourceID"]
        if rid not in last_run_rids:
            # Make datetime object unaware of timezone for comparison
            parsed_date = dateparser.parse(item["Timestamp"])
            assert parsed_date is not None, f"failed parsing {item['Timestamp']}"
            incident_created_time = parsed_date.replace(tzinfo=None)

            incident = {
                "name": item.get("Description", None),
                "occurred": incident_created_time.strftime(DATE_FORMAT),
                "severity": CTD_TO_DEMISTO_SEVERITY.get(item.get("Severity", None), None),
                "rawJSON": json.dumps(item),
            }

            incidents.append(incident)
            current_rids.append(rid)

    # If there were no items queried, latest_created_time is the same as last run
    if latest_created_time is None:
        latest_created_time = last_fetch

    # If no new items were retrieved, last_run_rids stay the same
    if not current_rids:
        current_rids = last_run_rids

    next_run = {"last_fetch": latest_created_time, "last_run_rids": current_rids, "page_to_query": page_to_query}
    return next_run, incidents


def main():
    username = demisto.params().get("credentials").get("identifier")
    password = demisto.params().get("credentials").get("password")

    base_url = demisto.params()["url"].rstrip("/")

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

    first_fetch_time = demisto.params().get("fetch_time", "7 days").strip()

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

    LOG(f"Command being called is {demisto.command()}")
    try:
        client = Client(
            base_url=base_url,
            verify=verify_certificate,
            credentials=(username, password),
            proxy=proxy,
        )

        if demisto.command() == "test-module":
            result = test_module(client)
            demisto.results(result)

        elif demisto.command() == "claroty-get-assets":
            return_outputs(*get_assets_command(client, demisto.args()))

        elif demisto.command() == "claroty-query-alerts":
            return_outputs(*query_alerts_command(client, demisto.args()))

        elif demisto.command() == "claroty-get-single-alert":
            return_outputs(*get_single_alert_command(client, demisto.args()))

        elif demisto.command() == "claroty-resolve-alert":
            return_outputs(*resolve_alert_command(client, demisto.args()))

        elif demisto.command() == "fetch-incidents":
            next_run, incidents = fetch_incidents(client=client, last_run=demisto.getLastRun(), first_fetch_time=first_fetch_time)

            demisto.setLastRun(next_run)
            demisto.incidents(incidents)

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


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