mal-toolbox 0.3.11__py3-none-any.whl → 1.0.1__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
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: mal-toolbox
3
- Version: 0.3.11
3
+ Version: 1.0.1
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,11 +18,11 @@ Requires-Python: >=3.10
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  License-File: AUTHORS
21
- Requires-Dist: py2neo>=2021.2.3
22
21
  Requires-Dist: antlr4-tools
23
22
  Requires-Dist: antlr4-python3-runtime
24
23
  Requires-Dist: docopt
25
24
  Requires-Dist: PyYAML
25
+ Dynamic: license-file
26
26
 
27
27
  # MAL Toolbox overview
28
28
 
@@ -30,7 +30,6 @@ MAL Toolbox is a collection of python modules to help developers create and work
30
30
  MAL ([Meta Attack Language](https://mal-lang.org/)) models and attack graphs.
31
31
 
32
32
  Attack graphs can be used to run simulations (see MAL Simulator) or analysis.
33
- MAL Toolbox also gives the ability to view the AttackGraph/Model graphically in neo4j.
34
33
 
35
34
  [Documentation](https://mal-lang.org/mal-toolbox/index.html)(Work in progress)
36
35
 
@@ -53,13 +52,7 @@ With a MAL language a Model (a MAL instance model) can be created either
53
52
  from a model file or empty.
54
53
 
55
54
  The model class will store all of the relevant information to the MAL
56
- instance model, most importantly the assets and associations that make it up.
57
-
58
- Assets and associations are objects of classes created using the language
59
- classes factory submodule in runtime. It also allows for `Attacker` objects
60
- to be created and associated with attack steps on assets in the model.
61
- The most relevant methods of the Model are the ones used to add different
62
- elements to the model, `add_asset`, `add_association`, and `add_attacker`.
55
+ instance model, most importantly the assets and their associations.
63
56
 
64
57
  Model objects can be used to generate attack graphs with the AttackGraph module.
65
58
 
@@ -76,11 +69,6 @@ nodes related and the asset field which will contain the object in the model
76
69
  instance to which this attack step belongs to, if this information is
77
70
  available.
78
71
 
79
- If it is relevant the `attach_attackers` function can be called on the
80
- resulting attack graph with the instance model given as a parameter in order
81
- to create attack step nodes that represent the entry points of the attackers
82
- and attach them to the attack steps specified in the instance model.
83
-
84
72
  ## Ingestors Module
85
73
 
86
74
  The ingestors module contains various tools that can make use of the instance
@@ -131,16 +119,9 @@ Arguments:
131
119
  <lang_file> Path to .mar or .mal file containing MAL spec.
132
120
  <output_file> Path to write the result of the compilation (yml/json).
133
121
 
134
- Options:
135
- --neo4j Ingest attack graph and instance model into a Neo4j instance
136
-
137
122
  Notes:
138
123
  - <lang_file> can be either a .mar file (generated by the older MAL
139
124
  compiler) or a .mal file containing the DSL written in MAL.
140
-
141
- - If --neo4j is used, the Neo4j instance should be running. The connection
142
- parameters required for this app to reach the Neo4j instance should be
143
- defined in the default.conf file.
144
125
  ```
145
126
 
146
127
  ## Code examples / Tutorial
@@ -0,0 +1,26 @@
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
5
+ maltoolbox/exceptions.py,sha256=0YjPx2v1yYumZ2o7pVZ1s_jS-GAb3Ng979KEFhROSNY,1399
6
+ maltoolbox/file_utils.py,sha256=fYG3UsvPQcU0ES_WI3nLfuzSZgc0jtE4IAxdMGgs9aA,1876
7
+ maltoolbox/model.py,sha256=xTK2jUr0Gz5pPVhdjh78zO5G46nV8N2ciIt6M5SwcGU,16058
8
+ maltoolbox/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ maltoolbox/attackgraph/__init__.py,sha256=m_81AjzwXONdclcW_R7mF2f8p-4DvoSRVfQ3Nyh7fak,298
10
+ maltoolbox/attackgraph/attackgraph.py,sha256=fuhpfD3YnQEdDdG7QPdYSylOX3XPL2mbx4cX0_bmX3c,26870
11
+ maltoolbox/attackgraph/node.py,sha256=Z2sdzXhPel9h7ySxP9fjgd1exVmpRbvRySVtLpI1_BM,3904
12
+ maltoolbox/attackgraph/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ maltoolbox/ingestors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ maltoolbox/language/__init__.py,sha256=TsTTryEyjChwHN1o5F2BSUlFsAss2N6J0H0-nzvXiD8,489
15
+ maltoolbox/language/languagegraph.py,sha256=WLV0-CvfrB4yqaDW3NTLyVOVDduNeD1By3Yqmjzn00Y,73810
16
+ maltoolbox/language/compiler/__init__.py,sha256=JQyAgDwJh1pU7AmuOhd1-d2b2PYXpgMVPtxnav8QHVc,15872
17
+ maltoolbox/language/compiler/mal_lexer.py,sha256=BeifykDAt4PloRASOaLzBgWF35ev_zgD8lXMIsSHykc,12063
18
+ maltoolbox/language/compiler/mal_parser.py,sha256=sUoaE43l2VKg-Dou30mk2wlVS1FvdOREwHNIyFe4IkY,114699
19
+ maltoolbox/translators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ maltoolbox/translators/securicad.py,sha256=F_rndv2JyKxfHAXPwf2RrdiFPnemJVArYUpVsFP6QQk,6997
21
+ 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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.0.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
maltoolbox/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # MAL Toolbox v0.3.11
2
+ # MAL Toolbox v1.0.1
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__ = "0.3.11"
24
+ __version__ = "1.0.1"
25
25
  __authors__ = [
26
26
  "Andrei Buhaiu",
27
27
  "Giuseppe Nebbione",
@@ -45,9 +45,9 @@ config: dict[str, Any] = {
45
45
  "log_file": "logs/log.txt",
46
46
  "attackgraph_file": "logs/attackgraph.yml",
47
47
  "model_file": "logs/model.yml",
48
- "langspec_file": "logs/langspec_file.yml",
48
+ "langspec_file": "logs/langspec_file.json",
49
+ "langgraph_file": "logs/langgraph.yml",
49
50
  },
50
- "neo4j": {"uri": None, "username": None, "password": None, "dbname": None},
51
51
  }
52
52
 
53
53
  config_file = os.getenv("MALTOOLBOX_CONFIG", "maltoolbox.yml")
@@ -56,8 +56,7 @@ if os.path.exists(config_file):
56
56
  with open(config_file) as f:
57
57
  config |= yaml.safe_load(f)
58
58
 
59
- log_configs, neo4j_configs = config.values()
60
-
59
+ log_configs = config['logging']
61
60
  os.makedirs(os.path.dirname(log_configs["log_file"]), exist_ok=True)
62
61
 
63
62
  formatter = logging.Formatter(
maltoolbox/__main__.py CHANGED
@@ -11,26 +11,18 @@ Arguments:
11
11
  <lang_file> Path to .mar or .mal file containing MAL spec.
12
12
  <output_file> Path to write the result of the compilation (yml/json).
13
13
 
14
- Options:
15
- --neo4j Ingest attack graph and instance model into a Neo4j instance
16
-
17
14
  Notes:
18
15
  - <lang_file> can be either a .mar file (generated by the older MAL
19
16
  compiler) or a .mal file containing the DSL written in MAL.
20
-
21
- - If --neo4j is used, the Neo4j instance should be running. The connection
22
- parameters required for this app to reach the Neo4j instance should be
23
- defined in the default.conf file.
24
17
  """
25
18
 
26
19
  import logging
27
20
  import json
28
21
  import docopt
29
22
 
30
- from . import log_configs, neo4j_configs
23
+ from . import log_configs
31
24
  from .attackgraph import create_attack_graph
32
25
  from .language.compiler import MalCompiler
33
- from .ingestors import neo4j
34
26
  from .language.languagegraph import LanguageGraph
35
27
  from .translators.updater import load_model_from_older_version
36
28
 
@@ -39,14 +31,12 @@ logger = logging.getLogger(__name__)
39
31
  def generate_attack_graph(
40
32
  model_file: str,
41
33
  lang_file: str,
42
- send_to_neo4j: bool
43
34
  ) -> None:
44
- """Create an attack graph and optionally send to neo4j
35
+ """Create an attack graph
45
36
 
46
37
  Args:
47
38
  model_file - path to the model file
48
39
  lang_file - path to the language file
49
- send_to_neo4j - whether to ingest into neo4j or not
50
40
  """
51
41
  attack_graph = create_attack_graph(lang_file, model_file)
52
42
  if log_configs['attackgraph_file']:
@@ -54,27 +44,6 @@ def generate_attack_graph(
54
44
  log_configs['attackgraph_file']
55
45
  )
56
46
 
57
- if send_to_neo4j:
58
- logger.debug('Ingest model graph into Neo4J database.')
59
- neo4j.ingest_model(
60
- attack_graph.model,
61
- neo4j_configs['uri'],
62
- neo4j_configs['username'],
63
- neo4j_configs['password'],
64
- neo4j_configs['dbname'],
65
- delete=True
66
- )
67
- logger.debug('Ingest attack graph into Neo4J database.')
68
- neo4j.ingest_attack_graph(
69
- attack_graph,
70
- neo4j_configs['uri'],
71
- neo4j_configs['username'],
72
- neo4j_configs['password'],
73
- neo4j_configs['dbname'],
74
- delete=False
75
- )
76
-
77
-
78
47
  def compile(lang_file: str, output_file: str) -> None:
79
48
  """Compile language and dump into output file"""
80
49
  compiler = MalCompiler()
@@ -97,7 +66,7 @@ def main():
97
66
 
98
67
  if args['attack-graph'] and args['generate']:
99
68
  generate_attack_graph(
100
- args['<model_file>'], args['<lang_file>'], args['--neo4j']
69
+ args['<model_file>'], args['<lang_file>']
101
70
  )
102
71
  elif args['compile']:
103
72
  compile(
@@ -3,6 +3,12 @@ Contains tools used to generate attack graphs from MAL instance
3
3
  models and analyze attack graphs.
4
4
  """
5
5
 
6
- from .attacker import Attacker
7
6
  from .attackgraph import AttackGraph, create_attack_graph
8
7
  from .node import AttackGraphNode
8
+
9
+ __all__ = [
10
+ "Attacker",
11
+ "AttackGraph",
12
+ "AttackGraphNode",
13
+ "create_attack_graph"
14
+ ]
@@ -11,9 +11,7 @@ import zipfile
11
11
  from itertools import chain
12
12
  from typing import TYPE_CHECKING
13
13
 
14
- from .analyzers.apriori import calculate_viability_and_necessity
15
14
  from .node import AttackGraphNode
16
- from .attacker import Attacker
17
15
  from .. import log_configs
18
16
  from ..exceptions import AttackGraphStepExpressionError, AttackGraphException
19
17
  from ..exceptions import LanguageGraphException
@@ -35,28 +33,44 @@ logger = logging.getLogger(__name__)
35
33
 
36
34
 
37
35
  def create_attack_graph(
38
- lang_file: str,
39
- model_file: str,
40
- attach_attackers=True,
41
- calc_viability_and_necessity=True
36
+ lang: str | LanguageGraph,
37
+ model: str | Model,
42
38
  ) -> AttackGraph:
43
39
  """Create and return an attack graph
44
40
 
45
41
  Args:
46
- lang_file - path to language file (.mar or .mal)
47
- model_file - path to model file (yaml or json)
48
- attach_attackers - whether to run attach_attackers or not
49
- calc_viability_and_necessity - whether run apriori calculations or not
42
+ lang - path to language file (.mar or .mal) or a LanguageGraph object
43
+ model - path to model file (yaml or json) or a Model object
50
44
  """
51
- try:
52
- lang_graph = LanguageGraph.from_mar_archive(lang_file)
53
- except zipfile.BadZipFile:
54
- lang_graph = LanguageGraph.from_mal_spec(lang_file)
55
45
 
56
- if log_configs['langspec_file']:
57
- lang_graph.save_to_file(log_configs['langspec_file'])
46
+ # Load language
47
+ if isinstance(lang, LanguageGraph):
48
+ lang_graph = lang
49
+ elif isinstance(lang, str):
50
+ # Load from path
51
+ try:
52
+ lang_graph = LanguageGraph.from_mar_archive(lang)
53
+ except zipfile.BadZipFile:
54
+ lang_graph = LanguageGraph.from_mal_spec(lang)
55
+ else:
56
+ raise TypeError("`lang` must be either string or LanguageGraph")
57
+
58
+ if 'langspec_file' in log_configs:
59
+ lang_graph.save_language_specification_to_json(
60
+ log_configs['langspec_file']
61
+ )
62
+
63
+ if 'langgraph_file' in log_configs:
64
+ lang_graph.save_to_file(log_configs['langgraph_file'])
58
65
 
59
- instance_model = Model.load_from_file(model_file, lang_graph)
66
+ # Load model
67
+ if isinstance(model, Model):
68
+ instance_model = model
69
+ elif isinstance(model, str):
70
+ # Load from path
71
+ instance_model = Model.load_from_file(model, lang_graph)
72
+ else:
73
+ raise TypeError("`model` must be either string or Model")
60
74
 
61
75
  if log_configs['model_file']:
62
76
  instance_model.save_to_file(log_configs['model_file'])
@@ -70,12 +84,6 @@ def create_attack_graph(
70
84
  )
71
85
  sys.exit(1)
72
86
 
73
- if attach_attackers:
74
- attack_graph.attach_attackers()
75
-
76
- if calc_viability_and_necessity:
77
- calculate_viability_and_necessity(attack_graph)
78
-
79
87
  return attack_graph
80
88
 
81
89
 
@@ -83,15 +91,13 @@ class AttackGraph():
83
91
  """Graph representation of attack steps"""
84
92
  def __init__(self, lang_graph, model: Optional[Model] = None):
85
93
  self.nodes: dict[int, AttackGraphNode] = {}
86
- self.attackers: dict[int, Attacker] = {}
87
- # Dictionaries used in optimization to get nodes and attackers by id
88
- # or full name faster
94
+ # Dictionaries used in optimization to get nodes by id or full name
95
+ # faster
89
96
  self._full_name_to_node: dict[str, AttackGraphNode] = {}
90
97
 
91
98
  self.model = model
92
99
  self.lang_graph = lang_graph
93
100
  self.next_node_id = 0
94
- self.next_attacker_id = 0
95
101
  if self.model is not None:
96
102
  self._generate_graph()
97
103
 
@@ -102,18 +108,11 @@ class AttackGraph():
102
108
  def _to_dict(self) -> dict:
103
109
  """Convert AttackGraph to dict"""
104
110
  serialized_attack_steps = {}
105
- serialized_attackers = {}
106
111
  for ag_node in self.nodes.values():
107
112
  serialized_attack_steps[ag_node.full_name] =\
108
113
  ag_node.to_dict()
109
- for attacker in self.attackers.values():
110
- serialized_attackers[attacker.name] = attacker.to_dict()
111
- logger.debug('Serialized %d attack steps and %d attackers.' %
112
- (len(self.nodes), len(self.attackers))
113
- )
114
114
  return {
115
- 'attack_steps': serialized_attack_steps,
116
- 'attackers': serialized_attackers,
115
+ 'attack_steps': serialized_attack_steps
117
116
  }
118
117
 
119
118
  def __deepcopy__(self, memo):
@@ -139,24 +138,12 @@ class AttackGraph():
139
138
  if node.children:
140
139
  memo[id(node)].children = copy.deepcopy(node.children, memo)
141
140
 
142
- # Deep copy attackers
143
- for attacker_id, attacker in self.attackers.items():
144
- copied_attacker = copy.deepcopy(attacker, memo)
145
- copied_attackgraph.attackers[attacker_id] = copied_attacker
146
-
147
- # Re-link attacker references
148
- for node in self.nodes.values():
149
- if node.compromised_by:
150
- memo[id(node)].compromised_by = copy.deepcopy(
151
- node.compromised_by, memo)
152
-
153
141
  # Copy lookup dicts
154
142
  copied_attackgraph._full_name_to_node = \
155
143
  copy.deepcopy(self._full_name_to_node, memo)
156
144
 
157
145
  # Copy counters
158
146
  copied_attackgraph.next_node_id = self.next_node_id
159
- copied_attackgraph.next_attacker_id = self.next_attacker_id
160
147
 
161
148
  return copied_attackgraph
162
149
 
@@ -181,7 +168,6 @@ class AttackGraph():
181
168
  attack_graph = AttackGraph(lang_graph)
182
169
  attack_graph.model = model
183
170
  serialized_attack_steps = serialized_object['attack_steps']
184
- serialized_attackers = serialized_object['attackers']
185
171
 
186
172
  # Create all of the nodes in the imported attack graph.
187
173
  for node_dict in serialized_attack_steps.values():
@@ -205,7 +191,7 @@ class AttackGraph():
205
191
  lg_attack_step = lg_attack_step,
206
192
  node_id = node_dict['id'],
207
193
  model_asset = node_asset,
208
- defense_status = node_dict.get('defense_status', None),
194
+ ttc_dist = node_dict['ttc'],
209
195
  existence_status = node_dict.get('existence_status', None)
210
196
  )
211
197
  ag_node.tags = set(node_dict.get('tags', []))
@@ -248,21 +234,6 @@ class AttackGraph():
248
234
  raise LookupError(msg % parent_id)
249
235
  _ag_node.parents.add(parent)
250
236
 
251
- for attacker in serialized_attackers.values():
252
- ag_attacker = Attacker(name = attacker['name'])
253
- attack_graph.add_attacker(
254
- attacker = ag_attacker,
255
- attacker_id = int(attacker['id']),
256
- entry_points = [
257
- int(node_id) # Convert to int since they can be strings
258
- for node_id in attacker['entry_points'].keys()
259
- ],
260
- reached_attack_steps = [
261
- int(node_id) # Convert to int since they can be strings
262
- for node_id in attacker['reached_attack_steps'].keys()
263
- ]
264
- )
265
-
266
237
  return attack_graph
267
238
 
268
239
  @classmethod
@@ -304,46 +275,6 @@ class AttackGraph():
304
275
  logger.debug(f'Looking up node with full name "%s"', full_name)
305
276
  return self._full_name_to_node.get(full_name)
306
277
 
307
- def attach_attackers(self) -> None:
308
- """
309
- Create attackers and their entry point nodes and attach them to the
310
- relevant attack step nodes and to the attackers.
311
- """
312
-
313
- if not self.model:
314
- msg = "Can not attach attackers without a model"
315
- logger.error(msg)
316
- raise AttackGraphException(msg)
317
-
318
- logger.info(
319
- 'Attach attackers from "%s" model to the graph.', self.model.name
320
- )
321
-
322
- for attacker_info in self.model.attackers:
323
-
324
- if not attacker_info.name:
325
- msg = "Can not attach attacker without name"
326
- logger.error(msg)
327
- raise AttackGraphException(msg)
328
-
329
- attacker = Attacker(name = attacker_info.name)
330
- self.add_attacker(attacker)
331
-
332
- for (asset, attack_steps) in attacker_info.entry_points:
333
- for attack_step in attack_steps:
334
- full_name = asset.name + ':' + attack_step
335
- ag_node = self.get_node_by_full_name(full_name)
336
- if not ag_node:
337
- logger.warning(
338
- 'Failed to find attacker entry point '
339
- '%s for %s.',
340
- full_name, attacker.name
341
- )
342
- continue
343
- attacker.compromise(ag_node)
344
-
345
- attacker.entry_points = set(attacker.reached_attack_steps)
346
-
347
278
  def _follow_expr_chain(
348
279
  self,
349
280
  model: Model,
@@ -535,18 +466,27 @@ class AttackGraph():
535
466
  'Generating attack step node for %s.', attack_step.name
536
467
  )
537
468
 
538
- defense_status = None
539
469
  existence_status = None
540
470
  node_name = asset.name + ':' + attack_step.name
541
471
 
472
+ ttc_dist = copy.deepcopy(attack_step.ttc)
542
473
  match (attack_step.type):
543
474
  case 'defense':
544
- # Set the defense status for defenses
545
- defense_status = asset.defenses[attack_step.name]
546
- logger.debug(
547
- 'Setting the defense status of \"%s\" to "%s".',
548
- node_name, defense_status
549
- )
475
+ # Set the TTC probability for defenses
476
+ # that were explicitly set in model
477
+ if attack_step.name in asset.defenses:
478
+ defense_value = float(
479
+ asset.defenses[attack_step.name]
480
+ )
481
+ ttc_dist = {
482
+ 'arguments': [defense_value],
483
+ 'name': 'Bernoulli',
484
+ 'type': 'function'
485
+ }
486
+ logger.debug(
487
+ 'Setting defense \"%s\" to "%s".',
488
+ node_name, defense_value
489
+ )
550
490
 
551
491
  case 'exist' | 'notExist':
552
492
  # Resolve step expression associated with
@@ -577,7 +517,7 @@ class AttackGraph():
577
517
  ag_node = self.add_node(
578
518
  lg_attack_step = attack_step,
579
519
  model_asset = asset,
580
- defense_status = defense_status,
520
+ ttc_dist = ttc_dist,
581
521
  existence_status = existence_status
582
522
  )
583
523
  attack_step_nodes.append(ag_node)
@@ -660,7 +600,6 @@ class AttackGraph():
660
600
  """
661
601
 
662
602
  self.nodes = {}
663
- self.attackers = {}
664
603
  self._generate_graph()
665
604
 
666
605
  def add_node(
@@ -668,7 +607,7 @@ class AttackGraph():
668
607
  lg_attack_step: LanguageGraphAttackStep,
669
608
  node_id: Optional[int] = None,
670
609
  model_asset: Optional[ModelAsset] = None,
671
- defense_status: Optional[float] = None,
610
+ ttc_dist: Optional[dict] = None,
672
611
  existence_status: Optional[bool] = None
673
612
  ) -> AttackGraphNode:
674
613
  """Create and add a node to the graph
@@ -685,9 +624,10 @@ class AttackGraph():
685
624
  only be ommitted if the model which was used to
686
625
  generate the attack graph is not available when
687
626
  loading an attack graph from a file.
688
- defese_status - the defense status of the node. Only, relevant
689
- for defense type nodes. A value between 0.0 and
690
- 1.0 is expected.
627
+ ttc_dist - the ttc distribution to assign to the node. This
628
+ is relevant for when we want to override the ttc
629
+ distribution as it is defined in the language.
630
+ Frequently used for defenses.
691
631
  existence_status - the existence status of the node. Only, relevant
692
632
  for exist and notExist type nodes.
693
633
 
@@ -712,7 +652,7 @@ class AttackGraph():
712
652
  node_id = node_id,
713
653
  lg_attack_step = lg_attack_step,
714
654
  model_asset = model_asset,
715
- defense_status = defense_status,
655
+ ttc_dist = ttc_dist,
716
656
  existence_status = existence_status
717
657
  )
718
658
 
@@ -738,80 +678,3 @@ class AttackGraph():
738
678
  raise ValueError(f'Invalid node id.')
739
679
  del self.nodes[node.id]
740
680
  del self._full_name_to_node[node.full_name]
741
-
742
- def add_attacker(
743
- self,
744
- attacker: Attacker,
745
- attacker_id: Optional[int] = None,
746
- entry_points: list[int] = [],
747
- reached_attack_steps: list[int] = []
748
- ):
749
- """Add an attacker to the graph
750
- Arguments:
751
- attacker - the attacker to add
752
- attacker_id - the id to assign to this attacker, usually
753
- used when loading an attack graph from a
754
- file
755
- entry_points - list of attack step ids that serve as entry
756
- points for the attacker
757
- reached_attack_steps - list of ids of the attack steps that the
758
- attacker has reached
759
- """
760
-
761
- if logger.isEnabledFor(logging.DEBUG):
762
- # Avoid running json.dumps when not in debug
763
- if attacker_id is not None:
764
- logger.debug('Add attacker "%s" with id:%d.',
765
- attacker.name,
766
- attacker_id
767
- )
768
- else:
769
- logger.debug('Add attacker "%s" without id.',
770
- attacker.name
771
- )
772
-
773
- attacker.id = attacker_id or self.next_attacker_id
774
- if attacker.id in self.attackers:
775
- raise ValueError(f'Attacker index {attacker_id} already in use.')
776
-
777
- self.next_attacker_id = max(attacker.id + 1, self.next_attacker_id)
778
- for node_id in reached_attack_steps:
779
- node = self.nodes[node_id]
780
- if node:
781
- attacker.compromise(node)
782
- else:
783
- msg = ("Could not find node with id %d"
784
- "in reached attack steps.")
785
- logger.error(msg, node_id)
786
- raise AttackGraphException(msg % node_id)
787
- for node_id in entry_points:
788
- node = self.nodes[node_id]
789
- if node:
790
- attacker.entry_points.add(node)
791
- else:
792
- msg = ("Could not find node with id %d"
793
- "in attacker entrypoints.")
794
- logger.error(msg, node_id)
795
- raise AttackGraphException(msg % node_id)
796
- self.attackers[attacker.id] = attacker
797
-
798
- def remove_attacker(self, attacker: Attacker):
799
- """Remove attacker from attack graph
800
- Arguments:
801
- attacker - the attacker we wish to remove from the attack graph
802
- """
803
- if logger.isEnabledFor(logging.DEBUG):
804
- # Avoid running json.dumps when not in debug
805
- logger.debug(
806
- 'Remove attacker "%s" with id:%d.',
807
- attacker.name, attacker.id
808
- )
809
-
810
- # Copy set - we can not remove elements from a set we are looping over
811
- nodes_to_uncompromise = set(attacker.reached_attack_steps)
812
- for node in nodes_to_uncompromise:
813
- attacker.undo_compromise(node)
814
-
815
- if not isinstance(attacker.id, int):
816
- raise ValueError(f'Invalid attacker id: {attacker.id}')
817
- del self.attackers[attacker.id]