From 3fe2d9abd1c4c40b62af5beaa03a75ce0cee9c48 Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Mon, 13 May 2024 15:14:29 +0200 Subject: [PATCH 01/24] Rearrange functions in conftest --- tests/conftest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 9087bd23..fb886a3e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,6 +36,14 @@ def corelang_lang_graph(): mar_file_path = path_testdata("org.mal-lang.coreLang-1.0.0.mar") return LanguageGraph.from_mar_archive(mar_file_path) +## Fixtures (can be ingested into tests) + +@pytest.fixture +def corelang_spec(): + """Fixture that returns the coreLang language specification as dict""" + mar_file_path = path_testdata("org.mal-lang.coreLang-1.0.0.mar") + return specification.load_language_specification_from_mar(mar_file_path) + @pytest.fixture def model(corelang_lang_graph): From 371403bf64c4e55eb4736058a3bcb4e079021b9a Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Wed, 15 May 2024 13:31:32 +0200 Subject: [PATCH 02/24] Add initial idea for pattern searching in attack graph --- maltoolbox/patterns/__init__.py | 0 maltoolbox/patterns/attackgraph_patterns.py | 110 ++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 maltoolbox/patterns/__init__.py create mode 100644 maltoolbox/patterns/attackgraph_patterns.py diff --git a/maltoolbox/patterns/__init__.py b/maltoolbox/patterns/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/maltoolbox/patterns/attackgraph_patterns.py b/maltoolbox/patterns/attackgraph_patterns.py new file mode 100644 index 00000000..3f6d2b08 --- /dev/null +++ b/maltoolbox/patterns/attackgraph_patterns.py @@ -0,0 +1,110 @@ +"""Functions for searching for patterns in the AttackGraph""" + +from __future__ import annotations +from dataclasses import dataclass +from maltoolbox.attackgraph import AttackGraph, AttackGraphNode + + +@dataclass +class AttackGraphPattern: + """A pattern to search for in a graph""" + attributes: dict + next_pattern: AttackGraphPattern | None = None + min_repeated: int = 1 + max_repeated: int = 1 + + +def find_in_graph(graph: AttackGraph, pattern: AttackGraphPattern): + """Query a graph for a pattern of attributes""" + + # Find the starting nodes + attribute = pattern.attributes[0] + starting_nodes = graph.get_nodes_by_attribute_value( + attribute[0], attribute[1] + ) + + matching_chains = [] + for node in starting_nodes: + matching_chains += find_recursively( + node, + pattern + ) + return matching_chains + + +def find_recursively( + node: AttackGraphNode, + pattern: AttackGraphPattern, + current_chain=None, + matching_chains=None, + pattern_match_count=0 + ): + """Follow a chain of attack graph nodes, check if they follow the pattern + and if they do, add them to the returned list of matching nodes + + Args: + node - node to check if current `pattern` matches for + pattern - pattern to match against `node` + matching_nodes - list of matched nodes so far (builds up recursively) + pattern_match_count - the number of matches on current pattern so far + + Return: list of AttackGraphNodes that match the pattern + """ + + # Init chain lists if None + current_chain = [] if current_chain is None else current_chain + matching_chains = [] if matching_chains is None else matching_chains + + # See if current node matches pattern + node_matches_pattern = True + for attr, value in pattern.attributes: + if getattr(node, attr) != value: + node_matches_pattern = False + break + + if node_matches_pattern: + # Current node matches, add to current_chain and increment match_count + current_chain.append(node) + pattern_match_count += 1 + + # See if current pattern is fulfilled + pattern_fulfilled = pattern_match_count >= pattern.min_repeated + pattern_can_be_used_again = pattern_match_count < pattern.max_repeated + + if pattern_fulfilled and pattern.next_pattern is None: + # This is the last pattern in the chain + # If it is fulfilled the current chain is done + matching_chains.append(current_chain) + + elif node_matches_pattern and pattern_can_be_used_again: + # Pattern has matches left + for child in node.children: + matching_chains = find_recursively( + child, + pattern, + current_chain=current_chain, + matching_chains=matching_chains, + pattern_match_count=pattern_match_count + ) + + elif node_matches_pattern and not pattern_can_be_used_again: + # Pattern has run out of matches, must move to next pattern + for child in node.children: + matching_chains = find_recursively( + child, + pattern.next_pattern, + current_chain=current_chain, + matching_chains=matching_chains + ) + + elif not node_matches_pattern and pattern_fulfilled: + # Node did not match current pattern, but we can try with + # the next pattern since current one is fulfilled + matching_chains = find_recursively( + node, + pattern.next_pattern, + current_chain=current_chain, + matching_chains=matching_chains + ) + + return matching_chains From 8f6fab9f4f49b49a56bb3e0aff14909c99942e4d Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Wed, 15 May 2024 13:33:00 +0200 Subject: [PATCH 03/24] Initial test file for patterns --- tests/patterns/test_attackgraph_patterns.py | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/patterns/test_attackgraph_patterns.py diff --git a/tests/patterns/test_attackgraph_patterns.py b/tests/patterns/test_attackgraph_patterns.py new file mode 100644 index 00000000..e2cbcda4 --- /dev/null +++ b/tests/patterns/test_attackgraph_patterns.py @@ -0,0 +1,50 @@ +"""Tests for attack graph pattern matching""" +import pytest +from maltoolbox.model import Model, AttackerAttachment +from maltoolbox.attackgraph import AttackGraph + +import maltoolbox.patterns.attackgraph_patterns as attackgraph_patterns +from maltoolbox.patterns.attackgraph_patterns import AttackGraphPattern + +from test_model import create_application_asset, create_association + +@pytest.fixture +def example_attackgraph(corelang_spec, model: Model): + """Fixture that generates an example attack graph + + Uses coreLang specification and model with two applications + with an association and an attacker to create and return + an AttackGraph object + """ + + # Create 2 assets + app1 = create_application_asset(model, "Application 1") + app2 = create_application_asset(model, "Application 2") + model.add_asset(app1) + model.add_asset(app2) + + # Create association between app1 and app2 + assoc = create_association(model, from_assets=[app1], to_assets=[app2]) + model.add_association(assoc) + + attacker = AttackerAttachment() + attacker.entry_points = [ + (app1, ['attemptCredentialsReuse']) + ] + model.add_attacker(attacker) + + return AttackGraph(lang_spec=corelang_spec, model=model) + + +def test_attackgraph_find_pattern(example_attackgraph): + """Test a simple pattern""" + pattern = AttackGraphPattern( + next_pattern=AttackGraphPattern( + attributes=[('id', 'Application 1:successfulUseVulnerability')], + min_repeated=1, max_repeated=1 + ), + attributes=[('id', 'Application 1:notPresent')], + min_repeated=1, max_repeated=1 + ) + chains = attackgraph_patterns.find_in_graph(example_attackgraph, pattern) + breakpoint() From f97bfd56fd20c33922e4f74855517d42a829227b Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Thu, 16 May 2024 15:39:00 +0200 Subject: [PATCH 04/24] Move some functionality to the AttackGraphPattern class and refactor/rename find_recursively->find_matches_recursively --- maltoolbox/patterns/attackgraph_patterns.py | 107 +++++++++++--------- 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/maltoolbox/patterns/attackgraph_patterns.py b/maltoolbox/patterns/attackgraph_patterns.py index 3f6d2b08..6231517f 100644 --- a/maltoolbox/patterns/attackgraph_patterns.py +++ b/maltoolbox/patterns/attackgraph_patterns.py @@ -13,6 +13,27 @@ class AttackGraphPattern: min_repeated: int = 1 max_repeated: int = 1 + def matches(self, node: AttackGraphNode): + """Returns true if pattern matches node""" + matches_pattern = True + for attr, value in self.attributes: + if getattr(node, attr) != value: + matches_pattern = False + break + return matches_pattern + + def can_match_again(self, num_matches): + """Returns true if pattern can be used again""" + return num_matches < self.max_repeated + + def must_match_again(self, num_matches): + """Returns true if pattern must match again to be fulfilled""" + return num_matches < self.min_repeated + + def is_last_pattern_in_chain(self): + """Returns true if no more patterns to match after this""" + return self.next_pattern is None + def find_in_graph(graph: AttackGraph, pattern: AttackGraphPattern): """Query a graph for a pattern of attributes""" @@ -25,22 +46,23 @@ def find_in_graph(graph: AttackGraph, pattern: AttackGraphPattern): matching_chains = [] for node in starting_nodes: - matching_chains += find_recursively( + matching_chains += find_matches_recursively( node, pattern ) return matching_chains -def find_recursively( +def find_matches_recursively( node: AttackGraphNode, pattern: AttackGraphPattern, current_chain=None, matching_chains=None, pattern_match_count=0 ): - """Follow a chain of attack graph nodes, check if they follow the pattern - and if they do, add them to the returned list of matching nodes + """Follow a chain of attack graph nodes, check if they follow the pattern. + When a sequence of patterns is fulfilled for a sequence of nodes, + add the list of nodes to the returned `matching_chains` Args: node - node to check if current `pattern` matches for @@ -48,63 +70,54 @@ def find_recursively( matching_nodes - list of matched nodes so far (builds up recursively) pattern_match_count - the number of matches on current pattern so far - Return: list of AttackGraphNodes that match the pattern + Return: list of lists of AttackGraphNodes that match the pattern """ # Init chain lists if None current_chain = [] if current_chain is None else current_chain matching_chains = [] if matching_chains is None else matching_chains - # See if current node matches pattern - node_matches_pattern = True - for attr, value in pattern.attributes: - if getattr(node, attr) != value: - node_matches_pattern = False - break - if node_matches_pattern: + if pattern.matches(node): # Current node matches, add to current_chain and increment match_count current_chain.append(node) pattern_match_count += 1 - # See if current pattern is fulfilled - pattern_fulfilled = pattern_match_count >= pattern.min_repeated - pattern_can_be_used_again = pattern_match_count < pattern.max_repeated - - if pattern_fulfilled and pattern.next_pattern is None: - # This is the last pattern in the chain - # If it is fulfilled the current chain is done - matching_chains.append(current_chain) - - elif node_matches_pattern and pattern_can_be_used_again: - # Pattern has matches left - for child in node.children: - matching_chains = find_recursively( - child, - pattern, - current_chain=current_chain, - matching_chains=matching_chains, - pattern_match_count=pattern_match_count - ) - - elif node_matches_pattern and not pattern_can_be_used_again: - # Pattern has run out of matches, must move to next pattern - for child in node.children: - matching_chains = find_recursively( - child, + if pattern.is_last_pattern_in_chain() and \ + not pattern.must_match_again(pattern_match_count): + # This is the last pattern in the chain, + #the current chain is matching + matching_chains.append(current_chain) + + elif pattern.can_match_again(pattern_match_count): + # Pattern has matches left, run recursively with current pattern + for child in node.children: + matching_chains = find_matches_recursively( + child, + pattern, + current_chain=current_chain, + matching_chains=matching_chains, + pattern_match_count=pattern_match_count + ) + else: + # Pattern has run out of matches, must move on to next pattern + for child in node.children: + matching_chains = find_matches_recursively( + child, + pattern.next_pattern, + current_chain=current_chain, + matching_chains=matching_chains + ) + else: + if not pattern.must_match_again(pattern_match_count)\ + and not pattern.is_last_pattern_in_chain(): + # Node did not match current pattern, but we can try with + # the next pattern since current one is 'fulfilled' + matching_chains = find_matches_recursively( + node, pattern.next_pattern, current_chain=current_chain, matching_chains=matching_chains ) - elif not node_matches_pattern and pattern_fulfilled: - # Node did not match current pattern, but we can try with - # the next pattern since current one is fulfilled - matching_chains = find_recursively( - node, - pattern.next_pattern, - current_chain=current_chain, - matching_chains=matching_chains - ) - return matching_chains From b5dcfe8bd113bd17e2a2592ca2e544707cd7c0c0 Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Thu, 16 May 2024 15:57:56 +0200 Subject: [PATCH 05/24] Use list of patterns as input, instead of AttackGraphPattern with reference to next pattern --- maltoolbox/patterns/attackgraph_patterns.py | 41 ++++++++------------- tests/patterns/test_attackgraph_patterns.py | 16 ++++---- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/maltoolbox/patterns/attackgraph_patterns.py b/maltoolbox/patterns/attackgraph_patterns.py index 6231517f..2332f60b 100644 --- a/maltoolbox/patterns/attackgraph_patterns.py +++ b/maltoolbox/patterns/attackgraph_patterns.py @@ -9,7 +9,6 @@ class AttackGraphPattern: """A pattern to search for in a graph""" attributes: dict - next_pattern: AttackGraphPattern | None = None min_repeated: int = 1 max_repeated: int = 1 @@ -30,32 +29,25 @@ def must_match_again(self, num_matches): """Returns true if pattern must match again to be fulfilled""" return num_matches < self.min_repeated - def is_last_pattern_in_chain(self): - """Returns true if no more patterns to match after this""" - return self.next_pattern is None - -def find_in_graph(graph: AttackGraph, pattern: AttackGraphPattern): +def find_in_graph(graph: AttackGraph, patterns: list[AttackGraphPattern]): """Query a graph for a pattern of attributes""" # Find the starting nodes - attribute = pattern.attributes[0] + attribute = patterns[0].attributes[0] starting_nodes = graph.get_nodes_by_attribute_value( attribute[0], attribute[1] ) - matching_chains = [] for node in starting_nodes: - matching_chains += find_matches_recursively( - node, - pattern - ) + matching_chains += find_matches_recursively(node, patterns) + return matching_chains def find_matches_recursively( node: AttackGraphNode, - pattern: AttackGraphPattern, + pattern_list: list[AttackGraphPattern], current_chain=None, matching_chains=None, pattern_match_count=0 @@ -72,29 +64,28 @@ def find_matches_recursively( Return: list of lists of AttackGraphNodes that match the pattern """ - + current_pattern = pattern_list[0] # Init chain lists if None current_chain = [] if current_chain is None else current_chain matching_chains = [] if matching_chains is None else matching_chains - - if pattern.matches(node): + if current_pattern.matches(node): # Current node matches, add to current_chain and increment match_count current_chain.append(node) pattern_match_count += 1 - if pattern.is_last_pattern_in_chain() and \ - not pattern.must_match_again(pattern_match_count): + if len(pattern_list) == 1 \ + and not current_pattern.must_match_again(pattern_match_count): # This is the last pattern in the chain, - #the current chain is matching + # and the current chain is matching matching_chains.append(current_chain) - elif pattern.can_match_again(pattern_match_count): + elif current_pattern.can_match_again(pattern_match_count): # Pattern has matches left, run recursively with current pattern for child in node.children: matching_chains = find_matches_recursively( child, - pattern, + pattern_list, current_chain=current_chain, matching_chains=matching_chains, pattern_match_count=pattern_match_count @@ -104,18 +95,18 @@ def find_matches_recursively( for child in node.children: matching_chains = find_matches_recursively( child, - pattern.next_pattern, + pattern_list[1:], current_chain=current_chain, matching_chains=matching_chains ) else: - if not pattern.must_match_again(pattern_match_count)\ - and not pattern.is_last_pattern_in_chain(): + if not current_pattern.must_match_again(pattern_match_count)\ + and len(pattern_list) > 1: # Node did not match current pattern, but we can try with # the next pattern since current one is 'fulfilled' matching_chains = find_matches_recursively( node, - pattern.next_pattern, + pattern_list[1:], current_chain=current_chain, matching_chains=matching_chains ) diff --git a/tests/patterns/test_attackgraph_patterns.py b/tests/patterns/test_attackgraph_patterns.py index e2cbcda4..fc03500c 100644 --- a/tests/patterns/test_attackgraph_patterns.py +++ b/tests/patterns/test_attackgraph_patterns.py @@ -38,13 +38,15 @@ def example_attackgraph(corelang_spec, model: Model): def test_attackgraph_find_pattern(example_attackgraph): """Test a simple pattern""" - pattern = AttackGraphPattern( - next_pattern=AttackGraphPattern( + patterns = [ + AttackGraphPattern( + attributes=[('id', 'Application 1:notPresent')], + min_repeated=1, max_repeated=1 + ), + AttackGraphPattern( attributes=[('id', 'Application 1:successfulUseVulnerability')], min_repeated=1, max_repeated=1 ), - attributes=[('id', 'Application 1:notPresent')], - min_repeated=1, max_repeated=1 - ) - chains = attackgraph_patterns.find_in_graph(example_attackgraph, pattern) - breakpoint() + ] + chains = attackgraph_patterns.find_in_graph(example_attackgraph, patterns) + breakpoint() \ No newline at end of file From f5ef342b150c9d0c7d0191a1c175a47eeb173806 Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Thu, 16 May 2024 16:05:06 +0200 Subject: [PATCH 06/24] Fix input to find_in_graph in test --- maltoolbox/patterns/attackgraph_patterns.py | 7 ++++--- tests/patterns/test_attackgraph_patterns.py | 8 ++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/maltoolbox/patterns/attackgraph_patterns.py b/maltoolbox/patterns/attackgraph_patterns.py index 2332f60b..3c4fd232 100644 --- a/maltoolbox/patterns/attackgraph_patterns.py +++ b/maltoolbox/patterns/attackgraph_patterns.py @@ -58,17 +58,18 @@ def find_matches_recursively( Args: node - node to check if current `pattern` matches for - pattern - pattern to match against `node` + pattern_list - will attempt to match first pattern against `node` matching_nodes - list of matched nodes so far (builds up recursively) - pattern_match_count - the number of matches on current pattern so far + pattern_match_count - the number of matches on current pattern so far Return: list of lists of AttackGraphNodes that match the pattern """ - current_pattern = pattern_list[0] # Init chain lists if None current_chain = [] if current_chain is None else current_chain matching_chains = [] if matching_chains is None else matching_chains + current_pattern = pattern_list[0] + if current_pattern.matches(node): # Current node matches, add to current_chain and increment match_count current_chain.append(node) diff --git a/tests/patterns/test_attackgraph_patterns.py b/tests/patterns/test_attackgraph_patterns.py index fc03500c..93edcde9 100644 --- a/tests/patterns/test_attackgraph_patterns.py +++ b/tests/patterns/test_attackgraph_patterns.py @@ -40,11 +40,15 @@ def test_attackgraph_find_pattern(example_attackgraph): """Test a simple pattern""" patterns = [ AttackGraphPattern( - attributes=[('id', 'Application 1:notPresent')], + attributes=[('name', 'attemptRead')], min_repeated=1, max_repeated=1 ), AttackGraphPattern( - attributes=[('id', 'Application 1:successfulUseVulnerability')], + attributes=[('name', 'successfulRead')], + min_repeated=1, max_repeated=1 + ), + AttackGraphPattern( + attributes=[('name', 'read')], min_repeated=1, max_repeated=1 ), ] From 646cbfbf5d32aa775e2475540f6337423ec65ca2 Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Fri, 17 May 2024 10:19:56 +0200 Subject: [PATCH 07/24] Use lambdas instead of just checking attributes --- maltoolbox/patterns/attackgraph_patterns.py | 166 +++++++++++--------- tests/patterns/test_attackgraph_patterns.py | 32 ++-- 2 files changed, 106 insertions(+), 92 deletions(-) diff --git a/maltoolbox/patterns/attackgraph_patterns.py b/maltoolbox/patterns/attackgraph_patterns.py index 3c4fd232..3f39400a 100644 --- a/maltoolbox/patterns/attackgraph_patterns.py +++ b/maltoolbox/patterns/attackgraph_patterns.py @@ -1,115 +1,129 @@ -"""Functions for searching for patterns in the AttackGraph""" +"""Utilities for finding patterns in the AttackGraph""" from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from maltoolbox.attackgraph import AttackGraph, AttackGraphNode +from typing import Callable +class SearchPattern: + """A pattern consists of conditions, the conditions are used + to find matching paths in an attack graph""" + conditions: list[Condition] + + def __init__(self, conditions): + self.conditions = conditions + + def find_matches(self, graph: AttackGraph): + """Search through a graph for a pattern using + its conditions, and return matching paths of nodes + + Args: + graph - The graph to search in + + Return: list[list[AttackGraphNode]] of matching paths + """ + + # Find the starting nodes + condition = self.conditions[0] + starting_nodes = [] + for node in graph.nodes: + if condition.matches(node): + starting_nodes.append(node) + + matching_paths = [] + for node in starting_nodes: + matching_paths.extend( + find_matches_recursively(node, self.conditions) + ) + return matching_paths @dataclass -class AttackGraphPattern: - """A pattern to search for in a graph""" - attributes: dict +class SearchCondition: + """A filter has a condition that has to be true for a node to match""" + + # `matches` should be a lambda that takes node as input and returns bool + # If lamdba returns True for a node, the node matches + # If the lamdba returns False for a node, the node does not match + matches: Callable[[AttackGraphNode], bool] + + # It is possible to require/allow a Condition to repeat min_repeated: int = 1 max_repeated: int = 1 - def matches(self, node: AttackGraphNode): - """Returns true if pattern matches node""" - matches_pattern = True - for attr, value in self.attributes: - if getattr(node, attr) != value: - matches_pattern = False - break - return matches_pattern - def can_match_again(self, num_matches): - """Returns true if pattern can be used again""" + """Returns true if condition can be used again""" return num_matches < self.max_repeated def must_match_again(self, num_matches): - """Returns true if pattern must match again to be fulfilled""" + """Returns true if condition must match again to be fulfilled""" return num_matches < self.min_repeated -def find_in_graph(graph: AttackGraph, patterns: list[AttackGraphPattern]): - """Query a graph for a pattern of attributes""" - - # Find the starting nodes - attribute = patterns[0].attributes[0] - starting_nodes = graph.get_nodes_by_attribute_value( - attribute[0], attribute[1] - ) - matching_chains = [] - for node in starting_nodes: - matching_chains += find_matches_recursively(node, patterns) - - return matching_chains - - def find_matches_recursively( node: AttackGraphNode, - pattern_list: list[AttackGraphPattern], - current_chain=None, - matching_chains=None, - pattern_match_count=0 + condition_list: list[Condition], + current_path=None, + matching_paths=None, + condition_match_count=0 ): - """Follow a chain of attack graph nodes, check if they follow the pattern. - When a sequence of patterns is fulfilled for a sequence of nodes, - add the list of nodes to the returned `matching_chains` + """Find all paths of nodes that match the list of conditions. + When a sequence of conditions is fulfilled for a path of nodes, + add the path of nodes to the returned `matching_paths` Args: - node - node to check if current `pattern` matches for - pattern_list - will attempt to match first pattern against `node` + node - node to check if current `condition` matches for + condition_list - first condition in list will attempt match `node` matching_nodes - list of matched nodes so far (builds up recursively) - pattern_match_count - the number of matches on current pattern so far + condition_match_count - the number of matches on current condition so far - Return: list of lists of AttackGraphNodes that match the pattern + Return: list of lists of AttackGraphNodes that match the condition """ - # Init chain lists if None - current_chain = [] if current_chain is None else current_chain - matching_chains = [] if matching_chains is None else matching_chains + # Init path lists if None + current_path = [] if current_path is None else current_path + matching_paths = [] if matching_paths is None else matching_paths - current_pattern = pattern_list[0] + current_exp = condition_list[0] - if current_pattern.matches(node): - # Current node matches, add to current_chain and increment match_count - current_chain.append(node) - pattern_match_count += 1 + if current_exp.matches(node): + # Current node matches, add to current_path and increment match_count + current_path.append(node) + condition_match_count += 1 - if len(pattern_list) == 1 \ - and not current_pattern.must_match_again(pattern_match_count): - # This is the last pattern in the chain, - # and the current chain is matching - matching_chains.append(current_chain) + if len(condition_list) == 1 \ + and not current_exp.must_match_again(condition_match_count): + # This is the last condition in the path, + # and the current path is matching + matching_paths.append(current_path) - elif current_pattern.can_match_again(pattern_match_count): - # Pattern has matches left, run recursively with current pattern + elif current_exp.can_match_again(condition_match_count): + # Pattern has matches left, run recursively with current condition for child in node.children: - matching_chains = find_matches_recursively( + matching_paths = find_matches_recursively( child, - pattern_list, - current_chain=current_chain, - matching_chains=matching_chains, - pattern_match_count=pattern_match_count + condition_list, + current_path=current_path, + matching_paths=matching_paths, + condition_match_count=condition_match_count ) else: - # Pattern has run out of matches, must move on to next pattern + # Pattern has run out of matches, must move on to next condition for child in node.children: - matching_chains = find_matches_recursively( + matching_paths = find_matches_recursively( child, - pattern_list[1:], - current_chain=current_chain, - matching_chains=matching_chains + condition_list[1:], + current_path=current_path, + matching_paths=matching_paths ) else: - if not current_pattern.must_match_again(pattern_match_count)\ - and len(pattern_list) > 1: - # Node did not match current pattern, but we can try with - # the next pattern since current one is 'fulfilled' - matching_chains = find_matches_recursively( + if not current_exp.must_match_again(condition_match_count)\ + and len(condition_list) > 1: + # Node did not match current condition, but we can try with + # the next condition since current one is 'fulfilled' + matching_paths = find_matches_recursively( node, - pattern_list[1:], - current_chain=current_chain, - matching_chains=matching_chains + condition_list[1:], + current_path=current_path, + matching_paths=matching_paths ) - return matching_chains + return matching_paths diff --git a/tests/patterns/test_attackgraph_patterns.py b/tests/patterns/test_attackgraph_patterns.py index 93edcde9..f3ddc4f0 100644 --- a/tests/patterns/test_attackgraph_patterns.py +++ b/tests/patterns/test_attackgraph_patterns.py @@ -4,7 +4,7 @@ from maltoolbox.attackgraph import AttackGraph import maltoolbox.patterns.attackgraph_patterns as attackgraph_patterns -from maltoolbox.patterns.attackgraph_patterns import AttackGraphPattern +from maltoolbox.patterns.attackgraph_patterns import SearchPattern, SearchCondition from test_model import create_application_asset, create_association @@ -38,19 +38,19 @@ def example_attackgraph(corelang_spec, model: Model): def test_attackgraph_find_pattern(example_attackgraph): """Test a simple pattern""" - patterns = [ - AttackGraphPattern( - attributes=[('name', 'attemptRead')], - min_repeated=1, max_repeated=1 - ), - AttackGraphPattern( - attributes=[('name', 'successfulRead')], - min_repeated=1, max_repeated=1 - ), - AttackGraphPattern( - attributes=[('name', 'read')], - min_repeated=1, max_repeated=1 - ), - ] - chains = attackgraph_patterns.find_in_graph(example_attackgraph, patterns) + pattern = SearchPattern( + [ + SearchCondition( + lambda n : n.name == "attemptRead" + ), + SearchCondition( + lambda n : n.name == "successfulRead" + ), + SearchCondition( + lambda n : n.name == "read" + ) + ] + ) + + chains = pattern.find_matches(example_attackgraph) breakpoint() \ No newline at end of file From 1b7c5db71ab7bd733304e77b7a2cf24dfd6c9d34 Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Fri, 17 May 2024 10:44:23 +0200 Subject: [PATCH 08/24] Docstrings, formatting --- maltoolbox/patterns/attackgraph_patterns.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/maltoolbox/patterns/attackgraph_patterns.py b/maltoolbox/patterns/attackgraph_patterns.py index 3f39400a..fccfa5ac 100644 --- a/maltoolbox/patterns/attackgraph_patterns.py +++ b/maltoolbox/patterns/attackgraph_patterns.py @@ -4,25 +4,27 @@ from dataclasses import dataclass, field from maltoolbox.attackgraph import AttackGraph, AttackGraphNode from typing import Callable + class SearchPattern: """A pattern consists of conditions, the conditions are used - to find matching paths in an attack graph""" - conditions: list[Condition] + to find matching sequence of nodes in an AttackGraph.""" + conditions: list[SearchCondition] def __init__(self, conditions): self.conditions = conditions def find_matches(self, graph: AttackGraph): """Search through a graph for a pattern using - its conditions, and return matching paths of nodes + its conditions, and return sequences of nodes + that match all the conditions in the pattern Args: - graph - The graph to search in + graph - The AttackGraph to search in - Return: list[list[AttackGraphNode]] of matching paths + Return: list[list[AttackGraphNode]] matching paths of Nodes """ - # Find the starting nodes + # Find the starting nodes which match the first condition condition = self.conditions[0] starting_nodes = [] for node in graph.nodes: @@ -39,7 +41,7 @@ def find_matches(self, graph: AttackGraph): @dataclass class SearchCondition: - """A filter has a condition that has to be true for a node to match""" + """A condition that has to be true for a node to match""" # `matches` should be a lambda that takes node as input and returns bool # If lamdba returns True for a node, the node matches @@ -61,12 +63,12 @@ def must_match_again(self, num_matches): def find_matches_recursively( node: AttackGraphNode, - condition_list: list[Condition], + condition_list: list[SearchCondition], current_path=None, matching_paths=None, condition_match_count=0 ): - """Find all paths of nodes that match the list of conditions. + """Find all sequences of nodes that match the list of conditions. When a sequence of conditions is fulfilled for a path of nodes, add the path of nodes to the returned `matching_paths` @@ -78,6 +80,7 @@ def find_matches_recursively( Return: list of lists of AttackGraphNodes that match the condition """ + # Init path lists if None current_path = [] if current_path is None else current_path matching_paths = [] if matching_paths is None else matching_paths From b2f3d2e665d9784441b67016ab033f6d9011baf1 Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Fri, 17 May 2024 16:25:10 +0200 Subject: [PATCH 09/24] Default to not greedy pattern condition --- maltoolbox/patterns/attackgraph_patterns.py | 39 ++++++++++++++------- tests/patterns/test_attackgraph_patterns.py | 28 +++++++++++---- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/maltoolbox/patterns/attackgraph_patterns.py b/maltoolbox/patterns/attackgraph_patterns.py index fccfa5ac..06322729 100644 --- a/maltoolbox/patterns/attackgraph_patterns.py +++ b/maltoolbox/patterns/attackgraph_patterns.py @@ -1,13 +1,13 @@ """Utilities for finding patterns in the AttackGraph""" from __future__ import annotations -from dataclasses import dataclass, field -from maltoolbox.attackgraph import AttackGraph, AttackGraphNode +from dataclasses import dataclass from typing import Callable +from maltoolbox.attackgraph import AttackGraph, AttackGraphNode class SearchPattern: """A pattern consists of conditions, the conditions are used - to find matching sequence of nodes in an AttackGraph.""" + to find all matching sequences of nodes in an AttackGraph.""" conditions: list[SearchCondition] def __init__(self, conditions): @@ -47,6 +47,7 @@ class SearchCondition: # If lamdba returns True for a node, the node matches # If the lamdba returns False for a node, the node does not match matches: Callable[[AttackGraphNode], bool] + greedy: bool = False # It is possible to require/allow a Condition to repeat min_repeated: int = 1 @@ -80,27 +81,39 @@ def find_matches_recursively( Return: list of lists of AttackGraphNodes that match the condition """ - # Init path lists if None - current_path = [] if current_path is None else current_path - matching_paths = [] if matching_paths is None else matching_paths + current_path = [] if current_path is None else list(current_path) + matching_paths = [] if matching_paths is None else list(matching_paths) - current_exp = condition_list[0] + curr_cond = condition_list[0] + next_condition = condition_list[1] if len(condition_list) > 1 else None - if current_exp.matches(node): + if curr_cond.matches(node): # Current node matches, add to current_path and increment match_count current_path.append(node) condition_match_count += 1 - if len(condition_list) == 1 \ - and not current_exp.must_match_again(condition_match_count): + if next_condition is None \ + and not curr_cond.must_match_again(condition_match_count): # This is the last condition in the path, # and the current path is matching matching_paths.append(current_path) - elif current_exp.can_match_again(condition_match_count): - # Pattern has matches left, run recursively with current condition + elif curr_cond.can_match_again(condition_match_count): + # Pattern has matches left + for child in node.children: + # If curr_cond not greedy and next condition matches child + # move to next condition, otherwise continue with current + move_to_next_condition = ( + not curr_cond.greedy and + next_condition.matches(child) and + not curr_cond.must_match_again(condition_match_count) + ) + if move_to_next_condition: + condition_list = condition_list[1:] + condition_match_count = 0 + matching_paths = find_matches_recursively( child, condition_list, @@ -118,7 +131,7 @@ def find_matches_recursively( matching_paths=matching_paths ) else: - if not current_exp.must_match_again(condition_match_count)\ + if not curr_cond.must_match_again(condition_match_count)\ and len(condition_list) > 1: # Node did not match current condition, but we can try with # the next condition since current one is 'fulfilled' diff --git a/tests/patterns/test_attackgraph_patterns.py b/tests/patterns/test_attackgraph_patterns.py index f3ddc4f0..96f0490f 100644 --- a/tests/patterns/test_attackgraph_patterns.py +++ b/tests/patterns/test_attackgraph_patterns.py @@ -2,8 +2,7 @@ import pytest from maltoolbox.model import Model, AttackerAttachment from maltoolbox.attackgraph import AttackGraph - -import maltoolbox.patterns.attackgraph_patterns as attackgraph_patterns +import math from maltoolbox.patterns.attackgraph_patterns import SearchPattern, SearchCondition from test_model import create_application_asset, create_association @@ -41,16 +40,31 @@ def test_attackgraph_find_pattern(example_attackgraph): pattern = SearchPattern( [ SearchCondition( - lambda n : n.name == "attemptRead" + lambda n : n.name == "attemptModify" ), SearchCondition( - lambda n : n.name == "successfulRead" + lambda n : True, + min_repeated=1, max_repeated=math.inf ), SearchCondition( - lambda n : n.name == "read" + lambda n : n.name == "attemptRead" ) ] ) - chains = pattern.find_matches(example_attackgraph) - breakpoint() \ No newline at end of file + paths = pattern.find_matches(example_attackgraph) + + assert paths + # Make sure the paths match the pattern + for path in paths: + conditions = list(pattern.conditions) + num_matches_curr_condition = 0 + for node in path: + if conditions[0].matches(node): + num_matches_curr_condition += 1 + elif not conditions[0].must_match_again( + num_matches_curr_condition): + conditions.pop(0) + num_matches_curr_condition = 0 + else: + assert False, "Chain does not match pattern conditions" From 704d1835811490a45c2aeddc597f69d28e58580f Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Mon, 20 May 2024 10:19:26 +0200 Subject: [PATCH 10/24] Docstring aligning --- maltoolbox/patterns/attackgraph_patterns.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/maltoolbox/patterns/attackgraph_patterns.py b/maltoolbox/patterns/attackgraph_patterns.py index 06322729..fd8ae821 100644 --- a/maltoolbox/patterns/attackgraph_patterns.py +++ b/maltoolbox/patterns/attackgraph_patterns.py @@ -74,10 +74,10 @@ def find_matches_recursively( add the path of nodes to the returned `matching_paths` Args: - node - node to check if current `condition` matches for - condition_list - first condition in list will attempt match `node` - matching_nodes - list of matched nodes so far (builds up recursively) - condition_match_count - the number of matches on current condition so far + node - node to check if current `condition` matches for + condition_list - first condition in list will attempt match `node` + matching_nodes - list of matched nodes so far (recursively built) + condition_match_count - number of matches on current condition so far Return: list of lists of AttackGraphNodes that match the condition """ From 4b131f8ecca7c28198461e220f5077ee78b70acd Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Mon, 20 May 2024 14:28:14 +0200 Subject: [PATCH 11/24] Variable renaming --- maltoolbox/patterns/attackgraph_patterns.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/maltoolbox/patterns/attackgraph_patterns.py b/maltoolbox/patterns/attackgraph_patterns.py index fd8ae821..96fd56e1 100644 --- a/maltoolbox/patterns/attackgraph_patterns.py +++ b/maltoolbox/patterns/attackgraph_patterns.py @@ -86,17 +86,17 @@ def find_matches_recursively( matching_paths = [] if matching_paths is None else list(matching_paths) curr_cond = condition_list[0] - next_condition = condition_list[1] if len(condition_list) > 1 else None + next_cond = condition_list[1] if len(condition_list) > 1 else None if curr_cond.matches(node): # Current node matches, add to current_path and increment match_count current_path.append(node) condition_match_count += 1 - if next_condition is None \ + if next_cond is None \ and not curr_cond.must_match_again(condition_match_count): # This is the last condition in the path, - # and the current path is matching + # and the current path is fulfilled matching_paths.append(current_path) elif curr_cond.can_match_again(condition_match_count): @@ -107,7 +107,7 @@ def find_matches_recursively( # move to next condition, otherwise continue with current move_to_next_condition = ( not curr_cond.greedy and - next_condition.matches(child) and + next_cond.matches(child) and not curr_cond.must_match_again(condition_match_count) ) if move_to_next_condition: From 654a545f676d302862a55756713524291d0ce2c2 Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Thu, 23 May 2024 15:47:59 +0200 Subject: [PATCH 12/24] Rename patterns -> patternfinder --- maltoolbox/{patterns => patternfinder}/__init__.py | 0 .../{patterns => patternfinder}/attackgraph_patterns.py | 0 .../{patterns => patternfinder}/test_attackgraph_patterns.py | 5 ++++- 3 files changed, 4 insertions(+), 1 deletion(-) rename maltoolbox/{patterns => patternfinder}/__init__.py (100%) rename maltoolbox/{patterns => patternfinder}/attackgraph_patterns.py (100%) rename tests/{patterns => patternfinder}/test_attackgraph_patterns.py (95%) diff --git a/maltoolbox/patterns/__init__.py b/maltoolbox/patternfinder/__init__.py similarity index 100% rename from maltoolbox/patterns/__init__.py rename to maltoolbox/patternfinder/__init__.py diff --git a/maltoolbox/patterns/attackgraph_patterns.py b/maltoolbox/patternfinder/attackgraph_patterns.py similarity index 100% rename from maltoolbox/patterns/attackgraph_patterns.py rename to maltoolbox/patternfinder/attackgraph_patterns.py diff --git a/tests/patterns/test_attackgraph_patterns.py b/tests/patternfinder/test_attackgraph_patterns.py similarity index 95% rename from tests/patterns/test_attackgraph_patterns.py rename to tests/patternfinder/test_attackgraph_patterns.py index 96f0490f..79a482ef 100644 --- a/tests/patterns/test_attackgraph_patterns.py +++ b/tests/patternfinder/test_attackgraph_patterns.py @@ -3,7 +3,10 @@ from maltoolbox.model import Model, AttackerAttachment from maltoolbox.attackgraph import AttackGraph import math -from maltoolbox.patterns.attackgraph_patterns import SearchPattern, SearchCondition + +from maltoolbox.patternfinder.attackgraph_patterns import ( + SearchPattern, SearchCondition +) from test_model import create_application_asset, create_association From a5003d6fe3aba2e99b4cf9f5db15942482796dfb Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Fri, 24 May 2024 16:12:22 +0200 Subject: [PATCH 13/24] Rework structure of find_matches_recursively to find all paths --- .../patternfinder/attackgraph_patterns.py | 74 ++++------ .../test_attackgraph_patterns.py | 134 ++++++++++++++++-- 2 files changed, 153 insertions(+), 55 deletions(-) diff --git a/maltoolbox/patternfinder/attackgraph_patterns.py b/maltoolbox/patternfinder/attackgraph_patterns.py index 96fd56e1..3e6504f6 100644 --- a/maltoolbox/patternfinder/attackgraph_patterns.py +++ b/maltoolbox/patternfinder/attackgraph_patterns.py @@ -1,7 +1,8 @@ """Utilities for finding patterns in the AttackGraph""" from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field +from itertools import count from typing import Callable from maltoolbox.attackgraph import AttackGraph, AttackGraphNode @@ -85,61 +86,48 @@ def find_matches_recursively( current_path = [] if current_path is None else list(current_path) matching_paths = [] if matching_paths is None else list(matching_paths) - curr_cond = condition_list[0] - next_cond = condition_list[1] if len(condition_list) > 1 else None + curr_cond, *next_conds = condition_list + + if node in current_path: + # Stop the chain, infinite loop + return matching_paths + + if next_conds and not curr_cond.must_match_again(condition_match_count): + # Try next condition for current node if current is fulfilled + matching_paths = find_matches_recursively( + node, + next_conds, + current_path=list(current_path), + matching_paths=list(matching_paths) + ) if curr_cond.matches(node): # Current node matches, add to current_path and increment match_count current_path.append(node) condition_match_count += 1 - if next_cond is None \ - and not curr_cond.must_match_again(condition_match_count): - # This is the last condition in the path, - # and the current path is fulfilled - matching_paths.append(current_path) - - elif curr_cond.can_match_again(condition_match_count): - # Pattern has matches left - + if next_conds: + # If there are more conditions, try next one for all children for child in node.children: - # If curr_cond not greedy and next condition matches child - # move to next condition, otherwise continue with current - move_to_next_condition = ( - not curr_cond.greedy and - next_cond.matches(child) and - not curr_cond.must_match_again(condition_match_count) - ) - if move_to_next_condition: - condition_list = condition_list[1:] - condition_match_count = 0 - matching_paths = find_matches_recursively( child, - condition_list, - current_path=current_path, - matching_paths=matching_paths, - condition_match_count=condition_match_count + next_conds, + current_path=list(current_path), + matching_paths=list(matching_paths), ) - else: - # Pattern has run out of matches, must move on to next condition + if curr_cond.can_match_again(condition_match_count): + # If we can match current condition again, try for all children for child in node.children: matching_paths = find_matches_recursively( child, - condition_list[1:], - current_path=current_path, - matching_paths=matching_paths + [curr_cond] + next_conds, + current_path=list(current_path), + matching_paths=list(matching_paths), + condition_match_count=condition_match_count ) - else: - if not curr_cond.must_match_again(condition_match_count)\ - and len(condition_list) > 1: - # Node did not match current condition, but we can try with - # the next condition since current one is 'fulfilled' - matching_paths = find_matches_recursively( - node, - condition_list[1:], - current_path=current_path, - matching_paths=matching_paths - ) + + if not next_conds and current_path not in matching_paths: + # Congrats - matched a full unique search pattern! + matching_paths.append(current_path) return matching_paths diff --git a/tests/patternfinder/test_attackgraph_patterns.py b/tests/patternfinder/test_attackgraph_patterns.py index 79a482ef..bd504c3f 100644 --- a/tests/patternfinder/test_attackgraph_patterns.py +++ b/tests/patternfinder/test_attackgraph_patterns.py @@ -1,7 +1,7 @@ """Tests for attack graph pattern matching""" import pytest from maltoolbox.model import Model, AttackerAttachment -from maltoolbox.attackgraph import AttackGraph +from maltoolbox.attackgraph import AttackGraph, AttackGraphNode import math from maltoolbox.patternfinder.attackgraph_patterns import ( @@ -38,36 +38,146 @@ def example_attackgraph(corelang_spec, model: Model): return AttackGraph(lang_spec=corelang_spec, model=model) -def test_attackgraph_find_pattern(example_attackgraph): +def test_attackgraph_find_pattern_example_graph(example_attackgraph): """Test a simple pattern""" pattern = SearchPattern( [ SearchCondition( - lambda n : n.name == "attemptModify" + lambda n : n.name == "attemptRead" ), SearchCondition( - lambda n : True, - min_repeated=1, max_repeated=math.inf + lambda n : n.name == "successfulRead" ), SearchCondition( - lambda n : n.name == "attemptRead" + lambda n : n.name == "read" ) ] ) paths = pattern.find_matches(example_attackgraph) - assert paths # Make sure the paths match the pattern + assert paths for path in paths: conditions = list(pattern.conditions) num_matches_curr_condition = 0 - for node in path: - if conditions[0].matches(node): + curr_condition = conditions.pop(0) + curr_node = path.pop(0) + + while path or conditions: + + if curr_condition.matches(curr_node): num_matches_curr_condition += 1 - elif not conditions[0].must_match_again( - num_matches_curr_condition): - conditions.pop(0) + curr_node = path.pop(0) + + elif not curr_condition.must_match_again( + num_matches_curr_condition + ): + curr_condition = conditions.pop(0) num_matches_curr_condition = 0 + else: assert False, "Chain does not match pattern conditions" + + +def test_attackgraph_find_multiple(): + """Create a simple AttackGraph, find pattern with more than one match + + Node1 + / \ + Node2 Node3 + / / \ + Node4 Node5 Node6 + / + Node7 + """ + attack_graph = AttackGraph() + + # Create the graph + node1 =AttackGraphNode("Application:Node1", "and", "Node1", {}) + node2 =AttackGraphNode("Application:Node2", "and", "Node2", {}, parents=[node1]) + node3 =AttackGraphNode("Application:Node3", "and", "Node3", {}, parents=[node1]) + node1.children = [node2, node3] + node4 =AttackGraphNode("Application:Node4", "and", "Node4", {}, parents=[node2]) + node2.children = [node4] + node5 =AttackGraphNode("Application:Node5", "and", "Node5", {}, parents=[node3]) + node6 =AttackGraphNode("Application:Node6", "and", "Node6", {}, parents=[node3]) + node3.children = [node5, node6] + node7 =AttackGraphNode("Application:Node7", "and", "Node7", {}, parents=[node4]) + node4.children = [node7] + attack_graph.nodes = [node1, node2, node3, node4, node5, node6, node7] + + # Create the search pattern from Node1 to either Node6 or Node7 + pattern = SearchPattern( + [ + SearchCondition( + lambda node: node.name == "Node1" + ), + SearchCondition( + lambda _: True, # Match any node any number of times + max_repeated=math.inf + ), + SearchCondition( + lambda node: node.name in ("Node6", "Node7") + ) + ] + ) + paths = pattern.find_matches(attack_graph) + + # Make sure we find two paths: (Node1->Node7) and (Node1->Node6) + assert len(paths) == 2 + + assert [node1, node2, node4, node7] in paths + assert [node1, node3, node6] in paths + + +def test_attackgraph_find_multiple_same_subpath(): + """Create a simple AttackGraph, find paths which match pattern + where several matching paths share same subpath in the graph + + Node1 + / \ + Node2 Node3 + / \ + Node4 Node5 + """ + attack_graph = AttackGraph() + + # Create the graph + node1 = AttackGraphNode("Application:Node1", "and", "Node1", {}) + node2 = AttackGraphNode( + "Application:Node2", "and", "Node2", {}, parents=[node1]) + node3 = AttackGraphNode( + "Application:Node3", "and", "Node3", {}, parents=[node1]) + node1.children = [node2, node3] + node4 = AttackGraphNode( + "Application:Node4", "and", "Node4", {}, parents=[node2]) + node2.children = [node4] + node5 = AttackGraphNode( + "Application:Node5", "and", "Node5", {}, parents=[node3]) + node3.children = [node5] + attack_graph.nodes = [node1, node2, node3, node4, node5] + + # Create the search pattern to find paths from Node1 to any node + pattern = SearchPattern( + [ + SearchCondition( + lambda node: node.name == "Node1" + ), + SearchCondition( + lambda node: True, + max_repeated=math.inf, + min_repeated=0, + ), + SearchCondition( + lambda node: node.name in ("Node2", "Node3", "Node4", "Node5") + ) + ] + ) + paths = pattern.find_matches(attack_graph) + + # Make sure we find two paths: (Node1->Node7) and (Node1->Node6) + assert [node1, node2, node4] in paths + assert [node1, node3, node5] in paths + assert [node1, node2] in paths + assert [node1, node3] in paths From 88b1d70320af40ae237bd7f0388701cea27cda91 Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Mon, 27 May 2024 13:18:18 +0200 Subject: [PATCH 14/24] Minor improvements --- .../patternfinder/attackgraph_patterns.py | 26 ++++++++++--------- .../test_attackgraph_patterns.py | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/maltoolbox/patternfinder/attackgraph_patterns.py b/maltoolbox/patternfinder/attackgraph_patterns.py index 3e6504f6..e78c042f 100644 --- a/maltoolbox/patternfinder/attackgraph_patterns.py +++ b/maltoolbox/patternfinder/attackgraph_patterns.py @@ -1,8 +1,7 @@ """Utilities for finding patterns in the AttackGraph""" from __future__ import annotations -from dataclasses import dataclass, field -from itertools import count +from dataclasses import dataclass from typing import Callable from maltoolbox.attackgraph import AttackGraph, AttackGraphNode @@ -70,9 +69,10 @@ def find_matches_recursively( matching_paths=None, condition_match_count=0 ): - """Find all sequences of nodes that match the list of conditions. + """Find all paths of nodes that match the list of conditions. When a sequence of conditions is fulfilled for a path of nodes, add the path of nodes to the returned `matching_paths` + The function runs recursively down all paths of children nodes. Args: node - node to check if current `condition` matches for @@ -80,9 +80,10 @@ def find_matches_recursively( matching_nodes - list of matched nodes so far (recursively built) condition_match_count - number of matches on current condition so far - Return: list of lists of AttackGraphNodes that match the condition + Return: list of lists (paths) of AttackGraphNodes that match the condition """ - # Init path lists if None + + # Init path lists if None, or copy/init into new lists for each iteration current_path = [] if current_path is None else list(current_path) matching_paths = [] if matching_paths is None else list(matching_paths) @@ -93,12 +94,13 @@ def find_matches_recursively( return matching_paths if next_conds and not curr_cond.must_match_again(condition_match_count): - # Try next condition for current node if current is fulfilled + # Try next condition for current node if there are more and + # current condition is already fulfilled. matching_paths = find_matches_recursively( node, next_conds, - current_path=list(current_path), - matching_paths=list(matching_paths) + current_path=current_path, + matching_paths=matching_paths ) if curr_cond.matches(node): @@ -112,8 +114,8 @@ def find_matches_recursively( matching_paths = find_matches_recursively( child, next_conds, - current_path=list(current_path), - matching_paths=list(matching_paths), + current_path=current_path, + matching_paths=matching_paths, ) if curr_cond.can_match_again(condition_match_count): # If we can match current condition again, try for all children @@ -121,8 +123,8 @@ def find_matches_recursively( matching_paths = find_matches_recursively( child, [curr_cond] + next_conds, - current_path=list(current_path), - matching_paths=list(matching_paths), + current_path=current_path, + matching_paths=matching_paths, condition_match_count=condition_match_count ) diff --git a/tests/patternfinder/test_attackgraph_patterns.py b/tests/patternfinder/test_attackgraph_patterns.py index bd504c3f..e0767c32 100644 --- a/tests/patternfinder/test_attackgraph_patterns.py +++ b/tests/patternfinder/test_attackgraph_patterns.py @@ -176,7 +176,7 @@ def test_attackgraph_find_multiple_same_subpath(): ) paths = pattern.find_matches(attack_graph) - # Make sure we find two paths: (Node1->Node7) and (Node1->Node6) + # Make sure we find all paths assert [node1, node2, node4] in paths assert [node1, node3, node5] in paths assert [node1, node2] in paths From aea23641c166699deeab98d7f1e871d5a78f0e9a Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Wed, 29 May 2024 15:05:15 +0200 Subject: [PATCH 15/24] Adapt to new LanguageGraph constructor --- tests/conftest.py | 9 --------- tests/patternfinder/test_attackgraph_patterns.py | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index fb886a3e..df3c3e7b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,15 +36,6 @@ def corelang_lang_graph(): mar_file_path = path_testdata("org.mal-lang.coreLang-1.0.0.mar") return LanguageGraph.from_mar_archive(mar_file_path) -## Fixtures (can be ingested into tests) - -@pytest.fixture -def corelang_spec(): - """Fixture that returns the coreLang language specification as dict""" - mar_file_path = path_testdata("org.mal-lang.coreLang-1.0.0.mar") - return specification.load_language_specification_from_mar(mar_file_path) - - @pytest.fixture def model(corelang_lang_graph): """Fixture that generates a model for tests diff --git a/tests/patternfinder/test_attackgraph_patterns.py b/tests/patternfinder/test_attackgraph_patterns.py index e0767c32..3661c052 100644 --- a/tests/patternfinder/test_attackgraph_patterns.py +++ b/tests/patternfinder/test_attackgraph_patterns.py @@ -11,7 +11,7 @@ from test_model import create_application_asset, create_association @pytest.fixture -def example_attackgraph(corelang_spec, model: Model): +def example_attackgraph(corelang_lang_graph, model: Model): """Fixture that generates an example attack graph Uses coreLang specification and model with two applications @@ -35,7 +35,7 @@ def example_attackgraph(corelang_spec, model: Model): ] model.add_attacker(attacker) - return AttackGraph(lang_spec=corelang_spec, model=model) + return AttackGraph(lang_graph=corelang_lang_graph, model=model) def test_attackgraph_find_pattern_example_graph(example_attackgraph): From 3182e54b5f8bd3dc868148b403d4584805a9728d Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Wed, 7 Aug 2024 12:52:33 +0200 Subject: [PATCH 16/24] Adapt to new signature for attackgraphnode and create_association --- .../test_attackgraph_patterns.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/patternfinder/test_attackgraph_patterns.py b/tests/patternfinder/test_attackgraph_patterns.py index 3661c052..699e6273 100644 --- a/tests/patternfinder/test_attackgraph_patterns.py +++ b/tests/patternfinder/test_attackgraph_patterns.py @@ -26,7 +26,7 @@ def example_attackgraph(corelang_lang_graph, model: Model): model.add_asset(app2) # Create association between app1 and app2 - assoc = create_association(model, from_assets=[app1], to_assets=[app2]) + assoc = create_association(model, left_assets=[app1], right_assets=[app2]) model.add_association(assoc) attacker = AttackerAttachment() @@ -94,16 +94,16 @@ def test_attackgraph_find_multiple(): attack_graph = AttackGraph() # Create the graph - node1 =AttackGraphNode("Application:Node1", "and", "Node1", {}) - node2 =AttackGraphNode("Application:Node2", "and", "Node2", {}, parents=[node1]) - node3 =AttackGraphNode("Application:Node3", "and", "Node3", {}, parents=[node1]) + node1 = AttackGraphNode("and", "Node1", {}) + node2 = AttackGraphNode("and", "Node2", {}, parents=[node1]) + node3 = AttackGraphNode("and", "Node3", {}, parents=[node1]) node1.children = [node2, node3] - node4 =AttackGraphNode("Application:Node4", "and", "Node4", {}, parents=[node2]) + node4 = AttackGraphNode("and", "Node4", {}, parents=[node2]) node2.children = [node4] - node5 =AttackGraphNode("Application:Node5", "and", "Node5", {}, parents=[node3]) - node6 =AttackGraphNode("Application:Node6", "and", "Node6", {}, parents=[node3]) + node5 = AttackGraphNode("and", "Node5", {}, parents=[node3]) + node6 = AttackGraphNode("and", "Node6", {}, parents=[node3]) node3.children = [node5, node6] - node7 =AttackGraphNode("Application:Node7", "and", "Node7", {}, parents=[node4]) + node7 = AttackGraphNode("and", "Node7", {}, parents=[node4]) node4.children = [node7] attack_graph.nodes = [node1, node2, node3, node4, node5, node6, node7] @@ -144,17 +144,17 @@ def test_attackgraph_find_multiple_same_subpath(): attack_graph = AttackGraph() # Create the graph - node1 = AttackGraphNode("Application:Node1", "and", "Node1", {}) + node1 = AttackGraphNode("and", "Node1", {}) node2 = AttackGraphNode( - "Application:Node2", "and", "Node2", {}, parents=[node1]) + "and", "Node2", {}, parents=[node1]) node3 = AttackGraphNode( - "Application:Node3", "and", "Node3", {}, parents=[node1]) + "and", "Node3", {}, parents=[node1]) node1.children = [node2, node3] node4 = AttackGraphNode( - "Application:Node4", "and", "Node4", {}, parents=[node2]) + "and", "Node4", {}, parents=[node2]) node2.children = [node4] node5 = AttackGraphNode( - "Application:Node5", "and", "Node5", {}, parents=[node3]) + "and", "Node5", {}, parents=[node3]) node3.children = [node5] attack_graph.nodes = [node1, node2, node3, node4, node5] From a36a8a3d22168148f4c0ce1d4e15566e7c0ff18c Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Wed, 7 Aug 2024 12:52:45 +0200 Subject: [PATCH 17/24] Add predefined ANY condition --- maltoolbox/patternfinder/attackgraph_patterns.py | 4 +++- tests/patternfinder/test_attackgraph_patterns.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/maltoolbox/patternfinder/attackgraph_patterns.py b/maltoolbox/patternfinder/attackgraph_patterns.py index e78c042f..ebc9e26b 100644 --- a/maltoolbox/patternfinder/attackgraph_patterns.py +++ b/maltoolbox/patternfinder/attackgraph_patterns.py @@ -36,13 +36,15 @@ def find_matches(self, graph: AttackGraph): matching_paths.extend( find_matches_recursively(node, self.conditions) ) - return matching_paths @dataclass class SearchCondition: """A condition that has to be true for a node to match""" + # Predefined search conditions + ANY = lambda _: True + # `matches` should be a lambda that takes node as input and returns bool # If lamdba returns True for a node, the node matches # If the lamdba returns False for a node, the node does not match diff --git a/tests/patternfinder/test_attackgraph_patterns.py b/tests/patternfinder/test_attackgraph_patterns.py index 699e6273..52ddeeb0 100644 --- a/tests/patternfinder/test_attackgraph_patterns.py +++ b/tests/patternfinder/test_attackgraph_patterns.py @@ -114,8 +114,8 @@ def test_attackgraph_find_multiple(): lambda node: node.name == "Node1" ), SearchCondition( - lambda _: True, # Match any node any number of times - max_repeated=math.inf + SearchCondition.ANY, # Match any node + max_repeated=math.inf # Any number of times ), SearchCondition( lambda node: node.name in ("Node6", "Node7") From cf6d99abaab794cc2610c36a9e5d56a1b43e299f Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Fri, 16 Aug 2024 11:15:57 +0200 Subject: [PATCH 18/24] Add another test for pattern matching --- .../test_attackgraph_patterns.py | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/patternfinder/test_attackgraph_patterns.py b/tests/patternfinder/test_attackgraph_patterns.py index 52ddeeb0..ae4d9a80 100644 --- a/tests/patternfinder/test_attackgraph_patterns.py +++ b/tests/patternfinder/test_attackgraph_patterns.py @@ -1,8 +1,10 @@ """Tests for attack graph pattern matching""" + +import math import pytest + from maltoolbox.model import Model, AttackerAttachment from maltoolbox.attackgraph import AttackGraph, AttackGraphNode -import math from maltoolbox.patternfinder.attackgraph_patterns import ( SearchPattern, SearchCondition @@ -181,3 +183,51 @@ def test_attackgraph_find_multiple_same_subpath(): assert [node1, node3, node5] in paths assert [node1, node2] in paths assert [node1, node3] in paths + + +def test_attackgraph_two_same_start_end_node(): + """Create a simple AttackGraph, find paths which match pattern + where both matching paths start and end att the same node + + Node1 + / \ + Node2 Node3 + \ / + Node4 + """ + attack_graph = AttackGraph() + + # Create the graph + node1 = AttackGraphNode("and", "Node1", {}) + assert node1.name == "Node1" + node2 = AttackGraphNode( + "and", "Node2", {}, parents=[node1]) + node3 = AttackGraphNode( + "and", "Node3", {}, parents=[node1]) + node1.children = [node2, node3] + node4 = AttackGraphNode( + "and", "Node4", {}, parents=[node2, node3]) + node2.children = [node4] + node3.children = [node4] + + attack_graph.nodes = [node1, node2, node3, node4] + + # Create the search pattern to find paths from Node1 to any node + pattern = SearchPattern( + [ + SearchCondition( + lambda node: node.name == "Node1" + ), + SearchCondition( + SearchCondition.ANY + ), + SearchCondition( + lambda node: node.name == "Node4" + ) + ] + ) + paths = pattern.find_matches(attack_graph) + + # Make sure we find expected paths + assert [node1, node2, node4] in paths + assert [node1, node3, node4] in paths From 919afc4e2e02a603f72467a2c6c29bc5723f3c6f Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Thu, 29 Aug 2024 10:17:51 +0200 Subject: [PATCH 19/24] Optimization: matching_paths list never reinitialized --- maltoolbox/attackgraph/node.py | 2 + .../patternfinder/attackgraph_patterns.py | 12 +++--- .../test_attackgraph_patterns.py | 38 +++++-------------- 3 files changed, 17 insertions(+), 35 deletions(-) diff --git a/maltoolbox/attackgraph/node.py b/maltoolbox/attackgraph/node.py index a5884172..9c0eb223 100644 --- a/maltoolbox/attackgraph/node.py +++ b/maltoolbox/attackgraph/node.py @@ -33,6 +33,8 @@ class AttackGraphNode: # Optional extra metadata for AttackGraphNode extras: dict = field(default_factory=dict) + def __hash__(self) -> int: + return hash((self.id)) def to_dict(self) -> dict: """Convert node to dictionary""" diff --git a/maltoolbox/patternfinder/attackgraph_patterns.py b/maltoolbox/patternfinder/attackgraph_patterns.py index ebc9e26b..589686b6 100644 --- a/maltoolbox/patternfinder/attackgraph_patterns.py +++ b/maltoolbox/patternfinder/attackgraph_patterns.py @@ -67,9 +67,9 @@ def must_match_again(self, num_matches): def find_matches_recursively( node: AttackGraphNode, condition_list: list[SearchCondition], - current_path=None, - matching_paths=None, - condition_match_count=0 + current_path: tuple[AttackGraphNode] = None, + matching_paths: list[tuple[AttackGraphNode]] = None, + condition_match_count: int = 0 ): """Find all paths of nodes that match the list of conditions. When a sequence of conditions is fulfilled for a path of nodes, @@ -87,7 +87,7 @@ def find_matches_recursively( # Init path lists if None, or copy/init into new lists for each iteration current_path = [] if current_path is None else list(current_path) - matching_paths = [] if matching_paths is None else list(matching_paths) + matching_paths = set() if matching_paths is None else matching_paths curr_cond, *next_conds = condition_list @@ -130,8 +130,8 @@ def find_matches_recursively( condition_match_count=condition_match_count ) - if not next_conds and current_path not in matching_paths: + if not next_conds: # Congrats - matched a full unique search pattern! - matching_paths.append(current_path) + matching_paths.add(tuple(current_path)) # tuple is hashable return matching_paths diff --git a/tests/patternfinder/test_attackgraph_patterns.py b/tests/patternfinder/test_attackgraph_patterns.py index ae4d9a80..c6ab096d 100644 --- a/tests/patternfinder/test_attackgraph_patterns.py +++ b/tests/patternfinder/test_attackgraph_patterns.py @@ -57,29 +57,9 @@ def test_attackgraph_find_pattern_example_graph(example_attackgraph): ) paths = pattern.find_matches(example_attackgraph) - # Make sure the paths match the pattern - assert paths for path in paths: - conditions = list(pattern.conditions) - num_matches_curr_condition = 0 - curr_condition = conditions.pop(0) - curr_node = path.pop(0) - - while path or conditions: - - if curr_condition.matches(curr_node): - num_matches_curr_condition += 1 - curr_node = path.pop(0) - - elif not curr_condition.must_match_again( - num_matches_curr_condition - ): - curr_condition = conditions.pop(0) - num_matches_curr_condition = 0 - - else: - assert False, "Chain does not match pattern conditions" + assert [n.name for n in path] == ['attemptRead', 'successfulRead', 'read'] def test_attackgraph_find_multiple(): @@ -129,8 +109,8 @@ def test_attackgraph_find_multiple(): # Make sure we find two paths: (Node1->Node7) and (Node1->Node6) assert len(paths) == 2 - assert [node1, node2, node4, node7] in paths - assert [node1, node3, node6] in paths + assert (node1, node2, node4, node7) in paths + assert (node1, node3, node6) in paths def test_attackgraph_find_multiple_same_subpath(): @@ -179,10 +159,10 @@ def test_attackgraph_find_multiple_same_subpath(): paths = pattern.find_matches(attack_graph) # Make sure we find all paths - assert [node1, node2, node4] in paths - assert [node1, node3, node5] in paths - assert [node1, node2] in paths - assert [node1, node3] in paths + assert (node1, node2, node4) in paths + assert (node1, node3, node5) in paths + assert (node1, node2) in paths + assert (node1, node3) in paths def test_attackgraph_two_same_start_end_node(): @@ -229,5 +209,5 @@ def test_attackgraph_two_same_start_end_node(): paths = pattern.find_matches(attack_graph) # Make sure we find expected paths - assert [node1, node2, node4] in paths - assert [node1, node3, node4] in paths + assert (node1, node2, node4) in paths + assert (node1, node3, node4) in paths From f063ad23ed1c000adb3fb0bf8eea754aa2dd3be6 Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Thu, 29 Aug 2024 10:53:42 +0200 Subject: [PATCH 20/24] Simplify code --- maltoolbox/patternfinder/attackgraph_patterns.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/maltoolbox/patternfinder/attackgraph_patterns.py b/maltoolbox/patternfinder/attackgraph_patterns.py index 589686b6..839c032e 100644 --- a/maltoolbox/patternfinder/attackgraph_patterns.py +++ b/maltoolbox/patternfinder/attackgraph_patterns.py @@ -26,16 +26,12 @@ def find_matches(self, graph: AttackGraph): # Find the starting nodes which match the first condition condition = self.conditions[0] - starting_nodes = [] + matching_paths = [] for node in graph.nodes: if condition.matches(node): - starting_nodes.append(node) - - matching_paths = [] - for node in starting_nodes: - matching_paths.extend( - find_matches_recursively(node, self.conditions) - ) + matching_paths.extend( + find_matches_recursively(node, self.conditions) + ) return matching_paths @dataclass From 40e8d438eda9ce3db39939f4a6ad75d24d897487 Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Thu, 29 Aug 2024 11:31:34 +0200 Subject: [PATCH 21/24] Fix typehints --- maltoolbox/patternfinder/attackgraph_patterns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maltoolbox/patternfinder/attackgraph_patterns.py b/maltoolbox/patternfinder/attackgraph_patterns.py index 839c032e..ecd50895 100644 --- a/maltoolbox/patternfinder/attackgraph_patterns.py +++ b/maltoolbox/patternfinder/attackgraph_patterns.py @@ -63,8 +63,8 @@ def must_match_again(self, num_matches): def find_matches_recursively( node: AttackGraphNode, condition_list: list[SearchCondition], - current_path: tuple[AttackGraphNode] = None, - matching_paths: list[tuple[AttackGraphNode]] = None, + current_path: list[AttackGraphNode] | None = None, + matching_paths: set[tuple[AttackGraphNode,...]] | None = None, condition_match_count: int = 0 ): """Find all paths of nodes that match the list of conditions. From c4b378c141bbbd467e9fa1fe66841e47e2464920 Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Thu, 29 Aug 2024 13:03:13 +0200 Subject: [PATCH 22/24] Fix docstrings --- maltoolbox/patternfinder/attackgraph_patterns.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/maltoolbox/patternfinder/attackgraph_patterns.py b/maltoolbox/patternfinder/attackgraph_patterns.py index ecd50895..bc0dcf8d 100644 --- a/maltoolbox/patternfinder/attackgraph_patterns.py +++ b/maltoolbox/patternfinder/attackgraph_patterns.py @@ -75,10 +75,11 @@ def find_matches_recursively( Args: node - node to check if current `condition` matches for condition_list - first condition in list will attempt match `node` - matching_nodes - list of matched nodes so far (recursively built) + current_path - list of matched nodes so far (recursively built) + matching_paths - set of matched paths so far (recursively built) condition_match_count - number of matches on current condition so far - Return: list of lists (paths) of AttackGraphNodes that match the condition + Return: set of tuples (paths) of AttackGraphNodes that match the condition """ # Init path lists if None, or copy/init into new lists for each iteration From 799d2318a9b3068a5be97188ff17a634b74d1c2a Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Mon, 27 Jan 2025 14:20:03 +0100 Subject: [PATCH 23/24] Add docs about pattern finding --- docs/about/attackgraph.rst | 32 ++++++++++++++++++- ...box.patternfinder.attackgraph_patterns.rst | 7 ++++ docs/apidocs/maltoolbox.patternfinder.rst | 15 +++++++++ docs/apidocs/maltoolbox.rst | 1 + 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 docs/apidocs/maltoolbox.patternfinder.attackgraph_patterns.rst create mode 100644 docs/apidocs/maltoolbox.patternfinder.rst diff --git a/docs/about/attackgraph.rst b/docs/about/attackgraph.rst index 2d3dbafd..0bf3ca80 100644 --- a/docs/about/attackgraph.rst +++ b/docs/about/attackgraph.rst @@ -67,4 +67,34 @@ From AttackGraph file :func:`maltoolbox.attackgraph.attackgraph.AttackGraph.load Analyzers """"""""" -:mod:`maltoolbox.attackgraph.analyzers` contains analyzers for the attackgraph used to calculate viability and necessity. \ No newline at end of file +:mod:`maltoolbox.attackgraph.analyzers` contains analyzers for the attackgraph used to calculate viability and necessity. + + +Pattern matching +"""""""""""""""" + +:mod:`maltoolbox.patternfinder.attack_graph_patterns` contains a regex-like feature to search for patterns in an attack graph. + +Example: + +.. code-block:: python + + attack_graph: AttackGraph + + # Create the search pattern to find paths from Node1 to any node + pattern = SearchPattern( + [ + SearchCondition( + lambda node: node.name == "Node1" + ), + SearchCondition( + SearchCondition.ANY + ), + SearchCondition( + lambda node: node.name == "Node4" + ) + ] + ) + + # Returns a list of node paths that match + paths = pattern.find_matches(attack_graph) diff --git a/docs/apidocs/maltoolbox.patternfinder.attackgraph_patterns.rst b/docs/apidocs/maltoolbox.patternfinder.attackgraph_patterns.rst new file mode 100644 index 00000000..6acbcb33 --- /dev/null +++ b/docs/apidocs/maltoolbox.patternfinder.attackgraph_patterns.rst @@ -0,0 +1,7 @@ +maltoolbox.patternfinder.attackgraph\_patterns module +===================================================== + +.. automodule:: maltoolbox.patternfinder.attackgraph_patterns + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/apidocs/maltoolbox.patternfinder.rst b/docs/apidocs/maltoolbox.patternfinder.rst new file mode 100644 index 00000000..fe3857db --- /dev/null +++ b/docs/apidocs/maltoolbox.patternfinder.rst @@ -0,0 +1,15 @@ +maltoolbox.patternfinder package +================================ + +.. automodule:: maltoolbox.patternfinder + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + maltoolbox.patternfinder.attackgraph_patterns diff --git a/docs/apidocs/maltoolbox.rst b/docs/apidocs/maltoolbox.rst index be078e1a..b0973bf8 100644 --- a/docs/apidocs/maltoolbox.rst +++ b/docs/apidocs/maltoolbox.rst @@ -15,6 +15,7 @@ Subpackages maltoolbox.attackgraph maltoolbox.ingestors maltoolbox.language + maltoolbox.patternfinder maltoolbox.translators Submodules From 92326ed2f42d8d7beebeac8f488122cf4bea0640 Mon Sep 17 00:00:00 2001 From: Joakim Loxdal Date: Mon, 27 Jan 2025 14:28:56 +0100 Subject: [PATCH 24/24] Adapt pattern matching test to maltoolbox 0.2 --- .../test_attackgraph_patterns.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/patternfinder/test_attackgraph_patterns.py b/tests/patternfinder/test_attackgraph_patterns.py index c6ab096d..a6f22835 100644 --- a/tests/patternfinder/test_attackgraph_patterns.py +++ b/tests/patternfinder/test_attackgraph_patterns.py @@ -73,19 +73,19 @@ def test_attackgraph_find_multiple(): / Node7 """ - attack_graph = AttackGraph() + attack_graph = AttackGraph(None) # Create the graph - node1 = AttackGraphNode("and", "Node1", {}) - node2 = AttackGraphNode("and", "Node2", {}, parents=[node1]) - node3 = AttackGraphNode("and", "Node3", {}, parents=[node1]) + node1 = AttackGraphNode("and", None, "Node1", {}) + node2 = AttackGraphNode("and", None, "Node2", {}, parents=[node1]) + node3 = AttackGraphNode("and", None, "Node3", {}, parents=[node1]) node1.children = [node2, node3] - node4 = AttackGraphNode("and", "Node4", {}, parents=[node2]) + node4 = AttackGraphNode("and", None, "Node4", {}, parents=[node2]) node2.children = [node4] - node5 = AttackGraphNode("and", "Node5", {}, parents=[node3]) - node6 = AttackGraphNode("and", "Node6", {}, parents=[node3]) + node5 = AttackGraphNode("and", None, "Node5", {}, parents=[node3]) + node6 = AttackGraphNode("and", None, "Node6", {}, parents=[node3]) node3.children = [node5, node6] - node7 = AttackGraphNode("and", "Node7", {}, parents=[node4]) + node7 = AttackGraphNode("and", None, "Node7", {}, parents=[node4]) node4.children = [node7] attack_graph.nodes = [node1, node2, node3, node4, node5, node6, node7] @@ -123,20 +123,20 @@ def test_attackgraph_find_multiple_same_subpath(): / \ Node4 Node5 """ - attack_graph = AttackGraph() + attack_graph = AttackGraph(None) # Create the graph - node1 = AttackGraphNode("and", "Node1", {}) + node1 = AttackGraphNode("and", None, "Node1", {}) node2 = AttackGraphNode( - "and", "Node2", {}, parents=[node1]) + "and", None, "Node2", {}, parents=[node1]) node3 = AttackGraphNode( - "and", "Node3", {}, parents=[node1]) + "and", None, "Node3", {}, parents=[node1]) node1.children = [node2, node3] node4 = AttackGraphNode( - "and", "Node4", {}, parents=[node2]) + "and", None, "Node4", {}, parents=[node2]) node2.children = [node4] node5 = AttackGraphNode( - "and", "Node5", {}, parents=[node3]) + "and", None, "Node5", {}, parents=[node3]) node3.children = [node5] attack_graph.nodes = [node1, node2, node3, node4, node5] @@ -175,18 +175,18 @@ def test_attackgraph_two_same_start_end_node(): \ / Node4 """ - attack_graph = AttackGraph() + attack_graph = AttackGraph(None) # Create the graph - node1 = AttackGraphNode("and", "Node1", {}) + node1 = AttackGraphNode("and", None, "Node1", {}) assert node1.name == "Node1" node2 = AttackGraphNode( - "and", "Node2", {}, parents=[node1]) + "and", None, "Node2", {}, parents=[node1]) node3 = AttackGraphNode( - "and", "Node3", {}, parents=[node1]) + "and", None, "Node3", {}, parents=[node1]) node1.children = [node2, node3] node4 = AttackGraphNode( - "and", "Node4", {}, parents=[node2, node3]) + "and", None, "Node4", {}, parents=[node2, node3]) node2.children = [node4] node3.children = [node4]