Zscaler

Zscaler is a cloud security solution built for performance and flexible scalability. This integration enables you to manage URL and IP address allow lists and block lists, manage and update categories, get Sandbox reports, create, manage, and update IP destination groups and manually log in, log out, and activate changes in a Zscaler session.

Network Security · Zscaler Internet Access

Details

IDZscaler
ProviderZscaler
CategoryNetwork Security
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM EDR Cortex Cloud Cloud Runtime Security

README

Zscaler is a cloud security solution built for performance and flexible scalability. This integration enables you to manage URL and IP address allow lists and block lists, manage and update categories, get Sandbox reports, create, manage, and update IP destination groups and manually log in, log out, and activate changes in a Zscaler session.

For the integration to work properly, the Zscaler user must have admin permissions.

Category ID is the same as the category name, except all letters are capitalized and each word is separated with an underscore instead of spaces. For example, if the category name is Other Education, then the Category ID is OTHER_EDUCATION.

A custom category ID has the format CUSTOM_01, which is not indicative of the category. Use the zscaler-get-categories command to get a custom category and its configured name.

Configure Zscaler Internet Access in Cortex

Parameter Description Required
Cloud Name (i.e. https://zsapi.zscalertwo.net) See https://help.zscaler.com/zia/getting-started-zia-api#RetrieveBaseURL on how to retrieve your cloud name. True
Username   True
Password   True
API Key   True
Source Reliability Reliability of the source providing the intelligence data. False
Auto Logout If enabled, the integration will log out after executing each command. False
Auto Activate Changes If enabled, the integration will activate the command changes after each execution. If disabled, use the ‘zscaler-activate-changes’ command to activate Zscaler command changes. False
Trust any certificate (not secure)   False
Use system proxy settings   False
Timeout (in seconds) for HTTP requests to Zscaler   False

Commands

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

zscaler-blacklist-url


Adds the specified URLs to the block list.

Base Command

zscaler-blacklist-url

Input

Argument Name Description Required
url A comma-separated list of URLs to add to block list. For example, snapchat.com,facebook.com. Required

Context Output

There is no context output for this command.

Command Example

!zscaler-blacklist-url url=phishing.com,malware.net

Human Readable Output

Added the following URLs to the block list successfully:
phishing.com
malware.net

url


Looks up the classification for the each of the specified URLs.

Base Command

url

Input

Argument Name Description Required
url A comma-separated list of URLs for which to look up the classification. For example, abc.com,xyz.com. The maximum number of URLs per call is 100. A URL cannot exceed 1024 characters. If there are multiple URLs, set the ‘multiple’ argument to ‘true’. Required
multiple Whether there are multiple URLs in the ‘url’ argument. If a URL contains commas, set this argument to ‘false’ and enter the single URL as the ‘url’ argument. Possible values are: true, false. Default is true. Optional

Context Output

Path Type Description
URL.Data string The URL that was searched.
URL.Address string The URL that was searched.
Zscaler.URL.urlClassifications string The classification of the URL. For example, MUSIC or WEB_SEARCH.
Zscaler.URL.urlClassificationsWithSecurityAlert string The classifications of the URLs that have security alerts.
URL.Malicious.Vendor string For malicious URLs, the vendor that tagged the URL as malicious.
URL.Malicious.Description string For malicious URLs, the reason the vendor tagged the URL as malicious.
DBotScore.Indicator string The indicator that was tested.
DBotScore.Type string The indicator type.
DBotScore.Vendor string The vendor used to calculate the score.
DBotScore.Score number The actual score.

Command Example

!url url=facebook.com

Context Example

{
    "DBotScore": [
        {
            "Indicator": "facebook.com",
            "Score": 1,
            "Type": "url",
            "Vendor": "Zscaler"
        }
    ],
    "URL": {
        "Address": "facebook.com",
        "Data": "facebook.com",
        "urlClassifications": "SOCIAL_NETWORKING"
    }
}

Human Readable Output

Zscaler URL Lookup

url urlClassifications
facebook.com SOCIAL_NETWORKING

ip


Looks up the classification for each of the specified IP addresses.

Base Command

ip

Input

Argument Name Description Required
ip A comma-separated list of IP address for which to look up the classification. For example, 8.8.8.8,1.2.3.4. The maximum number of URLs per call is 100. An IP address cannot exceed 1024 characters. Required

Context Output

Path Type Description
IP.Address string The IP address that was searched.
Zscaler.IP.ipClassifications string The classification of the IP address. For example, MUSIC or WEB_SEARCH.
Zscaler.IP.iplClassificationsWithSecurityAlert string Classifications that have a security alert for the IP address.
IP.Malicious.Vendor string For malicious IP addresses, the vendor that tagged the IP address as malicious.
IP.Malicious.Description string For malicious IP addresses, the reason the vendor tagged the IP address as malicious.
DBotScore.Indicator string The indicator that was tested.
DBotScore.Type string The indicator type.
DBotScore.Vendor string The vendor used to calculate the score.
DBotScore.Score number The actual score.

Command Example

!ip ip=8.8.8.8

Context Example

{
    "DBotScore": [
        {
            "Indicator": "8.8.8.8",
            "Score": 1,
            "Type": "ip",
            "Vendor": "Zscaler"
        }
    ],
    "IP": {
        "Address": "8.8.8.8",
        "ipClassifications": "WEB_SEARCH"
    }
}

Human Readable Output

Zscaler IP Lookup

ip ipClassifications
8.8.8.8 WEB_SEARCH

domain


Looks up the classification for each of the specified domains.

Base Command

domain

Input

Argument Name Description Required
domain A comma-separated list of domains for which to look up the classification. For example, example.com,google.com. Required
multiple Whether there are multiple domains in the ‘domain’ argument. If a domain contains commas, set this argument to ‘false’ and enter the single domain as the ‘domain’ argument. Possible values are: true, false. Default is true. Optional

Context Output

Path Type Description
Domain.Name string The domain that was searched.
Zscaler.Domain.domainClassifications string The classification of the domain. For example, MUSIC or WEB_SEARCH.
Zscaler.Domain.domainClassificationsWithSecurityAlert string The classifications of the domains that have security alerts.
Domain.Malicious.Vendor string For malicious domains, the vendor that tagged the domain as malicious.
Domain.Malicious.Description string For malicious domains, the reason the vendor tagged the domain as malicious.
DBotScore.Indicator string The indicator that was tested.
DBotScore.Type string The indicator type.
DBotScore.Vendor string The vendor used to calculate the score.
DBotScore.Score number The actual score.

Command Example

!domain domain=example.com

Context Example

{
    "DBotScore": [
        {
            "Indicator": "example.com",
            "Score": 1,
            "Type": "domain",
            "Vendor": "Zscaler"
        }
    ],
    "Domain": {
        "Name": "example.com"
    },
    "Zscaler": {
        "Domain": {
            "Name": "example.com",
            "domainClassifications": "REFERENCE_SITES"
        }
    }
}

Human Readable Output

Zscaler Domain Lookup

domain domainClassifications
example.com REFERENCE_SITES

Command Example with Multiple Domains

!domain domain=example.com,google.com multiple=true

Context Example

{
    "DBotScore": [
        {
            "Indicator": "example.com",
            "Score": 1,
            "Type": "domain",
            "Vendor": "Zscaler"
        },
        {
            "Indicator": "google.com",
            "Score": 1,
            "Type": "domain",
            "Vendor": "Zscaler"
        }
    ],
    "Domain": [
        {
            "Name": "example.com"
        },
        {
            "Name": "google.com"
        }
    ],
    "Zscaler": {
        "Domain": [
            {
                "Name": "example.com",
                "domainClassifications": "REFERENCE_SITES"
            },
            {
                "Name": "google.com",
                "domainClassifications": "WEB_SEARCH"
            }
        ]
    }
}

Human Readable Output

Zscaler Domain Lookup

domain domainClassifications
example.com REFERENCE_SITES
google.com WEB_SEARCH

zscaler-undo-blacklist-url


Removes the specified URLs from the block list.

Base Command

zscaler-undo-blacklist-url

Input

Argument Name Description Required
url A comma-separated list of URLs to remove from the block list. For example, snapchat.com,facebook.com. Required

Context Output

There is no context output for this command.

zscaler-whitelist-url


Adds the specified URLs to the allow list.

Base Command

zscaler-whitelist-url

Input

Argument Name Description Required
url A comma-separated list of URLs to add to the allow list. For example, snapchat.com,facebook.com. Required

Context Output

There is no context output for this command.

Command Example

!zscaler-whitelist-url url=phising.com,malware.net

Human Readable Output

Added the following URLs to the allow list successfully:
phishing.com
malware.net

zscaler-undo-whitelist-url


Removes the specified URLs from the allow list.

Base Command

zscaler-undo-whitelist-url

Input

Argument Name Description Required
url A comma-separated list of URLs to remove from the allow list. For example, snapchat.com,facebook.com. Required

Context Output

There is no context output for this command.

Command Example

!zscaler-undo-whitelist-url url=phising.com,malware.net

Human Readable Output

Removed the following URLs from the allow list successfully:
phishing.com
malware.net

zscaler-undo-whitelist-ip


Removes the specified IP addresses from the allow list.

Base Command

zscaler-undo-whitelist-ip

Input

Argument Name Description Required
ip A comma-separated list of IP addresses to remove from the allow list. For example, 8.8.8.8,1.2.3.4. Required

Context Output

There is no context output for this command.

Command Example

!zscaler-undo-whitelist-ip ip=2.2.2.2,3.3.3.3

Human Readable Output

Removed the following IP addresses from the allow list successfully:
2.2.2.2
3.3.3.3

zscaler-whitelist-ip


Adds the specified IP address to the allow list.

Base Command

zscaler-whitelist-ip

Input

Argument Name Description Required
ip A comma-separated list of IP addresses to add to the allow list. For example, 8.8.8.8,1.2.3.4. Required

Context Output

There is no context output for this command.

Command Example

!zscaler-whitelist-ip ip=2.2.2.2,3.3.3.3

Human Readable Output

Added the following IP addresses to the allow list successfully:
2.2.2.2
3.3.3.3

zscaler-undo-blacklist-ip


Removes the specified IP addresses from the block list.

Base Command

zscaler-undo-blacklist-ip

Input

Argument Name Description Required
ip A comma-separated list of IP addresses to remove from the allow list. For example, 8.8.8.8,1.2.3.4. Required

Context Output

There is no context output for this command.

Command Example

!zscaler-undo-blacklist-ip ip=2.2.2.2,3.3.3.3

Human Readable Output

Removed the following IP addresses from the block list successfully:
2.2.2.2
3.3.3.3

zscaler-blacklist-ip


Adds the specified IP addresses to the block list.

Base Command

zscaler-blacklist-ip

Input

Argument Name Description Required
ip A comma-separated list of IP addresses to add to the block list. For example, 8.8.8.8,1.2.3.4. Required

Context Output

There is no context output for this command.

Command Example

!zscaler-blacklist-ip ip=2.2.2.2,3.3.3.3

Human Readable Output

Added the following IP addresses to the block list successfully:
2.2.2.2
3.3.3.3

zscaler-category-add-url


Adds URLs to the specified category.
Ensure that the URLs are properly formatted according to Zscaler’s guidelines. For more information on valid URL formats, refer to the Zscaler URL Format Guidelines.

Base Command

zscaler-category-add-url

Input

Argument Name Description Required
category-id The ID of the category to add the specified URLs to. For example, RADIO_STATIONS. You can retrieve the category IDs by running the ‘zscaler-get-categories’ command. Required
url A comma-separated list of URLs to add to the specified category. For example, pandora.com,spotify.com.
Important:
If any URL contains a comma (,), you must pass the url argument as a JSON list wrapped in backticks (`).
Example (single URL with comma):
url=`[“https://example.com/foo,bar”]`
Example (multiple URLs with commas):
url=`[“https://example.com/foo,bar”,”https://example2.com/foo,bar”]`
Optional
retaining-parent-category-url A comma-separated list of URLs to add to the retaining parent category section inside the specified category. For example, pandora.com,spotify.com. Optional

Context Output

Path Type Description
Zscaler.Category.CustomCategory boolean True, if the category is a custom category. Otherwise, false.
Zscaler.Category.Description string The description of the category.
Zscaler.Category.ID string The ID of the category.
Zscaler.Category.URL string The URL of the category.

Command Example

!zscaler-category-add-url category-id=MUSIC url=demisto.com,apple.com

Example with a URL containing a comma:

!zscaler-category-add-url category-id="CUSTOM_123" url=`["https://example.com/foo,bar"]`

Context example

{
    "Zscaler": {
      "Category": {
        "CustomCategory": false,
        "Description": "MUSIC_DESC",
        "ID": "MUSIC",
        "URL": [
            "demisto.com",
            "apple.com"
        ]
      }
    }
}

Human Readable Output

Added the following URL addresses to category MUSIC:

  • demisto.com
  • apple.com

zscaler-category-add-ip


Adds IP address to the specified category.

Base Command

zscaler-category-add-ip

Input

Argument Name Description Required
category-id The ID of the category to add the specified IP addresses to. For example, RADIO_STATIONS. You can retrieve the category IDs by running the ‘zscaler-get-categories’ command. Required
ip A comma-separated list of IP address to add to the specified category. For example, 1.2.3.4,8.8.8.8. Optional
retaining-parent-category-ip A comma-separated list of IP address to add to the retaining parent category section inside the specified category. For example, 1.2.3.4,8.8.8.8. Optional

Context Output

Path Type Description
Zscaler.Category.CustomCategory boolean True, if the category is a custom category. Otherwise, false.
Zscaler.Category.Description string The description of the category.
Zscaler.Category.ID string The ID of the category.
Zscaler.Category.URL string The URL of the category

Command Example

!zscaler-category-add-ip category-id=REFERENCE_SITES ip=1.2.3.4,8.8.8.8

Context Example

{
    "Zscaler": {
      "Category": {
        "CustomCategory": false,
        "Description": "REFERENCE_SITES_DESC",
        "ID": "REFERENCE_SITES",
        "URL": [
            "1.2.3.4",
            "8.8.8.8"
        ]
      }
    }
}

Human Readable Output

Added the following IP addresses to category REFERENCE_SITES:

  • 1.2.3.4
  • 8.8.8.8

zscaler-category-remove-url


Removes URLs from the specified category.

Base Command

zscaler-category-remove-url

Input

Argument Name Description Required
category-id The ID of the category to remove the specified URLs from. For example, RADIO_STATIONS. You can retrieve the category IDs by running the ‘zscaler-get-categories’ command. Required
url A comma-separated list of URLs to remove from the specified category. For example, pandora.com,spotify.com. Optional
retaining-parent-category-url A comma-separated list of URLs to remove from the retaining parent category section inside the specified category. For example, pandora.com,spotify.com. Optional

Context Output

Path Type Description
Zscaler.Category.CustomCategory boolean True, if the category is a custom category. Otherwise, false.
Zscaler.Category.Description string The description of the category.
Zscaler.Category.ID string The ID of the category.
Zscaler.Category.URL string The URL of the category.

Command Example

!zscaler-category-remove-url category-id=MUSIC url=apple.com

Context Example

{
    "Zscaler": {
      "Category": {
        "CustomCategory": false,
        "Description": "MUSIC_DESC",
        "ID": "MUSIC",
        "URL": [
            "demisto.com"
        ]
      }
    }
}
Human Readable Output

Removed the following URL addresses to category MUSIC:

  • apple.com

zscaler-category-remove-ip


Removes IP address from the specified category.

Base Command

zscaler-category-remove-ip

Input

Argument Name Description Required
category-id The ID of the category to remove the specified IP addresses from. For example, RADIO_STATIONS. You can retrieve the category IDs by running the ‘zscaler-get-categories’ command. Required
ip A comma-separated list of IP addresses to remove from the specified category. For example, 1.2.3.4,8.8.8.8. Optional
retaining-parent-category-ip A comma-separated list of IP address to remove from the retaining parent category section inside the specified category. For example, 1.2.3.4,8.8.8.8. Optional

Context Output

Path Type Description
Zscaler.Category.CustomCategory boolean True, if the category is a custom category. Otherwise, false.
Zscaler.Category.Description string The description of the category.
Zscaler.Category.ID string The ID of the category.
Zscaler.Category.URL string The URL of the category.

Command Example

!zscaler-category-remove-ip category-id=REFERENCE_SITES ip=1.2.3.4

Context Example
{
    "Zscaler": {
      "Category": {
        "CustomCategory": false,
        "Description": "REFERENCE_SITES_DESC",
        "ID": "REFERENCE_SITES",
        "URL": [
            "8.8.8.8"
        ]
      }
    }
}
Human Readable Output

Removed the following IP addresses to category REFERENCE_SITES:

  • 1.2.3.4

zscaler-get-categories


Retrieves a list of all categories.

Base Command

zscaler-get-categories

Input

Argument Name Description Required
displayURL Whether to display the URLs of each category in the War Room. URLs will always be returned to the Context Data. Possible values are: true, false. Default is false. Optional
custom_categories_only Whether to retrieve only custom categories to the War Room. Possible values are: true, false. Default is false. Optional
get_ids_and_names_only Whether to retrieve only a list containing URL category IDs and names. Even if displayURL is set to true, URLs will not be returned. Please note - the API does not support the combination of custom_only and get_ids_and_names_only. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
Zscaler.Category.ID string The ID of the category.
Zscaler.Category.CustomCategory boolean True, if the category is a custom category. Otherwise, false.
Zscaler.Category.URL string The URL of the category.
Zscaler.Category.RetainingParentCategoryURL string The URLs of the retaining parent category.
Zscaler.Category.Description string The description of the category.
Zscaler.Category.Name string The name of the category.

Command Example


#### Context Example

```json
{  
   "Zscaler":{  
      "Category":{  
         "ID":"INTERNET_SERVICES",
         "Description":"INTERNET_SERVICES_DESC",
         "URL":[  
            "google.com",
            "facebook.com"
         ],
         "CustomCategory":"false"
      },
      "ID":"CUSTOM_01",
      "Name":"CustomCategory",
      "URL":[  
         "demisto.com",
         "apple.com"
      ],
      "RetainingParentCategoryURL":[  
         "pandora.com",
         "spotify.com"
      ],
      "CustomCategory":"true"
   }
}

Human Readable Output

CustomCategory Description ID Name URL RetainingParentCategoryURL
false INTERNET_SERVICES_DESC INTERNET_SERVICES   google.com,facebook.com  
true   CUSTOM_01 CustomCategory demisto.com,apple.com pandora.com”,”spotify.com

zscaler-get-blacklist


Retrieves the Zscaler default block list.

Base Command

zscaler-get-blacklist

Input

Argument Name Description Required
filter Filter results by URL or IP objects. Possible values are: url, ip. Optional
query Query (Python regular expression) to match against. For example, 8...8. Optional

Context Output

Path Type Description
Zscaler.Blacklist string The Zscaler block list.

Command Example


#### Context Example

```json
{
    "Zscaler": {
        "Blacklist": [
            "malicious.com,
            "bad.net"
        ]
    }
}

Human Readable Output

Zscaler block list

  • malicious.com
  • bad.net

zscaler-get-whitelist


Retrieves the Zscaler default allow list.

Base Command

zscaler-get-whitelist

Input

There are no input arguments for this command.

Context Output

Path Type Description
Zscaler.Whitelist string The Zscaler allow list.

Command Example


#### Context Example

```json
{
    "Zscaler": {
        "Whitelist": [
            "demisto.com,
            "apple.com"
        ]
    }
}

Human Readable Output

Zscaler whitelist

  • demisto.com
  • apple.net

zscaler-sandbox-report


Retrieves a full or summary report of the file that was analyzed by Sandbox. The file is represented by the specified MD5 hash.

Base Command

zscaler-sandbox-report

Input

Argument Name Description Required
md5 The MD5 hash of a file. Required
details The type of report. Possible values are ‘full’ or ‘summary’. Default is ‘full’. Optional

Context Output

Path Type Description
File.MD5 string The MD5 hash of the file.
File.Malicious.Vendor string For malicious files, the vendor that tagged the file as malicious.
File.Malicious.Description string For malicious files, the reason the vendor tagged the file as malicious.
File.DetectedMalware string The malware detected in the file.
File.FileType string The file type.
DBotScore.Indicator string The MD5 hash file that was tested.
DBotScore.Type string The MD5 hash file type.
DBotScore.Vendor string The vendor that calculated the DBot score.
DBotScore.Score number The actual DBot score.

Command Example

!zscaler-sandbox-report md5=3FD0EA0AE759D58274310C022FB0CBBA details=summary

Context Example

{
    "DBotScore": {
        "Vendor": "Zscaler", 
        "Indicator": "3FD0EA0AE759D58274310C022FB0CBBA", 
        "Score": 3, 
        "Type": "file"
    }, 
    "File": {
        "Zscaler": {
            "FileType": null, 
            "DetectedMalware": ""
        }, 
        "Malicious": {
            "Vendor": "Zscaler", 
            "Description": "Classified as Malicious, with threat score: 100"
        }, 
        "MD5": "3FD0EA0AE759D58274310C022FB0CBBA"
    }
}

Human Readable Output

Full Sandbox Report
Category Indicator Vendor Score Zscaler Score Type
MALWARE_BOTNET 3FD0EA0AE759D58274310C022FB0CBBA Zscaler 3 100 file

Additional Information

image

image

zscaler-login


Manually create a Zscaler login session. This command will also try to log out of the previous session.

Base Command

zscaler-login

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example


#### Human Readable Output

>Zscaler session created successfully.

### zscaler-logout

***
Logs out of the current Zscaler session.

#### Base Command

`zscaler-logout`

#### Input

There are no input arguments for this command.

#### Context Output

There is no context output for this command.

#### Command Example

```!zscaler-logout```

#### Human Readable Output

>API session logged out of Zscaler successfully.

### zscaler-activate-changes

***
Activates the changes executed by other Zscaler commands in this session.

#### Base Command

`zscaler-activate-changes`

#### Input

There are no input arguments for this command.

#### Context Output

There is no context output for this command.

#### Command Example

```!zscaler-activate-changes```

#### Human Readable Output

>Changes have been activated successfully.

### zscaler-url-quota

***
Gets information on the number of unique URLs that are currently provisioned for your organization as well as how many URLs you can add before reaching that number.

#### Base Command

`zscaler-url-quota`

#### Input

There are no input arguments for this command.

#### Context Output

| **Path** | **Type** | **Description** |
| --- | --- | --- |
| Zscaler.remainingUrlsQuota | Number | The number of URLs you can add before reaching the quota. |
| Zscaler.uniqueUrlsProvisioned | Number | The number of unique URLs that are currently provisioned for your organization. |

### zscaler-get-users

***
Get Zscaler users

#### Base Command

`zscaler-get-users`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| name | Filer by user name. | Optional |
| page | Specifies the page offset. | Optional |
| pageSize | Specifies the page size. Default is 100. | Optional |

#### Context Output

There is no context output for this command.

### zscaler-update-user

***
Updates the user information for the specified ID.

#### Base Command

`zscaler-update-user`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| id | The unique identifier for the user. | Required |
| user | New user information. Docs: <https://help.zscaler.com/zia/api#/User%20Management/updateUser>. | Required |

#### Context Output

There is no context output for this command.

### zscaler-get-departments

***
Get a list of departments. It can be searched by name.

#### Base Command

`zscaler-get-departments`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| name | Filter by department name. | Optional |
| page | Specifies the page offset. | Optional |
| pageSize | Specifies the page size. Default is 100. | Optional |

#### Context Output

There is no context output for this command.

### zscaler-get-usergroups

***
Gets a list of groups

#### Base Command

`zscaler-get-usergroups`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| name | Filter by group name or comment. | Optional |
| page | Specifies the page offset. | Optional |
| pageSize | Specifies the page size. Default is 100. | Optional |

#### Context Output

There is no context output for this command.

### zscaler-create-ip-destination-group

***
Adds a new IP destination group.

#### Base Command

`zscaler-create-ip-destination-group`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| name | Destination IP group name. | Required |
| type | Destination IP group type (i.e., the group can contain destination IP addresses, countries, URL categories or FQDNs). Possible values are: DSTN_IP, DSTN_FQDN, DSTN_DOMAIN, DSTN_OTHER. | Required |
| addresses | Destination IP addresses, FQDNs, or wildcard FQDNs added to the group. | Optional |
| description | Additional information about the destination IP group. | Optional |
| ip_categories | Destination IP address URL categories. You can identify destinations based on the URL category of the domain. To retrieve a list of possible values, you can execute the zscaler-get-categories command. | Optional |
| countries | Destination IP address countries. You can identify destinations based on the location of a server. A list of possible values can be found here <https://help.zscaler.com/zia/firewall-policies#/ipDestinationGroups-post>. | Optional |

#### Context Output

| **Path** | **Type** | **Description** |
| --- | --- | --- |
| Zscaler.IPDestinationGroup.ID | number | Unique identifier for the destination IP group. |
| Zscaler.IPDestinationGroup.Name | string | Destination IP group name. |
| Zscaler.IPDestinationGroup.Type | string | Destination IP group type \(i.e., the group can contain destination IP addresses, countries, URL categories or FQDNs\). |
| Zscaler.IPDestinationGroup.Description | string | Destination IP group description. |
| Zscaler.IPDestinationGroup.Addresses | string | Destination IP addresses, FQDNs, or wildcard FQDNs added to the group. |
| Zscaler.IPDestinationGroup.IpCategories | string | Destination IP address URL categories. You can identify destinations based on the URL category of the domain. |
| Zscaler.IPDestinationGroup.Countries | string | Destination IP address countries. You can identify destinations based on the location of a server. |
| Zscaler.IPDestinationGroup.IsNonEditable | boolean | If set to true, the destination IP address group is non-editable. This field is applicable only to predefined IP address groups, which cannot be modified. |

#### Command Example

```!zscaler-create-ip-destination-group addresses="127.0.0.2,127.0.0.1" description=Localhost name=Test99 type=DSTN_IP```

#### Context example

```json
{
    "Zscaler.IPDestinationGroup": {
        "ID": 2000359, 
        "Name": "Test99", 
        "Type": "DSTN_IP", 
        "Addresses": [
            "127.0.0.2", 
            "127.0.0.1"
        ], 
        "Description": "Localhost",
        "IpCategories": [], 
        "Countries": [], 
        "IsNonEditable": false
    }
}

Human Readable Output

IP Destination group created

Addresses Countries Description ID IpCategories IsNonEditable Name Type
127.0.0.2,
127.0.0.1
  Localhost 2000359   false Test99 DSTN_IP

zscaler-edit-ip-destination-group


Updates the IP destination group information for the specified group ID.

Base Command

zscaler-edit-ip-destination-group

Input

Argument Name Description Required
ip_group_id The unique identifier for the IP destination group. Required
name Destination IP group name. Optional
addresses Destination IP addresses, FQDNs, or wildcard FQDNs added to the group. Optional
description Additional information about the destination IP group. Optional
ip_categories Destination IP address URL categories. You can identify destinations based on the URL category of the domain. To retrieve a list of possible values you can execute the zscaler-get-categories command. Optional
countries Destination IP address countries. You can identify destinations based on the location of a server. A list of possible values can be found here https://help.zscaler.com/zia/firewall-policies#/ipDestinationGroups/{ipGroupId}-put. Optional
is_non_editable If set to true, the destination IP address group is non-editable. This field is applicable only to predefined IP address groups, which cannot be modified. Possible values are: True, False. Default is False. Optional
override When set to true (default), the existing IP destination group configuration is replaced with the new values. When set to false, the existing configuration is preserved and new entries are appended. Possible values are: True, False. Default is True. Optional

Context Output

Path Type Description
Zscaler.IPDestinationGroup.ID number Unique identifier for the destination IP group.
Zscaler.IPDestinationGroup.Name string Destination IP group name.
Zscaler.IPDestinationGroup.Type string Destination IP group type (i.e., the group can contain destination IP addresses, countries, URL categories or FQDNs).
Zscaler.IPDestinationGroup.Description string Destination IP group description.
Zscaler.IPDestinationGroup.Addresses string Destination IP addresses, FQDNs, or wildcard FQDNs added to the group.
Zscaler.IPDestinationGroup.IpCategories string Destination IP address URL categories. You can identify destinations based on the URL category of the domain.
Zscaler.IPDestinationGroup.Countries string Destination IP address countries. You can identify destinations based on the location of a server.

Command Example

!zscaler-edit-ip-destination-group ip_group_id=2000359 addresses="127.0.0.2" description="Localhost v2" name=Test01

Context example

{
    "Zscaler.IPDestinationGroup": {
        "ID": 2000359, 
        "Name": "Test01", 
        "Type": "DSTN_IP", 
        "Description": "Localhost v2",
        "Addresses": [
          "127.0.0.2"
        ],
        "IpCategories": [], 
        "Countries": []
    }
}

Human Readable Output

IP Destination group updated

Addresses Countries Description ID IpCategories Name Type
127.0.0.2   Localhost v2 2000359   Test01 DSTN_IP

zscaler-list-ip-destination-groups


Gets a list of all IP destination groups or the IP destination group information for the specified group ID.

Base Command

zscaler-list-ip-destination-groups

Input

Argument Name Description Required
ip_group_id A comma-separated list of unique identifiers for the IP destination groups. Optional
exclude_type The IP group type to be excluded from the results. Possible values are: DSTN_IP, DSTN_FQDN, DSTN_DOMAIN, DSTN_OTHER. Optional
category_type The IP group type to be filtered from results. This argument is only supported when the ‘lite’ argument is set to True. Possible values are: DSTN_IP, DSTN_FQDN, DSTN_DOMAIN, DSTN_OTHER. Optional
include_ipv6 Retrieve IPv6 destination groups. Possible values are: True, False. Default is False. Optional
limit Limit of the results to be retrieved. Default is 50. Optional
all_results Whether to retrieve all results at once. Possible values are: True, False. Default is False. Optional
lite Whether to retrieve only limited information of IP destination groups. Includes ID, name and type of the IP destination groups. Possible values are: True, False. Default is False. Optional

Context Output

Path Type Description
Zscaler.IPDestinationGroup.ID string Unique identifier for the destination IP group.
Zscaler.IPDestinationGroup.Name string Destination IP group name.
Zscaler.IPDestinationGroup.Type string Destination IP group type (i.e., the group can contain destination IP addresses, countries, URL categories or FQDNs).
Zscaler.IPDestinationGroup.Addresses string Destination IP addresses, FQDNs, or wildcard FQDNs added to the group.
Zscaler.IPDestinationGroup.Description string Additional information about the destination IP group
Zscaler.IPDestinationGroup.IpCategories string Destination IP address URL categories. You can identify destinations based on the URL category of the domain.
Zscaler.IPDestinationGroup.Countries string Destination IP address countries. You can identify destinations based on the location of a server.

Command Example

!zscaler-list-ip-destination-groups exclude_type=DSTN_OTHER

Context example

{
    "Zscaler.IPDestinationGroup": [
    {
        "ID": 1997898, 
        "Name": "Test99", 
        "Type": "DSTN_IP", 
        "Addresses": ["127.0.0.2"], 
        "Description": "Localhost v2", 
        "IpCategories": [], 
        "Countries": []
    },
    {
        "ID": 2001335, 
        "Name": "Test01", 
        "Type": "DSTN_IP", 
        "Addresses": ["127.0.0.1"], 
        "Description": "Localhost v1", 
        "IpCategories": [], 
        "Countries": []
    }      
    ]
}

Human Readable Output

IPv4 Destination groups (2)

Addresses Countries Description ID IpCategories Name Type
127.0.0.2   Localhost v2 1997898   Test99 DSTN_IP
127.0.0.1   Localhost v1 2001335   Test01 DSTN_IP

!zscaler-list-ip-destination-groups lite=True

Context Example

[
    {
        "ID": 1964949, 
        "Name": "Russia-Region", 
        "Type": "DSTN_OTHER"
    }
]

Human Readable Output

IPv4 Destination groups lite (1)

ID Name Type    
  1964949   Russia-Region DSTN_OTHER

zscaler-delete-ip-destination-groups


Deletes the IP destination group associated with the specified group ID.

Base Command

zscaler-delete-ip-destination-groups

Input

Argument Name Description Required
ip_group_ids A comma-separated list of unique identifiers for the IP destination groups. Optional

Context Output

There is no context output for this command.

Command Example

!zscaler-delete-ip-destination-groups ip_group_id=2077311,2030563

Human Readable Output

IP Destination Group 2077311,2030563 deleted successfully

Configuration parameters

  • cloud — Cloud Name (i.e., <https://zsapi.zscalertwo.net>) (required)
  • credentials — Username (required)
  • creds_key
  • key — API Key
  • reliability — Source Reliability
  • auto_logout — Auto Logout
  • auto_activate — Auto Activate Changes
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • requestTimeout — Timeout (in seconds) for HTTP requests to Zscaler

Commands (31)

  • domain

    Looks up the classification for each of the specified Domains.

  • ip

    Looks up the classification for each of the specified IP addresses.

  • url

    Looks up the classification for the each of the specified URLs.

  • zscaler-activate-changes

    Activates the changes executed by other Zscaler commands in this session.

  • zscaler-blacklist-ip

    Adds the specified IP addresses to the block list.

  • zscaler-blacklist-url

    Adds the specified URLs to the block list.

  • zscaler-category-add-ip

    Adds IP address to the specified category.

  • zscaler-category-add-url

    Adds URLs to the specified category.

  • zscaler-category-remove-ip

    Removes IP address from the specified category.

  • zscaler-category-remove-url

    Removes URLs from the specified category.

  • zscaler-create-ip-destination-group

    Adds a new IP destination group.

  • zscaler-delete-ip-destination-groups

    Deletes the IP destination group associated with the specified group ID.

  • zscaler-edit-ip-destination-group

    Updates the IP destination group information for the specified group ID.

  • zscaler-get-blacklist

    Retrieves the Zscaler default block list.

  • zscaler-get-categories

    Retrieves a list of all categories.

  • zscaler-get-departments

    Get a list of departments. It can be searched by name.

  • zscaler-get-usergroups

    Gets a list of groups.

  • zscaler-get-users

    Get Zscaler users.

  • zscaler-get-whitelist

    Retrieves the Zscaler default allow list.

  • zscaler-list-ip-destination-groups

    Gets a list of all IP destination groups or the IP destination group information for the specified group ID.

  • zscaler-login

    Manually create a Zscaler login session. This command will also try to log out of the previous session.

  • zscaler-logout

    Logs out of the current Zscaler session.

  • zscaler-sandbox-report

    Retrieves a full or summary report of the file that was analyzed by Sandbox. The file is represented by the specified MD5 hash.

  • zscaler-undo-blacklist-ip

    Removes the specified IP addresses from the block list.

  • zscaler-undo-blacklist-url

    Removes the specified URLs from the block list.

  • zscaler-undo-whitelist-ip

    Removes the specified IP addresses from the allow list.

  • zscaler-undo-whitelist-url

    Removes the specified URLs from the allow list.

  • zscaler-update-user

    Updates the user information for the specified ID.

  • zscaler-url-quota

    Gets information on the number of unique URLs that are currently provisioned for your organization as well as how many URLs you can add before reaching that number.

  • zscaler-whitelist-ip

    Adds the specified IP address to the allow list.

  • zscaler-whitelist-url

    Adds the specified URLs to the allow list.

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


""" GLOBAL VARS """
ADD = "ADD_TO_LIST"
REMOVE = "REMOVE_FROM_LIST"
INTEGRATION_NAME = "Zscaler"
SUSPICIOUS_CATEGORIES = ["SUSPICIOUS_DESTINATION", "SPYWARE_OR_ADWARE"]
CLOUD_NAME = demisto.params()["cloud"]
USERNAME = demisto.params()["credentials"]["identifier"]
PASSWORD = demisto.params()["credentials"]["password"]
API_KEY = str(demisto.params().get("creds_key", {}).get("password", "")) or str(demisto.params().get("key", ""))
if not API_KEY:
    raise Exception("API Key is missing. Please provide an API Key.")
BASE_URL = CLOUD_NAME + "/api/v1"
USE_SSL = not demisto.params().get("insecure", False)
PROXY = demisto.params().get("proxy", True)
REQUEST_TIMEOUT = int(demisto.params().get("requestTimeout", 15))
DEFAULT_HEADERS = {"content-type": "application/json"}
SESSION_ID_KEY = "session_id"
ERROR_CODES_DICT = {
    400: "Invalid or bad request",
    401: "Session is not authenticated or timed out",
    403: "One of the following permission errors occurred:\n-The API key was disabled by your service provider\n"
    "-User role has no access permissions or functional scope\n-A required SKU subscription is missing\n"
    "Contact support or your account team for assistance.",
    404: "Resource does not exist",
    409: "Request could not be processed because of possible edit conflict occurred. Another admin might be saving a "
    "configuration change at the same time. In this scenario, the client is expected to retry after a short "
    "time period.",
    406: "Not Acceptable",
    415: "Unsupported media type.",
    429: "Exceeded the rate limit or quota.",
    500: "Unexpected error",
    503: "Service is temporarily unavailable",
}
AUTO_ACTIVATE_CHANGES_COMMANDS = (
    "zscaler-blacklist-url",
    "zscaler-undo-blacklist-url",
    "zscaler-whitelist-url",
    "zscaler-undo-whitelist-url",
    "zscaler-blacklist-ip",
    "zscaler-undo-blacklist-ip",
    "zscaler-whitelist-ip",
    "zscaler-undo-whitelist-ip",
    "zscaler-category-add-url",
    "zscaler-category-add-ip",
    "zscaler-category-remove-url",
    "zscaler-category-remove-ip",
    "zscaler-list-ip-destination-groups",
    "zscaler-edit-ip-destination-group",
    "zscaler-create-ip-destination-group",
    "zscaler-delete-ip-destination-groups",
)

""" HANDLE PROXY """
# Remove proxy if not set to true in params
handle_proxy()

""" HELPER CLASSES """


class AuthorizationError(DemistoException):
    """Error to be raised when 401/403 headers are present in http response"""


""" HELPER FUNCTIONS """


def error_handler(res):
    """
    Deals with unsuccessful calls
    """
    if res.status_code in (401, 403):
        raise AuthorizationError(res.content)
    elif res.status_code == 400 and res.request.method == "PUT" and "/urlCategories/" in res.request.url:
        raise Exception(
            f"The request failed with the following error: {res.status_code}.\nMessage: {res.text}\n"
            f"This error might be due to an invalid URL or exceeding your organization's quota.\n"
            f"For more information about URL formatting, refer to the Zscaler URL Format Guidelines: "
            f"https://help.zscaler.com/zia/url-format-guidelines\n"
            f"To check your quota usage, run the command `zscaler-url-quota`."
        )
    else:
        if res.status_code in ERROR_CODES_DICT:
            raise Exception(
                f"The request failed with the following error: {ERROR_CODES_DICT[res.status_code]}.\nMessage: {res.text}"
            )
        else:
            raise Exception(f"The request failed with the following error: {res.status_code}.\nMessage: {res.text}")


def http_request(method, url_suffix, data=None, headers=None, resp_type="json"):
    time_sensitive = is_time_sensitive()
    demisto.debug(f"{time_sensitive=}")
    retries = 0 if time_sensitive else 3
    status_list_to_retry = None if time_sensitive else [429]
    timeout = 2 if time_sensitive else REQUEST_TIMEOUT
    try:
        res = generic_http_request(
            method=method,
            server_url=BASE_URL,
            timeout=timeout,
            verify=USE_SSL,
            proxy=PROXY,
            client_headers=DEFAULT_HEADERS,
            headers=headers,
            url_suffix=url_suffix,
            data=data or {},
            ok_codes=(200, 204),
            error_handler=error_handler,
            retries=retries,
            status_list_to_retry=status_list_to_retry,
            resp_type=resp_type,
        )

    except Exception as e:
        LOG(f"Zscaler request failed with url suffix={url_suffix}\tdata={data}")
        LOG(e)
        raise e
    return res


def validate_urls(urls):
    for url in urls:
        if url.startswith(("http://", "https://", "*")):
            return_error(
                "Enter a valid URL address without an http:// or https:// or * prefix. URL should have at least host."
                "domain pattern to qualify."
            )


""" FUNCTIONS """


def login():
    """
    Try to use integration context if available and valid, otherwise create new session
    """
    cmd_url = "/authenticatedSession"

    def obfuscateApiKey(seed):
        now = str(int(time.time() * 1000))
        n = now[-6:]
        r = str(int(n) >> 1).zfill(6)
        key = ""
        for i in range(0, len(n), 1):
            key += seed[int(n[i])]
        for j in range(0, len(r), 1):
            key += seed[int(r[j]) + 2]
        return now, key

    ctx = get_integration_context() or {}
    session_id = ctx.get(SESSION_ID_KEY)
    if session_id:
        DEFAULT_HEADERS["cookie"] = session_id
        try:
            return test_module()
        except AuthorizationError as e:
            demisto.info(f"Zscaler encountered an authentication error.\nError: {str(e)}")
    ts, key = obfuscateApiKey(API_KEY)
    add_sensitive_log_strs(key)
    data = {"username": USERNAME, "timestamp": ts, "password": PASSWORD, "apiKey": key}
    json_data = json.dumps(data)
    result = http_request("POST", cmd_url, json_data, DEFAULT_HEADERS, resp_type="response")
    auth = result.headers["Set-Cookie"]
    ctx[SESSION_ID_KEY] = DEFAULT_HEADERS["cookie"] = auth[: auth.index(";")]
    set_integration_context(ctx)
    return test_module()


def activate_changes():
    cmd_url = "/status/activate"
    return http_request("POST", cmd_url, None, DEFAULT_HEADERS)


def logout():
    cmd_url = "/authenticatedSession"
    return http_request("DELETE", cmd_url, None, DEFAULT_HEADERS)


def blacklist_url(url):
    urls_to_blacklist = argToList(url)
    validate_urls(urls_to_blacklist)
    cmd_url = "/security/advanced/blacklistUrls?action=ADD_TO_LIST"
    data = {"blacklistUrls": urls_to_blacklist}
    json_data = json.dumps(data)
    http_request("POST", cmd_url, json_data, DEFAULT_HEADERS, resp_type="response")
    list_of_urls = ""
    for url in urls_to_blacklist:
        list_of_urls += "- " + url + "\n"
    return "Added the following URLs to the blacklist successfully:\n" + list_of_urls


def unblacklist_url(url):
    urls_to_unblacklist = argToList(url)
    cmd_url = "/security/advanced/blacklistUrls?action=REMOVE_FROM_LIST"

    # Check if given URLs is blacklisted
    blacklisted_urls = get_blacklist()["blacklistUrls"]
    if len(urls_to_unblacklist) == 1:  # Given only one URL to unblacklist
        if urls_to_unblacklist[0] not in blacklisted_urls:
            raise Exception("Given URL is not blacklisted.")
    elif not any(url in urls_to_unblacklist for url in blacklisted_urls):  # Given more than one URL to blacklist
        raise Exception("Given URLs are not blacklisted.")

    data = {"blacklistUrls": urls_to_unblacklist}
    json_data = json.dumps(data)
    http_request("POST", cmd_url, json_data, DEFAULT_HEADERS, resp_type="response")
    list_of_urls = ""
    for url in urls_to_unblacklist:
        list_of_urls += "- " + url + "\n"
    return "Removed the following URLs from the blacklist successfully:\n" + list_of_urls


def blacklist_ip(ip):
    ips_to_blacklist = argToList(ip)
    cmd_url = "/security/advanced/blacklistUrls?action=ADD_TO_LIST"
    data = {"blacklistUrls": ips_to_blacklist}
    json_data = json.dumps(data)
    http_request("POST", cmd_url, json_data, DEFAULT_HEADERS, resp_type="response")
    list_of_ips = ""
    for ip in ips_to_blacklist:
        list_of_ips += "- " + ip + "\n"
    return "Added the following IP addresses to the blacklist successfully:\n" + list_of_ips


def unblacklist_ip(ip):
    ips_to_unblacklist = argToList(ip)
    cmd_url = "/security/advanced/blacklistUrls?action=REMOVE_FROM_LIST"
    # Check if given IPs is blacklisted
    blacklisted_ips = get_blacklist()["blacklistUrls"]
    if len(ips_to_unblacklist) == 1:  # Given only one IP address to blacklist
        if ips_to_unblacklist[0] not in blacklisted_ips:
            raise Exception("Given IP address is not blacklisted.")
    elif not set(ips_to_unblacklist).issubset(set(blacklisted_ips)):  # Given more than one IP address to blacklist
        raise Exception("Given IP addresses are not blacklisted.")
    data = {"blacklistUrls": ips_to_unblacklist}
    json_data = json.dumps(data)
    http_request("POST", cmd_url, json_data, DEFAULT_HEADERS, resp_type="response")
    list_of_ips = ""
    for ip in ips_to_unblacklist:
        list_of_ips += "- " + ip + "\n"
    return "Removed the following IP addresses from the blacklist successfully:\n" + list_of_ips


def whitelist_url(url):
    cmd_url = "/security"
    urls_to_whitelist = argToList(url)
    # Get the current whitelist
    whitelist_urls = get_whitelist()
    if not whitelist_urls:
        whitelist_urls["whitelistUrls"] = []

    whitelist_urls["whitelistUrls"] += urls_to_whitelist
    json_data = json.dumps(whitelist_urls)
    http_request("PUT", cmd_url, json_data, DEFAULT_HEADERS)
    list_of_urls = ""
    for url in urls_to_whitelist:
        list_of_urls += "- " + url + "\n"
    return "Added the following URLs to the whitelist successfully:\n" + list_of_urls


def unwhitelist_url(url):
    cmd_url = "/security"
    urls_to_unwhitelist = argToList(url)
    # Get the current whitelist
    whitelist_urls = get_whitelist()
    if not whitelist_urls:
        whitelist_urls["whitelistUrls"] = []

    # Check if given URL is whitelisted
    if len(urls_to_unwhitelist) == 1:  # Given only one URL to whitelist
        if urls_to_unwhitelist[0] not in whitelist_urls["whitelistUrls"]:
            raise Exception("Given host address is not whitelisted.")
    elif not set(urls_to_unwhitelist).issubset(set(whitelist_urls["whitelistUrls"])):  # Given more than one URL to whitelist
        raise Exception("Given host addresses are not whitelisted.")
    # List comprehension to remove requested URLs from the whitelist
    whitelist_urls["whitelistUrls"] = [x for x in whitelist_urls["whitelistUrls"] if x not in urls_to_unwhitelist]
    json_data = json.dumps(whitelist_urls)
    http_request("PUT", cmd_url, json_data, DEFAULT_HEADERS)
    list_of_urls = ""
    for url in whitelist_urls:
        list_of_urls += "- " + url + "\n"
    return "Removed the following URLs from the whitelist successfully:\n" + list_of_urls


def whitelist_ip(ip):
    cmd_url = "/security"
    ips_to_whitelist = argToList(ip)
    # Get the current whitelist
    whitelist_ips = get_whitelist()
    if not whitelist_ips:
        whitelist_ips["whitelistUrls"] = []

    whitelist_ips["whitelistUrls"] += ips_to_whitelist
    json_data = json.dumps(whitelist_ips)
    http_request("PUT", cmd_url, json_data, DEFAULT_HEADERS)
    list_of_ips = ""
    for ip in ips_to_whitelist:
        list_of_ips += "- " + ip + "\n"
    return "Added the following URLs to the whitelist successfully:\n" + list_of_ips


def unwhitelist_ip(ip):
    cmd_url = "/security"
    ips_to_unwhitelist = argToList(ip)
    # Get the current whitelist
    whitelist_ips = get_whitelist()
    if not whitelist_ips:
        whitelist_ips["whitelistUrls"] = []

    # Check if given IP is whitelisted
    if len(ips_to_unwhitelist) == 1:  # Given only one IP to whitelist
        if ips_to_unwhitelist[0] not in whitelist_ips["whitelistUrls"]:
            raise Exception("Given IP address is not whitelisted.")
    elif not set(ips_to_unwhitelist).issubset(set(whitelist_ips["whitelistUrls"])):  # Given more than one IP to whitelist
        raise Exception("Given IP address is not whitelisted.")
    # List comprehension to remove requested IPs from the whitelist
    whitelist_ips["whitelistUrls"] = [x for x in whitelist_ips["whitelistUrls"] if x not in ips_to_unwhitelist]
    json_data = json.dumps(whitelist_ips)
    http_request("PUT", cmd_url, json_data, DEFAULT_HEADERS)
    list_of_ips = ""
    for ip in ips_to_unwhitelist:
        list_of_ips += "- " + ip + "\n"
    return "Removed the following IP addresses from the whitelist successfully:\n" + list_of_ips


def get_blacklist_command(args):
    blacklist = get_blacklist().get("blacklistUrls")
    if blacklist:
        filter_ = args.get("filter", "")
        query = args.get("query", "")
        if filter_ or query:
            filtered_blacklist = []
            for entity in blacklist:
                # if filter / query were not provided, then there is a match on it vacuously
                is_filter_match = not filter_
                is_query_match = not query
                if filter_:
                    if re.match(ipv4Regex, entity):
                        if filter_ == "ip":
                            is_filter_match = True
                    elif filter_ == "url":
                        is_filter_match = True
                if query:
                    if re.search(query, entity):
                        is_query_match = True
                    else:
                        is_query_match = False
                if is_filter_match and is_query_match:
                    filtered_blacklist.append(entity)
            blacklist = filtered_blacklist
    if blacklist:
        hr = "### Zscaler blacklist\n"
        for url in blacklist:
            hr += "- " + url + "\n"
        ec = {"Zscaler.Blacklist": blacklist}
        entry = {
            "Type": entryTypes["note"],
            "Contents": blacklist,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": hr,
            "EntryContext": ec,
        }
        return entry
    else:
        return "No results found"


def get_blacklist():
    cmd_url = "/security/advanced"
    result = http_request("GET", cmd_url, None, DEFAULT_HEADERS, resp_type="content")
    return json.loads(result)


def get_whitelist_command():
    whitelist = get_whitelist().get("whitelistUrls")
    if whitelist:
        hr = "### Zscaler whitelist\n"
        for url in whitelist:
            hr += "- " + url + "\n"
        ec = {"Zscaler.Whitelist": whitelist}
        entry = {
            "Type": entryTypes["note"],
            "Contents": whitelist,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": hr,
            "EntryContext": ec,
        }
        return entry
    else:
        return "No results found"


def get_whitelist():
    cmd_url = "/security"
    result = http_request("GET", cmd_url, None, DEFAULT_HEADERS, resp_type="content")
    return json.loads(result)


def url_lookup(args):
    url = args.get("url", "")
    multiple = args.get("multiple", "true").lower() == "true"
    response = lookup_request(url, multiple)
    raw_res = json.loads(response)

    urls_list = argToList(url)
    results: List[CommandResults] = []

    for data in raw_res:
        res_url = data.get("url")
        for url in urls_list:
            # since zscaler expects to recieve a URL without the protocol, we omit it in `lookup_request`
            # in the response, the URL is returned as it was sent, so we add back the protocol by replacing
            # the URL retruned with the one we got as an argument
            if "http://" + res_url in url or "https://" + res_url in url:
                data["url"] = url

        ioc_context = {"Address": data["url"], "Data": data["url"]}
        score = Common.DBotScore.GOOD

        if len(data["urlClassifications"]) == 0:
            data["urlClassifications"] = ""
        else:
            data["urlClassifications"] = "".join(data["urlClassifications"])
            ioc_context["urlClassifications"] = data["urlClassifications"]
            if data["urlClassifications"] == "MISCELLANEOUS_OR_UNKNOWN":
                score = Common.DBotScore.NONE

        if len(data["urlClassificationsWithSecurityAlert"]) == 0:
            data["urlClassificationsWithSecurityAlert"] = ""
        else:
            data["urlClassificationsWithSecurityAlert"] = "".join(data["urlClassificationsWithSecurityAlert"])
            ioc_context["urlClassificationsWithSecurityAlert"] = data["urlClassificationsWithSecurityAlert"]
            if data["urlClassificationsWithSecurityAlert"] in SUSPICIOUS_CATEGORIES:
                score = Common.DBotScore.SUSPICIOUS
            else:
                score = Common.DBotScore.BAD

            data["ip"] = data.pop("url")

        url_indicator = Common.URL(
            url=ioc_context["Data"],
            dbot_score=Common.DBotScore(
                indicator=ioc_context["Data"],
                indicator_type=DBotScoreType.URL,
                integration_name=INTEGRATION_NAME,
                malicious_description=data.get("urlClassificationsWithSecurityAlert", None),
                score=score,
                reliability=demisto.params().get("reliability"),
            ),
        )

        results.append(
            CommandResults(
                outputs_prefix=f"{INTEGRATION_NAME}.URL",
                outputs_key_field="Data",
                indicator=url_indicator,
                readable_output=tableToMarkdown(
                    f'Zscaler URL Lookup for {ioc_context["Data"]}',
                    data,
                    removeNull=True,
                ),
                outputs=createContext(data=ioc_context, removeNull=True),
                raw_response=data,
            )
        )

    return results or "No results found."


def ip_lookup(ip):
    results: List[CommandResults] = []

    response = lookup_request(ip, multiple=True)
    raw_res = json.loads(response)

    for data in raw_res:
        ioc_context = {"Address": data["url"]}
        score = Common.DBotScore.GOOD

        if len(data["urlClassifications"]) == 0:
            data["iplClassifications"] = ""
        else:
            data["ipClassifications"] = "".join(data["urlClassifications"])
            ioc_context["ipClassifications"] = data["ipClassifications"]

        del data["urlClassifications"]

        if len(data["urlClassificationsWithSecurityAlert"]) == 0:
            data["ipClassificationsWithSecurityAlert"] = ""
        else:
            data["ipClassificationsWithSecurityAlert"] = "".join(data["urlClassificationsWithSecurityAlert"])
            ioc_context["ipClassificationsWithSecurityAlert"] = data["ipClassificationsWithSecurityAlert"]
            if data["urlClassificationsWithSecurityAlert"] in SUSPICIOUS_CATEGORIES:
                score = Common.DBotScore.SUSPICIOUS
            else:
                score = Common.DBotScore.BAD

        del data["urlClassificationsWithSecurityAlert"]

        data["ip"] = data.pop("url")

        ip_indicator = Common.IP(
            ip=data["ip"],
            dbot_score=Common.DBotScore(
                indicator=data["ip"],
                indicator_type=DBotScoreType.IP,
                integration_name=INTEGRATION_NAME,
                malicious_description=data.get("ipClassificationsWithSecurityAlert", None),
                score=score,
                reliability=demisto.params().get("reliability"),
            ),
        )
        results.append(
            CommandResults(
                outputs_prefix=f"{INTEGRATION_NAME}.IP",
                indicator=ip_indicator,
                outputs_key_field="Address",
                readable_output=tableToMarkdown(
                    f'Zscaler IP Lookup for {ioc_context["Address"]}',
                    data,
                    removeNull=True,
                ),
                outputs=createContext(data=ioc_context, removeNull=True),
                raw_response=data,
            )
        )

    return results or "No results found."


def domain_lookup(args):
    domain_arg = args.get("domain", "")
    multiple = argToBoolean(args.get("multiple", "true"))
    response = lookup_request(domain_arg, multiple)
    raw_res = json.loads(response)

    results: List[CommandResults] = []

    for data in raw_res:
        res_domain = data.get("url")

        ioc_context = {"Name": res_domain}
        score = Common.DBotScore.GOOD

        if not data.get("urlClassifications", []):
            data["domainClassifications"] = ""
            ioc_context["domainClassifications"] = ""
        else:
            data["domainClassifications"] = "".join(data["urlClassifications"])
            ioc_context["domainClassifications"] = data["domainClassifications"]
            if data["domainClassifications"] == "MISCELLANEOUS_OR_UNKNOWN":
                score = Common.DBotScore.NONE

        data.pop("urlClassifications", None)

        if not data.get("urlClassificationsWithSecurityAlert", []):
            data["domainClassificationsWithSecurityAlert"] = ""
            ioc_context["domainClassificationsWithSecurityAlert"] = ""
        else:
            data["domainClassificationsWithSecurityAlert"] = "".join(data["urlClassificationsWithSecurityAlert"])
            ioc_context["domainClassificationsWithSecurityAlert"] = data["domainClassificationsWithSecurityAlert"]
            if data["domainClassificationsWithSecurityAlert"] in SUSPICIOUS_CATEGORIES:
                score = Common.DBotScore.SUSPICIOUS
            else:
                score = Common.DBotScore.BAD

        data.pop("urlClassificationsWithSecurityAlert", None)

        data["domain"] = data.pop("url", None)

        domain_indicator = Common.Domain(
            domain=ioc_context["Name"],
            dbot_score=Common.DBotScore(
                indicator=ioc_context["Name"],
                indicator_type=DBotScoreType.DOMAIN,
                integration_name=INTEGRATION_NAME,
                malicious_description=data.get("domainClassificationsWithSecurityAlert", None),
                score=score,
                reliability=demisto.params().get("reliability"),
            ),
        )

        results.append(
            CommandResults(
                outputs_prefix=f"{INTEGRATION_NAME}.Domain",
                outputs_key_field="Name",
                indicator=domain_indicator,
                readable_output=tableToMarkdown(
                    f'Zscaler Domain Lookup for {ioc_context["Name"]}',
                    data,
                    removeNull=True,
                ),
                outputs=createContext(data=ioc_context, removeNull=True),
                raw_response=data,
            )
        )

    return results or "No results found."


def lookup_request(ioc, multiple=True):
    cmd_url = "/urlLookup"
    if multiple:
        ioc_list = argToList(ioc)
    else:
        ioc_list = [ioc]
    ioc_list = [url.replace("https://", "").replace("http://", "") for url in ioc_list]
    json_data = json.dumps(ioc_list)
    response = http_request("POST", cmd_url, json_data, DEFAULT_HEADERS, resp_type="content")
    return response


def category_add(category_id, data, retaining_parent_category_data, data_type, category_name=None):
    if not any((data, retaining_parent_category_data)):
        return_error(f"Either {data_type} argument or retaining-parent-category-{data_type} argument must be provided.")

    category_data = get_category_by_id(category_id, category_name)
    demisto.debug(f"{category_data=}")
    if category_data:  # check if the category exists
        data_list = argToList(data)
        all_data = data_list[:]
        all_data.extend([x.strip() for x in category_data["urls"]])
        category_data["urls"] = all_data
        retaining_parent_category_data_list = argToList(retaining_parent_category_data)
        if not any((data_list, retaining_parent_category_data_list)):
            return_error(f"Either {data_type} argument or retaining-parent-category-{data_type} argument must be provided.")

        add_or_remove_urls_from_category(
            ADD, data_list, category_data, retaining_parent_category_data_list, category_name
        )  # add the urls to the category
        context = {
            "ID": category_id,
            "CustomCategory": category_data.get("customCategory"),
            "URL": category_data.get("urls"),
        }
        if category_data.get("description"):  # Custom might not have description
            context["Description"] = category_data["description"]
        ec = {"Zscaler.Category(val.ID && val.ID === obj.ID)": context}

        added_data = "\n".join(f"- {item}" for item in data_list) + "\n".join(
            f"- {item}" for item in retaining_parent_category_data_list
        )
        hr = (
            f"Added the following {data_type.upper()}, retaining-parent-category-{data_type} "
            f"addresses to category {category_id}:\n{added_data}\n"
        )
        entry = {
            "Type": entryTypes["note"],
            "Contents": category_data,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": hr,
            "EntryContext": ec,
        }
        return entry
    else:
        return return_error("Category could not be found.")


def category_remove(category_id, data, retaining_parent_category_data, data_type, category_name=None):
    if not any((data, retaining_parent_category_data)):
        return_error(f"Either {data_type} argument or retaining-parent-category-{data_type} argument must be provided.")

    category_data = get_category_by_id(category_id, category_name)  # check if the category exists
    demisto.debug(f"{category_data=}")

    if category_data:
        removed_data = ""
        data_list = []
        retaining_parent_category_data_list = []

        if data:
            data_list = argToList(data)
            updated_data = [item for item in category_data["urls"] if item not in data_list]
            if updated_data == category_data["urls"]:
                return return_error(f"Could not find given {data_type.upper()} in the category.")
            category_data["urls"] = updated_data
            for item in data_list:
                removed_data += f"- {item}\n"

        if retaining_parent_category_data:
            retaining_parent_category_data_list = argToList(retaining_parent_category_data)
            for item in retaining_parent_category_data_list:
                removed_data += f"- {item}\n"

        add_or_remove_urls_from_category(
            REMOVE, data_list, category_data, retaining_parent_category_data_list, category_name
        )  # remove the urls from list

        context = {
            "ID": category_id,
            "CustomCategory": category_data.get("customCategory"),
            "URL": category_data.get("urls"),
        }
        if category_data.get("description"):  # Custom might not have description
            context["Description"] = category_data["description"]

        hr = f"Removed the following {data_type.upper()} addresses to category {category_id}:\n{removed_data}"

        ec = {"Zscaler.Category(val.ID && val.ID === obj.ID)": context}
        entry = {
            "Type": entryTypes["note"],
            "Contents": category_data,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": hr,
            "EntryContext": ec,
        }
        return entry
    else:
        return return_error("Category could not be found.")


def add_or_remove_urls_from_category(action, urls, category_data, retaining_parent_category_data=None, category_name=None):
    """
    Add or remove urls from a category.
    Args:
        str action: The action requested, can be 'ADD_TO_LIST' for adding or 'REMOVE_FROM'_LIST for removing.
        List[Any] urls: the list of urls to add or remove from the category
        Dict[str: Any] category_data: the data of the category as returned from the API
        List[Any] retaining_parent_category_data: optional list of URLs to add/remove from dbCategorizedUrls
        str category_name: optional category name to use (required for custom categories in newer API versions)

    Returns:
        The response as returned from the API

    """

    demisto.debug("##### add_or_remove_urls_from_category function is now running")
    cmd_url = "/urlCategories/" + category_data.get("id") + "?action=" + action
    data = {
        "customCategory": category_data.get("customCategory"),
        "urls": urls,
        "id": category_data.get("id"),
    }
    if retaining_parent_category_data:
        data["dbCategorizedUrls"] = retaining_parent_category_data
    if "description" in category_data:
        data["description"] = category_data["description"]

    # Per Zscaler API documentation: If you are modifying a custom URL category, this request must
    # additionally specify the configuredName and superCategory information in the request Body.
    # For predefined categories, these fields are optional but included if available.
    configured_name = category_name or category_data.get("configuredName")
    super_category = category_data.get("superCategory")
    is_custom = argToBoolean(category_data.get("customCategory"))

    if is_custom and not configured_name:
        raise DemistoException(
            "The 'configuredName' field is required for custom categories but was not found in the category data. "
            "Please provide the category name using the 'category-name' argument. "
            "You can find the category name by running 'zscaler-get-categories' command."
        )

    if configured_name:
        data["configuredName"] = configured_name
    if super_category:
        data["superCategory"] = super_category

    demisto.debug(f"{data=}")
    json_data = json.dumps(data)
    http_request("PUT", cmd_url, json_data)  # if the request is successful, it returns an empty response


def url_quota_command():
    cmd_url = "/urlCategories/urlQuota"
    response = http_request("GET", cmd_url)

    human_readable = {
        "Unique Provisioned URLs": response.get("uniqueUrlsProvisioned"),
        "Remaining URLs Quota": response.get("remainingUrlsQuota"),
    }
    entry = {
        "Type": entryTypes["note"],
        "Contents": response,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown("Quota Information", human_readable),
        "EntryContext": {"Zscaler.Quota": response},
    }
    return entry


def get_categories_command(args):
    display_urls = argToBoolean(args.get("displayURL"))  # urls returned to context data even if set to false
    custom_only = argToBoolean(args.get("custom_categories_only", False))
    ids_and_names_only = argToBoolean(args.get("get_ids_and_names_only", False))  # won't get URLs at all
    categories = []
    raw_categories = get_categories(custom_only, ids_and_names_only)
    for raw_category in raw_categories:
        category = {
            "ID": raw_category["id"],
            "CustomCategory": raw_category["customCategory"],
        }
        if raw_category.get("urls"):
            category["URL"] = raw_category["urls"]
        if raw_category.get("dbCategorizedUrls"):
            category["RetainingParentCategoryURL"] = raw_category["dbCategorizedUrls"]
        if "description" in raw_category:
            category["Description"] = raw_category["description"]
        if "configuredName" in raw_category:
            category["Name"] = raw_category["configuredName"]
        categories.append(category)
    ec = {"Zscaler.Category(val.ID && val.ID === obj.ID)": categories}
    if display_urls and not ids_and_names_only:
        headers = ["ID", "Description", "URL", "RetainingParentCategoryURL", "CustomCategory", "Name"]
    else:
        headers = ["ID", "Description", "CustomCategory", "Name"]
    title = "Zscaler Categories"
    entry = {
        "Type": entryTypes["note"],
        "Contents": raw_categories,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown(title, categories, headers),
        "EntryContext": ec,
    }
    return entry


def get_categories(custom_only=False, ids_and_names_only=False):
    if ids_and_names_only:
        # if you only want a list of URL category IDs and names (i.e without urls list).
        # Note: API does not support the combination of custom_only and 'lite' endpoint
        cmd_url = "/urlCategories/lite"
    else:
        cmd_url = "/urlCategories?customOnly=true" if custom_only else "/urlCategories"

    response = http_request("GET", cmd_url)
    return response


def sandbox_report_command():
    md5 = demisto.getArg("md5")
    details = demisto.getArg("details")
    res = sandbox_report(md5, details)

    report = "Full Details" if details == "full" else "Summary"
    ctype = demisto.get(res, f"{report}.Classification.Type")
    dbot_score = 3 if ctype == "MALICIOUS" else 2 if ctype == "SUSPICIOUS" else 1 if ctype == "BENIGN" else 0

    ec = {
        outputPaths["dbotscore"]: {
            "Indicator": md5,
            "Type": "file",
            "Vendor": "Zscaler",
            "Score": dbot_score,
            "Reliability": demisto.params().get("reliability"),
        }
    }

    human_readable_report = ec["DBotScore"].copy()
    human_readable_report["Detected Malware"] = str(demisto.get(res, f"{report}.Classification.DetectedMalware"))
    human_readable_report["Zscaler Score"] = demisto.get(res, f"{report}.Classification.Score")
    human_readable_report["Category"] = demisto.get(res, f"{report}.Classification.Category")
    ec[outputPaths["file"]] = {
        "MD5": md5,
        "Zscaler": {
            "DetectedMalware": demisto.get(res, f"{report}.Classification.DetectedMalware"),
            "FileType": demisto.get(res, f"{report}.File Properties.File Type"),
        },
    }
    if dbot_score == 3:
        ec[outputPaths["file"]]["Malicious"] = {
            "Vendor": "Zscaler",
            "Description": "Classified as Malicious, with threat score: " + str(human_readable_report["Zscaler Score"]),
        }
    entry = {
        "Type": entryTypes["note"],
        "Contents": res,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": tableToMarkdown("Full Sandbox Report", human_readable_report, removeNull=True),
        "EntryContext": ec,
    }

    return entry


def sandbox_report(md5, details):
    cmd_url = f"/sandbox/report/{md5}?details={details}"

    response = http_request("GET", cmd_url)
    return response


def login_command():
    ctx = get_integration_context() or {}
    session_id = ctx.get(SESSION_ID_KEY)
    if session_id:
        try:
            DEFAULT_HEADERS["cookie"] = session_id
            demisto.info("Zscaler logout active session triggered by zscaler-login command.")
            logout()
        except Exception as e:
            demisto.info(f"Zscaler logout failed with: {str(e)}")
    login()
    return CommandResults(readable_output="Zscaler session created successfully.")


def logout_command():
    ctx = get_integration_context() or {}
    session_id = ctx.get(SESSION_ID_KEY)
    if not session_id:
        return CommandResults(readable_output="No API session was found. No action was performed.")
    try:
        DEFAULT_HEADERS["cookie"] = session_id
        raw_res = logout()
    except AuthorizationError:
        return CommandResults(readable_output="API session is not authenticated. No action was performed.")
    return CommandResults(
        readable_output="API session logged out of Zscaler successfully.",
        raw_response=raw_res,
    )


def activate_command():
    raw_res = activate_changes()
    return CommandResults(
        readable_output="Changes have been activated successfully.",
        raw_response=raw_res,
    )


def test_module():
    http_request("GET", "/status", None, DEFAULT_HEADERS)
    return "ok"


def get_category_by_id(category_id, category_name=None):
    """
    Get category data by ID.

    Args:
        category_id: The category ID to look up
        category_name: Optional category name to inject into the result for custom categories

    Returns:
        The category data dict, or None if not found
    """
    categories = get_categories()
    for category in categories:
        if category["id"] == category_id:
            # If category_name is provided and the category doesn't have configuredName, add it
            if category_name and "configuredName" not in category:
                category["configuredName"] = category_name
            return category
    return None


def get_users_command(args):
    name = args.get("name", None)
    pageSize = args.get("pageSize")
    pageNo = args.get("page", 1)
    if name is not None:
        cmd_url = f"/users?page={pageNo}&pageSize={pageSize}&name={name}"
    else:
        cmd_url = f"/users?page={pageNo}&pageSize={pageSize}"
    response = http_request("GET", cmd_url)

    if len(response) < 10:
        human_readable = tableToMarkdown(f"Users ({len(response)})", response)
    else:
        human_readable = f"Retrieved {len(response)} users"

    entry = {
        "Type": entryTypes["note"],
        "Contents": response,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": human_readable,
        "EntryContext": {"Zscaler.Users": response},
    }
    return entry


def get_departments_command(args):
    name = args.get("name", None)
    pageSize = args.get("pageSize")
    pageNo = args.get("page", 1)
    if name is not None:
        cmd_url = f"/departments?page={pageNo}&pageSize={pageSize}&search={name}&limitSearch=true"
    else:
        cmd_url = f"/departments?page={pageNo}&pageSize={pageSize}"
    response = http_request("GET", cmd_url)

    if len(response) < 10:
        human_readable = tableToMarkdown(f"Departments ({len(response)})", response)
    else:
        human_readable = f"Retrieved {len(response)} departments"

    entry = {
        "Type": entryTypes["note"],
        "Contents": response,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": human_readable,
        "EntryContext": {"Zscaler.Departments": response},
    }
    return entry


def get_usergroups_command(args):
    name = args.get("name", None)
    pageSize = args.get("pageSize")
    pageNo = args.get("page", 1)
    if name is not None:
        cmd_url = f"/groups?page={pageNo}&pageSize={pageSize}&search={name}"
    else:
        cmd_url = f"/groups?page={pageNo}&pageSize={pageSize}"
    response = http_request("GET", cmd_url)

    if len(response) < 10:
        human_readable = tableToMarkdown(f"User groups ({len(response)})", response)
    else:
        human_readable = f"Retrieved {len(response)} user groups"

    entry = {
        "Type": entryTypes["note"],
        "Contents": response,
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": human_readable,
        "EntryContext": {"Zscaler.UserGroups": response},
    }
    return entry


def set_user_command(args):
    userId = args.get("id")
    params = json.loads(args.get("user"))
    cmd_url = f"/users/{userId}"

    response = http_request("PUT", cmd_url, json.dumps(params), DEFAULT_HEADERS, resp_type="response")
    responseJson = response.json()
    if response.status_code == 200:
        entry = {
            "Type": entryTypes["note"],
            "Contents": responseJson,
            "ContentsFormat": formats["json"],
            "ReadableContentsFormat": formats["markdown"],
            "HumanReadable": "Successfully updated the user (id: {} name: {})".format(responseJson["id"], responseJson["name"]),
            "EntryContext": {"Zscaler.Users": responseJson},
        }
        return entry
    else:
        return responseJson


def create_ip_destination_group(args: dict):
    headers = [
        "ID",
        "Name",
        "Type",
        "Description",
        "Addresses",
        "Countries",
        "IpCategories",
        "IsNonEditable",
    ]
    payload = {
        "name": args.get("name", ""),
        "type": args.get("type", ""),
        "countries": argToList(args.get("countries", "")),
        "ipCategories": argToList(args.get("ip_categories", "")),
        "description": args.get("description", ""),
        "addresses": argToList(args.get("addresses", "")),
        "isNonEditable": args.get("is_non_editable", False),
    }
    cmd_url = "/ipDestinationGroups"
    response = http_request("POST", cmd_url, data=json.dumps(payload), headers=DEFAULT_HEADERS)
    content = {
        "ID": int(response.get("id", "")),
        "Name": response.get("name", ""),
        "Type": response.get("type", ""),
        "Description": response.get("description", ""),
        "Addresses": response.get("addresses", []),
        "IpCategories": response.get("ipCategories", []),
        "Countries": response.get("countries", []),
        "IsNonEditable": response.get("isNonEditable", False),
    }
    markdown = tableToMarkdown("IPv4 Destination group created", content, headers, removeNull=True)
    results = CommandResults(
        readable_output=markdown,
        outputs_prefix="Zscaler.IPDestinationGroup",
        outputs_key_field="ID",
        outputs=content,
    )
    return results


def list_ip_destination_groups(args: dict):
    ip_group_ids = argToList(args.get("ip_group_id", ""))
    exclude_type = str(args.get("exclude_type", "")).strip()
    category_type = argToList(args.get("type", ""))
    include_ipv6 = argToBoolean(args.get("include_ipv6", False))
    limit = arg_to_number(args.get("limit", 50))
    all_results = argToBoolean(args.get("all_results", False))
    lite = argToBoolean(args.get("lite", False))
    headers = [
        "ID",
        "Name",
        "Type",
        "Description",
        "Addresses",
        "Countries",
        "IpCategories",
    ]

    def get_contents(responses: List[dict]):
        contents = []
        for response in responses:
            content = {
                "ID": int(response.get("id", "")),
                "Name": response.get("name", ""),
                "Type": response.get("type", ""),
                "Description": response.get("description", ""),
                "Addresses": response.get("addresses", []),
                "IpCategories": response.get("ipCategories", []),
                "Countries": response.get("countries", []),
            }
            contents.append(content)
        return contents

    def get_contents_lite(responses: List[dict]):
        contents = []
        for response in responses:
            content = {}
            for key, value in response.items():
                if key == "extensions":
                    for extensions_key, extensions_value in value.items():
                        content[f"{extensions_key.capitalize()}"] = extensions_value
                elif key == "id":
                    content[key.upper()] = value
                else:
                    content[key.capitalize()] = value
            contents.append(content)
        return contents

    if len(ip_group_ids) == 0:
        lite_endpoint = "/lite" if lite else ""
        if exclude_type:
            exclude_type_param = f"?excludeType={exclude_type}&"
        else:
            exclude_type_param = "?"
        type_params = [f"type={t}" for t in category_type]
        type_params_str = "&".join(type_params)
        if include_ipv6:
            ipv4_cmd_url = "/ipDestinationGroups" + lite_endpoint + exclude_type_param + type_params_str
            ipv6_cmd_url = "/ipDestinationGroups/ipv6DestinationGroups" + lite_endpoint + exclude_type_param + type_params_str
            ipv4_responses = http_request("GET", ipv4_cmd_url)
            ipv6_responses = http_request("GET", ipv6_cmd_url)
            ipv4_contents_filter = get_contents_lite(ipv4_responses) if lite else get_contents(ipv4_responses)
            ipv4_contents = ipv4_contents_filter if all_results else ipv4_contents_filter[:limit]
            ipv6_contents_filter = get_contents_lite(ipv6_responses) if lite else get_contents(ipv6_responses)
            ipv6_contents = ipv6_contents_filter if all_results else ipv6_contents_filter[:limit]
            markdown = tableToMarkdown(
                f"IPv4 Destination groups ({len(ipv4_contents)})",
                ipv4_contents,
                headers,
                removeNull=True,
            )
            markdown += tableToMarkdown(
                f"IPv6 Destination groups ({len(ipv6_contents)})",
                ipv6_contents,
                headers,
                removeNull=True,
            )
            contents = ipv4_contents + ipv6_contents

            results = CommandResults(
                readable_output=markdown,
                outputs_prefix="Zscaler.IPDestinationGroup",
                outputs_key_field="ID",
                outputs=contents,
            )
            return results
        else:
            cmd_url = "/ipDestinationGroups" + lite_endpoint + exclude_type_param + type_params_str
            responses = http_request("GET", cmd_url)
            contents_filter = get_contents_lite(responses) if lite else get_contents(responses)
            contents = contents_filter if all_results else contents_filter[:limit]
            markdown = tableToMarkdown(
                f"IPv4 Destination groups ({len(contents)})",
                contents,
                headers,
                removeNull=True,
            )

            results = CommandResults(
                readable_output=markdown,
                outputs_prefix="Zscaler.IPDestinationGroup",
                outputs_key_field="ID",
                outputs=contents,
            )
            return results
    else:
        responses = []
        for ip_group_id in ip_group_ids:
            cmd_url = f"/ipDestinationGroups/{ip_group_id}"
            responses.append(http_request("GET", cmd_url))
        contents = get_contents(responses)
        markdown = tableToMarkdown(
            f"IPv4 Destination groups ({len(contents)})",
            contents,
            headers,
            removeNull=True,
        )

        results = CommandResults(
            readable_output=markdown,
            outputs_prefix="Zscaler.IPDestinationGroup",
            outputs_key_field="ID",
            outputs=contents,
        )
        return results


def edit_ip_destination_group(args: dict):
    headers = [
        "ID",
        "Name",
        "Type",
        "Description",
        "Addresses",
        "Countries",
        "IpCategories",
    ]
    payload: dict = {}
    ip_group_id = str(args["ip_group_id"]).strip()

    check_url = f"/ipDestinationGroups/{ip_group_id}"
    demisto.debug(f"Fetching existing IP destination group: {ip_group_id}")
    response_data = http_request("GET", check_url)
    demisto.debug(f"Fetched group data: {response_data}")
    if not response_data or not response_data.get("id"):
        raise Exception(f"Resource not found with ip_group_id {ip_group_id}")

    payload["name"] = args.get("name", response_data.get("name", ""))
    payload["countries"] = argToList(args.get("countries", response_data.get("countries", [])))
    payload["ipCategories"] = argToList(args.get("ip_categories", response_data.get("ipCategories", [])))
    payload["addresses"] = argToList(args.get("addresses", response_data.get("addresses", [])))
    payload["description"] = args.get("description", response_data.get("description", ""))
    payload["isNonEditable"] = args.get("is_non_editable", False)
    payload["type"] = response_data.get("type", "")

    cmd_url = f"/ipDestinationGroups/{ip_group_id}"
    # The override query parameter controls whether existing entries are replaced (true) or preserved (false). Default is true.
    override: bool = argToBoolean(args.get("override", True))
    if not override:
        cmd_url = f"{cmd_url}?override=false"
    json_data = json.dumps(payload)
    demisto.debug(f"Updating IP destination group {ip_group_id} with payload={payload}, override={override}")
    response = http_request("PUT", cmd_url, json_data, DEFAULT_HEADERS)
    content = {
        "ID": int(response.get("id", "")),
        "Name": response.get("name", ""),
        "Type": response.get("type", ""),
        "Description": response.get("description", ""),
        "Addresses": response.get("addresses", []),
        "IpCategories": response.get("ipCategories", []),
        "Countries": response.get("countries", []),
    }
    markdown = tableToMarkdown("IPv4 Destination group updated", content, headers, removeNull=True)
    results = CommandResults(
        readable_output=markdown, outputs_prefix="Zscaler.IPDestinationGroup", outputs_key_field="ID", outputs=content
    )
    return results


def delete_ip_destination_groups(args: dict):
    ip_group_ids = argToList(args.get("ip_group_id", ""))
    for ip_group_id in ip_group_ids:
        cmd_url = f"/ipDestinationGroups/{ip_group_id}"
        _ = http_request("DELETE", cmd_url, None, DEFAULT_HEADERS)
    markdown = "### IP Destination Group {} deleted successfully".format(",".join(ip_group_ids))

    results = CommandResults(
        readable_output=markdown, outputs_prefix="Zscaler.IPDestinationGroup", outputs_key_field=None, outputs=None
    )
    return results


""" EXECUTION CODE """


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

    add_sensitive_log_strs(USERNAME)
    add_sensitive_log_strs(PASSWORD)

    demisto.debug(f"command is {command}")

    if command == "zscaler-login":
        return_results(login_command())
    elif command == "zscaler-logout":
        return_results(logout_command())
    else:
        try:
            login()
            if command == "test-module":
                return_results(test_module())
            elif command == "url":
                return_results(url_lookup(args))
            elif command == "ip":
                return_results(ip_lookup(args.get("ip")))
            elif command == "domain":
                return_results(domain_lookup(args))
            elif command == "zscaler-blacklist-url":
                return_results(blacklist_url(args.get("url")))
            elif command == "zscaler-undo-blacklist-url":
                return_results(unblacklist_url(args.get("url")))
            elif command == "zscaler-whitelist-url":
                return_results(whitelist_url(args.get("url")))
            elif command == "zscaler-undo-whitelist-url":
                return_results(unwhitelist_url(args.get("url")))
            elif command == "zscaler-blacklist-ip":
                return_results(blacklist_ip(args.get("ip")))
            elif command == "zscaler-undo-blacklist-ip":
                return_results(unblacklist_ip(args.get("ip")))
            elif command == "zscaler-whitelist-ip":
                return_results(whitelist_ip(args.get("ip")))
            elif command == "zscaler-undo-whitelist-ip":
                return_results(unwhitelist_ip(args.get("ip")))
            elif command == "zscaler-category-add-url":
                return_results(
                    category_add(
                        args.get("category-id"),
                        args.get("url"),
                        args.get("retaining-parent-category-url"),
                        "url",
                        args.get("category-name"),
                    )
                )
            elif command == "zscaler-category-add-ip":
                return_results(
                    category_add(
                        args.get("category-id"),
                        args.get("ip"),
                        args.get("retaining-parent-category-ip"),
                        "ip",
                        args.get("category-name"),
                    )
                )
            elif command == "zscaler-category-remove-url":
                return_results(
                    category_remove(
                        args.get("category-id"),
                        args.get("url"),
                        args.get("retaining-parent-category-url"),
                        "url",
                        args.get("category-name"),
                    )
                )
            elif command == "zscaler-category-remove-ip":
                return_results(
                    category_remove(
                        args.get("category-id"),
                        args.get("ip"),
                        args.get("retaining-parent-category-ip"),
                        "ip",
                        args.get("category-name"),
                    )
                )
            elif command == "zscaler-get-categories":
                return_results(get_categories_command(args))
            elif command == "zscaler-get-blacklist":
                return_results(get_blacklist_command(args))
            elif command == "zscaler-get-whitelist":
                return_results(get_whitelist_command())
            elif command == "zscaler-sandbox-report":
                return_results(sandbox_report_command())
            elif command == "zscaler-activate-changes":
                return_results(activate_command())
            elif command == "zscaler-url-quota":
                return_results(url_quota_command())
            elif command == "zscaler-get-users":
                return_results(get_users_command(args))
            elif command == "zscaler-update-user":
                return_results(set_user_command(args))
            elif command == "zscaler-get-departments":
                return_results(get_departments_command(args))
            elif command == "zscaler-get-usergroups":
                return_results(get_usergroups_command(args))
            elif command == "zscaler-list-ip-destination-groups":
                return_results(list_ip_destination_groups(args))
            elif command == "zscaler-create-ip-destination-group":
                return_results(create_ip_destination_group(args))
            elif command == "zscaler-edit-ip-destination-group":
                return_results(edit_ip_destination_group(args))
            elif command == "zscaler-delete-ip-destination-groups":
                return_results(delete_ip_destination_groups(args))
            else:
                raise NotImplementedError(f"Command {command} is not implemented")
        except Exception as e:
            return_error(f"Failed to execute {command} command. Error: {str(e)}")
        finally:
            try:
                # activate changes only when required
                if params.get("auto_activate") and command in AUTO_ACTIVATE_CHANGES_COMMANDS:
                    demisto.debug(f"Activating changes for command {command}")
                    activate_changes()
                if params.get("auto_logout"):
                    demisto.debug("Logging out")
                    logout()
            except Exception as err:
                return_error("Zscaler error: " + str(err))


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