forked from p34709852/monkey
Merge pull request #1000 from guardicore/unsafe-options-confirmation
Unsafe options confirmation
This commit is contained in:
commit
cfaf4a15c3
|
@ -11,6 +11,9 @@ from monkey_island.cc.services.config_schema.monkey import MONKEY
|
||||||
SCHEMA = {
|
SCHEMA = {
|
||||||
"title": "Monkey",
|
"title": "Monkey",
|
||||||
"type": "object",
|
"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": {
|
"definitions": {
|
||||||
"exploiter_classes": EXPLOITER_CLASSES,
|
"exploiter_classes": EXPLOITER_CLASSES,
|
||||||
"system_info_collector_classes": SYSTEM_INFO_COLLECTOR_CLASSES,
|
"system_info_collector_classes": SYSTEM_INFO_COLLECTOR_CLASSES,
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {Modal, Button} from 'react-bootstrap';
|
||||||
|
|
||||||
|
function UnsafeOptionsConfirmationModal(props) {
|
||||||
|
return (
|
||||||
|
<Modal show={props.show}>
|
||||||
|
<Modal.Body>
|
||||||
|
<h2>
|
||||||
|
<div className='text-center'>Warning</div>
|
||||||
|
</h2>
|
||||||
|
<p className='text-center' style={{'fontSize': '1.2em', 'marginBottom': '2em'}}>
|
||||||
|
Some of the selected options could cause systems to become unstable or malfunction.
|
||||||
|
Are you sure you want to submit the selected settings?
|
||||||
|
</p>
|
||||||
|
<div className='text-center'>
|
||||||
|
<Button type='button'
|
||||||
|
className='btn btn-secondary'
|
||||||
|
size='lg'
|
||||||
|
style={{margin: '5px'}}
|
||||||
|
onClick={props.onCancelClick}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type='button'
|
||||||
|
className='btn btn-danger'
|
||||||
|
size='lg'
|
||||||
|
style={{margin: '5px'}}
|
||||||
|
onClick={props.onContinueClick}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UnsafeOptionsConfirmationModal;
|
|
@ -11,6 +11,8 @@ import {faExclamationCircle} from '@fortawesome/free-solid-svg-icons/faExclamati
|
||||||
import {formValidationFormats} from '../configuration-components/ValidationFormats';
|
import {formValidationFormats} from '../configuration-components/ValidationFormats';
|
||||||
import transformErrors from '../configuration-components/ValidationErrorMessages';
|
import transformErrors from '../configuration-components/ValidationErrorMessages';
|
||||||
import InternalConfig from '../configuration-components/InternalConfig';
|
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 ATTACK_URL = '/api/attack';
|
||||||
const CONFIG_URL = '/api/configuration/island';
|
const CONFIG_URL = '/api/configuration/island';
|
||||||
|
@ -28,13 +30,15 @@ class ConfigurePageComponent extends AuthComponent {
|
||||||
this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'internal'];
|
this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'internal'];
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
schema: {},
|
|
||||||
configuration: {},
|
|
||||||
attackConfig: {},
|
attackConfig: {},
|
||||||
|
configuration: {},
|
||||||
|
importCandidateConfig: null,
|
||||||
lastAction: 'none',
|
lastAction: 'none',
|
||||||
|
schema: {},
|
||||||
sections: [],
|
sections: [],
|
||||||
selectedSection: 'attack',
|
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 = () => {
|
updateConfig = () => {
|
||||||
this.authFetch(CONFIG_URL)
|
this.authFetch(CONFIG_URL)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
|
@ -85,12 +103,16 @@ class ConfigurePageComponent extends AuthComponent {
|
||||||
|
|
||||||
onSubmit = () => {
|
onSubmit = () => {
|
||||||
if (this.state.selectedSection === 'attack') {
|
if (this.state.selectedSection === 'attack') {
|
||||||
this.matrixSubmit()
|
this.matrixSubmit();
|
||||||
} else {
|
} else {
|
||||||
this.configSubmit()
|
this.attemptConfigSubmit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
canSafelySubmitConfig(config) {
|
||||||
|
return !isUnsafeOptionSelected(this.state.schema, config);
|
||||||
|
}
|
||||||
|
|
||||||
matrixSubmit = () => {
|
matrixSubmit = () => {
|
||||||
// Submit attack matrix
|
// Submit attack matrix
|
||||||
this.authFetch(ATTACK_URL,
|
this.authFetch(ATTACK_URL,
|
||||||
|
@ -116,9 +138,19 @@ class ConfigurePageComponent extends AuthComponent {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
configSubmit = () => {
|
attemptConfigSubmit() {
|
||||||
// Submit monkey configuration
|
|
||||||
this.updateConfigSection();
|
this.updateConfigSection();
|
||||||
|
this.setState({lastAction: 'submit_attempt'}, () => {
|
||||||
|
if (this.canSafelySubmitConfig(this.state.configuration)) {
|
||||||
|
this.configSubmit();
|
||||||
|
} else {
|
||||||
|
this.setState({showUnsafeOptionsConfirmation: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
configSubmit() {
|
||||||
this.sendConfig()
|
this.sendConfig()
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
@ -133,7 +165,7 @@ class ConfigurePageComponent extends AuthComponent {
|
||||||
console.log('Bad configuration: ' + error.toString());
|
console.log('Bad configuration: ' + error.toString());
|
||||||
this.setState({lastAction: 'invalid_configuration'});
|
this.setState({lastAction: 'invalid_configuration'});
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
// Alters attack configuration when user toggles technique
|
// Alters attack configuration when user toggles technique
|
||||||
attackTechniqueChange = (technique, value, mapped = false) => {
|
attackTechniqueChange = (technique, value, mapped = false) => {
|
||||||
|
@ -201,6 +233,16 @@ class ConfigurePageComponent extends AuthComponent {
|
||||||
</Modal>)
|
</Modal>)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renderUnsafeOptionsConfirmationModal() {
|
||||||
|
return (
|
||||||
|
<UnsafeOptionsConfirmationModal
|
||||||
|
show={this.state.showUnsafeOptionsConfirmation}
|
||||||
|
onCancelClick={this.onUnsafeConfirmationCancelClick}
|
||||||
|
onContinueClick={this.onUnsafeConfirmationContinueClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
userChangedConfig() {
|
userChangedConfig() {
|
||||||
if (JSON.stringify(this.state.configuration) === JSON.stringify(this.initialConfig)) {
|
if (JSON.stringify(this.state.configuration) === JSON.stringify(this.initialConfig)) {
|
||||||
if (Object.keys(this.currentFormData).length === 0 ||
|
if (Object.keys(this.currentFormData).length === 0 ||
|
||||||
|
@ -276,18 +318,33 @@ class ConfigurePageComponent extends AuthComponent {
|
||||||
|
|
||||||
setConfigOnImport = (event) => {
|
setConfigOnImport = (event) => {
|
||||||
try {
|
try {
|
||||||
this.setState({
|
var newConfig = JSON.parse(event.target.result);
|
||||||
configuration: JSON.parse(event.target.result),
|
|
||||||
lastAction: 'import_success'
|
|
||||||
}, () => {
|
|
||||||
this.sendConfig();
|
|
||||||
this.setInitialConfig(JSON.parse(event.target.result))
|
|
||||||
});
|
|
||||||
this.currentFormData = {};
|
|
||||||
} catch (SyntaxError) {
|
} catch (SyntaxError) {
|
||||||
this.setState({lastAction: 'import_failure'});
|
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 = () => {
|
exportConfig = () => {
|
||||||
this.updateConfigSection();
|
this.updateConfigSection();
|
||||||
|
@ -410,6 +467,7 @@ class ConfigurePageComponent extends AuthComponent {
|
||||||
lg={{offset: 3, span: 8}} xl={{offset: 2, span: 8}}
|
lg={{offset: 3, span: 8}} xl={{offset: 2, span: 8}}
|
||||||
className={'main'}>
|
className={'main'}>
|
||||||
{this.renderAttackAlertModal()}
|
{this.renderAttackAlertModal()}
|
||||||
|
{this.renderUnsafeOptionsConfirmationModal()}
|
||||||
<h1 className='page-title'>Monkey Configuration</h1>
|
<h1 className='page-title'>Monkey Configuration</h1>
|
||||||
{this.renderNav()}
|
{this.renderNav()}
|
||||||
{content}
|
{content}
|
||||||
|
@ -424,7 +482,7 @@ class ConfigurePageComponent extends AuthComponent {
|
||||||
<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'}}>
|
||||||
Import Config
|
Import config
|
||||||
</button>
|
</button>
|
||||||
<input id='uploadInputInternal' type='file' accept='.conf' onChange={this.importConfig}
|
<input id='uploadInputInternal' type='file' accept='.conf' onChange={this.importConfig}
|
||||||
style={{display: 'none'}}/>
|
style={{display: 'none'}}/>
|
||||||
|
|
|
@ -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) => {
|
compareOptions = (a, b) => {
|
||||||
// Apparently, you can use additive operators with boolean types. Ultimately,
|
// Apparently, you can use additive operators with boolean types. Ultimately,
|
||||||
// the ToNumber() abstraction operation is called to convert the booleans to
|
// the ToNumber() abstraction operation is called to convert the booleans to
|
||||||
// numbers: https://tc39.es/ecma262/#sec-tonumeric
|
// numbers: https://tc39.es/ecma262/#sec-tonumeric
|
||||||
if (this.isSafe(b.value) - this.isSafe(a.value) !== 0) {
|
if (this.isSafe(a.value) - this.isSafe(b.value) !== 0) {
|
||||||
return this.isSafe(b.value) - this.isSafe(a.value);
|
return this.isSafe(a.value) - this.isSafe(b.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.value.localeCompare(b.value);
|
return a.value.localeCompare(b.value);
|
||||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue