Cylance Protect v2

Manage Endpoints using Cylance protect.

Endpoint · Cylance Protect

Details

IDCylance Protect v2
ProviderArctic Wolf
CategoryEndpoint
From Version5.0.0
Docker Imagedemisto/auth-utils:1.0.0.11671917
Supported ModulesAgentix XSIAM EDR Cortex Cloud Cloud Runtime Security

README

Overview

Use the Cylance Protect v2 integration to manage endpoints, streamline remediation, and response from Cortex XSOAR.

This integration was integrated and tested with version 2.0.5 rev6 of Cylance Protect and Optics.

Prerequisites

Before you integrate Cylance Protect on Cortex XSOAR, you need to obtain a Cylance token.

  1. In Cylance, navigate to Settings > Integrations.
  2. Click Add Application.
  3. Enter an Application Name, and select the necessary privileges.
  4. Click Save.
  5. Record the Application ID and Application Secret for later use. You will not be able to access these later.
  6. Locate the Tenant ID at the top right side of the Integrations page and record it for later use. 

Configure the Cylance Protect v2 Integration on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Cylance Protect v2.
  3. Click Add instance to create and configure a new integration instance.
    • Name: A textual name for the integration instance.
    • Server URL: URL of Cylance server.
    • Application ID
    • Application Secret
    • Tenant API Key
    • Use system proxy settings
    • File Threshold: Default is -59
    • Fetch Incidents
    • Trust any certificate (not secure)
  4. Click Test to validate the URLs and connection.

Understanding the Cylance Score

The Cylance score ranges from -100 to 100, and is translated as follows.

Score translation

Score Range Color Severity Level
-100 to -60  Red Malicious
-59 to 0 Red Supsicious
1-100 Green Good

Use Cases

  • Retrieve and update threats and devices.
  • Produce threat data report of indicators.
  • Retrieve and create policies and zones.

 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. List console device resources for a tenant: cylance-protect-get-devices
  2. Get a console device resource for a tenant: cylance-protect-get-device
  3. Update a device: cylance-protect-update-device
  4. Get information for device threats: cylance-protect-get-device-threats
  5. Get information for console policy resources: cylance-protect-get-policies
  6. Create a zone: cylance-protect-create-zone
  7. Get information for multiple zones: cylance-protect-get-zones
  8. Get information for a single zone: cylance-protect-get-zone
  9. Update a zone: cylance-protect-update-zone
  10. Get information for a threat: cylance-protect-get-threat
  11. Get information for a threat device: cylance-protect-get-threat-devices
  12. Generate a report for indicators: cylance-protect-get-indicators-report
  13. Get information for threats: cylance-protect-get-threats
  14. Update device threats: cylance-protect-update-device-threats
  15. Get a list for hashes: cylance-protect-get-list
  16. Download a threat: cylance-protect-download-threat
  17. Add a hash to a list: cylance-protect-add-hash-to-list
  18. Delete a hash from a list: cylance-protect-delete-hash-from-lists
  19. Get details of a policy: cylance-protect-get-policy-details
  20. Delete devices: cylance-protect-delete-devices
  21. Create a new Instaquery: cylance-optics-create-instaquery
  22. Get Instaquery result: cylance-optics-get-instaquery-result
  23. List current Instaquery: cylance-optics-list-instaquery

1. List console device resources for a tenant


Returns a list of console device resources that belong to a tenant. The list is sorted by registration created date, with the most recent at the top of the list.

Base Command

cylance-protect-get-devices

Input
Input Parameter Description
pageNumber Page number, default is 1
pageSize Specifies if the command polls for the result of the analysis, default is 100, maximum is 200

 

Context Output
Path Description
CylanceProtect.Device.AgentVersion CylancePROTECT Agent version installed on the device
CylanceProtect.Device.DateFirstRegistered Date and time (in UTC) when the device record was created
CylanceProtect.Device.ID Device’s unique identifier
Endpoint.IPAddress List of IP addresses for the device
Endpoint.MACAddress List of MAC addresses for the device
Endpoint.Hostname Device name
CylanceProtect.Device.Policy.ID Device policy ID
CylanceProtect.Device.State Machine state
CylanceProtect.Device.Policy.Name Device policy name
CylanceProtect.Device.Hostname Device name
CylanceProtect.Device.MACAddress List of MAC addresses for the device
CylanceProtect.Device.IPAddress List of IP addresses for the device

 

Command Example

!cylance-protect-get-devices pageNumber=2 pageSize=75

Raw Output
{  
   "agent_version":"2.0.1440",
   "date_first_registered":"2018-01-21T15:45:42",
   "id":"652bbfa9-cf74-4e24-90f7-d01b16429701",
   "ip_addresses":[  
      "172.31.31.110"
   ],
   "mac_addresses":[  
      "06-F8-13-8B-16-C9"
   ],
   "name":"WIN-0VJ9RO3P33Q",
   "policy":{  
      "id":null,
      "name":"Default"
   },
   "state":"Online"
}

2. Get a console device resource for a tenant


Returns a single device resource that belongs to a tenant.

Base Command

cylance-protect-get-device

Input
Input Parameter Description
id Device ID

 

Context Output
Path Description
CylanceProtect.Device.AgentVersion CylancePROTECT Agent version installed on the device
CylanceProtect.Device.DateFirstRegistered Date and time (in UTC) when the device record was created
CylanceProtect.Device.BackgroundDetection If true, the agent is running
CylanceProtect.Device.DateLastModified Date and time (in UTC) when the device record was last modified
CylanceProtect.Device.DateOffline Date and time (in UTC) when the device last communicated with the console
CylanceProtect.Device.Hostname Hostname for the device
CylanceProtect.Device.ID Unique identifier for the device
CylanceProtect.Device.IPAddress List of IP addresses for the device
CylanceProtect.Device.MACAddress List of MAC addresses for the device
CylanceProtect.Device.IsSafe If true, there are no outstanding threats
CylanceProtect.Device.UpdateAvailable If true, there is available update for the device
CylanceProtect.Device.State Machine state
Endpoint.Hostname Device hostname
Endpoint.MACAddress List of MAC addresses for the device
Endpoint.IPAddress List of IP addresses for the device
Endpoint.OSVersion Device OS version
CylanceProtect.Device.OSVersion Device OS version
CylanceProtect.Device.Name Device name

 

Command Example

!cylance-protect-get-devices pageNumber=2 pageSize=75

Raw Output
{  
   "agent_version":"2.0.1440",
   "date_first_registered":"2018-01-21T15:45:42",
   "id":"652bbfa9-cf74-4e24-90f7-d01b16429701",
   "ip_addresses":[  
      "172.31.31.110"
   ],
   "mac_addresses":[  
      "06-F8-13-8B-16-C9"
   ],
   "name":"WIN-0VJ9RO3P33Q",
   "policy":{  
      "id":null,
      "name":"Default"
   },
   "state":"Online"
}

3. Update a device


Updates a specified device.

Base Command

cylance-protect-update-device

Input
Input Parameter Description
id Device ID
name Device name
policyId Policy ID
addZones Zones IDs to add
removeZones Zones IDs to remove

 

Context Output

There is no context output for this command.

 

Command Example

!cylance-protect-update-device id=652bbfa9-cf74-4e24-90f7-d01b16429701

Raw Output
{  
   "Name":"TestName",
   "PolicyID":"7bcb0817-e9c9-444d-96e2-be9b59f429cb",
   "id":"6033f7a1-e66c-4aef-9c7d-ed454457d071"
}

4. Get information for device threats


Returns information about threats to devices, including classification, threat score, and more.

Base Command

cylance-protect-get-device-threats

 

Input
Input Parameter Description
id Device ID
pageNumber Page number, default is 1
pageSize Specifies if the command polls for the result of the analysis, default is 100, maximum is 200

 

Context Output
Path Description
File.Classification Cylance threat classification assigned to the threat
File.CylanceScore Cylance score assigned to the threat
File.DateFound Date and time (in UTC) when the threat was found on the device
File.FilePath File path where the threat was found on the device
File.FileStatus Current status of the file on the device.
  • 0 = Default
  • 1 = Quarantined
  • 2 = Whitelisted
  • 3 = Suspicious
  • 4 = FileRemoved
  • 5 = Corrupt
 File.Name Threat name
File.Sha256 SHA-256 has for the threat
File.SubClassification Cylance threat sub-classification assigned to the threat
DBotScore.Indicator Tested indicator
DBotScore.Type Indicator type
DBotScore.Vendor Vendor used to calculate the score
DbotScore.Score Actual score

 

Command Example

!cylance-protect-get-device-threats id=6033f7a1-e66c-4aef-9c7d-ed454457d071 pageNumber=2 pageSize=75

Raw Output
{  
   "classification":"Malware",
   "cylance_score":-1,
   "date_found":"2017-11-21T17:34:51",
   "file_path":"C:\\$Recycle.Bin\\S-1-5-21-3378384064-522475393-1698893855-1001\\$RPJNCM8\\artifacts\\2017-08-12-Trickbot-binary-from-usdata.estoreseller.com.exe",
   "file_status":"Default",
   "name":"2017-08-12-Trickbot-binary-from-usdata.estoreseller.com.exe",
   "sha256":"5DA547E87D6EF12349FB4DBBA9CF3146A358E284F72361DD07BBABFC95B0BAC3",
   "sub_classification":"Trojan"
}

5. Get information for console policy resources


Returns information for console policy resources.

Base Command

cylance-protect-get-policies

 

Input
Input Parameter Description
pageNumber Page number, default is 1
pageItems Number of items on a page, default is 100

 

Context Output
Path Description
CylanceProtect.Policies.DateAdded Date and time (in UTC) when the Console policy resource was first created
CylanceProtect.Policies.DateModified Date and time (in UTC) when the Console policy resource was last modified
CylanceProtect.Policies.DeviceCount Number of devices assigned to this policy
CylanceProtect.Policies.Id Unique ID for the policy resource
CylanceProtect.Policies.Name Policy name
CylanceProtect.Policies.ZoneCount Number of zones assigned to this policy

 

Command Example

!cylance-protect-get-policies id=6033f7a1-e66c-4aef-9c7d-ed454457d071 pageNumber=2 pageSize=75

Raw Output
{  
   "date_added":"2018-03-05T12:29:02",
   "date_modified":"2018-03-05T12:29:02",
   "device_count":0,
   "id":"7bcb0817-e9c9-444d-96e2-be9b59f429cb",
   "name":"Test_Policy",
   "zone_count":4
}

6. Create a zone


Creates a zone with a policy ID and criticality level.

Base Command

cylance-protect-create-zone

 

Input
Input Parameter Description
name Zone name
policy_id Unique ID for the policy assigned to the zone
criticality Criticality value of the zone

 

Context Output

There is no context output for this command.

 

Command Example

!cylance-protect-create-zone name=TestingZone3 criticality=High

Raw Output
{  
   "criticality":"High",
   "date_created":"2018-03-13T11:38:52.2065082Z",
   "id":"f15b2f79-c100-4146-b056-a8005c13b2de",
   "name":"TestingZone3",
   "policy_id":"7bcb0817-e9c9-444d-96e2-be9b59f429cb"
}

7. Get information for multiple zones


Returns information for multiple zones.

Base Command

cylance-protect-get-zones

 

Input
Input Parameter Description
pageNumber Page number to request
pageItems Number of zone records to retrieve for each page

 

Context Output
Path Description
CylanceProtect.Zones.Criticality

Zone value.

  • Low
  • Medium
  • High
CylanceProtect.Zones.DateCreated Date and time (in UTC) when the zone was created
CylanceProtect.Zones.DateModified Date and time (in UTC) when the zone was last modified
CylanceProtect.Zones.Id Zone unique ID
CylanceProtect.Zones.Name Zone name
CylanceProtect.Zones.PolicyId Unique ID of the policy assigned to the zone
CylanceProtect.Zones.UpdateType Update type for the zone
CylanceProtect.Zones.ZoneRuleId Unique ID for the zone rule created for the zone

 

Command Example

!cylance-protect-get-zones pageNumber=2 pageItems=10

Raw Output
{  
   "criticality":"High",
   "date_created":"2018-03-13T11:38:52",
   "date_modified":"2018-03-13T11:38:52",
   "id":"f15b2f79-c100-4146-b056-a8005c13b2de",
   "name":"TestingZone3",
   "policy_id":"7bcb0817-e9c9-444d-96e2-be9b59f429cb",
   "update_type":"Production",
   "zone_rule_id":null
}

8. Get information for a single zone


Returns information for a single zone.

Base Command

cylance-protect-get-zone

 

Input
Input Parameter Description
id Zone ID

 

Context Output
Path Description
CylanceProtect.Zones.Criticality

Zone value.

  • Low
  • Medium
  • High
CylanceProtect.Zones.DateCreated Date and time (in UTC) when the zone was created
CylanceProtect.Zones.DateModified Date and time (in UTC) when the zone was last modified
CylanceProtect.Zones.Id Zone unique ID
CylanceProtect.Zones.Name Zone name
CylanceProtect.Zones.PolicyId Unique ID of the policy assigned to the zone
CylanceProtect.Zones.UpdateType Update type for the zone
CylanceProtect.Zones.ZoneRuleId Unique ID for the zone rule created for the zone

 

Command Example

!cylance-protect-get-zone id=f15b2f79-c100-4146-b056-a8005c13b2de

Raw Output
{  
   "criticality":"High",
   "date_created":"2018-03-13T11:38:52",
   "date_modified":"2018-03-13T11:38:52",
   "id":"f15b2f79-c100-4146-b056-a8005c13b2de",
   "name":"TestingZone3",
   "policy_id":"7bcb0817-e9c9-444d-96e2-be9b59f429cb",
   "update_type":"Production",
   "zone_rule_id":null
}

9. Update a zone


Updates a specified zone.

Base Command

cylance-protect-update-zone

Input
Input Parameter Description
id Zone ID
name Zone name
policyId Unique ID for the policy assigned to the Zone
criticality Criticality value of the zone

 

Context Output

There is no context output for this command.

 

Command Example

!cylance-protect-update-zone id=f15b2f79-c100-4146-b056-a8005c13b2de

Raw Output
true

10. Get information for a threat


Returns information for a threat.

Base Command

cylance-protect-get-threat

 

Input
Input Parameter Description
sha256 SHA-256 hash of the threat
theshold Threat threshold

 

Context Output
Path Description
File.AutoRun Indicates if the file is set to automatically run on system startup
File.AvIndustry The score provided by the Anti-Virus industry
File.CertIssuer ID for the certificate issuer
File.CertPublisher ID for the certificate publisher
File.CertTimestamp Date and time (in UTC) when the file was signed using the certificate
File.Classification Threat classification for the threat
File.CylanceScore Cylance Score assigned to the threat
File.DetectedBy Name of the Cylance module that detected the threat
File.FileSize Size of the file
File.GlobalQuarantine Identifies if the threat is on the Global Quarantine list
File.Md5 MD5 hash for the threat
File.Name Threat name
File.Running Identifies if the threat is executing, or another executable loaded or called it
File.Safelisted Identifies if the threat is on the Safe List
File.Sha256 SHA-256 hash for the threat
File.Signed Identifies the file as signed or not signed
File.SubClassification The threat sub-classification for the threat
File.UniqueToCylance The threat was identified by Cylance but not by other anti-virus sources
DBotScore.Indicator The tested indicator
DBotScore.Type Indicator type
DBotScore.Vendor Vendor used to calculate the score
DBotScore.Score The actual score

 

Command Example

!cylance-protect-get-threat sha256=ED01EBFBC9EB5BBEA545AF4D01BF5F1071661840480439C6E5BABE8E080E41AA

Raw Output
{  
   "auto_run":false,
   "av_industry":null,
   "cert_issuer":"",
   "cert_publisher":"",
   "cert_timestamp":"0001-01-01T00:00:00",
   "classification":"Malware",
   "cylance_score":-1,
   "detected_by":"File Watcher",
   "file_size":3514368,
   "global_quarantined":true,
   "md5":"84C82835A5D21BBCF75A61706D8AB549",
   "name":"wanncry.exe",
   "running":false,
   "safelisted":false,
  "sha256":"ED01EBFBC9EB5BBEA545AF4D01BF5F1071661840480439C6E5BABE8E080E41AA",
   "signed":false,
   "sub_classification":"Ransom",
   "unique_to_cylance":false
}

11. Get information for a threat device


Returns information for a threat device.

Base Command

cylance-protect-get-threat-devices

 

Input
Input Parameter Description
sha256 SHA-256 hash of the threat

 

Context Output
Path Description
CylanceProtect.Threat.Devices.ID Device ID
CylanceProtect.Threat.Devices.DateFound Date and time (in UTC) when the threat was found on the device
CylanceProtect.Threat.Devices.AgentVersion Agent version installed on the device
CylanceProtect.Threat.Devices.FileStatus

Current quarantine status of the file on the device.

  • 0 = Default
  • 1 = Quarantined
  • 2 = Whitelisted
  • 3 = Suspicious
  • 4 = FileRemoved
  • 5 = Corrupt
Endpoint.IPAddress List of IP addresses for the device
Endpoint.MACAddress List of MAC addresses for the device
Endpoint.Hostname Device name for the device
CylanceProtect.Threat.Devices.PolicyID Unique identifier of the policy assigned to the device, or null if no policy is assigned
CylanceProtect.Threat.Devices.State

Device state.

  • Online
  • Offline
File.SHA256 SHA-256 hash of the threat
File.Path Path where the file was found on the device
CylanceProtect.Threat.Devices.Hostname Device name for the device
CylanceProtect.Threat.Devices.IPAddress List of IP addresses for the device
CylanceProtect.Threat.Devices.MACAddress List of MAC addresses for the device

 

Command Example

!cylance-protect-get-threat-devices sha256=ED01EBFBC9EB5BBEA545AF4D01BF5F1071661840480439C6E5BABE8E080E41AA

Raw Output
{  
   "agent_version":"2.0.1440",
   "date_found":"2018-01-21T15:45:46",
   "file_status":"Whitelisted",
   "id":"652bbfa9-cf74-4e24-90f7-d01b16429701",
   "ip_addresses":"172.31.31.110",
   "mac_addresses":"06-F8-13-8B-16-C9",
   "name":"WIN-0VJ9RO3P33Q",
   "policy_id":null,
   "state":"Online"
}

12. Generate a report for indicators


Generates a report for indicators.

Base Command

cylance-protect-get-indicators-report

 

Input
Input Parameter Description
token Threat data report token

 

Context Output

There is not context output for this command.

Command Example

cylance-protect-get-indicators-report token=As3424$%

Raw Output

There is no raw output for this command.

13. Get information for threats


Returns information for threats.

Base Command

cylance-protect-get-threats

 

Input
Input Parameter Description
page_size Number of device records to retrieve for each page
page Page number to request
threshold Threat threshold

 

Context Output
Path Description
File.Classification Threat classification for the threat
File.SubClassification Threat sub-classification for the threat
File.Sha256 SHA-256 hash for the threat
File.Safelisted Identifies if the threat is on the Safe List
File.Name Threat name
File.LastFound Date and time (in UTC) when the file was last found
File.CylanceScore The Cylance Score assigned to the threat
File.GlobalQuarantine Identifies if the threat is on the Global Quarantine list
File.UniqueToCylance The threat was identified by Cylance but not by other anti-virus sources
File.FileSize File size
File.Md5 MD5 hash for the threat
DBotScore.Indicator The tested indicator
DBotScore.Type Indicator type
DBotScore.Vendor Vendor used to calculate the score
DBotScore.Score The actual score

 

Command Example

!cylance-protect-get-threats page_size=4

Raw Output
{  
   "av_industry":null,
   "classification":"Malware",
   "cylance_score":-1,
   "file_size":3514368,
   "global_quarantined":true,
   "last_found":"2018-01-21T15:45:46",
   "md5":"84C82835A5D21BBCF75A61706D8AB549",
   "name":"wanncry.exe",
   "safelisted":false,
   "sha256":"ED01EBFBC9EB5BBEA545AF4D01BF5F1071661840480439C6E5BABE8E080E41AA",
   "sub_classification":"Ransom",
   "unique_to_cylance":false
}

14. Update device threats


Updates multiple device threats.

Base Command

cylance-protect-update-device-threats

 

Input
Input Parameter Description
threat_id SHA-256 of the convicted threat
event Requested status update for the convicted threat
device_id ID of the device to update

 

Context Output

There is no context output for this command.

 

Command Example

!cylance-protect-update-device-threats threat_id=ED01EBFBC9EB5BBEA545AF4D01BF5F1071661840480439C6E5BABE8E080E41AA

Raw Output
true

15. Get a list for hashes


Returns a list for hashes.

Base Command

cylance-protect-get-list

 

Input
Input Parameter Description
listTypeId

Type of list to retrieve hashes for.

  • 0 = GlobalQuarantine
  • 1 = GlobalSafe
page_size Number of device records to retrieve for each page
page Page number to request
threshold Threat threshold

 

Context Output
Path Description
File.Added Timestamp when the file was added to the list
File.AddedBy Tenant user ID who added the file to the list
File.AvIndustry The score provided by the Anti-Virus industry
File.Category The category for the list specified (for the Global Safe list only)
File.Classification Threat classification assigned by Cylance
File.CylanceScore The Cylance score assigned to the threat
File.ListType list type that the threat belongs to
File.Md5 MD5 of the threat
File.Sha256 SHA-256 of the threat
File.Name Threat name
DBotScore.Indicator The tested indicator
DBotScore.Type Indicator type
DBotScore.Vendor Vendor used to calculate the score
DBotScore.Score The actual score

 

Command Example

!cylance-protect-get-list listTypeId=0 page_size=4

Raw Output
{  
   "added":"2017-11-07T04:30:04",
   "added_by":"3ff9b11e-b64e-4350-97ba-aeb0a099b8ee",
   "av_industry":null,
   "category":"",
   "classification":"Malware",
   "cylance_score":-1,
   "list_type":"GlobalQuarantine",
   "md5":"84C82835A5D21BBCF75A61706D8AB549",
   "name":"wanncry.exe",
   "reason":"Malicious File Found",
   "sha256":"ED01EBFBC9EB5BBEA545AF4D01BF5F1071661840480439C6E5BABE8E080E41AA",
   "sub_classification":"Ransom"
}

 

16. Download a threat


Downloads a threat attached to a specific SHA-256 hash.

Base Command
cylance-protect-download-threat
Input
Argument Name Description Required
sha256 The SHA-256 hash for the file you want to download Required
threshold File threshold to determine reputation Optional
unzip Check to return the file unzipped to the War Room Optional
 
Context Output
Path Type Description
File.SHA256 string SHA-256 of the file
File.Name string File name
File.Size number File size
File.Safelisted boolean Safelisted
File.Timestamp string Timestamp
File.Md5 string MD5
DBotScore.Indicator string The Indicator
DBotScore.Score number The DBot score
DBotScore.Type string The Indicator type
DBotScore.Vendor string The DBot score vendor
File.Malicious.Vendor string For malicious files, the vendor that made the decision
File.Malicious.Description string For malicious files, the reason that the vendor made the decision
 
Command Example
!cylance-protect-download-threat sha256="0f427b33b824110427b2ba7be20740b45ea4da41bc1416dd55771edfb0c18f09" unzip="yes"
Context Example
DBotScore
{
  "Indicator": "AutoitLocker.exe",
  "Score": 3,
  "Type": "file",
  "Vendor": "Cylance Protect"
}
File
{
  "DownloadURL":       "https://cylanceephemeralfilestore.s3.amazonaws.com/0F/42/7B/33/0F427B33B824110427B2BA7BE20740B45EA4DA41BC1416DD55771EDFB0C18F09.zip? Signature=98kI7a19I2q%2BeE7Ef1un4BjSolQ%3D&Expires=1541875473&AWSAccessKeyId=AKIAIAD6JC2YTYVBFRFA",
  "MD5": "2FC103D0D52466B63D44444CE12A5901",
  "Malicious": {
  "Description": "Score determined by get threat command",
  "Vendor": "Cylance Protect"
},
  "Name": "AutoitLocker.exe",
  "SHA256": "0F427B33B824110427B2BA7BE20740B45EA4DA41BC1416DD55771EDFB0C18F09",
  "Safelisted": false,
  "Size": 405345,
  "Timestamp": "0001-01-01T00:00:00"
}
Human Readable Output

screen shot 2018-11-10 at 20 56 40

17. Add a hash to a list


Adds an identified threat to either the Global Quarantine list or the Global Safe list for a particular Tenant.

Base Command
cylance-protect-add-hash-to-list
Input
Argument Name Description Required
sha256 SHA-256 hash to add to the Global Safe list Required
listType The list type the threat belongs to (GlobalQuarantine or GlobalSafe) Required
reason The reason why the file was added to the list Optional
category This field is required only if the list_type value is Global Safe. The value can be one ofthe following:• Admin Tool• Commercial Software• Drivers• Internal Application• Operating System• Security Software• None Optional
 
Context Output
Path Type Description
File.SHA256 string The SHA-256 hash for the threat
File.Cylance.ListType string The list type the threat belongs to (GlobalQuarantine or GlobalSafe)
File.Cylance.Category string This field is required only if the list_type value is Global Safe. The value can be one of the following: • Admin Tool • Commercial Software • Drivers • Internal Application • Operating System • Security Software • None
 
Command Example
!cylance-protect-add-hash-to-list sha256="9ACD45F5F3F2C7629E51FE3123D31296EF763F6ABC1F895CDD1BF1AFB9A7453B" listType="GlobalQuarantine"
Human Readable Output

screen shot 2018-11-10 at 20 58 58

18. Remove a threat from a list


Removes an identified threat from either the Global Quarantine list or the Global Safe list for a particular Tenant.

Base Command
cylance-protect-delete-hash-from-lists
Input
Argument Name Description Required
sha256 The SHA-256 hash for the threat True
listType The list type the threat belongs to (GlobalQuarantine or GlobalSafe)  True
 
Context Output
Path Type Description
File.SHA256 string SHA-256 of the file
File.Cylance.ListType string The list type the threat belongs to (GlobalQuarantine or GlobalSafe)
 
Command Example
!cylance-protect-delete-hash-from-lists sha256="9ACD45F5F3F2C7629E51FE3123D31296EF763F6ABC1F895CDD1BF1AFB9A7453B" listType="GlobalQuarantine"
Human Readable Output

screen shot 2018-11-10 at 21 01 23

19. Get details for a policy


Gets details for a single policy.

Base Command
cylance-protect-get-policy-details
Input
Argument Name Description Required
policyID The Tenant policy ID to the service endpoint. True
 
Context Output
Path Type Description
Cylance.Policy.ID string Policy ID
Cylance.Policy.Name string Policy name
Cylance.Policy.Timestamp string The date and time the policy was created, in UTC.
 
 

20. Delete devices


Deletes one or more devices from an organization.

Base Command
cylance-protect-delete-devices
Input
Argument Name Description Required
deviceIds The unique identifiers for the devices to delete. The maximum number of Device IDs per request is 20. Required
batch_size The number of devices to delete per request (batch) Optional
 
Context Output
Path Type Description
Cylance.Device.Id string The unique identifier of the deletion request
Cylance.Device.Name string Device name
Cylance.Device.Deleted string Checks if the device was deleted (boolean)
 
Command Example
!cylance-protect-get-policy-details policyID="7bcb0817-e9c9-444d-96e2-be9b59f429cb"
Context Example
Cylance
{
  "Policy": {
  "ID": "7bcb0817-e9c9-444d-96e2-be9b59f429cb",
  "Name": "Test_Policy",
  "Timestamp": "2018-03-05T12:29:03.000000+00:00"
               }
}
Human Readable Output

screen shot 2018-11-10 at 21 02 56

cylance-protect-get-device-by-hostname


Allows a caller to request a specific device resource belonging to a Tenant by hostname

Base Command

cylance-protect-get-device-by-hostname

Input

Argument Name Description Required
hostname The hostname (DNS name). Required

Context Output

Path Type Description
CylanceProtect.Device.AgentVersion String The CylancePROTECT Agent version installed on the device.
CylanceProtect.Device.IPAddress Unknown The list of IP addresses for the device.
CylanceProtect.Device.MACAddress Unknown The list of MAC addresses for the device.
CylanceProtect.Device.Hostname string The hostname for the device.
CylanceProtect.Device.OSVersion string Device OS version.
CylanceProtect.Device.UpdateAvailable boolean If true, there is available update for the device.
CylanceProtect.Device.BackgroundDetection boolean If true, the Agent is currently running.
CylanceProtect.Device.DateFirstRegistered date The date and time (in UTC) when the device record was created.
CylanceProtect.Device.DateLastModified date The date and time (in UTC) when the device record was last modified.
CylanceProtect.Device.DateOffline date The date and time (in UTC) when the device last communicated with the Console.
CylanceProtect.Device.IsSafe boolean If true, there are no outstanding threats.
CylanceProtect.Device.LastLoggedInUser string Last logged in user.
CylanceProtect.Device.State string Machine state.
CylanceProtect.Device.ID string The unique identifier for the device.
CylanceProtect.Device.Name string Device name.
CylanceProtect.Device.UpdateType string Device update type.
CylanceProtect.Device.Policy.ID string Device policy ID.
CylanceProtect.Device.Policy.Name string Device policy name.
Endpoint.Hostname string Device hostname.
Endpoint.MACAddress Unknown The list of MAC addresses for the device.
Endpoint.IPAddress Unknown The list of IP addresses for the device.
Endpoint.OSVersion string Device OS version.

Command Example

!cylance-protect-get-device-by-hostname hostname=WIN-5HMOGIEG6M5

Context Example

{
    "CylanceProtect": {
        "Device": {
            "AgentVersion": "1.2.1418",
            "BackgroundDetection": false,
            "DateFirstRegistered": "2017-12-29T04:07:56",
            "DateLastModified": null,
            "DateOffline": "2020-02-07T02:25:34.151",
            "Hostname": "WIN-5HMOGIEG6M5",
            "ID": "b4eceeb0-8699-4d42-b853-155513042d6e",
            "IPAddress": [
                "127.0.0.1"
            ],
            "IsSafe": true,
            "LastLoggedInUser": "",
            "MACAdress": [
                "02-76-91-6B-0A-BB"
            ],
            "Name": "WIN-5HMOGIEG6M5",
            "OSVersion": "Microsoft Windows Server 2012 R2 Standard",
            "Policy": {
                "ID": "32e4aacd-7698-4ef0-93e8-3e6f1f5c6857",
                "Name": "Default"
            },
            "State": "Offline",
            "UpdateAvailable": false
        }
    },
    "Endpoint": {
        "Hostname": "WIN-5HMOGIEG6M5",
        "IPAddress": [
            "127.0.0.1"
        ],
        "MACAdress": [
            "02-76-91-6B-0A-BB"
        ],
        "OSVersion": "Microsoft Windows Server 2012 R2 Standard"
    }
}

Human Readable Output

Cylance Protect Device WIN-5HMOGIEG6M5

AgentVersion BackgroundDetection DateFirstRegistered DateOffline DlcmStatus HostName Id IpAddresses IsSafe MacAddresses Name OsKernelVersion OsVersion Policy Products State UpdateAvailable
1.2.1418 false 2017-12-29T04:07:56 2020-02-07T02:25:34.151 Unknown WIN-5HMOGIEG6M5 b4eceeb0-8699-4d42-b853-155513042d6e 127.0.0.1 true 02-76-91-6B-0A-BB WIN-5HMOGIEG6M5 6.3.0 Microsoft Windows Server 2012 R2 Standard Default {u’status’: u’Offline’, u’version’: u’1.2.1418’, u’name’: u’protect’} Offline false

21. Create a new Instaquery

cylance-optics-create-instaquery


Create a cylance InstaQuery

Base Command

cylance-optics-create-instaquery

Input

Argument Name Description Required
name InstaQuery name. Required
description InstaQuery description. Required
artifact InstaQuery artifact, select from the list. Possible values are: File, Process, NetworkConnection, RegistryKey. Required
match_value_type InstaQuery value type to match, select from the list. Possible values are: File.Path, File.Md5, File.Sha2, File.Owner, File.CreationDateTime, Process.Name, Process.Commandline, Process.PrimaryImagePath, Process.PrimaryImageMd5, Process.StartDateTime, NetworkConnection.DestAddr, NetworkConnection.DestPort, RegistryKey.ProcessName, RegistryKey.ProcessPrimaryImagePath, RegistryKey.ValueName, RegistryKey.FilePath, RegistryKey.FileMd5, RegistryKey.IsPersistencePoint. Required
match_values Value to search in InstaQuery. Required
zone Zone of the object. Required
match_type Match type fuzzy or exact. Possible values are: Fuzzy, Exact. Required

Context Output

Path Type Description
InstaQuery.New.id string The unique identifier of the created InstaQuery.
InstaQuery.New.created_at date The Date and Time that the InstaQuery was created.
InstaQuery.New.progress string The progress of the InstaQuery.

Command Example

!cylance-optics-create-instaquery name="Test Insta continue" description="Test only" artifact="File" match_value_type="File.Path" match_values="exe" zone="6608ca0e-88c6-4647-b276-271cc5ea4295" match_type="Fuzzy"

Human Readable Output

Result  
case_sensitive false
artifact File
created_at 2022-05-05T05:52:36Z
description Test only
id 9E2CCDA5A93918C588E6865ED6FEEA70
match_type Fuzzy
match_value_type Path
match_values exe
name Test Insta continue
progress  
results_available false
zones 6608CA0E88C64647B276271CC5EA4295

22. Get Instaquery result

cylance-optics-get-instaquery-result


Get a cylance InstaQuery search result

Base Command

cylance-optics-get-instaquery-result

Input

Argument Name Description Required
query_id InstaQuery ID. Required

Context Output

Path Type Description
InstaQuery.Results.result string The InstaQuery results.

Command Example

!cylance-optics-get-instaquery-result query_id=9E2CCDA5A93918C588E6865ED6FEEA70

Human Readable Output

  Result
id 9E2CCDA5A93918C588E6865ED6FEEA70
result false
status {u’@timestamp’: 1651729959.177779, u’HostName’: u’windows-server-‘, u’DeviceId’: u’ 65DB26864E364409B50DDC23291A3511 ‘, u’@version’: u’1’, u’CorrelationId’: u’ 9E2CCDA5A93918C588E6865ED6FEEA70 ‘, u’Result’: u’{“FirstObservedTime”: “1970-01-01T00:00:00.000Z”, “LastObservedTime”: “1970-01-01T00:00:00.000Z”, “Uid”: “dHrtLYQzbt9oJPxO8HaeyA==”, “Type”: “File”, “Properties”: {“Path”: “c:\program files\cylance\optics\ cyoptics.exe “, “CreationDateTime”: “2021-03-29T22:34:14.000Z”, “Md5”: “ A081D3268531485BF95DC1A15A5BC6B0 “, “Sha256”: “ 256809AABD3AB57949003B9AFCB556A9973222CDE81929982DAE7D306648E462 “, “Owner”: “NT AUTHORITY\SYSTEM”, “SuspectedFileType”: “Executable/PE”, “FileSignature”: “”, “Size”: “594104”, “OwnerUid”: “P3p6fdq3FlMsld6Rz95EOA==”}}’}

23. List current Instaqueries

cylance-optics-list-instaquery


Get a list of InstaQuery

Base Command

cylance-optics-list-instaquery

Input

Argument Name Description Required
page number of page to collect. Optional
page_size number of items per page to collect. Required

Context Output

Path Type Description
InstaQuery.List string The list of InstaQuery

Command Example

!cylance-optics-list-instaquery page_size="10"

Human Readable Output

  Result
page_items {u’match_type’: u’Fuzzy’, u’name’: u’Test Insta continue’, u’created_at’: u’2022-05-05T05:52:36Z’, u’artifact’: u’File’, u’case_sensitive’: False, u’zones’: [u’6608CA0E88C64647B276271CC5EA4295’], u’progress’: {u’queried’: 0, u’responded’: 0}, u’match_value_type’: u’Path’, u’results_available’: True, u’match_values’: [u’exe’], u’id’: u’9E2CCDA5A93918C588E6865ED6FEEA70’, u’description’: u’Test only’}
page_number 1
page_size 10
total_number_of_items 8
total_pages 1

Configuration parameters

  • server — Server URL (required)
  • app_id — Application ID
  • app_secret — Application Secret
  • tid — Tenant API Key
  • app_creds — Application ID
  • api_key
  • proxy — Use system proxy settings
  • unsecure — Trust any certificate (not secure)
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • file_threshold — File Threshold
  • incidentFetchInterval — Incidents Fetch Interval

Commands (24)

  • cylance-optics-create-instaquery

    Create a cylance InstaQuery to search for artifacts in one or multiple zones.

  • cylance-optics-get-instaquery-result

    Get a cylance InstaQuery search result.

  • cylance-optics-list-instaquery

    Get a list of all current InstaQueries.

  • cylance-protect-add-hash-to-list

    Adds a convicted threat for a particular tenant to either the Global Quarantine list or the Global Safe list.

  • cylance-protect-create-zone

    Creates (adds) a zone to your Console.

  • cylance-protect-delete-devices

    Delete one or more devices from an organization.

  • cylance-protect-delete-hash-from-lists

    Removes a convicted threat for a particular tenant from either the Global Quarantine list or the Global Safe list.

  • cylance-protect-download-threat

    Downloads the threat (file) attached to a specific SHA256 hash.

  • cylance-protect-get-device

    Allows a caller to request a specific device resource belonging to a Tenant.

  • cylance-protect-get-device-by-hostname

    Allows a caller to request a specific device resource belonging to a Tenant by hostname.

  • cylance-protect-get-device-threats

    Allows a caller to request a page with a list of threats found on a specific device.

  • cylance-protect-get-devices

    Allows a caller to request a page with a list of Console device resources that belongings to a tenant, sorted by registration (created) date in descending order (most recent device registered listed first).

  • cylance-protect-get-indicators-report

    Produces a CSV threat data report of the indicators.

  • cylance-protect-get-list

    Returns a list of global list resources for a tenant.

  • cylance-protect-get-policies

    Allows the caller to get a list of tenant policies.

  • cylance-protect-get-policy-details

    Returns details for a single policy.

  • cylance-protect-get-threat

    Requests threat details for a specific threat.

  • cylance-protect-get-threat-devices

    Allows a caller to request a list of devices on a specific threat.

  • cylance-protect-get-threats

    Returns information about Cylance Protect threats.

  • cylance-protect-get-zone

    Request zone information for a specific zone in your organization.

  • cylance-protect-get-zones

    Request zone information for your organization. This will return the top 100 records.

  • cylance-protect-update-device

    Allows a caller to update a specific Console device resource belonging to a Tenant.

  • cylance-protect-update-device-threats

    Updates the status of a convicted threat. Can be "Quarantine" or "Waive".

  • cylance-protect-update-zone

    Updates a zone in your organization.

import demistomock as demisto
from CommonServerPython import *

import jwt
import uuid
import requests
import urllib3
import json
import re
import zipfile
from io import BytesIO
from datetime import datetime, timedelta

# disable insecure warnings
urllib3.disable_warnings()

# CONSTANTS
TOKEN_TIMEOUT = 300  # 5 minutes

URI_AUTH = "auth/v2/token"
URI_DEVICES = "devices/v2"
URI_POLICIES = "policies/v2"
URI_ZONES = "zones/v2"
URI_THREATS = "threats/v2"
URI_LISTS = "globallists/v2"
URI_HOSTNAME = "devices/v2/hostname"
URI_OPTICS = "instaqueries/v2"  # Optics InstaQuery API Endpoint

SCOPE_DEVICE_LIST = "device:list"
SCOPE_DEVICE_READ = "device:read"
SCOPE_DEVICE_UPDATE = "device:update"
SCOPE_DEVICE_THREAT_LIST = "device:threatlist"
SCOPE_POLICY_LIST = "policy:list"
SCOPE_POLICY_READ = "policy:read"
SCOPE_ZONE_CREATE = "zone:create"
SCOPE_ZONE_LIST = "zone:list"
SCOPE_ZONE_READ = "zone:read"
SCOPE_ZONE_UPDATE = "zone:update"
SCOPE_THREAT_READ = "threat:read"
SCOPE_THREAT_DEVICE_LIST = "threat:devicelist"
SCOPE_THREAT_UPDATE = "threat:update"
SCOPE_GLOBAL_LIST = "globallist:list"
SCOPE_THREAT_LIST = "threat:list"
SCOPE_GLOBAL_LIST_CREATE = "globallist:create"
SCOPE_GLOBAL_LIST_DELETE = "globallist:delete"
SCOPE_OPTICS_LIST = "opticssurvey:list"  # Get InstaQueries
SCOPE_OPTICS_CREATE = "opticssurvey:create"  # Create InstaQuery
SCOPE_OPTICS_GET = "opticssurvey:read"  # Read a InstaQuery


# PREREQUISITES
def load_server_url():  # pragma: no cover
    """Cleans and loads the server url from the configuration"""
    url = demisto.params()["server"]
    url = re.sub(r"/[\/]+$/", "", url)
    url = re.sub(r"\/$", "", url)
    return url


# GLOBALS
APP_ID = ""
APP_SECRET = ""
TID = ""
SERVER_URL = ""
FILE_THRESHOLD = ""
USE_SSL = False


# HELPERS
def generate_jwt_times():  # pragma: no cover
    """
    Generates the epoch time window in which the token will be valid
    Returns the current timestamp and the timeout timestamp (in that order)
    """
    now = datetime.utcnow()
    timeout_datetime = now + timedelta(seconds=TOKEN_TIMEOUT)
    epoch_time = int((now - datetime(1970, 1, 1)).total_seconds())
    epoch_timeout = int((timeout_datetime - datetime(1970, 1, 1)).total_seconds())
    return epoch_time, epoch_timeout


def api_call(uri, method="post", headers={}, body={}, params={}, accept_404=False, access_token=""):  # pragma: no cover
    """
    Makes an API call to the server URL with the supplied uri, method, headers, body and params
    """
    if not headers:
        headers = {"Content-Type": "application/json", "Authorization": "Bearer " + access_token}

    url = f"{SERVER_URL}/{uri}"
    res = requests.request(method, url, headers=headers, data=json.dumps(body), params=params, verify=USE_SSL)
    if res.status_code < 200 or res.status_code >= 300:
        if res.status_code == 409 and str(res.content).find("already an entry for this threat") != -1:
            raise Warning(res.content)
        if res.status_code != 404 and not accept_404:
            return_error(
                "Got status code " + str(res.status_code) + " with body " + str(res.content) + " with headers " + str(res.headers)
            )
    return json.loads(res.text) if res.text else res.ok


def get_authentication_token(scope=None):
    """
    Generates a JWT authorization token with an optional scope and queries the API for an access token
    Returns the received API access token
    """
    # Generate token ID
    token_id = str(uuid.uuid4())

    # Generate current time & token timeout
    epoch_time, epoch_timeout = generate_jwt_times()
    # Token claims
    claims = {"exp": epoch_timeout, "iat": epoch_time, "iss": "http://cylance.com", "sub": APP_ID, "tid": TID, "jti": token_id}

    if scope:
        claims["scp"] = scope

    # Encode the token
    encoded = jwt.encode(claims, APP_SECRET, algorithm="HS256")
    payload = {"auth_token": encoded}
    headers = {"Content-Type": "application/json; charset=utf-8"}
    res = api_call(method="post", uri=URI_AUTH, body=payload, headers=headers)
    return res["access_token"]


def threat_to_incident(threat):
    incident = {
        "name": "Cylance Protect v2 threat " + threat["name"],
        "occurred": threat["last_found"] + "Z",
        "rawJSON": json.dumps(threat),
    }

    host_name = None
    devices = get_threat_devices_request(threat["sha256"], None, None)["page_items"]
    for device in devices:
        if device["date_found"] == threat["last_found"]:
            host_name = device["name"]

    labels = [
        {"type": "Classification", "value": threat["classification"]},
        {"type": "MD5", "value": threat["md5"]},
        {"type": "SHA256", "value": threat["sha256"]},
        {"type": "ThreatLastFound", "value": threat["last_found"]},
        {"type": "HostName", "value": host_name},
    ]
    incident["labels"] = labels
    return incident


def normalize_score(score):  # pragma: no cover
    """
    Translates API raw float (-1 to 1) score to UI score (-100 to 100)
    """
    return score * 100


def translate_score(score, threshold):  # pragma: no cover
    if score > 0:
        dbot_score = 1
    elif threshold <= score:
        dbot_score = 2
    else:
        dbot_score = 3
    return dbot_score


# FUNCTIONS
def test():  # pragma: no cover
    access_token = get_authentication_token()
    if not access_token:
        raise Exception("Unable to get access token")
    demisto.results("ok")


def get_devices():
    page = demisto.args().get("pageNumber")
    page_size = demisto.args().get("pageSize")
    result = get_devices_request(page, page_size)
    devices = result["page_items"]
    hr = []
    devices_context = []
    endpoint_context = []
    for device in devices:
        current_device_context = {
            "AgentVersion": device["agent_version"],
            "DateFirstRegistered": device["date_first_registered"],
            "ID": device["id"],
            "IPAddress": device["ip_addresses"],
            "MACAdress": device["mac_addresses"],
            "Hostname": device["name"],
            "State": device["state"],
        }
        if device["policy"]:
            policy = {}
            if device["policy"]["id"]:
                policy["ID"] = device["policy"]["id"]
            if device["policy"]["name"]:
                policy["Name"] = device["policy"]["name"]
            if policy:
                current_device_context["Policy"] = policy
        devices_context.append(current_device_context)
        endpoint_context.append(
            {"IPAddress": device["ip_addresses"], "MACAdress": device["mac_addresses"], "Hostname": device["name"]}
        )
        current_device = dict(device)
        current_device["ip_addresses"] = ", ".join(current_device["ip_addresses"])
        current_device["mac_addresses"] = ", ".join(current_device["mac_addresses"])
        current_device["policy"] = current_device["policy"]["name"]
        hr.append(current_device)

    ec = {
        "CylanceProtect.Device(val.ID && val.ID === obj.ID)": devices_context,
        "Endpoint(val.Hostname && val.Hostname === obj.Hostname)": endpoint_context,
    }

    entry = {
        "Type": entryTypes["note"],
        "Contents": devices,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown("Cylance Protect Devices", hr, headerTransform=underscoreToCamelCase, removeNull=True),
        "EntryContext": ec,
    }

    demisto.results(entry)


def get_devices_request(page=None, page_size=None):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_DEVICE_LIST)

    params = {}
    if page:
        params["page"] = page
    if page_size:
        params["page_size"] = page_size
    res = api_call(uri=URI_DEVICES, method="get", params=params, access_token=access_token)
    return res


def get_device():
    device_id = demisto.args()["id"]
    device = get_device_request(device_id)
    hr = []
    if device:
        device_context = {
            "IPAddress": device["ip_addresses"],
            "MACAdress": device["mac_addresses"],
            "Hostname": device["host_name"],
            "OSVersion": device["os_version"],
            "UpdateAvailable": device["update_available"],
            "BackgroundDetection": device["background_detection"],
            "DateFirstRegistered": device["date_first_registered"],
            "DateLastModified": device["date_last_modified"],
            "DateOffline": device["date_offline"],
            "IsSafe": device["is_safe"],
            "LastLoggedInUser": device["last_logged_in_user"],
            "State": device["state"],
            "ID": device["id"],
            "Name": device["name"],
        }
        if device["update_type"]:
            device_context["UpdateType"] = device["update_type"]
        if device["policy"]:
            policy = {}
            if device["policy"]["id"]:
                policy["ID"] = device["policy"]["id"]
            if device["policy"]["name"]:
                policy["Name"] = device["policy"]["name"]
            if policy:
                device_context["Policy"] = policy
        endpoint_context = {
            "IPAddress": device["ip_addresses"],
            "MACAdress": device["mac_addresses"],
            "Hostname": device["host_name"],
            "OSVersion": device["os_version"],
        }
        ec = {
            "Endpoint(val.Hostname && val.Hostname === obj.Hostname)": endpoint_context,
            "CylanceProtect.Device(val.ID && val.ID === obj.ID)": device_context,
        }

        current_device = dict(device)
        current_device["ip_addresses"] = ", ".join(current_device["ip_addresses"])
        current_device["mac_addresses"] = ", ".join(current_device["mac_addresses"])
        current_device["policy"] = current_device["policy"]["name"]
        hr.append(current_device)

    else:
        ec = {}

    title = "Cylance Protect Device " + device_id

    entry = {
        "Type": entryTypes["note"],
        "Contents": device,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown(title, hr, headerTransform=underscoreToCamelCase, removeNull=True),
        "EntryContext": ec,
    }

    demisto.results(entry)


def get_device_request(device_id):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_DEVICE_READ)

    uri = f"{URI_DEVICES}/{device_id}"
    res = api_call(uri=uri, method="get", access_token=access_token)
    return res


def get_device_by_hostname():
    hostname_id = demisto.args()["hostname"]
    device = get_hostname_request(hostname_id)
    title = "Cylance Protect Device " + hostname_id
    if device:
        device_context = {
            "AgentVersion": device["agent_version"],
            "IPAddress": device["ip_addresses"],
            "MACAdress": device["mac_addresses"],
            "Hostname": device["host_name"],
            "OSVersion": device["os_version"],
            "UpdateAvailable": device["update_available"],
            "BackgroundDetection": device["background_detection"],
            "DateFirstRegistered": device["date_first_registered"],
            "DateLastModified": device["date_last_modified"],
            "DateOffline": device["date_offline"],
            "IsSafe": device["is_safe"],
            "LastLoggedInUser": device["last_logged_in_user"],
            "State": device["state"],
            "ID": device["id"],
            "Name": device["name"],
        }
        if device["update_type"]:
            device_context["UpdateType"] = device["update_type"]
        if device["policy"]:
            policy = {}
            if device["policy"]["id"]:
                policy["ID"] = device["policy"]["id"]
            if device["policy"]["name"]:
                policy["Name"] = device["policy"]["name"]
            if policy:
                device_context["Policy"] = policy
        endpoint_context = {
            "IPAddress": device["ip_addresses"],
            "MACAdress": device["mac_addresses"],
            "Hostname": device["host_name"],
            "OSVersion": device["os_version"],
        }
        ec = {
            "Endpoint(val.Hostname && val.Hostname === obj.Hostname)": endpoint_context,
            "CylanceProtect.Device(val.ID && val.ID === obj.ID)": device_context,
        }

        current_device = dict(device)
        current_device["ip_addresses"] = ", ".join(current_device["ip_addresses"])
        current_device["mac_addresses"] = ", ".join(current_device["mac_addresses"])
        current_device["policy"] = current_device["policy"]["name"]
        hr = tableToMarkdown(title, [current_device], headerTransform=underscoreToCamelCase, removeNull=True)

    else:
        ec = {}
        hr = "### Device For Hostname " + hostname_id + " Was Not Found"

    entry = {
        "Type": entryTypes["note"],
        "Contents": device,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": hr,
        "EntryContext": ec,
    }

    demisto.results(entry)


def get_hostname_request(hostname):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_DEVICE_READ)

    uri = f"{URI_HOSTNAME}/{hostname}"
    res = api_call(uri=uri, method="get", access_token=access_token)
    if not res:
        return None
    return res[0]


def update_device():
    device_id = demisto.args()["id"]

    name = demisto.args().get("name")
    policy_id = demisto.args().get("policyId")
    add_zones = demisto.args().get("addZones")
    remove_zones = demisto.args().get("removeZones")

    update_device_request(device_id, name, policy_id, add_zones, remove_zones)

    hr = {}

    if name:
        hr["Name"] = name
    if policy_id:
        hr["PolicyID"] = policy_id
    if add_zones:
        hr["AddedZones"] = add_zones
    if remove_zones:
        hr["RemovedZones"] = remove_zones

    device = hr.copy()
    device["id"] = device_id

    title = "Device " + device_id + " was updated successfully."
    entry = {
        "Type": entryTypes["note"],
        "Contents": device,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown(title, [hr]),
    }

    demisto.results(entry)


def update_device_request(device_id, name=None, policy_id=None, add_zones=None, remove_zones=None):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_DEVICE_UPDATE)

    body = {}
    if name:
        body["name"] = name
    if policy_id:
        body["policy_id"] = policy_id
    if add_zones:
        body["add_zone_ids"] = [add_zones]
    if remove_zones:
        body["remove_zone_ids"] = [remove_zones]

    # Do we have anything to update?
    if not body:
        raise Exception("No changes detected")

    uri = f"{URI_DEVICES}/{device_id}"
    res = api_call(uri=uri, method="put", access_token=access_token, body=body)
    return res


def get_device_threats():
    device_id = demisto.args()["id"]
    page = demisto.args().get("pageNumber")
    page_size = demisto.args().get("pageSize")

    device_threats = get_device_threats_request(device_id, page, page_size)["page_items"]
    dbot_score_array = []

    for threat in device_threats:
        dbot_score = 0
        score = threat.get("cylance_score", None)
        if score:
            threat["cylance_score"] = normalize_score(threat["cylance_score"])
            threshold = demisto.args().get("threshold", FILE_THRESHOLD)
            dbot_score = translate_score(threat["cylance_score"], int(threshold))
        dbot_score_array.append(create_dbot_score_entry(threat, dbot_score).to_context())
    if device_threats:
        dbot_score_dict = {Common.DBotScore.get_context_path(): []}  # type: Dict[str, List[Dict[str, str]]]
        for dbot_score_entry in dbot_score_array:
            for _key, value in list(dbot_score_entry.items()):
                dbot_score_dict[Common.DBotScore.get_context_path()].append(value)

        threats_context = createContext(data=device_threats, keyTransform=underscoreToCamelCase)
        threats_context = add_capitalized_hash_to_context(threats_context)
        ec = {"File": threats_context}
        ec.update(dbot_score_dict)

        title = "Cylance Protect Device Threat " + device_id
        demisto.results(
            {
                "Type": entryTypes["note"],
                "Contents": device_threats,
                "ContentsFormat": formats["json"],
                "ReadableContentsFormat": formats["markdown"],
                "HumanReadable": tableToMarkdown(title, device_threats, headerTransform=underscoreToCamelCase),
                "EntryContext": ec,
            }
        )
    else:
        demisto.results("No threats found.")


def get_device_threats_request(device_id, page=None, page_size=None):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_DEVICE_THREAT_LIST)

    params = {}
    if page:
        params["page"] = page
    if page_size:
        params["page_size"] = page_size
    uri = f"{URI_DEVICES}/{device_id}/threats"
    res = api_call(uri=uri, method="get", access_token=access_token, params=params)
    return res


def get_policies():
    page = demisto.args().get("pageNumber")
    page_size = demisto.args().get("pageSize")

    policies = get_policies_request(page, page_size)["page_items"]

    context_policies = createContext(data=policies, keyTransform=underscoreToCamelCase)
    ec = {"CylanceProtect.Policies(val.id && val.id === obj.id)": context_policies}

    title = "Cylance Protect Policies"
    entry = {
        "Type": entryTypes["note"],
        "Contents": policies,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown(title, policies, headerTransform=underscoreToCamelCase),
        "EntryContext": ec,
    }

    demisto.results(entry)


def get_policies_request(page=None, page_size=None):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_POLICY_LIST)

    params = {}
    if page:
        params["page"] = page
    if page_size:
        params["page_size"] = page_size

    res = api_call(uri=URI_POLICIES, method="get", access_token=access_token, params=params)
    return res


def create_zone():
    name = demisto.args()["name"]
    policy_id = demisto.args()["policy_id"]
    criticality = demisto.args()["criticality"]

    zone = create_zone_request(name, policy_id, criticality)

    title = "Zone " + name + " was created successfully."
    demisto.results(
        {
            "Type": entryTypes["note"],
            "Contents": zone,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown(title, [zone], headerTransform=underscoreToCamelCase),
        }
    )


def create_zone_request(name, policy_id, criticality):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_ZONE_CREATE)

    body = {"name": name, "policy_id": policy_id, "criticality": criticality}
    res = api_call(uri=URI_ZONES, method="post", access_token=access_token, body=body)
    return res


def get_zones():
    page = demisto.args().get("pageNumber")
    page_size = demisto.args().get("pageSize")

    zones = get_zones_request(page, page_size)["page_items"]

    context_zones = createContext(data=zones, keyTransform=underscoreToCamelCase, removeNull=True)
    ec = {"CylanceProtect.Zones(val.Id && val.Id === obj.Id)": context_zones}
    title = "Cylance Protect Zones"

    demisto.results(
        {
            "Type": entryTypes["note"],
            "Contents": zones,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown(title, zones, headerTransform=underscoreToCamelCase, removeNull=True),
            "EntryContext": ec,
        }
    )


def get_zones_request(page=None, page_size=None):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_ZONE_LIST)

    params = {}
    if page:
        params["page"] = page
    if page_size:
        params["page_size"] = page_size

    res = api_call(uri=URI_ZONES, method="get", access_token=access_token, params=params)
    return res


def get_zone():
    zone_id = demisto.args()["id"]
    zone = get_zone_request(zone_id)

    context_zone = createContext(data=zone, keyTransform=underscoreToCamelCase, removeNull=True)
    ec = {"CylanceProtect.Zones(val.Id && val.Id === obj.Id)": context_zone}
    title = "Cylance Protect Zone " + zone_id

    demisto.results(
        {
            "Type": entryTypes["note"],
            "Contents": zone,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown(title, zone, headerTransform=underscoreToCamelCase, removeNull=True),
            "EntryContext": ec,
        }
    )


def get_zone_request(zone_id):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_ZONE_READ)

    uri = f"{URI_ZONES}/{zone_id}"
    res = api_call(uri=uri, method="get", access_token=access_token)
    return res


def update_zone():
    zone_id = demisto.args()["id"]

    # Get current zone and fill in requires missing arguments
    current_zone = get_zone_request(zone_id)

    # Details to update
    name = demisto.args()["name"] if "name" in demisto.args() else current_zone["name"]
    policy_id = demisto.args()["policy_id"] if "policy_id" in demisto.args() else current_zone["policy_id"]
    criticality = demisto.args()["criticality"] if "criticality" in demisto.args() else current_zone["criticality"]
    zone = update_zone_request(zone_id, name, policy_id, criticality)
    hr = {}
    if name:
        hr["Name"] = name
    if policy_id:
        hr["PolicyID"] = policy_id
    if criticality:
        hr["Criticality"] = criticality
    title = "Zone was updated successfully."

    demisto.results(
        {
            "Type": entryTypes["note"],
            "Contents": zone,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown(title, [hr]),
        }
    )


def update_zone_request(zone_id, name, policy_id, criticality):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_ZONE_UPDATE)

    body = {}
    if name:
        body["name"] = name
    if policy_id:
        body["policy_id"] = policy_id
    if criticality:
        body["criticality"] = criticality

    # Do we have anything to update?
    if not body:
        raise Exception("No changes detected")

    uri = f"{URI_ZONES}/{zone_id}"
    res = api_call(uri=uri, method="put", access_token=access_token, body=body)
    return res


def get_threat():
    sha256 = demisto.args().get("sha256")
    threat = get_threat_request(sha256)
    if threat:
        dbot_score = 0
        score = threat.get("cylance_score", None)
        if score:
            threat["cylance_score"] = normalize_score(threat["cylance_score"])
            threshold = demisto.args().get("threshold", FILE_THRESHOLD)
            dbot_score = translate_score(threat["cylance_score"], int(threshold))
        context_threat = createContext(data=threat, keyTransform=underscoreToCamelCase, removeNull=True)
        context_threat = add_capitalized_hash_to_context(context_threat)
        ec = {"File": context_threat}
        ec.update(create_dbot_score_entry(threat, dbot_score).to_context())

        title = "Cylance Protect Threat " + sha256

        demisto.results(
            {
                "Type": entryTypes["note"],
                "Contents": threat,
                "ContentsFormat": formats["json"],
                "ReadableContentsFormat": formats["markdown"],
                "HumanReadable": tableToMarkdown(title, threat, headerTransform=underscoreToCamelCase, removeNull=True),
                "EntryContext": ec,
            }
        )
    else:
        demisto.results("Threat was not found.")


def get_threat_request(sha256):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_THREAT_READ)

    uri = f"{URI_THREATS}/{sha256}"
    res = api_call(uri=uri, method="get", access_token=access_token, body={}, params={}, accept_404=False)
    return res


def create_dbot_score_entry(threat, dbot_score):
    dbot_score_entry = Common.DBotScore(
        threat.get("sha256"), DBotScoreType.FILE, integration_name="Cylance Protect", score=dbot_score
    )
    return dbot_score_entry


def get_threats():
    page = demisto.args().get("pageNumber")
    page_size = demisto.args().get("pageSize")

    threats = get_threats_request(page, page_size)["page_items"]
    dbot_score_array = []
    for threat in threats:
        dbot_score = 0
        score = threat.get("cylance_score", None)
        if score:
            threat["cylance_score"] = normalize_score(threat["cylance_score"])
            threshold = demisto.args().get("threshold", FILE_THRESHOLD)
            dbot_score = translate_score(threat["cylance_score"], int(threshold))
        dbot_score_array.append(create_dbot_score_entry(threat, dbot_score).to_context())

    dbot_score_dict = {Common.DBotScore.get_context_path(): []}  # type: Dict[str, List[Dict[str, str]]]
    for dbot_score_entry in dbot_score_array:
        for _key, value in list(dbot_score_entry.items()):
            dbot_score_dict[Common.DBotScore.get_context_path()].append(value)

    context_threat = createContext(data=threats, keyTransform=underscoreToCamelCase, removeNull=True)
    context_threat = add_capitalized_hash_to_context(context_threat)
    ec = {"File": context_threat}
    ec.update(dbot_score_dict)
    title = "Cylance Protect Threats"
    demisto.results(
        {
            "Type": entryTypes["note"],
            "Contents": threats,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown(title, threats, headerTransform=underscoreToCamelCase, removeNull=True),
            "EntryContext": ec,
        }
    )


def get_threats_request(page=None, page_size=None):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_THREAT_LIST)

    params = {}
    if page in demisto.args():
        params["page"] = demisto.args()["page"]
    if page_size in demisto.args():
        params["page_size"] = demisto.args()["pageSize"]

    res = api_call(uri=URI_THREATS, method="get", access_token=access_token, params=params)
    return res


def get_threat_devices():
    threat_hash = demisto.args()["sha256"]
    page = demisto.args().get("pageNumber")
    page_size = demisto.args().get("pageSize")

    threats = get_threat_devices_request(threat_hash, page, page_size)["page_items"]

    if threats:
        threats_context = threats[:]

        for threat in threats:
            threat["ip_addresses"] = ", ".join(threat["ip_addresses"])
            threat["mac_addresses"] = ", ".join(threat["mac_addresses"])

        file_paths = []
        endpoint_context = []
        devices_context = []
        for threat in threats_context:
            endpoint_context.append(
                {"Hostname": threat["name"], "IPAddress": threat["ip_addresses"], "MACAddress": threat["mac_addresses"]}
            )
            current_device = {
                "Hostname": threat["name"],
                "IPAddress": threat["ip_addresses"],
                "MACAddress": threat["mac_addresses"],
                "AgentVersion": threat["agent_version"],
                "DateFound": threat["date_found"],
                "FilePath": threat["file_path"],
                "ID": threat["id"],
                "State": threat["state"],
                "FileStatus": threat["file_status"],
            }
            if threat["policy_id"]:
                current_device["PolicyID"] = threat["policy_id"]
            devices_context.append(current_device)
            file_path = threat.pop("file_path")
            file_paths.append({"FilePath": file_path})

        file_context = {"SHA256": threat_hash, "Path": file_paths}

        ec = {
            "File": file_context,
            "Endpoint(val.Hostname && val.Hostname === obj.Hostname)": endpoint_context,
            "CylanceProtect.Threat(val.SHA256 && val.SHA256 === obj.SHA256)": {"SHA256": threat_hash, "Devices": devices_context},
        }

        title = "Cylance Protect Threat " + threat_hash + " Devices"
        demisto.results(
            {
                "Type": entryTypes["note"],
                "Contents": threats,
                "ContentsFormat": formats["json"],
                "ReadableContentsFormat": formats["markdown"],
                "HumanReadable": tableToMarkdown(title, threats, headerTransform=underscoreToCamelCase, removeNull=True),
                "EntryContext": ec,
            }
        )
    else:
        demisto.results("No devices found on given threat.")


def get_threat_devices_request(threat_hash, page=None, page_size=None):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_THREAT_DEVICE_LIST)

    params = {}
    if page:
        params["page"] = page
    if page_size:
        params["page_size"] = page_size

    uri = f"{URI_THREATS}/{threat_hash}/devices"
    res = api_call(uri=uri, method="get", access_token=access_token, params=params)
    return res


def get_list():
    page = demisto.args().get("pageNumber")
    page_size = demisto.args().get("pageSize")

    lst = get_list_request(demisto.args()["listTypeId"], page, page_size)["page_items"]
    dbot_score_array = []
    for threat in lst:
        dbot_score = 0
        score = threat.get("cylance_score", None)
        if score:
            threat["cylance_score"] = normalize_score(threat["cylance_score"])
            threshold = demisto.args().get("threshold", FILE_THRESHOLD)
            dbot_score = translate_score(threat["cylance_score"], int(threshold))
        dbot_score_array.append(create_dbot_score_entry(threat, dbot_score).to_context())
    if lst:
        dbot_score_dict = {Common.DBotScore.get_context_path(): []}  # type: Dict[str, List[Dict[str, str]]]
        for dbot_score_entry in dbot_score_array:
            for _key, value in list(dbot_score_entry.items()):
                dbot_score_dict[Common.DBotScore.get_context_path()].append(value)

        context_list = createContext(data=lst, keyTransform=underscoreToCamelCase, removeNull=True)
        context_list = add_capitalized_hash_to_context(context_list)
        ec = {"File": context_list}
        ec.update(dbot_score_dict)

        title = "Cylance Protect Global List"
        demisto.results(
            {
                "Type": entryTypes["note"],
                "Contents": lst,
                "ContentsFormat": formats["json"],
                "ReadableContentsFormat": formats["markdown"],
                "HumanReadable": tableToMarkdown(title, lst, headerTransform=underscoreToCamelCase, removeNull=True),
                "EntryContext": ec,
            }
        )
    else:
        demisto.results("No list of this type was found.")


def get_list_request(list_type_id, page=None, page_size=None):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_GLOBAL_LIST)

    params = {}
    if list_type_id == "GlobalQuarantine":
        params["listTypeId"] = 0
    else:  # List Type ID is GlobalSafe
        params["listTypeId"] = 1
    if page:
        params["page"] = page
    if page_size:
        params["page_size"] = page_size
    res = api_call(uri=URI_LISTS, method="get", access_token=access_token, params=params)
    return res


def get_list_entry_by_hash(sha256=None, list_type_id=None):
    if not sha256:
        sha256 = demisto.args()["sha256"]
    if not list_type_id:
        list_type_id = demisto.args()["listTypeId"]
    total_pages = 0
    current_page = 0
    found_hash = None
    while not found_hash and total_pages >= current_page:
        if not current_page:
            current_page = 1
        lst = get_list_request(list_type_id, current_page, 200)
        if not total_pages:
            total_pages = lst["total_pages"]
        for i in lst["page_items"]:
            if i["sha256"] == sha256:
                found_hash = i
                break
        current_page += 1
    if demisto.command() == "cylance-protect-get-list-entry":
        if found_hash:
            context_list = createContext(data=found_hash, keyTransform=underscoreToCamelCase, removeNull=True)
            ec = {"CylanceListSearch": context_list}
            title = "Cylance Protect Global List Entry"

            demisto.results(
                {
                    "Type": entryTypes["note"],
                    "Contents": found_hash,
                    "ContentsFormat": formats["json"],
                    "ReadableContentsFormat": formats["markdown"],
                    "HumanReadable": tableToMarkdown(title, found_hash, headerTransform=underscoreToCamelCase, removeNull=True),
                    "EntryContext": ec,
                }
            )
            return None
        else:
            demisto.results("Hash not found")
            return None
    else:
        return found_hash


def get_indicators_report():  # pragma: no cover
    url = "https://protect.cylance.com/Reports/ThreatDataReportV1/indicators/" + demisto.args()["token"]
    res = requests.request("GET", url, verify=USE_SSL)
    filename = "Indicators_Report.csv"
    demisto.results(fileResult(filename, res.content))


def update_device_threats():  # pragma: no cover
    device_id = demisto.args()["device_id"]
    threat_id = demisto.args()["threat_id"]
    event = demisto.args()["event"]
    update_device_threats_request(device_id, threat_id, event)
    demisto.results("Device threat was updated successfully.")


def update_device_threats_request(device_id, threat_id, event):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_THREAT_UPDATE)

    body = {"threat_id": threat_id, "event": event}

    uri = f"{URI_DEVICES}/{device_id}/threats"
    res = api_call(uri=uri, method="post", access_token=access_token, body=body)

    return res


def download_threat():
    contents = {}
    context = {}
    dbot_score = 0

    sha256 = demisto.args()["sha256"]
    threat_url = download_threat_request(sha256)

    threat_file = requests.get(threat_url, allow_redirects=True, verify=USE_SSL)
    if threat_file.status_code == 200:
        if demisto.args()["unzip"] == "yes":
            file_archive = BytesIO(threat_file.content)
            zip_file = zipfile.ZipFile(file_archive)
            file_data = zip_file.read(sha256.upper(), pwd=b"infected")
            demisto.results(fileResult(sha256, file_data))
        else:
            demisto.results(fileResult(sha256, threat_file.content + b".zip"))
    else:
        return_error("Could not fetch the file")

    threat = get_threat_request(sha256)
    if threat:
        # add data about the threat if found
        if threat.get("cylance_score"):
            score = normalize_score(threat.get("cylance_score"))
            threshold = demisto.args().get("threshold", FILE_THRESHOLD)
            dbot_score = translate_score(score, int(threshold))

        contents = {
            "Download URL": threat_url,
            "File Name": threat.get("name"),
            "File Size": threat.get("file_size"),
            "Detected By": threat.get("detected_by"),
            "GlobalQuarantine": threat.get("global_quarantined"),
            "Safelisted": threat.get("safelisted"),
            "Timestamp": threat.get("cert_timestamp"),
        }

        context[outputPaths["file"]] = {
            "DownloadURL": threat_url,
            "SHA256": threat.get("sha256"),
            "Name": threat.get("name"),
            "Size": threat.get("file_size"),
            "Safelisted": threat.get("safelisted"),
            "Timestamp": threat.get("cert_timestamp"),
            "MD5": threat.get("md5"),
        }

        if dbot_score == 3:
            context[outputPaths["file"]]["Malicious"] = {
                "Vendor": "Cylance Protect",
                "Description": "Score determined by get threat command",
            }

        context[outputPaths["dbotscore"]] = {
            "Indicator": threat.get("sha256"),
            "Type": "file",
            "Vendor": "Cylance Protect",
            "Score": dbot_score,
        }

    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": contents,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown(
                "Cylance Protect - Downloading threat attached to the following hash: " + sha256, contents
            ),
            "EntryContext": context,
        }
    )


def download_threat_request(hash):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_THREAT_READ)

    uri = "{}/{}/{}".format(URI_THREATS, "download", hash)
    res = api_call(uri=uri, method="get", access_token=access_token)
    if not isinstance(res, dict) or not res.get("url"):
        error_msg = f"No url was found - {res}" if isinstance(res, str) else "No url was found"
        return_error(error_msg)
    return res["url"]


def add_hash_to_list():
    context = {}

    sha256 = demisto.args().get("sha256")
    list_type = demisto.args().get("listType")
    reason = demisto.args().get("reason")
    category = demisto.args().get("category")

    if list_type == "GlobalSafe" and not category:
        return_error("Category argument is required for list type of Global Safe")

    add_hash = add_hash_to_list_request(sha256, list_type, reason, category)
    if not add_hash:
        return_error("Could not add hash to list")

    contents = {"Threat File SHA256": sha256, "List Type": list_type, "Category": category, "Reason": reason}

    context[outputPaths["file"]] = {"SHA256": sha256, "Cylance": {"ListType": list_type, "Category": category}}

    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": contents,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown(
                f"The requested threat has been successfully added to {list_type} hashlist.", contents
            ),
            "EntryContext": context,
        }
    )


def add_hash_to_list_request(sha256, list_type, reason, category=None):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_GLOBAL_LIST_CREATE)

    body = {"sha256": sha256, "list_type": list_type, "reason": reason}
    if category:
        body["category"] = category.replace(" ", "")
    res = api_call(uri=URI_LISTS, method="post", access_token=access_token, body=body)
    return res


def delete_hash_from_lists():
    sha256 = demisto.args().get("sha256")
    list_type = demisto.args().get("listType")
    context = {}

    delete_hash_from_lists_request(sha256, list_type)

    contents = {"Threat File SHA256": sha256, "Threat List Type": list_type}

    context[outputPaths["file"]] = {"SHA256": sha256, "Cylance": {"ListType": list_type}}

    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": contents,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown(
                f"The requested threat has been successfully removed from {list_type} hashlist.", contents
            ),
            "EntryContext": context,
        }
    )


def delete_hash_from_lists_request(sha256, list_type):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_GLOBAL_LIST_DELETE)

    body = {"sha256": sha256, "list_type": list_type}
    res = api_call(uri=URI_LISTS, method="delete", access_token=access_token, body=body)
    return res


def delete_devices():
    device_ids = demisto.args().get("deviceIds")
    device_ids_list = argToList(device_ids)
    contents = []
    context_list = []

    for device_id in device_ids_list:
        device = get_device_request(device_id)
        if not device:
            continue
        device_name = device.get("name")
        context_list.append({"Id": device_id, "Name": device_name, "Deleted": True})
        contents.append({"Device Removed": device_id, "Device Name": device_name, "Deletion status": True})
    batch_size = demisto.args().get("batch_size", 20)
    try:
        batch_size = int(batch_size)
    except ValueError:
        return_error("Error: Batch Size specified must represent an int.")
    for i in range(0, len(device_ids_list), batch_size):
        current_deleted_devices_batch = device_ids_list[i : i + batch_size]
        delete_devices_request(current_deleted_devices_batch)

    context = {"Cylance.Device(val.Id && val.Id == obj.Id)": context_list}

    demisto.results(
        {
            "Type": entryTypes["note"],
            "ContentsFormat": formats["json"],
            "Contents": contents,
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": tableToMarkdown(
                "The requested devices have been successfully removed from your organization list.", contents
            ),
            "EntryContext": context,
        }
    )


def delete_devices_request(device_ids):  # pragma: no cover
    access_token = get_authentication_token()

    body = {"device_ids": device_ids}

    res = api_call(uri=URI_DEVICES, method="delete", access_token=access_token, body=body)
    if not res or not res.get("request_id"):
        return_error("Delete response does not contain request id")

    return res


def get_policy_details():
    policy_id = demisto.args()["policyID"]
    contents = {}  # type: Dict
    context = {}  # type: Dict
    title = "Could not find policy details for that ID"
    filetype_actions_threat_contents = []  # type: list
    filetype_actions_suspicious_contents = []  # type: list
    safelist_contents = []  # type: list
    title_filetype_actions_threat = "Cylance Policy Details - FileType Actions Threat Files"
    title_filetype_actions_suspicious = "Cylance Policy Details - FileType Actions Suspicious Files"
    title_safelist = "Cylance Policy Details - File Exclusions - SafeList"
    title_memory_exclusion = (
        "Cylance Policy Details - Memory Violation Actions \n"
        + "This table provides detailed information about the memory violation settings. \n"
        + "Memory protections Exclusion List :"
    )
    title_memory_violation = "Memory Violation Settings: "
    title_additional_settings = (
        "Cylance Policy Details - Policy Settings. \n" + "Various policy settings are contained within this section."
    )

    policy_details = get_policy_details_request(policy_id)
    memory_violations_content = []

    if policy_details:
        title = "Cylance Policy Details for: " + policy_id
        date_time = ""
        # timestamp in response comes back as bugged string, convert to actual timestamp.
        timestamp = policy_details.get("policy_utctimestamp")
        if timestamp:
            reg = re.search(r"\d{13}", timestamp)
            if reg:
                ts = float(reg.group())
                date_time = datetime.fromtimestamp(ts / 1000).strftime("%Y-%m-%dT%H:%M:%S.%f+00:00")

        context = {"ID": policy_details.get("policy_id"), "Name": policy_details.get("policy_name"), "Timestamp": date_time}

        contents = {"Policy Name": policy_details.get("policy_name"), "Policy Created At": date_time}

        suspicious_files = policy_details.get("filetype_actions").get("suspicious_files")
        if suspicious_files:
            suspicious_files_list = []
            for file in suspicious_files:
                suspicious_files_list.append({"Actions": file.get("actions"), "File Type": file.get("file_type")})

        threat_files = policy_details.get("filetype_actions").get("threat_files")
        if threat_files:
            threat_files_list = []
            for file in threat_files:
                threat_files_list.append({"Actions": file.get("actions"), "File Type": file.get("file_type")})

        filetype_actions_suspicious_contents = suspicious_files_list
        filetype_actions_threat_contents = threat_files_list
        safelist = policy_details.get("file_exclusions")
        if safelist:
            file_exclusions_list = []
            for file_exclusion in safelist:
                file_exclusions_list.append(
                    {
                        "Research Class ID": file_exclusion.get("research_class_id"),
                        "Infinity": file_exclusion.get("infinity"),
                        "File Type": file_exclusion.get("file_type"),
                        "AV Industry": file_exclusion.get("av_industry"),
                        "Cloud Score": file_exclusion.get("cloud_score"),
                        "File Hash": file_exclusion.get("file_hash"),
                        "Research Subclass ID": file_exclusion.get("research_subclass_id"),
                        "Reason": file_exclusion.get("reason"),
                        "File Name": file_exclusion.get("file_name"),
                        "Category Id": file_exclusion.get("category_id"),
                        "MD5": file_exclusion.get("md5"),
                    }
                )

            safelist_contents = file_exclusions_list

        memory_violations = policy_details.get("memoryviolation_actions").get("memory_violations")
        for memory_violation in memory_violations:
            memory_violations_content.append(
                {"Action": memory_violation.get("action"), "Violation Type": memory_violation.get("violation_type")}
            )

        additional_settings = policy_details.get("policy")
        additional_settings_content = []
        for additional_setting in additional_settings:
            additional_settings_content.append({"Name": additional_setting.get("name"), "Value": additional_setting.get("value")})

    context.update(policy_details)
    results = CommandResults(
        outputs=context,
        outputs_prefix="Cylance.Policy",
        outputs_key_field="policy_id",
        readable_output=tableToMarkdown(title, contents)
        + tableToMarkdown(title_filetype_actions_suspicious, filetype_actions_suspicious_contents)
        + tableToMarkdown(title_filetype_actions_threat, filetype_actions_threat_contents)
        + tableToMarkdown(title_safelist, safelist_contents)
        + tableToMarkdown(title_memory_exclusion, policy_details.get("memory_exclusion_list"))
        + tableToMarkdown(title_memory_violation, memory_violations_content)
        + tableToMarkdown(title_additional_settings, memory_violations_content),
        raw_response=policy_details,
    )
    return_results(results)


def get_policy_details_request(policy_id):  # pragma: no cover
    access_token = get_authentication_token(scope=SCOPE_POLICY_READ)

    uri = f"{URI_POLICIES}/{policy_id}"
    res = api_call(uri=uri, method="get", access_token=access_token)
    return res


def create_instaquery_request(name, description, artifact, value_type, match_values, match_type, zone_list):
    # Create request
    data = {
        "name": name,
        "description": description,
        "artifact": artifact,
        "match_value_type": value_type,
        "match_values": match_values,
        "case_sensitive": False,
        "match_type": match_type,
        "zones": zone_list,
    }

    access_token = get_authentication_token([SCOPE_OPTICS_CREATE, SCOPE_OPTICS_GET])
    headers = {"Content-Type": "application/json", "Authorization": "Bearer " + access_token}
    uri = URI_OPTICS
    res = api_call(uri=uri, method="post", body=data, headers=headers)
    return res


def create_instaquery():
    query_args = demisto.args()
    name = query_args.get("name")
    description = query_args.get("description")
    artifact = query_args.get("artifact")
    match_value_type = query_args.get("match_value_type")
    match_values = query_args.get("match_values").split(",")
    match_type = query_args.get("match_type")
    zones = "".join(query_args.get("zone").split("-")).upper()  # Remove '-' and upper case
    zone_list = zones.split(",")

    # Process the match value
    if artifact in match_value_type:
        value_type = re.findall(r"(?<=\.).*", match_value_type)[0]  # Remove the artifact prefix
    else:
        value_type = []
        demisto.error("The value type is not suitable with the selected artifact")

    # Create request
    res = create_instaquery_request(name, description, artifact, value_type, match_values, match_type, zone_list)

    if res:
        # Return results to context and war room
        results = CommandResults(outputs=res, outputs_prefix="InstaQuery.New", outputs_key_field="id")
        return_results(results)


def get_instaquery_result_request(query_id):
    # Create request
    access_token = get_authentication_token([SCOPE_OPTICS_GET, SCOPE_OPTICS_CREATE])
    headers = {"Content-Type": "application/json", "Authorization": "Bearer " + access_token}
    # Endpoint format /instaqueries/v2/{queryID}/results
    uri = URI_OPTICS + "/" + query_id + "/results"
    res = api_call(uri=uri, method="get", headers=headers)
    return res


def get_instaquery_result():
    query_id = demisto.args().get("query_id")
    res = get_instaquery_result_request(query_id)

    if res["result"]:
        results_count = len(res.get("result"))
        result_title = (
            str(results_count) + " results found, find more details in context. Here is the 1st result:"
            if results_count > 1
            else "1 result found:"
        )
        readable_results = tableToMarkdown(result_title, json.loads(res["result"][0]["Result"]).get("Properties"))
    else:
        readable_results = "### No result found"

    # Return results to context and war room
    results = CommandResults(
        outputs=res, outputs_prefix="InstaQuery.Results", outputs_key_field="id", readable_output=readable_results
    )
    return_results(results)


def list_instaquery_request(page, page_size):
    # Create request
    access_token = get_authentication_token([SCOPE_OPTICS_LIST, SCOPE_OPTICS_GET])
    headers = {"Content-Type": "application/json", "Authorization": "Bearer " + access_token}
    # Endpoint format /instaqueries/v2/{queryID}/results
    uri = URI_OPTICS + "?page=" + page + "&page_size=" + page_size
    res = api_call(uri=uri, method="get", headers=headers)
    return res


def list_instaquery():
    page = demisto.args().get("page_number")
    page_size = demisto.args().get("page_size")
    res = list_instaquery_request(page, page_size)
    if res:
        # Return results to context and war room
        results = CommandResults(
            outputs=res,
            outputs_prefix="InstaQuery.List",
        )
        return_results(results)


def fetch_incidents():
    now = datetime.utcnow()
    last_run = demisto.getLastRun().get("time")
    if last_run is None:
        now = now - timedelta(days=3)
        last_run = now
    else:
        last_run = datetime.strptime(last_run, "%Y-%m-%dT%H:%M:%S")  # Converts string to datetime object
    current_run = last_run
    threats = get_threats_request().get("page_items", [])

    incidents = []
    for threat in threats:
        last_found = datetime.strptime(threat["last_found"], "%Y-%m-%dT%H:%M:%S")
        if last_found > last_run:
            incident = threat_to_incident(threat)
            incidents.append(incident)
        if last_found > current_run:
            current_run = last_found

    demisto.incidents(incidents)
    demisto.setLastRun({"time": current_run.isoformat().split(".")[0]})


def add_capitalized_hash_to_context(threats_context):  # pragma: no cover
    """Add capitalized hash keys to the context such as SHA256 and MD5,
    the keys are redundant since they are used for avoiding BC issues.

    Args:
        threats_context(list): list of dicts of context outputs for the threats of interest, each containing
        the key 'Sha256' (and possibly (Md5)).

    Returns:
        threats_context(list): list of dicts of context outputs for the threats of interest, each containing
        the key and value 'Sha256' (and possibly Md5) as well as the key and value 'SHA256' (and possible MD5).
    """
    if not isinstance(threats_context, list):
        threats_context = [threats_context]

    for context_item in threats_context:
        if context_item.get("Sha256"):
            context_item["SHA256"] = context_item.get("Sha256")
        if context_item.get("Md5"):
            context_item["MD5"] = context_item.get("Md5")

    return threats_context


# EXECUTION
def main():  # pragma: no cover
    params = demisto.params()

    global APP_ID
    APP_ID = params.get("app_creds", {}).get("identifier") or params.get("app_id")
    global APP_SECRET
    APP_SECRET = params.get("app_creds", {}).get("password") or params.get("app_secret")
    global TID
    TID = params.get("api_key", {}).get("password") or params.get("tid")
    global SERVER_URL
    SERVER_URL = load_server_url()
    global FILE_THRESHOLD
    FILE_THRESHOLD = params["file_threshold"]
    global USE_SSL
    USE_SSL = not params.get("unsecure", False)
    command = demisto.command()

    if not all((APP_ID, APP_SECRET, TID)):
        raise DemistoException("Please fill out the Application ID, Application Secret and Tenant API Key")

    LOG(f"Command being called is {command}")
    try:
        handle_proxy()
        if demisto.command() == "test-module":
            test()

        if demisto.command() == "fetch-incidents":
            fetch_incidents()

        elif demisto.command() == "cylance-protect-get-devices":
            get_devices()

        elif demisto.command() == "cylance-protect-get-device":
            get_device()

        elif demisto.command() == "cylance-protect-get-device-by-hostname":
            get_device_by_hostname()

        elif demisto.command() == "cylance-protect-update-device":
            update_device()

        elif demisto.command() == "cylance-protect-get-device-threats":
            get_device_threats()

        elif demisto.command() == "cylance-protect-get-policies":
            get_policies()

        elif demisto.command() == "cylance-protect-create-zone":
            create_zone()

        elif demisto.command() == "cylance-protect-get-zones":
            get_zones()

        elif demisto.command() == "cylance-protect-get-zone":
            get_zone()

        elif demisto.command() == "cylance-protect-update-zone":
            update_zone()

        elif demisto.command() == "cylance-protect-get-threat":
            get_threat()

        elif demisto.command() == "cylance-protect-get-threats":
            get_threats()

        elif demisto.command() == "cylance-protect-get-threat-devices":
            get_threat_devices()

        elif demisto.command() == "cylance-protect-get-indicators-report":
            get_indicators_report()

        elif demisto.command() == "cylance-protect-update-device-threats":
            update_device_threats()

        elif demisto.command() == "cylance-protect-get-list":
            get_list()

        elif demisto.command() == "cylance-protect-get-list-entry":
            get_list_entry_by_hash()

        # new commands
        elif demisto.command() == "cylance-protect-download-threat":
            download_threat()

        elif demisto.command() == "cylance-protect-add-hash-to-list":
            add_hash_to_list()

        elif demisto.command() == "cylance-protect-delete-hash-from-lists":
            delete_hash_from_lists()

        elif demisto.command() == "cylance-protect-delete-devices":
            delete_devices()

        elif demisto.command() == "cylance-protect-get-policy-details":
            get_policy_details()

        # Optics InstaQuery command
        elif demisto.command() == "cylance-optics-create-instaquery":
            create_instaquery()

        elif demisto.command() == "cylance-optics-get-instaquery-result":
            get_instaquery_result()

        elif demisto.command() == "cylance-optics-list-instaquery":
            list_instaquery()

    except Warning as w:
        demisto.results({"Type": 11, "Contents": str(w), "ContentsFormat": formats["text"]})

    except Exception as e:
        demisto.error("#### error in Cylance Protect v2: " + str(e))
        if demisto.command() == "fetch-incidents":
            LOG.print_log()
            raise
        else:
            return_error(str(e))


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