zscaler-sdk-python 0.9.0__tar.gz → 0.9.2__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.9.0 → zscaler_sdk_python-0.9.2}/PKG-INFO +4 -4
  2. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/pyproject.toml +5 -5
  3. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/__init__.py +2 -1
  4. zscaler_sdk_python-0.9.2/zscaler/zcc/__init__.py +265 -0
  5. zscaler_sdk_python-0.9.2/zscaler/zcc/client.py +65 -0
  6. zscaler_sdk_python-0.9.2/zscaler/zcc/devices.py +220 -0
  7. zscaler_sdk_python-0.9.2/zscaler/zcc/secrets.py +66 -0
  8. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zcon/__init__.py +1 -1
  9. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/dlp.py +4 -30
  10. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/LICENSE.md +0 -0
  11. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/README.md +0 -0
  12. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/cache/__init__.py +0 -0
  13. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/cache/cache.py +0 -0
  14. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/cache/no_op_cache.py +0 -0
  15. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/cache/zscaler_cache.py +0 -0
  16. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/constants.py +0 -0
  17. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/errors/__init__.py +0 -0
  18. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/errors/error.py +0 -0
  19. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/errors/http_error.py +0 -0
  20. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/errors/zscaler_api_error.py +0 -0
  21. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/exceptions/__init__.py +0 -0
  22. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/exceptions/exceptions.py +0 -0
  23. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/logger.py +0 -0
  24. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/ratelimiter/__init__.py +0 -0
  25. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/ratelimiter/ratelimiter.py +0 -0
  26. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/user_agent.py +0 -0
  27. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/utils.py +0 -0
  28. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zcon/activation.py +0 -0
  29. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zcon/admin_and_role_management.py +0 -0
  30. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zcon/client.py +0 -0
  31. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zcon/ecgroups.py +0 -0
  32. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zcon/locations.py +0 -0
  33. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zcon/provisioning.py +0 -0
  34. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zdx/__init__.py +0 -0
  35. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zdx/admin.py +0 -0
  36. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zdx/alerts.py +0 -0
  37. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zdx/apps.py +0 -0
  38. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zdx/devices.py +0 -0
  39. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zdx/filters.py +0 -0
  40. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zdx/inventory.py +0 -0
  41. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zdx/troubleshooting.py +0 -0
  42. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zdx/users.py +0 -0
  43. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zdx/zdx_client.py +0 -0
  44. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/__init__.py +0 -0
  45. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/activate.py +0 -0
  46. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/admin_and_role_management.py +0 -0
  47. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/apptotal.py +0 -0
  48. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/audit_logs.py +0 -0
  49. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/authentication_settings.py +0 -0
  50. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/client.py +0 -0
  51. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/cloud_apps.py +0 -0
  52. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/cloudappcontrol.py +0 -0
  53. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/device_management.py +0 -0
  54. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/errors.py +0 -0
  55. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/firewall.py +0 -0
  56. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/forwarding_control.py +0 -0
  57. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/isolation_profile.py +0 -0
  58. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/labels.py +0 -0
  59. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/locations.py +0 -0
  60. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/sandbox.py +0 -0
  61. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/security.py +0 -0
  62. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/ssl_inspection.py +0 -0
  63. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/traffic.py +0 -0
  64. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/url_categories.py +0 -0
  65. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/url_filtering.py +0 -0
  66. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/users.py +0 -0
  67. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/web_dlp.py +0 -0
  68. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/workload_groups.py +0 -0
  69. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zia/zpa_gateway.py +0 -0
  70. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/README.md +0 -0
  71. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/__init__.py +0 -0
  72. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/app_segments.py +0 -0
  73. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/app_segments_inspection.py +0 -0
  74. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/app_segments_pra.py +0 -0
  75. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/authdomains.py +0 -0
  76. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/certificates.py +0 -0
  77. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/client.py +0 -0
  78. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/cloud_connector_groups.py +0 -0
  79. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/connectors.py +0 -0
  80. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/emergency_access.py +0 -0
  81. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/errors.py +0 -0
  82. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/idp.py +0 -0
  83. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/inspection.py +0 -0
  84. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/isolation.py +0 -0
  85. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/lss.py +0 -0
  86. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/machine_groups.py +0 -0
  87. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/microtenants.py +0 -0
  88. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/policies.py +0 -0
  89. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/posture_profiles.py +0 -0
  90. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/privileged_remote_access.py +0 -0
  91. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/provisioning.py +0 -0
  92. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/saml_attributes.py +0 -0
  93. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/scim_attributes.py +0 -0
  94. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/scim_groups.py +0 -0
  95. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/segment_groups.py +0 -0
  96. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/server_groups.py +0 -0
  97. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/servers.py +0 -0
  98. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/zscaler/zpa/service_edges.py +0 -0
  99. {zscaler_sdk_python-0.9.0 → zscaler_sdk_python-0.9.2}/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.9.0
3
+ Version: 0.9.2
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
@@ -22,7 +22,7 @@ Classifier: Topic :: Security
22
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Provides-Extra: dev
24
24
  Requires-Dist: aenum ; extra == "dev"
25
- Requires-Dist: aiohttp (>=3.10.5)
25
+ Requires-Dist: aiohttp (>=3.10.2)
26
26
  Requires-Dist: arrow
27
27
  Requires-Dist: black (>=24.3.0) ; extra == "dev"
28
28
  Requires-Dist: certifi (>=2024.2.2)
@@ -30,13 +30,13 @@ Requires-Dist: charset-normalizer
30
30
  Requires-Dist: cryptography (>=43.0.0)
31
31
  Requires-Dist: flake8
32
32
  Requires-Dist: flatdict
33
- Requires-Dist: idna (>=3.7,<4.0)
33
+ 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
37
  Requires-Dist: python-box (>=7.2.0)
38
38
  Requires-Dist: python-dateutil
39
- Requires-Dist: pytz
39
+ Requires-Dist: pytz (>=2024.2)
40
40
  Requires-Dist: pyyaml
41
41
  Requires-Dist: requests (>=2.32.3)
42
42
  Requires-Dist: responses (>=0.25.3)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "zscaler-sdk-python"
3
- version = "0.9.0"
3
+ version = "0.9.2"
4
4
  description = "Official Python SDK for the Zscaler Products (Beta)"
5
5
  authors = ["Zscaler, Inc. <devrel@zscaler.com>"]
6
6
  license = "MIT"
@@ -32,7 +32,7 @@ python = ">=3.9,<4.0"
32
32
  arrow = "*"
33
33
  certifi = ">=2024.2.2"
34
34
  charset-normalizer = "*"
35
- idna = "^3.7"
35
+ idna = "*"
36
36
  python-box = ">=7.2.0"
37
37
  python-dateutil = "*"
38
38
  requests = ">=2.32.3"
@@ -47,17 +47,17 @@ pycryptodomex = "*"
47
47
  aenum = "*"
48
48
  pydash = ">=8.0.3"
49
49
  flake8 = "*"
50
- pytz = "*"
50
+ pytz = ">=2024.2"
51
51
  black = ">=24.3.0"
52
52
  # cryptography = ">=3.4,<43.0"
53
53
  cryptography = ">=43.0.0"
54
54
  okta = ">=2.9.7"
55
- aiohttp = ">=3.10.5"
55
+ aiohttp = ">=3.10.2"
56
56
 
57
57
  [tool.poetry.dev-dependencies]
58
58
  black = ">=24.3.0"
59
59
  pytest = ">=8.3.1"
60
- pytest-asyncio = ">=0.23.8"
60
+ pytest-asyncio = "^0.23.8"
61
61
  pytest-mock = "*"
62
62
  pytest-recording = "^0.13.2"
63
63
  pytest-cov = "*"
@@ -29,9 +29,10 @@ __license__ = "MIT"
29
29
  __contributors__ = [
30
30
  "William Guilherme",
31
31
  ]
32
- __version__ = "0.9.0"
32
+ __version__ = "0.9.2"
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
@@ -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"
@@ -398,38 +398,9 @@ class DLPAPI:
398
398
  else:
399
399
  return response
400
400
 
401
- def add_dlp_engine(self, name: str, engine_expression=None, custom_dlp_engine=None, **kwargs) -> Box:
401
+ def add_dlp_engine(self, name: str, engine_expression=None, custom_dlp_engine=None, description=None, **kwargs) -> Box:
402
402
  """
403
403
  Adds a new dlp engine.
404
- Args:
405
- name (str): The order of the rule, defaults to adding rule to bottom of list.
406
- **kwargs: Optional keyword args.
407
-
408
- Keyword Args:
409
- description (str): The admin rank of the rule.
410
- engine_expression (str, optional): The logical expression defining a DLP engine by
411
- combining DLP dictionaries using logical operators: All (AND), Any (OR), Exclude (NOT),
412
- and Sum (total number of content matches).
413
- custom_dlp_engine (bool, optional): If true, indicates a custom DLP engine.
414
- description (str, optional): The DLP engine description.
415
-
416
- Returns:
417
- :obj:`Box`: The updated dlp engine resource record.
418
-
419
- Examples:
420
- Update the dlp engine:
421
-
422
- >>> zia.dlp.add_dlp_engine(name='new_dlp_engine',
423
- ... description='TT#1965432122',
424
- ... engine_expression="((D63.S > 1))",
425
- ... custom_dlp_engine=False)
426
-
427
- Update a rule to enable custom dlp engine:
428
-
429
- >>> zia.dlp.add_dlp_engine(name='new_dlp_engine',
430
- ... custom_dlp_engine=True,
431
- ... engine_expression="((D63.S > 1))",
432
- ... description="TT#1965232866")
433
404
  """
434
405
  payload = {
435
406
  "name": name,
@@ -441,6 +412,9 @@ class DLPAPI:
441
412
  if custom_dlp_engine is not None:
442
413
  payload["customDlpEngine"] = custom_dlp_engine
443
414
 
415
+ if description is not None:
416
+ payload["description"] = description
417
+
444
418
  # Convert the payload keys to camelCase
445
419
  camel_payload = {snake_to_camel(key): value for key, value in payload.items()}
446
420