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