Merge pull request #107 from guardicore/develop

Finish the current small sprint.
New additions
* New icons
* Encrypted creds in the DB
* Option to pull logs from the monkey
* Rename C&C to Monkey Island 
* Get rid of chaos monkey (at last!)
* Async scanning of victims
This commit is contained in:
Daniel Goldberg 2018-03-10 18:44:21 +02:00 committed by GitHub
commit 0fb4feb78c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 927 additions and 527 deletions

View File

@ -6,7 +6,7 @@ Infection Monkey
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 Command and Control(C&C) server.
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.
<img src=".github/map-full.png" >
@ -14,7 +14,7 @@ The Infection Monkey is an open source security tool for testing a data center's
The Infection Monkey is comprised of two parts:
* Monkey - A tool which infects other machines and propagates to them
* Monkey Island - A C&C server with a dedicated UI to visualize the Chaos Monkey's progress inside the data center
* Monkey Island - A dedicated server to control and visualize the Infection Monkey's progress inside the data center
To read more about the Monkey, visit http://infectionmonkey.com
@ -26,7 +26,7 @@ The Infection Monkey uses the following techniques and exploits to propagate to
* Multiple propagation techniques:
* Predefined passwords
* Common logical exploits
* Password stealing using mimikatz
* Password stealing using Mimikatz
* Multiple exploit methods:
* SSH
* SMB
@ -45,7 +45,7 @@ Check out the [Setup](https://github.com/guardicore/monkey/wiki/setup) page in t
Building the Monkey from source
-------------------------------
If you want to build the monkey from source, see [Setup](https://github.com/guardicore/monkey/wiki/setup)
and follow the instructions at the readme files under [chaos_monkey](chaos_monkey) and [monkey_island](monkey_island).
and follow the instructions at the readme files under [infection_monkey](infection_monkey) and [monkey_island](monkey_island).
License

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

View File

@ -1,45 +0,0 @@
import time
from random import shuffle
from network import HostScanner, HostFinger
from model.host import VictimHost
from network.tools import check_port_tcp
__author__ = 'itamar'
BANNER_READ = 1024
class TcpScanner(HostScanner, HostFinger):
def __init__(self):
self._config = __import__('config').WormConfiguration
def is_host_alive(self, host):
return self.get_host_fingerprint(host, True)
def get_host_fingerprint(self, host, only_one_port=False):
assert isinstance(host, VictimHost)
count = 0
# maybe hide under really bad detection systems
target_ports = self._config.tcp_target_ports[:]
shuffle(target_ports)
for target_port in target_ports:
is_open, banner = check_port_tcp(host.ip_addr,
target_port,
self._config.tcp_scan_timeout / 1000.0,
self._config.tcp_scan_get_banner)
if is_open:
count += 1
service = 'tcp-' + str(target_port)
host.services[service] = {}
if banner:
host.services[service]['banner'] = banner
if only_one_port:
break
else:
time.sleep(self._config.tcp_scan_interval / 1000.0)
return count != 0

View File

@ -1,79 +0,0 @@
import socket
import select
import logging
import struct
DEFAULT_TIMEOUT = 10
BANNER_READ = 1024
LOG = logging.getLogger(__name__)
def struct_unpack_tracker(data, index, fmt):
"""
Unpacks a struct from the specified index according to specified format.
Returns the data and the next index
:param data: Buffer
:param index: Position index
:param fmt: Struct format
:return: (Data, new index)
"""
unpacked = struct.unpack_from(fmt, data, index)
return unpacked, struct.calcsize(fmt)
def struct_unpack_tracker_string(data, index):
"""
Unpacks a null terminated string from the specified index
Returns the data and the next index
:param data: Buffer
:param index: Position index
:return: (Data, new index)
"""
ascii_len = data[index:].find('\0')
fmt = "%ds" % ascii_len
return struct_unpack_tracker(data,index,fmt)
def check_port_tcp(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
sock.connect((ip, port))
except socket.timeout:
return False, None
except socket.error, exc:
LOG.debug("Check port: %s:%s, Exception: %s", ip, port, exc)
return False, None
banner = None
try:
if get_banner:
read_ready, _, _ = select.select([sock], [], [], timeout)
if len(read_ready) > 0:
banner = sock.recv(BANNER_READ)
except:
pass
sock.close()
return True, banner
def check_port_udp(ip, port, timeout=DEFAULT_TIMEOUT):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(timeout)
data = None
is_open = False
try:
sock.sendto("-", (ip, port))
data, _ = sock.recvfrom(BANNER_READ)
is_open = True
except:
pass
sock.close()
return is_open, data

View File

@ -106,6 +106,7 @@ class Configuration(object):
dropper_log_path_linux = '/tmp/user-1562'
monkey_log_path_windows = '%temp%\\~df1563.tmp'
monkey_log_path_linux = '/tmp/user-1563'
send_log_to_server = True
###########################
# dropper config

View File

@ -25,7 +25,7 @@ class ControlClient(object):
@staticmethod
def wakeup(parent=None, default_tunnel=None, has_internet_access=None):
LOG.debug("Trying to wake up with C&C servers list: %r" % WormConfiguration.command_servers)
LOG.debug("Trying to wake up with Monkey Island servers list: %r" % WormConfiguration.command_servers)
if parent or default_tunnel:
LOG.debug("parent: %s, default_tunnel: %s" % (parent, default_tunnel))
hostname = gethostname()
@ -111,6 +111,21 @@ class ControlClient(object):
LOG.warn("Error connecting to control server %s: %s",
WormConfiguration.current_server, exc)
@staticmethod
def send_log(log):
if not WormConfiguration.current_server:
return
try:
telemetry = {'monkey_guid': GUID, 'log': json.dumps(log)}
reply = requests.post("https://%s/api/log" % (WormConfiguration.current_server,),
data=json.dumps(telemetry),
headers={'content-type': 'application/json'},
verify=False,
proxies=ControlClient.proxies)
except Exception as exc:
LOG.warn("Error connecting to control server %s: %s",
WormConfiguration.current_server, exc)
@staticmethod
def load_control_config():
if not WormConfiguration.current_server:

View File

@ -48,6 +48,7 @@
"max_iterations": 3,
"monkey_log_path_windows": "%temp%\\~df1563.tmp",
"monkey_log_path_linux": "/tmp/user-1563",
"send_log_to_server": true,
"ms08_067_exploit_attempts": 5,
"ms08_067_remote_user_add": "Monkey_IUSER_SUPPORT",
"ms08_067_remote_user_pass": "Password1!",

View File

@ -13,7 +13,7 @@ from exploit import HostExploiter
from exploit.tools import HTTPTools, get_monkey_depth
from exploit.tools import get_target_monkey
from model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS
from network.tools import check_port_tcp
from network.tools import check_tcp_port
from tools import build_monkey_commandline
__author__ = 'hoffer'
@ -245,7 +245,7 @@ class RdpExploiter(HostExploiter):
return True
if not self.host.os.get('type'):
is_open, _ = check_port_tcp(self.host.ip_addr, RDP_PORT)
is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT)
if is_open:
self.host.os['type'] = 'windows'
return True
@ -254,7 +254,7 @@ class RdpExploiter(HostExploiter):
def exploit_host(self):
global g_reactor
is_open, _ = check_port_tcp(self.host.ip_addr, RDP_PORT)
is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT)
if not is_open:
LOG.info("RDP port is closed on %r, skipping", self.host)
return False

View File

@ -7,7 +7,7 @@ from exploit import HostExploiter
from exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
from model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS
from network import SMBFinger
from network.tools import check_port_tcp
from network.tools import check_tcp_port
from tools import build_monkey_commandline
LOG = getLogger(__name__)
@ -31,12 +31,12 @@ class SmbExploiter(HostExploiter):
return True
if not self.host.os.get('type'):
is_smb_open, _ = check_port_tcp(self.host.ip_addr, 445)
is_smb_open, _ = check_tcp_port(self.host.ip_addr, 445)
if is_smb_open:
smb_finger = SMBFinger()
smb_finger.get_host_fingerprint(self.host)
else:
is_nb_open, _ = check_port_tcp(self.host.ip_addr, 139)
is_nb_open, _ = check_tcp_port(self.host.ip_addr, 139)
if is_nb_open:
self.host.os['type'] = 'windows'
return self.host.os.get('type') in self._TARGET_OS_TYPE

View File

@ -7,7 +7,7 @@ import monkeyfs
from exploit import HostExploiter
from exploit.tools import get_target_monkey, get_monkey_depth
from model import MONKEY_ARG
from network.tools import check_port_tcp
from network.tools import check_tcp_port
from tools import build_monkey_commandline
__author__ = 'hoffer'
@ -41,7 +41,7 @@ class SSHExploiter(HostExploiter):
if servdata.get('name') == 'ssh' and servkey.startswith('tcp-'):
port = int(servkey.replace('tcp-', ''))
is_open, _ = check_port_tcp(self.host.ip_addr, port)
is_open, _ = check_tcp_port(self.host.ip_addr, port)
if not is_open:
LOG.info("SSH port is closed on %r, skipping", self.host)
return False

View File

@ -17,7 +17,7 @@ from impacket.dcerpc.v5 import transport
from exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
from model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
from network import SMBFinger
from network.tools import check_port_tcp
from network.tools import check_tcp_port
from tools import build_monkey_commandline
from . import HostExploiter
@ -168,7 +168,7 @@ class Ms08_067_Exploiter(HostExploiter):
if not self.host.os.get('type') or (
self.host.os.get('type') in self._TARGET_OS_TYPE and not self.host.os.get('version')):
is_smb_open, _ = check_port_tcp(self.host.ip_addr, 445)
is_smb_open, _ = check_tcp_port(self.host.ip_addr, 445)
if is_smb_open:
smb_finger = SMBFinger()
if smb_finger.get_host_fingerprint(self.host):

View File

@ -11,7 +11,8 @@ import traceback
from config import WormConfiguration, EXTERNAL_CONFIG_FILE
from dropper import MonkeyDrops
from model import MONKEY_ARG, DROPPER_ARG
from monkey import ChaosMonkey
from monkey import InfectionMonkey
import utils
if __name__ == "__main__":
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@ -78,12 +79,10 @@ def main():
try:
if MONKEY_ARG == monkey_mode:
log_path = os.path.expandvars(
WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" else WormConfiguration.monkey_log_path_linux
monkey_cls = ChaosMonkey
log_path = utils.get_monkey_log_path()
monkey_cls = InfectionMonkey
elif DROPPER_ARG == monkey_mode:
log_path = os.path.expandvars(
WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" else WormConfiguration.dropper_log_path_linux
log_path = utils.get_dropper_log_path()
monkey_cls = MonkeyDrops
else:
return True
@ -91,6 +90,8 @@ def main():
return True
if WormConfiguration.use_file_logging:
if os.path.exists(log_path):
os.remove(log_path)
LOG_CONFIG['handlers']['file']['filename'] = log_path
LOG_CONFIG['root']['handlers'].append('file')
else:
@ -120,6 +121,8 @@ def main():
json.dump(json_dict, config_fo, skipkeys=True, sort_keys=True, indent=4, separators=(',', ': '))
return True
except Exception:
LOG.exception("Exception thrown from monkey's start function")
finally:
monkey.cleanup()

BIN
infection_monkey/monkey.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -6,6 +6,7 @@ import sys
import time
import tunnel
import utils
from config import WormConfiguration
from control import ControlClient
from model import DELAY_DELETE_CMD
@ -19,7 +20,7 @@ __author__ = 'itamar'
LOG = logging.getLogger(__name__)
class ChaosMonkey(object):
class InfectionMonkey(object):
def __init__(self, args):
self._keep_running = False
self._exploited_machines = set()
@ -226,6 +227,9 @@ class ChaosMonkey(object):
firewall.close()
if WormConfiguration.send_log_to_server:
self.send_log()
self._singleton.unlock()
if WormConfiguration.self_delete_in_cleanup and -1 == sys.executable.find('python'):
@ -244,3 +248,13 @@ class ChaosMonkey(object):
LOG.error("Exception in self delete: %s", exc)
LOG.info("Monkey is shutting down")
def send_log(self):
monkey_log_path = utils.get_monkey_log_path()
if os.path.exists(monkey_log_path):
with open(monkey_log_path, 'r') as f:
log = f.read()
else:
log = ''
ControlClient.send_log(log)

View File

@ -1,9 +1,10 @@
import time
import logging
from . import HostScanner
import time
from config import WormConfiguration
from info import local_ips, get_ips_from_interfaces
from range import *
from . import HostScanner
__author__ = 'itamar'
@ -18,6 +19,12 @@ class NetworkScanner(object):
self._ranges = None
def initialize(self):
"""
Set up scanning based on configuration
FixedRange -> Reads from range_fixed field in configuration
otherwise, takes a range from every IP address the current host has.
:return:
"""
# get local ip addresses
self._ip_addresses = local_ips()
@ -27,7 +34,7 @@ class NetworkScanner(object):
LOG.info("Found local IP addresses of the machine: %r", self._ip_addresses)
# for fixed range, only scan once.
if WormConfiguration.range_class is FixedRange:
self._ranges = [WormConfiguration.range_class(None)]
self._ranges = [WormConfiguration.range_class(fixed_addresses=WormConfiguration.range_fixed)]
else:
self._ranges = [WormConfiguration.range_class(ip_address)
for ip_address in self._ip_addresses]

View File

@ -1,10 +1,11 @@
import os
import sys
import subprocess
import logging
from . import HostScanner, HostFinger
from model.host import VictimHost
import os
import re
import subprocess
import sys
from model.host import VictimHost
from . import HostScanner, HostFinger
__author__ = 'itamar'
@ -62,7 +63,7 @@ class PingScanner(HostScanner, HostFinger):
elif WINDOWS_TTL == ttl:
host.os['type'] = 'windows'
return True
except Exception, exc:
except Exception as exc:
LOG.debug("Error parsing ping fingerprint: %s", exc)
return False

View File

@ -1,7 +1,8 @@
import socket
import random
import socket
import struct
from abc import ABCMeta, abstractmethod
from model.host import VictimHost
__author__ = 'itamar'
@ -77,5 +78,5 @@ class FixedRange(NetworkRange):
for address in self._fixed_addresses:
if not address: # Empty string
continue
address_range.append(struct.unpack(">L", socket.inet_aton(address))[0])
address_range.append(struct.unpack(">L", socket.inet_aton(address.strip()))[0])
return address_range

View File

@ -1,7 +1,8 @@
import re
from network import HostFinger
from network.tools import check_port_tcp
from model.host import VictimHost
from network import HostFinger
from network.tools import check_tcp_port
SSH_PORT = 22
SSH_SERVICE_DEFAULT = 'tcp-22'
@ -38,7 +39,7 @@ class SSHFinger(HostFinger):
self._banner_match(name, host, banner)
return
is_open, banner = check_port_tcp(host.ip_addr, SSH_PORT, TIMEOUT, True)
is_open, banner = check_tcp_port(host.ip_addr, SSH_PORT, TIMEOUT, True)
if is_open:
host.services[SSH_SERVICE_DEFAULT] = {}

View File

@ -0,0 +1,41 @@
from itertools import izip_longest
from random import shuffle
from network import HostScanner, HostFinger
from network.tools import check_tcp_ports
__author__ = 'itamar'
BANNER_READ = 1024
class TcpScanner(HostScanner, HostFinger):
def __init__(self):
self._config = __import__('config').WormConfiguration
def is_host_alive(self, host):
return self.get_host_fingerprint(host, True)
def get_host_fingerprint(self, host, only_one_port=False):
"""
Scans a target host to see if it's alive using the tcp_target_ports specified in the configuration.
:param host: VictimHost structure
:param only_one_port: Currently unused.
:return: T/F if there is at least one open port. In addition, the host object is updated to mark those services as alive.
"""
# maybe hide under really bad detection systems
target_ports = self._config.tcp_target_ports[:]
shuffle(target_ports)
ports, banners = check_tcp_ports(host.ip_addr, target_ports, self._config.tcp_scan_timeout / 1000.0,
self._config.tcp_scan_get_banner)
for target_port, banner in izip_longest(ports, banners, fillvalue=None):
service = 'tcp-' + str(target_port)
host.services[service] = {}
if banner:
host.services[service]['banner'] = banner
if only_one_port:
break
return len(ports) != 0

View File

@ -0,0 +1,150 @@
import logging
import select
import socket
import struct
import time
DEFAULT_TIMEOUT = 10
BANNER_READ = 1024
LOG = logging.getLogger(__name__)
def struct_unpack_tracker(data, index, fmt):
"""
Unpacks a struct from the specified index according to specified format.
Returns the data and the next index
:param data: Buffer
:param index: Position index
:param fmt: Struct format
:return: (Data, new index)
"""
unpacked = struct.unpack_from(fmt, data, index)
return unpacked, struct.calcsize(fmt)
def struct_unpack_tracker_string(data, index):
"""
Unpacks a null terminated string from the specified index
Returns the data and the next index
:param data: Buffer
:param index: Position index
:return: (Data, new index)
"""
ascii_len = data[index:].find('\0')
fmt = "%ds" % ascii_len
return struct_unpack_tracker(data, index, fmt)
def check_tcp_port(ip, port, timeout=DEFAULT_TIMEOUT, get_banner=False):
"""
Checks if a given TCP port is open
:param ip: Target IP
:param port: Target Port
:param timeout: Timeout for socket connection
:param get_banner: if true, pulls first BANNER_READ bytes from the socket.
:return: Tuple, T/F + banner if requested.
"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
sock.connect((ip, port))
except socket.timeout:
return False, None
except socket.error as exc:
LOG.debug("Check port: %s:%s, Exception: %s", ip, port, exc)
return False, None
banner = None
try:
if get_banner:
read_ready, _, _ = select.select([sock], [], [], timeout)
if len(read_ready) > 0:
banner = sock.recv(BANNER_READ)
except socket.error:
pass
sock.close()
return True, banner
def check_udp_port(ip, port, timeout=DEFAULT_TIMEOUT):
"""
Checks if a given UDP port is open by checking if it replies to an empty message
:param ip: Target IP
:param port: Target port
:param timeout: Timeout to wait
:return: Tuple, T/F + banner
"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(timeout)
data = None
is_open = False
try:
sock.sendto("-", (ip, port))
data, _ = sock.recvfrom(BANNER_READ)
is_open = True
except socket.error:
pass
sock.close()
return is_open, data
def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False):
"""
Checks whether any of the given ports are open on a target IP.
:param ip: IP of host to attack
:param ports: List of ports to attack. Must not be empty.
:param timeout: Amount of time to wait for connection
:param get_banner: T/F if to get first packets from server
:return: list of open ports. If get_banner=True, then a matching list of banners.
"""
sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for _ in range(len(ports))]
[s.setblocking(0) for s in sockets]
good_ports = []
try:
LOG.debug("Connecting to the following ports %s" % ",".join((str(x) for x in ports)))
for sock, port in zip(sockets, ports):
err = sock.connect_ex((ip, port))
if err == 0:
good_ports.append((port, sock))
continue
if err == 10035: # WSAEWOULDBLOCK is valid, see https://msdn.microsoft.com/en-us/library/windows/desktop/ms740668%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396
good_ports.append((port, sock))
continue
if err == 115: # EINPROGRESS 115 /* Operation now in progress */
good_ports.append((port, sock))
continue
LOG.warning("Failed to connect to port %s, error code is %d", port, err)
if len(good_ports) != 0:
time.sleep(timeout)
# this is possibly connected. meaning after timeout wait, we expect to see a connection up
# Possible valid errors codes if we chose to check for actually closed are
# ECONNREFUSED (111) or WSAECONNREFUSED (10061) or WSAETIMEDOUT(10060)
connected_ports_sockets = [s for s in good_ports if
s[1].getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) == 0]
LOG.debug(
"On host %s discovered the following ports %s" %
(str(ip), ",".join([str(x[0]) for x in connected_ports_sockets])))
banners = []
if get_banner:
readable_sockets, _, _ = select.select([s[1] for s in connected_ports_sockets], [], [], 0)
# read first BANNER_READ bytes
banners = [sock.recv(BANNER_READ) if sock in readable_sockets else ""
for port, sock in connected_ports_sockets]
pass
# try to cleanup
[s[1].close() for s in good_ports]
return [port for port, sock in connected_ports_sockets], banners
else:
return [], []
except socket.error as exc:
LOG.warning("Exception when checking ports on host %s, Exception: %s", str(ip), exc)
return [], []

View File

@ -28,13 +28,13 @@ The monkey is composed of three separate parts.
64bit: http://www.microsoft.com/en-us/download/details.aspx?id=13523
6. Download the dependent python packages using
pip install -r requirements.txt
7. Download and extract UPX binary to [source-path]\monkey\chaos_monkey\bin\upx.exe:
7. Download and extract UPX binary to [source-path]\monkey\infection_monkey\bin\upx.exe:
https://github.com/upx/upx/releases/download/v3.94/upx394w.zip
8. Build/Download Sambacry and Mimikatz binaries
a. Build/Download according to sections at the end of this readme.
b. Place the binaries under [code location]\chaos_monkey\bin
b. Place the binaries under [code location]\infection_monkey\bin
9. To build the final exe:
cd [code location]/chaos_monkey
cd [code location]/infection_monkey
build_windows.bat
output is placed under dist\monkey.exe
@ -46,13 +46,13 @@ Tested on Ubuntu 16.04 and 17.04.
sudo apt-get update
sudo apt-get install python-pip python-dev libffi-dev upx libssl-dev libc++1
Install the python packages listed in requirements.txt using pip
cd [code location]/chaos_monkey
cd [code location]/infection_monkey
pip install -r requirements.txt
2. Build Sambacry binaries
a. Build/Download according to sections at the end of this readme.
b. Place the binaries under [code location]\chaos_monkey\bin
b. Place the binaries under [code location]\infection_monkey\bin
3. To build, run in terminal:
cd [code location]/chaos_monkey
cd [code location]/infection_monkey
chmod +x build_linux.sh
./build_linux.sh
output is placed under dist/monkey
@ -63,11 +63,11 @@ Sambacry requires two standalone binaries to execute remotely.
1. Install gcc-multilib if it's not installed
sudo apt-get install gcc-multilib
2. Build the binaries
cd [code location]/chaos_monkey/monkey_utils/sambacry_monkey_runner
cd [code location]/infection_monkey/monkey_utils/sambacry_monkey_runner
./build.sh
-- Mimikatz --
Mimikatz is required for the Monkey to be able to steal credentials on Windows. It's possible to either compile from sources (requires Visual Studio 2013 and up) or download the binaries from
https://github.com/guardicore/mimikatz/releases/tag/1.0.0
Download both 32 and 64 bit DLLs and place them under [code location]\chaos_monkey\bin
Download both 32 and 64 bit DLLs and place them under [code location]\infection_monkey\bin

View File

@ -1,5 +1,5 @@
# -*- coding: UTF-8 -*-
# NOTE: Launch all tests with `nosetests` command from chaos_monkey dir.
# NOTE: Launch all tests with `nosetests` command from infection_monkey dir.
import json
import unittest

View File

@ -8,7 +8,7 @@ from threading import Thread
from model import VictimHost
from network.firewall import app as firewall
from network.info import local_ips, get_free_tcp_port
from network.tools import check_port_tcp
from network.tools import check_tcp_port
from transport.base import get_last_serve_time
__author__ = 'hoffer'
@ -40,7 +40,7 @@ def _check_tunnel(address, port, existing_sock=None):
sock = existing_sock
LOG.debug("Checking tunnel %s:%s", address, port)
is_open, _ = check_port_tcp(address, int(port))
is_open, _ = check_tcp_port(address, int(port))
if not is_open:
LOG.debug("Could not connect to %s:%s", address, port)
if not existing_sock:

14
infection_monkey/utils.py Normal file
View File

@ -0,0 +1,14 @@
import os
import sys
from config import WormConfiguration
def get_monkey_log_path():
return os.path.expandvars(WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" \
else WormConfiguration.monkey_log_path_linux
def get_dropper_log_path():
return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \
else WormConfiguration.dropper_log_path_linux

View File

@ -8,11 +8,12 @@ from flask import Flask, send_from_directory, make_response
from werkzeug.exceptions import NotFound
from cc.auth import init_jwt
from cc.database import mongo
from cc.database import mongo, database
from cc.environment.environment import env
from cc.resources.client_run import ClientRun
from cc.resources.edge import Edge
from cc.resources.local_run import LocalRun
from cc.resources.log import Log
from cc.resources.monkey import Monkey
from cc.resources.monkey_configuration import MonkeyConfiguration
from cc.resources.monkey_download import MonkeyDownload
@ -83,6 +84,7 @@ def init_app(mongo_url):
mongo.init_app(app)
with app.app_context():
database.init()
ConfigService.init_config()
app.add_url_rule('/', 'serve_home', serve_home)
@ -101,5 +103,6 @@ def init_app(mongo_url):
api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/')
api.add_resource(Report, '/api/report', '/api/report/')
api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/')
api.add_resource(Log, '/api/log', '/api/log/')
return app

View File

@ -1,5 +1,5 @@
from flask_pymongo import PyMongo
from flask_pymongo import MongoClient
import gridfs
from flask_pymongo import MongoClient, PyMongo
from pymongo.errors import ServerSelectionTimeoutError
__author__ = 'Barak'
@ -7,6 +7,17 @@ __author__ = 'Barak'
mongo = PyMongo()
class Database:
def __init__(self):
self.gridfs = None
def init(self):
self.gridfs = gridfs.GridFS(mongo.db)
database = Database()
def is_db_server_up(mongo_url):
client = MongoClient(mongo_url, serverSelectionTimeoutMS=100)
try:

View File

@ -0,0 +1,51 @@
import base64
import os
from Crypto import Random
from Crypto.Cipher import AES
__author__ = "itay.mizeretz"
class Encryptor:
_BLOCK_SIZE = 32
_DB_PASSWORD_FILENAME = "mongo_key.bin"
def __init__(self):
self._load_key()
def _init_key(self):
self._cipher_key = Random.new().read(self._BLOCK_SIZE)
with open(self._DB_PASSWORD_FILENAME, 'wb') as f:
f.write(self._cipher_key)
def _load_existing_key(self):
with open(self._DB_PASSWORD_FILENAME, 'rb') as f:
self._cipher_key = f.read()
def _load_key(self):
if os.path.exists(self._DB_PASSWORD_FILENAME):
self._load_existing_key()
else:
self._init_key()
def _pad(self, message):
return message + (self._BLOCK_SIZE - (len(message) % self._BLOCK_SIZE)) * chr(
self._BLOCK_SIZE - (len(message) % self._BLOCK_SIZE))
def _unpad(self, message):
return message[0:-ord(message[len(message) - 1])]
def enc(self, message):
cipher_iv = Random.new().read(AES.block_size)
cipher = AES.new(self._cipher_key, AES.MODE_CBC, cipher_iv)
return base64.b64encode(cipher_iv + cipher.encrypt(self._pad(message)))
def dec(self, enc_message):
enc_message = base64.b64decode(enc_message)
cipher_iv = enc_message[0:AES.block_size]
cipher = AES.new(self._cipher_key, AES.MODE_CBC, cipher_iv)
return self._unpad(cipher.decrypt(enc_message[AES.block_size:]))
encryptor = Encryptor()

View File

@ -33,6 +33,6 @@ if __name__ == '__main__':
ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'),
'keyfile': os.environ.get('SERVER_KEY', 'server.key')})
http_server.listen(env.get_island_port())
print('Monkey Island C&C Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port()))
print('Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port()))
IOLoop.instance().start()

View File

@ -0,0 +1,34 @@
import json
import flask_restful
from bson import ObjectId
from flask import request
from cc.auth import jwt_required
from cc.database import mongo
from cc.services.log import LogService
from cc.services.node import NodeService
__author__ = "itay.mizeretz"
class Log(flask_restful.Resource):
@jwt_required()
def get(self):
monkey_id = request.args.get('id')
exists_monkey_id = request.args.get('exists')
if monkey_id:
return LogService.get_log_by_monkey_id(ObjectId(monkey_id))
else:
return LogService.log_exists(ObjectId(exists_monkey_id))
# Used by monkey. can't secure.
def post(self):
telemetry_json = json.loads(request.data)
monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])['_id']
# This shouldn't contain any unicode characters. this'll take 2 time less space.
log_data = str(telemetry_json['log'])
log_id = LogService.add_log(monkey_id, log_data)
return mongo.db.log.find_one_or_404({"_id": log_id})

View File

@ -65,7 +65,7 @@ class Monkey(flask_restful.Resource):
# if new monkey telem, change config according to "new monkeys" config.
db_monkey = mongo.db.monkey.find_one({"guid": monkey_json["guid"]})
if not db_monkey:
new_config = ConfigService.get_flat_config()
new_config = ConfigService.get_flat_config(False, True)
monkey_json['config'] = monkey_json.get('config', {})
monkey_json['config'].update(new_config)
else:

View File

@ -12,7 +12,7 @@ __author__ = 'Barak'
class MonkeyConfiguration(flask_restful.Resource):
@jwt_required()
def get(self):
return jsonify(schema=ConfigService.get_config_schema(), configuration=ConfigService.get_config())
return jsonify(schema=ConfigService.get_config_schema(), configuration=ConfigService.get_config(False, True))
@jwt_required()
def post(self):
@ -20,5 +20,5 @@ class MonkeyConfiguration(flask_restful.Resource):
if config_json.has_key('reset'):
ConfigService.reset_config()
else:
ConfigService.update_config(config_json)
ConfigService.update_config(config_json, should_encrypt=True)
return self.get()

View File

@ -36,7 +36,8 @@ class Root(flask_restful.Resource):
@staticmethod
def reset_db():
[mongo.db[x].drop() for x in ['config', 'monkey', 'telemetry', 'node', 'edge', 'report']]
# We can't drop system collections.
[mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')]
ConfigService.init_config()
return jsonify(status='OK')

View File

@ -12,6 +12,7 @@ from cc.database import mongo
from cc.services.config import ConfigService
from cc.services.edge import EdgeService
from cc.services.node import NodeService
from cc.encryptor import encryptor
__author__ = 'Barak'
@ -112,6 +113,8 @@ class Telemetry(flask_restful.Resource):
@staticmethod
def process_exploit_telemetry(telemetry_json):
edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json)
Telemetry.encrypt_exploit_creds(telemetry_json)
new_exploit = copy.deepcopy(telemetry_json['data'])
new_exploit.pop('machine')
@ -166,25 +169,49 @@ class Telemetry(flask_restful.Resource):
def process_system_info_telemetry(telemetry_json):
if 'credentials' in telemetry_json['data']:
creds = telemetry_json['data']['credentials']
for user in creds:
ConfigService.creds_add_username(user)
if 'password' in creds[user]:
ConfigService.creds_add_password(creds[user]['password'])
if 'lm_hash' in creds[user]:
ConfigService.creds_add_lm_hash(creds[user]['lm_hash'])
if 'ntlm_hash' in creds[user]:
ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash'])
for user in creds:
if -1 != user.find('.'):
new_user = user.replace('.', ',')
creds[new_user] = creds.pop(user)
Telemetry.encrypt_system_info_creds(creds)
Telemetry.add_system_info_creds_to_config(creds)
Telemetry.replace_user_dot_with_comma(creds)
@staticmethod
def process_trace_telemetry(telemetry_json):
# Nothing to do
return
@staticmethod
def replace_user_dot_with_comma(creds):
for user in creds:
if -1 != user.find('.'):
new_user = user.replace('.', ',')
creds[new_user] = creds.pop(user)
@staticmethod
def encrypt_system_info_creds(creds):
for user in creds:
for field in ['password', 'lm_hash', 'ntlm_hash']:
if field in creds[user]:
creds[user][field] = encryptor.enc(creds[user][field])
@staticmethod
def add_system_info_creds_to_config(creds):
for user in creds:
ConfigService.creds_add_username(user)
if 'password' in creds[user]:
ConfigService.creds_add_password(creds[user]['password'])
if 'lm_hash' in creds[user]:
ConfigService.creds_add_lm_hash(creds[user]['lm_hash'])
if 'ntlm_hash' in creds[user]:
ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash'])
@staticmethod
def encrypt_exploit_creds(telemetry_json):
attempts = telemetry_json['data']['attempts']
for i in range(len(attempts)):
for field in ['password', 'lm_hash', 'ntlm_hash']:
credential = attempts[i][field]
if len(credential) > 0:
attempts[i][field] = encryptor.enc(credential)
TELEM_PROCESS_DICT = \
{

View File

@ -1,6 +1,9 @@
from cc.database import mongo
import copy
import functools
from jsonschema import Draft4Validator, validators
from cc.database import mongo
from cc.encryptor import encryptor
from cc.environment.environment import env
from cc.utils import local_ip_addresses
@ -17,60 +20,60 @@ SCHEMA = {
"type": "string",
"anyOf": [
{
"type": "string",
"enum": [
"SmbExploiter"
],
"title": "SMB Exploiter"
"type": "string",
"enum": [
"SmbExploiter"
],
"title": "SMB Exploiter"
},
{
"type": "string",
"enum": [
"WmiExploiter"
],
"title": "WMI Exploiter"
"type": "string",
"enum": [
"WmiExploiter"
],
"title": "WMI Exploiter"
},
{
"type": "string",
"enum": [
"RdpExploiter"
],
"title": "RDP Exploiter (UNSAFE)"
"type": "string",
"enum": [
"RdpExploiter"
],
"title": "RDP Exploiter (UNSAFE)"
},
{
"type": "string",
"enum": [
"Ms08_067_Exploiter"
],
"title": "MS08-067 Exploiter (UNSAFE)"
"type": "string",
"enum": [
"Ms08_067_Exploiter"
],
"title": "MS08-067 Exploiter (UNSAFE)"
},
{
"type": "string",
"enum": [
"SSHExploiter"
],
"title": "SSH Exploiter"
"type": "string",
"enum": [
"SSHExploiter"
],
"title": "SSH Exploiter"
},
{
"type": "string",
"enum": [
"ShellShockExploiter"
],
"title": "ShellShock Exploiter"
"type": "string",
"enum": [
"ShellShockExploiter"
],
"title": "ShellShock Exploiter"
},
{
"type": "string",
"enum": [
"SambaCryExploiter"
],
"title": "SambaCry Exploiter"
"type": "string",
"enum": [
"SambaCryExploiter"
],
"title": "SambaCry Exploiter"
},
{
"type": "string",
"enum": [
"ElasticGroovyExploiter"
],
"title": "ElasticGroovy Exploiter"
"type": "string",
"enum": [
"ElasticGroovyExploiter"
],
"title": "ElasticGroovy Exploiter"
},
]
},
@ -79,46 +82,46 @@ SCHEMA = {
"type": "string",
"anyOf": [
{
"type": "string",
"enum": [
"SMBFinger"
],
"title": "SMBFinger"
"type": "string",
"enum": [
"SMBFinger"
],
"title": "SMBFinger"
},
{
"type": "string",
"enum": [
"SSHFinger"
],
"title": "SSHFinger"
"type": "string",
"enum": [
"SSHFinger"
],
"title": "SSHFinger"
},
{
"type": "string",
"enum": [
"PingScanner"
],
"title": "PingScanner"
"type": "string",
"enum": [
"PingScanner"
],
"title": "PingScanner"
},
{
"type": "string",
"enum": [
"HTTPFinger"
],
"title": "HTTPFinger"
"type": "string",
"enum": [
"HTTPFinger"
],
"title": "HTTPFinger"
},
{
"type": "string",
"enum": [
"MySQLFinger"
],
"title": "MySQLFinger"
"type": "string",
"enum": [
"MySQLFinger"
],
"title": "MySQLFinger"
},
{
"type": "string",
"enum": [
"ElasticFinger"
],
"title": "ElasticFinger"
"type": "string",
"enum": [
"ElasticFinger"
],
"title": "ElasticFinger"
}
]
}
@ -483,6 +486,12 @@ SCHEMA = {
"type": "string",
"default": "%temp%\\~df1563.tmp",
"description": "The fullpath of the monkey log file on Windows"
},
"send_log_to_server": {
"title": "Send log to server",
"type": "boolean",
"default": True,
"description": "Determines whether the monkey sends its log to the Monkey Island server"
}
}
},
@ -528,7 +537,7 @@ SCHEMA = {
}
},
"cnc": {
"title": "C&C",
"title": "Monkey Island",
"type": "object",
"properties": {
"servers": {
@ -794,29 +803,56 @@ SCHEMA = {
}
}
ENCRYPTED_CONFIG_ARRAYS = \
[
['basic', 'credentials', 'exploit_password_list'],
['internal', 'exploits', 'exploit_lm_hash_list'],
['internal', 'exploits', 'exploit_ntlm_hash_list']
]
class ConfigService:
default_config = None
def __init__(self):
pass
@staticmethod
def get_config(is_initial_config=False):
def get_config(is_initial_config=False, should_decrypt=True):
"""
Gets the entire global config.
:param is_initial_config: If True, the initial config will be returned instead of the current config.
:param should_decrypt: If True, all config values which are set as encrypted will be decrypted.
:return: The entire global config.
"""
config = mongo.db.config.find_one({'name': 'initial' if is_initial_config else 'newconfig'}) or {}
for field in ('name', '_id'):
config.pop(field, None)
if should_decrypt and len(config) > 0:
ConfigService.decrypt_config(config)
return config
@staticmethod
def get_config_value(config_key_as_arr, is_initial_config=False):
config_key = reduce(lambda x, y: x+'.'+y, config_key_as_arr)
def get_config_value(config_key_as_arr, is_initial_config=False, should_decrypt=True):
"""
Get a specific config value.
:param config_key_as_arr: The config key as an array. e.g. ['basic', 'credentials', 'exploit_password_list'].
:param is_initial_config: If True, returns the value of the initial config instead of the current config.
:param should_decrypt: If True, the value of the config key will be decrypted
(if it's in the list of encrypted config values).
:return: The value of the requested config key.
"""
config_key = functools.reduce(lambda x, y: x + '.' + y, config_key_as_arr)
config = mongo.db.config.find_one({'name': 'initial' if is_initial_config else 'newconfig'}, {config_key: 1})
for config_key_part in config_key_as_arr:
config = config[config_key_part]
if should_decrypt and (config_key_as_arr in ENCRYPTED_CONFIG_ARRAYS):
config = [encryptor.dec(x) for x in config]
return config
@staticmethod
def get_flat_config(is_initial_config=False):
config_json = ConfigService.get_config(is_initial_config)
def get_flat_config(is_initial_config=False, should_decrypt=True):
config_json = ConfigService.get_config(is_initial_config, should_decrypt)
flat_config_json = {}
for i in config_json:
for j in config_json[i]:
@ -860,27 +896,38 @@ class ConfigService:
ConfigService.add_item_to_config_set('internal.exploits.exploit_ntlm_hash_list', ntlm_hash)
@staticmethod
def update_config(config_json):
def update_config(config_json, should_encrypt):
if should_encrypt:
ConfigService.encrypt_config(config_json)
mongo.db.config.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True)
@staticmethod
def get_default_config():
defaultValidatingDraft4Validator = ConfigService._extend_config_with_default(Draft4Validator)
config = {}
defaultValidatingDraft4Validator(SCHEMA).validate(config)
def init_default_config():
if ConfigService.default_config is None:
defaultValidatingDraft4Validator = ConfigService._extend_config_with_default(Draft4Validator)
config = {}
defaultValidatingDraft4Validator(SCHEMA).validate(config)
ConfigService.default_config = config
@staticmethod
def get_default_config(should_encrypt=False):
ConfigService.init_default_config()
config = copy.deepcopy(ConfigService.default_config)
if should_encrypt:
ConfigService.encrypt_config(config)
return config
@staticmethod
def init_config():
if ConfigService.get_config() != {}:
if ConfigService.get_config(should_decrypt=False) != {}:
return
ConfigService.reset_config()
@staticmethod
def reset_config():
config = ConfigService.get_default_config()
config = ConfigService.get_default_config(True)
ConfigService.set_server_ips_in_config(config)
ConfigService.update_config(config)
ConfigService.update_config(config, should_encrypt=False)
@staticmethod
def set_server_ips_in_config(config):
@ -922,3 +969,21 @@ class ConfigService:
return validators.extend(
validator_class, {"properties": set_defaults},
)
@staticmethod
def decrypt_config(config):
ConfigService._encrypt_or_decrypt_config(config, True)
@staticmethod
def encrypt_config(config):
ConfigService._encrypt_or_decrypt_config(config, False)
@staticmethod
def _encrypt_or_decrypt_config(config, is_decrypt=False):
for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS:
config_arr = config
for config_key_part in config_arr_as_array:
config_arr = config_arr[config_key_part]
for i in range(len(config_arr)):
config_arr[i] = encryptor.dec(config_arr[i]) if is_decrypt else encryptor.enc(config_arr[i])

View File

@ -0,0 +1,48 @@
from datetime import datetime
import cc.services.node
from cc.database import mongo, database
__author__ = "itay.mizeretz"
class LogService:
def __init__(self):
pass
@staticmethod
def get_log_by_monkey_id(monkey_id):
log = mongo.db.log.find_one({'monkey_id': monkey_id})
if log:
log_file = database.gridfs.get(log['file_id'])
monkey_label = cc.services.node.NodeService.get_monkey_label(
cc.services.node.NodeService.get_monkey_by_id(log['monkey_id']))
return \
{
'monkey_label': monkey_label,
'log': log_file.read(),
'timestamp': log['timestamp']
}
@staticmethod
def remove_logs_by_monkey_id(monkey_id):
log = mongo.db.log.find_one({'monkey_id': monkey_id})
if log is not None:
database.gridfs.delete(log['file_id'])
mongo.db.log.delete_one({'monkey_id': monkey_id})
@staticmethod
def add_log(monkey_id, log_data, timestamp=datetime.now()):
LogService.remove_logs_by_monkey_id(monkey_id)
file_id = database.gridfs.put(log_data)
return mongo.db.log.insert(
{
'monkey_id': monkey_id,
'file_id': file_id,
'timestamp': timestamp
}
)
@staticmethod
def log_exists(monkey_id):
return mongo.db.log.find_one({'monkey_id': monkey_id}) is not None

View File

@ -1,9 +1,12 @@
from datetime import datetime, timedelta
from bson import ObjectId
import cc.services.log
from cc.database import mongo
from cc.services.edge import EdgeService
from cc.utils import local_ip_addresses
__author__ = "itay.mizeretz"
@ -54,6 +57,7 @@ class NodeService:
else:
new_node["services"] = []
new_node['has_log'] = cc.services.log.LogService.log_exists(ObjectId(node_id))
return new_node
@staticmethod
@ -241,7 +245,7 @@ class NodeService:
@staticmethod
def get_monkey_island_pseudo_net_node():
return\
return \
{
"id": NodeService.get_monkey_island_pseudo_id(),
"label": "MonkeyIsland",

View File

@ -293,19 +293,19 @@ class ReportService:
@staticmethod
def get_config_users():
return ConfigService.get_config_value(['basic', 'credentials', 'exploit_user_list'], True)
return ConfigService.get_config_value(['basic', 'credentials', 'exploit_user_list'], True, True)
@staticmethod
def get_config_passwords():
return ConfigService.get_config_value(['basic', 'credentials', 'exploit_password_list'], True)
return ConfigService.get_config_value(['basic', 'credentials', 'exploit_password_list'], True, True)
@staticmethod
def get_config_exploits():
exploits_config_value = ['exploits', 'general', 'exploiter_classes']
default_exploits = ConfigService.get_default_config()
default_exploits = ConfigService.get_default_config(False)
for namespace in exploits_config_value:
default_exploits = default_exploits[namespace]
exploits = ConfigService.get_config_value(exploits_config_value, True)
exploits = ConfigService.get_config_value(exploits_config_value, True, True)
if exploits == default_exploits:
return ['default']
@ -315,13 +315,13 @@ class ReportService:
@staticmethod
def get_config_ips():
if ConfigService.get_config_value(['basic_network', 'network_range', 'range_class'], True) != 'FixedRange':
if ConfigService.get_config_value(['basic_network', 'network_range', 'range_class'], True, True) != 'FixedRange':
return []
return ConfigService.get_config_value(['basic_network', 'network_range', 'range_fixed'], True)
return ConfigService.get_config_value(['basic_network', 'network_range', 'range_fixed'], True, True)
@staticmethod
def get_config_scan():
return ConfigService.get_config_value(['basic_network', 'general', 'local_network_scan'], True)
return ConfigService.get_config_value(['basic_network', 'general', 'local_network_scan'], True, True)
@staticmethod
def get_issues_overview(issues, config_users, config_passwords):

View File

@ -12,13 +12,6 @@ module.exports = {
devtool: 'eval',
module: {
preLoaders: [
{
test: /\.(js|jsx)$/,
loader: 'isparta-instrumenter-loader',
include: [
path.join(__dirname, '/../src')
]
}
],
loaders: [
{

View File

@ -1,5 +1,5 @@
{
"version": "0.0.1",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -42,6 +42,22 @@
}
}
},
"active-event-stack": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/active-event-stack/-/active-event-stack-1.0.0.tgz",
"integrity": "sha1-a1uS661xmvrpgs1R9Jw4xbaADFA=",
"requires": {
"immutable": "3.8.2",
"lodash": "3.10.1"
},
"dependencies": {
"lodash": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
"integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y="
}
}
},
"after": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
@ -136,7 +152,7 @@
"arr-flatten": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
"integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==",
"integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=",
"dev": true
},
"array-find-index": {
@ -1521,6 +1537,11 @@
"q": "1.5.0"
}
},
"bowser": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.2.tgz",
"integrity": "sha512-fuiANC1Bqbqa/S4gmvfCt7bGBmNELMsGZj4Wg3PrP6esP66Ttoj1JSlzFlXtHyduMv07kDNmDsX6VsMWT/MLGg=="
},
"brace-expansion": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
@ -1768,16 +1789,6 @@
"integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=",
"dev": true
},
"clipboard": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz",
"integrity": "sha1-Ng1taUbpmnof7zleQrqStem1oWs=",
"requires": {
"good-listener": "1.2.2",
"select": "1.1.2",
"tiny-emitter": "2.0.2"
}
},
"cliui": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
@ -1976,7 +1987,7 @@
"readable-stream": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
"integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
"integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
@ -1991,7 +2002,7 @@
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
@ -2065,7 +2076,7 @@
"copy-to-clipboard": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz",
"integrity": "sha512-c3GdeY8qxCHGezVb1EFQfHYK/8NZRemgcTIzPq7PuxjHAf/raKibn2QdhHPb/y6q74PMgH6yizaDZlRmw6QyKw==",
"integrity": "sha1-9OgvSogw3ORma3643tDJvMMTq6k=",
"requires": {
"toggle-selection": "1.0.6"
}
@ -2132,6 +2143,14 @@
"integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=",
"dev": true
},
"css-in-js-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.0.tgz",
"integrity": "sha512-yuWmPMD9FLi50Xf3k8W8oO3WM1eVnxEGCldCLyfusQ+CgivFk0s23yst4ooW6tfxMuSa03S6uUEga9UhX6GRrA==",
"requires": {
"hyphenate-style-name": "1.0.2"
}
},
"css-loader": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.23.1.tgz",
@ -2349,11 +2368,6 @@
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
"delegate": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.1.3.tgz",
"integrity": "sha1-moJRp3fXAl+qVXN7w7BxdCEnqf0="
},
"depd": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
@ -2434,6 +2448,16 @@
"integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=",
"dev": true
},
"downloadjs": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz",
"integrity": "sha1-9p+W+UDg0FU9rCkROYZaPNAQHjw="
},
"dynamics.js": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/dynamics.js/-/dynamics.js-1.1.5.tgz",
"integrity": "sha1-uQvcM2Bc7+ZSuEFucB95v27vzjI="
},
"ecc-jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
@ -2456,6 +2480,11 @@
"integrity": "sha1-PcyZ2j5rZl9qu8ccKK1Ros1zGpw=",
"dev": true
},
"element-resize-event": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/element-resize-event/-/element-resize-event-2.0.9.tgz",
"integrity": "sha1-L14VgaKW61J1IQwUG8VjQuIY+HY="
},
"emitter-component": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.1.tgz",
@ -3149,14 +3178,6 @@
"websocket-driver": "0.6.5"
}
},
"fbemitter": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-2.1.1.tgz",
"integrity": "sha1-Uj4U/a9SSIBbsC9i78M75wP1GGU=",
"requires": {
"fbjs": "0.8.14"
}
},
"fbjs": {
"version": "0.8.14",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.14.tgz",
@ -3298,15 +3319,6 @@
"integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=",
"dev": true
},
"flux": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/flux/-/flux-3.1.3.tgz",
"integrity": "sha1-0jvtUVp5oi2TOrU6tK2hnQWy8Io=",
"requires": {
"fbemitter": "2.1.1",
"fbjs": "0.8.14"
}
},
"font-awesome": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
@ -4148,14 +4160,6 @@
}
}
},
"string_decoder": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"requires": {
"safe-buffer": "5.0.1"
}
},
"string-width": {
"version": "1.0.2",
"bundled": true,
@ -4166,6 +4170,14 @@
"strip-ansi": "3.0.1"
}
},
"string_decoder": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"requires": {
"safe-buffer": "5.0.1"
}
},
"stringstream": {
"version": "0.0.5",
"bundled": true,
@ -4325,7 +4337,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@ -4376,7 +4388,7 @@
"globals": {
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
"integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==",
"integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=",
"dev": true
},
"globby": {
@ -4393,14 +4405,6 @@
"pinkie-promise": "2.0.1"
}
},
"good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
"requires": {
"delegate": "3.1.3"
}
},
"graceful-fs": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
@ -4490,12 +4494,6 @@
"isarray": "0.0.1"
}
},
"has-color": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz",
"integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=",
"dev": true
},
"has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
@ -4533,7 +4531,7 @@
"history": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz",
"integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==",
"integrity": "sha1-IrXH8xYzxbgCHH9KipVKwTnujVs=",
"requires": {
"invariant": "2.2.2",
"loose-envify": "1.3.1",
@ -4566,7 +4564,7 @@
"hosted-git-info": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz",
"integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==",
"integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=",
"dev": true
},
"html-comment-regex": {
@ -4643,10 +4641,15 @@
"integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=",
"dev": true
},
"hyphenate-style-name": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz",
"integrity": "sha1-MRYKNpMK2vH8BMYHT360FGXU7Es="
},
"iconv-lite": {
"version": "0.4.18",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz",
"integrity": "sha512-sr1ZQph3UwHTR0XftSbK85OvBbxe/abLGzEnPENCQwmHf7sck8Oyu4ob3LgBxWWxRoM+QszeUyl7jbqapu2TqA=="
"integrity": "sha1-I9hlaxaq5nQqwpcy6o8DNqR4nPI="
},
"icss-replace-symbols": {
"version": "1.1.0",
@ -4666,6 +4669,11 @@
"integrity": "sha1-QyNS5XrM2HqzEQ6C0/6g5HgSFW0=",
"dev": true
},
"immutable": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
"integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
},
"imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@ -4709,6 +4717,15 @@
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true
},
"inline-style-prefixer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-4.0.0.tgz",
"integrity": "sha1-MKA98bNGumsfuKgSvDydq+9IAi0=",
"requires": {
"bowser": "1.9.2",
"css-in-js-utils": "2.0.0"
}
},
"inquirer": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz",
@ -5014,42 +5031,6 @@
"whatwg-fetch": "2.0.3"
}
},
"isparta": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/isparta/-/isparta-4.0.0.tgz",
"integrity": "sha1-HekZlvSAsi3LGsqFECVbrhV0RG4=",
"dev": true,
"requires": {
"babel-core": "6.26.0",
"escodegen": "1.8.1",
"esprima": "2.7.3",
"istanbul": "0.4.5",
"mkdirp": "0.5.1",
"nomnomnomnom": "2.0.1",
"object-assign": "4.1.1",
"source-map": "0.5.6",
"which": "1.3.0"
}
},
"isparta-instrumenter-loader": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/isparta-instrumenter-loader/-/isparta-instrumenter-loader-1.0.1.tgz",
"integrity": "sha1-nDCMm+A6e8fjC62bbuh8ID4ClSY=",
"dev": true,
"requires": {
"isparta": "4.0.0",
"loader-utils": "0.2.17",
"lodash": "3.10.1"
},
"dependencies": {
"lodash": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
"integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=",
"dev": true
}
}
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@ -5114,6 +5095,11 @@
"integrity": "sha1-8OgK4DmkvWVLXygfyT8EqRSn/M4=",
"dev": true
},
"js-file-download": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.1.tgz",
"integrity": "sha1-3g3S1mHVY19QanO5YqtY3bZQvts="
},
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
@ -5142,6 +5128,11 @@
"integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=",
"dev": true
},
"json-loader": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz",
"integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w=="
},
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
@ -5197,9 +5188,9 @@
"dev": true
},
"jsonschema": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.1.1.tgz",
"integrity": "sha1-PO3o4+QR03eHLu+8n98mODy8Ptk="
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.2.tgz",
"integrity": "sha512-iX5OFQ6yx9NgbHCwse51ohhKgLuLL7Z5cNOeZOPIlDUtAMrxlruHLzVZxbltdHE5mEDXN+75oFOwq6Gn0MZwsA=="
},
"jsprim": {
"version": "1.4.1",
@ -5227,6 +5218,11 @@
"integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=",
"dev": true
},
"jwt-decode": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
"integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk="
},
"karma": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/karma/-/karma-1.7.1.tgz",
@ -5830,7 +5826,7 @@
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
@ -5922,7 +5918,7 @@
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
"dev": true,
"requires": {
"brace-expansion": "1.1.8"
@ -6005,9 +6001,9 @@
}
},
"moment": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz",
"integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8="
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz",
"integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ=="
},
"ms": {
"version": "2.0.0",
@ -6028,6 +6024,14 @@
"dev": true,
"optional": true
},
"narcissus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/narcissus/-/narcissus-1.0.0.tgz",
"integrity": "sha1-JGKgfEWYzpBl60Gyq72zDQ4w9G4=",
"requires": {
"inline-style-prefixer": "4.0.0"
}
},
"natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -6114,41 +6118,6 @@
}
}
},
"nomnomnomnom": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/nomnomnomnom/-/nomnomnomnom-2.0.1.tgz",
"integrity": "sha1-siOfAxyNBNpn4yg24eMZnhL3qOI=",
"dev": true,
"requires": {
"chalk": "0.4.0",
"underscore": "1.6.0"
},
"dependencies": {
"ansi-styles": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz",
"integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=",
"dev": true
},
"chalk": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz",
"integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=",
"dev": true,
"requires": {
"ansi-styles": "1.0.0",
"has-color": "0.1.7",
"strip-ansi": "0.1.1"
}
},
"strip-ansi": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz",
"integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=",
"dev": true
}
}
},
"noms": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz",
@ -6171,7 +6140,7 @@
"normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
"integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
"integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=",
"dev": true,
"requires": {
"hosted-git-info": "2.5.0",
@ -7218,7 +7187,7 @@
"promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=",
"requires": {
"asap": "2.0.6"
}
@ -7267,7 +7236,7 @@
"psl": {
"version": "1.1.20",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.20.tgz",
"integrity": "sha512-JWUi+8DYZnEn9vfV0ppHFLBP0Lk7wxzpobILpBEMDV4nFket4YK+6Rn1Zn6DHmD9PqqsV96AM6l4R/2oirzkgw=="
"integrity": "sha1-NjOC8zI4iICxVeJQY0WVcIQojp0="
},
"punycode": {
"version": "1.4.1",
@ -7329,7 +7298,7 @@
"randomatic": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
"integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==",
"integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=",
"dev": true,
"requires": {
"is-number": "3.0.0",
@ -7392,6 +7361,15 @@
}
}
},
"rc-progress": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-2.2.5.tgz",
"integrity": "sha1-5h0FRL+dQgjlujL8UJYhWef5UqM=",
"requires": {
"babel-runtime": "6.25.0",
"prop-types": "15.5.10"
}
},
"react": {
"version": "15.6.1",
"resolved": "https://registry.npmjs.org/react/-/react-15.6.1.tgz",
@ -7439,6 +7417,14 @@
"warning": "3.0.0"
}
},
"react-center-component": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/react-center-component/-/react-center-component-3.0.0.tgz",
"integrity": "sha1-0omGv0NOD46/9jyRJ38b9q0YnHI=",
"requires": {
"lodash": "4.17.4"
}
},
"react-copy-to-clipboard": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.0.tgz",
@ -7456,17 +7442,12 @@
"lodash": "4.17.4"
}
},
"react-data-grid": {
"version": "2.0.58",
"resolved": "https://registry.npmjs.org/react-data-grid/-/react-data-grid-2.0.58.tgz",
"integrity": "sha1-g5Rr+cw00URVI0nDQ+FYiMLGKTw="
},
"react-data-grid-addons": {
"version": "2.0.58",
"resolved": "https://registry.npmjs.org/react-data-grid-addons/-/react-data-grid-addons-2.0.58.tgz",
"integrity": "sha1-fi2f48741CjCAChuuzHBbJPQTGc=",
"react-dimensions": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/react-dimensions/-/react-dimensions-1.3.1.tgz",
"integrity": "sha512-go5vMuGUxaB5PiTSIk+ZfAxLbHwcIgIfLhkBZ2SIMQjaCgnpttxa30z5ijEzfDjeOCTGRpxvkzcmE4Vt4Ppvyw==",
"requires": {
"react-data-grid": "2.0.58"
"element-resize-event": "2.0.9"
}
},
"react-dom": {
@ -7544,27 +7525,39 @@
"react-base16-styling": "0.5.3"
}
},
"react-json-view": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/react-json-view/-/react-json-view-1.12.1.tgz",
"integrity": "sha512-klz1PrwmMcX8tQuvutsoBOABqLtyLKudeXi9Pv0MkBF1Ja1d6JnXrZdYZxIH9WOdwLpCHA+7NnKsdFc/RGFBqg==",
"requires": {
"clipboard": "1.7.1",
"flux": "3.1.3",
"react-base16-styling": "0.5.3"
}
},
"react-jsonschema-form": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/react-jsonschema-form/-/react-jsonschema-form-0.49.0.tgz",
"integrity": "sha512-MTbsAY5s7hNhneVFpSfQjflLal2kuK9YYpdWdr8l54K0B6O4EDIIwpp5visc+1QCs04w2FUo8HqvItaFsQYqig==",
"version": "0.50.1",
"resolved": "https://registry.npmjs.org/react-jsonschema-form/-/react-jsonschema-form-0.50.1.tgz",
"integrity": "sha512-pjhbIQhLmaezfRXZDYgEpM3Ua5m7Pz+jGiQBmRlBjSQ056uegUCKehxY7h4C6qJL8xH3RagteYDuHXxg2yYyww==",
"requires": {
"jsonschema": "1.1.1",
"jsonschema": "1.2.2",
"lodash.topath": "4.5.2",
"prop-types": "15.5.10",
"setimmediate": "1.0.5"
}
},
"react-modal-dialog": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/react-modal-dialog/-/react-modal-dialog-4.0.7.tgz",
"integrity": "sha1-OSbaLfqR/wny0xSVSejW7ly62bU=",
"requires": {
"active-event-stack": "1.0.0",
"classnames": "2.2.5",
"dynamics.js": "1.1.5",
"immutable": "3.8.2",
"keycode": "2.1.9",
"lodash": "3.10.1",
"narcissus": "1.0.0",
"react-center-component": "3.0.0"
},
"dependencies": {
"lodash": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
"integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y="
}
}
},
"react-overlays": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.7.0.tgz",
@ -7601,7 +7594,7 @@
"react-router": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.2.0.tgz",
"integrity": "sha512-DY6pjwRhdARE4TDw7XjxjZsbx9lKmIcyZoZ+SDO7SBJ1KUeWNxT22Kara2AC7u6/c2SYEHlEDLnzBCcNhLE8Vg==",
"integrity": "sha1-Yfez43cNrrJAYtrj7t7xsFQVWYY=",
"requires": {
"history": "4.7.2",
"hoist-non-react-statics": "2.3.1",
@ -7625,7 +7618,7 @@
"react-router-dom": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.2.2.tgz",
"integrity": "sha512-cHMFC1ZoLDfEaMFoKTjN7fry/oczMgRt5BKfMAkTu5zEuJvUiPp1J8d0eXSVTnBh6pxlbdqDhozunOOLtmKfPA==",
"integrity": "sha1-yKgd863Fi7qKdngulGy9Tq5km40=",
"requires": {
"history": "4.7.2",
"invariant": "2.2.2",
@ -7635,6 +7628,22 @@
"warning": "3.0.0"
}
},
"react-table": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/react-table/-/react-table-6.8.0.tgz",
"integrity": "sha1-XOQC63Nd9oU0wD2rs/qgMUeLalg=",
"requires": {
"classnames": "2.2.5"
}
},
"react-toggle": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/react-toggle/-/react-toggle-4.0.2.tgz",
"integrity": "sha512-EPTWnN7gQHgEAUEmjheanZXNzY5TPnQeyyHfEs3YshaiWZf5WNjfYDrglO5F1Hl/dNveX18i4l0grTEsYH2Ccw==",
"requires": {
"classnames": "2.2.5"
}
},
"read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
@ -7781,7 +7790,7 @@
"redux": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
"integrity": "sha1-BrcxIyFZAdJdBlvjQusCa8HIU3s=",
"requires": {
"lodash": "4.17.4",
"lodash-es": "4.17.4",
@ -7955,7 +7964,7 @@
"resolve-pathname": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz",
"integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg=="
"integrity": "sha1-fpriHtgV/WOrGJre7mTcgx7vqHk="
},
"restore-cursor": {
"version": "1.0.1",
@ -8009,7 +8018,7 @@
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
"integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=",
"dev": true
},
"sax": {
@ -8018,11 +8027,6 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0="
},
"semver": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz",
@ -8349,7 +8353,7 @@
"source-map-support": {
"version": "0.4.17",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.17.tgz",
"integrity": "sha512-30c1Ch8FSjV0FwC253iftbbj0dU/OXoSg1LAEGZJUlGgjTNj6cu+DVqJWWIZJY5RXLWV4eFtR+4ouo0VIOYOTg==",
"integrity": "sha1-byFQVT5jdTddDMsxgFAreMGLpDA=",
"dev": true,
"requires": {
"source-map": "0.5.6"
@ -8511,12 +8515,6 @@
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
"dev": true
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@ -8528,6 +8526,12 @@
"strip-ansi": "3.0.1"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true
},
"stringstream": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
@ -8717,7 +8721,7 @@
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
@ -8741,11 +8745,6 @@
"setimmediate": "1.0.5"
}
},
"tiny-emitter": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz",
"integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow=="
},
"tmp": {
"version": "0.0.31",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz",
@ -8895,12 +8894,6 @@
"invariant": "2.2.2"
}
},
"underscore": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz",
"integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=",
"dev": true
},
"uniq": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
@ -9054,7 +9047,7 @@
"value-equal": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz",
"integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw=="
"integrity": "sha1-xb3S9U7gk8BIOdcc4uR1imiQq8c="
},
"vary": {
"version": "1.1.1",
@ -9095,7 +9088,7 @@
"emitter-component": "1.1.1",
"hammerjs": "2.0.8",
"keycharm": "0.2.0",
"moment": "2.18.1",
"moment": "2.21.0",
"propagating-hammerjs": "1.4.6"
}
},
@ -9342,7 +9335,7 @@
"which": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz",
"integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==",
"integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=",
"dev": true,
"requires": {
"isexe": "2.0.0"

View File

@ -38,7 +38,6 @@
"eslint-plugin-react": "^6.0.0",
"file-loader": "^0.9.0",
"glob": "^7.0.0",
"isparta-instrumenter-loader": "^1.0.0",
"karma": "^1.7.1",
"karma-chai": "^0.1.0",
"karma-coverage": "^1.0.0",
@ -63,10 +62,12 @@
"dependencies": {
"bootstrap": "^3.3.7",
"core-js": "^2.5.1",
"downloadjs": "^1.4.7",
"fetch": "^1.1.0",
"js-file-download": "^0.4.1",
"json-loader": "^0.5.7",
"jwt-decode": "^2.2.0",
"moment": "^2.21.0",
"normalize.css": "^4.0.0",
"prop-types": "^15.5.10",
"rc-progress": "^2.2.5",

View File

@ -97,7 +97,7 @@ class AppComponent extends AuthComponent {
<li>
<NavLink to="/" exact={true}>
<span className="number">1.</span>
Run C&C Server
Run Monkey Island Server
{this.state.completedSteps.run_server ?
<Icon name="check" className="pull-right checkmark text-success"/>
: ''}

View File

@ -2,6 +2,7 @@ import React from 'react';
import {Icon} from 'react-fa';
import Toggle from 'react-toggle';
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import download from 'downloadjs'
import AuthComponent from '../../AuthComponent';
class PreviewPaneComponent extends AuthComponent {
@ -82,16 +83,56 @@ class PreviewPaneComponent extends AuthComponent {
</th>
<td>
<Toggle id={asset.id} checked={!asset.config.alive} icons={false} disabled={asset.dead}
onChange={(e) => this.forceKill(e, asset)} />
onChange={(e) => this.forceKill(e, asset)}/>
</td>
</tr>
);
}
unescapeLog(st) {
return st.substr(1, st.length - 2) // remove quotation marks on beginning and end of string.
.replace(/\\n/g, "\n")
.replace(/\\r/g, "\r")
.replace(/\\t/g, "\t")
.replace(/\\b/g, "\b")
.replace(/\\f/g, "\f")
.replace(/\\"/g, '\"')
.replace(/\\'/g, "\'")
.replace(/\\&/g, "\&");
}
downloadLog(asset) {
fetch('/api/log?id=' + asset.id)
.then(res => res.json())
.then(res => {
let timestamp = res['timestamp'];
timestamp = timestamp.substr(0, timestamp.indexOf('.'));
let filename = res['monkey_label'].split(':').join('-') + ' - ' + timestamp + '.log';
let logContent = this.unescapeLog(res['log']);
download(logContent, filename, 'text/plain');
});
}
downloadLogRow(asset) {
return (
<tr>
<th>
Download Log
</th>
<td>
<a type="button" className="btn btn-primary"
disabled={!asset.has_log}
onClick={() => this.downloadLog(asset)}>Download</a>
</td>
</tr>
);
}
exploitsTimeline(asset) {
if (asset.exploits.length === 0) {
return (<div />);
return (<div/>);
}
return (
@ -101,9 +142,9 @@ class PreviewPaneComponent extends AuthComponent {
{this.generateToolTip('Timeline of exploit attempts. Red is successful. Gray is unsuccessful')}
</h4>
<ul className="timeline">
{ asset.exploits.map(exploit =>
{asset.exploits.map(exploit =>
<li key={exploit.timestamp}>
<div className={'bullet ' + (exploit.result ? 'bad' : '')} />
<div className={'bullet ' + (exploit.result ? 'bad' : '')}/>
<div>{new Date(exploit.timestamp).toLocaleString()}</div>
<div>{exploit.origin}</div>
<div>{exploit.exploiter}</div>
@ -119,10 +160,10 @@ class PreviewPaneComponent extends AuthComponent {
<div>
<table className="table table-condensed">
<tbody>
{this.osRow(asset)}
{this.ipsRow(asset)}
{this.servicesRow(asset)}
{this.accessibleRow(asset)}
{this.osRow(asset)}
{this.ipsRow(asset)}
{this.servicesRow(asset)}
{this.accessibleRow(asset)}
</tbody>
</table>
{this.exploitsTimeline(asset)}
@ -135,12 +176,13 @@ class PreviewPaneComponent extends AuthComponent {
<div>
<table className="table table-condensed">
<tbody>
{this.osRow(asset)}
{this.statusRow(asset)}
{this.ipsRow(asset)}
{this.servicesRow(asset)}
{this.accessibleRow(asset)}
{this.forceKillRow(asset)}
{this.osRow(asset)}
{this.statusRow(asset)}
{this.ipsRow(asset)}
{this.servicesRow(asset)}
{this.accessibleRow(asset)}
{this.forceKillRow(asset)}
{this.downloadLogRow(asset)}
</tbody>
</table>
{this.exploitsTimeline(asset)}
@ -173,9 +215,9 @@ class PreviewPaneComponent extends AuthComponent {
<div>
<h4 style={{'marginTop': '2em'}}>Timeline</h4>
<ul className="timeline">
{ edge.exploits.map(exploit =>
{edge.exploits.map(exploit =>
<li key={exploit.timestamp}>
<div className={'bullet ' + (exploit.result ? 'bad' : '')} />
<div className={'bullet ' + (exploit.result ? 'bad' : '')}/>
<div>{new Date(exploit.timestamp).toLocaleString()}</div>
<div>{exploit.origin}</div>
<div>{exploit.exploiter}</div>
@ -206,8 +248,8 @@ class PreviewPaneComponent extends AuthComponent {
this.infectedAssetInfo(this.props.item) : this.assetInfo(this.props.item);
break;
case 'island_edge':
info = this.islandEdgeInfo();
break;
info = this.islandEdgeInfo();
break;
}
let label = '';
@ -221,12 +263,12 @@ class PreviewPaneComponent extends AuthComponent {
return (
<div className="preview-pane">
{ !info ?
{!info ?
<span>
<Icon name="hand-o-left" style={{'marginRight': '0.5em'}} />
<Icon name="hand-o-left" style={{'marginRight': '0.5em'}}/>
Select an item on the map for a detailed look
</span>
:
:
<div>
<h3>
{label}

View File

@ -611,7 +611,7 @@ class ReportPageComponent extends AuthComponent {
The network can probably be segmented. A monkey instance on <span
className="label label-primary">{issue.machine}</span> in the
networks {this.generateInfoBadges(issue.networks)}
could directly access the Monkey Island C&C server in the
could directly access the Monkey Island server in the
networks {this.generateInfoBadges(issue.server_networks)}.
</CollapsibleWellComponent>
</li>

View File

@ -147,7 +147,7 @@ class RunMonkeyPageComponent extends AuthComponent {
className="btn btn-default btn-lg center-block"
disabled={this.state.runningOnIslandState !== 'not_running'}
>
Run on C&C Server
Run on Monkey Island Server
{ this.renderIconByState(this.state.runningOnIslandState) }
</button>
{

View File

@ -10,7 +10,7 @@ class RunServerPageComponent extends React.Component {
render() {
return (
<Col xs={12} lg={8}>
<h1 className="page-title">1. Monkey Island C&C Server</h1>
<h1 className="page-title">1. Monkey Island Server</h1>
<div style={{'fontSize': '1.2em'}}>
<p style={{'marginTop': '30px'}}>Congrats! You have successfully set up the Monkey Island
server. &#x1F44F; &#x1F44F;</p>
@ -18,7 +18,7 @@ class RunServerPageComponent extends React.Component {
The Infection Monkey is an open source security tool for testing a data center's resiliency to perimeter
breaches and internal server infections.
The Monkey uses various methods to propagate across a data
center and reports to this Command and Control (C&C) server.
center and reports to this Monkey Island Command and Control server.
</p>
<p>
To read more about the Monkey, visit <a href="http://infectionmonkey.com"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8">
<title>Infection Monkey C&C</title>
<title>Infection Monkey Island Server</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

View File

@ -4,5 +4,5 @@ Maintainer: Guardicore
Homepage: http://www.guardicore.com
Priority: optional
Version: 1.0
Description: Guardicore Infection Monkey Island (C&C) installation package
Description: Guardicore Infection Monkey Island installation package
Depends: openssl, python-pip

View File

@ -13,4 +13,5 @@ jsonschema
netifaces
ipaddress
enum34
PyCrypto
virtualenv

View File

@ -1,4 +1,4 @@
How to set C&C server:
How to set up the Monkey Island server:
---------------- On Windows ----------------:
1. Create folder "bin" under monkey_island
@ -18,7 +18,7 @@ How to set C&C server:
5.1. Download and install from: https://go.microsoft.com/fwlink/?LinkId=746572
6. Generate SSL Certificate
6.1. run create_certificate.bat when your current working directory is monkey_island
7. Create the monkey_island\cc\binaries folder and put chaos monkey binaries inside
7. Create the monkey_island\cc\binaries folder and put Infection Monkey binaries inside
monkey-linux-64 - monkey binary for linux 64bit
monkey-linux-32 - monkey binary for linux 32bit
monkey-windows-32.exe - monkey binary for windows 32bit

View File

@ -13,3 +13,4 @@ jsonschema
netifaces
ipaddress
enum34
PyCrypto