mal-toolbox 0.0.28__py3-none-any.whl → 0.1.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {mal_toolbox-0.0.28.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.28.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 +84 -25
  10. maltoolbox/attackgraph/attackgraph.py +503 -215
  11. maltoolbox/attackgraph/node.py +92 -31
  12. maltoolbox/attackgraph/query.py +125 -19
  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.28.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 -282
  35. {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/AUTHORS +0 -0
  36. {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/LICENSE +0 -0
  37. {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/top_level.txt +0 -0
@@ -2,30 +2,45 @@
2
2
  MAL-Toolbox Language Graph Module
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
7
+ import copy
5
8
  import logging
6
9
  import json
7
-
8
- from dataclasses import dataclass
9
- from typing import Any, List, Optional, ForwardRef
10
-
11
- from maltoolbox.language import specification
12
-
10
+ import zipfile
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Optional
14
+
15
+ from maltoolbox.file_utils import (
16
+ load_dict_from_yaml_file, load_dict_from_json_file,
17
+ save_dict_to_file
18
+ )
19
+ from .compiler import MalCompiler
20
+ from ..exceptions import (
21
+ LanguageGraphAssociationError,
22
+ LanguageGraphStepExpressionError,
23
+ LanguageGraphException,
24
+ LanguageGraphSuperAssetNotFoundError
25
+ )
13
26
 
14
27
  logger = logging.getLogger(__name__)
15
28
 
16
29
  @dataclass
17
30
  class LanguageGraphAsset:
18
- name: str = None
19
- associations: List[ForwardRef('LanguageGraphAssociation')] = None
20
- attack_steps: List[ForwardRef('LanguageGraphAttackStep')] = None
21
- description: dict = None
31
+ name: Optional[str] = None
32
+ associations: list[LanguageGraphAssociation] = field(default_factory=lambda: [])
33
+ attack_steps: list[LanguageGraphAttackStep] = field(default_factory=lambda: [])
34
+ description: dict = field(default_factory=lambda: {})
22
35
  # MAL languages currently do not support multiple inheritance, but this is
23
36
  # futureproofing at its most hopeful.
24
- super_assets: list = None
25
- sub_assets: list = None
37
+ super_assets: list = field(default_factory=lambda: [])
38
+ sub_assets: list = field(default_factory=lambda: [])
39
+ is_abstract: Optional[bool] = None
26
40
 
27
- def to_dict(self):
28
- node_dict = {
41
+ def to_dict(self) -> dict:
42
+ """Convert LanguageGraphAsset to dictionary"""
43
+ node_dict: dict[str, Any] = {
29
44
  'name': self.name,
30
45
  'associations': [],
31
46
  'attack_steps': [],
@@ -46,7 +61,10 @@ class LanguageGraphAsset:
46
61
  node_dict['sub_assets'].append(sub_asset.name)
47
62
  return node_dict
48
63
 
49
- def is_subasset_of(self, target_asset):
64
+ def __repr__(self) -> str:
65
+ return str(self.to_dict())
66
+
67
+ def is_subasset_of(self, target_asset: LanguageGraphAsset) -> bool:
50
68
  """
51
69
  Check if an asset extends the target asset through inheritance.
52
70
 
@@ -66,7 +84,7 @@ class LanguageGraphAsset:
66
84
  current_assets.extend(current_asset.super_assets)
67
85
  return False
68
86
 
69
- def get_all_subassets(self):
87
+ def get_all_subassets(self) -> list[LanguageGraphAsset]:
70
88
  """
71
89
  Return a list of all of the assets that directly or indirectly extend
72
90
  this asset.
@@ -82,7 +100,7 @@ class LanguageGraphAsset:
82
100
  subassets.extend(current_asset.sub_assets)
83
101
  return subassets
84
102
 
85
- def get_all_superassets(self):
103
+ def get_all_superassets(self) -> list[LanguageGraphAsset]:
86
104
  """
87
105
  Return a list of all of the assets that this asset directly or
88
106
  indirectly extends.
@@ -98,6 +116,20 @@ class LanguageGraphAsset:
98
116
  superassets.extend(current_asset.super_assets)
99
117
  return superassets
100
118
 
119
+ def get_all_common_superassets(
120
+ self, other: LanguageGraphAsset
121
+ ) -> set[Optional[str]]:
122
+ """Return a set of all common ancestors between this asset
123
+ and the other asset given as parameter"""
124
+ self_superassets = set(
125
+ asset.name for asset in self.get_all_superassets()
126
+ )
127
+ other_superassets = set(
128
+ asset.name for asset in other.get_all_superassets()
129
+ )
130
+ return self_superassets.intersection(other_superassets)
131
+
132
+
101
133
  @dataclass
102
134
  class LanguageGraphAssociationField:
103
135
  asset: LanguageGraphAsset
@@ -105,14 +137,16 @@ class LanguageGraphAssociationField:
105
137
  minimum: int
106
138
  maximum: int
107
139
 
140
+
108
141
  @dataclass
109
142
  class LanguageGraphAssociation:
110
143
  name: str
111
144
  left_field: LanguageGraphAssociationField
112
145
  right_field: LanguageGraphAssociationField
113
- description: dict = None
146
+ description: Optional[dict] = None
114
147
 
115
- def to_dict(self):
148
+ def to_dict(self) -> dict:
149
+ """Convert LanguageGraphAssociation to dictionary"""
116
150
  node_dict = {
117
151
  'name': self.name,
118
152
  'left': {
@@ -132,7 +166,10 @@ class LanguageGraphAssociation:
132
166
 
133
167
  return node_dict
134
168
 
135
- def contains_fieldname(self, fieldname):
169
+ def __repr__(self) -> str:
170
+ return str(self.to_dict())
171
+
172
+ def contains_fieldname(self, fieldname: str) -> bool:
136
173
  """
137
174
  Check if the association contains the field name given as a parameter.
138
175
 
@@ -147,7 +184,7 @@ class LanguageGraphAssociation:
147
184
  return True
148
185
  return False
149
186
 
150
- def contains_asset(self, asset):
187
+ def contains_asset(self, asset: Any) -> bool:
151
188
  """
152
189
  Check if the association matches the asset given as a parameter. A
153
190
  match can either be an explicit one or if the asset given subassets
@@ -164,7 +201,7 @@ class LanguageGraphAssociation:
164
201
  return True
165
202
  return False
166
203
 
167
- def get_opposite_fieldname(self, fieldname):
204
+ def get_opposite_fieldname(self, fieldname: str) -> str:
168
205
  """
169
206
  Return the opposite field name if the association contains the field
170
207
  name given as a parameter.
@@ -179,11 +216,14 @@ class LanguageGraphAssociation:
179
216
  if self.right_field.fieldname == fieldname:
180
217
  return self.left_field.fieldname
181
218
 
182
- logger.warning(f'Requested fieldname \"{fieldname}\" from '
183
- f'association {self.name} which did not contain it!')
184
- return None
219
+ msg = ('Requested fieldname "%s" from association '
220
+ '%s which did not contain it!')
221
+ logger.error(msg, fieldname, self.name)
222
+ raise LanguageGraphAssociationError(msg % (fieldname, self.name))
185
223
 
186
- def get_opposite_asset(self, asset):
224
+ def get_opposite_asset(
225
+ self, asset: LanguageGraphAsset
226
+ ) -> Optional[LanguageGraphAsset]:
187
227
  """
188
228
  Return the opposite asset if the association matches the asset given
189
229
  as a parameter. A match can either be an explicit one or if the asset
@@ -203,22 +243,30 @@ class LanguageGraphAssociation:
203
243
  if asset.is_subasset_of(self.right_field.asset):
204
244
  return self.left_field.asset
205
245
 
206
- logger.warning(f'Requested asset \"{asset.name}\" from '
207
- f'association {self.name} which did not contain it!')
246
+ logger.warning(
247
+ 'Requested asset "%s" from association %s'
248
+ 'which did not contain it!', asset.name, self.name
249
+ )
208
250
  return None
209
251
 
252
+
210
253
  @dataclass
211
254
  class LanguageGraphAttackStep:
212
- name: str = None
213
- type: str = None
214
- asset: List[ForwardRef('LanguageGraphAsset')] = None
215
- ttc: dict = None
216
- children: dict = None
217
- parents: dict = None
218
- description: dict = None
219
-
220
- def to_dict(self):
221
- node_dict = {
255
+ name: str
256
+ type: str
257
+ asset: LanguageGraphAsset
258
+ ttc: dict = field(default_factory = lambda: {})
259
+ children: dict = field(default_factory = lambda: {})
260
+ parents: dict = field(default_factory = lambda: {})
261
+ description: dict = field(default_factory = lambda: {})
262
+ attributes: Optional[dict] = None
263
+
264
+ @property
265
+ def qualified_name(self) -> str:
266
+ return f"{self.asset.name}:{self.name}"
267
+
268
+ def to_dict(self) -> dict:
269
+ node_dict: dict[Any, Any] = {
222
270
  'name': self.name,
223
271
  'type': self.type,
224
272
  'asset': self.asset.name,
@@ -248,11 +296,20 @@ class LanguageGraphAttackStep:
248
296
 
249
297
  return node_dict
250
298
 
299
+ def __repr__(self) -> str:
300
+ return str(self.to_dict())
301
+
251
302
 
252
303
  class DependencyChain:
253
- def __init__(self, type, next_link):
304
+ def __init__(self, type: str, next_link: Optional[DependencyChain]):
254
305
  self.type = type
255
- self.next_link = next_link
306
+ self.next_link: Optional[DependencyChain] = next_link
307
+ self.fieldname: str = ""
308
+ self.association: Optional[LanguageGraphAssociation] = None
309
+ self.left_chain: Optional[DependencyChain] = None
310
+ self.right_chain: Optional[DependencyChain] = None
311
+ self.subtype: Optional[Any] = None
312
+ self.current_link: Optional[DependencyChain] = None
256
313
 
257
314
  def __iter__(self):
258
315
  self.current_link = self
@@ -265,55 +322,97 @@ class DependencyChain:
265
322
  return dep_chain
266
323
  raise StopIteration
267
324
 
268
- def to_dict(self):
325
+ def to_dict(self) -> dict:
326
+ """Convert DependencyChain to dictionary"""
269
327
  match (self.type):
270
328
  case 'union' | 'intersection' | 'difference':
271
329
  return {self.type: {
272
- 'left': self.left_chain.to_dict(),
330
+ 'left': self.left_chain.to_dict()
331
+ if self.left_chain else {},
273
332
  'right': self.right_chain.to_dict()
333
+ if self.right_chain else {}
274
334
  }
275
335
  }
276
336
 
277
337
  case 'field':
278
- association = self.association
279
- return {association.name:
338
+ if not self.association:
339
+ raise LanguageGraphAssociationError("Missing association for dep chain")
340
+ return {self.association.name:
280
341
  {'fieldname': self.fieldname,
281
342
  'next_association':
282
- self.next_link.to_dict() if self.next_link else None
343
+ self.next_link.to_dict()
344
+ if self.next_link else {}
345
+ }
283
346
  }
284
- }
285
347
 
286
348
  case 'transitive':
287
349
  return {'transitive':
288
350
  self.next_link.to_dict()
351
+ if self.next_link else {}
289
352
  }
290
353
 
291
354
  case 'subType':
355
+ if not self.subtype:
356
+ raise LanguageGraphException(
357
+ "No subtype for dependency chain"
358
+ )
359
+ if not self.next_link:
360
+ raise LanguageGraphException(
361
+ "No next link for subtype dependency chain"
362
+ )
292
363
  return {'subType': self.subtype.name,
293
364
  'expression': self.next_link.to_dict()
294
365
  }
295
366
 
296
367
  case _:
297
- logger.error('Unknown associations chain element '
298
- f'{self.type}!')
299
- return None
368
+ msg = 'Unknown associations chain element %s!'
369
+ logger.error(msg, self.type)
370
+ raise LanguageGraphAssociationError(msg % self.type)
371
+
372
+ def __repr__(self) -> str:
373
+ return str(self.to_dict())
374
+
375
+
376
+ class LanguageGraph():
377
+ """Graph representation of a MAL language"""
378
+ def __init__(self, lang: dict):
379
+ self.assets: list = []
380
+ self.associations: list = []
381
+ self.attack_steps: list = []
382
+ self._lang_spec: dict = lang
383
+ self.metadata = {
384
+ "version": lang["defines"]["version"],
385
+ "id": lang["defines"]["id"],
386
+ }
387
+ self._generate_graph()
300
388
 
389
+ @classmethod
390
+ def from_mal_spec(cls, mal_spec_file: str) -> LanguageGraph:
391
+ """
392
+ Create a LanguageGraph from a .mal file (a MAL spec).
301
393
 
302
- class LanguageGraph:
303
- def __init__(self):
304
- self.assets = []
305
- self.associations = []
306
- self.attack_steps = []
394
+ Arguments:
395
+ mal_spec_file - the path to the .mal file
396
+ """
397
+ logger.info("Loading mal spec %s", mal_spec_file)
398
+ return LanguageGraph(MalCompiler().compile(mal_spec_file))
307
399
 
308
- def save_to_file(self, filename: str):
400
+ @classmethod
401
+ def from_mar_archive(cls, mar_archive: str) -> LanguageGraph:
309
402
  """
310
- Save the language graph to a json file.
403
+ Create a LanguageGraph from a ".mar" archive provided by malc
404
+ (https://github.com/mal-lang/malc).
311
405
 
312
406
  Arguments:
313
- filename - the name of the output file
407
+ mar_archive - the path to a ".mar" archive
314
408
  """
409
+ logger.info('Loading mar archive %s', mar_archive)
410
+ with zipfile.ZipFile(mar_archive, 'r') as archive:
411
+ langspec = archive.read('langspec.json')
412
+ return LanguageGraph(json.loads(langspec))
315
413
 
316
- logger.info(f'Saving language graph to \"{filename}\" file.')
414
+ def _to_dict(self):
415
+ """Converts LanguageGraph into a dict"""
317
416
  serialized_assets = []
318
417
  for asset in self.assets:
319
418
  serialized_assets.append(asset.to_dict())
@@ -323,24 +422,67 @@ class LanguageGraph:
323
422
  serialized_attack_steps = []
324
423
  for attack_step in self.attack_steps:
325
424
  serialized_attack_steps.append(attack_step.to_dict())
326
- logger.debug(f'Saving {len(serialized_assets)} assets, '
327
- f'{len(serialized_associations)} associations, and '
328
- f'{len(serialized_attack_steps)} attack steps to '
329
- f'\"{filename}\" file')
425
+
426
+ logger.debug(
427
+ 'Serializing %s assets, %s associations, and %s attack steps',
428
+ len(serialized_assets), len(serialized_associations),
429
+ len(serialized_attack_steps)
430
+ )
431
+
330
432
  serialized_graph = {
331
433
  'Assets': serialized_assets,
332
434
  'Associations': serialized_associations,
333
435
  'Attack Steps': serialized_attack_steps
334
436
  }
335
- with open(filename, 'w', encoding='utf-8') as file:
336
- json.dump(serialized_graph, file, indent=4)
437
+ return serialized_graph
438
+
439
+ def save_to_file(self, filename: str) -> None:
440
+ """Save to json/yml depending on extension"""
441
+ return save_dict_to_file(filename, self._to_dict())
442
+
443
+ @classmethod
444
+ def _from_dict(cls, serialized_object: dict) -> None:
445
+ raise NotImplementedError(
446
+ "Converting from dict feature is not implemented yet")
447
+
448
+ @classmethod
449
+ def load_from_file(cls, filename: str) -> LanguageGraph:
450
+ """Create LanguageGraph from mal, mar, yaml or json"""
451
+ lang_graph = None
452
+ if filename.endswith('.mal'):
453
+ lang_graph = cls.from_mal_spec(filename)
454
+ elif filename.endswith('.mar'):
455
+ lang_graph = cls.from_mar_archive(filename)
456
+ elif filename.endswith(('yaml', 'yml')):
457
+ lang_graph = cls._from_dict(load_dict_from_yaml_file(filename))
458
+ elif filename.endswith(('json')):
459
+ lang_graph = cls._from_dict(load_dict_from_json_file(filename))
460
+
461
+ if lang_graph:
462
+ return lang_graph
463
+
464
+ raise TypeError(
465
+ "Unknown file extension, expected json/mal/mar/yml/yaml"
466
+ )
467
+
468
+ def save_language_specification_to_json(self, filename: str) -> None:
469
+ """
470
+ Save a MAL language specification dictionary to a JSON file
337
471
 
472
+ Arguments:
473
+ filename - the JSON filename where the language specification will be written
474
+ """
475
+ logger.info('Save language specification to %s', filename)
476
+
477
+ with open(filename, 'w', encoding='utf-8') as file:
478
+ json.dump(self._lang_spec, file, indent=4)
338
479
 
339
480
  def process_step_expression(self,
340
- lang: dict,
341
- target_asset,
342
- dep_chain,
343
- step_expression: dict):
481
+ lang: dict,
482
+ target_asset,
483
+ dep_chain,
484
+ step_expression: dict
485
+ ) -> tuple:
344
486
  """
345
487
  Recursively process an attack step expression.
346
488
 
@@ -363,8 +505,13 @@ class LanguageGraph:
363
505
  A tuple triplet containing the target asset, the resulting parent
364
506
  associations chain, and the name of the attack step.
365
507
  """
366
- logger.debug('Processing Step Expression:\n' \
367
- + json.dumps(step_expression, indent = 2))
508
+
509
+ if logger.isEnabledFor(logging.DEBUG):
510
+ # Avoid running json.dumps when not in debug
511
+ logger.debug(
512
+ 'Processing Step Expression:\n%s',
513
+ json.dumps(step_expression, indent = 2)
514
+ )
368
515
 
369
516
  match (step_expression['type']):
370
517
  case 'attackStep':
@@ -383,10 +530,12 @@ class LanguageGraph:
383
530
  rh_target_asset, rh_dep_chain, _ = self.process_step_expression(
384
531
  lang, target_asset, dep_chain, step_expression['rhs'])
385
532
 
386
- if lh_target_asset != rh_target_asset:
387
- logger.error('Set operation has different target asset '
388
- 'types for each side of the expression: '
389
- f'{lh_target_asset.name} and {rh_target_asset.name}!')
533
+ if not lh_target_asset.get_all_common_superassets(rh_target_asset):
534
+ logger.error(
535
+ "Set operation attempted between targets that"
536
+ " do not share any common superassets: %s and %s!",
537
+ lh_target_asset.name, rh_target_asset.name
538
+ )
390
539
  return (None, None, None)
391
540
 
392
541
  new_dep_chain = DependencyChain(
@@ -401,9 +550,8 @@ class LanguageGraph:
401
550
  case 'variable':
402
551
  # Fetch the step expression associated with the variable from
403
552
  # the language specification and resolve that.
404
- variable_step_expr = specification.\
405
- get_variable_for_class_by_name(lang,
406
- target_asset.name, step_expression['name'])
553
+ variable_step_expr = self._get_variable_for_asset_type_by_name(
554
+ target_asset.name, step_expression['name'])
407
555
  if variable_step_expr:
408
556
  return self.process_step_expression(
409
557
  lang,
@@ -412,8 +560,10 @@ class LanguageGraph:
412
560
  variable_step_expr)
413
561
 
414
562
  else:
415
- logger.error('Failed to find variable '
416
- f'{step_expression["name"]} for {target_asset.name}')
563
+ logger.error(
564
+ 'Failed to find variable %s for %s',
565
+ step_expression["name"], target_asset.name
566
+ )
417
567
  return (None, None, None)
418
568
 
419
569
  case 'field':
@@ -422,7 +572,9 @@ class LanguageGraph:
422
572
  # fieldname and association to the parent associations chain.
423
573
  fieldname = step_expression['name']
424
574
  if not target_asset:
425
- logger.error(f'Missing target asset for field \"{fieldname}\"!')
575
+ logger.error(
576
+ 'Missing target asset for field "%s"!', fieldname
577
+ )
426
578
  return (None, None, None)
427
579
 
428
580
  new_target_asset = None
@@ -447,8 +599,10 @@ class LanguageGraph:
447
599
  return (new_target_asset,
448
600
  new_dep_chain,
449
601
  None)
450
- logger.error(f'Failed to find field \"{fieldname}\" on '
451
- f'asset \"{target_asset.name}\"!')
602
+ logger.error(
603
+ 'Failed to find field "%s" on asset "%s"!',
604
+ fieldname, target_asset.name
605
+ )
452
606
  return (None, None, None)
453
607
 
454
608
  case 'transitive':
@@ -481,19 +635,23 @@ class LanguageGraph:
481
635
  dep_chain,
482
636
  step_expression['stepExpression'])
483
637
 
484
- subtype_asset = next((asset for asset in self.assets \
485
- if asset.name == subtype_name), None)
638
+ subtype_asset = next((asset for asset in self.assets if asset.name == subtype_name), None)
639
+
486
640
  if not subtype_asset:
487
- logger.error('Failed to find subtype attack step '
488
- f'\"{subtype_name}\"')
641
+ msg = 'Failed to find subtype attackstep "{subtype_name}"'
642
+ logger.error(msg)
643
+ raise LanguageGraphException(msg)
644
+
489
645
  if not subtype_asset.is_subasset_of(result_target_asset):
490
- logger.error(f'Found subtype \"{subtype_name}\" which '
491
- f'does not extend \"{result_target_asset.name}\". '
492
- 'Therefore the subtype cannot be resolved.')
646
+ logger.error(
647
+ 'Found subtype "%s" which does not extend "%s", '
648
+ 'therefore the subtype cannot be resolved.',
649
+ subtype_name, result_target_asset.name
650
+ )
493
651
  return (None, None, None)
494
652
 
495
653
  new_dep_chain = DependencyChain(
496
- type = 'subtype',
654
+ type = 'subType',
497
655
  next_link = result_dep_chain)
498
656
  new_dep_chain.subtype = subtype_asset
499
657
  return (subtype_asset,
@@ -519,11 +677,16 @@ class LanguageGraph:
519
677
  rh_attack_step_name)
520
678
 
521
679
  case _:
522
- logger.error('Unknown attack step type: '
523
- f'{step_expression["type"]}')
680
+ logger.error(
681
+ 'Unknown attack step type: "%s"', step_expression["type"]
682
+ )
524
683
  return (None, None, None)
525
684
 
526
- def reverse_dep_chain(self, dep_chain, reverse_chain):
685
+ def reverse_dep_chain(
686
+ self,
687
+ dep_chain: Optional[DependencyChain],
688
+ reverse_chain: Optional[DependencyChain]
689
+ ) -> Optional[DependencyChain]:
527
690
  """
528
691
  Recursively reverse the associations chain. From parent to child or
529
692
  vice versa.
@@ -566,94 +729,109 @@ class LanguageGraph:
566
729
 
567
730
  case 'field':
568
731
  association = dep_chain.association
732
+
733
+ if not association:
734
+ raise LanguageGraphException(
735
+ "Missing association for dep chain"
736
+ )
737
+
569
738
  opposite_fieldname = association.get_opposite_fieldname(
570
739
  dep_chain.fieldname)
571
740
  new_dep_chain = DependencyChain(
572
741
  type = 'field',
573
- next_link = reverse_chain)
742
+ next_link = reverse_chain
743
+ )
574
744
  new_dep_chain.fieldname = opposite_fieldname
575
745
  new_dep_chain.association = association
576
- return self.reverse_dep_chain(dep_chain.next_link,
577
- new_dep_chain)
746
+ return self.reverse_dep_chain(
747
+ dep_chain.next_link,
748
+ new_dep_chain
749
+ )
578
750
 
579
751
  case 'subType':
580
752
  result_reverse_chain = self.reverse_dep_chain(
581
- new_dep_chain.next_link,
582
- reverse_chain)
753
+ dep_chain.next_link,
754
+ reverse_chain
755
+ )
583
756
  new_dep_chain = DependencyChain(
584
- type = 'subtype',
585
- next_link = result_reverse_chain)
757
+ type = 'subType',
758
+ next_link = result_reverse_chain
759
+ )
586
760
  new_dep_chain.subtype = dep_chain.subtype
587
761
  return new_dep_chain
762
+ # return reverse_chain
588
763
 
589
764
  case _:
590
- logger.error('Unknown associations chain element '
591
- f'{dep_chain.type}!')
592
- return None
593
-
765
+ msg = 'Unknown assoc chain element "%s"'
766
+ logger.error(msg, dep_chain.type)
767
+ raise LanguageGraphAssociationError(msg % dep_chain.type)
594
768
 
595
- def generate_graph(self, lang: dict):
769
+ def _generate_graph(self) -> None:
596
770
  """
597
- Generate language graph starting from a MAL language specification
598
-
599
- Arguments:
600
- lang - a dictionary representing the MAL language specification
771
+ Generate language graph starting from the MAL language specification
772
+ given in the constructor.
601
773
  """
602
774
  # Generate all of the asset nodes of the language graph.
603
- for asset in lang['assets']:
604
- logger.debug(f'Create asset language graph nodes for asset '
605
- f'{asset["name"]}')
775
+ for asset in self._lang_spec['assets']:
776
+ logger.debug(
777
+ 'Create asset language graph nodes for asset %s',
778
+ asset["name"]
779
+ )
606
780
  asset_node = LanguageGraphAsset(
607
781
  name = asset['name'],
608
782
  associations = [],
609
783
  attack_steps = [],
610
784
  description = asset['meta'],
611
785
  super_assets = [],
612
- sub_assets = []
786
+ sub_assets = [],
787
+ is_abstract = asset['isAbstract']
613
788
  )
614
789
  self.assets.append(asset_node)
615
790
 
616
791
  # Link assets based on inheritance
617
- for asset_info in lang['assets']:
792
+ for asset_info in self._lang_spec['assets']:
618
793
  asset = next((asset for asset in self.assets \
619
794
  if asset.name == asset_info['name']), None)
620
- if not asset:
621
- logger.error('Failed to find asset '
622
- f'\"{asset_info["name"]}\"!')
623
- return 1
624
795
  if asset_info['superAsset']:
625
796
  super_asset = next((asset for asset in self.assets \
626
797
  if asset.name == asset_info['superAsset']), None)
627
798
  if not super_asset:
628
- logger.error('Failed to find super asset '
629
- f'\"{asset_info["superAsset"]}\" '
630
- f'for asset \"{asset_info["name"]}\"!')
631
- return 1
799
+ msg = 'Failed to find super asset "%s" for asset "%s"!'
800
+ logger.error(
801
+ msg, asset_info["superAsset"], asset_info["name"])
802
+ raise LanguageGraphSuperAssetNotFoundError(
803
+ msg % (asset_info["superAsset"], asset_info["name"]))
804
+
632
805
  super_asset.sub_assets.append(asset)
633
806
  asset.super_assets.append(super_asset)
634
807
 
635
808
  # Generate all of the association nodes of the language graph.
636
809
  for asset in self.assets:
637
- logger.debug(f'Create association language graph nodes for asset '
638
- f'{asset.name}')
639
- associations_nodes = []
640
- associations = specification.get_associations_for_class(lang,
641
- asset.name)
810
+ logger.debug(
811
+ 'Create association language graph nodes for asset %s',
812
+ asset.name
813
+ )
814
+
815
+ associations = self._get_associations_for_asset_type(asset.name)
642
816
  for association in associations:
643
817
  left_asset = next((asset for asset in self.assets \
644
818
  if asset.name == association['leftAsset']), None)
645
819
  if not left_asset:
646
- logger.error('Failed to find left hand asset '
647
- f'\"{association["leftAsset"]}\" for '
648
- f'association \"{association["name"]}\"!')
649
- return 1
820
+ msg = 'Left asset "%s" for association "%s" not found!'
821
+ logger.error(
822
+ msg, association["leftAsset"], association["name"])
823
+ raise LanguageGraphAssociationError(
824
+ msg % (association["leftAsset"], association["name"]))
825
+
650
826
  right_asset = next((asset for asset in self.assets \
651
827
  if asset.name == association['rightAsset']), None)
652
828
  if not right_asset:
653
- logger.error('Failed to find right hand asset '
654
- f'\"{association["rightAsset"]}\" for '
655
- f'association \"{association["name"]}\"!')
656
- return 1
829
+ msg = 'Right asset "%s" for association "%s" not found!'
830
+ logger.error(
831
+ msg, association["rightAsset"], association["name"])
832
+ raise LanguageGraphAssociationError(
833
+ msg % (association["rightAsset"], association["name"])
834
+ )
657
835
 
658
836
  # Technically we should be more exhaustive and check the
659
837
  # flipped version too and all of the fieldnames as well.
@@ -694,17 +872,19 @@ class LanguageGraph:
694
872
 
695
873
  # Generate all of the attack step nodes of the language graph.
696
874
  for asset in self.assets:
697
- logger.debug(f'Create attack steps language graph nodes for asset '
698
- f'{asset.name}.')
699
- attack_step_nodes = []
700
- attack_steps = specification.get_attacks_for_class(lang,
701
- asset.name)
875
+ logger.debug(
876
+ 'Create attack steps language graph nodes for asset %s',
877
+ asset.name
878
+ )
879
+ attack_steps = self._get_attacks_for_asset_type(asset.name)
702
880
  for attack_step_name, attack_step_attribs in attack_steps.items():
703
- logger.debug(f'Create attack step language graph nodes for '
704
- f'{attack_step_name}.')
881
+ logger.debug(
882
+ 'Create attack step language graph nodes for %s',
883
+ attack_step_name
884
+ )
705
885
 
706
886
  attack_step_node = LanguageGraphAttackStep(
707
- name = asset.name + ':' + attack_step_name,
887
+ name = attack_step_name,
708
888
  type = attack_step_attribs['type'],
709
889
  asset = asset,
710
890
  ttc = attack_step_attribs['ttc'],
@@ -718,8 +898,10 @@ class LanguageGraph:
718
898
 
719
899
  # Then, link all of the attack step nodes according to their associations.
720
900
  for attack_step in self.attack_steps:
721
- logger.debug('Determining children for attack step '\
722
- f'{attack_step.name}.')
901
+ logger.debug(
902
+ 'Determining children for attack step %s',
903
+ attack_step.name
904
+ )
723
905
  step_expressions = \
724
906
  attack_step.attributes['reaches']['stepExpressions'] if \
725
907
  attack_step.attributes['reaches'] else []
@@ -728,36 +910,31 @@ class LanguageGraph:
728
910
  # Resolve each of the attack step expressions listed for this
729
911
  # attack step to determine children.
730
912
  (target_asset, dep_chain, attack_step_name) = \
731
- self.process_step_expression(lang,
913
+ self.process_step_expression(self._lang_spec,
732
914
  attack_step.asset,
733
915
  None,
734
916
  step_expression)
735
917
  if not target_asset:
736
- logger.error('Failed to find target asset ' \
737
- f'to link with for step expression:\n' +
738
- json.dumps(step_expression, indent = 2))
739
- print('Failed to find target asset ' \
740
- f'to link with for step expression:\n' +
741
- json.dumps(step_expression, indent = 2))
742
- return 1
743
-
744
- attack_step_fullname = target_asset.name + ':' + attack_step_name
918
+ msg = 'Failed to find target asset to link with for ' \
919
+ 'step expression:\n%s'
920
+ raise LanguageGraphStepExpressionError(
921
+ msg % json.dumps(step_expression, indent = 2)
922
+ )
923
+
745
924
  target_attack_step = next((attack_step \
746
925
  for attack_step in target_asset.attack_steps \
747
- if attack_step.name == attack_step_fullname), None)
926
+ if attack_step.name == attack_step_name), None)
748
927
 
749
928
  if not target_attack_step:
750
- logger.error('Failed to find target attack step '
751
- f'{attack_step_fullname} on '
752
- f'{target_asset.name} to link with for step '
753
- 'expression:\n' +
754
- json.dumps(step_expression, indent = 2))
755
- print('Failed to find target attack step '
756
- f'{attack_step_fullname} on '
757
- f'{target_asset.name} to link with for step '
758
- 'expression:\n' +
759
- json.dumps(step_expression, indent = 2))
760
- return 1
929
+ msg = 'Failed to find target attack step %s on %s to ' \
930
+ 'link with for step expression:\n%s'
931
+ raise LanguageGraphStepExpressionError(
932
+ msg % (
933
+ attack_step_name,
934
+ target_asset.name,
935
+ json.dumps(step_expression, indent = 2)
936
+ )
937
+ )
761
938
 
762
939
  # It is easier to create the parent associations chain due to
763
940
  # the left-hand first progression.
@@ -780,4 +957,228 @@ class LanguageGraph:
780
957
  self.reverse_dep_chain(dep_chain,
781
958
  None))]
782
959
 
783
- return 0
960
+ def _get_attacks_for_asset_type(self, asset_type: str) -> dict:
961
+ """
962
+ Get all Attack Steps for a specific Class
963
+
964
+ Arguments:
965
+ asset_type - a string representing the class for which we want to list
966
+ the possible attack steps
967
+
968
+ Return:
969
+ A dictionary representing the set of possible attacks for the specified
970
+ class. Each key in the dictionary is an attack name and is associated
971
+ with a dictionary containing other characteristics of the attack such as
972
+ type of attack, TTC distribution, child attack steps and other information
973
+ """
974
+ attack_steps: dict = {}
975
+ try:
976
+ asset = next((asset for asset in self._lang_spec['assets'] if asset['name'] == asset_type))
977
+ except StopIteration:
978
+ logger.error(
979
+ 'Failed to find asset type %s when looking'
980
+ 'for attack steps.', asset_type
981
+ )
982
+ return attack_steps
983
+
984
+ logger.debug(
985
+ 'Get attack steps for %s asset from '
986
+ 'language specification.', asset["name"]
987
+ )
988
+ if asset['superAsset']:
989
+ logger.debug('Asset extends another one, fetch the superclass '\
990
+ 'attack steps for it.')
991
+ attack_steps = self._get_attacks_for_asset_type(asset['superAsset'])
992
+
993
+ for step in asset['attackSteps']:
994
+ if step['name'] not in attack_steps:
995
+ attack_steps[step['name']] = copy.deepcopy(step)
996
+ elif not step['reaches']:
997
+ # This attack step does not lead to any attack steps
998
+ continue
999
+ elif step['reaches']['overrides'] == True:
1000
+ attack_steps[step['name']] = copy.deepcopy(step)
1001
+ else:
1002
+ if attack_steps[step['name']]['reaches'] is not None and \
1003
+ 'stepExpressions' in \
1004
+ attack_steps[step['name']]['reaches']:
1005
+ attack_steps[step['name']]['reaches']['stepExpressions'].\
1006
+ extend(step['reaches']['stepExpressions'])
1007
+ else:
1008
+ attack_steps[step['name']]['reaches'] = {
1009
+ 'overrides': False,
1010
+ 'stepExpressions': step['reaches']['stepExpressions']
1011
+ }
1012
+
1013
+
1014
+ return attack_steps
1015
+
1016
+ def _get_associations_for_asset_type(self, asset_type: str) -> list:
1017
+ """
1018
+ Get all Associations for a specific Class
1019
+
1020
+ Arguments:
1021
+ asset_type - a string representing the class for which we want to list
1022
+ the associations
1023
+
1024
+ Return:
1025
+ A dictionary representing the set of associations for the specified
1026
+ class. Each key in the dictionary is an attack name and is associated
1027
+ with a dictionary containing other characteristics of the attack such as
1028
+ type of attack, TTC distribution, child attack steps and other information
1029
+ """
1030
+ logger.debug(
1031
+ 'Get associations for %s asset from '
1032
+ 'language specification.', asset_type
1033
+ )
1034
+ associations: list = []
1035
+
1036
+ asset = next((asset for asset in self._lang_spec['assets'] if asset['name'] == \
1037
+ asset_type), None)
1038
+ if not asset:
1039
+ logger.error(
1040
+ 'Failed to find asset type %s when '
1041
+ 'looking for associations.', asset_type
1042
+ )
1043
+ return associations
1044
+
1045
+ if asset['superAsset']:
1046
+ logger.debug('Asset extends another one, fetch the superclass '\
1047
+ 'associations for it.')
1048
+ associations.extend(self._get_associations_for_asset_type(asset['superAsset']))
1049
+ assoc_iter = (assoc for assoc in self._lang_spec['associations'] \
1050
+ if assoc['leftAsset'] == asset_type or \
1051
+ assoc['rightAsset'] == asset_type)
1052
+ assoc = next(assoc_iter, None)
1053
+ while (assoc):
1054
+ associations.append(assoc)
1055
+ assoc = next(assoc_iter, None)
1056
+
1057
+ return associations
1058
+
1059
+ def _get_variable_for_asset_type_by_name(
1060
+ self, asset_type: str, variable_name: str) -> dict:
1061
+ """
1062
+ Get a variables for a specific asset type by name.
1063
+ NOTE: Variables are the ones specified in MAL through `let` statements
1064
+
1065
+ Arguments:
1066
+ asset_type - a string representing the type of asset which
1067
+ contains the variable
1068
+ variable_name - the name of the variable to search for
1069
+
1070
+ Return:
1071
+ A dictionary representing the step expressions for the specified variable.
1072
+ """
1073
+
1074
+ asset = next((asset for asset in self._lang_spec['assets'] if asset['name'] == \
1075
+ asset_type), None)
1076
+ if not asset:
1077
+ msg = 'Failed to find asset type %s when looking for variable.'
1078
+ logger.error(msg, asset_type)
1079
+ raise LanguageGraphException(msg % asset_type)
1080
+
1081
+ variable_dict = next((variable for variable in \
1082
+ asset['variables'] if variable['name'] == variable_name), None)
1083
+ if not variable_dict:
1084
+ if asset['superAsset']:
1085
+ variable_dict = self._get_variable_for_asset_type_by_name(asset['superAsset'],
1086
+ variable_name)
1087
+ if variable_dict:
1088
+ return variable_dict
1089
+ else:
1090
+ msg = 'Failed to find variable %s in %s lang specification.'
1091
+ logger.error(msg, variable_name, asset_type)
1092
+ raise LanguageGraphException(
1093
+ msg % (variable_name, asset_type))
1094
+
1095
+ return variable_dict['stepExpression']
1096
+
1097
+ def regenerate_graph(self) -> None:
1098
+ """
1099
+ Regenerate language graph starting from the MAL language specification
1100
+ given in the constructor.
1101
+ """
1102
+
1103
+ self.assets = []
1104
+ self.associations = []
1105
+ self.attack_steps = []
1106
+ self._generate_graph()
1107
+
1108
+ def get_asset_by_name(
1109
+ self,
1110
+ asset_name
1111
+ ) -> Optional[LanguageGraphAsset]:
1112
+ """
1113
+ Get an asset based on its name
1114
+
1115
+ Arguments:
1116
+ asset_name - a string containing the asset name
1117
+
1118
+ Return:
1119
+ The asset matching the name.
1120
+ None if there is no match.
1121
+ """
1122
+ for asset in self.assets:
1123
+ if asset.name == asset_name:
1124
+ return asset
1125
+
1126
+ return None
1127
+
1128
+ def get_association_by_fields_and_assets(
1129
+ self,
1130
+ first_field: str,
1131
+ second_field: str,
1132
+ first_asset_name: str,
1133
+ second_asset_name: str
1134
+ ) -> Optional[LanguageGraphAssociation]:
1135
+ """
1136
+ Get an association based on its field names and asset types
1137
+
1138
+ Arguments:
1139
+ first_field - a string containing the first field
1140
+ second_field - a string containing the second field
1141
+ first_asset_name - a string representing the first asset type
1142
+ second_asset_name - a string representing the second asset type
1143
+
1144
+ Return:
1145
+ The association matching the fieldnames and asset types.
1146
+ None if there is no match.
1147
+ """
1148
+ first_asset = self.get_asset_by_name(first_asset_name)
1149
+ if first_asset is None:
1150
+ raise LookupError(
1151
+ f'Failed to find asset with name \"{first_asset_name}\" in '
1152
+ 'the language graph.'
1153
+ )
1154
+
1155
+ second_asset = self.get_asset_by_name(second_asset_name)
1156
+ if second_asset is None:
1157
+ raise LookupError(
1158
+ f'Failed to find asset with name \"{second_asset_name}\" in '
1159
+ 'the language graph.'
1160
+ )
1161
+
1162
+ for assoc in self.associations:
1163
+ logger.debug(
1164
+ 'Compare ("%s", "%s", "%s", "%s") to ("%s", "%s", "%s", "%s").',
1165
+ first_asset_name, first_field,
1166
+ second_asset_name, second_field,
1167
+ assoc.left_field.asset.name, assoc.left_field.fieldname,
1168
+ assoc.right_field.asset.name, assoc.right_field.fieldname
1169
+ )
1170
+
1171
+ # If the asset and fields match either way we accept it as a match.
1172
+ if assoc.left_field.fieldname == first_field and \
1173
+ assoc.right_field.fieldname == second_field and \
1174
+ first_asset.is_subasset_of(assoc.left_field.asset) and \
1175
+ second_asset.is_subasset_of(assoc.right_field.asset):
1176
+ return assoc
1177
+
1178
+ if assoc.left_field.fieldname == second_field and \
1179
+ assoc.right_field.fieldname == first_field and \
1180
+ second_asset.is_subasset_of(assoc.left_field.asset) and \
1181
+ first_asset.is_subasset_of(assoc.right_field.asset):
1182
+ return assoc
1183
+
1184
+ return None