mal-toolbox 0.0.28__py3-none-any.whl → 0.1.12__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.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/METADATA +60 -28
- mal_toolbox-0.1.12.dist-info/RECORD +32 -0
- {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/WHEEL +1 -1
- maltoolbox/__init__.py +31 -31
- maltoolbox/__main__.py +80 -4
- maltoolbox/attackgraph/__init__.py +8 -0
- maltoolbox/attackgraph/analyzers/__init__.py +0 -0
- maltoolbox/attackgraph/analyzers/apriori.py +173 -27
- maltoolbox/attackgraph/attacker.py +84 -25
- maltoolbox/attackgraph/attackgraph.py +503 -215
- maltoolbox/attackgraph/node.py +92 -31
- maltoolbox/attackgraph/query.py +125 -19
- maltoolbox/default.conf +8 -7
- maltoolbox/exceptions.py +45 -0
- maltoolbox/file_utils.py +66 -0
- maltoolbox/ingestors/__init__.py +0 -0
- maltoolbox/ingestors/neo4j.py +95 -84
- maltoolbox/language/__init__.py +4 -0
- maltoolbox/language/classes_factory.py +145 -64
- maltoolbox/language/{lexer_parser/__main__.py → compiler/__init__.py} +5 -12
- maltoolbox/language/{lexer_parser → compiler}/mal_lexer.py +1 -1
- maltoolbox/language/{lexer_parser → compiler}/mal_parser.py +1 -1
- maltoolbox/language/{lexer_parser → compiler}/mal_visitor.py +4 -5
- maltoolbox/language/languagegraph.py +569 -168
- maltoolbox/model.py +858 -0
- maltoolbox/translators/__init__.py +0 -0
- maltoolbox/translators/securicad.py +76 -52
- maltoolbox/translators/updater.py +132 -0
- maltoolbox/wrappers.py +62 -0
- mal_toolbox-0.0.28.dist-info/RECORD +0 -26
- maltoolbox/cl_parser.py +0 -89
- maltoolbox/language/specification.py +0 -265
- maltoolbox/main.py +0 -84
- maltoolbox/model/model.py +0 -282
- {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/AUTHORS +0 -0
- {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/LICENSE +0 -0
- {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/top_level.txt +0 -0
|
@@ -2,30 +2,45 @@
|
|
|
2
2
|
MAL-Toolbox Language Graph Module
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import copy
|
|
5
8
|
import logging
|
|
6
9
|
import json
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
import zipfile
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
from maltoolbox.file_utils import (
|
|
16
|
+
load_dict_from_yaml_file, load_dict_from_json_file,
|
|
17
|
+
save_dict_to_file
|
|
18
|
+
)
|
|
19
|
+
from .compiler import MalCompiler
|
|
20
|
+
from ..exceptions import (
|
|
21
|
+
LanguageGraphAssociationError,
|
|
22
|
+
LanguageGraphStepExpressionError,
|
|
23
|
+
LanguageGraphException,
|
|
24
|
+
LanguageGraphSuperAssetNotFoundError
|
|
25
|
+
)
|
|
13
26
|
|
|
14
27
|
logger = logging.getLogger(__name__)
|
|
15
28
|
|
|
16
29
|
@dataclass
|
|
17
30
|
class LanguageGraphAsset:
|
|
18
|
-
name: str = None
|
|
19
|
-
associations:
|
|
20
|
-
attack_steps:
|
|
21
|
-
description: dict =
|
|
31
|
+
name: Optional[str] = None
|
|
32
|
+
associations: list[LanguageGraphAssociation] = field(default_factory=lambda: [])
|
|
33
|
+
attack_steps: list[LanguageGraphAttackStep] = field(default_factory=lambda: [])
|
|
34
|
+
description: dict = field(default_factory=lambda: {})
|
|
22
35
|
# MAL languages currently do not support multiple inheritance, but this is
|
|
23
36
|
# futureproofing at its most hopeful.
|
|
24
|
-
super_assets: list =
|
|
25
|
-
sub_assets: list =
|
|
37
|
+
super_assets: list = field(default_factory=lambda: [])
|
|
38
|
+
sub_assets: list = field(default_factory=lambda: [])
|
|
39
|
+
is_abstract: Optional[bool] = None
|
|
26
40
|
|
|
27
|
-
def to_dict(self):
|
|
28
|
-
|
|
41
|
+
def to_dict(self) -> dict:
|
|
42
|
+
"""Convert LanguageGraphAsset to dictionary"""
|
|
43
|
+
node_dict: dict[str, Any] = {
|
|
29
44
|
'name': self.name,
|
|
30
45
|
'associations': [],
|
|
31
46
|
'attack_steps': [],
|
|
@@ -46,7 +61,10 @@ class LanguageGraphAsset:
|
|
|
46
61
|
node_dict['sub_assets'].append(sub_asset.name)
|
|
47
62
|
return node_dict
|
|
48
63
|
|
|
49
|
-
def
|
|
64
|
+
def __repr__(self) -> str:
|
|
65
|
+
return str(self.to_dict())
|
|
66
|
+
|
|
67
|
+
def is_subasset_of(self, target_asset: LanguageGraphAsset) -> bool:
|
|
50
68
|
"""
|
|
51
69
|
Check if an asset extends the target asset through inheritance.
|
|
52
70
|
|
|
@@ -66,7 +84,7 @@ class LanguageGraphAsset:
|
|
|
66
84
|
current_assets.extend(current_asset.super_assets)
|
|
67
85
|
return False
|
|
68
86
|
|
|
69
|
-
def get_all_subassets(self):
|
|
87
|
+
def get_all_subassets(self) -> list[LanguageGraphAsset]:
|
|
70
88
|
"""
|
|
71
89
|
Return a list of all of the assets that directly or indirectly extend
|
|
72
90
|
this asset.
|
|
@@ -82,7 +100,7 @@ class LanguageGraphAsset:
|
|
|
82
100
|
subassets.extend(current_asset.sub_assets)
|
|
83
101
|
return subassets
|
|
84
102
|
|
|
85
|
-
def get_all_superassets(self):
|
|
103
|
+
def get_all_superassets(self) -> list[LanguageGraphAsset]:
|
|
86
104
|
"""
|
|
87
105
|
Return a list of all of the assets that this asset directly or
|
|
88
106
|
indirectly extends.
|
|
@@ -98,6 +116,20 @@ class LanguageGraphAsset:
|
|
|
98
116
|
superassets.extend(current_asset.super_assets)
|
|
99
117
|
return superassets
|
|
100
118
|
|
|
119
|
+
def get_all_common_superassets(
|
|
120
|
+
self, other: LanguageGraphAsset
|
|
121
|
+
) -> set[Optional[str]]:
|
|
122
|
+
"""Return a set of all common ancestors between this asset
|
|
123
|
+
and the other asset given as parameter"""
|
|
124
|
+
self_superassets = set(
|
|
125
|
+
asset.name for asset in self.get_all_superassets()
|
|
126
|
+
)
|
|
127
|
+
other_superassets = set(
|
|
128
|
+
asset.name for asset in other.get_all_superassets()
|
|
129
|
+
)
|
|
130
|
+
return self_superassets.intersection(other_superassets)
|
|
131
|
+
|
|
132
|
+
|
|
101
133
|
@dataclass
|
|
102
134
|
class LanguageGraphAssociationField:
|
|
103
135
|
asset: LanguageGraphAsset
|
|
@@ -105,14 +137,16 @@ class LanguageGraphAssociationField:
|
|
|
105
137
|
minimum: int
|
|
106
138
|
maximum: int
|
|
107
139
|
|
|
140
|
+
|
|
108
141
|
@dataclass
|
|
109
142
|
class LanguageGraphAssociation:
|
|
110
143
|
name: str
|
|
111
144
|
left_field: LanguageGraphAssociationField
|
|
112
145
|
right_field: LanguageGraphAssociationField
|
|
113
|
-
description: dict = None
|
|
146
|
+
description: Optional[dict] = None
|
|
114
147
|
|
|
115
|
-
def to_dict(self):
|
|
148
|
+
def to_dict(self) -> dict:
|
|
149
|
+
"""Convert LanguageGraphAssociation to dictionary"""
|
|
116
150
|
node_dict = {
|
|
117
151
|
'name': self.name,
|
|
118
152
|
'left': {
|
|
@@ -132,7 +166,10 @@ class LanguageGraphAssociation:
|
|
|
132
166
|
|
|
133
167
|
return node_dict
|
|
134
168
|
|
|
135
|
-
def
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
return str(self.to_dict())
|
|
171
|
+
|
|
172
|
+
def contains_fieldname(self, fieldname: str) -> bool:
|
|
136
173
|
"""
|
|
137
174
|
Check if the association contains the field name given as a parameter.
|
|
138
175
|
|
|
@@ -147,7 +184,7 @@ class LanguageGraphAssociation:
|
|
|
147
184
|
return True
|
|
148
185
|
return False
|
|
149
186
|
|
|
150
|
-
def contains_asset(self, asset):
|
|
187
|
+
def contains_asset(self, asset: Any) -> bool:
|
|
151
188
|
"""
|
|
152
189
|
Check if the association matches the asset given as a parameter. A
|
|
153
190
|
match can either be an explicit one or if the asset given subassets
|
|
@@ -164,7 +201,7 @@ class LanguageGraphAssociation:
|
|
|
164
201
|
return True
|
|
165
202
|
return False
|
|
166
203
|
|
|
167
|
-
def get_opposite_fieldname(self, fieldname):
|
|
204
|
+
def get_opposite_fieldname(self, fieldname: str) -> str:
|
|
168
205
|
"""
|
|
169
206
|
Return the opposite field name if the association contains the field
|
|
170
207
|
name given as a parameter.
|
|
@@ -179,11 +216,14 @@ class LanguageGraphAssociation:
|
|
|
179
216
|
if self.right_field.fieldname == fieldname:
|
|
180
217
|
return self.left_field.fieldname
|
|
181
218
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
219
|
+
msg = ('Requested fieldname "%s" from association '
|
|
220
|
+
'%s which did not contain it!')
|
|
221
|
+
logger.error(msg, fieldname, self.name)
|
|
222
|
+
raise LanguageGraphAssociationError(msg % (fieldname, self.name))
|
|
185
223
|
|
|
186
|
-
def get_opposite_asset(
|
|
224
|
+
def get_opposite_asset(
|
|
225
|
+
self, asset: LanguageGraphAsset
|
|
226
|
+
) -> Optional[LanguageGraphAsset]:
|
|
187
227
|
"""
|
|
188
228
|
Return the opposite asset if the association matches the asset given
|
|
189
229
|
as a parameter. A match can either be an explicit one or if the asset
|
|
@@ -203,22 +243,30 @@ class LanguageGraphAssociation:
|
|
|
203
243
|
if asset.is_subasset_of(self.right_field.asset):
|
|
204
244
|
return self.left_field.asset
|
|
205
245
|
|
|
206
|
-
logger.warning(
|
|
207
|
-
|
|
246
|
+
logger.warning(
|
|
247
|
+
'Requested asset "%s" from association %s'
|
|
248
|
+
'which did not contain it!', asset.name, self.name
|
|
249
|
+
)
|
|
208
250
|
return None
|
|
209
251
|
|
|
252
|
+
|
|
210
253
|
@dataclass
|
|
211
254
|
class LanguageGraphAttackStep:
|
|
212
|
-
name: str
|
|
213
|
-
type: str
|
|
214
|
-
asset:
|
|
215
|
-
ttc: dict =
|
|
216
|
-
children: dict =
|
|
217
|
-
parents: dict =
|
|
218
|
-
description: dict =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
255
|
+
name: str
|
|
256
|
+
type: str
|
|
257
|
+
asset: LanguageGraphAsset
|
|
258
|
+
ttc: dict = field(default_factory = lambda: {})
|
|
259
|
+
children: dict = field(default_factory = lambda: {})
|
|
260
|
+
parents: dict = field(default_factory = lambda: {})
|
|
261
|
+
description: dict = field(default_factory = lambda: {})
|
|
262
|
+
attributes: Optional[dict] = None
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def qualified_name(self) -> str:
|
|
266
|
+
return f"{self.asset.name}:{self.name}"
|
|
267
|
+
|
|
268
|
+
def to_dict(self) -> dict:
|
|
269
|
+
node_dict: dict[Any, Any] = {
|
|
222
270
|
'name': self.name,
|
|
223
271
|
'type': self.type,
|
|
224
272
|
'asset': self.asset.name,
|
|
@@ -248,11 +296,20 @@ class LanguageGraphAttackStep:
|
|
|
248
296
|
|
|
249
297
|
return node_dict
|
|
250
298
|
|
|
299
|
+
def __repr__(self) -> str:
|
|
300
|
+
return str(self.to_dict())
|
|
301
|
+
|
|
251
302
|
|
|
252
303
|
class DependencyChain:
|
|
253
|
-
def __init__(self, type, next_link):
|
|
304
|
+
def __init__(self, type: str, next_link: Optional[DependencyChain]):
|
|
254
305
|
self.type = type
|
|
255
|
-
self.next_link = next_link
|
|
306
|
+
self.next_link: Optional[DependencyChain] = next_link
|
|
307
|
+
self.fieldname: str = ""
|
|
308
|
+
self.association: Optional[LanguageGraphAssociation] = None
|
|
309
|
+
self.left_chain: Optional[DependencyChain] = None
|
|
310
|
+
self.right_chain: Optional[DependencyChain] = None
|
|
311
|
+
self.subtype: Optional[Any] = None
|
|
312
|
+
self.current_link: Optional[DependencyChain] = None
|
|
256
313
|
|
|
257
314
|
def __iter__(self):
|
|
258
315
|
self.current_link = self
|
|
@@ -265,55 +322,97 @@ class DependencyChain:
|
|
|
265
322
|
return dep_chain
|
|
266
323
|
raise StopIteration
|
|
267
324
|
|
|
268
|
-
def to_dict(self):
|
|
325
|
+
def to_dict(self) -> dict:
|
|
326
|
+
"""Convert DependencyChain to dictionary"""
|
|
269
327
|
match (self.type):
|
|
270
328
|
case 'union' | 'intersection' | 'difference':
|
|
271
329
|
return {self.type: {
|
|
272
|
-
'left': self.left_chain.to_dict()
|
|
330
|
+
'left': self.left_chain.to_dict()
|
|
331
|
+
if self.left_chain else {},
|
|
273
332
|
'right': self.right_chain.to_dict()
|
|
333
|
+
if self.right_chain else {}
|
|
274
334
|
}
|
|
275
335
|
}
|
|
276
336
|
|
|
277
337
|
case 'field':
|
|
278
|
-
|
|
279
|
-
|
|
338
|
+
if not self.association:
|
|
339
|
+
raise LanguageGraphAssociationError("Missing association for dep chain")
|
|
340
|
+
return {self.association.name:
|
|
280
341
|
{'fieldname': self.fieldname,
|
|
281
342
|
'next_association':
|
|
282
|
-
|
|
343
|
+
self.next_link.to_dict()
|
|
344
|
+
if self.next_link else {}
|
|
345
|
+
}
|
|
283
346
|
}
|
|
284
|
-
}
|
|
285
347
|
|
|
286
348
|
case 'transitive':
|
|
287
349
|
return {'transitive':
|
|
288
350
|
self.next_link.to_dict()
|
|
351
|
+
if self.next_link else {}
|
|
289
352
|
}
|
|
290
353
|
|
|
291
354
|
case 'subType':
|
|
355
|
+
if not self.subtype:
|
|
356
|
+
raise LanguageGraphException(
|
|
357
|
+
"No subtype for dependency chain"
|
|
358
|
+
)
|
|
359
|
+
if not self.next_link:
|
|
360
|
+
raise LanguageGraphException(
|
|
361
|
+
"No next link for subtype dependency chain"
|
|
362
|
+
)
|
|
292
363
|
return {'subType': self.subtype.name,
|
|
293
364
|
'expression': self.next_link.to_dict()
|
|
294
365
|
}
|
|
295
366
|
|
|
296
367
|
case _:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
368
|
+
msg = 'Unknown associations chain element %s!'
|
|
369
|
+
logger.error(msg, self.type)
|
|
370
|
+
raise LanguageGraphAssociationError(msg % self.type)
|
|
371
|
+
|
|
372
|
+
def __repr__(self) -> str:
|
|
373
|
+
return str(self.to_dict())
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class LanguageGraph():
|
|
377
|
+
"""Graph representation of a MAL language"""
|
|
378
|
+
def __init__(self, lang: dict):
|
|
379
|
+
self.assets: list = []
|
|
380
|
+
self.associations: list = []
|
|
381
|
+
self.attack_steps: list = []
|
|
382
|
+
self._lang_spec: dict = lang
|
|
383
|
+
self.metadata = {
|
|
384
|
+
"version": lang["defines"]["version"],
|
|
385
|
+
"id": lang["defines"]["id"],
|
|
386
|
+
}
|
|
387
|
+
self._generate_graph()
|
|
300
388
|
|
|
389
|
+
@classmethod
|
|
390
|
+
def from_mal_spec(cls, mal_spec_file: str) -> LanguageGraph:
|
|
391
|
+
"""
|
|
392
|
+
Create a LanguageGraph from a .mal file (a MAL spec).
|
|
301
393
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
394
|
+
Arguments:
|
|
395
|
+
mal_spec_file - the path to the .mal file
|
|
396
|
+
"""
|
|
397
|
+
logger.info("Loading mal spec %s", mal_spec_file)
|
|
398
|
+
return LanguageGraph(MalCompiler().compile(mal_spec_file))
|
|
307
399
|
|
|
308
|
-
|
|
400
|
+
@classmethod
|
|
401
|
+
def from_mar_archive(cls, mar_archive: str) -> LanguageGraph:
|
|
309
402
|
"""
|
|
310
|
-
|
|
403
|
+
Create a LanguageGraph from a ".mar" archive provided by malc
|
|
404
|
+
(https://github.com/mal-lang/malc).
|
|
311
405
|
|
|
312
406
|
Arguments:
|
|
313
|
-
|
|
407
|
+
mar_archive - the path to a ".mar" archive
|
|
314
408
|
"""
|
|
409
|
+
logger.info('Loading mar archive %s', mar_archive)
|
|
410
|
+
with zipfile.ZipFile(mar_archive, 'r') as archive:
|
|
411
|
+
langspec = archive.read('langspec.json')
|
|
412
|
+
return LanguageGraph(json.loads(langspec))
|
|
315
413
|
|
|
316
|
-
|
|
414
|
+
def _to_dict(self):
|
|
415
|
+
"""Converts LanguageGraph into a dict"""
|
|
317
416
|
serialized_assets = []
|
|
318
417
|
for asset in self.assets:
|
|
319
418
|
serialized_assets.append(asset.to_dict())
|
|
@@ -323,24 +422,67 @@ class LanguageGraph:
|
|
|
323
422
|
serialized_attack_steps = []
|
|
324
423
|
for attack_step in self.attack_steps:
|
|
325
424
|
serialized_attack_steps.append(attack_step.to_dict())
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
425
|
+
|
|
426
|
+
logger.debug(
|
|
427
|
+
'Serializing %s assets, %s associations, and %s attack steps',
|
|
428
|
+
len(serialized_assets), len(serialized_associations),
|
|
429
|
+
len(serialized_attack_steps)
|
|
430
|
+
)
|
|
431
|
+
|
|
330
432
|
serialized_graph = {
|
|
331
433
|
'Assets': serialized_assets,
|
|
332
434
|
'Associations': serialized_associations,
|
|
333
435
|
'Attack Steps': serialized_attack_steps
|
|
334
436
|
}
|
|
335
|
-
|
|
336
|
-
|
|
437
|
+
return serialized_graph
|
|
438
|
+
|
|
439
|
+
def save_to_file(self, filename: str) -> None:
|
|
440
|
+
"""Save to json/yml depending on extension"""
|
|
441
|
+
return save_dict_to_file(filename, self._to_dict())
|
|
442
|
+
|
|
443
|
+
@classmethod
|
|
444
|
+
def _from_dict(cls, serialized_object: dict) -> None:
|
|
445
|
+
raise NotImplementedError(
|
|
446
|
+
"Converting from dict feature is not implemented yet")
|
|
447
|
+
|
|
448
|
+
@classmethod
|
|
449
|
+
def load_from_file(cls, filename: str) -> LanguageGraph:
|
|
450
|
+
"""Create LanguageGraph from mal, mar, yaml or json"""
|
|
451
|
+
lang_graph = None
|
|
452
|
+
if filename.endswith('.mal'):
|
|
453
|
+
lang_graph = cls.from_mal_spec(filename)
|
|
454
|
+
elif filename.endswith('.mar'):
|
|
455
|
+
lang_graph = cls.from_mar_archive(filename)
|
|
456
|
+
elif filename.endswith(('yaml', 'yml')):
|
|
457
|
+
lang_graph = cls._from_dict(load_dict_from_yaml_file(filename))
|
|
458
|
+
elif filename.endswith(('json')):
|
|
459
|
+
lang_graph = cls._from_dict(load_dict_from_json_file(filename))
|
|
460
|
+
|
|
461
|
+
if lang_graph:
|
|
462
|
+
return lang_graph
|
|
463
|
+
|
|
464
|
+
raise TypeError(
|
|
465
|
+
"Unknown file extension, expected json/mal/mar/yml/yaml"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def save_language_specification_to_json(self, filename: str) -> None:
|
|
469
|
+
"""
|
|
470
|
+
Save a MAL language specification dictionary to a JSON file
|
|
337
471
|
|
|
472
|
+
Arguments:
|
|
473
|
+
filename - the JSON filename where the language specification will be written
|
|
474
|
+
"""
|
|
475
|
+
logger.info('Save language specification to %s', filename)
|
|
476
|
+
|
|
477
|
+
with open(filename, 'w', encoding='utf-8') as file:
|
|
478
|
+
json.dump(self._lang_spec, file, indent=4)
|
|
338
479
|
|
|
339
480
|
def process_step_expression(self,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
481
|
+
lang: dict,
|
|
482
|
+
target_asset,
|
|
483
|
+
dep_chain,
|
|
484
|
+
step_expression: dict
|
|
485
|
+
) -> tuple:
|
|
344
486
|
"""
|
|
345
487
|
Recursively process an attack step expression.
|
|
346
488
|
|
|
@@ -363,8 +505,13 @@ class LanguageGraph:
|
|
|
363
505
|
A tuple triplet containing the target asset, the resulting parent
|
|
364
506
|
associations chain, and the name of the attack step.
|
|
365
507
|
"""
|
|
366
|
-
|
|
367
|
-
|
|
508
|
+
|
|
509
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
510
|
+
# Avoid running json.dumps when not in debug
|
|
511
|
+
logger.debug(
|
|
512
|
+
'Processing Step Expression:\n%s',
|
|
513
|
+
json.dumps(step_expression, indent = 2)
|
|
514
|
+
)
|
|
368
515
|
|
|
369
516
|
match (step_expression['type']):
|
|
370
517
|
case 'attackStep':
|
|
@@ -383,10 +530,12 @@ class LanguageGraph:
|
|
|
383
530
|
rh_target_asset, rh_dep_chain, _ = self.process_step_expression(
|
|
384
531
|
lang, target_asset, dep_chain, step_expression['rhs'])
|
|
385
532
|
|
|
386
|
-
if lh_target_asset
|
|
387
|
-
logger.error(
|
|
388
|
-
|
|
389
|
-
|
|
533
|
+
if not lh_target_asset.get_all_common_superassets(rh_target_asset):
|
|
534
|
+
logger.error(
|
|
535
|
+
"Set operation attempted between targets that"
|
|
536
|
+
" do not share any common superassets: %s and %s!",
|
|
537
|
+
lh_target_asset.name, rh_target_asset.name
|
|
538
|
+
)
|
|
390
539
|
return (None, None, None)
|
|
391
540
|
|
|
392
541
|
new_dep_chain = DependencyChain(
|
|
@@ -401,9 +550,8 @@ class LanguageGraph:
|
|
|
401
550
|
case 'variable':
|
|
402
551
|
# Fetch the step expression associated with the variable from
|
|
403
552
|
# the language specification and resolve that.
|
|
404
|
-
variable_step_expr =
|
|
405
|
-
|
|
406
|
-
target_asset.name, step_expression['name'])
|
|
553
|
+
variable_step_expr = self._get_variable_for_asset_type_by_name(
|
|
554
|
+
target_asset.name, step_expression['name'])
|
|
407
555
|
if variable_step_expr:
|
|
408
556
|
return self.process_step_expression(
|
|
409
557
|
lang,
|
|
@@ -412,8 +560,10 @@ class LanguageGraph:
|
|
|
412
560
|
variable_step_expr)
|
|
413
561
|
|
|
414
562
|
else:
|
|
415
|
-
logger.error(
|
|
416
|
-
|
|
563
|
+
logger.error(
|
|
564
|
+
'Failed to find variable %s for %s',
|
|
565
|
+
step_expression["name"], target_asset.name
|
|
566
|
+
)
|
|
417
567
|
return (None, None, None)
|
|
418
568
|
|
|
419
569
|
case 'field':
|
|
@@ -422,7 +572,9 @@ class LanguageGraph:
|
|
|
422
572
|
# fieldname and association to the parent associations chain.
|
|
423
573
|
fieldname = step_expression['name']
|
|
424
574
|
if not target_asset:
|
|
425
|
-
logger.error(
|
|
575
|
+
logger.error(
|
|
576
|
+
'Missing target asset for field "%s"!', fieldname
|
|
577
|
+
)
|
|
426
578
|
return (None, None, None)
|
|
427
579
|
|
|
428
580
|
new_target_asset = None
|
|
@@ -447,8 +599,10 @@ class LanguageGraph:
|
|
|
447
599
|
return (new_target_asset,
|
|
448
600
|
new_dep_chain,
|
|
449
601
|
None)
|
|
450
|
-
logger.error(
|
|
451
|
-
|
|
602
|
+
logger.error(
|
|
603
|
+
'Failed to find field "%s" on asset "%s"!',
|
|
604
|
+
fieldname, target_asset.name
|
|
605
|
+
)
|
|
452
606
|
return (None, None, None)
|
|
453
607
|
|
|
454
608
|
case 'transitive':
|
|
@@ -481,19 +635,23 @@ class LanguageGraph:
|
|
|
481
635
|
dep_chain,
|
|
482
636
|
step_expression['stepExpression'])
|
|
483
637
|
|
|
484
|
-
subtype_asset = next((asset for asset in self.assets
|
|
485
|
-
|
|
638
|
+
subtype_asset = next((asset for asset in self.assets if asset.name == subtype_name), None)
|
|
639
|
+
|
|
486
640
|
if not subtype_asset:
|
|
487
|
-
|
|
488
|
-
|
|
641
|
+
msg = 'Failed to find subtype attackstep "{subtype_name}"'
|
|
642
|
+
logger.error(msg)
|
|
643
|
+
raise LanguageGraphException(msg)
|
|
644
|
+
|
|
489
645
|
if not subtype_asset.is_subasset_of(result_target_asset):
|
|
490
|
-
logger.error(
|
|
491
|
-
|
|
492
|
-
'
|
|
646
|
+
logger.error(
|
|
647
|
+
'Found subtype "%s" which does not extend "%s", '
|
|
648
|
+
'therefore the subtype cannot be resolved.',
|
|
649
|
+
subtype_name, result_target_asset.name
|
|
650
|
+
)
|
|
493
651
|
return (None, None, None)
|
|
494
652
|
|
|
495
653
|
new_dep_chain = DependencyChain(
|
|
496
|
-
type = '
|
|
654
|
+
type = 'subType',
|
|
497
655
|
next_link = result_dep_chain)
|
|
498
656
|
new_dep_chain.subtype = subtype_asset
|
|
499
657
|
return (subtype_asset,
|
|
@@ -519,11 +677,16 @@ class LanguageGraph:
|
|
|
519
677
|
rh_attack_step_name)
|
|
520
678
|
|
|
521
679
|
case _:
|
|
522
|
-
logger.error(
|
|
523
|
-
|
|
680
|
+
logger.error(
|
|
681
|
+
'Unknown attack step type: "%s"', step_expression["type"]
|
|
682
|
+
)
|
|
524
683
|
return (None, None, None)
|
|
525
684
|
|
|
526
|
-
def reverse_dep_chain(
|
|
685
|
+
def reverse_dep_chain(
|
|
686
|
+
self,
|
|
687
|
+
dep_chain: Optional[DependencyChain],
|
|
688
|
+
reverse_chain: Optional[DependencyChain]
|
|
689
|
+
) -> Optional[DependencyChain]:
|
|
527
690
|
"""
|
|
528
691
|
Recursively reverse the associations chain. From parent to child or
|
|
529
692
|
vice versa.
|
|
@@ -566,94 +729,109 @@ class LanguageGraph:
|
|
|
566
729
|
|
|
567
730
|
case 'field':
|
|
568
731
|
association = dep_chain.association
|
|
732
|
+
|
|
733
|
+
if not association:
|
|
734
|
+
raise LanguageGraphException(
|
|
735
|
+
"Missing association for dep chain"
|
|
736
|
+
)
|
|
737
|
+
|
|
569
738
|
opposite_fieldname = association.get_opposite_fieldname(
|
|
570
739
|
dep_chain.fieldname)
|
|
571
740
|
new_dep_chain = DependencyChain(
|
|
572
741
|
type = 'field',
|
|
573
|
-
next_link = reverse_chain
|
|
742
|
+
next_link = reverse_chain
|
|
743
|
+
)
|
|
574
744
|
new_dep_chain.fieldname = opposite_fieldname
|
|
575
745
|
new_dep_chain.association = association
|
|
576
|
-
return self.reverse_dep_chain(
|
|
577
|
-
|
|
746
|
+
return self.reverse_dep_chain(
|
|
747
|
+
dep_chain.next_link,
|
|
748
|
+
new_dep_chain
|
|
749
|
+
)
|
|
578
750
|
|
|
579
751
|
case 'subType':
|
|
580
752
|
result_reverse_chain = self.reverse_dep_chain(
|
|
581
|
-
|
|
582
|
-
reverse_chain
|
|
753
|
+
dep_chain.next_link,
|
|
754
|
+
reverse_chain
|
|
755
|
+
)
|
|
583
756
|
new_dep_chain = DependencyChain(
|
|
584
|
-
type = '
|
|
585
|
-
next_link = result_reverse_chain
|
|
757
|
+
type = 'subType',
|
|
758
|
+
next_link = result_reverse_chain
|
|
759
|
+
)
|
|
586
760
|
new_dep_chain.subtype = dep_chain.subtype
|
|
587
761
|
return new_dep_chain
|
|
762
|
+
# return reverse_chain
|
|
588
763
|
|
|
589
764
|
case _:
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
765
|
+
msg = 'Unknown assoc chain element "%s"'
|
|
766
|
+
logger.error(msg, dep_chain.type)
|
|
767
|
+
raise LanguageGraphAssociationError(msg % dep_chain.type)
|
|
594
768
|
|
|
595
|
-
def
|
|
769
|
+
def _generate_graph(self) -> None:
|
|
596
770
|
"""
|
|
597
|
-
Generate language graph starting from
|
|
598
|
-
|
|
599
|
-
Arguments:
|
|
600
|
-
lang - a dictionary representing the MAL language specification
|
|
771
|
+
Generate language graph starting from the MAL language specification
|
|
772
|
+
given in the constructor.
|
|
601
773
|
"""
|
|
602
774
|
# Generate all of the asset nodes of the language graph.
|
|
603
|
-
for asset in
|
|
604
|
-
logger.debug(
|
|
605
|
-
|
|
775
|
+
for asset in self._lang_spec['assets']:
|
|
776
|
+
logger.debug(
|
|
777
|
+
'Create asset language graph nodes for asset %s',
|
|
778
|
+
asset["name"]
|
|
779
|
+
)
|
|
606
780
|
asset_node = LanguageGraphAsset(
|
|
607
781
|
name = asset['name'],
|
|
608
782
|
associations = [],
|
|
609
783
|
attack_steps = [],
|
|
610
784
|
description = asset['meta'],
|
|
611
785
|
super_assets = [],
|
|
612
|
-
sub_assets = []
|
|
786
|
+
sub_assets = [],
|
|
787
|
+
is_abstract = asset['isAbstract']
|
|
613
788
|
)
|
|
614
789
|
self.assets.append(asset_node)
|
|
615
790
|
|
|
616
791
|
# Link assets based on inheritance
|
|
617
|
-
for asset_info in
|
|
792
|
+
for asset_info in self._lang_spec['assets']:
|
|
618
793
|
asset = next((asset for asset in self.assets \
|
|
619
794
|
if asset.name == asset_info['name']), None)
|
|
620
|
-
if not asset:
|
|
621
|
-
logger.error('Failed to find asset '
|
|
622
|
-
f'\"{asset_info["name"]}\"!')
|
|
623
|
-
return 1
|
|
624
795
|
if asset_info['superAsset']:
|
|
625
796
|
super_asset = next((asset for asset in self.assets \
|
|
626
797
|
if asset.name == asset_info['superAsset']), None)
|
|
627
798
|
if not super_asset:
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
799
|
+
msg = 'Failed to find super asset "%s" for asset "%s"!'
|
|
800
|
+
logger.error(
|
|
801
|
+
msg, asset_info["superAsset"], asset_info["name"])
|
|
802
|
+
raise LanguageGraphSuperAssetNotFoundError(
|
|
803
|
+
msg % (asset_info["superAsset"], asset_info["name"]))
|
|
804
|
+
|
|
632
805
|
super_asset.sub_assets.append(asset)
|
|
633
806
|
asset.super_assets.append(super_asset)
|
|
634
807
|
|
|
635
808
|
# Generate all of the association nodes of the language graph.
|
|
636
809
|
for asset in self.assets:
|
|
637
|
-
logger.debug(
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
810
|
+
logger.debug(
|
|
811
|
+
'Create association language graph nodes for asset %s',
|
|
812
|
+
asset.name
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
associations = self._get_associations_for_asset_type(asset.name)
|
|
642
816
|
for association in associations:
|
|
643
817
|
left_asset = next((asset for asset in self.assets \
|
|
644
818
|
if asset.name == association['leftAsset']), None)
|
|
645
819
|
if not left_asset:
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
820
|
+
msg = 'Left asset "%s" for association "%s" not found!'
|
|
821
|
+
logger.error(
|
|
822
|
+
msg, association["leftAsset"], association["name"])
|
|
823
|
+
raise LanguageGraphAssociationError(
|
|
824
|
+
msg % (association["leftAsset"], association["name"]))
|
|
825
|
+
|
|
650
826
|
right_asset = next((asset for asset in self.assets \
|
|
651
827
|
if asset.name == association['rightAsset']), None)
|
|
652
828
|
if not right_asset:
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
829
|
+
msg = 'Right asset "%s" for association "%s" not found!'
|
|
830
|
+
logger.error(
|
|
831
|
+
msg, association["rightAsset"], association["name"])
|
|
832
|
+
raise LanguageGraphAssociationError(
|
|
833
|
+
msg % (association["rightAsset"], association["name"])
|
|
834
|
+
)
|
|
657
835
|
|
|
658
836
|
# Technically we should be more exhaustive and check the
|
|
659
837
|
# flipped version too and all of the fieldnames as well.
|
|
@@ -694,17 +872,19 @@ class LanguageGraph:
|
|
|
694
872
|
|
|
695
873
|
# Generate all of the attack step nodes of the language graph.
|
|
696
874
|
for asset in self.assets:
|
|
697
|
-
logger.debug(
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
875
|
+
logger.debug(
|
|
876
|
+
'Create attack steps language graph nodes for asset %s',
|
|
877
|
+
asset.name
|
|
878
|
+
)
|
|
879
|
+
attack_steps = self._get_attacks_for_asset_type(asset.name)
|
|
702
880
|
for attack_step_name, attack_step_attribs in attack_steps.items():
|
|
703
|
-
logger.debug(
|
|
704
|
-
|
|
881
|
+
logger.debug(
|
|
882
|
+
'Create attack step language graph nodes for %s',
|
|
883
|
+
attack_step_name
|
|
884
|
+
)
|
|
705
885
|
|
|
706
886
|
attack_step_node = LanguageGraphAttackStep(
|
|
707
|
-
name =
|
|
887
|
+
name = attack_step_name,
|
|
708
888
|
type = attack_step_attribs['type'],
|
|
709
889
|
asset = asset,
|
|
710
890
|
ttc = attack_step_attribs['ttc'],
|
|
@@ -718,8 +898,10 @@ class LanguageGraph:
|
|
|
718
898
|
|
|
719
899
|
# Then, link all of the attack step nodes according to their associations.
|
|
720
900
|
for attack_step in self.attack_steps:
|
|
721
|
-
logger.debug(
|
|
722
|
-
|
|
901
|
+
logger.debug(
|
|
902
|
+
'Determining children for attack step %s',
|
|
903
|
+
attack_step.name
|
|
904
|
+
)
|
|
723
905
|
step_expressions = \
|
|
724
906
|
attack_step.attributes['reaches']['stepExpressions'] if \
|
|
725
907
|
attack_step.attributes['reaches'] else []
|
|
@@ -728,36 +910,31 @@ class LanguageGraph:
|
|
|
728
910
|
# Resolve each of the attack step expressions listed for this
|
|
729
911
|
# attack step to determine children.
|
|
730
912
|
(target_asset, dep_chain, attack_step_name) = \
|
|
731
|
-
self.process_step_expression(
|
|
913
|
+
self.process_step_expression(self._lang_spec,
|
|
732
914
|
attack_step.asset,
|
|
733
915
|
None,
|
|
734
916
|
step_expression)
|
|
735
917
|
if not target_asset:
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
return 1
|
|
743
|
-
|
|
744
|
-
attack_step_fullname = target_asset.name + ':' + attack_step_name
|
|
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)
|
|
922
|
+
)
|
|
923
|
+
|
|
745
924
|
target_attack_step = next((attack_step \
|
|
746
925
|
for attack_step in target_asset.attack_steps \
|
|
747
|
-
if attack_step.name ==
|
|
926
|
+
if attack_step.name == attack_step_name), None)
|
|
748
927
|
|
|
749
928
|
if not target_attack_step:
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
json.dumps(step_expression, indent = 2))
|
|
760
|
-
return 1
|
|
929
|
+
msg = 'Failed to find target attack step %s on %s to ' \
|
|
930
|
+
'link with for step expression:\n%s'
|
|
931
|
+
raise LanguageGraphStepExpressionError(
|
|
932
|
+
msg % (
|
|
933
|
+
attack_step_name,
|
|
934
|
+
target_asset.name,
|
|
935
|
+
json.dumps(step_expression, indent = 2)
|
|
936
|
+
)
|
|
937
|
+
)
|
|
761
938
|
|
|
762
939
|
# It is easier to create the parent associations chain due to
|
|
763
940
|
# the left-hand first progression.
|
|
@@ -780,4 +957,228 @@ class LanguageGraph:
|
|
|
780
957
|
self.reverse_dep_chain(dep_chain,
|
|
781
958
|
None))]
|
|
782
959
|
|
|
783
|
-
|
|
960
|
+
def _get_attacks_for_asset_type(self, asset_type: str) -> dict:
|
|
961
|
+
"""
|
|
962
|
+
Get all Attack Steps for a specific Class
|
|
963
|
+
|
|
964
|
+
Arguments:
|
|
965
|
+
asset_type - a string representing the class for which we want to list
|
|
966
|
+
the possible attack steps
|
|
967
|
+
|
|
968
|
+
Return:
|
|
969
|
+
A dictionary representing the set of possible attacks for the specified
|
|
970
|
+
class. Each key in the dictionary is an attack name and is associated
|
|
971
|
+
with a dictionary containing other characteristics of the attack such as
|
|
972
|
+
type of attack, TTC distribution, child attack steps and other information
|
|
973
|
+
"""
|
|
974
|
+
attack_steps: dict = {}
|
|
975
|
+
try:
|
|
976
|
+
asset = next((asset for asset in self._lang_spec['assets'] if asset['name'] == asset_type))
|
|
977
|
+
except StopIteration:
|
|
978
|
+
logger.error(
|
|
979
|
+
'Failed to find asset type %s when looking'
|
|
980
|
+
'for attack steps.', asset_type
|
|
981
|
+
)
|
|
982
|
+
return attack_steps
|
|
983
|
+
|
|
984
|
+
logger.debug(
|
|
985
|
+
'Get attack steps for %s asset from '
|
|
986
|
+
'language specification.', asset["name"]
|
|
987
|
+
)
|
|
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
|
+
|
|
1013
|
+
|
|
1014
|
+
return attack_steps
|
|
1015
|
+
|
|
1016
|
+
def _get_associations_for_asset_type(self, asset_type: str) -> list:
|
|
1017
|
+
"""
|
|
1018
|
+
Get all Associations for a specific Class
|
|
1019
|
+
|
|
1020
|
+
Arguments:
|
|
1021
|
+
asset_type - a string representing the class for which we want to list
|
|
1022
|
+
the associations
|
|
1023
|
+
|
|
1024
|
+
Return:
|
|
1025
|
+
A dictionary representing the set of associations for the specified
|
|
1026
|
+
class. Each key in the dictionary is an attack name and is associated
|
|
1027
|
+
with a dictionary containing other characteristics of the attack such as
|
|
1028
|
+
type of attack, TTC distribution, child attack steps and other information
|
|
1029
|
+
"""
|
|
1030
|
+
logger.debug(
|
|
1031
|
+
'Get associations for %s asset from '
|
|
1032
|
+
'language specification.', asset_type
|
|
1033
|
+
)
|
|
1034
|
+
associations: list = []
|
|
1035
|
+
|
|
1036
|
+
asset = next((asset for asset in self._lang_spec['assets'] if asset['name'] == \
|
|
1037
|
+
asset_type), None)
|
|
1038
|
+
if not asset:
|
|
1039
|
+
logger.error(
|
|
1040
|
+
'Failed to find asset type %s when '
|
|
1041
|
+
'looking for associations.', asset_type
|
|
1042
|
+
)
|
|
1043
|
+
return associations
|
|
1044
|
+
|
|
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
|
+
assoc_iter = (assoc for assoc in self._lang_spec['associations'] \
|
|
1050
|
+
if assoc['leftAsset'] == asset_type or \
|
|
1051
|
+
assoc['rightAsset'] == asset_type)
|
|
1052
|
+
assoc = next(assoc_iter, None)
|
|
1053
|
+
while (assoc):
|
|
1054
|
+
associations.append(assoc)
|
|
1055
|
+
assoc = next(assoc_iter, None)
|
|
1056
|
+
|
|
1057
|
+
return associations
|
|
1058
|
+
|
|
1059
|
+
def _get_variable_for_asset_type_by_name(
|
|
1060
|
+
self, asset_type: str, variable_name: str) -> dict:
|
|
1061
|
+
"""
|
|
1062
|
+
Get a variables for a specific asset type by name.
|
|
1063
|
+
NOTE: Variables are the ones specified in MAL through `let` statements
|
|
1064
|
+
|
|
1065
|
+
Arguments:
|
|
1066
|
+
asset_type - a string representing the type of asset which
|
|
1067
|
+
contains the variable
|
|
1068
|
+
variable_name - the name of the variable to search for
|
|
1069
|
+
|
|
1070
|
+
Return:
|
|
1071
|
+
A dictionary representing the step expressions for the specified variable.
|
|
1072
|
+
"""
|
|
1073
|
+
|
|
1074
|
+
asset = next((asset for asset in self._lang_spec['assets'] if asset['name'] == \
|
|
1075
|
+
asset_type), None)
|
|
1076
|
+
if not asset:
|
|
1077
|
+
msg = 'Failed to find asset type %s when looking for variable.'
|
|
1078
|
+
logger.error(msg, asset_type)
|
|
1079
|
+
raise LanguageGraphException(msg % asset_type)
|
|
1080
|
+
|
|
1081
|
+
variable_dict = next((variable for variable in \
|
|
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']
|
|
1096
|
+
|
|
1097
|
+
def regenerate_graph(self) -> None:
|
|
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]:
|
|
1112
|
+
"""
|
|
1113
|
+
Get an asset based on its name
|
|
1114
|
+
|
|
1115
|
+
Arguments:
|
|
1116
|
+
asset_name - a string containing the asset name
|
|
1117
|
+
|
|
1118
|
+
Return:
|
|
1119
|
+
The asset matching the name.
|
|
1120
|
+
None if there is no match.
|
|
1121
|
+
"""
|
|
1122
|
+
for asset in self.assets:
|
|
1123
|
+
if asset.name == asset_name:
|
|
1124
|
+
return asset
|
|
1125
|
+
|
|
1126
|
+
return None
|
|
1127
|
+
|
|
1128
|
+
def get_association_by_fields_and_assets(
|
|
1129
|
+
self,
|
|
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
|
|
1137
|
+
|
|
1138
|
+
Arguments:
|
|
1139
|
+
first_field - a string containing the first field
|
|
1140
|
+
second_field - a string containing the second field
|
|
1141
|
+
first_asset_name - a string representing the first asset type
|
|
1142
|
+
second_asset_name - a string representing the second asset type
|
|
1143
|
+
|
|
1144
|
+
Return:
|
|
1145
|
+
The association matching the fieldnames and asset types.
|
|
1146
|
+
None if there is no match.
|
|
1147
|
+
"""
|
|
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
|
+
|
|
1171
|
+
# If the asset and fields match either way we accept it as a match.
|
|
1172
|
+
if assoc.left_field.fieldname == first_field and \
|
|
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
|
|
1177
|
+
|
|
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
|
+
|
|
1184
|
+
return None
|