mal-toolbox 0.2.0__py3-none-any.whl → 0.3.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.
@@ -1,416 +0,0 @@
1
- # mypy: ignore-errors
2
- from collections.abc import MutableMapping, MutableSequence
3
-
4
- from antlr4 import ParseTreeVisitor
5
- from .mal_parser import malParser
6
-
7
- # In a rule like `rule: one? two* three`:
8
- # - ctx.one() would be None if the token was not found on a matching line
9
- # - ctx.two() would be []
10
-
11
- class malVisitor(ParseTreeVisitor):
12
- def __init__(self, compiler, *args, **kwargs):
13
- self.compiler = compiler
14
- self.current_file = compiler.current_file # for debug purposes
15
-
16
- super().__init__(*args, **kwargs)
17
-
18
- def visitMal(self, ctx):
19
- langspec = {
20
- "formatVersion": "1.0.0",
21
- "defines": {},
22
- "categories": [],
23
- "assets": [],
24
- "associations": [],
25
- }
26
-
27
- # no visitDeclaration method needed, `declaration` is a thin rule
28
- for declaration in (d.getChild(0) for d in ctx.declaration()):
29
- if result := self.visit(declaration) or True:
30
- key, value = result
31
-
32
- if key == "categories":
33
- category, assets = value
34
- langspec["categories"].extend(category)
35
- langspec["assets"].extend(assets)
36
- continue
37
-
38
- if key == "defines":
39
- langspec[key].update(value)
40
-
41
- if key == "associations":
42
- langspec[key].extend(value)
43
-
44
- if key == "include":
45
- included_file = self.compiler.compile(value)
46
- for k, v in langspec.items():
47
- if isinstance(v, MutableMapping):
48
- langspec[k].update(included_file.get(k, {}))
49
- if isinstance(v, MutableSequence) and k in included_file:
50
- langspec[k].extend(included_file[k])
51
-
52
- for key in ("categories", "assets", "associations"):
53
- unique = []
54
- for item in langspec[key]:
55
- if item not in unique:
56
- unique.append(item)
57
- langspec[key] = unique
58
-
59
- return langspec
60
-
61
- def visitInclude(self, ctx):
62
- return ("include", ctx.STRING().getText().strip('"'))
63
-
64
- def visitDefine(self, ctx):
65
- return ("defines", {ctx.ID().getText(): ctx.STRING().getText().strip('"')})
66
-
67
- def visitCategory(self, ctx):
68
- category = {}
69
- category["name"] = ctx.ID().getText()
70
- category["meta"] = {k: v for meta in ctx.meta() for k, v in self.visit(meta)}
71
-
72
- assets = [self.visit(asset) for asset in ctx.asset()]
73
-
74
- return ("categories", ([category], assets))
75
-
76
- def visitMeta(self, ctx):
77
- return ((ctx.ID().getText(), ctx.STRING().getText().strip('"')),)
78
-
79
- def visitAsset(self, ctx):
80
- asset = {}
81
- asset["name"] = ctx.ID()[0].getText()
82
- asset["meta"] = {k: v for meta in ctx.meta() for k, v in self.visit(meta)}
83
- asset["category"] = ctx.parentCtx.ID().getText()
84
- asset["isAbstract"] = ctx.ABSTRACT() is not None
85
-
86
- asset["superAsset"] = None
87
- if len(ctx.ID()) > 1 and ctx.ID()[1]:
88
- asset["superAsset"] = ctx.ID()[1].getText()
89
-
90
- asset["variables"] = [self.visit(variable) for variable in ctx.variable()]
91
- asset["attackSteps"] = [self.visit(step) for step in ctx.step()]
92
-
93
- return asset
94
-
95
- def visitStep(self, ctx):
96
- step = {}
97
- step["name"] = ctx.ID().getText()
98
- step["meta"] = {k: v for meta in ctx.meta() for k, v in self.visit(meta)}
99
- step["type"] = self.visit(ctx.steptype())
100
- step["tags"] = [self.visit(tag) for tag in ctx.tag()]
101
- step["risk"] = self.visit(ctx.cias()) if ctx.cias() else None
102
- step["ttc"] = self.visit(ctx.ttc()) if ctx.ttc() else None
103
- step["requires"] = (
104
- self.visit(ctx.precondition()) if ctx.precondition() else None
105
- )
106
- step["reaches"] = self.visit(ctx.reaches()) if ctx.reaches() else None
107
-
108
- return step
109
-
110
- def visitSteptype(self, ctx):
111
- return (
112
- "or"
113
- if ctx.OR()
114
- else "and"
115
- if ctx.AND()
116
- else "defense"
117
- if ctx.HASH()
118
- else "exist"
119
- if ctx.EXISTS()
120
- else "notExist"
121
- if ctx.NOTEXISTS()
122
- else None # should never happen, the grammar limits it
123
- )
124
-
125
- def visitTag(self, ctx):
126
- return ctx.ID().getText()
127
-
128
- def visitCias(self, ctx):
129
- risk = {
130
- "isConfidentiality": False,
131
- "isIntegrity": False,
132
- "isAvailability": False,
133
- }
134
-
135
- for cia in ctx.cia():
136
- risk.update(self.visit(cia))
137
-
138
- return risk
139
-
140
- def visitCia(self, ctx):
141
- key = (
142
- "isConfidentiality"
143
- if ctx.C()
144
- else "isIntegrity"
145
- if ctx.I()
146
- else "isAvailability"
147
- if ctx.A()
148
- else None
149
- )
150
-
151
- return {key: True}
152
-
153
- def visitTtc(self, ctx):
154
- ret = self.visit(ctx.ttcexpr())
155
-
156
- return ret
157
-
158
- def visitTtcexpr(self, ctx):
159
- if len(terms := ctx.ttcterm()) == 1:
160
- return self.visit(terms[0])
161
-
162
- ret = {}
163
-
164
- lhs = self.visit(terms[0])
165
- for i in range(1, len(terms)):
166
- ret["type"] = (
167
- "addition"
168
- if ctx.children[2 * i - 1].getText() == "+"
169
- else "subtraction"
170
- )
171
- ret["lhs"] = lhs
172
- ret["rhs"] = self.visit(terms[i])
173
-
174
- lhs = ret.copy()
175
-
176
- return ret
177
-
178
- def visitTtcterm(self, ctx):
179
- if len(factors := ctx.ttcfact()) == 1:
180
- ret = self.visit(factors[0])
181
- else:
182
- ret = {}
183
- ret["type"] = "multiplication" if ctx.STAR() else "division"
184
- ret["lhs"] = self.visit(factors[0])
185
- ret["rhs"] = self.visit(factors[1])
186
-
187
- return ret
188
-
189
- def visitTtcfact(self, ctx):
190
- if len(atoms := ctx.ttcatom()) == 1:
191
- ret = self.visit(atoms[0])
192
- else:
193
- ret = {}
194
- ret["type"] = "exponentiation"
195
- ret["lhs"] = self.visit(atoms[0])
196
- ret["rhs"] = self.visit(atoms[1])
197
-
198
- return ret
199
-
200
- def visitTtcatom(self, ctx):
201
- if ctx.ttcdist():
202
- ret = self.visit(ctx.ttcdist())
203
- elif ctx.ttcexpr():
204
- ret = self.visit(ctx.ttcexpr())
205
- elif ctx.number():
206
- ret = self.visit(ctx.number())
207
-
208
- return ret
209
-
210
- def visitTtcdist(self, ctx):
211
- ret = {"type": "function"}
212
- ret["name"] = ctx.ID().getText()
213
- ret["arguments"] = []
214
-
215
- if ctx.LPAREN():
216
- ret["arguments"] = [self.visit(number)["value"] for number in ctx.number()]
217
-
218
- return ret
219
-
220
- def visitPrecondition(self, ctx):
221
- ret = {}
222
- ret["overrides"] = True
223
- ret["stepExpressions"] = [self.visit(expr) for expr in ctx.expr()]
224
- return ret
225
-
226
- def visitReaches(self, ctx):
227
- ret = {}
228
- ret["overrides"] = ctx.INHERITS() is None
229
- ret["stepExpressions"] = [self.visit(expr) for expr in ctx.expr()]
230
-
231
- return ret
232
-
233
- def visitNumber(self, ctx):
234
- ret = {"type": "number"}
235
- ret["value"] = float(ctx.getText())
236
-
237
- return ret
238
-
239
- def visitVariable(self, ctx):
240
- ret = {}
241
- ret["name"] = ctx.ID().getText()
242
- ret["stepExpression"] = self.visit(ctx.expr())
243
-
244
- return ret
245
-
246
- def visitExpr(self, ctx):
247
- if len(ctx.parts()) == 1:
248
- return self.visit(ctx.parts()[0])
249
-
250
- ret = {}
251
- lhs = self.visit(ctx.parts()[0])
252
- for i in range(1, len(ctx.parts())):
253
- ret["type"] = self.visit(ctx.children[2 * i - 1])
254
- ret["lhs"] = lhs
255
- ret["rhs"] = self.visit(ctx.parts()[i])
256
- lhs = ret.copy()
257
-
258
- return ret
259
-
260
- def visitParts(self, ctx):
261
- if len(ctx.part()) == 1:
262
- return self.visit(ctx.part()[0])
263
-
264
- ret = {}
265
-
266
- lhs = self.visit(ctx.part()[0])
267
-
268
- for i in range(1, len(ctx.part())):
269
- ret["type"] = "collect"
270
- ret["lhs"] = lhs
271
- ret["rhs"] = self.visit(ctx.part()[i])
272
-
273
- lhs = ret.copy()
274
-
275
- return ret
276
-
277
- def visitPart(self, ctx):
278
- ret = {}
279
- if ctx.varsubst():
280
- ret["type"] = "variable"
281
- ret["name"] = self.visit(ctx.varsubst())
282
- elif ctx.LPAREN():
283
- ret = self.visit(ctx.expr())
284
- else: # ctx.ID()
285
- # Resolve type: field or attackStep?
286
- ret["type"] = self._resolve_part_ID_type(ctx)
287
-
288
- ret["name"] = ctx.ID().getText()
289
-
290
- if ctx.STAR():
291
- ret = {"type": "transitive", "stepExpression": ret}
292
-
293
- for type_ in ctx.type_(): # mind the trailing underscore
294
- ret = {
295
- "type": "subType",
296
- "subType": self.visit(type_),
297
- "stepExpression": ret,
298
- }
299
-
300
- return ret
301
-
302
- def _resolve_part_ID_type(self, ctx):
303
- pctx = ctx.parentCtx
304
-
305
- # Traverse up the tree until we find the parent of the topmost expr
306
- # (saying "topmost" as expr can be nested) or the root of the tree.
307
- while pctx and not isinstance(
308
- pctx,
309
- malParser.ReachesContext
310
- # Expressions are also valid in `let` variable assignments, but
311
- # there every lexical component of expr is considered a "field",
312
- # no need to resolve the type in that case. Similarly, preconditions
313
- # (`<-`) only accept fields.
314
- ):
315
- pctx = pctx.parentCtx
316
-
317
- if pctx is None:
318
- # ctx (the `part`) belongs to a "let" assignment or a precondition.
319
- return "field"
320
-
321
- # scan for a dot to the right of `ctx`
322
- file_tokens = ctx.parser.getTokenStream().tokens
323
- for i in range(ctx.start.tokenIndex, pctx.stop.tokenIndex + 1):
324
- if file_tokens[i].type == malParser.DOT:
325
- return "field"
326
-
327
- # We are looping until the end of pctx (which is a `reaches` or
328
- # `precondition` context). This could include multiple comma
329
- # separated `expr`s, we only care for the current one.
330
- if file_tokens[i].type == malParser.COMMA: # end of current `expr`
331
- return "attackStep"
332
-
333
- return "attackStep"
334
-
335
- def visitVarsubst(self, ctx):
336
- return ctx.ID().getText()
337
-
338
- def visitType(self, ctx):
339
- return ctx.ID().getText()
340
-
341
- def visitSetop(self, ctx):
342
- return (
343
- "union"
344
- if ctx.UNION()
345
- else "intersection"
346
- if ctx.INTERSECT()
347
- else "difference"
348
- if ctx.INTERSECT
349
- else None
350
- )
351
-
352
- def visitAssociations(self, ctx):
353
- associations = []
354
- for assoc in ctx.association():
355
- associations.append(self.visit(assoc))
356
-
357
- return ("associations", associations)
358
-
359
- def visitAssociation(self, ctx):
360
- association = {}
361
- association["name"] = self.visit(ctx.linkname())
362
- association["meta"] = {k: v for meta in ctx.meta() for k, v in self.visit(meta)}
363
- association["leftAsset"] = ctx.ID()[0].getText()
364
- association["leftField"] = self.visit(ctx.field()[0])
365
-
366
- # no self.visitMult or self.visitMultatom methods, reading them here
367
- # directly
368
- association["leftMultiplicity"] = {
369
- "min": (multatoms := ctx.mult()[0].multatom()).pop(0).getText(),
370
- "max": multatoms.pop().getText() if multatoms else None,
371
- }
372
- association["rightAsset"] = ctx.ID()[1].getText()
373
- association["rightField"] = self.visit(ctx.field()[1])
374
- association["rightMultiplicity"] = {
375
- "min": (multatoms := ctx.mult()[1].multatom()).pop(0).getText(),
376
- "max": multatoms.pop().getText() if multatoms else None,
377
- }
378
-
379
- self._post_process_multitudes(association)
380
- return association
381
-
382
- def _post_process_multitudes(self, association):
383
- mult_keys = [
384
- # start the multatoms from right to left to make sure the rules
385
- # below get applied cleanly
386
- "rightMultiplicity.max",
387
- "rightMultiplicity.min",
388
- "leftMultiplicity.max",
389
- "leftMultiplicity.min",
390
- ]
391
-
392
- for mult_key in mult_keys:
393
- key, subkey = mult_key.split(".")
394
-
395
- # upper limit equals lower limit if not given
396
- if subkey == "max" and association[key][subkey] is None:
397
- association[key][subkey] = association[key]["min"]
398
-
399
- if association[key][subkey] == "*":
400
- # 'any' as lower limit means start from 0
401
- if subkey == "min":
402
- association[key][subkey] = 0
403
-
404
- # 'any' as upper limit means not limit
405
- else:
406
- association[key][subkey] = None
407
-
408
- # cast numerical strings to integers
409
- if (multatom := association[key][subkey]) and multatom.isdigit():
410
- association[key][subkey] = int(association[key][subkey])
411
-
412
- def visitField(self, ctx):
413
- return ctx.ID().getText()
414
-
415
- def visitLinkname(self, ctx):
416
- return ctx.ID().getText()
maltoolbox/wrappers.py DELETED
@@ -1,62 +0,0 @@
1
- """Contains wrappers combining more than one of the maltoolbox submodules"""
2
-
3
- import logging
4
- import sys
5
- import zipfile
6
-
7
- from maltoolbox.model import Model
8
- from maltoolbox.language import LanguageGraph, LanguageClassesFactory
9
- from maltoolbox.attackgraph import AttackGraph
10
- from maltoolbox.attackgraph.analyzers.apriori import (
11
- calculate_viability_and_necessity
12
- )
13
- from maltoolbox.exceptions import AttackGraphStepExpressionError
14
- from maltoolbox import log_configs
15
-
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
- def create_attack_graph(
20
- lang_file: str,
21
- model_file: str,
22
- attach_attackers=True,
23
- calc_viability_and_necessity=True
24
- ) -> AttackGraph:
25
- """Create and return an attack graph
26
-
27
- Args:
28
- lang_file - path to language file (.mar or .mal)
29
- model_file - path to model file (yaml or json)
30
- attach_attackers - whether to run attach_attackers or not
31
- calc_viability_and_necessity - whether run apriori calculations or not
32
- """
33
- try:
34
- lang_graph = LanguageGraph.from_mar_archive(lang_file)
35
- except zipfile.BadZipFile:
36
- lang_graph = LanguageGraph.from_mal_spec(lang_file)
37
-
38
- if log_configs['langspec_file']:
39
- lang_graph.save_to_file(log_configs['langspec_file'])
40
-
41
- lang_classes_factory = LanguageClassesFactory(lang_graph)
42
- instance_model = Model.load_from_file(model_file, lang_classes_factory)
43
-
44
- if log_configs['model_file']:
45
- instance_model.save_to_file(log_configs['model_file'])
46
-
47
- try:
48
- attack_graph = AttackGraph(lang_graph, instance_model)
49
- except AttackGraphStepExpressionError:
50
- logger.error(
51
- 'Attack graph generation failed when attempting '
52
- 'to resolve attack step expression!'
53
- )
54
- sys.exit(1)
55
-
56
- if attach_attackers:
57
- attack_graph.attach_attackers()
58
-
59
- if calc_viability_and_necessity:
60
- calculate_viability_and_necessity(attack_graph)
61
-
62
- return attack_graph