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.
Files changed (29) hide show
  1. {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/METADATA +2 -2
  2. mal_toolbox-2.1.0.dist-info/RECORD +51 -0
  3. {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/WHEEL +1 -1
  4. maltoolbox/__init__.py +2 -2
  5. maltoolbox/attackgraph/__init__.py +2 -2
  6. maltoolbox/attackgraph/attackgraph.py +121 -549
  7. maltoolbox/attackgraph/factories.py +68 -0
  8. maltoolbox/attackgraph/file_utils.py +0 -0
  9. maltoolbox/attackgraph/generate.py +338 -0
  10. maltoolbox/attackgraph/node_getters.py +36 -0
  11. maltoolbox/attackgraph/ttcs.py +28 -0
  12. maltoolbox/language/__init__.py +2 -2
  13. maltoolbox/language/compiler/mal_compiler.py +4 -3
  14. maltoolbox/language/detector.py +43 -0
  15. maltoolbox/language/expression_chain.py +218 -0
  16. maltoolbox/language/language_graph_asset.py +180 -0
  17. maltoolbox/language/language_graph_assoc.py +147 -0
  18. maltoolbox/language/language_graph_attack_step.py +129 -0
  19. maltoolbox/language/language_graph_builder.py +282 -0
  20. maltoolbox/language/language_graph_loaders.py +7 -0
  21. maltoolbox/language/language_graph_lookup.py +140 -0
  22. maltoolbox/language/language_graph_serialization.py +5 -0
  23. maltoolbox/language/languagegraph.py +244 -1537
  24. maltoolbox/language/step_expression_processor.py +491 -0
  25. mal_toolbox-2.0.0.dist-info/RECORD +0 -36
  26. {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/entry_points.txt +0 -0
  27. {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/AUTHORS +0 -0
  28. {mal_toolbox-2.0.0.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/LICENSE +0 -0
  29. {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,7 @@
1
+ import logging
2
+
3
+
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
@@ -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
@@ -0,0 +1,5 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
5
+