mal-toolbox 1.0.2__tar.gz → 1.0.3__tar.gz

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 (35) hide show
  1. {mal_toolbox-1.0.2/mal_toolbox.egg-info → mal_toolbox-1.0.3}/PKG-INFO +2 -1
  2. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3/mal_toolbox.egg-info}/PKG-INFO +2 -1
  3. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/mal_toolbox.egg-info/SOURCES.txt +4 -1
  4. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/mal_toolbox.egg-info/requires.txt +1 -0
  5. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/__init__.py +2 -2
  6. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/__main__.py +21 -6
  7. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/attackgraph/attackgraph.py +7 -7
  8. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/language/languagegraph.py +84 -54
  9. mal_toolbox-1.0.3/maltoolbox/patternfinder/attackgraph_patterns.py +134 -0
  10. mal_toolbox-1.0.3/maltoolbox/visualization/__init__.py +0 -0
  11. mal_toolbox-1.0.3/maltoolbox/visualization/graphviz_utils.py +102 -0
  12. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/pyproject.toml +2 -1
  13. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/AUTHORS +0 -0
  14. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/LICENSE +0 -0
  15. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/README.md +0 -0
  16. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/mal_toolbox.egg-info/dependency_links.txt +0 -0
  17. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/mal_toolbox.egg-info/entry_points.txt +0 -0
  18. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/mal_toolbox.egg-info/top_level.txt +0 -0
  19. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/attackgraph/__init__.py +0 -0
  20. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/attackgraph/analyzers/__init__.py +0 -0
  21. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/attackgraph/node.py +0 -0
  22. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/exceptions.py +0 -0
  23. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/file_utils.py +0 -0
  24. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/language/__init__.py +0 -0
  25. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/language/compiler/__init__.py +0 -0
  26. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/language/compiler/mal_lexer.py +0 -0
  27. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/language/compiler/mal_parser.py +0 -0
  28. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/model.py +0 -0
  29. {mal_toolbox-1.0.2/maltoolbox/ingestors → mal_toolbox-1.0.3/maltoolbox/patternfinder}/__init__.py +0 -0
  30. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/py.typed +0 -0
  31. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/translators/__init__.py +0 -0
  32. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/translators/securicad.py +0 -0
  33. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/maltoolbox/translators/updater.py +0 -0
  34. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/setup.cfg +0 -0
  35. {mal_toolbox-1.0.2 → mal_toolbox-1.0.3}/tests/test_model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mal-toolbox
3
- Version: 1.0.2
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mal-toolbox
3
- Version: 1.0.2
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
@@ -18,13 +18,16 @@ maltoolbox/attackgraph/__init__.py
18
18
  maltoolbox/attackgraph/attackgraph.py
19
19
  maltoolbox/attackgraph/node.py
20
20
  maltoolbox/attackgraph/analyzers/__init__.py
21
- maltoolbox/ingestors/__init__.py
22
21
  maltoolbox/language/__init__.py
23
22
  maltoolbox/language/languagegraph.py
24
23
  maltoolbox/language/compiler/__init__.py
25
24
  maltoolbox/language/compiler/mal_lexer.py
26
25
  maltoolbox/language/compiler/mal_parser.py
26
+ maltoolbox/patternfinder/__init__.py
27
+ maltoolbox/patternfinder/attackgraph_patterns.py
27
28
  maltoolbox/translators/__init__.py
28
29
  maltoolbox/translators/securicad.py
29
30
  maltoolbox/translators/updater.py
31
+ maltoolbox/visualization/__init__.py
32
+ maltoolbox/visualization/graphviz_utils.py
30
33
  tests/test_model.py
@@ -1,3 +1,4 @@
1
+ graphviz
1
2
  antlr4-tools
2
3
  antlr4-python3-runtime
3
4
  docopt
@@ -1,5 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # MAL Toolbox v1.0.2
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.2"
24
+ __version__ = "1.0.3"
25
25
  __authors__ = [
26
26
  "Andrei Buhaiu",
27
27
  "Giuseppe Nebbione",
@@ -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
@@ -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,8 +485,13 @@ 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)
@@ -497,6 +502,39 @@ class LanguageGraphAttackStep:
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,8 +1000,8 @@ 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
1006
  tags = list(attack_step_dict['tags'])
972
1007
  )
@@ -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,8 +1721,8 @@ 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
1727
  tags = list(attack_step_attribs['tags'])
1696
1728
  )
@@ -1732,8 +1764,8 @@ 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
1770
  tags = list(attack_step.tags)
1739
1771
  )
@@ -1799,16 +1831,14 @@ class LanguageGraph():
1799
1831
  target_attack_step_name]
1800
1832
 
1801
1833
  # Link to the children target attack steps
1802
- attack_step.children.setdefault(target_attack_step.full_name, [])
1803
- attack_step.children[target_attack_step.full_name].append(
1804
- (target_attack_step, expr_chain)
1805
- )
1834
+ attack_step.own_children.setdefault(target_attack_step, [])
1835
+ attack_step.own_children[target_attack_step].append(expr_chain)
1806
1836
 
1807
1837
  # Reverse the children associations chains to get the
1808
1838
  # parents associations chain.
1809
- target_attack_step.parents.setdefault(attack_step.full_name, [])
1810
- target_attack_step.parents[attack_step.full_name].append(
1811
- (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)
1812
1842
  )
1813
1843
 
1814
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)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mal-toolbox"
3
- version = "1.0.2"
3
+ version = "1.0.3"
4
4
  authors = [
5
5
  { name="Andrei Buhaiu", email="buhaiu@kth.se" },
6
6
  { name="Joakim Loxdal", email="loxdal@kth.se" },
@@ -12,6 +12,7 @@ description = "A collection of tools used to create MAL models and attack graphs
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
14
14
  dependencies = [
15
+ "graphviz",
15
16
  "antlr4-tools",
16
17
  "antlr4-python3-runtime",
17
18
  "docopt",
File without changes
File without changes
File without changes
File without changes