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.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(

View File

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

View File

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

View File

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

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 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:

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.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

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

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
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 */

View File

@ -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':

View File

@ -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 {
</div>
{this.renderTelemetryConsole()}
<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>
</Col>
<Col xs={4}>