mal-toolbox 0.3.11__py3-none-any.whl → 1.0.1__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.
@@ -26,6 +26,131 @@ from ..exceptions import (
26
26
 
27
27
  logger = logging.getLogger(__name__)
28
28
 
29
+ predef_ttcs: dict[str, dict] = {
30
+ 'EasyAndUncertain':
31
+ {
32
+ 'arguments': [0.5],
33
+ 'name': 'Bernoulli',
34
+ 'type': 'function'
35
+ },
36
+ 'HardAndUncertain':
37
+ {
38
+ 'lhs':
39
+ {
40
+ 'arguments': [0.1],
41
+ 'name': 'Exponential',
42
+ 'type': 'function'
43
+ },
44
+ 'rhs':
45
+ {
46
+ 'arguments': [0.5],
47
+ 'name': 'Bernoulli',
48
+ 'type': 'function'
49
+ },
50
+ 'type': 'multiplication'
51
+ },
52
+ 'VeryHardAndUncertain':
53
+ {
54
+ 'lhs':
55
+ {
56
+ 'arguments': [0.01],
57
+ 'name': 'Exponential',
58
+ 'type': 'function'
59
+ },
60
+ 'rhs':
61
+ {
62
+ 'arguments': [0.5],
63
+ 'name': 'Bernoulli',
64
+ 'type': 'function'
65
+ },
66
+ 'type': 'multiplication'
67
+ },
68
+ 'EasyAndCertain':
69
+ {
70
+ 'arguments': [1.0],
71
+ 'name': 'Exponential',
72
+ 'type': 'function'
73
+ },
74
+ 'HardAndCertain':
75
+ {
76
+ 'arguments': [0.1],
77
+ 'name': 'Exponential',
78
+ 'type': 'function'
79
+ },
80
+ 'VeryHardAndCertain':
81
+ {
82
+ 'arguments': [0.01],
83
+ 'name': 'Exponential',
84
+ 'type': 'function'
85
+ },
86
+ 'Enabled':
87
+ {
88
+ 'arguments': [1.0],
89
+ 'name': 'Bernoulli',
90
+ 'type': 'function'
91
+ },
92
+ 'Instant':
93
+ {
94
+ 'arguments': [1.0],
95
+ 'name': 'Bernoulli',
96
+ 'type': 'function'
97
+ },
98
+ 'Disabled':
99
+ {
100
+ 'arguments': [0.0],
101
+ 'name': 'Bernoulli',
102
+ 'type': 'function'
103
+ },
104
+ }
105
+
106
+ def get_ttc_distribution(
107
+ step_dict: dict,
108
+ defense_default_ttc = None,
109
+ attack_default_ttc = None
110
+ ) -> Optional[dict]:
111
+ """Convert step TTC to a TTC distribution if needed
112
+
113
+ - If no TTC is set, set return default TTC.
114
+ - If the TTC provided is a predefined name replace it with the
115
+ probability distribution it corresponds to.
116
+ - Otherwise return the TTC distribution as is.
117
+
118
+ Arguments:
119
+ step_dict - A dict with the attack step data
120
+ defense_default_ttc - the value to give a defense ttc if none is set
121
+ attack_default_ttc - the value to give an attack ttc if none is set
122
+
123
+ Returns:
124
+ A dict with the steps TTC distribution, or None if the step is not
125
+ a defense or attack step
126
+ """
127
+
128
+ if defense_default_ttc is None:
129
+ defense_default_ttc = predef_ttcs['Disabled'].copy()
130
+ if attack_default_ttc is None:
131
+ attack_default_ttc = predef_ttcs['Instant'].copy()
132
+
133
+ step_ttc = step_dict['ttc']
134
+
135
+ if step_dict['type'] == 'defense':
136
+ if step_ttc is None:
137
+ # No step ttc set in language for defense
138
+ step_ttc = defense_default_ttc
139
+ elif step_dict['type'] in ('or', 'and'):
140
+ if step_ttc is None:
141
+ # No step ttc set in language for attack
142
+ step_ttc = attack_default_ttc
143
+ else:
144
+ # No TTC for other step types
145
+ return None
146
+
147
+ if 'name' in step_ttc and step_ttc['name'] in predef_ttcs:
148
+ # Predefined step ttc set in language, fetch from dict
149
+ step_ttc = predef_ttcs[step_ttc['name']].copy()
150
+
151
+ return step_ttc
152
+
153
+
29
154
 
30
155
  def disaggregate_attack_step_full_name(
31
156
  attack_step_full_name: str) -> list[str]:
@@ -49,6 +174,7 @@ class Detector:
49
174
 
50
175
 
51
176
  class Context(dict):
177
+ """Context is part of detectors to provide meta data about attackers"""
52
178
  def __init__(self, context) -> None:
53
179
  super().__init__(context)
54
180
  self._context_dict = context
@@ -66,6 +192,7 @@ class Context(dict):
66
192
 
67
193
  @dataclass
68
194
  class LanguageGraphAsset:
195
+ """An asset type as defined in the MAL language"""
69
196
  name: str
70
197
  own_associations: dict[str, LanguageGraphAssociation] = \
71
198
  field(default_factory = dict)
@@ -127,7 +254,7 @@ class LanguageGraphAsset:
127
254
  False otherwise.
128
255
  """
129
256
  current_asset: Optional[LanguageGraphAsset] = self
130
- while (current_asset):
257
+ while current_asset:
131
258
  if current_asset == target_asset:
132
259
  return True
133
260
  current_asset = current_asset.own_super_asset
@@ -164,7 +291,7 @@ class LanguageGraphAsset:
164
291
  """
165
292
  current_asset: Optional[LanguageGraphAsset] = self
166
293
  superassets = []
167
- while (current_asset):
294
+ while current_asset:
168
295
  superassets.append(current_asset)
169
296
  current_asset = current_asset.own_super_asset
170
297
  return superassets
@@ -188,7 +315,9 @@ class LanguageGraphAsset:
188
315
 
189
316
 
190
317
  @property
191
- def variables(self) -> dict[str, ExpressionsChain]:
318
+ def variables(
319
+ self
320
+ ) -> dict[str, tuple[LanguageGraphAsset, ExpressionsChain]]:
192
321
  """
193
322
  Return a list of all of the variables that belong to this asset
194
323
  directly or indirectly via inheritance.
@@ -204,27 +333,6 @@ class LanguageGraphAsset:
204
333
  return all_vars
205
334
 
206
335
 
207
- def get_variable(
208
- self,
209
- var_name: str,
210
- ) -> Optional[tuple]:
211
- """
212
- Return a variable matching the given name if the asset or any of its
213
- super assets has its definition.
214
-
215
- Return:
216
- A tuple containing the target asset and expressions chain to it if the
217
- variable was defined.
218
- None otherwise.
219
- """
220
- current_asset: Optional[LanguageGraphAsset] = self
221
- while (current_asset):
222
- if var_name in current_asset.own_variables:
223
- return current_asset.own_variables[var_name]
224
- current_asset = current_asset.own_super_asset
225
- return None
226
-
227
-
228
336
  def get_all_common_superassets(
229
337
  self, other: LanguageGraphAsset
230
338
  ) -> set[str]:
@@ -241,6 +349,7 @@ class LanguageGraphAsset:
241
349
 
242
350
  @dataclass
243
351
  class LanguageGraphAssociationField:
352
+ """A field in an association"""
244
353
  asset: LanguageGraphAsset
245
354
  fieldname: str
246
355
  minimum: int
@@ -249,6 +358,9 @@ class LanguageGraphAssociationField:
249
358
 
250
359
  @dataclass
251
360
  class LanguageGraphAssociation:
361
+ """
362
+ An association type between asset types as defined in the MAL language
363
+ """
252
364
  name: str
253
365
  left_field: LanguageGraphAssociationField
254
366
  right_field: LanguageGraphAssociationField
@@ -277,9 +389,11 @@ class LanguageGraphAssociation:
277
389
 
278
390
 
279
391
  def __repr__(self) -> str:
280
- return (f'LanguageGraphAssociation(name: "{self.name}", '
392
+ return (
393
+ f'LanguageGraphAssociation(name: "{self.name}", '
281
394
  f'left_field: {self.left_field}, '
282
- f'right_field: {self.right_field})')
395
+ f'right_field: {self.right_field})'
396
+ )
283
397
 
284
398
 
285
399
  @property
@@ -293,7 +407,7 @@ class LanguageGraphAssociation:
293
407
  self.name,\
294
408
  self.left_field.fieldname,\
295
409
  self.right_field.fieldname
296
- )
410
+ )
297
411
  return full_name
298
412
 
299
413
 
@@ -363,17 +477,20 @@ class LanguageGraphAssociation:
363
477
 
364
478
  @dataclass
365
479
  class LanguageGraphAttackStep:
480
+ """
481
+ An attack step belonging to an asset type in the MAL language
482
+ """
366
483
  name: str
367
484
  type: str
368
485
  asset: LanguageGraphAsset
369
- ttc: dict = field(default_factory = dict)
486
+ ttc: Optional[dict] = field(default_factory = dict)
370
487
  overrides: bool = False
371
488
  children: dict = field(default_factory = dict)
372
489
  parents: dict = field(default_factory = dict)
373
490
  info: dict = field(default_factory = dict)
374
491
  inherits: Optional[LanguageGraphAttackStep] = None
492
+ own_requires: list[ExpressionsChain] = field(default_factory=list)
375
493
  tags: set = field(default_factory = set)
376
- _attributes: Optional[dict] = None
377
494
  detectors: dict = field(default_factory = lambda: {})
378
495
 
379
496
 
@@ -450,6 +567,10 @@ class LanguageGraphAttackStep:
450
567
 
451
568
 
452
569
  class ExpressionsChain:
570
+ """
571
+ A series of linked step expressions that specify the association path and
572
+ operations to take to reach the child/parent attack step.
573
+ """
453
574
  def __init__(self,
454
575
  type: str,
455
576
  left_link: Optional[ExpressionsChain] = None,
@@ -561,7 +682,6 @@ class ExpressionsChain:
561
682
  msg = 'Missing expressions chain type!'
562
683
  logger.error(msg)
563
684
  raise LanguageGraphAssociationError(msg)
564
- return None
565
685
 
566
686
  expr_chain_type = serialized_expr_chain['type']
567
687
  match (expr_chain_type):
@@ -597,9 +717,10 @@ class ExpressionsChain:
597
717
  if association is None:
598
718
  msg = 'Failed to find association "%s" with '\
599
719
  'fieldname "%s"'
600
- logger.error(msg % (assoc_name, fieldname))
601
- raise LanguageGraphException(msg % (assoc_name,
602
- fieldname))
720
+ logger.error(msg, assoc_name, fieldname)
721
+ raise LanguageGraphException(
722
+ msg % (assoc_name, fieldname)
723
+ )
603
724
 
604
725
  new_expr_chain = ExpressionsChain(
605
726
  type = 'field',
@@ -629,7 +750,7 @@ class ExpressionsChain:
629
750
  subtype_asset = lang_graph.assets[subtype_name]
630
751
  else:
631
752
  msg = 'Failed to find subtype %s'
632
- logger.error(msg % subtype_name)
753
+ logger.error(msg, subtype_name)
633
754
  raise LanguageGraphException(msg % subtype_name)
634
755
 
635
756
  new_expr_chain = ExpressionsChain(
@@ -642,8 +763,9 @@ class ExpressionsChain:
642
763
  case _:
643
764
  msg = 'Unknown expressions chain type %s!'
644
765
  logger.error(msg, serialized_expr_chain['type'])
645
- raise LanguageGraphAssociationError(msg %
646
- serialized_expr_chain['type'])
766
+ raise LanguageGraphAssociationError(
767
+ msg % serialized_expr_chain['type']
768
+ )
647
769
 
648
770
 
649
771
  def __repr__(self) -> str:
@@ -653,7 +775,7 @@ class ExpressionsChain:
653
775
  class LanguageGraph():
654
776
  """Graph representation of a MAL language"""
655
777
  def __init__(self, lang: Optional[dict] = None):
656
- self.assets: dict = {}
778
+ self.assets: dict[str, LanguageGraphAsset] = {}
657
779
  if lang is not None:
658
780
  self._lang_spec: dict = lang
659
781
  self.metadata = {
@@ -694,7 +816,6 @@ class LanguageGraph():
694
816
  langspec = archive.read('langspec.json')
695
817
  return LanguageGraph(json.loads(langspec))
696
818
 
697
-
698
819
  def _to_dict(self):
699
820
  """Converts LanguageGraph into a dict"""
700
821
 
@@ -708,10 +829,12 @@ class LanguageGraph():
708
829
 
709
830
  return serialized_graph
710
831
 
711
- def _link_association_to_assets(cls,
712
- assoc: LanguageGraphAssociation,
713
- left_asset: LanguageGraphAsset,
714
- right_asset: LanguageGraphAsset):
832
+ @staticmethod
833
+ def _link_association_to_assets(
834
+ assoc: LanguageGraphAssociation,
835
+ left_asset: LanguageGraphAsset,
836
+ right_asset: LanguageGraphAsset
837
+ ):
715
838
  left_asset.own_associations[assoc.right_field.fieldname] = assoc
716
839
  right_asset.own_associations[assoc.left_field.fieldname] = assoc
717
840
 
@@ -840,7 +963,7 @@ class LanguageGraph():
840
963
  name = attack_step_dict['name'],
841
964
  type = attack_step_dict['type'],
842
965
  asset = asset,
843
- ttc = attack_step_dict['ttc'],
966
+ ttc = get_ttc_distribution(attack_step_dict),
844
967
  overrides = attack_step_dict['overrides'],
845
968
  children = {},
846
969
  parents = {},
@@ -925,7 +1048,8 @@ class LanguageGraph():
925
1048
  expr_chain_dict,
926
1049
  lang_graph
927
1050
  )
928
- attack_step.own_requires.append(expr_chain)
1051
+ if expr_chain:
1052
+ attack_step.own_requires.append(expr_chain)
929
1053
 
930
1054
  return lang_graph
931
1055
 
@@ -955,7 +1079,6 @@ class LanguageGraph():
955
1079
  )
956
1080
 
957
1081
 
958
-
959
1082
  def save_language_specification_to_json(self, filename: str) -> None:
960
1083
  """
961
1084
  Save a MAL language specification dictionary to a JSON file
@@ -968,12 +1091,290 @@ class LanguageGraph():
968
1091
  with open(filename, 'w', encoding='utf-8') as file:
969
1092
  json.dump(self._lang_spec, file, indent=4)
970
1093
 
1094
+ def process_attack_step_expression(
1095
+ self,
1096
+ target_asset: LanguageGraphAsset,
1097
+ step_expression: dict[str, Any]
1098
+ ) -> tuple[
1099
+ LanguageGraphAsset,
1100
+ None,
1101
+ str
1102
+ ]:
1103
+ """
1104
+ The attack step expression just adds the name of the attack
1105
+ step. All other step expressions only modify the target
1106
+ asset and parent associations chain.
1107
+ """
1108
+ return (
1109
+ target_asset,
1110
+ None,
1111
+ step_expression['name']
1112
+ )
1113
+
1114
+ def process_set_operation_step_expression(
1115
+ self,
1116
+ target_asset: LanguageGraphAsset,
1117
+ expr_chain: Optional[ExpressionsChain],
1118
+ step_expression: dict[str, Any]
1119
+ ) -> tuple[
1120
+ LanguageGraphAsset,
1121
+ ExpressionsChain,
1122
+ None
1123
+ ]:
1124
+ """
1125
+ The set operators are used to combine the left hand and right
1126
+ hand targets accordingly.
1127
+ """
971
1128
 
972
- def process_step_expression(self,
1129
+ lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
973
1130
  target_asset,
974
1131
  expr_chain,
1132
+ step_expression['lhs']
1133
+ )
1134
+ rh_target_asset, rh_expr_chain, _ = self.process_step_expression(
1135
+ target_asset,
1136
+ expr_chain,
1137
+ step_expression['rhs']
1138
+ )
1139
+
1140
+ assert lh_target_asset, (
1141
+ f"No lh target in step expression {step_expression}"
1142
+ )
1143
+ assert rh_target_asset, (
1144
+ f"No rh target in step expression {step_expression}"
1145
+ )
1146
+
1147
+ if not lh_target_asset.get_all_common_superassets(rh_target_asset):
1148
+ raise ValueError(
1149
+ "Set operation attempted between targets that do not share "
1150
+ f"any common superassets: {lh_target_asset.name} "
1151
+ f"and {rh_target_asset.name}!"
1152
+ )
1153
+
1154
+ new_expr_chain = ExpressionsChain(
1155
+ type = step_expression['type'],
1156
+ left_link = lh_expr_chain,
1157
+ right_link = rh_expr_chain
1158
+ )
1159
+ return (
1160
+ lh_target_asset,
1161
+ new_expr_chain,
1162
+ None
1163
+ )
1164
+
1165
+ def process_variable_step_expression(
1166
+ self,
1167
+ target_asset: LanguageGraphAsset,
1168
+ step_expression: dict[str, Any]
1169
+ ) -> tuple[
1170
+ LanguageGraphAsset,
1171
+ ExpressionsChain,
1172
+ None
1173
+ ]:
1174
+
1175
+ var_name = step_expression['name']
1176
+ var_target_asset, var_expr_chain = (
1177
+ self._resolve_variable(target_asset, var_name)
1178
+ )
1179
+
1180
+ if var_expr_chain is None:
1181
+ raise LookupError(
1182
+ f'Failed to find variable "{step_expression["name"]}" '
1183
+ f'for {target_asset.name}',
1184
+ )
1185
+
1186
+ return (
1187
+ var_target_asset,
1188
+ var_expr_chain,
1189
+ None
1190
+ )
1191
+
1192
+ def process_field_step_expression(
1193
+ self,
1194
+ target_asset: LanguageGraphAsset,
1195
+ step_expression: dict[str, Any]
1196
+ ) -> tuple[
1197
+ LanguageGraphAsset,
1198
+ ExpressionsChain,
1199
+ None
1200
+ ]:
1201
+ """
1202
+ Change the target asset from the current one to the associated
1203
+ asset given the specified field name and add the parent
1204
+ fieldname and association to the parent associations chain.
1205
+ """
1206
+
1207
+ fieldname = step_expression['name']
1208
+
1209
+ if target_asset is None:
1210
+ raise ValueError(
1211
+ f'Missing target asset for field "{fieldname}"!'
1212
+ )
1213
+
1214
+ new_target_asset = None
1215
+ for association in target_asset.associations.values():
1216
+ if (association.left_field.fieldname == fieldname and \
1217
+ target_asset.is_subasset_of(
1218
+ association.right_field.asset)):
1219
+ new_target_asset = association.left_field.asset
1220
+
1221
+ if (association.right_field.fieldname == fieldname and \
1222
+ target_asset.is_subasset_of(
1223
+ association.left_field.asset)):
1224
+ new_target_asset = association.right_field.asset
1225
+
1226
+ if new_target_asset:
1227
+ new_expr_chain = ExpressionsChain(
1228
+ type = 'field',
1229
+ fieldname = fieldname,
1230
+ association = association
1231
+ )
1232
+ return (
1233
+ new_target_asset,
1234
+ new_expr_chain,
1235
+ None
1236
+ )
1237
+
1238
+ raise LookupError(
1239
+ f'Failed to find field {fieldname} on asset {target_asset.name}!',
1240
+ )
1241
+
1242
+ def process_transitive_step_expression(
1243
+ self,
1244
+ target_asset: LanguageGraphAsset,
1245
+ expr_chain: Optional[ExpressionsChain],
1246
+ step_expression: dict[str, Any]
1247
+ ) -> tuple[
1248
+ LanguageGraphAsset,
1249
+ ExpressionsChain,
1250
+ None
1251
+ ]:
1252
+ """
1253
+ Create a transitive tuple entry that applies to the next
1254
+ component of the step expression.
1255
+ """
1256
+ result_target_asset, result_expr_chain, _ = (
1257
+ self.process_step_expression(
1258
+ target_asset,
1259
+ expr_chain,
1260
+ step_expression['stepExpression']
1261
+ )
1262
+ )
1263
+ new_expr_chain = ExpressionsChain(
1264
+ type = 'transitive',
1265
+ sub_link = result_expr_chain
1266
+ )
1267
+ return (
1268
+ result_target_asset,
1269
+ new_expr_chain,
1270
+ None
1271
+ )
1272
+
1273
+ def process_subType_step_expression(
1274
+ self,
1275
+ target_asset: LanguageGraphAsset,
1276
+ expr_chain: Optional[ExpressionsChain],
1277
+ step_expression: dict[str, Any]
1278
+ ) -> tuple[
1279
+ LanguageGraphAsset,
1280
+ ExpressionsChain,
1281
+ None
1282
+ ]:
1283
+ """
1284
+ Create a subType tuple entry that applies to the next
1285
+ component of the step expression and changes the target
1286
+ asset to the subasset.
1287
+ """
1288
+
1289
+ subtype_name = step_expression['subType']
1290
+ result_target_asset, result_expr_chain, _ = (
1291
+ self.process_step_expression(
1292
+ target_asset,
1293
+ expr_chain,
1294
+ step_expression['stepExpression']
1295
+ )
1296
+ )
1297
+
1298
+ if subtype_name not in self.assets:
1299
+ raise LanguageGraphException(
1300
+ f'Failed to find subtype {subtype_name}'
1301
+ )
1302
+
1303
+ subtype_asset = self.assets[subtype_name]
1304
+
1305
+ if result_target_asset is None:
1306
+ raise LookupError("Nonexisting asset for subtype")
1307
+
1308
+ if not subtype_asset.is_subasset_of(result_target_asset):
1309
+ raise ValueError(
1310
+ f'Found subtype {subtype_name} which does not extend '
1311
+ f'{result_target_asset.name}, subtype cannot be resolved.'
1312
+ )
1313
+
1314
+ new_expr_chain = ExpressionsChain(
1315
+ type = 'subType',
1316
+ sub_link = result_expr_chain,
1317
+ subtype = subtype_asset
1318
+ )
1319
+ return (
1320
+ subtype_asset,
1321
+ new_expr_chain,
1322
+ None
1323
+ )
1324
+
1325
+ def process_collect_step_expression(
1326
+ self,
1327
+ target_asset: LanguageGraphAsset,
1328
+ expr_chain: Optional[ExpressionsChain],
1329
+ step_expression: dict[str, Any]
1330
+ ) -> tuple[
1331
+ LanguageGraphAsset,
1332
+ Optional[ExpressionsChain],
1333
+ Optional[str]
1334
+ ]:
1335
+ """
1336
+ Apply the right hand step expression to left hand step
1337
+ expression target asset and parent associations chain.
1338
+ """
1339
+ lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
1340
+ target_asset, expr_chain, step_expression['lhs']
1341
+ )
1342
+
1343
+ if lh_target_asset is None:
1344
+ raise ValueError(
1345
+ 'No left hand asset in collect expression '
1346
+ f'{step_expression["lhs"]}'
1347
+ )
1348
+
1349
+ rh_target_asset, rh_expr_chain, rh_attack_step_name = (
1350
+ self.process_step_expression(
1351
+ lh_target_asset, None, step_expression['rhs']
1352
+ )
1353
+ )
1354
+
1355
+ new_expr_chain = lh_expr_chain
1356
+ if rh_expr_chain:
1357
+ new_expr_chain = ExpressionsChain(
1358
+ type = 'collect',
1359
+ left_link = lh_expr_chain,
1360
+ right_link = rh_expr_chain
1361
+ )
1362
+
1363
+ return (
1364
+ rh_target_asset,
1365
+ new_expr_chain,
1366
+ rh_attack_step_name
1367
+ )
1368
+
1369
+ def process_step_expression(self,
1370
+ target_asset: LanguageGraphAsset,
1371
+ expr_chain: Optional[ExpressionsChain],
975
1372
  step_expression: dict
976
- ) -> tuple:
1373
+ ) -> tuple[
1374
+ LanguageGraphAsset,
1375
+ Optional[ExpressionsChain],
1376
+ Optional[str]
1377
+ ]:
977
1378
  """
978
1379
  Recursively process an attack step expression.
979
1380
 
@@ -1002,210 +1403,46 @@ class LanguageGraph():
1002
1403
  json.dumps(step_expression, indent = 2)
1003
1404
  )
1004
1405
 
1406
+ result: tuple[
1407
+ LanguageGraphAsset,
1408
+ Optional[ExpressionsChain],
1409
+ Optional[str]
1410
+ ]
1411
+
1005
1412
  match (step_expression['type']):
1006
1413
  case 'attackStep':
1007
- # The attack step expression just adds the name of the attack
1008
- # step. All other step expressions only modify the target
1009
- # asset and parent associations chain.
1010
- return (
1011
- target_asset,
1012
- None,
1013
- step_expression['name']
1414
+ result = self.process_attack_step_expression(
1415
+ target_asset, step_expression
1014
1416
  )
1015
-
1016
1417
  case 'union' | 'intersection' | 'difference':
1017
- # The set operators are used to combine the left hand and right
1018
- # hand targets accordingly.
1019
- lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
1020
- target_asset,
1021
- expr_chain,
1022
- step_expression['lhs']
1023
- )
1024
- rh_target_asset, rh_expr_chain, _ = \
1025
- self.process_step_expression(
1026
- target_asset,
1027
- expr_chain,
1028
- step_expression['rhs']
1029
- )
1030
-
1031
- if not lh_target_asset.get_all_common_superassets(
1032
- rh_target_asset):
1033
- logger.error(
1034
- "Set operation attempted between targets that"
1035
- " do not share any common superassets: %s and %s!",
1036
- lh_target_asset.name, rh_target_asset.name
1037
- )
1038
- return (None, None, None)
1039
-
1040
- new_expr_chain = ExpressionsChain(
1041
- type = step_expression['type'],
1042
- left_link = lh_expr_chain,
1043
- right_link = rh_expr_chain
1044
- )
1045
- return (
1046
- lh_target_asset,
1047
- new_expr_chain,
1048
- None
1418
+ result = self.process_set_operation_step_expression(
1419
+ target_asset, expr_chain, step_expression
1049
1420
  )
1050
-
1051
1421
  case 'variable':
1052
- var_name = step_expression['name']
1053
- var_target_asset, var_expr_chain = self._resolve_variable(
1054
- target_asset, var_name)
1055
- var_target_asset, var_expr_chain = \
1056
- target_asset.get_variable(var_name)
1057
- if var_expr_chain is not None:
1058
- return (
1059
- var_target_asset,
1060
- var_expr_chain,
1061
- None
1062
- )
1063
- else:
1064
- logger.error(
1065
- 'Failed to find variable \"%s\" for %s',
1066
- step_expression["name"], target_asset.name
1067
- )
1068
- return (None, None, None)
1069
-
1422
+ result = self.process_variable_step_expression(
1423
+ target_asset, step_expression
1424
+ )
1070
1425
  case 'field':
1071
- # Change the target asset from the current one to the associated
1072
- # asset given the specified field name and add the parent
1073
- # fieldname and association to the parent associations chain.
1074
- fieldname = step_expression['name']
1075
- if not target_asset:
1076
- logger.error(
1077
- 'Missing target asset for field "%s"!', fieldname
1078
- )
1079
- return (None, None, None)
1080
-
1081
- new_target_asset = None
1082
- for association in target_asset.associations.values():
1083
- if (association.left_field.fieldname == fieldname and \
1084
- target_asset.is_subasset_of(
1085
- association.right_field.asset)):
1086
- new_target_asset = association.left_field.asset
1087
-
1088
- if (association.right_field.fieldname == fieldname and \
1089
- target_asset.is_subasset_of(
1090
- association.left_field.asset)):
1091
- new_target_asset = association.right_field.asset
1092
-
1093
- if new_target_asset:
1094
- new_expr_chain = ExpressionsChain(
1095
- type = 'field',
1096
- fieldname = fieldname,
1097
- association = association
1098
- )
1099
- return (
1100
- new_target_asset,
1101
- new_expr_chain,
1102
- None
1103
- )
1104
- logger.error(
1105
- 'Failed to find field "%s" on asset "%s"!',
1106
- fieldname, target_asset.name
1426
+ result = self.process_field_step_expression(
1427
+ target_asset, step_expression
1107
1428
  )
1108
- return (None, None, None)
1109
-
1110
1429
  case 'transitive':
1111
- # Create a transitive tuple entry that applies to the next
1112
- # component of the step expression.
1113
- result_target_asset, \
1114
- result_expr_chain, \
1115
- attack_step = \
1116
- self.process_step_expression(
1117
- target_asset,
1118
- expr_chain,
1119
- step_expression['stepExpression']
1120
- )
1121
- new_expr_chain = ExpressionsChain(
1122
- type = 'transitive',
1123
- sub_link = result_expr_chain
1430
+ result = self.process_transitive_step_expression(
1431
+ target_asset, expr_chain, step_expression
1124
1432
  )
1125
- return (
1126
- result_target_asset,
1127
- new_expr_chain,
1128
- attack_step
1129
- )
1130
-
1131
1433
  case 'subType':
1132
- # Create a subType tuple entry that applies to the next
1133
- # component of the step expression and changes the target
1134
- # asset to the subasset.
1135
- subtype_name = step_expression['subType']
1136
- result_target_asset, \
1137
- result_expr_chain, \
1138
- attack_step = \
1139
- self.process_step_expression(
1140
- target_asset,
1141
- expr_chain,
1142
- step_expression['stepExpression']
1143
- )
1144
-
1145
- if subtype_name in self.assets:
1146
- subtype_asset = self.assets[subtype_name]
1147
- else:
1148
- msg = 'Failed to find subtype %s'
1149
- logger.error(msg % subtype_name)
1150
- raise LanguageGraphException(msg % subtype_name)
1151
-
1152
- if not subtype_asset.is_subasset_of(result_target_asset):
1153
- logger.error(
1154
- 'Found subtype "%s" which does not extend "%s", '
1155
- 'therefore the subtype cannot be resolved.',
1156
- subtype_name, result_target_asset.name
1157
- )
1158
- return (None, None, None)
1159
-
1160
- new_expr_chain = ExpressionsChain(
1161
- type = 'subType',
1162
- sub_link = result_expr_chain,
1163
- subtype = subtype_asset
1164
- )
1165
- return (
1166
- subtype_asset,
1167
- new_expr_chain,
1168
- attack_step
1434
+ result = self.process_subType_step_expression(
1435
+ target_asset, expr_chain, step_expression
1169
1436
  )
1170
-
1171
1437
  case 'collect':
1172
- # Apply the right hand step expression to left hand step
1173
- # expression target asset and parent associations chain.
1174
- (lh_target_asset, lh_expr_chain, _) = \
1175
- self.process_step_expression(
1176
- target_asset,
1177
- expr_chain,
1178
- step_expression['lhs']
1179
- )
1180
- (rh_target_asset,
1181
- rh_expr_chain,
1182
- rh_attack_step_name) = \
1183
- self.process_step_expression(
1184
- lh_target_asset,
1185
- None,
1186
- step_expression['rhs']
1187
- )
1188
- if rh_expr_chain:
1189
- new_expr_chain = ExpressionsChain(
1190
- type = 'collect',
1191
- left_link = lh_expr_chain,
1192
- right_link = rh_expr_chain
1193
- )
1194
- else:
1195
- new_expr_chain = lh_expr_chain
1196
-
1197
- return (
1198
- rh_target_asset,
1199
- new_expr_chain,
1200
- rh_attack_step_name
1438
+ result = self.process_collect_step_expression(
1439
+ target_asset, expr_chain, step_expression
1201
1440
  )
1202
-
1203
1441
  case _:
1204
- logger.error(
1205
- 'Unknown attack step type: "%s"', step_expression["type"]
1442
+ raise LookupError(
1443
+ f'Unknown attack step type: "{step_expression["type"]}"'
1206
1444
  )
1207
- return (None, None, None)
1208
-
1445
+ return result
1209
1446
 
1210
1447
  def reverse_expr_chain(
1211
1448
  self,
@@ -1300,7 +1537,7 @@ class LanguageGraph():
1300
1537
  logger.error(msg, expr_chain.type)
1301
1538
  raise LanguageGraphAssociationError(msg % expr_chain.type)
1302
1539
 
1303
- def _resolve_variable(self, asset, var_name) -> tuple:
1540
+ def _resolve_variable(self, asset: LanguageGraphAsset, var_name) -> tuple:
1304
1541
  """
1305
1542
  Resolve a variable for a specific asset by variable name.
1306
1543
 
@@ -1323,35 +1560,75 @@ class LanguageGraph():
1323
1560
  return (target_asset, expr_chain)
1324
1561
  return asset.variables[var_name]
1325
1562
 
1326
-
1327
- def _generate_graph(self) -> None:
1328
- """
1329
- Generate language graph starting from the MAL language specification
1330
- given in the constructor.
1563
+ def _create_associations_for_assets(
1564
+ self,
1565
+ lang_spec: dict[str, Any],
1566
+ assets: dict[str, LanguageGraphAsset]
1567
+ ) -> None:
1568
+ """ Link associations to assets based on the language specification.
1569
+ Arguments:
1570
+ lang_spec - the language specification dictionary
1571
+ assets - a dictionary of LanguageGraphAsset objects
1572
+ indexed by their names
1331
1573
  """
1332
- # Generate all of the asset nodes of the language graph.
1333
- for asset_dict in self._lang_spec['assets']:
1574
+
1575
+ for association_dict in lang_spec['associations']:
1334
1576
  logger.debug(
1335
- 'Create asset language graph nodes for asset %s',
1336
- asset_dict['name']
1577
+ 'Create association language graph nodes for association %s',
1578
+ association_dict['name']
1337
1579
  )
1338
- asset_node = LanguageGraphAsset(
1339
- name = asset_dict['name'],
1340
- own_associations = {},
1341
- attack_steps = {},
1342
- info = asset_dict['meta'],
1343
- own_super_asset = None,
1344
- own_sub_assets = set(),
1345
- own_variables = {},
1346
- is_abstract = asset_dict['isAbstract']
1580
+
1581
+ left_asset_name = association_dict['leftAsset']
1582
+ right_asset_name = association_dict['rightAsset']
1583
+
1584
+ if left_asset_name not in assets:
1585
+ raise LanguageGraphAssociationError(
1586
+ f'Left asset "{left_asset_name}" for '
1587
+ f'association "{association_dict["name"]}" not found!'
1588
+ )
1589
+ if right_asset_name not in assets:
1590
+ raise LanguageGraphAssociationError(
1591
+ f'Right asset "{right_asset_name}" for '
1592
+ f'association "{association_dict["name"]}" not found!'
1593
+ )
1594
+
1595
+ left_asset = assets[left_asset_name]
1596
+ right_asset = assets[right_asset_name]
1597
+
1598
+ assoc_node = LanguageGraphAssociation(
1599
+ name = association_dict['name'],
1600
+ left_field = LanguageGraphAssociationField(
1601
+ left_asset,
1602
+ association_dict['leftField'],
1603
+ association_dict['leftMultiplicity']['min'],
1604
+ association_dict['leftMultiplicity']['max']
1605
+ ),
1606
+ right_field = LanguageGraphAssociationField(
1607
+ right_asset,
1608
+ association_dict['rightField'],
1609
+ association_dict['rightMultiplicity']['min'],
1610
+ association_dict['rightMultiplicity']['max']
1611
+ ),
1612
+ info = association_dict['meta']
1347
1613
  )
1348
- self.assets[asset_dict['name']] = asset_node
1349
1614
 
1350
- # Link assets based on inheritance
1351
- for asset_dict in self._lang_spec['assets']:
1352
- asset = self.assets[asset_dict['name']]
1615
+ # Add the association to the left and right asset
1616
+ self._link_association_to_assets(
1617
+ assoc_node, left_asset, right_asset
1618
+ )
1619
+
1620
+ def _link_assets(
1621
+ self,
1622
+ lang_spec: dict[str, Any],
1623
+ assets: dict[str, LanguageGraphAsset]
1624
+ ) -> None:
1625
+ """
1626
+ Link assets based on inheritance and associations.
1627
+ """
1628
+ for asset_dict in lang_spec['assets']:
1629
+ asset = assets[asset_dict['name']]
1353
1630
  if asset_dict['superAsset']:
1354
- super_asset = self.assets[asset_dict['superAsset']]
1631
+ super_asset = assets[asset_dict['superAsset']]
1355
1632
  if not super_asset:
1356
1633
  msg = 'Failed to find super asset "%s" for asset "%s"!'
1357
1634
  logger.error(
@@ -1362,54 +1639,21 @@ class LanguageGraph():
1362
1639
  super_asset.own_sub_assets.add(asset)
1363
1640
  asset.own_super_asset = super_asset
1364
1641
 
1365
- # Generate all of the association nodes of the language graph.
1366
- for asset in self.assets.values():
1642
+ def _set_variables_for_assets(
1643
+ self, assets: dict[str, LanguageGraphAsset]
1644
+ ) -> None:
1645
+ """ Set the variables for each asset based on the language specification.
1646
+ Arguments:
1647
+ assets - a dictionary of LanguageGraphAsset objects
1648
+ indexed by their names
1649
+ """
1650
+
1651
+ for asset in assets.values():
1367
1652
  logger.debug(
1368
- 'Create association language graph nodes for asset %s',
1369
- asset.name
1653
+ 'Set variables for asset %s', asset.name
1370
1654
  )
1371
-
1372
- associations = self._get_associations_for_asset_type(asset.name)
1373
- for association in associations:
1374
- left_asset = self.assets[association['leftAsset']]
1375
- if not left_asset:
1376
- msg = 'Left asset "%s" for association "%s" not found!'
1377
- logger.error(
1378
- msg, association["leftAsset"], association["name"])
1379
- raise LanguageGraphAssociationError(
1380
- msg % (association["leftAsset"], association["name"]))
1381
-
1382
- right_asset = self.assets[association['rightAsset']]
1383
- if not right_asset:
1384
- msg = 'Right asset "%s" for association "%s" not found!'
1385
- logger.error(
1386
- msg, association["rightAsset"], association["name"])
1387
- raise LanguageGraphAssociationError(
1388
- msg % (association["rightAsset"], association["name"])
1389
- )
1390
-
1391
- assoc_node = LanguageGraphAssociation(
1392
- name = association['name'],
1393
- left_field = LanguageGraphAssociationField(
1394
- left_asset,
1395
- association['leftField'],
1396
- association['leftMultiplicity']['min'],
1397
- association['leftMultiplicity']['max']),
1398
- right_field = LanguageGraphAssociationField(
1399
- right_asset,
1400
- association['rightField'],
1401
- association['rightMultiplicity']['min'],
1402
- association['rightMultiplicity']['max']),
1403
- info = association['meta']
1404
- )
1405
-
1406
- # Add the association to the left and right asset
1407
- self._link_association_to_assets(assoc_node,
1408
- left_asset, right_asset)
1409
-
1410
- # Set the variables
1411
- for asset in self.assets.values():
1412
- for variable in self._get_variables_for_asset_type(asset.name):
1655
+ variables = self._get_variables_for_asset_type(asset.name)
1656
+ for variable in variables:
1413
1657
  if logger.isEnabledFor(logging.DEBUG):
1414
1658
  # Avoid running json.dumps when not in debug
1415
1659
  logger.debug(
@@ -1418,9 +1662,13 @@ class LanguageGraph():
1418
1662
  )
1419
1663
  self._resolve_variable(asset, variable['name'])
1420
1664
 
1421
-
1422
- # Generate all of the attack step nodes of the language graph.
1423
- for asset in self.assets.values():
1665
+ def _generate_attack_steps(self, assets) -> None:
1666
+ """
1667
+ Generate all of the attack steps for each asset type
1668
+ based on the language specification.
1669
+ """
1670
+ langspec_dict = {}
1671
+ for asset in assets.values():
1424
1672
  logger.debug(
1425
1673
  'Create attack steps language graph nodes for asset %s',
1426
1674
  asset.name
@@ -1436,24 +1684,27 @@ class LanguageGraph():
1436
1684
  name = attack_step_attribs['name'],
1437
1685
  type = attack_step_attribs['type'],
1438
1686
  asset = asset,
1439
- ttc = attack_step_attribs['ttc'],
1440
- overrides = attack_step_attribs['reaches']['overrides'] \
1441
- if attack_step_attribs['reaches'] else False,
1687
+ ttc = get_ttc_distribution(attack_step_attribs),
1688
+ overrides = (
1689
+ attack_step_attribs['reaches']['overrides']
1690
+ if attack_step_attribs['reaches'] else False
1691
+ ),
1442
1692
  children = {},
1443
1693
  parents = {},
1444
1694
  info = attack_step_attribs['meta'],
1445
1695
  tags = set(attack_step_attribs['tags'])
1446
1696
  )
1447
- attack_step_node._attributes = attack_step_attribs
1448
- asset.attack_steps[attack_step_attribs['name']] = \
1697
+ langspec_dict[attack_step_node.full_name] = \
1698
+ attack_step_attribs
1699
+ asset.attack_steps[attack_step_node.name] = \
1449
1700
  attack_step_node
1450
1701
 
1451
- for detector in attack_step_attribs.get("detectors",
1452
- {}).values():
1702
+ detectors: dict = attack_step_attribs.get("detectors", {})
1703
+ for detector in detectors.values():
1453
1704
  attack_step_node.detectors[detector["name"]] = Detector(
1454
1705
  context=Context(
1455
1706
  {
1456
- label: self.assets[asset]
1707
+ label: assets[asset]
1457
1708
  for label, asset in detector["context"].items()
1458
1709
  }
1459
1710
  ),
@@ -1508,13 +1759,15 @@ class LanguageGraph():
1508
1759
  attack_step.name
1509
1760
  )
1510
1761
 
1511
- if attack_step._attributes is None:
1762
+ if attack_step.full_name not in langspec_dict:
1512
1763
  # This is simply an empty inherited attack step
1513
1764
  continue
1514
1765
 
1515
- step_expressions = \
1516
- attack_step._attributes['reaches']['stepExpressions'] if \
1517
- attack_step._attributes['reaches'] else []
1766
+ langspec_entry = langspec_dict[attack_step.full_name]
1767
+ step_expressions = (
1768
+ langspec_entry['reaches']['stepExpressions']
1769
+ if langspec_entry['reaches'] else []
1770
+ )
1518
1771
 
1519
1772
  for step_expression in step_expressions:
1520
1773
  # Resolve each of the attack step expressions listed for
@@ -1549,65 +1802,94 @@ class LanguageGraph():
1549
1802
  target_attack_step_name]
1550
1803
 
1551
1804
  # Link to the children target attack steps
1552
- if target_attack_step.full_name in attack_step.children:
1553
- attack_step.children[target_attack_step.full_name].\
1554
- append((target_attack_step, expr_chain))
1555
- else:
1556
- attack_step.children[target_attack_step.full_name] = \
1557
- [(target_attack_step, expr_chain)]
1805
+ attack_step.children.setdefault(target_attack_step.full_name, [])
1806
+ attack_step.children[target_attack_step.full_name].append(
1807
+ (target_attack_step, expr_chain)
1808
+ )
1809
+
1558
1810
  # Reverse the children associations chains to get the
1559
1811
  # parents associations chain.
1560
- if attack_step.full_name in target_attack_step.parents:
1561
- target_attack_step.parents[attack_step.full_name].\
1562
- append((attack_step,
1563
- self.reverse_expr_chain(expr_chain,
1564
- None)))
1565
- else:
1566
- target_attack_step.parents[attack_step.full_name] = \
1567
- [(attack_step,
1568
- self.reverse_expr_chain(expr_chain,
1569
- None))]
1812
+ target_attack_step.parents.setdefault(attack_step.full_name, [])
1813
+ target_attack_step.parents[attack_step.full_name].append(
1814
+ (attack_step, self.reverse_expr_chain(expr_chain, None))
1815
+ )
1570
1816
 
1571
1817
  # Evaluate the requirements of exist and notExist attack steps
1572
- if attack_step.type == 'exist' or \
1573
- attack_step.type == 'notExist':
1574
- step_expressions = \
1575
- attack_step._attributes['requires']['stepExpressions'] \
1576
- if attack_step._attributes['requires'] else []
1818
+ if attack_step.type in ('exist', 'notExist'):
1819
+ step_expressions = (
1820
+ langspec_entry['requires']['stepExpressions']
1821
+ if langspec_entry['requires'] else []
1822
+ )
1577
1823
  if not step_expressions:
1578
- msg = 'Failed to find requirements for attack step' \
1579
- ' "%s" of type "%s":\n%s'
1580
1824
  raise LanguageGraphStepExpressionError(
1581
- msg % (
1825
+ 'Failed to find requirements for attack step'
1826
+ ' "%s" of type "%s":\n%s' % (
1582
1827
  attack_step.name,
1583
1828
  attack_step.type,
1584
- json.dumps(attack_step._attributes, indent = 2)
1829
+ json.dumps(langspec_entry, indent = 2)
1585
1830
  )
1586
1831
  )
1587
1832
 
1588
- attack_step.own_requires = []
1589
1833
  for step_expression in step_expressions:
1590
- _, \
1591
- result_expr_chain, \
1592
- _ = \
1834
+ _, result_expr_chain, _ = \
1593
1835
  self.process_step_expression(
1594
1836
  attack_step.asset,
1595
1837
  None,
1596
1838
  step_expression
1597
1839
  )
1840
+ if result_expr_chain is None:
1841
+ raise LanguageGraphException('Failed to find '
1842
+ 'existence step requirement for step '
1843
+ f'expression:\n%s' % step_expression)
1598
1844
  attack_step.own_requires.append(result_expr_chain)
1599
1845
 
1600
- def _get_attacks_for_asset_type(self, asset_type: str) -> dict:
1846
+ def _generate_graph(self) -> None:
1847
+ """
1848
+ Generate language graph starting from the MAL language specification
1849
+ given in the constructor.
1850
+ """
1851
+ # Generate all of the asset nodes of the language graph.
1852
+ self.assets = {}
1853
+ for asset_dict in self._lang_spec['assets']:
1854
+ logger.debug(
1855
+ 'Create asset language graph nodes for asset %s',
1856
+ asset_dict['name']
1857
+ )
1858
+ asset_node = LanguageGraphAsset(
1859
+ name = asset_dict['name'],
1860
+ own_associations = {},
1861
+ attack_steps = {},
1862
+ info = asset_dict['meta'],
1863
+ own_super_asset = None,
1864
+ own_sub_assets = set(),
1865
+ own_variables = {},
1866
+ is_abstract = asset_dict['isAbstract']
1867
+ )
1868
+ self.assets[asset_dict['name']] = asset_node
1869
+
1870
+ # Link assets to each other
1871
+ self._link_assets(self._lang_spec, self.assets)
1872
+
1873
+ # Add and link associations to assets
1874
+ self._create_associations_for_assets(self._lang_spec, self.assets)
1875
+
1876
+ # Set the variables for each asset
1877
+ self._set_variables_for_assets(self.assets)
1878
+
1879
+ # Add attack steps to the assets
1880
+ self._generate_attack_steps(self.assets)
1881
+
1882
+ def _get_attacks_for_asset_type(self, asset_type: str) -> dict[str, dict]:
1601
1883
  """
1602
- Get all Attack Steps for a specific Class
1884
+ Get all Attack Steps for a specific asset type.
1603
1885
 
1604
1886
  Arguments:
1605
- asset_type - a string representing the class for which we want to
1606
- list the possible attack steps
1887
+ asset_type - the name of the asset type we want to
1888
+ list the possible attack steps for
1607
1889
 
1608
1890
  Return:
1609
- A dictionary representing the set of possible attacks for the
1610
- specified class. Each key in the dictionary is an attack name and is
1891
+ A dictionary containing the possible attacks for the
1892
+ specified asset type. Each key in the dictionary is an attack name
1611
1893
  associated with a dictionary containing other characteristics of the
1612
1894
  attack such as type of attack, TTC distribution, child attack steps
1613
1895
  and other information
@@ -1634,20 +1916,18 @@ class LanguageGraph():
1634
1916
 
1635
1917
  return attack_steps
1636
1918
 
1637
- def _get_associations_for_asset_type(self, asset_type: str) -> list:
1919
+ def _get_associations_for_asset_type(self, asset_type: str) -> list[dict]:
1638
1920
  """
1639
- Get all Associations for a specific Class
1921
+ Get all associations for a specific asset type.
1640
1922
 
1641
1923
  Arguments:
1642
- asset_type - a string representing the class for which we want to
1924
+ asset_type - the name of the asset type for which we want to
1643
1925
  list the associations
1644
1926
 
1645
1927
  Return:
1646
- A dictionary representing the set of associations for the specified
1647
- class. Each key in the dictionary is an attack name and is associated
1648
- with a dictionary containing other characteristics of the attack such
1649
- as type of attack, TTC distribution, child attack steps and other
1650
- information
1928
+ A list of dicts, where each dict represents an associations
1929
+ for the specified asset type. Each dictionary contains
1930
+ name and meta information about the association.
1651
1931
  """
1652
1932
  logger.debug(
1653
1933
  'Get associations for %s asset from '
@@ -1668,25 +1948,25 @@ class LanguageGraph():
1668
1948
  if assoc['leftAsset'] == asset_type or \
1669
1949
  assoc['rightAsset'] == asset_type)
1670
1950
  assoc = next(assoc_iter, None)
1671
- while (assoc):
1951
+ while assoc:
1672
1952
  associations.append(assoc)
1673
1953
  assoc = next(assoc_iter, None)
1674
1954
 
1675
1955
  return associations
1676
1956
 
1677
1957
  def _get_variables_for_asset_type(
1678
- self, asset_type: str) -> dict:
1958
+ self, asset_type: str) -> list[dict]:
1679
1959
  """
1680
- Get a variables for a specific asset type by name.
1960
+ Get variables for a specific asset type.
1681
1961
  Note: Variables are the ones specified in MAL through `let` statements
1682
1962
 
1683
1963
  Arguments:
1684
- asset_type - a string representing the type of asset which
1964
+ asset_type - a string representing the asset type which
1685
1965
  contains the variables
1686
1966
 
1687
1967
  Return:
1688
- A dictionary representing the step expressions for the variables
1689
- belonging to the asset.
1968
+ A list of dicts representing the step expressions for the variables
1969
+ belonging to the asset.
1690
1970
  """
1691
1971
 
1692
1972
  asset_dict = next((asset for asset in self._lang_spec['assets'] \
@@ -1733,5 +2013,3 @@ class LanguageGraph():
1733
2013
 
1734
2014
  self.assets = {}
1735
2015
  self._generate_graph()
1736
-
1737
-