mal-toolbox 1.1.1__py3-none-any.whl → 1.1.3__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.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/METADATA +25 -2
- mal_toolbox-1.1.3.dist-info/RECORD +32 -0
- maltoolbox/__init__.py +6 -7
- maltoolbox/__main__.py +17 -9
- maltoolbox/attackgraph/__init__.py +2 -3
- maltoolbox/attackgraph/attackgraph.py +379 -362
- maltoolbox/attackgraph/node.py +14 -19
- maltoolbox/exceptions.py +7 -10
- maltoolbox/file_utils.py +10 -4
- maltoolbox/language/__init__.py +1 -1
- maltoolbox/language/compiler/__init__.py +4 -4
- maltoolbox/language/compiler/mal_lexer.py +154 -154
- maltoolbox/language/compiler/mal_parser.py +784 -1136
- maltoolbox/language/languagegraph.py +487 -639
- maltoolbox/model.py +64 -77
- maltoolbox/patternfinder/attackgraph_patterns.py +17 -8
- maltoolbox/translators/__init__.py +8 -0
- maltoolbox/translators/networkx.py +42 -0
- maltoolbox/translators/updater.py +18 -25
- maltoolbox/visualization/__init__.py +4 -4
- maltoolbox/visualization/draw_io_utils.py +6 -5
- maltoolbox/visualization/graphviz_utils.py +4 -2
- maltoolbox/visualization/neo4j_utils.py +13 -14
- maltoolbox/visualization/utils.py +2 -3
- mal_toolbox-1.1.1.dist-info/RECORD +0 -32
- maltoolbox/translators/securicad.py +0 -179
- {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/WHEEL +0 -0
- {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/entry_points.txt +0 -0
- {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/licenses/AUTHORS +0 -0
- {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/licenses/LICENSE +0 -0
- {mal_toolbox-1.1.1.dist-info → mal_toolbox-1.1.3.dist-info}/top_level.txt +0 -0
|
@@ -1,43 +1,45 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MAL-Toolbox Language Graph Module
|
|
1
|
+
"""MAL-Toolbox Language Graph Module
|
|
3
2
|
"""
|
|
4
3
|
|
|
5
4
|
from __future__ import annotations
|
|
6
5
|
|
|
7
|
-
import logging
|
|
8
6
|
import json
|
|
7
|
+
import logging
|
|
9
8
|
import zipfile
|
|
10
|
-
|
|
11
9
|
from dataclasses import dataclass, field
|
|
12
10
|
from functools import cached_property
|
|
13
|
-
from typing import Any, Literal
|
|
11
|
+
from typing import Any, Literal
|
|
14
12
|
|
|
15
13
|
from maltoolbox.file_utils import (
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
load_dict_from_json_file,
|
|
15
|
+
load_dict_from_yaml_file,
|
|
16
|
+
save_dict_to_file,
|
|
18
17
|
)
|
|
19
|
-
|
|
18
|
+
|
|
20
19
|
from ..exceptions import (
|
|
21
20
|
LanguageGraphAssociationError,
|
|
22
|
-
LanguageGraphStepExpressionError,
|
|
23
21
|
LanguageGraphException,
|
|
24
|
-
|
|
22
|
+
LanguageGraphStepExpressionError,
|
|
23
|
+
LanguageGraphSuperAssetNotFoundError,
|
|
25
24
|
)
|
|
25
|
+
from .compiler import MalCompiler
|
|
26
26
|
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def disaggregate_attack_step_full_name(
|
|
31
|
-
|
|
31
|
+
attack_step_full_name: str
|
|
32
|
+
) -> list[str]:
|
|
33
|
+
"""From an attack step full name, get (asset_name, attack_step_name)"""
|
|
32
34
|
return attack_step_full_name.split(':')
|
|
33
35
|
|
|
34
36
|
|
|
35
|
-
@dataclass
|
|
37
|
+
@dataclass(frozen=True, eq=True)
|
|
36
38
|
class Detector:
|
|
37
|
-
name:
|
|
39
|
+
name: str | None
|
|
38
40
|
context: Context
|
|
39
|
-
type:
|
|
40
|
-
tprate:
|
|
41
|
+
type: str | None
|
|
42
|
+
tprate: dict | None
|
|
41
43
|
|
|
42
44
|
def to_dict(self) -> dict:
|
|
43
45
|
return {
|
|
@@ -50,6 +52,7 @@ class Detector:
|
|
|
50
52
|
|
|
51
53
|
class Context(dict):
|
|
52
54
|
"""Context is part of detectors to provide meta data about attackers"""
|
|
55
|
+
|
|
53
56
|
def __init__(self, context) -> None:
|
|
54
57
|
super().__init__(context)
|
|
55
58
|
self._context_dict = context
|
|
@@ -63,22 +66,23 @@ class Context(dict):
|
|
|
63
66
|
return str({label: asset.name for label, asset in self._context_dict.items()})
|
|
64
67
|
|
|
65
68
|
def __repr__(self) -> str:
|
|
66
|
-
return f"Context({
|
|
69
|
+
return f"Context({self!s}))"
|
|
70
|
+
|
|
67
71
|
|
|
68
72
|
@dataclass
|
|
69
73
|
class LanguageGraphAsset:
|
|
70
74
|
"""An asset type as defined in the MAL language"""
|
|
75
|
+
|
|
71
76
|
name: str
|
|
72
77
|
own_associations: dict[str, LanguageGraphAssociation] = \
|
|
73
|
-
field(default_factory
|
|
78
|
+
field(default_factory=dict)
|
|
74
79
|
attack_steps: dict[str, LanguageGraphAttackStep] = \
|
|
75
|
-
field(default_factory
|
|
76
|
-
info: dict = field(default_factory
|
|
77
|
-
own_super_asset:
|
|
78
|
-
own_sub_assets:
|
|
79
|
-
own_variables: dict = field(default_factory
|
|
80
|
-
is_abstract:
|
|
81
|
-
|
|
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
|
|
82
86
|
|
|
83
87
|
def to_dict(self) -> dict:
|
|
84
88
|
"""Convert LanguageGraphAsset to dictionary"""
|
|
@@ -87,7 +91,7 @@ class LanguageGraphAsset:
|
|
|
87
91
|
'associations': {},
|
|
88
92
|
'attack_steps': {},
|
|
89
93
|
'info': self.info,
|
|
90
|
-
'super_asset': self.own_super_asset.name
|
|
94
|
+
'super_asset': self.own_super_asset.name
|
|
91
95
|
if self.own_super_asset else "",
|
|
92
96
|
'sub_assets': [asset.name for asset in self.own_sub_assets],
|
|
93
97
|
'variables': {},
|
|
@@ -107,43 +111,42 @@ class LanguageGraphAsset:
|
|
|
107
111
|
)
|
|
108
112
|
return node_dict
|
|
109
113
|
|
|
110
|
-
|
|
111
114
|
def __repr__(self) -> str:
|
|
112
115
|
return f'LanguageGraphAsset(name: "{self.name}")'
|
|
113
116
|
|
|
114
|
-
|
|
115
117
|
def __hash__(self):
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
+
return id(self)
|
|
118
119
|
|
|
119
120
|
def is_subasset_of(self, target_asset: LanguageGraphAsset) -> bool:
|
|
120
|
-
"""
|
|
121
|
-
Check if an asset extends the target asset through inheritance.
|
|
121
|
+
"""Check if an asset extends the target asset through inheritance.
|
|
122
122
|
|
|
123
123
|
Arguments:
|
|
124
|
+
---------
|
|
124
125
|
target_asset - the target asset we wish to evaluate if this asset
|
|
125
126
|
extends
|
|
126
127
|
|
|
127
128
|
Return:
|
|
129
|
+
------
|
|
128
130
|
True if this asset extends the target_asset via inheritance.
|
|
129
131
|
False otherwise.
|
|
132
|
+
|
|
130
133
|
"""
|
|
131
|
-
current_asset:
|
|
134
|
+
current_asset: LanguageGraphAsset | None = self
|
|
132
135
|
while current_asset:
|
|
133
136
|
if current_asset == target_asset:
|
|
134
137
|
return True
|
|
135
138
|
current_asset = current_asset.own_super_asset
|
|
136
139
|
return False
|
|
137
140
|
|
|
138
|
-
|
|
139
141
|
@cached_property
|
|
140
142
|
def sub_assets(self) -> set[LanguageGraphAsset]:
|
|
141
|
-
"""
|
|
142
|
-
Return a list of all of the assets that directly or indirectly extend
|
|
143
|
+
"""Return a list of all of the assets that directly or indirectly extend
|
|
143
144
|
this asset.
|
|
144
145
|
|
|
145
146
|
Return:
|
|
147
|
+
------
|
|
146
148
|
A list of all of the assets that extend this asset plus itself.
|
|
149
|
+
|
|
147
150
|
"""
|
|
148
151
|
subassets: list[LanguageGraphAsset] = []
|
|
149
152
|
for subasset in self.own_sub_assets:
|
|
@@ -154,17 +157,17 @@ class LanguageGraphAsset:
|
|
|
154
157
|
|
|
155
158
|
return set(subassets)
|
|
156
159
|
|
|
157
|
-
|
|
158
160
|
@cached_property
|
|
159
161
|
def super_assets(self) -> list[LanguageGraphAsset]:
|
|
160
|
-
"""
|
|
161
|
-
Return a list of all of the assets that this asset directly or
|
|
162
|
+
"""Return a list of all of the assets that this asset directly or
|
|
162
163
|
indirectly extends.
|
|
163
164
|
|
|
164
165
|
Return:
|
|
166
|
+
------
|
|
165
167
|
A list of all of the assets that this asset extends plus itself.
|
|
168
|
+
|
|
166
169
|
"""
|
|
167
|
-
current_asset:
|
|
170
|
+
current_asset: LanguageGraphAsset | None = self
|
|
168
171
|
superassets = []
|
|
169
172
|
while current_asset:
|
|
170
173
|
superassets.append(current_asset)
|
|
@@ -174,8 +177,7 @@ class LanguageGraphAsset:
|
|
|
174
177
|
def associations_to(
|
|
175
178
|
self, asset_type: LanguageGraphAsset
|
|
176
179
|
) -> dict[str, LanguageGraphAssociation]:
|
|
177
|
-
"""
|
|
178
|
-
Return dict of association types that go from self
|
|
180
|
+
"""Return dict of association types that go from self
|
|
179
181
|
to given `asset_type`
|
|
180
182
|
"""
|
|
181
183
|
associations_to_asset_type = {}
|
|
@@ -186,45 +188,44 @@ class LanguageGraphAsset:
|
|
|
186
188
|
|
|
187
189
|
@cached_property
|
|
188
190
|
def associations(self) -> dict[str, LanguageGraphAssociation]:
|
|
189
|
-
"""
|
|
190
|
-
Return a list of all of the associations that belong to this asset
|
|
191
|
+
"""Return a list of all of the associations that belong to this asset
|
|
191
192
|
directly or indirectly via inheritance.
|
|
192
193
|
|
|
193
194
|
Return:
|
|
195
|
+
------
|
|
194
196
|
A list of all of the associations that apply to this asset, either
|
|
195
197
|
directly or via inheritance.
|
|
196
|
-
"""
|
|
197
198
|
|
|
199
|
+
"""
|
|
198
200
|
associations = dict(self.own_associations)
|
|
199
201
|
if self.own_super_asset:
|
|
200
202
|
associations |= self.own_super_asset.associations
|
|
201
203
|
return associations
|
|
202
204
|
|
|
203
|
-
|
|
204
205
|
@property
|
|
205
206
|
def variables(
|
|
206
207
|
self
|
|
207
208
|
) -> dict[str, tuple[LanguageGraphAsset, ExpressionsChain]]:
|
|
208
|
-
"""
|
|
209
|
-
Return a list of all of the variables that belong to this asset
|
|
209
|
+
"""Return a list of all of the variables that belong to this asset
|
|
210
210
|
directly or indirectly via inheritance.
|
|
211
211
|
|
|
212
212
|
Return:
|
|
213
|
+
------
|
|
213
214
|
A list of all of the variables that apply to this asset, either
|
|
214
215
|
directly or via inheritance.
|
|
215
|
-
"""
|
|
216
216
|
|
|
217
|
+
"""
|
|
217
218
|
all_vars = dict(self.own_variables)
|
|
218
219
|
if self.own_super_asset:
|
|
219
220
|
all_vars |= self.own_super_asset.variables
|
|
220
221
|
return all_vars
|
|
221
222
|
|
|
222
|
-
|
|
223
223
|
def get_all_common_superassets(
|
|
224
224
|
self, other: LanguageGraphAsset
|
|
225
225
|
) -> set[str]:
|
|
226
226
|
"""Return a set of all common ancestors between this asset
|
|
227
|
-
and the other asset given as parameter
|
|
227
|
+
and the other asset given as parameter
|
|
228
|
+
"""
|
|
228
229
|
self_superassets = set(
|
|
229
230
|
asset.name for asset in self.super_assets
|
|
230
231
|
)
|
|
@@ -234,9 +235,10 @@ class LanguageGraphAsset:
|
|
|
234
235
|
return self_superassets.intersection(other_superassets)
|
|
235
236
|
|
|
236
237
|
|
|
237
|
-
@dataclass(frozen=True)
|
|
238
|
+
@dataclass(frozen=True, eq=True)
|
|
238
239
|
class LanguageGraphAssociationField:
|
|
239
240
|
"""A field in an association"""
|
|
241
|
+
|
|
240
242
|
asset: LanguageGraphAsset
|
|
241
243
|
fieldname: str
|
|
242
244
|
minimum: int
|
|
@@ -245,13 +247,13 @@ class LanguageGraphAssociationField:
|
|
|
245
247
|
|
|
246
248
|
@dataclass(frozen=True, eq=True)
|
|
247
249
|
class LanguageGraphAssociation:
|
|
250
|
+
"""An association type between asset types as defined in the MAL language
|
|
248
251
|
"""
|
|
249
|
-
|
|
250
|
-
"""
|
|
252
|
+
|
|
251
253
|
name: str
|
|
252
254
|
left_field: LanguageGraphAssociationField
|
|
253
255
|
right_field: LanguageGraphAssociationField
|
|
254
|
-
info: dict = field(default_factory
|
|
256
|
+
info: dict = field(default_factory=dict, compare=False)
|
|
255
257
|
|
|
256
258
|
def to_dict(self) -> dict:
|
|
257
259
|
"""Convert LanguageGraphAssociation to dictionary"""
|
|
@@ -274,7 +276,6 @@ class LanguageGraphAssociation:
|
|
|
274
276
|
|
|
275
277
|
return assoc_dict
|
|
276
278
|
|
|
277
|
-
|
|
278
279
|
def __repr__(self) -> str:
|
|
279
280
|
return (
|
|
280
281
|
f'LanguageGraphAssociation(name: "{self.name}", '
|
|
@@ -282,39 +283,35 @@ class LanguageGraphAssociation:
|
|
|
282
283
|
f'right_field: {self.right_field})'
|
|
283
284
|
)
|
|
284
285
|
|
|
285
|
-
|
|
286
286
|
@property
|
|
287
287
|
def full_name(self) -> str:
|
|
288
|
-
"""
|
|
289
|
-
Return the full name of the association. This is a combination of the
|
|
288
|
+
"""Return the full name of the association. This is a combination of the
|
|
290
289
|
association name, left field name, left asset type, right field name,
|
|
291
290
|
and right asset type.
|
|
292
291
|
"""
|
|
293
292
|
full_name = '%s_%s_%s' % (
|
|
294
|
-
self.name
|
|
295
|
-
self.left_field.fieldname
|
|
293
|
+
self.name,
|
|
294
|
+
self.left_field.fieldname,
|
|
296
295
|
self.right_field.fieldname
|
|
297
296
|
)
|
|
298
297
|
return full_name
|
|
299
298
|
|
|
300
|
-
|
|
301
299
|
def get_field(self, fieldname: str) -> LanguageGraphAssociationField:
|
|
302
|
-
"""
|
|
303
|
-
Return the field that matches the `fieldname` given as parameter.
|
|
300
|
+
"""Return the field that matches the `fieldname` given as parameter.
|
|
304
301
|
"""
|
|
305
302
|
if self.right_field.fieldname == fieldname:
|
|
306
303
|
return self.right_field
|
|
307
304
|
return self.left_field
|
|
308
305
|
|
|
309
|
-
|
|
310
306
|
def contains_fieldname(self, fieldname: str) -> bool:
|
|
311
|
-
"""
|
|
312
|
-
Check if the association contains the field name given as a parameter.
|
|
307
|
+
"""Check if the association contains the field name given as a parameter.
|
|
313
308
|
|
|
314
309
|
Arguments:
|
|
310
|
+
---------
|
|
315
311
|
fieldname - the field name to look for
|
|
316
312
|
Return True if either of the two field names matches.
|
|
317
313
|
False, otherwise.
|
|
314
|
+
|
|
318
315
|
"""
|
|
319
316
|
if self.left_field.fieldname == fieldname:
|
|
320
317
|
return True
|
|
@@ -322,17 +319,17 @@ class LanguageGraphAssociation:
|
|
|
322
319
|
return True
|
|
323
320
|
return False
|
|
324
321
|
|
|
325
|
-
|
|
326
322
|
def contains_asset(self, asset: Any) -> bool:
|
|
327
|
-
"""
|
|
328
|
-
Check if the association matches the asset given as a parameter. A
|
|
323
|
+
"""Check if the association matches the asset given as a parameter. A
|
|
329
324
|
match can either be an explicit one or if the asset given subassets
|
|
330
325
|
either of the two assets that are part of the association.
|
|
331
326
|
|
|
332
327
|
Arguments:
|
|
328
|
+
---------
|
|
333
329
|
asset - the asset to look for
|
|
334
330
|
Return True if either of the two asset matches.
|
|
335
331
|
False, otherwise.
|
|
332
|
+
|
|
336
333
|
"""
|
|
337
334
|
if asset.is_subasset_of(self.left_field.asset):
|
|
338
335
|
return True
|
|
@@ -340,16 +337,16 @@ class LanguageGraphAssociation:
|
|
|
340
337
|
return True
|
|
341
338
|
return False
|
|
342
339
|
|
|
343
|
-
|
|
344
340
|
def get_opposite_fieldname(self, fieldname: str) -> str:
|
|
345
|
-
"""
|
|
346
|
-
Return the opposite field name if the association contains the field
|
|
341
|
+
"""Return the opposite field name if the association contains the field
|
|
347
342
|
name given as a parameter.
|
|
348
343
|
|
|
349
344
|
Arguments:
|
|
345
|
+
---------
|
|
350
346
|
fieldname - the field name to look for
|
|
351
347
|
Return the other field name if the parameter matched either of the
|
|
352
348
|
two. None, otherwise.
|
|
349
|
+
|
|
353
350
|
"""
|
|
354
351
|
if self.left_field.fieldname == fieldname:
|
|
355
352
|
return self.right_field.fieldname
|
|
@@ -364,39 +361,36 @@ class LanguageGraphAssociation:
|
|
|
364
361
|
|
|
365
362
|
@dataclass
|
|
366
363
|
class LanguageGraphAttackStep:
|
|
364
|
+
"""An attack step belonging to an asset type in the MAL language
|
|
367
365
|
"""
|
|
368
|
-
|
|
369
|
-
"""
|
|
366
|
+
|
|
370
367
|
name: str
|
|
371
368
|
type: Literal["or", "and", "defense", "exist", "notExist"]
|
|
372
369
|
asset: LanguageGraphAsset
|
|
373
|
-
ttc:
|
|
370
|
+
ttc: dict | None = field(default_factory=dict)
|
|
374
371
|
overrides: bool = False
|
|
375
372
|
|
|
376
373
|
own_children: dict[LanguageGraphAttackStep, list[ExpressionsChain | None]] = (
|
|
377
|
-
field(default_factory
|
|
374
|
+
field(default_factory=dict)
|
|
378
375
|
)
|
|
379
376
|
own_parents: dict[LanguageGraphAttackStep, list[ExpressionsChain | None]] = (
|
|
380
|
-
field(default_factory
|
|
377
|
+
field(default_factory=dict)
|
|
381
378
|
)
|
|
382
|
-
info: dict = field(default_factory
|
|
383
|
-
inherits:
|
|
379
|
+
info: dict = field(default_factory=dict)
|
|
380
|
+
inherits: LanguageGraphAttackStep | None = None
|
|
384
381
|
own_requires: list[ExpressionsChain] = field(default_factory=list)
|
|
385
|
-
tags: list = field(default_factory
|
|
386
|
-
detectors: dict = field(default_factory
|
|
387
|
-
|
|
382
|
+
tags: list = field(default_factory=list)
|
|
383
|
+
detectors: dict = field(default_factory=dict)
|
|
388
384
|
|
|
389
385
|
def __hash__(self):
|
|
390
|
-
return
|
|
386
|
+
return id(self)
|
|
391
387
|
|
|
392
388
|
@property
|
|
393
389
|
def children(self) -> dict[
|
|
394
390
|
LanguageGraphAttackStep, list[ExpressionsChain | None]
|
|
395
391
|
]:
|
|
392
|
+
"""Get all (both own and inherited) children of a LanguageGraphAttackStep
|
|
396
393
|
"""
|
|
397
|
-
Get all (both own and inherited) children of a LanguageGraphAttackStep
|
|
398
|
-
"""
|
|
399
|
-
|
|
400
394
|
all_children = dict(self.own_children)
|
|
401
395
|
|
|
402
396
|
if self.overrides:
|
|
@@ -425,8 +419,7 @@ class LanguageGraphAttackStep:
|
|
|
425
419
|
|
|
426
420
|
@property
|
|
427
421
|
def full_name(self) -> str:
|
|
428
|
-
"""
|
|
429
|
-
Return the full name of the attack step. This is a combination of the
|
|
422
|
+
"""Return the full name of the attack step. This is a combination of the
|
|
430
423
|
asset type name to which the attack step belongs and attack step name
|
|
431
424
|
itself.
|
|
432
425
|
"""
|
|
@@ -471,7 +464,6 @@ class LanguageGraphAttackStep:
|
|
|
471
464
|
|
|
472
465
|
return node_dict
|
|
473
466
|
|
|
474
|
-
|
|
475
467
|
@cached_property
|
|
476
468
|
def requires(self):
|
|
477
469
|
if not hasattr(self, 'own_requires'):
|
|
@@ -483,33 +475,31 @@ class LanguageGraphAttackStep:
|
|
|
483
475
|
requirements.extend(self.inherits.requires)
|
|
484
476
|
return requirements
|
|
485
477
|
|
|
486
|
-
|
|
487
478
|
def __repr__(self) -> str:
|
|
488
479
|
return str(self.to_dict())
|
|
489
480
|
|
|
490
481
|
|
|
491
482
|
class ExpressionsChain:
|
|
492
|
-
"""
|
|
493
|
-
A series of linked step expressions that specify the association path and
|
|
483
|
+
"""A series of linked step expressions that specify the association path and
|
|
494
484
|
operations to take to reach the child/parent attack step.
|
|
495
485
|
"""
|
|
486
|
+
|
|
496
487
|
def __init__(self,
|
|
497
488
|
type: str,
|
|
498
|
-
left_link:
|
|
499
|
-
right_link:
|
|
500
|
-
sub_link:
|
|
501
|
-
fieldname:
|
|
502
|
-
association
|
|
503
|
-
subtype
|
|
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
|
|
504
495
|
):
|
|
505
496
|
self.type = type
|
|
506
|
-
self.left_link:
|
|
507
|
-
self.right_link:
|
|
508
|
-
self.sub_link:
|
|
509
|
-
self.fieldname:
|
|
510
|
-
self.association:
|
|
511
|
-
self.subtype:
|
|
512
|
-
|
|
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
|
|
513
503
|
|
|
514
504
|
def to_dict(self) -> dict:
|
|
515
505
|
"""Convert ExpressionsChain to dictionary"""
|
|
@@ -540,7 +530,7 @@ class ExpressionsChain:
|
|
|
540
530
|
(
|
|
541
531
|
self.fieldname,
|
|
542
532
|
json.dumps(self.association.to_dict(),
|
|
543
|
-
indent
|
|
533
|
+
indent=2)
|
|
544
534
|
)
|
|
545
535
|
)
|
|
546
536
|
|
|
@@ -587,7 +577,7 @@ class ExpressionsChain:
|
|
|
587
577
|
def _from_dict(cls,
|
|
588
578
|
serialized_expr_chain: dict,
|
|
589
579
|
lang_graph: LanguageGraph,
|
|
590
|
-
) ->
|
|
580
|
+
) -> ExpressionsChain | None:
|
|
591
581
|
"""Create ExpressionsChain from dict
|
|
592
582
|
Args:
|
|
593
583
|
serialized_expr_chain - expressions chain in dict format
|
|
@@ -595,12 +585,11 @@ class ExpressionsChain:
|
|
|
595
585
|
associations, and attack steps relevant for
|
|
596
586
|
the expressions chain
|
|
597
587
|
"""
|
|
598
|
-
|
|
599
588
|
if serialized_expr_chain is None or not serialized_expr_chain:
|
|
600
589
|
return None
|
|
601
590
|
|
|
602
591
|
if 'type' not in serialized_expr_chain:
|
|
603
|
-
logger.debug(json.dumps(serialized_expr_chain, indent
|
|
592
|
+
logger.debug(json.dumps(serialized_expr_chain, indent=2))
|
|
604
593
|
msg = 'Missing expressions chain type!'
|
|
605
594
|
logger.error(msg)
|
|
606
595
|
raise LanguageGraphAssociationError(msg)
|
|
@@ -617,15 +606,15 @@ class ExpressionsChain:
|
|
|
617
606
|
lang_graph
|
|
618
607
|
)
|
|
619
608
|
new_expr_chain = ExpressionsChain(
|
|
620
|
-
type
|
|
621
|
-
left_link
|
|
622
|
-
right_link
|
|
609
|
+
type=expr_chain_type,
|
|
610
|
+
left_link=left_link,
|
|
611
|
+
right_link=right_link
|
|
623
612
|
)
|
|
624
613
|
return new_expr_chain
|
|
625
614
|
|
|
626
615
|
case 'field':
|
|
627
616
|
assoc_name = list(serialized_expr_chain.keys())[0]
|
|
628
|
-
target_asset = lang_graph.assets[
|
|
617
|
+
target_asset = lang_graph.assets[
|
|
629
618
|
serialized_expr_chain[assoc_name]['asset type']]
|
|
630
619
|
fieldname = serialized_expr_chain[assoc_name]['fieldname']
|
|
631
620
|
|
|
@@ -645,9 +634,9 @@ class ExpressionsChain:
|
|
|
645
634
|
)
|
|
646
635
|
|
|
647
636
|
new_expr_chain = ExpressionsChain(
|
|
648
|
-
type
|
|
649
|
-
association
|
|
650
|
-
fieldname
|
|
637
|
+
type='field',
|
|
638
|
+
association=association,
|
|
639
|
+
fieldname=fieldname
|
|
651
640
|
)
|
|
652
641
|
return new_expr_chain
|
|
653
642
|
|
|
@@ -657,8 +646,8 @@ class ExpressionsChain:
|
|
|
657
646
|
lang_graph
|
|
658
647
|
)
|
|
659
648
|
new_expr_chain = ExpressionsChain(
|
|
660
|
-
type
|
|
661
|
-
sub_link
|
|
649
|
+
type='transitive',
|
|
650
|
+
sub_link=sub_link
|
|
662
651
|
)
|
|
663
652
|
return new_expr_chain
|
|
664
653
|
|
|
@@ -676,9 +665,9 @@ class ExpressionsChain:
|
|
|
676
665
|
raise LanguageGraphException(msg % subtype_name)
|
|
677
666
|
|
|
678
667
|
new_expr_chain = ExpressionsChain(
|
|
679
|
-
type
|
|
680
|
-
sub_link
|
|
681
|
-
subtype
|
|
668
|
+
type='subType',
|
|
669
|
+
sub_link=sub_link,
|
|
670
|
+
subtype=subtype_asset
|
|
682
671
|
)
|
|
683
672
|
return new_expr_chain
|
|
684
673
|
|
|
@@ -689,14 +678,14 @@ class ExpressionsChain:
|
|
|
689
678
|
msg % serialized_expr_chain['type']
|
|
690
679
|
)
|
|
691
680
|
|
|
692
|
-
|
|
693
681
|
def __repr__(self) -> str:
|
|
694
682
|
return str(self.to_dict())
|
|
695
683
|
|
|
696
684
|
|
|
697
|
-
class LanguageGraph
|
|
685
|
+
class LanguageGraph:
|
|
698
686
|
"""Graph representation of a MAL language"""
|
|
699
|
-
|
|
687
|
+
|
|
688
|
+
def __init__(self, lang: dict | None = None):
|
|
700
689
|
self.assets: dict[str, LanguageGraphAsset] = {}
|
|
701
690
|
if lang is not None:
|
|
702
691
|
self._lang_spec: dict = lang
|
|
@@ -706,32 +695,31 @@ class LanguageGraph():
|
|
|
706
695
|
}
|
|
707
696
|
self._generate_graph()
|
|
708
697
|
|
|
709
|
-
|
|
710
698
|
def __repr__(self) -> str:
|
|
711
699
|
return (f'LanguageGraph(id: "{self.metadata.get("id", "N/A")}", '
|
|
712
700
|
f'version: "{self.metadata.get("version", "N/A")}")')
|
|
713
701
|
|
|
714
|
-
|
|
715
702
|
@classmethod
|
|
716
703
|
def from_mal_spec(cls, mal_spec_file: str) -> LanguageGraph:
|
|
717
|
-
"""
|
|
718
|
-
Create a LanguageGraph from a .mal file (a MAL spec).
|
|
704
|
+
"""Create a LanguageGraph from a .mal file (a MAL spec).
|
|
719
705
|
|
|
720
706
|
Arguments:
|
|
707
|
+
---------
|
|
721
708
|
mal_spec_file - the path to the .mal file
|
|
709
|
+
|
|
722
710
|
"""
|
|
723
711
|
logger.info("Loading mal spec %s", mal_spec_file)
|
|
724
712
|
return LanguageGraph(MalCompiler().compile(mal_spec_file))
|
|
725
713
|
|
|
726
|
-
|
|
727
714
|
@classmethod
|
|
728
715
|
def from_mar_archive(cls, mar_archive: str) -> LanguageGraph:
|
|
729
|
-
"""
|
|
730
|
-
Create a LanguageGraph from a ".mar" archive provided by malc
|
|
716
|
+
"""Create a LanguageGraph from a ".mar" archive provided by malc
|
|
731
717
|
(https://github.com/mal-lang/malc).
|
|
732
718
|
|
|
733
719
|
Arguments:
|
|
720
|
+
---------
|
|
734
721
|
mar_archive - the path to a ".mar" archive
|
|
722
|
+
|
|
735
723
|
"""
|
|
736
724
|
logger.info('Loading mar archive %s', mar_archive)
|
|
737
725
|
with zipfile.ZipFile(mar_archive, 'r') as archive:
|
|
@@ -740,7 +728,6 @@ class LanguageGraph():
|
|
|
740
728
|
|
|
741
729
|
def _to_dict(self):
|
|
742
730
|
"""Converts LanguageGraph into a dict"""
|
|
743
|
-
|
|
744
731
|
logger.debug(
|
|
745
732
|
'Serializing %s assets.', len(self.assets.items())
|
|
746
733
|
)
|
|
@@ -753,8 +740,7 @@ class LanguageGraph():
|
|
|
753
740
|
|
|
754
741
|
@property
|
|
755
742
|
def associations(self) -> set[LanguageGraphAssociation]:
|
|
756
|
-
"""
|
|
757
|
-
Return all associations in the language graph.
|
|
743
|
+
"""Return all associations in the language graph.
|
|
758
744
|
"""
|
|
759
745
|
return {assoc for asset in self.assets.values() for assoc in asset.associations.values()}
|
|
760
746
|
|
|
@@ -771,214 +757,127 @@ class LanguageGraph():
|
|
|
771
757
|
"""Save to json/yml depending on extension"""
|
|
772
758
|
return save_dict_to_file(filename, self._to_dict())
|
|
773
759
|
|
|
774
|
-
|
|
775
760
|
@classmethod
|
|
776
761
|
def _from_dict(cls, serialized_graph: dict) -> LanguageGraph:
|
|
777
|
-
"""
|
|
778
|
-
Args:
|
|
779
|
-
serialized_graph - LanguageGraph in dict format
|
|
780
|
-
"""
|
|
781
|
-
|
|
762
|
+
"""Rebuild a LanguageGraph instance from its serialized dict form."""
|
|
782
763
|
logger.debug('Create language graph from dictionary.')
|
|
783
764
|
lang_graph = LanguageGraph()
|
|
784
765
|
lang_graph.metadata = serialized_graph.pop('metadata')
|
|
785
766
|
|
|
786
|
-
#
|
|
787
|
-
for
|
|
788
|
-
logger.debug(
|
|
789
|
-
|
|
790
|
-
|
|
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']
|
|
791
779
|
)
|
|
792
|
-
asset_node = LanguageGraphAsset(
|
|
793
|
-
name = asset_dict['name'],
|
|
794
|
-
own_associations = {},
|
|
795
|
-
attack_steps = {},
|
|
796
|
-
info = asset_dict['info'],
|
|
797
|
-
own_super_asset = None,
|
|
798
|
-
own_sub_assets = set(),
|
|
799
|
-
own_variables = {},
|
|
800
|
-
is_abstract = asset_dict['is_abstract']
|
|
801
|
-
)
|
|
802
|
-
lang_graph.assets[asset_dict['name']] = asset_node
|
|
803
|
-
|
|
804
|
-
# Relink assets based on inheritance
|
|
805
|
-
for asset_dict in serialized_graph.values():
|
|
806
|
-
asset = lang_graph.assets[asset_dict['name']]
|
|
807
|
-
super_asset_name = asset_dict['super_asset']
|
|
808
|
-
if not super_asset_name:
|
|
809
|
-
continue
|
|
810
|
-
|
|
811
|
-
super_asset = lang_graph.assets[super_asset_name]
|
|
812
|
-
if not super_asset:
|
|
813
|
-
msg = 'Failed to find super asset "%s" for asset "%s"!'
|
|
814
|
-
logger.error(
|
|
815
|
-
msg, asset_dict["super_asset"], asset_dict["name"])
|
|
816
|
-
raise LanguageGraphSuperAssetNotFoundError(
|
|
817
|
-
msg % (asset_dict["super_asset"], asset_dict["name"]))
|
|
818
|
-
|
|
819
|
-
super_asset.own_sub_assets.add(asset)
|
|
820
|
-
asset.own_super_asset = super_asset
|
|
821
|
-
|
|
822
|
-
# Generate all of the association nodes of the language graph.
|
|
823
|
-
for asset_dict in serialized_graph.values():
|
|
824
|
-
logger.debug(
|
|
825
|
-
'Create association language graph nodes for asset %s',
|
|
826
|
-
asset_dict['name']
|
|
827
|
-
)
|
|
828
|
-
|
|
829
|
-
asset = lang_graph.assets[asset_dict['name']]
|
|
830
|
-
for association in asset_dict['associations'].values():
|
|
831
|
-
left_asset = lang_graph.assets[association['left']['asset']]
|
|
832
|
-
if not left_asset:
|
|
833
|
-
msg = 'Left asset "%s" for association "%s" not found!'
|
|
834
|
-
logger.error(
|
|
835
|
-
msg, association['left']['asset'],
|
|
836
|
-
association['name'])
|
|
837
|
-
raise LanguageGraphAssociationError(
|
|
838
|
-
msg % (association['left']['asset'],
|
|
839
|
-
association['name']))
|
|
840
|
-
|
|
841
|
-
right_asset = lang_graph.assets[association['right']['asset']]
|
|
842
|
-
if not right_asset:
|
|
843
|
-
msg = 'Right asset "%s" for association "%s" not found!'
|
|
844
|
-
logger.error(
|
|
845
|
-
msg, association['right']['asset'],
|
|
846
|
-
association['name'])
|
|
847
|
-
raise LanguageGraphAssociationError(
|
|
848
|
-
msg % (association['right']['asset'],
|
|
849
|
-
association['name'])
|
|
850
|
-
)
|
|
851
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)
|
|
852
808
|
assoc_node = LanguageGraphAssociation(
|
|
853
|
-
name
|
|
854
|
-
left_field
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
association['right']['max']),
|
|
864
|
-
info = association['info']
|
|
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']
|
|
865
819
|
)
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
(target_asset_name, expr_chain_dict) = var_target
|
|
876
|
-
target_asset = lang_graph.assets[target_asset_name]
|
|
877
|
-
expr_chain = ExpressionsChain._from_dict(
|
|
878
|
-
expr_chain_dict,
|
|
879
|
-
lang_graph
|
|
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)
|
|
880
829
|
)
|
|
881
|
-
asset.own_variables[variable_name] = (target_asset, expr_chain)
|
|
882
830
|
|
|
883
|
-
#
|
|
884
|
-
for
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
'
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
overrides = attack_step_dict['overrides'],
|
|
897
|
-
own_children = {},
|
|
898
|
-
own_parents = {},
|
|
899
|
-
info = attack_step_dict['info'],
|
|
900
|
-
tags = list(attack_step_dict['tags'])
|
|
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'])
|
|
901
844
|
)
|
|
902
|
-
asset.attack_steps[attack_step_dict['name']] = \
|
|
903
|
-
attack_step_node
|
|
904
|
-
|
|
905
|
-
# Relink attack steps based on inheritence
|
|
906
|
-
for asset_dict in serialized_graph.values():
|
|
907
|
-
asset = lang_graph.assets[asset_dict['name']]
|
|
908
|
-
for attack_step_dict in asset_dict['attack_steps'].values():
|
|
909
|
-
if 'inherits' in attack_step_dict and \
|
|
910
|
-
attack_step_dict['inherits'] is not None:
|
|
911
|
-
attack_step = asset.attack_steps[
|
|
912
|
-
attack_step_dict['name']]
|
|
913
|
-
ancestor_asset_name, ancestor_attack_step_name = \
|
|
914
|
-
disaggregate_attack_step_full_name(
|
|
915
|
-
attack_step_dict['inherits'])
|
|
916
|
-
ancestor_asset = lang_graph.assets[ancestor_asset_name]
|
|
917
|
-
ancestor_attack_step = ancestor_asset.attack_steps[\
|
|
918
|
-
ancestor_attack_step_name]
|
|
919
|
-
attack_step.inherits = ancestor_attack_step
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
# Relink attack steps based on expressions chains
|
|
923
|
-
for asset_dict in serialized_graph.values():
|
|
924
|
-
asset = lang_graph.assets[asset_dict['name']]
|
|
925
|
-
for attack_step_dict in asset_dict['attack_steps'].values():
|
|
926
|
-
attack_step = asset.attack_steps[attack_step_dict['name']]
|
|
927
|
-
for child_target in attack_step_dict['own_children'].items():
|
|
928
|
-
target_full_attack_step_name = child_target[0]
|
|
929
|
-
expr_chains = child_target[1]
|
|
930
|
-
target_asset_name, target_attack_step_name = \
|
|
931
|
-
disaggregate_attack_step_full_name(target_full_attack_step_name)
|
|
932
|
-
target_asset = lang_graph.assets[target_asset_name]
|
|
933
|
-
target_attack_step = target_asset.attack_steps[
|
|
934
|
-
target_attack_step_name]
|
|
935
|
-
for expr_chain_dict in expr_chains:
|
|
936
|
-
expr_chain = ExpressionsChain._from_dict(
|
|
937
|
-
expr_chain_dict,
|
|
938
|
-
lang_graph
|
|
939
|
-
)
|
|
940
845
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
expr_chain_dict,
|
|
974
|
-
lang_graph
|
|
975
|
-
)
|
|
976
|
-
if expr_chain:
|
|
977
|
-
attack_step.own_requires.append(expr_chain)
|
|
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
|
+
]
|
|
978
878
|
|
|
979
879
|
return lang_graph
|
|
980
880
|
|
|
981
|
-
|
|
982
881
|
@classmethod
|
|
983
882
|
def load_from_file(cls, filename: str) -> LanguageGraph:
|
|
984
883
|
"""Create LanguageGraph from mal, mar, yaml or json"""
|
|
@@ -989,7 +888,7 @@ class LanguageGraph():
|
|
|
989
888
|
lang_graph = cls.from_mar_archive(filename)
|
|
990
889
|
elif filename.endswith(('.yaml', '.yml')):
|
|
991
890
|
lang_graph = cls._from_dict(load_dict_from_yaml_file(filename))
|
|
992
|
-
elif filename.endswith(
|
|
891
|
+
elif filename.endswith('.json'):
|
|
993
892
|
lang_graph = cls._from_dict(load_dict_from_json_file(filename))
|
|
994
893
|
else:
|
|
995
894
|
raise TypeError(
|
|
@@ -998,18 +897,17 @@ class LanguageGraph():
|
|
|
998
897
|
|
|
999
898
|
if lang_graph:
|
|
1000
899
|
return lang_graph
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
)
|
|
1005
|
-
|
|
900
|
+
raise LanguageGraphException(
|
|
901
|
+
f'Failed to load language graph from file "{filename}".'
|
|
902
|
+
)
|
|
1006
903
|
|
|
1007
904
|
def save_language_specification_to_json(self, filename: str) -> None:
|
|
1008
|
-
"""
|
|
1009
|
-
Save a MAL language specification dictionary to a JSON file
|
|
905
|
+
"""Save a MAL language specification dictionary to a JSON file
|
|
1010
906
|
|
|
1011
907
|
Arguments:
|
|
908
|
+
---------
|
|
1012
909
|
filename - the JSON filename where the language specification will be written
|
|
910
|
+
|
|
1013
911
|
"""
|
|
1014
912
|
logger.info('Save language specification to %s', filename)
|
|
1015
913
|
|
|
@@ -1025,8 +923,7 @@ class LanguageGraph():
|
|
|
1025
923
|
None,
|
|
1026
924
|
str
|
|
1027
925
|
]:
|
|
1028
|
-
"""
|
|
1029
|
-
The attack step expression just adds the name of the attack
|
|
926
|
+
"""The attack step expression just adds the name of the attack
|
|
1030
927
|
step. All other step expressions only modify the target
|
|
1031
928
|
asset and parent associations chain.
|
|
1032
929
|
"""
|
|
@@ -1039,18 +936,16 @@ class LanguageGraph():
|
|
|
1039
936
|
def process_set_operation_step_expression(
|
|
1040
937
|
self,
|
|
1041
938
|
target_asset: LanguageGraphAsset,
|
|
1042
|
-
expr_chain:
|
|
939
|
+
expr_chain: ExpressionsChain | None,
|
|
1043
940
|
step_expression: dict[str, Any]
|
|
1044
941
|
) -> tuple[
|
|
1045
942
|
LanguageGraphAsset,
|
|
1046
943
|
ExpressionsChain,
|
|
1047
944
|
None
|
|
1048
945
|
]:
|
|
1049
|
-
"""
|
|
1050
|
-
The set operators are used to combine the left hand and right
|
|
946
|
+
"""The set operators are used to combine the left hand and right
|
|
1051
947
|
hand targets accordingly.
|
|
1052
948
|
"""
|
|
1053
|
-
|
|
1054
949
|
lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
|
|
1055
950
|
target_asset,
|
|
1056
951
|
expr_chain,
|
|
@@ -1077,9 +972,9 @@ class LanguageGraph():
|
|
|
1077
972
|
)
|
|
1078
973
|
|
|
1079
974
|
new_expr_chain = ExpressionsChain(
|
|
1080
|
-
type
|
|
1081
|
-
left_link
|
|
1082
|
-
right_link
|
|
975
|
+
type=step_expression['type'],
|
|
976
|
+
left_link=lh_expr_chain,
|
|
977
|
+
right_link=rh_expr_chain
|
|
1083
978
|
)
|
|
1084
979
|
return (
|
|
1085
980
|
lh_target_asset,
|
|
@@ -1123,12 +1018,10 @@ class LanguageGraph():
|
|
|
1123
1018
|
ExpressionsChain,
|
|
1124
1019
|
None
|
|
1125
1020
|
]:
|
|
1126
|
-
"""
|
|
1127
|
-
Change the target asset from the current one to the associated
|
|
1021
|
+
"""Change the target asset from the current one to the associated
|
|
1128
1022
|
asset given the specified field name and add the parent
|
|
1129
1023
|
fieldname and association to the parent associations chain.
|
|
1130
1024
|
"""
|
|
1131
|
-
|
|
1132
1025
|
fieldname = step_expression['name']
|
|
1133
1026
|
|
|
1134
1027
|
if target_asset is None:
|
|
@@ -1138,21 +1031,21 @@ class LanguageGraph():
|
|
|
1138
1031
|
|
|
1139
1032
|
new_target_asset = None
|
|
1140
1033
|
for association in target_asset.associations.values():
|
|
1141
|
-
if (association.left_field.fieldname == fieldname and
|
|
1034
|
+
if (association.left_field.fieldname == fieldname and
|
|
1142
1035
|
target_asset.is_subasset_of(
|
|
1143
1036
|
association.right_field.asset)):
|
|
1144
1037
|
new_target_asset = association.left_field.asset
|
|
1145
1038
|
|
|
1146
|
-
if (association.right_field.fieldname == fieldname and
|
|
1039
|
+
if (association.right_field.fieldname == fieldname and
|
|
1147
1040
|
target_asset.is_subasset_of(
|
|
1148
1041
|
association.left_field.asset)):
|
|
1149
1042
|
new_target_asset = association.right_field.asset
|
|
1150
1043
|
|
|
1151
1044
|
if new_target_asset:
|
|
1152
1045
|
new_expr_chain = ExpressionsChain(
|
|
1153
|
-
type
|
|
1154
|
-
fieldname
|
|
1155
|
-
association
|
|
1046
|
+
type='field',
|
|
1047
|
+
fieldname=fieldname,
|
|
1048
|
+
association=association
|
|
1156
1049
|
)
|
|
1157
1050
|
return (
|
|
1158
1051
|
new_target_asset,
|
|
@@ -1167,15 +1060,14 @@ class LanguageGraph():
|
|
|
1167
1060
|
def process_transitive_step_expression(
|
|
1168
1061
|
self,
|
|
1169
1062
|
target_asset: LanguageGraphAsset,
|
|
1170
|
-
expr_chain:
|
|
1063
|
+
expr_chain: ExpressionsChain | None,
|
|
1171
1064
|
step_expression: dict[str, Any]
|
|
1172
1065
|
) -> tuple[
|
|
1173
1066
|
LanguageGraphAsset,
|
|
1174
1067
|
ExpressionsChain,
|
|
1175
1068
|
None
|
|
1176
1069
|
]:
|
|
1177
|
-
"""
|
|
1178
|
-
Create a transitive tuple entry that applies to the next
|
|
1070
|
+
"""Create a transitive tuple entry that applies to the next
|
|
1179
1071
|
component of the step expression.
|
|
1180
1072
|
"""
|
|
1181
1073
|
result_target_asset, result_expr_chain, _ = (
|
|
@@ -1186,8 +1078,8 @@ class LanguageGraph():
|
|
|
1186
1078
|
)
|
|
1187
1079
|
)
|
|
1188
1080
|
new_expr_chain = ExpressionsChain(
|
|
1189
|
-
type
|
|
1190
|
-
sub_link
|
|
1081
|
+
type='transitive',
|
|
1082
|
+
sub_link=result_expr_chain
|
|
1191
1083
|
)
|
|
1192
1084
|
return (
|
|
1193
1085
|
result_target_asset,
|
|
@@ -1198,19 +1090,17 @@ class LanguageGraph():
|
|
|
1198
1090
|
def process_subType_step_expression(
|
|
1199
1091
|
self,
|
|
1200
1092
|
target_asset: LanguageGraphAsset,
|
|
1201
|
-
expr_chain:
|
|
1093
|
+
expr_chain: ExpressionsChain | None,
|
|
1202
1094
|
step_expression: dict[str, Any]
|
|
1203
1095
|
) -> tuple[
|
|
1204
1096
|
LanguageGraphAsset,
|
|
1205
1097
|
ExpressionsChain,
|
|
1206
1098
|
None
|
|
1207
1099
|
]:
|
|
1208
|
-
"""
|
|
1209
|
-
Create a subType tuple entry that applies to the next
|
|
1100
|
+
"""Create a subType tuple entry that applies to the next
|
|
1210
1101
|
component of the step expression and changes the target
|
|
1211
1102
|
asset to the subasset.
|
|
1212
1103
|
"""
|
|
1213
|
-
|
|
1214
1104
|
subtype_name = step_expression['subType']
|
|
1215
1105
|
result_target_asset, result_expr_chain, _ = (
|
|
1216
1106
|
self.process_step_expression(
|
|
@@ -1237,9 +1127,9 @@ class LanguageGraph():
|
|
|
1237
1127
|
)
|
|
1238
1128
|
|
|
1239
1129
|
new_expr_chain = ExpressionsChain(
|
|
1240
|
-
type
|
|
1241
|
-
sub_link
|
|
1242
|
-
subtype
|
|
1130
|
+
type='subType',
|
|
1131
|
+
sub_link=result_expr_chain,
|
|
1132
|
+
subtype=subtype_asset
|
|
1243
1133
|
)
|
|
1244
1134
|
return (
|
|
1245
1135
|
subtype_asset,
|
|
@@ -1250,15 +1140,14 @@ class LanguageGraph():
|
|
|
1250
1140
|
def process_collect_step_expression(
|
|
1251
1141
|
self,
|
|
1252
1142
|
target_asset: LanguageGraphAsset,
|
|
1253
|
-
expr_chain:
|
|
1143
|
+
expr_chain: ExpressionsChain | None,
|
|
1254
1144
|
step_expression: dict[str, Any]
|
|
1255
1145
|
) -> tuple[
|
|
1256
1146
|
LanguageGraphAsset,
|
|
1257
|
-
|
|
1258
|
-
|
|
1147
|
+
ExpressionsChain | None,
|
|
1148
|
+
str | None
|
|
1259
1149
|
]:
|
|
1260
|
-
"""
|
|
1261
|
-
Apply the right hand step expression to left hand step
|
|
1150
|
+
"""Apply the right hand step expression to left hand step
|
|
1262
1151
|
expression target asset and parent associations chain.
|
|
1263
1152
|
"""
|
|
1264
1153
|
lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
|
|
@@ -1280,9 +1169,9 @@ class LanguageGraph():
|
|
|
1280
1169
|
new_expr_chain = lh_expr_chain
|
|
1281
1170
|
if rh_expr_chain:
|
|
1282
1171
|
new_expr_chain = ExpressionsChain(
|
|
1283
|
-
type
|
|
1284
|
-
left_link
|
|
1285
|
-
right_link
|
|
1172
|
+
type='collect',
|
|
1173
|
+
left_link=lh_expr_chain,
|
|
1174
|
+
right_link=rh_expr_chain
|
|
1286
1175
|
)
|
|
1287
1176
|
|
|
1288
1177
|
return (
|
|
@@ -1293,17 +1182,17 @@ class LanguageGraph():
|
|
|
1293
1182
|
|
|
1294
1183
|
def process_step_expression(self,
|
|
1295
1184
|
target_asset: LanguageGraphAsset,
|
|
1296
|
-
expr_chain:
|
|
1185
|
+
expr_chain: ExpressionsChain | None,
|
|
1297
1186
|
step_expression: dict
|
|
1298
1187
|
) -> tuple[
|
|
1299
1188
|
LanguageGraphAsset,
|
|
1300
|
-
|
|
1301
|
-
|
|
1189
|
+
ExpressionsChain | None,
|
|
1190
|
+
str | None
|
|
1302
1191
|
]:
|
|
1303
|
-
"""
|
|
1304
|
-
Recursively process an attack step expression.
|
|
1192
|
+
"""Recursively process an attack step expression.
|
|
1305
1193
|
|
|
1306
1194
|
Arguments:
|
|
1195
|
+
---------
|
|
1307
1196
|
target_asset - The asset type that this step expression should
|
|
1308
1197
|
apply to. Initially it will contain the asset
|
|
1309
1198
|
type to which the attack step belongs.
|
|
@@ -1317,21 +1206,22 @@ class LanguageGraph():
|
|
|
1317
1206
|
step_expression - A dictionary containing the step expression.
|
|
1318
1207
|
|
|
1319
1208
|
Return:
|
|
1209
|
+
------
|
|
1320
1210
|
A tuple triplet containing the target asset, the resulting parent
|
|
1321
1211
|
associations chain, and the name of the attack step.
|
|
1322
|
-
"""
|
|
1323
1212
|
|
|
1213
|
+
"""
|
|
1324
1214
|
if logger.isEnabledFor(logging.DEBUG):
|
|
1325
1215
|
# Avoid running json.dumps when not in debug
|
|
1326
1216
|
logger.debug(
|
|
1327
1217
|
'Processing Step Expression:\n%s',
|
|
1328
|
-
json.dumps(step_expression, indent
|
|
1218
|
+
json.dumps(step_expression, indent=2)
|
|
1329
1219
|
)
|
|
1330
1220
|
|
|
1331
1221
|
result: tuple[
|
|
1332
1222
|
LanguageGraphAsset,
|
|
1333
|
-
|
|
1334
|
-
|
|
1223
|
+
ExpressionsChain | None,
|
|
1224
|
+
str | None
|
|
1335
1225
|
]
|
|
1336
1226
|
|
|
1337
1227
|
match (step_expression['type']):
|
|
@@ -1371,14 +1261,14 @@ class LanguageGraph():
|
|
|
1371
1261
|
|
|
1372
1262
|
def reverse_expr_chain(
|
|
1373
1263
|
self,
|
|
1374
|
-
expr_chain:
|
|
1375
|
-
reverse_chain:
|
|
1376
|
-
) ->
|
|
1377
|
-
"""
|
|
1378
|
-
Recursively reverse the associations chain. From parent to child or
|
|
1264
|
+
expr_chain: ExpressionsChain | None,
|
|
1265
|
+
reverse_chain: ExpressionsChain | None
|
|
1266
|
+
) -> ExpressionsChain | None:
|
|
1267
|
+
"""Recursively reverse the associations chain. From parent to child or
|
|
1379
1268
|
vice versa.
|
|
1380
1269
|
|
|
1381
1270
|
Arguments:
|
|
1271
|
+
---------
|
|
1382
1272
|
expr_chain - A chain of nested tuples that specify the
|
|
1383
1273
|
associations and set operations chain from an
|
|
1384
1274
|
attack step to its connected attack step.
|
|
@@ -1386,93 +1276,96 @@ class LanguageGraph():
|
|
|
1386
1276
|
current reversed associations chain.
|
|
1387
1277
|
|
|
1388
1278
|
Return:
|
|
1279
|
+
------
|
|
1389
1280
|
The resulting reversed associations chain.
|
|
1281
|
+
|
|
1390
1282
|
"""
|
|
1391
1283
|
if not expr_chain:
|
|
1392
1284
|
return reverse_chain
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
else:
|
|
1409
|
-
new_expr_chain = ExpressionsChain(
|
|
1410
|
-
type = expr_chain.type,
|
|
1411
|
-
left_link = left_reverse_chain,
|
|
1412
|
-
right_link = right_reverse_chain
|
|
1413
|
-
)
|
|
1414
|
-
|
|
1415
|
-
return new_expr_chain
|
|
1416
|
-
|
|
1417
|
-
case 'transitive':
|
|
1418
|
-
result_reverse_chain = self.reverse_expr_chain(
|
|
1419
|
-
expr_chain.sub_link, reverse_chain)
|
|
1285
|
+
match (expr_chain.type):
|
|
1286
|
+
case 'union' | 'intersection' | 'difference' | 'collect':
|
|
1287
|
+
left_reverse_chain = \
|
|
1288
|
+
self.reverse_expr_chain(expr_chain.left_link,
|
|
1289
|
+
reverse_chain)
|
|
1290
|
+
right_reverse_chain = \
|
|
1291
|
+
self.reverse_expr_chain(expr_chain.right_link,
|
|
1292
|
+
reverse_chain)
|
|
1293
|
+
if expr_chain.type == 'collect':
|
|
1294
|
+
new_expr_chain = ExpressionsChain(
|
|
1295
|
+
type=expr_chain.type,
|
|
1296
|
+
left_link=right_reverse_chain,
|
|
1297
|
+
right_link=left_reverse_chain
|
|
1298
|
+
)
|
|
1299
|
+
else:
|
|
1420
1300
|
new_expr_chain = ExpressionsChain(
|
|
1421
|
-
type
|
|
1422
|
-
|
|
1301
|
+
type=expr_chain.type,
|
|
1302
|
+
left_link=left_reverse_chain,
|
|
1303
|
+
right_link=right_reverse_chain
|
|
1423
1304
|
)
|
|
1424
|
-
return new_expr_chain
|
|
1425
1305
|
|
|
1426
|
-
|
|
1427
|
-
association = expr_chain.association
|
|
1306
|
+
return new_expr_chain
|
|
1428
1307
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
|
1433
1316
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
"Missing field name for expressions chain"
|
|
1437
|
-
)
|
|
1317
|
+
case 'field':
|
|
1318
|
+
association = expr_chain.association
|
|
1438
1319
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
type = 'field',
|
|
1443
|
-
association = association,
|
|
1444
|
-
fieldname = opposite_fieldname
|
|
1320
|
+
if not association:
|
|
1321
|
+
raise LanguageGraphException(
|
|
1322
|
+
"Missing association for expressions chain"
|
|
1445
1323
|
)
|
|
1446
|
-
return new_expr_chain
|
|
1447
1324
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
reverse_chain
|
|
1452
|
-
)
|
|
1453
|
-
new_expr_chain = ExpressionsChain(
|
|
1454
|
-
type = 'subType',
|
|
1455
|
-
sub_link = result_reverse_chain,
|
|
1456
|
-
subtype = expr_chain.subtype
|
|
1325
|
+
if not expr_chain.fieldname:
|
|
1326
|
+
raise LanguageGraphException(
|
|
1327
|
+
"Missing field name for expressions chain"
|
|
1457
1328
|
)
|
|
1458
|
-
return new_expr_chain
|
|
1459
1329
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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)
|
|
1464
1355
|
|
|
1465
1356
|
def _resolve_variable(self, asset: LanguageGraphAsset, var_name) -> tuple:
|
|
1466
|
-
"""
|
|
1467
|
-
Resolve a variable for a specific asset by variable name.
|
|
1357
|
+
"""Resolve a variable for a specific asset by variable name.
|
|
1468
1358
|
|
|
1469
1359
|
Arguments:
|
|
1360
|
+
---------
|
|
1470
1361
|
asset - a language graph asset to which the variable belongs
|
|
1471
1362
|
var_name - a string representing the variable name
|
|
1472
1363
|
|
|
1473
1364
|
Return:
|
|
1365
|
+
------
|
|
1474
1366
|
A tuple containing the target asset and expressions chain required to
|
|
1475
1367
|
reach it.
|
|
1368
|
+
|
|
1476
1369
|
"""
|
|
1477
1370
|
if var_name not in asset.variables:
|
|
1478
1371
|
var_expr = self._get_var_expr_for_asset(asset.name, var_name)
|
|
@@ -1490,13 +1383,15 @@ class LanguageGraph():
|
|
|
1490
1383
|
lang_spec: dict[str, Any],
|
|
1491
1384
|
assets: dict[str, LanguageGraphAsset]
|
|
1492
1385
|
) -> None:
|
|
1493
|
-
"""
|
|
1386
|
+
"""Link associations to assets based on the language specification.
|
|
1387
|
+
|
|
1494
1388
|
Arguments:
|
|
1389
|
+
---------
|
|
1495
1390
|
lang_spec - the language specification dictionary
|
|
1496
1391
|
assets - a dictionary of LanguageGraphAsset objects
|
|
1497
1392
|
indexed by their names
|
|
1498
|
-
"""
|
|
1499
1393
|
|
|
1394
|
+
"""
|
|
1500
1395
|
for association_dict in lang_spec['associations']:
|
|
1501
1396
|
logger.debug(
|
|
1502
1397
|
'Create association language graph nodes for association %s',
|
|
@@ -1521,20 +1416,20 @@ class LanguageGraph():
|
|
|
1521
1416
|
right_asset = assets[right_asset_name]
|
|
1522
1417
|
|
|
1523
1418
|
assoc_node = LanguageGraphAssociation(
|
|
1524
|
-
name
|
|
1525
|
-
left_field
|
|
1419
|
+
name=association_dict['name'],
|
|
1420
|
+
left_field=LanguageGraphAssociationField(
|
|
1526
1421
|
left_asset,
|
|
1527
1422
|
association_dict['leftField'],
|
|
1528
1423
|
association_dict['leftMultiplicity']['min'],
|
|
1529
1424
|
association_dict['leftMultiplicity']['max']
|
|
1530
1425
|
),
|
|
1531
|
-
right_field
|
|
1426
|
+
right_field=LanguageGraphAssociationField(
|
|
1532
1427
|
right_asset,
|
|
1533
1428
|
association_dict['rightField'],
|
|
1534
1429
|
association_dict['rightMultiplicity']['min'],
|
|
1535
1430
|
association_dict['rightMultiplicity']['max']
|
|
1536
1431
|
),
|
|
1537
|
-
info
|
|
1432
|
+
info=association_dict['meta']
|
|
1538
1433
|
)
|
|
1539
1434
|
|
|
1540
1435
|
# Add the association to the left and right asset
|
|
@@ -1547,10 +1442,8 @@ class LanguageGraph():
|
|
|
1547
1442
|
lang_spec: dict[str, Any],
|
|
1548
1443
|
assets: dict[str, LanguageGraphAsset]
|
|
1549
1444
|
) -> None:
|
|
1445
|
+
"""Link assets based on inheritance and associations.
|
|
1550
1446
|
"""
|
|
1551
|
-
Link assets based on inheritance and associations.
|
|
1552
|
-
"""
|
|
1553
|
-
|
|
1554
1447
|
for asset_dict in lang_spec['assets']:
|
|
1555
1448
|
asset = assets[asset_dict['name']]
|
|
1556
1449
|
if asset_dict['superAsset']:
|
|
@@ -1562,18 +1455,20 @@ class LanguageGraph():
|
|
|
1562
1455
|
raise LanguageGraphSuperAssetNotFoundError(
|
|
1563
1456
|
msg % (asset_dict["superAsset"], asset_dict["name"]))
|
|
1564
1457
|
|
|
1565
|
-
super_asset.own_sub_assets.
|
|
1458
|
+
super_asset.own_sub_assets.append(asset)
|
|
1566
1459
|
asset.own_super_asset = super_asset
|
|
1567
1460
|
|
|
1568
1461
|
def _set_variables_for_assets(
|
|
1569
1462
|
self, assets: dict[str, LanguageGraphAsset]
|
|
1570
1463
|
) -> None:
|
|
1571
|
-
"""
|
|
1464
|
+
"""Set the variables for each asset based on the language specification.
|
|
1465
|
+
|
|
1572
1466
|
Arguments:
|
|
1467
|
+
---------
|
|
1573
1468
|
assets - a dictionary of LanguageGraphAsset objects
|
|
1574
1469
|
indexed by their names
|
|
1575
|
-
"""
|
|
1576
1470
|
|
|
1471
|
+
"""
|
|
1577
1472
|
for asset in assets.values():
|
|
1578
1473
|
logger.debug(
|
|
1579
1474
|
'Set variables for asset %s', asset.name
|
|
@@ -1584,189 +1479,138 @@ class LanguageGraph():
|
|
|
1584
1479
|
# Avoid running json.dumps when not in debug
|
|
1585
1480
|
logger.debug(
|
|
1586
1481
|
'Processing Variable Expression:\n%s',
|
|
1587
|
-
json.dumps(variable, indent
|
|
1482
|
+
json.dumps(variable, indent=2)
|
|
1588
1483
|
)
|
|
1589
1484
|
self._resolve_variable(asset, variable['name'])
|
|
1590
1485
|
|
|
1591
1486
|
def _generate_attack_steps(self, assets) -> None:
|
|
1592
1487
|
"""
|
|
1593
|
-
Generate all
|
|
1594
|
-
|
|
1488
|
+
Generate attack steps for all assets and link them according to the
|
|
1489
|
+
language specification.
|
|
1490
|
+
|
|
1491
|
+
This method performs three phases:
|
|
1492
|
+
|
|
1493
|
+
1. Create attack step nodes for each asset, including detectors.
|
|
1494
|
+
2. Inherit attack steps from super-assets, respecting overrides.
|
|
1495
|
+
3. Link attack steps via 'reaches' and evaluate 'exist'/'notExist'
|
|
1496
|
+
requirements.
|
|
1497
|
+
|
|
1498
|
+
Args:
|
|
1499
|
+
assets (dict): Mapping of asset names to asset objects.
|
|
1500
|
+
|
|
1501
|
+
Raises:
|
|
1502
|
+
LanguageGraphStepExpressionError: If a step expression cannot be
|
|
1503
|
+
resolved to a target asset or attack step.
|
|
1504
|
+
LanguageGraphException: If an existence requirement cannot be
|
|
1505
|
+
resolved.
|
|
1595
1506
|
"""
|
|
1596
1507
|
langspec_dict = {}
|
|
1508
|
+
|
|
1597
1509
|
for asset in assets.values():
|
|
1598
|
-
logger.debug(
|
|
1599
|
-
|
|
1600
|
-
asset.name
|
|
1601
|
-
)
|
|
1602
|
-
attack_steps = self._get_attacks_for_asset_type(asset.name)
|
|
1603
|
-
for attack_step_attribs in attack_steps.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():
|
|
1604
1512
|
logger.debug(
|
|
1605
|
-
'Create attack step language graph nodes for %s',
|
|
1606
|
-
attack_step_attribs['name']
|
|
1513
|
+
'Create attack step language graph nodes for %s', step_dict['name']
|
|
1607
1514
|
)
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
if attack_step_attribs['reaches'] else False
|
|
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
|
|
1617
1523
|
),
|
|
1618
|
-
own_children =
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
tags = list(attack_step_attribs['tags'])
|
|
1524
|
+
own_children={}, own_parents={},
|
|
1525
|
+
info=step_dict['meta'],
|
|
1526
|
+
tags=list(step_dict['tags'])
|
|
1622
1527
|
)
|
|
1623
|
-
langspec_dict[
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
detectors: dict = attack_step_attribs.get("detectors", {})
|
|
1629
|
-
for detector in detectors.values():
|
|
1630
|
-
attack_step_node.detectors[detector["name"]] = Detector(
|
|
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(
|
|
1631
1533
|
context=Context(
|
|
1632
|
-
{
|
|
1633
|
-
label: assets[asset]
|
|
1634
|
-
for label, asset in detector["context"].items()
|
|
1635
|
-
}
|
|
1534
|
+
{lbl: assets[a] for lbl, a in det['context'].items()}
|
|
1636
1535
|
),
|
|
1637
|
-
name=
|
|
1638
|
-
type=
|
|
1639
|
-
tprate=
|
|
1536
|
+
name=det.get('name'),
|
|
1537
|
+
type=det.get('type'),
|
|
1538
|
+
tprate=det.get('tprate'),
|
|
1640
1539
|
)
|
|
1641
1540
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
if
|
|
1647
|
-
#
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
# The inherited attack step was already overridden.
|
|
1670
|
-
continue
|
|
1671
|
-
else:
|
|
1672
|
-
asset.attack_steps[attack_step.name].inherits = attack_step
|
|
1673
|
-
asset.attack_steps[attack_step.name].tags += attack_step.tags
|
|
1674
|
-
asset.attack_steps[attack_step.name].info |= attack_step.info
|
|
1675
|
-
|
|
1676
|
-
# Then, link all of the attack step nodes according to their
|
|
1677
|
-
# associations.
|
|
1678
|
-
for asset in self.assets.values():
|
|
1679
|
-
for attack_step in asset.attack_steps.values():
|
|
1680
|
-
logger.debug(
|
|
1681
|
-
'Determining children for attack step %s',
|
|
1682
|
-
attack_step.name
|
|
1683
|
-
)
|
|
1684
|
-
|
|
1685
|
-
if attack_step.full_name not in langspec_dict:
|
|
1686
|
-
# This is simply an empty inherited attack step
|
|
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:
|
|
1550
|
+
continue
|
|
1551
|
+
for super_step in super_asset.attack_steps.values():
|
|
1552
|
+
current_step = asset.attack_steps.get(super_step.name)
|
|
1553
|
+
if not current_step:
|
|
1554
|
+
node = LanguageGraphAttackStep(
|
|
1555
|
+
name=super_step.name,
|
|
1556
|
+
type=super_step.type,
|
|
1557
|
+
asset=asset,
|
|
1558
|
+
ttc=super_step.ttc,
|
|
1559
|
+
overrides=False,
|
|
1560
|
+
own_children={},
|
|
1561
|
+
own_parents={},
|
|
1562
|
+
info=super_step.info,
|
|
1563
|
+
tags=list(super_step.tags)
|
|
1564
|
+
)
|
|
1565
|
+
node.inherits = super_step
|
|
1566
|
+
asset.attack_steps[super_step.name] = node
|
|
1567
|
+
elif current_step.overrides:
|
|
1687
1568
|
continue
|
|
1569
|
+
else:
|
|
1570
|
+
current_step.inherits = super_step
|
|
1571
|
+
current_step.tags += super_step.tags
|
|
1572
|
+
current_step.info |= super_step.info
|
|
1688
1573
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1574
|
+
for asset in self.assets.values():
|
|
1575
|
+
for step in asset.attack_steps.values():
|
|
1576
|
+
logger.debug('Determining children for attack step %s', step.name)
|
|
1577
|
+
if step.full_name not in langspec_dict:
|
|
1578
|
+
continue
|
|
1694
1579
|
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
self.process_step_expression(
|
|
1700
|
-
attack_step.asset,
|
|
1701
|
-
None,
|
|
1702
|
-
step_expression
|
|
1703
|
-
)
|
|
1704
|
-
if not target_asset:
|
|
1705
|
-
msg = 'Failed to find target asset to link with for ' \
|
|
1706
|
-
'step expression:\n%s'
|
|
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:
|
|
1707
1584
|
raise LanguageGraphStepExpressionError(
|
|
1708
|
-
|
|
1585
|
+
'Failed to find target asset for:\n%s' % json.dumps(expr, indent=2)
|
|
1709
1586
|
)
|
|
1710
|
-
|
|
1711
|
-
target_asset_attack_steps = target_asset.attack_steps
|
|
1712
|
-
if target_attack_step_name not in \
|
|
1713
|
-
target_asset_attack_steps:
|
|
1714
|
-
msg = 'Failed to find target attack step %s on %s to ' \
|
|
1715
|
-
'link with for step expression:\n%s'
|
|
1587
|
+
if tgt_name not in tgt_asset.attack_steps:
|
|
1716
1588
|
raise LanguageGraphStepExpressionError(
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
target_asset.name,
|
|
1720
|
-
json.dumps(step_expression, indent = 2)
|
|
1721
|
-
)
|
|
1589
|
+
'Failed to find target attack step %s on %s:\n%s' %
|
|
1590
|
+
(tgt_name, tgt_asset.name, json.dumps(expr, indent=2))
|
|
1722
1591
|
)
|
|
1723
1592
|
|
|
1724
|
-
|
|
1725
|
-
|
|
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))
|
|
1726
1596
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
# Reverse the children associations chains to get the
|
|
1732
|
-
# parents associations chain.
|
|
1733
|
-
target_attack_step.own_parents.setdefault(attack_step, [])
|
|
1734
|
-
target_attack_step.own_parents[attack_step].append(
|
|
1735
|
-
self.reverse_expr_chain(expr_chain, None)
|
|
1736
|
-
)
|
|
1737
|
-
|
|
1738
|
-
# Evaluate the requirements of exist and notExist attack steps
|
|
1739
|
-
if attack_step.type in ('exist', 'notExist'):
|
|
1740
|
-
step_expressions = (
|
|
1741
|
-
langspec_entry['requires']['stepExpressions']
|
|
1742
|
-
if langspec_entry['requires'] else []
|
|
1743
|
-
)
|
|
1744
|
-
if not step_expressions:
|
|
1597
|
+
if step.type in ('exist', 'notExist'):
|
|
1598
|
+
reqs = entry['requires']['stepExpressions'] if entry['requires'] else []
|
|
1599
|
+
if not reqs:
|
|
1745
1600
|
raise LanguageGraphStepExpressionError(
|
|
1746
|
-
'
|
|
1747
|
-
|
|
1748
|
-
attack_step.name,
|
|
1749
|
-
attack_step.type,
|
|
1750
|
-
json.dumps(langspec_entry, indent = 2)
|
|
1751
|
-
)
|
|
1601
|
+
'Missing requirements for "%s" of type "%s":\n%s' %
|
|
1602
|
+
(step.name, step.type, json.dumps(entry, indent=2))
|
|
1752
1603
|
)
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
None,
|
|
1759
|
-
step_expression
|
|
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}'
|
|
1760
1609
|
)
|
|
1761
|
-
|
|
1762
|
-
raise LanguageGraphException('Failed to find '
|
|
1763
|
-
'existence step requirement for step '
|
|
1764
|
-
f'expression:\n%s' % step_expression)
|
|
1765
|
-
attack_step.own_requires.append(result_expr_chain)
|
|
1610
|
+
step.own_requires.append(chain)
|
|
1766
1611
|
|
|
1767
1612
|
def _generate_graph(self) -> None:
|
|
1768
|
-
"""
|
|
1769
|
-
Generate language graph starting from the MAL language specification
|
|
1613
|
+
"""Generate language graph starting from the MAL language specification
|
|
1770
1614
|
given in the constructor.
|
|
1771
1615
|
"""
|
|
1772
1616
|
# Generate all of the asset nodes of the language graph.
|
|
@@ -1777,14 +1621,14 @@ class LanguageGraph():
|
|
|
1777
1621
|
asset_dict['name']
|
|
1778
1622
|
)
|
|
1779
1623
|
asset_node = LanguageGraphAsset(
|
|
1780
|
-
name
|
|
1781
|
-
own_associations
|
|
1782
|
-
attack_steps
|
|
1783
|
-
info
|
|
1784
|
-
own_super_asset
|
|
1785
|
-
own_sub_assets
|
|
1786
|
-
own_variables
|
|
1787
|
-
is_abstract
|
|
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']
|
|
1788
1632
|
)
|
|
1789
1633
|
self.assets[asset_dict['name']] = asset_node
|
|
1790
1634
|
|
|
@@ -1801,24 +1645,26 @@ class LanguageGraph():
|
|
|
1801
1645
|
self._generate_attack_steps(self.assets)
|
|
1802
1646
|
|
|
1803
1647
|
def _get_attacks_for_asset_type(self, asset_type: str) -> dict[str, dict]:
|
|
1804
|
-
"""
|
|
1805
|
-
Get all Attack Steps for a specific asset type.
|
|
1648
|
+
"""Get all Attack Steps for a specific asset type.
|
|
1806
1649
|
|
|
1807
1650
|
Arguments:
|
|
1651
|
+
---------
|
|
1808
1652
|
asset_type - the name of the asset type we want to
|
|
1809
1653
|
list the possible attack steps for
|
|
1810
1654
|
|
|
1811
1655
|
Return:
|
|
1656
|
+
------
|
|
1812
1657
|
A dictionary containing the possible attacks for the
|
|
1813
1658
|
specified asset type. Each key in the dictionary is an attack name
|
|
1814
1659
|
associated with a dictionary containing other characteristics of the
|
|
1815
1660
|
attack such as type of attack, TTC distribution, child attack steps
|
|
1816
1661
|
and other information
|
|
1662
|
+
|
|
1817
1663
|
"""
|
|
1818
1664
|
attack_steps: dict = {}
|
|
1819
1665
|
try:
|
|
1820
1666
|
asset = next(
|
|
1821
|
-
asset for asset in self._lang_spec['assets']
|
|
1667
|
+
asset for asset in self._lang_spec['assets']
|
|
1822
1668
|
if asset['name'] == asset_type
|
|
1823
1669
|
)
|
|
1824
1670
|
except StopIteration:
|
|
@@ -1838,17 +1684,19 @@ class LanguageGraph():
|
|
|
1838
1684
|
return attack_steps
|
|
1839
1685
|
|
|
1840
1686
|
def _get_associations_for_asset_type(self, asset_type: str) -> list[dict]:
|
|
1841
|
-
"""
|
|
1842
|
-
Get all associations for a specific asset type.
|
|
1687
|
+
"""Get all associations for a specific asset type.
|
|
1843
1688
|
|
|
1844
1689
|
Arguments:
|
|
1690
|
+
---------
|
|
1845
1691
|
asset_type - the name of the asset type for which we want to
|
|
1846
1692
|
list the associations
|
|
1847
1693
|
|
|
1848
1694
|
Return:
|
|
1695
|
+
------
|
|
1849
1696
|
A list of dicts, where each dict represents an associations
|
|
1850
1697
|
for the specified asset type. Each dictionary contains
|
|
1851
1698
|
name and meta information about the association.
|
|
1699
|
+
|
|
1852
1700
|
"""
|
|
1853
1701
|
logger.debug(
|
|
1854
1702
|
'Get associations for %s asset from '
|
|
@@ -1856,7 +1704,7 @@ class LanguageGraph():
|
|
|
1856
1704
|
)
|
|
1857
1705
|
associations: list = []
|
|
1858
1706
|
|
|
1859
|
-
asset = next((asset for asset in self._lang_spec['assets']
|
|
1707
|
+
asset = next((asset for asset in self._lang_spec['assets']
|
|
1860
1708
|
if asset['name'] == asset_type), None)
|
|
1861
1709
|
if not asset:
|
|
1862
1710
|
logger.error(
|
|
@@ -1865,8 +1713,8 @@ class LanguageGraph():
|
|
|
1865
1713
|
)
|
|
1866
1714
|
return associations
|
|
1867
1715
|
|
|
1868
|
-
assoc_iter = (assoc for assoc in self._lang_spec['associations']
|
|
1869
|
-
if assoc['leftAsset'] == asset_type or
|
|
1716
|
+
assoc_iter = (assoc for assoc in self._lang_spec['associations']
|
|
1717
|
+
if assoc['leftAsset'] == asset_type or
|
|
1870
1718
|
assoc['rightAsset'] == asset_type)
|
|
1871
1719
|
assoc = next(assoc_iter, None)
|
|
1872
1720
|
while assoc:
|
|
@@ -1877,20 +1725,21 @@ class LanguageGraph():
|
|
|
1877
1725
|
|
|
1878
1726
|
def _get_variables_for_asset_type(
|
|
1879
1727
|
self, asset_type: str) -> list[dict]:
|
|
1880
|
-
"""
|
|
1881
|
-
Get variables for a specific asset type.
|
|
1728
|
+
"""Get variables for a specific asset type.
|
|
1882
1729
|
Note: Variables are the ones specified in MAL through `let` statements
|
|
1883
1730
|
|
|
1884
1731
|
Arguments:
|
|
1732
|
+
---------
|
|
1885
1733
|
asset_type - a string representing the asset type which
|
|
1886
1734
|
contains the variables
|
|
1887
1735
|
|
|
1888
1736
|
Return:
|
|
1737
|
+
------
|
|
1889
1738
|
A list of dicts representing the step expressions for the variables
|
|
1890
1739
|
belonging to the asset.
|
|
1891
|
-
"""
|
|
1892
1740
|
|
|
1893
|
-
|
|
1741
|
+
"""
|
|
1742
|
+
asset_dict = next((asset for asset in self._lang_spec['assets']
|
|
1894
1743
|
if asset['name'] == asset_type), None)
|
|
1895
1744
|
if not asset_dict:
|
|
1896
1745
|
msg = 'Failed to find asset type %s in language specification '\
|
|
@@ -1902,21 +1751,22 @@ class LanguageGraph():
|
|
|
1902
1751
|
|
|
1903
1752
|
def _get_var_expr_for_asset(
|
|
1904
1753
|
self, asset_type: str, var_name) -> dict:
|
|
1905
|
-
"""
|
|
1906
|
-
Get a variable for a specific asset type by variable name.
|
|
1754
|
+
"""Get a variable for a specific asset type by variable name.
|
|
1907
1755
|
|
|
1908
1756
|
Arguments:
|
|
1757
|
+
---------
|
|
1909
1758
|
asset_type - a string representing the type of asset which
|
|
1910
1759
|
contains the variable
|
|
1911
1760
|
var_name - a string representing the variable name
|
|
1912
1761
|
|
|
1913
1762
|
Return:
|
|
1763
|
+
------
|
|
1914
1764
|
A dictionary representing the step expression for the variable.
|
|
1915
|
-
"""
|
|
1916
1765
|
|
|
1766
|
+
"""
|
|
1917
1767
|
vars_dict = self._get_variables_for_asset_type(asset_type)
|
|
1918
1768
|
|
|
1919
|
-
var_expr = next((var_entry['stepExpression'] for var_entry
|
|
1769
|
+
var_expr = next((var_entry['stepExpression'] for var_entry
|
|
1920
1770
|
in vars_dict if var_entry['name'] == var_name), None)
|
|
1921
1771
|
|
|
1922
1772
|
if not var_expr:
|
|
@@ -1927,10 +1777,8 @@ class LanguageGraph():
|
|
|
1927
1777
|
return var_expr
|
|
1928
1778
|
|
|
1929
1779
|
def regenerate_graph(self) -> None:
|
|
1930
|
-
"""
|
|
1931
|
-
Regenerate language graph starting from the MAL language specification
|
|
1780
|
+
"""Regenerate language graph starting from the MAL language specification
|
|
1932
1781
|
given in the constructor.
|
|
1933
1782
|
"""
|
|
1934
|
-
|
|
1935
1783
|
self.assets = {}
|
|
1936
1784
|
self._generate_graph()
|