initial commit - move from thesis
This commit is contained in:
commit
c1803514dc
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2020 Lin Pavelu
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Adaptive game assistant
|
||||||
|
This adaptive assistant was written for the remote testing of the adaptive version of the Captain Slovakistan game. It is licensed under MIT license.
|
||||||
|
|
||||||
|
It requires Python version at least 3.7, and the Python yaml package (`pip install -r python-requirements.txt`) to run. The cybersecurity game the assistant runs requires Vagrant and VirtualBox. Full instructions for installing are in the the [installation guide](../wiki/installation-guide.md).
|
||||||
|
|
||||||
|
The assistant loads configuration from the `levels.yml` file.
|
||||||
|
|
||||||
|
## Assistant usage
|
||||||
|
The adaptive game assistant is ran by running `./assistant.py` or `python assistant.py` in the project folder.
|
||||||
|
Essentially, it automatically runs the commands listed below in manual usage,
|
||||||
|
but all the user has to specify is that they want to advance to the next level.
|
||||||
|
|
||||||
|
Basic commands:
|
||||||
|
- (S)tart - starts the game from level 1.
|
||||||
|
- (N)ext - continues the game to the next level.
|
||||||
|
- (E)xit - properly ends the game and exits the assistant.
|
||||||
|
- (C)eck - Checks versions of all required apps.
|
||||||
|
- (H)elp - displays a full list of commands.
|
||||||
|
|
||||||
|
For more information about the game, including the levels' instructions, see the [game text](../wiki/game-text.md).
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
The assistant consists of a `Game` class, that represents the game itself, and a `game_loop()` function, which keeps an instance of a `Game` and translates inputs from the command line into method calls on the `Game` object.
|
||||||
|
|
||||||
|
The assistant is written with modularity in mind, so it should support other adaptive games, as long as they provide a correct `levels.yml` file, and has complete tagging in the Ansible playbooks.
|
||||||
|
|
||||||
|
### levels.yml
|
||||||
|
`levels.yml` is the configuration file that tells the assistant what possible levels are in the game, what tags to run to set up that level, and what machines need to be provisioned for that level.
|
||||||
|
|
||||||
|
Every line in the file represents a level, and each line must have the following format:
|
||||||
|
|
||||||
|
`level_name: {branch_name: [machines_to_provision]}`
|
||||||
|
|
||||||
|
## Manual usage without assistant
|
||||||
|
How to play the game without using the adaptive game assistant.
|
||||||
|
1. Run `ANSIBLE_ARGS='--tags "setup"' vagrant up`.
|
||||||
|
During the instantiation of `br` machine, you will be prompted for network
|
||||||
|
interface which connects you to internet (usually the first or second in the list).
|
||||||
|
2. When the player finishes a level, it's time to prepare the next one.
|
||||||
|
Some levels have multiple versions (level2a, level2b), some levels have only one version (level3).
|
||||||
|
The file `levels.yml` lists all possible levels,
|
||||||
|
along with the vagrant boxes that need an update for the level to be ran.
|
||||||
|
To prepare the next level, run `ANSIBLE_ARGS='--tags "<level>"' vagrant up <boxes> --provision`,
|
||||||
|
where `<level>` is a key from levels.yml, and `<boxes>` is the corresponding value from the same file.
|
||||||
|
(Examples: `ANSIBLE_ARGS='--tags "level2a"' vagrant up web attacker --provision`,
|
||||||
|
`ANSIBLE_ARGS='--tags "level3"' vagrant up web --provision`)
|
||||||
|
3. After the game is finished, run `vagrant destroy` to remove the game components and virtual machines.
|
||||||
|
The game can be reran after this step.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
### Set-up hangs on provisioning machine `br`
|
||||||
|
```
|
||||||
|
==> br: Running provisioner: ansible_local...
|
||||||
|
```
|
||||||
|
On Windows, the setup of the game rarely hangs on provisioning of the `br` machine.
|
||||||
|
When playing via the assistant, you will have to exist by pressing `Ctrl+C`. Then, you should kill the stuck process in the task manager (usually called similarly to `Ruby interpreter` or `Vagrant`)
|
||||||
|
Then you can resume the game by launching the assistant again, running `abort`, and then `start` again. (You can also attempt to make the assistant pick up where it ended before it got stuck, by not running `abort`, and running `start` only.)
|
|
@ -0,0 +1,311 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
import yaml # reads level configurations from a .yml file
|
||||||
|
|
||||||
|
|
||||||
|
class NoLevelFoundError(Exception):
|
||||||
|
"""Error thrown by the Game class when a nonexistant level is selected."""
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
|
class Game:
|
||||||
|
"""Class representing the adaptive game, its progress, setup+solving times.
|
||||||
|
|
||||||
|
An object of the class will remember the possible branching of the game,
|
||||||
|
it will keep track of the level and branch the player is on,
|
||||||
|
and it will save loading times and solving times of the player."""
|
||||||
|
|
||||||
|
def __init__(self, mapping_file, game_directory,
|
||||||
|
level_number=0, levelBranch=''):
|
||||||
|
"""Create an object of class Game.
|
||||||
|
|
||||||
|
The game itself is NOT started at this point. To start it, call
|
||||||
|
self.start_game() next.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
mapping_file
|
||||||
|
location of the .yml file with level mappings.
|
||||||
|
game_directory
|
||||||
|
directory with the game files (Vagrantfile, etc.)
|
||||||
|
levelNumber
|
||||||
|
number of level to start on
|
||||||
|
(useful when continuing a running game)
|
||||||
|
levelBranch
|
||||||
|
branch of level being started on
|
||||||
|
(useful when continuing a running game)"""
|
||||||
|
self.game_directory = game_directory
|
||||||
|
self.read_mapping(mapping_file)
|
||||||
|
self.load_times = []
|
||||||
|
self.solving_times = []
|
||||||
|
self.level_start_time = 0
|
||||||
|
self.level = level_number
|
||||||
|
self.branch = ''
|
||||||
|
|
||||||
|
def read_mapping(self, file):
|
||||||
|
"""Read a mapping of levels for the adaptive game from a .yml file."""
|
||||||
|
|
||||||
|
with open(file) as f:
|
||||||
|
self.level_mapping = yaml.load(f, Loader=yaml.FullLoader)
|
||||||
|
|
||||||
|
def start_game(self):
|
||||||
|
"""Start a new game, if there isn't one already in progress."""
|
||||||
|
|
||||||
|
if (self.level != 0):
|
||||||
|
return False
|
||||||
|
self.level = 1
|
||||||
|
start_time = time.time()
|
||||||
|
subprocess.run(["vagrant", "up"], cwd=self.game_directory,
|
||||||
|
env=dict(os.environ, ANSIBLE_ARGS='--tags \"setup\"'))
|
||||||
|
end_time = time.time()
|
||||||
|
load_time = int(end_time - start_time)
|
||||||
|
self.load_times.append(load_time)
|
||||||
|
self.level_start_time = time.time()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def next_level_exists(self):
|
||||||
|
"""Return true if next level exists."""
|
||||||
|
|
||||||
|
next_level = "level" + str(self.level + 1)
|
||||||
|
return next_level in self.level_mapping
|
||||||
|
|
||||||
|
def next_level_is_forked(self):
|
||||||
|
"""Return true if next level is forked."""
|
||||||
|
|
||||||
|
next_level = "level" + str(self.level + 1)
|
||||||
|
if next_level in self.level_mapping:
|
||||||
|
if len(self.level_mapping[next_level]) > 1:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def next_level(self, next_branch=''):
|
||||||
|
"""Advance the game to next level with branch `next_branch`.
|
||||||
|
|
||||||
|
Throws NoLevelFoundError if there is no such level present.
|
||||||
|
next_branch == '' is understood as no branch selected, a level
|
||||||
|
without possible branching."""
|
||||||
|
|
||||||
|
if (self.level == 0):
|
||||||
|
raise NoLevelFoundError("Can't continue, (S)tart the game first!")
|
||||||
|
if (not self.next_level_exists()):
|
||||||
|
raise NoLevelFoundError("No next level found! Perhaps you already finished the game?")
|
||||||
|
if (self.next_level_is_forked()):
|
||||||
|
next_level = "level" + str(self.level + 1)
|
||||||
|
if next_level + next_branch in self.level_mapping[next_level]:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise NoLevelFoundError("No branch called {} found.".format(next_branch))
|
||||||
|
elif (next_branch != ''):
|
||||||
|
raise NoLevelFoundError("Next level has no branch, but branch was given.")
|
||||||
|
self.solving_times.append(int(time.time() - self.level_start_time))
|
||||||
|
self.level += 1
|
||||||
|
self.branch = next_branch
|
||||||
|
self.levelname = "level" + str(self.level) + self.branch
|
||||||
|
self.boxes = self.level_mapping["level" + str(self.level)][self.levelname]
|
||||||
|
start_time = time.time()
|
||||||
|
subprocess.run(["vagrant", "up"] + self.boxes + ["--provision"],
|
||||||
|
cwd=self.game_directory,
|
||||||
|
env=dict(os.environ, ANSIBLE_ARGS='--tags \"' + self.levelname + '\"'))
|
||||||
|
end_time = time.time()
|
||||||
|
load_time = int(end_time - start_time)
|
||||||
|
self.load_times.append(load_time)
|
||||||
|
self.level_start_time = time.time()
|
||||||
|
|
||||||
|
def abort_game(self):
|
||||||
|
"""Abort game and reset attributes to their default state."""
|
||||||
|
|
||||||
|
subprocess.run(["vagrant", "destroy", "-f"], cwd=self.game_directory)
|
||||||
|
self.level = 0
|
||||||
|
self.branch = ''
|
||||||
|
self.load_times = []
|
||||||
|
self.solving_times = []
|
||||||
|
self.level_start_time = 0
|
||||||
|
|
||||||
|
# # # METHODS THAT OUTPUT INTO STDOUT # # #
|
||||||
|
def print_time(self, load_time, concise=False):
|
||||||
|
"""Print time in minutes and seconds.
|
||||||
|
|
||||||
|
If concise is True, prints only numbers and letters."""
|
||||||
|
|
||||||
|
if (load_time < 60):
|
||||||
|
if concise:
|
||||||
|
print("{}s".format(load_time))
|
||||||
|
else:
|
||||||
|
print("Time elapsed: {} seconds.".format(load_time))
|
||||||
|
else:
|
||||||
|
minutes = load_time // 60
|
||||||
|
seconds = load_time % 60
|
||||||
|
if concise:
|
||||||
|
print("{}m{}s".format(minutes, seconds))
|
||||||
|
else:
|
||||||
|
print("Time elapsed: {} minutes, {} seconds.".format(minutes, seconds))
|
||||||
|
|
||||||
|
def print_info(self):
|
||||||
|
"""Print info about the game in a human-readable way."""
|
||||||
|
|
||||||
|
if (self.level == 0):
|
||||||
|
print("Game is not in progress.")
|
||||||
|
else:
|
||||||
|
print("Game in progress. Level:{} Branch:{}".format(self.level, self.branch))
|
||||||
|
print("Setup times:")
|
||||||
|
i = 1
|
||||||
|
for load_time in self.load_times:
|
||||||
|
print("Level {} : ".format(i), end="")
|
||||||
|
self.print_time(load_time, True)
|
||||||
|
i += 1
|
||||||
|
i = 1
|
||||||
|
print("Solving times:")
|
||||||
|
for solving_time in self.solving_times:
|
||||||
|
print("Level {} : ".format(i), end="")
|
||||||
|
self.print_time(solving_time, True)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
|
||||||
|
def print_help():
|
||||||
|
"""Print list of arguments that game_loop accepts."""
|
||||||
|
|
||||||
|
print("Functions that control the game:")
|
||||||
|
print("(A)bort - destroys all VMs, resets progress to level 0.")
|
||||||
|
print("(E)xit - aborts run, then exits the assistant.")
|
||||||
|
print("(S)tart - starts a new run of the adaptive game, if one isn't in progress.")
|
||||||
|
print("(N)ext - advances to the next level. Asks for branch when applicable.")
|
||||||
|
print("(I)nfo - displays info about current run - Level number and name.")
|
||||||
|
print("Helper functions:")
|
||||||
|
print("(H)elp - explains all commands on the screen.")
|
||||||
|
print("(C)heck - checks if prerequisites to run the game are installed.")
|
||||||
|
|
||||||
|
|
||||||
|
def check_prerequisites():
|
||||||
|
"""Check for the right version of required tools and print the result.
|
||||||
|
|
||||||
|
VirtualBox check fails on windows even when VirtualBox is present."""
|
||||||
|
|
||||||
|
print("checking Python version:")
|
||||||
|
found = True
|
||||||
|
try:
|
||||||
|
version = subprocess.run(["python", "--version"], capture_output=True, text=True)
|
||||||
|
except FileNotFoundError:
|
||||||
|
found = False
|
||||||
|
print("NOK, Python not found.")
|
||||||
|
if found:
|
||||||
|
version_number = version.stdout.split(" ", 1)[1].split(".")
|
||||||
|
if (version_number[0] == "3"):
|
||||||
|
if (int(version_number[1]) > 7):
|
||||||
|
print("OK, Python version higher than 3.7.")
|
||||||
|
else:
|
||||||
|
print("NOK, Python version lower than 3.7.")
|
||||||
|
print("assistant may not function correctly.")
|
||||||
|
else:
|
||||||
|
print("NOK, Python 3 not detected.")
|
||||||
|
|
||||||
|
found = True
|
||||||
|
print("checking Vagrant version:")
|
||||||
|
try:
|
||||||
|
version = subprocess.run(["vagrant", "--version"], capture_output=True, text=True)
|
||||||
|
except FileNotFoundError:
|
||||||
|
found = False
|
||||||
|
print("NOK, Vagrant not found.")
|
||||||
|
if found:
|
||||||
|
version_number = version.stdout.split(" ", 1)[1].split(".")
|
||||||
|
if (version_number[0] == "2"):
|
||||||
|
if (int(version_number[1]) > 1):
|
||||||
|
print("OK, Vagrant version higher than 2.2.")
|
||||||
|
else:
|
||||||
|
print("NOK, Vagrant version lower than 2.2.")
|
||||||
|
else:
|
||||||
|
print("NOK, Vagrant 2 not detected.")
|
||||||
|
|
||||||
|
found = True
|
||||||
|
print("checking Virtualbox version:")
|
||||||
|
try:
|
||||||
|
version = subprocess.run(["vboxmanage", "--version"], capture_output=True, text=True)
|
||||||
|
except FileNotFoundError:
|
||||||
|
found = False
|
||||||
|
print("Virtualbox not found.")
|
||||||
|
print("If you are on Windows, this is probably OK.")
|
||||||
|
print("(You can double check VirtualBox version yourself to be sure)")
|
||||||
|
print("If you are on Linux, you don't have VirtualBox installed, NOK.")
|
||||||
|
if found:
|
||||||
|
version_number = version.stdout.split(".")
|
||||||
|
if (int(version_number[0]) > 5):
|
||||||
|
print("OK, VirtualBox version higher than 5 detected.")
|
||||||
|
else:
|
||||||
|
print("NOK, VirtualBox version lower than 6 detected.")
|
||||||
|
|
||||||
|
|
||||||
|
def game_loop():
|
||||||
|
"""Interactively assist the player with playing the game.
|
||||||
|
|
||||||
|
Transform inputs from user into method calls.
|
||||||
|
|
||||||
|
Possible inputs
|
||||||
|
---------------
|
||||||
|
Functions that control the game:
|
||||||
|
(A)bort - destroys all VMs, resets progress to level 0.
|
||||||
|
(E)xit - aborts run, then exits the assistant.
|
||||||
|
(S)tart - starts a new run of the adaptive game, if one isn't in progress.
|
||||||
|
(N)ext - advances to the next level. Asks for branch when applicable.
|
||||||
|
(I)nfo - displays info about current run - Level number and name.
|
||||||
|
Helper functions:
|
||||||
|
(H)elp - explains all commands on the screen.
|
||||||
|
(C)heck - checks if prerequisites to run the game are installed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
game = Game("levels.yml", "../game")
|
||||||
|
print("Welcome to the adaptive game assistant.")
|
||||||
|
print("Basic commands are:")
|
||||||
|
print("(S)tart, (N)ext, (H)elp, (C)heck, (E)xit")
|
||||||
|
while(True):
|
||||||
|
print("Waiting for next input:")
|
||||||
|
command = input()
|
||||||
|
command = command.lower()
|
||||||
|
if ((command == "a") or (command == "abort")):
|
||||||
|
print("Aborting game, deleting all VMs.")
|
||||||
|
game.abort_game()
|
||||||
|
print("Game aborted, progress reset, VMs deleted.")
|
||||||
|
elif ((command == "e") or (command == "exit")):
|
||||||
|
print("Going to exit assistant, abort game and delete VMs.")
|
||||||
|
game.abort_game()
|
||||||
|
print("Exiting...")
|
||||||
|
return
|
||||||
|
elif ((command == "s") or (command == "start")):
|
||||||
|
print("Trying to start the game.")
|
||||||
|
print("The initial setup may take a while, up to 20 minutes.")
|
||||||
|
if (game.start_game()):
|
||||||
|
print("If you do not see any error messages above,")
|
||||||
|
print("then the game was started succesfully!")
|
||||||
|
print("You can start playing now.")
|
||||||
|
else:
|
||||||
|
print("Game was not started, it's already in progress!")
|
||||||
|
print("To start over, please run `abort` first.")
|
||||||
|
elif ((command == "n") or (command == "next")):
|
||||||
|
try:
|
||||||
|
print("Going to set up level {}".format(game.level + 1))
|
||||||
|
if game.next_level_is_forked():
|
||||||
|
print("Choose level's branch:")
|
||||||
|
branch = input()
|
||||||
|
branch = branch.lower() # just lowered, no sanitization
|
||||||
|
game.next_level(branch)
|
||||||
|
else:
|
||||||
|
game.next_level()
|
||||||
|
print("Level deployed. You can continue playing.")
|
||||||
|
except NoLevelFoundError as err:
|
||||||
|
print("Error encountered: {}".format(err))
|
||||||
|
elif ((command == "i") or (command == "info")):
|
||||||
|
game.print_info()
|
||||||
|
elif ((command == "h") or (command == "help")):
|
||||||
|
print_help()
|
||||||
|
elif ((command == "c") or (command == "check")):
|
||||||
|
check_prerequisites()
|
||||||
|
else:
|
||||||
|
print("Unknown command. Enter another command or try (H)elp.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
game_loop()
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
# level_name: {branch_name: [machines_to_provision]}
|
||||||
|
level2: {level2a: [web], level2b:[web]}
|
||||||
|
level3: {level3: [web, attacker]}
|
||||||
|
level4: {level4a: [web], level4b:[web, attacker]}
|
||||||
|
level5: {level5: [web]}
|
||||||
|
...
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Python installation requirements
|
||||||
|
pyyaml
|
Reference in New Issue