mal-toolbox 0.3.5__tar.gz → 0.3.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {mal_toolbox-0.3.5/mal_toolbox.egg-info → mal_toolbox-0.3.7}/PKG-INFO +1 -1
  2. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7/mal_toolbox.egg-info}/PKG-INFO +1 -1
  3. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/__init__.py +2 -2
  4. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/attackgraph/analyzers/apriori.py +5 -5
  5. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/attackgraph/query.py +31 -51
  6. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/model.py +12 -8
  7. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/pyproject.toml +1 -1
  8. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/tests/test_model.py +9 -1
  9. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/AUTHORS +0 -0
  10. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/LICENSE +0 -0
  11. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/README.md +0 -0
  12. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/mal_toolbox.egg-info/SOURCES.txt +0 -0
  13. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/mal_toolbox.egg-info/dependency_links.txt +0 -0
  14. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/mal_toolbox.egg-info/entry_points.txt +0 -0
  15. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/mal_toolbox.egg-info/requires.txt +0 -0
  16. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/mal_toolbox.egg-info/top_level.txt +0 -0
  17. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/__main__.py +0 -0
  18. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/attackgraph/__init__.py +0 -0
  19. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/attackgraph/analyzers/__init__.py +0 -0
  20. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/attackgraph/attacker.py +0 -0
  21. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/attackgraph/attackgraph.py +0 -0
  22. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/attackgraph/node.py +0 -0
  23. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/exceptions.py +0 -0
  24. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/file_utils.py +0 -0
  25. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/ingestors/__init__.py +0 -0
  26. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/ingestors/neo4j.py +0 -0
  27. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/language/__init__.py +0 -0
  28. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/language/compiler/__init__.py +0 -0
  29. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/language/compiler/mal_lexer.py +0 -0
  30. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/language/compiler/mal_parser.py +0 -0
  31. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/language/languagegraph.py +0 -0
  32. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/translators/__init__.py +0 -0
  33. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/translators/securicad.py +0 -0
  34. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/maltoolbox/translators/updater.py +0 -0
  35. {mal_toolbox-0.3.5 → mal_toolbox-0.3.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mal-toolbox
3
- Version: 0.3.5
3
+ Version: 0.3.7
4
4
  Summary: A collection of tools used to create MAL models and attack graphs.
5
5
  Author-email: Andrei Buhaiu <buhaiu@kth.se>, Joakim Loxdal <loxdal@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>, Giuseppe Nebbione <nebbione@kth.se>
6
6
  License: Apache Software License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mal-toolbox
3
- Version: 0.3.5
3
+ Version: 0.3.7
4
4
  Summary: A collection of tools used to create MAL models and attack graphs.
5
5
  Author-email: Andrei Buhaiu <buhaiu@kth.se>, Joakim Loxdal <loxdal@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>, Giuseppe Nebbione <nebbione@kth.se>
6
6
  License: Apache Software License
@@ -1,5 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # MAL Toolbox v0.3.5
2
+ # MAL Toolbox v0.3.7
3
3
  # Copyright 2025, Andrei Buhaiu.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,7 +21,7 @@ MAL-Toolbox Framework
21
21
  """
22
22
 
23
23
  __title__ = "maltoolbox"
24
- __version__ = "0.3.5"
24
+ __version__ = "0.3.7"
25
25
  __authors__ = [
26
26
  "Andrei Buhaiu",
27
27
  "Giuseppe Nebbione",
@@ -195,7 +195,7 @@ def prune_unviable_and_unnecessary_nodes(graph: AttackGraph) -> None:
195
195
 
196
196
  def propagate_viability_from_unviable_node(
197
197
  unviable_node: AttackGraphNode,
198
- ) -> list[AttackGraphNode]:
198
+ ) -> set[AttackGraphNode]:
199
199
  """
200
200
  Update viability of nodes affected by newly enabled defense
201
201
  `unviable_node` in the graph and return any attack steps
@@ -207,12 +207,12 @@ def propagate_viability_from_unviable_node(
207
207
  unviable_node - the node to propagate viability from
208
208
 
209
209
  Returns:
210
- attack_steps_made_unviable - list of the attack steps that have been
210
+ attack_steps_made_unviable - set of the attack steps that have been
211
211
  made unviable by a defense enabled in the
212
212
  current step. Builds up recursively.
213
213
  """
214
214
 
215
- attack_steps_made_unviable = []
215
+ attack_steps_made_unviable = set()
216
216
 
217
217
  logger.debug(
218
218
  'Update viability for node "%s"(%d)',
@@ -226,7 +226,7 @@ def propagate_viability_from_unviable_node(
226
226
  )
227
227
 
228
228
  if unviable_node.type in ('and', 'or'):
229
- attack_steps_made_unviable.append(unviable_node)
229
+ attack_steps_made_unviable.add(unviable_node)
230
230
 
231
231
  for child in unviable_node.children:
232
232
  original_value = child.is_viable
@@ -238,7 +238,7 @@ def propagate_viability_from_unviable_node(
238
238
  child.is_viable = False
239
239
 
240
240
  if child.is_viable != original_value:
241
- attack_steps_made_unviable += \
241
+ attack_steps_made_unviable |= \
242
242
  propagate_viability_from_unviable_node(child)
243
243
 
244
244
  return attack_steps_made_unviable
@@ -5,8 +5,9 @@ This submodule contains functions that analyze the information present in the
5
5
  attack graph, but do not alter the structure or nodes in any way.
6
6
  """
7
7
  from __future__ import annotations
8
+ from collections.abc import Iterable
8
9
  import logging
9
- from typing import TYPE_CHECKING
10
+ from typing import TYPE_CHECKING, Optional
10
11
 
11
12
  from .attackgraph import AttackGraph, Attacker
12
13
 
@@ -113,25 +114,37 @@ def is_node_traversable_by_attacker(
113
114
  )
114
115
  return False
115
116
 
116
- def get_attack_surface(
117
- attacker: Attacker
118
- ) -> list[AttackGraphNode]:
117
+
118
+ def calculate_attack_surface(
119
+ attacker: Attacker,
120
+ *,
121
+ from_nodes: Optional[Iterable[AttackGraphNode]] = None,
122
+ skip_compromised: bool = False,
123
+ ) -> set[AttackGraphNode]:
119
124
  """
120
- Get the current attack surface of an attacker. This includes all of the
121
- viable children nodes of already reached attack steps that are of 'or'
122
- type and the 'and' type children nodes which have all of their necessary
123
- parents in the attack steps reached.
125
+ Calculate the attack surface of the attacker. If from_nodes are provided
126
+ only calculate the attack surface stemming from those nodes, otherwise use
127
+ all nodes the attacker has compromised. If skip_compromised is true,
128
+ exclude already compromised nodes from the returned attack surface.
129
+
130
+ The attack surface includes all of the viable children nodes that are of
131
+ 'or' type and the 'and' type children nodes which have all of their
132
+ necessary parents compromised by the attacker.
124
133
 
125
134
  Arguments:
126
- attacker - the Attacker whose attack surface is sought
135
+ attacker - the Attacker whose attack surface is sought
136
+ from_nodes - the nodes to calculate the attack surface from; defaults
137
+ to the attackers compromised nodes list if omitted
138
+ skip_compromised - if true do not add already compromised nodes to the
139
+ attack surface
127
140
  """
128
141
  logger.debug(
129
142
  'Get the attack surface for Attacker "%s"(%d).',
130
143
  attacker.name,
131
144
  attacker.id
132
145
  )
133
- attack_surface = []
134
- for attack_step in attacker.reached_attack_steps:
146
+ attack_surface = set()
147
+ for attack_step in from_nodes or attacker.reached_attack_steps:
135
148
  logger.debug(
136
149
  'Determine attack surface stemming from '
137
150
  '"%s"(%d) for Attacker "%s"(%d).',
@@ -141,44 +154,10 @@ def get_attack_surface(
141
154
  attacker.id
142
155
  )
143
156
  for child in attack_step.children:
157
+ if skip_compromised and child.is_compromised_by(attacker):
158
+ continue
144
159
  if is_node_traversable_by_attacker(child, attacker) and \
145
160
  child not in attack_surface:
146
- attack_surface.append(child)
147
- return attack_surface
148
-
149
- def update_attack_surface_add_nodes(
150
- attacker: Attacker,
151
- current_attack_surface: list[AttackGraphNode],
152
- nodes: list[AttackGraphNode]
153
- ) -> list[AttackGraphNode]:
154
- """
155
- Update the attack surface of an attacker with the new attack step nodes
156
- provided to see if any of their children can be added.
157
-
158
- Arguments:
159
- attacker - the Attacker whose attack surface is sought
160
- current_attack_surface - the current attack surface that we wish to
161
- expand
162
- nodes - the newly compromised attack step nodes that we
163
- wish to see if any of their children should be
164
- added to the attack surface
165
- """
166
- logger.debug('Update the attack surface for Attacker "%s"(%d).',
167
- attacker.name,
168
- attacker.id)
169
- attack_surface = current_attack_surface
170
- for attack_step in nodes:
171
- logger.debug(
172
- 'Determine attack surface stemming from "%s"(%d) '
173
- 'for Attacker "%s"(%d).',
174
- attack_step.full_name,
175
- attack_step.id,
176
- attacker.name,
177
- attacker.id
178
- )
179
- for child in attack_step.children:
180
- is_traversable = is_node_traversable_by_attacker(child, attacker)
181
- if is_traversable and child not in attack_surface:
182
161
  logger.debug(
183
162
  'Add node "%s"(%d) to the attack surface of '
184
163
  'Attacker "%s"(%d).',
@@ -187,10 +166,11 @@ def update_attack_surface_add_nodes(
187
166
  attacker.name,
188
167
  attacker.id
189
168
  )
190
- attack_surface.append(child)
169
+ attack_surface.add(child)
170
+
191
171
  return attack_surface
192
172
 
193
- def get_defense_surface(graph: AttackGraph) -> list[AttackGraphNode]:
173
+ def get_defense_surface(graph: AttackGraph) -> set[AttackGraphNode]:
194
174
  """
195
175
  Get the defense surface. All non-suppressed defense steps that are not
196
176
  already fully enabled.
@@ -199,8 +179,8 @@ def get_defense_surface(graph: AttackGraph) -> list[AttackGraphNode]:
199
179
  graph - the attack graph
200
180
  """
201
181
  logger.debug('Get the defense surface.')
202
- return [node for node in graph.nodes.values()
203
- if node.is_available_defense()]
182
+ return {node for node in graph.nodes.values()
183
+ if node.is_available_defense()}
204
184
 
205
185
  def get_enabled_defenses(graph: AttackGraph) -> list[AttackGraphNode]:
206
186
  """
@@ -245,7 +245,10 @@ class Model():
245
245
  )
246
246
 
247
247
  # First remove all of the associated assets
248
- for fieldname, assoc_assets in asset.associated_assets.items():
248
+ # We can not remove from the dict while iterating over it
249
+ # so we first have to copy the keys and then remove those assets
250
+ associated_fieldnames = dict(asset.associated_assets)
251
+ for fieldname, assoc_assets in associated_fieldnames.items():
249
252
  asset.remove_associated_assets(fieldname, assoc_assets)
250
253
 
251
254
  # Also remove all of the entry points
@@ -649,17 +652,13 @@ class ModelAsset:
649
652
  other_fieldname, set()
650
653
  ).add(self)
651
654
 
652
- def remove_associated_assets(self, fieldname: str,
653
- assets: set[ModelAsset]):
655
+ def remove_associated_assets(
656
+ self, fieldname: str, assets: set[ModelAsset]):
654
657
  """ Remove the assets provided as a parameter from the set of
655
658
  associated assets dictionary entry corresponding to the fieldname
656
659
  parameter.
657
660
  """
658
- self._associated_assets[fieldname] -= set(assets)
659
- if len(self._associated_assets[fieldname]) == 0:
660
- del self._associated_assets[fieldname]
661
-
662
- # Also remove this asset to the associated assets' dictionaries
661
+ # Remove this asset from its associated assets' dictionaries
663
662
  lg_assoc = self.lg_asset.associations[fieldname]
664
663
  other_fieldname = lg_assoc.get_opposite_fieldname(fieldname)
665
664
  for asset in assets:
@@ -667,6 +666,11 @@ class ModelAsset:
667
666
  if len(asset._associated_assets[other_fieldname]) == 0:
668
667
  del asset._associated_assets[other_fieldname]
669
668
 
669
+ # Remove associated assets from this asset
670
+ self._associated_assets[fieldname] -= set(assets)
671
+ if len(self._associated_assets[fieldname]) == 0:
672
+ del self._associated_assets[fieldname]
673
+
670
674
 
671
675
  @property
672
676
  def associated_assets(self):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mal-toolbox"
3
- version = "0.3.5"
3
+ version = "0.3.7"
4
4
  authors = [
5
5
  { name="Andrei Buhaiu", email="buhaiu@kth.se" },
6
6
  { name="Joakim Loxdal", email="loxdal@kth.se" },
@@ -230,16 +230,23 @@ def test_model_add_asset_duplicate_name(model: Model):
230
230
  assert len(model.assets) == 2
231
231
 
232
232
 
233
- def test_model_remove_asset(model: Model):
233
+ def test_model_remove_asset_with_association(model: Model):
234
234
  """Remove assets from a model"""
235
235
 
236
236
  # Add two program assets to the model
237
237
  asset1 = model.add_asset(asset_type = 'Application')
238
238
  asset2 = model.add_asset(asset_type = 'Application')
239
+ asset1.add_associated_assets('hostApp', {asset2})
239
240
 
241
+ assert asset1.associated_assets == {'hostApp': {asset2}}
242
+ assert asset2.associated_assets == {'appExecutedApps': {asset1}}
240
243
  num_assets_before = len(model.assets)
244
+
241
245
  model.remove_asset(asset1)
242
246
 
247
+ assert asset1.associated_assets == {}
248
+ assert asset2.associated_assets == {}
249
+
243
250
  # Make sure asset asset1 was deleted, but asset2 still exists
244
251
  assert asset1 not in model.assets.values()
245
252
  assert asset2 in model.assets.values()
@@ -362,6 +369,7 @@ def test_model_remove_associated_asset(model: Model):
362
369
  # removed from assets and model
363
370
  asset1.remove_associated_assets(
364
371
  fieldname = 'appExecutedApps', assets = {asset2})
372
+
365
373
  assert 'appExecutedApps' not in asset1.associated_assets
366
374
  assert 'hostApp' not in asset2.associated_assets
367
375
 
File without changes
File without changes
File without changes
File without changes