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.
- {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/METADATA +8 -75
- mal_toolbox-2.1.0.dist-info/RECORD +51 -0
- {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/WHEEL +1 -1
- maltoolbox/__init__.py +2 -2
- maltoolbox/attackgraph/__init__.py +2 -2
- maltoolbox/attackgraph/attackgraph.py +121 -549
- maltoolbox/attackgraph/factories.py +68 -0
- maltoolbox/attackgraph/file_utils.py +0 -0
- maltoolbox/attackgraph/generate.py +338 -0
- maltoolbox/attackgraph/node.py +1 -0
- maltoolbox/attackgraph/node_getters.py +36 -0
- maltoolbox/attackgraph/ttcs.py +28 -0
- maltoolbox/language/__init__.py +2 -2
- maltoolbox/language/compiler/__init__.py +4 -499
- maltoolbox/language/compiler/distributions.py +158 -0
- maltoolbox/language/compiler/exceptions.py +37 -0
- maltoolbox/language/compiler/lang.py +5 -0
- maltoolbox/language/compiler/mal_analyzer.py +920 -0
- maltoolbox/language/compiler/mal_compiler.py +1071 -0
- maltoolbox/language/detector.py +43 -0
- maltoolbox/language/expression_chain.py +218 -0
- maltoolbox/language/language_graph_asset.py +180 -0
- maltoolbox/language/language_graph_assoc.py +147 -0
- maltoolbox/language/language_graph_attack_step.py +129 -0
- maltoolbox/language/language_graph_builder.py +282 -0
- maltoolbox/language/language_graph_loaders.py +7 -0
- maltoolbox/language/language_graph_lookup.py +140 -0
- maltoolbox/language/language_graph_serialization.py +5 -0
- maltoolbox/language/languagegraph.py +244 -1536
- maltoolbox/language/step_expression_processor.py +491 -0
- mal_toolbox-1.2.1.dist-info/RECORD +0 -33
- maltoolbox/language/compiler/mal_lexer.py +0 -232
- maltoolbox/language/compiler/mal_parser.py +0 -3159
- {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/entry_points.txt +0 -0
- {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/AUTHORS +0 -0
- {mal_toolbox-1.2.1.dist-info → mal_toolbox-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
|