mal-toolbox 0.1.2__tar.gz → 0.1.3__tar.gz
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.1.2/mal_toolbox.egg-info → mal_toolbox-0.1.3}/PKG-INFO +2 -2
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3/mal_toolbox.egg-info}/PKG-INFO +2 -2
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/mal_toolbox.egg-info/SOURCES.txt +0 -1
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/mal_toolbox.egg-info/requires.txt +1 -1
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/__init__.py +2 -2
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/attackgraph/attackgraph.py +49 -26
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/attackgraph/node.py +1 -1
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/ingestors/neo4j.py +32 -18
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/language/classes_factory.py +57 -1
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/language/languagegraph.py +89 -2
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/model.py +4 -1
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/translators/securicad.py +34 -26
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/pyproject.toml +2 -2
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/tests/test_model.py +46 -49
- mal_toolbox-0.1.2/maltoolbox/language/specification.py +0 -90
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/AUTHORS +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/LICENSE +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/README.md +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/mal_toolbox.egg-info/dependency_links.txt +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/mal_toolbox.egg-info/top_level.txt +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/__main__.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/attackgraph/__init__.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/attackgraph/analyzers/__init__.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/attackgraph/analyzers/apriori.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/attackgraph/attacker.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/attackgraph/query.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/default.conf +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/exceptions.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/file_utils.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/ingestors/__init__.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/language/__init__.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/language/compiler/__init__.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/language/compiler/mal_lexer.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/language/compiler/mal_parser.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/language/compiler/mal_visitor.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/translators/__init__.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/translators/updater.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/maltoolbox/wrappers.py +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/setup.cfg +0 -0
- {mal_toolbox-0.1.2 → mal_toolbox-0.1.3}/tests/test_wrappers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mal-toolbox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A collection of tools used to create MAL models and attack graphs.
|
|
5
5
|
Author-email: Andrei Buhaiu <buhaiu@kth.se>, Giuseppe Nebbione <nebbione@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>, Joakim Loxdal <loxdal@kth.se>
|
|
6
6
|
License: Apache Software License
|
|
@@ -19,7 +19,7 @@ Description-Content-Type: text/markdown
|
|
|
19
19
|
License-File: LICENSE
|
|
20
20
|
License-File: AUTHORS
|
|
21
21
|
Requires-Dist: py2neo>=2021.2.3
|
|
22
|
-
Requires-Dist: python-jsonschema-objects>=0.
|
|
22
|
+
Requires-Dist: python-jsonschema-objects>=0.5.5
|
|
23
23
|
Requires-Dist: antlr4-tools
|
|
24
24
|
Requires-Dist: antlr4-python3-runtime
|
|
25
25
|
Requires-Dist: docopt
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mal-toolbox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A collection of tools used to create MAL models and attack graphs.
|
|
5
5
|
Author-email: Andrei Buhaiu <buhaiu@kth.se>, Giuseppe Nebbione <nebbione@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>, Joakim Loxdal <loxdal@kth.se>
|
|
6
6
|
License: Apache Software License
|
|
@@ -19,7 +19,7 @@ Description-Content-Type: text/markdown
|
|
|
19
19
|
License-File: LICENSE
|
|
20
20
|
License-File: AUTHORS
|
|
21
21
|
Requires-Dist: py2neo>=2021.2.3
|
|
22
|
-
Requires-Dist: python-jsonschema-objects>=0.
|
|
22
|
+
Requires-Dist: python-jsonschema-objects>=0.5.5
|
|
23
23
|
Requires-Dist: antlr4-tools
|
|
24
24
|
Requires-Dist: antlr4-python3-runtime
|
|
25
25
|
Requires-Dist: docopt
|
|
@@ -26,7 +26,6 @@ maltoolbox/ingestors/neo4j.py
|
|
|
26
26
|
maltoolbox/language/__init__.py
|
|
27
27
|
maltoolbox/language/classes_factory.py
|
|
28
28
|
maltoolbox/language/languagegraph.py
|
|
29
|
-
maltoolbox/language/specification.py
|
|
30
29
|
maltoolbox/language/compiler/__init__.py
|
|
31
30
|
maltoolbox/language/compiler/mal_lexer.py
|
|
32
31
|
maltoolbox/language/compiler/mal_parser.py
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# MAL Toolbox v0.1.
|
|
2
|
+
# MAL Toolbox v0.1.3
|
|
3
3
|
# Copyright 2024, Andrei Buhaiu.
|
|
4
4
|
#
|
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -21,7 +21,7 @@ MAL-Toolbox Framework
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
__title__ = 'maltoolbox'
|
|
24
|
-
__version__ = '0.1.
|
|
24
|
+
__version__ = '0.1.3'
|
|
25
25
|
__authors__ = ['Andrei Buhaiu',
|
|
26
26
|
'Giuseppe Nebbione',
|
|
27
27
|
'Nikolaos Kakouros',
|
|
@@ -10,7 +10,6 @@ from typing import TYPE_CHECKING
|
|
|
10
10
|
from .node import AttackGraphNode
|
|
11
11
|
from .attacker import Attacker
|
|
12
12
|
from ..exceptions import AttackGraphStepExpressionError
|
|
13
|
-
from ..language import specification
|
|
14
13
|
from ..model import Model
|
|
15
14
|
from ..exceptions import AttackGraphException
|
|
16
15
|
from ..file_utils import (
|
|
@@ -37,7 +36,8 @@ def _process_step_expression(
|
|
|
37
36
|
Recursively process an attack step expression.
|
|
38
37
|
|
|
39
38
|
Arguments:
|
|
40
|
-
|
|
39
|
+
lang_graph - a language graph representing the MAL language
|
|
40
|
+
specification
|
|
41
41
|
model - a maltoolbox.model.Model instance from which the attack
|
|
42
42
|
graph was generated
|
|
43
43
|
target_assets - the list of assets that this step expression should apply
|
|
@@ -150,11 +150,28 @@ def _process_step_expression(
|
|
|
150
150
|
step_expression['stepExpression'])
|
|
151
151
|
new_target_assets.extend(assets)
|
|
152
152
|
|
|
153
|
-
selected_new_target_assets = [
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
asset.type
|
|
157
|
-
|
|
153
|
+
selected_new_target_assets = []
|
|
154
|
+
for asset in new_target_assets:
|
|
155
|
+
lang_graph_asset = lang_graph.get_asset_by_name(
|
|
156
|
+
asset.type
|
|
157
|
+
)
|
|
158
|
+
if not lang_graph_asset:
|
|
159
|
+
raise LookupError(
|
|
160
|
+
f'Failed to find asset \"{asset.type}\" in the '
|
|
161
|
+
'language graph.'
|
|
162
|
+
)
|
|
163
|
+
lang_graph_subtype_asset = lang_graph.get_asset_by_name(
|
|
164
|
+
step_expression['subType']
|
|
165
|
+
)
|
|
166
|
+
if not lang_graph_subtype_asset:
|
|
167
|
+
raise LookupError(
|
|
168
|
+
'Failed to find asset '
|
|
169
|
+
f'\"{step_expression["subType"]}\" in the '
|
|
170
|
+
'language graph.'
|
|
171
|
+
)
|
|
172
|
+
if lang_graph_asset.is_subasset_of(lang_graph_subtype_asset):
|
|
173
|
+
selected_new_target_assets.append(asset)
|
|
174
|
+
|
|
158
175
|
return (selected_new_target_assets, None)
|
|
159
176
|
|
|
160
177
|
case 'collect':
|
|
@@ -225,17 +242,39 @@ class AttackGraph():
|
|
|
225
242
|
"""
|
|
226
243
|
|
|
227
244
|
attack_graph = AttackGraph()
|
|
245
|
+
attack_graph.model = model
|
|
228
246
|
serialized_attack_steps = serialized_object['attack_steps']
|
|
229
247
|
serialized_attackers = serialized_object['attackers']
|
|
230
248
|
|
|
231
249
|
# Create all of the nodes in the imported attack graph.
|
|
232
250
|
for node_full_name, node_dict in serialized_attack_steps.items():
|
|
251
|
+
|
|
252
|
+
# Recreate asset links if model is available.
|
|
253
|
+
node_asset = None
|
|
254
|
+
if model and 'asset' in node_dict:
|
|
255
|
+
node_asset = model.get_asset_by_name(node_dict['asset'])
|
|
256
|
+
if node_asset is None:
|
|
257
|
+
msg = ('Failed to find asset with id %s'
|
|
258
|
+
'when loading from attack graph dict')
|
|
259
|
+
logger.error(msg, node_dict["asset"])
|
|
260
|
+
raise LookupError(msg % node_dict["asset"])
|
|
261
|
+
|
|
233
262
|
ag_node = AttackGraphNode(
|
|
234
263
|
type=node_dict['type'],
|
|
235
264
|
name=node_dict['name'],
|
|
236
|
-
ttc=node_dict['ttc']
|
|
265
|
+
ttc=node_dict['ttc'],
|
|
266
|
+
asset=node_asset
|
|
237
267
|
)
|
|
238
268
|
|
|
269
|
+
if node_asset:
|
|
270
|
+
# Add AttackGraphNode to attack_step_nodes of asset
|
|
271
|
+
if hasattr(node_asset, 'attack_step_nodes'):
|
|
272
|
+
node_attack_steps = list(node_asset.attack_step_nodes)
|
|
273
|
+
node_attack_steps.append(ag_node)
|
|
274
|
+
node_asset.attack_step_nodes = node_attack_steps
|
|
275
|
+
else:
|
|
276
|
+
node_asset.attack_step_nodes = [ag_node]
|
|
277
|
+
|
|
239
278
|
ag_node.defense_status = float(node_dict['defense_status']) if \
|
|
240
279
|
'defense_status' in node_dict else None
|
|
241
280
|
ag_node.existence_status = node_dict['existence_status'] \
|
|
@@ -249,7 +288,8 @@ class AttackGraph():
|
|
|
249
288
|
ag_node.tags = node_dict['tags'] if \
|
|
250
289
|
'tags' in node_dict else []
|
|
251
290
|
|
|
252
|
-
|
|
291
|
+
# Add AttackGraphNode to AttackGraph
|
|
292
|
+
attack_graph.add_node(ag_node, node_id=node_dict['id'])
|
|
253
293
|
|
|
254
294
|
# Re-establish links between nodes.
|
|
255
295
|
for node_full_name, node_dict in serialized_attack_steps.items():
|
|
@@ -278,23 +318,6 @@ class AttackGraph():
|
|
|
278
318
|
raise LookupError(msg % parent_id)
|
|
279
319
|
_ag_node.parents.append(parent)
|
|
280
320
|
|
|
281
|
-
# Also recreate asset links if model is available.
|
|
282
|
-
if model and 'asset' in node_dict:
|
|
283
|
-
asset = model.get_asset_by_name(
|
|
284
|
-
node_dict['asset'])
|
|
285
|
-
if asset is None:
|
|
286
|
-
msg = ('Failed to find asset with id %s'
|
|
287
|
-
'when loading from attack graph dict')
|
|
288
|
-
logger.error(msg, node_dict["asset"])
|
|
289
|
-
raise LookupError(msg % node_dict["asset"])
|
|
290
|
-
_ag_node.asset = asset
|
|
291
|
-
if hasattr(asset, 'attack_step_nodes'):
|
|
292
|
-
attack_step_nodes = list(asset.attack_step_nodes)
|
|
293
|
-
attack_step_nodes.append(_ag_node)
|
|
294
|
-
asset.attack_step_nodes = attack_step_nodes
|
|
295
|
-
else:
|
|
296
|
-
asset.attack_step_nodes = [_ag_node]
|
|
297
|
-
|
|
298
321
|
for attacker_name, attacker in serialized_attackers.items():
|
|
299
322
|
ag_attacker = Attacker(
|
|
300
323
|
name = attacker['name'],
|
|
@@ -8,7 +8,7 @@ import logging
|
|
|
8
8
|
from py2neo import Graph, Node, Relationship, Subgraph
|
|
9
9
|
|
|
10
10
|
from ..model import AttackerAttachment, Model
|
|
11
|
-
from ..language import
|
|
11
|
+
from ..language import LanguageGraph, LanguageClassesFactory
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
@@ -123,7 +123,7 @@ def get_model(
|
|
|
123
123
|
username: str,
|
|
124
124
|
password: str,
|
|
125
125
|
dbname: str,
|
|
126
|
-
|
|
126
|
+
lang_graph: LanguageGraph,
|
|
127
127
|
lang_classes_factory: LanguageClassesFactory
|
|
128
128
|
) -> Model:
|
|
129
129
|
"""Load a model from Neo4j"""
|
|
@@ -184,7 +184,7 @@ def get_model(
|
|
|
184
184
|
target_id = right_id
|
|
185
185
|
target_prop = left_field
|
|
186
186
|
|
|
187
|
-
if attacker_id:
|
|
187
|
+
if attacker_id is not None:
|
|
188
188
|
attacker = instance_model.get_attacker_by_id(attacker_id)
|
|
189
189
|
if not attacker:
|
|
190
190
|
msg = 'Failed to find attacker with id %s in model!'
|
|
@@ -192,7 +192,7 @@ def get_model(
|
|
|
192
192
|
raise LookupError(msg % attacker_id)
|
|
193
193
|
target_asset = instance_model.get_asset_by_id(target_id)
|
|
194
194
|
if not target_asset:
|
|
195
|
-
msg = 'Failed to find asset with id %
|
|
195
|
+
msg = 'Failed to find asset with id %d in model!'
|
|
196
196
|
logger.error(msg, target_id)
|
|
197
197
|
raise LookupError(msg % target_id)
|
|
198
198
|
attacker.entry_points.append((target_asset,
|
|
@@ -200,25 +200,23 @@ def get_model(
|
|
|
200
200
|
continue
|
|
201
201
|
|
|
202
202
|
left_asset = instance_model.get_asset_by_id(left_id)
|
|
203
|
-
if
|
|
204
|
-
msg = 'Failed to find asset with id %
|
|
203
|
+
if left_asset is None:
|
|
204
|
+
msg = 'Failed to find asset with id %d in model!'
|
|
205
205
|
logger.error(msg, left_id)
|
|
206
206
|
raise LookupError(msg % left_id)
|
|
207
207
|
right_asset = instance_model.get_asset_by_id(right_id)
|
|
208
|
-
if
|
|
209
|
-
msg = 'Failed to find asset with id %
|
|
208
|
+
if right_asset is None:
|
|
209
|
+
msg = 'Failed to find asset with id %d in model!'
|
|
210
210
|
logger.error(msg, right_id)
|
|
211
211
|
raise LookupError(msg % right_id)
|
|
212
212
|
|
|
213
|
-
|
|
214
|
-
lang_spec,
|
|
213
|
+
assoc = lang_graph.get_association_by_fields_and_assets(
|
|
215
214
|
left_field,
|
|
216
215
|
right_field,
|
|
217
216
|
left_asset.type,
|
|
218
217
|
right_asset.type)
|
|
219
|
-
logger.debug('Found "%s" association.', assoc_name)
|
|
220
218
|
|
|
221
|
-
if not
|
|
219
|
+
if not assoc:
|
|
222
220
|
logger.error(
|
|
223
221
|
'Failed to find ("%s", "%s", "%s", "%s")'
|
|
224
222
|
'association in language specification!',
|
|
@@ -227,15 +225,31 @@ def get_model(
|
|
|
227
225
|
)
|
|
228
226
|
return None
|
|
229
227
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
228
|
+
logger.debug('Found "%s" association.', assoc.name)
|
|
229
|
+
|
|
230
|
+
assoc_name = lang_classes_factory.get_association_by_signature(
|
|
231
|
+
assoc.name,
|
|
232
|
+
left_asset.type,
|
|
233
|
+
right_asset.type
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if not assoc_name:
|
|
237
|
+
msg = 'Failed to find \"%s\" association in language specification!'
|
|
238
|
+
logger.error(msg, assoc.name)
|
|
239
|
+
raise LookupError(msg % assoc.name)
|
|
235
240
|
|
|
236
241
|
assoc = getattr(lang_classes_factory.ns, assoc_name)()
|
|
237
242
|
setattr(assoc, left_field, [left_asset])
|
|
238
243
|
setattr(assoc, right_field, [right_asset])
|
|
239
|
-
instance_model.
|
|
244
|
+
if not (instance_model.association_exists_between_assets(
|
|
245
|
+
assoc_name,
|
|
246
|
+
left_asset,
|
|
247
|
+
right_asset
|
|
248
|
+
) or instance_model.association_exists_between_assets(
|
|
249
|
+
assoc_name,
|
|
250
|
+
right_asset,
|
|
251
|
+
left_asset
|
|
252
|
+
)):
|
|
253
|
+
instance_model.add_association(assoc)
|
|
240
254
|
|
|
241
255
|
return instance_model
|
|
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING
|
|
|
9
9
|
import python_jsonschema_objects as pjs
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
|
-
from typing import Literal, TypeAlias
|
|
12
|
+
from typing import Literal, Optional, TypeAlias
|
|
13
13
|
from maltoolbox.language import LanguageGraph
|
|
14
14
|
from python_jsonschema_objects.classbuilder import ProtocolBase
|
|
15
15
|
|
|
@@ -184,3 +184,59 @@ class LanguageClassesFactory:
|
|
|
184
184
|
# Once we have the JSON schema we create the actual classes.
|
|
185
185
|
builder = pjs.ObjectBuilder(self.json_schema)
|
|
186
186
|
self.ns = builder.build_classes(standardize_names=False)
|
|
187
|
+
|
|
188
|
+
def get_association_by_signature(
|
|
189
|
+
self,
|
|
190
|
+
assoc_name: str,
|
|
191
|
+
left_asset: str,
|
|
192
|
+
right_asset: str
|
|
193
|
+
) -> Optional[str]:
|
|
194
|
+
"""
|
|
195
|
+
Get association name based on its signature. This is primarily
|
|
196
|
+
relevant for getting the exact association full name when multiple
|
|
197
|
+
associations with the same name exist.
|
|
198
|
+
|
|
199
|
+
Arguments:
|
|
200
|
+
assoc_name - the association name
|
|
201
|
+
left_asset - the name of the left asset type
|
|
202
|
+
right_asset - the name of the right asset type
|
|
203
|
+
|
|
204
|
+
Return: The matching association name if a match is found.
|
|
205
|
+
None if there is no match.
|
|
206
|
+
"""
|
|
207
|
+
lang_assocs_entries = self.json_schema['definitions']\
|
|
208
|
+
['LanguageAssociation']['definitions']
|
|
209
|
+
if not assoc_name in lang_assocs_entries:
|
|
210
|
+
raise LookupError(
|
|
211
|
+
'Failed to find "%s" association in the language json '
|
|
212
|
+
'schema.' % assoc_name
|
|
213
|
+
)
|
|
214
|
+
assoc_entry = lang_assocs_entries[assoc_name]
|
|
215
|
+
# If the association has a oneOf property it should always have more
|
|
216
|
+
# than just one alternative, but check just in case
|
|
217
|
+
if 'definitions' in assoc_entry and \
|
|
218
|
+
len(assoc_entry['definitions']) > 1:
|
|
219
|
+
full_name = '%s_%s_%s' % (
|
|
220
|
+
assoc_name,
|
|
221
|
+
left_asset,
|
|
222
|
+
right_asset
|
|
223
|
+
)
|
|
224
|
+
full_name_flipped = '%s_%s_%s' % (
|
|
225
|
+
assoc_name,
|
|
226
|
+
right_asset,
|
|
227
|
+
left_asset
|
|
228
|
+
)
|
|
229
|
+
if not full_name in assoc_entry['definitions']:
|
|
230
|
+
if not full_name_flipped in assoc_entry['definitions']:
|
|
231
|
+
raise LookupError(
|
|
232
|
+
'Failed to find "%s" or "%s" association in the '
|
|
233
|
+
'language json schema.'
|
|
234
|
+
% (full_name,
|
|
235
|
+
full_name_flipped)
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
return full_name_flipped
|
|
239
|
+
else:
|
|
240
|
+
return full_name
|
|
241
|
+
else:
|
|
242
|
+
return assoc_name
|
|
@@ -1000,8 +1000,17 @@ class LanguageGraph():
|
|
|
1000
1000
|
elif step['reaches']['overrides'] == True:
|
|
1001
1001
|
attack_steps[step['name']] = copy.deepcopy(step)
|
|
1002
1002
|
else:
|
|
1003
|
-
attack_steps[step['name']]['reaches']
|
|
1004
|
-
|
|
1003
|
+
if attack_steps[step['name']]['reaches'] is not None and \
|
|
1004
|
+
'stepExpression' in \
|
|
1005
|
+
attack_steps[step['name']]['reaches']:
|
|
1006
|
+
attack_steps[step['name']]['reaches']['stepExpressions'].\
|
|
1007
|
+
extend(step['reaches']['stepExpressions'])
|
|
1008
|
+
else:
|
|
1009
|
+
attack_steps[step['name']]['reaches'] = {
|
|
1010
|
+
'overrides': False,
|
|
1011
|
+
'stepExpressions': step['reaches']['stepExpressions']
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1005
1014
|
|
|
1006
1015
|
return attack_steps
|
|
1007
1016
|
|
|
@@ -1096,3 +1105,81 @@ class LanguageGraph():
|
|
|
1096
1105
|
self.associations = []
|
|
1097
1106
|
self.attack_steps = []
|
|
1098
1107
|
self._generate_graph()
|
|
1108
|
+
|
|
1109
|
+
def get_asset_by_name(
|
|
1110
|
+
self,
|
|
1111
|
+
asset_name
|
|
1112
|
+
) -> Optional[LanguageGraphAsset]:
|
|
1113
|
+
"""
|
|
1114
|
+
Get an asset based on its name
|
|
1115
|
+
|
|
1116
|
+
Arguments:
|
|
1117
|
+
asset_name - a string containing the asset name
|
|
1118
|
+
|
|
1119
|
+
Return:
|
|
1120
|
+
The asset matching the name.
|
|
1121
|
+
None if there is no match.
|
|
1122
|
+
"""
|
|
1123
|
+
for asset in self.assets:
|
|
1124
|
+
if asset.name == asset_name:
|
|
1125
|
+
return asset
|
|
1126
|
+
|
|
1127
|
+
return None
|
|
1128
|
+
|
|
1129
|
+
def get_association_by_fields_and_assets(
|
|
1130
|
+
self,
|
|
1131
|
+
first_field: str,
|
|
1132
|
+
second_field: str,
|
|
1133
|
+
first_asset_name: str,
|
|
1134
|
+
second_asset_name: str
|
|
1135
|
+
) -> Optional[LanguageGraphAssociation]:
|
|
1136
|
+
"""
|
|
1137
|
+
Get an association based on its field names and asset types
|
|
1138
|
+
|
|
1139
|
+
Arguments:
|
|
1140
|
+
first_field - a string containing the first field
|
|
1141
|
+
second_field - a string containing the second field
|
|
1142
|
+
first_asset_name - a string representing the first asset type
|
|
1143
|
+
second_asset_name - a string representing the second asset type
|
|
1144
|
+
|
|
1145
|
+
Return:
|
|
1146
|
+
The association matching the fieldnames and asset types.
|
|
1147
|
+
None if there is no match.
|
|
1148
|
+
"""
|
|
1149
|
+
first_asset = self.get_asset_by_name(first_asset_name)
|
|
1150
|
+
if first_asset is None:
|
|
1151
|
+
raise LookupError(
|
|
1152
|
+
f'Failed to find asset with name \"{first_asset_name}\" in '
|
|
1153
|
+
'the language graph.'
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
second_asset = self.get_asset_by_name(second_asset_name)
|
|
1157
|
+
if second_asset is None:
|
|
1158
|
+
raise LookupError(
|
|
1159
|
+
f'Failed to find asset with name \"{second_asset_name}\" in '
|
|
1160
|
+
'the language graph.'
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
for assoc in self.associations:
|
|
1164
|
+
logger.debug(
|
|
1165
|
+
'Compare ("%s", "%s", "%s", "%s") to ("%s", "%s", "%s", "%s").',
|
|
1166
|
+
first_asset_name, first_field,
|
|
1167
|
+
second_asset_name, second_field,
|
|
1168
|
+
assoc.left_field.asset.name, assoc.left_field.fieldname,
|
|
1169
|
+
assoc.right_field.asset.name, assoc.right_field.fieldname
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
# If the asset and fields match either way we accept it as a match.
|
|
1173
|
+
if assoc.left_field.fieldname == first_field and \
|
|
1174
|
+
assoc.right_field.fieldname == second_field and \
|
|
1175
|
+
first_asset.is_subasset_of(assoc.left_field.asset) and \
|
|
1176
|
+
second_asset.is_subasset_of(assoc.right_field.asset):
|
|
1177
|
+
return assoc
|
|
1178
|
+
|
|
1179
|
+
if assoc.left_field.fieldname == second_field and \
|
|
1180
|
+
assoc.right_field.fieldname == first_field and \
|
|
1181
|
+
second_asset.is_subasset_of(assoc.left_field.asset) and \
|
|
1182
|
+
first_asset.is_subasset_of(assoc.right_field.asset):
|
|
1183
|
+
return assoc
|
|
1184
|
+
|
|
1185
|
+
return None
|
|
@@ -400,7 +400,10 @@ class Model():
|
|
|
400
400
|
)
|
|
401
401
|
|
|
402
402
|
def association_exists_between_assets(
|
|
403
|
-
self,
|
|
403
|
+
self,
|
|
404
|
+
association_type: str,
|
|
405
|
+
left_asset: SchemaGeneratedClass,
|
|
406
|
+
right_asset: SchemaGeneratedClass
|
|
404
407
|
):
|
|
405
408
|
"""Return True if the association already exists between the assets"""
|
|
406
409
|
logger.debug(
|
|
@@ -7,26 +7,28 @@ import json
|
|
|
7
7
|
import logging
|
|
8
8
|
import xml.etree.ElementTree as ET
|
|
9
9
|
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
10
12
|
from ..model import AttackerAttachment, Model
|
|
11
|
-
from ..language import
|
|
13
|
+
from ..language import LanguageGraph, LanguageClassesFactory
|
|
12
14
|
|
|
13
15
|
logger = logging.getLogger(__name__)
|
|
14
16
|
|
|
15
17
|
def load_model_from_scad_archive(
|
|
16
18
|
scad_archive: str,
|
|
17
|
-
|
|
19
|
+
lang_graph: LanguageGraph,
|
|
18
20
|
lang_classes_factory: LanguageClassesFactory
|
|
19
|
-
) -> Model:
|
|
21
|
+
) -> Optional[Model]:
|
|
20
22
|
"""
|
|
21
23
|
Reads a '.sCAD' archive generated by securiCAD representing an instance
|
|
22
24
|
model and loads the information into a maltoobox.model.Model object.
|
|
23
25
|
|
|
24
26
|
Arguments:
|
|
25
27
|
scad_archive - the path to a '.sCAD' archive
|
|
26
|
-
|
|
27
|
-
specification
|
|
28
|
+
lang_graph - a language graph representing the MAL
|
|
29
|
+
language specification
|
|
28
30
|
lang_classes_factory - a language classes factory that contains
|
|
29
|
-
the
|
|
31
|
+
the classes defined by the
|
|
30
32
|
language specification
|
|
31
33
|
|
|
32
34
|
Return:
|
|
@@ -39,7 +41,6 @@ def load_model_from_scad_archive(
|
|
|
39
41
|
root = ET.fromstring(scad_model)
|
|
40
42
|
|
|
41
43
|
instance_model = Model(scad_archive,
|
|
42
|
-
lang_spec,
|
|
43
44
|
lang_classes_factory)
|
|
44
45
|
|
|
45
46
|
for child in root.iter('objects'):
|
|
@@ -52,10 +53,13 @@ def load_model_from_scad_archive(
|
|
|
52
53
|
)
|
|
53
54
|
|
|
54
55
|
if child.attrib['metaConcept'] == 'Attacker':
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
instance_model.add_attacker(
|
|
56
|
+
attacker_obj_id = int(child.attrib['id'])
|
|
57
|
+
attacker_at = AttackerAttachment()
|
|
58
|
+
attacker_at.entry_points = []
|
|
59
|
+
instance_model.add_attacker(
|
|
60
|
+
attacker_at,
|
|
61
|
+
attacker_id = attacker_obj_id
|
|
62
|
+
)
|
|
59
63
|
continue
|
|
60
64
|
|
|
61
65
|
if not hasattr(lang_classes_factory.ns,
|
|
@@ -100,7 +104,7 @@ def load_model_from_scad_archive(
|
|
|
100
104
|
target_id = right_id
|
|
101
105
|
target_prop = child.attrib['sourceProperty']
|
|
102
106
|
|
|
103
|
-
if attacker_id:
|
|
107
|
+
if attacker_id is not None:
|
|
104
108
|
attacker = instance_model.get_attacker_by_id(attacker_id)
|
|
105
109
|
if not attacker:
|
|
106
110
|
logger.error(
|
|
@@ -137,28 +141,32 @@ def load_model_from_scad_archive(
|
|
|
137
141
|
# matches the target field and vice versa.
|
|
138
142
|
left_field = child.attrib['sourceProperty']
|
|
139
143
|
right_field = child.attrib['targetProperty']
|
|
140
|
-
|
|
141
|
-
lang_spec,
|
|
144
|
+
lang_graph_assoc = lang_graph.get_association_by_fields_and_assets(
|
|
142
145
|
left_field,
|
|
143
146
|
right_field,
|
|
144
|
-
left_asset.
|
|
145
|
-
right_asset.
|
|
146
|
-
logger.debug('Found "%s" association.', assoc_name)
|
|
147
|
+
left_asset.type,
|
|
148
|
+
right_asset.type)
|
|
147
149
|
|
|
148
|
-
if not
|
|
149
|
-
|
|
150
|
+
if not lang_graph_assoc:
|
|
151
|
+
raise LookupError(
|
|
150
152
|
'Failed to find ("%s", "%s", "%s", "%s")'
|
|
151
|
-
'association in lang specification'
|
|
152
|
-
left_asset.
|
|
153
|
-
left_field, right_field
|
|
153
|
+
'association in lang specification.' %
|
|
154
|
+
(left_asset.type, right_asset.type,
|
|
155
|
+
left_field, right_field)
|
|
154
156
|
)
|
|
155
157
|
return None
|
|
156
158
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
logger.debug('Found "%s" association.', lang_graph_assoc.name)
|
|
160
|
+
assoc_name = lang_classes_factory.get_association_by_signature(
|
|
161
|
+
lang_graph_assoc.name,
|
|
162
|
+
left_asset.type,
|
|
163
|
+
right_asset.type
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if assoc_name is None:
|
|
159
167
|
logger.error(
|
|
160
|
-
'Failed to find %s
|
|
161
|
-
|
|
168
|
+
'Failed to find association with name \"%s\" in model!',
|
|
169
|
+
lang_graph_assoc.name
|
|
162
170
|
)
|
|
163
171
|
return None
|
|
164
172
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mal-toolbox"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="Andrei Buhaiu", email="buhaiu@kth.se" },
|
|
6
6
|
{ name="Giuseppe Nebbione", email="nebbione@kth.se" },
|
|
@@ -13,7 +13,7 @@ readme = "README.md"
|
|
|
13
13
|
requires-python = ">=3.10"
|
|
14
14
|
dependencies = [
|
|
15
15
|
"py2neo>=2021.2.3",
|
|
16
|
-
"python-jsonschema-objects>=0.
|
|
16
|
+
"python-jsonschema-objects>=0.5.5",
|
|
17
17
|
"antlr4-tools",
|
|
18
18
|
"antlr4-python3-runtime",
|
|
19
19
|
"docopt",
|
|
@@ -505,55 +505,52 @@ def test_model_get_associated_assets_by_fieldname(model: Model):
|
|
|
505
505
|
assert ret == []
|
|
506
506
|
|
|
507
507
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
#
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
#
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
#
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
#
|
|
522
|
-
#
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
#
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
#
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
#
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
#
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
#
|
|
545
|
-
#
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
#
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
# assert p1_dict.get('defenses') == {
|
|
555
|
-
# 'notPresent': 1.0
|
|
556
|
-
# }
|
|
508
|
+
def test_model_asset_to_dict(model: Model):
|
|
509
|
+
"""Make sure assets are converted to dictionaries correctly"""
|
|
510
|
+
# Create and add asset
|
|
511
|
+
p1 = create_application_asset(model, "Program 1")
|
|
512
|
+
model.add_asset(p1)
|
|
513
|
+
|
|
514
|
+
# Tuple is returned
|
|
515
|
+
ret = model.asset_to_dict(p1)
|
|
516
|
+
|
|
517
|
+
# First element should be the id
|
|
518
|
+
p1_id = ret[0]
|
|
519
|
+
assert p1_id == p1.id
|
|
520
|
+
|
|
521
|
+
# Second element is the dict, each value should
|
|
522
|
+
# be set as below for an 'Application' asset in coreLang
|
|
523
|
+
p1_dict = ret[1]
|
|
524
|
+
assert p1_dict.get('name') == p1.name
|
|
525
|
+
assert p1_dict.get('type') == 'Application'
|
|
526
|
+
|
|
527
|
+
# Default values should not be saved
|
|
528
|
+
assert p1_dict.get('defenses') == None
|
|
529
|
+
|
|
530
|
+
def test_model_asset_with_nondefault_defense_to_dict(model: Model):
|
|
531
|
+
"""Make sure assets are converted to dictionaries correctly"""
|
|
532
|
+
# Create and add asset
|
|
533
|
+
p1 = create_application_asset(model, "Program 1")
|
|
534
|
+
p1.notPresent = 1.0
|
|
535
|
+
model.add_asset(p1)
|
|
536
|
+
|
|
537
|
+
# Tuple is returned
|
|
538
|
+
ret = model.asset_to_dict(p1)
|
|
539
|
+
|
|
540
|
+
# First element should be the id
|
|
541
|
+
p1_id = ret[0]
|
|
542
|
+
assert p1_id == p1.id
|
|
543
|
+
|
|
544
|
+
# Second element is the dict, each value should
|
|
545
|
+
# be set as below for an 'Application' asset in coreLang
|
|
546
|
+
p1_dict = ret[1]
|
|
547
|
+
assert p1_dict.get('name') == p1.name
|
|
548
|
+
assert p1_dict.get('type') == 'Application'
|
|
549
|
+
|
|
550
|
+
# Default values for 'Application' defenses in coreLang
|
|
551
|
+
assert p1_dict.get('defenses') == {
|
|
552
|
+
'notPresent': 1.0
|
|
553
|
+
}
|
|
557
554
|
|
|
558
555
|
|
|
559
556
|
def test_model_association_to_dict(model: Model):
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MAL-Toolbox Language Specification Module
|
|
3
|
-
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import logging
|
|
7
|
-
|
|
8
|
-
from typing import Optional
|
|
9
|
-
logger = logging.getLogger(__name__)
|
|
10
|
-
|
|
11
|
-
# TODO move these functions to their relevant module/class
|
|
12
|
-
|
|
13
|
-
def get_association_by_fields_and_assets(
|
|
14
|
-
lang_spec: dict,
|
|
15
|
-
first_field: str,
|
|
16
|
-
second_field: str,
|
|
17
|
-
first_asset: str,
|
|
18
|
-
second_asset: str
|
|
19
|
-
) -> Optional[str]:
|
|
20
|
-
"""
|
|
21
|
-
Get an association based on its field names and asset types
|
|
22
|
-
|
|
23
|
-
Arguments:
|
|
24
|
-
lang_spec - a dictionary containing the MAL language specification
|
|
25
|
-
first_field - a string containing the first field
|
|
26
|
-
second_field - a string containing the second field
|
|
27
|
-
first_asset - a string representing the first asset type
|
|
28
|
-
second_asset - a string representing the second asset type
|
|
29
|
-
|
|
30
|
-
Return:
|
|
31
|
-
The name of the association matching the fieldnames and asset types.
|
|
32
|
-
None if there is no match.
|
|
33
|
-
"""
|
|
34
|
-
for assoc in lang_spec['associations']:
|
|
35
|
-
logger.debug(
|
|
36
|
-
'Compare ("%s", "%s". "%s". "%s") to ("%s", "%s", "%s", "%s").',
|
|
37
|
-
first_asset, first_field, second_asset, second_field,
|
|
38
|
-
assoc["leftAsset"], assoc["leftField"],
|
|
39
|
-
assoc["rightAsset"], assoc["rightField"]
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
# If the asset and fields match either way we accept it as a match.
|
|
43
|
-
if assoc['leftField'] == first_field and \
|
|
44
|
-
assoc['rightField'] == second_field and \
|
|
45
|
-
extends_asset(lang_spec, first_asset, assoc['leftAsset']) and \
|
|
46
|
-
extends_asset(lang_spec, second_asset, assoc['rightAsset']):
|
|
47
|
-
return assoc['name']
|
|
48
|
-
if assoc['leftField'] == second_field and \
|
|
49
|
-
assoc['rightField'] == first_field and \
|
|
50
|
-
extends_asset(lang_spec, second_asset, assoc['leftAsset']) and \
|
|
51
|
-
extends_asset(lang_spec, first_asset, assoc['rightAsset']):
|
|
52
|
-
return assoc['name']
|
|
53
|
-
|
|
54
|
-
return None
|
|
55
|
-
|
|
56
|
-
def extends_asset(lang_spec: dict, asset: str, target_asset: str) -> bool:
|
|
57
|
-
"""
|
|
58
|
-
Check if an asset extends the target asset through inheritance.
|
|
59
|
-
|
|
60
|
-
Arguments:
|
|
61
|
-
lang_spec - a dictionary containing the MAL language specification
|
|
62
|
-
asset - the asset name we wish to evaluate
|
|
63
|
-
target_asset - the target asset name we wish to evaluate if it
|
|
64
|
-
is extended
|
|
65
|
-
|
|
66
|
-
Return:
|
|
67
|
-
True if this asset extends the target_asset via inheritance.
|
|
68
|
-
False otherwise.
|
|
69
|
-
"""
|
|
70
|
-
|
|
71
|
-
logger.debug('Check if %s extends %s via inheritance.', asset, target_asset)
|
|
72
|
-
|
|
73
|
-
if asset == target_asset:
|
|
74
|
-
return True
|
|
75
|
-
|
|
76
|
-
asset_dict = next((asset_info for asset_info in lang_spec['assets'] \
|
|
77
|
-
if asset_info['name'] == asset), None)
|
|
78
|
-
if not asset_dict:
|
|
79
|
-
logger.error(
|
|
80
|
-
'Failed to find asset type %s when looking for variable.', asset
|
|
81
|
-
)
|
|
82
|
-
return False
|
|
83
|
-
if asset_dict['superAsset']:
|
|
84
|
-
if asset_dict['superAsset'] == target_asset:
|
|
85
|
-
return True
|
|
86
|
-
else:
|
|
87
|
-
return extends_asset(lang_spec, asset_dict['superAsset'],
|
|
88
|
-
target_asset)
|
|
89
|
-
|
|
90
|
-
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|