mal-toolbox 1.0.1__py3-none-any.whl → 1.0.3__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mal-toolbox
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: A collection of tools used to create MAL models and attack graphs.
5
5
  Author-email: Andrei Buhaiu <buhaiu@kth.se>, Joakim Loxdal <loxdal@kth.se>, Nikolaos Kakouros <nkak@kth.se>, Jakob Nyberg <jaknyb@kth.se>, Giuseppe Nebbione <nebbione@kth.se>
6
6
  License: Apache Software License
@@ -18,6 +18,7 @@ Requires-Python: >=3.10
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  License-File: AUTHORS
21
+ Requires-Dist: graphviz
21
22
  Requires-Dist: antlr4-tools
22
23
  Requires-Dist: antlr4-python3-runtime
23
24
  Requires-Dist: docopt
@@ -86,13 +87,26 @@ pip install mal-toolbox
86
87
 
87
88
  ## Configuration
88
89
  You can use a `maltoolbox.yml` file in the current working directory to
89
- configure the toolbox. Alternatively, you can use the `MALTOOLBOX_CONFIG`
90
- environment variable to set a custom config file location:
90
+ configure the toolbox.
91
+
92
+ The config should look like this:
93
+ ```yml
94
+ logging:
95
+ log_level: INFO
96
+ log_file: "logs/log.txt"
97
+ attackgraph_file: "logs/attackgraph.json"
98
+ model_file: "logs/model.yml"
99
+ langspec_file: "logs/langspec_file.yml"
100
+ langgraph_file: "logs/langspec_file.yml"
101
+ ```
102
+
103
+ Alternatively, you can use the `MALTOOLBOX_CONFIG`
104
+ environment variable to set a custom config file location.
91
105
 
92
- """bash
106
+ ```bash
93
107
  # in your shell, e.g. bash do:
94
108
  export MALTOOLBOX_CONFIG=path/to/yml/config/file
95
- """
109
+ ```
96
110
 
97
111
  The default configuration can be found here:
98
112
 
@@ -1,26 +1,29 @@
1
- mal_toolbox-1.0.1.dist-info/licenses/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
2
- mal_toolbox-1.0.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
3
- maltoolbox/__init__.py,sha256=AO8CtBtHZ2gaI_kkyQW51Wn44s9xb0ErllNbR_AB0Do,2043
4
- maltoolbox/__main__.py,sha256=QBloKCJ_RMsFPZ8qiWZQnoP2gnnRyECIJBfA1zTAYJM,2394
1
+ mal_toolbox-1.0.3.dist-info/licenses/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
2
+ mal_toolbox-1.0.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
3
+ maltoolbox/__init__.py,sha256=ThfOuukbgjz1agfeDM8wvVb0jSZmy1OlTc2oVBB5JaM,2043
4
+ maltoolbox/__main__.py,sha256=A9jsYy94l1grHeSR3G3Ddn8Xg_nOihMSj1zZtVnYPSI,2974
5
5
  maltoolbox/exceptions.py,sha256=0YjPx2v1yYumZ2o7pVZ1s_jS-GAb3Ng979KEFhROSNY,1399
6
6
  maltoolbox/file_utils.py,sha256=fYG3UsvPQcU0ES_WI3nLfuzSZgc0jtE4IAxdMGgs9aA,1876
7
7
  maltoolbox/model.py,sha256=xTK2jUr0Gz5pPVhdjh78zO5G46nV8N2ciIt6M5SwcGU,16058
8
8
  maltoolbox/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  maltoolbox/attackgraph/__init__.py,sha256=m_81AjzwXONdclcW_R7mF2f8p-4DvoSRVfQ3Nyh7fak,298
10
- maltoolbox/attackgraph/attackgraph.py,sha256=fuhpfD3YnQEdDdG7QPdYSylOX3XPL2mbx4cX0_bmX3c,26870
10
+ maltoolbox/attackgraph/attackgraph.py,sha256=I2jms_X1rRLkmvkmc0z3zQAa_TnO4xA7ulPliRl5kOo,26930
11
11
  maltoolbox/attackgraph/node.py,sha256=Z2sdzXhPel9h7ySxP9fjgd1exVmpRbvRySVtLpI1_BM,3904
12
12
  maltoolbox/attackgraph/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- maltoolbox/ingestors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
13
  maltoolbox/language/__init__.py,sha256=TsTTryEyjChwHN1o5F2BSUlFsAss2N6J0H0-nzvXiD8,489
15
- maltoolbox/language/languagegraph.py,sha256=WLV0-CvfrB4yqaDW3NTLyVOVDduNeD1By3Yqmjzn00Y,73810
14
+ maltoolbox/language/languagegraph.py,sha256=1GMLYov2alHRLl4w9-o3-4Fe4d2FPRLr4qyXTot6mys,74428
16
15
  maltoolbox/language/compiler/__init__.py,sha256=JQyAgDwJh1pU7AmuOhd1-d2b2PYXpgMVPtxnav8QHVc,15872
17
16
  maltoolbox/language/compiler/mal_lexer.py,sha256=BeifykDAt4PloRASOaLzBgWF35ev_zgD8lXMIsSHykc,12063
18
17
  maltoolbox/language/compiler/mal_parser.py,sha256=sUoaE43l2VKg-Dou30mk2wlVS1FvdOREwHNIyFe4IkY,114699
18
+ maltoolbox/patternfinder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ maltoolbox/patternfinder/attackgraph_patterns.py,sha256=jgW7UG1yBJ08jvuokdpSiHm7jDMg2PTcjfGXZ8UJAnw,5082
19
20
  maltoolbox/translators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
21
  maltoolbox/translators/securicad.py,sha256=F_rndv2JyKxfHAXPwf2RrdiFPnemJVArYUpVsFP6QQk,6997
21
22
  maltoolbox/translators/updater.py,sha256=UZPnx22udROiocCcSmtrgUJUupkjktkxl-M7rhBxUPc,8660
22
- mal_toolbox-1.0.1.dist-info/METADATA,sha256=LSJ7hnJewGb32qVSdpv-3KBinzJ_ZHUbsWxWhHEebII,5097
23
- mal_toolbox-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- mal_toolbox-1.0.1.dist-info/entry_points.txt,sha256=oqby5O6cUP_OHCm70k_iYPA6UlbTBf7se1i3XwdK3uU,56
25
- mal_toolbox-1.0.1.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
26
- mal_toolbox-1.0.1.dist-info/RECORD,,
23
+ maltoolbox/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ maltoolbox/visualization/graphviz_utils.py,sha256=dfQhPL6Z2hvlMFpThsDr-5tm4Pa22SGHEiXw5ym9JJc,3906
25
+ mal_toolbox-1.0.3.dist-info/METADATA,sha256=fIwAnRYoYzKPvs7zJF0UOzmuJtUQBXh974N-zAR3BTY,5382
26
+ mal_toolbox-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ mal_toolbox-1.0.3.dist-info/entry_points.txt,sha256=oqby5O6cUP_OHCm70k_iYPA6UlbTBf7se1i3XwdK3uU,56
28
+ mal_toolbox-1.0.3.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
29
+ mal_toolbox-1.0.3.dist-info/RECORD,,
maltoolbox/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # MAL Toolbox v1.0.1
2
+ # MAL Toolbox v1.0.3
3
3
  # Copyright 2025, Andrei Buhaiu.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,7 +21,7 @@ MAL-Toolbox Framework
21
21
  """
22
22
 
23
23
  __title__ = "maltoolbox"
24
- __version__ = "1.0.1"
24
+ __version__ = "1.0.3"
25
25
  __authors__ = [
26
26
  "Andrei Buhaiu",
27
27
  "Giuseppe Nebbione",
maltoolbox/__main__.py CHANGED
@@ -2,15 +2,20 @@
2
2
  Command-line interface for MAL toolbox operations
3
3
 
4
4
  Usage:
5
- maltoolbox attack-graph generate [options] <model_file> <lang_file>
6
5
  maltoolbox compile <lang_file> <output_file>
6
+ maltoolbox generate-attack-graph [--graphviz] <model_file> <lang_file>
7
7
  maltoolbox upgrade-model <model_file> <lang_file> <output_file>
8
+ maltoolbox visualize-model <model_file> <lang_file>
8
9
 
9
10
  Arguments:
10
11
  <model_file> Path to JSON instance model file.
11
12
  <lang_file> Path to .mar or .mal file containing MAL spec.
12
13
  <output_file> Path to write the result of the compilation (yml/json).
13
14
 
15
+ Options:
16
+ -h --help Show this screen.
17
+ -g --graphviz Visualize with graphviz
18
+
14
19
  Notes:
15
20
  - <lang_file> can be either a .mar file (generated by the older MAL
16
21
  compiler) or a .mal file containing the DSL written in MAL.
@@ -21,17 +26,19 @@ import json
21
26
  import docopt
22
27
 
23
28
  from . import log_configs
24
- from .attackgraph import create_attack_graph
29
+ from .attackgraph import create_attack_graph, AttackGraph
25
30
  from .language.compiler import MalCompiler
26
31
  from .language.languagegraph import LanguageGraph
27
32
  from .translators.updater import load_model_from_older_version
33
+ from .visualization.graphviz_utils import render_model, render_attack_graph
34
+ from .model import Model
28
35
 
29
36
  logger = logging.getLogger(__name__)
30
37
 
31
38
  def generate_attack_graph(
32
39
  model_file: str,
33
- lang_file: str,
34
- ) -> None:
40
+ lang_file: str
41
+ ) -> AttackGraph:
35
42
  """Create an attack graph
36
43
 
37
44
  Args:
@@ -43,6 +50,7 @@ def generate_attack_graph(
43
50
  attack_graph.save_to_file(
44
51
  log_configs['attackgraph_file']
45
52
  )
53
+ return attack_graph
46
54
 
47
55
  def compile(lang_file: str, output_file: str) -> None:
48
56
  """Compile language and dump into output file"""
@@ -64,10 +72,13 @@ def upgrade_model(model_file: str, lang_file: str, output_file: str):
64
72
  def main():
65
73
  args = docopt.docopt(__doc__)
66
74
 
67
- if args['attack-graph'] and args['generate']:
68
- generate_attack_graph(
75
+ if args['generate-attack-graph']:
76
+ attack_graph = generate_attack_graph(
69
77
  args['<model_file>'], args['<lang_file>']
70
78
  )
79
+ if args['--graphviz']:
80
+ render_attack_graph(attack_graph)
81
+
71
82
  elif args['compile']:
72
83
  compile(
73
84
  args['<lang_file>'], args['<output_file>']
@@ -76,6 +87,10 @@ def main():
76
87
  upgrade_model(
77
88
  args['<model_file>'], args['<lang_file>'], args['<output_file>']
78
89
  )
90
+ elif args['visualize-model']:
91
+ lang_graph = LanguageGraph.load_from_file(args['<lang_file>'])
92
+ model = Model.load_from_file(args['<model_file>'], lang_graph)
93
+ render_model(model)
79
94
 
80
95
  if __name__ == "__main__":
81
96
  main()
@@ -89,7 +89,7 @@ def create_attack_graph(
89
89
 
90
90
  class AttackGraph():
91
91
  """Graph representation of attack steps"""
92
- def __init__(self, lang_graph, model: Optional[Model] = None):
92
+ def __init__(self, lang_graph: LanguageGraph, model: Optional[Model] = None):
93
93
  self.nodes: dict[int, AttackGraphNode] = {}
94
94
  # Dictionaries used in optimization to get nodes by id or full name
95
95
  # faster
@@ -194,7 +194,7 @@ class AttackGraph():
194
194
  ttc_dist = node_dict['ttc'],
195
195
  existence_status = node_dict.get('existence_status', None)
196
196
  )
197
- ag_node.tags = set(node_dict.get('tags', []))
197
+ ag_node.tags = list(node_dict.get('tags', []))
198
198
  ag_node.extras = node_dict.get('extras', {})
199
199
 
200
200
  if node_asset:
@@ -535,15 +535,15 @@ class AttackGraph():
535
535
  if not ag_node.model_asset:
536
536
  raise AttackGraphException('Attack graph node is missing '
537
537
  'asset link')
538
- lang_graph_asset = self.lang_graph.assets[
539
- ag_node.model_asset.type]
540
538
 
541
- lang_graph_attack_step = lang_graph_asset.attack_steps[
542
- ag_node.name]
539
+ lang_graph_asset = self.lang_graph.assets[ag_node.model_asset.type]
540
+ lang_graph_attack_step: Optional[LanguageGraphAttackStep] = (
541
+ lang_graph_asset.attack_steps[ag_node.name]
542
+ )
543
543
 
544
544
  while lang_graph_attack_step:
545
- for child in lang_graph_attack_step.children.values():
546
- for target_attack_step, expr_chain in child:
545
+ for target_attack_step, expr_chains in lang_graph_attack_step.children.items():
546
+ for expr_chain in expr_chains:
547
547
  target_assets = self._follow_expr_chain(
548
548
  self.model,
549
549
  set([ag_node.model_asset]),
@@ -485,18 +485,56 @@ class LanguageGraphAttackStep:
485
485
  asset: LanguageGraphAsset
486
486
  ttc: Optional[dict] = field(default_factory = dict)
487
487
  overrides: bool = False
488
- children: dict = field(default_factory = dict)
489
- parents: dict = field(default_factory = dict)
488
+
489
+ own_children: dict[LanguageGraphAttackStep, list[ExpressionsChain | None]] = (
490
+ field(default_factory = dict)
491
+ )
492
+ own_parents: dict[LanguageGraphAttackStep, list[ExpressionsChain | None]] = (
493
+ field(default_factory = dict)
494
+ )
490
495
  info: dict = field(default_factory = dict)
491
496
  inherits: Optional[LanguageGraphAttackStep] = None
492
497
  own_requires: list[ExpressionsChain] = field(default_factory=list)
493
- tags: set = field(default_factory = set)
498
+ tags: list = field(default_factory = list)
494
499
  detectors: dict = field(default_factory = lambda: {})
495
500
 
496
501
 
497
502
  def __hash__(self):
498
503
  return hash(self.full_name)
499
504
 
505
+ @property
506
+ def children(self) -> dict[
507
+ LanguageGraphAttackStep, list[ExpressionsChain | None]
508
+ ]:
509
+ """
510
+ Get all (both own and inherited) children of a LanguageGraphAttackStep
511
+ """
512
+
513
+ all_children = dict(self.own_children)
514
+
515
+ if self.overrides:
516
+ # Override overrides the children
517
+ return all_children
518
+
519
+ if not self.inherits:
520
+ return all_children
521
+
522
+ for child_step, chains in self.inherits.children.items():
523
+ if child_step in all_children:
524
+ all_children[child_step] += [
525
+ chain for chain in chains
526
+ if chain not in all_children[child_step]
527
+ ]
528
+ else:
529
+ all_children[child_step] = chains
530
+
531
+ return all_children
532
+
533
+ @property
534
+ def parents(self) -> None:
535
+ raise NotImplementedError(
536
+ "Can not fetch parents of a LanguageGraphAttackStep"
537
+ )
500
538
 
501
539
  @property
502
540
  def full_name(self) -> str:
@@ -514,8 +552,8 @@ class LanguageGraphAttackStep:
514
552
  'type': self.type,
515
553
  'asset': self.asset.name,
516
554
  'ttc': self.ttc,
517
- 'children': {},
518
- 'parents': {},
555
+ 'own_children': {},
556
+ 'own_parents': {},
519
557
  'info': self.info,
520
558
  'overrides': self.overrides,
521
559
  'inherits': self.inherits.full_name if self.inherits else None,
@@ -524,25 +562,22 @@ class LanguageGraphAttackStep:
524
562
  self.detectors.items()},
525
563
  }
526
564
 
527
- for child in self.children:
528
- node_dict['children'][child] = []
529
- for (_, expr_chain) in self.children[child]:
530
- if expr_chain:
531
- node_dict['children'][child].append(
532
- expr_chain.to_dict())
565
+ for child, expr_chains in self.own_children.items():
566
+ node_dict['own_children'][child.full_name] = []
567
+ for chain in expr_chains:
568
+ if chain:
569
+ node_dict['own_children'][child.full_name].append(chain.to_dict())
533
570
  else:
534
- node_dict['children'][child].append(None)
535
-
536
- for parent in self.parents:
537
- node_dict['parents'][parent] = []
538
- for (_, expr_chain) in self.parents[parent]:
539
- if expr_chain:
540
- node_dict['parents'][parent].append(
541
- expr_chain.to_dict())
571
+ node_dict['own_children'][child.full_name].append(None)
572
+ for parent, expr_chains in self.own_children.items():
573
+ node_dict['own_parents'][parent.full_name] = []
574
+ for chain in expr_chains:
575
+ if chain:
576
+ node_dict['own_parents'][parent.full_name].append(chain.to_dict())
542
577
  else:
543
- node_dict['parents'][parent].append(None)
578
+ node_dict['own_parents'][parent.full_name].append(None)
544
579
 
545
- if hasattr(self, 'own_requires'):
580
+ if self.own_requires:
546
581
  node_dict['requires'] = []
547
582
  for requirement in self.own_requires:
548
583
  node_dict['requires'].append(requirement.to_dict())
@@ -965,10 +1000,10 @@ class LanguageGraph():
965
1000
  asset = asset,
966
1001
  ttc = get_ttc_distribution(attack_step_dict),
967
1002
  overrides = attack_step_dict['overrides'],
968
- children = {},
969
- parents = {},
1003
+ own_children = {},
1004
+ own_parents = {},
970
1005
  info = attack_step_dict['info'],
971
- tags = set(attack_step_dict['tags'])
1006
+ tags = list(attack_step_dict['tags'])
972
1007
  )
973
1008
  asset.attack_steps[attack_step_dict['name']] = \
974
1009
  attack_step_node
@@ -995,12 +1030,11 @@ class LanguageGraph():
995
1030
  asset = lang_graph.assets[asset_dict['name']]
996
1031
  for attack_step_dict in asset_dict['attack_steps'].values():
997
1032
  attack_step = asset.attack_steps[attack_step_dict['name']]
998
- for child_target in attack_step_dict['children'].items():
1033
+ for child_target in attack_step_dict['own_children'].items():
999
1034
  target_full_attack_step_name = child_target[0]
1000
1035
  expr_chains = child_target[1]
1001
1036
  target_asset_name, target_attack_step_name = \
1002
- disaggregate_attack_step_full_name(
1003
- target_full_attack_step_name)
1037
+ disaggregate_attack_step_full_name(target_full_attack_step_name)
1004
1038
  target_asset = lang_graph.assets[target_asset_name]
1005
1039
  target_attack_step = target_asset.attack_steps[
1006
1040
  target_attack_step_name]
@@ -1009,33 +1043,30 @@ class LanguageGraph():
1009
1043
  expr_chain_dict,
1010
1044
  lang_graph
1011
1045
  )
1012
- if target_attack_step.full_name in attack_step.children:
1013
- attack_step.children[target_attack_step.full_name].\
1014
- append((target_attack_step, expr_chain))
1046
+
1047
+ if target_attack_step in attack_step.own_children:
1048
+ attack_step.own_children[target_attack_step].append(expr_chain)
1015
1049
  else:
1016
- attack_step.children[target_attack_step.full_name] = \
1017
- [(target_attack_step, expr_chain)]
1050
+ attack_step.own_children[target_attack_step] = [expr_chain]
1018
1051
 
1019
- for parent_target in attack_step_dict['parents'].items():
1020
- target_full_attack_step_name = parent_target[0]
1021
- expr_chains = parent_target[1]
1052
+ for (target_step_full_name, expr_chains) in attack_step_dict['own_parents'].items():
1022
1053
  target_asset_name, target_attack_step_name = \
1023
1054
  disaggregate_attack_step_full_name(
1024
- target_full_attack_step_name)
1055
+ target_step_full_name
1056
+ )
1025
1057
  target_asset = lang_graph.assets[target_asset_name]
1026
- target_attack_step = target_asset.attack_steps[
1027
- target_attack_step_name]
1058
+ target_attack_step = target_asset.attack_steps[target_attack_step_name]
1028
1059
  for expr_chain_dict in expr_chains:
1029
1060
  expr_chain = ExpressionsChain._from_dict(
1030
- expr_chain_dict,
1031
- lang_graph
1061
+ expr_chain_dict, lang_graph
1032
1062
  )
1033
- if target_attack_step.full_name in attack_step.parents:
1034
- attack_step.parents[target_attack_step.full_name].\
1035
- append((target_attack_step, expr_chain))
1063
+
1064
+ if target_attack_step in attack_step.own_parents:
1065
+ attack_step.own_parents[target_attack_step].append(
1066
+ expr_chain
1067
+ )
1036
1068
  else:
1037
- attack_step.parents[target_attack_step.full_name] = \
1038
- [(target_attack_step, expr_chain)]
1069
+ attack_step.own_parents[target_attack_step] = [expr_chain]
1039
1070
 
1040
1071
  # Recreate the requirements of exist and notExist attack steps
1041
1072
  if attack_step.type == 'exist' or \
@@ -1625,6 +1656,7 @@ class LanguageGraph():
1625
1656
  """
1626
1657
  Link assets based on inheritance and associations.
1627
1658
  """
1659
+
1628
1660
  for asset_dict in lang_spec['assets']:
1629
1661
  asset = assets[asset_dict['name']]
1630
1662
  if asset_dict['superAsset']:
@@ -1689,10 +1721,10 @@ class LanguageGraph():
1689
1721
  attack_step_attribs['reaches']['overrides']
1690
1722
  if attack_step_attribs['reaches'] else False
1691
1723
  ),
1692
- children = {},
1693
- parents = {},
1724
+ own_children = {},
1725
+ own_parents = {},
1694
1726
  info = attack_step_attribs['meta'],
1695
- tags = set(attack_step_attribs['tags'])
1727
+ tags = list(attack_step_attribs['tags'])
1696
1728
  )
1697
1729
  langspec_dict[attack_step_node.full_name] = \
1698
1730
  attack_step_attribs
@@ -1732,10 +1764,10 @@ class LanguageGraph():
1732
1764
  asset = asset,
1733
1765
  ttc = attack_step.ttc,
1734
1766
  overrides = False,
1735
- children = {},
1736
- parents = {},
1767
+ own_children = {},
1768
+ own_parents = {},
1737
1769
  info = attack_step.info,
1738
- tags = set(attack_step.tags)
1770
+ tags = list(attack_step.tags)
1739
1771
  )
1740
1772
  attack_step_node.inherits = attack_step
1741
1773
  asset.attack_steps[attack_step.name] = attack_step_node
@@ -1743,12 +1775,9 @@ class LanguageGraph():
1743
1775
  # The inherited attack step was already overridden.
1744
1776
  continue
1745
1777
  else:
1746
- asset.attack_steps[attack_step.name].inherits = \
1747
- attack_step
1748
- asset.attack_steps[attack_step.name].tags |= \
1749
- attack_step.tags
1750
- asset.attack_steps[attack_step.name].info |= \
1751
- attack_step.info
1778
+ asset.attack_steps[attack_step.name].inherits = attack_step
1779
+ asset.attack_steps[attack_step.name].tags += attack_step.tags
1780
+ asset.attack_steps[attack_step.name].info |= attack_step.info
1752
1781
 
1753
1782
  # Then, link all of the attack step nodes according to their
1754
1783
  # associations.
@@ -1802,16 +1831,14 @@ class LanguageGraph():
1802
1831
  target_attack_step_name]
1803
1832
 
1804
1833
  # Link to the children target attack steps
1805
- attack_step.children.setdefault(target_attack_step.full_name, [])
1806
- attack_step.children[target_attack_step.full_name].append(
1807
- (target_attack_step, expr_chain)
1808
- )
1834
+ attack_step.own_children.setdefault(target_attack_step, [])
1835
+ attack_step.own_children[target_attack_step].append(expr_chain)
1809
1836
 
1810
1837
  # Reverse the children associations chains to get the
1811
1838
  # parents associations chain.
1812
- target_attack_step.parents.setdefault(attack_step.full_name, [])
1813
- target_attack_step.parents[attack_step.full_name].append(
1814
- (attack_step, self.reverse_expr_chain(expr_chain, None))
1839
+ target_attack_step.own_parents.setdefault(attack_step, [])
1840
+ target_attack_step.own_parents[attack_step].append(
1841
+ self.reverse_expr_chain(expr_chain, None)
1815
1842
  )
1816
1843
 
1817
1844
  # Evaluate the requirements of exist and notExist attack steps
@@ -0,0 +1,134 @@
1
+ """Utilities for finding patterns in the AttackGraph"""
2
+
3
+ from __future__ import annotations
4
+ from dataclasses import dataclass
5
+ from typing import Callable
6
+ from maltoolbox.attackgraph import AttackGraph, AttackGraphNode
7
+
8
+ class SearchPattern:
9
+ """A pattern consists of conditions, the conditions are used
10
+ to find all matching sequences of nodes in an AttackGraph."""
11
+ conditions: list[SearchCondition]
12
+
13
+ def __init__(self, conditions):
14
+ self.conditions = conditions
15
+
16
+ def find_matches(self, graph: AttackGraph):
17
+ """Search through a graph for a pattern using
18
+ its conditions, and return sequences of nodes
19
+ that match all the conditions in the pattern
20
+
21
+ Args:
22
+ graph - The AttackGraph to search in
23
+
24
+ Return: list[list[AttackGraphNode]] matching paths of Nodes
25
+ """
26
+
27
+ # Find the starting nodes which match the first condition
28
+ condition = self.conditions[0]
29
+ matching_paths = []
30
+ for node in graph.nodes.values():
31
+ if condition.matches(node):
32
+ matching_paths.extend(
33
+ find_matches_recursively(node, self.conditions)
34
+ )
35
+ return matching_paths
36
+
37
+ @dataclass
38
+ class SearchCondition:
39
+ """A condition that has to be true for a node to match"""
40
+
41
+ # Predefined search conditions
42
+ ANY = lambda _: True
43
+
44
+ # `matches` should be a lambda that takes node as input and returns bool
45
+ # If lamdba returns True for a node, the node matches
46
+ # If the lamdba returns False for a node, the node does not match
47
+ matches: Callable[[AttackGraphNode], bool]
48
+ greedy: bool = False
49
+
50
+ # It is possible to require/allow a Condition to repeat
51
+ min_repeated: int = 1
52
+ max_repeated: int = 1
53
+
54
+ def can_match_again(self, num_matches):
55
+ """Returns true if condition can be used again"""
56
+ return num_matches < self.max_repeated
57
+
58
+ def must_match_again(self, num_matches):
59
+ """Returns true if condition must match again to be fulfilled"""
60
+ return num_matches < self.min_repeated
61
+
62
+
63
+ def find_matches_recursively(
64
+ node: AttackGraphNode,
65
+ condition_list: list[SearchCondition],
66
+ current_path: list[AttackGraphNode] | None = None,
67
+ matching_paths: set[tuple[AttackGraphNode,...]] | None = None,
68
+ condition_match_count: int = 0
69
+ ):
70
+ """Find all paths of nodes that match the list of conditions.
71
+ When a sequence of conditions is fulfilled for a path of nodes,
72
+ add the path of nodes to the returned `matching_paths`
73
+ The function runs recursively down all paths of children nodes.
74
+
75
+ Args:
76
+ node - node to check if current `condition` matches for
77
+ condition_list - first condition in list will attempt match `node`
78
+ current_path - list of matched nodes so far (recursively built)
79
+ matching_paths - set of matched paths so far (recursively built)
80
+ condition_match_count - number of matches on current condition so far
81
+
82
+ Return: set of tuples (paths) of AttackGraphNodes that match the condition
83
+ """
84
+
85
+ # Init path lists if None, or copy/init into new lists for each iteration
86
+ current_path = [] if current_path is None else list(current_path)
87
+ matching_paths = set() if matching_paths is None else matching_paths
88
+
89
+ curr_cond, *next_conds = condition_list
90
+
91
+ if node in current_path:
92
+ # Stop the chain, infinite loop
93
+ return matching_paths
94
+
95
+ if next_conds and not curr_cond.must_match_again(condition_match_count):
96
+ # Try next condition for current node if there are more and
97
+ # current condition is already fulfilled.
98
+ matching_paths = find_matches_recursively(
99
+ node,
100
+ next_conds,
101
+ current_path=current_path,
102
+ matching_paths=matching_paths
103
+ )
104
+
105
+ if curr_cond.matches(node):
106
+ # Current node matches, add to current_path and increment match_count
107
+ current_path.append(node)
108
+ condition_match_count += 1
109
+
110
+ if next_conds:
111
+ # If there are more conditions, try next one for all children
112
+ for child in node.children:
113
+ matching_paths = find_matches_recursively(
114
+ child,
115
+ next_conds,
116
+ current_path=current_path,
117
+ matching_paths=matching_paths,
118
+ )
119
+ if curr_cond.can_match_again(condition_match_count):
120
+ # If we can match current condition again, try for all children
121
+ for child in node.children:
122
+ matching_paths = find_matches_recursively(
123
+ child,
124
+ [curr_cond] + next_conds,
125
+ current_path=current_path,
126
+ matching_paths=matching_paths,
127
+ condition_match_count=condition_match_count
128
+ )
129
+
130
+ if not next_conds:
131
+ # Congrats - matched a full unique search pattern!
132
+ matching_paths.add(tuple(current_path)) # tuple is hashable
133
+
134
+ return matching_paths
File without changes
@@ -0,0 +1,102 @@
1
+ import random
2
+ import graphviz
3
+
4
+ from ..model import Model
5
+ from ..attackgraph import AttackGraph
6
+
7
+ graphviz_bright_colors = [
8
+ "aliceblue", "antiquewhite", "antiquewhite1", "antiquewhite2", "azure", "azure1", "azure2",
9
+ "beige", "bisque", "bisque1", "bisque2", "blanchedalmond",
10
+ "cornsilk", "cornsilk1", "cornsilk2",
11
+ "floralwhite", "gainsboro", "ghostwhite", "gold", "gold1", "gold2",
12
+ "honeydew", "honeydew1", "honeydew2",
13
+ "ivory", "ivory1", "ivory2",
14
+ "khaki", "khaki1", "khaki2",
15
+ "lavender", "lavenderblush", "lavenderblush1", "lavenderblush2",
16
+ "lemonchiffon", "lemonchiffon1", "lemonchiffon2",
17
+ "lightblue", "lightblue1", "lightblue2",
18
+ "lightcyan", "lightcyan1", "lightcyan2",
19
+ "lightgoldenrod", "lightgoldenrod1", "lightgoldenrod2",
20
+ "lightgoldenrodyellow", "lightgray",
21
+ "lightpink", "lightpink1", "lightpink2",
22
+ "lightsalmon", "lightsalmon1", "lightsalmon2",
23
+ "lightskyblue", "lightskyblue1", "lightskyblue2",
24
+ "lightslategray", "lightsteelblue", "lightsteelblue1", "lightsteelblue2",
25
+ "lightyellow", "lightyellow1", "lightyellow2",
26
+ "linen", "mintcream", "mistyrose", "mistyrose1", "mistyrose2",
27
+ "moccasin", "navajowhite", "navajowhite1", "navajowhite2",
28
+ "oldlace", "papayawhip", "peachpuff", "peachpuff1", "peachpuff2",
29
+ "pink", "pink1", "pink2", "plum", "plum1", "plum2", "powderblue",
30
+ "seashell", "seashell1", "seashell2",
31
+ "snow", "snow1", "snow2",
32
+ "thistle", "thistle1", "thistle2",
33
+ "wheat", "wheat1", "wheat2",
34
+ "white", "whitesmoke", "yellow", "yellow1", "yellow2",
35
+ ]
36
+
37
+
38
+ def render_model(model: Model):
39
+ """Render a model in graphviz, create pdf and open it"""
40
+ dot = graphviz.Digraph(model.name)
41
+
42
+ # Create nodes
43
+ asset_type_colors: dict[str, str] = {}
44
+ for asset in model.assets.values():
45
+ bg_color = asset_type_colors.get(asset.lg_asset.name)
46
+ if not bg_color:
47
+ bg_color = random.choice(graphviz_bright_colors)
48
+ asset_type_colors[asset.lg_asset.name] = bg_color
49
+ dot.node(
50
+ str(asset.id), asset.name, style="filled", fillcolor=bg_color
51
+ )
52
+
53
+ # Create edges
54
+ for from_asset in model.assets.values():
55
+
56
+ for fieldname, to_assets in from_asset.associated_assets.items():
57
+ for to_asset in to_assets:
58
+ dot.edge(
59
+ str(from_asset.id), str(to_asset.id), label=fieldname
60
+ )
61
+ dot.render(directory='.', view=True)
62
+
63
+ def render_attack_graph(attack_graph: AttackGraph):
64
+ """Render attack graph graphviz, create pdf and open it"""
65
+ assert attack_graph.model, "Attack graph needs a model"
66
+ dot = graphviz.Graph(attack_graph.model.name)
67
+ dot.graph_attr['nodesep'] = '3.0' # Node separation
68
+ dot.graph_attr['ratio'] = 'compress'
69
+
70
+ # Create nodes
71
+ asset_colors: dict[str, str] = {}
72
+ for node in attack_graph.nodes.values():
73
+ assert node.model_asset, "Node needs model"
74
+ bg_color = asset_colors.get(node.model_asset.name)
75
+ if not bg_color:
76
+ bg_color = random.choice(graphviz_bright_colors)
77
+ asset_colors[node.model_asset.name] = bg_color
78
+ path_color = 'white'
79
+ match node.type:
80
+ case 'defense':
81
+ path_color = 'blue'
82
+ case 'or':
83
+ path_color = 'red'
84
+ case 'and':
85
+ path_color = 'red'
86
+ case 'exist':
87
+ path_color = 'grey'
88
+ case 'notExist':
89
+ path_color = 'grey'
90
+ case t:
91
+ raise ValueError(f'Type {t} not supported')
92
+
93
+ dot.node(
94
+ str(node.id), node.full_name, style="filled", color=path_color, fillcolor=bg_color
95
+ )
96
+
97
+ # Create edges
98
+ for parent in attack_graph.nodes.values():
99
+ for child in parent.children:
100
+ dot.edge(str(parent.id), str(child.id))
101
+
102
+ dot.render(directory='.', view=True)
File without changes