mal-toolbox 1.2.1__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 (37) hide show
  1. {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/METADATA +8 -75
  2. mal_toolbox-2.1.0.dist-info/RECORD +51 -0
  3. {mal_toolbox-1.2.1.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.py +1 -0
  11. maltoolbox/attackgraph/node_getters.py +36 -0
  12. maltoolbox/attackgraph/ttcs.py +28 -0
  13. maltoolbox/language/__init__.py +2 -2
  14. maltoolbox/language/compiler/__init__.py +4 -499
  15. maltoolbox/language/compiler/distributions.py +158 -0
  16. maltoolbox/language/compiler/exceptions.py +37 -0
  17. maltoolbox/language/compiler/lang.py +5 -0
  18. maltoolbox/language/compiler/mal_analyzer.py +920 -0
  19. maltoolbox/language/compiler/mal_compiler.py +1071 -0
  20. maltoolbox/language/detector.py +43 -0
  21. maltoolbox/language/expression_chain.py +218 -0
  22. maltoolbox/language/language_graph_asset.py +180 -0
  23. maltoolbox/language/language_graph_assoc.py +147 -0
  24. maltoolbox/language/language_graph_attack_step.py +129 -0
  25. maltoolbox/language/language_graph_builder.py +282 -0
  26. maltoolbox/language/language_graph_loaders.py +7 -0
  27. maltoolbox/language/language_graph_lookup.py +140 -0
  28. maltoolbox/language/language_graph_serialization.py +5 -0
  29. maltoolbox/language/languagegraph.py +244 -1536
  30. maltoolbox/language/step_expression_processor.py +491 -0
  31. mal_toolbox-1.2.1.dist-info/RECORD +0 -33
  32. maltoolbox/language/compiler/mal_lexer.py +0 -232
  33. maltoolbox/language/compiler/mal_parser.py +0 -3159
  34. {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/entry_points.txt +0 -0
  35. {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/AUTHORS +0 -0
  36. {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/LICENSE +0 -0
  37. {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,491 @@
1
+ from __future__ import annotations
2
+ import json
3
+ import logging
4
+ from typing import Any
5
+
6
+ from maltoolbox.language.expression_chain import ExpressionsChain
7
+ from maltoolbox.language.language_graph_lookup import get_var_expr_for_asset
8
+ from maltoolbox.language.language_graph_asset import LanguageGraphAsset
9
+ from maltoolbox.exceptions import (
10
+ LanguageGraphException,
11
+ LanguageGraphAssociationError,
12
+ )
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ def process_attack_step_expression(
17
+ target_asset: LanguageGraphAsset,
18
+ step_expression: dict[str, Any]
19
+ ) -> tuple[
20
+ LanguageGraphAsset,
21
+ None,
22
+ str
23
+ ]:
24
+ """The attack step expression just adds the name of the attack
25
+ step. All other step expressions only modify the target
26
+ asset and parent associations chain.
27
+ """
28
+ return (
29
+ target_asset,
30
+ None,
31
+ step_expression['name']
32
+ )
33
+
34
+ def process_set_operation_step_expression(
35
+ assets: dict[str, LanguageGraphAsset],
36
+ target_asset: LanguageGraphAsset,
37
+ expr_chain: ExpressionsChain | None,
38
+ step_expression: dict[str, Any],
39
+ lang_spec
40
+ ) -> tuple[
41
+ LanguageGraphAsset,
42
+ ExpressionsChain,
43
+ None
44
+ ]:
45
+ """The set operators are used to combine the left hand and right
46
+ hand targets accordingly.
47
+ """
48
+ lh_target_asset, lh_expr_chain, _ = process_step_expression(
49
+ assets,
50
+ target_asset,
51
+ expr_chain,
52
+ step_expression['lhs'],
53
+ lang_spec
54
+ )
55
+ rh_target_asset, rh_expr_chain, _ = process_step_expression(
56
+ assets,
57
+ target_asset,
58
+ expr_chain,
59
+ step_expression['rhs'],
60
+ lang_spec
61
+ )
62
+
63
+ assert lh_target_asset, (
64
+ f"No lh target in step expression {step_expression}"
65
+ )
66
+ assert rh_target_asset, (
67
+ f"No rh target in step expression {step_expression}"
68
+ )
69
+
70
+ if not lh_target_asset.get_all_common_superassets(rh_target_asset):
71
+ raise ValueError(
72
+ "Set operation attempted between targets that do not share "
73
+ f"any common superassets: {lh_target_asset.name} "
74
+ f"and {rh_target_asset.name}!"
75
+ )
76
+
77
+ new_expr_chain = ExpressionsChain(
78
+ type=step_expression['type'],
79
+ left_link=lh_expr_chain,
80
+ right_link=rh_expr_chain
81
+ )
82
+ return (
83
+ lh_target_asset,
84
+ new_expr_chain,
85
+ None
86
+ )
87
+
88
+ def process_variable_step_expression(
89
+ assets: dict[str, LanguageGraphAsset],
90
+ target_asset: LanguageGraphAsset,
91
+ step_expression: dict[str, Any],
92
+ lang_spec
93
+ ) -> tuple[
94
+ LanguageGraphAsset,
95
+ ExpressionsChain,
96
+ None
97
+ ]:
98
+
99
+ var_name = step_expression['name']
100
+ var_target_asset, var_expr_chain = (
101
+ resolve_variable(assets, target_asset, var_name, lang_spec)
102
+ )
103
+
104
+ if var_expr_chain is None:
105
+ raise LookupError(
106
+ f'Failed to find variable "{step_expression["name"]}" '
107
+ f'for {target_asset.name}',
108
+ )
109
+
110
+ return (
111
+ var_target_asset,
112
+ var_expr_chain,
113
+ None
114
+ )
115
+
116
+ def process_field_step_expression(
117
+ target_asset: LanguageGraphAsset,
118
+ step_expression: dict[str, Any]
119
+ ) -> tuple[
120
+ LanguageGraphAsset,
121
+ ExpressionsChain,
122
+ None
123
+ ]:
124
+ """Change the target asset from the current one to the associated
125
+ asset given the specified field name and add the parent
126
+ fieldname and association to the parent associations chain.
127
+ """
128
+ fieldname = step_expression['name']
129
+
130
+ if target_asset is None:
131
+ raise ValueError(
132
+ f'Missing target asset for field "{fieldname}"!'
133
+ )
134
+
135
+ new_target_asset = None
136
+ for association in target_asset.associations.values():
137
+ if (association.left_field.fieldname == fieldname and
138
+ target_asset.is_subasset_of(
139
+ association.right_field.asset)):
140
+ new_target_asset = association.left_field.asset
141
+
142
+ if (association.right_field.fieldname == fieldname and
143
+ target_asset.is_subasset_of(
144
+ association.left_field.asset)):
145
+ new_target_asset = association.right_field.asset
146
+
147
+ if new_target_asset:
148
+ new_expr_chain = ExpressionsChain(
149
+ type='field',
150
+ fieldname=fieldname,
151
+ association=association
152
+ )
153
+ return (
154
+ new_target_asset,
155
+ new_expr_chain,
156
+ None
157
+ )
158
+
159
+ raise LookupError(
160
+ f'Failed to find field {fieldname} on asset {target_asset.name}!',
161
+ )
162
+
163
+ def process_transitive_step_expression(
164
+ assets: dict[str, LanguageGraphAsset],
165
+ target_asset: LanguageGraphAsset,
166
+ expr_chain: ExpressionsChain | None,
167
+ step_expression: dict[str, Any],
168
+ lang_spec
169
+ ) -> tuple[
170
+ LanguageGraphAsset,
171
+ ExpressionsChain,
172
+ None
173
+ ]:
174
+ """Create a transitive tuple entry that applies to the next
175
+ component of the step expression.
176
+ """
177
+ result_target_asset, result_expr_chain, _ = (
178
+ process_step_expression(
179
+ assets,
180
+ target_asset,
181
+ expr_chain,
182
+ step_expression['stepExpression'],
183
+ lang_spec
184
+ )
185
+ )
186
+ new_expr_chain = ExpressionsChain(
187
+ type='transitive',
188
+ sub_link=result_expr_chain
189
+ )
190
+ return (
191
+ result_target_asset,
192
+ new_expr_chain,
193
+ None
194
+ )
195
+
196
+ def process_subType_step_expression(
197
+ assets: dict[str, LanguageGraphAsset],
198
+ target_asset: LanguageGraphAsset,
199
+ expr_chain: ExpressionsChain | None,
200
+ step_expression: dict[str, Any],
201
+ lang_spec
202
+ ) -> tuple[
203
+ LanguageGraphAsset,
204
+ ExpressionsChain,
205
+ None
206
+ ]:
207
+ """Create a subType tuple entry that applies to the next
208
+ component of the step expression and changes the target
209
+ asset to the subasset.
210
+ """
211
+ subtype_name = step_expression['subType']
212
+ result_target_asset, result_expr_chain, _ = (
213
+ process_step_expression(
214
+ assets,
215
+ target_asset,
216
+ expr_chain,
217
+ step_expression['stepExpression'],
218
+ lang_spec
219
+ )
220
+ )
221
+
222
+ if subtype_name not in assets:
223
+ raise LanguageGraphException(
224
+ f'Failed to find subtype {subtype_name}'
225
+ )
226
+
227
+ subtype_asset = assets[subtype_name]
228
+
229
+ if result_target_asset is None:
230
+ raise LookupError("Nonexisting asset for subtype")
231
+
232
+ if not subtype_asset.is_subasset_of(result_target_asset):
233
+ raise ValueError(
234
+ f'Found subtype {subtype_name} which does not extend '
235
+ f'{result_target_asset.name}, subtype cannot be resolved.'
236
+ )
237
+
238
+ new_expr_chain = ExpressionsChain(
239
+ type='subType',
240
+ sub_link=result_expr_chain,
241
+ subtype=subtype_asset
242
+ )
243
+ return (
244
+ subtype_asset,
245
+ new_expr_chain,
246
+ None
247
+ )
248
+
249
+ def process_collect_step_expression(
250
+ assets: dict[str, LanguageGraphAsset],
251
+ target_asset: LanguageGraphAsset,
252
+ expr_chain: ExpressionsChain | None,
253
+ step_expression: dict[str, Any],
254
+ lang_spec
255
+ ) -> tuple[
256
+ LanguageGraphAsset,
257
+ ExpressionsChain | None,
258
+ str | None
259
+ ]:
260
+ """Apply the right hand step expression to left hand step
261
+ expression target asset and parent associations chain.
262
+ """
263
+ lh_target_asset, lh_expr_chain, _ = process_step_expression(
264
+ assets, target_asset, expr_chain, step_expression['lhs'], lang_spec
265
+ )
266
+
267
+ if lh_target_asset is None:
268
+ raise ValueError(
269
+ 'No left hand asset in collect expression '
270
+ f'{step_expression["lhs"]}'
271
+ )
272
+
273
+ rh_target_asset, rh_expr_chain, rh_attack_step_name = (
274
+ process_step_expression(
275
+ assets, lh_target_asset, None, step_expression['rhs'], lang_spec
276
+ )
277
+ )
278
+
279
+ new_expr_chain = lh_expr_chain
280
+ if rh_expr_chain:
281
+ new_expr_chain = ExpressionsChain(
282
+ type='collect',
283
+ left_link=lh_expr_chain,
284
+ right_link=rh_expr_chain
285
+ )
286
+
287
+ return (
288
+ rh_target_asset,
289
+ new_expr_chain,
290
+ rh_attack_step_name
291
+ )
292
+
293
+ def process_step_expression(
294
+ assets: dict[str, LanguageGraphAsset],
295
+ target_asset: LanguageGraphAsset,
296
+ expr_chain: ExpressionsChain | None,
297
+ step_expression: dict,
298
+ lang_spec
299
+ ) -> tuple[
300
+ LanguageGraphAsset,
301
+ ExpressionsChain | None,
302
+ str | None
303
+ ]:
304
+ """Recursively process an attack step expression.
305
+
306
+ Arguments:
307
+ ---------
308
+ target_asset - The asset type that this step expression should
309
+ apply to. Initially it will contain the asset
310
+ type to which the attack step belongs.
311
+ expr_chain - A expressions chain of linked of associations
312
+ and set operations from the attack step to its
313
+ parent attack step.
314
+ Note: This was done for the parent attack step
315
+ because it was easier to construct recursively
316
+ given the left-hand first expansion of the
317
+ current MAL language specification.
318
+ step_expression - A dictionary containing the step expression.
319
+
320
+ Return:
321
+ ------
322
+ A tuple triplet containing the target asset, the resulting parent
323
+ associations chain, and the name of the attack step.
324
+
325
+ """
326
+ if logger.isEnabledFor(logging.DEBUG):
327
+ # Avoid running json.dumps when not in debug
328
+ logger.debug(
329
+ 'Processing Step Expression:\n%s',
330
+ json.dumps(step_expression, indent=2)
331
+ )
332
+
333
+ result: tuple[
334
+ LanguageGraphAsset,
335
+ ExpressionsChain | None,
336
+ str | None
337
+ ]
338
+
339
+ match (step_expression['type']):
340
+ case 'attackStep':
341
+ result = process_attack_step_expression(
342
+ target_asset, step_expression
343
+ )
344
+ case 'union' | 'intersection' | 'difference':
345
+ result = process_set_operation_step_expression(
346
+ assets, target_asset, expr_chain, step_expression, lang_spec
347
+ )
348
+ case 'variable':
349
+ result = process_variable_step_expression(
350
+ assets, target_asset, step_expression, lang_spec
351
+ )
352
+ case 'field':
353
+ result = process_field_step_expression(
354
+ target_asset, step_expression
355
+ )
356
+ case 'transitive':
357
+ result = process_transitive_step_expression(
358
+ assets, target_asset, expr_chain, step_expression, lang_spec
359
+ )
360
+ case 'subType':
361
+ result = process_subType_step_expression(
362
+ assets, target_asset, expr_chain, step_expression, lang_spec
363
+ )
364
+ case 'collect':
365
+ result = process_collect_step_expression(
366
+ assets, target_asset, expr_chain, step_expression, lang_spec
367
+ )
368
+ case _:
369
+ raise LookupError(
370
+ f'Unknown attack step type: "{step_expression["type"]}"'
371
+ )
372
+ return result
373
+
374
+ def reverse_expr_chain(
375
+ expr_chain: ExpressionsChain | None,
376
+ reverse_chain: ExpressionsChain | None
377
+ ) -> ExpressionsChain | None:
378
+ """Recursively reverse the associations chain. From parent to child or
379
+ vice versa.
380
+
381
+ Arguments:
382
+ ---------
383
+ expr_chain - A chain of nested tuples that specify the
384
+ associations and set operations chain from an
385
+ attack step to its connected attack step.
386
+ reverse_chain - A chain of nested tuples that represents the
387
+ current reversed associations chain.
388
+
389
+ Return:
390
+ ------
391
+ The resulting reversed associations chain.
392
+
393
+ """
394
+ if not expr_chain:
395
+ return reverse_chain
396
+ match (expr_chain.type):
397
+ case 'union' | 'intersection' | 'difference' | 'collect':
398
+ left_reverse_chain = \
399
+ reverse_expr_chain(expr_chain.left_link, reverse_chain)
400
+ right_reverse_chain = \
401
+ reverse_expr_chain(expr_chain.right_link, reverse_chain)
402
+ if expr_chain.type == 'collect':
403
+ new_expr_chain = ExpressionsChain(
404
+ type=expr_chain.type,
405
+ left_link=right_reverse_chain,
406
+ right_link=left_reverse_chain
407
+ )
408
+ else:
409
+ new_expr_chain = ExpressionsChain(
410
+ type=expr_chain.type,
411
+ left_link=left_reverse_chain,
412
+ right_link=right_reverse_chain
413
+ )
414
+
415
+ return new_expr_chain
416
+
417
+ case 'transitive':
418
+ result_reverse_chain = reverse_expr_chain(
419
+ expr_chain.sub_link, reverse_chain)
420
+ new_expr_chain = ExpressionsChain(
421
+ type='transitive',
422
+ sub_link=result_reverse_chain
423
+ )
424
+ return new_expr_chain
425
+
426
+ case 'field':
427
+ association = expr_chain.association
428
+
429
+ if not association:
430
+ raise LanguageGraphException(
431
+ "Missing association for expressions chain"
432
+ )
433
+
434
+ if not expr_chain.fieldname:
435
+ raise LanguageGraphException(
436
+ "Missing field name for expressions chain"
437
+ )
438
+
439
+ opposite_fieldname = association.get_opposite_fieldname(
440
+ expr_chain.fieldname)
441
+ new_expr_chain = ExpressionsChain(
442
+ type='field',
443
+ association=association,
444
+ fieldname=opposite_fieldname
445
+ )
446
+ return new_expr_chain
447
+
448
+ case 'subType':
449
+ result_reverse_chain = reverse_expr_chain(
450
+ expr_chain.sub_link,
451
+ reverse_chain
452
+ )
453
+ new_expr_chain = ExpressionsChain(
454
+ type='subType',
455
+ sub_link=result_reverse_chain,
456
+ subtype=expr_chain.subtype
457
+ )
458
+ return new_expr_chain
459
+
460
+ case _:
461
+ msg = 'Unknown assoc chain element "%s"'
462
+ logger.error(msg, expr_chain.type)
463
+ raise LanguageGraphAssociationError(msg % expr_chain.type)
464
+
465
+ def resolve_variable(
466
+ assets: dict[str, LanguageGraphAsset],
467
+ asset: LanguageGraphAsset,
468
+ var_name: str,
469
+ lang_spec
470
+ ) -> tuple:
471
+ """Resolve a variable for a specific asset by variable name.
472
+
473
+ Arguments:
474
+ ---------
475
+ asset - a language graph asset to which the variable belongs
476
+ var_name - a string representing the variable name
477
+
478
+ Return:
479
+ ------
480
+ A tuple containing the target asset and expressions chain required to
481
+ reach it.
482
+
483
+ """
484
+ if var_name not in asset.variables:
485
+ var_expr = get_var_expr_for_asset(asset.name, var_name, lang_spec)
486
+ target_asset, expr_chain, _ = process_step_expression(
487
+ assets, asset, None, var_expr, lang_spec
488
+ )
489
+ asset.own_variables[var_name] = (target_asset, expr_chain)
490
+ return (target_asset, expr_chain)
491
+ return asset.variables[var_name]
@@ -1,33 +0,0 @@
1
- mal_toolbox-1.2.1.dist-info/licenses/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
2
- mal_toolbox-1.2.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
3
- maltoolbox/__init__.py,sha256=649BOg_Bo4OazvePvnuF2tjM0q4VbyS2Fsz9em_KEtM,2132
4
- maltoolbox/__main__.py,sha256=aAm6NcZ-HtPmY9hfFlGNnTs5rydoI6NAc88RgXt1G9U,3515
5
- maltoolbox/exceptions.py,sha256=4rwqzu8Cgj0ShjUoCXP2yik-bJaqYqj6Y-0tqxHy4vs,1316
6
- maltoolbox/file_utils.py,sha256=IXA0cvyopjRFGGKqRPkRQ0RJOtKzq_XF13aHgcz-TFc,1911
7
- maltoolbox/model.py,sha256=bqYPYNW8MxuS2wUef51z6yjaA5F13YXbeicM2qaYUd4,18138
8
- maltoolbox/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- maltoolbox/str_utils.py,sha256=zZXHOFfXguhpQQJ5nyGe1VGWchOkanQUc8viV7nhQho,639
10
- maltoolbox/attackgraph/__init__.py,sha256=l7dJ7jOqpcj7PdsOKZt1NuXlPyjd6vZYvcXlj8Kq09w,297
11
- maltoolbox/attackgraph/attackgraph.py,sha256=WBRwnVmQqwpjp3x_kSR8HH49MnlRIhF6tZ_r7nqTAJw,27162
12
- maltoolbox/attackgraph/node.py,sha256=7NAdEl40w6KMt_gKK1mP4SRAmmyCQGSTMaD-GHMNXHk,4186
13
- maltoolbox/attackgraph/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- maltoolbox/language/__init__.py,sha256=RTTfhnCYa5PRGJCgDAWLpLLAzNCvNMn91UHNiOXszbg,490
15
- maltoolbox/language/languagegraph.py,sha256=Tjqk5zv_37ghmepx8KBHPTH8tRlboaYcW836MiUS1d8,65140
16
- maltoolbox/language/compiler/__init__.py,sha256=Rbdeco6SWHyFw-VJfpxLRSZO3UMjJxPGenMR8OujVpA,15846
17
- maltoolbox/language/compiler/mal_lexer.py,sha256=TQvzEW7yCN0iY6Js5O6wCDFxSAE0_LAX4JVy96TnLro,14808
18
- maltoolbox/language/compiler/mal_parser.py,sha256=bVfYWRZyyhU-s2tJI-D_YwbVQkl3AHzdrD6LWM0BQbI,116108
19
- maltoolbox/patternfinder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- maltoolbox/patternfinder/attackgraph_patterns.py,sha256=jw6BlQHXLmiWm9xajLHbUZUWVQGM2qAgMHOs0Yo2YsE,5168
21
- maltoolbox/translators/__init__.py,sha256=7qbhjeu--s2A-O_031a7xzSq794iQlrrUxM8gE1LxH4,201
22
- maltoolbox/translators/networkx.py,sha256=v1JQAqO7st6-ktx5P3oy93DsL2SEUPly_3zcALv08o8,1352
23
- maltoolbox/translators/updater.py,sha256=mFmTT2GHCw6nsoHe_ChnvAHd5j6UxKnvAqLFDSziqC4,8566
24
- maltoolbox/visualization/__init__.py,sha256=7rrGclkGdP6LrxpfSh1esYFG_MnvnVruuEdUJI-DX-g,350
25
- maltoolbox/visualization/draw_io_utils.py,sha256=CgsD0HEFpxZ6ZIWtUZtMekdPB2Irtmvhz0TNEm7x1ig,14378
26
- maltoolbox/visualization/graphviz_utils.py,sha256=87B2pxRMrlmm2RHu0JM7nQRHWZD0Yt3zCmEwl8xUXdU,4709
27
- maltoolbox/visualization/neo4j_utils.py,sha256=R2Qm2gC5GDpfiPhhB3oymuBI2W580SRXGVtQuFRYiIA,3496
28
- maltoolbox/visualization/utils.py,sha256=EZWsxukO5hbRwGFW9GM9ZemKT-nYg-VMCup-SsntaQM,1480
29
- mal_toolbox-1.2.1.dist-info/METADATA,sha256=Nlsaw-sJu-Pqz6hWme_f6K-hryjFK8-QHYyMqfXBfWA,6696
30
- mal_toolbox-1.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- mal_toolbox-1.2.1.dist-info/entry_points.txt,sha256=oqby5O6cUP_OHCm70k_iYPA6UlbTBf7se1i3XwdK3uU,56
32
- mal_toolbox-1.2.1.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
33
- mal_toolbox-1.2.1.dist-info/RECORD,,