mal-toolbox 0.2.0__py3-none-any.whl → 0.3.1__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,59 +1,68 @@
1
1
  """
2
- MAL-Toolbox Attack Graph Node Dataclass
2
+ MAL-Toolbox Attack Graph Node
3
3
  """
4
4
 
5
5
  from __future__ import annotations
6
6
  import copy
7
- from dataclasses import field, dataclass
8
7
  from functools import cached_property
9
- from typing import Any, Optional
8
+ from typing import TYPE_CHECKING
10
9
 
11
- from . import Attacker
12
- from ..language import LanguageGraphAttackStep
10
+ if TYPE_CHECKING:
11
+ from typing import Any, Optional
12
+ from . import Attacker
13
+ from ..language import LanguageGraphAttackStep, Detector
14
+ from ..model import ModelAsset
13
15
 
14
- @dataclass
15
16
  class AttackGraphNode:
16
17
  """Node part of AttackGraph"""
17
- type: str
18
- lang_graph_attack_step: LanguageGraphAttackStep
19
- name: str
20
- ttc: Optional[dict] = None
21
- id: Optional[int] = None
22
- asset: Optional[Any] = None
23
- children: list[AttackGraphNode] = field(default_factory=list)
24
- parents: list[AttackGraphNode] = field(default_factory=list)
25
- defense_status: Optional[float] = None
26
- existence_status: Optional[bool] = None
27
- is_viable: bool = True
28
- is_necessary: bool = True
29
- compromised_by: list[Attacker] = field(default_factory=list)
30
- tags: set[str] = field(default_factory=set)
31
- attributes: Optional[dict] = None
32
-
33
- # Optional extra metadata for AttackGraphNode
34
- extras: dict = field(default_factory=dict)
35
18
 
19
+ def __init__(
20
+ self,
21
+ node_id: int,
22
+ lg_attack_step: LanguageGraphAttackStep,
23
+ model_asset: Optional[ModelAsset] = None,
24
+ defense_status: Optional[float] = None,
25
+ existence_status: Optional[bool] = None
26
+ ):
27
+ self.lg_attack_step = lg_attack_step
28
+ self.name = lg_attack_step.name
29
+ self.type = lg_attack_step.type
30
+ self.ttc = lg_attack_step.ttc
31
+ self.tags = lg_attack_step.tags
32
+ self.detectors = lg_attack_step.detectors
33
+
34
+ self.id = node_id
35
+ self.model_asset = model_asset
36
+ self.defense_status = defense_status
37
+ self.existence_status = existence_status
38
+
39
+ self.children: set[AttackGraphNode] = set()
40
+ self.parents: set[AttackGraphNode] = set()
41
+ self.is_viable: bool = True
42
+ self.is_necessary: bool = True
43
+ self.compromised_by: set[Attacker] = set()
44
+ self.extras: dict = {}
36
45
 
37
46
  def to_dict(self) -> dict:
38
47
  """Convert node to dictionary"""
39
48
  node_dict: dict = {
40
49
  'id': self.id,
41
50
  'type': self.type,
42
- 'lang_graph_attack_step': self.lang_graph_attack_step.full_name,
51
+ 'lang_graph_attack_step': self.lg_attack_step.full_name,
43
52
  'name': self.name,
44
53
  'ttc': self.ttc,
45
- 'children': {},
46
- 'parents': {},
54
+ 'children': {child.id: child.full_name for child in
55
+ self.children},
56
+ 'parents': {parent.id: parent.full_name for parent in
57
+ self.parents},
47
58
  'compromised_by': [attacker.name for attacker in \
48
59
  self.compromised_by]
49
60
  }
50
61
 
51
- for child in self.children:
52
- node_dict['children'][child.id] = child.full_name
53
- for parent in self.parents:
54
- node_dict['parents'][parent.id] = parent.full_name
55
- if self.asset is not None:
56
- node_dict['asset'] = str(self.asset.name)
62
+ for detector in self.detectors.values():
63
+ node_dict.setdefault('detectors', {})[detector.name] = detector.to_dict()
64
+ if self.model_asset is not None:
65
+ node_dict['asset'] = str(self.model_asset.name)
57
66
  if self.defense_status is not None:
58
67
  node_dict['defense_status'] = str(self.defense_status)
59
68
  if self.existence_status is not None:
@@ -71,7 +80,8 @@ class AttackGraphNode:
71
80
 
72
81
 
73
82
  def __repr__(self) -> str:
74
- return str(self.to_dict())
83
+ return (f'AttackGraphNode(name: "{self.full_name}", id: {self.id}, '
84
+ f'type: {self.type})')
75
85
 
76
86
 
77
87
  def __deepcopy__(self, memo) -> AttackGraphNode:
@@ -89,27 +99,19 @@ class AttackGraphNode:
89
99
  return memo[id(self)]
90
100
 
91
101
  copied_node = AttackGraphNode(
92
- self.type,
93
- self.lang_graph_attack_step,
94
- self.name,
95
- self.ttc,
96
- self.id,
97
- self.asset,
98
- [],
99
- [],
100
- self.defense_status,
101
- self.existence_status,
102
- self.is_viable,
103
- self.is_necessary,
104
- [],
105
- set(),
106
- {},
107
- {}
102
+ node_id = self.id,
103
+ model_asset = self.model_asset,
104
+ lg_attack_step = self.lg_attack_step
108
105
  )
109
106
 
110
107
  copied_node.tags = copy.deepcopy(self.tags, memo)
111
- copied_node.attributes = copy.deepcopy(self.attributes, memo)
112
108
  copied_node.extras = copy.deepcopy(self.extras, memo)
109
+ copied_node.ttc = copy.deepcopy(self.ttc, memo)
110
+
111
+ copied_node.defense_status = self.defense_status
112
+ copied_node.existence_status = self.existence_status
113
+ copied_node.is_viable = self.is_viable
114
+ copied_node.is_necessary = self.is_necessary
113
115
 
114
116
  # Remember that self was already copied
115
117
  memo[id(self)] = copied_node
@@ -187,8 +189,8 @@ class AttackGraphNode:
187
189
  asset name to which the attack step belongs and attack step name
188
190
  itself.
189
191
  """
190
- if self.asset:
191
- full_name = self.asset.name + ':' + self.name
192
+ if self.model_asset:
193
+ full_name = self.model_asset.name + ':' + self.name
192
194
  else:
193
195
  full_name = str(self.id) + ':' + self.name
194
196
  return full_name
@@ -196,4 +198,4 @@ class AttackGraphNode:
196
198
 
197
199
  @cached_property
198
200
  def info(self) -> dict[str, str]:
199
- return self.lang_graph_attack_step.info
201
+ return self.lg_attack_step.info
@@ -186,7 +186,8 @@ def get_defense_surface(graph: AttackGraph) -> list[AttackGraphNode]:
186
186
  graph - the attack graph
187
187
  """
188
188
  logger.debug('Get the defense surface.')
189
- return [node for node in graph.nodes if node.is_available_defense()]
189
+ return [node for node in graph.nodes.values()
190
+ if node.is_available_defense()]
190
191
 
191
192
  def get_enabled_defenses(graph: AttackGraph) -> list[AttackGraphNode]:
192
193
  """
@@ -197,4 +198,5 @@ def get_enabled_defenses(graph: AttackGraph) -> list[AttackGraphNode]:
197
198
  graph - the attack graph
198
199
  """
199
200
  logger.debug('Get the enabled defenses.')
200
- return [node for node in graph.nodes if node.is_enabled_defense()]
201
+ return [node for node in graph.nodes.values()
202
+ if node.is_enabled_defense()]
maltoolbox/file_utils.py CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  import json
4
4
  import yaml
5
- from python_jsonschema_objects.literals import LiteralValue
6
5
 
7
6
  def save_dict_to_json_file(filename: str, serialized_object: dict) -> None:
8
7
  """Save serialized object to a json file.
@@ -28,13 +27,6 @@ def save_dict_to_yaml_file(filename: str, serialized_object: dict) -> None:
28
27
  def ignore_aliases(self, data):
29
28
  return True
30
29
 
31
- # Handle Literal values from jsonschema_objects
32
- yaml.add_multi_representer(
33
- LiteralValue,
34
- lambda dumper, data: dumper.represent_data(data._value),
35
- NoAliasSafeDumper
36
- )
37
-
38
30
  with open(filename, 'w', encoding='utf-8') as f:
39
31
  yaml.dump(serialized_object, f, Dumper=NoAliasSafeDumper)
40
32
 
@@ -7,9 +7,6 @@ import logging
7
7
 
8
8
  from py2neo import Graph, Node, Relationship, Subgraph
9
9
 
10
- from ..model import AttackerAttachment, Model
11
- from ..language import LanguageGraph, LanguageClassesFactory
12
-
13
10
  logger = logging.getLogger(__name__)
14
11
 
15
12
  def ingest_attack_graph(graph,
@@ -90,26 +87,24 @@ def ingest_model(model,
90
87
  nodes = {}
91
88
  rels = []
92
89
 
93
- for asset in model.assets:
94
-
95
- nodes[str(asset.id)] = Node(str(asset.type),
96
- name=str(asset.name),
97
- asset_id=str(asset.id),
98
- type=str(asset.type))
99
-
100
- for assoc in model.associations:
101
- firstElementName, secondElementName = assoc._properties.keys()
102
- firstElements = getattr(assoc, firstElementName)
103
- secondElements = getattr(assoc, secondElementName)
104
- for first_asset in firstElements:
105
- for second_asset in secondElements:
106
- rels.append(Relationship(nodes[str(first_asset.id)],
107
- str(firstElementName),
108
- nodes[str(second_asset.id)]))
109
- rels.append(Relationship(nodes[str(second_asset.id)],
110
- str(secondElementName),
111
- nodes[str(first_asset.id)]))
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
+ )
112
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
+ )
113
108
 
114
109
  subgraph = Subgraph(list(nodes.values()), rels)
115
110
 
@@ -118,138 +113,132 @@ def ingest_model(model,
118
113
  g.commit(tx)
119
114
 
120
115
 
121
- def get_model(
122
- uri: str,
123
- username: str,
124
- password: str,
125
- dbname: str,
126
- lang_graph: LanguageGraph,
127
- lang_classes_factory: LanguageClassesFactory
128
- ) -> Model:
129
- """Load a model from Neo4j"""
130
-
131
- g = Graph(uri=uri, user=username, password=password, name=dbname)
132
-
133
- instance_model = Model('Neo4j imported model', lang_classes_factory)
134
- # Get all assets
135
- assets_results = g.run('MATCH (a) WHERE a.type IS NOT NULL RETURN DISTINCT a').data()
136
- for asset in assets_results:
137
- asset_data = dict(asset['a'])
138
- logger.debug(
139
- 'Loading asset from Neo4j instance:\n%s', str(asset_data)
140
- )
141
- if asset_data['type'] == 'Attacker':
142
- attacker_id = int(asset_data['asset_id'])
143
- attacker = AttackerAttachment()
144
- attacker.entry_points = []
145
- instance_model.add_attacker(attacker, attacker_id = attacker_id)
146
- continue
147
-
148
- if not hasattr(lang_classes_factory.ns, asset_data['type']):
149
- msg = 'Failed to find %s asset in language specification!'
150
- logger.error(msg, asset_data["type"])
151
- raise LookupError(msg % asset_data["type"])
152
-
153
- asset_obj = getattr(lang_classes_factory.ns,
154
- asset_data['type'])(name = asset_data['name'])
155
- asset_id = int(asset_data['asset_id'])
156
-
157
- #TODO Process defense values when they are included in Neo4j
158
- instance_model.add_asset(asset_obj, asset_id)
159
-
160
- # Get all relationships
161
- assocs_results = g.run('MATCH (a)-[r1]->(b),(a)<-[r2]-(b) WHERE a.type IS NOT NULL RETURN DISTINCT a, r1, r2, b').data()
162
-
163
- for assoc in assocs_results:
164
- left_field = list(assoc['r1'].types())[0]
165
- right_field = list(assoc['r2'].types())[0]
166
- left_asset = dict(assoc['a'])
167
- right_asset = dict(assoc['b'])
168
-
169
- logger.debug(
170
- 'Load association ("%s", "%s", "%s", "%s") from Neo4j instance.',
171
- left_field, right_field, left_asset["type"], right_asset["type"]
172
- )
173
-
174
- left_id = int(left_asset['asset_id'])
175
- right_id = int(right_asset['asset_id'])
176
-
177
- attacker_id = None
178
- if left_field == 'firstSteps':
179
- attacker_id = right_id
180
- target_id = left_id
181
- target_prop = right_field
182
- elif right_field == 'firstSteps':
183
- attacker_id = left_id
184
- target_id = right_id
185
- target_prop = left_field
186
-
187
- if attacker_id is not None:
188
- attacker = instance_model.get_attacker_by_id(attacker_id)
189
- if not attacker:
190
- msg = 'Failed to find attacker with id %s in model!'
191
- logger.error(msg, attacker_id)
192
- raise LookupError(msg % attacker_id)
193
- target_asset = instance_model.get_asset_by_id(target_id)
194
- if not target_asset:
195
- msg = 'Failed to find asset with id %d in model!'
196
- logger.error(msg, target_id)
197
- raise LookupError(msg % target_id)
198
- attacker.entry_points.append((target_asset,
199
- [target_prop]))
200
- continue
201
-
202
- left_asset = instance_model.get_asset_by_id(left_id)
203
- if left_asset is None:
204
- msg = 'Failed to find asset with id %d in model!'
205
- logger.error(msg, left_id)
206
- raise LookupError(msg % left_id)
207
- right_asset = instance_model.get_asset_by_id(right_id)
208
- if right_asset is None:
209
- msg = 'Failed to find asset with id %d in model!'
210
- logger.error(msg, right_id)
211
- raise LookupError(msg % right_id)
212
-
213
- assoc = lang_graph.get_association_by_fields_and_assets(
214
- left_field,
215
- right_field,
216
- left_asset.type,
217
- right_asset.type)
218
-
219
- if not assoc:
220
- logger.error(
221
- 'Failed to find ("%s", "%s", "%s", "%s")'
222
- 'association in language specification!',
223
- left_asset.type, right_asset.type,
224
- left_field, right_field
225
- )
226
- return None
227
-
228
- logger.debug('Found "%s" association.', assoc.name)
229
-
230
- assoc_name = lang_classes_factory.get_association_by_signature(
231
- assoc.name,
232
- left_asset.type,
233
- right_asset.type
234
- )
235
-
236
- if not assoc_name:
237
- msg = 'Failed to find \"%s\" association in language specification!'
238
- logger.error(msg, assoc.name)
239
- raise LookupError(msg % assoc.name)
240
-
241
- assoc = getattr(lang_classes_factory.ns, assoc_name)()
242
- setattr(assoc, left_field, [left_asset])
243
- setattr(assoc, right_field, [right_asset])
244
- if not (instance_model.association_exists_between_assets(
245
- assoc_name,
246
- left_asset,
247
- right_asset
248
- ) or instance_model.association_exists_between_assets(
249
- assoc_name,
250
- right_asset,
251
- left_asset
252
- )):
253
- instance_model.add_association(assoc)
254
-
255
- return instance_model
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
@@ -1,8 +1,12 @@
1
1
  """Contains tools to process MAL languages"""
2
2
 
3
- from .languagegraph import (LanguageGraph,
3
+ from .languagegraph import (
4
+ Context,
5
+ Detector,
4
6
  ExpressionsChain,
7
+ LanguageGraph,
5
8
  LanguageGraphAsset,
9
+ LanguageGraphAssociation,
6
10
  LanguageGraphAttackStep,
7
- disaggregate_attack_step_full_name)
8
- from .classes_factory import LanguageClassesFactory
11
+ disaggregate_attack_step_full_name,
12
+ )