mal-toolbox 0.1.7__tar.gz → 0.1.9__tar.gz
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.1.7/mal_toolbox.egg-info → mal_toolbox-0.1.9}/PKG-INFO +7 -1
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/README.md +6 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9/mal_toolbox.egg-info}/PKG-INFO +7 -1
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/__init__.py +2 -2
- mal_toolbox-0.1.9/maltoolbox/attackgraph/__init__.py +8 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/attackgraph/attackgraph.py +26 -1
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/attackgraph/node.py +34 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/attackgraph/query.py +43 -2
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/language/__init__.py +2 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/language/classes_factory.py +3 -2
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/model.py +52 -23
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/pyproject.toml +2 -3
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/tests/test_model.py +77 -21
- mal_toolbox-0.1.7/maltoolbox/attackgraph/__init__.py +0 -3
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/AUTHORS +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/LICENSE +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/mal_toolbox.egg-info/SOURCES.txt +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/mal_toolbox.egg-info/dependency_links.txt +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/mal_toolbox.egg-info/requires.txt +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/mal_toolbox.egg-info/top_level.txt +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/__main__.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/attackgraph/analyzers/__init__.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/attackgraph/analyzers/apriori.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/attackgraph/attacker.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/default.conf +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/exceptions.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/file_utils.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/ingestors/__init__.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/ingestors/neo4j.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/language/compiler/__init__.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/language/compiler/mal_lexer.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/language/compiler/mal_parser.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/language/compiler/mal_visitor.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/language/languagegraph.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/translators/__init__.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/translators/securicad.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/translators/updater.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/maltoolbox/wrappers.py +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/setup.cfg +0 -0
- {mal_toolbox-0.1.7 → mal_toolbox-0.1.9}/tests/test_wrappers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mal-toolbox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: A collection of tools used to create MAL models and attack graphs.
|
|
5
5
|
Author-email: Andrei Buhaiu <buhaiu@kth.se>, Giuseppe Nebbione <nebbione@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>, Joakim Loxdal <loxdal@kth.se>
|
|
6
6
|
License: Apache Software License
|
|
@@ -102,6 +102,12 @@ and it can be used to visualise the instance model and the attack graph.
|
|
|
102
102
|
|
|
103
103
|
# Usage
|
|
104
104
|
|
|
105
|
+
## Installation
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
pip install mal-toolbox
|
|
109
|
+
```
|
|
110
|
+
|
|
105
111
|
## Configuration
|
|
106
112
|
A default configuration file `default.conf` can be found in the package
|
|
107
113
|
directory. This contains the default values to use for logging and can also be
|
|
@@ -75,6 +75,12 @@ and it can be used to visualise the instance model and the attack graph.
|
|
|
75
75
|
|
|
76
76
|
# Usage
|
|
77
77
|
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
pip install mal-toolbox
|
|
82
|
+
```
|
|
83
|
+
|
|
78
84
|
## Configuration
|
|
79
85
|
A default configuration file `default.conf` can be found in the package
|
|
80
86
|
directory. This contains the default values to use for logging and can also be
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mal-toolbox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: A collection of tools used to create MAL models and attack graphs.
|
|
5
5
|
Author-email: Andrei Buhaiu <buhaiu@kth.se>, Giuseppe Nebbione <nebbione@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>, Joakim Loxdal <loxdal@kth.se>
|
|
6
6
|
License: Apache Software License
|
|
@@ -102,6 +102,12 @@ and it can be used to visualise the instance model and the attack graph.
|
|
|
102
102
|
|
|
103
103
|
# Usage
|
|
104
104
|
|
|
105
|
+
## Installation
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
pip install mal-toolbox
|
|
109
|
+
```
|
|
110
|
+
|
|
105
111
|
## Configuration
|
|
106
112
|
A default configuration file `default.conf` can be found in the package
|
|
107
113
|
directory. This contains the default values to use for logging and can also be
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# MAL Toolbox v0.1.
|
|
2
|
+
# MAL Toolbox v0.1.9
|
|
3
3
|
# Copyright 2024, Andrei Buhaiu.
|
|
4
4
|
#
|
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -21,7 +21,7 @@ MAL-Toolbox Framework
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
__title__ = 'maltoolbox'
|
|
24
|
-
__version__ = '0.1.
|
|
24
|
+
__version__ = '0.1.9'
|
|
25
25
|
__authors__ = ['Andrei Buhaiu',
|
|
26
26
|
'Giuseppe Nebbione',
|
|
27
27
|
'Nikolaos Kakouros',
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
MAL-Toolbox Attack Graph Module
|
|
3
3
|
"""
|
|
4
4
|
from __future__ import annotations
|
|
5
|
+
import copy
|
|
5
6
|
import logging
|
|
6
7
|
import json
|
|
7
8
|
|
|
@@ -224,6 +225,30 @@ class AttackGraph():
|
|
|
224
225
|
'attackers': serialized_attackers,
|
|
225
226
|
}
|
|
226
227
|
|
|
228
|
+
def __deepcopy__(self, memo):
|
|
229
|
+
copied_attackgraph = AttackGraph(self.lang_graph)
|
|
230
|
+
copied_attackgraph.model = self.model
|
|
231
|
+
|
|
232
|
+
# Deep copy nodes and add references to them
|
|
233
|
+
copied_attackgraph.nodes = copy.deepcopy(self.nodes, memo)
|
|
234
|
+
|
|
235
|
+
# Deep copy attackers and references to them
|
|
236
|
+
copied_attackgraph.attackers = copy.deepcopy(self.attackers, memo)
|
|
237
|
+
|
|
238
|
+
# Copy lookup dicts
|
|
239
|
+
copied_attackgraph._id_to_attacker = \
|
|
240
|
+
copy.deepcopy(self._id_to_attacker, memo)
|
|
241
|
+
copied_attackgraph._id_to_node = \
|
|
242
|
+
copy.deepcopy(self._id_to_node, memo)
|
|
243
|
+
copied_attackgraph._full_name_to_node = \
|
|
244
|
+
copy.deepcopy(self._full_name_to_node, memo)
|
|
245
|
+
|
|
246
|
+
# Copy counters
|
|
247
|
+
copied_attackgraph.next_node_id = self.next_node_id
|
|
248
|
+
copied_attackgraph.next_attacker_id = self.next_attacker_id
|
|
249
|
+
|
|
250
|
+
return copied_attackgraph
|
|
251
|
+
|
|
227
252
|
def save_to_file(self, filename: str) -> None:
|
|
228
253
|
"""Save to json/yml depending on extension"""
|
|
229
254
|
logger.debug('Save attack graph to file "%s".', filename)
|
|
@@ -382,7 +407,7 @@ class AttackGraph():
|
|
|
382
407
|
The attack step node that matches the given full name.
|
|
383
408
|
"""
|
|
384
409
|
|
|
385
|
-
logger.debug(f'Looking up node with
|
|
410
|
+
logger.debug(f'Looking up node with full name "{full_name}"')
|
|
386
411
|
return self._full_name_to_node.get(full_name)
|
|
387
412
|
|
|
388
413
|
def get_attacker_by_id(self, attacker_id: int) -> Optional[Attacker]:
|
|
@@ -3,6 +3,7 @@ MAL-Toolbox Attack Graph Node Dataclass
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
|
+
import copy
|
|
6
7
|
from dataclasses import field, dataclass
|
|
7
8
|
from typing import Any, Optional
|
|
8
9
|
|
|
@@ -69,6 +70,39 @@ class AttackGraphNode:
|
|
|
69
70
|
def __repr__(self) -> str:
|
|
70
71
|
return str(self.to_dict())
|
|
71
72
|
|
|
73
|
+
def __deepcopy__(self, memo) -> AttackGraphNode:
|
|
74
|
+
"""Deep copy an attackgraph node"""
|
|
75
|
+
|
|
76
|
+
copied_node = AttackGraphNode(
|
|
77
|
+
self.type,
|
|
78
|
+
self.name,
|
|
79
|
+
self.ttc,
|
|
80
|
+
self.id,
|
|
81
|
+
self.asset,
|
|
82
|
+
[],
|
|
83
|
+
[],
|
|
84
|
+
self.defense_status,
|
|
85
|
+
self.existence_status,
|
|
86
|
+
self.is_viable,
|
|
87
|
+
self.is_necessary,
|
|
88
|
+
copy.deepcopy(self.compromised_by, memo),
|
|
89
|
+
self.mitre_info,
|
|
90
|
+
copy.deepcopy(self.tags, memo),
|
|
91
|
+
copy.deepcopy(self.attributes, memo),
|
|
92
|
+
copy.deepcopy(self.extras, memo)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Remember that self was already copied
|
|
96
|
+
memo[id(self)] = copied_node
|
|
97
|
+
|
|
98
|
+
# Deep copy children and parents, send memo (avoid infinite recursion)
|
|
99
|
+
if self.parents:
|
|
100
|
+
copied_node.parents = copy.deepcopy(self.parents, memo)
|
|
101
|
+
if self.children:
|
|
102
|
+
copied_node.children = copy.deepcopy(self.children, memo)
|
|
103
|
+
|
|
104
|
+
return copied_node
|
|
105
|
+
|
|
72
106
|
def is_compromised(self) -> bool:
|
|
73
107
|
"""
|
|
74
108
|
Return True if any attackers have compromised this node.
|
|
@@ -37,10 +37,21 @@ def is_node_traversable_by_attacker(
|
|
|
37
37
|
attacker.id
|
|
38
38
|
)
|
|
39
39
|
if not node.is_viable:
|
|
40
|
+
logger.debug(
|
|
41
|
+
'"%s"(%d) is not traversable because it is non-viable',
|
|
42
|
+
node.full_name,
|
|
43
|
+
node.id,
|
|
44
|
+
)
|
|
40
45
|
return False
|
|
41
46
|
|
|
42
47
|
match(node.type):
|
|
43
48
|
case 'or':
|
|
49
|
+
logger.debug(
|
|
50
|
+
'"%s"(%d) is traversable because it is viable and '
|
|
51
|
+
'of type "or".',
|
|
52
|
+
node.full_name,
|
|
53
|
+
node.id
|
|
54
|
+
)
|
|
44
55
|
return True
|
|
45
56
|
|
|
46
57
|
case 'and':
|
|
@@ -49,14 +60,44 @@ def is_node_traversable_by_attacker(
|
|
|
49
60
|
not parent.is_compromised_by(attacker):
|
|
50
61
|
# If the parent is not present in the attacks steps
|
|
51
62
|
# already reached and is necessary.
|
|
63
|
+
logger.debug(
|
|
64
|
+
'"%s"(%d) is not traversable because while it is '
|
|
65
|
+
'viable, and of type "and", its necessary parent '
|
|
66
|
+
'"%s(%d)" has not already been compromised.',
|
|
67
|
+
node.full_name,
|
|
68
|
+
node.id,
|
|
69
|
+
parent.full_name,
|
|
70
|
+
parent.id
|
|
71
|
+
)
|
|
52
72
|
return False
|
|
73
|
+
logger.debug(
|
|
74
|
+
'"%s"(%d) is traversable because it is viable, '
|
|
75
|
+
'of type "and", and all of its necessary parents have '
|
|
76
|
+
'already been compromised.',
|
|
77
|
+
node.full_name,
|
|
78
|
+
node.id
|
|
79
|
+
)
|
|
53
80
|
return True
|
|
54
81
|
|
|
55
|
-
case 'exist' | 'notExist' |
|
|
82
|
+
case 'exist' | 'notExist' | 'defense':
|
|
83
|
+
logger.warning(
|
|
84
|
+
'Nodes of type "exist", "notExist", and "defense" are never '
|
|
85
|
+
'marked as traversable. However, we do not normally check '
|
|
86
|
+
'if they are traversable. Node "%s"(%d) of type "%s" was '
|
|
87
|
+
'checked for traversability.',
|
|
88
|
+
node.full_name,
|
|
89
|
+
node.id,
|
|
90
|
+
node.type
|
|
91
|
+
)
|
|
56
92
|
return False
|
|
57
93
|
|
|
58
94
|
case _:
|
|
59
|
-
logger.error(
|
|
95
|
+
logger.error(
|
|
96
|
+
'Node "%s"(%d) has an unknown type "%s".',
|
|
97
|
+
node.full_name,
|
|
98
|
+
node.id,
|
|
99
|
+
node.type
|
|
100
|
+
)
|
|
60
101
|
return False
|
|
61
102
|
|
|
62
103
|
def get_attack_surface(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
MAL-Toolbox Language Classes Factory Module
|
|
3
|
+
Uses python_jsonschema_objects to generate python classes from a MAL language
|
|
3
4
|
"""
|
|
4
5
|
from __future__ import annotations
|
|
5
6
|
import json
|
|
@@ -25,7 +26,7 @@ class LanguageClassesFactory:
|
|
|
25
26
|
|
|
26
27
|
def _generate_assets(self) -> None:
|
|
27
28
|
"""
|
|
28
|
-
Generate JSON Schema for
|
|
29
|
+
Generate JSON Schema for asset types in the language specification.
|
|
29
30
|
"""
|
|
30
31
|
for asset in self.lang_graph.assets:
|
|
31
32
|
logger.debug('Creating %s asset JSON schema entry.', asset.name)
|
|
@@ -67,7 +68,7 @@ class LanguageClassesFactory:
|
|
|
67
68
|
|
|
68
69
|
def _generate_associations(self) -> None:
|
|
69
70
|
"""
|
|
70
|
-
Generate JSON Schema for
|
|
71
|
+
Generate JSON Schema for association types in the language specification.
|
|
71
72
|
"""
|
|
72
73
|
def create_association_entry(assoc: SchemaGeneratedClass):
|
|
73
74
|
logger.debug('Creating %s association JSON schema entry.', assoc.name)
|
|
@@ -28,24 +28,45 @@ logger = logging.getLogger(__name__)
|
|
|
28
28
|
|
|
29
29
|
@dataclass
|
|
30
30
|
class AttackerAttachment:
|
|
31
|
-
"""Used to attach attackers to attack step
|
|
31
|
+
"""Used to attach attackers to attack step entry points of assets"""
|
|
32
32
|
id: Optional[int] = None
|
|
33
33
|
name: Optional[str] = None
|
|
34
34
|
entry_points: list[tuple[SchemaGeneratedClass, list[str]]] = \
|
|
35
35
|
field(default_factory=lambda: [])
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
def
|
|
38
|
+
def get_entry_point_tuple(
|
|
39
|
+
self,
|
|
40
|
+
asset: SchemaGeneratedClass
|
|
41
|
+
) -> Optional[tuple[SchemaGeneratedClass, list[str]]]:
|
|
42
|
+
"""Return an entry point tuple of an AttackerAttachment matching the
|
|
43
|
+
asset provided.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Arguments:
|
|
47
|
+
asset - the asset to add entry point to
|
|
48
|
+
|
|
49
|
+
Return:
|
|
50
|
+
The entry point tuple containing the asset and the list of attack
|
|
51
|
+
steps if the asset has any entry points defined for this attacker
|
|
52
|
+
attachemnt.
|
|
53
|
+
None, otherwise.
|
|
54
|
+
"""
|
|
55
|
+
return next((ep_tuple for ep_tuple in self.entry_points
|
|
56
|
+
if ep_tuple[0] == asset), None)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def add_entry_point(
|
|
39
60
|
self, asset: SchemaGeneratedClass, attackstep_name: str):
|
|
40
|
-
"""Add an
|
|
61
|
+
"""Add an entry point to an AttackerAttachment
|
|
41
62
|
|
|
42
63
|
self.entry_points contain tuples, first element of each tuple
|
|
43
64
|
is an asset, second element is a list of attack step names that
|
|
44
65
|
are entry points for the attacker.
|
|
45
66
|
|
|
46
|
-
|
|
47
|
-
asset - the asset to add
|
|
48
|
-
attackstep_name - the name of the attack step to add as an
|
|
67
|
+
Arguments:
|
|
68
|
+
asset - the asset to add the entry point to
|
|
69
|
+
attackstep_name - the name of the attack step to add as an entry point
|
|
49
70
|
"""
|
|
50
71
|
|
|
51
72
|
logger.debug(
|
|
@@ -53,42 +74,44 @@ class AttackerAttachment:
|
|
|
53
74
|
f'to AttackerAttachment "{self.name}".'
|
|
54
75
|
)
|
|
55
76
|
|
|
56
|
-
# Get the
|
|
57
|
-
|
|
58
|
-
if ep_tuple[0] == asset), None)
|
|
77
|
+
# Get the entry point tuple for the asset if it already exists
|
|
78
|
+
entry_point_tuple = self.get_entry_point_tuple(asset)
|
|
59
79
|
|
|
60
|
-
if
|
|
61
|
-
if attackstep_name not in
|
|
80
|
+
if entry_point_tuple:
|
|
81
|
+
if attackstep_name not in entry_point_tuple[1]:
|
|
62
82
|
# If it exists and does not already have the attack step,
|
|
63
83
|
# add it
|
|
64
|
-
|
|
84
|
+
entry_point_tuple[1].append(attackstep_name)
|
|
65
85
|
else:
|
|
66
86
|
logger.info(
|
|
67
87
|
f'Entry point "{attackstep_name}" on asset "{asset.name}"'
|
|
68
88
|
f' already existed for AttackerAttachment "{self.name}".'
|
|
69
89
|
)
|
|
70
90
|
else:
|
|
71
|
-
# Otherwise, create the
|
|
91
|
+
# Otherwise, create the entry point tuple and the initial entry
|
|
72
92
|
# point
|
|
73
93
|
self.entry_points.append((asset, [attackstep_name]))
|
|
74
94
|
|
|
75
|
-
def
|
|
95
|
+
def remove_entry_point(
|
|
76
96
|
self, asset: SchemaGeneratedClass, attackstep_name: str):
|
|
77
|
-
"""Remove an
|
|
97
|
+
"""Remove an entry point from an AttackerAttachment if it exists
|
|
98
|
+
|
|
99
|
+
Arguments:
|
|
100
|
+
asset - the asset to remove the entry point from
|
|
101
|
+
"""
|
|
78
102
|
|
|
79
103
|
logger.debug(
|
|
80
104
|
f'Remove entry point "{attackstep_name}" on asset "{asset.name}" '
|
|
81
105
|
f'from AttackerAttachment "{self.name}".'
|
|
82
106
|
)
|
|
83
107
|
|
|
84
|
-
# Get the
|
|
85
|
-
|
|
86
|
-
if ep_tuple[0] == asset), None)
|
|
108
|
+
# Get the entry point tuple for the asset if it exists
|
|
109
|
+
entry_point_tuple = self.get_entry_point_tuple(asset)
|
|
87
110
|
|
|
88
|
-
if
|
|
89
|
-
if attackstep_name in
|
|
111
|
+
if entry_point_tuple:
|
|
112
|
+
if attackstep_name in entry_point_tuple[1]:
|
|
90
113
|
# If it exists and not already has the attack step, add it
|
|
91
|
-
|
|
114
|
+
entry_point_tuple[1].remove(attackstep_name)
|
|
92
115
|
else:
|
|
93
116
|
logger.warning(
|
|
94
117
|
f'Failed to find entry point "{attackstep_name}" on '
|
|
@@ -96,8 +119,8 @@ class AttackerAttachment:
|
|
|
96
119
|
f'"{self.name}". Nothing to remove.'
|
|
97
120
|
)
|
|
98
121
|
|
|
99
|
-
if not
|
|
100
|
-
self.entry_points.remove(
|
|
122
|
+
if not entry_point_tuple[1]:
|
|
123
|
+
self.entry_points.remove(entry_point_tuple)
|
|
101
124
|
else:
|
|
102
125
|
logger.warning(
|
|
103
126
|
f'Failed to find entry points on asset "{asset.name}" '
|
|
@@ -205,6 +228,12 @@ class Model():
|
|
|
205
228
|
for association in asset.associations:
|
|
206
229
|
self.remove_asset_from_association(asset, association)
|
|
207
230
|
|
|
231
|
+
# Also remove all of the entry points
|
|
232
|
+
for attacker in self.attackers:
|
|
233
|
+
entry_point_tuple = attacker.get_entry_point_tuple(asset)
|
|
234
|
+
if entry_point_tuple:
|
|
235
|
+
attacker.entry_points.remove(entry_point_tuple)
|
|
236
|
+
|
|
208
237
|
self.assets.remove(asset)
|
|
209
238
|
|
|
210
239
|
def remove_asset_from_association(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mal-toolbox"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.9"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="Andrei Buhaiu", email="buhaiu@kth.se" },
|
|
6
6
|
{ name="Giuseppe Nebbione", email="nebbione@kth.se" },
|
|
@@ -53,7 +53,6 @@ filterwarnings = "ignore:invalid escape sequence"
|
|
|
53
53
|
exclude = [
|
|
54
54
|
'maltoolbox/ingestors',
|
|
55
55
|
'maltoolbox/translators',
|
|
56
|
-
'maltoolbox/language/compiler'
|
|
57
|
-
'venv'
|
|
56
|
+
'maltoolbox/language/compiler'
|
|
58
57
|
]
|
|
59
58
|
allow_redefinition = true
|
|
@@ -51,7 +51,7 @@ def create_association(
|
|
|
51
51
|
|
|
52
52
|
### Tests
|
|
53
53
|
|
|
54
|
-
def
|
|
54
|
+
def test_attacker_attachment_add_entry_point(model: Model):
|
|
55
55
|
""""""
|
|
56
56
|
|
|
57
57
|
asset1 = create_application_asset(model, "Asset1")
|
|
@@ -63,29 +63,29 @@ def test_attacker_attachment_add_entrypoint(model: Model):
|
|
|
63
63
|
attacker1 = AttackerAttachment()
|
|
64
64
|
model.add_attacker(attacker1)
|
|
65
65
|
|
|
66
|
-
attacker1.
|
|
66
|
+
attacker1.add_entry_point(asset1, 'read')
|
|
67
67
|
assert len(attacker1.entry_points) == 1
|
|
68
68
|
assert attacker1.entry_points[0][0] == asset1
|
|
69
69
|
assert attacker1.entry_points[0][1] == ['read']
|
|
70
70
|
|
|
71
|
-
attacker1.
|
|
71
|
+
attacker1.add_entry_point(asset1, 'access')
|
|
72
72
|
assert len(attacker1.entry_points) == 1
|
|
73
73
|
assert attacker1.entry_points[0][0] == asset1
|
|
74
74
|
assert attacker1.entry_points[0][1] == ['read', 'access']
|
|
75
75
|
|
|
76
76
|
# Try to add already existing entry point
|
|
77
|
-
attacker1.
|
|
77
|
+
attacker1.add_entry_point(asset1, 'access')
|
|
78
78
|
assert len(attacker1.entry_points) == 1
|
|
79
79
|
assert attacker1.entry_points[0][0] == asset1
|
|
80
80
|
assert attacker1.entry_points[0][1] == ['read', 'access']
|
|
81
81
|
|
|
82
|
-
attacker1.
|
|
82
|
+
attacker1.add_entry_point(asset2, 'access')
|
|
83
83
|
assert len(attacker1.entry_points) == 2
|
|
84
84
|
assert attacker1.entry_points[1][0] == asset2
|
|
85
85
|
assert attacker1.entry_points[1][1] == ['access']
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
def
|
|
88
|
+
def test_attacker_attachment_remove_entry_point(model: Model):
|
|
89
89
|
""""""
|
|
90
90
|
|
|
91
91
|
asset1 = create_application_asset(model, "Asset1")
|
|
@@ -97,9 +97,9 @@ def test_attacker_attachment_remove_entrypoint(model: Model):
|
|
|
97
97
|
attacker1 = AttackerAttachment()
|
|
98
98
|
model.add_attacker(attacker1)
|
|
99
99
|
|
|
100
|
-
attacker1.
|
|
101
|
-
attacker1.
|
|
102
|
-
attacker1.
|
|
100
|
+
attacker1.add_entry_point(asset1, 'read')
|
|
101
|
+
attacker1.add_entry_point(asset1, 'access')
|
|
102
|
+
attacker1.add_entry_point(asset2, 'access')
|
|
103
103
|
|
|
104
104
|
assert len(attacker1.entry_points) == 2
|
|
105
105
|
assert attacker1.entry_points[0][0] == asset1
|
|
@@ -107,7 +107,7 @@ def test_attacker_attachment_remove_entrypoint(model: Model):
|
|
|
107
107
|
assert attacker1.entry_points[1][0] == asset2
|
|
108
108
|
assert attacker1.entry_points[1][1] == ['access']
|
|
109
109
|
|
|
110
|
-
attacker1.
|
|
110
|
+
attacker1.remove_entry_point(asset1, 'read')
|
|
111
111
|
assert len(attacker1.entry_points) == 2
|
|
112
112
|
assert attacker1.entry_points[0][0] == asset1
|
|
113
113
|
assert attacker1.entry_points[0][1] == ['access']
|
|
@@ -116,29 +116,85 @@ def test_attacker_attachment_remove_entrypoint(model: Model):
|
|
|
116
116
|
|
|
117
117
|
# Try to remove inexistent entry point, but the asset is still present in
|
|
118
118
|
# the list of entry points
|
|
119
|
-
attacker1.
|
|
119
|
+
attacker1.remove_entry_point(asset1, 'read')
|
|
120
120
|
assert len(attacker1.entry_points) == 2
|
|
121
121
|
assert attacker1.entry_points[0][0] == asset1
|
|
122
122
|
assert attacker1.entry_points[0][1] == ['access']
|
|
123
123
|
assert attacker1.entry_points[1][0] == asset2
|
|
124
124
|
assert attacker1.entry_points[1][1] == ['access']
|
|
125
125
|
|
|
126
|
-
attacker1.
|
|
126
|
+
attacker1.remove_entry_point(asset1, 'access')
|
|
127
127
|
assert len(attacker1.entry_points) == 1
|
|
128
128
|
assert attacker1.entry_points[0][0] == asset2
|
|
129
129
|
assert attacker1.entry_points[0][1] == ['access']
|
|
130
130
|
|
|
131
131
|
# Try to remove inexistent entry point, where the asset is no longer in
|
|
132
132
|
# the list of entry points
|
|
133
|
-
attacker1.
|
|
133
|
+
attacker1.remove_entry_point(asset1, 'access')
|
|
134
134
|
assert len(attacker1.entry_points) == 1
|
|
135
135
|
assert attacker1.entry_points[0][0] == asset2
|
|
136
136
|
assert attacker1.entry_points[0][1] == ['access']
|
|
137
137
|
|
|
138
|
-
attacker1.
|
|
138
|
+
attacker1.remove_entry_point(asset2, 'access')
|
|
139
139
|
assert len(attacker1.entry_points) == 0
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
def test_attacker_attachment_remove_asset(model: Model):
|
|
143
|
+
""""""
|
|
144
|
+
|
|
145
|
+
asset1 = create_application_asset(model, "Asset1")
|
|
146
|
+
asset2 = create_application_asset(model, "Asset2")
|
|
147
|
+
model.add_asset(asset1)
|
|
148
|
+
model.add_asset(asset2)
|
|
149
|
+
|
|
150
|
+
attacker1 = AttackerAttachment()
|
|
151
|
+
model.add_attacker(attacker1)
|
|
152
|
+
attacker2 = AttackerAttachment()
|
|
153
|
+
model.add_attacker(attacker2)
|
|
154
|
+
|
|
155
|
+
attacker1.add_entry_point(asset1, 'read')
|
|
156
|
+
attacker1.add_entry_point(asset1, 'access')
|
|
157
|
+
attacker1.add_entry_point(asset2, 'access')
|
|
158
|
+
|
|
159
|
+
attacker2.add_entry_point(asset1, 'read')
|
|
160
|
+
attacker2.add_entry_point(asset2, 'read')
|
|
161
|
+
attacker2.add_entry_point(asset2, 'access')
|
|
162
|
+
|
|
163
|
+
assert len(attacker1.entry_points) == 2
|
|
164
|
+
assert attacker1.entry_points[0][0] == asset1
|
|
165
|
+
assert attacker1.entry_points[0][1] == ['read', 'access']
|
|
166
|
+
assert attacker1.entry_points[1][0] == asset2
|
|
167
|
+
assert attacker1.entry_points[1][1] == ['access']
|
|
168
|
+
|
|
169
|
+
assert len(attacker2.entry_points) == 2
|
|
170
|
+
assert attacker2.entry_points[0][0] == asset1
|
|
171
|
+
assert attacker2.entry_points[0][1] == ['read']
|
|
172
|
+
assert attacker2.entry_points[1][0] == asset2
|
|
173
|
+
assert attacker2.entry_points[1][1] == ['read', 'access']
|
|
174
|
+
|
|
175
|
+
model.remove_asset(asset2)
|
|
176
|
+
# All of the entry points of the asset removed should be gone, but the
|
|
177
|
+
# other assets should not be impacted.
|
|
178
|
+
assert len(attacker1.entry_points) == 1
|
|
179
|
+
assert attacker1.entry_points[0][0] == asset1
|
|
180
|
+
assert attacker1.entry_points[0][1] == ['read', 'access']
|
|
181
|
+
|
|
182
|
+
assert len(attacker2.entry_points) == 1
|
|
183
|
+
assert attacker2.entry_points[0][0] == asset1
|
|
184
|
+
assert attacker2.entry_points[0][1] == ['read']
|
|
185
|
+
|
|
186
|
+
# Try to remove inexistent entry point, where the asset is no longer in
|
|
187
|
+
# the list of entry points
|
|
188
|
+
attacker1.remove_entry_point(asset2, 'access')
|
|
189
|
+
assert len(attacker1.entry_points) == 1
|
|
190
|
+
assert attacker1.entry_points[0][0] == asset1
|
|
191
|
+
assert attacker1.entry_points[0][1] == ['read', 'access']
|
|
192
|
+
|
|
193
|
+
assert len(attacker2.entry_points) == 1
|
|
194
|
+
assert attacker2.entry_points[0][0] == asset1
|
|
195
|
+
assert attacker2.entry_points[0][1] == ['read']
|
|
196
|
+
|
|
197
|
+
|
|
142
198
|
def test_model_add_asset(model: Model):
|
|
143
199
|
"""Make sure assets are added correctly"""
|
|
144
200
|
|
|
@@ -688,17 +744,17 @@ def test_model_attacker_to_dict(model: Model):
|
|
|
688
744
|
attacker_dict = ret[1]
|
|
689
745
|
assert attacker_dict.get('name') == attacker.name
|
|
690
746
|
|
|
691
|
-
#
|
|
692
|
-
|
|
747
|
+
# entry_points_dict has asset IDs as keys
|
|
748
|
+
entry_points_dict = attacker_dict.get('entry_points')
|
|
693
749
|
|
|
694
750
|
# attacker should be attached to p1, therefore p1s
|
|
695
|
-
# id should be a key in the
|
|
696
|
-
assert p1.id is not None and
|
|
697
|
-
assert p1.id in
|
|
751
|
+
# id should be a key in the entry_points_dict
|
|
752
|
+
assert p1.id is not None and entry_points_dict
|
|
753
|
+
assert p1.id in entry_points_dict
|
|
698
754
|
|
|
699
|
-
# The given steps should be inside the
|
|
755
|
+
# The given steps should be inside the entry_point of
|
|
700
756
|
# the attacker for asset p1
|
|
701
|
-
assert
|
|
757
|
+
assert entry_points_dict[p1.id]['attack_steps'] == attack_steps
|
|
702
758
|
|
|
703
759
|
|
|
704
760
|
def test_serialize(model: Model):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|