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
maltoolbox/model.py
CHANGED
|
@@ -27,108 +27,6 @@ if TYPE_CHECKING:
|
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
30
|
-
@dataclass
|
|
31
|
-
class AttackerAttachment:
|
|
32
|
-
"""Used to attach attackers to attack step entry points of assets"""
|
|
33
|
-
id: Optional[int] = None
|
|
34
|
-
name: Optional[str] = None
|
|
35
|
-
entry_points: list[tuple[ModelAsset, list[str]]] = \
|
|
36
|
-
field(default_factory=lambda: [])
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def get_entry_point_tuple(
|
|
40
|
-
self,
|
|
41
|
-
asset: ModelAsset
|
|
42
|
-
) -> Optional[tuple[ModelAsset, list[str]]]:
|
|
43
|
-
"""Return an entry point tuple of an AttackerAttachment matching the
|
|
44
|
-
asset provided.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Arguments:
|
|
48
|
-
asset - the asset to add entry point to
|
|
49
|
-
|
|
50
|
-
Return:
|
|
51
|
-
The entry point tuple containing the asset and the list of attack
|
|
52
|
-
steps if the asset has any entry points defined for this attacker
|
|
53
|
-
attachemnt.
|
|
54
|
-
None, otherwise.
|
|
55
|
-
"""
|
|
56
|
-
return next((ep_tuple for ep_tuple in self.entry_points
|
|
57
|
-
if ep_tuple[0] == asset), None)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def add_entry_point(
|
|
61
|
-
self, asset: ModelAsset, attackstep_name: str):
|
|
62
|
-
"""Add an entry point to an AttackerAttachment
|
|
63
|
-
|
|
64
|
-
self.entry_points contain tuples, first element of each tuple
|
|
65
|
-
is an asset, second element is a list of attack step names that
|
|
66
|
-
are entry points for the attacker.
|
|
67
|
-
|
|
68
|
-
Arguments:
|
|
69
|
-
asset - the asset to add the entry point to
|
|
70
|
-
attackstep_name - the name of the attack step to add as an entry point
|
|
71
|
-
"""
|
|
72
|
-
|
|
73
|
-
logger.debug(
|
|
74
|
-
f'Add entry point "{attackstep_name}" on asset "{asset.name}" '
|
|
75
|
-
f'to AttackerAttachment "{self.name}".'
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
# Get the entry point tuple for the asset if it already exists
|
|
79
|
-
entry_point_tuple = self.get_entry_point_tuple(asset)
|
|
80
|
-
|
|
81
|
-
if entry_point_tuple:
|
|
82
|
-
if attackstep_name not in entry_point_tuple[1]:
|
|
83
|
-
# If it exists and does not already have the attack step,
|
|
84
|
-
# add it
|
|
85
|
-
entry_point_tuple[1].append(attackstep_name)
|
|
86
|
-
else:
|
|
87
|
-
logger.info(
|
|
88
|
-
f'Entry point "{attackstep_name}" on asset "{asset.name}"'
|
|
89
|
-
f' already existed for AttackerAttachment "{self.name}".'
|
|
90
|
-
)
|
|
91
|
-
else:
|
|
92
|
-
# Otherwise, create the entry point tuple and the initial entry
|
|
93
|
-
# point
|
|
94
|
-
self.entry_points.append((asset, [attackstep_name]))
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def remove_entry_point(
|
|
98
|
-
self, asset: ModelAsset, attackstep_name: str):
|
|
99
|
-
"""Remove an entry point from an AttackerAttachment if it exists
|
|
100
|
-
|
|
101
|
-
Arguments:
|
|
102
|
-
asset - the asset to remove the entry point from
|
|
103
|
-
"""
|
|
104
|
-
|
|
105
|
-
logger.debug(
|
|
106
|
-
f'Remove entry point "{attackstep_name}" on asset "{asset.name}" '
|
|
107
|
-
f'from AttackerAttachment "{self.name}".'
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
# Get the entry point tuple for the asset if it exists
|
|
111
|
-
entry_point_tuple = self.get_entry_point_tuple(asset)
|
|
112
|
-
|
|
113
|
-
if entry_point_tuple:
|
|
114
|
-
if attackstep_name in entry_point_tuple[1]:
|
|
115
|
-
# If it exists and not already has the attack step, add it
|
|
116
|
-
entry_point_tuple[1].remove(attackstep_name)
|
|
117
|
-
else:
|
|
118
|
-
logger.warning(
|
|
119
|
-
f'Failed to find entry point "{attackstep_name}" on '
|
|
120
|
-
f'asset "{asset.name}" for AttackerAttachment '
|
|
121
|
-
f'"{self.name}". Nothing to remove.'
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
if not entry_point_tuple[1]:
|
|
125
|
-
self.entry_points.remove(entry_point_tuple)
|
|
126
|
-
else:
|
|
127
|
-
logger.warning(
|
|
128
|
-
f'Failed to find entry points on asset "{asset.name}" '
|
|
129
|
-
f'for AttackerAttachment "{self.name}". Nothing to remove.'
|
|
130
|
-
)
|
|
131
|
-
|
|
132
30
|
|
|
133
31
|
class Model():
|
|
134
32
|
"""An implementation of a MAL language model containing assets"""
|
|
@@ -148,7 +46,6 @@ class Model():
|
|
|
148
46
|
self.name = name
|
|
149
47
|
self.assets: dict[int, ModelAsset] = {}
|
|
150
48
|
self._name_to_asset:dict[str, ModelAsset] = {} # optimization
|
|
151
|
-
self.attackers: list[AttackerAttachment] = []
|
|
152
49
|
self.lang_graph = lang_graph
|
|
153
50
|
self.maltoolbox_version: str = mt_version
|
|
154
51
|
|
|
@@ -222,11 +119,6 @@ class Model():
|
|
|
222
119
|
return asset
|
|
223
120
|
|
|
224
121
|
|
|
225
|
-
def remove_attacker(self, attacker: AttackerAttachment) -> None:
|
|
226
|
-
"""Remove attacker"""
|
|
227
|
-
self.attackers.remove(attacker)
|
|
228
|
-
|
|
229
|
-
|
|
230
122
|
def remove_asset(self, asset: ModelAsset) -> None:
|
|
231
123
|
"""Remove an asset from the model.
|
|
232
124
|
|
|
@@ -251,39 +143,10 @@ class Model():
|
|
|
251
143
|
for fieldname, assoc_assets in associated_fieldnames.items():
|
|
252
144
|
asset.remove_associated_assets(fieldname, assoc_assets)
|
|
253
145
|
|
|
254
|
-
# Also remove all of the entry points
|
|
255
|
-
for attacker in self.attackers:
|
|
256
|
-
entry_point_tuple = attacker.get_entry_point_tuple(asset)
|
|
257
|
-
if entry_point_tuple:
|
|
258
|
-
attacker.entry_points.remove(entry_point_tuple)
|
|
259
|
-
|
|
260
146
|
del self.assets[asset.id]
|
|
261
147
|
del self._name_to_asset[asset.name]
|
|
262
148
|
|
|
263
149
|
|
|
264
|
-
def add_attacker(
|
|
265
|
-
self,
|
|
266
|
-
attacker: AttackerAttachment,
|
|
267
|
-
attacker_id: Optional[int] = None
|
|
268
|
-
) -> None:
|
|
269
|
-
"""Add an attacker to the model.
|
|
270
|
-
|
|
271
|
-
Arguments:
|
|
272
|
-
attacker - the attacker to add
|
|
273
|
-
attacker_id - optional id for the attacker
|
|
274
|
-
"""
|
|
275
|
-
|
|
276
|
-
if attacker_id is not None:
|
|
277
|
-
attacker.id = attacker_id
|
|
278
|
-
else:
|
|
279
|
-
attacker.id = self.next_id
|
|
280
|
-
self.next_id = max(attacker.id + 1, self.next_id)
|
|
281
|
-
|
|
282
|
-
if not hasattr(attacker, 'name') or not attacker.name:
|
|
283
|
-
attacker.name = 'Attacker:' + str(attacker.id)
|
|
284
|
-
self.attackers.append(attacker)
|
|
285
|
-
|
|
286
|
-
|
|
287
150
|
def get_asset_by_id(
|
|
288
151
|
self, asset_id: int
|
|
289
152
|
) -> Optional[ModelAsset]:
|
|
@@ -322,57 +185,12 @@ class Model():
|
|
|
322
185
|
return self._name_to_asset.get(asset_name, None)
|
|
323
186
|
|
|
324
187
|
|
|
325
|
-
def get_attacker_by_id(
|
|
326
|
-
self, attacker_id: int
|
|
327
|
-
) -> Optional[AttackerAttachment]:
|
|
328
|
-
"""
|
|
329
|
-
Find an attacker in the model based on its id.
|
|
330
|
-
|
|
331
|
-
Arguments:
|
|
332
|
-
attacker_id - the id of the attacker we are looking for
|
|
333
|
-
|
|
334
|
-
Return:
|
|
335
|
-
An attacker matching the id if it exists in the model.
|
|
336
|
-
"""
|
|
337
|
-
logger.debug(
|
|
338
|
-
'Get attacker with id %d from model "%s".',
|
|
339
|
-
attacker_id, self.name
|
|
340
|
-
)
|
|
341
|
-
return next(
|
|
342
|
-
(attacker for attacker in self.attackers
|
|
343
|
-
if attacker.id == attacker_id), None
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
def attacker_to_dict(
|
|
348
|
-
self, attacker: AttackerAttachment
|
|
349
|
-
) -> tuple[Optional[int], dict]:
|
|
350
|
-
"""Get dictionary representation of the attacker.
|
|
351
|
-
|
|
352
|
-
Arguments:
|
|
353
|
-
attacker - attacker to get dictionary representation of
|
|
354
|
-
"""
|
|
355
|
-
|
|
356
|
-
logger.debug('Translating %s to dictionary.', attacker.name)
|
|
357
|
-
attacker_dict: dict[str, Any] = {
|
|
358
|
-
'name': attacker.name,
|
|
359
|
-
'entry_points': {},
|
|
360
|
-
}
|
|
361
|
-
for (asset, attack_steps) in attacker.entry_points:
|
|
362
|
-
attacker_dict['entry_points'][asset.name] = {
|
|
363
|
-
'asset_id': asset.id,
|
|
364
|
-
'attack_steps' : attack_steps
|
|
365
|
-
}
|
|
366
|
-
return (attacker.id, attacker_dict)
|
|
367
|
-
|
|
368
|
-
|
|
369
188
|
def _to_dict(self) -> dict:
|
|
370
189
|
"""Get dictionary representation of the model."""
|
|
371
190
|
logger.debug('Translating model to dict.')
|
|
372
191
|
contents: dict[str, Any] = {
|
|
373
192
|
'metadata': {},
|
|
374
193
|
'assets': {},
|
|
375
|
-
'attackers' : {}
|
|
376
194
|
}
|
|
377
195
|
contents['metadata'] = {
|
|
378
196
|
'name': self.name,
|
|
@@ -387,10 +205,6 @@ class Model():
|
|
|
387
205
|
for asset in self.assets.values():
|
|
388
206
|
contents['assets'].update(asset._to_dict())
|
|
389
207
|
|
|
390
|
-
logger.debug('Translating attackers to dictionary.')
|
|
391
|
-
for attacker in self.attackers:
|
|
392
|
-
(attacker_id, attacker_dict) = self.attacker_to_dict(attacker)
|
|
393
|
-
contents['attackers'][attacker_id] = attacker_dict
|
|
394
208
|
return contents
|
|
395
209
|
|
|
396
210
|
|
|
@@ -459,29 +273,13 @@ class Model():
|
|
|
459
273
|
for assoc_asset_id in assoc_assets}
|
|
460
274
|
)
|
|
461
275
|
|
|
462
|
-
#
|
|
276
|
+
# Attackers no longer part of mal-toolbox
|
|
463
277
|
if 'attackers' in serialized_object:
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
target_asset = model.get_asset_by_id(
|
|
470
|
-
entry_points_dict['asset_id'])
|
|
471
|
-
if target_asset is None:
|
|
472
|
-
raise LookupError(
|
|
473
|
-
'Asset "%s"(%d) is not part of model "%s".' % (
|
|
474
|
-
asset_name,
|
|
475
|
-
entry_points_dict['asset_id'],
|
|
476
|
-
model.name)
|
|
477
|
-
)
|
|
478
|
-
attacker.entry_points.append(
|
|
479
|
-
(
|
|
480
|
-
target_asset,
|
|
481
|
-
entry_points_dict['attack_steps']
|
|
482
|
-
)
|
|
483
|
-
)
|
|
484
|
-
model.add_attacker(attacker, attacker_id = int(attacker_id))
|
|
278
|
+
msg = ("Defining attackers in a model file is deprecated,"
|
|
279
|
+
" use mal-simulator for attacker simulations.")
|
|
280
|
+
print(msg)
|
|
281
|
+
logger.warning(msg)
|
|
282
|
+
|
|
485
283
|
return model
|
|
486
284
|
|
|
487
285
|
|
maltoolbox/py.typed
ADDED
|
File without changes
|
|
@@ -82,7 +82,7 @@ def convert_model_dict_from_version_0_0(model_dict: dict) -> dict:
|
|
|
82
82
|
|
|
83
83
|
# Meta data and attackers did not change
|
|
84
84
|
new_model_dict['metadata'] = model_dict['metadata']
|
|
85
|
-
new_model_dict['attackers'] = model_dict
|
|
85
|
+
new_model_dict['attackers'] = model_dict.get('attackers', {})
|
|
86
86
|
|
|
87
87
|
new_model_dict['assets'] = {}
|
|
88
88
|
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
maltoolbox/__init__.py,sha256=m3yZVHy12wt-vRUtI-8HnaiRFnuYJxzt_Z5fOH3D77g,2090
|
|
2
|
-
maltoolbox/__main__.py,sha256=PSg8vFS8X-klJBJdSzrg0aLh9ykZgbcoSSEy3DTQoQQ,3499
|
|
3
|
-
maltoolbox/exceptions.py,sha256=0YjPx2v1yYumZ2o7pVZ1s_jS-GAb3Ng979KEFhROSNY,1399
|
|
4
|
-
maltoolbox/file_utils.py,sha256=tBR8Kjl8IoFzAtYaLNHNALuQrdMT3pD1ZpczHm1pu2g,1875
|
|
5
|
-
maltoolbox/model.py,sha256=Y3FKyWyRGPzJlpP92XRrWS52xVeKaz9rgOLoQJvJ808,24008
|
|
6
|
-
maltoolbox/attackgraph/__init__.py,sha256=AHDyX6dAkx3mDic2K56v1xche9N6ofDfbaHkKbdJ2qQ,230
|
|
7
|
-
maltoolbox/attackgraph/attacker.py,sha256=Lq7g_uFDvThU0wah-CiYA6oTshxt1TlgPJfkojlSyRQ,3132
|
|
8
|
-
maltoolbox/attackgraph/attackgraph.py,sha256=QEfWblZDBpvX7o17LL3LHf1gyB080fTGt7uVC8082mM,32537
|
|
9
|
-
maltoolbox/attackgraph/node.py,sha256=Ec67_u_8qf_MgCHaUg4wIbZFC013GWxbIsC8EjoguzE,6465
|
|
10
|
-
maltoolbox/attackgraph/query.py,sha256=iuaLAc3bMnQefgGa1g62re8-3yQrgBW_cS5W_DgWEjY,6835
|
|
11
|
-
maltoolbox/attackgraph/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
maltoolbox/attackgraph/analyzers/apriori.py,sha256=yERuk5M96Cpv2WyqNEpM_j7sf04SNYXSw_hFoL7UPW4,8986
|
|
13
|
-
maltoolbox/ingestors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
maltoolbox/ingestors/neo4j.py,sha256=W3AH3nymRQHI9N65HsSyyeQKcETPXmY_SLKc-iB4sBI,8328
|
|
15
|
-
maltoolbox/language/__init__.py,sha256=9p5nvVqDCKEhXbDMIz1MtwZ9GN7x1jmUUXbpjEwuqnw,269
|
|
16
|
-
maltoolbox/language/languagegraph.py,sha256=eBPTyoDpfc01ONEj321-RmIJV3DfVenYfHdVu0TiITo,67856
|
|
17
|
-
maltoolbox/language/compiler/__init__.py,sha256=JQyAgDwJh1pU7AmuOhd1-d2b2PYXpgMVPtxnav8QHVc,15872
|
|
18
|
-
maltoolbox/language/compiler/mal_lexer.py,sha256=BeifykDAt4PloRASOaLzBgWF35ev_zgD8lXMIsSHykc,12063
|
|
19
|
-
maltoolbox/language/compiler/mal_parser.py,sha256=sUoaE43l2VKg-Dou30mk2wlVS1FvdOREwHNIyFe4IkY,114699
|
|
20
|
-
maltoolbox/translators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
maltoolbox/translators/securicad.py,sha256=PJYjieioWN5tE_oKm83dtgV5UkC8EUH9Vsy3-FxBtUo,7017
|
|
22
|
-
maltoolbox/translators/updater.py,sha256=8bisZnzMWjGaG5tu8jdF-Oq6bPwIjXkVO-_yZDGc6cA,8652
|
|
23
|
-
mal_toolbox-0.3.11.dist-info/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
|
|
24
|
-
mal_toolbox-0.3.11.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
25
|
-
mal_toolbox-0.3.11.dist-info/METADATA,sha256=OHt2yl_bXfmzSH47-OhHzK6jDADEsSAYh3hqmDc9ZKM,6159
|
|
26
|
-
mal_toolbox-0.3.11.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
27
|
-
mal_toolbox-0.3.11.dist-info/entry_points.txt,sha256=oqby5O6cUP_OHCm70k_iYPA6UlbTBf7se1i3XwdK3uU,56
|
|
28
|
-
mal_toolbox-0.3.11.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
|
|
29
|
-
mal_toolbox-0.3.11.dist-info/RECORD,,
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MAL-Toolbox Attack Graph Apriori Analyzer Submodule
|
|
3
|
-
|
|
4
|
-
This submodule contains analyzers that are relevant before attackers are even
|
|
5
|
-
connected to the attack graph.
|
|
6
|
-
Currently these are:
|
|
7
|
-
- Viability = Determine if a node can be traversed under any circumstances or
|
|
8
|
-
if the model structure makes it unviable.
|
|
9
|
-
- Necessity = Determine if a node is necessary for the attacker or if the
|
|
10
|
-
model structure means it is not needed(it behaves as if it were already
|
|
11
|
-
compromised) to compromise children attack steps.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
from typing import Optional, TYPE_CHECKING
|
|
16
|
-
import logging
|
|
17
|
-
|
|
18
|
-
if TYPE_CHECKING:
|
|
19
|
-
from ..attackgraph import AttackGraph
|
|
20
|
-
from ..node import AttackGraphNode
|
|
21
|
-
|
|
22
|
-
logger = logging.getLogger(__name__)
|
|
23
|
-
|
|
24
|
-
def propagate_viability_from_node(node: AttackGraphNode) -> None:
|
|
25
|
-
"""
|
|
26
|
-
Arguments:
|
|
27
|
-
node - the attack graph node from which to propagate the viable
|
|
28
|
-
status
|
|
29
|
-
"""
|
|
30
|
-
logger.debug(
|
|
31
|
-
'Propagate viability from "%s"(%d) with viability status %s.',
|
|
32
|
-
node.full_name, node.id, node.is_viable
|
|
33
|
-
)
|
|
34
|
-
for child in node.children:
|
|
35
|
-
original_value = child.is_viable
|
|
36
|
-
if child.type == 'or':
|
|
37
|
-
child.is_viable = False
|
|
38
|
-
for parent in child.parents:
|
|
39
|
-
child.is_viable = child.is_viable or parent.is_viable
|
|
40
|
-
if child.type == 'and':
|
|
41
|
-
child.is_viable = False
|
|
42
|
-
|
|
43
|
-
if child.is_viable != original_value:
|
|
44
|
-
propagate_viability_from_node(child)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def propagate_necessity_from_node(node: AttackGraphNode) -> None:
|
|
48
|
-
"""
|
|
49
|
-
Arguments:
|
|
50
|
-
node - the attack graph node from which to propagate the necessary
|
|
51
|
-
status
|
|
52
|
-
"""
|
|
53
|
-
logger.debug(
|
|
54
|
-
'Propagate necessity from "%s"(%d) with necessity status %s.',
|
|
55
|
-
node.full_name, node.id, node.is_necessary
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
for child in node.children:
|
|
59
|
-
if child.ttc and child.ttc.get('name', None) not in ['Enabled',
|
|
60
|
-
'Disabled', 'Instant']:
|
|
61
|
-
# Do not propagate unnecessary state from nodes that have a TTC
|
|
62
|
-
# probability distribution associated with them.
|
|
63
|
-
# TODO: Evaluate this more carefully, how do we want to have TTCs
|
|
64
|
-
# impact necessity and viability.
|
|
65
|
-
# TODO: Have this condition be any probability that has a
|
|
66
|
-
# Bernoulli component
|
|
67
|
-
continue
|
|
68
|
-
original_value = child.is_necessary
|
|
69
|
-
if child.type == 'or':
|
|
70
|
-
child.is_necessary = False
|
|
71
|
-
if child.type == 'and':
|
|
72
|
-
child.is_necessary = False
|
|
73
|
-
for parent in child.parents:
|
|
74
|
-
child.is_necessary = child.is_necessary or parent.is_necessary
|
|
75
|
-
|
|
76
|
-
# TODO: Update TTC for child attack step before if it is not necessary
|
|
77
|
-
# before propagating it further.
|
|
78
|
-
if child.is_necessary != original_value:
|
|
79
|
-
propagate_necessity_from_node(child)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def evaluate_viability(node: AttackGraphNode) -> None:
|
|
83
|
-
"""
|
|
84
|
-
Arguments:
|
|
85
|
-
graph - the node to evaluate viability for.
|
|
86
|
-
"""
|
|
87
|
-
match (node.type):
|
|
88
|
-
case 'exist':
|
|
89
|
-
assert isinstance(node.existence_status, bool), \
|
|
90
|
-
f'Existence status not defined for {node.full_name}.'
|
|
91
|
-
node.is_viable = node.existence_status
|
|
92
|
-
case 'notExist':
|
|
93
|
-
assert isinstance(node.existence_status, bool), \
|
|
94
|
-
f'Existence status not defined for {node.full_name}.'
|
|
95
|
-
node.is_viable = not node.existence_status
|
|
96
|
-
case 'defense':
|
|
97
|
-
assert node.defense_status is not None and \
|
|
98
|
-
0.0 <= node.defense_status <= 1.0, \
|
|
99
|
-
f'{node.full_name} defense status invalid: {node.defense_status}.'
|
|
100
|
-
node.is_viable = node.defense_status != 1.0
|
|
101
|
-
case 'or':
|
|
102
|
-
node.is_viable = False
|
|
103
|
-
for parent in node.parents:
|
|
104
|
-
node.is_viable = node.is_viable or parent.is_viable
|
|
105
|
-
case 'and':
|
|
106
|
-
node.is_viable = True
|
|
107
|
-
for parent in node.parents:
|
|
108
|
-
node.is_viable = node.is_viable and parent.is_viable
|
|
109
|
-
case _:
|
|
110
|
-
msg = ('Evaluate viability was provided node "%s"(%d) which '
|
|
111
|
-
'is of unknown type "%s"')
|
|
112
|
-
logger.error(msg, node.full_name, node.id, node.type)
|
|
113
|
-
raise ValueError(msg % (node.full_name, node.id, node.type))
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def evaluate_necessity(node: AttackGraphNode) -> None:
|
|
117
|
-
"""
|
|
118
|
-
Arguments:
|
|
119
|
-
graph - the node to evaluate necessity for.
|
|
120
|
-
"""
|
|
121
|
-
match (node.type):
|
|
122
|
-
case 'exist':
|
|
123
|
-
assert isinstance(node.existence_status, bool), \
|
|
124
|
-
f'Existence status not defined for {node.full_name}.'
|
|
125
|
-
node.is_necessary = not node.existence_status
|
|
126
|
-
case 'notExist':
|
|
127
|
-
assert isinstance(node.existence_status, bool), \
|
|
128
|
-
f'Existence status not defined for {node.full_name}.'
|
|
129
|
-
node.is_necessary = bool(node.existence_status)
|
|
130
|
-
case 'defense':
|
|
131
|
-
assert node.defense_status is not None and \
|
|
132
|
-
0.0 <= node.defense_status <= 1.0, \
|
|
133
|
-
f'{node.full_name} defense status invalid: {node.defense_status}.'
|
|
134
|
-
node.is_necessary = node.defense_status != 0.0
|
|
135
|
-
case 'or':
|
|
136
|
-
node.is_necessary = True
|
|
137
|
-
for parent in node.parents:
|
|
138
|
-
node.is_necessary = node.is_necessary and parent.is_necessary
|
|
139
|
-
case 'and':
|
|
140
|
-
node.is_necessary = False
|
|
141
|
-
for parent in node.parents:
|
|
142
|
-
node.is_necessary = node.is_necessary or parent.is_necessary
|
|
143
|
-
case _:
|
|
144
|
-
msg = ('Evaluate necessity was provided node "%s"(%d) which '
|
|
145
|
-
'is of unknown type "%s"')
|
|
146
|
-
logger.error(msg, node.full_name, node.id, node.type)
|
|
147
|
-
raise ValueError(msg % (node.full_name, node.id, node.type))
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def evaluate_viability_and_necessity(node: AttackGraphNode) -> None:
|
|
151
|
-
"""
|
|
152
|
-
Arguments:
|
|
153
|
-
graph - the node to evaluate viability and necessity for.
|
|
154
|
-
"""
|
|
155
|
-
evaluate_viability(node)
|
|
156
|
-
evaluate_necessity(node)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def calculate_viability_and_necessity(graph: AttackGraph) -> None:
|
|
160
|
-
"""
|
|
161
|
-
Arguments:
|
|
162
|
-
graph - the attack graph for which we wish to determine the
|
|
163
|
-
viability and necessity statuses for the nodes.
|
|
164
|
-
"""
|
|
165
|
-
for node in graph.nodes.values():
|
|
166
|
-
if node.type in ['exist', 'notExist', 'defense']:
|
|
167
|
-
evaluate_viability_and_necessity(node)
|
|
168
|
-
if not node.is_viable:
|
|
169
|
-
propagate_viability_from_node(node)
|
|
170
|
-
if not node.is_necessary:
|
|
171
|
-
propagate_necessity_from_node(node)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def prune_unviable_and_unnecessary_nodes(graph: AttackGraph) -> None:
|
|
175
|
-
"""
|
|
176
|
-
Arguments:
|
|
177
|
-
graph - the attack graph for which we wish to remove the
|
|
178
|
-
the nodes which are not viable or necessary.
|
|
179
|
-
"""
|
|
180
|
-
logger.debug(
|
|
181
|
-
'Prune unviable and unnecessary nodes from the attack graph.')
|
|
182
|
-
|
|
183
|
-
nodes_to_remove = set()
|
|
184
|
-
for node in graph.nodes.values():
|
|
185
|
-
if node.type in ('or', 'and') and \
|
|
186
|
-
(not node.is_viable or not node.is_necessary):
|
|
187
|
-
nodes_to_remove.add(node)
|
|
188
|
-
|
|
189
|
-
# Do the removal separatly so we don't remove
|
|
190
|
-
# nodes from a set we are looping over
|
|
191
|
-
for node in nodes_to_remove:
|
|
192
|
-
graph.remove_node(node)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def propagate_viability_from_unviable_node(
|
|
196
|
-
unviable_node: AttackGraphNode,
|
|
197
|
-
) -> set[AttackGraphNode]:
|
|
198
|
-
"""
|
|
199
|
-
Update viability of nodes affected by newly enabled defense
|
|
200
|
-
`unviable_node` in the graph and return any attack steps
|
|
201
|
-
that are no longer viable because of it.
|
|
202
|
-
|
|
203
|
-
Propagate recursively via children as long as changes occur.
|
|
204
|
-
|
|
205
|
-
Arguments:
|
|
206
|
-
unviable_node - the node to propagate viability from
|
|
207
|
-
|
|
208
|
-
Returns:
|
|
209
|
-
attack_steps_made_unviable - set of the attack steps that have been
|
|
210
|
-
made unviable by a defense enabled in the
|
|
211
|
-
current step. Builds up recursively.
|
|
212
|
-
"""
|
|
213
|
-
|
|
214
|
-
attack_steps_made_unviable = set()
|
|
215
|
-
|
|
216
|
-
logger.debug(
|
|
217
|
-
'Update viability for node "%s"(%d)',
|
|
218
|
-
unviable_node.full_name,
|
|
219
|
-
unviable_node.id
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
assert not unviable_node.is_viable, (
|
|
223
|
-
"propagate_viability_from_unviable_node should not be called"
|
|
224
|
-
f" on viable node {unviable_node.full_name}"
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
if unviable_node.type in ('and', 'or'):
|
|
228
|
-
attack_steps_made_unviable.add(unviable_node)
|
|
229
|
-
|
|
230
|
-
for child in unviable_node.children:
|
|
231
|
-
original_value = child.is_viable
|
|
232
|
-
if child.type == 'or':
|
|
233
|
-
child.is_viable = False
|
|
234
|
-
for parent in child.parents:
|
|
235
|
-
child.is_viable = child.is_viable or parent.is_viable
|
|
236
|
-
if child.type == 'and':
|
|
237
|
-
child.is_viable = False
|
|
238
|
-
|
|
239
|
-
if child.is_viable != original_value:
|
|
240
|
-
attack_steps_made_unviable |= \
|
|
241
|
-
propagate_viability_from_unviable_node(child)
|
|
242
|
-
|
|
243
|
-
return attack_steps_made_unviable
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MAL-Toolbox Attack Graph Attacker Class
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
import logging
|
|
7
|
-
|
|
8
|
-
from typing import Optional
|
|
9
|
-
from typing import TYPE_CHECKING
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from .attackgraph import AttackGraphNode
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
class Attacker:
|
|
16
|
-
|
|
17
|
-
def __init__(
|
|
18
|
-
self,
|
|
19
|
-
name: str,
|
|
20
|
-
entry_points: Optional[set[AttackGraphNode]] = None,
|
|
21
|
-
reached_attack_steps: Optional[set[AttackGraphNode]] = None,
|
|
22
|
-
attacker_id: Optional[int] = None
|
|
23
|
-
):
|
|
24
|
-
self.name = name
|
|
25
|
-
self.id = attacker_id
|
|
26
|
-
self.entry_points = entry_points or set()
|
|
27
|
-
self.reached_attack_steps: set[AttackGraphNode] = set()
|
|
28
|
-
for node in reached_attack_steps or {}:
|
|
29
|
-
self.compromise(node)
|
|
30
|
-
|
|
31
|
-
def to_dict(self) -> dict:
|
|
32
|
-
attacker_dict: dict = {
|
|
33
|
-
'id': self.id,
|
|
34
|
-
'name': self.name,
|
|
35
|
-
'entry_points': {},
|
|
36
|
-
'reached_attack_steps': {}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
for entry_point in self.entry_points:
|
|
40
|
-
attacker_dict['entry_points'][entry_point.id] = \
|
|
41
|
-
entry_point.full_name
|
|
42
|
-
for attack_step in self.reached_attack_steps:
|
|
43
|
-
attacker_dict['reached_attack_steps'][attack_step.id] = \
|
|
44
|
-
attack_step.full_name
|
|
45
|
-
|
|
46
|
-
return attacker_dict
|
|
47
|
-
|
|
48
|
-
def __repr__(self) -> str:
|
|
49
|
-
return f'Attacker(name: "{self.name}", id: {self.id})'
|
|
50
|
-
|
|
51
|
-
def compromise(self, node: AttackGraphNode) -> None:
|
|
52
|
-
"""
|
|
53
|
-
Have the attacker compromise the node given as a parameter.
|
|
54
|
-
|
|
55
|
-
Arguments:
|
|
56
|
-
node - the node that the attacker will compromise
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
logger.debug(
|
|
60
|
-
'Attacker "%s"(%d) is compromising node "%s"(%d).',
|
|
61
|
-
self.name,
|
|
62
|
-
self.id,
|
|
63
|
-
node.full_name,
|
|
64
|
-
node.id
|
|
65
|
-
)
|
|
66
|
-
if node.is_compromised_by(self):
|
|
67
|
-
logger.info(
|
|
68
|
-
'Attacker "%s"(%d) already compromised node "%s"(%d). '
|
|
69
|
-
'Do nothing.',
|
|
70
|
-
self.name,
|
|
71
|
-
self.id,
|
|
72
|
-
node.full_name,
|
|
73
|
-
node.id
|
|
74
|
-
)
|
|
75
|
-
return
|
|
76
|
-
|
|
77
|
-
node.compromised_by.add(self)
|
|
78
|
-
self.reached_attack_steps.add(node)
|
|
79
|
-
|
|
80
|
-
def undo_compromise(self, node: AttackGraphNode) -> None:
|
|
81
|
-
"""
|
|
82
|
-
Remove the attacker from the list of attackers that have compromised
|
|
83
|
-
the node given as a parameter.
|
|
84
|
-
|
|
85
|
-
Arguments:
|
|
86
|
-
node - the node that we wish to remove this attacker from.
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
|
-
logger.debug(
|
|
90
|
-
'Removing attacker "%s"(%d) from compromised_by '
|
|
91
|
-
'list of node "%s"(%d).',
|
|
92
|
-
self.name,
|
|
93
|
-
self.id,
|
|
94
|
-
node.full_name,
|
|
95
|
-
node.id
|
|
96
|
-
)
|
|
97
|
-
if not node.is_compromised_by(self):
|
|
98
|
-
logger.info(
|
|
99
|
-
'Attacker "%s"(%d) had not compromised node "%s"(%d).'
|
|
100
|
-
' Do nothing.',
|
|
101
|
-
self.name,
|
|
102
|
-
self.id,
|
|
103
|
-
node.full_name,
|
|
104
|
-
node.id
|
|
105
|
-
)
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
node.compromised_by.remove(self)
|
|
109
|
-
self.reached_attack_steps.remove(node)
|