mal-toolbox 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mal-toolbox
3
- Version: 0.2.0
3
+ Version: 0.3.0
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>, Joakim Loxdal <loxdal@kth.se>
5
+ Author-email: Andrei Buhaiu <buhaiu@kth.se>, Joakim Loxdal <loxdal@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>, Giuseppe Nebbione <nebbione@kth.se>
6
6
  License: Apache Software License
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
@@ -19,7 +19,6 @@ Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  License-File: AUTHORS
21
21
  Requires-Dist: py2neo>=2021.2.3
22
- Requires-Dist: python-jsonschema-objects>=0.5.5
23
22
  Requires-Dist: antlr4-tools
24
23
  Requires-Dist: antlr4-python3-runtime
25
24
  Requires-Dist: docopt
@@ -48,17 +47,6 @@ then be used to generate python classes representing the assets and
48
47
  associations of the language and to determine the attack steps for each asset
49
48
  when generating the attack graph.
50
49
 
51
- ### The Language Classes Factory Submodule
52
-
53
- The language classes factory submodule is used to generate python classes
54
- using the `python_jsonschema_objects` package from a language specification.
55
- The classes generated by the `create_classes` function can then be accessed
56
- from within that namespace(.e.g: `lang_classes_factory.ns.Application()`,
57
- `lang_classes_factory.ns.AppExecution()`). Because these classes are built
58
- using JSON Schema validators they will enforce their restrictions when using
59
- the python objects created. These classes are typically used in conjunction
60
- with model module to create instance models.
61
-
62
50
  ## The Model Module
63
51
 
64
52
  With a MAL language a Model (a MAL instance model) can be created either
@@ -109,21 +97,51 @@ pip install mal-toolbox
109
97
  ```
110
98
 
111
99
  ## 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.
100
+ You can use a `maltoolbox.yml` file in the current working directory to
101
+ configure the toolbox. Alternatively, you can use the `MALTOOLBOX_CONFIG`
102
+ environment variable to set a custom config file location:
103
+
104
+ """bash
105
+ # in your shell, e.g. bash do:
106
+ export MALTOOLBOX_CONFIG=path/to/yml/config/file
107
+ """
108
+
109
+ The default configuration can be found here:
110
+
111
+ https://github.com/mal-lang/mal-toolbox/blob/main/maltoolbox/__init__.py#L39-L53
115
112
 
116
113
  ## Command Line Client
117
- In addition to the modules that make up the MAL-Toolbox package it also
118
- provides a simple command line client that can be used to easily generate
119
- attack graphs from a .mar language specification file and a JSON instance
120
- model file.
121
114
 
122
- The usage is: `maltoolbox gen_ag [--neo4j] <model_json_file>
123
- <language_mar_file>`
115
+ You can use the maltoolbox cli to:
116
+
117
+ - Generate attack graphs from model files
118
+ - Compile MAL languages
119
+ - Upgrade model files from older versions
124
120
 
125
- If the `--neo4j` flag is specified the model and attack graph will be loaded
126
- into a local Neo4J instance.
121
+ ```
122
+ Command-line interface for MAL toolbox operations
123
+
124
+ Usage:
125
+ maltoolbox attack-graph generate [options] <model_file> <lang_file>
126
+ maltoolbox compile <lang_file> <output_file>
127
+ maltoolbox upgrade-model <model_file> <lang_file> <output_file>
128
+
129
+ Arguments:
130
+ <model_file> Path to JSON instance model file.
131
+ <lang_file> Path to .mar or .mal file containing MAL spec.
132
+ <output_file> Path to write the result of the compilation (yml/json).
133
+
134
+ Options:
135
+ --neo4j Ingest attack graph and instance model into a Neo4j instance
136
+
137
+ Notes:
138
+ - <lang_file> can be either a .mar file (generated by the older MAL
139
+ compiler) or a .mal file containing the DSL written in MAL.
140
+
141
+ - If --neo4j is used, the Neo4j instance should be running. The connection
142
+ parameters required for this app to reach the Neo4j instance should be
143
+ defined in the default.conf file.
144
+ ```
127
145
 
128
146
  ## Code examples / Tutorial
129
147
 
@@ -0,0 +1,29 @@
1
+ maltoolbox/__init__.py,sha256=q_M3mEsQW1IFQ5xVZ3fK0gVyt8r_c2kxUC3ZDAtPmII,2088
2
+ maltoolbox/__main__.py,sha256=PSg8vFS8X-klJBJdSzrg0aLh9ykZgbcoSSEy3DTQoQQ,3499
3
+ maltoolbox/exceptions.py,sha256=0YjPx2v1yYumZ2o7pVZ1s_jS-GAb3Ng979KEFhROSNY,1399
4
+ maltoolbox/file_utils.py,sha256=tBR8Kjl8IoFzAtYaLNHNALuQrdMT3pD1ZpczHm1pu2g,1875
5
+ maltoolbox/model.py,sha256=XCneM5Hdl2GQhtgXYq0SUAlHBksXn7okevIsSKMSwVY,24084
6
+ maltoolbox/attackgraph/__init__.py,sha256=AHDyX6dAkx3mDic2K56v1xche9N6ofDfbaHkKbdJ2qQ,230
7
+ maltoolbox/attackgraph/attacker.py,sha256=dKMAcOwlNM3LL8qh1tBJuzjFxlTNG-QI2st1LzP8Ofc,4030
8
+ maltoolbox/attackgraph/attackgraph.py,sha256=A5TjDE-B4c0hY28M4IVwHVFhM_ww2eAt7uITjSUCNKc,32495
9
+ maltoolbox/attackgraph/node.py,sha256=Ec67_u_8qf_MgCHaUg4wIbZFC013GWxbIsC8EjoguzE,6465
10
+ maltoolbox/attackgraph/query.py,sha256=s66EQcHlldn7XLCWxZ2FRjZTgk8o3V0mD4LnRaImdCw,6836
11
+ maltoolbox/attackgraph/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ maltoolbox/attackgraph/analyzers/apriori.py,sha256=iH1KvKbMi9J3_1qia8FXI80O3ZMQ1LP67HkXbqweq-c,8802
13
+ maltoolbox/ingestors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ maltoolbox/ingestors/neo4j.py,sha256=gjQQqk0HVI8ojIqwS1ymt7-_ZvDOTNi8AcczDAlvsGs,8310
15
+ maltoolbox/language/__init__.py,sha256=9p5nvVqDCKEhXbDMIz1MtwZ9GN7x1jmUUXbpjEwuqnw,269
16
+ maltoolbox/language/languagegraph.py,sha256=npP1lX1EE1rqqKNr9qTb4Jylvdn5JYApNyJhjwSebJs,67785
17
+ maltoolbox/language/compiler/__init__.py,sha256=JQyAgDwJh1pU7AmuOhd1-d2b2PYXpgMVPtxnav8QHVc,15872
18
+ maltoolbox/language/compiler/mal_lexer.py,sha256=BeifykDAt4PloRASOaLzBgWF35ev_zgD8lXMIsSHykc,12063
19
+ maltoolbox/language/compiler/mal_parser.py,sha256=sUoaE43l2VKg-Dou30mk2wlVS1FvdOREwHNIyFe4IkY,114699
20
+ maltoolbox/translators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ maltoolbox/translators/securicad.py,sha256=PJYjieioWN5tE_oKm83dtgV5UkC8EUH9Vsy3-FxBtUo,7017
22
+ maltoolbox/translators/updater.py,sha256=8bisZnzMWjGaG5tu8jdF-Oq6bPwIjXkVO-_yZDGc6cA,8652
23
+ mal_toolbox-0.3.0.dist-info/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
24
+ mal_toolbox-0.3.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
25
+ mal_toolbox-0.3.0.dist-info/METADATA,sha256=Yfkp5QVZL6W4DIsYkQqRQU3HpTC6dkrVCHsfchgb4k0,6158
26
+ mal_toolbox-0.3.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
27
+ mal_toolbox-0.3.0.dist-info/entry_points.txt,sha256=oqby5O6cUP_OHCm70k_iYPA6UlbTBf7se1i3XwdK3uU,56
28
+ mal_toolbox-0.3.0.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
29
+ mal_toolbox-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ maltoolbox = maltoolbox.__main__:main
maltoolbox/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # MAL Toolbox v0.2.0
2
+ # MAL Toolbox v0.3.0
3
3
  # Copyright 2024, Andrei Buhaiu.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,75 +20,56 @@
20
20
  MAL-Toolbox Framework
21
21
  """
22
22
 
23
- __title__ = 'maltoolbox'
24
- __version__ = '0.2.0'
25
- __authors__ = ['Andrei Buhaiu',
26
- 'Giuseppe Nebbione',
27
- 'Nikolaos Kakouros',
28
- 'Jakob Nyberg',
29
- 'Joakim Loxdal']
30
- __license__ = 'Apache 2.0'
31
- __docformat__ = 'restructuredtext en'
23
+ __title__ = "maltoolbox"
24
+ __version__ = "0.3.0"
25
+ __authors__ = [
26
+ "Andrei Buhaiu",
27
+ "Giuseppe Nebbione",
28
+ "Nikolaos Kakouros",
29
+ "Jakob Nyberg",
30
+ "Joakim Loxdal",
31
+ ]
32
+ __license__ = "Apache 2.0"
33
+ __docformat__ = "restructuredtext en"
32
34
 
33
35
  __all__ = ()
34
36
 
35
37
  import os
36
- import configparser
38
+ import yaml
37
39
  import logging
40
+ from typing import Any
41
+
42
+ config: dict[str, Any] = {
43
+ "logging": {
44
+ "log_level": logging.INFO,
45
+ "log_file": "logs/log.txt",
46
+ "attackgraph_file": "logs/attackgraph.yml",
47
+ "model_file": "logs/model.yml",
48
+ "langspec_file": "logs/langspec_file.yml",
49
+ },
50
+ "neo4j": {"uri": None, "username": None, "password": None, "dbname": None},
51
+ }
38
52
 
39
- ERROR_INCORRECT_CONFIG = 1
40
-
41
- CONFIGFILE = os.path.join(
42
- os.path.dirname(os.path.abspath(__file__)),
43
- "default.conf"
44
- )
45
-
46
- config = configparser.ConfigParser()
47
- config.read(CONFIGFILE)
53
+ config_file = os.getenv("MALTOOLBOX_CONFIG", "maltoolbox.yml")
48
54
 
49
- if 'logging' not in config:
50
- raise ValueError('Config file is missing essential information, cannot proceed.')
55
+ if os.path.exists(config_file):
56
+ with open(config_file) as f:
57
+ config |= yaml.safe_load(f)
51
58
 
52
- if 'log_file' not in config['logging']:
53
- raise ValueError('Config file is missing a log_file location, cannot proceed.')
59
+ log_configs, neo4j_configs = config.values()
54
60
 
55
- log_configs = {
56
- 'log_file': config['logging']['log_file'],
57
- 'log_level': config['logging']['log_level'],
58
- 'attackgraph_file': config['logging']['attackgraph_file'],
59
- 'model_file': config['logging']['model_file'],
60
- 'langspec_file': config['logging']['langspec_file'],
61
- }
61
+ os.makedirs(os.path.dirname(log_configs["log_file"]), exist_ok=True)
62
62
 
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')
63
+ formatter = logging.Formatter(
64
+ "%(asctime)s %(name)-12s %(levelname)-8s %(message)s", datefmt="%m-%d %H:%M"
65
+ )
66
+ file_handler = logging.FileHandler(log_configs["log_file"], mode="w")
67
67
  file_handler.setFormatter(formatter)
68
68
 
69
69
  logger = logging.getLogger(__name__)
70
70
  logger.addHandler(file_handler)
71
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)
77
-
78
- if 'neo4j' in config:
79
- for term in ['uri', 'username', 'password', 'dbname']:
80
- if term not in config['neo4j']:
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)
72
+ logger.setLevel(log_configs.get("log_level"))
73
+ logger.info("Set loggin level of %s to %s.", __name__, log_configs.get("log_level"))
88
74
 
89
- neo4j_configs = {
90
- 'uri': config['neo4j']['uri'],
91
- 'username': config['neo4j']['username'],
92
- 'password': config['neo4j']['password'],
93
- 'dbname': config['neo4j']['dbname'],
94
- }
75
+ logger.debug("Config file location: %s", config_file)
maltoolbox/__main__.py CHANGED
@@ -2,13 +2,14 @@
2
2
  Command-line interface for MAL toolbox operations
3
3
 
4
4
  Usage:
5
- maltoolbox attack-graph generate [options] <model> <lang_file>
5
+ maltoolbox attack-graph generate [options] <model_file> <lang_file>
6
6
  maltoolbox compile <lang_file> <output_file>
7
+ maltoolbox upgrade-model <model_file> <lang_file> <output_file>
7
8
 
8
9
  Arguments:
9
- <model> Path to JSON instance model file.
10
+ <model_file> Path to JSON instance model file.
10
11
  <lang_file> Path to .mar or .mal file containing MAL spec.
11
- <output_file> Path to write the JSON result of the compilation.
12
+ <output_file> Path to write the result of the compilation (yml/json).
12
13
 
13
14
  Options:
14
15
  --neo4j Ingest attack graph and instance model into a Neo4j instance
@@ -26,10 +27,12 @@ import logging
26
27
  import json
27
28
  import docopt
28
29
 
29
- from maltoolbox.wrappers import create_attack_graph
30
30
  from . import log_configs, neo4j_configs
31
+ from .attackgraph import create_attack_graph
31
32
  from .language.compiler import MalCompiler
32
33
  from .ingestors import neo4j
34
+ from .language.languagegraph import LanguageGraph
35
+ from .translators.updater import load_model_from_older_version
33
36
 
34
37
  logger = logging.getLogger(__name__)
35
38
 
@@ -39,7 +42,7 @@ def generate_attack_graph(
39
42
  send_to_neo4j: bool
40
43
  ) -> None:
41
44
  """Create an attack graph and optionally send to neo4j
42
-
45
+
43
46
  Args:
44
47
  model_file - path to the model file
45
48
  lang_file - path to the language file
@@ -53,19 +56,23 @@ def generate_attack_graph(
53
56
 
54
57
  if send_to_neo4j:
55
58
  logger.debug('Ingest model graph into Neo4J database.')
56
- neo4j.ingest_model(attack_graph.model,
59
+ neo4j.ingest_model(
60
+ attack_graph.model,
57
61
  neo4j_configs['uri'],
58
62
  neo4j_configs['username'],
59
63
  neo4j_configs['password'],
60
64
  neo4j_configs['dbname'],
61
- delete=True)
65
+ delete=True
66
+ )
62
67
  logger.debug('Ingest attack graph into Neo4J database.')
63
- neo4j.ingest_attack_graph(attack_graph,
68
+ neo4j.ingest_attack_graph(
69
+ attack_graph,
64
70
  neo4j_configs['uri'],
65
71
  neo4j_configs['username'],
66
72
  neo4j_configs['password'],
67
73
  neo4j_configs['dbname'],
68
- delete=False)
74
+ delete=False
75
+ )
69
76
 
70
77
 
71
78
  def compile(lang_file: str, output_file: str) -> None:
@@ -75,9 +82,31 @@ def compile(lang_file: str, output_file: str) -> None:
75
82
  json.dump(compiler.compile(lang_file), f, indent=2)
76
83
 
77
84
 
78
- args = docopt.docopt(__doc__)
85
+ def upgrade_model(model_file: str, lang_file: str, output_file: str):
86
+ lang_graph = LanguageGraph.load_from_file(lang_file)
87
+
88
+ if log_configs['langspec_file']:
89
+ lang_graph.save_to_file(log_configs['langspec_file'])
90
+
91
+ model = load_model_from_older_version(model_file, lang_graph)
92
+ model.save_to_file(output_file)
93
+
94
+
95
+ def main():
96
+ args = docopt.docopt(__doc__)
97
+
98
+ if args['attack-graph'] and args['generate']:
99
+ generate_attack_graph(
100
+ args['<model_file>'], args['<lang_file>'], args['--neo4j']
101
+ )
102
+ elif args['compile']:
103
+ compile(
104
+ args['<lang_file>'], args['<output_file>']
105
+ )
106
+ elif args['upgrade-model']:
107
+ upgrade_model(
108
+ args['<model_file>'], args['<lang_file>'], args['<output_file>']
109
+ )
79
110
 
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>'])
111
+ if __name__ == "__main__":
112
+ main()
@@ -4,5 +4,5 @@ models and analyze attack graphs.
4
4
  """
5
5
 
6
6
  from .attacker import Attacker
7
- from .attackgraph import AttackGraph
7
+ from .attackgraph import AttackGraph, create_attack_graph
8
8
  from .node import AttackGraphNode
@@ -12,11 +12,12 @@ Currently these are:
12
12
  """
13
13
 
14
14
  from __future__ import annotations
15
- from typing import Optional
15
+ from typing import Optional, TYPE_CHECKING
16
16
  import logging
17
17
 
18
- from ..attackgraph import AttackGraph
19
- from ..node import AttackGraphNode
18
+ if TYPE_CHECKING:
19
+ from ..attackgraph import AttackGraph
20
+ from ..node import AttackGraphNode
20
21
 
21
22
  logger = logging.getLogger(__name__)
22
23
 
@@ -162,7 +163,7 @@ def calculate_viability_and_necessity(graph: AttackGraph) -> None:
162
163
  graph - the attack graph for which we wish to determine the
163
164
  viability and necessity statuses for the nodes.
164
165
  """
165
- for node in graph.nodes:
166
+ for node in graph.nodes.values():
166
167
  if node.type in ['exist', 'notExist', 'defense']:
167
168
  evaluate_viability_and_necessity(node)
168
169
  if not node.is_viable:
@@ -178,7 +179,7 @@ def prune_unviable_and_unnecessary_nodes(graph: AttackGraph) -> None:
178
179
  the nodes which are not viable or necessary.
179
180
  """
180
181
  logger.debug('Prune unviable and unnecessary nodes from the attack graph.')
181
- for node in graph.nodes:
182
+ for node in graph.nodes.values():
182
183
  if (node.type == 'or' or node.type == 'and') and \
183
184
  (not node.is_viable or not node.is_necessary):
184
185
  graph.remove_node(node)
@@ -3,7 +3,6 @@ MAL-Toolbox Attack Graph Attacker Class
3
3
  """
4
4
 
5
5
  from __future__ import annotations
6
- from dataclasses import dataclass, field
7
6
  import copy
8
7
  import logging
9
8
 
@@ -14,13 +13,19 @@ if TYPE_CHECKING:
14
13
 
15
14
  logger = logging.getLogger(__name__)
16
15
 
17
- @dataclass
18
16
  class Attacker:
19
- name: str
20
- entry_points: list[AttackGraphNode] = field(default_factory=list)
21
- reached_attack_steps: list[AttackGraphNode] = \
22
- field(default_factory=list)
23
- id: Optional[int] = None
17
+
18
+ def __init__(
19
+ self,
20
+ name: str,
21
+ entry_points: set[AttackGraphNode],
22
+ reached_attack_steps: set[AttackGraphNode],
23
+ attacker_id: Optional[int] = None
24
+ ):
25
+ self.name = name
26
+ self.entry_points = entry_points
27
+ self.reached_attack_steps = reached_attack_steps
28
+ self.id = attacker_id
24
29
 
25
30
  def to_dict(self) -> dict:
26
31
  attacker_dict: dict = {
@@ -40,18 +45,26 @@ class Attacker:
40
45
  return attacker_dict
41
46
 
42
47
  def __repr__(self) -> str:
43
- return str(self.to_dict())
48
+ return f'Attacker(name: "{self.name}", id: {self.id})'
44
49
 
45
50
  def __deepcopy__(self, memo) -> Attacker:
46
- """Deep copy an Attacker"""
51
+ """Deep copy an Attacker
52
+ The deepcopy will copy over attacker specific information, name and
53
+ id, but it will not copy relations to attack graph nodes, reached
54
+ attack steps or entry points. These references should be recreated
55
+ when deepcopying the attack graph itself.
56
+
57
+ """
47
58
 
48
59
  # Check if the object is already in the memo dictionary
49
60
  if id(self) in memo:
50
61
  return memo[id(self)]
51
62
 
52
63
  copied_attacker = Attacker(
53
- id = self.id,
54
64
  name = self.name,
65
+ attacker_id = self.id,
66
+ entry_points = set(),
67
+ reached_attack_steps = set()
55
68
  )
56
69
 
57
70
  # Remember that self was already copied
@@ -66,7 +79,7 @@ class Attacker:
66
79
 
67
80
  def compromise(self, node: AttackGraphNode) -> None:
68
81
  """
69
- Have the attacke compromise the node given as a parameter.
82
+ Have the attacker compromise the node given as a parameter.
70
83
 
71
84
  Arguments:
72
85
  node - the node that the attacker will compromise
@@ -90,8 +103,8 @@ class Attacker:
90
103
  )
91
104
  return
92
105
 
93
- node.compromised_by.append(self)
94
- self.reached_attack_steps.append(node)
106
+ node.compromised_by.add(self)
107
+ self.reached_attack_steps.add(node)
95
108
 
96
109
  def undo_compromise(self, node: AttackGraphNode) -> None:
97
110
  """