Merge branch 'nadler/pth' of github.com:guardicore/monkey into nadler/pth

This commit is contained in:
Oran Nadler 2018-02-28 04:53:18 -08:00
commit f3b013b8b1
45 changed files with 536 additions and 93 deletions

View File

@ -4,6 +4,8 @@
* [ ] Have you added an explanation of what your changes do and why you'd like to include them? * [ ] Have you added an explanation of what your changes do and why you'd like to include them?
* [ ] Have you successfully tested your changes locally? * [ ] Have you successfully tested your changes locally?
* Example screenshot/log transcript of the feature working
## Changes ## Changes
- -
- -

BIN
.github/Security-overview.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
.github/map-full.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -8,7 +8,9 @@ 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. 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.
![Infection Monkey map](.github/map-full.png) <img src=".github/map-full.png" >
<img src=".github/Security-overview.png" width="800" height="500">
The Infection Monkey is comprised of two parts: The Infection Monkey is comprised of two parts:
* Monkey - A tool which infects other machines and propagates to them * Monkey - A tool which infects other machines and propagates to them

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -1,22 +1,26 @@
import os
from datetime import datetime from datetime import datetime
import bson import bson
from bson.json_util import dumps
from flask import Flask, send_from_directory, redirect, make_response
import flask_restful import flask_restful
from bson.json_util import dumps
from flask import Flask, send_from_directory, make_response
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from cc.auth import init_jwt
from cc.database import mongo from cc.database import mongo
from cc.environment.environment import env
from cc.resources.client_run import ClientRun from cc.resources.client_run import ClientRun
from cc.resources.monkey import Monkey from cc.resources.edge import Edge
from cc.resources.local_run import LocalRun from cc.resources.local_run import LocalRun
from cc.resources.telemetry import Telemetry from cc.resources.monkey import Monkey
from cc.resources.monkey_configuration import MonkeyConfiguration from cc.resources.monkey_configuration import MonkeyConfiguration
from cc.resources.monkey_download import MonkeyDownload from cc.resources.monkey_download import MonkeyDownload
from cc.resources.netmap import NetMap from cc.resources.netmap import NetMap
from cc.resources.edge import Edge
from cc.resources.node import Node from cc.resources.node import Node
from cc.resources.report import Report from cc.resources.report import Report
from cc.resources.root import Root from cc.resources.root import Root
from cc.resources.telemetry import Telemetry
from cc.resources.telemetry_feed import TelemetryFeed from cc.resources.telemetry_feed import TelemetryFeed
from cc.services.config import ConfigService from cc.services.config import ConfigService
@ -70,6 +74,12 @@ def init_app(mongo_url):
api.representations = {'application/json': output_json} api.representations = {'application/json': output_json}
app.config['MONGO_URI'] = mongo_url app.config['MONGO_URI'] = mongo_url
app.config['SECRET_KEY'] = os.urandom(32)
app.config['JWT_AUTH_URL_RULE'] = '/api/auth'
app.config['JWT_EXPIRATION_DELTA'] = env.get_auth_expiration_time()
init_jwt(app)
mongo.init_app(app) mongo.init_app(app)
with app.app_context(): with app.app_context():

53
monkey_island/cc/auth.py Normal file
View File

@ -0,0 +1,53 @@
from functools import wraps
from flask import current_app, abort
from flask_jwt import JWT, _jwt_required, JWTError
from werkzeug.security import safe_str_cmp
from cc.environment.environment import env
__author__ = 'itay.mizeretz'
class User(object):
def __init__(self, id, username, secret):
self.id = id
self.username = username
self.secret = secret
def __str__(self):
return "User(id='%s')" % self.id
def init_jwt(app):
users = env.get_auth_users()
username_table = {u.username: u for u in users}
userid_table = {u.id: u for u in users}
def authenticate(username, secret):
user = username_table.get(username, None)
if user and safe_str_cmp(user.secret.encode('utf-8'), secret.encode('utf-8')):
return user
def identity(payload):
user_id = payload['identity']
return userid_table.get(user_id, None)
if env.is_auth_enabled():
JWT(app, authenticate, identity)
def jwt_required(realm=None):
def wrapper(fn):
@wraps(fn)
def decorator(*args, **kwargs):
if env.is_auth_enabled():
try:
_jwt_required(realm or current_app.config['JWT_DEFAULT_REALM'])
except JWTError:
abort(401)
return fn(*args, **kwargs)
return decorator
return wrapper

View File

@ -0,0 +1,33 @@
import abc
from datetime import timedelta
__author__ = 'itay.mizeretz'
class Environment(object):
__metaclass__ = abc.ABCMeta
_ISLAND_PORT = 5000
_MONGO_URL = "mongodb://localhost:27017/monkeyisland"
_DEBUG_SERVER = False
_AUTH_EXPIRATION_TIME = timedelta(hours=1)
def get_island_port(self):
return self._ISLAND_PORT
def get_mongo_url(self):
return self._MONGO_URL
def is_debug(self):
return self._DEBUG_SERVER
def get_auth_expiration_time(self):
return self._AUTH_EXPIRATION_TIME
@abc.abstractmethod
def is_auth_enabled(self):
return
@abc.abstractmethod
def get_auth_users(self):
return

View File

@ -0,0 +1,24 @@
import urllib2
import cc.auth
from cc.environment import Environment
__author__ = 'itay.mizeretz'
class AwsEnvironment(Environment):
def __init__(self):
super(AwsEnvironment, self).__init__()
self._instance_id = AwsEnvironment._get_instance_id()
@staticmethod
def _get_instance_id():
return urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read()
def is_auth_enabled(self):
return True
def get_auth_users(self):
return [
cc.auth.User(1, 'monkey', self._instance_id)
]

View File

@ -0,0 +1,23 @@
import json
import standard
import aws
ENV_DICT = {
'standard': standard.StandardEnvironment,
'aws': aws.AwsEnvironment
}
def load_env_from_file():
with open('server_config.json', 'r') as f:
config_content = f.read()
config_json = json.loads(config_content)
return config_json['server_config']
try:
__env_type = load_env_from_file()
env = ENV_DICT[__env_type]()
except Exception:
print('Failed initializing environment: %s' % __env_type)
raise

View File

@ -0,0 +1,12 @@
from cc.environment import Environment
__author__ = 'itay.mizeretz'
class StandardEnvironment(Environment):
def is_auth_enabled(self):
return False
def get_auth_users(self):
return []

View File

@ -1,5 +0,0 @@
__author__ = 'itay.mizeretz'
ISLAND_PORT = 5000
DEFAULT_MONGO_URL = "mongodb://localhost:27017/monkeyisland"
DEBUG_SERVER = False

View File

@ -11,7 +11,7 @@ if BASE_PATH not in sys.path:
from cc.app import init_app from cc.app import init_app
from cc.utils import local_ip_addresses from cc.utils import local_ip_addresses
from cc.island_config import DEFAULT_MONGO_URL, ISLAND_PORT, DEBUG_SERVER from cc.environment.environment import env
from cc.database import is_db_server_up from cc.database import is_db_server_up
if __name__ == '__main__': if __name__ == '__main__':
@ -19,20 +19,20 @@ if __name__ == '__main__':
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
mongo_url = os.environ.get('MONGO_URL', DEFAULT_MONGO_URL) mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url())
while not is_db_server_up(mongo_url): while not is_db_server_up(mongo_url):
print('Waiting for MongoDB server') print('Waiting for MongoDB server')
time.sleep(1) time.sleep(1)
app = init_app(mongo_url) app = init_app(mongo_url)
if DEBUG_SERVER: if env.is_debug():
app.run(host='0.0.0.0', debug=True, ssl_context=('server.crt', 'server.key')) app.run(host='0.0.0.0', debug=True, ssl_context=('server.crt', 'server.key'))
else: else:
http_server = HTTPServer(WSGIContainer(app), http_server = HTTPServer(WSGIContainer(app),
ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'), ssl_options={'certfile': os.environ.get('SERVER_CRT', 'server.crt'),
'keyfile': os.environ.get('SERVER_KEY', 'server.key')}) 'keyfile': os.environ.get('SERVER_KEY', 'server.key')})
http_server.listen(ISLAND_PORT) http_server.listen(env.get_island_port())
print('Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], ISLAND_PORT)) print('Monkey Island Server is running on https://{}:{}'.format(local_ip_addresses()[0], env.get_island_port()))
IOLoop.instance().start() IOLoop.instance().start()

View File

@ -6,8 +6,8 @@ import sys
from flask import request, jsonify, make_response from flask import request, jsonify, make_response
import flask_restful import flask_restful
from cc.environment.environment import env
from cc.resources.monkey_download import get_monkey_executable from cc.resources.monkey_download import get_monkey_executable
from cc.island_config import ISLAND_PORT
from cc.services.node import NodeService from cc.services.node import NodeService
from cc.utils import local_ip_addresses from cc.utils import local_ip_addresses
@ -36,7 +36,7 @@ def run_local_monkey():
# run the monkey # run the monkey
try: try:
args = ['"%s" m0nk3y -s %s:%s' % (target_path, local_ip_addresses()[0], ISLAND_PORT)] args = ['"%s" m0nk3y -s %s:%s' % (target_path, local_ip_addresses()[0], env.get_island_port())]
if sys.platform == "win32": if sys.platform == "win32":
args = "".join(args) args = "".join(args)
pid = subprocess.Popen(args, shell=True).pid pid = subprocess.Popen(args, shell=True).pid

View File

@ -15,23 +15,20 @@ __author__ = 'Barak'
class Monkey(flask_restful.Resource): class Monkey(flask_restful.Resource):
# Used by monkey. can't secure.
def get(self, guid=None, **kw): def get(self, guid=None, **kw):
NodeService.update_dead_monkeys() # refresh monkeys status NodeService.update_dead_monkeys() # refresh monkeys status
if not guid: if not guid:
guid = request.args.get('guid') guid = request.args.get('guid')
timestamp = request.args.get('timestamp')
if guid: if guid:
monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid}) monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid})
return monkey_json return monkey_json
else:
result = {'timestamp': datetime.now().isoformat()}
find_filter = {}
if timestamp is not None:
find_filter['modifytime'] = {'$gt': dateutil.parser.parse(timestamp)}
result['objects'] = [x for x in mongo.db.monkey.find(find_filter)]
return result
return {}
# Used by monkey. can't secure.
def patch(self, guid): def patch(self, guid):
monkey_json = json.loads(request.data) monkey_json = json.loads(request.data)
update = {"$set": {'modifytime': datetime.now()}} update = {"$set": {'modifytime': datetime.now()}}
@ -51,6 +48,7 @@ class Monkey(flask_restful.Resource):
return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False) return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False)
# Used by monkey. can't secure.
def post(self, **kw): def post(self, **kw):
monkey_json = json.loads(request.data) monkey_json = json.loads(request.data)
monkey_json['creds'] = [] monkey_json['creds'] = []

View File

@ -1,18 +1,20 @@
import json import json
from flask import request, jsonify
import flask_restful import flask_restful
from flask import request, jsonify
from cc.database import mongo from cc.auth import jwt_required
from cc.services.config import ConfigService from cc.services.config import ConfigService
__author__ = 'Barak' __author__ = 'Barak'
class MonkeyConfiguration(flask_restful.Resource): class MonkeyConfiguration(flask_restful.Resource):
@jwt_required()
def get(self): 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())
@jwt_required()
def post(self): def post(self):
config_json = json.loads(request.data) config_json = json.loads(request.data)
if config_json.has_key('reset'): if config_json.has_key('reset'):
@ -20,4 +22,3 @@ class MonkeyConfiguration(flask_restful.Resource):
else: else:
ConfigService.update_config(config_json) ConfigService.update_config(config_json)
return self.get() return self.get()

View File

@ -47,9 +47,12 @@ def get_monkey_executable(host_os, machine):
class MonkeyDownload(flask_restful.Resource): class MonkeyDownload(flask_restful.Resource):
# Used by monkey. can't secure.
def get(self, path): def get(self, path):
return send_from_directory('binaries', path) return send_from_directory('binaries', path)
# Used by monkey. can't secure.
def post(self): def post(self):
host_json = json.loads(request.data) host_json = json.loads(request.data)
host_os = host_json.get('os') host_os = host_json.get('os')

View File

@ -1,5 +1,6 @@
import flask_restful import flask_restful
from cc.auth import jwt_required
from cc.services.edge import EdgeService from cc.services.edge import EdgeService
from cc.services.node import NodeService from cc.services.node import NodeService
from cc.database import mongo from cc.database import mongo
@ -8,6 +9,7 @@ __author__ = 'Barak'
class NetMap(flask_restful.Resource): class NetMap(flask_restful.Resource):
@jwt_required()
def get(self, **kw): def get(self, **kw):
monkeys = [NodeService.monkey_to_net_node(x) for x in mongo.db.monkey.find({})] monkeys = [NodeService.monkey_to_net_node(x) for x in mongo.db.monkey.find({})]
nodes = [NodeService.node_to_net_node(x) for x in mongo.db.node.find({})] nodes = [NodeService.node_to_net_node(x) for x in mongo.db.node.find({})]

View File

@ -1,12 +1,14 @@
from flask import request from flask import request
import flask_restful import flask_restful
from cc.auth import jwt_required
from cc.services.node import NodeService from cc.services.node import NodeService
__author__ = 'Barak' __author__ = 'Barak'
class Node(flask_restful.Resource): class Node(flask_restful.Resource):
@jwt_required()
def get(self): def get(self):
node_id = request.args.get('id') node_id = request.args.get('id')
if node_id: if node_id:

View File

@ -1,10 +1,13 @@
import flask_restful import flask_restful
from cc.auth import jwt_required
from cc.services.report import ReportService from cc.services.report import ReportService
__author__ = "itay.mizeretz" __author__ = "itay.mizeretz"
class Report(flask_restful.Resource): class Report(flask_restful.Resource):
@jwt_required()
def get(self): def get(self):
return ReportService.get_report() return ReportService.get_report()

View File

@ -3,6 +3,7 @@ from datetime import datetime
import flask_restful import flask_restful
from flask import request, make_response, jsonify from flask import request, make_response, jsonify
from cc.auth import jwt_required
from cc.database import mongo from cc.database import mongo
from cc.services.config import ConfigService from cc.services.config import ConfigService
from cc.services.node import NodeService from cc.services.node import NodeService
@ -13,6 +14,8 @@ __author__ = 'Barak'
class Root(flask_restful.Resource): class Root(flask_restful.Resource):
@jwt_required()
def get(self, action=None): def get(self, action=None):
if not action: if not action:
action = request.args.get('action') action = request.args.get('action')

View File

@ -7,6 +7,7 @@ import dateutil
import flask_restful import flask_restful
from flask import request from flask import request
from cc.auth import jwt_required
from cc.database import mongo from cc.database import mongo
from cc.services.config import ConfigService from cc.services.config import ConfigService
from cc.services.edge import EdgeService from cc.services.edge import EdgeService
@ -16,6 +17,7 @@ __author__ = 'Barak'
class Telemetry(flask_restful.Resource): class Telemetry(flask_restful.Resource):
@jwt_required()
def get(self, **kw): def get(self, **kw):
monkey_guid = request.args.get('monkey_guid') monkey_guid = request.args.get('monkey_guid')
telem_type = request.args.get('telem_type') telem_type = request.args.get('telem_type')
@ -36,6 +38,7 @@ class Telemetry(flask_restful.Resource):
result['objects'] = self.telemetry_to_displayed_telemetry(mongo.db.telemetry.find(find_filter)) result['objects'] = self.telemetry_to_displayed_telemetry(mongo.db.telemetry.find(find_filter))
return result return result
# Used by monkey. can't secure.
def post(self): def post(self):
telemetry_json = json.loads(request.data) telemetry_json = json.loads(request.data)
telemetry_json['timestamp'] = datetime.now() telemetry_json['timestamp'] = datetime.now()

View File

@ -5,6 +5,7 @@ import flask_restful
from flask import request from flask import request
import flask_pymongo import flask_pymongo
from cc.auth import jwt_required
from cc.database import mongo from cc.database import mongo
from cc.services.node import NodeService from cc.services.node import NodeService
@ -12,6 +13,7 @@ __author__ = 'itay.mizeretz'
class TelemetryFeed(flask_restful.Resource): class TelemetryFeed(flask_restful.Resource):
@jwt_required()
def get(self, **kw): def get(self, **kw):
timestamp = request.args.get('timestamp') timestamp = request.args.get('timestamp')
if "null" == timestamp or timestamp is None: # special case to avoid ugly JS code... if "null" == timestamp or timestamp is None: # special case to avoid ugly JS code...

View File

@ -0,0 +1,3 @@
{
"server_config": "standard"
}

View File

@ -1,7 +1,7 @@
from cc.database import mongo from cc.database import mongo
from jsonschema import Draft4Validator, validators from jsonschema import Draft4Validator, validators
from cc.island_config import ISLAND_PORT from cc.environment.environment import env
from cc.utils import local_ip_addresses from cc.utils import local_ip_addresses
__author__ = "itay.mizeretz" __author__ = "itay.mizeretz"
@ -885,8 +885,8 @@ class ConfigService:
@staticmethod @staticmethod
def set_server_ips_in_config(config): def set_server_ips_in_config(config):
ips = local_ip_addresses() ips = local_ip_addresses()
config["cnc"]["servers"]["command_servers"] = ["%s:%d" % (ip, ISLAND_PORT) for ip in ips] config["cnc"]["servers"]["command_servers"] = ["%s:%d" % (ip, env.get_island_port()) for ip in ips]
config["cnc"]["servers"]["current_server"] = "%s:%d" % (ips[0], ISLAND_PORT) config["cnc"]["servers"]["current_server"] = "%s:%d" % (ips[0], env.get_island_port())
@staticmethod @staticmethod
def save_initial_config_if_needed(): def save_initial_config_if_needed():

View File

@ -65,6 +65,8 @@
"core-js": "^2.5.1", "core-js": "^2.5.1",
"fetch": "^1.1.0", "fetch": "^1.1.0",
"js-file-download": "^0.4.1", "js-file-download": "^0.4.1",
"json-loader": "^0.5.7",
"jwt-decode": "^2.2.0",
"normalize.css": "^4.0.0", "normalize.css": "^4.0.0",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"rc-progress": "^2.2.5", "rc-progress": "^2.2.5",

View File

@ -0,0 +1,12 @@
import React from 'react';
import AuthService from '../services/AuthService';
class AuthComponent extends React.Component {
constructor(props) {
super(props);
this.auth = new AuthService();
this.authFetch = this.auth.authFetch;
}
}
export default AuthComponent;

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import {NavLink, Route, BrowserRouter as Router} from 'react-router-dom'; import {BrowserRouter as Router, NavLink, Redirect, Route} from 'react-router-dom';
import {Col, Grid, Row} from 'react-bootstrap'; import {Col, Grid, Row} from 'react-bootstrap';
import {Icon} from 'react-fa'; import {Icon} from 'react-fa';
@ -11,6 +11,8 @@ import TelemetryPage from 'components/pages/TelemetryPage';
import StartOverPage from 'components/pages/StartOverPage'; import StartOverPage from 'components/pages/StartOverPage';
import ReportPage from 'components/pages/ReportPage'; import ReportPage from 'components/pages/ReportPage';
import LicensePage from 'components/pages/LicensePage'; import LicensePage from 'components/pages/LicensePage';
import AuthComponent from 'components/AuthComponent';
import LoginPageComponent from 'components/pages/LoginPage';
require('normalize.css/normalize.css'); require('normalize.css/normalize.css');
require('react-data-components/css/table-twbs.css'); require('react-data-components/css/table-twbs.css');
@ -22,7 +24,43 @@ let logoImage = require('../images/monkey-icon.svg');
let infectionMonkeyImage = require('../images/infection-monkey.svg'); let infectionMonkeyImage = require('../images/infection-monkey.svg');
let guardicoreLogoImage = require('../images/guardicore-logo.png'); let guardicoreLogoImage = require('../images/guardicore-logo.png');
class AppComponent extends React.Component { class AppComponent extends AuthComponent {
updateStatus = () => {
if (this.auth.loggedIn()){
this.authFetch('/api')
.then(res => res.json())
.then(res => {
// This check is used to prevent unnecessary re-rendering
let isChanged = false;
for (let step in this.state.completedSteps) {
if (this.state.completedSteps[step] !== res['completed_steps'][step]) {
isChanged = true;
break;
}
}
if (isChanged) {
this.setState({completedSteps: res['completed_steps']});
}
});
}
};
renderRoute = (route_path, page_component, is_exact_path = false) => {
let render_func = (props) => {
if (this.auth.loggedIn()) {
return page_component;
} else {
return <Redirect to={{pathname: '/login'}}/>;
}
};
if (is_exact_path) {
return <Route exact path={route_path} render={render_func}/>;
} else {
return <Route path={route_path} render={render_func}/>;
}
};
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -35,24 +73,6 @@ class AppComponent extends React.Component {
}; };
} }
updateStatus = () => {
fetch('/api')
.then(res => res.json())
.then(res => {
// This check is used to prevent unnecessary re-rendering
let isChanged = false;
for (let step in this.state.completedSteps) {
if (this.state.completedSteps[step] !== res['completed_steps'][step]) {
isChanged = true;
break;
}
}
if (isChanged) {
this.setState({completedSteps: res['completed_steps']});
}
});
};
componentDidMount() { componentDidMount() {
this.updateStatus(); this.updateStatus();
this.interval = setInterval(this.updateStatus, 2000); this.interval = setInterval(this.updateStatus, 2000);
@ -78,7 +98,7 @@ class AppComponent extends React.Component {
<NavLink to="/" exact={true}> <NavLink to="/" exact={true}>
<span className="number">1.</span> <span className="number">1.</span>
Run Monkey Island Server Run Monkey Island Server
{ this.state.completedSteps.run_server ? {this.state.completedSteps.run_server ?
<Icon name="check" className="pull-right checkmark text-success"/> <Icon name="check" className="pull-right checkmark text-success"/>
: ''} : ''}
</NavLink> </NavLink>
@ -87,7 +107,7 @@ class AppComponent extends React.Component {
<NavLink to="/run-monkey"> <NavLink to="/run-monkey">
<span className="number">2.</span> <span className="number">2.</span>
Run Monkey Run Monkey
{ this.state.completedSteps.run_monkey ? {this.state.completedSteps.run_monkey ?
<Icon name="check" className="pull-right checkmark text-success"/> <Icon name="check" className="pull-right checkmark text-success"/>
: ''} : ''}
</NavLink> </NavLink>
@ -96,7 +116,7 @@ class AppComponent extends React.Component {
<NavLink to="/infection/map"> <NavLink to="/infection/map">
<span className="number">3.</span> <span className="number">3.</span>
Infection Map Infection Map
{ this.state.completedSteps.infection_done ? {this.state.completedSteps.infection_done ?
<Icon name="check" className="pull-right checkmark text-success"/> <Icon name="check" className="pull-right checkmark text-success"/>
: ''} : ''}
</NavLink> </NavLink>
@ -105,7 +125,7 @@ class AppComponent extends React.Component {
<NavLink to="/report"> <NavLink to="/report">
<span className="number">4.</span> <span className="number">4.</span>
Security Report Security Report
{ this.state.completedSteps.report_done ? {this.state.completedSteps.report_done ?
<Icon name="check" className="pull-right checkmark text-success"/> <Icon name="check" className="pull-right checkmark text-success"/>
: ''} : ''}
</NavLink> </NavLink>
@ -136,14 +156,15 @@ class AppComponent extends React.Component {
</div> </div>
</Col> </Col>
<Col sm={9} md={10} smOffset={3} mdOffset={2} className="main"> <Col sm={9} md={10} smOffset={3} mdOffset={2} className="main">
<Route exact path="/" render={(props) => ( <RunServerPage onStatusChange={this.updateStatus} /> )} /> <Route path='/login' render={(props) => (<LoginPageComponent onStatusChange={this.updateStatus}/>)}/>
<Route path="/configure" render={(props) => ( <ConfigurePage onStatusChange={this.updateStatus} /> )} /> {this.renderRoute('/', <RunServerPage onStatusChange={this.updateStatus}/>, true)}
<Route path="/run-monkey" render={(props) => ( <RunMonkeyPage onStatusChange={this.updateStatus} /> )} /> {this.renderRoute('/configure', <ConfigurePage onStatusChange={this.updateStatus}/>)}
<Route path="/infection/map" render={(props) => ( <MapPage onStatusChange={this.updateStatus} /> )} /> {this.renderRoute('/run-monkey', <RunMonkeyPage onStatusChange={this.updateStatus}/>)}
<Route path="/infection/telemetry" render={(props) => ( <TelemetryPage onStatusChange={this.updateStatus} /> )} /> {this.renderRoute('/infection/map', <MapPage onStatusChange={this.updateStatus}/>)}
<Route path="/start-over" render={(props) => ( <StartOverPage onStatusChange={this.updateStatus} /> )} /> {this.renderRoute('/infection/telemetry', <TelemetryPage onStatusChange={this.updateStatus}/>)}
<Route path="/report" render={(props) => ( <ReportPage onStatusChange={this.updateStatus} /> )} /> {this.renderRoute('/start-over', <StartOverPage onStatusChange={this.updateStatus}/>)}
<Route path="/license" render={(props) => ( <LicensePage onStatusChange={this.updateStatus} /> )} /> {this.renderRoute('/report', <ReportPage onStatusChange={this.updateStatus}/>)}
{this.renderRoute('/license', <LicensePage onStatusChange={this.updateStatus}/>)}
</Col> </Col>
</Row> </Row>
</Grid> </Grid>

View File

@ -2,8 +2,9 @@ import React from 'react';
import {Icon} from 'react-fa'; import {Icon} from 'react-fa';
import Toggle from 'react-toggle'; import Toggle from 'react-toggle';
import {OverlayTrigger, Tooltip} from 'react-bootstrap'; import {OverlayTrigger, Tooltip} from 'react-bootstrap';
import AuthComponent from '../../AuthComponent';
class PreviewPaneComponent extends React.Component { class PreviewPaneComponent extends AuthComponent {
generateToolTip(text) { generateToolTip(text) {
return ( return (
@ -64,7 +65,7 @@ class PreviewPaneComponent extends React.Component {
forceKill(event, asset) { forceKill(event, asset) {
let newConfig = asset.config; let newConfig = asset.config;
newConfig['alive'] = !event.target.checked; newConfig['alive'] = !event.target.checked;
fetch('/api/monkey/' + asset.guid, this.authFetch('/api/monkey/' + asset.guid,
{ {
method: 'PATCH', method: 'PATCH',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},

View File

@ -2,8 +2,9 @@ import React from 'react';
import Form from 'react-jsonschema-form'; import Form from 'react-jsonschema-form';
import {Col, Nav, NavItem} from 'react-bootstrap'; import {Col, Nav, NavItem} from 'react-bootstrap';
import fileDownload from 'js-file-download'; import fileDownload from 'js-file-download';
import AuthComponent from '../AuthComponent';
class ConfigurePageComponent extends React.Component { class ConfigurePageComponent extends AuthComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@ -23,7 +24,7 @@ class ConfigurePageComponent extends React.Component {
} }
componentDidMount() { componentDidMount() {
fetch('/api/configuration') this.authFetch('/api/configuration')
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
let sections = []; let sections = [];
@ -43,7 +44,7 @@ class ConfigurePageComponent extends React.Component {
onSubmit = ({formData}) => { onSubmit = ({formData}) => {
this.currentFormData = formData; this.currentFormData = formData;
this.updateConfigSection(); this.updateConfigSection();
fetch('/api/configuration', this.authFetch('/api/configuration',
{ {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
@ -82,7 +83,7 @@ class ConfigurePageComponent extends React.Component {
}; };
resetConfig = () => { resetConfig = () => {
fetch('/api/configuration', this.authFetch('/api/configuration',
{ {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
@ -126,7 +127,7 @@ class ConfigurePageComponent extends React.Component {
}; };
updateMonkeysRunning = () => { updateMonkeysRunning = () => {
fetch('/api') this.authFetch('/api')
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
// This check is used to prevent unnecessary re-rendering // This check is used to prevent unnecessary re-rendering

View File

@ -0,0 +1,78 @@
import React from 'react';
import {Col} from 'react-bootstrap';
import AuthService from '../../services/AuthService'
class LoginPageComponent extends React.Component {
login = () => {
this.auth.login(this.username, this.password).then(res => {
if (res['result']) {
this.redirectToHome();
} else {
this.setState({failed: true});
}
});
};
updateUsername = (evt) => {
this.username = evt.target.value;
};
updatePassword = (evt) => {
this.password = evt.target.value;
};
redirectToHome = () => {
window.location.href = '/';
};
constructor(props) {
super(props);
this.username = '';
this.password = '';
this.auth = new AuthService();
this.state = {
failed: false
};
if (this.auth.loggedIn()) {
this.redirectToHome();
}
}
render() {
return (
<Col xs={12} lg={8}>
<h1 className="page-title">Login</h1>
<div className="col-sm-6 col-sm-offset-3" style={{'fontSize': '1.2em'}}>
<div className="panel panel-default">
<div className="panel-heading text-center">
<b>Login</b>
</div>
<div className="panel-body">
<div className="input-group center-block text-center">
<input type="text" className="form-control" placeholder="Username"
onChange={evt => this.updateUsername(evt)}/>
<input type="password" className="form-control" placeholder="Password"
onChange={evt => this.updatePassword(evt)}/>
<button type="button" className="btn btn-primary btn-lg" style={{margin: '5px'}}
onClick={() => {
this.login()
}}>
Login
</button>
{
this.state.failed ?
<div className="alert alert-danger" role="alert">Login failed. Bad credentials.</div>
:
''
}
</div>
</div>
</div>
</div>
</Col>
);
}
}
export default LoginPageComponent;

View File

@ -6,8 +6,9 @@ import PreviewPane from 'components/map/preview-pane/PreviewPane';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import {ModalContainer, ModalDialog} from 'react-modal-dialog'; import {ModalContainer, ModalDialog} from 'react-modal-dialog';
import {options, edgeGroupToColor} from 'components/map/MapOptions'; import {options, edgeGroupToColor} from 'components/map/MapOptions';
import AuthComponent from '../AuthComponent';
class MapPageComponent extends React.Component { class MapPageComponent extends AuthComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -40,7 +41,7 @@ class MapPageComponent extends React.Component {
}; };
updateMapFromServer = () => { updateMapFromServer = () => {
fetch('/api/netmap') this.authFetch('/api/netmap')
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
res.edges.forEach(edge => { res.edges.forEach(edge => {
@ -52,7 +53,7 @@ class MapPageComponent extends React.Component {
}; };
updateTelemetryFromServer = () => { updateTelemetryFromServer = () => {
fetch('/api/telemetry-feed?timestamp='+this.state.telemetryLastTimestamp) this.authFetch('/api/telemetry-feed?timestamp='+this.state.telemetryLastTimestamp)
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
let newTelem = this.state.telemetry.concat(res['telemetries']); let newTelem = this.state.telemetry.concat(res['telemetries']);
@ -68,7 +69,7 @@ class MapPageComponent extends React.Component {
selectionChanged(event) { selectionChanged(event) {
if (event.nodes.length === 1) { if (event.nodes.length === 1) {
fetch('/api/netmap/node?id=' + event.nodes[0]) this.authFetch('/api/netmap/node?id=' + event.nodes[0])
.then(res => res.json()) .then(res => res.json())
.then(res => this.setState({selected: res, selectedType: 'node'})); .then(res => this.setState({selected: res, selectedType: 'node'}));
} }
@ -80,7 +81,7 @@ class MapPageComponent extends React.Component {
if (displayedEdge['group'] === 'island') { if (displayedEdge['group'] === 'island') {
this.setState({selected: displayedEdge, selectedType: 'island_edge'}); this.setState({selected: displayedEdge, selectedType: 'island_edge'});
} else { } else {
fetch('/api/netmap/edge?id=' + event.edges[0]) this.authFetch('/api/netmap/edge?id=' + event.edges[0])
.then(res => res.json()) .then(res => res.json())
.then(res => this.setState({selected: res.edge, selectedType: 'edge'})); .then(res => this.setState({selected: res.edge, selectedType: 'edge'}));
} }
@ -91,7 +92,7 @@ class MapPageComponent extends React.Component {
} }
killAllMonkeys = () => { killAllMonkeys = () => {
fetch('/api?action=killall') this.authFetch('/api?action=killall')
.then(res => res.json()) .then(res => res.json())
.then(res => this.setState({killPressed: (res.status === 'OK')})); .then(res => this.setState({killPressed: (res.status === 'OK')}));
}; };

View File

@ -7,11 +7,12 @@ import {edgeGroupToColor, options} from 'components/map/MapOptions';
import StolenPasswords from 'components/report-components/StolenPasswords'; import StolenPasswords from 'components/report-components/StolenPasswords';
import CollapsibleWellComponent from 'components/report-components/CollapsibleWell'; import CollapsibleWellComponent from 'components/report-components/CollapsibleWell';
import {Line} from 'rc-progress'; import {Line} from 'rc-progress';
import AuthComponent from '../AuthComponent';
let guardicoreLogoImage = require('../../images/guardicore-logo.png'); let guardicoreLogoImage = require('../../images/guardicore-logo.png');
let monkeyLogoImage = require('../../images/monkey-icon.svg'); let monkeyLogoImage = require('../../images/monkey-icon.svg');
class ReportPageComponent extends React.Component { class ReportPageComponent extends AuthComponent {
Issue = Issue =
{ {
@ -76,7 +77,7 @@ class ReportPageComponent extends React.Component {
} }
updateMonkeysRunning = () => { updateMonkeysRunning = () => {
return fetch('/api') return this.authFetch('/api')
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
// This check is used to prevent unnecessary re-rendering // This check is used to prevent unnecessary re-rendering
@ -89,7 +90,7 @@ class ReportPageComponent extends React.Component {
}; };
updateMapFromServer = () => { updateMapFromServer = () => {
fetch('/api/netmap') this.authFetch('/api/netmap')
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
res.edges.forEach(edge => { res.edges.forEach(edge => {
@ -102,7 +103,7 @@ class ReportPageComponent extends React.Component {
getReportFromServer(res) { getReportFromServer(res) {
if (res['completed_steps']['run_monkey']) { if (res['completed_steps']['run_monkey']) {
fetch('/api/report') this.authFetch('/api/report')
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
this.setState({ this.setState({

View File

@ -3,8 +3,9 @@ import {Button, Col, Well, Nav, NavItem, Collapse} from 'react-bootstrap';
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import {Icon} from 'react-fa'; import {Icon} from 'react-fa';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import AuthComponent from '../AuthComponent';
class RunMonkeyPageComponent extends React.Component { class RunMonkeyPageComponent extends AuthComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@ -19,14 +20,14 @@ class RunMonkeyPageComponent extends React.Component {
} }
componentDidMount() { componentDidMount() {
fetch('/api') this.authFetch('/api')
.then(res => res.json()) .then(res => res.json())
.then(res => this.setState({ .then(res => this.setState({
ips: res['ip_addresses'], ips: res['ip_addresses'],
selectedIp: res['ip_addresses'][0] selectedIp: res['ip_addresses'][0]
})); }));
fetch('/api/local-monkey') this.authFetch('/api/local-monkey')
.then(res => res.json()) .then(res => res.json())
.then(res =>{ .then(res =>{
if (res['is_running']) { if (res['is_running']) {
@ -36,7 +37,7 @@ class RunMonkeyPageComponent extends React.Component {
} }
}); });
fetch('/api/client-monkey') this.authFetch('/api/client-monkey')
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
if (res['is_running']) { if (res['is_running']) {
@ -60,7 +61,7 @@ class RunMonkeyPageComponent extends React.Component {
} }
runLocalMonkey = () => { runLocalMonkey = () => {
fetch('/api/local-monkey', this.authFetch('/api/local-monkey',
{ {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},

View File

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

View File

@ -2,8 +2,9 @@ import React from 'react';
import {Col} from 'react-bootstrap'; import {Col} from 'react-bootstrap';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import {ModalContainer, ModalDialog} from 'react-modal-dialog'; import {ModalContainer, ModalDialog} from 'react-modal-dialog';
import AuthComponent from '../AuthComponent';
class StartOverPageComponent extends React.Component { class StartOverPageComponent extends AuthComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@ -15,7 +16,7 @@ class StartOverPageComponent extends React.Component {
} }
updateMonkeysRunning = () => { updateMonkeysRunning = () => {
fetch('/api') this.authFetch('/api')
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
// This check is used to prevent unnecessary re-rendering // This check is used to prevent unnecessary re-rendering
@ -104,7 +105,7 @@ class StartOverPageComponent extends React.Component {
this.setState({ this.setState({
cleaned: false cleaned: false
}); });
fetch('/api?action=reset') this.authFetch('/api?action=reset')
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
if (res['status'] === 'OK') { if (res['status'] === 'OK') {

View File

@ -2,6 +2,7 @@ import React from 'react';
import {Col} from 'react-bootstrap'; import {Col} from 'react-bootstrap';
import JSONTree from 'react-json-tree' import JSONTree from 'react-json-tree'
import {DataTable} from 'react-data-components'; import {DataTable} from 'react-data-components';
import AuthComponent from '../AuthComponent';
const renderJson = (val) => <JSONTree data={val} level={1} theme="eighties" invertTheme={true} />; const renderJson = (val) => <JSONTree data={val} level={1} theme="eighties" invertTheme={true} />;
const renderTime = (val) => val.split('.')[0]; const renderTime = (val) => val.split('.')[0];
@ -13,7 +14,7 @@ const columns = [
{ title: 'Details', prop: 'data', render: renderJson, width: '40%' } { title: 'Details', prop: 'data', render: renderJson, width: '40%' }
]; ];
class TelemetryPageComponent extends React.Component { class TelemetryPageComponent extends AuthComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -22,7 +23,7 @@ class TelemetryPageComponent extends React.Component {
} }
componentDidMount = () => { componentDidMount = () => {
fetch('/api/telemetry') this.authFetch('/api/telemetry')
.then(res => res.json()) .then(res => res.json())
.then(res => this.setState({data: res.objects})); .then(res => this.setState({data: res.objects}));
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -0,0 +1,9 @@
import BaseConfig from './BaseConfig';
class AwsConfig extends BaseConfig{
isAuthEnabled() {
return true;
}
}
export default AwsConfig;

View File

@ -0,0 +1,8 @@
class BaseConfig {
isAuthEnabled() {
throw new Error('Abstract function');
}
}
export default BaseConfig;

View File

@ -0,0 +1,12 @@
import StandardConfig from './StandardConfig';
import AwsConfig from './AwsConfig';
const SERVER_CONFIG_JSON = require('json-loader!../../../server_config.json');
const CONFIG_DICT =
{
'standard': StandardConfig,
'aws': AwsConfig
};
export const SERVER_CONFIG = new CONFIG_DICT[SERVER_CONFIG_JSON['server_config']]();

View File

@ -0,0 +1,10 @@
import BaseConfig from './BaseConfig';
class StandardConfig extends BaseConfig {
isAuthEnabled () {
return false;
}
}
export default StandardConfig;

View File

@ -0,0 +1,106 @@
import decode from 'jwt-decode';
import {SERVER_CONFIG} from '../server_config/ServerConfig';
export default class AuthService {
AUTH_ENABLED = SERVER_CONFIG.isAuthEnabled();
login = (username, password) => {
if (this.AUTH_ENABLED) {
return this._login(username, password);
} else {
return {result: true};
}
};
authFetch = (url, options) => {
if (this.AUTH_ENABLED) {
return this._authFetch(url, options);
} else {
return fetch(url, options);
}
};
_login = (username, password) => {
return this._authFetch('/api/auth', {
method: 'POST',
body: JSON.stringify({
username,
password
})
}).then(response => response.json())
.then(res => {
if (res.hasOwnProperty('access_token')) {
this._setToken(res['access_token']);
return {result: true};
} else {
this._removeToken();
return {result: false};
}
})
};
_authFetch = (url, options = {}) => {
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
if (this.loggedIn()) {
headers['Authorization'] = 'JWT ' + this._getToken();
}
if (options.hasOwnProperty('headers')) {
for (let header in headers) {
options['headers'][header] = headers[header];
}
} else {
options['headers'] = headers;
}
return fetch(url, options)
.then(res => {
if (res.status === 401) {
this._removeToken();
}
return res;
});
};
loggedIn() {
if (!this.AUTH_ENABLED) {
return true;
}
const token = this._getToken();
return ((token !== null) && !this._isTokenExpired(token));
}
logout() {
if (this.AUTH_ENABLED) {
this._removeToken();
}
}
_isTokenExpired(token) {
try {
return decode(token)['exp'] < Date.now() / 1000;
}
catch (err) {
return false;
}
}
_setToken(idToken) {
localStorage.setItem('jwt', idToken);
}
_removeToken() {
localStorage.removeItem('jwt');
}
_getToken() {
return localStorage.getItem('jwt')
}
}

View File

@ -8,6 +8,7 @@ click
flask flask
Flask-Pymongo Flask-Pymongo
Flask-Restful Flask-Restful
Flask-JWT
jsonschema jsonschema
netifaces netifaces
ipaddress ipaddress

View File

@ -8,6 +8,7 @@ click
flask flask
Flask-Pymongo Flask-Pymongo
Flask-Restful Flask-Restful
Flask-JWT
jsonschema jsonschema
netifaces netifaces
ipaddress ipaddress