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/__init__.py +1 -1
- pangea/asyncio/request.py +19 -9
- pangea/asyncio/services/__init__.py +1 -0
- pangea/asyncio/services/share.py +621 -0
- pangea/asyncio/services/vault.py +1571 -787
- pangea/crypto/rsa.py +88 -0
- pangea/request.py +46 -41
- pangea/response.py +12 -0
- pangea/services/__init__.py +1 -0
- pangea/services/audit/signing.py +5 -4
- pangea/services/share/file_format.py +170 -0
- pangea/services/share/share.py +1256 -0
- pangea/services/vault/models/asymmetric.py +120 -20
- pangea/services/vault/models/common.py +293 -171
- pangea/services/vault/models/keys.py +94 -0
- pangea/services/vault/models/secret.py +27 -3
- pangea/services/vault/models/symmetric.py +66 -24
- pangea/services/vault/vault.py +1551 -782
- pangea/tools.py +6 -7
- pangea/utils.py +92 -18
- pangea/verify_audit.py +4 -4
- {pangea_sdk-4.4.0.dist-info → pangea_sdk-5.1.0.dist-info}/METADATA +3 -4
- {pangea_sdk-4.4.0.dist-info → pangea_sdk-5.1.0.dist-info}/RECORD +24 -20
- {pangea_sdk-4.4.0.dist-info → pangea_sdk-5.1.0.dist-info}/WHEEL +0 -0
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
|
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
|
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
|
-
|
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
|
-
|
161
|
+
if status == ResponseStatus.TOO_MANY_REQUESTS.value:
|
161
162
|
raise pe.RateLimitException(summary, response)
|
162
|
-
|
163
|
+
if status == ResponseStatus.NO_CREDIT.value:
|
163
164
|
raise pe.NoCreditException(summary, response)
|
164
|
-
|
165
|
+
if status == ResponseStatus.UNAUTHORIZED.value:
|
165
166
|
raise pe.UnauthorizedException(self.service, response)
|
166
|
-
|
167
|
+
if status == ResponseStatus.SERVICE_NOT_ENABLED.value:
|
167
168
|
raise pe.ServiceNotEnabledException(self.service, response)
|
168
|
-
|
169
|
+
if status == ResponseStatus.PROVIDER_ERR.value:
|
169
170
|
raise pe.ProviderErrorException(summary, response)
|
170
|
-
|
171
|
+
if status in (ResponseStatus.MISSING_CONFIG_ID_SCOPE.value, ResponseStatus.MISSING_CONFIG_ID.value):
|
171
172
|
raise pe.MissingConfigID(self.service, response)
|
172
|
-
|
173
|
+
if status == ResponseStatus.SERVICE_NOT_AVAILABLE.value:
|
173
174
|
raise pe.ServiceNotAvailableException(summary, response)
|
174
|
-
|
175
|
+
if status == ResponseStatus.TREE_NOT_FOUND.value:
|
175
176
|
raise pe.TreeNotFoundException(summary, response)
|
176
|
-
|
177
|
+
if status == ResponseStatus.IP_NOT_FOUND.value:
|
177
178
|
raise pe.IPNotFoundException(summary, response)
|
178
|
-
|
179
|
+
if status == ResponseStatus.BAD_OFFSET.value:
|
179
180
|
raise pe.BadOffsetException(summary, response)
|
180
|
-
|
181
|
+
if status == ResponseStatus.FORBIDDEN_VAULT_OPERATION.value:
|
181
182
|
raise pe.ForbiddenVaultOperation(summary, response)
|
182
|
-
|
183
|
+
if status == ResponseStatus.VAULT_ITEM_NOT_FOUND.value:
|
183
184
|
raise pe.VaultItemNotFound(summary, response)
|
184
|
-
|
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
|
-
|
187
|
+
if status == ResponseStatus.INTERNAL_SERVER_ERROR.value:
|
187
188
|
raise pe.InternalServerError(response)
|
188
|
-
|
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:
|
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
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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."""
|
pangea/services/__init__.py
CHANGED
pangea/services/audit/signing.py
CHANGED
@@ -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.
|
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
|
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
|
-
|
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)
|