diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md old mode 100644 new mode 100755 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst old mode 100644 new mode 100755 diff --git a/README.rst b/README.rst index 38a7c572..2d930aa8 100755 --- a/README.rst +++ b/README.rst @@ -79,6 +79,16 @@ License What's new ---------- +- 2024-11-01 (v 0.12.1) (MAJOR release, WOP) + + - Modified version of NASim based on the paper "Evaluation of Reinforcement Learning for Autonomous Penetration Testing using A3C, Q-learning and DQN" `[Norman Becker et al.] `_. Please refer to the documentation for further information + + + Addition of Credentials and Vulnerabilities + + Modifications to existing functions and structures to reflect the intended + + Modifications to the generator in regards to the new additions + + + - 2023-05-14 (v 0.12.0) (MINOR release) diff --git a/docs/Makefile b/docs/Makefile old mode 100644 new mode 100755 diff --git a/docs/make.bat b/docs/make.bat old mode 100644 new mode 100755 index 6247f7e2..9534b018 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,35 +1,35 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements.txt b/docs/requirements.txt old mode 100644 new mode 100755 diff --git a/docs/source/community/acknowledgements.rst b/docs/source/community/acknowledgements.rst old mode 100644 new mode 100755 diff --git a/docs/source/community/development.rst b/docs/source/community/development.rst old mode 100644 new mode 100755 diff --git a/docs/source/community/distributing.rst b/docs/source/community/distributing.rst old mode 100644 new mode 100755 diff --git a/docs/source/community/index.rst b/docs/source/community/index.rst old mode 100644 new mode 100755 diff --git a/docs/source/conf.py b/docs/source/conf.py old mode 100644 new mode 100755 diff --git a/docs/source/explanations/index.rst b/docs/source/explanations/index.rst old mode 100644 new mode 100755 diff --git a/docs/source/explanations/scenario_generation.rst b/docs/source/explanations/scenario_generation.rst old mode 100644 new mode 100755 index e452dbeb..a37af294 --- a/docs/source/explanations/scenario_generation.rst +++ b/docs/source/explanations/scenario_generation.rst @@ -57,3 +57,47 @@ Firstly, there exists no firewall between subnets in the user zone. So communica Secondly, the number of services blocked is controlled by the ``restrictiveness`` parameter. This controls the number of services to block between zones (i.e. between the internet, DMZ, sensitive, and user zones). Thirdly, to ensure that the goal can be reached, traffic from at least one service running on each subnet will be allowed between each zone. This may mean more services will be allowed than restrictiveness parameter. + + +Credentials (Optional) +---------------------- + +To include credentials the following arguments are introduced: + +- ``num_cred=0`` - represents the number of credentials used. (max. of 9) + +- ``wiretapping_cost=0`` - cost to wiretap a target + +In general every exploit can be used with each credential, each exploit is generated with every possible credential as an input. These permutations are handled as one exploit to not affect the ``num_exploits=None`` variable. + +**Distribution**: Each credentials only has singular source to be discovered on. They are randomly handed to each subnet and then further distributed to the hosts in each subnet. + +Since every Host is vulnerable to at least one exploit, the credentials are placed in the ``credentials_tofind`` variable of the host and can be found by ``Wiretapping()``. + +**Locking**: Following the distribution the hosts are potentionaly locked. For this a random order to visit each host is generated and an array of ``found_credentials`` is being tracked. + +Each visited host is locked with a random already 'found' credential (if possible), credentials saved in ``credentials_tofind`` are noted for future use. + +**Limitations**: 'Distribution' and 'Locking' are simple representations of the potential applications. + +- Both are fully random and not influenced by ``seed`` or ``restrictiveness`` + +- credentials found by ``PrivelegeEscalation()`` are not represented nor implemented + +- 'Locking' does not uniformally 'lock' existing hosts, resulting in a random density of this property + + +Vulnerabilities (Optional) +-------------------------- + +- ``num_vul=0`` - number of vulnerabilities + +- ``vul_scan_cost=0`` - cost to scan a host for vulnerabilities + +Similiar to the depiction of how vulnerabilities work, each one is linked to a service. The exploits utilizing these vulnerabilities are not bound to a OS and have a ``prob=1``. + +Hosts running a 'vulnerable' service also get the vulnerability alocated to them to ensure susceptibility. + +**Limitations**: Vulnerabilities are randomly bound to services and not additionally to OS's, making it less diverse in terms of permutations. + +Furthermore, the possibility of a service with a linked vulnerability running on a host without the vulnerability is not present. \ No newline at end of file diff --git a/docs/source/explanations/sim_to_real.rst b/docs/source/explanations/sim_to_real.rst old mode 100644 new mode 100755 diff --git a/docs/source/index.rst b/docs/source/index.rst old mode 100644 new mode 100755 index 4c44be26..3ffbabf3 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,6 +9,16 @@ The environment is modelled after the `gymnasium (formerly Open AI gym) `_. Please refer to the documentation for further information + + - Addition of Credentials and Vulnerabilities + - Modifications to existing functions and structures to reflect the intended + - Modifications to the generator in regards to the new additions + Version 0.12.0 ************** diff --git a/docs/source/reference/agents/index.rst b/docs/source/reference/agents/index.rst old mode 100644 new mode 100755 diff --git a/docs/source/reference/envs/actions.rst b/docs/source/reference/envs/actions.rst old mode 100644 new mode 100755 diff --git a/docs/source/reference/envs/environment.rst b/docs/source/reference/envs/environment.rst old mode 100644 new mode 100755 diff --git a/docs/source/reference/envs/host_vector.rst b/docs/source/reference/envs/host_vector.rst old mode 100644 new mode 100755 diff --git a/docs/source/reference/envs/index.rst b/docs/source/reference/envs/index.rst old mode 100644 new mode 100755 diff --git a/docs/source/reference/envs/observation.rst b/docs/source/reference/envs/observation.rst old mode 100644 new mode 100755 diff --git a/docs/source/reference/envs/state.rst b/docs/source/reference/envs/state.rst old mode 100644 new mode 100755 diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst old mode 100644 new mode 100755 diff --git a/docs/source/reference/load.rst b/docs/source/reference/load.rst old mode 100644 new mode 100755 diff --git a/docs/source/reference/scenarios/benchmark_scenarios.rst b/docs/source/reference/scenarios/benchmark_scenarios.rst old mode 100644 new mode 100755 diff --git a/docs/source/reference/scenarios/benchmark_scenarios_agent_scores.csv b/docs/source/reference/scenarios/benchmark_scenarios_agent_scores.csv old mode 100644 new mode 100755 diff --git a/docs/source/reference/scenarios/benchmark_scenarios_table.csv b/docs/source/reference/scenarios/benchmark_scenarios_table.csv old mode 100644 new mode 100755 diff --git a/docs/source/reference/scenarios/generator.rst b/docs/source/reference/scenarios/generator.rst old mode 100644 new mode 100755 diff --git a/docs/source/reference/scenarios/index.rst b/docs/source/reference/scenarios/index.rst old mode 100644 new mode 100755 diff --git a/docs/source/tutorials/creating_scenarios.rst b/docs/source/tutorials/creating_scenarios.rst old mode 100644 new mode 100755 diff --git a/docs/source/tutorials/credentials_vulnerabilities.rst b/docs/source/tutorials/credentials_vulnerabilities.rst new file mode 100755 index 00000000..77fee601 --- /dev/null +++ b/docs/source/tutorials/credentials_vulnerabilities.rst @@ -0,0 +1,153 @@ +.. _`cred_vul_tute`: + +Credentials and Vulnerabilities +=============================== + +The following additions are optional and do not need to be defined in costum scenario.yaml files. Nevertheless, internal structures had to be modified which are 'visible' even if not manually defined. + + +Idea +---- + +To better reflect real world complexity, credentials and vulnerabilities are introduced to NASim. + +These modifications are implemented with the idea to train RL-agents in more realistic enviroments. + + +Credentials +----------- + +Credentials are implemented as single digits from **1** to **9**, each describing a full credential pair of, theoretically, a username and password. **0** is in meaning equivilant to ``None``. + + +Host +^^^^ + +Hosts can now be further described by ``credentials_needed: int`` and ``credentials_tofind: int``. + +.. code-block:: yaml + + host_configurations: + (1, 0): + os: linux + services: [ssh] + processes: [tomcat] + firewall: + (3, 0): [ssh] + value: 0 + credentials_needed: 0 + credentials_tofind: 12 + +``credentials_needed: int`` describes the credentials needed to perform a succesful exploit. If 0 or not defined no credentials are needed. + +``credentials_tofind: int`` describes the credentials which can be found by wiretapping from a specific host. +This is a concatination of integer, meaning `123` is equal to its permutations for example `213`. + + +PrivelegeEscalation +^^^^^^^^^^^^^^^^^^^ + +Under the pretext of post-exploitation, **PrivelegeEscalation()** can be further extended with ``credentials_tofind: int``. + +.. code-block:: yaml + + privilege_escalation: + pe_gain_Credentails_from_Shadow: + process: shadow + os: Rasbian + prob: 1.0 + cost: 3 + access: root + credentials_tofind: 3 + +``credentials_tofind: int`` describes the credentials which are found by exploiting / attacking a process on the specific host. + + +Wiretapping +^^^^^^^^^^^ + + +**Wiretapping()** is a new action with the goal to 'listen' from a targeted node. A compromised host can tap to reveal the credentials hold in ``cred_tofind: int``. + +The cost ``wiretapping_cost: int`` can be set in the scenario.yaml. + + + +Vulnerabilities +--------------- + +A vulnerability is a weakness or flaw on a host which can be exploited. + +It is implemented as a ``string`` and with its introduction both **Host** and **Exploit** get additional options and a new action **VulScan()** is implemented. + + +Host +^^^^ + +.. code-block:: yaml + + host_configurations: + (1, 0): + os: Rasbian + services: [openssh, samba] + vul: [CVE20072447] + processes: [shadow] + credentials_needed: 0 + credentials_tofind: 21 + +``vul: list[str]`` describes the existing vulnerabilities on a host. + + +Exploit +^^^^^^^ + +.. code-block:: yaml + + e_samba: + service: samba + os: None + vul: CVE20072447 + prob: 1.0 + cost: 3 + access: root + +``vul: str`` describes the exploited vulnerability. This `vul` has to exist on the targeted host for the exploit to succeed. +Can further be *None* or not defined if not needed. + + +VulScan +^^^^^^^ + +Similiar to **OSScan()** it is an action performed on a host returning its vulnerabilites ``vul: list[str]``. + +The cost ``vul_scan_cost: int`` can be set in the scenario.yaml. + + +Scenarios +^^^^^^^^^ + +Some scenarios are added featuring credentials and vulnerabilities, namely *tiny-wire*, *tiny-post*, *tiny-cred*, *small-wire* and *small-post*. + +In addition, some scenarios from previous work got an equivalant *.yaml* with credentials and vulnerabilities specifically defined. (names ending on *-cwp*) + + +Information +----------- + +If no credentials or vulnerabilities are used, following properties are "visible": + +- **HostVector** will still hold ``cred_tofind = 0`` and ``cred_found = 0``. + +- **Exploit** will need ``cred_needed = 0`` and ``vul = None`` + +- **Host** will have ``vul = []``, ``cred_tofind = 0`` and ``cred_needed = 0`` + + +These modifications are based on the Bachelor's Thesis of Norman Becker + +Contributions by Georg Blum @Class1G + + **Evaluation of Reinforcement Learning for Autonomous Penetration Testing using A3C, Q-learning and DQN** Norman Becker et al. *2024* +https://arxiv.org/abs/2407.15656 + +-- Copyright © Norman Becker -- \ No newline at end of file diff --git a/docs/source/tutorials/environment.rst b/docs/source/tutorials/environment.rst old mode 100644 new mode 100755 diff --git a/docs/source/tutorials/example_network.png b/docs/source/tutorials/example_network.png old mode 100644 new mode 100755 diff --git a/docs/source/tutorials/gym_load.rst b/docs/source/tutorials/gym_load.rst old mode 100644 new mode 100755 diff --git a/docs/source/tutorials/index.rst b/docs/source/tutorials/index.rst old mode 100644 new mode 100755 index bc9f235f..91a15f5d --- a/docs/source/tutorials/index.rst +++ b/docs/source/tutorials/index.rst @@ -12,3 +12,6 @@ Tutorials environment scenarios creating_scenarios + credentials_vulnerabilities + + diff --git a/docs/source/tutorials/installation.rst b/docs/source/tutorials/installation.rst old mode 100644 new mode 100755 index 5e2f35a8..12fac2f5 --- a/docs/source/tutorials/installation.rst +++ b/docs/source/tutorials/installation.rst @@ -12,15 +12,16 @@ This framework is tested to work under Python 3.7 or later. The required dependencies: * Python >= 3.7 -* Gym >= 0.17 -* NumPy >= 1.18 -* PyYaml >= 5.3 +* Gym == 0.26.3 +* NumPy == 1.26.0 +* PyYaml >= 6.0.2 For rendering: -* NetworkX >= 2.4 -* prettytable >= 0.7.2 -* Matplotlib >= 3.1.3 +* NetworkX >= 3.3 +* prettytable >= 3.11.0 +* Matplotlib >= 3.9.2 + We recommend to use the bleeding-edge version and to install it by following the :ref:`dev-install`. If you want a simpler installation procedure and do not intend to modify yourself the learning algorithms etc., you can look at the :ref:`user-install`. diff --git a/docs/source/tutorials/loading.rst b/docs/source/tutorials/loading.rst old mode 100644 new mode 100755 diff --git a/docs/source/tutorials/scenarios.rst b/docs/source/tutorials/scenarios.rst old mode 100644 new mode 100755 diff --git a/nasim/agents/__init__.py b/nasim/agents/__init__.py old mode 100644 new mode 100755 diff --git a/nasim/agents/bruteforce_agent.py b/nasim/agents/bruteforce_agent.py old mode 100644 new mode 100755 diff --git a/nasim/agents/dqn_agent.py b/nasim/agents/dqn_agent.py old mode 100644 new mode 100755 diff --git a/nasim/agents/keyboard_agent.py b/nasim/agents/keyboard_agent.py old mode 100644 new mode 100755 index d86da03e..7d724e23 --- a/nasim/agents/keyboard_agent.py +++ b/nasim/agents/keyboard_agent.py @@ -173,12 +173,13 @@ def run_keyboard_agent(env): print("\n" + LINE_BREAK2) print("OBSERVATION RECIEVED") print(LINE_BREAK2) - env.render() + print(env.current_state.tensor) print(f"Reward={r}") print(f"Done={done}") print(f"Step limit reached={step_limit_reached}") print(LINE_BREAK) + return total_reward, total_steps, done diff --git a/nasim/agents/policies/dqn_tiny.pt b/nasim/agents/policies/dqn_tiny.pt old mode 100644 new mode 100755 diff --git a/nasim/agents/ql_agent.py b/nasim/agents/ql_agent.py old mode 100644 new mode 100755 diff --git a/nasim/agents/ql_replay_agent.py b/nasim/agents/ql_replay_agent.py old mode 100644 new mode 100755 diff --git a/nasim/agents/random_agent.py b/nasim/agents/random_agent.py old mode 100644 new mode 100755 diff --git a/nasim/demo.py b/nasim/demo.py old mode 100644 new mode 100755 diff --git a/nasim/envs/action.py b/nasim/envs/action.py index 0d655fc6..e710421c 100755 --- a/nasim/envs/action.py +++ b/nasim/envs/action.py @@ -58,9 +58,15 @@ def load_action_list(scenario): action_list.append( ServiceScan(address, scenario.service_scan_cost) ) + action_list.append( + VulScan(address, scenario.vul_scan_cost) + ) action_list.append( OSScan(address, scenario.os_scan_cost) ) + action_list.append( + Wiretapping(address, scenario.wiretapping_cost) + ) action_list.append( SubnetScan(address, scenario.subnet_scan_cost) ) @@ -165,7 +171,7 @@ def is_scan(self): bool True if action is scan, otherwise False """ - return isinstance(self, (ServiceScan, OSScan, SubnetScan, ProcessScan)) + return isinstance(self, (ServiceScan, OSScan, SubnetScan, ProcessScan, VulScan, Wiretapping)) def is_remote(self): """Check if action is a remote action @@ -178,7 +184,7 @@ def is_remote(self): bool True if action is remote, otherwise False """ - return isinstance(self, (ServiceScan, OSScan, Exploit)) + return isinstance(self, (ServiceScan, OSScan, Exploit, Wiretapping)) def is_service_scan(self): """Check if action is a service scan @@ -190,6 +196,26 @@ def is_service_scan(self): """ return isinstance(self, ServiceScan) + def is_vul_scan(self): + """Check if action is a service scan + + Returns + ------- + bool + True if action is service scan, otherwise False + """ + return isinstance(self, VulScan) + + def is_wiretapping(self): + """Check if action is a service scan + + Returns + ------- + bool + True if action is service scan, otherwise False + """ + return isinstance(self, Wiretapping) + def is_os_scan(self): """Check if action is an OS scan @@ -275,6 +301,8 @@ def __init__(self, target, cost, service, + vul, + credentials_needed, os=None, access=0, prob=1.0, @@ -289,6 +317,10 @@ def __init__(self, cost of performing action service : str the target service + vul : str + the vulnerability targeted + cred_needed : int + credentials needed to perform exploit os : str, optional the target OS of exploit, if None then exploit works for all OS (default=None) @@ -304,22 +336,29 @@ def __init__(self, target=target, cost=cost, prob=prob, - req_access=req_access) + req_access=req_access, + credentials_needed=credentials_needed) self.os = os self.service = service self.access = access + self.vul = vul + self.credentials_needed = credentials_needed def __str__(self): - return (f"{super().__str__()}, os={self.os}, " - f"service={self.service}, access={self.access}") + return (f"{super().__str__()}, " + f"os = {self.os}, " + f"vul = {self.vul}, " + f"cred_needed = {self.credentials_needed}, " + f"service = {self.service}, access = {self.access}") def __eq__(self, other): if not super().__eq__(other): return False return self.service == other.service \ and self.os == other.os \ - and self.access == other.access - + and self.access == other.access \ + and self.vul == other.vul \ + and self.credentials_needed == other.credentials_needed class PrivilegeEscalation(Action): """A privilege escalation action in the environment @@ -345,6 +384,7 @@ def __init__(self, target, cost, access, + credentials_tofind, process=None, os=None, prob=1.0, @@ -359,6 +399,8 @@ def __init__(self, cost of performing action access : int the access level resulting from the privilege escalation + credentials_tofind : + credentials which will be found if PE is successful process : str, optional the target process, if None the action does not require a process to work (default=None) @@ -375,21 +417,24 @@ def __init__(self, target=target, cost=cost, prob=prob, - req_access=req_access) + req_access=req_access, + credentials_tofind=credentials_tofind) self.access = access self.os = os self.process = process + self.credentials_tofind = credentials_tofind def __str__(self): - return (f"{super().__str__()}, os={self.os}, " - f"process={self.process}, access={self.access}") + return (f"{super().__str__()}, os = {self.os}, cred_tofind = {self.credentials_tofind}, " + f"process = {self.process}, access = {self.access}") def __eq__(self, other): if not super().__eq__(other): return False return self.process == other.process \ and self.os == other.os \ - and self.access == other.access + and self.access == other.access \ + and self.credentials_tofind == other.credentials_tofind class ServiceScan(Action): @@ -424,6 +469,69 @@ def __init__(self, req_access=req_access, **kwargs) +class VulScan(Action): + """An Vulnerability Scan action in the environment + + Inherits from the base Action Class. + """ + + def __init__(self, + target, + cost, + prob=1.0, + req_access=AccessLevel.USER, + **kwargs): + """ + Parameters + --------- + target : (int, int) + address of target + cost : float + cost of performing action + prob : float, optional + probability of success for a given action (default=1.0) + req_access : AccessLevel, optional + the required access level to perform action + (default=AccessLevel.USER) + """ + super().__init__("vul_scan", + target=target, + cost=cost, + prob=prob, + req_access=req_access, + **kwargs) + +class Wiretapping(Action): + """An Wiretapping action in the environment + + Inherits from the base Action Class. + """ + + def __init__(self, + target, + cost, + prob=1.0, + req_access=AccessLevel.USER, + **kwargs): + """ + Parameters + --------- + target : (int, int) + address of target + cost : float + cost of performing action + prob : float, optional + probability of success for a given action (default=1.0) + req_access : AccessLevel, optional + the required access level to perform action + (default=AccessLevel.USER) + """ + super().__init__("wiretapping", + target=target, + cost=cost, + prob=prob, + req_access=req_access, + **kwargs) class OSScan(Action): """An OS Scan action in the environment @@ -457,7 +565,6 @@ def __init__(self, req_access=req_access, **kwargs) - class SubnetScan(Action): """A Subnet Scan action in the environment @@ -556,6 +663,16 @@ class ActionResult: services identified by action. os : dict OS identified by action + + vul : dict + vulnerabilies discovered by action + cred_tofind : dict + discoverable credentials identified by action + cred_found : dict + discovered credentials by action + cred_needed : dict + needed credentials identified by action + processes : dict processes identified by action access : dict @@ -580,6 +697,10 @@ def __init__(self, value=0.0, services=None, os=None, + vul=None, + cred_tofind=None, + cred_found=None, + cred_needed=None, processes=None, access=None, discovered=None, @@ -598,6 +719,16 @@ def __init__(self, services identified by action (default=None={}) os : dict, optional OS identified by action (default=None={}) + + vul : dict, optional + vulnerabilies discovered by action (default=None={}) + cred_tofind : dict, optional + discoverable credentials identified by action (default=0={}) + cred_found : dict, optional + discovered credentials by action (default={}) + cred_needed : dict, optional + needed credentials identified by action (default=0={}) + processes : dict, optional processes identified by action (default=None={}) access : dict, optional @@ -617,6 +748,10 @@ def __init__(self, self.value = value self.services = {} if services is None else services self.os = {} if os is None else os + self.vul = {} if vul is None else vul + self.cred_tofind = {} if cred_tofind == 0 else cred_tofind + self.cred_found = {} if cred_found == 0 else cred_found + self.cred_needed = {} if cred_needed == 0 else cred_needed self.processes = {} if processes is None else processes self.access = {} if access is None else access self.discovered = {} if discovered is None else discovered @@ -641,6 +776,10 @@ def info(self): value=self.value, services=self.services, os=self.os, + vul=self.vul, + cred_tofind=self.cred_tofind, + cred_found=self.cred_found, + cred_needed=self.cred_needed, processes=self.processes, access=self.access, discovered=self.discovered, @@ -709,7 +848,7 @@ class ParameterisedActionSpace(spaces.MultiDiscrete): The action parameters (in order) are: - 0. Action Type = [0, 5] + 0. Action Type = [0, 7] Where: @@ -720,10 +859,15 @@ class ParameterisedActionSpace(spaces.MultiDiscrete): 2=ServiceScan, 3=OSScan, + + 4=VulScan, + + 5=Wiretapping, - 4=SubnetScan, + 6=SubnetScan, - 5=ProcessScan, + 7=ProcessScan, + 1. Subnet = [0, #subnets-1] @@ -733,13 +877,21 @@ class ParameterisedActionSpace(spaces.MultiDiscrete): 3. OS = [0, #OS] Where 0=None. - + 4. Service = [0, #services - 1] 5. Process = [0, #processes] Where 0=None. - - Note that OS, Service and Process are only important for exploits and + + 6. Vulnerability = [0, #vul] + + Where 0=None. + + 7. Credential = [0, #cred] + + Where 0=None. + + Note that OS, Service, Process, Vulnerability and Credentials are only important for exploits and privilege escalation actions. ... @@ -756,6 +908,8 @@ class ParameterisedActionSpace(spaces.MultiDiscrete): Exploit, PrivilegeEscalation, ServiceScan, + VulScan, + Wiretapping, OSScan, SubnetScan, ProcessScan @@ -777,9 +931,10 @@ def __init__(self, scenario): max(self.scenario.subnets), self.scenario.num_os+1, self.scenario.num_services, - self.scenario.num_processes + self.scenario.num_processes+1, + self.scenario.num_vul+1, + self.scenario.num_cred ] - super().__init__(nvec) def get_action(self, action_vec): @@ -821,18 +976,21 @@ def get_action(self, action_vec): os = None if action_vec[3] == 0 else self.scenario.os[action_vec[3]-1] + if a_class == Exploit: # have to make sure it is valid choice # and also get constant params (name, cost, prob, access) - service = self.scenario.services[action_vec[4]] - a_def = self._get_exploit_def(service, os) + service = self.scenario.services[action_vec[4]-1] + vul = None if action_vec[6] == 0 else self.scenario.vul[action_vec[6]-1] + cred_needed = None if action_vec[7] == 0 else action_vec[7] + a_def = self._get_exploit_def(service, os, vul, cred_needed) else: # privilege escalation # have to make sure it is valid choice # and also get constant params (name, cost, prob, access) - proc = self.scenario.processes[action_vec[5]] + proc = None if action_vec[5] == 0 else self.scenario.processes[action_vec[5]-1] a_def = self._get_privesc_def(proc, os) - + if a_def is None: return NoOp() return a_class(target=target, **a_def) @@ -841,6 +999,10 @@ def _get_scan_action_def(self, a_class): """Get the constants for scan actions definitions """ if a_class == ServiceScan: cost = self.scenario.service_scan_cost + elif a_class == VulScan: + cost = self.scenario.vul_scan_cost + elif a_class == Wiretapping: + cost = self.scenario.wiretapping_cost elif a_class == OSScan: cost = self.scenario.os_scan_cost elif a_class == SubnetScan: @@ -851,14 +1013,19 @@ def _get_scan_action_def(self, a_class): raise TypeError(f"Not implemented for Action class {a_class}") return {"cost": cost} - def _get_exploit_def(self, service, os): + + def _get_exploit_def(self, service, os, vul, cred_needed): """Check if exploit parameters are valid """ e_map = self.scenario.exploit_map if service not in e_map: return None if os not in e_map[service]: return None - return e_map[service][os] + if vul not in e_map[service][os]: + return None + if cred_needed not in e_map[service][os][vul]: + return None + return e_map[service][os][vul][cred_needed] def _get_privesc_def(self, proc, os): """Check if privilege escalation parameters are valid """ diff --git a/nasim/envs/environment.py b/nasim/envs/environment.py index dcd74be4..e80a09d2 100755 --- a/nasim/envs/environment.py +++ b/nasim/envs/environment.py @@ -128,7 +128,8 @@ def reset(self, *, seed=None, options=None): """ super().reset(seed=seed, options=options) self.steps = 0 - self.current_state = self.network.reset(self.current_state) + #self.current_state = self.network.reset(self.current_state) + self.current_state = State.generate_initial_state(self.network) self.last_obs = self.current_state.get_initial_observation( self.fully_obs ) diff --git a/nasim/envs/gym_env.py b/nasim/envs/gym_env.py old mode 100644 new mode 100755 diff --git a/nasim/envs/host_vector.py b/nasim/envs/host_vector.py old mode 100644 new mode 100755 index be3d556e..1b0c5864 --- a/nasim/envs/host_vector.py +++ b/nasim/envs/host_vector.py @@ -9,6 +9,8 @@ from nasim.envs.utils import AccessLevel from nasim.envs.action import ActionResult +from pprint import pprint + class HostVector: """ A Vector representation of a single host in NASim. @@ -19,28 +21,33 @@ class HostVector: Features in the vector, listed in order, are: - 1. subnet address - one-hot encoding with length equal to the number + 1. subnet address - one-hot encoding with length equal to the number of subnets - 2. host address - one-hot encoding with length equal to the maximum number + 2. host address - one-hot encoding with length equal to the maximum number of hosts in any subnet - 3. compromised - bool - 4. reachable - bool - 5. discovered - bool - 6. value - float - 7. discovery value - float - 8. access - int - 9. OS - bool for each OS in scenario (only one OS has value of true) - 10. services running - bool for each service in scenario - 11. processes running - bool for each process in scenario + 3. compromised - bool + 4. reachable - bool + 5. discovered - bool + 6. value - float + 7. cred_tofind - concatenation of int of discoverable credentials + 8. cred_needed - int of needed credential + 9. cred_found - concatenation of int of found credentials + 10. discovery value - float + 11. access - int + 12. OS - bool for each OS in scenario (only one OS has value of true) + 13. services running - bool for each service in scenario + 14. vulnerability - bool for each vulnerability + 15. processes running - bool for each process in scenario Notes ----- - The size of the vector is equal to: - #subnets + max #hosts in any subnet + 6 + #OS + #services + #processes. + #subnets + max #hosts in any subnet + 6 + 3 + #OS + #services + #processes + #vulnerabilites. - Where the +6 is for compromised, reachable, discovered, value, discovery_value, and access features + - +3 being cred_tofind, cred_needed and cred_found - The vector is a float vector so True/False is actually represented as 1.0/0.0. @@ -58,6 +65,10 @@ class HostVector: num_services = None # map from service name to its index in host vector service_idx_map = {} + + num_vul = None + vul_idx_map = {} + # number of processes in scenario num_processes = None # map from process name to its index in host vector @@ -73,6 +84,10 @@ class HostVector: _reachable_idx = None _discovered_idx = None _value_idx = None + _vul_start_idx = None + _credentials_tofind_idx = None + _credentials_needed_idx = None + _credentials_found_idx = None _discovery_value_idx = None _access_idx = None _os_start_idx = None @@ -82,52 +97,6 @@ class HostVector: def __init__(self, vector): self.vector = vector - @classmethod - def vectorize(cls, host, address_space_bounds, vector=None): - if cls.address_space_bounds is None: - cls._initialize( - address_space_bounds, host.services, host.os, host.processes - ) - - if vector is None: - vector = np.zeros(cls.state_size, dtype=np.float32) - else: - assert len(vector) == cls.state_size - - vector[cls._subnet_address_idx + host.address[0]] = 1 - vector[cls._host_address_idx + host.address[1]] = 1 - vector[cls._compromised_idx] = int(host.compromised) - vector[cls._reachable_idx] = int(host.reachable) - vector[cls._discovered_idx] = int(host.discovered) - vector[cls._value_idx] = host.value - vector[cls._discovery_value_idx] = host.discovery_value - vector[cls._access_idx] = host.access - for os_num, (os_key, os_val) in enumerate(host.os.items()): - vector[cls._get_os_idx(os_num)] = int(os_val) - for srv_num, (srv_key, srv_val) in enumerate(host.services.items()): - vector[cls._get_service_idx(srv_num)] = int(srv_val) - host_procs = host.processes.items() - for proc_num, (proc_key, proc_val) in enumerate(host_procs): - vector[cls._get_process_idx(proc_num)] = int(proc_val) - return cls(vector) - - @classmethod - def vectorize_random(cls, host, address_space_bounds, vector=None): - hvec = cls.vectorize(host, vector) - # random variables - for srv_num in cls.service_idx_map.values(): - srv_val = np.random.randint(0, 2) - hvec.vector[cls._get_service_idx(srv_num)] = srv_val - - chosen_os = np.random.choice(list(cls.os_idx_map.values())) - for os_num in cls.os_idx_map.values(): - hvec.vector[cls._get_os_idx(os_num)] = int(os_num == chosen_os) - - for proc_num in cls.process_idx_map.values(): - proc_val = np.random.randint(0, 2) - hvec.vector[cls._get_process_idx(proc_num)] = proc_val - return hvec - @property def compromised(self): return self.vector[self._compromised_idx] @@ -163,6 +132,29 @@ def address(self): def value(self): return self.vector[self._value_idx] + @property + def credentials_needed(self): + return self.vector[self._credentials_needed_idx] + + @property + def credentials_tofind(self): + return self.vector[self._credentials_tofind_idx] + + @property + def credentials_found(self): + return self.vector[self._credentials_found_idx] + + + def credentials_found_set(self, val): + self.vector[self._credentials_found_idx] = float(val) + + @property + def vul(self): + vul = {} + for v, vul_num in self.vul_idx_map.items(): + vul[v] = self.vector[self._get_vul_idx(vul_num)] + return vul + @property def discovery_value(self): return self.vector[self._discovery_value_idx] @@ -196,10 +188,84 @@ def processes(self): processes[proc] = self.vector[self._get_process_idx(proc_num)] return processes + + @classmethod + def vectorize(cls, host, address_space_bounds, vector=None): + if cls.address_space_bounds is None: + cls._initialize( + address_space_bounds, host.services, host.vul, host.os, host.processes + ) + + if vector is None: + vector = np.zeros(cls.state_size, dtype=np.float32) + else: + assert len(vector) == cls.state_size + + vector[cls._subnet_address_idx + host.address[0]] = 1 + vector[cls._host_address_idx + host.address[1]] = 1 + vector[cls._compromised_idx] = int(host.compromised) + vector[cls._reachable_idx] = int(host.reachable) + vector[cls._discovered_idx] = int(host.discovered) + vector[cls._value_idx] = host.value + vector[cls._credentials_tofind_idx] = host.cred_tofind + vector[cls._credentials_found_idx] = host.cred_found + vector[cls._credentials_needed_idx] = host.cred_needed + vector[cls._discovery_value_idx] = host.discovery_value + vector[cls._access_idx] = host.access + for os_num, (os_key, os_val) in enumerate(host.os.items()): + vector[cls._get_os_idx(os_num)] = int(os_val) + for srv_num, (srv_key, srv_val) in enumerate(host.services.items()): + vector[cls._get_service_idx(srv_num)] = int(srv_val) + + for vul_num, (vul_key, vul_val) in enumerate(host.vul.items()): + vector[cls._get_vul_idx(vul_num)] = int(vul_val) + + host_procs = host.processes.items() + for proc_num, (proc_key, proc_val) in enumerate(host_procs): + vector[cls._get_process_idx(proc_num)] = int(proc_val) + return cls(vector) + + @classmethod + def vectorize_random(cls, host, address_space_bounds, vector=None): + hvec = cls.vectorize(host, vector) + # random variables + for srv_num in cls.service_idx_map.values(): + srv_val = np.random.randint(0, 2) + hvec.vector[cls._get_service_idx(srv_num)] = srv_val + + for vul_num in cls.vul_idx_map.values(): + vul_val = np.random.randint(0, 2) + hvec.vector[cls._get_vul_idx(vul_num)] = vul_val + + chosen_os = np.random.choice(list(cls.os_idx_map.values())) + for os_num in cls.os_idx_map.values(): + hvec.vector[cls._get_os_idx(os_num)] = int(os_num == chosen_os) + + for proc_num in cls.process_idx_map.values(): + proc_val = np.random.randint(0, 2) + hvec.vector[cls._get_process_idx(proc_num)] = proc_val + return hvec + + + def is_running_service(self, srv): srv_num = self.service_idx_map[srv] return bool(self.vector[self._get_service_idx(srv_num)]) + def got_credentials(self, c): + tmp1 = str(int(self.vector[self._credentials_found_idx])) + tmp2 = str(int(self.vector[self._credentials_needed_idx])) + if str(c) in tmp1 and str(c) in tmp2: + return True + else: + return False + + def is_running_vul(self, v): + if v == []: + return False + vul_num = self.vul_idx_map[v] + return bool(self.vector[self._get_vul_idx(vul_num)]) + def is_running_os(self, os): os_num = self.os_idx_map[os] return bool(self.vector[self._get_os_idx(os_num)]) @@ -224,16 +290,27 @@ def perform_action(self, action): the result from the action """ next_state = self.copy() + if action.is_service_scan(): result = ActionResult(True, 0, services=self.services) return next_state, result + if action.is_vul_scan(): + result = ActionResult(True, 0, vul=self.vul) + return next_state, result + + if action.is_wiretapping(): + result = ActionResult(True, 0, cred_tofind=self.credentials_tofind) + return next_state, result + if action.is_os_scan(): return next_state, ActionResult(True, 0, os=self.os) if action.is_exploit(): if self.is_running_service(action.service) and \ - (action.os is None or self.is_running_os(action.os)): + (action.os is None or self.is_running_os(action.os)) and \ + (action.vul == "None" or action.vul == None or self.is_running_vul(action.vul))and \ + (action.credentials_needed == 0 or action.credentials_needed == None or self.got_credentials(action.credentials_needed)): # service and os is present so exploit is successful value = 0 next_state.compromised = True @@ -249,6 +326,8 @@ def perform_action(self, action): value=value, services=self.services, os=self.os, + vul=self.vul, + cred_tofind=self.credentials_tofind, access=action.access ) return next_state, result @@ -287,7 +366,8 @@ def perform_action(self, action): value=value, processes=self.processes, os=self.os, - access=action.access + access=action.access, + cred_tofind=action.credentials_tofind ) return next_state, result @@ -303,6 +383,10 @@ def observe(self, value=False, discovery_value=False, services=False, + vul=False, + cred_tofind=False, + cred_found=False, + cred_needed=False, processes=False, os=False): obs = np.zeros(self.state_size, dtype=np.float32) @@ -319,6 +403,12 @@ def observe(self, obs[self._discovered_idx] = self.vector[self._discovered_idx] if value: obs[self._value_idx] = self.vector[self._value_idx] + if cred_tofind: + obs[self._credentials_tofind_idx] = self.vector[self._credentials_tofind_idx] + if cred_found: + obs[self._credentials_found_idx] = self.vector[self._credentials_found_idx] + if cred_needed: + obs[self._credentials_needed_idx] = self.vector[self._credentials_needed_idx] if discovery_value: v = self.vector[self._discovery_value_idx] obs[self._discovery_value_idx] = v @@ -330,6 +420,11 @@ def observe(self, if services: idxs = self._service_idx_slice() obs[idxs] = self.vector[idxs] + + if vul: + idxs = self._vul_idx_slice() + obs[idxs] = self.vector[idxs] + if processes: idxs = self._process_idx_slice() obs[idxs] = self.vector[idxs] @@ -346,19 +441,24 @@ def numpy(self): return self.vector @classmethod - def _initialize(cls, address_space_bounds, services, os_info, processes): + def _initialize(cls, address_space_bounds, services, vul, os_info, processes): cls.os_idx_map = {} cls.service_idx_map = {} + cls.vul_idx_map = {} cls.process_idx_map = {} cls.address_space_bounds = address_space_bounds cls.num_os = len(os_info) cls.num_services = len(services) + cls.num_vul = len(vul) cls.num_processes = len(processes) cls._update_vector_idxs() for os_num, (os_key, os_val) in enumerate(os_info.items()): cls.os_idx_map[os_key] = os_num for srv_num, (srv_key, srv_val) in enumerate(services.items()): cls.service_idx_map[srv_key] = srv_num + for vul_num, (vul_key, vul_val) in enumerate(vul.items()): + cls.vul_idx_map[vul_key] = vul_num + for proc_num, (proc_key, proc_val) in enumerate(processes.items()): cls.process_idx_map[proc_key] = proc_num @@ -374,9 +474,13 @@ def _update_vector_idxs(cls): cls._value_idx = cls._discovered_idx + 1 cls._discovery_value_idx = cls._value_idx + 1 cls._access_idx = cls._discovery_value_idx + 1 - cls._os_start_idx = cls._access_idx + 1 + cls._credentials_tofind_idx = cls._access_idx + 1 + cls._credentials_found_idx = cls._credentials_tofind_idx + 1 + cls._credentials_needed_idx = cls._credentials_found_idx + 1 + cls._os_start_idx = cls._credentials_needed_idx + 1 cls._service_start_idx = cls._os_start_idx + cls.num_os - cls._process_start_idx = cls._service_start_idx + cls.num_services + cls._vul_start_idx = cls._service_start_idx + cls.num_services + cls._process_start_idx = cls._vul_start_idx + cls.num_vul #cls._service_start_idx + cls.num_services cls.state_size = cls._process_start_idx + cls.num_processes @classmethod @@ -393,7 +497,17 @@ def _get_service_idx(cls, srv_num): @classmethod def _service_idx_slice(cls): - return slice(cls._service_start_idx, cls._process_start_idx) + return slice(cls._service_start_idx, cls._vul_start_idx) #cls._process_start_idx + + + @classmethod + def _get_vul_idx(cls, vul_num): + return cls._vul_start_idx+vul_num + + @classmethod + def _vul_idx_slice(cls): + return slice(cls._vul_start_idx, cls._process_start_idx) + @classmethod def _get_os_idx(cls, os_num): @@ -422,10 +536,15 @@ def get_readable(cls, vector): readable_dict["Value"] = hvec.value readable_dict["Discovery Value"] = hvec.discovery_value readable_dict["Access"] = hvec.access + readable_dict["cred_tofind"] = hvec.credentials_tofind + readable_dict["cred_found"] = hvec.credentials_found + readable_dict["cred_needed"] = hvec.credentials_needed for os_name in cls.os_idx_map: readable_dict[f"{os_name}"] = hvec.is_running_os(os_name) for srv_name in cls.service_idx_map: readable_dict[f"{srv_name}"] = hvec.is_running_service(srv_name) + for vul_name in cls.vul_idx_map: + readable_dict[f"{vul_name}"] = hvec.is_running_vul(vul_name) for proc_name in cls.process_idx_map: readable_dict[f"{proc_name}"] = hvec.is_running_process(proc_name) diff --git a/nasim/envs/network.py b/nasim/envs/network.py index 5284d1f4..ff65c611 100755 --- a/nasim/envs/network.py +++ b/nasim/envs/network.py @@ -22,17 +22,6 @@ def __init__(self, scenario): self.sensitive_addresses = scenario.sensitive_addresses self.sensitive_hosts = scenario.sensitive_hosts - def reset(self, state): - """Reset the network state to initial state """ - next_state = state.copy() - for host_addr in self.address_space: - host = next_state.get_host(host_addr) - host.compromised = False - host.access = AccessLevel.NONE - host.reachable = self.subnet_public(host_addr[0]) - host.discovered = host.reachable - return next_state - def perform_action(self, state, action): """Perform the given Action against the network. @@ -90,12 +79,52 @@ def perform_action(self, state, action): if action.is_subnet_scan(): return self._perform_subnet_scan(next_state, action) + if action.is_wiretapping(): + return self._perform_wiretapping(next_state, action) + + #if action.is_privilege_escalation() and t_host.is_running_process(action.process): + # self._perform_privilege_escalation(state, action) + # self._perform_privilege_escalation(next_state, action) + t_host = state.get_host(action.target) + + if action.is_privilege_escalation(): + has_proc = ( + action.process is None + or t_host.is_running_process(action.process) + ) + has_os = ( + action.os is None or t_host.is_running_os(action.os) + ) + if has_os and has_proc and action.req_access <= t_host.access: + self._perform_privilege_escalation(state, action) + self._perform_privilege_escalation(next_state, action) + next_host_state, action_obs = t_host.perform_action(action) next_state.update_host(action.target, next_host_state) self._update(next_state, action, action_obs) return next_state, action_obs + + + def _perform_privilege_escalation(self, next_state, action): + credentials = action.credentials_tofind + + if credentials != 0: + cred_found = {} + for h in self.hosts: + host = next_state.get_host(h) + if str(int(credentials)) in str(int(host.credentials_found)): + continue + else: + if int(host.credentials_found) == 0: + tmp = int(credentials) + else: + tmp = int(str(int(host.credentials_found)) + str(int(credentials))) + + host.credentials_found_set(tmp) + cred_found[h] = tmp + def _perform_subnet_scan(self, next_state, action): if not next_state.host_compromised(action.target): result = ActionResult(False, 0.0, connection_error=True) @@ -128,6 +157,46 @@ def _perform_subnet_scan(self, next_state, action): ) return next_state, obs + + def _perform_wiretapping(self, next_state, action): + # Get credentials_tofind then update each Hostvector + if not next_state.host_compromised(action.target): + result = ActionResult(False, 0.0, connection_error=True) + return next_state, result + + if not next_state.host_has_access(action.target, action.req_access): + result = ActionResult(False, 0.0, permission_error=True) + return next_state, result + + host = next_state.get_host(action.target) + credentials = host.credentials_tofind + #Update + + if credentials == 0: + result = ActionResult(True, 0.0) + return next_state, result + + cred_found = {} + for h in self.hosts: + host = next_state.get_host(h) + if str(int(credentials)) in str(int(host.credentials_found)): + continue + else: + if int(host.credentials_found) == 0: + tmp = int(credentials) + else: + tmp = int(str(int(host.credentials_found)) + str(int(credentials))) + + host.credentials_found_set(tmp) + cred_found[h] = tmp + + obs = ActionResult( + True, + cred_tofind=credentials, + cred_found=cred_found, + ) + return next_state, obs + def _update(self, state, action, action_obs): if action.is_exploit() and action_obs.success: self._update_reachable(state, action.target) @@ -233,6 +302,19 @@ def get_minimal_hops(self): def get_subnet_depths(self): return min_subnet_depth(self.topology) + + def reset(self, state): + """Reset the network state to initial state """ + next_state = state.copy() + for host_addr in self.address_space: + host = next_state.get_host(host_addr) + host.compromised = False + host.access = AccessLevel.NONE + host.cred_found = 0 + host.reachable = self.subnet_public(host_addr[0]) + host.discovered = host.reachable + return next_state + def __str__(self): output = "\n--- Network ---\n" output += "Subnets: " + str(self.subnets) + "\n" @@ -243,6 +325,7 @@ def __str__(self): for addr, value in self.sensitive_hosts.items(): output += f"\t{addr}: {value}\n" output += "Num_services: {self.scenario.num_services}\n" + output += "Num_Vul: {self.scenario.num_vul}\n" output += "Hosts:\n" for m in self.hosts.values(): output += str(m) + "\n" diff --git a/nasim/envs/observation.py b/nasim/envs/observation.py old mode 100644 new mode 100755 diff --git a/nasim/envs/state.py b/nasim/envs/state.py index f3addc19..fe1c8712 100755 --- a/nasim/envs/state.py +++ b/nasim/envs/state.py @@ -81,11 +81,6 @@ def from_numpy(cls, s_array, state_shape, host_num_map): s_array = s_array.reshape(state_shape) return State(s_array, host_num_map) - @classmethod - def reset(cls): - """Reset any class attributes for state """ - HostVector.reset() - @property def hosts(self): hosts = [] @@ -97,6 +92,7 @@ def copy(self): new_tensor = np.copy(self.tensor) return State(new_tensor, self.host_num_map) + def get_initial_observation(self, fully_obs): """Get the initial observation of network. @@ -161,7 +157,12 @@ def get_observation(self, action, action_result, fully_obs): services=False, processes=False, os=False, - access=False + access=False, + vul=False, + cred_tofind=False, + cred_found=False, + cred_needed=False + ) if action.is_exploit(): # exploit action, so get all observations for host @@ -169,12 +170,34 @@ def get_observation(self, action, action_result, fully_obs): obs_kwargs["services"] = True obs_kwargs["os"] = True obs_kwargs["access"] = True - obs_kwargs["value"] = True + obs_kwargs["vul"] = True + obs_kwargs["cred_needed"] = True + obs_kwargs["cred_found"] = True + + # if action.access == AccessLevel.ROOT: + if action_result.value > 0: + # Means exploit gained ROOT access for first time + # So observe value + obs_kwargs["value"] = True elif action.is_privilege_escalation(): obs_kwargs["compromised"] = True obs_kwargs["access"] = True + obs_kwargs["cred_found"] = True + obs_kwargs["cred_tofind"] = True + if action_result.value > 0: + # Means exploit gained ROOT access for first time + # So observe value + obs_kwargs["value"] = True elif action.is_service_scan(): obs_kwargs["services"] = True + + elif action.is_vul_scan(): + obs_kwargs["vul"] = True + + elif action.is_wiretapping(): + obs_kwargs["cred_found"] = True + obs_kwargs["cred_tofind"] = True + elif action.is_os_scan(): obs_kwargs["os"] = True elif action.is_process_scan(): @@ -211,10 +234,6 @@ def numpy_flat(self): def numpy(self): return self.tensor - def update_host(self, host_addr, host_vector): - host_idx = self.host_num_map[host_addr] - self.tensor[host_idx] = host_vector.vector - def get_host(self, host_addr): host_idx = self.host_num_map[host_addr] return HostVector(self.tensor[host_idx]) @@ -238,21 +257,15 @@ def host_discovered(self, host_addr): def host_has_access(self, host_addr, access_level): return self.get_host(host_addr).access >= access_level - def set_host_compromised(self, host_addr): - self.get_host(host_addr).compromised = True - - def set_host_reachable(self, host_addr): - self.get_host(host_addr).reachable = True - - def set_host_discovered(self, host_addr): - self.get_host(host_addr).discovered = True - def get_host_value(self, host_address): return self.hosts[host_address].get_value() def host_is_running_service(self, host_addr, service): return self.get_host(host_addr).is_running_service(service) + def host_is_running_vul(self, host_addr, vul): + return self.get_host(host_addr).is_running_vul(vul) + def host_is_running_os(self, host_addr, os): return self.get_host(host_addr).is_running_os(os) @@ -274,6 +287,25 @@ def get_readable(self): host_obs.append(readable_dict) return host_obs + + def update_host(self, host_addr, host_vector): + host_idx = self.host_num_map[host_addr] + self.tensor[host_idx] = host_vector.vector + + def set_host_compromised(self, host_addr): + self.get_host(host_addr).compromised = True + + def set_host_reachable(self, host_addr): + self.get_host(host_addr).reachable = True + + def set_host_discovered(self, host_addr): + self.get_host(host_addr).discovered = True + + @classmethod + def reset(cls): + """Reset any class attributes for state """ + HostVector.reset() + def __str__(self): output = "\n--- State ---\n" output += "Hosts:\n" diff --git a/nasim/envs/utils.py b/nasim/envs/utils.py old mode 100644 new mode 100755 index 7b4fca8c..51598160 --- a/nasim/envs/utils.py +++ b/nasim/envs/utils.py @@ -35,6 +35,17 @@ def __str__(self): def __repr__(self): return self.name +class VulState(enum.IntEnum): + # values for possible Vul knowledge states + UNKNOWN = 0 # Vul may or may not be running on host + PRESENT = 1 # Vul is running on the host + ABSENT = 2 # Vul not running on the host + + def __str__(self): + return self.name + + def __repr__(self): + return self.name class AccessLevel(enum.IntEnum): @@ -48,7 +59,6 @@ def __str__(self): def __repr__(self): return self.name - def get_minimal_hops_to_goal(topology, sensitive_addresses): """Get minimum network hops required to reach all sensitive hosts. diff --git a/nasim/scenarios/__init__.py b/nasim/scenarios/__init__.py old mode 100644 new mode 100755 diff --git a/nasim/scenarios/benchmark/__init__.py b/nasim/scenarios/benchmark/__init__.py old mode 100644 new mode 100755 diff --git a/nasim/scenarios/benchmark/generated.py b/nasim/scenarios/benchmark/generated.py old mode 100644 new mode 100755 index 8d6974a6..c7b98cf7 --- a/nasim/scenarios/benchmark/generated.py +++ b/nasim/scenarios/benchmark/generated.py @@ -29,9 +29,11 @@ base_host_value=1, host_discovery_value=1, step_limit=1000, - address_space_bounds=None + address_space_bounds=None, + num_vul=3 ) + # Generated Scenario definitions TINY_GEN = {**DEFAULTS, "name": "tiny-gen", @@ -106,6 +108,18 @@ "restrictiveness": 5, "step_limit": 30000} +POCP_2_GEN_CRED = {**DEFAULTS, + "name": "pocp-2-gen-cred", + "num_hosts": 95, + "num_os": 3, + "num_services": 10, + "num_exploits": 30, + "num_processes": 3, + "restrictiveness": 5, + "step_limit": 30000, + "num_vul": 30, + "num_cred": 4} + AVAIL_GEN_BENCHMARKS = { "tiny-gen": TINY_GEN, @@ -116,5 +130,6 @@ "large-gen": LARGE_GEN, "huge-gen": HUGE_GEN, "pocp-1-gen": POCP_1_GEN, - "pocp-2-gen": POCP_2_GEN + "pocp-2-gen": POCP_2_GEN, + "pocp-2-gen-cred": POCP_2_GEN_CRED } diff --git a/nasim/scenarios/benchmark/medium-cwp.yaml b/nasim/scenarios/benchmark/medium-cwp.yaml new file mode 100755 index 00000000..bfa09588 --- /dev/null +++ b/nasim/scenarios/benchmark/medium-cwp.yaml @@ -0,0 +1,257 @@ +# medium scenario +# with added credentials and vulnerabilities + +subnets: [1, 1, 5, 5, 4] + +topology: [[ 1, 1, 0, 0, 0, 0], + [ 1, 1, 1, 1, 0, 0], + [ 0, 1, 1, 1, 0, 0], + [ 0, 1, 1, 1, 1, 1], + [ 0, 0, 0, 1, 1, 0], + [ 0, 0, 0, 1, 0, 1]] + +sensitive_hosts: + (2, 0): 100 + (5, 0): 100 + +os: + - linux + - windows + +services: + - ssh + - ftp + - http + - samba + - smtp + +processes: + - tomcat + - daclsvc + - schtask + +exploits: + e_ssh: + service: ssh + os: linux + prob: 0.9 + cost: 3 + access: user + vul: None + credentials_needed: 0 + + e_ftp: + service: ftp + os: windows + prob: 0.6 + cost: 1 + access: root + vul: None + credentials_needed: 0 + + e_http: + service: http + os: None + prob: 0.9 + cost: 2 + access: user + vul: None + credentials_needed: 0 + + e_samba: + service: samba + os: linux + prob: 0.3 + cost: 2 + access: root + vul: None + credentials_needed: 0 + + e_smtp: + service: smtp + os: windows + prob: 0.6 + cost: 3 + access: user + vul: None + credentials_needed: 0 + +privilege_escalation: + pe_tomcat: + process: tomcat + os: linux + prob: 1.0 + cost: 1 + access: root + credentials_tofind: 0 + + pe_daclsvc: + process: daclsvc + os: windows + prob: 1.0 + cost: 1 + access: root + credentials_tofind: 0 + + pe_schtask: + process: schtask + os: windows + prob: 1.0 + cost: 1 + access: root + credentials_tofind: 0 + +service_scan_cost: 1 +os_scan_cost: 1 +subnet_scan_cost: 1 +process_scan_cost: 1 +vul_scan_cost: 0 +wiretapping_cost: 0 + +vul: + - none + +host_configurations: + (1, 0): + os: linux + services: [http] + processes: [] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (2, 0): + os: windows + services: [smtp] + processes: [schtask] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (3, 0): + os: windows + services: [ftp] + processes: [schtask] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (3, 1): + os: windows + services: [ftp, http] + processes: [daclsvc] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (3, 2): + os: windows + services: [ftp] + processes: [] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (3, 3): + os: windows + services: [ftp] + processes: [schtask] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (3, 4): + os: windows + services: [ftp] + processes: [schtask] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (4, 0): + os: linux + services: [ssh] + processes: [] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (4, 1): + os: linux + services: [ssh] + processes: [] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (4, 2): + os: linux + services: [ssh] + processes: [] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (4, 3): + os: windows + services: [ssh, ftp] + processes: [tomcat] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (4, 4): + os: windows + services: [ssh, ftp] + processes: [tomcat] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (5, 0): + os: linux + services: [ssh, samba] + processes: [] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (5, 1): + os: linux + services: [ssh, http] + processes: [tomcat] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (5, 2): + os: linux + services: [ssh] + processes: [] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (5, 3): + os: linux + services: [ssh] + processes: [] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + +firewall: + (0, 1): [http] + (1, 0): [] + (1, 2): [smtp] + (2, 1): [ssh] + (1, 3): [] + (3, 1): [ssh] + (2, 3): [http] + (3, 2): [smtp] + (3, 4): [ssh, ftp] + (4, 3): [ftp, ssh] + (3, 5): [ssh, ftp] + (5, 3): [ftp, ssh] + +step_limit: 2000 diff --git a/nasim/scenarios/benchmark/medium-multi-site.yaml b/nasim/scenarios/benchmark/medium-multi-site.yaml old mode 100644 new mode 100755 diff --git a/nasim/scenarios/benchmark/medium-single-site.yaml b/nasim/scenarios/benchmark/medium-single-site.yaml old mode 100644 new mode 100755 diff --git a/nasim/scenarios/benchmark/medium.yaml b/nasim/scenarios/benchmark/medium.yaml index b0566d67..f320dbea 100755 --- a/nasim/scenarios/benchmark/medium.yaml +++ b/nasim/scenarios/benchmark/medium.yaml @@ -17,28 +17,35 @@ # subnets: [1, 1, 5, 5, 4] + topology: [[ 1, 1, 0, 0, 0, 0], [ 1, 1, 1, 1, 0, 0], [ 0, 1, 1, 1, 0, 0], [ 0, 1, 1, 1, 1, 1], [ 0, 0, 0, 1, 1, 0], [ 0, 0, 0, 1, 0, 1]] + sensitive_hosts: (2, 0): 100 (5, 0): 100 + os: - linux - windows + services: - ssh - ftp - http - samba - smtp + processes: - tomcat - daclsvc - schtask + + exploits: e_ssh: service: ssh @@ -46,30 +53,36 @@ exploits: prob: 0.9 cost: 3 access: user + e_ftp: service: ftp os: windows prob: 0.6 cost: 1 access: root + e_http: service: http os: None prob: 0.9 cost: 2 access: user + e_samba: service: samba os: linux prob: 0.3 cost: 2 access: root + e_smtp: service: smtp os: windows prob: 0.6 cost: 3 access: user + + privilege_escalation: pe_tomcat: process: tomcat @@ -77,87 +90,108 @@ privilege_escalation: prob: 1.0 cost: 1 access: root + pe_daclsvc: process: daclsvc os: windows prob: 1.0 cost: 1 access: root + pe_schtask: process: schtask os: windows prob: 1.0 cost: 1 access: root + service_scan_cost: 1 os_scan_cost: 1 subnet_scan_cost: 1 process_scan_cost: 1 + + host_configurations: (1, 0): os: linux services: [http] processes: [] + (2, 0): os: windows services: [smtp] processes: [schtask] + (3, 0): os: windows services: [ftp] processes: [schtask] + (3, 1): os: windows services: [ftp, http] processes: [daclsvc] + (3, 2): os: windows services: [ftp] processes: [] + (3, 3): os: windows services: [ftp] processes: [schtask] + (3, 4): os: windows services: [ftp] processes: [schtask] + (4, 0): os: linux services: [ssh] processes: [] + (4, 1): os: linux services: [ssh] processes: [] + (4, 2): os: linux services: [ssh] processes: [] + (4, 3): os: windows services: [ssh, ftp] processes: [tomcat] + (4, 4): os: windows services: [ssh, ftp] processes: [tomcat] + (5, 0): os: linux services: [ssh, samba] processes: [] + (5, 1): os: linux services: [ssh, http] processes: [tomcat] + (5, 2): os: linux services: [ssh] processes: [] + (5, 3): os: linux services: [ssh] processes: [] + firewall: (0, 1): [http] (1, 0): [] @@ -171,4 +205,5 @@ firewall: (4, 3): [ftp, ssh] (3, 5): [ssh, ftp] (5, 3): [ftp, ssh] + step_limit: 2000 diff --git a/nasim/scenarios/benchmark/small-cwp.yaml b/nasim/scenarios/benchmark/small-cwp.yaml new file mode 100755 index 00000000..742b9503 --- /dev/null +++ b/nasim/scenarios/benchmark/small-cwp.yaml @@ -0,0 +1,177 @@ +subnets: [1,1,3,4] + +topology: [[ 1, 1, 0, 0, 0], + [ 1, 1, 1, 1, 1], + [ 0, 1, 1, 0, 0], + [ 0, 1, 0, 1, 0], + [ 0, 1, 0, 0, 1]] + +sensitive_hosts: + (4, 1): 100 + (4, 2): 100 + (4, 3): 100 + +os: + - Rasbian + - Penbox + +services: + - samba + - openssh + - init + +vul: + - CVE20072447 + +processes: + - shadow + + +exploits: + e_init: + service: init + os: None + vul : None + prob: 1.0 + cost: 1 + access: root + credentials_needed: 0 + + e_ssh_cred1_login: + service: openssh + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 1 + + e_ssh_cred2_login: + service: openssh + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 2 + + e_ssh_cred3_login: + service: openssh + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 3 + + e_samba: + service: samba + os: None + vul: CVE20072447 + prob: 1.0 + cost: 3 + access: root + credentials_needed: 0 + +privilege_escalation: + pe_shadow: + process: shadow + os: Rasbian + prob: 1.0 + cost: 3 + access: root + credentials_tofind: 3 + + +service_scan_cost: 1 +os_scan_cost: 1 +vul_scan_cost: 1 +subnet_scan_cost: 1 +process_scan_cost: 1 +wiretapping_cost: 1 + +host_configurations: + (1, 0): + os: Penbox + services: [init] + vul: [] + processes: [] + credentials_needed: 0 + credentials_tofind: 0 + + (2, 0): + os: Rasbian + services: [openssh, samba] + vul: [CVE20072447] + processes: [shadow] + credentials_needed: 0 + credentials_tofind: 1 + + (3, 0): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 3 + credentials_tofind: 2 + + (3, 1): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 1 + credentials_tofind: 2 + + (3, 2): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 1 + credentials_tofind: 2 + + (4, 0): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 1 + credentials_tofind: 0 + + (4, 1): + os: Rasbian + services: [openssh] + vul: [] + processes: [shadow] + credentials_needed: 1 + credentials_tofind: 0 + + (4, 2): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 2 + credentials_tofind: 0 + + (4, 3): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 3 + credentials_tofind: 0 + + +firewall: + (0, 1): [samba, openssh, init] + (1, 0): [samba, openssh, init] + (1, 2): [samba, openssh, init] + (2, 1): [samba, openssh, init] + (1, 3): [samba, openssh, init] + (3, 1): [samba, openssh, init] + (1, 4): [samba, openssh, init] + (4, 1): [samba, openssh, init] + +step_limit: 5000 \ No newline at end of file diff --git a/nasim/scenarios/benchmark/small-honeypot.yaml b/nasim/scenarios/benchmark/small-honeypot.yaml old mode 100644 new mode 100755 index 8636b524..941afc12 --- a/nasim/scenarios/benchmark/small-honeypot.yaml +++ b/nasim/scenarios/benchmark/small-honeypot.yaml @@ -16,24 +16,30 @@ # Score = 200 - (2 + 1 + 3 + 1 + 2 + 1 + 3 + 1) = 186 # subnets: [1, 1, 5, 1] + topology: [[ 1, 1, 0, 0, 0], [ 1, 1, 1, 1, 0], [ 0, 1, 1, 1, 0], [ 0, 1, 1, 1, 1], [ 0, 0, 0, 1, 1]] + sensitive_hosts: (2, 0): 100 (4, 0): 100 + os: - linux - windows + services: - ssh - ftp - http + processes: - tomcat - daclsvc + exploits: e_ssh: service: ssh @@ -41,18 +47,21 @@ exploits: prob: 0.9 cost: 3 access: user + e_ftp: service: ftp os: windows prob: 0.6 cost: 1 access: user + e_http: service: http os: None prob: 0.9 cost: 2 access: user + privilege_escalation: pe_tomcat: process: tomcat @@ -119,4 +128,5 @@ firewall: (3, 2): [ftp] (3, 4): [ssh, ftp] (4, 3): [ftp] + step_limit: 1000 diff --git a/nasim/scenarios/benchmark/small-linear.yaml b/nasim/scenarios/benchmark/small-linear.yaml old mode 100644 new mode 100755 index 0cb231fe..c319c9ed --- a/nasim/scenarios/benchmark/small-linear.yaml +++ b/nasim/scenarios/benchmark/small-linear.yaml @@ -19,6 +19,7 @@ # Score = 200 - (2+1+3+1+3+1+2+1+3+1+1+1) = 179 # subnets: [1, 1, 2, 1, 2, 1] + topology: [[ 1, 1, 0, 0, 0, 0, 1], # 0 connected to 1 and 6 [ 1, 1, 1, 0, 0, 0, 0], # 1 connected to 0 and 2 [ 0, 1, 1, 1, 0, 0, 0], # 2 connected to 1 and 3 @@ -26,19 +27,24 @@ topology: [[ 1, 1, 0, 0, 0, 0, 1], # 0 connected to 1 and 6 [ 0, 0, 0, 1, 1, 1, 0], # 4 connected to 3 and 5 [ 0, 0, 0, 0, 1, 1, 1], # 5 connected to 4 and 6 [ 1, 0, 0, 0, 0, 1, 1]] # 6 connected to 5 and 0 + sensitive_hosts: (3, 0): 100 (4, 0): 100 + os: - linux - windows + services: - ssh - ftp - http + processes: - tomcat - daclsvc + exploits: e_ssh: service: ssh @@ -46,18 +52,21 @@ exploits: prob: 0.9 cost: 3 access: user + e_ftp: service: ftp os: windows prob: 0.6 cost: 1 access: root + e_http: service: http os: None prob: 0.9 cost: 2 access: user + privilege_escalation: pe_tomcat: process: tomcat @@ -65,49 +74,62 @@ privilege_escalation: prob: 1.0 cost: 1 access: root + pe_daclsvc: process: daclsvc os: windows prob: 1.0 cost: 1 access: root + service_scan_cost: 1 os_scan_cost: 1 subnet_scan_cost: 1 process_scan_cost: 1 + + host_configurations: (1, 0): os: linux services: [http] processes: [] + (2, 0): os: linux services: [ssh, ftp] processes: [tomcat] + (3, 0): os: windows services: [ftp] processes: [] + (3, 1): os: linux services: [ssh] processes: [] + (4, 0): os: windows services: [http] processes: [daclsvc] + (5, 0): os: linux services: [ftp, ssh] processes: [] + (5, 1): os: windows services: [ftp] processes: [daclsvc] + (6, 0): os: linux services: [http] processes: [tomcat] + + # two row for each connection between subnets as defined by topology # one for each direction of connection # list which services to allow @@ -126,4 +148,5 @@ firewall: (6, 5): [ssh] (6, 0): [] (0, 6): [http] + step_limit: 1000 diff --git a/nasim/scenarios/benchmark/small-post.yaml b/nasim/scenarios/benchmark/small-post.yaml new file mode 100755 index 00000000..3bcb3130 --- /dev/null +++ b/nasim/scenarios/benchmark/small-post.yaml @@ -0,0 +1,177 @@ + +subnets: [1,1,3,4] + +topology: [[ 1, 1, 0, 0, 0], + [ 1, 1, 1, 1, 1], + [ 0, 1, 1, 0, 0], + [ 0, 1, 0, 1, 0], + [ 0, 1, 0, 0, 1]] + +sensitive_hosts: + (3, 0): 100 + (4, 1): 50 + (4, 2): 50 + (4, 3): 50 + +os: + - Rasbian + - Penbox + +services: + - samba + - openssh + - init + +vul: + - CVE20072447 + +processes: + - shadow1 + - shadow2 + +exploits: + e_init: + service: init + os: None + vul : None + prob: 1.0 + cost: 1 + access: root + credentials_needed: 0 + + e_ssh_cred1_login: + service: openssh + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 1 + + e_ssh_cred2_login: + service: openssh + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 2 + + e_samba: + service: samba + os: None + vul: CVE20072447 + prob: 1.0 + cost: 3 + access: root + credentials_needed: 0 + +privilege_escalation: + pe_shadow1: + process: shadow1 + os: Rasbian + prob: 1.0 + cost: 3 + access: root + credentials_tofind: 1 + + pe_shadow2: + process: shadow2 + os: Rasbian + prob: 1.0 + cost: 3 + access: root + credentials_tofind: 2 + +service_scan_cost: 1 +os_scan_cost: 1 +vul_scan_cost: 1 +subnet_scan_cost: 1 +process_scan_cost: 1 +wiretapping_cost: 1 + +host_configurations: + (1, 0): + os: Penbox + services: [init] + vul: [] + processes: [] + credentials_needed: 0 + credentials_tofind: 0 + + (2, 0): + os: Rasbian + services: [openssh, samba] + vul: [CVE20072447] + processes: [shadow1] + credentials_needed: 1 + credentials_tofind: 0 + + (3, 0): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 1 + credentials_tofind: 0 + + (3, 1): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 1 + credentials_tofind: 0 + + (3, 2): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 1 + credentials_tofind: 0 + + (4, 0): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 1 + credentials_tofind: 0 + + (4, 1): + os: Rasbian + services: [openssh] + vul: [] + processes: [shadow2] + credentials_needed: 1 + credentials_tofind: 0 + + (4, 2): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 2 + credentials_tofind: 0 + + (4, 3): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 2 + credentials_tofind: 0 + + +firewall: + (0, 1): [samba, openssh, init] + (1, 0): [samba, openssh, init] + (1, 2): [samba, openssh, init] + (2, 1): [samba, openssh, init] + (1, 3): [samba, openssh, init] + (3, 1): [samba, openssh, init] + (1, 4): [samba, openssh, init] + (4, 1): [samba, openssh, init] + +step_limit: 1000 \ No newline at end of file diff --git a/nasim/scenarios/benchmark/small-wire.yaml b/nasim/scenarios/benchmark/small-wire.yaml new file mode 100755 index 00000000..b17641a4 --- /dev/null +++ b/nasim/scenarios/benchmark/small-wire.yaml @@ -0,0 +1,169 @@ +subnets: [1,1,3,4] + +topology: [[ 1, 1, 0, 0, 0], + [ 1, 1, 1, 1, 1], + [ 0, 1, 1, 0, 0], + [ 0, 1, 0, 1, 0], + [ 0, 1, 0, 0, 1]] + +sensitive_hosts: + (3, 0): 100 + (4, 1): 50 + (4, 2): 50 + (4, 3): 50 + +os: + - Rasbian + - Penbox + +services: + - samba + - openssh + - init + +vul: + - CVE20072447 + +processes: + - shadow1 + - shadow2 + +exploits: + e_init: + service: init + os: None + vul : None + prob: 1.0 + cost: 1 + access: root + credentials_needed: 0 + + e_ssh_cred1_login: + service: openssh + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 1 + + e_ssh_cred2_login: + service: openssh + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 2 + + e_samba: + service: samba + os: None + vul: CVE20072447 + prob: 1.0 + cost: 3 + access: root + credentials_needed: 0 + +privilege_escalation: + pe_shadow1: + process: shadow1 + os: Rasbian + prob: 1.0 + cost: 3 + access: root + credentials_tofind: 0 + + +service_scan_cost: 1 +os_scan_cost: 1 +vul_scan_cost: 1 +subnet_scan_cost: 1 +process_scan_cost: 1 +wiretapping_cost: 1 + +host_configurations: + (1, 0): + os: Penbox + services: [init] + vul: [] + processes: [] + credentials_needed: 0 + credentials_tofind: 0 + + (2, 0): + os: Rasbian + services: [openssh, samba] + vul: [CVE20072447] + processes: [shadow1] + credentials_needed: 1 + credentials_tofind: 1 + + (3, 0): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 1 + credentials_tofind: 0 + + (3, 1): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 1 + credentials_tofind: 0 + + (3, 2): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 1 + credentials_tofind: 0 + + (4, 0): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 1 + credentials_tofind: 2 + + (4, 1): + os: Rasbian + services: [openssh] + vul: [] + processes: [shadow2] + credentials_needed: 1 + credentials_tofind: 2 + + (4, 2): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 2 + credentials_tofind: 2 + + (4, 3): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 2 + credentials_tofind: 2 + + +firewall: + (0, 1): [samba, openssh, init] + (1, 0): [samba, openssh, init] + (1, 2): [samba, openssh, init] + (2, 1): [samba, openssh, init] + (1, 3): [samba, openssh, init] + (3, 1): [samba, openssh, init] + (1, 4): [samba, openssh, init] + (4, 1): [samba, openssh, init] + +step_limit: 1000 \ No newline at end of file diff --git a/nasim/scenarios/benchmark/small.yaml b/nasim/scenarios/benchmark/small.yaml old mode 100644 new mode 100755 index e04ef182..1e6e1a98 --- a/nasim/scenarios/benchmark/small.yaml +++ b/nasim/scenarios/benchmark/small.yaml @@ -15,24 +15,30 @@ # Score = 200 - (2 + 1 + 3 + 1 + 2 + 1 + 3 + 1) = 186 # subnets: [1, 1, 5, 1] + topology: [[ 1, 1, 0, 0, 0], [ 1, 1, 1, 1, 0], [ 0, 1, 1, 1, 0], [ 0, 1, 1, 1, 1], [ 0, 0, 0, 1, 1]] + sensitive_hosts: (2, 0): 100 (4, 0): 100 + os: - linux - windows + services: - ssh - ftp - http + processes: - tomcat - daclsvc + exploits: e_ssh: service: ssh @@ -40,18 +46,22 @@ exploits: prob: 0.9 cost: 3 access: user + e_ftp: service: ftp os: windows prob: 0.6 cost: 1 access: user + e_http: service: http os: None prob: 0.9 cost: 2 access: user + + privilege_escalation: pe_tomcat: process: tomcat @@ -59,49 +69,63 @@ privilege_escalation: prob: 1.0 cost: 1 access: root + pe_daclsvc: process: daclsvc os: windows prob: 1.0 cost: 1 access: root + + service_scan_cost: 1 os_scan_cost: 1 subnet_scan_cost: 1 process_scan_cost: 1 + + host_configurations: (1, 0): os: linux services: [http] processes: [] + (2, 0): os: linux services: [ssh, ftp] processes: [tomcat] + (3, 0): os: windows services: [ftp] processes: [] + (3, 1): os: windows services: [ftp, http] processes: [daclsvc] + (3, 2): os: windows services: [ftp] processes: [daclsvc] + (3, 3): os: windows services: [ftp] processes: [] + (3, 4): os: windows services: [ftp] processes: [daclsvc] + (4, 0): os: linux services: [ssh, ftp] processes: [tomcat] + + # two row for each connection between subnets as defined by topology # one for each direction of connection # list which services to allow @@ -116,4 +140,5 @@ firewall: (3, 2): [ftp] (3, 4): [ssh, ftp] (4, 3): [ftp] + step_limit: 1000 diff --git a/nasim/scenarios/benchmark/tiny-cred.yaml b/nasim/scenarios/benchmark/tiny-cred.yaml new file mode 100755 index 00000000..3c672b48 --- /dev/null +++ b/nasim/scenarios/benchmark/tiny-cred.yaml @@ -0,0 +1,181 @@ +#Showing how to implement Vulnerabilities, Credentials and Wiretapping +#Based on Topology A of the Bachelor Thesis of Norman Becker + + +subnets: [1,4] + +topology: [[ 1, 1, 0], + [ 1, 1, 1], + [ 0, 1, 1]] + +sensitive_hosts: + (2, 1): 100 + (2, 2): 100 + (2, 3): 100 + +os: + - Rasbian + - Penbox + +services: + - samba3.0.20 + - samba4.1 + - opensmtp6.0.6 + - opensmtp8 + - vsftpd-2.3.4 + - vsftpd-6.0.0 + - proftpd-5 + - proftpd-1.3.3d + - xrdp + - mysql + - openssh + - ApacheJserver + - init + - http + +vul: + - CVE20072447 + - CVE20201938 + + +processes: + - shadow + +exploits: + e_init: + service: init + os: None + vul : None + prob: 1.0 + cost: 1 + access: root + credentials_needed: 0 + + e_ssh_cred1_login: + service: openssh + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 1 + + e_ssh_cred2_login: + service: openssh + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 2 + + e_samba3.0.20: + service: samba3.0.20 + os: None + vul: CVE20072447 + prob: 1.0 + cost: 3 + access: root + credentials_needed: 0 + + e_opensmtp6.0.6: + service: opensmtp6.0.6 + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 0 + + e_vsftpd-2.3.4: + service: vsftpd-2.3.4 + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 0 + + e_proftpd-1.3.3d: + service: proftpd-1.3.3d + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 0 + + + e_ApacheJserver: + service: ApacheJserver + os: None + vul: CVE20201938 + prob: 1.0 + cost: 3 + access: root + credentials_needed: 0 + +privilege_escalation: + pe_gain_Credentails_from_Shadow: + process: shadow + os: Rasbian + prob: 1.0 + cost: 3 + access: root + credentials_tofind: 1 + +service_scan_cost: 1 +os_scan_cost: 1 +vul_scan_cost: 1 +subnet_scan_cost: 1 +process_scan_cost: 1 +wiretapping_cost: 1 + +host_configurations: + (1, 0): + os: Penbox + services: [ init ] + vul: [] + processes: [ ] + credentials_needed: 0 + credentials_tofind: 0 + + (2, 0): + os: Rasbian + services: [ openssh,samba3.0.20 ] + vul: [ ] + processes: [ shadow ] + credentials_needed: 1 + credentials_tofind: 2 + + (2, 1): + os: Rasbian + services: [ openssh,samba3.0.20 ] #samba3.0.20 to hide + vul: [ CVE20072447 ] + processes: [ ] + credentials_needed: 1 + credentials_tofind: 0 + + (2, 2): + os: Rasbian + services: [openssh,http,opensmtp6.0.6] + vul: [] + processes: [] + credentials_needed: 1 + credentials_tofind: 2 + + (2, 3): + os: Rasbian + services: [ proftpd-1.3.3d,openssh ] + vul: [ ] + processes: [ ] + credentials_needed: 1 + credentials_tofind: 2 + +firewall: + (0, 1): [samba3.0.20, http, samba4.1, opensmtp6.0.6, opensmtp8, vsftpd-2.3.4, vsftpd-6.0.0, proftpd-5, proftpd-1.3.3d, xrdp, mysql, openssh, ApacheJserver, init] + (1, 0): [samba3.0.20, http, samba4.1, opensmtp6.0.6, opensmtp8, vsftpd-2.3.4, vsftpd-6.0.0, proftpd-5, proftpd-1.3.3d, xrdp, mysql, openssh, ApacheJserver, init] + (2, 1): [samba3.0.20, http, samba4.1, opensmtp6.0.6, opensmtp8, vsftpd-2.3.4, vsftpd-6.0.0, proftpd-5, proftpd-1.3.3d, xrdp, mysql, openssh, ApacheJserver, init] + (1, 2): [samba3.0.20, http, samba4.1, opensmtp6.0.6, opensmtp8, vsftpd-2.3.4, vsftpd-6.0.0, proftpd-5, proftpd-1.3.3d, xrdp, mysql, openssh, ApacheJserver, init] + +step_limit: 1000 \ No newline at end of file diff --git a/nasim/scenarios/benchmark/tiny-hard-cwp.yaml b/nasim/scenarios/benchmark/tiny-hard-cwp.yaml new file mode 100755 index 00000000..704207d3 --- /dev/null +++ b/nasim/scenarios/benchmark/tiny-hard-cwp.yaml @@ -0,0 +1,120 @@ +# tiny-hadrc scenario +# with added credentials and vulnerabilities +subnets: [1, 1, 1] + +topology: [[ 1, 1, 0, 0], + [ 1, 1, 1, 1], + [ 0, 1, 1, 1], + [ 0, 1, 1, 1]] + +sensitive_hosts: + (2, 0): 100 + (3, 0): 100 + +os: + - linux + - windows + +services: + - ssh + - ftp + - http + +processes: + - tomcat + - daclsvc + +exploits: + e_ssh: + service: ssh + os: linux + prob: 0.9 + cost: 3 + access: user + vul: None + credentials_needed: 0 + + e_ftp: + service: ftp + os: windows + prob: 0.6 + cost: 1 + access: root + vul: None + credentials_needed: 0 + + e_http: + service: http + os: None + prob: 0.9 + cost: 2 + access: user + vul: None + credentials_needed: 0 + +privilege_escalation: + pe_tomcat: + process: tomcat + os: linux + prob: 1.0 + cost: 1 + access: root + credentials_tofind: 0 + + pe_daclsvc: + process: daclsvc + os: windows + prob: 1.0 + cost: 1 + access: root + credentials_tofind: 0 + +service_scan_cost: 1 +os_scan_cost: 1 +subnet_scan_cost: 1 +process_scan_cost: 1 +vul_scan_cost: 0 +wiretapping_cost: 0 + +vul: + - none + +host_configurations: + (1, 0): + os: linux + services: [http] + processes: [] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (2, 0): + os: linux + services: [ssh, ftp] + processes: [tomcat] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (3, 0): + os: windows + services: [ftp] + processes: [daclsvc] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + +# two row for each connection between subnets as defined by topology +# one for each direction of connection +# list which services to allow +firewall: + (0, 1): [http] + (1, 0): [] + (1, 2): [ssh] + (2, 1): [ssh] + (1, 3): [] + (3, 1): [ssh] + (2, 3): [ftp, ssh] + (3, 2): [ftp, ssh] + +step_limit: 1000 diff --git a/nasim/scenarios/benchmark/tiny-hard.yaml b/nasim/scenarios/benchmark/tiny-hard.yaml old mode 100644 new mode 100755 index 76fff1d5..e04d5fdb --- a/nasim/scenarios/benchmark/tiny-hard.yaml +++ b/nasim/scenarios/benchmark/tiny-hard.yaml @@ -13,23 +13,29 @@ # Score = 200 - (2 + 1 + 3 + 1 + 1) = 192 # subnets: [1, 1, 1] + topology: [[ 1, 1, 0, 0], [ 1, 1, 1, 1], [ 0, 1, 1, 1], [ 0, 1, 1, 1]] + sensitive_hosts: (2, 0): 100 (3, 0): 100 + os: - linux - windows + services: - ssh - ftp - http + processes: - tomcat - daclsvc + exploits: e_ssh: service: ssh @@ -37,18 +43,22 @@ exploits: prob: 0.9 cost: 3 access: user + e_ftp: service: ftp os: windows prob: 0.6 cost: 1 access: root + e_http: service: http os: None prob: 0.9 cost: 2 access: user + + privilege_escalation: pe_tomcat: process: tomcat @@ -56,29 +66,38 @@ privilege_escalation: prob: 1.0 cost: 1 access: root + pe_daclsvc: process: daclsvc os: windows prob: 1.0 cost: 1 access: root + + service_scan_cost: 1 os_scan_cost: 1 subnet_scan_cost: 1 process_scan_cost: 1 + + host_configurations: (1, 0): os: linux services: [http] processes: [] + (2, 0): os: linux services: [ssh, ftp] processes: [tomcat] + (3, 0): os: windows services: [ftp] processes: [daclsvc] + + # two row for each connection between subnets as defined by topology # one for each direction of connection # list which services to allow @@ -91,4 +110,5 @@ firewall: (3, 1): [ssh] (2, 3): [ftp, ssh] (3, 2): [ftp, ssh] + step_limit: 1000 diff --git a/nasim/scenarios/benchmark/tiny-post.yaml b/nasim/scenarios/benchmark/tiny-post.yaml new file mode 100755 index 00000000..558a5a0e --- /dev/null +++ b/nasim/scenarios/benchmark/tiny-post.yaml @@ -0,0 +1,111 @@ +#Showing how to implement Vulnerabilities, Credentials and Wiretapping +#Based on Topology C of the Bachelor Thesis of Norman Becker + + +subnets: [1,1,1] + +topology: [[ 1, 1, 0, 0], + [ 1, 1, 1, 1], + [ 0, 1, 1, 0], + [ 0, 1, 0, 1]] + +sensitive_hosts: + (3, 0): 50 + +os: + - Rasbian + - Penbox + +services: + - samba + - openssh + - init + +vul: + - CVE20072447 + +processes: + - shadow + +exploits: + e_init: + service: init + os: None + vul : None + prob: 1.0 + cost: 1 + access: root + credentials_needed: 0 + + e_ssh_cred_login: + service: openssh + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 1 + + e_samba: + service: samba + os: None + vul: CVE20072447 + prob: 1.0 + cost: 3 + access: root + credentials_needed: 0 + +privilege_escalation: + pe_gain_Credentails_from_Shadow: + process: shadow + os: Rasbian + prob: 1.0 + cost: 3 + access: root + credentials_tofind: 1 + +service_scan_cost: 1 +os_scan_cost: 1 +vul_scan_cost: 1 +subnet_scan_cost: 1 +process_scan_cost: 1 +wiretapping_cost: 1 + +host_configurations: + (1, 0): + os: Penbox + services: [ init ] + vul: [] + processes: [ ] + credentials_needed: 0 + credentials_tofind: 0 + + (2, 0): + os: Rasbian + services: [openssh, samba] + vul: [CVE20072447] + processes: [shadow] + credentials_needed: 1 + credentials_tofind: 0 + + (3, 0): + os: Rasbian + services: [openssh] + vul: [] + processes: [] + credentials_needed: 1 + credentials_tofind: 0 + + +firewall: + (0, 1): [samba, openssh, init] + (1, 0): [samba, openssh, init] + (1, 2): [samba, openssh, init] + (2, 1): [samba, openssh, init] + (1, 3): [samba, openssh, init] + (3, 1): [samba, openssh, init] + +step_limit: 1000 + + + diff --git a/nasim/scenarios/benchmark/tiny-small-cwp.yaml b/nasim/scenarios/benchmark/tiny-small-cwp.yaml new file mode 100755 index 00000000..6c1d6c39 --- /dev/null +++ b/nasim/scenarios/benchmark/tiny-small-cwp.yaml @@ -0,0 +1,140 @@ +# tiny-small scenario but +# with added credentials and vulnerabilities + +subnets: [1, 1, 2, 1] + +topology: [[ 1, 1, 0, 0, 0], + [ 1, 1, 1, 1, 0], + [ 0, 1, 1, 1, 0], + [ 0, 1, 1, 1, 1], + [ 0, 0, 0, 1, 1]] + +sensitive_hosts: + (2, 0): 100 + (4, 0): 100 + +os: + - linux + - windows + +services: + - ssh + - ftp + - http + +processes: + - tomcat + - daclsvc + +exploits: + e_ssh: + service: ssh + os: linux + prob: 0.9 + cost: 3 + access: user + vul: None + credentials_needed: 0 + + e_ftp: + service: ftp + os: windows + prob: 0.6 + cost: 1 + access: root + vul: None + credentials_needed: 0 + + e_http: + service: http + os: None + prob: 0.9 + cost: 2 + access: user + vul: None + credentials_needed: 0 + +privilege_escalation: + pe_tomcat: + process: tomcat + os: linux + prob: 1.0 + cost: 1 + access: root + credentials_tofind: 0 + + pe_daclsvc: + process: daclsvc + os: windows + prob: 1.0 + cost: 1 + access: root + credentials_tofind: 0 + +service_scan_cost: 1 +os_scan_cost: 1 +subnet_scan_cost: 1 +process_scan_cost: 1 +vul_scan_cost: 0 +wiretapping_cost: 0 + +vul: + - none + +host_configurations: + (1, 0): + os: linux + services: [http] + processes: [tomcat] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (2, 0): + os: linux + services: [ssh, ftp] + processes: [tomcat] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (3, 0): + os: windows + services: [ftp] + processes: [] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (3, 1): + os: windows + services: [ftp, http] + processes: [daclsvc] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + + (4, 0): + os: windows + services: [ssh, ftp] + processes: [] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + +# two row for each connection between subnets as defined by topology +# one for each direction of connection +# list which services to allow +firewall: + (0, 1): [http] + (1, 0): [] + (1, 2): [ssh] + (2, 1): [ssh] + (1, 3): [] + (3, 1): [ssh] + (2, 3): [http] + (3, 2): [ftp] + (3, 4): [ssh, ftp] + (4, 3): [ftp] + +step_limit: 1000 diff --git a/nasim/scenarios/benchmark/tiny-small.yaml b/nasim/scenarios/benchmark/tiny-small.yaml old mode 100644 new mode 100755 index 1603b6e4..80803ef0 --- a/nasim/scenarios/benchmark/tiny-small.yaml +++ b/nasim/scenarios/benchmark/tiny-small.yaml @@ -15,24 +15,30 @@ # Score = 200 - (2 + 1 + 3 + 1 + 2 + 1 + 1) = 189 # subnets: [1, 1, 2, 1] + topology: [[ 1, 1, 0, 0, 0], [ 1, 1, 1, 1, 0], [ 0, 1, 1, 1, 0], [ 0, 1, 1, 1, 1], [ 0, 0, 0, 1, 1]] + sensitive_hosts: (2, 0): 100 (4, 0): 100 + os: - linux - windows + services: - ssh - ftp - http + processes: - tomcat - daclsvc + exploits: e_ssh: service: ssh @@ -40,18 +46,21 @@ exploits: prob: 0.9 cost: 3 access: user + e_ftp: service: ftp os: windows prob: 0.6 cost: 1 access: root + e_http: service: http os: None prob: 0.9 cost: 2 access: user + privilege_escalation: pe_tomcat: process: tomcat @@ -59,37 +68,45 @@ privilege_escalation: prob: 1.0 cost: 1 access: root + pe_daclsvc: process: daclsvc os: windows prob: 1.0 cost: 1 access: root + service_scan_cost: 1 os_scan_cost: 1 subnet_scan_cost: 1 process_scan_cost: 1 + host_configurations: (1, 0): os: linux services: [http] processes: [tomcat] + (2, 0): os: linux services: [ssh, ftp] processes: [tomcat] + (3, 0): os: windows services: [ftp] processes: [] + (3, 1): os: windows services: [ftp, http] processes: [daclsvc] + (4, 0): os: windows services: [ssh, ftp] processes: [] + # two row for each connection between subnets as defined by topology # one for each direction of connection # list which services to allow @@ -104,4 +121,5 @@ firewall: (3, 2): [ftp] (3, 4): [ssh, ftp] (4, 3): [ftp] + step_limit: 1000 diff --git a/nasim/scenarios/benchmark/tiny-wire.yaml b/nasim/scenarios/benchmark/tiny-wire.yaml new file mode 100755 index 00000000..cd01bb15 --- /dev/null +++ b/nasim/scenarios/benchmark/tiny-wire.yaml @@ -0,0 +1,104 @@ +#Showing how to implement Vulnerabilities, Credentials and Wiretapping +#Based on Topology B of the Bachelor Thesis of Norman Becker + + +subnets: [1, 1, 1] + +topology: [[ 1, 1, 0, 0], + [ 1, 1, 1, 1], + [ 0, 1, 1, 1], + [ 0, 1, 1, 1]] + +sensitive_hosts: + (3, 0): 100 + +os: + - Penbox + - Rasbian + +services: + - openssh + - init + - http + +vul: + - CVE20072447 + - CVE20201938 + + +processes: + - shadow + +exploits: + e_init: + service: init + os: None + vul : None + prob: 1.0 + cost: 1 + access: root + credentials_needed: 0 + + e_ssh_cred_login: + service: openssh + os: None + vul: None + prob: 1.0 + cost: 3 + access: root + credentials_needed: 1 + + +privilege_escalation: + pe_gain_Credentails_from_Shadow: + process: shadow + os: Rasbian + prob: 1.0 + cost: 3 + access: root + credentials_tofind: 0 + +service_scan_cost: 1 +os_scan_cost: 1 +vul_scan_cost: 1 +subnet_scan_cost: 1 +process_scan_cost: 1 +wiretapping_cost: 1 + +host_configurations: + + (1, 0): + os: Penbox + services: [ init ] + vul: [] + processes: [ ] + credentials_needed: 0 + credentials_tofind: 12 + + (2, 0): + os: Rasbian + services: [ openssh, http ] + vul: [] + processes: [ ] + credentials_needed: 0 + credentials_tofind: 1 + + (3, 0): + os: Rasbian + services: [ openssh, http ] + vul: [] + processes: [ ] + credentials_needed: 1 + credentials_tofind: 1 + +firewall: + (0, 1): [init] + (1, 0): [] + (1, 2): [http, openssh ] + (2, 1): [] + (1, 3): [http, openssh ] + (3, 1): [] + (2, 3): [http, openssh ] + (3, 2): [http, openssh ] + +step_limit: 5000 \ No newline at end of file diff --git a/nasim/scenarios/benchmark/tiny.yaml b/nasim/scenarios/benchmark/tiny.yaml index 079e0645..6893ab14 100755 --- a/nasim/scenarios/benchmark/tiny.yaml +++ b/nasim/scenarios/benchmark/tiny.yaml @@ -14,19 +14,25 @@ # Score = 200 - (6*1) = 195 # subnets: [1, 1, 1] + topology: [[ 1, 1, 0, 0], [ 1, 1, 1, 1], [ 0, 1, 1, 1], [ 0, 1, 1, 1]] + sensitive_hosts: (2, 0): 100 (3, 0): 100 + os: - linux + services: - ssh + processes: - tomcat + exploits: e_ssh: service: ssh @@ -34,6 +40,9 @@ exploits: prob: 0.8 cost: 1 access: user + vul: None + credentials_needed: 0 + privilege_escalation: pe_tomcat: process: tomcat @@ -41,15 +50,26 @@ privilege_escalation: prob: 1.0 cost: 1 access: root + credentials_tofind: 0 + service_scan_cost: 1 os_scan_cost: 1 subnet_scan_cost: 1 process_scan_cost: 1 +vul_scan_cost: 0 +wiretapping_cost: 0 + +vul: + - None + host_configurations: (1, 0): os: linux services: [ssh] processes: [tomcat] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 # which services to deny between individual hosts firewall: (3, 0): [ssh] @@ -57,12 +77,19 @@ host_configurations: os: linux services: [ssh] processes: [tomcat] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 firewall: (1, 0): [ssh] (3, 0): os: linux services: [ssh] processes: [tomcat] + vul: [] + credentials_needed: 0 + credentials_tofind: 0 + # two row for each connection between subnets as defined by topology # one for each direction of connection # list which services to allow @@ -75,4 +102,5 @@ firewall: (3, 1): [ssh] (2, 3): [ssh] (3, 2): [ssh] + step_limit: 1000 diff --git a/nasim/scenarios/generator.py b/nasim/scenarios/generator.py old mode 100644 new mode 100755 index 880cc37e..7e05c43d --- a/nasim/scenarios/generator.py +++ b/nasim/scenarios/generator.py @@ -46,6 +46,7 @@ class ScenarioGenerator: corresponding value in list For deterministic exploits set ``exploit_probs=1.0``. + This includes exploits upon a vulnerability. **Privilege Escalation Probabilities**: @@ -70,6 +71,8 @@ def generate(self, num_processes=2, num_exploits=None, num_privescs=None, + num_vul=0, + num_cred=0, r_sensitive=10, r_user=10, exploit_cost=1, @@ -80,6 +83,8 @@ def generate(self, os_scan_cost=1, subnet_scan_cost=1, process_scan_cost=1, + vul_scan_cost=0, + wiretapping_cost=0, uniform=False, alpha_H=2.0, alpha_V=2.0, @@ -101,6 +106,8 @@ def generate(self, number of hosts to include in network (minimum is 3) num_services : int number of services running on network (minimum is 1) + num_vul : int + number of vulnerabilites existing in network (minimum is 1) num_os : int, optional number of OS running on network (minimum is 1) (default=2) num_processes : int, optional @@ -132,6 +139,10 @@ def generate(self, cost for a subnet scan (default=1) process_scan_cost : int or float, optional cost for a process scan (default=1) + vul_scan_cost : int + cost for a vulnerability scan (default=0) + wiretapping_cost : int + cost for a wiretapping action (default=0) uniform : bool, optional whether to use uniform distribution or correlated host configs (default=False) @@ -183,6 +194,7 @@ def generate(self, assert 0 < r_sensitive and 0 < r_user assert 0 < alpha_H and 0 < alpha_V and 0 < lambda_V assert 0 < restrictiveness + assert 0 <= num_cred <= 9 if seed is not None: np.random.seed(seed) @@ -190,16 +202,28 @@ def generate(self, if num_exploits is None: num_exploits = num_services + if num_exploits > num_services: + num_exploits = num_services + if num_privescs is None: num_privescs = num_processes + + if num_vul > num_services: + num_vul = num_services + + self.serv_vul_map = {} + self.subnet_cred_map = {} + self.subnet_host_cred_map = {} self._generate_subnets(num_hosts) self._generate_topology() self._generate_address_space_bounds(address_space_bounds) self._generate_os(num_os) self._generate_services(num_services) + self._generate_vul(num_vul) + self._generate_service_vul_map() self._generate_processes(num_processes) - self._generate_exploits(num_exploits, exploit_cost, exploit_probs) + self._generate_exploits(num_exploits, exploit_cost, exploit_probs, num_cred) self._generate_privescs(num_privescs, privesc_cost, privesc_probs) self._generate_sensitive_hosts(r_sensitive, r_user, random_goal) self.base_host_value = base_host_value @@ -210,10 +234,17 @@ def generate(self, self._generate_correlated_hosts(alpha_H, alpha_V, lambda_V) self._ensure_host_vulnerability() self._generate_firewall(restrictiveness) + + self._distribute_credentials(num_cred) + self._distribute_locks() + self.service_scan_cost = service_scan_cost self.os_scan_cost = os_scan_cost self.subnet_scan_cost = subnet_scan_cost self.process_scan_cost = process_scan_cost + self.vul_scan_cost = vul_scan_cost + self.wiretapping_cost = wiretapping_cost + if name is None: name = f"gen_H{num_hosts}_E{num_exploits}_S{num_services}" @@ -241,6 +272,9 @@ def _construct_scenario(self): scenario_dict[u.FIREWALL] = self.firewall scenario_dict[u.HOSTS] = self.hosts scenario_dict[u.STEP_LIMIT] = self.step_limit + scenario_dict[u.VUL_SCAN_COST] = self.vul_scan_cost + scenario_dict[u.WIRETAPPING_COST] = self.wiretapping_cost + scenario_dict[u.VUL] = self.vul scenario = Scenario( scenario_dict, name=self.name, generated=True ) @@ -324,35 +358,136 @@ def _generate_address_space_bounds(self, address_space_bounds): def _generate_os(self, num_os): self.os = [f"os_{i}" for i in range(num_os)] - + + def _generate_vul(self, num_vul): + self.vul = [f"vul_{i}" for i in range(num_vul)] + + def _generate_service_vul_map(self): + # Randomly generate dependencies betweens a service and a vulnerability + mapped = {} + r_vul = np.random.permutation(self.vul) + i = 0 + while i < len(r_vul): + mapped[self.services[i]] = r_vul[i] + i += 1 + self.serv_vul_map = mapped + + def _distribute_credentials(self, num_cred): + #distribution of credentials to be found by Wiretapping() + #1. randomly assigned into each subnet + num_sub = len(self.subnets) + subnet_cred_list = [""] * num_sub + for cred in range(1, num_cred + 1): + subnet = np.random.randint(0, num_sub - 1) + subnet_cred_list[subnet] += str(cred) + + for i in range(1, num_sub + 1): + subnet_cred = subnet_cred_list[i - 1] + if subnet_cred != "": + self.subnet_cred_map[i] = int(subnet_cred_list[i - 1]) + + #2. distribute credentials to hosts in a subnet + #mapping of subnet-id's to a list consisting of credentials, randomly assigned + #list index corresponds to host-id + + for subnet, size in enumerate(self.subnets): + if subnet == u.INTERNET: + continue + else: + host_cred_list = [""] * size + for cred in subnet_cred_list[subnet]: + host_id = np.random.randint(0, size) + host_cred_list[host_id] += cred + self.subnet_host_cred_map[subnet] = host_cred_list + + self.update_host_cred_tofind() + + def update_host_cred_tofind(self): + for subnet, size in enumerate(self.subnets): + if subnet == u.INTERNET: + continue + else: + for host_id in range(size): + credentials = self.subnet_host_cred_map[subnet][host_id] + #credentials got distributed so update the value + if credentials != "": + self.hosts[(subnet, host_id)].cred_tofind = int(credentials) + + + def _distribute_locks(self): + #generate all host-id's, permutate and randomly 'lock' them using 'known' credentials while going through the hosts one by one + probability = 1 + found_credentials = [] + host_ids = [] + for subnet, size in enumerate(self.subnets): + if subnet == u.INTERNET: + continue + else: + for h in range(size): + host_ids.append((subnet, h)) + list(np.random.permutation(host_ids)) + + for id in host_ids: + host = self.hosts[id] + cred_tofind = host.cred_tofind + if found_credentials != []: + #assign the lock by chance + if np.random.choice([False, True],p=[1 - probability, probability]): + lock = np.random.choice(found_credentials) + self.update_host_cred_needed(id, lock) + #check for possible 'found' credentials + if cred_tofind > 0: + for c in str(cred_tofind): + if c not in found_credentials: + found_credentials.append(int(c)) + + + def update_host_cred_needed(self, host_id, cred_needed): + self.hosts[host_id].cred_needed = int(cred_needed) + + def _generate_services(self, num_services): self.services = [f"srv_{s}" for s in range(num_services)] def _generate_processes(self, num_processes): self.processes = [f"proc_{s}" for s in range(num_processes)] - def _generate_exploits(self, num_exploits, exploit_cost, exploit_probs): + def _generate_exploits(self, num_exploits, exploit_cost, exploit_probs, num_cred): exploits = {} exploit_probs = self._get_action_probs(num_exploits, exploit_probs) # add None since some exploits might work for all OS possible_os = self.os + [None] # we create one exploit per service + # if service is vulnerability bound, prob = 1 + e_names = [] exploits_added = 0 while exploits_added < num_exploits: + e_prob = exploit_probs[exploits_added] + vul = None srv = np.random.choice(self.services) - os = np.random.choice(possible_os) + if srv in self.serv_vul_map: + vul = self.serv_vul_map[srv] + os = None + e_prob = 1.0 + else: + os = np.random.choice(possible_os) al = np.random.randint(u.USER_ACCESS, u.ROOT_ACCESS+1) - e_name = f"e_{srv}" + e_name = f"e_{srv}" + f"_{vul}" if os is not None: e_name += f"_{os}" - if e_name not in exploits: - exploits[e_name] = { - u.EXPLOIT_SERVICE: srv, - u.EXPLOIT_OS: os, - u.EXPLOIT_PROB: exploit_probs[exploits_added], - u.EXPLOIT_COST: exploit_cost, - u.EXPLOIT_ACCESS: al - } + if e_name not in e_names: + e_names.append(e_name) + for c in range(num_cred + 1): + e_name_tmp = e_name + f"_{c}" + exploits[e_name_tmp] = { + u.EXPLOIT_SERVICE: srv, + u.EXPLOIT_OS: os, + u.EXPLOIT_PROB: e_prob, + u.EXPLOIT_COST: exploit_cost, + u.EXPLOIT_ACCESS: al, + u.EXPLOIT_VUL: vul, + u.EXPLOIT_CREDENTIALS_NEEDED: c + } exploits_added += 1 self.exploits = exploits @@ -380,7 +515,7 @@ def _generate_privescs(self, num_privesc, privesc_cost, privesc_probs): or all([os in os_choices for os in self.os]): break - # we create one exploit per service + # we create one privesc per process privescs_added = 0 while privescs_added < num_privesc: proc = np.random.choice(self.processes) @@ -394,7 +529,8 @@ def _generate_privescs(self, num_privesc, privesc_cost, privesc_probs): u.PRIVESC_OS: os, u.PRIVESC_PROB: privesc_probs[privescs_added], u.PRIVESC_COST: privesc_cost, - u.PRIVESC_ACCESS: u.ROOT_ACCESS + u.PRIVESC_ACCESS: u.ROOT_ACCESS, + u.PRIVESC_CREDENTIALS_TOFIND: 0 } privescs_added += 1 self.privescs = privescs @@ -445,6 +581,14 @@ def _generate_sensitive_hosts(self, r_sensitive, r_user, random_goal): # second last host in USER network is goal sensitive_hosts[(len(self.subnets)-1, self.subnets[-1]-1)] = r_user self.sensitive_hosts = sensitive_hosts + + def _generate_vul_cfg(self, srv_cfg): + # Generates a matching vul_cfg to a srv_cfg + vul_cfg = {} + for srv_cfg_key in srv_cfg.keys(): + if self.serv_vul_map.get(srv_cfg_key) is not None: + vul_cfg[self.serv_vul_map[srv_cfg_key]] = True + return vul_cfg def _generate_uniform_hosts(self): hosts = dict() @@ -457,7 +601,7 @@ def _generate_uniform_hosts(self): continue for h in range(size): srv_cfg = srv_config_set[np.random.choice(num_srv_configs)] - srv_cfg = self._convert_to_service_map(srv_cfg) + srv_cfg = self._convert_to_service_map(srv_cfg) proc_cfg = proc_config_set[np.random.choice(num_proc_configs)] proc_cfg = self._convert_to_process_map(proc_cfg) @@ -474,8 +618,13 @@ def _generate_uniform_hosts(self): processes=proc_cfg.copy(), firewall={}, value=value, - discovery_value=self.host_discovery_value + discovery_value=self.host_discovery_value, + vul = self._generate_vul_cfg(srv_cfg), + cred_needed=0, + cred_tofind=0 ) + print(host) + hosts[address] = host self.hosts = hosts @@ -567,7 +716,11 @@ def _generate_correlated_hosts(self, alpha_H, alpha_V, lambda_V): processes=process_cfg.copy(), firewall={}, value=value, - discovery_value=self.host_discovery_value + discovery_value=self.host_discovery_value, + vul=self._generate_vul_cfg(service_cfg), + + cred_tofind = 0, + cred_needed = 0 ) hosts[address] = host self.hosts = hosts diff --git a/nasim/scenarios/host.py b/nasim/scenarios/host.py index 8deb3fdd..3b5572b9 100755 --- a/nasim/scenarios/host.py +++ b/nasim/scenarios/host.py @@ -13,6 +13,10 @@ def __init__(self, services, processes, firewall, + vul, + cred_tofind, + cred_needed, + cred_found=0, value=0.0, discovery_value=0.0, compromised=False, @@ -38,6 +42,16 @@ def __init__(self, firewall assumes all services allowed value : float, optional value of the host (default=0.0) + + vul : list + List of vulnerabilities running on the host. Can be empty but not None + cred_tofind : int + credential which is held by the host, wiretapping + cred_needed : int + credential needed to succesfully perform exploits on the host + cred_found : int, optional + #string# of unique integers representing the credentials found over the whole network + discovery_value : float, optional the reward gained for discovering the host (default=0.0) compromised : bool, optional @@ -61,10 +75,17 @@ def __init__(self, self.reachable = reachable self.discovered = discovered self.access = access + self.vul = vul + self.cred_tofind = cred_tofind + self.cred_found = cred_found + self.cred_needed = cred_needed def is_running_service(self, service): return self.services[service] + def is_running_vul(self, vul): + return self.vul[vul] + def is_running_os(self, os): return self.os[os] @@ -81,6 +102,10 @@ def __str__(self): output.append(f"\treachable: {self.reachable}") output.append(f"\tvalue: {self.value}") output.append(f"\taccess: {self.access}") + output.append(f"\tcred_found: {self.cred_found}") + output.append(f"\tcred_needed: {self.cred_needed}") + output.append(f"\tcred_tofind: {self.cred_tofind}") + output.append("\tOS: {") for os_name, val in self.os.items(): @@ -92,6 +117,11 @@ def __str__(self): output.append(f"\t\t{name}: {val}") output.append("\t}") + output.append("\tvul: {") + for name, val in self.vul.items(): + output.append(f"\t\t{name}: {val}") + output.append("\t}") + output.append("\tprocesses: {") for name, val in self.processes.items(): output.append(f"\t\t{name}: {val}") diff --git a/nasim/scenarios/loader.py b/nasim/scenarios/loader.py index 16b5c3ae..a08e4bce 100755 --- a/nasim/scenarios/loader.py +++ b/nasim/scenarios/loader.py @@ -26,7 +26,11 @@ u.FIREWALL: dict } -OPTIONAL_CONFIG_KEYS = {u.STEP_LIMIT: int} +OPTIONAL_CONFIG_KEYS = {u.STEP_LIMIT: int, + u.WIRETAPPING_COST: (int, float), + u.VUL_SCAN_COST: (int, float), + u.VUL: list +} VALID_ACCESS_VALUES = ["user", "root", u.USER_ACCESS, u.ROOT_ACCESS] ACCESS_LEVEL_MAP = { @@ -44,6 +48,11 @@ u.EXPLOIT_ACCESS: (str, int) } +#optional privesc config keys +OPTIONAL_EXPLOIT_KEYS = {u.EXPLOIT_VUL: str, + u.EXPLOIT_CREDENTIALS_NEEDED: int +} + # required keys for privesc actions PRIVESC_KEYS = { u.PRIVESC_OS: str, @@ -53,6 +62,9 @@ u.PRIVESC_ACCESS: (str, int) } +#optional privesc config keys +OPTIONAL_PRIVESC_CONFIG_KEYS = {u.PRIVESC_CREDENTIALS_TOFIND: int} + # required keys for host configs HOST_CONFIG_KEYS = { u.HOST_OS: (str, None), @@ -60,6 +72,14 @@ u.HOST_PROCESSES: list } +#optional host config keys +OPTIONAL_HOST_CONFIG_KEYS = {u.HOST_VUL: list, + u.HOST_CREDENTIALS_NEEDED: int, + u.HOST_CREDENTIALS_TOFIND: int, + u.HOST_FIREWALL: dict, + u.HOST_VALUE: int +} + class ScenarioLoader: @@ -94,6 +114,7 @@ def load(self, file_path, name=None): self._parse_topology() self._parse_os() self._parse_services() + self._parse_vul() self._parse_processes() self._parse_sensitive_hosts() self._parse_exploits() @@ -111,12 +132,15 @@ def _construct_scenario(self): scenario_dict[u.TOPOLOGY] = self.topology scenario_dict[u.OS] = self.os scenario_dict[u.SERVICES] = self.services + scenario_dict[u.VUL] = self.vul scenario_dict[u.PROCESSES] = self.processes scenario_dict[u.SENSITIVE_HOSTS] = self.sensitive_hosts scenario_dict[u.EXPLOITS] = self.exploits scenario_dict[u.PRIVESCS] = self.privescs scenario_dict[u.OS_SCAN_COST] = self.os_scan_cost scenario_dict[u.SERVICE_SCAN_COST] = self.service_scan_cost + scenario_dict[u.VUL_SCAN_COST] = self.vul_scan_cost + scenario_dict[u.WIRETAPPING_COST] = self.wiretapping_cost scenario_dict[u.SUBNET_SCAN_COST] = self.subnet_scan_cost scenario_dict[u.PROCESS_SCAN_COST] = self.process_scan_cost scenario_dict[u.FIREWALL] = self.firewall @@ -131,6 +155,7 @@ def _check_scenario_sections_valid(self): they are valid type. """ # 0. check correct number of keys + #print(self.yaml_dict) assert len(self.yaml_dict) >= len(VALID_CONFIG_KEYS), \ (f"Too few config file keys: {len(self.yaml_dict)} " f"< {len(VALID_CONFIG_KEYS)}") @@ -208,6 +233,20 @@ def _validate_services(self, services): assert len(services) == len(set(services)), \ f"{services}. Services must not contain duplicates" + def _parse_vul(self): + vul = self.yaml_dict.get(u.VUL) + if vul is not None: + self._validate_vul(vul) + else: + vul = [] + self.vul = vul + + def _validate_vul(self, vul): + assert len(vul) >= 0, \ + f"{len(vul)}. Invalid number of vul, must be > 0" + assert len(vul) == len(set(vul)), \ + f"{vul}. vul must not contain duplicates" + def _parse_processes(self): processes = self.yaml_dict[u.PROCESSES] self._validate_processes(processes) @@ -215,7 +254,7 @@ def _parse_processes(self): def _validate_processes(self, processes): assert len(processes) >= 1, \ - f"{len(processes)}. Invalid number of services, must be > 0" + f"{len(processes)}. Invalid number of processes, must be > 0" assert len(processes) == len(set(processes)), \ f"{processes}. Processes must not contain duplicates" @@ -298,6 +337,10 @@ def _validate_single_exploit(self, e_name, e): assert k in e, f"{e_name}. Exploit missing key: '{k}'" assert isinstance(e[k], t), \ f"{e_name}. Exploit '{k}' incorrect type. Expected {t}" + + for e_key in e.keys(): + assert e_key in EXPLOIT_KEYS or e_key in OPTIONAL_EXPLOIT_KEYS, \ + (f"{e_name}. {e_key} is not a valid key for exploits") assert e[u.EXPLOIT_SERVICE] in self.services, \ (f"{e_name}. Exploit target service invalid: " @@ -310,11 +353,30 @@ def _validate_single_exploit(self, e_name, e): (f"{e_name}. Exploit target OS is invalid. '{e[u.EXPLOIT_OS]}'." " Should be None or one of the OS in the os list.") - assert 0 <= e[u.EXPLOIT_PROB] < 1, \ + assert 0 <= e[u.EXPLOIT_PROB] <= 1, \ (f"{e_name}. Exploit probability, '{e[u.EXPLOIT_PROB]}' not " "a valid probability") - assert e[u.EXPLOIT_COST] > 0, f"{e_name}. Exploit cost must be > 0." + e_vul = e.get(u.EXPLOIT_VUL) + if str(e_vul).lower() == "none": + e[u.EXPLOIT_VUL] = None + + assert e[u.EXPLOIT_VUL] is None or e[u.EXPLOIT_VUL] in self.vul, \ + (f"{e_name}. Exploit vulnerability, '{e[u.EXPLOIT_VUL]}' not " + "valid") + + e_cred_needed = e.get(u.EXPLOIT_CREDENTIALS_NEEDED) + if e_cred_needed is None: + e[u.EXPLOIT_CREDENTIALS_NEEDED] = 0 + + assert type(e[u.EXPLOIT_CREDENTIALS_NEEDED]) == int and len(str(e[u.EXPLOIT_CREDENTIALS_NEEDED])) == 1, \ + (f"{e_name}. credentials_needed, '{e[u.EXPLOIT_CREDENTIALS_NEEDED]}' has to be a single digit integer") + + e_cost = e.get(u.EXPLOIT_COST) + if e_cost is None: + e[u.EXPLOIT_COST] = 0 + + assert e[u.EXPLOIT_COST] >= 0, f"{e_name}. Exploit cost must be >= 0." assert e[u.EXPLOIT_ACCESS] in VALID_ACCESS_VALUES, \ (f"{e_name}. Exploit access value '{e[u.EXPLOIT_ACCESS]}' " @@ -332,7 +394,7 @@ def _validate_privescs(self, privescs): self._validate_single_privesc(pe_name, pe) def _validate_single_privesc(self, pe_name, pe): - s_name = "Priviledge Escalation" + s_name = "Privilege Escalation" assert isinstance(pe, dict), f"{pe_name}. {s_name} must be a dict." @@ -340,6 +402,10 @@ def _validate_single_privesc(self, pe_name, pe): assert k in pe, f"{pe_name}. {s_name} missing key: '{k}'" assert isinstance(pe[k], t), \ (f"{pe_name}. {s_name} '{k}' incorrect type. Expected {t}") + + for pe_key in pe.keys(): + assert pe_key in PRIVESC_KEYS or pe_key in OPTIONAL_PRIVESC_CONFIG_KEYS, \ + (f"{pe_name}. {pe_key} is not a valid key for privilige escalation") assert pe[u.PRIVESC_PROCESS] in self.processes, \ (f"{pe_name}. {s_name} target process invalid: " @@ -362,7 +428,15 @@ def _validate_single_privesc(self, pe_name, pe): assert pe[u.PRIVESC_ACCESS] in VALID_ACCESS_VALUES, \ (f"{pe_name}. {s_name} access value '{pe[u.PRIVESC_ACCESS]}' " f"invalid. Must be one of {VALID_ACCESS_VALUES}") - + + pe_cred_tofind = pe.get(u.PRIVESC_CREDENTIALS_TOFIND) + if pe_cred_tofind is None: + pe[u.PRIVESC_CREDENTIALS_TOFIND] = 0 + + assert type(pe[u.PRIVESC_CREDENTIALS_TOFIND]) == int and len(str(pe[u.PRIVESC_CREDENTIALS_TOFIND])) == 1, \ + (f"{pe_name}. {s_name} credentials_tofind, '{pe[u.PRIVESC_CREDENTIALS_TOFIND]}' has to be a single digit integer") + + if isinstance(pe[u.PRIVESC_ACCESS], str): pe[u.PRIVESC_ACCESS] = ACCESS_LEVEL_MAP[pe[u.PRIVESC_ACCESS]] @@ -371,11 +445,24 @@ def _parse_scan_costs(self): self.service_scan_cost = self.yaml_dict[u.SERVICE_SCAN_COST] self.subnet_scan_cost = self.yaml_dict[u.SUBNET_SCAN_COST] self.process_scan_cost = self.yaml_dict[u.PROCESS_SCAN_COST] + + vul_scan_cost = self.yaml_dict.get(u.VUL_SCAN_COST) + if vul_scan_cost is None: + vul_scan_cost = 0 + self.vul_scan_cost = vul_scan_cost + + wiretapping_cost = self.yaml_dict.get(u.WIRETAPPING_COST) + if wiretapping_cost is None: + wiretapping_cost = 0 + self.wiretapping_cost = wiretapping_cost + for (n, c) in [ ("OS", self.os_scan_cost), ("Service", self.service_scan_cost), + ("Vul", self.vul_scan_cost), ("Subnet", self.subnet_scan_cost), - ("Process", self.process_scan_cost) + ("Process", self.process_scan_cost), + ("Wiretapping", self.wiretapping_cost) ]: self._validate_scan_cost(n, c) @@ -420,6 +507,11 @@ def _validate_host_config(self, addr, cfg): for k in HOST_CONFIG_KEYS: assert k in cfg, f"{err_prefix} configuration missing key: {k}" + + #Check for invalid keys in Host Config + for cfg_key in cfg.keys(): + assert cfg_key in HOST_CONFIG_KEYS or cfg_key in OPTIONAL_HOST_CONFIG_KEYS, \ + (f"{cfg_key} is not a valid key") host_services = cfg[u.HOST_SERVICES] for service in host_services: @@ -431,6 +523,18 @@ def _validate_host_config(self, addr, cfg): (f"{err_prefix} configuration services list cannot contain " "duplicates") + host_vul = cfg.get(u.HOST_VUL) + if host_vul is None: + cfg[u.HOST_VUL] = [] + for v in cfg[u.HOST_VUL]: + assert v in self.vul, \ + (f"{err_prefix} Invalid vulnerability in configuration vul " + f"list: {service}") + + assert len(cfg[u.HOST_VUL]) == len(set(cfg[u.HOST_VUL])), \ + (f"{err_prefix} configuration vul list cannot contain " + "duplicates") + host_processes = cfg[u.HOST_PROCESSES] for process in host_processes: assert process in self.processes, \ @@ -440,6 +544,14 @@ def _validate_host_config(self, addr, cfg): assert len(host_processes) == len(set(host_processes)), \ (f"{err_prefix} configuation processes list cannot contain " "duplicates") + + cfg_cred = cfg.get(u.HOST_CREDENTIALS_NEEDED) + if cfg_cred is None: + cfg_cred = 0 + + assert type(cfg_cred) == int and len(str(cfg_cred)) == 1, \ + (f"{err_prefix} credentials_needed '{cfg[u.HOST_CREDENTIALS_NEEDED]}' " + "has to be a single digit integer") host_os = cfg[u.HOST_OS] assert host_os in self.os, \ @@ -545,12 +657,15 @@ def _parse_hosts(self): hosts = dict() for address, h_cfg in self.host_configs.items(): formatted_address = eval(address) - os_cfg, srv_cfg, proc_cfg = self._construct_host_config(h_cfg) + os_cfg, srv_cfg, vul_cfg, proc_cfg,cred_needed_cfg, cred_tofind_cfg = self._construct_host_config(h_cfg) value = self._get_host_value(formatted_address, h_cfg) hosts[formatted_address] = Host( address=formatted_address, os=os_cfg, - services=srv_cfg, + services=srv_cfg, + vul=vul_cfg, + cred_tofind=cred_tofind_cfg, + cred_needed=cred_needed_cfg, processes=proc_cfg, firewall=h_cfg[u.HOST_FIREWALL], value=value @@ -559,15 +674,35 @@ def _parse_hosts(self): def _construct_host_config(self, host_cfg): os_cfg = {} + h_os = host_cfg.get(u.HOST_OS) for os_name in self.os: - os_cfg[os_name] = os_name == host_cfg[u.HOST_OS] + os_cfg[os_name] = os_name == h_os + services_cfg = {} + h_services = host_cfg.get(u.HOST_SERVICES) for service in self.services: - services_cfg[service] = service in host_cfg[u.HOST_SERVICES] + services_cfg[service] = service in h_services + + h_vul = host_cfg.get(u.HOST_VUL) + if h_vul is None: + h_vul = [] + vul_cfg = {} + for v in self.vul: + vul_cfg[v] = v in h_vul + + cred_needed_cfg = host_cfg.get(u.HOST_CREDENTIALS_NEEDED) + if cred_needed_cfg is None: + cred_needed_cfg = 0 + + cred_tofind_cfg = host_cfg.get(u.HOST_CREDENTIALS_TOFIND) + if cred_tofind_cfg is None: + cred_tofind_cfg = 0 + processes_cfg = {} + h_processes = host_cfg.get(u.HOST_PROCESSES) for process in self.processes: - processes_cfg[process] = process in host_cfg[u.HOST_PROCESSES] - return os_cfg, services_cfg, processes_cfg + processes_cfg[process] = process in h_processes + return os_cfg, services_cfg, vul_cfg, processes_cfg, cred_needed_cfg, cred_tofind_cfg def _get_host_value(self, address, host_cfg): if address in self.sensitive_hosts: @@ -575,11 +710,9 @@ def _get_host_value(self, address, host_cfg): return float(host_cfg.get(u.HOST_VALUE, u.DEFAULT_HOST_VALUE)) def _parse_step_limit(self): - if u.STEP_LIMIT not in self.yaml_dict: - step_limit = None - else: - step_limit = self.yaml_dict[u.STEP_LIMIT] - assert step_limit > 0, \ - f"Step limit must be positive int: {step_limit} is invalid" + step_limit = self.yaml_dict.get(u.STEP_LIMIT) + + assert step_limit is None or step_limit > 0, \ + f"Step limit must be positive int or None: {step_limit} is invalid" self.step_limit = step_limit diff --git a/nasim/scenarios/scenario.py b/nasim/scenarios/scenario.py old mode 100644 new mode 100755 index c4c6ffb8..12c683f5 --- a/nasim/scenarios/scenario.py +++ b/nasim/scenarios/scenario.py @@ -12,6 +12,7 @@ def __init__(self, scenario_dict, name=None, generated=False): self.generated = generated self._e_map = None self._pe_map = None + self._cred = None # this is used for consistent positioning of # host state and obs in state and obs matrices @@ -30,6 +31,30 @@ def services(self): @property def num_services(self): return len(self.services) + + @property + def credentials(self): + if self._cred is None: + credentials = [] + for _, e_def in self.exploits.items(): + cred_needed = e_def[u.EXPLOIT_CREDENTIALS_NEEDED] + serv_name = e_def[u.EXPLOIT_SERVICE] + if cred_needed not in credentials: + credentials.append(cred_needed) + self._cred = credentials + return self._cred + + @property + def num_cred(self): + return len(self.credentials) + + @property + def vul(self): + return self.scenario_dict[u.VUL] + + @property + def num_vul(self): + return len(self.vul) @property def os(self): @@ -63,12 +88,15 @@ def privescs(self): def exploit_map(self): """A nested dictionary for all exploits in scenario. - I.e. {service_name: { - os_name: { - name: e_name, - cost: e_cost, - prob: e_prob, - access: e_access + I.e. {service_name: { + os_name: { + vul: { + name: e_name, + vul: e_vul, + cost: e_cost, + prob: e_prob, + access: e_access + cred_needed: e_cred_needed } } """ @@ -82,13 +110,25 @@ def exploit_map(self): os = e_def[u.EXPLOIT_OS] if os not in srv_map: - srv_map[os] = { + srv_map[os] = {} + srv_map2 = srv_map[os] + + vul = e_def[u.EXPLOIT_VUL] + if vul not in srv_map2: + srv_map2[vul] = {} + srv_map3 = srv_map2[vul] + + cred_needed = e_def[u.EXPLOIT_CREDENTIALS_NEEDED] + if cred_needed not in srv_map3: + srv_map3[vul] = { "name": e_name, u.EXPLOIT_SERVICE: srv_name, u.EXPLOIT_OS: os, + u.EXPLOIT_VUL: vul, u.EXPLOIT_COST: e_def[u.EXPLOIT_COST], u.EXPLOIT_PROB: e_def[u.EXPLOIT_PROB], - u.EXPLOIT_ACCESS: e_def[u.EXPLOIT_ACCESS] + u.EXPLOIT_ACCESS: e_def[u.EXPLOIT_ACCESS], + u.EXPLOIT_CREDENTIALS_NEEDED: e_def[u.EXPLOIT_CREDENTIALS_NEEDED] } self._e_map = e_map return self._e_map @@ -103,6 +143,7 @@ def privesc_map(self): cost: pe_cost, prob: pe_prob, access: pe_access + credentials_tofind: pe_credentials_tofind } } """ @@ -122,7 +163,8 @@ def privesc_map(self): u.PRIVESC_OS: os, u.PRIVESC_COST: pe_def[u.PRIVESC_COST], u.PRIVESC_PROB: pe_def[u.PRIVESC_PROB], - u.PRIVESC_ACCESS: pe_def[u.PRIVESC_ACCESS] + u.PRIVESC_ACCESS: pe_def[u.PRIVESC_ACCESS], + u.PRIVESC_CREDENTIALS_TOFIND: pe_def[u.PRIVESC_CREDENTIALS_TOFIND] } self._pe_map = pe_map return self._pe_map @@ -159,6 +201,14 @@ def address_space(self): def service_scan_cost(self): return self.scenario_dict[u.SERVICE_SCAN_COST] + @property + def vul_scan_cost(self): + return self.scenario_dict[u.VUL_SCAN_COST] + + @property + def wiretapping_cost(self): + return self.scenario_dict[u.WIRETAPPING_COST] + @property def os_scan_cost(self): return self.scenario_dict[u.OS_SCAN_COST] @@ -173,9 +223,7 @@ def process_scan_cost(self): @property def address_space_bounds(self): - return self.scenario_dict.get( - u.ADDRESS_SPACE_BOUNDS, (len(self.subnets), max(self.subnets)) - ) + return len(self.subnets), max(self.subnets) @property def host_value_bounds(self): @@ -215,9 +263,10 @@ def display(self): def get_action_space_size(self): num_exploits = len(self.exploits) num_privescs = len(self.privescs) + num_vul = len(self.vul) # OSScan, ServiceScan, SubnetScan, ProcessScan num_scans = 4 - actions_per_host = num_exploits + num_privescs + num_scans + actions_per_host = num_exploits + num_privescs + num_scans + num_vul return len(self.hosts) * actions_per_host def get_state_space_size(self): @@ -227,6 +276,7 @@ def get_state_space_size(self): host_aux_bin_features + self.num_os + self.num_services + + self.num_vul + self.num_processes ) # access @@ -243,6 +293,7 @@ def get_state_dims(self): + host_aux_features + self.num_os + self.num_services + + self.num_vul + self.num_processes ) return len(self.hosts), host_state_size @@ -259,6 +310,7 @@ def get_description(self): "Hosts": len(self.hosts), "OS": self.num_os, "Services": self.num_services, + "Vul": self.num_vul, "Processes": self.num_processes, "Exploits": len(self.exploits), "PrivEscs": len(self.privescs), diff --git a/nasim/scenarios/utils.py b/nasim/scenarios/utils.py old mode 100644 new mode 100755 index 21c5fe24..24c7dbfd --- a/nasim/scenarios/utils.py +++ b/nasim/scenarios/utils.py @@ -34,6 +34,9 @@ STEP_LIMIT = "step_limit" ACCESS_LEVELS = "access_levels" ADDRESS_SPACE_BOUNDS = "address_space_bounds" +VUL = "vul" +WIRETAPPING_COST = "wiretapping_cost" +VUL_SCAN_COST = "vul_scan_cost" # scenario exploit keys EXPLOIT_SERVICE = "service" @@ -41,6 +44,8 @@ EXPLOIT_PROB = "prob" EXPLOIT_COST = "cost" EXPLOIT_ACCESS = "access" +EXPLOIT_VUL = "vul" +EXPLOIT_CREDENTIALS_NEEDED = "credentials_needed" # scenario privilege escalation keys PRIVESC_PROCESS = "process" @@ -48,6 +53,7 @@ PRIVESC_PROB = "prob" PRIVESC_COST = "cost" PRIVESC_ACCESS = "access" +PRIVESC_CREDENTIALS_TOFIND = "credentials_tofind" # host configuration keys HOST_SERVICES = "services" @@ -55,7 +61,9 @@ HOST_OS = "os" HOST_FIREWALL = "firewall" HOST_VALUE = "value" - +HOST_VUL = "vul" +HOST_CREDENTIALS_NEEDED = "credentials_needed" +HOST_CREDENTIALS_TOFIND = "credentials_tofind" def load_yaml(file_path): """Load yaml file located at file path. diff --git a/nasim/scripts/__init__.py b/nasim/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/nasim/scripts/describe_scenarios.py b/nasim/scripts/describe_scenarios.py old mode 100644 new mode 100755 diff --git a/nasim/scripts/run_dqn_policy.py b/nasim/scripts/run_dqn_policy.py old mode 100644 new mode 100755 diff --git a/nasim/scripts/run_random_benchmarks.py b/nasim/scripts/run_random_benchmarks.py old mode 100644 new mode 100755 diff --git a/nasim/scripts/train_dqn.py b/nasim/scripts/train_dqn.py old mode 100644 new mode 100755 diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 00000000..a6f86194 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +Gymnasium==0.26.3 +matplotlib>=3.9.2 +networkx>=3.3 +numpy==1.26.0 +prettytable>=3.11.0 +pyyaml>=6.0.2 diff --git a/setup.py b/setup.py index 99035ecb..2a1e2874 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ 'sphinx-rtd-theme>=0.4' ], 'test': [ - 'pytest>=5.4' + 'pytest>=8.3.2' ] } @@ -45,15 +45,15 @@ def get_version(): if package.startswith('nasim') ], install_requires=[ - 'gymnasium>=0.26', - 'numpy>=1.18', - 'networkx>=2.4', - 'matplotlib>=3.1', - 'pyyaml>=5.3', - 'prettytable>=0.7' + 'gymnasium==0.26.3', + 'numpy==1.26.0', + 'networkx>=3.3', + 'matplotlib>=3.9.2', + 'pyyaml>=6.0.2', + 'prettytable>=3.11.0' ], extras_require=extras, - python_requires='>=3.8', + python_requires='>=3.12.4', package_data={ 'nasim': ['scenarios/benchmark/*.yaml'] }, @@ -65,7 +65,7 @@ def get_version(): 'Development Status :: 3 - Alpha', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.12.4', ], zip_safe=False ) diff --git a/test/test_bruteforce.py b/test/test_bruteforce.py old mode 100644 new mode 100755 index 30922a69..86ac590a --- a/test/test_bruteforce.py +++ b/test/test_bruteforce.py @@ -33,6 +33,7 @@ def test_bruteforce_static(scenario, seed, fully_obs, flat_actions, flat_obs): @pytest.mark.parametrize("fully_obs", [True, False]) @pytest.mark.parametrize("flat_actions", [True, False]) @pytest.mark.parametrize("flat_obs", [True, False]) +@pytest.mark.parametrize("flat_obs", [3, ]) def test_bruteforce_gen(scenario, seed, fully_obs, flat_actions, flat_obs): """Tests all generated benchmark scenarios using every possible environment setting, using bruteforce agent, checking for any errors @@ -42,5 +43,5 @@ def test_bruteforce_gen(scenario, seed, fully_obs, flat_actions, flat_obs): fully_obs=fully_obs, flat_actions=flat_actions, flat_obs=flat_obs, - render_mode=None) + render_mode=None,) run_bruteforce_agent(env, verbose=False) diff --git a/test/test_bruteforce_cwp.py b/test/test_bruteforce_cwp.py new file mode 100755 index 00000000..165440df --- /dev/null +++ b/test/test_bruteforce_cwp.py @@ -0,0 +1,21 @@ +"""Runs bruteforce agent on environment for different static scenarios and +using credentials, vulnerabilities and wiretapping +""" + +import pytest + +import nasim +from nasim import load +from nasim.agents.bruteforce_agent import run_bruteforce_agent + + +@pytest.mark.parametrize(("scenario"), ( + ("tiny-cred"),("tiny-wire"),("tiny-post"), + ("small-wire"),("small-post"),("small-cwp"), + ("tiny-small-cwp"),("tiny-hard-cwp"),("medium-cwp"))) + +def test_bruteforce_cwp(scenario): + + env = load('nasim/scenarios/benchmark/' + scenario + '.yaml') + + run_bruteforce_agent(env, verbose = False) \ No newline at end of file diff --git a/test/test_env.py b/test/test_env.py old mode 100644 new mode 100755 diff --git a/test/test_generator.py b/test/test_generator.py old mode 100644 new mode 100755 diff --git a/test/test_gym_bruteforce.py b/test/test_gym_bruteforce.py old mode 100644 new mode 100755