pangea-sdk 3.7.0__tar.gz → 3.8.0__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 (46) hide show
  1. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/PKG-INFO +33 -6
  2. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/README.md +27 -2
  3. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/__init__.py +1 -1
  4. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/asyncio/request.py +123 -32
  5. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/asyncio/services/__init__.py +1 -0
  6. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/asyncio/services/audit.py +236 -21
  7. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/asyncio/services/authn.py +79 -50
  8. pangea_sdk-3.8.0/pangea/asyncio/services/authz.py +259 -0
  9. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/asyncio/services/base.py +9 -6
  10. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/asyncio/services/file_scan.py +3 -4
  11. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/asyncio/services/intel.py +5 -6
  12. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/asyncio/services/redact.py +21 -3
  13. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/asyncio/services/vault.py +28 -12
  14. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/config.py +10 -18
  15. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/dump_audit.py +1 -0
  16. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/exceptions.py +8 -0
  17. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/request.py +164 -74
  18. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/response.py +63 -17
  19. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/__init__.py +1 -0
  20. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/audit/audit.py +241 -55
  21. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/audit/exceptions.py +1 -2
  22. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/audit/models.py +83 -21
  23. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/audit/signing.py +1 -0
  24. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/audit/util.py +1 -0
  25. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/authn/authn.py +38 -4
  26. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/authn/models.py +9 -9
  27. pangea_sdk-3.8.0/pangea/services/authz.py +377 -0
  28. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/base.py +34 -14
  29. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/embargo.py +1 -2
  30. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/file_scan.py +3 -4
  31. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/intel.py +3 -4
  32. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/redact.py +21 -3
  33. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/vault/vault.py +29 -12
  34. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/utils.py +2 -3
  35. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pyproject.toml +13 -7
  36. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/asyncio/services/embargo.py +0 -0
  37. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/audit_logger.py +0 -0
  38. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/deep_verify.py +0 -0
  39. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/deprecated.py +0 -0
  40. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/py.typed +0 -0
  41. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/vault/models/asymmetric.py +0 -0
  42. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/vault/models/common.py +0 -0
  43. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/vault/models/secret.py +0 -0
  44. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/services/vault/models/symmetric.py +0 -0
  45. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/tools.py +0 -0
  46. {pangea_sdk-3.7.0 → pangea_sdk-3.8.0}/pangea/verify_audit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pangea-sdk
3
- Version: 3.7.0
3
+ Version: 3.8.0
4
4
  Summary: Pangea API SDK
5
5
  Home-page: https://pangea.cloud/docs/sdk/python/
6
6
  License: MIT
@@ -18,12 +18,14 @@ Classifier: Topic :: Software Development
18
18
  Classifier: Topic :: Software Development :: Libraries
19
19
  Requires-Dist: aiohttp (>=3.8.6,<4.0.0)
20
20
  Requires-Dist: asyncio (>=3.4.3,<4.0.0)
21
- Requires-Dist: cryptography (>=42.0.5,<43.0.0)
21
+ Requires-Dist: cryptography (>=42.0.7,<43.0.0)
22
22
  Requires-Dist: deprecated (>=1.2.14,<2.0.0)
23
23
  Requires-Dist: google-crc32c (>=1.5.0,<2.0.0)
24
- Requires-Dist: pydantic (>=1.10.14,<2.0.0)
25
- Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
24
+ Requires-Dist: pydantic (>=1.10.15,<2.0.0)
25
+ Requires-Dist: python-dateutil (>=2.9.0,<3.0.0)
26
26
  Requires-Dist: requests (>=2.31.0,<3.0.0)
27
+ Requires-Dist: requests-toolbelt (>=1.0.0,<2.0.0)
28
+ Requires-Dist: typing-extensions (>=4.7.1,<5.0.0)
27
29
  Project-URL: Repository, https://github.com/pangeacyber/pangea-python/tree/main/packages/pangea-sdk
28
30
  Description-Content-Type: text/markdown
29
31
 
@@ -43,6 +45,8 @@ above.
43
45
 
44
46
  ## Installation
45
47
 
48
+ #### GA releases
49
+
46
50
  Via pip:
47
51
 
48
52
  ```bash
@@ -55,10 +59,32 @@ Via poetry:
55
59
  $ poetry add pangea-sdk
56
60
  ```
57
61
 
62
+ <a name="beta-releases"></a>
63
+
64
+ #### Beta releases
65
+
66
+ Pre-release versions may be available with the `b` (beta) denotation in the
67
+ version number. These releases serve to preview beta services and APIs. Per
68
+ Semantic Versioning, they are considered unstable and do not carry the same
69
+ compatibility guarantees as stable releases. [Beta changelog](https://github.com/pangeacyber/pangea-python/blob/beta/CHANGELOG.md).
70
+
71
+ Via pip:
72
+
73
+ ```bash
74
+ $ pip3 install pangea-sdk==3.8.0b2
75
+ ```
76
+
77
+ Via poetry:
78
+
79
+ ```bash
80
+ $ poetry add pangea-sdk==3.8.0b2
81
+ ```
82
+
58
83
  ## Usage
59
84
 
60
85
  - [Documentation][]
61
- - [Examples][]
86
+ - [GA Examples][]
87
+ - [Beta Examples][]
62
88
 
63
89
  General usage would be to create a token for a service through the
64
90
  [Pangea Console][] and then construct an API client for that respective service.
@@ -202,7 +228,8 @@ It accepts multiple file formats:
202
228
 
203
229
 
204
230
  [Documentation]: https://pangea.cloud/docs/sdk/python/
205
- [Examples]: https://github.com/pangeacyber/pangea-python/tree/main/examples
231
+ [GA Examples]: https://github.com/pangeacyber/pangea-python/tree/main/examples
232
+ [Beta Examples]: https://github.com/pangeacyber/pangea-python/tree/beta/examples
206
233
  [Pangea Console]: https://console.pangea.cloud/
207
234
  [Slack]: https://pangea.cloud/join-slack/
208
235
  [Secure Audit Log]: https://pangea.cloud/docs/audit
@@ -14,6 +14,8 @@ above.
14
14
 
15
15
  ## Installation
16
16
 
17
+ #### GA releases
18
+
17
19
  Via pip:
18
20
 
19
21
  ```bash
@@ -26,10 +28,32 @@ Via poetry:
26
28
  $ poetry add pangea-sdk
27
29
  ```
28
30
 
31
+ <a name="beta-releases"></a>
32
+
33
+ #### Beta releases
34
+
35
+ Pre-release versions may be available with the `b` (beta) denotation in the
36
+ version number. These releases serve to preview beta services and APIs. Per
37
+ Semantic Versioning, they are considered unstable and do not carry the same
38
+ compatibility guarantees as stable releases. [Beta changelog](https://github.com/pangeacyber/pangea-python/blob/beta/CHANGELOG.md).
39
+
40
+ Via pip:
41
+
42
+ ```bash
43
+ $ pip3 install pangea-sdk==3.8.0b2
44
+ ```
45
+
46
+ Via poetry:
47
+
48
+ ```bash
49
+ $ poetry add pangea-sdk==3.8.0b2
50
+ ```
51
+
29
52
  ## Usage
30
53
 
31
54
  - [Documentation][]
32
- - [Examples][]
55
+ - [GA Examples][]
56
+ - [Beta Examples][]
33
57
 
34
58
  General usage would be to create a token for a service through the
35
59
  [Pangea Console][] and then construct an API client for that respective service.
@@ -173,7 +197,8 @@ It accepts multiple file formats:
173
197
 
174
198
 
175
199
  [Documentation]: https://pangea.cloud/docs/sdk/python/
176
- [Examples]: https://github.com/pangeacyber/pangea-python/tree/main/examples
200
+ [GA Examples]: https://github.com/pangeacyber/pangea-python/tree/main/examples
201
+ [Beta Examples]: https://github.com/pangeacyber/pangea-python/tree/beta/examples
177
202
  [Pangea Console]: https://console.pangea.cloud/
178
203
  [Slack]: https://pangea.cloud/join-slack/
179
204
  [Secure Audit Log]: https://pangea.cloud/docs/audit
@@ -1,4 +1,4 @@
1
- __version__ = "3.7.0"
1
+ __version__ = "3.8.0"
2
2
 
3
3
  from pangea.asyncio.request import PangeaRequestAsync
4
4
  from pangea.config import PangeaConfig
@@ -7,14 +7,16 @@ import time
7
7
  from typing import Dict, List, Optional, Tuple, Type, Union
8
8
 
9
9
  import aiohttp
10
- import pangea.exceptions as pe
11
10
  from aiohttp import FormData
11
+ from typing_extensions import TypeVar
12
12
 
13
- # from requests.adapters import HTTPAdapter, Retry
14
- from pangea.request import PangeaRequestBase
15
- from pangea.response import AcceptedResult, PangeaResponse, PangeaResponseResult, ResponseStatus, TransferMethod
13
+ import pangea.exceptions as pe
14
+ from pangea.request import MultipartResponse, PangeaRequestBase
15
+ from pangea.response import AttachedFile, PangeaResponse, PangeaResponseResult, ResponseStatus, TransferMethod
16
16
  from pangea.utils import default_encoder
17
17
 
18
+ TResult = TypeVar("TResult", bound=PangeaResponseResult, default=PangeaResponseResult)
19
+
18
20
 
19
21
  class PangeaRequestAsync(PangeaRequestBase):
20
22
  """An object that makes direct calls to Pangea Service APIs.
@@ -28,12 +30,12 @@ class PangeaRequestAsync(PangeaRequestBase):
28
30
  async def post(
29
31
  self,
30
32
  endpoint: str,
31
- result_class: Type[PangeaResponseResult],
33
+ result_class: Type[TResult],
32
34
  data: Union[str, Dict] = {},
33
- files: Optional[List[Tuple]] = None,
35
+ files: List[Tuple] = [],
34
36
  poll_result: bool = True,
35
37
  url: Optional[str] = None,
36
- ) -> PangeaResponse:
38
+ ) -> PangeaResponse[TResult]:
37
39
  """Makes the POST call to a Pangea Service endpoint.
38
40
 
39
41
  Args:
@@ -56,7 +58,7 @@ class PangeaRequestAsync(PangeaRequestBase):
56
58
  )
57
59
  transfer_method = data.get("transfer_method", None) # type: ignore[union-attr]
58
60
 
59
- if files is not None and type(data) is dict and (transfer_method == TransferMethod.POST_URL.value):
61
+ if files and type(data) is dict and (transfer_method == TransferMethod.POST_URL.value):
60
62
  requests_response = await self._full_post_presigned_url(
61
63
  endpoint, result_class=result_class, data=data, files=files
62
64
  )
@@ -66,18 +68,32 @@ class PangeaRequestAsync(PangeaRequestBase):
66
68
  )
67
69
 
68
70
  await self._check_http_errors(requests_response)
69
- json_resp = await requests_response.json()
70
- self.logger.debug(json.dumps({"service": self.service, "action": "post", "url": url, "response": json_resp}))
71
71
 
72
- pangea_response = PangeaResponse(requests_response, result_class=result_class, json=json_resp) # type: ignore[var-annotated]
72
+ if "multipart/form-data" in requests_response.headers.get("content-type", ""):
73
+ multipart_response = await self._process_multipart_response(requests_response)
74
+ pangea_response: PangeaResponse = PangeaResponse(
75
+ requests_response,
76
+ result_class=result_class,
77
+ json=multipart_response.pangea_json,
78
+ attached_files=multipart_response.attached_files,
79
+ )
80
+ else:
81
+ try:
82
+ json_resp = await requests_response.json()
83
+ self.logger.debug(
84
+ json.dumps({"service": self.service, "action": "post", "url": url, "response": json_resp})
85
+ )
86
+
87
+ pangea_response = PangeaResponse(requests_response, result_class=result_class, json=json_resp)
88
+ except aiohttp.ContentTypeError as e:
89
+ raise pe.PangeaException(f"Failed to decode json response. {e}. Body: {await requests_response.text()}")
90
+
73
91
  if poll_result:
74
92
  pangea_response = await self._handle_queued_result(pangea_response)
75
93
 
76
94
  return self._check_response(pangea_response)
77
95
 
78
- async def get(
79
- self, path: str, result_class: Type[PangeaResponseResult], check_response: bool = True
80
- ) -> PangeaResponse:
96
+ async def get(self, path: str, result_class: Type[TResult], check_response: bool = True) -> PangeaResponse[TResult]:
81
97
  """Makes the GET call to a Pangea Service endpoint.
82
98
 
83
99
  Args:
@@ -94,7 +110,7 @@ class PangeaRequestAsync(PangeaRequestBase):
94
110
 
95
111
  async with self.session.get(url, headers=self._headers()) as requests_response:
96
112
  await self._check_http_errors(requests_response)
97
- pangea_response = PangeaResponse( # type: ignore[var-annotated]
113
+ pangea_response = PangeaResponse(
98
114
  requests_response, result_class=result_class, json=await requests_response.json()
99
115
  )
100
116
 
@@ -115,11 +131,11 @@ class PangeaRequestAsync(PangeaRequestBase):
115
131
  raise pe.ServiceTemporarilyUnavailable(await resp.json())
116
132
 
117
133
  async def poll_result_by_id(
118
- self, request_id: str, result_class: Union[Type[PangeaResponseResult], dict], check_response: bool = True
119
- ):
134
+ self, request_id: str, result_class: Type[TResult], check_response: bool = True
135
+ ) -> PangeaResponse[TResult]:
120
136
  path = self._get_poll_path(request_id)
121
137
  self.logger.debug(json.dumps({"service": self.service, "action": "poll_result_once", "url": path}))
122
- return await self.get(path, result_class, check_response=check_response) # type: ignore[arg-type]
138
+ return await self.get(path, result_class, check_response=check_response)
123
139
 
124
140
  async def poll_result_once(self, response: PangeaResponse, check_response: bool = True):
125
141
  request_id = response.request_id
@@ -157,12 +173,84 @@ class PangeaRequestAsync(PangeaRequestBase):
157
173
  if resp.status < 200 or resp.status >= 300:
158
174
  raise pe.PresignedUploadError(f"presigned PUT failure: {resp.status}", await resp.text())
159
175
 
176
+ async def download_file(self, url: str, filename: Optional[str] = None) -> AttachedFile:
177
+ self.logger.debug(
178
+ json.dumps(
179
+ {
180
+ "service": self.service,
181
+ "action": "download_file",
182
+ "url": url,
183
+ "status": "start",
184
+ }
185
+ )
186
+ )
187
+ async with self.session.get(url, headers={}) as response:
188
+ if response.status == 200:
189
+ if filename is None:
190
+ content_disposition = response.headers.get("Content-Disposition", "")
191
+ filename = self._get_filename_from_content_disposition(content_disposition)
192
+ if filename is None:
193
+ filename = self._get_filename_from_url(url)
194
+ if filename is None:
195
+ filename = "default_filename"
196
+
197
+ content_type = response.headers.get("Content-Type", "")
198
+ self.logger.debug(
199
+ json.dumps(
200
+ {
201
+ "service": self.service,
202
+ "action": "download_file",
203
+ "url": url,
204
+ "filename": filename,
205
+ "status": "success",
206
+ }
207
+ )
208
+ )
209
+
210
+ return AttachedFile(filename=filename, file=await response.read(), content_type=content_type)
211
+ else:
212
+ raise pe.DownloadFileError(f"Failed to download file. Status: {response.status}", await response.text())
213
+
214
+ async def _get_pangea_json(self, reader: aiohttp.MultipartReader) -> Optional[Dict]:
215
+ # Iterate through parts
216
+ async for part in reader:
217
+ return await part.json()
218
+ return None
219
+
220
+ async def _get_attached_files(self, reader: aiohttp.MultipartReader) -> List[AttachedFile]:
221
+ files = []
222
+ i = 0
223
+
224
+ async for part in reader:
225
+ content_type = part.headers.get("Content-Type", "")
226
+ content_disposition = part.headers.get("Content-Disposition", "")
227
+ name = self._get_filename_from_content_disposition(content_disposition)
228
+ if name is None:
229
+ name = f"default_file_name_{i}"
230
+ i += 1
231
+ files.append(AttachedFile(name, await part.read(), content_type))
232
+
233
+ return files
234
+
235
+ async def _process_multipart_response(self, resp: aiohttp.ClientResponse) -> MultipartResponse:
236
+ # Parse the multipart response
237
+ multipart_reader = aiohttp.MultipartReader.from_response(resp)
238
+
239
+ pangea_json = await self._get_pangea_json(multipart_reader) # type: ignore[arg-type]
240
+ self.logger.debug(
241
+ json.dumps({"service": self.service, "action": "multipart response", "response": pangea_json})
242
+ )
243
+
244
+ multipart_reader = multipart_reader.__aiter__()
245
+ attached_files = await self._get_attached_files(multipart_reader) # type: ignore[arg-type]
246
+ return MultipartResponse(pangea_json, attached_files) # type: ignore[arg-type]
247
+
160
248
  async def _http_post(
161
249
  self,
162
250
  url: str,
163
251
  headers: Dict = {},
164
252
  data: Union[str, Dict] = {},
165
- files: Optional[List[Tuple]] = None,
253
+ files: List[Tuple] = [],
166
254
  presigned_url_post: bool = False,
167
255
  ) -> aiohttp.ClientResponse:
168
256
  if files:
@@ -193,26 +281,29 @@ class PangeaRequestAsync(PangeaRequestBase):
193
281
  self.logger.debug(
194
282
  json.dumps({"service": self.service, "action": "http_put", "url": url}, default=default_encoder)
195
283
  )
196
- form = FormData()
197
- name, value = files[0]
198
- form.add_field(name, value[1], filename=value[0], content_type=value[2])
199
- return await self.session.put(url, headers=headers, data=form)
284
+ _, value = files[0]
285
+ return await self.session.put(url, headers=headers, data=value[1])
200
286
 
201
287
  async def _full_post_presigned_url(
202
288
  self,
203
289
  endpoint: str,
204
290
  result_class: Type[PangeaResponseResult],
205
291
  data: Union[str, Dict] = {},
206
- files: Optional[List[Tuple]] = None,
292
+ files: List[Tuple] = [],
207
293
  ):
208
- if len(files) == 0: # type: ignore[arg-type]
294
+ if len(files) == 0:
209
295
  raise AttributeError("files attribute should have at least 1 file")
210
296
 
211
297
  response = await self.request_presigned_url(endpoint=endpoint, result_class=result_class, data=data)
212
- data_to_presigned = response.accepted_result.post_form_data # type: ignore[union-attr]
213
- presigned_url = response.accepted_result.post_url # type: ignore[union-attr]
298
+ if response.accepted_result is None:
299
+ raise pe.PangeaException("No accepted_result field when requesting presigned url")
300
+ if response.accepted_result.post_url is None:
301
+ raise pe.PresignedURLException("No presigned url", response)
302
+
303
+ data_to_presigned = response.accepted_result.post_form_data
304
+ presigned_url = response.accepted_result.post_url
214
305
 
215
- await self.post_presigned_url(url=presigned_url, data=data_to_presigned, files=files) # type: ignore[arg-type]
306
+ await self.post_presigned_url(url=presigned_url, data=data_to_presigned, files=files)
216
307
  return response.raw_response
217
308
 
218
309
  async def request_presigned_url(
@@ -232,14 +323,14 @@ class PangeaRequestAsync(PangeaRequestBase):
232
323
  raise e
233
324
 
234
325
  # Receive 202
235
- return await self._poll_presigned_url(accepted_exception.response) # type: ignore[return-value]
326
+ return await self._poll_presigned_url(accepted_exception.response)
236
327
 
237
- async def _poll_presigned_url(self, response: PangeaResponse) -> AcceptedResult:
328
+ async def _poll_presigned_url(self, response: PangeaResponse[TResult]) -> PangeaResponse[TResult]:
238
329
  if response.http_status != 202:
239
330
  raise AttributeError("Response should be 202")
240
331
 
241
332
  if response.accepted_result is not None and response.accepted_result.has_upload_url:
242
- return response # type: ignore[return-value]
333
+ return response
243
334
 
244
335
  self.logger.debug(json.dumps({"service": self.service, "action": "poll_presigned_url", "step": "start"}))
245
336
  retry_count = 1
@@ -276,7 +367,7 @@ class PangeaRequestAsync(PangeaRequestBase):
276
367
  self.logger.debug(json.dumps({"service": self.service, "action": "poll_presigned_url", "step": "exit"}))
277
368
 
278
369
  if loop_resp.accepted_result is not None and not loop_resp.accepted_result.has_upload_url:
279
- return loop_resp # type: ignore[return-value]
370
+ return loop_resp
280
371
  else:
281
372
  raise loop_exc
282
373
 
@@ -1,5 +1,6 @@
1
1
  from .audit import AuditAsync
2
2
  from .authn import AuthNAsync
3
+ from .authz import AuthZAsync
3
4
  from .embargo import EmbargoAsync
4
5
  from .file_scan import FileScanAsync
5
6
  from .intel import DomainIntelAsync, FileIntelAsync, IpIntelAsync, UrlIntelAsync, UserIntelAsync