mal-toolbox 0.1.12__py3-none-any.whl → 0.2.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.
@@ -5,14 +5,17 @@ MAL-Toolbox Attack Graph Node Dataclass
5
5
  from __future__ import annotations
6
6
  import copy
7
7
  from dataclasses import field, dataclass
8
+ from functools import cached_property
8
9
  from typing import Any, Optional
9
10
 
10
11
  from . import Attacker
12
+ from ..language import LanguageGraphAttackStep
11
13
 
12
14
  @dataclass
13
15
  class AttackGraphNode:
14
16
  """Node part of AttackGraph"""
15
17
  type: str
18
+ lang_graph_attack_step: LanguageGraphAttackStep
16
19
  name: str
17
20
  ttc: Optional[dict] = None
18
21
  id: Optional[int] = None
@@ -24,18 +27,19 @@ class AttackGraphNode:
24
27
  is_viable: bool = True
25
28
  is_necessary: bool = True
26
29
  compromised_by: list[Attacker] = field(default_factory=list)
27
- mitre_info: Optional[str] = None
28
- tags: list[str] = field(default_factory=lambda: [])
30
+ tags: set[str] = field(default_factory=set)
29
31
  attributes: Optional[dict] = None
30
32
 
31
33
  # Optional extra metadata for AttackGraphNode
32
34
  extras: dict = field(default_factory=dict)
33
35
 
36
+
34
37
  def to_dict(self) -> dict:
35
38
  """Convert node to dictionary"""
36
39
  node_dict: dict = {
37
40
  'id': self.id,
38
41
  'type': self.type,
42
+ 'lang_graph_attack_step': self.lang_graph_attack_step.full_name,
39
43
  'name': self.name,
40
44
  'ttc': self.ttc,
41
45
  'children': {},
@@ -58,18 +62,18 @@ class AttackGraphNode:
58
62
  node_dict['is_viable'] = str(self.is_viable)
59
63
  if self.is_necessary is not None:
60
64
  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
65
  if self.tags:
64
- node_dict['tags'] = str(self.tags)
66
+ node_dict['tags'] = list(self.tags)
65
67
  if self.extras:
66
68
  node_dict['extras'] = self.extras
67
69
 
68
70
  return node_dict
69
71
 
72
+
70
73
  def __repr__(self) -> str:
71
74
  return str(self.to_dict())
72
75
 
76
+
73
77
  def __deepcopy__(self, memo) -> AttackGraphNode:
74
78
  """Deep copy an attackgraph node
75
79
 
@@ -86,6 +90,7 @@ class AttackGraphNode:
86
90
 
87
91
  copied_node = AttackGraphNode(
88
92
  self.type,
93
+ self.lang_graph_attack_step,
89
94
  self.name,
90
95
  self.ttc,
91
96
  self.id,
@@ -97,8 +102,7 @@ class AttackGraphNode:
97
102
  self.is_viable,
98
103
  self.is_necessary,
99
104
  [],
100
- self.mitre_info,
101
- [],
105
+ set(),
102
106
  {},
103
107
  {}
104
108
  )
@@ -112,6 +116,7 @@ class AttackGraphNode:
112
116
 
113
117
  return copied_node
114
118
 
119
+
115
120
  def is_compromised(self) -> bool:
116
121
  """
117
122
  Return True if any attackers have compromised this node.
@@ -119,6 +124,7 @@ class AttackGraphNode:
119
124
  """
120
125
  return len(self.compromised_by) > 0
121
126
 
127
+
122
128
  def is_compromised_by(self, attacker: Attacker) -> bool:
123
129
  """
124
130
  Return True if the attacker given as an argument has compromised this
@@ -130,6 +136,7 @@ class AttackGraphNode:
130
136
  """
131
137
  return attacker in self.compromised_by
132
138
 
139
+
133
140
  def compromise(self, attacker: Attacker) -> None:
134
141
  """
135
142
  Have the attacker given as a parameter compromise this node.
@@ -139,6 +146,7 @@ class AttackGraphNode:
139
146
  """
140
147
  attacker.compromise(self)
141
148
 
149
+
142
150
  def undo_compromise(self, attacker: Attacker) -> None:
143
151
  """
144
152
  Remove the attacker given as a parameter from the list of attackers
@@ -150,6 +158,7 @@ class AttackGraphNode:
150
158
  """
151
159
  attacker.undo_compromise(self)
152
160
 
161
+
153
162
  def is_enabled_defense(self) -> bool:
154
163
  """
155
164
  Return True if this node is a defense node and it is enabled and not
@@ -160,6 +169,7 @@ class AttackGraphNode:
160
169
  'suppress' not in self.tags and \
161
170
  self.defense_status == 1.0
162
171
 
172
+
163
173
  def is_available_defense(self) -> bool:
164
174
  """
165
175
  Return True if this node is a defense node and it is not fully enabled
@@ -169,6 +179,7 @@ class AttackGraphNode:
169
179
  'suppress' not in self.tags and \
170
180
  self.defense_status != 1.0
171
181
 
182
+
172
183
  @property
173
184
  def full_name(self) -> str:
174
185
  """
@@ -181,3 +192,8 @@ class AttackGraphNode:
181
192
  else:
182
193
  full_name = str(self.id) + ':' + self.name
183
194
  return full_name
195
+
196
+
197
+ @cached_property
198
+ def info(self) -> dict[str, str]:
199
+ return self.lang_graph_attack_step.info
maltoolbox/file_utils.py CHANGED
@@ -24,15 +24,19 @@ def save_dict_to_yaml_file(filename: str, serialized_object: dict) -> None:
24
24
  data - dict to output as yaml
25
25
  """
26
26
 
27
+ class NoAliasSafeDumper(yaml.SafeDumper):
28
+ def ignore_aliases(self, data):
29
+ return True
30
+
27
31
  # Handle Literal values from jsonschema_objects
28
32
  yaml.add_multi_representer(
29
33
  LiteralValue,
30
34
  lambda dumper, data: dumper.represent_data(data._value),
31
- yaml.SafeDumper
35
+ NoAliasSafeDumper
32
36
  )
33
37
 
34
38
  with open(filename, 'w', encoding='utf-8') as f:
35
- yaml.dump(serialized_object, f, Dumper=yaml.SafeDumper)
39
+ yaml.dump(serialized_object, f, Dumper=NoAliasSafeDumper)
36
40
 
37
41
 
38
42
  def load_dict_from_yaml_file(filename: str) -> dict:
@@ -1,4 +1,8 @@
1
1
  """Contains tools to process MAL languages"""
2
2
 
3
- from .languagegraph import LanguageGraph
3
+ from .languagegraph import (LanguageGraph,
4
+ ExpressionsChain,
5
+ LanguageGraphAsset,
6
+ LanguageGraphAttackStep,
7
+ disaggregate_attack_step_full_name)
4
8
  from .classes_factory import LanguageClassesFactory
@@ -28,10 +28,10 @@ class LanguageClassesFactory:
28
28
  """
29
29
  Generate JSON Schema for asset types in the language specification.
30
30
  """
31
- for asset in self.lang_graph.assets:
31
+ for asset_name, asset in self.lang_graph.assets.items():
32
32
  logger.debug('Creating %s asset JSON schema entry.', asset.name)
33
33
  asset_json_entry = {
34
- 'title': asset.name,
34
+ 'title': 'Asset_' + asset.name,
35
35
  'type': 'object',
36
36
  'properties': {},
37
37
  }
@@ -41,29 +41,30 @@ class LanguageClassesFactory:
41
41
  asset_json_entry['properties']['type'] = \
42
42
  {
43
43
  'type' : 'string',
44
- 'default': asset.name
44
+ 'default': asset_name
45
45
  }
46
- if asset.super_assets:
46
+ if asset.own_super_asset:
47
47
  asset_json_entry['allOf'] = [
48
- {'$ref': '#/definitions/LanguageAsset/definitions/' + superasset.name}
49
- for superasset in asset.super_assets
48
+ {'$ref': '#/definitions/LanguageAsset/definitions/Asset_'\
49
+ + asset.own_super_asset.name}
50
50
  ]
51
- for defense in filter(lambda step: step.type == 'defense', asset.attack_steps):
52
- if defense.ttc and defense.ttc['name'] == 'Enabled':
53
- default_defense_value = 1.0
54
- else:
55
- default_defense_value = 0.0
56
- asset_json_entry['properties'][defense.name] = \
57
- {
58
- 'type' : 'number',
59
- 'minimum' : 0,
60
- 'maximum' : 1,
61
- 'default': default_defense_value
62
- }
51
+ for step_name, step in asset.attack_steps.items():
52
+ if step.type == 'defense':
53
+ if step.ttc and step.ttc['name'] == 'Enabled':
54
+ default_defense_value = 1.0
55
+ else:
56
+ default_defense_value = 0.0
57
+ asset_json_entry['properties'][step_name] = \
58
+ {
59
+ 'type' : 'number',
60
+ 'minimum' : 0,
61
+ 'maximum' : 1,
62
+ 'default': default_defense_value
63
+ }
63
64
  self.json_schema['definitions']['LanguageAsset']['definitions']\
64
- [asset.name] = asset_json_entry
65
+ ['Asset_' + asset_name] = asset_json_entry
65
66
  self.json_schema['definitions']['LanguageAsset']['oneOf'].append(
66
- {'$ref': '#/definitions/LanguageAsset/definitions/' + asset.name}
67
+ {'$ref': '#/definitions/LanguageAsset/definitions/Asset_' + asset_name}
67
68
  )
68
69
 
69
70
  def _generate_associations(self) -> None:
@@ -72,44 +73,21 @@ class LanguageClassesFactory:
72
73
  """
73
74
  def create_association_entry(assoc: SchemaGeneratedClass):
74
75
  logger.debug('Creating %s association JSON schema entry.', assoc.name)
75
- assoc_json_entry = {'title': assoc.name, 'type': 'object', 'properties': {}}
76
+ assoc_json_entry = {
77
+ 'title': 'Association_' + assoc.full_name,
78
+ 'type': 'object',
79
+ 'properties': {}
80
+ }
81
+ assoc_json_entry['properties']['type'] = \
82
+ {
83
+ 'type' : 'string',
84
+ 'default': assoc.name
85
+ }
76
86
 
77
87
  create_association_field(assoc, assoc_json_entry, 'left')
78
88
  create_association_field(assoc, assoc_json_entry, 'right')
79
89
  return assoc_json_entry
80
90
 
81
- def create_association_with_subentries(assoc: SchemaGeneratedClass):
82
- if (assoc.name not in self.json_schema['definitions']\
83
- ['LanguageAssociation']['definitions']):
84
- logger.info(
85
- 'Multiple associations with name %s exist. '
86
- 'Creating subentries for each one.', assoc.name
87
- )
88
- self.json_schema['definitions']['LanguageAssociation']\
89
- ['definitions'][assoc.name] =\
90
- {
91
- 'title': assoc.name,
92
- 'type': 'object',
93
- 'oneOf': [],
94
- 'definitions': {}
95
- }
96
- self.json_schema['definitions']['LanguageAssociation']['oneOf'].\
97
- append({'$ref': '#/definitions/LanguageAssociation/definitions/'
98
- + assoc.name})
99
-
100
- assoc_json_subentry = create_association_entry(assoc)
101
- subentry_name = assoc.name + '_' + assoc.left_field.asset.name + '_' \
102
- + assoc.right_field.asset.name
103
-
104
- logger.info('Creating %s subentry association.', subentry_name)
105
- assoc_json_subentry['title'] = subentry_name
106
- self.json_schema['definitions']['LanguageAssociation']\
107
- ['definitions'][assoc.name]['definitions'][subentry_name] = assoc_json_subentry
108
- self.json_schema['definitions']['LanguageAssociation']\
109
- ['definitions'][assoc.name]['oneOf'].append(
110
- {'$ref': '#/definitions/LanguageAssociation/definitions/' \
111
- + assoc.name + '/definitions/' + subentry_name})
112
-
113
91
  def create_association_field(
114
92
  assoc: SchemaGeneratedClass,
115
93
  assoc_json_entry: dict,
@@ -122,7 +100,7 @@ class LanguageClassesFactory:
122
100
  'items' :
123
101
  {
124
102
  '$ref':
125
- '#/definitions/LanguageAsset/definitions/' +
103
+ '#/definitions/LanguageAsset/definitions/Asset_' +
126
104
  field.asset.name
127
105
  }
128
106
  }
@@ -130,22 +108,17 @@ class LanguageClassesFactory:
130
108
  assoc_json_entry['properties'][field.fieldname]\
131
109
  ['maxItems'] = field.maximum
132
110
 
133
-
134
- for assoc in self.lang_graph.associations:
135
- count = len(list(filter(lambda temp_assoc: temp_assoc.name ==
136
- assoc.name, self.lang_graph.associations)))
137
- if count > 1:
138
- # If there are multiple associations with the same name we
139
- # will need to create separate entries for each using their
140
- # fieldnames.
141
- create_association_with_subentries(assoc)
142
- else:
143
- assoc_json_entry = create_association_entry(assoc)
144
- self.json_schema['definitions']['LanguageAssociation']\
145
- ['definitions'][assoc.name] = assoc_json_entry
146
- self.json_schema['definitions']['LanguageAssociation']['oneOf'].\
147
- append({'$ref': '#/definitions/LanguageAssociation/' +
148
- 'definitions/' + assoc.name})
111
+ for asset_name, asset in self.lang_graph.assets.items():
112
+ for assoc_name, assoc in asset.associations.items():
113
+ if assoc_name not in self.json_schema['definitions']\
114
+ ['LanguageAssociation']['definitions']:
115
+ assoc_json_entry = create_association_entry(assoc)
116
+ self.json_schema['definitions']['LanguageAssociation']\
117
+ ['definitions']['Association_' + assoc_name] = \
118
+ assoc_json_entry
119
+ self.json_schema['definitions']['LanguageAssociation']['oneOf'].\
120
+ append({'$ref': '#/definitions/LanguageAssociation/' +
121
+ 'definitions/Association_' + assoc_name})
149
122
 
150
123
  def _create_classes(self) -> None:
151
124
  """
@@ -241,3 +214,46 @@ class LanguageClassesFactory:
241
214
  return full_name
242
215
  else:
243
216
  return assoc_name
217
+
218
+ def get_asset_class(self,
219
+ asset_name: str
220
+ ) -> Optional[SchemaGeneratedClass]:
221
+ class_name = 'Asset_' + asset_name
222
+ if hasattr(self.ns, class_name):
223
+ class_obj = getattr(self.ns, class_name)
224
+ class_obj.__hash__ = lambda self: hash(self.name)
225
+ return class_obj
226
+ else:
227
+ logger.warning('Could not find Asset "%s" in classes factory.' %
228
+ asset_name)
229
+ return None
230
+
231
+ def get_association_class(self,
232
+ assoc_name: str
233
+ ) -> Optional[SchemaGeneratedClass]:
234
+ class_name = 'Association_' + assoc_name
235
+ if hasattr(self.ns, class_name):
236
+ return getattr(self.ns, class_name)
237
+ else:
238
+ logger.warning('Could not find Association "%s" in classes factory.' %
239
+ assoc_name)
240
+ return None
241
+
242
+ def get_association_class_by_fieldnames(self,
243
+ assoc_name: str,
244
+ fieldname1: str,
245
+ fieldname2: str
246
+ ) -> Optional[SchemaGeneratedClass]:
247
+ class_name = 'Association_%s_%s_%s' % (assoc_name,
248
+ fieldname1, fieldname2)
249
+ class_name_alt = 'Association_%s_%s_%s' % (assoc_name,
250
+ fieldname2, fieldname1)
251
+
252
+ if hasattr(self.ns, class_name):
253
+ return getattr(self.ns, class_name)
254
+ elif hasattr(self.ns, class_name_alt):
255
+ return getattr(self.ns, class_name_alt)
256
+ else:
257
+ logger.warning('Could not find Association "%s" or "%s" in '
258
+ 'classes factory.' % (class_name, class_name_alt))
259
+ return None