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.
- {mal_toolbox-2.0.0/mal_toolbox.egg-info → mal_toolbox-2.1.0}/PKG-INFO +2 -2
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/README.md +1 -1
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0/mal_toolbox.egg-info}/PKG-INFO +2 -2
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/mal_toolbox.egg-info/SOURCES.txt +15 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/__init__.py +2 -2
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/attackgraph/__init__.py +2 -2
- mal_toolbox-2.1.0/maltoolbox/attackgraph/attackgraph.py +309 -0
- mal_toolbox-2.1.0/maltoolbox/attackgraph/factories.py +68 -0
- mal_toolbox-2.1.0/maltoolbox/attackgraph/generate.py +338 -0
- mal_toolbox-2.1.0/maltoolbox/attackgraph/node_getters.py +36 -0
- mal_toolbox-2.1.0/maltoolbox/attackgraph/ttcs.py +28 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/__init__.py +2 -2
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/compiler/mal_compiler.py +4 -3
- mal_toolbox-2.1.0/maltoolbox/language/detector.py +43 -0
- mal_toolbox-2.1.0/maltoolbox/language/expression_chain.py +218 -0
- mal_toolbox-2.1.0/maltoolbox/language/language_graph_asset.py +180 -0
- mal_toolbox-2.1.0/maltoolbox/language/language_graph_assoc.py +147 -0
- mal_toolbox-2.1.0/maltoolbox/language/language_graph_attack_step.py +129 -0
- mal_toolbox-2.1.0/maltoolbox/language/language_graph_builder.py +282 -0
- mal_toolbox-2.1.0/maltoolbox/language/language_graph_loaders.py +7 -0
- mal_toolbox-2.1.0/maltoolbox/language/language_graph_lookup.py +140 -0
- mal_toolbox-2.1.0/maltoolbox/language/language_graph_serialization.py +5 -0
- mal_toolbox-2.1.0/maltoolbox/language/languagegraph.py +492 -0
- mal_toolbox-2.1.0/maltoolbox/language/step_expression_processor.py +491 -0
- mal_toolbox-2.1.0/maltoolbox/py.typed +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/pyproject.toml +1 -1
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/tests/test_model.py +2 -1
- mal_toolbox-2.0.0/maltoolbox/attackgraph/attackgraph.py +0 -737
- mal_toolbox-2.0.0/maltoolbox/language/languagegraph.py +0 -1785
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/AUTHORS +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/LICENSE +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/mal_toolbox.egg-info/dependency_links.txt +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/mal_toolbox.egg-info/entry_points.txt +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/mal_toolbox.egg-info/requires.txt +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/mal_toolbox.egg-info/top_level.txt +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/__main__.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/attackgraph/analyzers/__init__.py +0 -0
- /mal_toolbox-2.0.0/maltoolbox/patternfinder/__init__.py → /mal_toolbox-2.1.0/maltoolbox/attackgraph/file_utils.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/attackgraph/node.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/exceptions.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/file_utils.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/compiler/__init__.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/compiler/distributions.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/compiler/exceptions.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/compiler/lang.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/language/compiler/mal_analyzer.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/model.py +0 -0
- /mal_toolbox-2.0.0/maltoolbox/py.typed → /mal_toolbox-2.1.0/maltoolbox/patternfinder/__init__.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/patternfinder/attackgraph_patterns.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/str_utils.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/translators/__init__.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/translators/networkx.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/translators/updater.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/visualization/__init__.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/visualization/draw_io_utils.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/visualization/graphviz_utils.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/visualization/neo4j_utils.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/maltoolbox/visualization/utils.py +0 -0
- {mal_toolbox-2.0.0 → mal_toolbox-2.1.0}/setup.cfg +0 -0
- {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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|