mal-toolbox 0.1.2__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {mal_toolbox-0.1.2/mal_toolbox.egg-info → mal_toolbox-0.1.4}/PKG-INFO +2 -2
  2. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4/mal_toolbox.egg-info}/PKG-INFO +2 -2
  3. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/mal_toolbox.egg-info/SOURCES.txt +0 -1
  4. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/mal_toolbox.egg-info/requires.txt +1 -1
  5. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/__init__.py +2 -2
  6. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/attackgraph/attackgraph.py +49 -26
  7. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/attackgraph/node.py +1 -1
  8. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/ingestors/neo4j.py +32 -18
  9. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/language/classes_factory.py +57 -1
  10. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/language/languagegraph.py +89 -2
  11. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/model.py +4 -1
  12. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/translators/securicad.py +34 -26
  13. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/pyproject.toml +2 -2
  14. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/tests/test_model.py +46 -49
  15. mal_toolbox-0.1.2/maltoolbox/language/specification.py +0 -90
  16. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/AUTHORS +0 -0
  17. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/LICENSE +0 -0
  18. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/README.md +0 -0
  19. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/mal_toolbox.egg-info/dependency_links.txt +0 -0
  20. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/mal_toolbox.egg-info/top_level.txt +0 -0
  21. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/__main__.py +0 -0
  22. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/attackgraph/__init__.py +0 -0
  23. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/attackgraph/analyzers/__init__.py +0 -0
  24. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/attackgraph/analyzers/apriori.py +0 -0
  25. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/attackgraph/attacker.py +0 -0
  26. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/attackgraph/query.py +0 -0
  27. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/default.conf +0 -0
  28. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/exceptions.py +0 -0
  29. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/file_utils.py +0 -0
  30. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/ingestors/__init__.py +0 -0
  31. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/language/__init__.py +0 -0
  32. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/language/compiler/__init__.py +0 -0
  33. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/language/compiler/mal_lexer.py +0 -0
  34. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/language/compiler/mal_parser.py +0 -0
  35. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/language/compiler/mal_visitor.py +0 -0
  36. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/translators/__init__.py +0 -0
  37. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/translators/updater.py +0 -0
  38. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/maltoolbox/wrappers.py +0 -0
  39. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/setup.cfg +0 -0
  40. {mal_toolbox-0.1.2 → mal_toolbox-0.1.4}/tests/test_wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mal-toolbox
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A collection of tools used to create MAL models and attack graphs.
5
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
@@ -19,7 +19,7 @@ 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.4.1
22
+ Requires-Dist: python-jsonschema-objects>=0.5.5
23
23
  Requires-Dist: antlr4-tools
24
24
  Requires-Dist: antlr4-python3-runtime
25
25
  Requires-Dist: docopt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mal-toolbox
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A collection of tools used to create MAL models and attack graphs.
5
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
@@ -19,7 +19,7 @@ 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.4.1
22
+ Requires-Dist: python-jsonschema-objects>=0.5.5
23
23
  Requires-Dist: antlr4-tools
24
24
  Requires-Dist: antlr4-python3-runtime
25
25
  Requires-Dist: docopt
@@ -26,7 +26,6 @@ maltoolbox/ingestors/neo4j.py
26
26
  maltoolbox/language/__init__.py
27
27
  maltoolbox/language/classes_factory.py
28
28
  maltoolbox/language/languagegraph.py
29
- maltoolbox/language/specification.py
30
29
  maltoolbox/language/compiler/__init__.py
31
30
  maltoolbox/language/compiler/mal_lexer.py
32
31
  maltoolbox/language/compiler/mal_parser.py
@@ -1,5 +1,5 @@
1
1
  py2neo>=2021.2.3
2
- python-jsonschema-objects>=0.4.1
2
+ python-jsonschema-objects>=0.5.5
3
3
  antlr4-tools
4
4
  antlr4-python3-runtime
5
5
  docopt
@@ -1,5 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # MAL Toolbox v0.1.2
2
+ # MAL Toolbox v0.1.4
3
3
  # Copyright 2024, 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__ = '0.1.2'
24
+ __version__ = '0.1.4'
25
25
  __authors__ = ['Andrei Buhaiu',
26
26
  'Giuseppe Nebbione',
27
27
  'Nikolaos Kakouros',
@@ -10,7 +10,6 @@ from typing import TYPE_CHECKING
10
10
  from .node import AttackGraphNode
11
11
  from .attacker import Attacker
12
12
  from ..exceptions import AttackGraphStepExpressionError
13
- from ..language import specification
14
13
  from ..model import Model
15
14
  from ..exceptions import AttackGraphException
16
15
  from ..file_utils import (
@@ -37,7 +36,8 @@ def _process_step_expression(
37
36
  Recursively process an attack step expression.
38
37
 
39
38
  Arguments:
40
- lang - a dictionary representing the MAL language specification
39
+ lang_graph - a language graph representing the MAL language
40
+ specification
41
41
  model - a maltoolbox.model.Model instance from which the attack
42
42
  graph was generated
43
43
  target_assets - the list of assets that this step expression should apply
@@ -150,11 +150,28 @@ def _process_step_expression(
150
150
  step_expression['stepExpression'])
151
151
  new_target_assets.extend(assets)
152
152
 
153
- selected_new_target_assets = [asset for asset in \
154
- new_target_assets if specification.extends_asset(
155
- lang_graph._lang_spec,
156
- asset.type,
157
- step_expression['subType'])]
153
+ selected_new_target_assets = []
154
+ for asset in new_target_assets:
155
+ lang_graph_asset = lang_graph.get_asset_by_name(
156
+ asset.type
157
+ )
158
+ if not lang_graph_asset:
159
+ raise LookupError(
160
+ f'Failed to find asset \"{asset.type}\" in the '
161
+ 'language graph.'
162
+ )
163
+ lang_graph_subtype_asset = lang_graph.get_asset_by_name(
164
+ step_expression['subType']
165
+ )
166
+ if not lang_graph_subtype_asset:
167
+ raise LookupError(
168
+ 'Failed to find asset '
169
+ f'\"{step_expression["subType"]}\" in the '
170
+ 'language graph.'
171
+ )
172
+ if lang_graph_asset.is_subasset_of(lang_graph_subtype_asset):
173
+ selected_new_target_assets.append(asset)
174
+
158
175
  return (selected_new_target_assets, None)
159
176
 
160
177
  case 'collect':
@@ -225,17 +242,39 @@ class AttackGraph():
225
242
  """
226
243
 
227
244
  attack_graph = AttackGraph()
245
+ attack_graph.model = model
228
246
  serialized_attack_steps = serialized_object['attack_steps']
229
247
  serialized_attackers = serialized_object['attackers']
230
248
 
231
249
  # Create all of the nodes in the imported attack graph.
232
250
  for node_full_name, node_dict in serialized_attack_steps.items():
251
+
252
+ # Recreate asset links if model is available.
253
+ node_asset = None
254
+ if model and 'asset' in node_dict:
255
+ node_asset = model.get_asset_by_name(node_dict['asset'])
256
+ if node_asset is None:
257
+ msg = ('Failed to find asset with id %s'
258
+ 'when loading from attack graph dict')
259
+ logger.error(msg, node_dict["asset"])
260
+ raise LookupError(msg % node_dict["asset"])
261
+
233
262
  ag_node = AttackGraphNode(
234
263
  type=node_dict['type'],
235
264
  name=node_dict['name'],
236
- ttc=node_dict['ttc']
265
+ ttc=node_dict['ttc'],
266
+ asset=node_asset
237
267
  )
238
268
 
269
+ if node_asset:
270
+ # Add AttackGraphNode to attack_step_nodes of asset
271
+ if hasattr(node_asset, 'attack_step_nodes'):
272
+ node_attack_steps = list(node_asset.attack_step_nodes)
273
+ node_attack_steps.append(ag_node)
274
+ node_asset.attack_step_nodes = node_attack_steps
275
+ else:
276
+ node_asset.attack_step_nodes = [ag_node]
277
+
239
278
  ag_node.defense_status = float(node_dict['defense_status']) if \
240
279
  'defense_status' in node_dict else None
241
280
  ag_node.existence_status = node_dict['existence_status'] \
@@ -249,7 +288,8 @@ class AttackGraph():
249
288
  ag_node.tags = node_dict['tags'] if \
250
289
  'tags' in node_dict else []
251
290
 
252
- attack_graph.add_node(ag_node, node_id = node_dict['id'])
291
+ # Add AttackGraphNode to AttackGraph
292
+ attack_graph.add_node(ag_node, node_id=node_dict['id'])
253
293
 
254
294
  # Re-establish links between nodes.
255
295
  for node_full_name, node_dict in serialized_attack_steps.items():
@@ -278,23 +318,6 @@ class AttackGraph():
278
318
  raise LookupError(msg % parent_id)
279
319
  _ag_node.parents.append(parent)
280
320
 
281
- # Also recreate asset links if model is available.
282
- if model and 'asset' in node_dict:
283
- asset = model.get_asset_by_name(
284
- node_dict['asset'])
285
- if asset is None:
286
- msg = ('Failed to find asset with id %s'
287
- 'when loading from attack graph dict')
288
- logger.error(msg, node_dict["asset"])
289
- raise LookupError(msg % node_dict["asset"])
290
- _ag_node.asset = asset
291
- if hasattr(asset, 'attack_step_nodes'):
292
- attack_step_nodes = list(asset.attack_step_nodes)
293
- attack_step_nodes.append(_ag_node)
294
- asset.attack_step_nodes = attack_step_nodes
295
- else:
296
- asset.attack_step_nodes = [_ag_node]
297
-
298
321
  for attacker_name, attacker in serialized_attackers.items():
299
322
  ag_attacker = Attacker(
300
323
  name = attacker['name'],
@@ -28,7 +28,7 @@ class AttackGraphNode:
28
28
  attributes: Optional[dict] = None
29
29
 
30
30
  # Optional extra metadata for AttackGraphNode
31
- extras: Optional[dict] = None
31
+ extras: dict = field(default_factory=dict)
32
32
 
33
33
  def to_dict(self) -> dict:
34
34
  """Convert node to dictionary"""
@@ -8,7 +8,7 @@ import logging
8
8
  from py2neo import Graph, Node, Relationship, Subgraph
9
9
 
10
10
  from ..model import AttackerAttachment, Model
11
- from ..language import specification, LanguageClassesFactory
11
+ from ..language import LanguageGraph, LanguageClassesFactory
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
@@ -123,7 +123,7 @@ def get_model(
123
123
  username: str,
124
124
  password: str,
125
125
  dbname: str,
126
- lang_spec: dict,
126
+ lang_graph: LanguageGraph,
127
127
  lang_classes_factory: LanguageClassesFactory
128
128
  ) -> Model:
129
129
  """Load a model from Neo4j"""
@@ -184,7 +184,7 @@ def get_model(
184
184
  target_id = right_id
185
185
  target_prop = left_field
186
186
 
187
- if attacker_id:
187
+ if attacker_id is not None:
188
188
  attacker = instance_model.get_attacker_by_id(attacker_id)
189
189
  if not attacker:
190
190
  msg = 'Failed to find attacker with id %s in model!'
@@ -192,7 +192,7 @@ def get_model(
192
192
  raise LookupError(msg % attacker_id)
193
193
  target_asset = instance_model.get_asset_by_id(target_id)
194
194
  if not target_asset:
195
- msg = 'Failed to find asset with id %s in model!'
195
+ msg = 'Failed to find asset with id %d in model!'
196
196
  logger.error(msg, target_id)
197
197
  raise LookupError(msg % target_id)
198
198
  attacker.entry_points.append((target_asset,
@@ -200,25 +200,23 @@ def get_model(
200
200
  continue
201
201
 
202
202
  left_asset = instance_model.get_asset_by_id(left_id)
203
- if not left_asset:
204
- msg = 'Failed to find asset with id %s in model!'
203
+ if left_asset is None:
204
+ msg = 'Failed to find asset with id %d in model!'
205
205
  logger.error(msg, left_id)
206
206
  raise LookupError(msg % left_id)
207
207
  right_asset = instance_model.get_asset_by_id(right_id)
208
- if not right_asset:
209
- msg = 'Failed to find asset with id %s in model!'
208
+ if right_asset is None:
209
+ msg = 'Failed to find asset with id %d in model!'
210
210
  logger.error(msg, right_id)
211
211
  raise LookupError(msg % right_id)
212
212
 
213
- assoc_name = specification.get_association_by_fields_and_assets(
214
- lang_spec,
213
+ assoc = lang_graph.get_association_by_fields_and_assets(
215
214
  left_field,
216
215
  right_field,
217
216
  left_asset.type,
218
217
  right_asset.type)
219
- logger.debug('Found "%s" association.', assoc_name)
220
218
 
221
- if not assoc_name:
219
+ if not assoc:
222
220
  logger.error(
223
221
  'Failed to find ("%s", "%s", "%s", "%s")'
224
222
  'association in language specification!',
@@ -227,15 +225,31 @@ def get_model(
227
225
  )
228
226
  return None
229
227
 
230
- if not hasattr(lang_classes_factory.ns,
231
- assoc_name):
232
- msg = 'Failed to find %s association in language specification!'
233
- logger.error(msg, assoc_name)
234
- raise LookupError(msg % assoc_name)
228
+ logger.debug('Found "%s" association.', assoc.name)
229
+
230
+ assoc_name = lang_classes_factory.get_association_by_signature(
231
+ assoc.name,
232
+ left_asset.type,
233
+ right_asset.type
234
+ )
235
+
236
+ if not assoc_name:
237
+ msg = 'Failed to find \"%s\" association in language specification!'
238
+ logger.error(msg, assoc.name)
239
+ raise LookupError(msg % assoc.name)
235
240
 
236
241
  assoc = getattr(lang_classes_factory.ns, assoc_name)()
237
242
  setattr(assoc, left_field, [left_asset])
238
243
  setattr(assoc, right_field, [right_asset])
239
- instance_model.add_association(assoc)
244
+ if not (instance_model.association_exists_between_assets(
245
+ assoc_name,
246
+ left_asset,
247
+ right_asset
248
+ ) or instance_model.association_exists_between_assets(
249
+ assoc_name,
250
+ right_asset,
251
+ left_asset
252
+ )):
253
+ instance_model.add_association(assoc)
240
254
 
241
255
  return instance_model
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING
9
9
  import python_jsonschema_objects as pjs
10
10
 
11
11
  if TYPE_CHECKING:
12
- from typing import Literal, TypeAlias
12
+ from typing import Literal, Optional, TypeAlias
13
13
  from maltoolbox.language import LanguageGraph
14
14
  from python_jsonschema_objects.classbuilder import ProtocolBase
15
15
 
@@ -184,3 +184,59 @@ class LanguageClassesFactory:
184
184
  # Once we have the JSON schema we create the actual classes.
185
185
  builder = pjs.ObjectBuilder(self.json_schema)
186
186
  self.ns = builder.build_classes(standardize_names=False)
187
+
188
+ def get_association_by_signature(
189
+ self,
190
+ assoc_name: str,
191
+ left_asset: str,
192
+ right_asset: str
193
+ ) -> Optional[str]:
194
+ """
195
+ Get association name based on its signature. This is primarily
196
+ relevant for getting the exact association full name when multiple
197
+ associations with the same name exist.
198
+
199
+ Arguments:
200
+ assoc_name - the association name
201
+ left_asset - the name of the left asset type
202
+ right_asset - the name of the right asset type
203
+
204
+ Return: The matching association name if a match is found.
205
+ None if there is no match.
206
+ """
207
+ lang_assocs_entries = self.json_schema['definitions']\
208
+ ['LanguageAssociation']['definitions']
209
+ if not assoc_name in lang_assocs_entries:
210
+ raise LookupError(
211
+ 'Failed to find "%s" association in the language json '
212
+ 'schema.' % assoc_name
213
+ )
214
+ assoc_entry = lang_assocs_entries[assoc_name]
215
+ # If the association has a oneOf property it should always have more
216
+ # than just one alternative, but check just in case
217
+ if 'definitions' in assoc_entry and \
218
+ len(assoc_entry['definitions']) > 1:
219
+ full_name = '%s_%s_%s' % (
220
+ assoc_name,
221
+ left_asset,
222
+ right_asset
223
+ )
224
+ full_name_flipped = '%s_%s_%s' % (
225
+ assoc_name,
226
+ right_asset,
227
+ left_asset
228
+ )
229
+ if not full_name in assoc_entry['definitions']:
230
+ if not full_name_flipped in assoc_entry['definitions']:
231
+ raise LookupError(
232
+ 'Failed to find "%s" or "%s" association in the '
233
+ 'language json schema.'
234
+ % (full_name,
235
+ full_name_flipped)
236
+ )
237
+ else:
238
+ return full_name_flipped
239
+ else:
240
+ return full_name
241
+ else:
242
+ return assoc_name
@@ -1000,8 +1000,17 @@ class LanguageGraph():
1000
1000
  elif step['reaches']['overrides'] == True:
1001
1001
  attack_steps[step['name']] = copy.deepcopy(step)
1002
1002
  else:
1003
- attack_steps[step['name']]['reaches']['stepExpressions'].\
1004
- extend(step['reaches']['stepExpressions'])
1003
+ if attack_steps[step['name']]['reaches'] is not None and \
1004
+ 'stepExpressions' in \
1005
+ attack_steps[step['name']]['reaches']:
1006
+ attack_steps[step['name']]['reaches']['stepExpressions'].\
1007
+ extend(step['reaches']['stepExpressions'])
1008
+ else:
1009
+ attack_steps[step['name']]['reaches'] = {
1010
+ 'overrides': False,
1011
+ 'stepExpressions': step['reaches']['stepExpressions']
1012
+ }
1013
+
1005
1014
 
1006
1015
  return attack_steps
1007
1016
 
@@ -1096,3 +1105,81 @@ class LanguageGraph():
1096
1105
  self.associations = []
1097
1106
  self.attack_steps = []
1098
1107
  self._generate_graph()
1108
+
1109
+ def get_asset_by_name(
1110
+ self,
1111
+ asset_name
1112
+ ) -> Optional[LanguageGraphAsset]:
1113
+ """
1114
+ Get an asset based on its name
1115
+
1116
+ Arguments:
1117
+ asset_name - a string containing the asset name
1118
+
1119
+ Return:
1120
+ The asset matching the name.
1121
+ None if there is no match.
1122
+ """
1123
+ for asset in self.assets:
1124
+ if asset.name == asset_name:
1125
+ return asset
1126
+
1127
+ return None
1128
+
1129
+ def get_association_by_fields_and_assets(
1130
+ self,
1131
+ first_field: str,
1132
+ second_field: str,
1133
+ first_asset_name: str,
1134
+ second_asset_name: str
1135
+ ) -> Optional[LanguageGraphAssociation]:
1136
+ """
1137
+ Get an association based on its field names and asset types
1138
+
1139
+ Arguments:
1140
+ first_field - a string containing the first field
1141
+ second_field - a string containing the second field
1142
+ first_asset_name - a string representing the first asset type
1143
+ second_asset_name - a string representing the second asset type
1144
+
1145
+ Return:
1146
+ The association matching the fieldnames and asset types.
1147
+ None if there is no match.
1148
+ """
1149
+ first_asset = self.get_asset_by_name(first_asset_name)
1150
+ if first_asset is None:
1151
+ raise LookupError(
1152
+ f'Failed to find asset with name \"{first_asset_name}\" in '
1153
+ 'the language graph.'
1154
+ )
1155
+
1156
+ second_asset = self.get_asset_by_name(second_asset_name)
1157
+ if second_asset is None:
1158
+ raise LookupError(
1159
+ f'Failed to find asset with name \"{second_asset_name}\" in '
1160
+ 'the language graph.'
1161
+ )
1162
+
1163
+ for assoc in self.associations:
1164
+ logger.debug(
1165
+ 'Compare ("%s", "%s", "%s", "%s") to ("%s", "%s", "%s", "%s").',
1166
+ first_asset_name, first_field,
1167
+ second_asset_name, second_field,
1168
+ assoc.left_field.asset.name, assoc.left_field.fieldname,
1169
+ assoc.right_field.asset.name, assoc.right_field.fieldname
1170
+ )
1171
+
1172
+ # If the asset and fields match either way we accept it as a match.
1173
+ if assoc.left_field.fieldname == first_field and \
1174
+ assoc.right_field.fieldname == second_field and \
1175
+ first_asset.is_subasset_of(assoc.left_field.asset) and \
1176
+ second_asset.is_subasset_of(assoc.right_field.asset):
1177
+ return assoc
1178
+
1179
+ if assoc.left_field.fieldname == second_field and \
1180
+ assoc.right_field.fieldname == first_field and \
1181
+ second_asset.is_subasset_of(assoc.left_field.asset) and \
1182
+ first_asset.is_subasset_of(assoc.right_field.asset):
1183
+ return assoc
1184
+
1185
+ return None
@@ -400,7 +400,10 @@ class Model():
400
400
  )
401
401
 
402
402
  def association_exists_between_assets(
403
- self, association_type, left_asset, right_asset
403
+ self,
404
+ association_type: str,
405
+ left_asset: SchemaGeneratedClass,
406
+ right_asset: SchemaGeneratedClass
404
407
  ):
405
408
  """Return True if the association already exists between the assets"""
406
409
  logger.debug(
@@ -7,26 +7,28 @@ import json
7
7
  import logging
8
8
  import xml.etree.ElementTree as ET
9
9
 
10
+ from typing import Optional
11
+
10
12
  from ..model import AttackerAttachment, Model
11
- from ..language import specification, LanguageClassesFactory
13
+ from ..language import LanguageGraph, LanguageClassesFactory
12
14
 
13
15
  logger = logging.getLogger(__name__)
14
16
 
15
17
  def load_model_from_scad_archive(
16
18
  scad_archive: str,
17
- lang_spec: dict,
19
+ lang_graph: LanguageGraph,
18
20
  lang_classes_factory: LanguageClassesFactory
19
- ) -> Model:
21
+ ) -> Optional[Model]:
20
22
  """
21
23
  Reads a '.sCAD' archive generated by securiCAD representing an instance
22
24
  model and loads the information into a maltoobox.model.Model object.
23
25
 
24
26
  Arguments:
25
27
  scad_archive - the path to a '.sCAD' archive
26
- lang_spec - a dictionary containing the MAL language
27
- specification
28
+ lang_graph - a language graph representing the MAL
29
+ language specification
28
30
  lang_classes_factory - a language classes factory that contains
29
- the same classes defined by the
31
+ the classes defined by the
30
32
  language specification
31
33
 
32
34
  Return:
@@ -39,7 +41,6 @@ def load_model_from_scad_archive(
39
41
  root = ET.fromstring(scad_model)
40
42
 
41
43
  instance_model = Model(scad_archive,
42
- lang_spec,
43
44
  lang_classes_factory)
44
45
 
45
46
  for child in root.iter('objects'):
@@ -52,10 +53,13 @@ def load_model_from_scad_archive(
52
53
  )
53
54
 
54
55
  if child.attrib['metaConcept'] == 'Attacker':
55
- attacker_id = int(child.attrib['id'])
56
- attacker = AttackerAttachment()
57
- attacker.entry_points = []
58
- instance_model.add_attacker(attacker, attacker_id = attacker_id)
56
+ attacker_obj_id = int(child.attrib['id'])
57
+ attacker_at = AttackerAttachment()
58
+ attacker_at.entry_points = []
59
+ instance_model.add_attacker(
60
+ attacker_at,
61
+ attacker_id = attacker_obj_id
62
+ )
59
63
  continue
60
64
 
61
65
  if not hasattr(lang_classes_factory.ns,
@@ -100,7 +104,7 @@ def load_model_from_scad_archive(
100
104
  target_id = right_id
101
105
  target_prop = child.attrib['sourceProperty']
102
106
 
103
- if attacker_id:
107
+ if attacker_id is not None:
104
108
  attacker = instance_model.get_attacker_by_id(attacker_id)
105
109
  if not attacker:
106
110
  logger.error(
@@ -137,28 +141,32 @@ def load_model_from_scad_archive(
137
141
  # matches the target field and vice versa.
138
142
  left_field = child.attrib['sourceProperty']
139
143
  right_field = child.attrib['targetProperty']
140
- assoc_name = specification.get_association_by_fields_and_assets(
141
- lang_spec,
144
+ lang_graph_assoc = lang_graph.get_association_by_fields_and_assets(
142
145
  left_field,
143
146
  right_field,
144
- left_asset.metaconcept,
145
- right_asset.metaconcept)
146
- logger.debug('Found "%s" association.', assoc_name)
147
+ left_asset.type,
148
+ right_asset.type)
147
149
 
148
- if not assoc_name:
149
- logger.error(
150
+ if not lang_graph_assoc:
151
+ raise LookupError(
150
152
  'Failed to find ("%s", "%s", "%s", "%s")'
151
- 'association in lang specification',
152
- left_asset.metaconcept, right_asset.metaconcept,
153
- left_field, right_field
153
+ 'association in lang specification.' %
154
+ (left_asset.type, right_asset.type,
155
+ left_field, right_field)
154
156
  )
155
157
  return None
156
158
 
157
- if not hasattr(lang_classes_factory.ns,
158
- assoc_name):
159
+ logger.debug('Found "%s" association.', lang_graph_assoc.name)
160
+ assoc_name = lang_classes_factory.get_association_by_signature(
161
+ lang_graph_assoc.name,
162
+ left_asset.type,
163
+ right_asset.type
164
+ )
165
+
166
+ if assoc_name is None:
159
167
  logger.error(
160
- 'Failed to find %s association in language specification!',
161
- assoc_name
168
+ 'Failed to find association with name \"%s\" in model!',
169
+ lang_graph_assoc.name
162
170
  )
163
171
  return None
164
172
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mal-toolbox"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  authors = [
5
5
  { name="Andrei Buhaiu", email="buhaiu@kth.se" },
6
6
  { name="Giuseppe Nebbione", email="nebbione@kth.se" },
@@ -13,7 +13,7 @@ readme = "README.md"
13
13
  requires-python = ">=3.10"
14
14
  dependencies = [
15
15
  "py2neo>=2021.2.3",
16
- "python-jsonschema-objects>=0.4.1",
16
+ "python-jsonschema-objects>=0.5.5",
17
17
  "antlr4-tools",
18
18
  "antlr4-python3-runtime",
19
19
  "docopt",
@@ -505,55 +505,52 @@ def test_model_get_associated_assets_by_fieldname(model: Model):
505
505
  assert ret == []
506
506
 
507
507
 
508
- # TODO: Re-enable these tests once we have a newer release of
509
- # python-jsonschema-objects that properly handles zero value defaults.
510
- #
511
- # def test_model_asset_to_dict(model: Model):
512
- # """Make sure assets are converted to dictionaries correctly"""
513
- # # Create and add asset
514
- # p1 = create_application_asset(model, "Program 1")
515
- # model.add_asset(p1)
516
- #
517
- # # Tuple is returned
518
- # ret = model.asset_to_dict(p1)
519
- #
520
- # # First element should be the id
521
- # p1_id = ret[0]
522
- # assert p1_id == p1.id
523
- #
524
- # # Second element is the dict, each value should
525
- # # be set as below for an 'Application' asset in coreLang
526
- # p1_dict = ret[1]
527
- # assert p1_dict.get('name') == p1.name
528
- # assert p1_dict.get('type') == 'Application'
529
- #
530
- # # Default values should not be saved
531
- # assert p1_dict.get('defenses') == None
532
- #
533
- # def test_model_asset_with_nondefault_defense_to_dict(model: Model):
534
- # """Make sure assets are converted to dictionaries correctly"""
535
- # # Create and add asset
536
- # p1 = create_application_asset(model, "Program 1")
537
- # p1.notPresent = 1.0
538
- # model.add_asset(p1)
539
- #
540
- # # Tuple is returned
541
- # ret = model.asset_to_dict(p1)
542
- #
543
- # # First element should be the id
544
- # p1_id = ret[0]
545
- # assert p1_id == p1.id
546
- #
547
- # # Second element is the dict, each value should
548
- # # be set as below for an 'Application' asset in coreLang
549
- # p1_dict = ret[1]
550
- # assert p1_dict.get('name') == p1.name
551
- # assert p1_dict.get('type') == 'Application'
552
- #
553
- # # Default values for 'Application' defenses in coreLang
554
- # assert p1_dict.get('defenses') == {
555
- # 'notPresent': 1.0
556
- # }
508
+ def test_model_asset_to_dict(model: Model):
509
+ """Make sure assets are converted to dictionaries correctly"""
510
+ # Create and add asset
511
+ p1 = create_application_asset(model, "Program 1")
512
+ model.add_asset(p1)
513
+
514
+ # Tuple is returned
515
+ ret = model.asset_to_dict(p1)
516
+
517
+ # First element should be the id
518
+ p1_id = ret[0]
519
+ assert p1_id == p1.id
520
+
521
+ # Second element is the dict, each value should
522
+ # be set as below for an 'Application' asset in coreLang
523
+ p1_dict = ret[1]
524
+ assert p1_dict.get('name') == p1.name
525
+ assert p1_dict.get('type') == 'Application'
526
+
527
+ # Default values should not be saved
528
+ assert p1_dict.get('defenses') == None
529
+
530
+ def test_model_asset_with_nondefault_defense_to_dict(model: Model):
531
+ """Make sure assets are converted to dictionaries correctly"""
532
+ # Create and add asset
533
+ p1 = create_application_asset(model, "Program 1")
534
+ p1.notPresent = 1.0
535
+ model.add_asset(p1)
536
+
537
+ # Tuple is returned
538
+ ret = model.asset_to_dict(p1)
539
+
540
+ # First element should be the id
541
+ p1_id = ret[0]
542
+ assert p1_id == p1.id
543
+
544
+ # Second element is the dict, each value should
545
+ # be set as below for an 'Application' asset in coreLang
546
+ p1_dict = ret[1]
547
+ assert p1_dict.get('name') == p1.name
548
+ assert p1_dict.get('type') == 'Application'
549
+
550
+ # Default values for 'Application' defenses in coreLang
551
+ assert p1_dict.get('defenses') == {
552
+ 'notPresent': 1.0
553
+ }
557
554
 
558
555
 
559
556
  def test_model_association_to_dict(model: Model):
@@ -1,90 +0,0 @@
1
- """
2
- MAL-Toolbox Language Specification Module
3
-
4
- """
5
-
6
- import logging
7
-
8
- from typing import Optional
9
- logger = logging.getLogger(__name__)
10
-
11
- # TODO move these functions to their relevant module/class
12
-
13
- def get_association_by_fields_and_assets(
14
- lang_spec: dict,
15
- first_field: str,
16
- second_field: str,
17
- first_asset: str,
18
- second_asset: str
19
- ) -> Optional[str]:
20
- """
21
- Get an association based on its field names and asset types
22
-
23
- Arguments:
24
- lang_spec - a dictionary containing the MAL language specification
25
- first_field - a string containing the first field
26
- second_field - a string containing the second field
27
- first_asset - a string representing the first asset type
28
- second_asset - a string representing the second asset type
29
-
30
- Return:
31
- The name of the association matching the fieldnames and asset types.
32
- None if there is no match.
33
- """
34
- for assoc in lang_spec['associations']:
35
- logger.debug(
36
- 'Compare ("%s", "%s". "%s". "%s") to ("%s", "%s", "%s", "%s").',
37
- first_asset, first_field, second_asset, second_field,
38
- assoc["leftAsset"], assoc["leftField"],
39
- assoc["rightAsset"], assoc["rightField"]
40
- )
41
-
42
- # If the asset and fields match either way we accept it as a match.
43
- if assoc['leftField'] == first_field and \
44
- assoc['rightField'] == second_field and \
45
- extends_asset(lang_spec, first_asset, assoc['leftAsset']) and \
46
- extends_asset(lang_spec, second_asset, assoc['rightAsset']):
47
- return assoc['name']
48
- if assoc['leftField'] == second_field and \
49
- assoc['rightField'] == first_field and \
50
- extends_asset(lang_spec, second_asset, assoc['leftAsset']) and \
51
- extends_asset(lang_spec, first_asset, assoc['rightAsset']):
52
- return assoc['name']
53
-
54
- return None
55
-
56
- def extends_asset(lang_spec: dict, asset: str, target_asset: str) -> bool:
57
- """
58
- Check if an asset extends the target asset through inheritance.
59
-
60
- Arguments:
61
- lang_spec - a dictionary containing the MAL language specification
62
- asset - the asset name we wish to evaluate
63
- target_asset - the target asset name we wish to evaluate if it
64
- is extended
65
-
66
- Return:
67
- True if this asset extends the target_asset via inheritance.
68
- False otherwise.
69
- """
70
-
71
- logger.debug('Check if %s extends %s via inheritance.', asset, target_asset)
72
-
73
- if asset == target_asset:
74
- return True
75
-
76
- asset_dict = next((asset_info for asset_info in lang_spec['assets'] \
77
- if asset_info['name'] == asset), None)
78
- if not asset_dict:
79
- logger.error(
80
- 'Failed to find asset type %s when looking for variable.', asset
81
- )
82
- return False
83
- if asset_dict['superAsset']:
84
- if asset_dict['superAsset'] == target_asset:
85
- return True
86
- else:
87
- return extends_asset(lang_spec, asset_dict['superAsset'],
88
- target_asset)
89
-
90
- return False
File without changes
File without changes
File without changes
File without changes