pangea-sdk 3.8.0b4__py3-none-any.whl → 4.0.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.
Files changed (43) hide show
  1. pangea/__init__.py +1 -2
  2. pangea/asyncio/request.py +17 -22
  3. pangea/asyncio/services/__init__.py +0 -2
  4. pangea/asyncio/services/audit.py +188 -23
  5. pangea/asyncio/services/authn.py +167 -108
  6. pangea/asyncio/services/authz.py +36 -45
  7. pangea/asyncio/services/embargo.py +2 -2
  8. pangea/asyncio/services/file_scan.py +3 -3
  9. pangea/asyncio/services/intel.py +44 -26
  10. pangea/asyncio/services/redact.py +60 -4
  11. pangea/asyncio/services/vault.py +145 -30
  12. pangea/dump_audit.py +1 -1
  13. pangea/request.py +30 -24
  14. pangea/response.py +34 -42
  15. pangea/services/__init__.py +0 -2
  16. pangea/services/audit/audit.py +202 -34
  17. pangea/services/audit/models.py +56 -8
  18. pangea/services/audit/util.py +3 -3
  19. pangea/services/authn/authn.py +116 -65
  20. pangea/services/authn/models.py +88 -4
  21. pangea/services/authz.py +51 -56
  22. pangea/services/base.py +23 -6
  23. pangea/services/embargo.py +2 -2
  24. pangea/services/file_scan.py +3 -2
  25. pangea/services/intel.py +25 -23
  26. pangea/services/redact.py +124 -4
  27. pangea/services/vault/models/common.py +121 -6
  28. pangea/services/vault/models/symmetric.py +2 -2
  29. pangea/services/vault/vault.py +143 -32
  30. pangea/utils.py +20 -109
  31. pangea/verify_audit.py +267 -83
  32. {pangea_sdk-3.8.0b4.dist-info → pangea_sdk-4.0.0.dist-info}/METADATA +12 -20
  33. pangea_sdk-4.0.0.dist-info/RECORD +46 -0
  34. {pangea_sdk-3.8.0b4.dist-info → pangea_sdk-4.0.0.dist-info}/WHEEL +1 -1
  35. pangea/asyncio/__init__.py +0 -1
  36. pangea/asyncio/file_uploader.py +0 -39
  37. pangea/asyncio/services/sanitize.py +0 -185
  38. pangea/asyncio/services/share.py +0 -573
  39. pangea/file_uploader.py +0 -35
  40. pangea/services/sanitize.py +0 -275
  41. pangea/services/share/file_format.py +0 -170
  42. pangea/services/share/share.py +0 -877
  43. pangea_sdk-3.8.0b4.dist-info/RECORD +0 -54
@@ -5,6 +5,4 @@ from .embargo import Embargo
5
5
  from .file_scan import FileScan
6
6
  from .intel import DomainIntel, FileIntel, IpIntel, UrlIntel, UserIntel
7
7
  from .redact import Redact
8
- from .sanitize import Sanitize
9
- from .share.share import Share
10
8
  from .vault.vault import Vault
@@ -1,11 +1,14 @@
1
1
  # Copyright 2022 Pangea Cyber Corporation
2
2
  # Author: Pangea Cyber Corporation
3
+ from __future__ import annotations
4
+
3
5
  import datetime
4
6
  import json
5
- from typing import Any, Dict, List, Optional, Set, Tuple, Union
7
+ from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union
6
8
 
7
9
  import pangea.exceptions as pexc
8
- from pangea.response import PangeaResponse
10
+ from pangea.config import PangeaConfig
11
+ from pangea.response import PangeaResponse, PangeaResponseResult
9
12
  from pangea.services.audit.exceptions import AuditException, EventCorruption
10
13
  from pangea.services.audit.models import (
11
14
  DownloadFormat,
@@ -14,6 +17,7 @@ from pangea.services.audit.models import (
14
17
  Event,
15
18
  EventEnvelope,
16
19
  EventVerification,
20
+ ExportRequest,
17
21
  LogBulkRequest,
18
22
  LogBulkResult,
19
23
  LogEvent,
@@ -51,8 +55,8 @@ from pangea.utils import canonicalize_nested_json
51
55
 
52
56
  class AuditBase:
53
57
  def __init__(
54
- self, private_key_file: str = "", public_key_info: Dict[str, str] = {}, tenant_id: Optional[str] = None
55
- ):
58
+ self, private_key_file: str = "", public_key_info: dict[str, str] = {}, tenant_id: str | None = None
59
+ ) -> None:
56
60
  self.pub_roots: Dict[int, PublishedRoot] = {}
57
61
  self.buffer_data: Optional[str] = None
58
62
  self.signer: Optional[Signer] = Signer(private_key_file) if private_key_file else None
@@ -195,7 +199,7 @@ class AuditBase:
195
199
 
196
200
  # verify consistency proofs
197
201
  if self.can_verify_consistency_proof(search_event):
198
- if self.verify_consistency_proof(self.pub_roots, search_event):
202
+ if search_event.leaf_index is not None and self.verify_consistency_proof(search_event.leaf_index):
199
203
  search_event.consistency_verification = EventVerification.PASS
200
204
  else:
201
205
  search_event.consistency_verification = EventVerification.FAIL
@@ -218,7 +222,7 @@ class AuditBase:
218
222
  tree_sizes.difference_update(self.pub_roots.keys())
219
223
 
220
224
  if tree_sizes:
221
- arweave_roots = get_arweave_published_roots(result.root.tree_name, list(tree_sizes))
225
+ arweave_roots = get_arweave_published_roots(result.root.tree_name, tree_sizes)
222
226
  else:
223
227
  arweave_roots = {}
224
228
 
@@ -280,7 +284,7 @@ class AuditBase:
280
284
  """
281
285
  return event.published and event.leaf_index is not None and event.leaf_index >= 0 # type: ignore[return-value]
282
286
 
283
- def verify_consistency_proof(self, pub_roots: Dict[int, PublishedRoot], event: SearchEvent) -> bool:
287
+ def verify_consistency_proof(self, tree_size: int) -> bool:
284
288
  """
285
289
  Verify consistency proof
286
290
 
@@ -289,18 +293,17 @@ class AuditBase:
289
293
  Read more at: [What is a consistency proof?](https://pangea.cloud/docs/audit/merkle-trees#what-is-a-consistency-proof)
290
294
 
291
295
  Args:
292
- pub_roots (dict[int, Root]): list of published root hashes across time
293
- event (SearchEvent): Audit event to be verified.
296
+ leaf_index (int): The tree size of the root to be verified.
294
297
 
295
298
  Returns:
296
299
  bool: True if consistency proof is verified, False otherwise.
297
300
  """
298
301
 
299
- if event.leaf_index == 0:
302
+ if tree_size == 0:
300
303
  return True
301
304
 
302
- curr_root = pub_roots.get(event.leaf_index + 1) # type: ignore[operator]
303
- prev_root = pub_roots.get(event.leaf_index) # type: ignore[arg-type]
305
+ curr_root = self.pub_roots.get(tree_size + 1)
306
+ prev_root = self.pub_roots.get(tree_size)
304
307
 
305
308
  if not curr_root or not prev_root:
306
309
  return False
@@ -310,9 +313,12 @@ class AuditBase:
310
313
  ):
311
314
  return False
312
315
 
316
+ if curr_root.consistency_proof is None:
317
+ return False
318
+
313
319
  curr_root_hash = decode_hash(curr_root.root_hash)
314
320
  prev_root_hash = decode_hash(prev_root.root_hash)
315
- proof = decode_consistency_proof(curr_root.consistency_proof) # type: ignore[arg-type]
321
+ proof = decode_consistency_proof(curr_root.consistency_proof)
316
322
 
317
323
  return verify_consistency_proof(curr_root_hash, prev_root_hash, proof)
318
324
 
@@ -332,7 +338,9 @@ class AuditBase:
332
338
  if audit_envelope and audit_envelope.signature and public_key:
333
339
  v = Verifier()
334
340
  verification = v.verify_signature(
335
- audit_envelope.signature, canonicalize_event(audit_envelope.event), public_key # type: ignore[arg-type]
341
+ audit_envelope.signature,
342
+ canonicalize_event(Event(**audit_envelope.event)),
343
+ public_key,
336
344
  )
337
345
  if verification is not None:
338
346
  return EventVerification.PASS if verification else EventVerification.FAIL
@@ -374,14 +382,32 @@ class Audit(ServiceBase, AuditBase):
374
382
 
375
383
  def __init__(
376
384
  self,
377
- token,
378
- config=None,
385
+ token: str,
386
+ config: PangeaConfig | None = None,
379
387
  private_key_file: str = "",
380
- public_key_info: Dict[str, str] = {},
381
- tenant_id: Optional[str] = None,
382
- logger_name="pangea",
383
- config_id: Optional[str] = None,
384
- ):
388
+ public_key_info: dict[str, str] = {},
389
+ tenant_id: str | None = None,
390
+ logger_name: str = "pangea",
391
+ config_id: str | None = None,
392
+ ) -> None:
393
+ """
394
+ Audit client
395
+
396
+ Initializes a new Audit client.
397
+
398
+ Args:
399
+ token: Pangea API token.
400
+ config: Configuration.
401
+ private_key_file: Private key filepath.
402
+ public_key_info: Public key information.
403
+ tenant_id: Tenant ID.
404
+ logger_name: Logger name.
405
+ config_id: Configuration ID.
406
+
407
+ Examples:
408
+ config = PangeaConfig(domain="pangea_domain")
409
+ audit = Audit(token="pangea_token", config=config)
410
+ """
385
411
  # FIXME: Temporary check to deprecate config_id from PangeaConfig.
386
412
  # Delete it when deprecate PangeaConfig.config_id
387
413
  if config_id and config is not None and config.config_id is not None:
@@ -495,7 +521,9 @@ class Audit(ServiceBase, AuditBase):
495
521
  """
496
522
 
497
523
  input = self._get_log_request(event, sign_local=sign_local, verify=verify, verbose=verbose)
498
- response: PangeaResponse[LogResult] = self.request.post("v1/log", LogResult, data=input.dict(exclude_none=True))
524
+ response: PangeaResponse[LogResult] = self.request.post(
525
+ "v1/log", LogResult, data=input.model_dump(exclude_none=True)
526
+ )
499
527
  if response.success and response.result is not None:
500
528
  self._process_log_result(response.result, verify=verify)
501
529
  return response
@@ -535,7 +563,7 @@ class Audit(ServiceBase, AuditBase):
535
563
 
536
564
  input = self._get_log_request(events, sign_local=sign_local, verify=False, verbose=verbose)
537
565
  response: PangeaResponse[LogBulkResult] = self.request.post(
538
- "v2/log", LogBulkResult, data=input.dict(exclude_none=True)
566
+ "v2/log", LogBulkResult, data=input.model_dump(exclude_none=True)
539
567
  )
540
568
 
541
569
  if response.success and response.result is not None:
@@ -579,7 +607,7 @@ class Audit(ServiceBase, AuditBase):
579
607
  try:
580
608
  # Calling to v2 methods will return always a 202.
581
609
  response: PangeaResponse[LogBulkResult] = self.request.post(
582
- "v2/log_async", LogBulkResult, data=input.dict(exclude_none=True), poll_result=False
610
+ "v2/log_async", LogBulkResult, data=input.model_dump(exclude_none=True), poll_result=False
583
611
  )
584
612
  except pexc.AcceptedRequestException as e:
585
613
  return e.response
@@ -598,10 +626,11 @@ class Audit(ServiceBase, AuditBase):
598
626
  end: Optional[Union[datetime.datetime, str]] = None,
599
627
  limit: Optional[int] = None,
600
628
  max_results: Optional[int] = None,
601
- search_restriction: Optional[dict] = None,
629
+ search_restriction: Optional[Dict[str, Sequence[str]]] = None,
602
630
  verbose: Optional[bool] = None,
603
631
  verify_consistency: bool = False,
604
632
  verify_events: bool = True,
633
+ return_context: Optional[bool] = None,
605
634
  ) -> PangeaResponse[SearchOutput]:
606
635
  """
607
636
  Search the log
@@ -627,10 +656,11 @@ class Audit(ServiceBase, AuditBase):
627
656
  end (datetime, optional): An RFC-3339 formatted timestamp, or relative time adjustment from the current time.
628
657
  limit (int, optional): Optional[int] = None,
629
658
  max_results (int, optional): Maximum number of results to return.
630
- search_restriction (dict, optional): A list of keys to restrict the search results to. Useful for partitioning data available to the query string.
659
+ search_restriction (Dict[str, Sequence[str]], optional): A list of keys to restrict the search results to. Useful for partitioning data available to the query string.
631
660
  verbose (bool, optional): If true, response include root and membership and consistency proofs.
632
661
  verify_consistency (bool): True to verify logs consistency
633
662
  verify_events (bool): True to verify hash events and signatures
663
+ return_context (bool): Return the context data needed to decrypt secure audit events that have been redacted with format preserving encryption.
634
664
 
635
665
  Raises:
636
666
  AuditException: If an audit based api exception happens
@@ -664,10 +694,11 @@ class Audit(ServiceBase, AuditBase):
664
694
  max_results=max_results,
665
695
  search_restriction=search_restriction,
666
696
  verbose=verbose,
697
+ return_context=return_context,
667
698
  )
668
699
 
669
700
  response: PangeaResponse[SearchOutput] = self.request.post(
670
- "v1/search", SearchOutput, data=input.dict(exclude_none=True)
701
+ "v1/search", SearchOutput, data=input.model_dump(exclude_none=True)
671
702
  )
672
703
  if verify_consistency and response.result is not None:
673
704
  self.update_published_roots(response.result)
@@ -678,8 +709,10 @@ class Audit(ServiceBase, AuditBase):
678
709
  id: str,
679
710
  limit: Optional[int] = 20,
680
711
  offset: Optional[int] = 0,
712
+ assert_search_restriction: Optional[Dict[str, Sequence[str]]] = None,
681
713
  verify_consistency: bool = False,
682
714
  verify_events: bool = True,
715
+ return_context: Optional[bool] = None,
683
716
  ) -> PangeaResponse[SearchResultOutput]:
684
717
  """
685
718
  Results of a search
@@ -692,8 +725,10 @@ class Audit(ServiceBase, AuditBase):
692
725
  id (string): the id of a search action, found in `response.result.id`
693
726
  limit (integer, optional): the maximum number of results to return, default is 20
694
727
  offset (integer, optional): the position of the first result to return, default is 0
728
+ 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.
695
729
  verify_consistency (bool): True to verify logs consistency
696
730
  verify_events (bool): True to verify hash events and signatures
731
+ return_context (bool): Return the context data needed to decrypt secure audit events that have been redacted with format preserving encryption.
697
732
  Raises:
698
733
  AuditException: If an audit based api exception happens
699
734
  PangeaAPIException: If an API Error happens
@@ -716,14 +751,117 @@ class Audit(ServiceBase, AuditBase):
716
751
  id=id,
717
752
  limit=limit,
718
753
  offset=offset,
754
+ assert_search_restriction=assert_search_restriction,
755
+ return_context=return_context,
719
756
  )
720
757
  response: PangeaResponse[SearchResultOutput] = self.request.post(
721
- "v1/results", SearchResultOutput, data=input.dict(exclude_none=True)
758
+ "v1/results", SearchResultOutput, data=input.model_dump(exclude_none=True)
722
759
  )
723
760
  if verify_consistency and response.result is not None:
724
761
  self.update_published_roots(response.result)
725
762
  return self.handle_results_response(response, verify_consistency, verify_events)
726
763
 
764
+ def export(
765
+ self,
766
+ *,
767
+ format: DownloadFormat = DownloadFormat.CSV,
768
+ start: Optional[datetime.datetime] = None,
769
+ end: Optional[datetime.datetime] = None,
770
+ order: Optional[SearchOrder] = None,
771
+ order_by: Optional[str] = None,
772
+ verbose: bool = True,
773
+ ) -> PangeaResponse[PangeaResponseResult]:
774
+ """
775
+ Export from the audit log
776
+
777
+ Bulk export of data from the Secure Audit Log, with optional filtering.
778
+
779
+ OperationId: audit_post_v1_export
780
+
781
+ Args:
782
+ format: Format for the records.
783
+ start: The start of the time range to perform the search on.
784
+ end: The end of the time range to perform the search on. If omitted,
785
+ then all records up to the latest will be searched.
786
+ order: Specify the sort order of the response.
787
+ order_by: Name of column to sort the results by.
788
+ verbose: Whether or not to include the root hash of the tree and the
789
+ membership proof for each record.
790
+
791
+ Raises:
792
+ AuditException: If an audit based api exception happens
793
+ PangeaAPIException: If an API Error happens
794
+
795
+ Examples:
796
+ export_res = audit.export(verbose=False)
797
+
798
+ # Export may take several dozens of minutes, so polling for the result
799
+ # should be done in a loop. That is omitted here for brevity's sake.
800
+ try:
801
+ audit.poll_result(request_id=export_res.request_id)
802
+ except AcceptedRequestException:
803
+ # Retry later.
804
+
805
+ # Download the result when it's ready.
806
+ download_res = audit.download_results(request_id=export_res.request_id)
807
+ download_res.result.dest_url
808
+ # => https://pangea-runtime.s3.amazonaws.com/audit/xxxxx/search_results_[...]
809
+ """
810
+ input = ExportRequest(
811
+ format=format,
812
+ start=start,
813
+ end=end,
814
+ order=order,
815
+ order_by=order_by,
816
+ verbose=verbose,
817
+ )
818
+ try:
819
+ return self.request.post(
820
+ "v1/export", PangeaResponseResult, data=input.model_dump(exclude_none=True), poll_result=False
821
+ )
822
+ except pexc.AcceptedRequestException as e:
823
+ return e.response
824
+
825
+ def log_stream(self, data: dict) -> PangeaResponse[PangeaResponseResult]:
826
+ """
827
+ Log streaming endpoint
828
+
829
+ This API allows 3rd party vendors (like Auth0) to stream events to this
830
+ endpoint where the structure of the payload varies across different
831
+ vendors.
832
+
833
+ OperationId: audit_post_v1_log_stream
834
+
835
+ Args:
836
+ data: Event data. The exact schema of this will vary by vendor.
837
+
838
+ Raises:
839
+ AuditException: If an audit based api exception happens
840
+ PangeaAPIException: If an API Error happens
841
+
842
+ Examples:
843
+ data = {
844
+ "logs": [
845
+ {
846
+ "log_id": "some log ID",
847
+ "data": {
848
+ "date": "2024-03-29T17:26:50.193Z",
849
+ "type": "sapi",
850
+ "description": "Create a log stream",
851
+ "client_id": "some client ID",
852
+ "ip": "127.0.0.1",
853
+ "user_agent": "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0",
854
+ "user_id": "some user ID",
855
+ },
856
+ }
857
+ # ...
858
+ ]
859
+ }
860
+
861
+ response = audit.log_stream(data)
862
+ """
863
+ return self.request.post("v1/log_stream", PangeaResponseResult, data=data)
864
+
727
865
  def root(self, tree_size: Optional[int] = None) -> PangeaResponse[RootResult]:
728
866
  """
729
867
  Tamperproof verification
@@ -746,10 +884,14 @@ class Audit(ServiceBase, AuditBase):
746
884
  response = audit.root(tree_size=7)
747
885
  """
748
886
  input = RootRequest(tree_size=tree_size)
749
- return self.request.post("v1/root", RootResult, data=input.dict(exclude_none=True))
887
+ return self.request.post("v1/root", RootResult, data=input.model_dump(exclude_none=True))
750
888
 
751
889
  def download_results(
752
- self, result_id: str, format: Optional[DownloadFormat] = None
890
+ self,
891
+ result_id: Optional[str] = None,
892
+ format: DownloadFormat = DownloadFormat.CSV,
893
+ request_id: Optional[str] = None,
894
+ return_context: Optional[bool] = None,
753
895
  ) -> PangeaResponse[DownloadResult]:
754
896
  """
755
897
  Download search results
@@ -761,6 +903,8 @@ class Audit(ServiceBase, AuditBase):
761
903
  Args:
762
904
  result_id: ID returned by the search API.
763
905
  format: Format for the records.
906
+ request_id: ID returned by the export API.
907
+ return_context (bool): Return the context data needed to decrypt secure audit events that have been redacted with format preserving encryption.
764
908
 
765
909
  Returns:
766
910
  URL where search results can be downloaded.
@@ -776,8 +920,13 @@ class Audit(ServiceBase, AuditBase):
776
920
  )
777
921
  """
778
922
 
779
- input = DownloadRequest(result_id=result_id, format=format)
780
- return self.request.post("v1/download_results", DownloadResult, data=input.dict(exclude_none=True))
923
+ if request_id is None and result_id is None:
924
+ raise ValueError("must pass one of `request_id` or `result_id`")
925
+
926
+ input = DownloadRequest(
927
+ request_id=request_id, result_id=result_id, format=format, return_context=return_context
928
+ )
929
+ return self.request.post("v1/download_results", DownloadResult, data=input.model_dump(exclude_none=True))
781
930
 
782
931
  def update_published_roots(self, result: SearchResultOutput):
783
932
  """Fetches series of published root hashes from Arweave
@@ -802,12 +951,31 @@ class Audit(ServiceBase, AuditBase):
802
951
  for tree_size in tree_sizes:
803
952
  pub_root = None
804
953
  if tree_size in arweave_roots:
805
- pub_root = PublishedRoot(**arweave_roots[tree_size].dict(exclude_none=True))
954
+ pub_root = PublishedRoot(**arweave_roots[tree_size].model_dump(exclude_none=True))
806
955
  pub_root.source = RootSource.ARWEAVE
807
956
  elif self.allow_server_roots:
808
957
  resp = self.root(tree_size=tree_size)
809
958
  if resp.success and resp.result is not None:
810
- pub_root = PublishedRoot(**resp.result.data.dict(exclude_none=True))
959
+ pub_root = PublishedRoot(**resp.result.data.model_dump(exclude_none=True))
811
960
  pub_root.source = RootSource.PANGEA
812
961
  if pub_root is not None:
813
962
  self.pub_roots[tree_size] = pub_root
963
+
964
+ self.fix_consistency_proofs(tree_sizes)
965
+
966
+ def fix_consistency_proofs(self, tree_sizes: Iterable[int]):
967
+ # on very rare occasions, the consistency proof in Arweave may be wrong
968
+ # override it with the proof from pangea (not the root hash, just the proof)
969
+ for tree_size in tree_sizes:
970
+ if tree_size not in self.pub_roots or tree_size - 1 not in self.pub_roots:
971
+ continue
972
+
973
+ if self.pub_roots[tree_size].source == RootSource.PANGEA:
974
+ continue
975
+
976
+ if self.verify_consistency_proof(tree_size):
977
+ continue
978
+
979
+ resp = self.root(tree_size=tree_size)
980
+ if resp.success and resp.result is not None and resp.result.data is not None:
981
+ self.pub_roots[tree_size].consistency_proof = resp.result.data.consistency_proof
@@ -1,10 +1,12 @@
1
1
  # Copyright 2022 Pangea Cyber Corporation
2
2
  # Author: Pangea Cyber Corporation
3
+ from __future__ import annotations
4
+
3
5
  import datetime
4
6
  import enum
5
- from typing import Any, Dict, List, Optional, Union
7
+ from typing import Any, Dict, List, Optional, Sequence, Union
6
8
 
7
- from pangea.response import APIRequestModel, APIResponseModel, PangeaResponseResult
9
+ from pangea.response import APIRequestModel, APIResponseModel, PangeaDateTime, PangeaResponseResult
8
10
 
9
11
 
10
12
  class EventVerification(str, enum.Enum):
@@ -19,14 +21,14 @@ class EventVerification(str, enum.Enum):
19
21
  return str(self.value)
20
22
 
21
23
 
22
- class Event(dict):
24
+ class Event(Dict[str, Any]):
23
25
  """
24
26
  Event to perform an auditable activity
25
27
 
26
28
  Auxiliary class to be compatible with older SDKs
27
29
  """
28
30
 
29
- def __init__(self, **data):
31
+ def __init__(self, **data) -> None:
30
32
  super().__init__(**data)
31
33
 
32
34
  @property
@@ -124,7 +126,7 @@ class EventEnvelope(APIResponseModel):
124
126
  event: Dict[str, Any]
125
127
  signature: Optional[str] = None
126
128
  public_key: Optional[str] = None
127
- received_at: datetime.datetime
129
+ received_at: PangeaDateTime
128
130
 
129
131
 
130
132
  class LogRequest(APIRequestModel):
@@ -269,6 +271,7 @@ class SearchRequest(APIRequestModel):
269
271
  max_results -- Maximum number of results to return.
270
272
  search_restriction -- A list of keys to restrict the search results to. Useful for partitioning data available to the query string.
271
273
  verbose -- If true, include root, membership and consistency proofs in response.
274
+ return_context -- Return the context data needed to decrypt secure audit events that have been redacted with format preserving encryption.
272
275
  """
273
276
 
274
277
  query: str
@@ -279,8 +282,9 @@ class SearchRequest(APIRequestModel):
279
282
  end: Optional[str] = None
280
283
  limit: Optional[int] = None
281
284
  max_results: Optional[int] = None
282
- search_restriction: Optional[dict] = None
285
+ search_restriction: Optional[Dict[str, Sequence[str]]] = None
283
286
  verbose: Optional[bool] = None
287
+ return_context: Optional[bool] = None
284
288
 
285
289
 
286
290
  class RootRequest(APIRequestModel):
@@ -361,6 +365,7 @@ class SearchEvent(APIResponseModel):
361
365
  consistency_verification -- Consistency verification calculated if required.
362
366
  membership_verification -- Membership verification calculated if required.
363
367
  signature_verification -- Signature verification calculated if required.
368
+ fpe_context -- The context data needed to decrypt secure audit events that have been redacted with format preserving encryption.
364
369
  """
365
370
 
366
371
  envelope: EventEnvelope
@@ -371,6 +376,7 @@ class SearchEvent(APIResponseModel):
371
376
  consistency_verification: EventVerification = EventVerification.NONE
372
377
  membership_verification: EventVerification = EventVerification.NONE
373
378
  signature_verification: EventVerification = EventVerification.NONE
379
+ fpe_context: Optional[str] = None
374
380
 
375
381
 
376
382
  class SearchResultOutput(PangeaResponseResult):
@@ -404,7 +410,7 @@ class SearchOutput(SearchResultOutput):
404
410
  """
405
411
 
406
412
  id: str
407
- expires_at: datetime.datetime
413
+ expires_at: PangeaDateTime
408
414
 
409
415
 
410
416
  class SearchResultRequest(APIRequestModel):
@@ -415,11 +421,15 @@ class SearchResultRequest(APIRequestModel):
415
421
  id -- A search results identifier returned by the search call.
416
422
  limit -- Number of audit records to include from the first page of the results.
417
423
  offset -- Offset from the start of the result set to start returning results from.
424
+ assert_search_restriction -- Assert the requested search results were queried with the exact same search restrictions, to ensure the results comply to the expected restrictions.
425
+ return_context -- Return the context data needed to decrypt secure audit events that have been redacted with format preserving encryption.
418
426
  """
419
427
 
420
428
  id: str
421
429
  limit: Optional[int] = 20
422
430
  offset: Optional[int] = 0
431
+ assert_search_restriction: Optional[Dict[str, Sequence[str]]] = None
432
+ return_context: Optional[bool] = None
423
433
 
424
434
 
425
435
  class DownloadFormat(str, enum.Enum):
@@ -437,13 +447,51 @@ class DownloadFormat(str, enum.Enum):
437
447
 
438
448
 
439
449
  class DownloadRequest(APIRequestModel):
440
- result_id: str
450
+ request_id: Optional[str] = None
451
+ """ID returned by the export API."""
452
+
453
+ result_id: Optional[str] = None
441
454
  """ID returned by the search API."""
442
455
 
443
456
  format: Optional[str] = None
444
457
  """Format for the records."""
445
458
 
459
+ return_context: Optional[bool] = None
460
+ """Return the context data needed to decrypt secure audit events that have been redacted with format preserving encryption."""
461
+
446
462
 
447
463
  class DownloadResult(PangeaResponseResult):
448
464
  dest_url: str
449
465
  """URL where search results can be downloaded."""
466
+
467
+ expires_at: str
468
+ """
469
+ The time when the results will no longer be available to page through via
470
+ the results API.
471
+ """
472
+
473
+
474
+ class ExportRequest(APIRequestModel):
475
+ format: DownloadFormat = DownloadFormat.CSV
476
+ """Format for the records."""
477
+
478
+ start: Optional[datetime.datetime] = None
479
+ """The start of the time range to perform the search on."""
480
+
481
+ end: Optional[datetime.datetime] = None
482
+ """
483
+ The end of the time range to perform the search on. If omitted, then all
484
+ records up to the latest will be searched.
485
+ """
486
+
487
+ order_by: Optional[str] = None
488
+ """Name of column to sort the results by."""
489
+
490
+ order: Optional[SearchOrder] = None
491
+ """Specify the sort order of the response."""
492
+
493
+ verbose: bool = True
494
+ """
495
+ Whether or not to include the root hash of the tree and the membership proof
496
+ for each record.
497
+ """
@@ -7,7 +7,7 @@ from binascii import hexlify, unhexlify
7
7
  from dataclasses import dataclass
8
8
  from datetime import datetime
9
9
  from hashlib import sha256
10
- from typing import Dict, List, Optional
10
+ from typing import Collection, Dict, List, Optional
11
11
 
12
12
  import requests
13
13
 
@@ -61,7 +61,7 @@ def verify_hash(hash1: str, hash2: str) -> bool:
61
61
 
62
62
 
63
63
  def verify_envelope_hash(envelope: EventEnvelope, hash: str):
64
- return verify_hash(hash_dict(normalize_log(envelope.dict(exclude_none=True))), hash)
64
+ return verify_hash(hash_dict(normalize_log(envelope.model_dump(exclude_none=True))), hash)
65
65
 
66
66
 
67
67
  def canonicalize_event(event: Event) -> bytes:
@@ -192,7 +192,7 @@ def arweave_graphql_url():
192
192
  return f"{ARWEAVE_BASE_URL}/graphql"
193
193
 
194
194
 
195
- def get_arweave_published_roots(tree_name: str, tree_sizes: List[int]) -> Dict[int, PublishedRoot]:
195
+ def get_arweave_published_roots(tree_name: str, tree_sizes: Collection[int]) -> Dict[int, PublishedRoot]:
196
196
  if len(tree_sizes) == 0:
197
197
  return {}
198
198