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.
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
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import datetime
6
6
  import json
7
- from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Union
7
+ from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union
8
8
 
9
9
  import pangea.exceptions as pexc
10
10
  from pangea.config import PangeaConfig
@@ -199,7 +199,7 @@ class AuditBase:
199
199
 
200
200
  # verify consistency proofs
201
201
  if self.can_verify_consistency_proof(search_event):
202
- 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):
203
203
  search_event.consistency_verification = EventVerification.PASS
204
204
  else:
205
205
  search_event.consistency_verification = EventVerification.FAIL
@@ -222,7 +222,7 @@ class AuditBase:
222
222
  tree_sizes.difference_update(self.pub_roots.keys())
223
223
 
224
224
  if tree_sizes:
225
- 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)
226
226
  else:
227
227
  arweave_roots = {}
228
228
 
@@ -284,7 +284,7 @@ class AuditBase:
284
284
  """
285
285
  return event.published and event.leaf_index is not None and event.leaf_index >= 0 # type: ignore[return-value]
286
286
 
287
- def verify_consistency_proof(self, pub_roots: Dict[int, PublishedRoot], event: SearchEvent) -> bool:
287
+ def verify_consistency_proof(self, tree_size: int) -> bool:
288
288
  """
289
289
  Verify consistency proof
290
290
 
@@ -293,18 +293,17 @@ class AuditBase:
293
293
  Read more at: [What is a consistency proof?](https://pangea.cloud/docs/audit/merkle-trees#what-is-a-consistency-proof)
294
294
 
295
295
  Args:
296
- pub_roots (dict[int, Root]): list of published root hashes across time
297
- event (SearchEvent): Audit event to be verified.
296
+ leaf_index (int): The tree size of the root to be verified.
298
297
 
299
298
  Returns:
300
299
  bool: True if consistency proof is verified, False otherwise.
301
300
  """
302
301
 
303
- if event.leaf_index == 0:
302
+ if tree_size == 0:
304
303
  return True
305
304
 
306
- curr_root = pub_roots.get(event.leaf_index + 1) # type: ignore[operator]
307
- 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)
308
307
 
309
308
  if not curr_root or not prev_root:
310
309
  return False
@@ -314,9 +313,12 @@ class AuditBase:
314
313
  ):
315
314
  return False
316
315
 
316
+ if curr_root.consistency_proof is None:
317
+ return False
318
+
317
319
  curr_root_hash = decode_hash(curr_root.root_hash)
318
320
  prev_root_hash = decode_hash(prev_root.root_hash)
319
- proof = decode_consistency_proof(curr_root.consistency_proof) # type: ignore[arg-type]
321
+ proof = decode_consistency_proof(curr_root.consistency_proof)
320
322
 
321
323
  return verify_consistency_proof(curr_root_hash, prev_root_hash, proof)
322
324
 
@@ -490,7 +492,7 @@ class Audit(ServiceBase, AuditBase):
490
492
  verbose: Optional[bool] = None,
491
493
  ) -> PangeaResponse[LogResult]:
492
494
  """
493
- Log an entry
495
+ Log an event
494
496
 
495
497
  Create a log entry in the Secure Audit Log.
496
498
 
@@ -499,6 +501,7 @@ class Audit(ServiceBase, AuditBase):
499
501
  verify (bool, optional): True to verify logs consistency after response.
500
502
  sign_local (bool, optional): True to sign event with local key.
501
503
  verbose (bool, optional): True to get a more verbose response.
504
+
502
505
  Raises:
503
506
  AuditException: If an audit based api exception happens
504
507
  PangeaAPIException: If an API Error happens
@@ -509,17 +512,13 @@ class Audit(ServiceBase, AuditBase):
509
512
  Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#/v1/log).
510
513
 
511
514
  Examples:
512
- try:
513
- log_response = audit.log({"message": "hello world"}, verbose=True)
514
- print(f"Response. Hash: {log_response.result.hash}")
515
- except pe.PangeaAPIException as e:
516
- print(f"Request Error: {e.response.summary}")
517
- for err in e.errors:
518
- print(f"\\t{err.detail} \\n")
515
+ response = audit.log_event({"message": "hello world"}, verbose=True)
519
516
  """
520
517
 
521
518
  input = self._get_log_request(event, sign_local=sign_local, verify=verify, verbose=verbose)
522
- response: PangeaResponse[LogResult] = self.request.post("v1/log", LogResult, data=input.dict(exclude_none=True))
519
+ response: PangeaResponse[LogResult] = self.request.post(
520
+ "v1/log", LogResult, data=input.model_dump(exclude_none=True)
521
+ )
523
522
  if response.success and response.result is not None:
524
523
  self._process_log_result(response.result, verify=verify)
525
524
  return response
@@ -559,7 +558,7 @@ class Audit(ServiceBase, AuditBase):
559
558
 
560
559
  input = self._get_log_request(events, sign_local=sign_local, verify=False, verbose=verbose)
561
560
  response: PangeaResponse[LogBulkResult] = self.request.post(
562
- "v2/log", LogBulkResult, data=input.dict(exclude_none=True)
561
+ "v2/log", LogBulkResult, data=input.model_dump(exclude_none=True)
563
562
  )
564
563
 
565
564
  if response.success and response.result is not None:
@@ -603,7 +602,7 @@ class Audit(ServiceBase, AuditBase):
603
602
  try:
604
603
  # Calling to v2 methods will return always a 202.
605
604
  response: PangeaResponse[LogBulkResult] = self.request.post(
606
- "v2/log_async", LogBulkResult, data=input.dict(exclude_none=True), poll_result=False
605
+ "v2/log_async", LogBulkResult, data=input.model_dump(exclude_none=True), poll_result=False
607
606
  )
608
607
  except pexc.AcceptedRequestException as e:
609
608
  return e.response
@@ -626,6 +625,7 @@ class Audit(ServiceBase, AuditBase):
626
625
  verbose: Optional[bool] = None,
627
626
  verify_consistency: bool = False,
628
627
  verify_events: bool = True,
628
+ return_context: Optional[bool] = None,
629
629
  ) -> PangeaResponse[SearchOutput]:
630
630
  """
631
631
  Search the log
@@ -655,6 +655,7 @@ class Audit(ServiceBase, AuditBase):
655
655
  verbose (bool, optional): If true, response include root and membership and consistency proofs.
656
656
  verify_consistency (bool): True to verify logs consistency
657
657
  verify_events (bool): True to verify hash events and signatures
658
+ return_context (bool): Return the context data needed to decrypt secure audit events that have been redacted with format preserving encryption.
658
659
 
659
660
  Raises:
660
661
  AuditException: If an audit based api exception happens
@@ -688,10 +689,11 @@ class Audit(ServiceBase, AuditBase):
688
689
  max_results=max_results,
689
690
  search_restriction=search_restriction,
690
691
  verbose=verbose,
692
+ return_context=return_context,
691
693
  )
692
694
 
693
695
  response: PangeaResponse[SearchOutput] = self.request.post(
694
- "v1/search", SearchOutput, data=input.dict(exclude_none=True)
696
+ "v1/search", SearchOutput, data=input.model_dump(exclude_none=True)
695
697
  )
696
698
  if verify_consistency and response.result is not None:
697
699
  self.update_published_roots(response.result)
@@ -705,6 +707,7 @@ class Audit(ServiceBase, AuditBase):
705
707
  assert_search_restriction: Optional[Dict[str, Sequence[str]]] = None,
706
708
  verify_consistency: bool = False,
707
709
  verify_events: bool = True,
710
+ return_context: Optional[bool] = None,
708
711
  ) -> PangeaResponse[SearchResultOutput]:
709
712
  """
710
713
  Results of a search
@@ -720,6 +723,7 @@ class Audit(ServiceBase, AuditBase):
720
723
  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.
721
724
  verify_consistency (bool): True to verify logs consistency
722
725
  verify_events (bool): True to verify hash events and signatures
726
+ return_context (bool): Return the context data needed to decrypt secure audit events that have been redacted with format preserving encryption.
723
727
  Raises:
724
728
  AuditException: If an audit based api exception happens
725
729
  PangeaAPIException: If an API Error happens
@@ -743,9 +747,10 @@ class Audit(ServiceBase, AuditBase):
743
747
  limit=limit,
744
748
  offset=offset,
745
749
  assert_search_restriction=assert_search_restriction,
750
+ return_context=return_context,
746
751
  )
747
752
  response: PangeaResponse[SearchResultOutput] = self.request.post(
748
- "v1/results", SearchResultOutput, data=input.dict(exclude_none=True)
753
+ "v1/results", SearchResultOutput, data=input.model_dump(exclude_none=True)
749
754
  )
750
755
  if verify_consistency and response.result is not None:
751
756
  self.update_published_roots(response.result)
@@ -807,7 +812,7 @@ class Audit(ServiceBase, AuditBase):
807
812
  )
808
813
  try:
809
814
  return self.request.post(
810
- "v1/export", PangeaResponseResult, data=input.dict(exclude_none=True), poll_result=False
815
+ "v1/export", PangeaResponseResult, data=input.model_dump(exclude_none=True), poll_result=False
811
816
  )
812
817
  except pexc.AcceptedRequestException as e:
813
818
  return e.response
@@ -874,13 +879,14 @@ class Audit(ServiceBase, AuditBase):
874
879
  response = audit.root(tree_size=7)
875
880
  """
876
881
  input = RootRequest(tree_size=tree_size)
877
- return self.request.post("v1/root", RootResult, data=input.dict(exclude_none=True))
882
+ return self.request.post("v1/root", RootResult, data=input.model_dump(exclude_none=True))
878
883
 
879
884
  def download_results(
880
885
  self,
881
886
  result_id: Optional[str] = None,
882
887
  format: DownloadFormat = DownloadFormat.CSV,
883
888
  request_id: Optional[str] = None,
889
+ return_context: Optional[bool] = None,
884
890
  ) -> PangeaResponse[DownloadResult]:
885
891
  """
886
892
  Download search results
@@ -893,6 +899,7 @@ class Audit(ServiceBase, AuditBase):
893
899
  result_id: ID returned by the search API.
894
900
  format: Format for the records.
895
901
  request_id: ID returned by the export API.
902
+ return_context (bool): Return the context data needed to decrypt secure audit events that have been redacted with format preserving encryption.
896
903
 
897
904
  Returns:
898
905
  URL where search results can be downloaded.
@@ -911,8 +918,10 @@ class Audit(ServiceBase, AuditBase):
911
918
  if request_id is None and result_id is None:
912
919
  raise ValueError("must pass one of `request_id` or `result_id`")
913
920
 
914
- input = DownloadRequest(request_id=request_id, result_id=result_id, format=format)
915
- return self.request.post("v1/download_results", DownloadResult, data=input.dict(exclude_none=True))
921
+ input = DownloadRequest(
922
+ request_id=request_id, result_id=result_id, format=format, return_context=return_context
923
+ )
924
+ return self.request.post("v1/download_results", DownloadResult, data=input.model_dump(exclude_none=True))
916
925
 
917
926
  def update_published_roots(self, result: SearchResultOutput):
918
927
  """Fetches series of published root hashes from Arweave
@@ -937,12 +946,31 @@ class Audit(ServiceBase, AuditBase):
937
946
  for tree_size in tree_sizes:
938
947
  pub_root = None
939
948
  if tree_size in arweave_roots:
940
- pub_root = PublishedRoot(**arweave_roots[tree_size].dict(exclude_none=True))
949
+ pub_root = PublishedRoot(**arweave_roots[tree_size].model_dump(exclude_none=True))
941
950
  pub_root.source = RootSource.ARWEAVE
942
951
  elif self.allow_server_roots:
943
952
  resp = self.root(tree_size=tree_size)
944
953
  if resp.success and resp.result is not None:
945
- pub_root = PublishedRoot(**resp.result.data.dict(exclude_none=True))
954
+ pub_root = PublishedRoot(**resp.result.data.model_dump(exclude_none=True))
946
955
  pub_root.source = RootSource.PANGEA
947
956
  if pub_root is not None:
948
957
  self.pub_roots[tree_size] = pub_root
958
+
959
+ self._fix_consistency_proofs(tree_sizes)
960
+
961
+ def _fix_consistency_proofs(self, tree_sizes: Iterable[int]) -> None:
962
+ # on very rare occasions, the consistency proof in Arweave may be wrong
963
+ # override it with the proof from pangea (not the root hash, just the proof)
964
+ for tree_size in tree_sizes:
965
+ if tree_size not in self.pub_roots or tree_size - 1 not in self.pub_roots:
966
+ continue
967
+
968
+ if self.pub_roots[tree_size].source == RootSource.PANGEA:
969
+ continue
970
+
971
+ if self.verify_consistency_proof(tree_size):
972
+ continue
973
+
974
+ resp = self.root(tree_size=tree_size)
975
+ if resp.success and resp.result is not None and resp.result.data is not None:
976
+ self.pub_roots[tree_size].consistency_proof = resp.result.data.consistency_proof
@@ -6,7 +6,7 @@ import datetime
6
6
  import enum
7
7
  from typing import Any, Dict, List, Optional, Sequence, Union
8
8
 
9
- from pangea.response import APIRequestModel, APIResponseModel, PangeaResponseResult
9
+ from pangea.response import APIRequestModel, APIResponseModel, PangeaDateTime, PangeaResponseResult
10
10
 
11
11
 
12
12
  class EventVerification(str, enum.Enum):
@@ -126,7 +126,7 @@ class EventEnvelope(APIResponseModel):
126
126
  event: Dict[str, Any]
127
127
  signature: Optional[str] = None
128
128
  public_key: Optional[str] = None
129
- received_at: datetime.datetime
129
+ received_at: PangeaDateTime
130
130
 
131
131
 
132
132
  class LogRequest(APIRequestModel):
@@ -271,6 +271,7 @@ class SearchRequest(APIRequestModel):
271
271
  max_results -- Maximum number of results to return.
272
272
  search_restriction -- A list of keys to restrict the search results to. Useful for partitioning data available to the query string.
273
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.
274
275
  """
275
276
 
276
277
  query: str
@@ -283,6 +284,7 @@ class SearchRequest(APIRequestModel):
283
284
  max_results: Optional[int] = None
284
285
  search_restriction: Optional[Dict[str, Sequence[str]]] = None
285
286
  verbose: Optional[bool] = None
287
+ return_context: Optional[bool] = None
286
288
 
287
289
 
288
290
  class RootRequest(APIRequestModel):
@@ -363,6 +365,7 @@ class SearchEvent(APIResponseModel):
363
365
  consistency_verification -- Consistency verification calculated if required.
364
366
  membership_verification -- Membership verification calculated if required.
365
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.
366
369
  """
367
370
 
368
371
  envelope: EventEnvelope
@@ -373,6 +376,7 @@ class SearchEvent(APIResponseModel):
373
376
  consistency_verification: EventVerification = EventVerification.NONE
374
377
  membership_verification: EventVerification = EventVerification.NONE
375
378
  signature_verification: EventVerification = EventVerification.NONE
379
+ fpe_context: Optional[str] = None
376
380
 
377
381
 
378
382
  class SearchResultOutput(PangeaResponseResult):
@@ -406,7 +410,7 @@ class SearchOutput(SearchResultOutput):
406
410
  """
407
411
 
408
412
  id: str
409
- expires_at: datetime.datetime
413
+ expires_at: PangeaDateTime
410
414
 
411
415
 
412
416
  class SearchResultRequest(APIRequestModel):
@@ -418,12 +422,14 @@ class SearchResultRequest(APIRequestModel):
418
422
  limit -- Number of audit records to include from the first page of the results.
419
423
  offset -- Offset from the start of the result set to start returning results from.
420
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.
421
426
  """
422
427
 
423
428
  id: str
424
429
  limit: Optional[int] = 20
425
430
  offset: Optional[int] = 0
426
431
  assert_search_restriction: Optional[Dict[str, Sequence[str]]] = None
432
+ return_context: Optional[bool] = None
427
433
 
428
434
 
429
435
  class DownloadFormat(str, enum.Enum):
@@ -450,6 +456,9 @@ class DownloadRequest(APIRequestModel):
450
456
  format: Optional[str] = None
451
457
  """Format for the records."""
452
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
+
453
462
 
454
463
  class DownloadResult(PangeaResponseResult):
455
464
  dest_url: str
@@ -1,5 +1,7 @@
1
1
  # Copyright 2022 Pangea Cyber Corporation
2
2
  # Author: Pangea Cyber Corporation
3
+ from __future__ import annotations
4
+
3
5
  from abc import ABC, abstractmethod
4
6
  from typing import Optional
5
7
 
@@ -10,7 +12,7 @@ from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes, Pub
10
12
 
11
13
  from pangea.exceptions import PangeaException
12
14
  from pangea.services.audit.util import b64decode, b64decode_ascii, b64encode_ascii
13
- from pangea.services.vault.models.common import AsymmetricAlgorithm
15
+ from pangea.services.vault.models.asymmetric import AsymmetricKeySigningAlgorithm
14
16
 
15
17
 
16
18
  class AlgorithmSigner(ABC):
@@ -43,7 +45,7 @@ class ED25519Signer(AlgorithmSigner):
43
45
  )
44
46
 
45
47
  def get_algorithm(self) -> str:
46
- return AsymmetricAlgorithm.Ed25519.value
48
+ return AsymmetricKeySigningAlgorithm.ED25519.value
47
49
 
48
50
 
49
51
  signers = {
@@ -96,7 +98,7 @@ class Signer:
96
98
 
97
99
  for func in (serialization.load_pem_private_key, serialization.load_ssh_private_key):
98
100
  try:
99
- return func(private_key, None) # type: ignore[operator]
101
+ return func(private_key, None)
100
102
  except exceptions.UnsupportedAlgorithm as e:
101
103
  raise e
102
104
  except ValueError:
@@ -144,8 +146,7 @@ class Verifier:
144
146
  for cls, verifier in verifiers.items():
145
147
  if isinstance(pubkey, cls):
146
148
  return verifier(pubkey).verify(message_bytes, signature_bytes)
147
- else:
148
- raise PangeaException(f"Not supported public key type: {type(pubkey)}")
149
+ raise PangeaException(f"Not supported public key type: {type(pubkey)}")
149
150
 
150
151
  def _decode_public_key(self, public_key: bytes):
151
152
  """Parse a public key in PEM or ssh format"""
@@ -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