pangea-sdk 3.8.0__py3-none-any.whl → 5.3.0__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.
- 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
|