pangea-sdk 4.4.0__py3-none-any.whl → 5.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pangea/crypto/rsa.py CHANGED
@@ -1,7 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
4
+ from typing import TYPE_CHECKING
5
+
6
+ from cryptography.hazmat.backends import default_backend
3
7
  from cryptography.hazmat.primitives import hashes, serialization
4
8
  from cryptography.hazmat.primitives.asymmetric import padding, rsa
9
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
10
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
11
+
12
+ from pangea.services.vault.models.common import ExportEncryptionAlgorithm
13
+ from pangea.services.vault.models.symmetric import SymmetricKeyEncryptionAlgorithm
14
+
15
+ if TYPE_CHECKING:
16
+ from pangea.services.vault.models.common import ExportResult
5
17
 
6
18
 
7
19
  def generate_key_pair() -> tuple[rsa.RSAPrivateKey, rsa.RSAPublicKey]:
@@ -45,3 +57,79 @@ def public_key_to_pem(public_key: rsa.RSAPublicKey) -> bytes:
45
57
  return public_key.public_bytes(
46
58
  encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo
47
59
  )
60
+
61
+
62
+ _AES_GCM_IV_SIZE = 12
63
+ """Standard nonce size for GCM."""
64
+
65
+ _KEY_LENGTH = 32
66
+ """AES-256 key length in bytes."""
67
+
68
+
69
+ def kem_decrypt(
70
+ private_key: rsa.RSAPrivateKey,
71
+ iv: bytes,
72
+ ciphertext: bytes,
73
+ symmetric_algorithm: str,
74
+ asymmetric_algorithm: str,
75
+ encrypted_salt: bytes,
76
+ password: str,
77
+ iteration_count: int,
78
+ hash_algorithm: str,
79
+ ) -> str:
80
+ if symmetric_algorithm.casefold() != SymmetricKeyEncryptionAlgorithm.AES_GCM_256.value.casefold():
81
+ raise NotImplementedError(f"Unsupported symmetric algorithm: {symmetric_algorithm}")
82
+
83
+ if asymmetric_algorithm != ExportEncryptionAlgorithm.RSA_NO_PADDING_4096_KEM:
84
+ raise NotImplementedError(f"Unsupported asymmetric algorithm: {asymmetric_algorithm}")
85
+
86
+ if hash_algorithm.casefold() != "SHA512".casefold():
87
+ raise NotImplementedError(f"Unsupported hash algorithm: {hash_algorithm}")
88
+
89
+ # No-padding RSA decryption.
90
+ n = private_key.private_numbers().public_numbers.n
91
+ salt = pow(
92
+ int.from_bytes(encrypted_salt, byteorder="big"),
93
+ private_key.private_numbers().d,
94
+ n,
95
+ ).to_bytes(n.bit_length() // 8, byteorder="big")
96
+
97
+ kdf = PBKDF2HMAC(
98
+ algorithm=hashes.SHA512(), length=_KEY_LENGTH, salt=salt, iterations=iteration_count, backend=default_backend()
99
+ )
100
+ symmetric_key = kdf.derive(password.encode("utf-8"))
101
+
102
+ decrypted = AESGCM(symmetric_key).decrypt(nonce=iv, data=ciphertext, associated_data=None)
103
+
104
+ return decrypted.decode("ascii")
105
+
106
+
107
+ def kem_decrypt_export_result(*, result: ExportResult, password: str, private_key: rsa.RSAPrivateKey) -> str:
108
+ """Decrypt the exported result of a KEM operation."""
109
+ cipher_encoded = result.private_key or result.key
110
+ if not cipher_encoded:
111
+ raise TypeError("`private_key` or `key` should be set.")
112
+
113
+ assert result.encrypted_salt
114
+ assert result.symmetric_algorithm
115
+ assert result.asymmetric_algorithm
116
+ assert result.iteration_count
117
+ assert result.hash_algorithm
118
+
119
+ cipher_with_iv = base64.b64decode(cipher_encoded)
120
+ encrypted_salt = base64.b64decode(result.encrypted_salt)
121
+
122
+ iv = cipher_with_iv[:_AES_GCM_IV_SIZE]
123
+ cipher = cipher_with_iv[_AES_GCM_IV_SIZE:]
124
+
125
+ return kem_decrypt(
126
+ private_key=private_key,
127
+ iv=iv,
128
+ ciphertext=cipher,
129
+ password=password,
130
+ encrypted_salt=encrypted_salt,
131
+ symmetric_algorithm=result.symmetric_algorithm,
132
+ asymmetric_algorithm=result.asymmetric_algorithm,
133
+ iteration_count=result.iteration_count,
134
+ hash_algorithm=result.hash_algorithm,
135
+ )
pangea/request.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # Copyright 2022 Pangea Cyber Corporation
2
2
  # Author: Pangea Cyber Corporation
3
+ from __future__ import annotations
3
4
 
4
5
  import copy
5
6
  import json
@@ -8,6 +9,7 @@ import time
8
9
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, Type, Union
9
10
 
10
11
  import requests
12
+ from pydantic import BaseModel
11
13
  from requests.adapters import HTTPAdapter, Retry
12
14
  from requests_toolbelt import MultipartDecoder # type: ignore
13
15
  from typing_extensions import TypeVar
@@ -22,7 +24,7 @@ if TYPE_CHECKING:
22
24
  import aiohttp
23
25
 
24
26
 
25
- class MultipartResponse(object):
27
+ class MultipartResponse:
26
28
  pangea_json: Dict[str, str]
27
29
  attached_files: List = []
28
30
 
@@ -31,7 +33,7 @@ class MultipartResponse(object):
31
33
  self.attached_files = attached_files
32
34
 
33
35
 
34
- class PangeaRequestBase(object):
36
+ class PangeaRequestBase:
35
37
  def __init__(
36
38
  self, config: PangeaConfig, token: str, service: str, logger: logging.Logger, config_id: Optional[str] = None
37
39
  ):
@@ -129,8 +131,7 @@ class PangeaRequestBase(object):
129
131
  filename_parts = content_disposition.split("name=")
130
132
  if len(filename_parts) > 1:
131
133
  return filename_parts[1].split(";")[0].strip('"')
132
- else:
133
- return None
134
+ return None
134
135
 
135
136
  def _get_filename_from_url(self, url: str) -> Optional[str]:
136
137
  return url.split("/")[-1].split("?")[0]
@@ -157,35 +158,35 @@ class PangeaRequestBase(object):
157
158
 
158
159
  if status == ResponseStatus.VALIDATION_ERR.value:
159
160
  raise pe.ValidationException(summary, response)
160
- elif status == ResponseStatus.TOO_MANY_REQUESTS.value:
161
+ if status == ResponseStatus.TOO_MANY_REQUESTS.value:
161
162
  raise pe.RateLimitException(summary, response)
162
- elif status == ResponseStatus.NO_CREDIT.value:
163
+ if status == ResponseStatus.NO_CREDIT.value:
163
164
  raise pe.NoCreditException(summary, response)
164
- elif status == ResponseStatus.UNAUTHORIZED.value:
165
+ if status == ResponseStatus.UNAUTHORIZED.value:
165
166
  raise pe.UnauthorizedException(self.service, response)
166
- elif status == ResponseStatus.SERVICE_NOT_ENABLED.value:
167
+ if status == ResponseStatus.SERVICE_NOT_ENABLED.value:
167
168
  raise pe.ServiceNotEnabledException(self.service, response)
168
- elif status == ResponseStatus.PROVIDER_ERR.value:
169
+ if status == ResponseStatus.PROVIDER_ERR.value:
169
170
  raise pe.ProviderErrorException(summary, response)
170
- elif status in (ResponseStatus.MISSING_CONFIG_ID_SCOPE.value, ResponseStatus.MISSING_CONFIG_ID.value):
171
+ if status in (ResponseStatus.MISSING_CONFIG_ID_SCOPE.value, ResponseStatus.MISSING_CONFIG_ID.value):
171
172
  raise pe.MissingConfigID(self.service, response)
172
- elif status == ResponseStatus.SERVICE_NOT_AVAILABLE.value:
173
+ if status == ResponseStatus.SERVICE_NOT_AVAILABLE.value:
173
174
  raise pe.ServiceNotAvailableException(summary, response)
174
- elif status == ResponseStatus.TREE_NOT_FOUND.value:
175
+ if status == ResponseStatus.TREE_NOT_FOUND.value:
175
176
  raise pe.TreeNotFoundException(summary, response)
176
- elif status == ResponseStatus.IP_NOT_FOUND.value:
177
+ if status == ResponseStatus.IP_NOT_FOUND.value:
177
178
  raise pe.IPNotFoundException(summary, response)
178
- elif status == ResponseStatus.BAD_OFFSET.value:
179
+ if status == ResponseStatus.BAD_OFFSET.value:
179
180
  raise pe.BadOffsetException(summary, response)
180
- elif status == ResponseStatus.FORBIDDEN_VAULT_OPERATION.value:
181
+ if status == ResponseStatus.FORBIDDEN_VAULT_OPERATION.value:
181
182
  raise pe.ForbiddenVaultOperation(summary, response)
182
- elif status == ResponseStatus.VAULT_ITEM_NOT_FOUND.value:
183
+ if status == ResponseStatus.VAULT_ITEM_NOT_FOUND.value:
183
184
  raise pe.VaultItemNotFound(summary, response)
184
- elif status == ResponseStatus.NOT_FOUND.value:
185
+ if status == ResponseStatus.NOT_FOUND.value:
185
186
  raise pe.NotFound(str(response.raw_response.url) if response.raw_response is not None else "", response)
186
- elif status == ResponseStatus.INTERNAL_SERVER_ERROR.value:
187
+ if status == ResponseStatus.INTERNAL_SERVER_ERROR.value:
187
188
  raise pe.InternalServerError(response)
188
- elif status == ResponseStatus.ACCEPTED.value:
189
+ if status == ResponseStatus.ACCEPTED.value:
189
190
  raise pe.AcceptedRequestException(response)
190
191
  raise pe.PangeaAPIException(f"{summary} ", response)
191
192
 
@@ -209,7 +210,7 @@ class PangeaRequest(PangeaRequestBase):
209
210
  self,
210
211
  endpoint: str,
211
212
  result_class: Type[TResult],
212
- data: Union[str, Dict] = {},
213
+ data: str | BaseModel | dict[str, Any] | None = None,
213
214
  files: Optional[List[Tuple]] = None,
214
215
  poll_result: bool = True,
215
216
  url: Optional[str] = None,
@@ -224,6 +225,13 @@ class PangeaRequest(PangeaRequestBase):
224
225
  PangeaResponse which contains the response in its entirety and
225
226
  various properties to retrieve individual fields
226
227
  """
228
+
229
+ if isinstance(data, BaseModel):
230
+ data = data.model_dump(exclude_none=True)
231
+
232
+ if data is None:
233
+ data = {}
234
+
227
235
  if url is None:
228
236
  url = self._url(endpoint)
229
237
 
@@ -336,19 +344,17 @@ class PangeaRequest(PangeaRequestBase):
336
344
  multi.extend(files)
337
345
  files = multi
338
346
  return None, files
339
- else:
340
- # Post to presigned url as form
341
- data_send: list = [] # type: ignore[no-redef]
342
- for k, v in data.items(): # type: ignore[union-attr]
343
- data_send.append((k, v)) # type: ignore[attr-defined]
344
- # When posting to presigned url, file key should be 'file'
345
- files = { # type: ignore[assignment]
346
- "file": files[0][1],
347
- }
348
- return data_send, files
349
- else:
350
- data_send = json.dumps(data, default=default_encoder) if isinstance(data, dict) else data
351
- return data_send, None
347
+ # Post to presigned url as form
348
+ data_send: list = [] # type: ignore[no-redef]
349
+ for k, v in data.items(): # type: ignore[union-attr]
350
+ data_send.append((k, v)) # type: ignore[attr-defined]
351
+ # When posting to presigned url, file key should be 'file'
352
+ files = { # type: ignore[assignment]
353
+ "file": files[0][1],
354
+ }
355
+ return data_send, files
356
+ data_send = json.dumps(data, default=default_encoder) if isinstance(data, dict) else data
357
+ return data_send, None
352
358
 
353
359
  return data, files
354
360
 
@@ -432,8 +438,7 @@ class PangeaRequest(PangeaRequestBase):
432
438
  )
433
439
  )
434
440
  return AttachedFile(filename=filename, file=response.content, content_type=content_type)
435
- else:
436
- raise pe.DownloadFileError(f"Failed to download file. Status: {response.status_code}", response.text)
441
+ raise pe.DownloadFileError(f"Failed to download file. Status: {response.status_code}", response.text)
437
442
 
438
443
  def poll_result_by_id(
439
444
  self, request_id: str, result_class: Type[TResult], check_response: bool = True
@@ -462,10 +467,8 @@ class PangeaRequest(PangeaRequestBase):
462
467
  ) -> PangeaResponse:
463
468
  # Send request
464
469
  try:
465
- # This should return 202 (AcceptedRequestException)
466
- resp = self.post(endpoint=endpoint, result_class=result_class, data=data, poll_result=False)
467
- raise pe.PresignedURLException("Should return 202", resp)
468
-
470
+ # This should return 202 (AcceptedRequestException) at least zero size file is sent
471
+ return self.post(endpoint=endpoint, result_class=result_class, data=data, poll_result=False)
469
472
  except pe.AcceptedRequestException as e:
470
473
  accepted_exception = e
471
474
  except Exception as e:
@@ -523,6 +526,9 @@ class PangeaRequest(PangeaRequestBase):
523
526
  raise AttributeError("files attribute should have at least 1 file")
524
527
 
525
528
  response = self.request_presigned_url(endpoint=endpoint, result_class=result_class, data=data)
529
+
530
+ if response.success: # This should only happen when uploading a zero bytes file
531
+ return response.raw_response
526
532
  if response.accepted_result is None:
527
533
  raise pe.PangeaException("No accepted_result field when requesting presigned url")
528
534
  if response.accepted_result.post_url is None:
@@ -589,8 +595,7 @@ class PangeaRequest(PangeaRequestBase):
589
595
 
590
596
  if loop_resp.accepted_result is not None and not loop_resp.accepted_result.has_upload_url:
591
597
  return loop_resp
592
- else:
593
- raise loop_exc
598
+ raise loop_exc
594
599
 
595
600
  def _init_session(self) -> requests.Session:
596
601
  retry_config = Retry(
pangea/response.py CHANGED
@@ -28,6 +28,7 @@ class AttachedFile(object):
28
28
  filename = self.filename if self.filename else "default_save_filename"
29
29
 
30
30
  filepath = os.path.join(dest_folder, filename)
31
+ filepath = self._find_available_file(filepath)
31
32
  directory = os.path.dirname(filepath)
32
33
  if not os.path.exists(directory):
33
34
  os.makedirs(directory)
@@ -35,6 +36,17 @@ class AttachedFile(object):
35
36
  with open(filepath, "wb") as file:
36
37
  file.write(self.file)
37
38
 
39
+ def _find_available_file(self, file_path):
40
+ base_name, ext = os.path.splitext(file_path)
41
+ counter = 1
42
+ while os.path.exists(file_path):
43
+ if ext:
44
+ file_path = f"{base_name}_{counter}{ext}"
45
+ else:
46
+ file_path = f"{base_name}_{counter}"
47
+ counter += 1
48
+ return file_path
49
+
38
50
 
39
51
  class TransferMethod(str, enum.Enum):
40
52
  """Transfer methods for uploading file data."""
@@ -6,4 +6,5 @@ from .file_scan import FileScan
6
6
  from .intel import DomainIntel, FileIntel, IpIntel, UrlIntel, UserIntel
7
7
  from .redact import Redact
8
8
  from .sanitize import Sanitize
9
+ from .share.share import Share
9
10
  from .vault.vault import Vault
@@ -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 = {
@@ -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"""
@@ -0,0 +1,170 @@
1
+ import enum
2
+
3
+
4
+ class FileFormat(str, enum.Enum):
5
+ F3G2 = "3G2"
6
+ F3GP = "3GP"
7
+ F3MF = "3MF"
8
+ F7Z = "7Z"
9
+ A = "A"
10
+ AAC = "AAC"
11
+ ACCDB = "ACCDB"
12
+ AIFF = "AIFF"
13
+ AMF = "AMF"
14
+ AMR = "AMR"
15
+ APE = "APE"
16
+ ASF = "ASF"
17
+ ATOM = "ATOM"
18
+ AU = "AU"
19
+ AVI = "AVI"
20
+ AVIF = "AVIF"
21
+ BIN = "BIN"
22
+ BMP = "BMP"
23
+ BPG = "BPG"
24
+ BZ2 = "BZ2"
25
+ CAB = "CAB"
26
+ CLASS = "CLASS"
27
+ CPIO = "CPIO"
28
+ CRX = "CRX"
29
+ CSV = "CSV"
30
+ DAE = "DAE"
31
+ DBF = "DBF"
32
+ DCM = "DCM"
33
+ DEB = "DEB"
34
+ DJVU = "DJVU"
35
+ DLL = "DLL"
36
+ DOC = "DOC"
37
+ DOCX = "DOCX"
38
+ DWG = "DWG"
39
+ EOT = "EOT"
40
+ EPUB = "EPUB"
41
+ EXE = "EXE"
42
+ FDF = "FDF"
43
+ FITS = "FITS"
44
+ FLAC = "FLAC"
45
+ FLV = "FLV"
46
+ GBR = "GBR"
47
+ GEOJSON = "GEOJSON"
48
+ GIF = "GIF"
49
+ GLB = "GLB"
50
+ GML = "GML"
51
+ GPX = "GPX"
52
+ GZ = "GZ"
53
+ HAR = "HAR"
54
+ HDR = "HDR"
55
+ HEIC = "HEIC"
56
+ HEIF = "HEIF"
57
+ HTML = "HTML"
58
+ ICNS = "ICNS"
59
+ ICO = "ICO"
60
+ ICS = "ICS"
61
+ ISO = "ISO"
62
+ JAR = "JAR"
63
+ JP2 = "JP2"
64
+ JPF = "JPF"
65
+ JPG = "JPG"
66
+ JPM = "JPM"
67
+ JS = "JS"
68
+ JSON = "JSON"
69
+ JXL = "JXL"
70
+ JXR = "JXR"
71
+ KML = "KML"
72
+ LIT = "LIT"
73
+ LNK = "LNK"
74
+ LUA = "LUA"
75
+ LZ = "LZ"
76
+ M3U = "M3U"
77
+ M4A = "M4A"
78
+ MACHO = "MACHO"
79
+ MDB = "MDB"
80
+ MIDI = "MIDI"
81
+ MKV = "MKV"
82
+ MOBI = "MOBI"
83
+ MOV = "MOV"
84
+ MP3 = "MP3"
85
+ MP4 = "MP4"
86
+ MPC = "MPC"
87
+ MPEG = "MPEG"
88
+ MQV = "MQV"
89
+ MRC = "MRC"
90
+ MSG = "MSG"
91
+ MSI = "MSI"
92
+ NDJSON = "NDJSON"
93
+ NES = "NES"
94
+ ODC = "ODC"
95
+ ODF = "ODF"
96
+ ODG = "ODG"
97
+ ODP = "ODP"
98
+ ODS = "ODS"
99
+ ODT = "ODT"
100
+ OGA = "OGA"
101
+ OGV = "OGV"
102
+ OTF = "OTF"
103
+ OTG = "OTG"
104
+ OTP = "OTP"
105
+ OTS = "OTS"
106
+ OTT = "OTT"
107
+ OWL = "OWL"
108
+ P7S = "P7S"
109
+ PAT = "PAT"
110
+ PDF = "PDF"
111
+ PHP = "PHP"
112
+ PL = "PL"
113
+ PNG = "PNG"
114
+ PPT = "PPT"
115
+ PPTX = "PPTX"
116
+ PS = "PS"
117
+ PSD = "PSD"
118
+ PUB = "PUB"
119
+ PY = "PY"
120
+ QCP = "QCP"
121
+ RAR = "RAR"
122
+ RMVB = "RMVB"
123
+ RPM = "RPM"
124
+ RSS = "RSS"
125
+ RTF = "RTF"
126
+ SHP = "SHP"
127
+ SHX = "SHX"
128
+ SO = "SO"
129
+ SQLITE = "SQLITE"
130
+ SRT = "SRT"
131
+ SVG = "SVG"
132
+ SWF = "SWF"
133
+ SXC = "SXC"
134
+ TAR = "TAR"
135
+ TCL = "TCL"
136
+ TCX = "TCX"
137
+ TIFF = "TIFF"
138
+ TORRENT = "TORRENT"
139
+ TSV = "TSV"
140
+ TTC = "TTC"
141
+ TTF = "TTF"
142
+ TXT = "TXT"
143
+ VCF = "VCF"
144
+ VOC = "VOC"
145
+ VTT = "VTT"
146
+ WARC = "WARC"
147
+ WASM = "WASM"
148
+ WAV = "WAV"
149
+ WEBM = "WEBM"
150
+ WEBP = "WEBP"
151
+ WOFF = "WOFF"
152
+ WOFF2 = "WOFF2"
153
+ X3D = "X3D"
154
+ XAR = "XAR"
155
+ XCF = "XCF"
156
+ XFDF = "XFDF"
157
+ XLF = "XLF"
158
+ XLS = "XLS"
159
+ XLSX = "XLSX"
160
+ XML = "XML"
161
+ XPM = "XPM"
162
+ XZ = "XZ"
163
+ ZIP = "ZIP"
164
+ ZST = "ZST"
165
+
166
+ def __str__(self):
167
+ return str(self.value)
168
+
169
+ def __repr__(self):
170
+ return str(self.value)