diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/PluginSelectorTemplate.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/PluginSelectorTemplate.tsx
index de1c77e52b8..7a73042b0b4 100644
--- a/monkey/monkey_island/cc/ui/src/components/configuration-components/PluginSelectorTemplate.tsx
+++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/PluginSelectorTemplate.tsx
@@ -1,48 +1,89 @@
-import {ObjectFieldTemplateProps} from '@rjsf/utils';
+import {getDefaultFormState, ObjectFieldTemplateProps} from '@rjsf/utils';
import React, {useState} from 'react';
-import {Dropdown} from 'react-bootstrap';
-
-const PluginSelector = (props) => {
- let selectOptions = [];
- for (const [name, plugin] of Object.entries(props.plugins)) {
- selectOptions.push(
- props.onClick(name)}
- eventKey={`plugin['title']`}>
- {plugin['title']}
- )}
-
- return (
-
-
- Select a plugin
-
-
- {selectOptions}
-
-
- )
-}
+import ChildCheckboxContainer from '../ui-components/ChildCheckbox';
+import {AdvancedMultiSelectHeader} from '../ui-components/AdvancedMultiSelect';
+import {MasterCheckboxState} from '../ui-components/MasterCheckbox';
export default function PluginSelectorTemplate(props: ObjectFieldTemplateProps) {
- function getPluginDisplay(plugin, allPlugins){
+ let [selectedPlugin, setSelectedPlugin] = useState(null);
+
+ function getPluginDisplay(plugin, allPlugins) {
let selectedPlugin = allPlugins.filter((pluginInArray) => pluginInArray.name == plugin)
- if(selectedPlugin.length === 1){
+ if (selectedPlugin.length === 1) {
return
{selectedPlugin[0].content}
}
}
- let [element, setElement] = useState(null);
+ function getOptions() {
+ let selectorOptions = [];
+ for (let [name, schema] of Object.entries(props.schema.properties)) {
+ selectorOptions.push({label: schema.title, value: name});
+ }
+ return selectorOptions;
+ }
+
+ function togglePluggin(pluginName) {
+ let plugins = new Set(props.formContext.selectedExploiters);
+ if (props.formContext.selectedExploiters.has(pluginName)) {
+ plugins.delete(pluginName);
+ } else {
+ plugins.add(pluginName);
+ }
+ props.formContext.setSelectedExploiters(plugins)
+ }
+
+ function getMasterCheckboxState(selectValues) {
+ if (Object.keys(selectValues).length === 0) {
+ return MasterCheckboxState.NONE;
+ }
+
+ if (Object.keys(selectValues).length !== getOptions().length) {
+ return MasterCheckboxState.MIXED;
+ }
+
+ return MasterCheckboxState.ALL;
+ }
+
+ function generateDefaultConfig() {
+ return getDefaultFormState(props.registry.schemaUtils.validator,
+ props.schema, {}, props.registry.rootSchema, true);
+ }
+
+ function onMasterPluginCheckboxClick() {
+ let checkboxState = getMasterCheckboxState(props.formContext.selectedExploiters);
+ if (checkboxState == MasterCheckboxState.ALL) {
+ props.formContext.setSelectedExploiters({});
+ } else {
+ props.formContext.setSelectedExploiters(generateDefaultConfig());
+ }
+ }
+
+ function isPluginSafe(itemKey) {
+ let itemSchema = Object.entries(props.schema.properties).filter(e => e[0] == itemKey)[0][1];
+ return itemSchema['safe'];
+ }
+
return (
-
- {props.title}
- {props.description}
-
{
- setElement(pluginName)
- }}/>
- {getPluginDisplay(element, props.properties)}
+
+
{
+ }}/>
+
+
+ {getPluginDisplay(selectedPlugin, props.properties)}
);
}
diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/PropagationConfig.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/PropagationConfig.tsx
index 1fd204010a6..472ef2d32b4 100644
--- a/monkey/monkey_island/cc/ui/src/components/configuration-components/PropagationConfig.tsx
+++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/PropagationConfig.tsx
@@ -14,6 +14,9 @@ const sectionOrder = [
const initialSection = sectionOrder[0];
+export const EXPLOITERS_PATH_PROPAGATION = 'exploitation.exploiters';
+export const EXPLOITERS_CONFIG_PATH = 'propagation.' + EXPLOITERS_PATH_PROPAGATION;
+
export default function PropagationConfig(props) {
const {
schema,
@@ -23,8 +26,11 @@ export default function PropagationConfig(props) {
className,
configuration,
credentials,
- onCredentialChange
+ onCredentialChange,
+ selectedExploiters,
+ setSelectedExploiters
} = props;
+
const [selectedSection, setSelectedSection] = useState(initialSection);
const onFormDataChange = (formData) => {
@@ -75,7 +81,11 @@ export default function PropagationConfig(props) {
key={selectedSection}
liveValidate
// children={true} hides the submit button
- children={true}/>
+ children={true}
+ formContext={{
+ 'selectedExploiters': selectedExploiters,
+ 'setSelectedExploiters': setSelectedExploiters
+ }}/>
}
}
diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js
index 83170d2e9de..558450d833d 100644
--- a/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js
+++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js
@@ -10,7 +10,7 @@ export default function UiSchema(props) {
propagation: {
exploitation: {
exploiters: {
- classNames: 'config-template-no-header',
+ 'ui:classNames': 'config-template-no-header',
'ui:ObjectFieldTemplate': PluginSelectorTemplate
},
options: {
@@ -31,19 +31,19 @@ export default function UiSchema(props) {
},
exploit_password_list: {
items: {
- classNames: 'config-template-no-header',
+ 'ui:classNames': 'config-template-no-header',
'ui:widget': SensitiveTextInput
}
},
exploit_lm_hash_list: {
items: {
- classNames: 'config-template-no-header',
+ 'ui:classNames': 'config-template-no-header',
'ui:widget': SensitiveTextInput
}
},
exploit_ntlm_hash_list: {
items: {
- classNames: 'config-template-no-header',
+ 'ui:classNames': 'config-template-no-header',
'ui:widget': SensitiveTextInput
}
}
@@ -52,12 +52,12 @@ export default function UiSchema(props) {
targets: {
blocked_ips: {
items: {
- classNames: 'config-template-no-header'
+ 'ui:classNames': 'config-template-no-header'
}
},
inaccessible_subnets: {
items: {
- classNames: 'config-template-no-header'
+ 'ui:classNames': 'config-template-no-header'
}
},
info_box_scan_my_networks: {
@@ -65,29 +65,29 @@ export default function UiSchema(props) {
},
subnets: {
items: {
- classNames: 'config-template-no-header'
+ 'ui:classNames': 'config-template-no-header'
}
}
},
tcp: {
ports: {
items: {
- classNames: 'config-template-no-header'
+ 'ui:classNames': 'config-template-no-header'
}
}
},
fingerprinters: {
- classNames: 'config-template-no-header',
+ 'ui:classNames': 'config-template-no-header',
'ui:widget': AdvancedMultiSelect,
fingerprinter_classes: {
- classNames: 'config-template-no-header'
+ 'ui:classNames': 'config-template-no-header'
}
}
}
},
payloads: {
- classNames: 'config-template-no-header',
+ 'ui:classNames': 'config-template-no-header',
encryption: {
info_box: {
'ui:field': InfoBox
@@ -110,10 +110,10 @@ export default function UiSchema(props) {
}
},
credential_collectors: {
- classNames: 'config-template-no-header',
+ 'ui:classNames': 'config-template-no-header',
'ui:widget': AdvancedMultiSelect,
credential_collectors_classes: {
- classNames: 'config-template-no-header'
+ 'ui:classNames': 'config-template-no-header'
}
}
};
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 b17f6b6bde6..eff4efe527c 100644
--- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js
+++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js
@@ -9,7 +9,9 @@ import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck';
import {faExclamationCircle} from '@fortawesome/free-solid-svg-icons/faExclamationCircle';
import {formValidationFormats} from '../configuration-components/ValidationFormats';
import transformErrors from '../configuration-components/ValidationErrorMessages';
-import PropagationConfig from '../configuration-components/PropagationConfig'
+import PropagationConfig, {
+ EXPLOITERS_CONFIG_PATH
+} from '../configuration-components/PropagationConfig'
import UnsafeConfigOptionsConfirmationModal
from '../configuration-components/UnsafeConfigOptionsConfirmationModal.js';
import isUnsafeOptionSelected from '../utils/SafeOptionValidator.js';
@@ -30,6 +32,10 @@ const CONFIG_URL = '/api/agent-configuration';
const SCHEMA_URL = '/api/agent-configuration-schema';
const RESET_URL = '/api/reset-agent-configuration';
const CONFIGURED_PROPAGATION_CREDENTIALS_URL = '/api/propagation-credentials/configured-credentials';
+// "new" schema is the one coming from back-end, the legacy one is defined in
+// monkey/monkey_island/cc/ui/src/services/configuration/configSchema.js
+const EXPLOITERS_SCHEMA_PATH_NEW = 'definitions.ExploitationConfiguration.properties.exploiters';
+const EXPLOITERS_SCHEMA_PATH_LEGACY = 'properties.propagation.properties.exploitation.properties.exploiters';
const configSubmitAction = 'config-submit';
const configExportAction = 'config-export';
@@ -39,7 +45,6 @@ class ConfigurePageComponent extends AuthComponent {
constructor(props) {
super(props);
- this.initialConfig = {};
this.currentSection = this.getSectionsOrder()[0];
this.state = {
@@ -53,7 +58,8 @@ class ConfigurePageComponent extends AuthComponent {
selectedSection: this.currentSection,
showUnsafeOptionsConfirmation: false,
showConfigExportModal: false,
- showConfigImportModal: false
+ showConfigImportModal: false,
+ selectedExploiters: new Set()
};
}
@@ -74,18 +80,12 @@ class ConfigurePageComponent extends AuthComponent {
return CONFIGURATION_TABS_PER_MODE[islandMode];
}
- setInitialConfig(config) {
- // Sets a reference to know if config was changed
- this.initialConfig = JSON.parse(JSON.stringify(config));
- }
-
injectExploitersIntoLegacySchema = (newSchema) => {
// legacy schema is defined in UI,
// but we should use the schema provided by "/api/agent-configuration-schema"
// Remove when #2750 is done
let injectedSchema = _.cloneDeep(this.state.schema);
- injectedSchema['properties']['propagation']['properties']['exploitation']['properties']['exploiters'] =
- newSchema['definitions']['ExploitationConfiguration']['properties']['exploiters']
+ _.set(injectedSchema, EXPLOITERS_SCHEMA_PATH_LEGACY, _.get(newSchema, EXPLOITERS_SCHEMA_PATH_NEW));
return injectedSchema;
}
@@ -99,18 +99,17 @@ class ConfigurePageComponent extends AuthComponent {
let sections = [];
monkeyConfig = reformatConfig(monkeyConfig);
- this.setInitialConfig(monkeyConfig);
for (let sectionKey of this.getSectionsOrder()) {
sections.push({
key: sectionKey,
title: SCHEMA.properties[sectionKey].title
});
}
-
this.setState({
configuration: monkeyConfig,
+ selectedExploiters: new Set(Object.keys(_.get(monkeyConfig, EXPLOITERS_CONFIG_PATH))),
sections: sections,
- currentFormData: monkeyConfig[this.state.selectedSection]
+ currentFormData: _.cloneDeep(monkeyConfig[this.state.selectedSection])
})
});
this.updateCredentials();
@@ -147,14 +146,18 @@ class ConfigurePageComponent extends AuthComponent {
.then(res => res.json())
.then(data => {
data = reformatConfig(data);
- this.setInitialConfig(data);
this.setState({
+ selectedExploiters: new Set(Object.keys(_.get(data, EXPLOITERS_CONFIG_PATH))),
configuration: data,
- currentFormData: data[this.state.selectedSection]
+ currentFormData: _.cloneDeep(data[this.state.selectedSection])
});
});
}
+ setSelectedExploiters = (exploiters) => {
+ this.setState({selectedExploiters: exploiters})
+ }
+
onSubmit = () => {
this.setState({lastAction: configSubmitAction}, this.attemptConfigSubmit)
};
@@ -164,9 +167,9 @@ class ConfigurePageComponent extends AuthComponent {
}
async attemptConfigSubmit() {
- await this.updateConfigSection();
- if (this.canSafelySubmitConfig(this.state.configuration)) {
- this.configSubmit();
+ let config = this.filterUnselectedPlugins()
+ if (this.canSafelySubmitConfig(config)) {
+ this.configSubmit(config);
if (this.state.lastAction === configExportAction) {
this.setState({showConfigExportModal: true})
}
@@ -175,10 +178,25 @@ class ConfigurePageComponent extends AuthComponent {
}
}
- configSubmit() {
+ // rjsf component automatically creates an instance from the defaults in the schema
+ // https://github.com/rjsf-team/react-jsonschema-form/issues/2980
+ // Until the issue is fixed, we need to manually remove plugins that were not selected before
+ // submitting/exporting the configuration
+ filterUnselectedPlugins() {
+ let filteredExploiters = {};
+ let exploiterFormData = _.get(this.state.configuration, EXPLOITERS_CONFIG_PATH);
+ for(let exploiter of [...this.state.selectedExploiters]){
+ filteredExploiters[exploiter] = exploiterFormData[exploiter];
+ }
+ let config = _.cloneDeep(this.state.configuration)
+ _.set(config, EXPLOITERS_CONFIG_PATH, filteredExploiters)
+ return config;
+ }
+
+ configSubmit(config) {
this.sendCredentials().then(res => {
if(res.ok) {
- this.sendConfig();
+ this.sendConfig(config);
}
});
}
@@ -193,18 +211,9 @@ class ConfigurePageComponent extends AuthComponent {
this.setState({credentials: credentials});
}
- updateConfigSection = () => {
- let newConfig = this.state.configuration;
-
- if (Object.keys(this.state.currentFormData).length > 0) {
- newConfig[this.currentSection] = this.state.currentFormData;
- }
- this.setState({configuration: newConfig});
- };
-
renderConfigExportModal = () => {
return ( {
this.setState({showConfigExportModal: false});
@@ -242,7 +251,6 @@ class ConfigurePageComponent extends AuthComponent {
setSelectedSection = (key) => {
this.resetLastAction();
- this.updateConfigSection();
this.currentSection = key;
let selectedSectionData = this.state.configuration[key];
@@ -274,8 +282,7 @@ class ConfigurePageComponent extends AuthComponent {
await this.attemptConfigSubmit();
};
- sendConfig() {
- let config = JSON.parse(JSON.stringify(this.state.configuration))
+ sendConfig(config) {
config = reformatConfig(config, true);
delete config['advanced'];
delete config['propagation']['general'];
@@ -345,6 +352,8 @@ class ConfigurePageComponent extends AuthComponent {
delete Object.assign(formProperties, {'configuration': formProperties.formData}).formData;
return ()
} else {
formProperties['onChange'] = (formData) => {
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 16eb4decdbb..90437deabb6 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
@@ -8,7 +8,7 @@ import {MasterCheckbox, MasterCheckboxState} from './MasterCheckbox';
import ChildCheckboxContainer from './ChildCheckbox';
import {getFullDefinitionByKey} from './JsonSchemaHelpers';
-function AdvancedMultiSelectHeader(props) {
+export function AdvancedMultiSelectHeader(props) {
const {
title,
onCheckboxClick,