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;