dictature 0.9.4__py3-none-any.whl → 0.9.6__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.
- dictature/backend/directory.py +15 -20
- dictature/backend/misp.py +10 -27
- dictature/backend/mock.py +81 -1
- dictature/dictature.py +4 -4
- dictature/transformer/gzip.py +53 -0
- {dictature-0.9.4.dist-info → dictature-0.9.6.dist-info}/METADATA +2 -1
- {dictature-0.9.4.dist-info → dictature-0.9.6.dist-info}/RECORD +10 -9
- {dictature-0.9.4.dist-info → dictature-0.9.6.dist-info}/WHEEL +1 -1
- {dictature-0.9.4.dist-info → dictature-0.9.6.dist-info}/LICENSE +0 -0
- {dictature-0.9.4.dist-info → dictature-0.9.6.dist-info}/top_level.txt +0 -0
dictature/backend/directory.py
CHANGED
@@ -1,14 +1,12 @@
|
|
1
|
-
from re import sub
|
2
|
-
from json import dumps, loads
|
3
1
|
from pathlib import Path
|
4
2
|
from typing import Iterable, Union
|
5
3
|
from shutil import rmtree
|
6
4
|
|
7
|
-
from .mock import DictatureTableMock, DictatureBackendMock, Value, ValueMode
|
5
|
+
from .mock import DictatureTableMock, DictatureBackendMock, Value, ValueMode, ValueSerializer, ValueSerializerMode
|
8
6
|
|
9
7
|
|
10
8
|
class DictatureBackendDirectory(DictatureBackendMock):
|
11
|
-
def __init__(self, directory: Union[Path, str], dir_prefix: str = 'db_') -> None:
|
9
|
+
def __init__(self, directory: Union[Path, str], dir_prefix: str = 'db_', item_prefix: str = 'item_') -> None:
|
12
10
|
"""
|
13
11
|
Create a new directory backend
|
14
12
|
:param directory: directory to store the data
|
@@ -18,6 +16,7 @@ class DictatureBackendDirectory(DictatureBackendMock):
|
|
18
16
|
directory = Path(directory)
|
19
17
|
self.__directory = directory
|
20
18
|
self.__dir_prefix = dir_prefix
|
19
|
+
self.__item_prefix = item_prefix
|
21
20
|
|
22
21
|
def keys(self) -> Iterable[str]:
|
23
22
|
for child in self.__directory.iterdir():
|
@@ -26,13 +25,14 @@ class DictatureBackendDirectory(DictatureBackendMock):
|
|
26
25
|
yield DictatureTableDirectory._filename_decode(child.name[len(self.__dir_prefix):], suffix='')
|
27
26
|
|
28
27
|
def table(self, name: str) -> 'DictatureTableMock':
|
29
|
-
return DictatureTableDirectory(self.__directory, name, self.__dir_prefix)
|
28
|
+
return DictatureTableDirectory(self.__directory, name, self.__dir_prefix, self.__item_prefix)
|
30
29
|
|
31
30
|
|
32
31
|
class DictatureTableDirectory(DictatureTableMock):
|
33
|
-
def __init__(self, path_root: Path, name: str, db_prefix: str, prefix: str
|
32
|
+
def __init__(self, path_root: Path, name: str, db_prefix: str, prefix: str) -> None:
|
34
33
|
self.__path = path_root / (db_prefix + self._filename_encode(name, suffix=''))
|
35
34
|
self.__prefix = prefix
|
35
|
+
self.__serializer = ValueSerializer(mode=ValueSerializerMode.filename_only)
|
36
36
|
|
37
37
|
def keys(self) -> Iterable[str]:
|
38
38
|
for child in self.__path.iterdir():
|
@@ -49,8 +49,7 @@ class DictatureTableDirectory(DictatureTableMock):
|
|
49
49
|
file_target = self.__item_path(item)
|
50
50
|
file_target_tmp = file_target.with_suffix('.tmp')
|
51
51
|
|
52
|
-
|
53
|
-
save_data = dumps({'value': value.value, 'mode': value.mode}, indent=1) if save_as_json else value.value
|
52
|
+
save_data = self.__serializer.serialize(value)
|
54
53
|
|
55
54
|
file_target_tmp.write_text(save_data)
|
56
55
|
file_target_tmp.rename(file_target)
|
@@ -58,10 +57,7 @@ class DictatureTableDirectory(DictatureTableMock):
|
|
58
57
|
def get(self, item: str) -> Value:
|
59
58
|
try:
|
60
59
|
save_data = self.__item_path(item).read_text()
|
61
|
-
|
62
|
-
data = loads(save_data)
|
63
|
-
return Value(data['value'], data['mode'])
|
64
|
-
return Value(save_data, ValueMode.string.value)
|
60
|
+
return self.__serializer.deserialize(save_data)
|
65
61
|
except FileNotFoundError:
|
66
62
|
raise KeyError(item)
|
67
63
|
|
@@ -74,14 +70,13 @@ class DictatureTableDirectory(DictatureTableMock):
|
|
74
70
|
|
75
71
|
@staticmethod
|
76
72
|
def _filename_encode(name: str, suffix: str = '.txt') -> str:
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
73
|
+
return ValueSerializer(mode=ValueSerializerMode.filename_only).serialize(Value(
|
74
|
+
value=name,
|
75
|
+
mode=ValueMode.string.value
|
76
|
+
)) + suffix
|
81
77
|
|
82
78
|
@staticmethod
|
83
79
|
def _filename_decode(name: str, suffix: str = '.txt') -> str:
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
return bytes.fromhex(encoded_name).decode('utf-8')
|
80
|
+
if suffix:
|
81
|
+
name = name[:-len(suffix)]
|
82
|
+
return ValueSerializer(mode=ValueSerializerMode.filename_only).deserialize(name).value
|
dictature/backend/misp.py
CHANGED
@@ -1,8 +1,6 @@
|
|
1
|
-
from json import dumps, loads
|
2
|
-
from re import sub
|
3
1
|
from typing import Iterable, Optional
|
4
2
|
|
5
|
-
from .mock import DictatureTableMock, DictatureBackendMock, Value, ValueMode
|
3
|
+
from .mock import DictatureTableMock, DictatureBackendMock, Value, ValueMode, ValueSerializer, ValueSerializerMode
|
6
4
|
|
7
5
|
try:
|
8
6
|
from pymisp import PyMISP, MISPEvent, MISPAttribute
|
@@ -39,6 +37,7 @@ class DictatureTableMISP(DictatureTableMock):
|
|
39
37
|
self.__event_description = event_description
|
40
38
|
self.__tag = tag
|
41
39
|
self.__event: Optional[MISPEvent] = None
|
40
|
+
self.__serializer = ValueSerializer(mode=ValueSerializerMode.ascii_only)
|
42
41
|
|
43
42
|
def keys(self) -> Iterable[str]:
|
44
43
|
for attribute in self.__event_attributes():
|
@@ -51,18 +50,18 @@ class DictatureTableMISP(DictatureTableMock):
|
|
51
50
|
self.__get_event()
|
52
51
|
|
53
52
|
def set(self, item: str, value: Value) -> None:
|
54
|
-
|
55
|
-
save_data =
|
53
|
+
item_name = self.__serializer.serialize(Value(value=item, mode=ValueMode.string.value))
|
54
|
+
save_data = self.__serializer.serialize(value)
|
56
55
|
|
57
56
|
for attribute in self.__event_attributes():
|
58
|
-
if attribute.value ==
|
59
|
-
attribute.value =
|
57
|
+
if attribute.value == item_name:
|
58
|
+
attribute.value = item_name
|
60
59
|
attribute.comment = save_data
|
61
60
|
self.__misp.update_attribute(attribute)
|
62
61
|
break
|
63
62
|
else:
|
64
63
|
attribute = MISPAttribute()
|
65
|
-
attribute.value =
|
64
|
+
attribute.value = item_name
|
66
65
|
attribute.comment = save_data
|
67
66
|
attribute.type = 'comment'
|
68
67
|
attribute.to_ids = False
|
@@ -71,12 +70,10 @@ class DictatureTableMISP(DictatureTableMock):
|
|
71
70
|
self.__get_event().attributes.append(attribute)
|
72
71
|
|
73
72
|
def get(self, item: str) -> Value:
|
73
|
+
item_name = self.__serializer.serialize(Value(value=item, mode=ValueMode.string.value))
|
74
74
|
for attribute in self.__event_attributes():
|
75
|
-
if attribute.value ==
|
76
|
-
|
77
|
-
data = loads(attribute.comment)
|
78
|
-
return Value(data['value'], data['mode'])
|
79
|
-
return Value(attribute.comment, ValueMode.string.value)
|
75
|
+
if attribute.value == item_name:
|
76
|
+
return self.__serializer.deserialize(attribute.comment)
|
80
77
|
raise KeyError(item)
|
81
78
|
|
82
79
|
def delete(self, item: str) -> None:
|
@@ -110,17 +107,3 @@ class DictatureTableMISP(DictatureTableMock):
|
|
110
107
|
if attribute.type != 'comment' or (hasattr(attribute, 'deleted') and attribute.deleted):
|
111
108
|
continue
|
112
109
|
yield attribute
|
113
|
-
|
114
|
-
@staticmethod
|
115
|
-
def _record_encode(name: str, suffix: str = '.txt') -> str:
|
116
|
-
if name == sub(r'[^\w_. -]', '_', name):
|
117
|
-
return f"d_{name}{suffix}"
|
118
|
-
name = name.encode('utf-8').hex()
|
119
|
-
return f'e_{name}{suffix}'
|
120
|
-
|
121
|
-
@staticmethod
|
122
|
-
def _record_decode(name: str, suffix: str = '.txt') -> str:
|
123
|
-
encoded_name = name[2:-len(suffix) if suffix else len(name)]
|
124
|
-
if name.startswith('d_'):
|
125
|
-
return encoded_name
|
126
|
-
return bytes.fromhex(encoded_name).decode('utf-8')
|
dictature/backend/mock.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
from json import dumps, loads
|
2
|
+
from string import hexdigits, ascii_letters, digits, printable
|
1
3
|
from typing import Iterable, NamedTuple
|
2
4
|
from enum import Enum
|
3
5
|
|
@@ -13,6 +15,84 @@ class Value(NamedTuple):
|
|
13
15
|
mode: int
|
14
16
|
|
15
17
|
|
18
|
+
class ValueSerializerMode(Enum):
|
19
|
+
any_string = 0
|
20
|
+
ascii_only = 1
|
21
|
+
filename_only = 2
|
22
|
+
hex_only = 3
|
23
|
+
|
24
|
+
|
25
|
+
class ValueSerializer:
|
26
|
+
prefix = 'a09e'
|
27
|
+
|
28
|
+
def __init__(self, mode: ValueSerializerMode = ValueSerializerMode.any_string):
|
29
|
+
self.__mode = mode
|
30
|
+
|
31
|
+
def deserialize(self, serialized: str) -> Value:
|
32
|
+
"""
|
33
|
+
Deserialize a string into a Value object
|
34
|
+
:param serialized: serialized string
|
35
|
+
:return: Value object
|
36
|
+
"""
|
37
|
+
# Check if the string starts with the prefix (hex-encoded)
|
38
|
+
if serialized.startswith(self.prefix):
|
39
|
+
# Decode the hex part
|
40
|
+
hex_data = serialized[len(self.prefix):]
|
41
|
+
decoded = bytes.fromhex(hex_data).decode('ascii')
|
42
|
+
# Recursively deserialize the decoded string
|
43
|
+
return ValueSerializer(mode=ValueSerializerMode.ascii_only).deserialize(decoded)
|
44
|
+
|
45
|
+
# Check if the string looks like JSON
|
46
|
+
if serialized.startswith('{'):
|
47
|
+
try:
|
48
|
+
data = loads(serialized)
|
49
|
+
return Value(value=data['value'], mode=data['mode'])
|
50
|
+
except (ValueError, KeyError):
|
51
|
+
pass # Not valid JSON or missing required keys
|
52
|
+
|
53
|
+
# Direct value (string mode)
|
54
|
+
return Value(value=serialized, mode=ValueMode.string.value)
|
55
|
+
|
56
|
+
def serialize(self, value: Value) -> str:
|
57
|
+
"""
|
58
|
+
Serializes a `Value` object into a `str`, converting its data representation
|
59
|
+
based on the serialization mode set in `ValueSerializerMode`. Depending on
|
60
|
+
the mode provided, the object can be serialized directly if its string
|
61
|
+
representation conforms to certain criteria, or it may be converted into
|
62
|
+
a JSON format. Handles customization of allowed characters and uses prefix
|
63
|
+
to encode conditions if the direct representation is not permitted.
|
64
|
+
|
65
|
+
If the mode is incompatible or unsupported, raises a `NotImplementedError`.
|
66
|
+
|
67
|
+
:param value: Instance of `Value` to be serialized.
|
68
|
+
|
69
|
+
:return: Serialized representation of the `Value` object as a string.
|
70
|
+
|
71
|
+
:raises NotImplementedError: If the mode provided in `ValueSerializerMode`
|
72
|
+
is unsupported.
|
73
|
+
"""
|
74
|
+
if self.__mode == ValueSerializerMode.hex_only:
|
75
|
+
allowed_chars = hexdigits
|
76
|
+
elif self.__mode == ValueSerializerMode.filename_only:
|
77
|
+
allowed_chars = ascii_letters + digits + '_.'
|
78
|
+
elif self.__mode in (ValueSerializerMode.any_string, ValueSerializerMode.ascii_only):
|
79
|
+
allowed_chars = None
|
80
|
+
else:
|
81
|
+
raise NotImplementedError(self.__mode)
|
82
|
+
|
83
|
+
if allowed_chars is not None:
|
84
|
+
# Only a subset of characters is allowed if all match and do not start with the reserved prefix, encode directly
|
85
|
+
if value.mode == ValueMode.string.value and all(map(lambda x: x in allowed_chars, value.value)) and not value.value.startswith(self.prefix):
|
86
|
+
return value.value
|
87
|
+
return self.prefix + ValueSerializer(mode=ValueSerializerMode.ascii_only).serialize(value).encode('ascii').hex()
|
88
|
+
|
89
|
+
# Save as JSON if not string or value is starting with { (indicating JSON)
|
90
|
+
save_as_json = value.mode != ValueMode.string.value or value.value.startswith('{') or value.value.startswith(self.prefix)
|
91
|
+
# Save as JSON if only ASCII strings are allowed
|
92
|
+
save_as_json = save_as_json or (self.__mode == ValueSerializerMode.ascii_only and any(filter(lambda x: x not in printable, value.value)))
|
93
|
+
return dumps({'value': value.value, 'mode': value.mode}, indent=1) if save_as_json else value.value
|
94
|
+
|
95
|
+
|
16
96
|
class DictatureBackendMock:
|
17
97
|
def keys(self) -> Iterable[str]:
|
18
98
|
"""
|
@@ -76,4 +156,4 @@ class DictatureTableMock:
|
|
76
156
|
:param item: key to delete
|
77
157
|
:return: None
|
78
158
|
"""
|
79
|
-
raise NotImplementedError("This method should be implemented by the subclass")
|
159
|
+
raise NotImplementedError("This method should be implemented by the subclass")
|
dictature/dictature.py
CHANGED
@@ -221,19 +221,19 @@ class DictatureTable:
|
|
221
221
|
:return: None
|
222
222
|
"""
|
223
223
|
self.__create_table()
|
224
|
-
value_mode = ValueMode.string
|
224
|
+
value_mode: int = ValueMode.string.value
|
225
225
|
|
226
226
|
if type(value) is not str:
|
227
227
|
try:
|
228
228
|
value = json.dumps(value)
|
229
|
-
value_mode = ValueMode.json
|
229
|
+
value_mode = ValueMode.json.value
|
230
230
|
except TypeError:
|
231
231
|
value = b64encode(compress(pickle.dumps(value))).decode('ascii')
|
232
|
-
value_mode =
|
232
|
+
value_mode = ValueMode.pickle.value
|
233
233
|
|
234
234
|
key = self.__item_key(key)
|
235
235
|
value = self.__value_transformer.forward(value)
|
236
|
-
self.__table.set(key, Value(value=value, mode=value_mode
|
236
|
+
self.__table.set(key, Value(value=value, mode=value_mode))
|
237
237
|
|
238
238
|
def __delitem__(self, key: str) -> None:
|
239
239
|
"""
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import gzip
|
2
|
+
import base64
|
3
|
+
|
4
|
+
from .mock import MockTransformer
|
5
|
+
|
6
|
+
|
7
|
+
class GzipTransformer(MockTransformer):
|
8
|
+
"""
|
9
|
+
Compresses and decompresses text using Gzip.
|
10
|
+
|
11
|
+
The compressed data is Base64 encoded to ensure it can be represented as a string.
|
12
|
+
"""
|
13
|
+
def __init__(self) -> None:
|
14
|
+
"""
|
15
|
+
Initializes the GzipTransformer. No parameters needed for basic Gzip.
|
16
|
+
"""
|
17
|
+
# No specific state needed for standard gzip compression/decompression
|
18
|
+
pass
|
19
|
+
|
20
|
+
def forward(self, text: str) -> str:
|
21
|
+
"""
|
22
|
+
Compresses the input text using Gzip and returns a Base64 encoded string.
|
23
|
+
:param text: The original text string.
|
24
|
+
:return: A Base64 encoded string representing the Gzipped data.
|
25
|
+
"""
|
26
|
+
byte_data = text.encode('utf-8')
|
27
|
+
compressed_bytes = gzip.compress(byte_data)
|
28
|
+
base64_bytes = base64.b64encode(compressed_bytes)
|
29
|
+
base64_string = base64_bytes.decode('ascii')
|
30
|
+
return base64_string
|
31
|
+
|
32
|
+
def backward(self, text: str) -> str:
|
33
|
+
"""
|
34
|
+
Decompresses the Base64 encoded Gzip data back into the original text.
|
35
|
+
:param text: A Base64 encoded string representing Gzipped data.
|
36
|
+
:return: The original text string.
|
37
|
+
:raises ValueError: If the input string is not valid Base64 or not valid Gzip data.
|
38
|
+
"""
|
39
|
+
try:
|
40
|
+
base64_bytes = text.encode('ascii')
|
41
|
+
compressed_bytes = base64.b64decode(base64_bytes)
|
42
|
+
original_bytes = gzip.decompress(compressed_bytes)
|
43
|
+
original_text = original_bytes.decode('utf-8')
|
44
|
+
return original_text
|
45
|
+
except (gzip.BadGzipFile, UnicodeDecodeError) as e:
|
46
|
+
# Catch errors related to Base64 decoding, Gzip decompression, or UTF-8 decoding
|
47
|
+
raise ValueError(f"Invalid input data for Gzip decompression: {e}") from e
|
48
|
+
|
49
|
+
@property
|
50
|
+
def static(self) -> bool:
|
51
|
+
return False
|
52
|
+
|
53
|
+
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dictature
|
3
|
-
Version: 0.9.
|
3
|
+
Version: 0.9.6
|
4
4
|
Summary: dictature -- A generic wrapper around dict-like interface with mulitple backends
|
5
5
|
Author-email: Adam Hlavacek <git@adamhlavacek.com>
|
6
6
|
Project-URL: Homepage, https://github.com/esoadamo/dictature
|
@@ -78,5 +78,6 @@ dictionary = Dictature(
|
|
78
78
|
Currently, the following transformers are supported:
|
79
79
|
- `AESTransformer`: encrypts/decrypts the data using AES
|
80
80
|
- `HmacTransformer`: signs the data using HMAC or performs hash integrity checks
|
81
|
+
- `GzipTransformer`: compresses given data
|
81
82
|
- `PassthroughTransformer`: does nothing
|
82
83
|
- `PipelineTransformer`: chains multiple transformers
|
@@ -1,18 +1,19 @@
|
|
1
1
|
dictature/__init__.py,sha256=UCPJKHeyirRZ0pCYoyeat-rwXa8pDezOJ3UWCipDdyc,33
|
2
|
-
dictature/dictature.py,sha256=
|
2
|
+
dictature/dictature.py,sha256=SHwG_XvGwm6qpvMt5OjeS8BoU5wLgJi_4jIBKFmYFyI,9330
|
3
3
|
dictature/backend/__init__.py,sha256=d5s6QCJOUzFglVNg8Cqqx_8b61S-AOTGjEUIF6FS69U,149
|
4
|
-
dictature/backend/directory.py,sha256=
|
5
|
-
dictature/backend/misp.py,sha256=
|
6
|
-
dictature/backend/mock.py,sha256=
|
4
|
+
dictature/backend/directory.py,sha256=YuwZbWxVKOuL_Aup-EHyK96MXLaD91xzIaXj6nv5WpA,3280
|
5
|
+
dictature/backend/misp.py,sha256=iPjvgnJg6WveNP2wvgN7OK2vkX-SC9qYPrdoa9ahRT0,4411
|
6
|
+
dictature/backend/mock.py,sha256=BzfLstxkTIjk6mcMTdFKj8rSaFgIqn9-2Cyelslj8bY,5889
|
7
7
|
dictature/backend/sqlite.py,sha256=zyphYEeLY4eGuBCor16i80_-brdipMpXZ3_kONwErsE,5237
|
8
8
|
dictature/transformer/__init__.py,sha256=JIFJpXU6iB9hIUM8L7HL2o9Nqjm_YbMEuQBQC8ZJ6b4,124
|
9
9
|
dictature/transformer/aes.py,sha256=ZhC1dT9QpnziErkDLriWLgXDEFNGQW0KG4aqSN2AZpA,1926
|
10
|
+
dictature/transformer/gzip.py,sha256=pngvJQeALa-lv98VBeJ1Pl6_gaAfGcPXD9UR7PexrYA,1921
|
10
11
|
dictature/transformer/hmac.py,sha256=vURsB0HlzRPn_Vkl7lGmZV9OKempQuds8AanmadDxIo,834
|
11
12
|
dictature/transformer/mock.py,sha256=7zu65ZqUV_AVRaPSzNd73cVMXixXt31SeuX9OKZxaJQ,948
|
12
13
|
dictature/transformer/passthrough.py,sha256=Pt3N6G_Qh6HJ_q75ETL5nfAwYHLB-SjkVwUwbbbMik8,344
|
13
14
|
dictature/transformer/pipeline.py,sha256=OaQaJeJ5NpICetJe08r8ontqstsXGuW8jDbKw1zxYs4,842
|
14
|
-
dictature-0.9.
|
15
|
-
dictature-0.9.
|
16
|
-
dictature-0.9.
|
17
|
-
dictature-0.9.
|
18
|
-
dictature-0.9.
|
15
|
+
dictature-0.9.6.dist-info/LICENSE,sha256=n1U9DKr8sM5EY2QHcvxSGiKTDWUT8MyXsOC79w94MT0,1072
|
16
|
+
dictature-0.9.6.dist-info/METADATA,sha256=46tXjvOFmBe0AxHyBr-DYMteFN2TVWcXFHa-YMdfJlw,2869
|
17
|
+
dictature-0.9.6.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
18
|
+
dictature-0.9.6.dist-info/top_level.txt,sha256=-RO39WWCF44lqiXhSUcACVqbk6SkgReZTz7ZmHKH3-U,10
|
19
|
+
dictature-0.9.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|