mal-toolbox 0.1.12__py3-none-any.whl → 0.3.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,55 +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
- from typing import Any, Optional
7
+ from functools import cached_property
8
+ from typing import TYPE_CHECKING
9
9
 
10
- from . import Attacker
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
11
15
 
12
- @dataclass
13
16
  class AttackGraphNode:
14
17
  """Node part of AttackGraph"""
15
- type: str
16
- name: str
17
- ttc: Optional[dict] = None
18
- id: Optional[int] = None
19
- asset: Optional[Any] = None
20
- children: list[AttackGraphNode] = field(default_factory=list)
21
- parents: list[AttackGraphNode] = field(default_factory=list)
22
- defense_status: Optional[float] = None
23
- existence_status: Optional[bool] = None
24
- is_viable: bool = True
25
- is_necessary: bool = True
26
- compromised_by: list[Attacker] = field(default_factory=list)
27
- mitre_info: Optional[str] = None
28
- tags: list[str] = field(default_factory=lambda: [])
29
- attributes: Optional[dict] = None
30
-
31
- # Optional extra metadata for AttackGraphNode
32
- extras: dict = field(default_factory=dict)
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 = {}
33
45
 
34
46
  def to_dict(self) -> dict:
35
47
  """Convert node to dictionary"""
36
48
  node_dict: dict = {
37
49
  'id': self.id,
38
50
  'type': self.type,
51
+ 'lang_graph_attack_step': self.lg_attack_step.full_name,
39
52
  'name': self.name,
40
53
  'ttc': self.ttc,
41
- 'children': {},
42
- '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},
43
58
  'compromised_by': [attacker.name for attacker in \
44
59
  self.compromised_by]
45
60
  }
46
61
 
47
- for child in self.children:
48
- node_dict['children'][child.id] = child.full_name
49
- for parent in self.parents:
50
- node_dict['parents'][parent.id] = parent.full_name
51
- if self.asset is not None:
52
- 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)
53
66
  if self.defense_status is not None:
54
67
  node_dict['defense_status'] = str(self.defense_status)
55
68
  if self.existence_status is not None:
@@ -58,17 +71,18 @@ class AttackGraphNode:
58
71
  node_dict['is_viable'] = str(self.is_viable)
59
72
  if self.is_necessary is not None:
60
73
  node_dict['is_necessary'] = str(self.is_necessary)
61
- if self.mitre_info is not None:
62
- node_dict['mitre_info'] = str(self.mitre_info)
63
74
  if self.tags:
64
- node_dict['tags'] = str(self.tags)
75
+ node_dict['tags'] = list(self.tags)
65
76
  if self.extras:
66
77
  node_dict['extras'] = self.extras
67
78
 
68
79
  return node_dict
69
80
 
81
+
70
82
  def __repr__(self) -> str:
71
- return str(self.to_dict())
83
+ return (f'AttackGraphNode(name: "{self.full_name}", id: {self.id}, '
84
+ f'type: {self.type})')
85
+
72
86
 
73
87
  def __deepcopy__(self, memo) -> AttackGraphNode:
74
88
  """Deep copy an attackgraph node
@@ -85,33 +99,26 @@ class AttackGraphNode:
85
99
  return memo[id(self)]
86
100
 
87
101
  copied_node = AttackGraphNode(
88
- self.type,
89
- self.name,
90
- self.ttc,
91
- self.id,
92
- self.asset,
93
- [],
94
- [],
95
- self.defense_status,
96
- self.existence_status,
97
- self.is_viable,
98
- self.is_necessary,
99
- [],
100
- self.mitre_info,
101
- [],
102
- {},
103
- {}
102
+ node_id = self.id,
103
+ model_asset = self.model_asset,
104
+ lg_attack_step = self.lg_attack_step
104
105
  )
105
106
 
106
107
  copied_node.tags = copy.deepcopy(self.tags, memo)
107
- copied_node.attributes = copy.deepcopy(self.attributes, memo)
108
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
109
115
 
110
116
  # Remember that self was already copied
111
117
  memo[id(self)] = copied_node
112
118
 
113
119
  return copied_node
114
120
 
121
+
115
122
  def is_compromised(self) -> bool:
116
123
  """
117
124
  Return True if any attackers have compromised this node.
@@ -119,6 +126,7 @@ class AttackGraphNode:
119
126
  """
120
127
  return len(self.compromised_by) > 0
121
128
 
129
+
122
130
  def is_compromised_by(self, attacker: Attacker) -> bool:
123
131
  """
124
132
  Return True if the attacker given as an argument has compromised this
@@ -130,6 +138,7 @@ class AttackGraphNode:
130
138
  """
131
139
  return attacker in self.compromised_by
132
140
 
141
+
133
142
  def compromise(self, attacker: Attacker) -> None:
134
143
  """
135
144
  Have the attacker given as a parameter compromise this node.
@@ -139,6 +148,7 @@ class AttackGraphNode:
139
148
  """
140
149
  attacker.compromise(self)
141
150
 
151
+
142
152
  def undo_compromise(self, attacker: Attacker) -> None:
143
153
  """
144
154
  Remove the attacker given as a parameter from the list of attackers
@@ -150,6 +160,7 @@ class AttackGraphNode:
150
160
  """
151
161
  attacker.undo_compromise(self)
152
162
 
163
+
153
164
  def is_enabled_defense(self) -> bool:
154
165
  """
155
166
  Return True if this node is a defense node and it is enabled and not
@@ -160,6 +171,7 @@ class AttackGraphNode:
160
171
  'suppress' not in self.tags and \
161
172
  self.defense_status == 1.0
162
173
 
174
+
163
175
  def is_available_defense(self) -> bool:
164
176
  """
165
177
  Return True if this node is a defense node and it is not fully enabled
@@ -169,6 +181,7 @@ class AttackGraphNode:
169
181
  'suppress' not in self.tags and \
170
182
  self.defense_status != 1.0
171
183
 
184
+
172
185
  @property
173
186
  def full_name(self) -> str:
174
187
  """
@@ -176,8 +189,13 @@ class AttackGraphNode:
176
189
  asset name to which the attack step belongs and attack step name
177
190
  itself.
178
191
  """
179
- if self.asset:
180
- full_name = self.asset.name + ':' + self.name
192
+ if self.model_asset:
193
+ full_name = self.model_asset.name + ':' + self.name
181
194
  else:
182
195
  full_name = str(self.id) + ':' + self.name
183
196
  return full_name
197
+
198
+
199
+ @cached_property
200
+ def info(self) -> dict[str, str]:
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.
@@ -24,15 +23,12 @@ def save_dict_to_yaml_file(filename: str, serialized_object: dict) -> None:
24
23
  data - dict to output as yaml
25
24
  """
26
25
 
27
- # Handle Literal values from jsonschema_objects
28
- yaml.add_multi_representer(
29
- LiteralValue,
30
- lambda dumper, data: dumper.represent_data(data._value),
31
- yaml.SafeDumper
32
- )
26
+ class NoAliasSafeDumper(yaml.SafeDumper):
27
+ def ignore_aliases(self, data):
28
+ return True
33
29
 
34
30
  with open(filename, 'w', encoding='utf-8') as f:
35
- yaml.dump(serialized_object, f, Dumper=yaml.SafeDumper)
31
+ yaml.dump(serialized_object, f, Dumper=NoAliasSafeDumper)
36
32
 
37
33
 
38
34
  def load_dict_from_yaml_file(filename: str) -> dict:
@@ -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,4 +1,12 @@
1
1
  """Contains tools to process MAL languages"""
2
2
 
3
- from .languagegraph import LanguageGraph
4
- from .classes_factory import LanguageClassesFactory
3
+ from .languagegraph import (
4
+ Context,
5
+ Detector,
6
+ ExpressionsChain,
7
+ LanguageGraph,
8
+ LanguageGraphAsset,
9
+ LanguageGraphAssociation,
10
+ LanguageGraphAttackStep,
11
+ disaggregate_attack_step_full_name,
12
+ )