mal-toolbox 0.1.11__py3-none-any.whl → 0.2.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.1.11.dist-info → mal_toolbox-0.2.0.dist-info}/METADATA +2 -2
- {mal_toolbox-0.1.11.dist-info → mal_toolbox-0.2.0.dist-info}/RECORD +15 -15
- {mal_toolbox-0.1.11.dist-info → mal_toolbox-0.2.0.dist-info}/WHEEL +1 -1
- maltoolbox/__init__.py +2 -2
- maltoolbox/attackgraph/analyzers/apriori.py +61 -1
- maltoolbox/attackgraph/attackgraph.py +294 -245
- maltoolbox/attackgraph/node.py +23 -7
- maltoolbox/file_utils.py +6 -2
- maltoolbox/language/__init__.py +5 -1
- maltoolbox/language/classes_factory.py +86 -70
- maltoolbox/language/languagegraph.py +1022 -475
- maltoolbox/model.py +44 -35
- {mal_toolbox-0.1.11.dist-info → mal_toolbox-0.2.0.dist-info}/AUTHORS +0 -0
- {mal_toolbox-0.1.11.dist-info → mal_toolbox-0.2.0.dist-info}/LICENSE +0 -0
- {mal_toolbox-0.1.11.dist-info → mal_toolbox-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -6,193 +6,32 @@ import copy
|
|
|
6
6
|
import logging
|
|
7
7
|
import json
|
|
8
8
|
|
|
9
|
+
from itertools import chain
|
|
9
10
|
from typing import TYPE_CHECKING
|
|
10
11
|
|
|
11
12
|
from .node import AttackGraphNode
|
|
12
13
|
from .attacker import Attacker
|
|
13
|
-
from ..exceptions import AttackGraphStepExpressionError
|
|
14
|
+
from ..exceptions import AttackGraphStepExpressionError, AttackGraphException
|
|
15
|
+
from ..exceptions import LanguageGraphException
|
|
14
16
|
from ..model import Model
|
|
15
|
-
from ..
|
|
17
|
+
from ..language import (LanguageGraph, ExpressionsChain,
|
|
18
|
+
disaggregate_attack_step_full_name)
|
|
16
19
|
from ..file_utils import (
|
|
17
20
|
load_dict_from_json_file,
|
|
18
21
|
load_dict_from_yaml_file,
|
|
19
22
|
save_dict_to_file
|
|
20
23
|
)
|
|
21
24
|
|
|
25
|
+
|
|
22
26
|
if TYPE_CHECKING:
|
|
23
27
|
from typing import Any, Optional
|
|
24
|
-
from ..language import LanguageGraph
|
|
25
28
|
|
|
26
29
|
logger = logging.getLogger(__name__)
|
|
27
30
|
|
|
28
|
-
# TODO see if (part of) this can be incorporated into the LanguageGraph, so that
|
|
29
|
-
# the LanguageGraph's _lang_spec private property does not need to be accessed
|
|
30
|
-
def _process_step_expression(
|
|
31
|
-
lang_graph: LanguageGraph,
|
|
32
|
-
model: Model,
|
|
33
|
-
target_assets: list[Any],
|
|
34
|
-
step_expression: dict[str, Any]
|
|
35
|
-
) -> tuple[list, Optional[str]]:
|
|
36
|
-
"""
|
|
37
|
-
Recursively process an attack step expression.
|
|
38
|
-
|
|
39
|
-
Arguments:
|
|
40
|
-
lang_graph - a language graph representing the MAL language
|
|
41
|
-
specification
|
|
42
|
-
model - a maltoolbox.model.Model instance from which the attack
|
|
43
|
-
graph was generated
|
|
44
|
-
target_assets - the list of assets that this step expression should apply
|
|
45
|
-
to. Initially it will contain the asset to which the
|
|
46
|
-
attack step belongs
|
|
47
|
-
step_expression - a dictionary containing the step expression
|
|
48
|
-
|
|
49
|
-
Return:
|
|
50
|
-
A tuple pair containing a list of all of the target assets and the name of
|
|
51
|
-
the attack step.
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
if logger.isEnabledFor(logging.DEBUG):
|
|
55
|
-
# Avoid running json.dumps when not in debug
|
|
56
|
-
logger.debug(
|
|
57
|
-
'Processing Step Expression:\n%s',
|
|
58
|
-
json.dumps(step_expression, indent = 2)
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
match (step_expression['type']):
|
|
62
|
-
case 'attackStep':
|
|
63
|
-
# The attack step expression just adds the name of the attack
|
|
64
|
-
# step. All other step expressions only modify the target assets.
|
|
65
|
-
return (target_assets, step_expression['name'])
|
|
66
|
-
|
|
67
|
-
case 'union' | 'intersection' | 'difference':
|
|
68
|
-
# The set operators are used to combine the left hand and right
|
|
69
|
-
# hand targets accordingly.
|
|
70
|
-
lh_targets, lh_attack_steps = _process_step_expression(
|
|
71
|
-
lang_graph, model, target_assets, step_expression['lhs'])
|
|
72
|
-
rh_targets, rh_attack_steps = _process_step_expression(
|
|
73
|
-
lang_graph, model, target_assets, step_expression['rhs'])
|
|
74
|
-
|
|
75
|
-
new_target_assets = []
|
|
76
|
-
match (step_expression['type']):
|
|
77
|
-
case 'union':
|
|
78
|
-
new_target_assets = lh_targets
|
|
79
|
-
for ag_node in rh_targets:
|
|
80
|
-
if next((lnode for lnode in new_target_assets \
|
|
81
|
-
if lnode.id != ag_node.id), None):
|
|
82
|
-
new_target_assets.append(ag_node)
|
|
83
|
-
|
|
84
|
-
case 'intersection':
|
|
85
|
-
for ag_node in rh_targets:
|
|
86
|
-
if next((lnode for lnode in lh_targets \
|
|
87
|
-
if lnode.id == ag_node.id), None):
|
|
88
|
-
new_target_assets.append(ag_node)
|
|
89
|
-
|
|
90
|
-
case 'difference':
|
|
91
|
-
new_target_assets = lh_targets
|
|
92
|
-
for ag_node in lh_targets:
|
|
93
|
-
if next((rnode for rnode in rh_targets \
|
|
94
|
-
if rnode.id != ag_node.id), None):
|
|
95
|
-
new_target_assets.remove(ag_node)
|
|
96
|
-
|
|
97
|
-
return (new_target_assets, None)
|
|
98
|
-
|
|
99
|
-
case 'variable':
|
|
100
|
-
# Fetch the step expression associated with the variable from
|
|
101
|
-
# the language specification and resolve that.
|
|
102
|
-
for target_asset in target_assets:
|
|
103
|
-
if (hasattr(target_asset, 'type')):
|
|
104
|
-
# TODO how can this info be accessed in the lang_graph
|
|
105
|
-
# directly without going through the private method?
|
|
106
|
-
variable_step_expr = lang_graph._get_variable_for_asset_type_by_name(
|
|
107
|
-
target_asset.type, step_expression['name'])
|
|
108
|
-
return _process_step_expression(
|
|
109
|
-
lang_graph, model, target_assets, variable_step_expr)
|
|
110
|
-
|
|
111
|
-
else:
|
|
112
|
-
logger.error(
|
|
113
|
-
'Requested variable from non-asset target node:'
|
|
114
|
-
'%s which cannot be resolved.', target_asset
|
|
115
|
-
)
|
|
116
|
-
return ([], None)
|
|
117
|
-
|
|
118
|
-
case 'field':
|
|
119
|
-
# Change the target assets from the current ones to the associated
|
|
120
|
-
# assets given the specified field name.
|
|
121
|
-
new_target_assets = []
|
|
122
|
-
for target_asset in target_assets:
|
|
123
|
-
new_target_assets.extend(model.\
|
|
124
|
-
get_associated_assets_by_field_name(target_asset,
|
|
125
|
-
step_expression['name']))
|
|
126
|
-
return (new_target_assets, None)
|
|
127
|
-
|
|
128
|
-
case 'transitive':
|
|
129
|
-
# The transitive expression is very similar to the field
|
|
130
|
-
# expression, but it proceeds recursively until no target is
|
|
131
|
-
# found and it and it sets the new targets to the entire list
|
|
132
|
-
# of assets identified during the entire transitive recursion.
|
|
133
|
-
new_target_assets = []
|
|
134
|
-
for target_asset in target_assets:
|
|
135
|
-
new_target_assets.extend(model.\
|
|
136
|
-
get_associated_assets_by_field_name(target_asset,
|
|
137
|
-
step_expression['stepExpression']['name']))
|
|
138
|
-
if new_target_assets:
|
|
139
|
-
(additional_assets, _) = _process_step_expression(
|
|
140
|
-
lang_graph, model, new_target_assets, step_expression)
|
|
141
|
-
new_target_assets.extend(additional_assets)
|
|
142
|
-
return (new_target_assets, None)
|
|
143
|
-
else:
|
|
144
|
-
return ([], None)
|
|
145
|
-
|
|
146
|
-
case 'subType':
|
|
147
|
-
new_target_assets = []
|
|
148
|
-
for target_asset in target_assets:
|
|
149
|
-
(assets, _) = _process_step_expression(
|
|
150
|
-
lang_graph, model, target_assets,
|
|
151
|
-
step_expression['stepExpression'])
|
|
152
|
-
new_target_assets.extend(assets)
|
|
153
|
-
|
|
154
|
-
selected_new_target_assets = []
|
|
155
|
-
for asset in new_target_assets:
|
|
156
|
-
lang_graph_asset = lang_graph.get_asset_by_name(
|
|
157
|
-
asset.type
|
|
158
|
-
)
|
|
159
|
-
if not lang_graph_asset:
|
|
160
|
-
raise LookupError(
|
|
161
|
-
f'Failed to find asset \"{asset.type}\" in the '
|
|
162
|
-
'language graph.'
|
|
163
|
-
)
|
|
164
|
-
lang_graph_subtype_asset = lang_graph.get_asset_by_name(
|
|
165
|
-
step_expression['subType']
|
|
166
|
-
)
|
|
167
|
-
if not lang_graph_subtype_asset:
|
|
168
|
-
raise LookupError(
|
|
169
|
-
'Failed to find asset '
|
|
170
|
-
f'\"{step_expression["subType"]}\" in the '
|
|
171
|
-
'language graph.'
|
|
172
|
-
)
|
|
173
|
-
if lang_graph_asset.is_subasset_of(lang_graph_subtype_asset):
|
|
174
|
-
selected_new_target_assets.append(asset)
|
|
175
|
-
|
|
176
|
-
return (selected_new_target_assets, None)
|
|
177
|
-
|
|
178
|
-
case 'collect':
|
|
179
|
-
# Apply the right hand step expression to left hand step
|
|
180
|
-
# expression target assets.
|
|
181
|
-
lh_targets, _ = _process_step_expression(
|
|
182
|
-
lang_graph, model, target_assets, step_expression['lhs'])
|
|
183
|
-
return _process_step_expression(lang_graph, model, lh_targets,
|
|
184
|
-
step_expression['rhs'])
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
case _:
|
|
188
|
-
logger.error(
|
|
189
|
-
'Unknown attack step type: %s', step_expression["type"]
|
|
190
|
-
)
|
|
191
|
-
return ([], None)
|
|
192
31
|
|
|
193
32
|
class AttackGraph():
|
|
194
33
|
"""Graph representation of attack steps"""
|
|
195
|
-
def __init__(self, lang_graph
|
|
34
|
+
def __init__(self, lang_graph, model: Optional[Model] = None):
|
|
196
35
|
self.nodes: list[AttackGraphNode] = []
|
|
197
36
|
self.attackers: list[Attacker] = []
|
|
198
37
|
# Dictionaries used in optimization to get nodes and attackers by id
|
|
@@ -205,7 +44,7 @@ class AttackGraph():
|
|
|
205
44
|
self.lang_graph = lang_graph
|
|
206
45
|
self.next_node_id = 0
|
|
207
46
|
self.next_attacker_id = 0
|
|
208
|
-
if self.model is not None
|
|
47
|
+
if self.model is not None:
|
|
209
48
|
self._generate_graph()
|
|
210
49
|
|
|
211
50
|
def __repr__(self) -> str:
|
|
@@ -220,6 +59,9 @@ class AttackGraph():
|
|
|
220
59
|
ag_node.to_dict()
|
|
221
60
|
for attacker in self.attackers:
|
|
222
61
|
serialized_attackers[attacker.name] = attacker.to_dict()
|
|
62
|
+
logger.debug('Serialized %d attack steps and %d attackers.' %
|
|
63
|
+
(len(self.nodes), len(self.attackers))
|
|
64
|
+
)
|
|
223
65
|
return {
|
|
224
66
|
'attack_steps': serialized_attack_steps,
|
|
225
67
|
'attackers': serialized_attackers,
|
|
@@ -280,6 +122,7 @@ class AttackGraph():
|
|
|
280
122
|
def _from_dict(
|
|
281
123
|
cls,
|
|
282
124
|
serialized_object: dict,
|
|
125
|
+
lang_graph: LanguageGraph,
|
|
283
126
|
model: Optional[Model]=None
|
|
284
127
|
) -> AttackGraph:
|
|
285
128
|
"""Create AttackGraph from dict
|
|
@@ -288,13 +131,13 @@ class AttackGraph():
|
|
|
288
131
|
model - Optional Model to add connections to
|
|
289
132
|
"""
|
|
290
133
|
|
|
291
|
-
attack_graph = AttackGraph()
|
|
134
|
+
attack_graph = AttackGraph(lang_graph)
|
|
292
135
|
attack_graph.model = model
|
|
293
136
|
serialized_attack_steps = serialized_object['attack_steps']
|
|
294
137
|
serialized_attackers = serialized_object['attackers']
|
|
295
138
|
|
|
296
139
|
# Create all of the nodes in the imported attack graph.
|
|
297
|
-
for
|
|
140
|
+
for node_dict in serialized_attack_steps.values():
|
|
298
141
|
|
|
299
142
|
# Recreate asset links if model is available.
|
|
300
143
|
node_asset = None
|
|
@@ -306,8 +149,14 @@ class AttackGraph():
|
|
|
306
149
|
logger.error(msg, node_dict["asset"])
|
|
307
150
|
raise LookupError(msg % node_dict["asset"])
|
|
308
151
|
|
|
152
|
+
lg_asset_name, lg_attack_step_name = \
|
|
153
|
+
disaggregate_attack_step_full_name(
|
|
154
|
+
node_dict['lang_graph_attack_step'])
|
|
155
|
+
lg_attack_step = lang_graph.assets[lg_asset_name].\
|
|
156
|
+
attack_steps[lg_attack_step_name]
|
|
309
157
|
ag_node = AttackGraphNode(
|
|
310
158
|
type=node_dict['type'],
|
|
159
|
+
lang_graph_attack_step = lg_attack_step,
|
|
311
160
|
name=node_dict['name'],
|
|
312
161
|
ttc=node_dict['ttc'],
|
|
313
162
|
asset=node_asset
|
|
@@ -330,17 +179,15 @@ class AttackGraph():
|
|
|
330
179
|
'is_viable' in node_dict else True
|
|
331
180
|
ag_node.is_necessary = node_dict['is_necessary'] == 'True' if \
|
|
332
181
|
'is_necessary' in node_dict else True
|
|
333
|
-
ag_node.
|
|
334
|
-
'
|
|
335
|
-
ag_node.tags = node_dict['tags'] if \
|
|
336
|
-
'tags' in node_dict else []
|
|
182
|
+
ag_node.tags = set(node_dict['tags']) if \
|
|
183
|
+
'tags' in node_dict else set()
|
|
337
184
|
ag_node.extras = node_dict.get('extras', {})
|
|
338
185
|
|
|
339
186
|
# Add AttackGraphNode to AttackGraph
|
|
340
187
|
attack_graph.add_node(ag_node, node_id=node_dict['id'])
|
|
341
188
|
|
|
342
189
|
# Re-establish links between nodes.
|
|
343
|
-
for
|
|
190
|
+
for node_dict in serialized_attack_steps.values():
|
|
344
191
|
_ag_node = attack_graph.get_node_by_id(node_dict['id'])
|
|
345
192
|
if not isinstance(_ag_node, AttackGraphNode):
|
|
346
193
|
msg = ('Failed to find node with id %s when loading'
|
|
@@ -366,7 +213,7 @@ class AttackGraph():
|
|
|
366
213
|
raise LookupError(msg % parent_id)
|
|
367
214
|
_ag_node.parents.append(parent)
|
|
368
215
|
|
|
369
|
-
for
|
|
216
|
+
for attacker in serialized_attackers.values():
|
|
370
217
|
ag_attacker = Attacker(
|
|
371
218
|
name = attacker['name'],
|
|
372
219
|
entry_points = [],
|
|
@@ -388,7 +235,8 @@ class AttackGraph():
|
|
|
388
235
|
def load_from_file(
|
|
389
236
|
cls,
|
|
390
237
|
filename: str,
|
|
391
|
-
|
|
238
|
+
lang_graph: LanguageGraph,
|
|
239
|
+
model: Optional[Model] = None
|
|
392
240
|
) -> AttackGraph:
|
|
393
241
|
"""Create from json or yaml file depending on file extension"""
|
|
394
242
|
if model is not None:
|
|
@@ -404,7 +252,8 @@ class AttackGraph():
|
|
|
404
252
|
serialized_attack_graph = load_dict_from_json_file(filename)
|
|
405
253
|
else:
|
|
406
254
|
raise ValueError('Unknown file extension, expected json/yml/yaml')
|
|
407
|
-
return cls._from_dict(serialized_attack_graph,
|
|
255
|
+
return cls._from_dict(serialized_attack_graph,
|
|
256
|
+
lang_graph, model = model)
|
|
408
257
|
|
|
409
258
|
def get_node_by_id(self, node_id: int) -> Optional[AttackGraphNode]:
|
|
410
259
|
"""
|
|
@@ -432,7 +281,7 @@ class AttackGraph():
|
|
|
432
281
|
The attack step node that matches the given full name.
|
|
433
282
|
"""
|
|
434
283
|
|
|
435
|
-
logger.debug(f'Looking up node with full name "
|
|
284
|
+
logger.debug(f'Looking up node with full name "%s"', full_name)
|
|
436
285
|
return self._full_name_to_node.get(full_name)
|
|
437
286
|
|
|
438
287
|
def get_attacker_by_id(self, attacker_id: int) -> Optional[Attacker]:
|
|
@@ -493,6 +342,170 @@ class AttackGraph():
|
|
|
493
342
|
|
|
494
343
|
attacker.entry_points = list(attacker.reached_attack_steps)
|
|
495
344
|
|
|
345
|
+
def _follow_expr_chain(
|
|
346
|
+
self,
|
|
347
|
+
model: Model,
|
|
348
|
+
target_assets: set[Any],
|
|
349
|
+
expr_chain: Optional[ExpressionsChain]
|
|
350
|
+
) -> set[Any]:
|
|
351
|
+
"""
|
|
352
|
+
Recursively follow a language graph expressions chain on an instance
|
|
353
|
+
model.
|
|
354
|
+
|
|
355
|
+
Arguments:
|
|
356
|
+
model - a maltoolbox.model.Model on which to follow the
|
|
357
|
+
expressions chain
|
|
358
|
+
target_assets - the set of assets that this expressions chain
|
|
359
|
+
should apply to. Initially it will contain the
|
|
360
|
+
asset to which the attack step belongs
|
|
361
|
+
expr_chain - the expressions chain we are following
|
|
362
|
+
|
|
363
|
+
Return:
|
|
364
|
+
A list of all of the target assets.
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
if expr_chain is None:
|
|
368
|
+
# There is no expressions chain link left to follow return the
|
|
369
|
+
# current target assets
|
|
370
|
+
return set(target_assets)
|
|
371
|
+
|
|
372
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
373
|
+
# Avoid running json.dumps when not in debug
|
|
374
|
+
logger.debug(
|
|
375
|
+
'Following Expressions Chain:\n%s',
|
|
376
|
+
json.dumps(expr_chain.to_dict(), indent = 2)
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
match (expr_chain.type):
|
|
380
|
+
case 'union' | 'intersection' | 'difference':
|
|
381
|
+
# The set operators are used to combine the left hand and
|
|
382
|
+
# right hand targets accordingly.
|
|
383
|
+
if not expr_chain.left_link:
|
|
384
|
+
raise LanguageGraphException('"%s" step expression chain'
|
|
385
|
+
' is missing the left link.' % expr_chain.type)
|
|
386
|
+
if not expr_chain.right_link:
|
|
387
|
+
raise LanguageGraphException('"%s" step expression chain'
|
|
388
|
+
' is missing the right link.' % expr_chain.type)
|
|
389
|
+
lh_targets = self._follow_expr_chain(
|
|
390
|
+
model,
|
|
391
|
+
target_assets,
|
|
392
|
+
expr_chain.left_link
|
|
393
|
+
)
|
|
394
|
+
rh_targets = self._follow_expr_chain(
|
|
395
|
+
model,
|
|
396
|
+
target_assets,
|
|
397
|
+
expr_chain.right_link
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
match (expr_chain.type):
|
|
401
|
+
# Once the assets become hashable set operations should be
|
|
402
|
+
# used instead.
|
|
403
|
+
case 'union':
|
|
404
|
+
new_target_assets = lh_targets.union(rh_targets)
|
|
405
|
+
|
|
406
|
+
case 'intersection':
|
|
407
|
+
new_target_assets = lh_targets.intersection(rh_targets)
|
|
408
|
+
|
|
409
|
+
case 'difference':
|
|
410
|
+
new_target_assets = lh_targets.difference(rh_targets)
|
|
411
|
+
|
|
412
|
+
return new_target_assets
|
|
413
|
+
|
|
414
|
+
case 'field':
|
|
415
|
+
# Change the target assets from the current ones to the
|
|
416
|
+
# associated assets given the specified field name.
|
|
417
|
+
if not expr_chain.fieldname:
|
|
418
|
+
raise LanguageGraphException('"field" step expression '
|
|
419
|
+
'chain is missing fieldname.')
|
|
420
|
+
new_target_assets = set()
|
|
421
|
+
new_target_assets.update(
|
|
422
|
+
*(
|
|
423
|
+
model.get_associated_assets_by_field_name(
|
|
424
|
+
asset, expr_chain.fieldname
|
|
425
|
+
)
|
|
426
|
+
for asset in target_assets
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
return new_target_assets
|
|
430
|
+
|
|
431
|
+
case 'transitive':
|
|
432
|
+
if not expr_chain.sub_link:
|
|
433
|
+
raise LanguageGraphException('"transitive" step '
|
|
434
|
+
'expression chain is missing sub link.')
|
|
435
|
+
|
|
436
|
+
new_assets = target_assets
|
|
437
|
+
|
|
438
|
+
while new_assets := self._follow_expr_chain(
|
|
439
|
+
model, new_assets, expr_chain.sub_link
|
|
440
|
+
):
|
|
441
|
+
if not (new_assets := new_assets.difference(target_assets)):
|
|
442
|
+
break
|
|
443
|
+
|
|
444
|
+
target_assets.update(new_assets)
|
|
445
|
+
|
|
446
|
+
return target_assets
|
|
447
|
+
|
|
448
|
+
case 'subType':
|
|
449
|
+
if not expr_chain.sub_link:
|
|
450
|
+
raise LanguageGraphException('"subType" step '
|
|
451
|
+
'expression chain is missing sub link.')
|
|
452
|
+
new_target_assets = set()
|
|
453
|
+
new_target_assets.update(
|
|
454
|
+
self._follow_expr_chain(
|
|
455
|
+
model, target_assets, expr_chain.sub_link
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
selected_new_target_assets = set()
|
|
460
|
+
for asset in new_target_assets:
|
|
461
|
+
lang_graph_asset = self.lang_graph.assets[asset.type]
|
|
462
|
+
if not lang_graph_asset:
|
|
463
|
+
raise LookupError(
|
|
464
|
+
f'Failed to find asset \"{asset.type}\" in the '
|
|
465
|
+
'language graph.'
|
|
466
|
+
)
|
|
467
|
+
lang_graph_subtype_asset = expr_chain.subtype
|
|
468
|
+
if not lang_graph_subtype_asset:
|
|
469
|
+
raise LookupError(
|
|
470
|
+
'Failed to find asset "%s" in the '
|
|
471
|
+
'language graph.' % expr_chain.subtype
|
|
472
|
+
)
|
|
473
|
+
if lang_graph_asset.is_subasset_of(
|
|
474
|
+
lang_graph_subtype_asset):
|
|
475
|
+
selected_new_target_assets.add(asset)
|
|
476
|
+
|
|
477
|
+
return selected_new_target_assets
|
|
478
|
+
|
|
479
|
+
case 'collect':
|
|
480
|
+
if not expr_chain.left_link:
|
|
481
|
+
raise LanguageGraphException('"collect" step expression chain'
|
|
482
|
+
' is missing the left link.')
|
|
483
|
+
if not expr_chain.right_link:
|
|
484
|
+
raise LanguageGraphException('"collect" step expression chain'
|
|
485
|
+
' is missing the right link.')
|
|
486
|
+
lh_targets = self._follow_expr_chain(
|
|
487
|
+
model,
|
|
488
|
+
target_assets,
|
|
489
|
+
expr_chain.left_link
|
|
490
|
+
)
|
|
491
|
+
rh_targets = self._follow_expr_chain(
|
|
492
|
+
model,
|
|
493
|
+
lh_targets,
|
|
494
|
+
expr_chain.right_link
|
|
495
|
+
)
|
|
496
|
+
return rh_targets
|
|
497
|
+
|
|
498
|
+
case _:
|
|
499
|
+
msg = 'Unknown attack expressions chain type: %s'
|
|
500
|
+
logger.error(
|
|
501
|
+
msg,
|
|
502
|
+
expr_chain.type
|
|
503
|
+
)
|
|
504
|
+
raise AttackGraphStepExpressionError(
|
|
505
|
+
msg % expr_chain.type
|
|
506
|
+
)
|
|
507
|
+
return None
|
|
508
|
+
|
|
496
509
|
def _generate_graph(self) -> None:
|
|
497
510
|
"""
|
|
498
511
|
Generate the attack graph based on the original model instance and the
|
|
@@ -514,57 +527,67 @@ class AttackGraph():
|
|
|
514
527
|
|
|
515
528
|
attack_step_nodes = []
|
|
516
529
|
|
|
517
|
-
|
|
518
|
-
|
|
530
|
+
lang_graph_asset = self.lang_graph.assets[asset.type]
|
|
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
|
+
)
|
|
519
536
|
|
|
520
|
-
for
|
|
537
|
+
for attack_step in lang_graph_asset.attack_steps.values():
|
|
521
538
|
logger.debug(
|
|
522
|
-
'Generating attack step node for %s.',
|
|
539
|
+
'Generating attack step node for %s.', attack_step.name
|
|
523
540
|
)
|
|
524
541
|
|
|
525
542
|
defense_status = None
|
|
526
543
|
existence_status = None
|
|
527
|
-
node_name = asset.name + ':' +
|
|
544
|
+
node_name = asset.name + ':' + attack_step.name
|
|
528
545
|
|
|
529
|
-
match (
|
|
546
|
+
match (attack_step.type):
|
|
530
547
|
case 'defense':
|
|
531
548
|
# Set the defense status for defenses
|
|
532
|
-
defense_status = getattr(asset,
|
|
549
|
+
defense_status = getattr(asset, attack_step.name)
|
|
533
550
|
logger.debug(
|
|
534
|
-
'Setting the defense status of %s to
|
|
551
|
+
'Setting the defense status of \"%s\" to '
|
|
552
|
+
'\"%s\".',
|
|
535
553
|
node_name, defense_status
|
|
536
554
|
)
|
|
537
555
|
|
|
538
556
|
case 'exist' | 'notExist':
|
|
539
|
-
# Resolve step expression associated with
|
|
540
|
-
# attack steps.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
self.
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
557
|
+
# Resolve step expression associated with
|
|
558
|
+
# (non-)existence attack steps.
|
|
559
|
+
existence_status = False
|
|
560
|
+
for requirement in attack_step.requires:
|
|
561
|
+
target_assets = self._follow_expr_chain(
|
|
562
|
+
self.model,
|
|
563
|
+
set([asset]),
|
|
564
|
+
requirement
|
|
565
|
+
)
|
|
566
|
+
# If the step expression resolution yielded
|
|
567
|
+
# the target assets then the required assets
|
|
568
|
+
# exist in the model.
|
|
569
|
+
if target_assets:
|
|
570
|
+
existence_status = True
|
|
571
|
+
break
|
|
572
|
+
|
|
573
|
+
case _:
|
|
574
|
+
pass
|
|
575
|
+
|
|
552
576
|
ag_node = AttackGraphNode(
|
|
553
|
-
type =
|
|
577
|
+
type = attack_step.type,
|
|
578
|
+
lang_graph_attack_step = attack_step,
|
|
554
579
|
asset = asset,
|
|
555
|
-
name =
|
|
556
|
-
ttc =
|
|
580
|
+
name = attack_step.name,
|
|
581
|
+
ttc = attack_step.ttc,
|
|
557
582
|
children = [],
|
|
558
583
|
parents = [],
|
|
559
584
|
defense_status = defense_status,
|
|
560
585
|
existence_status = existence_status,
|
|
561
586
|
is_viable = True,
|
|
562
587
|
is_necessary = True,
|
|
563
|
-
|
|
564
|
-
tags = attack_step_attribs['tags'],
|
|
588
|
+
tags = set(attack_step.tags),
|
|
565
589
|
compromised_by = []
|
|
566
590
|
)
|
|
567
|
-
ag_node.attributes = attack_step_attribs
|
|
568
591
|
attack_step_nodes.append(ag_node)
|
|
569
592
|
self.add_node(ag_node)
|
|
570
593
|
asset.attack_step_nodes = attack_step_nodes
|
|
@@ -576,42 +599,66 @@ class AttackGraph():
|
|
|
576
599
|
ag_node.full_name,
|
|
577
600
|
ag_node.id
|
|
578
601
|
)
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
target_node_full_name
|
|
596
|
-
)
|
|
597
|
-
if not target_node:
|
|
598
|
-
msg = ('Failed to find target node '
|
|
599
|
-
'"%s" to link with for attack step "%s"(%d)!')
|
|
600
|
-
logger.error(
|
|
601
|
-
msg,
|
|
602
|
-
target_node_full_name,
|
|
603
|
-
ag_node.full_name,
|
|
604
|
-
ag_node.id
|
|
605
|
-
)
|
|
606
|
-
raise AttackGraphStepExpressionError(
|
|
607
|
-
msg % (
|
|
608
|
-
target_node_full_name,
|
|
609
|
-
ag_node.full_name,
|
|
610
|
-
ag_node.id
|
|
611
|
-
)
|
|
602
|
+
|
|
603
|
+
if not ag_node.asset:
|
|
604
|
+
raise AttackGraphException('Attack graph node is missing '
|
|
605
|
+
'asset link')
|
|
606
|
+
lang_graph_asset = self.lang_graph.assets[ag_node.asset.type]
|
|
607
|
+
|
|
608
|
+
lang_graph_attack_step = lang_graph_asset.attack_steps[\
|
|
609
|
+
ag_node.name]
|
|
610
|
+
|
|
611
|
+
while lang_graph_attack_step:
|
|
612
|
+
for child in lang_graph_attack_step.children.values():
|
|
613
|
+
for target_attack_step, expr_chain in child:
|
|
614
|
+
target_assets = self._follow_expr_chain(
|
|
615
|
+
self.model,
|
|
616
|
+
set([ag_node.asset]),
|
|
617
|
+
expr_chain
|
|
612
618
|
)
|
|
613
|
-
|
|
614
|
-
|
|
619
|
+
|
|
620
|
+
for target_asset in target_assets:
|
|
621
|
+
if target_asset is not None:
|
|
622
|
+
target_node_full_name = target_asset.name + \
|
|
623
|
+
':' + target_attack_step.name
|
|
624
|
+
target_node = self.get_node_by_full_name(
|
|
625
|
+
target_node_full_name)
|
|
626
|
+
if target_node is None:
|
|
627
|
+
msg = ('Failed to find target node '
|
|
628
|
+
'"%s" to link with for attack '
|
|
629
|
+
'step "%s"(%d)!')
|
|
630
|
+
logger.error(
|
|
631
|
+
msg,
|
|
632
|
+
target_node_full_name,
|
|
633
|
+
ag_node.full_name,
|
|
634
|
+
ag_node.id
|
|
635
|
+
)
|
|
636
|
+
raise AttackGraphStepExpressionError(
|
|
637
|
+
msg % (
|
|
638
|
+
target_node_full_name,
|
|
639
|
+
ag_node.full_name,
|
|
640
|
+
ag_node.id
|
|
641
|
+
)
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
assert ag_node.id is not None
|
|
645
|
+
assert target_node.id is not None
|
|
646
|
+
|
|
647
|
+
logger.debug('Linking attack step "%s"(%d) '
|
|
648
|
+
'to attack step "%s"(%d)' %
|
|
649
|
+
(
|
|
650
|
+
ag_node.full_name,
|
|
651
|
+
ag_node.id,
|
|
652
|
+
target_node.full_name,
|
|
653
|
+
target_node.id
|
|
654
|
+
)
|
|
655
|
+
)
|
|
656
|
+
ag_node.children.append(target_node)
|
|
657
|
+
target_node.parents.append(ag_node)
|
|
658
|
+
if lang_graph_attack_step.overrides:
|
|
659
|
+
break
|
|
660
|
+
lang_graph_attack_step = lang_graph_attack_step.inherits
|
|
661
|
+
|
|
615
662
|
|
|
616
663
|
def regenerate_graph(self) -> None:
|
|
617
664
|
"""
|
|
@@ -687,16 +734,18 @@ class AttackGraph():
|
|
|
687
734
|
reached_attack_steps - list of ids of the attack steps that the
|
|
688
735
|
attacker has reached
|
|
689
736
|
"""
|
|
737
|
+
|
|
690
738
|
if logger.isEnabledFor(logging.DEBUG):
|
|
691
739
|
# Avoid running json.dumps when not in debug
|
|
692
740
|
if attacker_id is not None:
|
|
693
741
|
logger.debug('Add attacker "%s" with id:%d.',
|
|
694
742
|
attacker.name,
|
|
695
|
-
attacker_id
|
|
743
|
+
attacker_id
|
|
744
|
+
)
|
|
696
745
|
else:
|
|
697
746
|
logger.debug('Add attacker "%s" without id.',
|
|
698
|
-
attacker.name
|
|
699
|
-
|
|
747
|
+
attacker.name
|
|
748
|
+
)
|
|
700
749
|
|
|
701
750
|
attacker.id = attacker_id or self.next_attacker_id
|
|
702
751
|
if attacker.id in self._id_to_attacker:
|