mal-toolbox 0.3.11__py3-none-any.whl → 1.0.0__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-0.3.11.dist-info → mal_toolbox-1.0.0.dist-info}/METADATA +4 -22
- mal_toolbox-1.0.0.dist-info/RECORD +26 -0
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.0.dist-info}/WHEEL +1 -1
- maltoolbox/__init__.py +5 -6
- maltoolbox/__main__.py +3 -34
- maltoolbox/attackgraph/__init__.py +7 -1
- maltoolbox/attackgraph/attackgraph.py +51 -192
- maltoolbox/attackgraph/node.py +2 -82
- maltoolbox/file_utils.py +1 -1
- maltoolbox/language/__init__.py +11 -0
- maltoolbox/language/languagegraph.py +631 -369
- maltoolbox/model.py +6 -208
- maltoolbox/py.typed +0 -0
- maltoolbox/translators/securicad.py +1 -1
- maltoolbox/translators/updater.py +1 -1
- mal_toolbox-0.3.11.dist-info/RECORD +0 -29
- maltoolbox/attackgraph/analyzers/apriori.py +0 -243
- maltoolbox/attackgraph/attacker.py +0 -109
- maltoolbox/attackgraph/query.py +0 -196
- maltoolbox/ingestors/neo4j.py +0 -244
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.0.dist-info}/entry_points.txt +0 -0
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.0.dist-info/licenses}/AUTHORS +0 -0
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.0.dist-info/licenses}/LICENSE +0 -0
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: mal-toolbox
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.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>
|
|
6
6
|
License: Apache Software License
|
|
@@ -23,6 +23,7 @@ Requires-Dist: antlr4-tools
|
|
|
23
23
|
Requires-Dist: antlr4-python3-runtime
|
|
24
24
|
Requires-Dist: docopt
|
|
25
25
|
Requires-Dist: PyYAML
|
|
26
|
+
Dynamic: license-file
|
|
26
27
|
|
|
27
28
|
# MAL Toolbox overview
|
|
28
29
|
|
|
@@ -30,7 +31,6 @@ MAL Toolbox is a collection of python modules to help developers create and work
|
|
|
30
31
|
MAL ([Meta Attack Language](https://mal-lang.org/)) models and attack graphs.
|
|
31
32
|
|
|
32
33
|
Attack graphs can be used to run simulations (see MAL Simulator) or analysis.
|
|
33
|
-
MAL Toolbox also gives the ability to view the AttackGraph/Model graphically in neo4j.
|
|
34
34
|
|
|
35
35
|
[Documentation](https://mal-lang.org/mal-toolbox/index.html)(Work in progress)
|
|
36
36
|
|
|
@@ -53,13 +53,7 @@ With a MAL language a Model (a MAL instance model) can be created either
|
|
|
53
53
|
from a model file or empty.
|
|
54
54
|
|
|
55
55
|
The model class will store all of the relevant information to the MAL
|
|
56
|
-
instance model, most importantly the assets and associations
|
|
57
|
-
|
|
58
|
-
Assets and associations are objects of classes created using the language
|
|
59
|
-
classes factory submodule in runtime. It also allows for `Attacker` objects
|
|
60
|
-
to be created and associated with attack steps on assets in the model.
|
|
61
|
-
The most relevant methods of the Model are the ones used to add different
|
|
62
|
-
elements to the model, `add_asset`, `add_association`, and `add_attacker`.
|
|
56
|
+
instance model, most importantly the assets and their associations.
|
|
63
57
|
|
|
64
58
|
Model objects can be used to generate attack graphs with the AttackGraph module.
|
|
65
59
|
|
|
@@ -76,11 +70,6 @@ nodes related and the asset field which will contain the object in the model
|
|
|
76
70
|
instance to which this attack step belongs to, if this information is
|
|
77
71
|
available.
|
|
78
72
|
|
|
79
|
-
If it is relevant the `attach_attackers` function can be called on the
|
|
80
|
-
resulting attack graph with the instance model given as a parameter in order
|
|
81
|
-
to create attack step nodes that represent the entry points of the attackers
|
|
82
|
-
and attach them to the attack steps specified in the instance model.
|
|
83
|
-
|
|
84
73
|
## Ingestors Module
|
|
85
74
|
|
|
86
75
|
The ingestors module contains various tools that can make use of the instance
|
|
@@ -131,16 +120,9 @@ Arguments:
|
|
|
131
120
|
<lang_file> Path to .mar or .mal file containing MAL spec.
|
|
132
121
|
<output_file> Path to write the result of the compilation (yml/json).
|
|
133
122
|
|
|
134
|
-
Options:
|
|
135
|
-
--neo4j Ingest attack graph and instance model into a Neo4j instance
|
|
136
|
-
|
|
137
123
|
Notes:
|
|
138
124
|
- <lang_file> can be either a .mar file (generated by the older MAL
|
|
139
125
|
compiler) or a .mal file containing the DSL written in MAL.
|
|
140
|
-
|
|
141
|
-
- If --neo4j is used, the Neo4j instance should be running. The connection
|
|
142
|
-
parameters required for this app to reach the Neo4j instance should be
|
|
143
|
-
defined in the default.conf file.
|
|
144
126
|
```
|
|
145
127
|
|
|
146
128
|
## Code examples / Tutorial
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
mal_toolbox-1.0.0.dist-info/licenses/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
|
|
2
|
+
mal_toolbox-1.0.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
3
|
+
maltoolbox/__init__.py,sha256=Y_GfqlViiQEqrvkQX84F4xponM9OffWQD3CsIxcuxLo,2043
|
|
4
|
+
maltoolbox/__main__.py,sha256=QBloKCJ_RMsFPZ8qiWZQnoP2gnnRyECIJBfA1zTAYJM,2394
|
|
5
|
+
maltoolbox/exceptions.py,sha256=0YjPx2v1yYumZ2o7pVZ1s_jS-GAb3Ng979KEFhROSNY,1399
|
|
6
|
+
maltoolbox/file_utils.py,sha256=fYG3UsvPQcU0ES_WI3nLfuzSZgc0jtE4IAxdMGgs9aA,1876
|
|
7
|
+
maltoolbox/model.py,sha256=f2j8GFwIWWD98f892dsH054PwDVpOjOMPQu2kPrZKr8,16644
|
|
8
|
+
maltoolbox/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
maltoolbox/attackgraph/__init__.py,sha256=m_81AjzwXONdclcW_R7mF2f8p-4DvoSRVfQ3Nyh7fak,298
|
|
10
|
+
maltoolbox/attackgraph/attackgraph.py,sha256=Gw6xvBmI8p16Nun8F0XBHniwyWpUGjP7PLq3NMTzU9M,26626
|
|
11
|
+
maltoolbox/attackgraph/node.py,sha256=Z2sdzXhPel9h7ySxP9fjgd1exVmpRbvRySVtLpI1_BM,3904
|
|
12
|
+
maltoolbox/attackgraph/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
maltoolbox/ingestors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
maltoolbox/language/__init__.py,sha256=TsTTryEyjChwHN1o5F2BSUlFsAss2N6J0H0-nzvXiD8,489
|
|
15
|
+
maltoolbox/language/languagegraph.py,sha256=wkv9WKje89uu2KDcm2dKS0st1IDz5xkYFkS4xVHQel8,73196
|
|
16
|
+
maltoolbox/language/compiler/__init__.py,sha256=JQyAgDwJh1pU7AmuOhd1-d2b2PYXpgMVPtxnav8QHVc,15872
|
|
17
|
+
maltoolbox/language/compiler/mal_lexer.py,sha256=BeifykDAt4PloRASOaLzBgWF35ev_zgD8lXMIsSHykc,12063
|
|
18
|
+
maltoolbox/language/compiler/mal_parser.py,sha256=sUoaE43l2VKg-Dou30mk2wlVS1FvdOREwHNIyFe4IkY,114699
|
|
19
|
+
maltoolbox/translators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
maltoolbox/translators/securicad.py,sha256=F_rndv2JyKxfHAXPwf2RrdiFPnemJVArYUpVsFP6QQk,6997
|
|
21
|
+
maltoolbox/translators/updater.py,sha256=UZPnx22udROiocCcSmtrgUJUupkjktkxl-M7rhBxUPc,8660
|
|
22
|
+
mal_toolbox-1.0.0.dist-info/METADATA,sha256=K-hIqrvW9LgMHdsaoiEJRM0t2NbBLNptdYQsgGyXblE,5129
|
|
23
|
+
mal_toolbox-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
mal_toolbox-1.0.0.dist-info/entry_points.txt,sha256=oqby5O6cUP_OHCm70k_iYPA6UlbTBf7se1i3XwdK3uU,56
|
|
25
|
+
mal_toolbox-1.0.0.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
|
|
26
|
+
mal_toolbox-1.0.0.dist-info/RECORD,,
|
maltoolbox/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# MAL Toolbox
|
|
2
|
+
# MAL Toolbox v1.0.0
|
|
3
3
|
# Copyright 2025, Andrei Buhaiu.
|
|
4
4
|
#
|
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -21,7 +21,7 @@ MAL-Toolbox Framework
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
__title__ = "maltoolbox"
|
|
24
|
-
__version__ = "0.
|
|
24
|
+
__version__ = "1.0.0"
|
|
25
25
|
__authors__ = [
|
|
26
26
|
"Andrei Buhaiu",
|
|
27
27
|
"Giuseppe Nebbione",
|
|
@@ -45,9 +45,9 @@ config: dict[str, Any] = {
|
|
|
45
45
|
"log_file": "logs/log.txt",
|
|
46
46
|
"attackgraph_file": "logs/attackgraph.yml",
|
|
47
47
|
"model_file": "logs/model.yml",
|
|
48
|
-
"langspec_file": "logs/langspec_file.
|
|
48
|
+
"langspec_file": "logs/langspec_file.json",
|
|
49
|
+
"langgraph_file": "logs/langgraph.yml",
|
|
49
50
|
},
|
|
50
|
-
"neo4j": {"uri": None, "username": None, "password": None, "dbname": None},
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
config_file = os.getenv("MALTOOLBOX_CONFIG", "maltoolbox.yml")
|
|
@@ -56,8 +56,7 @@ if os.path.exists(config_file):
|
|
|
56
56
|
with open(config_file) as f:
|
|
57
57
|
config |= yaml.safe_load(f)
|
|
58
58
|
|
|
59
|
-
log_configs
|
|
60
|
-
|
|
59
|
+
log_configs = config['logging']
|
|
61
60
|
os.makedirs(os.path.dirname(log_configs["log_file"]), exist_ok=True)
|
|
62
61
|
|
|
63
62
|
formatter = logging.Formatter(
|
maltoolbox/__main__.py
CHANGED
|
@@ -11,26 +11,18 @@ Arguments:
|
|
|
11
11
|
<lang_file> Path to .mar or .mal file containing MAL spec.
|
|
12
12
|
<output_file> Path to write the result of the compilation (yml/json).
|
|
13
13
|
|
|
14
|
-
Options:
|
|
15
|
-
--neo4j Ingest attack graph and instance model into a Neo4j instance
|
|
16
|
-
|
|
17
14
|
Notes:
|
|
18
15
|
- <lang_file> can be either a .mar file (generated by the older MAL
|
|
19
16
|
compiler) or a .mal file containing the DSL written in MAL.
|
|
20
|
-
|
|
21
|
-
- If --neo4j is used, the Neo4j instance should be running. The connection
|
|
22
|
-
parameters required for this app to reach the Neo4j instance should be
|
|
23
|
-
defined in the default.conf file.
|
|
24
17
|
"""
|
|
25
18
|
|
|
26
19
|
import logging
|
|
27
20
|
import json
|
|
28
21
|
import docopt
|
|
29
22
|
|
|
30
|
-
from . import log_configs
|
|
23
|
+
from . import log_configs
|
|
31
24
|
from .attackgraph import create_attack_graph
|
|
32
25
|
from .language.compiler import MalCompiler
|
|
33
|
-
from .ingestors import neo4j
|
|
34
26
|
from .language.languagegraph import LanguageGraph
|
|
35
27
|
from .translators.updater import load_model_from_older_version
|
|
36
28
|
|
|
@@ -39,14 +31,12 @@ logger = logging.getLogger(__name__)
|
|
|
39
31
|
def generate_attack_graph(
|
|
40
32
|
model_file: str,
|
|
41
33
|
lang_file: str,
|
|
42
|
-
send_to_neo4j: bool
|
|
43
34
|
) -> None:
|
|
44
|
-
"""Create an attack graph
|
|
35
|
+
"""Create an attack graph
|
|
45
36
|
|
|
46
37
|
Args:
|
|
47
38
|
model_file - path to the model file
|
|
48
39
|
lang_file - path to the language file
|
|
49
|
-
send_to_neo4j - whether to ingest into neo4j or not
|
|
50
40
|
"""
|
|
51
41
|
attack_graph = create_attack_graph(lang_file, model_file)
|
|
52
42
|
if log_configs['attackgraph_file']:
|
|
@@ -54,27 +44,6 @@ def generate_attack_graph(
|
|
|
54
44
|
log_configs['attackgraph_file']
|
|
55
45
|
)
|
|
56
46
|
|
|
57
|
-
if send_to_neo4j:
|
|
58
|
-
logger.debug('Ingest model graph into Neo4J database.')
|
|
59
|
-
neo4j.ingest_model(
|
|
60
|
-
attack_graph.model,
|
|
61
|
-
neo4j_configs['uri'],
|
|
62
|
-
neo4j_configs['username'],
|
|
63
|
-
neo4j_configs['password'],
|
|
64
|
-
neo4j_configs['dbname'],
|
|
65
|
-
delete=True
|
|
66
|
-
)
|
|
67
|
-
logger.debug('Ingest attack graph into Neo4J database.')
|
|
68
|
-
neo4j.ingest_attack_graph(
|
|
69
|
-
attack_graph,
|
|
70
|
-
neo4j_configs['uri'],
|
|
71
|
-
neo4j_configs['username'],
|
|
72
|
-
neo4j_configs['password'],
|
|
73
|
-
neo4j_configs['dbname'],
|
|
74
|
-
delete=False
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
47
|
def compile(lang_file: str, output_file: str) -> None:
|
|
79
48
|
"""Compile language and dump into output file"""
|
|
80
49
|
compiler = MalCompiler()
|
|
@@ -97,7 +66,7 @@ def main():
|
|
|
97
66
|
|
|
98
67
|
if args['attack-graph'] and args['generate']:
|
|
99
68
|
generate_attack_graph(
|
|
100
|
-
args['<model_file>'], args['<lang_file>']
|
|
69
|
+
args['<model_file>'], args['<lang_file>']
|
|
101
70
|
)
|
|
102
71
|
elif args['compile']:
|
|
103
72
|
compile(
|
|
@@ -3,6 +3,12 @@ Contains tools used to generate attack graphs from MAL instance
|
|
|
3
3
|
models and analyze attack graphs.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from .attacker import Attacker
|
|
7
6
|
from .attackgraph import AttackGraph, create_attack_graph
|
|
8
7
|
from .node import AttackGraphNode
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Attacker",
|
|
11
|
+
"AttackGraph",
|
|
12
|
+
"AttackGraphNode",
|
|
13
|
+
"create_attack_graph"
|
|
14
|
+
]
|
|
@@ -11,9 +11,7 @@ import zipfile
|
|
|
11
11
|
from itertools import chain
|
|
12
12
|
from typing import TYPE_CHECKING
|
|
13
13
|
|
|
14
|
-
from .analyzers.apriori import calculate_viability_and_necessity
|
|
15
14
|
from .node import AttackGraphNode
|
|
16
|
-
from .attacker import Attacker
|
|
17
15
|
from .. import log_configs
|
|
18
16
|
from ..exceptions import AttackGraphStepExpressionError, AttackGraphException
|
|
19
17
|
from ..exceptions import LanguageGraphException
|
|
@@ -35,28 +33,44 @@ logger = logging.getLogger(__name__)
|
|
|
35
33
|
|
|
36
34
|
|
|
37
35
|
def create_attack_graph(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
attach_attackers=True,
|
|
41
|
-
calc_viability_and_necessity=True
|
|
36
|
+
lang: str | LanguageGraph,
|
|
37
|
+
model: str | Model,
|
|
42
38
|
) -> AttackGraph:
|
|
43
39
|
"""Create and return an attack graph
|
|
44
40
|
|
|
45
41
|
Args:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
attach_attackers - whether to run attach_attackers or not
|
|
49
|
-
calc_viability_and_necessity - whether run apriori calculations or not
|
|
42
|
+
lang - path to language file (.mar or .mal) or a LanguageGraph object
|
|
43
|
+
model - path to model file (yaml or json) or a Model object
|
|
50
44
|
"""
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
lang_graph =
|
|
45
|
+
|
|
46
|
+
# Load language
|
|
47
|
+
if isinstance(lang, LanguageGraph):
|
|
48
|
+
lang_graph = lang
|
|
49
|
+
elif isinstance(lang, str):
|
|
50
|
+
# Load from path
|
|
51
|
+
try:
|
|
52
|
+
lang_graph = LanguageGraph.from_mar_archive(lang)
|
|
53
|
+
except zipfile.BadZipFile:
|
|
54
|
+
lang_graph = LanguageGraph.from_mal_spec(lang)
|
|
55
|
+
else:
|
|
56
|
+
raise TypeError("`lang` must be either string or LanguageGraph")
|
|
55
57
|
|
|
56
58
|
if log_configs['langspec_file']:
|
|
57
|
-
lang_graph.
|
|
59
|
+
lang_graph.save_language_specification_to_json(
|
|
60
|
+
log_configs['langspec_file']
|
|
61
|
+
)
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
if log_configs['langgraph_file']:
|
|
64
|
+
lang_graph.save_to_file(log_configs['langgraph_file'])
|
|
65
|
+
|
|
66
|
+
# Load model
|
|
67
|
+
if isinstance(model, Model):
|
|
68
|
+
instance_model = model
|
|
69
|
+
elif isinstance(model, str):
|
|
70
|
+
# Load from path
|
|
71
|
+
instance_model = Model.load_from_file(model, lang_graph)
|
|
72
|
+
else:
|
|
73
|
+
raise TypeError("`model` must be either string or Model")
|
|
60
74
|
|
|
61
75
|
if log_configs['model_file']:
|
|
62
76
|
instance_model.save_to_file(log_configs['model_file'])
|
|
@@ -70,12 +84,6 @@ def create_attack_graph(
|
|
|
70
84
|
)
|
|
71
85
|
sys.exit(1)
|
|
72
86
|
|
|
73
|
-
if attach_attackers:
|
|
74
|
-
attack_graph.attach_attackers()
|
|
75
|
-
|
|
76
|
-
if calc_viability_and_necessity:
|
|
77
|
-
calculate_viability_and_necessity(attack_graph)
|
|
78
|
-
|
|
79
87
|
return attack_graph
|
|
80
88
|
|
|
81
89
|
|
|
@@ -83,15 +91,13 @@ class AttackGraph():
|
|
|
83
91
|
"""Graph representation of attack steps"""
|
|
84
92
|
def __init__(self, lang_graph, model: Optional[Model] = None):
|
|
85
93
|
self.nodes: dict[int, AttackGraphNode] = {}
|
|
86
|
-
|
|
87
|
-
#
|
|
88
|
-
# or full name faster
|
|
94
|
+
# Dictionaries used in optimization to get nodes by id or full name
|
|
95
|
+
# faster
|
|
89
96
|
self._full_name_to_node: dict[str, AttackGraphNode] = {}
|
|
90
97
|
|
|
91
98
|
self.model = model
|
|
92
99
|
self.lang_graph = lang_graph
|
|
93
100
|
self.next_node_id = 0
|
|
94
|
-
self.next_attacker_id = 0
|
|
95
101
|
if self.model is not None:
|
|
96
102
|
self._generate_graph()
|
|
97
103
|
|
|
@@ -102,18 +108,11 @@ class AttackGraph():
|
|
|
102
108
|
def _to_dict(self) -> dict:
|
|
103
109
|
"""Convert AttackGraph to dict"""
|
|
104
110
|
serialized_attack_steps = {}
|
|
105
|
-
serialized_attackers = {}
|
|
106
111
|
for ag_node in self.nodes.values():
|
|
107
112
|
serialized_attack_steps[ag_node.full_name] =\
|
|
108
113
|
ag_node.to_dict()
|
|
109
|
-
for attacker in self.attackers.values():
|
|
110
|
-
serialized_attackers[attacker.name] = attacker.to_dict()
|
|
111
|
-
logger.debug('Serialized %d attack steps and %d attackers.' %
|
|
112
|
-
(len(self.nodes), len(self.attackers))
|
|
113
|
-
)
|
|
114
114
|
return {
|
|
115
|
-
'attack_steps': serialized_attack_steps
|
|
116
|
-
'attackers': serialized_attackers,
|
|
115
|
+
'attack_steps': serialized_attack_steps
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
def __deepcopy__(self, memo):
|
|
@@ -139,24 +138,12 @@ class AttackGraph():
|
|
|
139
138
|
if node.children:
|
|
140
139
|
memo[id(node)].children = copy.deepcopy(node.children, memo)
|
|
141
140
|
|
|
142
|
-
# Deep copy attackers
|
|
143
|
-
for attacker_id, attacker in self.attackers.items():
|
|
144
|
-
copied_attacker = copy.deepcopy(attacker, memo)
|
|
145
|
-
copied_attackgraph.attackers[attacker_id] = copied_attacker
|
|
146
|
-
|
|
147
|
-
# Re-link attacker references
|
|
148
|
-
for node in self.nodes.values():
|
|
149
|
-
if node.compromised_by:
|
|
150
|
-
memo[id(node)].compromised_by = copy.deepcopy(
|
|
151
|
-
node.compromised_by, memo)
|
|
152
|
-
|
|
153
141
|
# Copy lookup dicts
|
|
154
142
|
copied_attackgraph._full_name_to_node = \
|
|
155
143
|
copy.deepcopy(self._full_name_to_node, memo)
|
|
156
144
|
|
|
157
145
|
# Copy counters
|
|
158
146
|
copied_attackgraph.next_node_id = self.next_node_id
|
|
159
|
-
copied_attackgraph.next_attacker_id = self.next_attacker_id
|
|
160
147
|
|
|
161
148
|
return copied_attackgraph
|
|
162
149
|
|
|
@@ -181,7 +168,6 @@ class AttackGraph():
|
|
|
181
168
|
attack_graph = AttackGraph(lang_graph)
|
|
182
169
|
attack_graph.model = model
|
|
183
170
|
serialized_attack_steps = serialized_object['attack_steps']
|
|
184
|
-
serialized_attackers = serialized_object['attackers']
|
|
185
171
|
|
|
186
172
|
# Create all of the nodes in the imported attack graph.
|
|
187
173
|
for node_dict in serialized_attack_steps.values():
|
|
@@ -205,7 +191,7 @@ class AttackGraph():
|
|
|
205
191
|
lg_attack_step = lg_attack_step,
|
|
206
192
|
node_id = node_dict['id'],
|
|
207
193
|
model_asset = node_asset,
|
|
208
|
-
|
|
194
|
+
ttc_dist = node_dict['ttc'],
|
|
209
195
|
existence_status = node_dict.get('existence_status', None)
|
|
210
196
|
)
|
|
211
197
|
ag_node.tags = set(node_dict.get('tags', []))
|
|
@@ -248,21 +234,6 @@ class AttackGraph():
|
|
|
248
234
|
raise LookupError(msg % parent_id)
|
|
249
235
|
_ag_node.parents.add(parent)
|
|
250
236
|
|
|
251
|
-
for attacker in serialized_attackers.values():
|
|
252
|
-
ag_attacker = Attacker(name = attacker['name'])
|
|
253
|
-
attack_graph.add_attacker(
|
|
254
|
-
attacker = ag_attacker,
|
|
255
|
-
attacker_id = int(attacker['id']),
|
|
256
|
-
entry_points = [
|
|
257
|
-
int(node_id) # Convert to int since they can be strings
|
|
258
|
-
for node_id in attacker['entry_points'].keys()
|
|
259
|
-
],
|
|
260
|
-
reached_attack_steps = [
|
|
261
|
-
int(node_id) # Convert to int since they can be strings
|
|
262
|
-
for node_id in attacker['reached_attack_steps'].keys()
|
|
263
|
-
]
|
|
264
|
-
)
|
|
265
|
-
|
|
266
237
|
return attack_graph
|
|
267
238
|
|
|
268
239
|
@classmethod
|
|
@@ -304,46 +275,6 @@ class AttackGraph():
|
|
|
304
275
|
logger.debug(f'Looking up node with full name "%s"', full_name)
|
|
305
276
|
return self._full_name_to_node.get(full_name)
|
|
306
277
|
|
|
307
|
-
def attach_attackers(self) -> None:
|
|
308
|
-
"""
|
|
309
|
-
Create attackers and their entry point nodes and attach them to the
|
|
310
|
-
relevant attack step nodes and to the attackers.
|
|
311
|
-
"""
|
|
312
|
-
|
|
313
|
-
if not self.model:
|
|
314
|
-
msg = "Can not attach attackers without a model"
|
|
315
|
-
logger.error(msg)
|
|
316
|
-
raise AttackGraphException(msg)
|
|
317
|
-
|
|
318
|
-
logger.info(
|
|
319
|
-
'Attach attackers from "%s" model to the graph.', self.model.name
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
for attacker_info in self.model.attackers:
|
|
323
|
-
|
|
324
|
-
if not attacker_info.name:
|
|
325
|
-
msg = "Can not attach attacker without name"
|
|
326
|
-
logger.error(msg)
|
|
327
|
-
raise AttackGraphException(msg)
|
|
328
|
-
|
|
329
|
-
attacker = Attacker(name = attacker_info.name)
|
|
330
|
-
self.add_attacker(attacker)
|
|
331
|
-
|
|
332
|
-
for (asset, attack_steps) in attacker_info.entry_points:
|
|
333
|
-
for attack_step in attack_steps:
|
|
334
|
-
full_name = asset.name + ':' + attack_step
|
|
335
|
-
ag_node = self.get_node_by_full_name(full_name)
|
|
336
|
-
if not ag_node:
|
|
337
|
-
logger.warning(
|
|
338
|
-
'Failed to find attacker entry point '
|
|
339
|
-
'%s for %s.',
|
|
340
|
-
full_name, attacker.name
|
|
341
|
-
)
|
|
342
|
-
continue
|
|
343
|
-
attacker.compromise(ag_node)
|
|
344
|
-
|
|
345
|
-
attacker.entry_points = set(attacker.reached_attack_steps)
|
|
346
|
-
|
|
347
278
|
def _follow_expr_chain(
|
|
348
279
|
self,
|
|
349
280
|
model: Model,
|
|
@@ -535,17 +466,22 @@ class AttackGraph():
|
|
|
535
466
|
'Generating attack step node for %s.', attack_step.name
|
|
536
467
|
)
|
|
537
468
|
|
|
538
|
-
defense_status = None
|
|
539
469
|
existence_status = None
|
|
540
470
|
node_name = asset.name + ':' + attack_step.name
|
|
541
471
|
|
|
472
|
+
ttc_dist = attack_step.ttc
|
|
542
473
|
match (attack_step.type):
|
|
543
474
|
case 'defense':
|
|
544
|
-
# Set the
|
|
545
|
-
|
|
475
|
+
# Set the TTC probability for defenses
|
|
476
|
+
defense_value = float(asset.defenses[attack_step.name])
|
|
477
|
+
ttc_dist = {
|
|
478
|
+
'arguments': [defense_value],
|
|
479
|
+
'name': 'Bernoulli',
|
|
480
|
+
'type': 'function'
|
|
481
|
+
}
|
|
546
482
|
logger.debug(
|
|
547
|
-
'Setting
|
|
548
|
-
node_name,
|
|
483
|
+
'Setting defense \"%s\" to "%s".',
|
|
484
|
+
node_name, defense_value
|
|
549
485
|
)
|
|
550
486
|
|
|
551
487
|
case 'exist' | 'notExist':
|
|
@@ -577,7 +513,7 @@ class AttackGraph():
|
|
|
577
513
|
ag_node = self.add_node(
|
|
578
514
|
lg_attack_step = attack_step,
|
|
579
515
|
model_asset = asset,
|
|
580
|
-
|
|
516
|
+
ttc_dist = ttc_dist,
|
|
581
517
|
existence_status = existence_status
|
|
582
518
|
)
|
|
583
519
|
attack_step_nodes.append(ag_node)
|
|
@@ -660,7 +596,6 @@ class AttackGraph():
|
|
|
660
596
|
"""
|
|
661
597
|
|
|
662
598
|
self.nodes = {}
|
|
663
|
-
self.attackers = {}
|
|
664
599
|
self._generate_graph()
|
|
665
600
|
|
|
666
601
|
def add_node(
|
|
@@ -668,7 +603,7 @@ class AttackGraph():
|
|
|
668
603
|
lg_attack_step: LanguageGraphAttackStep,
|
|
669
604
|
node_id: Optional[int] = None,
|
|
670
605
|
model_asset: Optional[ModelAsset] = None,
|
|
671
|
-
|
|
606
|
+
ttc_dist: Optional[dict] = None,
|
|
672
607
|
existence_status: Optional[bool] = None
|
|
673
608
|
) -> AttackGraphNode:
|
|
674
609
|
"""Create and add a node to the graph
|
|
@@ -685,9 +620,10 @@ class AttackGraph():
|
|
|
685
620
|
only be ommitted if the model which was used to
|
|
686
621
|
generate the attack graph is not available when
|
|
687
622
|
loading an attack graph from a file.
|
|
688
|
-
|
|
689
|
-
for
|
|
690
|
-
|
|
623
|
+
ttc_dist - the ttc distribution to assign to the node. This
|
|
624
|
+
is relevant for when we want to override the ttc
|
|
625
|
+
distribution as it is defined in the language.
|
|
626
|
+
Frequently used for defenses.
|
|
691
627
|
existence_status - the existence status of the node. Only, relevant
|
|
692
628
|
for exist and notExist type nodes.
|
|
693
629
|
|
|
@@ -712,7 +648,7 @@ class AttackGraph():
|
|
|
712
648
|
node_id = node_id,
|
|
713
649
|
lg_attack_step = lg_attack_step,
|
|
714
650
|
model_asset = model_asset,
|
|
715
|
-
|
|
651
|
+
ttc_dist = ttc_dist,
|
|
716
652
|
existence_status = existence_status
|
|
717
653
|
)
|
|
718
654
|
|
|
@@ -738,80 +674,3 @@ class AttackGraph():
|
|
|
738
674
|
raise ValueError(f'Invalid node id.')
|
|
739
675
|
del self.nodes[node.id]
|
|
740
676
|
del self._full_name_to_node[node.full_name]
|
|
741
|
-
|
|
742
|
-
def add_attacker(
|
|
743
|
-
self,
|
|
744
|
-
attacker: Attacker,
|
|
745
|
-
attacker_id: Optional[int] = None,
|
|
746
|
-
entry_points: list[int] = [],
|
|
747
|
-
reached_attack_steps: list[int] = []
|
|
748
|
-
):
|
|
749
|
-
"""Add an attacker to the graph
|
|
750
|
-
Arguments:
|
|
751
|
-
attacker - the attacker to add
|
|
752
|
-
attacker_id - the id to assign to this attacker, usually
|
|
753
|
-
used when loading an attack graph from a
|
|
754
|
-
file
|
|
755
|
-
entry_points - list of attack step ids that serve as entry
|
|
756
|
-
points for the attacker
|
|
757
|
-
reached_attack_steps - list of ids of the attack steps that the
|
|
758
|
-
attacker has reached
|
|
759
|
-
"""
|
|
760
|
-
|
|
761
|
-
if logger.isEnabledFor(logging.DEBUG):
|
|
762
|
-
# Avoid running json.dumps when not in debug
|
|
763
|
-
if attacker_id is not None:
|
|
764
|
-
logger.debug('Add attacker "%s" with id:%d.',
|
|
765
|
-
attacker.name,
|
|
766
|
-
attacker_id
|
|
767
|
-
)
|
|
768
|
-
else:
|
|
769
|
-
logger.debug('Add attacker "%s" without id.',
|
|
770
|
-
attacker.name
|
|
771
|
-
)
|
|
772
|
-
|
|
773
|
-
attacker.id = attacker_id or self.next_attacker_id
|
|
774
|
-
if attacker.id in self.attackers:
|
|
775
|
-
raise ValueError(f'Attacker index {attacker_id} already in use.')
|
|
776
|
-
|
|
777
|
-
self.next_attacker_id = max(attacker.id + 1, self.next_attacker_id)
|
|
778
|
-
for node_id in reached_attack_steps:
|
|
779
|
-
node = self.nodes[node_id]
|
|
780
|
-
if node:
|
|
781
|
-
attacker.compromise(node)
|
|
782
|
-
else:
|
|
783
|
-
msg = ("Could not find node with id %d"
|
|
784
|
-
"in reached attack steps.")
|
|
785
|
-
logger.error(msg, node_id)
|
|
786
|
-
raise AttackGraphException(msg % node_id)
|
|
787
|
-
for node_id in entry_points:
|
|
788
|
-
node = self.nodes[node_id]
|
|
789
|
-
if node:
|
|
790
|
-
attacker.entry_points.add(node)
|
|
791
|
-
else:
|
|
792
|
-
msg = ("Could not find node with id %d"
|
|
793
|
-
"in attacker entrypoints.")
|
|
794
|
-
logger.error(msg, node_id)
|
|
795
|
-
raise AttackGraphException(msg % node_id)
|
|
796
|
-
self.attackers[attacker.id] = attacker
|
|
797
|
-
|
|
798
|
-
def remove_attacker(self, attacker: Attacker):
|
|
799
|
-
"""Remove attacker from attack graph
|
|
800
|
-
Arguments:
|
|
801
|
-
attacker - the attacker we wish to remove from the attack graph
|
|
802
|
-
"""
|
|
803
|
-
if logger.isEnabledFor(logging.DEBUG):
|
|
804
|
-
# Avoid running json.dumps when not in debug
|
|
805
|
-
logger.debug(
|
|
806
|
-
'Remove attacker "%s" with id:%d.',
|
|
807
|
-
attacker.name, attacker.id
|
|
808
|
-
)
|
|
809
|
-
|
|
810
|
-
# Copy set - we can not remove elements from a set we are looping over
|
|
811
|
-
nodes_to_uncompromise = set(attacker.reached_attack_steps)
|
|
812
|
-
for node in nodes_to_uncompromise:
|
|
813
|
-
attacker.undo_compromise(node)
|
|
814
|
-
|
|
815
|
-
if not isinstance(attacker.id, int):
|
|
816
|
-
raise ValueError(f'Invalid attacker id: {attacker.id}')
|
|
817
|
-
del self.attackers[attacker.id]
|