mal-toolbox 2.0.0__tar.gz → 2.1.0__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 (60) hide show
  1. {mal_toolbox-2.0.0/mal_toolbox.egg-info → mal_toolbox-2.1.0}/PKG-INFO +2 -2
  2. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/README.md +1 -1
  3. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0/mal_toolbox.egg-info}/PKG-INFO +2 -2
  4. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/mal_toolbox.egg-info/SOURCES.txt +15 -0
  5. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/__init__.py +2 -2
  6. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/attackgraph/__init__.py +2 -2
  7. mal_toolbox-2.1.0/maltoolbox/attackgraph/attackgraph.py +309 -0
  8. mal_toolbox-2.1.0/maltoolbox/attackgraph/factories.py +68 -0
  9. mal_toolbox-2.1.0/maltoolbox/attackgraph/generate.py +338 -0
  10. mal_toolbox-2.1.0/maltoolbox/attackgraph/node_getters.py +36 -0
  11. mal_toolbox-2.1.0/maltoolbox/attackgraph/ttcs.py +28 -0
  12. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/__init__.py +2 -2
  13. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/compiler/mal_compiler.py +4 -3
  14. mal_toolbox-2.1.0/maltoolbox/language/detector.py +43 -0
  15. mal_toolbox-2.1.0/maltoolbox/language/expression_chain.py +218 -0
  16. mal_toolbox-2.1.0/maltoolbox/language/language_graph_asset.py +180 -0
  17. mal_toolbox-2.1.0/maltoolbox/language/language_graph_assoc.py +147 -0
  18. mal_toolbox-2.1.0/maltoolbox/language/language_graph_attack_step.py +129 -0
  19. mal_toolbox-2.1.0/maltoolbox/language/language_graph_builder.py +282 -0
  20. mal_toolbox-2.1.0/maltoolbox/language/language_graph_loaders.py +7 -0
  21. mal_toolbox-2.1.0/maltoolbox/language/language_graph_lookup.py +140 -0
  22. mal_toolbox-2.1.0/maltoolbox/language/language_graph_serialization.py +5 -0
  23. mal_toolbox-2.1.0/maltoolbox/language/languagegraph.py +492 -0
  24. mal_toolbox-2.1.0/maltoolbox/language/step_expression_processor.py +491 -0
  25. mal_toolbox-2.1.0/maltoolbox/py.typed +0 -0
  26. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/pyproject.toml +1 -1
  27. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/tests/test_model.py +2 -1
  28. mal_toolbox-2.0.0/maltoolbox/attackgraph/attackgraph.py +0 -737
  29. mal_toolbox-2.0.0/maltoolbox/language/languagegraph.py +0 -1785
  30. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/AUTHORS +0 -0
  31. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/LICENSE +0 -0
  32. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/mal_toolbox.egg-info/dependency_links.txt +0 -0
  33. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/mal_toolbox.egg-info/entry_points.txt +0 -0
  34. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/mal_toolbox.egg-info/requires.txt +0 -0
  35. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/mal_toolbox.egg-info/top_level.txt +0 -0
  36. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/__main__.py +0 -0
  37. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/attackgraph/analyzers/__init__.py +0 -0
  38. /mal_toolbox-2.0.0/maltoolbox/patternfinder/__init__.py → /mal_toolbox-2.1.0/maltoolbox/attackgraph/file_utils.py +0 -0
  39. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/attackgraph/node.py +0 -0
  40. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/exceptions.py +0 -0
  41. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/file_utils.py +0 -0
  42. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/compiler/__init__.py +0 -0
  43. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/compiler/distributions.py +0 -0
  44. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/compiler/exceptions.py +0 -0
  45. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/compiler/lang.py +0 -0
  46. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/compiler/mal_analyzer.py +0 -0
  47. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/model.py +0 -0
  48. /mal_toolbox-2.0.0/maltoolbox/py.typed → /mal_toolbox-2.1.0/maltoolbox/patternfinder/__init__.py +0 -0
  49. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/patternfinder/attackgraph_patterns.py +0 -0
  50. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/str_utils.py +0 -0
  51. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/translators/__init__.py +0 -0
  52. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/translators/networkx.py +0 -0
  53. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/translators/updater.py +0 -0
  54. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/visualization/__init__.py +0 -0
  55. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/visualization/draw_io_utils.py +0 -0
  56. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/visualization/graphviz_utils.py +0 -0
  57. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/visualization/neo4j_utils.py +0 -0
  58. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/visualization/utils.py +0 -0
  59. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/setup.cfg +0 -0
  60. {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/tests/test_visualization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mal-toolbox
3
- Version: 2.0.0
3
+ Version: 2.1.0
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>, Sandor Berglund <sandor@kth.se>
6
6
  License: Apache Software License
@@ -38,7 +38,7 @@ MAL ([Meta Attack Language](https://mal-lang.org/)) models and attack graphs.
38
38
 
39
39
  Attack graphs can be used to run simulations in [MAL Simulator](https://github.com/mal-lang/mal-simulator) or run your own custom analysis on.
40
40
 
41
- - [MAL Toolbox Documentation](https://github.com/mal-lang/mal-toolbox/wiki)
41
+ - [MAL Toolbox Wiki](https://github.com/mal-lang/mal-toolbox/wiki)
42
42
  - [MAL Toolbox tutorial](https://github.com/mal-lang/mal-toolbox-tutorial)
43
43
 
44
44
  # Usage
@@ -5,7 +5,7 @@ MAL ([Meta Attack Language](https://mal-lang.org/)) models and attack graphs.
5
5
 
6
6
  Attack graphs can be used to run simulations in [MAL Simulator](https://github.com/mal-lang/mal-simulator) or run your own custom analysis on.
7
7
 
8
- - [MAL Toolbox Documentation](https://github.com/mal-lang/mal-toolbox/wiki)
8
+ - [MAL Toolbox Wiki](https://github.com/mal-lang/mal-toolbox/wiki)
9
9
  - [MAL Toolbox tutorial](https://github.com/mal-lang/mal-toolbox-tutorial)
10
10
 
11
11
  # Usage
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mal-toolbox
3
- Version: 2.0.0
3
+ Version: 2.1.0
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>, Sandor Berglund <sandor@kth.se>
6
6
  License: Apache Software License
@@ -38,7 +38,7 @@ MAL ([Meta Attack Language](https://mal-lang.org/)) models and attack graphs.
38
38
 
39
39
  Attack graphs can be used to run simulations in [MAL Simulator](https://github.com/mal-lang/mal-simulator) or run your own custom analysis on.
40
40
 
41
- - [MAL Toolbox Documentation](https://github.com/mal-lang/mal-toolbox/wiki)
41
+ - [MAL Toolbox Wiki](https://github.com/mal-lang/mal-toolbox/wiki)
42
42
  - [MAL Toolbox tutorial](https://github.com/mal-lang/mal-toolbox-tutorial)
43
43
 
44
44
  # Usage
@@ -17,10 +17,25 @@ maltoolbox/py.typed
17
17
  maltoolbox/str_utils.py
18
18
  maltoolbox/attackgraph/__init__.py
19
19
  maltoolbox/attackgraph/attackgraph.py
20
+ maltoolbox/attackgraph/factories.py
21
+ maltoolbox/attackgraph/file_utils.py
22
+ maltoolbox/attackgraph/generate.py
20
23
  maltoolbox/attackgraph/node.py
24
+ maltoolbox/attackgraph/node_getters.py
25
+ maltoolbox/attackgraph/ttcs.py
21
26
  maltoolbox/attackgraph/analyzers/__init__.py
22
27
  maltoolbox/language/__init__.py
28
+ maltoolbox/language/detector.py
29
+ maltoolbox/language/expression_chain.py
30
+ maltoolbox/language/language_graph_asset.py
31
+ maltoolbox/language/language_graph_assoc.py
32
+ maltoolbox/language/language_graph_attack_step.py
33
+ maltoolbox/language/language_graph_builder.py
34
+ maltoolbox/language/language_graph_loaders.py
35
+ maltoolbox/language/language_graph_lookup.py
36
+ maltoolbox/language/language_graph_serialization.py
23
37
  maltoolbox/language/languagegraph.py
38
+ maltoolbox/language/step_expression_processor.py
24
39
  maltoolbox/language/compiler/__init__.py
25
40
  maltoolbox/language/compiler/distributions.py
26
41
  maltoolbox/language/compiler/exceptions.py
@@ -1,4 +1,4 @@
1
- # MAL Toolbox v2.0.0
1
+ # MAL Toolbox v2.1.0
2
2
  # Copyright 2025, Andrei Buhaiu.
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,7 +19,7 @@
19
19
  """
20
20
 
21
21
  __title__ = "maltoolbox"
22
- __version__ = "2.0.0"
22
+ __version__ = "2.1.0"
23
23
  __authors__ = [
24
24
  "Andrei Buhaiu",
25
25
  "Giuseppe Nebbione",
@@ -2,12 +2,12 @@
2
2
  models and analyze attack graphs.
3
3
  """
4
4
 
5
- from .attackgraph import AttackGraph, create_attack_graph
5
+ from .attackgraph import AttackGraph
6
+ from .factories import create_attack_graph
6
7
  from .node import AttackGraphNode
7
8
 
8
9
  __all__ = [
9
10
  "AttackGraph",
10
11
  "AttackGraphNode",
11
- "Attacker",
12
12
  "create_attack_graph"
13
13
  ]
@@ -0,0 +1,309 @@
1
+ """MAL-Toolbox Attack Graph Module
2
+ """
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import logging
7
+ from typing import TYPE_CHECKING, Optional
8
+
9
+ from maltoolbox.attackgraph.generate import generate_graph
10
+ from maltoolbox.attackgraph.node_getters import get_node_by_full_name
11
+ from maltoolbox.language.languagegraph import disaggregate_attack_step_full_name
12
+
13
+ from ..file_utils import load_dict_from_json_file, load_dict_from_yaml_file, save_dict_to_file
14
+ from ..language import LanguageGraph, LanguageGraphAttackStep
15
+ from ..model import Model
16
+ from .node import AttackGraphNode
17
+
18
+ if TYPE_CHECKING:
19
+ from ..model import ModelAsset
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def attack_graph_from_dict(
25
+ serialized_object: dict, lang_graph: LanguageGraph, model: Optional[Model]
26
+ ):
27
+ attack_graph = AttackGraph(lang_graph)
28
+ attack_graph.model = model
29
+ serialized_attack_steps: dict[str, dict] = serialized_object['attack_steps']
30
+
31
+ # Create all of the nodes in the imported attack graph.
32
+ for node_full_name, node_dict in serialized_attack_steps.items():
33
+
34
+ # Recreate asset links if model is available.
35
+ node_asset = None
36
+ if model and 'asset' in node_dict:
37
+ node_asset = model.get_asset_by_name(node_dict['asset'])
38
+ if node_asset is None:
39
+ msg = (
40
+ 'Failed to find asset with name "%s"'
41
+ ' when loading from attack graph dict'
42
+ )
43
+ logger.error(msg, node_dict["asset"])
44
+ raise LookupError(msg % node_dict["asset"])
45
+
46
+ lg_asset_name, lg_attack_step_name = (
47
+ disaggregate_attack_step_full_name(
48
+ node_dict['lang_graph_attack_step']
49
+ )
50
+ )
51
+ lg_attack_step = (
52
+ lang_graph.assets[lg_asset_name].attack_steps[lg_attack_step_name]
53
+ )
54
+ ag_node = attack_graph.add_node(
55
+ lg_attack_step=lg_attack_step,
56
+ node_id=node_dict['id'],
57
+ model_asset=node_asset,
58
+ ttc_dist=node_dict['ttc'],
59
+ existence_status=(
60
+ bool(node_dict['existence_status'])
61
+ if 'existence_status' in node_dict else None
62
+ ),
63
+ # Give explicit full name if model is missing, otherwise
64
+ # it will generate automatically in node.full_name
65
+ full_name=node_full_name if not model else None
66
+ )
67
+ ag_node.tags = list(node_dict.get('tags', []))
68
+ ag_node.extras = node_dict.get('extras', {})
69
+
70
+ if node_asset:
71
+ # Add AttackGraphNode to attack_step_nodes of asset
72
+ if hasattr(node_asset, 'attack_step_nodes'):
73
+ node_attack_steps = list(node_asset.attack_step_nodes)
74
+ node_attack_steps.append(ag_node)
75
+ node_asset.attack_step_nodes = node_attack_steps
76
+ else:
77
+ node_asset.attack_step_nodes = [ag_node]
78
+
79
+ # Re-establish links between nodes.
80
+ for node_dict in serialized_attack_steps.values():
81
+ _ag_node = attack_graph.nodes[node_dict['id']]
82
+ if not isinstance(_ag_node, AttackGraphNode):
83
+ msg = ('Failed to find node with id %s when loading'
84
+ ' attack graph from dict')
85
+ logger.error(msg, node_dict["id"])
86
+ raise LookupError(msg % node_dict["id"])
87
+ for child_id in node_dict['children']:
88
+ child = attack_graph.nodes[int(child_id)]
89
+ if child is None:
90
+ msg = ('Failed to find child node with id %s'
91
+ ' when loading from attack graph from dict')
92
+ logger.error(msg, child_id)
93
+ raise LookupError(msg % child_id)
94
+ _ag_node.children.add(child)
95
+
96
+ for parent_id in node_dict['parents']:
97
+ parent = attack_graph.nodes[int(parent_id)]
98
+ if parent is None:
99
+ msg = ('Failed to find parent node with id %s '
100
+ 'when loading from attack graph from dict')
101
+ logger.error(msg, parent_id)
102
+ raise LookupError(msg % parent_id)
103
+ _ag_node.parents.add(parent)
104
+
105
+ return attack_graph
106
+
107
+
108
+ def attack_graph_from_file(
109
+ filename: str, lang_graph: LanguageGraph, model: Optional[Model]
110
+ ):
111
+ if model is not None:
112
+ logger.debug('Load attack graph from file "%s" with '
113
+ 'model "%s".', filename, model.name)
114
+ else:
115
+ logger.debug('Load attack graph from file "%s" '
116
+ 'without model.', filename)
117
+ serialized_attack_graph = None
118
+ if filename.endswith(('.yml', '.yaml')):
119
+ serialized_attack_graph = load_dict_from_yaml_file(filename)
120
+ elif filename.endswith('.json'):
121
+ serialized_attack_graph = load_dict_from_json_file(filename)
122
+ else:
123
+ raise ValueError('Unknown file extension, expected json/yml/yaml')
124
+ return attack_graph_from_dict(serialized_attack_graph, lang_graph, model)
125
+
126
+
127
+ class AttackGraph:
128
+ """Graph representation of attack and defense steps"""
129
+
130
+ def __init__(self, lang_graph: LanguageGraph, model: Model | None = None):
131
+ self.nodes: dict[int, AttackGraphNode] = {}
132
+ self.attack_steps: list[AttackGraphNode] = []
133
+ self.defense_steps: list[AttackGraphNode] = []
134
+ self.model = model
135
+ self.lang_graph = lang_graph
136
+ self.next_node_id = 0
137
+ self.full_name_to_node: dict[str, AttackGraphNode] = {}
138
+
139
+ if self.model is not None:
140
+ self.nodes, self.attack_steps, self.defense_steps, self.full_name_to_node = (
141
+ generate_graph(self.model)
142
+ )
143
+
144
+ def __repr__(self) -> str:
145
+ return (
146
+ f'AttackGraph(Number of nodes: {len(self.nodes)}, '
147
+ f'model: {self.model}, language: {self.lang_graph}'
148
+ )
149
+
150
+ def _to_dict(self) -> dict:
151
+ """Convert AttackGraph to dict"""
152
+ serialized_attack_steps = {}
153
+ for ag_node in self.nodes.values():
154
+ serialized_attack_steps[ag_node.full_name] = ag_node.to_dict()
155
+ return {
156
+ 'attack_steps': serialized_attack_steps
157
+ }
158
+
159
+ def __deepcopy__(self, memo):
160
+ """Custom deepcopy implementation for attack graph"""
161
+ # Check if the object is already in the memo dictionary
162
+ if id(self) in memo:
163
+ return memo[id(self)]
164
+
165
+ copied_attackgraph = AttackGraph(self.lang_graph)
166
+ copied_attackgraph.model = self.model
167
+ copied_attackgraph.nodes = {}
168
+
169
+ # Deep copy nodes
170
+ for node_id, node in self.nodes.items():
171
+ copied_node = copy.deepcopy(node, memo)
172
+ copied_attackgraph.nodes[node_id] = copied_node
173
+
174
+ # Re-link node references
175
+ for node in self.nodes.values():
176
+ if node.parents:
177
+ memo[id(node)].parents = copy.deepcopy(node.parents, memo)
178
+ if node.children:
179
+ memo[id(node)].children = copy.deepcopy(node.children, memo)
180
+
181
+ # Copy lookup dicts
182
+ copied_attackgraph.full_name_to_node = \
183
+ copy.deepcopy(self.full_name_to_node, memo)
184
+
185
+ # Copy counters
186
+ copied_attackgraph.next_node_id = self.next_node_id
187
+ return copied_attackgraph
188
+
189
+ def save_to_file(self, filename: str) -> None:
190
+ """Save to json/yml depending on extension"""
191
+ logger.debug('Save attack graph to file "%s".', filename)
192
+ return save_dict_to_file(filename, self._to_dict())
193
+
194
+ @classmethod
195
+ def load_from_file(
196
+ cls,
197
+ filename: str,
198
+ lang_graph: LanguageGraph,
199
+ model: Model | None = None
200
+ ) -> AttackGraph:
201
+ """Create from json or yaml file depending on file extension"""
202
+ return attack_graph_from_file(filename, lang_graph, model)
203
+
204
+ def get_node_by_full_name(self, full_name: str) -> AttackGraphNode:
205
+ """Return the attack node that matches the full name provided.
206
+
207
+ Arguments:
208
+ ---------
209
+ full_name - the full name of the attack graph node we are looking
210
+ for
211
+
212
+ Return:
213
+ ------
214
+ The attack step node that matches the given full name.
215
+
216
+ """
217
+ return get_node_by_full_name(self.full_name_to_node, full_name)
218
+
219
+ def regenerate_graph(self) -> None:
220
+ """Regenerate the attack graph based on the original model instance and
221
+ the MAL language specification provided at initialization.
222
+ """
223
+ assert self.model, "Model required to generate graph"
224
+ self.nodes, self.attack_steps, self.defense_steps, self.full_name_to_node = (
225
+ generate_graph(self.model)
226
+ )
227
+
228
+ def add_node(
229
+ self,
230
+ lg_attack_step: LanguageGraphAttackStep,
231
+ node_id: int | None = None,
232
+ model_asset: ModelAsset | None = None,
233
+ ttc_dist: dict | None = None,
234
+ existence_status: bool | None = None,
235
+ full_name: str | None = None
236
+ ) -> AttackGraphNode:
237
+ """Create and add a node to the graph
238
+ Arguments:
239
+ lg_attack_step - the language graph attack step that corresponds
240
+ to the attack graph node to create
241
+ node_id - id to assign to the newly created node, usually
242
+ provided only when loading an existing attack
243
+ graph from a file. If not provided the id will
244
+ be set to the next highest id available.
245
+ model_asset - the model asset that corresponds to the attack
246
+ step node. While optional it is highly
247
+ recommended that this be provided. It should
248
+ only be ommitted if the model which was used to
249
+ generate the attack graph is not available when
250
+ loading an attack graph from a file.
251
+ ttc_dist - the ttc distribution to assign to the node. This
252
+ is relevant for when we want to override the ttc
253
+ distribution as it is defined in the language.
254
+ Frequently used for defenses.
255
+ existence_status - the existence status of the node. Only, relevant
256
+ for exist and notExist type nodes.
257
+
258
+ Return:
259
+ ------
260
+ The newly created attack step node.
261
+
262
+ """
263
+
264
+ node_id = node_id if node_id is not None else self.next_node_id
265
+ if node_id in self.nodes:
266
+ raise ValueError(f'Node index {node_id} already in use.')
267
+ self.next_node_id = node_id + 1
268
+
269
+ logger.debug(
270
+ 'Create and add to attackgraph node of type "%s" with id:%d.\n',
271
+ lg_attack_step.full_name, node_id
272
+ )
273
+
274
+ node = AttackGraphNode(
275
+ node_id=node_id,
276
+ lg_attack_step=lg_attack_step,
277
+ model_asset=model_asset,
278
+ ttc_dist=ttc_dist,
279
+ existence_status=existence_status,
280
+ full_name=full_name
281
+ )
282
+
283
+ # Add to different lists depending on types
284
+ # Useful but not vital for functionality
285
+ if node.type in ('or', 'and'):
286
+ self.attack_steps.append(node)
287
+ if node.type == 'defense':
288
+ self.defense_steps.append(node)
289
+
290
+ self.nodes[node_id] = node
291
+ self.full_name_to_node[node.full_name] = node
292
+
293
+ return node
294
+
295
+ def remove_node(self, node: AttackGraphNode) -> None:
296
+ """Remove node from attack graph
297
+ Arguments:
298
+ node - the node we wish to remove from the attack graph
299
+ """
300
+ logger.debug(
301
+ 'Remove node "%s"(%d).', node.full_name, node.id
302
+ )
303
+ for child in node.children:
304
+ child.parents.remove(node)
305
+ for parent in node.parents:
306
+ parent.children.remove(node)
307
+ del self.nodes[node.id]
308
+ del self.full_name_to_node[node.full_name]
309
+
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+ import logging
3
+ import zipfile
4
+ from maltoolbox.exceptions import AttackGraphStepExpressionError
5
+ from maltoolbox.language.languagegraph import LanguageGraph
6
+ from maltoolbox.model import Model
7
+
8
+ from maltoolbox.attackgraph.attackgraph import AttackGraph
9
+
10
+ from .. import log_configs
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def create_attack_graph(
15
+ lang: str | LanguageGraph,
16
+ model: str | Model,
17
+ ) -> AttackGraph:
18
+ """Create and return an attack graph
19
+
20
+ Args:
21
+ ----
22
+ lang - path to language file (.mar or .mal) or a LanguageGraph object
23
+ model - path to model file (yaml or json) or a Model object
24
+
25
+ """
26
+ # Load language
27
+ if isinstance(lang, LanguageGraph):
28
+ lang_graph = lang
29
+ elif isinstance(lang, str):
30
+ # Load from path
31
+ try:
32
+ lang_graph = LanguageGraph.from_mar_archive(lang)
33
+ except zipfile.BadZipFile:
34
+ lang_graph = LanguageGraph.from_mal_spec(lang)
35
+ else:
36
+ raise TypeError("`lang` must be either string or LanguageGraph")
37
+
38
+ if 'langspec_file' in log_configs:
39
+ lang_graph.save_language_specification_to_json(
40
+ log_configs['langspec_file']
41
+ )
42
+
43
+ if 'langgraph_file' in log_configs:
44
+ lang_graph.save_to_file(log_configs['langgraph_file'])
45
+
46
+ # Load model
47
+ if isinstance(model, Model):
48
+ instance_model = model
49
+ elif isinstance(model, str):
50
+ # Load from path
51
+ instance_model = Model.load_from_file(model, lang_graph)
52
+ else:
53
+ raise TypeError("`model` must be either string or Model")
54
+
55
+ if log_configs['model_file']:
56
+ instance_model.save_to_file(log_configs['model_file'])
57
+
58
+ try:
59
+ attack_graph = AttackGraph(lang_graph, instance_model)
60
+
61
+ except AttackGraphStepExpressionError as e:
62
+ logger.error(
63
+ 'Attack graph generation failed when attempting '
64
+ 'to resolve attack step expression!'
65
+ )
66
+ raise e
67
+
68
+ return attack_graph