mal-toolbox 0.0.27__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.27.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.27.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 +99 -21
- maltoolbox/attackgraph/attackgraph.py +507 -217
- maltoolbox/attackgraph/node.py +143 -21
- maltoolbox/attackgraph/query.py +128 -26
- 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.27.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 -279
- {mal_toolbox-0.0.27.dist-info → mal_toolbox-0.1.12.dist-info}/AUTHORS +0 -0
- {mal_toolbox-0.0.27.dist-info → mal_toolbox-0.1.12.dist-info}/LICENSE +0 -0
- {mal_toolbox-0.0.27.dist-info → mal_toolbox-0.1.12.dist-info}/top_level.txt +0 -0
maltoolbox/attackgraph/node.py
CHANGED
|
@@ -2,47 +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
|
+
|
|
34
|
+
def to_dict(self) -> dict:
|
|
35
|
+
"""Convert node to dictionary"""
|
|
36
|
+
node_dict: dict = {
|
|
33
37
|
'id': self.id,
|
|
34
38
|
'type': self.type,
|
|
35
39
|
'name': self.name,
|
|
36
40
|
'ttc': self.ttc,
|
|
37
|
-
'children':
|
|
38
|
-
'parents':
|
|
39
|
-
'compromised_by': [
|
|
41
|
+
'children': {},
|
|
42
|
+
'parents': {},
|
|
43
|
+
'compromised_by': [attacker.name for attacker in \
|
|
40
44
|
self.compromised_by]
|
|
41
45
|
}
|
|
42
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
|
|
43
51
|
if self.asset is not None:
|
|
44
|
-
node_dict['asset'] = self.asset.
|
|
45
|
-
+ str(self.asset.id)
|
|
52
|
+
node_dict['asset'] = str(self.asset.name)
|
|
46
53
|
if self.defense_status is not None:
|
|
47
54
|
node_dict['defense_status'] = str(self.defense_status)
|
|
48
55
|
if self.existence_status is not None:
|
|
@@ -55,7 +62,122 @@ class AttackGraphNode:
|
|
|
55
62
|
node_dict['mitre_info'] = str(self.mitre_info)
|
|
56
63
|
if self.tags:
|
|
57
64
|
node_dict['tags'] = str(self.tags)
|
|
58
|
-
if self.
|
|
59
|
-
node_dict['
|
|
65
|
+
if self.extras:
|
|
66
|
+
node_dict['extras'] = self.extras
|
|
60
67
|
|
|
61
68
|
return node_dict
|
|
69
|
+
|
|
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:
|
|
116
|
+
"""
|
|
117
|
+
Return True if any attackers have compromised this node.
|
|
118
|
+
False, otherwise.
|
|
119
|
+
"""
|
|
120
|
+
return len(self.compromised_by) > 0
|
|
121
|
+
|
|
122
|
+
def is_compromised_by(self, attacker: Attacker) -> bool:
|
|
123
|
+
"""
|
|
124
|
+
Return True if the attacker given as an argument has compromised this
|
|
125
|
+
node.
|
|
126
|
+
False, otherwise.
|
|
127
|
+
|
|
128
|
+
Arguments:
|
|
129
|
+
attacker - the attacker we are interested in
|
|
130
|
+
"""
|
|
131
|
+
return attacker in self.compromised_by
|
|
132
|
+
|
|
133
|
+
def compromise(self, attacker: Attacker) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Have the attacker given as a parameter compromise this node.
|
|
136
|
+
|
|
137
|
+
Arguments:
|
|
138
|
+
attacker - the attacker that will compromise the node
|
|
139
|
+
"""
|
|
140
|
+
attacker.compromise(self)
|
|
141
|
+
|
|
142
|
+
def undo_compromise(self, attacker: Attacker) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Remove the attacker given as a parameter from the list of attackers
|
|
145
|
+
that have compromised this node.
|
|
146
|
+
|
|
147
|
+
Arguments:
|
|
148
|
+
attacker - the attacker that we wish to remove from the compromised
|
|
149
|
+
list.
|
|
150
|
+
"""
|
|
151
|
+
attacker.undo_compromise(self)
|
|
152
|
+
|
|
153
|
+
def is_enabled_defense(self) -> bool:
|
|
154
|
+
"""
|
|
155
|
+
Return True if this node is a defense node and it is enabled and not
|
|
156
|
+
suppressed via tags.
|
|
157
|
+
False, otherwise.
|
|
158
|
+
"""
|
|
159
|
+
return self.type == 'defense' and \
|
|
160
|
+
'suppress' not in self.tags and \
|
|
161
|
+
self.defense_status == 1.0
|
|
162
|
+
|
|
163
|
+
def is_available_defense(self) -> bool:
|
|
164
|
+
"""
|
|
165
|
+
Return True if this node is a defense node and it is not fully enabled
|
|
166
|
+
and not suppressed via tags. False otherwise.
|
|
167
|
+
"""
|
|
168
|
+
return self.type == 'defense' and \
|
|
169
|
+
'suppress' not in self.tags and \
|
|
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,33 +27,82 @@ 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':
|
|
35
58
|
for parent in node.parents:
|
|
36
59
|
if parent.is_necessary and \
|
|
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,12 +185,10 @@ def get_defense_surface(graph: attackgraph.AttackGraph):
|
|
|
78
185
|
Arguments:
|
|
79
186
|
graph - the attack graph
|
|
80
187
|
"""
|
|
81
|
-
logger.debug(
|
|
82
|
-
return
|
|
83
|
-
'suppress' not in node.tags and \
|
|
84
|
-
node.defense_status != 1.0)
|
|
188
|
+
logger.debug('Get the defense surface.')
|
|
189
|
+
return [node for node in graph.nodes if node.is_available_defense()]
|
|
85
190
|
|
|
86
|
-
def get_enabled_defenses(graph:
|
|
191
|
+
def get_enabled_defenses(graph: AttackGraph) -> list[AttackGraphNode]:
|
|
87
192
|
"""
|
|
88
193
|
Get the defenses already enabled. All non-suppressed defense steps that
|
|
89
194
|
are already fully enabled.
|
|
@@ -91,8 +196,5 @@ def get_enabled_defenses(graph: attackgraph.AttackGraph):
|
|
|
91
196
|
Arguments:
|
|
92
197
|
graph - the attack graph
|
|
93
198
|
"""
|
|
94
|
-
logger.debug(
|
|
95
|
-
return
|
|
96
|
-
'suppress' not in node.tags and \
|
|
97
|
-
node.defense_status == 1.0)
|
|
98
|
-
|
|
199
|
+
logger.debug('Get the enabled defenses.')
|
|
200
|
+
return [node for node in graph.nodes if node.is_enabled_defense()]
|
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
|