├── src └── nasimemu │ ├── nasim │ ├── agents │ │ ├── __init__.py │ │ ├── policies │ │ │ └── dqn_tiny.pt │ │ ├── random_agent.py │ │ ├── bruteforce_agent.py │ │ └── keyboard_agent.py │ ├── envs │ │ ├── __init__.py │ │ ├── gym_env.py │ │ ├── utils.py │ │ ├── observation.py │ │ ├── network.py │ │ └── state.py │ ├── README.txt │ ├── scripts │ │ ├── visualize_graph.py │ │ ├── describe_scenarios.py │ │ ├── run_dqn_policy.py │ │ ├── train_dqn.py │ │ └── run_random_benchmarks.py │ ├── scenarios │ │ ├── benchmark │ │ │ ├── tiny.yaml │ │ │ ├── __init__.py │ │ │ ├── tiny-hard.yaml │ │ │ ├── tiny-small.yaml │ │ │ ├── small.yaml │ │ │ ├── small-honeypot.yaml │ │ │ ├── small-linear.yaml │ │ │ ├── medium-single-site.yaml │ │ │ ├── medium.yaml │ │ │ ├── medium-multi-site.yaml │ │ │ └── generated.py │ │ ├── utils.py │ │ ├── __init__.py │ │ └── host.py │ ├── demo.py │ └── __init__.py │ ├── pymetasploit3 │ ├── __init__.py │ ├── README.txt │ ├── scripts │ │ ├── pymsfrpc.py │ │ └── pymsfconsole.py │ ├── utils.py │ └── msfconsole.py │ ├── __init__.py │ ├── vagrant_gen.py │ └── env_utils.py ├── vagrant ├── attacker │ ├── http_dir.txt │ ├── setup-network.py │ └── bootstrap.sh ├── target │ ├── bootstrap.sh │ ├── linux-up-down-script │ │ ├── down-phpwiki.sh │ │ ├── down-mysql.sh │ │ ├── down-proftpd.sh │ │ ├── down-apache2.sh │ │ ├── up-mysql.sh │ │ ├── up-apache2.sh │ │ ├── up-phpwiki.sh │ │ ├── up-proftpd.sh │ │ ├── up-drupal.sh │ │ └── down-drupal.sh │ ├── windows-up-down-script │ │ ├── down-mysql.ps1 │ │ ├── down-elasticsearch.ps1 │ │ ├── up-elasticsearch.ps1 │ │ ├── down-wp-ninja.ps1 │ │ ├── down-wamp.ps1 │ │ ├── down-windows-services.ps1 │ │ ├── up-mysql.ps1 │ │ ├── up-wamp.ps1 │ │ └── up-wp-ninja.ps1 │ ├── linux-insert-loot.sh │ ├── linux-setup-network.py │ ├── windows-insert-loot.ps1 │ ├── windows-setup-network.ps1 │ ├── linux-service-script.sh │ ├── windows-service-script.ps1 │ └── linux-install-script │ │ └── install-phpwiki-1.5.0.sh └── router │ ├── bootstrap-after-firewall.rsc │ └── bootstrap.rsc ├── .gitignore ├── scenarios ├── README.md ├── md_entry_dmz_one_subnet.v2.yaml ├── sm_entry_dmz_one_subnet.v2.yaml ├── sm_entry_dmz_two_subnets.v2.yaml ├── md_entry_dmz_two_subnets.v2.yaml ├── md_entry_user_three_subnets.v2.yaml ├── sm_entry_user_three_subnets.v2.yaml ├── sm_entry_dmz_three_subnets.v2.yaml ├── md_entry_dmz_three_subnets.v2.yaml ├── corp.v2.yaml ├── uni.v2.yaml └── visualize_scenario.py ├── setup_vagrant.sh ├── pyproject.toml ├── LICENSE ├── docs ├── SERVICES.md ├── EMULATION.md └── SCENARIOS.md └── README.md /src/nasimemu/nasim/agents/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vagrant/attacker/http_dir.txt: -------------------------------------------------------------------------------- 1 | wordpress 2 | drupal 3 | phpwiki 4 | uploads 5 | -------------------------------------------------------------------------------- /vagrant/target/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Bootstrapping target" 4 | -------------------------------------------------------------------------------- /vagrant/target/linux-up-down-script/down-phpwiki.sh: -------------------------------------------------------------------------------- 1 | rm -f -R /var/www/html/phpwiki -------------------------------------------------------------------------------- /vagrant/target/linux-up-down-script/down-mysql.sh: -------------------------------------------------------------------------------- 1 | sudo iptables -I INPUT 1 -p tcp --dport 3306 -j DROP 2 | -------------------------------------------------------------------------------- /vagrant/target/linux-up-down-script/down-proftpd.sh: -------------------------------------------------------------------------------- 1 | service proftpd stop 2 | update-rc.d -f proftpd remove -------------------------------------------------------------------------------- /vagrant/target/windows-up-down-script/down-mysql.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "Stopping mysql (no-op - already stopped)" -------------------------------------------------------------------------------- /src/nasimemu/nasim/envs/__init__.py: -------------------------------------------------------------------------------- 1 | from .gym_env import NASimGymEnv 2 | from .environment import NASimEnv 3 | -------------------------------------------------------------------------------- /vagrant/target/linux-up-down-script/down-apache2.sh: -------------------------------------------------------------------------------- 1 | 2 | service apache2 stop 3 | update-rc.d -f apache2 remove -------------------------------------------------------------------------------- /vagrant/target/linux-up-down-script/up-mysql.sh: -------------------------------------------------------------------------------- 1 | sudo iptables -D INPUT 1 # delete the first rule, added in down-mysql.sh 2 | -------------------------------------------------------------------------------- /vagrant/target/linux-up-down-script/up-apache2.sh: -------------------------------------------------------------------------------- 1 | echo "Enabling apache2" 2 | update-rc.d apache2 defaults 3 | service apache2 start -------------------------------------------------------------------------------- /vagrant/target/linux-up-down-script/up-phpwiki.sh: -------------------------------------------------------------------------------- 1 | echo "Enabling phpwiki" 2 | /vagrant/linux-install-script/install-phpwiki-1.5.0.sh -------------------------------------------------------------------------------- /vagrant/target/linux-up-down-script/up-proftpd.sh: -------------------------------------------------------------------------------- 1 | echo "Enabling proftpd" 2 | update-rc.d proftpd defaults 3 | service proftpd start -------------------------------------------------------------------------------- /vagrant/target/linux-up-down-script/up-drupal.sh: -------------------------------------------------------------------------------- 1 | 2 | echo "Enabling drupal" 3 | cp -a /home/vagrant/backup/drupal /var/www/html/drupal -------------------------------------------------------------------------------- /src/nasimemu/nasim/agents/policies/dqn_tiny.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaromiru/NASimEmu/HEAD/src/nasimemu/nasim/agents/policies/dqn_tiny.pt -------------------------------------------------------------------------------- /src/nasimemu/pymetasploit3/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | __all__ = [ 5 | 'msfconsole', 6 | 'msfrpc', 7 | 'utils' 8 | ] 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.egg-info 3 | _*/ 4 | vagrant/Vagrantfile 5 | vagrant/firewall.rsc 6 | scenarios/*.pdf 7 | scenarios/*.rsc 8 | scenarios/*.vagrant -------------------------------------------------------------------------------- /src/nasimemu/__init__.py: -------------------------------------------------------------------------------- 1 | import gym 2 | from .env import NASimEmuEnv 3 | 4 | gym.envs.registration.register(id='NASimEmu-v0', entry_point='nasimemu.env:NASimEmuEnv') 5 | -------------------------------------------------------------------------------- /vagrant/target/linux-insert-loot.sh: -------------------------------------------------------------------------------- 1 | echo 'Creating loot at /home/kylo_ren/loot' 2 | su kylo_ren -c 'echo LOOT=`date +%M-%S-%N | md5sum | cut -f 1 -d " "` > ~/loot; chmod 600 ~/loot' -------------------------------------------------------------------------------- /src/nasimemu/nasim/README.txt: -------------------------------------------------------------------------------- 1 | - forked from https://github.com/Jjschwartz/NetworkAttackSimulator/tree/2e443e6cfca54ff56e73bbe40288d90ecdd76615 (licensed under MIT) 2 | - several changes made to the simulator -------------------------------------------------------------------------------- /src/nasimemu/pymetasploit3/README.txt: -------------------------------------------------------------------------------- 1 | - forked from https://github.com/DanMcInerney/pymetasploit3/tree/9776da55b0abacfa843a32204f3972ec7d9b3de7 (licensed under MIT) 2 | - several changes made to stabilize the connection -------------------------------------------------------------------------------- /vagrant/target/windows-up-down-script/down-elasticsearch.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "Stopping elasticsearch" 2 | Set-Service -Name elasticsearch-service-x64 -StartupType Disabled 3 | Stop-Service -Name elasticsearch-service-x64 -------------------------------------------------------------------------------- /vagrant/target/windows-up-down-script/up-elasticsearch.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "Starting elasticsearch" 2 | 3 | Set-Service -Name elasticsearch-service-x64 -StartupType Automatic 4 | Start-Service -Name elasticsearch-service-x64 -------------------------------------------------------------------------------- /vagrant/target/windows-up-down-script/down-wp-ninja.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "Stopping wordpress ninja" 2 | 3 | 4 | $htaccess_file = "C:/wamp/www/wordpress/.htaccess" 5 | Set-Content $htaccess_file -Value "Deny from All" 6 | 7 | 8 | -------------------------------------------------------------------------------- /scenarios/README.md: -------------------------------------------------------------------------------- 1 | For each scenario, there is a `*.yaml` file, representing the nasim scenario, `*.vagrant` file for vagrant and `*.rsc` file for router (routerboard script). These are generated with `python -m nasimemu.vagrant_gen `. -------------------------------------------------------------------------------- /vagrant/target/windows-up-down-script/down-wamp.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "Stopping wamp" 2 | 3 | Set-Service -Name wampapache -StartupType Disabled 4 | Stop-Service -Name wampapache 5 | Set-Service -Name wampmysqld -StartupType Disabled 6 | Stop-Service -Name wampmysqld -------------------------------------------------------------------------------- /setup_vagrant.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $# -ne 1 ]] ; then 4 | echo 'Use: setup_vagrant.sh ' 5 | exit 1 6 | fi 7 | 8 | SCENARIO=$1 9 | python3 -m nasimemu.vagrant_gen $SCENARIO --dst vagrant/Vagrantfile --routeros vagrant/firewall.rsc -------------------------------------------------------------------------------- /vagrant/target/linux-up-down-script/down-drupal.sh: -------------------------------------------------------------------------------- 1 | 2 | mkdir -p /home/vagrant/backup 3 | if [ -d /var/www/html/drupal ] && [ ! -d /home/vagrant/backup/drupal ] 4 | then 5 | cp -a /var/www/html/drupal /home/vagrant/backup/drupal 6 | fi 7 | rm -f -R /var/www/html/drupal -------------------------------------------------------------------------------- /vagrant/target/windows-up-down-script/down-windows-services.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "Stopping unwanted services" 2 | 3 | Set-Service -Name w3svc -StartupType Disabled 4 | Stop-Service -Name w3svc 5 | 6 | Set-Service -Name ftpsvc -StartupType Disabled 7 | Stop-Service -Name ftpsvc -------------------------------------------------------------------------------- /vagrant/target/windows-up-down-script/up-mysql.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "Starting mysql (disabling firewall rule)" 2 | 3 | netsh advfirewall firewall delete rule name="Closed Port 3306 for MySQL" 4 | netsh advfirewall firewall add rule name="Open Port 3306 for MySQL" dir=in action=allow protocol=TCP localport=3306 -------------------------------------------------------------------------------- /vagrant/router/bootstrap-after-firewall.rsc: -------------------------------------------------------------------------------- 1 | /ip firewall filter add chain=forward action=drop log=yes log-prefix=denied 2 | 3 | # /ip firewall filter add chain=forward action=accept protocol=icmp src-address=192.168.0.0/16 dst-address=192.168.0.0/16 4 | # /ip firewall filter add chain=forward action=drop connection-state=invalid log=yes log-prefix=invalid 5 | 6 | :put "Firewall configuration:" 7 | /ip firewall filter print -------------------------------------------------------------------------------- /vagrant/attacker/setup-network.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os, argparse 4 | 5 | # ----- 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument('-subnetid', type=int) 8 | args = parser.parse_args() 9 | 10 | # ----- 11 | 12 | for subnet in range(10): 13 | if subnet == args.subnetid: 14 | continue 15 | 16 | os.system('route add -net 192.168.{remote_subnet}.0/24 gw 192.168.{local_subnet}.10'.format(remote_subnet=subnet, local_subnet=args.subnetid)) 17 | -------------------------------------------------------------------------------- /vagrant/target/linux-setup-network.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os, argparse 4 | 5 | # ----- 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument('-subnetid', type=int) 8 | args = parser.parse_args() 9 | 10 | # ----- 11 | 12 | for subnet in range(10): 13 | if subnet == args.subnetid: 14 | continue 15 | 16 | os.system('route add -net 192.168.{remote_subnet}.0/24 gw 192.168.{local_subnet}.10'.format(remote_subnet=subnet, local_subnet=args.subnetid)) 17 | -------------------------------------------------------------------------------- /vagrant/target/windows-up-down-script/up-wamp.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "Starting wamp" 2 | 3 | $wampConfig = "C:\wamp\bin\apache\Apache2.2.21\conf\httpd.conf" 4 | 5 | (Get-Content $wampConfig) -Replace 'Listen 8585', 'Listen 80' | Set-Content $wampConfig 6 | (Get-Content $wampConfig) -Replace 'ServerName localhost:8585', 'ServerName localhost:80' | Set-Content $wampConfig 7 | 8 | Set-Service -Name wampmysqld -StartupType Automatic 9 | Start-Service -Name wampmysqld 10 | 11 | Set-Service -Name wampapache -StartupType Automatic 12 | Start-Service -Name wampapache -------------------------------------------------------------------------------- /vagrant/target/windows-insert-loot.ps1: -------------------------------------------------------------------------------- 1 | 2 | $loot_path = "C:/loot" 3 | Write-Host "Creating loot at $loot_path" 4 | 5 | $enc = [system.Text.Encoding]::UTF8 6 | $date = Get-Date -Format "mm-ss-FFFFFFF" 7 | $date_in_byte = $enc.GetBytes($date) 8 | $md5 = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider 9 | 10 | # Issue with dash 11 | $hash = [System.BitConverter]::ToString($md5.ComputeHash($date_in_byte)) 12 | 13 | Set-Content $loot_path -Value "LOOT=$hash" 14 | icacls $loot_path /setowner kylo_ren 15 | icacls $loot_path /deny BUILTIN\Users:F 16 | -------------------------------------------------------------------------------- /vagrant/target/windows-setup-network.ps1: -------------------------------------------------------------------------------- 1 | 2 | 3 | param ( 4 | [string]$subnetid = '0' 5 | ) 6 | 7 | 8 | for($remote_subnet = 0; $remote_subnet -lt 10; $remote_subnet++) { 9 | if($remote_subnet -ne $subnetid){ 10 | route -p ADD "192.168.$remote_subnet.0" MASK "255.255.255.0" "192.168.$subnetid.10" 11 | } 12 | } 13 | 14 | # allow 4444 port (for bind payloads), ping & enable firewall 15 | Write-Host "Enabling firewall" 16 | netsh advfirewall firewall add rule name="Open Port 4444" dir=in action=allow protocol=TCP localport=4444 17 | netsh advfirewall firewall add rule name="ICMP Allow incoming V4 echo request" protocol=icmpv4:8,any dir=in action=allow 18 | netsh advfirewall set allprofiles state on 19 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scripts/visualize_graph.py: -------------------------------------------------------------------------------- 1 | """Environment network graph visualizer 2 | 3 | This script allows the user to visualize the network graph for a chosen 4 | benchmark scenario. 5 | """ 6 | 7 | from nasimemu import nasim 8 | 9 | 10 | if __name__ == "__main__": 11 | import argparse 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument("scenario_name", type=str, 14 | help="benchmark scenario name") 15 | parser.add_argument("-s", "--seed", type=int, default=0, 16 | help="random seed (default=0)") 17 | args = parser.parse_args() 18 | 19 | env = nasim.make_benchmark(args.scenario_name, args.seed) 20 | env.render_network_graph(show=True) 21 | -------------------------------------------------------------------------------- /vagrant/attacker/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Bootstrapping attacker" 4 | sudo systemctl stop NetworkManager.service 5 | 6 | su vagrant -c 'msfrpcd -P msfpassword' 7 | 8 | sudo apt update 9 | sudo apt install -y lynx # lynx is a useful tool for debugging http servers 10 | echo "accept_all_cookies=on" >> /home/vagrant/.lynxrc 11 | 12 | #sudo apt remove -y libwacom2 13 | #sudo apt install -y libwacom9 14 | #sudo apt upgrade -y metasploit-framework 15 | 16 | # partial search in console... 17 | echo 'autoload -U up-line-or-beginning-search' >> ~/.zshrc 18 | echo 'autoload -U down-line-or-beginning-search' >> ~/.zshrc 19 | echo 'zle -N up-line-or-beginning-search' >> ~/.zshrc 20 | echo 'zle -N down-line-or-beginning-search' >> ~/.zshrc 21 | echo 'bindkey "$key[Up]" up-line-or-beginning-search' >> ~/.zshrc 22 | echo 'bindkey "$key[Down]" down-line-or-beginning-search' >> ~/.zshrc -------------------------------------------------------------------------------- /vagrant/target/windows-up-down-script/up-wp-ninja.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string]$ip = '127.0.0.1' 3 | ) 4 | 5 | Write-Host "Starting wordpresss" 6 | 7 | 8 | $htaccess_file = "C:/wamp/www/wordpress/.htaccess" 9 | Set-Content $htaccess_file -Value "" 10 | 11 | $script_location = "C:/Program Files/wordpress/update_ip.ps1" 12 | $script_fixed_location = "C:/Program Files/wordpress/update_ip_fix.ps1" 13 | 14 | set-content $script_fixed_location -Value ("`$ipaddr = `"$ip`"`n") 15 | get-content -Path $script_location -Tail 15 | add-Content $script_fixed_location 16 | 17 | 18 | (Get-Content $script_fixed_location) -Replace ':8585', '' | Set-Content $script_fixed_location 19 | 20 | 21 | schtasks /End /tn "update_wp_db" 22 | schtasks /Delete /tn "update_wp_db" /f 23 | schtasks /Create /tn "update_wp_db_fix" /tr "'cmd.exe' /c powershell -File '$script_fixed_location'" /sc onstart /NP /ru "SYSTEM" /f 24 | schtasks /Run /tn "update_wp_db_fix" -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "nasimemu" 7 | version = "0.9.0" 8 | authors = [ 9 | {name="Jaromir Janisch", email="author@jaromiru.com"} 10 | ] 11 | description = "Gym based environment for training offensive RL agents in simulated and emulated computer networks." 12 | readme = "README.md" 13 | dependencies = [ 14 | "gym == 0.21.0", 15 | "networkx", # For nasim render 16 | "prettytable", # For nasim render 17 | "matplotlib", # For nasim render (suppress warning) 18 | "pyyaml", # For nasim 19 | "msgpack", # For pymetasploit3 20 | "requests", # For pymetasploit3 21 | "retry", # For pymetasploit3 22 | "pillow", # For nasim_problem rendering 23 | "plotly", # For nasim_problem debug 24 | "scipy", # For networkx 25 | # "pygame", # For nasim play 26 | # "kaleido", # Image with plotly for nasim play 27 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jaromír Janisch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/nasimemu/pymetasploit3/scripts/pymsfrpc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from code import InteractiveConsole 4 | from atexit import register 5 | from os import path 6 | import readline 7 | 8 | from nasimemu.pymetasploit3.msfrpc import MsfRpcClient, MsfRpcError 9 | from nasimemu.pymetasploit3.utils import parseargs 10 | 11 | 12 | class MsfRpc(InteractiveConsole): 13 | def __init__(self, password, **kwargs): 14 | self.client = MsfRpcClient(password, **kwargs) 15 | InteractiveConsole.__init__(self, {'rpc' : self.client}, '') 16 | self.init_history(path.expanduser('~/.msfrpc_history')) 17 | 18 | def init_history(self, histfile): 19 | readline.parse_and_bind('tab: complete') 20 | if hasattr(readline, "read_history_file"): 21 | try: 22 | readline.read_history_file(histfile) 23 | except IOError: 24 | pass 25 | register(self.save_history, histfile) 26 | 27 | def save_history(self, histfile): 28 | readline.write_history_file(histfile) 29 | 30 | 31 | if __name__ == '__main__': 32 | o = parseargs() 33 | try: 34 | m = MsfRpc(o.__dict__.pop('password'), **o.__dict__) 35 | m.interact('') 36 | except MsfRpcError as m: 37 | print(str(m)) 38 | exit(-1) 39 | exit(0) 40 | -------------------------------------------------------------------------------- /vagrant/target/linux-service-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | apache2IsStarted=0 4 | 5 | 6 | disable_everything() 7 | { 8 | echo "Disabling all services" 9 | 10 | /vagrant/linux-up-down-script/down-apache2.sh 11 | /vagrant/linux-up-down-script/down-drupal.sh 12 | /vagrant/linux-up-down-script/down-phpwiki.sh 13 | /vagrant/linux-up-down-script/down-proftpd.sh 14 | /vagrant/linux-up-down-script/down-mysql.sh 15 | } 16 | 17 | apache2() 18 | { 19 | if [ $apache2IsStarted -eq 0 ] 20 | then 21 | apache2IsStarted=1 22 | /vagrant/linux-up-down-script/up-apache2.sh 23 | fi 24 | } 25 | 26 | drupal() 27 | { 28 | apache2 29 | /vagrant/linux-up-down-script/up-drupal.sh 30 | } 31 | 32 | proftpd() 33 | { 34 | apache2 35 | /vagrant/linux-up-down-script/up-proftpd.sh 36 | } 37 | 38 | phpwiki(){ 39 | apache2 40 | /vagrant/linux-up-down-script/up-phpwiki.sh 41 | } 42 | 43 | mysql(){ 44 | /vagrant/linux-up-down-script/up-mysql.sh 45 | } 46 | 47 | disable_everything 48 | 49 | for option in "$@" 50 | do 51 | case $option in 52 | "80_linux_drupal") 53 | drupal 54 | ;; 55 | "21_linux_proftpd") 56 | proftpd 57 | ;; 58 | "80_linux_phpwiki") 59 | phpwiki 60 | ;; 61 | "3306_any_mysql") 62 | mysql 63 | ;; 64 | *) 65 | >&2 echo "Unknown arg : $option" 66 | ;; 67 | esac 68 | done 69 | -------------------------------------------------------------------------------- /vagrant/target/windows-service-script.ps1: -------------------------------------------------------------------------------- 1 | 2 | param ( 3 | [string[]]$services = '', 4 | [string]$ip = '127.0.0.1' 5 | ) 6 | 7 | 8 | function ResetEverything 9 | { 10 | C:/vagrant/windows-up-down-script/down-windows-services.ps1 11 | # be careful, there are some dependencies between wamp, mysql and wp-ninja 12 | # C:/vagrant/windows-up-down-script/down-wamp.ps1 13 | C:/vagrant/windows-up-down-script/up-wamp.ps1 14 | # C:/vagrant/windows-up-down-script/down-mysql.ps1 15 | C:/vagrant/windows-up-down-script/down-wp-ninja.ps1 16 | C:/vagrant/windows-up-down-script/down-elasticsearch.ps1 17 | 18 | } 19 | 20 | 21 | function WordpressNinja 22 | { 23 | # C:/vagrant/windows-up-down-script/up-wamp.ps1 24 | C:/vagrant/windows-up-down-script/up-wp-ninja.ps1 -ip $ip 25 | } 26 | 27 | function ElasticSearch 28 | { 29 | C:/vagrant/windows-up-down-script/up-elasticsearch.ps1 30 | } 31 | 32 | function MySQL 33 | { 34 | C:/vagrant/windows-up-down-script/up-mysql.ps1 35 | } 36 | 37 | ResetEverything 38 | if($services){ 39 | foreach ($service in $services.Split(',')) { 40 | switch($service){ 41 | "80_windows_wp_ninja"{ 42 | WordpressNinja 43 | } 44 | "9200_windows_elasticsearch"{ 45 | ElasticSearch 46 | } 47 | "3306_any_mysql"{ 48 | MySQL 49 | } 50 | default{ 51 | Write-Error "Unknown arg $service" 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/nasimemu/nasim/envs/gym_env.py: -------------------------------------------------------------------------------- 1 | from .environment import NASimEnv 2 | from nasimemu.nasim.scenarios import Scenario, make_benchmark_scenario 3 | 4 | 5 | class NASimGymEnv(NASimEnv): 6 | """A wrapper around the NASimEnv compatible with OpenAI gym.make() 7 | 8 | See nasim.NASimEnv for details. 9 | """ 10 | 11 | def __init__(self, 12 | scenario, 13 | fully_obs=False, 14 | flat_actions=True, 15 | flat_obs=True): 16 | """ 17 | Parameters 18 | ---------- 19 | scenario : str or or nasim.scenarios.Scenario 20 | either the name of benchmark environment (str) or a nasim Scenario 21 | instance 22 | fully_obs : bool, optional 23 | the observability mode of environment, if True then uses fully 24 | observable mode, otherwise partially observable (default=False) 25 | flat_actions : bool, optional 26 | if true then uses a flat action space, otherwise will use 27 | parameterised action space (default=True). 28 | flat_obs : bool, optional 29 | if true then uses a 1D observation space. If False 30 | will use a 2D observation space (default=True) 31 | """ 32 | if not isinstance(scenario, Scenario): 33 | scenario = make_benchmark_scenario(scenario) 34 | super().__init__(scenario, 35 | fully_obs=fully_obs, 36 | flat_actions=flat_actions, 37 | flat_obs=flat_obs) 38 | -------------------------------------------------------------------------------- /vagrant/router/bootstrap.rsc: -------------------------------------------------------------------------------- 1 | :put "Bootstrapping router" 2 | 3 | # find the correct eth3 interface 4 | # ------------------------------- 5 | #/import vagrant_provision_mac_addr.rsc 6 | :global vmNICMACs 7 | # vmNICMACs array is zero-based 8 | :local eth3MACaddr [:pick $vmNICMACs 2] 9 | :local eth3name [/interface ethernet get [find mac-address="$eth3MACaddr"] name] 10 | 11 | # initialize the networks 12 | # ------------------------------- 13 | /ip address add address=192.168.0.10/24 interface=$eth3name 14 | /ip address add address=192.168.1.10/24 interface=$eth3name 15 | /ip address add address=192.168.2.10/24 interface=$eth3name 16 | /ip address add address=192.168.3.10/24 interface=$eth3name 17 | /ip address add address=192.168.4.10/24 interface=$eth3name 18 | /ip address add address=192.168.5.10/24 interface=$eth3name 19 | /ip address add address=192.168.6.10/24 interface=$eth3name 20 | /ip address add address=192.168.7.10/24 interface=$eth3name 21 | /ip address add address=192.168.8.10/24 interface=$eth3name 22 | /ip address add address=192.168.9.10/24 interface=$eth3name 23 | /ip address add address=192.168.10.10/24 interface=$eth3name 24 | 25 | :put "This is the new configuration:" 26 | /ip address print 27 | 28 | 29 | # initialize firewall 30 | # ------------------------------- 31 | 32 | # no ICMP from the router itself 33 | /ip firewall filter add chain=output action=drop protocol=icmp 34 | 35 | # allow established connections 36 | /ip firewall filter add chain=forward action=fasttrack-connection connection-state=established,related 37 | /ip firewall filter add chain=forward action=accept connection-state=established,related 38 | -------------------------------------------------------------------------------- /vagrant/target/linux-install-script/install-phpwiki-1.5.0.sh: -------------------------------------------------------------------------------- 1 | # working on metasploitable3-ub1404 2 | sudo DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::="--force-confdef" php5 php5-mysql 3 | 4 | # get phpwiki-1.5.0 5 | wget --no-check-certificate -O phpwiki-1.5.0.zip https://sourceforge.net/projects/phpwiki/files/PhpWiki%201.5%20%28current%29/phpwiki-1.5.0.zip/download -nv 6 | unzip -q phpwiki-1.5.0.zip 7 | 8 | # configure phpwiki 9 | cp phpwiki-1.5.0/config/config-dist.ini phpwiki-1.5.0/config/config.ini 10 | sed -i '/;ADMIN_USER/c\ADMIN_USER = admin' phpwiki-1.5.0/config/config.ini 11 | sed -i '/;ADMIN_PASSWD/c\ADMIN_PASSWD = admin1245' phpwiki-1.5.0/config/config.ini 12 | sed -i '/DATABASE_TYPE =/c\DATABASE_TYPE = SQL' phpwiki-1.5.0/config/config.ini 13 | sed -i '/DATABASE_DSN =/c\DATABASE_DSN = "mysql://phpwiki:wiki1234@unix(/run/mysql-default/mysqld.sock)/phpwiki"' phpwiki-1.5.0/config/config.ini 14 | 15 | # setup mysql 16 | sudo mysqladmin create phpwiki -S /run/mysql-default/mysqld.sock -u root --password='sploitme' 17 | sudo mysql -e "SET @@global.sql_mode='MYSQL40'" -S /run/mysql-default/mysqld.sock -u root --password='sploitme' 18 | sudo mysql -e "GRANT select, insert, update, delete, lock tables ON phpwiki.* TO phpwiki@localhost IDENTIFIED BY 'wiki1234';" -S /run/mysql-default/mysqld.sock -u root --password='sploitme' 19 | sudo mysql phpwiki -S /run/mysql-default/mysqld.sock -u root --password='sploitme' < phpwiki-1.5.0/schemas/mysql-initialize.sql 20 | 21 | # setup www-data 22 | sudo mkdir /var/www/html/phpwiki 23 | sudo cp -r phpwiki-1.5.0/* /var/www/html/phpwiki 24 | sudo chown -R www-data:www-data /var/www/html/phpwiki/ 25 | 26 | # cleanup 27 | rm phpwiki-1.5.0.zip 28 | sudo service apache2 restart 29 | -------------------------------------------------------------------------------- /src/nasimemu/pymetasploit3/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from optparse import OptionParser 4 | import msgpack 5 | 6 | __all__ = [ 7 | 'parseargs', 8 | 'convert', 9 | 'decode', 10 | 'encode' 11 | ] 12 | 13 | 14 | def parseargs(): 15 | p = OptionParser() 16 | p.add_option("-P", dest="password", help="Specify the password to access msfrpcd", metavar="opt") 17 | p.add_option("-S", dest="ssl", help="Disable SSL on the RPC socket", action="store_false", default=True) 18 | p.add_option("-U", dest="username", help="Specify the username to access msfrpcd", metavar="opt", default="msf") 19 | p.add_option("-a", dest="server", help="Connect to this IP address", metavar="host", default="127.0.0.1") 20 | p.add_option("-p", dest="port", help="Connect to the specified port instead of 55553", metavar="opt", default=55553) 21 | o, a = p.parse_args() 22 | if o.password is None: 23 | print('[-] Error: a password must be specified (-P)\n') 24 | p.print_help() 25 | exit(-1) 26 | return o 27 | 28 | def convert(data, encoding="utf-8"): 29 | """ 30 | Converts all bytestrings to utf8 31 | """ 32 | if isinstance(data, bytes): return data.decode(encoding=encoding) 33 | if isinstance(data, list): return list(map(lambda iter: convert(iter, encoding=encoding), data)) 34 | if isinstance(data, set): return set(map(lambda iter: convert(iter, encoding=encoding), data)) 35 | if isinstance(data, dict): return dict(map(lambda iter: convert(iter, encoding=encoding), data.items())) 36 | if isinstance(data, tuple): return map(lambda iter: convert(iter, encoding=encoding), data) 37 | return data 38 | 39 | def encode(data): 40 | return msgpack.packb(data) 41 | 42 | def decode(data): 43 | return msgpack.unpackb(data, strict_map_key=False) 44 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/benchmark/tiny.yaml: -------------------------------------------------------------------------------- 1 | # A tiny standard (one public network) network configuration 2 | # 3 | # 3 hosts 4 | # 3 subnets 5 | # 1 service 6 | # 1 process 7 | # 1 os 8 | # 1 exploit 9 | # 1 privilege escalation 10 | # 11 | # Optimal path: 12 | # (e_ssh, (1, 0)) -> subnet_scan -> (e_ssh, (3, 0)) -> (pe_tomcat, (3, 0)) 13 | # -> (e_ssh, (2, 0)) -> (pe_tomcat, (2, 0)) 14 | # Score = 200 - (6*1) = 195 15 | # 16 | subnets: [1, 1, 1] 17 | topology: [[ 1, 1, 0, 0], 18 | [ 1, 1, 1, 1], 19 | [ 0, 1, 1, 1], 20 | [ 0, 1, 1, 1]] 21 | sensitive_hosts: 22 | (2, 0): 100 23 | (3, 0): 100 24 | os: 25 | - linux 26 | services: 27 | - ssh 28 | processes: 29 | - tomcat 30 | exploits: 31 | e_ssh: 32 | service: ssh 33 | os: linux 34 | prob: 0.8 35 | cost: 1 36 | access: user 37 | privilege_escalation: 38 | pe_tomcat: 39 | process: tomcat 40 | os: linux 41 | prob: 1.0 42 | cost: 1 43 | access: root 44 | service_scan_cost: 1 45 | os_scan_cost: 1 46 | subnet_scan_cost: 1 47 | process_scan_cost: 1 48 | host_configurations: 49 | (1, 0): 50 | os: linux 51 | services: [ssh] 52 | processes: [tomcat] 53 | # which services to deny between individual hosts 54 | firewall: 55 | (3, 0): [ssh] 56 | (2, 0): 57 | os: linux 58 | services: [ssh] 59 | processes: [tomcat] 60 | firewall: 61 | (1, 0): [ssh] 62 | (3, 0): 63 | os: linux 64 | services: [ssh] 65 | processes: [tomcat] 66 | # two row for each connection between subnets as defined by topology 67 | # one for each direction of connection 68 | # list which services to allow 69 | firewall: 70 | (0, 1): [ssh] 71 | (1, 0): [] 72 | (1, 2): [] 73 | (2, 1): [ssh] 74 | (1, 3): [ssh] 75 | (3, 1): [ssh] 76 | (2, 3): [ssh] 77 | (3, 2): [ssh] 78 | step_limit: 1000 79 | -------------------------------------------------------------------------------- /scenarios/md_entry_dmz_one_subnet.v2.yaml: -------------------------------------------------------------------------------- 1 | # This is special scenario format, that leaves some variables to chance. Not compatible with the original nasim generator/loader. 2 | 3 | subnets: [1, 6-10] # ranges of hosts in subnets 4 | # address_space_bounds: (2, 8) # max number of subnets & hosts in a subnet 5 | address_space_bounds: (5, 10) # fix for the experiment - all scenarios have to have the same bounds 6 | topology: [[ 1, 1, 0], 7 | [ 1, 1, 1], 8 | [ 0, 1, 1]] 9 | subnet_labels: # useful only for visualization and debugging 10 | 1: DMZ 11 | 2: user 12 | sensitive_hosts: # probabilities of sensitive hosts in specific subnets 13 | 1: 0. # DMZ 14 | 2: 0. # user subnet 15 | os: 16 | - linux 17 | - windows 18 | services: 19 | - 21_linux_proftpd 20 | - 80_linux_drupal 21 | - 80_linux_phpwiki 22 | - 9200_windows_elasticsearch 23 | - 80_windows_wp_ninja 24 | - 3306_any_mysql 25 | sensitive_services: 26 | - 3306_any_mysql 27 | processes: 28 | - ~ 29 | exploits: 30 | e_proftpd: 31 | service: 21_linux_proftpd 32 | os: linux 33 | prob: 1.0 34 | cost: 1 35 | access: user 36 | e_drupal: 37 | service: 80_linux_drupal 38 | os: linux 39 | prob: 1.0 40 | cost: 1 41 | access: user 42 | e_phpwiki: 43 | service: 80_linux_phpwiki 44 | os: linux 45 | prob: 1.0 46 | cost: 1 47 | access: user 48 | e_elasticsearch: 49 | service: 9200_windows_elasticsearch 50 | os: windows 51 | prob: 1.0 52 | cost: 1 53 | access: root 54 | e_wp_ninja: 55 | service: 80_windows_wp_ninja 56 | os: windows 57 | prob: 1.0 58 | cost: 1 59 | access: user 60 | privilege_escalation: 61 | pe_kernel: 62 | process: ~ 63 | os: linux 64 | prob: 1.0 65 | cost: 1 66 | access: root 67 | service_scan_cost: 1 68 | os_scan_cost: 1 69 | subnet_scan_cost: 1 70 | process_scan_cost: 1 71 | host_configurations: _random 72 | firewall: _subnets 73 | -------------------------------------------------------------------------------- /scenarios/sm_entry_dmz_one_subnet.v2.yaml: -------------------------------------------------------------------------------- 1 | # This is special scenario format, that leaves some variables to chance. Not compatible with the original nasim generator/loader. 2 | 3 | subnets: [1, 2-5] # ranges of hosts in subnets 4 | # address_space_bounds: (2, 8) # max number of subnets & hosts in a subnet 5 | address_space_bounds: (5, 10) # fix for the experiment - all scenarios have to have the same bounds 6 | topology: [[ 1, 1, 0], 7 | [ 1, 1, 1], 8 | [ 0, 1, 1]] 9 | subnet_labels: # useful only for visualization and debugging 10 | 1: DMZ 11 | 2: user 12 | sensitive_hosts: # probabilities of sensitive hosts in specific subnets 13 | 1: 0. # DMZ 14 | 2: 0. # user subnet 15 | os: 16 | - linux 17 | - windows 18 | services: 19 | - 21_linux_proftpd 20 | - 80_linux_drupal 21 | - 80_linux_phpwiki 22 | - 9200_windows_elasticsearch 23 | - 80_windows_wp_ninja 24 | - 3306_any_mysql 25 | sensitive_services: 26 | - 3306_any_mysql 27 | processes: 28 | - ~ 29 | exploits: 30 | e_proftpd: 31 | service: 21_linux_proftpd 32 | os: linux 33 | prob: 1.0 34 | cost: 1 35 | access: user 36 | e_drupal: 37 | service: 80_linux_drupal 38 | os: linux 39 | prob: 1.0 40 | cost: 1 41 | access: user 42 | e_phpwiki: 43 | service: 80_linux_phpwiki 44 | os: linux 45 | prob: 1.0 46 | cost: 1 47 | access: user 48 | e_elasticsearch: 49 | service: 9200_windows_elasticsearch 50 | os: windows 51 | prob: 1.0 52 | cost: 1 53 | access: root 54 | e_wp_ninja: 55 | service: 80_windows_wp_ninja 56 | os: windows 57 | prob: 1.0 58 | cost: 1 59 | access: user 60 | privilege_escalation: 61 | pe_kernel: 62 | process: ~ 63 | os: linux 64 | prob: 1.0 65 | cost: 1 66 | access: root 67 | service_scan_cost: 1 68 | os_scan_cost: 1 69 | subnet_scan_cost: 1 70 | process_scan_cost: 1 71 | host_configurations: _random 72 | firewall: _subnets 73 | -------------------------------------------------------------------------------- /src/nasimemu/pymetasploit3/scripts/pymsfconsole.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from code import InteractiveConsole 4 | from atexit import register 5 | from sys import stdout 6 | from os import path 7 | import readline 8 | 9 | from nasimemu.pymetasploit3.msfrpc import MsfRpcClient, MsfRpcError 10 | from nasimemu.pymetasploit3.msfconsole import MsfRpcConsole 11 | from nasimemu.pymetasploit3.utils import parseargs 12 | 13 | 14 | class MsfConsole(InteractiveConsole): 15 | 16 | def __init__(self, password, **kwargs): 17 | self.fl = True 18 | self.client = MsfRpcConsole(MsfRpcClient(password, **kwargs), cb=self.callback) 19 | InteractiveConsole.__init__(self, {'rpc': self.client}) 20 | self.init_history(path.expanduser('~/.msfconsole_history')) 21 | 22 | def raw_input(self, prompt): 23 | line = InteractiveConsole.raw_input(self, prompt=self.client.prompt) 24 | return "rpc.execute('%s')" % line.replace("'", r"\'") 25 | 26 | def init_history(self, histfile): 27 | readline.parse_and_bind('tab: complete') 28 | if hasattr(readline, "read_history_file"): 29 | try: 30 | readline.read_history_file(histfile) 31 | except IOError: 32 | pass 33 | register(self.save_history, histfile) 34 | 35 | def save_history(self, histfile): 36 | readline.write_history_file(histfile) 37 | del self.client 38 | print('bye!') 39 | 40 | def callback(self, d): 41 | stdout.write('\n%s' % d['data']) 42 | if not self.fl: 43 | stdout.write('\n%s' % d['prompt']) 44 | stdout.flush() 45 | else: 46 | self.fl = False 47 | 48 | 49 | if __name__ == '__main__': 50 | o = parseargs() 51 | try: 52 | m = MsfConsole(o.__dict__.pop('password'), **o.__dict__) 53 | m.interact('') 54 | except MsfRpcError as m: 55 | print(str(m)) 56 | exit(-1) 57 | exit(0) 58 | -------------------------------------------------------------------------------- /scenarios/sm_entry_dmz_two_subnets.v2.yaml: -------------------------------------------------------------------------------- 1 | # This is special scenario format, that leaves some variables to chance. Not compatible with the original nasim generator/loader. 2 | 3 | subnets: [1, 2-5, 2-5] # ranges of hosts in subnets 4 | # address_space_bounds: (3, 5) # max number of subnets & hosts in a subnet 5 | address_space_bounds: (5, 10) # fix for the experiment - all scenarios have to have the same bounds 6 | topology: [[ 1, 1, 0, 0], 7 | [ 1, 1, 1, 1], 8 | [ 0, 1, 1, 0], 9 | [ 0, 1, 0, 1]] 10 | subnet_labels: 11 | 1: DMZ 12 | 2: user 13 | 3: service 14 | sensitive_hosts: # probabilities of sensitive hosts in specific subnets 15 | 1: 0. # DMZ 16 | 2: 0. # user subnet 17 | 3: 0.7 # service subnet 18 | os: 19 | - linux 20 | - windows 21 | services: 22 | - 21_linux_proftpd 23 | - 80_linux_drupal 24 | - 80_linux_phpwiki 25 | - 9200_windows_elasticsearch 26 | - 80_windows_wp_ninja 27 | - 3306_any_mysql 28 | sensitive_services: 29 | - 3306_any_mysql 30 | processes: 31 | - ~ 32 | exploits: 33 | e_proftpd: 34 | service: 21_linux_proftpd 35 | os: linux 36 | prob: 1.0 37 | cost: 1 38 | access: user 39 | e_drupal: 40 | service: 80_linux_drupal 41 | os: linux 42 | prob: 1.0 43 | cost: 1 44 | access: user 45 | e_phpwiki: 46 | service: 80_linux_phpwiki 47 | os: linux 48 | prob: 1.0 49 | cost: 1 50 | access: user 51 | e_elasticsearch: 52 | service: 9200_windows_elasticsearch 53 | os: windows 54 | prob: 1.0 55 | cost: 1 56 | access: root 57 | e_wp_ninja: 58 | service: 80_windows_wp_ninja 59 | os: windows 60 | prob: 1.0 61 | cost: 1 62 | access: user 63 | privilege_escalation: 64 | pe_kernel: 65 | process: ~ 66 | os: linux 67 | prob: 1.0 68 | cost: 1 69 | access: root 70 | service_scan_cost: 1 71 | os_scan_cost: 1 72 | subnet_scan_cost: 1 73 | process_scan_cost: 1 74 | host_configurations: _random 75 | firewall: _subnets 76 | -------------------------------------------------------------------------------- /scenarios/md_entry_dmz_two_subnets.v2.yaml: -------------------------------------------------------------------------------- 1 | # This is special scenario format, that leaves some variables to chance. Not compatible with the original nasim generator/loader. 2 | 3 | subnets: [1, 6-10, 6-10] # ranges of hosts in subnets 4 | # address_space_bounds: (3, 5) # max number of subnets & hosts in a subnet 5 | address_space_bounds: (5, 10) # fix for the experiment - all scenarios have to have the same bounds 6 | topology: [[ 1, 1, 0, 0], 7 | [ 1, 1, 1, 1], 8 | [ 0, 1, 1, 0], 9 | [ 0, 1, 0, 1]] 10 | subnet_labels: 11 | 1: DMZ 12 | 2: user 13 | 3: service 14 | sensitive_hosts: # probabilities of sensitive hosts in specific subnets 15 | 1: 0. # DMZ 16 | 2: 0. # user subnet 17 | 3: 0.7 # service subnet 18 | os: 19 | - linux 20 | - windows 21 | services: 22 | - 21_linux_proftpd 23 | - 80_linux_drupal 24 | - 80_linux_phpwiki 25 | - 9200_windows_elasticsearch 26 | - 80_windows_wp_ninja 27 | - 3306_any_mysql 28 | sensitive_services: 29 | - 3306_any_mysql 30 | processes: 31 | - ~ 32 | exploits: 33 | e_proftpd: 34 | service: 21_linux_proftpd 35 | os: linux 36 | prob: 1.0 37 | cost: 1 38 | access: user 39 | e_drupal: 40 | service: 80_linux_drupal 41 | os: linux 42 | prob: 1.0 43 | cost: 1 44 | access: user 45 | e_phpwiki: 46 | service: 80_linux_phpwiki 47 | os: linux 48 | prob: 1.0 49 | cost: 1 50 | access: user 51 | e_elasticsearch: 52 | service: 9200_windows_elasticsearch 53 | os: windows 54 | prob: 1.0 55 | cost: 1 56 | access: root 57 | e_wp_ninja: 58 | service: 80_windows_wp_ninja 59 | os: windows 60 | prob: 1.0 61 | cost: 1 62 | access: user 63 | privilege_escalation: 64 | pe_kernel: 65 | process: ~ 66 | os: linux 67 | prob: 1.0 68 | cost: 1 69 | access: root 70 | service_scan_cost: 1 71 | os_scan_cost: 1 72 | subnet_scan_cost: 1 73 | process_scan_cost: 1 74 | host_configurations: _random 75 | firewall: _subnets 76 | -------------------------------------------------------------------------------- /scenarios/md_entry_user_three_subnets.v2.yaml: -------------------------------------------------------------------------------- 1 | # This is special scenario format, that leaves some variables to chance. Not compatible with the original nasim generator/loader. 2 | subnets: [1, 6-10, 6-10, 4-6] # ranges of hosts in subnets 3 | # address_space_bounds: (4, 5) # max number of subnets & hosts in a subnet 4 | address_space_bounds: (5, 10) # max number of subnets & hosts in a subnet 5 | topology: [[ 1, 0, 1, 0, 0], 6 | [ 0, 1, 1, 1, 0], 7 | [ 1, 1, 1, 0, 1], 8 | [ 0, 1, 0, 1, 0], 9 | [ 0, 0, 1, 0, 1]] 10 | subnet_labels: 11 | 1: DMZ 12 | 2: user 13 | 3: service 14 | 4: db 15 | sensitive_hosts: # probabilities of sensitive hosts in specific subnets 16 | 1: 0. # DMZ 17 | 2: 0. # user subnet 18 | 3: 0.7 # service subnet 19 | 4: 1.0 # db subnet 20 | os: 21 | - linux 22 | - windows 23 | services: 24 | - 21_linux_proftpd 25 | - 80_linux_drupal 26 | - 80_linux_phpwiki 27 | - 9200_windows_elasticsearch 28 | - 80_windows_wp_ninja 29 | - 3306_any_mysql 30 | sensitive_services: 31 | - 3306_any_mysql 32 | processes: 33 | - ~ 34 | exploits: 35 | e_proftpd: 36 | service: 21_linux_proftpd 37 | os: linux 38 | prob: 1.0 39 | cost: 1 40 | access: user 41 | e_drupal: 42 | service: 80_linux_drupal 43 | os: linux 44 | prob: 1.0 45 | cost: 1 46 | access: user 47 | e_phpwiki: 48 | service: 80_linux_phpwiki 49 | os: linux 50 | prob: 1.0 51 | cost: 1 52 | access: user 53 | e_elasticsearch: 54 | service: 9200_windows_elasticsearch 55 | os: windows 56 | prob: 1.0 57 | cost: 1 58 | access: root 59 | e_wp_ninja: 60 | service: 80_windows_wp_ninja 61 | os: windows 62 | prob: 1.0 63 | cost: 1 64 | access: user 65 | privilege_escalation: 66 | pe_kernel: 67 | process: ~ 68 | os: linux 69 | prob: 1.0 70 | cost: 1 71 | access: root 72 | service_scan_cost: 1 73 | os_scan_cost: 1 74 | subnet_scan_cost: 1 75 | process_scan_cost: 1 76 | host_configurations: _random 77 | firewall: _subnets 78 | -------------------------------------------------------------------------------- /scenarios/sm_entry_user_three_subnets.v2.yaml: -------------------------------------------------------------------------------- 1 | # This is special scenario format, that leaves some variables to chance. Not compatible with the original nasim generator/loader. 2 | subnets: [1, 2-5, 2-5, 1-3] # ranges of hosts in subnets 3 | # address_space_bounds: (4, 5) # max number of subnets & hosts in a subnet 4 | address_space_bounds: (5, 10) # max number of subnets & hosts in a subnet 5 | topology: [[ 1, 0, 1, 0, 0], 6 | [ 0, 1, 1, 1, 0], 7 | [ 1, 1, 1, 0, 1], 8 | [ 0, 1, 0, 1, 0], 9 | [ 0, 0, 1, 0, 1]] 10 | subnet_labels: 11 | 1: DMZ 12 | 2: user 13 | 3: service 14 | 4: db 15 | sensitive_hosts: # probabilities of sensitive hosts in specific subnets 16 | 1: 0. # DMZ 17 | 2: 0. # user subnet 18 | 3: 0.7 # service subnet 19 | 4: 1.0 # db subnet 20 | os: 21 | - linux 22 | - windows 23 | services: 24 | - 21_linux_proftpd 25 | - 80_linux_drupal 26 | - 80_linux_phpwiki 27 | - 9200_windows_elasticsearch 28 | - 80_windows_wp_ninja 29 | - 3306_any_mysql 30 | sensitive_services: 31 | - 3306_any_mysql 32 | processes: 33 | - ~ 34 | exploits: 35 | e_proftpd: 36 | service: 21_linux_proftpd 37 | os: linux 38 | prob: 1.0 39 | cost: 1 40 | access: user 41 | e_drupal: 42 | service: 80_linux_drupal 43 | os: linux 44 | prob: 1.0 45 | cost: 1 46 | access: user 47 | e_phpwiki: 48 | service: 80_linux_phpwiki 49 | os: linux 50 | prob: 1.0 51 | cost: 1 52 | access: user 53 | e_elasticsearch: 54 | service: 9200_windows_elasticsearch 55 | os: windows 56 | prob: 1.0 57 | cost: 1 58 | access: root 59 | e_wp_ninja: 60 | service: 80_windows_wp_ninja 61 | os: windows 62 | prob: 1.0 63 | cost: 1 64 | access: user 65 | privilege_escalation: 66 | pe_kernel: 67 | process: ~ 68 | os: linux 69 | prob: 1.0 70 | cost: 1 71 | access: root 72 | service_scan_cost: 1 73 | os_scan_cost: 1 74 | subnet_scan_cost: 1 75 | process_scan_cost: 1 76 | host_configurations: _random 77 | firewall: _subnets 78 | -------------------------------------------------------------------------------- /scenarios/sm_entry_dmz_three_subnets.v2.yaml: -------------------------------------------------------------------------------- 1 | # This is special scenario format, that leaves some variables to chance. Not compatible with the original nasim generator/loader. 2 | 3 | subnets: [1, 2-5, 2-5, 1-3] # ranges of hosts in subnets 4 | # address_space_bounds: (4, 5) # max number of subnets & hosts in a subnet 5 | address_space_bounds: (5, 10) # max number of subnets & hosts in a subnet 6 | topology: [[ 1, 1, 0, 0, 0], 7 | [ 1, 1, 1, 1, 0], 8 | [ 0, 1, 1, 0, 1], 9 | [ 0, 1, 0, 1, 0], 10 | [ 0, 0, 1, 0, 1]] 11 | subnet_labels: 12 | 1: DMZ 13 | 2: user 14 | 3: service 15 | 4: db 16 | sensitive_hosts: # probabilities of sensitive hosts in specific subnets 17 | 1: 0. # DMZ 18 | 2: 0. # user subnet 19 | 3: 0.7 # service subnet 20 | 4: 1.0 # db subnet 21 | os: 22 | - linux 23 | - windows 24 | services: 25 | - 21_linux_proftpd 26 | - 80_linux_drupal 27 | - 80_linux_phpwiki 28 | - 9200_windows_elasticsearch 29 | - 80_windows_wp_ninja 30 | - 3306_any_mysql 31 | sensitive_services: 32 | - 3306_any_mysql 33 | processes: 34 | - ~ 35 | exploits: 36 | e_proftpd: 37 | service: 21_linux_proftpd 38 | os: linux 39 | prob: 1.0 40 | cost: 1 41 | access: user 42 | e_drupal: 43 | service: 80_linux_drupal 44 | os: linux 45 | prob: 1.0 46 | cost: 1 47 | access: user 48 | e_phpwiki: 49 | service: 80_linux_phpwiki 50 | os: linux 51 | prob: 1.0 52 | cost: 1 53 | access: user 54 | e_elasticsearch: 55 | service: 9200_windows_elasticsearch 56 | os: windows 57 | prob: 1.0 58 | cost: 1 59 | access: root 60 | e_wp_ninja: 61 | service: 80_windows_wp_ninja 62 | os: windows 63 | prob: 1.0 64 | cost: 1 65 | access: user 66 | privilege_escalation: 67 | pe_kernel: 68 | process: ~ 69 | os: linux 70 | prob: 1.0 71 | cost: 1 72 | access: root 73 | service_scan_cost: 1 74 | os_scan_cost: 1 75 | subnet_scan_cost: 1 76 | process_scan_cost: 1 77 | host_configurations: _random 78 | firewall: _subnets 79 | -------------------------------------------------------------------------------- /scenarios/md_entry_dmz_three_subnets.v2.yaml: -------------------------------------------------------------------------------- 1 | # This is special scenario format, that leaves some variables to chance. Not compatible with the original nasim generator/loader. 2 | 3 | subnets: [1, 6-10, 6-10, 4-6] # ranges of hosts in subnets 4 | # address_space_bounds: (4, 5) # max number of subnets & hosts in a subnet 5 | address_space_bounds: (5, 10) # max number of subnets & hosts in a subnet 6 | topology: [[ 1, 1, 0, 0, 0], 7 | [ 1, 1, 1, 1, 0], 8 | [ 0, 1, 1, 0, 1], 9 | [ 0, 1, 0, 1, 0], 10 | [ 0, 0, 1, 0, 1]] 11 | subnet_labels: 12 | 1: DMZ 13 | 2: user 14 | 3: service 15 | 4: db 16 | sensitive_hosts: # probabilities of sensitive hosts in specific subnets 17 | 1: 0. # DMZ 18 | 2: 0. # user subnet 19 | 3: 0.7 # service subnet 20 | 4: 1.0 # db subnet 21 | os: 22 | - linux 23 | - windows 24 | services: 25 | - 21_linux_proftpd 26 | - 80_linux_drupal 27 | - 80_linux_phpwiki 28 | - 9200_windows_elasticsearch 29 | - 80_windows_wp_ninja 30 | - 3306_any_mysql 31 | sensitive_services: 32 | - 3306_any_mysql 33 | processes: 34 | - ~ 35 | exploits: 36 | e_proftpd: 37 | service: 21_linux_proftpd 38 | os: linux 39 | prob: 1.0 40 | cost: 1 41 | access: user 42 | e_drupal: 43 | service: 80_linux_drupal 44 | os: linux 45 | prob: 1.0 46 | cost: 1 47 | access: user 48 | e_phpwiki: 49 | service: 80_linux_phpwiki 50 | os: linux 51 | prob: 1.0 52 | cost: 1 53 | access: user 54 | e_elasticsearch: 55 | service: 9200_windows_elasticsearch 56 | os: windows 57 | prob: 1.0 58 | cost: 1 59 | access: root 60 | e_wp_ninja: 61 | service: 80_windows_wp_ninja 62 | os: windows 63 | prob: 1.0 64 | cost: 1 65 | access: user 66 | privilege_escalation: 67 | pe_kernel: 68 | process: ~ 69 | os: linux 70 | prob: 1.0 71 | cost: 1 72 | access: root 73 | service_scan_cost: 1 74 | os_scan_cost: 1 75 | subnet_scan_cost: 1 76 | process_scan_cost: 1 77 | host_configurations: _random 78 | firewall: _subnets 79 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/benchmark/__init__.py: -------------------------------------------------------------------------------- 1 | import os.path as osp 2 | 3 | from .generated import AVAIL_GEN_BENCHMARKS 4 | 5 | BENCHMARK_DIR = osp.dirname(osp.abspath(__file__)) 6 | 7 | AVAIL_STATIC_BENCHMARKS = { 8 | "tiny": { 9 | "file": osp.join(BENCHMARK_DIR, "tiny.yaml"), 10 | "name": "tiny", 11 | "step_limit": 1000, 12 | "max_score": 195 13 | }, 14 | "tiny-hard": { 15 | "file": osp.join(BENCHMARK_DIR, "tiny-hard.yaml"), 16 | "name": "tiny-hard", 17 | "step_limit": 1000, 18 | "max_score": 192 19 | }, 20 | "tiny-small": { 21 | "file": osp.join(BENCHMARK_DIR, "tiny-small.yaml"), 22 | "name": "tiny-small", 23 | "step_limit": 1000, 24 | "max_score": 189 25 | }, 26 | "small": { 27 | "file": osp.join(BENCHMARK_DIR, "small.yaml"), 28 | "name": "small", 29 | "step_limit": 1000, 30 | "max_score": 186 31 | }, 32 | "small-honeypot": { 33 | "file": osp.join(BENCHMARK_DIR, "small-honeypot.yaml"), 34 | "name": "small-honeypot", 35 | "step_limit": 1000, 36 | "max_score": 186 37 | }, 38 | "small-linear": { 39 | "file": osp.join(BENCHMARK_DIR, "small-linear.yaml"), 40 | "name": "small-linear", 41 | "step_limit": 1000, 42 | "max_score": 187 43 | }, 44 | "medium": { 45 | "file": osp.join(BENCHMARK_DIR, "medium.yaml"), 46 | "name": "medium", 47 | "step_limit": 2000, 48 | "max_score": 190 49 | }, 50 | "medium-single-site": { 51 | "file": osp.join(BENCHMARK_DIR, "medium-single-site.yaml"), 52 | "name": "medium-single-site", 53 | "step_limit": 2000, 54 | "max_score": 195 55 | }, 56 | "medium-multi-site": { 57 | "file": osp.join(BENCHMARK_DIR, "medium-multi-site.yaml"), 58 | "name": "medium-multi-site", 59 | "step_limit": 2000, 60 | "max_score": 190 61 | }, 62 | } 63 | 64 | AVAIL_BENCHMARKS = list(AVAIL_STATIC_BENCHMARKS.keys()) \ 65 | + list(AVAIL_GEN_BENCHMARKS.keys()) 66 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/benchmark/tiny-hard.yaml: -------------------------------------------------------------------------------- 1 | # A harder version of the tiny standard (one public network) network configuration 2 | # 3 | # 3 subnets 4 | # 3 hosts 5 | # 2 OS 6 | # 3 services 7 | # 2 processes 8 | # 3 exploits 9 | # 2 priv esc actions 10 | # 11 | # Optimal path: 12 | # (e_http, (1, 0)) -> subnet scan -> (e_ssh, (2, 0)) -> (pe_tomcat, (2, 0)) -> (e_ftp, (3, 0)) 13 | # Score = 200 - (2 + 1 + 3 + 1 + 1) = 192 14 | # 15 | subnets: [1, 1, 1] 16 | topology: [[ 1, 1, 0, 0], 17 | [ 1, 1, 1, 1], 18 | [ 0, 1, 1, 1], 19 | [ 0, 1, 1, 1]] 20 | sensitive_hosts: 21 | (2, 0): 100 22 | (3, 0): 100 23 | os: 24 | - linux 25 | - windows 26 | services: 27 | - ssh 28 | - ftp 29 | - http 30 | processes: 31 | - tomcat 32 | - daclsvc 33 | exploits: 34 | e_ssh: 35 | service: ssh 36 | os: linux 37 | prob: 0.9 38 | cost: 3 39 | access: user 40 | e_ftp: 41 | service: ftp 42 | os: windows 43 | prob: 0.6 44 | cost: 1 45 | access: root 46 | e_http: 47 | service: http 48 | os: None 49 | prob: 0.9 50 | cost: 2 51 | access: user 52 | privilege_escalation: 53 | pe_tomcat: 54 | process: tomcat 55 | os: linux 56 | prob: 1.0 57 | cost: 1 58 | access: root 59 | pe_daclsvc: 60 | process: daclsvc 61 | os: windows 62 | prob: 1.0 63 | cost: 1 64 | access: root 65 | service_scan_cost: 1 66 | os_scan_cost: 1 67 | subnet_scan_cost: 1 68 | process_scan_cost: 1 69 | host_configurations: 70 | (1, 0): 71 | os: linux 72 | services: [http] 73 | processes: [] 74 | (2, 0): 75 | os: linux 76 | services: [ssh, ftp] 77 | processes: [tomcat] 78 | (3, 0): 79 | os: windows 80 | services: [ftp] 81 | processes: [daclsvc] 82 | # two row for each connection between subnets as defined by topology 83 | # one for each direction of connection 84 | # list which services to allow 85 | firewall: 86 | (0, 1): [http] 87 | (1, 0): [] 88 | (1, 2): [ssh] 89 | (2, 1): [ssh] 90 | (1, 3): [] 91 | (3, 1): [ssh] 92 | (2, 3): [ftp, ssh] 93 | (3, 2): [ftp, ssh] 94 | step_limit: 1000 95 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scripts/describe_scenarios.py: -------------------------------------------------------------------------------- 1 | """This script will output description statistics of all benchmark 2 | scenarios. 3 | 4 | It will output a table to stdout (and optionally to a .csv file) which 5 | contains the following headers: 6 | 7 | - Name : the scenarios name 8 | - Type : static or generated 9 | - Subnets : the number of subnets 10 | - Hosts : the number of hosts 11 | - OS : the number of OS 12 | - Services : the number of services 13 | - Processes : the number of processes 14 | - Exploits : the number of exploits 15 | - PrivEsc : the number of priviledge escalation actions 16 | - Actions : the total number of actions available to agent 17 | - States : the total number of states 18 | - Step limit : the step limit for the scenario 19 | 20 | Usage 21 | ----- 22 | 23 | $ python describe_scenarios.py [-o --output filename.csv] 24 | 25 | """ 26 | import prettytable 27 | 28 | from nasimemu.nasim.scenarios import make_benchmark_scenario 29 | from nasimemu.nasim.scenarios.benchmark import AVAIL_BENCHMARKS 30 | 31 | 32 | def describe_scenarios(output=None): 33 | rows = [] 34 | headers = None 35 | for name in AVAIL_BENCHMARKS: 36 | scenario = make_benchmark_scenario(name, seed=0) 37 | des = scenario.get_description() 38 | if headers is None: 39 | headers = list(des.keys()) 40 | 41 | if des["States"] > 1e8: 42 | des["States"] = f"{des['States']:.2E}" 43 | 44 | rows.append([str(des[h]) for h in headers]) 45 | 46 | table = prettytable.PrettyTable(headers) 47 | for row in rows: 48 | table.add_row(row) 49 | 50 | print(table) 51 | 52 | if output is not None: 53 | print(f"\nSaving to {output}") 54 | with open(output, "w") as fout: 55 | fout.write(",".join(headers) + "\n") 56 | for row in rows: 57 | fout.write(",".join(row) + "\n") 58 | 59 | 60 | 61 | if __name__ == "__main__": 62 | import argparse 63 | parser = argparse.ArgumentParser() 64 | parser.add_argument("-o", "--output", type=str, default=None, 65 | help="File name to output as CSV too") 66 | args = parser.parse_args() 67 | 68 | describe_scenarios(args.output) 69 | -------------------------------------------------------------------------------- /scenarios/corp.v2.yaml: -------------------------------------------------------------------------------- 1 | subnets: [1, 1-4, 1-4, 1-6, 2-9, 1-4, 1-4, 1-4] # ranges of hosts in subnets 2 | # address_space_bounds: (2, 8) # max number of subnets & hosts in a subnet 3 | address_space_bounds: (10, 10) # fix for the experiment - all scenarios have to have the same bounds 4 | topology: [[ 1, 1, 0, 0, 0, 0, 0, 0, 0], 5 | [ 1, 1, 1, 0, 0, 0, 0, 0, 0], 6 | [ 0, 1, 1, 0, 0, 0, 0, 0, 1], 7 | [ 0, 0, 0, 1, 1, 1, 1, 1, 1], 8 | [ 0, 0, 0, 1, 1, 0, 0, 0, 0], 9 | [ 0, 0, 0, 1, 0, 1, 0, 0, 0], 10 | [ 0, 0, 0, 1, 0, 0, 1, 0, 0], 11 | [ 0, 0, 0, 1, 0, 0, 0, 1, 1], 12 | [ 0, 0, 1, 1, 0, 0, 0, 1, 1]] 13 | subnet_labels: # useful only for visualization and debugging 14 | 1: private_wifi 15 | 2: public_servers 16 | 3: intranet_servers 17 | 4: hr 18 | 5: employees 19 | 6: management 20 | 7: db 21 | 8: it_maintenance 22 | sensitive_hosts: # probabilities of sensitive hosts in specific subnets 23 | 1: 0. 24 | 2: 0. 25 | 3: 0. 26 | 4: 0.3 27 | 5: 0.3 28 | 6: 0.5 29 | 7: 0.3 30 | 8: 0. 31 | os: 32 | - linux 33 | - windows 34 | services: 35 | - 21_linux_proftpd 36 | - 80_linux_drupal 37 | - 80_linux_phpwiki 38 | - 9200_windows_elasticsearch 39 | - 80_windows_wp_ninja 40 | - 3306_any_mysql 41 | sensitive_services: 42 | - 3306_any_mysql 43 | processes: 44 | - ~ 45 | exploits: 46 | e_proftpd: 47 | service: 21_linux_proftpd 48 | os: linux 49 | prob: 0.8 50 | cost: 1 51 | access: user 52 | e_drupal: 53 | service: 80_linux_drupal 54 | os: linux 55 | prob: 0.8 56 | cost: 1 57 | access: user 58 | e_phpwiki: 59 | service: 80_linux_phpwiki 60 | os: linux 61 | prob: 0.8 62 | cost: 1 63 | access: user 64 | e_elasticsearch: 65 | service: 9200_windows_elasticsearch 66 | os: windows 67 | prob: 0.8 68 | cost: 1 69 | access: root 70 | e_wp_ninja: 71 | service: 80_windows_wp_ninja 72 | os: windows 73 | prob: 0.8 74 | cost: 1 75 | access: user 76 | privilege_escalation: 77 | pe_kernel: 78 | process: ~ 79 | os: linux 80 | prob: 0.8 81 | cost: 1 82 | access: root 83 | service_scan_cost: 1 84 | os_scan_cost: 1 85 | subnet_scan_cost: 1 86 | process_scan_cost: 1 87 | host_configurations: _random 88 | firewall: _subnets 89 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import yaml 3 | import os.path as osp 4 | 5 | 6 | SCENARIO_DIR = osp.dirname(osp.abspath(__file__)) 7 | 8 | # default subnet address for internet 9 | INTERNET = 0 10 | 11 | # Constants 12 | NUM_ACCESS_LEVELS = 2 13 | NO_ACCESS = 0 14 | USER_ACCESS = 1 15 | ROOT_ACCESS = 2 16 | DEFAULT_HOST_VALUE = 0 17 | 18 | # scenario property keys 19 | SUBNETS = "subnets" 20 | TOPOLOGY = "topology" 21 | SENSITIVE_HOSTS = "sensitive_hosts" 22 | SERVICES = "services" 23 | OS = "os" 24 | PROCESSES = "processes" 25 | EXPLOITS = "exploits" 26 | PRIVESCS = "privilege_escalation" 27 | SERVICE_SCAN_COST = "service_scan_cost" 28 | OS_SCAN_COST = "os_scan_cost" 29 | SUBNET_SCAN_COST = "subnet_scan_cost" 30 | PROCESS_SCAN_COST = "process_scan_cost" 31 | HOST_CONFIGS = "host_configurations" 32 | FIREWALL = "firewall" 33 | HOSTS = "host" 34 | STEP_LIMIT = "step_limit" 35 | ACCESS_LEVELS = "access_levels" 36 | 37 | # scenario exploit keys 38 | EXPLOIT_SERVICE = "service" 39 | EXPLOIT_OS = "os" 40 | EXPLOIT_PROB = "prob" 41 | EXPLOIT_COST = "cost" 42 | EXPLOIT_ACCESS = "access" 43 | 44 | # scenario privilege escalation keys 45 | PRIVESC_PROCESS = "process" 46 | PRIVESC_OS = "os" 47 | PRIVESC_PROB = "prob" 48 | PRIVESC_COST = "cost" 49 | PRIVESC_ACCESS = "access" 50 | 51 | # host configuration keys 52 | HOST_SERVICES = "services" 53 | HOST_PROCESSES = "processes" 54 | HOST_OS = "os" 55 | HOST_FIREWALL = "firewall" 56 | HOST_VALUE = "value" 57 | 58 | 59 | def load_yaml(file_path): 60 | """Load yaml file located at file path. 61 | 62 | Parameters 63 | ---------- 64 | file_path : str 65 | path to yaml file 66 | 67 | Returns 68 | ------- 69 | dict 70 | contents of yaml file 71 | 72 | Raises 73 | ------ 74 | Exception 75 | if theres an issue loading file. """ 76 | with open(file_path) as fin: 77 | content = yaml.load(fin, Loader=yaml.FullLoader) 78 | return content 79 | 80 | 81 | def get_file_name(file_path): 82 | """Extracts the file or dir name from file path 83 | 84 | Parameters 85 | ---------- 86 | file_path : str 87 | file path 88 | 89 | Returns 90 | ------- 91 | str 92 | file name with any path and extensions removed 93 | """ 94 | full_file_name = file_path.split(os.sep)[-1] 95 | file_name = full_file_name.split(".")[0] 96 | return file_name 97 | -------------------------------------------------------------------------------- /scenarios/uni.v2.yaml: -------------------------------------------------------------------------------- 1 | subnets: [1, 1-4, 1-6, 1-6, 2-9, 3-10, 1-4, 2-6, 1-4] # ranges of hosts in subnets 2 | # address_space_bounds: (2, 8) # max number of subnets & hosts in a subnet 3 | address_space_bounds: (10, 10) # fix for the experiment - all scenarios have to have the same bounds 4 | topology: [[ 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], 5 | [ 1, 1, 1, 0, 0, 0, 0, 0, 0, 0], 6 | [ 0, 1, 1, 1, 1, 1, 0, 0, 0, 0], 7 | [ 0, 0, 1, 1, 0, 0, 0, 1, 0, 0], 8 | [ 0, 0, 1, 0, 1, 0, 1, 1, 1, 1], 9 | [ 0, 0, 1, 0, 0, 1, 1, 0, 1, 0], 10 | [ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0], 11 | [ 0, 0, 0, 1, 1, 0, 0, 1, 0, 0], 12 | [ 0, 0, 0, 0, 1, 1, 0, 0, 1, 0], 13 | [ 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]] 14 | subnet_labels: # useful only for visualization and debugging 15 | 1: private_wifi 16 | 2: public_servers 17 | 3: study_dept 18 | 4: it_maintenance 19 | 5: employees 20 | 6: classrooms 21 | 7: db 22 | 8: hpc 23 | 9: backup 24 | sensitive_hosts: # probabilities of sensitive hosts in specific subnets 25 | 1: 0. 26 | 2: 0. 27 | 3: 0. 28 | 4: 0. 29 | 5: 0. 30 | 6: 0. 31 | 7: 0.5 32 | 8: 0.2 33 | 9: 0.9 34 | os: 35 | - linux 36 | - windows 37 | services: 38 | - 21_linux_proftpd 39 | - 80_linux_drupal 40 | - 80_linux_phpwiki 41 | - 9200_windows_elasticsearch 42 | - 80_windows_wp_ninja 43 | - 3306_any_mysql 44 | sensitive_services: 45 | - 3306_any_mysql 46 | processes: 47 | - ~ 48 | exploits: 49 | e_proftpd: 50 | service: 21_linux_proftpd 51 | os: linux 52 | prob: 0.8 53 | cost: 1 54 | access: user 55 | e_drupal: 56 | service: 80_linux_drupal 57 | os: linux 58 | prob: 0.8 59 | cost: 1 60 | access: user 61 | e_phpwiki: 62 | service: 80_linux_phpwiki 63 | os: linux 64 | prob: 0.8 65 | cost: 1 66 | access: user 67 | e_elasticsearch: 68 | service: 9200_windows_elasticsearch 69 | os: windows 70 | prob: 0.8 71 | cost: 1 72 | access: root 73 | e_wp_ninja: 74 | service: 80_windows_wp_ninja 75 | os: windows 76 | prob: 0.8 77 | cost: 1 78 | access: user 79 | privilege_escalation: 80 | pe_kernel: 81 | process: ~ 82 | os: linux 83 | prob: 0.8 84 | cost: 1 85 | access: root 86 | service_scan_cost: 1 87 | os_scan_cost: 1 88 | subnet_scan_cost: 1 89 | process_scan_cost: 1 90 | host_configurations: _random 91 | firewall: _subnets 92 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/demo.py: -------------------------------------------------------------------------------- 1 | """Script for running NASim demo 2 | 3 | Usage 4 | ----- 5 | 6 | $ python demo [-ai] [-h] env_name 7 | """ 8 | 9 | import os.path as osp 10 | 11 | from nasimemu import nasim 12 | from nasimemu.nasim.agents.dqn_agent import DQNAgent 13 | from nasimemu.nasim.agents.keyboard_agent import run_keyboard_agent 14 | 15 | 16 | DQN_POLICY_DIR = osp.join( 17 | osp.dirname(osp.abspath(__file__)), 18 | "agents", 19 | "policies" 20 | ) 21 | DQN_POLICIES = { 22 | "tiny": osp.join(DQN_POLICY_DIR, "dqn_tiny.pt"), 23 | "small": osp.join(DQN_POLICY_DIR, "dqn_small.pt") 24 | } 25 | 26 | 27 | if __name__ == "__main__": 28 | import argparse 29 | parser = argparse.ArgumentParser( 30 | description=( 31 | "NASim demo. Play as the hacker, trying to gain access" 32 | " to sensitive information on the network, or run a pre-trained" 33 | " AI hacker." 34 | ) 35 | ) 36 | parser.add_argument("env_name", type=str, 37 | help="benchmark scenario name") 38 | parser.add_argument("-ai", "--run_ai", action="store_true", 39 | help=("Run AI policy (currently ony supported for" 40 | " 'tiny' and 'small' environments")) 41 | args = parser.parse_args() 42 | 43 | if args.run_ai: 44 | assert args.env_name in DQN_POLICIES, \ 45 | ("AI demo only supported for the following environments:" 46 | f" {list(DQN_POLICIES)}") 47 | 48 | env = nasim.make_benchmark( 49 | args.env_name, 50 | fully_obs=False, 51 | flat_actions=True, 52 | flat_obs=True 53 | ) 54 | 55 | line_break = f"\n{'-'*60}" 56 | print(line_break) 57 | print(f"Running Demo on {args.env_name} environment") 58 | if args.run_ai: 59 | print("Using AI policy") 60 | print(line_break) 61 | dqn_agent = DQNAgent(env, verbose=False, **vars(args)) 62 | dqn_agent.load(DQN_POLICIES[args.env_name]) 63 | ret, steps, goal = dqn_agent.run_eval_episode( 64 | env, True, 0.01, "readable" 65 | ) 66 | else: 67 | print("Player controlled") 68 | print(line_break) 69 | ret, steps, goal = run_keyboard_agent(env, "readable") 70 | 71 | print(line_break) 72 | print(f"Episode Complete") 73 | print(line_break) 74 | if goal: 75 | print("Goal accomplished. Sensitive data retrieved!") 76 | print(f"Final Score={ret}") 77 | print(f"Steps taken={steps}") 78 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/benchmark/tiny-small.yaml: -------------------------------------------------------------------------------- 1 | # A tiny-small standard (one public network) network configuration 2 | # (Not quite tiny, not quite small) 3 | # 4 | # 4 subnets 5 | # 5 hosts 6 | # 2 OS 7 | # 3 services 8 | # 2 processes 9 | # 3 exploits 10 | # 2 priv esc actions 11 | # 12 | # Optimal path: 13 | # (e_http, (1, 0)) -> subnet_scan -> (e_ssh, (2, 0)) -> (pe_tomcat, (2,0)) -> (e_http, (3, 1)) 14 | # -> subnet_scan -> (e_ftp, (4, 0)) 15 | # Score = 200 - (2 + 1 + 3 + 1 + 2 + 1 + 1) = 189 16 | # 17 | subnets: [1, 1, 2, 1] 18 | topology: [[ 1, 1, 0, 0, 0], 19 | [ 1, 1, 1, 1, 0], 20 | [ 0, 1, 1, 1, 0], 21 | [ 0, 1, 1, 1, 1], 22 | [ 0, 0, 0, 1, 1]] 23 | sensitive_hosts: 24 | (2, 0): 100 25 | (4, 0): 100 26 | os: 27 | - linux 28 | - windows 29 | services: 30 | - ssh 31 | - ftp 32 | - http 33 | processes: 34 | - tomcat 35 | - daclsvc 36 | exploits: 37 | e_ssh: 38 | service: ssh 39 | os: linux 40 | prob: 0.9 41 | cost: 3 42 | access: user 43 | e_ftp: 44 | service: ftp 45 | os: windows 46 | prob: 0.6 47 | cost: 1 48 | access: root 49 | e_http: 50 | service: http 51 | os: None 52 | prob: 0.9 53 | cost: 2 54 | access: user 55 | privilege_escalation: 56 | pe_tomcat: 57 | process: tomcat 58 | os: linux 59 | prob: 1.0 60 | cost: 1 61 | access: root 62 | pe_daclsvc: 63 | process: daclsvc 64 | os: windows 65 | prob: 1.0 66 | cost: 1 67 | access: root 68 | service_scan_cost: 1 69 | os_scan_cost: 1 70 | subnet_scan_cost: 1 71 | process_scan_cost: 1 72 | host_configurations: 73 | (1, 0): 74 | os: linux 75 | services: [http] 76 | processes: [tomcat] 77 | (2, 0): 78 | os: linux 79 | services: [ssh, ftp] 80 | processes: [tomcat] 81 | (3, 0): 82 | os: windows 83 | services: [ftp] 84 | processes: [] 85 | (3, 1): 86 | os: windows 87 | services: [ftp, http] 88 | processes: [daclsvc] 89 | (4, 0): 90 | os: windows 91 | services: [ssh, ftp] 92 | processes: [] 93 | # two row for each connection between subnets as defined by topology 94 | # one for each direction of connection 95 | # list which services to allow 96 | firewall: 97 | (0, 1): [http] 98 | (1, 0): [] 99 | (1, 2): [ssh] 100 | (2, 1): [ssh] 101 | (1, 3): [] 102 | (3, 1): [ssh] 103 | (2, 3): [http] 104 | (3, 2): [ftp] 105 | (3, 4): [ssh, ftp] 106 | (4, 3): [ftp] 107 | step_limit: 1000 108 | -------------------------------------------------------------------------------- /src/nasimemu/pymetasploit3/msfconsole.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from threading import Timer, Lock 4 | from nasimemu.pymetasploit3.msfrpc import ShellSession 5 | 6 | 7 | class MsfRpcConsoleType: 8 | Console = 0 9 | Meterpreter = 1 10 | Shell = 2 11 | 12 | 13 | class MsfRpcConsole(object): 14 | 15 | def __init__(self, rpc, token=None, cb=None): 16 | """ 17 | Emulates the msfconsole in msf except over RPC. 18 | 19 | Mandatory Arguments: 20 | - rpc : an msfrpc client object 21 | 22 | Optional Arguments: 23 | - cb : a callback function that gets called when data is received from the console. 24 | """ 25 | 26 | self.callback = cb 27 | 28 | if token is not None: 29 | self.console = rpc.sessions.session(token) 30 | self.type_ = MsfRpcConsoleType.Shell if isinstance(self.console, ShellSession) else MsfRpcConsoleType.Meterpreter 31 | self.prompt = '>>> ' 32 | self.callback(dict(data='', prompt=self.prompt)) 33 | else: 34 | self.console = rpc.consoles.console() 35 | self.type_ = MsfRpcConsoleType.Console 36 | self.prompt = '' 37 | 38 | self.lock = Lock() 39 | self.running = True 40 | self._poller() 41 | 42 | def _poller(self): 43 | self.lock.acquire() 44 | if not self.running: 45 | return 46 | d = self.console.read() 47 | self.lock.release() 48 | 49 | if self.type_ == MsfRpcConsoleType.Console: 50 | if d['data'] or self.prompt != d['prompt']: 51 | self.prompt = d['prompt'] 52 | if self.callback is not None: 53 | self.callback(d) 54 | else: 55 | print(d['data']) 56 | else: 57 | if d: 58 | if self.callback is not None: 59 | self.callback(dict(data=d, prompt=self.prompt)) 60 | else: 61 | print(d) 62 | Timer(0.5, self._poller).start() 63 | 64 | def execute(self, command): 65 | """ 66 | Execute a command on the console. 67 | 68 | Mandatory Arguments: 69 | - command : the command to execute 70 | """ 71 | if not command.endswith('\n'): 72 | command += '\n' 73 | self.lock.acquire() 74 | self.console.write(command) 75 | self.lock.release() 76 | 77 | def __del__(self): 78 | self.lock.acquire() 79 | if self.type_ == MsfRpcConsoleType.Console: 80 | self.console.destroy() 81 | self.running = False 82 | self.lock.release() 83 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scripts/run_dqn_policy.py: -------------------------------------------------------------------------------- 1 | """A script for running a pre-trained DQN agent 2 | 3 | Note, user must ensure the DQN policy matches the NASim 4 | Environment used to train it in terms of size. 5 | 6 | E.g. A policy trained on the 'tiny-gen' env can be tested 7 | against the 'tiny' env since they both have the same Action 8 | and Observation spaces. 9 | 10 | But a policy trained on 'tiny-gen' could not be used on the 11 | 'small' environment (or any non-'tiny' environment for that 12 | matter) 13 | """ 14 | 15 | from nasimemu import nasim 16 | from nasimemu.nasim.agents.dqn_agent import DQNAgent 17 | 18 | 19 | if __name__ == "__main__": 20 | import argparse 21 | parser = argparse.ArgumentParser() 22 | parser.add_argument("env_name", type=str, help="benchmark scenario name") 23 | parser.add_argument("policy_path", type=str, help="path to policy") 24 | parser.add_argument("-o", "--partially_obs", action="store_true", 25 | help="Partially Observable Mode") 26 | parser.add_argument("--eval_eps", type=int, default=1, 27 | help="Number of episodes to run (default=1)") 28 | parser.add_argument("--seed", type=int, default=0, 29 | help="Random seed (default=0)") 30 | parser.add_argument("--epsilon", type=float, default=0.05, 31 | help=("Epsilon (i.e. random action probability) to use" 32 | "(default=0.05)")) 33 | parser.add_argument("--render", action="store_true", 34 | help="Render the episode/s") 35 | args = parser.parse_args() 36 | 37 | env = nasim.make_benchmark(args.env_name, 38 | args.seed, 39 | fully_obs=not args.partially_obs, 40 | flat_actions=True, 41 | flat_obs=True) 42 | dqn_agent = DQNAgent(env, verbose=False, **vars(args)) 43 | dqn_agent.load(args.policy_path) 44 | 45 | total_ret = 0 46 | total_steps = 0 47 | goals = 0 48 | print(f"\n{'-'*60}\nRunning DQN Policy:\n\t{args.policy_path}\n{'-'*60}") 49 | for i in range(args.eval_eps): 50 | ret, steps, goal = dqn_agent.run_eval_episode( 51 | env, args.render, args.epsilon 52 | ) 53 | print(f"Episode {i} return={ret}, steps={steps}, goal reached={goal}") 54 | total_ret += ret 55 | total_steps += steps 56 | goals += int(goal) 57 | 58 | print(f"\n{'-'*60}\nDone\n{'-'*60}") 59 | print(f"Average Return = {total_ret / args.eval_eps:.2f}") 60 | print(f"Average Steps = {total_steps / args.eval_eps:.2f}") 61 | print(f"Goals = {goals} / {args.eval_eps}") 62 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/benchmark/small.yaml: -------------------------------------------------------------------------------- 1 | # A small standard (one public network) network configuration 2 | # 3 | # 4 subnets 4 | # 8 hosts 5 | # 2 OS 6 | # 3 services 7 | # 2 processes 8 | # 3 exploits 9 | # 2 priv esc 10 | # 11 | # Optimal path: 12 | # (e_http, (1, 0)) -> subnet_scan -> (e_ssh, (2, 0)) -> (pe_tomcat, (2, 0)) 13 | # -> (e_http, (3, 1)) -> subnet_scan -> (e_ssh, (4, 0) 14 | # -> (pe_tomcat, (4, 0)) 15 | # Score = 200 - (2 + 1 + 3 + 1 + 2 + 1 + 3 + 1) = 186 16 | # 17 | subnets: [1, 1, 5, 1] 18 | topology: [[ 1, 1, 0, 0, 0], 19 | [ 1, 1, 1, 1, 0], 20 | [ 0, 1, 1, 1, 0], 21 | [ 0, 1, 1, 1, 1], 22 | [ 0, 0, 0, 1, 1]] 23 | sensitive_hosts: 24 | (2, 0): 100 25 | (4, 0): 100 26 | os: 27 | - linux 28 | - windows 29 | services: 30 | - ssh 31 | - ftp 32 | - http 33 | processes: 34 | - tomcat 35 | - daclsvc 36 | exploits: 37 | e_ssh: 38 | service: ssh 39 | os: linux 40 | prob: 0.9 41 | cost: 3 42 | access: user 43 | e_ftp: 44 | service: ftp 45 | os: windows 46 | prob: 0.6 47 | cost: 1 48 | access: user 49 | e_http: 50 | service: http 51 | os: None 52 | prob: 0.9 53 | cost: 2 54 | access: user 55 | privilege_escalation: 56 | pe_tomcat: 57 | process: tomcat 58 | os: linux 59 | prob: 1.0 60 | cost: 1 61 | access: root 62 | pe_daclsvc: 63 | process: daclsvc 64 | os: windows 65 | prob: 1.0 66 | cost: 1 67 | access: root 68 | service_scan_cost: 1 69 | os_scan_cost: 1 70 | subnet_scan_cost: 1 71 | process_scan_cost: 1 72 | host_configurations: 73 | (1, 0): 74 | os: linux 75 | services: [http] 76 | processes: [] 77 | (2, 0): 78 | os: linux 79 | services: [ssh, ftp] 80 | processes: [tomcat] 81 | (3, 0): 82 | os: windows 83 | services: [ftp] 84 | processes: [] 85 | (3, 1): 86 | os: windows 87 | services: [ftp, http] 88 | processes: [daclsvc] 89 | (3, 2): 90 | os: windows 91 | services: [ftp] 92 | processes: [daclsvc] 93 | (3, 3): 94 | os: windows 95 | services: [ftp] 96 | processes: [] 97 | (3, 4): 98 | os: windows 99 | services: [ftp] 100 | processes: [daclsvc] 101 | (4, 0): 102 | os: linux 103 | services: [ssh, ftp] 104 | processes: [tomcat] 105 | # two row for each connection between subnets as defined by topology 106 | # one for each direction of connection 107 | # list which services to allow 108 | firewall: 109 | (0, 1): [http] 110 | (1, 0): [] 111 | (1, 2): [ssh] 112 | (2, 1): [ssh] 113 | (1, 3): [] 114 | (3, 1): [ssh] 115 | (2, 3): [http] 116 | (3, 2): [ftp] 117 | (3, 4): [ssh, ftp] 118 | (4, 3): [ftp] 119 | step_limit: 1000 120 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/benchmark/small-honeypot.yaml: -------------------------------------------------------------------------------- 1 | # A small standard (one public network) network configuration containing a 2 | # honeypot host (3, 2). 3 | # 4 | # 4 subnets 5 | # 8 hosts 6 | # 2 OS 7 | # 3 services 8 | # 2 processes 9 | # 3 exploits 10 | # 2 priv esc 11 | # 12 | # Optimal path: 13 | # (e_http, (1, 0)) -> subnet_scan -> (e_ssh, (2, 0)) -> (pe_tomcat, (2, 0)) 14 | # -> (e_http, (3, 1)) -> subnet_scan -> (e_ssh, (4, 0) 15 | # -> (pe_tomcat, (4, 0)) 16 | # Score = 200 - (2 + 1 + 3 + 1 + 2 + 1 + 3 + 1) = 186 17 | # 18 | subnets: [1, 1, 5, 1] 19 | topology: [[ 1, 1, 0, 0, 0], 20 | [ 1, 1, 1, 1, 0], 21 | [ 0, 1, 1, 1, 0], 22 | [ 0, 1, 1, 1, 1], 23 | [ 0, 0, 0, 1, 1]] 24 | sensitive_hosts: 25 | (2, 0): 100 26 | (4, 0): 100 27 | os: 28 | - linux 29 | - windows 30 | services: 31 | - ssh 32 | - ftp 33 | - http 34 | processes: 35 | - tomcat 36 | - daclsvc 37 | exploits: 38 | e_ssh: 39 | service: ssh 40 | os: linux 41 | prob: 0.9 42 | cost: 3 43 | access: user 44 | e_ftp: 45 | service: ftp 46 | os: windows 47 | prob: 0.6 48 | cost: 1 49 | access: user 50 | e_http: 51 | service: http 52 | os: None 53 | prob: 0.9 54 | cost: 2 55 | access: user 56 | privilege_escalation: 57 | pe_tomcat: 58 | process: tomcat 59 | os: linux 60 | prob: 1.0 61 | cost: 1 62 | access: root 63 | pe_daclsvc: 64 | process: daclsvc 65 | os: windows 66 | prob: 1.0 67 | cost: 1 68 | access: root 69 | service_scan_cost: 1 70 | os_scan_cost: 1 71 | subnet_scan_cost: 1 72 | process_scan_cost: 1 73 | host_configurations: 74 | (1, 0): 75 | os: linux 76 | services: [http] 77 | processes: [] 78 | (2, 0): 79 | os: linux 80 | services: [ssh, ftp] 81 | processes: [tomcat] 82 | (3, 0): 83 | os: windows 84 | services: [ftp] 85 | processes: [] 86 | (3, 1): 87 | os: windows 88 | services: [ftp, http] 89 | processes: [daclsvc] 90 | (3, 2): 91 | os: windows 92 | services: [ftp, http] 93 | processes: [daclsvc] 94 | # This host is the honeypot so has large negative value 95 | value: -100 96 | (3, 3): 97 | os: windows 98 | services: [ftp] 99 | processes: [] 100 | (3, 4): 101 | os: windows 102 | services: [ftp] 103 | processes: [daclsvc] 104 | (4, 0): 105 | os: linux 106 | services: [ssh, ftp] 107 | processes: [tomcat] 108 | # two row for each connection between subnets as defined by topology 109 | # one for each direction of connection 110 | # list which services to allow 111 | firewall: 112 | (0, 1): [http] 113 | (1, 0): [] 114 | (1, 2): [ssh] 115 | (2, 1): [ssh] 116 | (1, 3): [] 117 | (3, 1): [ssh] 118 | (2, 3): [http] 119 | (3, 2): [ftp] 120 | (3, 4): [ssh, ftp] 121 | (4, 3): [ftp] 122 | step_limit: 1000 123 | -------------------------------------------------------------------------------- /docs/SERVICES.md: -------------------------------------------------------------------------------- 1 | # OS 2 | 3 | ## Attacker 4 | 5 | * OS: Kali linux 6 | * Box: [kalilinux/rolling](https://app.vagrantup.com/kalilinux/boxes/rolling) 7 | 8 | 9 | ## Linux 10 | 11 | * OS: Ubuntu 14.04 Metasploitable3 12 | * Box: [rapid7/metasploitable3-ub1404](https://app.vagrantup.com/rapid7/boxes/metasploitable3-ub1404) 13 | 14 | 15 | ## Windows 16 | 17 | * OS: Windows 2008 Metasploitable3 18 | * Box: [rapid7/metasploitable3-win2k8](https://app.vagrantup.com/rapid7/boxes/metasploitable3-win2k8) 19 | 20 | # Services and Exploits 21 | 22 | ## mysql 23 | * OS: any 24 | * Port: 3306 25 | * CVE: - 26 | * Service name in NASimEmu: 3306_any_mysql 27 | 28 | This service is not exploitable. In the default scenarios, it is used as a `sensitive_service`, which is installed on all sensitive nodes (and on 10% of non-sensitive nodes). 29 | 30 | ## proftpd 31 | 32 | * OS: Linux 33 | * Port: 21 34 | * CVE: [CVE-2015-3306](https://www.cve.org/CVERecord?id=CVE-2015-3306) 35 | * Service name in NASimEmu: 21_linux_proftpd 36 | 37 | 38 | [More information about the exploit.](https://www.rapid7.com/db/modules/exploit/unix/ftp/proftpd_modcopy_exec/) 39 | 40 | ## drupal 41 | 42 | * Os: Linux 43 | * Port: 80 44 | * Path: /drupal/ 45 | * Service name in NASimEmu: 80_linux_drupal 46 | 47 | [More information about the vulnerability](https://www.drupal.org/node/2765575). 48 | 49 | [More information about the exploit.](https://www.rapid7.com/db/modules/exploit/unix/webapp/drupal_coder_exec/) 50 | 51 | ## Elasticsearch 52 | 53 | The exploit use a security issue in the ElasticSearch prior to version 1.2.0. 54 | 55 | * OS: Windows 56 | * Port: 9200 57 | * CVE: [CVE-2014-3120](https://www.cve.org/CVERecord?id=CVE-2014-3120) 58 | * Service name in NASimEmu: 9200_windows_elasticsearch 59 | 60 | The vulnerability is available in the Windows metasploitable VM : for more information [here](https://github.com/rapid7/metasploitable3/wiki/Vulnerabilities#elasticsearch). 61 | 62 | [More information about the exploit.](https://www.rapid7.com/db/modules/exploit/multi/elasticsearch/script_mvel_rce) 63 | 64 | 65 | ## Wordpress 66 | 67 | The exploit use a security issue in the Ninja Forms plugin (before version 2.9.42.1). 68 | 69 | * OS: Windows 70 | * Port: 80 71 | * Path: /wordpress/ 72 | * CVE: [CVE-2016-1209](https://www.cve.org/CVERecord?id=CVE-2016-1209) 73 | * Service name in NASimEmu: 80_windows_wp_ninja 74 | 75 | The vulnerability is available in the Windows metasploitable VM : for more information [here](https://github.com/rapid7/metasploitable3/wiki/Vulnerabilities#wordpress). 76 | 77 | [More information about the exploit.](https://www.rapid7.com/db/modules/exploit/multi/http/wp_ninja_forms_unauthenticated_file_upload/) 78 | 79 | 80 | ## phpwiki 81 | 82 | * OS: Linux 83 | * Port: 80 84 | * Path: /phpwiki/ 85 | * CVE: [CVE-2014-5519](https://www.cve.org/CVERecord?id=CVE-2014-5519) 86 | * Service name in NASimEmu: 80_linux_phpwiki 87 | 88 | [More information about the exploit.](https://www.rapid7.com/db/modules/exploit/multi/http/phpwiki_ploticus_exec/) 89 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils import INTERNET 2 | from .scenario import Scenario 3 | from .loader import ScenarioLoader 4 | from .loader_v2 import ScenarioLoaderV2 5 | from .generator import ScenarioGenerator 6 | import nasimemu.nasim.scenarios.benchmark as benchmark 7 | 8 | from pathlib import Path 9 | 10 | def make_benchmark_scenario(scenario_name, seed=None): 11 | """Generate or Load a benchmark Scenario. 12 | 13 | Parameters 14 | ---------- 15 | scenario_name : str 16 | the name of the benchmark environment 17 | seed : int, optional 18 | random seed to use to generate environment (default=None) 19 | 20 | Returns 21 | ------- 22 | Scenario 23 | a new scenario instance 24 | 25 | Raises 26 | ------ 27 | NotImplementederror 28 | if scenario_name does no match any implemented benchmark scenarios. 29 | """ 30 | if scenario_name in benchmark.AVAIL_GEN_BENCHMARKS: 31 | params = benchmark.AVAIL_GEN_BENCHMARKS[scenario_name] 32 | params['seed'] = seed 33 | return generate_scenario(**params) 34 | elif scenario_name in benchmark.AVAIL_STATIC_BENCHMARKS: 35 | scenario_def = benchmark.AVAIL_STATIC_BENCHMARKS[scenario_name] 36 | return load_scenario(scenario_def["file"], name=scenario_name) 37 | else: 38 | raise NotImplementedError( 39 | f"Benchmark scenario '{scenario_name}' not available." 40 | f"Available scenarios are: {benchmark.AVAIL_BENCHMARKS}" 41 | ) 42 | 43 | 44 | def generate_scenario(num_hosts, num_services, **params): 45 | """Generate Scenario from network parameters. 46 | 47 | Parameters 48 | ---------- 49 | num_hosts : int 50 | number of hosts to include in network (minimum is 3) 51 | num_services : int 52 | number of services to use in environment (minimum is 1) 53 | params : dict, optional 54 | generator params (see :class:`ScenarioGenertor` for full list) 55 | 56 | Returns 57 | ------- 58 | Scenario 59 | a new scenario object 60 | """ 61 | generator = ScenarioGenerator() 62 | return generator.generate(num_hosts, num_services, **params) 63 | 64 | 65 | def load_scenario(path, name=None): 66 | """Load NASim Environment from a .yaml scenario file. 67 | 68 | Parameters 69 | ---------- 70 | path : str 71 | path to the .yaml scenario file 72 | name : str, optional 73 | the scenarios name, if None name will be generated from path 74 | (default=None) 75 | 76 | Returns 77 | ------- 78 | Scenario 79 | a new scenario object 80 | """ 81 | if '.v2' in Path(path).suffixes: 82 | loader = ScenarioLoaderV2() 83 | else: 84 | loader = ScenarioLoader() 85 | 86 | return loader.load(path, name=name) 87 | 88 | 89 | def get_scenario_max(scenario_name): 90 | if scenario_name in benchmark.AVAIL_GEN_BENCHMARKS: 91 | return benchmark.AVAIL_GEN_BENCHMARKS[scenario_name]["max_score"] 92 | elif scenario_name in benchmark.AVAIL_STATIC_BENCHMARKS: 93 | return benchmark.AVAIL_STATIC_BENCHMARKS[scenario_name]["max_score"] 94 | return None 95 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/benchmark/small-linear.yaml: -------------------------------------------------------------------------------- 1 | # A small network with 2 | # 3 | # 6 subnets 4 | # 8 hosts 5 | # 2 OS 6 | # 3 services 7 | # 2 processes 8 | # 3 exploits 9 | # 2 priv esc 10 | # 11 | # - subnets organized in a linear network 12 | # - sensitive documents located in two middle subnets 13 | # - end subnets are both connected to internet 14 | # - two middle subnets are not connected to each other 15 | # 16 | # Optimal path: 17 | # (e_http, (1, 0)) -> subnet_scan -> (e_ssh, (2, 0)) -> subnet_scan -> (e_ssh, (3, 1)) -> (e_ftp, (3, 0)) 18 | # (e_http, (6, 0)) -> subnet_scan -> (e_ssh, (5, 0)) -> subnet_scan -> (e_http, (4, 0)) -> (pe_daclsvc, (4, 0)) 19 | # Score = 200 - (2+1+3+1+3+1+2+1+3+1+1+1) = 179 20 | # 21 | subnets: [1, 1, 2, 1, 2, 1] 22 | topology: [[ 1, 1, 0, 0, 0, 0, 1], # 0 connected to 1 and 6 23 | [ 1, 1, 1, 0, 0, 0, 0], # 1 connected to 0 and 2 24 | [ 0, 1, 1, 1, 0, 0, 0], # 2 connected to 1 and 3 25 | [ 0, 0, 1, 1, 1, 0, 0], # 3 connected to 2 and 4 26 | [ 0, 0, 0, 1, 1, 1, 0], # 4 connected to 3 and 5 27 | [ 0, 0, 0, 0, 1, 1, 1], # 5 connected to 4 and 6 28 | [ 1, 0, 0, 0, 0, 1, 1]] # 6 connected to 5 and 0 29 | sensitive_hosts: 30 | (3, 0): 100 31 | (4, 0): 100 32 | os: 33 | - linux 34 | - windows 35 | services: 36 | - ssh 37 | - ftp 38 | - http 39 | processes: 40 | - tomcat 41 | - daclsvc 42 | exploits: 43 | e_ssh: 44 | service: ssh 45 | os: linux 46 | prob: 0.9 47 | cost: 3 48 | access: user 49 | e_ftp: 50 | service: ftp 51 | os: windows 52 | prob: 0.6 53 | cost: 1 54 | access: root 55 | e_http: 56 | service: http 57 | os: None 58 | prob: 0.9 59 | cost: 2 60 | access: user 61 | privilege_escalation: 62 | pe_tomcat: 63 | process: tomcat 64 | os: linux 65 | prob: 1.0 66 | cost: 1 67 | access: root 68 | pe_daclsvc: 69 | process: daclsvc 70 | os: windows 71 | prob: 1.0 72 | cost: 1 73 | access: root 74 | service_scan_cost: 1 75 | os_scan_cost: 1 76 | subnet_scan_cost: 1 77 | process_scan_cost: 1 78 | host_configurations: 79 | (1, 0): 80 | os: linux 81 | services: [http] 82 | processes: [] 83 | (2, 0): 84 | os: linux 85 | services: [ssh, ftp] 86 | processes: [tomcat] 87 | (3, 0): 88 | os: windows 89 | services: [ftp] 90 | processes: [] 91 | (3, 1): 92 | os: linux 93 | services: [ssh] 94 | processes: [] 95 | (4, 0): 96 | os: windows 97 | services: [http] 98 | processes: [daclsvc] 99 | (5, 0): 100 | os: linux 101 | services: [ftp, ssh] 102 | processes: [] 103 | (5, 1): 104 | os: windows 105 | services: [ftp] 106 | processes: [daclsvc] 107 | (6, 0): 108 | os: linux 109 | services: [http] 110 | processes: [tomcat] 111 | # two row for each connection between subnets as defined by topology 112 | # one for each direction of connection 113 | # list which services to allow 114 | firewall: 115 | (0, 1): [http] 116 | (1, 0): [] 117 | (1, 2): [ssh, ftp] 118 | (2, 1): [http] 119 | (2, 3): [ssh] 120 | (3, 2): [ssh, ftp] 121 | (3, 4): [] # no traffic permitted between middle networks 122 | (4, 3): [] # no traffic permitted between middle networks 123 | (4, 5): [ftp] 124 | (5, 4): [ftp, http] 125 | (5, 6): [http] 126 | (6, 5): [ssh] 127 | (6, 0): [] 128 | (0, 6): [http] 129 | step_limit: 1000 130 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/benchmark/medium-single-site.yaml: -------------------------------------------------------------------------------- 1 | # A network with a single subnet that has one vulnerable host that must be compromised 2 | # to access other hosts behind firewall 3 | # 4 | # 1 subnet 5 | # 16 hosts 6 | # 2 OS 7 | # 5 services 8 | # 3 processes 9 | # 5 exploits 10 | # 3 priv esc 11 | # 12 | # |A| = 16 * (5 + 3 + 4) = 192 13 | # 14 | # Optimal path: 15 | # (e_http, (1, 7) or (1, 15)) -> (e_smtp, (1, 3)) -> (pe_schtask, (1, 3)) 16 | # -> (e_ssh, (1, 8)) -> (pe_tomcat, (1, 8)) 17 | # Score = 200 - (2 + 3 + 1 + 3 + 1) = 190 18 | # 19 | subnets: [16] 20 | topology: [[ 1, 1], 21 | [ 1, 1]] 22 | sensitive_hosts: 23 | (1, 3): 100 24 | (1, 8): 100 25 | os: 26 | - linux 27 | - windows 28 | services: 29 | - ssh 30 | - ftp 31 | - http 32 | - samba 33 | - smtp 34 | processes: 35 | - tomcat 36 | - daclsvc 37 | - schtask 38 | exploits: 39 | e_ssh: 40 | service: ssh 41 | os: linux 42 | prob: 0.9 43 | cost: 3 44 | access: user 45 | e_ftp: 46 | service: ftp 47 | os: windows 48 | prob: 0.6 49 | cost: 1 50 | access: root 51 | e_http: 52 | service: http 53 | os: None 54 | prob: 0.9 55 | cost: 2 56 | access: user 57 | e_samba: 58 | service: samba 59 | os: linux 60 | prob: 0.3 61 | cost: 2 62 | access: root 63 | e_smtp: 64 | service: smtp 65 | os: windows 66 | prob: 0.6 67 | cost: 3 68 | access: user 69 | privilege_escalation: 70 | pe_tomcat: 71 | process: tomcat 72 | os: linux 73 | prob: 1.0 74 | cost: 1 75 | access: root 76 | pe_daclsvc: 77 | process: daclsvc 78 | os: windows 79 | prob: 1.0 80 | cost: 1 81 | access: root 82 | pe_schtask: 83 | process: schtask 84 | os: windows 85 | prob: 1.0 86 | cost: 1 87 | access: root 88 | service_scan_cost: 1 89 | os_scan_cost: 1 90 | subnet_scan_cost: 1 91 | process_scan_cost: 1 92 | host_configurations: 93 | (1, 0): 94 | os: linux 95 | services: [ftp] 96 | processes: [tomcat] 97 | (1, 1): 98 | os: linux 99 | services: [ftp, ssh] 100 | processes: [tomcat] 101 | (1, 2): 102 | os: windows 103 | services: [ftp] 104 | processes: [schtask] 105 | (1, 3): 106 | os: windows 107 | services: [smtp] 108 | processes: [schtask] 109 | (1, 4): 110 | os: windows 111 | services: [ftp] 112 | processes: [schtask] 113 | (1, 5): 114 | os: linux 115 | services: [ftp, ssh] 116 | processes: [tomcat] 117 | (1, 6): 118 | os: windows 119 | services: [ftp] 120 | processes: [daclsvc] 121 | (1, 7): 122 | os: windows 123 | services: [http] 124 | processes: [] 125 | (1, 8): 126 | os: linux 127 | services: [ssh] 128 | processes: [tomcat] 129 | (1, 9): 130 | os: windows 131 | services: [ftp] 132 | processes: [schtask] 133 | (1, 10): 134 | os: windows 135 | services: [ssh] 136 | processes: [] 137 | (1, 11): 138 | os: windows 139 | services: [ftp] 140 | processes: [daclsvc] 141 | (1, 12): 142 | os: windows 143 | services: [ftp, ssh] 144 | processes: [] 145 | (1, 13): 146 | os: windows 147 | services: [ftp] 148 | processes: [] 149 | (1, 14): 150 | os: windows 151 | services: [ftp] 152 | processes: [schtask] 153 | (1, 15): 154 | os: linux 155 | services: [http] 156 | processes: [] 157 | firewall: 158 | (0, 1): [http] 159 | (1, 0): [] 160 | step_limit: 2000 161 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scripts/train_dqn.py: -------------------------------------------------------------------------------- 1 | """A script for training a DQN agent and storing best policy """ 2 | 3 | from nasimemu import nasim 4 | from nasimemu.nasim.agents.dqn_agent import DQNAgent 5 | 6 | 7 | class BestDQN(DQNAgent): 8 | """A DQN Agent which saves best policy found during training """ 9 | 10 | def __init__(self, 11 | env, 12 | save_path, 13 | eval_epsilon=0.01, 14 | **kwargs): 15 | super().__init__(env, **kwargs) 16 | self.save_path = save_path 17 | self.eval_epsilon = eval_epsilon 18 | self.best_score = -float("inf") 19 | 20 | def run_train_episode(self, step_limit): 21 | ep_ret, steps, goal_reached = super().run_train_episode(step_limit) 22 | 23 | if self.steps_done > self.exploration_steps: 24 | eval_ret, _, _ = self.run_eval_episode( 25 | eval_epsilon=self.eval_epsilon 26 | ) 27 | if eval_ret > self.best_score: 28 | print(f"Saving New Best Score = {ep_ret}") 29 | self.best_score = eval_ret 30 | self.save(self.save_path) 31 | 32 | return ep_ret, steps, goal_reached 33 | 34 | 35 | if __name__ == "__main__": 36 | import argparse 37 | parser = argparse.ArgumentParser() 38 | parser.add_argument("env_name", type=str, help="benchmark scenario name") 39 | parser.add_argument("save_path", type=str, help="save path for agent") 40 | parser.add_argument("-o", "--partially_obs", action="store_true", 41 | help="Partially Observable Mode") 42 | parser.add_argument("--eval_epsilon", type=float, default=0.01, 43 | help="Epsilon to use for evaluation (default=0.01)") 44 | parser.add_argument("--hidden_sizes", type=int, nargs="*", 45 | default=[64, 64], 46 | help="(default=[64. 64])") 47 | parser.add_argument("--lr", type=float, default=0.001, 48 | help="Learning rate (default=0.001)") 49 | parser.add_argument("--training_steps", type=int, default=10000, 50 | help="training steps (default=10000)") 51 | parser.add_argument("--batch_size", type=int, default=32, 52 | help="(default=32)") 53 | parser.add_argument("--target_update_freq", type=int, default=1000, 54 | help="(default=1000)") 55 | parser.add_argument("--seed", type=int, default=0, 56 | help="(default=0)") 57 | parser.add_argument("--replay_size", type=int, default=100000, 58 | help="(default=100000)") 59 | parser.add_argument("--final_epsilon", type=float, default=0.05, 60 | help="(default=0.05)") 61 | parser.add_argument("--exploration_steps", type=int, default=5000, 62 | help="(default=5000)") 63 | parser.add_argument("--gamma", type=float, default=0.99, 64 | help="(default=0.99)") 65 | args = parser.parse_args() 66 | assert args.training_steps > args.exploration_steps 67 | 68 | env = nasim.make_benchmark(args.env_name, 69 | args.seed, 70 | fully_obs=not args.partially_obs, 71 | flat_actions=True, 72 | flat_obs=True) 73 | dqn_agent = BestDQN(env, **vars(args)) 74 | dqn_agent.train() 75 | 76 | print(f"\n{'-'*60}\nDone\n{'-'*60}") 77 | print(f"Best Policy score = {dqn_agent.best_score}") 78 | print(f"Policy saved to: {dqn_agent.save_path}") 79 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/agents/random_agent.py: -------------------------------------------------------------------------------- 1 | """A random agent that selects a random action at each step 2 | 3 | To run 'tiny' benchmark scenario with default settings, run the following from 4 | the nasim/agents dir: 5 | 6 | $ python random_agent.py tiny 7 | 8 | This will run the agent and display progress and final results to stdout. 9 | 10 | To see available running arguments: 11 | 12 | $ python random_agent.py --help 13 | """ 14 | 15 | import numpy as np 16 | 17 | from nasimemu import nasim 18 | 19 | LINE_BREAK = "-"*60 20 | 21 | 22 | def run_random_agent(env, step_limit=1e6, verbose=True): 23 | if verbose: 24 | print(LINE_BREAK) 25 | print("STARTING EPISODE") 26 | print(LINE_BREAK) 27 | print(f"t: Reward") 28 | 29 | env.reset() 30 | total_reward = 0 31 | done = False 32 | t = 0 33 | a = 0 34 | 35 | while not done and t < step_limit: 36 | a = env.action_space.sample() 37 | _, r, done, _ = env.step(a) 38 | total_reward += r 39 | if (t+1) % 100 == 0 and verbose: 40 | print(f"{t}: {total_reward}") 41 | t += 1 42 | 43 | if done and verbose: 44 | print(LINE_BREAK) 45 | print("EPISODE FINISHED") 46 | print(LINE_BREAK) 47 | print(f"Total steps = {t}") 48 | print(f"Total reward = {total_reward}") 49 | elif verbose: 50 | print(LINE_BREAK) 51 | print("STEP LIMIT REACHED") 52 | print(LINE_BREAK) 53 | 54 | if done: 55 | done = env.goal_reached() 56 | 57 | return t, total_reward, done 58 | 59 | 60 | if __name__ == "__main__": 61 | import argparse 62 | parser = argparse.ArgumentParser() 63 | parser.add_argument("env_name", type=str, 64 | help="benchmark scenario name") 65 | parser.add_argument("-s", "--seed", type=int, default=0, 66 | help="random seed") 67 | parser.add_argument("-r", "--runs", type=int, default=1, 68 | help="number of random runs to perform (default=1)") 69 | parser.add_argument("-o", "--partially_obs", action="store_true", 70 | help="Partially Observable Mode") 71 | parser.add_argument("-p", "--param_actions", action="store_true", 72 | help="Use Parameterised action space") 73 | parser.add_argument("-f", "--box_obs", action="store_true", 74 | help="Use 2D observation space") 75 | args = parser.parse_args() 76 | 77 | seed = args.seed 78 | run_steps = [] 79 | run_rewards = [] 80 | run_goals = 0 81 | for i in range(args.runs): 82 | env = nasim.make_benchmark(args.env_name, 83 | seed, 84 | not args.partially_obs, 85 | not args.param_actions, 86 | not args.box_obs) 87 | steps, reward, done = run_random_agent(env, verbose=False) 88 | run_steps.append(steps) 89 | run_rewards.append(reward) 90 | run_goals += int(done) 91 | seed += 1 92 | 93 | if args.runs > 1: 94 | print(f"Run {i}:") 95 | print(f"\tSteps = {steps}") 96 | print(f"\tReward = {reward}") 97 | print(f"\tGoal reached = {done}") 98 | 99 | run_steps = np.array(run_steps) 100 | run_rewards = np.array(run_rewards) 101 | 102 | print(LINE_BREAK) 103 | print("Random Agent Runs Complete") 104 | print(LINE_BREAK) 105 | print(f"Mean steps = {run_steps.mean():.2f} +/- {run_steps.std():.2f}") 106 | print(f"Mean rewards = {run_rewards.mean():.2f} " 107 | f"+/- {run_rewards.std():.2f}") 108 | print(f"Goals reached = {run_goals} / {args.runs}") 109 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/benchmark/medium.yaml: -------------------------------------------------------------------------------- 1 | # A medium standard (one public subnet) network configuration 2 | # 3 | # 16 hosts 4 | # 5 subnets 5 | # 2 OS 6 | # 5 services 7 | # 3 processes 8 | # 5 exploits 9 | # 3 priv esc 10 | # 11 | # |A| = 16 * (5 + 3 + 4) = 192 12 | # 13 | # Optimal path: 14 | # (e_http, (1, 0)) -> subnet_scan -> (e_smtp, (2, 0)) -> (pe_schtask, (2, 0) -> (e_http, (3, 1)) 15 | # -> subnet_scan -> (e_ssh, (5, 0)) -> (e_samba, (5, 0)) 16 | # Score = 200 - (2+1+3+1+2+1+3+2) = 185 17 | # 18 | 19 | subnets: [1, 1, 5, 5, 4] 20 | topology: [[ 1, 1, 0, 0, 0, 0], 21 | [ 1, 1, 1, 1, 0, 0], 22 | [ 0, 1, 1, 1, 0, 0], 23 | [ 0, 1, 1, 1, 1, 1], 24 | [ 0, 0, 0, 1, 1, 0], 25 | [ 0, 0, 0, 1, 0, 1]] 26 | sensitive_hosts: 27 | (2, 0): 100 28 | (5, 0): 100 29 | os: 30 | - linux 31 | - windows 32 | services: 33 | - ssh 34 | - ftp 35 | - http 36 | - samba 37 | - smtp 38 | processes: 39 | - tomcat 40 | - daclsvc 41 | - schtask 42 | exploits: 43 | e_ssh: 44 | service: ssh 45 | os: linux 46 | prob: 0.9 47 | cost: 3 48 | access: user 49 | e_ftp: 50 | service: ftp 51 | os: windows 52 | prob: 0.6 53 | cost: 1 54 | access: root 55 | e_http: 56 | service: http 57 | os: None 58 | prob: 0.9 59 | cost: 2 60 | access: user 61 | e_samba: 62 | service: samba 63 | os: linux 64 | prob: 0.3 65 | cost: 2 66 | access: root 67 | e_smtp: 68 | service: smtp 69 | os: windows 70 | prob: 0.6 71 | cost: 3 72 | access: user 73 | privilege_escalation: 74 | pe_tomcat: 75 | process: tomcat 76 | os: linux 77 | prob: 1.0 78 | cost: 1 79 | access: root 80 | pe_daclsvc: 81 | process: daclsvc 82 | os: windows 83 | prob: 1.0 84 | cost: 1 85 | access: root 86 | pe_schtask: 87 | process: schtask 88 | os: windows 89 | prob: 1.0 90 | cost: 1 91 | access: root 92 | service_scan_cost: 1 93 | os_scan_cost: 1 94 | subnet_scan_cost: 1 95 | process_scan_cost: 1 96 | host_configurations: 97 | (1, 0): 98 | os: linux 99 | services: [http] 100 | processes: [] 101 | (2, 0): 102 | os: windows 103 | services: [smtp] 104 | processes: [schtask] 105 | (3, 0): 106 | os: windows 107 | services: [ftp] 108 | processes: [schtask] 109 | (3, 1): 110 | os: windows 111 | services: [ftp, http] 112 | processes: [daclsvc] 113 | (3, 2): 114 | os: windows 115 | services: [ftp] 116 | processes: [] 117 | (3, 3): 118 | os: windows 119 | services: [ftp] 120 | processes: [schtask] 121 | (3, 4): 122 | os: windows 123 | services: [ftp] 124 | processes: [schtask] 125 | (4, 0): 126 | os: linux 127 | services: [ssh] 128 | processes: [] 129 | (4, 1): 130 | os: linux 131 | services: [ssh] 132 | processes: [] 133 | (4, 2): 134 | os: linux 135 | services: [ssh] 136 | processes: [] 137 | (4, 3): 138 | os: windows 139 | services: [ssh, ftp] 140 | processes: [tomcat] 141 | (4, 4): 142 | os: windows 143 | services: [ssh, ftp] 144 | processes: [tomcat] 145 | (5, 0): 146 | os: linux 147 | services: [ssh, samba] 148 | processes: [] 149 | (5, 1): 150 | os: linux 151 | services: [ssh, http] 152 | processes: [tomcat] 153 | (5, 2): 154 | os: linux 155 | services: [ssh] 156 | processes: [] 157 | (5, 3): 158 | os: linux 159 | services: [ssh] 160 | processes: [] 161 | firewall: 162 | (0, 1): [http] 163 | (1, 0): [] 164 | (1, 2): [smtp] 165 | (2, 1): [ssh] 166 | (1, 3): [] 167 | (3, 1): [ssh] 168 | (2, 3): [http] 169 | (3, 2): [smtp] 170 | (3, 4): [ssh, ftp] 171 | (4, 3): [ftp, ssh] 172 | (3, 5): [ssh, ftp] 173 | (5, 3): [ftp, ssh] 174 | step_limit: 2000 175 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/agents/bruteforce_agent.py: -------------------------------------------------------------------------------- 1 | """An bruteforce agent that repeatedly cycles through all available actions in 2 | order. 3 | 4 | To run 'tiny' benchmark scenario with default settings, run the following from 5 | the nasim/agents dir: 6 | 7 | $ python bruteforce_agent.py tiny 8 | 9 | This will run the agent and display progress and final results to stdout. 10 | 11 | To see available running arguments: 12 | 13 | $ python bruteforce_agent.py --help 14 | """ 15 | 16 | from itertools import product 17 | 18 | from nasimemu import nasim 19 | 20 | LINE_BREAK = "-"*60 21 | 22 | 23 | def run_bruteforce_agent(env, step_limit=1e6, verbose=True): 24 | """Run bruteforce agent on nasim environment. 25 | 26 | Parameters 27 | ---------- 28 | env : nasim.NASimEnv 29 | the nasim environment to run agent on 30 | step_limit : int, optional 31 | the maximum number of steps to run agent for (default=1e6) 32 | verbose : bool, optional 33 | whether to print out progress messages or not (default=True) 34 | 35 | Returns 36 | ------- 37 | int 38 | timesteps agent ran for 39 | float 40 | the total reward recieved by agent 41 | bool 42 | whether the goal was reached or not 43 | """ 44 | if verbose: 45 | print(LINE_BREAK) 46 | print("STARTING EPISODE") 47 | print(LINE_BREAK) 48 | print("t: Reward") 49 | 50 | env.reset() 51 | total_reward = 0 52 | done = False 53 | steps = 0 54 | cycle_complete = False 55 | 56 | if env.flat_actions: 57 | act = 0 58 | else: 59 | act_iter = product(*[range(n) for n in env.action_space.nvec]) 60 | 61 | while not done and steps < step_limit: 62 | if env.flat_actions: 63 | act = (act + 1) % env.action_space.n 64 | cycle_complete = (steps > 0 and act == 0) 65 | else: 66 | try: 67 | act = next(act_iter) 68 | cycle_complete = False 69 | except StopIteration: 70 | act_iter = product(*[range(n) for n in env.action_space.nvec]) 71 | act = next(act_iter) 72 | cycle_complete = True 73 | 74 | _, rew, done, _ = env.step(act) 75 | total_reward += rew 76 | 77 | if cycle_complete and verbose: 78 | print(f"{steps}: {total_reward}") 79 | steps += 1 80 | 81 | if done and verbose: 82 | print(LINE_BREAK) 83 | print("EPISODE FINISHED") 84 | print(LINE_BREAK) 85 | print(f"Goal reached = {env.goal_reached()}") 86 | print(f"Total steps = {steps}") 87 | print(f"Total reward = {total_reward}") 88 | elif verbose: 89 | print(LINE_BREAK) 90 | print("STEP LIMIT REACHED") 91 | print(LINE_BREAK) 92 | 93 | if done: 94 | done = env.goal_reached() 95 | 96 | return steps, total_reward, done 97 | 98 | 99 | if __name__ == "__main__": 100 | import argparse 101 | parser = argparse.ArgumentParser() 102 | parser.add_argument("env_name", type=str, help="benchmark scenario name") 103 | parser.add_argument("-s", "--seed", type=int, default=0, 104 | help="random seed") 105 | parser.add_argument("-o", "--partially_obs", action="store_true", 106 | help="Partially Observable Mode") 107 | parser.add_argument("-p", "--param_actions", action="store_true", 108 | help="Use Parameterised action space") 109 | parser.add_argument("-f", "--box_obs", action="store_true", 110 | help="Use 2D observation space") 111 | args = parser.parse_args() 112 | 113 | nasimenv = nasim.make_benchmark( 114 | args.env_name, 115 | args.seed, 116 | not args.partially_obs, 117 | not args.param_actions, 118 | not args.box_obs 119 | ) 120 | if not args.param_actions: 121 | print(nasimenv.action_space.n) 122 | else: 123 | print(nasimenv.action_space.nvec) 124 | run_bruteforce_agent(nasimenv) 125 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scripts/run_random_benchmarks.py: -------------------------------------------------------------------------------- 1 | """This script runs the random agent for all benchmarks scenarios 2 | 3 | The mean (+/- stdev) steps and reward are reported in table to stdout 4 | (and to optional CSV file) 5 | 6 | Usage 7 | ----- 8 | $ python run_random_benchmarks.py [-n --num_cpus NUM_CPUS] 9 | [-o --output OUTPUT_FILENAME] [-s --num_seeds NUM_SEEDS] 10 | 11 | """ 12 | import os 13 | import numpy as np 14 | import multiprocessing as mp 15 | from prettytable import PrettyTable 16 | 17 | from nasimemu import nasim 18 | from nasimemu.nasim.agents.random_agent import run_random_agent 19 | from nasimemu.nasim.scenarios.benchmark import AVAIL_BENCHMARKS 20 | 21 | 22 | def print_msg(msg): 23 | print(f"[PID={os.getpid()}] {msg}") 24 | 25 | 26 | class Result: 27 | 28 | def __init__(self, name): 29 | self.name = name 30 | self.run_steps = [] 31 | self.run_rewards = [] 32 | 33 | def add(self, steps, reward): 34 | self.run_steps.append(steps) 35 | self.run_rewards.append(reward) 36 | 37 | def summarize(self): 38 | steps_mean = np.mean(self.run_steps) 39 | steps_std = np.std(self.run_steps) 40 | reward_mean = np.mean(self.run_rewards) 41 | reward_std = np.std(self.run_rewards) 42 | return steps_mean, steps_std, reward_mean, reward_std 43 | 44 | def get_formatted_summary(self): 45 | steps_mean, steps_std, reward_mean, reward_std = self.summarize() 46 | return ( 47 | f"{steps_mean:.2f} +/- {steps_std:.2f}", 48 | f"{reward_mean:.2f} +/- {reward_std:.2f}" 49 | ) 50 | 51 | 52 | def run_scenario(args): 53 | scenario_name, seed = args 54 | print_msg(f"Running '{scenario_name}' scenario with seed={seed}") 55 | env = nasim.make_benchmark(scenario_name, seed, False, True, True) 56 | steps, total_reward, done = run_random_agent(env, verbose=False) 57 | return { 58 | "Name": scenario_name, 59 | "Seed": seed, 60 | "Steps": steps, 61 | "Total reward": total_reward 62 | } 63 | 64 | 65 | def collate_results(results): 66 | scenario_results = {} 67 | for res in results: 68 | name = res["Name"] 69 | if name not in scenario_results: 70 | scenario_results[name] = Result(name) 71 | scenario_results[name].add(res["Steps"], res["Total reward"]) 72 | return scenario_results 73 | 74 | 75 | def output_results(results, output=None): 76 | headers = ["Scenario Name", "Steps", "Total Reward"] 77 | rows = [] 78 | for name in AVAIL_BENCHMARKS: 79 | rows.append([ 80 | name, *results[name].get_formatted_summary() 81 | ]) 82 | 83 | table = PrettyTable(headers) 84 | for row in rows: 85 | table.add_row(row) 86 | 87 | if output is not None: 88 | with open(output, "w") as fout: 89 | fout.write(",".join(headers) + "\n") 90 | for row in rows: 91 | fout.write(",".join(row) + "\n") 92 | 93 | 94 | def run_random_benchmark(num_cpus=1, num_seeds=10, output=None): 95 | run_args_list = [] 96 | for name in AVAIL_BENCHMARKS: 97 | for seed in range(num_seeds): 98 | run_args_list.append((name, seed)) 99 | 100 | with mp.Pool(num_cpus) as p: 101 | results = p.map(run_scenario, run_args_list) 102 | 103 | results = collate_results(results) 104 | output_results(results, output) 105 | 106 | 107 | if __name__ == "__main__": 108 | import argparse 109 | parser = argparse.ArgumentParser() 110 | parser.add_argument("-n", "--num_cpus", type=int, default=1, 111 | help="Number of CPUS to use in parallel (default=1)") 112 | parser.add_argument("-o", "--output", type=str, default=None, 113 | help="File name to output as CSV too") 114 | parser.add_argument("-s", "--num_seeds", type=int, default=10, 115 | help=("Number of seeds to run for each scenario" 116 | " (default=10)")) 117 | args = parser.parse_args() 118 | 119 | run_random_benchmark(**vars(args)) 120 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/host.py: -------------------------------------------------------------------------------- 1 | 2 | class Host: 3 | """A single host in the network. 4 | 5 | Note this class is mainly used to store initial scenario data for a host. 6 | The HostVector class is used to store and track the current state of a 7 | host (for efficiency and ease of use reasons). 8 | """ 9 | 10 | def __init__(self, 11 | address, 12 | os, 13 | services, 14 | processes, 15 | firewall, 16 | value=0.0, 17 | discovery_value=0.0, 18 | compromised=False, 19 | reachable=False, 20 | discovered=False, 21 | access=0): 22 | """ 23 | Arguments 24 | --------- 25 | address : (int, int) 26 | address of host as (subnet, id) 27 | os : dict 28 | A os_name: bool dictionary indicating which OS the host is runinng 29 | services : dict 30 | a (service_name, bool) dictionary indicating which services 31 | are present/absent 32 | processes : dict 33 | a (process_name, bool) dictionary indicating which processes are 34 | running on host or not 35 | firewall : dict 36 | a (addr, denied services) dictionary defining which services are 37 | blocked from other hosts in the network. If other host not in 38 | firewall assumes all services allowed 39 | value : float, optional 40 | value of the host (default=0.0) 41 | discovery_value : float, optional 42 | the reward gained for discovering the host (default=0.0) 43 | compromised : bool, optional 44 | whether host has been compromised or not (default=False) 45 | reachable : bool, optional 46 | whether host is reachable by attacker or not (default=False) 47 | discovered : bool, optional 48 | whether host has been reachable discovered by attacker or not 49 | (default=False) 50 | access : int, optional 51 | access level of attacker on host (default=0) 52 | """ 53 | self.address = address 54 | self.os = os 55 | self.services = services 56 | self.processes = processes 57 | self.firewall = firewall 58 | self.value = value 59 | self.discovery_value = discovery_value 60 | self.compromised = compromised 61 | self.reachable = reachable 62 | self.discovered = discovered 63 | self.access = access 64 | 65 | def is_running_service(self, service): 66 | return self.services[service] 67 | 68 | def is_running_os(self, os): 69 | return self.os[os] 70 | 71 | def is_running_process(self, process): 72 | return self.processes[process] 73 | 74 | def traffic_permitted(self, addr, service): 75 | return service not in self.firewall.get(addr, []) 76 | 77 | def __str__(self): 78 | output = ["Host: {"] 79 | output.append(f"\taddress: {self.address}") 80 | output.append(f"\tcompromised: {self.compromised}") 81 | output.append(f"\treachable: {self.reachable}") 82 | output.append(f"\tvalue: {self.value}") 83 | output.append(f"\taccess: {self.access}") 84 | 85 | output.append("\tOS: {") 86 | for os_name, val in self.os.items(): 87 | output.append(f"\t\t{os_name}: {val}") 88 | output.append("\t}") 89 | 90 | output.append("\tservices: {") 91 | for name, val in self.services.items(): 92 | output.append(f"\t\t{name}: {val}") 93 | output.append("\t}") 94 | 95 | output.append("\tprocesses: {") 96 | for name, val in self.processes.items(): 97 | output.append(f"\t\t{name}: {val}") 98 | output.append("\t}") 99 | 100 | output.append("\tfirewall: {") 101 | for addr, val in self.firewall.items(): 102 | output.append(f"\t\t{addr}: {val}") 103 | output.append("\t}") 104 | return "\n".join(output) 105 | 106 | def __repr__(self): 107 | return f"Host: {self.address}" 108 | -------------------------------------------------------------------------------- /scenarios/visualize_scenario.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import plotly.graph_objects as go 3 | import plotly.io as pio 4 | import numpy as np 5 | import argparse, yaml, string, pathlib, math 6 | 7 | def plot(graph, node_data): 8 | edge_x = [] 9 | edge_y = [] 10 | for edge in graph.edges(): 11 | x0, y0 = node_data['x'][edge[0]], node_data['y'][edge[0]] 12 | x1, y1 = node_data['x'][edge[1]], node_data['y'][edge[1]] 13 | edge_x.append(x0) 14 | edge_x.append(x1) 15 | edge_x.append(None) 16 | edge_y.append(y0) 17 | edge_y.append(y1) 18 | edge_y.append(None) 19 | 20 | edge_trace = go.Scatter( 21 | x=edge_x, y=edge_y, 22 | line=dict(width=0.5, color='#444'), 23 | hoverinfo='none', 24 | mode='lines') 25 | 26 | node_trace = go.Scatter( 27 | node_data, 28 | mode='markers+text', 29 | # hoverinfo='text', 30 | # marker=dict(showscale=False, size=15,), 31 | textposition="top center") 32 | 33 | fig = go.Figure(data=[edge_trace, node_trace], 34 | layout=go.Layout( 35 | showlegend=False, 36 | hovermode='closest', 37 | margin=dict(b=0,l=0,r=0,t=0), 38 | xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), 39 | yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)), 40 | ) 41 | 42 | return fig 43 | 44 | def make_graph(scenario): 45 | topology = np.array(scenario['topology']) 46 | 47 | # check topology is symetrical 48 | assert (topology == topology.T).all() 49 | 50 | # check dimensions 51 | assert (topology.shape[0] - 1 == len(scenario['subnets']) == len(scenario['subnet_labels']) == len(scenario['sensitive_hosts'])) 52 | 53 | graph = nx.from_numpy_array(topology) 54 | 55 | node_positions = None 56 | # node_positions = nx.shell_layout(graph) 57 | node_positions = nx.kamada_kawai_layout(graph, pos=node_positions) 58 | node_positions = np.stack([(node_positions[node]) for node in graph.nodes]) 59 | 60 | node_data = {} 61 | node_data['x'] = node_positions[:, 0] 62 | node_data['y'] = node_positions[:, 1] 63 | 64 | subnet_annotation = lambda node_id: f'{string.ascii_uppercase[node_id-1]}: {scenario["subnet_labels"][node_id]}
nodes=[{scenario["subnets"][node_id-1]}], sens={scenario["sensitive_hosts"][node_id]}' 65 | node_color = lambda node_id: f'rgb({scenario["sensitive_hosts"][node_id] * 191 + 64}, 64, 64)' 66 | 67 | def node_size(node_id): 68 | sub = scenario["subnets"][node_id-1] 69 | if type(sub) is str: 70 | sub_size = int(scenario["subnets"][node_id-1].split('-')[-1]) 71 | else: 72 | sub_size = sub 73 | 74 | return math.sqrt(sub_size) * 10 75 | 76 | node_data['text'] = ['Attacker' if node_id == 0 else subnet_annotation(node_id) for node_id in graph.nodes] 77 | node_data['marker'] = dict(opacity=1.0, size=[15 if node_id == 0 else node_size(node_id) for node_id in graph.nodes], color=['orange' if node_id == 0 else node_color(node_id) for node_id in graph.nodes]) 78 | 79 | return graph, node_data 80 | 81 | if __name__ == '__main__': 82 | parser = argparse.ArgumentParser() 83 | parser.add_argument('scenario', type=str, help='v2.yaml scenario file') 84 | cmd_args = parser.parse_args() 85 | 86 | # load yaml 87 | with open(cmd_args.scenario, 'r') as stream: 88 | scenario = yaml.safe_load(stream) 89 | 90 | graph, node_data = make_graph(scenario) 91 | fig = plot(graph, node_data) 92 | 93 | fig.update_layout( 94 | paper_bgcolor="rgba(0,0,0,0)", 95 | plot_bgcolor="rgba(0,0,0,0)", 96 | font=dict(size=8) 97 | ) 98 | 99 | # fig.show() 100 | 101 | fig.update_xaxes(range=[-1.1, 1.1]) 102 | fig.update_yaxes(range=[-1.1, 1.1]) 103 | 104 | pdf_file = pathlib.Path(cmd_args.scenario).with_suffix('.pdf') 105 | print(f'Exporting to "{pdf_file}"') 106 | 107 | pio.kaleido.scope.mathjax = None 108 | fig.write_image(pdf_file, width=500, height=500, scale=1.0) 109 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/envs/utils.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import numpy as np 3 | from queue import deque 4 | from itertools import permutations 5 | 6 | INTERNET = 0 7 | 8 | 9 | class OneHotBool(enum.IntEnum): 10 | NONE = 0 11 | TRUE = 1 12 | FALSE = 2 13 | 14 | @staticmethod 15 | def from_bool(b): 16 | if b: 17 | return OneHotBool.TRUE 18 | return OneHotBool.FALSE 19 | 20 | def __str__(self): 21 | return self.name 22 | 23 | def __repr__(self): 24 | return self.name 25 | 26 | 27 | class ServiceState(enum.IntEnum): 28 | # values for possible service knowledge states 29 | UNKNOWN = 0 # service may or may not be running on host 30 | PRESENT = 1 # service is running on the host 31 | ABSENT = 2 # service not running on the host 32 | 33 | def __str__(self): 34 | return self.name 35 | 36 | def __repr__(self): 37 | return self.name 38 | 39 | 40 | class AccessLevel(enum.IntEnum): 41 | NONE = 0 42 | USER = 1 43 | ROOT = 2 44 | 45 | def __str__(self): 46 | return self.name 47 | 48 | def __repr__(self): 49 | return self.name 50 | 51 | 52 | def get_minimal_steps_to_goal(topology, sensitive_addresses): 53 | """Get the minimum total number of steps required to reach all sensitive 54 | hosts in the network starting from outside the network (i.e. can only 55 | reach exposed subnets). 56 | 57 | Returns 58 | ------- 59 | int 60 | minimum number of steps to reach all sensitive hosts 61 | """ 62 | num_subnets = len(topology) 63 | max_value = np.iinfo(np.int16).max 64 | distance = np.full((num_subnets, num_subnets), 65 | max_value, 66 | dtype=np.int16) 67 | 68 | # set distances for each edge to 1 69 | for s1 in range(num_subnets): 70 | for s2 in range(num_subnets): 71 | if s1 == s2: 72 | distance[s1][s2] = 0 73 | elif topology[s1][s2] == 1: 74 | distance[s1][s2] = 1 75 | # find all pair minimum shortest path distance 76 | for k in range(num_subnets): 77 | for i in range(num_subnets): 78 | for j in range(num_subnets): 79 | if distance[i][k] == max_value \ 80 | or distance[k][j] == max_value: 81 | dis = max_value 82 | else: 83 | dis = distance[i][k] + distance[k][j] 84 | if distance[i][j] > dis: 85 | distance[i][j] = distance[i][k] + distance[k][j] 86 | 87 | # get list of all subnets we need to visit 88 | subnets_to_visit = [INTERNET] 89 | for subnet, host in sensitive_addresses: 90 | if subnet not in subnets_to_visit: 91 | subnets_to_visit.append(subnet) 92 | 93 | # find minimum shortest path that visits internet subnet and all 94 | # sensitive subnets by checking all possible permutations 95 | shortest = max_value 96 | for pm in permutations(subnets_to_visit): 97 | pm_sum = 0 98 | for i in range(len(pm) - 1): 99 | pm_sum += distance[pm[i]][pm[i+1]] 100 | shortest = min(shortest, pm_sum) 101 | 102 | return shortest 103 | 104 | 105 | def min_subnet_depth(topology): 106 | """Find the minumum depth of each subnet in the network graph in terms of steps 107 | from an exposed subnet to each subnet 108 | 109 | Parameters 110 | ---------- 111 | topology : 2D matrix 112 | An adjacency matrix representing the network, with first subnet 113 | representing the internet (i.e. exposed) 114 | 115 | Returns 116 | ------- 117 | depths : list 118 | depth of each subnet ordered by subnet index in topology 119 | """ 120 | num_subnets = len(topology) 121 | 122 | assert len(topology[0]) == num_subnets 123 | 124 | depths = [] 125 | Q = deque() 126 | for subnet in range(num_subnets): 127 | if topology[subnet][INTERNET] == 1: 128 | depths.append(0) 129 | Q.appendleft(subnet) 130 | else: 131 | depths.append(float('inf')) 132 | 133 | while len(Q) > 0: 134 | parent = Q.pop() 135 | for child in range(num_subnets): 136 | if topology[parent][child] == 1: 137 | # child is connected to parent 138 | if depths[child] > depths[parent] + 1: 139 | depths[child] = depths[parent] + 1 140 | Q.appendleft(child) 141 | return depths 142 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/benchmark/medium-multi-site.yaml: -------------------------------------------------------------------------------- 1 | # A WAN which has multiple 3 remote sites (subnets) connected to the main site 2 | # sensitive hosts: 3 | # 1) a server in server subnet on the main site, 4 | # 2) a host in user subnet in main site 5 | # 6 | # main site has 3 subnets (1 server, 1 DMZ, 1 user) 7 | # subnet 1 = main site DMZ (exposed, but not vulnerable) - contains 2 webservers 8 | # subnet 2 = main site server (not exposed) - contains 2 data servers 9 | # subnet 3 = main site user (not exposed) - contains 6 user hosts 10 | # subnet 4 = remote site 1 (exposed) - contains 2 user hosts 11 | # subnet 5 = remote site 2 (exposed) - contains 2 user hosts 12 | # subnet 6 = remote site 3 (exposed) - contains 2 user hosts 13 | # each remote site is connected to main site server subnet 14 | # 15 | # 16 hosts 16 | # 6 subnets 17 | # 2 OS 18 | # 5 services 19 | # 3 processes 20 | # 5 exploits 21 | # 3 priv esc 22 | # 23 | # |A| = 16 * (5 + 3 + 4) = 192 24 | # 25 | # Optimal path: 26 | # (e_samba, (6, 1)) -> (subnet_scan, (6, 1)) -> (e_smtp, (2, 1)) -> (pe_schtask, (2, 1)) 27 | # -> (e_http, (3, 1)) -> (e_ssh, (3, 4)) -> (pe_tomcat, (3, 4)) 28 | # Score = 200 - (2 + 3 + 2 + 3) = 190 29 | # 30 | subnets: [2, 2, 6, 2, 2, 2] 31 | topology: [[ 1, 1, 0, 0, 1, 1, 1], # 0 - internet 32 | [ 1, 1, 1, 1, 0, 0, 0], # 1 - MS-DMZ 33 | [ 0, 1, 1, 1, 1, 1, 1], # 2 - MS-Server 34 | [ 0, 1, 1, 1, 0, 0, 0], # 3 - MS-User 35 | [ 1, 0, 1, 0, 1, 0, 0], # 4 - RS-1 36 | [ 1, 0, 1, 0, 0, 1, 0], # 5 - RS-2 37 | [ 1, 0, 1, 0, 0, 0, 1]] # 6 - RS-3 38 | sensitive_hosts: 39 | (2, 1): 100 40 | (3, 4): 100 41 | os: 42 | - linux 43 | - windows 44 | services: 45 | - ssh 46 | - ftp 47 | - http 48 | - samba 49 | - smtp 50 | processes: 51 | - tomcat 52 | - daclsvc 53 | - schtask 54 | exploits: 55 | e_ssh: 56 | service: ssh 57 | os: linux 58 | prob: 0.9 59 | cost: 3 60 | access: user 61 | e_ftp: 62 | service: ftp 63 | os: windows 64 | prob: 0.6 65 | cost: 1 66 | access: root 67 | e_http: 68 | service: http 69 | os: None 70 | prob: 0.9 71 | cost: 2 72 | access: user 73 | e_samba: 74 | service: samba 75 | os: linux 76 | prob: 0.3 77 | cost: 2 78 | access: root 79 | e_smtp: 80 | service: smtp 81 | os: windows 82 | prob: 0.6 83 | cost: 3 84 | access: user 85 | privilege_escalation: 86 | pe_tomcat: 87 | process: tomcat 88 | os: linux 89 | prob: 1.0 90 | cost: 1 91 | access: root 92 | pe_daclsvc: 93 | process: daclsvc 94 | os: windows 95 | prob: 1.0 96 | cost: 1 97 | access: root 98 | pe_schtask: 99 | process: schtask 100 | os: windows 101 | prob: 1.0 102 | cost: 1 103 | access: root 104 | service_scan_cost: 1 105 | os_scan_cost: 1 106 | subnet_scan_cost: 1 107 | process_scan_cost: 1 108 | host_configurations: 109 | (1, 0): 110 | os: linux 111 | services: [ssh] 112 | processes: [tomcat] 113 | (1, 1): 114 | os: linux 115 | services: [ssh] 116 | processes: [tomcat] 117 | (2, 0): 118 | os: windows 119 | services: [smtp] 120 | processes: [] 121 | (2, 1): 122 | os: windows 123 | services: [smtp] 124 | processes: [schtask] 125 | (3, 0): 126 | os: linux 127 | services: [ssh] 128 | processes: [tomcat] 129 | (3, 1): 130 | os: linux 131 | services: [ssh, http] 132 | processes: [] 133 | (3, 2): 134 | os: linux 135 | services: [ssh] 136 | processes: [] 137 | (3, 3): 138 | os: linux 139 | services: [ssh] 140 | processes: [] 141 | (3, 4): 142 | os: linux 143 | services: [ssh] 144 | processes: [tomcat] 145 | (3, 5): 146 | os: linux 147 | services: [ssh] 148 | processes: [] 149 | (4, 0): 150 | os: windows 151 | services: [ftp] 152 | processes: [daclsvc] 153 | (4, 1): 154 | os: windows 155 | services: [ftp] 156 | processes: [daclsvc] 157 | (5, 0): 158 | os: windows 159 | services: [ftp] 160 | processes: [daclsvc, schtask] 161 | (5, 1): 162 | os: windows 163 | services: [ftp, http] 164 | processes: [] 165 | (6, 0): 166 | os: linux 167 | services: [ssh] 168 | processes: [tomcat] 169 | (6, 1): 170 | os: windows 171 | services: [ssh, samba] 172 | processes: [] 173 | firewall: 174 | (0, 1): [] 175 | (1, 0): [] 176 | (0, 4): [] 177 | (4, 0): [] 178 | (0, 5): [http] 179 | (5, 0): [] 180 | (0, 6): [samba] 181 | (6, 0): [] 182 | (1, 2): [] 183 | (2, 1): [ssh] 184 | (1, 3): [] 185 | (3, 1): [ssh] 186 | (2, 3): [http] 187 | (3, 2): [smtp] 188 | (2, 4): [ftp] 189 | (4, 2): [smtp] 190 | (2, 5): [ftp] 191 | (5, 2): [smtp] 192 | (2, 6): [ftp, ssh] 193 | (6, 2): [smtp] 194 | step_limit: 2000 195 | -------------------------------------------------------------------------------- /docs/EMULATION.md: -------------------------------------------------------------------------------- 1 | # NASimEmu Emulation 2 | 3 | ## Installation 4 | To use the emulation, you should install [Vagrant](https://www.vagrantup.com/downloads) and [VirtualBox](https://www.virtualbox.org/wiki/Linux_Downloads) 5 | 6 | *Warning!* Install Vagrant from the official site: https://www.vagrantup.com/downloads (ver >= 2.2.19). Do not use obsolete packages in your distribution. 7 | 8 | Also, install these plugins: 9 | ``` 10 | vagrant plugin install winrm 11 | vagrant plugin install winrm-fs 12 | vagrant plugin install winrm-elevated 13 | ``` 14 | 15 | ## Setting vagrant up 16 | First, Vagrant needs to be configured with the chosen scenario: 17 | ```./setup_vagrant ``` 18 | This command which will create `vagrant/Vagrantfile` and `vagrant/firewall.rsc` files. 19 | 20 | Next, go the the `vagrant` folder and start Vagrant: 21 | ``` 22 | cd vagrant 23 | vagrant status 24 | vagrant up 25 | ``` 26 | 27 | The command will download corresponding virtual images and run the virtual network. The download is slow, but is done only once. There is a special host `attacker`, which runs Kali linux with metasploit framework and a remote interface `msfrcpd`. Also, a node `router` is the network's router and runs RouterOS (note that without license, the router stops working in 24 hours and needs to be reset). 28 | 29 | You can login into individual hosts with `vagrant ssh attacker`, or `vagrant ssh target10` (in scenario, it is the host in subnet `1` and host id `0`). The default password for the `vagrant` user is `vagrant`. 30 | 31 | ## Resetting emulation 32 | If you need to reset the emulation, run `vagrant destroy -f`, followed by `vagrant up`. 33 | 34 | ## Running NASimEmu 35 | By default, the Vagrant is configured to forward the `msfrcpd` to your localhost on port `55553`, which is also configured by default in `nasimemu.msf_interface`. Then, when creating an environment, use `emulation=True`, such as: 36 | ``` 37 | env = gym.make('NASimEmu-v0', emulate=True, scenario_name='...') 38 | ``` 39 | In this case, the provided scenario will be used inform the agent about available exploits, privescs and `address_space_bounds`. The actual topology and host configuration is given by the emulation. 40 | 41 | ## Other details 42 | ### How the emulation works 43 | When the agent issues an action to NASimEmuEnv, it is forwarded either to nasim (simulation), or `EmulatedNASimEnv` (emulation). Then it goes to `EmulatedNetwork`, then to `MsfClient`, where it is finally forwarded as a metaspoit action to the `attacker` node. The result of the action is translated back to the nasim observation format. To see the current list of exploits that can be used, see [SERVICES](/docs/SERVICES.md). 44 | 45 | ### Running emulation on a remote machine 46 | You can run the vagrant on a remote machine. If you use localhost for performing experiments, forward the port `55553` to your localhost, like this: 47 | ```ssh -L 55553:127.0.0.1:55553 remote-machine -N &``` 48 | 49 | ### Pivoting 50 | The attacker machine often does not see the other hosts directly. To overcome this issue, NASimEmu uses automatic pivoting and bind payloads. 51 | 52 | ### Sensitive hosts 53 | Sensitive hosts contain a *loot*, represented as a specific file on the host with limited permissions. On linux, the file is `/home/kylo_ren/loot`, owned by the user 'kylo_ren' and permission `600`. On windows, the file is `C:/loot`, owned by the user 'kylo_ren' and other users have access denied. 54 | 55 | If the attacker manages to read the content of the file, the loot is considered obtained. 56 | 57 | ### Networking 58 | The network consists of `192.168.*.*/24` subnets, where subnet `0` is the attacker (internet) subnet. 59 | 60 | There is node called router, which is a routeros instance, sitting at `192.168.*.10`. It also filters the traffic, according its firewall rules. 61 | 62 | Network hosts use `192.168.{subnet_id}.{host_id+100}` address and are configured to route its traffic via router, if it's destined to different subnets. Traffic inside a subnet is unfiltered. 63 | 64 | ### Network debugging 65 | You can login to the router `vagrant ssh router` and try `/tool sniffer quick interface=ether3` to see the traffic that goes through the router. 66 | 67 | Alternatively you can also login to a specific machine and try `sudo tcpdump -i eth1`. 68 | 69 | Currently, all the machines are on **the same interface**, which is only virtually partitioned to different IP segments by router firewall. However different protocols, such as ARP ignore this partitioning, hence the machines can see each other via ARP. This makes ARP scanning not useful in our abstraction. 70 | 71 | ### Troubleshooting 72 | Sometimes, bringing the `router` up fails and it is necessary to remove VirtualBox's dhcpservers like this: 73 | ``` 74 | VBoxManage list dhcpservers 75 | VBoxManage dhcpserver remove --netname HostInterfaceNetworking-vboxnet5 # substitute the correct interface 76 | ``` 77 | 78 | If you leave the network up for longer than 24 hours, the router needs to be reset (because it runs without a license). Use: 79 | ``` 80 | vagrant destroy -f router 81 | vagrant up router 82 | ``` 83 | 84 | Sometimes, the `msfrcpd` does not respond properly. In that case, log in to the `attacker` machine and restart it. 85 | ``` 86 | vagrant ssh attacker 87 | ps x # find the msfrcpd PID 88 | kill 89 | msfrpcd -P msfpassword 90 | ``` -------------------------------------------------------------------------------- /src/nasimemu/nasim/scenarios/benchmark/generated.py: -------------------------------------------------------------------------------- 1 | """A collection of definitions for generated benchmark scenarios. 2 | 3 | Each generated scenario is defined by the a number of parameters that 4 | control the size of the problem (see scenario.generator for more info): 5 | 6 | There are also some parameters, where default values are used for all 7 | scenarios, see DEFAULTS dict. 8 | """ 9 | 10 | # generated environment constants 11 | DEFAULTS = dict( 12 | num_exploits=None, 13 | num_privescs=None, 14 | r_sensitive=100, 15 | r_user=100, 16 | exploit_cost=1, 17 | exploit_probs='mixed', 18 | privesc_cost=1, 19 | privesc_probs=1.0, 20 | service_scan_cost=1, 21 | os_scan_cost=1, 22 | subnet_scan_cost=1, 23 | process_scan_cost=1, 24 | uniform=False, 25 | alpha_H=2.0, 26 | alpha_V=2.0, 27 | lambda_V=1.0, 28 | random_goal=False, 29 | base_host_value=1, 30 | host_discovery_value=1, 31 | step_limit=1000 32 | ) 33 | 34 | # Generated Scenario definitions 35 | TINY_GEN = {**DEFAULTS, 36 | "name": "tiny-gen", 37 | "num_hosts": 3, 38 | "num_os": 1, 39 | "num_services": 1, 40 | "num_processes": 1, 41 | "restrictiveness": 1} 42 | TINY_GEN_RGOAL = {**DEFAULTS, 43 | "name": "tiny-gen-rgoal", 44 | "num_hosts": 3, 45 | "num_os": 1, 46 | "num_services": 1, 47 | "num_processes": 1, 48 | "restrictiveness": 1, 49 | "random_goal": True} 50 | SMALL_GEN = {**DEFAULTS, 51 | "name": "small-gen", 52 | "num_hosts": 8, 53 | "num_os": 2, 54 | "num_services": 3, 55 | "num_processes": 2, 56 | "restrictiveness": 2} 57 | SMALL_GEN_RGOAL = {**DEFAULTS, 58 | "name": "small-gen-rgoal", 59 | "num_hosts": 8, 60 | "num_os": 2, 61 | "num_services": 3, 62 | "num_processes": 2, 63 | "restrictiveness": 2, 64 | "random_goal": True} 65 | MEDIUM_GEN = {**DEFAULTS, 66 | "name": "medium-gen", 67 | "num_hosts": 16, 68 | "num_os": 2, 69 | "num_services": 5, 70 | "num_processes": 2, 71 | "restrictiveness": 3, 72 | "step_limit": 2000} 73 | LARGE_GEN = {**DEFAULTS, 74 | "name": "large-gen", 75 | "num_hosts": 23, 76 | "num_os": 3, 77 | "num_services": 7, 78 | "num_processes": 3, 79 | "restrictiveness": 3, 80 | "step_limit": 5000} 81 | HUGE_GEN = {**DEFAULTS, 82 | "name": "huge-gen", 83 | "num_hosts": 38, 84 | "num_os": 4, 85 | "num_services": 10, 86 | "num_processes": 4, 87 | "restrictiveness": 3, 88 | "step_limit": 10000} 89 | 90 | POCP_1_GEN = {**DEFAULTS, 91 | "name": "pocp-1-gen", 92 | "num_hosts": 35, 93 | "num_os": 2, 94 | "num_services": 50, 95 | "num_exploits": 60, 96 | "num_processes": 2, 97 | "restrictiveness": 5, 98 | "step_limit": 30000} 99 | POCP_2_GEN = {**DEFAULTS, 100 | "name": "pocp-2-gen", 101 | "num_hosts": 95, 102 | "num_os": 3, 103 | "num_services": 10, 104 | "num_exploits": 30, 105 | "num_processes": 3, 106 | "restrictiveness": 5, 107 | "step_limit": 30000} 108 | 109 | # added 110 | MEDIUM_GEN_RGOAL = {**DEFAULTS, 111 | "name": "medium-gen-rgoal", 112 | "num_hosts": 16, 113 | "num_os": 2, 114 | "num_services": 5, 115 | "num_processes": 2, 116 | "restrictiveness": 3, 117 | "step_limit": 2000, # not used 118 | "random_goal": True, 119 | "address_space_bounds": (12, 6)} 120 | 121 | # this is the scenario used in dissertation Janisch 2024 122 | HUGE_GEN_RGOAL_STOCH = {**DEFAULTS, 123 | "name": "huge-gen-rgoal", 124 | "num_hosts": 38, 125 | "num_os": 4, 126 | "num_services": 10, 127 | "num_processes": 4, 128 | "restrictiveness": 3, 129 | "base_host_value": 0., 130 | "host_discovery_value": 0., 131 | "privesc_probs": 0.8, 132 | "exploit_probs": 0.8, 133 | "random_goal": True, 134 | "step_limit": 10000, # not used -- disabled in env.py `scenario_params['step_limit'] = None` 135 | "address_space_bounds": (12, 6)} # subnet sizes are randomly in [4-6] 136 | 137 | AVAIL_GEN_BENCHMARKS = { 138 | "tiny-gen": TINY_GEN, 139 | "tiny-gen-rgoal": TINY_GEN_RGOAL, 140 | "small-gen": SMALL_GEN, 141 | "small-gen-rgoal": SMALL_GEN_RGOAL, 142 | "medium-gen-rgoal": MEDIUM_GEN_RGOAL, 143 | "large-gen": LARGE_GEN, 144 | "huge-gen": HUGE_GEN, 145 | "huge-gen-rgoal-stoch": HUGE_GEN_RGOAL_STOCH, 146 | "pocp-1-gen": POCP_1_GEN, 147 | "pocp-2-gen": POCP_2_GEN 148 | } 149 | -------------------------------------------------------------------------------- /docs/SCENARIOS.md: -------------------------------------------------------------------------------- 1 | To set a custom environment or to use emulation, you have custom scenarios that describe the environment. 2 | 3 | # Version 4 | 5 | NASimEmu can use 2 versions of scenario: 6 | - v1 : filename ends with .yaml (original NASim format with a few improvements) 7 | - v2 : filename ends with .v2.yaml (custom format, not compatible with NASim) 8 | 9 | The V2 format allows to have some variables to be random. 10 | 11 | # V1 Scenario 12 | See the [original documentation](https://networkattacksimulator.readthedocs.io/en/latest/tutorials/scenarios.html) for the V1 scenarios. 13 | 14 | You can set : 15 | - subnet 16 | - A list containing the number of host in each subnet 17 | - For example, [2, 1, 5] means there is 2 hosts in subnet (1, X), 1 in (2, X) and 5 in (3, X) 18 | - topology 19 | - Define which subnet are connected together 20 | - Also, the topology should always be symmetrical (topology[x][y] = topology[y][x]) 21 | - For example, topology[2][3] = 1 means that subnet (2, X) and (3, X) are connected. 22 | - sensitive hosts 23 | - Set the value of hosts 24 | - os 25 | - List of the os name 26 | - services 27 | - List of the service name 28 | - processes 29 | - List of the processes name 30 | - exploits 31 | - List of the exploits 32 | - privilege escalations 33 | - List of the privilege escalations 34 | - service scan cost 35 | - The cost of service scan (int) 36 | - os scan cost 37 | - The cost of os scan (int) 38 | - subnet scan cost 39 | - The cost of subnet scan (int) 40 | - process scan cost 41 | - The cost of process scan (int) 42 | - host configuration 43 | - List of the host with their configuration 44 | - firewall 45 | - Set services that can communicate between 2 subnet with a list of service or [_all] to allow all services 46 | 47 | ## Example 48 | 49 | ```yaml 50 | subnets: [1] 51 | topology: [[ 1, 1], 52 | [ 1, 1]] 53 | sensitive_hosts: 54 | (1, 0): 100 55 | os: 56 | - linux 57 | services: 58 | - 21_proftpd 59 | - 80_drupal 60 | processes: 61 | - ~ 62 | exploits: 63 | e_21_proftpd: 64 | service: 21_proftpd 65 | os: linux 66 | prob: 1.0 67 | cost: 1 68 | access: user 69 | e_drupal: 70 | service: 80_drupal 71 | os: linux 72 | prob: 1.0 73 | cost: 1 74 | access: user 75 | privilege_escalation: 76 | pe_kernel: 77 | process: ~ 78 | os: linux 79 | prob: 1.0 80 | cost: 1 81 | access: root 82 | service_scan_cost: 1 83 | os_scan_cost: 1 84 | subnet_scan_cost: 1 85 | process_scan_cost: 1 86 | host_configurations: 87 | (1, 0): 88 | os: linux 89 | services: [80_phpwiki] 90 | processes: [] 91 | firewall: 92 | (0, 1): [_all] 93 | (1, 0): [_all] 94 | step_limit: 1000 95 | ``` 96 | 97 | # V2 Scenario 98 | 99 | V2 scenario is an upgrade of V1 scenario adding some randomness to the generation. 100 | 101 | With this scenario, subnet can have variable size by setting the minimum and maximum number of hosts in the 'subnets' field (for example, 1-5 for 1 to 5 hosts). 102 | 103 | With `address_space_bounds`, you set the max number of subnet and the max number of hosts per subnet. This is crucial if you run the agent in multiple scenarios at once. Use the maximum possible values for all scenarios to enforce the same vector size per host. 104 | 105 | Sensitive hosts (hosts with value) are calculated differently. You set the percent of sensitive hosts per subnet. 106 | 107 | You can set `firewall: _subnets` to set the firewall to allow all services according to the network topology. 108 | 109 | You can set `host_configurations: _random` to generate random host configurations. 110 | 111 | The field `sensitive_services` defines services which will **be installed on every sentitive hosts**. However, there is also 10% chance that this service will be installed in a non-sensitive host. 112 | 113 | ## Example 114 | 115 | 116 | ```yaml 117 | subnets: [1, 3-8] # ranges of hosts in subnets 118 | # address_space_bounds: (2, 8) # max number of subnets & hosts in a subnet 119 | address_space_bounds: (5, 10) # fix for the experiment - all scenarios have to have the same bounds 120 | topology: [[ 1, 1, 0], 121 | [ 1, 1, 1], 122 | [ 0, 1, 1]] 123 | sensitive_hosts: # probabilities of sensitive hosts in specific subnets 124 | 1: 0. # DMZ 125 | 2: 0. # user subnet 126 | os: 127 | - linux 128 | - windows 129 | services: 130 | - 21_linux_proftpd 131 | - 80_linux_drupal 132 | - 80_linux_phpwiki 133 | - 9200_windows_elasticsearch 134 | - 80_windows_wp_ninja 135 | - 3306_any_mysql 136 | sensitive_services: 137 | - 3306_any_mysql 138 | processes: 139 | - ~ 140 | exploits: 141 | e_proftpd: 142 | service: 21_linux_proftpd 143 | os: linux 144 | prob: 1.0 145 | cost: 1 146 | access: user 147 | e_drupal: 148 | service: 80_linux_drupal 149 | os: linux 150 | prob: 1.0 151 | cost: 1 152 | access: user 153 | e_phpwiki: 154 | service: 80_linux_phpwiki 155 | os: linux 156 | prob: 1.0 157 | cost: 1 158 | access: user 159 | e_elasticsearch: 160 | service: 9200_windows_elasticsearch 161 | os: windows 162 | prob: 1.0 163 | cost: 1 164 | access: root 165 | e_wp_ninja: 166 | service: 80_windows_wp_ninja 167 | os: windows 168 | prob: 1.0 169 | cost: 1 170 | access: user 171 | privilege_escalation: 172 | pe_kernel: 173 | process: ~ 174 | os: linux 175 | prob: 1.0 176 | cost: 1 177 | access: root 178 | service_scan_cost: 1 179 | os_scan_cost: 1 180 | subnet_scan_cost: 1 181 | process_scan_cost: 1 182 | host_configurations: _random 183 | firewall: _subnets 184 | ``` -------------------------------------------------------------------------------- /src/nasimemu/nasim/__init__.py: -------------------------------------------------------------------------------- 1 | import gym 2 | from gym.envs.registration import register 3 | 4 | from nasimemu.nasim.envs import NASimEnv 5 | from nasimemu.nasim.scenarios.benchmark import AVAIL_BENCHMARKS 6 | from nasimemu.nasim.scenarios import \ 7 | make_benchmark_scenario, load_scenario, generate_scenario 8 | 9 | 10 | __all__ = ['make_benchmark', 'load', 'generate'] 11 | 12 | def make_benchmark(scenario_name, 13 | seed=None, 14 | fully_obs=False, 15 | flat_actions=True, 16 | flat_obs=True): 17 | """Make a new benchmark NASim environment. 18 | 19 | Parameters 20 | ---------- 21 | scenario_name : str 22 | the name of the benchmark environment 23 | seed : int, optional 24 | random seed to use to generate environment (default=None) 25 | fully_obs : bool, optional 26 | the observability mode of environment, if True then uses fully 27 | observable mode, otherwise partially observable (default=False) 28 | flat_actions : bool, optional 29 | if true then uses a flat action space, otherwise will use 30 | parameterised action space (default=True). 31 | flat_obs : bool, optional 32 | if true then uses a 1D observation space. If False 33 | will use a 2D observation space (default=True) 34 | 35 | Returns 36 | ------- 37 | NASimEnv 38 | a new environment instance 39 | 40 | Raises 41 | ------ 42 | NotImplementederror 43 | if scenario_name does no match any implemented benchmark scenarios. 44 | """ 45 | env_kwargs = {"fully_obs": fully_obs, 46 | "flat_actions": flat_actions, 47 | "flat_obs": flat_obs} 48 | scenario = make_benchmark_scenario(scenario_name, seed) 49 | return NASimEnv(scenario, **env_kwargs) 50 | 51 | 52 | def load(path, 53 | fully_obs=False, 54 | flat_actions=True, 55 | flat_obs=True, 56 | name=None): 57 | """Load NASim Environment from a .yaml scenario file. 58 | 59 | Parameters 60 | ---------- 61 | path : str 62 | path to the .yaml scenario file 63 | fully_obs : bool, optional 64 | The observability mode of environment, if True then uses fully 65 | observable mode, otherwise partially observable (default=False) 66 | flat_actions : bool, optional 67 | if true then uses a flat action space, otherwise will use 68 | parameterised action space (default=True). 69 | flat_obs : bool, optional 70 | if true then uses a 1D observation space. If False 71 | will use a 2D observation space (default=True) 72 | name : str, optional 73 | the scenarios name, if None name will be generated from path 74 | (default=None) 75 | 76 | Returns 77 | ------- 78 | NASimEnv 79 | a new environment object 80 | """ 81 | env_kwargs = {"fully_obs": fully_obs, 82 | "flat_actions": flat_actions, 83 | "flat_obs": flat_obs} 84 | scenario = load_scenario(path, name=name) 85 | return NASimEnv(scenario, **env_kwargs) 86 | 87 | 88 | def generate(num_hosts, 89 | num_services, 90 | fully_obs=False, 91 | flat_actions=True, 92 | flat_obs=True, 93 | **params): 94 | """Construct Environment from an auto generated network. 95 | 96 | Parameters 97 | ---------- 98 | num_hosts : int 99 | number of hosts to include in network (minimum is 3) 100 | num_services : int 101 | number of services to use in environment (minimum is 1) 102 | fully_obs : bool, optional 103 | The observability mode of environment, if True then uses fully 104 | observable mode, otherwise partially observable (default=False) 105 | flat_actions : bool, optional 106 | if true then uses a flat action space, otherwise will use 107 | parameterised action space (default=True). 108 | flat_obs : bool, optional 109 | if true then uses a 1D observation space. If False 110 | will use a 2D observation space (default=True) 111 | params : dict, optional 112 | generator params (see :class:`ScenarioGenertor` for full list) 113 | 114 | Returns 115 | ------- 116 | NASimEnv 117 | a new environment object 118 | """ 119 | env_kwargs = {"fully_obs": fully_obs, 120 | "flat_actions": flat_actions, 121 | "flat_obs": flat_obs} 122 | scenario = generate_scenario(num_hosts, num_services, **params) 123 | return NASimEnv(scenario, **env_kwargs) 124 | 125 | 126 | # Register NASimEnv with OpenAI gym 127 | def _register(id, entry_point, kwargs, nondeterministic, force=True): 128 | """Registers NASim Open AI Gym Environment. 129 | 130 | Handles issues with re-registering gym environments. 131 | """ 132 | env_specs = gym.envs.registry.env_specs 133 | if id in env_specs.keys(): 134 | if not force: 135 | return 136 | del env_specs[id] 137 | register( 138 | id=id, 139 | entry_point=entry_point, 140 | kwargs=kwargs, 141 | nondeterministic=nondeterministic 142 | ) 143 | 144 | 145 | for benchmark in AVAIL_BENCHMARKS: 146 | # v0 - flat_actions, flat_obs 147 | # v1 - flat_actions, 2D_obs 148 | # v2 - param_actions, flat obs 149 | # v3 - param_actions, 2D obs 150 | # tiny should yield Tiny and tiny-small should yield TinySmall 151 | for fully_obs in [True, False]: 152 | name = ''.join([g.capitalize() for g in benchmark.split("-")]) 153 | if not fully_obs: 154 | name = f"{name}-PO" 155 | 156 | _register( 157 | id=f"{name}-v0", 158 | entry_point='nasim.envs:NASimGymEnv', 159 | kwargs={ 160 | "scenario": benchmark, 161 | "fully_obs": fully_obs, 162 | "flat_actions": True, 163 | "flat_obs": True 164 | }, 165 | nondeterministic=True 166 | ) 167 | 168 | _register( 169 | id=f"{name}-v1", 170 | entry_point='nasim.envs:NASimGymEnv', 171 | kwargs={ 172 | "scenario": benchmark, 173 | "fully_obs": fully_obs, 174 | "flat_actions": True, 175 | "flat_obs": False 176 | }, 177 | nondeterministic=True 178 | ) 179 | 180 | _register( 181 | id=f"{name}-v2", 182 | entry_point='nasim.envs:NASimGymEnv', 183 | kwargs={ 184 | "scenario": benchmark, 185 | "fully_obs": fully_obs, 186 | "flat_actions": False, 187 | "flat_obs": True 188 | }, 189 | nondeterministic=True 190 | ) 191 | 192 | _register( 193 | id=f"{name}-v3", 194 | entry_point='nasim.envs:NASimGymEnv', 195 | kwargs={ 196 | "scenario": benchmark, 197 | "fully_obs": fully_obs, 198 | "flat_actions": False, 199 | "flat_obs": False 200 | }, 201 | nondeterministic=True 202 | ) 203 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/envs/observation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from .utils import AccessLevel 4 | from .host_vector import HostVector 5 | 6 | 7 | class Observation: 8 | """An observation for NASim. 9 | 10 | Each observation is a 2D tensor with a row for each host and an additional 11 | row containing auxiliary observations. Each host row is a host_vector (for 12 | details see :class:`HostVector`) while the auxiliary 13 | row contains non-host specific observations (see Notes section). 14 | 15 | ... 16 | 17 | Attributes 18 | ---------- 19 | obs_shape : (int, int) 20 | the shape of the observation 21 | aux_row : int 22 | the row index for the auxiliary row 23 | tensor : numpy.ndarray 24 | 2D Numpy array storing the observation 25 | 26 | Notes 27 | ----- 28 | The auxiliary row is the final row in the observation tensor and has the 29 | following features (in order): 30 | 31 | 1. Action success - True (1) or False (0) 32 | indicates whether the action succeeded or failed 33 | 2. Connection error - True (1) or False (0) 34 | indicates whether there was a connection error or not 35 | 3. Permission error - True (1) or False (0) 36 | indicates whether there was a permission error or not 37 | 4. Undefined error - True (1) or False (0) 38 | indicates whether there was an undefined error or not (e.g. failure due 39 | to stochastic nature of exploits) 40 | 41 | Since the number of features in the auxiliary row is less than the number 42 | of features in each host row, the remainder of the row is all zeros. 43 | """ 44 | 45 | # obs vector positions for auxiliary observations 46 | _success_idx = 0 47 | _conn_error_idx = _success_idx + 1 48 | _perm_error_idx = _conn_error_idx + 1 49 | _undef_error_idx = _perm_error_idx + 1 50 | 51 | def __init__(self, state_shape): 52 | """ 53 | Parameters 54 | ---------- 55 | state_shape : (int, int) 56 | 2D shape of the state (i.e. num_hosts, host_vector_size) 57 | """ 58 | self.obs_shape = (state_shape[0]+1, state_shape[1]) 59 | self.aux_row = -1 60 | self.tensor = np.zeros(self.obs_shape, dtype=np.float32) 61 | 62 | @staticmethod 63 | def get_space_bounds(scenario): 64 | value_bounds = scenario.host_value_bounds 65 | discovery_bounds = scenario.host_discovery_value_bounds 66 | obs_low = min( 67 | 0, 68 | value_bounds[0], 69 | discovery_bounds[0] 70 | ) 71 | obs_high = max( 72 | 1, 73 | value_bounds[1], 74 | discovery_bounds[1], 75 | AccessLevel.ROOT, 76 | len(scenario.subnets), 77 | max(scenario.subnets) 78 | ) 79 | return (obs_low, obs_high) 80 | 81 | @classmethod 82 | def from_numpy(cls, o_array, state_shape): 83 | obs = cls(state_shape) 84 | # if o_array.shape != (state_shape[0]+1, state_shape[1]): 85 | # o_array = o_array.reshape(state_shape[0]+1, state_shape[1]) 86 | obs.tensor = o_array 87 | return obs 88 | 89 | def from_state(self, state): 90 | self.tensor[:self.aux_row] = state.tensor 91 | 92 | def from_action_result(self, action_result): 93 | success = int(action_result.success) 94 | self.tensor[self.aux_row][self._success_idx] = success 95 | con_err = int(action_result.connection_error) 96 | self.tensor[self.aux_row][self._conn_error_idx] = con_err 97 | perm_err = int(action_result.permission_error) 98 | self.tensor[self.aux_row][self._perm_error_idx] = perm_err 99 | undef_err = int(action_result.undefined_error) 100 | self.tensor[self.aux_row][self._undef_error_idx] = undef_err 101 | 102 | def from_state_and_action(self, state, action_result): 103 | self.from_state(state) 104 | self.from_action_result(action_result) 105 | 106 | def update_from_host(self, host_idx, host_obs_vector): 107 | self.tensor[host_idx][:] = host_obs_vector 108 | 109 | @property 110 | def success(self): 111 | """Whether the action succeded or not 112 | 113 | Returns 114 | ------- 115 | bool 116 | True if the action succeeded, otherwise False 117 | """ 118 | return bool(self.tensor[self.aux_row][self._success_idx]) 119 | 120 | @property 121 | def connection_error(self): 122 | """Whether there was a connection error or not 123 | 124 | Returns 125 | ------- 126 | bool 127 | True if there was a connection error, otherwise False 128 | """ 129 | return bool(self.tensor[self.aux_row][self._conn_error_idx]) 130 | 131 | @property 132 | def permission_error(self): 133 | """Whether there was a permission error or not 134 | 135 | Returns 136 | ------- 137 | bool 138 | True if there was a permission error, otherwise False 139 | """ 140 | return bool(self.tensor[self.aux_row][self._perm_error_idx]) 141 | 142 | @property 143 | def undefined_error(self): 144 | """Whether there was an undefined error or not 145 | 146 | Returns 147 | ------- 148 | bool 149 | True if there was a undefined error, otherwise False 150 | """ 151 | return bool(self.tensor[self.aux_row][self._undef_error_idx]) 152 | 153 | def shape_flat(self): 154 | """Get the flat (1D) shape of the Observation. 155 | 156 | Returns 157 | ------- 158 | (int, ) 159 | the flattened shape of observation 160 | """ 161 | return self.numpy_flat().shape 162 | 163 | def shape(self): 164 | """Get the (2D) shape of the observation 165 | 166 | Returns 167 | ------- 168 | (int, int) 169 | the 2D shape of the observation 170 | """ 171 | return self.obs_shape 172 | 173 | def numpy_flat(self): 174 | """Get the flattened observation tensor 175 | 176 | Returns 177 | ------- 178 | numpy.ndarray 179 | the flattened (1D) observation tenser 180 | """ 181 | return self.tensor.flatten() 182 | 183 | def numpy(self): 184 | """Get the observation tensor 185 | 186 | Returns 187 | ------- 188 | numpy.ndarray 189 | the (2D) observation tenser 190 | """ 191 | return self.tensor 192 | 193 | def get_readable(self): 194 | """Get a human readable version of the observation 195 | 196 | Returns 197 | ------- 198 | list[dict] 199 | list of host observations as human-readable dictionary 200 | dict[str, bool] 201 | auxiliary observation dictionary 202 | """ 203 | host_obs = [] 204 | for host_idx in range(self.tensor.shape[0]-1): 205 | host_obs_vec = self.tensor[host_idx] 206 | readable_dict = HostVector.get_readable(host_obs_vec) 207 | host_obs.append(readable_dict) 208 | 209 | aux_obs = { 210 | "Success": self.success, 211 | "Connection Error": self.connection_error, 212 | "Permission Error": self.permission_error, 213 | "Undefined Error": self.undefined_error 214 | } 215 | return host_obs, aux_obs 216 | 217 | def __str__(self): 218 | return str(self.tensor) 219 | 220 | def __eq__(self, other): 221 | return np.array_equal(self.tensor, other.tensor) 222 | 223 | def __hash__(self): 224 | return hash(str(self.tensor)) 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NASimEmu: Network Attack Simulator & Emulator 2 | 3 |
4 | NASimEmu observation 5 |
6 | 7 | ## Related 8 | - [Repository with deep RL agents](https://github.com/jaromiru/NASimEmu-agents) 9 | - *NASimEmu: Network Attack Simulator & Emulator for Training Agents Generalizing to Novel Scenarios*, SECAI workshop @ ESORICS 2023, [preprint](https://arxiv.org/abs/2305.17246) 10 | - *Applications of Deep Reinforcement Learning in Practical Sequential Information Acquisition Problems*, Chapter 6 - *Case Study: Automated Penetration Testing*, Doctoral Dissertation, CTU, 2024, [link](https://dspace.cvut.cz/bitstream/handle/10467/114377/F3-D-2024-Janisch-Jaromir-dizertace.pdf) 11 | 12 | ## Introduction 13 | NASimEmu is a framework for training deep RL agents in offensive penetration-testing scenarios. It includes both a simulator and an emulator so that a simulation-trained agent can be seamlessly deployed in emulation. Additionally, it includes a random generator that can create scenario instances varying in network configuration and size while fixing certain features, such as exploits and privilege escalations. Furthermore, agents can be trained and tested in multiple scenarios simultaneously. 14 | 15 | The simulation is based on [Network Attack Simulator](https://github.com/Jjschwartz/NetworkAttackSimulator) and can be used to train and evaluate agents. It is a memory-based fast and parallelizable abstraction of real computer networks and can generate random scenario instances which can vary in network topology, configuration and number of hosts. The emulation is a controlled environment using industry-level tools, such as Vagrant, VirtualBox, and Metasploit. Agents trained in simulation can be transparently deployed in emulation. 16 | 17 | NASimEmu introduces a few changes for training general agents that can transfer to novel, unseen scenarios. It includes *dynamic scenarios* that represent prototypical situations, e.g., typical university or corporate networks. In these scenarios, some attributes are fixed (network topology, OSs, services, exploits, etc.), while some are left to chance (network size and hosts' configuration). Lastly, multiple scenarios can be specified for training or testing. 18 | 19 | The accompanying paper *NASimEmu: Network Attack Simulator & Emulator for Training Agents Generalizing to Novel Scenarios* can be found on [arXiv](https://arxiv.org/abs/2305.17246). 20 | 21 | ## Installation 22 | Make sure you use latest `pip`: 23 | ``` 24 | pip install --upgrade pip 25 | ``` 26 | 27 | Clone the repository and install it locally (`-e` for development mode): 28 | ``` 29 | git clone https://github.com/jaromiru/NASimEmu.git 30 | cd NASimEmu; pip install -e . 31 | ``` 32 | 33 | To use emulation, you have to install [Vagrant](https://developer.hashicorp.com/vagrant/downloads) yourself; see [EMULATION](docs/EMULATION.md). 34 | 35 | ## Usage 36 | ```python 37 | import gym, random, logging 38 | import nasimemu, nasimemu.env_utils as env_utils 39 | 40 | # In this example, a scenario instance is randomly generated from either 'entry_dmz_one_subnet' or 'entry_dmz_two_subnets' on every new episode. Make sure the path to scenarios is correct. 41 | # To use emulation, setup Vagrant and change emulate=True. 42 | env = gym.make('NASimEmu-v0', emulate=False, scenario_name='scenarios/entry_dmz_one_subnet.v2.yaml:scenarios/entry_dmz_two_subnets.v2.yaml') 43 | s = env.reset() 44 | 45 | # To see the emulation logs, uncomment the following: 46 | # logging.basicConfig(level=logging.DEBUG) 47 | # logging.getLogger('urllib3').setLevel(logging.INFO) 48 | 49 | # To see the whole network, use (only in simulation): 50 | # env.render_state() 51 | 52 | for _ in range(3): 53 | actions = env_utils.get_possible_actions(env, s) 54 | env.render(s) 55 | 56 | # you can convert the observation into a graph format (e.g., for Pytorch Geometric) as: 57 | # s_graph = env_utils.convert_to_graph(s) 58 | 59 | action = random.choice(actions) 60 | s, r, done, info = env.step(action) 61 | 62 | print(f"Possible actions: {actions}") 63 | 64 | (action_subnet, action_host), action_id = action 65 | print(f"Taken action: {action}; subnet_id={action_subnet}, host_id={action_host}, action={env.action_list[action_id]}") 66 | print(f"reward: {r}, done: {done}\n") 67 | input() 68 | ``` 69 | 70 | ## Gym integration 71 | NASimEmu currently doesn't support many of the default agents made for gym, because it uses custom environment spaces (the size of the environment is unknown to the attacker at the beginning) and an action space that changes at each step when a new host is discovered. 72 | 73 | ## Implemented Deep RL Agents 74 | See the separate repository [NASimEmu-agents](https://github.com/jaromiru/NASimEmu-agents) for implemented agents. 75 | 76 | ## Simulation 77 | The simulation is based on Network Attack Simulator and you can read its docs here: https://networkattacksimulator.readthedocs.io 78 | 79 | These are the changes in NaSimEmu: 80 | 81 | - Support for new [v2 scenarios](docs/SCENARIOS.md), which define classes of problems from which random instances are generated. The v2 scenarios define a topology, sensitive subnets, services, processes and exploits. However, the generated instances vary in number of hosts in subnets and hosts' configuration. For example, you can define scenarios representing "bank", "university" or "enterprise" domains. 82 | - Agent can be trained or deployed in multiple scerarios at once. This is useful to test generalization and transfer learning of the agent. E.g., how does it perform in the "enterprise" scenario when trained only in "bank" and "university"? 83 | - The interface has been changed to true partial observability. That is, the agent cannot infer the number of hosts or subnets in the current scenario instance and all other unknown information is masked. To aleviate memorization of acquired information, the observation at each step records all the information gathered so far. Further, host and subnet ids are scrambled to avoid their memorization. 84 | 85 | ## Emulation 86 | The emulation is made with [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/). You will need to install these on your system first. A scenario is converted into a Vagrantfile and deployed. The vagrant spawns a virtual machine for each host in the scenario (currently, Ubuntu and Windows machines are available) and configures them properly. Additionally, it deploys one [RouterOS](https://wiki.mikrotik.com/wiki/Manual:RouterOS_FAQ) based router that segments the network into subnets and one attacker machine that runs [Kali Linux](https://www.kali.org/) and uses [Metasploit](https://www.metasploit.com/) with pre-configured exploits to run. 87 | 88 | For more information about emulation, see [EMULATION](docs/EMULATION.md). 89 | 90 | ## Limitations / Properties 91 | Please, be aware of the following: 92 | 93 | - exploits are assumed to always work (in emulation, this is not true) 94 | - the services have a single version and are always exploitable, if the exploit is defined 95 | - currently, the maximum number of subnets and hosts in subnets have to be defined, so that the host vector size is always the same 96 | - if there are multiple paths from the attacker to a target (some of which are filtered by firewalls), the simulator assumes that the host is exploitable if any of the paths allow it; in emulation, this is not implemented (the first discovered path will be used); avoid doing this 97 | - we assume that a firewall either block or allows **all** traffic between subnets 98 | - in emulation, all hosts are on the same interface, which is only virtually segmented by the router; avoid using protocols that bypass the router (such as ARP) 99 | - if defined in the scenario, sensitive services always run on all sensitive nodes; sensitive services also run on 10% of non-sensitive nodes 100 | - emulation do not provide any rewards 101 | 102 | ## Acknowledgements 103 | 104 | [@bastien-buil](https://www.github.com/bastien-buil) - windows emulation, bugfixes and part of documentation 105 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/agents/keyboard_agent.py: -------------------------------------------------------------------------------- 1 | """An agent that lets the user interact with NASim using the keyboard. 2 | 3 | To run 'tiny' benchmark scenario with default settings, run the following from 4 | the nasim/agents dir: 5 | 6 | $ python keyboard_agent.py tiny 7 | 8 | This will run the agent and display the game in stdout. 9 | 10 | To see available running arguments: 11 | 12 | $ python keyboard_agent.py--help 13 | """ 14 | from nasimemu import nasim 15 | from nasimemu.nasim.envs.action import Exploit, PrivilegeEscalation 16 | 17 | 18 | LINE_BREAK = "-"*60 19 | LINE_BREAK2 = "="*60 20 | 21 | 22 | def print_actions(action_space): 23 | for a in range(action_space.n): 24 | print(f"{a} {action_space.get_action(a)}") 25 | print(LINE_BREAK) 26 | 27 | 28 | def choose_flat_action(env): 29 | print_actions(env.action_space) 30 | while True: 31 | try: 32 | idx = int(input("Choose action number: ")) 33 | action = env.action_space.get_action(idx) 34 | print(f"Performing: {action}") 35 | return action 36 | except Exception: 37 | print("Invalid choice. Try again.") 38 | 39 | 40 | def display_actions(actions): 41 | action_names = list(actions) 42 | for i, name in enumerate(action_names): 43 | a_def = actions[name] 44 | output = [f"{i} {name}:"] 45 | output.extend([f"{k}={v}" for k, v in a_def.items()]) 46 | print(" ".join(output)) 47 | 48 | 49 | def choose_item(items): 50 | while True: 51 | try: 52 | idx = int(input("Choose number: ")) 53 | return items[idx] 54 | except Exception: 55 | print("Invalid choice. Try again.") 56 | 57 | 58 | def choose_param_action(env): 59 | print("1. Choose Action Type:") 60 | print("----------------------") 61 | for i, atype in enumerate(env.action_space.action_types): 62 | print(f"{i} {atype.__name__}") 63 | while True: 64 | try: 65 | atype_idx = int(input("Choose index: ")) 66 | # check idx valid 67 | atype = env.action_space.action_types[atype_idx] 68 | break 69 | except Exception: 70 | print("Invalid choice. Try again.") 71 | 72 | print("------------------------") 73 | print("2. Choose Target Subnet:") 74 | print("------------------------") 75 | num_subnets = env.action_space.nvec[1] 76 | while True: 77 | try: 78 | subnet = int(input(f"Choose subnet in [1, {num_subnets}]: ")) 79 | if subnet < 1 or subnet > num_subnets: 80 | raise ValueError() 81 | break 82 | except Exception: 83 | print("Invalid choice. Try again.") 84 | 85 | print("----------------------") 86 | print("3. Choose Target Host:") 87 | print("----------------------") 88 | num_hosts = env.scenario.subnets[subnet] 89 | while True: 90 | try: 91 | host = int(input(f"Choose host in [0, {num_hosts-1}]: ")) 92 | if host < 0 or host > num_hosts-1: 93 | raise ValueError() 94 | break 95 | except Exception: 96 | print("Invalid choice. Try again.") 97 | 98 | # subnet-1, since action_space handles exclusion of internet subnet 99 | avec = [atype_idx, subnet-1, host, 0, 0] 100 | if atype not in (Exploit, PrivilegeEscalation): 101 | action = env.action_space.get_action(avec) 102 | print("----------------") 103 | print(f"ACTION SELECTED: {action}") 104 | return action 105 | 106 | target = (subnet, host) 107 | if atype == Exploit: 108 | print("------------------") 109 | print("4. Choose Exploit:") 110 | print("------------------") 111 | exploits = env.scenario.exploits 112 | display_actions(exploits) 113 | e_name = choose_item(list(exploits)) 114 | action = Exploit(name=e_name, target=target, **exploits[e_name]) 115 | else: 116 | print("------------------") 117 | print("4. Choose Privilege Escalation:") 118 | print("------------------") 119 | privescs = env.scenario.privescs 120 | display_actions(privescs) 121 | pe_name = choose_item(list(privescs)) 122 | action = PrivilegeEscalation( 123 | name=pe_name, target=target, **privescs[pe_name] 124 | ) 125 | 126 | print("----------------") 127 | print(f"ACTION SELECTED: {action}") 128 | return action 129 | 130 | 131 | def choose_action(env): 132 | input("Press enter to choose next action..") 133 | print("\n" + LINE_BREAK2) 134 | print("CHOOSE ACTION") 135 | print(LINE_BREAK2) 136 | if env.flat_actions: 137 | return choose_flat_action(env) 138 | return choose_param_action(env) 139 | 140 | 141 | def run_keyboard_agent(env, render_mode="readable"): 142 | """Run Keyboard agent 143 | 144 | Parameters 145 | ---------- 146 | env : NASimEnv 147 | the environment 148 | render_mode : str, optional 149 | display mode for environment (default="readable") 150 | 151 | Returns 152 | ------- 153 | int 154 | final return 155 | int 156 | steps taken 157 | bool 158 | whether goal reached or not 159 | """ 160 | print(LINE_BREAK2) 161 | print("STARTING EPISODE") 162 | print(LINE_BREAK2) 163 | 164 | o = env.reset() 165 | env.render(render_mode) 166 | total_reward = 0 167 | total_steps = 0 168 | done = False 169 | while not done: 170 | a = choose_action(env) 171 | o, r, done, _ = env.step(a) 172 | total_reward += r 173 | total_steps += 1 174 | print("\n" + LINE_BREAK2) 175 | print("OBSERVATION RECIEVED") 176 | print(LINE_BREAK2) 177 | env.render(render_mode) 178 | print(f"Reward={r}") 179 | print(f"Done={done}") 180 | print(LINE_BREAK) 181 | 182 | if done: 183 | done = env.goal_reached() 184 | 185 | return total_reward, total_steps, done 186 | 187 | 188 | def run_generative_keyboard_agent(env, render_mode="readable"): 189 | """Run Keyboard agent in generative mode. 190 | 191 | The experience is the same as the normal mode, this is mainly useful 192 | for testing. 193 | 194 | Parameters 195 | ---------- 196 | env : NASimEnv 197 | the environment 198 | render_mode : str, optional 199 | display mode for environment (default="readable") 200 | 201 | Returns 202 | ------- 203 | int 204 | final return 205 | int 206 | steps taken 207 | bool 208 | whether goal reached or not 209 | """ 210 | print(LINE_BREAK2) 211 | print("STARTING EPISODE") 212 | print(LINE_BREAK2) 213 | 214 | o = env.reset() 215 | s = env.current_state 216 | env.render_state(render_mode, s) 217 | env.render(render_mode, o) 218 | 219 | total_reward = 0 220 | total_steps = 0 221 | done = False 222 | while not done: 223 | a = choose_action(env) 224 | ns, o, r, done, _ = env.generative_step(s, a) 225 | total_reward += r 226 | total_steps += 1 227 | print(LINE_BREAK2) 228 | print("NEXT STATE") 229 | print(LINE_BREAK2) 230 | env.render_state(render_mode, ns) 231 | print("\n" + LINE_BREAK2) 232 | print("OBSERVATION RECIEVED") 233 | print(LINE_BREAK2) 234 | env.render(render_mode, o) 235 | print(f"Reward={r}") 236 | print(f"Done={done}") 237 | print(LINE_BREAK) 238 | s = ns 239 | 240 | if done: 241 | done = env.goal_reached() 242 | 243 | return total_reward, total_steps, done 244 | 245 | 246 | if __name__ == "__main__": 247 | import argparse 248 | parser = argparse.ArgumentParser() 249 | parser.add_argument("env_name", type=str, 250 | help="benchmark scenario name") 251 | parser.add_argument("-s", "--seed", type=int, default=None, 252 | help="random seed (default=None)") 253 | parser.add_argument("-o", "--partially_obs", action="store_true", 254 | help="Partially Observable Mode") 255 | parser.add_argument("-p", "--param_actions", action="store_true", 256 | help="Use Parameterised action space") 257 | parser.add_argument("-g", "--use_generative", action="store_true", 258 | help=("Generative environment mode. This makes no" 259 | " difference for the player, but is useful" 260 | " for testing.")) 261 | args = parser.parse_args() 262 | 263 | env = nasim.make_benchmark(args.env_name, 264 | args.seed, 265 | fully_obs=not args.partially_obs, 266 | flat_actions=not args.param_actions, 267 | flat_obs=True) 268 | if args.use_generative: 269 | total_reward, steps, goal = run_generative_keyboard_agent(env) 270 | else: 271 | total_reward, steps, goal = run_keyboard_agent(env) 272 | 273 | print(LINE_BREAK2) 274 | print("EPISODE FINISHED") 275 | print(LINE_BREAK) 276 | print(f"Goal reached = {goal}") 277 | print(f"Total reward = {total_reward}") 278 | print(f"Steps taken = {steps}") 279 | -------------------------------------------------------------------------------- /src/nasimemu/vagrant_gen.py: -------------------------------------------------------------------------------- 1 | # This is an self-executable module to create Vagrantfiles and RouterOS configuration from scenarios. 2 | 3 | import logging 4 | import sys 5 | import argparse 6 | import os.path as path 7 | from pathlib import Path 8 | 9 | from .env_emu import EmulatedNetwork 10 | from .nasim.scenarios import load_scenario 11 | from .nasim.envs import NASimEnv 12 | 13 | class VagrantGenerator: 14 | 15 | def __init__(self, scenario, vagrant_file): 16 | self.scenario = scenario 17 | self.vagrant_file = vagrant_file 18 | self.generate_vagrant() 19 | 20 | def generate_vagrant(self): 21 | self.write_header() 22 | for host in self.scenario.hosts: 23 | self.add_host(self.scenario.hosts[host], self.scenario.hosts[host].address in self.scenario.sensitive_hosts) 24 | self.write_footer() 25 | 26 | def write_header(self): 27 | self.vagrant_file.write(f"""# Use: 28 | # copy .vagrant file to ../vagrant/Vagrantfile 29 | # copy .rsc file to ../vagrant/firewall.rsc 30 | # cd to ../vagrant 31 | # use vagrant to run the emulation: 32 | # `vagrant status` 33 | # `vagrant up` 34 | 35 | Vagrant.configure("2") do |config| 36 | config.vm.provider "virtualbox" do |vb| 37 | vb.gui = false 38 | vb.memory = "1024" 39 | # vb.check_guest_additions = false 40 | end 41 | 42 | # see https://github.com/cheretbe/packer-routeros 43 | config.vm.define "router" do |router| 44 | router.vm.box = "cheretbe/routeros" 45 | router.vm.box_version = "6.48.4-0" 46 | router.vm.hostname = "router" 47 | 48 | # it needs `VBoxManage dhcpserver remove --netname HostInterfaceNetworking-vboxnet0` 49 | # if you receive 'A host only network interface you're attempting to configure via DHCP' error 50 | router.trigger.before :up do |trigger| 51 | trigger.warn = "removing standard dhcp host interface if existent" 52 | trigger.run = {{inline: "bash -c 'if [ $( VBoxManage list dhcpservers | grep -c vboxnet0 ) != \\"0\\" ]; then VBoxManage dhcpserver remove --netname HostInterfaceNetworking-vboxnet0; fi'"}} 53 | end 54 | 55 | router.ssh.username = "admin" 56 | router.ssh.password = "vagrant" 57 | 58 | router.vm.network "private_network", virtualbox__intnet: "nasim-network", auto_config: false 59 | 60 | router.vm.provision "routeros_file", source: "router/bootstrap.rsc", destination: "bootstrap.rsc" 61 | router.vm.provision "routeros_command", command: "/import bootstrap.rsc", check_script_error: true 62 | 63 | router.vm.provision "routeros_file", source: "firewall.rsc", destination: "firewall.rsc" # make sure you copied the .rsc file as well! 64 | router.vm.provision "routeros_command", command: "/import firewall.rsc", check_script_error: true 65 | 66 | router.vm.provision "routeros_file", source: "router/bootstrap-after-firewall.rsc", destination: "bootstrap-after-firewall.rsc" 67 | router.vm.provision "routeros_command", command: "/import bootstrap-after-firewall.rsc", check_script_error: true 68 | end 69 | 70 | config.vm.define "attacker" do |attacker| 71 | attacker.vm.box = "kalilinux/rolling" 72 | attacker.vm.box_version = "2022.1.0" 73 | attacker.vm.hostname = "attacker" 74 | attacker.vm.network "private_network", ip: "192.168.0.100", netmask: "255.255.255.0", virtualbox__intnet: "nasim-network" 75 | attacker.vm.network "forwarded_port", guest: 55553, host: 55553 # msfrpcd 76 | 77 | attacker.vm.synced_folder 'attacker', '/vagrant', type: 'rsync' 78 | attacker.vm.provision "shell", path: 'attacker/bootstrap.sh' 79 | attacker.vm.provision "shell", path: 'attacker/setup-network.py', args: '-subnetid 0' 80 | end 81 | """) 82 | 83 | def write_footer(self): 84 | self.vagrant_file.write(""" 85 | end 86 | """) 87 | 88 | @staticmethod 89 | def _get_provision_line(os, services, ip, subnet, varname, is_sensitive): 90 | enabled_services = [service for service in services if services[service]] 91 | if 'windows' in os and os['windows']: 92 | add_loot_line = f"{varname}.vm.provision \"shell\", path: 'target/windows-insert-loot.ps1'" if is_sensitive else "" 93 | return f""" 94 | {varname}.vm.synced_folder 'target', '/vagrant' 95 | {varname}.vm.provision "shell", path: 'target/windows-service-script.ps1', args: '-services {",".join(enabled_services)} -ip {ip}' 96 | {varname}.winrm.retry_limit = 100 97 | {varname}.winrm.retry_delay = 10 98 | {varname}.vm.provision "shell", path: 'target/windows-setup-network.ps1', args: '-subnetid {subnet}' 99 | {add_loot_line} 100 | {varname}.vm.provision :shell do |shell| 101 | shell.privileged = true 102 | shell.inline = <<-'SCRIPT' 103 | slmgr.vbs /rearm 104 | echo "Rebooting for license renewal" 105 | SCRIPT 106 | shell.reboot = true 107 | end 108 | """ 109 | elif 'linux' in os and os['linux']: 110 | add_loot_line = f"{varname}.vm.provision \"shell\", path: 'target/linux-insert-loot.sh'" if is_sensitive else "" 111 | return f""" 112 | {varname}.vm.synced_folder 'target', '/vagrant', type: 'rsync' 113 | {varname}.vm.provision "shell", path: 'target/linux-service-script.sh', args: '{" ".join(enabled_services)}' 114 | {varname}.vm.provision "shell", path: 'target/bootstrap.sh' 115 | {varname}.vm.provision "shell", path: 'target/linux-setup-network.py', args: '-subnetid {subnet}' 116 | {add_loot_line} 117 | """ 118 | 119 | @staticmethod 120 | def _get_box_from_os(os): 121 | if 'windows' in os and os['windows']: 122 | return "rapid7/metasploitable3-win2k8", "0.1.0-weekly" 123 | elif 'linux' in os and os['linux']: 124 | return "rapid7/metasploitable3-ub1404", "0.1.12-weekly" 125 | 126 | @staticmethod 127 | def _get_host_description(varname, hostname, ip, box, box_version, netmask, provision): 128 | return f""" 129 | config.vm.define "{hostname}" do |{varname}| 130 | {varname}.vm.box = "{box}" 131 | {varname}.vm.box_version = "{box_version}" 132 | {varname}.vm.hostname = "{hostname}" 133 | {varname}.vm.network "private_network", ip: "{ip}", netmask: "{netmask}", virtualbox__intnet: "nasim-network" 134 | 135 | {varname}.ssh.username = 'vagrant' 136 | {varname}.ssh.password = 'vagrant' 137 | {provision} 138 | end 139 | """ 140 | 141 | def add_host(self, host, is_sensitive): 142 | varname = 'target' 143 | hostname = f"target{host.address[0]}{host.address[1]}" 144 | ip = EmulatedNetwork._target_to_ip(host.address) 145 | subnet = host.address[0] 146 | box, box_version = self._get_box_from_os(host.os) 147 | netmask = "255.255.255.0" 148 | provision = self._get_provision_line(host.os, host.services, ip, subnet, varname, is_sensitive) 149 | 150 | self.vagrant_file.write(self._get_host_description(varname, hostname, ip, box, box_version, netmask, provision)) 151 | 152 | class RouteOsGenerator: 153 | def __init__(self, scenario, out=sys.stdout): 154 | self.out = out 155 | self.firewall = scenario.firewall 156 | self.topology = scenario.topology 157 | self.generate_firewall() 158 | 159 | def generate_firewall(self): 160 | """for link in self.firewall: 161 | for service_allowed in self.firewall[link]: 162 | print(service_allowed, self.firewall[link][service_allowed])""" 163 | text_output = "" 164 | for address_in in range(len(self.topology)): 165 | for address_out in range(len(self.topology[address_in])): 166 | if address_in != address_out and self.topology[address_in][address_out] == 1: 167 | text_output += f"/ip firewall filter add chain=forward action=accept src-address=192.168.{address_in}.0/24 dst-address=192.168.{address_out}.0/24 proto=icmp\n" 168 | text_output += f"/ip firewall filter add chain=forward action=accept src-address=192.168.{address_in}.0/24 dst-address=192.168.{address_out}.0/24 proto=tcp\n" 169 | self.out.write(text_output) 170 | 171 | class VagrantClient: 172 | def __init__(self, scenario, dst_file, routeos_file): 173 | self.scenario = scenario 174 | 175 | self.dst_file = dst_file 176 | self.routeos_file = routeos_file 177 | 178 | self.logger = logging.getLogger("VagrantInterface") 179 | 180 | self.generate_routeos_file() 181 | self.generate_vagrant_file() 182 | # self.launch_vagrant() 183 | 184 | def generate_routeos_file(self): 185 | self.logger.info("Generating routeos firewall file.") 186 | with open(self.routeos_file, "w") as file: 187 | RouteOsGenerator(self.scenario, out=file) 188 | 189 | 190 | def generate_vagrant_file(self): 191 | self.logger.info("Generating Vagrantfile.") 192 | with open(self.dst_file, "w") as file: 193 | VagrantGenerator(self.scenario, file) 194 | 195 | # def launch_vagrant(self): 196 | # self.logger.info("Vagrant file generated, you can now launch the virtual machines with vagrant.") 197 | # # input() 198 | # subprocess.run(["vagrant", "up", "--provision", "--parallel", "--no-tty"], cwd="nasim_emulation/vagrant") 199 | 200 | if __name__ == '__main__': 201 | logging.basicConfig(level=logging.DEBUG) 202 | parser = argparse.ArgumentParser() 203 | parser.add_argument('file', type=str, help="Path to the scenario file") 204 | parser.add_argument('--dst', type=str, help="Path to the vagrant file") 205 | parser.add_argument('--routeros', type=str, help="Path to the routeros file") 206 | 207 | args = parser.parse_args() 208 | 209 | if args.dst is None: 210 | args.dst = str(Path(args.file).with_suffix('.vagrant')) 211 | 212 | if args.routeros is None: 213 | args.routeros = str(Path(args.file).with_suffix('.rsc')) 214 | 215 | print(f"Vagrantfile: {args.dst}, routeros file: {args.routeros}") 216 | 217 | scenario = load_scenario(args.file) 218 | 219 | print("\nGenerated scenario:") 220 | env = NASimEnv(scenario) 221 | env.reset() 222 | env.render_state() 223 | print("") 224 | 225 | VagrantClient(scenario, args.dst, args.routeros) 226 | -------------------------------------------------------------------------------- /src/nasimemu/nasim/envs/network.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from .action import ActionResult 4 | from .utils import get_minimal_steps_to_goal, min_subnet_depth, AccessLevel 5 | 6 | # column in topology adjacency matrix that represents connection between 7 | # subnet and public 8 | INTERNET = 0 9 | 10 | 11 | class Network: 12 | """A computer network """ 13 | 14 | def __init__(self, scenario): 15 | self.hosts = scenario.hosts 16 | self.host_num_map = scenario.host_num_map 17 | self.subnets = scenario.subnets 18 | self.topology = scenario.topology 19 | self.firewall = scenario.firewall 20 | self.address_space = scenario.address_space 21 | self.address_space_bounds = scenario.address_space_bounds 22 | self.sensitive_addresses = scenario.sensitive_addresses 23 | self.sensitive_hosts = scenario.sensitive_hosts 24 | 25 | def reset(self, state): 26 | """Reset the network state to initial state """ 27 | next_state = state.copy() 28 | for host_addr in self.address_space: 29 | host = next_state.get_host(host_addr) 30 | host.compromised = False 31 | host.access = AccessLevel.NONE 32 | host.reachable = self.subnet_public(host_addr[0]) 33 | host.discovered = host.reachable 34 | return next_state 35 | 36 | def perform_action(self, state, action): 37 | """Perform the given Action against the network. 38 | 39 | Arguments 40 | --------- 41 | state : State 42 | the current state 43 | action : Action 44 | the action to perform 45 | 46 | Returns 47 | ------- 48 | State 49 | the state after the action is performed 50 | ActionObservation 51 | the result from the action 52 | """ 53 | tgt_subnet, tgt_id = action.target 54 | assert 0 < tgt_subnet < len(self.subnets) 55 | assert tgt_id <= self.subnets[tgt_subnet] 56 | 57 | next_state = state.copy() 58 | 59 | if action.is_noop(): 60 | return next_state, ActionResult(True) 61 | 62 | if not state.host_reachable(action.target) \ 63 | or not state.host_discovered(action.target): 64 | result = ActionResult(False, 0.0, connection_error=True) 65 | return next_state, result 66 | 67 | has_req_permission = self.has_required_remote_permission(state, action) 68 | if action.is_remote() and not has_req_permission: 69 | result = ActionResult(False, 0.0, permission_error=True) 70 | return next_state, result 71 | 72 | if action.is_exploit() \ 73 | and not self.traffic_permitted( 74 | state, action.target, action.service 75 | ): 76 | result = ActionResult(False, 0.0, connection_error=True) 77 | return next_state, result 78 | 79 | host_compromised = state.host_compromised(action.target) 80 | if action.is_privilege_escalation() and not host_compromised: 81 | result = ActionResult(False, 0.0, connection_error=True) 82 | return next_state, result 83 | 84 | if action.is_process_scan() and not host_compromised: # processes can only be scanned if the user has a local access 85 | result = ActionResult(False, 0.0, connection_error=True) 86 | return next_state, result 87 | 88 | if action.is_exploit() and host_compromised: 89 | # host already compromised so exploits don't fail due to randomness 90 | pass 91 | elif np.random.rand() > action.prob: 92 | return next_state, ActionResult(False, 0.0, undefined_error=True) 93 | 94 | if action.is_subnet_scan(): 95 | return self._perform_subnet_scan(next_state, action) 96 | 97 | t_host = state.get_host(action.target) 98 | next_host_state, action_obs = t_host.perform_action(action) 99 | next_state.update_host(action.target, next_host_state) 100 | self._update(next_state, action, action_obs) 101 | return next_state, action_obs 102 | 103 | def _perform_subnet_scan(self, next_state, action): 104 | if not next_state.host_compromised(action.target): 105 | result = ActionResult(False, 0.0, connection_error=True) 106 | return next_state, result 107 | 108 | if not next_state.host_has_access(action.target, action.req_access): 109 | result = ActionResult(False, 0.0, permission_error=True) 110 | return next_state, result 111 | 112 | discovered = {} 113 | newly_discovered = {} 114 | discovery_reward = 0 115 | target_subnet = action.target[0] 116 | for h_addr in self.address_space: 117 | newly_discovered[h_addr] = False 118 | discovered[h_addr] = False 119 | if self.subnets_connected(target_subnet, h_addr[0]): 120 | host = next_state.get_host(h_addr) 121 | discovered[h_addr] = True 122 | if not host.discovered: 123 | newly_discovered[h_addr] = True 124 | host.discovered = True 125 | discovery_reward += host.discovery_value 126 | 127 | obs = ActionResult( 128 | True, 129 | discovery_reward, 130 | discovered=discovered, 131 | newly_discovered=newly_discovered 132 | ) 133 | return next_state, obs 134 | 135 | def _update(self, state, action, action_obs): 136 | if action.is_exploit() and action_obs.success: 137 | self._update_reachable(state, action.target) 138 | 139 | def _update_reachable(self, state, compromised_addr): 140 | """Updates the reachable status of hosts on network, based on current 141 | state and newly exploited host 142 | """ 143 | comp_subnet = compromised_addr[0] 144 | for addr in self.address_space: 145 | if state.host_reachable(addr): 146 | continue 147 | if self.subnets_connected(comp_subnet, addr[0]): 148 | state.set_host_reachable(addr) 149 | 150 | def get_sensitive_hosts(self): 151 | return self.sensitive_addresses 152 | 153 | def is_sensitive_host(self, host_address): 154 | return host_address in self.sensitive_addresses 155 | 156 | def subnets_connected(self, subnet_1, subnet_2): 157 | return self.topology[subnet_1][subnet_2] == 1 158 | 159 | def subnet_traffic_permitted(self, src_subnet, dest_subnet, service): 160 | if src_subnet == dest_subnet: 161 | # in same subnet so permitted 162 | return True 163 | if not self.subnets_connected(src_subnet, dest_subnet): 164 | return False 165 | return service in self.firewall[(src_subnet, dest_subnet)] 166 | 167 | def host_traffic_permitted(self, src_addr, dest_addr, service): 168 | dest_host = self.hosts[dest_addr] 169 | return dest_host.traffic_permitted(src_addr, service) 170 | 171 | def has_required_remote_permission(self, state, action): 172 | """Checks attacker has necessary permissions for remote action """ 173 | if self.subnet_public(action.target[0]): 174 | return True 175 | 176 | for src_addr in self.address_space: 177 | if not state.host_compromised(src_addr): 178 | continue 179 | if action.is_scan() and \ 180 | not self.subnets_connected(src_addr[0], action.target[0]): 181 | continue 182 | if action.is_exploit() and \ 183 | not self.subnet_traffic_permitted( 184 | src_addr[0], action.target[0], action.service 185 | ): 186 | continue 187 | if state.host_has_access(src_addr, action.req_access): 188 | return True 189 | return False 190 | 191 | def traffic_permitted(self, state, host_addr, service): 192 | """Checks whether the subnet and host firewalls permits traffic to a 193 | given host and service, based on current set of compromised hosts on 194 | network. 195 | """ 196 | for src_addr in self.address_space: 197 | if not state.host_compromised(src_addr) and \ 198 | not self.subnet_public(src_addr[0]): 199 | continue 200 | if not self.subnet_traffic_permitted( 201 | src_addr[0], host_addr[0], service 202 | ): 203 | continue 204 | if self.host_traffic_permitted(src_addr, host_addr, service): 205 | return True 206 | return False 207 | 208 | def subnet_public(self, subnet): 209 | return self.topology[subnet][INTERNET] == 1 210 | 211 | def get_number_of_subnets(self): 212 | return len(self.subnets) 213 | 214 | def all_sensitive_hosts_compromised(self, state): 215 | for host_addr in self.sensitive_addresses: 216 | if not state.host_has_access(host_addr, AccessLevel.ROOT): 217 | return False 218 | return True 219 | 220 | def get_total_sensitive_host_value(self): 221 | total = 0 222 | for host_value in self.sensitive_hosts.values(): 223 | total += host_value 224 | return total 225 | 226 | def get_total_discovery_value(self): 227 | total = 0 228 | for host in self.hosts: 229 | total += host.discovery_value 230 | return total 231 | 232 | def get_minimal_steps(self): 233 | return get_minimal_steps_to_goal( 234 | self.topology, self.sensitive_addresses 235 | ) 236 | 237 | def get_subnet_depths(self): 238 | return min_subnet_depth(self.topology) 239 | 240 | def __str__(self): 241 | output = "\n--- Network ---\n" 242 | output += "Subnets: " + str(self.subnets) + "\n" 243 | output += "Topology:\n" 244 | for row in self.topology: 245 | output += f"\t{row}\n" 246 | output += "Sensitive hosts: \n" 247 | for addr, value in self.sensitive_hosts.items(): 248 | output += f"\t{addr}: {value}\n" 249 | output += "Num_services: {self.scenario.num_services}\n" 250 | output += "Hosts:\n" 251 | for m in self.hosts.values(): 252 | output += str(m) + "\n" 253 | output += "Firewall:\n" 254 | for c, a in self.firewall.items(): 255 | output += f"\t{c}: {a}\n" 256 | return output 257 | -------------------------------------------------------------------------------- /src/nasimemu/env_utils.py: -------------------------------------------------------------------------------- 1 | from nasimemu.nasim.envs.host_vector import HostVector 2 | import numpy as np 3 | 4 | import networkx as nx 5 | import plotly.graph_objects as go 6 | 7 | def get_possible_actions(env, s): 8 | # gather all possible addresses 9 | addresses = [] 10 | for host_data in s[:-1]: 11 | host_vector = HostVector(host_data) 12 | addresses.append(host_vector.address) 13 | 14 | possible_actions = [] 15 | 16 | for host_add in addresses: 17 | possible_actions.extend([(host_add, action_id) for action_id in range(len(env.action_list))]) 18 | 19 | return possible_actions 20 | 21 | def _complete_graph(n): 22 | return [(a, b) for a in n for b in n if a != b] 23 | 24 | def _gen_edge_index_v1(node_index, subnets, subnet_graph): 25 | edge_index = [] 26 | 27 | # hosts in subnets are connected together 28 | for subnet in subnets: 29 | hosts_in_subnet = np.flatnonzero( node_index[:, 0] == subnet ) 30 | edge_index.append( _complete_graph(hosts_in_subnet) ) 31 | 32 | # all subnets together 33 | # sub_index = np.flatnonzero( node_index[:, 1] == -1 ) 34 | # if len(sub_index) > 1: 35 | # edge_index.append( _complete_graph(sub_index) ) 36 | 37 | host_len = len(node_index) - len(subnets) 38 | 39 | for subnet_edge in subnet_graph: 40 | s_from = subnets.index(subnet_edge[0]) + host_len 41 | s_to = subnets.index(subnet_edge[1]) + host_len 42 | 43 | edge_index.append([(s_from, s_to), (s_to, s_from)]) 44 | 45 | edge_index = np.concatenate(edge_index).T 46 | 47 | return edge_index 48 | 49 | def _gen_edge_index_v2(node_index, subnets, subnet_graph): 50 | edge_index = [] 51 | 52 | # hosts in subnets are connected to their subnet 53 | for subnet in subnets: 54 | hosts_in_subnet = np.flatnonzero( node_index[:, 0] == subnet ) 55 | subnet_node = np.flatnonzero( (node_index[:, 0] == subnet) * (node_index[:, 1] == -1))[0] 56 | 57 | edge_index.append( [(x, subnet_node) for x in hosts_in_subnet if x != subnet_node] ) 58 | edge_index.append( [(subnet_node, x) for x in hosts_in_subnet if x != subnet_node] ) 59 | # edge_index.append( _complete_graph(hosts_in_subnet) ) 60 | 61 | # all subnets together 62 | # sub_index = np.flatnonzero( node_index[:, 1] == -1 ) 63 | # if len(sub_index) > 1: 64 | # edge_index.append( _complete_graph(sub_index) ) 65 | 66 | host_len = len(node_index) - len(subnets) 67 | 68 | for subnet_edge in subnet_graph: 69 | s_from = subnets.index(subnet_edge[0]) + host_len 70 | s_to = subnets.index(subnet_edge[1]) + host_len 71 | 72 | edge_index.append([(s_from, s_to), (s_to, s_from)]) 73 | 74 | edge_index = np.concatenate(edge_index).T 75 | return edge_index 76 | 77 | # v1 = nodes are connected to each other; v2 = they're not 78 | def convert_to_graph(s, subnet_graph, version=1): 79 | # hosts_discovered = s[:-1, HostVector._discovered_idx] == 1 80 | # host_feats = s[:-1][hosts_discovered] 81 | 82 | host_feats = s[:-1] # skip the result row 83 | host_index = np.array([HostVector(x).address for x in host_feats]) 84 | # host_index = self.host_index[hosts_discovered] 85 | 86 | subnets_discovered = list( {x[0] for x in host_index} ) 87 | subnet_index = np.array( [(x, -1) for x in subnets_discovered]) 88 | # subnet_index = self.subnet_index[subnets_discovered] 89 | subnet_feats = np.eye(max(subnets_discovered)+1, host_feats.shape[1])[subnets_discovered] 90 | 91 | # subnet 0 is internet, and will always be empty 92 | 93 | node_type = np.zeros( (len(host_feats) + len(subnet_feats), 1) ) 94 | node_type[len(host_feats):] = 1 # 0 for hosts; 1 for subnets 95 | 96 | node_feats = np.concatenate( [host_feats, subnet_feats] ) 97 | node_feats = np.concatenate( [node_type, node_feats], axis=1 ) 98 | 99 | node_index = np.concatenate( [host_index, subnet_index] ) 100 | pos_index = np.concatenate( [np.arange(len(host_index)), np.zeros(len(subnet_index))] ) # for positional encoding 101 | 102 | # create edge index 103 | if version == 1: 104 | edge_index = _gen_edge_index_v1(node_index, subnets_discovered, subnet_graph) 105 | elif version == 2: 106 | edge_index = _gen_edge_index_v2(node_index, subnets_discovered, subnet_graph) 107 | else: 108 | raise NotImplementedError() 109 | 110 | return node_feats, edge_index, node_index, pos_index 111 | 112 | # inspired from https://plotly.com/python/network-graphs/ 113 | def _plot(G): 114 | def get_edges(type): 115 | edge_x = [] 116 | edge_y = [] 117 | 118 | for edge in G.edges(): 119 | node_in = G.nodes[edge[0]] 120 | node_out = G.nodes[edge[1]] 121 | 122 | if type == 'subnet': # both have to be subnets 123 | if not (node_in['type'] == 'subnet' and node_out['type'] == 'subnet'): 124 | continue 125 | 126 | if type == 'node': # both can't be subnets 127 | if node_in['type'] == 'subnet' and node_out['type'] == 'subnet': 128 | continue 129 | 130 | x0, y0 = node_in['pos'] 131 | x1, y1 = node_out['pos'] 132 | edge_x.append(x0) 133 | edge_x.append(x1) 134 | edge_x.append(None) 135 | edge_y.append(y0) 136 | edge_y.append(y1) 137 | edge_y.append(None) 138 | 139 | return edge_x, edge_y 140 | 141 | node_edges = get_edges(type='node') 142 | subnet_edges = get_edges(type='subnet') 143 | 144 | edge_trace = go.Scatter( 145 | x=node_edges[0], y=node_edges[1], 146 | line=dict(width=1, color='black', dash='dash'), 147 | hoverinfo='none', 148 | mode='lines') 149 | 150 | subnet_trace = go.Scatter( 151 | x=subnet_edges[0], y=subnet_edges[1], 152 | line=dict(width=1, color='black'), 153 | hoverinfo='none', 154 | mode='lines') 155 | 156 | node_x = [] 157 | node_y = [] 158 | node_text = [] 159 | node_color = [] 160 | node_symbols = [] 161 | node_line_widths = [] 162 | 163 | for node_id, node in G.nodes.items(): 164 | x, y = node['pos'] 165 | node_x.append(x) 166 | node_y.append(y) 167 | 168 | node_text.append(f"{node['label']}") 169 | node_color.append(node['color']) 170 | node_symbols.append(node['symbol']) 171 | node_line_widths.append(node['line_width']) 172 | 173 | node_trace = go.Scatter( 174 | x=node_x, y=node_y, 175 | mode='markers+text', 176 | hoverinfo='text', 177 | marker=dict(showscale=False, color=node_color, symbol=node_symbols, size=40, line_width=node_line_widths, line_color="black"), 178 | text=node_text, 179 | # text_color="black", 180 | textposition="top center") 181 | 182 | fig = go.Figure(data=[edge_trace, subnet_trace, node_trace], 183 | layout=go.Layout( 184 | showlegend=False, 185 | hovermode='closest', 186 | margin=dict(b=0,l=0,r=0,t=0), 187 | xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), 188 | yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)) 189 | ) 190 | 191 | fig.update_layout( 192 | paper_bgcolor="rgba(0,0,0,0)", 193 | plot_bgcolor="rgba(0,0,0,0)", 194 | font=dict( 195 | # family="Courier New, monospace", 196 | size=25, 197 | color="black" 198 | ) 199 | ) 200 | 201 | return fig 202 | 203 | def _make_graph(s, a): 204 | node_feats, edge_index, node_index, pos_index = s 205 | 206 | G = nx.Graph() 207 | G.add_edges_from(edge_index.T) 208 | pos = nx.kamada_kawai_layout(G) 209 | 210 | a_target = (-1, -1) if a.__class__.__name__ == "TerminalAction" else a.target 211 | 212 | def get_host_conf(host_id): 213 | host_vec = HostVector(node_feats[host_id][1:]) 214 | 215 | running_services = [] 216 | for srv_name in HostVector.service_idx_map: 217 | if host_vec.is_running_service(srv_name): 218 | running_services.append(srv_name.split('_')[-1]) 219 | 220 | service_str = ", ".join(running_services) 221 | if service_str: 222 | service_str = "
" + service_str 223 | 224 | return service_str 225 | 226 | def get_host_string(i): 227 | node_idx = node_index[i] 228 | host_conf = get_host_conf(i) 229 | 230 | node_action = a.name if np.all(np.array(a_target) == np.array(node_idx)) else "" 231 | 232 | access_colors = ['black', 'orange', 'red'] 233 | node_str = f"{node_idx}" 234 | 235 | return f"{node_str} {node_action}{host_conf}" 236 | 237 | def is_host_sensitive(i): 238 | host_vec = HostVector(node_feats[i][1:]) 239 | return host_vec.value > 0 240 | 241 | def is_host_controlled(i): 242 | host_vec = HostVector(node_feats[i][1:]) 243 | return int(host_vec.access) 244 | 245 | def get_node_color(i): 246 | if node_index[i][1] == -1: 247 | return 'grey' 248 | else: 249 | return 'red' if is_host_sensitive(i) else 'white' 250 | 251 | 252 | node_labels = {i: f"Subnet {node_index[i][0]}" if node_index[i][1] == -1 else get_host_string(i) for i in G.nodes} 253 | node_types = {i: 'subnet' if node_index[i][1] == -1 else 'node' for i in G.nodes} 254 | node_colors = {i: get_node_color(i) for i in G.nodes} 255 | node_symbols = {i: 'triangle-up' if node_index[i][1] == -1 else 'circle' for i in G.nodes} 256 | node_line_widths = {i: 6.0 if np.all(np.array(a_target) == np.array(node_index[i])) else 1.0 for i in G.nodes} 257 | 258 | nx.set_node_attributes(G, pos, 'pos') 259 | nx.set_node_attributes(G, node_labels, 'label') 260 | nx.set_node_attributes(G, node_types, 'type') 261 | nx.set_node_attributes(G, node_colors, 'color') 262 | nx.set_node_attributes(G, node_symbols, 'symbol') 263 | nx.set_node_attributes(G, node_line_widths, 'line_width') 264 | 265 | return G 266 | 267 | def plot_network(s, subnet_graph, last_action): 268 | s_graph = convert_to_graph(s, subnet_graph, version=2) 269 | G = _make_graph(s_graph, last_action) 270 | fig = _plot(G) 271 | 272 | # use: 273 | # fig.show() 274 | 275 | # or: 276 | # pio.kaleido.scope.mathjax = None 277 | # fig.write_image(f"trace.pdf", width=1200, height=600, scale=1.0) 278 | 279 | return fig -------------------------------------------------------------------------------- /src/nasimemu/nasim/envs/state.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from .utils import AccessLevel 4 | from .host_vector import HostVector 5 | from .observation import Observation 6 | 7 | 8 | class State: 9 | """A state in the NASim Environment. 10 | 11 | Each row in the state tensor represents the state of a single host on the 12 | network. For details on host the state a single host is represented see 13 | :class:`HostVector` 14 | 15 | ... 16 | 17 | Attributes 18 | ---------- 19 | tensor : numpy.Array 20 | tensor representation of the state of network 21 | host_num_map : dict 22 | mapping from host address to host number (this is used 23 | to map host address to host row in the network tensor) 24 | """ 25 | 26 | def __init__(self, network_tensor, host_num_map): 27 | """ 28 | Parameters 29 | ---------- 30 | state_tensor : np.Array 31 | the tensor representation of the network state 32 | host_num_map : dict 33 | mapping from host address to host number (this is used 34 | to map host address to host row in the network tensor) 35 | """ 36 | self.tensor = network_tensor 37 | self.host_num_map = host_num_map 38 | 39 | @classmethod 40 | def tensorize(cls, network): 41 | h0 = network.hosts[(1, 0)] 42 | h0_vector = HostVector.vectorize(h0, network.address_space_bounds) 43 | tensor = np.zeros( 44 | (len(network.hosts), h0_vector.state_size), 45 | dtype=np.float32 46 | ) 47 | for host_addr, host in network.hosts.items(): 48 | host_num = network.host_num_map[host_addr] 49 | HostVector.vectorize( 50 | host, network.address_space_bounds, tensor[host_num] 51 | ) 52 | return cls(tensor, network.host_num_map) 53 | 54 | @classmethod 55 | def generate_initial_state(cls, network): 56 | cls.reset() 57 | state = cls.tensorize(network) 58 | return network.reset(state) 59 | 60 | @classmethod 61 | def generate_random_initial_state(cls, network): 62 | h0 = network.hosts[(1, 0)] 63 | h0_vector = HostVector.vectorize_random( 64 | h0, network.address_space_bounds 65 | ) 66 | tensor = np.zeros( 67 | (len(network.hosts), h0_vector.state_size), 68 | dtype=np.float32 69 | ) 70 | for host_addr, host in network.hosts.items(): 71 | host_num = network.host_num_map[host_addr] 72 | HostVector.vectorize_random( 73 | host, network.address_space_bounds, tensor[host_num] 74 | ) 75 | state = cls(tensor, network.host_num_map) 76 | # ensure host state set correctly 77 | return network.reset(state) 78 | 79 | @classmethod 80 | def from_numpy(cls, s_array, state_shape, host_num_map): 81 | if s_array.shape != state_shape: 82 | s_array = s_array.reshape(state_shape) 83 | return State(s_array, host_num_map) 84 | 85 | @classmethod 86 | def reset(cls): 87 | """Reset any class attributes for state """ 88 | HostVector.reset() 89 | 90 | @property 91 | def hosts(self): 92 | hosts = [] 93 | for host_addr in self.host_num_map: 94 | hosts.append((host_addr, self.get_host(host_addr))) 95 | return hosts 96 | 97 | def copy(self): 98 | new_tensor = np.copy(self.tensor) 99 | return State(new_tensor, self.host_num_map) 100 | 101 | def get_initial_observation(self, fully_obs): 102 | """Get the initial observation of network. 103 | 104 | Returns 105 | ------- 106 | Observation 107 | an observation object 108 | """ 109 | obs = Observation(self.shape()) 110 | if fully_obs: 111 | obs.from_state(self) 112 | return obs 113 | 114 | for host_addr, host in self.hosts: 115 | if not host.reachable: 116 | continue 117 | host_obs = host.observe(address=True, 118 | reachable=True, 119 | discovered=True) 120 | host_idx = self.get_host_idx(host_addr) 121 | obs.update_from_host(host_idx, host_obs) 122 | return obs 123 | 124 | def get_observation(self, action, action_result, fully_obs): 125 | """Get observation given last action and action result 126 | 127 | Parameters 128 | ---------- 129 | action : Action 130 | last action performed 131 | action_result : ActionResult 132 | observation from performing action 133 | fully_obs : bool 134 | whether problem is fully observable or not 135 | 136 | Returns 137 | ------- 138 | Observation 139 | an observation object 140 | """ 141 | obs = Observation(self.shape()) 142 | obs.from_action_result(action_result) 143 | if fully_obs: 144 | obs.from_state(self) 145 | return obs 146 | 147 | if action.is_noop(): 148 | return obs 149 | 150 | if not action_result.success: 151 | # action failed so no observation 152 | return obs 153 | 154 | t_idx, t_host = self.get_host_and_idx(action.target) 155 | obs_kwargs = dict( 156 | address=True, # must be true for success 157 | compromised=False, 158 | reachable=True, # must be true for success 159 | discovered=True, # must be true for success 160 | value=False, 161 | # discovery_value=False, # this is only added as needed 162 | services=False, 163 | processes=False, 164 | os=False, 165 | access=False 166 | ) 167 | if action.is_exploit(): 168 | # exploit action, so get all observations for host 169 | obs_kwargs["compromised"] = True 170 | obs_kwargs["services"] = True 171 | obs_kwargs["os"] = True 172 | obs_kwargs["access"] = True 173 | obs_kwargs["value"] = True 174 | elif action.is_privilege_escalation(): 175 | obs_kwargs["compromised"] = True 176 | obs_kwargs["access"] = True 177 | elif action.is_service_scan(): 178 | obs_kwargs["services"] = True 179 | elif action.is_os_scan(): 180 | obs_kwargs["os"] = True 181 | elif action.is_process_scan(): 182 | obs_kwargs["processes"] = True 183 | obs_kwargs["access"] = True 184 | elif action.is_subnet_scan(): 185 | for host_addr in action_result.discovered: 186 | discovered = action_result.discovered[host_addr] 187 | if not discovered: 188 | continue 189 | d_idx, d_host = self.get_host_and_idx(host_addr) 190 | newly_discovered = action_result.newly_discovered[host_addr] 191 | d_obs = d_host.observe( 192 | discovery_value=newly_discovered, **obs_kwargs 193 | ) 194 | obs.update_from_host(d_idx, d_obs) 195 | # this is for target host (where scan was performed on) 196 | obs_kwargs["compromised"] = True 197 | else: 198 | raise NotImplementedError(f"Action {action} not implemented") 199 | target_obs = t_host.observe(**obs_kwargs) 200 | obs.update_from_host(t_idx, target_obs) 201 | return obs 202 | 203 | def shape_flat(self): 204 | return self.numpy_flat().shape 205 | 206 | def shape(self): 207 | return self.tensor.shape 208 | 209 | def numpy_flat(self): 210 | return self.tensor.flatten() 211 | 212 | def numpy(self): 213 | return self.tensor 214 | 215 | def update_host(self, host_addr, host_vector): 216 | host_idx = self.host_num_map[host_addr] 217 | self.tensor[host_idx] = host_vector.vector 218 | 219 | def get_host(self, host_addr): 220 | host_idx = self.host_num_map[host_addr] 221 | return HostVector(self.tensor[host_idx]) 222 | 223 | def get_host_idx(self, host_addr): 224 | return self.host_num_map[host_addr] 225 | 226 | def get_host_and_idx(self, host_addr): 227 | host_idx = self.host_num_map[host_addr] 228 | return host_idx, HostVector(self.tensor[host_idx]) 229 | 230 | def host_reachable(self, host_addr): 231 | return self.get_host(host_addr).reachable 232 | 233 | def host_compromised(self, host_addr): 234 | return self.get_host(host_addr).compromised 235 | 236 | def host_discovered(self, host_addr): 237 | return self.get_host(host_addr).discovered 238 | 239 | def host_has_access(self, host_addr, access_level): 240 | return self.get_host(host_addr).access >= access_level 241 | 242 | def set_host_compromised(self, host_addr): 243 | self.get_host(host_addr).compromised = True 244 | 245 | def set_host_reachable(self, host_addr): 246 | self.get_host(host_addr).reachable = True 247 | 248 | def set_host_discovered(self, host_addr): 249 | self.get_host(host_addr).discovered = True 250 | 251 | def get_host_value(self, host_address): 252 | return self.hosts[host_address].get_value() 253 | 254 | def host_is_running_service(self, host_addr, service): 255 | return self.get_host(host_addr).is_running_service(service) 256 | 257 | def host_is_running_os(self, host_addr, os): 258 | return self.get_host(host_addr).is_running_os(os) 259 | 260 | def get_total_host_value(self): 261 | total_value = 0 262 | for host_addr in self.host_num_map: 263 | host = self.get_host(host_addr) 264 | total_value += host.value 265 | return total_value 266 | 267 | def state_size(self): 268 | return self.tensor.size 269 | 270 | def get_readable(self): 271 | host_obs = [] 272 | for host_addr in self.host_num_map: 273 | host = self.get_host(host_addr) 274 | readable_dict = host.readable() 275 | host_obs.append(readable_dict) 276 | return host_obs 277 | 278 | def __str__(self): 279 | output = "\n--- State ---\n" 280 | output += "Hosts:\n" 281 | for host in self.hosts: 282 | output += str(host) + "\n" 283 | return output 284 | 285 | def __hash__(self): 286 | return hash(str(self.tensor)) 287 | 288 | def __eq__(self, other): 289 | return np.array_equal(self.tensor, other.tensor) 290 | --------------------------------------------------------------------------------