mal-toolbox 0.1.12__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mal_toolbox-0.1.12.dist-info → mal_toolbox-0.3.0.dist-info}/METADATA +43 -25
- mal_toolbox-0.3.0.dist-info/RECORD +29 -0
- mal_toolbox-0.3.0.dist-info/entry_points.txt +2 -0
- maltoolbox/__init__.py +38 -57
- maltoolbox/__main__.py +43 -14
- maltoolbox/attackgraph/__init__.py +1 -1
- maltoolbox/attackgraph/analyzers/apriori.py +10 -6
- maltoolbox/attackgraph/attacker.py +26 -13
- maltoolbox/attackgraph/attackgraph.py +431 -355
- maltoolbox/attackgraph/node.py +72 -54
- maltoolbox/attackgraph/query.py +4 -2
- maltoolbox/file_utils.py +4 -8
- maltoolbox/ingestors/neo4j.py +146 -157
- maltoolbox/language/__init__.py +10 -2
- maltoolbox/language/compiler/__init__.py +485 -17
- maltoolbox/language/compiler/mal_lexer.py +172 -152
- maltoolbox/language/compiler/mal_parser.py +1370 -663
- maltoolbox/language/languagegraph.py +1096 -545
- maltoolbox/model.py +312 -485
- maltoolbox/translators/securicad.py +164 -163
- maltoolbox/translators/updater.py +231 -108
- mal_toolbox-0.1.12.dist-info/RECORD +0 -32
- maltoolbox/default.conf +0 -17
- maltoolbox/language/classes_factory.py +0 -243
- maltoolbox/language/compiler/mal_visitor.py +0 -416
- maltoolbox/wrappers.py +0 -62
- {mal_toolbox-0.1.12.dist-info → mal_toolbox-0.3.0.dist-info}/AUTHORS +0 -0
- {mal_toolbox-0.1.12.dist-info → mal_toolbox-0.3.0.dist-info}/LICENSE +0 -0
- {mal_toolbox-0.1.12.dist-info → mal_toolbox-0.3.0.dist-info}/WHEEL +0 -0
- {mal_toolbox-0.1.12.dist-info → mal_toolbox-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -4,12 +4,12 @@ MAL-Toolbox Language Graph Module
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
import copy
|
|
8
7
|
import logging
|
|
9
8
|
import json
|
|
10
9
|
import zipfile
|
|
11
10
|
|
|
12
11
|
from dataclasses import dataclass, field
|
|
12
|
+
from functools import cached_property
|
|
13
13
|
from typing import Any, Optional
|
|
14
14
|
|
|
15
15
|
from maltoolbox.file_utils import (
|
|
@@ -26,43 +26,91 @@ from ..exceptions import (
|
|
|
26
26
|
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
28
28
|
|
|
29
|
+
|
|
30
|
+
def disaggregate_attack_step_full_name(
|
|
31
|
+
attack_step_full_name: str) -> list[str]:
|
|
32
|
+
return attack_step_full_name.split(':')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Detector:
|
|
37
|
+
context: Context
|
|
38
|
+
name: Optional[str]
|
|
39
|
+
type: Optional[str]
|
|
40
|
+
tprate: Optional[dict]
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict:
|
|
43
|
+
return {
|
|
44
|
+
"context": self.context.to_dict(),
|
|
45
|
+
"name": self.name,
|
|
46
|
+
"type": self.type,
|
|
47
|
+
"tprate": self.tprate,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Context(dict):
|
|
52
|
+
def __init__(self, context) -> None:
|
|
53
|
+
super().__init__(context)
|
|
54
|
+
self._context_dict = context
|
|
55
|
+
for label, asset in context.items():
|
|
56
|
+
setattr(self, label, asset)
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> dict:
|
|
59
|
+
return {label: asset.name for label, asset in self.items()}
|
|
60
|
+
|
|
61
|
+
def __str__(self) -> str:
|
|
62
|
+
return str({label: asset.name for label, asset in self._context_dict.items()})
|
|
63
|
+
|
|
64
|
+
|
|
29
65
|
@dataclass
|
|
30
66
|
class LanguageGraphAsset:
|
|
31
|
-
name:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
67
|
+
name: str
|
|
68
|
+
own_associations: dict[str, LanguageGraphAssociation] = \
|
|
69
|
+
field(default_factory = dict)
|
|
70
|
+
attack_steps: dict[str, LanguageGraphAttackStep] = \
|
|
71
|
+
field(default_factory = dict)
|
|
72
|
+
info: dict = field(default_factory = dict)
|
|
73
|
+
own_super_asset: Optional[LanguageGraphAsset] = None
|
|
74
|
+
own_sub_assets: set[LanguageGraphAsset] = field(default_factory = set)
|
|
75
|
+
own_variables: dict = field(default_factory = dict)
|
|
39
76
|
is_abstract: Optional[bool] = None
|
|
40
77
|
|
|
78
|
+
|
|
41
79
|
def to_dict(self) -> dict:
|
|
42
80
|
"""Convert LanguageGraphAsset to dictionary"""
|
|
43
81
|
node_dict: dict[str, Any] = {
|
|
44
82
|
'name': self.name,
|
|
45
|
-
'associations':
|
|
46
|
-
'attack_steps':
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
|
|
83
|
+
'associations': {},
|
|
84
|
+
'attack_steps': {},
|
|
85
|
+
'info': self.info,
|
|
86
|
+
'super_asset': self.own_super_asset.name \
|
|
87
|
+
if self.own_super_asset else "",
|
|
88
|
+
'sub_assets': [asset.name for asset in self.own_sub_assets],
|
|
89
|
+
'variables': {},
|
|
90
|
+
'is_abstract': self.is_abstract
|
|
50
91
|
}
|
|
51
92
|
|
|
52
|
-
for assoc in self.
|
|
53
|
-
node_dict['associations'].
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
node_dict['
|
|
60
|
-
|
|
61
|
-
|
|
93
|
+
for fieldname, assoc in self.own_associations.items():
|
|
94
|
+
node_dict['associations'][fieldname] = assoc.to_dict()
|
|
95
|
+
for attack_step in self.attack_steps.values():
|
|
96
|
+
node_dict['attack_steps'][attack_step.name] = \
|
|
97
|
+
attack_step.to_dict()
|
|
98
|
+
for variable_name, (var_target_asset, var_expr_chain) in \
|
|
99
|
+
self.own_variables.items():
|
|
100
|
+
node_dict['variables'][variable_name] = (
|
|
101
|
+
var_target_asset.name,
|
|
102
|
+
var_expr_chain.to_dict()
|
|
103
|
+
)
|
|
62
104
|
return node_dict
|
|
63
105
|
|
|
106
|
+
|
|
64
107
|
def __repr__(self) -> str:
|
|
65
|
-
return
|
|
108
|
+
return f'LanguageGraphAsset(name: "{self.name}")'
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def __hash__(self):
|
|
112
|
+
return hash(self.name)
|
|
113
|
+
|
|
66
114
|
|
|
67
115
|
def is_subasset_of(self, target_asset: LanguageGraphAsset) -> bool:
|
|
68
116
|
"""
|
|
@@ -76,15 +124,16 @@ class LanguageGraphAsset:
|
|
|
76
124
|
True if this asset extends the target_asset via inheritance.
|
|
77
125
|
False otherwise.
|
|
78
126
|
"""
|
|
79
|
-
|
|
80
|
-
while (
|
|
81
|
-
current_asset = current_assets.pop()
|
|
127
|
+
current_asset: Optional[LanguageGraphAsset] = self
|
|
128
|
+
while (current_asset):
|
|
82
129
|
if current_asset == target_asset:
|
|
83
130
|
return True
|
|
84
|
-
|
|
131
|
+
current_asset = current_asset.own_super_asset
|
|
85
132
|
return False
|
|
86
133
|
|
|
87
|
-
|
|
134
|
+
|
|
135
|
+
@cached_property
|
|
136
|
+
def sub_assets(self) -> set[LanguageGraphAsset]:
|
|
88
137
|
"""
|
|
89
138
|
Return a list of all of the assets that directly or indirectly extend
|
|
90
139
|
this asset.
|
|
@@ -92,15 +141,18 @@ class LanguageGraphAsset:
|
|
|
92
141
|
Return:
|
|
93
142
|
A list of all of the assets that extend this asset plus itself.
|
|
94
143
|
"""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
144
|
+
subassets: list[LanguageGraphAsset] = []
|
|
145
|
+
for subasset in self.own_sub_assets:
|
|
146
|
+
subassets.extend(subasset.sub_assets)
|
|
147
|
+
|
|
148
|
+
subassets.extend(self.own_sub_assets)
|
|
149
|
+
subassets.append(self)
|
|
150
|
+
|
|
151
|
+
return set(subassets)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@cached_property
|
|
155
|
+
def super_assets(self) -> list[LanguageGraphAsset]:
|
|
104
156
|
"""
|
|
105
157
|
Return a list of all of the assets that this asset directly or
|
|
106
158
|
indirectly extends.
|
|
@@ -108,24 +160,79 @@ class LanguageGraphAsset:
|
|
|
108
160
|
Return:
|
|
109
161
|
A list of all of the assets that this asset extends plus itself.
|
|
110
162
|
"""
|
|
111
|
-
|
|
112
|
-
superassets = [
|
|
113
|
-
while (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
superassets.extend(current_asset.super_assets)
|
|
163
|
+
current_asset: Optional[LanguageGraphAsset] = self
|
|
164
|
+
superassets = []
|
|
165
|
+
while (current_asset):
|
|
166
|
+
superassets.append(current_asset)
|
|
167
|
+
current_asset = current_asset.own_super_asset
|
|
117
168
|
return superassets
|
|
118
169
|
|
|
170
|
+
|
|
171
|
+
@cached_property
|
|
172
|
+
def associations(self) -> dict[str, LanguageGraphAssociation]:
|
|
173
|
+
"""
|
|
174
|
+
Return a list of all of the associations that belong to this asset
|
|
175
|
+
directly or indirectly via inheritance.
|
|
176
|
+
|
|
177
|
+
Return:
|
|
178
|
+
A list of all of the associations that apply to this asset, either
|
|
179
|
+
directly or via inheritance.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
associations = dict(self.own_associations)
|
|
183
|
+
if self.own_super_asset:
|
|
184
|
+
associations |= self.own_super_asset.associations
|
|
185
|
+
return associations
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def variables(self) -> dict[str, ExpressionsChain]:
|
|
190
|
+
"""
|
|
191
|
+
Return a list of all of the variables that belong to this asset
|
|
192
|
+
directly or indirectly via inheritance.
|
|
193
|
+
|
|
194
|
+
Return:
|
|
195
|
+
A list of all of the variables that apply to this asset, either
|
|
196
|
+
directly or via inheritance.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
all_vars = dict(self.own_variables)
|
|
200
|
+
if self.own_super_asset:
|
|
201
|
+
all_vars |= self.own_super_asset.variables
|
|
202
|
+
return all_vars
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_variable(
|
|
206
|
+
self,
|
|
207
|
+
var_name: str,
|
|
208
|
+
) -> Optional[tuple]:
|
|
209
|
+
"""
|
|
210
|
+
Return a variable matching the given name if the asset or any of its
|
|
211
|
+
super assets has its definition.
|
|
212
|
+
|
|
213
|
+
Return:
|
|
214
|
+
A tuple containing the target asset and expressions chain to it if the
|
|
215
|
+
variable was defined.
|
|
216
|
+
None otherwise.
|
|
217
|
+
"""
|
|
218
|
+
current_asset: Optional[LanguageGraphAsset] = self
|
|
219
|
+
while (current_asset):
|
|
220
|
+
if var_name in current_asset.own_variables:
|
|
221
|
+
return current_asset.own_variables[var_name]
|
|
222
|
+
current_asset = current_asset.own_super_asset
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
|
|
119
226
|
def get_all_common_superassets(
|
|
120
227
|
self, other: LanguageGraphAsset
|
|
121
|
-
) -> set[
|
|
228
|
+
) -> set[str]:
|
|
122
229
|
"""Return a set of all common ancestors between this asset
|
|
123
230
|
and the other asset given as parameter"""
|
|
124
231
|
self_superassets = set(
|
|
125
|
-
asset.name for asset in self.
|
|
232
|
+
asset.name for asset in self.super_assets
|
|
126
233
|
)
|
|
127
234
|
other_superassets = set(
|
|
128
|
-
asset.name for asset in other.
|
|
235
|
+
asset.name for asset in other.super_assets
|
|
129
236
|
)
|
|
130
237
|
return self_superassets.intersection(other_superassets)
|
|
131
238
|
|
|
@@ -143,12 +250,13 @@ class LanguageGraphAssociation:
|
|
|
143
250
|
name: str
|
|
144
251
|
left_field: LanguageGraphAssociationField
|
|
145
252
|
right_field: LanguageGraphAssociationField
|
|
146
|
-
|
|
253
|
+
info: dict = field(default_factory = dict)
|
|
147
254
|
|
|
148
255
|
def to_dict(self) -> dict:
|
|
149
256
|
"""Convert LanguageGraphAssociation to dictionary"""
|
|
150
|
-
|
|
257
|
+
assoc_dict = {
|
|
151
258
|
'name': self.name,
|
|
259
|
+
'info': self.info,
|
|
152
260
|
'left': {
|
|
153
261
|
'asset': self.left_field.asset.name,
|
|
154
262
|
'fieldname': self.left_field.fieldname,
|
|
@@ -160,14 +268,41 @@ class LanguageGraphAssociation:
|
|
|
160
268
|
'fieldname': self.right_field.fieldname,
|
|
161
269
|
'min': self.right_field.minimum,
|
|
162
270
|
'max': self.right_field.maximum
|
|
163
|
-
}
|
|
164
|
-
'description': self.description
|
|
271
|
+
}
|
|
165
272
|
}
|
|
166
273
|
|
|
167
|
-
return
|
|
274
|
+
return assoc_dict
|
|
275
|
+
|
|
168
276
|
|
|
169
277
|
def __repr__(self) -> str:
|
|
170
|
-
return
|
|
278
|
+
return (f'LanguageGraphAssociation(name: "{self.name}", '
|
|
279
|
+
f'left_field: {self.left_field}, '
|
|
280
|
+
f'right_field: {self.right_field})')
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def full_name(self) -> str:
|
|
285
|
+
"""
|
|
286
|
+
Return the full name of the association. This is a combination of the
|
|
287
|
+
association name, left field name, left asset type, right field name,
|
|
288
|
+
and right asset type.
|
|
289
|
+
"""
|
|
290
|
+
full_name = '%s_%s_%s' % (
|
|
291
|
+
self.name,\
|
|
292
|
+
self.left_field.fieldname,\
|
|
293
|
+
self.right_field.fieldname
|
|
294
|
+
)
|
|
295
|
+
return full_name
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def get_field(self, fieldname: str) -> LanguageGraphAssociationField:
|
|
299
|
+
"""
|
|
300
|
+
Return the field that matches the `fieldname` given as parameter.
|
|
301
|
+
"""
|
|
302
|
+
if self.right_field.fieldname == fieldname:
|
|
303
|
+
return self.right_field
|
|
304
|
+
return self.left_field
|
|
305
|
+
|
|
171
306
|
|
|
172
307
|
def contains_fieldname(self, fieldname: str) -> bool:
|
|
173
308
|
"""
|
|
@@ -184,6 +319,7 @@ class LanguageGraphAssociation:
|
|
|
184
319
|
return True
|
|
185
320
|
return False
|
|
186
321
|
|
|
322
|
+
|
|
187
323
|
def contains_asset(self, asset: Any) -> bool:
|
|
188
324
|
"""
|
|
189
325
|
Check if the association matches the asset given as a parameter. A
|
|
@@ -201,6 +337,7 @@ class LanguageGraphAssociation:
|
|
|
201
337
|
return True
|
|
202
338
|
return False
|
|
203
339
|
|
|
340
|
+
|
|
204
341
|
def get_opposite_fieldname(self, fieldname: str) -> str:
|
|
205
342
|
"""
|
|
206
343
|
Return the opposite field name if the association contains the field
|
|
@@ -221,49 +358,36 @@ class LanguageGraphAssociation:
|
|
|
221
358
|
logger.error(msg, fieldname, self.name)
|
|
222
359
|
raise LanguageGraphAssociationError(msg % (fieldname, self.name))
|
|
223
360
|
|
|
224
|
-
def get_opposite_asset(
|
|
225
|
-
self, asset: LanguageGraphAsset
|
|
226
|
-
) -> Optional[LanguageGraphAsset]:
|
|
227
|
-
"""
|
|
228
|
-
Return the opposite asset if the association matches the asset given
|
|
229
|
-
as a parameter. A match can either be an explicit one or if the asset
|
|
230
|
-
given subassets either of the two assets that are part of the
|
|
231
|
-
association.
|
|
232
|
-
|
|
233
|
-
Arguments:
|
|
234
|
-
asset - the asset to look for
|
|
235
|
-
Return the other asset if the parameter matched either of the
|
|
236
|
-
two. None, otherwise.
|
|
237
|
-
"""
|
|
238
|
-
#TODO Should check to see which one is the tightest fit for
|
|
239
|
-
# associations between assets on different levels of the same
|
|
240
|
-
# inheritance chain.
|
|
241
|
-
if asset.is_subasset_of(self.left_field.asset):
|
|
242
|
-
return self.right_field.asset
|
|
243
|
-
if asset.is_subasset_of(self.right_field.asset):
|
|
244
|
-
return self.left_field.asset
|
|
245
|
-
|
|
246
|
-
logger.warning(
|
|
247
|
-
'Requested asset "%s" from association %s'
|
|
248
|
-
'which did not contain it!', asset.name, self.name
|
|
249
|
-
)
|
|
250
|
-
return None
|
|
251
|
-
|
|
252
361
|
|
|
253
362
|
@dataclass
|
|
254
363
|
class LanguageGraphAttackStep:
|
|
255
364
|
name: str
|
|
256
365
|
type: str
|
|
257
366
|
asset: LanguageGraphAsset
|
|
258
|
-
ttc: dict = field(default_factory =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
367
|
+
ttc: dict = field(default_factory = dict)
|
|
368
|
+
overrides: bool = False
|
|
369
|
+
children: dict = field(default_factory = dict)
|
|
370
|
+
parents: dict = field(default_factory = dict)
|
|
371
|
+
info: dict = field(default_factory = dict)
|
|
372
|
+
inherits: Optional[LanguageGraphAttackStep] = None
|
|
373
|
+
tags: set = field(default_factory = set)
|
|
374
|
+
_attributes: Optional[dict] = None
|
|
375
|
+
detectors: dict = field(default_factory = lambda: {})
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def __hash__(self):
|
|
379
|
+
return hash(self.full_name)
|
|
380
|
+
|
|
263
381
|
|
|
264
382
|
@property
|
|
265
|
-
def
|
|
266
|
-
|
|
383
|
+
def full_name(self) -> str:
|
|
384
|
+
"""
|
|
385
|
+
Return the full name of the attack step. This is a combination of the
|
|
386
|
+
asset type name to which the attack step belongs and attack step name
|
|
387
|
+
itself.
|
|
388
|
+
"""
|
|
389
|
+
full_name = self.asset.name + ':' + self.name
|
|
390
|
+
return full_name
|
|
267
391
|
|
|
268
392
|
def to_dict(self) -> dict:
|
|
269
393
|
node_dict: dict[Any, Any] = {
|
|
@@ -273,95 +397,140 @@ class LanguageGraphAttackStep:
|
|
|
273
397
|
'ttc': self.ttc,
|
|
274
398
|
'children': {},
|
|
275
399
|
'parents': {},
|
|
276
|
-
'
|
|
400
|
+
'info': self.info,
|
|
401
|
+
'overrides': self.overrides,
|
|
402
|
+
'inherits': self.inherits.full_name if self.inherits else None,
|
|
403
|
+
'tags': list(self.tags),
|
|
404
|
+
'detectors': {label: detector.to_dict() for label, detector in
|
|
405
|
+
self.detectors.items()},
|
|
277
406
|
}
|
|
278
407
|
|
|
279
408
|
for child in self.children:
|
|
280
409
|
node_dict['children'][child] = []
|
|
281
|
-
for (_,
|
|
282
|
-
if
|
|
410
|
+
for (_, expr_chain) in self.children[child]:
|
|
411
|
+
if expr_chain:
|
|
283
412
|
node_dict['children'][child].append(
|
|
284
|
-
|
|
413
|
+
expr_chain.to_dict())
|
|
285
414
|
else:
|
|
286
415
|
node_dict['children'][child].append(None)
|
|
287
416
|
|
|
288
417
|
for parent in self.parents:
|
|
289
418
|
node_dict['parents'][parent] = []
|
|
290
|
-
for (_,
|
|
291
|
-
if
|
|
419
|
+
for (_, expr_chain) in self.parents[parent]:
|
|
420
|
+
if expr_chain:
|
|
292
421
|
node_dict['parents'][parent].append(
|
|
293
|
-
|
|
422
|
+
expr_chain.to_dict())
|
|
294
423
|
else:
|
|
295
424
|
node_dict['parents'][parent].append(None)
|
|
296
425
|
|
|
426
|
+
if hasattr(self, 'own_requires'):
|
|
427
|
+
node_dict['requires'] = []
|
|
428
|
+
for requirement in self.own_requires:
|
|
429
|
+
node_dict['requires'].append(requirement.to_dict())
|
|
430
|
+
|
|
297
431
|
return node_dict
|
|
298
432
|
|
|
433
|
+
|
|
434
|
+
@cached_property
|
|
435
|
+
def requires(self):
|
|
436
|
+
if not hasattr(self, 'own_requires'):
|
|
437
|
+
requirements = []
|
|
438
|
+
else:
|
|
439
|
+
requirements = self.own_requires
|
|
440
|
+
|
|
441
|
+
if self.inherits:
|
|
442
|
+
requirements.extend(self.inherits.requires)
|
|
443
|
+
return requirements
|
|
444
|
+
|
|
445
|
+
|
|
299
446
|
def __repr__(self) -> str:
|
|
300
447
|
return str(self.to_dict())
|
|
301
448
|
|
|
302
449
|
|
|
303
|
-
class
|
|
304
|
-
def __init__(self,
|
|
450
|
+
class ExpressionsChain:
|
|
451
|
+
def __init__(self,
|
|
452
|
+
type: str,
|
|
453
|
+
left_link: Optional[ExpressionsChain] = None,
|
|
454
|
+
right_link: Optional[ExpressionsChain] = None,
|
|
455
|
+
sub_link: Optional[ExpressionsChain] = None,
|
|
456
|
+
fieldname: Optional[str] = None,
|
|
457
|
+
association = None,
|
|
458
|
+
subtype = None
|
|
459
|
+
):
|
|
305
460
|
self.type = type
|
|
306
|
-
self.
|
|
307
|
-
self.
|
|
308
|
-
self.
|
|
309
|
-
self.
|
|
310
|
-
self.
|
|
311
|
-
self.subtype: Optional[Any] =
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
def __iter__(self):
|
|
315
|
-
self.current_link = self
|
|
316
|
-
return self
|
|
317
|
-
|
|
318
|
-
def __next__(self):
|
|
319
|
-
if self.current_link:
|
|
320
|
-
dep_chain = self.current_link
|
|
321
|
-
self.current_link = self.current_link.next_link
|
|
322
|
-
return dep_chain
|
|
323
|
-
raise StopIteration
|
|
461
|
+
self.left_link: Optional[ExpressionsChain] = left_link
|
|
462
|
+
self.right_link: Optional[ExpressionsChain] = right_link
|
|
463
|
+
self.sub_link: Optional[ExpressionsChain] = sub_link
|
|
464
|
+
self.fieldname: Optional[str] = fieldname
|
|
465
|
+
self.association: Optional[LanguageGraphAssociation] = association
|
|
466
|
+
self.subtype: Optional[Any] = subtype
|
|
467
|
+
|
|
324
468
|
|
|
325
469
|
def to_dict(self) -> dict:
|
|
326
|
-
"""Convert
|
|
470
|
+
"""Convert ExpressionsChain to dictionary"""
|
|
327
471
|
match (self.type):
|
|
328
|
-
case 'union' | 'intersection' | 'difference':
|
|
329
|
-
return {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
472
|
+
case 'union' | 'intersection' | 'difference' | 'collect':
|
|
473
|
+
return {
|
|
474
|
+
self.type: {
|
|
475
|
+
'left': self.left_link.to_dict()
|
|
476
|
+
if self.left_link else {},
|
|
477
|
+
'right': self.right_link.to_dict()
|
|
478
|
+
if self.right_link else {}
|
|
479
|
+
},
|
|
480
|
+
'type': self.type
|
|
335
481
|
}
|
|
336
482
|
|
|
337
483
|
case 'field':
|
|
338
484
|
if not self.association:
|
|
339
|
-
raise LanguageGraphAssociationError(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
485
|
+
raise LanguageGraphAssociationError(
|
|
486
|
+
"Missing association for expressions chain"
|
|
487
|
+
)
|
|
488
|
+
if self.fieldname == self.association.left_field.fieldname:
|
|
489
|
+
asset_type = self.association.left_field.asset.name
|
|
490
|
+
elif self.fieldname == self.association.right_field.fieldname:
|
|
491
|
+
asset_type = self.association.right_field.asset.name
|
|
492
|
+
else:
|
|
493
|
+
raise LanguageGraphException(
|
|
494
|
+
'Failed to find fieldname "%s" in association:\n%s' %
|
|
495
|
+
(
|
|
496
|
+
self.fieldname,
|
|
497
|
+
json.dumps(self.association.to_dict(),
|
|
498
|
+
indent = 2)
|
|
499
|
+
)
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
self.association.name:
|
|
504
|
+
{
|
|
505
|
+
'fieldname': self.fieldname,
|
|
506
|
+
'asset type': asset_type
|
|
507
|
+
},
|
|
508
|
+
'type': self.type
|
|
509
|
+
}
|
|
347
510
|
|
|
348
511
|
case 'transitive':
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
512
|
+
if not self.sub_link:
|
|
513
|
+
raise LanguageGraphException(
|
|
514
|
+
"No sub link for transitive expressions chain"
|
|
515
|
+
)
|
|
516
|
+
return {
|
|
517
|
+
'transitive': self.sub_link.to_dict(),
|
|
518
|
+
'type': self.type
|
|
352
519
|
}
|
|
353
520
|
|
|
354
521
|
case 'subType':
|
|
355
522
|
if not self.subtype:
|
|
356
523
|
raise LanguageGraphException(
|
|
357
|
-
"No subtype for
|
|
524
|
+
"No subtype for expressions chain"
|
|
358
525
|
)
|
|
359
|
-
if not self.
|
|
526
|
+
if not self.sub_link:
|
|
360
527
|
raise LanguageGraphException(
|
|
361
|
-
"No
|
|
528
|
+
"No sub link for subtype expressions chain"
|
|
362
529
|
)
|
|
363
|
-
return {
|
|
364
|
-
'
|
|
530
|
+
return {
|
|
531
|
+
'subType': self.subtype.name,
|
|
532
|
+
'expression': self.sub_link.to_dict(),
|
|
533
|
+
'type': self.type
|
|
365
534
|
}
|
|
366
535
|
|
|
367
536
|
case _:
|
|
@@ -369,22 +538,133 @@ class DependencyChain:
|
|
|
369
538
|
logger.error(msg, self.type)
|
|
370
539
|
raise LanguageGraphAssociationError(msg % self.type)
|
|
371
540
|
|
|
541
|
+
@classmethod
|
|
542
|
+
def _from_dict(cls,
|
|
543
|
+
serialized_expr_chain: dict,
|
|
544
|
+
lang_graph: LanguageGraph,
|
|
545
|
+
) -> Optional[ExpressionsChain]:
|
|
546
|
+
"""Create ExpressionsChain from dict
|
|
547
|
+
Args:
|
|
548
|
+
serialized_expr_chain - expressions chain in dict format
|
|
549
|
+
lang_graph - the LanguageGraph that contains the assets,
|
|
550
|
+
associations, and attack steps relevant for
|
|
551
|
+
the expressions chain
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
if serialized_expr_chain is None or not serialized_expr_chain:
|
|
555
|
+
return None
|
|
556
|
+
|
|
557
|
+
if 'type' not in serialized_expr_chain:
|
|
558
|
+
logger.debug(json.dumps(serialized_expr_chain, indent = 2))
|
|
559
|
+
msg = 'Missing expressions chain type!'
|
|
560
|
+
logger.error(msg)
|
|
561
|
+
raise LanguageGraphAssociationError(msg)
|
|
562
|
+
return None
|
|
563
|
+
|
|
564
|
+
expr_chain_type = serialized_expr_chain['type']
|
|
565
|
+
match (expr_chain_type):
|
|
566
|
+
case 'union' | 'intersection' | 'difference' | 'collect':
|
|
567
|
+
left_link = cls._from_dict(
|
|
568
|
+
serialized_expr_chain[expr_chain_type]['left'],
|
|
569
|
+
lang_graph
|
|
570
|
+
)
|
|
571
|
+
right_link = cls._from_dict(
|
|
572
|
+
serialized_expr_chain[expr_chain_type]['right'],
|
|
573
|
+
lang_graph
|
|
574
|
+
)
|
|
575
|
+
new_expr_chain = ExpressionsChain(
|
|
576
|
+
type = expr_chain_type,
|
|
577
|
+
left_link = left_link,
|
|
578
|
+
right_link = right_link
|
|
579
|
+
)
|
|
580
|
+
return new_expr_chain
|
|
581
|
+
|
|
582
|
+
case 'field':
|
|
583
|
+
assoc_name = list(serialized_expr_chain.keys())[0]
|
|
584
|
+
target_asset = lang_graph.assets[\
|
|
585
|
+
serialized_expr_chain[assoc_name]['asset type']]
|
|
586
|
+
fieldname = serialized_expr_chain[assoc_name]['fieldname']
|
|
587
|
+
|
|
588
|
+
association = None
|
|
589
|
+
for assoc in target_asset.associations.values():
|
|
590
|
+
if assoc.contains_fieldname(fieldname) and \
|
|
591
|
+
assoc.name == assoc_name:
|
|
592
|
+
association = assoc
|
|
593
|
+
break
|
|
594
|
+
|
|
595
|
+
if association is None:
|
|
596
|
+
msg = 'Failed to find association "%s" with '\
|
|
597
|
+
'fieldname "%s"'
|
|
598
|
+
logger.error(msg % (assoc_name, fieldname))
|
|
599
|
+
raise LanguageGraphException(msg % (assoc_name,
|
|
600
|
+
fieldname))
|
|
601
|
+
|
|
602
|
+
new_expr_chain = ExpressionsChain(
|
|
603
|
+
type = 'field',
|
|
604
|
+
association = association,
|
|
605
|
+
fieldname = fieldname
|
|
606
|
+
)
|
|
607
|
+
return new_expr_chain
|
|
608
|
+
|
|
609
|
+
case 'transitive':
|
|
610
|
+
sub_link = cls._from_dict(
|
|
611
|
+
serialized_expr_chain['transitive'],
|
|
612
|
+
lang_graph
|
|
613
|
+
)
|
|
614
|
+
new_expr_chain = ExpressionsChain(
|
|
615
|
+
type = 'transitive',
|
|
616
|
+
sub_link = sub_link
|
|
617
|
+
)
|
|
618
|
+
return new_expr_chain
|
|
619
|
+
|
|
620
|
+
case 'subType':
|
|
621
|
+
sub_link = cls._from_dict(
|
|
622
|
+
serialized_expr_chain['expression'],
|
|
623
|
+
lang_graph
|
|
624
|
+
)
|
|
625
|
+
subtype_name = serialized_expr_chain['subType']
|
|
626
|
+
if subtype_name in lang_graph.assets:
|
|
627
|
+
subtype_asset = lang_graph.assets[subtype_name]
|
|
628
|
+
else:
|
|
629
|
+
msg = 'Failed to find subtype %s'
|
|
630
|
+
logger.error(msg % subtype_name)
|
|
631
|
+
raise LanguageGraphException(msg % subtype_name)
|
|
632
|
+
|
|
633
|
+
new_expr_chain = ExpressionsChain(
|
|
634
|
+
type = 'subType',
|
|
635
|
+
sub_link = sub_link,
|
|
636
|
+
subtype = subtype_asset
|
|
637
|
+
)
|
|
638
|
+
return new_expr_chain
|
|
639
|
+
|
|
640
|
+
case _:
|
|
641
|
+
msg = 'Unknown expressions chain type %s!'
|
|
642
|
+
logger.error(msg, serialized_expr_chain['type'])
|
|
643
|
+
raise LanguageGraphAssociationError(msg %
|
|
644
|
+
serialized_expr_chain['type'])
|
|
645
|
+
|
|
646
|
+
|
|
372
647
|
def __repr__(self) -> str:
|
|
373
648
|
return str(self.to_dict())
|
|
374
649
|
|
|
375
650
|
|
|
376
651
|
class LanguageGraph():
|
|
377
652
|
"""Graph representation of a MAL language"""
|
|
378
|
-
def __init__(self, lang: dict):
|
|
379
|
-
self.assets:
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
653
|
+
def __init__(self, lang: Optional[dict] = None):
|
|
654
|
+
self.assets: dict = {}
|
|
655
|
+
if lang is not None:
|
|
656
|
+
self._lang_spec: dict = lang
|
|
657
|
+
self.metadata = {
|
|
658
|
+
"version": lang["defines"]["version"],
|
|
659
|
+
"id": lang["defines"]["id"],
|
|
660
|
+
}
|
|
661
|
+
self._generate_graph()
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def __repr__(self) -> str:
|
|
665
|
+
return (f'LanguageGraph(id: "{self.metadata.get("id", "N/A")}", '
|
|
666
|
+
f'version: "{self.metadata.get("version", "N/A")}")')
|
|
667
|
+
|
|
388
668
|
|
|
389
669
|
@classmethod
|
|
390
670
|
def from_mal_spec(cls, mal_spec_file: str) -> LanguageGraph:
|
|
@@ -397,6 +677,7 @@ class LanguageGraph():
|
|
|
397
677
|
logger.info("Loading mal spec %s", mal_spec_file)
|
|
398
678
|
return LanguageGraph(MalCompiler().compile(mal_spec_file))
|
|
399
679
|
|
|
680
|
+
|
|
400
681
|
@classmethod
|
|
401
682
|
def from_mar_archive(cls, mar_archive: str) -> LanguageGraph:
|
|
402
683
|
"""
|
|
@@ -411,39 +692,241 @@ class LanguageGraph():
|
|
|
411
692
|
langspec = archive.read('langspec.json')
|
|
412
693
|
return LanguageGraph(json.loads(langspec))
|
|
413
694
|
|
|
695
|
+
|
|
414
696
|
def _to_dict(self):
|
|
415
697
|
"""Converts LanguageGraph into a dict"""
|
|
416
|
-
serialized_assets = []
|
|
417
|
-
for asset in self.assets:
|
|
418
|
-
serialized_assets.append(asset.to_dict())
|
|
419
|
-
serialized_associations = []
|
|
420
|
-
for associations in self.associations:
|
|
421
|
-
serialized_associations.append(associations.to_dict())
|
|
422
|
-
serialized_attack_steps = []
|
|
423
|
-
for attack_step in self.attack_steps:
|
|
424
|
-
serialized_attack_steps.append(attack_step.to_dict())
|
|
425
698
|
|
|
426
699
|
logger.debug(
|
|
427
|
-
'Serializing %s assets,
|
|
428
|
-
len(serialized_assets), len(serialized_associations),
|
|
429
|
-
len(serialized_attack_steps)
|
|
700
|
+
'Serializing %s assets.', len(self.assets.items())
|
|
430
701
|
)
|
|
431
702
|
|
|
432
|
-
serialized_graph = {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
703
|
+
serialized_graph = {'metadata': self.metadata}
|
|
704
|
+
for asset in self.assets.values():
|
|
705
|
+
serialized_graph[asset.name] = asset.to_dict()
|
|
706
|
+
|
|
437
707
|
return serialized_graph
|
|
438
708
|
|
|
709
|
+
def _link_association_to_assets(cls,
|
|
710
|
+
assoc: LanguageGraphAssociation,
|
|
711
|
+
left_asset: LanguageGraphAsset,
|
|
712
|
+
right_asset: LanguageGraphAsset):
|
|
713
|
+
left_asset.own_associations[assoc.right_field.fieldname] = assoc
|
|
714
|
+
right_asset.own_associations[assoc.left_field.fieldname] = assoc
|
|
715
|
+
|
|
439
716
|
def save_to_file(self, filename: str) -> None:
|
|
440
717
|
"""Save to json/yml depending on extension"""
|
|
441
718
|
return save_dict_to_file(filename, self._to_dict())
|
|
442
719
|
|
|
720
|
+
|
|
443
721
|
@classmethod
|
|
444
|
-
def _from_dict(cls,
|
|
445
|
-
|
|
446
|
-
|
|
722
|
+
def _from_dict(cls, serialized_graph: dict) -> LanguageGraph:
|
|
723
|
+
"""Create LanguageGraph from dict
|
|
724
|
+
Args:
|
|
725
|
+
serialized_graph - LanguageGraph in dict format
|
|
726
|
+
"""
|
|
727
|
+
|
|
728
|
+
logger.debug('Create language graph from dictionary.')
|
|
729
|
+
lang_graph = LanguageGraph()
|
|
730
|
+
lang_graph.metadata = serialized_graph.pop('metadata')
|
|
731
|
+
|
|
732
|
+
# Recreate all of the assets
|
|
733
|
+
for asset_dict in serialized_graph.values():
|
|
734
|
+
logger.debug(
|
|
735
|
+
'Create asset language graph nodes for asset %s',
|
|
736
|
+
asset_dict['name']
|
|
737
|
+
)
|
|
738
|
+
asset_node = LanguageGraphAsset(
|
|
739
|
+
name = asset_dict['name'],
|
|
740
|
+
own_associations = {},
|
|
741
|
+
attack_steps = {},
|
|
742
|
+
info = asset_dict['info'],
|
|
743
|
+
own_super_asset = None,
|
|
744
|
+
own_sub_assets = set(),
|
|
745
|
+
own_variables = {},
|
|
746
|
+
is_abstract = asset_dict['is_abstract']
|
|
747
|
+
)
|
|
748
|
+
lang_graph.assets[asset_dict['name']] = asset_node
|
|
749
|
+
|
|
750
|
+
# Relink assets based on inheritance
|
|
751
|
+
for asset_dict in serialized_graph.values():
|
|
752
|
+
asset = lang_graph.assets[asset_dict['name']]
|
|
753
|
+
super_asset_name = asset_dict['super_asset']
|
|
754
|
+
if not super_asset_name:
|
|
755
|
+
continue
|
|
756
|
+
|
|
757
|
+
super_asset = lang_graph.assets[super_asset_name]
|
|
758
|
+
if not super_asset:
|
|
759
|
+
msg = 'Failed to find super asset "%s" for asset "%s"!'
|
|
760
|
+
logger.error(
|
|
761
|
+
msg, asset_dict["super_asset"], asset_dict["name"])
|
|
762
|
+
raise LanguageGraphSuperAssetNotFoundError(
|
|
763
|
+
msg % (asset_dict["super_asset"], asset_dict["name"]))
|
|
764
|
+
|
|
765
|
+
super_asset.own_sub_assets.add(asset)
|
|
766
|
+
asset.own_super_asset = super_asset
|
|
767
|
+
|
|
768
|
+
# Generate all of the association nodes of the language graph.
|
|
769
|
+
for asset_dict in serialized_graph.values():
|
|
770
|
+
logger.debug(
|
|
771
|
+
'Create association language graph nodes for asset %s',
|
|
772
|
+
asset_dict['name']
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
asset = lang_graph.assets[asset_dict['name']]
|
|
776
|
+
for association in asset_dict['associations'].values():
|
|
777
|
+
left_asset = lang_graph.assets[association['left']['asset']]
|
|
778
|
+
if not left_asset:
|
|
779
|
+
msg = 'Left asset "%s" for association "%s" not found!'
|
|
780
|
+
logger.error(
|
|
781
|
+
msg, association['left']['asset'],
|
|
782
|
+
association['name'])
|
|
783
|
+
raise LanguageGraphAssociationError(
|
|
784
|
+
msg % (association['left']['asset'],
|
|
785
|
+
association['name']))
|
|
786
|
+
|
|
787
|
+
right_asset = lang_graph.assets[association['right']['asset']]
|
|
788
|
+
if not right_asset:
|
|
789
|
+
msg = 'Right asset "%s" for association "%s" not found!'
|
|
790
|
+
logger.error(
|
|
791
|
+
msg, association['right']['asset'],
|
|
792
|
+
association['name'])
|
|
793
|
+
raise LanguageGraphAssociationError(
|
|
794
|
+
msg % (association['right']['asset'],
|
|
795
|
+
association['name'])
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
assoc_node = LanguageGraphAssociation(
|
|
799
|
+
name = association['name'],
|
|
800
|
+
left_field = LanguageGraphAssociationField(
|
|
801
|
+
left_asset,
|
|
802
|
+
association['left']['fieldname'],
|
|
803
|
+
association['left']['min'],
|
|
804
|
+
association['left']['max']),
|
|
805
|
+
right_field = LanguageGraphAssociationField(
|
|
806
|
+
right_asset,
|
|
807
|
+
association['right']['fieldname'],
|
|
808
|
+
association['right']['min'],
|
|
809
|
+
association['right']['max']),
|
|
810
|
+
info = association['info']
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
# Add the association to the left and right asset
|
|
814
|
+
lang_graph._link_association_to_assets(assoc_node,
|
|
815
|
+
left_asset, right_asset)
|
|
816
|
+
|
|
817
|
+
# Recreate the variables
|
|
818
|
+
for asset_dict in serialized_graph.values():
|
|
819
|
+
asset = lang_graph.assets[asset_dict['name']]
|
|
820
|
+
for variable_name, var_target in asset_dict['variables'].items():
|
|
821
|
+
(target_asset_name, expr_chain_dict) = var_target
|
|
822
|
+
target_asset = lang_graph.assets[target_asset_name]
|
|
823
|
+
expr_chain = ExpressionsChain._from_dict(
|
|
824
|
+
expr_chain_dict,
|
|
825
|
+
lang_graph
|
|
826
|
+
)
|
|
827
|
+
asset.own_variables[variable_name] = (target_asset, expr_chain)
|
|
828
|
+
|
|
829
|
+
# Recreate the attack steps
|
|
830
|
+
for asset_dict in serialized_graph.values():
|
|
831
|
+
asset = lang_graph.assets[asset_dict['name']]
|
|
832
|
+
logger.debug(
|
|
833
|
+
'Create attack steps language graph nodes for asset %s',
|
|
834
|
+
asset_dict['name']
|
|
835
|
+
)
|
|
836
|
+
for attack_step_dict in asset_dict['attack_steps'].values():
|
|
837
|
+
attack_step_node = LanguageGraphAttackStep(
|
|
838
|
+
name = attack_step_dict['name'],
|
|
839
|
+
type = attack_step_dict['type'],
|
|
840
|
+
asset = asset,
|
|
841
|
+
ttc = attack_step_dict['ttc'],
|
|
842
|
+
overrides = attack_step_dict['overrides'],
|
|
843
|
+
children = {},
|
|
844
|
+
parents = {},
|
|
845
|
+
info = attack_step_dict['info'],
|
|
846
|
+
tags = set(attack_step_dict['tags'])
|
|
847
|
+
)
|
|
848
|
+
asset.attack_steps[attack_step_dict['name']] = \
|
|
849
|
+
attack_step_node
|
|
850
|
+
|
|
851
|
+
# Relink attack steps based on inheritence
|
|
852
|
+
for asset_dict in serialized_graph.values():
|
|
853
|
+
asset = lang_graph.assets[asset_dict['name']]
|
|
854
|
+
for attack_step_dict in asset_dict['attack_steps'].values():
|
|
855
|
+
if 'inherits' in attack_step_dict and \
|
|
856
|
+
attack_step_dict['inherits'] is not None:
|
|
857
|
+
attack_step = asset.attack_steps[
|
|
858
|
+
attack_step_dict['name']]
|
|
859
|
+
ancestor_asset_name, ancestor_attack_step_name = \
|
|
860
|
+
disaggregate_attack_step_full_name(
|
|
861
|
+
attack_step_dict['inherits'])
|
|
862
|
+
ancestor_asset = lang_graph.assets[ancestor_asset_name]
|
|
863
|
+
ancestor_attack_step = ancestor_asset.attack_steps[\
|
|
864
|
+
ancestor_attack_step_name]
|
|
865
|
+
attack_step.inherits = ancestor_attack_step
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
# Relink attack steps based on expressions chains
|
|
869
|
+
for asset_dict in serialized_graph.values():
|
|
870
|
+
asset = lang_graph.assets[asset_dict['name']]
|
|
871
|
+
for attack_step_dict in asset_dict['attack_steps'].values():
|
|
872
|
+
attack_step = asset.attack_steps[attack_step_dict['name']]
|
|
873
|
+
for child_target in attack_step_dict['children'].items():
|
|
874
|
+
target_full_attack_step_name = child_target[0]
|
|
875
|
+
expr_chains = child_target[1]
|
|
876
|
+
target_asset_name, target_attack_step_name = \
|
|
877
|
+
disaggregate_attack_step_full_name(
|
|
878
|
+
target_full_attack_step_name)
|
|
879
|
+
target_asset = lang_graph.assets[target_asset_name]
|
|
880
|
+
target_attack_step = target_asset.attack_steps[
|
|
881
|
+
target_attack_step_name]
|
|
882
|
+
for expr_chain_dict in expr_chains:
|
|
883
|
+
expr_chain = ExpressionsChain._from_dict(
|
|
884
|
+
expr_chain_dict,
|
|
885
|
+
lang_graph
|
|
886
|
+
)
|
|
887
|
+
if target_attack_step.full_name in attack_step.children:
|
|
888
|
+
attack_step.children[target_attack_step.full_name].\
|
|
889
|
+
append((target_attack_step, expr_chain))
|
|
890
|
+
else:
|
|
891
|
+
attack_step.children[target_attack_step.full_name] = \
|
|
892
|
+
[(target_attack_step, expr_chain)]
|
|
893
|
+
|
|
894
|
+
for parent_target in attack_step_dict['parents'].items():
|
|
895
|
+
target_full_attack_step_name = parent_target[0]
|
|
896
|
+
expr_chains = parent_target[1]
|
|
897
|
+
target_asset_name, target_attack_step_name = \
|
|
898
|
+
disaggregate_attack_step_full_name(
|
|
899
|
+
target_full_attack_step_name)
|
|
900
|
+
target_asset = lang_graph.assets[target_asset_name]
|
|
901
|
+
target_attack_step = target_asset.attack_steps[
|
|
902
|
+
target_attack_step_name]
|
|
903
|
+
for expr_chain_dict in expr_chains:
|
|
904
|
+
expr_chain = ExpressionsChain._from_dict(
|
|
905
|
+
expr_chain_dict,
|
|
906
|
+
lang_graph
|
|
907
|
+
)
|
|
908
|
+
if target_attack_step.full_name in attack_step.parents:
|
|
909
|
+
attack_step.parents[target_attack_step.full_name].\
|
|
910
|
+
append((target_attack_step, expr_chain))
|
|
911
|
+
else:
|
|
912
|
+
attack_step.parents[target_attack_step.full_name] = \
|
|
913
|
+
[(target_attack_step, expr_chain)]
|
|
914
|
+
|
|
915
|
+
# Recreate the requirements of exist and notExist attack steps
|
|
916
|
+
if attack_step.type == 'exist' or \
|
|
917
|
+
attack_step.type == 'notExist':
|
|
918
|
+
if 'requires' in attack_step_dict:
|
|
919
|
+
expr_chains = attack_step_dict['requires']
|
|
920
|
+
attack_step.own_requires = []
|
|
921
|
+
for expr_chain_dict in expr_chains:
|
|
922
|
+
expr_chain = ExpressionsChain._from_dict(
|
|
923
|
+
expr_chain_dict,
|
|
924
|
+
lang_graph
|
|
925
|
+
)
|
|
926
|
+
attack_step.own_requires.append(expr_chain)
|
|
927
|
+
|
|
928
|
+
return lang_graph
|
|
929
|
+
|
|
447
930
|
|
|
448
931
|
@classmethod
|
|
449
932
|
def load_from_file(cls, filename: str) -> LanguageGraph:
|
|
@@ -453,17 +936,23 @@ class LanguageGraph():
|
|
|
453
936
|
lang_graph = cls.from_mal_spec(filename)
|
|
454
937
|
elif filename.endswith('.mar'):
|
|
455
938
|
lang_graph = cls.from_mar_archive(filename)
|
|
456
|
-
elif filename.endswith(('yaml', 'yml')):
|
|
939
|
+
elif filename.endswith(('.yaml', '.yml')):
|
|
457
940
|
lang_graph = cls._from_dict(load_dict_from_yaml_file(filename))
|
|
458
|
-
elif filename.endswith(('json')):
|
|
941
|
+
elif filename.endswith(('.json')):
|
|
459
942
|
lang_graph = cls._from_dict(load_dict_from_json_file(filename))
|
|
943
|
+
else:
|
|
944
|
+
raise TypeError(
|
|
945
|
+
"Unknown file extension, expected json/mal/mar/yml/yaml"
|
|
946
|
+
)
|
|
460
947
|
|
|
461
948
|
if lang_graph:
|
|
462
949
|
return lang_graph
|
|
950
|
+
else:
|
|
951
|
+
raise LanguageGraphException(
|
|
952
|
+
f'Failed to load language graph from file "{filename}".'
|
|
953
|
+
)
|
|
954
|
+
|
|
463
955
|
|
|
464
|
-
raise TypeError(
|
|
465
|
-
"Unknown file extension, expected json/mal/mar/yml/yaml"
|
|
466
|
-
)
|
|
467
956
|
|
|
468
957
|
def save_language_specification_to_json(self, filename: str) -> None:
|
|
469
958
|
"""
|
|
@@ -477,23 +966,21 @@ class LanguageGraph():
|
|
|
477
966
|
with open(filename, 'w', encoding='utf-8') as file:
|
|
478
967
|
json.dump(self._lang_spec, file, indent=4)
|
|
479
968
|
|
|
969
|
+
|
|
480
970
|
def process_step_expression(self,
|
|
481
|
-
lang: dict,
|
|
482
971
|
target_asset,
|
|
483
|
-
|
|
972
|
+
expr_chain,
|
|
484
973
|
step_expression: dict
|
|
485
974
|
) -> tuple:
|
|
486
975
|
"""
|
|
487
976
|
Recursively process an attack step expression.
|
|
488
977
|
|
|
489
978
|
Arguments:
|
|
490
|
-
lang - A dictionary representing the MAL language
|
|
491
|
-
specification.
|
|
492
979
|
target_asset - The asset type that this step expression should
|
|
493
980
|
apply to. Initially it will contain the asset
|
|
494
981
|
type to which the attack step belongs.
|
|
495
|
-
|
|
496
|
-
set operations from the attack step to its
|
|
982
|
+
expr_chain - A expressions chain of linked of associations
|
|
983
|
+
and set operations from the attack step to its
|
|
497
984
|
parent attack step.
|
|
498
985
|
Note: This was done for the parent attack step
|
|
499
986
|
because it was easier to construct recursively
|
|
@@ -518,19 +1005,29 @@ class LanguageGraph():
|
|
|
518
1005
|
# The attack step expression just adds the name of the attack
|
|
519
1006
|
# step. All other step expressions only modify the target
|
|
520
1007
|
# asset and parent associations chain.
|
|
521
|
-
return (
|
|
522
|
-
|
|
523
|
-
|
|
1008
|
+
return (
|
|
1009
|
+
target_asset,
|
|
1010
|
+
None,
|
|
1011
|
+
step_expression['name']
|
|
1012
|
+
)
|
|
524
1013
|
|
|
525
1014
|
case 'union' | 'intersection' | 'difference':
|
|
526
1015
|
# The set operators are used to combine the left hand and right
|
|
527
1016
|
# hand targets accordingly.
|
|
528
|
-
lh_target_asset,
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
1017
|
+
lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
|
|
1018
|
+
target_asset,
|
|
1019
|
+
expr_chain,
|
|
1020
|
+
step_expression['lhs']
|
|
1021
|
+
)
|
|
1022
|
+
rh_target_asset, rh_expr_chain, _ = \
|
|
1023
|
+
self.process_step_expression(
|
|
1024
|
+
target_asset,
|
|
1025
|
+
expr_chain,
|
|
1026
|
+
step_expression['rhs']
|
|
1027
|
+
)
|
|
532
1028
|
|
|
533
|
-
if not lh_target_asset.get_all_common_superassets(
|
|
1029
|
+
if not lh_target_asset.get_all_common_superassets(
|
|
1030
|
+
rh_target_asset):
|
|
534
1031
|
logger.error(
|
|
535
1032
|
"Set operation attempted between targets that"
|
|
536
1033
|
" do not share any common superassets: %s and %s!",
|
|
@@ -538,30 +1035,32 @@ class LanguageGraph():
|
|
|
538
1035
|
)
|
|
539
1036
|
return (None, None, None)
|
|
540
1037
|
|
|
541
|
-
|
|
1038
|
+
new_expr_chain = ExpressionsChain(
|
|
542
1039
|
type = step_expression['type'],
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
return (
|
|
547
|
-
|
|
548
|
-
|
|
1040
|
+
left_link = lh_expr_chain,
|
|
1041
|
+
right_link = rh_expr_chain
|
|
1042
|
+
)
|
|
1043
|
+
return (
|
|
1044
|
+
lh_target_asset,
|
|
1045
|
+
new_expr_chain,
|
|
1046
|
+
None
|
|
1047
|
+
)
|
|
549
1048
|
|
|
550
1049
|
case 'variable':
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
1050
|
+
var_name = step_expression['name']
|
|
1051
|
+
var_target_asset, var_expr_chain = self._resolve_variable(
|
|
1052
|
+
target_asset, var_name)
|
|
1053
|
+
var_target_asset, var_expr_chain = \
|
|
1054
|
+
target_asset.get_variable(var_name)
|
|
1055
|
+
if var_expr_chain is not None:
|
|
1056
|
+
return (
|
|
1057
|
+
var_target_asset,
|
|
1058
|
+
var_expr_chain,
|
|
1059
|
+
None
|
|
1060
|
+
)
|
|
562
1061
|
else:
|
|
563
1062
|
logger.error(
|
|
564
|
-
'Failed to find variable %s for %s',
|
|
1063
|
+
'Failed to find variable \"%s\" for %s',
|
|
565
1064
|
step_expression["name"], target_asset.name
|
|
566
1065
|
)
|
|
567
1066
|
return (None, None, None)
|
|
@@ -578,7 +1077,7 @@ class LanguageGraph():
|
|
|
578
1077
|
return (None, None, None)
|
|
579
1078
|
|
|
580
1079
|
new_target_asset = None
|
|
581
|
-
for association in target_asset.associations:
|
|
1080
|
+
for association in target_asset.associations.values():
|
|
582
1081
|
if (association.left_field.fieldname == fieldname and \
|
|
583
1082
|
target_asset.is_subasset_of(
|
|
584
1083
|
association.right_field.asset)):
|
|
@@ -590,15 +1089,16 @@ class LanguageGraph():
|
|
|
590
1089
|
new_target_asset = association.right_field.asset
|
|
591
1090
|
|
|
592
1091
|
if new_target_asset:
|
|
593
|
-
|
|
1092
|
+
new_expr_chain = ExpressionsChain(
|
|
594
1093
|
type = 'field',
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
None
|
|
1094
|
+
fieldname = fieldname,
|
|
1095
|
+
association = association
|
|
1096
|
+
)
|
|
1097
|
+
return (
|
|
1098
|
+
new_target_asset,
|
|
1099
|
+
new_expr_chain,
|
|
1100
|
+
None
|
|
1101
|
+
)
|
|
602
1102
|
logger.error(
|
|
603
1103
|
'Failed to find field "%s" on asset "%s"!',
|
|
604
1104
|
fieldname, target_asset.name
|
|
@@ -609,18 +1109,22 @@ class LanguageGraph():
|
|
|
609
1109
|
# Create a transitive tuple entry that applies to the next
|
|
610
1110
|
# component of the step expression.
|
|
611
1111
|
result_target_asset, \
|
|
612
|
-
|
|
1112
|
+
result_expr_chain, \
|
|
613
1113
|
attack_step = \
|
|
614
|
-
self.process_step_expression(
|
|
1114
|
+
self.process_step_expression(
|
|
615
1115
|
target_asset,
|
|
616
|
-
|
|
617
|
-
step_expression['stepExpression']
|
|
618
|
-
|
|
1116
|
+
expr_chain,
|
|
1117
|
+
step_expression['stepExpression']
|
|
1118
|
+
)
|
|
1119
|
+
new_expr_chain = ExpressionsChain(
|
|
619
1120
|
type = 'transitive',
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1121
|
+
sub_link = result_expr_chain
|
|
1122
|
+
)
|
|
1123
|
+
return (
|
|
1124
|
+
result_target_asset,
|
|
1125
|
+
new_expr_chain,
|
|
1126
|
+
attack_step
|
|
1127
|
+
)
|
|
624
1128
|
|
|
625
1129
|
case 'subType':
|
|
626
1130
|
# Create a subType tuple entry that applies to the next
|
|
@@ -628,19 +1132,20 @@ class LanguageGraph():
|
|
|
628
1132
|
# asset to the subasset.
|
|
629
1133
|
subtype_name = step_expression['subType']
|
|
630
1134
|
result_target_asset, \
|
|
631
|
-
|
|
1135
|
+
result_expr_chain, \
|
|
632
1136
|
attack_step = \
|
|
633
|
-
self.process_step_expression(
|
|
1137
|
+
self.process_step_expression(
|
|
634
1138
|
target_asset,
|
|
635
|
-
|
|
636
|
-
step_expression['stepExpression']
|
|
637
|
-
|
|
638
|
-
subtype_asset = next((asset for asset in self.assets if asset.name == subtype_name), None)
|
|
1139
|
+
expr_chain,
|
|
1140
|
+
step_expression['stepExpression']
|
|
1141
|
+
)
|
|
639
1142
|
|
|
640
|
-
if
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
1143
|
+
if subtype_name in self.assets:
|
|
1144
|
+
subtype_asset = self.assets[subtype_name]
|
|
1145
|
+
else:
|
|
1146
|
+
msg = 'Failed to find subtype %s'
|
|
1147
|
+
logger.error(msg % subtype_name)
|
|
1148
|
+
raise LanguageGraphException(msg % subtype_name)
|
|
644
1149
|
|
|
645
1150
|
if not subtype_asset.is_subasset_of(result_target_asset):
|
|
646
1151
|
logger.error(
|
|
@@ -650,31 +1155,48 @@ class LanguageGraph():
|
|
|
650
1155
|
)
|
|
651
1156
|
return (None, None, None)
|
|
652
1157
|
|
|
653
|
-
|
|
1158
|
+
new_expr_chain = ExpressionsChain(
|
|
654
1159
|
type = 'subType',
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1160
|
+
sub_link = result_expr_chain,
|
|
1161
|
+
subtype = subtype_asset
|
|
1162
|
+
)
|
|
1163
|
+
return (
|
|
1164
|
+
subtype_asset,
|
|
1165
|
+
new_expr_chain,
|
|
1166
|
+
attack_step
|
|
1167
|
+
)
|
|
660
1168
|
|
|
661
1169
|
case 'collect':
|
|
662
1170
|
# Apply the right hand step expression to left hand step
|
|
663
1171
|
# expression target asset and parent associations chain.
|
|
664
|
-
(lh_target_asset,
|
|
665
|
-
self.process_step_expression(
|
|
1172
|
+
(lh_target_asset, lh_expr_chain, _) = \
|
|
1173
|
+
self.process_step_expression(
|
|
666
1174
|
target_asset,
|
|
667
|
-
|
|
668
|
-
step_expression['lhs']
|
|
1175
|
+
expr_chain,
|
|
1176
|
+
step_expression['lhs']
|
|
1177
|
+
)
|
|
669
1178
|
(rh_target_asset,
|
|
670
|
-
|
|
1179
|
+
rh_expr_chain,
|
|
671
1180
|
rh_attack_step_name) = \
|
|
672
|
-
self.process_step_expression(
|
|
1181
|
+
self.process_step_expression(
|
|
673
1182
|
lh_target_asset,
|
|
674
|
-
|
|
675
|
-
step_expression['rhs']
|
|
676
|
-
|
|
677
|
-
|
|
1183
|
+
None,
|
|
1184
|
+
step_expression['rhs']
|
|
1185
|
+
)
|
|
1186
|
+
if rh_expr_chain:
|
|
1187
|
+
new_expr_chain = ExpressionsChain(
|
|
1188
|
+
type = 'collect',
|
|
1189
|
+
left_link = lh_expr_chain,
|
|
1190
|
+
right_link = rh_expr_chain
|
|
1191
|
+
)
|
|
1192
|
+
else:
|
|
1193
|
+
new_expr_chain = lh_expr_chain
|
|
1194
|
+
|
|
1195
|
+
return (
|
|
1196
|
+
rh_target_asset,
|
|
1197
|
+
new_expr_chain,
|
|
1198
|
+
rh_attack_step_name
|
|
1199
|
+
)
|
|
678
1200
|
|
|
679
1201
|
case _:
|
|
680
1202
|
logger.error(
|
|
@@ -682,17 +1204,18 @@ class LanguageGraph():
|
|
|
682
1204
|
)
|
|
683
1205
|
return (None, None, None)
|
|
684
1206
|
|
|
685
|
-
|
|
1207
|
+
|
|
1208
|
+
def reverse_expr_chain(
|
|
686
1209
|
self,
|
|
687
|
-
|
|
688
|
-
reverse_chain: Optional[
|
|
689
|
-
) -> Optional[
|
|
1210
|
+
expr_chain: Optional[ExpressionsChain],
|
|
1211
|
+
reverse_chain: Optional[ExpressionsChain]
|
|
1212
|
+
) -> Optional[ExpressionsChain]:
|
|
690
1213
|
"""
|
|
691
1214
|
Recursively reverse the associations chain. From parent to child or
|
|
692
1215
|
vice versa.
|
|
693
1216
|
|
|
694
1217
|
Arguments:
|
|
695
|
-
|
|
1218
|
+
expr_chain - A chain of nested tuples that specify the
|
|
696
1219
|
associations and set operations chain from an
|
|
697
1220
|
attack step to its connected attack step.
|
|
698
1221
|
reverse_chain - A chain of nested tuples that represents the
|
|
@@ -701,70 +1224,103 @@ class LanguageGraph():
|
|
|
701
1224
|
Return:
|
|
702
1225
|
The resulting reversed associations chain.
|
|
703
1226
|
"""
|
|
704
|
-
if not
|
|
1227
|
+
if not expr_chain:
|
|
705
1228
|
return reverse_chain
|
|
706
1229
|
else:
|
|
707
|
-
match (
|
|
708
|
-
case 'union' | 'intersection' | 'difference':
|
|
1230
|
+
match (expr_chain.type):
|
|
1231
|
+
case 'union' | 'intersection' | 'difference' | 'collect':
|
|
709
1232
|
left_reverse_chain = \
|
|
710
|
-
self.
|
|
1233
|
+
self.reverse_expr_chain(expr_chain.left_link,
|
|
711
1234
|
reverse_chain)
|
|
712
1235
|
right_reverse_chain = \
|
|
713
|
-
self.
|
|
1236
|
+
self.reverse_expr_chain(expr_chain.right_link,
|
|
714
1237
|
reverse_chain)
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1238
|
+
if expr_chain.type == 'collect':
|
|
1239
|
+
new_expr_chain = ExpressionsChain(
|
|
1240
|
+
type = expr_chain.type,
|
|
1241
|
+
left_link = right_reverse_chain,
|
|
1242
|
+
right_link = left_reverse_chain
|
|
1243
|
+
)
|
|
1244
|
+
else:
|
|
1245
|
+
new_expr_chain = ExpressionsChain(
|
|
1246
|
+
type = expr_chain.type,
|
|
1247
|
+
left_link = left_reverse_chain,
|
|
1248
|
+
right_link = right_reverse_chain
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
return new_expr_chain
|
|
721
1252
|
|
|
722
1253
|
case 'transitive':
|
|
723
|
-
result_reverse_chain = self.
|
|
724
|
-
|
|
725
|
-
|
|
1254
|
+
result_reverse_chain = self.reverse_expr_chain(
|
|
1255
|
+
expr_chain.sub_link, reverse_chain)
|
|
1256
|
+
new_expr_chain = ExpressionsChain(
|
|
726
1257
|
type = 'transitive',
|
|
727
|
-
|
|
728
|
-
|
|
1258
|
+
sub_link = result_reverse_chain
|
|
1259
|
+
)
|
|
1260
|
+
return new_expr_chain
|
|
729
1261
|
|
|
730
1262
|
case 'field':
|
|
731
|
-
association =
|
|
1263
|
+
association = expr_chain.association
|
|
732
1264
|
|
|
733
1265
|
if not association:
|
|
734
1266
|
raise LanguageGraphException(
|
|
735
|
-
"Missing association for
|
|
1267
|
+
"Missing association for expressions chain"
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
if not expr_chain.fieldname:
|
|
1271
|
+
raise LanguageGraphException(
|
|
1272
|
+
"Missing field name for expressions chain"
|
|
736
1273
|
)
|
|
737
1274
|
|
|
738
1275
|
opposite_fieldname = association.get_opposite_fieldname(
|
|
739
|
-
|
|
740
|
-
|
|
1276
|
+
expr_chain.fieldname)
|
|
1277
|
+
new_expr_chain = ExpressionsChain(
|
|
741
1278
|
type = 'field',
|
|
742
|
-
|
|
1279
|
+
association = association,
|
|
1280
|
+
fieldname = opposite_fieldname
|
|
743
1281
|
)
|
|
744
|
-
|
|
745
|
-
new_dep_chain.association = association
|
|
746
|
-
return self.reverse_dep_chain(
|
|
747
|
-
dep_chain.next_link,
|
|
748
|
-
new_dep_chain
|
|
749
|
-
)
|
|
1282
|
+
return new_expr_chain
|
|
750
1283
|
|
|
751
1284
|
case 'subType':
|
|
752
|
-
result_reverse_chain = self.
|
|
753
|
-
|
|
1285
|
+
result_reverse_chain = self.reverse_expr_chain(
|
|
1286
|
+
expr_chain.sub_link,
|
|
754
1287
|
reverse_chain
|
|
755
1288
|
)
|
|
756
|
-
|
|
1289
|
+
new_expr_chain = ExpressionsChain(
|
|
757
1290
|
type = 'subType',
|
|
758
|
-
|
|
1291
|
+
sub_link = result_reverse_chain,
|
|
1292
|
+
subtype = expr_chain.subtype
|
|
759
1293
|
)
|
|
760
|
-
|
|
761
|
-
return new_dep_chain
|
|
762
|
-
# return reverse_chain
|
|
1294
|
+
return new_expr_chain
|
|
763
1295
|
|
|
764
1296
|
case _:
|
|
765
1297
|
msg = 'Unknown assoc chain element "%s"'
|
|
766
|
-
logger.error(msg,
|
|
767
|
-
raise LanguageGraphAssociationError(msg %
|
|
1298
|
+
logger.error(msg, expr_chain.type)
|
|
1299
|
+
raise LanguageGraphAssociationError(msg % expr_chain.type)
|
|
1300
|
+
|
|
1301
|
+
def _resolve_variable(self, asset, var_name) -> tuple:
|
|
1302
|
+
"""
|
|
1303
|
+
Resolve a variable for a specific asset by variable name.
|
|
1304
|
+
|
|
1305
|
+
Arguments:
|
|
1306
|
+
asset - a language graph asset to which the variable belongs
|
|
1307
|
+
var_name - a string representing the variable name
|
|
1308
|
+
|
|
1309
|
+
Return:
|
|
1310
|
+
A tuple containing the target asset and expressions chain required to
|
|
1311
|
+
reach it.
|
|
1312
|
+
"""
|
|
1313
|
+
if var_name not in asset.variables:
|
|
1314
|
+
var_expr = self._get_var_expr_for_asset(asset.name, var_name)
|
|
1315
|
+
target_asset, expr_chain, _ = self.process_step_expression(
|
|
1316
|
+
asset,
|
|
1317
|
+
None,
|
|
1318
|
+
var_expr
|
|
1319
|
+
)
|
|
1320
|
+
asset.own_variables[var_name] = (target_asset, expr_chain)
|
|
1321
|
+
return (target_asset, expr_chain)
|
|
1322
|
+
return asset.variables[var_name]
|
|
1323
|
+
|
|
768
1324
|
|
|
769
1325
|
def _generate_graph(self) -> None:
|
|
770
1326
|
"""
|
|
@@ -772,41 +1328,40 @@ class LanguageGraph():
|
|
|
772
1328
|
given in the constructor.
|
|
773
1329
|
"""
|
|
774
1330
|
# Generate all of the asset nodes of the language graph.
|
|
775
|
-
for
|
|
1331
|
+
for asset_dict in self._lang_spec['assets']:
|
|
776
1332
|
logger.debug(
|
|
777
1333
|
'Create asset language graph nodes for asset %s',
|
|
778
|
-
|
|
1334
|
+
asset_dict['name']
|
|
779
1335
|
)
|
|
780
1336
|
asset_node = LanguageGraphAsset(
|
|
781
|
-
name =
|
|
782
|
-
|
|
783
|
-
attack_steps =
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1337
|
+
name = asset_dict['name'],
|
|
1338
|
+
own_associations = {},
|
|
1339
|
+
attack_steps = {},
|
|
1340
|
+
info = asset_dict['meta'],
|
|
1341
|
+
own_super_asset = None,
|
|
1342
|
+
own_sub_assets = set(),
|
|
1343
|
+
own_variables = {},
|
|
1344
|
+
is_abstract = asset_dict['isAbstract']
|
|
788
1345
|
)
|
|
789
|
-
self.assets
|
|
1346
|
+
self.assets[asset_dict['name']] = asset_node
|
|
790
1347
|
|
|
791
1348
|
# Link assets based on inheritance
|
|
792
|
-
for
|
|
793
|
-
asset =
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
super_asset = next((asset for asset in self.assets \
|
|
797
|
-
if asset.name == asset_info['superAsset']), None)
|
|
1349
|
+
for asset_dict in self._lang_spec['assets']:
|
|
1350
|
+
asset = self.assets[asset_dict['name']]
|
|
1351
|
+
if asset_dict['superAsset']:
|
|
1352
|
+
super_asset = self.assets[asset_dict['superAsset']]
|
|
798
1353
|
if not super_asset:
|
|
799
1354
|
msg = 'Failed to find super asset "%s" for asset "%s"!'
|
|
800
1355
|
logger.error(
|
|
801
|
-
msg,
|
|
1356
|
+
msg, asset_dict["superAsset"], asset_dict["name"])
|
|
802
1357
|
raise LanguageGraphSuperAssetNotFoundError(
|
|
803
|
-
msg % (
|
|
1358
|
+
msg % (asset_dict["superAsset"], asset_dict["name"]))
|
|
804
1359
|
|
|
805
|
-
super_asset.
|
|
806
|
-
asset.
|
|
1360
|
+
super_asset.own_sub_assets.add(asset)
|
|
1361
|
+
asset.own_super_asset = super_asset
|
|
807
1362
|
|
|
808
1363
|
# Generate all of the association nodes of the language graph.
|
|
809
|
-
for asset in self.assets:
|
|
1364
|
+
for asset in self.assets.values():
|
|
810
1365
|
logger.debug(
|
|
811
1366
|
'Create association language graph nodes for asset %s',
|
|
812
1367
|
asset.name
|
|
@@ -814,8 +1369,7 @@ class LanguageGraph():
|
|
|
814
1369
|
|
|
815
1370
|
associations = self._get_associations_for_asset_type(asset.name)
|
|
816
1371
|
for association in associations:
|
|
817
|
-
left_asset =
|
|
818
|
-
if asset.name == association['leftAsset']), None)
|
|
1372
|
+
left_asset = self.assets[association['leftAsset']]
|
|
819
1373
|
if not left_asset:
|
|
820
1374
|
msg = 'Left asset "%s" for association "%s" not found!'
|
|
821
1375
|
logger.error(
|
|
@@ -823,8 +1377,7 @@ class LanguageGraph():
|
|
|
823
1377
|
raise LanguageGraphAssociationError(
|
|
824
1378
|
msg % (association["leftAsset"], association["name"]))
|
|
825
1379
|
|
|
826
|
-
right_asset =
|
|
827
|
-
if asset.name == association['rightAsset']), None)
|
|
1380
|
+
right_asset = self.assets[association['rightAsset']]
|
|
828
1381
|
if not right_asset:
|
|
829
1382
|
msg = 'Right asset "%s" for association "%s" not found!'
|
|
830
1383
|
logger.error(
|
|
@@ -833,17 +1386,6 @@ class LanguageGraph():
|
|
|
833
1386
|
msg % (association["rightAsset"], association["name"])
|
|
834
1387
|
)
|
|
835
1388
|
|
|
836
|
-
# Technically we should be more exhaustive and check the
|
|
837
|
-
# flipped version too and all of the fieldnames as well.
|
|
838
|
-
assoc_node = next((assoc for assoc in self.associations \
|
|
839
|
-
if assoc.name == association['name'] and
|
|
840
|
-
assoc.left_field.asset == left_asset and
|
|
841
|
-
assoc.right_field.asset == right_asset),
|
|
842
|
-
None)
|
|
843
|
-
if assoc_node:
|
|
844
|
-
# The association was already created, skip it
|
|
845
|
-
continue
|
|
846
|
-
|
|
847
1389
|
assoc_node = LanguageGraphAssociation(
|
|
848
1390
|
name = association['name'],
|
|
849
1391
|
left_field = LanguageGraphAssociationField(
|
|
@@ -856,124 +1398,224 @@ class LanguageGraph():
|
|
|
856
1398
|
association['rightField'],
|
|
857
1399
|
association['rightMultiplicity']['min'],
|
|
858
1400
|
association['rightMultiplicity']['max']),
|
|
859
|
-
|
|
1401
|
+
info = association['meta']
|
|
860
1402
|
)
|
|
861
1403
|
|
|
862
|
-
# Add the association to the left and right asset
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1404
|
+
# Add the association to the left and right asset
|
|
1405
|
+
self._link_association_to_assets(assoc_node,
|
|
1406
|
+
left_asset, right_asset)
|
|
1407
|
+
|
|
1408
|
+
# Set the variables
|
|
1409
|
+
for asset in self.assets.values():
|
|
1410
|
+
for variable in self._get_variables_for_asset_type(asset.name):
|
|
1411
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
1412
|
+
# Avoid running json.dumps when not in debug
|
|
1413
|
+
logger.debug(
|
|
1414
|
+
'Processing Variable Expression:\n%s',
|
|
1415
|
+
json.dumps(variable, indent = 2)
|
|
1416
|
+
)
|
|
1417
|
+
self._resolve_variable(asset, variable['name'])
|
|
870
1418
|
|
|
871
|
-
self.associations.append(assoc_node)
|
|
872
1419
|
|
|
873
1420
|
# Generate all of the attack step nodes of the language graph.
|
|
874
|
-
for asset in self.assets:
|
|
1421
|
+
for asset in self.assets.values():
|
|
875
1422
|
logger.debug(
|
|
876
1423
|
'Create attack steps language graph nodes for asset %s',
|
|
877
1424
|
asset.name
|
|
878
1425
|
)
|
|
879
1426
|
attack_steps = self._get_attacks_for_asset_type(asset.name)
|
|
880
|
-
for
|
|
1427
|
+
for attack_step_attribs in attack_steps.values():
|
|
881
1428
|
logger.debug(
|
|
882
1429
|
'Create attack step language graph nodes for %s',
|
|
883
|
-
|
|
1430
|
+
attack_step_attribs['name']
|
|
884
1431
|
)
|
|
885
1432
|
|
|
886
1433
|
attack_step_node = LanguageGraphAttackStep(
|
|
887
|
-
name =
|
|
1434
|
+
name = attack_step_attribs['name'],
|
|
888
1435
|
type = attack_step_attribs['type'],
|
|
889
1436
|
asset = asset,
|
|
890
1437
|
ttc = attack_step_attribs['ttc'],
|
|
1438
|
+
overrides = attack_step_attribs['reaches']['overrides'] \
|
|
1439
|
+
if attack_step_attribs['reaches'] else False,
|
|
891
1440
|
children = {},
|
|
892
1441
|
parents = {},
|
|
893
|
-
|
|
1442
|
+
info = attack_step_attribs['meta'],
|
|
1443
|
+
tags = set(attack_step_attribs['tags'])
|
|
894
1444
|
)
|
|
895
|
-
attack_step_node.
|
|
896
|
-
asset.attack_steps
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
# attack step to determine children.
|
|
912
|
-
(target_asset, dep_chain, attack_step_name) = \
|
|
913
|
-
self.process_step_expression(self._lang_spec,
|
|
914
|
-
attack_step.asset,
|
|
915
|
-
None,
|
|
916
|
-
step_expression)
|
|
917
|
-
if not target_asset:
|
|
918
|
-
msg = 'Failed to find target asset to link with for ' \
|
|
919
|
-
'step expression:\n%s'
|
|
920
|
-
raise LanguageGraphStepExpressionError(
|
|
921
|
-
msg % json.dumps(step_expression, indent = 2)
|
|
1445
|
+
attack_step_node._attributes = attack_step_attribs
|
|
1446
|
+
asset.attack_steps[attack_step_attribs['name']] = \
|
|
1447
|
+
attack_step_node
|
|
1448
|
+
|
|
1449
|
+
for detector in attack_step_attribs.get("detectors",
|
|
1450
|
+
{}).values():
|
|
1451
|
+
attack_step_node.detectors[detector["name"]] = Detector(
|
|
1452
|
+
context=Context(
|
|
1453
|
+
{
|
|
1454
|
+
label: self.assets[asset]
|
|
1455
|
+
for label, asset in detector["context"].items()
|
|
1456
|
+
}
|
|
1457
|
+
),
|
|
1458
|
+
name=detector.get("name"),
|
|
1459
|
+
type=detector.get("type"),
|
|
1460
|
+
tprate=detector.get("tprate"),
|
|
922
1461
|
)
|
|
923
1462
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1463
|
+
# Create the inherited attack steps
|
|
1464
|
+
assets = list(self.assets.values())
|
|
1465
|
+
while len(assets) > 0:
|
|
1466
|
+
asset = assets.pop(0)
|
|
1467
|
+
if asset.own_super_asset in assets:
|
|
1468
|
+
# The asset still has super assets that should be resolved
|
|
1469
|
+
# first, moved it to the back.
|
|
1470
|
+
assets.append(asset)
|
|
1471
|
+
else:
|
|
1472
|
+
if asset.own_super_asset:
|
|
1473
|
+
for attack_step in \
|
|
1474
|
+
asset.own_super_asset.attack_steps.values():
|
|
1475
|
+
if attack_step.name not in asset.attack_steps:
|
|
1476
|
+
attack_step_node = LanguageGraphAttackStep(
|
|
1477
|
+
name = attack_step.name,
|
|
1478
|
+
type = attack_step.type,
|
|
1479
|
+
asset = asset,
|
|
1480
|
+
ttc = attack_step.ttc,
|
|
1481
|
+
overrides = False,
|
|
1482
|
+
children = {},
|
|
1483
|
+
parents = {},
|
|
1484
|
+
info = attack_step.info,
|
|
1485
|
+
tags = set(attack_step.tags)
|
|
1486
|
+
)
|
|
1487
|
+
attack_step_node.inherits = attack_step
|
|
1488
|
+
asset.attack_steps[attack_step.name] = attack_step_node
|
|
1489
|
+
elif asset.attack_steps[attack_step.name].overrides:
|
|
1490
|
+
# The inherited attack step was already overridden.
|
|
1491
|
+
continue
|
|
1492
|
+
else:
|
|
1493
|
+
asset.attack_steps[attack_step.name].inherits = \
|
|
1494
|
+
attack_step
|
|
1495
|
+
asset.attack_steps[attack_step.name].tags |= \
|
|
1496
|
+
attack_step.tags
|
|
1497
|
+
asset.attack_steps[attack_step.name].info |= \
|
|
1498
|
+
attack_step.info
|
|
1499
|
+
|
|
1500
|
+
# Then, link all of the attack step nodes according to their
|
|
1501
|
+
# associations.
|
|
1502
|
+
for asset in self.assets.values():
|
|
1503
|
+
for attack_step in asset.attack_steps.values():
|
|
1504
|
+
logger.debug(
|
|
1505
|
+
'Determining children for attack step %s',
|
|
1506
|
+
attack_step.name
|
|
1507
|
+
)
|
|
1508
|
+
|
|
1509
|
+
if attack_step._attributes is None:
|
|
1510
|
+
# This is simply an empty inherited attack step
|
|
1511
|
+
continue
|
|
1512
|
+
|
|
1513
|
+
step_expressions = \
|
|
1514
|
+
attack_step._attributes['reaches']['stepExpressions'] if \
|
|
1515
|
+
attack_step._attributes['reaches'] else []
|
|
1516
|
+
|
|
1517
|
+
for step_expression in step_expressions:
|
|
1518
|
+
# Resolve each of the attack step expressions listed for
|
|
1519
|
+
# this attack step to determine children.
|
|
1520
|
+
(target_asset, expr_chain, target_attack_step_name) = \
|
|
1521
|
+
self.process_step_expression(
|
|
1522
|
+
attack_step.asset,
|
|
1523
|
+
None,
|
|
1524
|
+
step_expression
|
|
1525
|
+
)
|
|
1526
|
+
if not target_asset:
|
|
1527
|
+
msg = 'Failed to find target asset to link with for ' \
|
|
1528
|
+
'step expression:\n%s'
|
|
1529
|
+
raise LanguageGraphStepExpressionError(
|
|
1530
|
+
msg % json.dumps(step_expression, indent = 2)
|
|
936
1531
|
)
|
|
937
|
-
)
|
|
938
1532
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1533
|
+
target_asset_attack_steps = target_asset.attack_steps
|
|
1534
|
+
if target_attack_step_name not in \
|
|
1535
|
+
target_asset_attack_steps:
|
|
1536
|
+
msg = 'Failed to find target attack step %s on %s to ' \
|
|
1537
|
+
'link with for step expression:\n%s'
|
|
1538
|
+
raise LanguageGraphStepExpressionError(
|
|
1539
|
+
msg % (
|
|
1540
|
+
target_attack_step_name,
|
|
1541
|
+
target_asset.name,
|
|
1542
|
+
json.dumps(step_expression, indent = 2)
|
|
1543
|
+
)
|
|
1544
|
+
)
|
|
1545
|
+
|
|
1546
|
+
target_attack_step = target_asset_attack_steps[
|
|
1547
|
+
target_attack_step_name]
|
|
1548
|
+
|
|
1549
|
+
# Link to the children target attack steps
|
|
1550
|
+
if target_attack_step.full_name in attack_step.children:
|
|
1551
|
+
attack_step.children[target_attack_step.full_name].\
|
|
1552
|
+
append((target_attack_step, expr_chain))
|
|
1553
|
+
else:
|
|
1554
|
+
attack_step.children[target_attack_step.full_name] = \
|
|
1555
|
+
[(target_attack_step, expr_chain)]
|
|
1556
|
+
# Reverse the children associations chains to get the
|
|
1557
|
+
# parents associations chain.
|
|
1558
|
+
if attack_step.full_name in target_attack_step.parents:
|
|
1559
|
+
target_attack_step.parents[attack_step.full_name].\
|
|
1560
|
+
append((attack_step,
|
|
1561
|
+
self.reverse_expr_chain(expr_chain,
|
|
1562
|
+
None)))
|
|
1563
|
+
else:
|
|
1564
|
+
target_attack_step.parents[attack_step.full_name] = \
|
|
1565
|
+
[(attack_step,
|
|
1566
|
+
self.reverse_expr_chain(expr_chain,
|
|
1567
|
+
None))]
|
|
1568
|
+
|
|
1569
|
+
# Evaluate the requirements of exist and notExist attack steps
|
|
1570
|
+
if attack_step.type == 'exist' or \
|
|
1571
|
+
attack_step.type == 'notExist':
|
|
1572
|
+
step_expressions = \
|
|
1573
|
+
attack_step._attributes['requires']['stepExpressions'] \
|
|
1574
|
+
if attack_step._attributes['requires'] else []
|
|
1575
|
+
if not step_expressions:
|
|
1576
|
+
msg = 'Failed to find requirements for attack step' \
|
|
1577
|
+
' "%s" of type "%s":\n%s'
|
|
1578
|
+
raise LanguageGraphStepExpressionError(
|
|
1579
|
+
msg % (
|
|
1580
|
+
attack_step.name,
|
|
1581
|
+
attack_step.type,
|
|
1582
|
+
json.dumps(attack_step._attributes, indent = 2)
|
|
1583
|
+
)
|
|
1584
|
+
)
|
|
1585
|
+
|
|
1586
|
+
attack_step.own_requires = []
|
|
1587
|
+
for step_expression in step_expressions:
|
|
1588
|
+
_, \
|
|
1589
|
+
result_expr_chain, \
|
|
1590
|
+
_ = \
|
|
1591
|
+
self.process_step_expression(
|
|
1592
|
+
attack_step.asset,
|
|
1593
|
+
None,
|
|
1594
|
+
step_expression
|
|
1595
|
+
)
|
|
1596
|
+
attack_step.own_requires.append(result_expr_chain)
|
|
959
1597
|
|
|
960
1598
|
def _get_attacks_for_asset_type(self, asset_type: str) -> dict:
|
|
961
1599
|
"""
|
|
962
1600
|
Get all Attack Steps for a specific Class
|
|
963
1601
|
|
|
964
1602
|
Arguments:
|
|
965
|
-
asset_type - a string representing the class for which we want to
|
|
966
|
-
the possible attack steps
|
|
1603
|
+
asset_type - a string representing the class for which we want to
|
|
1604
|
+
list the possible attack steps
|
|
967
1605
|
|
|
968
1606
|
Return:
|
|
969
|
-
A dictionary representing the set of possible attacks for the
|
|
970
|
-
class. Each key in the dictionary is an attack name and is
|
|
971
|
-
with a dictionary containing other characteristics of the
|
|
972
|
-
type of attack, TTC distribution, child attack steps
|
|
1607
|
+
A dictionary representing the set of possible attacks for the
|
|
1608
|
+
specified class. Each key in the dictionary is an attack name and is
|
|
1609
|
+
associated with a dictionary containing other characteristics of the
|
|
1610
|
+
attack such as type of attack, TTC distribution, child attack steps
|
|
1611
|
+
and other information
|
|
973
1612
|
"""
|
|
974
1613
|
attack_steps: dict = {}
|
|
975
1614
|
try:
|
|
976
|
-
asset = next(
|
|
1615
|
+
asset = next(
|
|
1616
|
+
asset for asset in self._lang_spec['assets'] \
|
|
1617
|
+
if asset['name'] == asset_type
|
|
1618
|
+
)
|
|
977
1619
|
except StopIteration:
|
|
978
1620
|
logger.error(
|
|
979
1621
|
'Failed to find asset type %s when looking'
|
|
@@ -983,33 +1625,10 @@ class LanguageGraph():
|
|
|
983
1625
|
|
|
984
1626
|
logger.debug(
|
|
985
1627
|
'Get attack steps for %s asset from '
|
|
986
|
-
'language specification.', asset[
|
|
1628
|
+
'language specification.', asset['name']
|
|
987
1629
|
)
|
|
988
|
-
if asset['superAsset']:
|
|
989
|
-
logger.debug('Asset extends another one, fetch the superclass '\
|
|
990
|
-
'attack steps for it.')
|
|
991
|
-
attack_steps = self._get_attacks_for_asset_type(asset['superAsset'])
|
|
992
|
-
|
|
993
|
-
for step in asset['attackSteps']:
|
|
994
|
-
if step['name'] not in attack_steps:
|
|
995
|
-
attack_steps[step['name']] = copy.deepcopy(step)
|
|
996
|
-
elif not step['reaches']:
|
|
997
|
-
# This attack step does not lead to any attack steps
|
|
998
|
-
continue
|
|
999
|
-
elif step['reaches']['overrides'] == True:
|
|
1000
|
-
attack_steps[step['name']] = copy.deepcopy(step)
|
|
1001
|
-
else:
|
|
1002
|
-
if attack_steps[step['name']]['reaches'] is not None and \
|
|
1003
|
-
'stepExpressions' in \
|
|
1004
|
-
attack_steps[step['name']]['reaches']:
|
|
1005
|
-
attack_steps[step['name']]['reaches']['stepExpressions'].\
|
|
1006
|
-
extend(step['reaches']['stepExpressions'])
|
|
1007
|
-
else:
|
|
1008
|
-
attack_steps[step['name']]['reaches'] = {
|
|
1009
|
-
'overrides': False,
|
|
1010
|
-
'stepExpressions': step['reaches']['stepExpressions']
|
|
1011
|
-
}
|
|
1012
1630
|
|
|
1631
|
+
attack_steps = {step['name']: step for step in asset['attackSteps']}
|
|
1013
1632
|
|
|
1014
1633
|
return attack_steps
|
|
1015
1634
|
|
|
@@ -1018,14 +1637,15 @@ class LanguageGraph():
|
|
|
1018
1637
|
Get all Associations for a specific Class
|
|
1019
1638
|
|
|
1020
1639
|
Arguments:
|
|
1021
|
-
asset_type - a string representing the class for which we want to
|
|
1022
|
-
the associations
|
|
1640
|
+
asset_type - a string representing the class for which we want to
|
|
1641
|
+
list the associations
|
|
1023
1642
|
|
|
1024
1643
|
Return:
|
|
1025
1644
|
A dictionary representing the set of associations for the specified
|
|
1026
1645
|
class. Each key in the dictionary is an attack name and is associated
|
|
1027
|
-
with a dictionary containing other characteristics of the attack such
|
|
1028
|
-
type of attack, TTC distribution, child attack steps and other
|
|
1646
|
+
with a dictionary containing other characteristics of the attack such
|
|
1647
|
+
as type of attack, TTC distribution, child attack steps and other
|
|
1648
|
+
information
|
|
1029
1649
|
"""
|
|
1030
1650
|
logger.debug(
|
|
1031
1651
|
'Get associations for %s asset from '
|
|
@@ -1033,8 +1653,8 @@ class LanguageGraph():
|
|
|
1033
1653
|
)
|
|
1034
1654
|
associations: list = []
|
|
1035
1655
|
|
|
1036
|
-
asset = next((asset for asset in self._lang_spec['assets']
|
|
1037
|
-
asset_type), None)
|
|
1656
|
+
asset = next((asset for asset in self._lang_spec['assets'] \
|
|
1657
|
+
if asset['name'] == asset_type), None)
|
|
1038
1658
|
if not asset:
|
|
1039
1659
|
logger.error(
|
|
1040
1660
|
'Failed to find asset type %s when '
|
|
@@ -1042,10 +1662,6 @@ class LanguageGraph():
|
|
|
1042
1662
|
)
|
|
1043
1663
|
return associations
|
|
1044
1664
|
|
|
1045
|
-
if asset['superAsset']:
|
|
1046
|
-
logger.debug('Asset extends another one, fetch the superclass '\
|
|
1047
|
-
'associations for it.')
|
|
1048
|
-
associations.extend(self._get_associations_for_asset_type(asset['superAsset']))
|
|
1049
1665
|
assoc_iter = (assoc for assoc in self._lang_spec['associations'] \
|
|
1050
1666
|
if assoc['leftAsset'] == asset_type or \
|
|
1051
1667
|
assoc['rightAsset'] == asset_type)
|
|
@@ -1056,129 +1672,64 @@ class LanguageGraph():
|
|
|
1056
1672
|
|
|
1057
1673
|
return associations
|
|
1058
1674
|
|
|
1059
|
-
def
|
|
1060
|
-
self, asset_type: str
|
|
1675
|
+
def _get_variables_for_asset_type(
|
|
1676
|
+
self, asset_type: str) -> dict:
|
|
1061
1677
|
"""
|
|
1062
1678
|
Get a variables for a specific asset type by name.
|
|
1063
|
-
|
|
1679
|
+
Note: Variables are the ones specified in MAL through `let` statements
|
|
1064
1680
|
|
|
1065
1681
|
Arguments:
|
|
1066
1682
|
asset_type - a string representing the type of asset which
|
|
1067
|
-
contains the
|
|
1068
|
-
variable_name - the name of the variable to search for
|
|
1683
|
+
contains the variables
|
|
1069
1684
|
|
|
1070
1685
|
Return:
|
|
1071
|
-
A dictionary representing the step expressions for the
|
|
1686
|
+
A dictionary representing the step expressions for the variables
|
|
1687
|
+
belonging to the asset.
|
|
1072
1688
|
"""
|
|
1073
1689
|
|
|
1074
|
-
|
|
1075
|
-
asset_type), None)
|
|
1076
|
-
if not
|
|
1077
|
-
msg = 'Failed to find asset type %s
|
|
1690
|
+
asset_dict = next((asset for asset in self._lang_spec['assets'] \
|
|
1691
|
+
if asset['name'] == asset_type), None)
|
|
1692
|
+
if not asset_dict:
|
|
1693
|
+
msg = 'Failed to find asset type %s in language specification '\
|
|
1694
|
+
'when looking for variables.'
|
|
1078
1695
|
logger.error(msg, asset_type)
|
|
1079
1696
|
raise LanguageGraphException(msg % asset_type)
|
|
1080
1697
|
|
|
1081
|
-
|
|
1082
|
-
asset['variables'] if variable['name'] == variable_name), None)
|
|
1083
|
-
if not variable_dict:
|
|
1084
|
-
if asset['superAsset']:
|
|
1085
|
-
variable_dict = self._get_variable_for_asset_type_by_name(asset['superAsset'],
|
|
1086
|
-
variable_name)
|
|
1087
|
-
if variable_dict:
|
|
1088
|
-
return variable_dict
|
|
1089
|
-
else:
|
|
1090
|
-
msg = 'Failed to find variable %s in %s lang specification.'
|
|
1091
|
-
logger.error(msg, variable_name, asset_type)
|
|
1092
|
-
raise LanguageGraphException(
|
|
1093
|
-
msg % (variable_name, asset_type))
|
|
1094
|
-
|
|
1095
|
-
return variable_dict['stepExpression']
|
|
1698
|
+
return asset_dict['variables']
|
|
1096
1699
|
|
|
1097
|
-
def
|
|
1098
|
-
|
|
1099
|
-
Regenerate language graph starting from the MAL language specification
|
|
1100
|
-
given in the constructor.
|
|
1101
|
-
"""
|
|
1102
|
-
|
|
1103
|
-
self.assets = []
|
|
1104
|
-
self.associations = []
|
|
1105
|
-
self.attack_steps = []
|
|
1106
|
-
self._generate_graph()
|
|
1107
|
-
|
|
1108
|
-
def get_asset_by_name(
|
|
1109
|
-
self,
|
|
1110
|
-
asset_name
|
|
1111
|
-
) -> Optional[LanguageGraphAsset]:
|
|
1700
|
+
def _get_var_expr_for_asset(
|
|
1701
|
+
self, asset_type: str, var_name) -> dict:
|
|
1112
1702
|
"""
|
|
1113
|
-
Get
|
|
1703
|
+
Get a variable for a specific asset type by variable name.
|
|
1114
1704
|
|
|
1115
1705
|
Arguments:
|
|
1116
|
-
|
|
1706
|
+
asset_type - a string representing the type of asset which
|
|
1707
|
+
contains the variable
|
|
1708
|
+
var_name - a string representing the variable name
|
|
1117
1709
|
|
|
1118
1710
|
Return:
|
|
1119
|
-
|
|
1120
|
-
None if there is no match.
|
|
1711
|
+
A dictionary representing the step expression for the variable.
|
|
1121
1712
|
"""
|
|
1122
|
-
for asset in self.assets:
|
|
1123
|
-
if asset.name == asset_name:
|
|
1124
|
-
return asset
|
|
1125
1713
|
|
|
1126
|
-
|
|
1714
|
+
vars_dict = self._get_variables_for_asset_type(asset_type)
|
|
1127
1715
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
first_field: str,
|
|
1131
|
-
second_field: str,
|
|
1132
|
-
first_asset_name: str,
|
|
1133
|
-
second_asset_name: str
|
|
1134
|
-
) -> Optional[LanguageGraphAssociation]:
|
|
1135
|
-
"""
|
|
1136
|
-
Get an association based on its field names and asset types
|
|
1716
|
+
var_expr = next((var_entry['stepExpression'] for var_entry \
|
|
1717
|
+
in vars_dict if var_entry['name'] == var_name), None)
|
|
1137
1718
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1719
|
+
if not var_expr:
|
|
1720
|
+
msg = 'Failed to find variable name "%s" in language '\
|
|
1721
|
+
'specification when looking for variables for "%s" asset.'
|
|
1722
|
+
logger.error(msg, var_name, asset_type)
|
|
1723
|
+
raise LanguageGraphException(msg % (var_name, asset_type))
|
|
1724
|
+
return var_expr
|
|
1143
1725
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1726
|
+
def regenerate_graph(self) -> None:
|
|
1727
|
+
"""
|
|
1728
|
+
Regenerate language graph starting from the MAL language specification
|
|
1729
|
+
given in the constructor.
|
|
1147
1730
|
"""
|
|
1148
|
-
first_asset = self.get_asset_by_name(first_asset_name)
|
|
1149
|
-
if first_asset is None:
|
|
1150
|
-
raise LookupError(
|
|
1151
|
-
f'Failed to find asset with name \"{first_asset_name}\" in '
|
|
1152
|
-
'the language graph.'
|
|
1153
|
-
)
|
|
1154
|
-
|
|
1155
|
-
second_asset = self.get_asset_by_name(second_asset_name)
|
|
1156
|
-
if second_asset is None:
|
|
1157
|
-
raise LookupError(
|
|
1158
|
-
f'Failed to find asset with name \"{second_asset_name}\" in '
|
|
1159
|
-
'the language graph.'
|
|
1160
|
-
)
|
|
1161
|
-
|
|
1162
|
-
for assoc in self.associations:
|
|
1163
|
-
logger.debug(
|
|
1164
|
-
'Compare ("%s", "%s", "%s", "%s") to ("%s", "%s", "%s", "%s").',
|
|
1165
|
-
first_asset_name, first_field,
|
|
1166
|
-
second_asset_name, second_field,
|
|
1167
|
-
assoc.left_field.asset.name, assoc.left_field.fieldname,
|
|
1168
|
-
assoc.right_field.asset.name, assoc.right_field.fieldname
|
|
1169
|
-
)
|
|
1170
1731
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
assoc.right_field.fieldname == second_field and \
|
|
1174
|
-
first_asset.is_subasset_of(assoc.left_field.asset) and \
|
|
1175
|
-
second_asset.is_subasset_of(assoc.right_field.asset):
|
|
1176
|
-
return assoc
|
|
1732
|
+
self.assets = {}
|
|
1733
|
+
self._generate_graph()
|
|
1177
1734
|
|
|
1178
|
-
if assoc.left_field.fieldname == second_field and \
|
|
1179
|
-
assoc.right_field.fieldname == first_field and \
|
|
1180
|
-
second_asset.is_subasset_of(assoc.left_field.asset) and \
|
|
1181
|
-
first_asset.is_subasset_of(assoc.right_field.asset):
|
|
1182
|
-
return assoc
|
|
1183
1735
|
|
|
1184
|
-
return None
|