pangea-sdk 4.2.0__tar.gz → 4.4.0__tar.gz

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 (52) hide show
  1. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/PKG-INFO +5 -5
  2. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/__init__.py +2 -1
  3. pangea_sdk-4.4.0/pangea/asyncio/__init__.py +1 -0
  4. pangea_sdk-4.4.0/pangea/asyncio/file_uploader.py +39 -0
  5. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/asyncio/request.py +11 -10
  6. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/asyncio/services/__init__.py +1 -0
  7. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/asyncio/services/authz.py +13 -7
  8. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/asyncio/services/file_scan.py +21 -6
  9. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/asyncio/services/intel.py +3 -0
  10. pangea_sdk-4.4.0/pangea/asyncio/services/sanitize.py +193 -0
  11. pangea_sdk-4.4.0/pangea/file_uploader.py +35 -0
  12. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/request.py +1 -1
  13. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/response.py +15 -1
  14. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/__init__.py +1 -0
  15. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/audit/signing.py +1 -1
  16. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/authz.py +14 -6
  17. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/file_scan.py +30 -13
  18. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/intel.py +5 -0
  19. pangea_sdk-4.4.0/pangea/services/sanitize.py +366 -0
  20. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pyproject.toml +6 -7
  21. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/README.md +0 -0
  22. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/asyncio/services/audit.py +0 -0
  23. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/asyncio/services/authn.py +0 -0
  24. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/asyncio/services/base.py +0 -0
  25. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/asyncio/services/embargo.py +0 -0
  26. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/asyncio/services/redact.py +0 -0
  27. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/asyncio/services/vault.py +0 -0
  28. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/audit_logger.py +0 -0
  29. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/config.py +0 -0
  30. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/crypto/rsa.py +0 -0
  31. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/deep_verify.py +0 -0
  32. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/deprecated.py +0 -0
  33. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/dump_audit.py +0 -0
  34. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/exceptions.py +0 -0
  35. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/py.typed +0 -0
  36. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/audit/audit.py +0 -0
  37. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/audit/exceptions.py +0 -0
  38. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/audit/models.py +0 -0
  39. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/audit/util.py +0 -0
  40. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/authn/authn.py +0 -0
  41. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/authn/models.py +0 -0
  42. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/base.py +0 -0
  43. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/embargo.py +0 -0
  44. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/redact.py +0 -0
  45. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/vault/models/asymmetric.py +0 -0
  46. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/vault/models/common.py +0 -0
  47. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/vault/models/secret.py +0 -0
  48. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/vault/models/symmetric.py +0 -0
  49. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/services/vault/vault.py +0 -0
  50. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/tools.py +0 -0
  51. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/utils.py +0 -0
  52. {pangea_sdk-4.2.0 → pangea_sdk-4.4.0}/pangea/verify_audit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pangea-sdk
3
- Version: 4.2.0
3
+ Version: 4.4.0
4
4
  Summary: Pangea API SDK
5
5
  Home-page: https://pangea.cloud/docs/sdk/python/
6
6
  License: MIT
@@ -15,14 +15,14 @@ Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: Topic :: Software Development
19
20
  Classifier: Topic :: Software Development :: Libraries
20
- Requires-Dist: aiohttp (>=3.9.3,<4.0.0)
21
- Requires-Dist: asyncio (>=3.4.3,<4.0.0)
22
- Requires-Dist: cryptography (>=42.0.8,<43.0.0)
21
+ Requires-Dist: aiohttp (>=3.10.10,<4.0.0)
22
+ Requires-Dist: cryptography (>=43.0.1,<44.0.0)
23
23
  Requires-Dist: deprecated (>=1.2.14,<2.0.0)
24
24
  Requires-Dist: google-crc32c (>=1.5.0,<2.0.0)
25
- Requires-Dist: pydantic (>=2.8.2,<3.0.0)
25
+ Requires-Dist: pydantic (>=2.9.2,<3.0.0)
26
26
  Requires-Dist: python-dateutil (>=2.9.0,<3.0.0)
27
27
  Requires-Dist: requests (>=2.31.0,<3.0.0)
28
28
  Requires-Dist: requests-toolbelt (>=1.0.0,<2.0.0)
@@ -1,6 +1,7 @@
1
- __version__ = "4.2.0"
1
+ __version__ = "4.4.0"
2
2
 
3
3
  from pangea.asyncio.request import PangeaRequestAsync
4
4
  from pangea.config import PangeaConfig
5
+ from pangea.file_uploader import FileUploader
5
6
  from pangea.request import PangeaRequest
6
7
  from pangea.response import PangeaResponse
@@ -0,0 +1 @@
1
+ from .file_uploader import FileUploaderAsync
@@ -0,0 +1,39 @@
1
+ # Copyright 2022 Pangea Cyber Corporation
2
+ # Author: Pangea Cyber Corporation
3
+ import io
4
+ import logging
5
+ from typing import Dict, Optional
6
+
7
+ from pangea.asyncio.request import PangeaRequestAsync
8
+ from pangea.request import PangeaConfig
9
+ from pangea.response import TransferMethod
10
+
11
+
12
+ class FileUploaderAsync:
13
+ def __init__(self) -> None:
14
+ self.logger = logging.getLogger("pangea")
15
+ self._request: PangeaRequestAsync = PangeaRequestAsync(
16
+ config=PangeaConfig(),
17
+ token="",
18
+ service="FileUploader",
19
+ logger=self.logger,
20
+ )
21
+
22
+ async def upload_file(
23
+ self,
24
+ url: str,
25
+ file: io.BufferedReader,
26
+ transfer_method: TransferMethod = TransferMethod.PUT_URL,
27
+ file_details: Optional[Dict] = None,
28
+ ) -> None:
29
+ if transfer_method == TransferMethod.PUT_URL:
30
+ files = [("file", ("filename", file, "application/octet-stream"))]
31
+ await self._request.put_presigned_url(url=url, files=files)
32
+ elif transfer_method == TransferMethod.POST_URL:
33
+ files = [("file", ("filename", file, "application/octet-stream"))]
34
+ await self._request.post_presigned_url(url=url, data=file_details, files=files) # type: ignore[arg-type]
35
+ else:
36
+ raise ValueError(f"Transfer method not supported: {transfer_method}")
37
+
38
+ async def close(self) -> None:
39
+ await self._request.session.close()
@@ -1,10 +1,11 @@
1
1
  # Copyright 2022 Pangea Cyber Corporation
2
2
  # Author: Pangea Cyber Corporation
3
+ from __future__ import annotations
3
4
 
4
5
  import asyncio
5
6
  import json
6
7
  import time
7
- from typing import Dict, List, Optional, Sequence, Tuple, Type, Union
8
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
8
9
 
9
10
  import aiohttp
10
11
  from aiohttp import FormData
@@ -32,7 +33,7 @@ class PangeaRequestAsync(PangeaRequestBase):
32
33
  endpoint: str,
33
34
  result_class: Type[TResult],
34
35
  data: Union[str, Dict] = {},
35
- files: List[Tuple] = [],
36
+ files: Optional[List[Tuple]] = None,
36
37
  poll_result: bool = True,
37
38
  url: Optional[str] = None,
38
39
  ) -> PangeaResponse[TResult]:
@@ -211,13 +212,14 @@ class PangeaRequestAsync(PangeaRequestBase):
211
212
  else:
212
213
  raise pe.DownloadFileError(f"Failed to download file. Status: {response.status}", await response.text())
213
214
 
214
- async def _get_pangea_json(self, reader: aiohttp.MultipartReader) -> Optional[Dict]:
215
+ async def _get_pangea_json(self, reader: aiohttp.multipart.MultipartResponseWrapper) -> Optional[Dict[str, Any]]:
215
216
  # Iterate through parts
216
217
  async for part in reader:
217
- return await part.json()
218
+ if isinstance(part, aiohttp.BodyPartReader):
219
+ return await part.json()
218
220
  return None
219
221
 
220
- async def _get_attached_files(self, reader: aiohttp.MultipartReader) -> List[AttachedFile]:
222
+ async def _get_attached_files(self, reader: aiohttp.multipart.MultipartResponseWrapper) -> List[AttachedFile]:
221
223
  files = []
222
224
  i = 0
223
225
 
@@ -228,7 +230,7 @@ class PangeaRequestAsync(PangeaRequestBase):
228
230
  if name is None:
229
231
  name = f"default_file_name_{i}"
230
232
  i += 1
231
- files.append(AttachedFile(name, await part.read(), content_type))
233
+ files.append(AttachedFile(name, await part.read(), content_type)) # type: ignore[union-attr]
232
234
 
233
235
  return files
234
236
 
@@ -236,13 +238,12 @@ class PangeaRequestAsync(PangeaRequestBase):
236
238
  # Parse the multipart response
237
239
  multipart_reader = aiohttp.MultipartReader.from_response(resp)
238
240
 
239
- pangea_json = await self._get_pangea_json(multipart_reader) # type: ignore[arg-type]
241
+ pangea_json = await self._get_pangea_json(multipart_reader)
240
242
  self.logger.debug(
241
243
  json.dumps({"service": self.service, "action": "multipart response", "response": pangea_json})
242
244
  )
243
245
 
244
- multipart_reader = multipart_reader.__aiter__()
245
- attached_files = await self._get_attached_files(multipart_reader) # type: ignore[arg-type]
246
+ attached_files = await self._get_attached_files(multipart_reader)
246
247
  return MultipartResponse(pangea_json, attached_files) # type: ignore[arg-type]
247
248
 
248
249
  async def _http_post(
@@ -250,7 +251,7 @@ class PangeaRequestAsync(PangeaRequestBase):
250
251
  url: str,
251
252
  headers: Dict = {},
252
253
  data: Union[str, Dict] = {},
253
- files: List[Tuple] = [],
254
+ files: Optional[List[Tuple]] = [],
254
255
  presigned_url_post: bool = False,
255
256
  ) -> aiohttp.ClientResponse:
256
257
  if files:
@@ -5,4 +5,5 @@ from .embargo import EmbargoAsync
5
5
  from .file_scan import FileScanAsync
6
6
  from .intel import DomainIntelAsync, FileIntelAsync, IpIntelAsync, UrlIntelAsync, UserIntelAsync
7
7
  from .redact import RedactAsync
8
+ from .sanitize import SanitizeAsync
8
9
  from .vault import VaultAsync
@@ -1,7 +1,7 @@
1
1
  # Copyright 2022 Pangea Cyber Corporation
2
2
  # Author: Pangea Cyber Corporation
3
3
 
4
- from typing import Dict, List, Optional, Union
4
+ from typing import Any, Dict, List, Optional
5
5
 
6
6
  from pangea.asyncio.services.base import ServiceBaseAsync
7
7
  from pangea.response import PangeaResponse
@@ -162,7 +162,7 @@ class AuthZAsync(ServiceBaseAsync):
162
162
  action: str,
163
163
  subject: Subject,
164
164
  debug: Optional[bool] = None,
165
- attributes: Optional[Dict[str, Union[int, str]]] = None,
165
+ attributes: Optional[Dict[str, Any]] = None,
166
166
  ) -> PangeaResponse[CheckResult]:
167
167
  """Perform a check request.
168
168
 
@@ -173,7 +173,7 @@ class AuthZAsync(ServiceBaseAsync):
173
173
  action (str): The action to check.
174
174
  subject (Subject): The subject to check.
175
175
  debug (Optional[bool]): Setting this value to True will provide a detailed analysis of the check.
176
- attributes (Optional[Dict[str, Union[int, str]]]): Additional attributes for the check.
176
+ attributes (Optional[Dict[str, Any]]): Additional attributes for the check.
177
177
 
178
178
  Raises:
179
179
  PangeaAPIException: If an API Error happens.
@@ -195,7 +195,9 @@ class AuthZAsync(ServiceBaseAsync):
195
195
  input_data = CheckRequest(resource=resource, action=action, subject=subject, debug=debug, attributes=attributes)
196
196
  return await self.request.post("v1/check", CheckResult, data=input_data.model_dump(exclude_none=True))
197
197
 
198
- async def list_resources(self, type: str, action: str, subject: Subject) -> PangeaResponse[ListResourcesResult]:
198
+ async def list_resources(
199
+ self, type: str, action: str, subject: Subject, attributes: Optional[Dict[str, Any]] = None
200
+ ) -> PangeaResponse[ListResourcesResult]:
199
201
  """List resources.
200
202
 
201
203
  Given a type, action, and subject, list all the resources in the
@@ -205,6 +207,7 @@ class AuthZAsync(ServiceBaseAsync):
205
207
  type (str): The type to filter resources.
206
208
  action (str): The action to filter resources.
207
209
  subject (Subject): The subject to filter resources.
210
+ attributes (Optional[Dict[str, Any]]): A JSON object of attribute data.
208
211
 
209
212
  Raises:
210
213
  PangeaAPIException: If an API Error happens.
@@ -222,12 +225,14 @@ class AuthZAsync(ServiceBaseAsync):
222
225
  )
223
226
  """
224
227
 
225
- input_data = ListResourcesRequest(type=type, action=action, subject=subject)
228
+ input_data = ListResourcesRequest(type=type, action=action, subject=subject, attributes=attributes)
226
229
  return await self.request.post(
227
230
  "v1/list-resources", ListResourcesResult, data=input_data.model_dump(exclude_none=True)
228
231
  )
229
232
 
230
- async def list_subjects(self, resource: Resource, action: str) -> PangeaResponse[ListSubjectsResult]:
233
+ async def list_subjects(
234
+ self, resource: Resource, action: str, attributes: Optional[Dict[str, Any]] = None
235
+ ) -> PangeaResponse[ListSubjectsResult]:
231
236
  """List subjects.
232
237
 
233
238
  Given a resource and an action, return the list of subjects who have
@@ -236,6 +241,7 @@ class AuthZAsync(ServiceBaseAsync):
236
241
  Args:
237
242
  resource (Resource): The resource to filter subjects.
238
243
  action (str): The action to filter subjects.
244
+ attributes (Optional[Dict[str, Any]]): A JSON object of attribute data.
239
245
 
240
246
  Raises:
241
247
  PangeaAPIException: If an API Error happens.
@@ -252,7 +258,7 @@ class AuthZAsync(ServiceBaseAsync):
252
258
  )
253
259
  """
254
260
 
255
- input_data = ListSubjectsRequest(resource=resource, action=action)
261
+ input_data = ListSubjectsRequest(resource=resource, action=action, attributes=attributes)
256
262
  return await self.request.post(
257
263
  "v1/list-subjects", ListSubjectsResult, data=input_data.model_dump(exclude_none=True)
258
264
  )
@@ -59,12 +59,14 @@ class FileScanAsync(ServiceBaseAsync):
59
59
  OperationId: file_scan_post_v1_scan
60
60
 
61
61
  Args:
62
- file (io.BufferedReader, optional): file to be scanned (should be opened with read permissions and in binary format)
63
62
  file_path (str, optional): filepath to be opened and scanned
63
+ file (io.BufferedReader, optional): file to be scanned (should be opened with read permissions and in binary format)
64
64
  verbose (bool, optional): Echo the API parameters in the response
65
65
  raw (bool, optional): Include raw data from this provider
66
66
  provider (str, optional): Scan file using this provider
67
67
  sync_call (bool, optional): True to wait until server returns a result, False to return immediately and retrieve result asynchronously
68
+ transfer_method (TransferMethod, optional): Transfer method used to upload the file data.
69
+ source_url (str, optional): A URL where the Pangea APIs can fetch the contents of the input file.
68
70
 
69
71
  Raises:
70
72
  PangeaAPIException: If an API Error happens
@@ -77,7 +79,7 @@ class FileScanAsync(ServiceBaseAsync):
77
79
  Examples:
78
80
  try:
79
81
  with open("./path/to/file.pdf", "rb") as f:
80
- response = client.file_scan(file=f, verbose=True, provider="crowdstrike")
82
+ response = await client.file_scan(file=f, verbose=True, provider="crowdstrike")
81
83
  print(f"Response: {response.result}")
82
84
  except pe.PangeaAPIException as e:
83
85
  print(f"Request Error: {e.response.summary}")
@@ -85,6 +87,15 @@ class FileScanAsync(ServiceBaseAsync):
85
87
  print(f"\\t{err.detail} \\n")
86
88
  """
87
89
 
90
+ if transfer_method == TransferMethod.SOURCE_URL and source_url is None:
91
+ raise ValueError("`source_url` argument is required when using `TransferMethod.SOURCE_URL`.")
92
+
93
+ if source_url is not None and transfer_method != TransferMethod.SOURCE_URL:
94
+ raise ValueError(
95
+ "`transfer_method` should be `TransferMethod.SOURCE_URL` when using the `source_url` argument."
96
+ )
97
+
98
+ files: Optional[List[Tuple]] = None
88
99
  if file or file_path:
89
100
  if file_path:
90
101
  file = open(file_path, "rb")
@@ -95,9 +106,9 @@ class FileScanAsync(ServiceBaseAsync):
95
106
  size = params.size
96
107
  else:
97
108
  crc, sha, size = None, None, None
98
- files: List[Tuple] = [("upload", ("filename", file, "application/octet-stream"))]
99
- else:
100
- raise ValueError("Need to set file_path or file arguments")
109
+ files = [("upload", ("filename", file, "application/octet-stream"))]
110
+ elif source_url is None:
111
+ raise ValueError("Need to set one of `file_path`, `file`, or `source_url` arguments.")
101
112
 
102
113
  input = m.FileScanRequest(
103
114
  verbose=verbose,
@@ -110,7 +121,11 @@ class FileScanAsync(ServiceBaseAsync):
110
121
  source_url=source_url,
111
122
  )
112
123
  data = input.model_dump(exclude_none=True)
113
- return await self.request.post("v1/scan", m.FileScanResult, data=data, files=files, poll_result=sync_call)
124
+ try:
125
+ return await self.request.post("v1/scan", m.FileScanResult, data=data, files=files, poll_result=sync_call)
126
+ finally:
127
+ if file_path and file:
128
+ file.close()
114
129
 
115
130
  async def request_upload_url(
116
131
  self,
@@ -846,6 +846,7 @@ class UserIntelAsync(ServiceBaseAsync):
846
846
  usernames: Optional[List[str]] = None,
847
847
  ips: Optional[List[str]] = None,
848
848
  phone_numbers: Optional[List[str]] = None,
849
+ domains: Optional[List[str]] = None,
849
850
  start: Optional[str] = None,
850
851
  end: Optional[str] = None,
851
852
  verbose: Optional[bool] = None,
@@ -864,6 +865,7 @@ class UserIntelAsync(ServiceBaseAsync):
864
865
  usernames (List[str]): An username's list to search for
865
866
  ips (List[str]): An ip's list to search for
866
867
  phone_numbers (List[str]): A phone number's list to search for. minLength: 7, maxLength: 15.
868
+ domains (List[str]): Search for user under these domains.
867
869
  start (str): Earliest date for search
868
870
  end (str): Latest date for search
869
871
  verbose (bool, optional): Echo the API parameters in the response
@@ -886,6 +888,7 @@ class UserIntelAsync(ServiceBaseAsync):
886
888
  phone_numbers=phone_numbers,
887
889
  usernames=usernames,
888
890
  ips=ips,
891
+ domains=domains,
889
892
  provider=provider,
890
893
  start=start,
891
894
  end=end,
@@ -0,0 +1,193 @@
1
+ # Copyright 2022 Pangea Cyber Corporation
2
+ # Author: Pangea Cyber Corporation
3
+ import io
4
+ from typing import List, Optional, Tuple
5
+
6
+ import pangea.services.sanitize as m
7
+ from pangea.asyncio.services.base import ServiceBaseAsync
8
+ from pangea.response import PangeaResponse, TransferMethod
9
+ from pangea.utils import FileUploadParams, get_file_upload_params
10
+
11
+
12
+ class SanitizeAsync(ServiceBaseAsync):
13
+ """Sanitize service client.
14
+
15
+ Examples:
16
+ import os
17
+
18
+ # Pangea SDK
19
+ from pangea.config import PangeaConfig
20
+ from pangea.asyncio.services import Sanitize
21
+
22
+ PANGEA_SANITIZE_TOKEN = os.getenv("PANGEA_SANITIZE_TOKEN")
23
+ config = PangeaConfig(domain="pangea.cloud")
24
+
25
+ sanitize = Sanitize(token=PANGEA_SANITIZE_TOKEN, config=config)
26
+ """
27
+
28
+ service_name = "sanitize"
29
+
30
+ async def sanitize(
31
+ self,
32
+ transfer_method: TransferMethod = TransferMethod.POST_URL,
33
+ file_path: Optional[str] = None,
34
+ file: Optional[io.BufferedReader] = None,
35
+ source_url: Optional[str] = None,
36
+ share_id: Optional[str] = None,
37
+ file_scan: Optional[m.SanitizeFile] = None,
38
+ content: Optional[m.SanitizeContent] = None,
39
+ share_output: Optional[m.SanitizeShareOutput] = None,
40
+ size: Optional[int] = None,
41
+ crc32c: Optional[str] = None,
42
+ sha256: Optional[str] = None,
43
+ uploaded_file_name: Optional[str] = None,
44
+ sync_call: bool = True,
45
+ ) -> PangeaResponse[m.SanitizeResult]:
46
+ """
47
+ Sanitize
48
+
49
+ Apply file sanitization actions according to specified rules.
50
+
51
+ OperationId: sanitize_post_v1_sanitize
52
+
53
+ Args:
54
+ transfer_method: The transfer method used to upload the file data.
55
+ file_path: Path to file to sanitize.
56
+ file: File to sanitize.
57
+ source_url: A URL where the file to be sanitized can be downloaded.
58
+ share_id: A Pangea Secure Share ID where the file to be sanitized is stored.
59
+ file_scan: Options for File Scan.
60
+ content: Options for how the file should be sanitized.
61
+ share_output: Integration with Secure Share.
62
+ size: The size (in bytes) of the file. If the upload doesn't match, the call will fail.
63
+ crc32c: The CRC32C hash of the file data, which will be verified by the server if provided.
64
+ sha256: The hexadecimal-encoded SHA256 hash of the file data, which will be verified by the server if provided.
65
+ uploaded_file_name: Name of the user-uploaded file, required for `TransferMethod.PUT_URL` and `TransferMethod.POST_URL`.
66
+ sync_call: Whether or not to poll on HTTP/202.
67
+
68
+ Raises:
69
+ PangeaAPIException: If an API error happens.
70
+
71
+ Returns:
72
+ The sanitized file and information on the sanitization that was
73
+ performed.
74
+
75
+ Examples:
76
+ with open("/path/to/file.pdf", "rb") as f:
77
+ response = await sanitize.sanitize(
78
+ file=f,
79
+ transfer_method=TransferMethod.POST_URL,
80
+ uploaded_file_name="uploaded_file",
81
+ )
82
+ """
83
+
84
+ if transfer_method == TransferMethod.SOURCE_URL and source_url is None:
85
+ raise ValueError("`source_url` argument is required when using `TransferMethod.SOURCE_URL`.")
86
+
87
+ if source_url is not None and transfer_method != TransferMethod.SOURCE_URL:
88
+ raise ValueError(
89
+ "`transfer_method` should be `TransferMethod.SOURCE_URL` when using the `source_url` argument."
90
+ )
91
+
92
+ files: Optional[List[Tuple]] = None
93
+ if file or file_path:
94
+ if file_path:
95
+ file = open(file_path, "rb")
96
+ if transfer_method == TransferMethod.POST_URL and (sha256 is None or crc32c is None or size is None):
97
+ params = get_file_upload_params(file) # type: ignore[arg-type]
98
+ crc32c = params.crc_hex if crc32c is None else crc32c
99
+ sha256 = params.sha256_hex if sha256 is None else sha256
100
+ size = params.size if size is None else size
101
+ else:
102
+ crc32c, sha256, size = None, None, None
103
+ files = [("upload", ("filename", file, "application/octet-stream"))]
104
+ elif source_url is None:
105
+ raise ValueError("Need to set one of `file_path`, `file`, or `source_url` arguments.")
106
+
107
+ input = m.SanitizeRequest(
108
+ transfer_method=transfer_method,
109
+ source_url=source_url,
110
+ share_id=share_id,
111
+ file=file_scan,
112
+ content=content,
113
+ share_output=share_output,
114
+ crc32c=crc32c,
115
+ sha256=sha256,
116
+ size=size,
117
+ uploaded_file_name=uploaded_file_name,
118
+ )
119
+ data = input.model_dump(exclude_none=True)
120
+ try:
121
+ return await self.request.post(
122
+ "v1/sanitize", m.SanitizeResult, data=data, files=files, poll_result=sync_call
123
+ )
124
+ finally:
125
+ if file_path and file is not None:
126
+ file.close()
127
+
128
+ async def request_upload_url(
129
+ self,
130
+ transfer_method: TransferMethod = TransferMethod.PUT_URL,
131
+ params: Optional[FileUploadParams] = None,
132
+ file_scan: Optional[m.SanitizeFile] = None,
133
+ content: Optional[m.SanitizeContent] = None,
134
+ share_output: Optional[m.SanitizeShareOutput] = None,
135
+ size: Optional[int] = None,
136
+ crc32c: Optional[str] = None,
137
+ sha256: Optional[str] = None,
138
+ uploaded_file_name: Optional[str] = None,
139
+ ) -> PangeaResponse[m.SanitizeResult]:
140
+ """
141
+ Sanitize via presigned URL
142
+
143
+ Apply file sanitization actions according to specified rules via a
144
+ [presigned URL](https://pangea.cloud/docs/api/transfer-methods).
145
+
146
+ OperationId: sanitize_post_v1_sanitize 2
147
+
148
+ Args:
149
+ transfer_method: The transfer method used to upload the file data.
150
+ params: File upload parameters.
151
+ file_scan: Options for File Scan.
152
+ content: Options for how the file should be sanitized.
153
+ share_output: Integration with Secure Share.
154
+ size: The size (in bytes) of the file. If the upload doesn't match, the call will fail.
155
+ crc32c: The CRC32C hash of the file data, which will be verified by the server if provided.
156
+ sha256: The hexadecimal-encoded SHA256 hash of the file data, which will be verified by the server if provided.
157
+ uploaded_file_name: Name of the user-uploaded file, required for `TransferMethod.PUT_URL` and `TransferMethod.POST_URL`.
158
+
159
+ Raises:
160
+ PangeaAPIException: If an API error happens.
161
+
162
+ Returns:
163
+ A presigned URL.
164
+
165
+ Examples:
166
+ presignedUrl = await sanitize.request_upload_url(
167
+ transfer_method=TransferMethod.PUT_URL,
168
+ uploaded_file_name="uploaded_file",
169
+ )
170
+
171
+ # Upload file to `presignedUrl.accepted_result.put_url`.
172
+
173
+ # Poll for Sanitize's result.
174
+ response: PangeaResponse[SanitizeResult] = await sanitize.poll_result(response=presignedUrl)
175
+ """
176
+
177
+ input = m.SanitizeRequest(
178
+ transfer_method=transfer_method,
179
+ file=file_scan,
180
+ content=content,
181
+ share_output=share_output,
182
+ crc32c=crc32c,
183
+ sha256=sha256,
184
+ size=size,
185
+ uploaded_file_name=uploaded_file_name,
186
+ )
187
+ if params is not None and (transfer_method == TransferMethod.POST_URL):
188
+ input.crc32c = params.crc_hex
189
+ input.sha256 = params.sha256_hex
190
+ input.size = params.size
191
+
192
+ data = input.model_dump(exclude_none=True)
193
+ return await self.request.request_presigned_url("v1/sanitize", m.SanitizeResult, data=data)
@@ -0,0 +1,35 @@
1
+ # Copyright 2022 Pangea Cyber Corporation
2
+ # Author: Pangea Cyber Corporation
3
+ import io
4
+ import logging
5
+ from typing import Dict, Optional
6
+
7
+ from pangea.request import PangeaConfig, PangeaRequest
8
+ from pangea.response import TransferMethod
9
+
10
+
11
+ class FileUploader:
12
+ def __init__(self):
13
+ self.logger = logging.getLogger("pangea")
14
+ self._request = PangeaRequest(
15
+ config=PangeaConfig(),
16
+ token="",
17
+ service="FileUploader",
18
+ logger=self.logger,
19
+ )
20
+
21
+ def upload_file(
22
+ self,
23
+ url: str,
24
+ file: io.BufferedReader,
25
+ transfer_method: TransferMethod = TransferMethod.PUT_URL,
26
+ file_details: Optional[Dict] = None,
27
+ ):
28
+ if transfer_method == TransferMethod.PUT_URL:
29
+ files = [("file", ("filename", file, "application/octet-stream"))]
30
+ self._request.put_presigned_url(url=url, files=files)
31
+ elif transfer_method == TransferMethod.POST_URL:
32
+ files = [("file", ("filename", file, "application/octet-stream"))]
33
+ self._request.post_presigned_url(url=url, data=file_details, files=files)
34
+ else:
35
+ raise ValueError(f"Transfer method not supported: {transfer_method}")
@@ -182,7 +182,7 @@ class PangeaRequestBase(object):
182
182
  elif status == ResponseStatus.VAULT_ITEM_NOT_FOUND.value:
183
183
  raise pe.VaultItemNotFound(summary, response)
184
184
  elif status == ResponseStatus.NOT_FOUND.value:
185
- raise pe.NotFound(str(response.raw_response.url) if response.raw_response is not None else "", response) # type: ignore[arg-type]
185
+ raise pe.NotFound(str(response.raw_response.url) if response.raw_response is not None else "", response)
186
186
  elif status == ResponseStatus.INTERNAL_SERVER_ERROR.value:
187
187
  raise pe.InternalServerError(response)
188
188
  elif status == ResponseStatus.ACCEPTED.value:
@@ -37,10 +37,24 @@ class AttachedFile(object):
37
37
 
38
38
 
39
39
  class TransferMethod(str, enum.Enum):
40
+ """Transfer methods for uploading file data."""
41
+
40
42
  MULTIPART = "multipart"
41
43
  POST_URL = "post-url"
42
44
  PUT_URL = "put-url"
43
45
  SOURCE_URL = "source-url"
46
+ """
47
+ A `source-url` is a caller-specified URL where the Pangea APIs can fetch the
48
+ contents of the input file. When calling a Pangea API with a
49
+ `transfer_method` of `source-url`, you must also specify a `source_url`
50
+ input parameter that provides a URL to the input file. The source URL can be
51
+ a presigned URL created by the caller, and it will be used to download the
52
+ content of the input file. The `source-url` transfer method is useful when
53
+ you already have a file in your storage and can provide a URL from which
54
+ Pangea API can fetch the input file—there is no need to transfer it to
55
+ Pangea with a separate POST or PUT request.
56
+ """
57
+
44
58
  DEST_URL = "dest-url"
45
59
 
46
60
  def __str__(self):
@@ -222,4 +236,4 @@ class PangeaResponse(ResponseHeader, Generic[T]):
222
236
 
223
237
  @property
224
238
  def url(self) -> str:
225
- return str(self.raw_response.url) # type: ignore[arg-type,union-attr]
239
+ return str(self.raw_response.url) # type: ignore[union-attr]
@@ -5,4 +5,5 @@ 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
8
9
  from .vault.vault import Vault
@@ -96,7 +96,7 @@ class Signer:
96
96
 
97
97
  for func in (serialization.load_pem_private_key, serialization.load_ssh_private_key):
98
98
  try:
99
- return func(private_key, None) # type: ignore[operator]
99
+ return func(private_key, None)
100
100
  except exceptions.UnsupportedAlgorithm as e:
101
101
  raise e
102
102
  except ValueError: