mal-toolbox 1.0.5__tar.gz → 1.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mal_toolbox-1.0.5/mal_toolbox.egg-info → mal_toolbox-1.0.7}/PKG-INFO +46 -18
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/README.md +45 -17
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7/mal_toolbox.egg-info}/PKG-INFO +46 -18
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/mal_toolbox.egg-info/SOURCES.txt +3 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/__init__.py +5 -2
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/__main__.py +20 -5
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/attackgraph/attackgraph.py +10 -5
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/attackgraph/node.py +19 -9
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/language/languagegraph.py +12 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/model.py +24 -1
- mal_toolbox-1.0.7/maltoolbox/visualization/__init__.py +11 -0
- mal_toolbox-1.0.7/maltoolbox/visualization/draw_io_utils.py +317 -0
- mal_toolbox-1.0.7/maltoolbox/visualization/neo4j_utils.py +117 -0
- mal_toolbox-1.0.7/maltoolbox/visualization/utils.py +41 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/pyproject.toml +1 -1
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/tests/test_model.py +1 -1
- mal_toolbox-1.0.5/maltoolbox/visualization/__init__.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/AUTHORS +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/LICENSE +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/mal_toolbox.egg-info/dependency_links.txt +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/mal_toolbox.egg-info/entry_points.txt +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/mal_toolbox.egg-info/requires.txt +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/mal_toolbox.egg-info/top_level.txt +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/attackgraph/__init__.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/attackgraph/analyzers/__init__.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/exceptions.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/file_utils.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/language/__init__.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/language/compiler/__init__.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/language/compiler/mal_lexer.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/language/compiler/mal_parser.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/patternfinder/__init__.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/patternfinder/attackgraph_patterns.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/py.typed +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/translators/__init__.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/translators/securicad.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/translators/updater.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/maltoolbox/visualization/graphviz_utils.py +0 -0
- {mal_toolbox-1.0.5 → mal_toolbox-1.0.7}/setup.cfg +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.7
|
|
4
4
|
Summary: A collection of tools used to create MAL models and attack graphs.
|
|
5
5
|
Author-email: Andrei Buhaiu <buhaiu@kth.se>, Joakim Loxdal <loxdal@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>, Giuseppe Nebbione <nebbione@kth.se>, Sandor Berglund <sandor@kth.se>
|
|
6
6
|
License: Apache Software License
|
|
@@ -34,21 +34,13 @@ MAL ([Meta Attack Language](https://mal-lang.org/)) models and attack graphs.
|
|
|
34
34
|
|
|
35
35
|
Attack graphs can be used to run simulations in [MAL Simulator](https://github.com/mal-lang/mal-simulator) or run your own custom analysis on.
|
|
36
36
|
|
|
37
|
-
[Documentation](https://mal-lang.org/mal-toolbox/index.html)
|
|
37
|
+
- [MAL Toolbox Documentation](https://mal-lang.org/mal-toolbox/index.html)
|
|
38
|
+
- [MAL Toolbox tutorial](https://github.com/mal-lang/mal-toolbox-tutorial)
|
|
38
39
|
|
|
39
40
|
## The Language Module
|
|
40
41
|
|
|
41
42
|
The language module provides various tools to process MAL languages.
|
|
42
43
|
|
|
43
|
-
### The Language Specification Submodule
|
|
44
|
-
|
|
45
|
-
The language specification submodule provides functions to load the
|
|
46
|
-
specification from a .mar archive(`load_language_specification_from_mar`) or a
|
|
47
|
-
JSON file(`load_language_specification_from_json`). This specification will
|
|
48
|
-
then be used to generate python classes representing the assets and
|
|
49
|
-
associations of the language and to determine the attack steps for each asset
|
|
50
|
-
when generating the attack graph.
|
|
51
|
-
|
|
52
44
|
## The Model Module
|
|
53
45
|
|
|
54
46
|
With a MAL language a Model (a MAL instance model) can be created either
|
|
@@ -72,12 +64,6 @@ nodes related and the asset field which will contain the object in the model
|
|
|
72
64
|
instance to which this attack step belongs to, if this information is
|
|
73
65
|
available.
|
|
74
66
|
|
|
75
|
-
## Ingestors Module
|
|
76
|
-
|
|
77
|
-
The ingestors module contains various tools that can make use of the instance
|
|
78
|
-
model or attack graph. Currently the Neo4J ingestor is the only one available
|
|
79
|
-
and it can be used to visualise the instance model and the attack graph.
|
|
80
|
-
|
|
81
67
|
|
|
82
68
|
# Usage
|
|
83
69
|
|
|
@@ -100,6 +86,11 @@ logging:
|
|
|
100
86
|
model_file: "logs/model.yml"
|
|
101
87
|
langspec_file: "logs/langspec_file.yml"
|
|
102
88
|
langgraph_file: "logs/langspec_file.yml"
|
|
89
|
+
neo4j:
|
|
90
|
+
uri: None
|
|
91
|
+
username: None
|
|
92
|
+
password: None
|
|
93
|
+
dbname: None
|
|
103
94
|
```
|
|
104
95
|
|
|
105
96
|
Alternatively, you can use the `MALTOOLBOX_CONFIG`
|
|
@@ -143,12 +134,49 @@ Options:
|
|
|
143
134
|
Notes:
|
|
144
135
|
- <lang_file> can be either a .mar file (generated by the older MAL
|
|
145
136
|
compiler) or a .mal file containing the DSL written in MAL.```
|
|
137
|
+
```
|
|
146
138
|
|
|
147
139
|
## Code examples / Tutorial
|
|
148
140
|
|
|
149
|
-
To find code examples and tutorials, visit the
|
|
141
|
+
To find more code examples and tutorials, visit the
|
|
150
142
|
[MAL Toolbox Tutorial](https://github.com/mal-lang/mal-toolbox-tutorial/tree/main) repository.
|
|
151
143
|
|
|
144
|
+
### Load a language
|
|
145
|
+
```python
|
|
146
|
+
|
|
147
|
+
from maltoolbox.language import LanguageGraph
|
|
148
|
+
|
|
149
|
+
# Will load the MAL language (.mal/.mar) or a saved language graph (yml/json)
|
|
150
|
+
lang_graph = LanguageGraph.load_from_file(lang_file_path)
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Generate a model
|
|
155
|
+
```python
|
|
156
|
+
from maltoolbox.model import Model
|
|
157
|
+
|
|
158
|
+
# Create an empty model
|
|
159
|
+
instance_model = Model("Example Model", lang_graph)
|
|
160
|
+
|
|
161
|
+
# Create and add assets of type supported by the MAL language
|
|
162
|
+
asset1 = instance_model.add_asset('Application', 'Application1')
|
|
163
|
+
asset2 = instance_model.add_asset('Application', 'Application2')
|
|
164
|
+
|
|
165
|
+
# Create association between the assets
|
|
166
|
+
asset1.add_associated_assets('appExecutedApps', asset2)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Generate an attack graph
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
|
|
173
|
+
from maltoolbox.attackgraph import AttackGraph
|
|
174
|
+
|
|
175
|
+
attack_graph = AttackGraph(lang_graph, model)
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
|
|
152
180
|
# Tests
|
|
153
181
|
There are unit tests inside of ./tests.
|
|
154
182
|
Before running the tests, make sure to install the requirements in ./tests/requirements.txt with `python -m pip install -r ./tests/requirements.txt`.
|
|
@@ -5,21 +5,13 @@ MAL ([Meta Attack Language](https://mal-lang.org/)) models and attack graphs.
|
|
|
5
5
|
|
|
6
6
|
Attack graphs can be used to run simulations in [MAL Simulator](https://github.com/mal-lang/mal-simulator) or run your own custom analysis on.
|
|
7
7
|
|
|
8
|
-
[Documentation](https://mal-lang.org/mal-toolbox/index.html)
|
|
8
|
+
- [MAL Toolbox Documentation](https://mal-lang.org/mal-toolbox/index.html)
|
|
9
|
+
- [MAL Toolbox tutorial](https://github.com/mal-lang/mal-toolbox-tutorial)
|
|
9
10
|
|
|
10
11
|
## The Language Module
|
|
11
12
|
|
|
12
13
|
The language module provides various tools to process MAL languages.
|
|
13
14
|
|
|
14
|
-
### The Language Specification Submodule
|
|
15
|
-
|
|
16
|
-
The language specification submodule provides functions to load the
|
|
17
|
-
specification from a .mar archive(`load_language_specification_from_mar`) or a
|
|
18
|
-
JSON file(`load_language_specification_from_json`). This specification will
|
|
19
|
-
then be used to generate python classes representing the assets and
|
|
20
|
-
associations of the language and to determine the attack steps for each asset
|
|
21
|
-
when generating the attack graph.
|
|
22
|
-
|
|
23
15
|
## The Model Module
|
|
24
16
|
|
|
25
17
|
With a MAL language a Model (a MAL instance model) can be created either
|
|
@@ -43,12 +35,6 @@ nodes related and the asset field which will contain the object in the model
|
|
|
43
35
|
instance to which this attack step belongs to, if this information is
|
|
44
36
|
available.
|
|
45
37
|
|
|
46
|
-
## Ingestors Module
|
|
47
|
-
|
|
48
|
-
The ingestors module contains various tools that can make use of the instance
|
|
49
|
-
model or attack graph. Currently the Neo4J ingestor is the only one available
|
|
50
|
-
and it can be used to visualise the instance model and the attack graph.
|
|
51
|
-
|
|
52
38
|
|
|
53
39
|
# Usage
|
|
54
40
|
|
|
@@ -71,6 +57,11 @@ logging:
|
|
|
71
57
|
model_file: "logs/model.yml"
|
|
72
58
|
langspec_file: "logs/langspec_file.yml"
|
|
73
59
|
langgraph_file: "logs/langspec_file.yml"
|
|
60
|
+
neo4j:
|
|
61
|
+
uri: None
|
|
62
|
+
username: None
|
|
63
|
+
password: None
|
|
64
|
+
dbname: None
|
|
74
65
|
```
|
|
75
66
|
|
|
76
67
|
Alternatively, you can use the `MALTOOLBOX_CONFIG`
|
|
@@ -114,12 +105,49 @@ Options:
|
|
|
114
105
|
Notes:
|
|
115
106
|
- <lang_file> can be either a .mar file (generated by the older MAL
|
|
116
107
|
compiler) or a .mal file containing the DSL written in MAL.```
|
|
108
|
+
```
|
|
117
109
|
|
|
118
110
|
## Code examples / Tutorial
|
|
119
111
|
|
|
120
|
-
To find code examples and tutorials, visit the
|
|
112
|
+
To find more code examples and tutorials, visit the
|
|
121
113
|
[MAL Toolbox Tutorial](https://github.com/mal-lang/mal-toolbox-tutorial/tree/main) repository.
|
|
122
114
|
|
|
115
|
+
### Load a language
|
|
116
|
+
```python
|
|
117
|
+
|
|
118
|
+
from maltoolbox.language import LanguageGraph
|
|
119
|
+
|
|
120
|
+
# Will load the MAL language (.mal/.mar) or a saved language graph (yml/json)
|
|
121
|
+
lang_graph = LanguageGraph.load_from_file(lang_file_path)
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Generate a model
|
|
126
|
+
```python
|
|
127
|
+
from maltoolbox.model import Model
|
|
128
|
+
|
|
129
|
+
# Create an empty model
|
|
130
|
+
instance_model = Model("Example Model", lang_graph)
|
|
131
|
+
|
|
132
|
+
# Create and add assets of type supported by the MAL language
|
|
133
|
+
asset1 = instance_model.add_asset('Application', 'Application1')
|
|
134
|
+
asset2 = instance_model.add_asset('Application', 'Application2')
|
|
135
|
+
|
|
136
|
+
# Create association between the assets
|
|
137
|
+
asset1.add_associated_assets('appExecutedApps', asset2)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Generate an attack graph
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
|
|
144
|
+
from maltoolbox.attackgraph import AttackGraph
|
|
145
|
+
|
|
146
|
+
attack_graph = AttackGraph(lang_graph, model)
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
|
|
123
151
|
# Tests
|
|
124
152
|
There are unit tests inside of ./tests.
|
|
125
153
|
Before running the tests, make sure to install the requirements in ./tests/requirements.txt with `python -m pip install -r ./tests/requirements.txt`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mal-toolbox
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.7
|
|
4
4
|
Summary: A collection of tools used to create MAL models and attack graphs.
|
|
5
5
|
Author-email: Andrei Buhaiu <buhaiu@kth.se>, Joakim Loxdal <loxdal@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>, Giuseppe Nebbione <nebbione@kth.se>, Sandor Berglund <sandor@kth.se>
|
|
6
6
|
License: Apache Software License
|
|
@@ -34,21 +34,13 @@ MAL ([Meta Attack Language](https://mal-lang.org/)) models and attack graphs.
|
|
|
34
34
|
|
|
35
35
|
Attack graphs can be used to run simulations in [MAL Simulator](https://github.com/mal-lang/mal-simulator) or run your own custom analysis on.
|
|
36
36
|
|
|
37
|
-
[Documentation](https://mal-lang.org/mal-toolbox/index.html)
|
|
37
|
+
- [MAL Toolbox Documentation](https://mal-lang.org/mal-toolbox/index.html)
|
|
38
|
+
- [MAL Toolbox tutorial](https://github.com/mal-lang/mal-toolbox-tutorial)
|
|
38
39
|
|
|
39
40
|
## The Language Module
|
|
40
41
|
|
|
41
42
|
The language module provides various tools to process MAL languages.
|
|
42
43
|
|
|
43
|
-
### The Language Specification Submodule
|
|
44
|
-
|
|
45
|
-
The language specification submodule provides functions to load the
|
|
46
|
-
specification from a .mar archive(`load_language_specification_from_mar`) or a
|
|
47
|
-
JSON file(`load_language_specification_from_json`). This specification will
|
|
48
|
-
then be used to generate python classes representing the assets and
|
|
49
|
-
associations of the language and to determine the attack steps for each asset
|
|
50
|
-
when generating the attack graph.
|
|
51
|
-
|
|
52
44
|
## The Model Module
|
|
53
45
|
|
|
54
46
|
With a MAL language a Model (a MAL instance model) can be created either
|
|
@@ -72,12 +64,6 @@ nodes related and the asset field which will contain the object in the model
|
|
|
72
64
|
instance to which this attack step belongs to, if this information is
|
|
73
65
|
available.
|
|
74
66
|
|
|
75
|
-
## Ingestors Module
|
|
76
|
-
|
|
77
|
-
The ingestors module contains various tools that can make use of the instance
|
|
78
|
-
model or attack graph. Currently the Neo4J ingestor is the only one available
|
|
79
|
-
and it can be used to visualise the instance model and the attack graph.
|
|
80
|
-
|
|
81
67
|
|
|
82
68
|
# Usage
|
|
83
69
|
|
|
@@ -100,6 +86,11 @@ logging:
|
|
|
100
86
|
model_file: "logs/model.yml"
|
|
101
87
|
langspec_file: "logs/langspec_file.yml"
|
|
102
88
|
langgraph_file: "logs/langspec_file.yml"
|
|
89
|
+
neo4j:
|
|
90
|
+
uri: None
|
|
91
|
+
username: None
|
|
92
|
+
password: None
|
|
93
|
+
dbname: None
|
|
103
94
|
```
|
|
104
95
|
|
|
105
96
|
Alternatively, you can use the `MALTOOLBOX_CONFIG`
|
|
@@ -143,12 +134,49 @@ Options:
|
|
|
143
134
|
Notes:
|
|
144
135
|
- <lang_file> can be either a .mar file (generated by the older MAL
|
|
145
136
|
compiler) or a .mal file containing the DSL written in MAL.```
|
|
137
|
+
```
|
|
146
138
|
|
|
147
139
|
## Code examples / Tutorial
|
|
148
140
|
|
|
149
|
-
To find code examples and tutorials, visit the
|
|
141
|
+
To find more code examples and tutorials, visit the
|
|
150
142
|
[MAL Toolbox Tutorial](https://github.com/mal-lang/mal-toolbox-tutorial/tree/main) repository.
|
|
151
143
|
|
|
144
|
+
### Load a language
|
|
145
|
+
```python
|
|
146
|
+
|
|
147
|
+
from maltoolbox.language import LanguageGraph
|
|
148
|
+
|
|
149
|
+
# Will load the MAL language (.mal/.mar) or a saved language graph (yml/json)
|
|
150
|
+
lang_graph = LanguageGraph.load_from_file(lang_file_path)
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Generate a model
|
|
155
|
+
```python
|
|
156
|
+
from maltoolbox.model import Model
|
|
157
|
+
|
|
158
|
+
# Create an empty model
|
|
159
|
+
instance_model = Model("Example Model", lang_graph)
|
|
160
|
+
|
|
161
|
+
# Create and add assets of type supported by the MAL language
|
|
162
|
+
asset1 = instance_model.add_asset('Application', 'Application1')
|
|
163
|
+
asset2 = instance_model.add_asset('Application', 'Application2')
|
|
164
|
+
|
|
165
|
+
# Create association between the assets
|
|
166
|
+
asset1.add_associated_assets('appExecutedApps', asset2)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Generate an attack graph
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
|
|
173
|
+
from maltoolbox.attackgraph import AttackGraph
|
|
174
|
+
|
|
175
|
+
attack_graph = AttackGraph(lang_graph, model)
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
|
|
152
180
|
# Tests
|
|
153
181
|
There are unit tests inside of ./tests.
|
|
154
182
|
Before running the tests, make sure to install the requirements in ./tests/requirements.txt with `python -m pip install -r ./tests/requirements.txt`.
|
|
@@ -29,5 +29,8 @@ maltoolbox/translators/__init__.py
|
|
|
29
29
|
maltoolbox/translators/securicad.py
|
|
30
30
|
maltoolbox/translators/updater.py
|
|
31
31
|
maltoolbox/visualization/__init__.py
|
|
32
|
+
maltoolbox/visualization/draw_io_utils.py
|
|
32
33
|
maltoolbox/visualization/graphviz_utils.py
|
|
34
|
+
maltoolbox/visualization/neo4j_utils.py
|
|
35
|
+
maltoolbox/visualization/utils.py
|
|
33
36
|
tests/test_model.py
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# MAL Toolbox v1.0.
|
|
2
|
+
# MAL Toolbox v1.0.7
|
|
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.7"
|
|
25
25
|
__authors__ = [
|
|
26
26
|
"Andrei Buhaiu",
|
|
27
27
|
"Giuseppe Nebbione",
|
|
@@ -48,6 +48,7 @@ config: dict[str, Any] = {
|
|
|
48
48
|
"langspec_file": "logs/langspec_file.json",
|
|
49
49
|
"langgraph_file": "logs/langgraph.yml",
|
|
50
50
|
},
|
|
51
|
+
"neo4j": {"uri": None, "username": None, "password": None, "dbname": None},
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
config_file = os.getenv("MALTOOLBOX_CONFIG", "maltoolbox.yml")
|
|
@@ -59,6 +60,8 @@ if os.path.exists(config_file):
|
|
|
59
60
|
log_configs = config['logging']
|
|
60
61
|
os.makedirs(os.path.dirname(log_configs["log_file"]), exist_ok=True)
|
|
61
62
|
|
|
63
|
+
neo4j_configs = config['logging']
|
|
64
|
+
|
|
62
65
|
formatter = logging.Formatter(
|
|
63
66
|
"%(asctime)s %(name)-12s %(levelname)-8s %(message)s", datefmt="%m-%d %H:%M"
|
|
64
67
|
)
|
|
@@ -3,9 +3,9 @@ Command-line interface for MAL toolbox operations
|
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
5
|
maltoolbox compile <lang_file> <output_file>
|
|
6
|
-
maltoolbox generate-attack-graph [--graphviz] <model_file> <lang_file>
|
|
6
|
+
maltoolbox generate-attack-graph [--graphviz] [--neo4j] <model_file> <lang_file>
|
|
7
7
|
maltoolbox upgrade-model <model_file> <lang_file> <output_file>
|
|
8
|
-
maltoolbox visualize-model <model_file> <lang_file>
|
|
8
|
+
maltoolbox visualize-model [--neo4j] [--graphviz] [-drawio] <model_file> <lang_file>
|
|
9
9
|
|
|
10
10
|
Arguments:
|
|
11
11
|
<model_file> Path to JSON instance model file.
|
|
@@ -15,6 +15,8 @@ Arguments:
|
|
|
15
15
|
Options:
|
|
16
16
|
-h --help Show this screen.
|
|
17
17
|
-g --graphviz Visualize with graphviz
|
|
18
|
+
-n --neo4j Send to neo4j
|
|
19
|
+
-d --drawio Export draw.io file
|
|
18
20
|
|
|
19
21
|
Notes:
|
|
20
22
|
- <lang_file> can be either a .mar file (generated by the older MAL
|
|
@@ -25,12 +27,18 @@ import logging
|
|
|
25
27
|
import json
|
|
26
28
|
import docopt
|
|
27
29
|
|
|
28
|
-
from . import log_configs
|
|
30
|
+
from . import log_configs, neo4j_configs
|
|
29
31
|
from .attackgraph import create_attack_graph, AttackGraph
|
|
30
32
|
from .language.compiler import MalCompiler
|
|
31
33
|
from .language.languagegraph import LanguageGraph
|
|
32
34
|
from .translators.updater import load_model_from_older_version
|
|
33
|
-
from .visualization
|
|
35
|
+
from .visualization import (
|
|
36
|
+
render_model,
|
|
37
|
+
render_attack_graph,
|
|
38
|
+
ingest_model_neo4j,
|
|
39
|
+
ingest_attack_graph_neo4j,
|
|
40
|
+
create_drawio_file_with_images
|
|
41
|
+
)
|
|
34
42
|
from .model import Model
|
|
35
43
|
|
|
36
44
|
logger = logging.getLogger(__name__)
|
|
@@ -78,6 +86,8 @@ def main():
|
|
|
78
86
|
)
|
|
79
87
|
if args['--graphviz']:
|
|
80
88
|
render_attack_graph(attack_graph)
|
|
89
|
+
if args['--neo4j']:
|
|
90
|
+
ingest_attack_graph_neo4j(attack_graph, neo4j_configs)
|
|
81
91
|
|
|
82
92
|
elif args['compile']:
|
|
83
93
|
compile(
|
|
@@ -90,7 +100,12 @@ def main():
|
|
|
90
100
|
elif args['visualize-model']:
|
|
91
101
|
lang_graph = LanguageGraph.load_from_file(args['<lang_file>'])
|
|
92
102
|
model = Model.load_from_file(args['<model_file>'], lang_graph)
|
|
93
|
-
|
|
103
|
+
if args['--graphviz']:
|
|
104
|
+
render_model(model)
|
|
105
|
+
if args['--neo4j']:
|
|
106
|
+
ingest_model_neo4j(model, neo4j_configs)
|
|
107
|
+
if args['--drawio']:
|
|
108
|
+
create_drawio_file_with_images(model)
|
|
94
109
|
|
|
95
110
|
if __name__ == "__main__":
|
|
96
111
|
main()
|
|
@@ -167,10 +167,10 @@ class AttackGraph():
|
|
|
167
167
|
|
|
168
168
|
attack_graph = AttackGraph(lang_graph)
|
|
169
169
|
attack_graph.model = model
|
|
170
|
-
serialized_attack_steps = serialized_object['attack_steps']
|
|
170
|
+
serialized_attack_steps: dict[str, dict] = serialized_object['attack_steps']
|
|
171
171
|
|
|
172
172
|
# Create all of the nodes in the imported attack graph.
|
|
173
|
-
for node_dict in serialized_attack_steps.
|
|
173
|
+
for node_full_name, node_dict in serialized_attack_steps.items():
|
|
174
174
|
|
|
175
175
|
# Recreate asset links if model is available.
|
|
176
176
|
node_asset = None
|
|
@@ -195,7 +195,10 @@ class AttackGraph():
|
|
|
195
195
|
existence_status = (
|
|
196
196
|
bool(node_dict['existence_status'])
|
|
197
197
|
if 'existence_status' in node_dict else None
|
|
198
|
-
)
|
|
198
|
+
),
|
|
199
|
+
# Give explicit full name if model is missing, otherwise
|
|
200
|
+
# it will generate automatically in node.full_name
|
|
201
|
+
full_name=node_full_name if not model else None
|
|
199
202
|
)
|
|
200
203
|
ag_node.tags = list(node_dict.get('tags', []))
|
|
201
204
|
ag_node.extras = node_dict.get('extras', {})
|
|
@@ -611,7 +614,8 @@ class AttackGraph():
|
|
|
611
614
|
node_id: Optional[int] = None,
|
|
612
615
|
model_asset: Optional[ModelAsset] = None,
|
|
613
616
|
ttc_dist: Optional[dict] = None,
|
|
614
|
-
existence_status: Optional[bool] = None
|
|
617
|
+
existence_status: Optional[bool] = None,
|
|
618
|
+
full_name: Optional[str] = None
|
|
615
619
|
) -> AttackGraphNode:
|
|
616
620
|
"""Create and add a node to the graph
|
|
617
621
|
Arguments:
|
|
@@ -656,7 +660,8 @@ class AttackGraph():
|
|
|
656
660
|
lg_attack_step = lg_attack_step,
|
|
657
661
|
model_asset = model_asset,
|
|
658
662
|
ttc_dist = ttc_dist,
|
|
659
|
-
existence_status = existence_status
|
|
663
|
+
existence_status = existence_status,
|
|
664
|
+
full_name = full_name
|
|
660
665
|
)
|
|
661
666
|
|
|
662
667
|
self.nodes[node_id] = node
|
|
@@ -21,7 +21,8 @@ class AttackGraphNode:
|
|
|
21
21
|
lg_attack_step: LanguageGraphAttackStep,
|
|
22
22
|
model_asset: Optional[ModelAsset] = None,
|
|
23
23
|
ttc_dist: Optional[dict] = None,
|
|
24
|
-
existence_status: Optional[bool] = None
|
|
24
|
+
existence_status: Optional[bool] = None,
|
|
25
|
+
full_name: Optional[str] = None
|
|
25
26
|
):
|
|
26
27
|
self.lg_attack_step = lg_attack_step
|
|
27
28
|
self.name = lg_attack_step.name
|
|
@@ -30,6 +31,7 @@ class AttackGraphNode:
|
|
|
30
31
|
self.tags = lg_attack_step.tags
|
|
31
32
|
self.detectors = lg_attack_step.detectors
|
|
32
33
|
|
|
34
|
+
self._full_name = full_name
|
|
33
35
|
self.id = node_id
|
|
34
36
|
self.model_asset = model_asset
|
|
35
37
|
self.existence_status = existence_status
|
|
@@ -45,10 +47,12 @@ class AttackGraphNode:
|
|
|
45
47
|
'lang_graph_attack_step': self.lg_attack_step.full_name,
|
|
46
48
|
'name': self.name,
|
|
47
49
|
'ttc': self.ttc,
|
|
48
|
-
'children': {
|
|
49
|
-
self.children
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
'children': {
|
|
51
|
+
child.id: child.full_name for child in self.children
|
|
52
|
+
},
|
|
53
|
+
'parents': {
|
|
54
|
+
parent.id: parent.full_name for parent in self.parents
|
|
55
|
+
},
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
for detector in self.detectors.values():
|
|
@@ -105,13 +109,19 @@ class AttackGraphNode:
|
|
|
105
109
|
@property
|
|
106
110
|
def full_name(self) -> str:
|
|
107
111
|
"""
|
|
108
|
-
Return the full name of the attack step. This is a
|
|
109
|
-
asset name to which the attack step
|
|
110
|
-
itself
|
|
112
|
+
Return the full name of the attack step. This is normally a
|
|
113
|
+
combination of the asset name to which the attack step
|
|
114
|
+
belongs and attack step name itself, but can also be
|
|
115
|
+
explicitly set or a combination of the step id and step name.
|
|
111
116
|
"""
|
|
112
|
-
if self.
|
|
117
|
+
if self._full_name:
|
|
118
|
+
# Explicitly set
|
|
119
|
+
return self._full_name
|
|
120
|
+
elif self.model_asset:
|
|
121
|
+
# Inherited from model asset
|
|
113
122
|
full_name = self.model_asset.name + ':' + self.name
|
|
114
123
|
else:
|
|
124
|
+
# Fallback: use ID
|
|
115
125
|
full_name = str(self.id) + ':' + self.name
|
|
116
126
|
return full_name
|
|
117
127
|
|
|
@@ -171,6 +171,18 @@ class LanguageGraphAsset:
|
|
|
171
171
|
current_asset = current_asset.own_super_asset
|
|
172
172
|
return superassets
|
|
173
173
|
|
|
174
|
+
def associations_to(
|
|
175
|
+
self, asset_type: LanguageGraphAsset
|
|
176
|
+
) -> dict[str, LanguageGraphAssociation]:
|
|
177
|
+
"""
|
|
178
|
+
Return dict of association types that go from self
|
|
179
|
+
to given `asset_type`
|
|
180
|
+
"""
|
|
181
|
+
associations_to_asset_type = {}
|
|
182
|
+
for fieldname, association in self.associations.items():
|
|
183
|
+
if association in asset_type.associations.values():
|
|
184
|
+
associations_to_asset_type[fieldname] = association
|
|
185
|
+
return associations_to_asset_type
|
|
174
186
|
|
|
175
187
|
@cached_property
|
|
176
188
|
def associations(self) -> dict[str, LanguageGraphAssociation]:
|
|
@@ -101,6 +101,12 @@ class Model():
|
|
|
101
101
|
' and we do not allow duplicates.'
|
|
102
102
|
)
|
|
103
103
|
|
|
104
|
+
if asset_type not in self.lang_graph.assets:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f'Asset type "{asset_type}" does not exist in language, '
|
|
107
|
+
'must be one of:\n -' +
|
|
108
|
+
'\n -'.join(self.lang_graph.assets.keys())
|
|
109
|
+
)
|
|
104
110
|
lg_asset = self.lang_graph.assets[asset_type]
|
|
105
111
|
|
|
106
112
|
asset = ModelAsset(
|
|
@@ -184,8 +190,11 @@ class Model():
|
|
|
184
190
|
)
|
|
185
191
|
return self._name_to_asset.get(asset_name, None)
|
|
186
192
|
|
|
187
|
-
|
|
188
193
|
def _to_dict(self) -> dict:
|
|
194
|
+
"""Backwards compatible"""
|
|
195
|
+
return self.to_dict()
|
|
196
|
+
|
|
197
|
+
def to_dict(self) -> dict:
|
|
189
198
|
"""Get dictionary representation of the model."""
|
|
190
199
|
logger.debug('Translating model to dict.')
|
|
191
200
|
contents: dict[str, Any] = {
|
|
@@ -446,6 +455,20 @@ class ModelAsset:
|
|
|
446
455
|
assets dictionary entry corresponding to the given fieldname.
|
|
447
456
|
"""
|
|
448
457
|
|
|
458
|
+
if fieldname not in self.lg_asset.associations:
|
|
459
|
+
if assets:
|
|
460
|
+
to_asset_type = next(iter(assets)).lg_asset
|
|
461
|
+
possible_associations = self.lg_asset.associations_to(to_asset_type)
|
|
462
|
+
else:
|
|
463
|
+
to_asset_type = None
|
|
464
|
+
possible_associations = self.lg_asset.associations
|
|
465
|
+
raise ValueError(
|
|
466
|
+
f'Association fieldname "{fieldname}" does not exist from '
|
|
467
|
+
f'<{self.lg_asset.name}> to <{to_asset_type.name if to_asset_type else "Any"}>'
|
|
468
|
+
', must be one of:\n -' +
|
|
469
|
+
'\n -'.join([a for a in possible_associations])
|
|
470
|
+
)
|
|
471
|
+
|
|
449
472
|
lg_assoc = self.lg_asset.associations[fieldname]
|
|
450
473
|
other_fieldname = lg_assoc.get_opposite_fieldname(fieldname)
|
|
451
474
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .graphviz_utils import render_attack_graph, render_model
|
|
2
|
+
from .neo4j_utils import ingest_attack_graph_neo4j, ingest_model_neo4j
|
|
3
|
+
from .draw_io_utils import create_drawio_file_with_images
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
'render_attack_graph',
|
|
7
|
+
'render_model',
|
|
8
|
+
'ingest_attack_graph_neo4j',
|
|
9
|
+
'ingest_model_neo4j',
|
|
10
|
+
'create_drawio_file_with_images'
|
|
11
|
+
]
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""DrawIO exporter made by Sandor"""
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
from xml.dom import minidom
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
from maltoolbox.model import Model
|
|
7
|
+
|
|
8
|
+
from .utils import position_assets
|
|
9
|
+
|
|
10
|
+
type2iconURL = {
|
|
11
|
+
"Hardware": "https://uxwing.com/wp-content/themes/uxwing/download/domain-hosting/server-rack-outline-icon.png",
|
|
12
|
+
"SoftwareProduct": "https://uxwing.com/wp-content/themes/uxwing/download/logistics-shipping-delivery/packing-icon.png",
|
|
13
|
+
"Application": "https://uxwing.com/wp-content/themes/uxwing/download/web-app-development/coding-icon.png",
|
|
14
|
+
"IDPS": "https://uxwing.com/wp-content/themes/uxwing/download/web-app-development/web-page-source-code-icon.png",
|
|
15
|
+
"PhysicalZone": "https://uxwing.com/wp-content/themes/uxwing/download/location-travel-map/address-location-icon.png",
|
|
16
|
+
"Information": "https://uxwing.com/wp-content/themes/uxwing/download/web-app-development/more-info-icon.png",
|
|
17
|
+
"Data": "https://uxwing.com/wp-content/themes/uxwing/download/web-app-development/database-line-icon.png",
|
|
18
|
+
"IAMObject": "https://uxwing.com/wp-content/themes/uxwing/download/communication-chat-call/name-id-icon.png",
|
|
19
|
+
"Identity": "https://uxwing.com/wp-content/themes/uxwing/download/communication-chat-call/name-id-icon.png",
|
|
20
|
+
"Privileges": "https://uxwing.com/wp-content/themes/uxwing/download/banking-finance/access-hand-key-icon.png",
|
|
21
|
+
"Group": "https://uxwing.com/wp-content/themes/uxwing/download/business-professional-services/team-icon.png",
|
|
22
|
+
"Credentials": "https://uxwing.com/wp-content/themes/uxwing/download/household-and-furniture/key-line-icon.png",
|
|
23
|
+
"User": "https://uxwing.com/wp-content/themes/uxwing/download/peoples-avatars/unisex-male-and-female-icon.png",
|
|
24
|
+
"Network": "https://uxwing.com/wp-content/themes/uxwing/download/internet-network-technology/big-data-icon.png",
|
|
25
|
+
"RoutingFirewall": "https://uxwing.com/wp-content/themes/uxwing/download/internet-network-technology/encryption-icon.png",
|
|
26
|
+
"ConnectionRule": "https://uxwing.com/wp-content/themes/uxwing/download/internet-network-technology/pc-network-icon.png",
|
|
27
|
+
"Vulnerability": "https://uxwing.com/wp-content/themes/uxwing/download/web-app-development/cyber-security-icon.png",
|
|
28
|
+
"SoftwareVulnerability": "https://uxwing.com/wp-content/themes/uxwing/download/web-app-development/cyber-security-icon.png",
|
|
29
|
+
"HardwareVulnerability": "https://uxwing.com/wp-content/themes/uxwing/download/crime-security-military-law/shield-sedo-line-icon.png",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def create_drawio_file_with_images(
|
|
33
|
+
model: Model,
|
|
34
|
+
show_edge_labels=True,
|
|
35
|
+
line_thickness=2,
|
|
36
|
+
coordinate_scale=0.75,
|
|
37
|
+
output_filename=None
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Create a draw.io file with all model assets as boxes using their actual positions and images
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
model: The model containing assets and associations
|
|
44
|
+
output_filename: Name of the output draw.io file
|
|
45
|
+
show_edge_labels: If True, show association type as text on edges. If False, edges will have no labels.
|
|
46
|
+
line_thickness: Thickness of the edges in pixels (default: 2)
|
|
47
|
+
coordinate_scale: Scale factor for model coordinates (default: 1.0, use 0.5 for half size, 2.0 for double size)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
if not all(a.extras.get('position') for a in model.assets.values()):
|
|
51
|
+
# Give assets positions if not already set
|
|
52
|
+
position_assets(model)
|
|
53
|
+
|
|
54
|
+
output_filename = output_filename or (
|
|
55
|
+
(model.name or "model_assets_with_images") + ".drawio"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Use the type2iconURL mapping for image URLs
|
|
59
|
+
type_images = type2iconURL
|
|
60
|
+
|
|
61
|
+
# Create root element
|
|
62
|
+
root = ET.Element("mxfile")
|
|
63
|
+
root.set("host", "app.diagrams.net")
|
|
64
|
+
root.set("modified", "2024-01-01T00:00:00.000Z")
|
|
65
|
+
root.set("agent", "5.0")
|
|
66
|
+
root.set("version", "24.7.17")
|
|
67
|
+
root.set("et", "https://www.diagrams.net/")
|
|
68
|
+
root.set("type", "device")
|
|
69
|
+
|
|
70
|
+
# Create diagram element
|
|
71
|
+
diagram = ET.SubElement(root, "diagram")
|
|
72
|
+
diagram.set("id", "model-assets")
|
|
73
|
+
diagram.set("name", "Model Assets")
|
|
74
|
+
|
|
75
|
+
# Create mxGraphModel
|
|
76
|
+
mxgraph = ET.SubElement(diagram, "mxGraphModel")
|
|
77
|
+
mxgraph.set("dx", "1422")
|
|
78
|
+
mxgraph.set("dy", "754")
|
|
79
|
+
mxgraph.set("grid", "1")
|
|
80
|
+
mxgraph.set("gridSize", "10")
|
|
81
|
+
mxgraph.set("guides", "1")
|
|
82
|
+
mxgraph.set("tooltips", "1")
|
|
83
|
+
mxgraph.set("connect", "1")
|
|
84
|
+
mxgraph.set("arrows", "1")
|
|
85
|
+
mxgraph.set("fold", "1")
|
|
86
|
+
mxgraph.set("page", "1")
|
|
87
|
+
mxgraph.set("pageScale", "1")
|
|
88
|
+
mxgraph.set("pageWidth", "2000")
|
|
89
|
+
mxgraph.set("pageHeight", "1500")
|
|
90
|
+
mxgraph.set("math", "0")
|
|
91
|
+
mxgraph.set("shadow", "0")
|
|
92
|
+
|
|
93
|
+
# Create root cell
|
|
94
|
+
root_cell = ET.SubElement(mxgraph, "root")
|
|
95
|
+
|
|
96
|
+
# Create default parent
|
|
97
|
+
default_parent = ET.SubElement(root_cell, "mxCell")
|
|
98
|
+
default_parent.set("id", "0")
|
|
99
|
+
|
|
100
|
+
# Create parent for edges (back layer)
|
|
101
|
+
edges_parent = ET.SubElement(root_cell, "mxCell")
|
|
102
|
+
edges_parent.set("id", "1")
|
|
103
|
+
edges_parent.set("parent", "0")
|
|
104
|
+
|
|
105
|
+
# Create default parent for model (front layer)
|
|
106
|
+
model_parent = ET.SubElement(root_cell, "mxCell")
|
|
107
|
+
model_parent.set("id", "2")
|
|
108
|
+
model_parent.set("parent", "0")
|
|
109
|
+
|
|
110
|
+
# Get assets and use their actual positions
|
|
111
|
+
assets = list(model.assets.values())
|
|
112
|
+
|
|
113
|
+
# Box dimensions
|
|
114
|
+
box_width = 150
|
|
115
|
+
box_height = 80
|
|
116
|
+
|
|
117
|
+
# Find the bounds of all assets to center the diagram
|
|
118
|
+
min_x = min_y = float("inf")
|
|
119
|
+
max_x = max_y = float("-inf")
|
|
120
|
+
|
|
121
|
+
for asset in assets:
|
|
122
|
+
if asset.extras:
|
|
123
|
+
x = asset.extras.get("position", {}).get("x", 0)
|
|
124
|
+
y = asset.extras.get("position", {}).get("y", 0)
|
|
125
|
+
min_x = min(min_x, x)
|
|
126
|
+
min_y = min(min_y, y)
|
|
127
|
+
max_x = max(max_x, x)
|
|
128
|
+
max_y = max(max_y, y)
|
|
129
|
+
|
|
130
|
+
# If no positions found, use default bounds
|
|
131
|
+
if min_x == float("inf"):
|
|
132
|
+
min_x = min_y = 0
|
|
133
|
+
max_x = max_y = 1000
|
|
134
|
+
|
|
135
|
+
# Add some padding
|
|
136
|
+
padding = 100
|
|
137
|
+
min_x -= padding
|
|
138
|
+
min_y -= padding
|
|
139
|
+
max_x += padding
|
|
140
|
+
max_y += padding
|
|
141
|
+
|
|
142
|
+
# Create boxes for each asset using their actual positions
|
|
143
|
+
for asset in assets:
|
|
144
|
+
|
|
145
|
+
# Get position from extras, with fallback to grid layout
|
|
146
|
+
if hasattr(asset, "extras") and asset.extras and "position" in asset.extras:
|
|
147
|
+
x = round(asset.extras["position"].get("x", 0) * coordinate_scale)
|
|
148
|
+
y = round(asset.extras["position"].get("y", 0) * coordinate_scale)
|
|
149
|
+
else:
|
|
150
|
+
# Fallback to grid layout if no position data
|
|
151
|
+
i = list(model.assets.keys()).index(asset.id)
|
|
152
|
+
cols = math.ceil(math.sqrt(len(assets)))
|
|
153
|
+
row = i // cols
|
|
154
|
+
col = i % cols
|
|
155
|
+
x = round((50 + col * 200) * coordinate_scale)
|
|
156
|
+
y = round((50 + row * 120) * coordinate_scale)
|
|
157
|
+
|
|
158
|
+
# Get image URL for asset type
|
|
159
|
+
image_url = type_images.get(
|
|
160
|
+
asset.type,
|
|
161
|
+
"https://uxwing.com/wp-content/themes/uxwing/download/communication-chat-call/question-mark-icon.png",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Define colors for each asset type (matching the design)
|
|
165
|
+
type_colors = {
|
|
166
|
+
"Hardware": "#4CAF50", # Green
|
|
167
|
+
"Application": "#2196F3", # Blue
|
|
168
|
+
"Network": "#9C27B0", # Purple
|
|
169
|
+
"ConnectionRule": "#FF9800", # Orange
|
|
170
|
+
"Identity": "#607D8B", # Blue Grey
|
|
171
|
+
"Credentials": "#4CAF50", # Green
|
|
172
|
+
"SoftwareVulnerability": "#F44336", # Red
|
|
173
|
+
"Data": "#795548", # Brown
|
|
174
|
+
"User": "#00BCD4", # Cyan
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Get color for this asset type
|
|
178
|
+
top_color = type_colors.get(asset.type, "#4CAF50")
|
|
179
|
+
|
|
180
|
+
# Create the HTML content for the two-segment box
|
|
181
|
+
html_content = f"""
|
|
182
|
+
<div style="width: {box_width}px; height: {box_height}px; position: relative; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
|
183
|
+
<!-- Top segment with icon and type -->
|
|
184
|
+
<div style="background: linear-gradient(to bottom, {top_color}, {top_color}dd); height: 40%; display: flex; align-items: center; padding: 0 8px; position: relative;">
|
|
185
|
+
<img src="{image_url}" width="20" height="20" style="margin-right: 8px;"/>
|
|
186
|
+
<span style="color: white; font-weight: bold; font-size: 11px; flex: 1;">{asset.type}</span>
|
|
187
|
+
<div style="width: 8px; height: 8px; background: {top_color}; position: absolute; top: 4px; right: 4px; border-radius: 2px;"></div>
|
|
188
|
+
</div>
|
|
189
|
+
<!-- Bottom segment with asset name -->
|
|
190
|
+
<div style="background: #424242; height: 60%; display: flex; align-items: center; justify-content: center; padding: 0 8px;">
|
|
191
|
+
<span style="color: white; font-size: 10px; text-align: center; line-height: 1.2;">{asset.name}</span>
|
|
192
|
+
</div>
|
|
193
|
+
<!-- Connecting line -->
|
|
194
|
+
<div style="position: absolute; bottom: 40%; left: 0; width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-top: 8px solid {top_color};"></div>
|
|
195
|
+
</div>
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
# Create mxCell for the asset box
|
|
199
|
+
cell = ET.SubElement(root_cell, "mxCell")
|
|
200
|
+
cell.set("id", f"asset_{asset.id}")
|
|
201
|
+
cell.set("value", html_content)
|
|
202
|
+
cell.set(
|
|
203
|
+
"style",
|
|
204
|
+
"rounded=0;whiteSpace=wrap;html=1;overflow=fill;"
|
|
205
|
+
"align=center;verticalAlign=middle;spacing=0;"
|
|
206
|
+
"fillColor=none;strokeColor=none;fontSize=10;fontStyle=1;",
|
|
207
|
+
)
|
|
208
|
+
cell.set("vertex", "1")
|
|
209
|
+
cell.set("parent", "2") # Put assets in front layer
|
|
210
|
+
|
|
211
|
+
# Create geometry element
|
|
212
|
+
geometry = ET.SubElement(cell, "mxGeometry")
|
|
213
|
+
geometry.set("x", str(x))
|
|
214
|
+
geometry.set("y", str(y))
|
|
215
|
+
geometry.set("width", str(box_width))
|
|
216
|
+
geometry.set("height", str(box_height))
|
|
217
|
+
geometry.set("as", "geometry")
|
|
218
|
+
|
|
219
|
+
# Create edges for associations (avoiding duplicates)
|
|
220
|
+
edge_id = 1000 # Start edge IDs from 1000 to avoid conflicts with asset IDs
|
|
221
|
+
processed_edges = set() # Track processed edges to avoid duplicates
|
|
222
|
+
|
|
223
|
+
for asset in assets:
|
|
224
|
+
if hasattr(asset, "associated_assets") and asset.associated_assets:
|
|
225
|
+
for asset_assoc_field, associated_assets in asset.associated_assets.items():
|
|
226
|
+
association_type = asset.lg_asset.associations[asset_assoc_field].name
|
|
227
|
+
for assoc_asset in associated_assets:
|
|
228
|
+
# Create a unique edge identifier (sorted to handle bidirectional associations)
|
|
229
|
+
edge_key = tuple(sorted([asset.id, assoc_asset.id]))
|
|
230
|
+
|
|
231
|
+
# Skip if we've already processed this edge
|
|
232
|
+
if edge_key in processed_edges:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# Find the target asset
|
|
236
|
+
target_asset = None
|
|
237
|
+
for a in assets:
|
|
238
|
+
if a.id == assoc_asset.id:
|
|
239
|
+
target_asset = a
|
|
240
|
+
break
|
|
241
|
+
|
|
242
|
+
if target_asset:
|
|
243
|
+
# Mark this edge as processed
|
|
244
|
+
processed_edges.add(edge_key)
|
|
245
|
+
# Get positions for both assets
|
|
246
|
+
source_x = asset.extras.get("position", {}).get("x", 0) + box_width / 2
|
|
247
|
+
source_y = asset.extras.get("position", {}).get("y", 0) + box_height / 2
|
|
248
|
+
target_x = (
|
|
249
|
+
target_asset.extras.get("position", {}).get("x", 0) + box_width / 2
|
|
250
|
+
)
|
|
251
|
+
target_y = (
|
|
252
|
+
target_asset.extras.get("position", {}).get("y", 0) + box_height / 2
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Create edge cell
|
|
256
|
+
edge_cell = ET.SubElement(root_cell, "mxCell")
|
|
257
|
+
edge_cell.set("id", f"edge_{edge_id}")
|
|
258
|
+
edge_cell.set(
|
|
259
|
+
"value", association_type if show_edge_labels else ""
|
|
260
|
+
)
|
|
261
|
+
edge_cell.set(
|
|
262
|
+
"style",
|
|
263
|
+
f"edgeStyle=straightEdgeStyle;rounded=0;html=1;fontSize=10;fontStyle=1;endArrow=none;startArrow=none;strokeWidth={line_thickness};",
|
|
264
|
+
)
|
|
265
|
+
edge_cell.set("edge", "1")
|
|
266
|
+
edge_cell.set("parent", "1") # Put edges in back layer
|
|
267
|
+
edge_cell.set("source", f"asset_{asset.id}")
|
|
268
|
+
edge_cell.set("target", f"asset_{assoc_asset.id}")
|
|
269
|
+
|
|
270
|
+
# Create geometry for edge
|
|
271
|
+
edge_geometry = ET.SubElement(edge_cell, "mxGeometry")
|
|
272
|
+
edge_geometry.set("relative", "1")
|
|
273
|
+
edge_geometry.set("as", "geometry")
|
|
274
|
+
|
|
275
|
+
# Create array of points for the edge
|
|
276
|
+
array = ET.SubElement(edge_geometry, "Array")
|
|
277
|
+
array.set("as", "points")
|
|
278
|
+
|
|
279
|
+
# Add source point
|
|
280
|
+
point1 = ET.SubElement(array, "mxPoint")
|
|
281
|
+
point1.set("x", str(source_x))
|
|
282
|
+
point1.set("y", str(source_y))
|
|
283
|
+
point1.set("as", "sourcePoint")
|
|
284
|
+
|
|
285
|
+
# Add target point
|
|
286
|
+
point2 = ET.SubElement(array, "mxPoint")
|
|
287
|
+
point2.set("x", str(target_x))
|
|
288
|
+
point2.set("y", str(target_y))
|
|
289
|
+
point2.set("as", "targetPoint")
|
|
290
|
+
|
|
291
|
+
edge_id += 1
|
|
292
|
+
|
|
293
|
+
# Convert to pretty XML
|
|
294
|
+
rough_string = ET.tostring(root, "utf-8")
|
|
295
|
+
reparsed = minidom.parseString(rough_string)
|
|
296
|
+
pretty_xml = reparsed.toprettyxml(indent=" ")
|
|
297
|
+
|
|
298
|
+
# Remove empty lines
|
|
299
|
+
lines = [line for line in pretty_xml.split("\n") if line.strip()]
|
|
300
|
+
pretty_xml = "\n".join(lines)
|
|
301
|
+
|
|
302
|
+
# Write to file
|
|
303
|
+
with open(output_filename, "w", encoding="utf-8") as f:
|
|
304
|
+
f.write(pretty_xml)
|
|
305
|
+
|
|
306
|
+
print(f"Draw.io file with images created: {output_filename}")
|
|
307
|
+
print(f"Total assets: {len(assets)}")
|
|
308
|
+
print(f"Diagram bounds: x={min_x:.0f} to {max_x:.0f}, y={min_y:.0f} to {max_y:.0f}")
|
|
309
|
+
|
|
310
|
+
# Print asset summary
|
|
311
|
+
type_counts: dict[str, int] = {}
|
|
312
|
+
for asset in assets:
|
|
313
|
+
type_counts[asset.type] = type_counts.get(asset.type, 0) + 1
|
|
314
|
+
|
|
315
|
+
print("\nAsset type distribution:")
|
|
316
|
+
for asset_type, count in sorted(type_counts.items()):
|
|
317
|
+
print(f" {asset_type}: {count}")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MAL-Toolbox Neo4j Ingestor Module
|
|
3
|
+
"""
|
|
4
|
+
# mypy: ignore-errors
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
from py2neo import Graph, Node, Relationship, Subgraph
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
def ingest_attack_graph_neo4j(
|
|
14
|
+
graph,
|
|
15
|
+
neo4j_config: dict[str, Any],
|
|
16
|
+
delete: bool = True
|
|
17
|
+
) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Ingest an attack graph into a neo4j database
|
|
20
|
+
|
|
21
|
+
Arguments:
|
|
22
|
+
graph - the attackgraph provided by the atkgraph.py module.
|
|
23
|
+
uri - the URI to a running neo4j instance
|
|
24
|
+
username - the username to login on Neo4J
|
|
25
|
+
password - the password to login on Neo4J
|
|
26
|
+
dbname - the selected database
|
|
27
|
+
delete - if True, the previous content of the database is deleted
|
|
28
|
+
before ingesting the new attack graph
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
uri = neo4j_config.get('uri')
|
|
32
|
+
username = neo4j_config.get('username')
|
|
33
|
+
password = neo4j_config.get('password')
|
|
34
|
+
dbname = neo4j_config.get('dbname')
|
|
35
|
+
|
|
36
|
+
g = Graph(uri=uri, user=username, password=password, name=dbname)
|
|
37
|
+
if delete:
|
|
38
|
+
g.delete_all()
|
|
39
|
+
|
|
40
|
+
nodes = {}
|
|
41
|
+
rels = []
|
|
42
|
+
for node in graph.nodes.values():
|
|
43
|
+
node_dict = node.to_dict()
|
|
44
|
+
nodes[node.id] = Node(
|
|
45
|
+
node_dict['asset'] if 'asset' in node_dict else node_dict['id'],
|
|
46
|
+
name = node_dict['name'],
|
|
47
|
+
full_name = node.full_name,
|
|
48
|
+
type = node_dict['type'],
|
|
49
|
+
ttc = str(node_dict['ttc']),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
for node in graph.nodes.values():
|
|
54
|
+
for child in node.children:
|
|
55
|
+
rels.append(Relationship(nodes[node.id], nodes[child.id]))
|
|
56
|
+
|
|
57
|
+
subgraph = Subgraph(list(nodes.values()), rels)
|
|
58
|
+
|
|
59
|
+
tx = g.begin()
|
|
60
|
+
tx.create(subgraph)
|
|
61
|
+
g.commit(tx)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ingest_model_neo4j(
|
|
65
|
+
model,
|
|
66
|
+
neo4j_config: dict[str, Any],
|
|
67
|
+
delete: bool = True
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Ingest an instance model graph into a Neo4J database
|
|
71
|
+
|
|
72
|
+
Arguments:
|
|
73
|
+
model - the instance model dictionary as provided by the model.py module
|
|
74
|
+
uri - the URI to a running neo4j instance
|
|
75
|
+
username - the username to login on Neo4J
|
|
76
|
+
password - the password to login on Neo4J
|
|
77
|
+
dbname - the selected database
|
|
78
|
+
delete - if True, the previous content of the database is deleted
|
|
79
|
+
before ingesting the new attack graph
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
uri = neo4j_config.get('uri')
|
|
83
|
+
username = neo4j_config.get('username')
|
|
84
|
+
password = neo4j_config.get('password')
|
|
85
|
+
dbname = neo4j_config.get('dbname')
|
|
86
|
+
|
|
87
|
+
g = Graph(uri=uri, user=username, password=password, name=dbname)
|
|
88
|
+
if delete:
|
|
89
|
+
g.delete_all()
|
|
90
|
+
|
|
91
|
+
nodes = {}
|
|
92
|
+
rels = []
|
|
93
|
+
|
|
94
|
+
for asset in model.assets.values():
|
|
95
|
+
nodes[str(asset.id)] = Node(
|
|
96
|
+
str(asset.type),
|
|
97
|
+
name=str(asset.name),
|
|
98
|
+
asset_id=str(asset.id),
|
|
99
|
+
type=str(asset.type)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
for asset in model.assets.values():
|
|
103
|
+
for fieldname, other_assets in asset.associated_assets.items():
|
|
104
|
+
for other_asset in other_assets:
|
|
105
|
+
rels.append(
|
|
106
|
+
Relationship(
|
|
107
|
+
nodes[str(asset.id)],
|
|
108
|
+
str(fieldname),
|
|
109
|
+
nodes[str(other_asset.id)]
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
subgraph = Subgraph(list(nodes.values()), rels)
|
|
114
|
+
|
|
115
|
+
tx = g.begin()
|
|
116
|
+
tx.create(subgraph)
|
|
117
|
+
g.commit(tx)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from maltoolbox.model import Model
|
|
2
|
+
|
|
3
|
+
def position_assets(model: Model):
|
|
4
|
+
"""
|
|
5
|
+
Assigns (x, y) positions to assets in a graph where relations are stored
|
|
6
|
+
in asset.associated_assets[relation_name] = [related_assets...].
|
|
7
|
+
Positions are stored in asset.extras['position'] = {'x': ..., 'y': ...}.
|
|
8
|
+
Layout is computed by traversing connected components.
|
|
9
|
+
Adds uniform padding between assets.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
visited = set()
|
|
13
|
+
x_spacing = 200
|
|
14
|
+
y_spacing = 200
|
|
15
|
+
padding = 50 # uniform padding
|
|
16
|
+
|
|
17
|
+
def traverse(asset, depth, index, component):
|
|
18
|
+
if asset in visited:
|
|
19
|
+
return
|
|
20
|
+
visited.add(asset)
|
|
21
|
+
component.append((asset, depth, index))
|
|
22
|
+
neighbors = []
|
|
23
|
+
for rel_list in asset.associated_assets.values():
|
|
24
|
+
neighbors.extend(rel_list)
|
|
25
|
+
for i, neighbor in enumerate(neighbors):
|
|
26
|
+
if neighbor not in visited:
|
|
27
|
+
traverse(neighbor, depth + 1, i, component)
|
|
28
|
+
|
|
29
|
+
def assign_positions(component):
|
|
30
|
+
for i, (asset, depth, index) in enumerate(component):
|
|
31
|
+
x = index * (x_spacing + padding)
|
|
32
|
+
y = depth * (y_spacing + padding)
|
|
33
|
+
asset.extras.setdefault('position', {})
|
|
34
|
+
asset.extras['position']['x'] = x
|
|
35
|
+
asset.extras['position']['y'] = y
|
|
36
|
+
|
|
37
|
+
for asset in model.assets.values():
|
|
38
|
+
if asset not in visited:
|
|
39
|
+
component: list[tuple] = []
|
|
40
|
+
traverse(asset, 0, 0, component)
|
|
41
|
+
assign_positions(component)
|
|
@@ -167,7 +167,7 @@ def test_model_add_association_nonexisting_fieldname(model: Model):
|
|
|
167
167
|
data = model.add_asset(asset_type = 'Data')
|
|
168
168
|
|
|
169
169
|
# Try create an association between asset1 and data
|
|
170
|
-
with pytest.raises(
|
|
170
|
+
with pytest.raises(ValueError):
|
|
171
171
|
# will raise error because fieldname does not exist
|
|
172
172
|
asset1.add_associated_assets(
|
|
173
173
|
fieldname = 'unknownFieldName', assets = {data}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|