diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index b1697f5a5..205785486 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -28,6 +28,7 @@ from monkey_island.cc.resources.root import Root from monkey_island.cc.resources.telemetry import Telemetry from monkey_island.cc.resources.telemetry_feed import TelemetryFeed from monkey_island.cc.resources.pba_file_download import PBAFileDownload +from monkey_island.cc.resources.version_update import VersionUpdate from monkey_island.cc.services.config import ConfigService from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService @@ -83,18 +84,14 @@ def output_json(obj, code, headers=None): return resp -def init_app(mongo_url): - app = Flask(__name__) - - api = flask_restful.Api(app) - api.representations = {'application/json': output_json} - +def init_app_config(app, mongo_url): app.config['MONGO_URI'] = mongo_url - app.config['SECRET_KEY'] = str(uuid.getnode()) app.config['JWT_AUTH_URL_RULE'] = '/api/auth' app.config['JWT_EXPIRATION_DELTA'] = env.get_auth_expiration_time() + +def init_app_services(app): init_jwt(app) mongo.init_app(app) @@ -105,9 +102,13 @@ def init_app(mongo_url): # If running on AWS, this will initialize the instance data, which is used "later" in the execution of the island. RemoteRunAwsService.init() + +def init_app_url_rules(app): app.add_url_rule('/', 'serve_home', serve_home) app.add_url_rule('/', 'serve_static_file', serve_static_file) + +def init_api_resources(api): api.add_resource(Root, '/api') api.add_resource(Monkey, '/api/monkey', '/api/monkey/', '/api/monkey/') api.add_resource(LocalRun, '/api/local-monkey', '/api/local-monkey/') @@ -130,5 +131,18 @@ def init_app(mongo_url): '/api/fileUpload/?restore=') api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/') api.add_resource(AttackTelem, '/api/attack/') + api.add_resource(VersionUpdate, '/api/version-update', '/api/version-update/') + + +def init_app(mongo_url): + app = Flask(__name__) + + api = flask_restful.Api(app) + api.representations = {'application/json': output_json} + + init_app_config(app, mongo_url) + init_app_services(app) + init_app_url_rules(app) + init_api_resources(api) return app diff --git a/monkey/monkey_island/cc/environment/__init__.py b/monkey/monkey_island/cc/environment/__init__.py index 62b0e9eed..ffa85693c 100644 --- a/monkey/monkey_island/cc/environment/__init__.py +++ b/monkey/monkey_island/cc/environment/__init__.py @@ -13,6 +13,7 @@ class Environment(object): _MONGO_URL = os.environ.get("MONKEY_MONGO_URL", "mongodb://localhost:27017/monkeyisland") _DEBUG_SERVER = False _AUTH_EXPIRATION_TIME = timedelta(hours=1) + _MONKEY_VERSION = "1.6.2" def __init__(self): self.config = None @@ -37,6 +38,21 @@ class Environment(object): h.update(secret) return h.hexdigest() + def get_deployment(self): + return self._get_from_config('deployment', 'unknown') + + def is_develop(self): + return self.get_deployment() == 'develop' + + def get_version(self): + return self._MONKEY_VERSION + ('-dev' if self.is_develop() else '') + + def _get_from_config(self, key, default_value=None): + val = default_value + if self.config is not None: + val = self.config.get(key, val) + return val + @abc.abstractmethod def get_auth_users(self): return diff --git a/monkey/monkey_island/cc/environment/environment.py b/monkey/monkey_island/cc/environment/environment.py index 3cd6bb587..b27880e07 100644 --- a/monkey/monkey_island/cc/environment/environment.py +++ b/monkey/monkey_island/cc/environment/environment.py @@ -32,6 +32,7 @@ def load_env_from_file(): config_json = load_server_configuration_from_file() return config_json['server_config'] + try: config_json = load_server_configuration_from_file() __env_type = config_json['server_config'] diff --git a/monkey/monkey_island/cc/resources/version_update.py b/monkey/monkey_island/cc/resources/version_update.py new file mode 100644 index 000000000..5b34f4206 --- /dev/null +++ b/monkey/monkey_island/cc/resources/version_update.py @@ -0,0 +1,24 @@ +import flask_restful +import logging + +from monkey_island.cc.environment.environment import env +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.services.version_update import VersionUpdateService + +__author__ = 'itay.mizeretz' + +logger = logging.getLogger(__name__) + + +class VersionUpdate(flask_restful.Resource): + def __init__(self): + super(VersionUpdate, self).__init__() + + # We don't secure this since it doesn't give out any private info and we want UI to know version + # even when not authenticated + def get(self): + return { + 'current_version': env.get_version(), + 'newer_version': VersionUpdateService.get_newer_version(), + 'download_link': VersionUpdateService.get_download_link() + } diff --git a/monkey/monkey_island/cc/server_config.json b/monkey/monkey_island/cc/server_config.json index 2d1a5995b..420f1b303 100644 --- a/monkey/monkey_island/cc/server_config.json +++ b/monkey/monkey_island/cc/server_config.json @@ -1,3 +1,4 @@ { - "server_config": "standard" -} \ No newline at end of file + "server_config": "standard", + "deployment": "develop" +} diff --git a/monkey/monkey_island/cc/services/version_update.py b/monkey/monkey_island/cc/services/version_update.py new file mode 100644 index 000000000..127e39f57 --- /dev/null +++ b/monkey/monkey_island/cc/services/version_update.py @@ -0,0 +1,57 @@ +import logging + +import requests + +from monkey_island.cc.environment.environment import env + +__author__ = "itay.mizeretz" + +logger = logging.getLogger(__name__) + + +class VersionUpdateService: + VERSION_SERVER_URL_PREF = 'https://monkey.guardicore.com' + VERSION_SERVER_CHECK_NEW_URL = VERSION_SERVER_URL_PREF + '?deployment=%s&monkey_version=%s' + VERSION_SERVER_DOWNLOAD_URL = VERSION_SERVER_CHECK_NEW_URL + '&is_download=true' + + newer_version = None + + def __init__(self): + pass + + @staticmethod + def get_newer_version(): + """ + Checks for newer version if never checked before. + :return: None if failed checking for newer version, result of '_check_new_version' otherwise + """ + if VersionUpdateService.newer_version is None: + try: + VersionUpdateService.newer_version = VersionUpdateService._check_new_version() + except Exception: + logger.exception('Failed updating version number') + + return VersionUpdateService.newer_version + + @staticmethod + def _check_new_version(): + """ + Checks if newer monkey version is available + :return: False if not, version in string format ('1.6.2') otherwise + """ + url = VersionUpdateService.VERSION_SERVER_CHECK_NEW_URL % (env.get_deployment(), env.get_version()) + + reply = requests.get(url, timeout=15) + + res = reply.json().get('newer_version', None) + + if res is False: + return res + + [int(x) for x in res.split('.')] # raises value error if version is invalid format + return res + + @staticmethod + def get_download_link(): + return VersionUpdateService.VERSION_SERVER_DOWNLOAD_URL % (env.get_deployment(), env.get_version()) + diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js index da8e59113..8229133e6 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.js +++ b/monkey/monkey_island/cc/ui/src/components/Main.js @@ -20,6 +20,7 @@ import 'react-data-components/css/table-twbs.css'; import 'styles/App.css'; import 'react-toggle/style.css'; import 'react-table/react-table.css'; +import VersionComponent from "./side-menu/VersionComponent"; let logoImage = require('../images/monkey-icon.svg'); let infectionMonkeyImage = require('../images/infection-monkey.svg'); @@ -85,7 +86,7 @@ class AppComponent extends AuthComponent { infection_done: false, report_done: false, isLoggedIn: undefined - } + }, }; } @@ -175,6 +176,7 @@ class AppComponent extends AuthComponent {
License
+ ()}/> diff --git a/monkey/monkey_island/cc/ui/src/components/side-menu/VersionComponent.js b/monkey/monkey_island/cc/ui/src/components/side-menu/VersionComponent.js new file mode 100644 index 000000000..1246b5b94 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/side-menu/VersionComponent.js @@ -0,0 +1,46 @@ +import React from 'react'; +import {Icon} from 'react-fa'; + +class VersionComponent extends React.Component { + constructor(props) { + super(props); + this.state = { + currentVersion: undefined, + newerVersion: undefined, + downloadLink: undefined + } + } + + componentDidMount() { + fetch('/api/version-update') // This is not authenticated on purpose + .then(res => res.json()) + .then(res => { + this.setState({ + currentVersion: res['current_version'], + newerVersion: res['newer_version'], + downloadLink: res['download_link'], + }); + }); + } + + render() { + return ( +
+ Infection Monkey Version: {this.state.currentVersion} + { + this.state.newerVersion ? +
+ Newer version available! +
+ Download here +
+ : + undefined + } +
+ ); + } +} + + +export default VersionComponent; diff --git a/monkey/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css index 6155a4dcc..b44fa4562 100644 --- a/monkey/monkey_island/cc/ui/src/styles/App.css +++ b/monkey/monkey_island/cc/ui/src/styles/App.css @@ -515,3 +515,13 @@ body { } } + +.version-text { + font-size: 0.9em; + position: absolute; + bottom: 5px; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; +} diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index 5e0faea19..ff09c5761 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -1,3 +1,4 @@ +bson python-dateutil tornado==5.1.1 werkzeug @@ -18,7 +19,7 @@ boto3 botocore PyInstaller awscli -bson cffi virtualenv wheel +requests