diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 0d44cb973..b1d761a3f 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -205,6 +205,7 @@ class Configuration(object): # exploiters config ########################### + should_exploit = True skip_exploit_if_file_exist = False ms08_067_exploit_attempts = 5 diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 7ad23fa7b..b78426262 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -1,4 +1,5 @@ { + "should_exploit": true, "command_servers": [ "192.0.2.0:5000" ], diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 6be73d41b..aed2f7760 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -17,6 +17,8 @@ from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton from infection_monkey.windows_upgrader import WindowsUpgrader 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' @@ -174,14 +176,15 @@ class InfectionMonkey(object): machine.set_default_server(self._default_server) # Order exploits according to their type - self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value) - host_exploited = False - for exploiter in [exploiter(machine) for exploiter in self._exploiters]: - if self.try_exploiting(machine, exploiter): - host_exploited = True - break - if not host_exploited: - self._fail_exploitation_machines.add(machine) + if WormConfiguration.should_exploit: + self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value) + host_exploited = False + for exploiter in [exploiter(machine) for exploiter in self._exploiters]: + if self.try_exploiting(machine, exploiter): + host_exploited = True + break + if not host_exploited: + self._fail_exploitation_machines.add(machine) if not self._keep_running: break diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 27b1fdc06..de7b00853 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -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.attack_telem import AttackTelem @@ -101,7 +101,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('/', 'serve_static_file', serve_static_file) diff --git a/monkey/monkey_island/cc/resources/attack/attack_config.py b/monkey/monkey_island/cc/resources/attack/attack_config.py index 482c3b5fe..da7651f24 100644 --- a/monkey/monkey_island/cc/resources/attack/attack_config.py +++ b/monkey/monkey_island/cc/resources/attack/attack_config.py @@ -3,7 +3,7 @@ import json from flask import jsonify, request 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" @@ -11,7 +11,7 @@ __author__ = "VakarisZ" class AttackConfiguration(flask_restful.Resource): @jwt_required() def get(self): - return jsonify(configuration=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: - reset_config() - return jsonify(configuration=get_config()['properties']) + AttackConfig.reset_config() + return jsonify(configuration=AttackConfig.get_config()['properties']) else: - update_config({'properties': json.loads(request.data)}) - apply_to_monkey_config() + AttackConfig.update_config({'properties': json.loads(request.data)}) + AttackConfig.apply_to_monkey_config() return {} diff --git a/monkey/monkey_island/cc/resources/attack/attack_telem.py b/monkey/monkey_island/cc/resources/attack/attack_telem.py index bef0a8585..8c30bb13c 100644 --- a/monkey/monkey_island/cc/resources/attack/attack_telem.py +++ b/monkey/monkey_island/cc/resources/attack/attack_telem.py @@ -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 {} diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py index 4bc43c601..902721346 100644 --- a/monkey/monkey_island/cc/resources/root.py +++ b/monkey/monkey_island/cc/resources/root.py @@ -6,13 +6,11 @@ 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 cc.services.attack.attack_report import AttackReportService 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' @@ -28,7 +26,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": @@ -42,17 +40,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(): diff --git a/monkey/monkey_island/cc/services/attack/attack_config.py b/monkey/monkey_island/cc/services/attack/attack_config.py index 4957fac98..f1a1d932b 100644 --- a/monkey/monkey_island/cc/services/attack/attack_config.py +++ b/monkey/monkey_island/cc/services/attack/attack_config.py @@ -9,168 +9,174 @@ __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_technique(technique_id): - """ - Gets technique by id - :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(): - for key, technique in attack_type['properties'].items(): - if key == technique_id: - return technique - return False + @staticmethod + def get_technique(technique_id): + """ + Gets technique by id + :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(): + for key, technique in attack_type['properties'].items(): + if key == technique_id: + return technique + return False + @staticmethod + def get_config_schema(): + return SCHEMA -def get_config_schema(): - return SCHEMA + @staticmethod + 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(): - update_config(SCHEMA) + @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) + @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): - mongo.db.attack.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True) - return True + @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) + @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(): - """ - 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 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) + @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): - """ - 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 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 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 + @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 diff --git a/monkey/monkey_island/cc/services/attack/attack_telem.py b/monkey/monkey_island/cc/services/attack/attack_telem.py index 8d7e08960..b402bda56 100644 --- a/monkey/monkey_island/cc/services/attack/attack_telem.py +++ b/monkey/monkey_island/cc/services/attack/attack_telem.py @@ -3,23 +3,28 @@ File that contains ATT&CK telemetry storing/retrieving logic """ import logging from monkey_island.cc.database import mongo -from time import time __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) - mongo.db.attack_results.update({'name': 'latest'}, {'name': 'latest', 'time': data['time']}, upsert=True) +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) + mongo.db.attack_results.update({'name': 'latest'}, {'name': 'latest', 'time': data['time']}, upsert=True) -def get_latest_telem(): - return mongo.db.attack_results.find_one({'name': 'latest'}) + @staticmethod + def get_latest_telem(): + return mongo.db.attack_results.find_one({'name': 'latest'}) diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index 8c7e6c154..73476e645 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -14,7 +14,7 @@ SCHEMA = { "SmbExploiter" ], "title": "SMB Exploiter", - "attack_techniques": ["T1110", "T1210", "T1075"] + "attack_techniques": ["T1110", "T1075"] }, { "type": "string", @@ -54,55 +54,49 @@ SCHEMA = { "SSHExploiter" ], "title": "SSH Exploiter", - "attack_techniques": ["T1110", "T1210"] + "attack_techniques": ["T1110"] }, { "type": "string", "enum": [ "ShellShockExploiter" ], - "title": "ShellShock Exploiter", - "attack_techniques": ["T1210"] + "title": "ShellShock Exploiter" }, { "type": "string", "enum": [ "SambaCryExploiter" ], - "title": "SambaCry Exploiter", - "attack_techniques": ["T1210"] + "title": "SambaCry Exploiter" }, { "type": "string", "enum": [ "ElasticGroovyExploiter" ], - "title": "ElasticGroovy Exploiter", - "attack_techniques": ["T1210"] + "title": "ElasticGroovy Exploiter" }, { "type": "string", "enum": [ "Struts2Exploiter" ], - "title": "Struts2 Exploiter", - "attack_techniques": ["T1210"] + "title": "Struts2 Exploiter" }, { "type": "string", "enum": [ "WebLogicExploiter" ], - "title": "Oracle Web Logic Exploiter", - "attack_techniques": ["T1210"] + "title": "Oracle Web Logic Exploiter" }, { "type": "string", "enum": [ "HadoopExploiter" ], - "title": "Hadoop/Yarn Exploiter", - "attack_techniques": ["T1210"] + "title": "Hadoop/Yarn Exploiter" } ] }, @@ -184,9 +178,22 @@ SCHEMA = { }, "properties": { "basic": { - "title": "Basic - Credentials", + "title": "Basic - Exploits", "type": "object", "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": { "title": "Credentials", "type": "object", @@ -399,7 +406,7 @@ SCHEMA = { "title": "Harvest Azure Credentials", "type": "boolean", "default": True, - "attack_techniques": ["T1110", "T1078"], + "attack_techniques": ["T1003", "T1078"], "description": "Determine if the Monkey should try to harvest password credentials from Azure VMs" }, @@ -413,7 +420,7 @@ SCHEMA = { "title": "Should use Mimikatz", "type": "boolean", "default": True, - "attack_techniques": ["T1110", "T1078"], + "attack_techniques": ["T1003", "T1078"], "description": "Determines whether to use Mimikatz" }, } diff --git a/monkey/monkey_island/cc/services/database.py b/monkey/monkey_island/cc/services/database.py new file mode 100644 index 000000000..ead2b2058 --- /dev/null +++ b/monkey/monkey_island/cc/services/database.py @@ -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') diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js index 7af05f6cb..80025dd68 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.js +++ b/monkey/monkey_island/cc/ui/src/components/Main.js @@ -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 AttackReportPage from 'components/pages/AttackReportPage'; import 'normalize.css/normalize.css'; @@ -173,7 +172,6 @@ class AppComponent extends AuthComponent {
@@ -200,7 +198,6 @@ class AppComponent extends AuthComponent { {this.renderRoute('/report', )} {this.renderRoute('/attack_report', )} {this.renderRoute('/license', )} - {this.renderRoute('/attack', )} diff --git a/monkey/monkey_island/cc/ui/src/components/attack/MatrixComponent.js b/monkey/monkey_island/cc/ui/src/components/attack/MatrixComponent.js index 1803a7cd9..1df65a50e 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/MatrixComponent.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/MatrixComponent.js @@ -1,58 +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); - // Copy ATT&CK configuration and parse it for ATT&CK matrix table - let configCopy = JSON.parse(JSON.stringify(this.props.configuration)); - this.state = {lastAction: 'none', - configData: this.props.configuration, - maxTechniques: findMaxTechniques(Object.values(configCopy))}; - this.state.matrixTableData = parseTechniques(Object.values(configCopy), this.state.maxTechniques); - this.state.columns = this.getColumns(this.state.matrixTableData) + 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,42 +62,19 @@ class MatrixComponent extends AuthComponent { renderTechnique(technique) { if (technique == null){ - return (
) + return (
) } else { return ( + changeHandler={this.props.change}> {technique.title} ) } }; - 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', { @@ -117,78 +88,38 @@ class MatrixComponent extends AuthComponent { }); }; - // Updates state based on values in config supplied. - updateStateFromConfig = (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); - this.setState({ - 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 ( -
-
- -
- { this.state.lastAction === 'reset' ? -
- - Matrix reset to default. -
- : ''} - { this.state.lastAction === 'saved' ? -
- - Matrix applied to configuration. -
- : ''} - { this.state.lastAction === 'invalid_configuration' ? -
- - An invalid matrix configuration supplied, check selected fields. -
- : ''} -
-
- - -
- +
+ +
+ +
); } } diff --git a/monkey/monkey_island/cc/ui/src/components/pages/AttackPage.js b/monkey/monkey_island/cc/ui/src/components/pages/AttackPage.js deleted file mode 100644 index d0db2d1c4..000000000 --- a/monkey/monkey_island/cc/ui/src/components/pages/AttackPage.js +++ /dev/null @@ -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 = (

Fetching configuration...

); - } else { - content = ( -
- - -
); - } - return
{content}
; - } -} - -export default AttackComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index bb369fa73..6c3257670 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -1,80 +1,152 @@ 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.uiSchema = { - behaviour: { - custom_PBA_linux_cmd: { - "ui:widget": "textarea", - "ui:emptyValue": "" - }, - PBA_linux_file: { - "ui:widget": this.PBAlinux - }, - custom_PBA_windows_cmd: { - "ui:widget": "textarea", - "ui:emptyValue": "" - }, - PBA_windows_file: { - "ui:widget": this.PBAwindows - }, - PBA_linux_filename: { - classNames: "linux-pba-file-info", - "ui:emptyValue": "" - }, - PBA_windows_filename: { - classNames: "windows-pba-file-info", - "ui:emptyValue": "" + this.initialConfig = {}; + this.initialAttackConfig = {}; + this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; + this.uiSchemas = { + basic: {"ui:order": ["general", "credentials"]}, + basic_network: {}, + monkey: { + behaviour: { + custom_PBA_linux_cmd: { + "ui:widget": "textarea", + "ui:emptyValue": "" + }, + PBA_linux_file: { + "ui:widget": this.PBAlinux + }, + custom_PBA_windows_cmd: { + "ui:widget": "textarea", + "ui:emptyValue": "" + }, + PBA_windows_file: { + "ui:widget": this.PBAwindows + }, + PBA_linux_filename: { + 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 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 +166,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 +205,49 @@ class ConfigurePageComponent extends AuthComponent { newConfig[this.currentSection] = this.currentFormData; this.currentFormData = {}; } - this.setState({configuration: newConfig}); + this.setState({configuration: newConfig, lastAction: 'none'}); }; + renderAttackAlertModal = () => { + return ( {this.setState({showAttackAlert: false})}}> + +

Warning

+

+ You have unsubmitted changes. Submit them before proceeding. +

+
+ +
+
+
) + }; + + 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 +256,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 +269,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 +412,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 = (
+
+ { this.state.allMonkeysAreDead ? + '' : +
+ + Some monkeys are currently running. Note that changing the configuration will only apply to new + infections. +
+ } +
+ ); + let attack_content = (); + 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 ( + {this.renderAttackAlertModal()}

Monkey Configuration

{ this.state.selectedSection === 'basic_network' ? @@ -296,33 +462,15 @@ class ConfigurePageComponent extends AuthComponent {
:
} - { this.state.selectedSection ? -
-
- { this.state.allMonkeysAreDead ? - '' : -
- - Some monkeys are currently running. Note that changing the configuration will only apply to new - infections. -
- } -
- - -
-
- - : ''} + { content } +
+ + +
) } } -export default Checkbox; +export default CheckboxComponent; diff --git a/monkey/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css index ade81039e..bc2d5d320 100644 --- a/monkey/monkey_island/cc/ui/src/styles/App.css +++ b/monkey/monkey_island/cc/ui/src/styles/App.css @@ -186,6 +186,10 @@ body { .nav-tabs > li > a { height: 63px } + +.nav > li > a:focus { + background-color: transparent !important; +} /* * Run Monkey Page */