From eefa869b1a8ea266707fb429197318d6b52f09a9 Mon Sep 17 00:00:00 2001 From: Amardeepsingh Siglani Date: Tue, 21 Feb 2023 17:34:56 -0800 Subject: [PATCH] Feature] create detector | make data source multi select field (#424) (#429) * [FEATURE] Detector must have at least one alert set #288 * [BUG] Create detector | Interval field can be empty #378 * Adjust styling for Finding details flyout #369 * unit tests * detector unit tests * detector unit tests * detector unit tests * detector unit tests * detector unit tests * detector unit tests * detector unit tests * detector unit tests * detector unit tests * unit tests review * unit tests review * unit tests review * unit tests review * unit tests review * unit tests review * unit tests review * unit tests review * Feature/update vertical domain #372 * Unit tests for public components #383 * Unit tests for public components #383 * Unit tests for public components #383 * Unit tests for public components #383 * Unit tests for public components #383 [BUG] Detector Edit | Custom rule are not selected on update rules #406 * Unit tests for public components #383 [BUG] Detector Edit | Custom rule are not selected on update rules #406 * PR code review * PR code review * PR code review * [FEATURE] Create detector | Make data source multi-select field #419 * [FEATURE] Create detector | Make data source multi-select field #419 * [FEATURE] Create detector | Make data source multi-select field #419 * [FEATURE] Create detector | Make data source multi-select field #419 * [FEATURE] Create detector | Make data source multi-select field #419 * [FEATURE] Create detector | Make data source multi-select field #419 * [FEATURE] Create detector | Make data source multi-select field #419 * [FEATURE] Create detector | Make data source multi-select field #419 * [FEATURE] Create detector | Make data source multi-select field #419 * unit tests fix --------- Signed-off-by: Jovan Cvetkovic Signed-off-by: Amardeepsingh Siglani Co-authored-by: Jovan Cvetkovic (cherry picked from commit ec2e438f14688de7ca57db254643a5d61dd962fe) --- cypress/integration/1_detectors.spec.js | 39 ++++++++- .../FieldMappingsTable.tsx | 11 ++- .../DetectorDataSource/DetectorDataSource.tsx | 2 +- .../containers/DefineDetector.tsx | 79 +++++++++++++++++-- .../containers/CreateDetector.tsx | 9 +-- .../AlertTriggersView.test.tsx | 3 + 6 files changed, 128 insertions(+), 15 deletions(-) diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js index 3d0ff2f0e..871941c82 100644 --- a/cypress/integration/1_detectors.spec.js +++ b/cypress/integration/1_detectors.spec.js @@ -6,6 +6,7 @@ import { OPENSEARCH_DASHBOARDS_URL } from '../support/constants'; import sample_index_settings from '../fixtures/sample_index_settings.json'; import dns_rule_data from '../fixtures/integration_tests/rule/create_dns_rule.json'; +import sample_dns_settings from '../fixtures/integration_tests/index/create_dns_settings.json'; const testMappings = { properties: { @@ -16,7 +17,7 @@ const testMappings = { }, }; -const cypressDNSRule = 'Cypress DNS Rule'; +const cypressDNSRule = dns_rule_data.title; describe('Detectors', () => { const indexName = 'cypress-test-dns'; @@ -24,6 +25,7 @@ describe('Detectors', () => { before(() => { cy.cleanUpTests(); + // Create test index cy.createIndex(indexName, sample_index_settings).then(() => cy @@ -41,8 +43,6 @@ describe('Detectors', () => { ); cy.createRule(dns_rule_data); - - cy.contains(detectorName).should('not.exist'); }); beforeEach(() => { @@ -57,6 +57,39 @@ 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 }); + + // Check to ensure process started + cy.waitForPageLoad('create-detector', { + contains: 'Define detector', + }); + + // Select our pre-seeded data source (check indexName) + cy.get(`[data-test-subj="define-detector-select-data-source"]`) + .find('input') + .focus() + .realType(indexName); + + // Select threat detector type (Windows logs) + cy.get(`input[id="dns"]`).click({ force: true }); + + // Select our pre-seeded data source (check indexName) + cy.get(`[data-test-subj="define-detector-select-data-source"]`) + .find('input') + .focus() + .realType(dnsName) + .realPress('Enter'); + + cy.get('.euiCallOut').should('be.visible').contains('Detector configuration warning'); + }); + it('...can be created', () => { // Locate Create detector button click to start cy.get('.euiButton').filter(':contains("Create detector")').click({ force: true }); diff --git a/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldMappingsTable.tsx b/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldMappingsTable.tsx index 1a255ffbd..094dfc8c0 100644 --- a/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldMappingsTable.tsx +++ b/public/pages/CreateDetector/components/ConfigureFieldMapping/components/RequiredFieldMapping/FieldMappingsTable.tsx @@ -12,6 +12,7 @@ import { EuiIcon, EuiInMemoryTable, EuiText, + EuiToolTip, } from '@elastic/eui'; import { DEFAULT_EMPTY_DATA } from '../../../../../../utils/constants'; import { STATUS_ICON_PROPS } from '../../utils/constants'; @@ -140,14 +141,22 @@ export default class FieldMappingsTable extends Compo const { existingMappings: createdMappings, invalidMappingFieldNames } = this.props .mappingProps as MappingProps[MappingViewType.Edit]; let iconProps = STATUS_ICON_PROPS['unmapped']; + let iconTooltip = 'This field needs to be mapped with a field from your log source.'; if ( createdMappings[entry.ruleFieldName] && !invalidMappingFieldNames.includes(entry.ruleFieldName) ) { iconProps = STATUS_ICON_PROPS['mapped']; + iconTooltip = 'This field has been mapped.'; } - return || DEFAULT_EMPTY_DATA; + return ( + ( + + + + ) || DEFAULT_EMPTY_DATA + ); }, }); } diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx index f0bbd221e..5592501cc 100644 --- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx @@ -101,7 +101,6 @@ export default class DetectorDataSource extends Component< error={isInvalid && (errorMessage || 'Select an input source.')} > diff --git a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx index 858e68250..49dded57d 100644 --- a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx @@ -5,13 +5,13 @@ import React, { Component } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; +import { EuiSpacer, EuiTitle, EuiText, EuiCallOut, EuiTextColor } from '@elastic/eui'; import { Detector, PeriodSchedule } from '../../../../../../models/interfaces'; import DetectorBasicDetailsForm from '../components/DetectorDetails'; import DetectorDataSource from '../components/DetectorDataSource'; import DetectorType from '../components/DetectorType'; import { EuiComboBoxOptionOption } from '@opensearch-project/oui'; -import { IndexService } from '../../../../../services'; +import { FieldMappingService, IndexService } from '../../../../../services'; import { MIN_NUM_DATA_SOURCES } from '../../../../Detectors/utils/constants'; import { DetectorCreationStep } from '../../../models/types'; import { DetectorSchedule } from '../components/DetectorSchedule/DetectorSchedule'; @@ -21,11 +21,13 @@ import { DetectionRules, } from '../components/DetectionRules/DetectionRules'; import { NotificationsStart } from 'opensearch-dashboards/public'; +import _ from 'lodash'; interface DefineDetectorProps extends RouteComponentProps { detector: Detector; isEdit: boolean; indexService: IndexService; + filedMappingService: FieldMappingService; rulesState: CreateDetectorRulesState; notifications: NotificationsStart; loadingRules?: boolean; @@ -36,16 +38,66 @@ interface DefineDetectorProps extends RouteComponentProps { onAllRulesToggle: (enabled: boolean) => void; } -interface DefineDetectorState {} +interface DefineDetectorState { + message: string[]; +} export default class DefineDetector extends Component { - updateDetectorCreationState(detector: Detector) { - const isDataValid = + state = { + message: [], + }; + + private indicesMappings: any = {}; + + async updateDetectorCreationState(detector: Detector) { + let 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.updateDataValidState(DetectorCreationStep.DEFINE_DETECTOR, isDataValid); } @@ -162,6 +214,7 @@ export default class DefineDetector extends Component + {message.length ? ( + <> + + {message.map((messageItem: string, index: number) => ( + + {messageItem} +
+
+ ))} +
+ + + ) : null} rule.enabled); - const options: CreateDetectorRulesOptions = enabledRules.map((rule) => ({ + return enabledRules.map((rule) => ({ id: rule._id, name: rule._source.title, severity: rule._source.level, tags: rule._source.tags.map((tag: { value: string }) => tag.value), })); - - return options; } async setupRulesState() { @@ -307,6 +305,7 @@ export default class CreateDetector extends Component spec', () => { it('renders the component', async () => { let wrapper; + await act(async () => { + jest.spyOn(React, 'useContext').mockImplementation(() => servicesContextMock); wrapper = await mount(); }); wrapper.update();