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
@@ -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