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.
- {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/METADATA +2 -2
- mal_toolbox-2.1.0.dist-info/RECORD +51 -0
- {mal_toolbox-2.0.0.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_getters.py +36 -0
- maltoolbox/attackgraph/ttcs.py +28 -0
- maltoolbox/language/__init__.py +2 -2
- maltoolbox/language/compiler/mal_compiler.py +4 -3
- 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 -1537
- maltoolbox/language/step_expression_processor.py +491 -0
- mal_toolbox-2.0.0.dist-info/RECORD +0 -36
- {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/entry_points.txt +0 -0
- {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/AUTHORS +0 -0
- {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
29
|
+
def __init__(self, lang_spec: dict | None = None):
|
|
30
|
+
|
|
687
31
|
self.assets: dict[str, LanguageGraphAsset] = {}
|
|
688
|
-
|
|
689
|
-
|
|
32
|
+
self.lang_spec = lang_spec
|
|
33
|
+
|
|
34
|
+
if self.lang_spec is not None:
|
|
690
35
|
self.metadata = {
|
|
691
|
-
"version":
|
|
692
|
-
"id":
|
|
36
|
+
"version": self.lang_spec["defines"]["version"],
|
|
37
|
+
"id": self.lang_spec["defines"]["id"],
|
|
693
38
|
}
|
|
694
|
-
self.
|
|
39
|
+
self.assets = generate_graph(self.lang_spec)
|
|
695
40
|
|
|
696
41
|
def __repr__(self) -> str:
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
995
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1073
|
-
self.
|
|
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
|
-
|
|
1104
|
-
|
|
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
|
-
|
|
1153
|
-
target_asset, expr_chain, step_expression
|
|
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
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
-
|
|
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
|
|
1381
|
-
|
|
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
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
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
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
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
|
-
|
|
1415
|
-
|
|
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=
|
|
363
|
+
name=assoc['name'],
|
|
1419
364
|
left_field=LanguageGraphAssociationField(
|
|
1420
|
-
|
|
1421
|
-
|
|
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
|
-
|
|
1427
|
-
|
|
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=
|
|
372
|
+
info=assoc['info']
|
|
1432
373
|
)
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
)
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
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
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
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
|
-
|
|
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
|
-
|
|
469
|
+
Arguments:
|
|
470
|
+
---------
|
|
471
|
+
mal_spec_file - the path to the .mal file
|
|
1752
472
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
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
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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
|
-
|
|
482
|
+
Arguments:
|
|
483
|
+
---------
|
|
484
|
+
mar_archive - the path to a ".mar" archive
|
|
1769
485
|
|
|
1770
|
-
|
|
1771
|
-
|
|
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()
|