mal-toolbox 0.3.10__py3-none-any.whl → 1.0.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.
@@ -26,6 +26,83 @@ 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': [1.0],
101
+ 'name': 'Bernoulli',
102
+ 'type': 'function'
103
+ },
104
+ }
105
+
29
106
 
30
107
  def disaggregate_attack_step_full_name(
31
108
  attack_step_full_name: str) -> list[str]:
@@ -49,6 +126,7 @@ class Detector:
49
126
 
50
127
 
51
128
  class Context(dict):
129
+ """Context is part of detectors to provide meta data about attackers"""
52
130
  def __init__(self, context) -> None:
53
131
  super().__init__(context)
54
132
  self._context_dict = context
@@ -66,6 +144,7 @@ class Context(dict):
66
144
 
67
145
  @dataclass
68
146
  class LanguageGraphAsset:
147
+ """An asset type as defined in the MAL language"""
69
148
  name: str
70
149
  own_associations: dict[str, LanguageGraphAssociation] = \
71
150
  field(default_factory = dict)
@@ -127,7 +206,7 @@ class LanguageGraphAsset:
127
206
  False otherwise.
128
207
  """
129
208
  current_asset: Optional[LanguageGraphAsset] = self
130
- while (current_asset):
209
+ while current_asset:
131
210
  if current_asset == target_asset:
132
211
  return True
133
212
  current_asset = current_asset.own_super_asset
@@ -164,7 +243,7 @@ class LanguageGraphAsset:
164
243
  """
165
244
  current_asset: Optional[LanguageGraphAsset] = self
166
245
  superassets = []
167
- while (current_asset):
246
+ while current_asset:
168
247
  superassets.append(current_asset)
169
248
  current_asset = current_asset.own_super_asset
170
249
  return superassets
@@ -188,7 +267,9 @@ class LanguageGraphAsset:
188
267
 
189
268
 
190
269
  @property
191
- def variables(self) -> dict[str, ExpressionsChain]:
270
+ def variables(
271
+ self
272
+ ) -> dict[str, tuple[LanguageGraphAsset, ExpressionsChain]]:
192
273
  """
193
274
  Return a list of all of the variables that belong to this asset
194
275
  directly or indirectly via inheritance.
@@ -204,27 +285,6 @@ class LanguageGraphAsset:
204
285
  return all_vars
205
286
 
206
287
 
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
288
  def get_all_common_superassets(
229
289
  self, other: LanguageGraphAsset
230
290
  ) -> set[str]:
@@ -241,6 +301,7 @@ class LanguageGraphAsset:
241
301
 
242
302
  @dataclass
243
303
  class LanguageGraphAssociationField:
304
+ """A field in an association"""
244
305
  asset: LanguageGraphAsset
245
306
  fieldname: str
246
307
  minimum: int
@@ -249,6 +310,9 @@ class LanguageGraphAssociationField:
249
310
 
250
311
  @dataclass
251
312
  class LanguageGraphAssociation:
313
+ """
314
+ An association type between asset types as defined in the MAL language
315
+ """
252
316
  name: str
253
317
  left_field: LanguageGraphAssociationField
254
318
  right_field: LanguageGraphAssociationField
@@ -277,9 +341,11 @@ class LanguageGraphAssociation:
277
341
 
278
342
 
279
343
  def __repr__(self) -> str:
280
- return (f'LanguageGraphAssociation(name: "{self.name}", '
344
+ return (
345
+ f'LanguageGraphAssociation(name: "{self.name}", '
281
346
  f'left_field: {self.left_field}, '
282
- f'right_field: {self.right_field})')
347
+ f'right_field: {self.right_field})'
348
+ )
283
349
 
284
350
 
285
351
  @property
@@ -293,7 +359,7 @@ class LanguageGraphAssociation:
293
359
  self.name,\
294
360
  self.left_field.fieldname,\
295
361
  self.right_field.fieldname
296
- )
362
+ )
297
363
  return full_name
298
364
 
299
365
 
@@ -363,6 +429,9 @@ class LanguageGraphAssociation:
363
429
 
364
430
  @dataclass
365
431
  class LanguageGraphAttackStep:
432
+ """
433
+ An attack step belonging to an asset type in the MAL language
434
+ """
366
435
  name: str
367
436
  type: str
368
437
  asset: LanguageGraphAsset
@@ -372,8 +441,8 @@ class LanguageGraphAttackStep:
372
441
  parents: dict = field(default_factory = dict)
373
442
  info: dict = field(default_factory = dict)
374
443
  inherits: Optional[LanguageGraphAttackStep] = None
444
+ own_requires: list[ExpressionsChain] = field(default_factory=list)
375
445
  tags: set = field(default_factory = set)
376
- _attributes: Optional[dict] = None
377
446
  detectors: dict = field(default_factory = lambda: {})
378
447
 
379
448
 
@@ -450,6 +519,10 @@ class LanguageGraphAttackStep:
450
519
 
451
520
 
452
521
  class ExpressionsChain:
522
+ """
523
+ A series of linked step expressions that specify the association path and
524
+ operations to take to reach the child/parent attack step.
525
+ """
453
526
  def __init__(self,
454
527
  type: str,
455
528
  left_link: Optional[ExpressionsChain] = None,
@@ -561,7 +634,6 @@ class ExpressionsChain:
561
634
  msg = 'Missing expressions chain type!'
562
635
  logger.error(msg)
563
636
  raise LanguageGraphAssociationError(msg)
564
- return None
565
637
 
566
638
  expr_chain_type = serialized_expr_chain['type']
567
639
  match (expr_chain_type):
@@ -597,9 +669,10 @@ class ExpressionsChain:
597
669
  if association is None:
598
670
  msg = 'Failed to find association "%s" with '\
599
671
  'fieldname "%s"'
600
- logger.error(msg % (assoc_name, fieldname))
601
- raise LanguageGraphException(msg % (assoc_name,
602
- fieldname))
672
+ logger.error(msg, assoc_name, fieldname)
673
+ raise LanguageGraphException(
674
+ msg % (assoc_name, fieldname)
675
+ )
603
676
 
604
677
  new_expr_chain = ExpressionsChain(
605
678
  type = 'field',
@@ -629,7 +702,7 @@ class ExpressionsChain:
629
702
  subtype_asset = lang_graph.assets[subtype_name]
630
703
  else:
631
704
  msg = 'Failed to find subtype %s'
632
- logger.error(msg % subtype_name)
705
+ logger.error(msg, subtype_name)
633
706
  raise LanguageGraphException(msg % subtype_name)
634
707
 
635
708
  new_expr_chain = ExpressionsChain(
@@ -642,8 +715,9 @@ class ExpressionsChain:
642
715
  case _:
643
716
  msg = 'Unknown expressions chain type %s!'
644
717
  logger.error(msg, serialized_expr_chain['type'])
645
- raise LanguageGraphAssociationError(msg %
646
- serialized_expr_chain['type'])
718
+ raise LanguageGraphAssociationError(
719
+ msg % serialized_expr_chain['type']
720
+ )
647
721
 
648
722
 
649
723
  def __repr__(self) -> str:
@@ -653,7 +727,8 @@ class ExpressionsChain:
653
727
  class LanguageGraph():
654
728
  """Graph representation of a MAL language"""
655
729
  def __init__(self, lang: Optional[dict] = None):
656
- self.assets: dict = {}
730
+ self.assets: dict[str, LanguageGraphAsset] = {}
731
+ self.predef_ttcs: dict[str, dict] = predef_ttcs
657
732
  if lang is not None:
658
733
  self._lang_spec: dict = lang
659
734
  self.metadata = {
@@ -695,6 +770,33 @@ class LanguageGraph():
695
770
  return LanguageGraph(json.loads(langspec))
696
771
 
697
772
 
773
+ def replace_if_predef_ttc(
774
+ self,
775
+ ttc_entry: Optional[dict]
776
+ ) -> dict:
777
+ """
778
+ If the TTC provided is a predefined name replace it with the
779
+ probability distribution it corresponds to. Otherwise, simply return
780
+ the TTC distribution provided as is.
781
+
782
+ Arguments:
783
+ ttc_entry - the TTC entry to check for predefined names
784
+
785
+ Returns:
786
+ If the TTC entry provided contained a predefined name the TTC
787
+ probability distrubtion corresponding to it. Otherwise, the TTC
788
+ distribution provided as a parameter as is.
789
+ """
790
+ if ttc_entry is None:
791
+ return {}
792
+
793
+ ttc = self.predef_ttcs.get(str(ttc_entry.get('name')))
794
+ if ttc is not None:
795
+ return ttc
796
+ else:
797
+ return ttc_entry
798
+
799
+
698
800
  def _to_dict(self):
699
801
  """Converts LanguageGraph into a dict"""
700
802
 
@@ -708,10 +810,12 @@ class LanguageGraph():
708
810
 
709
811
  return serialized_graph
710
812
 
711
- def _link_association_to_assets(cls,
712
- assoc: LanguageGraphAssociation,
713
- left_asset: LanguageGraphAsset,
714
- right_asset: LanguageGraphAsset):
813
+ @staticmethod
814
+ def _link_association_to_assets(
815
+ assoc: LanguageGraphAssociation,
816
+ left_asset: LanguageGraphAsset,
817
+ right_asset: LanguageGraphAsset
818
+ ):
715
819
  left_asset.own_associations[assoc.right_field.fieldname] = assoc
716
820
  right_asset.own_associations[assoc.left_field.fieldname] = assoc
717
821
 
@@ -836,11 +940,13 @@ class LanguageGraph():
836
940
  asset_dict['name']
837
941
  )
838
942
  for attack_step_dict in asset_dict['attack_steps'].values():
943
+ ttc = lang_graph.replace_if_predef_ttc(
944
+ attack_step_dict['ttc'])
839
945
  attack_step_node = LanguageGraphAttackStep(
840
946
  name = attack_step_dict['name'],
841
947
  type = attack_step_dict['type'],
842
948
  asset = asset,
843
- ttc = attack_step_dict['ttc'],
949
+ ttc = ttc,
844
950
  overrides = attack_step_dict['overrides'],
845
951
  children = {},
846
952
  parents = {},
@@ -925,7 +1031,8 @@ class LanguageGraph():
925
1031
  expr_chain_dict,
926
1032
  lang_graph
927
1033
  )
928
- attack_step.own_requires.append(expr_chain)
1034
+ if expr_chain:
1035
+ attack_step.own_requires.append(expr_chain)
929
1036
 
930
1037
  return lang_graph
931
1038
 
@@ -955,7 +1062,6 @@ class LanguageGraph():
955
1062
  )
956
1063
 
957
1064
 
958
-
959
1065
  def save_language_specification_to_json(self, filename: str) -> None:
960
1066
  """
961
1067
  Save a MAL language specification dictionary to a JSON file
@@ -968,12 +1074,290 @@ class LanguageGraph():
968
1074
  with open(filename, 'w', encoding='utf-8') as file:
969
1075
  json.dump(self._lang_spec, file, indent=4)
970
1076
 
1077
+ def process_attack_step_expression(
1078
+ self,
1079
+ target_asset: LanguageGraphAsset,
1080
+ step_expression: dict[str, Any]
1081
+ ) -> tuple[
1082
+ LanguageGraphAsset,
1083
+ None,
1084
+ str
1085
+ ]:
1086
+ """
1087
+ The attack step expression just adds the name of the attack
1088
+ step. All other step expressions only modify the target
1089
+ asset and parent associations chain.
1090
+ """
1091
+ return (
1092
+ target_asset,
1093
+ None,
1094
+ step_expression['name']
1095
+ )
971
1096
 
972
- def process_step_expression(self,
1097
+ def process_set_operation_step_expression(
1098
+ self,
1099
+ target_asset: LanguageGraphAsset,
1100
+ expr_chain: Optional[ExpressionsChain],
1101
+ step_expression: dict[str, Any]
1102
+ ) -> tuple[
1103
+ LanguageGraphAsset,
1104
+ ExpressionsChain,
1105
+ None
1106
+ ]:
1107
+ """
1108
+ The set operators are used to combine the left hand and right
1109
+ hand targets accordingly.
1110
+ """
1111
+
1112
+ lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
1113
+ target_asset,
1114
+ expr_chain,
1115
+ step_expression['lhs']
1116
+ )
1117
+ rh_target_asset, rh_expr_chain, _ = self.process_step_expression(
973
1118
  target_asset,
974
1119
  expr_chain,
1120
+ step_expression['rhs']
1121
+ )
1122
+
1123
+ assert lh_target_asset, (
1124
+ f"No lh target in step expression {step_expression}"
1125
+ )
1126
+ assert rh_target_asset, (
1127
+ f"No rh target in step expression {step_expression}"
1128
+ )
1129
+
1130
+ if not lh_target_asset.get_all_common_superassets(rh_target_asset):
1131
+ raise ValueError(
1132
+ "Set operation attempted between targets that do not share "
1133
+ f"any common superassets: {lh_target_asset.name} "
1134
+ f"and {rh_target_asset.name}!"
1135
+ )
1136
+
1137
+ new_expr_chain = ExpressionsChain(
1138
+ type = step_expression['type'],
1139
+ left_link = lh_expr_chain,
1140
+ right_link = rh_expr_chain
1141
+ )
1142
+ return (
1143
+ lh_target_asset,
1144
+ new_expr_chain,
1145
+ None
1146
+ )
1147
+
1148
+ def process_variable_step_expression(
1149
+ self,
1150
+ target_asset: LanguageGraphAsset,
1151
+ step_expression: dict[str, Any]
1152
+ ) -> tuple[
1153
+ LanguageGraphAsset,
1154
+ ExpressionsChain,
1155
+ None
1156
+ ]:
1157
+
1158
+ var_name = step_expression['name']
1159
+ var_target_asset, var_expr_chain = (
1160
+ self._resolve_variable(target_asset, var_name)
1161
+ )
1162
+
1163
+ if var_expr_chain is None:
1164
+ raise LookupError(
1165
+ f'Failed to find variable "{step_expression["name"]}" '
1166
+ f'for {target_asset.name}',
1167
+ )
1168
+
1169
+ return (
1170
+ var_target_asset,
1171
+ var_expr_chain,
1172
+ None
1173
+ )
1174
+
1175
+ def process_field_step_expression(
1176
+ self,
1177
+ target_asset: LanguageGraphAsset,
1178
+ step_expression: dict[str, Any]
1179
+ ) -> tuple[
1180
+ LanguageGraphAsset,
1181
+ ExpressionsChain,
1182
+ None
1183
+ ]:
1184
+ """
1185
+ Change the target asset from the current one to the associated
1186
+ asset given the specified field name and add the parent
1187
+ fieldname and association to the parent associations chain.
1188
+ """
1189
+
1190
+ fieldname = step_expression['name']
1191
+
1192
+ if target_asset is None:
1193
+ raise ValueError(
1194
+ f'Missing target asset for field "{fieldname}"!'
1195
+ )
1196
+
1197
+ new_target_asset = None
1198
+ for association in target_asset.associations.values():
1199
+ if (association.left_field.fieldname == fieldname and \
1200
+ target_asset.is_subasset_of(
1201
+ association.right_field.asset)):
1202
+ new_target_asset = association.left_field.asset
1203
+
1204
+ if (association.right_field.fieldname == fieldname and \
1205
+ target_asset.is_subasset_of(
1206
+ association.left_field.asset)):
1207
+ new_target_asset = association.right_field.asset
1208
+
1209
+ if new_target_asset:
1210
+ new_expr_chain = ExpressionsChain(
1211
+ type = 'field',
1212
+ fieldname = fieldname,
1213
+ association = association
1214
+ )
1215
+ return (
1216
+ new_target_asset,
1217
+ new_expr_chain,
1218
+ None
1219
+ )
1220
+
1221
+ raise LookupError(
1222
+ f'Failed to find field {fieldname} on asset {target_asset.name}!',
1223
+ )
1224
+
1225
+ def process_transitive_step_expression(
1226
+ self,
1227
+ target_asset: LanguageGraphAsset,
1228
+ expr_chain: Optional[ExpressionsChain],
1229
+ step_expression: dict[str, Any]
1230
+ ) -> tuple[
1231
+ LanguageGraphAsset,
1232
+ ExpressionsChain,
1233
+ None
1234
+ ]:
1235
+ """
1236
+ Create a transitive tuple entry that applies to the next
1237
+ component of the step expression.
1238
+ """
1239
+ result_target_asset, result_expr_chain, _ = (
1240
+ self.process_step_expression(
1241
+ target_asset,
1242
+ expr_chain,
1243
+ step_expression['stepExpression']
1244
+ )
1245
+ )
1246
+ new_expr_chain = ExpressionsChain(
1247
+ type = 'transitive',
1248
+ sub_link = result_expr_chain
1249
+ )
1250
+ return (
1251
+ result_target_asset,
1252
+ new_expr_chain,
1253
+ None
1254
+ )
1255
+
1256
+ def process_subType_step_expression(
1257
+ self,
1258
+ target_asset: LanguageGraphAsset,
1259
+ expr_chain: Optional[ExpressionsChain],
1260
+ step_expression: dict[str, Any]
1261
+ ) -> tuple[
1262
+ LanguageGraphAsset,
1263
+ ExpressionsChain,
1264
+ None
1265
+ ]:
1266
+ """
1267
+ Create a subType tuple entry that applies to the next
1268
+ component of the step expression and changes the target
1269
+ asset to the subasset.
1270
+ """
1271
+
1272
+ subtype_name = step_expression['subType']
1273
+ result_target_asset, result_expr_chain, _ = (
1274
+ self.process_step_expression(
1275
+ target_asset,
1276
+ expr_chain,
1277
+ step_expression['stepExpression']
1278
+ )
1279
+ )
1280
+
1281
+ if subtype_name not in self.assets:
1282
+ raise LanguageGraphException(
1283
+ f'Failed to find subtype {subtype_name}'
1284
+ )
1285
+
1286
+ subtype_asset = self.assets[subtype_name]
1287
+
1288
+ if result_target_asset is None:
1289
+ raise LookupError("Nonexisting asset for subtype")
1290
+
1291
+ if not subtype_asset.is_subasset_of(result_target_asset):
1292
+ raise ValueError(
1293
+ f'Found subtype {subtype_name} which does not extend '
1294
+ f'{result_target_asset.name}, subtype cannot be resolved.'
1295
+ )
1296
+
1297
+ new_expr_chain = ExpressionsChain(
1298
+ type = 'subType',
1299
+ sub_link = result_expr_chain,
1300
+ subtype = subtype_asset
1301
+ )
1302
+ return (
1303
+ subtype_asset,
1304
+ new_expr_chain,
1305
+ None
1306
+ )
1307
+
1308
+ def process_collect_step_expression(
1309
+ self,
1310
+ target_asset: LanguageGraphAsset,
1311
+ expr_chain: Optional[ExpressionsChain],
1312
+ step_expression: dict[str, Any]
1313
+ ) -> tuple[
1314
+ LanguageGraphAsset,
1315
+ Optional[ExpressionsChain],
1316
+ Optional[str]
1317
+ ]:
1318
+ """
1319
+ Apply the right hand step expression to left hand step
1320
+ expression target asset and parent associations chain.
1321
+ """
1322
+ lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
1323
+ target_asset, expr_chain, step_expression['lhs']
1324
+ )
1325
+
1326
+ if lh_target_asset is None:
1327
+ raise ValueError(
1328
+ 'No left hand asset in collect expression '
1329
+ f'{step_expression["lhs"]}'
1330
+ )
1331
+
1332
+ rh_target_asset, rh_expr_chain, rh_attack_step_name = (
1333
+ self.process_step_expression(
1334
+ lh_target_asset, None, step_expression['rhs']
1335
+ )
1336
+ )
1337
+
1338
+ new_expr_chain = lh_expr_chain
1339
+ if rh_expr_chain:
1340
+ new_expr_chain = ExpressionsChain(
1341
+ type = 'collect',
1342
+ left_link = lh_expr_chain,
1343
+ right_link = rh_expr_chain
1344
+ )
1345
+
1346
+ return (
1347
+ rh_target_asset,
1348
+ new_expr_chain,
1349
+ rh_attack_step_name
1350
+ )
1351
+
1352
+ def process_step_expression(self,
1353
+ target_asset: LanguageGraphAsset,
1354
+ expr_chain: Optional[ExpressionsChain],
975
1355
  step_expression: dict
976
- ) -> tuple:
1356
+ ) -> tuple[
1357
+ LanguageGraphAsset,
1358
+ Optional[ExpressionsChain],
1359
+ Optional[str]
1360
+ ]:
977
1361
  """
978
1362
  Recursively process an attack step expression.
979
1363
 
@@ -1002,210 +1386,46 @@ class LanguageGraph():
1002
1386
  json.dumps(step_expression, indent = 2)
1003
1387
  )
1004
1388
 
1389
+ result: tuple[
1390
+ LanguageGraphAsset,
1391
+ Optional[ExpressionsChain],
1392
+ Optional[str]
1393
+ ]
1394
+
1005
1395
  match (step_expression['type']):
1006
1396
  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']
1397
+ result = self.process_attack_step_expression(
1398
+ target_asset, step_expression
1014
1399
  )
1015
-
1016
1400
  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
1401
+ result = self.process_set_operation_step_expression(
1402
+ target_asset, expr_chain, step_expression
1044
1403
  )
1045
- return (
1046
- lh_target_asset,
1047
- new_expr_chain,
1048
- None
1049
- )
1050
-
1051
1404
  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
-
1405
+ result = self.process_variable_step_expression(
1406
+ target_asset, step_expression
1407
+ )
1070
1408
  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
1409
+ result = self.process_field_step_expression(
1410
+ target_asset, step_expression
1107
1411
  )
1108
- return (None, None, None)
1109
-
1110
1412
  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
1413
+ result = self.process_transitive_step_expression(
1414
+ target_asset, expr_chain, step_expression
1124
1415
  )
1125
- return (
1126
- result_target_asset,
1127
- new_expr_chain,
1128
- attack_step
1129
- )
1130
-
1131
1416
  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
1417
+ result = self.process_subType_step_expression(
1418
+ target_asset, expr_chain, step_expression
1164
1419
  )
1165
- return (
1166
- subtype_asset,
1167
- new_expr_chain,
1168
- attack_step
1169
- )
1170
-
1171
1420
  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
1421
+ result = self.process_collect_step_expression(
1422
+ target_asset, expr_chain, step_expression
1201
1423
  )
1202
-
1203
1424
  case _:
1204
- logger.error(
1205
- 'Unknown attack step type: "%s"', step_expression["type"]
1425
+ raise LookupError(
1426
+ f'Unknown attack step type: "{step_expression["type"]}"'
1206
1427
  )
1207
- return (None, None, None)
1208
-
1428
+ return result
1209
1429
 
1210
1430
  def reverse_expr_chain(
1211
1431
  self,
@@ -1300,7 +1520,7 @@ class LanguageGraph():
1300
1520
  logger.error(msg, expr_chain.type)
1301
1521
  raise LanguageGraphAssociationError(msg % expr_chain.type)
1302
1522
 
1303
- def _resolve_variable(self, asset, var_name) -> tuple:
1523
+ def _resolve_variable(self, asset: LanguageGraphAsset, var_name) -> tuple:
1304
1524
  """
1305
1525
  Resolve a variable for a specific asset by variable name.
1306
1526
 
@@ -1323,35 +1543,75 @@ class LanguageGraph():
1323
1543
  return (target_asset, expr_chain)
1324
1544
  return asset.variables[var_name]
1325
1545
 
1326
-
1327
- def _generate_graph(self) -> None:
1328
- """
1329
- Generate language graph starting from the MAL language specification
1330
- given in the constructor.
1546
+ def _create_associations_for_assets(
1547
+ self,
1548
+ lang_spec: dict[str, Any],
1549
+ assets: dict[str, LanguageGraphAsset]
1550
+ ) -> None:
1551
+ """ Link associations to assets based on the language specification.
1552
+ Arguments:
1553
+ lang_spec - the language specification dictionary
1554
+ assets - a dictionary of LanguageGraphAsset objects
1555
+ indexed by their names
1331
1556
  """
1332
- # Generate all of the asset nodes of the language graph.
1333
- for asset_dict in self._lang_spec['assets']:
1557
+
1558
+ for association_dict in lang_spec['associations']:
1334
1559
  logger.debug(
1335
- 'Create asset language graph nodes for asset %s',
1336
- asset_dict['name']
1560
+ 'Create association language graph nodes for association %s',
1561
+ association_dict['name']
1337
1562
  )
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']
1563
+
1564
+ left_asset_name = association_dict['leftAsset']
1565
+ right_asset_name = association_dict['rightAsset']
1566
+
1567
+ if left_asset_name not in assets:
1568
+ raise LanguageGraphAssociationError(
1569
+ f'Left asset "{left_asset_name}" for '
1570
+ f'association "{association_dict["name"]}" not found!'
1571
+ )
1572
+ if right_asset_name not in assets:
1573
+ raise LanguageGraphAssociationError(
1574
+ f'Right asset "{right_asset_name}" for '
1575
+ f'association "{association_dict["name"]}" not found!'
1576
+ )
1577
+
1578
+ left_asset = assets[left_asset_name]
1579
+ right_asset = assets[right_asset_name]
1580
+
1581
+ assoc_node = LanguageGraphAssociation(
1582
+ name = association_dict['name'],
1583
+ left_field = LanguageGraphAssociationField(
1584
+ left_asset,
1585
+ association_dict['leftField'],
1586
+ association_dict['leftMultiplicity']['min'],
1587
+ association_dict['leftMultiplicity']['max']
1588
+ ),
1589
+ right_field = LanguageGraphAssociationField(
1590
+ right_asset,
1591
+ association_dict['rightField'],
1592
+ association_dict['rightMultiplicity']['min'],
1593
+ association_dict['rightMultiplicity']['max']
1594
+ ),
1595
+ info = association_dict['meta']
1347
1596
  )
1348
- self.assets[asset_dict['name']] = asset_node
1349
1597
 
1350
- # Link assets based on inheritance
1351
- for asset_dict in self._lang_spec['assets']:
1352
- asset = self.assets[asset_dict['name']]
1598
+ # Add the association to the left and right asset
1599
+ self._link_association_to_assets(
1600
+ assoc_node, left_asset, right_asset
1601
+ )
1602
+
1603
+ def _link_assets(
1604
+ self,
1605
+ lang_spec: dict[str, Any],
1606
+ assets: dict[str, LanguageGraphAsset]
1607
+ ) -> None:
1608
+ """
1609
+ Link assets based on inheritance and associations.
1610
+ """
1611
+ for asset_dict in lang_spec['assets']:
1612
+ asset = assets[asset_dict['name']]
1353
1613
  if asset_dict['superAsset']:
1354
- super_asset = self.assets[asset_dict['superAsset']]
1614
+ super_asset = assets[asset_dict['superAsset']]
1355
1615
  if not super_asset:
1356
1616
  msg = 'Failed to find super asset "%s" for asset "%s"!'
1357
1617
  logger.error(
@@ -1362,54 +1622,21 @@ class LanguageGraph():
1362
1622
  super_asset.own_sub_assets.add(asset)
1363
1623
  asset.own_super_asset = super_asset
1364
1624
 
1365
- # Generate all of the association nodes of the language graph.
1366
- for asset in self.assets.values():
1625
+ def _set_variables_for_assets(
1626
+ self, assets: dict[str, LanguageGraphAsset]
1627
+ ) -> None:
1628
+ """ Set the variables for each asset based on the language specification.
1629
+ Arguments:
1630
+ assets - a dictionary of LanguageGraphAsset objects
1631
+ indexed by their names
1632
+ """
1633
+
1634
+ for asset in assets.values():
1367
1635
  logger.debug(
1368
- 'Create association language graph nodes for asset %s',
1369
- asset.name
1636
+ 'Set variables for asset %s', asset.name
1370
1637
  )
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):
1638
+ variables = self._get_variables_for_asset_type(asset.name)
1639
+ for variable in variables:
1413
1640
  if logger.isEnabledFor(logging.DEBUG):
1414
1641
  # Avoid running json.dumps when not in debug
1415
1642
  logger.debug(
@@ -1418,9 +1645,13 @@ class LanguageGraph():
1418
1645
  )
1419
1646
  self._resolve_variable(asset, variable['name'])
1420
1647
 
1421
-
1422
- # Generate all of the attack step nodes of the language graph.
1423
- for asset in self.assets.values():
1648
+ def _generate_attack_steps(self, assets) -> None:
1649
+ """
1650
+ Generate all of the attack steps for each asset type
1651
+ based on the language specification.
1652
+ """
1653
+ langspec_dict = {}
1654
+ for asset in assets.values():
1424
1655
  logger.debug(
1425
1656
  'Create attack steps language graph nodes for asset %s',
1426
1657
  asset.name
@@ -1432,28 +1663,32 @@ class LanguageGraph():
1432
1663
  attack_step_attribs['name']
1433
1664
  )
1434
1665
 
1666
+ ttc = self.replace_if_predef_ttc(attack_step_attribs['ttc'])
1435
1667
  attack_step_node = LanguageGraphAttackStep(
1436
1668
  name = attack_step_attribs['name'],
1437
1669
  type = attack_step_attribs['type'],
1438
1670
  asset = asset,
1439
- ttc = attack_step_attribs['ttc'],
1440
- overrides = attack_step_attribs['reaches']['overrides'] \
1441
- if attack_step_attribs['reaches'] else False,
1671
+ ttc = ttc,
1672
+ overrides = (
1673
+ attack_step_attribs['reaches']['overrides']
1674
+ if attack_step_attribs['reaches'] else False
1675
+ ),
1442
1676
  children = {},
1443
1677
  parents = {},
1444
1678
  info = attack_step_attribs['meta'],
1445
1679
  tags = set(attack_step_attribs['tags'])
1446
1680
  )
1447
- attack_step_node._attributes = attack_step_attribs
1448
- asset.attack_steps[attack_step_attribs['name']] = \
1681
+ langspec_dict[attack_step_node.full_name] = \
1682
+ attack_step_attribs
1683
+ asset.attack_steps[attack_step_node.name] = \
1449
1684
  attack_step_node
1450
1685
 
1451
- for detector in attack_step_attribs.get("detectors",
1452
- {}).values():
1686
+ detectors: dict = attack_step_attribs.get("detectors", {})
1687
+ for detector in detectors.values():
1453
1688
  attack_step_node.detectors[detector["name"]] = Detector(
1454
1689
  context=Context(
1455
1690
  {
1456
- label: self.assets[asset]
1691
+ label: assets[asset]
1457
1692
  for label, asset in detector["context"].items()
1458
1693
  }
1459
1694
  ),
@@ -1508,13 +1743,15 @@ class LanguageGraph():
1508
1743
  attack_step.name
1509
1744
  )
1510
1745
 
1511
- if attack_step._attributes is None:
1746
+ if attack_step.full_name not in langspec_dict:
1512
1747
  # This is simply an empty inherited attack step
1513
1748
  continue
1514
1749
 
1515
- step_expressions = \
1516
- attack_step._attributes['reaches']['stepExpressions'] if \
1517
- attack_step._attributes['reaches'] else []
1750
+ langspec_entry = langspec_dict[attack_step.full_name]
1751
+ step_expressions = (
1752
+ langspec_entry['reaches']['stepExpressions']
1753
+ if langspec_entry['reaches'] else []
1754
+ )
1518
1755
 
1519
1756
  for step_expression in step_expressions:
1520
1757
  # Resolve each of the attack step expressions listed for
@@ -1549,65 +1786,94 @@ class LanguageGraph():
1549
1786
  target_attack_step_name]
1550
1787
 
1551
1788
  # 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)]
1789
+ attack_step.children.setdefault(target_attack_step.full_name, [])
1790
+ attack_step.children[target_attack_step.full_name].append(
1791
+ (target_attack_step, expr_chain)
1792
+ )
1793
+
1558
1794
  # Reverse the children associations chains to get the
1559
1795
  # 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))]
1796
+ target_attack_step.parents.setdefault(attack_step.full_name, [])
1797
+ target_attack_step.parents[attack_step.full_name].append(
1798
+ (attack_step, self.reverse_expr_chain(expr_chain, None))
1799
+ )
1570
1800
 
1571
1801
  # 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 []
1802
+ if attack_step.type in ('exist', 'notExist'):
1803
+ step_expressions = (
1804
+ langspec_entry['requires']['stepExpressions']
1805
+ if langspec_entry['requires'] else []
1806
+ )
1577
1807
  if not step_expressions:
1578
- msg = 'Failed to find requirements for attack step' \
1579
- ' "%s" of type "%s":\n%s'
1580
1808
  raise LanguageGraphStepExpressionError(
1581
- msg % (
1809
+ 'Failed to find requirements for attack step'
1810
+ ' "%s" of type "%s":\n%s' % (
1582
1811
  attack_step.name,
1583
1812
  attack_step.type,
1584
- json.dumps(attack_step._attributes, indent = 2)
1813
+ json.dumps(langspec_entry, indent = 2)
1585
1814
  )
1586
1815
  )
1587
1816
 
1588
- attack_step.own_requires = []
1589
1817
  for step_expression in step_expressions:
1590
- _, \
1591
- result_expr_chain, \
1592
- _ = \
1818
+ _, result_expr_chain, _ = \
1593
1819
  self.process_step_expression(
1594
1820
  attack_step.asset,
1595
1821
  None,
1596
1822
  step_expression
1597
1823
  )
1824
+ if result_expr_chain is None:
1825
+ raise LanguageGraphException('Failed to find '
1826
+ 'existence step requirement for step '
1827
+ f'expression:\n%s' % step_expression)
1598
1828
  attack_step.own_requires.append(result_expr_chain)
1599
1829
 
1600
- def _get_attacks_for_asset_type(self, asset_type: str) -> dict:
1830
+ def _generate_graph(self) -> None:
1831
+ """
1832
+ Generate language graph starting from the MAL language specification
1833
+ given in the constructor.
1834
+ """
1835
+ # Generate all of the asset nodes of the language graph.
1836
+ self.assets = {}
1837
+ for asset_dict in self._lang_spec['assets']:
1838
+ logger.debug(
1839
+ 'Create asset language graph nodes for asset %s',
1840
+ asset_dict['name']
1841
+ )
1842
+ asset_node = LanguageGraphAsset(
1843
+ name = asset_dict['name'],
1844
+ own_associations = {},
1845
+ attack_steps = {},
1846
+ info = asset_dict['meta'],
1847
+ own_super_asset = None,
1848
+ own_sub_assets = set(),
1849
+ own_variables = {},
1850
+ is_abstract = asset_dict['isAbstract']
1851
+ )
1852
+ self.assets[asset_dict['name']] = asset_node
1853
+
1854
+ # Link assets to each other
1855
+ self._link_assets(self._lang_spec, self.assets)
1856
+
1857
+ # Add and link associations to assets
1858
+ self._create_associations_for_assets(self._lang_spec, self.assets)
1859
+
1860
+ # Set the variables for each asset
1861
+ self._set_variables_for_assets(self.assets)
1862
+
1863
+ # Add attack steps to the assets
1864
+ self._generate_attack_steps(self.assets)
1865
+
1866
+ def _get_attacks_for_asset_type(self, asset_type: str) -> dict[str, dict]:
1601
1867
  """
1602
- Get all Attack Steps for a specific Class
1868
+ Get all Attack Steps for a specific asset type.
1603
1869
 
1604
1870
  Arguments:
1605
- asset_type - a string representing the class for which we want to
1606
- list the possible attack steps
1871
+ asset_type - the name of the asset type we want to
1872
+ list the possible attack steps for
1607
1873
 
1608
1874
  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
1875
+ A dictionary containing the possible attacks for the
1876
+ specified asset type. Each key in the dictionary is an attack name
1611
1877
  associated with a dictionary containing other characteristics of the
1612
1878
  attack such as type of attack, TTC distribution, child attack steps
1613
1879
  and other information
@@ -1634,20 +1900,18 @@ class LanguageGraph():
1634
1900
 
1635
1901
  return attack_steps
1636
1902
 
1637
- def _get_associations_for_asset_type(self, asset_type: str) -> list:
1903
+ def _get_associations_for_asset_type(self, asset_type: str) -> list[dict]:
1638
1904
  """
1639
- Get all Associations for a specific Class
1905
+ Get all associations for a specific asset type.
1640
1906
 
1641
1907
  Arguments:
1642
- asset_type - a string representing the class for which we want to
1908
+ asset_type - the name of the asset type for which we want to
1643
1909
  list the associations
1644
1910
 
1645
1911
  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
1912
+ A list of dicts, where each dict represents an associations
1913
+ for the specified asset type. Each dictionary contains
1914
+ name and meta information about the association.
1651
1915
  """
1652
1916
  logger.debug(
1653
1917
  'Get associations for %s asset from '
@@ -1668,25 +1932,25 @@ class LanguageGraph():
1668
1932
  if assoc['leftAsset'] == asset_type or \
1669
1933
  assoc['rightAsset'] == asset_type)
1670
1934
  assoc = next(assoc_iter, None)
1671
- while (assoc):
1935
+ while assoc:
1672
1936
  associations.append(assoc)
1673
1937
  assoc = next(assoc_iter, None)
1674
1938
 
1675
1939
  return associations
1676
1940
 
1677
1941
  def _get_variables_for_asset_type(
1678
- self, asset_type: str) -> dict:
1942
+ self, asset_type: str) -> list[dict]:
1679
1943
  """
1680
- Get a variables for a specific asset type by name.
1944
+ Get variables for a specific asset type.
1681
1945
  Note: Variables are the ones specified in MAL through `let` statements
1682
1946
 
1683
1947
  Arguments:
1684
- asset_type - a string representing the type of asset which
1948
+ asset_type - a string representing the asset type which
1685
1949
  contains the variables
1686
1950
 
1687
1951
  Return:
1688
- A dictionary representing the step expressions for the variables
1689
- belonging to the asset.
1952
+ A list of dicts representing the step expressions for the variables
1953
+ belonging to the asset.
1690
1954
  """
1691
1955
 
1692
1956
  asset_dict = next((asset for asset in self._lang_spec['assets'] \
@@ -1733,5 +1997,3 @@ class LanguageGraph():
1733
1997
 
1734
1998
  self.assets = {}
1735
1999
  self._generate_graph()
1736
-
1737
-