forked from p34709852/monkey
Attack configuration page moved into island configuration
This commit is contained in:
parent
c055820a0d
commit
cdd3270730
|
@ -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_feed import TelemetryFeed
|
||||
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.resources.pba_file_upload import FileUpload
|
||||
from monkey_island.cc.resources.attack_telem import AttackTelem
|
||||
|
@ -100,7 +100,7 @@ def init_app(mongo_url):
|
|||
|
||||
with app.app_context():
|
||||
database.init()
|
||||
ConfigService.init_config()
|
||||
Database.reset_db()
|
||||
|
||||
app.add_url_rule('/', 'serve_home', serve_home)
|
||||
app.add_url_rule('/<path:static_path>', 'serve_static_file', serve_static_file)
|
||||
|
|
|
@ -3,7 +3,7 @@ import json
|
|||
from flask import jsonify, request
|
||||
|
||||
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"
|
||||
|
||||
|
@ -11,7 +11,7 @@ __author__ = "VakarisZ"
|
|||
class AttackConfiguration(flask_restful.Resource):
|
||||
@jwt_required()
|
||||
def get(self):
|
||||
return jsonify(configuration=attack_config.get_config()['properties'])
|
||||
return jsonify(configuration=AttackConfig.get_config()['properties'])
|
||||
|
||||
@jwt_required()
|
||||
def post(self):
|
||||
|
@ -21,10 +21,10 @@ class AttackConfiguration(flask_restful.Resource):
|
|||
"""
|
||||
config_json = json.loads(request.data)
|
||||
if 'reset_attack_matrix' in config_json:
|
||||
attack_config.reset_config()
|
||||
return jsonify(configuration=attack_config.get_config()['properties'])
|
||||
AttackConfig.reset_config()
|
||||
return jsonify(configuration=AttackConfig.get_config()['properties'])
|
||||
else:
|
||||
attack_config.update_config({'properties': json.loads(request.data)})
|
||||
attack_config.apply_to_monkey_config()
|
||||
AttackConfig.update_config({'properties': json.loads(request.data)})
|
||||
AttackConfig.apply_to_monkey_config()
|
||||
return {}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import flask_restful
|
||||
from flask import request
|
||||
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
|
||||
|
||||
__author__ = 'VakarisZ'
|
||||
|
@ -20,5 +20,5 @@ class AttackTelem(flask_restful.Resource):
|
|||
:param technique: Technique ID, e.g. T1111
|
||||
"""
|
||||
data = json.loads(request.data)
|
||||
set_results(technique, data)
|
||||
AttackTelemService.set_results(technique, data)
|
||||
return {}
|
||||
|
|
|
@ -6,12 +6,10 @@ from flask import request, make_response, jsonify
|
|||
|
||||
from monkey_island.cc.auth import jwt_required
|
||||
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.report import ReportService
|
||||
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'
|
||||
|
||||
|
@ -27,7 +25,7 @@ class Root(flask_restful.Resource):
|
|||
if not action:
|
||||
return Root.get_server_info()
|
||||
elif action == "reset":
|
||||
return Root.reset_db()
|
||||
return jwt_required()(Database.reset_db())
|
||||
elif action == "killall":
|
||||
return Root.kill_all()
|
||||
elif action == "is-up":
|
||||
|
@ -41,17 +39,6 @@ class Root(flask_restful.Resource):
|
|||
return jsonify(ip_addresses=local_ip_addresses(), mongo=str(mongo.db),
|
||||
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
|
||||
@jwt_required()
|
||||
def kill_all():
|
||||
|
|
|
@ -9,154 +9,160 @@ __author__ = "VakarisZ"
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_config():
|
||||
config = mongo.db.attack.find_one({'name': 'newconfig'}) or reset_config()
|
||||
return config
|
||||
class AttackConfig(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_config():
|
||||
config = mongo.db.attack.find_one({'name': 'newconfig'})
|
||||
return config
|
||||
|
||||
def get_config_schema():
|
||||
return SCHEMA
|
||||
@staticmethod
|
||||
def get_config_schema():
|
||||
return SCHEMA
|
||||
|
||||
@staticmethod
|
||||
def reset_config():
|
||||
AttackConfig.update_config(SCHEMA)
|
||||
|
||||
def reset_config():
|
||||
update_config(SCHEMA)
|
||||
@staticmethod
|
||||
def update_config(config_json):
|
||||
mongo.db.attack.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def apply_to_monkey_config():
|
||||
"""
|
||||
Applies ATT&CK matrix to the monkey configuration
|
||||
:return:
|
||||
"""
|
||||
attack_techniques = AttackConfig.get_technique_values()
|
||||
monkey_config = ConfigService.get_config(False, True, True)
|
||||
monkey_schema = ConfigService.get_config_schema()
|
||||
AttackConfig.set_arrays(attack_techniques, monkey_config, monkey_schema)
|
||||
AttackConfig.set_booleans(attack_techniques, monkey_config, monkey_schema)
|
||||
ConfigService.update_config(monkey_config, True)
|
||||
|
||||
def update_config(config_json):
|
||||
mongo.db.attack.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True)
|
||||
return True
|
||||
@staticmethod
|
||||
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
|
||||
:param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...}
|
||||
:param monkey_config: Monkey island's configuration
|
||||
:param monkey_schema: Monkey configuration schema
|
||||
"""
|
||||
for key, definition in monkey_schema['definitions'].items():
|
||||
for array_field in definition['anyOf']:
|
||||
# Check if current array field has attack_techniques assigned to it
|
||||
if 'attack_techniques' not in array_field:
|
||||
continue
|
||||
try:
|
||||
should_remove = not AttackConfig.should_enable_field(array_field['attack_techniques'], attack_techniques)
|
||||
except KeyError:
|
||||
# Monkey schema field contains not yet implemented technique
|
||||
continue
|
||||
# If exploiter's attack technique is disabled, disable the exploiter/scanner/PBA
|
||||
AttackConfig.r_alter_array(monkey_config, key, array_field['enum'][0], remove=should_remove)
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
:param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...}
|
||||
:param monkey_config: Monkey island's configuration
|
||||
:param monkey_schema: Monkey configuration schema
|
||||
"""
|
||||
for key, value in monkey_schema['properties'].items():
|
||||
AttackConfig.r_set_booleans([key], value, attack_techniques, monkey_config)
|
||||
|
||||
def apply_to_monkey_config():
|
||||
"""
|
||||
Applies ATT&CK matrix to the monkey configuration
|
||||
:return:
|
||||
"""
|
||||
attack_techniques = get_technique_values()
|
||||
monkey_config = ConfigService.get_config(False, True, True)
|
||||
monkey_schema = ConfigService.get_config_schema()
|
||||
set_arrays(attack_techniques, monkey_config, monkey_schema)
|
||||
set_booleans(attack_techniques, monkey_config, monkey_schema)
|
||||
ConfigService.update_config(monkey_config, True)
|
||||
@staticmethod
|
||||
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
|
||||
according to ATT&CK matrix.
|
||||
:param path: Property names that leads to current value. E.g. ['monkey', 'system_info', 'should_use_mimikatz']
|
||||
:param value: Value of config property
|
||||
:param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...}
|
||||
:param monkey_config: Monkey island's configuration
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
dictionary = {}
|
||||
# If 'value' is a boolean value that should be set:
|
||||
if 'type' in value and value['type'] == 'boolean' and 'attack_techniques' in value:
|
||||
try:
|
||||
AttackConfig.set_bool_conf_val(path,
|
||||
AttackConfig.should_enable_field(value['attack_techniques'],
|
||||
attack_techniques),
|
||||
monkey_config)
|
||||
except KeyError:
|
||||
# Monkey schema has a technique that is not yet implemented
|
||||
pass
|
||||
# If 'value' is dict, we go over each of it's fields to search for booleans
|
||||
elif 'properties' in value:
|
||||
dictionary = value['properties']
|
||||
else:
|
||||
dictionary = value
|
||||
for key, item in dictionary.items():
|
||||
path.append(key)
|
||||
AttackConfig.r_set_booleans(path, item, attack_techniques, monkey_config)
|
||||
# Method enumerated everything in current path, goes back a level.
|
||||
del path[-1]
|
||||
|
||||
@staticmethod
|
||||
def set_bool_conf_val(path, val, monkey_config):
|
||||
"""
|
||||
Changes monkey's configuration by setting one of its boolean fields value
|
||||
:param path: Path to boolean value in monkey's configuration. E.g. ['monkey', 'system_info', 'should_use_mimikatz']
|
||||
:param val: Boolean
|
||||
:param monkey_config: Monkey's configuration
|
||||
"""
|
||||
util.set(monkey_config, '/'.join(path), val)
|
||||
|
||||
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
|
||||
:param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...}
|
||||
:param monkey_config: Monkey island's configuration
|
||||
:param monkey_schema: Monkey configuration schema
|
||||
"""
|
||||
for key, definition in monkey_schema['definitions'].items():
|
||||
for array_field in definition['anyOf']:
|
||||
# Check if current array field has attack_techniques assigned to it
|
||||
if 'attack_techniques' not in array_field:
|
||||
continue
|
||||
try:
|
||||
should_remove = not should_enable_field(array_field['attack_techniques'], attack_techniques)
|
||||
except KeyError:
|
||||
# Monkey schema field contains not yet implemented technique
|
||||
continue
|
||||
# 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)
|
||||
@staticmethod
|
||||
def should_enable_field(field_techniques, users_techniques):
|
||||
"""
|
||||
Determines whether a single config field should be enabled or not.
|
||||
:param field_techniques: ATT&CK techniques that field uses
|
||||
:param users_techniques: ATT&CK techniques that user chose
|
||||
:return: True, if user enabled all techniques used by the field, false otherwise
|
||||
"""
|
||||
# Method can't decide field value because it has no attack techniques assign to it.
|
||||
if not field_techniques:
|
||||
raise KeyError
|
||||
for technique in field_techniques:
|
||||
if not users_techniques[technique]:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def r_alter_array(config_value, array_name, field, remove=True):
|
||||
"""
|
||||
Recursively searches config (DFS) for array and removes/adds a field.
|
||||
:param config_value: Some object/value from config
|
||||
:param array_name: Name of array this method should search
|
||||
:param field: Field in array that this method should add/remove
|
||||
:param remove: Removes field from array if true, adds it if false
|
||||
"""
|
||||
if isinstance(config_value, dict):
|
||||
if array_name in config_value and isinstance(config_value[array_name], list):
|
||||
if remove and field in config_value[array_name]:
|
||||
config_value[array_name].remove(field)
|
||||
elif not remove and field not in config_value[array_name]:
|
||||
config_value[array_name].append(field)
|
||||
else:
|
||||
for prop in config_value.items():
|
||||
AttackConfig.r_alter_array(prop[1], array_name, field, remove)
|
||||
|
||||
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
|
||||
:param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...}
|
||||
:param monkey_config: Monkey island's configuration
|
||||
:param monkey_schema: Monkey configuration schema
|
||||
"""
|
||||
for key, value in monkey_schema['properties'].items():
|
||||
r_set_booleans([key], 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
|
||||
according to ATT&CK matrix.
|
||||
:param path: Property names that leads to current value. E.g. ['monkey', 'system_info', 'should_use_mimikatz']
|
||||
:param value: Value of config property
|
||||
:param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...}
|
||||
:param monkey_config: Monkey island's configuration
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
dictionary = {}
|
||||
# If 'value' is a boolean value that should be set:
|
||||
if 'type' in value and value['type'] == 'boolean' and 'attack_techniques' in value:
|
||||
try:
|
||||
set_bool_conf_val(path,
|
||||
should_enable_field(value['attack_techniques'], attack_techniques),
|
||||
monkey_config)
|
||||
except KeyError:
|
||||
# Monkey schema has a technique that is not yet implemented
|
||||
pass
|
||||
# If 'value' is dict, we go over each of it's fields to search for booleans
|
||||
elif 'properties' in value:
|
||||
dictionary = value['properties']
|
||||
else:
|
||||
dictionary = value
|
||||
for key, item in dictionary.items():
|
||||
path.append(key)
|
||||
r_set_booleans(path, item, attack_techniques, monkey_config)
|
||||
# Method enumerated everything in current path, goes back a level.
|
||||
del path[-1]
|
||||
|
||||
|
||||
def set_bool_conf_val(path, val, monkey_config):
|
||||
"""
|
||||
Changes monkey's configuration by setting one of its boolean fields value
|
||||
:param path: Path to boolean value in monkey's configuration. E.g. ['monkey', 'system_info', 'should_use_mimikatz']
|
||||
:param val: Boolean
|
||||
:param monkey_config: Monkey's configuration
|
||||
"""
|
||||
util.set(monkey_config, '/'.join(path), val)
|
||||
|
||||
|
||||
def should_enable_field(field_techniques, users_techniques):
|
||||
"""
|
||||
Determines whether a single config field should be enabled or not.
|
||||
:param field_techniques: ATT&CK techniques that field uses
|
||||
:param users_techniques: ATT&CK techniques that user chose
|
||||
:return: True, if user enabled all techniques used by the field, false otherwise
|
||||
"""
|
||||
# Method can't decide field value because it has no attack techniques assign to it.
|
||||
if not field_techniques:
|
||||
raise KeyError
|
||||
for technique in field_techniques:
|
||||
if not users_techniques[technique]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def r_alter_array(config_value, array_name, field, remove=True):
|
||||
"""
|
||||
Recursively searches config (DFS) for array and removes/adds a field.
|
||||
:param config_value: Some object/value from config
|
||||
:param array_name: Name of array this method should search
|
||||
:param field: Field in array that this method should add/remove
|
||||
:param remove: Removes field from array if true, adds it if false
|
||||
"""
|
||||
if isinstance(config_value, dict):
|
||||
if array_name in config_value and isinstance(config_value[array_name], list):
|
||||
if remove and field in config_value[array_name]:
|
||||
config_value[array_name].remove(field)
|
||||
elif not remove and field not in config_value[array_name]:
|
||||
config_value[array_name].append(field)
|
||||
else:
|
||||
for prop in config_value.items():
|
||||
r_alter_array(prop[1], array_name, field, remove)
|
||||
|
||||
|
||||
def get_technique_values():
|
||||
"""
|
||||
Parses ATT&CK config into a dict of techniques and corresponding values.
|
||||
:return: Dictionary of techniques. Format: {"T1110": True, "T1075": False, ...}
|
||||
"""
|
||||
attack_config = get_config()
|
||||
techniques = {}
|
||||
for type_name, attack_type in attack_config['properties'].items():
|
||||
for key, technique in attack_type['properties'].items():
|
||||
techniques[key] = technique['value']
|
||||
return techniques
|
||||
@staticmethod
|
||||
def get_technique_values():
|
||||
"""
|
||||
Parses ATT&CK config into a dict of techniques and corresponding values.
|
||||
:return: Dictionary of techniques. Format: {"T1110": True, "T1075": False, ...}
|
||||
"""
|
||||
attack_config = AttackConfig.get_config()
|
||||
techniques = {}
|
||||
for type_name, attack_type in attack_config['properties'].items():
|
||||
for key, technique in attack_type['properties'].items():
|
||||
techniques[key] = technique['value']
|
||||
return techniques
|
||||
|
|
|
@ -9,11 +9,16 @@ __author__ = "VakarisZ"
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def set_results(technique, data):
|
||||
"""
|
||||
Adds ATT&CK technique results(telemetry) to the database
|
||||
:param technique: technique ID string e.g. T1110
|
||||
:param data: Data, relevant to the technique
|
||||
"""
|
||||
data.update({'technique': technique})
|
||||
mongo.db.attack_results.insert(data)
|
||||
class AttackTelemService(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def set_results(technique, data):
|
||||
"""
|
||||
Adds ATT&CK technique results(telemetry) to the database
|
||||
:param technique: technique ID string e.g. T1110
|
||||
:param data: Data, relevant to the technique
|
||||
"""
|
||||
data.update({'technique': technique})
|
||||
mongo.db.attack_results.insert(data)
|
||||
|
|
|
@ -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')
|
|
@ -14,7 +14,6 @@ import ReportPage from 'components/pages/ReportPage';
|
|||
import LicensePage from 'components/pages/LicensePage';
|
||||
import AuthComponent from 'components/AuthComponent';
|
||||
import LoginPageComponent from 'components/pages/LoginPage';
|
||||
import AttckPage from 'components/pages/AttackPage'
|
||||
|
||||
import 'normalize.css/normalize.css';
|
||||
import 'react-data-components/css/table-twbs.css';
|
||||
|
@ -162,7 +161,6 @@ class AppComponent extends AuthComponent {
|
|||
|
||||
<hr/>
|
||||
<ul>
|
||||
<li><NavLink to="/attack">ATT&CK Configuration</NavLink></li>
|
||||
<li><NavLink to="/configure">Configuration</NavLink></li>
|
||||
<li><NavLink to="/infection/telemetry">Log</NavLink></li>
|
||||
</ul>
|
||||
|
@ -188,7 +186,6 @@ class AppComponent extends AuthComponent {
|
|||
{this.renderRoute('/start-over', <StartOverPage onStatusChange={this.updateStatus}/>)}
|
||||
{this.renderRoute('/report', <ReportPage onStatusChange={this.updateStatus}/>)}
|
||||
{this.renderRoute('/license', <LicensePage onStatusChange={this.updateStatus}/>)}
|
||||
{this.renderRoute('/attack', <AttckPage onStatusChange={this.updateStatus}/>)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Grid>
|
||||
|
|
|
@ -1,52 +1,52 @@
|
|||
import React from 'react';
|
||||
import Checkbox from '../ui-components/checkbox'
|
||||
import Checkbox from '../ui-components/Checkbox'
|
||||
import Tooltip from 'react-tooltip-lite'
|
||||
import AuthComponent from '../AuthComponent';
|
||||
import ReactTable from "react-table";
|
||||
import 'filepond/dist/filepond.min.css';
|
||||
import '../../styles/Tooltip.scss';
|
||||
|
||||
|
||||
// Finds which attack type has most techniques and returns that number
|
||||
let findMaxTechniques = function (data){
|
||||
let maxLen = 0;
|
||||
data.forEach(function(techType) {
|
||||
if (Object.keys(techType.properties).length > maxLen){
|
||||
maxLen = Object.keys(techType.properties).length
|
||||
}
|
||||
});
|
||||
return maxLen
|
||||
};
|
||||
|
||||
// Parses ATT&CK config schema into data suitable for react-table (ATT&CK matrix)
|
||||
let parseTechniques = function (data, maxLen) {
|
||||
let techniques = [];
|
||||
// Create rows with attack techniques
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
let row = {};
|
||||
data.forEach(function(techType){
|
||||
let rowColumn = {};
|
||||
rowColumn.techName = techType.title;
|
||||
|
||||
if (i <= Object.keys(techType.properties).length) {
|
||||
rowColumn.technique = Object.values(techType.properties)[i];
|
||||
if (rowColumn.technique){
|
||||
rowColumn.technique.name = Object.keys(techType.properties)[i]
|
||||
}
|
||||
} else {
|
||||
rowColumn.technique = null
|
||||
}
|
||||
row[rowColumn.techName] = rowColumn
|
||||
});
|
||||
techniques.push(row)
|
||||
}
|
||||
return techniques;
|
||||
};
|
||||
import {Col} from "react-bootstrap";
|
||||
|
||||
class MatrixComponent extends AuthComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = this.getStateFromConfig(this.props.configuration, 'none');
|
||||
this.state = {lastAction: 'none'}
|
||||
};
|
||||
|
||||
// Finds which attack type has most techniques and returns that number
|
||||
static findMaxTechniques(data){
|
||||
let maxLen = 0;
|
||||
data.forEach(function(techType) {
|
||||
if (Object.keys(techType.properties).length > maxLen){
|
||||
maxLen = Object.keys(techType.properties).length
|
||||
}
|
||||
});
|
||||
return maxLen
|
||||
};
|
||||
|
||||
// Parses ATT&CK config schema into data suitable for react-table (ATT&CK matrix)
|
||||
static parseTechniques (data, maxLen) {
|
||||
let techniques = [];
|
||||
// Create rows with attack techniques
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
let row = {};
|
||||
data.forEach(function(techType){
|
||||
let rowColumn = {};
|
||||
rowColumn.techName = techType.title;
|
||||
|
||||
if (i <= Object.keys(techType.properties).length) {
|
||||
rowColumn.technique = Object.values(techType.properties)[i];
|
||||
if (rowColumn.technique){
|
||||
rowColumn.technique.name = Object.keys(techType.properties)[i]
|
||||
}
|
||||
} else {
|
||||
rowColumn.technique = null
|
||||
}
|
||||
row[rowColumn.techName] = rowColumn
|
||||
});
|
||||
techniques.push(row)
|
||||
}
|
||||
return techniques;
|
||||
};
|
||||
|
||||
getColumns(matrixData) {
|
||||
|
@ -68,36 +68,13 @@ class MatrixComponent extends AuthComponent {
|
|||
<Checkbox checked={technique.value}
|
||||
necessary={technique.necessary}
|
||||
name={technique.name}
|
||||
changeHandler={this.handleTechniqueChange}>
|
||||
changeHandler={this.props.change}>
|
||||
{technique.title}
|
||||
</Checkbox>
|
||||
</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 = () => {
|
||||
this.authFetch('/api/attack',
|
||||
{
|
||||
|
@ -111,82 +88,38 @@ class MatrixComponent extends AuthComponent {
|
|||
});
|
||||
};
|
||||
|
||||
// Updates state based on values in config supplied.
|
||||
updateStateFromConfig = (config, lastAction = '') => {
|
||||
this.setState(this.getStateFromConfig(config, lastAction));
|
||||
};
|
||||
|
||||
getStateFromConfig = (config, lastAction) => {
|
||||
getTableData = (config) => {
|
||||
let configCopy = JSON.parse(JSON.stringify(config));
|
||||
let maxTechniques = findMaxTechniques(Object.values(configCopy));
|
||||
let matrixTableData = parseTechniques(Object.values(configCopy), maxTechniques);
|
||||
let maxTechniques = MatrixComponent.findMaxTechniques(Object.values(configCopy));
|
||||
let matrixTableData = MatrixComponent.parseTechniques(Object.values(configCopy), maxTechniques);
|
||||
let columns = this.getColumns(matrixTableData);
|
||||
return {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
return {'columns': columns, 'matrixTableData': matrixTableData, 'maxTechniques': maxTechniques}
|
||||
};
|
||||
|
||||
render() {
|
||||
let tableData = this.getTableData(this.props.configuration);
|
||||
return (
|
||||
<div className={"attack-matrix"}>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<ReactTable
|
||||
columns={this.state.columns}
|
||||
data={this.state.matrixTableData}
|
||||
showPagination={false}
|
||||
defaultPageSize={this.state.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>
|
||||
: ''}
|
||||
{ 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 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"}>
|
||||
<ReactTable columns={tableData['columns']}
|
||||
data={tableData['matrixTableData']}
|
||||
showPagination={false}
|
||||
defaultPageSize={tableData['maxTechniques']} />
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -1,19 +1,26 @@
|
|||
import React from 'react';
|
||||
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 AuthComponent from '../AuthComponent';
|
||||
import { FilePond } from 'react-filepond';
|
||||
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 {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.PBAwindowsPond = null;
|
||||
this.PBAlinuxPond = null;
|
||||
this.currentSection = 'basic';
|
||||
this.currentSection = 'attack';
|
||||
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 = {
|
||||
behaviour: {
|
||||
custom_PBA_linux_cmd: {
|
||||
|
@ -44,37 +51,94 @@ class ConfigurePageComponent extends AuthComponent {
|
|||
this.state = {
|
||||
schema: {},
|
||||
configuration: {},
|
||||
attackConfig: {},
|
||||
lastAction: 'none',
|
||||
sections: [],
|
||||
selectedSection: 'basic',
|
||||
selectedSection: 'attack',
|
||||
allMonkeysAreDead: true,
|
||||
PBAwinFile: [],
|
||||
PBAlinuxFile: []
|
||||
PBAlinuxFile: [],
|
||||
showAttackAlert: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.authFetch('/api/configuration/island')
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
setInitialConfig(config) {
|
||||
this.initialConfig = JSON.parse(JSON.stringify(config));
|
||||
}
|
||||
|
||||
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 attackConfig = data[1];
|
||||
let monkeyConfig = data[0];
|
||||
this.setInitialConfig(monkeyConfig.configuration);
|
||||
this.setInitialAttackConfig(attackConfig.configuration);
|
||||
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({
|
||||
schema: res.schema,
|
||||
configuration: res.configuration,
|
||||
schema: monkeyConfig.schema,
|
||||
configuration: monkeyConfig.configuration,
|
||||
attackConfig: attackConfig.configuration,
|
||||
sections: sections,
|
||||
selectedSection: 'basic'
|
||||
selectedSection: 'attack'
|
||||
})
|
||||
});
|
||||
this.updateMonkeysRunning();
|
||||
}
|
||||
};
|
||||
|
||||
onSubmit = ({formData}) => {
|
||||
this.currentFormData = formData;
|
||||
updateConfig = () => {
|
||||
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.authFetch('/api/configuration/island',
|
||||
this.authFetch(CONFIG_URL,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
|
@ -94,11 +158,33 @@ class ConfigurePageComponent extends AuthComponent {
|
|||
schema: res.schema,
|
||||
configuration: res.configuration
|
||||
});
|
||||
this.setInitialConfig(res.configuration);
|
||||
this.props.onStatusChange();
|
||||
}).catch(error => {
|
||||
console.log('bad 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}) => {
|
||||
|
@ -111,11 +197,49 @@ class ConfigurePageComponent extends AuthComponent {
|
|||
newConfig[this.currentSection] = 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) => {
|
||||
this.updateConfigSection();
|
||||
if ((key === 'attack' && this.userChangedConfig()) ||
|
||||
(this.currentSection === 'attack' && this.userChangedMatrix())){
|
||||
this.setState({showAttackAlert: true});
|
||||
return
|
||||
}
|
||||
this.currentSection = key;
|
||||
this.setState({
|
||||
selectedSection: key
|
||||
|
@ -124,7 +248,7 @@ class ConfigurePageComponent extends AuthComponent {
|
|||
|
||||
resetConfig = () => {
|
||||
this.removePBAfiles();
|
||||
this.authFetch('/api/configuration/island',
|
||||
this.authFetch(CONFIG_URL,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
|
@ -137,8 +261,15 @@ class ConfigurePageComponent extends AuthComponent {
|
|||
schema: res.schema,
|
||||
configuration: res.configuration
|
||||
});
|
||||
this.setInitialConfig(res.configuration);
|
||||
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(){
|
||||
|
@ -273,19 +404,46 @@ class ConfigurePageComponent extends AuthComponent {
|
|||
|
||||
render() {
|
||||
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['definitions'] = this.state.schema['definitions'];
|
||||
}
|
||||
let config_content = (<Form schema={displayedSchema}
|
||||
uiSchema={this.uiSchema}
|
||||
formData={this.state.configuration[this.state.selectedSection]}
|
||||
onChange={this.onChange}
|
||||
noValidate={true}>
|
||||
<div>
|
||||
{ this.state.allMonkeysAreDead ?
|
||||
'' :
|
||||
<div className="alert alert-warning">
|
||||
<i className="glyphicon glyphicon-warning-sign" style={{'marginRight': '5px'}}/>
|
||||
Some monkeys are currently running. Note that changing the configuration will only apply to new
|
||||
infections.
|
||||
</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>
|
||||
)}
|
||||
{this.state.sections.map(section => <NavItem key={section.key} eventKey={section.key}>{section.title}</NavItem>)}
|
||||
</Nav>
|
||||
{
|
||||
this.state.selectedSection === 'basic_network' ?
|
||||
|
@ -296,33 +454,15 @@ class ConfigurePageComponent extends AuthComponent {
|
|||
</div>
|
||||
: <div />
|
||||
}
|
||||
{ this.state.selectedSection ?
|
||||
<Form schema={displayedSchema}
|
||||
uiSchema={this.uiSchema}
|
||||
formData={this.state.configuration[this.state.selectedSection]}
|
||||
onSubmit={this.onSubmit}
|
||||
onChange={this.onChange}
|
||||
noValidate={true}>
|
||||
<div>
|
||||
{ this.state.allMonkeysAreDead ?
|
||||
'' :
|
||||
<div className="alert alert-warning">
|
||||
<i className="glyphicon glyphicon-warning-sign" style={{'marginRight': '5px'}}/>
|
||||
Some monkeys are currently running. Note that changing the configuration will only apply to new
|
||||
infections.
|
||||
</div>
|
||||
}
|
||||
<div className="text-center">
|
||||
<button type="submit" className="btn btn-success btn-lg" style={{margin: '5px'}}>
|
||||
Submit
|
||||
</button>
|
||||
<button type="button" onClick={this.resetConfig} className="btn btn-danger btn-lg" style={{margin: '5px'}}>
|
||||
Reset to defaults
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
: ''}
|
||||
{ content }
|
||||
<div className="text-center">
|
||||
<button type="submit" onClick={this.onSubmit} className="btn btn-success btn-lg" style={{margin: '5px'}}>
|
||||
Submit
|
||||
</button>
|
||||
<button type="button" onClick={this.resetConfig} className="btn btn-danger btn-lg" style={{margin: '5px'}}>
|
||||
Reset to defaults
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<button onClick={() => document.getElementById('uploadInputInternal').click()}
|
||||
className="btn btn-info btn-lg" style={{margin: '5px'}}>
|
||||
|
|
|
@ -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) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -17,20 +23,21 @@ class CheckboxComponent extends React.PureComponent {
|
|||
isAnimating: false
|
||||
};
|
||||
this.toggleChecked = this.toggleChecked.bind(this);
|
||||
this.ping = this.ping.bind(this);
|
||||
this.stopAnimation = this.stopAnimation.bind(this);
|
||||
this.composeStateClasses = this.composeStateClasses.bind(this);
|
||||
}
|
||||
|
||||
//Toggles component.
|
||||
toggleChecked() {
|
||||
if (this.state.isAnimating) return false;
|
||||
if (this.state.isAnimating) {return false;}
|
||||
this.setState({
|
||||
checked: !this.state.checked,
|
||||
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
|
||||
ping() {
|
||||
// Stops ping animation on checkbox after click
|
||||
stopAnimation() {
|
||||
this.setState({ isAnimating: false })
|
||||
}
|
||||
|
||||
|
@ -57,7 +64,7 @@ class CheckboxComponent extends React.PureComponent {
|
|||
type="checkbox" value={this.state.checked}
|
||||
name={this.props.name}/>
|
||||
<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>
|
||||
)
|
||||
}
|
|
@ -186,6 +186,10 @@ body {
|
|||
.nav-tabs > li > a {
|
||||
height: 63px
|
||||
}
|
||||
|
||||
.nav > li > a:focus {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
/*
|
||||
* Run Monkey Page
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue