mal-toolbox 0.3.11__py3-none-any.whl → 1.0.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.3.11.dist-info → mal_toolbox-1.0.0.dist-info}/METADATA +4 -22
- mal_toolbox-1.0.0.dist-info/RECORD +26 -0
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.0.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 +51 -192
- maltoolbox/attackgraph/node.py +2 -82
- maltoolbox/file_utils.py +1 -1
- maltoolbox/language/__init__.py +11 -0
- maltoolbox/language/languagegraph.py +631 -369
- maltoolbox/model.py +6 -208
- 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.0.dist-info}/entry_points.txt +0 -0
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.0.dist-info/licenses}/AUTHORS +0 -0
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.0.dist-info/licenses}/LICENSE +0 -0
- {mal_toolbox-0.3.11.dist-info → mal_toolbox-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -26,6 +26,83 @@ 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': [1.0],
|
|
101
|
+
'name': 'Bernoulli',
|
|
102
|
+
'type': 'function'
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
29
106
|
|
|
30
107
|
def disaggregate_attack_step_full_name(
|
|
31
108
|
attack_step_full_name: str) -> list[str]:
|
|
@@ -49,6 +126,7 @@ class Detector:
|
|
|
49
126
|
|
|
50
127
|
|
|
51
128
|
class Context(dict):
|
|
129
|
+
"""Context is part of detectors to provide meta data about attackers"""
|
|
52
130
|
def __init__(self, context) -> None:
|
|
53
131
|
super().__init__(context)
|
|
54
132
|
self._context_dict = context
|
|
@@ -66,6 +144,7 @@ class Context(dict):
|
|
|
66
144
|
|
|
67
145
|
@dataclass
|
|
68
146
|
class LanguageGraphAsset:
|
|
147
|
+
"""An asset type as defined in the MAL language"""
|
|
69
148
|
name: str
|
|
70
149
|
own_associations: dict[str, LanguageGraphAssociation] = \
|
|
71
150
|
field(default_factory = dict)
|
|
@@ -127,7 +206,7 @@ class LanguageGraphAsset:
|
|
|
127
206
|
False otherwise.
|
|
128
207
|
"""
|
|
129
208
|
current_asset: Optional[LanguageGraphAsset] = self
|
|
130
|
-
while
|
|
209
|
+
while current_asset:
|
|
131
210
|
if current_asset == target_asset:
|
|
132
211
|
return True
|
|
133
212
|
current_asset = current_asset.own_super_asset
|
|
@@ -164,7 +243,7 @@ class LanguageGraphAsset:
|
|
|
164
243
|
"""
|
|
165
244
|
current_asset: Optional[LanguageGraphAsset] = self
|
|
166
245
|
superassets = []
|
|
167
|
-
while
|
|
246
|
+
while current_asset:
|
|
168
247
|
superassets.append(current_asset)
|
|
169
248
|
current_asset = current_asset.own_super_asset
|
|
170
249
|
return superassets
|
|
@@ -188,7 +267,9 @@ class LanguageGraphAsset:
|
|
|
188
267
|
|
|
189
268
|
|
|
190
269
|
@property
|
|
191
|
-
def variables(
|
|
270
|
+
def variables(
|
|
271
|
+
self
|
|
272
|
+
) -> dict[str, tuple[LanguageGraphAsset, ExpressionsChain]]:
|
|
192
273
|
"""
|
|
193
274
|
Return a list of all of the variables that belong to this asset
|
|
194
275
|
directly or indirectly via inheritance.
|
|
@@ -204,27 +285,6 @@ class LanguageGraphAsset:
|
|
|
204
285
|
return all_vars
|
|
205
286
|
|
|
206
287
|
|
|
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
288
|
def get_all_common_superassets(
|
|
229
289
|
self, other: LanguageGraphAsset
|
|
230
290
|
) -> set[str]:
|
|
@@ -241,6 +301,7 @@ class LanguageGraphAsset:
|
|
|
241
301
|
|
|
242
302
|
@dataclass
|
|
243
303
|
class LanguageGraphAssociationField:
|
|
304
|
+
"""A field in an association"""
|
|
244
305
|
asset: LanguageGraphAsset
|
|
245
306
|
fieldname: str
|
|
246
307
|
minimum: int
|
|
@@ -249,6 +310,9 @@ class LanguageGraphAssociationField:
|
|
|
249
310
|
|
|
250
311
|
@dataclass
|
|
251
312
|
class LanguageGraphAssociation:
|
|
313
|
+
"""
|
|
314
|
+
An association type between asset types as defined in the MAL language
|
|
315
|
+
"""
|
|
252
316
|
name: str
|
|
253
317
|
left_field: LanguageGraphAssociationField
|
|
254
318
|
right_field: LanguageGraphAssociationField
|
|
@@ -277,9 +341,11 @@ class LanguageGraphAssociation:
|
|
|
277
341
|
|
|
278
342
|
|
|
279
343
|
def __repr__(self) -> str:
|
|
280
|
-
return (
|
|
344
|
+
return (
|
|
345
|
+
f'LanguageGraphAssociation(name: "{self.name}", '
|
|
281
346
|
f'left_field: {self.left_field}, '
|
|
282
|
-
f'right_field: {self.right_field})'
|
|
347
|
+
f'right_field: {self.right_field})'
|
|
348
|
+
)
|
|
283
349
|
|
|
284
350
|
|
|
285
351
|
@property
|
|
@@ -293,7 +359,7 @@ class LanguageGraphAssociation:
|
|
|
293
359
|
self.name,\
|
|
294
360
|
self.left_field.fieldname,\
|
|
295
361
|
self.right_field.fieldname
|
|
296
|
-
|
|
362
|
+
)
|
|
297
363
|
return full_name
|
|
298
364
|
|
|
299
365
|
|
|
@@ -363,6 +429,9 @@ class LanguageGraphAssociation:
|
|
|
363
429
|
|
|
364
430
|
@dataclass
|
|
365
431
|
class LanguageGraphAttackStep:
|
|
432
|
+
"""
|
|
433
|
+
An attack step belonging to an asset type in the MAL language
|
|
434
|
+
"""
|
|
366
435
|
name: str
|
|
367
436
|
type: str
|
|
368
437
|
asset: LanguageGraphAsset
|
|
@@ -372,8 +441,8 @@ class LanguageGraphAttackStep:
|
|
|
372
441
|
parents: dict = field(default_factory = dict)
|
|
373
442
|
info: dict = field(default_factory = dict)
|
|
374
443
|
inherits: Optional[LanguageGraphAttackStep] = None
|
|
444
|
+
own_requires: list[ExpressionsChain] = field(default_factory=list)
|
|
375
445
|
tags: set = field(default_factory = set)
|
|
376
|
-
_attributes: Optional[dict] = None
|
|
377
446
|
detectors: dict = field(default_factory = lambda: {})
|
|
378
447
|
|
|
379
448
|
|
|
@@ -450,6 +519,10 @@ class LanguageGraphAttackStep:
|
|
|
450
519
|
|
|
451
520
|
|
|
452
521
|
class ExpressionsChain:
|
|
522
|
+
"""
|
|
523
|
+
A series of linked step expressions that specify the association path and
|
|
524
|
+
operations to take to reach the child/parent attack step.
|
|
525
|
+
"""
|
|
453
526
|
def __init__(self,
|
|
454
527
|
type: str,
|
|
455
528
|
left_link: Optional[ExpressionsChain] = None,
|
|
@@ -561,7 +634,6 @@ class ExpressionsChain:
|
|
|
561
634
|
msg = 'Missing expressions chain type!'
|
|
562
635
|
logger.error(msg)
|
|
563
636
|
raise LanguageGraphAssociationError(msg)
|
|
564
|
-
return None
|
|
565
637
|
|
|
566
638
|
expr_chain_type = serialized_expr_chain['type']
|
|
567
639
|
match (expr_chain_type):
|
|
@@ -597,9 +669,10 @@ class ExpressionsChain:
|
|
|
597
669
|
if association is None:
|
|
598
670
|
msg = 'Failed to find association "%s" with '\
|
|
599
671
|
'fieldname "%s"'
|
|
600
|
-
logger.error(msg
|
|
601
|
-
raise LanguageGraphException(
|
|
602
|
-
fieldname)
|
|
672
|
+
logger.error(msg, assoc_name, fieldname)
|
|
673
|
+
raise LanguageGraphException(
|
|
674
|
+
msg % (assoc_name, fieldname)
|
|
675
|
+
)
|
|
603
676
|
|
|
604
677
|
new_expr_chain = ExpressionsChain(
|
|
605
678
|
type = 'field',
|
|
@@ -629,7 +702,7 @@ class ExpressionsChain:
|
|
|
629
702
|
subtype_asset = lang_graph.assets[subtype_name]
|
|
630
703
|
else:
|
|
631
704
|
msg = 'Failed to find subtype %s'
|
|
632
|
-
logger.error(msg
|
|
705
|
+
logger.error(msg, subtype_name)
|
|
633
706
|
raise LanguageGraphException(msg % subtype_name)
|
|
634
707
|
|
|
635
708
|
new_expr_chain = ExpressionsChain(
|
|
@@ -642,8 +715,9 @@ class ExpressionsChain:
|
|
|
642
715
|
case _:
|
|
643
716
|
msg = 'Unknown expressions chain type %s!'
|
|
644
717
|
logger.error(msg, serialized_expr_chain['type'])
|
|
645
|
-
raise LanguageGraphAssociationError(
|
|
646
|
-
serialized_expr_chain['type']
|
|
718
|
+
raise LanguageGraphAssociationError(
|
|
719
|
+
msg % serialized_expr_chain['type']
|
|
720
|
+
)
|
|
647
721
|
|
|
648
722
|
|
|
649
723
|
def __repr__(self) -> str:
|
|
@@ -653,7 +727,8 @@ class ExpressionsChain:
|
|
|
653
727
|
class LanguageGraph():
|
|
654
728
|
"""Graph representation of a MAL language"""
|
|
655
729
|
def __init__(self, lang: Optional[dict] = None):
|
|
656
|
-
self.assets: dict = {}
|
|
730
|
+
self.assets: dict[str, LanguageGraphAsset] = {}
|
|
731
|
+
self.predef_ttcs: dict[str, dict] = predef_ttcs
|
|
657
732
|
if lang is not None:
|
|
658
733
|
self._lang_spec: dict = lang
|
|
659
734
|
self.metadata = {
|
|
@@ -695,6 +770,33 @@ class LanguageGraph():
|
|
|
695
770
|
return LanguageGraph(json.loads(langspec))
|
|
696
771
|
|
|
697
772
|
|
|
773
|
+
def replace_if_predef_ttc(
|
|
774
|
+
self,
|
|
775
|
+
ttc_entry: Optional[dict]
|
|
776
|
+
) -> dict:
|
|
777
|
+
"""
|
|
778
|
+
If the TTC provided is a predefined name replace it with the
|
|
779
|
+
probability distribution it corresponds to. Otherwise, simply return
|
|
780
|
+
the TTC distribution provided as is.
|
|
781
|
+
|
|
782
|
+
Arguments:
|
|
783
|
+
ttc_entry - the TTC entry to check for predefined names
|
|
784
|
+
|
|
785
|
+
Returns:
|
|
786
|
+
If the TTC entry provided contained a predefined name the TTC
|
|
787
|
+
probability distrubtion corresponding to it. Otherwise, the TTC
|
|
788
|
+
distribution provided as a parameter as is.
|
|
789
|
+
"""
|
|
790
|
+
if ttc_entry is None:
|
|
791
|
+
return {}
|
|
792
|
+
|
|
793
|
+
ttc = self.predef_ttcs.get(str(ttc_entry.get('name')))
|
|
794
|
+
if ttc is not None:
|
|
795
|
+
return ttc
|
|
796
|
+
else:
|
|
797
|
+
return ttc_entry
|
|
798
|
+
|
|
799
|
+
|
|
698
800
|
def _to_dict(self):
|
|
699
801
|
"""Converts LanguageGraph into a dict"""
|
|
700
802
|
|
|
@@ -708,10 +810,12 @@ class LanguageGraph():
|
|
|
708
810
|
|
|
709
811
|
return serialized_graph
|
|
710
812
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
813
|
+
@staticmethod
|
|
814
|
+
def _link_association_to_assets(
|
|
815
|
+
assoc: LanguageGraphAssociation,
|
|
816
|
+
left_asset: LanguageGraphAsset,
|
|
817
|
+
right_asset: LanguageGraphAsset
|
|
818
|
+
):
|
|
715
819
|
left_asset.own_associations[assoc.right_field.fieldname] = assoc
|
|
716
820
|
right_asset.own_associations[assoc.left_field.fieldname] = assoc
|
|
717
821
|
|
|
@@ -836,11 +940,13 @@ class LanguageGraph():
|
|
|
836
940
|
asset_dict['name']
|
|
837
941
|
)
|
|
838
942
|
for attack_step_dict in asset_dict['attack_steps'].values():
|
|
943
|
+
ttc = lang_graph.replace_if_predef_ttc(
|
|
944
|
+
attack_step_dict['ttc'])
|
|
839
945
|
attack_step_node = LanguageGraphAttackStep(
|
|
840
946
|
name = attack_step_dict['name'],
|
|
841
947
|
type = attack_step_dict['type'],
|
|
842
948
|
asset = asset,
|
|
843
|
-
ttc =
|
|
949
|
+
ttc = ttc,
|
|
844
950
|
overrides = attack_step_dict['overrides'],
|
|
845
951
|
children = {},
|
|
846
952
|
parents = {},
|
|
@@ -925,7 +1031,8 @@ class LanguageGraph():
|
|
|
925
1031
|
expr_chain_dict,
|
|
926
1032
|
lang_graph
|
|
927
1033
|
)
|
|
928
|
-
|
|
1034
|
+
if expr_chain:
|
|
1035
|
+
attack_step.own_requires.append(expr_chain)
|
|
929
1036
|
|
|
930
1037
|
return lang_graph
|
|
931
1038
|
|
|
@@ -955,7 +1062,6 @@ class LanguageGraph():
|
|
|
955
1062
|
)
|
|
956
1063
|
|
|
957
1064
|
|
|
958
|
-
|
|
959
1065
|
def save_language_specification_to_json(self, filename: str) -> None:
|
|
960
1066
|
"""
|
|
961
1067
|
Save a MAL language specification dictionary to a JSON file
|
|
@@ -968,12 +1074,290 @@ class LanguageGraph():
|
|
|
968
1074
|
with open(filename, 'w', encoding='utf-8') as file:
|
|
969
1075
|
json.dump(self._lang_spec, file, indent=4)
|
|
970
1076
|
|
|
1077
|
+
def process_attack_step_expression(
|
|
1078
|
+
self,
|
|
1079
|
+
target_asset: LanguageGraphAsset,
|
|
1080
|
+
step_expression: dict[str, Any]
|
|
1081
|
+
) -> tuple[
|
|
1082
|
+
LanguageGraphAsset,
|
|
1083
|
+
None,
|
|
1084
|
+
str
|
|
1085
|
+
]:
|
|
1086
|
+
"""
|
|
1087
|
+
The attack step expression just adds the name of the attack
|
|
1088
|
+
step. All other step expressions only modify the target
|
|
1089
|
+
asset and parent associations chain.
|
|
1090
|
+
"""
|
|
1091
|
+
return (
|
|
1092
|
+
target_asset,
|
|
1093
|
+
None,
|
|
1094
|
+
step_expression['name']
|
|
1095
|
+
)
|
|
971
1096
|
|
|
972
|
-
def
|
|
1097
|
+
def process_set_operation_step_expression(
|
|
1098
|
+
self,
|
|
1099
|
+
target_asset: LanguageGraphAsset,
|
|
1100
|
+
expr_chain: Optional[ExpressionsChain],
|
|
1101
|
+
step_expression: dict[str, Any]
|
|
1102
|
+
) -> tuple[
|
|
1103
|
+
LanguageGraphAsset,
|
|
1104
|
+
ExpressionsChain,
|
|
1105
|
+
None
|
|
1106
|
+
]:
|
|
1107
|
+
"""
|
|
1108
|
+
The set operators are used to combine the left hand and right
|
|
1109
|
+
hand targets accordingly.
|
|
1110
|
+
"""
|
|
1111
|
+
|
|
1112
|
+
lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
|
|
1113
|
+
target_asset,
|
|
1114
|
+
expr_chain,
|
|
1115
|
+
step_expression['lhs']
|
|
1116
|
+
)
|
|
1117
|
+
rh_target_asset, rh_expr_chain, _ = self.process_step_expression(
|
|
973
1118
|
target_asset,
|
|
974
1119
|
expr_chain,
|
|
1120
|
+
step_expression['rhs']
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
assert lh_target_asset, (
|
|
1124
|
+
f"No lh target in step expression {step_expression}"
|
|
1125
|
+
)
|
|
1126
|
+
assert rh_target_asset, (
|
|
1127
|
+
f"No rh target in step expression {step_expression}"
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
if not lh_target_asset.get_all_common_superassets(rh_target_asset):
|
|
1131
|
+
raise ValueError(
|
|
1132
|
+
"Set operation attempted between targets that do not share "
|
|
1133
|
+
f"any common superassets: {lh_target_asset.name} "
|
|
1134
|
+
f"and {rh_target_asset.name}!"
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
new_expr_chain = ExpressionsChain(
|
|
1138
|
+
type = step_expression['type'],
|
|
1139
|
+
left_link = lh_expr_chain,
|
|
1140
|
+
right_link = rh_expr_chain
|
|
1141
|
+
)
|
|
1142
|
+
return (
|
|
1143
|
+
lh_target_asset,
|
|
1144
|
+
new_expr_chain,
|
|
1145
|
+
None
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
def process_variable_step_expression(
|
|
1149
|
+
self,
|
|
1150
|
+
target_asset: LanguageGraphAsset,
|
|
1151
|
+
step_expression: dict[str, Any]
|
|
1152
|
+
) -> tuple[
|
|
1153
|
+
LanguageGraphAsset,
|
|
1154
|
+
ExpressionsChain,
|
|
1155
|
+
None
|
|
1156
|
+
]:
|
|
1157
|
+
|
|
1158
|
+
var_name = step_expression['name']
|
|
1159
|
+
var_target_asset, var_expr_chain = (
|
|
1160
|
+
self._resolve_variable(target_asset, var_name)
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
if var_expr_chain is None:
|
|
1164
|
+
raise LookupError(
|
|
1165
|
+
f'Failed to find variable "{step_expression["name"]}" '
|
|
1166
|
+
f'for {target_asset.name}',
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
return (
|
|
1170
|
+
var_target_asset,
|
|
1171
|
+
var_expr_chain,
|
|
1172
|
+
None
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
def process_field_step_expression(
|
|
1176
|
+
self,
|
|
1177
|
+
target_asset: LanguageGraphAsset,
|
|
1178
|
+
step_expression: dict[str, Any]
|
|
1179
|
+
) -> tuple[
|
|
1180
|
+
LanguageGraphAsset,
|
|
1181
|
+
ExpressionsChain,
|
|
1182
|
+
None
|
|
1183
|
+
]:
|
|
1184
|
+
"""
|
|
1185
|
+
Change the target asset from the current one to the associated
|
|
1186
|
+
asset given the specified field name and add the parent
|
|
1187
|
+
fieldname and association to the parent associations chain.
|
|
1188
|
+
"""
|
|
1189
|
+
|
|
1190
|
+
fieldname = step_expression['name']
|
|
1191
|
+
|
|
1192
|
+
if target_asset is None:
|
|
1193
|
+
raise ValueError(
|
|
1194
|
+
f'Missing target asset for field "{fieldname}"!'
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
new_target_asset = None
|
|
1198
|
+
for association in target_asset.associations.values():
|
|
1199
|
+
if (association.left_field.fieldname == fieldname and \
|
|
1200
|
+
target_asset.is_subasset_of(
|
|
1201
|
+
association.right_field.asset)):
|
|
1202
|
+
new_target_asset = association.left_field.asset
|
|
1203
|
+
|
|
1204
|
+
if (association.right_field.fieldname == fieldname and \
|
|
1205
|
+
target_asset.is_subasset_of(
|
|
1206
|
+
association.left_field.asset)):
|
|
1207
|
+
new_target_asset = association.right_field.asset
|
|
1208
|
+
|
|
1209
|
+
if new_target_asset:
|
|
1210
|
+
new_expr_chain = ExpressionsChain(
|
|
1211
|
+
type = 'field',
|
|
1212
|
+
fieldname = fieldname,
|
|
1213
|
+
association = association
|
|
1214
|
+
)
|
|
1215
|
+
return (
|
|
1216
|
+
new_target_asset,
|
|
1217
|
+
new_expr_chain,
|
|
1218
|
+
None
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
raise LookupError(
|
|
1222
|
+
f'Failed to find field {fieldname} on asset {target_asset.name}!',
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
def process_transitive_step_expression(
|
|
1226
|
+
self,
|
|
1227
|
+
target_asset: LanguageGraphAsset,
|
|
1228
|
+
expr_chain: Optional[ExpressionsChain],
|
|
1229
|
+
step_expression: dict[str, Any]
|
|
1230
|
+
) -> tuple[
|
|
1231
|
+
LanguageGraphAsset,
|
|
1232
|
+
ExpressionsChain,
|
|
1233
|
+
None
|
|
1234
|
+
]:
|
|
1235
|
+
"""
|
|
1236
|
+
Create a transitive tuple entry that applies to the next
|
|
1237
|
+
component of the step expression.
|
|
1238
|
+
"""
|
|
1239
|
+
result_target_asset, result_expr_chain, _ = (
|
|
1240
|
+
self.process_step_expression(
|
|
1241
|
+
target_asset,
|
|
1242
|
+
expr_chain,
|
|
1243
|
+
step_expression['stepExpression']
|
|
1244
|
+
)
|
|
1245
|
+
)
|
|
1246
|
+
new_expr_chain = ExpressionsChain(
|
|
1247
|
+
type = 'transitive',
|
|
1248
|
+
sub_link = result_expr_chain
|
|
1249
|
+
)
|
|
1250
|
+
return (
|
|
1251
|
+
result_target_asset,
|
|
1252
|
+
new_expr_chain,
|
|
1253
|
+
None
|
|
1254
|
+
)
|
|
1255
|
+
|
|
1256
|
+
def process_subType_step_expression(
|
|
1257
|
+
self,
|
|
1258
|
+
target_asset: LanguageGraphAsset,
|
|
1259
|
+
expr_chain: Optional[ExpressionsChain],
|
|
1260
|
+
step_expression: dict[str, Any]
|
|
1261
|
+
) -> tuple[
|
|
1262
|
+
LanguageGraphAsset,
|
|
1263
|
+
ExpressionsChain,
|
|
1264
|
+
None
|
|
1265
|
+
]:
|
|
1266
|
+
"""
|
|
1267
|
+
Create a subType tuple entry that applies to the next
|
|
1268
|
+
component of the step expression and changes the target
|
|
1269
|
+
asset to the subasset.
|
|
1270
|
+
"""
|
|
1271
|
+
|
|
1272
|
+
subtype_name = step_expression['subType']
|
|
1273
|
+
result_target_asset, result_expr_chain, _ = (
|
|
1274
|
+
self.process_step_expression(
|
|
1275
|
+
target_asset,
|
|
1276
|
+
expr_chain,
|
|
1277
|
+
step_expression['stepExpression']
|
|
1278
|
+
)
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
if subtype_name not in self.assets:
|
|
1282
|
+
raise LanguageGraphException(
|
|
1283
|
+
f'Failed to find subtype {subtype_name}'
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
subtype_asset = self.assets[subtype_name]
|
|
1287
|
+
|
|
1288
|
+
if result_target_asset is None:
|
|
1289
|
+
raise LookupError("Nonexisting asset for subtype")
|
|
1290
|
+
|
|
1291
|
+
if not subtype_asset.is_subasset_of(result_target_asset):
|
|
1292
|
+
raise ValueError(
|
|
1293
|
+
f'Found subtype {subtype_name} which does not extend '
|
|
1294
|
+
f'{result_target_asset.name}, subtype cannot be resolved.'
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
new_expr_chain = ExpressionsChain(
|
|
1298
|
+
type = 'subType',
|
|
1299
|
+
sub_link = result_expr_chain,
|
|
1300
|
+
subtype = subtype_asset
|
|
1301
|
+
)
|
|
1302
|
+
return (
|
|
1303
|
+
subtype_asset,
|
|
1304
|
+
new_expr_chain,
|
|
1305
|
+
None
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1308
|
+
def process_collect_step_expression(
|
|
1309
|
+
self,
|
|
1310
|
+
target_asset: LanguageGraphAsset,
|
|
1311
|
+
expr_chain: Optional[ExpressionsChain],
|
|
1312
|
+
step_expression: dict[str, Any]
|
|
1313
|
+
) -> tuple[
|
|
1314
|
+
LanguageGraphAsset,
|
|
1315
|
+
Optional[ExpressionsChain],
|
|
1316
|
+
Optional[str]
|
|
1317
|
+
]:
|
|
1318
|
+
"""
|
|
1319
|
+
Apply the right hand step expression to left hand step
|
|
1320
|
+
expression target asset and parent associations chain.
|
|
1321
|
+
"""
|
|
1322
|
+
lh_target_asset, lh_expr_chain, _ = self.process_step_expression(
|
|
1323
|
+
target_asset, expr_chain, step_expression['lhs']
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
if lh_target_asset is None:
|
|
1327
|
+
raise ValueError(
|
|
1328
|
+
'No left hand asset in collect expression '
|
|
1329
|
+
f'{step_expression["lhs"]}'
|
|
1330
|
+
)
|
|
1331
|
+
|
|
1332
|
+
rh_target_asset, rh_expr_chain, rh_attack_step_name = (
|
|
1333
|
+
self.process_step_expression(
|
|
1334
|
+
lh_target_asset, None, step_expression['rhs']
|
|
1335
|
+
)
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
new_expr_chain = lh_expr_chain
|
|
1339
|
+
if rh_expr_chain:
|
|
1340
|
+
new_expr_chain = ExpressionsChain(
|
|
1341
|
+
type = 'collect',
|
|
1342
|
+
left_link = lh_expr_chain,
|
|
1343
|
+
right_link = rh_expr_chain
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
return (
|
|
1347
|
+
rh_target_asset,
|
|
1348
|
+
new_expr_chain,
|
|
1349
|
+
rh_attack_step_name
|
|
1350
|
+
)
|
|
1351
|
+
|
|
1352
|
+
def process_step_expression(self,
|
|
1353
|
+
target_asset: LanguageGraphAsset,
|
|
1354
|
+
expr_chain: Optional[ExpressionsChain],
|
|
975
1355
|
step_expression: dict
|
|
976
|
-
) -> tuple
|
|
1356
|
+
) -> tuple[
|
|
1357
|
+
LanguageGraphAsset,
|
|
1358
|
+
Optional[ExpressionsChain],
|
|
1359
|
+
Optional[str]
|
|
1360
|
+
]:
|
|
977
1361
|
"""
|
|
978
1362
|
Recursively process an attack step expression.
|
|
979
1363
|
|
|
@@ -1002,210 +1386,46 @@ class LanguageGraph():
|
|
|
1002
1386
|
json.dumps(step_expression, indent = 2)
|
|
1003
1387
|
)
|
|
1004
1388
|
|
|
1389
|
+
result: tuple[
|
|
1390
|
+
LanguageGraphAsset,
|
|
1391
|
+
Optional[ExpressionsChain],
|
|
1392
|
+
Optional[str]
|
|
1393
|
+
]
|
|
1394
|
+
|
|
1005
1395
|
match (step_expression['type']):
|
|
1006
1396
|
case 'attackStep':
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
# asset and parent associations chain.
|
|
1010
|
-
return (
|
|
1011
|
-
target_asset,
|
|
1012
|
-
None,
|
|
1013
|
-
step_expression['name']
|
|
1397
|
+
result = self.process_attack_step_expression(
|
|
1398
|
+
target_asset, step_expression
|
|
1014
1399
|
)
|
|
1015
|
-
|
|
1016
1400
|
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
|
|
1401
|
+
result = self.process_set_operation_step_expression(
|
|
1402
|
+
target_asset, expr_chain, step_expression
|
|
1044
1403
|
)
|
|
1045
|
-
return (
|
|
1046
|
-
lh_target_asset,
|
|
1047
|
-
new_expr_chain,
|
|
1048
|
-
None
|
|
1049
|
-
)
|
|
1050
|
-
|
|
1051
1404
|
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
|
-
|
|
1405
|
+
result = self.process_variable_step_expression(
|
|
1406
|
+
target_asset, step_expression
|
|
1407
|
+
)
|
|
1070
1408
|
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
|
|
1409
|
+
result = self.process_field_step_expression(
|
|
1410
|
+
target_asset, step_expression
|
|
1107
1411
|
)
|
|
1108
|
-
return (None, None, None)
|
|
1109
|
-
|
|
1110
1412
|
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
|
|
1413
|
+
result = self.process_transitive_step_expression(
|
|
1414
|
+
target_asset, expr_chain, step_expression
|
|
1124
1415
|
)
|
|
1125
|
-
return (
|
|
1126
|
-
result_target_asset,
|
|
1127
|
-
new_expr_chain,
|
|
1128
|
-
attack_step
|
|
1129
|
-
)
|
|
1130
|
-
|
|
1131
1416
|
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
|
|
1417
|
+
result = self.process_subType_step_expression(
|
|
1418
|
+
target_asset, expr_chain, step_expression
|
|
1164
1419
|
)
|
|
1165
|
-
return (
|
|
1166
|
-
subtype_asset,
|
|
1167
|
-
new_expr_chain,
|
|
1168
|
-
attack_step
|
|
1169
|
-
)
|
|
1170
|
-
|
|
1171
1420
|
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
|
|
1421
|
+
result = self.process_collect_step_expression(
|
|
1422
|
+
target_asset, expr_chain, step_expression
|
|
1201
1423
|
)
|
|
1202
|
-
|
|
1203
1424
|
case _:
|
|
1204
|
-
|
|
1205
|
-
'Unknown attack step type: "
|
|
1425
|
+
raise LookupError(
|
|
1426
|
+
f'Unknown attack step type: "{step_expression["type"]}"'
|
|
1206
1427
|
)
|
|
1207
|
-
|
|
1208
|
-
|
|
1428
|
+
return result
|
|
1209
1429
|
|
|
1210
1430
|
def reverse_expr_chain(
|
|
1211
1431
|
self,
|
|
@@ -1300,7 +1520,7 @@ class LanguageGraph():
|
|
|
1300
1520
|
logger.error(msg, expr_chain.type)
|
|
1301
1521
|
raise LanguageGraphAssociationError(msg % expr_chain.type)
|
|
1302
1522
|
|
|
1303
|
-
def _resolve_variable(self, asset, var_name) -> tuple:
|
|
1523
|
+
def _resolve_variable(self, asset: LanguageGraphAsset, var_name) -> tuple:
|
|
1304
1524
|
"""
|
|
1305
1525
|
Resolve a variable for a specific asset by variable name.
|
|
1306
1526
|
|
|
@@ -1323,35 +1543,75 @@ class LanguageGraph():
|
|
|
1323
1543
|
return (target_asset, expr_chain)
|
|
1324
1544
|
return asset.variables[var_name]
|
|
1325
1545
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1546
|
+
def _create_associations_for_assets(
|
|
1547
|
+
self,
|
|
1548
|
+
lang_spec: dict[str, Any],
|
|
1549
|
+
assets: dict[str, LanguageGraphAsset]
|
|
1550
|
+
) -> None:
|
|
1551
|
+
""" Link associations to assets based on the language specification.
|
|
1552
|
+
Arguments:
|
|
1553
|
+
lang_spec - the language specification dictionary
|
|
1554
|
+
assets - a dictionary of LanguageGraphAsset objects
|
|
1555
|
+
indexed by their names
|
|
1331
1556
|
"""
|
|
1332
|
-
|
|
1333
|
-
for
|
|
1557
|
+
|
|
1558
|
+
for association_dict in lang_spec['associations']:
|
|
1334
1559
|
logger.debug(
|
|
1335
|
-
'Create
|
|
1336
|
-
|
|
1560
|
+
'Create association language graph nodes for association %s',
|
|
1561
|
+
association_dict['name']
|
|
1337
1562
|
)
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1563
|
+
|
|
1564
|
+
left_asset_name = association_dict['leftAsset']
|
|
1565
|
+
right_asset_name = association_dict['rightAsset']
|
|
1566
|
+
|
|
1567
|
+
if left_asset_name not in assets:
|
|
1568
|
+
raise LanguageGraphAssociationError(
|
|
1569
|
+
f'Left asset "{left_asset_name}" for '
|
|
1570
|
+
f'association "{association_dict["name"]}" not found!'
|
|
1571
|
+
)
|
|
1572
|
+
if right_asset_name not in assets:
|
|
1573
|
+
raise LanguageGraphAssociationError(
|
|
1574
|
+
f'Right asset "{right_asset_name}" for '
|
|
1575
|
+
f'association "{association_dict["name"]}" not found!'
|
|
1576
|
+
)
|
|
1577
|
+
|
|
1578
|
+
left_asset = assets[left_asset_name]
|
|
1579
|
+
right_asset = assets[right_asset_name]
|
|
1580
|
+
|
|
1581
|
+
assoc_node = LanguageGraphAssociation(
|
|
1582
|
+
name = association_dict['name'],
|
|
1583
|
+
left_field = LanguageGraphAssociationField(
|
|
1584
|
+
left_asset,
|
|
1585
|
+
association_dict['leftField'],
|
|
1586
|
+
association_dict['leftMultiplicity']['min'],
|
|
1587
|
+
association_dict['leftMultiplicity']['max']
|
|
1588
|
+
),
|
|
1589
|
+
right_field = LanguageGraphAssociationField(
|
|
1590
|
+
right_asset,
|
|
1591
|
+
association_dict['rightField'],
|
|
1592
|
+
association_dict['rightMultiplicity']['min'],
|
|
1593
|
+
association_dict['rightMultiplicity']['max']
|
|
1594
|
+
),
|
|
1595
|
+
info = association_dict['meta']
|
|
1347
1596
|
)
|
|
1348
|
-
self.assets[asset_dict['name']] = asset_node
|
|
1349
1597
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1598
|
+
# Add the association to the left and right asset
|
|
1599
|
+
self._link_association_to_assets(
|
|
1600
|
+
assoc_node, left_asset, right_asset
|
|
1601
|
+
)
|
|
1602
|
+
|
|
1603
|
+
def _link_assets(
|
|
1604
|
+
self,
|
|
1605
|
+
lang_spec: dict[str, Any],
|
|
1606
|
+
assets: dict[str, LanguageGraphAsset]
|
|
1607
|
+
) -> None:
|
|
1608
|
+
"""
|
|
1609
|
+
Link assets based on inheritance and associations.
|
|
1610
|
+
"""
|
|
1611
|
+
for asset_dict in lang_spec['assets']:
|
|
1612
|
+
asset = assets[asset_dict['name']]
|
|
1353
1613
|
if asset_dict['superAsset']:
|
|
1354
|
-
super_asset =
|
|
1614
|
+
super_asset = assets[asset_dict['superAsset']]
|
|
1355
1615
|
if not super_asset:
|
|
1356
1616
|
msg = 'Failed to find super asset "%s" for asset "%s"!'
|
|
1357
1617
|
logger.error(
|
|
@@ -1362,54 +1622,21 @@ class LanguageGraph():
|
|
|
1362
1622
|
super_asset.own_sub_assets.add(asset)
|
|
1363
1623
|
asset.own_super_asset = super_asset
|
|
1364
1624
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1625
|
+
def _set_variables_for_assets(
|
|
1626
|
+
self, assets: dict[str, LanguageGraphAsset]
|
|
1627
|
+
) -> None:
|
|
1628
|
+
""" Set the variables for each asset based on the language specification.
|
|
1629
|
+
Arguments:
|
|
1630
|
+
assets - a dictionary of LanguageGraphAsset objects
|
|
1631
|
+
indexed by their names
|
|
1632
|
+
"""
|
|
1633
|
+
|
|
1634
|
+
for asset in assets.values():
|
|
1367
1635
|
logger.debug(
|
|
1368
|
-
'
|
|
1369
|
-
asset.name
|
|
1636
|
+
'Set variables for asset %s', asset.name
|
|
1370
1637
|
)
|
|
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):
|
|
1638
|
+
variables = self._get_variables_for_asset_type(asset.name)
|
|
1639
|
+
for variable in variables:
|
|
1413
1640
|
if logger.isEnabledFor(logging.DEBUG):
|
|
1414
1641
|
# Avoid running json.dumps when not in debug
|
|
1415
1642
|
logger.debug(
|
|
@@ -1418,9 +1645,13 @@ class LanguageGraph():
|
|
|
1418
1645
|
)
|
|
1419
1646
|
self._resolve_variable(asset, variable['name'])
|
|
1420
1647
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
for asset
|
|
1648
|
+
def _generate_attack_steps(self, assets) -> None:
|
|
1649
|
+
"""
|
|
1650
|
+
Generate all of the attack steps for each asset type
|
|
1651
|
+
based on the language specification.
|
|
1652
|
+
"""
|
|
1653
|
+
langspec_dict = {}
|
|
1654
|
+
for asset in assets.values():
|
|
1424
1655
|
logger.debug(
|
|
1425
1656
|
'Create attack steps language graph nodes for asset %s',
|
|
1426
1657
|
asset.name
|
|
@@ -1432,28 +1663,32 @@ class LanguageGraph():
|
|
|
1432
1663
|
attack_step_attribs['name']
|
|
1433
1664
|
)
|
|
1434
1665
|
|
|
1666
|
+
ttc = self.replace_if_predef_ttc(attack_step_attribs['ttc'])
|
|
1435
1667
|
attack_step_node = LanguageGraphAttackStep(
|
|
1436
1668
|
name = attack_step_attribs['name'],
|
|
1437
1669
|
type = attack_step_attribs['type'],
|
|
1438
1670
|
asset = asset,
|
|
1439
|
-
ttc =
|
|
1440
|
-
overrides =
|
|
1441
|
-
|
|
1671
|
+
ttc = ttc,
|
|
1672
|
+
overrides = (
|
|
1673
|
+
attack_step_attribs['reaches']['overrides']
|
|
1674
|
+
if attack_step_attribs['reaches'] else False
|
|
1675
|
+
),
|
|
1442
1676
|
children = {},
|
|
1443
1677
|
parents = {},
|
|
1444
1678
|
info = attack_step_attribs['meta'],
|
|
1445
1679
|
tags = set(attack_step_attribs['tags'])
|
|
1446
1680
|
)
|
|
1447
|
-
attack_step_node.
|
|
1448
|
-
|
|
1681
|
+
langspec_dict[attack_step_node.full_name] = \
|
|
1682
|
+
attack_step_attribs
|
|
1683
|
+
asset.attack_steps[attack_step_node.name] = \
|
|
1449
1684
|
attack_step_node
|
|
1450
1685
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1686
|
+
detectors: dict = attack_step_attribs.get("detectors", {})
|
|
1687
|
+
for detector in detectors.values():
|
|
1453
1688
|
attack_step_node.detectors[detector["name"]] = Detector(
|
|
1454
1689
|
context=Context(
|
|
1455
1690
|
{
|
|
1456
|
-
label:
|
|
1691
|
+
label: assets[asset]
|
|
1457
1692
|
for label, asset in detector["context"].items()
|
|
1458
1693
|
}
|
|
1459
1694
|
),
|
|
@@ -1508,13 +1743,15 @@ class LanguageGraph():
|
|
|
1508
1743
|
attack_step.name
|
|
1509
1744
|
)
|
|
1510
1745
|
|
|
1511
|
-
if attack_step.
|
|
1746
|
+
if attack_step.full_name not in langspec_dict:
|
|
1512
1747
|
# This is simply an empty inherited attack step
|
|
1513
1748
|
continue
|
|
1514
1749
|
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1750
|
+
langspec_entry = langspec_dict[attack_step.full_name]
|
|
1751
|
+
step_expressions = (
|
|
1752
|
+
langspec_entry['reaches']['stepExpressions']
|
|
1753
|
+
if langspec_entry['reaches'] else []
|
|
1754
|
+
)
|
|
1518
1755
|
|
|
1519
1756
|
for step_expression in step_expressions:
|
|
1520
1757
|
# Resolve each of the attack step expressions listed for
|
|
@@ -1549,65 +1786,94 @@ class LanguageGraph():
|
|
|
1549
1786
|
target_attack_step_name]
|
|
1550
1787
|
|
|
1551
1788
|
# Link to the children target attack steps
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
[(target_attack_step, expr_chain)]
|
|
1789
|
+
attack_step.children.setdefault(target_attack_step.full_name, [])
|
|
1790
|
+
attack_step.children[target_attack_step.full_name].append(
|
|
1791
|
+
(target_attack_step, expr_chain)
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1558
1794
|
# Reverse the children associations chains to get the
|
|
1559
1795
|
# 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))]
|
|
1796
|
+
target_attack_step.parents.setdefault(attack_step.full_name, [])
|
|
1797
|
+
target_attack_step.parents[attack_step.full_name].append(
|
|
1798
|
+
(attack_step, self.reverse_expr_chain(expr_chain, None))
|
|
1799
|
+
)
|
|
1570
1800
|
|
|
1571
1801
|
# Evaluate the requirements of exist and notExist attack steps
|
|
1572
|
-
if attack_step.type
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1802
|
+
if attack_step.type in ('exist', 'notExist'):
|
|
1803
|
+
step_expressions = (
|
|
1804
|
+
langspec_entry['requires']['stepExpressions']
|
|
1805
|
+
if langspec_entry['requires'] else []
|
|
1806
|
+
)
|
|
1577
1807
|
if not step_expressions:
|
|
1578
|
-
msg = 'Failed to find requirements for attack step' \
|
|
1579
|
-
' "%s" of type "%s":\n%s'
|
|
1580
1808
|
raise LanguageGraphStepExpressionError(
|
|
1581
|
-
|
|
1809
|
+
'Failed to find requirements for attack step'
|
|
1810
|
+
' "%s" of type "%s":\n%s' % (
|
|
1582
1811
|
attack_step.name,
|
|
1583
1812
|
attack_step.type,
|
|
1584
|
-
json.dumps(
|
|
1813
|
+
json.dumps(langspec_entry, indent = 2)
|
|
1585
1814
|
)
|
|
1586
1815
|
)
|
|
1587
1816
|
|
|
1588
|
-
attack_step.own_requires = []
|
|
1589
1817
|
for step_expression in step_expressions:
|
|
1590
|
-
_, \
|
|
1591
|
-
result_expr_chain, \
|
|
1592
|
-
_ = \
|
|
1818
|
+
_, result_expr_chain, _ = \
|
|
1593
1819
|
self.process_step_expression(
|
|
1594
1820
|
attack_step.asset,
|
|
1595
1821
|
None,
|
|
1596
1822
|
step_expression
|
|
1597
1823
|
)
|
|
1824
|
+
if result_expr_chain is None:
|
|
1825
|
+
raise LanguageGraphException('Failed to find '
|
|
1826
|
+
'existence step requirement for step '
|
|
1827
|
+
f'expression:\n%s' % step_expression)
|
|
1598
1828
|
attack_step.own_requires.append(result_expr_chain)
|
|
1599
1829
|
|
|
1600
|
-
def
|
|
1830
|
+
def _generate_graph(self) -> None:
|
|
1831
|
+
"""
|
|
1832
|
+
Generate language graph starting from the MAL language specification
|
|
1833
|
+
given in the constructor.
|
|
1834
|
+
"""
|
|
1835
|
+
# Generate all of the asset nodes of the language graph.
|
|
1836
|
+
self.assets = {}
|
|
1837
|
+
for asset_dict in self._lang_spec['assets']:
|
|
1838
|
+
logger.debug(
|
|
1839
|
+
'Create asset language graph nodes for asset %s',
|
|
1840
|
+
asset_dict['name']
|
|
1841
|
+
)
|
|
1842
|
+
asset_node = LanguageGraphAsset(
|
|
1843
|
+
name = asset_dict['name'],
|
|
1844
|
+
own_associations = {},
|
|
1845
|
+
attack_steps = {},
|
|
1846
|
+
info = asset_dict['meta'],
|
|
1847
|
+
own_super_asset = None,
|
|
1848
|
+
own_sub_assets = set(),
|
|
1849
|
+
own_variables = {},
|
|
1850
|
+
is_abstract = asset_dict['isAbstract']
|
|
1851
|
+
)
|
|
1852
|
+
self.assets[asset_dict['name']] = asset_node
|
|
1853
|
+
|
|
1854
|
+
# Link assets to each other
|
|
1855
|
+
self._link_assets(self._lang_spec, self.assets)
|
|
1856
|
+
|
|
1857
|
+
# Add and link associations to assets
|
|
1858
|
+
self._create_associations_for_assets(self._lang_spec, self.assets)
|
|
1859
|
+
|
|
1860
|
+
# Set the variables for each asset
|
|
1861
|
+
self._set_variables_for_assets(self.assets)
|
|
1862
|
+
|
|
1863
|
+
# Add attack steps to the assets
|
|
1864
|
+
self._generate_attack_steps(self.assets)
|
|
1865
|
+
|
|
1866
|
+
def _get_attacks_for_asset_type(self, asset_type: str) -> dict[str, dict]:
|
|
1601
1867
|
"""
|
|
1602
|
-
Get all Attack Steps for a specific
|
|
1868
|
+
Get all Attack Steps for a specific asset type.
|
|
1603
1869
|
|
|
1604
1870
|
Arguments:
|
|
1605
|
-
asset_type -
|
|
1606
|
-
list the possible attack steps
|
|
1871
|
+
asset_type - the name of the asset type we want to
|
|
1872
|
+
list the possible attack steps for
|
|
1607
1873
|
|
|
1608
1874
|
Return:
|
|
1609
|
-
A dictionary
|
|
1610
|
-
specified
|
|
1875
|
+
A dictionary containing the possible attacks for the
|
|
1876
|
+
specified asset type. Each key in the dictionary is an attack name
|
|
1611
1877
|
associated with a dictionary containing other characteristics of the
|
|
1612
1878
|
attack such as type of attack, TTC distribution, child attack steps
|
|
1613
1879
|
and other information
|
|
@@ -1634,20 +1900,18 @@ class LanguageGraph():
|
|
|
1634
1900
|
|
|
1635
1901
|
return attack_steps
|
|
1636
1902
|
|
|
1637
|
-
def _get_associations_for_asset_type(self, asset_type: str) -> list:
|
|
1903
|
+
def _get_associations_for_asset_type(self, asset_type: str) -> list[dict]:
|
|
1638
1904
|
"""
|
|
1639
|
-
Get all
|
|
1905
|
+
Get all associations for a specific asset type.
|
|
1640
1906
|
|
|
1641
1907
|
Arguments:
|
|
1642
|
-
asset_type -
|
|
1908
|
+
asset_type - the name of the asset type for which we want to
|
|
1643
1909
|
list the associations
|
|
1644
1910
|
|
|
1645
1911
|
Return:
|
|
1646
|
-
A
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
as type of attack, TTC distribution, child attack steps and other
|
|
1650
|
-
information
|
|
1912
|
+
A list of dicts, where each dict represents an associations
|
|
1913
|
+
for the specified asset type. Each dictionary contains
|
|
1914
|
+
name and meta information about the association.
|
|
1651
1915
|
"""
|
|
1652
1916
|
logger.debug(
|
|
1653
1917
|
'Get associations for %s asset from '
|
|
@@ -1668,25 +1932,25 @@ class LanguageGraph():
|
|
|
1668
1932
|
if assoc['leftAsset'] == asset_type or \
|
|
1669
1933
|
assoc['rightAsset'] == asset_type)
|
|
1670
1934
|
assoc = next(assoc_iter, None)
|
|
1671
|
-
while
|
|
1935
|
+
while assoc:
|
|
1672
1936
|
associations.append(assoc)
|
|
1673
1937
|
assoc = next(assoc_iter, None)
|
|
1674
1938
|
|
|
1675
1939
|
return associations
|
|
1676
1940
|
|
|
1677
1941
|
def _get_variables_for_asset_type(
|
|
1678
|
-
self, asset_type: str) -> dict:
|
|
1942
|
+
self, asset_type: str) -> list[dict]:
|
|
1679
1943
|
"""
|
|
1680
|
-
Get
|
|
1944
|
+
Get variables for a specific asset type.
|
|
1681
1945
|
Note: Variables are the ones specified in MAL through `let` statements
|
|
1682
1946
|
|
|
1683
1947
|
Arguments:
|
|
1684
|
-
asset_type - a string representing the type
|
|
1948
|
+
asset_type - a string representing the asset type which
|
|
1685
1949
|
contains the variables
|
|
1686
1950
|
|
|
1687
1951
|
Return:
|
|
1688
|
-
A
|
|
1689
|
-
|
|
1952
|
+
A list of dicts representing the step expressions for the variables
|
|
1953
|
+
belonging to the asset.
|
|
1690
1954
|
"""
|
|
1691
1955
|
|
|
1692
1956
|
asset_dict = next((asset for asset in self._lang_spec['assets'] \
|
|
@@ -1733,5 +1997,3 @@ class LanguageGraph():
|
|
|
1733
1997
|
|
|
1734
1998
|
self.assets = {}
|
|
1735
1999
|
self._generate_graph()
|
|
1736
|
-
|
|
1737
|
-
|