pangea-sdk 4.2.0__py3-none-any.whl → 4.4.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/__init__.py +2 -1
- pangea/asyncio/__init__.py +1 -0
- pangea/asyncio/file_uploader.py +39 -0
- pangea/asyncio/request.py +11 -10
- pangea/asyncio/services/__init__.py +1 -0
- pangea/asyncio/services/authz.py +13 -7
- pangea/asyncio/services/file_scan.py +21 -6
- pangea/asyncio/services/intel.py +3 -0
- pangea/asyncio/services/sanitize.py +193 -0
- pangea/file_uploader.py +35 -0
- pangea/request.py +1 -1
- pangea/response.py +15 -1
- pangea/services/__init__.py +1 -0
- pangea/services/audit/signing.py +1 -1
- pangea/services/authz.py +14 -6
- pangea/services/file_scan.py +30 -13
- pangea/services/intel.py +5 -0
- pangea/services/sanitize.py +366 -0
- {pangea_sdk-4.2.0.dist-info → pangea_sdk-4.4.0.dist-info}/METADATA +5 -5
- {pangea_sdk-4.2.0.dist-info → pangea_sdk-4.4.0.dist-info}/RECORD +21 -16
- {pangea_sdk-4.2.0.dist-info → pangea_sdk-4.4.0.dist-info}/WHEEL +1 -1
pangea/__init__.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
__version__ = "4.
|
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()
|
pangea/asyncio/request.py
CHANGED
@@ -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.
|
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
|
-
|
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.
|
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)
|
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
|
-
|
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
|
pangea/asyncio/services/authz.py
CHANGED
@@ -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
|
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,
|
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,
|
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(
|
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(
|
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
|
99
|
-
|
100
|
-
raise ValueError("Need to set file_path or
|
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
|
-
|
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,
|
pangea/asyncio/services/intel.py
CHANGED
@@ -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)
|
pangea/file_uploader.py
ADDED
@@ -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}")
|
pangea/request.py
CHANGED
@@ -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)
|
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:
|
pangea/response.py
CHANGED
@@ -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[
|
239
|
+
return str(self.raw_response.url) # type: ignore[union-attr]
|
pangea/services/__init__.py
CHANGED
pangea/services/audit/signing.py
CHANGED
@@ -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)
|
99
|
+
return func(private_key, None)
|
100
100
|
except exceptions.UnsupportedAlgorithm as e:
|
101
101
|
raise e
|
102
102
|
except ValueError:
|
pangea/services/authz.py
CHANGED
@@ -132,6 +132,7 @@ class ListResourcesRequest(APIRequestModel):
|
|
132
132
|
type: str
|
133
133
|
action: str
|
134
134
|
subject: Subject
|
135
|
+
attributes: Optional[Dict[str, Any]] = None
|
135
136
|
|
136
137
|
|
137
138
|
class ListResourcesResult(PangeaResponseResult):
|
@@ -141,6 +142,7 @@ class ListResourcesResult(PangeaResponseResult):
|
|
141
142
|
class ListSubjectsRequest(APIRequestModel):
|
142
143
|
resource: Resource
|
143
144
|
action: str
|
145
|
+
attributes: Optional[Dict[str, Any]] = None
|
144
146
|
|
145
147
|
|
146
148
|
class ListSubjectsResult(PangeaResponseResult):
|
@@ -278,7 +280,7 @@ class AuthZ(ServiceBase):
|
|
278
280
|
action: str,
|
279
281
|
subject: Subject,
|
280
282
|
debug: Optional[bool] = None,
|
281
|
-
attributes: Optional[Dict[str,
|
283
|
+
attributes: Optional[Dict[str, Any]] = None,
|
282
284
|
) -> PangeaResponse[CheckResult]:
|
283
285
|
"""Perform a check request.
|
284
286
|
|
@@ -289,7 +291,7 @@ class AuthZ(ServiceBase):
|
|
289
291
|
action (str): The action to check.
|
290
292
|
subject (Subject): The subject to check.
|
291
293
|
debug (Optional[bool]): Setting this value to True will provide a detailed analysis of the check.
|
292
|
-
attributes (Optional[Dict[str,
|
294
|
+
attributes (Optional[Dict[str, Any]]): Additional attributes for the check.
|
293
295
|
|
294
296
|
Raises:
|
295
297
|
PangeaAPIException: If an API Error happens.
|
@@ -311,7 +313,9 @@ class AuthZ(ServiceBase):
|
|
311
313
|
input_data = CheckRequest(resource=resource, action=action, subject=subject, debug=debug, attributes=attributes)
|
312
314
|
return self.request.post("v1/check", CheckResult, data=input_data.model_dump(exclude_none=True))
|
313
315
|
|
314
|
-
def list_resources(
|
316
|
+
def list_resources(
|
317
|
+
self, type: str, action: str, subject: Subject, attributes: Optional[Dict[str, Any]] = None
|
318
|
+
) -> PangeaResponse[ListResourcesResult]:
|
315
319
|
"""List resources.
|
316
320
|
|
317
321
|
Given a type, action, and subject, list all the resources in the
|
@@ -321,6 +325,7 @@ class AuthZ(ServiceBase):
|
|
321
325
|
type (str): The type to filter resources.
|
322
326
|
action (str): The action to filter resources.
|
323
327
|
subject (Subject): The subject to filter resources.
|
328
|
+
attributes (Optional[Dict[str, Any]]): A JSON object of attribute data.
|
324
329
|
|
325
330
|
Raises:
|
326
331
|
PangeaAPIException: If an API Error happens.
|
@@ -338,12 +343,14 @@ class AuthZ(ServiceBase):
|
|
338
343
|
)
|
339
344
|
"""
|
340
345
|
|
341
|
-
input_data = ListResourcesRequest(type=type, action=action, subject=subject)
|
346
|
+
input_data = ListResourcesRequest(type=type, action=action, subject=subject, attributes=attributes)
|
342
347
|
return self.request.post(
|
343
348
|
"v1/list-resources", ListResourcesResult, data=input_data.model_dump(exclude_none=True)
|
344
349
|
)
|
345
350
|
|
346
|
-
def list_subjects(
|
351
|
+
def list_subjects(
|
352
|
+
self, resource: Resource, action: str, attributes: Optional[Dict[str, Any]] = None
|
353
|
+
) -> PangeaResponse[ListSubjectsResult]:
|
347
354
|
"""List subjects.
|
348
355
|
|
349
356
|
Given a resource and an action, return the list of subjects who have
|
@@ -352,6 +359,7 @@ class AuthZ(ServiceBase):
|
|
352
359
|
Args:
|
353
360
|
resource (Resource): The resource to filter subjects.
|
354
361
|
action (str): The action to filter subjects.
|
362
|
+
attributes (Optional[Dict[str, Any]]): A JSON object of attribute data.
|
355
363
|
|
356
364
|
Raises:
|
357
365
|
PangeaAPIException: If an API Error happens.
|
@@ -368,5 +376,5 @@ class AuthZ(ServiceBase):
|
|
368
376
|
)
|
369
377
|
"""
|
370
378
|
|
371
|
-
input_data = ListSubjectsRequest(resource=resource, action=action)
|
379
|
+
input_data = ListSubjectsRequest(resource=resource, action=action, attributes=attributes)
|
372
380
|
return self.request.post("v1/list-subjects", ListSubjectsResult, data=input_data.model_dump(exclude_none=True))
|
pangea/services/file_scan.py
CHANGED
@@ -11,22 +11,25 @@ from pangea.utils import FileUploadParams, get_file_upload_params
|
|
11
11
|
|
12
12
|
|
13
13
|
class FileScanRequest(APIRequestModel):
|
14
|
-
"""
|
15
|
-
File Scan request data
|
16
|
-
|
17
|
-
provider (str, optional): Provider of the information. Default provider defined by the configuration.
|
18
|
-
verbose (bool, optional): Echo back the parameters of the API in the response
|
19
|
-
raw (bool, optional): Return additional details from the provider.
|
20
|
-
"""
|
14
|
+
"""File Scan request data."""
|
21
15
|
|
22
16
|
verbose: Optional[bool] = None
|
17
|
+
"""Echo back the parameters of the API in the response."""
|
18
|
+
|
23
19
|
raw: Optional[bool] = None
|
20
|
+
"""Return additional details from the provider."""
|
21
|
+
|
24
22
|
provider: Optional[str] = None
|
23
|
+
"""Provider of the information. Default provider defined by the configuration."""
|
24
|
+
|
25
25
|
size: Optional[int] = None
|
26
26
|
crc32c: Optional[str] = None
|
27
27
|
sha256: Optional[str] = None
|
28
28
|
source_url: Optional[str] = None
|
29
|
+
"""A URL where the file to be scanned can be downloaded."""
|
30
|
+
|
29
31
|
transfer_method: TransferMethod = TransferMethod.POST_URL
|
32
|
+
"""The transfer method used to upload the file data."""
|
30
33
|
|
31
34
|
|
32
35
|
class FileScanData(PangeaResponseResult):
|
@@ -71,7 +74,6 @@ class FileScan(ServiceBase):
|
|
71
74
|
"""
|
72
75
|
|
73
76
|
service_name = "file-scan"
|
74
|
-
version = "v1"
|
75
77
|
|
76
78
|
def file_scan(
|
77
79
|
self,
|
@@ -92,12 +94,14 @@ class FileScan(ServiceBase):
|
|
92
94
|
OperationId: file_scan_post_v1_scan
|
93
95
|
|
94
96
|
Args:
|
95
|
-
file (io.BufferedReader, optional): file to be scanned (should be opened with read permissions and in binary format)
|
96
97
|
file_path (str, optional): filepath to be opened and scanned
|
98
|
+
file (io.BufferedReader, optional): file to be scanned (should be opened with read permissions and in binary format)
|
97
99
|
verbose (bool, optional): Echo the API parameters in the response
|
98
100
|
raw (bool, optional): Include raw data from this provider
|
99
101
|
provider (str, optional): Scan file using this provider
|
100
102
|
sync_call (bool, optional): True to wait until server returns a result, False to return immediately and retrieve result asynchronously
|
103
|
+
transfer_method (TransferMethod, optional): Transfer method used to upload the file data.
|
104
|
+
source_url (str, optional): A URL where the Pangea APIs can fetch the contents of the input file.
|
101
105
|
|
102
106
|
Raises:
|
103
107
|
PangeaAPIException: If an API Error happens
|
@@ -118,6 +122,15 @@ class FileScan(ServiceBase):
|
|
118
122
|
print(f"\\t{err.detail} \\n")
|
119
123
|
"""
|
120
124
|
|
125
|
+
if transfer_method == TransferMethod.SOURCE_URL and source_url is None:
|
126
|
+
raise ValueError("`source_url` argument is required when using `TransferMethod.SOURCE_URL`.")
|
127
|
+
|
128
|
+
if source_url is not None and transfer_method != TransferMethod.SOURCE_URL:
|
129
|
+
raise ValueError(
|
130
|
+
"`transfer_method` should be `TransferMethod.SOURCE_URL` when using the `source_url` argument."
|
131
|
+
)
|
132
|
+
|
133
|
+
files: Optional[List[Tuple]] = None
|
121
134
|
if file or file_path:
|
122
135
|
if file_path:
|
123
136
|
file = open(file_path, "rb")
|
@@ -128,9 +141,9 @@ class FileScan(ServiceBase):
|
|
128
141
|
size = params.size
|
129
142
|
else:
|
130
143
|
crc, sha, size = None, None, None
|
131
|
-
files
|
132
|
-
|
133
|
-
raise ValueError("Need to set file_path or
|
144
|
+
files = [("upload", ("filename", file, "application/octet-stream"))]
|
145
|
+
elif source_url is None:
|
146
|
+
raise ValueError("Need to set one of `file_path`, `file`, or `source_url` arguments.")
|
134
147
|
|
135
148
|
input = FileScanRequest(
|
136
149
|
verbose=verbose,
|
@@ -143,7 +156,11 @@ class FileScan(ServiceBase):
|
|
143
156
|
source_url=source_url,
|
144
157
|
)
|
145
158
|
data = input.model_dump(exclude_none=True)
|
146
|
-
|
159
|
+
try:
|
160
|
+
return self.request.post("v1/scan", FileScanResult, data=data, files=files, poll_result=sync_call)
|
161
|
+
finally:
|
162
|
+
if file_path and file:
|
163
|
+
file.close()
|
147
164
|
|
148
165
|
def request_upload_url(
|
149
166
|
self,
|
pangea/services/intel.py
CHANGED
@@ -1255,6 +1255,7 @@ class UserBreachedBulkRequest(IntelCommonRequest):
|
|
1255
1255
|
usernames (List[str]): An username' list to search for
|
1256
1256
|
ips (List[str]): An ip's list to search for
|
1257
1257
|
phone_numbers (List[str]): A phone number's list to search for. minLength: 7, maxLength: 15.
|
1258
|
+
domains (List[str]): Search for user under these domains.
|
1258
1259
|
start (str): Earliest date for search
|
1259
1260
|
end (str): Latest date for search
|
1260
1261
|
"""
|
@@ -1263,6 +1264,7 @@ class UserBreachedBulkRequest(IntelCommonRequest):
|
|
1263
1264
|
usernames: Optional[List[str]] = None
|
1264
1265
|
ips: Optional[List[str]] = None
|
1265
1266
|
phone_numbers: Optional[List[str]] = None
|
1267
|
+
domains: Optional[List[str]] = None
|
1266
1268
|
start: Optional[str] = None
|
1267
1269
|
end: Optional[str] = None
|
1268
1270
|
|
@@ -1439,6 +1441,7 @@ class UserIntel(ServiceBase):
|
|
1439
1441
|
usernames: Optional[List[str]] = None,
|
1440
1442
|
ips: Optional[List[str]] = None,
|
1441
1443
|
phone_numbers: Optional[List[str]] = None,
|
1444
|
+
domains: Optional[List[str]] = None,
|
1442
1445
|
start: Optional[str] = None,
|
1443
1446
|
end: Optional[str] = None,
|
1444
1447
|
verbose: Optional[bool] = None,
|
@@ -1457,6 +1460,7 @@ class UserIntel(ServiceBase):
|
|
1457
1460
|
usernames (List[str]): A list of usernames to search for
|
1458
1461
|
ips (List[str]): A list of ips to search for
|
1459
1462
|
phone_numbers (List[str]): A list of phone numbers to search for. minLength: 7, maxLength: 15.
|
1463
|
+
domains (List[str]): Search for user under these domains.
|
1460
1464
|
start (str): Earliest date for search
|
1461
1465
|
end (str): Latest date for search
|
1462
1466
|
verbose (bool, optional): Echo the API parameters in the response
|
@@ -1484,6 +1488,7 @@ class UserIntel(ServiceBase):
|
|
1484
1488
|
phone_numbers=phone_numbers,
|
1485
1489
|
usernames=usernames,
|
1486
1490
|
ips=ips,
|
1491
|
+
domains=domains,
|
1487
1492
|
provider=provider,
|
1488
1493
|
start=start,
|
1489
1494
|
end=end,
|
@@ -0,0 +1,366 @@
|
|
1
|
+
# Copyright 2022 Pangea Cyber Corporation
|
2
|
+
# Author: Pangea Cyber Corporation
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import io
|
6
|
+
from typing import Dict, List, Optional, Tuple
|
7
|
+
|
8
|
+
from pydantic import Field
|
9
|
+
|
10
|
+
from pangea.response import APIRequestModel, PangeaResponse, PangeaResponseResult, TransferMethod
|
11
|
+
from pangea.services.base import ServiceBase
|
12
|
+
from pangea.utils import FileUploadParams, get_file_upload_params
|
13
|
+
|
14
|
+
|
15
|
+
class SanitizeFile(APIRequestModel):
|
16
|
+
scan_provider: Optional[str] = None
|
17
|
+
"""Provider to use for File Scan."""
|
18
|
+
|
19
|
+
|
20
|
+
class SanitizeContent(APIRequestModel):
|
21
|
+
url_intel: Optional[bool] = None
|
22
|
+
"""Perform URL Intel lookup."""
|
23
|
+
|
24
|
+
url_intel_provider: Optional[str] = None
|
25
|
+
"""Provider to use for URL Intel."""
|
26
|
+
|
27
|
+
domain_intel: Optional[bool] = None
|
28
|
+
"""Perform Domain Intel lookup."""
|
29
|
+
|
30
|
+
domain_intel_provider: Optional[str] = None
|
31
|
+
"""Provider to use for Domain Intel lookup."""
|
32
|
+
|
33
|
+
defang: Optional[bool] = None
|
34
|
+
"""Defang external links."""
|
35
|
+
|
36
|
+
defang_threshold: Optional[int] = None
|
37
|
+
"""Defang risk threshold."""
|
38
|
+
|
39
|
+
redact: Optional[bool] = None
|
40
|
+
"""Redact sensitive content."""
|
41
|
+
|
42
|
+
redact_detect_only: Optional[bool] = None
|
43
|
+
"""
|
44
|
+
If redact is enabled, avoids redacting the file and instead returns the PII
|
45
|
+
analysis engine results. Only works if redact is enabled.
|
46
|
+
"""
|
47
|
+
|
48
|
+
remove_attachments: Optional[bool] = None
|
49
|
+
"""Remove file attachments (PDF only)."""
|
50
|
+
|
51
|
+
remove_interactive: Optional[bool] = None
|
52
|
+
"""Remove interactive content (PDF only)."""
|
53
|
+
|
54
|
+
|
55
|
+
class SanitizeShareOutput(APIRequestModel):
|
56
|
+
enabled: Optional[bool] = None
|
57
|
+
"""Store Sanitized files to Pangea Secure Share."""
|
58
|
+
|
59
|
+
output_folder: Optional[str] = None
|
60
|
+
"""
|
61
|
+
Store Sanitized files to this Secure Share folder (will be auto-created if
|
62
|
+
it does not exist)
|
63
|
+
"""
|
64
|
+
|
65
|
+
|
66
|
+
class SanitizeRequest(APIRequestModel):
|
67
|
+
transfer_method: TransferMethod = TransferMethod.POST_URL
|
68
|
+
"""The transfer method used to upload the file data."""
|
69
|
+
|
70
|
+
source_url: Optional[str] = None
|
71
|
+
"""A URL where the file to be sanitized can be downloaded."""
|
72
|
+
|
73
|
+
share_id: Optional[str] = None
|
74
|
+
"""A Pangea Secure Share ID where the file to be Sanitized is stored."""
|
75
|
+
|
76
|
+
file: Optional[SanitizeFile] = None
|
77
|
+
"""File."""
|
78
|
+
|
79
|
+
content: Optional[SanitizeContent] = None
|
80
|
+
"""Content."""
|
81
|
+
|
82
|
+
share_output: Optional[SanitizeShareOutput] = None
|
83
|
+
"""Share output."""
|
84
|
+
|
85
|
+
size: Optional[int] = None
|
86
|
+
"""The size (in bytes) of the file. If the upload doesn't match, the call will fail."""
|
87
|
+
|
88
|
+
crc32c: Optional[str] = None
|
89
|
+
"""The CRC32C hash of the file data, which will be verified by the server if provided."""
|
90
|
+
|
91
|
+
sha256: Optional[str] = None
|
92
|
+
"""The hexadecimal-encoded SHA256 hash of the file data, which will be verified by the server if provided."""
|
93
|
+
|
94
|
+
uploaded_file_name: Optional[str] = None
|
95
|
+
"""Name of the user-uploaded file, required for transfer-method 'put-url' and 'post-url'."""
|
96
|
+
|
97
|
+
|
98
|
+
class DefangData(PangeaResponseResult):
|
99
|
+
external_urls_count: Optional[int] = None
|
100
|
+
"""Number of external links found."""
|
101
|
+
|
102
|
+
external_domains_count: Optional[int] = None
|
103
|
+
"""Number of external domains found."""
|
104
|
+
|
105
|
+
defanged_count: Optional[int] = None
|
106
|
+
"""Number of items defanged per provided rules and detections."""
|
107
|
+
|
108
|
+
url_intel_summary: Optional[str] = None
|
109
|
+
"""Processed N URLs: X are malicious, Y are suspicious, Z are unknown."""
|
110
|
+
|
111
|
+
domain_intel_summary: Optional[str] = None
|
112
|
+
"""Processed N Domains: X are malicious, Y are suspicious, Z are unknown."""
|
113
|
+
|
114
|
+
|
115
|
+
class RedactRecognizerResult(PangeaResponseResult):
|
116
|
+
field_type: str
|
117
|
+
"""The entity name."""
|
118
|
+
|
119
|
+
score: float
|
120
|
+
"""The certainty score that the entity matches this specific snippet."""
|
121
|
+
|
122
|
+
text: str
|
123
|
+
"""The text snippet that matched."""
|
124
|
+
|
125
|
+
start: int
|
126
|
+
"""The starting index of a snippet."""
|
127
|
+
|
128
|
+
end: int
|
129
|
+
"""The ending index of a snippet."""
|
130
|
+
|
131
|
+
redacted: bool
|
132
|
+
"""Indicates if this rule was used to anonymize a text snippet."""
|
133
|
+
|
134
|
+
|
135
|
+
class RedactData(PangeaResponseResult):
|
136
|
+
redaction_count: int
|
137
|
+
"""Number of items redacted"""
|
138
|
+
|
139
|
+
summary_counts: Dict[str, int] = Field(default_factory=dict)
|
140
|
+
"""Summary counts."""
|
141
|
+
|
142
|
+
recognizer_results: Optional[List[RedactRecognizerResult]] = None
|
143
|
+
"""The scoring result of a set of rules."""
|
144
|
+
|
145
|
+
|
146
|
+
class CDR(PangeaResponseResult):
|
147
|
+
file_attachments_removed: Optional[int] = None
|
148
|
+
"""Number of file attachments removed."""
|
149
|
+
|
150
|
+
interactive_contents_removed: Optional[int] = None
|
151
|
+
"""Number of interactive content items removed."""
|
152
|
+
|
153
|
+
|
154
|
+
class SanitizeData(PangeaResponseResult):
|
155
|
+
defang: Optional[DefangData] = None
|
156
|
+
"""Defang."""
|
157
|
+
|
158
|
+
redact: Optional[RedactData] = None
|
159
|
+
"""Redact."""
|
160
|
+
|
161
|
+
malicious_file: Optional[bool] = None
|
162
|
+
"""If the file scanned was malicious."""
|
163
|
+
|
164
|
+
cdr: Optional[CDR] = None
|
165
|
+
"""Content Disarm and Reconstruction."""
|
166
|
+
|
167
|
+
|
168
|
+
class SanitizeResult(PangeaResponseResult):
|
169
|
+
dest_url: Optional[str] = None
|
170
|
+
"""A URL where the Sanitized file can be downloaded."""
|
171
|
+
|
172
|
+
dest_share_id: Optional[str] = None
|
173
|
+
"""Pangea Secure Share ID of the Sanitized file."""
|
174
|
+
|
175
|
+
data: SanitizeData
|
176
|
+
"""Sanitize data."""
|
177
|
+
|
178
|
+
parameters: Dict = {}
|
179
|
+
"""The parameters, which were passed in the request, echoed back."""
|
180
|
+
|
181
|
+
|
182
|
+
class Sanitize(ServiceBase):
|
183
|
+
"""Sanitize service client.
|
184
|
+
|
185
|
+
Examples:
|
186
|
+
import os
|
187
|
+
|
188
|
+
# Pangea SDK
|
189
|
+
from pangea.config import PangeaConfig
|
190
|
+
from pangea.services import Sanitize
|
191
|
+
|
192
|
+
PANGEA_SANITIZE_TOKEN = os.getenv("PANGEA_SANITIZE_TOKEN")
|
193
|
+
config = PangeaConfig(domain="pangea.cloud")
|
194
|
+
|
195
|
+
sanitize = Sanitize(token=PANGEA_SANITIZE_TOKEN, config=config)
|
196
|
+
"""
|
197
|
+
|
198
|
+
service_name = "sanitize"
|
199
|
+
|
200
|
+
def sanitize(
|
201
|
+
self,
|
202
|
+
transfer_method: TransferMethod = TransferMethod.POST_URL,
|
203
|
+
file_path: Optional[str] = None,
|
204
|
+
file: Optional[io.BufferedReader] = None,
|
205
|
+
source_url: Optional[str] = None,
|
206
|
+
share_id: Optional[str] = None,
|
207
|
+
file_scan: Optional[SanitizeFile] = None,
|
208
|
+
content: Optional[SanitizeContent] = None,
|
209
|
+
share_output: Optional[SanitizeShareOutput] = None,
|
210
|
+
size: Optional[int] = None,
|
211
|
+
crc32c: Optional[str] = None,
|
212
|
+
sha256: Optional[str] = None,
|
213
|
+
uploaded_file_name: Optional[str] = None,
|
214
|
+
sync_call: bool = True,
|
215
|
+
) -> PangeaResponse[SanitizeResult]:
|
216
|
+
"""
|
217
|
+
Sanitize
|
218
|
+
|
219
|
+
Apply file sanitization actions according to specified rules.
|
220
|
+
|
221
|
+
OperationId: sanitize_post_v1_sanitize
|
222
|
+
|
223
|
+
Args:
|
224
|
+
transfer_method: The transfer method used to upload the file data.
|
225
|
+
file_path: Path to file to sanitize.
|
226
|
+
file: File to sanitize.
|
227
|
+
source_url: A URL where the file to be sanitized can be downloaded.
|
228
|
+
share_id: A Pangea Secure Share ID where the file to be sanitized is stored.
|
229
|
+
file_scan: Options for File Scan.
|
230
|
+
content: Options for how the file should be sanitized.
|
231
|
+
share_output: Integration with Secure Share.
|
232
|
+
size: The size (in bytes) of the file. If the upload doesn't match, the call will fail.
|
233
|
+
crc32c: The CRC32C hash of the file data, which will be verified by the server if provided.
|
234
|
+
sha256: The hexadecimal-encoded SHA256 hash of the file data, which will be verified by the server if provided.
|
235
|
+
uploaded_file_name: Name of the user-uploaded file, required for `TransferMethod.PUT_URL` and `TransferMethod.POST_URL`.
|
236
|
+
sync_call: Whether or not to poll on HTTP/202.
|
237
|
+
|
238
|
+
Raises:
|
239
|
+
PangeaAPIException: If an API error happens.
|
240
|
+
|
241
|
+
Returns:
|
242
|
+
The sanitized file and information on the sanitization that was
|
243
|
+
performed.
|
244
|
+
|
245
|
+
Examples:
|
246
|
+
with open("/path/to/file.pdf", "rb") as f:
|
247
|
+
response = sanitize.sanitize(
|
248
|
+
file=f,
|
249
|
+
transfer_method=TransferMethod.POST_URL,
|
250
|
+
uploaded_file_name="uploaded_file",
|
251
|
+
)
|
252
|
+
"""
|
253
|
+
|
254
|
+
if transfer_method == TransferMethod.SOURCE_URL and source_url is None:
|
255
|
+
raise ValueError("`source_url` argument is required when using `TransferMethod.SOURCE_URL`.")
|
256
|
+
|
257
|
+
if source_url is not None and transfer_method != TransferMethod.SOURCE_URL:
|
258
|
+
raise ValueError(
|
259
|
+
"`transfer_method` should be `TransferMethod.SOURCE_URL` when using the `source_url` argument."
|
260
|
+
)
|
261
|
+
|
262
|
+
files: Optional[List[Tuple]] = None
|
263
|
+
if file or file_path:
|
264
|
+
if file_path:
|
265
|
+
file = open(file_path, "rb")
|
266
|
+
if (
|
267
|
+
transfer_method == TransferMethod.POST_URL
|
268
|
+
and file
|
269
|
+
and (sha256 is None or crc32c is None or size is None)
|
270
|
+
):
|
271
|
+
params = get_file_upload_params(file)
|
272
|
+
crc32c = params.crc_hex if crc32c is None else crc32c
|
273
|
+
sha256 = params.sha256_hex if sha256 is None else sha256
|
274
|
+
size = params.size if size is None else size
|
275
|
+
else:
|
276
|
+
crc32c, sha256, size = None, None, None
|
277
|
+
files = [("upload", ("filename", file, "application/octet-stream"))]
|
278
|
+
elif source_url is None:
|
279
|
+
raise ValueError("Need to set one of `file_path`, `file`, or `source_url` arguments.")
|
280
|
+
|
281
|
+
input = SanitizeRequest(
|
282
|
+
transfer_method=transfer_method,
|
283
|
+
source_url=source_url,
|
284
|
+
share_id=share_id,
|
285
|
+
file=file_scan,
|
286
|
+
content=content,
|
287
|
+
share_output=share_output,
|
288
|
+
crc32c=crc32c,
|
289
|
+
sha256=sha256,
|
290
|
+
size=size,
|
291
|
+
uploaded_file_name=uploaded_file_name,
|
292
|
+
)
|
293
|
+
data = input.model_dump(exclude_none=True)
|
294
|
+
try:
|
295
|
+
response = self.request.post("v1/sanitize", SanitizeResult, data=data, files=files, poll_result=sync_call)
|
296
|
+
finally:
|
297
|
+
if file_path and file is not None:
|
298
|
+
file.close()
|
299
|
+
return response
|
300
|
+
|
301
|
+
def request_upload_url(
|
302
|
+
self,
|
303
|
+
transfer_method: TransferMethod = TransferMethod.PUT_URL,
|
304
|
+
params: Optional[FileUploadParams] = None,
|
305
|
+
file_scan: Optional[SanitizeFile] = None,
|
306
|
+
content: Optional[SanitizeContent] = None,
|
307
|
+
share_output: Optional[SanitizeShareOutput] = None,
|
308
|
+
size: Optional[int] = None,
|
309
|
+
crc32c: Optional[str] = None,
|
310
|
+
sha256: Optional[str] = None,
|
311
|
+
uploaded_file_name: Optional[str] = None,
|
312
|
+
) -> PangeaResponse[SanitizeResult]:
|
313
|
+
"""
|
314
|
+
Sanitize via presigned URL
|
315
|
+
|
316
|
+
Apply file sanitization actions according to specified rules via a
|
317
|
+
[presigned URL](https://pangea.cloud/docs/api/transfer-methods).
|
318
|
+
|
319
|
+
OperationId: sanitize_post_v1_sanitize 2
|
320
|
+
|
321
|
+
Args:
|
322
|
+
transfer_method: The transfer method used to upload the file data.
|
323
|
+
params: File upload parameters.
|
324
|
+
file_scan: Options for File Scan.
|
325
|
+
content: Options for how the file should be sanitized.
|
326
|
+
share_output: Integration with Secure Share.
|
327
|
+
size: The size (in bytes) of the file. If the upload doesn't match, the call will fail.
|
328
|
+
crc32c: The CRC32C hash of the file data, which will be verified by the server if provided.
|
329
|
+
sha256: The hexadecimal-encoded SHA256 hash of the file data, which will be verified by the server if provided.
|
330
|
+
uploaded_file_name: Name of the user-uploaded file, required for `TransferMethod.PUT_URL` and `TransferMethod.POST_URL`.
|
331
|
+
|
332
|
+
Raises:
|
333
|
+
PangeaAPIException: If an API error happens.
|
334
|
+
|
335
|
+
Returns:
|
336
|
+
A presigned URL.
|
337
|
+
|
338
|
+
Examples:
|
339
|
+
presignedUrl = sanitize.request_upload_url(
|
340
|
+
transfer_method=TransferMethod.PUT_URL,
|
341
|
+
uploaded_file_name="uploaded_file",
|
342
|
+
)
|
343
|
+
|
344
|
+
# Upload file to `presignedUrl.accepted_result.put_url`.
|
345
|
+
|
346
|
+
# Poll for Sanitize's result.
|
347
|
+
response: PangeaResponse[SanitizeResult] = sanitize.poll_result(response=presignedUrl)
|
348
|
+
"""
|
349
|
+
|
350
|
+
input = SanitizeRequest(
|
351
|
+
transfer_method=transfer_method,
|
352
|
+
file=file_scan,
|
353
|
+
content=content,
|
354
|
+
share_output=share_output,
|
355
|
+
crc32c=crc32c,
|
356
|
+
sha256=sha256,
|
357
|
+
size=size,
|
358
|
+
uploaded_file_name=uploaded_file_name,
|
359
|
+
)
|
360
|
+
if params is not None and (transfer_method == TransferMethod.POST_URL):
|
361
|
+
input.crc32c = params.crc_hex
|
362
|
+
input.sha256 = params.sha256_hex
|
363
|
+
input.size = params.size
|
364
|
+
|
365
|
+
data = input.model_dump(exclude_none=True)
|
366
|
+
return self.request.request_presigned_url("v1/sanitize", SanitizeResult, data=data)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: pangea-sdk
|
3
|
-
Version: 4.
|
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.
|
21
|
-
Requires-Dist:
|
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.
|
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,14 +1,17 @@
|
|
1
|
-
pangea/__init__.py,sha256=
|
2
|
-
pangea/asyncio/
|
3
|
-
pangea/asyncio/
|
1
|
+
pangea/__init__.py,sha256=v6ZDmf842vyF60xDR-QFmFgqOrACNjTFpxIBQcx3gFc,246
|
2
|
+
pangea/asyncio/__init__.py,sha256=kjEMkqMQ521LlMSu5jn3_WgweyArwVZ2C-s3x7mR6Pk,45
|
3
|
+
pangea/asyncio/file_uploader.py,sha256=wI7epib7Rc5jtZw4eJ1L1SlmutDG6CPv59C8N2UPhtY,1436
|
4
|
+
pangea/asyncio/request.py,sha256=KMfQYC027EBeyYyMlvnDtjxbpaRj9xubeqAdpot3F3U,17168
|
5
|
+
pangea/asyncio/services/__init__.py,sha256=hMeTMnksGimg-fS_XMpRgh02a7BNcLYCt56tnChYeC4,356
|
4
6
|
pangea/asyncio/services/audit.py,sha256=bZ7gdkVWkzqLqUVc1Wnf3oDAaCLg97-zTWhY8UdX0_Y,26549
|
5
7
|
pangea/asyncio/services/authn.py,sha256=rPeLJweL8mYH_t4ebcQn4n_Wglr3kClKNnCXNCimZU4,46622
|
6
|
-
pangea/asyncio/services/authz.py,sha256=
|
8
|
+
pangea/asyncio/services/authz.py,sha256=HgW9R8DeW19wS7fpgq0NWOx41wZWcn6NYS4NMbi8p1A,9482
|
7
9
|
pangea/asyncio/services/base.py,sha256=4FtKtlq74NmE9myrgIt9HMA6JDnP4mPZ6krafWr286o,2663
|
8
10
|
pangea/asyncio/services/embargo.py,sha256=ctzj3kip6xos-Eu3JuOskrCGYC8T3JlsgAopZHiPSXM,3068
|
9
|
-
pangea/asyncio/services/file_scan.py,sha256=
|
10
|
-
pangea/asyncio/services/intel.py,sha256=
|
11
|
+
pangea/asyncio/services/file_scan.py,sha256=PLG1O-PL4Yk9uY9D6NbMrZ5LHg70Z311s7bFe46UMZA,7108
|
12
|
+
pangea/asyncio/services/intel.py,sha256=cCm3VwWxUzEUCNhuPCeejJvr4uOeLXuYDbDwTzNG6Aw,38121
|
11
13
|
pangea/asyncio/services/redact.py,sha256=jRNtXr_DZ_cY7guhut-eZmOEhy2uN_VCXrjGH6bkh74,7265
|
14
|
+
pangea/asyncio/services/sanitize.py,sha256=bf98J-s-P51oSKqNBgR0wj5mlHOCBwpjWz7k0NdXCKQ,7899
|
12
15
|
pangea/asyncio/services/vault.py,sha256=gFch7dVFZzjcTryn68AR4Xbj37U4A_LxfCMrX2mhtSk,53271
|
13
16
|
pangea/audit_logger.py,sha256=gRkCfUUT5LDNaycwxkhZUySgY47jDfn1ZeKOul4XCQI,3842
|
14
17
|
pangea/config.py,sha256=mQUu8GX_6weIuv3vjNdG5plppXskXYASmxMWtFQh-hc,1662
|
@@ -17,23 +20,25 @@ pangea/deep_verify.py,sha256=mocaGbC6XLbMTVWxTpMv4oJtXGPWpT-SbFqT3obpiZs,8443
|
|
17
20
|
pangea/deprecated.py,sha256=IjFYEVvY1E0ld0SMkEYC1o62MAleX3nnT1If2dFVbHo,608
|
18
21
|
pangea/dump_audit.py,sha256=1Je8D2fXwU4PWcZ-ZD4icfO3DNFvWqJkwsac4qFEhOo,7025
|
19
22
|
pangea/exceptions.py,sha256=OBtzUECpNa6vNp8ySkHC-tm4QjFRCOAHBkMHqzAlOu8,5656
|
23
|
+
pangea/file_uploader.py,sha256=4RQ44xt-faApC61nn2PlwHT7XYrJ4GeQA8Ug4tySEAg,1227
|
20
24
|
pangea/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
21
|
-
pangea/request.py,sha256=
|
22
|
-
pangea/response.py,sha256=
|
23
|
-
pangea/services/__init__.py,sha256=
|
25
|
+
pangea/request.py,sha256=fKMuf2tuAguK2TnSSMpmdcBdHlgsYFQoYCBVh3mUkBY,24259
|
26
|
+
pangea/response.py,sha256=rjQuTAHLSAB0m2uAsdbt11xUVL5cCGT4uTRzQhUz9hE,7321
|
27
|
+
pangea/services/__init__.py,sha256=YPXqGGUlAm2oHQNArhDVfr6Y1Gq2o4nhwx_dpdKe74c,309
|
24
28
|
pangea/services/audit/audit.py,sha256=IFv7jANA8S2SypQVS47x94_Cr5Z9zSsL9Dp9eXw9RHk,39593
|
25
29
|
pangea/services/audit/exceptions.py,sha256=bhVuYe4ammacOVxwg98CChxvwZf5FKgR2DcgqILOcwc,471
|
26
30
|
pangea/services/audit/models.py,sha256=1h1B9eSYQMYG3f8WNi1UcDX2-impRrET_ErjJYUnj7M,14678
|
27
|
-
pangea/services/audit/signing.py,sha256=
|
31
|
+
pangea/services/audit/signing.py,sha256=EYuZN6pcFOjDJBG6S65jesE_8xOz5SNms6qHZ1qambQ,5541
|
28
32
|
pangea/services/audit/util.py,sha256=Zq1qvfeplYfhCP_ud5YMvntSB0UvnCdsuYbOzZkHbjg,7620
|
29
33
|
pangea/services/authn/authn.py,sha256=cZKl2Ixc6HwHnkRecpSaAGTQUgaZUtxfLa0T3S03HMs,45478
|
30
34
|
pangea/services/authn/models.py,sha256=HH5su6jx3O9AwVGzASXZ99-eIWjgXEP5LhIVdewM13s,22394
|
31
|
-
pangea/services/authz.py,sha256=
|
35
|
+
pangea/services/authz.py,sha256=HfDnovAokzAHvnjYdOCwceM-1sCmzODnjNEbQBUSfo8,12222
|
32
36
|
pangea/services/base.py,sha256=lwhHoe5Juy28Ir3Mfj2lHdM58gxZRaxa2SRFi4_DBRw,3453
|
33
37
|
pangea/services/embargo.py,sha256=9Wfku4td5ORaIENKmnGmS5jxJJIRfWp6Q51L36Jsy0I,3897
|
34
|
-
pangea/services/file_scan.py,sha256=
|
35
|
-
pangea/services/intel.py,sha256=
|
38
|
+
pangea/services/file_scan.py,sha256=QiO80uKqB_BnAOiYQKznXfxpa5j40qqETE3-zBRT_QE,7813
|
39
|
+
pangea/services/intel.py,sha256=CziBhC5K6O_kBXpD8zgJLpDtLHzBRgATGW4gHHFJT48,52039
|
36
40
|
pangea/services/redact.py,sha256=ZYXkzEoriLJyCqaj5dqmgsC56mIz4T3pPToZ7TcNfhg,11465
|
41
|
+
pangea/services/sanitize.py,sha256=XP5D4CcbCZfzgU567X6H5eFBWwZuYSsHdvsdrQAZekY,12767
|
37
42
|
pangea/services/vault/models/asymmetric.py,sha256=xr8oZnjzExMYcbzPJRG3xPXSmhumKDKn7RO90RvdrwU,1526
|
38
43
|
pangea/services/vault/models/common.py,sha256=FOmi2UN5cEgSsrM-aDT1KWLK4TTgrtMukqNczrnWH6w,15491
|
39
44
|
pangea/services/vault/models/secret.py,sha256=cLgEj-_BeGkB4-pmSeTkWVyasFbaJwcEltIEcOyf1U8,481
|
@@ -42,6 +47,6 @@ pangea/services/vault/vault.py,sha256=pv52dpZM0yicdtNra0Yd4AdkWUZC91Yk4rATthu1bs
|
|
42
47
|
pangea/tools.py,sha256=sa2pSz-L8tB6GcZg6lghsmm8w0qMQAIkzqcv7dilU6Q,6429
|
43
48
|
pangea/utils.py,sha256=pMSwL8B-owtrjeWYRjxuyaTQN4V-HsCT669KtOLU3Sw,3195
|
44
49
|
pangea/verify_audit.py,sha256=rvni5akz_P2kYLAGAeA1A5gY6XGpXpAQpbIa7V1PoRY,17458
|
45
|
-
pangea_sdk-4.
|
46
|
-
pangea_sdk-4.
|
47
|
-
pangea_sdk-4.
|
50
|
+
pangea_sdk-4.4.0.dist-info/METADATA,sha256=Pdac62iEWB9wChXMZrR2saCrBMXnsLuWJlhIx6ovA44,7545
|
51
|
+
pangea_sdk-4.4.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
52
|
+
pangea_sdk-4.4.0.dist-info/RECORD,,
|