From 7475cff2887ac0d866976ccb7027a55e3a27c1c6 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 6 Mar 2020 17:22:53 +0200 Subject: [PATCH] Refactored to have node state list only on backend and more CR fixes --- monkey/monkey_island/cc/app.py | 2 ++ monkey/monkey_island/cc/bootloader_server.py | 21 +++++++++++------- monkey/monkey_island/cc/main.py | 4 ---- .../monkey_island/cc/resources/bootloader.py | 12 +++++----- .../monkey_island/cc/resources/node_states.py | 11 ++++++++++ .../monkey_island/cc/services/bootloader.py | 12 +++++----- monkey/monkey_island/cc/services/node.py | 15 ++++++------- .../cc/services/utils/node_groups_test.py | 18 --------------- .../utils/{node_groups.py => node_states.py} | 9 ++++---- .../cc/services/utils/node_states_test.py | 21 ++++++++++++++++++ .../cc/ui/src/components/map/MapOptions.js | 22 ++++++------------- .../map/preview-pane/PreviewPane.js | 9 +++++--- .../cc/ui/src/components/pages/MapPage.js | 14 ++++++++++-- 13 files changed, 95 insertions(+), 75 deletions(-) create mode 100644 monkey/monkey_island/cc/resources/node_states.py delete mode 100644 monkey/monkey_island/cc/services/utils/node_groups_test.py rename monkey/monkey_island/cc/services/utils/{node_groups.py => node_states.py} (85%) create mode 100644 monkey/monkey_island/cc/services/utils/node_states_test.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 80d743209..0152e5825 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -19,6 +19,7 @@ from monkey_island.cc.resources.island_configuration import IslandConfiguration from monkey_island.cc.resources.monkey_download import MonkeyDownload from monkey_island.cc.resources.netmap import NetMap from monkey_island.cc.resources.node import Node +from monkey_island.cc.resources.node_states import NodeStates from monkey_island.cc.resources.remote_run import RemoteRun from monkey_island.cc.resources.reporting.report import Report from monkey_island.cc.resources.root import Root @@ -98,6 +99,7 @@ def init_api_resources(api): api.add_resource(NetMap, '/api/netmap', '/api/netmap/') api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/') api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') + api.add_resource(NodeStates, '/api/netmap/nodeStates') # report_type: zero_trust or security api.add_resource( diff --git a/monkey/monkey_island/cc/bootloader_server.py b/monkey/monkey_island/cc/bootloader_server.py index 57ec26cef..a8fdda498 100644 --- a/monkey/monkey_island/cc/bootloader_server.py +++ b/monkey/monkey_island/cc/bootloader_server.py @@ -2,13 +2,14 @@ from http.server import HTTPServer, BaseHTTPRequestHandler from socketserver import ThreadingMixIn from urllib import parse import urllib3 +import logging import requests import pymongo # Disable "unverified certificate" warnings when sending requests to island urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - +logger = logging.getLogger(__name__) class BootloaderHttpServer(ThreadingMixIn, HTTPServer): @@ -30,13 +31,17 @@ class BootloaderHTTPRequestHandler(BaseHTTPRequestHandler): island_server_path = parse.urljoin(island_server_path, self.path[1:]) r = requests.post(url=island_server_path, data=post_data, verify=False) - if r.status_code != 200: - self.send_response(404) - else: - self.send_response(200) - self.end_headers() - self.wfile.write(r.content) - self.connection.close() + try: + if r.status_code != 200: + self.send_response(404) + else: + self.send_response(200) + self.end_headers() + self.wfile.write(r.content) + except Exception as e: + logger.error("Failed to respond to bootloader: {}".format(e)) + finally: + self.connection.close() @staticmethod def get_bootloader_resource_path_from_config(config): diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py index 31e227ff1..508479287 100644 --- a/monkey/monkey_island/cc/main.py +++ b/monkey/monkey_island/cc/main.py @@ -30,17 +30,13 @@ from monkey_island.cc.bootloader_server import BootloaderHttpServer def main(): - logger.info("Starting bootloader server") mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url()) bootloader_server_thread = Thread(target=BootloaderHttpServer(mongo_url).serve_forever, daemon=True) - # island_server_thread = Thread(target=start_island_server) bootloader_server_thread.start() - #island_server_thread.start() start_island_server() bootloader_server_thread.join() - #island_server_thread.join() def start_island_server(): diff --git a/monkey/monkey_island/cc/resources/bootloader.py b/monkey/monkey_island/cc/resources/bootloader.py index 9ef11cc25..ce70c20f6 100644 --- a/monkey/monkey_island/cc/resources/bootloader.py +++ b/monkey/monkey_island/cc/resources/bootloader.py @@ -12,21 +12,21 @@ class Bootloader(flask_restful.Resource): # Used by monkey. can't secure. def post(self, os): if os == 'linux': - data = Bootloader.parse_bootloader_request_linux(request.data) + data = Bootloader.get_request_contents_linux(request.data) elif os == 'windows': - data = Bootloader.parse_bootloader_request_windows(request.data) + data = Bootloader.get_request_contents_windows(request.data) else: return make_response({"status": "OS_NOT_FOUND"}, 404) - resp = BootloaderService.parse_bootloader_telem(data) + result = BootloaderService.parse_bootloader_telem(data) - if resp: + if result: return make_response({"status": "RUN"}, 200) else: return make_response({"status": "ABORT"}, 200) @staticmethod - def parse_bootloader_request_linux(request_data: bytes) -> Dict[str, str]: + def get_request_contents_linux(request_data: bytes) -> Dict[str, str]: parsed_data = json.loads(request_data.decode().replace("\n", "") .replace("NAME=\"", "") .replace("\"\"", "\"") @@ -34,5 +34,5 @@ class Bootloader(flask_restful.Resource): return parsed_data @staticmethod - def parse_bootloader_request_windows(request_data: bytes) -> Dict[str, str]: + def get_request_contents_windows(request_data: bytes) -> Dict[str, str]: return json.loads(request_data.decode("utf-16", "ignore")) diff --git a/monkey/monkey_island/cc/resources/node_states.py b/monkey/monkey_island/cc/resources/node_states.py new file mode 100644 index 000000000..c7c99cc94 --- /dev/null +++ b/monkey/monkey_island/cc/resources/node_states.py @@ -0,0 +1,11 @@ +from flask import request +import flask_restful + +from monkey_island.cc.auth import jwt_required +from monkey_island.cc.services.utils.node_states import NodeStates as NodeStateList + + +class NodeStates(flask_restful.Resource): + @jwt_required() + def get(self): + return {'node_states': [state.value for state in NodeStateList]} diff --git a/monkey/monkey_island/cc/services/bootloader.py b/monkey/monkey_island/cc/services/bootloader.py index f84bcf745..49dbe3154 100644 --- a/monkey/monkey_island/cc/services/bootloader.py +++ b/monkey/monkey_island/cc/services/bootloader.py @@ -3,8 +3,8 @@ from typing import Dict, List from bson import ObjectId from monkey_island.cc.database import mongo -from monkey_island.cc.services.node import NodeService, NodeNotFoundException -from monkey_island.cc.services.utils.node_groups import NodeGroups +from monkey_island.cc.services.node import NodeService, NodeCreationException +from monkey_island.cc.services.utils.node_states import NodeStates from monkey_island.cc.services.utils.bootloader_config import SUPPORTED_WINDOWS_VERSIONS, MIN_GLIBC_VERSION @@ -22,7 +22,7 @@ class BootloaderService: will_monkey_run = BootloaderService.is_os_compatible(telem) try: node = NodeService.get_or_create_node_from_bootloader_telem(telem, will_monkey_run) - except NodeNotFoundException: + except NodeCreationException: # Didn't find the node, but allow monkey to run anyways return True @@ -32,13 +32,13 @@ class BootloaderService: return will_monkey_run @staticmethod - def get_next_node_state(node: Dict, system: str, will_monkey_run: bool) -> NodeGroups: + def get_next_node_state(node: Dict, system: str, will_monkey_run: bool) -> NodeStates: group_keywords = [system, 'monkey'] if 'group' in node and node['group'] == 'island': group_keywords.extend(['island', 'starting']) else: group_keywords.append('starting') if will_monkey_run else group_keywords.append('old') - node_group = NodeGroups.get_group_by_keywords(group_keywords) + node_group = NodeStates.get_by_keywords(group_keywords) return node_group @staticmethod @@ -56,7 +56,7 @@ class BootloaderService: @staticmethod def is_windows_version_supported(windows_version) -> bool: - return SUPPORTED_WINDOWS_VERSIONS.get(windows_version) + return SUPPORTED_WINDOWS_VERSIONS.get(windows_version, True) @staticmethod def is_glibc_supported(glibc_version_string) -> bool: diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 1f0723925..b45a12609 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -10,7 +10,7 @@ from monkey_island.cc.models import Monkey from monkey_island.cc.services.edge import EdgeService from monkey_island.cc.utils import local_ip_addresses, is_local_ips from monkey_island.cc import models -from monkey_island.cc.services.utils.node_groups import NodeGroups +from monkey_island.cc.services.utils.node_states import NodeStates __author__ = "itay.mizeretz" @@ -134,7 +134,7 @@ class NodeService: keywords.append(NodeService.get_monkey_os(monkey)) if not Monkey.get_single_monkey_by_id(monkey["_id"]).is_dead(): keywords.append("running") - return NodeGroups.get_group_by_keywords(keywords).value + return NodeStates.get_by_keywords(keywords).value @staticmethod def get_node_group(node) -> str: @@ -142,7 +142,7 @@ class NodeService: return node['group'] node_type = "exploited" if node.get("exploited") else "clean" node_os = NodeService.get_node_os(node) - return NodeGroups.get_group_by_keywords([node_type, node_os]).value + return NodeStates.get_by_keywords([node_type, node_os]).value @staticmethod def monkey_to_net_node(monkey, for_report=False): @@ -174,7 +174,7 @@ class NodeService: } @staticmethod - def set_node_group(node_id: str, node_group: NodeGroups): + def set_node_group(node_id: str, node_group: NodeStates): mongo.db.node.update({"_id": node_id}, {'$set': {'group': node_group.value}}, upsert=False) @@ -240,8 +240,7 @@ class NodeService: @staticmethod def get_or_create_node_from_bootloader_telem(bootloader_telem: Dict, will_monkey_run: bool) -> Dict: if is_local_ips(bootloader_telem['ips']): - raise NodeNotFoundException("Bootloader ran on island, no need to create new node.") - #return NodeService.get_monkey_island_pseudo_net_node() + raise NodeCreationException("Bootloader ran on island, no need to create new node.") new_node = mongo.db.node.find_one({"domain_name": bootloader_telem['hostname'], "ip_addresses": bootloader_telem['ips']}) @@ -255,7 +254,7 @@ class NodeService: mongo.db.edge.update({"_id": edge["_id"]}, {'$set': {'tunnel': bool(bootloader_telem['tunnel']), 'ip_address': bootloader_telem['ips'][0], - 'group': NodeGroups.get_group_by_keywords(['island']).value}}, + 'group': NodeStates.get_by_keywords(['island']).value}}, upsert=False) return new_node @@ -407,5 +406,5 @@ class NodeService: def get_hostname_by_id(node_id): return NodeService.get_node_hostname(mongo.db.monkey.find_one({'_id': node_id}, {'hostname': 1})) -class NodeNotFoundException(Exception): +class NodeCreationException(Exception): pass diff --git a/monkey/monkey_island/cc/services/utils/node_groups_test.py b/monkey/monkey_island/cc/services/utils/node_groups_test.py deleted file mode 100644 index ef7a5b555..000000000 --- a/monkey/monkey_island/cc/services/utils/node_groups_test.py +++ /dev/null @@ -1,18 +0,0 @@ -from unittest import TestCase - -from monkey_island.cc.services.utils.node_groups import NodeGroups, NoGroupsFoundException - - -class TestNodeGroups(TestCase): - - def test_get_group_by_keywords(self): - tst1 = NodeGroups.get_group_by_keywords(['island']) == NodeGroups.ISLAND - tst2 = NodeGroups.get_group_by_keywords(['running', 'linux', 'monkey']) == NodeGroups.MONKEY_LINUX_RUNNING - tst3 = NodeGroups.get_group_by_keywords(['monkey', 'linux', 'running']) == NodeGroups.MONKEY_LINUX_RUNNING - tst4 = False - try: - NodeGroups.get_group_by_keywords(['bogus', 'values', 'from', 'long', 'list', 'should', 'fail']) - except NoGroupsFoundException: - tst4 = True - self.assertTrue(tst1 and tst2 and tst3 and tst4) - diff --git a/monkey/monkey_island/cc/services/utils/node_groups.py b/monkey/monkey_island/cc/services/utils/node_states.py similarity index 85% rename from monkey/monkey_island/cc/services/utils/node_groups.py rename to monkey/monkey_island/cc/services/utils/node_states.py index b5c25cd9e..db8dd6429 100644 --- a/monkey/monkey_island/cc/services/utils/node_groups.py +++ b/monkey/monkey_island/cc/services/utils/node_states.py @@ -5,8 +5,7 @@ from typing import List import collections -# This list must correspond to the one on front end in src/components/map/MapOptions.js -class NodeGroups(Enum): +class NodeStates(Enum): CLEAN_UNKNOWN = 'clean_unknown' CLEAN_LINUX = 'clean_linux' CLEAN_WINDOWS = 'clean_windows' @@ -33,8 +32,8 @@ class NodeGroups(Enum): MONKEY_LINUX_OLD = 'monkey_linux_old' @staticmethod - def get_group_by_keywords(keywords: List) -> NodeGroups: - potential_groups = [i for i in NodeGroups if NodeGroups._is_group_from_keywords(i, keywords)] + def get_by_keywords(keywords: List) -> NodeStates: + potential_groups = [i for i in NodeStates if NodeStates._is_state_from_keywords(i, keywords)] if len(potential_groups) > 1: raise MultipleGroupsFoundException("Multiple groups contain provided keywords. " "Manually build group string to ensure keyword order.") @@ -44,7 +43,7 @@ class NodeGroups(Enum): return potential_groups[0] @staticmethod - def _is_group_from_keywords(group, keywords) -> bool: + def _is_state_from_keywords(group, keywords) -> bool: group_keywords = group.value.split("_") return collections.Counter(group_keywords) == collections.Counter(keywords) diff --git a/monkey/monkey_island/cc/services/utils/node_states_test.py b/monkey/monkey_island/cc/services/utils/node_states_test.py new file mode 100644 index 000000000..db404cc90 --- /dev/null +++ b/monkey/monkey_island/cc/services/utils/node_states_test.py @@ -0,0 +1,21 @@ +from unittest import TestCase + +from monkey_island.cc.services.utils.node_states import NodeStates, NoGroupsFoundException + + +class TestNodeGroups(TestCase): + + def test_get_group_by_keywords(self): + tst1 = NodeStates.get_by_keywords(['island']) == NodeStates.ISLAND + tst2 = NodeStates.get_by_keywords(['running', 'linux', 'monkey']) == NodeStates.MONKEY_LINUX_RUNNING + tst3 = NodeStates.get_by_keywords(['monkey', 'linux', 'running']) == NodeStates.MONKEY_LINUX_RUNNING + tst4 = False + try: + NodeStates.get_by_keywords(['bogus', 'values', 'from', 'long', 'list', 'should', 'fail']) + except NoGroupsFoundException: + tst4 = True + self.assertTrue(tst1) + self.assertTrue(tst2) + self.assertTrue(tst3) + self.assertTrue(tst4) + diff --git a/monkey/monkey_island/cc/ui/src/components/map/MapOptions.js b/monkey/monkey_island/cc/ui/src/components/map/MapOptions.js index c1474f5ec..742d061f4 100644 --- a/monkey/monkey_island/cc/ui/src/components/map/MapOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/map/MapOptions.js @@ -1,19 +1,11 @@ -// This list must correspond to the one on back end in cc/services/utils/node_groups.py -const groupNames = ['clean_unknown', 'clean_linux', 'clean_windows', 'exploited_linux', 'exploited_windows', 'island', - 'island_monkey_linux', 'island_monkey_linux_running', 'island_monkey_linux_starting', 'island_monkey_windows', - 'island_monkey_windows_running', 'island_monkey_windows_starting', 'manual_linux', 'manual_linux_running', - 'manual_windows', 'manual_windows_running', 'monkey_linux', 'monkey_linux_running', 'monkey_windows', - 'monkey_windows_running', 'monkey_windows_starting', 'monkey_linux_starting', 'monkey_windows_old', - 'monkey_linux_old' ]; - -let getGroupsOptions = () => { +let getGroupsOptions = (stateList) => { let groupOptions = {}; - for (let groupName of groupNames) { - groupOptions[groupName] = + for (let stateName of stateList) { + groupOptions[stateName] = { shape: 'image', size: 50, - image: require('../../images/nodes/' + groupName + '.png') + image: require('../../images/nodes/' + stateName + '.png') }; } @@ -55,11 +47,11 @@ export const basic_options = { } }; -export const options = (() => { +export function getOptions(stateList) { let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */ - opts.groups = getGroupsOptions(); + opts.groups = getGroupsOptions(stateList); return opts; -})(); +} export const optionsPth = (() => { let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */ diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js index a4386d851..dd99c8503 100644 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js +++ b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js @@ -253,11 +253,14 @@ class PreviewPaneComponent extends AuthComponent { info = this.scanInfo(this.props.item); break; case 'node': - if(this.props.item.group.includes('monkey') && this.props.item.group.includes('starting')){ + if (this.props.item.group.includes('monkey') && this.props.item.group.includes('starting')) { info = this.assetInfo(this.props.item); + } else if (this.props.item.group.includes('monkey', 'manual')) { + info = this.infectedAssetInfo(this.props.item) + } else if (this.props.item.group !== 'island') { + info = this.assetInfo(this.props.item) } else { - info = this.props.item.group.includes('monkey', 'manual') ? this.infectedAssetInfo(this.props.item) : - this.props.item.group !== 'island' ? this.assetInfo(this.props.item) : this.islandAssetInfo(); + info = this.islandAssetInfo(); } break; case 'island_edge': diff --git a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js index 9394eed54..a04b8b9ae 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faStopCircle, faMinus } from '@fortawesome/free-solid-svg-icons' import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; -import {options, edgeGroupToColor} from 'components/map/MapOptions'; +import {getOptions, edgeGroupToColor} from 'components/map/MapOptions'; import AuthComponent from '../AuthComponent'; class MapPageComponent extends AuthComponent { @@ -13,6 +13,7 @@ class MapPageComponent extends AuthComponent { super(props); this.state = { graph: {nodes: [], edges: []}, + nodeStateList:[], selected: null, selectedType: null, killPressed: false, @@ -27,6 +28,7 @@ class MapPageComponent extends AuthComponent { }; componentDidMount() { + this.getNodeStateListFromServer(); this.updateMapFromServer(); this.interval = setInterval(this.timedEvents, 5000); } @@ -35,6 +37,14 @@ class MapPageComponent extends AuthComponent { clearInterval(this.interval); } + getNodeStateListFromServer = () => { + this.authFetch('/api/netmap/nodeStates') + .then(res => res.json()) + .then(res => { + this.setState({nodeStateList: res.node_states}); + }); + }; + timedEvents = () => { this.updateMapFromServer(); this.updateTelemetryFromServer(); @@ -168,7 +178,7 @@ class MapPageComponent extends AuthComponent { {this.renderTelemetryConsole()}
- +