mal-toolbox 0.3.11__py3-none-any.whl → 1.0.1__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.3.11.dist-info → mal_toolbox-1.0.1.dist-info}/METADATA +4 -23
- mal_toolbox-1.0.1.dist-info/RECORD +26 -0
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.1.dist-info}/WHEEL +1 -1
- maltoolbox/__init__.py +5 -6
- maltoolbox/__main__.py +3 -34
- maltoolbox/attackgraph/__init__.py +7 -1
- maltoolbox/attackgraph/attackgraph.py +58 -195
- maltoolbox/attackgraph/node.py +2 -82
- maltoolbox/file_utils.py +1 -1
- maltoolbox/language/__init__.py +11 -0
- maltoolbox/language/languagegraph.py +649 -371
- maltoolbox/model.py +7 -221
- maltoolbox/py.typed +0 -0
- maltoolbox/translators/securicad.py +1 -1
- maltoolbox/translators/updater.py +1 -1
- mal_toolbox-0.3.11.dist-info/RECORD +0 -29
- maltoolbox/attackgraph/analyzers/apriori.py +0 -243
- maltoolbox/attackgraph/attacker.py +0 -109
- maltoolbox/attackgraph/query.py +0 -196
- maltoolbox/ingestors/neo4j.py +0 -244
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.1.dist-info}/entry_points.txt +0 -0
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.1.dist-info/licenses}/AUTHORS +0 -0
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.1.dist-info/licenses}/LICENSE +0 -0
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -26,6 +26,131 @@ from ..exceptions import (
|
|
|
26
26
|
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
28
28
|
|
|
29
|
+
predef_ttcs: dict[str, dict] = {
|
|
30
|
+
'EasyAndUncertain':
|
|
31
|
+
{
|
|
32
|
+
'arguments': [0.5],
|
|
33
|
+
'name': 'Bernoulli',
|
|
34
|
+
'type': 'function'
|
|
35
|
+
},
|
|
36
|
+
'HardAndUncertain':
|
|
37
|
+
{
|
|
38
|
+
'lhs':
|
|
39
|
+
{
|
|
40
|
+
'arguments': [0.1],
|
|
41
|
+
'name': 'Exponential',
|
|
42
|
+
'type': 'function'
|
|
43
|
+
},
|
|
44
|
+
'rhs':
|
|
45
|
+
{
|
|
46
|
+
'arguments': [0.5],
|
|
47
|
+
'name': 'Bernoulli',
|
|
48
|
+
'type': 'function'
|
|
49
|
+
},
|
|
50
|
+
'type': 'multiplication'
|
|
51
|
+
},
|
|
52
|
+
'VeryHardAndUncertain':
|
|
53
|
+
{
|
|
54
|
+
'lhs':
|
|
55
|
+
{
|
|
56
|
+
'arguments': [0.01],
|
|
57
|
+
'name': 'Exponential',
|
|
58
|
+
'type': 'function'
|
|
59
|
+
},
|
|
60
|
+
'rhs':
|
|
61
|
+
{
|
|
62
|
+
'arguments': [0.5],
|
|
63
|
+
'name': 'Bernoulli',
|
|
64
|
+
'type': 'function'
|
|
65
|
+
},
|
|
66
|
+
'type': 'multiplication'
|
|
67
|
+
},
|
|
68
|
+
'EasyAndCertain':
|
|
69
|
+
{
|
|
70
|
+
'arguments': [1.0],
|
|
71
|
+
'name': 'Exponential',
|
|
72
|
+
'type': 'function'
|
|
73
|
+
},
|
|
74
|
+
'HardAndCertain':
|
|
75
|
+
{
|
|
76
|
+
'arguments': [0.1],
|
|
77
|
+
'name': 'Exponential',
|
|
78
|
+
'type': 'function'
|
|
79
|
+
},
|
|
80
|
+
'VeryHardAndCertain':
|
|
81
|
+
{
|
|
82
|
+
'arguments': [0.01],
|
|
83
|
+
'name': 'Exponential',
|
|
84
|
+
'type': 'function'
|
|
85
|
+
},
|
|
86
|
+
'Enabled':
|
|
87
|
+
{
|
|
88
|
+
'arguments': [1.0],
|
|
89
|
+
'name': 'Bernoulli',
|
|
90
|
+
'type': 'function'
|
|
91
|
+
},
|
|
92
|
+
'Instant':
|
|
93
|
+
{
|
|
94
|
+
'arguments': [1.0],
|
|
95
|
+
'name': 'Bernoulli',
|
|
96
|
+
'type': 'function'
|
|
97
|
+
},
|
|
98
|
+
'Disabled':
|
|
99
|
+
{
|
|
100
|
+
'arguments': [0.0],
|
|
101
|
+
'name': 'Bernoulli',
|
|
102
|
+
'type': 'function'
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def get_ttc_distribution(
|
|
107
|
+
step_dict: dict,
|
|
108
|
+
defense_default_ttc = None,
|
|
109
|
+
attack_default_ttc = None
|
|
110
|
+
) -> Optional[dict]:
|
|
111
|
+
"""Convert step TTC to a TTC distribution if needed
|
|
112
|
+
|
|
113
|
+
- If no TTC is set, set return default TTC.
|
|
114
|
+
- If the TTC provided is a predefined name replace it with the
|
|
115
|
+
probability distribution it corresponds to.
|
|
116
|
+
- Otherwise return the TTC distribution as is.
|
|
117
|
+
|
|
118
|
+
Arguments:
|
|
119
|
+
step_dict - A dict with the attack step data
|
|
120
|
+
defense_default_ttc - the value to give a defense ttc if none is set
|
|
121
|
+
attack_default_ttc - the value to give an attack ttc if none is set
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
A dict with the steps TTC distribution, or None if the step is not
|
|
125
|
+
a defense or attack step
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
if defense_default_ttc is None:
|
|
129
|
+
defense_default_ttc = predef_ttcs['Disabled'].copy()
|
|
130
|
+
if attack_default_ttc is None:
|
|
131
|
+
attack_default_ttc = predef_ttcs['Instant'].copy()
|
|
132
|
+
|
|
133
|
+
step_ttc = step_dict['ttc']
|
|
134
|
+
|
|
135
|
+
if step_dict['type'] == 'defense':
|
|
136
|
+
if step_ttc is None:
|
|
137
|
+
# No step ttc set in language for defense
|
|
138
|
+
step_ttc = defense_default_ttc
|
|
139
|
+
elif step_dict['type'] in ('or', 'and'):
|
|
140
|
+
if step_ttc is None:
|
|
141
|
+
# No step ttc set in language for attack
|
|
142
|
+
step_ttc = attack_default_ttc
|
|
143
|
+
else:
|
|
144
|
+
# No TTC for other step types
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
if 'name' in step_ttc and step_ttc['name'] in predef_ttcs:
|
|
148
|
+
# Predefined step ttc set in language, fetch from dict
|
|
149
|
+
step_ttc = predef_ttcs[step_ttc['name']].copy()
|
|
150
|
+
|
|
151
|
+
return step_ttc
|
|
152
|
+
|
|
153
|
+
|
|
29
154
|
|
|
30
155
|
def disaggregate_attack_step_full_name(
|
|
31
156
|
attack_step_full_name: str) -> list[str]:
|
|
@@ -49,6 +174,7 @@ class Detector:
|
|
|
49
174
|
|
|
50
175
|
|
|
51
176
|
class Context(dict):
|
|
177
|
+
"""Context is part of detectors to provide meta data about attackers"""
|
|
52
178
|
def __init__(self, context) -> None:
|
|
53
179
|
super().__init__(context)
|
|
54
180
|
self._context_dict = context
|
|
@@ -66,6 +192,7 @@ class Context(dict):
|
|
|
66
192
|
|
|
67
193
|
@dataclass
|
|
68
194
|
class LanguageGraphAsset:
|
|
195
|
+
"""An asset type as defined in the MAL language"""
|
|
69
196
|
name: str
|
|
70
197
|
own_associations: dict[str, LanguageGraphAssociation] = \
|
|
71
198
|
field(default_factory = dict)
|
|
@@ -127,7 +254,7 @@ class LanguageGraphAsset:
|
|
|
127
254
|
False otherwise.
|
|
128
255
|
"""
|
|
129
256
|
current_asset: Optional[LanguageGraphAsset] = self
|
|
130
|
-
while
|
|
257
|
+
while current_asset:
|
|
131
258
|
if current_asset == target_asset:
|
|
132
259
|
return True
|
|
133
260
|
current_asset = current_asset.own_super_asset
|
|
@@ -164,7 +291,7 @@ class LanguageGraphAsset:
|
|
|
164
291
|
"""
|
|
165
292
|
current_asset: Optional[LanguageGraphAsset] = self
|
|
166
293
|
superassets = []
|
|
167
|
-
while
|
|
294
|
+
while current_asset:
|
|
168
295
|
superassets.append(current_asset)
|
|
169
296
|
current_asset = current_asset.own_super_asset
|
|
170
297
|
return superassets
|
|
@@ -188,7 +315,9 @@ class LanguageGraphAsset:
|
|
|
188
315
|
|
|
189
316
|
|
|
190
317
|
@property
|
|
191
|
-
def variables(
|
|
318
|
+
def variables(
|
|
319
|
+
self
|
|
320
|
+
) -> dict[str, tuple[LanguageGraphAsset, ExpressionsChain]]:
|
|
192
321
|
"""
|
|
193
322
|
Return a list of all of the variables that belong to this asset
|
|
194
323
|
directly or indirectly via inheritance.
|
|
@@ -204,27 +333,6 @@ class LanguageGraphAsset:
|
|
|
204
333
|
return all_vars
|
|
205
334
|
|
|
206
335
|
|
|
207
|
-
def get_variable(
|
|
208
|
-
self,
|
|
209
|
-
var_name: str,
|
|
210
|
-
) -> Optional[tuple]:
|
|
211
|
-
"""
|
|
212
|
-
Return a variable matching the given name if the asset or any of its
|
|
213
|
-
super assets has its definition.
|
|
214
|
-
|
|
215
|
-
Return:
|
|
216
|
-
A tuple containing the target asset and expressions chain to it if the
|
|
217
|
-
variable was defined.
|
|
218
|
-
None otherwise.
|
|
219
|
-
"""
|
|
220
|
-
current_asset: Optional[LanguageGraphAsset] = self
|
|
221
|
-
while (current_asset):
|
|
222
|
-
if var_name in current_asset.own_variables:
|
|
223
|
-
return current_asset.own_variables[var_name]
|
|
224
|
-
current_asset = current_asset.own_super_asset
|
|
225
|
-
return None
|
|
226
|
-
|
|
227
|
-
|
|
228
336
|
def get_all_common_superassets(
|
|
229
337
|
self, other: LanguageGraphAsset
|
|
230
338
|
) -> set[str]:
|
|
@@ -241,6 +349,7 @@ class LanguageGraphAsset:
|
|
|
241
349
|
|
|
242
350
|
@dataclass
|
|
243
351
|
class LanguageGraphAssociationField:
|
|
352
|
+
"""A field in an association"""
|
|
244
353
|
asset: LanguageGraphAsset
|
|
245
354
|
fieldname: str
|
|
246
355
|
minimum: int
|
|
@@ -249,6 +358,9 @@ class LanguageGraphAssociationField:
|
|
|
249
358
|
|
|
250
359
|
@dataclass
|
|
251
360
|
class LanguageGraphAssociation:
|
|
361
|
+
"""
|
|
362
|
+
An association type between asset types as defined in the MAL language
|
|
363
|
+
"""
|
|
252
364
|
name: str
|
|
253
365
|
left_field: LanguageGraphAssociationField
|
|
254
366
|
right_field: LanguageGraphAssociationField
|
|
@@ -277,9 +389,11 @@ class LanguageGraphAssociation:
|
|
|
277
389
|
|
|
278
390
|
|
|
279
391
|
def __repr__(self) -> str:
|
|
280
|
-
return (
|
|
392
|
+
return (
|
|
393
|
+
f'LanguageGraphAssociation(name: "{self.name}", '
|
|
281
394
|
f'left_field: {self.left_field}, '
|
|
282
|
-
f'right_field: {self.right_field})'
|
|
395
|
+
f'right_field: {self.right_field})'
|
|
396
|
+
)
|
|
283
397
|
|
|
284
398
|
|
|
285
399
|
@property
|
|
@@ -293,7 +407,7 @@ class LanguageGraphAssociation:
|
|
|
293
407
|
self.name,\
|
|
294
408
|
self.left_field.fieldname,\
|
|
295
409
|
self.right_field.fieldname
|
|
296
|
-
|
|
410
|
+
)
|
|
297
411
|
return full_name
|
|
298
412
|
|
|
299
413
|
|
|
@@ -363,17 +477,20 @@ class LanguageGraphAssociation:
|
|
|
363
477
|
|
|
364
478
|
@dataclass
|
|
365
479
|
class LanguageGraphAttackStep:
|
|
480
|
+
"""
|
|
481
|
+
An attack step belonging to an asset type in the MAL language
|
|
482
|
+
"""
|
|
366
483
|
name: str
|
|
367
484
|
type: str
|
|
368
485
|
asset: LanguageGraphAsset
|
|
369
|
-
ttc: dict = field(default_factory = dict)
|
|
486
|
+
ttc: Optional[dict] = field(default_factory = dict)
|
|
370
487
|
overrides: bool = False
|
|
371
488
|
children: dict = field(default_factory = dict)
|
|
372
489
|
parents: dict = field(default_factory = dict)
|
|
373
490
|
info: dict = field(default_factory = dict)
|
|
374
491
|
inherits: Optional[LanguageGraphAttackStep] = None
|
|
492
|
+
own_requires: list[ExpressionsChain] = field(default_factory=list)
|
|
375
493
|
tags: set = field(default_factory = set)
|
|
376
|
-
_attributes: Optional[dict] = None
|
|
377
494
|
detectors: dict = field(default_factory = lambda: {})
|
|
378
495
|
|
|
379
496
|
|
|
@@ -450,6 +567,10 @@ class LanguageGraphAttackStep:
|
|
|
450
567
|
|
|
451
568
|
|
|
452
569
|
class ExpressionsChain:
|
|
570
|
+
"""
|
|
571
|
+
A series of linked step expressions that specify the association path and
|
|
572
|
+
operations to take to reach the child/parent attack step.
|
|
573
|
+
"""
|
|
453
574
|
def __init__(self,
|
|
454
575
|
type: str,
|
|
455
576
|
left_link: Optional[ExpressionsChain] = None,
|
|
@@ -561,7 +682,6 @@ class ExpressionsChain:
|
|
|
561
682
|
msg = 'Missing expressions chain type!'
|
|
562
683
|
logger.error(msg)
|
|
563
684
|
raise LanguageGraphAssociationError(msg)
|
|
564
|
-
return None
|
|
565
685
|
|
|
566
686
|
expr_chain_type = serialized_expr_chain['type']
|
|
567
687
|
match (expr_chain_type):
|
|
@@ -597,9 +717,10 @@ class ExpressionsChain:
|
|
|
597
717
|
if association is None:
|
|
598
718
|
msg = 'Failed to find association "%s" with '\
|
|
599
719
|
'fieldname "%s"'
|
|
600
|
-
logger.error(msg
|
|
601
|
-
raise LanguageGraphException(
|
|
602
|
-
fieldname)
|
|
720
|
+
logger.error(msg, assoc_name, fieldname)
|
|
721
|
+
raise LanguageGraphException(
|
|
722
|
+
msg % (assoc_name, fieldname)
|
|
723
|
+
)
|
|
603
724
|
|
|
604
725
|
new_expr_chain = ExpressionsChain(
|
|
605
726
|
type = 'field',
|
|
@@ -629,7 +750,7 @@ class ExpressionsChain:
|
|
|
629
750
|
subtype_asset = lang_graph.assets[subtype_name]
|
|
630
751
|
else:
|
|
631
752
|
msg = 'Failed to find subtype %s'
|
|
632
|
-
logger.error(msg
|
|
753
|
+
logger.error(msg, subtype_name)
|
|
633
754
|
raise LanguageGraphException(msg % subtype_name)
|
|
634
755
|
|
|
635
756
|
new_expr_chain = ExpressionsChain(
|
|
@@ -642,8 +763,9 @@ class ExpressionsChain:
|
|
|
642
763
|
case _:
|
|
643
764
|
msg = 'Unknown expressions chain type %s!'
|
|
644
765
|
logger.error(msg, serialized_expr_chain['type'])
|
|
645
|
-
raise LanguageGraphAssociationError(
|
|
646
|
-
serialized_expr_chain['type']
|
|
766
|
+
raise LanguageGraphAssociationError(
|
|
767
|
+
msg % serialized_expr_chain['type']
|
|
768
|
+
)
|
|
647
769
|
|
|
648
770
|
|
|
649
771
|
def __repr__(self) -> str:
|
|
@@ -653,7 +775,7 @@ class ExpressionsChain:
|
|
|
653
775
|
class LanguageGraph():
|
|
654
776
|
"""Graph representation of a MAL language"""
|
|
655
777
|
def __init__(self, lang: Optional[dict] = None):
|
|
656
|
-
self.assets: dict = {}
|
|
778
|
+
self.assets: dict[str, LanguageGraphAsset] = {}
|
|
657
779
|
if lang is not None:
|
|
658
780
|
self._lang_spec: dict = lang
|
|
659
781
|
self.metadata = {
|
|
@@ -694,7 +816,6 @@ class LanguageGraph():
|
|
|
694
816
|
langspec = archive.read('langspec.json')
|
|
695
817
|
return LanguageGraph(json.loads(langspec))
|
|
696
818
|
|
|
697
|
-
|
|
698
819
|
def _to_dict(self):
|
|
699
820
|
"""Converts LanguageGraph into a dict"""
|
|
700
821
|
|
|
@@ -708,10 +829,12 @@ class LanguageGraph():
|
|
|
708
829
|
|
|
709
830
|
return serialized_graph
|
|
710
831
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
832
|
+
@staticmethod
|
|
833
|
+
def _link_association_to_assets(
|
|
834
|
+
assoc: LanguageGraphAssociation,
|
|
835
|
+
left_asset: LanguageGraphAsset,
|
|
836
|
+
right_asset: LanguageGraphAsset
|
|
837
|
+
):
|
|
715
838
|
left_asset.own_associations[assoc.right_field.fieldname] = assoc
|
|
716
839
|
right_asset.own_associations[assoc.left_field.fieldname] = assoc
|
|
717
840
|
|
|
@@ -840,7 +963,7 @@ class LanguageGraph():
|
|
|
840
963
|
name = attack_step_dict['name'],
|
|
841
964
|
type = attack_step_dict['type'],
|
|
842
965
|
asset = asset,
|
|
843
|
-
ttc = attack_step_dict
|
|
966
|
+
ttc = get_ttc_distribution(attack_step_dict),
|
|
844
967
|
overrides = attack_step_dict['overrides'],
|
|
845
968
|
children = {},
|
|
846
969
|
parents = {},
|
|
@@ -925,7 +1048,8 @@ class LanguageGraph():
|
|
|
925
1048
|
expr_chain_dict,
|
|
926
1049
|
lang_graph
|
|
927
1050
|
)
|
|
928
|
-
|
|
1051
|
+
if expr_chain:
|
|
1052
|
+
attack_step.own_requires.append(expr_chain)
|
|
929
1053
|
|
|
930
1054
|
return lang_graph
|
|
931
1055
|
|
|
@@ -955,7 +1079,6 @@ class LanguageGraph():
|
|
|
955
1079
|
)
|
|
956
1080
|
|
|
957
1081
|
|
|
958
|
-
|
|
959
1082
|
def save_language_specification_to_json(self, filename: str) -> None:
|
|
960
1083
|
"""
|
|
961
1084
|
Save a MAL language specification dictionary to a JSON file
|
|
@@ -968,12 +1091,290 @@ class LanguageGraph():
|
|
|
968
1091
|
with open(filename, 'w', encoding='utf-8') as file:
|
|
969
1092
|
json.dump(self._lang_spec, file, indent=4)
|
|
970
1093
|
|
|
1094
|
+
def process_attack_step_expression(
|
|
1095
|
+
self,
|
|
1096
|
+
target_asset: LanguageGraphAsset,
|
|
1097
|
+
step_expression: dict[str, Any]
|
|
1098
|
+
) -> tuple[
|
|
1099
|
+
LanguageGraphAsset,
|
|
1100
|
+
None,
|
|
1101
|
+
str
|
|
1102
|
+
]:
|
|
1103
|
+
"""
|
|
1104
|
+
The attack step expression just adds the name of the attack
|
|
1105
|
+
step. All other step expressions only modify the target
|
|
1106
|
+
asset and parent associations chain.
|
|
1107
|
+
"""
|
|
1108
|
+
return (
|
|
1109
|
+
target_asset,
|
|
1110
|
+
None,
|
|
1111
|
+
step_expression['name']
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
def process_set_operation_step_expression(
|
|
1115
|
+
self,
|
|
1116
|
+
target_asset: LanguageGraphAsset,
|
|
1117
|
+
expr_chain: Optional[ExpressionsChain],
|
|
1118
|
+
step_expression: dict[str, Any]
|
|
1119
|
+
) -> tuple[
|
|
1120
|
+
LanguageGraphAsset,
|
|
1121
|
+
ExpressionsChain,
|
|
1122
|
+
None
|
|
1123
|
+
]:
|
|
1124
|
+
"""
|
|
1125
|
+
The set operators are used to combine the left hand and right
|
|
1126
|
+
hand targets accordingly.
|
|
1127
|
+
"""
|
|
971
1128
|
|
|
972
|
-
|
|
1129
|
+
lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
|
|
973
1130
|
target_asset,
|
|
974
1131
|
expr_chain,
|
|
1132
|
+
step_expression['lhs']
|
|
1133
|
+
)
|
|
1134
|
+
rh_target_asset, rh_expr_chain, _ = self.process_step_expression(
|
|
1135
|
+
target_asset,
|
|
1136
|
+
expr_chain,
|
|
1137
|
+
step_expression['rhs']
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
assert lh_target_asset, (
|
|
1141
|
+
f"No lh target in step expression {step_expression}"
|
|
1142
|
+
)
|
|
1143
|
+
assert rh_target_asset, (
|
|
1144
|
+
f"No rh target in step expression {step_expression}"
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
if not lh_target_asset.get_all_common_superassets(rh_target_asset):
|
|
1148
|
+
raise ValueError(
|
|
1149
|
+
"Set operation attempted between targets that do not share "
|
|
1150
|
+
f"any common superassets: {lh_target_asset.name} "
|
|
1151
|
+
f"and {rh_target_asset.name}!"
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
new_expr_chain = ExpressionsChain(
|
|
1155
|
+
type = step_expression['type'],
|
|
1156
|
+
left_link = lh_expr_chain,
|
|
1157
|
+
right_link = rh_expr_chain
|
|
1158
|
+
)
|
|
1159
|
+
return (
|
|
1160
|
+
lh_target_asset,
|
|
1161
|
+
new_expr_chain,
|
|
1162
|
+
None
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
def process_variable_step_expression(
|
|
1166
|
+
self,
|
|
1167
|
+
target_asset: LanguageGraphAsset,
|
|
1168
|
+
step_expression: dict[str, Any]
|
|
1169
|
+
) -> tuple[
|
|
1170
|
+
LanguageGraphAsset,
|
|
1171
|
+
ExpressionsChain,
|
|
1172
|
+
None
|
|
1173
|
+
]:
|
|
1174
|
+
|
|
1175
|
+
var_name = step_expression['name']
|
|
1176
|
+
var_target_asset, var_expr_chain = (
|
|
1177
|
+
self._resolve_variable(target_asset, var_name)
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
if var_expr_chain is None:
|
|
1181
|
+
raise LookupError(
|
|
1182
|
+
f'Failed to find variable "{step_expression["name"]}" '
|
|
1183
|
+
f'for {target_asset.name}',
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
return (
|
|
1187
|
+
var_target_asset,
|
|
1188
|
+
var_expr_chain,
|
|
1189
|
+
None
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
def process_field_step_expression(
|
|
1193
|
+
self,
|
|
1194
|
+
target_asset: LanguageGraphAsset,
|
|
1195
|
+
step_expression: dict[str, Any]
|
|
1196
|
+
) -> tuple[
|
|
1197
|
+
LanguageGraphAsset,
|
|
1198
|
+
ExpressionsChain,
|
|
1199
|
+
None
|
|
1200
|
+
]:
|
|
1201
|
+
"""
|
|
1202
|
+
Change the target asset from the current one to the associated
|
|
1203
|
+
asset given the specified field name and add the parent
|
|
1204
|
+
fieldname and association to the parent associations chain.
|
|
1205
|
+
"""
|
|
1206
|
+
|
|
1207
|
+
fieldname = step_expression['name']
|
|
1208
|
+
|
|
1209
|
+
if target_asset is None:
|
|
1210
|
+
raise ValueError(
|
|
1211
|
+
f'Missing target asset for field "{fieldname}"!'
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
new_target_asset = None
|
|
1215
|
+
for association in target_asset.associations.values():
|
|
1216
|
+
if (association.left_field.fieldname == fieldname and \
|
|
1217
|
+
target_asset.is_subasset_of(
|
|
1218
|
+
association.right_field.asset)):
|
|
1219
|
+
new_target_asset = association.left_field.asset
|
|
1220
|
+
|
|
1221
|
+
if (association.right_field.fieldname == fieldname and \
|
|
1222
|
+
target_asset.is_subasset_of(
|
|
1223
|
+
association.left_field.asset)):
|
|
1224
|
+
new_target_asset = association.right_field.asset
|
|
1225
|
+
|
|
1226
|
+
if new_target_asset:
|
|
1227
|
+
new_expr_chain = ExpressionsChain(
|
|
1228
|
+
type = 'field',
|
|
1229
|
+
fieldname = fieldname,
|
|
1230
|
+
association = association
|
|
1231
|
+
)
|
|
1232
|
+
return (
|
|
1233
|
+
new_target_asset,
|
|
1234
|
+
new_expr_chain,
|
|
1235
|
+
None
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
raise LookupError(
|
|
1239
|
+
f'Failed to find field {fieldname} on asset {target_asset.name}!',
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
def process_transitive_step_expression(
|
|
1243
|
+
self,
|
|
1244
|
+
target_asset: LanguageGraphAsset,
|
|
1245
|
+
expr_chain: Optional[ExpressionsChain],
|
|
1246
|
+
step_expression: dict[str, Any]
|
|
1247
|
+
) -> tuple[
|
|
1248
|
+
LanguageGraphAsset,
|
|
1249
|
+
ExpressionsChain,
|
|
1250
|
+
None
|
|
1251
|
+
]:
|
|
1252
|
+
"""
|
|
1253
|
+
Create a transitive tuple entry that applies to the next
|
|
1254
|
+
component of the step expression.
|
|
1255
|
+
"""
|
|
1256
|
+
result_target_asset, result_expr_chain, _ = (
|
|
1257
|
+
self.process_step_expression(
|
|
1258
|
+
target_asset,
|
|
1259
|
+
expr_chain,
|
|
1260
|
+
step_expression['stepExpression']
|
|
1261
|
+
)
|
|
1262
|
+
)
|
|
1263
|
+
new_expr_chain = ExpressionsChain(
|
|
1264
|
+
type = 'transitive',
|
|
1265
|
+
sub_link = result_expr_chain
|
|
1266
|
+
)
|
|
1267
|
+
return (
|
|
1268
|
+
result_target_asset,
|
|
1269
|
+
new_expr_chain,
|
|
1270
|
+
None
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
def process_subType_step_expression(
|
|
1274
|
+
self,
|
|
1275
|
+
target_asset: LanguageGraphAsset,
|
|
1276
|
+
expr_chain: Optional[ExpressionsChain],
|
|
1277
|
+
step_expression: dict[str, Any]
|
|
1278
|
+
) -> tuple[
|
|
1279
|
+
LanguageGraphAsset,
|
|
1280
|
+
ExpressionsChain,
|
|
1281
|
+
None
|
|
1282
|
+
]:
|
|
1283
|
+
"""
|
|
1284
|
+
Create a subType tuple entry that applies to the next
|
|
1285
|
+
component of the step expression and changes the target
|
|
1286
|
+
asset to the subasset.
|
|
1287
|
+
"""
|
|
1288
|
+
|
|
1289
|
+
subtype_name = step_expression['subType']
|
|
1290
|
+
result_target_asset, result_expr_chain, _ = (
|
|
1291
|
+
self.process_step_expression(
|
|
1292
|
+
target_asset,
|
|
1293
|
+
expr_chain,
|
|
1294
|
+
step_expression['stepExpression']
|
|
1295
|
+
)
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
if subtype_name not in self.assets:
|
|
1299
|
+
raise LanguageGraphException(
|
|
1300
|
+
f'Failed to find subtype {subtype_name}'
|
|
1301
|
+
)
|
|
1302
|
+
|
|
1303
|
+
subtype_asset = self.assets[subtype_name]
|
|
1304
|
+
|
|
1305
|
+
if result_target_asset is None:
|
|
1306
|
+
raise LookupError("Nonexisting asset for subtype")
|
|
1307
|
+
|
|
1308
|
+
if not subtype_asset.is_subasset_of(result_target_asset):
|
|
1309
|
+
raise ValueError(
|
|
1310
|
+
f'Found subtype {subtype_name} which does not extend '
|
|
1311
|
+
f'{result_target_asset.name}, subtype cannot be resolved.'
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
new_expr_chain = ExpressionsChain(
|
|
1315
|
+
type = 'subType',
|
|
1316
|
+
sub_link = result_expr_chain,
|
|
1317
|
+
subtype = subtype_asset
|
|
1318
|
+
)
|
|
1319
|
+
return (
|
|
1320
|
+
subtype_asset,
|
|
1321
|
+
new_expr_chain,
|
|
1322
|
+
None
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1325
|
+
def process_collect_step_expression(
|
|
1326
|
+
self,
|
|
1327
|
+
target_asset: LanguageGraphAsset,
|
|
1328
|
+
expr_chain: Optional[ExpressionsChain],
|
|
1329
|
+
step_expression: dict[str, Any]
|
|
1330
|
+
) -> tuple[
|
|
1331
|
+
LanguageGraphAsset,
|
|
1332
|
+
Optional[ExpressionsChain],
|
|
1333
|
+
Optional[str]
|
|
1334
|
+
]:
|
|
1335
|
+
"""
|
|
1336
|
+
Apply the right hand step expression to left hand step
|
|
1337
|
+
expression target asset and parent associations chain.
|
|
1338
|
+
"""
|
|
1339
|
+
lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
|
|
1340
|
+
target_asset, expr_chain, step_expression['lhs']
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
if lh_target_asset is None:
|
|
1344
|
+
raise ValueError(
|
|
1345
|
+
'No left hand asset in collect expression '
|
|
1346
|
+
f'{step_expression["lhs"]}'
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1349
|
+
rh_target_asset, rh_expr_chain, rh_attack_step_name = (
|
|
1350
|
+
self.process_step_expression(
|
|
1351
|
+
lh_target_asset, None, step_expression['rhs']
|
|
1352
|
+
)
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
new_expr_chain = lh_expr_chain
|
|
1356
|
+
if rh_expr_chain:
|
|
1357
|
+
new_expr_chain = ExpressionsChain(
|
|
1358
|
+
type = 'collect',
|
|
1359
|
+
left_link = lh_expr_chain,
|
|
1360
|
+
right_link = rh_expr_chain
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
return (
|
|
1364
|
+
rh_target_asset,
|
|
1365
|
+
new_expr_chain,
|
|
1366
|
+
rh_attack_step_name
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
def process_step_expression(self,
|
|
1370
|
+
target_asset: LanguageGraphAsset,
|
|
1371
|
+
expr_chain: Optional[ExpressionsChain],
|
|
975
1372
|
step_expression: dict
|
|
976
|
-
) -> tuple
|
|
1373
|
+
) -> tuple[
|
|
1374
|
+
LanguageGraphAsset,
|
|
1375
|
+
Optional[ExpressionsChain],
|
|
1376
|
+
Optional[str]
|
|
1377
|
+
]:
|
|
977
1378
|
"""
|
|
978
1379
|
Recursively process an attack step expression.
|
|
979
1380
|
|
|
@@ -1002,210 +1403,46 @@ class LanguageGraph():
|
|
|
1002
1403
|
json.dumps(step_expression, indent = 2)
|
|
1003
1404
|
)
|
|
1004
1405
|
|
|
1406
|
+
result: tuple[
|
|
1407
|
+
LanguageGraphAsset,
|
|
1408
|
+
Optional[ExpressionsChain],
|
|
1409
|
+
Optional[str]
|
|
1410
|
+
]
|
|
1411
|
+
|
|
1005
1412
|
match (step_expression['type']):
|
|
1006
1413
|
case 'attackStep':
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
# asset and parent associations chain.
|
|
1010
|
-
return (
|
|
1011
|
-
target_asset,
|
|
1012
|
-
None,
|
|
1013
|
-
step_expression['name']
|
|
1414
|
+
result = self.process_attack_step_expression(
|
|
1415
|
+
target_asset, step_expression
|
|
1014
1416
|
)
|
|
1015
|
-
|
|
1016
1417
|
case 'union' | 'intersection' | 'difference':
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
|
|
1020
|
-
target_asset,
|
|
1021
|
-
expr_chain,
|
|
1022
|
-
step_expression['lhs']
|
|
1023
|
-
)
|
|
1024
|
-
rh_target_asset, rh_expr_chain, _ = \
|
|
1025
|
-
self.process_step_expression(
|
|
1026
|
-
target_asset,
|
|
1027
|
-
expr_chain,
|
|
1028
|
-
step_expression['rhs']
|
|
1029
|
-
)
|
|
1030
|
-
|
|
1031
|
-
if not lh_target_asset.get_all_common_superassets(
|
|
1032
|
-
rh_target_asset):
|
|
1033
|
-
logger.error(
|
|
1034
|
-
"Set operation attempted between targets that"
|
|
1035
|
-
" do not share any common superassets: %s and %s!",
|
|
1036
|
-
lh_target_asset.name, rh_target_asset.name
|
|
1037
|
-
)
|
|
1038
|
-
return (None, None, None)
|
|
1039
|
-
|
|
1040
|
-
new_expr_chain = ExpressionsChain(
|
|
1041
|
-
type = step_expression['type'],
|
|
1042
|
-
left_link = lh_expr_chain,
|
|
1043
|
-
right_link = rh_expr_chain
|
|
1044
|
-
)
|
|
1045
|
-
return (
|
|
1046
|
-
lh_target_asset,
|
|
1047
|
-
new_expr_chain,
|
|
1048
|
-
None
|
|
1418
|
+
result = self.process_set_operation_step_expression(
|
|
1419
|
+
target_asset, expr_chain, step_expression
|
|
1049
1420
|
)
|
|
1050
|
-
|
|
1051
1421
|
case 'variable':
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
var_target_asset, var_expr_chain = \
|
|
1056
|
-
target_asset.get_variable(var_name)
|
|
1057
|
-
if var_expr_chain is not None:
|
|
1058
|
-
return (
|
|
1059
|
-
var_target_asset,
|
|
1060
|
-
var_expr_chain,
|
|
1061
|
-
None
|
|
1062
|
-
)
|
|
1063
|
-
else:
|
|
1064
|
-
logger.error(
|
|
1065
|
-
'Failed to find variable \"%s\" for %s',
|
|
1066
|
-
step_expression["name"], target_asset.name
|
|
1067
|
-
)
|
|
1068
|
-
return (None, None, None)
|
|
1069
|
-
|
|
1422
|
+
result = self.process_variable_step_expression(
|
|
1423
|
+
target_asset, step_expression
|
|
1424
|
+
)
|
|
1070
1425
|
case 'field':
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
# fieldname and association to the parent associations chain.
|
|
1074
|
-
fieldname = step_expression['name']
|
|
1075
|
-
if not target_asset:
|
|
1076
|
-
logger.error(
|
|
1077
|
-
'Missing target asset for field "%s"!', fieldname
|
|
1078
|
-
)
|
|
1079
|
-
return (None, None, None)
|
|
1080
|
-
|
|
1081
|
-
new_target_asset = None
|
|
1082
|
-
for association in target_asset.associations.values():
|
|
1083
|
-
if (association.left_field.fieldname == fieldname and \
|
|
1084
|
-
target_asset.is_subasset_of(
|
|
1085
|
-
association.right_field.asset)):
|
|
1086
|
-
new_target_asset = association.left_field.asset
|
|
1087
|
-
|
|
1088
|
-
if (association.right_field.fieldname == fieldname and \
|
|
1089
|
-
target_asset.is_subasset_of(
|
|
1090
|
-
association.left_field.asset)):
|
|
1091
|
-
new_target_asset = association.right_field.asset
|
|
1092
|
-
|
|
1093
|
-
if new_target_asset:
|
|
1094
|
-
new_expr_chain = ExpressionsChain(
|
|
1095
|
-
type = 'field',
|
|
1096
|
-
fieldname = fieldname,
|
|
1097
|
-
association = association
|
|
1098
|
-
)
|
|
1099
|
-
return (
|
|
1100
|
-
new_target_asset,
|
|
1101
|
-
new_expr_chain,
|
|
1102
|
-
None
|
|
1103
|
-
)
|
|
1104
|
-
logger.error(
|
|
1105
|
-
'Failed to find field "%s" on asset "%s"!',
|
|
1106
|
-
fieldname, target_asset.name
|
|
1426
|
+
result = self.process_field_step_expression(
|
|
1427
|
+
target_asset, step_expression
|
|
1107
1428
|
)
|
|
1108
|
-
return (None, None, None)
|
|
1109
|
-
|
|
1110
1429
|
case 'transitive':
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
result_target_asset, \
|
|
1114
|
-
result_expr_chain, \
|
|
1115
|
-
attack_step = \
|
|
1116
|
-
self.process_step_expression(
|
|
1117
|
-
target_asset,
|
|
1118
|
-
expr_chain,
|
|
1119
|
-
step_expression['stepExpression']
|
|
1120
|
-
)
|
|
1121
|
-
new_expr_chain = ExpressionsChain(
|
|
1122
|
-
type = 'transitive',
|
|
1123
|
-
sub_link = result_expr_chain
|
|
1430
|
+
result = self.process_transitive_step_expression(
|
|
1431
|
+
target_asset, expr_chain, step_expression
|
|
1124
1432
|
)
|
|
1125
|
-
return (
|
|
1126
|
-
result_target_asset,
|
|
1127
|
-
new_expr_chain,
|
|
1128
|
-
attack_step
|
|
1129
|
-
)
|
|
1130
|
-
|
|
1131
1433
|
case 'subType':
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
# asset to the subasset.
|
|
1135
|
-
subtype_name = step_expression['subType']
|
|
1136
|
-
result_target_asset, \
|
|
1137
|
-
result_expr_chain, \
|
|
1138
|
-
attack_step = \
|
|
1139
|
-
self.process_step_expression(
|
|
1140
|
-
target_asset,
|
|
1141
|
-
expr_chain,
|
|
1142
|
-
step_expression['stepExpression']
|
|
1143
|
-
)
|
|
1144
|
-
|
|
1145
|
-
if subtype_name in self.assets:
|
|
1146
|
-
subtype_asset = self.assets[subtype_name]
|
|
1147
|
-
else:
|
|
1148
|
-
msg = 'Failed to find subtype %s'
|
|
1149
|
-
logger.error(msg % subtype_name)
|
|
1150
|
-
raise LanguageGraphException(msg % subtype_name)
|
|
1151
|
-
|
|
1152
|
-
if not subtype_asset.is_subasset_of(result_target_asset):
|
|
1153
|
-
logger.error(
|
|
1154
|
-
'Found subtype "%s" which does not extend "%s", '
|
|
1155
|
-
'therefore the subtype cannot be resolved.',
|
|
1156
|
-
subtype_name, result_target_asset.name
|
|
1157
|
-
)
|
|
1158
|
-
return (None, None, None)
|
|
1159
|
-
|
|
1160
|
-
new_expr_chain = ExpressionsChain(
|
|
1161
|
-
type = 'subType',
|
|
1162
|
-
sub_link = result_expr_chain,
|
|
1163
|
-
subtype = subtype_asset
|
|
1164
|
-
)
|
|
1165
|
-
return (
|
|
1166
|
-
subtype_asset,
|
|
1167
|
-
new_expr_chain,
|
|
1168
|
-
attack_step
|
|
1434
|
+
result = self.process_subType_step_expression(
|
|
1435
|
+
target_asset, expr_chain, step_expression
|
|
1169
1436
|
)
|
|
1170
|
-
|
|
1171
1437
|
case 'collect':
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
(lh_target_asset, lh_expr_chain, _) = \
|
|
1175
|
-
self.process_step_expression(
|
|
1176
|
-
target_asset,
|
|
1177
|
-
expr_chain,
|
|
1178
|
-
step_expression['lhs']
|
|
1179
|
-
)
|
|
1180
|
-
(rh_target_asset,
|
|
1181
|
-
rh_expr_chain,
|
|
1182
|
-
rh_attack_step_name) = \
|
|
1183
|
-
self.process_step_expression(
|
|
1184
|
-
lh_target_asset,
|
|
1185
|
-
None,
|
|
1186
|
-
step_expression['rhs']
|
|
1187
|
-
)
|
|
1188
|
-
if rh_expr_chain:
|
|
1189
|
-
new_expr_chain = ExpressionsChain(
|
|
1190
|
-
type = 'collect',
|
|
1191
|
-
left_link = lh_expr_chain,
|
|
1192
|
-
right_link = rh_expr_chain
|
|
1193
|
-
)
|
|
1194
|
-
else:
|
|
1195
|
-
new_expr_chain = lh_expr_chain
|
|
1196
|
-
|
|
1197
|
-
return (
|
|
1198
|
-
rh_target_asset,
|
|
1199
|
-
new_expr_chain,
|
|
1200
|
-
rh_attack_step_name
|
|
1438
|
+
result = self.process_collect_step_expression(
|
|
1439
|
+
target_asset, expr_chain, step_expression
|
|
1201
1440
|
)
|
|
1202
|
-
|
|
1203
1441
|
case _:
|
|
1204
|
-
|
|
1205
|
-
'Unknown attack step type: "
|
|
1442
|
+
raise LookupError(
|
|
1443
|
+
f'Unknown attack step type: "{step_expression["type"]}"'
|
|
1206
1444
|
)
|
|
1207
|
-
|
|
1208
|
-
|
|
1445
|
+
return result
|
|
1209
1446
|
|
|
1210
1447
|
def reverse_expr_chain(
|
|
1211
1448
|
self,
|
|
@@ -1300,7 +1537,7 @@ class LanguageGraph():
|
|
|
1300
1537
|
logger.error(msg, expr_chain.type)
|
|
1301
1538
|
raise LanguageGraphAssociationError(msg % expr_chain.type)
|
|
1302
1539
|
|
|
1303
|
-
def _resolve_variable(self, asset, var_name) -> tuple:
|
|
1540
|
+
def _resolve_variable(self, asset: LanguageGraphAsset, var_name) -> tuple:
|
|
1304
1541
|
"""
|
|
1305
1542
|
Resolve a variable for a specific asset by variable name.
|
|
1306
1543
|
|
|
@@ -1323,35 +1560,75 @@ class LanguageGraph():
|
|
|
1323
1560
|
return (target_asset, expr_chain)
|
|
1324
1561
|
return asset.variables[var_name]
|
|
1325
1562
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1563
|
+
def _create_associations_for_assets(
|
|
1564
|
+
self,
|
|
1565
|
+
lang_spec: dict[str, Any],
|
|
1566
|
+
assets: dict[str, LanguageGraphAsset]
|
|
1567
|
+
) -> None:
|
|
1568
|
+
""" Link associations to assets based on the language specification.
|
|
1569
|
+
Arguments:
|
|
1570
|
+
lang_spec - the language specification dictionary
|
|
1571
|
+
assets - a dictionary of LanguageGraphAsset objects
|
|
1572
|
+
indexed by their names
|
|
1331
1573
|
"""
|
|
1332
|
-
|
|
1333
|
-
for
|
|
1574
|
+
|
|
1575
|
+
for association_dict in lang_spec['associations']:
|
|
1334
1576
|
logger.debug(
|
|
1335
|
-
'Create
|
|
1336
|
-
|
|
1577
|
+
'Create association language graph nodes for association %s',
|
|
1578
|
+
association_dict['name']
|
|
1337
1579
|
)
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1580
|
+
|
|
1581
|
+
left_asset_name = association_dict['leftAsset']
|
|
1582
|
+
right_asset_name = association_dict['rightAsset']
|
|
1583
|
+
|
|
1584
|
+
if left_asset_name not in assets:
|
|
1585
|
+
raise LanguageGraphAssociationError(
|
|
1586
|
+
f'Left asset "{left_asset_name}" for '
|
|
1587
|
+
f'association "{association_dict["name"]}" not found!'
|
|
1588
|
+
)
|
|
1589
|
+
if right_asset_name not in assets:
|
|
1590
|
+
raise LanguageGraphAssociationError(
|
|
1591
|
+
f'Right asset "{right_asset_name}" for '
|
|
1592
|
+
f'association "{association_dict["name"]}" not found!'
|
|
1593
|
+
)
|
|
1594
|
+
|
|
1595
|
+
left_asset = assets[left_asset_name]
|
|
1596
|
+
right_asset = assets[right_asset_name]
|
|
1597
|
+
|
|
1598
|
+
assoc_node = LanguageGraphAssociation(
|
|
1599
|
+
name = association_dict['name'],
|
|
1600
|
+
left_field = LanguageGraphAssociationField(
|
|
1601
|
+
left_asset,
|
|
1602
|
+
association_dict['leftField'],
|
|
1603
|
+
association_dict['leftMultiplicity']['min'],
|
|
1604
|
+
association_dict['leftMultiplicity']['max']
|
|
1605
|
+
),
|
|
1606
|
+
right_field = LanguageGraphAssociationField(
|
|
1607
|
+
right_asset,
|
|
1608
|
+
association_dict['rightField'],
|
|
1609
|
+
association_dict['rightMultiplicity']['min'],
|
|
1610
|
+
association_dict['rightMultiplicity']['max']
|
|
1611
|
+
),
|
|
1612
|
+
info = association_dict['meta']
|
|
1347
1613
|
)
|
|
1348
|
-
self.assets[asset_dict['name']] = asset_node
|
|
1349
1614
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1615
|
+
# Add the association to the left and right asset
|
|
1616
|
+
self._link_association_to_assets(
|
|
1617
|
+
assoc_node, left_asset, right_asset
|
|
1618
|
+
)
|
|
1619
|
+
|
|
1620
|
+
def _link_assets(
|
|
1621
|
+
self,
|
|
1622
|
+
lang_spec: dict[str, Any],
|
|
1623
|
+
assets: dict[str, LanguageGraphAsset]
|
|
1624
|
+
) -> None:
|
|
1625
|
+
"""
|
|
1626
|
+
Link assets based on inheritance and associations.
|
|
1627
|
+
"""
|
|
1628
|
+
for asset_dict in lang_spec['assets']:
|
|
1629
|
+
asset = assets[asset_dict['name']]
|
|
1353
1630
|
if asset_dict['superAsset']:
|
|
1354
|
-
super_asset =
|
|
1631
|
+
super_asset = assets[asset_dict['superAsset']]
|
|
1355
1632
|
if not super_asset:
|
|
1356
1633
|
msg = 'Failed to find super asset "%s" for asset "%s"!'
|
|
1357
1634
|
logger.error(
|
|
@@ -1362,54 +1639,21 @@ class LanguageGraph():
|
|
|
1362
1639
|
super_asset.own_sub_assets.add(asset)
|
|
1363
1640
|
asset.own_super_asset = super_asset
|
|
1364
1641
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1642
|
+
def _set_variables_for_assets(
|
|
1643
|
+
self, assets: dict[str, LanguageGraphAsset]
|
|
1644
|
+
) -> None:
|
|
1645
|
+
""" Set the variables for each asset based on the language specification.
|
|
1646
|
+
Arguments:
|
|
1647
|
+
assets - a dictionary of LanguageGraphAsset objects
|
|
1648
|
+
indexed by their names
|
|
1649
|
+
"""
|
|
1650
|
+
|
|
1651
|
+
for asset in assets.values():
|
|
1367
1652
|
logger.debug(
|
|
1368
|
-
'
|
|
1369
|
-
asset.name
|
|
1653
|
+
'Set variables for asset %s', asset.name
|
|
1370
1654
|
)
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
for association in associations:
|
|
1374
|
-
left_asset = self.assets[association['leftAsset']]
|
|
1375
|
-
if not left_asset:
|
|
1376
|
-
msg = 'Left asset "%s" for association "%s" not found!'
|
|
1377
|
-
logger.error(
|
|
1378
|
-
msg, association["leftAsset"], association["name"])
|
|
1379
|
-
raise LanguageGraphAssociationError(
|
|
1380
|
-
msg % (association["leftAsset"], association["name"]))
|
|
1381
|
-
|
|
1382
|
-
right_asset = self.assets[association['rightAsset']]
|
|
1383
|
-
if not right_asset:
|
|
1384
|
-
msg = 'Right asset "%s" for association "%s" not found!'
|
|
1385
|
-
logger.error(
|
|
1386
|
-
msg, association["rightAsset"], association["name"])
|
|
1387
|
-
raise LanguageGraphAssociationError(
|
|
1388
|
-
msg % (association["rightAsset"], association["name"])
|
|
1389
|
-
)
|
|
1390
|
-
|
|
1391
|
-
assoc_node = LanguageGraphAssociation(
|
|
1392
|
-
name = association['name'],
|
|
1393
|
-
left_field = LanguageGraphAssociationField(
|
|
1394
|
-
left_asset,
|
|
1395
|
-
association['leftField'],
|
|
1396
|
-
association['leftMultiplicity']['min'],
|
|
1397
|
-
association['leftMultiplicity']['max']),
|
|
1398
|
-
right_field = LanguageGraphAssociationField(
|
|
1399
|
-
right_asset,
|
|
1400
|
-
association['rightField'],
|
|
1401
|
-
association['rightMultiplicity']['min'],
|
|
1402
|
-
association['rightMultiplicity']['max']),
|
|
1403
|
-
info = association['meta']
|
|
1404
|
-
)
|
|
1405
|
-
|
|
1406
|
-
# Add the association to the left and right asset
|
|
1407
|
-
self._link_association_to_assets(assoc_node,
|
|
1408
|
-
left_asset, right_asset)
|
|
1409
|
-
|
|
1410
|
-
# Set the variables
|
|
1411
|
-
for asset in self.assets.values():
|
|
1412
|
-
for variable in self._get_variables_for_asset_type(asset.name):
|
|
1655
|
+
variables = self._get_variables_for_asset_type(asset.name)
|
|
1656
|
+
for variable in variables:
|
|
1413
1657
|
if logger.isEnabledFor(logging.DEBUG):
|
|
1414
1658
|
# Avoid running json.dumps when not in debug
|
|
1415
1659
|
logger.debug(
|
|
@@ -1418,9 +1662,13 @@ class LanguageGraph():
|
|
|
1418
1662
|
)
|
|
1419
1663
|
self._resolve_variable(asset, variable['name'])
|
|
1420
1664
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
for asset
|
|
1665
|
+
def _generate_attack_steps(self, assets) -> None:
|
|
1666
|
+
"""
|
|
1667
|
+
Generate all of the attack steps for each asset type
|
|
1668
|
+
based on the language specification.
|
|
1669
|
+
"""
|
|
1670
|
+
langspec_dict = {}
|
|
1671
|
+
for asset in assets.values():
|
|
1424
1672
|
logger.debug(
|
|
1425
1673
|
'Create attack steps language graph nodes for asset %s',
|
|
1426
1674
|
asset.name
|
|
@@ -1436,24 +1684,27 @@ class LanguageGraph():
|
|
|
1436
1684
|
name = attack_step_attribs['name'],
|
|
1437
1685
|
type = attack_step_attribs['type'],
|
|
1438
1686
|
asset = asset,
|
|
1439
|
-
ttc = attack_step_attribs
|
|
1440
|
-
overrides =
|
|
1441
|
-
|
|
1687
|
+
ttc = get_ttc_distribution(attack_step_attribs),
|
|
1688
|
+
overrides = (
|
|
1689
|
+
attack_step_attribs['reaches']['overrides']
|
|
1690
|
+
if attack_step_attribs['reaches'] else False
|
|
1691
|
+
),
|
|
1442
1692
|
children = {},
|
|
1443
1693
|
parents = {},
|
|
1444
1694
|
info = attack_step_attribs['meta'],
|
|
1445
1695
|
tags = set(attack_step_attribs['tags'])
|
|
1446
1696
|
)
|
|
1447
|
-
attack_step_node.
|
|
1448
|
-
|
|
1697
|
+
langspec_dict[attack_step_node.full_name] = \
|
|
1698
|
+
attack_step_attribs
|
|
1699
|
+
asset.attack_steps[attack_step_node.name] = \
|
|
1449
1700
|
attack_step_node
|
|
1450
1701
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1702
|
+
detectors: dict = attack_step_attribs.get("detectors", {})
|
|
1703
|
+
for detector in detectors.values():
|
|
1453
1704
|
attack_step_node.detectors[detector["name"]] = Detector(
|
|
1454
1705
|
context=Context(
|
|
1455
1706
|
{
|
|
1456
|
-
label:
|
|
1707
|
+
label: assets[asset]
|
|
1457
1708
|
for label, asset in detector["context"].items()
|
|
1458
1709
|
}
|
|
1459
1710
|
),
|
|
@@ -1508,13 +1759,15 @@ class LanguageGraph():
|
|
|
1508
1759
|
attack_step.name
|
|
1509
1760
|
)
|
|
1510
1761
|
|
|
1511
|
-
if attack_step.
|
|
1762
|
+
if attack_step.full_name not in langspec_dict:
|
|
1512
1763
|
# This is simply an empty inherited attack step
|
|
1513
1764
|
continue
|
|
1514
1765
|
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1766
|
+
langspec_entry = langspec_dict[attack_step.full_name]
|
|
1767
|
+
step_expressions = (
|
|
1768
|
+
langspec_entry['reaches']['stepExpressions']
|
|
1769
|
+
if langspec_entry['reaches'] else []
|
|
1770
|
+
)
|
|
1518
1771
|
|
|
1519
1772
|
for step_expression in step_expressions:
|
|
1520
1773
|
# Resolve each of the attack step expressions listed for
|
|
@@ -1549,65 +1802,94 @@ class LanguageGraph():
|
|
|
1549
1802
|
target_attack_step_name]
|
|
1550
1803
|
|
|
1551
1804
|
# Link to the children target attack steps
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
[(target_attack_step, expr_chain)]
|
|
1805
|
+
attack_step.children.setdefault(target_attack_step.full_name, [])
|
|
1806
|
+
attack_step.children[target_attack_step.full_name].append(
|
|
1807
|
+
(target_attack_step, expr_chain)
|
|
1808
|
+
)
|
|
1809
|
+
|
|
1558
1810
|
# Reverse the children associations chains to get the
|
|
1559
1811
|
# parents associations chain.
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
None)))
|
|
1565
|
-
else:
|
|
1566
|
-
target_attack_step.parents[attack_step.full_name] = \
|
|
1567
|
-
[(attack_step,
|
|
1568
|
-
self.reverse_expr_chain(expr_chain,
|
|
1569
|
-
None))]
|
|
1812
|
+
target_attack_step.parents.setdefault(attack_step.full_name, [])
|
|
1813
|
+
target_attack_step.parents[attack_step.full_name].append(
|
|
1814
|
+
(attack_step, self.reverse_expr_chain(expr_chain, None))
|
|
1815
|
+
)
|
|
1570
1816
|
|
|
1571
1817
|
# Evaluate the requirements of exist and notExist attack steps
|
|
1572
|
-
if attack_step.type
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1818
|
+
if attack_step.type in ('exist', 'notExist'):
|
|
1819
|
+
step_expressions = (
|
|
1820
|
+
langspec_entry['requires']['stepExpressions']
|
|
1821
|
+
if langspec_entry['requires'] else []
|
|
1822
|
+
)
|
|
1577
1823
|
if not step_expressions:
|
|
1578
|
-
msg = 'Failed to find requirements for attack step' \
|
|
1579
|
-
' "%s" of type "%s":\n%s'
|
|
1580
1824
|
raise LanguageGraphStepExpressionError(
|
|
1581
|
-
|
|
1825
|
+
'Failed to find requirements for attack step'
|
|
1826
|
+
' "%s" of type "%s":\n%s' % (
|
|
1582
1827
|
attack_step.name,
|
|
1583
1828
|
attack_step.type,
|
|
1584
|
-
json.dumps(
|
|
1829
|
+
json.dumps(langspec_entry, indent = 2)
|
|
1585
1830
|
)
|
|
1586
1831
|
)
|
|
1587
1832
|
|
|
1588
|
-
attack_step.own_requires = []
|
|
1589
1833
|
for step_expression in step_expressions:
|
|
1590
|
-
_, \
|
|
1591
|
-
result_expr_chain, \
|
|
1592
|
-
_ = \
|
|
1834
|
+
_, result_expr_chain, _ = \
|
|
1593
1835
|
self.process_step_expression(
|
|
1594
1836
|
attack_step.asset,
|
|
1595
1837
|
None,
|
|
1596
1838
|
step_expression
|
|
1597
1839
|
)
|
|
1840
|
+
if result_expr_chain is None:
|
|
1841
|
+
raise LanguageGraphException('Failed to find '
|
|
1842
|
+
'existence step requirement for step '
|
|
1843
|
+
f'expression:\n%s' % step_expression)
|
|
1598
1844
|
attack_step.own_requires.append(result_expr_chain)
|
|
1599
1845
|
|
|
1600
|
-
def
|
|
1846
|
+
def _generate_graph(self) -> None:
|
|
1847
|
+
"""
|
|
1848
|
+
Generate language graph starting from the MAL language specification
|
|
1849
|
+
given in the constructor.
|
|
1850
|
+
"""
|
|
1851
|
+
# Generate all of the asset nodes of the language graph.
|
|
1852
|
+
self.assets = {}
|
|
1853
|
+
for asset_dict in self._lang_spec['assets']:
|
|
1854
|
+
logger.debug(
|
|
1855
|
+
'Create asset language graph nodes for asset %s',
|
|
1856
|
+
asset_dict['name']
|
|
1857
|
+
)
|
|
1858
|
+
asset_node = LanguageGraphAsset(
|
|
1859
|
+
name = asset_dict['name'],
|
|
1860
|
+
own_associations = {},
|
|
1861
|
+
attack_steps = {},
|
|
1862
|
+
info = asset_dict['meta'],
|
|
1863
|
+
own_super_asset = None,
|
|
1864
|
+
own_sub_assets = set(),
|
|
1865
|
+
own_variables = {},
|
|
1866
|
+
is_abstract = asset_dict['isAbstract']
|
|
1867
|
+
)
|
|
1868
|
+
self.assets[asset_dict['name']] = asset_node
|
|
1869
|
+
|
|
1870
|
+
# Link assets to each other
|
|
1871
|
+
self._link_assets(self._lang_spec, self.assets)
|
|
1872
|
+
|
|
1873
|
+
# Add and link associations to assets
|
|
1874
|
+
self._create_associations_for_assets(self._lang_spec, self.assets)
|
|
1875
|
+
|
|
1876
|
+
# Set the variables for each asset
|
|
1877
|
+
self._set_variables_for_assets(self.assets)
|
|
1878
|
+
|
|
1879
|
+
# Add attack steps to the assets
|
|
1880
|
+
self._generate_attack_steps(self.assets)
|
|
1881
|
+
|
|
1882
|
+
def _get_attacks_for_asset_type(self, asset_type: str) -> dict[str, dict]:
|
|
1601
1883
|
"""
|
|
1602
|
-
Get all Attack Steps for a specific
|
|
1884
|
+
Get all Attack Steps for a specific asset type.
|
|
1603
1885
|
|
|
1604
1886
|
Arguments:
|
|
1605
|
-
asset_type -
|
|
1606
|
-
list the possible attack steps
|
|
1887
|
+
asset_type - the name of the asset type we want to
|
|
1888
|
+
list the possible attack steps for
|
|
1607
1889
|
|
|
1608
1890
|
Return:
|
|
1609
|
-
A dictionary
|
|
1610
|
-
specified
|
|
1891
|
+
A dictionary containing the possible attacks for the
|
|
1892
|
+
specified asset type. Each key in the dictionary is an attack name
|
|
1611
1893
|
associated with a dictionary containing other characteristics of the
|
|
1612
1894
|
attack such as type of attack, TTC distribution, child attack steps
|
|
1613
1895
|
and other information
|
|
@@ -1634,20 +1916,18 @@ class LanguageGraph():
|
|
|
1634
1916
|
|
|
1635
1917
|
return attack_steps
|
|
1636
1918
|
|
|
1637
|
-
def _get_associations_for_asset_type(self, asset_type: str) -> list:
|
|
1919
|
+
def _get_associations_for_asset_type(self, asset_type: str) -> list[dict]:
|
|
1638
1920
|
"""
|
|
1639
|
-
Get all
|
|
1921
|
+
Get all associations for a specific asset type.
|
|
1640
1922
|
|
|
1641
1923
|
Arguments:
|
|
1642
|
-
asset_type -
|
|
1924
|
+
asset_type - the name of the asset type for which we want to
|
|
1643
1925
|
list the associations
|
|
1644
1926
|
|
|
1645
1927
|
Return:
|
|
1646
|
-
A
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
as type of attack, TTC distribution, child attack steps and other
|
|
1650
|
-
information
|
|
1928
|
+
A list of dicts, where each dict represents an associations
|
|
1929
|
+
for the specified asset type. Each dictionary contains
|
|
1930
|
+
name and meta information about the association.
|
|
1651
1931
|
"""
|
|
1652
1932
|
logger.debug(
|
|
1653
1933
|
'Get associations for %s asset from '
|
|
@@ -1668,25 +1948,25 @@ class LanguageGraph():
|
|
|
1668
1948
|
if assoc['leftAsset'] == asset_type or \
|
|
1669
1949
|
assoc['rightAsset'] == asset_type)
|
|
1670
1950
|
assoc = next(assoc_iter, None)
|
|
1671
|
-
while
|
|
1951
|
+
while assoc:
|
|
1672
1952
|
associations.append(assoc)
|
|
1673
1953
|
assoc = next(assoc_iter, None)
|
|
1674
1954
|
|
|
1675
1955
|
return associations
|
|
1676
1956
|
|
|
1677
1957
|
def _get_variables_for_asset_type(
|
|
1678
|
-
self, asset_type: str) -> dict:
|
|
1958
|
+
self, asset_type: str) -> list[dict]:
|
|
1679
1959
|
"""
|
|
1680
|
-
Get
|
|
1960
|
+
Get variables for a specific asset type.
|
|
1681
1961
|
Note: Variables are the ones specified in MAL through `let` statements
|
|
1682
1962
|
|
|
1683
1963
|
Arguments:
|
|
1684
|
-
asset_type - a string representing the type
|
|
1964
|
+
asset_type - a string representing the asset type which
|
|
1685
1965
|
contains the variables
|
|
1686
1966
|
|
|
1687
1967
|
Return:
|
|
1688
|
-
A
|
|
1689
|
-
|
|
1968
|
+
A list of dicts representing the step expressions for the variables
|
|
1969
|
+
belonging to the asset.
|
|
1690
1970
|
"""
|
|
1691
1971
|
|
|
1692
1972
|
asset_dict = next((asset for asset in self._lang_spec['assets'] \
|
|
@@ -1733,5 +2013,3 @@ class LanguageGraph():
|
|
|
1733
2013
|
|
|
1734
2014
|
self.assets = {}
|
|
1735
2015
|
self._generate_graph()
|
|
1736
|
-
|
|
1737
|
-
|