Attack configuration page moved into island configuration

This commit is contained in:
VakarisZ 2019-05-02 17:11:30 +03:00
parent c055820a0d
commit cdd3270730
13 changed files with 469 additions and 431 deletions

View File

@ -28,7 +28,7 @@ from monkey_island.cc.resources.root import Root
from monkey_island.cc.resources.telemetry import Telemetry from monkey_island.cc.resources.telemetry import Telemetry
from monkey_island.cc.resources.telemetry_feed import TelemetryFeed from monkey_island.cc.resources.telemetry_feed import TelemetryFeed
from monkey_island.cc.resources.pba_file_download import PBAFileDownload from monkey_island.cc.resources.pba_file_download import PBAFileDownload
from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.database import Database
from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH
from monkey_island.cc.resources.pba_file_upload import FileUpload from monkey_island.cc.resources.pba_file_upload import FileUpload
from monkey_island.cc.resources.attack_telem import AttackTelem from monkey_island.cc.resources.attack_telem import AttackTelem
@ -100,7 +100,7 @@ def init_app(mongo_url):
with app.app_context(): with app.app_context():
database.init() database.init()
ConfigService.init_config() Database.reset_db()
app.add_url_rule('/', 'serve_home', serve_home) app.add_url_rule('/', 'serve_home', serve_home)
app.add_url_rule('/<path:static_path>', 'serve_static_file', serve_static_file) app.add_url_rule('/<path:static_path>', 'serve_static_file', serve_static_file)

View File

@ -3,7 +3,7 @@ import json
from flask import jsonify, request from flask import jsonify, request
from monkey_island.cc.auth import jwt_required from monkey_island.cc.auth import jwt_required
import monkey_island.cc.services.attack.attack_config as attack_config from monkey_island.cc.services.attack.attack_config import AttackConfig
__author__ = "VakarisZ" __author__ = "VakarisZ"
@ -11,7 +11,7 @@ __author__ = "VakarisZ"
class AttackConfiguration(flask_restful.Resource): class AttackConfiguration(flask_restful.Resource):
@jwt_required() @jwt_required()
def get(self): def get(self):
return jsonify(configuration=attack_config.get_config()['properties']) return jsonify(configuration=AttackConfig.get_config()['properties'])
@jwt_required() @jwt_required()
def post(self): def post(self):
@ -21,10 +21,10 @@ class AttackConfiguration(flask_restful.Resource):
""" """
config_json = json.loads(request.data) config_json = json.loads(request.data)
if 'reset_attack_matrix' in config_json: if 'reset_attack_matrix' in config_json:
attack_config.reset_config() AttackConfig.reset_config()
return jsonify(configuration=attack_config.get_config()['properties']) return jsonify(configuration=AttackConfig.get_config()['properties'])
else: else:
attack_config.update_config({'properties': json.loads(request.data)}) AttackConfig.update_config({'properties': json.loads(request.data)})
attack_config.apply_to_monkey_config() AttackConfig.apply_to_monkey_config()
return {} return {}

View File

@ -1,7 +1,7 @@
import flask_restful import flask_restful
from flask import request from flask import request
import json import json
from monkey_island.cc.services.attack.attack_telem import set_results from monkey_island.cc.services.attack.attack_telem import AttackTelemService
import logging import logging
__author__ = 'VakarisZ' __author__ = 'VakarisZ'
@ -20,5 +20,5 @@ class AttackTelem(flask_restful.Resource):
:param technique: Technique ID, e.g. T1111 :param technique: Technique ID, e.g. T1111
""" """
data = json.loads(request.data) data = json.loads(request.data)
set_results(technique, data) AttackTelemService.set_results(technique, data)
return {} return {}

View File

@ -6,12 +6,10 @@ from flask import request, make_response, jsonify
from monkey_island.cc.auth import jwt_required from monkey_island.cc.auth import jwt_required
from monkey_island.cc.database import mongo from monkey_island.cc.database import mongo
from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.services.attack.attack_config import reset_config as reset_attack_config
from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.report import ReportService from monkey_island.cc.services.report import ReportService
from monkey_island.cc.utils import local_ip_addresses from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.services.post_breach_files import remove_PBA_files from monkey_island.cc.services.database import Database
__author__ = 'Barak' __author__ = 'Barak'
@ -27,7 +25,7 @@ class Root(flask_restful.Resource):
if not action: if not action:
return Root.get_server_info() return Root.get_server_info()
elif action == "reset": elif action == "reset":
return Root.reset_db() return jwt_required()(Database.reset_db())
elif action == "killall": elif action == "killall":
return Root.kill_all() return Root.kill_all()
elif action == "is-up": elif action == "is-up":
@ -41,17 +39,6 @@ class Root(flask_restful.Resource):
return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db), return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db),
completed_steps=Root.get_completed_steps()) completed_steps=Root.get_completed_steps())
@staticmethod
@jwt_required()
def reset_db():
remove_PBA_files()
# We can't drop system collections.
[mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')]
ConfigService.init_config()
reset_attack_config()
logger.info('DB was reset')
return jsonify(status='OK')
@staticmethod @staticmethod
@jwt_required() @jwt_required()
def kill_all(): def kill_all():

View File

@ -9,37 +9,42 @@ __author__ = "VakarisZ"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AttackConfig(object):
def __init__(self):
pass
@staticmethod
def get_config(): def get_config():
config = mongo.db.attack.find_one({'name': 'newconfig'}) or reset_config() config = mongo.db.attack.find_one({'name': 'newconfig'})
return config return config
@staticmethod
def get_config_schema(): def get_config_schema():
return SCHEMA return SCHEMA
@staticmethod
def reset_config(): def reset_config():
update_config(SCHEMA) AttackConfig.update_config(SCHEMA)
@staticmethod
def update_config(config_json): def update_config(config_json):
mongo.db.attack.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True) mongo.db.attack.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True)
return True return True
@staticmethod
def apply_to_monkey_config(): def apply_to_monkey_config():
""" """
Applies ATT&CK matrix to the monkey configuration Applies ATT&CK matrix to the monkey configuration
:return: :return:
""" """
attack_techniques = get_technique_values() attack_techniques = AttackConfig.get_technique_values()
monkey_config = ConfigService.get_config(False, True, True) monkey_config = ConfigService.get_config(False, True, True)
monkey_schema = ConfigService.get_config_schema() monkey_schema = ConfigService.get_config_schema()
set_arrays(attack_techniques, monkey_config, monkey_schema) AttackConfig.set_arrays(attack_techniques, monkey_config, monkey_schema)
set_booleans(attack_techniques, monkey_config, monkey_schema) AttackConfig.set_booleans(attack_techniques, monkey_config, monkey_schema)
ConfigService.update_config(monkey_config, True) ConfigService.update_config(monkey_config, True)
@staticmethod
def set_arrays(attack_techniques, monkey_config, monkey_schema): def set_arrays(attack_techniques, monkey_config, monkey_schema):
""" """
Sets exploiters/scanners/PBAs and other array type fields in monkey's config according to ATT&CK matrix Sets exploiters/scanners/PBAs and other array type fields in monkey's config according to ATT&CK matrix
@ -53,14 +58,14 @@ def set_arrays(attack_techniques, monkey_config, monkey_schema):
if 'attack_techniques' not in array_field: if 'attack_techniques' not in array_field:
continue continue
try: try:
should_remove = not should_enable_field(array_field['attack_techniques'], attack_techniques) should_remove = not AttackConfig.should_enable_field(array_field['attack_techniques'], attack_techniques)
except KeyError: except KeyError:
# Monkey schema field contains not yet implemented technique # Monkey schema field contains not yet implemented technique
continue continue
# If exploiter's attack technique is disabled, disable the exploiter/scanner/PBA # If exploiter's attack technique is disabled, disable the exploiter/scanner/PBA
r_alter_array(monkey_config, key, array_field['enum'][0], remove=should_remove) AttackConfig.r_alter_array(monkey_config, key, array_field['enum'][0], remove=should_remove)
@staticmethod
def set_booleans(attack_techniques, monkey_config, monkey_schema): def set_booleans(attack_techniques, monkey_config, monkey_schema):
""" """
Sets boolean type fields, like "should use mimikatz?" in monkey's config according to ATT&CK matrix Sets boolean type fields, like "should use mimikatz?" in monkey's config according to ATT&CK matrix
@ -69,9 +74,9 @@ def set_booleans(attack_techniques, monkey_config, monkey_schema):
:param monkey_schema: Monkey configuration schema :param monkey_schema: Monkey configuration schema
""" """
for key, value in monkey_schema['properties'].items(): for key, value in monkey_schema['properties'].items():
r_set_booleans([key], value, attack_techniques, monkey_config) AttackConfig.r_set_booleans([key], value, attack_techniques, monkey_config)
@staticmethod
def r_set_booleans(path, value, attack_techniques, monkey_config): def r_set_booleans(path, value, attack_techniques, monkey_config):
""" """
Recursively walks trough monkey configuration (DFS) to find which boolean fields needs to be set and sets them Recursively walks trough monkey configuration (DFS) to find which boolean fields needs to be set and sets them
@ -86,8 +91,9 @@ def r_set_booleans(path, value, attack_techniques, monkey_config):
# If 'value' is a boolean value that should be set: # If 'value' is a boolean value that should be set:
if 'type' in value and value['type'] == 'boolean' and 'attack_techniques' in value: if 'type' in value and value['type'] == 'boolean' and 'attack_techniques' in value:
try: try:
set_bool_conf_val(path, AttackConfig.set_bool_conf_val(path,
should_enable_field(value['attack_techniques'], attack_techniques), AttackConfig.should_enable_field(value['attack_techniques'],
attack_techniques),
monkey_config) monkey_config)
except KeyError: except KeyError:
# Monkey schema has a technique that is not yet implemented # Monkey schema has a technique that is not yet implemented
@ -99,11 +105,11 @@ def r_set_booleans(path, value, attack_techniques, monkey_config):
dictionary = value dictionary = value
for key, item in dictionary.items(): for key, item in dictionary.items():
path.append(key) path.append(key)
r_set_booleans(path, item, attack_techniques, monkey_config) AttackConfig.r_set_booleans(path, item, attack_techniques, monkey_config)
# Method enumerated everything in current path, goes back a level. # Method enumerated everything in current path, goes back a level.
del path[-1] del path[-1]
@staticmethod
def set_bool_conf_val(path, val, monkey_config): def set_bool_conf_val(path, val, monkey_config):
""" """
Changes monkey's configuration by setting one of its boolean fields value Changes monkey's configuration by setting one of its boolean fields value
@ -113,7 +119,7 @@ def set_bool_conf_val(path, val, monkey_config):
""" """
util.set(monkey_config, '/'.join(path), val) util.set(monkey_config, '/'.join(path), val)
@staticmethod
def should_enable_field(field_techniques, users_techniques): def should_enable_field(field_techniques, users_techniques):
""" """
Determines whether a single config field should be enabled or not. Determines whether a single config field should be enabled or not.
@ -129,7 +135,7 @@ def should_enable_field(field_techniques, users_techniques):
return False return False
return True return True
@staticmethod
def r_alter_array(config_value, array_name, field, remove=True): def r_alter_array(config_value, array_name, field, remove=True):
""" """
Recursively searches config (DFS) for array and removes/adds a field. Recursively searches config (DFS) for array and removes/adds a field.
@ -146,15 +152,15 @@ def r_alter_array(config_value, array_name, field, remove=True):
config_value[array_name].append(field) config_value[array_name].append(field)
else: else:
for prop in config_value.items(): for prop in config_value.items():
r_alter_array(prop[1], array_name, field, remove) AttackConfig.r_alter_array(prop[1], array_name, field, remove)
@staticmethod
def get_technique_values(): def get_technique_values():
""" """
Parses ATT&CK config into a dict of techniques and corresponding values. Parses ATT&CK config into a dict of techniques and corresponding values.
:return: Dictionary of techniques. Format: {"T1110": True, "T1075": False, ...} :return: Dictionary of techniques. Format: {"T1110": True, "T1075": False, ...}
""" """
attack_config = get_config() attack_config = AttackConfig.get_config()
techniques = {} techniques = {}
for type_name, attack_type in attack_config['properties'].items(): for type_name, attack_type in attack_config['properties'].items():
for key, technique in attack_type['properties'].items(): for key, technique in attack_type['properties'].items():

View File

@ -9,6 +9,11 @@ __author__ = "VakarisZ"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AttackTelemService(object):
def __init__(self):
pass
@staticmethod
def set_results(technique, data): def set_results(technique, data):
""" """
Adds ATT&CK technique results(telemetry) to the database Adds ATT&CK technique results(telemetry) to the database

View File

@ -0,0 +1,25 @@
import logging
from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.services.attack.attack_config import AttackConfig
from monkey_island.cc.services.post_breach_files import remove_PBA_files
from flask import jsonify
from monkey_island.cc.database import mongo
logger = logging.getLogger(__name__)
class Database(object):
def __init__(self):
pass
@staticmethod
def reset_db():
remove_PBA_files()
# We can't drop system collections.
[mongo.db[x].drop() for x in mongo.db.collection_names() if not x.startswith('system.')]
ConfigService.init_config()
AttackConfig.reset_config()
logger.info('DB was reset')
return jsonify(status='OK')

View File

@ -14,7 +14,6 @@ import ReportPage from 'components/pages/ReportPage';
import LicensePage from 'components/pages/LicensePage'; import LicensePage from 'components/pages/LicensePage';
import AuthComponent from 'components/AuthComponent'; import AuthComponent from 'components/AuthComponent';
import LoginPageComponent from 'components/pages/LoginPage'; import LoginPageComponent from 'components/pages/LoginPage';
import AttckPage from 'components/pages/AttackPage'
import 'normalize.css/normalize.css'; import 'normalize.css/normalize.css';
import 'react-data-components/css/table-twbs.css'; import 'react-data-components/css/table-twbs.css';
@ -162,7 +161,6 @@ class AppComponent extends AuthComponent {
<hr/> <hr/>
<ul> <ul>
<li><NavLink to="/attack">ATT&CK Configuration</NavLink></li>
<li><NavLink to="/configure">Configuration</NavLink></li> <li><NavLink to="/configure">Configuration</NavLink></li>
<li><NavLink to="/infection/telemetry">Log</NavLink></li> <li><NavLink to="/infection/telemetry">Log</NavLink></li>
</ul> </ul>
@ -188,7 +186,6 @@ class AppComponent extends AuthComponent {
{this.renderRoute('/start-over', <StartOverPage onStatusChange={this.updateStatus}/>)} {this.renderRoute('/start-over', <StartOverPage onStatusChange={this.updateStatus}/>)}
{this.renderRoute('/report', <ReportPage onStatusChange={this.updateStatus}/>)} {this.renderRoute('/report', <ReportPage onStatusChange={this.updateStatus}/>)}
{this.renderRoute('/license', <LicensePage onStatusChange={this.updateStatus}/>)} {this.renderRoute('/license', <LicensePage onStatusChange={this.updateStatus}/>)}
{this.renderRoute('/attack', <AttckPage onStatusChange={this.updateStatus}/>)}
</Col> </Col>
</Row> </Row>
</Grid> </Grid>

View File

@ -1,14 +1,20 @@
import React from 'react'; import React from 'react';
import Checkbox from '../ui-components/checkbox' import Checkbox from '../ui-components/Checkbox'
import Tooltip from 'react-tooltip-lite' import Tooltip from 'react-tooltip-lite'
import AuthComponent from '../AuthComponent'; import AuthComponent from '../AuthComponent';
import ReactTable from "react-table"; import ReactTable from "react-table";
import 'filepond/dist/filepond.min.css'; import 'filepond/dist/filepond.min.css';
import '../../styles/Tooltip.scss'; import '../../styles/Tooltip.scss';
import {Col} from "react-bootstrap";
class MatrixComponent extends AuthComponent {
constructor(props) {
super(props);
this.state = {lastAction: 'none'}
};
// Finds which attack type has most techniques and returns that number // Finds which attack type has most techniques and returns that number
let findMaxTechniques = function (data){ static findMaxTechniques(data){
let maxLen = 0; let maxLen = 0;
data.forEach(function(techType) { data.forEach(function(techType) {
if (Object.keys(techType.properties).length > maxLen){ if (Object.keys(techType.properties).length > maxLen){
@ -19,7 +25,7 @@ let findMaxTechniques = function (data){
}; };
// Parses ATT&CK config schema into data suitable for react-table (ATT&CK matrix) // Parses ATT&CK config schema into data suitable for react-table (ATT&CK matrix)
let parseTechniques = function (data, maxLen) { static parseTechniques (data, maxLen) {
let techniques = []; let techniques = [];
// Create rows with attack techniques // Create rows with attack techniques
for (let i = 0; i < maxLen; i++) { for (let i = 0; i < maxLen; i++) {
@ -43,12 +49,6 @@ let parseTechniques = function (data, maxLen) {
return techniques; return techniques;
}; };
class MatrixComponent extends AuthComponent {
constructor(props) {
super(props);
this.state = this.getStateFromConfig(this.props.configuration, 'none');
};
getColumns(matrixData) { getColumns(matrixData) {
return Object.keys(matrixData[0]).map((key)=>{ return Object.keys(matrixData[0]).map((key)=>{
return { return {
@ -68,36 +68,13 @@ class MatrixComponent extends AuthComponent {
<Checkbox checked={technique.value} <Checkbox checked={technique.value}
necessary={technique.necessary} necessary={technique.necessary}
name={technique.name} name={technique.name}
changeHandler={this.handleTechniqueChange}> changeHandler={this.props.change}>
{technique.title} {technique.title}
</Checkbox> </Checkbox>
</Tooltip>) </Tooltip>)
} }
}; };
onSubmit = () => {
this.authFetch('/api/attack',
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(this.state.configData)
})
.then(res => {
if (!res.ok)
{
throw Error()
}
return res;
}).then(
this.setState({
lastAction: 'saved'
})
).catch(error => {
console.log('bad configuration');
this.setState({lastAction: 'invalid_configuration'});
});
};
resetConfig = () => { resetConfig = () => {
this.authFetch('/api/attack', this.authFetch('/api/attack',
{ {
@ -111,82 +88,38 @@ class MatrixComponent extends AuthComponent {
}); });
}; };
// Updates state based on values in config supplied. getTableData = (config) => {
updateStateFromConfig = (config, lastAction = '') => {
this.setState(this.getStateFromConfig(config, lastAction));
};
getStateFromConfig = (config, lastAction) => {
let configCopy = JSON.parse(JSON.stringify(config)); let configCopy = JSON.parse(JSON.stringify(config));
let maxTechniques = findMaxTechniques(Object.values(configCopy)); let maxTechniques = MatrixComponent.findMaxTechniques(Object.values(configCopy));
let matrixTableData = parseTechniques(Object.values(configCopy), maxTechniques); let matrixTableData = MatrixComponent.parseTechniques(Object.values(configCopy), maxTechniques);
let columns = this.getColumns(matrixTableData); let columns = this.getColumns(matrixTableData);
return { return {'columns': columns, 'matrixTableData': matrixTableData, 'maxTechniques': maxTechniques}
lastAction: lastAction,
configData: config,
maxTechniques: maxTechniques,
matrixTableData: matrixTableData,
columns: columns
};
};
// Handles change in technique, when user toggles it
handleTechniqueChange = (technique, value, mapped=false) => {
// Change value on configuration
Object.entries(this.state.configData).forEach(techType => {
if(techType[1].properties.hasOwnProperty(technique)){
let tempMatrix = this.state.configData;
tempMatrix[techType[0]].properties[technique].value = value;
// Toggle all mapped techniques
if (! mapped && tempMatrix[techType[0]].properties[technique].hasOwnProperty('depends_on')){
tempMatrix[techType[0]].properties[technique].depends_on.forEach(mappedTechnique => {
this.handleTechniqueChange(mappedTechnique, value, true)
})
}
this.updateStateFromConfig(tempMatrix);
}
});
}; };
render() { render() {
let tableData = this.getTableData(this.props.configuration);
return ( return (
<div>
<div id="header" className="row justify-content-between attack-legend">
<Col xs={4}>
<i className="fa fa-circle-thin icon-unchecked"></i>
<span> - Dissabled</span>
</Col>
<Col xs={4}>
<i className="fa fa-circle icon-checked"></i>
<span> - Enabled</span>
</Col>
<Col xs={4}>
<i className="fa fa-circle icon-mandatory"></i>
<span> - Mandatory</span>
</Col>
</div>
<div className={"attack-matrix"}> <div className={"attack-matrix"}>
<form onSubmit={this.onSubmit}> <ReactTable columns={tableData['columns']}
<ReactTable data={tableData['matrixTableData']}
columns={this.state.columns}
data={this.state.matrixTableData}
showPagination={false} showPagination={false}
defaultPageSize={this.state.maxTechniques} /> defaultPageSize={tableData['maxTechniques']} />
<div className={"messages"}>
{ this.state.lastAction === 'reset' ?
<div className="alert alert-success">
<i className="glyphicon glyphicon-ok-sign" style={{'marginRight': '5px'}}/>
Matrix reset to default.
</div> </div>
: ''}
{ this.state.lastAction === 'saved' ?
<div className="alert alert-success">
<i className="glyphicon glyphicon-ok-sign" style={{'marginRight': '5px'}}/>
Matrix applied to configuration.
</div>
: ''}
{ this.state.lastAction === 'invalid_configuration' ?
<div className="alert alert-danger">
<i className="glyphicon glyphicon-exclamation-sign" style={{'marginRight': '5px'}}/>
An invalid matrix configuration supplied, check selected fields.
</div>
: ''}
</div>
<div className="text-center">
<button type="button" onClick={this.onSubmit} className="btn btn-success btn-lg" style={{margin: '5px'}}>
Apply to configuration
</button>
<button type="button" onClick={this.resetConfig} className="btn btn-danger btn-lg" style={{margin: '5px'}}>
Reset to default matrix
</button>
</div>
</form>
</div>); </div>);
} }
} }

View File

@ -1,66 +0,0 @@
import React from 'react';
import AuthComponent from '../AuthComponent';
import 'filepond/dist/filepond.min.css';
import MatrixComponent from '../attack/MatrixComponent'
import {Col} from "react-bootstrap";
import '../../styles/Checkbox.scss'
class AttackComponent extends AuthComponent {
constructor(props) {
super(props);
this.currentSection = 'ATT&CK matrix';
this.currentFormData = {};
this.sectionsOrder = ['ATT&CK matrix'];
// set schema from server
this.state = {
configuration: {},
sections: [],
selectedSection: 'ATT&CK matrix',
};
}
componentDidMount() {
this.authFetch('/api/attack')
.then(res => res.json())
.then(res => {
let sections = [];
for (let sectionKey of this.sectionsOrder) {
sections.push({key: sectionKey, title: res.configuration.title});
}
this.setState({
configuration: res.configuration,
sections: sections,
selectedSection: 'ATT&CK matrix'
});
});
}
render() {
let content;
if (Object.keys(this.state.configuration).length === 0) {
content = (<h1>Fetching configuration...</h1>);
} else {
content = (
<div>
<div id="header" className="row justify-content-between attack-legend">
<Col xs={4}>
<i className="fa fa-circle-thin icon-unchecked"></i>
<span> - Dissabled</span>
</Col>
<Col xs={4}>
<i className="fa fa-circle icon-checked"></i>
<span> - Enabled</span>
</Col>
<Col xs={4}>
<i className="fa fa-circle icon-mandatory"></i>
<span> - Mandatory</span>
</Col>
</div>
<MatrixComponent configuration={this.state.configuration} />
</div>);
}
return <div>{content}</div>;
}
}
export default AttackComponent;

View File

@ -1,19 +1,26 @@
import React from 'react'; 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, Modal, Nav, NavItem} from 'react-bootstrap';
import fileDownload from 'js-file-download'; import fileDownload from 'js-file-download';
import AuthComponent from '../AuthComponent'; import AuthComponent from '../AuthComponent';
import { FilePond } from 'react-filepond'; import { FilePond } from 'react-filepond';
import 'filepond/dist/filepond.min.css'; import 'filepond/dist/filepond.min.css';
import MatrixComponent from "../attack/MatrixComponent";
const ATTACK_URL = '/api/attack';
const CONFIG_URL = '/api/configuration/island';
class ConfigurePageComponent extends AuthComponent { class ConfigurePageComponent extends AuthComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.PBAwindowsPond = null; this.PBAwindowsPond = null;
this.PBAlinuxPond = null; this.PBAlinuxPond = null;
this.currentSection = 'basic'; this.currentSection = 'attack';
this.currentFormData = {}; this.currentFormData = {};
this.sectionsOrder = ['basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; this.initialConfig = {};
this.initialAttackConfig = {};
this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal'];
this.uiSchema = { this.uiSchema = {
behaviour: { behaviour: {
custom_PBA_linux_cmd: { custom_PBA_linux_cmd: {
@ -44,37 +51,94 @@ class ConfigurePageComponent extends AuthComponent {
this.state = { this.state = {
schema: {}, schema: {},
configuration: {}, configuration: {},
attackConfig: {},
lastAction: 'none', lastAction: 'none',
sections: [], sections: [],
selectedSection: 'basic', selectedSection: 'attack',
allMonkeysAreDead: true, allMonkeysAreDead: true,
PBAwinFile: [], PBAwinFile: [],
PBAlinuxFile: [] PBAlinuxFile: [],
showAttackAlert: false
}; };
} }
componentDidMount() { setInitialConfig(config) {
this.authFetch('/api/configuration/island') this.initialConfig = JSON.parse(JSON.stringify(config));
.then(res => res.json()) }
.then(res => {
setInitialAttackConfig(attackConfig) {
this.initialAttackConfig = JSON.parse(JSON.stringify(attackConfig));
}
componentDidMount = () => {
let urls = [CONFIG_URL, ATTACK_URL];
Promise.all(urls.map(url => this.authFetch(url).then(res => res.json())))
.then(data => {
let sections = []; let sections = [];
let attackConfig = data[1];
let monkeyConfig = data[0];
this.setInitialConfig(monkeyConfig.configuration);
this.setInitialAttackConfig(attackConfig.configuration);
for (let sectionKey of this.sectionsOrder) { for (let sectionKey of this.sectionsOrder) {
sections.push({key: sectionKey, title: res.schema.properties[sectionKey].title}); if (sectionKey === 'attack') {sections.push({key:sectionKey, title: "ATT&CK"})}
else {sections.push({key: sectionKey, title: monkeyConfig.schema.properties[sectionKey].title});}
} }
this.setState({ this.setState({
schema: res.schema, schema: monkeyConfig.schema,
configuration: res.configuration, configuration: monkeyConfig.configuration,
attackConfig: attackConfig.configuration,
sections: sections, sections: sections,
selectedSection: 'basic' selectedSection: 'attack'
}) })
}); });
this.updateMonkeysRunning(); this.updateMonkeysRunning();
} };
onSubmit = ({formData}) => { updateConfig = () => {
this.currentFormData = formData; this.authFetch(CONFIG_URL)
.then(res => res.json())
.then(data => {
this.setInitialConfig(data.configuration);
this.setState({configuration: data.configuration})
})
};
onSubmit = () => {
if (this.state.selectedSection === 'attack'){
this.matrixSubmit()
} else {
this.configSubmit()
}
};
matrixSubmit = () => {
// Submit attack matrix
this.authFetch(ATTACK_URL,
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(this.state.attackConfig)
})
.then(res => {
if (!res.ok)
{
throw Error()
}
return res;
})
.then(() => {this.setInitialAttackConfig(this.state.attackConfig);
this.setState({lastAction: 'saved'})})
.then(this.updateConfig())
.catch(error => {
console.log('bad attack configuration');
this.setState({lastAction: 'invalid_configuration'});
});
};
configSubmit = () => {
// Submit monkey configuration
this.updateConfigSection(); this.updateConfigSection();
this.authFetch('/api/configuration/island', this.authFetch(CONFIG_URL,
{ {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
@ -94,11 +158,33 @@ class ConfigurePageComponent extends AuthComponent {
schema: res.schema, schema: res.schema,
configuration: res.configuration configuration: res.configuration
}); });
this.setInitialConfig(res.configuration);
this.props.onStatusChange(); this.props.onStatusChange();
}).catch(error => { }).catch(error => {
console.log('bad configuration'); console.log('bad configuration');
this.setState({lastAction: 'invalid_configuration'}); this.setState({lastAction: 'invalid_configuration'});
}); });
};
// Alters attack configuration when user toggles technique
attackTechniqueChange = (technique, value, mapped=false) => {
// Change value in attack configuration
// Go trough each column in matrix, searching for technique
Object.entries(this.state.attackConfig).forEach(techType => {
if(techType[1].properties.hasOwnProperty(technique)){
let tempMatrix = this.state.attackConfig;
tempMatrix[techType[0]].properties[technique].value = value;
this.setState({attackConfig: tempMatrix});
// Toggle all mapped techniques
if (! mapped && tempMatrix[techType[0]].properties[technique].hasOwnProperty('depends_on')){
tempMatrix[techType[0]].properties[technique].depends_on.forEach(mappedTechnique => {
this.attackTechniqueChange(mappedTechnique, value, true)
})
}
}
});
}; };
onChange = ({formData}) => { onChange = ({formData}) => {
@ -111,11 +197,49 @@ class ConfigurePageComponent extends AuthComponent {
newConfig[this.currentSection] = this.currentFormData; newConfig[this.currentSection] = this.currentFormData;
this.currentFormData = {}; this.currentFormData = {};
} }
this.setState({configuration: newConfig}); this.setState({configuration: newConfig, lastAction: 'none'});
}; };
renderAttackAlertModal = () => {
return (<Modal show={this.state.showAttackAlert} onHide={() => {this.setState({showAttackAlert: false})}}>
<Modal.Body>
<h2><div className="text-center">Warning</div></h2>
<p className = "text-center" style={{'fontSize': '1.2em', 'marginBottom': '2em'}}>
You have unsubmitted changes. Submit them before proceeding.
</p>
<div className="text-center">
<button type="button"
className="btn btn-success btn-lg"
style={{margin: '5px'}}
onClick={() => {this.setState({showAttackAlert: false})}} >
Cancel
</button>
</div>
</Modal.Body>
</Modal>)
};
userChangedConfig(){
if(JSON.stringify(this.state.configuration) === JSON.stringify(this.initialConfig)){
if(Object.keys(this.currentFormData).length === 0 ||
JSON.stringify(this.initialConfig[this.currentSection]) === JSON.stringify(this.currentFormData)){
return false;
}
}
return true;
}
userChangedMatrix(){
return (JSON.stringify(this.state.attackConfig) !== JSON.stringify(this.initialAttackConfig))
}
setSelectedSection = (key) => { setSelectedSection = (key) => {
this.updateConfigSection(); this.updateConfigSection();
if ((key === 'attack' && this.userChangedConfig()) ||
(this.currentSection === 'attack' && this.userChangedMatrix())){
this.setState({showAttackAlert: true});
return
}
this.currentSection = key; this.currentSection = key;
this.setState({ this.setState({
selectedSection: key selectedSection: key
@ -124,7 +248,7 @@ class ConfigurePageComponent extends AuthComponent {
resetConfig = () => { resetConfig = () => {
this.removePBAfiles(); this.removePBAfiles();
this.authFetch('/api/configuration/island', this.authFetch(CONFIG_URL,
{ {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
@ -137,8 +261,15 @@ class ConfigurePageComponent extends AuthComponent {
schema: res.schema, schema: res.schema,
configuration: res.configuration configuration: res.configuration
}); });
this.setInitialConfig(res.configuration);
this.props.onStatusChange(); this.props.onStatusChange();
}); }).then(this.authFetch(ATTACK_URL,{ method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify('reset_attack_matrix')}))
.then(res => res.json())
.then(res => {
this.setState({attackConfig: res.configuration})
})
}; };
removePBAfiles(){ removePBAfiles(){
@ -273,34 +404,13 @@ class ConfigurePageComponent extends AuthComponent {
render() { render() {
let displayedSchema = {}; let displayedSchema = {};
if (this.state.schema.hasOwnProperty('properties')) { if (this.state.schema.hasOwnProperty('properties') && this.state.selectedSection !== 'attack') {
displayedSchema = this.state.schema['properties'][this.state.selectedSection]; displayedSchema = this.state.schema['properties'][this.state.selectedSection];
displayedSchema['definitions'] = this.state.schema['definitions']; displayedSchema['definitions'] = this.state.schema['definitions'];
} }
return ( let config_content = (<Form schema={displayedSchema}
<Col xs={12} lg={8}>
<h1 className="page-title">Monkey Configuration</h1>
<Nav bsStyle="tabs" justified
activeKey={this.state.selectedSection} onSelect={this.setSelectedSection}
style={{'marginBottom': '2em'}}>
{this.state.sections.map(section =>
<NavItem key={section.key} eventKey={section.key}>{section.title}</NavItem>
)}
</Nav>
{
this.state.selectedSection === 'basic_network' ?
<div className="alert alert-info">
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
The Monkey scans its subnet if "Local network scan" is ticked. Additionally the monkey scans machines
according to its range class.
</div>
: <div />
}
{ this.state.selectedSection ?
<Form schema={displayedSchema}
uiSchema={this.uiSchema} uiSchema={this.uiSchema}
formData={this.state.configuration[this.state.selectedSection]} formData={this.state.configuration[this.state.selectedSection]}
onSubmit={this.onSubmit}
onChange={this.onChange} onChange={this.onChange}
noValidate={true}> noValidate={true}>
<div> <div>
@ -312,17 +422,47 @@ class ConfigurePageComponent extends AuthComponent {
infections. infections.
</div> </div>
} }
</div>
</Form>);
let attack_content = (<MatrixComponent configuration={this.state.attackConfig}
submit={this.componentDidMount}
reset={this.resetConfig}
change={this.attackTechniqueChange}/>);
let content = '';
if (this.state.selectedSection === 'attack' && Object.entries(this.state.attackConfig).length !== 0 ) {
content = attack_content
} else if(this.state.selectedSection !== 'attack') {
content = config_content
}
return (
<Col xs={12} lg={8}>
{this.renderAttackAlertModal()}
<h1 className="page-title">Monkey Configuration</h1>
<Nav bsStyle="tabs" justified
activeKey={this.state.selectedSection} onSelect={this.setSelectedSection}
style={{'marginBottom': '2em'}}>
{this.state.sections.map(section => <NavItem key={section.key} eventKey={section.key}>{section.title}</NavItem>)}
</Nav>
{
this.state.selectedSection === 'basic_network' ?
<div className="alert alert-info">
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
The Monkey scans its subnet if "Local network scan" is ticked. Additionally the monkey scans machines
according to its range class.
</div>
: <div />
}
{ content }
<div className="text-center"> <div className="text-center">
<button type="submit" className="btn btn-success btn-lg" style={{margin: '5px'}}> <button type="submit" onClick={this.onSubmit} className="btn btn-success btn-lg" style={{margin: '5px'}}>
Submit Submit
</button> </button>
<button type="button" onClick={this.resetConfig} className="btn btn-danger btn-lg" style={{margin: '5px'}}> <button type="button" onClick={this.resetConfig} className="btn btn-danger btn-lg" style={{margin: '5px'}}>
Reset to defaults Reset to defaults
</button> </button>
</div> </div>
</div>
</Form>
: ''}
<div className="text-center"> <div className="text-center">
<button onClick={() => document.getElementById('uploadInputInternal').click()} <button onClick={() => document.getElementById('uploadInputInternal').click()}
className="btn btn-info btn-lg" style={{margin: '5px'}}> className="btn btn-info btn-lg" style={{margin: '5px'}}>

View File

@ -9,6 +9,12 @@ class CheckboxComponent extends React.PureComponent {
} }
} }
/*
Parent component can pass a name and a changeHandler (function) for this component in props.
changeHandler(name, checked) function will be called with these parameters:
this.props.name (the name of this component) and
this.state.checked (boolean indicating if this component is checked or not)
*/
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -17,20 +23,21 @@ class CheckboxComponent extends React.PureComponent {
isAnimating: false isAnimating: false
}; };
this.toggleChecked = this.toggleChecked.bind(this); this.toggleChecked = this.toggleChecked.bind(this);
this.ping = this.ping.bind(this); this.stopAnimation = this.stopAnimation.bind(this);
this.composeStateClasses = this.composeStateClasses.bind(this); this.composeStateClasses = this.composeStateClasses.bind(this);
} }
//Toggles component.
toggleChecked() { toggleChecked() {
if (this.state.isAnimating) return false; if (this.state.isAnimating) {return false;}
this.setState({ this.setState({
checked: !this.state.checked, checked: !this.state.checked,
isAnimating: true, isAnimating: true,
}, () => { this.props.changeHandler(this.props.name, this.state.checked)}); }, () => { this.props.changeHandler ? this.props.changeHandler(this.props.name, this.state.checked) : null});
} }
// Stops animation // Stops ping animation on checkbox after click
ping() { stopAnimation() {
this.setState({ isAnimating: false }) this.setState({ isAnimating: false })
} }
@ -57,7 +64,7 @@ class CheckboxComponent extends React.PureComponent {
type="checkbox" value={this.state.checked} type="checkbox" value={this.state.checked}
name={this.props.name}/> name={this.props.name}/>
<label className="text">{ this.props.children }</label> <label className="text">{ this.props.children }</label>
<div className="ui-btn-ping" onTransitionEnd={this.ping}></div> <div className="ui-btn-ping" onTransitionEnd={this.stopAnimation}></div>
</div> </div>
) )
} }

View File

@ -186,6 +186,10 @@ body {
.nav-tabs > li > a { .nav-tabs > li > a {
height: 63px height: 63px
} }
.nav > li > a:focus {
background-color: transparent !important;
}
/* /*
* Run Monkey Page * Run Monkey Page
*/ */