mal-toolbox 1.1.1__py3-none-any.whl → 1.1.3__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.1.dist-info → mal_toolbox-1.1.3.dist-info}/METADATA +25 -2
  2. mal_toolbox-1.1.3.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 +487 -639
  15. maltoolbox/model.py +64 -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.1.dist-info/RECORD +0 -32
  26. maltoolbox/translators/securicad.py +0 -179
  27. {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/WHEEL +0 -0
  28. {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/entry_points.txt +0 -0
  29. {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/licenses/AUTHORS +0 -0
  30. {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/licenses/LICENSE +0 -0
  31. {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.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: list[LanguageGraphAsset] = field(default_factory=list)
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
- return hash(self.name)
117
-
118
+ return id(self)
118
119
 
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,39 +361,36 @@ 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
- return hash(self.full_name)
386
+ return id(self)
391
387
 
392
388
  @property
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,127 @@ 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']
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=list(),
777
+ own_variables={},
778
+ is_abstract=asset['is_abstract']
791
779
  )
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']
827
- )
828
-
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
780
 
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
+
792
+ super_asset.own_sub_assets.append(asset_node)
793
+ asset_node.own_super_asset = super_asset
794
+
795
+ # Associations
796
+ for asset in serialized_graph.values():
797
+ logger.debug('Create associations for asset %s', asset['name'])
798
+ a_node = lang_graph.assets[asset['name']]
799
+ for assoc in asset['associations'].values():
800
+ try:
801
+ left = lang_graph.assets[assoc['left']['asset']]
802
+ right = lang_graph.assets[assoc['right']['asset']]
803
+ except KeyError as e:
804
+ side = 'Left' if 'left' in str(e) else 'Right'
805
+ msg = f'{side} asset for association "{assoc["name"]}" not found'
806
+ logger.error(msg)
807
+ raise LanguageGraphAssociationError(msg)
852
808
  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']
809
+ name=assoc['name'],
810
+ left_field=LanguageGraphAssociationField(
811
+ left, assoc['left']['fieldname'],
812
+ assoc['left']['min'], assoc['left']['max']
813
+ ),
814
+ right_field=LanguageGraphAssociationField(
815
+ right, assoc['right']['fieldname'],
816
+ assoc['right']['min'], assoc['right']['max']
817
+ ),
818
+ info=assoc['info']
865
819
  )
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
820
+ lang_graph._link_association_to_assets(assoc_node, left, right)
821
+
822
+ # Variables
823
+ for asset in serialized_graph.values():
824
+ a_node = lang_graph.assets[asset['name']]
825
+ for var, (target_name, expr_dict) in asset['variables'].items():
826
+ target = lang_graph.assets[target_name]
827
+ a_node.own_variables[var] = (
828
+ target, ExpressionsChain._from_dict(expr_dict, lang_graph)
880
829
  )
881
- asset.own_variables[variable_name] = (target_asset, expr_chain)
882
830
 
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'])
831
+ # Attack steps
832
+ for asset in serialized_graph.values():
833
+ a_node = lang_graph.assets[asset['name']]
834
+ for step in asset['attack_steps'].values():
835
+ a_node.attack_steps[step['name']] = LanguageGraphAttackStep(
836
+ name=step['name'],
837
+ type=step['type'],
838
+ asset=a_node,
839
+ ttc=step['ttc'],
840
+ overrides=step['overrides'],
841
+ own_children={}, own_parents={},
842
+ info=step['info'],
843
+ tags=list(step['tags'])
901
844
  )
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
845
 
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
-
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)
846
+ # Inheritance for attack steps
847
+ for asset in serialized_graph.values():
848
+ a_node = lang_graph.assets[asset['name']]
849
+ for step in asset['attack_steps'].values():
850
+ if not (inh := step.get('inherits')):
851
+ continue
852
+ a_step = a_node.attack_steps[step['name']]
853
+ a_name, s_name = disaggregate_attack_step_full_name(inh)
854
+ a_step.inherits = lang_graph.assets[a_name].attack_steps[s_name]
855
+
856
+ # Expression chains and requirements
857
+ for asset in serialized_graph.values():
858
+ a_node = lang_graph.assets[asset['name']]
859
+ for step in asset['attack_steps'].values():
860
+ s_node = a_node.attack_steps[step['name']]
861
+ for tgt_name, exprs in step['own_children'].items():
862
+ t_asset, t_step = disaggregate_attack_step_full_name(tgt_name)
863
+ t_node = lang_graph.assets[t_asset].attack_steps[t_step]
864
+ for expr in exprs:
865
+ chain = ExpressionsChain._from_dict(expr, lang_graph)
866
+ s_node.own_children.setdefault(t_node, []).append(chain)
867
+ for tgt_name, exprs in step['own_parents'].items():
868
+ t_asset, t_step = disaggregate_attack_step_full_name(tgt_name)
869
+ t_node = lang_graph.assets[t_asset].attack_steps[t_step]
870
+ for expr in exprs:
871
+ chain = ExpressionsChain._from_dict(expr, lang_graph)
872
+ s_node.own_parents.setdefault(t_node, []).append(chain)
873
+ if step['type'] in ('exist', 'notExist') and (reqs := step.get('requires')):
874
+ s_node.own_requires = [
875
+ chain for expr in reqs
876
+ if (chain := ExpressionsChain._from_dict(expr, lang_graph))
877
+ ]
978
878
 
979
879
  return lang_graph
980
880
 
981
-
982
881
  @classmethod
983
882
  def load_from_file(cls, filename: str) -> LanguageGraph:
984
883
  """Create LanguageGraph from mal, mar, yaml or json"""
@@ -989,7 +888,7 @@ class LanguageGraph():
989
888
  lang_graph = cls.from_mar_archive(filename)
990
889
  elif filename.endswith(('.yaml', '.yml')):
991
890
  lang_graph = cls._from_dict(load_dict_from_yaml_file(filename))
992
- elif filename.endswith(('.json')):
891
+ elif filename.endswith('.json'):
993
892
  lang_graph = cls._from_dict(load_dict_from_json_file(filename))
994
893
  else:
995
894
  raise TypeError(
@@ -998,18 +897,17 @@ class LanguageGraph():
998
897
 
999
898
  if lang_graph:
1000
899
  return lang_graph
1001
- else:
1002
- raise LanguageGraphException(
1003
- f'Failed to load language graph from file "{filename}".'
1004
- )
1005
-
900
+ raise LanguageGraphException(
901
+ f'Failed to load language graph from file "{filename}".'
902
+ )
1006
903
 
1007
904
  def save_language_specification_to_json(self, filename: str) -> None:
1008
- """
1009
- Save a MAL language specification dictionary to a JSON file
905
+ """Save a MAL language specification dictionary to a JSON file
1010
906
 
1011
907
  Arguments:
908
+ ---------
1012
909
  filename - the JSON filename where the language specification will be written
910
+
1013
911
  """
1014
912
  logger.info('Save language specification to %s', filename)
1015
913
 
@@ -1025,8 +923,7 @@ class LanguageGraph():
1025
923
  None,
1026
924
  str
1027
925
  ]:
1028
- """
1029
- The attack step expression just adds the name of the attack
926
+ """The attack step expression just adds the name of the attack
1030
927
  step. All other step expressions only modify the target
1031
928
  asset and parent associations chain.
1032
929
  """
@@ -1039,18 +936,16 @@ class LanguageGraph():
1039
936
  def process_set_operation_step_expression(
1040
937
  self,
1041
938
  target_asset: LanguageGraphAsset,
1042
- expr_chain: Optional[ExpressionsChain],
939
+ expr_chain: ExpressionsChain | None,
1043
940
  step_expression: dict[str, Any]
1044
941
  ) -> tuple[
1045
942
  LanguageGraphAsset,
1046
943
  ExpressionsChain,
1047
944
  None
1048
945
  ]:
1049
- """
1050
- The set operators are used to combine the left hand and right
946
+ """The set operators are used to combine the left hand and right
1051
947
  hand targets accordingly.
1052
948
  """
1053
-
1054
949
  lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
1055
950
  target_asset,
1056
951
  expr_chain,
@@ -1077,9 +972,9 @@ class LanguageGraph():
1077
972
  )
1078
973
 
1079
974
  new_expr_chain = ExpressionsChain(
1080
- type = step_expression['type'],
1081
- left_link = lh_expr_chain,
1082
- right_link = rh_expr_chain
975
+ type=step_expression['type'],
976
+ left_link=lh_expr_chain,
977
+ right_link=rh_expr_chain
1083
978
  )
1084
979
  return (
1085
980
  lh_target_asset,
@@ -1123,12 +1018,10 @@ class LanguageGraph():
1123
1018
  ExpressionsChain,
1124
1019
  None
1125
1020
  ]:
1126
- """
1127
- Change the target asset from the current one to the associated
1021
+ """Change the target asset from the current one to the associated
1128
1022
  asset given the specified field name and add the parent
1129
1023
  fieldname and association to the parent associations chain.
1130
1024
  """
1131
-
1132
1025
  fieldname = step_expression['name']
1133
1026
 
1134
1027
  if target_asset is None:
@@ -1138,21 +1031,21 @@ class LanguageGraph():
1138
1031
 
1139
1032
  new_target_asset = None
1140
1033
  for association in target_asset.associations.values():
1141
- if (association.left_field.fieldname == fieldname and \
1034
+ if (association.left_field.fieldname == fieldname and
1142
1035
  target_asset.is_subasset_of(
1143
1036
  association.right_field.asset)):
1144
1037
  new_target_asset = association.left_field.asset
1145
1038
 
1146
- if (association.right_field.fieldname == fieldname and \
1039
+ if (association.right_field.fieldname == fieldname and
1147
1040
  target_asset.is_subasset_of(
1148
1041
  association.left_field.asset)):
1149
1042
  new_target_asset = association.right_field.asset
1150
1043
 
1151
1044
  if new_target_asset:
1152
1045
  new_expr_chain = ExpressionsChain(
1153
- type = 'field',
1154
- fieldname = fieldname,
1155
- association = association
1046
+ type='field',
1047
+ fieldname=fieldname,
1048
+ association=association
1156
1049
  )
1157
1050
  return (
1158
1051
  new_target_asset,
@@ -1167,15 +1060,14 @@ class LanguageGraph():
1167
1060
  def process_transitive_step_expression(
1168
1061
  self,
1169
1062
  target_asset: LanguageGraphAsset,
1170
- expr_chain: Optional[ExpressionsChain],
1063
+ expr_chain: ExpressionsChain | None,
1171
1064
  step_expression: dict[str, Any]
1172
1065
  ) -> tuple[
1173
1066
  LanguageGraphAsset,
1174
1067
  ExpressionsChain,
1175
1068
  None
1176
1069
  ]:
1177
- """
1178
- Create a transitive tuple entry that applies to the next
1070
+ """Create a transitive tuple entry that applies to the next
1179
1071
  component of the step expression.
1180
1072
  """
1181
1073
  result_target_asset, result_expr_chain, _ = (
@@ -1186,8 +1078,8 @@ class LanguageGraph():
1186
1078
  )
1187
1079
  )
1188
1080
  new_expr_chain = ExpressionsChain(
1189
- type = 'transitive',
1190
- sub_link = result_expr_chain
1081
+ type='transitive',
1082
+ sub_link=result_expr_chain
1191
1083
  )
1192
1084
  return (
1193
1085
  result_target_asset,
@@ -1198,19 +1090,17 @@ class LanguageGraph():
1198
1090
  def process_subType_step_expression(
1199
1091
  self,
1200
1092
  target_asset: LanguageGraphAsset,
1201
- expr_chain: Optional[ExpressionsChain],
1093
+ expr_chain: ExpressionsChain | None,
1202
1094
  step_expression: dict[str, Any]
1203
1095
  ) -> tuple[
1204
1096
  LanguageGraphAsset,
1205
1097
  ExpressionsChain,
1206
1098
  None
1207
1099
  ]:
1208
- """
1209
- Create a subType tuple entry that applies to the next
1100
+ """Create a subType tuple entry that applies to the next
1210
1101
  component of the step expression and changes the target
1211
1102
  asset to the subasset.
1212
1103
  """
1213
-
1214
1104
  subtype_name = step_expression['subType']
1215
1105
  result_target_asset, result_expr_chain, _ = (
1216
1106
  self.process_step_expression(
@@ -1237,9 +1127,9 @@ class LanguageGraph():
1237
1127
  )
1238
1128
 
1239
1129
  new_expr_chain = ExpressionsChain(
1240
- type = 'subType',
1241
- sub_link = result_expr_chain,
1242
- subtype = subtype_asset
1130
+ type='subType',
1131
+ sub_link=result_expr_chain,
1132
+ subtype=subtype_asset
1243
1133
  )
1244
1134
  return (
1245
1135
  subtype_asset,
@@ -1250,15 +1140,14 @@ class LanguageGraph():
1250
1140
  def process_collect_step_expression(
1251
1141
  self,
1252
1142
  target_asset: LanguageGraphAsset,
1253
- expr_chain: Optional[ExpressionsChain],
1143
+ expr_chain: ExpressionsChain | None,
1254
1144
  step_expression: dict[str, Any]
1255
1145
  ) -> tuple[
1256
1146
  LanguageGraphAsset,
1257
- Optional[ExpressionsChain],
1258
- Optional[str]
1147
+ ExpressionsChain | None,
1148
+ str | None
1259
1149
  ]:
1260
- """
1261
- Apply the right hand step expression to left hand step
1150
+ """Apply the right hand step expression to left hand step
1262
1151
  expression target asset and parent associations chain.
1263
1152
  """
1264
1153
  lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
@@ -1280,9 +1169,9 @@ class LanguageGraph():
1280
1169
  new_expr_chain = lh_expr_chain
1281
1170
  if rh_expr_chain:
1282
1171
  new_expr_chain = ExpressionsChain(
1283
- type = 'collect',
1284
- left_link = lh_expr_chain,
1285
- right_link = rh_expr_chain
1172
+ type='collect',
1173
+ left_link=lh_expr_chain,
1174
+ right_link=rh_expr_chain
1286
1175
  )
1287
1176
 
1288
1177
  return (
@@ -1293,17 +1182,17 @@ class LanguageGraph():
1293
1182
 
1294
1183
  def process_step_expression(self,
1295
1184
  target_asset: LanguageGraphAsset,
1296
- expr_chain: Optional[ExpressionsChain],
1185
+ expr_chain: ExpressionsChain | None,
1297
1186
  step_expression: dict
1298
1187
  ) -> tuple[
1299
1188
  LanguageGraphAsset,
1300
- Optional[ExpressionsChain],
1301
- Optional[str]
1189
+ ExpressionsChain | None,
1190
+ str | None
1302
1191
  ]:
1303
- """
1304
- Recursively process an attack step expression.
1192
+ """Recursively process an attack step expression.
1305
1193
 
1306
1194
  Arguments:
1195
+ ---------
1307
1196
  target_asset - The asset type that this step expression should
1308
1197
  apply to. Initially it will contain the asset
1309
1198
  type to which the attack step belongs.
@@ -1317,21 +1206,22 @@ class LanguageGraph():
1317
1206
  step_expression - A dictionary containing the step expression.
1318
1207
 
1319
1208
  Return:
1209
+ ------
1320
1210
  A tuple triplet containing the target asset, the resulting parent
1321
1211
  associations chain, and the name of the attack step.
1322
- """
1323
1212
 
1213
+ """
1324
1214
  if logger.isEnabledFor(logging.DEBUG):
1325
1215
  # Avoid running json.dumps when not in debug
1326
1216
  logger.debug(
1327
1217
  'Processing Step Expression:\n%s',
1328
- json.dumps(step_expression, indent = 2)
1218
+ json.dumps(step_expression, indent=2)
1329
1219
  )
1330
1220
 
1331
1221
  result: tuple[
1332
1222
  LanguageGraphAsset,
1333
- Optional[ExpressionsChain],
1334
- Optional[str]
1223
+ ExpressionsChain | None,
1224
+ str | None
1335
1225
  ]
1336
1226
 
1337
1227
  match (step_expression['type']):
@@ -1371,14 +1261,14 @@ class LanguageGraph():
1371
1261
 
1372
1262
  def reverse_expr_chain(
1373
1263
  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
1264
+ expr_chain: ExpressionsChain | None,
1265
+ reverse_chain: ExpressionsChain | None
1266
+ ) -> ExpressionsChain | None:
1267
+ """Recursively reverse the associations chain. From parent to child or
1379
1268
  vice versa.
1380
1269
 
1381
1270
  Arguments:
1271
+ ---------
1382
1272
  expr_chain - A chain of nested tuples that specify the
1383
1273
  associations and set operations chain from an
1384
1274
  attack step to its connected attack step.
@@ -1386,93 +1276,96 @@ class LanguageGraph():
1386
1276
  current reversed associations chain.
1387
1277
 
1388
1278
  Return:
1279
+ ------
1389
1280
  The resulting reversed associations chain.
1281
+
1390
1282
  """
1391
1283
  if not expr_chain:
1392
1284
  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)
1285
+ match (expr_chain.type):
1286
+ case 'union' | 'intersection' | 'difference' | 'collect':
1287
+ left_reverse_chain = \
1288
+ self.reverse_expr_chain(expr_chain.left_link,
1289
+ reverse_chain)
1290
+ right_reverse_chain = \
1291
+ self.reverse_expr_chain(expr_chain.right_link,
1292
+ reverse_chain)
1293
+ if expr_chain.type == 'collect':
1294
+ new_expr_chain = ExpressionsChain(
1295
+ type=expr_chain.type,
1296
+ left_link=right_reverse_chain,
1297
+ right_link=left_reverse_chain
1298
+ )
1299
+ else:
1420
1300
  new_expr_chain = ExpressionsChain(
1421
- type = 'transitive',
1422
- sub_link = result_reverse_chain
1301
+ type=expr_chain.type,
1302
+ left_link=left_reverse_chain,
1303
+ right_link=right_reverse_chain
1423
1304
  )
1424
- return new_expr_chain
1425
1305
 
1426
- case 'field':
1427
- association = expr_chain.association
1306
+ return new_expr_chain
1428
1307
 
1429
- if not association:
1430
- raise LanguageGraphException(
1431
- "Missing association for expressions chain"
1432
- )
1308
+ case 'transitive':
1309
+ result_reverse_chain = self.reverse_expr_chain(
1310
+ expr_chain.sub_link, reverse_chain)
1311
+ new_expr_chain = ExpressionsChain(
1312
+ type='transitive',
1313
+ sub_link=result_reverse_chain
1314
+ )
1315
+ return new_expr_chain
1433
1316
 
1434
- if not expr_chain.fieldname:
1435
- raise LanguageGraphException(
1436
- "Missing field name for expressions chain"
1437
- )
1317
+ case 'field':
1318
+ association = expr_chain.association
1438
1319
 
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
1320
+ if not association:
1321
+ raise LanguageGraphException(
1322
+ "Missing association for expressions chain"
1445
1323
  )
1446
- return new_expr_chain
1447
1324
 
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
1325
+ if not expr_chain.fieldname:
1326
+ raise LanguageGraphException(
1327
+ "Missing field name for expressions chain"
1457
1328
  )
1458
- return new_expr_chain
1459
1329
 
1460
- case _:
1461
- msg = 'Unknown assoc chain element "%s"'
1462
- logger.error(msg, expr_chain.type)
1463
- raise LanguageGraphAssociationError(msg % expr_chain.type)
1330
+ opposite_fieldname = association.get_opposite_fieldname(
1331
+ expr_chain.fieldname)
1332
+ new_expr_chain = ExpressionsChain(
1333
+ type='field',
1334
+ association=association,
1335
+ fieldname=opposite_fieldname
1336
+ )
1337
+ return new_expr_chain
1338
+
1339
+ case 'subType':
1340
+ result_reverse_chain = self.reverse_expr_chain(
1341
+ expr_chain.sub_link,
1342
+ reverse_chain
1343
+ )
1344
+ new_expr_chain = ExpressionsChain(
1345
+ type='subType',
1346
+ sub_link=result_reverse_chain,
1347
+ subtype=expr_chain.subtype
1348
+ )
1349
+ return new_expr_chain
1350
+
1351
+ case _:
1352
+ msg = 'Unknown assoc chain element "%s"'
1353
+ logger.error(msg, expr_chain.type)
1354
+ raise LanguageGraphAssociationError(msg % expr_chain.type)
1464
1355
 
1465
1356
  def _resolve_variable(self, asset: LanguageGraphAsset, var_name) -> tuple:
1466
- """
1467
- Resolve a variable for a specific asset by variable name.
1357
+ """Resolve a variable for a specific asset by variable name.
1468
1358
 
1469
1359
  Arguments:
1360
+ ---------
1470
1361
  asset - a language graph asset to which the variable belongs
1471
1362
  var_name - a string representing the variable name
1472
1363
 
1473
1364
  Return:
1365
+ ------
1474
1366
  A tuple containing the target asset and expressions chain required to
1475
1367
  reach it.
1368
+
1476
1369
  """
1477
1370
  if var_name not in asset.variables:
1478
1371
  var_expr = self._get_var_expr_for_asset(asset.name, var_name)
@@ -1490,13 +1383,15 @@ class LanguageGraph():
1490
1383
  lang_spec: dict[str, Any],
1491
1384
  assets: dict[str, LanguageGraphAsset]
1492
1385
  ) -> None:
1493
- """ Link associations to assets based on the language specification.
1386
+ """Link associations to assets based on the language specification.
1387
+
1494
1388
  Arguments:
1389
+ ---------
1495
1390
  lang_spec - the language specification dictionary
1496
1391
  assets - a dictionary of LanguageGraphAsset objects
1497
1392
  indexed by their names
1498
- """
1499
1393
 
1394
+ """
1500
1395
  for association_dict in lang_spec['associations']:
1501
1396
  logger.debug(
1502
1397
  'Create association language graph nodes for association %s',
@@ -1521,20 +1416,20 @@ class LanguageGraph():
1521
1416
  right_asset = assets[right_asset_name]
1522
1417
 
1523
1418
  assoc_node = LanguageGraphAssociation(
1524
- name = association_dict['name'],
1525
- left_field = LanguageGraphAssociationField(
1419
+ name=association_dict['name'],
1420
+ left_field=LanguageGraphAssociationField(
1526
1421
  left_asset,
1527
1422
  association_dict['leftField'],
1528
1423
  association_dict['leftMultiplicity']['min'],
1529
1424
  association_dict['leftMultiplicity']['max']
1530
1425
  ),
1531
- right_field = LanguageGraphAssociationField(
1426
+ right_field=LanguageGraphAssociationField(
1532
1427
  right_asset,
1533
1428
  association_dict['rightField'],
1534
1429
  association_dict['rightMultiplicity']['min'],
1535
1430
  association_dict['rightMultiplicity']['max']
1536
1431
  ),
1537
- info = association_dict['meta']
1432
+ info=association_dict['meta']
1538
1433
  )
1539
1434
 
1540
1435
  # Add the association to the left and right asset
@@ -1547,10 +1442,8 @@ class LanguageGraph():
1547
1442
  lang_spec: dict[str, Any],
1548
1443
  assets: dict[str, LanguageGraphAsset]
1549
1444
  ) -> None:
1445
+ """Link assets based on inheritance and associations.
1550
1446
  """
1551
- Link assets based on inheritance and associations.
1552
- """
1553
-
1554
1447
  for asset_dict in lang_spec['assets']:
1555
1448
  asset = assets[asset_dict['name']]
1556
1449
  if asset_dict['superAsset']:
@@ -1562,18 +1455,20 @@ class LanguageGraph():
1562
1455
  raise LanguageGraphSuperAssetNotFoundError(
1563
1456
  msg % (asset_dict["superAsset"], asset_dict["name"]))
1564
1457
 
1565
- super_asset.own_sub_assets.add(asset)
1458
+ super_asset.own_sub_assets.append(asset)
1566
1459
  asset.own_super_asset = super_asset
1567
1460
 
1568
1461
  def _set_variables_for_assets(
1569
1462
  self, assets: dict[str, LanguageGraphAsset]
1570
1463
  ) -> None:
1571
- """ Set the variables for each asset based on the language specification.
1464
+ """Set the variables for each asset based on the language specification.
1465
+
1572
1466
  Arguments:
1467
+ ---------
1573
1468
  assets - a dictionary of LanguageGraphAsset objects
1574
1469
  indexed by their names
1575
- """
1576
1470
 
1471
+ """
1577
1472
  for asset in assets.values():
1578
1473
  logger.debug(
1579
1474
  'Set variables for asset %s', asset.name
@@ -1584,189 +1479,138 @@ class LanguageGraph():
1584
1479
  # Avoid running json.dumps when not in debug
1585
1480
  logger.debug(
1586
1481
  'Processing Variable Expression:\n%s',
1587
- json.dumps(variable, indent = 2)
1482
+ json.dumps(variable, indent=2)
1588
1483
  )
1589
1484
  self._resolve_variable(asset, variable['name'])
1590
1485
 
1591
1486
  def _generate_attack_steps(self, assets) -> None:
1592
1487
  """
1593
- Generate all of the attack steps for each asset type
1594
- based on the language specification.
1488
+ Generate attack steps for all assets and link them according to the
1489
+ language specification.
1490
+
1491
+ This method performs three phases:
1492
+
1493
+ 1. Create attack step nodes for each asset, including detectors.
1494
+ 2. Inherit attack steps from super-assets, respecting overrides.
1495
+ 3. Link attack steps via 'reaches' and evaluate 'exist'/'notExist'
1496
+ requirements.
1497
+
1498
+ Args:
1499
+ assets (dict): Mapping of asset names to asset objects.
1500
+
1501
+ Raises:
1502
+ LanguageGraphStepExpressionError: If a step expression cannot be
1503
+ resolved to a target asset or attack step.
1504
+ LanguageGraphException: If an existence requirement cannot be
1505
+ resolved.
1595
1506
  """
1596
1507
  langspec_dict = {}
1508
+
1597
1509
  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():
1510
+ logger.debug('Create attack steps language graph nodes for asset %s', asset.name)
1511
+ for step_dict in self._get_attacks_for_asset_type(asset.name).values():
1604
1512
  logger.debug(
1605
- 'Create attack step language graph nodes for %s',
1606
- attack_step_attribs['name']
1513
+ 'Create attack step language graph nodes for %s', step_dict['name']
1607
1514
  )
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
1515
+ node = LanguageGraphAttackStep(
1516
+ name=step_dict['name'],
1517
+ type=step_dict['type'],
1518
+ asset=asset,
1519
+ ttc=step_dict['ttc'],
1520
+ overrides=(
1521
+ step_dict['reaches']['overrides']
1522
+ if step_dict['reaches'] else False
1617
1523
  ),
1618
- own_children = {},
1619
- own_parents = {},
1620
- info = attack_step_attribs['meta'],
1621
- tags = list(attack_step_attribs['tags'])
1524
+ own_children={}, own_parents={},
1525
+ info=step_dict['meta'],
1526
+ tags=list(step_dict['tags'])
1622
1527
  )
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(
1528
+ langspec_dict[node.full_name] = step_dict
1529
+ asset.attack_steps[node.name] = node
1530
+
1531
+ for det in step_dict.get('detectors', {}).values():
1532
+ node.detectors[det['name']] = Detector(
1631
1533
  context=Context(
1632
- {
1633
- label: assets[asset]
1634
- for label, asset in detector["context"].items()
1635
- }
1534
+ {lbl: assets[a] for lbl, a in det['context'].items()}
1636
1535
  ),
1637
- name=detector.get("name"),
1638
- type=detector.get("type"),
1639
- tprate=detector.get("tprate"),
1536
+ name=det.get('name'),
1537
+ type=det.get('type'),
1538
+ tprate=det.get('tprate'),
1640
1539
  )
1641
1540
 
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
1541
+ pending = list(self.assets.values())
1542
+ while pending:
1543
+ asset = pending.pop(0)
1544
+ super_asset = asset.own_super_asset
1545
+ if super_asset in pending:
1546
+ # Super asset still needs processing, defer this asset
1547
+ pending.append(asset)
1548
+ continue
1549
+ if not super_asset:
1550
+ continue
1551
+ for super_step in super_asset.attack_steps.values():
1552
+ current_step = asset.attack_steps.get(super_step.name)
1553
+ if not current_step:
1554
+ node = LanguageGraphAttackStep(
1555
+ name=super_step.name,
1556
+ type=super_step.type,
1557
+ asset=asset,
1558
+ ttc=super_step.ttc,
1559
+ overrides=False,
1560
+ own_children={},
1561
+ own_parents={},
1562
+ info=super_step.info,
1563
+ tags=list(super_step.tags)
1564
+ )
1565
+ node.inherits = super_step
1566
+ asset.attack_steps[super_step.name] = node
1567
+ elif current_step.overrides:
1687
1568
  continue
1569
+ else:
1570
+ current_step.inherits = super_step
1571
+ current_step.tags += super_step.tags
1572
+ current_step.info |= super_step.info
1688
1573
 
1689
- langspec_entry = langspec_dict[attack_step.full_name]
1690
- step_expressions = (
1691
- langspec_entry['reaches']['stepExpressions']
1692
- if langspec_entry['reaches'] else []
1693
- )
1574
+ for asset in self.assets.values():
1575
+ for step in asset.attack_steps.values():
1576
+ logger.debug('Determining children for attack step %s', step.name)
1577
+ if step.full_name not in langspec_dict:
1578
+ continue
1694
1579
 
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'
1580
+ entry = langspec_dict[step.full_name]
1581
+ for expr in (entry['reaches']['stepExpressions'] if entry['reaches'] else []):
1582
+ tgt_asset, chain, tgt_name = self.process_step_expression(step.asset, None, expr)
1583
+ if not tgt_asset:
1707
1584
  raise LanguageGraphStepExpressionError(
1708
- msg % json.dumps(step_expression, indent = 2)
1585
+ 'Failed to find target asset for:\n%s' % json.dumps(expr, indent=2)
1709
1586
  )
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'
1587
+ if tgt_name not in tgt_asset.attack_steps:
1716
1588
  raise LanguageGraphStepExpressionError(
1717
- msg % (
1718
- target_attack_step_name,
1719
- target_asset.name,
1720
- json.dumps(step_expression, indent = 2)
1721
- )
1589
+ 'Failed to find target attack step %s on %s:\n%s' %
1590
+ (tgt_name, tgt_asset.name, json.dumps(expr, indent=2))
1722
1591
  )
1723
1592
 
1724
- target_attack_step = target_asset_attack_steps[
1725
- target_attack_step_name]
1593
+ tgt = tgt_asset.attack_steps[tgt_name]
1594
+ step.own_children.setdefault(tgt, []).append(chain)
1595
+ tgt.own_parents.setdefault(step, []).append(self.reverse_expr_chain(chain, None))
1726
1596
 
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:
1597
+ if step.type in ('exist', 'notExist'):
1598
+ reqs = entry['requires']['stepExpressions'] if entry['requires'] else []
1599
+ if not reqs:
1745
1600
  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
- )
1601
+ 'Missing requirements for "%s" of type "%s":\n%s' %
1602
+ (step.name, step.type, json.dumps(entry, indent=2))
1752
1603
  )
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
1604
+ for expr in reqs:
1605
+ _, chain, _ = self.process_step_expression(step.asset, None, expr)
1606
+ if chain is None:
1607
+ raise LanguageGraphException(
1608
+ f'Failed to find existence step requirement for:\n{expr}'
1760
1609
  )
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)
1610
+ step.own_requires.append(chain)
1766
1611
 
1767
1612
  def _generate_graph(self) -> None:
1768
- """
1769
- Generate language graph starting from the MAL language specification
1613
+ """Generate language graph starting from the MAL language specification
1770
1614
  given in the constructor.
1771
1615
  """
1772
1616
  # Generate all of the asset nodes of the language graph.
@@ -1777,14 +1621,14 @@ class LanguageGraph():
1777
1621
  asset_dict['name']
1778
1622
  )
1779
1623
  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']
1624
+ name=asset_dict['name'],
1625
+ own_associations={},
1626
+ attack_steps={},
1627
+ info=asset_dict['meta'],
1628
+ own_super_asset=None,
1629
+ own_sub_assets=list(),
1630
+ own_variables={},
1631
+ is_abstract=asset_dict['isAbstract']
1788
1632
  )
1789
1633
  self.assets[asset_dict['name']] = asset_node
1790
1634
 
@@ -1801,24 +1645,26 @@ class LanguageGraph():
1801
1645
  self._generate_attack_steps(self.assets)
1802
1646
 
1803
1647
  def _get_attacks_for_asset_type(self, asset_type: str) -> dict[str, dict]:
1804
- """
1805
- Get all Attack Steps for a specific asset type.
1648
+ """Get all Attack Steps for a specific asset type.
1806
1649
 
1807
1650
  Arguments:
1651
+ ---------
1808
1652
  asset_type - the name of the asset type we want to
1809
1653
  list the possible attack steps for
1810
1654
 
1811
1655
  Return:
1656
+ ------
1812
1657
  A dictionary containing the possible attacks for the
1813
1658
  specified asset type. Each key in the dictionary is an attack name
1814
1659
  associated with a dictionary containing other characteristics of the
1815
1660
  attack such as type of attack, TTC distribution, child attack steps
1816
1661
  and other information
1662
+
1817
1663
  """
1818
1664
  attack_steps: dict = {}
1819
1665
  try:
1820
1666
  asset = next(
1821
- asset for asset in self._lang_spec['assets'] \
1667
+ asset for asset in self._lang_spec['assets']
1822
1668
  if asset['name'] == asset_type
1823
1669
  )
1824
1670
  except StopIteration:
@@ -1838,17 +1684,19 @@ class LanguageGraph():
1838
1684
  return attack_steps
1839
1685
 
1840
1686
  def _get_associations_for_asset_type(self, asset_type: str) -> list[dict]:
1841
- """
1842
- Get all associations for a specific asset type.
1687
+ """Get all associations for a specific asset type.
1843
1688
 
1844
1689
  Arguments:
1690
+ ---------
1845
1691
  asset_type - the name of the asset type for which we want to
1846
1692
  list the associations
1847
1693
 
1848
1694
  Return:
1695
+ ------
1849
1696
  A list of dicts, where each dict represents an associations
1850
1697
  for the specified asset type. Each dictionary contains
1851
1698
  name and meta information about the association.
1699
+
1852
1700
  """
1853
1701
  logger.debug(
1854
1702
  'Get associations for %s asset from '
@@ -1856,7 +1704,7 @@ class LanguageGraph():
1856
1704
  )
1857
1705
  associations: list = []
1858
1706
 
1859
- asset = next((asset for asset in self._lang_spec['assets'] \
1707
+ asset = next((asset for asset in self._lang_spec['assets']
1860
1708
  if asset['name'] == asset_type), None)
1861
1709
  if not asset:
1862
1710
  logger.error(
@@ -1865,8 +1713,8 @@ class LanguageGraph():
1865
1713
  )
1866
1714
  return associations
1867
1715
 
1868
- assoc_iter = (assoc for assoc in self._lang_spec['associations'] \
1869
- if assoc['leftAsset'] == asset_type or \
1716
+ assoc_iter = (assoc for assoc in self._lang_spec['associations']
1717
+ if assoc['leftAsset'] == asset_type or
1870
1718
  assoc['rightAsset'] == asset_type)
1871
1719
  assoc = next(assoc_iter, None)
1872
1720
  while assoc:
@@ -1877,20 +1725,21 @@ class LanguageGraph():
1877
1725
 
1878
1726
  def _get_variables_for_asset_type(
1879
1727
  self, asset_type: str) -> list[dict]:
1880
- """
1881
- Get variables for a specific asset type.
1728
+ """Get variables for a specific asset type.
1882
1729
  Note: Variables are the ones specified in MAL through `let` statements
1883
1730
 
1884
1731
  Arguments:
1732
+ ---------
1885
1733
  asset_type - a string representing the asset type which
1886
1734
  contains the variables
1887
1735
 
1888
1736
  Return:
1737
+ ------
1889
1738
  A list of dicts representing the step expressions for the variables
1890
1739
  belonging to the asset.
1891
- """
1892
1740
 
1893
- asset_dict = next((asset for asset in self._lang_spec['assets'] \
1741
+ """
1742
+ asset_dict = next((asset for asset in self._lang_spec['assets']
1894
1743
  if asset['name'] == asset_type), None)
1895
1744
  if not asset_dict:
1896
1745
  msg = 'Failed to find asset type %s in language specification '\
@@ -1902,21 +1751,22 @@ class LanguageGraph():
1902
1751
 
1903
1752
  def _get_var_expr_for_asset(
1904
1753
  self, asset_type: str, var_name) -> dict:
1905
- """
1906
- Get a variable for a specific asset type by variable name.
1754
+ """Get a variable for a specific asset type by variable name.
1907
1755
 
1908
1756
  Arguments:
1757
+ ---------
1909
1758
  asset_type - a string representing the type of asset which
1910
1759
  contains the variable
1911
1760
  var_name - a string representing the variable name
1912
1761
 
1913
1762
  Return:
1763
+ ------
1914
1764
  A dictionary representing the step expression for the variable.
1915
- """
1916
1765
 
1766
+ """
1917
1767
  vars_dict = self._get_variables_for_asset_type(asset_type)
1918
1768
 
1919
- var_expr = next((var_entry['stepExpression'] for var_entry \
1769
+ var_expr = next((var_entry['stepExpression'] for var_entry
1920
1770
  in vars_dict if var_entry['name'] == var_name), None)
1921
1771
 
1922
1772
  if not var_expr:
@@ -1927,10 +1777,8 @@ class LanguageGraph():
1927
1777
  return var_expr
1928
1778
 
1929
1779
  def regenerate_graph(self) -> None:
1930
- """
1931
- Regenerate language graph starting from the MAL language specification
1780
+ """Regenerate language graph starting from the MAL language specification
1932
1781
  given in the constructor.
1933
1782
  """
1934
-
1935
1783
  self.assets = {}
1936
1784
  self._generate_graph()