Refactored to have node state list only on backend and more CR fixes

This commit is contained in:
VakarisZ 2020-03-06 17:22:53 +02:00
parent 1e7775a2bc
commit 7475cff288
13 changed files with 95 additions and 75 deletions

View File

@ -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.monkey_download import MonkeyDownload
from monkey_island.cc.resources.netmap import NetMap from monkey_island.cc.resources.netmap import NetMap
from monkey_island.cc.resources.node import Node 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.remote_run import RemoteRun
from monkey_island.cc.resources.reporting.report import Report from monkey_island.cc.resources.reporting.report import Report
from monkey_island.cc.resources.root import Root 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(NetMap, '/api/netmap', '/api/netmap/')
api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/') api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/')
api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/')
api.add_resource(NodeStates, '/api/netmap/nodeStates')
# report_type: zero_trust or security # report_type: zero_trust or security
api.add_resource( api.add_resource(

View File

@ -2,13 +2,14 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
from urllib import parse from urllib import parse
import urllib3 import urllib3
import logging
import requests import requests
import pymongo import pymongo
# Disable "unverified certificate" warnings when sending requests to island # Disable "unverified certificate" warnings when sending requests to island
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
logger = logging.getLogger(__name__)
class BootloaderHttpServer(ThreadingMixIn, HTTPServer): class BootloaderHttpServer(ThreadingMixIn, HTTPServer):
@ -30,13 +31,17 @@ class BootloaderHTTPRequestHandler(BaseHTTPRequestHandler):
island_server_path = parse.urljoin(island_server_path, self.path[1:]) island_server_path = parse.urljoin(island_server_path, self.path[1:])
r = requests.post(url=island_server_path, data=post_data, verify=False) r = requests.post(url=island_server_path, data=post_data, verify=False)
if r.status_code != 200: try:
self.send_response(404) if r.status_code != 200:
else: self.send_response(404)
self.send_response(200) else:
self.end_headers() self.send_response(200)
self.wfile.write(r.content) self.end_headers()
self.connection.close() self.wfile.write(r.content)
except Exception as e:
logger.error("Failed to respond to bootloader: {}".format(e))
finally:
self.connection.close()
@staticmethod @staticmethod
def get_bootloader_resource_path_from_config(config): def get_bootloader_resource_path_from_config(config):

View File

@ -30,17 +30,13 @@ from monkey_island.cc.bootloader_server import BootloaderHttpServer
def main(): def main():
logger.info("Starting bootloader server") logger.info("Starting bootloader server")
mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url()) mongo_url = os.environ.get('MONGO_URL', env.get_mongo_url())
bootloader_server_thread = Thread(target=BootloaderHttpServer(mongo_url).serve_forever, daemon=True) bootloader_server_thread = Thread(target=BootloaderHttpServer(mongo_url).serve_forever, daemon=True)
# island_server_thread = Thread(target=start_island_server)
bootloader_server_thread.start() bootloader_server_thread.start()
#island_server_thread.start()
start_island_server() start_island_server()
bootloader_server_thread.join() bootloader_server_thread.join()
#island_server_thread.join()
def start_island_server(): def start_island_server():

View File

@ -12,21 +12,21 @@ class Bootloader(flask_restful.Resource):
# Used by monkey. can't secure. # Used by monkey. can't secure.
def post(self, os): def post(self, os):
if os == 'linux': if os == 'linux':
data = Bootloader.parse_bootloader_request_linux(request.data) data = Bootloader.get_request_contents_linux(request.data)
elif os == 'windows': elif os == 'windows':
data = Bootloader.parse_bootloader_request_windows(request.data) data = Bootloader.get_request_contents_windows(request.data)
else: else:
return make_response({"status": "OS_NOT_FOUND"}, 404) 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) return make_response({"status": "RUN"}, 200)
else: else:
return make_response({"status": "ABORT"}, 200) return make_response({"status": "ABORT"}, 200)
@staticmethod @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", "") parsed_data = json.loads(request_data.decode().replace("\n", "")
.replace("NAME=\"", "") .replace("NAME=\"", "")
.replace("\"\"", "\"") .replace("\"\"", "\"")
@ -34,5 +34,5 @@ class Bootloader(flask_restful.Resource):
return parsed_data return parsed_data
@staticmethod @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")) return json.loads(request_data.decode("utf-16", "ignore"))

View File

@ -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]}

View File

@ -3,8 +3,8 @@ from typing import Dict, List
from bson import ObjectId from bson import ObjectId
from monkey_island.cc.database import mongo from monkey_island.cc.database import mongo
from monkey_island.cc.services.node import NodeService, NodeNotFoundException from monkey_island.cc.services.node import NodeService, NodeCreationException
from monkey_island.cc.services.utils.node_groups import NodeGroups 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 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) will_monkey_run = BootloaderService.is_os_compatible(telem)
try: try:
node = NodeService.get_or_create_node_from_bootloader_telem(telem, will_monkey_run) 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 # Didn't find the node, but allow monkey to run anyways
return True return True
@ -32,13 +32,13 @@ class BootloaderService:
return will_monkey_run return will_monkey_run
@staticmethod @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'] group_keywords = [system, 'monkey']
if 'group' in node and node['group'] == 'island': if 'group' in node and node['group'] == 'island':
group_keywords.extend(['island', 'starting']) group_keywords.extend(['island', 'starting'])
else: else:
group_keywords.append('starting') if will_monkey_run else group_keywords.append('old') 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 return node_group
@staticmethod @staticmethod
@ -56,7 +56,7 @@ class BootloaderService:
@staticmethod @staticmethod
def is_windows_version_supported(windows_version) -> bool: def is_windows_version_supported(windows_version) -> bool:
return SUPPORTED_WINDOWS_VERSIONS.get(windows_version) return SUPPORTED_WINDOWS_VERSIONS.get(windows_version, True)
@staticmethod @staticmethod
def is_glibc_supported(glibc_version_string) -> bool: def is_glibc_supported(glibc_version_string) -> bool:

View File

@ -10,7 +10,7 @@ from monkey_island.cc.models import Monkey
from monkey_island.cc.services.edge import EdgeService from monkey_island.cc.services.edge import EdgeService
from monkey_island.cc.utils import local_ip_addresses, is_local_ips from monkey_island.cc.utils import local_ip_addresses, is_local_ips
from monkey_island.cc import models 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" __author__ = "itay.mizeretz"
@ -134,7 +134,7 @@ class NodeService:
keywords.append(NodeService.get_monkey_os(monkey)) keywords.append(NodeService.get_monkey_os(monkey))
if not Monkey.get_single_monkey_by_id(monkey["_id"]).is_dead(): if not Monkey.get_single_monkey_by_id(monkey["_id"]).is_dead():
keywords.append("running") keywords.append("running")
return NodeGroups.get_group_by_keywords(keywords).value return NodeStates.get_by_keywords(keywords).value
@staticmethod @staticmethod
def get_node_group(node) -> str: def get_node_group(node) -> str:
@ -142,7 +142,7 @@ class NodeService:
return node['group'] return node['group']
node_type = "exploited" if node.get("exploited") else "clean" node_type = "exploited" if node.get("exploited") else "clean"
node_os = NodeService.get_node_os(node) 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 @staticmethod
def monkey_to_net_node(monkey, for_report=False): def monkey_to_net_node(monkey, for_report=False):
@ -174,7 +174,7 @@ class NodeService:
} }
@staticmethod @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}, mongo.db.node.update({"_id": node_id},
{'$set': {'group': node_group.value}}, {'$set': {'group': node_group.value}},
upsert=False) upsert=False)
@ -240,8 +240,7 @@ class NodeService:
@staticmethod @staticmethod
def get_or_create_node_from_bootloader_telem(bootloader_telem: Dict, will_monkey_run: bool) -> Dict: def get_or_create_node_from_bootloader_telem(bootloader_telem: Dict, will_monkey_run: bool) -> Dict:
if is_local_ips(bootloader_telem['ips']): if is_local_ips(bootloader_telem['ips']):
raise NodeNotFoundException("Bootloader ran on island, no need to create new node.") raise NodeCreationException("Bootloader ran on island, no need to create new node.")
#return NodeService.get_monkey_island_pseudo_net_node()
new_node = mongo.db.node.find_one({"domain_name": bootloader_telem['hostname'], new_node = mongo.db.node.find_one({"domain_name": bootloader_telem['hostname'],
"ip_addresses": bootloader_telem['ips']}) "ip_addresses": bootloader_telem['ips']})
@ -255,7 +254,7 @@ class NodeService:
mongo.db.edge.update({"_id": edge["_id"]}, mongo.db.edge.update({"_id": edge["_id"]},
{'$set': {'tunnel': bool(bootloader_telem['tunnel']), {'$set': {'tunnel': bool(bootloader_telem['tunnel']),
'ip_address': bootloader_telem['ips'][0], 'ip_address': bootloader_telem['ips'][0],
'group': NodeGroups.get_group_by_keywords(['island']).value}}, 'group': NodeStates.get_by_keywords(['island']).value}},
upsert=False) upsert=False)
return new_node return new_node
@ -407,5 +406,5 @@ class NodeService:
def get_hostname_by_id(node_id): def get_hostname_by_id(node_id):
return NodeService.get_node_hostname(mongo.db.monkey.find_one({'_id': node_id}, {'hostname': 1})) return NodeService.get_node_hostname(mongo.db.monkey.find_one({'_id': node_id}, {'hostname': 1}))
class NodeNotFoundException(Exception): class NodeCreationException(Exception):
pass pass

View File

@ -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)

View File

@ -5,8 +5,7 @@ from typing import List
import collections import collections
# This list must correspond to the one on front end in src/components/map/MapOptions.js class NodeStates(Enum):
class NodeGroups(Enum):
CLEAN_UNKNOWN = 'clean_unknown' CLEAN_UNKNOWN = 'clean_unknown'
CLEAN_LINUX = 'clean_linux' CLEAN_LINUX = 'clean_linux'
CLEAN_WINDOWS = 'clean_windows' CLEAN_WINDOWS = 'clean_windows'
@ -33,8 +32,8 @@ class NodeGroups(Enum):
MONKEY_LINUX_OLD = 'monkey_linux_old' MONKEY_LINUX_OLD = 'monkey_linux_old'
@staticmethod @staticmethod
def get_group_by_keywords(keywords: List) -> NodeGroups: def get_by_keywords(keywords: List) -> NodeStates:
potential_groups = [i for i in NodeGroups if NodeGroups._is_group_from_keywords(i, keywords)] potential_groups = [i for i in NodeStates if NodeStates._is_state_from_keywords(i, keywords)]
if len(potential_groups) > 1: if len(potential_groups) > 1:
raise MultipleGroupsFoundException("Multiple groups contain provided keywords. " raise MultipleGroupsFoundException("Multiple groups contain provided keywords. "
"Manually build group string to ensure keyword order.") "Manually build group string to ensure keyword order.")
@ -44,7 +43,7 @@ class NodeGroups(Enum):
return potential_groups[0] return potential_groups[0]
@staticmethod @staticmethod
def _is_group_from_keywords(group, keywords) -> bool: def _is_state_from_keywords(group, keywords) -> bool:
group_keywords = group.value.split("_") group_keywords = group.value.split("_")
return collections.Counter(group_keywords) == collections.Counter(keywords) return collections.Counter(group_keywords) == collections.Counter(keywords)

View File

@ -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)

View File

@ -1,19 +1,11 @@
// This list must correspond to the one on back end in cc/services/utils/node_groups.py let getGroupsOptions = (stateList) => {
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 groupOptions = {}; let groupOptions = {};
for (let groupName of groupNames) { for (let stateName of stateList) {
groupOptions[groupName] = groupOptions[stateName] =
{ {
shape: 'image', shape: 'image',
size: 50, 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 */ let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */
opts.groups = getGroupsOptions(); opts.groups = getGroupsOptions(stateList);
return opts; return opts;
})(); }
export const optionsPth = (() => { export const optionsPth = (() => {
let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */ let opts = JSON.parse(JSON.stringify(basic_options)); /* Deep copy */

View File

@ -253,11 +253,14 @@ class PreviewPaneComponent extends AuthComponent {
info = this.scanInfo(this.props.item); info = this.scanInfo(this.props.item);
break; break;
case 'node': 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); 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 { } else {
info = this.props.item.group.includes('monkey', 'manual') ? this.infectedAssetInfo(this.props.item) : info = this.islandAssetInfo();
this.props.item.group !== 'island' ? this.assetInfo(this.props.item) : this.islandAssetInfo();
} }
break; break;
case 'island_edge': case 'island_edge':

View File

@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faStopCircle, faMinus } from '@fortawesome/free-solid-svg-icons' import { faStopCircle, faMinus } from '@fortawesome/free-solid-svg-icons'
import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane'; import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; 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'; import AuthComponent from '../AuthComponent';
class MapPageComponent extends AuthComponent { class MapPageComponent extends AuthComponent {
@ -13,6 +13,7 @@ class MapPageComponent extends AuthComponent {
super(props); super(props);
this.state = { this.state = {
graph: {nodes: [], edges: []}, graph: {nodes: [], edges: []},
nodeStateList:[],
selected: null, selected: null,
selectedType: null, selectedType: null,
killPressed: false, killPressed: false,
@ -27,6 +28,7 @@ class MapPageComponent extends AuthComponent {
}; };
componentDidMount() { componentDidMount() {
this.getNodeStateListFromServer();
this.updateMapFromServer(); this.updateMapFromServer();
this.interval = setInterval(this.timedEvents, 5000); this.interval = setInterval(this.timedEvents, 5000);
} }
@ -35,6 +37,14 @@ class MapPageComponent extends AuthComponent {
clearInterval(this.interval); clearInterval(this.interval);
} }
getNodeStateListFromServer = () => {
this.authFetch('/api/netmap/nodeStates')
.then(res => res.json())
.then(res => {
this.setState({nodeStateList: res.node_states});
});
};
timedEvents = () => { timedEvents = () => {
this.updateMapFromServer(); this.updateMapFromServer();
this.updateTelemetryFromServer(); this.updateTelemetryFromServer();
@ -168,7 +178,7 @@ class MapPageComponent extends AuthComponent {
</div> </div>
{this.renderTelemetryConsole()} {this.renderTelemetryConsole()}
<div style={{height: '80vh'}}> <div style={{height: '80vh'}}>
<ReactiveGraph graph={this.state.graph} options={options} events={this.events}/> <ReactiveGraph graph={this.state.graph} options={getOptions(this.state.nodeStateList)} events={this.events}/>
</div> </div>
</Col> </Col>
<Col xs={4}> <Col xs={4}>