dictature 0.9.3__py3-none-any.whl → 0.9.5__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 +16 -17
- dictature/backend/misp.py +109 -0
- dictature/backend/mock.py +119 -1
- dictature/backend/sqlite.py +4 -1
- dictature/dictature.py +133 -5
- dictature/transformer/aes.py +6 -0
- dictature/transformer/hmac.py +4 -0
- dictature/transformer/mock.py +16 -0
- dictature/transformer/passthrough.py +7 -0
- dictature/transformer/pipeline.py +4 -0
- {dictature-0.9.3.dist-info → dictature-0.9.5.dist-info}/METADATA +8 -1
- dictature-0.9.5.dist-info/RECORD +18 -0
- {dictature-0.9.3.dist-info → dictature-0.9.5.dist-info}/WHEEL +1 -1
- dictature-0.9.3.dist-info/RECORD +0 -17
- {dictature-0.9.3.dist-info → dictature-0.9.5.dist-info}/LICENSE +0 -0
- {dictature-0.9.3.dist-info → dictature-0.9.5.dist-info}/top_level.txt +0 -0
dictature/backend/directory.py
CHANGED
@@ -1,14 +1,17 @@
|
|
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
9
|
def __init__(self, directory: Union[Path, str], dir_prefix: str = 'db_') -> None:
|
10
|
+
"""
|
11
|
+
Create a new directory backend
|
12
|
+
:param directory: directory to store the data
|
13
|
+
:param dir_prefix: prefix for the directories of the tables
|
14
|
+
"""
|
12
15
|
if isinstance(directory, str):
|
13
16
|
directory = Path(directory)
|
14
17
|
self.__directory = directory
|
@@ -28,6 +31,7 @@ class DictatureTableDirectory(DictatureTableMock):
|
|
28
31
|
def __init__(self, path_root: Path, name: str, db_prefix: str, prefix: str = 'item_') -> None:
|
29
32
|
self.__path = path_root / (db_prefix + self._filename_encode(name, suffix=''))
|
30
33
|
self.__prefix = prefix
|
34
|
+
self.__serializer = ValueSerializer(mode=ValueSerializerMode.filename_only)
|
31
35
|
|
32
36
|
def keys(self) -> Iterable[str]:
|
33
37
|
for child in self.__path.iterdir():
|
@@ -44,8 +48,7 @@ class DictatureTableDirectory(DictatureTableMock):
|
|
44
48
|
file_target = self.__item_path(item)
|
45
49
|
file_target_tmp = file_target.with_suffix('.tmp')
|
46
50
|
|
47
|
-
|
48
|
-
save_data = dumps({'value': value.value, 'mode': value.mode}, indent=1) if save_as_json else value.value
|
51
|
+
save_data = self.__serializer.serialize(value)
|
49
52
|
|
50
53
|
file_target_tmp.write_text(save_data)
|
51
54
|
file_target_tmp.rename(file_target)
|
@@ -53,10 +56,7 @@ class DictatureTableDirectory(DictatureTableMock):
|
|
53
56
|
def get(self, item: str) -> Value:
|
54
57
|
try:
|
55
58
|
save_data = self.__item_path(item).read_text()
|
56
|
-
|
57
|
-
data = loads(save_data)
|
58
|
-
return Value(data['value'], data['mode'])
|
59
|
-
return Value(save_data, ValueMode.string.value)
|
59
|
+
return self.__serializer.deserialize(save_data)
|
60
60
|
except FileNotFoundError:
|
61
61
|
raise KeyError(item)
|
62
62
|
|
@@ -69,14 +69,13 @@ class DictatureTableDirectory(DictatureTableMock):
|
|
69
69
|
|
70
70
|
@staticmethod
|
71
71
|
def _filename_encode(name: str, suffix: str = '.txt') -> str:
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
72
|
+
return ValueSerializer(mode=ValueSerializerMode.filename_only).serialize(Value(
|
73
|
+
value=name,
|
74
|
+
mode=ValueMode.string.value
|
75
|
+
)) + suffix
|
76
76
|
|
77
77
|
@staticmethod
|
78
78
|
def _filename_decode(name: str, suffix: str = '.txt') -> str:
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
return bytes.fromhex(encoded_name).decode('utf-8')
|
79
|
+
if suffix:
|
80
|
+
name = name[:-len(suffix)]
|
81
|
+
return ValueSerializer(mode=ValueSerializerMode.filename_only).deserialize(name).value
|
@@ -0,0 +1,109 @@
|
|
1
|
+
from typing import Iterable, Optional
|
2
|
+
|
3
|
+
from .mock import DictatureTableMock, DictatureBackendMock, Value, ValueMode, ValueSerializer, ValueSerializerMode
|
4
|
+
|
5
|
+
try:
|
6
|
+
from pymisp import PyMISP, MISPEvent, MISPAttribute
|
7
|
+
except ImportError as e:
|
8
|
+
raise ImportError("Please install the 'pymisp' package to use the 'DictatureBackendMISP' backend.") from e
|
9
|
+
|
10
|
+
|
11
|
+
class DictatureBackendMISP(DictatureBackendMock):
|
12
|
+
def __init__(self, misp: PyMISP, tag_name: str = 'storage:dictature', prefix: str = 'Dictature storage: ') -> None:
|
13
|
+
"""
|
14
|
+
Create a new MISP backend
|
15
|
+
:param misp: PyMISP instance
|
16
|
+
:param tag_name: tag name to use for the tables
|
17
|
+
:param prefix: prefix for the event names
|
18
|
+
"""
|
19
|
+
self.__misp = misp
|
20
|
+
self.__tag_name = tag_name
|
21
|
+
self.__prefix = prefix
|
22
|
+
|
23
|
+
def keys(self) -> Iterable[str]:
|
24
|
+
for event in self.__misp.search(tags=[self.__tag_name], pythonify=True):
|
25
|
+
name = event.info
|
26
|
+
if not name.startswith(self.__prefix):
|
27
|
+
continue
|
28
|
+
yield name[len(self.__prefix):]
|
29
|
+
|
30
|
+
def table(self, name: str) -> 'DictatureTableMock':
|
31
|
+
return DictatureTableMISP(self.__misp, self.__prefix + name, self.__tag_name)
|
32
|
+
|
33
|
+
|
34
|
+
class DictatureTableMISP(DictatureTableMock):
|
35
|
+
def __init__(self, misp: PyMISP, event_description: str, tag: str) -> None:
|
36
|
+
self.__misp = misp
|
37
|
+
self.__event_description = event_description
|
38
|
+
self.__tag = tag
|
39
|
+
self.__event: Optional[MISPEvent] = None
|
40
|
+
self.__serializer = ValueSerializer(mode=ValueSerializerMode.ascii_only)
|
41
|
+
|
42
|
+
def keys(self) -> Iterable[str]:
|
43
|
+
for attribute in self.__event_attributes():
|
44
|
+
yield attribute.value
|
45
|
+
|
46
|
+
def drop(self) -> None:
|
47
|
+
self.__misp.delete_event(self.__get_event())
|
48
|
+
|
49
|
+
def create(self) -> None:
|
50
|
+
self.__get_event()
|
51
|
+
|
52
|
+
def set(self, item: str, value: Value) -> None:
|
53
|
+
item_name = self.__serializer.serialize(Value(value=item, mode=ValueMode.string.value))
|
54
|
+
save_data = self.__serializer.serialize(value)
|
55
|
+
|
56
|
+
for attribute in self.__event_attributes():
|
57
|
+
if attribute.value == item_name:
|
58
|
+
attribute.value = item_name
|
59
|
+
attribute.comment = save_data
|
60
|
+
self.__misp.update_attribute(attribute)
|
61
|
+
break
|
62
|
+
else:
|
63
|
+
attribute = MISPAttribute()
|
64
|
+
attribute.value = item_name
|
65
|
+
attribute.comment = save_data
|
66
|
+
attribute.type = 'comment'
|
67
|
+
attribute.to_ids = False
|
68
|
+
attribute.disable_correlation = True
|
69
|
+
self.__misp.add_attribute(self.__get_event(), attribute)
|
70
|
+
self.__get_event().attributes.append(attribute)
|
71
|
+
|
72
|
+
def get(self, item: str) -> Value:
|
73
|
+
item_name = self.__serializer.serialize(Value(value=item, mode=ValueMode.string.value))
|
74
|
+
for attribute in self.__event_attributes():
|
75
|
+
if attribute.value == item_name:
|
76
|
+
return self.__serializer.deserialize(attribute.comment)
|
77
|
+
raise KeyError(item)
|
78
|
+
|
79
|
+
def delete(self, item: str) -> None:
|
80
|
+
for attribute in self.__event_attributes():
|
81
|
+
if attribute.value == item:
|
82
|
+
# First update the attribute as deletion is not recognized immediately
|
83
|
+
attribute.type = 'other'
|
84
|
+
self.__misp.update_attribute(attribute)
|
85
|
+
self.__misp.delete_attribute(attribute)
|
86
|
+
break
|
87
|
+
|
88
|
+
def __get_event(self) -> MISPEvent:
|
89
|
+
if self.__event is None:
|
90
|
+
for event in self.__misp.search(tags=[self.__tag], eventinfo=self.__event_description, pythonify=True):
|
91
|
+
if event.info == self.__event_description:
|
92
|
+
self.__event = event
|
93
|
+
break
|
94
|
+
else:
|
95
|
+
event = MISPEvent()
|
96
|
+
event.info = self.__event_description
|
97
|
+
event.distribution = 0
|
98
|
+
event.threat_level_id = 4
|
99
|
+
event.analysis = 0
|
100
|
+
event.add_tag(self.__tag)
|
101
|
+
self.__misp.add_event(event)
|
102
|
+
self.__event = event
|
103
|
+
return self.__event
|
104
|
+
|
105
|
+
def __event_attributes(self) -> Iterable[MISPAttribute]:
|
106
|
+
for attribute in self.__get_event().attributes:
|
107
|
+
if attribute.type != 'comment' or (hasattr(attribute, 'deleted') and attribute.deleted):
|
108
|
+
continue
|
109
|
+
yield attribute
|
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,29 +15,145 @@ 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]:
|
98
|
+
"""
|
99
|
+
Return all table names
|
100
|
+
:return: all table names
|
101
|
+
"""
|
18
102
|
raise NotImplementedError("This method should be implemented by the subclass")
|
19
103
|
|
20
104
|
def table(self, name: str) -> 'DictatureTableMock':
|
105
|
+
"""
|
106
|
+
Create a table object based on the name
|
107
|
+
:param name: name of the table
|
108
|
+
:return: table object
|
109
|
+
"""
|
21
110
|
raise NotImplementedError("This method should be implemented by the subclass")
|
22
111
|
|
23
112
|
|
24
113
|
class DictatureTableMock:
|
25
114
|
def keys(self) -> Iterable[str]:
|
115
|
+
"""
|
116
|
+
Return all keys in the table
|
117
|
+
:return: all keys in the table
|
118
|
+
"""
|
26
119
|
raise NotImplementedError("This method should be implemented by the subclass")
|
27
120
|
|
28
121
|
def drop(self) -> None:
|
122
|
+
"""
|
123
|
+
Delete the table
|
124
|
+
:return: None
|
125
|
+
"""
|
29
126
|
raise NotImplementedError("This method should be implemented by the subclass")
|
30
127
|
|
31
128
|
def create(self) -> None:
|
129
|
+
"""
|
130
|
+
Create the table in the backend
|
131
|
+
:return: None
|
132
|
+
"""
|
32
133
|
raise NotImplementedError("This method should be implemented by the subclass")
|
33
134
|
|
34
135
|
def set(self, item: str, value: Value) -> None:
|
136
|
+
"""
|
137
|
+
Set a value in the table
|
138
|
+
:param item: key to set
|
139
|
+
:param value: value to set
|
140
|
+
:return: None
|
141
|
+
"""
|
35
142
|
raise NotImplementedError("This method should be implemented by the subclass")
|
36
143
|
|
37
144
|
def get(self, item: str) -> Value:
|
145
|
+
"""
|
146
|
+
Get a value from the table
|
147
|
+
:param item: key to get
|
148
|
+
:return: value
|
149
|
+
:raises KeyError: if the key does not exist
|
150
|
+
"""
|
38
151
|
raise NotImplementedError("This method should be implemented by the subclass")
|
39
152
|
|
40
153
|
def delete(self, item: str) -> None:
|
41
|
-
|
154
|
+
"""
|
155
|
+
Delete a value from the table
|
156
|
+
:param item: key to delete
|
157
|
+
:return: None
|
158
|
+
"""
|
159
|
+
raise NotImplementedError("This method should be implemented by the subclass")
|
dictature/backend/sqlite.py
CHANGED
@@ -7,6 +7,10 @@ from .mock import DictatureBackendMock, DictatureTableMock, Value, ValueMode
|
|
7
7
|
|
8
8
|
class DictatureBackendSQLite(DictatureBackendMock):
|
9
9
|
def __init__(self, file: Union[str, Path]) -> None:
|
10
|
+
"""
|
11
|
+
Create a new SQLite backend
|
12
|
+
:param file: file to store the database
|
13
|
+
"""
|
10
14
|
if isinstance(file, str):
|
11
15
|
file = Path(file)
|
12
16
|
self.__file = file
|
@@ -33,7 +37,6 @@ class DictatureBackendSQLite(DictatureBackendMock):
|
|
33
37
|
self.__connection.close()
|
34
38
|
|
35
39
|
|
36
|
-
|
37
40
|
class DictatureTableSQLite(DictatureTableMock):
|
38
41
|
def __init__(self, parent: "DictatureBackendSQLite", name: str) -> None:
|
39
42
|
self.__parent = parent
|
dictature/dictature.py
CHANGED
@@ -17,30 +17,63 @@ class Dictature:
|
|
17
17
|
value_transformer: MockTransformer = PassthroughTransformer(),
|
18
18
|
table_name_transformer: Optional[MockTransformer] = None,
|
19
19
|
) -> None:
|
20
|
+
"""
|
21
|
+
Create a new Dictature object
|
22
|
+
:param backend: backend to use
|
23
|
+
:param name_transformer: transformer to use for table and key names
|
24
|
+
:param value_transformer: transformer to use for values
|
25
|
+
:param table_name_transformer: transformer to use for table names, if None, name_transformer is used
|
26
|
+
"""
|
20
27
|
self.__backend = backend
|
21
28
|
self.__table_cache: Dict[str, "DictatureTable"] = {}
|
22
29
|
self.__name_transformer = name_transformer
|
23
30
|
self.__value_transformer = value_transformer
|
24
31
|
self.__table_name_transformer = table_name_transformer or name_transformer
|
32
|
+
self.__cache_size = 4096
|
25
33
|
|
26
34
|
def keys(self) -> Set[str]:
|
35
|
+
"""
|
36
|
+
Return all table names
|
37
|
+
:return: all table names
|
38
|
+
"""
|
27
39
|
return set(map(self.__name_transformer.backward, self.__backend.keys()))
|
28
40
|
|
29
41
|
def values(self) -> Iterator["DictatureTable"]:
|
42
|
+
"""
|
43
|
+
Return all tables
|
44
|
+
:return: all tables
|
45
|
+
"""
|
30
46
|
return map(lambda x: x[1], self.items())
|
31
47
|
|
32
48
|
def items(self) -> Iterator[Tuple[str, "DictatureTable"]]:
|
49
|
+
"""
|
50
|
+
Return all tables with their instances
|
51
|
+
:return: all tables with their instances
|
52
|
+
"""
|
33
53
|
for k in self.keys():
|
34
54
|
yield k, self[k]
|
35
55
|
|
36
56
|
def to_dict(self) -> Dict[str, Any]:
|
57
|
+
"""
|
58
|
+
Return all tables as a dictionary
|
59
|
+
:return: all tables as a dictionary
|
60
|
+
"""
|
37
61
|
return {k: v.to_dict() for k, v in self.items()}
|
38
62
|
|
39
63
|
def __str__(self):
|
64
|
+
"""
|
65
|
+
Return all tables as a string
|
66
|
+
:return: all tables as a string
|
67
|
+
"""
|
40
68
|
return str(self.to_dict())
|
41
69
|
|
42
70
|
def __getitem__(self, item: str) -> "DictatureTable":
|
43
|
-
|
71
|
+
"""
|
72
|
+
Get a table by name
|
73
|
+
:param item: name of the table
|
74
|
+
:return: table instance
|
75
|
+
"""
|
76
|
+
if len(self.__table_cache) > self.__cache_size:
|
44
77
|
del self.__table_cache[choice(list(self.__table_cache.keys()))]
|
45
78
|
if item not in self.__table_cache:
|
46
79
|
self.__table_cache[item] = DictatureTable(
|
@@ -52,12 +85,26 @@ class Dictature:
|
|
52
85
|
return self.__table_cache[item]
|
53
86
|
|
54
87
|
def __delitem__(self, key: str) -> None:
|
88
|
+
"""
|
89
|
+
Delete a table
|
90
|
+
:param key: name of the table
|
91
|
+
:return: None
|
92
|
+
"""
|
55
93
|
self[key].drop()
|
56
94
|
|
57
95
|
def __contains__(self, item: str) -> bool:
|
96
|
+
"""
|
97
|
+
Check if a table exists
|
98
|
+
:param item: name of the table
|
99
|
+
:return: True if the table exists, False otherwise
|
100
|
+
"""
|
58
101
|
return item in self.keys()
|
59
102
|
|
60
103
|
def __bool__(self) -> bool:
|
104
|
+
"""
|
105
|
+
Check if there are any tables
|
106
|
+
:return: True if there are tables, False otherwise
|
107
|
+
"""
|
61
108
|
return not not self.keys()
|
62
109
|
|
63
110
|
|
@@ -69,6 +116,13 @@ class DictatureTable:
|
|
69
116
|
name_transformer: MockTransformer = PassthroughTransformer(),
|
70
117
|
value_transformer: MockTransformer = PassthroughTransformer()
|
71
118
|
):
|
119
|
+
"""
|
120
|
+
Create a new DictatureTable object
|
121
|
+
:param backend: backend to use
|
122
|
+
:param table_name: name of the table
|
123
|
+
:param name_transformer: transformer to use for key names
|
124
|
+
:param value_transformer: transformer to use for values
|
125
|
+
"""
|
72
126
|
self.__backend = backend
|
73
127
|
self.__name_transformer = name_transformer
|
74
128
|
self.__value_transformer = value_transformer
|
@@ -76,37 +130,77 @@ class DictatureTable:
|
|
76
130
|
self.__table_created = False
|
77
131
|
|
78
132
|
def get(self, item: str, default: Optional[Any] = None) -> Any:
|
133
|
+
"""
|
134
|
+
Get a value from the table
|
135
|
+
:param item: key to get
|
136
|
+
:param default: default value to return if the key does not exist
|
137
|
+
:return: value or default
|
138
|
+
"""
|
79
139
|
try:
|
80
140
|
return self[item]
|
81
141
|
except KeyError:
|
82
142
|
return default
|
83
143
|
|
84
144
|
def key_exists(self, item: str) -> bool:
|
145
|
+
"""
|
146
|
+
Check if a key exists
|
147
|
+
:param item: key to check
|
148
|
+
:return: True if the key exists, False otherwise
|
149
|
+
"""
|
85
150
|
self.__create_table()
|
86
151
|
return item in self.keys()
|
87
152
|
|
88
153
|
def keys(self) -> Set[str]:
|
154
|
+
"""
|
155
|
+
Return all keys in the table
|
156
|
+
:return: all keys in the table
|
157
|
+
"""
|
89
158
|
self.__create_table()
|
90
159
|
return set(map(self.__name_transformer.backward, self.__table.keys()))
|
91
160
|
|
92
161
|
def values(self) -> Iterator[Any]:
|
162
|
+
"""
|
163
|
+
Return all values in the table
|
164
|
+
:return: all values in the table
|
165
|
+
"""
|
93
166
|
return map(lambda x: x[1], self.items())
|
94
167
|
|
95
168
|
def items(self) -> Iterator[Tuple[str, Any]]:
|
169
|
+
"""
|
170
|
+
Return all items in the table
|
171
|
+
:return: all items in the table
|
172
|
+
"""
|
96
173
|
for k in self.keys():
|
97
174
|
yield k, self[k]
|
98
175
|
|
99
176
|
def drop(self) -> None:
|
177
|
+
"""
|
178
|
+
Delete the table
|
179
|
+
:return: None
|
180
|
+
"""
|
100
181
|
self.__create_table()
|
101
182
|
self.__table.drop()
|
102
183
|
|
103
184
|
def to_dict(self) -> Dict[str, Any]:
|
185
|
+
"""
|
186
|
+
Return all items as a dictionary
|
187
|
+
:return: all items as a dictionary
|
188
|
+
"""
|
104
189
|
return {k: v for k, v in self.items()}
|
105
190
|
|
106
191
|
def __str__(self):
|
192
|
+
"""
|
193
|
+
Return all items as a string
|
194
|
+
:return: all items as a string
|
195
|
+
"""
|
107
196
|
return str(self.to_dict())
|
108
197
|
|
109
198
|
def __getitem__(self, item: str) -> Any:
|
199
|
+
"""
|
200
|
+
Get a value from the table
|
201
|
+
:param item: key to get
|
202
|
+
:return: value
|
203
|
+
"""
|
110
204
|
self.__create_table()
|
111
205
|
saved_value = self.__table.get(self.__item_key(item))
|
112
206
|
mode = ValueMode(saved_value.mode)
|
@@ -120,37 +214,66 @@ class DictatureTable:
|
|
120
214
|
raise ValueError(f"Unknown mode '{mode}'")
|
121
215
|
|
122
216
|
def __setitem__(self, key: str, value: Any) -> None:
|
217
|
+
"""
|
218
|
+
Set a value in the table
|
219
|
+
:param key: key to set
|
220
|
+
:param value: value to set
|
221
|
+
:return: None
|
222
|
+
"""
|
123
223
|
self.__create_table()
|
124
|
-
value_mode = ValueMode.string
|
224
|
+
value_mode: int = ValueMode.string.value
|
125
225
|
|
126
226
|
if type(value) is not str:
|
127
227
|
try:
|
128
228
|
value = json.dumps(value)
|
129
|
-
value_mode = ValueMode.json
|
229
|
+
value_mode = ValueMode.json.value
|
130
230
|
except TypeError:
|
131
231
|
value = b64encode(compress(pickle.dumps(value))).decode('ascii')
|
132
|
-
value_mode =
|
232
|
+
value_mode = ValueMode.pickle.value
|
133
233
|
|
134
234
|
key = self.__item_key(key)
|
135
235
|
value = self.__value_transformer.forward(value)
|
136
|
-
self.__table.set(key, Value(value=value, mode=value_mode
|
236
|
+
self.__table.set(key, Value(value=value, mode=value_mode))
|
137
237
|
|
138
238
|
def __delitem__(self, key: str) -> None:
|
239
|
+
"""
|
240
|
+
Delete a key from the table
|
241
|
+
:param key: key to delete
|
242
|
+
:return: None
|
243
|
+
"""
|
139
244
|
self.__table.delete(self.__item_key(key))
|
140
245
|
|
141
246
|
def __contains__(self, item: str):
|
247
|
+
"""
|
248
|
+
Check if a key exists
|
249
|
+
:param item: key to check
|
250
|
+
:return: True if the key exists, False otherwise
|
251
|
+
"""
|
142
252
|
return item in self.keys()
|
143
253
|
|
144
254
|
def __bool__(self) -> bool:
|
255
|
+
"""
|
256
|
+
Check if there are any items in the table
|
257
|
+
:return: True if there are items, False otherwise
|
258
|
+
"""
|
145
259
|
return not not self.keys()
|
146
260
|
|
147
261
|
def __create_table(self) -> None:
|
262
|
+
"""
|
263
|
+
Create the table if it does not exist
|
264
|
+
:return: None
|
265
|
+
"""
|
148
266
|
if self.__table_created:
|
149
267
|
return
|
150
268
|
self.__table.create()
|
151
269
|
self.__table_created = True
|
152
270
|
|
153
271
|
def __item_key(self, item: str) -> str:
|
272
|
+
"""
|
273
|
+
Transform the key for storage
|
274
|
+
:param item: key to transform
|
275
|
+
:return: transformed key
|
276
|
+
"""
|
154
277
|
if not self.__name_transformer.static:
|
155
278
|
for key in self.__table.keys():
|
156
279
|
if self.__name_transformer.backward(key) == item:
|
@@ -158,6 +281,11 @@ class DictatureTable:
|
|
158
281
|
return self.__name_transformer.forward(item)
|
159
282
|
|
160
283
|
def __table_key(self, table_name: str) -> str:
|
284
|
+
"""
|
285
|
+
Transform the table name for storage
|
286
|
+
:param table_name: table name to transform
|
287
|
+
:return: transformed table name
|
288
|
+
"""
|
161
289
|
if not self.__name_transformer.static:
|
162
290
|
for key in self.__backend.keys():
|
163
291
|
if self.__name_transformer.backward(key) == table_name:
|
dictature/transformer/aes.py
CHANGED
@@ -10,6 +10,12 @@ from .mock import MockTransformer
|
|
10
10
|
|
11
11
|
class AESTransformer(MockTransformer):
|
12
12
|
def __init__(self, passphrase: str, static_names_mode: bool, salt: str = 'dictature') -> None:
|
13
|
+
"""
|
14
|
+
Create a new AES transformer
|
15
|
+
:param passphrase: secret passphrase to encrypt/decrypt the data
|
16
|
+
:param static_names_mode: if True, the transformer will use ECB mode instead of GCM (True decreases security, increases speed)
|
17
|
+
:param salt: salt to use for the key derivation
|
18
|
+
"""
|
13
19
|
self.__key = scrypt(passphrase, salt, 16, N=2 ** 14, r=8, p=1)
|
14
20
|
self.__mode = AES.MODE_GCM if not static_names_mode else AES.MODE_ECB
|
15
21
|
self.__static = static_names_mode
|
dictature/transformer/hmac.py
CHANGED
@@ -5,6 +5,10 @@ from .mock import MockTransformer
|
|
5
5
|
|
6
6
|
class HmacTransformer(MockTransformer):
|
7
7
|
def __init__(self, secret: str = 'dictature') -> None:
|
8
|
+
"""
|
9
|
+
Perform HMAC on the text.
|
10
|
+
:param secret: secret key to use for HMAC, if not provided works as a simple hash function
|
11
|
+
"""
|
8
12
|
self.__secret = secret
|
9
13
|
|
10
14
|
def forward(self, text: str) -> str:
|
dictature/transformer/mock.py
CHANGED
@@ -1,9 +1,25 @@
|
|
1
1
|
|
2
2
|
class MockTransformer:
|
3
3
|
def forward(self, text: str) -> str:
|
4
|
+
"""
|
5
|
+
Transform the text in some way to the data format in data storage
|
6
|
+
:param text: text to transform
|
7
|
+
:return: transformed text
|
8
|
+
"""
|
4
9
|
raise NotImplementedError("This method should be implemented by the child class")
|
10
|
+
|
5
11
|
def backward(self, text: str) -> str:
|
12
|
+
"""
|
13
|
+
Transform the data format in data storage to the text
|
14
|
+
:param text: text to transform
|
15
|
+
:return: original text
|
16
|
+
"""
|
6
17
|
raise NotImplementedError("This method should be implemented by the child class")
|
18
|
+
|
7
19
|
@property
|
8
20
|
def static(self) -> bool:
|
21
|
+
"""
|
22
|
+
Returns True only if when the forward transformation is applied to the same text, the result is always the same
|
23
|
+
:return: True if the transformation is static
|
24
|
+
"""
|
9
25
|
raise NotImplementedError("This method should be implemented by the child class")
|
@@ -1,10 +1,17 @@
|
|
1
1
|
from .mock import MockTransformer
|
2
2
|
|
3
|
+
|
3
4
|
class PassthroughTransformer(MockTransformer):
|
5
|
+
"""
|
6
|
+
Passthrough transformer, does not modify the text.
|
7
|
+
"""
|
8
|
+
|
4
9
|
def forward(self, text: str) -> str:
|
5
10
|
return text
|
11
|
+
|
6
12
|
def backward(self, text: str) -> str:
|
7
13
|
return text
|
14
|
+
|
8
15
|
@property
|
9
16
|
def static(self) -> bool:
|
10
17
|
return True
|
@@ -5,6 +5,10 @@ from .mock import MockTransformer
|
|
5
5
|
|
6
6
|
class PipelineTransformer(MockTransformer):
|
7
7
|
def __init__(self, transformers: List[MockTransformer]) -> None:
|
8
|
+
"""
|
9
|
+
Create a pipeline of transformers. The text is passed through each transformer in the order they are provided.
|
10
|
+
:param transformers: list of transformers to use
|
11
|
+
"""
|
8
12
|
self.__transformers = transformers
|
9
13
|
|
10
14
|
def forward(self, text: str) -> str:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dictature
|
3
|
-
Version: 0.9.
|
3
|
+
Version: 0.9.5
|
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
|
@@ -53,6 +53,7 @@ del dictionary['test'] # drops whole table
|
|
53
53
|
Currently, the following backends are supported:
|
54
54
|
- `DictatureBackendDirectory`: stores the data in a directory as json files
|
55
55
|
- `DictatureBackendSQLite`: stores the data in a SQLite database
|
56
|
+
- `DictatureBackendMISP`: stores the data in a MISP instance
|
56
57
|
|
57
58
|
### Transformers
|
58
59
|
|
@@ -73,3 +74,9 @@ dictionary = Dictature(
|
|
73
74
|
value_transformer=value_transformer
|
74
75
|
)
|
75
76
|
```
|
77
|
+
|
78
|
+
Currently, the following transformers are supported:
|
79
|
+
- `AESTransformer`: encrypts/decrypts the data using AES
|
80
|
+
- `HmacTransformer`: signs the data using HMAC or performs hash integrity checks
|
81
|
+
- `PassthroughTransformer`: does nothing
|
82
|
+
- `PipelineTransformer`: chains multiple transformers
|
@@ -0,0 +1,18 @@
|
|
1
|
+
dictature/__init__.py,sha256=UCPJKHeyirRZ0pCYoyeat-rwXa8pDezOJ3UWCipDdyc,33
|
2
|
+
dictature/dictature.py,sha256=SHwG_XvGwm6qpvMt5OjeS8BoU5wLgJi_4jIBKFmYFyI,9330
|
3
|
+
dictature/backend/__init__.py,sha256=d5s6QCJOUzFglVNg8Cqqx_8b61S-AOTGjEUIF6FS69U,149
|
4
|
+
dictature/backend/directory.py,sha256=pVXzwswxu9I38tiWhpqXmXhFFW57BzW_ScKHanOCPK0,3201
|
5
|
+
dictature/backend/misp.py,sha256=iPjvgnJg6WveNP2wvgN7OK2vkX-SC9qYPrdoa9ahRT0,4411
|
6
|
+
dictature/backend/mock.py,sha256=BzfLstxkTIjk6mcMTdFKj8rSaFgIqn9-2Cyelslj8bY,5889
|
7
|
+
dictature/backend/sqlite.py,sha256=zyphYEeLY4eGuBCor16i80_-brdipMpXZ3_kONwErsE,5237
|
8
|
+
dictature/transformer/__init__.py,sha256=JIFJpXU6iB9hIUM8L7HL2o9Nqjm_YbMEuQBQC8ZJ6b4,124
|
9
|
+
dictature/transformer/aes.py,sha256=ZhC1dT9QpnziErkDLriWLgXDEFNGQW0KG4aqSN2AZpA,1926
|
10
|
+
dictature/transformer/hmac.py,sha256=vURsB0HlzRPn_Vkl7lGmZV9OKempQuds8AanmadDxIo,834
|
11
|
+
dictature/transformer/mock.py,sha256=7zu65ZqUV_AVRaPSzNd73cVMXixXt31SeuX9OKZxaJQ,948
|
12
|
+
dictature/transformer/passthrough.py,sha256=Pt3N6G_Qh6HJ_q75ETL5nfAwYHLB-SjkVwUwbbbMik8,344
|
13
|
+
dictature/transformer/pipeline.py,sha256=OaQaJeJ5NpICetJe08r8ontqstsXGuW8jDbKw1zxYs4,842
|
14
|
+
dictature-0.9.5.dist-info/LICENSE,sha256=n1U9DKr8sM5EY2QHcvxSGiKTDWUT8MyXsOC79w94MT0,1072
|
15
|
+
dictature-0.9.5.dist-info/METADATA,sha256=1qIyWXXTEogMI-mJ98ra_DCpUhU3soP8UNiV74zA8ek,2826
|
16
|
+
dictature-0.9.5.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
|
17
|
+
dictature-0.9.5.dist-info/top_level.txt,sha256=-RO39WWCF44lqiXhSUcACVqbk6SkgReZTz7ZmHKH3-U,10
|
18
|
+
dictature-0.9.5.dist-info/RECORD,,
|
dictature-0.9.3.dist-info/RECORD
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
dictature/__init__.py,sha256=UCPJKHeyirRZ0pCYoyeat-rwXa8pDezOJ3UWCipDdyc,33
|
2
|
-
dictature/dictature.py,sha256=eFjUc5Q1DUXLNqk-UeHzFqWgCZppTsoK3TXCJ1UuCS8,5666
|
3
|
-
dictature/backend/__init__.py,sha256=d5s6QCJOUzFglVNg8Cqqx_8b61S-AOTGjEUIF6FS69U,149
|
4
|
-
dictature/backend/directory.py,sha256=KVbKS1CibXmY1NsZRWuTE0uC4DXRLOXqYEHxyHJidCc,3266
|
5
|
-
dictature/backend/mock.py,sha256=Qd7KSh-qM763Jc7biDf5xYFWdgDax30dUHh2gXWwTZE,1266
|
6
|
-
dictature/backend/sqlite.py,sha256=aExNxDtx1kiPrZn-jfCzbpV4alEXyGc6f12tuCJK1tk,5130
|
7
|
-
dictature/transformer/__init__.py,sha256=JIFJpXU6iB9hIUM8L7HL2o9Nqjm_YbMEuQBQC8ZJ6b4,124
|
8
|
-
dictature/transformer/aes.py,sha256=6H3jNkUpgWBX88BduMzbi9MDSRxMHnWmZZEIJ70BLi0,1601
|
9
|
-
dictature/transformer/hmac.py,sha256=pYw6ybUIMoNdU2JFI9ffePr-33ails-CN9J6rFt7RVE,677
|
10
|
-
dictature/transformer/mock.py,sha256=osETvYZjlgos0trJy0YvXcmtNy0L6x2h2099t1aHMFc,421
|
11
|
-
dictature/transformer/passthrough.py,sha256=63hZCPQMUJa-G6ZKdv_xt2fMiMZpmPoL84PY5eb2ueE,269
|
12
|
-
dictature/transformer/pipeline.py,sha256=-2r9FxLXEnk3qpCfXC0qp0KqNC2qkpChCJYEbZAQRYM,642
|
13
|
-
dictature-0.9.3.dist-info/LICENSE,sha256=n1U9DKr8sM5EY2QHcvxSGiKTDWUT8MyXsOC79w94MT0,1072
|
14
|
-
dictature-0.9.3.dist-info/METADATA,sha256=BPA99McqtwhQxjALGLwrWvaZiqRx3XHnjYZ5tEfxYIk,2478
|
15
|
-
dictature-0.9.3.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
16
|
-
dictature-0.9.3.dist-info/top_level.txt,sha256=-RO39WWCF44lqiXhSUcACVqbk6SkgReZTz7ZmHKH3-U,10
|
17
|
-
dictature-0.9.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|