diff --git a/monkey/monkey_island/cc/services/config_schema/config_schema.py b/monkey/monkey_island/cc/services/config_schema/config_schema.py index d1cd7a68c..17d7752c0 100644 --- a/monkey/monkey_island/cc/services/config_schema/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema/config_schema.py @@ -11,6 +11,9 @@ from monkey_island.cc.services.config_schema.monkey import MONKEY SCHEMA = { "title": "Monkey", "type": "object", + # Newly added definitions should also be added to + # monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js so that + # users will not accidentally chose unsafe options "definitions": { "exploiter_classes": EXPLOITER_CLASSES, "system_info_collector_classes": SYSTEM_INFO_COLLECTOR_CLASSES, diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js new file mode 100644 index 000000000..d21bf5601 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js @@ -0,0 +1,36 @@ +import React from 'react'; +import {Modal, Button} from 'react-bootstrap'; + +function UnsafeOptionsConfirmationModal(props) { + return ( + + +

+
Warning
+

+

+ Some of the selected options could cause systems to become unstable or malfunction. + Are you sure you want to submit the selected settings? +

+
+ + +
+
+
+ ) +} + +export default UnsafeOptionsConfirmationModal; 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 426e66c0a..2f82ae30c 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -11,6 +11,8 @@ import {faExclamationCircle} from '@fortawesome/free-solid-svg-icons/faExclamati import {formValidationFormats} from '../configuration-components/ValidationFormats'; import transformErrors from '../configuration-components/ValidationErrorMessages'; import InternalConfig from '../configuration-components/InternalConfig'; +import UnsafeOptionsConfirmationModal from '../configuration-components/UnsafeOptionsConfirmationModal.js'; +import isUnsafeOptionSelected from '../utils/SafeOptionValidator.js'; const ATTACK_URL = '/api/attack'; const CONFIG_URL = '/api/configuration/island'; @@ -28,13 +30,15 @@ class ConfigurePageComponent extends AuthComponent { this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'internal']; this.state = { - schema: {}, - configuration: {}, attackConfig: {}, + configuration: {}, + importCandidateConfig: null, lastAction: 'none', + schema: {}, sections: [], selectedSection: 'attack', - showAttackAlert: false + showAttackAlert: false, + showUnsafeOptionsConfirmation: false }; } @@ -74,6 +78,20 @@ class ConfigurePageComponent extends AuthComponent { }); }; + onUnsafeConfirmationCancelClick = () => { + this.setState({showUnsafeOptionsConfirmation: false}); + } + + onUnsafeConfirmationContinueClick = () => { + this.setState({showUnsafeOptionsConfirmation: false}); + + if (this.state.lastAction == 'submit_attempt') { + this.configSubmit(); + } else if (this.state.lastAction == 'import_attempt') { + this.setConfigFromImportCandidate(); + } + } + updateConfig = () => { this.authFetch(CONFIG_URL) .then(res => res.json()) @@ -85,12 +103,16 @@ class ConfigurePageComponent extends AuthComponent { onSubmit = () => { if (this.state.selectedSection === 'attack') { - this.matrixSubmit() + this.matrixSubmit(); } else { - this.configSubmit() + this.attemptConfigSubmit(); } }; + canSafelySubmitConfig(config) { + return !isUnsafeOptionSelected(this.state.schema, config); + } + matrixSubmit = () => { // Submit attack matrix this.authFetch(ATTACK_URL, @@ -116,9 +138,19 @@ class ConfigurePageComponent extends AuthComponent { }); }; - configSubmit = () => { - // Submit monkey configuration + attemptConfigSubmit() { this.updateConfigSection(); + this.setState({lastAction: 'submit_attempt'}, () => { + if (this.canSafelySubmitConfig(this.state.configuration)) { + this.configSubmit(); + } else { + this.setState({showUnsafeOptionsConfirmation: true}); + } + } + ); + } + + configSubmit() { this.sendConfig() .then(res => res.json()) .then(res => { @@ -133,7 +165,7 @@ class ConfigurePageComponent extends AuthComponent { console.log('Bad configuration: ' + error.toString()); this.setState({lastAction: 'invalid_configuration'}); }); - }; + } // Alters attack configuration when user toggles technique attackTechniqueChange = (technique, value, mapped = false) => { @@ -201,6 +233,16 @@ class ConfigurePageComponent extends AuthComponent { ) }; + renderUnsafeOptionsConfirmationModal() { + return ( + + ); + } + userChangedConfig() { if (JSON.stringify(this.state.configuration) === JSON.stringify(this.initialConfig)) { if (Object.keys(this.currentFormData).length === 0 || @@ -276,18 +318,33 @@ class ConfigurePageComponent extends AuthComponent { setConfigOnImport = (event) => { try { - this.setState({ - configuration: JSON.parse(event.target.result), - lastAction: 'import_success' - }, () => { - this.sendConfig(); - this.setInitialConfig(JSON.parse(event.target.result)) - }); - this.currentFormData = {}; + var newConfig = JSON.parse(event.target.result); } catch (SyntaxError) { this.setState({lastAction: 'import_failure'}); + return; } - }; + + this.setState({lastAction: 'import_attempt', importCandidateConfig: newConfig}, + () => { + if (this.canSafelySubmitConfig(newConfig)) { + this.setConfigFromImportCandidate(); + } else { + this.setState({showUnsafeOptionsConfirmation: true}); + } + } + ); + } + + setConfigFromImportCandidate(){ + this.setState({ + configuration: this.state.importCandidateConfig, + lastAction: 'import_success' + }, () => { + this.sendConfig(); + this.setInitialConfig(this.state.importCandidateConfig); + }); + this.currentFormData = {}; + } exportConfig = () => { this.updateConfigSection(); @@ -410,6 +467,7 @@ class ConfigurePageComponent extends AuthComponent { lg={{offset: 3, span: 8}} xl={{offset: 2, span: 8}} className={'main'}> {this.renderAttackAlertModal()} + {this.renderUnsafeOptionsConfirmationModal()}

Monkey Configuration

{this.renderNav()} {content} @@ -424,7 +482,7 @@ class ConfigurePageComponent extends AuthComponent {
diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 670b99cd7..8503a74fc 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -48,13 +48,14 @@ class AdvancedMultiSelect extends React.Component { }; } - // Sort options alphabetically. "Unsafe" options float to the bottom" + // Sort options alphabetically. "Unsafe" options float to the top so that they + // do not get selected and hidden at the bottom of the list. compareOptions = (a, b) => { // Apparently, you can use additive operators with boolean types. Ultimately, // the ToNumber() abstraction operation is called to convert the booleans to // numbers: https://tc39.es/ecma262/#sec-tonumeric - if (this.isSafe(b.value) - this.isSafe(a.value) !== 0) { - return this.isSafe(b.value) - this.isSafe(a.value); + if (this.isSafe(a.value) - this.isSafe(b.value) !== 0) { + return this.isSafe(a.value) - this.isSafe(b.value); } return a.value.localeCompare(b.value); diff --git a/monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js b/monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js new file mode 100644 index 000000000..3de39fffe --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js @@ -0,0 +1,51 @@ +function getPluginDescriptors(schema, config) { + return ([ + { + name: 'Exploiters', + allPlugins: schema.definitions.exploiter_classes.anyOf, + selectedPlugins: config.basic.exploiters.exploiter_classes + }, + { + name: 'Fingerprinters', + allPlugins: schema.definitions.finger_classes.anyOf, + selectedPlugins: config.internal.classes.finger_classes + }, + { + name: 'PostBreachActions', + allPlugins: schema.definitions.post_breach_actions.anyOf, + selectedPlugins: config.monkey.post_breach.post_breach_actions + }, + { + name: 'SystemInfoCollectors', + allPlugins: schema.definitions.system_info_collector_classes.anyOf, + selectedPlugins: config.monkey.system_info.system_info_collector_classes + } + ]); +} + +function isUnsafeOptionSelected(schema, config) { + let pluginDescriptors = getPluginDescriptors(schema, config); + + for (let descriptor of pluginDescriptors) { + if (isUnsafePluginSelected(descriptor)) { + return true; + } + } + + return false; +} + +function isUnsafePluginSelected(pluginDescriptor) { + let pluginSafety = new Map(); + pluginDescriptor.allPlugins.forEach(i => pluginSafety[i.enum[0]] = i.safe); + + for (let selected of pluginDescriptor.selectedPlugins) { + if (!pluginSafety[selected]) { + return true; + } + } + + return false; +} + +export default isUnsafeOptionSelected;