1
0
Fork 0

initial commit - move from thesis

This commit is contained in:
Lili (Tlapka) 2021-03-16 14:11:29 +01:00
commit c1803514dc
5 changed files with 396 additions and 0 deletions

19
LICENSE Normal file
View File

@ -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.

57
README.md Normal file
View File

@ -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.)

311
assistant.py Executable file
View File

@ -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()

7
levels.yml Normal file
View File

@ -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]}
...

2
python-requirements.txt Normal file
View File

@ -0,0 +1,2 @@
# Python installation requirements
pyyaml