mal-toolbox 0.0.28__py3-none-any.whl → 0.1.12__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.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/METADATA +60 -28
- mal_toolbox-0.1.12.dist-info/RECORD +32 -0
- {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/WHEEL +1 -1
- maltoolbox/__init__.py +31 -31
- maltoolbox/__main__.py +80 -4
- maltoolbox/attackgraph/__init__.py +8 -0
- maltoolbox/attackgraph/analyzers/__init__.py +0 -0
- maltoolbox/attackgraph/analyzers/apriori.py +173 -27
- maltoolbox/attackgraph/attacker.py +84 -25
- maltoolbox/attackgraph/attackgraph.py +503 -215
- maltoolbox/attackgraph/node.py +92 -31
- maltoolbox/attackgraph/query.py +125 -19
- maltoolbox/default.conf +8 -7
- maltoolbox/exceptions.py +45 -0
- maltoolbox/file_utils.py +66 -0
- maltoolbox/ingestors/__init__.py +0 -0
- maltoolbox/ingestors/neo4j.py +95 -84
- maltoolbox/language/__init__.py +4 -0
- maltoolbox/language/classes_factory.py +145 -64
- maltoolbox/language/{lexer_parser/__main__.py → compiler/__init__.py} +5 -12
- maltoolbox/language/{lexer_parser → compiler}/mal_lexer.py +1 -1
- maltoolbox/language/{lexer_parser → compiler}/mal_parser.py +1 -1
- maltoolbox/language/{lexer_parser → compiler}/mal_visitor.py +4 -5
- maltoolbox/language/languagegraph.py +569 -168
- maltoolbox/model.py +858 -0
- maltoolbox/translators/__init__.py +0 -0
- maltoolbox/translators/securicad.py +76 -52
- maltoolbox/translators/updater.py +132 -0
- maltoolbox/wrappers.py +62 -0
- mal_toolbox-0.0.28.dist-info/RECORD +0 -26
- maltoolbox/cl_parser.py +0 -89
- maltoolbox/language/specification.py +0 -265
- maltoolbox/main.py +0 -84
- maltoolbox/model/model.py +0 -282
- {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/AUTHORS +0 -0
- {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/LICENSE +0 -0
- {mal_toolbox-0.0.28.dist-info → mal_toolbox-0.1.12.dist-info}/top_level.txt +0 -0
maltoolbox/attackgraph/node.py
CHANGED
|
@@ -2,50 +2,54 @@
|
|
|
2
2
|
MAL-Toolbox Attack Graph Node Dataclass
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import copy
|
|
5
7
|
from dataclasses import field, dataclass
|
|
6
|
-
from typing import Any,
|
|
8
|
+
from typing import Any, Optional
|
|
7
9
|
|
|
8
|
-
from
|
|
10
|
+
from . import Attacker
|
|
9
11
|
|
|
10
12
|
@dataclass
|
|
11
13
|
class AttackGraphNode:
|
|
12
|
-
|
|
14
|
+
"""Node part of AttackGraph"""
|
|
13
15
|
type: str
|
|
14
16
|
name: str
|
|
15
|
-
ttc: dict
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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)
|
|
19
22
|
defense_status: Optional[float] = None
|
|
20
23
|
existence_status: Optional[bool] = None
|
|
21
24
|
is_viable: bool = True
|
|
22
25
|
is_necessary: bool = True
|
|
23
|
-
compromised_by:
|
|
24
|
-
extra: Optional[dict] = None
|
|
26
|
+
compromised_by: list[Attacker] = field(default_factory=list)
|
|
25
27
|
mitre_info: Optional[str] = None
|
|
26
|
-
tags:
|
|
27
|
-
observations: Optional[dict] = None
|
|
28
|
+
tags: list[str] = field(default_factory=lambda: [])
|
|
28
29
|
attributes: Optional[dict] = None
|
|
29
|
-
attacker: Optional['Attacker'] = None
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
# Optional extra metadata for AttackGraphNode
|
|
32
|
+
extras: dict = field(default_factory=dict)
|
|
33
33
|
|
|
34
|
-
def to_dict(self):
|
|
35
|
-
|
|
34
|
+
def to_dict(self) -> dict:
|
|
35
|
+
"""Convert node to dictionary"""
|
|
36
|
+
node_dict: dict = {
|
|
36
37
|
'id': self.id,
|
|
37
38
|
'type': self.type,
|
|
38
39
|
'name': self.name,
|
|
39
40
|
'ttc': self.ttc,
|
|
40
|
-
'children':
|
|
41
|
-
'parents':
|
|
42
|
-
'compromised_by': [
|
|
41
|
+
'children': {},
|
|
42
|
+
'parents': {},
|
|
43
|
+
'compromised_by': [attacker.name for attacker in \
|
|
43
44
|
self.compromised_by]
|
|
44
45
|
}
|
|
45
46
|
|
|
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
|
|
46
51
|
if self.asset is not None:
|
|
47
|
-
node_dict['asset'] = self.asset.
|
|
48
|
-
+ str(self.asset.id)
|
|
52
|
+
node_dict['asset'] = str(self.asset.name)
|
|
49
53
|
if self.defense_status is not None:
|
|
50
54
|
node_dict['defense_status'] = str(self.defense_status)
|
|
51
55
|
if self.existence_status is not None:
|
|
@@ -58,19 +62,64 @@ class AttackGraphNode:
|
|
|
58
62
|
node_dict['mitre_info'] = str(self.mitre_info)
|
|
59
63
|
if self.tags:
|
|
60
64
|
node_dict['tags'] = str(self.tags)
|
|
61
|
-
if self.
|
|
62
|
-
node_dict['
|
|
65
|
+
if self.extras:
|
|
66
|
+
node_dict['extras'] = self.extras
|
|
63
67
|
|
|
64
68
|
return node_dict
|
|
65
69
|
|
|
66
|
-
def
|
|
70
|
+
def __repr__(self) -> str:
|
|
71
|
+
return str(self.to_dict())
|
|
72
|
+
|
|
73
|
+
def __deepcopy__(self, memo) -> AttackGraphNode:
|
|
74
|
+
"""Deep copy an attackgraph node
|
|
75
|
+
|
|
76
|
+
The deepcopy will copy over node specific information, such as type,
|
|
77
|
+
name, etc., but it will not copy attack graph relations such as
|
|
78
|
+
parents, children, or attackers it is compromised by. These references
|
|
79
|
+
should be recreated when deepcopying the attack graph itself.
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
# Check if the object is already in the memo dictionary
|
|
84
|
+
if id(self) in memo:
|
|
85
|
+
return memo[id(self)]
|
|
86
|
+
|
|
87
|
+
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
|
+
{}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
copied_node.tags = copy.deepcopy(self.tags, memo)
|
|
107
|
+
copied_node.attributes = copy.deepcopy(self.attributes, memo)
|
|
108
|
+
copied_node.extras = copy.deepcopy(self.extras, memo)
|
|
109
|
+
|
|
110
|
+
# Remember that self was already copied
|
|
111
|
+
memo[id(self)] = copied_node
|
|
112
|
+
|
|
113
|
+
return copied_node
|
|
114
|
+
|
|
115
|
+
def is_compromised(self) -> bool:
|
|
67
116
|
"""
|
|
68
117
|
Return True if any attackers have compromised this node.
|
|
69
118
|
False, otherwise.
|
|
70
119
|
"""
|
|
71
120
|
return len(self.compromised_by) > 0
|
|
72
121
|
|
|
73
|
-
def is_compromised_by(self, attacker):
|
|
122
|
+
def is_compromised_by(self, attacker: Attacker) -> bool:
|
|
74
123
|
"""
|
|
75
124
|
Return True if the attacker given as an argument has compromised this
|
|
76
125
|
node.
|
|
@@ -81,7 +130,7 @@ class AttackGraphNode:
|
|
|
81
130
|
"""
|
|
82
131
|
return attacker in self.compromised_by
|
|
83
132
|
|
|
84
|
-
def compromise(self, attacker):
|
|
133
|
+
def compromise(self, attacker: Attacker) -> None:
|
|
85
134
|
"""
|
|
86
135
|
Have the attacker given as a parameter compromise this node.
|
|
87
136
|
|
|
@@ -90,7 +139,7 @@ class AttackGraphNode:
|
|
|
90
139
|
"""
|
|
91
140
|
attacker.compromise(self)
|
|
92
141
|
|
|
93
|
-
def undo_compromise(self, attacker):
|
|
142
|
+
def undo_compromise(self, attacker: Attacker) -> None:
|
|
94
143
|
"""
|
|
95
144
|
Remove the attacker given as a parameter from the list of attackers
|
|
96
145
|
that have compromised this node.
|
|
@@ -101,7 +150,7 @@ class AttackGraphNode:
|
|
|
101
150
|
"""
|
|
102
151
|
attacker.undo_compromise(self)
|
|
103
152
|
|
|
104
|
-
def is_enabled_defense(self):
|
|
153
|
+
def is_enabled_defense(self) -> bool:
|
|
105
154
|
"""
|
|
106
155
|
Return True if this node is a defense node and it is enabled and not
|
|
107
156
|
suppressed via tags.
|
|
@@ -111,12 +160,24 @@ class AttackGraphNode:
|
|
|
111
160
|
'suppress' not in self.tags and \
|
|
112
161
|
self.defense_status == 1.0
|
|
113
162
|
|
|
114
|
-
def is_available_defense(self):
|
|
163
|
+
def is_available_defense(self) -> bool:
|
|
115
164
|
"""
|
|
116
165
|
Return True if this node is a defense node and it is not fully enabled
|
|
117
|
-
and not suppressed via tags.
|
|
118
|
-
False, otherwise.
|
|
166
|
+
and not suppressed via tags. False otherwise.
|
|
119
167
|
"""
|
|
120
168
|
return self.type == 'defense' and \
|
|
121
169
|
'suppress' not in self.tags and \
|
|
122
170
|
self.defense_status != 1.0
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def full_name(self) -> str:
|
|
174
|
+
"""
|
|
175
|
+
Return the full name of the attack step. This is a combination of the
|
|
176
|
+
asset name to which the attack step belongs and attack step name
|
|
177
|
+
itself.
|
|
178
|
+
"""
|
|
179
|
+
if self.asset:
|
|
180
|
+
full_name = self.asset.name + ':' + self.name
|
|
181
|
+
else:
|
|
182
|
+
full_name = str(self.id) + ':' + self.name
|
|
183
|
+
return full_name
|
maltoolbox/attackgraph/query.py
CHANGED
|
@@ -4,15 +4,20 @@ MAL-Toolbox Attack Graph Query Submodule
|
|
|
4
4
|
This submodule contains functions that analyze the information present in the
|
|
5
5
|
attack graph, but do not alter the structure or nodes in any way.
|
|
6
6
|
"""
|
|
7
|
-
|
|
7
|
+
from __future__ import annotations
|
|
8
8
|
import logging
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from .attackgraph import AttackGraph, Attacker
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
from
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .attackgraph import AttackGraphNode
|
|
12
15
|
|
|
13
16
|
logger = logging.getLogger(__name__)
|
|
14
17
|
|
|
15
|
-
def is_node_traversable_by_attacker(
|
|
18
|
+
def is_node_traversable_by_attacker(
|
|
19
|
+
node: AttackGraphNode, attacker: Attacker
|
|
20
|
+
) -> bool:
|
|
16
21
|
"""
|
|
17
22
|
Return True or False depending if the node specified is traversable
|
|
18
23
|
for the attacker given.
|
|
@@ -22,13 +27,31 @@ def is_node_traversable_by_attacker(node, attacker) -> bool:
|
|
|
22
27
|
attacker - the attacker whose traversability we are interested in
|
|
23
28
|
"""
|
|
24
29
|
|
|
25
|
-
logger.debug(
|
|
26
|
-
|
|
30
|
+
logger.debug(
|
|
31
|
+
'Evaluate if "%s"(%d), of type "%s", is traversable by Attacker '
|
|
32
|
+
'"%s"(%d)',
|
|
33
|
+
node.full_name,
|
|
34
|
+
node.id,
|
|
35
|
+
node.type,
|
|
36
|
+
attacker.name,
|
|
37
|
+
attacker.id
|
|
38
|
+
)
|
|
27
39
|
if not node.is_viable:
|
|
40
|
+
logger.debug(
|
|
41
|
+
'"%s"(%d) is not traversable because it is non-viable',
|
|
42
|
+
node.full_name,
|
|
43
|
+
node.id,
|
|
44
|
+
)
|
|
28
45
|
return False
|
|
29
46
|
|
|
30
47
|
match(node.type):
|
|
31
48
|
case 'or':
|
|
49
|
+
logger.debug(
|
|
50
|
+
'"%s"(%d) is traversable because it is viable and '
|
|
51
|
+
'of type "or".',
|
|
52
|
+
node.full_name,
|
|
53
|
+
node.id
|
|
54
|
+
)
|
|
32
55
|
return True
|
|
33
56
|
|
|
34
57
|
case 'and':
|
|
@@ -37,18 +60,49 @@ def is_node_traversable_by_attacker(node, attacker) -> bool:
|
|
|
37
60
|
not parent.is_compromised_by(attacker):
|
|
38
61
|
# If the parent is not present in the attacks steps
|
|
39
62
|
# already reached and is necessary.
|
|
63
|
+
logger.debug(
|
|
64
|
+
'"%s"(%d) is not traversable because while it is '
|
|
65
|
+
'viable, and of type "and", its necessary parent '
|
|
66
|
+
'"%s(%d)" has not already been compromised.',
|
|
67
|
+
node.full_name,
|
|
68
|
+
node.id,
|
|
69
|
+
parent.full_name,
|
|
70
|
+
parent.id
|
|
71
|
+
)
|
|
40
72
|
return False
|
|
73
|
+
logger.debug(
|
|
74
|
+
'"%s"(%d) is traversable because it is viable, '
|
|
75
|
+
'of type "and", and all of its necessary parents have '
|
|
76
|
+
'already been compromised.',
|
|
77
|
+
node.full_name,
|
|
78
|
+
node.id
|
|
79
|
+
)
|
|
41
80
|
return True
|
|
42
81
|
|
|
43
|
-
case 'exist' | 'notExist' |
|
|
82
|
+
case 'exist' | 'notExist' | 'defense':
|
|
83
|
+
logger.warning(
|
|
84
|
+
'Nodes of type "exist", "notExist", and "defense" are never '
|
|
85
|
+
'marked as traversable. However, we do not normally check '
|
|
86
|
+
'if they are traversable. Node "%s"(%d) of type "%s" was '
|
|
87
|
+
'checked for traversability.',
|
|
88
|
+
node.full_name,
|
|
89
|
+
node.id,
|
|
90
|
+
node.type
|
|
91
|
+
)
|
|
44
92
|
return False
|
|
45
93
|
|
|
46
94
|
case _:
|
|
47
|
-
logger.error(
|
|
95
|
+
logger.error(
|
|
96
|
+
'Node "%s"(%d) has an unknown type "%s".',
|
|
97
|
+
node.full_name,
|
|
98
|
+
node.id,
|
|
99
|
+
node.type
|
|
100
|
+
)
|
|
48
101
|
return False
|
|
49
102
|
|
|
50
|
-
def get_attack_surface(
|
|
51
|
-
|
|
103
|
+
def get_attack_surface(
|
|
104
|
+
attacker: Attacker
|
|
105
|
+
) -> list[AttackGraphNode]:
|
|
52
106
|
"""
|
|
53
107
|
Get the current attack surface of an attacker. This includes all of the
|
|
54
108
|
viable children nodes of already reached attack steps that are of 'or'
|
|
@@ -56,21 +110,74 @@ def get_attack_surface(graph: attackgraph.AttackGraph,
|
|
|
56
110
|
parents in the attack steps reached.
|
|
57
111
|
|
|
58
112
|
Arguments:
|
|
59
|
-
graph - the attack graph
|
|
60
113
|
attacker - the Attacker whose attack surface is sought
|
|
61
114
|
"""
|
|
62
|
-
logger.debug(
|
|
115
|
+
logger.debug(
|
|
116
|
+
'Get the attack surface for Attacker "%s"(%d).',
|
|
117
|
+
attacker.name,
|
|
118
|
+
attacker.id
|
|
119
|
+
)
|
|
63
120
|
attack_surface = []
|
|
64
121
|
for attack_step in attacker.reached_attack_steps:
|
|
65
|
-
logger.debug(
|
|
66
|
-
|
|
122
|
+
logger.debug(
|
|
123
|
+
'Determine attack surface stemming from '
|
|
124
|
+
'"%s"(%d) for Attacker "%s"(%d).',
|
|
125
|
+
attack_step.full_name,
|
|
126
|
+
attack_step.id,
|
|
127
|
+
attacker.name,
|
|
128
|
+
attacker.id
|
|
129
|
+
)
|
|
67
130
|
for child in attack_step.children:
|
|
68
131
|
if is_node_traversable_by_attacker(child, attacker) and \
|
|
69
132
|
child not in attack_surface:
|
|
70
133
|
attack_surface.append(child)
|
|
71
134
|
return attack_surface
|
|
72
135
|
|
|
73
|
-
def
|
|
136
|
+
def update_attack_surface_add_nodes(
|
|
137
|
+
attacker: Attacker,
|
|
138
|
+
current_attack_surface: list[AttackGraphNode],
|
|
139
|
+
nodes: list[AttackGraphNode]
|
|
140
|
+
) -> list[AttackGraphNode]:
|
|
141
|
+
"""
|
|
142
|
+
Update the attack surface of an attacker with the new attack step nodes
|
|
143
|
+
provided to see if any of their children can be added.
|
|
144
|
+
|
|
145
|
+
Arguments:
|
|
146
|
+
attacker - the Attacker whose attack surface is sought
|
|
147
|
+
current_attack_surface - the current attack surface that we wish to
|
|
148
|
+
expand
|
|
149
|
+
nodes - the newly compromised attack step nodes that we
|
|
150
|
+
wish to see if any of their children should be
|
|
151
|
+
added to the attack surface
|
|
152
|
+
"""
|
|
153
|
+
logger.debug('Update the attack surface for Attacker "%s"(%d).',
|
|
154
|
+
attacker.name,
|
|
155
|
+
attacker.id)
|
|
156
|
+
attack_surface = current_attack_surface
|
|
157
|
+
for attack_step in nodes:
|
|
158
|
+
logger.debug(
|
|
159
|
+
'Determine attack surface stemming from "%s"(%d) '
|
|
160
|
+
'for Attacker "%s"(%d).',
|
|
161
|
+
attack_step.full_name,
|
|
162
|
+
attack_step.id,
|
|
163
|
+
attacker.name,
|
|
164
|
+
attacker.id
|
|
165
|
+
)
|
|
166
|
+
for child in attack_step.children:
|
|
167
|
+
is_traversable = is_node_traversable_by_attacker(child, attacker)
|
|
168
|
+
if is_traversable and child not in attack_surface:
|
|
169
|
+
logger.debug(
|
|
170
|
+
'Add node "%s"(%d) to the attack surface of '
|
|
171
|
+
'Attacker "%s"(%d).',
|
|
172
|
+
child.full_name,
|
|
173
|
+
child.id,
|
|
174
|
+
attacker.name,
|
|
175
|
+
attacker.id
|
|
176
|
+
)
|
|
177
|
+
attack_surface.append(child)
|
|
178
|
+
return attack_surface
|
|
179
|
+
|
|
180
|
+
def get_defense_surface(graph: AttackGraph) -> list[AttackGraphNode]:
|
|
74
181
|
"""
|
|
75
182
|
Get the defense surface. All non-suppressed defense steps that are not
|
|
76
183
|
already fully enabled.
|
|
@@ -78,10 +185,10 @@ def get_defense_surface(graph: attackgraph.AttackGraph):
|
|
|
78
185
|
Arguments:
|
|
79
186
|
graph - the attack graph
|
|
80
187
|
"""
|
|
81
|
-
logger.debug(
|
|
188
|
+
logger.debug('Get the defense surface.')
|
|
82
189
|
return [node for node in graph.nodes if node.is_available_defense()]
|
|
83
190
|
|
|
84
|
-
def get_enabled_defenses(graph:
|
|
191
|
+
def get_enabled_defenses(graph: AttackGraph) -> list[AttackGraphNode]:
|
|
85
192
|
"""
|
|
86
193
|
Get the defenses already enabled. All non-suppressed defense steps that
|
|
87
194
|
are already fully enabled.
|
|
@@ -89,6 +196,5 @@ def get_enabled_defenses(graph: attackgraph.AttackGraph):
|
|
|
89
196
|
Arguments:
|
|
90
197
|
graph - the attack graph
|
|
91
198
|
"""
|
|
92
|
-
logger.debug(
|
|
199
|
+
logger.debug('Get the enabled defenses.')
|
|
93
200
|
return [node for node in graph.nodes if node.is_enabled_defense()]
|
|
94
|
-
|
maltoolbox/default.conf
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
[logging]
|
|
2
2
|
output_dir = tmp
|
|
3
|
+
log_level =
|
|
3
4
|
log_file = %(output_dir)s/log.txt
|
|
4
|
-
attackgraph_file = %(output_dir)s/attackgraph.
|
|
5
|
-
model_file = %(output_dir)s/model.
|
|
6
|
-
langspec_file = %(output_dir)s/langspec_file.
|
|
5
|
+
attackgraph_file = %(output_dir)s/attackgraph.yml
|
|
6
|
+
model_file = %(output_dir)s/model.yml
|
|
7
|
+
langspec_file = %(output_dir)s/langspec_file.yml
|
|
7
8
|
|
|
8
9
|
[input]
|
|
9
10
|
model_file =
|
|
10
11
|
lang_spec_file =
|
|
11
12
|
|
|
12
13
|
[neo4j]
|
|
13
|
-
uri=
|
|
14
|
-
username=
|
|
15
|
-
password=
|
|
16
|
-
dbname=
|
|
14
|
+
uri =
|
|
15
|
+
username =
|
|
16
|
+
password =
|
|
17
|
+
dbname =
|
maltoolbox/exceptions.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class MalToolboxException(Exception):
|
|
2
|
+
"""Base exception for all other maltoolbox exceptions to inherit from."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class LanguageGraphException(MalToolboxException):
|
|
6
|
+
"""Base exception for all language-graph related exceptions."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class LanguageGraphSuperAssetNotFoundError(LanguageGraphException):
|
|
10
|
+
"""Asset's super asset not found in language graph during attack graph construction."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class LanguageGraphAssociationError(LanguageGraphException):
|
|
14
|
+
"""Error in building an association.
|
|
15
|
+
|
|
16
|
+
For example, right or left-hand side asset of association missing in
|
|
17
|
+
language graph.
|
|
18
|
+
"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
class LanguageGraphStepExpressionError(LanguageGraphException):
|
|
22
|
+
"""A target asset cannot be linked with for a step expression."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
class AttackGraphException(MalToolboxException):
|
|
26
|
+
"""Base exception for all attack-graph related exceptions."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
class AttackGraphStepExpressionError(AttackGraphException):
|
|
30
|
+
"""A target attack step cannot be linked with for a step expression."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ModelException(MalToolboxException):
|
|
35
|
+
"""Base Exception for all Model related exceptions"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ModelAssociationException(ModelException):
|
|
40
|
+
"""Exception related to associations in Model"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
class DuplicateModelAssociationError(ModelException):
|
|
44
|
+
"""Associations should be unique as part of Model"""
|
|
45
|
+
pass
|
maltoolbox/file_utils.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Utily functions for file handling"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import yaml
|
|
5
|
+
from python_jsonschema_objects.literals import LiteralValue
|
|
6
|
+
|
|
7
|
+
def save_dict_to_json_file(filename: str, serialized_object: dict) -> None:
|
|
8
|
+
"""Save serialized object to a json file.
|
|
9
|
+
|
|
10
|
+
Arguments:
|
|
11
|
+
filename - the name of the output file
|
|
12
|
+
data - dict to output as json
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
with open(filename, 'w', encoding='utf-8') as f:
|
|
16
|
+
json.dump(serialized_object, f, indent=4)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def save_dict_to_yaml_file(filename: str, serialized_object: dict) -> None:
|
|
20
|
+
"""Save serialized object to a yaml file.
|
|
21
|
+
|
|
22
|
+
Arguments:
|
|
23
|
+
filename - the name of the output file
|
|
24
|
+
data - dict to output as yaml
|
|
25
|
+
"""
|
|
26
|
+
|
|
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
|
+
)
|
|
33
|
+
|
|
34
|
+
with open(filename, 'w', encoding='utf-8') as f:
|
|
35
|
+
yaml.dump(serialized_object, f, Dumper=yaml.SafeDumper)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_dict_from_yaml_file(filename: str) -> dict:
|
|
39
|
+
"""Open json file and read as dict"""
|
|
40
|
+
with open(filename, 'r', encoding='utf-8') as file:
|
|
41
|
+
object_dict = yaml.safe_load(file)
|
|
42
|
+
return object_dict
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_dict_from_json_file(filename: str) -> dict:
|
|
46
|
+
"""Open yaml file and read as dict"""
|
|
47
|
+
with open(filename, 'r', encoding='utf-8') as file:
|
|
48
|
+
object_dict = json.loads(file.read())
|
|
49
|
+
return object_dict
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def save_dict_to_file(filename: str, dictionary: dict) -> None:
|
|
53
|
+
"""Save serialized object to json or yaml file
|
|
54
|
+
depending on file extension.
|
|
55
|
+
|
|
56
|
+
Arguments:
|
|
57
|
+
filename - the name of the output file
|
|
58
|
+
dictionary - the dict to save to the file
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
if filename.endswith(('.yml', '.yaml')):
|
|
62
|
+
save_dict_to_yaml_file(filename, dictionary)
|
|
63
|
+
elif filename.endswith('.json'):
|
|
64
|
+
save_dict_to_json_file(filename, dictionary)
|
|
65
|
+
else:
|
|
66
|
+
raise ValueError('Unknown file extension, expected json/yml/yaml')
|
|
File without changes
|