mal-toolbox 2.0.0__py3-none-any.whl → 2.1.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-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/METADATA +2 -2
- mal_toolbox-2.1.0.dist-info/RECORD +51 -0
- {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/WHEEL +1 -1
- maltoolbox/__init__.py +2 -2
- maltoolbox/attackgraph/__init__.py +2 -2
- maltoolbox/attackgraph/attackgraph.py +121 -549
- maltoolbox/attackgraph/factories.py +68 -0
- maltoolbox/attackgraph/file_utils.py +0 -0
- maltoolbox/attackgraph/generate.py +338 -0
- maltoolbox/attackgraph/node_getters.py +36 -0
- maltoolbox/attackgraph/ttcs.py +28 -0
- maltoolbox/language/__init__.py +2 -2
- maltoolbox/language/compiler/mal_compiler.py +4 -3
- maltoolbox/language/detector.py +43 -0
- maltoolbox/language/expression_chain.py +218 -0
- maltoolbox/language/language_graph_asset.py +180 -0
- maltoolbox/language/language_graph_assoc.py +147 -0
- maltoolbox/language/language_graph_attack_step.py +129 -0
- maltoolbox/language/language_graph_builder.py +282 -0
- maltoolbox/language/language_graph_loaders.py +7 -0
- maltoolbox/language/language_graph_lookup.py +140 -0
- maltoolbox/language/language_graph_serialization.py +5 -0
- maltoolbox/language/languagegraph.py +244 -1537
- maltoolbox/language/step_expression_processor.py +491 -0
- mal_toolbox-2.0.0.dist-info/RECORD +0 -36
- {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/entry_points.txt +0 -0
- {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/AUTHORS +0 -0
- {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from maltoolbox.exceptions import LanguageGraphAssociationError, LanguageGraphException, LanguageGraphStepExpressionError, LanguageGraphSuperAssetNotFoundError
|
|
7
|
+
from maltoolbox.language.detector import Context, Detector
|
|
8
|
+
from maltoolbox.language.language_graph_lookup import get_attacks_for_asset_type, get_variables_for_asset_type
|
|
9
|
+
from maltoolbox.language.language_graph_asset import LanguageGraphAsset
|
|
10
|
+
from maltoolbox.language.language_graph_assoc import LanguageGraphAssociation, LanguageGraphAssociationField, link_association_to_assets
|
|
11
|
+
from maltoolbox.language.language_graph_attack_step import LanguageGraphAttackStep
|
|
12
|
+
from maltoolbox.language.step_expression_processor import process_step_expression, resolve_variable, reverse_expr_chain
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def generate_graph(lang_spec) -> dict[str, LanguageGraphAsset]:
|
|
19
|
+
"""Generate language graph starting from the MAL language specification
|
|
20
|
+
given in the constructor."""
|
|
21
|
+
# Generate all of the asset nodes of the language graph.
|
|
22
|
+
assets = {}
|
|
23
|
+
for asset_dict in lang_spec['assets']:
|
|
24
|
+
logger.debug(
|
|
25
|
+
'Create asset language graph nodes for asset %s',
|
|
26
|
+
asset_dict['name']
|
|
27
|
+
)
|
|
28
|
+
asset_node = LanguageGraphAsset(
|
|
29
|
+
name=asset_dict['name'],
|
|
30
|
+
own_associations={},
|
|
31
|
+
attack_steps={},
|
|
32
|
+
info=asset_dict['meta'],
|
|
33
|
+
own_super_asset=None,
|
|
34
|
+
own_sub_assets=list(),
|
|
35
|
+
own_variables={},
|
|
36
|
+
is_abstract=asset_dict['isAbstract']
|
|
37
|
+
)
|
|
38
|
+
assets[asset_dict['name']] = asset_node
|
|
39
|
+
|
|
40
|
+
# Link assets to each other
|
|
41
|
+
link_assets(lang_spec, assets)
|
|
42
|
+
|
|
43
|
+
# Add and link associations to assets
|
|
44
|
+
create_associations_for_assets(lang_spec, assets)
|
|
45
|
+
|
|
46
|
+
# Set the variables for each asset
|
|
47
|
+
set_variables_for_assets(assets, lang_spec)
|
|
48
|
+
|
|
49
|
+
# Add attack steps to the assets
|
|
50
|
+
generate_attack_steps(assets, lang_spec)
|
|
51
|
+
|
|
52
|
+
return assets
|
|
53
|
+
|
|
54
|
+
def link_assets(
|
|
55
|
+
lang_spec: dict[str, Any],
|
|
56
|
+
assets: dict[str, LanguageGraphAsset]
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Link assets based on inheritance and associations."""
|
|
59
|
+
for asset_dict in lang_spec['assets']:
|
|
60
|
+
asset = assets[asset_dict['name']]
|
|
61
|
+
if asset_dict['superAsset']:
|
|
62
|
+
super_asset = assets[asset_dict['superAsset']]
|
|
63
|
+
if not super_asset:
|
|
64
|
+
msg = 'Failed to find super asset "%s" for asset "%s"!'
|
|
65
|
+
logger.error(
|
|
66
|
+
msg, asset_dict["superAsset"], asset_dict["name"])
|
|
67
|
+
raise LanguageGraphSuperAssetNotFoundError(
|
|
68
|
+
msg % (asset_dict["superAsset"], asset_dict["name"]))
|
|
69
|
+
|
|
70
|
+
super_asset.own_sub_assets.append(asset)
|
|
71
|
+
asset.own_super_asset = super_asset
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def set_variables_for_assets(assets: dict[str, LanguageGraphAsset], lang_spec) -> None:
|
|
75
|
+
"""Set the variables for each asset based on the language specification.
|
|
76
|
+
|
|
77
|
+
Arguments:
|
|
78
|
+
---------
|
|
79
|
+
assets - a dictionary of LanguageGraphAsset objects
|
|
80
|
+
indexed by their names
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
for asset in assets.values():
|
|
84
|
+
logger.debug(
|
|
85
|
+
'Set variables for asset %s', asset.name
|
|
86
|
+
)
|
|
87
|
+
variables = get_variables_for_asset_type(asset.name, lang_spec)
|
|
88
|
+
for variable in variables:
|
|
89
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
90
|
+
# Avoid running json.dumps when not in debug
|
|
91
|
+
logger.debug(
|
|
92
|
+
'Processing Variable Expression:\n%s',
|
|
93
|
+
json.dumps(variable, indent=2)
|
|
94
|
+
)
|
|
95
|
+
resolve_variable(assets, asset, variable['name'], lang_spec)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def generate_attack_steps(assets: dict[str, LanguageGraphAsset], lang_spec: dict) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Generate attack steps for all assets and link them according to the
|
|
101
|
+
language specification.
|
|
102
|
+
|
|
103
|
+
This method performs three phases:
|
|
104
|
+
|
|
105
|
+
1. Create attack step nodes for each asset, including detectors.
|
|
106
|
+
2. Inherit attack steps from super-assets, respecting overrides.
|
|
107
|
+
3. Link attack steps via 'reaches' and evaluate 'exist'/'notExist'
|
|
108
|
+
requirements.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
assets (dict): Mapping of asset names to asset objects.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
LanguageGraphStepExpressionError: If a step expression cannot be
|
|
115
|
+
resolved to a target asset or attack step.
|
|
116
|
+
LanguageGraphException: If an existence requirement cannot be
|
|
117
|
+
resolved.
|
|
118
|
+
"""
|
|
119
|
+
langspec_dict = {}
|
|
120
|
+
|
|
121
|
+
for asset in assets.values():
|
|
122
|
+
logger.debug('Create attack steps language graph nodes for asset %s', asset.name)
|
|
123
|
+
for step_dict in get_attacks_for_asset_type(asset.name, lang_spec).values():
|
|
124
|
+
logger.debug(
|
|
125
|
+
'Create attack step language graph nodes for %s', step_dict['name']
|
|
126
|
+
)
|
|
127
|
+
node = LanguageGraphAttackStep(
|
|
128
|
+
name=step_dict['name'],
|
|
129
|
+
type=step_dict['type'],
|
|
130
|
+
asset=asset,
|
|
131
|
+
causal_mode=step_dict.get('causal_mode'),
|
|
132
|
+
ttc=step_dict['ttc'],
|
|
133
|
+
overrides=(
|
|
134
|
+
step_dict['reaches']['overrides']
|
|
135
|
+
if step_dict['reaches'] else False
|
|
136
|
+
),
|
|
137
|
+
own_children={}, own_parents={},
|
|
138
|
+
info=step_dict['meta'],
|
|
139
|
+
tags=list(step_dict['tags'])
|
|
140
|
+
)
|
|
141
|
+
langspec_dict[node.full_name] = step_dict
|
|
142
|
+
asset.attack_steps[node.name] = node
|
|
143
|
+
|
|
144
|
+
for det in step_dict.get('detectors', {}).values():
|
|
145
|
+
node.detectors[det['name']] = Detector(
|
|
146
|
+
context=Context(
|
|
147
|
+
{lbl: assets[a] for lbl, a in det['context'].items()}
|
|
148
|
+
),
|
|
149
|
+
name=det.get('name'),
|
|
150
|
+
type=det.get('type'),
|
|
151
|
+
tprate=det.get('tprate'),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
pending = list(assets.values())
|
|
155
|
+
while pending:
|
|
156
|
+
asset = pending.pop(0)
|
|
157
|
+
super_asset = asset.own_super_asset
|
|
158
|
+
if super_asset in pending:
|
|
159
|
+
# Super asset still needs processing, defer this asset
|
|
160
|
+
pending.append(asset)
|
|
161
|
+
continue
|
|
162
|
+
if not super_asset:
|
|
163
|
+
continue
|
|
164
|
+
for super_step in super_asset.attack_steps.values():
|
|
165
|
+
current_step = asset.attack_steps.get(super_step.name)
|
|
166
|
+
if not current_step:
|
|
167
|
+
node = LanguageGraphAttackStep(
|
|
168
|
+
name=super_step.name,
|
|
169
|
+
type=super_step.type,
|
|
170
|
+
asset=asset,
|
|
171
|
+
causal_mode=super_step.causal_mode,
|
|
172
|
+
ttc=super_step.ttc,
|
|
173
|
+
overrides=False,
|
|
174
|
+
own_children={},
|
|
175
|
+
own_parents={},
|
|
176
|
+
info=super_step.info,
|
|
177
|
+
tags=list(super_step.tags)
|
|
178
|
+
)
|
|
179
|
+
node.inherits = super_step
|
|
180
|
+
asset.attack_steps[super_step.name] = node
|
|
181
|
+
elif current_step.overrides:
|
|
182
|
+
continue
|
|
183
|
+
else:
|
|
184
|
+
current_step.inherits = super_step
|
|
185
|
+
current_step.tags += super_step.tags
|
|
186
|
+
current_step.info |= super_step.info
|
|
187
|
+
|
|
188
|
+
for asset in assets.values():
|
|
189
|
+
for step in asset.attack_steps.values():
|
|
190
|
+
logger.debug('Determining children for attack step %s', step.name)
|
|
191
|
+
if step.full_name not in langspec_dict:
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
entry = langspec_dict[step.full_name]
|
|
195
|
+
for expr in (entry['reaches']['stepExpressions'] if entry['reaches'] else []):
|
|
196
|
+
tgt_asset, chain, tgt_name = process_step_expression(assets, step.asset, None, expr, lang_spec)
|
|
197
|
+
if not tgt_asset:
|
|
198
|
+
raise LanguageGraphStepExpressionError(
|
|
199
|
+
'Failed to find target asset for:\n%s' % json.dumps(expr, indent=2)
|
|
200
|
+
)
|
|
201
|
+
if tgt_name not in tgt_asset.attack_steps:
|
|
202
|
+
raise LanguageGraphStepExpressionError(
|
|
203
|
+
'Failed to find target attack step %s on %s:\n%s' %
|
|
204
|
+
(tgt_name, tgt_asset.name, json.dumps(expr, indent=2))
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
tgt = tgt_asset.attack_steps[tgt_name]
|
|
208
|
+
step.own_children.setdefault(tgt, []).append(chain)
|
|
209
|
+
tgt.own_parents.setdefault(step, []).append(reverse_expr_chain(chain, None))
|
|
210
|
+
|
|
211
|
+
if step.type in ('exist', 'notExist'):
|
|
212
|
+
reqs = entry['requires']['stepExpressions'] if entry['requires'] else []
|
|
213
|
+
if not reqs:
|
|
214
|
+
raise LanguageGraphStepExpressionError(
|
|
215
|
+
'Missing requirements for "%s" of type "%s":\n%s' %
|
|
216
|
+
(step.name, step.type, json.dumps(entry, indent=2))
|
|
217
|
+
)
|
|
218
|
+
for expr in reqs:
|
|
219
|
+
_, chain, _ = process_step_expression(assets, step.asset, None, expr, lang_spec)
|
|
220
|
+
if chain is None:
|
|
221
|
+
raise LanguageGraphException(
|
|
222
|
+
f'Failed to find existence step requirement for:\n{expr}'
|
|
223
|
+
)
|
|
224
|
+
step.own_requires.append(chain)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def create_associations_for_assets(
|
|
228
|
+
lang_spec: dict[str, Any], assets: dict[str, LanguageGraphAsset]
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Link associations to assets based on the language specification.
|
|
231
|
+
|
|
232
|
+
Arguments:
|
|
233
|
+
---------
|
|
234
|
+
lang_spec - the language specification dictionary
|
|
235
|
+
assets - a dictionary of LanguageGraphAsset objects
|
|
236
|
+
indexed by their names
|
|
237
|
+
|
|
238
|
+
"""
|
|
239
|
+
for association_dict in lang_spec['associations']:
|
|
240
|
+
logger.debug(
|
|
241
|
+
'Create association language graph nodes for association %s',
|
|
242
|
+
association_dict['name']
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
left_asset_name = association_dict['leftAsset']
|
|
246
|
+
right_asset_name = association_dict['rightAsset']
|
|
247
|
+
|
|
248
|
+
if left_asset_name not in assets:
|
|
249
|
+
raise LanguageGraphAssociationError(
|
|
250
|
+
f'Left asset "{left_asset_name}" for '
|
|
251
|
+
f'association "{association_dict["name"]}" not found!'
|
|
252
|
+
)
|
|
253
|
+
if right_asset_name not in assets:
|
|
254
|
+
raise LanguageGraphAssociationError(
|
|
255
|
+
f'Right asset "{right_asset_name}" for '
|
|
256
|
+
f'association "{association_dict["name"]}" not found!'
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
left_asset = assets[left_asset_name]
|
|
260
|
+
right_asset = assets[right_asset_name]
|
|
261
|
+
|
|
262
|
+
assoc_node = LanguageGraphAssociation(
|
|
263
|
+
name=association_dict['name'],
|
|
264
|
+
left_field=LanguageGraphAssociationField(
|
|
265
|
+
left_asset,
|
|
266
|
+
association_dict['leftField'],
|
|
267
|
+
association_dict['leftMultiplicity']['min'],
|
|
268
|
+
association_dict['leftMultiplicity']['max']
|
|
269
|
+
),
|
|
270
|
+
right_field=LanguageGraphAssociationField(
|
|
271
|
+
right_asset,
|
|
272
|
+
association_dict['rightField'],
|
|
273
|
+
association_dict['rightMultiplicity']['min'],
|
|
274
|
+
association_dict['rightMultiplicity']['max']
|
|
275
|
+
),
|
|
276
|
+
info=association_dict['meta']
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Add the association to the left and right asset
|
|
280
|
+
link_association_to_assets(
|
|
281
|
+
assoc_node, left_asset, right_asset
|
|
282
|
+
)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from maltoolbox.exceptions import LanguageGraphException
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
def get_attacks_for_asset_type(asset_type: str, lang_spec) -> dict[str, dict]:
|
|
10
|
+
"""Get all Attack Steps for a specific asset type.
|
|
11
|
+
|
|
12
|
+
Arguments:
|
|
13
|
+
---------
|
|
14
|
+
asset_type - the name of the asset type we want to
|
|
15
|
+
list the possible attack steps for
|
|
16
|
+
|
|
17
|
+
Return:
|
|
18
|
+
------
|
|
19
|
+
A dictionary containing the possible attacks for the
|
|
20
|
+
specified asset type. Each key in the dictionary is an attack name
|
|
21
|
+
associated with a dictionary containing other characteristics of the
|
|
22
|
+
attack such as type of attack, TTC distribution, child attack steps
|
|
23
|
+
and other information
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
attack_steps: dict = {}
|
|
27
|
+
try:
|
|
28
|
+
asset = next(
|
|
29
|
+
asset for asset in lang_spec['assets']
|
|
30
|
+
if asset['name'] == asset_type
|
|
31
|
+
)
|
|
32
|
+
except StopIteration:
|
|
33
|
+
logger.error(
|
|
34
|
+
'Failed to find asset type %s when looking'
|
|
35
|
+
'for attack steps.', asset_type
|
|
36
|
+
)
|
|
37
|
+
return attack_steps
|
|
38
|
+
|
|
39
|
+
logger.debug(
|
|
40
|
+
'Get attack steps for %s asset from '
|
|
41
|
+
'language specification.', asset['name']
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
attack_steps = {step['name']: step for step in asset['attackSteps']}
|
|
45
|
+
|
|
46
|
+
return attack_steps
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_associations_for_asset_type(asset_type: str, lang_spec) -> list[dict]:
|
|
50
|
+
"""Get all associations for a specific asset type.
|
|
51
|
+
|
|
52
|
+
Arguments:
|
|
53
|
+
---------
|
|
54
|
+
asset_type - the name of the asset type for which we want to
|
|
55
|
+
list the associations
|
|
56
|
+
|
|
57
|
+
Return:
|
|
58
|
+
------
|
|
59
|
+
A list of dicts, where each dict represents an associations
|
|
60
|
+
for the specified asset type. Each dictionary contains
|
|
61
|
+
name and meta information about the association.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
logger.debug(
|
|
65
|
+
'Get associations for %s asset from '
|
|
66
|
+
'language specification.', asset_type
|
|
67
|
+
)
|
|
68
|
+
associations: list = []
|
|
69
|
+
|
|
70
|
+
asset = next((asset for asset in lang_spec['assets']
|
|
71
|
+
if asset['name'] == asset_type), None)
|
|
72
|
+
if not asset:
|
|
73
|
+
logger.error(
|
|
74
|
+
'Failed to find asset type %s when '
|
|
75
|
+
'looking for associations.', asset_type
|
|
76
|
+
)
|
|
77
|
+
return associations
|
|
78
|
+
|
|
79
|
+
assoc_iter = (assoc for assoc in lang_spec['associations']
|
|
80
|
+
if assoc['leftAsset'] == asset_type or
|
|
81
|
+
assoc['rightAsset'] == asset_type)
|
|
82
|
+
assoc = next(assoc_iter, None)
|
|
83
|
+
while assoc:
|
|
84
|
+
associations.append(assoc)
|
|
85
|
+
assoc = next(assoc_iter, None)
|
|
86
|
+
|
|
87
|
+
return associations
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_variables_for_asset_type(asset_type: str, lang_spec) -> list[dict]:
|
|
91
|
+
"""Get variables for a specific asset type.
|
|
92
|
+
Note: Variables are the ones specified in MAL through `let` statements
|
|
93
|
+
|
|
94
|
+
Arguments:
|
|
95
|
+
---------
|
|
96
|
+
asset_type - a string representing the asset type which
|
|
97
|
+
contains the variables
|
|
98
|
+
|
|
99
|
+
Return:
|
|
100
|
+
------
|
|
101
|
+
A list of dicts representing the step expressions for the variables
|
|
102
|
+
belonging to the asset.
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
asset_dict = next((asset for asset in lang_spec['assets']
|
|
106
|
+
if asset['name'] == asset_type), None)
|
|
107
|
+
if not asset_dict:
|
|
108
|
+
msg = 'Failed to find asset type %s in language specification '\
|
|
109
|
+
'when looking for variables.'
|
|
110
|
+
logger.error(msg, asset_type)
|
|
111
|
+
raise LanguageGraphException(msg % asset_type)
|
|
112
|
+
|
|
113
|
+
return asset_dict['variables']
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_var_expr_for_asset(asset_type: str, var_name: str, lang_spec) -> dict:
|
|
117
|
+
"""Get a variable for a specific asset type by variable name.
|
|
118
|
+
|
|
119
|
+
Arguments:
|
|
120
|
+
---------
|
|
121
|
+
asset_type - a string representing the type of asset which
|
|
122
|
+
contains the variable
|
|
123
|
+
var_name - a string representing the variable name
|
|
124
|
+
|
|
125
|
+
Return:
|
|
126
|
+
------
|
|
127
|
+
A dictionary representing the step expression for the variable.
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
vars_dict = get_variables_for_asset_type(asset_type, lang_spec)
|
|
131
|
+
|
|
132
|
+
var_expr = next((var_entry['stepExpression'] for var_entry
|
|
133
|
+
in vars_dict if var_entry['name'] == var_name), None)
|
|
134
|
+
|
|
135
|
+
if not var_expr:
|
|
136
|
+
msg = 'Failed to find variable name "%s" in language '\
|
|
137
|
+
'specification when looking for variables for "%s" asset.'
|
|
138
|
+
logger.error(msg, var_name, asset_type)
|
|
139
|
+
raise LanguageGraphException(msg % (var_name, asset_type))
|
|
140
|
+
return var_expr
|