mal-toolbox 0.0.28__py3-none-any.whl → 0.1.12__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.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/METADATA +60 -28
- mal_toolbox-0.1.12.dist-info/RECORD +32 -0
- {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/WHEEL +1 -1
- maltoolbox/__init__.py +31 -31
- maltoolbox/__main__.py +80 -4
- maltoolbox/attackgraph/__init__.py +8 -0
- maltoolbox/attackgraph/analyzers/__init__.py +0 -0
- maltoolbox/attackgraph/analyzers/apriori.py +173 -27
- maltoolbox/attackgraph/attacker.py +84 -25
- maltoolbox/attackgraph/attackgraph.py +503 -215
- maltoolbox/attackgraph/node.py +92 -31
- maltoolbox/attackgraph/query.py +125 -19
- maltoolbox/default.conf +8 -7
- maltoolbox/exceptions.py +45 -0
- maltoolbox/file_utils.py +66 -0
- maltoolbox/ingestors/__init__.py +0 -0
- maltoolbox/ingestors/neo4j.py +95 -84
- maltoolbox/language/__init__.py +4 -0
- maltoolbox/language/classes_factory.py +145 -64
- maltoolbox/language/{lexer_parser/__main__.py → compiler/__init__.py} +5 -12
- maltoolbox/language/{lexer_parser → compiler}/mal_lexer.py +1 -1
- maltoolbox/language/{lexer_parser → compiler}/mal_parser.py +1 -1
- maltoolbox/language/{lexer_parser → compiler}/mal_visitor.py +4 -5
- maltoolbox/language/languagegraph.py +569 -168
- maltoolbox/model.py +858 -0
- maltoolbox/translators/__init__.py +0 -0
- maltoolbox/translators/securicad.py +76 -52
- maltoolbox/translators/updater.py +132 -0
- maltoolbox/wrappers.py +62 -0
- mal_toolbox-0.0.28.dist-info/RECORD +0 -26
- maltoolbox/cl_parser.py +0 -89
- maltoolbox/language/specification.py +0 -265
- maltoolbox/main.py +0 -84
- maltoolbox/model/model.py +0 -282
- {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/AUTHORS +0 -0
- {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/LICENSE +0 -0
- {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: mal-toolbox
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.12
|
|
4
4
|
Summary: A collection of tools used to create MAL models and attack graphs.
|
|
5
|
-
Author-email: Andrei Buhaiu <buhaiu@kth.se>, Giuseppe Nebbione <nebbione@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>
|
|
5
|
+
Author-email: Andrei Buhaiu <buhaiu@kth.se>, Giuseppe Nebbione <nebbione@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>, Joakim Loxdal <loxdal@kth.se>
|
|
6
6
|
License: Apache Software License
|
|
7
7
|
Project-URL: Homepage, https://github.com/mal-lang/mal-toolbox
|
|
8
8
|
Project-URL: Bug Tracker, https://github.com/mal-lang/mal-toolbox/issues
|
|
@@ -18,19 +18,28 @@ Requires-Python: >=3.10
|
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE
|
|
20
20
|
License-File: AUTHORS
|
|
21
|
-
Requires-Dist: py2neo
|
|
22
|
-
Requires-Dist: python-jsonschema-objects
|
|
21
|
+
Requires-Dist: py2neo>=2021.2.3
|
|
22
|
+
Requires-Dist: python-jsonschema-objects>=0.5.5
|
|
23
|
+
Requires-Dist: antlr4-tools
|
|
24
|
+
Requires-Dist: antlr4-python3-runtime
|
|
25
|
+
Requires-Dist: docopt
|
|
26
|
+
Requires-Dist: PyYAML
|
|
23
27
|
|
|
24
|
-
#
|
|
28
|
+
# MAL Toolbox overview
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
models and attack graphs.
|
|
30
|
+
MAL Toolbox is a collection of python modules to help developers create and work with
|
|
31
|
+
MAL ([Meta Attack Language](https://mal-lang.org/)) models and attack graphs.
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
Attack graphs can be used to run simulations (see MAL Simulator) or analysis.
|
|
34
|
+
MAL Toolbox also gives the ability to view the AttackGraph/Model graphically in neo4j.
|
|
35
|
+
|
|
36
|
+
[Documentation](https://mal-lang.org/mal-toolbox/index.html)(Work in progress)
|
|
37
|
+
|
|
38
|
+
## The Language Module
|
|
30
39
|
|
|
31
40
|
The language module provides various tools to process MAL languages.
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
### The Language Specification Submodule
|
|
34
43
|
|
|
35
44
|
The language specification submodule provides functions to load the
|
|
36
45
|
specification from a .mar archive(`load_language_specification_from_mar`) or a
|
|
@@ -39,7 +48,7 @@ then be used to generate python classes representing the assets and
|
|
|
39
48
|
associations of the language and to determine the attack steps for each asset
|
|
40
49
|
when generating the attack graph.
|
|
41
50
|
|
|
42
|
-
|
|
51
|
+
### The Language Classes Factory Submodule
|
|
43
52
|
|
|
44
53
|
The language classes factory submodule is used to generate python classes
|
|
45
54
|
using the `python_jsonschema_objects` package from a language specification.
|
|
@@ -50,19 +59,23 @@ using JSON Schema validators they will enforce their restrictions when using
|
|
|
50
59
|
the python objects created. These classes are typically used in conjunction
|
|
51
60
|
with model module to create instance models.
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
## The Model Module
|
|
63
|
+
|
|
64
|
+
With a MAL language a Model (a MAL instance model) can be created either
|
|
65
|
+
from a model file or empty.
|
|
66
|
+
|
|
67
|
+
The model class will store all of the relevant information to the MAL
|
|
68
|
+
instance model, most importantly the assets and associations that make it up.
|
|
69
|
+
|
|
70
|
+
Assets and associations are objects of classes created using the language
|
|
71
|
+
classes factory submodule in runtime. It also allows for `Attacker` objects
|
|
72
|
+
to be created and associated with attack steps on assets in the model.
|
|
73
|
+
The most relevant methods of the Model are the ones used to add different
|
|
74
|
+
elements to the model, `add_asset`, `add_association`, and `add_attacker`.
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
store all of the relevant information to the MAL instance model, most
|
|
57
|
-
importantly the assets and associations that make it up. These assets and
|
|
58
|
-
associations should be objects created using the language classes factory
|
|
59
|
-
submodule. It also allows for `Attacker` objects to be created and associated
|
|
60
|
-
with attack steps on assets in the model. The most relevant functions here are
|
|
61
|
-
the ones used to add different elements to the model, `add_asset`,
|
|
62
|
-
`add_association`, and `add_attacker`. Model objects can be used to generate
|
|
63
|
-
attack graphs using the attack graph module.
|
|
76
|
+
Model objects can be used to generate attack graphs with the AttackGraph module.
|
|
64
77
|
|
|
65
|
-
|
|
78
|
+
## The Attack Graph Module
|
|
66
79
|
|
|
67
80
|
The attack graph module contains tools used to generate attack graphs from
|
|
68
81
|
existing MAL instance models and analyse MAL attack graphs. The function used
|
|
@@ -80,14 +93,27 @@ resulting attack graph with the instance model given as a parameter in order
|
|
|
80
93
|
to create attack step nodes that represent the entry points of the attackers
|
|
81
94
|
and attach them to the attack steps specified in the instance model.
|
|
82
95
|
|
|
83
|
-
|
|
96
|
+
## Ingestors Module
|
|
84
97
|
|
|
85
98
|
The ingestors module contains various tools that can make use of the instance
|
|
86
99
|
model or attack graph. Currently the Neo4J ingestor is the only one available
|
|
87
100
|
and it can be used to visualise the instance model and the attack graph.
|
|
88
101
|
|
|
89
|
-
# Command Line Client
|
|
90
102
|
|
|
103
|
+
# Usage
|
|
104
|
+
|
|
105
|
+
## Installation
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
pip install mal-toolbox
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Configuration
|
|
112
|
+
A default configuration file `default.conf` can be found in the package
|
|
113
|
+
directory. This contains the default values to use for logging and can also be
|
|
114
|
+
used to store the information needed to access the local Neo4J instance.
|
|
115
|
+
|
|
116
|
+
## Command Line Client
|
|
91
117
|
In addition to the modules that make up the MAL-Toolbox package it also
|
|
92
118
|
provides a simple command line client that can be used to easily generate
|
|
93
119
|
attack graphs from a .mar language specification file and a JSON instance
|
|
@@ -99,7 +125,13 @@ The usage is: `maltoolbox gen_ag [--neo4j] <model_json_file>
|
|
|
99
125
|
If the `--neo4j` flag is specified the model and attack graph will be loaded
|
|
100
126
|
into a local Neo4J instance.
|
|
101
127
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
128
|
+
## Code examples / Tutorial
|
|
129
|
+
|
|
130
|
+
To find code examples and tutorials, visit the
|
|
131
|
+
[MAL Toolbox Tutorial](https://github.com/mal-lang/mal-toolbox-tutorial/tree/main) repository.
|
|
132
|
+
|
|
133
|
+
# Tests
|
|
134
|
+
There are unit tests inside of ./tests.
|
|
135
|
+
Before running the tests, make sure to install the requirements in ./tests/requirements.txt with `python -m pip install -r ./tests/requirements.txt`.
|
|
136
|
+
|
|
137
|
+
To run all tests, use the `pytest` command. To run just a specific file or test function use `pytest tests/<filename>` or `pytest -k <function_name>`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
maltoolbox/__init__.py,sha256=Gkuzd3_D5OscXXfTzYQf5Vdcmooe67qcYyScGAeFpcQ,2778
|
|
2
|
+
maltoolbox/__main__.py,sha256=1lOOOme_y56VWrEE1jkarTt-WoUo9yilCo8sUrivyns,2680
|
|
3
|
+
maltoolbox/default.conf,sha256=YLGBSJh2q8hn3RzRRBbib9F6E6pcvquoHeALMRtA0wU,295
|
|
4
|
+
maltoolbox/exceptions.py,sha256=0YjPx2v1yYumZ2o7pVZ1s_jS-GAb3Ng979KEFhROSNY,1399
|
|
5
|
+
maltoolbox/file_utils.py,sha256=6KFEEZvf9x8yfNAq7hadF7lUGlLimNFMJ0W_DK2rh6Q,2024
|
|
6
|
+
maltoolbox/model.py,sha256=gYGFdjkHPYpBuEngBRsj_Ki6fFSJtfBG9p8oCARz7gk,30556
|
|
7
|
+
maltoolbox/wrappers.py,sha256=BYYNcIdTlyumADQCPcy1xmPEabfmi0P1l9RcbdVWm9w,2002
|
|
8
|
+
maltoolbox/attackgraph/__init__.py,sha256=Oqqj5iCwnrzjDoJEFZnVI_kebjJPVbPXK-mWHy0lf-8,209
|
|
9
|
+
maltoolbox/attackgraph/attacker.py,sha256=OaBNDYZF8shbFuQctzuNYVkOrpNb_KhxxV19k0SRa50,3541
|
|
10
|
+
maltoolbox/attackgraph/attackgraph.py,sha256=T9snTC8kzgN017leI29CYj2YdlrU8IDxYiV69yDgz7o,30060
|
|
11
|
+
maltoolbox/attackgraph/node.py,sha256=oFaGCz4QPvDcS7xM5lxaG_-GUR-PKT2xtQ1ryzYRWaU,5869
|
|
12
|
+
maltoolbox/attackgraph/query.py,sha256=JnoNTUEIlLv2VIk3u5Rq3GpleOn9TZVGBVijniRY_44,6802
|
|
13
|
+
maltoolbox/attackgraph/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
maltoolbox/attackgraph/analyzers/apriori.py,sha256=Af4NOSiE6Z0UnI_fuhxBA6YtSkDUj1kMie1rj09I0qM,8548
|
|
15
|
+
maltoolbox/ingestors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
maltoolbox/ingestors/neo4j.py,sha256=jdulYsQ2eZT2r0Af_yYjyGkmVx4l5h8viu1Z70NjVAM,8811
|
|
17
|
+
maltoolbox/language/__init__.py,sha256=0tvCJDayrLwpqKRny7LBkMOrvcDE6JfJj7U7Jd64Okg,140
|
|
18
|
+
maltoolbox/language/classes_factory.py,sha256=0QFF5Z9e4UeWwavvH1jM4BkeDZqKKZ4Ij7leGmSfXn4,10063
|
|
19
|
+
maltoolbox/language/languagegraph.py,sha256=f79ovmrGQb6tvY9ze-zqP031N6rApB9WsbZd5LRyhk8,47031
|
|
20
|
+
maltoolbox/language/compiler/__init__.py,sha256=fJ22-FlXfr907WCPkqlr_eBTzPqsrg6m3i7J_ZWpuAo,840
|
|
21
|
+
maltoolbox/language/compiler/mal_lexer.py,sha256=wocRzBkLbqYefpGvq2W77x7439-AdZKVgPWhRiRubXg,10776
|
|
22
|
+
maltoolbox/language/compiler/mal_parser.py,sha256=M1EVZFV73TNfQHz2KJ8-iloqOD4KUhHyajszD8UrNow,91349
|
|
23
|
+
maltoolbox/language/compiler/mal_visitor.py,sha256=9gG06D7GZKlBY-62SmbIkRYkGBUBIC6fl1GOg7v2IuM,13223
|
|
24
|
+
maltoolbox/translators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
maltoolbox/translators/securicad.py,sha256=FAIHnoqFTmNYbCGxLsK6pX5g1oiNFfPTqkT_3qq3GG8,6692
|
|
26
|
+
maltoolbox/translators/updater.py,sha256=Ap08-AsU_7or5ESQvZL2i4nWz3B5pvgfftZyc_-Gd8M,4766
|
|
27
|
+
mal_toolbox-0.1.12.dist-info/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
|
|
28
|
+
mal_toolbox-0.1.12.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
29
|
+
mal_toolbox-0.1.12.dist-info/METADATA,sha256=XNkHzeC0F3E4Mtp4GhxAm2d2SWqDteCPNVZ1Px0q2rw,6002
|
|
30
|
+
mal_toolbox-0.1.12.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
31
|
+
mal_toolbox-0.1.12.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
|
|
32
|
+
mal_toolbox-0.1.12.dist-info/RECORD,,
|
maltoolbox/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# MAL Toolbox v0.
|
|
2
|
+
# MAL Toolbox v0.1.12
|
|
3
3
|
# Copyright 2024, Andrei Buhaiu.
|
|
4
4
|
#
|
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -21,69 +21,70 @@ MAL-Toolbox Framework
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
__title__ = 'maltoolbox'
|
|
24
|
-
__version__ = '0.
|
|
24
|
+
__version__ = '0.1.12'
|
|
25
25
|
__authors__ = ['Andrei Buhaiu',
|
|
26
26
|
'Giuseppe Nebbione',
|
|
27
27
|
'Nikolaos Kakouros',
|
|
28
|
-
'Jakob Nyberg'
|
|
28
|
+
'Jakob Nyberg',
|
|
29
|
+
'Joakim Loxdal']
|
|
29
30
|
__license__ = 'Apache 2.0'
|
|
30
31
|
__docformat__ = 'restructuredtext en'
|
|
31
32
|
|
|
32
33
|
__all__ = ()
|
|
33
34
|
|
|
34
35
|
import os
|
|
35
|
-
import sys
|
|
36
36
|
import configparser
|
|
37
37
|
import logging
|
|
38
38
|
|
|
39
|
-
from pkg_resources import Requirement, resource_filename
|
|
40
|
-
|
|
41
39
|
ERROR_INCORRECT_CONFIG = 1
|
|
42
40
|
|
|
43
|
-
CONFIGFILE =
|
|
44
|
-
|
|
41
|
+
CONFIGFILE = os.path.join(
|
|
42
|
+
os.path.dirname(os.path.abspath(__file__)),
|
|
43
|
+
"default.conf"
|
|
44
|
+
)
|
|
45
45
|
|
|
46
46
|
config = configparser.ConfigParser()
|
|
47
47
|
config.read(CONFIGFILE)
|
|
48
48
|
|
|
49
49
|
if 'logging' not in config:
|
|
50
|
-
|
|
51
|
-
sys.exit(ERROR_INCORRECT_CONFIG)
|
|
50
|
+
raise ValueError('Config file is missing essential information, cannot proceed.')
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
logger.critical('Config file is missing essential '\
|
|
56
|
-
'information, cannot proceed.')
|
|
57
|
-
print('Config file is missing essential information, cannot '\
|
|
58
|
-
'proceed.')
|
|
59
|
-
sys.exit(ERROR_INCORRECT_CONFIG)
|
|
52
|
+
if 'log_file' not in config['logging']:
|
|
53
|
+
raise ValueError('Config file is missing a log_file location, cannot proceed.')
|
|
60
54
|
|
|
61
55
|
log_configs = {
|
|
62
|
-
'output_dir': config['logging']['output_dir'],
|
|
63
56
|
'log_file': config['logging']['log_file'],
|
|
57
|
+
'log_level': config['logging']['log_level'],
|
|
64
58
|
'attackgraph_file': config['logging']['attackgraph_file'],
|
|
65
59
|
'model_file': config['logging']['model_file'],
|
|
66
60
|
'langspec_file': config['logging']['langspec_file'],
|
|
67
61
|
}
|
|
68
62
|
|
|
69
|
-
os.makedirs(log_configs['
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
filemode='w')
|
|
75
|
-
logging.getLogger('python_jsonschema_objects').setLevel(logging.WARNING)
|
|
63
|
+
os.makedirs(os.path.dirname(log_configs['log_file']), exist_ok = True)
|
|
64
|
+
|
|
65
|
+
formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M')
|
|
66
|
+
file_handler = logging.FileHandler(log_configs['log_file'], mode='w')
|
|
67
|
+
file_handler.setFormatter(formatter)
|
|
76
68
|
|
|
77
69
|
logger = logging.getLogger(__name__)
|
|
70
|
+
logger.addHandler(file_handler)
|
|
71
|
+
|
|
72
|
+
log_level = log_configs['log_level']
|
|
73
|
+
if log_level != '':
|
|
74
|
+
level = logging.getLevelName(log_level)
|
|
75
|
+
logger.setLevel(level)
|
|
76
|
+
logger.info('Set loggin level of %s to %s.', __name__, log_level)
|
|
78
77
|
|
|
79
78
|
if 'neo4j' in config:
|
|
80
79
|
for term in ['uri', 'username', 'password', 'dbname']:
|
|
81
80
|
if term not in config['neo4j']:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
f'
|
|
86
|
-
|
|
81
|
+
|
|
82
|
+
msg = (
|
|
83
|
+
'Config file is missing essential Neo4J '
|
|
84
|
+
f'information: {term}, cannot proceed.'
|
|
85
|
+
)
|
|
86
|
+
logger.critical(msg)
|
|
87
|
+
raise ValueError(msg)
|
|
87
88
|
|
|
88
89
|
neo4j_configs = {
|
|
89
90
|
'uri': config['neo4j']['uri'],
|
|
@@ -91,4 +92,3 @@ if 'neo4j' in config:
|
|
|
91
92
|
'password': config['neo4j']['password'],
|
|
92
93
|
'dbname': config['neo4j']['dbname'],
|
|
93
94
|
}
|
|
94
|
-
|
maltoolbox/__main__.py
CHANGED
|
@@ -1,7 +1,83 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for MAL toolbox operations
|
|
2
3
|
|
|
4
|
+
Usage:
|
|
5
|
+
maltoolbox attack-graph generate [options] <model> <lang_file>
|
|
6
|
+
maltoolbox compile <lang_file> <output_file>
|
|
3
7
|
|
|
4
|
-
|
|
8
|
+
Arguments:
|
|
9
|
+
<model> Path to JSON instance model file.
|
|
10
|
+
<lang_file> Path to .mar or .mal file containing MAL spec.
|
|
11
|
+
<output_file> Path to write the JSON result of the compilation.
|
|
5
12
|
|
|
6
|
-
|
|
7
|
-
|
|
13
|
+
Options:
|
|
14
|
+
--neo4j Ingest attack graph and instance model into a Neo4j instance
|
|
15
|
+
|
|
16
|
+
Notes:
|
|
17
|
+
- <lang_file> can be either a .mar file (generated by the older MAL
|
|
18
|
+
compiler) or a .mal file containing the DSL written in MAL.
|
|
19
|
+
|
|
20
|
+
- If --neo4j is used, the Neo4j instance should be running. The connection
|
|
21
|
+
parameters required for this app to reach the Neo4j instance should be
|
|
22
|
+
defined in the default.conf file.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
import json
|
|
27
|
+
import docopt
|
|
28
|
+
|
|
29
|
+
from maltoolbox.wrappers import create_attack_graph
|
|
30
|
+
from . import log_configs, neo4j_configs
|
|
31
|
+
from .language.compiler import MalCompiler
|
|
32
|
+
from .ingestors import neo4j
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
def generate_attack_graph(
|
|
37
|
+
model_file: str,
|
|
38
|
+
lang_file: str,
|
|
39
|
+
send_to_neo4j: bool
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Create an attack graph and optionally send to neo4j
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
model_file - path to the model file
|
|
45
|
+
lang_file - path to the language file
|
|
46
|
+
send_to_neo4j - whether to ingest into neo4j or not
|
|
47
|
+
"""
|
|
48
|
+
attack_graph = create_attack_graph(lang_file, model_file)
|
|
49
|
+
if log_configs['attackgraph_file']:
|
|
50
|
+
attack_graph.save_to_file(
|
|
51
|
+
log_configs['attackgraph_file']
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if send_to_neo4j:
|
|
55
|
+
logger.debug('Ingest model graph into Neo4J database.')
|
|
56
|
+
neo4j.ingest_model(attack_graph.model,
|
|
57
|
+
neo4j_configs['uri'],
|
|
58
|
+
neo4j_configs['username'],
|
|
59
|
+
neo4j_configs['password'],
|
|
60
|
+
neo4j_configs['dbname'],
|
|
61
|
+
delete=True)
|
|
62
|
+
logger.debug('Ingest attack graph into Neo4J database.')
|
|
63
|
+
neo4j.ingest_attack_graph(attack_graph,
|
|
64
|
+
neo4j_configs['uri'],
|
|
65
|
+
neo4j_configs['username'],
|
|
66
|
+
neo4j_configs['password'],
|
|
67
|
+
neo4j_configs['dbname'],
|
|
68
|
+
delete=False)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def compile(lang_file: str, output_file: str) -> None:
|
|
72
|
+
"""Compile language and dump into output file"""
|
|
73
|
+
compiler = MalCompiler()
|
|
74
|
+
with open(output_file, "w") as f:
|
|
75
|
+
json.dump(compiler.compile(lang_file), f, indent=2)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
args = docopt.docopt(__doc__)
|
|
79
|
+
|
|
80
|
+
if args['attack-graph'] and args['generate']:
|
|
81
|
+
generate_attack_graph(args['<model>'], args['<lang_file>'], args['--neo4j'])
|
|
82
|
+
elif args['compile']:
|
|
83
|
+
compile(args['<lang_file>'], args['<output_file>'])
|
|
File without changes
|
|
@@ -11,21 +11,25 @@ Currently these are:
|
|
|
11
11
|
compromised) to compromise children attack steps.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
from typing import Optional
|
|
14
16
|
import logging
|
|
15
17
|
|
|
16
|
-
from
|
|
17
|
-
from
|
|
18
|
+
from ..attackgraph import AttackGraph
|
|
19
|
+
from ..node import AttackGraphNode
|
|
18
20
|
|
|
19
21
|
logger = logging.getLogger(__name__)
|
|
20
22
|
|
|
21
|
-
def propagate_viability_from_node(node: AttackGraphNode):
|
|
23
|
+
def propagate_viability_from_node(node: AttackGraphNode) -> None:
|
|
22
24
|
"""
|
|
23
25
|
Arguments:
|
|
24
26
|
node - the attack graph node from which to propagate the viable
|
|
25
27
|
status
|
|
26
28
|
"""
|
|
27
|
-
logger.debug(
|
|
28
|
-
|
|
29
|
+
logger.debug(
|
|
30
|
+
'Propagate viability from "%s"(%d) with viability status %s.',
|
|
31
|
+
node.full_name, node.id, node.is_viable
|
|
32
|
+
)
|
|
29
33
|
for child in node.children:
|
|
30
34
|
original_value = child.is_viable
|
|
31
35
|
if child.type == 'or':
|
|
@@ -38,14 +42,26 @@ def propagate_viability_from_node(node: AttackGraphNode):
|
|
|
38
42
|
if child.is_viable != original_value:
|
|
39
43
|
propagate_viability_from_node(child)
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
|
|
46
|
+
def propagate_necessity_from_node(node: AttackGraphNode) -> None:
|
|
42
47
|
"""
|
|
43
48
|
Arguments:
|
|
44
49
|
node - the attack graph node from which to propagate the necessary
|
|
45
50
|
status
|
|
46
51
|
"""
|
|
47
|
-
logger.debug(
|
|
48
|
-
|
|
52
|
+
logger.debug(
|
|
53
|
+
'Propagate necessity from "%s"(%d) with necessity status %s.',
|
|
54
|
+
node.full_name, node.id, node.is_necessary
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if node.ttc and 'name' in node.ttc:
|
|
58
|
+
if node.ttc['name'] not in ['Enabled', 'Disabled']:
|
|
59
|
+
# Do not propagate unnecessary state from nodes that have a TTC
|
|
60
|
+
# probability distribution associated with them.
|
|
61
|
+
# TODO: Evaluate this more carefully, how do we want to have TTCs
|
|
62
|
+
# impact necessity and viability.
|
|
63
|
+
return
|
|
64
|
+
|
|
49
65
|
for child in node.children:
|
|
50
66
|
original_value = child.is_necessary
|
|
51
67
|
if child.type == 'or':
|
|
@@ -60,27 +76,157 @@ def propagate_necessity_from_node(node: AttackGraphNode):
|
|
|
60
76
|
if child.is_necessary != original_value:
|
|
61
77
|
propagate_necessity_from_node(child)
|
|
62
78
|
|
|
63
|
-
|
|
79
|
+
|
|
80
|
+
def evaluate_viability(node: AttackGraphNode) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Arguments:
|
|
83
|
+
graph - the node to evaluate viability for.
|
|
84
|
+
"""
|
|
85
|
+
match (node.type):
|
|
86
|
+
case 'exist':
|
|
87
|
+
assert isinstance(node.existence_status, bool), \
|
|
88
|
+
f'Existence status not defined for {node.full_name}.'
|
|
89
|
+
node.is_viable = node.existence_status
|
|
90
|
+
case 'notExist':
|
|
91
|
+
assert isinstance(node.existence_status, bool), \
|
|
92
|
+
f'Existence status not defined for {node.full_name}.'
|
|
93
|
+
node.is_viable = not node.existence_status
|
|
94
|
+
case 'defense':
|
|
95
|
+
assert node.defense_status is not None and \
|
|
96
|
+
0.0 <= node.defense_status <= 1.0, \
|
|
97
|
+
f'{node.full_name} defense status invalid: {node.defense_status}.'
|
|
98
|
+
node.is_viable = node.defense_status != 1.0
|
|
99
|
+
case 'or':
|
|
100
|
+
node.is_viable = False
|
|
101
|
+
for parent in node.parents:
|
|
102
|
+
node.is_viable = node.is_viable or parent.is_viable
|
|
103
|
+
case 'and':
|
|
104
|
+
node.is_viable = True
|
|
105
|
+
for parent in node.parents:
|
|
106
|
+
node.is_viable = node.is_viable and parent.is_viable
|
|
107
|
+
case _:
|
|
108
|
+
msg = ('Evaluate viability was provided node "%s"(%d) which '
|
|
109
|
+
'is of unknown type "%s"')
|
|
110
|
+
logger.error(msg, node.full_name, node.id, node.type)
|
|
111
|
+
raise ValueError(msg % (node.full_name, node.id, node.type))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def evaluate_necessity(node: AttackGraphNode) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Arguments:
|
|
117
|
+
graph - the node to evaluate necessity for.
|
|
118
|
+
"""
|
|
119
|
+
match (node.type):
|
|
120
|
+
case 'exist':
|
|
121
|
+
assert isinstance(node.existence_status, bool), \
|
|
122
|
+
f'Existence status not defined for {node.full_name}.'
|
|
123
|
+
node.is_necessary = not node.existence_status
|
|
124
|
+
case 'notExist':
|
|
125
|
+
assert isinstance(node.existence_status, bool), \
|
|
126
|
+
f'Existence status not defined for {node.full_name}.'
|
|
127
|
+
node.is_necessary = bool(node.existence_status)
|
|
128
|
+
case 'defense':
|
|
129
|
+
assert node.defense_status is not None and \
|
|
130
|
+
0.0 <= node.defense_status <= 1.0, \
|
|
131
|
+
f'{node.full_name} defense status invalid: {node.defense_status}.'
|
|
132
|
+
node.is_necessary = node.defense_status != 0.0
|
|
133
|
+
case 'or':
|
|
134
|
+
node.is_necessary = True
|
|
135
|
+
for parent in node.parents:
|
|
136
|
+
node.is_necessary = node.is_necessary and parent.is_necessary
|
|
137
|
+
case 'and':
|
|
138
|
+
node.is_necessary = False
|
|
139
|
+
for parent in node.parents:
|
|
140
|
+
node.is_necessary = node.is_necessary or parent.is_necessary
|
|
141
|
+
case _:
|
|
142
|
+
msg = ('Evaluate necessity was provided node "%s"(%d) which '
|
|
143
|
+
'is of unknown type "%s"')
|
|
144
|
+
logger.error(msg, node.full_name, node.id, node.type)
|
|
145
|
+
raise ValueError(msg % (node.full_name, node.id, node.type))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def evaluate_viability_and_necessity(node: AttackGraphNode) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Arguments:
|
|
151
|
+
graph - the node to evaluate viability and necessity for.
|
|
152
|
+
"""
|
|
153
|
+
evaluate_viability(node)
|
|
154
|
+
evaluate_necessity(node)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def calculate_viability_and_necessity(graph: AttackGraph) -> None:
|
|
64
158
|
"""
|
|
65
159
|
Arguments:
|
|
66
|
-
|
|
160
|
+
graph - the attack graph for which we wish to determine the
|
|
67
161
|
viability and necessity statuses for the nodes.
|
|
68
162
|
"""
|
|
69
163
|
for node in graph.nodes:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
node
|
|
74
|
-
|
|
75
|
-
node
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if
|
|
86
|
-
|
|
164
|
+
if node.type in ['exist', 'notExist', 'defense']:
|
|
165
|
+
evaluate_viability_and_necessity(node)
|
|
166
|
+
if not node.is_viable:
|
|
167
|
+
propagate_viability_from_node(node)
|
|
168
|
+
if not node.is_necessary:
|
|
169
|
+
propagate_necessity_from_node(node)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def prune_unviable_and_unnecessary_nodes(graph: AttackGraph) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Arguments:
|
|
175
|
+
graph - the attack graph for which we wish to remove the
|
|
176
|
+
the nodes which are not viable or necessary.
|
|
177
|
+
"""
|
|
178
|
+
for node in graph.nodes:
|
|
179
|
+
if (node.type == 'or' or node.type == 'and') and \
|
|
180
|
+
(not node.is_viable or not node.is_necessary):
|
|
181
|
+
graph.remove_node(node)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def propagate_viability_from_unviable_node(
|
|
185
|
+
unviable_node: AttackGraphNode,
|
|
186
|
+
) -> list[AttackGraphNode]:
|
|
187
|
+
"""
|
|
188
|
+
Update viability of nodes affected by newly enabled defense
|
|
189
|
+
`unviable_node` in the graph and return any attack steps
|
|
190
|
+
that are no longer viable because of it.
|
|
191
|
+
|
|
192
|
+
Propagate recursively via children as long as changes occur.
|
|
193
|
+
|
|
194
|
+
Arguments:
|
|
195
|
+
unviable_node - the node to propagate viability from
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
attack_steps_made_unviable - list of the attack steps that have been
|
|
199
|
+
made unviable by a defense enabled in the
|
|
200
|
+
current step. Builds up recursively.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
attack_steps_made_unviable = []
|
|
204
|
+
|
|
205
|
+
logger.debug(
|
|
206
|
+
'Update viability for node "%s"(%d)',
|
|
207
|
+
unviable_node.full_name,
|
|
208
|
+
unviable_node.id
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
assert not unviable_node.is_viable, (
|
|
212
|
+
"propagate_viability_from_unviable_node should not be called"
|
|
213
|
+
f" on viable node {unviable_node.full_name}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if unviable_node.type in ('and', 'or'):
|
|
217
|
+
attack_steps_made_unviable.append(unviable_node)
|
|
218
|
+
|
|
219
|
+
for child in unviable_node.children:
|
|
220
|
+
original_value = child.is_viable
|
|
221
|
+
if child.type == 'or':
|
|
222
|
+
child.is_viable = False
|
|
223
|
+
for parent in child.parents:
|
|
224
|
+
child.is_viable = child.is_viable or parent.is_viable
|
|
225
|
+
if child.type == 'and':
|
|
226
|
+
child.is_viable = False
|
|
227
|
+
|
|
228
|
+
if child.is_viable != original_value:
|
|
229
|
+
attack_steps_made_unviable += \
|
|
230
|
+
propagate_viability_from_unviable_node(child)
|
|
231
|
+
|
|
232
|
+
return attack_steps_made_unviable
|