mal-toolbox 1.1.0__py3-none-any.whl → 1.1.2__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 (31) hide show
  1. {mal_toolbox-1.1.0.dist-info → mal_toolbox-1.1.2.dist-info}/METADATA +26 -2
  2. mal_toolbox-1.1.2.dist-info/RECORD +32 -0
  3. maltoolbox/__init__.py +6 -7
  4. maltoolbox/__main__.py +17 -9
  5. maltoolbox/attackgraph/__init__.py +2 -3
  6. maltoolbox/attackgraph/attackgraph.py +379 -362
  7. maltoolbox/attackgraph/node.py +14 -19
  8. maltoolbox/exceptions.py +7 -10
  9. maltoolbox/file_utils.py +10 -4
  10. maltoolbox/language/__init__.py +1 -1
  11. maltoolbox/language/compiler/__init__.py +4 -4
  12. maltoolbox/language/compiler/mal_lexer.py +154 -154
  13. maltoolbox/language/compiler/mal_parser.py +784 -1136
  14. maltoolbox/language/languagegraph.py +491 -636
  15. maltoolbox/model.py +85 -77
  16. maltoolbox/patternfinder/attackgraph_patterns.py +17 -8
  17. maltoolbox/translators/__init__.py +8 -0
  18. maltoolbox/translators/networkx.py +42 -0
  19. maltoolbox/translators/updater.py +18 -25
  20. maltoolbox/visualization/__init__.py +4 -4
  21. maltoolbox/visualization/draw_io_utils.py +6 -5
  22. maltoolbox/visualization/graphviz_utils.py +4 -2
  23. maltoolbox/visualization/neo4j_utils.py +13 -14
  24. maltoolbox/visualization/utils.py +2 -3
  25. mal_toolbox-1.1.0.dist-info/RECORD +0 -32
  26. maltoolbox/translators/securicad.py +0 -179
  27. {mal_toolbox-1.1.0.dist-info → mal_toolbox-1.1.2.dist-info}/WHEEL +0 -0
  28. {mal_toolbox-1.1.0.dist-info → mal_toolbox-1.1.2.dist-info}/entry_points.txt +0 -0
  29. {mal_toolbox-1.1.0.dist-info → mal_toolbox-1.1.2.dist-info}/licenses/AUTHORS +0 -0
  30. {mal_toolbox-1.1.0.dist-info → mal_toolbox-1.1.2.dist-info}/licenses/LICENSE +0 -0
  31. {mal_toolbox-1.1.0.dist-info → mal_toolbox-1.1.2.dist-info}/top_level.txt +0 -0
@@ -1,43 +1,45 @@
1
- """
2
- MAL-Toolbox Language Graph Module
1
+ """MAL-Toolbox Language Graph Module
3
2
  """
4
3
 
5
4
  from __future__ import annotations
6
5
 
7
- import logging
8
6
  import json
7
+ import logging
9
8
  import zipfile
10
-
11
9
  from dataclasses import dataclass, field
12
10
  from functools import cached_property
13
- from typing import Any, Literal, Optional
11
+ from typing import Any, Literal
14
12
 
15
13
  from maltoolbox.file_utils import (
16
- load_dict_from_yaml_file, load_dict_from_json_file,
17
- save_dict_to_file
14
+ load_dict_from_json_file,
15
+ load_dict_from_yaml_file,
16
+ save_dict_to_file,
18
17
  )
19
- from .compiler import MalCompiler
18
+
20
19
  from ..exceptions import (
21
20
  LanguageGraphAssociationError,
22
- LanguageGraphStepExpressionError,
23
21
  LanguageGraphException,
24
- LanguageGraphSuperAssetNotFoundError
22
+ LanguageGraphStepExpressionError,
23
+ LanguageGraphSuperAssetNotFoundError,
25
24
  )
25
+ from .compiler import MalCompiler
26
26
 
27
27
  logger = logging.getLogger(__name__)
28
28
 
29
29
 
30
30
  def disaggregate_attack_step_full_name(
31
- attack_step_full_name: str) -> list[str]:
31
+ attack_step_full_name: str
32
+ ) -> list[str]:
33
+ """From an attack step full name, get (asset_name, attack_step_name)"""
32
34
  return attack_step_full_name.split(':')
33
35
 
34
36
 
35
- @dataclass
37
+ @dataclass(frozen=True, eq=True)
36
38
  class Detector:
37
- name: Optional[str]
39
+ name: str | None
38
40
  context: Context
39
- type: Optional[str]
40
- tprate: Optional[dict]
41
+ type: str | None
42
+ tprate: dict | None
41
43
 
42
44
  def to_dict(self) -> dict:
43
45
  return {
@@ -50,6 +52,7 @@ class Detector:
50
52
 
51
53
  class Context(dict):
52
54
  """Context is part of detectors to provide meta data about attackers"""
55
+
53
56
  def __init__(self, context) -> None:
54
57
  super().__init__(context)
55
58
  self._context_dict = context
@@ -63,22 +66,23 @@ class Context(dict):
63
66
  return str({label: asset.name for label, asset in self._context_dict.items()})
64
67
 
65
68
  def __repr__(self) -> str:
66
- return f"Context({str(self)}))"
69
+ return f"Context({self!s}))"
70
+
67
71
 
68
72
  @dataclass
69
73
  class LanguageGraphAsset:
70
74
  """An asset type as defined in the MAL language"""
75
+
71
76
  name: str
72
77
  own_associations: dict[str, LanguageGraphAssociation] = \
73
- field(default_factory = dict)
78
+ field(default_factory=dict)
74
79
  attack_steps: dict[str, LanguageGraphAttackStep] = \
75
- field(default_factory = dict)
76
- info: dict = field(default_factory = dict)
77
- own_super_asset: Optional[LanguageGraphAsset] = None
78
- own_sub_assets: set[LanguageGraphAsset] = field(default_factory = set)
79
- own_variables: dict = field(default_factory = dict)
80
- is_abstract: Optional[bool] = None
81
-
80
+ field(default_factory=dict)
81
+ info: dict = field(default_factory=dict)
82
+ own_super_asset: LanguageGraphAsset | None = None
83
+ own_sub_assets: set[LanguageGraphAsset] = field(default_factory=set)
84
+ own_variables: dict = field(default_factory=dict)
85
+ is_abstract: bool | None = None
82
86
 
83
87
  def to_dict(self) -> dict:
84
88
  """Convert LanguageGraphAsset to dictionary"""
@@ -87,7 +91,7 @@ class LanguageGraphAsset:
87
91
  'associations': {},
88
92
  'attack_steps': {},
89
93
  'info': self.info,
90
- 'super_asset': self.own_super_asset.name \
94
+ 'super_asset': self.own_super_asset.name
91
95
  if self.own_super_asset else "",
92
96
  'sub_assets': [asset.name for asset in self.own_sub_assets],
93
97
  'variables': {},
@@ -107,43 +111,42 @@ class LanguageGraphAsset:
107
111
  )
108
112
  return node_dict
109
113
 
110
-
111
114
  def __repr__(self) -> str:
112
115
  return f'LanguageGraphAsset(name: "{self.name}")'
113
116
 
114
-
115
117
  def __hash__(self):
116
118
  return hash(self.name)
117
119
 
118
-
119
120
  def is_subasset_of(self, target_asset: LanguageGraphAsset) -> bool:
120
- """
121
- Check if an asset extends the target asset through inheritance.
121
+ """Check if an asset extends the target asset through inheritance.
122
122
 
123
123
  Arguments:
124
+ ---------
124
125
  target_asset - the target asset we wish to evaluate if this asset
125
126
  extends
126
127
 
127
128
  Return:
129
+ ------
128
130
  True if this asset extends the target_asset via inheritance.
129
131
  False otherwise.
132
+
130
133
  """
131
- current_asset: Optional[LanguageGraphAsset] = self
134
+ current_asset: LanguageGraphAsset | None = self
132
135
  while current_asset:
133
136
  if current_asset == target_asset:
134
137
  return True
135
138
  current_asset = current_asset.own_super_asset
136
139
  return False
137
140
 
138
-
139
141
  @cached_property
140
142
  def sub_assets(self) -> set[LanguageGraphAsset]:
141
- """
142
- Return a list of all of the assets that directly or indirectly extend
143
+ """Return a list of all of the assets that directly or indirectly extend
143
144
  this asset.
144
145
 
145
146
  Return:
147
+ ------
146
148
  A list of all of the assets that extend this asset plus itself.
149
+
147
150
  """
148
151
  subassets: list[LanguageGraphAsset] = []
149
152
  for subasset in self.own_sub_assets:
@@ -154,17 +157,17 @@ class LanguageGraphAsset:
154
157
 
155
158
  return set(subassets)
156
159
 
157
-
158
160
  @cached_property
159
161
  def super_assets(self) -> list[LanguageGraphAsset]:
160
- """
161
- Return a list of all of the assets that this asset directly or
162
+ """Return a list of all of the assets that this asset directly or
162
163
  indirectly extends.
163
164
 
164
165
  Return:
166
+ ------
165
167
  A list of all of the assets that this asset extends plus itself.
168
+
166
169
  """
167
- current_asset: Optional[LanguageGraphAsset] = self
170
+ current_asset: LanguageGraphAsset | None = self
168
171
  superassets = []
169
172
  while current_asset:
170
173
  superassets.append(current_asset)
@@ -174,8 +177,7 @@ class LanguageGraphAsset:
174
177
  def associations_to(
175
178
  self, asset_type: LanguageGraphAsset
176
179
  ) -> dict[str, LanguageGraphAssociation]:
177
- """
178
- Return dict of association types that go from self
180
+ """Return dict of association types that go from self
179
181
  to given `asset_type`
180
182
  """
181
183
  associations_to_asset_type = {}
@@ -186,45 +188,44 @@ class LanguageGraphAsset:
186
188
 
187
189
  @cached_property
188
190
  def associations(self) -> dict[str, LanguageGraphAssociation]:
189
- """
190
- Return a list of all of the associations that belong to this asset
191
+ """Return a list of all of the associations that belong to this asset
191
192
  directly or indirectly via inheritance.
192
193
 
193
194
  Return:
195
+ ------
194
196
  A list of all of the associations that apply to this asset, either
195
197
  directly or via inheritance.
196
- """
197
198
 
199
+ """
198
200
  associations = dict(self.own_associations)
199
201
  if self.own_super_asset:
200
202
  associations |= self.own_super_asset.associations
201
203
  return associations
202
204
 
203
-
204
205
  @property
205
206
  def variables(
206
207
  self
207
208
  ) -> dict[str, tuple[LanguageGraphAsset, ExpressionsChain]]:
208
- """
209
- Return a list of all of the variables that belong to this asset
209
+ """Return a list of all of the variables that belong to this asset
210
210
  directly or indirectly via inheritance.
211
211
 
212
212
  Return:
213
+ ------
213
214
  A list of all of the variables that apply to this asset, either
214
215
  directly or via inheritance.
215
- """
216
216
 
217
+ """
217
218
  all_vars = dict(self.own_variables)
218
219
  if self.own_super_asset:
219
220
  all_vars |= self.own_super_asset.variables
220
221
  return all_vars
221
222
 
222
-
223
223
  def get_all_common_superassets(
224
224
  self, other: LanguageGraphAsset
225
225
  ) -> set[str]:
226
226
  """Return a set of all common ancestors between this asset
227
- and the other asset given as parameter"""
227
+ and the other asset given as parameter
228
+ """
228
229
  self_superassets = set(
229
230
  asset.name for asset in self.super_assets
230
231
  )
@@ -234,9 +235,10 @@ class LanguageGraphAsset:
234
235
  return self_superassets.intersection(other_superassets)
235
236
 
236
237
 
237
- @dataclass(frozen=True)
238
+ @dataclass(frozen=True, eq=True)
238
239
  class LanguageGraphAssociationField:
239
240
  """A field in an association"""
241
+
240
242
  asset: LanguageGraphAsset
241
243
  fieldname: str
242
244
  minimum: int
@@ -245,13 +247,13 @@ class LanguageGraphAssociationField:
245
247
 
246
248
  @dataclass(frozen=True, eq=True)
247
249
  class LanguageGraphAssociation:
250
+ """An association type between asset types as defined in the MAL language
248
251
  """
249
- An association type between asset types as defined in the MAL language
250
- """
252
+
251
253
  name: str
252
254
  left_field: LanguageGraphAssociationField
253
255
  right_field: LanguageGraphAssociationField
254
- info: dict = field(default_factory = dict, compare=False)
256
+ info: dict = field(default_factory=dict, compare=False)
255
257
 
256
258
  def to_dict(self) -> dict:
257
259
  """Convert LanguageGraphAssociation to dictionary"""
@@ -274,7 +276,6 @@ class LanguageGraphAssociation:
274
276
 
275
277
  return assoc_dict
276
278
 
277
-
278
279
  def __repr__(self) -> str:
279
280
  return (
280
281
  f'LanguageGraphAssociation(name: "{self.name}", '
@@ -282,39 +283,35 @@ class LanguageGraphAssociation:
282
283
  f'right_field: {self.right_field})'
283
284
  )
284
285
 
285
-
286
286
  @property
287
287
  def full_name(self) -> str:
288
- """
289
- Return the full name of the association. This is a combination of the
288
+ """Return the full name of the association. This is a combination of the
290
289
  association name, left field name, left asset type, right field name,
291
290
  and right asset type.
292
291
  """
293
292
  full_name = '%s_%s_%s' % (
294
- self.name,\
295
- self.left_field.fieldname,\
293
+ self.name,
294
+ self.left_field.fieldname,
296
295
  self.right_field.fieldname
297
296
  )
298
297
  return full_name
299
298
 
300
-
301
299
  def get_field(self, fieldname: str) -> LanguageGraphAssociationField:
302
- """
303
- Return the field that matches the `fieldname` given as parameter.
300
+ """Return the field that matches the `fieldname` given as parameter.
304
301
  """
305
302
  if self.right_field.fieldname == fieldname:
306
303
  return self.right_field
307
304
  return self.left_field
308
305
 
309
-
310
306
  def contains_fieldname(self, fieldname: str) -> bool:
311
- """
312
- Check if the association contains the field name given as a parameter.
307
+ """Check if the association contains the field name given as a parameter.
313
308
 
314
309
  Arguments:
310
+ ---------
315
311
  fieldname - the field name to look for
316
312
  Return True if either of the two field names matches.
317
313
  False, otherwise.
314
+
318
315
  """
319
316
  if self.left_field.fieldname == fieldname:
320
317
  return True
@@ -322,17 +319,17 @@ class LanguageGraphAssociation:
322
319
  return True
323
320
  return False
324
321
 
325
-
326
322
  def contains_asset(self, asset: Any) -> bool:
327
- """
328
- Check if the association matches the asset given as a parameter. A
323
+ """Check if the association matches the asset given as a parameter. A
329
324
  match can either be an explicit one or if the asset given subassets
330
325
  either of the two assets that are part of the association.
331
326
 
332
327
  Arguments:
328
+ ---------
333
329
  asset - the asset to look for
334
330
  Return True if either of the two asset matches.
335
331
  False, otherwise.
332
+
336
333
  """
337
334
  if asset.is_subasset_of(self.left_field.asset):
338
335
  return True
@@ -340,16 +337,16 @@ class LanguageGraphAssociation:
340
337
  return True
341
338
  return False
342
339
 
343
-
344
340
  def get_opposite_fieldname(self, fieldname: str) -> str:
345
- """
346
- Return the opposite field name if the association contains the field
341
+ """Return the opposite field name if the association contains the field
347
342
  name given as a parameter.
348
343
 
349
344
  Arguments:
345
+ ---------
350
346
  fieldname - the field name to look for
351
347
  Return the other field name if the parameter matched either of the
352
348
  two. None, otherwise.
349
+
353
350
  """
354
351
  if self.left_field.fieldname == fieldname:
355
352
  return self.right_field.fieldname
@@ -364,27 +361,26 @@ class LanguageGraphAssociation:
364
361
 
365
362
  @dataclass
366
363
  class LanguageGraphAttackStep:
364
+ """An attack step belonging to an asset type in the MAL language
367
365
  """
368
- An attack step belonging to an asset type in the MAL language
369
- """
366
+
370
367
  name: str
371
368
  type: Literal["or", "and", "defense", "exist", "notExist"]
372
369
  asset: LanguageGraphAsset
373
- ttc: Optional[dict] = field(default_factory = dict)
370
+ ttc: dict | None = field(default_factory=dict)
374
371
  overrides: bool = False
375
372
 
376
373
  own_children: dict[LanguageGraphAttackStep, list[ExpressionsChain | None]] = (
377
- field(default_factory = dict)
374
+ field(default_factory=dict)
378
375
  )
379
376
  own_parents: dict[LanguageGraphAttackStep, list[ExpressionsChain | None]] = (
380
- field(default_factory = dict)
377
+ field(default_factory=dict)
381
378
  )
382
- info: dict = field(default_factory = dict)
383
- inherits: Optional[LanguageGraphAttackStep] = None
379
+ info: dict = field(default_factory=dict)
380
+ inherits: LanguageGraphAttackStep | None = None
384
381
  own_requires: list[ExpressionsChain] = field(default_factory=list)
385
- tags: list = field(default_factory = list)
386
- detectors: dict = field(default_factory = lambda: {})
387
-
382
+ tags: list = field(default_factory=list)
383
+ detectors: dict = field(default_factory=dict)
388
384
 
389
385
  def __hash__(self):
390
386
  return hash(self.full_name)
@@ -393,10 +389,8 @@ class LanguageGraphAttackStep:
393
389
  def children(self) -> dict[
394
390
  LanguageGraphAttackStep, list[ExpressionsChain | None]
395
391
  ]:
392
+ """Get all (both own and inherited) children of a LanguageGraphAttackStep
396
393
  """
397
- Get all (both own and inherited) children of a LanguageGraphAttackStep
398
- """
399
-
400
394
  all_children = dict(self.own_children)
401
395
 
402
396
  if self.overrides:
@@ -425,8 +419,7 @@ class LanguageGraphAttackStep:
425
419
 
426
420
  @property
427
421
  def full_name(self) -> str:
428
- """
429
- Return the full name of the attack step. This is a combination of the
422
+ """Return the full name of the attack step. This is a combination of the
430
423
  asset type name to which the attack step belongs and attack step name
431
424
  itself.
432
425
  """
@@ -471,7 +464,6 @@ class LanguageGraphAttackStep:
471
464
 
472
465
  return node_dict
473
466
 
474
-
475
467
  @cached_property
476
468
  def requires(self):
477
469
  if not hasattr(self, 'own_requires'):
@@ -483,33 +475,31 @@ class LanguageGraphAttackStep:
483
475
  requirements.extend(self.inherits.requires)
484
476
  return requirements
485
477
 
486
-
487
478
  def __repr__(self) -> str:
488
479
  return str(self.to_dict())
489
480
 
490
481
 
491
482
  class ExpressionsChain:
492
- """
493
- A series of linked step expressions that specify the association path and
483
+ """A series of linked step expressions that specify the association path and
494
484
  operations to take to reach the child/parent attack step.
495
485
  """
486
+
496
487
  def __init__(self,
497
488
  type: str,
498
- left_link: Optional[ExpressionsChain] = None,
499
- right_link: Optional[ExpressionsChain] = None,
500
- sub_link: Optional[ExpressionsChain] = None,
501
- fieldname: Optional[str] = None,
502
- association = None,
503
- subtype = None
489
+ left_link: ExpressionsChain | None = None,
490
+ right_link: ExpressionsChain | None = None,
491
+ sub_link: ExpressionsChain | None = None,
492
+ fieldname: str | None = None,
493
+ association=None,
494
+ subtype=None
504
495
  ):
505
496
  self.type = type
506
- self.left_link: Optional[ExpressionsChain] = left_link
507
- self.right_link: Optional[ExpressionsChain] = right_link
508
- self.sub_link: Optional[ExpressionsChain] = sub_link
509
- self.fieldname: Optional[str] = fieldname
510
- self.association: Optional[LanguageGraphAssociation] = association
511
- self.subtype: Optional[Any] = subtype
512
-
497
+ self.left_link: ExpressionsChain | None = left_link
498
+ self.right_link: ExpressionsChain | None = right_link
499
+ self.sub_link: ExpressionsChain | None = sub_link
500
+ self.fieldname: str | None = fieldname
501
+ self.association: LanguageGraphAssociation | None = association
502
+ self.subtype: Any | None = subtype
513
503
 
514
504
  def to_dict(self) -> dict:
515
505
  """Convert ExpressionsChain to dictionary"""
@@ -540,7 +530,7 @@ class ExpressionsChain:
540
530
  (
541
531
  self.fieldname,
542
532
  json.dumps(self.association.to_dict(),
543
- indent = 2)
533
+ indent=2)
544
534
  )
545
535
  )
546
536
 
@@ -587,7 +577,7 @@ class ExpressionsChain:
587
577
  def _from_dict(cls,
588
578
  serialized_expr_chain: dict,
589
579
  lang_graph: LanguageGraph,
590
- ) -> Optional[ExpressionsChain]:
580
+ ) -> ExpressionsChain | None:
591
581
  """Create ExpressionsChain from dict
592
582
  Args:
593
583
  serialized_expr_chain - expressions chain in dict format
@@ -595,12 +585,11 @@ class ExpressionsChain:
595
585
  associations, and attack steps relevant for
596
586
  the expressions chain
597
587
  """
598
-
599
588
  if serialized_expr_chain is None or not serialized_expr_chain:
600
589
  return None
601
590
 
602
591
  if 'type' not in serialized_expr_chain:
603
- logger.debug(json.dumps(serialized_expr_chain, indent = 2))
592
+ logger.debug(json.dumps(serialized_expr_chain, indent=2))
604
593
  msg = 'Missing expressions chain type!'
605
594
  logger.error(msg)
606
595
  raise LanguageGraphAssociationError(msg)
@@ -617,15 +606,15 @@ class ExpressionsChain:
617
606
  lang_graph
618
607
  )
619
608
  new_expr_chain = ExpressionsChain(
620
- type = expr_chain_type,
621
- left_link = left_link,
622
- right_link = right_link
609
+ type=expr_chain_type,
610
+ left_link=left_link,
611
+ right_link=right_link
623
612
  )
624
613
  return new_expr_chain
625
614
 
626
615
  case 'field':
627
616
  assoc_name = list(serialized_expr_chain.keys())[0]
628
- target_asset = lang_graph.assets[\
617
+ target_asset = lang_graph.assets[
629
618
  serialized_expr_chain[assoc_name]['asset type']]
630
619
  fieldname = serialized_expr_chain[assoc_name]['fieldname']
631
620
 
@@ -645,9 +634,9 @@ class ExpressionsChain:
645
634
  )
646
635
 
647
636
  new_expr_chain = ExpressionsChain(
648
- type = 'field',
649
- association = association,
650
- fieldname = fieldname
637
+ type='field',
638
+ association=association,
639
+ fieldname=fieldname
651
640
  )
652
641
  return new_expr_chain
653
642
 
@@ -657,8 +646,8 @@ class ExpressionsChain:
657
646
  lang_graph
658
647
  )
659
648
  new_expr_chain = ExpressionsChain(
660
- type = 'transitive',
661
- sub_link = sub_link
649
+ type='transitive',
650
+ sub_link=sub_link
662
651
  )
663
652
  return new_expr_chain
664
653
 
@@ -676,9 +665,9 @@ class ExpressionsChain:
676
665
  raise LanguageGraphException(msg % subtype_name)
677
666
 
678
667
  new_expr_chain = ExpressionsChain(
679
- type = 'subType',
680
- sub_link = sub_link,
681
- subtype = subtype_asset
668
+ type='subType',
669
+ sub_link=sub_link,
670
+ subtype=subtype_asset
682
671
  )
683
672
  return new_expr_chain
684
673
 
@@ -689,14 +678,14 @@ class ExpressionsChain:
689
678
  msg % serialized_expr_chain['type']
690
679
  )
691
680
 
692
-
693
681
  def __repr__(self) -> str:
694
682
  return str(self.to_dict())
695
683
 
696
684
 
697
- class LanguageGraph():
685
+ class LanguageGraph:
698
686
  """Graph representation of a MAL language"""
699
- def __init__(self, lang: Optional[dict] = None):
687
+
688
+ def __init__(self, lang: dict | None = None):
700
689
  self.assets: dict[str, LanguageGraphAsset] = {}
701
690
  if lang is not None:
702
691
  self._lang_spec: dict = lang
@@ -706,32 +695,31 @@ class LanguageGraph():
706
695
  }
707
696
  self._generate_graph()
708
697
 
709
-
710
698
  def __repr__(self) -> str:
711
699
  return (f'LanguageGraph(id: "{self.metadata.get("id", "N/A")}", '
712
700
  f'version: "{self.metadata.get("version", "N/A")}")')
713
701
 
714
-
715
702
  @classmethod
716
703
  def from_mal_spec(cls, mal_spec_file: str) -> LanguageGraph:
717
- """
718
- Create a LanguageGraph from a .mal file (a MAL spec).
704
+ """Create a LanguageGraph from a .mal file (a MAL spec).
719
705
 
720
706
  Arguments:
707
+ ---------
721
708
  mal_spec_file - the path to the .mal file
709
+
722
710
  """
723
711
  logger.info("Loading mal spec %s", mal_spec_file)
724
712
  return LanguageGraph(MalCompiler().compile(mal_spec_file))
725
713
 
726
-
727
714
  @classmethod
728
715
  def from_mar_archive(cls, mar_archive: str) -> LanguageGraph:
729
- """
730
- Create a LanguageGraph from a ".mar" archive provided by malc
716
+ """Create a LanguageGraph from a ".mar" archive provided by malc
731
717
  (https://github.com/mal-lang/malc).
732
718
 
733
719
  Arguments:
720
+ ---------
734
721
  mar_archive - the path to a ".mar" archive
722
+
735
723
  """
736
724
  logger.info('Loading mar archive %s', mar_archive)
737
725
  with zipfile.ZipFile(mar_archive, 'r') as archive:
@@ -740,7 +728,6 @@ class LanguageGraph():
740
728
 
741
729
  def _to_dict(self):
742
730
  """Converts LanguageGraph into a dict"""
743
-
744
731
  logger.debug(
745
732
  'Serializing %s assets.', len(self.assets.items())
746
733
  )
@@ -753,8 +740,7 @@ class LanguageGraph():
753
740
 
754
741
  @property
755
742
  def associations(self) -> set[LanguageGraphAssociation]:
756
- """
757
- Return all associations in the language graph.
743
+ """Return all associations in the language graph.
758
744
  """
759
745
  return {assoc for asset in self.assets.values() for assoc in asset.associations.values()}
760
746
 
@@ -771,214 +757,126 @@ class LanguageGraph():
771
757
  """Save to json/yml depending on extension"""
772
758
  return save_dict_to_file(filename, self._to_dict())
773
759
 
774
-
775
760
  @classmethod
776
761
  def _from_dict(cls, serialized_graph: dict) -> LanguageGraph:
777
- """Create LanguageGraph from dict
778
- Args:
779
- serialized_graph - LanguageGraph in dict format
780
- """
781
-
762
+ """Rebuild a LanguageGraph instance from its serialized dict form."""
782
763
  logger.debug('Create language graph from dictionary.')
783
764
  lang_graph = LanguageGraph()
784
765
  lang_graph.metadata = serialized_graph.pop('metadata')
785
766
 
786
- # Recreate all of the assets
787
- for asset_dict in serialized_graph.values():
788
- logger.debug(
789
- 'Create asset language graph nodes for asset %s',
790
- asset_dict['name']
791
- )
792
- asset_node = LanguageGraphAsset(
793
- name = asset_dict['name'],
794
- own_associations = {},
795
- attack_steps = {},
796
- info = asset_dict['info'],
797
- own_super_asset = None,
798
- own_sub_assets = set(),
799
- own_variables = {},
800
- is_abstract = asset_dict['is_abstract']
801
- )
802
- lang_graph.assets[asset_dict['name']] = asset_node
803
-
804
- # Relink assets based on inheritance
805
- for asset_dict in serialized_graph.values():
806
- asset = lang_graph.assets[asset_dict['name']]
807
- super_asset_name = asset_dict['super_asset']
808
- if not super_asset_name:
809
- continue
810
-
811
- super_asset = lang_graph.assets[super_asset_name]
812
- if not super_asset:
813
- msg = 'Failed to find super asset "%s" for asset "%s"!'
814
- logger.error(
815
- msg, asset_dict["super_asset"], asset_dict["name"])
816
- raise LanguageGraphSuperAssetNotFoundError(
817
- msg % (asset_dict["super_asset"], asset_dict["name"]))
818
-
819
- super_asset.own_sub_assets.add(asset)
820
- asset.own_super_asset = super_asset
821
-
822
- # Generate all of the association nodes of the language graph.
823
- for asset_dict in serialized_graph.values():
824
- logger.debug(
825
- 'Create association language graph nodes for asset %s',
826
- asset_dict['name']
767
+ # Create asset nodes
768
+ for asset in serialized_graph.values():
769
+ logger.debug('Create asset %s', asset['name'])
770
+ lang_graph.assets[asset['name']] = LanguageGraphAsset(
771
+ name=asset['name'],
772
+ own_associations={},
773
+ attack_steps={},
774
+ info=asset['info'],
775
+ own_super_asset=None,
776
+ own_sub_assets=set(),
777
+ own_variables={},
778
+ is_abstract=asset['is_abstract']
827
779
  )
828
780
 
829
- asset = lang_graph.assets[asset_dict['name']]
830
- for association in asset_dict['associations'].values():
831
- left_asset = lang_graph.assets[association['left']['asset']]
832
- if not left_asset:
833
- msg = 'Left asset "%s" for association "%s" not found!'
834
- logger.error(
835
- msg, association['left']['asset'],
836
- association['name'])
837
- raise LanguageGraphAssociationError(
838
- msg % (association['left']['asset'],
839
- association['name']))
840
-
841
- right_asset = lang_graph.assets[association['right']['asset']]
842
- if not right_asset:
843
- msg = 'Right asset "%s" for association "%s" not found!'
844
- logger.error(
845
- msg, association['right']['asset'],
846
- association['name'])
847
- raise LanguageGraphAssociationError(
848
- msg % (association['right']['asset'],
849
- association['name'])
850
- )
851
-
781
+ # Link inheritance
782
+ for asset in serialized_graph.values():
783
+ asset_node = lang_graph.assets[asset['name']]
784
+ if super_name := asset['super_asset']:
785
+ try:
786
+ super_asset = lang_graph.assets[super_name]
787
+ except KeyError:
788
+ msg = f'Super asset "{super_name}" for "{asset["name"]}" not found'
789
+ logger.error(msg)
790
+ raise LanguageGraphSuperAssetNotFoundError(msg)
791
+ super_asset.own_sub_assets.add(asset_node)
792
+ asset_node.own_super_asset = super_asset
793
+
794
+ # Associations
795
+ for asset in serialized_graph.values():
796
+ logger.debug('Create associations for asset %s', asset['name'])
797
+ a_node = lang_graph.assets[asset['name']]
798
+ for assoc in asset['associations'].values():
799
+ try:
800
+ left = lang_graph.assets[assoc['left']['asset']]
801
+ right = lang_graph.assets[assoc['right']['asset']]
802
+ except KeyError as e:
803
+ side = 'Left' if 'left' in str(e) else 'Right'
804
+ msg = f'{side} asset for association "{assoc["name"]}" not found'
805
+ logger.error(msg)
806
+ raise LanguageGraphAssociationError(msg)
852
807
  assoc_node = LanguageGraphAssociation(
853
- name = association['name'],
854
- left_field = LanguageGraphAssociationField(
855
- left_asset,
856
- association['left']['fieldname'],
857
- association['left']['min'],
858
- association['left']['max']),
859
- right_field = LanguageGraphAssociationField(
860
- right_asset,
861
- association['right']['fieldname'],
862
- association['right']['min'],
863
- association['right']['max']),
864
- info = association['info']
808
+ name=assoc['name'],
809
+ left_field=LanguageGraphAssociationField(
810
+ left, assoc['left']['fieldname'],
811
+ assoc['left']['min'], assoc['left']['max']
812
+ ),
813
+ right_field=LanguageGraphAssociationField(
814
+ right, assoc['right']['fieldname'],
815
+ assoc['right']['min'], assoc['right']['max']
816
+ ),
817
+ info=assoc['info']
865
818
  )
866
-
867
- # Add the association to the left and right asset
868
- lang_graph._link_association_to_assets(assoc_node,
869
- left_asset, right_asset)
870
-
871
- # Recreate the variables
872
- for asset_dict in serialized_graph.values():
873
- asset = lang_graph.assets[asset_dict['name']]
874
- for variable_name, var_target in asset_dict['variables'].items():
875
- (target_asset_name, expr_chain_dict) = var_target
876
- target_asset = lang_graph.assets[target_asset_name]
877
- expr_chain = ExpressionsChain._from_dict(
878
- expr_chain_dict,
879
- lang_graph
819
+ lang_graph._link_association_to_assets(assoc_node, left, right)
820
+
821
+ # Variables
822
+ for asset in serialized_graph.values():
823
+ a_node = lang_graph.assets[asset['name']]
824
+ for var, (target_name, expr_dict) in asset['variables'].items():
825
+ target = lang_graph.assets[target_name]
826
+ a_node.own_variables[var] = (
827
+ target, ExpressionsChain._from_dict(expr_dict, lang_graph)
880
828
  )
881
- asset.own_variables[variable_name] = (target_asset, expr_chain)
882
829
 
883
- # Recreate the attack steps
884
- for asset_dict in serialized_graph.values():
885
- asset = lang_graph.assets[asset_dict['name']]
886
- logger.debug(
887
- 'Create attack steps language graph nodes for asset %s',
888
- asset_dict['name']
889
- )
890
- for attack_step_dict in asset_dict['attack_steps'].values():
891
- attack_step_node = LanguageGraphAttackStep(
892
- name = attack_step_dict['name'],
893
- type = attack_step_dict['type'],
894
- asset = asset,
895
- ttc = attack_step_dict['ttc'],
896
- overrides = attack_step_dict['overrides'],
897
- own_children = {},
898
- own_parents = {},
899
- info = attack_step_dict['info'],
900
- tags = list(attack_step_dict['tags'])
830
+ # Attack steps
831
+ for asset in serialized_graph.values():
832
+ a_node = lang_graph.assets[asset['name']]
833
+ for step in asset['attack_steps'].values():
834
+ a_node.attack_steps[step['name']] = LanguageGraphAttackStep(
835
+ name=step['name'],
836
+ type=step['type'],
837
+ asset=a_node,
838
+ ttc=step['ttc'],
839
+ overrides=step['overrides'],
840
+ own_children={}, own_parents={},
841
+ info=step['info'],
842
+ tags=list(step['tags'])
901
843
  )
902
- asset.attack_steps[attack_step_dict['name']] = \
903
- attack_step_node
904
-
905
- # Relink attack steps based on inheritence
906
- for asset_dict in serialized_graph.values():
907
- asset = lang_graph.assets[asset_dict['name']]
908
- for attack_step_dict in asset_dict['attack_steps'].values():
909
- if 'inherits' in attack_step_dict and \
910
- attack_step_dict['inherits'] is not None:
911
- attack_step = asset.attack_steps[
912
- attack_step_dict['name']]
913
- ancestor_asset_name, ancestor_attack_step_name = \
914
- disaggregate_attack_step_full_name(
915
- attack_step_dict['inherits'])
916
- ancestor_asset = lang_graph.assets[ancestor_asset_name]
917
- ancestor_attack_step = ancestor_asset.attack_steps[\
918
- ancestor_attack_step_name]
919
- attack_step.inherits = ancestor_attack_step
920
-
921
-
922
- # Relink attack steps based on expressions chains
923
- for asset_dict in serialized_graph.values():
924
- asset = lang_graph.assets[asset_dict['name']]
925
- for attack_step_dict in asset_dict['attack_steps'].values():
926
- attack_step = asset.attack_steps[attack_step_dict['name']]
927
- for child_target in attack_step_dict['own_children'].items():
928
- target_full_attack_step_name = child_target[0]
929
- expr_chains = child_target[1]
930
- target_asset_name, target_attack_step_name = \
931
- disaggregate_attack_step_full_name(target_full_attack_step_name)
932
- target_asset = lang_graph.assets[target_asset_name]
933
- target_attack_step = target_asset.attack_steps[
934
- target_attack_step_name]
935
- for expr_chain_dict in expr_chains:
936
- expr_chain = ExpressionsChain._from_dict(
937
- expr_chain_dict,
938
- lang_graph
939
- )
940
-
941
- if target_attack_step in attack_step.own_children:
942
- attack_step.own_children[target_attack_step].append(expr_chain)
943
- else:
944
- attack_step.own_children[target_attack_step] = [expr_chain]
945
844
 
946
- for (target_step_full_name, expr_chains) in attack_step_dict['own_parents'].items():
947
- target_asset_name, target_attack_step_name = \
948
- disaggregate_attack_step_full_name(
949
- target_step_full_name
950
- )
951
- target_asset = lang_graph.assets[target_asset_name]
952
- target_attack_step = target_asset.attack_steps[target_attack_step_name]
953
- for expr_chain_dict in expr_chains:
954
- expr_chain = ExpressionsChain._from_dict(
955
- expr_chain_dict, lang_graph
956
- )
957
-
958
- if target_attack_step in attack_step.own_parents:
959
- attack_step.own_parents[target_attack_step].append(
960
- expr_chain
961
- )
962
- else:
963
- attack_step.own_parents[target_attack_step] = [expr_chain]
964
-
965
- # Recreate the requirements of exist and notExist attack steps
966
- if attack_step.type == 'exist' or \
967
- attack_step.type == 'notExist':
968
- if 'requires' in attack_step_dict:
969
- expr_chains = attack_step_dict['requires']
970
- attack_step.own_requires = []
971
- for expr_chain_dict in expr_chains:
972
- expr_chain = ExpressionsChain._from_dict(
973
- expr_chain_dict,
974
- lang_graph
975
- )
976
- if expr_chain:
977
- attack_step.own_requires.append(expr_chain)
845
+ # Inheritance for attack steps
846
+ for asset in serialized_graph.values():
847
+ a_node = lang_graph.assets[asset['name']]
848
+ for step in asset['attack_steps'].values():
849
+ if not (inh := step.get('inherits')):
850
+ continue
851
+ a_step = a_node.attack_steps[step['name']]
852
+ a_name, s_name = disaggregate_attack_step_full_name(inh)
853
+ a_step.inherits = lang_graph.assets[a_name].attack_steps[s_name]
854
+
855
+ # Expression chains and requirements
856
+ for asset in serialized_graph.values():
857
+ a_node = lang_graph.assets[asset['name']]
858
+ for step in asset['attack_steps'].values():
859
+ s_node = a_node.attack_steps[step['name']]
860
+ for tgt_name, exprs in step['own_children'].items():
861
+ t_asset, t_step = disaggregate_attack_step_full_name(tgt_name)
862
+ t_node = lang_graph.assets[t_asset].attack_steps[t_step]
863
+ for expr in exprs:
864
+ chain = ExpressionsChain._from_dict(expr, lang_graph)
865
+ s_node.own_children.setdefault(t_node, []).append(chain)
866
+ for tgt_name, exprs in step['own_parents'].items():
867
+ t_asset, t_step = disaggregate_attack_step_full_name(tgt_name)
868
+ t_node = lang_graph.assets[t_asset].attack_steps[t_step]
869
+ for expr in exprs:
870
+ chain = ExpressionsChain._from_dict(expr, lang_graph)
871
+ s_node.own_parents.setdefault(t_node, []).append(chain)
872
+ if step['type'] in ('exist', 'notExist') and (reqs := step.get('requires')):
873
+ s_node.own_requires = [
874
+ chain for expr in reqs
875
+ if (chain := ExpressionsChain._from_dict(expr, lang_graph))
876
+ ]
978
877
 
979
878
  return lang_graph
980
879
 
981
-
982
880
  @classmethod
983
881
  def load_from_file(cls, filename: str) -> LanguageGraph:
984
882
  """Create LanguageGraph from mal, mar, yaml or json"""
@@ -989,7 +887,7 @@ class LanguageGraph():
989
887
  lang_graph = cls.from_mar_archive(filename)
990
888
  elif filename.endswith(('.yaml', '.yml')):
991
889
  lang_graph = cls._from_dict(load_dict_from_yaml_file(filename))
992
- elif filename.endswith(('.json')):
890
+ elif filename.endswith('.json'):
993
891
  lang_graph = cls._from_dict(load_dict_from_json_file(filename))
994
892
  else:
995
893
  raise TypeError(
@@ -998,18 +896,17 @@ class LanguageGraph():
998
896
 
999
897
  if lang_graph:
1000
898
  return lang_graph
1001
- else:
1002
- raise LanguageGraphException(
1003
- f'Failed to load language graph from file "{filename}".'
1004
- )
1005
-
899
+ raise LanguageGraphException(
900
+ f'Failed to load language graph from file "{filename}".'
901
+ )
1006
902
 
1007
903
  def save_language_specification_to_json(self, filename: str) -> None:
1008
- """
1009
- Save a MAL language specification dictionary to a JSON file
904
+ """Save a MAL language specification dictionary to a JSON file
1010
905
 
1011
906
  Arguments:
907
+ ---------
1012
908
  filename - the JSON filename where the language specification will be written
909
+
1013
910
  """
1014
911
  logger.info('Save language specification to %s', filename)
1015
912
 
@@ -1025,8 +922,7 @@ class LanguageGraph():
1025
922
  None,
1026
923
  str
1027
924
  ]:
1028
- """
1029
- The attack step expression just adds the name of the attack
925
+ """The attack step expression just adds the name of the attack
1030
926
  step. All other step expressions only modify the target
1031
927
  asset and parent associations chain.
1032
928
  """
@@ -1039,18 +935,16 @@ class LanguageGraph():
1039
935
  def process_set_operation_step_expression(
1040
936
  self,
1041
937
  target_asset: LanguageGraphAsset,
1042
- expr_chain: Optional[ExpressionsChain],
938
+ expr_chain: ExpressionsChain | None,
1043
939
  step_expression: dict[str, Any]
1044
940
  ) -> tuple[
1045
941
  LanguageGraphAsset,
1046
942
  ExpressionsChain,
1047
943
  None
1048
944
  ]:
1049
- """
1050
- The set operators are used to combine the left hand and right
945
+ """The set operators are used to combine the left hand and right
1051
946
  hand targets accordingly.
1052
947
  """
1053
-
1054
948
  lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
1055
949
  target_asset,
1056
950
  expr_chain,
@@ -1077,9 +971,9 @@ class LanguageGraph():
1077
971
  )
1078
972
 
1079
973
  new_expr_chain = ExpressionsChain(
1080
- type = step_expression['type'],
1081
- left_link = lh_expr_chain,
1082
- right_link = rh_expr_chain
974
+ type=step_expression['type'],
975
+ left_link=lh_expr_chain,
976
+ right_link=rh_expr_chain
1083
977
  )
1084
978
  return (
1085
979
  lh_target_asset,
@@ -1123,12 +1017,10 @@ class LanguageGraph():
1123
1017
  ExpressionsChain,
1124
1018
  None
1125
1019
  ]:
1126
- """
1127
- Change the target asset from the current one to the associated
1020
+ """Change the target asset from the current one to the associated
1128
1021
  asset given the specified field name and add the parent
1129
1022
  fieldname and association to the parent associations chain.
1130
1023
  """
1131
-
1132
1024
  fieldname = step_expression['name']
1133
1025
 
1134
1026
  if target_asset is None:
@@ -1138,21 +1030,21 @@ class LanguageGraph():
1138
1030
 
1139
1031
  new_target_asset = None
1140
1032
  for association in target_asset.associations.values():
1141
- if (association.left_field.fieldname == fieldname and \
1033
+ if (association.left_field.fieldname == fieldname and
1142
1034
  target_asset.is_subasset_of(
1143
1035
  association.right_field.asset)):
1144
1036
  new_target_asset = association.left_field.asset
1145
1037
 
1146
- if (association.right_field.fieldname == fieldname and \
1038
+ if (association.right_field.fieldname == fieldname and
1147
1039
  target_asset.is_subasset_of(
1148
1040
  association.left_field.asset)):
1149
1041
  new_target_asset = association.right_field.asset
1150
1042
 
1151
1043
  if new_target_asset:
1152
1044
  new_expr_chain = ExpressionsChain(
1153
- type = 'field',
1154
- fieldname = fieldname,
1155
- association = association
1045
+ type='field',
1046
+ fieldname=fieldname,
1047
+ association=association
1156
1048
  )
1157
1049
  return (
1158
1050
  new_target_asset,
@@ -1167,15 +1059,14 @@ class LanguageGraph():
1167
1059
  def process_transitive_step_expression(
1168
1060
  self,
1169
1061
  target_asset: LanguageGraphAsset,
1170
- expr_chain: Optional[ExpressionsChain],
1062
+ expr_chain: ExpressionsChain | None,
1171
1063
  step_expression: dict[str, Any]
1172
1064
  ) -> tuple[
1173
1065
  LanguageGraphAsset,
1174
1066
  ExpressionsChain,
1175
1067
  None
1176
1068
  ]:
1177
- """
1178
- Create a transitive tuple entry that applies to the next
1069
+ """Create a transitive tuple entry that applies to the next
1179
1070
  component of the step expression.
1180
1071
  """
1181
1072
  result_target_asset, result_expr_chain, _ = (
@@ -1186,8 +1077,8 @@ class LanguageGraph():
1186
1077
  )
1187
1078
  )
1188
1079
  new_expr_chain = ExpressionsChain(
1189
- type = 'transitive',
1190
- sub_link = result_expr_chain
1080
+ type='transitive',
1081
+ sub_link=result_expr_chain
1191
1082
  )
1192
1083
  return (
1193
1084
  result_target_asset,
@@ -1198,19 +1089,17 @@ class LanguageGraph():
1198
1089
  def process_subType_step_expression(
1199
1090
  self,
1200
1091
  target_asset: LanguageGraphAsset,
1201
- expr_chain: Optional[ExpressionsChain],
1092
+ expr_chain: ExpressionsChain | None,
1202
1093
  step_expression: dict[str, Any]
1203
1094
  ) -> tuple[
1204
1095
  LanguageGraphAsset,
1205
1096
  ExpressionsChain,
1206
1097
  None
1207
1098
  ]:
1208
- """
1209
- Create a subType tuple entry that applies to the next
1099
+ """Create a subType tuple entry that applies to the next
1210
1100
  component of the step expression and changes the target
1211
1101
  asset to the subasset.
1212
1102
  """
1213
-
1214
1103
  subtype_name = step_expression['subType']
1215
1104
  result_target_asset, result_expr_chain, _ = (
1216
1105
  self.process_step_expression(
@@ -1237,9 +1126,9 @@ class LanguageGraph():
1237
1126
  )
1238
1127
 
1239
1128
  new_expr_chain = ExpressionsChain(
1240
- type = 'subType',
1241
- sub_link = result_expr_chain,
1242
- subtype = subtype_asset
1129
+ type='subType',
1130
+ sub_link=result_expr_chain,
1131
+ subtype=subtype_asset
1243
1132
  )
1244
1133
  return (
1245
1134
  subtype_asset,
@@ -1250,15 +1139,14 @@ class LanguageGraph():
1250
1139
  def process_collect_step_expression(
1251
1140
  self,
1252
1141
  target_asset: LanguageGraphAsset,
1253
- expr_chain: Optional[ExpressionsChain],
1142
+ expr_chain: ExpressionsChain | None,
1254
1143
  step_expression: dict[str, Any]
1255
1144
  ) -> tuple[
1256
1145
  LanguageGraphAsset,
1257
- Optional[ExpressionsChain],
1258
- Optional[str]
1146
+ ExpressionsChain | None,
1147
+ str | None
1259
1148
  ]:
1260
- """
1261
- Apply the right hand step expression to left hand step
1149
+ """Apply the right hand step expression to left hand step
1262
1150
  expression target asset and parent associations chain.
1263
1151
  """
1264
1152
  lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
@@ -1280,9 +1168,9 @@ class LanguageGraph():
1280
1168
  new_expr_chain = lh_expr_chain
1281
1169
  if rh_expr_chain:
1282
1170
  new_expr_chain = ExpressionsChain(
1283
- type = 'collect',
1284
- left_link = lh_expr_chain,
1285
- right_link = rh_expr_chain
1171
+ type='collect',
1172
+ left_link=lh_expr_chain,
1173
+ right_link=rh_expr_chain
1286
1174
  )
1287
1175
 
1288
1176
  return (
@@ -1293,17 +1181,17 @@ class LanguageGraph():
1293
1181
 
1294
1182
  def process_step_expression(self,
1295
1183
  target_asset: LanguageGraphAsset,
1296
- expr_chain: Optional[ExpressionsChain],
1184
+ expr_chain: ExpressionsChain | None,
1297
1185
  step_expression: dict
1298
1186
  ) -> tuple[
1299
1187
  LanguageGraphAsset,
1300
- Optional[ExpressionsChain],
1301
- Optional[str]
1188
+ ExpressionsChain | None,
1189
+ str | None
1302
1190
  ]:
1303
- """
1304
- Recursively process an attack step expression.
1191
+ """Recursively process an attack step expression.
1305
1192
 
1306
1193
  Arguments:
1194
+ ---------
1307
1195
  target_asset - The asset type that this step expression should
1308
1196
  apply to. Initially it will contain the asset
1309
1197
  type to which the attack step belongs.
@@ -1317,21 +1205,22 @@ class LanguageGraph():
1317
1205
  step_expression - A dictionary containing the step expression.
1318
1206
 
1319
1207
  Return:
1208
+ ------
1320
1209
  A tuple triplet containing the target asset, the resulting parent
1321
1210
  associations chain, and the name of the attack step.
1322
- """
1323
1211
 
1212
+ """
1324
1213
  if logger.isEnabledFor(logging.DEBUG):
1325
1214
  # Avoid running json.dumps when not in debug
1326
1215
  logger.debug(
1327
1216
  'Processing Step Expression:\n%s',
1328
- json.dumps(step_expression, indent = 2)
1217
+ json.dumps(step_expression, indent=2)
1329
1218
  )
1330
1219
 
1331
1220
  result: tuple[
1332
1221
  LanguageGraphAsset,
1333
- Optional[ExpressionsChain],
1334
- Optional[str]
1222
+ ExpressionsChain | None,
1223
+ str | None
1335
1224
  ]
1336
1225
 
1337
1226
  match (step_expression['type']):
@@ -1371,14 +1260,14 @@ class LanguageGraph():
1371
1260
 
1372
1261
  def reverse_expr_chain(
1373
1262
  self,
1374
- expr_chain: Optional[ExpressionsChain],
1375
- reverse_chain: Optional[ExpressionsChain]
1376
- ) -> Optional[ExpressionsChain]:
1377
- """
1378
- Recursively reverse the associations chain. From parent to child or
1263
+ expr_chain: ExpressionsChain | None,
1264
+ reverse_chain: ExpressionsChain | None
1265
+ ) -> ExpressionsChain | None:
1266
+ """Recursively reverse the associations chain. From parent to child or
1379
1267
  vice versa.
1380
1268
 
1381
1269
  Arguments:
1270
+ ---------
1382
1271
  expr_chain - A chain of nested tuples that specify the
1383
1272
  associations and set operations chain from an
1384
1273
  attack step to its connected attack step.
@@ -1386,93 +1275,96 @@ class LanguageGraph():
1386
1275
  current reversed associations chain.
1387
1276
 
1388
1277
  Return:
1278
+ ------
1389
1279
  The resulting reversed associations chain.
1280
+
1390
1281
  """
1391
1282
  if not expr_chain:
1392
1283
  return reverse_chain
1393
- else:
1394
- match (expr_chain.type):
1395
- case 'union' | 'intersection' | 'difference' | 'collect':
1396
- left_reverse_chain = \
1397
- self.reverse_expr_chain(expr_chain.left_link,
1398
- reverse_chain)
1399
- right_reverse_chain = \
1400
- self.reverse_expr_chain(expr_chain.right_link,
1401
- reverse_chain)
1402
- if expr_chain.type == 'collect':
1403
- new_expr_chain = ExpressionsChain(
1404
- type = expr_chain.type,
1405
- left_link = right_reverse_chain,
1406
- right_link = left_reverse_chain
1407
- )
1408
- else:
1409
- new_expr_chain = ExpressionsChain(
1410
- type = expr_chain.type,
1411
- left_link = left_reverse_chain,
1412
- right_link = right_reverse_chain
1413
- )
1414
-
1415
- return new_expr_chain
1416
-
1417
- case 'transitive':
1418
- result_reverse_chain = self.reverse_expr_chain(
1419
- expr_chain.sub_link, reverse_chain)
1284
+ match (expr_chain.type):
1285
+ case 'union' | 'intersection' | 'difference' | 'collect':
1286
+ left_reverse_chain = \
1287
+ self.reverse_expr_chain(expr_chain.left_link,
1288
+ reverse_chain)
1289
+ right_reverse_chain = \
1290
+ self.reverse_expr_chain(expr_chain.right_link,
1291
+ reverse_chain)
1292
+ if expr_chain.type == 'collect':
1420
1293
  new_expr_chain = ExpressionsChain(
1421
- type = 'transitive',
1422
- sub_link = result_reverse_chain
1294
+ type=expr_chain.type,
1295
+ left_link=right_reverse_chain,
1296
+ right_link=left_reverse_chain
1297
+ )
1298
+ else:
1299
+ new_expr_chain = ExpressionsChain(
1300
+ type=expr_chain.type,
1301
+ left_link=left_reverse_chain,
1302
+ right_link=right_reverse_chain
1423
1303
  )
1424
- return new_expr_chain
1425
1304
 
1426
- case 'field':
1427
- association = expr_chain.association
1305
+ return new_expr_chain
1428
1306
 
1429
- if not association:
1430
- raise LanguageGraphException(
1431
- "Missing association for expressions chain"
1432
- )
1307
+ case 'transitive':
1308
+ result_reverse_chain = self.reverse_expr_chain(
1309
+ expr_chain.sub_link, reverse_chain)
1310
+ new_expr_chain = ExpressionsChain(
1311
+ type='transitive',
1312
+ sub_link=result_reverse_chain
1313
+ )
1314
+ return new_expr_chain
1433
1315
 
1434
- if not expr_chain.fieldname:
1435
- raise LanguageGraphException(
1436
- "Missing field name for expressions chain"
1437
- )
1316
+ case 'field':
1317
+ association = expr_chain.association
1438
1318
 
1439
- opposite_fieldname = association.get_opposite_fieldname(
1440
- expr_chain.fieldname)
1441
- new_expr_chain = ExpressionsChain(
1442
- type = 'field',
1443
- association = association,
1444
- fieldname = opposite_fieldname
1319
+ if not association:
1320
+ raise LanguageGraphException(
1321
+ "Missing association for expressions chain"
1445
1322
  )
1446
- return new_expr_chain
1447
1323
 
1448
- case 'subType':
1449
- result_reverse_chain = self.reverse_expr_chain(
1450
- expr_chain.sub_link,
1451
- reverse_chain
1452
- )
1453
- new_expr_chain = ExpressionsChain(
1454
- type = 'subType',
1455
- sub_link = result_reverse_chain,
1456
- subtype = expr_chain.subtype
1324
+ if not expr_chain.fieldname:
1325
+ raise LanguageGraphException(
1326
+ "Missing field name for expressions chain"
1457
1327
  )
1458
- return new_expr_chain
1459
1328
 
1460
- case _:
1461
- msg = 'Unknown assoc chain element "%s"'
1462
- logger.error(msg, expr_chain.type)
1463
- raise LanguageGraphAssociationError(msg % expr_chain.type)
1329
+ opposite_fieldname = association.get_opposite_fieldname(
1330
+ expr_chain.fieldname)
1331
+ new_expr_chain = ExpressionsChain(
1332
+ type='field',
1333
+ association=association,
1334
+ fieldname=opposite_fieldname
1335
+ )
1336
+ return new_expr_chain
1337
+
1338
+ case 'subType':
1339
+ result_reverse_chain = self.reverse_expr_chain(
1340
+ expr_chain.sub_link,
1341
+ reverse_chain
1342
+ )
1343
+ new_expr_chain = ExpressionsChain(
1344
+ type='subType',
1345
+ sub_link=result_reverse_chain,
1346
+ subtype=expr_chain.subtype
1347
+ )
1348
+ return new_expr_chain
1349
+
1350
+ case _:
1351
+ msg = 'Unknown assoc chain element "%s"'
1352
+ logger.error(msg, expr_chain.type)
1353
+ raise LanguageGraphAssociationError(msg % expr_chain.type)
1464
1354
 
1465
1355
  def _resolve_variable(self, asset: LanguageGraphAsset, var_name) -> tuple:
1466
- """
1467
- Resolve a variable for a specific asset by variable name.
1356
+ """Resolve a variable for a specific asset by variable name.
1468
1357
 
1469
1358
  Arguments:
1359
+ ---------
1470
1360
  asset - a language graph asset to which the variable belongs
1471
1361
  var_name - a string representing the variable name
1472
1362
 
1473
1363
  Return:
1364
+ ------
1474
1365
  A tuple containing the target asset and expressions chain required to
1475
1366
  reach it.
1367
+
1476
1368
  """
1477
1369
  if var_name not in asset.variables:
1478
1370
  var_expr = self._get_var_expr_for_asset(asset.name, var_name)
@@ -1490,13 +1382,15 @@ class LanguageGraph():
1490
1382
  lang_spec: dict[str, Any],
1491
1383
  assets: dict[str, LanguageGraphAsset]
1492
1384
  ) -> None:
1493
- """ Link associations to assets based on the language specification.
1385
+ """Link associations to assets based on the language specification.
1386
+
1494
1387
  Arguments:
1388
+ ---------
1495
1389
  lang_spec - the language specification dictionary
1496
1390
  assets - a dictionary of LanguageGraphAsset objects
1497
1391
  indexed by their names
1498
- """
1499
1392
 
1393
+ """
1500
1394
  for association_dict in lang_spec['associations']:
1501
1395
  logger.debug(
1502
1396
  'Create association language graph nodes for association %s',
@@ -1521,20 +1415,20 @@ class LanguageGraph():
1521
1415
  right_asset = assets[right_asset_name]
1522
1416
 
1523
1417
  assoc_node = LanguageGraphAssociation(
1524
- name = association_dict['name'],
1525
- left_field = LanguageGraphAssociationField(
1418
+ name=association_dict['name'],
1419
+ left_field=LanguageGraphAssociationField(
1526
1420
  left_asset,
1527
1421
  association_dict['leftField'],
1528
1422
  association_dict['leftMultiplicity']['min'],
1529
1423
  association_dict['leftMultiplicity']['max']
1530
1424
  ),
1531
- right_field = LanguageGraphAssociationField(
1425
+ right_field=LanguageGraphAssociationField(
1532
1426
  right_asset,
1533
1427
  association_dict['rightField'],
1534
1428
  association_dict['rightMultiplicity']['min'],
1535
1429
  association_dict['rightMultiplicity']['max']
1536
1430
  ),
1537
- info = association_dict['meta']
1431
+ info=association_dict['meta']
1538
1432
  )
1539
1433
 
1540
1434
  # Add the association to the left and right asset
@@ -1547,10 +1441,8 @@ class LanguageGraph():
1547
1441
  lang_spec: dict[str, Any],
1548
1442
  assets: dict[str, LanguageGraphAsset]
1549
1443
  ) -> None:
1444
+ """Link assets based on inheritance and associations.
1550
1445
  """
1551
- Link assets based on inheritance and associations.
1552
- """
1553
-
1554
1446
  for asset_dict in lang_spec['assets']:
1555
1447
  asset = assets[asset_dict['name']]
1556
1448
  if asset_dict['superAsset']:
@@ -1568,12 +1460,14 @@ class LanguageGraph():
1568
1460
  def _set_variables_for_assets(
1569
1461
  self, assets: dict[str, LanguageGraphAsset]
1570
1462
  ) -> None:
1571
- """ Set the variables for each asset based on the language specification.
1463
+ """Set the variables for each asset based on the language specification.
1464
+
1572
1465
  Arguments:
1466
+ ---------
1573
1467
  assets - a dictionary of LanguageGraphAsset objects
1574
1468
  indexed by their names
1575
- """
1576
1469
 
1470
+ """
1577
1471
  for asset in assets.values():
1578
1472
  logger.debug(
1579
1473
  'Set variables for asset %s', asset.name
@@ -1584,189 +1478,138 @@ class LanguageGraph():
1584
1478
  # Avoid running json.dumps when not in debug
1585
1479
  logger.debug(
1586
1480
  'Processing Variable Expression:\n%s',
1587
- json.dumps(variable, indent = 2)
1481
+ json.dumps(variable, indent=2)
1588
1482
  )
1589
1483
  self._resolve_variable(asset, variable['name'])
1590
1484
 
1591
1485
  def _generate_attack_steps(self, assets) -> None:
1592
1486
  """
1593
- Generate all of the attack steps for each asset type
1594
- based on the language specification.
1487
+ Generate attack steps for all assets and link them according to the
1488
+ language specification.
1489
+
1490
+ This method performs three phases:
1491
+
1492
+ 1. Create attack step nodes for each asset, including detectors.
1493
+ 2. Inherit attack steps from super-assets, respecting overrides.
1494
+ 3. Link attack steps via 'reaches' and evaluate 'exist'/'notExist'
1495
+ requirements.
1496
+
1497
+ Args:
1498
+ assets (dict): Mapping of asset names to asset objects.
1499
+
1500
+ Raises:
1501
+ LanguageGraphStepExpressionError: If a step expression cannot be
1502
+ resolved to a target asset or attack step.
1503
+ LanguageGraphException: If an existence requirement cannot be
1504
+ resolved.
1595
1505
  """
1596
1506
  langspec_dict = {}
1507
+
1597
1508
  for asset in assets.values():
1598
- logger.debug(
1599
- 'Create attack steps language graph nodes for asset %s',
1600
- asset.name
1601
- )
1602
- attack_steps = self._get_attacks_for_asset_type(asset.name)
1603
- for attack_step_attribs in attack_steps.values():
1509
+ logger.debug('Create attack steps language graph nodes for asset %s', asset.name)
1510
+ for step_dict in self._get_attacks_for_asset_type(asset.name).values():
1604
1511
  logger.debug(
1605
- 'Create attack step language graph nodes for %s',
1606
- attack_step_attribs['name']
1512
+ 'Create attack step language graph nodes for %s', step_dict['name']
1607
1513
  )
1608
-
1609
- attack_step_node = LanguageGraphAttackStep(
1610
- name = attack_step_attribs['name'],
1611
- type = attack_step_attribs['type'],
1612
- asset = asset,
1613
- ttc = attack_step_attribs['ttc'],
1614
- overrides = (
1615
- attack_step_attribs['reaches']['overrides']
1616
- if attack_step_attribs['reaches'] else False
1514
+ node = LanguageGraphAttackStep(
1515
+ name=step_dict['name'],
1516
+ type=step_dict['type'],
1517
+ asset=asset,
1518
+ ttc=step_dict['ttc'],
1519
+ overrides=(
1520
+ step_dict['reaches']['overrides']
1521
+ if step_dict['reaches'] else False
1617
1522
  ),
1618
- own_children = {},
1619
- own_parents = {},
1620
- info = attack_step_attribs['meta'],
1621
- tags = list(attack_step_attribs['tags'])
1523
+ own_children={}, own_parents={},
1524
+ info=step_dict['meta'],
1525
+ tags=list(step_dict['tags'])
1622
1526
  )
1623
- langspec_dict[attack_step_node.full_name] = \
1624
- attack_step_attribs
1625
- asset.attack_steps[attack_step_node.name] = \
1626
- attack_step_node
1627
-
1628
- detectors: dict = attack_step_attribs.get("detectors", {})
1629
- for detector in detectors.values():
1630
- attack_step_node.detectors[detector["name"]] = Detector(
1527
+ langspec_dict[node.full_name] = step_dict
1528
+ asset.attack_steps[node.name] = node
1529
+
1530
+ for det in step_dict.get('detectors', {}).values():
1531
+ node.detectors[det['name']] = Detector(
1631
1532
  context=Context(
1632
- {
1633
- label: assets[asset]
1634
- for label, asset in detector["context"].items()
1635
- }
1533
+ {lbl: assets[a] for lbl, a in det['context'].items()}
1636
1534
  ),
1637
- name=detector.get("name"),
1638
- type=detector.get("type"),
1639
- tprate=detector.get("tprate"),
1535
+ name=det.get('name'),
1536
+ type=det.get('type'),
1537
+ tprate=det.get('tprate'),
1640
1538
  )
1641
1539
 
1642
- # Create the inherited attack steps
1643
- assets = list(self.assets.values())
1644
- while len(assets) > 0:
1645
- asset = assets.pop(0)
1646
- if asset.own_super_asset in assets:
1647
- # The asset still has super assets that should be resolved
1648
- # first, moved it to the back.
1649
- assets.append(asset)
1650
- else:
1651
- if asset.own_super_asset:
1652
- for attack_step in \
1653
- asset.own_super_asset.attack_steps.values():
1654
- if attack_step.name not in asset.attack_steps:
1655
- attack_step_node = LanguageGraphAttackStep(
1656
- name = attack_step.name,
1657
- type = attack_step.type,
1658
- asset = asset,
1659
- ttc = attack_step.ttc,
1660
- overrides = False,
1661
- own_children = {},
1662
- own_parents = {},
1663
- info = attack_step.info,
1664
- tags = list(attack_step.tags)
1665
- )
1666
- attack_step_node.inherits = attack_step
1667
- asset.attack_steps[attack_step.name] = attack_step_node
1668
- elif asset.attack_steps[attack_step.name].overrides:
1669
- # The inherited attack step was already overridden.
1670
- continue
1671
- else:
1672
- asset.attack_steps[attack_step.name].inherits = attack_step
1673
- asset.attack_steps[attack_step.name].tags += attack_step.tags
1674
- asset.attack_steps[attack_step.name].info |= attack_step.info
1675
-
1676
- # Then, link all of the attack step nodes according to their
1677
- # associations.
1678
- for asset in self.assets.values():
1679
- for attack_step in asset.attack_steps.values():
1680
- logger.debug(
1681
- 'Determining children for attack step %s',
1682
- attack_step.name
1683
- )
1684
-
1685
- if attack_step.full_name not in langspec_dict:
1686
- # This is simply an empty inherited attack step
1540
+ pending = list(self.assets.values())
1541
+ while pending:
1542
+ asset = pending.pop(0)
1543
+ super_asset = asset.own_super_asset
1544
+ if super_asset in pending:
1545
+ # Super asset still needs processing, defer this asset
1546
+ pending.append(asset)
1547
+ continue
1548
+ if not super_asset:
1549
+ continue
1550
+ for super_step in super_asset.attack_steps.values():
1551
+ current_step = asset.attack_steps.get(super_step.name)
1552
+ if not current_step:
1553
+ node = LanguageGraphAttackStep(
1554
+ name=super_step.name,
1555
+ type=super_step.type,
1556
+ asset=asset,
1557
+ ttc=super_step.ttc,
1558
+ overrides=False,
1559
+ own_children={},
1560
+ own_parents={},
1561
+ info=super_step.info,
1562
+ tags=list(super_step.tags)
1563
+ )
1564
+ node.inherits = super_step
1565
+ asset.attack_steps[super_step.name] = node
1566
+ elif current_step.overrides:
1687
1567
  continue
1568
+ else:
1569
+ current_step.inherits = super_step
1570
+ current_step.tags += super_step.tags
1571
+ current_step.info |= super_step.info
1688
1572
 
1689
- langspec_entry = langspec_dict[attack_step.full_name]
1690
- step_expressions = (
1691
- langspec_entry['reaches']['stepExpressions']
1692
- if langspec_entry['reaches'] else []
1693
- )
1573
+ for asset in self.assets.values():
1574
+ for step in asset.attack_steps.values():
1575
+ logger.debug('Determining children for attack step %s', step.name)
1576
+ if step.full_name not in langspec_dict:
1577
+ continue
1694
1578
 
1695
- for step_expression in step_expressions:
1696
- # Resolve each of the attack step expressions listed for
1697
- # this attack step to determine children.
1698
- (target_asset, expr_chain, target_attack_step_name) = \
1699
- self.process_step_expression(
1700
- attack_step.asset,
1701
- None,
1702
- step_expression
1703
- )
1704
- if not target_asset:
1705
- msg = 'Failed to find target asset to link with for ' \
1706
- 'step expression:\n%s'
1579
+ entry = langspec_dict[step.full_name]
1580
+ for expr in (entry['reaches']['stepExpressions'] if entry['reaches'] else []):
1581
+ tgt_asset, chain, tgt_name = self.process_step_expression(step.asset, None, expr)
1582
+ if not tgt_asset:
1707
1583
  raise LanguageGraphStepExpressionError(
1708
- msg % json.dumps(step_expression, indent = 2)
1584
+ 'Failed to find target asset for:\n%s' % json.dumps(expr, indent=2)
1709
1585
  )
1710
-
1711
- target_asset_attack_steps = target_asset.attack_steps
1712
- if target_attack_step_name not in \
1713
- target_asset_attack_steps:
1714
- msg = 'Failed to find target attack step %s on %s to ' \
1715
- 'link with for step expression:\n%s'
1586
+ if tgt_name not in tgt_asset.attack_steps:
1716
1587
  raise LanguageGraphStepExpressionError(
1717
- msg % (
1718
- target_attack_step_name,
1719
- target_asset.name,
1720
- json.dumps(step_expression, indent = 2)
1721
- )
1588
+ 'Failed to find target attack step %s on %s:\n%s' %
1589
+ (tgt_name, tgt_asset.name, json.dumps(expr, indent=2))
1722
1590
  )
1723
1591
 
1724
- target_attack_step = target_asset_attack_steps[
1725
- target_attack_step_name]
1592
+ tgt = tgt_asset.attack_steps[tgt_name]
1593
+ step.own_children.setdefault(tgt, []).append(chain)
1594
+ tgt.own_parents.setdefault(step, []).append(self.reverse_expr_chain(chain, None))
1726
1595
 
1727
- # Link to the children target attack steps
1728
- attack_step.own_children.setdefault(target_attack_step, [])
1729
- attack_step.own_children[target_attack_step].append(expr_chain)
1730
-
1731
- # Reverse the children associations chains to get the
1732
- # parents associations chain.
1733
- target_attack_step.own_parents.setdefault(attack_step, [])
1734
- target_attack_step.own_parents[attack_step].append(
1735
- self.reverse_expr_chain(expr_chain, None)
1736
- )
1737
-
1738
- # Evaluate the requirements of exist and notExist attack steps
1739
- if attack_step.type in ('exist', 'notExist'):
1740
- step_expressions = (
1741
- langspec_entry['requires']['stepExpressions']
1742
- if langspec_entry['requires'] else []
1743
- )
1744
- if not step_expressions:
1596
+ if step.type in ('exist', 'notExist'):
1597
+ reqs = entry['requires']['stepExpressions'] if entry['requires'] else []
1598
+ if not reqs:
1745
1599
  raise LanguageGraphStepExpressionError(
1746
- 'Failed to find requirements for attack step'
1747
- ' "%s" of type "%s":\n%s' % (
1748
- attack_step.name,
1749
- attack_step.type,
1750
- json.dumps(langspec_entry, indent = 2)
1751
- )
1600
+ 'Missing requirements for "%s" of type "%s":\n%s' %
1601
+ (step.name, step.type, json.dumps(entry, indent=2))
1752
1602
  )
1753
-
1754
- for step_expression in step_expressions:
1755
- _, result_expr_chain, _ = \
1756
- self.process_step_expression(
1757
- attack_step.asset,
1758
- None,
1759
- step_expression
1603
+ for expr in reqs:
1604
+ _, chain, _ = self.process_step_expression(step.asset, None, expr)
1605
+ if chain is None:
1606
+ raise LanguageGraphException(
1607
+ f'Failed to find existence step requirement for:\n{expr}'
1760
1608
  )
1761
- if result_expr_chain is None:
1762
- raise LanguageGraphException('Failed to find '
1763
- 'existence step requirement for step '
1764
- f'expression:\n%s' % step_expression)
1765
- attack_step.own_requires.append(result_expr_chain)
1609
+ step.own_requires.append(chain)
1766
1610
 
1767
1611
  def _generate_graph(self) -> None:
1768
- """
1769
- Generate language graph starting from the MAL language specification
1612
+ """Generate language graph starting from the MAL language specification
1770
1613
  given in the constructor.
1771
1614
  """
1772
1615
  # Generate all of the asset nodes of the language graph.
@@ -1777,14 +1620,14 @@ class LanguageGraph():
1777
1620
  asset_dict['name']
1778
1621
  )
1779
1622
  asset_node = LanguageGraphAsset(
1780
- name = asset_dict['name'],
1781
- own_associations = {},
1782
- attack_steps = {},
1783
- info = asset_dict['meta'],
1784
- own_super_asset = None,
1785
- own_sub_assets = set(),
1786
- own_variables = {},
1787
- is_abstract = asset_dict['isAbstract']
1623
+ name=asset_dict['name'],
1624
+ own_associations={},
1625
+ attack_steps={},
1626
+ info=asset_dict['meta'],
1627
+ own_super_asset=None,
1628
+ own_sub_assets=set(),
1629
+ own_variables={},
1630
+ is_abstract=asset_dict['isAbstract']
1788
1631
  )
1789
1632
  self.assets[asset_dict['name']] = asset_node
1790
1633
 
@@ -1801,24 +1644,26 @@ class LanguageGraph():
1801
1644
  self._generate_attack_steps(self.assets)
1802
1645
 
1803
1646
  def _get_attacks_for_asset_type(self, asset_type: str) -> dict[str, dict]:
1804
- """
1805
- Get all Attack Steps for a specific asset type.
1647
+ """Get all Attack Steps for a specific asset type.
1806
1648
 
1807
1649
  Arguments:
1650
+ ---------
1808
1651
  asset_type - the name of the asset type we want to
1809
1652
  list the possible attack steps for
1810
1653
 
1811
1654
  Return:
1655
+ ------
1812
1656
  A dictionary containing the possible attacks for the
1813
1657
  specified asset type. Each key in the dictionary is an attack name
1814
1658
  associated with a dictionary containing other characteristics of the
1815
1659
  attack such as type of attack, TTC distribution, child attack steps
1816
1660
  and other information
1661
+
1817
1662
  """
1818
1663
  attack_steps: dict = {}
1819
1664
  try:
1820
1665
  asset = next(
1821
- asset for asset in self._lang_spec['assets'] \
1666
+ asset for asset in self._lang_spec['assets']
1822
1667
  if asset['name'] == asset_type
1823
1668
  )
1824
1669
  except StopIteration:
@@ -1838,17 +1683,19 @@ class LanguageGraph():
1838
1683
  return attack_steps
1839
1684
 
1840
1685
  def _get_associations_for_asset_type(self, asset_type: str) -> list[dict]:
1841
- """
1842
- Get all associations for a specific asset type.
1686
+ """Get all associations for a specific asset type.
1843
1687
 
1844
1688
  Arguments:
1689
+ ---------
1845
1690
  asset_type - the name of the asset type for which we want to
1846
1691
  list the associations
1847
1692
 
1848
1693
  Return:
1694
+ ------
1849
1695
  A list of dicts, where each dict represents an associations
1850
1696
  for the specified asset type. Each dictionary contains
1851
1697
  name and meta information about the association.
1698
+
1852
1699
  """
1853
1700
  logger.debug(
1854
1701
  'Get associations for %s asset from '
@@ -1856,7 +1703,7 @@ class LanguageGraph():
1856
1703
  )
1857
1704
  associations: list = []
1858
1705
 
1859
- asset = next((asset for asset in self._lang_spec['assets'] \
1706
+ asset = next((asset for asset in self._lang_spec['assets']
1860
1707
  if asset['name'] == asset_type), None)
1861
1708
  if not asset:
1862
1709
  logger.error(
@@ -1865,8 +1712,8 @@ class LanguageGraph():
1865
1712
  )
1866
1713
  return associations
1867
1714
 
1868
- assoc_iter = (assoc for assoc in self._lang_spec['associations'] \
1869
- if assoc['leftAsset'] == asset_type or \
1715
+ assoc_iter = (assoc for assoc in self._lang_spec['associations']
1716
+ if assoc['leftAsset'] == asset_type or
1870
1717
  assoc['rightAsset'] == asset_type)
1871
1718
  assoc = next(assoc_iter, None)
1872
1719
  while assoc:
@@ -1877,20 +1724,21 @@ class LanguageGraph():
1877
1724
 
1878
1725
  def _get_variables_for_asset_type(
1879
1726
  self, asset_type: str) -> list[dict]:
1880
- """
1881
- Get variables for a specific asset type.
1727
+ """Get variables for a specific asset type.
1882
1728
  Note: Variables are the ones specified in MAL through `let` statements
1883
1729
 
1884
1730
  Arguments:
1731
+ ---------
1885
1732
  asset_type - a string representing the asset type which
1886
1733
  contains the variables
1887
1734
 
1888
1735
  Return:
1736
+ ------
1889
1737
  A list of dicts representing the step expressions for the variables
1890
1738
  belonging to the asset.
1891
- """
1892
1739
 
1893
- asset_dict = next((asset for asset in self._lang_spec['assets'] \
1740
+ """
1741
+ asset_dict = next((asset for asset in self._lang_spec['assets']
1894
1742
  if asset['name'] == asset_type), None)
1895
1743
  if not asset_dict:
1896
1744
  msg = 'Failed to find asset type %s in language specification '\
@@ -1902,21 +1750,22 @@ class LanguageGraph():
1902
1750
 
1903
1751
  def _get_var_expr_for_asset(
1904
1752
  self, asset_type: str, var_name) -> dict:
1905
- """
1906
- Get a variable for a specific asset type by variable name.
1753
+ """Get a variable for a specific asset type by variable name.
1907
1754
 
1908
1755
  Arguments:
1756
+ ---------
1909
1757
  asset_type - a string representing the type of asset which
1910
1758
  contains the variable
1911
1759
  var_name - a string representing the variable name
1912
1760
 
1913
1761
  Return:
1762
+ ------
1914
1763
  A dictionary representing the step expression for the variable.
1915
- """
1916
1764
 
1765
+ """
1917
1766
  vars_dict = self._get_variables_for_asset_type(asset_type)
1918
1767
 
1919
- var_expr = next((var_entry['stepExpression'] for var_entry \
1768
+ var_expr = next((var_entry['stepExpression'] for var_entry
1920
1769
  in vars_dict if var_entry['name'] == var_name), None)
1921
1770
 
1922
1771
  if not var_expr:
@@ -1927,10 +1776,16 @@ class LanguageGraph():
1927
1776
  return var_expr
1928
1777
 
1929
1778
  def regenerate_graph(self) -> None:
1930
- """
1931
- Regenerate language graph starting from the MAL language specification
1779
+ """Regenerate language graph starting from the MAL language specification
1932
1780
  given in the constructor.
1933
1781
  """
1934
-
1935
1782
  self.assets = {}
1936
1783
  self._generate_graph()
1784
+
1785
+ def __getstate__(self):
1786
+ return self._to_dict()
1787
+
1788
+ def __setstate__(self, state):
1789
+ temp_lang_graph = self._from_dict(state)
1790
+ self.assets = temp_lang_graph.assets
1791
+ self.metadata = temp_lang_graph.metadata