-
Notifications
You must be signed in to change notification settings - Fork 2
Simplify MalSimulator and create wrappers for other interfaces #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 47 commits
Commits
Show all changes
87 commits
Select commit
Hold shift + click to select a range
737439f
Make malsim.agents a package and rename compute_action_from_dict to g…
mrkickling 2f0145f
Simplify MAL Simulator by removing observation creation, add separate…
mrkickling cbcdc44
Deprecate attacker_agent_class and defender_agent_class from scenario
mrkickling b7fbd8b
Update CLI to use new simulator structure
mrkickling 577039a
Update README with new scenario format
mrkickling c3cecaf
Update Attacker/DefenderEnv to use new MalSim and scenario format
mrkickling a73f65e
Update testdata and tests to work with new scenario format and simulator
mrkickling db42070
Inherit DecisionAgent in all decision agents
mrkickling b11c528
Fix cli: log issue when no action is selected
mrkickling 806b6a2
Implement and use MalSimulator.get_agent
mrkickling 894fb86
Move serialized obs simulator into a MalSimVectorizedObsEnv that inhe…
mrkickling a0ce2ff
Make MalSimVectorizedObsEnv a proper wrapper taking a simulator as on…
mrkickling 97a7273
Move logging utils for vectorized obs mal sim env to ./wrappers
mrkickling 44268bc
Comment adjustments
mrkickling dc3554c
Make agents_dict private _agents_dict
mrkickling aab11d0
_initialize_rewards -> _init_agent_rewards
mrkickling a1de11c
Add MalSimAgentView - a public read only interface for agent states i…
mrkickling 7489788
Adapt MalSimVectorizedObsEnv to new MalSimAgentView interface
mrkickling 1ad08bd
Fix bug: defenders need to perform action before attacker
mrkickling 3dc117c
Use kwarg 'action_mask' in searcher decision agents, use that in rest…
mrkickling a8596c9
Fix log stmt
mrkickling be7cd66
rename module agents.base_agent->agents.decision_agent to easier diff…
mrkickling 1c0f574
Rewrite searcher agents to use AttackGraphNodes instead of serialized…
mrkickling 91f3a02
Add types to MalSimAgentView
mrkickling d0822a2
Add tests for searcher agents
mrkickling 52eaf6a
Fix return types decision agents, make KeyboardAgent return nodes, ad…
mrkickling 773f692
Remove unused variable
mrkickling 9a47597
Have CLI use MalSimulator instead of SerializedObsEnv
mrkickling 0b35b89
Fix cli test
mrkickling 38c987f
Remove unused vars
mrkickling 629ddc2
Rename MalSimAgent->MalSimAgentState
mrkickling 1e2300e
Simplify example scenario test
mrkickling 9947605
Rename: get_agent -> get_agent_state
mrkickling 3623162
typing
mrkickling 465ff6d
Make sure seed is sent to agents when running MalSimVectorizedObsEnv
mrkickling 2b873af
Simplify agents
nkakouros c4f76c8
Update malsim vector obs env to abide to new language graph
mrkickling 6f94db7
Give AttackGraphpNode lang_graph_attack_step in tests the tests does …
mrkickling 5eb094a
Update test to work with new mal simulator - but for some reason it f…
mrkickling 37c1ae3
Make sure searcher agents are deterministic by sorting the action sur…
mrkickling 80f44b5
Allow to give agent config when loading scenario
mrkickling 34dc192
Use relative imports
mrkickling 029e7a2
Move logging methods into malsim_vectorized_obs_env.py
mrkickling 63135bf
Use new API of maltoolbox=0.3
mrkickling 561d090
Use iterator instead of list when getting first attacker in graph
mrkickling 785d716
Remove unused import/variables
mrkickling 410d18b
Include simulation step count in cli
mrkickling 0a1a050
Move all MalSimulator related classes into one file (mal_simulator.py)
mrkickling dd877af
Import VectorizedObsEnv from wrappers instead of from sims
mrkickling e23b9d8
Move mal_simulator.py to root package directory instead of in ./sims …
mrkickling f9de585
Add MalSimulator to __init__.py so it can be imported directly from t…
mrkickling 79793dc
Add MalSimVectorized to its __init__.py __all__
mrkickling 57f69b6
Check asset names without using the now remove model.asset_names
mrkickling afc8278
Add error message if node that is stepped through is not part of simu…
mrkickling a6d0a90
Update tests to work with mal-toolbox version 0.3.6, also fix some mi…
andrewbwm d42e1a3
Move the passive agent to its own file
andrewbwm 0ac43f6
Update gitignore to hide the logs file now used for logging
andrewbwm d4c123b
Make code more pythonic and simpler
nkakouros eaa4503
Address feedback
nkakouros 99be8b8
Imports with extra deps should not be top-level
nkakouros 3775f87
Rename wrappers to envs
nkakouros 7439c5c
Have test files follow package structure
nkakouros d3aa4e3
Stricter check for accepting to compromise node
nkakouros 7ce7276
Don't spam the debug log when all agents are terminated
nkakouros cef8b55
Fix failing tests
mrkickling 2b5e99e
Fix broken reference to old cli module
mrkickling 01d8997
Rework apply_attacker_entrypoints to add_attacker_entrypoints
andrewbwm 4607704
Update malsim/mal_simulator.py
nkakouros a608e8a
Rework MalSimulatorAgentState to track action surface additions and r…
andrewbwm 4bab692
Remove unused import and change default_factory in set in MalSimDefen…
mrkickling 1168a06
Add assert to test_example_scenario test
mrkickling 9c4a904
Remove Generic
nkakouros 1d38531
Remove unused malsim setting
nkakouros 938f313
Minor rewrite
nkakouros c27aa65
Fix tests
nkakouros b720c7e
Apply compromised defender penalty once
nkakouros 2db215e
Unnest loops for faster sim initialization
nkakouros 68cbcd0
Fix typo/bug, add comment
mrkickling 986c8b4
Rename _disable_attack_steps -> _uncompromise_attack_steps and fix do…
mrkickling 2da1ca3
Make MalSimulator _agents and _alive_agents private
mrkickling 597fbe3
Fix typehint
mrkickling f86a66c
Have one list with registered agents, one with living agent and one d…
mrkickling 6062ba7
Add more action surface assertions in MalSim step test
mrkickling 2c1fcf8
Re-enable clearing the step_action_surface_removals at the beginning …
andrewbwm 65db799
Re-enable cummulative step action surface removals
andrewbwm 1f61dfd
Add a simple AgentStateView test to check some of the basic functiona…
andrewbwm 785c0c9
Remove _registered_agents, have _get_defender_agents and _get_attacke…
mrkickling File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| from .decision_agent import PassiveAgent, DecisionAgent | ||
| from .keyboard_input import KeyboardAgent | ||
| from .searchers import BreadthFirstAttacker, DepthFirstAttacker | ||
|
|
||
| __all__ = [ | ||
| 'PassiveAgent', | ||
| 'DecisionAgent', | ||
| 'KeyboardAgent', | ||
| 'BreadthFirstAttacker', | ||
| 'DepthFirstAttacker' | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| """A decision agent is a heuristic agent""" | ||
|
|
||
| from __future__ import annotations | ||
| from typing import TYPE_CHECKING, Optional | ||
| from abc import ABC, abstractmethod | ||
|
|
||
| if TYPE_CHECKING: | ||
| from ..sims import MalSimAgentStateView | ||
| from maltoolbox.attackgraph import AttackGraphNode | ||
|
|
||
| class DecisionAgent(ABC): | ||
|
|
||
| @abstractmethod | ||
| def get_next_action( | ||
| self, | ||
| agent: MalSimAgentStateView, | ||
| **kwargs | ||
| ) -> Optional[AttackGraphNode]: | ||
| """ | ||
| Select next action the agent will work with. | ||
| Attributes: | ||
| agent: Current state of and other info about the agent from the simulator | ||
| Returns: | ||
| The selected action or None if there are no actions to select from. | ||
| """ | ||
| ... | ||
|
|
||
| class PassiveAgent(DecisionAgent): | ||
| def __init__(self, *args, **kwargs): | ||
| ... | ||
|
|
||
| def get_next_action( | ||
| self, | ||
| agent: MalSimAgentStateView, | ||
| **kwargs | ||
| ) -> Optional[AttackGraphNode]: | ||
| ... | ||
mrkickling marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,137 +1,98 @@ | ||
| from __future__ import annotations | ||
| import logging | ||
| import re | ||
|
|
||
| from collections import deque | ||
| from typing import Any, Deque, Dict, List, Set, Union | ||
| from typing import Optional, TYPE_CHECKING | ||
|
|
||
| import numpy as np | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| from .decision_agent import DecisionAgent | ||
| from ..sims import MalSimAgentStateView | ||
|
|
||
| def get_new_targets( | ||
| observation: dict, discovered_targets: Set[int], mask: tuple | ||
| ) -> List[int]: | ||
| attack_surface = mask[1] | ||
| surface_indexes = list(np.flatnonzero(attack_surface)) | ||
| new_targets = [idx for idx in surface_indexes if idx not in discovered_targets] | ||
| return new_targets, surface_indexes | ||
| if TYPE_CHECKING: | ||
| from maltoolbox.attackgraph import AttackGraphNode | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| class PassiveAttacker: | ||
| def compute_action_from_dict(self, observation, mask): | ||
| return (0, None) | ||
|
|
||
| class BreadthFirstAttacker: | ||
| def __init__(self, agent_config: dict) -> None: | ||
| self.targets: Deque[int] = deque([]) | ||
| self.current_target: int = None | ||
| seed = ( | ||
| agent_config["seed"] | ||
| if agent_config.get("seed", None) | ||
| else np.random.SeedSequence().entropy | ||
| ) | ||
| self.rng = ( | ||
| np.random.default_rng(seed) | ||
| if agent_config.get("randomize", False) | ||
| else None | ||
| ) | ||
| class BreadthFirstAttacker(DecisionAgent): | ||
| """A Breadth-First agent, with possible randomization at each level.""" | ||
|
|
||
| def compute_action_from_dict(self, observation: Dict[str, Any], mask: tuple): | ||
| new_targets, surface_indexes = get_new_targets(observation, self.targets, mask) | ||
| _extend_method = "extendleft" | ||
| # Controls where newly discovered steps will be appended to the list of | ||
| # available actions. Currently used to differentiate between BFS and DFS | ||
| # agents. | ||
|
|
||
| # Add new targets to the back of the queue | ||
| # if desired, shuffle the new targets to make the attacker more unpredictable | ||
| if self.rng: | ||
| self.rng.shuffle(new_targets) | ||
| for c in new_targets: | ||
| self.targets.appendleft(c) | ||
| name = ' '.join(re.findall(r'[A-Z][^A-Z]*', __qualname__)) | ||
| # A human-friendly name for the agent. | ||
|
|
||
| self.current_target, done = self.select_next_target( | ||
| self.current_target, self.targets, surface_indexes | ||
| ) | ||
| default_settings = { | ||
| 'randomize': False, | ||
| # Whether to randomize next target selection, still respecting the | ||
| # policy of the agent (e.g. BFS or DFS). | ||
| 'seed': None, | ||
| # The random seed to initialize the randomness engine with. | ||
| } | ||
|
|
||
| self.current_target = None if done else self.current_target | ||
| action = 0 if done else 1 | ||
| if action == 0: | ||
| logger.debug( | ||
| "Attacker Breadth First agent does not have " | ||
| "any valid targets it will terminate" | ||
| ) | ||
| def __init__(self, agent_config: dict) -> None: | ||
| """Initialize a BFS agent. | ||
| return (action, self.current_target) | ||
| Args: | ||
| agent_config: Dict with settings to override defaults | ||
| """ | ||
| self.targets: deque[AttackGraphNode] = deque() | ||
| self.current_target: Optional[AttackGraphNode] = None | ||
|
|
||
| @staticmethod | ||
| def select_next_target( | ||
| current_target: int, | ||
| targets: Union[List[int], Deque[int]], | ||
| attack_surface: Set[int], | ||
| ) -> int: | ||
| # If the current target was not compromised, put it | ||
| # back, but on the bottom of the stack. | ||
| if current_target in attack_surface: | ||
| targets.appendleft(current_target) | ||
| current_target = targets.pop() | ||
| self.settings = self.default_settings | agent_config | ||
|
|
||
| while current_target not in attack_surface: | ||
| if len(targets) == 0: | ||
| return None, True | ||
| self.rng = np.random.default_rng( | ||
| self.settings['seed'] or np.random.SeedSequence() | ||
| ) | ||
nkakouros marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| current_target = targets.pop() | ||
| def get_next_action( | ||
| self, agent: MalSimAgentStateView, **kwargs | ||
| ) -> Optional[AttackGraphNode]: | ||
nkakouros marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self._update_targets(agent.action_surface) | ||
| self._select_next_target() | ||
|
|
||
| return current_target, False | ||
| return self.current_target | ||
|
|
||
| def _update_targets(self, action_surface: list[AttackGraphNode]): | ||
|
|
||
| class DepthFirstAttacker: | ||
| def __init__(self, agent_config: dict) -> None: | ||
| self.current_target = -1 | ||
| self.targets: List[int] = [] | ||
| seed = ( | ||
| agent_config["seed"] | ||
| if agent_config.get("seed", None) | ||
| else np.random.SeedSequence().entropy | ||
| ) | ||
| self.rng = ( | ||
| np.random.default_rng(seed) | ||
| if agent_config.get("randomize", False) | ||
| else None | ||
| ) | ||
| # action surface does not have a guaranteed order, | ||
| # so for the agent to be deterministic we need to sort | ||
| action_surface.sort(key=lambda n: n.id) | ||
nkakouros marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| def compute_action_from_dict(self, observation: Dict[str, Any], mask: tuple): | ||
| new_targets, surface_indexes = get_new_targets(observation, self.targets, mask) | ||
| new_targets = [ | ||
| step | ||
| for step in action_surface | ||
| if step not in self.targets and not step.is_compromised() | ||
nkakouros marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ] | ||
|
|
||
| # Add new targets to the top of the stack | ||
| if self.rng: | ||
| if self.settings['randomize']: | ||
| self.rng.shuffle(new_targets) | ||
| for c in new_targets: | ||
| self.targets.append(c) | ||
|
|
||
| self.current_target, done = self.select_next_target( | ||
| self.current_target, self.targets, surface_indexes | ||
| ) | ||
|
|
||
| self.current_target = None if done else self.current_target | ||
| action = 0 if done else 1 | ||
| return (action, self.current_target) | ||
|
|
||
| @staticmethod | ||
| def select_next_target( | ||
| current_target: int, | ||
| targets: Union[List[int], Deque[int]], | ||
| attack_surface: Set[int], | ||
| ) -> int: | ||
| if current_target in attack_surface: | ||
| return current_target, False | ||
| if self.current_target in new_targets: | ||
| # If self.current_target is not yet compromised, e.g. due to TTCs, | ||
| # keep using that as the target. | ||
| new_targets.remove(self.current_target) | ||
| new_targets.append(self.current_target) | ||
|
|
||
| while current_target not in attack_surface: | ||
| if len(targets) == 0: | ||
| return None, True | ||
| # Enabled defenses may remove previously possible attack steps. | ||
| self.targets = deque(filter(lambda n: n.is_viable, self.targets)) | ||
|
|
||
| current_target = targets.pop() | ||
| getattr(self.targets, self._extend_method)(new_targets) | ||
|
|
||
| return current_target, False | ||
| def _select_next_target(self) -> None: | ||
| """ | ||
| Implement the actual next target selection logic. | ||
| """ | ||
| try: | ||
| self.current_target = self.targets.pop() | ||
| except IndexError: | ||
| self.current_target = None | ||
|
|
||
|
|
||
| AGENTS = { | ||
| BreadthFirstAttacker.__name__: BreadthFirstAttacker, | ||
| DepthFirstAttacker.__name__: DepthFirstAttacker, | ||
| } | ||
| class DepthFirstAttacker(BreadthFirstAttacker): | ||
| _extend_method = "extend" | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.