diff --git a/cypress/integration/1_detectors.spec.js b/cypress/integration/1_detectors.spec.js index 092006e45..edd34bd8a 100644 --- a/cypress/integration/1_detectors.spec.js +++ b/cypress/integration/1_detectors.spec.js @@ -336,6 +336,127 @@ describe('Detectors', () => { cy.contains('Active rules (1)'); }); + it('...should update field mappings if data source is changed', () => { + // Click on detector name + cy.contains(detectorName).click({ force: true }); + cy.waitForPageLoad('detector-details', { + contains: detectorName, + }); + + // Click "Edit" button in detector details + cy.get(`[data-test-subj="edit-detector-basic-details"]`).click({ force: true }); + + // Confirm arrival at "Edit detector details" page + cy.waitForPageLoad('edit-detector-details', { + contains: 'Edit detector details', + }); + + cy.get('.reviewFieldMappings').should('not.exist'); + + // Change input source + cy.get(`[data-test-subj="define-detector-select-data-source"]`) + .find('input') + .ospClear() + .focus() + .realType(cypressIndexWindows) + .realPress('Enter'); + + cy.get('.reviewFieldMappings').should('be.visible'); + cy.get('.reviewFieldMappings').within(($el) => { + cy.get($el).contains('Automatically mapped fields (0)'); + }); + + // Change input source + cy.get(`[data-test-subj="define-detector-select-data-source"]`) + .find('input') + .ospClear() + .focus() + .realType(cypressIndexDns) + .realPress('Enter'); + + cy.get('.reviewFieldMappings').should('be.visible'); + cy.get('.reviewFieldMappings').within(($el) => { + cy.get($el).contains('Automatically mapped fields (1)'); + }); + + // Save changes to detector details + cy.get(`[data-test-subj="save-basic-details-edits"]`).click({ force: true }); + }); + + it('...should update field mappings if rule selection is changed', () => { + // Click on detector name + cy.contains(detectorName).click({ force: true }); + cy.waitForPageLoad('detector-details', { + contains: detectorName, + }); + + // Click "Edit" button in detector details + cy.get(`[data-test-subj="edit-detector-rules"]`).click({ force: true }); + + // Confirm arrival at "Edit detector details" page + cy.waitForPageLoad('edit-detector-rules', { + contains: 'Edit detector rules', + }); + + cy.get('.reviewFieldMappings').should('not.exist'); + + // Search for specific rule + cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule); + + cy.intercept('mappings/view').as('getMappingsView'); + + // Toggle single search result to unchecked + cy.contains('table tr', cypressDNSRule).within(() => { + // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case. + cy.wait(1000); + cy.get('button').eq(1).click(); + }); + + cy.wait('@getMappingsView'); + cy.get('.reviewFieldMappings').should('be.visible'); + cy.get('.reviewFieldMappings').within(($el) => { + cy.get($el).contains('Automatically mapped fields (0)'); + }); + + //Suspicious DNS Query with B64 Encoded String + cy.get(`input[placeholder="Search..."]`).ospSearch(cypressDNSRule); + cy.contains('table tr', cypressDNSRule).within(() => { + // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case. + cy.wait(1000); + cy.get('button').eq(1).click(); + }); + + cy.wait('@getMappingsView'); + cy.get(`input[placeholder="Search..."]`).ospSearch( + 'Suspicious DNS Query with B64 Encoded String' + ); + cy.contains('table tr', 'Suspicious DNS Query with B64 Encoded String').within(() => { + // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case. + cy.wait(1000); + cy.get('button').eq(1).click(); + }); + + cy.wait('@getMappingsView'); + cy.get('.reviewFieldMappings').should('be.visible'); + cy.get('.reviewFieldMappings').within(($el) => { + cy.get($el).contains('Automatically mapped fields (1)'); + }); + + cy.get(`input[placeholder="Search..."]`).ospSearch('High TXT Records Requests Rate'); + cy.contains('table tr', 'High TXT Records Requests Rate').within(() => { + // Of note, timeout can sometimes work instead of wait here, but is very unreliable from case to case. + cy.wait(1000); + cy.get('button').eq(1).click(); + }); + + cy.wait('@getMappingsView'); + cy.get('.reviewFieldMappings').should('be.visible'); + cy.get('.reviewFieldMappings').within(($el) => { + cy.get($el).contains('Automatically mapped fields (1)'); + cy.get($el).contains('1 rule fields may need manual mapping'); + }); + }); + it('...can be deleted', () => { // Click on detector to be removed cy.contains('test detector edited').click({ force: true }); diff --git a/public/app.scss b/public/app.scss index ffbc4e3a0..524fdfc11 100644 --- a/public/app.scss +++ b/public/app.scss @@ -13,6 +13,7 @@ $euiTextColor: $euiColorDarkestShade !default; @import "./components/Charts/ChartContainer.scss"; @import "./pages/Overview/components/Widgets/WidgetContainer.scss"; +@import "./pages/Detectors/components/ReviewFieldMappings/ReviewFieldMappings.scss"; .selected-radio-panel { background-color: tintOrShade($euiColorPrimary, 90%, 70%); @@ -119,4 +120,4 @@ $euiTextColor: $euiColorDarkestShade !default; .sa-overview-widget-empty tbody > .euiTableRow > .euiTableRowCell { border-bottom: none; -} +} diff --git a/public/components/ContentPanel/ContentPanel.tsx b/public/components/ContentPanel/ContentPanel.tsx index b4f1ea59f..93794ae95 100644 --- a/public/components/ContentPanel/ContentPanel.tsx +++ b/public/components/ContentPanel/ContentPanel.tsx @@ -22,6 +22,7 @@ interface ContentPanelProps { horizontalRuleClassName?: string; actions?: React.ReactNode | React.ReactNode[]; children: React.ReactNode | React.ReactNode[]; + className?: string; } const renderSubTitleText = (subTitleText: string | JSX.Element): JSX.Element | null => { @@ -45,8 +46,12 @@ const ContentPanel: React.SFC = ({ horizontalRuleClassName = '', actions, children, + className = '', }) => ( - + {typeof title === 'string' ? ( diff --git a/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap b/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap index 050feb771..08f80250c 100644 --- a/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap +++ b/public/pages/Alerts/containers/Alerts/__snapshots__/Alerts.test.tsx.snap @@ -943,6 +943,7 @@ exports[` spec renders the component 1`] = ` title="Alerts" > , + prevState: Readonly, + snapshot?: any + ): void { + if (this.props.selectedField !== prevProps.selectedField) { + // if the props.selectedField is changed, update the state + this.setState({ + selectedOptions: [{ label: this.props.selectedField }], + }); + } + } + onMappingChange = (selectedOptions: EuiComboBoxOptionOption[]) => { this.setState({ selectedOptions }); this.props.onChange(selectedOptions[0]?.label); diff --git a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx index 1be4d27cc..2194d6dc8 100644 --- a/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx +++ b/public/pages/CreateDetector/components/ConfigureFieldMapping/containers/ConfigureFieldMapping.tsx @@ -32,7 +32,7 @@ export interface ruleFieldToIndexFieldMap { interface ConfigureFieldMappingProps extends RouteComponentProps { isEdit: boolean; detector: Detector; - filedMappingService: FieldMappingService; + fieldMappingService: FieldMappingService; fieldMappings: FieldMapping[]; loading: boolean; enabledRules: CreateDetectorRulesState['allRules']; @@ -82,7 +82,7 @@ export default class ConfigureFieldMapping extends Component< getAllMappings = async () => { this.setState({ loading: true }); - const mappingsView = await this.props.filedMappingService.getMappingsView( + const mappingsView = await this.props.fieldMappingService.getMappingsView( this.props.detector.inputs[0].detector_input.indices[0], this.props.detector.detector_type.toLowerCase() ); diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces.ts b/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces.ts index 6977ff601..1d7858523 100644 --- a/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces.ts +++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectionRules/types/interfaces.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { RuleItemInfoBase } from '../../../../../../Rules/models/types'; -import { RuleInfo } from './../../../../../../../../server/models/interfaces/Rules'; +import { RuleInfo } from '../../../../../../../../server/models/interfaces'; +import { RuleItemInfoBase } from '../../../../../../../../types'; export interface RuleItem { name: string; diff --git a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx index 1f19fba0a..7aa617ec8 100644 --- a/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/components/DetectorDataSource/DetectorDataSource.tsx @@ -25,7 +25,7 @@ import { FieldMappingService } from '../../../../../../services'; interface DetectorDataSourceProps { detectorIndices: string[]; indexService: IndexService; - filedMappingService: FieldMappingService; + fieldMappingService: FieldMappingService; isEdit: boolean; onDetectorInputIndicesChange: (selectedOptions: EuiComboBoxOptionOption[]) => void; notifications: NotificationsStart; @@ -109,7 +109,7 @@ export default class DetectorDataSource extends Component< for (const indexName of allIndices) { if (!this.indicesMappings[indexName]) { const detectorType = this.props.detector_type.toLowerCase(); - const result = await this.props.filedMappingService.getMappingsView( + const result = await this.props.fieldMappingService.getMappingsView( indexName, detectorType ); diff --git a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx index 474288563..a42326c40 100644 --- a/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx +++ b/public/pages/CreateDetector/components/DefineDetector/containers/DefineDetector.tsx @@ -28,7 +28,7 @@ interface DefineDetectorProps extends RouteComponentProps { detector: Detector; isEdit: boolean; indexService: IndexService; - filedMappingService: FieldMappingService; + fieldMappingService: FieldMappingService; rulesState: CreateDetectorRulesState; notifications: NotificationsStart; loadingRules?: boolean; @@ -165,7 +165,7 @@ export default class DefineDetector extends Component diff --git a/public/pages/CreateDetector/containers/CreateDetector.tsx b/public/pages/CreateDetector/containers/CreateDetector.tsx index e71a2b1d5..ee00219ae 100644 --- a/public/pages/CreateDetector/containers/CreateDetector.tsx +++ b/public/pages/CreateDetector/containers/CreateDetector.tsx @@ -289,7 +289,7 @@ export default class CreateDetector extends Component rule.enabled)} replaceFieldMappings={this.replaceFieldMappings} diff --git a/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap b/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap index 6598f2bc1..90549eaa3 100644 --- a/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap +++ b/public/pages/Detectors/components/DetectorRulesView/__snapshots__/DetectorRulesView.test.tsx.snap @@ -203,6 +203,7 @@ exports[` spec renders the component 1`] = ` title="Active rules (2)" > { + detector: Detector; + fieldMappingService?: FieldMappingService; + notifications: NotificationsStart; + onFieldMappingChange: (fieldMappings: FieldMapping[]) => void; + ruleQueryFields?: Set; +} + +export interface UpdateFieldMappingsState { + fieldMappings: FieldMapping[]; + loading: boolean; + ruleQueryFields?: Set; +} + +export default class ReviewFieldMappings extends Component< + UpdateFieldMappingsProps, + UpdateFieldMappingsState +> { + static contextType = CoreServicesContext; + + constructor(props: UpdateFieldMappingsProps) { + super(props); + + this.state = { + ruleQueryFields: props.ruleQueryFields ? props.ruleQueryFields : new Set(), + fieldMappings: [], + loading: false, + }; + } + + public componentDidUpdate( + prevProps: Readonly, + prevState: Readonly, + snapshot?: any + ): void { + if (prevProps.ruleQueryFields !== this.props.ruleQueryFields) { + this.setState({ + ruleQueryFields: this.props.ruleQueryFields, + }); + } + } + + replaceFieldMappings = (fieldMappings: FieldMapping[]): void => { + this.setState({ fieldMappings }, () => this.props.onFieldMappingChange(fieldMappings)); + }; + + render() { + const { detector, fieldMappingService } = this.props; + const { fieldMappings = [], loading, ruleQueryFields = new Set([]) } = this.state; + return ( + + +

New field mappings for your detector changes

+
+ + + When adding new log data sources, you may need to map additional log fields to rule + field names. To perform threat detection, known field names from your log data source + are automatically mapped to rule field names. Additional fields that may require + manual mapping will be shown below. + + + } + > + {!loading && ( + + )} +
+ ); + } +} diff --git a/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx b/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx index 6ff215498..7b87e59c7 100644 --- a/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx +++ b/public/pages/Detectors/components/UpdateBasicDetails/UpdateBasicDetails.tsx @@ -16,7 +16,7 @@ import React, { useContext, useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import DetectorBasicDetailsForm from '../../../CreateDetector/components/DefineDetector/components/DetectorDetails'; import DetectorDataSource from '../../../CreateDetector/components/DefineDetector/components/DetectorDataSource'; -import { IndexService, ServicesContext } from '../../../../services'; +import { FieldMappingService, IndexService, ServicesContext } from '../../../../services'; import { DetectorSchedule } from '../../../CreateDetector/components/DefineDetector/components/DetectorSchedule/DetectorSchedule'; import { useCallback } from 'react'; import { DetectorHit, SearchDetectorsResponse } from '../../../../../server/models/interfaces'; @@ -25,7 +25,8 @@ import { ServerResponse } from '../../../../../server/models/types'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { errorNotificationToast, successNotificationToast } from '../../../../utils/helpers'; import { CoreServicesContext } from '../../../../components/core_services'; -import { Detector } from '../../../../../types'; +import ReviewFieldMappings from '../ReviewFieldMappings/ReviewFieldMappings'; +import { FieldMapping, Detector } from '../../../../../types'; export interface UpdateDetectorBasicDetailsProps extends RouteComponentProps { @@ -35,11 +36,13 @@ export interface UpdateDetectorBasicDetailsProps export const UpdateDetectorBasicDetails: React.FC = (props) => { const services = useContext(ServicesContext); const [detector, setDetector] = useState( - props.location.state?.detectorHit?._source || EMPTY_DEFAULT_DETECTOR + (props.location.state?.detectorHit?._source || EMPTY_DEFAULT_DETECTOR) as Detector ); + const [fieldMappings, setFieldMappings] = useState(); const { name, inputs } = detector; const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); + const [fieldMappingsIsVisible, setFieldMappingsIsVisible] = useState(false); const description = inputs[0].detector_input.description; const detectorId = props.location.pathname.replace(`${ROUTES.EDIT_DETECTOR_DETAILS}/`, ''); @@ -54,7 +57,7 @@ export const UpdateDetectorBasicDetails: React.FC detectorHit._id === detectorId ) as DetectorHit; - setDetector(detectorHit._source); + setDetector(detectorHit._source as Detector); context?.chrome.setBreadcrumbs([ BREADCRUMBS.SECURITY_ANALYTICS, @@ -67,7 +70,10 @@ export const UpdateDetectorBasicDetails: React.FC { + setFieldMappings(mappings); + }, + [setFieldMappings] + ); + const onDetectorNameChange = useCallback( (detectorName: string) => { const newDetector: Detector = { @@ -146,9 +159,10 @@ export const UpdateDetectorBasicDetails: React.FC { + const updatedFields = [...fields]; + updateFieldMappingsState(updatedFields); + }, + [fieldMappings, updateFieldMappingsState] + ); + const onCancel = useCallback(() => { props.history.replace({ pathname: `${ROUTES.DETECTOR_DETAILS}/${detectorId}`, @@ -170,11 +192,11 @@ export const UpdateDetectorBasicDetails: React.FC { - const detectorHit = props.location.state.detectorHit; + const onSave = useCallback(async () => { + setSubmitting(true); const updateDetector = async () => { - setSubmitting(true); + const detectorHit = props.location.state.detectorHit; const updateDetectorRes = await services?.detectorsService?.updateDetector( detectorHit._id, detector @@ -187,18 +209,39 @@ export const UpdateDetectorBasicDetails: React.FC { - errorNotificationToast(props.notifications, 'update', 'detector', e); - }); - }, [detector]); + if (fieldMappings?.length) { + const createMappingsResponse = await services?.fieldMappingService?.createMappings( + detector.inputs[0].detector_input.indices[0], + detector.detector_type.toLowerCase(), + fieldMappings + ); + + if (!createMappingsResponse?.ok) { + errorNotificationToast( + props.notifications, + 'update', + 'field mappings', + createMappingsResponse?.error + ); + } else { + await updateDetector(); + } + } else { + await updateDetector(); + } + }, [detector, fieldMappings]); return (
@@ -222,7 +265,7 @@ export const UpdateDetectorBasicDetails: React.FC @@ -230,6 +273,18 @@ export const UpdateDetectorBasicDetails: React.FC + {fieldMappingsIsVisible ? ( + <> + + + + ) : null} + 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 a787a5377..2c3482053 100644 --- a/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap +++ b/public/pages/Detectors/components/UpdateBasicDetails/__snapshots__/UpdateDetectorBasicDetails.test.tsx.snap @@ -609,6 +609,7 @@ exports[` spec renders the component 1`] = ` titleSize="m" > spec renders the component 1`] = ` title="Detector schedule" > { detectorService: DetectorsService; - filedMappingService: FieldMappingService; + fieldMappingService: FieldMappingService; notifications: NotificationsStart; } @@ -111,12 +111,12 @@ export default class UpdateFieldMappings extends Component< state: { detectorHit }, }, detectorService, - filedMappingService, + fieldMappingService, } = this.props; const { detector, fieldMappings } = this.state; try { - const createMappingsResponse = await filedMappingService.createMappings( + const createMappingsResponse = await fieldMappingService.createMappings( detector.inputs[0].detector_input.indices[0], detector.detector_type.toLowerCase(), fieldMappings @@ -162,7 +162,7 @@ export default class UpdateFieldMappings extends Component< }; render() { - const { filedMappingService } = this.props; + const { fieldMappingService } = this.props; const { submitting, detector, fieldMappings, loading } = this.state; return (
@@ -184,7 +184,7 @@ export default class UpdateFieldMappings extends Component< {...this.props} detector={detector} fieldMappings={fieldMappings} - filedMappingService={filedMappingService} + fieldMappingService={fieldMappingService} replaceFieldMappings={this.replaceFieldMappings} loading={loading} /> diff --git a/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx b/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx index b53748e6d..b7c93ce4c 100644 --- a/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx +++ b/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx @@ -23,7 +23,8 @@ import { RuleTableItem } from '../../../Rules/utils/helpers'; import { RuleViewerFlyout } from '../../../Rules/components/RuleViewerFlyout/RuleViewerFlyout'; import { ContentPanel } from '../../../../components/ContentPanel'; import { DataStore } from '../../../../store/DataStore'; -import { Detector } from '../../../../../types'; +import ReviewFieldMappings from '../ReviewFieldMappings/ReviewFieldMappings'; +import { FieldMapping, Detector } from '../../../../../types'; export interface UpdateDetectorRulesProps extends RouteComponentProps< @@ -38,11 +39,14 @@ export const UpdateDetectorRules: React.FC = (props) = const services = useContext(ServicesContext); const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); - const [detector, setDetector] = useState(EMPTY_DEFAULT_DETECTOR); + const [detector, setDetector] = useState(EMPTY_DEFAULT_DETECTOR as Detector); const [customRuleItems, setCustomRuleItems] = useState([]); const [prePackagedRuleItems, setPrePackagedRuleItems] = useState([]); const detectorId = props.location.pathname.replace(`${ROUTES.EDIT_DETECTOR_RULES}/`, ''); const [flyoutData, setFlyoutData] = useState(null); + const [fieldMappings, setFieldMappings] = useState(); + const [fieldMappingsIsVisible, setFieldMappingsIsVisible] = useState(false); + const [ruleQueryFields, setRuleQueryFields] = useState>(); const context = useContext(CoreServicesContext); @@ -56,7 +60,7 @@ export const UpdateDetectorRules: React.FC = (props) = const detectorHit = response.response.hits.hits.find( (detectorHit) => detectorHit._id === detectorId ) as DetectorHit; - const newDetector = { ...detectorHit._source, id: detectorId }; + const newDetector = { ...detectorHit._source, id: detectorId } as Detector; setDetector(newDetector); context?.chrome.setBreadcrumbs([ @@ -123,30 +127,46 @@ export const UpdateDetectorRules: React.FC = (props) = }); }, [services, detectorId]); - const onToggle = (changedItem: RuleItem, isActive: boolean) => { + const onToggle = async (changedItem: RuleItem, isActive: boolean) => { + setFieldMappingsIsVisible(true); switch (changedItem.library) { case 'Custom': - setCustomRuleItems( - customRuleItems.map((rule) => - rule.id === changedItem.id ? { ...rule, active: isActive } : rule - ) + const updatedCustomRules: RuleItem[] = customRuleItems.map((rule) => + rule.id === changedItem.id ? { ...rule, active: isActive } : rule ); + setCustomRuleItems(updatedCustomRules); + const withCustomRulesUpdated = prePackagedRuleItems + .concat(updatedCustomRules) + .filter((rule) => rule.active); + await getRuleFieldsForEnabledRules(withCustomRulesUpdated); break; case 'Sigma': - setPrePackagedRuleItems( - prePackagedRuleItems.map((rule) => - rule.id === changedItem.id ? { ...rule, active: isActive } : rule - ) + const updatedPrePackgedRules: RuleItem[] = prePackagedRuleItems.map((rule) => + rule.id === changedItem.id ? { ...rule, active: isActive } : rule ); + setPrePackagedRuleItems(updatedPrePackgedRules); + const withPrePackagedRulesUpdated = updatedPrePackgedRules + .concat(customRuleItems) + .filter((rule) => rule.active); + await getRuleFieldsForEnabledRules(withPrePackagedRulesUpdated); break; default: console.warn('Unsupported rule library detected.'); } }; - const onAllRulesToggle = (isActive: boolean) => { - setCustomRuleItems(customRuleItems.map((rule) => ({ ...rule, active: isActive }))); - setPrePackagedRuleItems(prePackagedRuleItems.map((rule) => ({ ...rule, active: isActive }))); + const onAllRulesToggle = async (isActive: boolean) => { + setFieldMappingsIsVisible(true); + const customRules: RuleItem[] = customRuleItems.map((rule) => ({ ...rule, active: isActive })); + const prePackagedRules: RuleItem[] = prePackagedRuleItems.map((rule) => ({ + ...rule, + active: isActive, + })); + setCustomRuleItems(customRules); + setPrePackagedRuleItems(prePackagedRules); + + const enabledRules = prePackagedRules.concat(customRules); + await getRuleFieldsForEnabledRules(enabledRules); }; const onCancel = useCallback(() => { @@ -158,7 +178,8 @@ export const UpdateDetectorRules: React.FC = (props) = const onSave = async () => { setSubmitting(true); - try { + + const updateDetector = async () => { const newDetector = { ...detector }; newDetector.inputs[0].detector_input.custom_rules = customRuleItems .filter((rule) => rule.active) @@ -178,9 +199,33 @@ export const UpdateDetectorRules: React.FC = (props) = successNotificationToast(props.notifications, 'updated', 'detector'); } + setSubmitting(false); props.history.replace({ pathname: `${ROUTES.DETECTOR_DETAILS}/${detectorId}`, }); + }; + + try { + if (fieldMappings?.length) { + const createMappingsResponse = await services?.fieldMappingService?.createMappings( + detector.inputs[0].detector_input.indices[0], + detector.detector_type.toLowerCase(), + fieldMappings + ); + + if (!createMappingsResponse?.ok) { + errorNotificationToast( + props.notifications, + 'update', + 'field mappings', + createMappingsResponse?.error + ); + } else { + await updateDetector(); + } + } else { + await updateDetector(); + } } catch (e: any) { errorNotificationToast(props.notifications, 'update', 'detector', e); } @@ -200,6 +245,37 @@ export const UpdateDetectorRules: React.FC = (props) = ruleId: ruleItem.id, })); }; + + const updateFieldMappingsState = useCallback( + (mapping: FieldMapping[]) => { + setFieldMappings(mapping); + }, + [setFieldMappings] + ); + + const onFieldMappingChange = useCallback( + (fields: FieldMapping[]) => { + const updatedFields = [...fields]; + updateFieldMappingsState(updatedFields); + }, + [fieldMappings, updateFieldMappingsState] + ); + + const getRuleFieldsForEnabledRules = useCallback( + async (enabledRules) => { + const ruleFieldsForEnabledRules = new Set(); + enabledRules.forEach((rule: RuleItem) => { + const fieldNames = rule.ruleInfo._source.query_field_names; + fieldNames.forEach((fieldname: { value: string }) => { + ruleFieldsForEnabledRules.add(fieldname.value); + }); + }); + + setRuleQueryFields(ruleFieldsForEnabledRules); + }, + [DataStore.rules.getAllRules, setRuleQueryFields] + ); + return (
{flyoutData ? ( @@ -229,6 +305,18 @@ export const UpdateDetectorRules: React.FC = (props) = + {fieldMappingsIsVisible ? ( + + ) : null} + + + diff --git a/public/pages/Detectors/components/UpdateRules/__snapshots__/UpdateDetectorRules.test.tsx.snap b/public/pages/Detectors/components/UpdateRules/__snapshots__/UpdateDetectorRules.test.tsx.snap index bfb5cdf4d..b158b0934 100644 --- a/public/pages/Detectors/components/UpdateRules/__snapshots__/UpdateDetectorRules.test.tsx.snap +++ b/public/pages/Detectors/components/UpdateRules/__snapshots__/UpdateDetectorRules.test.tsx.snap @@ -336,6 +336,9 @@ Object {
+
@@ -714,6 +717,9 @@ Object {
+
diff --git a/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap b/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap index 4b134ef5d..5d4b67e59 100644 --- a/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap +++ b/public/pages/Detectors/containers/AlertTriggersView/__snapshots__/AlertTriggersView.test.tsx.snap @@ -200,6 +200,7 @@ exports[` spec renders the component 1`] = ` title="Alert triggers (2)" > spec renders the component 1`] = ` title="Detector details" > spec renders the component 1`] = ` title="Active rules (2)" > spec renders the component 1`] = ` title="Detector details" > spec renders the component 1`] = ` title="Active rules (2)" > void; + initialIsOpen?: boolean; + ruleQueryFields?: Set; } interface EditFieldMappingsState { @@ -39,6 +42,7 @@ interface EditFieldMappingsState { mappedRuleFields: string[]; unmappedRuleFields: string[]; logFieldOptions: string[]; + ruleQueryFields?: Set; } export default class EditFieldMappings extends Component< @@ -52,6 +56,7 @@ export default class EditFieldMappings extends Component< createdMappings[mapping.ruleFieldName] = mapping.indexFieldName; }); this.state = { + ruleQueryFields: props.ruleQueryFields ? props.ruleQueryFields : new Set(), loading: props.loading || false, createdMappings, invalidMappingFieldNames: [], @@ -65,42 +70,95 @@ export default class EditFieldMappings extends Component< this.getAllMappings(); }; + public componentDidUpdate( + prevProps: Readonly, + prevState: Readonly, + snapshot?: any + ): void { + const indexVariablePath = 'detector.inputs[0].detector_input.indices[0]'; + const currentIndex: string = _.get(this.props, indexVariablePath); + const previousIndex: string = _.get(prevProps, indexVariablePath); + + // if index is changed reload mappings + if (!!currentIndex && currentIndex !== previousIndex) { + this.getAllMappings(); + } + + // if rule selection is changed reload mappings + if (prevProps.ruleQueryFields !== this.props.ruleQueryFields) { + this.setState( + { + // update ruleQueryField, this is used to filter field mappings based on rule selection + ruleQueryFields: this.props.ruleQueryFields, + }, + () => this.getAllMappings() + ); + } + } + getAllMappings = async () => { this.setState({ loading: true }); const indexName = this.props.detector.inputs[0].detector_input.indices[0]; + if (indexName) { + const mappingsViewRes = await this.props.fieldMappingService?.getMappingsView( + indexName, + this.props.detector.detector_type.toLowerCase() + ); - const mappingsViewRes = await this.props.filedMappingService.getMappingsView( - indexName, - this.props.detector.detector_type.toLowerCase() - ); - - if (mappingsViewRes.ok) { - const mappingsRes = await this.props.filedMappingService.getMappings(indexName); - if (mappingsRes.ok) { - const mappedFieldsInfo = mappingsRes.response[indexName].mappings.properties; - const mappedRuleFields = Object.keys(mappedFieldsInfo); - const unmappedRuleFields = (mappingsViewRes.response.unmapped_field_aliases || []).filter( - (ruleField) => { - return !mappedRuleFields.includes(ruleField); - } - ); - + if (mappingsViewRes?.ok) { + let unmappedRuleFields = mappingsViewRes.response.unmapped_field_aliases || []; const logFieldsSet = new Set(mappingsViewRes.response.unmapped_index_fields); Object.values(mappingsViewRes.response.properties).forEach((val) => { logFieldsSet.add(val.path); }); const logFieldOptions = Array.from(logFieldsSet); const existingMappings = { ...this.state.createdMappings }; - mappedRuleFields.forEach((ruleField) => { - existingMappings[ruleField] = mappedFieldsInfo[ruleField].path; - }); - this.setState({ - mappedRuleFields, - unmappedRuleFields, - logFieldOptions, - createdMappings: existingMappings, - }); + const mappingsRes = await this.props.fieldMappingService?.getMappings(indexName); + if (mappingsRes?.ok) { + const mappedFieldsInfo = mappingsRes.response[indexName].mappings.properties; + let mappedRuleFields = Object.keys(mappedFieldsInfo); + unmappedRuleFields = unmappedRuleFields.filter((ruleField) => { + return !mappedRuleFields.includes(ruleField); + }); + + mappedRuleFields.forEach((ruleField) => { + existingMappings[ruleField] = mappedFieldsInfo[ruleField].path; + }); + + for (let key in existingMappings) { + if (logFieldOptions.indexOf(existingMappings[key]) === -1) { + delete existingMappings[key]; + } + } + + if (this.state.ruleQueryFields?.size) { + mappedRuleFields = _.intersection(mappedRuleFields, [...this.state.ruleQueryFields]); + unmappedRuleFields = _.intersection(unmappedRuleFields, [ + ...this.state.ruleQueryFields, + ]); + } + + this.setState({ + mappedRuleFields, + unmappedRuleFields, + logFieldOptions, + createdMappings: existingMappings, + }); + } else { + if (this.state.ruleQueryFields?.size) { + unmappedRuleFields = _.intersection(unmappedRuleFields, [ + ...this.state.ruleQueryFields, + ]); + } + + this.setState({ + mappedRuleFields: [], + unmappedRuleFields, + logFieldOptions, + createdMappings: existingMappings, + }); + } } } this.setState({ loading: false }); @@ -160,12 +218,13 @@ export default class EditFieldMappings extends Component< unmappedRuleFields, logFieldOptions, } = this.state; + const { initialIsOpen } = this.props; const existingMappings: ruleFieldToIndexFieldMap = { ...createdMappings, }; return ( -
+
@@ -226,8 +287,13 @@ export default class EditFieldMappings extends Component< ) : ( - -

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

+ +

+ Your data sources have been mapped with every rule field name. No action is needed. +

)} 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 e44d1149f..bc1c033e1 100644 --- a/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap +++ b/public/pages/Detectors/containers/FieldMappings/__snapshots__/EditFieldMappings.test.tsx.snap @@ -190,7 +190,9 @@ exports[` spec renders the component 1`] = ` loading={false} replaceFieldMappings={[MockFunction]} > -
+
spec renders the component 1`] = `
spec renders the component 1`] = ` - All rule fields have been mapped + We have automatically mapped 0 field(s)
spec renders the component 1`] = ` className="euiTextColor euiTextColor--default" >

- Your data source have been mapped with all security rule fields. + Your data sources have been mapped with every rule field name. No action is needed.

diff --git a/public/pages/Main/Main.tsx b/public/pages/Main/Main.tsx index 9c3cad9d0..e4e3ef65b 100644 --- a/public/pages/Main/Main.tsx +++ b/public/pages/Main/Main.tsx @@ -241,7 +241,6 @@ export default class Main extends Component { findingsService={services.findingsService} opensearchService={services.opensearchService} detectorService={services.detectorsService} - ruleService={services.ruleService} notificationsService={services.notificationsService} indexPatternsService={services.indexPatternsService} notifications={core?.notifications} @@ -353,7 +352,6 @@ export default class Main extends Component { alertService={services.alertService} detectorService={services.detectorsService} findingService={services.findingsService} - ruleService={services.ruleService} notifications={core?.notifications} opensearchService={services.opensearchService} /> @@ -390,7 +388,7 @@ export default class Main extends Component { render={(props: RouteComponentProps) => ( @@ -402,7 +400,6 @@ export default class Main extends Component {