From 8da80fe82781bdf86f8e3c369dd66bab75102a71 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Tue, 14 Jul 2020 15:39:26 -0600 Subject: [PATCH] [Security] Adds field mapping support to rule creation Part II (#71402) ## Summary Followup to https://github.com/elastic/kibana/pull/70288, which includes: - [X] Rule Execution logic for: - [X] Severity Override - [X] Risk Score Override - [X] Rule Name Override - [X] Timestamp Override - [X] Support for toggling display of Building Block Rules: - [X] Main Detections Page - [X] Rule Details Page - [X] Integrates `AutocompleteField` for: - [X] Severity Override - [X] Risk Score Override - [X] Rule Name Override - [X] Timestamp Override - [X] Fixes rehydration of `EditAboutStep` in `Edit Rule` - [X] Fixes `Rule Details` Description rollup Additional followup cleanup: - [ ] Adds risk_score` to `risk_score_mapping` - [ ] Improves field validation - [ ] Disables override fields for ML Rules - [ ] Orders `SeverityMapping` by `severity` on create/update - [ ] Allow unbounded max-signals ### Checklist Delete any items that are not applicable to this PR. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials - Syncing w/ @benskelker - [X] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ### For maintainers - [X] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../schemas/common/schemas.ts | 1 + .../common/components/autocomplete/field.tsx | 16 ++- .../autocomplete/field_value_match.tsx | 3 + .../common/components/utility_bar/index.ts | 1 + .../common/components/utility_bar/styles.tsx | 33 ++++- .../utility_bar/utility_bar_group.tsx | 8 +- .../utility_bar/utility_bar_section.tsx | 8 +- .../utility_bar/utility_bar_spacer.tsx | 19 +++ .../alerts_utility_bar/index.test.tsx | 2 + .../alerts_table/alerts_utility_bar/index.tsx | 42 ++++++- .../alerts_utility_bar/translations.ts | 14 +++ .../alerts_table/default_config.tsx | 19 +++ .../components/alerts_table/index.test.tsx | 2 + .../components/alerts_table/index.tsx | 8 ++ .../components/alerts_table/translations.ts | 6 +- .../rules/autocomplete_field/index.tsx | 75 +++++++++++ .../rules/description_step/helpers.test.tsx | 17 ++- .../rules/description_step/helpers.tsx | 75 ++++++++++- .../rules/description_step/index.test.tsx | 4 +- .../rules/description_step/index.tsx | 15 +-- .../rules/risk_score_mapping/index.tsx | 103 ++++++++++----- .../rules/risk_score_mapping/translations.tsx | 7 ++ .../rules/severity_mapping/index.tsx | 119 ++++++++++++++---- .../rules/severity_mapping/translations.tsx | 7 ++ .../rules/step_about_rule/index.tsx | 69 +++++----- .../rules/step_about_rule/translations.ts | 6 + .../detection_engine/detection_engine.tsx | 27 +++- .../rules/create/helpers.test.ts | 8 -- .../detection_engine/rules/create/helpers.ts | 4 +- .../detection_engine/rules/details/index.tsx | 29 ++++- .../detection_engine/rules/edit/index.tsx | 3 +- .../pages/detection_engine/rules/types.ts | 4 +- .../signals/build_bulk_body.ts | 1 + .../signals/build_events_query.test.ts | 6 + .../signals/build_events_query.ts | 11 +- .../signals/build_rule.test.ts | 5 +- .../detection_engine/signals/build_rule.ts | 34 ++++- .../signals/find_threshold_signals.ts | 1 + .../build_risk_score_from_mapping.test.ts | 26 ++++ .../mappings/build_risk_score_from_mapping.ts | 42 +++++++ .../build_rule_name_from_mapping.test.ts | 26 ++++ .../mappings/build_rule_name_from_mapping.ts | 40 ++++++ .../build_severity_from_mapping.test.ts | 26 ++++ .../mappings/build_severity_from_mapping.ts | 50 ++++++++ .../signals/search_after_bulk_create.ts | 1 + .../signals/single_search_after.test.ts | 3 + .../signals/single_search_after.ts | 4 + 47 files changed, 874 insertions(+), 156 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_spacer.tsx create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 542cbe89160329..273ea72a2ffe30 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -255,6 +255,7 @@ export const severity_mapping_item = t.exact( severity, }) ); +export type SeverityMappingItem = t.TypeOf; export const severity_mapping = t.array(severity_mapping_item); export type SeverityMapping = t.TypeOf; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx index 8a6f049c960372..ed844b5130c77f 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx @@ -17,6 +17,7 @@ interface OperatorProps { isLoading: boolean; isDisabled: boolean; isClearable: boolean; + fieldTypeFilter?: string[]; fieldInputWidth?: number; onChange: (a: IFieldType[]) => void; } @@ -28,13 +29,22 @@ export const FieldComponent: React.FC = ({ isLoading = false, isDisabled = false, isClearable = false, + fieldTypeFilter = [], fieldInputWidth = 190, onChange, }): JSX.Element => { const getLabel = useCallback((field): string => field.name, []); - const optionsMemo = useMemo((): IFieldType[] => (indexPattern ? indexPattern.fields : []), [ - indexPattern, - ]); + const optionsMemo = useMemo((): IFieldType[] => { + if (indexPattern != null) { + if (fieldTypeFilter.length > 0) { + return indexPattern.fields.filter((f) => fieldTypeFilter.includes(f.type)); + } else { + return indexPattern.fields; + } + } else { + return []; + } + }, [fieldTypeFilter, indexPattern]); const selectedOptionsMemo = useMemo((): IFieldType[] => (selectedField ? [selectedField] : []), [ selectedField, ]); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx index 4d96d6638132b3..32a82af114baed 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx @@ -22,6 +22,7 @@ interface AutocompleteFieldMatchProps { isLoading: boolean; isDisabled: boolean; isClearable: boolean; + fieldInputWidth?: number; onChange: (arg: string) => void; } @@ -33,6 +34,7 @@ export const AutocompleteFieldMatchComponent: React.FC { const [isLoadingSuggestions, suggestions, updateSuggestions] = useFieldValueAutocomplete({ @@ -97,6 +99,7 @@ export const AutocompleteFieldMatchComponent: React.FC diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/index.ts b/x-pack/plugins/security_solution/public/common/components/utility_bar/index.ts index b07fe8bb847c7b..44e19a951b6acd 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/index.ts @@ -8,4 +8,5 @@ export { UtilityBar } from './utility_bar'; export { UtilityBarAction } from './utility_bar_action'; export { UtilityBarGroup } from './utility_bar_group'; export { UtilityBarSection } from './utility_bar_section'; +export { UtilityBarSpacer } from './utility_bar_spacer'; export { UtilityBarText } from './utility_bar_text'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx index e1554da491a8b3..dd6b66350052e7 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/styles.tsx @@ -14,6 +14,14 @@ export interface BarProps { border?: boolean; } +export interface BarSectionProps { + grow?: boolean; +} + +export interface BarGroupProps { + grow?: boolean; +} + export const Bar = styled.aside.attrs({ className: 'siemUtilityBar', })` @@ -36,8 +44,8 @@ Bar.displayName = 'Bar'; export const BarSection = styled.div.attrs({ className: 'siemUtilityBar__section', -})` - ${({ theme }) => css` +})` + ${({ grow, theme }) => css` & + & { margin-top: ${theme.eui.euiSizeS}; } @@ -53,14 +61,18 @@ export const BarSection = styled.div.attrs({ margin-left: ${theme.eui.euiSize}; } } + ${grow && + css` + flex: 1; + `} `} `; BarSection.displayName = 'BarSection'; export const BarGroup = styled.div.attrs({ className: 'siemUtilityBar__group', -})` - ${({ theme }) => css` +})` + ${({ grow, theme }) => css` align-items: flex-start; display: flex; flex-wrap: wrap; @@ -93,6 +105,10 @@ export const BarGroup = styled.div.attrs({ margin-right: 0; } } + ${grow && + css` + flex: 1; + `} `} `; BarGroup.displayName = 'BarGroup'; @@ -118,3 +134,12 @@ export const BarAction = styled.div.attrs({ `} `; BarAction.displayName = 'BarAction'; + +export const BarSpacer = styled.div.attrs({ + className: 'siemUtilityBar__spacer', +})` + ${() => css` + flex: 1; + `} +`; +BarSpacer.displayName = 'BarSpacer'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_group.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_group.tsx index 723035df672a93..d67be4882ceec1 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_group.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_group.tsx @@ -6,14 +6,14 @@ import React from 'react'; -import { BarGroup } from './styles'; +import { BarGroup, BarGroupProps } from './styles'; -export interface UtilityBarGroupProps { +export interface UtilityBarGroupProps extends BarGroupProps { children: React.ReactNode; } -export const UtilityBarGroup = React.memo(({ children }) => ( - {children} +export const UtilityBarGroup = React.memo(({ grow, children }) => ( + {children} )); UtilityBarGroup.displayName = 'UtilityBarGroup'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx index 42532c03556073..d88ec35f977c34 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx @@ -6,14 +6,14 @@ import React from 'react'; -import { BarSection } from './styles'; +import { BarSection, BarSectionProps } from './styles'; -export interface UtilityBarSectionProps { +export interface UtilityBarSectionProps extends BarSectionProps { children: React.ReactNode; } -export const UtilityBarSection = React.memo(({ children }) => ( - {children} +export const UtilityBarSection = React.memo(({ grow, children }) => ( + {children} )); UtilityBarSection.displayName = 'UtilityBarSection'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_spacer.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_spacer.tsx new file mode 100644 index 00000000000000..f57b300266f7b3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_spacer.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { BarSpacer } from './styles'; + +export interface UtilityBarSpacerProps { + dataTestSubj?: string; +} + +export const UtilityBarSpacer = React.memo(({ dataTestSubj }) => ( + +)); + +UtilityBarSpacer.displayName = 'UtilityBarSpacer'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx index 7c884d773209ad..cbbe43cc03568f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx @@ -24,6 +24,8 @@ describe('AlertsUtilityBar', () => { currentFilter="closed" selectAll={jest.fn()} showClearSelection={true} + showBuildingBlockAlerts={false} + onShowBuildingBlockAlertsChanged={jest.fn()} updateAlertsStatus={jest.fn()} /> ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index 6533be1a9b09c9..bedc23790541c2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -8,8 +8,9 @@ import { isEmpty } from 'lodash/fp'; import React, { useCallback } from 'react'; import numeral from '@elastic/numeral'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiCheckbox } from '@elastic/eui'; import styled from 'styled-components'; + import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { Link } from '../../../../common/components/link_icon'; import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; @@ -18,6 +19,7 @@ import { UtilityBarAction, UtilityBarGroup, UtilityBarSection, + UtilityBarSpacer, UtilityBarText, } from '../../../../common/components/utility_bar'; import * as i18n from './translations'; @@ -34,6 +36,8 @@ interface AlertsUtilityBarProps { currentFilter: Status; selectAll: () => void; selectedEventIds: Readonly>; + showBuildingBlockAlerts: boolean; + onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; showClearSelection: boolean; totalCount: number; updateAlertsStatus: UpdateAlertsStatus; @@ -52,6 +56,8 @@ const AlertsUtilityBarComponent: React.FC = ({ selectedEventIds, currentFilter, selectAll, + showBuildingBlockAlerts, + onShowBuildingBlockAlertsChanged, showClearSelection, updateAlertsStatus, }) => { @@ -125,17 +131,36 @@ const AlertsUtilityBarComponent: React.FC = ({ ); + const UtilityBarAdditionalFiltersContent = (closePopover: () => void) => ( + + + ) => { + closePopover(); + onShowBuildingBlockAlertsChanged(e.target.checked); + }} + checked={showBuildingBlockAlerts} + color="text" + data-test-subj="showBuildingBlockAlertsCheckbox" + label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK} + /> + + + ); + return ( <> - + {i18n.SHOWING_ALERTS(formattedTotalCount, totalCount)} - + {canUserCRUD && hasIndexWrite && ( <> @@ -174,6 +199,17 @@ const AlertsUtilityBarComponent: React.FC = ({ )} + + + {i18n.ADDITIONAL_FILTERS_ACTIONS} + diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts index 51e1b6f6e4c46a..eb4ca405b084e2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/translations.ts @@ -27,6 +27,20 @@ export const SELECT_ALL_ALERTS = (totalAlertsFormatted: string, totalAlerts: num 'Select all {totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}', }); +export const ADDITIONAL_FILTERS_ACTIONS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersTitle', + { + defaultMessage: 'Additional filters', + } +); + +export const ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.utilityBar.additionalFiltersActions.showBuildingBlockTitle', + { + defaultMessage: 'Include building block alerts', + } +); + export const CLEAR_SELECTION = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.utilityBar.clearSelectionTitle', { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 6f1f2e46dce3d9..71cf5c10de7644 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -81,6 +81,25 @@ export const buildAlertsRuleIdFilter = (ruleId: string): Filter[] => [ }, ]; +export const buildShowBuildingBlockFilter = (showBuildingBlockAlerts: boolean): Filter[] => [ + ...(showBuildingBlockAlerts + ? [] + : [ + { + meta: { + alias: null, + negate: true, + disabled: false, + type: 'exists', + key: 'signal.rule.building_block_type', + value: 'exists', + }, + // @ts-ignore TODO: Rework parent typings to support ExistsFilter[] + exists: { field: 'signal.rule.building_block_type' }, + }, + ]), +]; + export const alertsHeaders: ColumnHeaderOptions[] = [ { columnHeaderType: defaultColumnHeaderType, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index 563f2ea60cded5..cc3a47017a835a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -37,6 +37,8 @@ describe('AlertsTableComponent', () => { clearEventsLoading={jest.fn()} setEventsDeleted={jest.fn()} clearEventsDeleted={jest.fn()} + showBuildingBlockAlerts={false} + onShowBuildingBlockAlertsChanged={jest.fn()} updateTimelineIsLoading={jest.fn()} updateTimeline={jest.fn()} /> diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 391598ebda03d6..87c631b80e38ba 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -64,6 +64,8 @@ interface OwnProps { hasIndexWrite: boolean; from: string; loading: boolean; + showBuildingBlockAlerts: boolean; + onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; signalsIndex: string; to: string; } @@ -94,6 +96,8 @@ export const AlertsTableComponent: React.FC = ({ selectedEventIds, setEventsDeleted, setEventsLoading, + showBuildingBlockAlerts, + onShowBuildingBlockAlertsChanged, signalsIndex, to, updateTimeline, @@ -302,6 +306,8 @@ export const AlertsTableComponent: React.FC = ({ currentFilter={filterGroup} selectAll={selectAllCallback} selectedEventIds={selectedEventIds} + showBuildingBlockAlerts={showBuildingBlockAlerts} + onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChanged} showClearSelection={showClearSelectionAction} totalCount={totalCount} updateAlertsStatus={updateAlertsStatusCallback.bind(null, refetchQuery)} @@ -313,6 +319,8 @@ export const AlertsTableComponent: React.FC = ({ hasIndexWrite, clearSelectionCallback, filterGroup, + showBuildingBlockAlerts, + onShowBuildingBlockAlertsChanged, loadingEventIds.length, selectAllCallback, selectedEventIds, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts index 0f55469bbfda22..e5e8635b9e7999 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/translations.ts @@ -20,21 +20,21 @@ export const ALERTS_DOCUMENT_TYPE = i18n.translate( export const OPEN_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.openAlertsTitle', { - defaultMessage: 'Open alerts', + defaultMessage: 'Open', } ); export const CLOSED_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.closedAlertsTitle', { - defaultMessage: 'Closed alerts', + defaultMessage: 'Closed', } ); export const IN_PROGRESS_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.alerts.inProgressAlertsTitle', { - defaultMessage: 'In progress alerts', + defaultMessage: 'In progress', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx new file mode 100644 index 00000000000000..03465118741043 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo } from 'react'; +import { EuiFormRow } from '@elastic/eui'; +import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { FieldComponent } from '../../../../common/components/autocomplete/field'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; + +interface AutocompleteFieldProps { + dataTestSubj: string; + field: FieldHook; + idAria: string; + indices: IIndexPattern; + isDisabled: boolean; + fieldType: string; + placeholder?: string; +} + +export const AutocompleteField = ({ + dataTestSubj, + field, + idAria, + indices, + isDisabled, + fieldType, + placeholder, +}: AutocompleteFieldProps) => { + const handleFieldChange = useCallback( + ([newField]: IFieldType[]): void => { + // TODO: Update onChange type in FieldComponent as newField can be undefined + field.setValue(newField?.name ?? ''); + }, + [field] + ); + + const selectedField = useMemo(() => { + const existingField = (field.value as string) ?? ''; + const [newSelectedField] = indices.fields.filter( + ({ name }) => existingField != null && existingField === name + ); + return newSelectedField; + }, [field.value, indices]); + + const fieldTypeFilter = useMemo(() => [fieldType], [fieldType]); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index 41ee91845a8ec8..2a6cd3fc5bb7a4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import { EuiLoadingSpinner } from '@elastic/eui'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; @@ -328,10 +328,19 @@ describe('helpers', () => { describe('buildSeverityDescription', () => { test('returns ListItem with passed in label and SeverityBadge component', () => { - const result: ListItems[] = buildSeverityDescription('Test label', 'Test description value'); + const result: ListItems[] = buildSeverityDescription({ + value: 'low', + mapping: [{ field: 'host.name', operator: 'equals', value: 'hello', severity: 'high' }], + }); - expect(result[0].title).toEqual('Test label'); - expect(result[0].description).toEqual(); + expect(result[0].title).toEqual('Severity'); + expect(result[0].description).toEqual(); + expect(result[1].title).toEqual('Severity override'); + + const wrapper = mount(result[1].description as React.ReactElement); + expect(wrapper.find('[data-test-subj="severityOverrideSeverity0"]').first().text()).toEqual( + 'High' + ); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 8393f2230dcfef..1110c8c098988b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -13,12 +13,16 @@ import { EuiSpacer, EuiLink, EuiText, + EuiIcon, + EuiToolTip, } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; +import * as i18nSeverity from '../severity_mapping/translations'; +import * as i18nRiskScore from '../risk_score_mapping/translations'; import { Threshold } from '../../../../../common/detection_engine/schemas/common/schemas'; import { RuleType } from '../../../../../common/detection_engine/types'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; @@ -30,6 +34,7 @@ import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './t import { SeverityBadge } from '../severity_badge'; import ListTreeIcon from './assets/list_tree_icon.svg'; import { assertUnreachable } from '../../../../common/lib/helpers'; +import { AboutStepRiskScore, AboutStepSeverity } from '../../../pages/detection_engine/rules/types'; const NoteDescriptionContainer = styled(EuiFlexItem)` height: 105px; @@ -219,11 +224,75 @@ export const buildStringArrayDescription = ( return []; }; -export const buildSeverityDescription = (label: string, value: string): ListItems[] => [ +const OverrideColumn = styled(EuiFlexItem)` + width: 125px; + max-width: 125px; + overflow: hidden; + text-overflow: ellipsis; +`; + +export const buildSeverityDescription = (severity: AboutStepSeverity): ListItems[] => [ { - title: label, - description: , + title: i18nSeverity.DEFAULT_SEVERITY, + description: , + }, + ...severity.mapping.map((severityItem, index) => { + return { + title: index === 0 ? i18nSeverity.SEVERITY_MAPPING : '', + description: ( + + + + <>{severityItem.field} + + + + <>{severityItem.value} + + + + + + + + + ), + }; + }), +]; + +export const buildRiskScoreDescription = (riskScore: AboutStepRiskScore): ListItems[] => [ + { + title: i18nRiskScore.RISK_SCORE, + description: riskScore.value, }, + ...riskScore.mapping.map((riskScoreItem, index) => { + return { + title: index === 0 ? i18nRiskScore.RISK_SCORE_MAPPING : '', + description: ( + + + + <>{riskScoreItem.field} + + + + + + {'signal.rule.risk_score'} + + ), + }; + }), ]; const MyRefUrlLink = styled(EuiLink)` diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx index 5a2a44a284e3b4..4a2d17ec126fb7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx @@ -450,7 +450,7 @@ describe('description_step', () => { mockFilterManager ); - expect(result[0].title).toEqual('Severity label'); + expect(result[0].title).toEqual('Severity'); expect(React.isValidElement(result[0].description)).toBeTruthy(); }); }); @@ -464,7 +464,7 @@ describe('description_step', () => { mockFilterManager ); - expect(result[0].title).toEqual('Risk score label'); + expect(result[0].title).toEqual('Risk score'); expect(result[0].description).toEqual(21); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 51624d04cb58b1..0b341050fa9d58 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -34,6 +34,7 @@ import { buildUnorderedListArrayDescription, buildUrlsDescription, buildNoteDescription, + buildRiskScoreDescription, buildRuleTypeDescription, buildThresholdDescription, } from './helpers'; @@ -192,18 +193,12 @@ export const getDescriptionItem = ( } else if (Array.isArray(get(field, data))) { const values: string[] = get(field, data); return buildStringArrayDescription(label, field, values); - // TODO: Add custom UI for Risk/Severity Mappings (and fix missing label) } else if (field === 'riskScore') { - const val: AboutStepRiskScore = get(field, data); - return [ - { - title: label, - description: val.value, - }, - ]; + const values: AboutStepRiskScore = get(field, data); + return buildRiskScoreDescription(values); } else if (field === 'severity') { - const val: AboutStepSeverity = get(field, data); - return buildSeverityDescription(label, val.value); + const values: AboutStepSeverity = get(field, data); + return buildSeverityDescription(values); } else if (field === 'timeline') { const timeline = get(field, data) as FieldValueTimeline; return [ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx index bdf1ac600faef6..c9e2cb1a8ca24f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx @@ -6,7 +6,6 @@ import { EuiFormRow, - EuiFieldText, EuiCheckbox, EuiText, EuiFlexGroup, @@ -15,12 +14,15 @@ import { EuiIcon, EuiSpacer, } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { CommonUseField } from '../../../../cases/components/create'; import { AboutStepRiskScore } from '../../../pages/detection_engine/rules/types'; +import { FieldComponent } from '../../../../common/components/autocomplete/field'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; const NestedContent = styled.div` margin-left: 24px; @@ -38,20 +40,47 @@ interface RiskScoreFieldProps { dataTestSubj: string; field: FieldHook; idAria: string; - indices: string[]; + indices: IIndexPattern; + placeholder?: string; } -export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskScoreFieldProps) => { - const [isRiskScoreMappingSelected, setIsRiskScoreMappingSelected] = useState(false); +export const RiskScoreField = ({ + dataTestSubj, + field, + idAria, + indices, + placeholder, +}: RiskScoreFieldProps) => { + const [isRiskScoreMappingChecked, setIsRiskScoreMappingChecked] = useState(false); + const [initialFieldCheck, setInitialFieldCheck] = useState(true); - const updateRiskScoreMapping = useCallback( - (event) => { + const fieldTypeFilter = useMemo(() => ['number'], []); + + useEffect(() => { + if ( + !isRiskScoreMappingChecked && + initialFieldCheck && + (field.value as AboutStepRiskScore).mapping?.length > 0 + ) { + setIsRiskScoreMappingChecked(true); + setInitialFieldCheck(false); + } + }, [ + field, + initialFieldCheck, + isRiskScoreMappingChecked, + setIsRiskScoreMappingChecked, + setInitialFieldCheck, + ]); + + const handleFieldChange = useCallback( + ([newField]: IFieldType[]): void => { const values = field.value as AboutStepRiskScore; field.setValue({ value: values.value, mapping: [ { - field: event.target.value, + field: newField?.name ?? '', operator: 'equals', value: '', }, @@ -61,11 +90,23 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco [field] ); - const severityLabel = useMemo(() => { + const selectedField = useMemo(() => { + const existingField = (field.value as AboutStepRiskScore).mapping?.[0]?.field ?? ''; + const [newSelectedField] = indices.fields.filter( + ({ name }) => existingField != null && existingField === name + ); + return newSelectedField; + }, [field.value, indices]); + + const handleRiskScoreMappingChecked = useCallback(() => { + setIsRiskScoreMappingChecked(!isRiskScoreMappingChecked); + }, [isRiskScoreMappingChecked, setIsRiskScoreMappingChecked]); + + const riskScoreLabel = useMemo(() => { return (
- {i18n.RISK_SCORE} + {i18n.DEFAULT_RISK_SCORE} {i18n.RISK_SCORE_DESCRIPTION} @@ -73,19 +114,15 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco ); }, []); - const severityMappingLabel = useMemo(() => { + const riskScoreMappingLabel = useMemo(() => { return (
- setIsRiskScoreMappingSelected(!isRiskScoreMappingSelected)} - > + setIsRiskScoreMappingSelected(e.target.checked)} + checked={isRiskScoreMappingChecked} + onChange={handleRiskScoreMappingChecked} /> {i18n.RISK_SCORE_MAPPING} @@ -96,13 +133,13 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco
); - }, [isRiskScoreMappingSelected, setIsRiskScoreMappingSelected]); + }, [handleRiskScoreMappingChecked, isRiskScoreMappingChecked]); return ( {i18n.RISK_SCORE_MAPPING_DETAILS} ) : ( '' @@ -147,7 +184,7 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco > - {isRiskScoreMappingSelected && ( + {isRiskScoreMappingChecked && ( @@ -156,7 +193,7 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco - {i18n.RISK_SCORE} + {i18n.DEFAULT_RISK_SCORE} @@ -164,12 +201,18 @@ export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskSco - diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx index a75bf19b5b3c4f..24e82a8f95a6b6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx @@ -8,6 +8,13 @@ import { i18n } from '@kbn/i18n'; export const RISK_SCORE = i18n.translate( 'xpack.securitySolution.alerts.riskScoreMapping.riskScoreTitle', + { + defaultMessage: 'Risk score', + } +); + +export const DEFAULT_RISK_SCORE = i18n.translate( + 'xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle', { defaultMessage: 'Default risk score', } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx index 47c45a6bdf88da..579c60579b32ee 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx @@ -6,7 +6,6 @@ import { EuiFormRow, - EuiFieldText, EuiCheckbox, EuiText, EuiFlexGroup, @@ -15,14 +14,23 @@ import { EuiIcon, EuiSpacer, } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import * as i18n from './translations'; import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; import { SeverityOptionItem } from '../step_about_rule/data'; import { CommonUseField } from '../../../../cases/components/create'; import { AboutStepSeverity } from '../../../pages/detection_engine/rules/types'; +import { + IFieldType, + IIndexPattern, +} from '../../../../../../../../src/plugins/data/common/index_patterns'; +import { FieldComponent } from '../../../../common/components/autocomplete/field'; +import { AutocompleteFieldMatchComponent } from '../../../../common/components/autocomplete/field_value_match'; +const SeverityMappingParentContainer = styled(EuiFlexItem)` + max-width: 471px; +`; const NestedContent = styled.div` margin-left: 24px; `; @@ -39,7 +47,7 @@ interface SeverityFieldProps { dataTestSubj: string; field: FieldHook; idAria: string; - indices: string[]; + indices: IIndexPattern; options: SeverityOptionItem[]; } @@ -47,13 +55,32 @@ export const SeverityField = ({ dataTestSubj, field, idAria, - indices, // TODO: To be used with autocomplete fields once https://github.com/elastic/kibana/pull/67013 is merged + indices, options, }: SeverityFieldProps) => { const [isSeverityMappingChecked, setIsSeverityMappingChecked] = useState(false); + const [initialFieldCheck, setInitialFieldCheck] = useState(true); + const fieldValueInputWidth = 160; - const updateSeverityMapping = useCallback( - (index: number, severity: string, mappingField: string, event) => { + useEffect(() => { + if ( + !isSeverityMappingChecked && + initialFieldCheck && + (field.value as AboutStepSeverity).mapping?.length > 0 + ) { + setIsSeverityMappingChecked(true); + setInitialFieldCheck(false); + } + }, [ + field, + initialFieldCheck, + isSeverityMappingChecked, + setIsSeverityMappingChecked, + setInitialFieldCheck, + ]); + + const handleFieldChange = useCallback( + (index: number, severity: string, [newField]: IFieldType[]): void => { const values = field.value as AboutStepSeverity; field.setValue({ value: values.value, @@ -61,7 +88,7 @@ export const SeverityField = ({ ...values.mapping.slice(0, index), { ...values.mapping[index], - [mappingField]: event.target.value, + field: newField?.name ?? '', operator: 'equals', severity, }, @@ -72,6 +99,41 @@ export const SeverityField = ({ [field] ); + const handleFieldMatchValueChange = useCallback( + (index: number, severity: string, newMatchValue: string): void => { + const values = field.value as AboutStepSeverity; + field.setValue({ + value: values.value, + mapping: [ + ...values.mapping.slice(0, index), + { + ...values.mapping[index], + value: newMatchValue, + operator: 'equals', + severity, + }, + ...values.mapping.slice(index + 1), + ], + }); + }, + [field] + ); + + const selectedState = useMemo(() => { + return ( + (field.value as AboutStepSeverity).mapping?.map((mapping) => { + const [newSelectedField] = indices.fields.filter( + ({ name }) => mapping.field != null && mapping.field === name + ); + return { field: newSelectedField, value: mapping.value }; + }) ?? [] + ); + }, [field.value, indices]); + + const handleSeverityMappingSelected = useCallback(() => { + setIsSeverityMappingChecked(!isSeverityMappingChecked); + }, [isSeverityMappingChecked, setIsSeverityMappingChecked]); + const severityLabel = useMemo(() => { return (
@@ -87,16 +149,12 @@ export const SeverityField = ({ const severityMappingLabel = useMemo(() => { return (
- setIsSeverityMappingChecked(!isSeverityMappingChecked)} - > + setIsSeverityMappingChecked(e.target.checked)} + onChange={handleSeverityMappingSelected} /> {i18n.SEVERITY_MAPPING} @@ -107,7 +165,7 @@ export const SeverityField = ({
); - }, [isSeverityMappingChecked, setIsSeverityMappingChecked]); + }, [handleSeverityMappingSelected, isSeverityMappingChecked]); return ( @@ -137,7 +195,7 @@ export const SeverityField = ({ - + - {i18n.SEVERITY} + {i18n.DEFAULT_SEVERITY} @@ -177,22 +235,33 @@ export const SeverityField = ({ - - @@ -208,7 +277,7 @@ export const SeverityField = ({ )} - + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx index 9c9784bac6b63a..f0bfc5f4637ab1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx @@ -13,6 +13,13 @@ export const SEVERITY = i18n.translate( } ); +export const DEFAULT_SEVERITY = i18n.translate( + 'xpack.securitySolution.alerts.severityMapping.defaultSeverityTitle', + { + defaultMessage: 'Severity', + } +); + export const SOURCE_FIELD = i18n.translate( 'xpack.securitySolution.alerts.severityMapping.sourceFieldTitle', { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 7f7ee94ed85b7a..3616643874a0ae 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -38,6 +38,8 @@ import { MarkdownEditorForm } from '../../../../common/components/markdown_edito import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; import { SeverityField } from '../severity_mapping'; import { RiskScoreField } from '../risk_score_mapping'; +import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules'; +import { AutocompleteField } from '../autocomplete_field'; const CommonUseField = getUseField({ component: Field }); @@ -90,6 +92,9 @@ const StepAboutRuleComponent: FC = ({ setStepData, }) => { const [myStepData, setMyStepData] = useState(stepAboutDefaultValue); + const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( + defineRuleData?.index ?? [] + ); const { form } = useForm({ defaultValue: myStepData, @@ -149,7 +154,6 @@ const StepAboutRuleComponent: FC = ({ }} /> - = ({ componentProps={{ 'data-test-subj': 'detectionEngineStepAboutRuleSeverityField', idAria: 'detectionEngineStepAboutRuleSeverityField', - isDisabled: isLoading, + isDisabled: isLoading || indexPatternLoading, options: severityOptions, - indices: defineRuleData?.index ?? [], + indices: indexPatterns, }} /> @@ -184,7 +188,8 @@ const StepAboutRuleComponent: FC = ({ componentProps={{ 'data-test-subj': 'detectionEngineStepAboutRuleRiskScore', idAria: 'detectionEngineStepAboutRuleRiskScore', - isDisabled: isLoading, + isDisabled: isLoading || indexPatternLoading, + indices: indexPatterns, }} /> @@ -196,7 +201,7 @@ const StepAboutRuleComponent: FC = ({ 'data-test-subj': 'detectionEngineStepAboutRuleTags', euiFieldProps: { fullWidth: true, - isDisabled: isLoading, + isDisabled: isLoading || indexPatternLoading, placeholder: '', }, }} @@ -277,7 +282,7 @@ const StepAboutRuleComponent: FC = ({ }} /> - + = ({ /> - - - + - - - + {({ severity }) => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts index c179128c56d924..3a5aa3c56c3df3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts @@ -26,6 +26,12 @@ export const ADD_FALSE_POSITIVE = i18n.translate( defaultMessage: 'Add false positive example', } ); +export const BUILDING_BLOCK = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.buildingBlockLabel', + { + defaultMessage: 'Building block', + } +); export const LOW = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index cdff8ea4ab928a..aef9f2adcbcc80 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -5,7 +5,7 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; @@ -39,6 +39,7 @@ import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unau import * as i18n from './translations'; import { LinkButton } from '../../../common/components/links'; import { useFormatUrl } from '../../../common/components/link_to'; +import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config'; export const DetectionEnginePageComponent: React.FC = ({ filters, @@ -62,6 +63,7 @@ export const DetectionEnginePageComponent: React.FC = ({ const history = useHistory(); const [lastAlerts] = useAlertInfo({}); const { formatUrl } = useFormatUrl(SecurityPageName.detections); + const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false); const loading = userInfoLoading || listsConfigLoading; const updateDateRangeCallback = useCallback( @@ -87,6 +89,24 @@ export const DetectionEnginePageComponent: React.FC = ({ [history] ); + const alertsHistogramDefaultFilters = useMemo( + () => [...filters, ...buildShowBuildingBlockFilter(showBuildingBlockAlerts)], + [filters, showBuildingBlockAlerts] + ); + + // AlertsTable manages global filters itself, so not including `filters` + const alertsTableDefaultFilters = useMemo( + () => buildShowBuildingBlockFilter(showBuildingBlockAlerts), + [showBuildingBlockAlerts] + ); + + const onShowBuildingBlockAlertsChangedCallback = useCallback( + (newShowBuildingBlockAlerts: boolean) => { + setShowBuildingBlockAlerts(newShowBuildingBlockAlerts); + }, + [setShowBuildingBlockAlerts] + ); + const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ signalIndexName, ]); @@ -145,7 +165,7 @@ export const DetectionEnginePageComponent: React.FC = ({ = ({ hasIndexWrite={hasIndexWrite ?? false} canUserCRUD={(canUserCRUD ?? false) && (hasEncryptionKey ?? false)} from={from} + defaultFilters={alertsTableDefaultFilters} + showBuildingBlockAlerts={showBuildingBlockAlerts} + onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback} signalsIndex={signalIndexName ?? ''} to={to} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index f402303c4c6212..745518b90df005 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -348,7 +348,6 @@ describe('helpers', () => { references: ['www.test.co'], risk_score: 21, risk_score_mapping: [], - rule_name_override: '', severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], @@ -369,7 +368,6 @@ describe('helpers', () => { ], }, ], - timestamp_override: '', }; expect(result).toEqual(expected); @@ -392,7 +390,6 @@ describe('helpers', () => { references: ['www.test.co'], risk_score: 21, risk_score_mapping: [], - rule_name_override: '', severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], @@ -413,7 +410,6 @@ describe('helpers', () => { ], }, ], - timestamp_override: '', }; expect(result).toEqual(expected); @@ -434,7 +430,6 @@ describe('helpers', () => { references: ['www.test.co'], risk_score: 21, risk_score_mapping: [], - rule_name_override: '', severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], @@ -455,7 +450,6 @@ describe('helpers', () => { ], }, ], - timestamp_override: '', }; expect(result).toEqual(expected); @@ -508,7 +502,6 @@ describe('helpers', () => { references: ['www.test.co'], risk_score: 21, risk_score_mapping: [], - rule_name_override: '', severity: 'low', severity_mapping: [], tags: ['tag1', 'tag2'], @@ -519,7 +512,6 @@ describe('helpers', () => { technique: [{ id: '456', name: 'technique1', reference: 'technique reference' }], }, ], - timestamp_override: '', }; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 4bb7196e17db57..c419dd142cfbe6 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -167,7 +167,7 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule references: references.filter((item) => !isEmpty(item)), risk_score: riskScore.value, risk_score_mapping: riskScore.mapping, - rule_name_override: ruleNameOverride, + rule_name_override: ruleNameOverride !== '' ? ruleNameOverride : undefined, severity: severity.value, severity_mapping: severity.mapping, threat: threat @@ -180,7 +180,7 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule return { id, name, reference }; }), })), - timestamp_override: timestampOverride, + timestamp_override: timestampOverride !== '' ? timestampOverride : undefined, ...(!isEmpty(note) ? { note } : {}), ...rest, }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 45a1c89cec621c..2e7ef1180f4e34 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -17,7 +17,7 @@ import { EuiToolTip, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FC, memo, useCallback, useMemo, useState } from 'react'; +import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; @@ -48,7 +48,10 @@ import { OverviewEmpty } from '../../../../../overview/components/overview_empty import { useAlertInfo } from '../../../../components/alerts_info'; import { StepDefineRule } from '../../../../components/rules/step_define_rule'; import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; -import { buildAlertsRuleIdFilter } from '../../../../components/alerts_table/default_config'; +import { + buildAlertsRuleIdFilter, + buildShowBuildingBlockFilter, +} from '../../../../components/alerts_table/default_config'; import { NoWriteAlertsCallOut } from '../../../../components/no_write_alerts_callout'; import * as detectionI18n from '../../translations'; import { ReadOnlyCallOut } from '../../../../components/rules/read_only_callout'; @@ -134,6 +137,7 @@ export const RuleDetailsPageComponent: FC = ({ scheduleRuleData: null, }; const [lastAlerts] = useAlertInfo({ ruleId }); + const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false); const mlCapabilities = useMlCapabilities(); const history = useHistory(); const { formatUrl } = useFormatUrl(SecurityPageName.detections); @@ -184,9 +188,17 @@ export const RuleDetailsPageComponent: FC = ({ [isLoading, rule] ); + // Set showBuildingBlockAlerts if rule is a Building Block Rule otherwise we won't show alerts + useEffect(() => { + setShowBuildingBlockAlerts(rule?.building_block_type != null); + }, [rule]); + const alertDefaultFilters = useMemo( - () => (ruleId != null ? buildAlertsRuleIdFilter(ruleId) : []), - [ruleId] + () => [ + ...(ruleId != null ? buildAlertsRuleIdFilter(ruleId) : []), + ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ], + [ruleId, showBuildingBlockAlerts] ); const alertMergedFilters = useMemo(() => [...alertDefaultFilters, ...filters], [ @@ -262,6 +274,13 @@ export const RuleDetailsPageComponent: FC = ({ [history, ruleId] ); + const onShowBuildingBlockAlertsChangedCallback = useCallback( + (newShowBuildingBlockAlerts: boolean) => { + setShowBuildingBlockAlerts(newShowBuildingBlockAlerts); + }, + [setShowBuildingBlockAlerts] + ); + const { indicesExist, indexPattern } = useWithSource('default', indexToAdd); const exceptionLists = useMemo((): { @@ -447,6 +466,8 @@ export const RuleDetailsPageComponent: FC = ({ hasIndexWrite={hasIndexWrite ?? false} from={from} loading={loading} + showBuildingBlockAlerts={showBuildingBlockAlerts} + onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback} signalsIndex={signalIndexName ?? ''} to={to} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 87cb5e77697b5f..0900cdb8f4789b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -160,12 +160,13 @@ const EditRulePageComponent: FC = () => { <> - {myAboutRuleForm.data != null && ( + {myAboutRuleForm.data != null && myDefineRuleForm.data != null && ( )} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index e7daff0947b0d5..b501536e5b387f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -145,10 +145,10 @@ export interface AboutStepRuleJson { risk_score_mapping: RiskScoreMapping; references: string[]; false_positives: string[]; - rule_name_override: RuleNameOverride; + rule_name_override?: RuleNameOverride; tags: string[]; threat: IMitreEnterpriseAttack[]; - timestamp_override: TimestampOverride; + timestamp_override?: TimestampOverride; note?: string; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index 75c4d75cedf1db..218750ac30a2aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -51,6 +51,7 @@ export const buildBulkBody = ({ enabled, createdAt, createdBy, + doc, updatedAt, updatedBy, interval, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts index 452ba958876d69..ccf8a9bec3159a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts @@ -15,6 +15,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: undefined, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -85,6 +86,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: '', + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -156,6 +158,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: fakeSortId, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -228,6 +231,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: fakeSortIdNumber, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -299,6 +303,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: undefined, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, @@ -377,6 +382,7 @@ describe('create_signals', () => { filter: {}, size: 100, searchAfterSortId: undefined, + timestampOverride: undefined, }); expect(query).toEqual({ allowNoIndices: true, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts index dcf3a90364a401..96db7e1eb53b7b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; + interface BuildEventsSearchQuery { aggregations?: unknown; index: string[]; @@ -12,6 +14,7 @@ interface BuildEventsSearchQuery { filter: unknown; size: number; searchAfterSortId: string | number | undefined; + timestampOverride: TimestampOverrideOrUndefined; } export const buildEventsSearchQuery = ({ @@ -22,7 +25,9 @@ export const buildEventsSearchQuery = ({ filter, size, searchAfterSortId, + timestampOverride, }: BuildEventsSearchQuery) => { + const timestamp = timestampOverride ?? '@timestamp'; const filterWithTime = [ filter, { @@ -33,7 +38,7 @@ export const buildEventsSearchQuery = ({ should: [ { range: { - '@timestamp': { + [timestamp]: { gte: from, }, }, @@ -47,7 +52,7 @@ export const buildEventsSearchQuery = ({ should: [ { range: { - '@timestamp': { + [timestamp]: { lte: to, }, }, @@ -79,7 +84,7 @@ export const buildEventsSearchQuery = ({ ...(aggregations ? { aggregations } : {}), sort: [ { - '@timestamp': { + [timestamp]: { order: 'asc', }, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts index ed632ee2576dc8..7257e5952ff055 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts @@ -5,7 +5,7 @@ */ import { buildRule } from './build_rule'; -import { sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results'; +import { sampleDocNoSortId, sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; @@ -29,6 +29,7 @@ describe('buildRule', () => { ]; const rule = buildRule({ actions: [], + doc: sampleDocNoSortId(), ruleParams, name: 'some-name', id: sampleRuleGuid, @@ -97,6 +98,7 @@ describe('buildRule', () => { ruleParams.filters = undefined; const rule = buildRule({ actions: [], + doc: sampleDocNoSortId(), ruleParams, name: 'some-name', id: sampleRuleGuid, @@ -154,6 +156,7 @@ describe('buildRule', () => { ruleParams.filters = undefined; const rule = buildRule({ actions: [], + doc: sampleDocNoSortId(), ruleParams, name: 'some-name', id: sampleRuleGuid, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts index 9e118f77a73e79..e02a0154d63c9b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts @@ -8,6 +8,10 @@ import { pickBy } from 'lodash/fp'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams } from '../types'; +import { buildRiskScoreFromMapping } from './mappings/build_risk_score_from_mapping'; +import { SignalSourceHit } from './types'; +import { buildSeverityFromMapping } from './mappings/build_severity_from_mapping'; +import { buildRuleNameFromMapping } from './mappings/build_rule_name_from_mapping'; interface BuildRuleParams { ruleParams: RuleTypeParams; @@ -17,6 +21,7 @@ interface BuildRuleParams { enabled: boolean; createdAt: string; createdBy: string; + doc: SignalSourceHit; updatedAt: string; updatedBy: string; interval: string; @@ -32,12 +37,33 @@ export const buildRule = ({ enabled, createdAt, createdBy, + doc, updatedAt, updatedBy, interval, tags, throttle, }: BuildRuleParams): Partial => { + const { riskScore, riskScoreMeta } = buildRiskScoreFromMapping({ + doc, + riskScore: ruleParams.riskScore, + riskScoreMapping: ruleParams.riskScoreMapping, + }); + + const { severity, severityMeta } = buildSeverityFromMapping({ + doc, + severity: ruleParams.severity, + severityMapping: ruleParams.severityMapping, + }); + + const { ruleName, ruleNameMeta } = buildRuleNameFromMapping({ + doc, + ruleName: name, + ruleNameMapping: ruleParams.ruleNameOverride, + }); + + const meta = { ...ruleParams.meta, ...riskScoreMeta, ...severityMeta, ...ruleNameMeta }; + return pickBy((value: unknown) => value != null, { id, rule_id: ruleParams.ruleId ?? '(unknown rule_id)', @@ -48,9 +74,9 @@ export const buildRule = ({ saved_id: ruleParams.savedId, timeline_id: ruleParams.timelineId, timeline_title: ruleParams.timelineTitle, - meta: ruleParams.meta, + meta: Object.keys(meta).length > 0 ? meta : undefined, max_signals: ruleParams.maxSignals, - risk_score: ruleParams.riskScore, // TODO: Risk Score Override via risk_score_mapping + risk_score: riskScore, risk_score_mapping: ruleParams.riskScoreMapping ?? [], output_index: ruleParams.outputIndex, description: ruleParams.description, @@ -61,11 +87,11 @@ export const buildRule = ({ interval, language: ruleParams.language, license: ruleParams.license, - name, // TODO: Rule Name Override via rule_name_override + name: ruleName, query: ruleParams.query, references: ruleParams.references, rule_name_override: ruleParams.ruleNameOverride, - severity: ruleParams.severity, // TODO: Severity Override via severity_mapping + severity, severity_mapping: ruleParams.severityMapping ?? [], tags, type: ruleParams.type, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts index a9a199f210da0f..251c043adb58be 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts @@ -50,6 +50,7 @@ export const findThresholdSignals = async ({ return singleSearchAfter({ aggregations, searchAfterSortId: undefined, + timestampOverride: undefined, index: inputIndexPattern, from, to, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts new file mode 100644 index 00000000000000..e1d9c7f7c8a5ca --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { buildRiskScoreFromMapping } from './build_risk_score_from_mapping'; + +describe('buildRiskScoreFromMapping', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('risk score defaults to provided if mapping is incomplete', () => { + const riskScore = buildRiskScoreFromMapping({ + doc: sampleDocNoSortId(), + riskScore: 57, + riskScoreMapping: undefined, + }); + + expect(riskScore).toEqual({ riskScore: 57, riskScoreMeta: {} }); + }); + + // TODO: Enhance... +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts new file mode 100644 index 00000000000000..356cf95fc0d24f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_risk_score_from_mapping.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash/fp'; +import { + Meta, + RiskScore, + RiskScoreMappingOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SignalSourceHit } from '../types'; +import { RiskScore as RiskScoreIOTS } from '../../../../../common/detection_engine/schemas/types'; + +interface BuildRiskScoreFromMappingProps { + doc: SignalSourceHit; + riskScore: RiskScore; + riskScoreMapping: RiskScoreMappingOrUndefined; +} + +interface BuildRiskScoreFromMappingReturn { + riskScore: RiskScore; + riskScoreMeta: Meta; // TODO: Stricter types +} + +export const buildRiskScoreFromMapping = ({ + doc, + riskScore, + riskScoreMapping, +}: BuildRiskScoreFromMappingProps): BuildRiskScoreFromMappingReturn => { + // MVP support is for mapping from a single field + if (riskScoreMapping != null && riskScoreMapping.length > 0) { + const mappedField = riskScoreMapping[0].field; + // TODO: Expand by verifying fieldType from index via doc._index + const mappedValue = get(mappedField, doc._source); + // TODO: This doesn't seem to validate...identified riskScore > 100 😬 + if (RiskScoreIOTS.is(mappedValue)) { + return { riskScore: mappedValue, riskScoreMeta: { riskScoreOverridden: true } }; + } + } + return { riskScore, riskScoreMeta: {} }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts new file mode 100644 index 00000000000000..b509020646d1b5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { buildRuleNameFromMapping } from './build_rule_name_from_mapping'; + +describe('buildRuleNameFromMapping', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('rule name defaults to provided if mapping is incomplete', () => { + const ruleName = buildRuleNameFromMapping({ + doc: sampleDocNoSortId(), + ruleName: 'rule-name', + ruleNameMapping: 'message', + }); + + expect(ruleName).toEqual({ ruleName: 'rule-name', ruleNameMeta: {} }); + }); + + // TODO: Enhance... +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts new file mode 100644 index 00000000000000..af540ed1454adf --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_rule_name_from_mapping.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { get } from 'lodash/fp'; +import { + Meta, + Name, + RuleNameOverrideOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SignalSourceHit } from '../types'; + +interface BuildRuleNameFromMappingProps { + doc: SignalSourceHit; + ruleName: Name; + ruleNameMapping: RuleNameOverrideOrUndefined; +} + +interface BuildRuleNameFromMappingReturn { + ruleName: Name; + ruleNameMeta: Meta; // TODO: Stricter types +} + +export const buildRuleNameFromMapping = ({ + doc, + ruleName, + ruleNameMapping, +}: BuildRuleNameFromMappingProps): BuildRuleNameFromMappingReturn => { + if (ruleNameMapping != null) { + // TODO: Expand by verifying fieldType from index via doc._index + const mappedValue = get(ruleNameMapping, doc._source); + if (t.string.is(mappedValue)) { + return { ruleName: mappedValue, ruleNameMeta: { ruleNameOverridden: true } }; + } + } + + return { ruleName, ruleNameMeta: {} }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts new file mode 100644 index 00000000000000..80950335934f4d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocNoSortId } from '../__mocks__/es_results'; +import { buildSeverityFromMapping } from './build_severity_from_mapping'; + +describe('buildSeverityFromMapping', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('severity defaults to provided if mapping is incomplete', () => { + const severity = buildSeverityFromMapping({ + doc: sampleDocNoSortId(), + severity: 'low', + severityMapping: undefined, + }); + + expect(severity).toEqual({ severity: 'low', severityMeta: {} }); + }); + + // TODO: Enhance... +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts new file mode 100644 index 00000000000000..a3c4f47b491be1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/mappings/build_severity_from_mapping.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get } from 'lodash/fp'; +import { + Meta, + Severity, + SeverityMappingItem, + severity as SeverityIOTS, + SeverityMappingOrUndefined, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +import { SignalSourceHit } from '../types'; + +interface BuildSeverityFromMappingProps { + doc: SignalSourceHit; + severity: Severity; + severityMapping: SeverityMappingOrUndefined; +} + +interface BuildSeverityFromMappingReturn { + severity: Severity; + severityMeta: Meta; // TODO: Stricter types +} + +export const buildSeverityFromMapping = ({ + doc, + severity, + severityMapping, +}: BuildSeverityFromMappingProps): BuildSeverityFromMappingReturn => { + if (severityMapping != null && severityMapping.length > 0) { + let severityMatch: SeverityMappingItem | undefined; + severityMapping.forEach((mapping) => { + // TODO: Expand by verifying fieldType from index via doc._index + const mappedValue = get(mapping.field, doc._source); + if (mapping.value === mappedValue) { + severityMatch = { ...mapping }; + } + }); + + if (severityMatch != null && SeverityIOTS.is(severityMatch.severity)) { + return { + severity: severityMatch.severity, + severityMeta: { severityOverrideField: severityMatch.field }, + }; + } + } + return { severity, severityMeta: {} }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index f3025ead69a05f..2a0e39cbbf237f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -144,6 +144,7 @@ export const searchAfterAndBulkCreate = async ({ logger, filter, pageSize: tuple.maxSignals < pageSize ? Math.ceil(tuple.maxSignals) : pageSize, // maximum number of docs to receive per search result. + timestampOverride: ruleParams.timestampOverride, } ); toReturn.searchAfterTimes.push(searchDuration); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts index 50b0cb27990f8f..250b891eb1f2cc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts @@ -31,6 +31,7 @@ describe('singleSearchAfter', () => { logger: mockLogger, pageSize: 1, filter: undefined, + timestampOverride: undefined, }); expect(searchResult).toEqual(sampleDocSearchResultsNoSortId); }); @@ -46,6 +47,7 @@ describe('singleSearchAfter', () => { logger: mockLogger, pageSize: 1, filter: undefined, + timestampOverride: undefined, }); expect(searchResult).toEqual(sampleDocSearchResultsWithSortId); }); @@ -64,6 +66,7 @@ describe('singleSearchAfter', () => { logger: mockLogger, pageSize: 1, filter: undefined, + timestampOverride: undefined, }) ).rejects.toThrow('Fake Error'); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index daea277f143682..5667f2e47b6d71 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -10,6 +10,7 @@ import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; import { buildEventsSearchQuery } from './build_events_query'; import { makeFloatString } from './utils'; +import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; interface SingleSearchAfterParams { aggregations?: unknown; @@ -21,6 +22,7 @@ interface SingleSearchAfterParams { logger: Logger; pageSize: number; filter: unknown; + timestampOverride: TimestampOverrideOrUndefined; } // utilize search_after for paging results into bulk. @@ -34,6 +36,7 @@ export const singleSearchAfter = async ({ filter, logger, pageSize, + timestampOverride, }: SingleSearchAfterParams): Promise<{ searchResult: SignalSearchResponse; searchDuration: string; @@ -47,6 +50,7 @@ export const singleSearchAfter = async ({ filter, size: pageSize, searchAfterSortId, + timestampOverride, }); const start = performance.now(); const nextSearchAfterResult: SignalSearchResponse = await services.callCluster(