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,