mal-toolbox 1.2.1__py3-none-any.whl → 2.1.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.
Files changed (37) hide show
  1. {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/METADATA +8 -75
  2. mal_toolbox-2.1.0.dist-info/RECORD +51 -0
  3. {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/WHEEL +1 -1
  4. maltoolbox/__init__.py +2 -2
  5. maltoolbox/attackgraph/__init__.py +2 -2
  6. maltoolbox/attackgraph/attackgraph.py +121 -549
  7. maltoolbox/attackgraph/factories.py +68 -0
  8. maltoolbox/attackgraph/file_utils.py +0 -0
  9. maltoolbox/attackgraph/generate.py +338 -0
  10. maltoolbox/attackgraph/node.py +1 -0
  11. maltoolbox/attackgraph/node_getters.py +36 -0
  12. maltoolbox/attackgraph/ttcs.py +28 -0
  13. maltoolbox/language/__init__.py +2 -2
  14. maltoolbox/language/compiler/__init__.py +4 -499
  15. maltoolbox/language/compiler/distributions.py +158 -0
  16. maltoolbox/language/compiler/exceptions.py +37 -0
  17. maltoolbox/language/compiler/lang.py +5 -0
  18. maltoolbox/language/compiler/mal_analyzer.py +920 -0
  19. maltoolbox/language/compiler/mal_compiler.py +1071 -0
  20. maltoolbox/language/detector.py +43 -0
  21. maltoolbox/language/expression_chain.py +218 -0
  22. maltoolbox/language/language_graph_asset.py +180 -0
  23. maltoolbox/language/language_graph_assoc.py +147 -0
  24. maltoolbox/language/language_graph_attack_step.py +129 -0
  25. maltoolbox/language/language_graph_builder.py +282 -0
  26. maltoolbox/language/language_graph_loaders.py +7 -0
  27. maltoolbox/language/language_graph_lookup.py +140 -0
  28. maltoolbox/language/language_graph_serialization.py +5 -0
  29. maltoolbox/language/languagegraph.py +244 -1536
  30. maltoolbox/language/step_expression_processor.py +491 -0
  31. mal_toolbox-1.2.1.dist-info/RECORD +0 -33
  32. maltoolbox/language/compiler/mal_lexer.py +0 -232
  33. maltoolbox/language/compiler/mal_parser.py +0 -3159
  34. {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/entry_points.txt +0 -0
  35. {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/AUTHORS +0 -0
  36. {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/LICENSE +0 -0
  37. {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,43 @@
1
+ """Detector functionality
2
+ - A detector represent a logging rule on an attack step
3
+ - It includes a context and a name
4
+ """
5
+
6
+
7
+ from __future__ import annotations
8
+ from dataclasses import dataclass
9
+
10
+
11
+ @dataclass(frozen=True, eq=True)
12
+ class Detector:
13
+ name: str | None
14
+ context: Context
15
+ type: str | None
16
+ tprate: dict | None
17
+
18
+ def to_dict(self) -> dict:
19
+ return {
20
+ "context": self.context.to_dict(),
21
+ "name": self.name,
22
+ "type": self.type,
23
+ "tprate": self.tprate,
24
+ }
25
+
26
+
27
+ class Context(dict):
28
+ """Context is part of detectors to provide meta data about attackers"""
29
+
30
+ def __init__(self, context) -> None:
31
+ super().__init__(context)
32
+ self._context_dict = context
33
+ for label, asset in context.items():
34
+ setattr(self, label, asset)
35
+
36
+ def to_dict(self) -> dict:
37
+ return {label: asset.name for label, asset in self.items()}
38
+
39
+ def __str__(self) -> str:
40
+ return str({label: asset.name for label, asset in self._context_dict.items()})
41
+
42
+ def __repr__(self) -> str:
43
+ return f"Context({self!s}))"
@@ -0,0 +1,218 @@
1
+ """Expression chain functionality
2
+ - Used to specify association paths and operations to reach children/parents of steps
3
+ """
4
+ from __future__ import annotations
5
+ from typing import TYPE_CHECKING
6
+ import json
7
+ import logging
8
+ from typing import Any
9
+
10
+ from maltoolbox.exceptions import LanguageGraphAssociationError, LanguageGraphException
11
+ from maltoolbox.language.language_graph_assoc import LanguageGraphAssociation
12
+
13
+ if TYPE_CHECKING:
14
+ from maltoolbox.language.languagegraph import LanguageGraph
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ class ExpressionsChain:
19
+ """A series of linked step expressions that specify the association path and
20
+ operations to take to reach the child/parent attack step.
21
+ """
22
+
23
+ def __init__(self,
24
+ type: str,
25
+ left_link: ExpressionsChain | None = None,
26
+ right_link: ExpressionsChain | None = None,
27
+ sub_link: ExpressionsChain | None = None,
28
+ fieldname: str | None = None,
29
+ association=None,
30
+ subtype=None
31
+ ):
32
+ self.type = type
33
+ self.left_link: ExpressionsChain | None = left_link
34
+ self.right_link: ExpressionsChain | None = right_link
35
+ self.sub_link: ExpressionsChain | None = sub_link
36
+ self.fieldname: str | None = fieldname
37
+ self.association: LanguageGraphAssociation | None = association
38
+ self.subtype: Any | None = subtype
39
+
40
+ def to_dict(self) -> dict:
41
+ """Convert ExpressionsChain to dictionary"""
42
+ match (self.type):
43
+ case 'union' | 'intersection' | 'difference' | 'collect':
44
+ return {
45
+ self.type: {
46
+ 'left': self.left_link.to_dict()
47
+ if self.left_link else {},
48
+ 'right': self.right_link.to_dict()
49
+ if self.right_link else {}
50
+ },
51
+ 'type': self.type
52
+ }
53
+
54
+ case 'field':
55
+ if not self.association:
56
+ raise LanguageGraphAssociationError(
57
+ "Missing association for expressions chain"
58
+ )
59
+ if self.fieldname == self.association.left_field.fieldname:
60
+ asset_type = self.association.left_field.asset.name
61
+ elif self.fieldname == self.association.right_field.fieldname:
62
+ asset_type = self.association.right_field.asset.name
63
+ else:
64
+ raise LanguageGraphException(
65
+ 'Failed to find fieldname "%s" in association:\n%s' %
66
+ (
67
+ self.fieldname,
68
+ json.dumps(self.association.to_dict(),
69
+ indent=2)
70
+ )
71
+ )
72
+
73
+ return {
74
+ self.association.name:
75
+ {
76
+ 'fieldname': self.fieldname,
77
+ 'asset type': asset_type
78
+ },
79
+ 'type': self.type
80
+ }
81
+
82
+ case 'transitive':
83
+ if not self.sub_link:
84
+ raise LanguageGraphException(
85
+ "No sub link for transitive expressions chain"
86
+ )
87
+ return {
88
+ 'transitive': self.sub_link.to_dict(),
89
+ 'type': self.type
90
+ }
91
+
92
+ case 'subType':
93
+ if not self.subtype:
94
+ raise LanguageGraphException(
95
+ "No subtype for expressions chain"
96
+ )
97
+ if not self.sub_link:
98
+ raise LanguageGraphException(
99
+ "No sub link for subtype expressions chain"
100
+ )
101
+ return {
102
+ 'subType': self.subtype.name,
103
+ 'expression': self.sub_link.to_dict(),
104
+ 'type': self.type
105
+ }
106
+
107
+ case _:
108
+ msg = 'Unknown associations chain element %s!'
109
+ logger.error(msg, self.type)
110
+ raise LanguageGraphAssociationError(msg % self.type)
111
+
112
+ @classmethod
113
+ def _from_dict(cls,
114
+ serialized_expr_chain: dict,
115
+ lang_graph: LanguageGraph,
116
+ ) -> ExpressionsChain | None:
117
+ """Create ExpressionsChain from dict
118
+ Args:
119
+ serialized_expr_chain - expressions chain in dict format
120
+ lang_graph - the LanguageGraph that contains the assets,
121
+ associations, and attack steps relevant for
122
+ the expressions chain
123
+ """
124
+ if serialized_expr_chain is None or not serialized_expr_chain:
125
+ return None
126
+
127
+ if 'type' not in serialized_expr_chain:
128
+ logger.debug(json.dumps(serialized_expr_chain, indent=2))
129
+ msg = 'Missing expressions chain type!'
130
+ logger.error(msg)
131
+ raise LanguageGraphAssociationError(msg)
132
+
133
+ expr_chain_type = serialized_expr_chain['type']
134
+ match (expr_chain_type):
135
+ case 'union' | 'intersection' | 'difference' | 'collect':
136
+ left_link = cls._from_dict(
137
+ serialized_expr_chain[expr_chain_type]['left'],
138
+ lang_graph
139
+ )
140
+ right_link = cls._from_dict(
141
+ serialized_expr_chain[expr_chain_type]['right'],
142
+ lang_graph
143
+ )
144
+ new_expr_chain = ExpressionsChain(
145
+ type=expr_chain_type,
146
+ left_link=left_link,
147
+ right_link=right_link
148
+ )
149
+ return new_expr_chain
150
+
151
+ case 'field':
152
+ assoc_name = list(serialized_expr_chain.keys())[0]
153
+ target_asset = lang_graph.assets[
154
+ serialized_expr_chain[assoc_name]['asset type']]
155
+ fieldname = serialized_expr_chain[assoc_name]['fieldname']
156
+
157
+ association = None
158
+ for assoc in target_asset.associations.values():
159
+ if assoc.contains_fieldname(fieldname) and \
160
+ assoc.name == assoc_name:
161
+ association = assoc
162
+ break
163
+
164
+ if association is None:
165
+ msg = 'Failed to find association "%s" with '\
166
+ 'fieldname "%s"'
167
+ logger.error(msg, assoc_name, fieldname)
168
+ raise LanguageGraphException(
169
+ msg % (assoc_name, fieldname)
170
+ )
171
+
172
+ new_expr_chain = ExpressionsChain(
173
+ type='field',
174
+ association=association,
175
+ fieldname=fieldname
176
+ )
177
+ return new_expr_chain
178
+
179
+ case 'transitive':
180
+ sub_link = cls._from_dict(
181
+ serialized_expr_chain['transitive'],
182
+ lang_graph
183
+ )
184
+ new_expr_chain = ExpressionsChain(
185
+ type='transitive',
186
+ sub_link=sub_link
187
+ )
188
+ return new_expr_chain
189
+
190
+ case 'subType':
191
+ sub_link = cls._from_dict(
192
+ serialized_expr_chain['expression'],
193
+ lang_graph
194
+ )
195
+ subtype_name = serialized_expr_chain['subType']
196
+ if subtype_name in lang_graph.assets:
197
+ subtype_asset = lang_graph.assets[subtype_name]
198
+ else:
199
+ msg = 'Failed to find subtype %s'
200
+ logger.error(msg, subtype_name)
201
+ raise LanguageGraphException(msg % subtype_name)
202
+
203
+ new_expr_chain = ExpressionsChain(
204
+ type='subType',
205
+ sub_link=sub_link,
206
+ subtype=subtype_asset
207
+ )
208
+ return new_expr_chain
209
+
210
+ case _:
211
+ msg = 'Unknown expressions chain type %s!'
212
+ logger.error(msg, serialized_expr_chain['type'])
213
+ raise LanguageGraphAssociationError(
214
+ msg % serialized_expr_chain['type']
215
+ )
216
+
217
+ def __repr__(self) -> str:
218
+ return str(self.to_dict())
@@ -0,0 +1,180 @@
1
+ """LanguageGraphAsset functionality
2
+ - Represents an asset (type) defined in a MAL language
3
+ """
4
+
5
+ from __future__ import annotations
6
+ from typing import TYPE_CHECKING
7
+ from dataclasses import dataclass, field
8
+ from functools import cached_property
9
+ from typing import Any
10
+
11
+ from maltoolbox.language.language_graph_attack_step import LanguageGraphAttackStep
12
+
13
+ if TYPE_CHECKING:
14
+ from maltoolbox.language.expression_chain import ExpressionsChain
15
+ from maltoolbox.language.language_graph_assoc import LanguageGraphAssociation
16
+
17
+ @dataclass
18
+ class LanguageGraphAsset:
19
+ """An asset type as defined in the MAL language"""
20
+
21
+ name: str
22
+ own_associations: dict[str, LanguageGraphAssociation] = \
23
+ field(default_factory=dict)
24
+ attack_steps: dict[str, LanguageGraphAttackStep] = \
25
+ field(default_factory=dict)
26
+ info: dict = field(default_factory=dict)
27
+ own_super_asset: LanguageGraphAsset | None = None
28
+ own_sub_assets: list[LanguageGraphAsset] = field(default_factory=list)
29
+ own_variables: dict = field(default_factory=dict)
30
+ is_abstract: bool | None = None
31
+
32
+ def to_dict(self) -> dict:
33
+ """Convert LanguageGraphAsset to dictionary"""
34
+ node_dict: dict[str, Any] = {
35
+ 'name': self.name,
36
+ 'associations': {},
37
+ 'attack_steps': {},
38
+ 'info': self.info,
39
+ 'super_asset': self.own_super_asset.name
40
+ if self.own_super_asset else "",
41
+ 'sub_assets': [asset.name for asset in self.own_sub_assets],
42
+ 'variables': {},
43
+ 'is_abstract': self.is_abstract
44
+ }
45
+
46
+ for fieldname, assoc in self.own_associations.items():
47
+ node_dict['associations'][fieldname] = assoc.to_dict()
48
+ for attack_step in self.attack_steps.values():
49
+ node_dict['attack_steps'][attack_step.name] = \
50
+ attack_step.to_dict()
51
+ for variable_name, (var_target_asset, var_expr_chain) in \
52
+ self.own_variables.items():
53
+ node_dict['variables'][variable_name] = (
54
+ var_target_asset.name,
55
+ var_expr_chain.to_dict()
56
+ )
57
+ return node_dict
58
+
59
+ def __repr__(self) -> str:
60
+ return f'LanguageGraphAsset(name: "{self.name}")'
61
+
62
+ def __hash__(self):
63
+ return id(self)
64
+
65
+ def is_subasset_of(self, target_asset: LanguageGraphAsset) -> bool:
66
+ """Check if an asset extends the target asset through inheritance.
67
+
68
+ Arguments:
69
+ ---------
70
+ target_asset - the target asset we wish to evaluate if this asset
71
+ extends
72
+
73
+ Return:
74
+ ------
75
+ True if this asset extends the target_asset via inheritance.
76
+ False otherwise.
77
+
78
+ """
79
+ current_asset: LanguageGraphAsset | None = self
80
+ while current_asset:
81
+ if current_asset == target_asset:
82
+ return True
83
+ current_asset = current_asset.own_super_asset
84
+ return False
85
+
86
+ @cached_property
87
+ def sub_assets(self) -> set[LanguageGraphAsset]:
88
+ """Return a list of all of the assets that directly or indirectly extend
89
+ this asset.
90
+
91
+ Return:
92
+ ------
93
+ A list of all of the assets that extend this asset plus itself.
94
+
95
+ """
96
+ subassets: list[LanguageGraphAsset] = []
97
+ for subasset in self.own_sub_assets:
98
+ subassets.extend(subasset.sub_assets)
99
+
100
+ subassets.extend(self.own_sub_assets)
101
+ subassets.append(self)
102
+
103
+ return set(subassets)
104
+
105
+ @cached_property
106
+ def super_assets(self) -> list[LanguageGraphAsset]:
107
+ """Return a list of all of the assets that this asset directly or
108
+ indirectly extends.
109
+
110
+ Return:
111
+ ------
112
+ A list of all of the assets that this asset extends plus itself.
113
+
114
+ """
115
+ current_asset: LanguageGraphAsset | None = self
116
+ superassets = []
117
+ while current_asset:
118
+ superassets.append(current_asset)
119
+ current_asset = current_asset.own_super_asset
120
+ return superassets
121
+
122
+ def associations_to(
123
+ self, asset_type: LanguageGraphAsset
124
+ ) -> dict[str, LanguageGraphAssociation]:
125
+ """Return dict of association types that go from self
126
+ to given `asset_type`
127
+ """
128
+ associations_to_asset_type = {}
129
+ for fieldname, association in self.associations.items():
130
+ if association in asset_type.associations.values():
131
+ associations_to_asset_type[fieldname] = association
132
+ return associations_to_asset_type
133
+
134
+ @cached_property
135
+ def associations(self) -> dict[str, LanguageGraphAssociation]:
136
+ """Return a list of all of the associations that belong to this asset
137
+ directly or indirectly via inheritance.
138
+
139
+ Return:
140
+ ------
141
+ A list of all of the associations that apply to this asset, either
142
+ directly or via inheritance.
143
+
144
+ """
145
+ associations = dict(self.own_associations)
146
+ if self.own_super_asset:
147
+ associations |= self.own_super_asset.associations
148
+ return associations
149
+
150
+ @property
151
+ def variables(
152
+ self
153
+ ) -> dict[str, tuple[LanguageGraphAsset, ExpressionsChain]]:
154
+ """Return a list of all of the variables that belong to this asset
155
+ directly or indirectly via inheritance.
156
+
157
+ Return:
158
+ ------
159
+ A list of all of the variables that apply to this asset, either
160
+ directly or via inheritance.
161
+
162
+ """
163
+ all_vars = dict(self.own_variables)
164
+ if self.own_super_asset:
165
+ all_vars |= self.own_super_asset.variables
166
+ return all_vars
167
+
168
+ def get_all_common_superassets(
169
+ self, other: LanguageGraphAsset
170
+ ) -> set[str]:
171
+ """Return a set of all common ancestors between this asset
172
+ and the other asset given as parameter
173
+ """
174
+ self_superassets = set(
175
+ asset.name for asset in self.super_assets
176
+ )
177
+ other_superassets = set(
178
+ asset.name for asset in other.super_assets
179
+ )
180
+ return self_superassets.intersection(other_superassets)
@@ -0,0 +1,147 @@
1
+ """LanguageGraphAssoc functionality
2
+ - Represents an association (type) defined in a MAL language
3
+ """
4
+
5
+ from __future__ import annotations
6
+ from dataclasses import dataclass, field
7
+ import logging
8
+ from typing import Any
9
+
10
+ from maltoolbox.exceptions import LanguageGraphAssociationError
11
+ from maltoolbox.language.language_graph_asset import LanguageGraphAsset
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def link_association_to_assets(
17
+ assoc: LanguageGraphAssociation,
18
+ left_asset: LanguageGraphAsset,
19
+ right_asset: LanguageGraphAsset,
20
+ ) -> None:
21
+ left_asset.own_associations[assoc.right_field.fieldname] = assoc
22
+ right_asset.own_associations[assoc.left_field.fieldname] = assoc
23
+
24
+
25
+ @dataclass(frozen=True, eq=True)
26
+ class LanguageGraphAssociationField:
27
+ """A field in an association"""
28
+
29
+ asset: LanguageGraphAsset
30
+ fieldname: str
31
+ minimum: int
32
+ maximum: int
33
+
34
+
35
+ @dataclass(frozen=True, eq=True)
36
+ class LanguageGraphAssociation:
37
+ """An association type between asset types as defined in the MAL language
38
+ """
39
+
40
+ name: str
41
+ left_field: LanguageGraphAssociationField
42
+ right_field: LanguageGraphAssociationField
43
+ info: dict = field(default_factory=dict, compare=False)
44
+
45
+ def to_dict(self) -> dict:
46
+ """Convert LanguageGraphAssociation to dictionary"""
47
+ assoc_dict = {
48
+ 'name': self.name,
49
+ 'info': self.info,
50
+ 'left': {
51
+ 'asset': self.left_field.asset.name,
52
+ 'fieldname': self.left_field.fieldname,
53
+ 'min': self.left_field.minimum,
54
+ 'max': self.left_field.maximum
55
+ },
56
+ 'right': {
57
+ 'asset': self.right_field.asset.name,
58
+ 'fieldname': self.right_field.fieldname,
59
+ 'min': self.right_field.minimum,
60
+ 'max': self.right_field.maximum
61
+ }
62
+ }
63
+
64
+ return assoc_dict
65
+
66
+ def __repr__(self) -> str:
67
+ return (
68
+ f'LanguageGraphAssociation(name: "{self.name}", '
69
+ f'left_field: {self.left_field}, '
70
+ f'right_field: {self.right_field})'
71
+ )
72
+
73
+ @property
74
+ def full_name(self) -> str:
75
+ """Return the full name of the association. This is a combination of the
76
+ association name, left field name, left asset type, right field name,
77
+ and right asset type.
78
+ """
79
+ full_name = '%s_%s_%s' % (
80
+ self.name,
81
+ self.left_field.fieldname,
82
+ self.right_field.fieldname
83
+ )
84
+ return full_name
85
+
86
+ def get_field(self, fieldname: str) -> LanguageGraphAssociationField:
87
+ """Return the field that matches the `fieldname` given as parameter.
88
+ """
89
+ if self.right_field.fieldname == fieldname:
90
+ return self.right_field
91
+ return self.left_field
92
+
93
+ def contains_fieldname(self, fieldname: str) -> bool:
94
+ """Check if the association contains the field name given as a parameter.
95
+
96
+ Arguments:
97
+ ---------
98
+ fieldname - the field name to look for
99
+ Return True if either of the two field names matches.
100
+ False, otherwise.
101
+
102
+ """
103
+ if self.left_field.fieldname == fieldname:
104
+ return True
105
+ if self.right_field.fieldname == fieldname:
106
+ return True
107
+ return False
108
+
109
+ def contains_asset(self, asset: Any) -> bool:
110
+ """Check if the association matches the asset given as a parameter. A
111
+ match can either be an explicit one or if the asset given subassets
112
+ either of the two assets that are part of the association.
113
+
114
+ Arguments:
115
+ ---------
116
+ asset - the asset to look for
117
+ Return True if either of the two asset matches.
118
+ False, otherwise.
119
+
120
+ """
121
+ if asset.is_subasset_of(self.left_field.asset):
122
+ return True
123
+ if asset.is_subasset_of(self.right_field.asset):
124
+ return True
125
+ return False
126
+
127
+ def get_opposite_fieldname(self, fieldname: str) -> str:
128
+ """Return the opposite field name if the association contains the field
129
+ name given as a parameter.
130
+
131
+ Arguments:
132
+ ---------
133
+ fieldname - the field name to look for
134
+ Return the other field name if the parameter matched either of the
135
+ two. None, otherwise.
136
+
137
+ """
138
+ if self.left_field.fieldname == fieldname:
139
+ return self.right_field.fieldname
140
+ if self.right_field.fieldname == fieldname:
141
+ return self.left_field.fieldname
142
+
143
+ msg = ('Requested fieldname "%s" from association '
144
+ '%s which did not contain it!')
145
+ logger.error(msg, fieldname, self.name)
146
+ raise LanguageGraphAssociationError(msg % (fieldname, self.name))
147
+