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