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.
Files changed (51) hide show
  1. pangea/__init__.py +2 -1
  2. pangea/asyncio/__init__.py +1 -0
  3. pangea/asyncio/file_uploader.py +39 -0
  4. pangea/asyncio/request.py +46 -23
  5. pangea/asyncio/services/__init__.py +2 -0
  6. pangea/asyncio/services/audit.py +46 -20
  7. pangea/asyncio/services/authn.py +123 -61
  8. pangea/asyncio/services/authz.py +57 -31
  9. pangea/asyncio/services/base.py +21 -2
  10. pangea/asyncio/services/embargo.py +2 -2
  11. pangea/asyncio/services/file_scan.py +24 -9
  12. pangea/asyncio/services/intel.py +104 -30
  13. pangea/asyncio/services/redact.py +52 -3
  14. pangea/asyncio/services/sanitize.py +217 -0
  15. pangea/asyncio/services/share.py +733 -0
  16. pangea/asyncio/services/vault.py +1709 -766
  17. pangea/crypto/rsa.py +135 -0
  18. pangea/deep_verify.py +7 -1
  19. pangea/dump_audit.py +9 -8
  20. pangea/file_uploader.py +35 -0
  21. pangea/request.py +70 -49
  22. pangea/response.py +36 -17
  23. pangea/services/__init__.py +2 -0
  24. pangea/services/audit/audit.py +57 -29
  25. pangea/services/audit/models.py +12 -3
  26. pangea/services/audit/signing.py +6 -5
  27. pangea/services/audit/util.py +3 -3
  28. pangea/services/authn/authn.py +120 -66
  29. pangea/services/authn/models.py +167 -11
  30. pangea/services/authz.py +53 -30
  31. pangea/services/base.py +16 -2
  32. pangea/services/embargo.py +2 -2
  33. pangea/services/file_scan.py +32 -15
  34. pangea/services/intel.py +155 -30
  35. pangea/services/redact.py +132 -3
  36. pangea/services/sanitize.py +388 -0
  37. pangea/services/share/file_format.py +170 -0
  38. pangea/services/share/share.py +1440 -0
  39. pangea/services/vault/models/asymmetric.py +120 -18
  40. pangea/services/vault/models/common.py +439 -141
  41. pangea/services/vault/models/keys.py +94 -0
  42. pangea/services/vault/models/secret.py +27 -3
  43. pangea/services/vault/models/symmetric.py +68 -22
  44. pangea/services/vault/vault.py +1690 -766
  45. pangea/tools.py +6 -7
  46. pangea/utils.py +94 -33
  47. pangea/verify_audit.py +270 -83
  48. {pangea_sdk-3.8.0.dist-info → pangea_sdk-5.3.0.dist-info}/METADATA +21 -29
  49. pangea_sdk-5.3.0.dist-info/RECORD +56 -0
  50. {pangea_sdk-3.8.0.dist-info → pangea_sdk-5.3.0.dist-info}/WHEEL +1 -1
  51. pangea_sdk-3.8.0.dist-info/RECORD +0 -46
pangea/__init__.py CHANGED
@@ -1,6 +1,7 @@
1
- __version__ = "3.8.0"
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 typing_extensions import TypeVar
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, default=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: Union[str, Dict] = {},
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: List[Tuple]):
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: Optional[str] = None) -> AttachedFile:
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
- else:
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.MultipartReader) -> Optional[Dict]:
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
- return await part.json()
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.MultipartReader) -> List[AttachedFile]:
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) # type: ignore[arg-type]
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
- multipart_reader = multipart_reader.__aiter__()
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: List[Tuple],
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
- resp = await self.post(endpoint=endpoint, result_class=result_class, data=data, poll_result=False)
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
- else:
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
@@ -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 entry
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
- try:
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.dict(exclude_none=True)
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.dict(exclude_none=True)
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.dict(exclude_none=True), poll_result=False
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.dict(exclude_none=True)
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.dict(exclude_none=True))
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.dict(exclude_none=True), poll_result=False
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.dict(exclude_none=True))
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(request_id=request_id, result_id=result_id, format=format)
589
- return await self.request.post("v1/download_results", DownloadResult, data=input.dict(exclude_none=True))
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].dict(exclude_none=True))
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.dict(exclude_none=True))
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