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.
- {mal_toolbox-0.1.12.dist-info → mal_toolbox-0.2.0.dist-info}/METADATA +1 -1
- {mal_toolbox-0.1.12.dist-info → mal_toolbox-0.2.0.dist-info}/RECORD +15 -15
- maltoolbox/__init__.py +2 -2
- maltoolbox/attackgraph/analyzers/apriori.py +4 -1
- maltoolbox/attackgraph/attackgraph.py +294 -245
- maltoolbox/attackgraph/node.py +23 -7
- maltoolbox/file_utils.py +6 -2
- maltoolbox/language/__init__.py +5 -1
- maltoolbox/language/classes_factory.py +86 -70
- maltoolbox/language/languagegraph.py +1022 -475
- maltoolbox/model.py +44 -35
- {mal_toolbox-0.1.12.dist-info → mal_toolbox-0.2.0.dist-info}/AUTHORS +0 -0
- {mal_toolbox-0.1.12.dist-info → mal_toolbox-0.2.0.dist-info}/LICENSE +0 -0
- {mal_toolbox-0.1.12.dist-info → mal_toolbox-0.2.0.dist-info}/WHEEL +0 -0
- {mal_toolbox-0.1.12.dist-info → mal_toolbox-0.2.0.dist-info}/top_level.txt +0 -0
maltoolbox/attackgraph/node.py
CHANGED
|
@@ -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
|
-
|
|
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'] =
|
|
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
|
-
|
|
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
|
-
|
|
35
|
+
NoAliasSafeDumper
|
|
32
36
|
)
|
|
33
37
|
|
|
34
38
|
with open(filename, 'w', encoding='utf-8') as f:
|
|
35
|
-
yaml.dump(serialized_object, f, Dumper=
|
|
39
|
+
yaml.dump(serialized_object, f, Dumper=NoAliasSafeDumper)
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
def load_dict_from_yaml_file(filename: str) -> dict:
|
maltoolbox/language/__init__.py
CHANGED
|
@@ -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':
|
|
44
|
+
'default': asset_name
|
|
45
45
|
}
|
|
46
|
-
if asset.
|
|
46
|
+
if asset.own_super_asset:
|
|
47
47
|
asset_json_entry['allOf'] = [
|
|
48
|
-
{'$ref': '#/definitions/LanguageAsset/definitions/'
|
|
49
|
-
|
|
48
|
+
{'$ref': '#/definitions/LanguageAsset/definitions/Asset_'\
|
|
49
|
+
+ asset.own_super_asset.name}
|
|
50
50
|
]
|
|
51
|
-
for
|
|
52
|
-
if
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
[
|
|
65
|
+
['Asset_' + asset_name] = asset_json_entry
|
|
65
66
|
self.json_schema['definitions']['LanguageAsset']['oneOf'].append(
|
|
66
|
-
{'$ref': '#/definitions/LanguageAsset/definitions/' +
|
|
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 = {
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|