mal-toolbox 1.0.2__py3-none-any.whl → 1.0.4__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.
- {mal_toolbox-1.0.2.dist-info → mal_toolbox-1.0.4.dist-info}/METADATA +2 -1
- {mal_toolbox-1.0.2.dist-info → mal_toolbox-1.0.4.dist-info}/RECORD +15 -12
- maltoolbox/__init__.py +2 -2
- maltoolbox/__main__.py +21 -6
- maltoolbox/attackgraph/attackgraph.py +7 -7
- maltoolbox/language/languagegraph.py +86 -181
- maltoolbox/patternfinder/attackgraph_patterns.py +134 -0
- maltoolbox/visualization/__init__.py +0 -0
- maltoolbox/visualization/graphviz_utils.py +102 -0
- {mal_toolbox-1.0.2.dist-info → mal_toolbox-1.0.4.dist-info}/WHEEL +0 -0
- {mal_toolbox-1.0.2.dist-info → mal_toolbox-1.0.4.dist-info}/entry_points.txt +0 -0
- {mal_toolbox-1.0.2.dist-info → mal_toolbox-1.0.4.dist-info}/licenses/AUTHORS +0 -0
- {mal_toolbox-1.0.2.dist-info → mal_toolbox-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {mal_toolbox-1.0.2.dist-info → mal_toolbox-1.0.4.dist-info}/top_level.txt +0 -0
- /maltoolbox/{ingestors → patternfinder}/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mal-toolbox
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
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
|
|
@@ -18,6 +18,7 @@ Requires-Python: >=3.10
|
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE
|
|
20
20
|
License-File: AUTHORS
|
|
21
|
+
Requires-Dist: graphviz
|
|
21
22
|
Requires-Dist: antlr4-tools
|
|
22
23
|
Requires-Dist: antlr4-python3-runtime
|
|
23
24
|
Requires-Dist: docopt
|
|
@@ -1,26 +1,29 @@
|
|
|
1
|
-
mal_toolbox-1.0.
|
|
2
|
-
mal_toolbox-1.0.
|
|
3
|
-
maltoolbox/__init__.py,sha256=
|
|
4
|
-
maltoolbox/__main__.py,sha256=
|
|
1
|
+
mal_toolbox-1.0.4.dist-info/licenses/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
|
|
2
|
+
mal_toolbox-1.0.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
3
|
+
maltoolbox/__init__.py,sha256=Rw4utlzsR268vbMpZlmp3bDGJ80U0CWHiEXKOePe3jU,2043
|
|
4
|
+
maltoolbox/__main__.py,sha256=A9jsYy94l1grHeSR3G3Ddn8Xg_nOihMSj1zZtVnYPSI,2974
|
|
5
5
|
maltoolbox/exceptions.py,sha256=0YjPx2v1yYumZ2o7pVZ1s_jS-GAb3Ng979KEFhROSNY,1399
|
|
6
6
|
maltoolbox/file_utils.py,sha256=fYG3UsvPQcU0ES_WI3nLfuzSZgc0jtE4IAxdMGgs9aA,1876
|
|
7
7
|
maltoolbox/model.py,sha256=xTK2jUr0Gz5pPVhdjh78zO5G46nV8N2ciIt6M5SwcGU,16058
|
|
8
8
|
maltoolbox/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
maltoolbox/attackgraph/__init__.py,sha256=m_81AjzwXONdclcW_R7mF2f8p-4DvoSRVfQ3Nyh7fak,298
|
|
10
|
-
maltoolbox/attackgraph/attackgraph.py,sha256=
|
|
10
|
+
maltoolbox/attackgraph/attackgraph.py,sha256=I2jms_X1rRLkmvkmc0z3zQAa_TnO4xA7ulPliRl5kOo,26930
|
|
11
11
|
maltoolbox/attackgraph/node.py,sha256=Z2sdzXhPel9h7ySxP9fjgd1exVmpRbvRySVtLpI1_BM,3904
|
|
12
12
|
maltoolbox/attackgraph/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
maltoolbox/ingestors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
13
|
maltoolbox/language/__init__.py,sha256=TsTTryEyjChwHN1o5F2BSUlFsAss2N6J0H0-nzvXiD8,489
|
|
15
|
-
maltoolbox/language/languagegraph.py,sha256=
|
|
14
|
+
maltoolbox/language/languagegraph.py,sha256=XWqVzHlf7EMWSkN3fQ-b6zA0pf_ysi_74YseAXAvSbs,71274
|
|
16
15
|
maltoolbox/language/compiler/__init__.py,sha256=JQyAgDwJh1pU7AmuOhd1-d2b2PYXpgMVPtxnav8QHVc,15872
|
|
17
16
|
maltoolbox/language/compiler/mal_lexer.py,sha256=BeifykDAt4PloRASOaLzBgWF35ev_zgD8lXMIsSHykc,12063
|
|
18
17
|
maltoolbox/language/compiler/mal_parser.py,sha256=sUoaE43l2VKg-Dou30mk2wlVS1FvdOREwHNIyFe4IkY,114699
|
|
18
|
+
maltoolbox/patternfinder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
maltoolbox/patternfinder/attackgraph_patterns.py,sha256=jgW7UG1yBJ08jvuokdpSiHm7jDMg2PTcjfGXZ8UJAnw,5082
|
|
19
20
|
maltoolbox/translators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
21
|
maltoolbox/translators/securicad.py,sha256=F_rndv2JyKxfHAXPwf2RrdiFPnemJVArYUpVsFP6QQk,6997
|
|
21
22
|
maltoolbox/translators/updater.py,sha256=UZPnx22udROiocCcSmtrgUJUupkjktkxl-M7rhBxUPc,8660
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
mal_toolbox-1.0.
|
|
25
|
-
mal_toolbox-1.0.
|
|
26
|
-
mal_toolbox-1.0.
|
|
23
|
+
maltoolbox/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
maltoolbox/visualization/graphviz_utils.py,sha256=dfQhPL6Z2hvlMFpThsDr-5tm4Pa22SGHEiXw5ym9JJc,3906
|
|
25
|
+
mal_toolbox-1.0.4.dist-info/METADATA,sha256=SbesLITkX_biOanT4o22POBe74UNEjZWBLOJY1P2xS0,5382
|
|
26
|
+
mal_toolbox-1.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
27
|
+
mal_toolbox-1.0.4.dist-info/entry_points.txt,sha256=oqby5O6cUP_OHCm70k_iYPA6UlbTBf7se1i3XwdK3uU,56
|
|
28
|
+
mal_toolbox-1.0.4.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
|
|
29
|
+
mal_toolbox-1.0.4.dist-info/RECORD,,
|
maltoolbox/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# MAL Toolbox v1.0.
|
|
2
|
+
# MAL Toolbox v1.0.4
|
|
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__ = "1.0.
|
|
24
|
+
__version__ = "1.0.4"
|
|
25
25
|
__authors__ = [
|
|
26
26
|
"Andrei Buhaiu",
|
|
27
27
|
"Giuseppe Nebbione",
|
maltoolbox/__main__.py
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
Command-line interface for MAL toolbox operations
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
|
-
maltoolbox attack-graph generate [options] <model_file> <lang_file>
|
|
6
5
|
maltoolbox compile <lang_file> <output_file>
|
|
6
|
+
maltoolbox generate-attack-graph [--graphviz] <model_file> <lang_file>
|
|
7
7
|
maltoolbox upgrade-model <model_file> <lang_file> <output_file>
|
|
8
|
+
maltoolbox visualize-model <model_file> <lang_file>
|
|
8
9
|
|
|
9
10
|
Arguments:
|
|
10
11
|
<model_file> Path to JSON instance model file.
|
|
11
12
|
<lang_file> Path to .mar or .mal file containing MAL spec.
|
|
12
13
|
<output_file> Path to write the result of the compilation (yml/json).
|
|
13
14
|
|
|
15
|
+
Options:
|
|
16
|
+
-h --help Show this screen.
|
|
17
|
+
-g --graphviz Visualize with graphviz
|
|
18
|
+
|
|
14
19
|
Notes:
|
|
15
20
|
- <lang_file> can be either a .mar file (generated by the older MAL
|
|
16
21
|
compiler) or a .mal file containing the DSL written in MAL.
|
|
@@ -21,17 +26,19 @@ import json
|
|
|
21
26
|
import docopt
|
|
22
27
|
|
|
23
28
|
from . import log_configs
|
|
24
|
-
from .attackgraph import create_attack_graph
|
|
29
|
+
from .attackgraph import create_attack_graph, AttackGraph
|
|
25
30
|
from .language.compiler import MalCompiler
|
|
26
31
|
from .language.languagegraph import LanguageGraph
|
|
27
32
|
from .translators.updater import load_model_from_older_version
|
|
33
|
+
from .visualization.graphviz_utils import render_model, render_attack_graph
|
|
34
|
+
from .model import Model
|
|
28
35
|
|
|
29
36
|
logger = logging.getLogger(__name__)
|
|
30
37
|
|
|
31
38
|
def generate_attack_graph(
|
|
32
39
|
model_file: str,
|
|
33
|
-
lang_file: str
|
|
34
|
-
) ->
|
|
40
|
+
lang_file: str
|
|
41
|
+
) -> AttackGraph:
|
|
35
42
|
"""Create an attack graph
|
|
36
43
|
|
|
37
44
|
Args:
|
|
@@ -43,6 +50,7 @@ def generate_attack_graph(
|
|
|
43
50
|
attack_graph.save_to_file(
|
|
44
51
|
log_configs['attackgraph_file']
|
|
45
52
|
)
|
|
53
|
+
return attack_graph
|
|
46
54
|
|
|
47
55
|
def compile(lang_file: str, output_file: str) -> None:
|
|
48
56
|
"""Compile language and dump into output file"""
|
|
@@ -64,10 +72,13 @@ def upgrade_model(model_file: str, lang_file: str, output_file: str):
|
|
|
64
72
|
def main():
|
|
65
73
|
args = docopt.docopt(__doc__)
|
|
66
74
|
|
|
67
|
-
if args['attack-graph']
|
|
68
|
-
generate_attack_graph(
|
|
75
|
+
if args['generate-attack-graph']:
|
|
76
|
+
attack_graph = generate_attack_graph(
|
|
69
77
|
args['<model_file>'], args['<lang_file>']
|
|
70
78
|
)
|
|
79
|
+
if args['--graphviz']:
|
|
80
|
+
render_attack_graph(attack_graph)
|
|
81
|
+
|
|
71
82
|
elif args['compile']:
|
|
72
83
|
compile(
|
|
73
84
|
args['<lang_file>'], args['<output_file>']
|
|
@@ -76,6 +87,10 @@ def main():
|
|
|
76
87
|
upgrade_model(
|
|
77
88
|
args['<model_file>'], args['<lang_file>'], args['<output_file>']
|
|
78
89
|
)
|
|
90
|
+
elif args['visualize-model']:
|
|
91
|
+
lang_graph = LanguageGraph.load_from_file(args['<lang_file>'])
|
|
92
|
+
model = Model.load_from_file(args['<model_file>'], lang_graph)
|
|
93
|
+
render_model(model)
|
|
79
94
|
|
|
80
95
|
if __name__ == "__main__":
|
|
81
96
|
main()
|
|
@@ -89,7 +89,7 @@ def create_attack_graph(
|
|
|
89
89
|
|
|
90
90
|
class AttackGraph():
|
|
91
91
|
"""Graph representation of attack steps"""
|
|
92
|
-
def __init__(self, lang_graph, model: Optional[Model] = None):
|
|
92
|
+
def __init__(self, lang_graph: LanguageGraph, model: Optional[Model] = None):
|
|
93
93
|
self.nodes: dict[int, AttackGraphNode] = {}
|
|
94
94
|
# Dictionaries used in optimization to get nodes by id or full name
|
|
95
95
|
# faster
|
|
@@ -535,15 +535,15 @@ class AttackGraph():
|
|
|
535
535
|
if not ag_node.model_asset:
|
|
536
536
|
raise AttackGraphException('Attack graph node is missing '
|
|
537
537
|
'asset link')
|
|
538
|
-
lang_graph_asset = self.lang_graph.assets[
|
|
539
|
-
ag_node.model_asset.type]
|
|
540
538
|
|
|
541
|
-
|
|
542
|
-
|
|
539
|
+
lang_graph_asset = self.lang_graph.assets[ag_node.model_asset.type]
|
|
540
|
+
lang_graph_attack_step: Optional[LanguageGraphAttackStep] = (
|
|
541
|
+
lang_graph_asset.attack_steps[ag_node.name]
|
|
542
|
+
)
|
|
543
543
|
|
|
544
544
|
while lang_graph_attack_step:
|
|
545
|
-
for
|
|
546
|
-
for
|
|
545
|
+
for target_attack_step, expr_chains in lang_graph_attack_step.children.items():
|
|
546
|
+
for expr_chain in expr_chains:
|
|
547
547
|
target_assets = self._follow_expr_chain(
|
|
548
548
|
self.model,
|
|
549
549
|
set([ag_node.model_asset]),
|
|
@@ -26,131 +26,6 @@ from ..exceptions import (
|
|
|
26
26
|
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
28
28
|
|
|
29
|
-
predef_ttcs: dict[str, dict] = {
|
|
30
|
-
'EasyAndUncertain':
|
|
31
|
-
{
|
|
32
|
-
'arguments': [0.5],
|
|
33
|
-
'name': 'Bernoulli',
|
|
34
|
-
'type': 'function'
|
|
35
|
-
},
|
|
36
|
-
'HardAndUncertain':
|
|
37
|
-
{
|
|
38
|
-
'lhs':
|
|
39
|
-
{
|
|
40
|
-
'arguments': [0.1],
|
|
41
|
-
'name': 'Exponential',
|
|
42
|
-
'type': 'function'
|
|
43
|
-
},
|
|
44
|
-
'rhs':
|
|
45
|
-
{
|
|
46
|
-
'arguments': [0.5],
|
|
47
|
-
'name': 'Bernoulli',
|
|
48
|
-
'type': 'function'
|
|
49
|
-
},
|
|
50
|
-
'type': 'multiplication'
|
|
51
|
-
},
|
|
52
|
-
'VeryHardAndUncertain':
|
|
53
|
-
{
|
|
54
|
-
'lhs':
|
|
55
|
-
{
|
|
56
|
-
'arguments': [0.01],
|
|
57
|
-
'name': 'Exponential',
|
|
58
|
-
'type': 'function'
|
|
59
|
-
},
|
|
60
|
-
'rhs':
|
|
61
|
-
{
|
|
62
|
-
'arguments': [0.5],
|
|
63
|
-
'name': 'Bernoulli',
|
|
64
|
-
'type': 'function'
|
|
65
|
-
},
|
|
66
|
-
'type': 'multiplication'
|
|
67
|
-
},
|
|
68
|
-
'EasyAndCertain':
|
|
69
|
-
{
|
|
70
|
-
'arguments': [1.0],
|
|
71
|
-
'name': 'Exponential',
|
|
72
|
-
'type': 'function'
|
|
73
|
-
},
|
|
74
|
-
'HardAndCertain':
|
|
75
|
-
{
|
|
76
|
-
'arguments': [0.1],
|
|
77
|
-
'name': 'Exponential',
|
|
78
|
-
'type': 'function'
|
|
79
|
-
},
|
|
80
|
-
'VeryHardAndCertain':
|
|
81
|
-
{
|
|
82
|
-
'arguments': [0.01],
|
|
83
|
-
'name': 'Exponential',
|
|
84
|
-
'type': 'function'
|
|
85
|
-
},
|
|
86
|
-
'Enabled':
|
|
87
|
-
{
|
|
88
|
-
'arguments': [1.0],
|
|
89
|
-
'name': 'Bernoulli',
|
|
90
|
-
'type': 'function'
|
|
91
|
-
},
|
|
92
|
-
'Instant':
|
|
93
|
-
{
|
|
94
|
-
'arguments': [1.0],
|
|
95
|
-
'name': 'Bernoulli',
|
|
96
|
-
'type': 'function'
|
|
97
|
-
},
|
|
98
|
-
'Disabled':
|
|
99
|
-
{
|
|
100
|
-
'arguments': [0.0],
|
|
101
|
-
'name': 'Bernoulli',
|
|
102
|
-
'type': 'function'
|
|
103
|
-
},
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
def get_ttc_distribution(
|
|
107
|
-
step_dict: dict,
|
|
108
|
-
defense_default_ttc = None,
|
|
109
|
-
attack_default_ttc = None
|
|
110
|
-
) -> Optional[dict]:
|
|
111
|
-
"""Convert step TTC to a TTC distribution if needed
|
|
112
|
-
|
|
113
|
-
- If no TTC is set, set return default TTC.
|
|
114
|
-
- If the TTC provided is a predefined name replace it with the
|
|
115
|
-
probability distribution it corresponds to.
|
|
116
|
-
- Otherwise return the TTC distribution as is.
|
|
117
|
-
|
|
118
|
-
Arguments:
|
|
119
|
-
step_dict - A dict with the attack step data
|
|
120
|
-
defense_default_ttc - the value to give a defense ttc if none is set
|
|
121
|
-
attack_default_ttc - the value to give an attack ttc if none is set
|
|
122
|
-
|
|
123
|
-
Returns:
|
|
124
|
-
A dict with the steps TTC distribution, or None if the step is not
|
|
125
|
-
a defense or attack step
|
|
126
|
-
"""
|
|
127
|
-
|
|
128
|
-
if defense_default_ttc is None:
|
|
129
|
-
defense_default_ttc = predef_ttcs['Disabled'].copy()
|
|
130
|
-
if attack_default_ttc is None:
|
|
131
|
-
attack_default_ttc = predef_ttcs['Instant'].copy()
|
|
132
|
-
|
|
133
|
-
step_ttc = step_dict['ttc']
|
|
134
|
-
|
|
135
|
-
if step_dict['type'] == 'defense':
|
|
136
|
-
if step_ttc is None:
|
|
137
|
-
# No step ttc set in language for defense
|
|
138
|
-
step_ttc = defense_default_ttc
|
|
139
|
-
elif step_dict['type'] in ('or', 'and'):
|
|
140
|
-
if step_ttc is None:
|
|
141
|
-
# No step ttc set in language for attack
|
|
142
|
-
step_ttc = attack_default_ttc
|
|
143
|
-
else:
|
|
144
|
-
# No TTC for other step types
|
|
145
|
-
return None
|
|
146
|
-
|
|
147
|
-
if 'name' in step_ttc and step_ttc['name'] in predef_ttcs:
|
|
148
|
-
# Predefined step ttc set in language, fetch from dict
|
|
149
|
-
step_ttc = predef_ttcs[step_ttc['name']].copy()
|
|
150
|
-
|
|
151
|
-
return step_ttc
|
|
152
|
-
|
|
153
|
-
|
|
154
29
|
|
|
155
30
|
def disaggregate_attack_step_full_name(
|
|
156
31
|
attack_step_full_name: str) -> list[str]:
|
|
@@ -485,8 +360,13 @@ class LanguageGraphAttackStep:
|
|
|
485
360
|
asset: LanguageGraphAsset
|
|
486
361
|
ttc: Optional[dict] = field(default_factory = dict)
|
|
487
362
|
overrides: bool = False
|
|
488
|
-
|
|
489
|
-
|
|
363
|
+
|
|
364
|
+
own_children: dict[LanguageGraphAttackStep, list[ExpressionsChain | None]] = (
|
|
365
|
+
field(default_factory = dict)
|
|
366
|
+
)
|
|
367
|
+
own_parents: dict[LanguageGraphAttackStep, list[ExpressionsChain | None]] = (
|
|
368
|
+
field(default_factory = dict)
|
|
369
|
+
)
|
|
490
370
|
info: dict = field(default_factory = dict)
|
|
491
371
|
inherits: Optional[LanguageGraphAttackStep] = None
|
|
492
372
|
own_requires: list[ExpressionsChain] = field(default_factory=list)
|
|
@@ -497,6 +377,39 @@ class LanguageGraphAttackStep:
|
|
|
497
377
|
def __hash__(self):
|
|
498
378
|
return hash(self.full_name)
|
|
499
379
|
|
|
380
|
+
@property
|
|
381
|
+
def children(self) -> dict[
|
|
382
|
+
LanguageGraphAttackStep, list[ExpressionsChain | None]
|
|
383
|
+
]:
|
|
384
|
+
"""
|
|
385
|
+
Get all (both own and inherited) children of a LanguageGraphAttackStep
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
all_children = dict(self.own_children)
|
|
389
|
+
|
|
390
|
+
if self.overrides:
|
|
391
|
+
# Override overrides the children
|
|
392
|
+
return all_children
|
|
393
|
+
|
|
394
|
+
if not self.inherits:
|
|
395
|
+
return all_children
|
|
396
|
+
|
|
397
|
+
for child_step, chains in self.inherits.children.items():
|
|
398
|
+
if child_step in all_children:
|
|
399
|
+
all_children[child_step] += [
|
|
400
|
+
chain for chain in chains
|
|
401
|
+
if chain not in all_children[child_step]
|
|
402
|
+
]
|
|
403
|
+
else:
|
|
404
|
+
all_children[child_step] = chains
|
|
405
|
+
|
|
406
|
+
return all_children
|
|
407
|
+
|
|
408
|
+
@property
|
|
409
|
+
def parents(self) -> None:
|
|
410
|
+
raise NotImplementedError(
|
|
411
|
+
"Can not fetch parents of a LanguageGraphAttackStep"
|
|
412
|
+
)
|
|
500
413
|
|
|
501
414
|
@property
|
|
502
415
|
def full_name(self) -> str:
|
|
@@ -514,8 +427,8 @@ class LanguageGraphAttackStep:
|
|
|
514
427
|
'type': self.type,
|
|
515
428
|
'asset': self.asset.name,
|
|
516
429
|
'ttc': self.ttc,
|
|
517
|
-
'
|
|
518
|
-
'
|
|
430
|
+
'own_children': {},
|
|
431
|
+
'own_parents': {},
|
|
519
432
|
'info': self.info,
|
|
520
433
|
'overrides': self.overrides,
|
|
521
434
|
'inherits': self.inherits.full_name if self.inherits else None,
|
|
@@ -524,25 +437,22 @@ class LanguageGraphAttackStep:
|
|
|
524
437
|
self.detectors.items()},
|
|
525
438
|
}
|
|
526
439
|
|
|
527
|
-
for child in self.
|
|
528
|
-
node_dict['
|
|
529
|
-
for
|
|
530
|
-
if
|
|
531
|
-
node_dict['
|
|
532
|
-
expr_chain.to_dict())
|
|
440
|
+
for child, expr_chains in self.own_children.items():
|
|
441
|
+
node_dict['own_children'][child.full_name] = []
|
|
442
|
+
for chain in expr_chains:
|
|
443
|
+
if chain:
|
|
444
|
+
node_dict['own_children'][child.full_name].append(chain.to_dict())
|
|
533
445
|
else:
|
|
534
|
-
node_dict['
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
node_dict['parents'][parent].append(
|
|
541
|
-
expr_chain.to_dict())
|
|
446
|
+
node_dict['own_children'][child.full_name].append(None)
|
|
447
|
+
for parent, expr_chains in self.own_children.items():
|
|
448
|
+
node_dict['own_parents'][parent.full_name] = []
|
|
449
|
+
for chain in expr_chains:
|
|
450
|
+
if chain:
|
|
451
|
+
node_dict['own_parents'][parent.full_name].append(chain.to_dict())
|
|
542
452
|
else:
|
|
543
|
-
node_dict['
|
|
453
|
+
node_dict['own_parents'][parent.full_name].append(None)
|
|
544
454
|
|
|
545
|
-
if
|
|
455
|
+
if self.own_requires:
|
|
546
456
|
node_dict['requires'] = []
|
|
547
457
|
for requirement in self.own_requires:
|
|
548
458
|
node_dict['requires'].append(requirement.to_dict())
|
|
@@ -963,10 +873,10 @@ class LanguageGraph():
|
|
|
963
873
|
name = attack_step_dict['name'],
|
|
964
874
|
type = attack_step_dict['type'],
|
|
965
875
|
asset = asset,
|
|
966
|
-
ttc =
|
|
876
|
+
ttc = attack_step_dict['ttc'],
|
|
967
877
|
overrides = attack_step_dict['overrides'],
|
|
968
|
-
|
|
969
|
-
|
|
878
|
+
own_children = {},
|
|
879
|
+
own_parents = {},
|
|
970
880
|
info = attack_step_dict['info'],
|
|
971
881
|
tags = list(attack_step_dict['tags'])
|
|
972
882
|
)
|
|
@@ -995,12 +905,11 @@ class LanguageGraph():
|
|
|
995
905
|
asset = lang_graph.assets[asset_dict['name']]
|
|
996
906
|
for attack_step_dict in asset_dict['attack_steps'].values():
|
|
997
907
|
attack_step = asset.attack_steps[attack_step_dict['name']]
|
|
998
|
-
for child_target in attack_step_dict['
|
|
908
|
+
for child_target in attack_step_dict['own_children'].items():
|
|
999
909
|
target_full_attack_step_name = child_target[0]
|
|
1000
910
|
expr_chains = child_target[1]
|
|
1001
911
|
target_asset_name, target_attack_step_name = \
|
|
1002
|
-
disaggregate_attack_step_full_name(
|
|
1003
|
-
target_full_attack_step_name)
|
|
912
|
+
disaggregate_attack_step_full_name(target_full_attack_step_name)
|
|
1004
913
|
target_asset = lang_graph.assets[target_asset_name]
|
|
1005
914
|
target_attack_step = target_asset.attack_steps[
|
|
1006
915
|
target_attack_step_name]
|
|
@@ -1009,33 +918,30 @@ class LanguageGraph():
|
|
|
1009
918
|
expr_chain_dict,
|
|
1010
919
|
lang_graph
|
|
1011
920
|
)
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
921
|
+
|
|
922
|
+
if target_attack_step in attack_step.own_children:
|
|
923
|
+
attack_step.own_children[target_attack_step].append(expr_chain)
|
|
1015
924
|
else:
|
|
1016
|
-
attack_step.
|
|
1017
|
-
[(target_attack_step, expr_chain)]
|
|
925
|
+
attack_step.own_children[target_attack_step] = [expr_chain]
|
|
1018
926
|
|
|
1019
|
-
for
|
|
1020
|
-
target_full_attack_step_name = parent_target[0]
|
|
1021
|
-
expr_chains = parent_target[1]
|
|
927
|
+
for (target_step_full_name, expr_chains) in attack_step_dict['own_parents'].items():
|
|
1022
928
|
target_asset_name, target_attack_step_name = \
|
|
1023
929
|
disaggregate_attack_step_full_name(
|
|
1024
|
-
|
|
930
|
+
target_step_full_name
|
|
931
|
+
)
|
|
1025
932
|
target_asset = lang_graph.assets[target_asset_name]
|
|
1026
|
-
target_attack_step = target_asset.attack_steps[
|
|
1027
|
-
target_attack_step_name]
|
|
933
|
+
target_attack_step = target_asset.attack_steps[target_attack_step_name]
|
|
1028
934
|
for expr_chain_dict in expr_chains:
|
|
1029
935
|
expr_chain = ExpressionsChain._from_dict(
|
|
1030
|
-
expr_chain_dict,
|
|
1031
|
-
lang_graph
|
|
936
|
+
expr_chain_dict, lang_graph
|
|
1032
937
|
)
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
938
|
+
|
|
939
|
+
if target_attack_step in attack_step.own_parents:
|
|
940
|
+
attack_step.own_parents[target_attack_step].append(
|
|
941
|
+
expr_chain
|
|
942
|
+
)
|
|
1036
943
|
else:
|
|
1037
|
-
attack_step.
|
|
1038
|
-
[(target_attack_step, expr_chain)]
|
|
944
|
+
attack_step.own_parents[target_attack_step] = [expr_chain]
|
|
1039
945
|
|
|
1040
946
|
# Recreate the requirements of exist and notExist attack steps
|
|
1041
947
|
if attack_step.type == 'exist' or \
|
|
@@ -1625,6 +1531,7 @@ class LanguageGraph():
|
|
|
1625
1531
|
"""
|
|
1626
1532
|
Link assets based on inheritance and associations.
|
|
1627
1533
|
"""
|
|
1534
|
+
|
|
1628
1535
|
for asset_dict in lang_spec['assets']:
|
|
1629
1536
|
asset = assets[asset_dict['name']]
|
|
1630
1537
|
if asset_dict['superAsset']:
|
|
@@ -1684,13 +1591,13 @@ class LanguageGraph():
|
|
|
1684
1591
|
name = attack_step_attribs['name'],
|
|
1685
1592
|
type = attack_step_attribs['type'],
|
|
1686
1593
|
asset = asset,
|
|
1687
|
-
ttc =
|
|
1594
|
+
ttc = attack_step_attribs['ttc'],
|
|
1688
1595
|
overrides = (
|
|
1689
1596
|
attack_step_attribs['reaches']['overrides']
|
|
1690
1597
|
if attack_step_attribs['reaches'] else False
|
|
1691
1598
|
),
|
|
1692
|
-
|
|
1693
|
-
|
|
1599
|
+
own_children = {},
|
|
1600
|
+
own_parents = {},
|
|
1694
1601
|
info = attack_step_attribs['meta'],
|
|
1695
1602
|
tags = list(attack_step_attribs['tags'])
|
|
1696
1603
|
)
|
|
@@ -1732,8 +1639,8 @@ class LanguageGraph():
|
|
|
1732
1639
|
asset = asset,
|
|
1733
1640
|
ttc = attack_step.ttc,
|
|
1734
1641
|
overrides = False,
|
|
1735
|
-
|
|
1736
|
-
|
|
1642
|
+
own_children = {},
|
|
1643
|
+
own_parents = {},
|
|
1737
1644
|
info = attack_step.info,
|
|
1738
1645
|
tags = list(attack_step.tags)
|
|
1739
1646
|
)
|
|
@@ -1799,16 +1706,14 @@ class LanguageGraph():
|
|
|
1799
1706
|
target_attack_step_name]
|
|
1800
1707
|
|
|
1801
1708
|
# Link to the children target attack steps
|
|
1802
|
-
attack_step.
|
|
1803
|
-
attack_step.
|
|
1804
|
-
(target_attack_step, expr_chain)
|
|
1805
|
-
)
|
|
1709
|
+
attack_step.own_children.setdefault(target_attack_step, [])
|
|
1710
|
+
attack_step.own_children[target_attack_step].append(expr_chain)
|
|
1806
1711
|
|
|
1807
1712
|
# Reverse the children associations chains to get the
|
|
1808
1713
|
# parents associations chain.
|
|
1809
|
-
target_attack_step.
|
|
1810
|
-
target_attack_step.
|
|
1811
|
-
|
|
1714
|
+
target_attack_step.own_parents.setdefault(attack_step, [])
|
|
1715
|
+
target_attack_step.own_parents[attack_step].append(
|
|
1716
|
+
self.reverse_expr_chain(expr_chain, None)
|
|
1812
1717
|
)
|
|
1813
1718
|
|
|
1814
1719
|
# Evaluate the requirements of exist and notExist attack steps
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Utilities for finding patterns in the AttackGraph"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Callable
|
|
6
|
+
from maltoolbox.attackgraph import AttackGraph, AttackGraphNode
|
|
7
|
+
|
|
8
|
+
class SearchPattern:
|
|
9
|
+
"""A pattern consists of conditions, the conditions are used
|
|
10
|
+
to find all matching sequences of nodes in an AttackGraph."""
|
|
11
|
+
conditions: list[SearchCondition]
|
|
12
|
+
|
|
13
|
+
def __init__(self, conditions):
|
|
14
|
+
self.conditions = conditions
|
|
15
|
+
|
|
16
|
+
def find_matches(self, graph: AttackGraph):
|
|
17
|
+
"""Search through a graph for a pattern using
|
|
18
|
+
its conditions, and return sequences of nodes
|
|
19
|
+
that match all the conditions in the pattern
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
graph - The AttackGraph to search in
|
|
23
|
+
|
|
24
|
+
Return: list[list[AttackGraphNode]] matching paths of Nodes
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Find the starting nodes which match the first condition
|
|
28
|
+
condition = self.conditions[0]
|
|
29
|
+
matching_paths = []
|
|
30
|
+
for node in graph.nodes.values():
|
|
31
|
+
if condition.matches(node):
|
|
32
|
+
matching_paths.extend(
|
|
33
|
+
find_matches_recursively(node, self.conditions)
|
|
34
|
+
)
|
|
35
|
+
return matching_paths
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class SearchCondition:
|
|
39
|
+
"""A condition that has to be true for a node to match"""
|
|
40
|
+
|
|
41
|
+
# Predefined search conditions
|
|
42
|
+
ANY = lambda _: True
|
|
43
|
+
|
|
44
|
+
# `matches` should be a lambda that takes node as input and returns bool
|
|
45
|
+
# If lamdba returns True for a node, the node matches
|
|
46
|
+
# If the lamdba returns False for a node, the node does not match
|
|
47
|
+
matches: Callable[[AttackGraphNode], bool]
|
|
48
|
+
greedy: bool = False
|
|
49
|
+
|
|
50
|
+
# It is possible to require/allow a Condition to repeat
|
|
51
|
+
min_repeated: int = 1
|
|
52
|
+
max_repeated: int = 1
|
|
53
|
+
|
|
54
|
+
def can_match_again(self, num_matches):
|
|
55
|
+
"""Returns true if condition can be used again"""
|
|
56
|
+
return num_matches < self.max_repeated
|
|
57
|
+
|
|
58
|
+
def must_match_again(self, num_matches):
|
|
59
|
+
"""Returns true if condition must match again to be fulfilled"""
|
|
60
|
+
return num_matches < self.min_repeated
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def find_matches_recursively(
|
|
64
|
+
node: AttackGraphNode,
|
|
65
|
+
condition_list: list[SearchCondition],
|
|
66
|
+
current_path: list[AttackGraphNode] | None = None,
|
|
67
|
+
matching_paths: set[tuple[AttackGraphNode,...]] | None = None,
|
|
68
|
+
condition_match_count: int = 0
|
|
69
|
+
):
|
|
70
|
+
"""Find all paths of nodes that match the list of conditions.
|
|
71
|
+
When a sequence of conditions is fulfilled for a path of nodes,
|
|
72
|
+
add the path of nodes to the returned `matching_paths`
|
|
73
|
+
The function runs recursively down all paths of children nodes.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
node - node to check if current `condition` matches for
|
|
77
|
+
condition_list - first condition in list will attempt match `node`
|
|
78
|
+
current_path - list of matched nodes so far (recursively built)
|
|
79
|
+
matching_paths - set of matched paths so far (recursively built)
|
|
80
|
+
condition_match_count - number of matches on current condition so far
|
|
81
|
+
|
|
82
|
+
Return: set of tuples (paths) of AttackGraphNodes that match the condition
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
# Init path lists if None, or copy/init into new lists for each iteration
|
|
86
|
+
current_path = [] if current_path is None else list(current_path)
|
|
87
|
+
matching_paths = set() if matching_paths is None else matching_paths
|
|
88
|
+
|
|
89
|
+
curr_cond, *next_conds = condition_list
|
|
90
|
+
|
|
91
|
+
if node in current_path:
|
|
92
|
+
# Stop the chain, infinite loop
|
|
93
|
+
return matching_paths
|
|
94
|
+
|
|
95
|
+
if next_conds and not curr_cond.must_match_again(condition_match_count):
|
|
96
|
+
# Try next condition for current node if there are more and
|
|
97
|
+
# current condition is already fulfilled.
|
|
98
|
+
matching_paths = find_matches_recursively(
|
|
99
|
+
node,
|
|
100
|
+
next_conds,
|
|
101
|
+
current_path=current_path,
|
|
102
|
+
matching_paths=matching_paths
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if curr_cond.matches(node):
|
|
106
|
+
# Current node matches, add to current_path and increment match_count
|
|
107
|
+
current_path.append(node)
|
|
108
|
+
condition_match_count += 1
|
|
109
|
+
|
|
110
|
+
if next_conds:
|
|
111
|
+
# If there are more conditions, try next one for all children
|
|
112
|
+
for child in node.children:
|
|
113
|
+
matching_paths = find_matches_recursively(
|
|
114
|
+
child,
|
|
115
|
+
next_conds,
|
|
116
|
+
current_path=current_path,
|
|
117
|
+
matching_paths=matching_paths,
|
|
118
|
+
)
|
|
119
|
+
if curr_cond.can_match_again(condition_match_count):
|
|
120
|
+
# If we can match current condition again, try for all children
|
|
121
|
+
for child in node.children:
|
|
122
|
+
matching_paths = find_matches_recursively(
|
|
123
|
+
child,
|
|
124
|
+
[curr_cond] + next_conds,
|
|
125
|
+
current_path=current_path,
|
|
126
|
+
matching_paths=matching_paths,
|
|
127
|
+
condition_match_count=condition_match_count
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if not next_conds:
|
|
131
|
+
# Congrats - matched a full unique search pattern!
|
|
132
|
+
matching_paths.add(tuple(current_path)) # tuple is hashable
|
|
133
|
+
|
|
134
|
+
return matching_paths
|
|
File without changes
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import graphviz
|
|
3
|
+
|
|
4
|
+
from ..model import Model
|
|
5
|
+
from ..attackgraph import AttackGraph
|
|
6
|
+
|
|
7
|
+
graphviz_bright_colors = [
|
|
8
|
+
"aliceblue", "antiquewhite", "antiquewhite1", "antiquewhite2", "azure", "azure1", "azure2",
|
|
9
|
+
"beige", "bisque", "bisque1", "bisque2", "blanchedalmond",
|
|
10
|
+
"cornsilk", "cornsilk1", "cornsilk2",
|
|
11
|
+
"floralwhite", "gainsboro", "ghostwhite", "gold", "gold1", "gold2",
|
|
12
|
+
"honeydew", "honeydew1", "honeydew2",
|
|
13
|
+
"ivory", "ivory1", "ivory2",
|
|
14
|
+
"khaki", "khaki1", "khaki2",
|
|
15
|
+
"lavender", "lavenderblush", "lavenderblush1", "lavenderblush2",
|
|
16
|
+
"lemonchiffon", "lemonchiffon1", "lemonchiffon2",
|
|
17
|
+
"lightblue", "lightblue1", "lightblue2",
|
|
18
|
+
"lightcyan", "lightcyan1", "lightcyan2",
|
|
19
|
+
"lightgoldenrod", "lightgoldenrod1", "lightgoldenrod2",
|
|
20
|
+
"lightgoldenrodyellow", "lightgray",
|
|
21
|
+
"lightpink", "lightpink1", "lightpink2",
|
|
22
|
+
"lightsalmon", "lightsalmon1", "lightsalmon2",
|
|
23
|
+
"lightskyblue", "lightskyblue1", "lightskyblue2",
|
|
24
|
+
"lightslategray", "lightsteelblue", "lightsteelblue1", "lightsteelblue2",
|
|
25
|
+
"lightyellow", "lightyellow1", "lightyellow2",
|
|
26
|
+
"linen", "mintcream", "mistyrose", "mistyrose1", "mistyrose2",
|
|
27
|
+
"moccasin", "navajowhite", "navajowhite1", "navajowhite2",
|
|
28
|
+
"oldlace", "papayawhip", "peachpuff", "peachpuff1", "peachpuff2",
|
|
29
|
+
"pink", "pink1", "pink2", "plum", "plum1", "plum2", "powderblue",
|
|
30
|
+
"seashell", "seashell1", "seashell2",
|
|
31
|
+
"snow", "snow1", "snow2",
|
|
32
|
+
"thistle", "thistle1", "thistle2",
|
|
33
|
+
"wheat", "wheat1", "wheat2",
|
|
34
|
+
"white", "whitesmoke", "yellow", "yellow1", "yellow2",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render_model(model: Model):
|
|
39
|
+
"""Render a model in graphviz, create pdf and open it"""
|
|
40
|
+
dot = graphviz.Digraph(model.name)
|
|
41
|
+
|
|
42
|
+
# Create nodes
|
|
43
|
+
asset_type_colors: dict[str, str] = {}
|
|
44
|
+
for asset in model.assets.values():
|
|
45
|
+
bg_color = asset_type_colors.get(asset.lg_asset.name)
|
|
46
|
+
if not bg_color:
|
|
47
|
+
bg_color = random.choice(graphviz_bright_colors)
|
|
48
|
+
asset_type_colors[asset.lg_asset.name] = bg_color
|
|
49
|
+
dot.node(
|
|
50
|
+
str(asset.id), asset.name, style="filled", fillcolor=bg_color
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Create edges
|
|
54
|
+
for from_asset in model.assets.values():
|
|
55
|
+
|
|
56
|
+
for fieldname, to_assets in from_asset.associated_assets.items():
|
|
57
|
+
for to_asset in to_assets:
|
|
58
|
+
dot.edge(
|
|
59
|
+
str(from_asset.id), str(to_asset.id), label=fieldname
|
|
60
|
+
)
|
|
61
|
+
dot.render(directory='.', view=True)
|
|
62
|
+
|
|
63
|
+
def render_attack_graph(attack_graph: AttackGraph):
|
|
64
|
+
"""Render attack graph graphviz, create pdf and open it"""
|
|
65
|
+
assert attack_graph.model, "Attack graph needs a model"
|
|
66
|
+
dot = graphviz.Graph(attack_graph.model.name)
|
|
67
|
+
dot.graph_attr['nodesep'] = '3.0' # Node separation
|
|
68
|
+
dot.graph_attr['ratio'] = 'compress'
|
|
69
|
+
|
|
70
|
+
# Create nodes
|
|
71
|
+
asset_colors: dict[str, str] = {}
|
|
72
|
+
for node in attack_graph.nodes.values():
|
|
73
|
+
assert node.model_asset, "Node needs model"
|
|
74
|
+
bg_color = asset_colors.get(node.model_asset.name)
|
|
75
|
+
if not bg_color:
|
|
76
|
+
bg_color = random.choice(graphviz_bright_colors)
|
|
77
|
+
asset_colors[node.model_asset.name] = bg_color
|
|
78
|
+
path_color = 'white'
|
|
79
|
+
match node.type:
|
|
80
|
+
case 'defense':
|
|
81
|
+
path_color = 'blue'
|
|
82
|
+
case 'or':
|
|
83
|
+
path_color = 'red'
|
|
84
|
+
case 'and':
|
|
85
|
+
path_color = 'red'
|
|
86
|
+
case 'exist':
|
|
87
|
+
path_color = 'grey'
|
|
88
|
+
case 'notExist':
|
|
89
|
+
path_color = 'grey'
|
|
90
|
+
case t:
|
|
91
|
+
raise ValueError(f'Type {t} not supported')
|
|
92
|
+
|
|
93
|
+
dot.node(
|
|
94
|
+
str(node.id), node.full_name, style="filled", color=path_color, fillcolor=bg_color
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Create edges
|
|
98
|
+
for parent in attack_graph.nodes.values():
|
|
99
|
+
for child in parent.children:
|
|
100
|
+
dot.edge(str(parent.id), str(child.id))
|
|
101
|
+
|
|
102
|
+
dot.render(directory='.', view=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|