ZscalerZIdentity
Zscaler Internet Access via ZIdentity OAuth 2.0. Provides URL/IP/domain classification, denylist and allowlist management, URL category management, sandbox reporting, user and group management, and IP destination group management using OAuth 2.0 client credentials authentication through ZIdentity.
Network Security · Zscaler Internet Access
Details
| ID | ZscalerZIdentity |
|---|---|
| Provider | Zscaler |
| Category | Network Security |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.14.12343672 |
| Supported Modules | Agentix XSIAM EDR Cortex Cloud Cloud Runtime Security |
README
This integration uses ZIdentity OAuth 2.0 client credentials to authenticate with Zscaler Internet Access (ZIA). It enables the management of denylists, allowlists, URL categories, IP destination groups, and users/groups, while also providing URL, IP, and domain classifications and sandbox reporting..
Configure Zscaler Internet Access via ZIdentity in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL | The Server URL assigned to your organization. For example, www.acme.zslogin.net. | True |
| Client ID | The OAuth 2.0 client ID from ZIdentity. | True |
| Client Secret | The OAuth 2.0 client secret from ZIdentity. | True |
| Auto Activate Changes | If enabled, the integration will activate the command changes after each execution. If disabled, use the ‘zia-activate-changes’ command to activate Zscaler command changes. | False |
| Source Reliability | Reliability of the source providing the intelligence data. | True |
| Suspicious URL categories | Suspicious URL categories for security alerts. Default: SUSPICIOUS_DESTINATION, SPYWARE_OR_ADWARE. | False |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | 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.
zia-denylist-list
Gets a list of URLs and IPs that are in the denylist.
Base Command
zia-denylist-list
Input
| Argument Name | Description | Required |
|---|---|---|
| filter | Filter results by URL or IP objects. Possible values are: url, ip. | Optional |
| query | Query to match against (Python regular expressions, for example, 8...8). | Optional |
| limit | The number of items to return. Default is 50. | Optional |
| all_results | Whether to retrieve all results at once. Default is False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.DenyList | String | The list of URLs and IPs on the denylist. |
Command Example
!zia-denylist-list filter=url limit=10
Human Readable Output
Denylist
URL malware.com phishing.net
zia-denylist-update
Updates the list of URLs in the denylist.
Base Command
zia-denylist-update
Input
| Argument Name | Description | Required |
|---|---|---|
| url | A comma-separated list of URLs to update the denylist. For example, snapchat.com,facebook.com. | Optional |
| ip | Deprecated. The Zscaler blacklistUrls field only supports URLs. Use the url argument instead. | Optional |
| action | The action applied to the denylist. Possible values are: ADD_TO_LIST, REMOVE_FROM_LIST, OVERWRITE. | Required |
Context Output
There is no context output for this command.
Command Example
!zia-denylist-update url=malware.com,phishing.net action=ADD_TO_LIST
Human Readable Output
Denylist updated successfully.
zia-allowlist-list
Gets a list of URLs and IPs that are in the allowlist.
Base Command
zia-allowlist-list
Input
| Argument Name | Description | Required |
|---|---|---|
| filter | Filter results by URL or IP objects. Possible values are: url, ip. | Optional |
| query | Query to match against (Python regular expressions, for example, 8...8). | Optional |
| limit | The number of items to return. Default is 50. | Optional |
| all_results | Whether to retrieve all results at once. Default is False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.AllowList | String | The list of URLs in the allowlist. |
Command Example
!zia-allowlist-list filter=url limit=10
Human Readable Output
Allowlist
URL trusted.com safe.net
zia-allowlist-update
Updates the list of URLs in the allowlist.
Base Command
zia-allowlist-update
Input
| Argument Name | Description | Required |
|---|---|---|
| url | A comma-separated list of URLs to update in the allowlist. For example, snapchat.com,facebook.com. | Optional |
| ip | Deprecated. The Zscaler whitelistUrls field only supports URLs. Use the url argument instead. | Optional |
| action | The action applied to the allowlist. Possible values are: ADD_TO_LIST, REMOVE_FROM_LIST, OVERWRITE. | Required |
Context Output
There is no context output for this command.
Command Example
!zia-allowlist-update url=trusted.com action=ADD_TO_LIST
Human Readable Output
Allowlist updated successfully.
zia-category-list
Gets information about all or custom URL categories. By default, the response includes keywords. The lite option cannot be used in combination with other parameters.
Base Command
zia-category-list
Input
| Argument Name | Description | Required |
|---|---|---|
| category_id | The URL category for the specified ID. For more information about category ID values, see the Zscaler documentation. | Optional |
| custom_only | If set to true, gets information on custom URL categories only. Default is false. | Optional |
| include_only_url_keyword_counts | If set to true, the response only includes URL and keyword counts. Default is false. | Optional |
| lite | Whether to get a lightweight key-value list of all or custom URL categories. Cannot be used with other parameters. Default is false. | Optional |
| limit | The number of items to return. Default is 50. | Optional |
| all_results | Whether to retrieve all results at once. Default is False. | Optional |
| display_url | Whether to display the URLs of each category in the War Room. URLs will always be returned to the Context Data. Default is false. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.Category.id | String | The ID of the category. |
| ZIA.Category.configuredName | String | The configured name of the category. |
| ZIA.Category.superCategory | String | The super category of the category. |
| ZIA.Category.keywords | String | The keywords associated with the category. |
| ZIA.Category.urls | String | The URLs in the category. |
| ZIA.Category.customCategory | Boolean | Whether the category is a custom category. |
Command Example
!zia-category-list custom_only=true
Context Example
{
"ZIA": {
"Category": [
{
"id": "CUSTOM_01",
"configuredName": "My Custom Category",
"superCategory": "USER_DEFINED",
"keywords": ["example"],
"urls": ["example.com"],
"customCategory": true
}
]
}
}
Human Readable Output
URL Categories
ID Configured Name Super Category Custom Category CUSTOM_01 My Custom Category USER_DEFINED true
zia-category-update
Updates the URL category for the specified ID.
Base Command
zia-category-update
Input
| Argument Name | Description | Required |
|---|---|---|
| category_id | The URL category for the specified ID. For more information about category ID values, see the Zscaler documentation. | Required |
| url | A comma-separated list of URLs to update in 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: url=`[“https://example.com/foo,bar”]`. | Optional |
| ip | A comma-separated list of custom IP address ranges to update in the specified category. Values must be in CIDR notation. For example, 1.2.3.4/32,8.8.8.8/32. Up to 2000 custom IP address ranges and retaining parent custom IP address ranges can be added, per organization, across all categories. Note: This field is available only if the option to configure custom IP ranges is enabled for your organization. To enable this option, contact Zscaler Support. | Optional |
| action | The action applied to the URL category. Possible values are: ADD_TO_LIST, REMOVE_FROM_LIST, OVERWRITE. | Required |
| keywords | Custom keywords associated with a URL category. Up to 2048 custom keywords can be added per organization across all categories. | Optional |
| description | Description of the URL category. Contains tag name and needs to be localized on client side in case of predefined category. | Optional |
| db_categorized_urls | URLs added to a custom URL category that are also retained under the original parent URL category. | Optional |
| keywords_retaining_parent_category | Retained custom keywords from the parent URL category. Up to 2048 retained parent keywords can be added per organization across all categories. | Optional |
| ip_ranges_retaining_parent_category | The retaining parent custom IP address ranges associated with a URL category. Up to 2000 custom IP ranges and retaining parent custom IP address ranges can be added, per organization, across all categories. | Optional |
Context Output
There is no context output for this command.
Command Example
!zia-category-update category_id=CUSTOM_01 url=pandora.com,spotify.com action=ADD_TO_LIST
Human Readable Output
Category CUSTOM_01 updated successfully.
zia-url-quota-get
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
zia-url-quota-get
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.UrlQuota.uniqueUrlsProvisioned | Number | The number of unique URLs that are currently provisioned for your organization. |
| ZIA.UrlQuota.remainingUrlsQuota | Number | The number of URLs you can add before reaching the quota. |
Command Example
#### Context Example
```json
{
"ZIA": {
"UrlQuota": {
"uniqueUrlsProvisioned": 25000,
"remainingUrlsQuota": 24850
}
}
}
Human Readable Output
URL Quota
Unique URLs Provisioned Remaining URLs Quota 25000 24850
zia-ip-destination-group-list
Gets a list of all IP destination groups or for the specified ID.
Base Command
zia-ip-destination-group-list
Input
| Argument Name | Description | Required |
|---|---|---|
| group_id | The unique identifier for the IP destination group. | Optional |
| include_ipv6 | Whether to retrieve IPv6 destination groups. Default is False. | Optional |
| exclude_type | Filter based on the IP destination group’s type. Possible values are: DSTN_IP, DSTN_FQDN, DSTN_DOMAIN, DSTN_OTHER. | Optional |
| category_type | Filter based on the IP destination group’s type. Possible values are: DSTN_IP, DSTN_FQDN, DSTN_DOMAIN, DSTN_OTHER. | Optional |
| lite | Gets a lightweight dictionary (name and ID) of all IP destination groups. Default is False. | Optional |
| limit | The number of items to return. Default is 50. | Optional |
| all_results | Whether to retrieve all results at once. Default is False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.IPDestinationGroup.id | Number | Unique identifier for the destination IP group. |
| ZIA.IPDestinationGroup.name | String | Destination IP group name. |
| ZIA.IPDestinationGroup.type | String | Destination IP group type. |
| ZIA.IPDestinationGroup.addresses | String | Destination IP addresses, FQDNs, or wildcard FQDNs added to the group. |
| ZIA.IPDestinationGroup.description | String | Additional information about the destination IP group. |
| ZIA.IPDestinationGroup.countries | String | Destination IP address countries. |
| ZIA.IPDestinationGroup.ipCategories | String | Destination IP address URL categories. |
Command Example
!zia-ip-destination-group-list limit=5
Context Example
{
"ZIA": {
"IPDestinationGroup": [
{
"id": 1234,
"name": "My IP Group",
"type": "DSTN_IP",
"addresses": ["8.8.8.8", "1.1.1.1"],
"description": "DNS servers",
"countries": [],
"ipCategories": []
}
]
}
}
Human Readable Output
IP Destination Groups
ID Name Type Addresses Description 1234 My IP Group DSTN_IP 8.8.8.8, 1.1.1.1 DNS servers
zia-ip-destination-group-update
Updates an existing IP destination group.
Base Command
zia-ip-destination-group-update
Input
| Argument Name | Description | Required |
|---|---|---|
| group_id | The unique identifier for the IP destination group. | Required |
| group_name | Destination IP group name. | Optional |
| group_type | Destination IP group type. Possible values are: DSTN_IP, DSTN_FQDN, DSTN_DOMAIN, DSTN_OTHER. | Optional |
| address | Destination IP addresses. | Optional |
| action | The action applied to the addresses list. Possible values are: ADD_TO_LIST, REMOVE_FROM_LIST, OVERWRITE. | Required |
| description | Additional information about the destination IP group. | Optional |
| ip_category | Destination IP address URL categories. Possible values can be found here. | Optional |
| country | Destination IP address countries. Possible values can be found here. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.IPDestinationGroup.id | Number | Unique identifier for the destination IP group. |
| ZIA.IPDestinationGroup.name | String | Destination IP group name. |
| ZIA.IPDestinationGroup.type | String | Destination IP group type. |
| ZIA.IPDestinationGroup.addresses | String | Destination IP addresses, FQDNs, or wildcard FQDNs added to the group. |
| ZIA.IPDestinationGroup.description | String | Additional information about the destination IP group. |
| ZIA.IPDestinationGroup.countries | String | Destination IP address countries. |
| ZIA.IPDestinationGroup.ipCategories | String | Destination IP address URL categories. |
Command Example
!zia-ip-destination-group-update group_id=1234 address=9.9.9.9 action=ADD_TO_LIST
Human Readable Output
IP destination group 1234 updated successfully.
zia-ip-destination-group-add
Adds a new IP destination group.
Base Command
zia-ip-destination-group-add
Input
| Argument Name | Description | Required |
|---|---|---|
| group_name | Destination IP group name. | Optional |
| group_type | Destination IP group type. Possible values are: DSTN_IP, DSTN_FQDN, DSTN_DOMAIN, DSTN_OTHER. | Optional |
| address | Destination IP addresses. | Optional |
| description | Additional information about the destination IP group. | Optional |
| ip_category | Destination IP address URL categories. Possible values can be found here. | Optional |
| country | Destination IP address countries. Possible values can be found here. | Optional |
| is_non_editable | If set to true, the destination IP address group is non-editable. Default is false. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.IPDestinationGroup.id | Number | Unique identifier for the destination IP group. |
| ZIA.IPDestinationGroup.name | String | Destination IP group name. |
| ZIA.IPDestinationGroup.type | String | Destination IP group type. |
| ZIA.IPDestinationGroup.addresses | String | Destination IP addresses, FQDNs, or wildcard FQDNs added to the group. |
| ZIA.IPDestinationGroup.description | String | Additional information about the destination IP group. |
| ZIA.IPDestinationGroup.countries | String | Destination IP address countries. |
| ZIA.IPDestinationGroup.ipCategories | String | Destination IP address URL categories. |
Command Example
!zia-ip-destination-group-add group_name="New Group" group_type=DSTN_IP address=10.0.0.1
Context Example
{
"ZIA": {
"IPDestinationGroup": {
"id": 5678,
"name": "New Group",
"type": "DSTN_IP",
"addresses": ["10.0.0.1"],
"description": "",
"countries": [],
"ipCategories": []
}
}
}
Human Readable Output
IP destination group created successfully with ID 5678.
zia-ip-destination-group-delete
Deletes the IP destination group for the specified ID.
Base Command
zia-ip-destination-group-delete
Input
| Argument Name | Description | Required |
|---|---|---|
| group_id | The unique identifier for the IP destination group. | Required |
Context Output
There is no context output for this command.
Command Example
!zia-ip-destination-group-delete group_id=5678
Human Readable Output
IP destination group 5678 deleted successfully.
zia-user-list
Gets a list of all users or the user information for the specified ID.
Base Command
zia-user-list
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | Filters by the unique identifier for the user. | Optional |
| dept | Filters by department name. | Optional |
| group | Filters by group name. | Optional |
| page | Specifies the page offset. Default is 1. | Optional |
| page_size | Specifies the page size. The maximum size is 10,000. Default is 100. | Optional |
| all_results | Whether to retrieve all results at once. Default is False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.User.id | Number | The unique identifier for the user. |
| ZIA.User.name | String | User name. |
| ZIA.User.email | String | User email address. |
| ZIA.User.comments | String | Additional information about the user. |
Command Example
!zia-user-list dept="Engineering" page_size=50
Context Example
{
"ZIA": {
"User": [
{
"id": 100,
"name": "John Doe",
"email": "john.doe@example.com",
"comments": "Engineering team member"
}
]
}
}
Human Readable Output
Users
ID Name Comments 100 John Doe john.doe@example.com Engineering team member
zia-user-update
Updates the user information for the specified ID.
Base Command
zia-user-update
Input
| Argument Name | Description | Required |
|---|---|---|
| user_id | The unique identifier for the user. | Required |
| user | Full user object as JSON. If provided, individual field arguments are applied on top of this. | Optional |
| user_name | User name. This appears when choosing users for policies. | Optional |
| User email consists of a user name and domain name. | Optional | |
| comments | Additional information about this user. | Optional |
| temp_auth_email | Temporary Authentication Email. | Optional |
| password | User’s password. Applicable only when authentication type is Hosted DB. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.User.id | Number | The unique identifier for the user. |
| ZIA.User.name | String | User name. |
| ZIA.User.email | String | User email address. |
| ZIA.User.comments | String | Additional information about the user. |
Command Example
!zia-user-update user_id=100 comments="Updated comment"
Human Readable Output
User 100 updated successfully.
zia-groups-list
Gets a list of groups.
Base Command
zia-groups-list
Input
| Argument Name | Description | Required |
|---|---|---|
| search | The search string used to match against a group’s name or comments attributes. | Optional |
| defined_by | The string value defined by the group name or other applicable attributes. | Optional |
| sort_by | Sorts the groups based on available values. Possible values are: id, name, expiry, status, externalId, rank, modTime. Default is id. | Optional |
| sort_order | Sorts the order of groups based on available values. Possible values are: asc, desc, ruleExecution. Default is asc. | Optional |
| page | Specifies the page offset. Default is 1. | Optional |
| page_size | Specifies the page size. The maximum size is 10,000. Default is 100. | Optional |
| all_results | Whether to retrieve all results at once. Default is False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.Groups.id | Number | The unique identifier for the group. |
| ZIA.Groups.name | String | Group name. |
| ZIA.Groups.idpId | Number | Unique identifier for the identity provider (IdP). |
| ZIA.Groups.comments | String | Additional information about the group. |
| ZIA.Groups.isSystemDefined | Boolean | Whether the group is system-defined. |
Command Example
!zia-groups-list search="Engineering" sort_by=name sort_order=asc
Context Example
{
"ZIA": {
"Groups": [
{
"id": 200,
"name": "Engineering",
"idpId": 1,
"comments": "Engineering department group",
"isSystemDefined": false
}
]
}
}
Human Readable Output
Groups
ID Name IdP ID Comments System Defined 200 Engineering 1 Engineering department group false
zia-departments-list
Gets a list of all departments or the department information for the specified ID.
Base Command
zia-departments-list
Input
| Argument Name | Description | Required |
|---|---|---|
| department_id | Unique identifier for the department. | Optional |
| search | The search string used to match against a department’s name or comments attributes. | Optional |
| limit_search | Whether to limit the search to match only against the department name. Default is false. | Optional |
| sort_by | Sorts the departments based on available values. Possible values are: id, name, expiry, status, externalId, rank. Default is id. | Optional |
| sort_order | Sorts the order of departments based on available values. Possible values are: asc, desc, ruleExecution. Default is asc. | Optional |
| page | Specifies the page offset. Default is 1. | Optional |
| page_size | Specifies the page size. The maximum size is 10,000. Default is 100. | Optional |
| all_results | Whether to retrieve all results at once. Default is False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.Department.id | Number | The unique identifier for the department. |
| ZIA.Department.name | String | Department name. |
| ZIA.Department.idpId | Number | Unique identifier for the identity provider (IdP). |
| ZIA.Department.comments | String | Additional information about the department. |
| ZIA.Department.deleted | Boolean | Whether the department is deleted. |
Command Example
!zia-departments-list search="Engineering"
Context Example
{
"ZIA": {
"Department": [
{
"id": 300,
"name": "Engineering",
"idpId": 1,
"comments": "Engineering department",
"deleted": false
}
]
}
}
Human Readable Output
Departments
ID Name IdP ID Comments Deleted 300 Engineering 1 Engineering department false
zia-sandbox-report-get
Gets a full or summary detail report for an MD5 hash of a file that was analyzed by Sandbox.
Base Command
zia-sandbox-report-get
Input
| Argument Name | Description | Required |
|---|---|---|
| md5 | MD5 hash of the file that was analyzed by Sandbox. | Required |
| report_type | Type of report, full or summary. Possible values are: full, summary. Default is summary. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.SandboxReport | Unknown | The full sandbox report response. |
| 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. |
| 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.FileType | String | The file type. |
Command Example
!zia-sandbox-report-get md5=9de5069c5afe602b2ea0a04b66beb2c0 report_type=summary
Context Example
{
"DBotScore": {
"Indicator": "9de5069c5afe602b2ea0a04b66beb2c0",
"Score": 3,
"Type": "file",
"Vendor": "Zscaler"
},
"File": {
"MD5": "9de5069c5afe602b2ea0a04b66beb2c0",
"FileType": "PE32",
"Malicious": {
"Vendor": "Zscaler",
"Description": "Malware detected"
}
},
"ZIA": {
"SandboxReport": {
"Summary": {
"Status": "MALICIOUS",
"Category": "Malware"
}
}
}
}
Human Readable Output
Sandbox Report for 9de5069c5afe602b2ea0a04b66beb2c0
Status Category File Type Score MALICIOUS Malware PE32 3
zia-activate-changes
Activates the saved configuration changes.
Base Command
zia-activate-changes
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.ActivationStatus.status | String | The activation status. |
Command Example
#### Context Example
```json
{
"ZIA": {
"ActivationStatus": {
"status": "ACTIVE"
}
}
}
Human Readable Output
Activation Status
Status ACTIVE
url
Retrieve Zscaler’s default classification for a given set of 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. Up to 100 URLs can be looked up per request, and a URL cannot exceed 1,024 characters. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.URL.Data | String | The URL that was searched. |
| ZIA.URL.Address | String | The URL that was searched. |
| ZIA.URL.urlClassifications | String | The classification of the URL. |
| ZIA.URL.urlClassificationsWithSecurityAlert | String | The classifications of the URLs that have security alerts. |
| URL.Data | String | The URL that was searched. |
| URL.Address | String | The URL that was searched. |
| 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. |
| DBotScore.Reliability | String | Reliability of the source providing the intelligence data. |
Command Example
!url url=facebook.com
Context Example
{
"DBotScore": [
{
"Indicator": "facebook.com",
"Score": 1,
"Type": "url",
"Vendor": "Zscaler",
"Reliability": "C - Fairly reliable"
}
],
"URL": {
"Address": "facebook.com",
"Data": "facebook.com"
},
"ZIA": {
"URL": {
"Address": "facebook.com",
"Data": "facebook.com",
"urlClassifications": "SOCIAL_NETWORKING",
"urlClassificationsWithSecurityAlert": []
}
}
}
Human Readable Output
Zscaler URL Lookup
URL Classifications Security Alert Classifications facebook.com SOCIAL_NETWORKING
ip
Retrieve the classification for each of the specified IP addresses.
Base Command
ip
Input
| Argument Name | Description | Required |
|---|---|---|
| ip | A comma-separated list of IP addresses for which to look up the classification. For example, 8.8.8.8,1.2.3.4. The maximum number of IPs per call is 100. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.IP.Address | String | The IP address that was searched. |
| ZIA.IP.Classifications | String | The classification of the IP address. |
| ZIA.IP.ClassificationsWithSecurityAlert | String | Classifications that have a security alert for the IP address. |
| IP.Address | String | The IP address that was searched. |
| 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. |
| DBotScore.Reliability | String | Reliability of the source providing the intelligence data. |
Command Example
!ip ip=8.8.8.8
Context Example
{
"DBotScore": [
{
"Indicator": "8.8.8.8",
"Score": 1,
"Type": "ip",
"Vendor": "Zscaler",
"Reliability": "C - Fairly reliable"
}
],
"IP": {
"Address": "8.8.8.8"
},
"ZIA": {
"IP": {
"Address": "8.8.8.8",
"Classifications": "WEB_SEARCH",
"ClassificationsWithSecurityAlert": []
}
}
}
Human Readable Output
Zscaler IP Lookup
IP Classifications Security Alert Classifications 8.8.8.8 WEB_SEARCH
domain
Retrieve Zscaler’s default classification for a given set of 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, abc.com,xyz.com. The maximum number of domains per call is 100. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| ZIA.Domain.Data | String | The domain that was searched. |
| ZIA.Domain.Address | String | The domain that was searched. |
| ZIA.Domain.Classifications | String | The classification of the domain. |
| ZIA.Domain.ClassificationsWithSecurityAlert | String | Classifications that have a security alert for the domain. |
| Domain.Name | String | The domain that was searched. |
| 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. |
| DBotScore.Reliability | String | Reliability of the source providing the intelligence data. |
Command Example
!domain domain=google.com
Context Example
{
"DBotScore": [
{
"Indicator": "google.com",
"Score": 1,
"Type": "domain",
"Vendor": "Zscaler",
"Reliability": "C - Fairly reliable"
}
],
"Domain": {
"Name": "google.com"
},
"ZIA": {
"Domain": {
"Data": "google.com",
"Address": "google.com",
"Classifications": "WEB_SEARCH",
"ClassificationsWithSecurityAlert": []
}
}
}
Human Readable Output
Zscaler Domain Lookup
Domain Classifications Security Alert Classifications google.com WEB_SEARCH
Configuration parameters
server_url— Server URL (required)credentials— Client ID (required)auto_activate— Auto Activate Changesreliability— Source Reliability (required)suspicious_categories— Suspicious URL categoriesinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (20)
-
domainRetrieve Zscaler's default classification for a given set of domains.
-
ipRetrieve the classification for each of the specified IP addresses.
-
urlRetrieve Zscaler's default classification for a given set of URLs.
-
zia-activate-changesActivates the saved configuration changes.
-
zia-allowlist-listGets a list of URLs and IPs that are in the allowlist.
-
zia-allowlist-updateUpdates the list of URLs in the allowlist.
-
zia-category-listGets information about all or custom URL categories. By default, the response includes keywords. The lite option cannot be used in combination with other parameters.
-
zia-category-updateUpdates the URL category for the specified ID.
-
zia-denylist-listGets a list of URLs and IPs that are in the denylist.
-
zia-denylist-updateUpdates the list of URLs in the denylist.
-
zia-departments-listGets a list of all departments or the department information for the specified ID.
-
zia-groups-listGets a list of groups.
-
zia-ip-destination-group-addAdds a new IP destination group.
-
zia-ip-destination-group-deleteDeletes the IP destination group for the specified ID.
-
zia-ip-destination-group-listGets a list of all IP destination groups or for the specified ID.
-
zia-ip-destination-group-updateUpdates an existing IP destination group.
-
zia-sandbox-report-getGets a full or summary detail report for an MD5 hash of a file that was analyzed by Sandbox.
-
zia-url-quota-getGets 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.
-
zia-user-listGets a list of all users or the user information for the specified ID.
-
zia-user-updateUpdates the user information for the specified ID.
import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 import re """ CONSTANTS """ INTEGRATION_NAME = "ZscalerZIA" BASE_API_URL = "https://api.zsapi.net/zia/api/v1" TOKEN_URL_TEMPLATE = "https://{server_url}/oauth2/v1/token" AUDIENCE = "https://api.zscaler.com" SUSPICIOUS_CATEGORIES = ["SUSPICIOUS_DESTINATION", "SPYWARE_OR_ADWARE"] TOKEN_EXPIRY_BUFFER_SECONDS = 120 # Refresh the cached token this many seconds before it actually expires ERROR_CODES_DICT = { 400: "Invalid or bad request", 404: "Resource does not exist", 406: "Not Acceptable", 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." ), 415: "Unsupported media type.", 429: "Exceeded the rate limit or quota.", 500: "Unexpected error", 503: "Service is temporarily unavailable", } AUTO_ACTIVATE_CHANGES_COMMANDS = ( "zia-denylist-update", "zia-allowlist-update", "zia-category-update", "zia-ip-destination-group-update", "zia-ip-destination-group-add", "zia-ip-destination-group-delete", "zia-user-update", ) """ HANDLE PROXY """ handle_proxy() """ CLIENT CLASS """ class Client(BaseClient): """Client for Zscaler ZIA via ZIdentity OAuth 2.0. Authenticates using the OAuth 2.0 client credentials grant type against the ZIdentity token endpoint and forwards Bearer tokens to the ZIA REST API. Attributes: server_url: The ZIdentity server URL (e.g. "www.vanity.zslogin.net"). client_id: The OAuth 2.0 client ID. client_secret: The OAuth 2.0 client secret. reliability: Source reliability string for DBotScore. auto_activate: Whether to auto-activate changes after write commands. suspicious_categories: URL categories treated as suspicious for scoring. """ def __init__( self, server_url: str, client_id: str, client_secret: str, verify: bool, proxy: bool, reliability: str, auto_activate: bool, suspicious_categories: list[str], ): """Initializes the Client. Args: server_url: The ZIdentity server URL (e.g. "www.vanity.zslogin.net"). client_id: The OAuth 2.0 client ID registered in ZIdentity. client_secret: The OAuth 2.0 client secret. verify: Whether to verify SSL certificates. proxy: Whether to use system proxy settings. reliability: Source reliability for DBotScore (e.g. "C - Fairly reliable"). auto_activate: If True, activate ZIA changes after each write command. suspicious_categories: List of URL categories considered suspicious. """ super().__init__(base_url=BASE_API_URL, verify=verify, proxy=proxy) self.server_url = server_url self.client_id = client_id self.client_secret = client_secret self.reliability = reliability self.auto_activate = auto_activate self.suspicious_categories = suspicious_categories self._access_token: str | None = None def _get_access_token(self) -> str: """Obtains an OAuth 2.0 access token from ZIdentity using client credentials flow. Checks the integration context for a cached, non-expired token first. If the cached token is missing or within 30 seconds of expiry, fetches a new token from the ZIdentity token endpoint and caches it. Returns: A valid Bearer access token string. Raises: DemistoException: If ZIdentity does not return an access_token in the response. """ ctx = get_integration_context() or {} token = ctx.get("access_token") expires_at = ctx.get("token_expires_at", 0) if token and time.time() < expires_at - TOKEN_EXPIRY_BUFFER_SECONDS: demisto.debug("Using cached ZIdentity access token.") return token demisto.debug("Fetching new ZIdentity access token.") token_url = TOKEN_URL_TEMPLATE.format(server_url=self.server_url) payload = { "grant_type": "client_credentials", "client_id": self.client_id, "client_secret": self.client_secret, "audience": AUDIENCE, } response = self._http_request( method="POST", full_url=token_url, data=payload, headers={"Content-Type": "application/x-www-form-urlencoded"}, resp_type="json", ok_codes=(200,), ) token = response.get("access_token") if not token: raise DemistoException("Failed to obtain access token from ZIdentity. Response: " + str(response)) expires_in = int(response.get("expires_in", 3600)) ctx["access_token"] = token ctx["token_expires_at"] = time.time() + expires_in set_integration_context(ctx) add_sensitive_log_strs(token) return token def _get_auth_headers(self) -> dict: """Builds the Authorization headers for ZIA API requests. Returns: A dict containing the Bearer Authorization header and Content-Type. """ token = self._get_access_token() return { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } def _error_handler(self, res) -> None: """Handles HTTP error responses from the ZIA API. Raises a DemistoException with a descriptive message based on the HTTP status code and response body. Args: res: The requests.Response object from the failed HTTP call. Raises: DemistoException: Always raised with an appropriate error message. """ if res.status_code in (401, 403): raise DemistoException( f"Authentication/Authorization error ({res.status_code}): {res.text}. " "Verify your Client ID, Client Secret, and Domain are correct.", res=res, ) elif res.status_code == 400 and res.request.method == "PUT" and "/urlCategories/" in res.request.url: raise DemistoException( f"The request failed with error {res.status_code}.\nMessage: {res.text}\n" "This error might be due to an invalid URL or exceeding your organization's quota.\n" "For more information about URL formatting, refer to the Zscaler URL Format Guidelines: " "https://help.zscaler.com/zia/url-format-guidelines\n" "To check your quota usage, run the command `zia-url-quota-get`." ) elif res.status_code in ERROR_CODES_DICT: raise DemistoException(f"The request failed with error: {ERROR_CODES_DICT[res.status_code]}.\nMessage: {res.text}") else: raise DemistoException(f"The request failed with status code {res.status_code}.\nMessage: {res.text}") def _do_http_request( self, method: str, url_suffix: str, data: dict | list | None, params: dict | None, resp_type: str, ): """Executes a single authenticated HTTP request to the ZIA API. Injects the current Bearer token Authorization header and retries automatically on HTTP 429 (rate limit) responses up to 3 times. Args: method: HTTP method string (e.g. "GET", "POST", "PUT", "DELETE"). url_suffix: The API path suffix appended to BASE_API_URL. data: Optional JSON-serializable body payload (dict or list). params: Optional URL query parameters dict. resp_type: Response parsing mode passed to _http_request. Returns: The parsed API response (type depends on resp_type). """ return self._http_request( method=method, url_suffix=url_suffix, json_data=data, params=params, headers=self._get_auth_headers(), error_handler=self._error_handler, ok_codes=(200, 204), resp_type=resp_type, retries=3, status_list_to_retry=[429], ) def api_request( self, method: str, url_suffix: str, data: dict | list | None = None, params: dict | None = None, resp_type: str = "json", ): """Makes an authenticated API request to the ZIA API. Automatically injects the Bearer token Authorization header and retries on HTTP 429 (rate limit) responses up to 3 times. If a 401 response is received (e.g. due to a stale cached token caused by clock drift), the cached token is cleared and the request is retried once with a freshly obtained token. Args: method: HTTP method string (e.g. "GET", "POST", "PUT", "DELETE"). url_suffix: The API path suffix appended to BASE_API_URL. data: Optional JSON-serializable body payload (dict or list). params: Optional URL query parameters dict. resp_type: Response parsing mode passed to _http_request (e.g. "json", "response", "content"). Returns: The parsed API response (type depends on resp_type). """ try: return self._do_http_request(method, url_suffix, data, params, resp_type) except DemistoException as e: if getattr(getattr(e, "res", None), "status_code", None) == 401: demisto.debug("401 detected - forcing token refresh and retrying request.") ctx = get_integration_context() or {} ctx.pop("access_token", None) ctx.pop("token_expires_at", None) set_integration_context(ctx) return self._do_http_request(method, url_suffix, data, params, resp_type) raise def activate_changes(self) -> dict: """Activates saved ZIA configuration changes. Returns: The API response dict containing the activation status. """ return self.api_request("POST", "/status/activate") # ---- Denylist ---- def get_denylist(self) -> dict: """Retrieves the current ZIA advanced security policy including the denylist. Returns: A dict with the full security/advanced policy, including 'blacklistUrls'. """ return self.api_request("GET", "/security/advanced") def update_denylist(self, urls: list[str], ips: list[str], action: str) -> None: """Updates the ZIA denylist with the given URLs and IPs. For OVERWRITE, fetches the current policy and replaces blacklistUrls entirely via PUT. For ADD_TO_LIST and REMOVE_FROM_LIST, uses the POST action endpoint. Args: urls: List of URL strings to add/remove/overwrite. ips: List of IP address strings to add/remove/overwrite. action: One of "ADD_TO_LIST", "REMOVE_FROM_LIST", or "OVERWRITE". """ items = urls + ips if action == "OVERWRITE": # OVERWRITE uses PUT on /security/advanced with blacklistUrls field current = self.get_denylist() current["blacklistUrls"] = items self.api_request("PUT", "/security/advanced", data=current, resp_type="response") else: payload = {"blacklistUrls": items} self.api_request( "POST", f"/security/advanced/blacklistUrls?action={action}", data=payload, resp_type="response", ) # ---- Allowlist ---- def get_allowlist(self) -> dict: """Retrieves the current ZIA security policy including the allowlist. Returns: A dict with the full security policy, including 'whitelistUrls'. """ return self.api_request("GET", "/security") def update_allowlist(self, items: list[str], action: str) -> None: """Updates the ZIA allowlist with the given URLs and IPs. Since the ZIA API only supports PUT (full replacement), this method fetches the current allowlist and merges the changes before sending. Deduplicates entries when adding. Args: items: List of URL/IP strings to add/remove/overwrite. action: One of "ADD_TO_LIST", "REMOVE_FROM_LIST", or "OVERWRITE". """ current = self.get_allowlist() existing = current.get("whitelistUrls", []) if action == "ADD_TO_LIST": # Only add items not already present new_items = [u for u in items if u not in existing] current["whitelistUrls"] = existing + new_items elif action == "REMOVE_FROM_LIST": current["whitelistUrls"] = [u for u in existing if u not in items] else: # OVERWRITE current["whitelistUrls"] = items self.api_request("PUT", "/security", data=current, resp_type="response") # ---- URL Categories ---- def get_url_categories( self, category_id: str | None = None, custom_only: bool = False, include_only_url_keyword_counts: bool = False, lite: bool = False, ) -> list | dict: """Retrieves URL categories from ZIA. Args: category_id: If provided, fetches only the category with this ID. custom_only: If True, returns only custom URL categories. include_only_url_keyword_counts: If True, returns only URL and keyword counts instead of full URL lists. lite: If True, returns a lightweight list of category IDs and names only. Cannot be combined with other parameters. Returns: A list of category dicts, or a single category dict if category_id is given. """ if category_id: return self.api_request("GET", f"/urlCategories/{category_id}") if lite: return self.api_request("GET", "/urlCategories/lite") params: dict = {} if custom_only: params["customOnly"] = "true" if include_only_url_keyword_counts: params["includeOnlyUrlKeywordCounts"] = "true" return self.api_request("GET", "/urlCategories", params=params) def update_url_category( self, category_id: str, urls: list[str], ips: list[str], action: str, keywords: list[str] | None = None, description: str | None = None, db_categorized_urls: list[str] | None = None, keywords_retaining_parent_category: list[str] | None = None, ip_ranges_retaining_parent_category: list[str] | None = None, ) -> None: """Updates a URL category by merging the given URLs and IPs with existing ones. Fetches the current category state first, then applies the action (ADD_TO_LIST, REMOVE_FROM_LIST, or OVERWRITE) to URLs and IP ranges, and sends the merged payload via PUT. Args: category_id: The unique identifier of the URL category to update. urls: List of URL strings to add/remove/overwrite. ips: List of IP range strings to add/remove/overwrite. action: One of "ADD_TO_LIST", "REMOVE_FROM_LIST", or "OVERWRITE". keywords: Optional list of custom keywords to associate with the category. description: Optional description string for the category. db_categorized_urls: Optional URLs to retain under the parent category. keywords_retaining_parent_category: Optional keywords retained from parent. ip_ranges_retaining_parent_category: Optional IP ranges retained from parent. """ # Fetch current category to merge current = self.api_request("GET", f"/urlCategories/{category_id}") existing_urls = current.get("urls", []) existing_ips = current.get("ipRanges", []) if action == "ADD_TO_LIST": new_urls = existing_urls + [u for u in urls if u not in existing_urls] new_ips = existing_ips + [ip for ip in ips if ip not in existing_ips] elif action == "REMOVE_FROM_LIST": new_urls = [u for u in existing_urls if u not in urls] new_ips = [ip for ip in existing_ips if ip not in ips] else: # OVERWRITE new_urls = urls new_ips = ips payload: dict = { "id": category_id, "customCategory": current.get("customCategory"), "urls": new_urls, "ipRanges": new_ips, } if current.get("configuredName"): payload["configuredName"] = current["configuredName"] if current.get("superCategory"): payload["superCategory"] = current["superCategory"] if description is not None: payload["description"] = description if keywords is not None: payload["keywords"] = keywords if db_categorized_urls is not None: payload["dbCategorizedUrls"] = db_categorized_urls if keywords_retaining_parent_category is not None: payload["keywordsRetainingParentCategory"] = keywords_retaining_parent_category if ip_ranges_retaining_parent_category is not None: payload["ipRangesRetainingParentCategory"] = ip_ranges_retaining_parent_category self.api_request("PUT", f"/urlCategories/{category_id}", data=payload, resp_type="response") # ---- URL Quota ---- def get_url_quota(self) -> dict: """Retrieves the URL quota information for the organization. Returns: A dict containing 'uniqueUrlsProvisioned' and 'remainingUrlsQuota'. """ return self.api_request("GET", "/urlCategories/urlQuota") # ---- IP Destination Groups ---- def list_ip_destination_groups( self, group_id: int | None = None, include_ipv6: bool = False, exclude_type: str | None = None, category_type: list[str] | None = None, lite: bool = False, ) -> list | dict: """Lists IP destination groups from ZIA. If group_id is provided, returns only that specific group. Otherwise, returns all IPv4 groups (and optionally IPv6 groups). Args: group_id: If provided, fetches only the group with this ID. include_ipv6: If True, also fetches IPv6 destination groups. exclude_type: Filter to exclude groups of this type (e.g. "DSTN_IP", "DSTN_FQDN", "DSTN_DOMAIN", "DSTN_OTHER"). category_type: Filter by group type (only valid with lite=True). lite: If True, returns lightweight name/ID-only results. Returns: A list of group dicts, or a single group dict if group_id is given. """ if group_id is not None: return self.api_request("GET", f"/ipDestinationGroups/{group_id}") lite_suffix = "/lite" if lite else "" params: dict = {} if exclude_type: params["excludeType"] = exclude_type if category_type and lite: params["type"] = category_type results = [] ipv4_resp = self.api_request("GET", f"/ipDestinationGroups{lite_suffix}", params=params) results.extend(ipv4_resp if isinstance(ipv4_resp, list) else [ipv4_resp]) if include_ipv6: ipv6_resp = self.api_request("GET", f"/ipDestinationGroups/ipv6DestinationGroups{lite_suffix}", params=params) results.extend(ipv6_resp if isinstance(ipv6_resp, list) else [ipv6_resp]) return results def update_ip_destination_group(self, group_id: int, payload: dict) -> dict: """Updates an existing IP destination group. Args: group_id: The unique identifier of the group to update. payload: A dict containing the full updated group definition. Returns: The updated group dict as returned by the API. """ return self.api_request("PUT", f"/ipDestinationGroups/{group_id}", data=payload) def add_ip_destination_group(self, payload: dict) -> dict: """Creates a new IP destination group. Args: payload: A dict containing the new group definition (name, type, addresses, description, ipCategories, countries, isNonEditable). Returns: The created group dict as returned by the API. """ return self.api_request("POST", "/ipDestinationGroups", data=payload) def delete_ip_destination_group(self, group_id: int) -> None: """Deletes an IP destination group by ID. Args: group_id: The unique identifier of the group to delete. """ self.api_request("DELETE", f"/ipDestinationGroups/{group_id}", resp_type="response") # ---- Users ---- def get_users( self, user_id: str | None = None, dept: str | None = None, group: str | None = None, page: int = 1, page_size: int = 100, ) -> list | dict: """Retrieves ZIA users. If user_id is provided, returns only that specific user. Otherwise, returns a paginated list of users with optional department/group filters. Args: user_id: If provided, fetches only the user with this ID. dept: Filter users by department name. group: Filter users by group name. page: Page offset for pagination (1-based). page_size: Number of results per page (max 10,000). Returns: A list of user dicts, or a single user dict if user_id is given. """ if user_id: return self.api_request("GET", f"/users/{user_id}") params: dict = {"page": page, "pageSize": page_size} if dept: params["dept"] = dept if group: params["group"] = group return self.api_request("GET", "/users", params=params) def update_user(self, user_id: str, payload: dict) -> dict: """Updates a ZIA user by ID. Args: user_id: The unique identifier of the user to update. payload: A dict containing the full updated user definition. Returns: The updated user dict as returned by the API. """ return self.api_request("PUT", f"/users/{user_id}", data=payload) # ---- Groups ---- def get_groups( self, search: str | None = None, defined_by: str | None = None, sort_by: str = "id", sort_order: str = "asc", page: int = 1, page_size: int = 100, ) -> list: """Retrieves a paginated list of ZIA user groups. Args: search: Search string matched against group name or comments. defined_by: Filter by the attribute that defines the group. sort_by: Field to sort results by (e.g. "id", "name", "modTime"). sort_order: Sort direction, one of "asc", "desc", or "ruleExecution". page: Page offset for pagination (1-based). page_size: Number of results per page (max 10,000). Returns: A list of group dicts. """ params: dict = {"page": page, "pageSize": page_size, "sortBy": sort_by, "sortOrder": sort_order} if search: params["search"] = search if defined_by: params["definedBy"] = defined_by return self.api_request("GET", "/groups", params=params) # ---- Departments ---- def get_departments( self, department_id: str | None = None, search: str | None = None, limit_search: bool = False, sort_by: str = "id", sort_order: str = "asc", page: int = 1, page_size: int = 100, ) -> list | dict: """Retrieves ZIA departments. If department_id is provided, returns only that specific department. Otherwise, returns a paginated list with optional search filters. Args: department_id: If provided, fetches only the department with this ID. search: Search string matched against department name or comments. limit_search: If True, restricts search to match only the department name. sort_by: Field to sort results by (e.g. "id", "name", "rank"). sort_order: Sort direction, one of "asc", "desc", or "ruleExecution". page: Page offset for pagination (1-based). page_size: Number of results per page (max 10,000). Returns: A list of department dicts, or a single department dict if department_id is given. """ if department_id: return self.api_request("GET", f"/departments/{department_id}") params: dict = {"page": page, "pageSize": page_size, "sortBy": sort_by, "sortOrder": sort_order} if search: params["search"] = search if limit_search: params["limitSearch"] = "true" return self.api_request("GET", "/departments", params=params) # ---- Sandbox ---- def get_sandbox_report(self, md5: str, report_type: str = "summary") -> dict: """Retrieves a Sandbox analysis report for a file identified by MD5 hash. Args: md5: The MD5 hash of the file analyzed by Sandbox. report_type: Type of report to retrieve, either "full" or "summary". Defaults to "summary". Returns: A dict containing the sandbox report data. """ details = "full" if report_type.lower() == "full" else "summary" return self.api_request("GET", f"/sandbox/report/{md5}?details={details}") # ---- URL Lookup ---- def url_lookup(self, ioc_list: list[str]) -> list: """Looks up the classification for a list of URLs, IPs, or domains. Strips http:// and https:// prefixes before sending to the ZIA API, as Zscaler expects bare hostnames/URLs without protocol schemes. Args: ioc_list: A list of URL, IP, or domain strings to classify. Maximum 100 items per request. Returns: A list of classification result dicts, each containing 'url', 'urlClassifications', and 'urlClassificationsWithSecurityAlert'. """ # Strip protocol prefixes as Zscaler expects bare URLs cleaned = [u.replace("https://", "").replace("http://", "") for u in ioc_list] return self.api_request("POST", "/urlLookup", data=cleaned) """ HELPER FUNCTIONS """ def _filter_and_limit(items: list, filter_: str, query: str, limit: int, all_results: bool) -> list: """Applies filter, query, and limit to a list of URL/IP strings. Filters items by type (url or ip) and/or by a regex query pattern, then applies a count limit unless all_results is True. Args: items: The list of URL or IP strings to filter. filter_: Type filter string, either "url" or "ip". Empty string means no filter. query: Python regex pattern to match against each item. Empty string means no filter. limit: Maximum number of items to return when all_results is False. all_results: If True, returns all matching items ignoring limit. Returns: A filtered and optionally limited list of strings. """ if filter_ or query: filtered = [] for entity in items: is_filter_match = not filter_ is_query_match = not query if filter_: if re.match(ipv4Regex, entity): is_filter_match = filter_ == "ip" else: is_filter_match = filter_ == "url" if query: is_query_match = bool(re.search(query, entity)) if is_filter_match and is_query_match: filtered.append(entity) items = filtered return items if all_results else items[:limit] def _dbot_score_for_url( url_classifications: str, url_classifications_with_security_alert: str, suspicious_list: list[str] ) -> int: """Calculates the DBotScore for a URL, IP, or domain based on Zscaler classifications. Scoring logic: - MISCELLANEOUS_OR_UNKNOWN classification → DBotScore.NONE - Security alert category in suspicious_list → DBotScore.SUSPICIOUS - Any other security alert category → DBotScore.BAD - No security alert → DBotScore.GOOD Args: url_classifications: The primary URL classification string from Zscaler. url_classifications_with_security_alert: The security alert classification string from Zscaler (empty string if none). suspicious_list: List of category strings considered suspicious (e.g. ["SUSPICIOUS_DESTINATION", "SPYWARE_OR_ADWARE"]). Returns: An integer DBotScore value (Common.DBotScore.NONE/GOOD/SUSPICIOUS/BAD). """ if url_classifications == "MISCELLANEOUS_OR_UNKNOWN": return Common.DBotScore.NONE if url_classifications_with_security_alert: if url_classifications_with_security_alert in suspicious_list: return Common.DBotScore.SUSPICIOUS return Common.DBotScore.BAD return Common.DBotScore.GOOD """ COMMAND FUNCTIONS """ def test_module_command(client: Client) -> str: """Tests connectivity to the ZIA API by fetching the status endpoint. Args: client: The authenticated ZIA Client instance. Returns: The string "ok" if the connection is successful. Raises: DemistoException: If the API request fails. """ client.api_request("GET", "/status") return "ok" def zia_denylist_list_command(client: Client, args: dict) -> CommandResults: """Retrieves the ZIA denylist with optional filtering and limiting. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with optional keys: - filter: "url" or "ip" to filter by type. - query: Python regex to match against entries. - limit: Maximum number of results (default 50). - all_results: If "True", returns all results ignoring limit. Returns: A CommandResults object with the denylist entries. """ filter_ = args.get("filter", "") query = args.get("query", "") limit = arg_to_number(args.get("limit", 50)) or 50 all_results = argToBoolean(args.get("all_results", False)) response = client.get_denylist() denylist = response.get("blacklistUrls", []) denylist = _filter_and_limit(denylist, filter_, query, limit, all_results) hr_lines = "\n".join(f"- {item}" for item in denylist) if denylist else "No items found." readable_output = f"### ZIA Denylist\n{hr_lines}" return CommandResults( outputs_prefix="ZIA.DenyList", outputs=response, readable_output=readable_output, raw_response=response, ) def zia_denylist_update_command(client: Client, args: dict) -> CommandResults: """Updates the ZIA denylist by adding, removing, or overwriting entries. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with keys: - url: Comma-separated list of URLs to update. - ip: Comma-separated list of IPs to update. - action: One of "ADD_TO_LIST", "REMOVE_FROM_LIST", "OVERWRITE" (required). Returns: A CommandResults object with a success message. Raises: DemistoException: If neither url nor ip is provided, or action is missing. """ urls = argToList(args.get("url", "")) ips = argToList(args.get("ip", "")) action = args.get("action", "") if not urls and not ips: raise DemistoException("At least one of 'url' or 'ip' arguments must be provided.") if not action: raise DemistoException("The 'action' argument is required.") client.update_denylist(urls, ips, action) return CommandResults(readable_output="The deny list has been successfully updated.") def zia_allowlist_list_command(client: Client, args: dict) -> CommandResults: """Retrieves the ZIA allowlist with optional filtering and limiting. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with optional keys: - filter: "url" or "ip" to filter by type. - query: Python regex to match against entries. - limit: Maximum number of results (default 50). - all_results: If "True", returns all results ignoring limit. Returns: A CommandResults object with the allowlist entries. """ filter_ = args.get("filter", "") query = args.get("query", "") limit = arg_to_number(args.get("limit", 50)) or 50 all_results = argToBoolean(args.get("all_results", False)) response = client.get_allowlist() allowlist = response.get("whitelistUrls", []) allowlist = _filter_and_limit(allowlist, filter_, query, limit, all_results) hr_lines = "\n".join(f"- {item}" for item in allowlist) if allowlist else "No items found." readable_output = f"### ZIA Allowlist\n{hr_lines}" return CommandResults( outputs_prefix="ZIA.AllowList", outputs=response, readable_output=readable_output, raw_response=response, ) def zia_allowlist_update_command(client: Client, args: dict) -> CommandResults: """Updates the ZIA allowlist by adding, removing, or overwriting entries. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with keys: - url: Comma-separated list of URLs to update. - ip: Comma-separated list of IPs to update. - action: One of "ADD_TO_LIST", "REMOVE_FROM_LIST", "OVERWRITE" (required). Returns: A CommandResults object with a success message. Raises: DemistoException: If neither url nor ip is provided, or action is missing. """ urls = argToList(args.get("url", "")) ips = argToList(args.get("ip", "")) action = args.get("action", "") if not urls and not ips: raise DemistoException("At least one of 'url' or 'ip' arguments must be provided.") if not action: raise DemistoException("The 'action' argument is required.") client.update_allowlist(urls + ips, action) return CommandResults(readable_output="The allowlist has been successfully updated.") def zia_category_list_command(client: Client, args: dict) -> CommandResults: """Retrieves ZIA URL categories with optional filtering and display options. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with optional keys: - category_id: Fetch a specific category by ID. - custom_only: If "true", returns only custom categories. - include_only_url_keyword_counts: If "true", returns counts only. - lite: If "true", returns lightweight name/ID list only. Cannot be combined with other parameters. - limit: Maximum number of results (default 50). - all_results: If "True", returns all results ignoring limit. - display_url: If "true", includes URLs in the human-readable output. Returns: A CommandResults object with the URL category data. Raises: DemistoException: If lite is combined with incompatible parameters. """ category_id = args.get("category_id") custom_only = argToBoolean(args.get("custom_only", False)) include_only_url_keyword_counts = argToBoolean(args.get("include_only_url_keyword_counts", False)) lite = argToBoolean(args.get("lite", False)) limit = arg_to_number(args.get("limit", 50)) or 50 all_results = argToBoolean(args.get("all_results", False)) display_url = argToBoolean(args.get("display_url", False)) if lite and (category_id or custom_only or include_only_url_keyword_counts): raise DemistoException("The 'lite' option cannot be used in combination with other parameters.") response = client.get_url_categories( category_id=category_id, custom_only=custom_only, include_only_url_keyword_counts=include_only_url_keyword_counts, lite=lite, ) if isinstance(response, dict): categories = [response] else: categories = response if all_results else response[:limit] hr_rows = [] for cat in categories: row: dict = { "Category ID": cat.get("id"), "Configured Name": cat.get("configuredName"), "Super Category": cat.get("superCategory"), "Keywords": cat.get("keywords"), } if display_url: row["Urls"] = cat.get("urls") hr_rows.append(row) readable_output = tableToMarkdown("ZIA URL Categories", hr_rows, removeNull=True) return CommandResults( outputs_prefix="ZIA.Category", outputs_key_field="id", outputs=categories, readable_output=readable_output, raw_response=response, ) def zia_category_update_command(client: Client, args: dict) -> CommandResults: """Updates a ZIA URL category with new URLs, IPs, keywords, or description. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with keys: - category_id: The category ID to update (required). - url: Comma-separated list of URLs to update. - ip: Comma-separated list of IP ranges to update. - action: One of "ADD_TO_LIST", "REMOVE_FROM_LIST", "OVERWRITE" (required). - keywords: Comma-separated custom keywords. - description: Category description string. - db_categorized_urls: URLs to retain under the parent category. - keywords_retaining_parent_category: Keywords retained from parent. - ip_ranges_retaining_parent_category: IP ranges retained from parent. Returns: A CommandResults object with a success message. Raises: DemistoException: If category_id is missing, neither url nor ip is provided, or action is missing. """ category_id = args.get("category_id", "") if not category_id: raise DemistoException("The 'category_id' argument is required.") urls = argToList(args.get("url", "")) ips = argToList(args.get("ip", "")) action = args.get("action", "") if not urls and not ips: raise DemistoException("At least one of 'url' or 'ip' arguments must be provided.") if not action: raise DemistoException("The 'action' argument is required.") keywords = argToList(args.get("keywords")) if args.get("keywords") else None description = args.get("description") db_categorized_urls = argToList(args.get("db_categorized_urls")) if args.get("db_categorized_urls") else None keywords_retaining = ( argToList(args.get("keywords_retaining_parent_category")) if args.get("keywords_retaining_parent_category") else None ) ip_ranges_retaining = ( argToList(args.get("ip_ranges_retaining_parent_category")) if args.get("ip_ranges_retaining_parent_category") else None ) client.update_url_category( category_id=category_id, urls=urls, ips=ips, action=action, keywords=keywords, description=description, db_categorized_urls=db_categorized_urls, keywords_retaining_parent_category=keywords_retaining, ip_ranges_retaining_parent_category=ip_ranges_retaining, ) return CommandResults(readable_output="The category has been successfully updated.") def zia_url_quota_get_command(client: Client, args: dict) -> CommandResults: """Retrieves the URL quota information for the organization. Args: client: The authenticated ZIA Client instance. args: Command arguments dict (no arguments required for this command). Returns: A CommandResults object with quota information including uniqueUrlsProvisioned and remainingUrlsQuota. """ response = client.get_url_quota() hr = tableToMarkdown( "ZIA URL Quota", { "Unique Urls Provisioned": response.get("uniqueUrlsProvisioned"), "Remaining Urls Quota": response.get("remainingUrlsQuota"), }, ) return CommandResults( outputs_prefix="ZIA.UrlQuota", outputs=response, readable_output=hr, raw_response=response, ) def zia_ip_destination_group_list_command(client: Client, args: dict) -> CommandResults: """Lists ZIA IP destination groups with optional filtering and limiting. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with optional keys: - group_id: Fetch a specific group by ID. - include_ipv6: If "True", also fetches IPv6 destination groups. - exclude_type: Exclude groups of this type from results. - category_type: Filter by group type (only valid with lite=True). - lite: If "True", returns lightweight name/ID results only. - limit: Maximum number of results (default 50). - all_results: If "True", returns all results ignoring limit. Returns: A CommandResults object with the IP destination group data. Raises: DemistoException: If category_type is used without lite=True. """ group_id = arg_to_number(args.get("group_id")) include_ipv6 = argToBoolean(args.get("include_ipv6", False)) exclude_type = args.get("exclude_type") category_type = argToList(args.get("category_type", "")) or None lite = argToBoolean(args.get("lite", False)) limit = arg_to_number(args.get("limit", 50)) or 50 all_results = argToBoolean(args.get("all_results", False)) if category_type and not lite: raise DemistoException("The 'category_type' argument only works with the 'lite' argument set to True.") response = client.list_ip_destination_groups( group_id=group_id, include_ipv6=include_ipv6, exclude_type=exclude_type, category_type=category_type, lite=lite, ) if isinstance(response, dict): groups = [response] else: groups = response if all_results else response[:limit] hr_rows = [ { "IP Destination Group ID": g.get("id"), "Name": g.get("name"), "Type": g.get("type"), "Addresses": g.get("addresses"), "Description": g.get("description"), "Countries": g.get("countries"), "Ip Categories": g.get("ipCategories"), } for g in groups ] readable_output = tableToMarkdown("ZIA IP Destination Groups", hr_rows, removeNull=True) return CommandResults( outputs_prefix="ZIA.IPDestinationGroup", outputs_key_field="id", outputs=groups, readable_output=readable_output, raw_response=response, ) def zia_ip_destination_group_update_command(client: Client, args: dict) -> CommandResults: """Updates an existing ZIA IP destination group. Fetches the current group state and merges provided arguments on top, so only specified fields are changed. The 'action' argument controls how the 'address' list is applied: ADD_TO_LIST adds new addresses, REMOVE_FROM_LIST removes specified addresses, and OVERWRITE replaces the entire list. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with keys: - group_id: The unique identifier of the group to update (required). - group_name: New name for the group. - group_type: New type (DSTN_IP, DSTN_FQDN, DSTN_DOMAIN, DSTN_OTHER). - address: Comma-separated list of addresses. - action: How to apply the address list: ADD_TO_LIST, REMOVE_FROM_LIST, or OVERWRITE (required). - description: Group description. - ip_category: Comma-separated list of IP categories. - country: Comma-separated list of country codes. Returns: A CommandResults object with the updated group data. Raises: DemistoException: If group_id is not provided. """ group_id = arg_to_number(args.get("group_id")) if group_id is None: raise DemistoException("The 'group_id' argument is required.") action = args.get("action", "OVERWRITE") new_addresses = argToList(args.get("address", [])) # Fetch existing group to merge existing = client.list_ip_destination_groups(group_id=group_id) if isinstance(existing, list): existing = existing[0] if existing else {} existing_addresses: list[str] = existing.get("addresses", []) if action == "ADD_TO_LIST": merged_addresses = existing_addresses + [a for a in new_addresses if a not in existing_addresses] elif action == "REMOVE_FROM_LIST": merged_addresses = [a for a in existing_addresses if a not in new_addresses] else: # OVERWRITE merged_addresses = new_addresses if new_addresses else existing_addresses payload: dict = { "id": group_id, "name": args.get("group_name", existing.get("name", "")), "type": args.get("group_type", existing.get("type", "")), "addresses": merged_addresses, "description": args.get("description", existing.get("description", "")), "ipCategories": argToList(args.get("ip_category", existing.get("ipCategories", []))), "countries": argToList(args.get("country", existing.get("countries", []))), } response = client.update_ip_destination_group(group_id, payload) hr_row = { "IP Destination Group ID": response.get("id"), "Name": response.get("name"), "Type": response.get("type"), "Addresses": response.get("addresses"), "Description": response.get("description"), "Countries": response.get("countries"), "Ip Categories": response.get("ipCategories"), } readable_output = tableToMarkdown("The Ip Destination Group Resource has been successfully edited", hr_row, removeNull=True) return CommandResults( outputs_prefix="ZIA.IPDestinationGroup", outputs_key_field="id", outputs=response, readable_output=readable_output, raw_response=response, ) def zia_ip_destination_group_add_command(client: Client, args: dict) -> CommandResults: """Creates a new ZIA IP destination group. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with optional keys: - group_name: Name for the new group. - group_type: Type (DSTN_IP, DSTN_FQDN, DSTN_DOMAIN, DSTN_OTHER). - address: Comma-separated list of addresses. - description: Group description. - ip_category: Comma-separated list of IP categories. - country: Comma-separated list of country codes. - is_non_editable: If "true", marks the group as non-editable. Returns: A CommandResults object with the newly created group data. """ payload: dict = { "name": args.get("group_name", ""), "type": args.get("group_type", ""), "addresses": argToList(args.get("address", [])), "description": args.get("description", ""), "ipCategories": argToList(args.get("ip_category", [])), "countries": argToList(args.get("country", [])), "isNonEditable": argToBoolean(args.get("is_non_editable", False)), } response = client.add_ip_destination_group(payload) hr_row = { "IP Destination Group ID": response.get("id"), "Name": response.get("name"), "Type": response.get("type"), "Addresses": response.get("addresses"), "Description": response.get("description"), "Countries": response.get("countries"), "Ip Categories": response.get("ipCategories"), } readable_output = tableToMarkdown("The Ip Destination Group Resource has been successfully added", hr_row, removeNull=True) return CommandResults( outputs_prefix="ZIA.IPDestinationGroup", outputs_key_field="id", outputs=response, readable_output=readable_output, raw_response=response, ) def zia_ip_destination_group_delete_command(client: Client, args: dict) -> CommandResults: """Deletes a ZIA IP destination group by ID. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with keys: - group_id: The unique identifier of the group to delete (required). Returns: A CommandResults object with a success message. Raises: DemistoException: If group_id is not provided. """ group_id = arg_to_number(args.get("group_id")) if group_id is None: raise DemistoException("The 'group_id' argument is required.") client.delete_ip_destination_group(group_id) return CommandResults(readable_output="The Ip Destination Group Resource has been successfully deleted.") def zia_user_list_command(client: Client, args: dict) -> CommandResults: """Retrieves ZIA users with optional filtering by department or group. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with optional keys: - user_id: Fetch a specific user by ID. - dept: Filter by department name. - group: Filter by group name. - page: Page offset for pagination (default 1). - page_size: Number of results per page (default 100, max 10,000). Returns: A CommandResults object with the user data. """ user_id = args.get("user_id") dept = args.get("dept") group = args.get("group") page = arg_to_number(args.get("page", 1)) or 1 page_size = arg_to_number(args.get("page_size", 100)) or 100 response = client.get_users(user_id=user_id, dept=dept, group=group, page=page, page_size=page_size) if isinstance(response, dict): users = [response] else: users = response hr_rows = [ { "User ID": u.get("id"), "Name": u.get("name"), "Email": u.get("email"), "Comment": u.get("comments"), } for u in users ] readable_output = tableToMarkdown(f"ZIA Users ({len(users)})", hr_rows, removeNull=True) return CommandResults( outputs_prefix="ZIA.User", outputs_key_field="id", outputs=users, readable_output=readable_output, raw_response=response, ) def zia_user_update_command(client: Client, args: dict) -> CommandResults: """Updates a ZIA user's information. Fetches the current user state and merges provided arguments on top. If a full JSON user object is provided via the 'user' argument, it is used as the base payload, with individual field arguments applied on top. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with keys: - user_id: The unique identifier of the user to update (required). - user: Full user object as a JSON string (optional base payload). - user_name: New display name for the user. - email: New email address. - comments: Additional information about the user. - temp_auth_email: Temporary authentication email. - password: New password (Hosted DB auth only). Returns: A CommandResults object with the updated user data. Raises: DemistoException: If user_id is missing or the 'user' JSON is invalid. """ user_id = args.get("user_id", "") if not user_id: raise DemistoException("The 'user_id' argument is required.") # Fetch existing user to merge existing = client.get_users(user_id=user_id) if isinstance(existing, list): existing = existing[0] if existing else {} # If a full JSON user object is provided, use it as base user_json = args.get("user") if user_json: try: payload = json.loads(user_json) except json.JSONDecodeError as e: raise DemistoException(f"Invalid JSON in 'user' argument: {e}") else: payload = dict(existing) # Override individual fields if provided if args.get("user_name"): payload["name"] = args["user_name"] if args.get("email"): payload["email"] = args["email"] if args.get("comments"): payload["comments"] = args["comments"] if args.get("temp_auth_email"): payload["tempAuthEmail"] = args["temp_auth_email"] if args.get("password"): payload["password"] = args["password"] response = client.update_user(user_id, payload) hr_row = { "User ID": response.get("id"), "Name": response.get("name"), "Email": response.get("email"), "Comment": response.get("comments"), } readable_output = tableToMarkdown("ZIA User Updated", hr_row, removeNull=True) return CommandResults( outputs_prefix="ZIA.User", outputs_key_field="id", outputs=response, readable_output=readable_output, raw_response=response, ) def zia_groups_list_command(client: Client, args: dict) -> CommandResults: """Retrieves a paginated list of ZIA user groups. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with optional keys: - search: Search string matched against group name or comments. - defined_by: Filter by the attribute that defines the group. - sort_by: Field to sort by (default "id"). - sort_order: Sort direction "asc", "desc", or "ruleExecution" (default "asc"). - page: Page offset for pagination (default 1). - page_size: Number of results per page (default 100, max 10,000). - all_results: Accepted but not used; pagination is server-side. Returns: A CommandResults object with the group data. """ search = args.get("search") defined_by = args.get("defined_by") sort_by = args.get("sort_by", "id") sort_order = args.get("sort_order", "asc") page = arg_to_number(args.get("page", 1)) or 1 page_size = arg_to_number(args.get("page_size", 100)) or 100 response = client.get_groups( search=search, defined_by=defined_by, sort_by=sort_by, sort_order=sort_order, page=page, page_size=page_size, ) hr_rows = [ { "Group ID": g.get("id"), "Name": g.get("name"), "IdpId": g.get("idpId"), "Comment": g.get("comments"), "Is System Defined": g.get("isSystemDefined"), } for g in response ] readable_output = tableToMarkdown(f"ZIA Groups ({len(response)})", hr_rows, removeNull=True) return CommandResults( outputs_prefix="ZIA.Groups", outputs_key_field="id", outputs=response, readable_output=readable_output, raw_response=response, ) def zia_departments_list_command(client: Client, args: dict) -> CommandResults: """Retrieves ZIA departments with optional filtering. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with optional keys: - department_id: Fetch a specific department by ID. - search: Search string matched against department name or comments. - limit_search: If "true", restricts search to department name only. - sort_by: Field to sort by (default "id"). - sort_order: Sort direction "asc", "desc", or "ruleExecution" (default "asc"). - page: Page offset for pagination (default 1). - page_size: Number of results per page (default 100, max 10,000). - all_results: Accepted but not used; pagination is server-side. Returns: A CommandResults object with the department data. """ department_id = args.get("department_id") search = args.get("search") limit_search = argToBoolean(args.get("limit_search", False)) sort_by = args.get("sort_by", "id") sort_order = args.get("sort_order", "asc") page = arg_to_number(args.get("page", 1)) or 1 page_size = arg_to_number(args.get("page_size", 100)) or 100 response = client.get_departments( department_id=department_id, search=search, limit_search=limit_search, sort_by=sort_by, sort_order=sort_order, page=page, page_size=page_size, ) if isinstance(response, dict): departments = [response] else: departments = response hr_rows = [ { "Department ID": d.get("id"), "Name": d.get("name"), "IdpId": d.get("idpId"), "Comment": d.get("comments"), "Deleted": d.get("deleted"), } for d in departments ] readable_output = tableToMarkdown(f"ZIA Departments ({len(departments)})", hr_rows, removeNull=True) return CommandResults( outputs_prefix="ZIA.Department", outputs_key_field="id", outputs=departments, readable_output=readable_output, raw_response=response, ) def zia_sandbox_report_get_command(client: Client, args: dict) -> CommandResults: """Retrieves a Sandbox analysis report for a file identified by MD5 hash. Calculates a DBotScore based on the classification type: - MALICIOUS → BAD - SUSPICIOUS → SUSPICIOUS - BENIGN → GOOD - Other/unknown → NONE Args: client: The authenticated ZIA Client instance. args: Command arguments dict with keys: - md5: The MD5 hash of the file analyzed by Sandbox (required). - report_type: "full" or "summary" (default "summary"). Returns: A CommandResults object with the sandbox report, a File indicator, and a DBotScore. Raises: DemistoException: If md5 is not provided. """ md5 = args.get("md5", "") if not md5: raise DemistoException("The 'md5' argument is required.") report_type = args.get("report_type", "summary") response = client.get_sandbox_report(md5, report_type) report_key = "Full Details" if report_type.lower() == "full" else "Summary" classification_type = demisto.get(response, f"{report_key}.Classification.Type") if classification_type == "MALICIOUS": dbot_score = Common.DBotScore.BAD elif classification_type == "SUSPICIOUS": dbot_score = Common.DBotScore.SUSPICIOUS elif classification_type == "BENIGN": dbot_score = Common.DBotScore.GOOD else: dbot_score = Common.DBotScore.NONE file_type = demisto.get(response, f"{report_key}.File Properties.File Type") detected_malware = demisto.get(response, f"{report_key}.Classification.DetectedMalware") zscaler_score = demisto.get(response, f"{report_key}.Classification.Score") category = demisto.get(response, f"{report_key}.Classification.Category") malicious_description = None if dbot_score == Common.DBotScore.BAD: malicious_description = f"Classified as Malicious, with threat score: {zscaler_score} Zscaler ZIA Score" dbot = Common.DBotScore( indicator=md5, indicator_type=DBotScoreType.FILE, integration_name=INTEGRATION_NAME, score=dbot_score, malicious_description=malicious_description, reliability=demisto.params().get("reliability"), ) file_indicator = Common.File( md5=md5, file_type=file_type, dbot_score=dbot, ) hr_data = { "DBotScore": dbot_score, "Detected Malware": detected_malware, "Zscaler ZIA Score": zscaler_score, "Category": category, } readable_output = tableToMarkdown("ZIA Sandbox Report", hr_data, removeNull=True) return CommandResults( outputs_prefix="ZIA.SandboxReport", outputs_key_field="md5", outputs=response, indicator=file_indicator, readable_output=readable_output, raw_response=response, ) def zia_activate_changes_command(client: Client, args: dict) -> CommandResults: """Activates saved ZIA configuration changes. Args: client: The authenticated ZIA Client instance. args: Command arguments dict (no arguments required for this command). Returns: A CommandResults object with the activation status. """ response = client.activate_changes() hr = tableToMarkdown("ZIA Activation Status", {"Status": response.get("status")}, removeNull=True) return CommandResults( outputs_prefix="ZIA.ActivationStatus", outputs=response, readable_output=hr, raw_response=response, ) def url_command(client: Client, args: dict) -> list[CommandResults]: """Looks up the classification for a list of URLs and creates URL indicators. Restores the original URL with protocol prefix in the output if the protocol was stripped before sending to the ZIA API. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with keys: - url: Comma-separated list of URLs to classify (required). Maximum 100 URLs per request. Returns: A list of CommandResults objects, one per URL, each containing a URL indicator with DBotScore. Returns a single "No results found" result if the API returns an empty response. """ url_arg = args.get("url", "") urls = argToList(url_arg) response = client.url_lookup(urls) results: list[CommandResults] = [] for data in response: res_url = data.get("url", "") # Restore original URL with protocol if it was stripped for original_url in urls: if "http://" + res_url in original_url or "https://" + res_url in original_url: data["url"] = original_url res_url = original_url break url_classifications = "".join(data.get("urlClassifications", [])) url_classifications_with_security_alert = "".join(data.get("urlClassificationsWithSecurityAlert", [])) score = _dbot_score_for_url(url_classifications, url_classifications_with_security_alert, client.suspicious_categories) url_indicator = Common.URL( url=res_url, dbot_score=Common.DBotScore( indicator=res_url, indicator_type=DBotScoreType.URL, integration_name=INTEGRATION_NAME, score=score, malicious_description=url_classifications_with_security_alert or None, reliability=client.reliability, ), ) context_data = { "Address": res_url, "Data": res_url, "urlClassifications": url_classifications, "urlClassificationsWithSecurityAlert": url_classifications_with_security_alert, } results.append( CommandResults( outputs_prefix=f"{INTEGRATION_NAME}.URL", outputs_key_field="Data", indicator=url_indicator, readable_output=tableToMarkdown(f"Zscaler URL Lookup for {res_url}", data, removeNull=True), outputs=createContext(data=context_data, removeNull=True), raw_response=data, ) ) return results or [CommandResults(readable_output="No results found.")] def ip_command(client: Client, args: dict) -> list[CommandResults]: """Looks up the classification for a list of IP addresses and creates IP indicators. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with keys: - ip: Comma-separated list of IP addresses to classify (required). Maximum 100 IPs per request. Returns: A list of CommandResults objects, one per IP address, each containing an IP indicator with DBotScore. Returns a single "No results found" result if the API returns an empty response. """ ip_arg = args.get("ip", "") ips = argToList(ip_arg) response = client.url_lookup(ips) results: list[CommandResults] = [] for data in response: ip_addr = data.get("url", "") ip_classifications = "".join(data.get("urlClassifications", [])) ip_classifications_with_security_alert = "".join(data.get("urlClassificationsWithSecurityAlert", [])) score = _dbot_score_for_url(ip_classifications, ip_classifications_with_security_alert, client.suspicious_categories) ip_indicator = Common.IP( ip=ip_addr, dbot_score=Common.DBotScore( indicator=ip_addr, indicator_type=DBotScoreType.IP, integration_name=INTEGRATION_NAME, score=score, malicious_description=ip_classifications_with_security_alert or None, reliability=client.reliability, ), ) context_data = { "Address": ip_addr, "Classifications": ip_classifications, "ClassificationsWithSecurityAlert": ip_classifications_with_security_alert, } results.append( CommandResults( outputs_prefix=f"{INTEGRATION_NAME}.IP", outputs_key_field="Address", indicator=ip_indicator, readable_output=tableToMarkdown(f"Zscaler IP Lookup for {ip_addr}", data, removeNull=True), outputs=createContext(data=context_data, removeNull=True), raw_response=data, ) ) return results or [CommandResults(readable_output="No results found.")] def domain_command(client: Client, args: dict) -> list[CommandResults]: """Looks up the classification for a list of domains and creates Domain indicators. Args: client: The authenticated ZIA Client instance. args: Command arguments dict with keys: - domain: Comma-separated list of domains to classify (required). Maximum 100 domains per request. Returns: A list of CommandResults objects, one per domain, each containing a Domain indicator with DBotScore. Returns a single "No results found" result if the API returns an empty response. """ domain_arg = args.get("domain", "") domains = argToList(domain_arg) response = client.url_lookup(domains) results: list[CommandResults] = [] for data in response: domain_val = data.get("url", "") domain_classifications = "".join(data.get("urlClassifications", [])) domain_classifications_with_security_alert = "".join(data.get("urlClassificationsWithSecurityAlert", [])) score = _dbot_score_for_url( domain_classifications, domain_classifications_with_security_alert, client.suspicious_categories ) domain_indicator = Common.Domain( domain=domain_val, dbot_score=Common.DBotScore( indicator=domain_val, indicator_type=DBotScoreType.DOMAIN, integration_name=INTEGRATION_NAME, score=score, malicious_description=domain_classifications_with_security_alert or None, reliability=client.reliability, ), ) context_data = { "Data": domain_val, "Address": domain_val, "Classifications": domain_classifications, "ClassificationsWithSecurityAlert": domain_classifications_with_security_alert, } results.append( CommandResults( outputs_prefix=f"{INTEGRATION_NAME}.Domain", outputs_key_field="Data", indicator=domain_indicator, readable_output=tableToMarkdown(f"Zscaler Domain Lookup for {domain_val}", data, removeNull=True), outputs=createContext(data=context_data, removeNull=True), raw_response=data, ) ) return results or [CommandResults(readable_output="No results found.")] """ MAIN """ def main() -> None: # pragma: no cover """Entry point for the integration. Reads instance parameters, constructs the Client, dispatches the command, and handles auto-activation of ZIA changes after write commands. """ params = demisto.params() args = demisto.args() command = demisto.command() server_url = params.get("server_url", "") client_id = params.get("credentials", {}).get("identifier", "") client_secret = params.get("credentials", {}).get("password", "") verify = not params.get("insecure", False) proxy = params.get("proxy", False) reliability = params.get("reliability", "C - Fairly reliable") auto_activate = argToBoolean(params.get("auto_activate", True)) suspicious_categories_param = params.get("suspicious_categories", "") if suspicious_categories_param: suspicious_categories = argToList(suspicious_categories_param) else: suspicious_categories = SUSPICIOUS_CATEGORIES add_sensitive_log_strs(client_secret) client = Client( server_url=server_url, client_id=client_id, client_secret=client_secret, verify=verify, proxy=proxy, reliability=reliability, auto_activate=auto_activate, suspicious_categories=suspicious_categories, ) demisto.debug(f"Command is: {command}") try: if command == "test-module": return_results(test_module_command(client)) elif command == "zia-denylist-list": return_results(zia_denylist_list_command(client, args)) elif command == "zia-denylist-update": return_results(zia_denylist_update_command(client, args)) elif command == "zia-allowlist-list": return_results(zia_allowlist_list_command(client, args)) elif command == "zia-allowlist-update": return_results(zia_allowlist_update_command(client, args)) elif command == "zia-category-list": return_results(zia_category_list_command(client, args)) elif command == "zia-category-update": return_results(zia_category_update_command(client, args)) elif command == "zia-url-quota-get": return_results(zia_url_quota_get_command(client, args)) elif command == "zia-ip-destination-group-list": return_results(zia_ip_destination_group_list_command(client, args)) elif command == "zia-ip-destination-group-update": return_results(zia_ip_destination_group_update_command(client, args)) elif command == "zia-ip-destination-group-add": return_results(zia_ip_destination_group_add_command(client, args)) elif command == "zia-ip-destination-group-delete": return_results(zia_ip_destination_group_delete_command(client, args)) elif command == "zia-user-list": return_results(zia_user_list_command(client, args)) elif command == "zia-user-update": return_results(zia_user_update_command(client, args)) elif command == "zia-groups-list": return_results(zia_groups_list_command(client, args)) elif command == "zia-departments-list": return_results(zia_departments_list_command(client, args)) elif command == "zia-sandbox-report-get": return_results(zia_sandbox_report_get_command(client, args)) elif command == "zia-activate-changes": return_results(zia_activate_changes_command(client, args)) elif command == "url": return_results(url_command(client, args)) elif command == "ip": return_results(ip_command(client, args)) elif command == "domain": return_results(domain_command(client, args)) else: raise NotImplementedError(f"Command '{command}' is not implemented.") except Exception as e: return_error(f"Failed to execute '{command}' command.\nError: {e}") finally: if auto_activate and command in AUTO_ACTIVATE_CHANGES_COMMANDS: try: demisto.debug(f"Auto-activating changes after command: {command}") client.activate_changes() except Exception as err: demisto.error(f"Failed to auto-activate changes: {err}") return_warning( f"Auto-activation of changes failed: {err}\n" "Your changes were saved but are not yet active. " "Run the 'zia-activate-changes' command to apply them manually." ) if __name__ in ("__builtin__", "builtins", "__main__"): # pragma: no cover main()