mal-toolbox 0.3.11__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
maltoolbox/model.py CHANGED
@@ -27,108 +27,6 @@ if TYPE_CHECKING:
27
27
 
28
28
  logger = logging.getLogger(__name__)
29
29
 
30
- @dataclass
31
- class AttackerAttachment:
32
- """Used to attach attackers to attack step entry points of assets"""
33
- id: Optional[int] = None
34
- name: Optional[str] = None
35
- entry_points: list[tuple[ModelAsset, list[str]]] = \
36
- field(default_factory=lambda: [])
37
-
38
-
39
- def get_entry_point_tuple(
40
- self,
41
- asset: ModelAsset
42
- ) -> Optional[tuple[ModelAsset, list[str]]]:
43
- """Return an entry point tuple of an AttackerAttachment matching the
44
- asset provided.
45
-
46
-
47
- Arguments:
48
- asset - the asset to add entry point to
49
-
50
- Return:
51
- The entry point tuple containing the asset and the list of attack
52
- steps if the asset has any entry points defined for this attacker
53
- attachemnt.
54
- None, otherwise.
55
- """
56
- return next((ep_tuple for ep_tuple in self.entry_points
57
- if ep_tuple[0] == asset), None)
58
-
59
-
60
- def add_entry_point(
61
- self, asset: ModelAsset, attackstep_name: str):
62
- """Add an entry point to an AttackerAttachment
63
-
64
- self.entry_points contain tuples, first element of each tuple
65
- is an asset, second element is a list of attack step names that
66
- are entry points for the attacker.
67
-
68
- Arguments:
69
- asset - the asset to add the entry point to
70
- attackstep_name - the name of the attack step to add as an entry point
71
- """
72
-
73
- logger.debug(
74
- f'Add entry point "{attackstep_name}" on asset "{asset.name}" '
75
- f'to AttackerAttachment "{self.name}".'
76
- )
77
-
78
- # Get the entry point tuple for the asset if it already exists
79
- entry_point_tuple = self.get_entry_point_tuple(asset)
80
-
81
- if entry_point_tuple:
82
- if attackstep_name not in entry_point_tuple[1]:
83
- # If it exists and does not already have the attack step,
84
- # add it
85
- entry_point_tuple[1].append(attackstep_name)
86
- else:
87
- logger.info(
88
- f'Entry point "{attackstep_name}" on asset "{asset.name}"'
89
- f' already existed for AttackerAttachment "{self.name}".'
90
- )
91
- else:
92
- # Otherwise, create the entry point tuple and the initial entry
93
- # point
94
- self.entry_points.append((asset, [attackstep_name]))
95
-
96
-
97
- def remove_entry_point(
98
- self, asset: ModelAsset, attackstep_name: str):
99
- """Remove an entry point from an AttackerAttachment if it exists
100
-
101
- Arguments:
102
- asset - the asset to remove the entry point from
103
- """
104
-
105
- logger.debug(
106
- f'Remove entry point "{attackstep_name}" on asset "{asset.name}" '
107
- f'from AttackerAttachment "{self.name}".'
108
- )
109
-
110
- # Get the entry point tuple for the asset if it exists
111
- entry_point_tuple = self.get_entry_point_tuple(asset)
112
-
113
- if entry_point_tuple:
114
- if attackstep_name in entry_point_tuple[1]:
115
- # If it exists and not already has the attack step, add it
116
- entry_point_tuple[1].remove(attackstep_name)
117
- else:
118
- logger.warning(
119
- f'Failed to find entry point "{attackstep_name}" on '
120
- f'asset "{asset.name}" for AttackerAttachment '
121
- f'"{self.name}". Nothing to remove.'
122
- )
123
-
124
- if not entry_point_tuple[1]:
125
- self.entry_points.remove(entry_point_tuple)
126
- else:
127
- logger.warning(
128
- f'Failed to find entry points on asset "{asset.name}" '
129
- f'for AttackerAttachment "{self.name}". Nothing to remove.'
130
- )
131
-
132
30
 
133
31
  class Model():
134
32
  """An implementation of a MAL language model containing assets"""
@@ -148,7 +46,6 @@ class Model():
148
46
  self.name = name
149
47
  self.assets: dict[int, ModelAsset] = {}
150
48
  self._name_to_asset:dict[str, ModelAsset] = {} # optimization
151
- self.attackers: list[AttackerAttachment] = []
152
49
  self.lang_graph = lang_graph
153
50
  self.maltoolbox_version: str = mt_version
154
51
 
@@ -222,11 +119,6 @@ class Model():
222
119
  return asset
223
120
 
224
121
 
225
- def remove_attacker(self, attacker: AttackerAttachment) -> None:
226
- """Remove attacker"""
227
- self.attackers.remove(attacker)
228
-
229
-
230
122
  def remove_asset(self, asset: ModelAsset) -> None:
231
123
  """Remove an asset from the model.
232
124
 
@@ -251,39 +143,10 @@ class Model():
251
143
  for fieldname, assoc_assets in associated_fieldnames.items():
252
144
  asset.remove_associated_assets(fieldname, assoc_assets)
253
145
 
254
- # Also remove all of the entry points
255
- for attacker in self.attackers:
256
- entry_point_tuple = attacker.get_entry_point_tuple(asset)
257
- if entry_point_tuple:
258
- attacker.entry_points.remove(entry_point_tuple)
259
-
260
146
  del self.assets[asset.id]
261
147
  del self._name_to_asset[asset.name]
262
148
 
263
149
 
264
- def add_attacker(
265
- self,
266
- attacker: AttackerAttachment,
267
- attacker_id: Optional[int] = None
268
- ) -> None:
269
- """Add an attacker to the model.
270
-
271
- Arguments:
272
- attacker - the attacker to add
273
- attacker_id - optional id for the attacker
274
- """
275
-
276
- if attacker_id is not None:
277
- attacker.id = attacker_id
278
- else:
279
- attacker.id = self.next_id
280
- self.next_id = max(attacker.id + 1, self.next_id)
281
-
282
- if not hasattr(attacker, 'name') or not attacker.name:
283
- attacker.name = 'Attacker:' + str(attacker.id)
284
- self.attackers.append(attacker)
285
-
286
-
287
150
  def get_asset_by_id(
288
151
  self, asset_id: int
289
152
  ) -> Optional[ModelAsset]:
@@ -322,57 +185,12 @@ class Model():
322
185
  return self._name_to_asset.get(asset_name, None)
323
186
 
324
187
 
325
- def get_attacker_by_id(
326
- self, attacker_id: int
327
- ) -> Optional[AttackerAttachment]:
328
- """
329
- Find an attacker in the model based on its id.
330
-
331
- Arguments:
332
- attacker_id - the id of the attacker we are looking for
333
-
334
- Return:
335
- An attacker matching the id if it exists in the model.
336
- """
337
- logger.debug(
338
- 'Get attacker with id %d from model "%s".',
339
- attacker_id, self.name
340
- )
341
- return next(
342
- (attacker for attacker in self.attackers
343
- if attacker.id == attacker_id), None
344
- )
345
-
346
-
347
- def attacker_to_dict(
348
- self, attacker: AttackerAttachment
349
- ) -> tuple[Optional[int], dict]:
350
- """Get dictionary representation of the attacker.
351
-
352
- Arguments:
353
- attacker - attacker to get dictionary representation of
354
- """
355
-
356
- logger.debug('Translating %s to dictionary.', attacker.name)
357
- attacker_dict: dict[str, Any] = {
358
- 'name': attacker.name,
359
- 'entry_points': {},
360
- }
361
- for (asset, attack_steps) in attacker.entry_points:
362
- attacker_dict['entry_points'][asset.name] = {
363
- 'asset_id': asset.id,
364
- 'attack_steps' : attack_steps
365
- }
366
- return (attacker.id, attacker_dict)
367
-
368
-
369
188
  def _to_dict(self) -> dict:
370
189
  """Get dictionary representation of the model."""
371
190
  logger.debug('Translating model to dict.')
372
191
  contents: dict[str, Any] = {
373
192
  'metadata': {},
374
193
  'assets': {},
375
- 'attackers' : {}
376
194
  }
377
195
  contents['metadata'] = {
378
196
  'name': self.name,
@@ -387,10 +205,6 @@ class Model():
387
205
  for asset in self.assets.values():
388
206
  contents['assets'].update(asset._to_dict())
389
207
 
390
- logger.debug('Translating attackers to dictionary.')
391
- for attacker in self.attackers:
392
- (attacker_id, attacker_dict) = self.attacker_to_dict(attacker)
393
- contents['attackers'][attacker_id] = attacker_dict
394
208
  return contents
395
209
 
396
210
 
@@ -459,29 +273,13 @@ class Model():
459
273
  for assoc_asset_id in assoc_assets}
460
274
  )
461
275
 
462
- # Reconstruct the attackers
276
+ # Attackers no longer part of mal-toolbox
463
277
  if 'attackers' in serialized_object:
464
- attackers_info = serialized_object['attackers']
465
- for attacker_id in attackers_info:
466
- attacker = AttackerAttachment(name = attackers_info[attacker_id]['name'])
467
- for asset_name, entry_points_dict in \
468
- attackers_info[attacker_id]['entry_points'].items():
469
- target_asset = model.get_asset_by_id(
470
- entry_points_dict['asset_id'])
471
- if target_asset is None:
472
- raise LookupError(
473
- 'Asset "%s"(%d) is not part of model "%s".' % (
474
- asset_name,
475
- entry_points_dict['asset_id'],
476
- model.name)
477
- )
478
- attacker.entry_points.append(
479
- (
480
- target_asset,
481
- entry_points_dict['attack_steps']
482
- )
483
- )
484
- model.add_attacker(attacker, attacker_id = int(attacker_id))
278
+ msg = ("Defining attackers in a model file is deprecated,"
279
+ " use mal-simulator for attacker simulations.")
280
+ print(msg)
281
+ logger.warning(msg)
282
+
485
283
  return model
486
284
 
487
285
 
@@ -528,12 +326,6 @@ class ModelAsset:
528
326
  self._associated_assets: dict[str, set[ModelAsset]] = {}
529
327
  self.attack_step_nodes: list = []
530
328
 
531
- for step in self.lg_asset.attack_steps.values():
532
- if step.type == 'defense' and step.name not in self.defenses:
533
- self.defenses[step.name] = 1.0 if step.ttc and \
534
- step.ttc['name'] == 'Enabled' else 0.0
535
-
536
-
537
329
  def _to_dict(self):
538
330
  """Get dictionary representation of the asset."""
539
331
 
@@ -547,14 +339,8 @@ class ModelAsset:
547
339
  'associated_assets': {}
548
340
  }
549
341
 
550
- # Only add non-default values for defenses to improve legibility of
551
- # the model format
552
342
  for defense, defense_value in self.defenses.items():
553
- lg_step = self.lg_asset.attack_steps[defense]
554
- default_defval = 1.0 if lg_step.ttc and \
555
- lg_step.ttc['name'] == 'Enabled' else 0.0
556
- if defense_value != default_defval:
557
- asset_dict['defenses'][defense] = defense_value
343
+ asset_dict['defenses'][defense] = defense_value
558
344
 
559
345
  for fieldname, assets in self.associated_assets.items():
560
346
  asset_dict['associated_assets'][fieldname] = {asset.id: asset.name
maltoolbox/py.typed ADDED
File without changes
@@ -9,7 +9,7 @@ import xml.etree.ElementTree as ET
9
9
 
10
10
  from typing import Optional
11
11
 
12
- from ..model import AttackerAttachment, Model
12
+ from ..model import Model
13
13
  from ..language import LanguageGraph
14
14
 
15
15
  logger = logging.getLogger(__name__)
@@ -82,7 +82,7 @@ def convert_model_dict_from_version_0_0(model_dict: dict) -> dict:
82
82
 
83
83
  # Meta data and attackers did not change
84
84
  new_model_dict['metadata'] = model_dict['metadata']
85
- new_model_dict['attackers'] = model_dict['attackers']
85
+ new_model_dict['attackers'] = model_dict.get('attackers', {})
86
86
 
87
87
  new_model_dict['assets'] = {}
88
88
 
@@ -1,29 +0,0 @@
1
- maltoolbox/__init__.py,sha256=m3yZVHy12wt-vRUtI-8HnaiRFnuYJxzt_Z5fOH3D77g,2090
2
- maltoolbox/__main__.py,sha256=PSg8vFS8X-klJBJdSzrg0aLh9ykZgbcoSSEy3DTQoQQ,3499
3
- maltoolbox/exceptions.py,sha256=0YjPx2v1yYumZ2o7pVZ1s_jS-GAb3Ng979KEFhROSNY,1399
4
- maltoolbox/file_utils.py,sha256=tBR8Kjl8IoFzAtYaLNHNALuQrdMT3pD1ZpczHm1pu2g,1875
5
- maltoolbox/model.py,sha256=Y3FKyWyRGPzJlpP92XRrWS52xVeKaz9rgOLoQJvJ808,24008
6
- maltoolbox/attackgraph/__init__.py,sha256=AHDyX6dAkx3mDic2K56v1xche9N6ofDfbaHkKbdJ2qQ,230
7
- maltoolbox/attackgraph/attacker.py,sha256=Lq7g_uFDvThU0wah-CiYA6oTshxt1TlgPJfkojlSyRQ,3132
8
- maltoolbox/attackgraph/attackgraph.py,sha256=QEfWblZDBpvX7o17LL3LHf1gyB080fTGt7uVC8082mM,32537
9
- maltoolbox/attackgraph/node.py,sha256=Ec67_u_8qf_MgCHaUg4wIbZFC013GWxbIsC8EjoguzE,6465
10
- maltoolbox/attackgraph/query.py,sha256=iuaLAc3bMnQefgGa1g62re8-3yQrgBW_cS5W_DgWEjY,6835
11
- maltoolbox/attackgraph/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- maltoolbox/attackgraph/analyzers/apriori.py,sha256=yERuk5M96Cpv2WyqNEpM_j7sf04SNYXSw_hFoL7UPW4,8986
13
- maltoolbox/ingestors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- maltoolbox/ingestors/neo4j.py,sha256=W3AH3nymRQHI9N65HsSyyeQKcETPXmY_SLKc-iB4sBI,8328
15
- maltoolbox/language/__init__.py,sha256=9p5nvVqDCKEhXbDMIz1MtwZ9GN7x1jmUUXbpjEwuqnw,269
16
- maltoolbox/language/languagegraph.py,sha256=eBPTyoDpfc01ONEj321-RmIJV3DfVenYfHdVu0TiITo,67856
17
- maltoolbox/language/compiler/__init__.py,sha256=JQyAgDwJh1pU7AmuOhd1-d2b2PYXpgMVPtxnav8QHVc,15872
18
- maltoolbox/language/compiler/mal_lexer.py,sha256=BeifykDAt4PloRASOaLzBgWF35ev_zgD8lXMIsSHykc,12063
19
- maltoolbox/language/compiler/mal_parser.py,sha256=sUoaE43l2VKg-Dou30mk2wlVS1FvdOREwHNIyFe4IkY,114699
20
- maltoolbox/translators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- maltoolbox/translators/securicad.py,sha256=PJYjieioWN5tE_oKm83dtgV5UkC8EUH9Vsy3-FxBtUo,7017
22
- maltoolbox/translators/updater.py,sha256=8bisZnzMWjGaG5tu8jdF-Oq6bPwIjXkVO-_yZDGc6cA,8652
23
- mal_toolbox-0.3.11.dist-info/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
24
- mal_toolbox-0.3.11.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
25
- mal_toolbox-0.3.11.dist-info/METADATA,sha256=OHt2yl_bXfmzSH47-OhHzK6jDADEsSAYh3hqmDc9ZKM,6159
26
- mal_toolbox-0.3.11.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
27
- mal_toolbox-0.3.11.dist-info/entry_points.txt,sha256=oqby5O6cUP_OHCm70k_iYPA6UlbTBf7se1i3XwdK3uU,56
28
- mal_toolbox-0.3.11.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
29
- mal_toolbox-0.3.11.dist-info/RECORD,,
@@ -1,243 +0,0 @@
1
- """
2
- MAL-Toolbox Attack Graph Apriori Analyzer Submodule
3
-
4
- This submodule contains analyzers that are relevant before attackers are even
5
- connected to the attack graph.
6
- Currently these are:
7
- - Viability = Determine if a node can be traversed under any circumstances or
8
- if the model structure makes it unviable.
9
- - Necessity = Determine if a node is necessary for the attacker or if the
10
- model structure means it is not needed(it behaves as if it were already
11
- compromised) to compromise children attack steps.
12
- """
13
-
14
- from __future__ import annotations
15
- from typing import Optional, TYPE_CHECKING
16
- import logging
17
-
18
- if TYPE_CHECKING:
19
- from ..attackgraph import AttackGraph
20
- from ..node import AttackGraphNode
21
-
22
- logger = logging.getLogger(__name__)
23
-
24
- def propagate_viability_from_node(node: AttackGraphNode) -> None:
25
- """
26
- Arguments:
27
- node - the attack graph node from which to propagate the viable
28
- status
29
- """
30
- logger.debug(
31
- 'Propagate viability from "%s"(%d) with viability status %s.',
32
- node.full_name, node.id, node.is_viable
33
- )
34
- for child in node.children:
35
- original_value = child.is_viable
36
- if child.type == 'or':
37
- child.is_viable = False
38
- for parent in child.parents:
39
- child.is_viable = child.is_viable or parent.is_viable
40
- if child.type == 'and':
41
- child.is_viable = False
42
-
43
- if child.is_viable != original_value:
44
- propagate_viability_from_node(child)
45
-
46
-
47
- def propagate_necessity_from_node(node: AttackGraphNode) -> None:
48
- """
49
- Arguments:
50
- node - the attack graph node from which to propagate the necessary
51
- status
52
- """
53
- logger.debug(
54
- 'Propagate necessity from "%s"(%d) with necessity status %s.',
55
- node.full_name, node.id, node.is_necessary
56
- )
57
-
58
- for child in node.children:
59
- if child.ttc and child.ttc.get('name', None) not in ['Enabled',
60
- 'Disabled', 'Instant']:
61
- # Do not propagate unnecessary state from nodes that have a TTC
62
- # probability distribution associated with them.
63
- # TODO: Evaluate this more carefully, how do we want to have TTCs
64
- # impact necessity and viability.
65
- # TODO: Have this condition be any probability that has a
66
- # Bernoulli component
67
- continue
68
- original_value = child.is_necessary
69
- if child.type == 'or':
70
- child.is_necessary = False
71
- if child.type == 'and':
72
- child.is_necessary = False
73
- for parent in child.parents:
74
- child.is_necessary = child.is_necessary or parent.is_necessary
75
-
76
- # TODO: Update TTC for child attack step before if it is not necessary
77
- # before propagating it further.
78
- if child.is_necessary != original_value:
79
- propagate_necessity_from_node(child)
80
-
81
-
82
- def evaluate_viability(node: AttackGraphNode) -> None:
83
- """
84
- Arguments:
85
- graph - the node to evaluate viability for.
86
- """
87
- match (node.type):
88
- case 'exist':
89
- assert isinstance(node.existence_status, bool), \
90
- f'Existence status not defined for {node.full_name}.'
91
- node.is_viable = node.existence_status
92
- case 'notExist':
93
- assert isinstance(node.existence_status, bool), \
94
- f'Existence status not defined for {node.full_name}.'
95
- node.is_viable = not node.existence_status
96
- case 'defense':
97
- assert node.defense_status is not None and \
98
- 0.0 <= node.defense_status <= 1.0, \
99
- f'{node.full_name} defense status invalid: {node.defense_status}.'
100
- node.is_viable = node.defense_status != 1.0
101
- case 'or':
102
- node.is_viable = False
103
- for parent in node.parents:
104
- node.is_viable = node.is_viable or parent.is_viable
105
- case 'and':
106
- node.is_viable = True
107
- for parent in node.parents:
108
- node.is_viable = node.is_viable and parent.is_viable
109
- case _:
110
- msg = ('Evaluate viability was provided node "%s"(%d) which '
111
- 'is of unknown type "%s"')
112
- logger.error(msg, node.full_name, node.id, node.type)
113
- raise ValueError(msg % (node.full_name, node.id, node.type))
114
-
115
-
116
- def evaluate_necessity(node: AttackGraphNode) -> None:
117
- """
118
- Arguments:
119
- graph - the node to evaluate necessity for.
120
- """
121
- match (node.type):
122
- case 'exist':
123
- assert isinstance(node.existence_status, bool), \
124
- f'Existence status not defined for {node.full_name}.'
125
- node.is_necessary = not node.existence_status
126
- case 'notExist':
127
- assert isinstance(node.existence_status, bool), \
128
- f'Existence status not defined for {node.full_name}.'
129
- node.is_necessary = bool(node.existence_status)
130
- case 'defense':
131
- assert node.defense_status is not None and \
132
- 0.0 <= node.defense_status <= 1.0, \
133
- f'{node.full_name} defense status invalid: {node.defense_status}.'
134
- node.is_necessary = node.defense_status != 0.0
135
- case 'or':
136
- node.is_necessary = True
137
- for parent in node.parents:
138
- node.is_necessary = node.is_necessary and parent.is_necessary
139
- case 'and':
140
- node.is_necessary = False
141
- for parent in node.parents:
142
- node.is_necessary = node.is_necessary or parent.is_necessary
143
- case _:
144
- msg = ('Evaluate necessity was provided node "%s"(%d) which '
145
- 'is of unknown type "%s"')
146
- logger.error(msg, node.full_name, node.id, node.type)
147
- raise ValueError(msg % (node.full_name, node.id, node.type))
148
-
149
-
150
- def evaluate_viability_and_necessity(node: AttackGraphNode) -> None:
151
- """
152
- Arguments:
153
- graph - the node to evaluate viability and necessity for.
154
- """
155
- evaluate_viability(node)
156
- evaluate_necessity(node)
157
-
158
-
159
- def calculate_viability_and_necessity(graph: AttackGraph) -> None:
160
- """
161
- Arguments:
162
- graph - the attack graph for which we wish to determine the
163
- viability and necessity statuses for the nodes.
164
- """
165
- for node in graph.nodes.values():
166
- if node.type in ['exist', 'notExist', 'defense']:
167
- evaluate_viability_and_necessity(node)
168
- if not node.is_viable:
169
- propagate_viability_from_node(node)
170
- if not node.is_necessary:
171
- propagate_necessity_from_node(node)
172
-
173
-
174
- def prune_unviable_and_unnecessary_nodes(graph: AttackGraph) -> None:
175
- """
176
- Arguments:
177
- graph - the attack graph for which we wish to remove the
178
- the nodes which are not viable or necessary.
179
- """
180
- logger.debug(
181
- 'Prune unviable and unnecessary nodes from the attack graph.')
182
-
183
- nodes_to_remove = set()
184
- for node in graph.nodes.values():
185
- if node.type in ('or', 'and') and \
186
- (not node.is_viable or not node.is_necessary):
187
- nodes_to_remove.add(node)
188
-
189
- # Do the removal separatly so we don't remove
190
- # nodes from a set we are looping over
191
- for node in nodes_to_remove:
192
- graph.remove_node(node)
193
-
194
-
195
- def propagate_viability_from_unviable_node(
196
- unviable_node: AttackGraphNode,
197
- ) -> set[AttackGraphNode]:
198
- """
199
- Update viability of nodes affected by newly enabled defense
200
- `unviable_node` in the graph and return any attack steps
201
- that are no longer viable because of it.
202
-
203
- Propagate recursively via children as long as changes occur.
204
-
205
- Arguments:
206
- unviable_node - the node to propagate viability from
207
-
208
- Returns:
209
- attack_steps_made_unviable - set of the attack steps that have been
210
- made unviable by a defense enabled in the
211
- current step. Builds up recursively.
212
- """
213
-
214
- attack_steps_made_unviable = set()
215
-
216
- logger.debug(
217
- 'Update viability for node "%s"(%d)',
218
- unviable_node.full_name,
219
- unviable_node.id
220
- )
221
-
222
- assert not unviable_node.is_viable, (
223
- "propagate_viability_from_unviable_node should not be called"
224
- f" on viable node {unviable_node.full_name}"
225
- )
226
-
227
- if unviable_node.type in ('and', 'or'):
228
- attack_steps_made_unviable.add(unviable_node)
229
-
230
- for child in unviable_node.children:
231
- original_value = child.is_viable
232
- if child.type == 'or':
233
- child.is_viable = False
234
- for parent in child.parents:
235
- child.is_viable = child.is_viable or parent.is_viable
236
- if child.type == 'and':
237
- child.is_viable = False
238
-
239
- if child.is_viable != original_value:
240
- attack_steps_made_unviable |= \
241
- propagate_viability_from_unviable_node(child)
242
-
243
- return attack_steps_made_unviable