pangea-sdk 3.8.0__py3-none-any.whl → 5.3.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- pangea/__init__.py +2 -1
- pangea/asyncio/__init__.py +1 -0
- pangea/asyncio/file_uploader.py +39 -0
- pangea/asyncio/request.py +46 -23
- pangea/asyncio/services/__init__.py +2 -0
- pangea/asyncio/services/audit.py +46 -20
- pangea/asyncio/services/authn.py +123 -61
- pangea/asyncio/services/authz.py +57 -31
- pangea/asyncio/services/base.py +21 -2
- pangea/asyncio/services/embargo.py +2 -2
- pangea/asyncio/services/file_scan.py +24 -9
- pangea/asyncio/services/intel.py +104 -30
- pangea/asyncio/services/redact.py +52 -3
- pangea/asyncio/services/sanitize.py +217 -0
- pangea/asyncio/services/share.py +733 -0
- pangea/asyncio/services/vault.py +1709 -766
- pangea/crypto/rsa.py +135 -0
- pangea/deep_verify.py +7 -1
- pangea/dump_audit.py +9 -8
- pangea/file_uploader.py +35 -0
- pangea/request.py +70 -49
- pangea/response.py +36 -17
- pangea/services/__init__.py +2 -0
- pangea/services/audit/audit.py +57 -29
- pangea/services/audit/models.py +12 -3
- pangea/services/audit/signing.py +6 -5
- pangea/services/audit/util.py +3 -3
- pangea/services/authn/authn.py +120 -66
- pangea/services/authn/models.py +167 -11
- pangea/services/authz.py +53 -30
- pangea/services/base.py +16 -2
- pangea/services/embargo.py +2 -2
- pangea/services/file_scan.py +32 -15
- pangea/services/intel.py +155 -30
- pangea/services/redact.py +132 -3
- pangea/services/sanitize.py +388 -0
- pangea/services/share/file_format.py +170 -0
- pangea/services/share/share.py +1440 -0
- pangea/services/vault/models/asymmetric.py +120 -18
- pangea/services/vault/models/common.py +439 -141
- pangea/services/vault/models/keys.py +94 -0
- pangea/services/vault/models/secret.py +27 -3
- pangea/services/vault/models/symmetric.py +68 -22
- pangea/services/vault/vault.py +1690 -766
- pangea/tools.py +6 -7
- pangea/utils.py +94 -33
- pangea/verify_audit.py +270 -83
- {pangea_sdk-3.8.0.dist-info → pangea_sdk-5.3.0.dist-info}/METADATA +21 -29
- pangea_sdk-5.3.0.dist-info/RECORD +56 -0
- {pangea_sdk-3.8.0.dist-info → pangea_sdk-5.3.0.dist-info}/WHEEL +1 -1
- pangea_sdk-3.8.0.dist-info/RECORD +0 -46
pangea/__init__.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
__version__ = "3.
|
1
|
+
__version__ = "5.3.0"
|
2
2
|
|
3
3
|
from pangea.asyncio.request import PangeaRequestAsync
|
4
4
|
from pangea.config import PangeaConfig
|
5
|
+
from pangea.file_uploader import FileUploader
|
5
6
|
from pangea.request import PangeaRequest
|
6
7
|
from pangea.response import PangeaResponse
|
@@ -0,0 +1 @@
|
|
1
|
+
from .file_uploader import FileUploaderAsync
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Copyright 2022 Pangea Cyber Corporation
|
2
|
+
# Author: Pangea Cyber Corporation
|
3
|
+
import io
|
4
|
+
import logging
|
5
|
+
from typing import Dict, Optional
|
6
|
+
|
7
|
+
from pangea.asyncio.request import PangeaRequestAsync
|
8
|
+
from pangea.request import PangeaConfig
|
9
|
+
from pangea.response import TransferMethod
|
10
|
+
|
11
|
+
|
12
|
+
class FileUploaderAsync:
|
13
|
+
def __init__(self) -> None:
|
14
|
+
self.logger = logging.getLogger("pangea")
|
15
|
+
self._request: PangeaRequestAsync = PangeaRequestAsync(
|
16
|
+
config=PangeaConfig(),
|
17
|
+
token="",
|
18
|
+
service="FileUploader",
|
19
|
+
logger=self.logger,
|
20
|
+
)
|
21
|
+
|
22
|
+
async def upload_file(
|
23
|
+
self,
|
24
|
+
url: str,
|
25
|
+
file: io.BufferedReader,
|
26
|
+
transfer_method: TransferMethod = TransferMethod.PUT_URL,
|
27
|
+
file_details: Optional[Dict] = None,
|
28
|
+
) -> None:
|
29
|
+
if transfer_method == TransferMethod.PUT_URL:
|
30
|
+
files = [("file", ("filename", file, "application/octet-stream"))]
|
31
|
+
await self._request.put_presigned_url(url=url, files=files)
|
32
|
+
elif transfer_method == TransferMethod.POST_URL:
|
33
|
+
files = [("file", ("filename", file, "application/octet-stream"))]
|
34
|
+
await self._request.post_presigned_url(url=url, data=file_details, files=files) # type: ignore[arg-type]
|
35
|
+
else:
|
36
|
+
raise ValueError(f"Transfer method not supported: {transfer_method}")
|
37
|
+
|
38
|
+
async def close(self) -> None:
|
39
|
+
await self._request.session.close()
|
pangea/asyncio/request.py
CHANGED
@@ -1,21 +1,23 @@
|
|
1
1
|
# Copyright 2022 Pangea Cyber Corporation
|
2
2
|
# Author: Pangea Cyber Corporation
|
3
|
+
from __future__ import annotations
|
3
4
|
|
4
5
|
import asyncio
|
5
6
|
import json
|
6
7
|
import time
|
7
|
-
from typing import Dict, List, Optional, Tuple, Type, Union
|
8
|
+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
|
8
9
|
|
9
10
|
import aiohttp
|
10
11
|
from aiohttp import FormData
|
11
|
-
from
|
12
|
+
from pydantic import BaseModel
|
13
|
+
from typing_extensions import Any, TypeVar
|
12
14
|
|
13
15
|
import pangea.exceptions as pe
|
14
16
|
from pangea.request import MultipartResponse, PangeaRequestBase
|
15
17
|
from pangea.response import AttachedFile, PangeaResponse, PangeaResponseResult, ResponseStatus, TransferMethod
|
16
18
|
from pangea.utils import default_encoder
|
17
19
|
|
18
|
-
TResult = TypeVar("TResult", bound=PangeaResponseResult
|
20
|
+
TResult = TypeVar("TResult", bound=PangeaResponseResult)
|
19
21
|
|
20
22
|
|
21
23
|
class PangeaRequestAsync(PangeaRequestBase):
|
@@ -31,8 +33,8 @@ class PangeaRequestAsync(PangeaRequestBase):
|
|
31
33
|
self,
|
32
34
|
endpoint: str,
|
33
35
|
result_class: Type[TResult],
|
34
|
-
data:
|
35
|
-
files: List[Tuple] =
|
36
|
+
data: str | BaseModel | dict[str, Any] | None = None,
|
37
|
+
files: Optional[List[Tuple]] = None,
|
36
38
|
poll_result: bool = True,
|
37
39
|
url: Optional[str] = None,
|
38
40
|
) -> PangeaResponse[TResult]:
|
@@ -46,6 +48,13 @@ class PangeaRequestAsync(PangeaRequestBase):
|
|
46
48
|
PangeaResponse which contains the response in its entirety and
|
47
49
|
various properties to retrieve individual fields
|
48
50
|
"""
|
51
|
+
|
52
|
+
if isinstance(data, BaseModel):
|
53
|
+
data = data.model_dump(exclude_none=True)
|
54
|
+
|
55
|
+
if data is None:
|
56
|
+
data = {}
|
57
|
+
|
49
58
|
if url is None:
|
50
59
|
url = self._url(endpoint)
|
51
60
|
|
@@ -160,7 +169,7 @@ class PangeaRequestAsync(PangeaRequestBase):
|
|
160
169
|
if resp.status < 200 or resp.status >= 300:
|
161
170
|
raise pe.PresignedUploadError(f"presigned POST failure: {resp.status}", await resp.text())
|
162
171
|
|
163
|
-
async def put_presigned_url(self, url: str, files:
|
172
|
+
async def put_presigned_url(self, url: str, files: Sequence[Tuple]):
|
164
173
|
# Send put request with file as body
|
165
174
|
resp = await self._http_put(url=url, files=files)
|
166
175
|
self.logger.debug(
|
@@ -173,13 +182,27 @@ class PangeaRequestAsync(PangeaRequestBase):
|
|
173
182
|
if resp.status < 200 or resp.status >= 300:
|
174
183
|
raise pe.PresignedUploadError(f"presigned PUT failure: {resp.status}", await resp.text())
|
175
184
|
|
176
|
-
async def download_file(self, url: str, filename:
|
185
|
+
async def download_file(self, url: str, filename: str | None = None) -> AttachedFile:
|
186
|
+
"""
|
187
|
+
Download file
|
188
|
+
|
189
|
+
Download a file from the specified URL and save it with the given
|
190
|
+
filename.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
url: URL of the file to download
|
194
|
+
filename: Name to save the downloaded file as. If not provided, the
|
195
|
+
filename will be determined from the Content-Disposition header or
|
196
|
+
the URL.
|
197
|
+
"""
|
198
|
+
|
177
199
|
self.logger.debug(
|
178
200
|
json.dumps(
|
179
201
|
{
|
180
202
|
"service": self.service,
|
181
203
|
"action": "download_file",
|
182
204
|
"url": url,
|
205
|
+
"filename": filename,
|
183
206
|
"status": "start",
|
184
207
|
}
|
185
208
|
)
|
@@ -208,16 +231,16 @@ class PangeaRequestAsync(PangeaRequestBase):
|
|
208
231
|
)
|
209
232
|
|
210
233
|
return AttachedFile(filename=filename, file=await response.read(), content_type=content_type)
|
211
|
-
|
212
|
-
raise pe.DownloadFileError(f"Failed to download file. Status: {response.status}", await response.text())
|
234
|
+
raise pe.DownloadFileError(f"Failed to download file. Status: {response.status}", await response.text())
|
213
235
|
|
214
|
-
async def _get_pangea_json(self, reader: aiohttp.
|
236
|
+
async def _get_pangea_json(self, reader: aiohttp.multipart.MultipartResponseWrapper) -> Optional[Dict[str, Any]]:
|
215
237
|
# Iterate through parts
|
216
238
|
async for part in reader:
|
217
|
-
|
239
|
+
if isinstance(part, aiohttp.BodyPartReader):
|
240
|
+
return await part.json()
|
218
241
|
return None
|
219
242
|
|
220
|
-
async def _get_attached_files(self, reader: aiohttp.
|
243
|
+
async def _get_attached_files(self, reader: aiohttp.multipart.MultipartResponseWrapper) -> List[AttachedFile]:
|
221
244
|
files = []
|
222
245
|
i = 0
|
223
246
|
|
@@ -228,7 +251,7 @@ class PangeaRequestAsync(PangeaRequestBase):
|
|
228
251
|
if name is None:
|
229
252
|
name = f"default_file_name_{i}"
|
230
253
|
i += 1
|
231
|
-
files.append(AttachedFile(name, await part.read(), content_type))
|
254
|
+
files.append(AttachedFile(name, await part.read(), content_type)) # type: ignore[union-attr]
|
232
255
|
|
233
256
|
return files
|
234
257
|
|
@@ -236,13 +259,12 @@ class PangeaRequestAsync(PangeaRequestBase):
|
|
236
259
|
# Parse the multipart response
|
237
260
|
multipart_reader = aiohttp.MultipartReader.from_response(resp)
|
238
261
|
|
239
|
-
pangea_json = await self._get_pangea_json(multipart_reader)
|
262
|
+
pangea_json = await self._get_pangea_json(multipart_reader)
|
240
263
|
self.logger.debug(
|
241
264
|
json.dumps({"service": self.service, "action": "multipart response", "response": pangea_json})
|
242
265
|
)
|
243
266
|
|
244
|
-
|
245
|
-
attached_files = await self._get_attached_files(multipart_reader) # type: ignore[arg-type]
|
267
|
+
attached_files = await self._get_attached_files(multipart_reader)
|
246
268
|
return MultipartResponse(pangea_json, attached_files) # type: ignore[arg-type]
|
247
269
|
|
248
270
|
async def _http_post(
|
@@ -250,7 +272,7 @@ class PangeaRequestAsync(PangeaRequestBase):
|
|
250
272
|
url: str,
|
251
273
|
headers: Dict = {},
|
252
274
|
data: Union[str, Dict] = {},
|
253
|
-
files: List[Tuple] = [],
|
275
|
+
files: Optional[List[Tuple]] = [],
|
254
276
|
presigned_url_post: bool = False,
|
255
277
|
) -> aiohttp.ClientResponse:
|
256
278
|
if files:
|
@@ -275,7 +297,7 @@ class PangeaRequestAsync(PangeaRequestBase):
|
|
275
297
|
async def _http_put(
|
276
298
|
self,
|
277
299
|
url: str,
|
278
|
-
files:
|
300
|
+
files: Sequence[Tuple],
|
279
301
|
headers: Dict = {},
|
280
302
|
) -> aiohttp.ClientResponse:
|
281
303
|
self.logger.debug(
|
@@ -295,6 +317,9 @@ class PangeaRequestAsync(PangeaRequestBase):
|
|
295
317
|
raise AttributeError("files attribute should have at least 1 file")
|
296
318
|
|
297
319
|
response = await self.request_presigned_url(endpoint=endpoint, result_class=result_class, data=data)
|
320
|
+
if response.success: # This should only happen when uploading a zero bytes file
|
321
|
+
return response.raw_response
|
322
|
+
|
298
323
|
if response.accepted_result is None:
|
299
324
|
raise pe.PangeaException("No accepted_result field when requesting presigned url")
|
300
325
|
if response.accepted_result.post_url is None:
|
@@ -314,9 +339,8 @@ class PangeaRequestAsync(PangeaRequestBase):
|
|
314
339
|
) -> PangeaResponse:
|
315
340
|
# Send request
|
316
341
|
try:
|
317
|
-
# This should return 202 (AcceptedRequestException)
|
318
|
-
|
319
|
-
raise pe.PresignedURLException("Should return 202", resp)
|
342
|
+
# This should return 202 (AcceptedRequestException) at least zero size file is sent
|
343
|
+
return await self.post(endpoint=endpoint, result_class=result_class, data=data, poll_result=False)
|
320
344
|
except pe.AcceptedRequestException as e:
|
321
345
|
accepted_exception = e
|
322
346
|
except Exception as e:
|
@@ -368,8 +392,7 @@ class PangeaRequestAsync(PangeaRequestBase):
|
|
368
392
|
|
369
393
|
if loop_resp.accepted_result is not None and not loop_resp.accepted_result.has_upload_url:
|
370
394
|
return loop_resp
|
371
|
-
|
372
|
-
raise loop_exc
|
395
|
+
raise loop_exc
|
373
396
|
|
374
397
|
async def _handle_queued_result(self, response: PangeaResponse) -> PangeaResponse:
|
375
398
|
if self._queued_retry_enabled and response.http_status == 202:
|
@@ -5,4 +5,6 @@ from .embargo import EmbargoAsync
|
|
5
5
|
from .file_scan import FileScanAsync
|
6
6
|
from .intel import DomainIntelAsync, FileIntelAsync, IpIntelAsync, UrlIntelAsync, UserIntelAsync
|
7
7
|
from .redact import RedactAsync
|
8
|
+
from .sanitize import SanitizeAsync
|
9
|
+
from .share import ShareAsync
|
8
10
|
from .vault import VaultAsync
|
pangea/asyncio/services/audit.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import datetime
|
6
|
-
from typing import Any, Dict, List, Optional, Sequence, Union
|
6
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union
|
7
7
|
|
8
8
|
import pangea.exceptions as pexc
|
9
9
|
from pangea.asyncio.services.base import ServiceBaseAsync
|
@@ -174,14 +174,16 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
174
174
|
verbose: Optional[bool] = None,
|
175
175
|
) -> PangeaResponse[LogResult]:
|
176
176
|
"""
|
177
|
-
Log an
|
177
|
+
Log an event
|
178
178
|
|
179
179
|
Create a log entry in the Secure Audit Log.
|
180
|
+
|
180
181
|
Args:
|
181
182
|
event (dict[str, Any]): event to be logged
|
182
183
|
verify (bool, optional): True to verify logs consistency after response.
|
183
184
|
sign_local (bool, optional): True to sign event with local key.
|
184
185
|
verbose (bool, optional): True to get a more verbose response.
|
186
|
+
|
185
187
|
Raises:
|
186
188
|
AuditException: If an audit based api exception happens
|
187
189
|
PangeaAPIException: If an API Error happens
|
@@ -192,18 +194,12 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
192
194
|
Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#log-an-entry).
|
193
195
|
|
194
196
|
Examples:
|
195
|
-
|
196
|
-
log_response = audit.log({"message"="Hello world"}, verbose=False)
|
197
|
-
print(f"Response. Hash: {log_response.result.hash}")
|
198
|
-
except pe.PangeaAPIException as e:
|
199
|
-
print(f"Request Error: {e.response.summary}")
|
200
|
-
for err in e.errors:
|
201
|
-
print(f"\\t{err.detail} \\n")
|
197
|
+
response = await audit.log_event({"message": "hello world"}, verbose=True)
|
202
198
|
"""
|
203
199
|
|
204
200
|
input = self._get_log_request(event, sign_local=sign_local, verify=verify, verbose=verbose)
|
205
201
|
response: PangeaResponse[LogResult] = await self.request.post(
|
206
|
-
"v1/log", LogResult, data=input.
|
202
|
+
"v1/log", LogResult, data=input.model_dump(exclude_none=True)
|
207
203
|
)
|
208
204
|
if response.success and response.result is not None:
|
209
205
|
self._process_log_result(response.result, verify=verify)
|
@@ -239,7 +235,7 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
239
235
|
|
240
236
|
input = self._get_log_request(events, sign_local=sign_local, verify=False, verbose=verbose)
|
241
237
|
response: PangeaResponse[LogBulkResult] = await self.request.post(
|
242
|
-
"v2/log", LogBulkResult, data=input.
|
238
|
+
"v2/log", LogBulkResult, data=input.model_dump(exclude_none=True)
|
243
239
|
)
|
244
240
|
if response.success and response.result is not None:
|
245
241
|
for result in response.result.results:
|
@@ -277,7 +273,7 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
277
273
|
input = self._get_log_request(events, sign_local=sign_local, verify=False, verbose=verbose)
|
278
274
|
try:
|
279
275
|
response: PangeaResponse[LogBulkResult] = await self.request.post(
|
280
|
-
"v2/log_async", LogBulkResult, data=input.
|
276
|
+
"v2/log_async", LogBulkResult, data=input.model_dump(exclude_none=True), poll_result=False
|
281
277
|
)
|
282
278
|
except pexc.AcceptedRequestException as e:
|
283
279
|
return e.response
|
@@ -299,6 +295,7 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
299
295
|
verbose: Optional[bool] = None,
|
300
296
|
verify_consistency: bool = False,
|
301
297
|
verify_events: bool = True,
|
298
|
+
return_context: Optional[bool] = None,
|
302
299
|
) -> PangeaResponse[SearchOutput]:
|
303
300
|
"""
|
304
301
|
Search the log
|
@@ -328,6 +325,7 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
328
325
|
verbose (bool, optional): If true, response include root and membership and consistency proofs.
|
329
326
|
verify_consistency (bool): True to verify logs consistency
|
330
327
|
verify_events (bool): True to verify hash events and signatures
|
328
|
+
return_context (bool): Return the context data needed to decrypt secure audit events that have been redacted with format preserving encryption.
|
331
329
|
|
332
330
|
Raises:
|
333
331
|
AuditException: If an audit based api exception happens
|
@@ -355,10 +353,11 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
355
353
|
max_results=max_results,
|
356
354
|
search_restriction=search_restriction,
|
357
355
|
verbose=verbose,
|
356
|
+
return_context=return_context,
|
358
357
|
)
|
359
358
|
|
360
359
|
response: PangeaResponse[SearchOutput] = await self.request.post(
|
361
|
-
"v1/search", SearchOutput, data=input.
|
360
|
+
"v1/search", SearchOutput, data=input.model_dump(exclude_none=True)
|
362
361
|
)
|
363
362
|
if verify_consistency:
|
364
363
|
await self.update_published_roots(response.result) # type: ignore[arg-type]
|
@@ -373,6 +372,7 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
373
372
|
assert_search_restriction: Optional[Dict[str, Sequence[str]]] = None,
|
374
373
|
verify_consistency: bool = False,
|
375
374
|
verify_events: bool = True,
|
375
|
+
return_context: Optional[bool] = None,
|
376
376
|
) -> PangeaResponse[SearchResultOutput]:
|
377
377
|
"""
|
378
378
|
Results of a search
|
@@ -388,6 +388,8 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
388
388
|
assert_search_restriction (Dict[str, Sequence[str]], optional): Assert the requested search results were queried with the exact same search restrictions, to ensure the results comply to the expected restrictions.
|
389
389
|
verify_consistency (bool): True to verify logs consistency
|
390
390
|
verify_events (bool): True to verify hash events and signatures
|
391
|
+
return_context (bool): Return the context data needed to decrypt secure audit events that have been redacted with format preserving encryption.
|
392
|
+
|
391
393
|
Raises:
|
392
394
|
AuditException: If an audit based api exception happens
|
393
395
|
PangeaAPIException: If an API Error happens
|
@@ -418,8 +420,9 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
418
420
|
limit=limit,
|
419
421
|
offset=offset,
|
420
422
|
assert_search_restriction=assert_search_restriction,
|
423
|
+
return_context=return_context,
|
421
424
|
)
|
422
|
-
response = await self.request.post("v1/results", SearchResultOutput, data=input.
|
425
|
+
response = await self.request.post("v1/results", SearchResultOutput, data=input.model_dump(exclude_none=True))
|
423
426
|
if verify_consistency and response.result is not None:
|
424
427
|
await self.update_published_roots(response.result)
|
425
428
|
|
@@ -481,7 +484,7 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
481
484
|
)
|
482
485
|
try:
|
483
486
|
return await self.request.post(
|
484
|
-
"v1/export", PangeaResponseResult, data=input.
|
487
|
+
"v1/export", PangeaResponseResult, data=input.model_dump(exclude_none=True), poll_result=False
|
485
488
|
)
|
486
489
|
except pexc.AcceptedRequestException as e:
|
487
490
|
return e.response
|
@@ -548,13 +551,14 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
548
551
|
response = audit.root(tree_size=7)
|
549
552
|
"""
|
550
553
|
input = RootRequest(tree_size=tree_size)
|
551
|
-
return await self.request.post("v1/root", RootResult, data=input.
|
554
|
+
return await self.request.post("v1/root", RootResult, data=input.model_dump(exclude_none=True))
|
552
555
|
|
553
556
|
async def download_results(
|
554
557
|
self,
|
555
558
|
result_id: Optional[str] = None,
|
556
559
|
format: DownloadFormat = DownloadFormat.CSV,
|
557
560
|
request_id: Optional[str] = None,
|
561
|
+
return_context: Optional[bool] = None,
|
558
562
|
) -> PangeaResponse[DownloadResult]:
|
559
563
|
"""
|
560
564
|
Download search results
|
@@ -567,6 +571,7 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
567
571
|
result_id: ID returned by the search API.
|
568
572
|
format: Format for the records.
|
569
573
|
request_id: ID returned by the export API.
|
574
|
+
return_context (bool): Return the context data needed to decrypt secure audit events that have been redacted with format preserving encryption.
|
570
575
|
|
571
576
|
Returns:
|
572
577
|
URL where search results can be downloaded.
|
@@ -585,8 +590,10 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
585
590
|
if request_id is None and result_id is None:
|
586
591
|
raise ValueError("must pass one of `request_id` or `result_id`")
|
587
592
|
|
588
|
-
input = DownloadRequest(
|
589
|
-
|
593
|
+
input = DownloadRequest(
|
594
|
+
request_id=request_id, result_id=result_id, format=format, return_context=return_context
|
595
|
+
)
|
596
|
+
return await self.request.post("v1/download_results", DownloadResult, data=input.model_dump(exclude_none=True))
|
590
597
|
|
591
598
|
async def update_published_roots(self, result: SearchResultOutput):
|
592
599
|
"""Fetches series of published root hashes from Arweave
|
@@ -611,12 +618,31 @@ class AuditAsync(ServiceBaseAsync, AuditBase):
|
|
611
618
|
for tree_size in tree_sizes:
|
612
619
|
pub_root = None
|
613
620
|
if tree_size in arweave_roots:
|
614
|
-
pub_root = PublishedRoot(**arweave_roots[tree_size].
|
621
|
+
pub_root = PublishedRoot(**arweave_roots[tree_size].model_dump(exclude_none=True))
|
615
622
|
pub_root.source = RootSource.ARWEAVE
|
616
623
|
elif self.allow_server_roots:
|
617
624
|
resp = await self.root(tree_size=tree_size)
|
618
625
|
if resp.success and resp.result is not None:
|
619
|
-
pub_root = PublishedRoot(**resp.result.data.
|
626
|
+
pub_root = PublishedRoot(**resp.result.data.model_dump(exclude_none=True))
|
620
627
|
pub_root.source = RootSource.PANGEA
|
621
628
|
if pub_root is not None:
|
622
629
|
self.pub_roots[tree_size] = pub_root
|
630
|
+
|
631
|
+
await self._fix_consistency_proofs(tree_sizes)
|
632
|
+
|
633
|
+
async def _fix_consistency_proofs(self, tree_sizes: Iterable[int]) -> None:
|
634
|
+
# on very rare occasions, the consistency proof in Arweave may be wrong
|
635
|
+
# override it with the proof from pangea (not the root hash, just the proof)
|
636
|
+
for tree_size in tree_sizes:
|
637
|
+
if tree_size not in self.pub_roots or tree_size - 1 not in self.pub_roots:
|
638
|
+
continue
|
639
|
+
|
640
|
+
if self.pub_roots[tree_size].source == RootSource.PANGEA:
|
641
|
+
continue
|
642
|
+
|
643
|
+
if self.verify_consistency_proof(tree_size):
|
644
|
+
continue
|
645
|
+
|
646
|
+
resp = await self.root(tree_size=tree_size)
|
647
|
+
if resp.success and resp.result is not None and resp.result.data is not None:
|
648
|
+
self.pub_roots[tree_size].consistency_proof = resp.result.data.consistency_proof
|