mal-toolbox 0.2.0__py3-none-any.whl → 0.3.1__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.
- {mal_toolbox-0.2.0.dist-info → mal_toolbox-0.3.1.dist-info}/METADATA +43 -25
- mal_toolbox-0.3.1.dist-info/RECORD +29 -0
- mal_toolbox-0.3.1.dist-info/entry_points.txt +2 -0
- maltoolbox/__init__.py +38 -57
- maltoolbox/__main__.py +43 -14
- maltoolbox/attackgraph/__init__.py +1 -1
- maltoolbox/attackgraph/analyzers/apriori.py +6 -5
- maltoolbox/attackgraph/attacker.py +26 -13
- maltoolbox/attackgraph/attackgraph.py +185 -153
- maltoolbox/attackgraph/node.py +56 -54
- maltoolbox/attackgraph/query.py +4 -2
- maltoolbox/file_utils.py +0 -8
- maltoolbox/ingestors/neo4j.py +146 -157
- maltoolbox/language/__init__.py +7 -3
- maltoolbox/language/compiler/__init__.py +485 -17
- maltoolbox/language/compiler/mal_lexer.py +172 -152
- maltoolbox/language/compiler/mal_parser.py +1370 -663
- maltoolbox/language/languagegraph.py +103 -99
- maltoolbox/model.py +306 -488
- maltoolbox/translators/securicad.py +164 -163
- maltoolbox/translators/updater.py +231 -108
- mal_toolbox-0.2.0.dist-info/RECORD +0 -32
- maltoolbox/default.conf +0 -17
- maltoolbox/language/classes_factory.py +0 -259
- maltoolbox/language/compiler/mal_visitor.py +0 -416
- maltoolbox/wrappers.py +0 -62
- {mal_toolbox-0.2.0.dist-info → mal_toolbox-0.3.1.dist-info}/AUTHORS +0 -0
- {mal_toolbox-0.2.0.dist-info → mal_toolbox-0.3.1.dist-info}/LICENSE +0 -0
- {mal_toolbox-0.2.0.dist-info → mal_toolbox-0.3.1.dist-info}/WHEEL +0 -0
- {mal_toolbox-0.2.0.dist-info → mal_toolbox-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -3,130 +3,253 @@ import logging
|
|
|
3
3
|
|
|
4
4
|
import yaml
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
from ..
|
|
6
|
+
import logging
|
|
7
|
+
from ..model import Model
|
|
8
|
+
from ..language import LanguageGraph
|
|
9
|
+
from ..file_utils import load_dict_from_json_file, load_dict_from_yaml_file
|
|
8
10
|
|
|
9
11
|
logger = logging.getLogger(__name__)
|
|
10
12
|
|
|
11
13
|
def load_model_from_older_version(
|
|
12
|
-
filename: str,
|
|
13
|
-
lang_classes_factory: LanguageClassesFactory,
|
|
14
|
-
version: str
|
|
14
|
+
filename: str, lang_graph: LanguageGraph,
|
|
15
15
|
) -> Model:
|
|
16
|
-
match (version):
|
|
17
|
-
case '0.0.39':
|
|
18
|
-
return load_model_from_version_0_0_39(filename,
|
|
19
|
-
lang_classes_factory)
|
|
20
|
-
case _:
|
|
21
|
-
msg = ('Unknown version "%s" format. Could not '
|
|
22
|
-
'load model from file "%s"')
|
|
23
|
-
logger.error(msg % (version, filename))
|
|
24
|
-
raise ValueError(msg % (version, filename))
|
|
25
16
|
|
|
26
|
-
|
|
27
|
-
filename: str,
|
|
28
|
-
lang_classes_factory: LanguageClassesFactory
|
|
29
|
-
) -> Model:
|
|
30
|
-
"""
|
|
31
|
-
Load model from file.
|
|
17
|
+
""" Load an older Model file
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
lang_classes_factory - the language classes factory that defines the
|
|
36
|
-
classes needed to build the model
|
|
19
|
+
Load an older model from given `filename` (yml/json)
|
|
20
|
+
convert the model to the new format and return a Model object.
|
|
37
21
|
"""
|
|
38
22
|
|
|
39
|
-
|
|
40
|
-
model = Model(model_dict['metadata']['name'], lang_classes_factory)
|
|
23
|
+
model_dict = load_model_dict_from_file(filename)
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
25
|
+
# Get the version of the model, default to 0.0
|
|
26
|
+
# since metadata was not given back then
|
|
27
|
+
version = model_dict.get(
|
|
28
|
+
'metadata', {}
|
|
29
|
+
).get('MAL-Toolbox Version', '0.0.')
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
match (version):
|
|
32
|
+
case x if '0.0.' in x:
|
|
33
|
+
model_dict = convert_model_dict_from_version_0_0(model_dict)
|
|
34
|
+
model_dict = convert_model_dict_from_version_0_1(model_dict)
|
|
35
|
+
model_dict = convert_model_dict_from_version_0_2(model_dict)
|
|
36
|
+
case x if '0.1.' in x:
|
|
37
|
+
model_dict = convert_model_dict_from_version_0_1(model_dict)
|
|
38
|
+
model_dict = convert_model_dict_from_version_0_2(model_dict)
|
|
39
|
+
case x if '0.2.' in x:
|
|
40
|
+
model_dict = convert_model_dict_from_version_0_2(model_dict)
|
|
41
|
+
case _:
|
|
42
|
+
msg = (
|
|
43
|
+
'Unknown version "%s" format.'
|
|
44
|
+
'Could not load model from file "%s"'
|
|
51
45
|
)
|
|
46
|
+
logger.error(msg, version, filename)
|
|
47
|
+
raise ValueError(msg % (version, filename))
|
|
48
|
+
|
|
49
|
+
# TODO: _from_dict should be public
|
|
50
|
+
return Model._from_dict(model_dict, lang_graph)
|
|
51
|
+
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
model.add_asset(asset, asset_id = int(asset_id))
|
|
60
|
-
|
|
61
|
-
# Reconstruct the associations
|
|
62
|
-
for assoc_dict in model_dict.get('associations', []):
|
|
63
|
-
association = getattr(model.lang_classes_factory.ns, assoc_dict.pop('metaconcept'))()
|
|
64
|
-
|
|
65
|
-
# compatibility with old format
|
|
66
|
-
assoc_dict = assoc_dict.get('association', assoc_dict)
|
|
67
|
-
|
|
68
|
-
for field, targets in assoc_dict.items():
|
|
69
|
-
targets = targets if isinstance(targets, list) else [targets]
|
|
70
|
-
setattr(
|
|
71
|
-
association,
|
|
72
|
-
field,
|
|
73
|
-
[model.get_asset_by_id(int(id)) for id in targets]
|
|
74
|
-
)
|
|
75
|
-
model.add_association(association)
|
|
76
|
-
|
|
77
|
-
# Reconstruct the attackers
|
|
78
|
-
if 'attackers' in model_dict:
|
|
79
|
-
attackers_info = model_dict['attackers']
|
|
80
|
-
for attacker_id in attackers_info:
|
|
81
|
-
attacker = AttackerAttachment(
|
|
82
|
-
name = attackers_info[attacker_id]['name']
|
|
83
|
-
)
|
|
84
|
-
attacker.entry_points = []
|
|
85
|
-
for asset_id in attackers_info[attacker_id]['entry_points']:
|
|
86
|
-
attacker.entry_points.append(
|
|
87
|
-
(model.get_asset_by_id(int(asset_id)),
|
|
88
|
-
attackers_info[attacker_id]['entry_points']\
|
|
89
|
-
[asset_id]['attack_steps']))
|
|
90
|
-
model.add_attacker(attacker, attacker_id = int(attacker_id))
|
|
91
|
-
return model
|
|
92
|
-
|
|
93
|
-
def load_from_json(
|
|
94
|
-
filename: str,
|
|
95
|
-
lang_classes_factory: LanguageClassesFactory
|
|
96
|
-
) -> Model:
|
|
97
|
-
"""
|
|
98
|
-
Load model from a json file.
|
|
99
|
-
|
|
100
|
-
Arguments:
|
|
101
|
-
filename - the name of the input file
|
|
102
|
-
"""
|
|
103
|
-
with open(filename, 'r', encoding='utf-8') as model_file:
|
|
104
|
-
model_dict = json.loads(model_file.read())
|
|
105
|
-
|
|
106
|
-
return _process_model(model_dict, lang_classes_factory)
|
|
107
|
-
|
|
108
|
-
def load_from_yaml(
|
|
109
|
-
filename: str,
|
|
110
|
-
lang_classes_factory: LanguageClassesFactory
|
|
111
|
-
) -> Model:
|
|
112
|
-
"""
|
|
113
|
-
Load model from a yaml file.
|
|
114
|
-
|
|
115
|
-
Arguments:
|
|
116
|
-
filename - the name of the input file
|
|
117
|
-
"""
|
|
118
|
-
with open(filename, 'r', encoding='utf-8') as model_file:
|
|
119
|
-
model_dict = yaml.safe_load(model_file)
|
|
120
|
-
|
|
121
|
-
return _process_model(model_dict, lang_classes_factory)
|
|
122
|
-
|
|
123
|
-
logger.info(f'Loading model from {filename} file.')
|
|
53
|
+
def load_model_dict_from_file(
|
|
54
|
+
filename: str,
|
|
55
|
+
) -> dict:
|
|
56
|
+
"""Load a json or yaml file to dict"""
|
|
57
|
+
|
|
58
|
+
model_dict = {}
|
|
124
59
|
if filename.endswith('.yml') or filename.endswith('.yaml'):
|
|
125
|
-
|
|
60
|
+
model_dict = load_dict_from_yaml_file(filename)
|
|
126
61
|
elif filename.endswith('.json'):
|
|
127
|
-
|
|
62
|
+
model_dict = load_dict_from_json_file(filename)
|
|
128
63
|
else:
|
|
129
64
|
msg = 'Unknown file extension for model file to load from.'
|
|
130
65
|
logger.error(msg)
|
|
131
66
|
raise ValueError(msg)
|
|
132
|
-
return
|
|
67
|
+
return model_dict
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def convert_model_dict_from_version_0_0(model_dict: dict) -> dict:
|
|
71
|
+
"""
|
|
72
|
+
Convert model dict version 0.0 to 0.1
|
|
73
|
+
|
|
74
|
+
Arguments:
|
|
75
|
+
model_dict - the dictionary containing the serialized model
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
A dictionary containing the version 0.1 equivalent serialized model
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
new_model_dict = {}
|
|
82
|
+
|
|
83
|
+
# Meta data and attackers did not change
|
|
84
|
+
new_model_dict['metadata'] = model_dict['metadata']
|
|
85
|
+
new_model_dict['attackers'] = model_dict['attackers']
|
|
86
|
+
|
|
87
|
+
new_model_dict['assets'] = {}
|
|
88
|
+
|
|
89
|
+
# Reconstruct the assets
|
|
90
|
+
new_assets_dict = {}
|
|
91
|
+
for asset_id, asset_info in model_dict['assets'].items():
|
|
92
|
+
# Make sure asset ids are ints for json compatibility
|
|
93
|
+
asset_id = int(asset_id)
|
|
94
|
+
new_assets_dict[asset_id] = asset_info
|
|
95
|
+
|
|
96
|
+
# Metaconcept renamed to type
|
|
97
|
+
new_assets_dict[asset_id]["type"] = (
|
|
98
|
+
new_assets_dict[asset_id]["metaconcept"]
|
|
99
|
+
)
|
|
100
|
+
del new_assets_dict[asset_id]["metaconcept"]
|
|
101
|
+
|
|
102
|
+
new_model_dict['assets'] = new_assets_dict
|
|
103
|
+
|
|
104
|
+
# Reconstruct the associations dict in new format
|
|
105
|
+
new_assoc_list = []
|
|
106
|
+
for assoc_dict in model_dict.get('associations', []):
|
|
107
|
+
new_assoc_dict: dict[str, dict] = {}
|
|
108
|
+
|
|
109
|
+
# Assocs are not mapped from association names
|
|
110
|
+
# metaconcept field removed
|
|
111
|
+
assoc_name = assoc_dict['metaconcept']
|
|
112
|
+
new_assoc_dict[assoc_name] = {}
|
|
113
|
+
|
|
114
|
+
for field, targets in assoc_dict['association'].items():
|
|
115
|
+
# Targets are now intes
|
|
116
|
+
new_targets = [int(asset_id) for asset_id in targets]
|
|
117
|
+
new_assoc_dict[assoc_name][field] = new_targets
|
|
118
|
+
|
|
119
|
+
new_assoc_list.append(new_assoc_dict)
|
|
120
|
+
|
|
121
|
+
# Add new assoc dict to new model dict
|
|
122
|
+
new_model_dict['associations'] = new_assoc_list
|
|
123
|
+
|
|
124
|
+
# Reconstruct the attackers
|
|
125
|
+
if 'attackers' in model_dict:
|
|
126
|
+
attackers_info = model_dict['attackers']
|
|
127
|
+
|
|
128
|
+
return new_model_dict
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def convert_model_dict_from_version_0_1(model_dict: dict) -> dict:
|
|
132
|
+
"""
|
|
133
|
+
Convert model dict version 0.1 to 0.2
|
|
134
|
+
|
|
135
|
+
Arguments:
|
|
136
|
+
model_dict - the dictionary containing the serialized model
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
A dictionary containing the version 0.2 equivalent serialized model
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
new_model_dict = {}
|
|
143
|
+
|
|
144
|
+
# Meta data and assets format did not change from version 0.1
|
|
145
|
+
new_model_dict['metadata'] = model_dict['metadata']
|
|
146
|
+
|
|
147
|
+
new_assets_dict = {}
|
|
148
|
+
for asset_id, asset_info in model_dict['assets'].items():
|
|
149
|
+
# Make sure asset ids are ints for json compatibility
|
|
150
|
+
asset_id = int(asset_id)
|
|
151
|
+
new_assets_dict[asset_id] = asset_info
|
|
152
|
+
|
|
153
|
+
new_model_dict['assets'] = new_assets_dict
|
|
154
|
+
|
|
155
|
+
# Reconstruct the associations dict in new format
|
|
156
|
+
new_assoc_list = []
|
|
157
|
+
for assoc_dict in model_dict.get('associations', []):
|
|
158
|
+
new_assoc_dict: dict[str, dict] = {}
|
|
159
|
+
|
|
160
|
+
assert len(assoc_dict.keys()) == 1, (
|
|
161
|
+
"Only one key per association in model file allowed"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
assoc_name = list(assoc_dict.keys())[0]
|
|
165
|
+
new_assoc_name = assoc_name.split("_")[0]
|
|
166
|
+
new_assoc_dict[new_assoc_name] = {}
|
|
167
|
+
for field, targets in assoc_dict[assoc_name].items():
|
|
168
|
+
new_assoc_dict[new_assoc_name][field] = targets
|
|
169
|
+
|
|
170
|
+
new_assoc_list.append(new_assoc_dict)
|
|
171
|
+
|
|
172
|
+
# Add new assoc dict to new model dict
|
|
173
|
+
new_model_dict['associations'] = new_assoc_list
|
|
174
|
+
|
|
175
|
+
# Reconstruct attackers dict for new format
|
|
176
|
+
new_attackers_dict: dict[int, dict] = {}
|
|
177
|
+
attackers_dict: dict = model_dict.get('attackers', {})
|
|
178
|
+
for attacker_id, attacker_dict in attackers_dict.items():
|
|
179
|
+
attacker_id = int(attacker_id) # JSON compatibility
|
|
180
|
+
new_attackers_dict[attacker_id] = {}
|
|
181
|
+
new_attackers_dict[attacker_id]['name'] = attacker_dict['name']
|
|
182
|
+
new_entry_points_dict = {}
|
|
183
|
+
|
|
184
|
+
entry_points_dict = attacker_dict['entry_points']
|
|
185
|
+
for asset_id, attack_steps in entry_points_dict.items():
|
|
186
|
+
asset_id = int(asset_id) # JSON compatibility
|
|
187
|
+
asset_name = new_assets_dict[asset_id]['name']
|
|
188
|
+
new_entry_points_dict[asset_name] = {
|
|
189
|
+
'asset_id': asset_id,
|
|
190
|
+
'attack_steps': attack_steps['attack_steps']
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
new_attackers_dict[attacker_id]['entry_points'] = new_entry_points_dict
|
|
194
|
+
|
|
195
|
+
# Add new attackers dict to new model dict
|
|
196
|
+
new_model_dict['attackers'] = new_attackers_dict
|
|
197
|
+
return new_model_dict
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def convert_model_dict_from_version_0_2(model_dict: dict) -> dict:
|
|
201
|
+
"""
|
|
202
|
+
Convert model dict version 0.2 to 0.3
|
|
203
|
+
|
|
204
|
+
Arguments:
|
|
205
|
+
model_dict - the dictionary containing the serialized model
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
A dictionary containing the version 0.3 equivalent serialized model
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
new_model_dict = {}
|
|
212
|
+
|
|
213
|
+
# Meta data and assets format did not change from version 0.1
|
|
214
|
+
new_model_dict['metadata'] = model_dict['metadata']
|
|
215
|
+
new_model_dict['attackers'] = model_dict['attackers']
|
|
216
|
+
|
|
217
|
+
new_assets_dict: dict[int, dict] = {}
|
|
218
|
+
for asset_id, asset_info in model_dict['assets'].items():
|
|
219
|
+
# Make sure asset ids are ints for json compatibility
|
|
220
|
+
asset_id = int(asset_id)
|
|
221
|
+
new_assets_dict[asset_id] = asset_info
|
|
222
|
+
new_assets_dict[asset_id]['associated_assets'] = {}
|
|
223
|
+
|
|
224
|
+
new_model_dict['assets'] = new_assets_dict
|
|
225
|
+
|
|
226
|
+
# Reconstruct the associations dict in new format
|
|
227
|
+
for assocs_dict in model_dict.get('associations', []):
|
|
228
|
+
|
|
229
|
+
assert len(assocs_dict.keys()) == 1, (
|
|
230
|
+
"Only one key per association in model file allowed"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
assoc_name = list(assocs_dict.keys())[0]
|
|
234
|
+
assoc_dict = assocs_dict[assoc_name]
|
|
235
|
+
left_field_name = list(assoc_dict.keys())[0]
|
|
236
|
+
right_field_name = list(assoc_dict.keys())[1]
|
|
237
|
+
|
|
238
|
+
for l_asset_id in assoc_dict[left_field_name]:
|
|
239
|
+
l_asset_id = int(l_asset_id) # json compatibility
|
|
240
|
+
for r_asset_id in assoc_dict[right_field_name]:
|
|
241
|
+
r_asset_id = int(r_asset_id) # json compatibility
|
|
242
|
+
l_asset_dict = new_model_dict['assets'][l_asset_id]
|
|
243
|
+
r_asset_dict = new_model_dict['assets'][r_asset_id]
|
|
244
|
+
|
|
245
|
+
# Add connections from left to right
|
|
246
|
+
l_asset_dict['associated_assets'].setdefault(
|
|
247
|
+
right_field_name, {}
|
|
248
|
+
)[r_asset_id] = r_asset_dict['name']
|
|
249
|
+
|
|
250
|
+
# And from right to left
|
|
251
|
+
r_asset_dict['associated_assets'].setdefault(
|
|
252
|
+
left_field_name, {}
|
|
253
|
+
)[l_asset_id] = l_asset_dict['name']
|
|
254
|
+
|
|
255
|
+
return new_model_dict
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
maltoolbox/__init__.py,sha256=4y4QJcwl6OKw8pCiPaUrOWtXRpEgFrKeoNLajc9p2Iw,2776
|
|
2
|
-
maltoolbox/__main__.py,sha256=1lOOOme_y56VWrEE1jkarTt-WoUo9yilCo8sUrivyns,2680
|
|
3
|
-
maltoolbox/default.conf,sha256=YLGBSJh2q8hn3RzRRBbib9F6E6pcvquoHeALMRtA0wU,295
|
|
4
|
-
maltoolbox/exceptions.py,sha256=0YjPx2v1yYumZ2o7pVZ1s_jS-GAb3Ng979KEFhROSNY,1399
|
|
5
|
-
maltoolbox/file_utils.py,sha256=TFLm32_RLfB4uEsToZ8ypDcRbbdFZMso34mfvqAb1bY,2139
|
|
6
|
-
maltoolbox/model.py,sha256=mQqwx0cSLV6E8wGBSDDKkKnFdbcPeBcjqIpFdBjtkYE,31042
|
|
7
|
-
maltoolbox/wrappers.py,sha256=BYYNcIdTlyumADQCPcy1xmPEabfmi0P1l9RcbdVWm9w,2002
|
|
8
|
-
maltoolbox/attackgraph/__init__.py,sha256=Oqqj5iCwnrzjDoJEFZnVI_kebjJPVbPXK-mWHy0lf-8,209
|
|
9
|
-
maltoolbox/attackgraph/attacker.py,sha256=OaBNDYZF8shbFuQctzuNYVkOrpNb_KhxxV19k0SRa50,3541
|
|
10
|
-
maltoolbox/attackgraph/attackgraph.py,sha256=HLAjF8Qx9W2iv6uSWRULL16ieuy7UM5fLZNwxIr544s,31348
|
|
11
|
-
maltoolbox/attackgraph/node.py,sha256=xrnY_YlX9ZFXDRsj92I9PZMGJR6s1LpTnOIppR6TGXo,6074
|
|
12
|
-
maltoolbox/attackgraph/query.py,sha256=JnoNTUEIlLv2VIk3u5Rq3GpleOn9TZVGBVijniRY_44,6802
|
|
13
|
-
maltoolbox/attackgraph/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
maltoolbox/attackgraph/analyzers/apriori.py,sha256=8dRZL22oym5goXBYtYA1ZYXxk8VwQS0RlNCvNCUhCAY,8743
|
|
15
|
-
maltoolbox/ingestors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
maltoolbox/ingestors/neo4j.py,sha256=jdulYsQ2eZT2r0Af_yYjyGkmVx4l5h8viu1Z70NjVAM,8811
|
|
17
|
-
maltoolbox/language/__init__.py,sha256=WNoPtcYHJE799vR6x269Gx5KPWxBgLOHQoycE1vhOF4,257
|
|
18
|
-
maltoolbox/language/classes_factory.py,sha256=-s4xxwcCGgKj1wzrgsrn-ndLhgU4VoEjrrSuGx8qvYE,10217
|
|
19
|
-
maltoolbox/language/languagegraph.py,sha256=ctoxS33DM7F4DF50SNDB2lTtokUJKyWMhMhss46yv_g,68123
|
|
20
|
-
maltoolbox/language/compiler/__init__.py,sha256=fJ22-FlXfr907WCPkqlr_eBTzPqsrg6m3i7J_ZWpuAo,840
|
|
21
|
-
maltoolbox/language/compiler/mal_lexer.py,sha256=wocRzBkLbqYefpGvq2W77x7439-AdZKVgPWhRiRubXg,10776
|
|
22
|
-
maltoolbox/language/compiler/mal_parser.py,sha256=M1EVZFV73TNfQHz2KJ8-iloqOD4KUhHyajszD8UrNow,91349
|
|
23
|
-
maltoolbox/language/compiler/mal_visitor.py,sha256=9gG06D7GZKlBY-62SmbIkRYkGBUBIC6fl1GOg7v2IuM,13223
|
|
24
|
-
maltoolbox/translators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
-
maltoolbox/translators/securicad.py,sha256=FAIHnoqFTmNYbCGxLsK6pX5g1oiNFfPTqkT_3qq3GG8,6692
|
|
26
|
-
maltoolbox/translators/updater.py,sha256=Ap08-AsU_7or5ESQvZL2i4nWz3B5pvgfftZyc_-Gd8M,4766
|
|
27
|
-
mal_toolbox-0.2.0.dist-info/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
|
|
28
|
-
mal_toolbox-0.2.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
29
|
-
mal_toolbox-0.2.0.dist-info/METADATA,sha256=AWtN4435t5ycp5k-dPfRSVRU4rhpyb6e_z1FBE2PYpY,6001
|
|
30
|
-
mal_toolbox-0.2.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
31
|
-
mal_toolbox-0.2.0.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
|
|
32
|
-
mal_toolbox-0.2.0.dist-info/RECORD,,
|
maltoolbox/default.conf
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
[logging]
|
|
2
|
-
output_dir = tmp
|
|
3
|
-
log_level =
|
|
4
|
-
log_file = %(output_dir)s/log.txt
|
|
5
|
-
attackgraph_file = %(output_dir)s/attackgraph.yml
|
|
6
|
-
model_file = %(output_dir)s/model.yml
|
|
7
|
-
langspec_file = %(output_dir)s/langspec_file.yml
|
|
8
|
-
|
|
9
|
-
[input]
|
|
10
|
-
model_file =
|
|
11
|
-
lang_spec_file =
|
|
12
|
-
|
|
13
|
-
[neo4j]
|
|
14
|
-
uri =
|
|
15
|
-
username =
|
|
16
|
-
password =
|
|
17
|
-
dbname =
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MAL-Toolbox Language Classes Factory Module
|
|
3
|
-
Uses python_jsonschema_objects to generate python classes from a MAL language
|
|
4
|
-
"""
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
import json
|
|
7
|
-
import logging
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
9
|
-
|
|
10
|
-
import python_jsonschema_objects as pjs
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from typing import Literal, Optional, TypeAlias
|
|
14
|
-
from maltoolbox.language import LanguageGraph
|
|
15
|
-
from python_jsonschema_objects.classbuilder import ProtocolBase
|
|
16
|
-
|
|
17
|
-
SchemaGeneratedClass: TypeAlias = ProtocolBase
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
class LanguageClassesFactory:
|
|
22
|
-
def __init__(self, lang_graph: LanguageGraph):
|
|
23
|
-
self.lang_graph: LanguageGraph = lang_graph
|
|
24
|
-
self.json_schema: dict = {}
|
|
25
|
-
self._create_classes()
|
|
26
|
-
|
|
27
|
-
def _generate_assets(self) -> None:
|
|
28
|
-
"""
|
|
29
|
-
Generate JSON Schema for asset types in the language specification.
|
|
30
|
-
"""
|
|
31
|
-
for asset_name, asset in self.lang_graph.assets.items():
|
|
32
|
-
logger.debug('Creating %s asset JSON schema entry.', asset.name)
|
|
33
|
-
asset_json_entry = {
|
|
34
|
-
'title': 'Asset_' + asset.name,
|
|
35
|
-
'type': 'object',
|
|
36
|
-
'properties': {},
|
|
37
|
-
}
|
|
38
|
-
asset_json_entry['properties']['id'] = {
|
|
39
|
-
'type' : 'integer',
|
|
40
|
-
}
|
|
41
|
-
asset_json_entry['properties']['type'] = \
|
|
42
|
-
{
|
|
43
|
-
'type' : 'string',
|
|
44
|
-
'default': asset_name
|
|
45
|
-
}
|
|
46
|
-
if asset.own_super_asset:
|
|
47
|
-
asset_json_entry['allOf'] = [
|
|
48
|
-
{'$ref': '#/definitions/LanguageAsset/definitions/Asset_'\
|
|
49
|
-
+ asset.own_super_asset.name}
|
|
50
|
-
]
|
|
51
|
-
for step_name, step in asset.attack_steps.items():
|
|
52
|
-
if step.type == 'defense':
|
|
53
|
-
if step.ttc and step.ttc['name'] == 'Enabled':
|
|
54
|
-
default_defense_value = 1.0
|
|
55
|
-
else:
|
|
56
|
-
default_defense_value = 0.0
|
|
57
|
-
asset_json_entry['properties'][step_name] = \
|
|
58
|
-
{
|
|
59
|
-
'type' : 'number',
|
|
60
|
-
'minimum' : 0,
|
|
61
|
-
'maximum' : 1,
|
|
62
|
-
'default': default_defense_value
|
|
63
|
-
}
|
|
64
|
-
self.json_schema['definitions']['LanguageAsset']['definitions']\
|
|
65
|
-
['Asset_' + asset_name] = asset_json_entry
|
|
66
|
-
self.json_schema['definitions']['LanguageAsset']['oneOf'].append(
|
|
67
|
-
{'$ref': '#/definitions/LanguageAsset/definitions/Asset_' + asset_name}
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
def _generate_associations(self) -> None:
|
|
71
|
-
"""
|
|
72
|
-
Generate JSON Schema for association types in the language specification.
|
|
73
|
-
"""
|
|
74
|
-
def create_association_entry(assoc: SchemaGeneratedClass):
|
|
75
|
-
logger.debug('Creating %s association JSON schema entry.', assoc.name)
|
|
76
|
-
assoc_json_entry = {
|
|
77
|
-
'title': 'Association_' + assoc.full_name,
|
|
78
|
-
'type': 'object',
|
|
79
|
-
'properties': {}
|
|
80
|
-
}
|
|
81
|
-
assoc_json_entry['properties']['type'] = \
|
|
82
|
-
{
|
|
83
|
-
'type' : 'string',
|
|
84
|
-
'default': assoc.name
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
create_association_field(assoc, assoc_json_entry, 'left')
|
|
88
|
-
create_association_field(assoc, assoc_json_entry, 'right')
|
|
89
|
-
return assoc_json_entry
|
|
90
|
-
|
|
91
|
-
def create_association_field(
|
|
92
|
-
assoc: SchemaGeneratedClass,
|
|
93
|
-
assoc_json_entry: dict,
|
|
94
|
-
position: Literal['left', 'right']
|
|
95
|
-
) -> None:
|
|
96
|
-
field = getattr(assoc, position + "_field")
|
|
97
|
-
assoc_json_entry['properties'][field.fieldname] = \
|
|
98
|
-
{
|
|
99
|
-
'type' : 'array',
|
|
100
|
-
'items' :
|
|
101
|
-
{
|
|
102
|
-
'$ref':
|
|
103
|
-
'#/definitions/LanguageAsset/definitions/Asset_' +
|
|
104
|
-
field.asset.name
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if field.maximum:
|
|
108
|
-
assoc_json_entry['properties'][field.fieldname]\
|
|
109
|
-
['maxItems'] = field.maximum
|
|
110
|
-
|
|
111
|
-
for asset_name, asset in self.lang_graph.assets.items():
|
|
112
|
-
for assoc_name, assoc in asset.associations.items():
|
|
113
|
-
if assoc_name not in self.json_schema['definitions']\
|
|
114
|
-
['LanguageAssociation']['definitions']:
|
|
115
|
-
assoc_json_entry = create_association_entry(assoc)
|
|
116
|
-
self.json_schema['definitions']['LanguageAssociation']\
|
|
117
|
-
['definitions']['Association_' + assoc_name] = \
|
|
118
|
-
assoc_json_entry
|
|
119
|
-
self.json_schema['definitions']['LanguageAssociation']['oneOf'].\
|
|
120
|
-
append({'$ref': '#/definitions/LanguageAssociation/' +
|
|
121
|
-
'definitions/Association_' + assoc_name})
|
|
122
|
-
|
|
123
|
-
def _create_classes(self) -> None:
|
|
124
|
-
"""
|
|
125
|
-
Create classes based on the language specification.
|
|
126
|
-
"""
|
|
127
|
-
|
|
128
|
-
# First, we have to translate the language specification into a JSON
|
|
129
|
-
# schema. Initialize the overall JSON schema structure.
|
|
130
|
-
self.json_schema = {
|
|
131
|
-
'$schema': 'http://json-schema.org/draft-04/schema#',
|
|
132
|
-
'id': f"urn:mal:{__name__.replace('.', ':')}",
|
|
133
|
-
'title': 'LanguageObject',
|
|
134
|
-
'type': 'object',
|
|
135
|
-
'oneOf':[
|
|
136
|
-
{'$ref': '#/definitions/LanguageAsset'},
|
|
137
|
-
{'$ref': '#/definitions/LanguageAssociation'}
|
|
138
|
-
],
|
|
139
|
-
'definitions': {}}
|
|
140
|
-
self.json_schema['definitions']['LanguageAsset'] = {
|
|
141
|
-
'title': 'LanguageAsset',
|
|
142
|
-
'type': 'object',
|
|
143
|
-
'oneOf': [],
|
|
144
|
-
'definitions': {}}
|
|
145
|
-
self.json_schema['definitions']['LanguageAssociation'] = {
|
|
146
|
-
'title': 'LanguageAssociation',
|
|
147
|
-
'type': 'object',
|
|
148
|
-
'oneOf': [],
|
|
149
|
-
'definitions': {}}
|
|
150
|
-
|
|
151
|
-
self._generate_assets()
|
|
152
|
-
self._generate_associations()
|
|
153
|
-
|
|
154
|
-
if logger.isEnabledFor(logging.DEBUG):
|
|
155
|
-
# Avoid running json.dumps when not in debug
|
|
156
|
-
logger.debug(json.dumps(self.json_schema, indent = 2))
|
|
157
|
-
|
|
158
|
-
# Once we have the JSON schema we create the actual classes.
|
|
159
|
-
builder = pjs.ObjectBuilder(self.json_schema)
|
|
160
|
-
self.ns = builder.build_classes(standardize_names=False)
|
|
161
|
-
|
|
162
|
-
def get_association_by_signature(
|
|
163
|
-
self,
|
|
164
|
-
assoc_name: str,
|
|
165
|
-
left_asset: str,
|
|
166
|
-
right_asset: str
|
|
167
|
-
) -> Optional[str]:
|
|
168
|
-
"""
|
|
169
|
-
Get association name based on its signature. This is primarily
|
|
170
|
-
relevant for getting the exact association full name when multiple
|
|
171
|
-
associations with the same name exist.
|
|
172
|
-
|
|
173
|
-
Arguments:
|
|
174
|
-
assoc_name - the association name
|
|
175
|
-
left_asset - the name of the left asset type
|
|
176
|
-
right_asset - the name of the right asset type
|
|
177
|
-
|
|
178
|
-
Return: The matching association name if a match is found.
|
|
179
|
-
None if there is no match.
|
|
180
|
-
"""
|
|
181
|
-
lang_assocs_entries = self.json_schema['definitions']\
|
|
182
|
-
['LanguageAssociation']['definitions']
|
|
183
|
-
if not assoc_name in lang_assocs_entries:
|
|
184
|
-
raise LookupError(
|
|
185
|
-
'Failed to find "%s" association in the language json '
|
|
186
|
-
'schema.' % assoc_name
|
|
187
|
-
)
|
|
188
|
-
assoc_entry = lang_assocs_entries[assoc_name]
|
|
189
|
-
# If the association has a oneOf property it should always have more
|
|
190
|
-
# than just one alternative, but check just in case
|
|
191
|
-
if 'definitions' in assoc_entry and \
|
|
192
|
-
len(assoc_entry['definitions']) > 1:
|
|
193
|
-
full_name = '%s_%s_%s' % (
|
|
194
|
-
assoc_name,
|
|
195
|
-
left_asset,
|
|
196
|
-
right_asset
|
|
197
|
-
)
|
|
198
|
-
full_name_flipped = '%s_%s_%s' % (
|
|
199
|
-
assoc_name,
|
|
200
|
-
right_asset,
|
|
201
|
-
left_asset
|
|
202
|
-
)
|
|
203
|
-
if not full_name in assoc_entry['definitions']:
|
|
204
|
-
if not full_name_flipped in assoc_entry['definitions']:
|
|
205
|
-
raise LookupError(
|
|
206
|
-
'Failed to find "%s" or "%s" association in the '
|
|
207
|
-
'language json schema.'
|
|
208
|
-
% (full_name,
|
|
209
|
-
full_name_flipped)
|
|
210
|
-
)
|
|
211
|
-
else:
|
|
212
|
-
return full_name_flipped
|
|
213
|
-
else:
|
|
214
|
-
return full_name
|
|
215
|
-
else:
|
|
216
|
-
return assoc_name
|
|
217
|
-
|
|
218
|
-
def get_asset_class(self,
|
|
219
|
-
asset_name: str
|
|
220
|
-
) -> Optional[SchemaGeneratedClass]:
|
|
221
|
-
class_name = 'Asset_' + asset_name
|
|
222
|
-
if hasattr(self.ns, class_name):
|
|
223
|
-
class_obj = getattr(self.ns, class_name)
|
|
224
|
-
class_obj.__hash__ = lambda self: hash(self.name)
|
|
225
|
-
return class_obj
|
|
226
|
-
else:
|
|
227
|
-
logger.warning('Could not find Asset "%s" in classes factory.' %
|
|
228
|
-
asset_name)
|
|
229
|
-
return None
|
|
230
|
-
|
|
231
|
-
def get_association_class(self,
|
|
232
|
-
assoc_name: str
|
|
233
|
-
) -> Optional[SchemaGeneratedClass]:
|
|
234
|
-
class_name = 'Association_' + assoc_name
|
|
235
|
-
if hasattr(self.ns, class_name):
|
|
236
|
-
return getattr(self.ns, class_name)
|
|
237
|
-
else:
|
|
238
|
-
logger.warning('Could not find Association "%s" in classes factory.' %
|
|
239
|
-
assoc_name)
|
|
240
|
-
return None
|
|
241
|
-
|
|
242
|
-
def get_association_class_by_fieldnames(self,
|
|
243
|
-
assoc_name: str,
|
|
244
|
-
fieldname1: str,
|
|
245
|
-
fieldname2: str
|
|
246
|
-
) -> Optional[SchemaGeneratedClass]:
|
|
247
|
-
class_name = 'Association_%s_%s_%s' % (assoc_name,
|
|
248
|
-
fieldname1, fieldname2)
|
|
249
|
-
class_name_alt = 'Association_%s_%s_%s' % (assoc_name,
|
|
250
|
-
fieldname2, fieldname1)
|
|
251
|
-
|
|
252
|
-
if hasattr(self.ns, class_name):
|
|
253
|
-
return getattr(self.ns, class_name)
|
|
254
|
-
elif hasattr(self.ns, class_name_alt):
|
|
255
|
-
return getattr(self.ns, class_name_alt)
|
|
256
|
-
else:
|
|
257
|
-
logger.warning('Could not find Association "%s" or "%s" in '
|
|
258
|
-
'classes factory.' % (class_name, class_name_alt))
|
|
259
|
-
return None
|