mal-toolbox 0.1.12__py3-none-any.whl → 0.3.0__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.
@@ -4,12 +4,12 @@ MAL-Toolbox Language Graph Module
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- import copy
8
7
  import logging
9
8
  import json
10
9
  import zipfile
11
10
 
12
11
  from dataclasses import dataclass, field
12
+ from functools import cached_property
13
13
  from typing import Any, Optional
14
14
 
15
15
  from maltoolbox.file_utils import (
@@ -26,43 +26,91 @@ from ..exceptions import (
26
26
 
27
27
  logger = logging.getLogger(__name__)
28
28
 
29
+
30
+ def disaggregate_attack_step_full_name(
31
+ attack_step_full_name: str) -> list[str]:
32
+ return attack_step_full_name.split(':')
33
+
34
+
35
+ @dataclass
36
+ class Detector:
37
+ context: Context
38
+ name: Optional[str]
39
+ type: Optional[str]
40
+ tprate: Optional[dict]
41
+
42
+ def to_dict(self) -> dict:
43
+ return {
44
+ "context": self.context.to_dict(),
45
+ "name": self.name,
46
+ "type": self.type,
47
+ "tprate": self.tprate,
48
+ }
49
+
50
+
51
+ class Context(dict):
52
+ def __init__(self, context) -> None:
53
+ super().__init__(context)
54
+ self._context_dict = context
55
+ for label, asset in context.items():
56
+ setattr(self, label, asset)
57
+
58
+ def to_dict(self) -> dict:
59
+ return {label: asset.name for label, asset in self.items()}
60
+
61
+ def __str__(self) -> str:
62
+ return str({label: asset.name for label, asset in self._context_dict.items()})
63
+
64
+
29
65
  @dataclass
30
66
  class LanguageGraphAsset:
31
- name: Optional[str] = None
32
- associations: list[LanguageGraphAssociation] = field(default_factory=lambda: [])
33
- attack_steps: list[LanguageGraphAttackStep] = field(default_factory=lambda: [])
34
- description: dict = field(default_factory=lambda: {})
35
- # MAL languages currently do not support multiple inheritance, but this is
36
- # futureproofing at its most hopeful.
37
- super_assets: list = field(default_factory=lambda: [])
38
- sub_assets: list = field(default_factory=lambda: [])
67
+ name: str
68
+ own_associations: dict[str, LanguageGraphAssociation] = \
69
+ field(default_factory = dict)
70
+ attack_steps: dict[str, LanguageGraphAttackStep] = \
71
+ field(default_factory = dict)
72
+ info: dict = field(default_factory = dict)
73
+ own_super_asset: Optional[LanguageGraphAsset] = None
74
+ own_sub_assets: set[LanguageGraphAsset] = field(default_factory = set)
75
+ own_variables: dict = field(default_factory = dict)
39
76
  is_abstract: Optional[bool] = None
40
77
 
78
+
41
79
  def to_dict(self) -> dict:
42
80
  """Convert LanguageGraphAsset to dictionary"""
43
81
  node_dict: dict[str, Any] = {
44
82
  'name': self.name,
45
- 'associations': [],
46
- 'attack_steps': [],
47
- 'description': self.description,
48
- 'super_assets': [],
49
- 'sub_assets': []
83
+ 'associations': {},
84
+ 'attack_steps': {},
85
+ 'info': self.info,
86
+ 'super_asset': self.own_super_asset.name \
87
+ if self.own_super_asset else "",
88
+ 'sub_assets': [asset.name for asset in self.own_sub_assets],
89
+ 'variables': {},
90
+ 'is_abstract': self.is_abstract
50
91
  }
51
92
 
52
- for assoc in self.associations:
53
- node_dict['associations'].append((assoc.name,
54
- assoc.left_field.fieldname,
55
- assoc.right_field.fieldname))
56
- for attack_step in self.attack_steps:
57
- node_dict['attack_steps'].append(attack_step.name)
58
- for super_asset in self.super_assets:
59
- node_dict['super_assets'].append(super_asset.name)
60
- for sub_asset in self.sub_assets:
61
- node_dict['sub_assets'].append(sub_asset.name)
93
+ for fieldname, assoc in self.own_associations.items():
94
+ node_dict['associations'][fieldname] = assoc.to_dict()
95
+ for attack_step in self.attack_steps.values():
96
+ node_dict['attack_steps'][attack_step.name] = \
97
+ attack_step.to_dict()
98
+ for variable_name, (var_target_asset, var_expr_chain) in \
99
+ self.own_variables.items():
100
+ node_dict['variables'][variable_name] = (
101
+ var_target_asset.name,
102
+ var_expr_chain.to_dict()
103
+ )
62
104
  return node_dict
63
105
 
106
+
64
107
  def __repr__(self) -> str:
65
- return str(self.to_dict())
108
+ return f'LanguageGraphAsset(name: "{self.name}")'
109
+
110
+
111
+ def __hash__(self):
112
+ return hash(self.name)
113
+
66
114
 
67
115
  def is_subasset_of(self, target_asset: LanguageGraphAsset) -> bool:
68
116
  """
@@ -76,15 +124,16 @@ class LanguageGraphAsset:
76
124
  True if this asset extends the target_asset via inheritance.
77
125
  False otherwise.
78
126
  """
79
- current_assets = [self]
80
- while (current_assets):
81
- current_asset = current_assets.pop()
127
+ current_asset: Optional[LanguageGraphAsset] = self
128
+ while (current_asset):
82
129
  if current_asset == target_asset:
83
130
  return True
84
- current_assets.extend(current_asset.super_assets)
131
+ current_asset = current_asset.own_super_asset
85
132
  return False
86
133
 
87
- def get_all_subassets(self) -> list[LanguageGraphAsset]:
134
+
135
+ @cached_property
136
+ def sub_assets(self) -> set[LanguageGraphAsset]:
88
137
  """
89
138
  Return a list of all of the assets that directly or indirectly extend
90
139
  this asset.
@@ -92,15 +141,18 @@ class LanguageGraphAsset:
92
141
  Return:
93
142
  A list of all of the assets that extend this asset plus itself.
94
143
  """
95
- current_assets = [self]
96
- subassets = [self]
97
- while (current_assets):
98
- current_asset = current_assets.pop()
99
- current_assets.extend(current_asset.sub_assets)
100
- subassets.extend(current_asset.sub_assets)
101
- return subassets
102
-
103
- def get_all_superassets(self) -> list[LanguageGraphAsset]:
144
+ subassets: list[LanguageGraphAsset] = []
145
+ for subasset in self.own_sub_assets:
146
+ subassets.extend(subasset.sub_assets)
147
+
148
+ subassets.extend(self.own_sub_assets)
149
+ subassets.append(self)
150
+
151
+ return set(subassets)
152
+
153
+
154
+ @cached_property
155
+ def super_assets(self) -> list[LanguageGraphAsset]:
104
156
  """
105
157
  Return a list of all of the assets that this asset directly or
106
158
  indirectly extends.
@@ -108,24 +160,79 @@ class LanguageGraphAsset:
108
160
  Return:
109
161
  A list of all of the assets that this asset extends plus itself.
110
162
  """
111
- current_assets = [self]
112
- superassets = [self]
113
- while (current_assets):
114
- current_asset = current_assets.pop()
115
- current_assets.extend(current_asset.super_assets)
116
- superassets.extend(current_asset.super_assets)
163
+ current_asset: Optional[LanguageGraphAsset] = self
164
+ superassets = []
165
+ while (current_asset):
166
+ superassets.append(current_asset)
167
+ current_asset = current_asset.own_super_asset
117
168
  return superassets
118
169
 
170
+
171
+ @cached_property
172
+ def associations(self) -> dict[str, LanguageGraphAssociation]:
173
+ """
174
+ Return a list of all of the associations that belong to this asset
175
+ directly or indirectly via inheritance.
176
+
177
+ Return:
178
+ A list of all of the associations that apply to this asset, either
179
+ directly or via inheritance.
180
+ """
181
+
182
+ associations = dict(self.own_associations)
183
+ if self.own_super_asset:
184
+ associations |= self.own_super_asset.associations
185
+ return associations
186
+
187
+
188
+ @property
189
+ def variables(self) -> dict[str, ExpressionsChain]:
190
+ """
191
+ Return a list of all of the variables that belong to this asset
192
+ directly or indirectly via inheritance.
193
+
194
+ Return:
195
+ A list of all of the variables that apply to this asset, either
196
+ directly or via inheritance.
197
+ """
198
+
199
+ all_vars = dict(self.own_variables)
200
+ if self.own_super_asset:
201
+ all_vars |= self.own_super_asset.variables
202
+ return all_vars
203
+
204
+
205
+ def get_variable(
206
+ self,
207
+ var_name: str,
208
+ ) -> Optional[tuple]:
209
+ """
210
+ Return a variable matching the given name if the asset or any of its
211
+ super assets has its definition.
212
+
213
+ Return:
214
+ A tuple containing the target asset and expressions chain to it if the
215
+ variable was defined.
216
+ None otherwise.
217
+ """
218
+ current_asset: Optional[LanguageGraphAsset] = self
219
+ while (current_asset):
220
+ if var_name in current_asset.own_variables:
221
+ return current_asset.own_variables[var_name]
222
+ current_asset = current_asset.own_super_asset
223
+ return None
224
+
225
+
119
226
  def get_all_common_superassets(
120
227
  self, other: LanguageGraphAsset
121
- ) -> set[Optional[str]]:
228
+ ) -> set[str]:
122
229
  """Return a set of all common ancestors between this asset
123
230
  and the other asset given as parameter"""
124
231
  self_superassets = set(
125
- asset.name for asset in self.get_all_superassets()
232
+ asset.name for asset in self.super_assets
126
233
  )
127
234
  other_superassets = set(
128
- asset.name for asset in other.get_all_superassets()
235
+ asset.name for asset in other.super_assets
129
236
  )
130
237
  return self_superassets.intersection(other_superassets)
131
238
 
@@ -143,12 +250,13 @@ class LanguageGraphAssociation:
143
250
  name: str
144
251
  left_field: LanguageGraphAssociationField
145
252
  right_field: LanguageGraphAssociationField
146
- description: Optional[dict] = None
253
+ info: dict = field(default_factory = dict)
147
254
 
148
255
  def to_dict(self) -> dict:
149
256
  """Convert LanguageGraphAssociation to dictionary"""
150
- node_dict = {
257
+ assoc_dict = {
151
258
  'name': self.name,
259
+ 'info': self.info,
152
260
  'left': {
153
261
  'asset': self.left_field.asset.name,
154
262
  'fieldname': self.left_field.fieldname,
@@ -160,14 +268,41 @@ class LanguageGraphAssociation:
160
268
  'fieldname': self.right_field.fieldname,
161
269
  'min': self.right_field.minimum,
162
270
  'max': self.right_field.maximum
163
- },
164
- 'description': self.description
271
+ }
165
272
  }
166
273
 
167
- return node_dict
274
+ return assoc_dict
275
+
168
276
 
169
277
  def __repr__(self) -> str:
170
- return str(self.to_dict())
278
+ return (f'LanguageGraphAssociation(name: "{self.name}", '
279
+ f'left_field: {self.left_field}, '
280
+ f'right_field: {self.right_field})')
281
+
282
+
283
+ @property
284
+ def full_name(self) -> str:
285
+ """
286
+ Return the full name of the association. This is a combination of the
287
+ association name, left field name, left asset type, right field name,
288
+ and right asset type.
289
+ """
290
+ full_name = '%s_%s_%s' % (
291
+ self.name,\
292
+ self.left_field.fieldname,\
293
+ self.right_field.fieldname
294
+ )
295
+ return full_name
296
+
297
+
298
+ def get_field(self, fieldname: str) -> LanguageGraphAssociationField:
299
+ """
300
+ Return the field that matches the `fieldname` given as parameter.
301
+ """
302
+ if self.right_field.fieldname == fieldname:
303
+ return self.right_field
304
+ return self.left_field
305
+
171
306
 
172
307
  def contains_fieldname(self, fieldname: str) -> bool:
173
308
  """
@@ -184,6 +319,7 @@ class LanguageGraphAssociation:
184
319
  return True
185
320
  return False
186
321
 
322
+
187
323
  def contains_asset(self, asset: Any) -> bool:
188
324
  """
189
325
  Check if the association matches the asset given as a parameter. A
@@ -201,6 +337,7 @@ class LanguageGraphAssociation:
201
337
  return True
202
338
  return False
203
339
 
340
+
204
341
  def get_opposite_fieldname(self, fieldname: str) -> str:
205
342
  """
206
343
  Return the opposite field name if the association contains the field
@@ -221,49 +358,36 @@ class LanguageGraphAssociation:
221
358
  logger.error(msg, fieldname, self.name)
222
359
  raise LanguageGraphAssociationError(msg % (fieldname, self.name))
223
360
 
224
- def get_opposite_asset(
225
- self, asset: LanguageGraphAsset
226
- ) -> Optional[LanguageGraphAsset]:
227
- """
228
- Return the opposite asset if the association matches the asset given
229
- as a parameter. A match can either be an explicit one or if the asset
230
- given subassets either of the two assets that are part of the
231
- association.
232
-
233
- Arguments:
234
- asset - the asset to look for
235
- Return the other asset if the parameter matched either of the
236
- two. None, otherwise.
237
- """
238
- #TODO Should check to see which one is the tightest fit for
239
- # associations between assets on different levels of the same
240
- # inheritance chain.
241
- if asset.is_subasset_of(self.left_field.asset):
242
- return self.right_field.asset
243
- if asset.is_subasset_of(self.right_field.asset):
244
- return self.left_field.asset
245
-
246
- logger.warning(
247
- 'Requested asset "%s" from association %s'
248
- 'which did not contain it!', asset.name, self.name
249
- )
250
- return None
251
-
252
361
 
253
362
  @dataclass
254
363
  class LanguageGraphAttackStep:
255
364
  name: str
256
365
  type: str
257
366
  asset: LanguageGraphAsset
258
- ttc: dict = field(default_factory = lambda: {})
259
- children: dict = field(default_factory = lambda: {})
260
- parents: dict = field(default_factory = lambda: {})
261
- description: dict = field(default_factory = lambda: {})
262
- attributes: Optional[dict] = None
367
+ ttc: dict = field(default_factory = dict)
368
+ overrides: bool = False
369
+ children: dict = field(default_factory = dict)
370
+ parents: dict = field(default_factory = dict)
371
+ info: dict = field(default_factory = dict)
372
+ inherits: Optional[LanguageGraphAttackStep] = None
373
+ tags: set = field(default_factory = set)
374
+ _attributes: Optional[dict] = None
375
+ detectors: dict = field(default_factory = lambda: {})
376
+
377
+
378
+ def __hash__(self):
379
+ return hash(self.full_name)
380
+
263
381
 
264
382
  @property
265
- def qualified_name(self) -> str:
266
- return f"{self.asset.name}:{self.name}"
383
+ def full_name(self) -> str:
384
+ """
385
+ Return the full name of the attack step. This is a combination of the
386
+ asset type name to which the attack step belongs and attack step name
387
+ itself.
388
+ """
389
+ full_name = self.asset.name + ':' + self.name
390
+ return full_name
267
391
 
268
392
  def to_dict(self) -> dict:
269
393
  node_dict: dict[Any, Any] = {
@@ -273,95 +397,140 @@ class LanguageGraphAttackStep:
273
397
  'ttc': self.ttc,
274
398
  'children': {},
275
399
  'parents': {},
276
- 'description': self.description
400
+ 'info': self.info,
401
+ 'overrides': self.overrides,
402
+ 'inherits': self.inherits.full_name if self.inherits else None,
403
+ 'tags': list(self.tags),
404
+ 'detectors': {label: detector.to_dict() for label, detector in
405
+ self.detectors.items()},
277
406
  }
278
407
 
279
408
  for child in self.children:
280
409
  node_dict['children'][child] = []
281
- for (_, dep_chain) in self.children[child]:
282
- if dep_chain:
410
+ for (_, expr_chain) in self.children[child]:
411
+ if expr_chain:
283
412
  node_dict['children'][child].append(
284
- dep_chain.to_dict())
413
+ expr_chain.to_dict())
285
414
  else:
286
415
  node_dict['children'][child].append(None)
287
416
 
288
417
  for parent in self.parents:
289
418
  node_dict['parents'][parent] = []
290
- for (_, dep_chain) in self.parents[parent]:
291
- if dep_chain:
419
+ for (_, expr_chain) in self.parents[parent]:
420
+ if expr_chain:
292
421
  node_dict['parents'][parent].append(
293
- dep_chain.to_dict())
422
+ expr_chain.to_dict())
294
423
  else:
295
424
  node_dict['parents'][parent].append(None)
296
425
 
426
+ if hasattr(self, 'own_requires'):
427
+ node_dict['requires'] = []
428
+ for requirement in self.own_requires:
429
+ node_dict['requires'].append(requirement.to_dict())
430
+
297
431
  return node_dict
298
432
 
433
+
434
+ @cached_property
435
+ def requires(self):
436
+ if not hasattr(self, 'own_requires'):
437
+ requirements = []
438
+ else:
439
+ requirements = self.own_requires
440
+
441
+ if self.inherits:
442
+ requirements.extend(self.inherits.requires)
443
+ return requirements
444
+
445
+
299
446
  def __repr__(self) -> str:
300
447
  return str(self.to_dict())
301
448
 
302
449
 
303
- class DependencyChain:
304
- def __init__(self, type: str, next_link: Optional[DependencyChain]):
450
+ class ExpressionsChain:
451
+ def __init__(self,
452
+ type: str,
453
+ left_link: Optional[ExpressionsChain] = None,
454
+ right_link: Optional[ExpressionsChain] = None,
455
+ sub_link: Optional[ExpressionsChain] = None,
456
+ fieldname: Optional[str] = None,
457
+ association = None,
458
+ subtype = None
459
+ ):
305
460
  self.type = type
306
- self.next_link: Optional[DependencyChain] = next_link
307
- self.fieldname: str = ""
308
- self.association: Optional[LanguageGraphAssociation] = None
309
- self.left_chain: Optional[DependencyChain] = None
310
- self.right_chain: Optional[DependencyChain] = None
311
- self.subtype: Optional[Any] = None
312
- self.current_link: Optional[DependencyChain] = None
313
-
314
- def __iter__(self):
315
- self.current_link = self
316
- return self
317
-
318
- def __next__(self):
319
- if self.current_link:
320
- dep_chain = self.current_link
321
- self.current_link = self.current_link.next_link
322
- return dep_chain
323
- raise StopIteration
461
+ self.left_link: Optional[ExpressionsChain] = left_link
462
+ self.right_link: Optional[ExpressionsChain] = right_link
463
+ self.sub_link: Optional[ExpressionsChain] = sub_link
464
+ self.fieldname: Optional[str] = fieldname
465
+ self.association: Optional[LanguageGraphAssociation] = association
466
+ self.subtype: Optional[Any] = subtype
467
+
324
468
 
325
469
  def to_dict(self) -> dict:
326
- """Convert DependencyChain to dictionary"""
470
+ """Convert ExpressionsChain to dictionary"""
327
471
  match (self.type):
328
- case 'union' | 'intersection' | 'difference':
329
- return {self.type: {
330
- 'left': self.left_chain.to_dict()
331
- if self.left_chain else {},
332
- 'right': self.right_chain.to_dict()
333
- if self.right_chain else {}
334
- }
472
+ case 'union' | 'intersection' | 'difference' | 'collect':
473
+ return {
474
+ self.type: {
475
+ 'left': self.left_link.to_dict()
476
+ if self.left_link else {},
477
+ 'right': self.right_link.to_dict()
478
+ if self.right_link else {}
479
+ },
480
+ 'type': self.type
335
481
  }
336
482
 
337
483
  case 'field':
338
484
  if not self.association:
339
- raise LanguageGraphAssociationError("Missing association for dep chain")
340
- return {self.association.name:
341
- {'fieldname': self.fieldname,
342
- 'next_association':
343
- self.next_link.to_dict()
344
- if self.next_link else {}
345
- }
346
- }
485
+ raise LanguageGraphAssociationError(
486
+ "Missing association for expressions chain"
487
+ )
488
+ if self.fieldname == self.association.left_field.fieldname:
489
+ asset_type = self.association.left_field.asset.name
490
+ elif self.fieldname == self.association.right_field.fieldname:
491
+ asset_type = self.association.right_field.asset.name
492
+ else:
493
+ raise LanguageGraphException(
494
+ 'Failed to find fieldname "%s" in association:\n%s' %
495
+ (
496
+ self.fieldname,
497
+ json.dumps(self.association.to_dict(),
498
+ indent = 2)
499
+ )
500
+ )
501
+
502
+ return {
503
+ self.association.name:
504
+ {
505
+ 'fieldname': self.fieldname,
506
+ 'asset type': asset_type
507
+ },
508
+ 'type': self.type
509
+ }
347
510
 
348
511
  case 'transitive':
349
- return {'transitive':
350
- self.next_link.to_dict()
351
- if self.next_link else {}
512
+ if not self.sub_link:
513
+ raise LanguageGraphException(
514
+ "No sub link for transitive expressions chain"
515
+ )
516
+ return {
517
+ 'transitive': self.sub_link.to_dict(),
518
+ 'type': self.type
352
519
  }
353
520
 
354
521
  case 'subType':
355
522
  if not self.subtype:
356
523
  raise LanguageGraphException(
357
- "No subtype for dependency chain"
524
+ "No subtype for expressions chain"
358
525
  )
359
- if not self.next_link:
526
+ if not self.sub_link:
360
527
  raise LanguageGraphException(
361
- "No next link for subtype dependency chain"
528
+ "No sub link for subtype expressions chain"
362
529
  )
363
- return {'subType': self.subtype.name,
364
- 'expression': self.next_link.to_dict()
530
+ return {
531
+ 'subType': self.subtype.name,
532
+ 'expression': self.sub_link.to_dict(),
533
+ 'type': self.type
365
534
  }
366
535
 
367
536
  case _:
@@ -369,22 +538,133 @@ class DependencyChain:
369
538
  logger.error(msg, self.type)
370
539
  raise LanguageGraphAssociationError(msg % self.type)
371
540
 
541
+ @classmethod
542
+ def _from_dict(cls,
543
+ serialized_expr_chain: dict,
544
+ lang_graph: LanguageGraph,
545
+ ) -> Optional[ExpressionsChain]:
546
+ """Create ExpressionsChain from dict
547
+ Args:
548
+ serialized_expr_chain - expressions chain in dict format
549
+ lang_graph - the LanguageGraph that contains the assets,
550
+ associations, and attack steps relevant for
551
+ the expressions chain
552
+ """
553
+
554
+ if serialized_expr_chain is None or not serialized_expr_chain:
555
+ return None
556
+
557
+ if 'type' not in serialized_expr_chain:
558
+ logger.debug(json.dumps(serialized_expr_chain, indent = 2))
559
+ msg = 'Missing expressions chain type!'
560
+ logger.error(msg)
561
+ raise LanguageGraphAssociationError(msg)
562
+ return None
563
+
564
+ expr_chain_type = serialized_expr_chain['type']
565
+ match (expr_chain_type):
566
+ case 'union' | 'intersection' | 'difference' | 'collect':
567
+ left_link = cls._from_dict(
568
+ serialized_expr_chain[expr_chain_type]['left'],
569
+ lang_graph
570
+ )
571
+ right_link = cls._from_dict(
572
+ serialized_expr_chain[expr_chain_type]['right'],
573
+ lang_graph
574
+ )
575
+ new_expr_chain = ExpressionsChain(
576
+ type = expr_chain_type,
577
+ left_link = left_link,
578
+ right_link = right_link
579
+ )
580
+ return new_expr_chain
581
+
582
+ case 'field':
583
+ assoc_name = list(serialized_expr_chain.keys())[0]
584
+ target_asset = lang_graph.assets[\
585
+ serialized_expr_chain[assoc_name]['asset type']]
586
+ fieldname = serialized_expr_chain[assoc_name]['fieldname']
587
+
588
+ association = None
589
+ for assoc in target_asset.associations.values():
590
+ if assoc.contains_fieldname(fieldname) and \
591
+ assoc.name == assoc_name:
592
+ association = assoc
593
+ break
594
+
595
+ if association is None:
596
+ msg = 'Failed to find association "%s" with '\
597
+ 'fieldname "%s"'
598
+ logger.error(msg % (assoc_name, fieldname))
599
+ raise LanguageGraphException(msg % (assoc_name,
600
+ fieldname))
601
+
602
+ new_expr_chain = ExpressionsChain(
603
+ type = 'field',
604
+ association = association,
605
+ fieldname = fieldname
606
+ )
607
+ return new_expr_chain
608
+
609
+ case 'transitive':
610
+ sub_link = cls._from_dict(
611
+ serialized_expr_chain['transitive'],
612
+ lang_graph
613
+ )
614
+ new_expr_chain = ExpressionsChain(
615
+ type = 'transitive',
616
+ sub_link = sub_link
617
+ )
618
+ return new_expr_chain
619
+
620
+ case 'subType':
621
+ sub_link = cls._from_dict(
622
+ serialized_expr_chain['expression'],
623
+ lang_graph
624
+ )
625
+ subtype_name = serialized_expr_chain['subType']
626
+ if subtype_name in lang_graph.assets:
627
+ subtype_asset = lang_graph.assets[subtype_name]
628
+ else:
629
+ msg = 'Failed to find subtype %s'
630
+ logger.error(msg % subtype_name)
631
+ raise LanguageGraphException(msg % subtype_name)
632
+
633
+ new_expr_chain = ExpressionsChain(
634
+ type = 'subType',
635
+ sub_link = sub_link,
636
+ subtype = subtype_asset
637
+ )
638
+ return new_expr_chain
639
+
640
+ case _:
641
+ msg = 'Unknown expressions chain type %s!'
642
+ logger.error(msg, serialized_expr_chain['type'])
643
+ raise LanguageGraphAssociationError(msg %
644
+ serialized_expr_chain['type'])
645
+
646
+
372
647
  def __repr__(self) -> str:
373
648
  return str(self.to_dict())
374
649
 
375
650
 
376
651
  class LanguageGraph():
377
652
  """Graph representation of a MAL language"""
378
- def __init__(self, lang: dict):
379
- self.assets: list = []
380
- self.associations: list = []
381
- self.attack_steps: list = []
382
- self._lang_spec: dict = lang
383
- self.metadata = {
384
- "version": lang["defines"]["version"],
385
- "id": lang["defines"]["id"],
386
- }
387
- self._generate_graph()
653
+ def __init__(self, lang: Optional[dict] = None):
654
+ self.assets: dict = {}
655
+ if lang is not None:
656
+ self._lang_spec: dict = lang
657
+ self.metadata = {
658
+ "version": lang["defines"]["version"],
659
+ "id": lang["defines"]["id"],
660
+ }
661
+ self._generate_graph()
662
+
663
+
664
+ def __repr__(self) -> str:
665
+ return (f'LanguageGraph(id: "{self.metadata.get("id", "N/A")}", '
666
+ f'version: "{self.metadata.get("version", "N/A")}")')
667
+
388
668
 
389
669
  @classmethod
390
670
  def from_mal_spec(cls, mal_spec_file: str) -> LanguageGraph:
@@ -397,6 +677,7 @@ class LanguageGraph():
397
677
  logger.info("Loading mal spec %s", mal_spec_file)
398
678
  return LanguageGraph(MalCompiler().compile(mal_spec_file))
399
679
 
680
+
400
681
  @classmethod
401
682
  def from_mar_archive(cls, mar_archive: str) -> LanguageGraph:
402
683
  """
@@ -411,39 +692,241 @@ class LanguageGraph():
411
692
  langspec = archive.read('langspec.json')
412
693
  return LanguageGraph(json.loads(langspec))
413
694
 
695
+
414
696
  def _to_dict(self):
415
697
  """Converts LanguageGraph into a dict"""
416
- serialized_assets = []
417
- for asset in self.assets:
418
- serialized_assets.append(asset.to_dict())
419
- serialized_associations = []
420
- for associations in self.associations:
421
- serialized_associations.append(associations.to_dict())
422
- serialized_attack_steps = []
423
- for attack_step in self.attack_steps:
424
- serialized_attack_steps.append(attack_step.to_dict())
425
698
 
426
699
  logger.debug(
427
- 'Serializing %s assets, %s associations, and %s attack steps',
428
- len(serialized_assets), len(serialized_associations),
429
- len(serialized_attack_steps)
700
+ 'Serializing %s assets.', len(self.assets.items())
430
701
  )
431
702
 
432
- serialized_graph = {
433
- 'Assets': serialized_assets,
434
- 'Associations': serialized_associations,
435
- 'Attack Steps': serialized_attack_steps
436
- }
703
+ serialized_graph = {'metadata': self.metadata}
704
+ for asset in self.assets.values():
705
+ serialized_graph[asset.name] = asset.to_dict()
706
+
437
707
  return serialized_graph
438
708
 
709
+ def _link_association_to_assets(cls,
710
+ assoc: LanguageGraphAssociation,
711
+ left_asset: LanguageGraphAsset,
712
+ right_asset: LanguageGraphAsset):
713
+ left_asset.own_associations[assoc.right_field.fieldname] = assoc
714
+ right_asset.own_associations[assoc.left_field.fieldname] = assoc
715
+
439
716
  def save_to_file(self, filename: str) -> None:
440
717
  """Save to json/yml depending on extension"""
441
718
  return save_dict_to_file(filename, self._to_dict())
442
719
 
720
+
443
721
  @classmethod
444
- def _from_dict(cls, serialized_object: dict) -> None:
445
- raise NotImplementedError(
446
- "Converting from dict feature is not implemented yet")
722
+ def _from_dict(cls, serialized_graph: dict) -> LanguageGraph:
723
+ """Create LanguageGraph from dict
724
+ Args:
725
+ serialized_graph - LanguageGraph in dict format
726
+ """
727
+
728
+ logger.debug('Create language graph from dictionary.')
729
+ lang_graph = LanguageGraph()
730
+ lang_graph.metadata = serialized_graph.pop('metadata')
731
+
732
+ # Recreate all of the assets
733
+ for asset_dict in serialized_graph.values():
734
+ logger.debug(
735
+ 'Create asset language graph nodes for asset %s',
736
+ asset_dict['name']
737
+ )
738
+ asset_node = LanguageGraphAsset(
739
+ name = asset_dict['name'],
740
+ own_associations = {},
741
+ attack_steps = {},
742
+ info = asset_dict['info'],
743
+ own_super_asset = None,
744
+ own_sub_assets = set(),
745
+ own_variables = {},
746
+ is_abstract = asset_dict['is_abstract']
747
+ )
748
+ lang_graph.assets[asset_dict['name']] = asset_node
749
+
750
+ # Relink assets based on inheritance
751
+ for asset_dict in serialized_graph.values():
752
+ asset = lang_graph.assets[asset_dict['name']]
753
+ super_asset_name = asset_dict['super_asset']
754
+ if not super_asset_name:
755
+ continue
756
+
757
+ super_asset = lang_graph.assets[super_asset_name]
758
+ if not super_asset:
759
+ msg = 'Failed to find super asset "%s" for asset "%s"!'
760
+ logger.error(
761
+ msg, asset_dict["super_asset"], asset_dict["name"])
762
+ raise LanguageGraphSuperAssetNotFoundError(
763
+ msg % (asset_dict["super_asset"], asset_dict["name"]))
764
+
765
+ super_asset.own_sub_assets.add(asset)
766
+ asset.own_super_asset = super_asset
767
+
768
+ # Generate all of the association nodes of the language graph.
769
+ for asset_dict in serialized_graph.values():
770
+ logger.debug(
771
+ 'Create association language graph nodes for asset %s',
772
+ asset_dict['name']
773
+ )
774
+
775
+ asset = lang_graph.assets[asset_dict['name']]
776
+ for association in asset_dict['associations'].values():
777
+ left_asset = lang_graph.assets[association['left']['asset']]
778
+ if not left_asset:
779
+ msg = 'Left asset "%s" for association "%s" not found!'
780
+ logger.error(
781
+ msg, association['left']['asset'],
782
+ association['name'])
783
+ raise LanguageGraphAssociationError(
784
+ msg % (association['left']['asset'],
785
+ association['name']))
786
+
787
+ right_asset = lang_graph.assets[association['right']['asset']]
788
+ if not right_asset:
789
+ msg = 'Right asset "%s" for association "%s" not found!'
790
+ logger.error(
791
+ msg, association['right']['asset'],
792
+ association['name'])
793
+ raise LanguageGraphAssociationError(
794
+ msg % (association['right']['asset'],
795
+ association['name'])
796
+ )
797
+
798
+ assoc_node = LanguageGraphAssociation(
799
+ name = association['name'],
800
+ left_field = LanguageGraphAssociationField(
801
+ left_asset,
802
+ association['left']['fieldname'],
803
+ association['left']['min'],
804
+ association['left']['max']),
805
+ right_field = LanguageGraphAssociationField(
806
+ right_asset,
807
+ association['right']['fieldname'],
808
+ association['right']['min'],
809
+ association['right']['max']),
810
+ info = association['info']
811
+ )
812
+
813
+ # Add the association to the left and right asset
814
+ lang_graph._link_association_to_assets(assoc_node,
815
+ left_asset, right_asset)
816
+
817
+ # Recreate the variables
818
+ for asset_dict in serialized_graph.values():
819
+ asset = lang_graph.assets[asset_dict['name']]
820
+ for variable_name, var_target in asset_dict['variables'].items():
821
+ (target_asset_name, expr_chain_dict) = var_target
822
+ target_asset = lang_graph.assets[target_asset_name]
823
+ expr_chain = ExpressionsChain._from_dict(
824
+ expr_chain_dict,
825
+ lang_graph
826
+ )
827
+ asset.own_variables[variable_name] = (target_asset, expr_chain)
828
+
829
+ # Recreate the attack steps
830
+ for asset_dict in serialized_graph.values():
831
+ asset = lang_graph.assets[asset_dict['name']]
832
+ logger.debug(
833
+ 'Create attack steps language graph nodes for asset %s',
834
+ asset_dict['name']
835
+ )
836
+ for attack_step_dict in asset_dict['attack_steps'].values():
837
+ attack_step_node = LanguageGraphAttackStep(
838
+ name = attack_step_dict['name'],
839
+ type = attack_step_dict['type'],
840
+ asset = asset,
841
+ ttc = attack_step_dict['ttc'],
842
+ overrides = attack_step_dict['overrides'],
843
+ children = {},
844
+ parents = {},
845
+ info = attack_step_dict['info'],
846
+ tags = set(attack_step_dict['tags'])
847
+ )
848
+ asset.attack_steps[attack_step_dict['name']] = \
849
+ attack_step_node
850
+
851
+ # Relink attack steps based on inheritence
852
+ for asset_dict in serialized_graph.values():
853
+ asset = lang_graph.assets[asset_dict['name']]
854
+ for attack_step_dict in asset_dict['attack_steps'].values():
855
+ if 'inherits' in attack_step_dict and \
856
+ attack_step_dict['inherits'] is not None:
857
+ attack_step = asset.attack_steps[
858
+ attack_step_dict['name']]
859
+ ancestor_asset_name, ancestor_attack_step_name = \
860
+ disaggregate_attack_step_full_name(
861
+ attack_step_dict['inherits'])
862
+ ancestor_asset = lang_graph.assets[ancestor_asset_name]
863
+ ancestor_attack_step = ancestor_asset.attack_steps[\
864
+ ancestor_attack_step_name]
865
+ attack_step.inherits = ancestor_attack_step
866
+
867
+
868
+ # Relink attack steps based on expressions chains
869
+ for asset_dict in serialized_graph.values():
870
+ asset = lang_graph.assets[asset_dict['name']]
871
+ for attack_step_dict in asset_dict['attack_steps'].values():
872
+ attack_step = asset.attack_steps[attack_step_dict['name']]
873
+ for child_target in attack_step_dict['children'].items():
874
+ target_full_attack_step_name = child_target[0]
875
+ expr_chains = child_target[1]
876
+ target_asset_name, target_attack_step_name = \
877
+ disaggregate_attack_step_full_name(
878
+ target_full_attack_step_name)
879
+ target_asset = lang_graph.assets[target_asset_name]
880
+ target_attack_step = target_asset.attack_steps[
881
+ target_attack_step_name]
882
+ for expr_chain_dict in expr_chains:
883
+ expr_chain = ExpressionsChain._from_dict(
884
+ expr_chain_dict,
885
+ lang_graph
886
+ )
887
+ if target_attack_step.full_name in attack_step.children:
888
+ attack_step.children[target_attack_step.full_name].\
889
+ append((target_attack_step, expr_chain))
890
+ else:
891
+ attack_step.children[target_attack_step.full_name] = \
892
+ [(target_attack_step, expr_chain)]
893
+
894
+ for parent_target in attack_step_dict['parents'].items():
895
+ target_full_attack_step_name = parent_target[0]
896
+ expr_chains = parent_target[1]
897
+ target_asset_name, target_attack_step_name = \
898
+ disaggregate_attack_step_full_name(
899
+ target_full_attack_step_name)
900
+ target_asset = lang_graph.assets[target_asset_name]
901
+ target_attack_step = target_asset.attack_steps[
902
+ target_attack_step_name]
903
+ for expr_chain_dict in expr_chains:
904
+ expr_chain = ExpressionsChain._from_dict(
905
+ expr_chain_dict,
906
+ lang_graph
907
+ )
908
+ if target_attack_step.full_name in attack_step.parents:
909
+ attack_step.parents[target_attack_step.full_name].\
910
+ append((target_attack_step, expr_chain))
911
+ else:
912
+ attack_step.parents[target_attack_step.full_name] = \
913
+ [(target_attack_step, expr_chain)]
914
+
915
+ # Recreate the requirements of exist and notExist attack steps
916
+ if attack_step.type == 'exist' or \
917
+ attack_step.type == 'notExist':
918
+ if 'requires' in attack_step_dict:
919
+ expr_chains = attack_step_dict['requires']
920
+ attack_step.own_requires = []
921
+ for expr_chain_dict in expr_chains:
922
+ expr_chain = ExpressionsChain._from_dict(
923
+ expr_chain_dict,
924
+ lang_graph
925
+ )
926
+ attack_step.own_requires.append(expr_chain)
927
+
928
+ return lang_graph
929
+
447
930
 
448
931
  @classmethod
449
932
  def load_from_file(cls, filename: str) -> LanguageGraph:
@@ -453,17 +936,23 @@ class LanguageGraph():
453
936
  lang_graph = cls.from_mal_spec(filename)
454
937
  elif filename.endswith('.mar'):
455
938
  lang_graph = cls.from_mar_archive(filename)
456
- elif filename.endswith(('yaml', 'yml')):
939
+ elif filename.endswith(('.yaml', '.yml')):
457
940
  lang_graph = cls._from_dict(load_dict_from_yaml_file(filename))
458
- elif filename.endswith(('json')):
941
+ elif filename.endswith(('.json')):
459
942
  lang_graph = cls._from_dict(load_dict_from_json_file(filename))
943
+ else:
944
+ raise TypeError(
945
+ "Unknown file extension, expected json/mal/mar/yml/yaml"
946
+ )
460
947
 
461
948
  if lang_graph:
462
949
  return lang_graph
950
+ else:
951
+ raise LanguageGraphException(
952
+ f'Failed to load language graph from file "{filename}".'
953
+ )
954
+
463
955
 
464
- raise TypeError(
465
- "Unknown file extension, expected json/mal/mar/yml/yaml"
466
- )
467
956
 
468
957
  def save_language_specification_to_json(self, filename: str) -> None:
469
958
  """
@@ -477,23 +966,21 @@ class LanguageGraph():
477
966
  with open(filename, 'w', encoding='utf-8') as file:
478
967
  json.dump(self._lang_spec, file, indent=4)
479
968
 
969
+
480
970
  def process_step_expression(self,
481
- lang: dict,
482
971
  target_asset,
483
- dep_chain,
972
+ expr_chain,
484
973
  step_expression: dict
485
974
  ) -> tuple:
486
975
  """
487
976
  Recursively process an attack step expression.
488
977
 
489
978
  Arguments:
490
- lang - A dictionary representing the MAL language
491
- specification.
492
979
  target_asset - The asset type that this step expression should
493
980
  apply to. Initially it will contain the asset
494
981
  type to which the attack step belongs.
495
- dep_chain - A dependency chain of linked of associations and
496
- set operations from the attack step to its
982
+ expr_chain - A expressions chain of linked of associations
983
+ and set operations from the attack step to its
497
984
  parent attack step.
498
985
  Note: This was done for the parent attack step
499
986
  because it was easier to construct recursively
@@ -518,19 +1005,29 @@ class LanguageGraph():
518
1005
  # The attack step expression just adds the name of the attack
519
1006
  # step. All other step expressions only modify the target
520
1007
  # asset and parent associations chain.
521
- return (target_asset,
522
- dep_chain,
523
- step_expression['name'])
1008
+ return (
1009
+ target_asset,
1010
+ None,
1011
+ step_expression['name']
1012
+ )
524
1013
 
525
1014
  case 'union' | 'intersection' | 'difference':
526
1015
  # The set operators are used to combine the left hand and right
527
1016
  # hand targets accordingly.
528
- lh_target_asset, lh_dep_chain, _ = self.process_step_expression(
529
- lang, target_asset, dep_chain, step_expression['lhs'])
530
- rh_target_asset, rh_dep_chain, _ = self.process_step_expression(
531
- lang, target_asset, dep_chain, step_expression['rhs'])
1017
+ lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
1018
+ target_asset,
1019
+ expr_chain,
1020
+ step_expression['lhs']
1021
+ )
1022
+ rh_target_asset, rh_expr_chain, _ = \
1023
+ self.process_step_expression(
1024
+ target_asset,
1025
+ expr_chain,
1026
+ step_expression['rhs']
1027
+ )
532
1028
 
533
- if not lh_target_asset.get_all_common_superassets(rh_target_asset):
1029
+ if not lh_target_asset.get_all_common_superassets(
1030
+ rh_target_asset):
534
1031
  logger.error(
535
1032
  "Set operation attempted between targets that"
536
1033
  " do not share any common superassets: %s and %s!",
@@ -538,30 +1035,32 @@ class LanguageGraph():
538
1035
  )
539
1036
  return (None, None, None)
540
1037
 
541
- new_dep_chain = DependencyChain(
1038
+ new_expr_chain = ExpressionsChain(
542
1039
  type = step_expression['type'],
543
- next_link = None)
544
- new_dep_chain.left_chain = lh_dep_chain
545
- new_dep_chain.right_chain = rh_dep_chain
546
- return (lh_target_asset,
547
- new_dep_chain,
548
- None)
1040
+ left_link = lh_expr_chain,
1041
+ right_link = rh_expr_chain
1042
+ )
1043
+ return (
1044
+ lh_target_asset,
1045
+ new_expr_chain,
1046
+ None
1047
+ )
549
1048
 
550
1049
  case 'variable':
551
- # Fetch the step expression associated with the variable from
552
- # the language specification and resolve that.
553
- variable_step_expr = self._get_variable_for_asset_type_by_name(
554
- target_asset.name, step_expression['name'])
555
- if variable_step_expr:
556
- return self.process_step_expression(
557
- lang,
558
- target_asset,
559
- dep_chain,
560
- variable_step_expr)
561
-
1050
+ var_name = step_expression['name']
1051
+ var_target_asset, var_expr_chain = self._resolve_variable(
1052
+ target_asset, var_name)
1053
+ var_target_asset, var_expr_chain = \
1054
+ target_asset.get_variable(var_name)
1055
+ if var_expr_chain is not None:
1056
+ return (
1057
+ var_target_asset,
1058
+ var_expr_chain,
1059
+ None
1060
+ )
562
1061
  else:
563
1062
  logger.error(
564
- 'Failed to find variable %s for %s',
1063
+ 'Failed to find variable \"%s\" for %s',
565
1064
  step_expression["name"], target_asset.name
566
1065
  )
567
1066
  return (None, None, None)
@@ -578,7 +1077,7 @@ class LanguageGraph():
578
1077
  return (None, None, None)
579
1078
 
580
1079
  new_target_asset = None
581
- for association in target_asset.associations:
1080
+ for association in target_asset.associations.values():
582
1081
  if (association.left_field.fieldname == fieldname and \
583
1082
  target_asset.is_subasset_of(
584
1083
  association.right_field.asset)):
@@ -590,15 +1089,16 @@ class LanguageGraph():
590
1089
  new_target_asset = association.right_field.asset
591
1090
 
592
1091
  if new_target_asset:
593
- new_dep_chain = DependencyChain(
1092
+ new_expr_chain = ExpressionsChain(
594
1093
  type = 'field',
595
- next_link = dep_chain)
596
- new_dep_chain.fieldname = \
597
- association.get_opposite_fieldname(fieldname)
598
- new_dep_chain.association = association
599
- return (new_target_asset,
600
- new_dep_chain,
601
- None)
1094
+ fieldname = fieldname,
1095
+ association = association
1096
+ )
1097
+ return (
1098
+ new_target_asset,
1099
+ new_expr_chain,
1100
+ None
1101
+ )
602
1102
  logger.error(
603
1103
  'Failed to find field "%s" on asset "%s"!',
604
1104
  fieldname, target_asset.name
@@ -609,18 +1109,22 @@ class LanguageGraph():
609
1109
  # Create a transitive tuple entry that applies to the next
610
1110
  # component of the step expression.
611
1111
  result_target_asset, \
612
- result_dep_chain, \
1112
+ result_expr_chain, \
613
1113
  attack_step = \
614
- self.process_step_expression(lang,
1114
+ self.process_step_expression(
615
1115
  target_asset,
616
- dep_chain,
617
- step_expression['stepExpression'])
618
- new_dep_chain = DependencyChain(
1116
+ expr_chain,
1117
+ step_expression['stepExpression']
1118
+ )
1119
+ new_expr_chain = ExpressionsChain(
619
1120
  type = 'transitive',
620
- next_link = result_dep_chain)
621
- return (result_target_asset,
622
- new_dep_chain,
623
- attack_step)
1121
+ sub_link = result_expr_chain
1122
+ )
1123
+ return (
1124
+ result_target_asset,
1125
+ new_expr_chain,
1126
+ attack_step
1127
+ )
624
1128
 
625
1129
  case 'subType':
626
1130
  # Create a subType tuple entry that applies to the next
@@ -628,19 +1132,20 @@ class LanguageGraph():
628
1132
  # asset to the subasset.
629
1133
  subtype_name = step_expression['subType']
630
1134
  result_target_asset, \
631
- result_dep_chain, \
1135
+ result_expr_chain, \
632
1136
  attack_step = \
633
- self.process_step_expression(lang,
1137
+ self.process_step_expression(
634
1138
  target_asset,
635
- dep_chain,
636
- step_expression['stepExpression'])
637
-
638
- subtype_asset = next((asset for asset in self.assets if asset.name == subtype_name), None)
1139
+ expr_chain,
1140
+ step_expression['stepExpression']
1141
+ )
639
1142
 
640
- if not subtype_asset:
641
- msg = 'Failed to find subtype attackstep "{subtype_name}"'
642
- logger.error(msg)
643
- raise LanguageGraphException(msg)
1143
+ if subtype_name in self.assets:
1144
+ subtype_asset = self.assets[subtype_name]
1145
+ else:
1146
+ msg = 'Failed to find subtype %s'
1147
+ logger.error(msg % subtype_name)
1148
+ raise LanguageGraphException(msg % subtype_name)
644
1149
 
645
1150
  if not subtype_asset.is_subasset_of(result_target_asset):
646
1151
  logger.error(
@@ -650,31 +1155,48 @@ class LanguageGraph():
650
1155
  )
651
1156
  return (None, None, None)
652
1157
 
653
- new_dep_chain = DependencyChain(
1158
+ new_expr_chain = ExpressionsChain(
654
1159
  type = 'subType',
655
- next_link = result_dep_chain)
656
- new_dep_chain.subtype = subtype_asset
657
- return (subtype_asset,
658
- new_dep_chain,
659
- attack_step)
1160
+ sub_link = result_expr_chain,
1161
+ subtype = subtype_asset
1162
+ )
1163
+ return (
1164
+ subtype_asset,
1165
+ new_expr_chain,
1166
+ attack_step
1167
+ )
660
1168
 
661
1169
  case 'collect':
662
1170
  # Apply the right hand step expression to left hand step
663
1171
  # expression target asset and parent associations chain.
664
- (lh_target_asset, lh_dep_chain, _) = \
665
- self.process_step_expression(lang,
1172
+ (lh_target_asset, lh_expr_chain, _) = \
1173
+ self.process_step_expression(
666
1174
  target_asset,
667
- dep_chain,
668
- step_expression['lhs'])
1175
+ expr_chain,
1176
+ step_expression['lhs']
1177
+ )
669
1178
  (rh_target_asset,
670
- rh_dep_chain,
1179
+ rh_expr_chain,
671
1180
  rh_attack_step_name) = \
672
- self.process_step_expression(lang,
1181
+ self.process_step_expression(
673
1182
  lh_target_asset,
674
- lh_dep_chain,
675
- step_expression['rhs'])
676
- return (rh_target_asset, rh_dep_chain,
677
- rh_attack_step_name)
1183
+ None,
1184
+ step_expression['rhs']
1185
+ )
1186
+ if rh_expr_chain:
1187
+ new_expr_chain = ExpressionsChain(
1188
+ type = 'collect',
1189
+ left_link = lh_expr_chain,
1190
+ right_link = rh_expr_chain
1191
+ )
1192
+ else:
1193
+ new_expr_chain = lh_expr_chain
1194
+
1195
+ return (
1196
+ rh_target_asset,
1197
+ new_expr_chain,
1198
+ rh_attack_step_name
1199
+ )
678
1200
 
679
1201
  case _:
680
1202
  logger.error(
@@ -682,17 +1204,18 @@ class LanguageGraph():
682
1204
  )
683
1205
  return (None, None, None)
684
1206
 
685
- def reverse_dep_chain(
1207
+
1208
+ def reverse_expr_chain(
686
1209
  self,
687
- dep_chain: Optional[DependencyChain],
688
- reverse_chain: Optional[DependencyChain]
689
- ) -> Optional[DependencyChain]:
1210
+ expr_chain: Optional[ExpressionsChain],
1211
+ reverse_chain: Optional[ExpressionsChain]
1212
+ ) -> Optional[ExpressionsChain]:
690
1213
  """
691
1214
  Recursively reverse the associations chain. From parent to child or
692
1215
  vice versa.
693
1216
 
694
1217
  Arguments:
695
- dep_chain - A chain of nested tuples that specify the
1218
+ expr_chain - A chain of nested tuples that specify the
696
1219
  associations and set operations chain from an
697
1220
  attack step to its connected attack step.
698
1221
  reverse_chain - A chain of nested tuples that represents the
@@ -701,70 +1224,103 @@ class LanguageGraph():
701
1224
  Return:
702
1225
  The resulting reversed associations chain.
703
1226
  """
704
- if not dep_chain:
1227
+ if not expr_chain:
705
1228
  return reverse_chain
706
1229
  else:
707
- match (dep_chain.type):
708
- case 'union' | 'intersection' | 'difference':
1230
+ match (expr_chain.type):
1231
+ case 'union' | 'intersection' | 'difference' | 'collect':
709
1232
  left_reverse_chain = \
710
- self.reverse_dep_chain(dep_chain.left_chain,
1233
+ self.reverse_expr_chain(expr_chain.left_link,
711
1234
  reverse_chain)
712
1235
  right_reverse_chain = \
713
- self.reverse_dep_chain(dep_chain.right_chain,
1236
+ self.reverse_expr_chain(expr_chain.right_link,
714
1237
  reverse_chain)
715
- new_dep_chain = DependencyChain(
716
- type = dep_chain.type,
717
- next_link = None)
718
- new_dep_chain.left_chain = left_reverse_chain
719
- new_dep_chain.right_chain = right_reverse_chain
720
- return new_dep_chain
1238
+ if expr_chain.type == 'collect':
1239
+ new_expr_chain = ExpressionsChain(
1240
+ type = expr_chain.type,
1241
+ left_link = right_reverse_chain,
1242
+ right_link = left_reverse_chain
1243
+ )
1244
+ else:
1245
+ new_expr_chain = ExpressionsChain(
1246
+ type = expr_chain.type,
1247
+ left_link = left_reverse_chain,
1248
+ right_link = right_reverse_chain
1249
+ )
1250
+
1251
+ return new_expr_chain
721
1252
 
722
1253
  case 'transitive':
723
- result_reverse_chain = self.reverse_dep_chain(
724
- dep_chain.next_link, reverse_chain)
725
- new_dep_chain = DependencyChain(
1254
+ result_reverse_chain = self.reverse_expr_chain(
1255
+ expr_chain.sub_link, reverse_chain)
1256
+ new_expr_chain = ExpressionsChain(
726
1257
  type = 'transitive',
727
- next_link = result_reverse_chain)
728
- return new_dep_chain
1258
+ sub_link = result_reverse_chain
1259
+ )
1260
+ return new_expr_chain
729
1261
 
730
1262
  case 'field':
731
- association = dep_chain.association
1263
+ association = expr_chain.association
732
1264
 
733
1265
  if not association:
734
1266
  raise LanguageGraphException(
735
- "Missing association for dep chain"
1267
+ "Missing association for expressions chain"
1268
+ )
1269
+
1270
+ if not expr_chain.fieldname:
1271
+ raise LanguageGraphException(
1272
+ "Missing field name for expressions chain"
736
1273
  )
737
1274
 
738
1275
  opposite_fieldname = association.get_opposite_fieldname(
739
- dep_chain.fieldname)
740
- new_dep_chain = DependencyChain(
1276
+ expr_chain.fieldname)
1277
+ new_expr_chain = ExpressionsChain(
741
1278
  type = 'field',
742
- next_link = reverse_chain
1279
+ association = association,
1280
+ fieldname = opposite_fieldname
743
1281
  )
744
- new_dep_chain.fieldname = opposite_fieldname
745
- new_dep_chain.association = association
746
- return self.reverse_dep_chain(
747
- dep_chain.next_link,
748
- new_dep_chain
749
- )
1282
+ return new_expr_chain
750
1283
 
751
1284
  case 'subType':
752
- result_reverse_chain = self.reverse_dep_chain(
753
- dep_chain.next_link,
1285
+ result_reverse_chain = self.reverse_expr_chain(
1286
+ expr_chain.sub_link,
754
1287
  reverse_chain
755
1288
  )
756
- new_dep_chain = DependencyChain(
1289
+ new_expr_chain = ExpressionsChain(
757
1290
  type = 'subType',
758
- next_link = result_reverse_chain
1291
+ sub_link = result_reverse_chain,
1292
+ subtype = expr_chain.subtype
759
1293
  )
760
- new_dep_chain.subtype = dep_chain.subtype
761
- return new_dep_chain
762
- # return reverse_chain
1294
+ return new_expr_chain
763
1295
 
764
1296
  case _:
765
1297
  msg = 'Unknown assoc chain element "%s"'
766
- logger.error(msg, dep_chain.type)
767
- raise LanguageGraphAssociationError(msg % dep_chain.type)
1298
+ logger.error(msg, expr_chain.type)
1299
+ raise LanguageGraphAssociationError(msg % expr_chain.type)
1300
+
1301
+ def _resolve_variable(self, asset, var_name) -> tuple:
1302
+ """
1303
+ Resolve a variable for a specific asset by variable name.
1304
+
1305
+ Arguments:
1306
+ asset - a language graph asset to which the variable belongs
1307
+ var_name - a string representing the variable name
1308
+
1309
+ Return:
1310
+ A tuple containing the target asset and expressions chain required to
1311
+ reach it.
1312
+ """
1313
+ if var_name not in asset.variables:
1314
+ var_expr = self._get_var_expr_for_asset(asset.name, var_name)
1315
+ target_asset, expr_chain, _ = self.process_step_expression(
1316
+ asset,
1317
+ None,
1318
+ var_expr
1319
+ )
1320
+ asset.own_variables[var_name] = (target_asset, expr_chain)
1321
+ return (target_asset, expr_chain)
1322
+ return asset.variables[var_name]
1323
+
768
1324
 
769
1325
  def _generate_graph(self) -> None:
770
1326
  """
@@ -772,41 +1328,40 @@ class LanguageGraph():
772
1328
  given in the constructor.
773
1329
  """
774
1330
  # Generate all of the asset nodes of the language graph.
775
- for asset in self._lang_spec['assets']:
1331
+ for asset_dict in self._lang_spec['assets']:
776
1332
  logger.debug(
777
1333
  'Create asset language graph nodes for asset %s',
778
- asset["name"]
1334
+ asset_dict['name']
779
1335
  )
780
1336
  asset_node = LanguageGraphAsset(
781
- name = asset['name'],
782
- associations = [],
783
- attack_steps = [],
784
- description = asset['meta'],
785
- super_assets = [],
786
- sub_assets = [],
787
- is_abstract = asset['isAbstract']
1337
+ name = asset_dict['name'],
1338
+ own_associations = {},
1339
+ attack_steps = {},
1340
+ info = asset_dict['meta'],
1341
+ own_super_asset = None,
1342
+ own_sub_assets = set(),
1343
+ own_variables = {},
1344
+ is_abstract = asset_dict['isAbstract']
788
1345
  )
789
- self.assets.append(asset_node)
1346
+ self.assets[asset_dict['name']] = asset_node
790
1347
 
791
1348
  # Link assets based on inheritance
792
- for asset_info in self._lang_spec['assets']:
793
- asset = next((asset for asset in self.assets \
794
- if asset.name == asset_info['name']), None)
795
- if asset_info['superAsset']:
796
- super_asset = next((asset for asset in self.assets \
797
- if asset.name == asset_info['superAsset']), None)
1349
+ for asset_dict in self._lang_spec['assets']:
1350
+ asset = self.assets[asset_dict['name']]
1351
+ if asset_dict['superAsset']:
1352
+ super_asset = self.assets[asset_dict['superAsset']]
798
1353
  if not super_asset:
799
1354
  msg = 'Failed to find super asset "%s" for asset "%s"!'
800
1355
  logger.error(
801
- msg, asset_info["superAsset"], asset_info["name"])
1356
+ msg, asset_dict["superAsset"], asset_dict["name"])
802
1357
  raise LanguageGraphSuperAssetNotFoundError(
803
- msg % (asset_info["superAsset"], asset_info["name"]))
1358
+ msg % (asset_dict["superAsset"], asset_dict["name"]))
804
1359
 
805
- super_asset.sub_assets.append(asset)
806
- asset.super_assets.append(super_asset)
1360
+ super_asset.own_sub_assets.add(asset)
1361
+ asset.own_super_asset = super_asset
807
1362
 
808
1363
  # Generate all of the association nodes of the language graph.
809
- for asset in self.assets:
1364
+ for asset in self.assets.values():
810
1365
  logger.debug(
811
1366
  'Create association language graph nodes for asset %s',
812
1367
  asset.name
@@ -814,8 +1369,7 @@ class LanguageGraph():
814
1369
 
815
1370
  associations = self._get_associations_for_asset_type(asset.name)
816
1371
  for association in associations:
817
- left_asset = next((asset for asset in self.assets \
818
- if asset.name == association['leftAsset']), None)
1372
+ left_asset = self.assets[association['leftAsset']]
819
1373
  if not left_asset:
820
1374
  msg = 'Left asset "%s" for association "%s" not found!'
821
1375
  logger.error(
@@ -823,8 +1377,7 @@ class LanguageGraph():
823
1377
  raise LanguageGraphAssociationError(
824
1378
  msg % (association["leftAsset"], association["name"]))
825
1379
 
826
- right_asset = next((asset for asset in self.assets \
827
- if asset.name == association['rightAsset']), None)
1380
+ right_asset = self.assets[association['rightAsset']]
828
1381
  if not right_asset:
829
1382
  msg = 'Right asset "%s" for association "%s" not found!'
830
1383
  logger.error(
@@ -833,17 +1386,6 @@ class LanguageGraph():
833
1386
  msg % (association["rightAsset"], association["name"])
834
1387
  )
835
1388
 
836
- # Technically we should be more exhaustive and check the
837
- # flipped version too and all of the fieldnames as well.
838
- assoc_node = next((assoc for assoc in self.associations \
839
- if assoc.name == association['name'] and
840
- assoc.left_field.asset == left_asset and
841
- assoc.right_field.asset == right_asset),
842
- None)
843
- if assoc_node:
844
- # The association was already created, skip it
845
- continue
846
-
847
1389
  assoc_node = LanguageGraphAssociation(
848
1390
  name = association['name'],
849
1391
  left_field = LanguageGraphAssociationField(
@@ -856,124 +1398,224 @@ class LanguageGraph():
856
1398
  association['rightField'],
857
1399
  association['rightMultiplicity']['min'],
858
1400
  association['rightMultiplicity']['max']),
859
- description = association['meta']
1401
+ info = association['meta']
860
1402
  )
861
1403
 
862
- # Add the association to the left and right asset and all of
863
- # the assets that inherit them
864
- associated_assets = [left_asset, right_asset]
865
- while associated_assets != []:
866
- asset = associated_assets.pop()
867
- associated_assets.extend(asset.sub_assets)
868
- if assoc_node not in asset.associations:
869
- asset.associations.append(assoc_node)
1404
+ # Add the association to the left and right asset
1405
+ self._link_association_to_assets(assoc_node,
1406
+ left_asset, right_asset)
1407
+
1408
+ # Set the variables
1409
+ for asset in self.assets.values():
1410
+ for variable in self._get_variables_for_asset_type(asset.name):
1411
+ if logger.isEnabledFor(logging.DEBUG):
1412
+ # Avoid running json.dumps when not in debug
1413
+ logger.debug(
1414
+ 'Processing Variable Expression:\n%s',
1415
+ json.dumps(variable, indent = 2)
1416
+ )
1417
+ self._resolve_variable(asset, variable['name'])
870
1418
 
871
- self.associations.append(assoc_node)
872
1419
 
873
1420
  # Generate all of the attack step nodes of the language graph.
874
- for asset in self.assets:
1421
+ for asset in self.assets.values():
875
1422
  logger.debug(
876
1423
  'Create attack steps language graph nodes for asset %s',
877
1424
  asset.name
878
1425
  )
879
1426
  attack_steps = self._get_attacks_for_asset_type(asset.name)
880
- for attack_step_name, attack_step_attribs in attack_steps.items():
1427
+ for attack_step_attribs in attack_steps.values():
881
1428
  logger.debug(
882
1429
  'Create attack step language graph nodes for %s',
883
- attack_step_name
1430
+ attack_step_attribs['name']
884
1431
  )
885
1432
 
886
1433
  attack_step_node = LanguageGraphAttackStep(
887
- name = attack_step_name,
1434
+ name = attack_step_attribs['name'],
888
1435
  type = attack_step_attribs['type'],
889
1436
  asset = asset,
890
1437
  ttc = attack_step_attribs['ttc'],
1438
+ overrides = attack_step_attribs['reaches']['overrides'] \
1439
+ if attack_step_attribs['reaches'] else False,
891
1440
  children = {},
892
1441
  parents = {},
893
- description = attack_step_attribs['meta']
1442
+ info = attack_step_attribs['meta'],
1443
+ tags = set(attack_step_attribs['tags'])
894
1444
  )
895
- attack_step_node.attributes = attack_step_attribs
896
- asset.attack_steps.append(attack_step_node)
897
- self.attack_steps.append(attack_step_node)
898
-
899
- # Then, link all of the attack step nodes according to their associations.
900
- for attack_step in self.attack_steps:
901
- logger.debug(
902
- 'Determining children for attack step %s',
903
- attack_step.name
904
- )
905
- step_expressions = \
906
- attack_step.attributes['reaches']['stepExpressions'] if \
907
- attack_step.attributes['reaches'] else []
908
-
909
- for step_expression in step_expressions:
910
- # Resolve each of the attack step expressions listed for this
911
- # attack step to determine children.
912
- (target_asset, dep_chain, attack_step_name) = \
913
- self.process_step_expression(self._lang_spec,
914
- attack_step.asset,
915
- None,
916
- step_expression)
917
- if not target_asset:
918
- msg = 'Failed to find target asset to link with for ' \
919
- 'step expression:\n%s'
920
- raise LanguageGraphStepExpressionError(
921
- msg % json.dumps(step_expression, indent = 2)
1445
+ attack_step_node._attributes = attack_step_attribs
1446
+ asset.attack_steps[attack_step_attribs['name']] = \
1447
+ attack_step_node
1448
+
1449
+ for detector in attack_step_attribs.get("detectors",
1450
+ {}).values():
1451
+ attack_step_node.detectors[detector["name"]] = Detector(
1452
+ context=Context(
1453
+ {
1454
+ label: self.assets[asset]
1455
+ for label, asset in detector["context"].items()
1456
+ }
1457
+ ),
1458
+ name=detector.get("name"),
1459
+ type=detector.get("type"),
1460
+ tprate=detector.get("tprate"),
922
1461
  )
923
1462
 
924
- target_attack_step = next((attack_step \
925
- for attack_step in target_asset.attack_steps \
926
- if attack_step.name == attack_step_name), None)
927
-
928
- if not target_attack_step:
929
- msg = 'Failed to find target attack step %s on %s to ' \
930
- 'link with for step expression:\n%s'
931
- raise LanguageGraphStepExpressionError(
932
- msg % (
933
- attack_step_name,
934
- target_asset.name,
935
- json.dumps(step_expression, indent = 2)
1463
+ # Create the inherited attack steps
1464
+ assets = list(self.assets.values())
1465
+ while len(assets) > 0:
1466
+ asset = assets.pop(0)
1467
+ if asset.own_super_asset in assets:
1468
+ # The asset still has super assets that should be resolved
1469
+ # first, moved it to the back.
1470
+ assets.append(asset)
1471
+ else:
1472
+ if asset.own_super_asset:
1473
+ for attack_step in \
1474
+ asset.own_super_asset.attack_steps.values():
1475
+ if attack_step.name not in asset.attack_steps:
1476
+ attack_step_node = LanguageGraphAttackStep(
1477
+ name = attack_step.name,
1478
+ type = attack_step.type,
1479
+ asset = asset,
1480
+ ttc = attack_step.ttc,
1481
+ overrides = False,
1482
+ children = {},
1483
+ parents = {},
1484
+ info = attack_step.info,
1485
+ tags = set(attack_step.tags)
1486
+ )
1487
+ attack_step_node.inherits = attack_step
1488
+ asset.attack_steps[attack_step.name] = attack_step_node
1489
+ elif asset.attack_steps[attack_step.name].overrides:
1490
+ # The inherited attack step was already overridden.
1491
+ continue
1492
+ else:
1493
+ asset.attack_steps[attack_step.name].inherits = \
1494
+ attack_step
1495
+ asset.attack_steps[attack_step.name].tags |= \
1496
+ attack_step.tags
1497
+ asset.attack_steps[attack_step.name].info |= \
1498
+ attack_step.info
1499
+
1500
+ # Then, link all of the attack step nodes according to their
1501
+ # associations.
1502
+ for asset in self.assets.values():
1503
+ for attack_step in asset.attack_steps.values():
1504
+ logger.debug(
1505
+ 'Determining children for attack step %s',
1506
+ attack_step.name
1507
+ )
1508
+
1509
+ if attack_step._attributes is None:
1510
+ # This is simply an empty inherited attack step
1511
+ continue
1512
+
1513
+ step_expressions = \
1514
+ attack_step._attributes['reaches']['stepExpressions'] if \
1515
+ attack_step._attributes['reaches'] else []
1516
+
1517
+ for step_expression in step_expressions:
1518
+ # Resolve each of the attack step expressions listed for
1519
+ # this attack step to determine children.
1520
+ (target_asset, expr_chain, target_attack_step_name) = \
1521
+ self.process_step_expression(
1522
+ attack_step.asset,
1523
+ None,
1524
+ step_expression
1525
+ )
1526
+ if not target_asset:
1527
+ msg = 'Failed to find target asset to link with for ' \
1528
+ 'step expression:\n%s'
1529
+ raise LanguageGraphStepExpressionError(
1530
+ msg % json.dumps(step_expression, indent = 2)
936
1531
  )
937
- )
938
1532
 
939
- # It is easier to create the parent associations chain due to
940
- # the left-hand first progression.
941
- if attack_step.name in target_attack_step.parents:
942
- target_attack_step.parents[attack_step.name].append(
943
- (attack_step, dep_chain))
944
- else:
945
- target_attack_step.parents[attack_step.name] = \
946
- [(attack_step, dep_chain)]
947
- # Reverse the parent associations chain to get the child
948
- # associations chain.
949
- if target_attack_step.name in attack_step.children:
950
- attack_step.children[target_attack_step.name].append(
951
- (target_attack_step,
952
- self.reverse_dep_chain(dep_chain,
953
- None)))
954
- else:
955
- attack_step.children[target_attack_step.name] = \
956
- [(target_attack_step,
957
- self.reverse_dep_chain(dep_chain,
958
- None))]
1533
+ target_asset_attack_steps = target_asset.attack_steps
1534
+ if target_attack_step_name not in \
1535
+ target_asset_attack_steps:
1536
+ msg = 'Failed to find target attack step %s on %s to ' \
1537
+ 'link with for step expression:\n%s'
1538
+ raise LanguageGraphStepExpressionError(
1539
+ msg % (
1540
+ target_attack_step_name,
1541
+ target_asset.name,
1542
+ json.dumps(step_expression, indent = 2)
1543
+ )
1544
+ )
1545
+
1546
+ target_attack_step = target_asset_attack_steps[
1547
+ target_attack_step_name]
1548
+
1549
+ # Link to the children target attack steps
1550
+ if target_attack_step.full_name in attack_step.children:
1551
+ attack_step.children[target_attack_step.full_name].\
1552
+ append((target_attack_step, expr_chain))
1553
+ else:
1554
+ attack_step.children[target_attack_step.full_name] = \
1555
+ [(target_attack_step, expr_chain)]
1556
+ # Reverse the children associations chains to get the
1557
+ # parents associations chain.
1558
+ if attack_step.full_name in target_attack_step.parents:
1559
+ target_attack_step.parents[attack_step.full_name].\
1560
+ append((attack_step,
1561
+ self.reverse_expr_chain(expr_chain,
1562
+ None)))
1563
+ else:
1564
+ target_attack_step.parents[attack_step.full_name] = \
1565
+ [(attack_step,
1566
+ self.reverse_expr_chain(expr_chain,
1567
+ None))]
1568
+
1569
+ # Evaluate the requirements of exist and notExist attack steps
1570
+ if attack_step.type == 'exist' or \
1571
+ attack_step.type == 'notExist':
1572
+ step_expressions = \
1573
+ attack_step._attributes['requires']['stepExpressions'] \
1574
+ if attack_step._attributes['requires'] else []
1575
+ if not step_expressions:
1576
+ msg = 'Failed to find requirements for attack step' \
1577
+ ' "%s" of type "%s":\n%s'
1578
+ raise LanguageGraphStepExpressionError(
1579
+ msg % (
1580
+ attack_step.name,
1581
+ attack_step.type,
1582
+ json.dumps(attack_step._attributes, indent = 2)
1583
+ )
1584
+ )
1585
+
1586
+ attack_step.own_requires = []
1587
+ for step_expression in step_expressions:
1588
+ _, \
1589
+ result_expr_chain, \
1590
+ _ = \
1591
+ self.process_step_expression(
1592
+ attack_step.asset,
1593
+ None,
1594
+ step_expression
1595
+ )
1596
+ attack_step.own_requires.append(result_expr_chain)
959
1597
 
960
1598
  def _get_attacks_for_asset_type(self, asset_type: str) -> dict:
961
1599
  """
962
1600
  Get all Attack Steps for a specific Class
963
1601
 
964
1602
  Arguments:
965
- asset_type - a string representing the class for which we want to list
966
- the possible attack steps
1603
+ asset_type - a string representing the class for which we want to
1604
+ list the possible attack steps
967
1605
 
968
1606
  Return:
969
- A dictionary representing the set of possible attacks for the specified
970
- class. Each key in the dictionary is an attack name and is associated
971
- with a dictionary containing other characteristics of the attack such as
972
- type of attack, TTC distribution, child attack steps and other information
1607
+ A dictionary representing the set of possible attacks for the
1608
+ specified class. Each key in the dictionary is an attack name and is
1609
+ associated with a dictionary containing other characteristics of the
1610
+ attack such as type of attack, TTC distribution, child attack steps
1611
+ and other information
973
1612
  """
974
1613
  attack_steps: dict = {}
975
1614
  try:
976
- asset = next((asset for asset in self._lang_spec['assets'] if asset['name'] == asset_type))
1615
+ asset = next(
1616
+ asset for asset in self._lang_spec['assets'] \
1617
+ if asset['name'] == asset_type
1618
+ )
977
1619
  except StopIteration:
978
1620
  logger.error(
979
1621
  'Failed to find asset type %s when looking'
@@ -983,33 +1625,10 @@ class LanguageGraph():
983
1625
 
984
1626
  logger.debug(
985
1627
  'Get attack steps for %s asset from '
986
- 'language specification.', asset["name"]
1628
+ 'language specification.', asset['name']
987
1629
  )
988
- if asset['superAsset']:
989
- logger.debug('Asset extends another one, fetch the superclass '\
990
- 'attack steps for it.')
991
- attack_steps = self._get_attacks_for_asset_type(asset['superAsset'])
992
-
993
- for step in asset['attackSteps']:
994
- if step['name'] not in attack_steps:
995
- attack_steps[step['name']] = copy.deepcopy(step)
996
- elif not step['reaches']:
997
- # This attack step does not lead to any attack steps
998
- continue
999
- elif step['reaches']['overrides'] == True:
1000
- attack_steps[step['name']] = copy.deepcopy(step)
1001
- else:
1002
- if attack_steps[step['name']]['reaches'] is not None and \
1003
- 'stepExpressions' in \
1004
- attack_steps[step['name']]['reaches']:
1005
- attack_steps[step['name']]['reaches']['stepExpressions'].\
1006
- extend(step['reaches']['stepExpressions'])
1007
- else:
1008
- attack_steps[step['name']]['reaches'] = {
1009
- 'overrides': False,
1010
- 'stepExpressions': step['reaches']['stepExpressions']
1011
- }
1012
1630
 
1631
+ attack_steps = {step['name']: step for step in asset['attackSteps']}
1013
1632
 
1014
1633
  return attack_steps
1015
1634
 
@@ -1018,14 +1637,15 @@ class LanguageGraph():
1018
1637
  Get all Associations for a specific Class
1019
1638
 
1020
1639
  Arguments:
1021
- asset_type - a string representing the class for which we want to list
1022
- the associations
1640
+ asset_type - a string representing the class for which we want to
1641
+ list the associations
1023
1642
 
1024
1643
  Return:
1025
1644
  A dictionary representing the set of associations for the specified
1026
1645
  class. Each key in the dictionary is an attack name and is associated
1027
- with a dictionary containing other characteristics of the attack such as
1028
- type of attack, TTC distribution, child attack steps and other information
1646
+ with a dictionary containing other characteristics of the attack such
1647
+ as type of attack, TTC distribution, child attack steps and other
1648
+ information
1029
1649
  """
1030
1650
  logger.debug(
1031
1651
  'Get associations for %s asset from '
@@ -1033,8 +1653,8 @@ class LanguageGraph():
1033
1653
  )
1034
1654
  associations: list = []
1035
1655
 
1036
- asset = next((asset for asset in self._lang_spec['assets'] if asset['name'] == \
1037
- asset_type), None)
1656
+ asset = next((asset for asset in self._lang_spec['assets'] \
1657
+ if asset['name'] == asset_type), None)
1038
1658
  if not asset:
1039
1659
  logger.error(
1040
1660
  'Failed to find asset type %s when '
@@ -1042,10 +1662,6 @@ class LanguageGraph():
1042
1662
  )
1043
1663
  return associations
1044
1664
 
1045
- if asset['superAsset']:
1046
- logger.debug('Asset extends another one, fetch the superclass '\
1047
- 'associations for it.')
1048
- associations.extend(self._get_associations_for_asset_type(asset['superAsset']))
1049
1665
  assoc_iter = (assoc for assoc in self._lang_spec['associations'] \
1050
1666
  if assoc['leftAsset'] == asset_type or \
1051
1667
  assoc['rightAsset'] == asset_type)
@@ -1056,129 +1672,64 @@ class LanguageGraph():
1056
1672
 
1057
1673
  return associations
1058
1674
 
1059
- def _get_variable_for_asset_type_by_name(
1060
- self, asset_type: str, variable_name: str) -> dict:
1675
+ def _get_variables_for_asset_type(
1676
+ self, asset_type: str) -> dict:
1061
1677
  """
1062
1678
  Get a variables for a specific asset type by name.
1063
- NOTE: Variables are the ones specified in MAL through `let` statements
1679
+ Note: Variables are the ones specified in MAL through `let` statements
1064
1680
 
1065
1681
  Arguments:
1066
1682
  asset_type - a string representing the type of asset which
1067
- contains the variable
1068
- variable_name - the name of the variable to search for
1683
+ contains the variables
1069
1684
 
1070
1685
  Return:
1071
- A dictionary representing the step expressions for the specified variable.
1686
+ A dictionary representing the step expressions for the variables
1687
+ belonging to the asset.
1072
1688
  """
1073
1689
 
1074
- asset = next((asset for asset in self._lang_spec['assets'] if asset['name'] == \
1075
- asset_type), None)
1076
- if not asset:
1077
- msg = 'Failed to find asset type %s when looking for variable.'
1690
+ asset_dict = next((asset for asset in self._lang_spec['assets'] \
1691
+ if asset['name'] == asset_type), None)
1692
+ if not asset_dict:
1693
+ msg = 'Failed to find asset type %s in language specification '\
1694
+ 'when looking for variables.'
1078
1695
  logger.error(msg, asset_type)
1079
1696
  raise LanguageGraphException(msg % asset_type)
1080
1697
 
1081
- variable_dict = next((variable for variable in \
1082
- asset['variables'] if variable['name'] == variable_name), None)
1083
- if not variable_dict:
1084
- if asset['superAsset']:
1085
- variable_dict = self._get_variable_for_asset_type_by_name(asset['superAsset'],
1086
- variable_name)
1087
- if variable_dict:
1088
- return variable_dict
1089
- else:
1090
- msg = 'Failed to find variable %s in %s lang specification.'
1091
- logger.error(msg, variable_name, asset_type)
1092
- raise LanguageGraphException(
1093
- msg % (variable_name, asset_type))
1094
-
1095
- return variable_dict['stepExpression']
1698
+ return asset_dict['variables']
1096
1699
 
1097
- def regenerate_graph(self) -> None:
1098
- """
1099
- Regenerate language graph starting from the MAL language specification
1100
- given in the constructor.
1101
- """
1102
-
1103
- self.assets = []
1104
- self.associations = []
1105
- self.attack_steps = []
1106
- self._generate_graph()
1107
-
1108
- def get_asset_by_name(
1109
- self,
1110
- asset_name
1111
- ) -> Optional[LanguageGraphAsset]:
1700
+ def _get_var_expr_for_asset(
1701
+ self, asset_type: str, var_name) -> dict:
1112
1702
  """
1113
- Get an asset based on its name
1703
+ Get a variable for a specific asset type by variable name.
1114
1704
 
1115
1705
  Arguments:
1116
- asset_name - a string containing the asset name
1706
+ asset_type - a string representing the type of asset which
1707
+ contains the variable
1708
+ var_name - a string representing the variable name
1117
1709
 
1118
1710
  Return:
1119
- The asset matching the name.
1120
- None if there is no match.
1711
+ A dictionary representing the step expression for the variable.
1121
1712
  """
1122
- for asset in self.assets:
1123
- if asset.name == asset_name:
1124
- return asset
1125
1713
 
1126
- return None
1714
+ vars_dict = self._get_variables_for_asset_type(asset_type)
1127
1715
 
1128
- def get_association_by_fields_and_assets(
1129
- self,
1130
- first_field: str,
1131
- second_field: str,
1132
- first_asset_name: str,
1133
- second_asset_name: str
1134
- ) -> Optional[LanguageGraphAssociation]:
1135
- """
1136
- Get an association based on its field names and asset types
1716
+ var_expr = next((var_entry['stepExpression'] for var_entry \
1717
+ in vars_dict if var_entry['name'] == var_name), None)
1137
1718
 
1138
- Arguments:
1139
- first_field - a string containing the first field
1140
- second_field - a string containing the second field
1141
- first_asset_name - a string representing the first asset type
1142
- second_asset_name - a string representing the second asset type
1719
+ if not var_expr:
1720
+ msg = 'Failed to find variable name "%s" in language '\
1721
+ 'specification when looking for variables for "%s" asset.'
1722
+ logger.error(msg, var_name, asset_type)
1723
+ raise LanguageGraphException(msg % (var_name, asset_type))
1724
+ return var_expr
1143
1725
 
1144
- Return:
1145
- The association matching the fieldnames and asset types.
1146
- None if there is no match.
1726
+ def regenerate_graph(self) -> None:
1727
+ """
1728
+ Regenerate language graph starting from the MAL language specification
1729
+ given in the constructor.
1147
1730
  """
1148
- first_asset = self.get_asset_by_name(first_asset_name)
1149
- if first_asset is None:
1150
- raise LookupError(
1151
- f'Failed to find asset with name \"{first_asset_name}\" in '
1152
- 'the language graph.'
1153
- )
1154
-
1155
- second_asset = self.get_asset_by_name(second_asset_name)
1156
- if second_asset is None:
1157
- raise LookupError(
1158
- f'Failed to find asset with name \"{second_asset_name}\" in '
1159
- 'the language graph.'
1160
- )
1161
-
1162
- for assoc in self.associations:
1163
- logger.debug(
1164
- 'Compare ("%s", "%s", "%s", "%s") to ("%s", "%s", "%s", "%s").',
1165
- first_asset_name, first_field,
1166
- second_asset_name, second_field,
1167
- assoc.left_field.asset.name, assoc.left_field.fieldname,
1168
- assoc.right_field.asset.name, assoc.right_field.fieldname
1169
- )
1170
1731
 
1171
- # If the asset and fields match either way we accept it as a match.
1172
- if assoc.left_field.fieldname == first_field and \
1173
- assoc.right_field.fieldname == second_field and \
1174
- first_asset.is_subasset_of(assoc.left_field.asset) and \
1175
- second_asset.is_subasset_of(assoc.right_field.asset):
1176
- return assoc
1732
+ self.assets = {}
1733
+ self._generate_graph()
1177
1734
 
1178
- if assoc.left_field.fieldname == second_field and \
1179
- assoc.right_field.fieldname == first_field and \
1180
- second_asset.is_subasset_of(assoc.left_field.asset) and \
1181
- first_asset.is_subasset_of(assoc.right_field.asset):
1182
- return assoc
1183
1735
 
1184
- return None