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.
Files changed (37) hide show
  1. {mal_toolbox-0.0.27.dist-info → mal_toolbox-0.1.12.dist-info}/METADATA +60 -28
  2. mal_toolbox-0.1.12.dist-info/RECORD +32 -0
  3. {mal_toolbox-0.0.27.dist-info → mal_toolbox-0.1.12.dist-info}/WHEEL +1 -1
  4. maltoolbox/__init__.py +31 -31
  5. maltoolbox/__main__.py +80 -4
  6. maltoolbox/attackgraph/__init__.py +8 -0
  7. maltoolbox/attackgraph/analyzers/__init__.py +0 -0
  8. maltoolbox/attackgraph/analyzers/apriori.py +173 -27
  9. maltoolbox/attackgraph/attacker.py +99 -21
  10. maltoolbox/attackgraph/attackgraph.py +507 -217
  11. maltoolbox/attackgraph/node.py +143 -21
  12. maltoolbox/attackgraph/query.py +128 -26
  13. maltoolbox/default.conf +8 -7
  14. maltoolbox/exceptions.py +45 -0
  15. maltoolbox/file_utils.py +66 -0
  16. maltoolbox/ingestors/__init__.py +0 -0
  17. maltoolbox/ingestors/neo4j.py +95 -84
  18. maltoolbox/language/__init__.py +4 -0
  19. maltoolbox/language/classes_factory.py +145 -64
  20. maltoolbox/language/{lexer_parser/__main__.py → compiler/__init__.py} +5 -12
  21. maltoolbox/language/{lexer_parser → compiler}/mal_lexer.py +1 -1
  22. maltoolbox/language/{lexer_parser → compiler}/mal_parser.py +1 -1
  23. maltoolbox/language/{lexer_parser → compiler}/mal_visitor.py +4 -5
  24. maltoolbox/language/languagegraph.py +569 -168
  25. maltoolbox/model.py +858 -0
  26. maltoolbox/translators/__init__.py +0 -0
  27. maltoolbox/translators/securicad.py +76 -52
  28. maltoolbox/translators/updater.py +132 -0
  29. maltoolbox/wrappers.py +62 -0
  30. mal_toolbox-0.0.27.dist-info/RECORD +0 -26
  31. maltoolbox/cl_parser.py +0 -89
  32. maltoolbox/language/specification.py +0 -265
  33. maltoolbox/main.py +0 -84
  34. maltoolbox/model/model.py +0 -279
  35. {mal_toolbox-0.0.27.dist-info → mal_toolbox-0.1.12.dist-info}/AUTHORS +0 -0
  36. {mal_toolbox-0.0.27.dist-info → mal_toolbox-0.1.12.dist-info}/LICENSE +0 -0
  37. {mal_toolbox-0.0.27.dist-info → mal_toolbox-0.1.12.dist-info}/top_level.txt +0 -0
@@ -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 maltoolbox.model import model
10
- from maltoolbox.language import specification
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 = node_dict['id'],
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.metaconcept),
95
+ nodes[str(asset.id)] = Node(str(asset.type),
97
96
  name=str(asset.name),
98
97
  asset_id=str(asset.id),
99
- metaconcept=str(asset.metaconcept))
98
+ type=str(asset.type))
100
99
 
101
100
  for assoc in model.associations:
102
- firstElementName = list(vars(assoc)['_properties'])[0]
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(uri,
124
- username,
125
- password,
126
- dbname,
127
- lang_spec,
128
- lang_classes_factory
129
- ) -> model.Model:
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 = model.Model('Neo4j imported model', lang_spec,
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.metaconcept IS NOT NULL RETURN DISTINCT a').data()
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('Loading asset from Neo4j instance:\n' \
140
- + str(asset_data))
141
- if asset_data['metaconcept'] == 'Attacker':
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 = model.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
- asset_data['metaconcept']):
150
- logger.error(f'Failed to find {asset_data["metaconcept"]} '
151
- 'asset in language specification!')
152
- return None
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['metaconcept'])(name = asset_data['name'])
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.metaconcept IS NOT NULL RETURN DISTINCT a, r1, r2, b').data()
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(f'Load association '
171
- f'(\"{left_field}\",'
172
- f'\"{right_field}\",'
173
- f'\"{left_asset["metaconcept"]}\",'
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
- logger.error(f'Failed to find attacker with id {attacker_id} '
193
- 'in model!')
194
- return None
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
- logger.error(f'Failed to find asset with id {target_id} '
198
- 'in model!')
199
- return None
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 not left_asset:
206
- logger.error(f'Failed to find asset with id {left_id} '
207
- 'in model!')
208
- return None
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 not right_asset:
211
- logger.error(f'Failed to find asset with id {right_id} '
212
- 'in model!')
213
- return None
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
- assoc_name = specification.get_association_by_fields_and_assets(
216
- lang_spec,
213
+ assoc = lang_graph.get_association_by_fields_and_assets(
217
214
  left_field,
218
215
  right_field,
219
- left_asset.metaconcept,
220
- right_asset.metaconcept)
221
- logger.debug(f'Found \"{assoc_name}\" association.')
222
-
223
- if not assoc_name:
224
- logger.error(f'Failed to find '
225
- f'(\"{left_asset.metaconcept}\",'
226
- f'\"{right_asset.metaconcept}\",'
227
- f'\"{left_field}\",'
228
- f'\"{right_field}\") '
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
- if not hasattr(lang_classes_factory.ns,
233
- assoc_name):
234
- logger.error(f'Failed to find {assoc_name} '
235
- 'association in language specification!')
236
- return None
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.add_association(assoc)
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
-
@@ -0,0 +1,4 @@
1
+ """Contains tools to process MAL languages"""
2
+
3
+ from .languagegraph import LanguageGraph
4
+ from .classes_factory import LanguageClassesFactory
@@ -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, langspec):
13
- self.langspec = langspec
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 generate_assets(self):
27
+ def _generate_assets(self) -> None:
17
28
  """
18
- Generate JSON Schema for the assets in the language specification.
29
+ Generate JSON Schema for asset types in the language specification.
19
30
  """
20
- for asset in self.langspec['assets']:
21
- logger.debug(f'Creating {asset["name"]} asset JSON '\
22
- 'schema entry.')
23
- asset_json_entry = {'title': asset['name'], 'type': 'object',
24
- 'properties': {}}
25
- asset_json_entry['properties']['metaconcept'] = \
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['name']
44
+ 'default': asset.name
29
45
  }
30
- if asset['superAsset']:
31
- asset_json_entry['allOf'] = [{
32
- '$ref': '#/definitions/LanguageAsset/definitions/' + asset['superAsset']
33
- }]
34
- for defense in filter(lambda item: item['type'] == 'defense',
35
- asset['attackSteps']):
36
- if defense['ttc'] and defense['ttc']['name'] == 'Enabled':
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['name']] = \
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['name']] = asset_json_entry
49
- self.json_schema['definitions']['LanguageAsset']['oneOf'].\
50
- append({'$ref': '#/definitions/LanguageAsset/definitions/'
51
- + asset['name']})
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 generate_associations(self):
69
+ def _generate_associations(self) -> None:
54
70
  """
55
- Generate JSON Schema for the associations in the language specification.
71
+ Generate JSON Schema for association types in the language specification.
56
72
  """
57
- def create_association_entry(assoc):
58
- logger.debug(f'Creating {assoc["name"]} association JSON '\
59
- 'schema entry.')
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['name'] not in self.json_schema['definitions']\
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('Multiple associations with the same '\
71
- f'name, {assoc["name"]}, exist. '\
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['name']] =\
89
+ ['definitions'][assoc.name] =\
75
90
  {
76
- 'title': assoc['name'],
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['name']})
98
+ + assoc.name})
84
99
 
85
100
  assoc_json_subentry = create_association_entry(assoc)
86
- subentry_name = assoc['name'] + '_' + assoc['leftAsset'] + '_' \
87
- + assoc['rightAsset']
101
+ subentry_name = assoc.name + '_' + assoc.left_field.asset.name + '_' \
102
+ + assoc.right_field.asset.name
88
103
 
89
- logger.info(f'Creating {subentry_name} subentry association.')
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['name']]['definitions'][subentry_name] = assoc_json_subentry
107
+ ['definitions'][assoc.name]['definitions'][subentry_name] = assoc_json_subentry
93
108
  self.json_schema['definitions']['LanguageAssociation']\
94
- ['definitions'][assoc['name']]['oneOf'].append(
109
+ ['definitions'][assoc.name]['oneOf'].append(
95
110
  {'$ref': '#/definitions/LanguageAssociation/definitions/' \
96
- + assoc['name'] + '/definitions/' + subentry_name})
97
-
98
- def create_association_field(assoc, assoc_json_entry, position):
99
- assoc_json_entry['properties'][assoc[position + 'Field']] = \
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
- assoc[position + 'Asset']
126
+ field.asset.name
107
127
  }
108
128
  }
109
- if assoc[position + 'Multiplicity']['max']:
110
- assoc_json_entry['properties'][assoc[position + 'Field']]\
111
- ['maxItems'] = assoc[position + 'Multiplicity']['max']
129
+ if field.maximum:
130
+ assoc_json_entry['properties'][field.fieldname]\
131
+ ['maxItems'] = field.maximum
112
132
 
113
133
 
114
- for assoc in self.langspec['associations']:
115
- count = len(list(filter(lambda temp_assoc: temp_assoc['name'] ==
116
- assoc['name'], self.langspec['associations'])))
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
- assoc_json_entry = create_association_with_subentries(assoc)
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['name']] = assoc_json_entry
145
+ ['definitions'][assoc.name] = assoc_json_entry
126
146
  self.json_schema['definitions']['LanguageAssociation']['oneOf'].\
127
147
  append({'$ref': '#/definitions/LanguageAssociation/' +
128
- 'definitions/' + assoc['name']})
148
+ 'definitions/' + assoc.name})
129
149
 
130
- def create_classes(self):
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.generate_assets()
157
- self.generate_associations()
158
- logger.debug(json.dumps(self.json_schema, indent = 2))
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)
@@ -1,6 +1,6 @@
1
+ # mypy: ignore-errors
1
2
  # Generated from mal.g4 by ANTLR 4.13.1
2
3
  from antlr4 import *
3
- from io import StringIO
4
4
  import sys
5
5
  if sys.version_info[1] > 5:
6
6
  from typing import TextIO
@@ -1,7 +1,7 @@
1
+ # mypy: ignore-errors
1
2
  # Generated from mal.g4 by ANTLR 4.13.1
2
3
  # encoding: utf-8
3
4
  from antlr4 import *
4
- from io import StringIO
5
5
  import sys
6
6
  if sys.version_info[1] > 5:
7
7
  from typing import TextIO