mal-toolbox 0.0.27__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.
Files changed (37) hide show
  1. {mal_toolbox-0.0.27.dist-info → mal_toolbox-0.1.12.dist-info}/METADATA +60 -28
  2. mal_toolbox-0.1.12.dist-info/RECORD +32 -0
  3. {mal_toolbox-0.0.27.dist-info → mal_toolbox-0.1.12.dist-info}/WHEEL +1 -1
  4. maltoolbox/__init__.py +31 -31
  5. maltoolbox/__main__.py +80 -4
  6. maltoolbox/attackgraph/__init__.py +8 -0
  7. maltoolbox/attackgraph/analyzers/__init__.py +0 -0
  8. maltoolbox/attackgraph/analyzers/apriori.py +173 -27
  9. maltoolbox/attackgraph/attacker.py +99 -21
  10. maltoolbox/attackgraph/attackgraph.py +507 -217
  11. maltoolbox/attackgraph/node.py +143 -21
  12. maltoolbox/attackgraph/query.py +128 -26
  13. maltoolbox/default.conf +8 -7
  14. maltoolbox/exceptions.py +45 -0
  15. maltoolbox/file_utils.py +66 -0
  16. maltoolbox/ingestors/__init__.py +0 -0
  17. maltoolbox/ingestors/neo4j.py +95 -84
  18. maltoolbox/language/__init__.py +4 -0
  19. maltoolbox/language/classes_factory.py +145 -64
  20. maltoolbox/language/{lexer_parser/__main__.py → compiler/__init__.py} +5 -12
  21. maltoolbox/language/{lexer_parser → compiler}/mal_lexer.py +1 -1
  22. maltoolbox/language/{lexer_parser → compiler}/mal_parser.py +1 -1
  23. maltoolbox/language/{lexer_parser → compiler}/mal_visitor.py +4 -5
  24. maltoolbox/language/languagegraph.py +569 -168
  25. maltoolbox/model.py +858 -0
  26. maltoolbox/translators/__init__.py +0 -0
  27. maltoolbox/translators/securicad.py +76 -52
  28. maltoolbox/translators/updater.py +132 -0
  29. maltoolbox/wrappers.py +62 -0
  30. mal_toolbox-0.0.27.dist-info/RECORD +0 -26
  31. maltoolbox/cl_parser.py +0 -89
  32. maltoolbox/language/specification.py +0 -265
  33. maltoolbox/main.py +0 -84
  34. maltoolbox/model/model.py +0 -279
  35. {mal_toolbox-0.0.27.dist-info → mal_toolbox-0.1.12.dist-info}/AUTHORS +0 -0
  36. {mal_toolbox-0.0.27.dist-info → mal_toolbox-0.1.12.dist-info}/LICENSE +0 -0
  37. {mal_toolbox-0.0.27.dist-info → mal_toolbox-0.1.12.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,8 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: mal-toolbox
3
- Version: 0.0.27
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 >=2021.2.3
22
- Requires-Dist: python-jsonschema-objects >=0.4.1
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
- # Overview
28
+ # MAL Toolbox overview
25
29
 
26
- A collection of python modules to help developers create and work with MAL
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
- # The Language Module
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
- ## The Language Specification Submodule
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
- ## The Language Classes Factory Submodule
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
- # The Model Module
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
- The model module is used to create MAL instance models. The model class will
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
- # The Attack Graph Module
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
- # Ingestors Module
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
- # Configuration
103
- A default configuration file `default.conf` can be found in the package
104
- directory. This contains the default values to use for logging and can also be
105
- used to store the information needed to access the local Neo4J instance.
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
maltoolbox/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # MAL Toolbox v0.0.27
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.0.27'
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 = resource_filename(Requirement.parse("mal-toolbox"),
44
- "maltoolbox/default.conf")
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
- print('Config file is missing essential information, cannot proceed.')
51
- sys.exit(ERROR_INCORRECT_CONFIG)
50
+ raise ValueError('Config file is missing essential information, cannot proceed.')
52
51
 
53
- for term in ['output_dir', 'log_file']:
54
- if term not in config['logging']:
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['output_dir'], exist_ok = True)
70
- logging.basicConfig(level=logging.DEBUG,
71
- format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
72
- datefmt='%m-%d %H:%M',
73
- filename=log_configs["log_file"],
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
- logger.critical('Config file is missing essential '\
83
- f'Neo4J information: {term}, cannot proceed.')
84
- print('Config file is missing essential '\
85
- f'Neo4J information: {term}, cannot proceed.')
86
- sys.exit(ERROR_INCORRECT_CONFIG)
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
- from maltoolbox.main import main
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
- __all__ = ('main',)
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
- if __name__ == '__main__':
7
- main()
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>'])
@@ -0,0 +1,8 @@
1
+ """
2
+ Contains tools used to generate attack graphs from MAL instance
3
+ models and analyze attack graphs.
4
+ """
5
+
6
+ from .attacker import Attacker
7
+ from .attackgraph import AttackGraph
8
+ from .node import AttackGraphNode
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 maltoolbox.attackgraph import attackgraph
17
- from maltoolbox.attackgraph.node import AttackGraphNode
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(f'Propagate viability from {node.id} with viability'
28
- f' status {node.is_viable}.')
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
- def propagate_necessity_from_node(node: AttackGraphNode):
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(f'Propagate necessity from {node.id} with necessity'
48
- f' status {node.is_necessary}.')
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
- def calculate_viability_and_necessity(graph: attackgraph.AttackGraph):
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
- node - the attack graph for which we wish to determine the
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
- match (node.type):
71
- case 'exist':
72
- node.is_viable = node.existence_status
73
- node.is_necessary = not node.existence_status
74
- case 'notExist':
75
- node.is_viable = not node.existence_status
76
- node.is_necessary = node.existence_status
77
- case 'defense':
78
- node.is_viable = node.defense_status != 1.0
79
- node.is_necessary = node.defense_status != 0.0
80
- case _:
81
- pass
82
-
83
- if not node.is_viable:
84
- propagate_viability_from_node(node)
85
- if not node.is_necessary:
86
- propagate_necessity_from_node(node)
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