diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js index 871941c82..252adbde2 100644 --- a/cypress/integration/1_detectors.spec.js +++ b/cypress/integration/1_detectors.spec.js @@ -20,14 +20,17 @@ const testMappings = { const cypressDNSRule = dns_rule_data.title; describe('Detectors', () => { - const indexName = 'cypress-test-dns'; + const cypressIndexDns = 'cypress-index-dns'; + const cypressIndexWindows = 'cypress-index-windows'; const detectorName = 'test detector'; before(() => { cy.cleanUpTests(); + cy.createIndex(cypressIndexWindows, sample_index_settings); + // Create test index - cy.createIndex(indexName, sample_index_settings).then(() => + cy.createIndex(cypressIndexDns, sample_index_settings).then(() => cy .request('POST', '_plugins/_security_analytics/rules/_search?prePackaged=true', { from: 0, @@ -58,11 +61,6 @@ describe('Detectors', () => { }); it('...should show mappings warning', () => { - const indexName = 'cypress-index-windows'; - const dnsName = 'cypress-index-dns'; - cy.createIndex(indexName, sample_index_settings); - cy.createIndex(dnsName, sample_dns_settings); - // Locate Create detector button click to start cy.get('.euiButton').filter(':contains("Create detector")').click({ force: true }); @@ -71,23 +69,25 @@ describe('Detectors', () => { contains: 'Define detector', }); - // Select our pre-seeded data source (check indexName) + // Select our pre-seeded data source (check cypressIndexDns) cy.get(`[data-test-subj="define-detector-select-data-source"]`) .find('input') .focus() - .realType(indexName); + .realType(cypressIndexDns); // Select threat detector type (Windows logs) cy.get(`input[id="dns"]`).click({ force: true }); - // Select our pre-seeded data source (check indexName) + // Select our pre-seeded data source (check cypressIndexDns) cy.get(`[data-test-subj="define-detector-select-data-source"]`) .find('input') .focus() - .realType(dnsName) + .realType(cypressIndexWindows) .realPress('Enter'); - cy.get('.euiCallOut').should('be.visible').contains('Detector configuration warning'); + cy.get('.euiCallOut') + .should('be.visible') + .contains('The selected log sources contain different types of logs'); }); it('...can be created', () => { @@ -102,11 +102,11 @@ describe('Detectors', () => { // Enter a name for the detector in the appropriate input cy.get(`input[placeholder="Enter a name for the detector."]`).focus().realType('test detector'); - // Select our pre-seeded data source (check indexName) + // Select our pre-seeded data source (check cypressIndexDns) cy.get(`[data-test-subj="define-detector-select-data-source"]`) .find('input') .focus() - .realType(indexName); + .realType(cypressIndexDns); cy.intercept({ pathname: '/_plugins/_security_analytics/rules/_search', @@ -198,7 +198,7 @@ describe('Detectors', () => { cy.contains('Detector details'); cy.contains(detectorName); cy.contains('dns'); - cy.contains(indexName); + cy.contains(cypressIndexDns); cy.contains('Alert on test_trigger'); // Create the detector @@ -252,8 +252,7 @@ describe('Detectors', () => { .find('input') .ospClear() .focus() - .realType('.opensearch-notifications-config') - .realPress('Enter'); + .realType(cypressIndexWindows); // Change detector scheduling cy.get(`[data-test-subj="detector-schedule-number-select"]`).ospClear().focus().realType('10'); @@ -271,7 +270,7 @@ describe('Detectors', () => { cy.contains('test detector edited'); cy.contains('Every 10 hours'); cy.contains('Edited description'); - cy.contains('.opensearch-notifications-config'); + cy.contains(cypressIndexWindows); }); it('...rules can be edited', () => { diff --git a/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldMappingsTable.tsx b/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldMappingsTable.tsx index 094dfc8c0..fbe5feee7 100644 --- a/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldMappingsTable.tsx +++ b/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldMappingsTable.tsx @@ -86,7 +86,7 @@ export default class FieldMappingsTable extends Compo const columns: EuiBasicTableColumn[] = [ { field: 'ruleFieldName', - name: 'Rule field name', + name: 'Detector field name', dataType: 'string', width: '25%', render: (ruleFieldName: string) => ruleFieldName || DEFAULT_EMPTY_DATA, @@ -100,7 +100,7 @@ export default class FieldMappingsTable extends Compo }, { field: 'logFieldName', - name: 'Log field name', + name: 'Log source field name', dataType: 'string', width: '45%', render: (logFieldName: string, entry: FieldMappingsTableItem) => { diff --git a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx index 281225cd1..fc7e4023d 100644 --- a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx +++ b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx @@ -180,6 +180,37 @@ export default class ConfigureFieldMapping extends Component< + + + +

{`Automatically mapped fields (${mappedRuleFields.length})`}

+
+ + } + buttonProps={{ style: { paddingLeft: '10px', paddingRight: '10px' } }} + id={'mappedFieldsAccordion'} + initialIsOpen={false} + > + + + {...this.props} + loading={loading} + ruleFields={mappedRuleFields} + indexFields={indexFieldOptions} + mappingProps={{ + type: MappingViewType.Edit, + existingMappings, + invalidMappingFieldNames, + onMappingCreation: this.onMappingCreation, + }} + /> +
+
+ + + {unmappedRuleFields.length > 0 ? ( <> {pendingCount > 0 ? ( @@ -227,34 +258,6 @@ export default class ConfigureFieldMapping extends Component< )} - - - -

{`Default mapped fields (${mappedRuleFields.length})`}

-
- - } - buttonProps={{ style: { paddingLeft: '10px', paddingRight: '10px' } }} - id={'mappedFieldsAccordion'} - initialIsOpen={false} - > - - - {...this.props} - loading={loading} - ruleFields={mappedRuleFields} - indexFields={indexFieldOptions} - mappingProps={{ - type: MappingViewType.Edit, - existingMappings, - invalidMappingFieldNames, - onMappingCreation: this.onMappingCreation, - }} - /> -
-
); diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx index 5592501cc..1f19fba0a 100644 --- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx @@ -5,20 +5,31 @@ import React, { Component } from 'react'; import { ContentPanel } from '../../../../../../components/ContentPanel'; -import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { + EuiComboBox, + EuiComboBoxOptionOption, + EuiFormRow, + EuiSpacer, + EuiCallOut, + EuiTextColor, +} from '@elastic/eui'; import { FormFieldHeader } from '../../../../../../components/FormFieldHeader/FormFieldHeader'; import { IndexOption } from '../../../../../Detectors/models/interfaces'; import { MIN_NUM_DATA_SOURCES } from '../../../../../Detectors/utils/constants'; import IndexService from '../../../../../../services/IndexService'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { errorNotificationToast } from '../../../../../../utils/helpers'; +import _ from 'lodash'; +import { FieldMappingService } from '../../../../../../services'; interface DetectorDataSourceProps { detectorIndices: string[]; indexService: IndexService; + filedMappingService: FieldMappingService; isEdit: boolean; onDetectorInputIndicesChange: (selectedOptions: EuiComboBoxOptionOption[]) => void; notifications: NotificationsStart; + detector_type: string; } interface DetectorDataSourceState { @@ -26,18 +37,22 @@ interface DetectorDataSourceState { fieldTouched: boolean; indexOptions: IndexOption[]; errorMessage?: string; + message: string[]; } export default class DetectorDataSource extends Component< DetectorDataSourceProps, DetectorDataSourceState > { + private indicesMappings: any = {}; + constructor(props: DetectorDataSourceProps) { super(props); this.state = { loading: true, fieldTouched: props.isEdit, indexOptions: [], + message: [], }; } @@ -82,17 +97,75 @@ export default class DetectorDataSource extends Component< this.onSelectionChange(parsedOptions); }; - onSelectionChange = (options: EuiComboBoxOptionOption[]) => { + onSelectionChange = async (options: EuiComboBoxOptionOption[]) => { + const allIndices = _.map(options, 'label'); + for (let indexName in this.indicesMappings) { + if (allIndices.indexOf(indexName) === -1) { + // cleanup removed indexes + delete this.indicesMappings[indexName]; + } + } + + for (const indexName of allIndices) { + if (!this.indicesMappings[indexName]) { + const detectorType = this.props.detector_type.toLowerCase(); + const result = await this.props.filedMappingService.getMappingsView( + indexName, + detectorType + ); + result.ok && (this.indicesMappings[indexName] = result.response.unmapped_field_aliases); + } + } + + if (!_.isEmpty(this.indicesMappings)) { + let firstMapping: string[] = []; + let firstMatchMappingIndex: string = ''; + let message: string[] = []; + for (let indexName in this.indicesMappings) { + if (this.indicesMappings.hasOwnProperty(indexName)) { + if (!firstMapping.length) firstMapping = this.indicesMappings[indexName]; + !firstMatchMappingIndex.length && (firstMatchMappingIndex = indexName); + if (!_.isEqual(firstMapping, this.indicesMappings[indexName])) { + message = [ + `We recommend creating separate detectors for each of the following log sources:`, + firstMatchMappingIndex, + indexName, + ]; + break; + } + } + } + + this.setState({ message }); + } + this.props.onDetectorInputIndicesChange(options); }; render() { const { detectorIndices } = this.props; - const { loading, fieldTouched, indexOptions, errorMessage } = this.state; + const { loading, fieldTouched, indexOptions, errorMessage, message } = this.state; const isInvalid = fieldTouched && detectorIndices.length < MIN_NUM_DATA_SOURCES; return ( + {message.length ? ( + <> + + {message.map((messageItem: string, index: number) => ( + + {index === 0 ? '' : 'ㅤ•ㅤ'} + {messageItem} +
+
+ ))} +
+ + + ) : null} diff --git a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx index 49dded57d..16e3e9bf1 100644 --- a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx @@ -5,7 +5,7 @@ import React, { Component } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { EuiSpacer, EuiTitle, EuiText, EuiCallOut, EuiTextColor } from '@elastic/eui'; +import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; import { Detector, PeriodSchedule } from '../../../../../../models/interfaces'; import DetectorBasicDetailsForm from '../components/DetectorDetails'; import DetectorDataSource from '../components/DetectorDataSource'; @@ -38,66 +38,17 @@ interface DefineDetectorProps extends RouteComponentProps { onAllRulesToggle: (enabled: boolean) => void; } -interface DefineDetectorState { - message: string[]; -} +interface DefineDetectorState {} export default class DefineDetector extends Component { - state = { - message: [], - }; - - private indicesMappings: any = {}; - - async updateDetectorCreationState(detector: Detector) { - let isDataValid = + updateDetectorCreationState(detector: Detector) { + const isDataValid = !!detector.name && !!detector.detector_type && detector.inputs[0].detector_input.indices.length >= MIN_NUM_DATA_SOURCES && !!detector.schedule.period.interval; - this.props.changeDetector(detector); - - const allIndices = detector.inputs[0].detector_input.indices; - for (let indexName in this.indicesMappings) { - if (allIndices.indexOf(indexName) === -1) { - // cleanup removed indexes - delete this.indicesMappings[indexName]; - } - } - - for (const indexName of allIndices) { - if (!this.indicesMappings[indexName]) { - const detectorType = this.props.detector.detector_type.toLowerCase(); - const result = await this.props.filedMappingService.getMappingsView( - indexName, - detectorType - ); - result.ok && (this.indicesMappings[indexName] = result.response.unmapped_field_aliases); - } - } - - if (!_.isEmpty(this.indicesMappings)) { - let firstMapping: string[] = []; - let firstMatchMappingIndex: string = ''; - let message: string[] = []; - for (let indexName in this.indicesMappings) { - if (this.indicesMappings.hasOwnProperty(indexName)) { - if (!firstMapping.length) firstMapping = this.indicesMappings[indexName]; - !firstMatchMappingIndex.length && (firstMatchMappingIndex = indexName); - if (!_.isEqual(firstMapping, this.indicesMappings[indexName])) { - message = [ - `The below log sources don't have the same fields, please consider creating separate detectors for them.`, - firstMatchMappingIndex, - indexName, - ]; - break; - } - } - } - - this.setState({ message }); - } + this.props.changeDetector(detector); this.props.updateDataValidState(DetectorCreationStep.DEFINE_DETECTOR, isDataValid); } @@ -213,8 +164,8 @@ export default class DefineDetector extends Component - {message.length ? ( - <> - - {message.map((messageItem: string, index: number) => ( - - {messageItem} -
-
- ))} -
- - - ) : null} diff --git a/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx b/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx index ad8fed8c4..edd77e191 100644 --- a/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx +++ b/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx @@ -217,9 +217,11 @@ export const UpdateDetectorBasicDetails: React.FC diff --git a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap index 5e033074f..e375ee4ae 100644 --- a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap +++ b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap @@ -887,6 +887,7 @@ exports[` spec renders the component 1`] = ` ".windows", ] } + detector_type="detector_type" indexService={ Object { "getIndices": [Function], diff --git a/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx b/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx index d66643258..03944e0f8 100644 --- a/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx +++ b/public/pages/Detectors/containers/FieldMappings/EditFieldMapping.tsx @@ -5,7 +5,14 @@ import React, { Component } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { EuiAccordion, EuiHorizontalRule, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { + EuiAccordion, + EuiHorizontalRule, + EuiPanel, + EuiSpacer, + EuiTitle, + EuiCallOut, +} from '@elastic/eui'; import FieldMappingsTable from '../../../CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping'; import { ContentPanel } from '../../../../components/ContentPanel'; import { Detector, FieldMapping } from '../../../../../models/interfaces'; @@ -158,34 +165,12 @@ export default class EditFieldMappings extends Component< return (
- {unmappedRuleFields.length > 0 && ( - <> - - - {...this.props} - loading={loading} - ruleFields={unmappedRuleFields} - indexFields={logFieldOptions} - mappingProps={{ - type: MappingViewType.Edit, - existingMappings, - invalidMappingFieldNames, - onMappingCreation: this.onMappingCreation, - }} - /> - - - - )} -

{`Mapped fields (${mappedRuleFields.length})`}

+

{`Automatically mapped fields (${mappedRuleFields.length})`}

} @@ -208,6 +193,46 @@ export default class EditFieldMappings extends Component< /> + + + + {unmappedRuleFields.length > 0 && ( + <> + {unmappedRuleFields.length > 0 ? ( + +

+ To generate accurate findings, we recommend mapping the following security rules + fields with the log fields in your data source. +

+
+ ) : ( + +

Your data source have been mapped with all security rule fields.

+
+ )} + + + + + {...this.props} + loading={loading} + ruleFields={unmappedRuleFields} + indexFields={logFieldOptions} + mappingProps={{ + type: MappingViewType.Edit, + existingMappings, + invalidMappingFieldNames, + onMappingCreation: this.onMappingCreation, + }} + /> + + + + )} + ); diff --git a/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap b/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap index f012c34ef..9bd7ee5b8 100644 --- a/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap +++ b/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap @@ -201,7 +201,7 @@ exports[` spec renders the component 1`] = ` >

- Mapped fields (0) + Automatically mapped fields (0)

@@ -261,7 +261,7 @@ exports[` spec renders the component 1`] = `

- Mapped fields (0) + Automatically mapped fields (0)

@@ -492,7 +492,7 @@ exports[` spec renders the component 1`] = ` Object { "dataType": "string", "field": "ruleFieldName", - "name": "Rule field name", + "name": "Detector field name", "render": [Function], "width": "25%", }, @@ -506,7 +506,7 @@ exports[` spec renders the component 1`] = ` Object { "dataType": "string", "field": "logFieldName", - "name": "Log field name", + "name": "Log source field name", "render": [Function], "width": "45%", }, @@ -563,7 +563,7 @@ exports[` spec renders the component 1`] = ` Object { "dataType": "string", "field": "ruleFieldName", - "name": "Rule field name", + "name": "Detector field name", "render": [Function], "width": "25%", }, @@ -577,7 +577,7 @@ exports[` spec renders the component 1`] = ` Object { "dataType": "string", "field": "logFieldName", - "name": "Log field name", + "name": "Log source field name", "render": [Function], "width": "45%", }, @@ -631,7 +631,7 @@ exports[` spec renders the component 1`] = ` "allowNeutralSort": true, "sort": Object { "direction": "asc", - "field": "Rule field name", + "field": "Detector field name", }, } } @@ -824,15 +824,15 @@ exports[` spec renders the component 1`] = ` values={ Object { "description": undefined, - "innerText": "Rule field name", + "innerText": "Detector field name", } } > - Rule field name + Detector field name @@ -918,15 +918,15 @@ exports[` spec renders the component 1`] = ` values={ Object { "description": undefined, - "innerText": "Log field name", + "innerText": "Log source field name", } } > - Log field name + Log source field name @@ -1099,6 +1099,13 @@ exports[` spec renders the component 1`] = ` className="euiSpacer euiSpacer--m" />
+ +
+
`;