mal-toolbox 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mal_toolbox-0.2.0.dist-info → mal_toolbox-0.3.0.dist-info}/METADATA +43 -25
- mal_toolbox-0.3.0.dist-info/RECORD +29 -0
- mal_toolbox-0.3.0.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 +175 -148
- 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.0.dist-info}/AUTHORS +0 -0
- {mal_toolbox-0.2.0.dist-info → mal_toolbox-0.3.0.dist-info}/LICENSE +0 -0
- {mal_toolbox-0.2.0.dist-info → mal_toolbox-0.3.0.dist-info}/WHEEL +0 -0
- {mal_toolbox-0.2.0.dist-info → mal_toolbox-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -5,17 +5,21 @@ from __future__ import annotations
|
|
|
5
5
|
import copy
|
|
6
6
|
import logging
|
|
7
7
|
import json
|
|
8
|
+
import sys
|
|
9
|
+
import zipfile
|
|
8
10
|
|
|
9
11
|
from itertools import chain
|
|
10
12
|
from typing import TYPE_CHECKING
|
|
11
13
|
|
|
14
|
+
from .analyzers.apriori import calculate_viability_and_necessity
|
|
12
15
|
from .node import AttackGraphNode
|
|
13
16
|
from .attacker import Attacker
|
|
17
|
+
from .. import log_configs
|
|
14
18
|
from ..exceptions import AttackGraphStepExpressionError, AttackGraphException
|
|
15
19
|
from ..exceptions import LanguageGraphException
|
|
16
20
|
from ..model import Model
|
|
17
21
|
from ..language import (LanguageGraph, ExpressionsChain,
|
|
18
|
-
disaggregate_attack_step_full_name)
|
|
22
|
+
LanguageGraphAttackStep, disaggregate_attack_step_full_name)
|
|
19
23
|
from ..file_utils import (
|
|
20
24
|
load_dict_from_json_file,
|
|
21
25
|
load_dict_from_yaml_file,
|
|
@@ -25,20 +29,64 @@ from ..file_utils import (
|
|
|
25
29
|
|
|
26
30
|
if TYPE_CHECKING:
|
|
27
31
|
from typing import Any, Optional
|
|
32
|
+
from ..model import ModelAsset
|
|
28
33
|
|
|
29
34
|
logger = logging.getLogger(__name__)
|
|
30
35
|
|
|
31
36
|
|
|
37
|
+
def create_attack_graph(
|
|
38
|
+
lang_file: str,
|
|
39
|
+
model_file: str,
|
|
40
|
+
attach_attackers=True,
|
|
41
|
+
calc_viability_and_necessity=True
|
|
42
|
+
) -> AttackGraph:
|
|
43
|
+
"""Create and return an attack graph
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
lang_file - path to language file (.mar or .mal)
|
|
47
|
+
model_file - path to model file (yaml or json)
|
|
48
|
+
attach_attackers - whether to run attach_attackers or not
|
|
49
|
+
calc_viability_and_necessity - whether run apriori calculations or not
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
lang_graph = LanguageGraph.from_mar_archive(lang_file)
|
|
53
|
+
except zipfile.BadZipFile:
|
|
54
|
+
lang_graph = LanguageGraph.from_mal_spec(lang_file)
|
|
55
|
+
|
|
56
|
+
if log_configs['langspec_file']:
|
|
57
|
+
lang_graph.save_to_file(log_configs['langspec_file'])
|
|
58
|
+
|
|
59
|
+
instance_model = Model.load_from_file(model_file, lang_graph)
|
|
60
|
+
|
|
61
|
+
if log_configs['model_file']:
|
|
62
|
+
instance_model.save_to_file(log_configs['model_file'])
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
attack_graph = AttackGraph(lang_graph, instance_model)
|
|
66
|
+
except AttackGraphStepExpressionError:
|
|
67
|
+
logger.error(
|
|
68
|
+
'Attack graph generation failed when attempting '
|
|
69
|
+
'to resolve attack step expression!'
|
|
70
|
+
)
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
if attach_attackers:
|
|
74
|
+
attack_graph.attach_attackers()
|
|
75
|
+
|
|
76
|
+
if calc_viability_and_necessity:
|
|
77
|
+
calculate_viability_and_necessity(attack_graph)
|
|
78
|
+
|
|
79
|
+
return attack_graph
|
|
80
|
+
|
|
81
|
+
|
|
32
82
|
class AttackGraph():
|
|
33
83
|
"""Graph representation of attack steps"""
|
|
34
84
|
def __init__(self, lang_graph, model: Optional[Model] = None):
|
|
35
|
-
self.nodes:
|
|
36
|
-
self.attackers:
|
|
85
|
+
self.nodes: dict[int, AttackGraphNode] = {}
|
|
86
|
+
self.attackers: dict[int, Attacker] = {}
|
|
37
87
|
# Dictionaries used in optimization to get nodes and attackers by id
|
|
38
88
|
# or full name faster
|
|
39
|
-
self._id_to_node: dict[int, AttackGraphNode] = {}
|
|
40
89
|
self._full_name_to_node: dict[str, AttackGraphNode] = {}
|
|
41
|
-
self._id_to_attacker: dict[int, Attacker] = {}
|
|
42
90
|
|
|
43
91
|
self.model = model
|
|
44
92
|
self.lang_graph = lang_graph
|
|
@@ -48,16 +96,17 @@ class AttackGraph():
|
|
|
48
96
|
self._generate_graph()
|
|
49
97
|
|
|
50
98
|
def __repr__(self) -> str:
|
|
51
|
-
return f'AttackGraph({len(self.nodes)}
|
|
99
|
+
return (f'AttackGraph(Number of nodes: {len(self.nodes)}, '
|
|
100
|
+
f'model: {self.model}, language: {self.lang_graph}')
|
|
52
101
|
|
|
53
102
|
def _to_dict(self) -> dict:
|
|
54
103
|
"""Convert AttackGraph to dict"""
|
|
55
104
|
serialized_attack_steps = {}
|
|
56
105
|
serialized_attackers = {}
|
|
57
|
-
for ag_node in self.nodes:
|
|
106
|
+
for ag_node in self.nodes.values():
|
|
58
107
|
serialized_attack_steps[ag_node.full_name] =\
|
|
59
108
|
ag_node.to_dict()
|
|
60
|
-
for attacker in self.attackers:
|
|
109
|
+
for attacker in self.attackers.values():
|
|
61
110
|
serialized_attackers[attacker.name] = attacker.to_dict()
|
|
62
111
|
logger.debug('Serialized %d attack steps and %d attackers.' %
|
|
63
112
|
(len(self.nodes), len(self.attackers))
|
|
@@ -76,34 +125,32 @@ class AttackGraph():
|
|
|
76
125
|
copied_attackgraph = AttackGraph(self.lang_graph)
|
|
77
126
|
copied_attackgraph.model = self.model
|
|
78
127
|
|
|
79
|
-
copied_attackgraph.nodes =
|
|
128
|
+
copied_attackgraph.nodes = {}
|
|
80
129
|
|
|
81
130
|
# Deep copy nodes
|
|
82
|
-
for node in self.nodes:
|
|
131
|
+
for node_id, node in self.nodes.items():
|
|
83
132
|
copied_node = copy.deepcopy(node, memo)
|
|
84
|
-
copied_attackgraph.nodes
|
|
133
|
+
copied_attackgraph.nodes[node_id] = copied_node
|
|
85
134
|
|
|
86
135
|
# Re-link node references
|
|
87
|
-
for node in self.nodes:
|
|
136
|
+
for node in self.nodes.values():
|
|
88
137
|
if node.parents:
|
|
89
138
|
memo[id(node)].parents = copy.deepcopy(node.parents, memo)
|
|
90
139
|
if node.children:
|
|
91
140
|
memo[id(node)].children = copy.deepcopy(node.children, memo)
|
|
92
141
|
|
|
93
|
-
# Deep copy attackers
|
|
94
|
-
|
|
142
|
+
# Deep copy attackers
|
|
143
|
+
for attacker_id, attacker in self.attackers.items():
|
|
144
|
+
copied_attacker = copy.deepcopy(attacker, memo)
|
|
145
|
+
copied_attackgraph.attackers[attacker_id] = copied_attacker
|
|
95
146
|
|
|
96
147
|
# Re-link attacker references
|
|
97
|
-
for node in self.nodes:
|
|
148
|
+
for node in self.nodes.values():
|
|
98
149
|
if node.compromised_by:
|
|
99
150
|
memo[id(node)].compromised_by = copy.deepcopy(
|
|
100
151
|
node.compromised_by, memo)
|
|
101
152
|
|
|
102
153
|
# Copy lookup dicts
|
|
103
|
-
copied_attackgraph._id_to_attacker = \
|
|
104
|
-
copy.deepcopy(self._id_to_attacker, memo)
|
|
105
|
-
copied_attackgraph._id_to_node = \
|
|
106
|
-
copy.deepcopy(self._id_to_node, memo)
|
|
107
154
|
copied_attackgraph._full_name_to_node = \
|
|
108
155
|
copy.deepcopy(self._full_name_to_node, memo)
|
|
109
156
|
|
|
@@ -144,8 +191,8 @@ class AttackGraph():
|
|
|
144
191
|
if model and 'asset' in node_dict:
|
|
145
192
|
node_asset = model.get_asset_by_name(node_dict['asset'])
|
|
146
193
|
if node_asset is None:
|
|
147
|
-
msg = ('Failed to find asset with
|
|
148
|
-
'when loading from attack graph dict')
|
|
194
|
+
msg = ('Failed to find asset with name "%s"'
|
|
195
|
+
' when loading from attack graph dict')
|
|
149
196
|
logger.error(msg, node_dict["asset"])
|
|
150
197
|
raise LookupError(msg % node_dict["asset"])
|
|
151
198
|
|
|
@@ -154,13 +201,15 @@ class AttackGraph():
|
|
|
154
201
|
node_dict['lang_graph_attack_step'])
|
|
155
202
|
lg_attack_step = lang_graph.assets[lg_asset_name].\
|
|
156
203
|
attack_steps[lg_attack_step_name]
|
|
157
|
-
ag_node =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
204
|
+
ag_node = attack_graph.add_node(
|
|
205
|
+
lg_attack_step = lg_attack_step,
|
|
206
|
+
node_id = node_dict['id'],
|
|
207
|
+
model_asset = node_asset,
|
|
208
|
+
defense_status = node_dict.get('defense_status', None),
|
|
209
|
+
existence_status = node_dict.get('existence_status', None)
|
|
163
210
|
)
|
|
211
|
+
ag_node.tags = set(node_dict.get('tags', []))
|
|
212
|
+
ag_node.extras = node_dict.get('extras', {})
|
|
164
213
|
|
|
165
214
|
if node_asset:
|
|
166
215
|
# Add AttackGraphNode to attack_step_nodes of asset
|
|
@@ -171,24 +220,10 @@ class AttackGraph():
|
|
|
171
220
|
else:
|
|
172
221
|
node_asset.attack_step_nodes = [ag_node]
|
|
173
222
|
|
|
174
|
-
ag_node.defense_status = float(node_dict['defense_status']) if \
|
|
175
|
-
'defense_status' in node_dict else None
|
|
176
|
-
ag_node.existence_status = node_dict['existence_status'] \
|
|
177
|
-
== 'True' if 'existence_status' in node_dict else None
|
|
178
|
-
ag_node.is_viable = node_dict['is_viable'] == 'True' if \
|
|
179
|
-
'is_viable' in node_dict else True
|
|
180
|
-
ag_node.is_necessary = node_dict['is_necessary'] == 'True' if \
|
|
181
|
-
'is_necessary' in node_dict else True
|
|
182
|
-
ag_node.tags = set(node_dict['tags']) if \
|
|
183
|
-
'tags' in node_dict else set()
|
|
184
|
-
ag_node.extras = node_dict.get('extras', {})
|
|
185
|
-
|
|
186
|
-
# Add AttackGraphNode to AttackGraph
|
|
187
|
-
attack_graph.add_node(ag_node, node_id=node_dict['id'])
|
|
188
223
|
|
|
189
224
|
# Re-establish links between nodes.
|
|
190
225
|
for node_dict in serialized_attack_steps.values():
|
|
191
|
-
_ag_node = attack_graph.
|
|
226
|
+
_ag_node = attack_graph.nodes[node_dict['id']]
|
|
192
227
|
if not isinstance(_ag_node, AttackGraphNode):
|
|
193
228
|
msg = ('Failed to find node with id %s when loading'
|
|
194
229
|
' attack graph from dict')
|
|
@@ -196,33 +231,36 @@ class AttackGraph():
|
|
|
196
231
|
raise LookupError(msg % node_dict["id"])
|
|
197
232
|
else:
|
|
198
233
|
for child_id in node_dict['children']:
|
|
199
|
-
child = attack_graph.
|
|
234
|
+
child = attack_graph.nodes[int(child_id)]
|
|
200
235
|
if child is None:
|
|
201
236
|
msg = ('Failed to find child node with id %s'
|
|
202
237
|
' when loading from attack graph from dict')
|
|
203
238
|
logger.error(msg, child_id)
|
|
204
239
|
raise LookupError(msg % child_id)
|
|
205
|
-
_ag_node.children.
|
|
240
|
+
_ag_node.children.add(child)
|
|
206
241
|
|
|
207
242
|
for parent_id in node_dict['parents']:
|
|
208
|
-
parent = attack_graph.
|
|
243
|
+
parent = attack_graph.nodes[int(parent_id)]
|
|
209
244
|
if parent is None:
|
|
210
245
|
msg = ('Failed to find parent node with id %s '
|
|
211
246
|
'when loading from attack graph from dict')
|
|
212
247
|
logger.error(msg, parent_id)
|
|
213
248
|
raise LookupError(msg % parent_id)
|
|
214
|
-
_ag_node.parents.
|
|
249
|
+
_ag_node.parents.add(parent)
|
|
215
250
|
|
|
216
251
|
for attacker in serialized_attackers.values():
|
|
217
252
|
ag_attacker = Attacker(
|
|
218
253
|
name = attacker['name'],
|
|
219
|
-
entry_points =
|
|
220
|
-
reached_attack_steps =
|
|
254
|
+
entry_points = set(),
|
|
255
|
+
reached_attack_steps = set()
|
|
221
256
|
)
|
|
222
257
|
attack_graph.add_attacker(
|
|
223
258
|
attacker = ag_attacker,
|
|
224
259
|
attacker_id = int(attacker['id']),
|
|
225
|
-
entry_points =
|
|
260
|
+
entry_points = [
|
|
261
|
+
int(node_id) # Convert to int since they can be strings
|
|
262
|
+
for node_id in attacker['entry_points'].keys()
|
|
263
|
+
],
|
|
226
264
|
reached_attack_steps = [
|
|
227
265
|
int(node_id) # Convert to int since they can be strings
|
|
228
266
|
for node_id in attacker['reached_attack_steps'].keys()
|
|
@@ -255,20 +293,6 @@ class AttackGraph():
|
|
|
255
293
|
return cls._from_dict(serialized_attack_graph,
|
|
256
294
|
lang_graph, model = model)
|
|
257
295
|
|
|
258
|
-
def get_node_by_id(self, node_id: int) -> Optional[AttackGraphNode]:
|
|
259
|
-
"""
|
|
260
|
-
Return the attack node that matches the id provided.
|
|
261
|
-
|
|
262
|
-
Arguments:
|
|
263
|
-
node_id - the id of the attack graph node we are looking for
|
|
264
|
-
|
|
265
|
-
Return:
|
|
266
|
-
The attack step node that matches the given id.
|
|
267
|
-
"""
|
|
268
|
-
|
|
269
|
-
logger.debug('Looking up node with id %s', node_id)
|
|
270
|
-
return self._id_to_node.get(node_id)
|
|
271
|
-
|
|
272
296
|
def get_node_by_full_name(self, full_name: str) -> Optional[AttackGraphNode]:
|
|
273
297
|
"""
|
|
274
298
|
Return the attack node that matches the full name provided.
|
|
@@ -284,20 +308,6 @@ class AttackGraph():
|
|
|
284
308
|
logger.debug(f'Looking up node with full name "%s"', full_name)
|
|
285
309
|
return self._full_name_to_node.get(full_name)
|
|
286
310
|
|
|
287
|
-
def get_attacker_by_id(self, attacker_id: int) -> Optional[Attacker]:
|
|
288
|
-
"""
|
|
289
|
-
Return the attacker that matches the id provided.
|
|
290
|
-
|
|
291
|
-
Arguments:
|
|
292
|
-
attacker_id - the id of the attacker we are looking for
|
|
293
|
-
|
|
294
|
-
Return:
|
|
295
|
-
The attacker that matches the given id.
|
|
296
|
-
"""
|
|
297
|
-
|
|
298
|
-
logger.debug(f'Looking up attacker with id {attacker_id}')
|
|
299
|
-
return self._id_to_attacker.get(attacker_id)
|
|
300
|
-
|
|
301
311
|
def attach_attackers(self) -> None:
|
|
302
312
|
"""
|
|
303
313
|
Create attackers and their entry point nodes and attach them to the
|
|
@@ -322,8 +332,8 @@ class AttackGraph():
|
|
|
322
332
|
|
|
323
333
|
attacker = Attacker(
|
|
324
334
|
name = attacker_info.name,
|
|
325
|
-
entry_points =
|
|
326
|
-
reached_attack_steps =
|
|
335
|
+
entry_points = set(),
|
|
336
|
+
reached_attack_steps = set()
|
|
327
337
|
)
|
|
328
338
|
self.add_attacker(attacker)
|
|
329
339
|
|
|
@@ -340,12 +350,12 @@ class AttackGraph():
|
|
|
340
350
|
continue
|
|
341
351
|
attacker.compromise(ag_node)
|
|
342
352
|
|
|
343
|
-
attacker.entry_points =
|
|
353
|
+
attacker.entry_points = set(attacker.reached_attack_steps)
|
|
344
354
|
|
|
345
355
|
def _follow_expr_chain(
|
|
346
356
|
self,
|
|
347
357
|
model: Model,
|
|
348
|
-
target_assets: set[
|
|
358
|
+
target_assets: set[ModelAsset],
|
|
349
359
|
expr_chain: Optional[ExpressionsChain]
|
|
350
360
|
) -> set[Any]:
|
|
351
361
|
"""
|
|
@@ -420,10 +430,9 @@ class AttackGraph():
|
|
|
420
430
|
new_target_assets = set()
|
|
421
431
|
new_target_assets.update(
|
|
422
432
|
*(
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
)
|
|
426
|
-
for asset in target_assets
|
|
433
|
+
asset.associated_assets.get(
|
|
434
|
+
expr_chain.fieldname, set()
|
|
435
|
+
) for asset in target_assets
|
|
427
436
|
)
|
|
428
437
|
)
|
|
429
438
|
return new_target_assets
|
|
@@ -518,7 +527,7 @@ class AttackGraph():
|
|
|
518
527
|
raise AttackGraphException(msg)
|
|
519
528
|
|
|
520
529
|
# First, generate all of the nodes of the attack graph.
|
|
521
|
-
for asset in self.model.assets:
|
|
530
|
+
for asset in self.model.assets.values():
|
|
522
531
|
|
|
523
532
|
logger.debug(
|
|
524
533
|
'Generating attack steps for asset %s which is of class %s.',
|
|
@@ -527,14 +536,7 @@ class AttackGraph():
|
|
|
527
536
|
|
|
528
537
|
attack_step_nodes = []
|
|
529
538
|
|
|
530
|
-
|
|
531
|
-
if lang_graph_asset is None:
|
|
532
|
-
raise LookupError(
|
|
533
|
-
f'Failed to find asset with name \"{asset.type}\" in '
|
|
534
|
-
'the language graph.'
|
|
535
|
-
)
|
|
536
|
-
|
|
537
|
-
for attack_step in lang_graph_asset.attack_steps.values():
|
|
539
|
+
for attack_step in asset.lg_asset.attack_steps.values():
|
|
538
540
|
logger.debug(
|
|
539
541
|
'Generating attack step node for %s.', attack_step.name
|
|
540
542
|
)
|
|
@@ -546,10 +548,9 @@ class AttackGraph():
|
|
|
546
548
|
match (attack_step.type):
|
|
547
549
|
case 'defense':
|
|
548
550
|
# Set the defense status for defenses
|
|
549
|
-
defense_status =
|
|
551
|
+
defense_status = asset.defenses[attack_step.name]
|
|
550
552
|
logger.debug(
|
|
551
|
-
'Setting the defense status of \"%s\" to '
|
|
552
|
-
'\"%s\".',
|
|
553
|
+
'Setting the defense status of \"%s\" to "%s".',
|
|
553
554
|
node_name, defense_status
|
|
554
555
|
)
|
|
555
556
|
|
|
@@ -570,42 +571,40 @@ class AttackGraph():
|
|
|
570
571
|
existence_status = True
|
|
571
572
|
break
|
|
572
573
|
|
|
574
|
+
logger.debug(
|
|
575
|
+
'Setting the existence status of \"%s\" to '
|
|
576
|
+
'%s.',
|
|
577
|
+
node_name, existence_status
|
|
578
|
+
)
|
|
579
|
+
|
|
573
580
|
case _:
|
|
574
581
|
pass
|
|
575
582
|
|
|
576
|
-
ag_node =
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
asset = asset,
|
|
580
|
-
name = attack_step.name,
|
|
581
|
-
ttc = attack_step.ttc,
|
|
582
|
-
children = [],
|
|
583
|
-
parents = [],
|
|
583
|
+
ag_node = self.add_node(
|
|
584
|
+
lg_attack_step = attack_step,
|
|
585
|
+
model_asset = asset,
|
|
584
586
|
defense_status = defense_status,
|
|
585
|
-
existence_status = existence_status
|
|
586
|
-
is_viable = True,
|
|
587
|
-
is_necessary = True,
|
|
588
|
-
tags = set(attack_step.tags),
|
|
589
|
-
compromised_by = []
|
|
587
|
+
existence_status = existence_status
|
|
590
588
|
)
|
|
591
589
|
attack_step_nodes.append(ag_node)
|
|
592
|
-
|
|
590
|
+
|
|
593
591
|
asset.attack_step_nodes = attack_step_nodes
|
|
594
592
|
|
|
595
593
|
# Then, link all of the nodes according to their associations.
|
|
596
|
-
for ag_node in self.nodes:
|
|
594
|
+
for ag_node in self.nodes.values():
|
|
597
595
|
logger.debug(
|
|
598
596
|
'Determining children for attack step "%s"(%d)',
|
|
599
597
|
ag_node.full_name,
|
|
600
598
|
ag_node.id
|
|
601
599
|
)
|
|
602
600
|
|
|
603
|
-
if not ag_node.
|
|
601
|
+
if not ag_node.model_asset:
|
|
604
602
|
raise AttackGraphException('Attack graph node is missing '
|
|
605
603
|
'asset link')
|
|
606
|
-
lang_graph_asset = self.lang_graph.assets[
|
|
604
|
+
lang_graph_asset = self.lang_graph.assets[
|
|
605
|
+
ag_node.model_asset.type]
|
|
607
606
|
|
|
608
|
-
lang_graph_attack_step = lang_graph_asset.attack_steps[
|
|
607
|
+
lang_graph_attack_step = lang_graph_asset.attack_steps[
|
|
609
608
|
ag_node.name]
|
|
610
609
|
|
|
611
610
|
while lang_graph_attack_step:
|
|
@@ -613,7 +612,7 @@ class AttackGraph():
|
|
|
613
612
|
for target_attack_step, expr_chain in child:
|
|
614
613
|
target_assets = self._follow_expr_chain(
|
|
615
614
|
self.model,
|
|
616
|
-
set([ag_node.
|
|
615
|
+
set([ag_node.model_asset]),
|
|
617
616
|
expr_chain
|
|
618
617
|
)
|
|
619
618
|
|
|
@@ -653,8 +652,8 @@ class AttackGraph():
|
|
|
653
652
|
target_node.id
|
|
654
653
|
)
|
|
655
654
|
)
|
|
656
|
-
ag_node.children.
|
|
657
|
-
target_node.parents.
|
|
655
|
+
ag_node.children.add(target_node)
|
|
656
|
+
target_node.parents.add(ag_node)
|
|
658
657
|
if lang_graph_attack_step.overrides:
|
|
659
658
|
break
|
|
660
659
|
lang_graph_attack_step = lang_graph_attack_step.inherits
|
|
@@ -666,37 +665,68 @@ class AttackGraph():
|
|
|
666
665
|
the MAL language specification provided at initialization.
|
|
667
666
|
"""
|
|
668
667
|
|
|
669
|
-
self.nodes =
|
|
670
|
-
self.attackers =
|
|
668
|
+
self.nodes = {}
|
|
669
|
+
self.attackers = {}
|
|
671
670
|
self._generate_graph()
|
|
672
671
|
|
|
673
672
|
def add_node(
|
|
674
673
|
self,
|
|
675
|
-
|
|
676
|
-
node_id: Optional[int] = None
|
|
677
|
-
|
|
678
|
-
|
|
674
|
+
lg_attack_step: LanguageGraphAttackStep,
|
|
675
|
+
node_id: Optional[int] = None,
|
|
676
|
+
model_asset: Optional[ModelAsset] = None,
|
|
677
|
+
defense_status: Optional[float] = None,
|
|
678
|
+
existence_status: Optional[bool] = None
|
|
679
|
+
) -> AttackGraphNode:
|
|
680
|
+
"""Create and add a node to the graph
|
|
679
681
|
Arguments:
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
682
|
+
lg_attack_step - the language graph attack step that corresponds
|
|
683
|
+
to the attack graph node to create
|
|
684
|
+
node_id - id to assign to the newly created node, usually
|
|
685
|
+
provided only when loading an existing attack
|
|
686
|
+
graph from a file. If not provided the id will
|
|
687
|
+
be set to the next highest id available.
|
|
688
|
+
model_asset - the model asset that corresponds to the attack
|
|
689
|
+
step node. While optional it is highly
|
|
690
|
+
recommended that this be provided. It should
|
|
691
|
+
only be ommitted if the model which was used to
|
|
692
|
+
generate the attack graph is not available when
|
|
693
|
+
loading an attack graph from a file.
|
|
694
|
+
defese_status - the defense status of the node. Only, relevant
|
|
695
|
+
for defense type nodes. A value between 0.0 and
|
|
696
|
+
1.0 is expected.
|
|
697
|
+
existence_status - the existence status of the node. Only, relevant
|
|
698
|
+
for exist and notExist type nodes.
|
|
689
699
|
|
|
690
|
-
|
|
700
|
+
Return:
|
|
701
|
+
The newly created attack step node.
|
|
702
|
+
"""
|
|
703
|
+
node_id = node_id if node_id is not None else self.next_node_id
|
|
704
|
+
if node_id in self.nodes:
|
|
691
705
|
raise ValueError(f'Node index {node_id} already in use.')
|
|
706
|
+
self.next_node_id = max(node_id + 1, self.next_node_id)
|
|
692
707
|
|
|
693
|
-
|
|
694
|
-
|
|
708
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
709
|
+
# Avoid running json.dumps when not in debug
|
|
710
|
+
logger.debug('Create and add to attackgraph node of type "%s" '
|
|
711
|
+
'with id:%d.\n' % (
|
|
712
|
+
lg_attack_step.full_name,
|
|
713
|
+
node_id
|
|
714
|
+
))
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
node = AttackGraphNode(
|
|
718
|
+
node_id = node_id,
|
|
719
|
+
lg_attack_step = lg_attack_step,
|
|
720
|
+
model_asset = model_asset,
|
|
721
|
+
defense_status = defense_status,
|
|
722
|
+
existence_status = existence_status
|
|
723
|
+
)
|
|
695
724
|
|
|
696
|
-
self.nodes
|
|
697
|
-
self._id_to_node[node.id] = node
|
|
725
|
+
self.nodes[node_id] = node
|
|
698
726
|
self._full_name_to_node[node.full_name] = node
|
|
699
727
|
|
|
728
|
+
return node
|
|
729
|
+
|
|
700
730
|
def remove_node(self, node: AttackGraphNode) -> None:
|
|
701
731
|
"""Remove node from attack graph
|
|
702
732
|
Arguments:
|
|
@@ -709,11 +739,10 @@ class AttackGraph():
|
|
|
709
739
|
child.parents.remove(node)
|
|
710
740
|
for parent in node.parents:
|
|
711
741
|
parent.children.remove(node)
|
|
712
|
-
self.nodes.remove(node)
|
|
713
742
|
|
|
714
743
|
if not isinstance(node.id, int):
|
|
715
744
|
raise ValueError(f'Invalid node id.')
|
|
716
|
-
del self.
|
|
745
|
+
del self.nodes[node.id]
|
|
717
746
|
del self._full_name_to_node[node.full_name]
|
|
718
747
|
|
|
719
748
|
def add_attacker(
|
|
@@ -748,12 +777,12 @@ class AttackGraph():
|
|
|
748
777
|
)
|
|
749
778
|
|
|
750
779
|
attacker.id = attacker_id or self.next_attacker_id
|
|
751
|
-
if attacker.id in self.
|
|
780
|
+
if attacker.id in self.attackers:
|
|
752
781
|
raise ValueError(f'Attacker index {attacker_id} already in use.')
|
|
753
782
|
|
|
754
783
|
self.next_attacker_id = max(attacker.id + 1, self.next_attacker_id)
|
|
755
784
|
for node_id in reached_attack_steps:
|
|
756
|
-
node = self.
|
|
785
|
+
node = self.nodes[node_id]
|
|
757
786
|
if node:
|
|
758
787
|
attacker.compromise(node)
|
|
759
788
|
else:
|
|
@@ -762,16 +791,15 @@ class AttackGraph():
|
|
|
762
791
|
logger.error(msg, node_id)
|
|
763
792
|
raise AttackGraphException(msg % node_id)
|
|
764
793
|
for node_id in entry_points:
|
|
765
|
-
node = self.
|
|
794
|
+
node = self.nodes[node_id]
|
|
766
795
|
if node:
|
|
767
|
-
attacker.entry_points.
|
|
796
|
+
attacker.entry_points.add(node)
|
|
768
797
|
else:
|
|
769
798
|
msg = ("Could not find node with id %d"
|
|
770
799
|
"in attacker entrypoints.")
|
|
771
800
|
logger.error(msg, node_id)
|
|
772
801
|
raise AttackGraphException(msg % node_id)
|
|
773
|
-
self.attackers.
|
|
774
|
-
self._id_to_attacker[attacker.id] = attacker
|
|
802
|
+
self.attackers[attacker.id] = attacker
|
|
775
803
|
|
|
776
804
|
def remove_attacker(self, attacker: Attacker):
|
|
777
805
|
"""Remove attacker from attack graph
|
|
@@ -785,7 +813,6 @@ class AttackGraph():
|
|
|
785
813
|
attacker.id)
|
|
786
814
|
for node in attacker.reached_attack_steps:
|
|
787
815
|
attacker.undo_compromise(node)
|
|
788
|
-
self.attackers.remove(attacker)
|
|
789
816
|
if not isinstance(attacker.id, int):
|
|
790
817
|
raise ValueError(f'Invalid attacker id.')
|
|
791
|
-
del self.
|
|
818
|
+
del self.attackers[attacker.id]
|