zscaler-sdk-python 0.8.0__tar.gz → 0.9.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/PKG-INFO +2 -2
  2. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/pyproject.toml +2 -2
  3. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/__init__.py +2 -1
  4. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/utils.py +1 -5
  5. zscaler_sdk_python-0.9.1/zscaler/zcc/__init__.py +265 -0
  6. zscaler_sdk_python-0.9.1/zscaler/zcc/client.py +65 -0
  7. zscaler_sdk_python-0.9.1/zscaler/zcc/devices.py +220 -0
  8. zscaler_sdk_python-0.9.1/zscaler/zcc/secrets.py +66 -0
  9. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zcon/__init__.py +1 -1
  10. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/cloudappcontrol.py +21 -4
  11. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/dlp.py +7 -13
  12. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/url_categories.py +1 -2
  13. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/segment_groups.py +43 -0
  14. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/LICENSE.md +0 -0
  15. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/README.md +0 -0
  16. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/cache/__init__.py +0 -0
  17. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/cache/cache.py +0 -0
  18. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/cache/no_op_cache.py +0 -0
  19. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/cache/zscaler_cache.py +0 -0
  20. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/constants.py +0 -0
  21. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/errors/__init__.py +0 -0
  22. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/errors/error.py +0 -0
  23. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/errors/http_error.py +0 -0
  24. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/errors/zscaler_api_error.py +0 -0
  25. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/exceptions/__init__.py +0 -0
  26. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/exceptions/exceptions.py +0 -0
  27. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/logger.py +0 -0
  28. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/ratelimiter/__init__.py +0 -0
  29. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/ratelimiter/ratelimiter.py +0 -0
  30. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/user_agent.py +0 -0
  31. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zcon/activation.py +0 -0
  32. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zcon/admin_and_role_management.py +0 -0
  33. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zcon/client.py +0 -0
  34. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zcon/ecgroups.py +0 -0
  35. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zcon/locations.py +0 -0
  36. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zcon/provisioning.py +0 -0
  37. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zdx/__init__.py +0 -0
  38. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zdx/admin.py +0 -0
  39. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zdx/alerts.py +0 -0
  40. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zdx/apps.py +0 -0
  41. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zdx/devices.py +0 -0
  42. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zdx/filters.py +0 -0
  43. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zdx/inventory.py +0 -0
  44. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zdx/troubleshooting.py +0 -0
  45. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zdx/users.py +0 -0
  46. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zdx/zdx_client.py +0 -0
  47. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/__init__.py +0 -0
  48. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/activate.py +0 -0
  49. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/admin_and_role_management.py +0 -0
  50. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/apptotal.py +0 -0
  51. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/audit_logs.py +0 -0
  52. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/authentication_settings.py +0 -0
  53. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/client.py +0 -0
  54. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/cloud_apps.py +0 -0
  55. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/device_management.py +0 -0
  56. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/errors.py +0 -0
  57. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/firewall.py +0 -0
  58. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/forwarding_control.py +0 -0
  59. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/isolation_profile.py +0 -0
  60. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/labels.py +0 -0
  61. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/locations.py +0 -0
  62. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/sandbox.py +0 -0
  63. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/security.py +0 -0
  64. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/ssl_inspection.py +0 -0
  65. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/traffic.py +0 -0
  66. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/url_filtering.py +0 -0
  67. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/users.py +0 -0
  68. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/web_dlp.py +0 -0
  69. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/workload_groups.py +0 -0
  70. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zia/zpa_gateway.py +0 -0
  71. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/README.md +0 -0
  72. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/__init__.py +0 -0
  73. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/app_segments.py +0 -0
  74. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/app_segments_inspection.py +0 -0
  75. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/app_segments_pra.py +0 -0
  76. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/authdomains.py +0 -0
  77. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/certificates.py +0 -0
  78. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/client.py +0 -0
  79. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/cloud_connector_groups.py +0 -0
  80. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/connectors.py +0 -0
  81. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/emergency_access.py +0 -0
  82. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/errors.py +0 -0
  83. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/idp.py +0 -0
  84. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/inspection.py +0 -0
  85. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/isolation.py +0 -0
  86. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/lss.py +0 -0
  87. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/machine_groups.py +0 -0
  88. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/microtenants.py +0 -0
  89. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/policies.py +0 -0
  90. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/posture_profiles.py +0 -0
  91. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/privileged_remote_access.py +0 -0
  92. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/provisioning.py +0 -0
  93. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/saml_attributes.py +0 -0
  94. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/scim_attributes.py +0 -0
  95. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/scim_groups.py +0 -0
  96. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/server_groups.py +0 -0
  97. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/servers.py +0 -0
  98. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/service_edges.py +0 -0
  99. {zscaler_sdk_python-0.8.0 → zscaler_sdk_python-0.9.1}/zscaler/zpa/trusted_networks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: zscaler-sdk-python
3
- Version: 0.8.0
3
+ Version: 0.9.1
4
4
  Summary: Official Python SDK for the Zscaler Products (Beta)
5
5
  Home-page: https://github.com/zscaler/zscaler-sdk-python
6
6
  License: MIT
@@ -34,7 +34,7 @@ Requires-Dist: idna
34
34
  Requires-Dist: okta (>=2.9.7)
35
35
  Requires-Dist: pycryptodomex
36
36
  Requires-Dist: pydash (>=8.0.3) ; extra == "dev"
37
- Requires-Dist: python-box (>=7.2.0,<8.0.0)
37
+ Requires-Dist: python-box (>=7.2.0)
38
38
  Requires-Dist: python-dateutil
39
39
  Requires-Dist: pytz
40
40
  Requires-Dist: pyyaml
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "zscaler-sdk-python"
3
- version = "0.8.0"
3
+ version = "0.9.1"
4
4
  description = "Official Python SDK for the Zscaler Products (Beta)"
5
5
  authors = ["Zscaler, Inc. <devrel@zscaler.com>"]
6
6
  license = "MIT"
@@ -33,7 +33,7 @@ arrow = "*"
33
33
  certifi = ">=2024.2.2"
34
34
  charset-normalizer = "*"
35
35
  idna = "*"
36
- python-box = "^7.2.0"
36
+ python-box = ">=7.2.0"
37
37
  python-dateutil = "*"
38
38
  requests = ">=2.32.3"
39
39
  responses = ">=0.25.3"
@@ -29,9 +29,10 @@ __license__ = "MIT"
29
29
  __contributors__ = [
30
30
  "William Guilherme",
31
31
  ]
32
- __version__ = "0.8.0"
32
+ __version__ = "0.9.1"
33
33
 
34
34
  from zscaler.zdx import ZDXClientHelper # noqa
35
35
  from zscaler.zia import ZIAClientHelper # noqa
36
36
  from zscaler.zpa import ZPAClientHelper # noqa
37
37
  from zscaler.zcon import ZCONClientHelper # noqa
38
+ from zscaler.zcc import ZCCClientHelper # noqa
@@ -184,11 +184,7 @@ def transform_common_id_fields(id_groups: list, kwargs: dict, payload: dict):
184
184
  elif key == "cbi_profile":
185
185
  # Special handling for cbi_profile
186
186
  if isinstance(value, dict) and all(k in value for k in ["id", "name", "url"]):
187
- payload[payload_key] = {
188
- "id": value["id"],
189
- "name": value["name"],
190
- "url": value["url"]
191
- }
187
+ payload[payload_key] = {"id": value["id"], "name": value["name"], "url": value["url"]}
192
188
  elif key == "cloud_app_risk_profile":
193
189
  # Special handling for cloudAppRiskProfile
194
190
  if isinstance(value, dict) and "id" in value:
@@ -0,0 +1,265 @@
1
+ import logging
2
+ import os
3
+ import urllib.parse
4
+ import uuid
5
+ import time
6
+ import requests
7
+ from datetime import datetime, timedelta
8
+
9
+ from zscaler import __version__
10
+ from zscaler.user_agent import UserAgent
11
+ from zscaler.utils import (
12
+ dump_request,
13
+ dump_response,
14
+ format_json_response,
15
+ is_token_expired,
16
+ retry_with_backoff,
17
+ )
18
+ from zscaler.logger import setup_logging
19
+ from .devices import DevicesAPI
20
+ from .secrets import SecretsAPI
21
+
22
+ from zscaler.zcc.client import ZCCClient
23
+
24
+ # Setup the logger
25
+ setup_logging(logger_name="zscaler-sdk-python")
26
+ logger = logging.getLogger("zscaler-sdk-python")
27
+
28
+
29
+ class ZCCClientHelper(ZCCClient):
30
+ """
31
+ A Controller to access Endpoints in the Zscaler Mobile Admin Portal API.
32
+
33
+ The ZCC object stores the session token and simplifies access to CRUD options within the ZCC Portal.
34
+
35
+ Attributes:
36
+ client_id (str): The ZCC Client ID generated from the ZCC Portal.
37
+ client_secret (str): The ZCC Client Secret generated from the ZCC Portal.
38
+ cloud (str): The Zscaler cloud for your tenancy, accepted values are:
39
+
40
+ * ``zscaler``
41
+ * ``zscalerone``
42
+ * ``zscalertwo``
43
+ * ``zscalerthree``
44
+ * ``zscloud``
45
+ * ``zscalerbeta``
46
+ override_url (str):
47
+ If supplied, this attribute can be used to override the production URL that is derived
48
+ from supplying the `cloud` attribute. Use this attribute if you have a non-standard tenant URL
49
+ (e.g. internal test instance etc). When using this attribute, there is no need to supply the `cloud`
50
+ attribute. The override URL will be prepended to the API endpoint suffixes. The protocol must be included
51
+ i.e. http:// or https://.
52
+
53
+ """
54
+
55
+ _vendor = "Zscaler"
56
+ _product = "Zscaler Mobile Admin Portal"
57
+ _backoff = 3
58
+ _build = __version__
59
+ _env_base = "ZCC"
60
+ _env_cloud = "zscaler"
61
+
62
+ RATE_LIMIT = 100 # 100 API calls per hour
63
+ DOWNLOAD_DEVICES_LIMIT = 3 # 3 calls per day
64
+ RATE_LIMIT_RESET_TIME = timedelta(hours=1)
65
+ DOWNLOAD_DEVICES_RESET_TIME = timedelta(days=1)
66
+
67
+ def __init__(self, **kw):
68
+ self._apikey = kw.get("apikey", os.getenv(f"{self._env_base}_CLIENT_ID"))
69
+ self._secret_key = kw.get("secret_key", os.getenv(f"{self._env_base}_CLIENT_SECRET"))
70
+ self._env_cloud = (
71
+ os.getenv(f"{self._env_base}_CLOUD") if os.getenv(f"{self._env_base}_CLOUD") is not None else kw.get("cloud")
72
+ )
73
+ self.login_url = f"https://api-mobile.{self._env_cloud}.net/papi/auth/v1/login"
74
+ self.url = f"https://api-mobile.{self._env_cloud}.net/papi/public/v1"
75
+
76
+ self.user_agent = UserAgent().get_user_agent_string() # Ensure this returns a string
77
+ self.auth_token = None
78
+ self.headers = {}
79
+ self.refreshToken()
80
+
81
+ # Initialize rate limit tracking
82
+ self.last_request_time = datetime.utcnow()
83
+ self.request_count = 0
84
+
85
+ # Track specific download devices endpoint usage
86
+ self.download_devices_count = 0
87
+ self.download_devices_last_reset = datetime.utcnow()
88
+
89
+ def __enter__(self):
90
+ self.refreshToken()
91
+ return self
92
+
93
+ def __exit__(self, exc_type, exc_val, exc_tb):
94
+ logger.debug("Deauthenticating...")
95
+
96
+ def refreshToken(self):
97
+ if not self.auth_token or is_token_expired(self.auth_token):
98
+ response = self.login()
99
+ if response is None or response.status_code > 299 or not response.json():
100
+ logger.error("Failed to login using provided credentials, response: %s", response)
101
+ raise Exception("Failed to login using provided credentials.")
102
+ self.auth_token = response.json().get("jwtToken")
103
+ self.headers = {
104
+ "Content-Type": "application/json",
105
+ "Accept": "*/*",
106
+ "auth-token": f"{self.auth_token}",
107
+ "User-Agent": self.user_agent,
108
+ }
109
+
110
+ @retry_with_backoff(retries=5)
111
+ def login(self):
112
+ data = {"apiKey": self._apikey, "secretKey": self._secret_key}
113
+ headers = {
114
+ "Content-Type": "application/json",
115
+ "Accept": "*/*",
116
+ "User-Agent": self.user_agent,
117
+ }
118
+ try:
119
+ url = self.login_url
120
+ resp = requests.post(url, json=data, headers=headers)
121
+ logger.info("Login attempt with status: %d", resp.status_code)
122
+ return resp
123
+ except Exception as e:
124
+ logger.error("Login failed due to an exception: %s", str(e))
125
+ return None
126
+
127
+ def check_rate_limit(self, path):
128
+ """
129
+ Checks the rate limit and adjusts the request timing accordingly.
130
+ """
131
+ current_time = datetime.utcnow()
132
+ time_since_last_request = current_time - self.last_request_time
133
+
134
+ # Reset the rate limit counter if the reset time has passed
135
+ if time_since_last_request >= self.RATE_LIMIT_RESET_TIME:
136
+ self.request_count = 0
137
+ self.last_request_time = current_time
138
+
139
+ if "/downloadDevices" in path:
140
+ time_since_last_reset = current_time - self.download_devices_last_reset
141
+ if time_since_last_reset >= self.DOWNLOAD_DEVICES_RESET_TIME:
142
+ self.download_devices_count = 0
143
+ self.download_devices_last_reset = current_time
144
+ if self.download_devices_count >= self.DOWNLOAD_DEVICES_LIMIT:
145
+ logger.warning("Rate limit exceeded for /downloadDevices endpoint. Backing off...")
146
+ time.sleep(24 * 60 * 60) # Back off for a day
147
+ self.download_devices_count = 0
148
+ self.download_devices_last_reset = datetime.utcnow()
149
+ self.download_devices_count += 1
150
+ else:
151
+ if self.request_count >= self.RATE_LIMIT:
152
+ logger.warning("Rate limit exceeded. Backing off...")
153
+ time.sleep(3600) # Back off for an hour
154
+ self.request_count = 0
155
+ self.last_request_time = datetime.utcnow()
156
+ self.request_count += 1
157
+
158
+ def send(self, method, path, json=None, params=None, stream=False):
159
+ api = self.url
160
+ if params is None:
161
+ params = {}
162
+ url = f"{api}/{path.lstrip('/')}"
163
+ if params:
164
+ url = f"{url}?{urllib.parse.urlencode(params)}"
165
+
166
+ start_time = time.time()
167
+ headers_with_user_agent = self.headers.copy()
168
+ headers_with_user_agent["User-Agent"] = self.user_agent
169
+ request_uuid = str(uuid.uuid4())
170
+ dump_request(logger, url, method, json, None, headers_with_user_agent, request_uuid)
171
+
172
+ # Check rate limits
173
+ self.check_rate_limit(path)
174
+
175
+ attempts = 0
176
+ while attempts < 5:
177
+ try:
178
+ self.refreshToken()
179
+ resp = requests.request(
180
+ method,
181
+ url,
182
+ json=json,
183
+ params=None,
184
+ headers=headers_with_user_agent,
185
+ stream=stream,
186
+ )
187
+ dump_response(
188
+ logger=logger,
189
+ url=url,
190
+ params=None,
191
+ method=method,
192
+ resp=resp,
193
+ request_uuid=request_uuid,
194
+ start_time=start_time,
195
+ )
196
+ if resp.status_code == 429:
197
+ retry_after = resp.headers.get("Retry-After")
198
+ if retry_after:
199
+ try:
200
+ sleep_time = int(retry_after)
201
+ except ValueError:
202
+ sleep_time = int(retry_after[:-1])
203
+ logger.warning(f"Rate limit exceeded. Retrying in {sleep_time} seconds.")
204
+ time.sleep(sleep_time)
205
+ else:
206
+ time.sleep(60)
207
+ attempts += 1
208
+ continue
209
+ else:
210
+ break
211
+ except requests.RequestException as e:
212
+ if attempts == 4:
213
+ logger.error(f"Failed to send {method} request to {url} after 5 attempts. Error: {str(e)}")
214
+ raise e
215
+ else:
216
+ logger.warning(f"Failed to send {method} request to {url}. Retrying... Error: {str(e)}")
217
+ attempts += 1
218
+ time.sleep(5)
219
+
220
+ return resp
221
+
222
+ def get(self, path, json=None, params=None, stream=False):
223
+ """
224
+ Send a GET request to the ZCC API.
225
+
226
+ Parameters:
227
+ path (str): API endpoint path.
228
+ json (dict, optional): Request payload. Defaults to None.
229
+ params (dict, optional): Query parameters. Defaults to None.
230
+ stream (bool, optional): Whether to stream the response content. Defaults to False.
231
+
232
+ Returns:
233
+ Response: The response object from the API request.
234
+ """
235
+ resp = self.send("GET", path, json, params, stream=stream)
236
+ if stream:
237
+ return resp
238
+ formatted_resp = format_json_response(resp, box_attrs=dict())
239
+ return formatted_resp
240
+
241
+ def post(self, path, json=None, params=None):
242
+ """
243
+ Send a POST request to the ZCC API.
244
+
245
+ Parameters:
246
+ path (str): API endpoint path.
247
+ json (dict, optional): Request payload. Defaults to None.
248
+ params (dict, optional): Query parameters. Defaults to None.
249
+
250
+ Returns:
251
+ dict: Formatted JSON response from the API.
252
+ """
253
+ resp = self.send("POST", path, json, params)
254
+ formatted_resp = format_json_response(resp, box_attrs=dict())
255
+ return formatted_resp
256
+
257
+ @property
258
+ def devices(self):
259
+ """The interface object for the :ref:`ZCC Devices interface <zcc-devices>`."""
260
+ return DevicesAPI(self)
261
+
262
+ @property
263
+ def secrets(self):
264
+ """The interface object for the :ref:`ZCC Secrets interface <zcc-secrets>`."""
265
+ return SecretsAPI(self)
@@ -0,0 +1,65 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Copyright (c) 2023, Zscaler Inc.
4
+ #
5
+ # Permission to use, copy, modify, and/or distribute this software for any
6
+ # purpose with or without fee is hereby granted, provided that the above
7
+ # copyright notice and this permission notice appear in all copies.
8
+ #
9
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
17
+
18
+ class ZCCClient:
19
+ def __init__():
20
+ pass
21
+
22
+ def get(
23
+ self,
24
+ path: str,
25
+ json=None,
26
+ params=None,
27
+ stream=False,
28
+ ):
29
+ """
30
+ Send a GET request to the ZCC API.
31
+ Parameters:
32
+ - path (str): API endpoint path.
33
+ - json (str): the request body.
34
+ - params (dict): the query params
35
+ """
36
+ pass
37
+
38
+ def get_paginated_data(
39
+ self,
40
+ path=None,
41
+ params=None,
42
+ search=None,
43
+ search_field="name",
44
+ page=None,
45
+ pagesize=20,
46
+ ):
47
+ """
48
+ Send a GET request to the ZCC API to fetch all pages of a resources.
49
+ Parameters:
50
+ - path (str): API endpoint path.
51
+ - data_key_name (str): list field key.
52
+ - data_per_page: the page size
53
+ - params (dict): the query params
54
+ """
55
+ pass
56
+
57
+ def post(self, path: str, json=None, params=None):
58
+ """
59
+ Send a POST request to the ZCC API.
60
+ Parameters:
61
+ - path (str): API endpoint path.
62
+ - json (str): the request body.
63
+ - params (dict): the query params
64
+ """
65
+ pass
@@ -0,0 +1,220 @@
1
+ import shutil
2
+ from datetime import datetime
3
+ from box import BoxList
4
+ from zscaler.zcc.client import ZCCClient
5
+ from zscaler.utils import convert_keys, zcc_param_map
6
+
7
+
8
+ class DevicesAPI:
9
+
10
+ def __init__(self, client: ZCCClient):
11
+ self.rest = client
12
+
13
+ def download_devices(
14
+ self,
15
+ filename: str = None,
16
+ os_types: list = None,
17
+ registration_types: list = None,
18
+ ):
19
+ """
20
+ Downloads the list of devices in the Client Connector Portal as a CSV file.
21
+
22
+ By default, this method will create a file named `zcc-devices-YYmmDD-HH_MM_SS.csv`. This can be overridden by
23
+ specifying the ``filename`` argument.
24
+
25
+ Notes:
26
+ This API endpoint is heavily rate-limited by Zscaler and as of NOV 2022 only 3 calls per-day are allowed.
27
+
28
+ Args:
29
+ filename (str):
30
+ The name of the file that you want to save to disk.
31
+ os_types (list):
32
+ A list of OS Types to filter the device list. Omitting this argument will result in all OS types being
33
+ matched.
34
+ Valid options are:
35
+
36
+ - ios
37
+ - android
38
+ - windows
39
+ - macos
40
+ - linux
41
+ registration_types (list):
42
+ A list of device registration states to filter the device list.
43
+ Valid options are:
44
+
45
+ - all (provides all states except for 'removed')
46
+ - registered
47
+ - removal_pending
48
+ - unregistered
49
+ - removed
50
+ - quarantined
51
+
52
+ Returns:
53
+ :obj:`str`: The local filename for the CSV file that was downloaded.
54
+
55
+ Examples:
56
+ Create a CSV with all OS types and all registration types:
57
+
58
+ >>> zcc.devices.download_devices(registration_types=["all", "removed"])
59
+
60
+ Create a CSV for Windows and macOS devices that are in the `registered` state:
61
+
62
+ >>> zcc.devices.download_devices(os_types=["windows", "macos"],
63
+ ... registration_types=["registered"])
64
+
65
+ Create a CSV with filename `unregistered.csv` for devices in the unregistered state:
66
+
67
+ >>> zcc.devices.download_devices(filename="unregistered.csv",
68
+ ... registration_types=["unregistered"])
69
+
70
+ """
71
+
72
+ if not filename:
73
+ filename = f"zcc-devices-{datetime.now().strftime('%Y%m%d-%H_%M_%S')}.csv"
74
+
75
+ params = {}
76
+
77
+ # Simplify the os_type argument, raise an error if the user supplies the wrong one.
78
+ if os_types:
79
+ for item in os_types:
80
+ os_type = zcc_param_map["os"].get(item, None)
81
+ if os_type:
82
+ if "osTypes" not in params:
83
+ params["osTypes"] = str(os_type)
84
+ else:
85
+ params["osTypes"] += "," + str(os_type)
86
+ else:
87
+ raise ValueError("Invalid os_type specified. Check the pyZscaler documentation for valid os_type options.")
88
+
89
+ # Simplify the registration_type argument, raise an error if the user supplies the wrong one.
90
+ if registration_types:
91
+ for item in registration_types:
92
+ reg_type = zcc_param_map["reg_type"].get(item, None)
93
+ if reg_type:
94
+ if "registrationTypes" not in params:
95
+ params["registrationTypes"] = str(reg_type)
96
+ else:
97
+ params["registrationTypes"] += "," + str(reg_type)
98
+ else:
99
+ raise ValueError(
100
+ "Invalid registration_type specified. Check the pyZscaler documentation for valid "
101
+ "registration_type options."
102
+ )
103
+
104
+ # Create the local file and stream the device list csv to it
105
+ with self.rest.get("downloadDevices", params=params, stream=True) as r:
106
+ with open(filename, "wb") as f:
107
+ f.write(r.content)
108
+
109
+ return filename
110
+
111
+ def list_devices(self, **kwargs) -> BoxList:
112
+ """
113
+ Returns the list of devices enrolled in the Client Connector Portal.
114
+
115
+ Keyword Args:
116
+ os_type (str):
117
+ Filter by device operating system. Valid options are:
118
+
119
+ - ios
120
+ - android
121
+ - windows
122
+ - macos
123
+ - linux
124
+ page (int):
125
+ Return a specific page number.
126
+ page_size (int):
127
+ Specify the number of devices per page, defaults to ``30``.
128
+ user_name (str):
129
+ Filter by the enrolled user for the device.
130
+
131
+ Returns:
132
+ :obj:`BoxList`: A list containing devices using ZCC enrolled in the Client Connector Portal.
133
+
134
+ Examples:
135
+ Prints all devices in the Client Connector Portal to the console:
136
+
137
+ >>> for device in zcc.devices.list_devices():
138
+ ... print(device)
139
+
140
+ """
141
+ payload = convert_keys(dict(kwargs))
142
+
143
+ # Simplify the os_type argument, raise an error if the user supplies the wrong one.
144
+ if kwargs.get("os_type"):
145
+ os_type = zcc_param_map["os"].get(payload["osType"], None)
146
+ if os_type:
147
+ payload["osType"] = os_type
148
+ else:
149
+ raise ValueError("Invalid os_type specified. Check the Zscaler documentation for valid os_type options.")
150
+
151
+ return self.rest.get("getDevices", **payload)
152
+
153
+ def remove_devices(self, force: bool = False, **kwargs):
154
+ """
155
+ Removes the specified devices from the Zscaler Client Connector Portal.
156
+
157
+ Notes:
158
+ You must be using API credentials with the `Write` role.
159
+ You must specify at least one criterion from `Keyword Args` to remove devices.
160
+
161
+ Args:
162
+ force (bool):
163
+ Setting force to ``True`` removes the enrolled device from the portal. You can only remove devices that
164
+ are in the `registered` or `device removal pending` state.
165
+ **kwargs:
166
+ Optional keyword args.
167
+
168
+ Keyword Args:
169
+ client_connector_version (list):
170
+ A list of client connector versions that will be removed. You must supply the exact version number, i.e.
171
+ if the Client Connector version is `3.2.0.18` you must specify `3.2.0.18` and not `3.2`.
172
+ os_type (str):
173
+ The OS Type for the devices to be removed. Valid options are:
174
+
175
+ - ios
176
+ - android
177
+ - windows
178
+ - macos
179
+ - linux
180
+ udids (list):
181
+ A list of Unique Device IDs.
182
+ user_name (str):
183
+ The username of the user whose devices will be removed.
184
+
185
+ Returns:
186
+ :obj:`Box`: Server response containing the total number of devices removed.
187
+
188
+ Examples:
189
+ Soft-remove devices using ZCC version 3.7.1.44 from the Client Connector Portal:
190
+
191
+ >>> zcc.devices.remove_devices(client_connector_version=["3.7.1.44"])
192
+
193
+ Soft-remove Android devices from the Client Connector Portal:
194
+
195
+ >>> zcc.devices.remove_devices(os_type="android")
196
+
197
+ Hard-remove devices from the Client Connector Portal by UDID:
198
+
199
+ >>> zcc.devices.remove_devices(force=True, udids=["99999", "88888", "77777"])
200
+
201
+ Hard-remove Android devices for johnno@widgets.co from the Client Connector Portal:
202
+
203
+ >>> zcc.devices.remove_devices(force=True, os_type="android",
204
+ ... user_name="johnno@widgets.co")
205
+
206
+ """
207
+ payload = convert_keys(dict(kwargs))
208
+
209
+ # Simplify the os_type argument, raise an error if the user supplies the wrong one.
210
+ if kwargs.get("os_type"):
211
+ os_type = zcc_param_map["os"].get(payload["osType"], None)
212
+ if os_type:
213
+ payload["osType"] = os_type
214
+ else:
215
+ raise ValueError("Invalid os_type specified. Check the pyZscaler documentation for valid os_type options.")
216
+
217
+ if force:
218
+ return self.rest.post("forceRemoveDevices", json=payload)
219
+ else:
220
+ return self.rest.post("removeDevices", json=payload)
@@ -0,0 +1,66 @@
1
+ from zscaler.zcc.client import ZCCClient
2
+ from zscaler.utils import zcc_param_map
3
+
4
+
5
+ class SecretsAPI:
6
+ def __init__(self, client: ZCCClient):
7
+ self.rest = client
8
+
9
+ def get_otp(self, device_id: str):
10
+ """
11
+ Returns the OTP code for the specified device id.
12
+
13
+ Args:
14
+ device_id (str): The unique id for the enrolled device that the OTP will be obtained for.
15
+
16
+ Returns:
17
+ :obj:`Box`: A dictionary containing the requested OTP code for the specified device id.
18
+
19
+ Examples:
20
+ Obtain the OTP code for a device and print it to console:
21
+
22
+ >>> otp_code = zcc.secrets.get_otp('System-Serial-Number:1234ABCDEF')
23
+ ... print(otp_code.otp)
24
+
25
+ """
26
+
27
+ payload = {"udid": device_id}
28
+
29
+ return self.rest.get("getOtp", params=payload)
30
+
31
+ def get_passwords(self, username: str, os_type: str = "windows"):
32
+ """
33
+ Return passwords for the specified username and device OS type.
34
+
35
+ Args:
36
+ username (str): The username that the device belongs to.
37
+ os_type (str): The OS Type for the device, defaults to `windows`. Valid options are:
38
+
39
+ - ios
40
+ - android
41
+ - windows
42
+ - macos
43
+ - linux
44
+
45
+ Returns:
46
+ :obj:`Box`: Dictionary containing passwords for the specified username's device.
47
+
48
+ Examples:
49
+ Print macOS device passwords for username test@example.com:
50
+
51
+ >>> print(zcc.secrets.get_passwords(username='test@example.com',
52
+ ... os_type='macos'))
53
+
54
+ """
55
+
56
+ # Simplify the os_type argument, raise an error if the user supplies the wrong one.
57
+ os_type = zcc_param_map["os"].get(os_type, None)
58
+ if not os_type:
59
+ raise ValueError("Invalid os_type specified. Check the pyZscaler documentation for valid os_type options.")
60
+
61
+ params = {
62
+ "username": username,
63
+ "osType": os_type,
64
+ }
65
+
66
+ return self.rest.get("getPasswords", params=params)
@@ -67,7 +67,7 @@ class ZCONClientHelper(ZCONClient):
67
67
  """
68
68
 
69
69
  _vendor = "Zscaler"
70
- _product = "Zscaler CON Cloud"
70
+ _product = "Zscaler Cloud and Branch Connector"
71
71
  _build = __version__
72
72
  _env_base = "ZCON"
73
73
  env_cloud = "zscaler"
@@ -110,6 +110,27 @@ class CloudAppControlAPI:
110
110
  """
111
111
  return self.rest.get(f"webApplicationRules/{rule_type}/{rule_id}")
112
112
 
113
+ def get_rule_by_name(self, rule_type: str, rule_name: str) -> Box:
114
+ """
115
+ Retrieves a specific Cloud App Control rule by its name and type.
116
+
117
+ Args:
118
+ rule_type (str): The type of rules to search within (e.g., "WEBMAIL").
119
+ rule_name (str): The name of the rule to retrieve.
120
+
121
+ Returns:
122
+ :obj:`Box`: The Cloud App Control rule if found, otherwise None.
123
+
124
+ Examples:
125
+ >>> rule = zia.cloudappcontrol.get_rule_by_name('WEBMAIL', 'Webmail Rule-1')
126
+ ... print(rule)
127
+ """
128
+ rules = self.list_rules(rule_type)
129
+ for rule in rules:
130
+ if rule.get("name") == rule_name:
131
+ return rule
132
+ return None
133
+
113
134
  def add_rule(self, rule_type: str, name: str, **kwargs) -> Box:
114
135
  """
115
136
  Adds a new cloud app control filter rule.
@@ -474,10 +495,6 @@ class CloudAppControlAPI:
474
495
  # Set payload to value of existing record and convert nested dict keys.
475
496
  payload = convert_keys(self.get_rule(rule_type, rule_id))
476
497
 
477
- # Convert enabled to API format if present in kwargs
478
- if "enabled" in kwargs:
479
- kwargs["state"] = "ENABLED" if kwargs.pop("enabled") else "DISABLED"
480
-
481
498
  # Convert enabled to API format if present in kwargs
482
499
  if "enabled" in kwargs:
483
500
  kwargs["state"] = "ENABLED" if kwargs.pop("enabled") else "DISABLED"
@@ -52,7 +52,7 @@ class DLPAPI:
52
52
  if isinstance(list, Response):
53
53
  return None
54
54
  return list
55
-
55
+
56
56
  def get_dict(self, dict_id: str) -> Box:
57
57
  """
58
58
  Returns the DLP Dictionary that matches the specified DLP Dictionary id.
@@ -398,13 +398,7 @@ class DLPAPI:
398
398
  else:
399
399
  return response
400
400
 
401
- def add_dlp_engine(
402
- self,
403
- name: str,
404
- engine_expression=None,
405
- custom_dlp_engine=None,
406
- **kwargs
407
- ) -> Box:
401
+ def add_dlp_engine(self, name: str, engine_expression=None, custom_dlp_engine=None, **kwargs) -> Box:
408
402
  """
409
403
  Adds a new dlp engine.
410
404
  Args:
@@ -888,7 +882,7 @@ class DLPAPI:
888
882
  if isinstance(response, Response):
889
883
  return None
890
884
  return response
891
-
885
+
892
886
  def list_edm_schema_lite(self, schema_name: str = None, active_only: bool = None, fetch_tokens: bool = None) -> BoxList:
893
887
  """
894
888
  Returns the list of active EDM templates (or EDM schemas) and their criteria (or token details), only.
@@ -896,7 +890,7 @@ class DLPAPI:
896
890
  Args:
897
891
  schema_name (str): The EDM schema name.
898
892
  active_only (bool): If set to true, only active EDM templates (or schemas) are returned in the response.
899
- fetch_tokens (bool): If set to true, the criteria (i.e., token details) for the active templates are returned in the response.
893
+ fetch_tokens (bool): If set to true, the criteria for the active templates are returned in the response.
900
894
 
901
895
  Returns:
902
896
  :obj:`BoxList`: A list containing ZIA DLP Engines.
@@ -911,11 +905,11 @@ class DLPAPI:
911
905
  """
912
906
  params = {}
913
907
  if schema_name is not None:
914
- params['schemaName'] = schema_name
908
+ params["schemaName"] = schema_name
915
909
  if active_only is not None:
916
- params['activeOnly'] = active_only
910
+ params["activeOnly"] = active_only
917
911
  if fetch_tokens is not None:
918
- params['fetchTokens'] = fetch_tokens
912
+ params["fetchTokens"] = fetch_tokens
919
913
 
920
914
  response = self.rest.get("/dlpExactDataMatchSchemas/lite", params=params)
921
915
  if isinstance(response, Response):
@@ -58,7 +58,7 @@ class URLCategoriesAPI:
58
58
  def review_domains_post(self, urls: list) -> BoxList:
59
59
  """
60
60
  For the specified list of URLs, finds matching entries present in existing custom URL categories.
61
- This endpoint allows you to review URLs for potential matches in existing categories to group related entries in a single category.
61
+ Allows the review of URLs for matches in existing categories to group related entries in a single category.
62
62
 
63
63
  Args:
64
64
  urls (list): A list of URLs to perform a category lookup on. Maximum 100 URLs per request.
@@ -104,7 +104,6 @@ class URLCategoriesAPI:
104
104
  else:
105
105
  return self.rest.put("urlCategories/review/domains", json=urls)
106
106
 
107
-
108
107
  def list_categories(self, custom_only: bool = False, only_counts: bool = False) -> BoxList:
109
108
  """
110
109
  Returns information on URL categories.
@@ -173,6 +173,49 @@ class SegmentGroupsAPI:
173
173
  if not isinstance(resp, Response):
174
174
  return self.get_group(group_id)
175
175
 
176
+ # REQUIRES DEPLOYMENT OF ET-76506 IN PRODUCTION BEFORE ENABLING IT.
177
+ def update_group_v2(self, group_id: str, **kwargs) -> Box:
178
+ """
179
+ Updates an existing segment group using v2 endpoint.
180
+
181
+ Args:
182
+ group_id (str): The unique identifier for the segment group to be updated.
183
+ **kwargs: Optional keyword args.
184
+
185
+ Keyword Args:
186
+ name (str): The name of the new segment group.
187
+ enabled (bool): Enable the segment group.
188
+ application_ids (:obj:`list` of :obj:`dict`): Unique application IDs to associate with the segment group.
189
+ config_space (str): The config space for the segment group. Can either be DEFAULT or SIEM.
190
+ description (str): A description for the segment group.
191
+ policy_migrated (bool):
192
+ microtenant_id (str): The microtenant ID to be used for this request.
193
+
194
+ Returns:
195
+ :obj:`Box`: The resource record for the updated segment group.
196
+
197
+ Examples:
198
+ Updating the name of a segment group:
199
+
200
+ >>> zpa.segment_groups.update_group_v2('99999',
201
+ ... name='updated_name')
202
+
203
+ """
204
+ payload = {snake_to_camel(k): v for k, v in self.get_group(group_id).items()}
205
+
206
+ if kwargs.get("application_ids"):
207
+ payload["applications"] = [{"id": app_id} for app_id in kwargs.pop("application_ids")]
208
+
209
+ for key, value in kwargs.items():
210
+ payload[snake_to_camel(key)] = value
211
+
212
+ microtenant_id = kwargs.pop("microtenant_id", None)
213
+ params = {"microtenantId": microtenant_id} if microtenant_id else {}
214
+
215
+ resp = self.rest.put(f"segmentGroup/{group_id}", json=payload, params=params, api_version="v2").status_code
216
+ if not isinstance(resp, Response):
217
+ return self.get_group(group_id)
218
+
176
219
  def delete_group(self, group_id: str, **kwargs) -> int:
177
220
  """
178
221
  Deletes the specified segment group.