Refactored to run series of tests, improved configurations, file structure

This commit is contained in:
VakarisZ 2019-09-11 12:39:28 +03:00
parent 0ee4445ca1
commit 36b6752827
18 changed files with 195 additions and 137 deletions

View File

@ -6,6 +6,6 @@ def pytest_addoption(parser):
help="Specify the Monkey Island address (host+port).")
@pytest.fixture
@pytest.fixture(scope='module')
def island(request):
request.cls.island = request.config.getoption("--island")
return request.config.getoption("--island")

View File

@ -21,7 +21,7 @@
"general": {
"blocked_ips": [],
"depth": 2,
"local_network_scan": true,
"local_network_scan": false,
"subnet_scan_list": [
"10.2.2.4",
"10.2.2.5"
@ -107,7 +107,7 @@
"exploit_ssh_keys": []
},
"general": {
"keep_tunnel_open_time": 60,
"keep_tunnel_open_time": 50,
"monkey_dir_name": "monkey_dir",
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
},
@ -181,4 +181,4 @@
]
}
}
}
}

View File

@ -118,7 +118,7 @@
"exploit_ssh_keys": []
},
"general": {
"keep_tunnel_open_time": 60,
"keep_tunnel_open_time": 50,
"monkey_dir_name": "monkey_dir",
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
},
@ -192,4 +192,4 @@
]
}
}
}
}

View File

@ -106,7 +106,7 @@
"exploit_ssh_keys": []
},
"general": {
"keep_tunnel_open_time": 60,
"keep_tunnel_open_time": 50,
"monkey_dir_name": "monkey_dir",
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
},
@ -180,4 +180,4 @@
]
}
}
}
}

View File

@ -106,7 +106,7 @@
"exploit_ssh_keys": []
},
"general": {
"keep_tunnel_open_time": 60,
"keep_tunnel_open_time": 50,
"monkey_dir_name": "monkey_dir",
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
},
@ -180,4 +180,4 @@
]
}
}
}
}

View File

@ -101,14 +101,11 @@
},
"exploits": {
"exploit_lm_hash_list": [],
"exploit_ntlm_hash_list": [
"5da0889ea2081aa79f6852294cba4a5e",
"5da0889ea2081aa79f6852294cba4a5e"
],
"exploit_ntlm_hash_list": [],
"exploit_ssh_keys": []
},
"general": {
"keep_tunnel_open_time": 60,
"keep_tunnel_open_time": 50,
"monkey_dir_name": "monkey_dir",
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
},
@ -182,4 +179,4 @@
]
}
}
}
}

View File

@ -112,23 +112,10 @@
"exploits": {
"exploit_lm_hash_list": [],
"exploit_ntlm_hash_list": [],
"exploit_ssh_keys": [
{
"ip": "10.2.2.41",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyH0k1LOILDTVli5NlqcvRdoRc2aMn5I5ZhJsnBNuzB28D6Fd\nAEbjDn/v+dPK58L4WGoGMpHqk47mNDgdTIkfP5BBgbuQpBUmrsCZn8QVRpqZ3ESC\nXsnMrOjrYRqTelquGWR9xJvIJwNz3UbME2c8SYOPc3tHsINyn8Tt2ssA8L9KjcTe\n6CzNpbCNbZ6Q3o7/isYP79ogiFY+VHK3rtBY17aG9bDx5vce8RoIr463u/+a+jYX\nPuzZgndTtO3EPwq4Ti1pydpuJo9PYh1iY6RP3XPMYNwpoKToYzyESeqqwolmz+nh\nEh/rQtuwwW7043IM+62w9UPkHWv28pqDZxBxhwIDAQABAoIBAGA1/ei8xwo/yIer\nbLxxOnRQ87LncXBaIYVkLg6wHKmDU25Ex3aMjgW1S5oeEu8pVzhGmPbHo0RwfPRu\nQVErNH2yYl05f23eYJPYBWDwHi2ln1Re5BlMyhXoKJyOvlsnDQlOejRRdbmTJJT5\nlpFxJzM4GS0X6g1A507YmDQ42xisOkmL/Wsv/t9/GiE9P6h0I1bNmXzSy8sDZwea\nNDe09U+rfuIkh2tO+nEzWs13AG3CxV9YlK4vMK7A0KiWF8LPvrbBegEm5VG+qrJ2\nsxoDkCBc5DV6QRyLU1SIyDIRIR2J0gTgfLDSbqNp0qm+Zby2o3V/q26bvrWWwP9a\nU/W2vJECgYEA5jK52pUMmriCiWNgQOiyOGx5OfHo6gRomiiiALivaMhNN3mm8CFI\nuIXMjU1V0BoHXCW8ciMOAeXl72rX/XVC+/E3GJQFCtBHJQ6tPNOOnWcxR/Ldwvxd\nsBz8Wx50MlxvbrxqtzTn+VmVnExKskwsZGI/GDPotPo7QKcBJUsGfhsCgYEA3vXx\ncyG805RsJH/J54cg+cHW5xDn6YNuHwbVdB4FWfi184oDDxtPT84XF1JA3dN3gwJF\nSfO1kNwpNK0C58evJA+6rZfUps/HOcQqFPvzCUhkLeZD5QgaOTQZBKndXYgeXkJD\ntpN+kjhCdxWN40N6FAMtLUYTbaQdTUHSBuXzoQUCgYEAgwXgTw+DCxV2ByjvAkLw\nHblwDpEoVvqHZyc1fl+gR22qtaaiZA8tywks8khQTZBjHAnGhth5Ao+OHoWbxoHV\nzHzxNSYa8Jq3w9nktLhddi3kGOWdX3ww/yqgYGSnEnsWWdsYioqsdnqM81dhNLay\nlbht3SK+kzPSQexMdKONYH0CgYAS1lKk+Ie8lICigM1tK0SE9XSTpyEA4KLQKkKk\ngdjP5ixxPArQHu2Pf4kB5mgmlbQ2NF3oRpfjekZc9fUV4hARCucpvXcw9MMPRVyM\n01CQSzZzjk3ULuAQTy+B7lwOh+6Q5iZUaZe7ANfUudR4C/5nbHFHrvD7RW9YVKRL\nAuiXhQKBgDMFeZRfu/dhTdVQ9XZigOWvkeXYxxoloiIHIg3ByZwEAlH/RnlA0M1Z\nOaLt/Q1KNh2UDKkstfOAJ1FdqLm3JU0Hqx/D8dpvTUQBkqoMf8U1WQC2WVmlpmUv\ndrIj1d5/r2N1Cxorx0IbVWsW7WPVM/lVyBU7+2QsKoI5YIervsJY\n-----END RSA PRIVATE KEY-----\n",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDIfSTUs4gsNNWWLk2Wpy9F2hFzZoyfkjlmEmycE27MHbwPoV0ARuMOf+/508rnwvhYagYykeqTjuY0OB1MiR8/kEGBu5CkFSauwJmfxBVGmpncRIJeycys6OthGpN6Wq4ZZH3Em8gnA3PdRswTZzxJg49ze0ewg3KfxO3aywDwv0qNxN7oLM2lsI1tnpDejv+Kxg/v2iCIVj5Ucreu0FjXtob1sPHm9x7xGgivjre7/5r6Nhc+7NmCd1O07cQ/CrhOLWnJ2m4mj09iHWJjpE/dc8xg3CmgpOhjPIRJ6qrCiWbP6eESH+tC27DBbvTjcgz7rbD1Q+Qda/bymoNnEHGH m0nk3y@sshkeys-11\n",
"user": "m0nk3y"
},
{
"ip": "10.2.2.42",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA5fmqwJV/NZOThZ0B0SXJ7jz+xp2zvVI+DR141JpEJGAm8xP8\nCMmDACkGVDZfwQQEWsmG6WHsRJnBTm1jawUbhgD8EGYNp9sj8G4oW6GvThzzlZvp\nABgtSyWtT6CLBoF7t3/wgFXfAaA33lTg/Ht+bXBMaAZiHyRlexPjmtfhu1Hw44cM\nBuxJwA31OqnO0OPKOh6wW0OWta9BsJnFBckEve1y2Bf2SOnFdlAEV9tNWwGt/AK8\nMAEpgwEZXISrqOucjn7YGEzfZ+Ygjq4/M3FGE31ttrTgERqAPchd1Cp6da6mlYtu\nzx35spV2kxf2ydYHYOOgvS8cCIIk7GIANZztmQIDAQABAoIBAQCAOcXQHUrRV5hw\nbkt+DuDZWd8AZRu7mqiIbX9aoy0NTyNIc0MaryjApR8nQy3+k7vN1wHHDNdMpKIY\nlXtSR4XCILhqeExy5AfU3cbY2HzDQ/c42rZ+W/ydIsPQWwZJOVb+yHITTE3MPUYJ\nTDAp9r1WTb/8XFrHXGbMyhkk0+vDoezj+FDS0YbFImt2iFgC7wgDjuoG5CnlW09/\n/r2bfH/qKKvs9sDwCypVqH5bki1VyQLRfdrCgPisBnhBAj8OBFN+IN5L3P+Zv5UI\nwyZA34RETijHb158vAtIfz2LT0Gin6CNGZY+QOOjM1hCevgrbLeD13Bc9Oog7367\nlm10eMKBAoGBAPUPrbpgA/rg77YrL7uBQhy13HtrRnxW4AjtDs/dEelXlF/Zdfi6\nzukVkb6PaYVPAOC58E21gOJNDYXUdwkbGrJSIDr6rfb9AeEnGIcyDPn36roLjGBn\nJO3Ikr4AhycjbQVVdvo4VY5baCtHNonbn4VFeb+ThszMOQWci4djPGWJAoGBAPA9\nmbboApqV/zoy4PAukFFWOUxb+3GxwcywlVYIuAk3DfM5zJuLgxV70KJ/Z6ppmTzF\n+uPkydTFQv/aLA3IiUioPijl2/l1HmspHpzQztdJaMHhFolKCB+X7wa7QlSFKKyE\ngT8dV8mPsW2HMoNv/yfVQ7lv2M9t4CKhzgssnFORAoGAbeqJXqYAQv+vJQM71ptE\nXwJHEWhtZgnFVNuXIC6lAQdSOqecHWMUuUD+bP2AM9Xaq/FaUlCNrXMoFJXWilQI\n5mClqi6T5stWk3lorAMMBPZo5ueVIAxDaQ/kmao89JYUKSdLRTINVduz3m2DjdPf\nOfSOhhoShTs4fEbZ4nDlPxkCgYEAhz1WM/YFyToZVIzhvbOowHD2jnrVYJp9i5n4\nZ1c/KsjYTIzEFugBoe1ydJeeJvuNLP4Sj4ny6Jknb1pFJHk4dzNm0qUbISICPrcG\nKacOWXlUxJfOlPic/BQOlc6Ct4vCauOo0nvVOH1wl0Tddcpta+INXu2MsrCa1UQa\n9oVld1ECgYADhllIDq3mkM4y58BSIOQ7uOKqbOgTyOjUCS2cFtYXtv6UY/OxFVxJ\n9soergMdd1uDOOwv96Yu9aNzgqjXFixb3Gyk+xh1viOpLrpbCUgg6j20IHcv8pwS\nhsp2wHDCuS8cKK9E8RJ3YYNgLh5uH1J+GhzSix1V8sizT0Y1UPOCDQ==\n-----END RSA PRIVATE KEY-----\n",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDl+arAlX81k5OFnQHRJcnuPP7GnbO9Uj4NHXjUmkQkYCbzE/wIyYMAKQZUNl/BBARayYbpYexEmcFObWNrBRuGAPwQZg2n2yPwbihboa9OHPOVm+kAGC1LJa1PoIsGgXu3f/CAVd8BoDfeVOD8e35tcExoBmIfJGV7E+Oa1+G7UfDjhwwG7EnADfU6qc7Q48o6HrBbQ5a1r0GwmcUFyQS97XLYF/ZI6cV2UARX201bAa38ArwwASmDARlchKuo65yOftgYTN9n5iCOrj8zcUYTfW22tOARGoA9yF3UKnp1rqaVi27PHfmylXaTF/bJ1gdg46C9LxwIgiTsYgA1nO2Z m0nk3y@sshkeys-12\n",
"user": "m0nk3y"
}
]
"exploit_ssh_keys": []
},
"general": {
"keep_tunnel_open_time": 60,
"keep_tunnel_open_time": 50,
"monkey_dir_name": "monkey_dir",
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
},
@ -202,4 +189,4 @@
]
}
}
}
}

View File

@ -116,7 +116,7 @@
"exploit_ssh_keys": []
},
"general": {
"keep_tunnel_open_time": 60,
"keep_tunnel_open_time": 50,
"monkey_dir_name": "monkey_dir",
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
},
@ -190,4 +190,4 @@
]
}
}
}
}

View File

@ -107,7 +107,7 @@
"exploit_ssh_keys": []
},
"general": {
"keep_tunnel_open_time": 60,
"keep_tunnel_open_time": 50,
"monkey_dir_name": "monkey_dir",
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
},
@ -181,4 +181,4 @@
]
}
}
}
}

View File

@ -109,13 +109,11 @@
},
"exploits": {
"exploit_lm_hash_list": [],
"exploit_ntlm_hash_list": [
"5da0889ea2081aa79f6852294cba4a5e"
],
"exploit_ntlm_hash_list": [],
"exploit_ssh_keys": []
},
"general": {
"keep_tunnel_open_time": 60,
"keep_tunnel_open_time": 50,
"monkey_dir_name": "monkey_dir",
"singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}"
},
@ -189,4 +187,4 @@
]
}
}
}
}

View File

@ -1,106 +1,89 @@
import unittest
from time import sleep, time
from time import sleep
import pytest
from envs.monkey_zoo.blackbox.monkey_island_client import MonkeyIslandClient
from envs.monkey_zoo.blackbox.utils.monkey_island_client import MonkeyIslandClient
from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import CommunicationAnalyzer
from envs.monkey_zoo.blackbox.island_config_parser import IslandConfigParser
from envs.monkey_zoo.blackbox.gcp_machine_handlers import GCPHandler
from envs.monkey_zoo.blackbox.utils.island_config_parser import IslandConfigParser
from envs.monkey_zoo.blackbox.utils import gcp_machine_handlers
from envs.monkey_zoo.blackbox.tests.basic_test import BasicTest
DEFAULT_TIMEOUT_SECONDS = 4 * 60 # 4 minutes
DELAY_BETWEEN_TESTS = 10
GCP_TEST_MACHINE_LIST = ['sshkeys-11', 'sshkeys-12', 'elastic-4', 'elastic-5', 'haddop-2-v3', 'hadoop-3', 'mssql-16',
'mimikatz-14', 'mimikatz-15', 'final-test-struts2-23', 'final-test-struts2-24',
'tunneling-9', 'tunneling-10', 'tunneling-11', 'weblogic-18', 'weblogic-19', 'shellshock-8']
MACHINE_BOOT_TIME_SECONDS = 20
TEST_TIME_SECONDS = 70
DELAY_BETWEEN_TESTS = 1
@pytest.fixture(autouse=True, scope='session')
def GCPHandler(request):
GCPHandler = gcp_machine_handlers.GCPHandler()
#GCPHandler.start_machines(" ".join(GCP_TEST_MACHINE_LIST))
def fin():
pass
# GCPHandler.stop_machines(" ".join(GCP_TEST_MACHINE_LIST))
request.addfinalizer(fin)
class BlackBoxTest(object):
def __init__(self, name, island_client, island_config, analyzers, timeout=TEST_TIME_SECONDS):
self.name = name
self.island_client = island_client
self.island_config = island_config
self.analyzers = analyzers
self.timeout = timeout
def run(self):
self.island_client.import_config(self.island_config)
self.island_client.run_monkey_local()
self.test_until_timeout()
self.island_client.reset_env()
def test_until_timeout(self):
timer = TestTimer(self.timeout)
while not timer.timed_out():
if self.analyzers_pass():
self.log_success(timer)
return
sleep(DELAY_BETWEEN_TESTS)
self.log_failure(timer)
assert False
def log_success(self, timer):
print(self.get_analyzer_logs())
print("{} test passed, time taken: {:.1f} seconds.".format(self.name, timer.get_time_taken()))
def log_failure(self, timer):
print(self.get_analyzer_logs())
print("{} test failed because of timeout. Time taken: {:.1f} seconds.".format(self.name,
timer.get_time_taken()))
def analyzers_pass(self):
for analyzer in self.analyzers:
if not analyzer.analyze_test_results():
return False
return True
def get_analyzer_logs(self):
log = ""
for analyzer in self.analyzers:
log += "\n"+analyzer.log.get_contents()
return log
@pytest.fixture(scope='class')
def island_client(island):
island_client_object = MonkeyIslandClient(island)
yield island_client_object
class TestTimer(object):
def __init__(self, timeout):
self.timeout_time = TestTimer.get_timeout_time(timeout)
self.start_time = time()
def timed_out(self):
return time() > self.timeout_time
def get_time_taken(self):
return time() - self.start_time
@staticmethod
def get_timeout_time(timeout):
return time() + timeout
@pytest.mark.usefixtures("island")
@pytest.mark.usefixtures('island_client')
# noinspection PyUnresolvedReferences
class TestMonkeyBlackbox(unittest.TestCase):
class TestMonkeyBlackbox(object):
def setUp(self):
self.GCPHandler = GCPHandler()
self.island_client = MonkeyIslandClient(self.island)
self.GCPHandler.start_machines("sshkeys-11 sshkeys-12")
TestMonkeyBlackbox.wait_for_machine_boot()
def tearDown(self):
self.GCPHandler.stop_machines("sshkeys-11 sshkeys-12")
print("Killing all GCP machines...")
def test_server_online(self):
assert self.island_client.get_api_status() is not None
def test_ssh_exec(self):
conf_file_name = 'SSH.conf'
config_parser = IslandConfigParser(conf_file_name)
analyzer = CommunicationAnalyzer(self.island_client, config_parser.get_ips_of_targets())
BlackBoxTest("SSH test", self.island_client, config_parser.config_raw, [analyzer]).run()
def run_basic_test(self, island_client, conf_filename, test_name, timeout_in_seconds=DEFAULT_TIMEOUT_SECONDS):
TestMonkeyBlackbox.wait_between_tests()
config_parser = IslandConfigParser(conf_filename)
analyzer = CommunicationAnalyzer(island_client, config_parser.get_ips_of_targets())
BasicTest(test_name,
island_client,
config_parser.config_raw,
[analyzer],
timeout_in_seconds).run()
@staticmethod
def wait_for_machine_boot(time=MACHINE_BOOT_TIME_SECONDS):
print("Waiting for machines to fully boot up({:.0f} seconds).".format(time))
sleep(time)
def wait_between_tests():
print("Waiting for ({:.0f} seconds) for old monkey's to die or GCP machines to boot up.".format(DELAY_BETWEEN_TESTS))
sleep(DELAY_BETWEEN_TESTS)
"""
def test_server_online(self, island_client):
assert island_client.get_api_status() is not None
def test_ssh_exploiter(self, island_client):
self.run_basic_test(island_client, "SSH.conf", "SSH exploiter and keys")
def test_hadoop_exploiter(self, island_client):
self.run_basic_test(island_client, "HADOOP.conf", "Hadoop exploiter")
def test_mssql_exploiter(self, island_client):
self.run_basic_test(island_client, "MSSQL.conf", "MSSQL exploiter")
"""
def test_smb_and_mimikatz_exploiters(self, island_client):
self.run_basic_test(island_client, "SMB_MIMIKATZ.conf", "SMB exploiter, mimikatz")
"""
def test_elastic_exploiter(self, island_client):
self.run_basic_test(island_client, "ELASTIC.conf", "Elastic exploiter", 180)
def test_struts_exploiter(self, island_client):
self.run_basic_test(island_client, "STRUTS2.conf", "Strtuts2 exploiter")
def test_weblogic_exploiter(self, island_client):
self.run_basic_test(island_client, "WEBLOGIC.conf", "Weblogic exploiter")
def test_shellshock_exploiter(self, island_client):
self.run_basic_test(island_client, "SHELLSHOCK.conf", "Shellschock exploiter")
def test_tunneling(self, island_client):
self.run_basic_test(island_client, "TUNNELING.conf", "Tunneling exploiter")
def test_wmi_exploiter(self, island_client):
self.run_basic_test(island_client, "WMI_MIMIKATZ.conf", "WMI exploiter, mimikatz")
"""

View File

@ -0,0 +1,55 @@
from time import sleep
from envs.monkey_zoo.blackbox.utils.test_timer import TestTimer
DELAY_BETWEEN_ANALYSIS = 1
class BasicTest(object):
def __init__(self, name, island_client, island_config, analyzers, timeout):
self.name = name
self.island_client = island_client
self.island_config = island_config
self.analyzers = analyzers
self.timeout = timeout
def run(self):
self.island_client.import_config(self.island_config)
try:
self.island_client.run_monkey_local()
self.test_until_timeout()
finally:
self.island_client.kill_all_monkeys()
self.island_client.reset_env()
def test_until_timeout(self):
timer = TestTimer(self.timeout)
while not timer.timed_out():
if self.all_analyzers_pass():
self.log_success(timer)
return
sleep(DELAY_BETWEEN_ANALYSIS)
self.log_failure(timer)
assert False
def log_success(self, timer):
print(self.get_analyzer_logs())
print("{} test passed, time taken: {:.1f} seconds.".format(self.name, timer.get_time_taken()))
def log_failure(self, timer):
print(self.get_analyzer_logs())
print("{} test failed because of timeout. Time taken: {:.1f} seconds.".format(self.name,
timer.get_time_taken()))
def all_analyzers_pass(self):
for analyzer in self.analyzers:
if not analyzer.analyze_test_results():
return False
return True
def get_analyzer_logs(self):
log = ""
for analyzer in self.analyzers:
log += "\n"+analyzer.log.get_contents()
return log

View File

@ -13,4 +13,6 @@ class IslandConfigParser(object):
@staticmethod
def get_conf_file_path(conf_file_name):
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "island_configs", conf_file_name)
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"island_configs",
conf_file_name)

View File

@ -10,7 +10,14 @@ NO_AUTH_CREDS = '55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d206
class MonkeyIslandClient(object):
def __init__(self, server_address):
self.addr = "https://{IP}/".format(IP=server_address)
self.token = self.get_jwt_from_server()
self.token = self.try_get_jwt_from_server()
def try_get_jwt_from_server(self):
try:
return self.get_jwt_from_server()
except requests.ConnectionError:
print("Unable to connect to island, aborting!")
assert False
def get_jwt_from_server(self):
resp = requests.post(self.addr + "api/auth",
@ -43,12 +50,24 @@ class MonkeyIslandClient(object):
_ = self.request_post("api/configuration/island", data=config_contents)
def run_monkey_local(self):
if self.request_post_json("api/local-monkey", dict_data={"action": "run"}).ok:
response = self.request_post_json("api/local-monkey", dict_data={"action": "run"})
if MonkeyIslandClient.monkey_ran_successfully(response):
print("Running the monkey.")
else:
print("Failed to run the monkey.")
assert False
@staticmethod
def monkey_ran_successfully(response):
return response.ok and json.loads(response.content)['is_running']
def kill_all_monkeys(self):
if self.request_get("api", {"action": "killall"}).ok:
print("Killing all monkeys after the test.")
else:
print("Failed to kill all monkeys.")
assert False
def reset_env(self):
if self.request_get("api", {"action": "reset"}).ok:
print("Resetting environment after the test.")

View File

@ -0,0 +1,17 @@
from time import time
class TestTimer(object):
def __init__(self, timeout):
self.timeout_time = TestTimer.get_timeout_time(timeout)
self.start_time = time()
def timed_out(self):
return time() > self.timeout_time
def get_time_taken(self):
return time() - self.start_time
@staticmethod
def get_timeout_time(timeout):
return time() + timeout