mal-toolbox 1.0.1__py3-none-any.whl → 1.0.3__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.1.dist-info → mal_toolbox-1.0.3.dist-info}/METADATA +19 -5
- {mal_toolbox-1.0.1.dist-info → mal_toolbox-1.0.3.dist-info}/RECORD +15 -12
- maltoolbox/__init__.py +2 -2
- maltoolbox/__main__.py +21 -6
- maltoolbox/attackgraph/attackgraph.py +8 -8
- maltoolbox/language/languagegraph.py +91 -64
- maltoolbox/patternfinder/attackgraph_patterns.py +134 -0
- maltoolbox/visualization/__init__.py +0 -0
- maltoolbox/visualization/graphviz_utils.py +102 -0
- {mal_toolbox-1.0.1.dist-info → mal_toolbox-1.0.3.dist-info}/WHEEL +0 -0
- {mal_toolbox-1.0.1.dist-info → mal_toolbox-1.0.3.dist-info}/entry_points.txt +0 -0
- {mal_toolbox-1.0.1.dist-info → mal_toolbox-1.0.3.dist-info}/licenses/AUTHORS +0 -0
- {mal_toolbox-1.0.1.dist-info → mal_toolbox-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {mal_toolbox-1.0.1.dist-info → mal_toolbox-1.0.3.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.3
|
|
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
|
|
@@ -86,13 +87,26 @@ pip install mal-toolbox
|
|
|
86
87
|
|
|
87
88
|
## Configuration
|
|
88
89
|
You can use a `maltoolbox.yml` file in the current working directory to
|
|
89
|
-
configure the toolbox.
|
|
90
|
-
|
|
90
|
+
configure the toolbox.
|
|
91
|
+
|
|
92
|
+
The config should look like this:
|
|
93
|
+
```yml
|
|
94
|
+
logging:
|
|
95
|
+
log_level: INFO
|
|
96
|
+
log_file: "logs/log.txt"
|
|
97
|
+
attackgraph_file: "logs/attackgraph.json"
|
|
98
|
+
model_file: "logs/model.yml"
|
|
99
|
+
langspec_file: "logs/langspec_file.yml"
|
|
100
|
+
langgraph_file: "logs/langspec_file.yml"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Alternatively, you can use the `MALTOOLBOX_CONFIG`
|
|
104
|
+
environment variable to set a custom config file location.
|
|
91
105
|
|
|
92
|
-
|
|
106
|
+
```bash
|
|
93
107
|
# in your shell, e.g. bash do:
|
|
94
108
|
export MALTOOLBOX_CONFIG=path/to/yml/config/file
|
|
95
|
-
|
|
109
|
+
```
|
|
96
110
|
|
|
97
111
|
The default configuration can be found here:
|
|
98
112
|
|
|
@@ -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.3.dist-info/licenses/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
|
|
2
|
+
mal_toolbox-1.0.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
3
|
+
maltoolbox/__init__.py,sha256=ThfOuukbgjz1agfeDM8wvVb0jSZmy1OlTc2oVBB5JaM,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=1GMLYov2alHRLl4w9-o3-4Fe4d2FPRLr4qyXTot6mys,74428
|
|
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.3.dist-info/METADATA,sha256=fIwAnRYoYzKPvs7zJF0UOzmuJtUQBXh974N-zAR3BTY,5382
|
|
26
|
+
mal_toolbox-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
27
|
+
mal_toolbox-1.0.3.dist-info/entry_points.txt,sha256=oqby5O6cUP_OHCm70k_iYPA6UlbTBf7se1i3XwdK3uU,56
|
|
28
|
+
mal_toolbox-1.0.3.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
|
|
29
|
+
mal_toolbox-1.0.3.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.3
|
|
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.3"
|
|
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
|
|
@@ -194,7 +194,7 @@ class AttackGraph():
|
|
|
194
194
|
ttc_dist = node_dict['ttc'],
|
|
195
195
|
existence_status = node_dict.get('existence_status', None)
|
|
196
196
|
)
|
|
197
|
-
ag_node.tags =
|
|
197
|
+
ag_node.tags = list(node_dict.get('tags', []))
|
|
198
198
|
ag_node.extras = node_dict.get('extras', {})
|
|
199
199
|
|
|
200
200
|
if node_asset:
|
|
@@ -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]),
|
|
@@ -485,18 +485,56 @@ class LanguageGraphAttackStep:
|
|
|
485
485
|
asset: LanguageGraphAsset
|
|
486
486
|
ttc: Optional[dict] = field(default_factory = dict)
|
|
487
487
|
overrides: bool = False
|
|
488
|
-
|
|
489
|
-
|
|
488
|
+
|
|
489
|
+
own_children: dict[LanguageGraphAttackStep, list[ExpressionsChain | None]] = (
|
|
490
|
+
field(default_factory = dict)
|
|
491
|
+
)
|
|
492
|
+
own_parents: dict[LanguageGraphAttackStep, list[ExpressionsChain | None]] = (
|
|
493
|
+
field(default_factory = dict)
|
|
494
|
+
)
|
|
490
495
|
info: dict = field(default_factory = dict)
|
|
491
496
|
inherits: Optional[LanguageGraphAttackStep] = None
|
|
492
497
|
own_requires: list[ExpressionsChain] = field(default_factory=list)
|
|
493
|
-
tags:
|
|
498
|
+
tags: list = field(default_factory = list)
|
|
494
499
|
detectors: dict = field(default_factory = lambda: {})
|
|
495
500
|
|
|
496
501
|
|
|
497
502
|
def __hash__(self):
|
|
498
503
|
return hash(self.full_name)
|
|
499
504
|
|
|
505
|
+
@property
|
|
506
|
+
def children(self) -> dict[
|
|
507
|
+
LanguageGraphAttackStep, list[ExpressionsChain | None]
|
|
508
|
+
]:
|
|
509
|
+
"""
|
|
510
|
+
Get all (both own and inherited) children of a LanguageGraphAttackStep
|
|
511
|
+
"""
|
|
512
|
+
|
|
513
|
+
all_children = dict(self.own_children)
|
|
514
|
+
|
|
515
|
+
if self.overrides:
|
|
516
|
+
# Override overrides the children
|
|
517
|
+
return all_children
|
|
518
|
+
|
|
519
|
+
if not self.inherits:
|
|
520
|
+
return all_children
|
|
521
|
+
|
|
522
|
+
for child_step, chains in self.inherits.children.items():
|
|
523
|
+
if child_step in all_children:
|
|
524
|
+
all_children[child_step] += [
|
|
525
|
+
chain for chain in chains
|
|
526
|
+
if chain not in all_children[child_step]
|
|
527
|
+
]
|
|
528
|
+
else:
|
|
529
|
+
all_children[child_step] = chains
|
|
530
|
+
|
|
531
|
+
return all_children
|
|
532
|
+
|
|
533
|
+
@property
|
|
534
|
+
def parents(self) -> None:
|
|
535
|
+
raise NotImplementedError(
|
|
536
|
+
"Can not fetch parents of a LanguageGraphAttackStep"
|
|
537
|
+
)
|
|
500
538
|
|
|
501
539
|
@property
|
|
502
540
|
def full_name(self) -> str:
|
|
@@ -514,8 +552,8 @@ class LanguageGraphAttackStep:
|
|
|
514
552
|
'type': self.type,
|
|
515
553
|
'asset': self.asset.name,
|
|
516
554
|
'ttc': self.ttc,
|
|
517
|
-
'
|
|
518
|
-
'
|
|
555
|
+
'own_children': {},
|
|
556
|
+
'own_parents': {},
|
|
519
557
|
'info': self.info,
|
|
520
558
|
'overrides': self.overrides,
|
|
521
559
|
'inherits': self.inherits.full_name if self.inherits else None,
|
|
@@ -524,25 +562,22 @@ class LanguageGraphAttackStep:
|
|
|
524
562
|
self.detectors.items()},
|
|
525
563
|
}
|
|
526
564
|
|
|
527
|
-
for child in self.
|
|
528
|
-
node_dict['
|
|
529
|
-
for
|
|
530
|
-
if
|
|
531
|
-
node_dict['
|
|
532
|
-
expr_chain.to_dict())
|
|
565
|
+
for child, expr_chains in self.own_children.items():
|
|
566
|
+
node_dict['own_children'][child.full_name] = []
|
|
567
|
+
for chain in expr_chains:
|
|
568
|
+
if chain:
|
|
569
|
+
node_dict['own_children'][child.full_name].append(chain.to_dict())
|
|
533
570
|
else:
|
|
534
|
-
node_dict['
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
node_dict['parents'][parent].append(
|
|
541
|
-
expr_chain.to_dict())
|
|
571
|
+
node_dict['own_children'][child.full_name].append(None)
|
|
572
|
+
for parent, expr_chains in self.own_children.items():
|
|
573
|
+
node_dict['own_parents'][parent.full_name] = []
|
|
574
|
+
for chain in expr_chains:
|
|
575
|
+
if chain:
|
|
576
|
+
node_dict['own_parents'][parent.full_name].append(chain.to_dict())
|
|
542
577
|
else:
|
|
543
|
-
node_dict['
|
|
578
|
+
node_dict['own_parents'][parent.full_name].append(None)
|
|
544
579
|
|
|
545
|
-
if
|
|
580
|
+
if self.own_requires:
|
|
546
581
|
node_dict['requires'] = []
|
|
547
582
|
for requirement in self.own_requires:
|
|
548
583
|
node_dict['requires'].append(requirement.to_dict())
|
|
@@ -965,10 +1000,10 @@ class LanguageGraph():
|
|
|
965
1000
|
asset = asset,
|
|
966
1001
|
ttc = get_ttc_distribution(attack_step_dict),
|
|
967
1002
|
overrides = attack_step_dict['overrides'],
|
|
968
|
-
|
|
969
|
-
|
|
1003
|
+
own_children = {},
|
|
1004
|
+
own_parents = {},
|
|
970
1005
|
info = attack_step_dict['info'],
|
|
971
|
-
tags =
|
|
1006
|
+
tags = list(attack_step_dict['tags'])
|
|
972
1007
|
)
|
|
973
1008
|
asset.attack_steps[attack_step_dict['name']] = \
|
|
974
1009
|
attack_step_node
|
|
@@ -995,12 +1030,11 @@ class LanguageGraph():
|
|
|
995
1030
|
asset = lang_graph.assets[asset_dict['name']]
|
|
996
1031
|
for attack_step_dict in asset_dict['attack_steps'].values():
|
|
997
1032
|
attack_step = asset.attack_steps[attack_step_dict['name']]
|
|
998
|
-
for child_target in attack_step_dict['
|
|
1033
|
+
for child_target in attack_step_dict['own_children'].items():
|
|
999
1034
|
target_full_attack_step_name = child_target[0]
|
|
1000
1035
|
expr_chains = child_target[1]
|
|
1001
1036
|
target_asset_name, target_attack_step_name = \
|
|
1002
|
-
disaggregate_attack_step_full_name(
|
|
1003
|
-
target_full_attack_step_name)
|
|
1037
|
+
disaggregate_attack_step_full_name(target_full_attack_step_name)
|
|
1004
1038
|
target_asset = lang_graph.assets[target_asset_name]
|
|
1005
1039
|
target_attack_step = target_asset.attack_steps[
|
|
1006
1040
|
target_attack_step_name]
|
|
@@ -1009,33 +1043,30 @@ class LanguageGraph():
|
|
|
1009
1043
|
expr_chain_dict,
|
|
1010
1044
|
lang_graph
|
|
1011
1045
|
)
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1046
|
+
|
|
1047
|
+
if target_attack_step in attack_step.own_children:
|
|
1048
|
+
attack_step.own_children[target_attack_step].append(expr_chain)
|
|
1015
1049
|
else:
|
|
1016
|
-
attack_step.
|
|
1017
|
-
[(target_attack_step, expr_chain)]
|
|
1050
|
+
attack_step.own_children[target_attack_step] = [expr_chain]
|
|
1018
1051
|
|
|
1019
|
-
for
|
|
1020
|
-
target_full_attack_step_name = parent_target[0]
|
|
1021
|
-
expr_chains = parent_target[1]
|
|
1052
|
+
for (target_step_full_name, expr_chains) in attack_step_dict['own_parents'].items():
|
|
1022
1053
|
target_asset_name, target_attack_step_name = \
|
|
1023
1054
|
disaggregate_attack_step_full_name(
|
|
1024
|
-
|
|
1055
|
+
target_step_full_name
|
|
1056
|
+
)
|
|
1025
1057
|
target_asset = lang_graph.assets[target_asset_name]
|
|
1026
|
-
target_attack_step = target_asset.attack_steps[
|
|
1027
|
-
target_attack_step_name]
|
|
1058
|
+
target_attack_step = target_asset.attack_steps[target_attack_step_name]
|
|
1028
1059
|
for expr_chain_dict in expr_chains:
|
|
1029
1060
|
expr_chain = ExpressionsChain._from_dict(
|
|
1030
|
-
expr_chain_dict,
|
|
1031
|
-
lang_graph
|
|
1061
|
+
expr_chain_dict, lang_graph
|
|
1032
1062
|
)
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1063
|
+
|
|
1064
|
+
if target_attack_step in attack_step.own_parents:
|
|
1065
|
+
attack_step.own_parents[target_attack_step].append(
|
|
1066
|
+
expr_chain
|
|
1067
|
+
)
|
|
1036
1068
|
else:
|
|
1037
|
-
attack_step.
|
|
1038
|
-
[(target_attack_step, expr_chain)]
|
|
1069
|
+
attack_step.own_parents[target_attack_step] = [expr_chain]
|
|
1039
1070
|
|
|
1040
1071
|
# Recreate the requirements of exist and notExist attack steps
|
|
1041
1072
|
if attack_step.type == 'exist' or \
|
|
@@ -1625,6 +1656,7 @@ class LanguageGraph():
|
|
|
1625
1656
|
"""
|
|
1626
1657
|
Link assets based on inheritance and associations.
|
|
1627
1658
|
"""
|
|
1659
|
+
|
|
1628
1660
|
for asset_dict in lang_spec['assets']:
|
|
1629
1661
|
asset = assets[asset_dict['name']]
|
|
1630
1662
|
if asset_dict['superAsset']:
|
|
@@ -1689,10 +1721,10 @@ class LanguageGraph():
|
|
|
1689
1721
|
attack_step_attribs['reaches']['overrides']
|
|
1690
1722
|
if attack_step_attribs['reaches'] else False
|
|
1691
1723
|
),
|
|
1692
|
-
|
|
1693
|
-
|
|
1724
|
+
own_children = {},
|
|
1725
|
+
own_parents = {},
|
|
1694
1726
|
info = attack_step_attribs['meta'],
|
|
1695
|
-
tags =
|
|
1727
|
+
tags = list(attack_step_attribs['tags'])
|
|
1696
1728
|
)
|
|
1697
1729
|
langspec_dict[attack_step_node.full_name] = \
|
|
1698
1730
|
attack_step_attribs
|
|
@@ -1732,10 +1764,10 @@ class LanguageGraph():
|
|
|
1732
1764
|
asset = asset,
|
|
1733
1765
|
ttc = attack_step.ttc,
|
|
1734
1766
|
overrides = False,
|
|
1735
|
-
|
|
1736
|
-
|
|
1767
|
+
own_children = {},
|
|
1768
|
+
own_parents = {},
|
|
1737
1769
|
info = attack_step.info,
|
|
1738
|
-
tags =
|
|
1770
|
+
tags = list(attack_step.tags)
|
|
1739
1771
|
)
|
|
1740
1772
|
attack_step_node.inherits = attack_step
|
|
1741
1773
|
asset.attack_steps[attack_step.name] = attack_step_node
|
|
@@ -1743,12 +1775,9 @@ class LanguageGraph():
|
|
|
1743
1775
|
# The inherited attack step was already overridden.
|
|
1744
1776
|
continue
|
|
1745
1777
|
else:
|
|
1746
|
-
asset.attack_steps[attack_step.name].inherits =
|
|
1747
|
-
|
|
1748
|
-
asset.attack_steps[attack_step.name].
|
|
1749
|
-
attack_step.tags
|
|
1750
|
-
asset.attack_steps[attack_step.name].info |= \
|
|
1751
|
-
attack_step.info
|
|
1778
|
+
asset.attack_steps[attack_step.name].inherits = attack_step
|
|
1779
|
+
asset.attack_steps[attack_step.name].tags += attack_step.tags
|
|
1780
|
+
asset.attack_steps[attack_step.name].info |= attack_step.info
|
|
1752
1781
|
|
|
1753
1782
|
# Then, link all of the attack step nodes according to their
|
|
1754
1783
|
# associations.
|
|
@@ -1802,16 +1831,14 @@ class LanguageGraph():
|
|
|
1802
1831
|
target_attack_step_name]
|
|
1803
1832
|
|
|
1804
1833
|
# Link to the children target attack steps
|
|
1805
|
-
attack_step.
|
|
1806
|
-
attack_step.
|
|
1807
|
-
(target_attack_step, expr_chain)
|
|
1808
|
-
)
|
|
1834
|
+
attack_step.own_children.setdefault(target_attack_step, [])
|
|
1835
|
+
attack_step.own_children[target_attack_step].append(expr_chain)
|
|
1809
1836
|
|
|
1810
1837
|
# Reverse the children associations chains to get the
|
|
1811
1838
|
# parents associations chain.
|
|
1812
|
-
target_attack_step.
|
|
1813
|
-
target_attack_step.
|
|
1814
|
-
|
|
1839
|
+
target_attack_step.own_parents.setdefault(attack_step, [])
|
|
1840
|
+
target_attack_step.own_parents[attack_step].append(
|
|
1841
|
+
self.reverse_expr_chain(expr_chain, None)
|
|
1815
1842
|
)
|
|
1816
1843
|
|
|
1817
1844
|
# 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
|