mal-toolbox 1.1.0__py3-none-any.whl → 1.1.2__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.0.dist-info → mal_toolbox-1.1.2.dist-info}/METADATA +26 -2
- mal_toolbox-1.1.2.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 +491 -636
- maltoolbox/model.py +85 -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.0.dist-info/RECORD +0 -32
- maltoolbox/translators/securicad.py +0 -179
- {mal_toolbox-1.1.0.dist-info → mal_toolbox-1.1.2.dist-info}/WHEEL +0 -0
- {mal_toolbox-1.1.0.dist-info → mal_toolbox-1.1.2.dist-info}/entry_points.txt +0 -0
- {mal_toolbox-1.1.0.dist-info → mal_toolbox-1.1.2.dist-info}/licenses/AUTHORS +0 -0
- {mal_toolbox-1.1.0.dist-info → mal_toolbox-1.1.2.dist-info}/licenses/LICENSE +0 -0
- {mal_toolbox-1.1.0.dist-info → mal_toolbox-1.1.2.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: set[LanguageGraphAsset] = field(default_factory
|
|
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: set[LanguageGraphAsset] = field(default_factory=set)
|
|
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
118
|
return hash(self.name)
|
|
117
119
|
|
|
118
|
-
|
|
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,27 +361,26 @@ 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
386
|
return hash(self.full_name)
|
|
@@ -393,10 +389,8 @@ class LanguageGraphAttackStep:
|
|
|
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,126 @@ 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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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']
|
|
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=set(),
|
|
777
|
+
own_variables={},
|
|
778
|
+
is_abstract=asset['is_abstract']
|
|
827
779
|
)
|
|
828
780
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
+
super_asset.own_sub_assets.add(asset_node)
|
|
792
|
+
asset_node.own_super_asset = super_asset
|
|
793
|
+
|
|
794
|
+
# Associations
|
|
795
|
+
for asset in serialized_graph.values():
|
|
796
|
+
logger.debug('Create associations for asset %s', asset['name'])
|
|
797
|
+
a_node = lang_graph.assets[asset['name']]
|
|
798
|
+
for assoc in asset['associations'].values():
|
|
799
|
+
try:
|
|
800
|
+
left = lang_graph.assets[assoc['left']['asset']]
|
|
801
|
+
right = lang_graph.assets[assoc['right']['asset']]
|
|
802
|
+
except KeyError as e:
|
|
803
|
+
side = 'Left' if 'left' in str(e) else 'Right'
|
|
804
|
+
msg = f'{side} asset for association "{assoc["name"]}" not found'
|
|
805
|
+
logger.error(msg)
|
|
806
|
+
raise LanguageGraphAssociationError(msg)
|
|
852
807
|
assoc_node = LanguageGraphAssociation(
|
|
853
|
-
name
|
|
854
|
-
left_field
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
association['right']['max']),
|
|
864
|
-
info = association['info']
|
|
808
|
+
name=assoc['name'],
|
|
809
|
+
left_field=LanguageGraphAssociationField(
|
|
810
|
+
left, assoc['left']['fieldname'],
|
|
811
|
+
assoc['left']['min'], assoc['left']['max']
|
|
812
|
+
),
|
|
813
|
+
right_field=LanguageGraphAssociationField(
|
|
814
|
+
right, assoc['right']['fieldname'],
|
|
815
|
+
assoc['right']['min'], assoc['right']['max']
|
|
816
|
+
),
|
|
817
|
+
info=assoc['info']
|
|
865
818
|
)
|
|
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
|
|
819
|
+
lang_graph._link_association_to_assets(assoc_node, left, right)
|
|
820
|
+
|
|
821
|
+
# Variables
|
|
822
|
+
for asset in serialized_graph.values():
|
|
823
|
+
a_node = lang_graph.assets[asset['name']]
|
|
824
|
+
for var, (target_name, expr_dict) in asset['variables'].items():
|
|
825
|
+
target = lang_graph.assets[target_name]
|
|
826
|
+
a_node.own_variables[var] = (
|
|
827
|
+
target, ExpressionsChain._from_dict(expr_dict, lang_graph)
|
|
880
828
|
)
|
|
881
|
-
asset.own_variables[variable_name] = (target_asset, expr_chain)
|
|
882
829
|
|
|
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'])
|
|
830
|
+
# Attack steps
|
|
831
|
+
for asset in serialized_graph.values():
|
|
832
|
+
a_node = lang_graph.assets[asset['name']]
|
|
833
|
+
for step in asset['attack_steps'].values():
|
|
834
|
+
a_node.attack_steps[step['name']] = LanguageGraphAttackStep(
|
|
835
|
+
name=step['name'],
|
|
836
|
+
type=step['type'],
|
|
837
|
+
asset=a_node,
|
|
838
|
+
ttc=step['ttc'],
|
|
839
|
+
overrides=step['overrides'],
|
|
840
|
+
own_children={}, own_parents={},
|
|
841
|
+
info=step['info'],
|
|
842
|
+
tags=list(step['tags'])
|
|
901
843
|
)
|
|
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
|
-
|
|
941
|
-
if target_attack_step in attack_step.own_children:
|
|
942
|
-
attack_step.own_children[target_attack_step].append(expr_chain)
|
|
943
|
-
else:
|
|
944
|
-
attack_step.own_children[target_attack_step] = [expr_chain]
|
|
945
844
|
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
+
]
|
|
978
877
|
|
|
979
878
|
return lang_graph
|
|
980
879
|
|
|
981
|
-
|
|
982
880
|
@classmethod
|
|
983
881
|
def load_from_file(cls, filename: str) -> LanguageGraph:
|
|
984
882
|
"""Create LanguageGraph from mal, mar, yaml or json"""
|
|
@@ -989,7 +887,7 @@ class LanguageGraph():
|
|
|
989
887
|
lang_graph = cls.from_mar_archive(filename)
|
|
990
888
|
elif filename.endswith(('.yaml', '.yml')):
|
|
991
889
|
lang_graph = cls._from_dict(load_dict_from_yaml_file(filename))
|
|
992
|
-
elif filename.endswith(
|
|
890
|
+
elif filename.endswith('.json'):
|
|
993
891
|
lang_graph = cls._from_dict(load_dict_from_json_file(filename))
|
|
994
892
|
else:
|
|
995
893
|
raise TypeError(
|
|
@@ -998,18 +896,17 @@ class LanguageGraph():
|
|
|
998
896
|
|
|
999
897
|
if lang_graph:
|
|
1000
898
|
return lang_graph
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
)
|
|
1005
|
-
|
|
899
|
+
raise LanguageGraphException(
|
|
900
|
+
f'Failed to load language graph from file "{filename}".'
|
|
901
|
+
)
|
|
1006
902
|
|
|
1007
903
|
def save_language_specification_to_json(self, filename: str) -> None:
|
|
1008
|
-
"""
|
|
1009
|
-
Save a MAL language specification dictionary to a JSON file
|
|
904
|
+
"""Save a MAL language specification dictionary to a JSON file
|
|
1010
905
|
|
|
1011
906
|
Arguments:
|
|
907
|
+
---------
|
|
1012
908
|
filename - the JSON filename where the language specification will be written
|
|
909
|
+
|
|
1013
910
|
"""
|
|
1014
911
|
logger.info('Save language specification to %s', filename)
|
|
1015
912
|
|
|
@@ -1025,8 +922,7 @@ class LanguageGraph():
|
|
|
1025
922
|
None,
|
|
1026
923
|
str
|
|
1027
924
|
]:
|
|
1028
|
-
"""
|
|
1029
|
-
The attack step expression just adds the name of the attack
|
|
925
|
+
"""The attack step expression just adds the name of the attack
|
|
1030
926
|
step. All other step expressions only modify the target
|
|
1031
927
|
asset and parent associations chain.
|
|
1032
928
|
"""
|
|
@@ -1039,18 +935,16 @@ class LanguageGraph():
|
|
|
1039
935
|
def process_set_operation_step_expression(
|
|
1040
936
|
self,
|
|
1041
937
|
target_asset: LanguageGraphAsset,
|
|
1042
|
-
expr_chain:
|
|
938
|
+
expr_chain: ExpressionsChain | None,
|
|
1043
939
|
step_expression: dict[str, Any]
|
|
1044
940
|
) -> tuple[
|
|
1045
941
|
LanguageGraphAsset,
|
|
1046
942
|
ExpressionsChain,
|
|
1047
943
|
None
|
|
1048
944
|
]:
|
|
1049
|
-
"""
|
|
1050
|
-
The set operators are used to combine the left hand and right
|
|
945
|
+
"""The set operators are used to combine the left hand and right
|
|
1051
946
|
hand targets accordingly.
|
|
1052
947
|
"""
|
|
1053
|
-
|
|
1054
948
|
lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
|
|
1055
949
|
target_asset,
|
|
1056
950
|
expr_chain,
|
|
@@ -1077,9 +971,9 @@ class LanguageGraph():
|
|
|
1077
971
|
)
|
|
1078
972
|
|
|
1079
973
|
new_expr_chain = ExpressionsChain(
|
|
1080
|
-
type
|
|
1081
|
-
left_link
|
|
1082
|
-
right_link
|
|
974
|
+
type=step_expression['type'],
|
|
975
|
+
left_link=lh_expr_chain,
|
|
976
|
+
right_link=rh_expr_chain
|
|
1083
977
|
)
|
|
1084
978
|
return (
|
|
1085
979
|
lh_target_asset,
|
|
@@ -1123,12 +1017,10 @@ class LanguageGraph():
|
|
|
1123
1017
|
ExpressionsChain,
|
|
1124
1018
|
None
|
|
1125
1019
|
]:
|
|
1126
|
-
"""
|
|
1127
|
-
Change the target asset from the current one to the associated
|
|
1020
|
+
"""Change the target asset from the current one to the associated
|
|
1128
1021
|
asset given the specified field name and add the parent
|
|
1129
1022
|
fieldname and association to the parent associations chain.
|
|
1130
1023
|
"""
|
|
1131
|
-
|
|
1132
1024
|
fieldname = step_expression['name']
|
|
1133
1025
|
|
|
1134
1026
|
if target_asset is None:
|
|
@@ -1138,21 +1030,21 @@ class LanguageGraph():
|
|
|
1138
1030
|
|
|
1139
1031
|
new_target_asset = None
|
|
1140
1032
|
for association in target_asset.associations.values():
|
|
1141
|
-
if (association.left_field.fieldname == fieldname and
|
|
1033
|
+
if (association.left_field.fieldname == fieldname and
|
|
1142
1034
|
target_asset.is_subasset_of(
|
|
1143
1035
|
association.right_field.asset)):
|
|
1144
1036
|
new_target_asset = association.left_field.asset
|
|
1145
1037
|
|
|
1146
|
-
if (association.right_field.fieldname == fieldname and
|
|
1038
|
+
if (association.right_field.fieldname == fieldname and
|
|
1147
1039
|
target_asset.is_subasset_of(
|
|
1148
1040
|
association.left_field.asset)):
|
|
1149
1041
|
new_target_asset = association.right_field.asset
|
|
1150
1042
|
|
|
1151
1043
|
if new_target_asset:
|
|
1152
1044
|
new_expr_chain = ExpressionsChain(
|
|
1153
|
-
type
|
|
1154
|
-
fieldname
|
|
1155
|
-
association
|
|
1045
|
+
type='field',
|
|
1046
|
+
fieldname=fieldname,
|
|
1047
|
+
association=association
|
|
1156
1048
|
)
|
|
1157
1049
|
return (
|
|
1158
1050
|
new_target_asset,
|
|
@@ -1167,15 +1059,14 @@ class LanguageGraph():
|
|
|
1167
1059
|
def process_transitive_step_expression(
|
|
1168
1060
|
self,
|
|
1169
1061
|
target_asset: LanguageGraphAsset,
|
|
1170
|
-
expr_chain:
|
|
1062
|
+
expr_chain: ExpressionsChain | None,
|
|
1171
1063
|
step_expression: dict[str, Any]
|
|
1172
1064
|
) -> tuple[
|
|
1173
1065
|
LanguageGraphAsset,
|
|
1174
1066
|
ExpressionsChain,
|
|
1175
1067
|
None
|
|
1176
1068
|
]:
|
|
1177
|
-
"""
|
|
1178
|
-
Create a transitive tuple entry that applies to the next
|
|
1069
|
+
"""Create a transitive tuple entry that applies to the next
|
|
1179
1070
|
component of the step expression.
|
|
1180
1071
|
"""
|
|
1181
1072
|
result_target_asset, result_expr_chain, _ = (
|
|
@@ -1186,8 +1077,8 @@ class LanguageGraph():
|
|
|
1186
1077
|
)
|
|
1187
1078
|
)
|
|
1188
1079
|
new_expr_chain = ExpressionsChain(
|
|
1189
|
-
type
|
|
1190
|
-
sub_link
|
|
1080
|
+
type='transitive',
|
|
1081
|
+
sub_link=result_expr_chain
|
|
1191
1082
|
)
|
|
1192
1083
|
return (
|
|
1193
1084
|
result_target_asset,
|
|
@@ -1198,19 +1089,17 @@ class LanguageGraph():
|
|
|
1198
1089
|
def process_subType_step_expression(
|
|
1199
1090
|
self,
|
|
1200
1091
|
target_asset: LanguageGraphAsset,
|
|
1201
|
-
expr_chain:
|
|
1092
|
+
expr_chain: ExpressionsChain | None,
|
|
1202
1093
|
step_expression: dict[str, Any]
|
|
1203
1094
|
) -> tuple[
|
|
1204
1095
|
LanguageGraphAsset,
|
|
1205
1096
|
ExpressionsChain,
|
|
1206
1097
|
None
|
|
1207
1098
|
]:
|
|
1208
|
-
"""
|
|
1209
|
-
Create a subType tuple entry that applies to the next
|
|
1099
|
+
"""Create a subType tuple entry that applies to the next
|
|
1210
1100
|
component of the step expression and changes the target
|
|
1211
1101
|
asset to the subasset.
|
|
1212
1102
|
"""
|
|
1213
|
-
|
|
1214
1103
|
subtype_name = step_expression['subType']
|
|
1215
1104
|
result_target_asset, result_expr_chain, _ = (
|
|
1216
1105
|
self.process_step_expression(
|
|
@@ -1237,9 +1126,9 @@ class LanguageGraph():
|
|
|
1237
1126
|
)
|
|
1238
1127
|
|
|
1239
1128
|
new_expr_chain = ExpressionsChain(
|
|
1240
|
-
type
|
|
1241
|
-
sub_link
|
|
1242
|
-
subtype
|
|
1129
|
+
type='subType',
|
|
1130
|
+
sub_link=result_expr_chain,
|
|
1131
|
+
subtype=subtype_asset
|
|
1243
1132
|
)
|
|
1244
1133
|
return (
|
|
1245
1134
|
subtype_asset,
|
|
@@ -1250,15 +1139,14 @@ class LanguageGraph():
|
|
|
1250
1139
|
def process_collect_step_expression(
|
|
1251
1140
|
self,
|
|
1252
1141
|
target_asset: LanguageGraphAsset,
|
|
1253
|
-
expr_chain:
|
|
1142
|
+
expr_chain: ExpressionsChain | None,
|
|
1254
1143
|
step_expression: dict[str, Any]
|
|
1255
1144
|
) -> tuple[
|
|
1256
1145
|
LanguageGraphAsset,
|
|
1257
|
-
|
|
1258
|
-
|
|
1146
|
+
ExpressionsChain | None,
|
|
1147
|
+
str | None
|
|
1259
1148
|
]:
|
|
1260
|
-
"""
|
|
1261
|
-
Apply the right hand step expression to left hand step
|
|
1149
|
+
"""Apply the right hand step expression to left hand step
|
|
1262
1150
|
expression target asset and parent associations chain.
|
|
1263
1151
|
"""
|
|
1264
1152
|
lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
|
|
@@ -1280,9 +1168,9 @@ class LanguageGraph():
|
|
|
1280
1168
|
new_expr_chain = lh_expr_chain
|
|
1281
1169
|
if rh_expr_chain:
|
|
1282
1170
|
new_expr_chain = ExpressionsChain(
|
|
1283
|
-
type
|
|
1284
|
-
left_link
|
|
1285
|
-
right_link
|
|
1171
|
+
type='collect',
|
|
1172
|
+
left_link=lh_expr_chain,
|
|
1173
|
+
right_link=rh_expr_chain
|
|
1286
1174
|
)
|
|
1287
1175
|
|
|
1288
1176
|
return (
|
|
@@ -1293,17 +1181,17 @@ class LanguageGraph():
|
|
|
1293
1181
|
|
|
1294
1182
|
def process_step_expression(self,
|
|
1295
1183
|
target_asset: LanguageGraphAsset,
|
|
1296
|
-
expr_chain:
|
|
1184
|
+
expr_chain: ExpressionsChain | None,
|
|
1297
1185
|
step_expression: dict
|
|
1298
1186
|
) -> tuple[
|
|
1299
1187
|
LanguageGraphAsset,
|
|
1300
|
-
|
|
1301
|
-
|
|
1188
|
+
ExpressionsChain | None,
|
|
1189
|
+
str | None
|
|
1302
1190
|
]:
|
|
1303
|
-
"""
|
|
1304
|
-
Recursively process an attack step expression.
|
|
1191
|
+
"""Recursively process an attack step expression.
|
|
1305
1192
|
|
|
1306
1193
|
Arguments:
|
|
1194
|
+
---------
|
|
1307
1195
|
target_asset - The asset type that this step expression should
|
|
1308
1196
|
apply to. Initially it will contain the asset
|
|
1309
1197
|
type to which the attack step belongs.
|
|
@@ -1317,21 +1205,22 @@ class LanguageGraph():
|
|
|
1317
1205
|
step_expression - A dictionary containing the step expression.
|
|
1318
1206
|
|
|
1319
1207
|
Return:
|
|
1208
|
+
------
|
|
1320
1209
|
A tuple triplet containing the target asset, the resulting parent
|
|
1321
1210
|
associations chain, and the name of the attack step.
|
|
1322
|
-
"""
|
|
1323
1211
|
|
|
1212
|
+
"""
|
|
1324
1213
|
if logger.isEnabledFor(logging.DEBUG):
|
|
1325
1214
|
# Avoid running json.dumps when not in debug
|
|
1326
1215
|
logger.debug(
|
|
1327
1216
|
'Processing Step Expression:\n%s',
|
|
1328
|
-
json.dumps(step_expression, indent
|
|
1217
|
+
json.dumps(step_expression, indent=2)
|
|
1329
1218
|
)
|
|
1330
1219
|
|
|
1331
1220
|
result: tuple[
|
|
1332
1221
|
LanguageGraphAsset,
|
|
1333
|
-
|
|
1334
|
-
|
|
1222
|
+
ExpressionsChain | None,
|
|
1223
|
+
str | None
|
|
1335
1224
|
]
|
|
1336
1225
|
|
|
1337
1226
|
match (step_expression['type']):
|
|
@@ -1371,14 +1260,14 @@ class LanguageGraph():
|
|
|
1371
1260
|
|
|
1372
1261
|
def reverse_expr_chain(
|
|
1373
1262
|
self,
|
|
1374
|
-
expr_chain:
|
|
1375
|
-
reverse_chain:
|
|
1376
|
-
) ->
|
|
1377
|
-
"""
|
|
1378
|
-
Recursively reverse the associations chain. From parent to child or
|
|
1263
|
+
expr_chain: ExpressionsChain | None,
|
|
1264
|
+
reverse_chain: ExpressionsChain | None
|
|
1265
|
+
) -> ExpressionsChain | None:
|
|
1266
|
+
"""Recursively reverse the associations chain. From parent to child or
|
|
1379
1267
|
vice versa.
|
|
1380
1268
|
|
|
1381
1269
|
Arguments:
|
|
1270
|
+
---------
|
|
1382
1271
|
expr_chain - A chain of nested tuples that specify the
|
|
1383
1272
|
associations and set operations chain from an
|
|
1384
1273
|
attack step to its connected attack step.
|
|
@@ -1386,93 +1275,96 @@ class LanguageGraph():
|
|
|
1386
1275
|
current reversed associations chain.
|
|
1387
1276
|
|
|
1388
1277
|
Return:
|
|
1278
|
+
------
|
|
1389
1279
|
The resulting reversed associations chain.
|
|
1280
|
+
|
|
1390
1281
|
"""
|
|
1391
1282
|
if not expr_chain:
|
|
1392
1283
|
return reverse_chain
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
if expr_chain.type == 'collect':
|
|
1403
|
-
new_expr_chain = ExpressionsChain(
|
|
1404
|
-
type = expr_chain.type,
|
|
1405
|
-
left_link = right_reverse_chain,
|
|
1406
|
-
right_link = left_reverse_chain
|
|
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)
|
|
1284
|
+
match (expr_chain.type):
|
|
1285
|
+
case 'union' | 'intersection' | 'difference' | 'collect':
|
|
1286
|
+
left_reverse_chain = \
|
|
1287
|
+
self.reverse_expr_chain(expr_chain.left_link,
|
|
1288
|
+
reverse_chain)
|
|
1289
|
+
right_reverse_chain = \
|
|
1290
|
+
self.reverse_expr_chain(expr_chain.right_link,
|
|
1291
|
+
reverse_chain)
|
|
1292
|
+
if expr_chain.type == 'collect':
|
|
1420
1293
|
new_expr_chain = ExpressionsChain(
|
|
1421
|
-
type
|
|
1422
|
-
|
|
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
|
|
1423
1303
|
)
|
|
1424
|
-
return new_expr_chain
|
|
1425
1304
|
|
|
1426
|
-
|
|
1427
|
-
association = expr_chain.association
|
|
1305
|
+
return new_expr_chain
|
|
1428
1306
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
|
1433
1315
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
"Missing field name for expressions chain"
|
|
1437
|
-
)
|
|
1316
|
+
case 'field':
|
|
1317
|
+
association = expr_chain.association
|
|
1438
1318
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
type = 'field',
|
|
1443
|
-
association = association,
|
|
1444
|
-
fieldname = opposite_fieldname
|
|
1319
|
+
if not association:
|
|
1320
|
+
raise LanguageGraphException(
|
|
1321
|
+
"Missing association for expressions chain"
|
|
1445
1322
|
)
|
|
1446
|
-
return new_expr_chain
|
|
1447
1323
|
|
|
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
|
|
1324
|
+
if not expr_chain.fieldname:
|
|
1325
|
+
raise LanguageGraphException(
|
|
1326
|
+
"Missing field name for expressions chain"
|
|
1457
1327
|
)
|
|
1458
|
-
return new_expr_chain
|
|
1459
1328
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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)
|
|
1464
1354
|
|
|
1465
1355
|
def _resolve_variable(self, asset: LanguageGraphAsset, var_name) -> tuple:
|
|
1466
|
-
"""
|
|
1467
|
-
Resolve a variable for a specific asset by variable name.
|
|
1356
|
+
"""Resolve a variable for a specific asset by variable name.
|
|
1468
1357
|
|
|
1469
1358
|
Arguments:
|
|
1359
|
+
---------
|
|
1470
1360
|
asset - a language graph asset to which the variable belongs
|
|
1471
1361
|
var_name - a string representing the variable name
|
|
1472
1362
|
|
|
1473
1363
|
Return:
|
|
1364
|
+
------
|
|
1474
1365
|
A tuple containing the target asset and expressions chain required to
|
|
1475
1366
|
reach it.
|
|
1367
|
+
|
|
1476
1368
|
"""
|
|
1477
1369
|
if var_name not in asset.variables:
|
|
1478
1370
|
var_expr = self._get_var_expr_for_asset(asset.name, var_name)
|
|
@@ -1490,13 +1382,15 @@ class LanguageGraph():
|
|
|
1490
1382
|
lang_spec: dict[str, Any],
|
|
1491
1383
|
assets: dict[str, LanguageGraphAsset]
|
|
1492
1384
|
) -> None:
|
|
1493
|
-
"""
|
|
1385
|
+
"""Link associations to assets based on the language specification.
|
|
1386
|
+
|
|
1494
1387
|
Arguments:
|
|
1388
|
+
---------
|
|
1495
1389
|
lang_spec - the language specification dictionary
|
|
1496
1390
|
assets - a dictionary of LanguageGraphAsset objects
|
|
1497
1391
|
indexed by their names
|
|
1498
|
-
"""
|
|
1499
1392
|
|
|
1393
|
+
"""
|
|
1500
1394
|
for association_dict in lang_spec['associations']:
|
|
1501
1395
|
logger.debug(
|
|
1502
1396
|
'Create association language graph nodes for association %s',
|
|
@@ -1521,20 +1415,20 @@ class LanguageGraph():
|
|
|
1521
1415
|
right_asset = assets[right_asset_name]
|
|
1522
1416
|
|
|
1523
1417
|
assoc_node = LanguageGraphAssociation(
|
|
1524
|
-
name
|
|
1525
|
-
left_field
|
|
1418
|
+
name=association_dict['name'],
|
|
1419
|
+
left_field=LanguageGraphAssociationField(
|
|
1526
1420
|
left_asset,
|
|
1527
1421
|
association_dict['leftField'],
|
|
1528
1422
|
association_dict['leftMultiplicity']['min'],
|
|
1529
1423
|
association_dict['leftMultiplicity']['max']
|
|
1530
1424
|
),
|
|
1531
|
-
right_field
|
|
1425
|
+
right_field=LanguageGraphAssociationField(
|
|
1532
1426
|
right_asset,
|
|
1533
1427
|
association_dict['rightField'],
|
|
1534
1428
|
association_dict['rightMultiplicity']['min'],
|
|
1535
1429
|
association_dict['rightMultiplicity']['max']
|
|
1536
1430
|
),
|
|
1537
|
-
info
|
|
1431
|
+
info=association_dict['meta']
|
|
1538
1432
|
)
|
|
1539
1433
|
|
|
1540
1434
|
# Add the association to the left and right asset
|
|
@@ -1547,10 +1441,8 @@ class LanguageGraph():
|
|
|
1547
1441
|
lang_spec: dict[str, Any],
|
|
1548
1442
|
assets: dict[str, LanguageGraphAsset]
|
|
1549
1443
|
) -> None:
|
|
1444
|
+
"""Link assets based on inheritance and associations.
|
|
1550
1445
|
"""
|
|
1551
|
-
Link assets based on inheritance and associations.
|
|
1552
|
-
"""
|
|
1553
|
-
|
|
1554
1446
|
for asset_dict in lang_spec['assets']:
|
|
1555
1447
|
asset = assets[asset_dict['name']]
|
|
1556
1448
|
if asset_dict['superAsset']:
|
|
@@ -1568,12 +1460,14 @@ class LanguageGraph():
|
|
|
1568
1460
|
def _set_variables_for_assets(
|
|
1569
1461
|
self, assets: dict[str, LanguageGraphAsset]
|
|
1570
1462
|
) -> None:
|
|
1571
|
-
"""
|
|
1463
|
+
"""Set the variables for each asset based on the language specification.
|
|
1464
|
+
|
|
1572
1465
|
Arguments:
|
|
1466
|
+
---------
|
|
1573
1467
|
assets - a dictionary of LanguageGraphAsset objects
|
|
1574
1468
|
indexed by their names
|
|
1575
|
-
"""
|
|
1576
1469
|
|
|
1470
|
+
"""
|
|
1577
1471
|
for asset in assets.values():
|
|
1578
1472
|
logger.debug(
|
|
1579
1473
|
'Set variables for asset %s', asset.name
|
|
@@ -1584,189 +1478,138 @@ class LanguageGraph():
|
|
|
1584
1478
|
# Avoid running json.dumps when not in debug
|
|
1585
1479
|
logger.debug(
|
|
1586
1480
|
'Processing Variable Expression:\n%s',
|
|
1587
|
-
json.dumps(variable, indent
|
|
1481
|
+
json.dumps(variable, indent=2)
|
|
1588
1482
|
)
|
|
1589
1483
|
self._resolve_variable(asset, variable['name'])
|
|
1590
1484
|
|
|
1591
1485
|
def _generate_attack_steps(self, assets) -> None:
|
|
1592
1486
|
"""
|
|
1593
|
-
Generate all
|
|
1594
|
-
|
|
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
|
+
|
|
1497
|
+
Args:
|
|
1498
|
+
assets (dict): Mapping of asset names to asset objects.
|
|
1499
|
+
|
|
1500
|
+
Raises:
|
|
1501
|
+
LanguageGraphStepExpressionError: If a step expression cannot be
|
|
1502
|
+
resolved to a target asset or attack step.
|
|
1503
|
+
LanguageGraphException: If an existence requirement cannot be
|
|
1504
|
+
resolved.
|
|
1595
1505
|
"""
|
|
1596
1506
|
langspec_dict = {}
|
|
1507
|
+
|
|
1597
1508
|
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():
|
|
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():
|
|
1604
1511
|
logger.debug(
|
|
1605
|
-
'Create attack step language graph nodes for %s',
|
|
1606
|
-
attack_step_attribs['name']
|
|
1512
|
+
'Create attack step language graph nodes for %s', step_dict['name']
|
|
1607
1513
|
)
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
if attack_step_attribs['reaches'] else False
|
|
1514
|
+
node = LanguageGraphAttackStep(
|
|
1515
|
+
name=step_dict['name'],
|
|
1516
|
+
type=step_dict['type'],
|
|
1517
|
+
asset=asset,
|
|
1518
|
+
ttc=step_dict['ttc'],
|
|
1519
|
+
overrides=(
|
|
1520
|
+
step_dict['reaches']['overrides']
|
|
1521
|
+
if step_dict['reaches'] else False
|
|
1617
1522
|
),
|
|
1618
|
-
own_children =
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
tags = list(attack_step_attribs['tags'])
|
|
1523
|
+
own_children={}, own_parents={},
|
|
1524
|
+
info=step_dict['meta'],
|
|
1525
|
+
tags=list(step_dict['tags'])
|
|
1622
1526
|
)
|
|
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(
|
|
1527
|
+
langspec_dict[node.full_name] = step_dict
|
|
1528
|
+
asset.attack_steps[node.name] = node
|
|
1529
|
+
|
|
1530
|
+
for det in step_dict.get('detectors', {}).values():
|
|
1531
|
+
node.detectors[det['name']] = Detector(
|
|
1631
1532
|
context=Context(
|
|
1632
|
-
{
|
|
1633
|
-
label: assets[asset]
|
|
1634
|
-
for label, asset in detector["context"].items()
|
|
1635
|
-
}
|
|
1533
|
+
{lbl: assets[a] for lbl, a in det['context'].items()}
|
|
1636
1534
|
),
|
|
1637
|
-
name=
|
|
1638
|
-
type=
|
|
1639
|
-
tprate=
|
|
1535
|
+
name=det.get('name'),
|
|
1536
|
+
type=det.get('type'),
|
|
1537
|
+
tprate=det.get('tprate'),
|
|
1640
1538
|
)
|
|
1641
1539
|
|
|
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
|
|
1540
|
+
pending = list(self.assets.values())
|
|
1541
|
+
while pending:
|
|
1542
|
+
asset = pending.pop(0)
|
|
1543
|
+
super_asset = asset.own_super_asset
|
|
1544
|
+
if super_asset in pending:
|
|
1545
|
+
# Super asset still needs processing, defer this asset
|
|
1546
|
+
pending.append(asset)
|
|
1547
|
+
continue
|
|
1548
|
+
if not super_asset:
|
|
1549
|
+
continue
|
|
1550
|
+
for super_step in super_asset.attack_steps.values():
|
|
1551
|
+
current_step = asset.attack_steps.get(super_step.name)
|
|
1552
|
+
if not current_step:
|
|
1553
|
+
node = LanguageGraphAttackStep(
|
|
1554
|
+
name=super_step.name,
|
|
1555
|
+
type=super_step.type,
|
|
1556
|
+
asset=asset,
|
|
1557
|
+
ttc=super_step.ttc,
|
|
1558
|
+
overrides=False,
|
|
1559
|
+
own_children={},
|
|
1560
|
+
own_parents={},
|
|
1561
|
+
info=super_step.info,
|
|
1562
|
+
tags=list(super_step.tags)
|
|
1563
|
+
)
|
|
1564
|
+
node.inherits = super_step
|
|
1565
|
+
asset.attack_steps[super_step.name] = node
|
|
1566
|
+
elif current_step.overrides:
|
|
1687
1567
|
continue
|
|
1568
|
+
else:
|
|
1569
|
+
current_step.inherits = super_step
|
|
1570
|
+
current_step.tags += super_step.tags
|
|
1571
|
+
current_step.info |= super_step.info
|
|
1688
1572
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1573
|
+
for asset in self.assets.values():
|
|
1574
|
+
for step in asset.attack_steps.values():
|
|
1575
|
+
logger.debug('Determining children for attack step %s', step.name)
|
|
1576
|
+
if step.full_name not in langspec_dict:
|
|
1577
|
+
continue
|
|
1694
1578
|
|
|
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'
|
|
1579
|
+
entry = langspec_dict[step.full_name]
|
|
1580
|
+
for expr in (entry['reaches']['stepExpressions'] if entry['reaches'] else []):
|
|
1581
|
+
tgt_asset, chain, tgt_name = self.process_step_expression(step.asset, None, expr)
|
|
1582
|
+
if not tgt_asset:
|
|
1707
1583
|
raise LanguageGraphStepExpressionError(
|
|
1708
|
-
|
|
1584
|
+
'Failed to find target asset for:\n%s' % json.dumps(expr, indent=2)
|
|
1709
1585
|
)
|
|
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'
|
|
1586
|
+
if tgt_name not in tgt_asset.attack_steps:
|
|
1716
1587
|
raise LanguageGraphStepExpressionError(
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
target_asset.name,
|
|
1720
|
-
json.dumps(step_expression, indent = 2)
|
|
1721
|
-
)
|
|
1588
|
+
'Failed to find target attack step %s on %s:\n%s' %
|
|
1589
|
+
(tgt_name, tgt_asset.name, json.dumps(expr, indent=2))
|
|
1722
1590
|
)
|
|
1723
1591
|
|
|
1724
|
-
|
|
1725
|
-
|
|
1592
|
+
tgt = tgt_asset.attack_steps[tgt_name]
|
|
1593
|
+
step.own_children.setdefault(tgt, []).append(chain)
|
|
1594
|
+
tgt.own_parents.setdefault(step, []).append(self.reverse_expr_chain(chain, None))
|
|
1726
1595
|
|
|
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:
|
|
1596
|
+
if step.type in ('exist', 'notExist'):
|
|
1597
|
+
reqs = entry['requires']['stepExpressions'] if entry['requires'] else []
|
|
1598
|
+
if not reqs:
|
|
1745
1599
|
raise LanguageGraphStepExpressionError(
|
|
1746
|
-
'
|
|
1747
|
-
|
|
1748
|
-
attack_step.name,
|
|
1749
|
-
attack_step.type,
|
|
1750
|
-
json.dumps(langspec_entry, indent = 2)
|
|
1751
|
-
)
|
|
1600
|
+
'Missing requirements for "%s" of type "%s":\n%s' %
|
|
1601
|
+
(step.name, step.type, json.dumps(entry, indent=2))
|
|
1752
1602
|
)
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
None,
|
|
1759
|
-
step_expression
|
|
1603
|
+
for expr in reqs:
|
|
1604
|
+
_, chain, _ = self.process_step_expression(step.asset, None, expr)
|
|
1605
|
+
if chain is None:
|
|
1606
|
+
raise LanguageGraphException(
|
|
1607
|
+
f'Failed to find existence step requirement for:\n{expr}'
|
|
1760
1608
|
)
|
|
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)
|
|
1609
|
+
step.own_requires.append(chain)
|
|
1766
1610
|
|
|
1767
1611
|
def _generate_graph(self) -> None:
|
|
1768
|
-
"""
|
|
1769
|
-
Generate language graph starting from the MAL language specification
|
|
1612
|
+
"""Generate language graph starting from the MAL language specification
|
|
1770
1613
|
given in the constructor.
|
|
1771
1614
|
"""
|
|
1772
1615
|
# Generate all of the asset nodes of the language graph.
|
|
@@ -1777,14 +1620,14 @@ class LanguageGraph():
|
|
|
1777
1620
|
asset_dict['name']
|
|
1778
1621
|
)
|
|
1779
1622
|
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
|
|
1623
|
+
name=asset_dict['name'],
|
|
1624
|
+
own_associations={},
|
|
1625
|
+
attack_steps={},
|
|
1626
|
+
info=asset_dict['meta'],
|
|
1627
|
+
own_super_asset=None,
|
|
1628
|
+
own_sub_assets=set(),
|
|
1629
|
+
own_variables={},
|
|
1630
|
+
is_abstract=asset_dict['isAbstract']
|
|
1788
1631
|
)
|
|
1789
1632
|
self.assets[asset_dict['name']] = asset_node
|
|
1790
1633
|
|
|
@@ -1801,24 +1644,26 @@ class LanguageGraph():
|
|
|
1801
1644
|
self._generate_attack_steps(self.assets)
|
|
1802
1645
|
|
|
1803
1646
|
def _get_attacks_for_asset_type(self, asset_type: str) -> dict[str, dict]:
|
|
1804
|
-
"""
|
|
1805
|
-
Get all Attack Steps for a specific asset type.
|
|
1647
|
+
"""Get all Attack Steps for a specific asset type.
|
|
1806
1648
|
|
|
1807
1649
|
Arguments:
|
|
1650
|
+
---------
|
|
1808
1651
|
asset_type - the name of the asset type we want to
|
|
1809
1652
|
list the possible attack steps for
|
|
1810
1653
|
|
|
1811
1654
|
Return:
|
|
1655
|
+
------
|
|
1812
1656
|
A dictionary containing the possible attacks for the
|
|
1813
1657
|
specified asset type. Each key in the dictionary is an attack name
|
|
1814
1658
|
associated with a dictionary containing other characteristics of the
|
|
1815
1659
|
attack such as type of attack, TTC distribution, child attack steps
|
|
1816
1660
|
and other information
|
|
1661
|
+
|
|
1817
1662
|
"""
|
|
1818
1663
|
attack_steps: dict = {}
|
|
1819
1664
|
try:
|
|
1820
1665
|
asset = next(
|
|
1821
|
-
asset for asset in self._lang_spec['assets']
|
|
1666
|
+
asset for asset in self._lang_spec['assets']
|
|
1822
1667
|
if asset['name'] == asset_type
|
|
1823
1668
|
)
|
|
1824
1669
|
except StopIteration:
|
|
@@ -1838,17 +1683,19 @@ class LanguageGraph():
|
|
|
1838
1683
|
return attack_steps
|
|
1839
1684
|
|
|
1840
1685
|
def _get_associations_for_asset_type(self, asset_type: str) -> list[dict]:
|
|
1841
|
-
"""
|
|
1842
|
-
Get all associations for a specific asset type.
|
|
1686
|
+
"""Get all associations for a specific asset type.
|
|
1843
1687
|
|
|
1844
1688
|
Arguments:
|
|
1689
|
+
---------
|
|
1845
1690
|
asset_type - the name of the asset type for which we want to
|
|
1846
1691
|
list the associations
|
|
1847
1692
|
|
|
1848
1693
|
Return:
|
|
1694
|
+
------
|
|
1849
1695
|
A list of dicts, where each dict represents an associations
|
|
1850
1696
|
for the specified asset type. Each dictionary contains
|
|
1851
1697
|
name and meta information about the association.
|
|
1698
|
+
|
|
1852
1699
|
"""
|
|
1853
1700
|
logger.debug(
|
|
1854
1701
|
'Get associations for %s asset from '
|
|
@@ -1856,7 +1703,7 @@ class LanguageGraph():
|
|
|
1856
1703
|
)
|
|
1857
1704
|
associations: list = []
|
|
1858
1705
|
|
|
1859
|
-
asset = next((asset for asset in self._lang_spec['assets']
|
|
1706
|
+
asset = next((asset for asset in self._lang_spec['assets']
|
|
1860
1707
|
if asset['name'] == asset_type), None)
|
|
1861
1708
|
if not asset:
|
|
1862
1709
|
logger.error(
|
|
@@ -1865,8 +1712,8 @@ class LanguageGraph():
|
|
|
1865
1712
|
)
|
|
1866
1713
|
return associations
|
|
1867
1714
|
|
|
1868
|
-
assoc_iter = (assoc for assoc in self._lang_spec['associations']
|
|
1869
|
-
if assoc['leftAsset'] == asset_type or
|
|
1715
|
+
assoc_iter = (assoc for assoc in self._lang_spec['associations']
|
|
1716
|
+
if assoc['leftAsset'] == asset_type or
|
|
1870
1717
|
assoc['rightAsset'] == asset_type)
|
|
1871
1718
|
assoc = next(assoc_iter, None)
|
|
1872
1719
|
while assoc:
|
|
@@ -1877,20 +1724,21 @@ class LanguageGraph():
|
|
|
1877
1724
|
|
|
1878
1725
|
def _get_variables_for_asset_type(
|
|
1879
1726
|
self, asset_type: str) -> list[dict]:
|
|
1880
|
-
"""
|
|
1881
|
-
Get variables for a specific asset type.
|
|
1727
|
+
"""Get variables for a specific asset type.
|
|
1882
1728
|
Note: Variables are the ones specified in MAL through `let` statements
|
|
1883
1729
|
|
|
1884
1730
|
Arguments:
|
|
1731
|
+
---------
|
|
1885
1732
|
asset_type - a string representing the asset type which
|
|
1886
1733
|
contains the variables
|
|
1887
1734
|
|
|
1888
1735
|
Return:
|
|
1736
|
+
------
|
|
1889
1737
|
A list of dicts representing the step expressions for the variables
|
|
1890
1738
|
belonging to the asset.
|
|
1891
|
-
"""
|
|
1892
1739
|
|
|
1893
|
-
|
|
1740
|
+
"""
|
|
1741
|
+
asset_dict = next((asset for asset in self._lang_spec['assets']
|
|
1894
1742
|
if asset['name'] == asset_type), None)
|
|
1895
1743
|
if not asset_dict:
|
|
1896
1744
|
msg = 'Failed to find asset type %s in language specification '\
|
|
@@ -1902,21 +1750,22 @@ class LanguageGraph():
|
|
|
1902
1750
|
|
|
1903
1751
|
def _get_var_expr_for_asset(
|
|
1904
1752
|
self, asset_type: str, var_name) -> dict:
|
|
1905
|
-
"""
|
|
1906
|
-
Get a variable for a specific asset type by variable name.
|
|
1753
|
+
"""Get a variable for a specific asset type by variable name.
|
|
1907
1754
|
|
|
1908
1755
|
Arguments:
|
|
1756
|
+
---------
|
|
1909
1757
|
asset_type - a string representing the type of asset which
|
|
1910
1758
|
contains the variable
|
|
1911
1759
|
var_name - a string representing the variable name
|
|
1912
1760
|
|
|
1913
1761
|
Return:
|
|
1762
|
+
------
|
|
1914
1763
|
A dictionary representing the step expression for the variable.
|
|
1915
|
-
"""
|
|
1916
1764
|
|
|
1765
|
+
"""
|
|
1917
1766
|
vars_dict = self._get_variables_for_asset_type(asset_type)
|
|
1918
1767
|
|
|
1919
|
-
var_expr = next((var_entry['stepExpression'] for var_entry
|
|
1768
|
+
var_expr = next((var_entry['stepExpression'] for var_entry
|
|
1920
1769
|
in vars_dict if var_entry['name'] == var_name), None)
|
|
1921
1770
|
|
|
1922
1771
|
if not var_expr:
|
|
@@ -1927,10 +1776,16 @@ class LanguageGraph():
|
|
|
1927
1776
|
return var_expr
|
|
1928
1777
|
|
|
1929
1778
|
def regenerate_graph(self) -> None:
|
|
1930
|
-
"""
|
|
1931
|
-
Regenerate language graph starting from the MAL language specification
|
|
1779
|
+
"""Regenerate language graph starting from the MAL language specification
|
|
1932
1780
|
given in the constructor.
|
|
1933
1781
|
"""
|
|
1934
|
-
|
|
1935
1782
|
self.assets = {}
|
|
1936
1783
|
self._generate_graph()
|
|
1784
|
+
|
|
1785
|
+
def __getstate__(self):
|
|
1786
|
+
return self._to_dict()
|
|
1787
|
+
|
|
1788
|
+
def __setstate__(self, state):
|
|
1789
|
+
temp_lang_graph = self._from_dict(state)
|
|
1790
|
+
self.assets = temp_lang_graph.assets
|
|
1791
|
+
self.metadata = temp_lang_graph.metadata
|