Merge branch 'attack_configuration' into attack_report

# Conflicts:
#	monkey/infection_monkey/monkey.py
#	monkey/monkey_island/cc/services/attack/attack_config.py
#	monkey/monkey_island/cc/services/attack/attack_telem.py
#	monkey/monkey_island/cc/ui/src/components/Main.js
This commit is contained in:
VakarisZ 2019-05-03 11:23:25 +03:00
commit 44077e6bfe
17 changed files with 559 additions and 503 deletions

View File

@ -205,6 +205,7 @@ class Configuration(object):
# exploiters config # exploiters config
########################### ###########################
should_exploit = True
skip_exploit_if_file_exist = False skip_exploit_if_file_exist = False
ms08_067_exploit_attempts = 5 ms08_067_exploit_attempts = 5

View File

@ -1,4 +1,5 @@
{ {
"should_exploit": true,
"command_servers": [ "command_servers": [
"192.0.2.0:5000" "192.0.2.0:5000"
], ],

View File

@ -17,6 +17,8 @@ from infection_monkey.system_info import SystemInfoCollector
from infection_monkey.system_singleton import SystemSingleton from infection_monkey.system_singleton import SystemSingleton
from infection_monkey.windows_upgrader import WindowsUpgrader from infection_monkey.windows_upgrader import WindowsUpgrader
from infection_monkey.post_breach.post_breach_handler import PostBreach from infection_monkey.post_breach.post_breach_handler import PostBreach
from common.utils.attack_utils import ScanStatus
from infection_monkey.transport.attack_telems.victim_host_telem import VictimHostTelem
__author__ = 'itamar' __author__ = 'itamar'
@ -174,14 +176,15 @@ class InfectionMonkey(object):
machine.set_default_server(self._default_server) machine.set_default_server(self._default_server)
# Order exploits according to their type # Order exploits according to their type
self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value) if WormConfiguration.should_exploit:
host_exploited = False self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value)
for exploiter in [exploiter(machine) for exploiter in self._exploiters]: host_exploited = False
if self.try_exploiting(machine, exploiter): for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
host_exploited = True if self.try_exploiting(machine, exploiter):
break host_exploited = True
if not host_exploited: break
self._fail_exploitation_machines.add(machine) if not host_exploited:
self._fail_exploitation_machines.add(machine)
if not self._keep_running: if not self._keep_running:
break break

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.attack_telem import AttackTelem from monkey_island.cc.resources.attack.attack_telem import AttackTelem
@ -101,7 +101,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
from monkey_island.cc.services.attack.attack_config import * 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=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:
reset_config() AttackConfig.reset_config()
return jsonify(configuration=get_config()['properties']) return jsonify(configuration=AttackConfig.get_config()['properties'])
else: else:
update_config({'properties': json.loads(request.data)}) AttackConfig.update_config({'properties': json.loads(request.data)})
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,13 +6,11 @@ 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 cc.services.attack.attack_report import AttackReportService from cc.services.attack.attack_report import AttackReportService
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'
@ -28,7 +26,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":
@ -42,17 +40,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,168 +9,174 @@ __author__ = "VakarisZ"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_config(): class AttackConfig(object):
config = mongo.db.attack.find_one({'name': 'newconfig'}) or reset_config() def __init__(self):
return config pass
@staticmethod
def get_config():
config = mongo.db.attack.find_one({'name': 'newconfig'})
return config
def get_technique(technique_id): @staticmethod
""" def get_technique(technique_id):
Gets technique by id """
:param technique_id: E.g. T1210 Gets technique by id
:return: Technique object or false if technique is not found :param technique_id: E.g. T1210
""" :return: Technique object or false if technique is not found
attack_config = get_config() """
for key, attack_type in attack_config['properties'].items(): attack_config = get_config()
for key, technique in attack_type['properties'].items(): for key, attack_type in attack_config['properties'].items():
if key == technique_id: for key, technique in attack_type['properties'].items():
return technique if key == technique_id:
return False return technique
return False
@staticmethod
def get_config_schema():
return SCHEMA
def get_config_schema(): @staticmethod
return SCHEMA def reset_config():
AttackConfig.update_config(SCHEMA)
@staticmethod
def update_config(config_json):
mongo.db.attack.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True)
return True
def reset_config(): @staticmethod
update_config(SCHEMA) 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)
@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)
def update_config(config_json): @staticmethod
mongo.db.attack.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True) def set_booleans(attack_techniques, monkey_config, monkey_schema):
return True """
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)
@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]
def apply_to_monkey_config(): @staticmethod
""" def set_bool_conf_val(path, val, monkey_config):
Applies ATT&CK matrix to the monkey configuration """
:return: 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']
attack_techniques = get_technique_values() :param val: Boolean
monkey_config = ConfigService.get_config(False, True, True) :param monkey_config: Monkey's configuration
monkey_schema = ConfigService.get_config_schema() """
set_arrays(attack_techniques, monkey_config, monkey_schema) util.set(monkey_config, '/'.join(path), val)
set_booleans(attack_techniques, monkey_config, monkey_schema)
ConfigService.update_config(monkey_config, True)
@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
def set_arrays(attack_techniques, monkey_config, monkey_schema): @staticmethod
""" def r_alter_array(config_value, array_name, field, remove=True):
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, ...} Recursively searches config (DFS) for array and removes/adds a field.
:param monkey_config: Monkey island's configuration :param config_value: Some object/value from config
:param monkey_schema: Monkey configuration schema :param array_name: Name of array this method should search
""" :param field: Field in array that this method should add/remove
for key, definition in monkey_schema['definitions'].items(): :param remove: Removes field from array if true, adds it if false
for array_field in definition['anyOf']: """
# Check if current array field has attack_techniques assigned to it if isinstance(config_value, dict):
if 'attack_techniques' not in array_field: if array_name in config_value and isinstance(config_value[array_name], list):
continue if remove and field in config_value[array_name]:
try: config_value[array_name].remove(field)
should_remove = not should_enable_field(array_field['attack_techniques'], attack_techniques) elif not remove and field not in config_value[array_name]:
except KeyError: config_value[array_name].append(field)
# Monkey schema field contains not yet implemented technique else:
continue for prop in config_value.items():
# If exploiter's attack technique is disabled, disable the exploiter/scanner/PBA AttackConfig.r_alter_array(prop[1], array_name, field, remove)
r_alter_array(monkey_config, key, array_field['enum'][0], remove=should_remove)
@staticmethod
def set_booleans(attack_techniques, monkey_config, monkey_schema): def get_technique_values():
""" """
Sets boolean type fields, like "should use mimikatz?" in monkey's config according to ATT&CK matrix Parses ATT&CK config into a dict of techniques and corresponding values.
:param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...} :return: Dictionary of techniques. Format: {"T1110": True, "T1075": False, ...}
:param monkey_config: Monkey island's configuration """
:param monkey_schema: Monkey configuration schema attack_config = AttackConfig.get_config()
""" techniques = {}
for key, value in monkey_schema['properties'].items(): for type_name, attack_type in attack_config['properties'].items():
r_set_booleans([key], value, attack_techniques, monkey_config) for key, technique in attack_type['properties'].items():
techniques[key] = technique['value']
return techniques
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 dic 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

View File

@ -3,23 +3,28 @@ File that contains ATT&CK telemetry storing/retrieving logic
""" """
import logging import logging
from monkey_island.cc.database import mongo from monkey_island.cc.database import mongo
from time import time
__author__ = "VakarisZ" __author__ = "VakarisZ"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def set_results(technique, data): class AttackTelemService(object):
""" def __init__(self):
Adds ATT&CK technique results(telemetry) to the database pass
:param technique: technique ID string e.g. T1110
:param data: Data, relevant to the technique @staticmethod
""" def set_results(technique, data):
data.update({'technique': technique}) """
mongo.db.attack_results.insert(data) Adds ATT&CK technique results(telemetry) to the database
mongo.db.attack_results.update({'name': 'latest'}, {'name': 'latest', 'time': data['time']}, upsert=True) :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)
mongo.db.attack_results.update({'name': 'latest'}, {'name': 'latest', 'time': data['time']}, upsert=True)
def get_latest_telem(): @staticmethod
return mongo.db.attack_results.find_one({'name': 'latest'}) def get_latest_telem():
return mongo.db.attack_results.find_one({'name': 'latest'})

View File

@ -14,7 +14,7 @@ SCHEMA = {
"SmbExploiter" "SmbExploiter"
], ],
"title": "SMB Exploiter", "title": "SMB Exploiter",
"attack_techniques": ["T1110", "T1210", "T1075"] "attack_techniques": ["T1110", "T1075"]
}, },
{ {
"type": "string", "type": "string",
@ -54,55 +54,49 @@ SCHEMA = {
"SSHExploiter" "SSHExploiter"
], ],
"title": "SSH Exploiter", "title": "SSH Exploiter",
"attack_techniques": ["T1110", "T1210"] "attack_techniques": ["T1110"]
}, },
{ {
"type": "string", "type": "string",
"enum": [ "enum": [
"ShellShockExploiter" "ShellShockExploiter"
], ],
"title": "ShellShock Exploiter", "title": "ShellShock Exploiter"
"attack_techniques": ["T1210"]
}, },
{ {
"type": "string", "type": "string",
"enum": [ "enum": [
"SambaCryExploiter" "SambaCryExploiter"
], ],
"title": "SambaCry Exploiter", "title": "SambaCry Exploiter"
"attack_techniques": ["T1210"]
}, },
{ {
"type": "string", "type": "string",
"enum": [ "enum": [
"ElasticGroovyExploiter" "ElasticGroovyExploiter"
], ],
"title": "ElasticGroovy Exploiter", "title": "ElasticGroovy Exploiter"
"attack_techniques": ["T1210"]
}, },
{ {
"type": "string", "type": "string",
"enum": [ "enum": [
"Struts2Exploiter" "Struts2Exploiter"
], ],
"title": "Struts2 Exploiter", "title": "Struts2 Exploiter"
"attack_techniques": ["T1210"]
}, },
{ {
"type": "string", "type": "string",
"enum": [ "enum": [
"WebLogicExploiter" "WebLogicExploiter"
], ],
"title": "Oracle Web Logic Exploiter", "title": "Oracle Web Logic Exploiter"
"attack_techniques": ["T1210"]
}, },
{ {
"type": "string", "type": "string",
"enum": [ "enum": [
"HadoopExploiter" "HadoopExploiter"
], ],
"title": "Hadoop/Yarn Exploiter", "title": "Hadoop/Yarn Exploiter"
"attack_techniques": ["T1210"]
} }
] ]
}, },
@ -184,9 +178,22 @@ SCHEMA = {
}, },
"properties": { "properties": {
"basic": { "basic": {
"title": "Basic - Credentials", "title": "Basic - Exploits",
"type": "object", "type": "object",
"properties": { "properties": {
"general": {
"title": "General",
"type": "object",
"properties": {
"should_exploit": {
"title": "Exploit network machines",
"type": "boolean",
"default": True,
"attack_techniques": ["T1210"],
"description": "Determines if monkey should try to safely exploit machines on the network"
}
}
},
"credentials": { "credentials": {
"title": "Credentials", "title": "Credentials",
"type": "object", "type": "object",
@ -399,7 +406,7 @@ SCHEMA = {
"title": "Harvest Azure Credentials", "title": "Harvest Azure Credentials",
"type": "boolean", "type": "boolean",
"default": True, "default": True,
"attack_techniques": ["T1110", "T1078"], "attack_techniques": ["T1003", "T1078"],
"description": "description":
"Determine if the Monkey should try to harvest password credentials from Azure VMs" "Determine if the Monkey should try to harvest password credentials from Azure VMs"
}, },
@ -413,7 +420,7 @@ SCHEMA = {
"title": "Should use Mimikatz", "title": "Should use Mimikatz",
"type": "boolean", "type": "boolean",
"default": True, "default": True,
"attack_techniques": ["T1110", "T1078"], "attack_techniques": ["T1003", "T1078"],
"description": "Determines whether to use Mimikatz" "description": "Determines whether to use Mimikatz"
}, },
} }

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 AttackReportPage from 'components/pages/AttackReportPage'; import AttackReportPage from 'components/pages/AttackReportPage';
import 'normalize.css/normalize.css'; import 'normalize.css/normalize.css';
@ -173,7 +172,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>
@ -200,7 +198,6 @@ class AppComponent extends AuthComponent {
{this.renderRoute('/report', <ReportPage onStatusChange={this.updateStatus}/>)} {this.renderRoute('/report', <ReportPage onStatusChange={this.updateStatus}/>)}
{this.renderRoute('/attack_report', <AttackReportPage onStatusChange={this.updateStatus}/>)} {this.renderRoute('/attack_report', <AttackReportPage 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,58 +1,52 @@
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";
// 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;
};
class MatrixComponent extends AuthComponent { class MatrixComponent extends AuthComponent {
constructor(props) { constructor(props) {
super(props); super(props);
// Copy ATT&CK configuration and parse it for ATT&CK matrix table this.state = {lastAction: 'none'}
let configCopy = JSON.parse(JSON.stringify(this.props.configuration)); };
this.state = {lastAction: 'none',
configData: this.props.configuration, // Finds which attack type has most techniques and returns that number
maxTechniques: findMaxTechniques(Object.values(configCopy))}; static findMaxTechniques(data){
this.state.matrixTableData = parseTechniques(Object.values(configCopy), this.state.maxTechniques); let maxLen = 0;
this.state.columns = this.getColumns(this.state.matrixTableData) 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) { getColumns(matrixData) {
@ -68,42 +62,19 @@ class MatrixComponent extends AuthComponent {
renderTechnique(technique) { renderTechnique(technique) {
if (technique == null){ if (technique == null){
return (<div></div>) return (<div />)
} else { } else {
return (<Tooltip content={technique.description} direction="down"> return (<Tooltip content={technique.description} direction="down">
<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',
{ {
@ -117,78 +88,38 @@ class MatrixComponent extends AuthComponent {
}); });
}; };
// Updates state based on values in config supplied. getTableData = (config) => {
updateStateFromConfig = (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);
this.setState({ 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 className={"attack-matrix"}> <div>
<form onSubmit={this.onSubmit}> <div id="header" className="row justify-content-between attack-legend">
<ReactTable <Col xs={4}>
columns={this.state.columns} <i className="fa fa-circle-thin icon-unchecked"></i>
data={this.state.matrixTableData} <span> - Dissabled</span>
showPagination={false} </Col>
defaultPageSize={this.state.maxTechniques} /> <Col xs={4}>
<div className={"messages"}> <i className="fa fa-circle icon-checked"></i>
{ this.state.lastAction === 'reset' ? <span> - Enabled</span>
<div className="alert alert-success"> </Col>
<i className="glyphicon glyphicon-ok-sign" style={{'marginRight': '5px'}}/> <Col xs={4}>
Matrix reset to default. <i className="fa fa-circle icon-mandatory"></i>
</div> <span> - Mandatory</span>
: ''} </Col>
{ this.state.lastAction === 'saved' ? </div>
<div className="alert alert-success"> <div className={"attack-matrix"}>
<i className="glyphicon glyphicon-ok-sign" style={{'marginRight': '5px'}}/> <ReactTable columns={tableData['columns']}
Matrix applied to configuration. data={tableData['matrixTableData']}
</div> showPagination={false}
: ''} defaultPageSize={tableData['maxTechniques']} />
{ this.state.lastAction === 'invalid_configuration' ? </div>
<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,80 +1,152 @@
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.uiSchema = { this.initialAttackConfig = {};
behaviour: { this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal'];
custom_PBA_linux_cmd: { this.uiSchemas = {
"ui:widget": "textarea", basic: {"ui:order": ["general", "credentials"]},
"ui:emptyValue": "" basic_network: {},
}, monkey: {
PBA_linux_file: { behaviour: {
"ui:widget": this.PBAlinux custom_PBA_linux_cmd: {
}, "ui:widget": "textarea",
custom_PBA_windows_cmd: { "ui:emptyValue": ""
"ui:widget": "textarea", },
"ui:emptyValue": "" PBA_linux_file: {
}, "ui:widget": this.PBAlinux
PBA_windows_file: { },
"ui:widget": this.PBAwindows custom_PBA_windows_cmd: {
}, "ui:widget": "textarea",
PBA_linux_filename: { "ui:emptyValue": ""
classNames: "linux-pba-file-info", },
"ui:emptyValue": "" PBA_windows_file: {
}, "ui:widget": this.PBAwindows
PBA_windows_filename: { },
classNames: "windows-pba-file-info", PBA_linux_filename: {
"ui:emptyValue": "" classNames: "linux-pba-file-info",
"ui:emptyValue": ""
},
PBA_windows_filename: {
classNames: "windows-pba-file-info",
"ui:emptyValue": ""
}
} }
} },
cnc: {},
network: {},
exploits: {},
internal: {}
}; };
// set schema from server // set schema from server
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 +166,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 +205,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 +256,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 +269,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,19 +412,46 @@ 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'];
} }
let config_content = (<Form schema={displayedSchema}
uiSchema={this.uiSchemas[this.state.selectedSection]}
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 ( return (
<Col xs={12} lg={8}> <Col xs={12} lg={8}>
{this.renderAttackAlertModal()}
<h1 className="page-title">Monkey Configuration</h1> <h1 className="page-title">Monkey Configuration</h1>
<Nav bsStyle="tabs" justified <Nav bsStyle="tabs" justified
activeKey={this.state.selectedSection} onSelect={this.setSelectedSection} activeKey={this.state.selectedSection} onSelect={this.setSelectedSection}
style={{'marginBottom': '2em'}}> style={{'marginBottom': '2em'}}>
{this.state.sections.map(section => {this.state.sections.map(section => <NavItem key={section.key} eventKey={section.key}>{section.title}</NavItem>)}
<NavItem key={section.key} eventKey={section.key}>{section.title}</NavItem>
)}
</Nav> </Nav>
{ {
this.state.selectedSection === 'basic_network' ? this.state.selectedSection === 'basic_network' ?
@ -296,33 +462,15 @@ class ConfigurePageComponent extends AuthComponent {
</div> </div>
: <div /> : <div />
} }
{ this.state.selectedSection ? { content }
<Form schema={displayedSchema} <div className="text-center">
uiSchema={this.uiSchema} <button type="submit" onClick={this.onSubmit} className="btn btn-success btn-lg" style={{margin: '5px'}}>
formData={this.state.configuration[this.state.selectedSection]} Submit
onSubmit={this.onSubmit} </button>
onChange={this.onChange} <button type="button" onClick={this.resetConfig} className="btn btn-danger btn-lg" style={{margin: '5px'}}>
noValidate={true}> Reset to defaults
<div> </button>
{ this.state.allMonkeysAreDead ? </div>
'' :
<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>
: ''}
<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

@ -1,7 +1,7 @@
import '../../styles/Checkbox.scss' import '../../styles/Checkbox.scss'
import React from 'react'; import React from 'react';
class Checkbox extends React.PureComponent { class CheckboxComponent extends React.PureComponent {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.checked !== prevProps.checked) { if (this.props.checked !== prevProps.checked) {
@ -9,6 +9,12 @@ class Checkbox 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 Checkbox 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,10 +64,10 @@ class Checkbox 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>
) )
} }
} }
export default Checkbox; export default CheckboxComponent;

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