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 import pytest import json import time from pathlib import Path from ZscalerZIdentity import ( Client, _filter_and_limit, _dbot_score_for_url, zia_denylist_list_command, zia_denylist_update_command, zia_allowlist_list_command, zia_allowlist_update_command, zia_category_list_command, zia_category_update_command, zia_url_quota_get_command, zia_ip_destination_group_list_command, zia_ip_destination_group_update_command, zia_ip_destination_group_add_command, zia_ip_destination_group_delete_command, zia_user_list_command, zia_user_update_command, zia_groups_list_command, zia_departments_list_command, zia_sandbox_report_get_command, zia_activate_changes_command, url_command, ip_command, domain_command, SUSPICIOUS_CATEGORIES, ) from CommonServerPython import DemistoException, Common, CommandResults # ---- Fixtures ---- def load_json(path: str): """Load a JSON file from disk and return its parsed content.""" full_path = Path(__file__).parent / path.lstrip("/") with open(full_path) as f: return json.load(f) def get_outputs(result: CommandResults) -> dict: """Extract outputs dict from a CommandResults, asserting it is a dict.""" ctx = result.to_context() # outputs are nested under EntryContext key entry_context = ctx.get("EntryContext", {}) if not entry_context: return {} # Return the first value (the actual outputs) for val in entry_context.values(): return val # type: ignore[return-value] return {} def get_outputs_list(result: CommandResults) -> list: """Extract outputs as a list from a CommandResults.""" ctx = result.to_context() entry_context = ctx.get("EntryContext", {}) if not entry_context: return [] for val in entry_context.values(): if isinstance(val, list): return val return [val] return [] @pytest.fixture def mock_client() -> Client: """Return a Client instance with mocked token retrieval.""" client = Client( server_url="www.testdomain.zslogin.net", client_id="test_client_id", client_secret="test_client_secret", verify=False, proxy=False, reliability="C - Fairly reliable", auto_activate=True, suspicious_categories=list(SUSPICIOUS_CATEGORIES), ) return client @pytest.fixture(autouse=True) def mock_demisto_params(mocker): """Patch demisto.params() with test instance configuration.""" mocker.patch.object( demisto, "params", return_value={ "server_url": "www.testdomain.zslogin.net", "credentials": {"identifier": "test_client_id", "password": "test_client_secret"}, "reliability": "C - Fairly reliable", "auto_activate": True, "suspicious_categories": "SUSPICIOUS_DESTINATION,SPYWARE_OR_ADWARE", "insecure": True, "proxy": False, }, ) mocker.patch.object(demisto, "command", return_value="test-module") @pytest.fixture(autouse=True) def mock_integration_context(mocker): """Mock integration context to return a valid cached token.""" mocker.patch( "ZscalerZIdentity.get_integration_context", return_value={ "access_token": "mock_access_token", "token_expires_at": time.time() + 3600, }, ) mocker.patch("ZscalerZIdentity.set_integration_context") # ---- Helper ---- def _patch_api(mocker, client: Client, return_value): """Patch client.api_request to return a fixed value.""" return mocker.patch.object(client, "api_request", return_value=return_value) # ---- Unit tests: helpers ---- class TestFilterAndLimit: def test_no_filter_no_query(self): """ Given: A mixed list of URLs and IPs with no filter or query. When: _filter_and_limit is called with empty filter and query strings. Then: The original list is returned unchanged. """ items = ["a.com", "1.2.3.4", "b.net"] result = _filter_and_limit(items, "", "", 50, False) assert result == items def test_filter_url(self): """ Given: A mixed list of URLs and IPs. When: _filter_and_limit is called with filter='url'. Then: Only URL entries (non-IP) are returned; IP addresses are excluded. """ items = ["a.com", "1.2.3.4", "b.net"] result = _filter_and_limit(items, "url", "", 50, False) assert "1.2.3.4" not in result assert "a.com" in result def test_filter_ip(self): """ Given: A mixed list of URLs and IPs. When: _filter_and_limit is called with filter='ip'. Then: Only IPv4 address entries are returned. """ items = ["a.com", "1.2.3.4", "b.net"] result = _filter_and_limit(items, "ip", "", 50, False) assert result == ["1.2.3.4"] def test_query_match(self): """ Given: A list of domain strings. When: _filter_and_limit is called with a regex query matching only one entry. Then: Only the matching entry is returned. """ items = ["malicious.com", "safe.net", "malware.org"] result = _filter_and_limit(items, "", "malicious", 50, False) assert result == ["malicious.com"] def test_limit(self): """ Given: A list of four items and a limit of 2. When: _filter_and_limit is called with all_results=False. Then: Only the first two items are returned. """ items = ["a.com", "b.com", "c.com", "d.com"] result = _filter_and_limit(items, "", "", 2, False) assert result == ["a.com", "b.com"] def test_all_results_overrides_limit(self): """ Given: A list of four items and a limit of 2. When: _filter_and_limit is called with all_results=True. Then: All four items are returned, ignoring the limit. """ items = ["a.com", "b.com", "c.com", "d.com"] result = _filter_and_limit(items, "", "", 2, True) assert result == items class TestDBotScoreCalculation: def test_miscellaneous_returns_none(self): """ Given: A URL classified as MISCELLANEOUS_OR_UNKNOWN with no security alert. When: _dbot_score_for_url is called. Then: DBotScore.NONE is returned. """ score = _dbot_score_for_url("MISCELLANEOUS_OR_UNKNOWN", "", list(SUSPICIOUS_CATEGORIES)) assert score == Common.DBotScore.NONE def test_suspicious_category(self): """ Given: A URL with a security alert category that is in the suspicious list. When: _dbot_score_for_url is called. Then: DBotScore.SUSPICIOUS is returned. """ score = _dbot_score_for_url("MALWARE_SITE", "SUSPICIOUS_DESTINATION", list(SUSPICIOUS_CATEGORIES)) assert score == Common.DBotScore.SUSPICIOUS def test_bad_category(self): """ Given: A URL with a security alert category that is NOT in the suspicious list. When: _dbot_score_for_url is called. Then: DBotScore.BAD is returned. """ score = _dbot_score_for_url("MALWARE_SITE", "MALWARE_SITE", list(SUSPICIOUS_CATEGORIES)) assert score == Common.DBotScore.BAD def test_good_no_security_alert(self): """ Given: A URL with a benign classification and no security alert. When: _dbot_score_for_url is called. Then: DBotScore.GOOD is returned. """ score = _dbot_score_for_url("BUSINESS_AND_ECONOMY", "", list(SUSPICIOUS_CATEGORIES)) assert score == Common.DBotScore.GOOD # ---- Unit tests: OAuth token caching ---- class TestTokenCaching: def test_uses_cached_token(self, mock_client, mocker): """ Given: A valid non-expired access token is cached in the integration context. When: _get_access_token is called. Then: The cached token is returned without making any HTTP request. """ http_mock = mocker.patch.object(mock_client, "_http_request") token = mock_client._get_access_token() assert token == "mock_access_token" http_mock.assert_not_called() def test_fetches_new_token_when_expired(self, mock_client, mocker): """ Given: An expired access token is in the integration context. When: _get_access_token is called. Then: A new token is fetched from ZIdentity and returned. """ token_response = load_json("/test_data/responses/token.json") mocker.patch( "ZscalerZIdentity.get_integration_context", return_value={ "access_token": "old_token", "token_expires_at": time.time() - 10, # expired }, ) mocker.patch.object( mock_client, "_http_request", return_value=token_response, ) token = mock_client._get_access_token() assert token == token_response["access_token"] def test_raises_on_missing_token(self, mock_client, mocker): """ Given: The integration context has an expired token and ZIdentity returns no access_token. When: _get_access_token is called. Then: A DemistoException is raised with a message about failing to obtain the token. """ mocker.patch( "ZscalerZIdentity.get_integration_context", return_value={"token_expires_at": time.time() - 10}, ) mocker.patch.object(mock_client, "_http_request", return_value={"error": "invalid_client"}) with pytest.raises(DemistoException, match="Failed to obtain access token"): mock_client._get_access_token() # ---- Unit tests: denylist ---- class TestDenylistCommands: def test_list_returns_items(self, mock_client, mocker): """ Given: The ZIA API returns a denylist with multiple entries. When: zia_denylist_list_command is called with no filters. Then: The result has the correct outputs_prefix and all entries appear in the output. """ data = load_json("/test_data/responses/denylist.json") _patch_api(mocker, mock_client, data) result = zia_denylist_list_command(mock_client, {}) assert result.outputs_prefix == "ZIA.DenyList" assert "malicious.com" in result.readable_output def test_list_with_filter_ip(self, mock_client, mocker): """ Given: The ZIA API returns a denylist containing both URLs and IPs. When: zia_denylist_list_command is called with filter='ip'. Then: Only IP addresses appear in the output; URL entries are excluded. """ data = load_json("/test_data/responses/denylist.json") _patch_api(mocker, mock_client, data) result = zia_denylist_list_command(mock_client, {"filter": "ip"}) assert "1.2.3.4" in result.readable_output assert "malicious.com" not in result.readable_output def test_list_with_query(self, mock_client, mocker): """ Given: The ZIA API returns a denylist with multiple entries. When: zia_denylist_list_command is called with a query matching only one entry. Then: Only the matching entry appears in the output. """ data = load_json("/test_data/responses/denylist.json") _patch_api(mocker, mock_client, data) result = zia_denylist_list_command(mock_client, {"query": "malicious"}) assert "malicious.com" in result.readable_output assert "badsite.net" not in result.readable_output def test_update_add(self, mock_client, mocker): """ Given: A URL and action=ADD_TO_LIST are provided. When: zia_denylist_update_command is called. Then: The denylist is updated and a success message is returned. """ mocker.patch.object(mock_client, "get_denylist", return_value={"blacklistUrls": []}) mocker.patch.object(mock_client, "api_request", return_value=None) result = zia_denylist_update_command(mock_client, {"url": "evil.com", "action": "ADD_TO_LIST"}) assert "successfully updated" in result.readable_output def test_update_requires_url_or_ip(self, mock_client): """ Given: Neither url nor ip argument is provided. When: zia_denylist_update_command is called. Then: A DemistoException is raised indicating at least one is required. """ with pytest.raises(DemistoException, match="At least one"): zia_denylist_update_command(mock_client, {"action": "ADD_TO_LIST"}) def test_update_requires_action(self, mock_client): """ Given: A URL is provided but the action argument is missing. When: zia_denylist_update_command is called. Then: A DemistoException is raised indicating action is required. """ with pytest.raises(DemistoException, match="'action' argument is required"): zia_denylist_update_command(mock_client, {"url": "evil.com"}) # ---- Unit tests: allowlist ---- class TestAllowlistCommands: def test_list_returns_items(self, mock_client, mocker): """ Given: The ZIA API returns an allowlist with multiple entries. When: zia_allowlist_list_command is called with no filters. Then: The result has the correct outputs_prefix and all entries appear in the output. """ data = load_json("/test_data/responses/allowlist.json") _patch_api(mocker, mock_client, data) result = zia_allowlist_list_command(mock_client, {}) assert result.outputs_prefix == "ZIA.AllowList" assert "trusted.com" in result.readable_output def test_update_add(self, mock_client, mocker): """ Given: A new URL and action=ADD_TO_LIST are provided. When: zia_allowlist_update_command is called. Then: The allowlist is updated and a success message is returned. """ mocker.patch.object(mock_client, "get_allowlist", return_value={"whitelistUrls": ["existing.com"]}) mocker.patch.object(mock_client, "api_request", return_value=None) result = zia_allowlist_update_command(mock_client, {"url": "new.com", "action": "ADD_TO_LIST"}) assert "successfully updated" in result.readable_output def test_update_requires_url_or_ip(self, mock_client): """ Given: Neither url nor ip argument is provided. When: zia_allowlist_update_command is called. Then: A DemistoException is raised indicating at least one is required. """ with pytest.raises(DemistoException, match="At least one of 'url' or 'ip'"): zia_allowlist_update_command(mock_client, {"action": "ADD_TO_LIST"}) def test_update_add_ip(self, mock_client, mocker): """ Given: A new IP and action=ADD_TO_LIST are provided. When: zia_allowlist_update_command is called with only the ip argument. Then: The allowlist is updated and a success message is returned. """ mocker.patch.object(mock_client, "get_allowlist", return_value={"whitelistUrls": ["existing.com"]}) api_mock = mocker.patch.object(mock_client, "api_request", return_value=None) result = zia_allowlist_update_command(mock_client, {"ip": "1.2.3.4", "action": "ADD_TO_LIST"}) assert "successfully updated" in result.readable_output sent_data = api_mock.call_args[1].get("data") or api_mock.call_args[0][2] assert "1.2.3.4" in sent_data["whitelistUrls"] def test_update_add_deduplicates(self, mock_client, mocker): """ Given: The allowlist already contains a URL that is being added again. When: zia_allowlist_update_command is called with action=ADD_TO_LIST. Then: The URL appears only once in the PUT payload sent to the API. """ existing = {"whitelistUrls": ["existing.com"]} mocker.patch.object(mock_client, "get_allowlist", return_value=existing) api_mock = mocker.patch.object(mock_client, "api_request", return_value=None) zia_allowlist_update_command(mock_client, {"url": "existing.com", "action": "ADD_TO_LIST"}) call_kwargs = api_mock.call_args sent_data = call_kwargs[1].get("data") or call_kwargs[0][2] assert sent_data["whitelistUrls"].count("existing.com") == 1 # ---- Unit tests: URL categories ---- class TestCategoryCommands: def test_list_all(self, mock_client, mocker): """ Given: The ZIA API returns a list of two URL categories. When: zia_category_list_command is called with no arguments. Then: The result has the correct outputs_prefix and two categories in the raw response. """ data = load_json("/test_data/responses/url_categories.json") _patch_api(mocker, mock_client, data) result = zia_category_list_command(mock_client, {}) assert result.outputs_prefix == "ZIA.Category" assert isinstance(result.raw_response, list) assert len(result.raw_response) == 2 # type: ignore[arg-type] def test_list_lite_with_other_params_raises(self, mock_client): """ Given: Both lite=true and custom_only=true are provided. When: zia_category_list_command is called. Then: A DemistoException is raised because lite cannot be combined with other parameters. """ with pytest.raises(DemistoException, match="cannot be used in combination"): zia_category_list_command(mock_client, {"lite": "true", "custom_only": "true"}) def test_update_requires_category_id(self, mock_client): """ Given: The category_id argument is missing. When: zia_category_update_command is called. Then: A DemistoException is raised indicating category_id is required. """ with pytest.raises(DemistoException, match="'category_id' argument is required"): zia_category_update_command(mock_client, {"url": "test.com", "action": "ADD_TO_LIST"}) def test_update_requires_url_or_ip(self, mock_client): """ Given: A category_id and action are provided but neither url nor ip is given. When: zia_category_update_command is called. Then: A DemistoException is raised indicating at least one of url or ip is required. """ with pytest.raises(DemistoException, match="At least one"): zia_category_update_command(mock_client, {"category_id": "MUSIC", "action": "ADD_TO_LIST"}) def test_update_success(self, mock_client, mocker): """ Given: A valid category_id, url, and action=ADD_TO_LIST are provided. When: zia_category_update_command is called. Then: The category is updated and a success message is returned. """ current_cat = { "id": "MUSIC", "configuredName": "Music", "superCategory": "ENTERTAINMENT", "customCategory": False, "urls": ["pandora.com"], "ipRanges": [], } mocker.patch.object(mock_client, "api_request", side_effect=[current_cat, None]) result = zia_category_update_command(mock_client, {"category_id": "MUSIC", "url": "spotify.com", "action": "ADD_TO_LIST"}) assert "successfully updated" in result.readable_output # ---- Unit tests: URL quota ---- class TestUrlQuotaCommand: def test_returns_quota(self, mock_client, mocker): """ Given: The ZIA API returns URL quota information. When: zia_url_quota_get_command is called. Then: The result has the correct outputs_prefix, raw_response matches the API data, and the human-readable output contains the provisioned URL count. """ data = load_json("/test_data/responses/url_quota.json") _patch_api(mocker, mock_client, data) result = zia_url_quota_get_command(mock_client, {}) assert result.outputs_prefix == "ZIA.UrlQuota" assert result.raw_response == data # type: ignore[comparison-overlap] assert "Unique Urls Provisioned" in result.readable_output assert "25000" in result.readable_output # ---- Unit tests: IP destination groups ---- class TestIPDestinationGroupCommands: def test_list_all(self, mock_client, mocker): """ Given: The ZIA API returns a list of two IP destination groups. When: zia_ip_destination_group_list_command is called with no arguments. Then: The result has the correct outputs_prefix and two groups in the raw response. """ data = load_json("/test_data/responses/ip_destination_groups.json") _patch_api(mocker, mock_client, data) result = zia_ip_destination_group_list_command(mock_client, {}) assert result.outputs_prefix == "ZIA.IPDestinationGroup" assert isinstance(result.raw_response, list) assert len(result.raw_response) == 2 # type: ignore[arg-type] def test_list_category_type_without_lite_raises(self, mock_client): """ Given: category_type is provided but lite is not set to True. When: zia_ip_destination_group_list_command is called. Then: A DemistoException is raised indicating category_type requires lite=True. """ with pytest.raises(DemistoException, match="only works with the 'lite' argument"): zia_ip_destination_group_list_command(mock_client, {"category_type": "DSTN_IP"}) def test_update_requires_group_id(self, mock_client): """ Given: The group_id argument is missing. When: zia_ip_destination_group_update_command is called. Then: A DemistoException is raised indicating group_id is required. """ with pytest.raises(DemistoException, match="'group_id' argument is required"): zia_ip_destination_group_update_command(mock_client, {}) def test_update_success(self, mock_client, mocker): """ Given: A valid group_id and a new group_name are provided. When: zia_ip_destination_group_update_command is called. Then: The group is updated, the success message appears in the output, and the raw_response matches the updated group data. """ existing = { "id": 1001, "name": "Test Group", "type": "DSTN_IP", "addresses": ["10.0.0.1"], "description": "desc", "ipCategories": [], "countries": [], } updated = {**existing, "name": "Updated Group"} mocker.patch.object(mock_client, "list_ip_destination_groups", return_value=existing) mocker.patch.object(mock_client, "update_ip_destination_group", return_value=updated) result = zia_ip_destination_group_update_command( mock_client, {"group_id": "1001", "group_name": "Updated Group", "action": "OVERWRITE"} ) assert "successfully edited" in result.readable_output assert result.raw_response == updated # type: ignore[comparison-overlap] def test_update_action_add_to_list(self, mock_client, mocker): """ Given: A valid group_id, address list, and action=ADD_TO_LIST are provided. When: zia_ip_destination_group_update_command is called. Then: The new address is appended to the existing addresses list. """ existing = { "id": 1001, "name": "Test Group", "type": "DSTN_IP", "addresses": ["10.0.0.1"], "description": "desc", "ipCategories": [], "countries": [], } updated = {**existing, "addresses": ["10.0.0.1", "10.0.0.2"]} mocker.patch.object(mock_client, "list_ip_destination_groups", return_value=existing) mock_update = mocker.patch.object(mock_client, "update_ip_destination_group", return_value=updated) zia_ip_destination_group_update_command(mock_client, {"group_id": "1001", "address": "10.0.0.2", "action": "ADD_TO_LIST"}) call_payload = mock_update.call_args[0][1] assert call_payload["addresses"] == ["10.0.0.1", "10.0.0.2"] def test_update_action_add_to_list_no_duplicates(self, mock_client, mocker): """ Given: A valid group_id, an address already in the list, and action=ADD_TO_LIST. When: zia_ip_destination_group_update_command is called. Then: The existing address is not duplicated in the resulting list. """ existing = { "id": 1001, "name": "Test Group", "type": "DSTN_IP", "addresses": ["10.0.0.1"], "description": "desc", "ipCategories": [], "countries": [], } mocker.patch.object(mock_client, "list_ip_destination_groups", return_value=existing) mock_update = mocker.patch.object(mock_client, "update_ip_destination_group", return_value=existing) zia_ip_destination_group_update_command(mock_client, {"group_id": "1001", "address": "10.0.0.1", "action": "ADD_TO_LIST"}) call_payload = mock_update.call_args[0][1] assert call_payload["addresses"] == ["10.0.0.1"] def test_update_action_remove_from_list(self, mock_client, mocker): """ Given: A valid group_id, an address to remove, and action=REMOVE_FROM_LIST. When: zia_ip_destination_group_update_command is called. Then: The specified address is removed from the addresses list. """ existing = { "id": 1001, "name": "Test Group", "type": "DSTN_IP", "addresses": ["10.0.0.1", "10.0.0.2"], "description": "desc", "ipCategories": [], "countries": [], } updated = {**existing, "addresses": ["10.0.0.1"]} mocker.patch.object(mock_client, "list_ip_destination_groups", return_value=existing) mock_update = mocker.patch.object(mock_client, "update_ip_destination_group", return_value=updated) zia_ip_destination_group_update_command( mock_client, {"group_id": "1001", "address": "10.0.0.2", "action": "REMOVE_FROM_LIST"} ) call_payload = mock_update.call_args[0][1] assert call_payload["addresses"] == ["10.0.0.1"] def test_update_action_overwrite(self, mock_client, mocker): """ Given: A valid group_id, a new address list, and action=OVERWRITE. When: zia_ip_destination_group_update_command is called. Then: The addresses list is fully replaced with the provided addresses. """ existing = { "id": 1001, "name": "Test Group", "type": "DSTN_IP", "addresses": ["10.0.0.1", "10.0.0.2"], "description": "desc", "ipCategories": [], "countries": [], } updated = {**existing, "addresses": ["192.168.1.1"]} mocker.patch.object(mock_client, "list_ip_destination_groups", return_value=existing) mock_update = mocker.patch.object(mock_client, "update_ip_destination_group", return_value=updated) zia_ip_destination_group_update_command( mock_client, {"group_id": "1001", "address": "192.168.1.1", "action": "OVERWRITE"} ) call_payload = mock_update.call_args[0][1] assert call_payload["addresses"] == ["192.168.1.1"] def test_add_success(self, mock_client, mocker): """ Given: Valid group_name, group_type, and address arguments are provided. When: zia_ip_destination_group_add_command is called. Then: A new group is created, the success message appears in the output, and the raw_response matches the created group data. """ new_group = { "id": 1003, "name": "New Group", "type": "DSTN_IP", "addresses": ["192.168.0.1"], "description": "", "ipCategories": [], "countries": [], } mocker.patch.object(mock_client, "add_ip_destination_group", return_value=new_group) result = zia_ip_destination_group_add_command( mock_client, {"group_name": "New Group", "group_type": "DSTN_IP", "address": "192.168.0.1"} ) assert "successfully added" in result.readable_output assert result.raw_response == new_group # type: ignore[comparison-overlap] def test_delete_requires_group_id(self, mock_client): """ Given: The group_id argument is missing. When: zia_ip_destination_group_delete_command is called. Then: A DemistoException is raised indicating group_id is required. """ with pytest.raises(DemistoException, match="'group_id' argument is required"): zia_ip_destination_group_delete_command(mock_client, {}) def test_delete_success(self, mock_client, mocker): """ Given: A valid group_id is provided. When: zia_ip_destination_group_delete_command is called. Then: The group is deleted and a success message is returned. """ mocker.patch.object(mock_client, "delete_ip_destination_group", return_value=None) result = zia_ip_destination_group_delete_command(mock_client, {"group_id": "1001"}) assert "successfully deleted" in result.readable_output # ---- Unit tests: users ---- class TestUserCommands: def test_list_all(self, mock_client, mocker): """ Given: The ZIA API returns a list of two users. When: zia_user_list_command is called with no arguments. Then: The result has the correct outputs_prefix, two users in the raw response, and the first user's name appears in the human-readable output. """ data = load_json("/test_data/responses/users.json") _patch_api(mocker, mock_client, data) result = zia_user_list_command(mock_client, {}) assert result.outputs_prefix == "ZIA.User" assert isinstance(result.raw_response, list) assert len(result.raw_response) == 2 # type: ignore[arg-type] assert "John Doe" in result.readable_output def test_update_requires_user_id(self, mock_client): """ Given: The user_id argument is missing. When: zia_user_update_command is called. Then: A DemistoException is raised indicating user_id is required. """ with pytest.raises(DemistoException, match="'user_id' argument is required"): zia_user_update_command(mock_client, {}) def test_update_success(self, mock_client, mocker): """ Given: A valid user_id and a new comments value are provided. When: zia_user_update_command is called. Then: The user is updated, the success heading appears in the output, and the raw_response matches the updated user data. """ existing = {"id": 1, "name": "John Doe", "email": "john@example.com", "comments": ""} updated = {**existing, "comments": "Updated comment"} mocker.patch.object(mock_client, "get_users", return_value=existing) mocker.patch.object(mock_client, "update_user", return_value=updated) result = zia_user_update_command(mock_client, {"user_id": "1", "comments": "Updated comment"}) assert "ZIA User Updated" in result.readable_output assert result.raw_response == updated # type: ignore[comparison-overlap] def test_update_with_json_user(self, mock_client, mocker): """ Given: A valid user_id and a full JSON user object are provided. When: zia_user_update_command is called. Then: The JSON object is used as the base payload and the user is updated accordingly. """ existing = {"id": 1, "name": "John Doe", "email": "john@example.com"} updated = {"id": 1, "name": "New Name", "email": "john@example.com"} mocker.patch.object(mock_client, "get_users", return_value=existing) mocker.patch.object(mock_client, "update_user", return_value=updated) result = zia_user_update_command( mock_client, {"user_id": "1", "user": '{"id": 1, "name": "New Name", "email": "john@example.com"}'} ) assert result.raw_response == updated # type: ignore[comparison-overlap] def test_update_invalid_json_raises(self, mock_client, mocker): """ Given: A user_id is provided and the 'user' argument contains invalid JSON. When: zia_user_update_command is called. Then: A DemistoException is raised indicating the JSON is invalid. """ mocker.patch.object(mock_client, "get_users", return_value={"id": 1}) with pytest.raises(DemistoException, match="Invalid JSON"): zia_user_update_command(mock_client, {"user_id": "1", "user": "not-json"}) # ---- Unit tests: groups ---- class TestGroupsCommand: def test_list_groups(self, mock_client, mocker): """ Given: The ZIA API returns a list of two user groups. When: zia_groups_list_command is called with no arguments. Then: The result has the correct outputs_prefix, two groups in the raw response, and the first group's name appears in the human-readable output. """ data = load_json("/test_data/responses/groups.json") _patch_api(mocker, mock_client, data) result = zia_groups_list_command(mock_client, {}) assert result.outputs_prefix == "ZIA.Groups" assert isinstance(result.raw_response, list) assert len(result.raw_response) == 2 # type: ignore[arg-type] assert "Engineering" in result.readable_output # ---- Unit tests: departments ---- class TestDepartmentsCommand: def test_list_departments(self, mock_client, mocker): """ Given: The ZIA API returns a list of two departments. When: zia_departments_list_command is called with no arguments. Then: The result has the correct outputs_prefix, two departments in the raw response, and the first department's name appears in the human-readable output. """ data = load_json("/test_data/responses/departments.json") _patch_api(mocker, mock_client, data) result = zia_departments_list_command(mock_client, {}) assert result.outputs_prefix == "ZIA.Department" assert isinstance(result.raw_response, list) assert len(result.raw_response) == 2 # type: ignore[arg-type] assert "IT" in result.readable_output # ---- Unit tests: sandbox report ---- class TestSandboxReportCommand: def test_malicious_report(self, mock_client, mocker): """ Given: The ZIA Sandbox API returns a report classifying the file as MALICIOUS. When: zia_sandbox_report_get_command is called with a valid MD5 hash. Then: The result has the correct outputs_prefix, a non-None indicator, a BAD DBotScore, and "Malicious" appears in the human-readable output. """ data = load_json("/test_data/responses/sandbox_report.json") _patch_api(mocker, mock_client, data) result = zia_sandbox_report_get_command( mock_client, {"md5": "abc123def456abc123def456abc123de", "report_type": "summary"} ) assert result.outputs_prefix == "ZIA.SandboxReport" assert result.indicator is not None # Access dbot_score via to_context() to avoid Pylance issues with base Indicator type ctx = result.to_context() dbot_entries = ctx.get("EntryContext", {}).get("DBotScore(val.Indicator && val.Indicator == obj.Indicator)", []) if isinstance(dbot_entries, list) and dbot_entries: assert dbot_entries[0].get("Score") == Common.DBotScore.BAD assert "3" in result.readable_output # "3" is the severity level for "Malicious" in XSOAR def test_requires_md5(self, mock_client): """ Given: The md5 argument is missing. When: zia_sandbox_report_get_command is called. Then: A DemistoException is raised indicating md5 is required. """ with pytest.raises(DemistoException, match="'md5' argument is required"): zia_sandbox_report_get_command(mock_client, {}) # ---- Unit tests: activate changes ---- class TestActivateChangesCommand: def test_activate(self, mock_client, mocker): """ Given: The ZIA API returns an activation status of ACTIVE. When: zia_activate_changes_command is called. Then: The result has the correct outputs_prefix, the raw_response matches the API data, and "ACTIVE" appears in the human-readable output. """ _patch_api(mocker, mock_client, {"status": "ACTIVE"}) result = zia_activate_changes_command(mock_client, {}) assert result.outputs_prefix == "ZIA.ActivationStatus" assert result.raw_response == {"status": "ACTIVE"} # type: ignore[comparison-overlap] assert "ACTIVE" in result.readable_output # ---- Unit tests: URL/IP/Domain lookup commands ---- class TestLookupCommands: def test_url_command_bad(self, mock_client, mocker): """ Given: The ZIA API returns a classification with a non-suspicious security alert for a URL. When: url_command is called with two URLs. Then: Two results are returned and the malicious URL has a BAD DBotScore. """ data = load_json("/test_data/responses/url_lookup.json") mocker.patch.object(mock_client, "url_lookup", return_value=data) results = url_command(mock_client, {"url": "malicious-site.com,safe-site.com"}) assert len(results) == 2 bad_result = next(r for r in results if "malicious-site.com" in r.readable_output) # Verify via to_context that DBotScore is BAD ctx = bad_result.to_context() dbot_key = next((k for k in ctx.get("EntryContext", {}) if "DBotScore" in k), None) assert dbot_key is not None dbot_val = ctx["EntryContext"][dbot_key] score = dbot_val.get("Score") if isinstance(dbot_val, dict) else dbot_val[0].get("Score") assert score == Common.DBotScore.BAD def test_url_command_good(self, mock_client, mocker): """ Given: The ZIA API returns a benign classification with no security alert for a URL. When: url_command is called with two URLs. Then: The benign URL has a GOOD DBotScore. """ data = load_json("/test_data/responses/url_lookup.json") mocker.patch.object(mock_client, "url_lookup", return_value=data) results = url_command(mock_client, {"url": "malicious-site.com,safe-site.com"}) good_result = next(r for r in results if "safe-site.com" in r.readable_output) ctx = good_result.to_context() dbot_key = next((k for k in ctx.get("EntryContext", {}) if "DBotScore" in k), None) assert dbot_key is not None dbot_val = ctx["EntryContext"][dbot_key] score = dbot_val.get("Score") if isinstance(dbot_val, dict) else dbot_val[0].get("Score") assert score == Common.DBotScore.GOOD def test_ip_command(self, mock_client, mocker): """ Given: The ZIA API returns a benign classification for an IP address. When: ip_command is called with a single IP. Then: One result is returned and the IP address appears in the human-readable output. """ data = [{"url": "8.8.8.8", "urlClassifications": ["DNS"], "urlClassificationsWithSecurityAlert": []}] mocker.patch.object(mock_client, "url_lookup", return_value=data) results = ip_command(mock_client, {"ip": "8.8.8.8"}) assert len(results) == 1 assert "8.8.8.8" in results[0].readable_output def test_domain_command(self, mock_client, mocker): """ Given: The ZIA API returns a benign classification for a domain. When: domain_command is called with a single domain. Then: One result is returned and the domain appears in the human-readable output. """ data = [{"url": "example.com", "urlClassifications": ["BUSINESS_AND_ECONOMY"], "urlClassificationsWithSecurityAlert": []}] mocker.patch.object(mock_client, "url_lookup", return_value=data) results = domain_command(mock_client, {"domain": "example.com"}) assert len(results) == 1 assert "example.com" in results[0].readable_output def test_url_command_suspicious(self, mock_client, mocker): """ Given: The ZIA API returns a security alert category that is in the suspicious list. When: url_command is called with the URL. Then: The result has a SUSPICIOUS DBotScore. """ data = [ { "url": "spyware-site.com", "urlClassifications": ["SPYWARE_OR_ADWARE"], "urlClassificationsWithSecurityAlert": ["SPYWARE_OR_ADWARE"], } ] mocker.patch.object(mock_client, "url_lookup", return_value=data) results = url_command(mock_client, {"url": "spyware-site.com"}) ctx = results[0].to_context() dbot_key = next((k for k in ctx.get("EntryContext", {}) if "DBotScore" in k), None) assert dbot_key is not None dbot_val = ctx["EntryContext"][dbot_key] score = dbot_val.get("Score") if isinstance(dbot_val, dict) else dbot_val[0].get("Score") assert score == Common.DBotScore.SUSPICIOUS def test_url_command_empty_response(self, mock_client, mocker): """ Given: The ZIA API returns an empty list for the URL lookup. When: url_command is called. Then: A single result with "No results found" in the readable output is returned. """ mocker.patch.object(mock_client, "url_lookup", return_value=[]) results = url_command(mock_client, {"url": "test.com"}) assert len(results) == 1 assert "No results found" in results[0].readable_output # ---- Unit tests: client allowlist update logic ---- class TestClientAllowlistUpdate: def test_add_to_list(self, mock_client, mocker): """ Given: The current allowlist contains "a.com" and "b.com" is being added. When: update_allowlist is called with action=ADD_TO_LIST. Then: Both "a.com" and "b.com" are present in the PUT payload sent to the API. """ mocker.patch.object(mock_client, "get_allowlist", return_value={"whitelistUrls": ["a.com"]}) api_mock = mocker.patch.object(mock_client, "api_request", return_value=None) mock_client.update_allowlist(["b.com"], "ADD_TO_LIST") sent = api_mock.call_args[1]["data"] assert "a.com" in sent["whitelistUrls"] assert "b.com" in sent["whitelistUrls"] def test_remove_from_list(self, mock_client, mocker): """ Given: The current allowlist contains "a.com" and "b.com", and "a.com" is being removed. When: update_allowlist is called with action=REMOVE_FROM_LIST. Then: "a.com" is absent and "b.com" is present in the PUT payload sent to the API. """ mocker.patch.object(mock_client, "get_allowlist", return_value={"whitelistUrls": ["a.com", "b.com"]}) api_mock = mocker.patch.object(mock_client, "api_request", return_value=None) mock_client.update_allowlist(["a.com"], "REMOVE_FROM_LIST") sent = api_mock.call_args[1]["data"] assert "a.com" not in sent["whitelistUrls"] assert "b.com" in sent["whitelistUrls"] def test_overwrite(self, mock_client, mocker): """ Given: The current allowlist contains "a.com" and "b.com", and OVERWRITE with "c.com" is requested. When: update_allowlist is called with action=OVERWRITE. Then: Only "c.com" is present in the PUT payload sent to the API. """ mocker.patch.object(mock_client, "get_allowlist", return_value={"whitelistUrls": ["a.com", "b.com"]}) api_mock = mocker.patch.object(mock_client, "api_request", return_value=None) mock_client.update_allowlist(["c.com"], "OVERWRITE") sent = api_mock.call_args[1]["data"] assert sent["whitelistUrls"] == ["c.com"] # ---- Unit tests: 401 retry logic ---- def _make_401_exception() -> DemistoException: """Build a DemistoException whose .res.status_code is 401, matching the production error-handler output.""" exc = DemistoException("Authentication/Authorization error (401): Unauthorized.") mock_res = type("MockResponse", (), {"status_code": 401})() exc.res = mock_res return exc def _make_500_exception() -> DemistoException: """Build a DemistoException whose .res.status_code is 500.""" exc = DemistoException("The request failed with status code 500.") mock_res = type("MockResponse", (), {"status_code": 500})() exc.res = mock_res return exc class TestApiRequest401Retry: def test_401_clears_token_and_retries(self, mock_client, mocker): """ Given: The first API call raises a DemistoException with res.status_code == 401 (simulating a stale cached token due to clock drift). When: api_request is called. Then: The cached token is cleared from integration context, a new token is fetched, and the request is retried exactly once, returning the successful response from the retry. """ success_response = {"data": "ok"} mock_do_http = mocker.patch.object( mock_client, "_do_http_request", side_effect=[ _make_401_exception(), success_response, ], ) mock_set_ctx = mocker.patch("ZscalerZIdentity.set_integration_context") result = mock_client.api_request("GET", "/some/endpoint") assert result == success_response assert mock_do_http.call_count == 2 # Verify the cached token was cleared from integration context cleared_ctx = mock_set_ctx.call_args[0][0] assert "access_token" not in cleared_ctx assert "token_expires_at" not in cleared_ctx def test_non_401_exception_is_reraised(self, mock_client, mocker): """ Given: The API call raises a DemistoException with res.status_code == 500 (not a 401 auth error). When: api_request is called. Then: The exception is re-raised without retrying and without clearing the integration context. """ mocker.patch.object( mock_client, "_do_http_request", side_effect=_make_500_exception(), ) mock_set_ctx = mocker.patch("ZscalerZIdentity.set_integration_context") with pytest.raises(DemistoException, match="500"): mock_client.api_request("GET", "/some/endpoint") mock_set_ctx.assert_not_called() def test_401_retry_also_fails_raises_exception(self, mock_client, mocker): """ Given: Both the initial request and the retry after token refresh raise a DemistoException with res.status_code == 401. When: api_request is called. Then: The exception from the retry is propagated to the caller. """ mocker.patch.object( mock_client, "_do_http_request", side_effect=[ _make_401_exception(), _make_401_exception(), ], ) mocker.patch("ZscalerZIdentity.set_integration_context") with pytest.raises(DemistoException, match="401"): mock_client.api_request("GET", "/some/endpoint") def test_successful_request_does_not_clear_token(self, mock_client, mocker): """ Given: The API call succeeds on the first attempt (no 401). When: api_request is called. Then: The integration context is never modified (token is not cleared). """ success_response = {"data": "success"} mocker.patch.object(mock_client, "_do_http_request", return_value=success_response) mock_set_ctx = mocker.patch("ZscalerZIdentity.set_integration_context") result = mock_client.api_request("GET", "/some/endpoint") assert result == success_response mock_set_ctx.assert_not_called() def test_exception_without_res_attribute_is_reraised(self, mock_client, mocker): """ Given: The API call raises a DemistoException that has no .res attribute (e.g. a network-level error before any HTTP response). When: api_request is called. Then: The exception is re-raised without retrying and without clearing the integration context. """ exc = DemistoException("Connection error: no response received.") # No .res attribute set — simulates a network-level failure mocker.patch.object(mock_client, "_do_http_request", side_effect=exc) mock_set_ctx = mocker.patch("ZscalerZIdentity.set_integration_context") with pytest.raises(DemistoException, match="Connection error"): mock_client.api_request("GET", "/some/endpoint") mock_set_ctx.assert_not_called() class TestErrorHandler: """Covers the authentication branch of Client._error_handler, which must attach the originating response to the raised DemistoException so that api_request's 401 retry logic can detect it.""" @staticmethod def _mock_response(status_code: int, text: str = "Unauthorized"): """Build a minimal stand-in for a requests.Response as consumed by _error_handler.""" return type("MockResponse", (), {"status_code": status_code, "text": text})() @pytest.mark.parametrize("status_code", [401, 403]) def test_auth_error_attaches_response_to_exception(self, mock_client, status_code): """ Given: The API returns an authentication/authorization failure (401 or 403). When: _error_handler is called with that response. Then: The raised DemistoException carries the originating response as .res, so callers can inspect the status code. """ res = self._mock_response(status_code) with pytest.raises(DemistoException) as exc_info: mock_client._error_handler(res) assert exc_info.value.res is res def test_401_from_error_handler_triggers_token_refresh_and_retry(self, mock_client, mocker): """ Given: A 401 exception produced by the real _error_handler rather than a hand-built one. When: api_request receives it from the underlying request. Then: The retry logic recognizes the 401, clears the cached token and retries, proving the contract between _error_handler and api_request holds. """ with pytest.raises(DemistoException) as exc_info: mock_client._error_handler(self._mock_response(401)) success_response = {"data": "ok"} mock_do_http = mocker.patch.object( mock_client, "_do_http_request", side_effect=[exc_info.value, success_response], ) mock_set_ctx = mocker.patch("ZscalerZIdentity.set_integration_context") result = mock_client.api_request("GET", "/some/endpoint") assert result == success_response assert mock_do_http.call_count == 2 cleared_ctx = mock_set_ctx.call_args[0][0] assert "access_token" not in cleared_ctx assert "token_expires_at" not in cleared_ctx