zscaler-sdk-python 1.0.0__py2.py3-none-any.whl
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.
- zscaler/__init__.py +34 -0
- zscaler/cache/__init__.py +0 -0
- zscaler/cache/cache.py +105 -0
- zscaler/cache/no_op_cache.py +68 -0
- zscaler/cache/zscaler_cache.py +161 -0
- zscaler/constants.py +26 -0
- zscaler/errors/__init__.py +0 -0
- zscaler/errors/error.py +10 -0
- zscaler/errors/http_error.py +20 -0
- zscaler/errors/zscaler_api_error.py +24 -0
- zscaler/exceptions/__init__.py +1 -0
- zscaler/exceptions/exceptions.py +101 -0
- zscaler/logger.py +57 -0
- zscaler/ratelimiter/__init__.py +0 -0
- zscaler/ratelimiter/ratelimiter.py +39 -0
- zscaler/user_agent.py +23 -0
- zscaler/utils.py +577 -0
- zscaler/zia/__init__.py +657 -0
- zscaler/zia/activate.py +52 -0
- zscaler/zia/admin_and_role_management.py +344 -0
- zscaler/zia/apptotal.py +71 -0
- zscaler/zia/audit_logs.py +95 -0
- zscaler/zia/authentication_settings.py +98 -0
- zscaler/zia/client.py +88 -0
- zscaler/zia/cloud_apps.py +406 -0
- zscaler/zia/device_management.py +90 -0
- zscaler/zia/dlp.py +784 -0
- zscaler/zia/errors.py +37 -0
- zscaler/zia/firewall.py +1104 -0
- zscaler/zia/forwarding_control.py +271 -0
- zscaler/zia/isolation_profile.py +83 -0
- zscaler/zia/labels.py +180 -0
- zscaler/zia/locations.py +661 -0
- zscaler/zia/sandbox.py +180 -0
- zscaler/zia/security.py +236 -0
- zscaler/zia/ssl_inspection.py +175 -0
- zscaler/zia/traffic.py +853 -0
- zscaler/zia/url_categories.py +442 -0
- zscaler/zia/url_filtering.py +310 -0
- zscaler/zia/users.py +386 -0
- zscaler/zia/web_dlp.py +295 -0
- zscaler/zia/workload_groups.py +58 -0
- zscaler/zia/zpa_gateway.py +187 -0
- zscaler/zpa/__init__.py +683 -0
- zscaler/zpa/app_segments.py +331 -0
- zscaler/zpa/app_segments_inspection.py +311 -0
- zscaler/zpa/app_segments_pra.py +310 -0
- zscaler/zpa/certificates.py +234 -0
- zscaler/zpa/client.py +113 -0
- zscaler/zpa/cloud_connector_groups.py +75 -0
- zscaler/zpa/connectors.py +518 -0
- zscaler/zpa/emergency_access.py +178 -0
- zscaler/zpa/errors.py +37 -0
- zscaler/zpa/idp.py +83 -0
- zscaler/zpa/inspection.py +1012 -0
- zscaler/zpa/isolation_profile.py +85 -0
- zscaler/zpa/lss.py +568 -0
- zscaler/zpa/machine_groups.py +79 -0
- zscaler/zpa/policies.py +848 -0
- zscaler/zpa/posture_profiles.py +122 -0
- zscaler/zpa/privileged_remote_access.py +862 -0
- zscaler/zpa/provisioning.py +271 -0
- zscaler/zpa/saml_attributes.py +100 -0
- zscaler/zpa/scim_attributes.py +117 -0
- zscaler/zpa/scim_groups.py +146 -0
- zscaler/zpa/segment_groups.py +191 -0
- zscaler/zpa/server_groups.py +217 -0
- zscaler/zpa/servers.py +202 -0
- zscaler/zpa/service_edges.py +404 -0
- zscaler/zpa/trusted_networks.py +127 -0
- zscaler_sdk_python-1.0.0.dist-info/LICENSE.md +21 -0
- zscaler_sdk_python-1.0.0.dist-info/METADATA +59 -0
- zscaler_sdk_python-1.0.0.dist-info/RECORD +75 -0
- zscaler_sdk_python-1.0.0.dist-info/WHEEL +6 -0
- zscaler_sdk_python-1.0.0.dist-info/top_level.txt +1 -0
zscaler/utils.py
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
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
|
+
import argparse
|
|
18
|
+
import base64
|
|
19
|
+
import datetime
|
|
20
|
+
import json
|
|
21
|
+
import json as jsonp
|
|
22
|
+
import logging
|
|
23
|
+
import random
|
|
24
|
+
import re
|
|
25
|
+
import time
|
|
26
|
+
from typing import Dict, Optional
|
|
27
|
+
from urllib.parse import urlencode
|
|
28
|
+
|
|
29
|
+
import pytz
|
|
30
|
+
from box import Box, BoxList
|
|
31
|
+
from dateutil import parser
|
|
32
|
+
from requests import Response
|
|
33
|
+
from restfly import APIIterator
|
|
34
|
+
|
|
35
|
+
from zscaler.constants import RETRYABLE_STATUS_CODES
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger("zscaler-sdk-python")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Recursive function to convert all keys and nested keys from camel case
|
|
41
|
+
# to snake case.
|
|
42
|
+
def convert_keys_to_snake(data):
|
|
43
|
+
if isinstance(data, (list, BoxList)):
|
|
44
|
+
return [convert_keys_to_snake(inner_dict) for inner_dict in data]
|
|
45
|
+
elif isinstance(data, (dict, Box)):
|
|
46
|
+
new_dict = {}
|
|
47
|
+
for k in data.keys():
|
|
48
|
+
v = data[k]
|
|
49
|
+
new_key = camel_to_snake(k)
|
|
50
|
+
new_dict[new_key] = (
|
|
51
|
+
convert_keys_to_snake(v) if isinstance(v, (dict, list)) else v
|
|
52
|
+
)
|
|
53
|
+
return new_dict
|
|
54
|
+
else:
|
|
55
|
+
return data
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def camel_to_snake(name: str):
|
|
59
|
+
"""Converts Python camelCase to Zscaler's lower snake_case."""
|
|
60
|
+
# Edge-cases where camelCase is breaking
|
|
61
|
+
edge_cases = {
|
|
62
|
+
"routableIP": "routable_ip",
|
|
63
|
+
"isNameL10nTag": "is_name_l10n_tag",
|
|
64
|
+
"nameL10nTag": "name_l10n_tag",
|
|
65
|
+
"surrogateIP": "surrogate_ip",
|
|
66
|
+
"surrogateIPEnforcedForKnownBrowsers": "surrogate_ip_enforced_for_known_browsers",
|
|
67
|
+
"startIPAddress": "start_ip_address",
|
|
68
|
+
"endIPAddress": "end_ip_address",
|
|
69
|
+
"isIncompleteDRConfig": "is_incomplete_dr_config",
|
|
70
|
+
}
|
|
71
|
+
return edge_cases.get(name, re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower())
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def snake_to_camel(name: str):
|
|
75
|
+
"""Converts Python Snake Case to Zscaler's lower camelCase."""
|
|
76
|
+
if "_" not in name:
|
|
77
|
+
return name
|
|
78
|
+
# Edge-cases where camelCase is breaking
|
|
79
|
+
edge_cases = {
|
|
80
|
+
"routable_ip": "routableIP",
|
|
81
|
+
"is_name_l10n_tag": "isNameL10nTag",
|
|
82
|
+
"name_l10n_tag": "nameL10nTag",
|
|
83
|
+
"surrogate_ip": "surrogateIP",
|
|
84
|
+
"surrogate_ip_enforced_for_known_browsers": "surrogateIPEnforcedForKnownBrowsers",
|
|
85
|
+
"is_incomplete_dr_config": "isIncompleteDRConfig",
|
|
86
|
+
"email_ids": "emailIds",
|
|
87
|
+
"page_size": "pageSize",
|
|
88
|
+
}
|
|
89
|
+
return edge_cases.get(name, name[0].lower() + name.title()[1:].replace("_", ""))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def recursive_snake_to_camel(data):
|
|
93
|
+
"""Recursively convert dictionary keys from snake_case to camelCase."""
|
|
94
|
+
if isinstance(data, dict):
|
|
95
|
+
return {
|
|
96
|
+
snake_to_camel(key): recursive_snake_to_camel(value)
|
|
97
|
+
for key, value in data.items()
|
|
98
|
+
}
|
|
99
|
+
elif isinstance(data, list):
|
|
100
|
+
return [recursive_snake_to_camel(item) for item in data]
|
|
101
|
+
else:
|
|
102
|
+
return data
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def chunker(lst, n):
|
|
106
|
+
"""Yield successive n-sized chunks from lst."""
|
|
107
|
+
for i in range(0, len(lst), n):
|
|
108
|
+
yield lst[i : i + n]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Recursive function to convert all keys and nested keys from snake case
|
|
112
|
+
# to camel case.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def convert_keys(data):
|
|
116
|
+
if isinstance(data, (list, BoxList)):
|
|
117
|
+
return [convert_keys(inner_dict) for inner_dict in data]
|
|
118
|
+
elif isinstance(data, (dict, Box)):
|
|
119
|
+
new_dict = {}
|
|
120
|
+
for k in data.keys():
|
|
121
|
+
v = data[k]
|
|
122
|
+
new_key = snake_to_camel(k)
|
|
123
|
+
new_dict[new_key] = convert_keys(v) if isinstance(v, (dict, list)) else v
|
|
124
|
+
return new_dict
|
|
125
|
+
else:
|
|
126
|
+
return data
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def keys_exists(element: dict, *keys):
|
|
130
|
+
"""
|
|
131
|
+
Check if *keys (nested) exists in `element` (dict).
|
|
132
|
+
"""
|
|
133
|
+
if not isinstance(element, dict):
|
|
134
|
+
raise AttributeError("keys_exists() expects dict as first argument.")
|
|
135
|
+
if not keys:
|
|
136
|
+
raise AttributeError("keys_exists() expects at least two arguments, one given.")
|
|
137
|
+
|
|
138
|
+
_element = element
|
|
139
|
+
for key in keys:
|
|
140
|
+
try:
|
|
141
|
+
_element = _element[key]
|
|
142
|
+
except KeyError:
|
|
143
|
+
return False
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Takes a tuple if id_groups, kwargs and the payload dict; reformat for API call
|
|
148
|
+
def add_id_groups(id_groups: list, kwargs: dict, payload: dict):
|
|
149
|
+
for entry in id_groups:
|
|
150
|
+
if kwargs.get(entry[0]):
|
|
151
|
+
payload[entry[1]] = [{"id": param_id} for param_id in kwargs.pop(entry[0])]
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def transform_common_id_fields(id_groups: list, kwargs: dict, payload: dict):
|
|
156
|
+
for entry in id_groups:
|
|
157
|
+
if kwargs.get(entry[0]):
|
|
158
|
+
# Ensure each ID is treated as an integer before adding it to the payload
|
|
159
|
+
payload[entry[1]] = [
|
|
160
|
+
{"id": int(param_id)} for param_id in kwargs.pop(entry[0])
|
|
161
|
+
]
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def transform_clientless_apps(clientless_app_ids):
|
|
166
|
+
transformed_apps = []
|
|
167
|
+
for app in clientless_app_ids:
|
|
168
|
+
# Transform each attribute in app as needed by your API
|
|
169
|
+
transformed_apps.append(
|
|
170
|
+
{
|
|
171
|
+
"name": app["name"],
|
|
172
|
+
"applicationProtocol": app["application_protocol"],
|
|
173
|
+
"applicationPort": app["application_port"],
|
|
174
|
+
"certificateId": app["certificate_id"],
|
|
175
|
+
"trustUntrustedCert": app["trust_untrusted_cert"],
|
|
176
|
+
"enabled": app["enabled"],
|
|
177
|
+
"domain": app["domain"],
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
return transformed_apps
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def format_clientless_apps(clientless_apps):
|
|
184
|
+
# Implement this function to format clientless_apps as needed for the update request
|
|
185
|
+
# This is just a placeholder example
|
|
186
|
+
formatted_apps = []
|
|
187
|
+
for app in clientless_apps:
|
|
188
|
+
formatted_app = {
|
|
189
|
+
"id": app["id"], # use the correct key
|
|
190
|
+
# Add other necessary attributes and format them as needed
|
|
191
|
+
}
|
|
192
|
+
formatted_apps.append(formatted_app)
|
|
193
|
+
return formatted_apps
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def obfuscate_api_key(seed: list):
|
|
197
|
+
now = int(time.time() * 1000)
|
|
198
|
+
n = str(now)[-6:]
|
|
199
|
+
r = str(int(n) >> 1).zfill(6)
|
|
200
|
+
key = "".join(seed[int(str(n)[i])] for i in range(len(str(n))))
|
|
201
|
+
for j in range(len(r)):
|
|
202
|
+
key += seed[int(r[j]) + 2]
|
|
203
|
+
|
|
204
|
+
return {"timestamp": now, "key": key}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def format_json_response(
|
|
208
|
+
response: Response,
|
|
209
|
+
box_attrs: Optional[Dict] = None,
|
|
210
|
+
conv_json: bool = True,
|
|
211
|
+
conv_box: bool = True,
|
|
212
|
+
):
|
|
213
|
+
"""
|
|
214
|
+
A simple utility to handle formatting the response object into either a
|
|
215
|
+
Box object or a Python native object from the JSON response. The function
|
|
216
|
+
will prefer box over python native if both flags are set to true. If none
|
|
217
|
+
of the flags are true, or if the content-type header reports as something
|
|
218
|
+
other than "applicagion/json", then the response object is instead
|
|
219
|
+
returned.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
response:
|
|
223
|
+
The response object that will be checked against.
|
|
224
|
+
box_attrs:
|
|
225
|
+
The optional box attributed to pass as part of instantiation.
|
|
226
|
+
conv_json:
|
|
227
|
+
A flag handling if we should run the JSON conversion to python
|
|
228
|
+
native datatypes.
|
|
229
|
+
conv_box:
|
|
230
|
+
A flaghandling if we should convert the data to a Box object.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
box.Box:
|
|
234
|
+
If the conv_box flag is True, and the response is a single object,
|
|
235
|
+
then the response is a Box obj.
|
|
236
|
+
box.BoxList:
|
|
237
|
+
If the conv_box flag is True, and the response is a list of
|
|
238
|
+
objects, then the response is a BoxList obj.
|
|
239
|
+
dict:
|
|
240
|
+
If the conv_json flag is True and the conv_box is False, and the
|
|
241
|
+
response is a single object, then the response is a dict obj.
|
|
242
|
+
list:
|
|
243
|
+
If the conv_json flag is True and conv_box is False, and the
|
|
244
|
+
response is a list of objects, then the response is a list obj.
|
|
245
|
+
requests.Response:
|
|
246
|
+
If neither flag is True, or if the response isn't JSON data, then
|
|
247
|
+
a response object is returned (pass-through).
|
|
248
|
+
"""
|
|
249
|
+
if response.status_code > 299:
|
|
250
|
+
return response
|
|
251
|
+
content_type = response.headers.get("content-type", "application/json")
|
|
252
|
+
if (
|
|
253
|
+
(conv_json or conv_box)
|
|
254
|
+
and "application/json" in content_type.lower()
|
|
255
|
+
and len(response.text) > 0
|
|
256
|
+
): # noqa: E124
|
|
257
|
+
if conv_box:
|
|
258
|
+
data = convert_keys_to_snake(response.json())
|
|
259
|
+
if isinstance(data, list):
|
|
260
|
+
return BoxList(data, **box_attrs)
|
|
261
|
+
elif isinstance(data, dict):
|
|
262
|
+
return Box(data, **box_attrs)
|
|
263
|
+
elif conv_json:
|
|
264
|
+
return convert_keys_to_snake(response.json())
|
|
265
|
+
return response
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def pick_version_profile(kwargs: list, payload: list):
|
|
269
|
+
# Used in ZPA endpoints.
|
|
270
|
+
# This function is used to convert the name of the version profile to
|
|
271
|
+
# the version profile id. This means our users don't need to look up the
|
|
272
|
+
# version profile id mapping themselves.
|
|
273
|
+
|
|
274
|
+
version_profile = kwargs.pop("version_profile", None)
|
|
275
|
+
if version_profile:
|
|
276
|
+
payload["overrideVersionProfile"] = True
|
|
277
|
+
if version_profile == "default":
|
|
278
|
+
payload["versionProfileId"] = 0
|
|
279
|
+
elif version_profile == "previous_default":
|
|
280
|
+
payload["versionProfileId"] = 1
|
|
281
|
+
elif version_profile == "new_release":
|
|
282
|
+
payload["versionProfileId"] = 2
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class Iterator(APIIterator):
|
|
286
|
+
"""Iterator class."""
|
|
287
|
+
|
|
288
|
+
page_size = 100
|
|
289
|
+
|
|
290
|
+
def __init__(self, api, path: str = "", **kw):
|
|
291
|
+
"""Initialize Iterator class."""
|
|
292
|
+
super().__init__(api, **kw)
|
|
293
|
+
|
|
294
|
+
self.path = path
|
|
295
|
+
self.max_items = kw.pop("max_items", 0)
|
|
296
|
+
self.max_pages = kw.pop("max_pages", 0)
|
|
297
|
+
self.payload = {}
|
|
298
|
+
if kw:
|
|
299
|
+
self.payload = {snake_to_camel(key): value for key, value in kw.items()}
|
|
300
|
+
|
|
301
|
+
def _get_page(self) -> None:
|
|
302
|
+
"""Iterator function to get the page."""
|
|
303
|
+
resp = self._api.get(
|
|
304
|
+
self.path,
|
|
305
|
+
params={**self.payload, "page": self.num_pages + 1},
|
|
306
|
+
)
|
|
307
|
+
try:
|
|
308
|
+
# If we are using ZPA then the API will return records under the
|
|
309
|
+
# 'list' key.
|
|
310
|
+
self.page = resp.get("list") or []
|
|
311
|
+
except AttributeError:
|
|
312
|
+
# If the list key doesn't exist then we're likely using ZIA so just
|
|
313
|
+
# return the full response.
|
|
314
|
+
self.page = resp
|
|
315
|
+
finally:
|
|
316
|
+
# If we use the default retry-after logic in Restfly then we are
|
|
317
|
+
# going to keep seeing 429 messages in stdout. ZIA and ZPA have a
|
|
318
|
+
# standard 1 sec rate limit on the API endpoints with pagination so
|
|
319
|
+
# we are going to include it here.
|
|
320
|
+
time.sleep(1)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def remove_cloud_suffix(str_name: str) -> str:
|
|
324
|
+
"""
|
|
325
|
+
Removes appended cloud name (e.g. "(zscalerthree.net)") from the string.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
str_name (str): The string from which to remove the cloud name.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
str: The string without the cloud name.
|
|
332
|
+
"""
|
|
333
|
+
reg = re.compile(r"(.*)\s+\([a-zA-Z0-9\-_\.]*\)\s*$")
|
|
334
|
+
res = reg.sub(r"\1", str_name)
|
|
335
|
+
return res.strip()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def should_retry(status_code):
|
|
339
|
+
"""Determine if a given status code should be retried."""
|
|
340
|
+
return status_code in RETRYABLE_STATUS_CODES
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def retry_with_backoff(method_type="GET", retries=5, backoff_in_seconds=0.5):
|
|
344
|
+
"""
|
|
345
|
+
Decorator to retry a function in case of an unsuccessful response.
|
|
346
|
+
|
|
347
|
+
Parameters:
|
|
348
|
+
- method_type (str): The HTTP method. Defaults to "GET".
|
|
349
|
+
- retries (int): Number of retries before giving up. Defaults to 5.
|
|
350
|
+
- backoff_in_seconds (float): Initial wait time (in seconds) before retry. Defaults to 0.5.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
- function: Decorated function with retry and backoff logic.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
if method_type != "GET":
|
|
357
|
+
retries = min(retries, 3) # more conservative retry count for non-GET
|
|
358
|
+
|
|
359
|
+
def decorator(f):
|
|
360
|
+
def wrapper(*args, **kwargs):
|
|
361
|
+
x = 0
|
|
362
|
+
while True:
|
|
363
|
+
resp = f(*args, **kwargs)
|
|
364
|
+
|
|
365
|
+
# Check if it's a successful status code, 400, or if it shouldn't be retried
|
|
366
|
+
if (
|
|
367
|
+
299 >= resp.status_code >= 200
|
|
368
|
+
or resp.status_code == 400
|
|
369
|
+
or not should_retry(resp.status_code)
|
|
370
|
+
):
|
|
371
|
+
return resp
|
|
372
|
+
|
|
373
|
+
if x == retries:
|
|
374
|
+
try:
|
|
375
|
+
error_msg = resp.json()
|
|
376
|
+
except Exception as e:
|
|
377
|
+
error_msg = str(e)
|
|
378
|
+
raise Exception(f"Reached max retries. Response: {error_msg}")
|
|
379
|
+
else:
|
|
380
|
+
sleep = backoff_in_seconds * 2**x + random.uniform(0, 1)
|
|
381
|
+
logger.info(
|
|
382
|
+
"Args: %s, retrying after %d seconds...", str(args), sleep
|
|
383
|
+
)
|
|
384
|
+
time.sleep(sleep)
|
|
385
|
+
x += 1
|
|
386
|
+
|
|
387
|
+
return wrapper
|
|
388
|
+
|
|
389
|
+
return decorator
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def is_token_expired(token_string):
|
|
393
|
+
# If token string is None or empty, consider it expired
|
|
394
|
+
if not token_string:
|
|
395
|
+
logger.warning("Token string is None or empty. Requesting a new token.")
|
|
396
|
+
return True
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
# Split the token into its parts
|
|
400
|
+
parts = token_string.split(".")
|
|
401
|
+
if len(parts) != 3:
|
|
402
|
+
return True
|
|
403
|
+
|
|
404
|
+
# Decode the payload
|
|
405
|
+
payload_bytes = base64.urlsafe_b64decode(
|
|
406
|
+
parts[1] + "=="
|
|
407
|
+
) # Padding might be needed
|
|
408
|
+
payload = jsonp.loads(payload_bytes)
|
|
409
|
+
|
|
410
|
+
# Check expiration time
|
|
411
|
+
if "exp" in payload:
|
|
412
|
+
# Deduct 10 seconds to account for any possible latency or clock skew
|
|
413
|
+
expiration_time = payload["exp"] - 10
|
|
414
|
+
if time.time() > expiration_time:
|
|
415
|
+
return True
|
|
416
|
+
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
except Exception as e:
|
|
420
|
+
logger.error(f"Error checking token expiration: {str(e)}")
|
|
421
|
+
return True
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def str2bool(v):
|
|
425
|
+
if isinstance(v, bool):
|
|
426
|
+
return v
|
|
427
|
+
if v.lower() in ("yes", "true", "t", "y", "1"):
|
|
428
|
+
return True
|
|
429
|
+
elif v.lower() in ("no", "false", "f", "n", "0"):
|
|
430
|
+
return False
|
|
431
|
+
else:
|
|
432
|
+
raise argparse.ArgumentTypeError("Boolean value expected.")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def is_valid_ssh_key(private_key: str) -> bool:
|
|
436
|
+
"""
|
|
437
|
+
Validate SSH private key format.
|
|
438
|
+
"""
|
|
439
|
+
# Basic pattern matching to check for RSA/ECDSA (OpenSSH/PEM) key headers
|
|
440
|
+
ssh_key_patterns = [
|
|
441
|
+
r"-----BEGIN OPENSSH PRIVATE KEY-----",
|
|
442
|
+
r"-----BEGIN RSA PRIVATE KEY-----",
|
|
443
|
+
r"-----BEGIN EC PRIVATE KEY-----",
|
|
444
|
+
]
|
|
445
|
+
return any(re.search(pattern, private_key) for pattern in ssh_key_patterns)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def validate_and_convert_times(start_time_str, end_time_str, time_zone_str):
|
|
449
|
+
"""
|
|
450
|
+
Validates and converts provided time strings to epoch.
|
|
451
|
+
Validates the time zone against IANA Time Zone database.
|
|
452
|
+
Ensures start time is not more than 1 hour in the past and within 1 year range of end time.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
start_time_str (str): Start time in RFC1123Z or RFC1123 format.
|
|
456
|
+
end_time_str (str): End time in RFC1123Z or RFC1123 format.
|
|
457
|
+
time_zone_str (str): IANA Time Zone database string.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
tuple: Converted start and end times in epoch format.
|
|
461
|
+
|
|
462
|
+
Raises:
|
|
463
|
+
ValueError: If any validation fails.
|
|
464
|
+
"""
|
|
465
|
+
# Validate time zone
|
|
466
|
+
if time_zone_str not in pytz.all_timezones:
|
|
467
|
+
raise ValueError(f"Invalid time zone: {time_zone_str}")
|
|
468
|
+
|
|
469
|
+
# Convert times
|
|
470
|
+
try:
|
|
471
|
+
start_time = parser.parse(start_time_str)
|
|
472
|
+
end_time = parser.parse(end_time_str)
|
|
473
|
+
except ValueError as e:
|
|
474
|
+
raise ValueError(f"Time parsing error: {e}")
|
|
475
|
+
|
|
476
|
+
# Handle timezone conversion
|
|
477
|
+
tz = pytz.timezone(time_zone_str)
|
|
478
|
+
if start_time.tzinfo is not None:
|
|
479
|
+
start_time = start_time.astimezone(tz)
|
|
480
|
+
else:
|
|
481
|
+
start_time = tz.localize(start_time)
|
|
482
|
+
|
|
483
|
+
if end_time.tzinfo is not None:
|
|
484
|
+
end_time = end_time.astimezone(tz)
|
|
485
|
+
else:
|
|
486
|
+
end_time = tz.localize(end_time)
|
|
487
|
+
|
|
488
|
+
# Ensure start time is not more than 1 hour in the past
|
|
489
|
+
now_in_tz = datetime.datetime.now(tz)
|
|
490
|
+
if start_time < (now_in_tz - datetime.timedelta(hours=1)):
|
|
491
|
+
raise ValueError("Start time cannot be more than 1 hour in the past.")
|
|
492
|
+
|
|
493
|
+
# Ensure start time is within a one year range of end time
|
|
494
|
+
if end_time > (start_time + datetime.timedelta(days=365)):
|
|
495
|
+
raise ValueError("Start time and end time range cannot exceed 1 year.")
|
|
496
|
+
|
|
497
|
+
# Convert to epoch
|
|
498
|
+
start_epoch = int(start_time.timestamp())
|
|
499
|
+
end_epoch = int(end_time.timestamp())
|
|
500
|
+
|
|
501
|
+
return start_epoch, end_epoch
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def dump_request(
|
|
505
|
+
logger, url: str, method: str, json, params, headers, request_uuid: str, body=True
|
|
506
|
+
):
|
|
507
|
+
request_headers_filtered = {
|
|
508
|
+
key: value for key, value in headers.items() if key != "Authorization"
|
|
509
|
+
}
|
|
510
|
+
# Log the request details before sending the request
|
|
511
|
+
request_data = {
|
|
512
|
+
"url": url,
|
|
513
|
+
"method": method,
|
|
514
|
+
"params": jsonp.dumps(params),
|
|
515
|
+
"uuid": str(request_uuid),
|
|
516
|
+
"request_headers": jsonp.dumps(request_headers_filtered),
|
|
517
|
+
}
|
|
518
|
+
log_lines = []
|
|
519
|
+
request_body = ""
|
|
520
|
+
if body:
|
|
521
|
+
request_body = jsonp.dumps(json)
|
|
522
|
+
log_lines.append(
|
|
523
|
+
f"\n---[ ZSCALER SDK REQUEST | ID:{request_uuid} ]-------------------------------"
|
|
524
|
+
)
|
|
525
|
+
log_lines.append(f"{method} {url}")
|
|
526
|
+
for key, value in headers.items():
|
|
527
|
+
log_lines.append(f"{key}: {value}")
|
|
528
|
+
if body and request_body != "" and request_body != "null":
|
|
529
|
+
log_lines.append(f"\n{request_body}")
|
|
530
|
+
log_lines.append(
|
|
531
|
+
"--------------------------------------------------------------------"
|
|
532
|
+
)
|
|
533
|
+
logger.info("\n".join(log_lines))
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def dump_response(
|
|
537
|
+
logger,
|
|
538
|
+
url: str,
|
|
539
|
+
method: str,
|
|
540
|
+
resp,
|
|
541
|
+
params,
|
|
542
|
+
request_uuid: str,
|
|
543
|
+
start_time,
|
|
544
|
+
from_cache: bool = None,
|
|
545
|
+
):
|
|
546
|
+
# Calculate the duration in seconds
|
|
547
|
+
end_time = time.time()
|
|
548
|
+
duration_seconds = end_time - start_time
|
|
549
|
+
# Convert the duration to milliseconds
|
|
550
|
+
duration_ms = duration_seconds * 1000
|
|
551
|
+
# Convert the headers to a regular dictionary
|
|
552
|
+
response_headers_dict = dict(resp.headers)
|
|
553
|
+
full_url = url
|
|
554
|
+
if params:
|
|
555
|
+
full_url += "?" + urlencode(params)
|
|
556
|
+
log_lines = []
|
|
557
|
+
response_body = ""
|
|
558
|
+
if resp.text:
|
|
559
|
+
response_body = resp.text
|
|
560
|
+
|
|
561
|
+
if from_cache:
|
|
562
|
+
log_lines.append(
|
|
563
|
+
f"\n---[ ZSCALER SDK RESPONSE | ID:{request_uuid} | "
|
|
564
|
+
f"FROM CACHE | DURATION:{duration_ms}ms ]" + "-" * 31
|
|
565
|
+
)
|
|
566
|
+
else:
|
|
567
|
+
log_lines.append(
|
|
568
|
+
f"\n---[ ZSCALER SDK RESPONSE | ID:{request_uuid} | "
|
|
569
|
+
f"DURATION:{duration_ms}ms ]" + "-" * 46
|
|
570
|
+
)
|
|
571
|
+
log_lines.append(f"{method} {full_url}")
|
|
572
|
+
for key, value in response_headers_dict.items():
|
|
573
|
+
log_lines.append(f"{key}: {value}")
|
|
574
|
+
if response_body and response_body != "" and response_body != "null":
|
|
575
|
+
log_lines.append(f"\n{response_body}")
|
|
576
|
+
log_lines.append("-" * 68)
|
|
577
|
+
logger.info("\n".join(log_lines))
|