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/ingestors/neo4j.py
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
"""
|
|
2
2
|
MAL-Toolbox Neo4j Ingestor Module
|
|
3
3
|
"""
|
|
4
|
+
# mypy: ignore-errors
|
|
4
5
|
|
|
5
6
|
import logging
|
|
6
7
|
|
|
7
8
|
from py2neo import Graph, Node, Relationship, Subgraph
|
|
8
9
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
10
|
+
from ..model import AttackerAttachment, Model
|
|
11
|
+
from ..language import LanguageGraph, LanguageClassesFactory
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
14
15
|
def ingest_attack_graph(graph,
|
|
15
|
-
uri,
|
|
16
|
-
username,
|
|
17
|
-
password,
|
|
18
|
-
dbname,
|
|
19
|
-
delete=False
|
|
20
|
-
) -> None:
|
|
16
|
+
uri: str,
|
|
17
|
+
username: str,
|
|
18
|
+
password: str,
|
|
19
|
+
dbname: str,
|
|
20
|
+
delete: bool = False
|
|
21
|
+
) -> None:
|
|
21
22
|
"""
|
|
22
23
|
Ingest an attack graph into a neo4j database
|
|
23
24
|
|
|
@@ -31,7 +32,6 @@ def ingest_attack_graph(graph,
|
|
|
31
32
|
before ingesting the new attack graph
|
|
32
33
|
"""
|
|
33
34
|
|
|
34
|
-
|
|
35
35
|
g = Graph(uri=uri, user=username, password=password, name=dbname)
|
|
36
36
|
if delete:
|
|
37
37
|
g.delete_all()
|
|
@@ -43,7 +43,7 @@ def ingest_attack_graph(graph,
|
|
|
43
43
|
nodes[node.id] = Node(
|
|
44
44
|
node_dict['asset'] if 'asset' in node_dict else node_dict['id'],
|
|
45
45
|
name = node_dict['name'],
|
|
46
|
-
full_name =
|
|
46
|
+
full_name = node.full_name,
|
|
47
47
|
type = node_dict['type'],
|
|
48
48
|
ttc = str(node_dict['ttc']),
|
|
49
49
|
is_necessary = str(node.is_necessary),
|
|
@@ -65,12 +65,12 @@ def ingest_attack_graph(graph,
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
def ingest_model(model,
|
|
68
|
-
uri,
|
|
69
|
-
username,
|
|
70
|
-
password,
|
|
71
|
-
dbname,
|
|
72
|
-
delete=False
|
|
73
|
-
) -> None:
|
|
68
|
+
uri: str,
|
|
69
|
+
username: str,
|
|
70
|
+
password: str,
|
|
71
|
+
dbname: str,
|
|
72
|
+
delete: bool = False
|
|
73
|
+
) -> None:
|
|
74
74
|
"""
|
|
75
75
|
Ingest an instance model graph into a Neo4J database
|
|
76
76
|
|
|
@@ -91,16 +91,14 @@ def ingest_model(model,
|
|
|
91
91
|
rels = []
|
|
92
92
|
|
|
93
93
|
for asset in model.assets:
|
|
94
|
-
nodeid = asset.name
|
|
95
94
|
|
|
96
|
-
nodes[str(asset.id)] = Node(str(asset.
|
|
95
|
+
nodes[str(asset.id)] = Node(str(asset.type),
|
|
97
96
|
name=str(asset.name),
|
|
98
97
|
asset_id=str(asset.id),
|
|
99
|
-
|
|
98
|
+
type=str(asset.type))
|
|
100
99
|
|
|
101
100
|
for assoc in model.associations:
|
|
102
|
-
firstElementName =
|
|
103
|
-
secondElementName = list(vars(assoc)['_properties'])[1]
|
|
101
|
+
firstElementName, secondElementName = assoc._properties.keys()
|
|
104
102
|
firstElements = getattr(assoc, firstElementName)
|
|
105
103
|
secondElements = getattr(assoc, secondElementName)
|
|
106
104
|
for first_asset in firstElements:
|
|
@@ -120,46 +118,47 @@ def ingest_model(model,
|
|
|
120
118
|
g.commit(tx)
|
|
121
119
|
|
|
122
120
|
|
|
123
|
-
def get_model(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
121
|
+
def get_model(
|
|
122
|
+
uri: str,
|
|
123
|
+
username: str,
|
|
124
|
+
password: str,
|
|
125
|
+
dbname: str,
|
|
126
|
+
lang_graph: LanguageGraph,
|
|
127
|
+
lang_classes_factory: LanguageClassesFactory
|
|
128
|
+
) -> Model:
|
|
129
|
+
"""Load a model from Neo4j"""
|
|
130
130
|
|
|
131
131
|
g = Graph(uri=uri, user=username, password=password, name=dbname)
|
|
132
132
|
|
|
133
|
-
instance_model =
|
|
134
|
-
lang_classes_factory)
|
|
133
|
+
instance_model = Model('Neo4j imported model', lang_classes_factory)
|
|
135
134
|
# Get all assets
|
|
136
|
-
assets_results = g.run('MATCH (a) WHERE a.
|
|
135
|
+
assets_results = g.run('MATCH (a) WHERE a.type IS NOT NULL RETURN DISTINCT a').data()
|
|
137
136
|
for asset in assets_results:
|
|
138
137
|
asset_data = dict(asset['a'])
|
|
139
|
-
logger.debug(
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
logger.debug(
|
|
139
|
+
'Loading asset from Neo4j instance:\n%s', str(asset_data)
|
|
140
|
+
)
|
|
141
|
+
if asset_data['type'] == 'Attacker':
|
|
142
142
|
attacker_id = int(asset_data['asset_id'])
|
|
143
|
-
attacker =
|
|
143
|
+
attacker = AttackerAttachment()
|
|
144
144
|
attacker.entry_points = []
|
|
145
145
|
instance_model.add_attacker(attacker, attacker_id = attacker_id)
|
|
146
146
|
continue
|
|
147
147
|
|
|
148
|
-
if not hasattr(lang_classes_factory.ns,
|
|
149
|
-
|
|
150
|
-
logger.error(
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
if not hasattr(lang_classes_factory.ns, asset_data['type']):
|
|
149
|
+
msg = 'Failed to find %s asset in language specification!'
|
|
150
|
+
logger.error(msg, asset_data["type"])
|
|
151
|
+
raise LookupError(msg % asset_data["type"])
|
|
152
|
+
|
|
153
153
|
asset_obj = getattr(lang_classes_factory.ns,
|
|
154
|
-
asset_data['
|
|
154
|
+
asset_data['type'])(name = asset_data['name'])
|
|
155
155
|
asset_id = int(asset_data['asset_id'])
|
|
156
156
|
|
|
157
157
|
#TODO Process defense values when they are included in Neo4j
|
|
158
|
-
|
|
159
158
|
instance_model.add_asset(asset_obj, asset_id)
|
|
160
159
|
|
|
161
160
|
# Get all relationships
|
|
162
|
-
assocs_results = g.run('MATCH (a)-[r1]->(b),(a)<-[r2]-(b) WHERE a.
|
|
161
|
+
assocs_results = g.run('MATCH (a)-[r1]->(b),(a)<-[r2]-(b) WHERE a.type IS NOT NULL RETURN DISTINCT a, r1, r2, b').data()
|
|
163
162
|
|
|
164
163
|
for assoc in assocs_results:
|
|
165
164
|
left_field = list(assoc['r1'].types())[0]
|
|
@@ -167,15 +166,14 @@ def get_model(uri,
|
|
|
167
166
|
left_asset = dict(assoc['a'])
|
|
168
167
|
right_asset = dict(assoc['b'])
|
|
169
168
|
|
|
170
|
-
logger.debug(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
f'\"{right_asset["metaconcept"]}\") '
|
|
175
|
-
f'from Neo4j instance.')
|
|
169
|
+
logger.debug(
|
|
170
|
+
'Load association ("%s", "%s", "%s", "%s") from Neo4j instance.',
|
|
171
|
+
left_field, right_field, left_asset["type"], right_asset["type"]
|
|
172
|
+
)
|
|
176
173
|
|
|
177
174
|
left_id = int(left_asset['asset_id'])
|
|
178
175
|
right_id = int(right_asset['asset_id'])
|
|
176
|
+
|
|
179
177
|
attacker_id = None
|
|
180
178
|
if left_field == 'firstSteps':
|
|
181
179
|
attacker_id = right_id
|
|
@@ -186,59 +184,72 @@ def get_model(uri,
|
|
|
186
184
|
target_id = right_id
|
|
187
185
|
target_prop = left_field
|
|
188
186
|
|
|
189
|
-
if attacker_id:
|
|
187
|
+
if attacker_id is not None:
|
|
190
188
|
attacker = instance_model.get_attacker_by_id(attacker_id)
|
|
191
189
|
if not attacker:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
msg = 'Failed to find attacker with id %s in model!'
|
|
191
|
+
logger.error(msg, attacker_id)
|
|
192
|
+
raise LookupError(msg % attacker_id)
|
|
195
193
|
target_asset = instance_model.get_asset_by_id(target_id)
|
|
196
194
|
if not target_asset:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
msg = 'Failed to find asset with id %d in model!'
|
|
196
|
+
logger.error(msg, target_id)
|
|
197
|
+
raise LookupError(msg % target_id)
|
|
200
198
|
attacker.entry_points.append((target_asset,
|
|
201
199
|
[target_prop]))
|
|
202
200
|
continue
|
|
203
201
|
|
|
204
202
|
left_asset = instance_model.get_asset_by_id(left_id)
|
|
205
|
-
if
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
if left_asset is None:
|
|
204
|
+
msg = 'Failed to find asset with id %d in model!'
|
|
205
|
+
logger.error(msg, left_id)
|
|
206
|
+
raise LookupError(msg % left_id)
|
|
209
207
|
right_asset = instance_model.get_asset_by_id(right_id)
|
|
210
|
-
if
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
208
|
+
if right_asset is None:
|
|
209
|
+
msg = 'Failed to find asset with id %d in model!'
|
|
210
|
+
logger.error(msg, right_id)
|
|
211
|
+
raise LookupError(msg % right_id)
|
|
214
212
|
|
|
215
|
-
|
|
216
|
-
lang_spec,
|
|
213
|
+
assoc = lang_graph.get_association_by_fields_and_assets(
|
|
217
214
|
left_field,
|
|
218
215
|
right_field,
|
|
219
|
-
left_asset.
|
|
220
|
-
right_asset.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
'association in language specification!')
|
|
216
|
+
left_asset.type,
|
|
217
|
+
right_asset.type)
|
|
218
|
+
|
|
219
|
+
if not assoc:
|
|
220
|
+
logger.error(
|
|
221
|
+
'Failed to find ("%s", "%s", "%s", "%s")'
|
|
222
|
+
'association in language specification!',
|
|
223
|
+
left_asset.type, right_asset.type,
|
|
224
|
+
left_field, right_field
|
|
225
|
+
)
|
|
230
226
|
return None
|
|
231
227
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
228
|
+
logger.debug('Found "%s" association.', assoc.name)
|
|
229
|
+
|
|
230
|
+
assoc_name = lang_classes_factory.get_association_by_signature(
|
|
231
|
+
assoc.name,
|
|
232
|
+
left_asset.type,
|
|
233
|
+
right_asset.type
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if not assoc_name:
|
|
237
|
+
msg = 'Failed to find \"%s\" association in language specification!'
|
|
238
|
+
logger.error(msg, assoc.name)
|
|
239
|
+
raise LookupError(msg % assoc.name)
|
|
237
240
|
|
|
238
241
|
assoc = getattr(lang_classes_factory.ns, assoc_name)()
|
|
239
242
|
setattr(assoc, left_field, [left_asset])
|
|
240
243
|
setattr(assoc, right_field, [right_asset])
|
|
241
|
-
instance_model.
|
|
244
|
+
if not (instance_model.association_exists_between_assets(
|
|
245
|
+
assoc_name,
|
|
246
|
+
left_asset,
|
|
247
|
+
right_asset
|
|
248
|
+
) or instance_model.association_exists_between_assets(
|
|
249
|
+
assoc_name,
|
|
250
|
+
right_asset,
|
|
251
|
+
left_asset
|
|
252
|
+
)):
|
|
253
|
+
instance_model.add_association(assoc)
|
|
242
254
|
|
|
243
255
|
return instance_model
|
|
244
|
-
|
|
@@ -1,43 +1,59 @@
|
|
|
1
1
|
"""
|
|
2
2
|
MAL-Toolbox Language Classes Factory Module
|
|
3
|
+
Uses python_jsonschema_objects to generate python classes from a MAL language
|
|
3
4
|
"""
|
|
4
|
-
|
|
5
|
-
import python_jsonschema_objects as pjs
|
|
5
|
+
from __future__ import annotations
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
import python_jsonschema_objects as pjs
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from typing import Literal, Optional, TypeAlias
|
|
14
|
+
from maltoolbox.language import LanguageGraph
|
|
15
|
+
from python_jsonschema_objects.classbuilder import ProtocolBase
|
|
16
|
+
|
|
17
|
+
SchemaGeneratedClass: TypeAlias = ProtocolBase
|
|
8
18
|
|
|
9
19
|
logger = logging.getLogger(__name__)
|
|
10
20
|
|
|
11
21
|
class LanguageClassesFactory:
|
|
12
|
-
def __init__(self,
|
|
13
|
-
self.
|
|
14
|
-
self.json_schema = {}
|
|
22
|
+
def __init__(self, lang_graph: LanguageGraph):
|
|
23
|
+
self.lang_graph: LanguageGraph = lang_graph
|
|
24
|
+
self.json_schema: dict = {}
|
|
25
|
+
self._create_classes()
|
|
15
26
|
|
|
16
|
-
def
|
|
27
|
+
def _generate_assets(self) -> None:
|
|
17
28
|
"""
|
|
18
|
-
Generate JSON Schema for
|
|
29
|
+
Generate JSON Schema for asset types in the language specification.
|
|
19
30
|
"""
|
|
20
|
-
for asset in self.
|
|
21
|
-
logger.debug(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'
|
|
25
|
-
|
|
31
|
+
for asset in self.lang_graph.assets:
|
|
32
|
+
logger.debug('Creating %s asset JSON schema entry.', asset.name)
|
|
33
|
+
asset_json_entry = {
|
|
34
|
+
'title': asset.name,
|
|
35
|
+
'type': 'object',
|
|
36
|
+
'properties': {},
|
|
37
|
+
}
|
|
38
|
+
asset_json_entry['properties']['id'] = {
|
|
39
|
+
'type' : 'integer',
|
|
40
|
+
}
|
|
41
|
+
asset_json_entry['properties']['type'] = \
|
|
26
42
|
{
|
|
27
43
|
'type' : 'string',
|
|
28
|
-
'default': asset
|
|
44
|
+
'default': asset.name
|
|
29
45
|
}
|
|
30
|
-
if asset
|
|
31
|
-
asset_json_entry['allOf'] = [
|
|
32
|
-
'$ref': '#/definitions/LanguageAsset/definitions/' +
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if defense
|
|
46
|
+
if asset.super_assets:
|
|
47
|
+
asset_json_entry['allOf'] = [
|
|
48
|
+
{'$ref': '#/definitions/LanguageAsset/definitions/' + superasset.name}
|
|
49
|
+
for superasset in asset.super_assets
|
|
50
|
+
]
|
|
51
|
+
for defense in filter(lambda step: step.type == 'defense', asset.attack_steps):
|
|
52
|
+
if defense.ttc and defense.ttc['name'] == 'Enabled':
|
|
37
53
|
default_defense_value = 1.0
|
|
38
54
|
else:
|
|
39
55
|
default_defense_value = 0.0
|
|
40
|
-
asset_json_entry['properties'][defense
|
|
56
|
+
asset_json_entry['properties'][defense.name] = \
|
|
41
57
|
{
|
|
42
58
|
'type' : 'number',
|
|
43
59
|
'minimum' : 0,
|
|
@@ -45,89 +61,93 @@ class LanguageClassesFactory:
|
|
|
45
61
|
'default': default_defense_value
|
|
46
62
|
}
|
|
47
63
|
self.json_schema['definitions']['LanguageAsset']['definitions']\
|
|
48
|
-
[asset
|
|
49
|
-
self.json_schema['definitions']['LanguageAsset']['oneOf']
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
[asset.name] = asset_json_entry
|
|
65
|
+
self.json_schema['definitions']['LanguageAsset']['oneOf'].append(
|
|
66
|
+
{'$ref': '#/definitions/LanguageAsset/definitions/' + asset.name}
|
|
67
|
+
)
|
|
52
68
|
|
|
53
|
-
def
|
|
69
|
+
def _generate_associations(self) -> None:
|
|
54
70
|
"""
|
|
55
|
-
Generate JSON Schema for
|
|
71
|
+
Generate JSON Schema for association types in the language specification.
|
|
56
72
|
"""
|
|
57
|
-
def create_association_entry(assoc):
|
|
58
|
-
logger.debug(
|
|
59
|
-
'
|
|
60
|
-
assoc_json_entry = {'title': assoc['name'], 'type': 'object',
|
|
61
|
-
'properties': {}}
|
|
73
|
+
def create_association_entry(assoc: SchemaGeneratedClass):
|
|
74
|
+
logger.debug('Creating %s association JSON schema entry.', assoc.name)
|
|
75
|
+
assoc_json_entry = {'title': assoc.name, 'type': 'object', 'properties': {}}
|
|
62
76
|
|
|
63
77
|
create_association_field(assoc, assoc_json_entry, 'left')
|
|
64
78
|
create_association_field(assoc, assoc_json_entry, 'right')
|
|
65
79
|
return assoc_json_entry
|
|
66
80
|
|
|
67
|
-
def create_association_with_subentries(assoc):
|
|
68
|
-
if (assoc
|
|
81
|
+
def create_association_with_subentries(assoc: SchemaGeneratedClass):
|
|
82
|
+
if (assoc.name not in self.json_schema['definitions']\
|
|
69
83
|
['LanguageAssociation']['definitions']):
|
|
70
|
-
logger.info(
|
|
71
|
-
|
|
72
|
-
'Creating subentries for each one.'
|
|
84
|
+
logger.info(
|
|
85
|
+
'Multiple associations with name %s exist. '
|
|
86
|
+
'Creating subentries for each one.', assoc.name
|
|
87
|
+
)
|
|
73
88
|
self.json_schema['definitions']['LanguageAssociation']\
|
|
74
|
-
['definitions'][assoc
|
|
89
|
+
['definitions'][assoc.name] =\
|
|
75
90
|
{
|
|
76
|
-
'title': assoc
|
|
91
|
+
'title': assoc.name,
|
|
77
92
|
'type': 'object',
|
|
78
93
|
'oneOf': [],
|
|
79
94
|
'definitions': {}
|
|
80
95
|
}
|
|
81
96
|
self.json_schema['definitions']['LanguageAssociation']['oneOf'].\
|
|
82
97
|
append({'$ref': '#/definitions/LanguageAssociation/definitions/'
|
|
83
|
-
+ assoc
|
|
98
|
+
+ assoc.name})
|
|
84
99
|
|
|
85
100
|
assoc_json_subentry = create_association_entry(assoc)
|
|
86
|
-
subentry_name = assoc
|
|
87
|
-
+ assoc
|
|
101
|
+
subentry_name = assoc.name + '_' + assoc.left_field.asset.name + '_' \
|
|
102
|
+
+ assoc.right_field.asset.name
|
|
88
103
|
|
|
89
|
-
logger.info(
|
|
104
|
+
logger.info('Creating %s subentry association.', subentry_name)
|
|
90
105
|
assoc_json_subentry['title'] = subentry_name
|
|
91
106
|
self.json_schema['definitions']['LanguageAssociation']\
|
|
92
|
-
['definitions'][assoc
|
|
107
|
+
['definitions'][assoc.name]['definitions'][subentry_name] = assoc_json_subentry
|
|
93
108
|
self.json_schema['definitions']['LanguageAssociation']\
|
|
94
|
-
['definitions'][assoc
|
|
109
|
+
['definitions'][assoc.name]['oneOf'].append(
|
|
95
110
|
{'$ref': '#/definitions/LanguageAssociation/definitions/' \
|
|
96
|
-
+ assoc
|
|
97
|
-
|
|
98
|
-
def create_association_field(
|
|
99
|
-
|
|
111
|
+
+ assoc.name + '/definitions/' + subentry_name})
|
|
112
|
+
|
|
113
|
+
def create_association_field(
|
|
114
|
+
assoc: SchemaGeneratedClass,
|
|
115
|
+
assoc_json_entry: dict,
|
|
116
|
+
position: Literal['left', 'right']
|
|
117
|
+
) -> None:
|
|
118
|
+
field = getattr(assoc, position + "_field")
|
|
119
|
+
assoc_json_entry['properties'][field.fieldname] = \
|
|
100
120
|
{
|
|
101
121
|
'type' : 'array',
|
|
102
122
|
'items' :
|
|
103
123
|
{
|
|
104
124
|
'$ref':
|
|
105
125
|
'#/definitions/LanguageAsset/definitions/' +
|
|
106
|
-
|
|
126
|
+
field.asset.name
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
|
-
if
|
|
110
|
-
assoc_json_entry['properties'][
|
|
111
|
-
['maxItems'] =
|
|
129
|
+
if field.maximum:
|
|
130
|
+
assoc_json_entry['properties'][field.fieldname]\
|
|
131
|
+
['maxItems'] = field.maximum
|
|
112
132
|
|
|
113
133
|
|
|
114
|
-
for assoc in self.
|
|
115
|
-
count = len(list(filter(lambda temp_assoc: temp_assoc
|
|
116
|
-
assoc
|
|
134
|
+
for assoc in self.lang_graph.associations:
|
|
135
|
+
count = len(list(filter(lambda temp_assoc: temp_assoc.name ==
|
|
136
|
+
assoc.name, self.lang_graph.associations)))
|
|
117
137
|
if count > 1:
|
|
118
138
|
# If there are multiple associations with the same name we
|
|
119
139
|
# will need to create separate entries for each using their
|
|
120
140
|
# fieldnames.
|
|
121
|
-
|
|
141
|
+
create_association_with_subentries(assoc)
|
|
122
142
|
else:
|
|
123
143
|
assoc_json_entry = create_association_entry(assoc)
|
|
124
144
|
self.json_schema['definitions']['LanguageAssociation']\
|
|
125
|
-
['definitions'][assoc
|
|
145
|
+
['definitions'][assoc.name] = assoc_json_entry
|
|
126
146
|
self.json_schema['definitions']['LanguageAssociation']['oneOf'].\
|
|
127
147
|
append({'$ref': '#/definitions/LanguageAssociation/' +
|
|
128
|
-
'definitions/' + assoc
|
|
148
|
+
'definitions/' + assoc.name})
|
|
129
149
|
|
|
130
|
-
def
|
|
150
|
+
def _create_classes(self) -> None:
|
|
131
151
|
"""
|
|
132
152
|
Create classes based on the language specification.
|
|
133
153
|
"""
|
|
@@ -135,6 +155,8 @@ class LanguageClassesFactory:
|
|
|
135
155
|
# First, we have to translate the language specification into a JSON
|
|
136
156
|
# schema. Initialize the overall JSON schema structure.
|
|
137
157
|
self.json_schema = {
|
|
158
|
+
'$schema': 'http://json-schema.org/draft-04/schema#',
|
|
159
|
+
'id': f"urn:mal:{__name__.replace('.', ':')}",
|
|
138
160
|
'title': 'LanguageObject',
|
|
139
161
|
'type': 'object',
|
|
140
162
|
'oneOf':[
|
|
@@ -153,10 +175,69 @@ class LanguageClassesFactory:
|
|
|
153
175
|
'oneOf': [],
|
|
154
176
|
'definitions': {}}
|
|
155
177
|
|
|
156
|
-
self.
|
|
157
|
-
self.
|
|
158
|
-
|
|
178
|
+
self._generate_assets()
|
|
179
|
+
self._generate_associations()
|
|
180
|
+
|
|
181
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
182
|
+
# Avoid running json.dumps when not in debug
|
|
183
|
+
logger.debug(json.dumps(self.json_schema, indent = 2))
|
|
159
184
|
|
|
160
185
|
# Once we have the JSON schema we create the actual classes.
|
|
161
186
|
builder = pjs.ObjectBuilder(self.json_schema)
|
|
162
187
|
self.ns = builder.build_classes(standardize_names=False)
|
|
188
|
+
|
|
189
|
+
def get_association_by_signature(
|
|
190
|
+
self,
|
|
191
|
+
assoc_name: str,
|
|
192
|
+
left_asset: str,
|
|
193
|
+
right_asset: str
|
|
194
|
+
) -> Optional[str]:
|
|
195
|
+
"""
|
|
196
|
+
Get association name based on its signature. This is primarily
|
|
197
|
+
relevant for getting the exact association full name when multiple
|
|
198
|
+
associations with the same name exist.
|
|
199
|
+
|
|
200
|
+
Arguments:
|
|
201
|
+
assoc_name - the association name
|
|
202
|
+
left_asset - the name of the left asset type
|
|
203
|
+
right_asset - the name of the right asset type
|
|
204
|
+
|
|
205
|
+
Return: The matching association name if a match is found.
|
|
206
|
+
None if there is no match.
|
|
207
|
+
"""
|
|
208
|
+
lang_assocs_entries = self.json_schema['definitions']\
|
|
209
|
+
['LanguageAssociation']['definitions']
|
|
210
|
+
if not assoc_name in lang_assocs_entries:
|
|
211
|
+
raise LookupError(
|
|
212
|
+
'Failed to find "%s" association in the language json '
|
|
213
|
+
'schema.' % assoc_name
|
|
214
|
+
)
|
|
215
|
+
assoc_entry = lang_assocs_entries[assoc_name]
|
|
216
|
+
# If the association has a oneOf property it should always have more
|
|
217
|
+
# than just one alternative, but check just in case
|
|
218
|
+
if 'definitions' in assoc_entry and \
|
|
219
|
+
len(assoc_entry['definitions']) > 1:
|
|
220
|
+
full_name = '%s_%s_%s' % (
|
|
221
|
+
assoc_name,
|
|
222
|
+
left_asset,
|
|
223
|
+
right_asset
|
|
224
|
+
)
|
|
225
|
+
full_name_flipped = '%s_%s_%s' % (
|
|
226
|
+
assoc_name,
|
|
227
|
+
right_asset,
|
|
228
|
+
left_asset
|
|
229
|
+
)
|
|
230
|
+
if not full_name in assoc_entry['definitions']:
|
|
231
|
+
if not full_name_flipped in assoc_entry['definitions']:
|
|
232
|
+
raise LookupError(
|
|
233
|
+
'Failed to find "%s" or "%s" association in the '
|
|
234
|
+
'language json schema.'
|
|
235
|
+
% (full_name,
|
|
236
|
+
full_name_flipped)
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
return full_name_flipped
|
|
240
|
+
else:
|
|
241
|
+
return full_name
|
|
242
|
+
else:
|
|
243
|
+
return assoc_name
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
|
+
# mypy: ignore-errors
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional
|
|
2
6
|
|
|
3
7
|
from antlr4 import FileStream, CommonTokenStream
|
|
4
8
|
from .mal_lexer import malLexer
|
|
5
9
|
from .mal_parser import malParser
|
|
6
10
|
from .mal_visitor import malVisitor
|
|
7
11
|
|
|
8
|
-
import sys
|
|
9
|
-
import os
|
|
10
|
-
import json
|
|
11
|
-
|
|
12
12
|
|
|
13
13
|
class MalCompiler:
|
|
14
14
|
def __init__(self):
|
|
15
15
|
self.path = None
|
|
16
16
|
self.current_file = None
|
|
17
17
|
|
|
18
|
-
def compile(self, malfile: str = None):
|
|
18
|
+
def compile(self, malfile: Optional[str] = None):
|
|
19
19
|
if not self.path:
|
|
20
20
|
self.path = os.path.dirname(malfile)
|
|
21
21
|
|
|
@@ -30,10 +30,3 @@ class MalCompiler:
|
|
|
30
30
|
tree = parser.mal()
|
|
31
31
|
|
|
32
32
|
return malVisitor(compiler=self).visit(tree)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if __name__ == "__main__":
|
|
36
|
-
compiler = MalCompiler()
|
|
37
|
-
|
|
38
|
-
with open("new_langspec.json", "w") as f:
|
|
39
|
-
json.dump(compiler.compile(sys.argv[1]), f, indent=2)
|