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.
@@ -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()]
@@ -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