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.
- {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/METADATA +8 -75
- mal_toolbox-2.1.0.dist-info/RECORD +51 -0
- {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/WHEEL +1 -1
- maltoolbox/__init__.py +2 -2
- maltoolbox/attackgraph/__init__.py +2 -2
- maltoolbox/attackgraph/attackgraph.py +121 -549
- maltoolbox/attackgraph/factories.py +68 -0
- maltoolbox/attackgraph/file_utils.py +0 -0
- maltoolbox/attackgraph/generate.py +338 -0
- maltoolbox/attackgraph/node.py +1 -0
- maltoolbox/attackgraph/node_getters.py +36 -0
- maltoolbox/attackgraph/ttcs.py +28 -0
- maltoolbox/language/__init__.py +2 -2
- maltoolbox/language/compiler/__init__.py +4 -499
- maltoolbox/language/compiler/distributions.py +158 -0
- maltoolbox/language/compiler/exceptions.py +37 -0
- maltoolbox/language/compiler/lang.py +5 -0
- maltoolbox/language/compiler/mal_analyzer.py +920 -0
- maltoolbox/language/compiler/mal_compiler.py +1071 -0
- maltoolbox/language/detector.py +43 -0
- maltoolbox/language/expression_chain.py +218 -0
- maltoolbox/language/language_graph_asset.py +180 -0
- maltoolbox/language/language_graph_assoc.py +147 -0
- maltoolbox/language/language_graph_attack_step.py +129 -0
- maltoolbox/language/language_graph_builder.py +282 -0
- maltoolbox/language/language_graph_loaders.py +7 -0
- maltoolbox/language/language_graph_lookup.py +140 -0
- maltoolbox/language/language_graph_serialization.py +5 -0
- maltoolbox/language/languagegraph.py +244 -1536
- maltoolbox/language/step_expression_processor.py +491 -0
- mal_toolbox-1.2.1.dist-info/RECORD +0 -33
- maltoolbox/language/compiler/mal_lexer.py +0 -232
- maltoolbox/language/compiler/mal_parser.py +0 -3159
- {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/entry_points.txt +0 -0
- {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/AUTHORS +0 -0
- {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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,
|
|
29
|
+
def __init__(self, lang_spec: dict | None = None):
|
|
30
|
+
|
|
689
31
|
self.assets: dict[str, LanguageGraphAsset] = {}
|
|
690
|
-
|
|
691
|
-
|
|
32
|
+
self.lang_spec = lang_spec
|
|
33
|
+
|
|
34
|
+
if self.lang_spec is not None:
|
|
692
35
|
self.metadata = {
|
|
693
|
-
"version":
|
|
694
|
-
"id":
|
|
36
|
+
"version": self.lang_spec["defines"]["version"],
|
|
37
|
+
"id": self.lang_spec["defines"]["id"],
|
|
695
38
|
}
|
|
696
|
-
self.
|
|
39
|
+
self.assets = generate_graph(self.lang_spec)
|
|
697
40
|
|
|
698
41
|
def __repr__(self) -> str:
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
996
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1074
|
-
self.
|
|
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
|
-
|
|
1105
|
-
|
|
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
|
-
|
|
1154
|
-
target_asset, expr_chain, step_expression
|
|
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
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
-
|
|
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
|
|
1382
|
-
|
|
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
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
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
|
-
|
|
1416
|
-
|
|
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=
|
|
363
|
+
name=assoc['name'],
|
|
1420
364
|
left_field=LanguageGraphAssociationField(
|
|
1421
|
-
|
|
1422
|
-
|
|
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
|
-
|
|
1428
|
-
|
|
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=
|
|
372
|
+
info=assoc['info']
|
|
1433
373
|
)
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
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
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
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
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
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
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
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
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1743
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
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
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
482
|
+
Arguments:
|
|
483
|
+
---------
|
|
484
|
+
mar_archive - the path to a ".mar" archive
|
|
1765
485
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
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()
|