mal-toolbox 1.0.4__py3-none-any.whl → 1.0.6__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.4
2
2
  Name: mal-toolbox
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: A collection of tools used to create MAL models and attack graphs.
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>
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
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
@@ -23,6 +23,8 @@ Requires-Dist: antlr4-tools
23
23
  Requires-Dist: antlr4-python3-runtime
24
24
  Requires-Dist: docopt
25
25
  Requires-Dist: PyYAML
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest; extra == "dev"
26
28
  Dynamic: license-file
27
29
 
28
30
  # MAL Toolbox overview
@@ -30,23 +32,15 @@ Dynamic: license-file
30
32
  MAL Toolbox is a collection of python modules to help developers create and work with
31
33
  MAL ([Meta Attack Language](https://mal-lang.org/)) models and attack graphs.
32
34
 
33
- Attack graphs can be used to run simulations (see MAL Simulator) or analysis.
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.
34
36
 
35
- [Documentation](https://mal-lang.org/mal-toolbox/index.html)(Work in progress)
37
+ - [MAL Toolbox Documentation](https://mal-lang.org/mal-toolbox/index.html)
38
+ - [MAL Toolbox tutorial](https://github.com/mal-lang/mal-toolbox-tutorial)
36
39
 
37
40
  ## The Language Module
38
41
 
39
42
  The language module provides various tools to process MAL languages.
40
43
 
41
- ### The Language Specification Submodule
42
-
43
- The language specification submodule provides functions to load the
44
- specification from a .mar archive(`load_language_specification_from_mar`) or a
45
- JSON file(`load_language_specification_from_json`). This specification will
46
- then be used to generate python classes representing the assets and
47
- associations of the language and to determine the attack steps for each asset
48
- when generating the attack graph.
49
-
50
44
  ## The Model Module
51
45
 
52
46
  With a MAL language a Model (a MAL instance model) can be created either
@@ -70,12 +64,6 @@ nodes related and the asset field which will contain the object in the model
70
64
  instance to which this attack step belongs to, if this information is
71
65
  available.
72
66
 
73
- ## Ingestors Module
74
-
75
- The ingestors module contains various tools that can make use of the instance
76
- model or attack graph. Currently the Neo4J ingestor is the only one available
77
- and it can be used to visualise the instance model and the attack graph.
78
-
79
67
 
80
68
  # Usage
81
69
 
@@ -98,6 +86,11 @@ logging:
98
86
  model_file: "logs/model.yml"
99
87
  langspec_file: "logs/langspec_file.yml"
100
88
  langgraph_file: "logs/langspec_file.yml"
89
+ neo4j:
90
+ uri: None
91
+ username: None
92
+ password: None
93
+ dbname: None
101
94
  ```
102
95
 
103
96
  Alternatively, you can use the `MALTOOLBOX_CONFIG`
@@ -124,25 +117,66 @@ You can use the maltoolbox cli to:
124
117
  Command-line interface for MAL toolbox operations
125
118
 
126
119
  Usage:
127
- maltoolbox attack-graph generate [options] <model_file> <lang_file>
128
120
  maltoolbox compile <lang_file> <output_file>
121
+ maltoolbox generate-attack-graph [--graphviz] <model_file> <lang_file>
129
122
  maltoolbox upgrade-model <model_file> <lang_file> <output_file>
123
+ maltoolbox visualize-model <model_file> <lang_file>
130
124
 
131
125
  Arguments:
132
126
  <model_file> Path to JSON instance model file.
133
127
  <lang_file> Path to .mar or .mal file containing MAL spec.
134
128
  <output_file> Path to write the result of the compilation (yml/json).
135
129
 
130
+ Options:
131
+ -h --help Show this screen.
132
+ -g --graphviz Visualize with graphviz
133
+
136
134
  Notes:
137
135
  - <lang_file> can be either a .mar file (generated by the older MAL
138
- compiler) or a .mal file containing the DSL written in MAL.
136
+ compiler) or a .mal file containing the DSL written in MAL.```
139
137
  ```
140
138
 
141
139
  ## Code examples / Tutorial
142
140
 
143
- To find code examples and tutorials, visit the
141
+ To find more code examples and tutorials, visit the
144
142
  [MAL Toolbox Tutorial](https://github.com/mal-lang/mal-toolbox-tutorial/tree/main) repository.
145
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
+
146
180
  # Tests
147
181
  There are unit tests inside of ./tests.
148
182
  Before running the tests, make sure to install the requirements in ./tests/requirements.txt with `python -m pip install -r ./tests/requirements.txt`.
@@ -1,17 +1,17 @@
1
- mal_toolbox-1.0.4.dist-info/licenses/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
2
- mal_toolbox-1.0.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
3
- maltoolbox/__init__.py,sha256=Rw4utlzsR268vbMpZlmp3bDGJ80U0CWHiEXKOePe3jU,2043
4
- maltoolbox/__main__.py,sha256=A9jsYy94l1grHeSR3G3Ddn8Xg_nOihMSj1zZtVnYPSI,2974
1
+ mal_toolbox-1.0.6.dist-info/licenses/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
2
+ mal_toolbox-1.0.6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
3
+ maltoolbox/__init__.py,sha256=oQl_SJyw4c6_IgOfO7SgV5jQQ1S2bVFBSazP9DZShqU,2158
4
+ maltoolbox/__main__.py,sha256=euRQktG-HLvL1wJEj98RLnkNuDWsjBjfBOovCpqxFGw,3392
5
5
  maltoolbox/exceptions.py,sha256=0YjPx2v1yYumZ2o7pVZ1s_jS-GAb3Ng979KEFhROSNY,1399
6
6
  maltoolbox/file_utils.py,sha256=fYG3UsvPQcU0ES_WI3nLfuzSZgc0jtE4IAxdMGgs9aA,1876
7
- maltoolbox/model.py,sha256=xTK2jUr0Gz5pPVhdjh78zO5G46nV8N2ciIt6M5SwcGU,16058
7
+ maltoolbox/model.py,sha256=0z0S0SH4lkslsAxPNxb7exfmNwBPXM_BK0Oujo899a4,18139
8
8
  maltoolbox/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  maltoolbox/attackgraph/__init__.py,sha256=m_81AjzwXONdclcW_R7mF2f8p-4DvoSRVfQ3Nyh7fak,298
10
- maltoolbox/attackgraph/attackgraph.py,sha256=I2jms_X1rRLkmvkmc0z3zQAa_TnO4xA7ulPliRl5kOo,26930
11
- maltoolbox/attackgraph/node.py,sha256=Z2sdzXhPel9h7ySxP9fjgd1exVmpRbvRySVtLpI1_BM,3904
10
+ maltoolbox/attackgraph/attackgraph.py,sha256=uN4QiWQ8eu_nGpUZSNXaSUkw-lDYdlEmvUIEs5mevz4,27348
11
+ maltoolbox/attackgraph/node.py,sha256=F48FgCeYQFEiAq1HswjOnHImYSSRINZpHDvUZP0FqRk,4265
12
12
  maltoolbox/attackgraph/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  maltoolbox/language/__init__.py,sha256=TsTTryEyjChwHN1o5F2BSUlFsAss2N6J0H0-nzvXiD8,489
14
- maltoolbox/language/languagegraph.py,sha256=XWqVzHlf7EMWSkN3fQ-b6zA0pf_ysi_74YseAXAvSbs,71274
14
+ maltoolbox/language/languagegraph.py,sha256=fNQRSDE_XKQMGgxZe75kpBbLpqjhDD5guAmmx9nSZR0,72143
15
15
  maltoolbox/language/compiler/__init__.py,sha256=JQyAgDwJh1pU7AmuOhd1-d2b2PYXpgMVPtxnav8QHVc,15872
16
16
  maltoolbox/language/compiler/mal_lexer.py,sha256=BeifykDAt4PloRASOaLzBgWF35ev_zgD8lXMIsSHykc,12063
17
17
  maltoolbox/language/compiler/mal_parser.py,sha256=sUoaE43l2VKg-Dou30mk2wlVS1FvdOREwHNIyFe4IkY,114699
@@ -20,10 +20,11 @@ maltoolbox/patternfinder/attackgraph_patterns.py,sha256=jgW7UG1yBJ08jvuokdpSiHm7
20
20
  maltoolbox/translators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  maltoolbox/translators/securicad.py,sha256=F_rndv2JyKxfHAXPwf2RrdiFPnemJVArYUpVsFP6QQk,6997
22
22
  maltoolbox/translators/updater.py,sha256=UZPnx22udROiocCcSmtrgUJUupkjktkxl-M7rhBxUPc,8660
23
- maltoolbox/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ maltoolbox/visualization/__init__.py,sha256=wEPJBVXOgN4PCewjzCWcl6qFAaZS5SKm-8I89UJ89h4,253
24
24
  maltoolbox/visualization/graphviz_utils.py,sha256=dfQhPL6Z2hvlMFpThsDr-5tm4Pa22SGHEiXw5ym9JJc,3906
25
- mal_toolbox-1.0.4.dist-info/METADATA,sha256=SbesLITkX_biOanT4o22POBe74UNEjZWBLOJY1P2xS0,5382
26
- mal_toolbox-1.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- mal_toolbox-1.0.4.dist-info/entry_points.txt,sha256=oqby5O6cUP_OHCm70k_iYPA6UlbTBf7se1i3XwdK3uU,56
28
- mal_toolbox-1.0.4.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
29
- mal_toolbox-1.0.4.dist-info/RECORD,,
25
+ maltoolbox/visualization/neo4j_utils.py,sha256=FHTkkaTvlrf36RXdVdD65TYtlKLNcna9pCl39d6o8wg,3487
26
+ mal_toolbox-1.0.6.dist-info/METADATA,sha256=E3557rDCp3rQwlv58FMjfXBHBb-y7sRNj5M1bZjKvi4,5938
27
+ mal_toolbox-1.0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ mal_toolbox-1.0.6.dist-info/entry_points.txt,sha256=oqby5O6cUP_OHCm70k_iYPA6UlbTBf7se1i3XwdK3uU,56
29
+ mal_toolbox-1.0.6.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
30
+ mal_toolbox-1.0.6.dist-info/RECORD,,
maltoolbox/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # MAL Toolbox v1.0.4
2
+ # MAL Toolbox v1.0.6
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.4"
24
+ __version__ = "1.0.6"
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
  )
maltoolbox/__main__.py CHANGED
@@ -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] <model_file> <lang_file>
9
9
 
10
10
  Arguments:
11
11
  <model_file> Path to JSON instance model file.
@@ -15,6 +15,7 @@ Arguments:
15
15
  Options:
16
16
  -h --help Show this screen.
17
17
  -g --graphviz Visualize with graphviz
18
+ -n --neo4j Send to neo4j
18
19
 
19
20
  Notes:
20
21
  - <lang_file> can be either a .mar file (generated by the older MAL
@@ -25,12 +26,17 @@ import logging
25
26
  import json
26
27
  import docopt
27
28
 
28
- from . import log_configs
29
+ from . import log_configs, neo4j_configs
29
30
  from .attackgraph import create_attack_graph, AttackGraph
30
31
  from .language.compiler import MalCompiler
31
32
  from .language.languagegraph import LanguageGraph
32
33
  from .translators.updater import load_model_from_older_version
33
- from .visualization.graphviz_utils import render_model, render_attack_graph
34
+ from .visualization import (
35
+ render_model,
36
+ render_attack_graph,
37
+ ingest_model_neo4j,
38
+ ingest_attack_graph_neo4j
39
+ )
34
40
  from .model import Model
35
41
 
36
42
  logger = logging.getLogger(__name__)
@@ -78,6 +84,8 @@ def main():
78
84
  )
79
85
  if args['--graphviz']:
80
86
  render_attack_graph(attack_graph)
87
+ if args['--neo4j']:
88
+ ingest_attack_graph_neo4j(attack_graph, neo4j_configs)
81
89
 
82
90
  elif args['compile']:
83
91
  compile(
@@ -90,7 +98,12 @@ def main():
90
98
  elif args['visualize-model']:
91
99
  lang_graph = LanguageGraph.load_from_file(args['<lang_file>'])
92
100
  model = Model.load_from_file(args['<model_file>'], lang_graph)
93
- render_model(model)
101
+ if args['--graphviz']:
102
+ render_model(model)
103
+ else:
104
+ print("Use flag --graphviz to generate a pdf")
105
+ if args['--neo4j']:
106
+ ingest_model_neo4j(model, neo4j_configs)
94
107
 
95
108
  if __name__ == "__main__":
96
109
  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.values():
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
@@ -192,7 +192,13 @@ class AttackGraph():
192
192
  node_id = node_dict['id'],
193
193
  model_asset = node_asset,
194
194
  ttc_dist = node_dict['ttc'],
195
- existence_status = node_dict.get('existence_status', None)
195
+ existence_status = (
196
+ bool(node_dict['existence_status'])
197
+ if 'existence_status' in node_dict else None
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
196
202
  )
197
203
  ag_node.tags = list(node_dict.get('tags', []))
198
204
  ag_node.extras = node_dict.get('extras', {})
@@ -608,7 +614,8 @@ class AttackGraph():
608
614
  node_id: Optional[int] = None,
609
615
  model_asset: Optional[ModelAsset] = None,
610
616
  ttc_dist: Optional[dict] = None,
611
- existence_status: Optional[bool] = None
617
+ existence_status: Optional[bool] = None,
618
+ full_name: Optional[str] = None
612
619
  ) -> AttackGraphNode:
613
620
  """Create and add a node to the graph
614
621
  Arguments:
@@ -653,7 +660,8 @@ class AttackGraph():
653
660
  lg_attack_step = lg_attack_step,
654
661
  model_asset = model_asset,
655
662
  ttc_dist = ttc_dist,
656
- existence_status = existence_status
663
+ existence_status = existence_status,
664
+ full_name = full_name
657
665
  )
658
666
 
659
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': {child.id: child.full_name for child in
49
- self.children},
50
- 'parents': {parent.id: parent.full_name for parent in
51
- self.parents},
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():
@@ -56,7 +60,7 @@ class AttackGraphNode:
56
60
  if self.model_asset is not None:
57
61
  node_dict['asset'] = str(self.model_asset.name)
58
62
  if self.existence_status is not None:
59
- node_dict['existence_status'] = str(self.existence_status)
63
+ node_dict['existence_status'] = self.existence_status
60
64
  if self.tags:
61
65
  node_dict['tags'] = list(self.tags)
62
66
  if self.extras:
@@ -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 combination of the
109
- asset name to which the attack step belongs and attack step name
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.model_asset:
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
 
@@ -10,7 +10,7 @@ import zipfile
10
10
 
11
11
  from dataclasses import dataclass, field
12
12
  from functools import cached_property
13
- from typing import Any, Optional
13
+ from typing import Any, Literal, Optional
14
14
 
15
15
  from maltoolbox.file_utils import (
16
16
  load_dict_from_yaml_file, load_dict_from_json_file,
@@ -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]:
@@ -222,7 +234,7 @@ class LanguageGraphAsset:
222
234
  return self_superassets.intersection(other_superassets)
223
235
 
224
236
 
225
- @dataclass
237
+ @dataclass(frozen=True)
226
238
  class LanguageGraphAssociationField:
227
239
  """A field in an association"""
228
240
  asset: LanguageGraphAsset
@@ -231,7 +243,7 @@ class LanguageGraphAssociationField:
231
243
  maximum: int
232
244
 
233
245
 
234
- @dataclass
246
+ @dataclass(frozen=True, eq=True)
235
247
  class LanguageGraphAssociation:
236
248
  """
237
249
  An association type between asset types as defined in the MAL language
@@ -239,7 +251,7 @@ class LanguageGraphAssociation:
239
251
  name: str
240
252
  left_field: LanguageGraphAssociationField
241
253
  right_field: LanguageGraphAssociationField
242
- info: dict = field(default_factory = dict)
254
+ info: dict = field(default_factory = dict, compare=False)
243
255
 
244
256
  def to_dict(self) -> dict:
245
257
  """Convert LanguageGraphAssociation to dictionary"""
@@ -356,7 +368,7 @@ class LanguageGraphAttackStep:
356
368
  An attack step belonging to an asset type in the MAL language
357
369
  """
358
370
  name: str
359
- type: str
371
+ type: Literal["or", "and", "defense", "exist", "notExist"]
360
372
  asset: LanguageGraphAsset
361
373
  ttc: Optional[dict] = field(default_factory = dict)
362
374
  overrides: bool = False
@@ -739,6 +751,13 @@ class LanguageGraph():
739
751
 
740
752
  return serialized_graph
741
753
 
754
+ @property
755
+ def associations(self) -> set[LanguageGraphAssociation]:
756
+ """
757
+ Return all associations in the language graph.
758
+ """
759
+ return {assoc for asset in self.assets.values() for assoc in asset.associations.values()}
760
+
742
761
  @staticmethod
743
762
  def _link_association_to_assets(
744
763
  assoc: LanguageGraphAssociation,
maltoolbox/model.py CHANGED
@@ -3,7 +3,6 @@ MAL-Toolbox Model Module
3
3
  """
4
4
 
5
5
  from __future__ import annotations
6
- from dataclasses import dataclass, field
7
6
  import json
8
7
  import logging
9
8
  from typing import TYPE_CHECKING
@@ -23,6 +22,7 @@ if TYPE_CHECKING:
23
22
  from .language import (
24
23
  LanguageGraph,
25
24
  LanguageGraphAsset,
25
+ LanguageGraphAssociation
26
26
  )
27
27
 
28
28
  logger = logging.getLogger(__name__)
@@ -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] = {
@@ -361,6 +370,34 @@ class ModelAsset:
361
370
  return (f'ModelAsset(name: "{self.name}", id: {self.id}, '
362
371
  f'type: {self.type})')
363
372
 
373
+ def associations_with(
374
+ self, b: ModelAsset
375
+ ) -> set[LanguageGraphAssociation]:
376
+ """Returns all associations from self to `b`"""
377
+ assocs_in_common = set()
378
+ for assoc in self.lg_asset.associations.values():
379
+ assets_to_left = self.associated_assets.get(
380
+ assoc.left_field.fieldname, set()
381
+ )
382
+ assets_to_right = self.associated_assets.get(
383
+ assoc.right_field.fieldname, set()
384
+ )
385
+ if b in assets_to_left or b in assets_to_right:
386
+ assocs_in_common.add(assoc)
387
+
388
+ return assocs_in_common
389
+
390
+ def has_association_with(self, b: ModelAsset, assoc_name: str) -> bool:
391
+ """
392
+ Returns True if association `assoc_name` exists between self and `b`
393
+ """
394
+
395
+ for fieldname, associated_assets in self.associated_assets.items():
396
+ assoc = self.lg_asset.associations[fieldname]
397
+ if assoc.name == assoc_name and b in associated_assets:
398
+ return True
399
+
400
+ return False
364
401
 
365
402
  def validate_associated_assets(
366
403
  self, fieldname: str, assets_to_add: set[ModelAsset]
@@ -418,6 +455,20 @@ class ModelAsset:
418
455
  assets dictionary entry corresponding to the given fieldname.
419
456
  """
420
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
+
421
472
  lg_assoc = self.lg_asset.associations[fieldname]
422
473
  other_fieldname = lg_assoc.get_opposite_fieldname(fieldname)
423
474
 
@@ -0,0 +1,9 @@
1
+ from .graphviz_utils import render_attack_graph, render_model
2
+ from .neo4j_utils import ingest_attack_graph_neo4j, ingest_model_neo4j
3
+
4
+ __all__ = [
5
+ 'render_attack_graph',
6
+ 'render_model',
7
+ 'ingest_attack_graph_neo4j',
8
+ 'ingest_model_neo4j'
9
+ ]
@@ -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)