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/attackgraph/query.py
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MAL-Toolbox Attack Graph Query Submodule
|
|
3
|
-
|
|
4
|
-
This submodule contains functions that analyze the information present in the
|
|
5
|
-
attack graph, but do not alter the structure or nodes in any way.
|
|
6
|
-
"""
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
from collections.abc import Iterable
|
|
9
|
-
import logging
|
|
10
|
-
from typing import TYPE_CHECKING, Optional
|
|
11
|
-
|
|
12
|
-
from .attackgraph import AttackGraph, Attacker
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from .attackgraph import AttackGraphNode
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
def is_node_traversable_by_attacker(
|
|
20
|
-
node: AttackGraphNode, attacker: Attacker
|
|
21
|
-
) -> bool:
|
|
22
|
-
"""
|
|
23
|
-
Return True or False depending if the node specified is traversable
|
|
24
|
-
for the attacker given.
|
|
25
|
-
|
|
26
|
-
Arguments:
|
|
27
|
-
node - the node we wish to evalute
|
|
28
|
-
attacker - the attacker whose traversability we are interested in
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
logger.debug(
|
|
32
|
-
'Evaluate if "%s"(%d), of type "%s", is traversable by Attacker '
|
|
33
|
-
'"%s"(%d)',
|
|
34
|
-
node.full_name,
|
|
35
|
-
node.id,
|
|
36
|
-
node.type,
|
|
37
|
-
attacker.name,
|
|
38
|
-
attacker.id
|
|
39
|
-
)
|
|
40
|
-
if not node.is_viable:
|
|
41
|
-
logger.debug(
|
|
42
|
-
'"%s"(%d) is not traversable because it is non-viable',
|
|
43
|
-
node.full_name,
|
|
44
|
-
node.id,
|
|
45
|
-
)
|
|
46
|
-
return False
|
|
47
|
-
|
|
48
|
-
match(node.type):
|
|
49
|
-
case 'or':
|
|
50
|
-
for parent in node.parents:
|
|
51
|
-
if parent.is_compromised_by(attacker):
|
|
52
|
-
logger.debug(
|
|
53
|
-
'"%s"(%d) is traversable because it is viable, and '
|
|
54
|
-
'of type "or", and its parent "%s(%d)" has already '
|
|
55
|
-
'been compromised.',
|
|
56
|
-
node.full_name,
|
|
57
|
-
node.id,
|
|
58
|
-
parent.full_name,
|
|
59
|
-
parent.id
|
|
60
|
-
)
|
|
61
|
-
return True
|
|
62
|
-
logger.debug(
|
|
63
|
-
'"%s"(%d) is not traversable because while it is '
|
|
64
|
-
'viable, and of type "or", none of its parents '
|
|
65
|
-
'have been compromised.',
|
|
66
|
-
node.full_name,
|
|
67
|
-
node.id
|
|
68
|
-
)
|
|
69
|
-
return False
|
|
70
|
-
|
|
71
|
-
case 'and':
|
|
72
|
-
for parent in node.parents:
|
|
73
|
-
if parent.is_necessary and \
|
|
74
|
-
not parent.is_compromised_by(attacker):
|
|
75
|
-
# If the parent is not present in the attacks steps
|
|
76
|
-
# already reached and is necessary.
|
|
77
|
-
logger.debug(
|
|
78
|
-
'"%s"(%d) is not traversable because while it is '
|
|
79
|
-
'viable, and of type "and", its necessary parent '
|
|
80
|
-
'"%s(%d)" has not already been compromised.',
|
|
81
|
-
node.full_name,
|
|
82
|
-
node.id,
|
|
83
|
-
parent.full_name,
|
|
84
|
-
parent.id
|
|
85
|
-
)
|
|
86
|
-
return False
|
|
87
|
-
logger.debug(
|
|
88
|
-
'"%s"(%d) is traversable because it is viable, '
|
|
89
|
-
'of type "and", and all of its necessary parents have '
|
|
90
|
-
'already been compromised.',
|
|
91
|
-
node.full_name,
|
|
92
|
-
node.id
|
|
93
|
-
)
|
|
94
|
-
return True
|
|
95
|
-
|
|
96
|
-
case 'exist' | 'notExist' | 'defense':
|
|
97
|
-
logger.warning(
|
|
98
|
-
'Nodes of type "exist", "notExist", and "defense" are never '
|
|
99
|
-
'marked as traversable. However, we do not normally check '
|
|
100
|
-
'if they are traversable. Node "%s"(%d) of type "%s" was '
|
|
101
|
-
'checked for traversability.',
|
|
102
|
-
node.full_name,
|
|
103
|
-
node.id,
|
|
104
|
-
node.type
|
|
105
|
-
)
|
|
106
|
-
return False
|
|
107
|
-
|
|
108
|
-
case _:
|
|
109
|
-
logger.error(
|
|
110
|
-
'Node "%s"(%d) has an unknown type "%s".',
|
|
111
|
-
node.full_name,
|
|
112
|
-
node.id,
|
|
113
|
-
node.type
|
|
114
|
-
)
|
|
115
|
-
return False
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def calculate_attack_surface(
|
|
119
|
-
attacker: Attacker,
|
|
120
|
-
*,
|
|
121
|
-
from_nodes: Optional[Iterable[AttackGraphNode]] = None,
|
|
122
|
-
skip_compromised: bool = False,
|
|
123
|
-
) -> set[AttackGraphNode]:
|
|
124
|
-
"""
|
|
125
|
-
Calculate the attack surface of the attacker. If from_nodes are provided
|
|
126
|
-
only calculate the attack surface stemming from those nodes, otherwise use
|
|
127
|
-
all nodes the attacker has compromised. If skip_compromised is true,
|
|
128
|
-
exclude already compromised nodes from the returned attack surface.
|
|
129
|
-
|
|
130
|
-
The attack surface includes all of the viable children nodes that are of
|
|
131
|
-
'or' type and the 'and' type children nodes which have all of their
|
|
132
|
-
necessary parents compromised by the attacker.
|
|
133
|
-
|
|
134
|
-
Arguments:
|
|
135
|
-
attacker - the Attacker whose attack surface is sought
|
|
136
|
-
from_nodes - the nodes to calculate the attack surface from; defaults
|
|
137
|
-
to the attackers compromised nodes list if omitted
|
|
138
|
-
skip_compromised - if true do not add already compromised nodes to the
|
|
139
|
-
attack surface
|
|
140
|
-
"""
|
|
141
|
-
logger.debug(
|
|
142
|
-
'Get the attack surface for Attacker "%s"(%d).',
|
|
143
|
-
attacker.name,
|
|
144
|
-
attacker.id
|
|
145
|
-
)
|
|
146
|
-
attack_surface = set()
|
|
147
|
-
frontier = from_nodes if from_nodes is not None else attacker.reached_attack_steps
|
|
148
|
-
for attack_step in frontier:
|
|
149
|
-
logger.debug(
|
|
150
|
-
'Determine attack surface stemming from '
|
|
151
|
-
'"%s"(%d) for Attacker "%s"(%d).',
|
|
152
|
-
attack_step.full_name,
|
|
153
|
-
attack_step.id,
|
|
154
|
-
attacker.name,
|
|
155
|
-
attacker.id
|
|
156
|
-
)
|
|
157
|
-
for child in attack_step.children:
|
|
158
|
-
if skip_compromised and child.is_compromised_by(attacker):
|
|
159
|
-
continue
|
|
160
|
-
if is_node_traversable_by_attacker(child, attacker) and \
|
|
161
|
-
child not in attack_surface:
|
|
162
|
-
logger.debug(
|
|
163
|
-
'Add node "%s"(%d) to the attack surface of '
|
|
164
|
-
'Attacker "%s"(%d).',
|
|
165
|
-
child.full_name,
|
|
166
|
-
child.id,
|
|
167
|
-
attacker.name,
|
|
168
|
-
attacker.id
|
|
169
|
-
)
|
|
170
|
-
attack_surface.add(child)
|
|
171
|
-
|
|
172
|
-
return attack_surface
|
|
173
|
-
|
|
174
|
-
def get_defense_surface(graph: AttackGraph) -> set[AttackGraphNode]:
|
|
175
|
-
"""
|
|
176
|
-
Get the defense surface. All non-suppressed defense steps that are not
|
|
177
|
-
already fully enabled.
|
|
178
|
-
|
|
179
|
-
Arguments:
|
|
180
|
-
graph - the attack graph
|
|
181
|
-
"""
|
|
182
|
-
logger.debug('Get the defense surface.')
|
|
183
|
-
return {node for node in graph.nodes.values()
|
|
184
|
-
if node.is_available_defense()}
|
|
185
|
-
|
|
186
|
-
def get_enabled_defenses(graph: AttackGraph) -> list[AttackGraphNode]:
|
|
187
|
-
"""
|
|
188
|
-
Get the defenses already enabled. All non-suppressed defense steps that
|
|
189
|
-
are already fully enabled.
|
|
190
|
-
|
|
191
|
-
Arguments:
|
|
192
|
-
graph - the attack graph
|
|
193
|
-
"""
|
|
194
|
-
logger.debug('Get the enabled defenses.')
|
|
195
|
-
return [node for node in graph.nodes.values()
|
|
196
|
-
if node.is_enabled_defense()]
|
maltoolbox/ingestors/neo4j.py
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MAL-Toolbox Neo4j Ingestor Module
|
|
3
|
-
"""
|
|
4
|
-
# mypy: ignore-errors
|
|
5
|
-
|
|
6
|
-
import logging
|
|
7
|
-
|
|
8
|
-
from py2neo import Graph, Node, Relationship, Subgraph
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
def ingest_attack_graph(graph,
|
|
13
|
-
uri: str,
|
|
14
|
-
username: str,
|
|
15
|
-
password: str,
|
|
16
|
-
dbname: str,
|
|
17
|
-
delete: bool = False
|
|
18
|
-
) -> None:
|
|
19
|
-
"""
|
|
20
|
-
Ingest an attack graph into a neo4j database
|
|
21
|
-
|
|
22
|
-
Arguments:
|
|
23
|
-
graph - the attackgraph provided by the atkgraph.py module.
|
|
24
|
-
uri - the URI to a running neo4j instance
|
|
25
|
-
username - the username to login on Neo4J
|
|
26
|
-
password - the password to login on Neo4J
|
|
27
|
-
dbname - the selected database
|
|
28
|
-
delete - if True, the previous content of the database is deleted
|
|
29
|
-
before ingesting the new attack graph
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
g = Graph(uri=uri, user=username, password=password, name=dbname)
|
|
33
|
-
if delete:
|
|
34
|
-
g.delete_all()
|
|
35
|
-
|
|
36
|
-
nodes = {}
|
|
37
|
-
rels = []
|
|
38
|
-
for node in graph.nodes.values():
|
|
39
|
-
node_dict = node.to_dict()
|
|
40
|
-
nodes[node.id] = Node(
|
|
41
|
-
node_dict['asset'] if 'asset' in node_dict else node_dict['id'],
|
|
42
|
-
name = node_dict['name'],
|
|
43
|
-
full_name = node.full_name,
|
|
44
|
-
type = node_dict['type'],
|
|
45
|
-
ttc = str(node_dict['ttc']),
|
|
46
|
-
is_necessary = str(node.is_necessary),
|
|
47
|
-
is_viable = str(node.is_viable),
|
|
48
|
-
compromised_by = str(node_dict['compromised_by']),
|
|
49
|
-
defense_status = node_dict['defense_status'] if 'defense_status'
|
|
50
|
-
in node_dict else 'N/A')
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
for node in graph.nodes.values():
|
|
54
|
-
for child in node.children:
|
|
55
|
-
rels.append(Relationship(nodes[node.id], nodes[child.id]))
|
|
56
|
-
|
|
57
|
-
subgraph = Subgraph(list(nodes.values()), rels)
|
|
58
|
-
|
|
59
|
-
tx = g.begin()
|
|
60
|
-
tx.create(subgraph)
|
|
61
|
-
g.commit(tx)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def ingest_model(model,
|
|
65
|
-
uri: str,
|
|
66
|
-
username: str,
|
|
67
|
-
password: str,
|
|
68
|
-
dbname: str,
|
|
69
|
-
delete: bool = False
|
|
70
|
-
) -> None:
|
|
71
|
-
"""
|
|
72
|
-
Ingest an instance model graph into a Neo4J database
|
|
73
|
-
|
|
74
|
-
Arguments:
|
|
75
|
-
model - the instance model dictionary as provided by the model.py module
|
|
76
|
-
uri - the URI to a running neo4j instance
|
|
77
|
-
username - the username to login on Neo4J
|
|
78
|
-
password - the password to login on Neo4J
|
|
79
|
-
dbname - the selected database
|
|
80
|
-
delete - if True, the previous content of the database is deleted
|
|
81
|
-
before ingesting the new attack graph
|
|
82
|
-
"""
|
|
83
|
-
g = Graph(uri=uri, user=username, password=password, name=dbname)
|
|
84
|
-
if delete:
|
|
85
|
-
g.delete_all()
|
|
86
|
-
|
|
87
|
-
nodes = {}
|
|
88
|
-
rels = []
|
|
89
|
-
|
|
90
|
-
for asset in model.assets.values():
|
|
91
|
-
nodes[str(asset.id)] = Node(
|
|
92
|
-
str(asset.type),
|
|
93
|
-
name=str(asset.name),
|
|
94
|
-
asset_id=str(asset.id),
|
|
95
|
-
type=str(asset.type)
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
for asset in model.assets.values():
|
|
99
|
-
for fieldname, other_assets in asset.associated_assets.items():
|
|
100
|
-
for other_asset in other_assets:
|
|
101
|
-
rels.append(
|
|
102
|
-
Relationship(
|
|
103
|
-
nodes[str(asset.id)],
|
|
104
|
-
str(fieldname),
|
|
105
|
-
nodes[str(other_asset.id)]
|
|
106
|
-
)
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
subgraph = Subgraph(list(nodes.values()), rels)
|
|
110
|
-
|
|
111
|
-
tx = g.begin()
|
|
112
|
-
tx.create(subgraph)
|
|
113
|
-
g.commit(tx)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
# def get_model(
|
|
117
|
-
# uri: str,
|
|
118
|
-
# username: str,
|
|
119
|
-
# password: str,
|
|
120
|
-
# dbname: str,
|
|
121
|
-
# lang_graph: LanguageGraph,
|
|
122
|
-
# ) -> Model:
|
|
123
|
-
# """Load a model from Neo4j"""
|
|
124
|
-
|
|
125
|
-
# g = Graph(uri=uri, user=username, password=password, name=dbname)
|
|
126
|
-
|
|
127
|
-
# instance_model = Model('Neo4j imported model', lang_graph)
|
|
128
|
-
# # Get all assets
|
|
129
|
-
# assets_results = g.run('MATCH (a) WHERE a.type IS NOT NULL RETURN DISTINCT a').data()
|
|
130
|
-
# for asset in assets_results:
|
|
131
|
-
# asset_data = dict(asset['a'])
|
|
132
|
-
# logger.debug(
|
|
133
|
-
# 'Loading asset from Neo4j instance:\n%s', str(asset_data)
|
|
134
|
-
# )
|
|
135
|
-
# if asset_data['type'] == 'Attacker':
|
|
136
|
-
# attacker_id = int(asset_data['asset_id'])
|
|
137
|
-
# attacker = AttackerAttachment()
|
|
138
|
-
# attacker.entry_points = []
|
|
139
|
-
# instance_model.add_attacker(attacker, attacker_id = attacker_id)
|
|
140
|
-
# continue
|
|
141
|
-
|
|
142
|
-
# asset_id = int(asset_data['asset_id'])
|
|
143
|
-
|
|
144
|
-
# #TODO Process defense values when they are included in Neo4j
|
|
145
|
-
# instance_model.add_asset(asset_data['type'], asset_id=asset_id)
|
|
146
|
-
|
|
147
|
-
# # Get all relationships
|
|
148
|
-
# assocs_results = g.run(
|
|
149
|
-
# 'MATCH (a)-[r1]->(b),(a)<-[r2]-(b) WHERE a.type IS NOT NULL RETURN DISTINCT a, r1, r2, b'
|
|
150
|
-
# ).data()
|
|
151
|
-
|
|
152
|
-
# for assoc in assocs_results:
|
|
153
|
-
# left_field = list(assoc['r1'].types())[0]
|
|
154
|
-
# right_field = list(assoc['r2'].types())[0]
|
|
155
|
-
# left_asset = dict(assoc['a'])
|
|
156
|
-
# right_asset = dict(assoc['b'])
|
|
157
|
-
|
|
158
|
-
# logger.debug(
|
|
159
|
-
# 'Load association ("%s", "%s", "%s", "%s") from Neo4j instance.',
|
|
160
|
-
# left_field, right_field, left_asset["type"], right_asset["type"]
|
|
161
|
-
# )
|
|
162
|
-
|
|
163
|
-
# left_id = int(left_asset['asset_id'])
|
|
164
|
-
# right_id = int(right_asset['asset_id'])
|
|
165
|
-
|
|
166
|
-
# attacker_id = None
|
|
167
|
-
# if left_field == 'firstSteps':
|
|
168
|
-
# attacker_id = right_id
|
|
169
|
-
# target_id = left_id
|
|
170
|
-
# target_prop = right_field
|
|
171
|
-
# elif right_field == 'firstSteps':
|
|
172
|
-
# attacker_id = left_id
|
|
173
|
-
# target_id = right_id
|
|
174
|
-
# target_prop = left_field
|
|
175
|
-
|
|
176
|
-
# if attacker_id is not None:
|
|
177
|
-
# attacker = instance_model.get_attacker_by_id(attacker_id)
|
|
178
|
-
# if not attacker:
|
|
179
|
-
# msg = 'Failed to find attacker with id %s in model!'
|
|
180
|
-
# logger.error(msg, attacker_id)
|
|
181
|
-
# raise LookupError(msg % attacker_id)
|
|
182
|
-
# target_asset = instance_model.get_asset_by_id(target_id)
|
|
183
|
-
# if not target_asset:
|
|
184
|
-
# msg = 'Failed to find asset with id %d in model!'
|
|
185
|
-
# logger.error(msg, target_id)
|
|
186
|
-
# raise LookupError(msg % target_id)
|
|
187
|
-
# attacker.entry_points.append((target_asset,
|
|
188
|
-
# [target_prop]))
|
|
189
|
-
# continue
|
|
190
|
-
|
|
191
|
-
# left_asset = instance_model.get_asset_by_id(left_id)
|
|
192
|
-
# if left_asset is None:
|
|
193
|
-
# msg = 'Failed to find asset with id %d in model!'
|
|
194
|
-
# logger.error(msg, left_id)
|
|
195
|
-
# raise LookupError(msg % left_id)
|
|
196
|
-
# right_asset = instance_model.get_asset_by_id(right_id)
|
|
197
|
-
# if right_asset is None:
|
|
198
|
-
# msg = 'Failed to find asset with id %d in model!'
|
|
199
|
-
# logger.error(msg, right_id)
|
|
200
|
-
# raise LookupError(msg % right_id)
|
|
201
|
-
|
|
202
|
-
# assoc = lang_graph.get_association_by_fields_and_assets(
|
|
203
|
-
# left_field,
|
|
204
|
-
# right_field,
|
|
205
|
-
# left_asset.type,
|
|
206
|
-
# right_asset.type)
|
|
207
|
-
|
|
208
|
-
# if not assoc:
|
|
209
|
-
# logger.error(
|
|
210
|
-
# 'Failed to find ("%s", "%s", "%s", "%s")'
|
|
211
|
-
# 'association in language specification!',
|
|
212
|
-
# left_asset.type, right_asset.type,
|
|
213
|
-
# left_field, right_field
|
|
214
|
-
# )
|
|
215
|
-
# return None
|
|
216
|
-
|
|
217
|
-
# logger.debug('Found "%s" association.', assoc.name)
|
|
218
|
-
|
|
219
|
-
# assoc_name = lang_classes_factory.get_association_by_signature(
|
|
220
|
-
# assoc.name,
|
|
221
|
-
# left_asset.type,
|
|
222
|
-
# right_asset.type
|
|
223
|
-
# )
|
|
224
|
-
|
|
225
|
-
# if not assoc_name:
|
|
226
|
-
# msg = 'Failed to find \"%s\" association in language specification!'
|
|
227
|
-
# logger.error(msg, assoc.name)
|
|
228
|
-
# raise LookupError(msg % assoc.name)
|
|
229
|
-
|
|
230
|
-
# assoc = getattr(lang_classes_factory.ns, assoc_name)()
|
|
231
|
-
# setattr(assoc, left_field, [left_asset])
|
|
232
|
-
# setattr(assoc, right_field, [right_asset])
|
|
233
|
-
# if not (instance_model.association_exists_between_assets(
|
|
234
|
-
# assoc_name,
|
|
235
|
-
# left_asset,
|
|
236
|
-
# right_asset
|
|
237
|
-
# ) or instance_model.association_exists_between_assets(
|
|
238
|
-
# assoc_name,
|
|
239
|
-
# right_asset,
|
|
240
|
-
# left_asset
|
|
241
|
-
# )):
|
|
242
|
-
# instance_model.add_association(assoc)
|
|
243
|
-
|
|
244
|
-
# return instance_model
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|