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.
@@ -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 ..exceptions import AttackGraphException
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 = None, model: Optional[Model] = None):
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 and self.lang_graph 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 node_full_name, node_dict in serialized_attack_steps.items():
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.mitre_info = str(node_dict['mitre_info']) if \
334
- 'mitre_info' in node_dict else None
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 node_full_name, node_dict in serialized_attack_steps.items():
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 attacker_name, attacker in serialized_attackers.items():
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
- model: Optional[Model]=None
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, model=model)
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 "{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
- # TODO probably part of what happens here is already done in lang_graph
518
- attack_steps = self.lang_graph._get_attacks_for_asset_type(asset.type)
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 attack_step_name, attack_step_attribs in attack_steps.items():
537
+ for attack_step in lang_graph_asset.attack_steps.values():
521
538
  logger.debug(
522
- 'Generating attack step node for %s.', attack_step_name
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 + ':' + attack_step_name
544
+ node_name = asset.name + ':' + attack_step.name
528
545
 
529
- match (attack_step_attribs['type']):
546
+ match (attack_step.type):
530
547
  case 'defense':
531
548
  # Set the defense status for defenses
532
- defense_status = getattr(asset, attack_step_name)
549
+ defense_status = getattr(asset, attack_step.name)
533
550
  logger.debug(
534
- 'Setting the defense status of %s to %s.',
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 (non-)existence
540
- # attack steps.
541
- (target_assets, attack_step) = _process_step_expression(
542
- self.lang_graph,
543
- self.model,
544
- [asset],
545
- attack_step_attribs['requires']['stepExpressions'][0])
546
- # If the step expression resolution yielded the target
547
- # assets then the required assets exist in the model.
548
- existence_status = target_assets != []
549
-
550
- mitre_info = attack_step_attribs['meta']['mitre'] if 'mitre' in\
551
- attack_step_attribs['meta'] else None
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 = attack_step_attribs['type'],
577
+ type = attack_step.type,
578
+ lang_graph_attack_step = attack_step,
554
579
  asset = asset,
555
- name = attack_step_name,
556
- ttc = attack_step_attribs['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
- mitre_info = mitre_info,
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
- step_expressions = \
580
- ag_node.attributes['reaches']['stepExpressions'] if \
581
- isinstance(ag_node.attributes, dict) and ag_node.attributes['reaches'] else []
582
-
583
- for step_expression in step_expressions:
584
- # Resolve each of the attack step expressions listed for this
585
- # attack step to determine children.
586
- (target_assets, attack_step) = _process_step_expression(
587
- self.lang_graph,
588
- self.model,
589
- [ag_node.asset],
590
- step_expression)
591
-
592
- for target in target_assets:
593
- target_node_full_name = target.name + ':' + attack_step
594
- target_node = self.get_node_by_full_name(
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
- ag_node.children.append(target_node)
614
- target_node.parents.append(ag_node)
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: