diff --git a/src/plugins/es_ui_shared/static/forms/components/field.tsx b/src/plugins/es_ui_shared/static/forms/components/field.tsx index 89dea53d75b38a..5b9a6dc9de002d 100644 --- a/src/plugins/es_ui_shared/static/forms/components/field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/field.tsx @@ -37,6 +37,7 @@ import { RadioGroupField, RangeField, SelectField, + SuperSelectField, ToggleField, } from './fields'; @@ -50,6 +51,7 @@ const mapTypeToFieldComponent = { [FIELD_TYPES.RADIO_GROUP]: RadioGroupField, [FIELD_TYPES.RANGE]: RangeField, [FIELD_TYPES.SELECT]: SelectField, + [FIELD_TYPES.SUPER_SELECT]: SuperSelectField, [FIELD_TYPES.TOGGLE]: ToggleField, }; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/index.ts b/src/plugins/es_ui_shared/static/forms/components/fields/index.ts index f973bb7b04d346..35635d0e8530c9 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/index.ts +++ b/src/plugins/es_ui_shared/static/forms/components/fields/index.ts @@ -25,5 +25,6 @@ export * from './multi_select_field'; export * from './radio_group_field'; export * from './range_field'; export * from './select_field'; +export * from './super_select_field'; export * from './toggle_field'; export * from './text_area_field'; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/super_select_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/super_select_field.tsx new file mode 100644 index 00000000000000..9b29d75230d7a6 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/components/fields/super_select_field.tsx @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiFormRow, EuiSuperSelect } from '@elastic/eui'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; + +interface Props { + field: FieldHook; + euiFieldProps?: Record; + idAria?: string; + [key: string]: any; +} + +export const SuperSelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + return ( + + { + field.setValue(value); + }} + options={[]} + isInvalid={isInvalid} + data-test-subj="select" + {...euiFieldProps} + /> + + ); +}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts index df2807e59ab465..4056947483107c 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts @@ -28,6 +28,7 @@ export const FIELD_TYPES = { RADIO_GROUP: 'radioGroup', RANGE: 'range', SELECT: 'select', + SUPER_SELECT: 'superSelect', MULTI_SELECT: 'multiSelect', }; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts index 91480f20d8b004..41e13408c1e01d 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts @@ -181,6 +181,7 @@ export interface QueryTimelineById { apolloClient: ApolloClient | ApolloClient<{}> | undefined; duplicate: boolean; timelineId: string; + onOpenTimeline?: (timeline: TimelineModel) => void; openTimeline?: boolean; updateIsLoading: ActionCreator<{ id: string; isLoading: boolean }>; updateTimeline: DispatchUpdateTimeline; @@ -190,6 +191,7 @@ export const queryTimelineById = ({ apolloClient, duplicate = false, timelineId, + onOpenTimeline, openTimeline = true, updateIsLoading, updateTimeline, @@ -209,7 +211,9 @@ export const queryTimelineById = ({ ); const { timeline, notes } = formatTimelineResultToModel(timelineToOpen, duplicate); - if (updateTimeline) { + if (onOpenTimeline != null) { + onOpenTimeline(timeline); + } else if (updateTimeline) { updateTimeline({ duplicate, from: getOr(getDefaultFromValue(), 'dateRange.start', timeline), diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx index c22c5fdbcfbc59..a97cfefaf0393f 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx @@ -12,18 +12,20 @@ import { Dispatch } from 'redux'; import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; import { deleteTimelineMutation } from '../../containers/timeline/delete/persist.gql_query'; import { AllTimelinesVariables, AllTimelinesQuery } from '../../containers/timeline/all'; - import { allTimelinesQuery } from '../../containers/timeline/all/index.gql_query'; import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../graphql/types'; import { State, timelineSelectors } from '../../store'; +import { timelineDefaults, TimelineModel } from '../../store/timeline/model'; import { createTimeline as dispatchCreateNewTimeline, updateIsLoading as dispatchUpdateIsLoading, } from '../../store/timeline/actions'; +import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { OpenTimeline } from './open_timeline'; import { OPEN_TIMELINE_CLASS_NAME, queryTimelineById, dispatchUpdateTimeline } from './helpers'; import { OpenTimelineModalBody } from './open_timeline_modal/open_timeline_modal_body'; import { + ActionTimelineToShow, DeleteTimelines, EuiSearchBarQuery, OnDeleteSelected, @@ -41,14 +43,14 @@ import { OpenTimelineReduxProps, } from './types'; import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; -import { ColumnHeader } from '../timeline/body/column_headers/column_header'; -import { timelineDefaults } from '../../store/timeline/model'; interface OwnProps { apolloClient: ApolloClient; /** Displays open timeline in modal */ isModal: boolean; closeModalTimeline?: () => void; + hideActions?: ActionTimelineToShow[]; + onOpenTimeline?: (timeline: TimelineModel) => void; } export type OpenTimelineOwnProps = OwnProps & @@ -69,15 +71,17 @@ export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): str /** Manages the state (e.g table selection) of the (pure) `OpenTimeline` component */ export const StatefulOpenTimelineComponent = React.memo( ({ + apolloClient, + closeModalTimeline, + createNewTimeline, defaultPageSize, + hideActions = [], isModal = false, + onOpenTimeline, + timeline, title, - apolloClient, - closeModalTimeline, updateTimeline, updateIsLoading, - timeline, - createNewTimeline, }) => { /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState< @@ -212,6 +216,7 @@ export const StatefulOpenTimelineComponent = React.memo( queryTimelineById({ apolloClient, duplicate, + onOpenTimeline, timelineId, updateIsLoading, updateTimeline, @@ -286,6 +291,7 @@ export const StatefulOpenTimelineComponent = React.memo( data-test-subj={'open-timeline-modal'} deleteTimelines={onDeleteOneTimeline} defaultPageSize={defaultPageSize} + hideActions={hideActions} isLoading={loading} itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap} onAddTimelinesToFavorites={undefined} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.test.tsx index 1ed08eee633aba..a1ca7812bba340 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.test.tsx @@ -143,7 +143,7 @@ describe('OpenTimeline', () => { ).toBe(true); }); - test('it shows extended columns and actions when onDeleteSelected and deleteTimelines are specified', () => { + test('it shows the delete action columns when onDeleteSelected and deleteTimelines are specified', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(true); + expect(props.actionTimelineToShow).toContain('delete'); }); - test('it does NOT show extended columns and actions when is onDeleteSelected undefined and deleteTimelines is specified', () => { + test('it does NOT show the delete action columns when is onDeleteSelected undefined and deleteTimelines is specified', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(false); + expect(props.actionTimelineToShow).not.toContain('delete'); }); - test('it does NOT show extended columns and actions when is onDeleteSelected provided and deleteTimelines is undefined', () => { + test('it does NOT show the delete action columns when is onDeleteSelected provided and deleteTimelines is undefined', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(false); + expect(props.actionTimelineToShow).not.toContain('delete'); }); - test('it does NOT show extended columns and actions when both onDeleteSelected and deleteTimelines are undefined', () => { + test('it does NOT show the delete action when both onDeleteSelected and deleteTimelines are undefined', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(false); + expect(props.actionTimelineToShow).not.toContain('delete'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx index 59ccfc8b250aa5..8aab02b495392f 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx @@ -57,6 +57,11 @@ export const OpenTimeline = React.memo( /> ( pageIndex={pageIndex} pageSize={pageSize} searchResults={searchResults} - showExtendedColumnsAndActions={onDeleteSelected != null && deleteTimelines != null} + showExtendedColumns={true} sortDirection={sortDirection} sortField={sortField} totalSearchResultsCount={totalSearchResultsCount} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx index cd89eb8aad6f43..c530929a3c96ee 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx @@ -7,39 +7,49 @@ import { EuiModal, EuiOverlayMask } from '@elastic/eui'; import React from 'react'; +import { TimelineModel } from '../../../store/timeline/model'; import { useApolloClient } from '../../../utils/apollo_context'; + import * as i18n from '../translations'; +import { ActionTimelineToShow } from '../types'; import { StatefulOpenTimeline } from '..'; export interface OpenTimelineModalProps { onClose: () => void; + hideActions?: ActionTimelineToShow[]; + modalTitle?: string; + onOpen?: (timeline: TimelineModel) => void; } const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; const OPEN_TIMELINE_MODAL_WIDTH = 1000; // px -export const OpenTimelineModal = React.memo(({ onClose }) => { - const apolloClient = useApolloClient(); - - if (!apolloClient) return null; - - return ( - - - - - - ); -}); +export const OpenTimelineModal = React.memo( + ({ hideActions = [], modalTitle, onClose, onOpen }) => { + const apolloClient = useApolloClient(); + + if (!apolloClient) return null; + + return ( + + + + + + ); + } +); OpenTimelineModal.displayName = 'OpenTimelineModal'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx index 1010504c0acace..2c3adb138b7acf 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx @@ -143,7 +143,7 @@ describe('OpenTimelineModal', () => { ).toBe(true); }); - test('it shows extended columns and actions when onDeleteSelected and deleteTimelines are specified', () => { + test('it shows the delete action when onDeleteSelected and deleteTimelines are specified', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(true); + expect(props.actionTimelineToShow).toContain('delete'); }); - test('it does NOT show extended columns and actions when is onDeleteSelected undefined and deleteTimelines is specified', () => { + test('it does NOT show the delete when is onDeleteSelected undefined and deleteTimelines is specified', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(false); + expect(props.actionTimelineToShow).not.toContain('delete'); }); - test('it does NOT show extended columns and actions when is onDeleteSelected provided and deleteTimelines is undefined', () => { + test('it does NOT show the delete action when is onDeleteSelected provided and deleteTimelines is undefined', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(false); + expect(props.actionTimelineToShow).not.toContain('delete'); }); - test('it does NOT show extended columns and actions when both onDeleteSelected and deleteTimelines are undefined', () => { + test('it does NOT show extended columns when both onDeleteSelected and deleteTimelines are undefined', () => { const wrapper = mountWithIntl( { .first() .props() as TimelinesTableProps; - expect(props.showExtendedColumnsAndActions).toBe(false); + expect(props.actionTimelineToShow).not.toContain('delete'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx index e28725973aff2f..dcd0b377705830 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx @@ -5,10 +5,10 @@ */ import { EuiModalBody, EuiModalHeader } from '@elastic/eui'; -import React from 'react'; +import React, { memo, useMemo } from 'react'; import styled from 'styled-components'; -import { OpenTimelineProps } from '../types'; +import { OpenTimelineProps, ActionTimelineToShow } from '../types'; import { SearchRow } from '../search_row'; import { TimelinesTable } from '../timelines_table'; import { TitleRow } from '../title_row'; @@ -19,10 +19,11 @@ export const HeaderContainer = styled.div` HeaderContainer.displayName = 'HeaderContainer'; -export const OpenTimelineModalBody = React.memo( +export const OpenTimelineModalBody = memo( ({ deleteTimelines, defaultPageSize, + hideActions = [], isLoading, itemIdToExpandedNotesRowMap, onAddTimelinesToFavorites, @@ -43,51 +44,61 @@ export const OpenTimelineModalBody = React.memo( sortField, title, totalSearchResultsCount, - }) => ( - <> - - - + }) => { + const actionsToShow = useMemo(() => { + const actions: ActionTimelineToShow[] = + onDeleteSelected != null && deleteTimelines != null + ? ['delete', 'duplicate'] + : ['duplicate']; + return actions.filter(action => !hideActions.includes(action)); + }, [onDeleteSelected, deleteTimelines, hideActions]); + return ( + <> + + + + + + + - + - - - - - - - - ) + + + ); + } ); OpenTimelineModalBody.displayName = 'OpenTimelineModalBody'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx index 89d6b4befa787c..eec11f571328f5 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx @@ -27,10 +27,11 @@ describe('#getActionsColumns', () => { mockResults = cloneDeep(mockTimelineResults); }); - test('it renders the delete timeline (trash icon) when showDeleteAction is true (because showExtendedColumnsAndActions is true)', () => { + test('it renders the delete timeline (trash icon) when actionTimelineToShow is including the action delete', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -53,10 +54,11 @@ describe('#getActionsColumns', () => { expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toBe(true); }); - test('it does NOT render the delete timeline (trash icon) when showDeleteAction is false (because showExtendedColumnsAndActions is false)', () => { + test('it does NOT render the delete timeline (trash icon) when actionTimelineToShow is NOT including the action delete', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -79,10 +81,65 @@ describe('#getActionsColumns', () => { expect(wrapper.find('[data-test-subj="delete-timeline"]').exists()).toBe(false); }); + test('it renders the duplicate icon timeline when actionTimelineToShow is including the action duplicate', () => { + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="open-duplicate"]').exists()).toBe(true); + }); + + test('it does NOT render the duplicate timeline when actionTimelineToShow is NOT including the action duplicate)', () => { + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="open-duplicate"]').exists()).toBe(false); + }); + test('it does NOT render the delete timeline (trash icon) when deleteTimelines is not provided', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -111,6 +168,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingSavedObjectId} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingSavedObjectId.length} @@ -141,6 +199,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -174,6 +233,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx index 51dba21ac225c2..2b8bd3339cca24 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx @@ -12,19 +12,24 @@ import React from 'react'; import { ACTION_COLUMN_WIDTH } from './common_styles'; import { DeleteTimelineModalButton } from '../delete_timeline_modal'; import * as i18n from '../translations'; -import { DeleteTimelines, OnOpenTimeline, OpenTimelineResult } from '../types'; +import { + ActionTimelineToShow, + DeleteTimelines, + OnOpenTimeline, + OpenTimelineResult, +} from '../types'; /** * Returns the action columns (e.g. delete, open duplicate timeline) */ export const getActionsColumns = ({ + actionTimelineToShow, onOpenTimeline, deleteTimelines, - showDeleteAction, }: { + actionTimelineToShow: ActionTimelineToShow[]; deleteTimelines?: DeleteTimelines; onOpenTimeline: OnOpenTimeline; - showDeleteAction: boolean; }) => { const openAsDuplicateColumn = { align: 'center', @@ -67,7 +72,10 @@ export const getActionsColumns = ({ width: ACTION_COLUMN_WIDTH, }; - return showDeleteAction && deleteTimelines != null - ? [openAsDuplicateColumn, deleteTimelineColumn] - : [openAsDuplicateColumn]; + return [ + actionTimelineToShow.includes('duplicate') ? openAsDuplicateColumn : null, + actionTimelineToShow.includes('delete') && deleteTimelines != null + ? deleteTimelineColumn + : null, + ].filter(action => action != null); }; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx index 559ee4a7bb4946..0f2cda9d79f0b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx @@ -37,6 +37,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={hasNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={hasNotes.length} @@ -63,6 +64,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingNotes.length} @@ -89,6 +91,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={nullNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={nullNotes.length} @@ -115,6 +118,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={emptylNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={emptylNotes.length} @@ -143,6 +147,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingSavedObjectId} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingSavedObjectId.length} @@ -169,6 +174,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={nullSavedObjectId} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={nullSavedObjectId.length} @@ -195,6 +201,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={hasNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={hasNotes.length} @@ -231,6 +238,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={hasNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={hasNotes.length} @@ -269,6 +277,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={hasNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={hasNotes.length} @@ -311,6 +320,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={hasNotes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={hasNotes.length} @@ -346,6 +356,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -377,6 +388,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -411,6 +423,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingSavedObjectId} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingSavedObjectId.length} @@ -442,6 +455,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingTitle} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingTitle.length} @@ -475,6 +489,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={withMissingSavedObjectIdAndTitle} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={withMissingSavedObjectIdAndTitle.length} @@ -508,6 +523,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={withJustWhitespaceTitle} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={withJustWhitespaceTitle.length} @@ -541,6 +557,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={withMissingSavedObjectId} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={withMissingSavedObjectId.length} @@ -571,6 +588,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -605,6 +623,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingSavedObjectId} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingSavedObjectId.length} @@ -637,6 +656,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -673,6 +693,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -704,6 +725,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -737,6 +759,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingDescription} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingDescription.length} @@ -771,6 +794,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={justWhitespaceDescription} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={justWhitespaceDescription.length} @@ -803,6 +827,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -834,6 +859,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -868,6 +894,7 @@ describe('#getCommonColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingUpdated} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingUpdated.length} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx index 0b51bd78283c5a..0d3a73a389050d 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx @@ -27,11 +27,9 @@ export const getCommonColumns = ({ itemIdToExpandedNotesRowMap, onOpenTimeline, onToggleShowNotes, - showExtendedColumnsAndActions, }: { onOpenTimeline: OnOpenTimeline; onToggleShowNotes: OnToggleShowNotes; - showExtendedColumnsAndActions: boolean; itemIdToExpandedNotesRowMap: Record; }) => [ { diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx index bc88603721e8a2..4cbe1e45c473b9 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx @@ -35,6 +35,7 @@ describe('#getExtendedColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -66,6 +67,7 @@ describe('#getExtendedColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -99,6 +101,7 @@ describe('#getExtendedColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={missingUpdatedBy} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={missingUpdatedBy.length} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx index 26836787efab10..31377d176acac9 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx @@ -30,6 +30,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -57,6 +58,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={with6Events} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={with6Events.length} @@ -82,6 +84,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -109,6 +112,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={with4Notes} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={with4Notes.length} @@ -134,6 +138,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -161,6 +166,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={undefinedFavorite} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={undefinedFavorite.length} @@ -187,6 +193,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={nullFavorite} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={nullFavorite.length} @@ -213,6 +220,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={emptyFavorite} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={emptyFavorite.length} @@ -249,6 +257,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={emptyFavorite} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={emptyFavorite.length} @@ -289,6 +298,7 @@ describe('#getActionsColumns', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={emptyFavorite} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={emptyFavorite.length} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx index 7d947cb28e9ece..26d9607a91fcdb 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx @@ -28,10 +28,11 @@ describe('TimelinesTable', () => { mockResults = cloneDeep(mockTimelineResults); }); - test('it renders the select all timelines header checkbox when showExtendedColumnsAndActions is true', () => { + test('it renders the select all timelines header checkbox when actionTimelineToShow has the action selectable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -59,10 +60,11 @@ describe('TimelinesTable', () => { ).toBe(true); }); - test('it does NOT render the select all timelines header checkbox when showExtendedColumnsAndActions is false', () => { + test('it does NOT render the select all timelines header checkbox when actionTimelineToShow has not the action selectable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -90,10 +92,11 @@ describe('TimelinesTable', () => { ).toBe(false); }); - test('it renders the Modified By column when showExtendedColumnsAndActions is true ', () => { + test('it renders the Modified By column when showExtendedColumns is true ', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -121,10 +124,11 @@ describe('TimelinesTable', () => { ).toContain(i18n.MODIFIED_BY); }); - test('it renders the notes column in the position of the Modified By column when showExtendedColumnsAndActions is false', () => { + test('it renders the notes column in the position of the Modified By column when showExtendedColumns is false', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -148,16 +152,17 @@ describe('TimelinesTable', () => { wrapper .find('thead tr th') .at(5) - .find('[data-test-subj="notes-count-header-icon"]') + .find('svg[data-test-subj="notes-count-header-icon"]') .first() .exists() ).toBe(true); }); - test('it renders the delete timeline (trash icon) when showExtendedColumnsAndActions is true', () => { + test('it renders the delete timeline (trash icon) when actionTimelineToShow has the delete action', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -185,10 +190,11 @@ describe('TimelinesTable', () => { ).toBe(true); }); - test('it does NOT render the delete timeline (trash icon) when showExtendedColumnsAndActions is false', () => { + test('it does NOT render the delete timeline (trash icon) when actionTimelineToShow has NOT the delete action', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -216,10 +222,11 @@ describe('TimelinesTable', () => { ).toBe(false); }); - test('it renders the rows per page selector when showExtendedColumnsAndActions is true', () => { + test('it renders the rows per page selector when showExtendedColumns is true', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -247,10 +254,11 @@ describe('TimelinesTable', () => { ).toBe(true); }); - test('it does NOT render the rows per page selector when showExtendedColumnsAndActions is false', () => { + test('it does NOT render the rows per page selector when showExtendedColumns is false', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -284,6 +292,7 @@ describe('TimelinesTable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={defaultPageSize} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -311,10 +320,11 @@ describe('TimelinesTable', () => { ).toEqual('Rows per page: 123'); }); - test('it sorts the Last Modified column in descending order when showExtendedColumnsAndActions is true ', () => { + test('it sorts the Last Modified column in descending order when showExtendedColumns is true ', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -342,10 +352,11 @@ describe('TimelinesTable', () => { ).toContain(i18n.LAST_MODIFIED); }); - test('it sorts the Last Modified column in descending order when showExtendedColumnsAndActions is false ', () => { + test('it sorts the Last Modified column in descending order when showExtendedColumns is false ', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -376,6 +387,7 @@ describe('TimelinesTable', () => { test('it displays the expected message when no search results are found', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={[]} - showExtendedColumnsAndActions={false} + showExtendedColumns={false} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={0} @@ -408,6 +420,7 @@ describe('TimelinesTable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -446,6 +459,7 @@ describe('TimelinesTable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -479,6 +493,7 @@ describe('TimelinesTable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} @@ -510,6 +525,7 @@ describe('TimelinesTable', () => { const wrapper = mountWithIntl( { pageIndex={0} pageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} searchResults={mockResults} - showExtendedColumnsAndActions={true} + showExtendedColumns={true} sortDirection={DEFAULT_SORT_DIRECTION} sortField={DEFAULT_SORT_FIELD} totalSearchResultsCount={mockResults.length} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.tsx index ce88ade01d2ef3..f09a9f6af048b5 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.tsx @@ -10,6 +10,7 @@ import styled from 'styled-components'; import * as i18n from '../translations'; import { + ActionTimelineToShow, DeleteTimelines, OnOpenTimeline, OnSelectionChange, @@ -36,8 +37,8 @@ const BasicTable = styled(EuiBasicTable)` `; BasicTable.displayName = 'BasicTable'; -const getExtendedColumnsIfEnabled = (showExtendedColumnsAndActions: boolean) => - showExtendedColumnsAndActions ? [...getExtendedColumns()] : []; +const getExtendedColumnsIfEnabled = (showExtendedColumns: boolean) => + showExtendedColumns ? [...getExtendedColumns()] : []; /** * Returns the column definitions (passed as the `columns` prop to @@ -46,34 +47,36 @@ const getExtendedColumnsIfEnabled = (showExtendedColumnsAndActions: boolean) => * `Timelines` page */ const getTimelinesTableColumns = ({ + actionTimelineToShow, deleteTimelines, itemIdToExpandedNotesRowMap, onOpenTimeline, onToggleShowNotes, - showExtendedColumnsAndActions, + showExtendedColumns, }: { + actionTimelineToShow: ActionTimelineToShow[]; deleteTimelines?: DeleteTimelines; itemIdToExpandedNotesRowMap: Record; onOpenTimeline: OnOpenTimeline; onToggleShowNotes: OnToggleShowNotes; - showExtendedColumnsAndActions: boolean; + showExtendedColumns: boolean; }) => [ ...getCommonColumns({ itemIdToExpandedNotesRowMap, onOpenTimeline, onToggleShowNotes, - showExtendedColumnsAndActions, }), - ...getExtendedColumnsIfEnabled(showExtendedColumnsAndActions), + ...getExtendedColumnsIfEnabled(showExtendedColumns), ...getIconHeaderColumns(), ...getActionsColumns({ deleteTimelines, onOpenTimeline, - showDeleteAction: showExtendedColumnsAndActions, + actionTimelineToShow, }), ]; export interface TimelinesTableProps { + actionTimelineToShow: ActionTimelineToShow[]; deleteTimelines?: DeleteTimelines; defaultPageSize: number; loading: boolean; @@ -85,7 +88,7 @@ export interface TimelinesTableProps { pageIndex: number; pageSize: number; searchResults: OpenTimelineResult[]; - showExtendedColumnsAndActions: boolean; + showExtendedColumns: boolean; sortDirection: 'asc' | 'desc'; sortField: string; totalSearchResultsCount: number; @@ -97,6 +100,7 @@ export interface TimelinesTableProps { */ export const TimelinesTable = React.memo( ({ + actionTimelineToShow, deleteTimelines, defaultPageSize, loading: isLoading, @@ -108,13 +112,13 @@ export const TimelinesTable = React.memo( pageIndex, pageSize, searchResults, - showExtendedColumnsAndActions, + showExtendedColumns, sortField, sortDirection, totalSearchResultsCount, }) => { const pagination = { - hidePerPageOptions: !showExtendedColumnsAndActions, + hidePerPageOptions: !showExtendedColumns, pageIndex, pageSize, pageSizeOptions: [ @@ -142,16 +146,17 @@ export const TimelinesTable = React.memo( return ( ( noItemsMessage={i18n.ZERO_TIMELINES_MATCH} onChange={onTableChange} pagination={pagination} - selection={showExtendedColumnsAndActions ? selection : undefined} + selection={actionTimelineToShow.includes('selectable') ? selection : undefined} sorting={sorting} /> ); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts index 7bbefb9efa99e1..e5e85ccf0954a2 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts @@ -95,6 +95,8 @@ export interface OnTableChangeParams { /** Invoked by the EUI table implementation when the user interacts with the table */ export type OnTableChange = (tableChange: OnTableChangeParams) => void; +export type ActionTimelineToShow = 'duplicate' | 'delete' | 'selectable'; + export interface OpenTimelineProps { /** Invoked when the user clicks the delete (trash) icon on an individual timeline */ deleteTimelines?: DeleteTimelines; @@ -140,6 +142,8 @@ export interface OpenTimelineProps { title: string; /** The total (server-side) count of the search results */ totalSearchResultsCount: number; + /** Hide action on timeline if needed it */ + hideActions?: ActionTimelineToShow[]; } export interface UpdateTimeline { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx new file mode 100644 index 00000000000000..ac47b352a6276e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx @@ -0,0 +1,276 @@ +/* + * 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 { + EuiHighlight, + EuiInputPopover, + EuiSuperSelect, + EuiSelectable, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiTextColor, + EuiFilterButton, + EuiFilterGroup, + EuiSpacer, +} from '@elastic/eui'; +import { Option } from '@elastic/eui/src/components/selectable/types'; +import { isEmpty } from 'lodash/fp'; +import React, { memo, useCallback, useMemo, useState } from 'react'; +import { ListProps } from 'react-virtualized'; +import styled, { createGlobalStyle } from 'styled-components'; + +import { AllTimelinesQuery } from '../../../containers/timeline/all'; +import { getEmptyTagValue } from '../../empty_value'; +import { isUntitled } from '../../../components/open_timeline/helpers'; +import * as i18nTimeline from '../../../components/open_timeline/translations'; +import { SortFieldTimeline, Direction } from '../../../graphql/types'; +import * as i18n from './translations'; + +const SearchTimelineSuperSelectGlobalStyle = createGlobalStyle` + .euiPopover__panel.euiPopover__panel-isOpen.timeline-search-super-select-popover__popoverPanel { + visibility: hidden; + z-index: 0; + } +`; + +const MyEuiHighlight = styled(EuiHighlight)<{ selected: boolean }>` + padding-left: ${({ selected }) => (selected ? '3px' : '0px')}; +`; + +const MyEuiTextColor = styled(EuiTextColor)<{ selected: boolean }>` + padding-left: ${({ selected }) => (selected ? '20px' : '0px')}; +`; + +interface SearchTimelineSuperSelectProps { + isDisabled: boolean; + timelineId: string | null; + timelineTitle: string | null; + onTimelineChange: (timelineTitle: string, timelineId: string | null) => void; +} + +const basicSuperSelectOptions = [ + { + value: '-1', + inputDisplay: i18n.DEFAULT_TIMELINE_TITLE, + }, +]; + +const getBasicSelectableOptions = (timelineId: string) => [ + { + description: i18n.DEFAULT_TIMELINE_DESCRIPTION, + label: i18n.DEFAULT_TIMELINE_TITLE, + id: null, + title: i18n.DEFAULT_TIMELINE_TITLE, + checked: timelineId === '-1' ? 'on' : undefined, + } as Option, +]; + +const ORIGINAL_PAGE_SIZE = 50; +const POPOVER_HEIGHT = 260; +const TIMELINE_ITEM_HEIGHT = 50; +const SearchTimelineSuperSelectComponent: React.FC = ({ + isDisabled, + timelineId, + timelineTitle, + onTimelineChange, +}) => { + const [pageSize, setPageSize] = useState(ORIGINAL_PAGE_SIZE); + const [heightTrigger, setHeightTrigger] = useState(0); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [searchTimelineValue, setSearchTimelineValue] = useState(''); + const [onlyFavorites, setOnlyFavorites] = useState(false); + + const onSearchTimeline = useCallback(val => { + setSearchTimelineValue(val); + }, []); + + const handleClosePopover = useCallback(() => { + setIsPopoverOpen(false); + }, []); + + const handleOpenPopover = useCallback(() => { + setIsPopoverOpen(true); + }, []); + + const handleOnToggleOnlyFavorites = useCallback(() => { + setOnlyFavorites(!onlyFavorites); + }, [onlyFavorites]); + + const renderTimelineOption = useCallback((option, searchValue) => { + return ( + <> + {option.checked === 'on' && } + + {isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title} + +
+ + + {option.description != null && option.description.trim().length > 0 + ? option.description + : getEmptyTagValue()} + + + + ); + }, []); + + const handleTimelineChange = useCallback(options => { + const selectedTimeline = options.filter( + (option: { checked: string }) => option.checked === 'on' + ); + if (selectedTimeline != null && selectedTimeline.length > 0 && onTimelineChange != null) { + onTimelineChange( + isEmpty(selectedTimeline[0].title) + ? i18nTimeline.UNTITLED_TIMELINE + : selectedTimeline[0].title, + selectedTimeline[0].id + ); + } + setIsPopoverOpen(false); + }, []); + + const handleOnScroll = useCallback( + ( + totalTimelines: number, + totalCount: number, + { + clientHeight, + scrollHeight, + scrollTop, + }: { + clientHeight: number; + scrollHeight: number; + scrollTop: number; + } + ) => { + if (totalTimelines < totalCount) { + const clientHeightTrigger = clientHeight * 1.2; + if ( + scrollTop > 10 && + scrollHeight - scrollTop < clientHeightTrigger && + scrollHeight > heightTrigger + ) { + setHeightTrigger(scrollHeight); + setPageSize(pageSize + ORIGINAL_PAGE_SIZE); + } + } + }, + [heightTrigger, pageSize] + ); + + const superSelect = useMemo( + () => ( + + ), + [handleOpenPopover, isDisabled, timelineId, timelineTitle] + ); + + return ( + + + {({ timelines, loading, totalCount }) => ( + <> + + + + + {i18nTimeline.ONLY_FAVORITES} + + + + + + + ({ + description: t.description, + label: t.title, + id: t.savedObjectId, + key: `${t.title}-${index}`, + title: t.title, + checked: t.savedObjectId === timelineId ? 'on' : undefined, + } as Option) + ), + ]} + > + {(list, search) => ( + <> + {search} + {list} + + )} + + + )} + + + + ); +}; + +export const SearchTimelineSuperSelect = memo(SearchTimelineSuperSelectComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/translations.ts new file mode 100644 index 00000000000000..bffee407bc9995 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/translations.ts @@ -0,0 +1,25 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const DEFAULT_TIMELINE_TITLE = i18n.translate('xpack.siem.timeline.defaultTimelineTitle', { + defaultMessage: 'Default blank timeline', +}); + +export const DEFAULT_TIMELINE_DESCRIPTION = i18n.translate( + 'xpack.siem.timeline.defaultTimelineDescription', + { + defaultMessage: 'Timeline offered by default when creating new timeline.', + } +); + +export const SEARCH_BOX_TIMELINE_PLACEHOLDER = i18n.translate( + 'xpack.siem.timeline.searchBoxPlaceholder', + { + defaultMessage: 'e.g. timeline name or description', + } +); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index 9f3cba7189fb19..655299c4a2a34b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -70,8 +70,8 @@ export const RuleSchema = t.intersection([ risk_score: t.number, rule_id: t.string, severity: t.string, - type: t.string, tags: t.array(t.string), + type: t.string, to: t.string, threats: t.array(t.unknown), updated_at: t.string, @@ -79,6 +79,8 @@ export const RuleSchema = t.intersection([ }), t.partial({ saved_id: t.string, + timeline_id: t.string, + timeline_title: t.string, }), ]); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx index e8a2c98a94a562..9c95c74cd62a54 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx @@ -56,13 +56,13 @@ export const DetectionEngineContainer = React.memo(() => { - + - + - + diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx index f176109b1d7a53..469745262d9446 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx @@ -17,7 +17,7 @@ import { import { Action } from './reducer'; export const editRuleAction = (rule: Rule, history: H.History) => { - history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/${rule.id}/edit`); + history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${rule.id}/edit`); }; export const runRuleAction = () => {}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts index 1909b75a85835e..f5d39553142429 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts @@ -13,7 +13,7 @@ export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[] id: rule.id, rule_id: rule.rule_id, rule: { - href: `#/detection-engine/rules/${encodeURIComponent(rule.id)}`, + href: `#/detection-engine/rules/id/${encodeURIComponent(rule.id)}`, name: rule.name, status: 'Status Placeholder', }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx index f090f6d97eaf9d..725c7eeeedcfe4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx @@ -4,9 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty, EuiButtonIcon, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldText, + EuiSpacer, +} from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react'; +import styled from 'styled-components'; import * as RuleI18n from '../../translations'; import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; @@ -17,11 +26,26 @@ interface AddItemProps { dataTestSubj: string; idAria: string; isDisabled: boolean; + validate?: (args: unknown) => boolean; } -export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: AddItemProps) => { +const MyEuiFormRow = styled(EuiFormRow)` + .euiFormRow__labelWrapper { + .euiText { + padding-right: 32px; + } + } +`; + +export const AddItem = ({ + addText, + dataTestSubj, + field, + idAria, + isDisabled, + validate, +}: AddItemProps) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - // const [items, setItems] = useState(['']); const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(-1); const inputsRef = useRef([]); @@ -104,7 +128,7 @@ export const AddItem = ({ addText, dataTestSubj, field, idAria, isDisabled }: Ad const values = field.value as string[]; return ( - - + + updateItem(e, index)} fullWidth {...euiFieldProps} /> + + removeItem(index)} aria-label={RuleI18n.DELETE} /> - } - onChange={e => updateItem(e, index)} - compressed - fullWidth - {...euiFieldProps} - /> + + + {values.length - 1 !== index && } ); })} - + {addText} - + ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx new file mode 100644 index 00000000000000..09d0c1131ea10d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -0,0 +1,222 @@ +/* + * 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 { + EuiBadge, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiLink, + EuiText, + EuiListGroup, +} from '@elastic/eui'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { isEmpty } from 'lodash/fp'; +import React from 'react'; +import styled from 'styled-components'; + +import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; + +import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; + +import { FilterLabel } from './filter_label'; +import * as i18n from './translations'; +import { BuildQueryBarDescription, BuildThreatsDescription, ListItems } from './types'; + +const EuiBadgeWrap = styled(EuiBadge)` + .euiBadge__text { + white-space: pre-wrap !important; + } +`; + +export const buildQueryBarDescription = ({ + field, + filters, + filterManager, + query, + savedId, + indexPatterns, +}: BuildQueryBarDescription): ListItems[] => { + let items: ListItems[] = []; + if (!isEmpty(filters)) { + filterManager.setFilters(filters); + items = [ + ...items, + { + title: <>{i18n.FILTERS_LABEL} , + description: ( + + {filterManager.getFilters().map((filter, index) => ( + + + {indexPatterns != null ? ( + + ) : ( + + )} + + + ))} + + ), + }, + ]; + } + if (!isEmpty(query.query)) { + items = [ + ...items, + { + title: <>{i18n.QUERY_LABEL} , + description: <>{query.query} , + }, + ]; + } + if (!isEmpty(savedId)) { + items = [ + ...items, + { + title: <>{i18n.SAVED_ID_LABEL} , + description: <>{savedId} , + }, + ]; + } + return items; +}; + +const ThreatsEuiFlexGroup = styled(EuiFlexGroup)` + .euiFlexItem { + margin-bottom: 0px; + } +`; + +const MyEuiListGroup = styled(EuiListGroup)` + padding: 0px; + .euiListGroupItem__button { + padding: 0px; + } +`; + +export const buildThreatsDescription = ({ + label, + threats, +}: BuildThreatsDescription): ListItems[] => { + if (threats.length > 0) { + return [ + { + title: label, + description: ( + + {threats.map((threat, index) => { + const tactic = tacticsOptions.find(t => t.name === threat.tactic.name); + return ( + + +
+ + {tactic != null ? tactic.text : ''} + +
+ { + const myTechnique = techniquesOptions.find(t => t.name === technique.name); + return { + label: myTechnique != null ? myTechnique.label : '', + href: technique.reference, + target: '_blank', + }; + })} + /> +
+
+ ); + })} +
+ ), + }, + ]; + } + return []; +}; + +export const buildStringArrayDescription = ( + label: string, + field: string, + values: string[] +): ListItems[] => { + if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) { + return [ + { + title: label, + description: ( + + {values.map((val: string) => + isEmpty(val) ? null : ( + + {val} + + ) + )} + + ), + }, + ]; + } + return []; +}; + +export const buildSeverityDescription = (label: string, value: string): ListItems[] => { + return [ + { + title: label, + description: ( + + {value} + + ), + }, + ]; +}; + +export const buildUrlsDescription = (label: string, values: string[]): ListItems[] => { + if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) { + return [ + { + title: label, + description: ( + ({ + label: val, + href: val, + iconType: 'link', + size: 'xs', + target: '_blank', + }))} + /> + ), + }, + ]; + } + return []; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index a05f43579e6694..198756fc2336bc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -4,19 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiBadge, - EuiDescriptionList, - EuiLoadingSpinner, - EuiFlexGroup, - EuiFlexItem, - EuiTextArea, - EuiLink, - EuiText, - EuiListGroup, -} from '@elastic/eui'; +import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem, EuiTextArea } from '@elastic/eui'; import { isEmpty, chunk, get, pick } from 'lodash/fp'; -import React, { memo, ReactNode, useState } from 'react'; +import React, { memo, useState } from 'react'; import styled from 'styled-components'; import { @@ -25,13 +15,19 @@ import { FilterManager, Query, } from '../../../../../../../../../../src/plugins/data/public'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/search_super_select/translations'; import { useKibana } from '../../../../../lib/kibana'; -import { FilterLabel } from './filter_label'; -import { FormSchema } from '../shared_imports'; -import * as I18n from './translations'; - import { IMitreEnterpriseAttack } from '../../types'; -import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; +import { FieldValueTimeline } from '../pick_timeline'; +import { FormSchema } from '../shared_imports'; +import { ListItems } from './types'; +import { + buildQueryBarDescription, + buildSeverityDescription, + buildStringArrayDescription, + buildThreatsDescription, + buildUrlsDescription, +} from './helpers'; interface StepRuleDescriptionProps { direction?: 'row' | 'column'; @@ -40,29 +36,10 @@ interface StepRuleDescriptionProps { schema: FormSchema; } -const EuiBadgeWrap = styled(EuiBadge)` - .euiBadge__text { - white-space: pre-wrap !important; - } -`; - const EuiFlexItemWidth = styled(EuiFlexItem)<{ direction: string }>` ${props => (props.direction === 'row' ? 'width : 50%;' : 'width: 100%;')}; `; -const MyEuiListGroup = styled(EuiListGroup)` - padding: 0px; - .euiListGroupItem__button { - padding: 0px; - } -`; - -const ThreatsEuiFlexGroup = styled(EuiFlexGroup)` - .euiFlexItem { - margin-bottom: 0px; - } -`; - const MyEuiTextArea = styled(EuiTextArea)` max-width: 100%; height: 80px; @@ -87,9 +64,9 @@ const StepRuleDescriptionComponent: React.FC = ({ ); return ( - {chunk(Math.ceil(listItems.length / 2), listItems).map((chunckListItems, index) => ( + {chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => ( - + ))} @@ -98,11 +75,6 @@ const StepRuleDescriptionComponent: React.FC = ({ export const StepRuleDescription = memo(StepRuleDescriptionComponent); -interface ListItems { - title: NonNullable; - description: NonNullable; -} - const buildListItems = ( data: unknown, schema: FormSchema, @@ -130,103 +102,23 @@ const getDescriptionItem = ( filterManager: FilterManager, indexPatterns?: IIndexPattern ): ListItems[] => { - if (field === 'useIndicesConfig') { - return []; - } else if (field === 'queryBar') { + if (field === 'queryBar') { const filters = get('queryBar.filters', value) as esFilters.Filter[]; const query = get('queryBar.query', value) as Query; const savedId = get('queryBar.saved_id', value); - let items: ListItems[] = []; - if (!isEmpty(filters)) { - filterManager.setFilters(filters); - items = [ - ...items, - { - title: <>{I18n.FILTERS_LABEL}, - description: ( - - {filterManager.getFilters().map((filter, index) => ( - - - {indexPatterns != null ? ( - - ) : ( - - )} - - - ))} - - ), - }, - ]; - } - if (!isEmpty(query.query)) { - items = [ - ...items, - { - title: <>{I18n.QUERY_LABEL}, - description: <>{query.query}, - }, - ]; - } - if (!isEmpty(savedId)) { - items = [ - ...items, - { - title: <>{I18n.SAVED_ID_LABEL}, - description: <>{savedId}, - }, - ]; - } - return items; + return buildQueryBarDescription({ + field, + filters, + filterManager, + query, + savedId, + indexPatterns, + }); } else if (field === 'threats') { const threats: IMitreEnterpriseAttack[] = get(field, value).filter( (threat: IMitreEnterpriseAttack) => threat.tactic.name !== 'none' ); - if (threats.length > 0) { - return [ - { - title: label, - description: ( - - {threats.map((threat, index) => { - const tactic = tacticsOptions.find(t => t.name === threat.tactic.name); - return ( - - -
- - {tactic != null ? tactic.text : ''} - -
- { - const myTechnique = techniquesOptions.find( - t => t.name === technique.name - ); - return { - label: myTechnique != null ? myTechnique.label : '', - href: technique.reference, - target: '_blank', - }; - })} - /> -
-
- ); - })} -
- ), - }, - ]; - } - return []; + return buildThreatsDescription({ label, threats }); } else if (field === 'description') { return [ { @@ -234,27 +126,23 @@ const getDescriptionItem = ( description: , }, ]; + } else if (field === 'references') { + const urls: string[] = get(field, value); + return buildUrlsDescription(label, urls); } else if (Array.isArray(get(field, value))) { const values: string[] = get(field, value); - if (!isEmpty(values) && values.filter(val => !isEmpty(val)).length > 0) { - return [ - { - title: label, - description: ( - - {values.map((val: string) => - isEmpty(val) ? null : ( - - {val} - - ) - )} - - ), - }, - ]; - } - return []; + return buildStringArrayDescription(label, field, values); + } else if (field === 'severity') { + const val: string = get(field, value); + return buildSeverityDescription(label, val); + } else if (field === 'timeline') { + const timeline = get(field, value) as FieldValueTimeline; + return [ + { + title: label, + description: timeline.title ?? DEFAULT_TIMELINE_TITLE, + }, + ]; } const description: string = get(field, value); if (!isEmpty(description)) { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts new file mode 100644 index 00000000000000..d32fbcd725d12f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts @@ -0,0 +1,33 @@ +/* + * 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 { ReactNode } from 'react'; + +import { + IIndexPattern, + esFilters, + FilterManager, + Query, +} from '../../../../../../../../../../src/plugins/data/public'; +import { IMitreEnterpriseAttack } from '../../types'; + +export interface ListItems { + title: NonNullable; + description: NonNullable; +} + +export interface BuildQueryBarDescription { + field: string; + filters: esFilters.Filter[]; + filterManager: FilterManager; + query: Query; + savedId: string; + indexPatterns?: IIndexPattern; +} + +export interface BuildThreatsDescription { + label: string; + threats: IMitreEnterpriseAttack[]; +} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts new file mode 100644 index 00000000000000..1202fe54ad194f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts @@ -0,0 +1,18 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; + +import { IMitreAttack } from '../../types'; + +export const isMitreAttackInvalid = ( + tacticName: string | null | undefined, + techniques: IMitreAttack[] | null | undefined +) => { + if (isEmpty(tacticName) || (tacticName !== 'none' && isEmpty(techniques))) { + return true; + } + return false; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx index a777506ee12ae5..97c4c2fdd050aa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx @@ -8,27 +8,30 @@ import { EuiButtonEmpty, EuiButtonIcon, EuiFormRow, - EuiSelect, + EuiSuperSelect, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiComboBox, - EuiFormControlLayout, + EuiText, } from '@elastic/eui'; import { isEmpty, kebabCase, camelCase } from 'lodash/fp'; -import React, { ChangeEvent, useCallback } from 'react'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; -import * as RuleI18n from '../../translations'; +import * as Rulei18n from '../../translations'; import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; -import * as I18n from './translations'; +import { threatsDefault } from '../step_about_rule/default_value'; import { IMitreEnterpriseAttack } from '../../types'; +import { isMitreAttackInvalid } from './helpers'; +import * as i18n from './translations'; -const MyEuiFormControlLayout = styled(EuiFormControlLayout)` - &.euiFormControlLayout--compressed { - height: fit-content !important; - } +const MitreContainer = styled.div` + margin-top: 16px; +`; +const MyEuiSuperSelect = styled(EuiSuperSelect)` + width: 280px; `; interface AddItemProps { field: FieldHook; @@ -43,7 +46,12 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI const removeItem = useCallback( (index: number) => { const values = field.value as string[]; - field.setValue([...values.slice(0, index), ...values.slice(index + 1)]); + const newValues = [...values.slice(0, index), ...values.slice(index + 1)]; + if (isEmpty(newValues)) { + field.setValue(threatsDefault); + } else { + field.setValue(newValues); + } }, [field] ); @@ -61,9 +69,9 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI }, [field]); const updateTactic = useCallback( - (index: number, event: ChangeEvent) => { + (index: number, value: string) => { const values = field.value as IMitreEnterpriseAttack[]; - const { id, reference, name } = tacticsOptions.find(t => t.value === event.target.value) || { + const { id, reference, name } = tacticsOptions.find(t => t.value === value) || { id: '', name: '', reference: '', @@ -97,75 +105,104 @@ export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddI const values = field.value as IMitreEnterpriseAttack[]; + const getSelectTactic = (tacticName: string, index: number, disabled: boolean) => ( + {i18n.TACTIC_PLACEHOLDER}, + value: 'none', + disabled, + }, + ] + : []), + ...tacticsOptions.map(t => ({ + inputDisplay: <>{t.text}, + value: t.value, + disabled, + })), + ]} + aria-label="" + onChange={updateTactic.bind(null, index)} + fullWidth={false} + valueOfSelected={camelCase(tacticName)} + /> + ); + + const getSelectTechniques = (item: IMitreEnterpriseAttack, index: number, disabled: boolean) => { + const invalid = isMitreAttackInvalid(item.tactic.name, item.techniques); + return ( + + + t.tactics.includes(kebabCase(item.tactic.name)))} + selectedOptions={item.techniques} + onChange={updateTechniques.bind(null, index)} + isDisabled={disabled} + fullWidth={true} + isInvalid={invalid} + /> + {invalid && ( + +

{errorMessage}

+
+ )} +
+ + removeItem(index)} + aria-label={Rulei18n.DELETE} + /> + +
+ ); + }; + return ( - - <> - {values.map((item, index) => { - const euiSelectFieldProps = { - disabled: isDisabled, - }; - return ( -
- - - ({ text: t.text, value: t.value })), - ]} - aria-label="" - onChange={updateTactic.bind(null, index)} - prepend={I18n.TACTIC} - compressed - fullWidth={false} - value={camelCase(item.tactic.name)} - {...euiSelectFieldProps} - /> - - - - - t.tactics.includes(kebabCase(item.tactic.name)) - )} - selectedOptions={item.techniques} - onChange={updateTechniques.bind(null, index)} - isDisabled={isDisabled} - fullWidth={true} - /> - - - - removeItem(index)} - aria-label={RuleI18n.DELETE} - /> - - - {values.length - 1 !== index && } -
- ); - })} - - {I18n.ADD_MITRE_ATTACK} - - -
+ + {values.map((item, index) => ( +
+ + + {index === 0 ? ( + + <>{getSelectTactic(item.tactic.name, index, isDisabled)} + + ) : ( + getSelectTactic(item.tactic.name, index, isDisabled) + )} + + + {index === 0 ? ( + + <>{getSelectTechniques(item, index, isDisabled)} + + ) : ( + getSelectTechniques(item, index, isDisabled) + )} + + + {values.length - 1 !== index && } +
+ ))} + + {i18n.ADD_MITRE_ATTACK} + +
); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts index 22ee6cc3ef911f..dd4c55c1503ec4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts @@ -7,13 +7,13 @@ import { i18n } from '@kbn/i18n'; export const TACTIC = i18n.translate('xpack.siem.detectionEngine.mitreAttack.tacticsDescription', { - defaultMessage: 'Tactic', + defaultMessage: 'tactic', }); -export const TECHNIQUES = i18n.translate( +export const TECHNIQUE = i18n.translate( 'xpack.siem.detectionEngine.mitreAttack.techniquesDescription', { - defaultMessage: 'Techniques', + defaultMessage: 'technique', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx new file mode 100644 index 00000000000000..873e0c2184c61d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx @@ -0,0 +1,74 @@ +/* + * 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 { EuiFormRow } from '@elastic/eui'; +import React, { useCallback, useEffect, useState } from 'react'; + +import { SearchTimelineSuperSelect } from '../../../../../components/timeline/search_super_select'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; + +export interface FieldValueTimeline { + id: string | null; + title: string | null; +} + +interface QueryBarDefineRuleProps { + dataTestSubj: string; + field: FieldHook; + idAria: string; + isDisabled: boolean; +} + +export const PickTimeline = ({ + dataTestSubj, + field, + idAria, + isDisabled = false, +}: QueryBarDefineRuleProps) => { + const [timelineId, setTimelineId] = useState(null); + const [timelineTitle, setTimelineTitle] = useState(null); + + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + useEffect(() => { + const { id, title } = field.value as FieldValueTimeline; + if (timelineTitle !== title && timelineId !== id) { + setTimelineId(id); + setTimelineTitle(title); + } + }, [field.value]); + + const handleOnTimelineChange = useCallback( + (title: string, id: string | null) => { + if (id === null) { + field.setValue({ id, title: null }); + } else if (timelineTitle !== title && timelineId !== id) { + field.setValue({ id, title }); + } + }, + [field] + ); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx index c294ec24c4cb70..3e39beb6e61b78 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -6,7 +6,7 @@ import { EuiFormRow, EuiMutationObserver } from '@elastic/eui'; import { isEqual } from 'lodash/fp'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Subscription } from 'rxjs'; import styled from 'styled-components'; @@ -19,11 +19,18 @@ import { SavedQueryTimeFilter, } from '../../../../../../../../../../src/plugins/data/public'; +import { BrowserFields } from '../../../../../containers/source'; +import { OpenTimelineModal } from '../../../../../components/open_timeline/open_timeline_modal'; +import { ActionTimelineToShow } from '../../../../../components/open_timeline/types'; import { QueryBar } from '../../../../../components/query_bar'; +import { buildGlobalQuery } from '../../../../../components/timeline/helpers'; +import { getDataProviderFilter } from '../../../../../components/timeline/query_bar'; +import { convertKueryToElasticSearchQuery } from '../../../../../lib/keury'; import { useKibana } from '../../../../../lib/kibana'; +import { TimelineModel } from '../../../../../store/timeline/model'; import { useSavedQueryServices } from '../../../../../utils/saved_query_services'; - import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import * as i18n from './translations'; export interface FieldValueQueryBar { filters: esFilters.Filter[]; @@ -31,11 +38,14 @@ export interface FieldValueQueryBar { saved_id: string | null; } interface QueryBarDefineRuleProps { + browserFields: BrowserFields; dataTestSubj: string; field: FieldHook; idAria: string; isLoading: boolean; indexPattern: IIndexPattern; + onCloseTimelineSearch: () => void; + openTimelineSearch: boolean; resizeParentContainer?: (height: number) => void; } @@ -56,14 +66,18 @@ const StyledEuiFormRow = styled(EuiFormRow)` // TODO need to add disabled in the SearchBar export const QueryBarDefineRule = ({ + browserFields, dataTestSubj, field, idAria, indexPattern, isLoading = false, + onCloseTimelineSearch, + openTimelineSearch = false, resizeParentContainer, }: QueryBarDefineRuleProps) => { const [originalHeight, setOriginalHeight] = useState(-1); + const [loadingTimeline, setLoadingTimeline] = useState(false); const [savedQuery, setSavedQuery] = useState(null); const [queryDraft, setQueryDraft] = useState({ query: '', language: 'kuery' }); const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); @@ -168,6 +182,38 @@ export const QueryBarDefineRule = ({ [field.value] ); + const onCloseTimelineModal = useCallback(() => { + setLoadingTimeline(true); + onCloseTimelineSearch(); + }, [onCloseTimelineSearch]); + + const onOpenTimeline = useCallback( + (timeline: TimelineModel) => { + setLoadingTimeline(false); + const newQuery = { + query: timeline.kqlQuery.filterQuery?.kuery?.expression ?? '', + language: timeline.kqlQuery.filterQuery?.kuery?.kind ?? 'kuery', + }; + const dataProvidersDsl = + timeline.dataProviders != null && timeline.dataProviders.length > 0 + ? convertKueryToElasticSearchQuery( + buildGlobalQuery(timeline.dataProviders, browserFields), + indexPattern + ) + : ''; + const newFilters = timeline.filters ?? []; + field.setValue({ + filters: + dataProvidersDsl !== '' + ? [...newFilters, getDataProviderFilter(dataProvidersDsl)] + : newFilters, + query: newQuery, + saved_id: '', + }); + }, + [browserFields, field, indexPattern] + ); + const onMutation = (event: unknown, observer: unknown) => { if (resizeParentContainer != null) { const suggestionContainer = document.getElementById('kbnTypeahead__items'); @@ -189,39 +235,51 @@ export const QueryBarDefineRule = ({ } }; + const actionTimelineToHide = useMemo(() => ['duplicate'], []); + return ( - - + - {mutationRef => ( -
- -
- )} -
-
+ + {mutationRef => ( +
+ +
+ )} +
+ + {openTimelineSearch ? ( + + ) : null} + ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx new file mode 100644 index 00000000000000..9b14e4f8599dac --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx @@ -0,0 +1,14 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const IMPORT_TIMELINE_MODAL = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.importTimelineModalTitle', + { + defaultMessage: 'Import query from saved timeline', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx index 2e57ff8ba2c4f0..8097c27cddfe8b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFieldNumber, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFieldNumber, EuiFormRow, EuiSelect } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; @@ -32,6 +32,10 @@ const StyledEuiFormRow = styled(EuiFormRow)` } `; +const MyEuiSelect = styled(EuiSelect)` + width: auto; +`; + export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: ScheduleItemProps) => { const [timeType, setTimeType] = useState('s'); const [timeVal, setTimeVal] = useState(0); @@ -79,22 +83,33 @@ export const ScheduleItem = ({ dataTestSubj, field, idAria, isDisabled }: Schedu // EUI missing some props const rest = { disabled: isDisabled }; + const label = useMemo( + () => ( + + + {field.label} + + + {field.labelAppend} + + + ), + [field.label, field.labelAppend] + ); return ( } - compressed fullWidth min={0} onChange={onChangeTimeVal} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx index 48ff0d80d0398f..3ec5bf1a12eb0f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx @@ -27,7 +27,7 @@ const RuleStatusIconStyled = styled.div` const RuleStatusIconComponent: React.FC = ({ name, type }) => { const theme = useEuiTheme(); - const color = type === 'passive' ? theme.euiColorLightestShade : theme.euiColorDarkestShade; + const color = type === 'passive' ? theme.euiColorLightestShade : theme.euiColorPrimary; return ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.ts deleted file mode 100644 index 7d6e434bcc8c6c..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 I18n from './translations'; - -export type SeverityValue = 'low' | 'medium' | 'high' | 'critical'; - -interface SeverityOptionItem { - value: SeverityValue; - text: string; -} - -export const severityOptions: SeverityOptionItem[] = [ - { value: 'low', text: I18n.LOW }, - { value: 'medium', text: I18n.MEDIUM }, - { value: 'high', text: I18n.HIGH }, - { value: 'critical', text: I18n.CRITICAL }, -]; - -export const defaultRiskScoreBySeverity: Record = { - low: 21, - medium: 47, - high: 73, - critical: 99, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx new file mode 100644 index 00000000000000..9fb64189ebd1af --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx @@ -0,0 +1,43 @@ +/* + * 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 { EuiHealth } from '@elastic/eui'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import React from 'react'; +import * as I18n from './translations'; + +export type SeverityValue = 'low' | 'medium' | 'high' | 'critical'; + +interface SeverityOptionItem { + value: SeverityValue; + inputDisplay: React.ReactElement; +} + +export const severityOptions: SeverityOptionItem[] = [ + { + value: 'low', + inputDisplay: {I18n.LOW}, + }, + { + value: 'medium', + inputDisplay: {I18n.MEDIUM} , + }, + { + value: 'high', + inputDisplay: {I18n.HIGH} , + }, + { + value: 'critical', + inputDisplay: {I18n.CRITICAL} , + }, +]; + +export const defaultRiskScoreBySeverity: Record = { + low: 21, + medium: 47, + high: 73, + critical: 99, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts index c0c5ae77a1960e..328c4a0f960667 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts @@ -6,6 +6,14 @@ import { AboutStepRule } from '../../types'; +export const threatsDefault = [ + { + framework: 'MITRE ATT&CK', + tactic: { id: 'none', name: 'none', reference: 'none' }, + techniques: [], + }, +]; + export const stepAboutDefaultValue: AboutStepRule = { name: '', description: '', @@ -15,11 +23,9 @@ export const stepAboutDefaultValue: AboutStepRule = { references: [''], falsePositives: [''], tags: [], - threats: [ - { - framework: 'MITRE ATT&CK', - tactic: { id: 'none', name: 'none', reference: 'none' }, - techniques: [], - }, - ], + timeline: { + id: null, + title: null, + }, + threats: threatsDefault, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts new file mode 100644 index 00000000000000..99b01c8b22974c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts @@ -0,0 +1,16 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; + +const urlExpression = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi; + +export const isUrlInvalid = (url: string | null | undefined) => { + if (!isEmpty(url) && url != null && url.match(urlExpression) == null) { + return true; + } + return false; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index e266c0b9ab47d6..8956776dcd3b21 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -7,17 +7,21 @@ import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEqual, get } from 'lodash/fp'; import React, { memo, useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; import { RuleStepProps, RuleStep, AboutStepRule } from '../../types'; import * as RuleI18n from '../../translations'; -import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; import { AddItem } from '../add_item_form'; +import { StepRuleDescription } from '../description_step'; +import { AddMitreThreat } from '../mitre'; +import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; + import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data'; import { stepAboutDefaultValue } from './default_value'; +import { isUrlInvalid } from './helpers'; import { schema } from './schema'; import * as I18n from './translations'; -import { StepRuleDescription } from '../description_step'; -import { AddMitreThreat } from '../mitre'; +import { PickTimeline } from '../pick_timeline'; const CommonUseField = getUseField({ component: Field }); @@ -25,6 +29,10 @@ interface StepAboutRuleProps extends RuleStepProps { defaultValues?: AboutStepRule | null; } +const TagContainer = styled.div` + margin-top: 16px; +`; + export const StepAboutRule = memo( ({ defaultValues, @@ -90,7 +98,6 @@ export const StepAboutRule = memo( idAria: 'detectionEngineStepAboutRuleName', 'data-test-subj': 'detectionEngineStepAboutRuleName', euiFieldProps: { - compressed: true, fullWidth: false, disabled: isLoading, }, @@ -99,11 +106,9 @@ export const StepAboutRule = memo( ( idAria: 'detectionEngineStepAboutRuleSeverity', 'data-test-subj': 'detectionEngineStepAboutRuleSeverity', euiFieldProps: { - compressed: true, fullWidth: false, disabled: isLoading, options: severityOptions, @@ -129,29 +133,38 @@ export const StepAboutRule = memo( euiFieldProps: { max: 100, min: 0, - compressed: true, fullWidth: false, disabled: isLoading, options: severityOptions, + showTicks: true, + tickInterval: 25, }, }} /> + ( path="threats" component={AddMitreThreat} componentProps={{ - compressed: true, idAria: 'detectionEngineStepAboutRuleMitreThreats', isDisabled: isLoading, dataTestSubj: 'detectionEngineStepAboutRuleMitreThreats', }} /> - + + + {({ severity }) => { const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; @@ -202,7 +216,7 @@ export const StepAboutRule = memo( > - {myStepData.isNew ? RuleI18n.CONTINUE : RuleI18n.UPDATE} + {RuleI18n.CONTINUE} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx index c72312bb908361..9355f1c8bfefa0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx @@ -6,7 +6,6 @@ import { EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { isEmpty } from 'lodash/fp'; import React from 'react'; import * as RuleI18n from '../../translations'; @@ -18,6 +17,8 @@ import { ValidationFunc, ERROR_CODE, } from '../shared_imports'; +import { isMitreAttackInvalid } from '../mitre/helpers'; +import { isUrlInvalid } from './helpers'; import * as I18n from './translations'; const { emptyField } = fieldValidators; @@ -63,7 +64,7 @@ export const schema: FormSchema = { ], }, severity: { - type: FIELD_TYPES.SELECT, + type: FIELD_TYPES.SUPER_SELECT, label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldSeverityLabel', { @@ -92,6 +93,14 @@ export const schema: FormSchema = { } ), }, + timeline: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel', + { + defaultMessage: 'Timeline template', + } + ), + }, references: { label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldReferenceUrlsLabel', @@ -100,6 +109,28 @@ export const schema: FormSchema = { } ), labelAppend: {RuleI18n.OPTIONAL_FIELD}, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path }] = args; + let hasError = false; + (value as string[]).forEach(url => { + if (isUrlInvalid(url)) { + hasError = true; + } + }); + return hasError + ? { + code: 'ERR_FIELD_FORMAT', + path, + message: I18n.URL_FORMAT_INVALID, + } + : undefined; + }, + }, + ], }, falsePositives: { label: i18n.translate( @@ -126,7 +157,7 @@ export const schema: FormSchema = { const [{ value, path }] = args; let hasError = false; (value as IMitreEnterpriseAttack[]).forEach(v => { - if (isEmpty(v.tactic.name) || (v.tactic.name !== 'none' && isEmpty(v.techniques))) { + if (isMitreAttackInvalid(v.tactic.name, v.techniques)) { hasError = true; } }); @@ -146,6 +177,13 @@ export const schema: FormSchema = { label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsLabel', { defaultMessage: 'Tags', }), + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsHelpText', + { + defaultMessage: + 'Type one or more custom identifying tags for this rule. Press enter after each tag to begin a new one.', + } + ), labelAppend: {RuleI18n.OPTIONAL_FIELD}, }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts index 017d4fe6fdf49e..052986480e9abf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts @@ -54,3 +54,10 @@ export const CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED = i18n.translate( defaultMessage: 'At least one Technique is required with a Tactic.', } ); + +export const URL_FORMAT_INVALID = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.referencesUrlInvalidError', + { + defaultMessage: 'Url is invalid format', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index cc4e959cc9c78b..ecd2ce442238fa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -4,8 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import { isEqual, get } from 'lodash/fp'; +import { + EuiButtonEmpty, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; +import { isEmpty, isEqual, get } from 'lodash/fp'; import React, { memo, useCallback, useState, useEffect } from 'react'; import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public'; @@ -18,7 +24,7 @@ import { StepRuleDescription } from '../description_step'; import { QueryBarDefineRule } from '../query_bar'; import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; import { schema } from './schema'; -import * as I18n from './translations'; +import * as i18n from './translations'; const CommonUseField = getUseField({ component: Field }); @@ -34,7 +40,6 @@ const stepDefineDefaultValue = { filters: [], saved_id: null, }, - useIndicesConfig: 'true', }; const getStepDefaultValue = ( @@ -45,7 +50,6 @@ const getStepDefaultValue = ( return { ...defaultValues, isNew: false, - useIndicesConfig: `${isEqual(defaultValues.index, indicesConfig)}`, }; } else { return { @@ -66,13 +70,22 @@ export const StepDefineRule = memo( setForm, setStepData, }) => { - const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(''); + const [openTimelineSearch, setOpenTimelineSearch] = useState(false); + const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(false); const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); + const [mylocalIndicesConfig, setMyLocalIndicesConfig] = useState( + defaultValues != null ? defaultValues.index : indicesConfig ?? [] + ); const [ - { indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, - setIndices, - ] = useFetchIndexPatterns(defaultValues != null ? defaultValues.index : indicesConfig ?? []); - const [myStepData, setMyStepData] = useState(stepDefineDefaultValue); + { + browserFields, + indexPatterns: indexPatternQueryBar, + isLoading: indexPatternLoadingQueryBar, + }, + ] = useFetchIndexPatterns(mylocalIndicesConfig); + const [myStepData, setMyStepData] = useState( + getStepDefaultValue(indicesConfig, null) + ); const { form } = useForm({ defaultValue: myStepData, @@ -96,7 +109,7 @@ export const StepDefineRule = memo( const myDefaultValues = getStepDefaultValue(indicesConfig, defaultValues); if (!isEqual(myDefaultValues, myStepData)) { setMyStepData(myDefaultValues); - setLocalUseIndicesConfig(myDefaultValues.useIndicesConfig); + setLocalUseIndicesConfig(isEqual(myDefaultValues.index, indicesConfig)); if (!isReadOnlyView) { Object.keys(schema).forEach(key => { const val = get(key, myDefaultValues); @@ -115,6 +128,19 @@ export const StepDefineRule = memo( } }, [form]); + const handleResetIndices = useCallback(() => { + const indexField = form.getFields().index; + indexField.setValue(indicesConfig); + }, [form, indicesConfig]); + + const handleOpenTimelineSearch = useCallback(() => { + setOpenTimelineSearch(true); + }, []); + + const handleCloseTimelineSearch = useCallback(() => { + setOpenTimelineSearch(false); + }, []); + return isReadOnlyView && myStepData != null ? ( ( ) : ( <>
- + {i18n.RESET_DEFAULT_INDEX} + + ) : null, + }} componentProps={{ idAria: 'detectionEngineStepDefineRuleIndices', 'data-test-subj': 'detectionEngineStepDefineRuleIndices', euiFieldProps: { - compressed: true, fullWidth: true, isDisabled: isLoading, + placeholder: '', }, }} /> + {i18n.IMPORT_TIMELINE_QUERY} + + ), + }} component={QueryBarDefineRule} componentProps={{ - compressed: true, + browserFields, loading: indexPatternLoadingQueryBar, idAria: 'detectionEngineStepDefineRuleQueryBar', indexPattern: indexPatternQueryBar, isDisabled: isLoading, isLoading: indexPatternLoadingQueryBar, dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', + openTimelineSearch, + onCloseTimelineSearch: handleCloseTimelineSearch, resizeParentContainer, }} /> - - {({ useIndicesConfig }) => { - if (localUseIndicesConfig !== useIndicesConfig) { - const indexField = form.getFields().index; - if ( - indexField != null && - useIndicesConfig === 'true' && - !isEqual(indexField.value, indicesConfig) - ) { - indexField.setValue(indicesConfig); - setIndices(indicesConfig); - } else if ( - indexField != null && - useIndicesConfig === 'false' && - isEqual(indexField.value, indicesConfig) - ) { - indexField.setValue([]); - setIndices([]); + + {({ index }) => { + if (index != null) { + if (isEqual(index, indicesConfig) && !localUseIndicesConfig) { + setLocalUseIndicesConfig(true); + } + if (!isEqual(index, indicesConfig) && localUseIndicesConfig) { + setLocalUseIndicesConfig(false); + } + if (index != null && !isEmpty(index) && !isEqual(index, mylocalIndicesConfig)) { + setMyLocalIndicesConfig(index); } - setLocalUseIndicesConfig(useIndicesConfig); } - return null; }} @@ -208,7 +223,7 @@ export const StepDefineRule = memo( > - {myStepData.isNew ? RuleI18n.CONTINUE : RuleI18n.UPDATE} + {RuleI18n.CONTINUE} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx index 9b54ada8227c61..dbd7e3b3f96aab 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx @@ -10,7 +10,6 @@ import { isEmpty } from 'lodash/fp'; import React from 'react'; import { esKuery } from '../../../../../../../../../../src/plugins/data/public'; -import * as RuleI18n from '../../translations'; import { FieldValueQueryBar } from '../query_bar'; import { ERROR_CODE, @@ -19,33 +18,27 @@ import { FormSchema, ValidationFunc, } from '../shared_imports'; -import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY } from './translations'; +import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; const { emptyField } = fieldValidators; export const schema: FormSchema = { - useIndicesConfig: { - type: FIELD_TYPES.RADIO_GROUP, + index: { + type: FIELD_TYPES.COMBO_BOX, label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldIndicesTypeLabel', + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndexPatternsLabel', { - defaultMessage: 'Indices type', + defaultMessage: 'Index patterns', } ), - }, - index: { - type: FIELD_TYPES.COMBO_BOX, - label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndicesLabel', { - defaultMessage: 'Indices', - }), - labelAppend: {RuleI18n.OPTIONAL_FIELD}, + helpText: {INDEX_HELPER_TEXT}, validations: [ { validator: emptyField( i18n.translate( 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', { - defaultMessage: 'An output indice name for signals is required.', + defaultMessage: 'Index patterns for signals is required.', } ) ), diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx index 0050c59a4a2c8b..8394f090e346cd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx @@ -33,3 +33,25 @@ export const CUSTOM_INDICES = i18n.translate( defaultMessage: 'Provide custom list of indices', } ); + +export const INDEX_HELPER_TEXT = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.indicesHelperDescription', + { + defaultMessage: + 'Enter the pattern of Elasticsearch indices where you would like this rule to run. By default, these will include index patterns defined in SIEM advanced settings.', + } +); + +export const RESET_DEFAULT_INDEX = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.resetDefaultIndicesButton', + { + defaultMessage: 'Reset to default index patterns', + } +); + +export const IMPORT_TIMELINE_QUERY = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.importTimelineQueryButton', + { + defaultMessage: 'Import query from saved timeline', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx index 6f7e49bc8ab9a9..35b8ca6650bf6d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx @@ -92,7 +92,6 @@ export const StepScheduleRule = memo( path="interval" component={ScheduleItem} componentProps={{ - compressed: true, idAria: 'detectionEngineStepScheduleRuleInterval', isDisabled: isLoading, dataTestSubj: 'detectionEngineStepScheduleRuleInterval', @@ -102,7 +101,6 @@ export const StepScheduleRule = memo( path="from" component={ScheduleItem} componentProps={{ - compressed: true, idAria: 'detectionEngineStepScheduleRuleFrom', isDisabled: isLoading, dataTestSubj: 'detectionEngineStepScheduleRuleFrom', diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts index a25ccce569dd43..12bbdbdfff3e92 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts @@ -40,14 +40,14 @@ const getTimeTypeValue = (time: string): { unit: string; value: number } => { }; const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { - const { queryBar, useIndicesConfig, isNew, ...rest } = defineStepData; + const { queryBar, isNew, ...rest } = defineStepData; const { filters, query, saved_id: savedId } = queryBar; return { ...rest, language: query.language, filters, query: query.query as string, - ...(savedId != null ? { saved_id: savedId } : {}), + ...(savedId != null && savedId !== '' ? { saved_id: savedId } : {}), }; }; @@ -72,11 +72,21 @@ const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRul }; const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { - const { falsePositives, references, riskScore, threats, isNew, ...rest } = aboutStepData; + const { + falsePositives, + references, + riskScore, + threats, + timeline, + isNew, + ...rest + } = aboutStepData; return { false_positives: falsePositives.filter(item => !isEmpty(item)), references: references.filter(item => !isEmpty(item)), risk_score: riskScore, + timeline_id: timeline.id, + timeline_title: timeline.title, threats: threats .filter(threat => threat.tactic.name !== 'none') .map(threat => ({ @@ -97,7 +107,7 @@ export const formatRule = ( scheduleData: ScheduleStepRule, ruleId?: string ): NewRule => { - const type: FormatRuleType = defineStepData.queryBar.saved_id != null ? 'saved_query' : 'query'; + const type: FormatRuleType = !isEmpty(defineStepData.queryBar.saved_id) ? 'saved_query' : 'query'; const persistData = { type, ...formatDefineStepData(defineStepData), diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index 3e8dbeba895461..848b17aadbff4d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -12,12 +12,13 @@ import styled from 'styled-components'; import { HeaderPage } from '../../../../components/header_page'; import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; import { WrapperPage } from '../../../../components/wrapper_page'; +import { usePersistRule } from '../../../../containers/detection_engine/rules'; +import { SpyRoute } from '../../../../utils/route/spy_routes'; import { AccordionTitle } from '../components/accordion_title'; +import { FormData, FormHook } from '../components/shared_imports'; import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; -import { usePersistRule } from '../../../../containers/detection_engine/rules'; -import { SpyRoute } from '../../../../utils/route/spy_routes'; import * as RuleI18n from '../translations'; import { AboutStepRule, DefineStepRule, RuleStep, RuleStepData, ScheduleStepRule } from '../types'; import { formatRule } from './helpers'; @@ -28,17 +29,43 @@ const stepsRuleOrder = [RuleStep.defineRule, RuleStep.aboutRule, RuleStep.schedu const ResizeEuiPanel = styled(EuiPanel)<{ height?: number; }>` + .euiAccordion__iconWrapper { + display: none; + } .euiAccordion__childWrapper { height: ${props => (props.height !== -1 ? `${props.height}px !important` : 'auto')}; } + .euiAccordion__button { + cursor: default !important; + &:hover { + text-decoration: none !important; + } + } +`; + +const MyEuiPanel = styled(EuiPanel)` + .euiAccordion__iconWrapper { + display: none; + } + .euiAccordion__button { + cursor: default !important; + &:hover { + text-decoration: none !important; + } + } `; export const CreateRuleComponent = React.memo(() => { const [heightAccordion, setHeightAccordion] = useState(-1); - const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); + const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); const defineRuleRef = useRef(null); const aboutRuleRef = useRef(null); const scheduleRuleRef = useRef(null); + const stepsForm = useRef | null>>({ + [RuleStep.defineRule]: null, + [RuleStep.aboutRule]: null, + [RuleStep.scheduleRule]: null, + }); const stepsData = useRef>({ [RuleStep.defineRule]: { isValid: false, data: {} }, [RuleStep.aboutRule]: { isValid: false, data: {} }, @@ -57,11 +84,17 @@ export const CreateRuleComponent = React.memo(() => { if (isValid) { const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item); if ([0, 1].includes(stepRuleIdx)) { - setIsStepRuleInEditView({ - ...isStepRuleInReadOnlyView, - [step]: true, - }); - if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) { + if (isStepRuleInReadOnlyView[stepsRuleOrder[stepRuleIdx + 1]]) { + setIsStepRuleInEditView({ + ...isStepRuleInReadOnlyView, + [step]: true, + [stepsRuleOrder[stepRuleIdx + 1]]: false, + }); + } else if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) { + setIsStepRuleInEditView({ + ...isStepRuleInReadOnlyView, + [step]: true, + }); openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]); setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); } @@ -80,9 +113,13 @@ export const CreateRuleComponent = React.memo(() => { } } }, - [openAccordionId, stepsData.current, setRule] + [isStepRuleInReadOnlyView, openAccordionId, stepsData.current, setRule] ); + const setStepsForm = useCallback((step: RuleStep, form: FormHook) => { + stepsForm.current[step] = form; + }, []); + const getAccordionType = useCallback( (accordionId: RuleStep) => { if (accordionId === openAccordionId) { @@ -135,42 +172,38 @@ export const CreateRuleComponent = React.memo(() => { (id: RuleStep, isOpen: boolean) => { const activeRuleIdx = stepsRuleOrder.findIndex(step => step === openAccordionId); const stepRuleIdx = stepsRuleOrder.findIndex(step => step === id); - const isLatestStepsRuleValid = - stepRuleIdx === 0 - ? true - : stepsRuleOrder - .filter((stepRule, index) => index < stepRuleIdx) - .every(stepRule => stepsData.current[stepRule].isValid); - if (stepRuleIdx < activeRuleIdx && !isOpen) { + if ((id === openAccordionId || stepRuleIdx < activeRuleIdx) && !isOpen) { openCloseAccordion(id); } else if (stepRuleIdx >= activeRuleIdx) { if ( - openAccordionId != null && openAccordionId !== id && !stepsData.current[openAccordionId].isValid && !isStepRuleInReadOnlyView[id] && isOpen ) { openCloseAccordion(id); - } else if (!isLatestStepsRuleValid && isOpen) { - openCloseAccordion(id); - } else if (id !== openAccordionId && isOpen) { - setOpenAccordionId(id); } } }, - [isStepRuleInReadOnlyView, openAccordionId] + [isStepRuleInReadOnlyView, openAccordionId, stepsData] ); const manageIsEditable = useCallback( - (id: RuleStep) => { - setIsStepRuleInEditView({ - ...isStepRuleInReadOnlyView, - [id]: false, - }); + async (id: RuleStep) => { + const activeForm = await stepsForm.current[openAccordionId]?.submit(); + if (activeForm != null && activeForm?.isValid) { + setOpenAccordionId(id); + openCloseAccordion(openAccordionId); + + setIsStepRuleInEditView({ + ...isStepRuleInReadOnlyView, + [openAccordionId]: openAccordionId === RuleStep.scheduleRule ? false : true, + [id]: false, + }); + } }, - [isStepRuleInReadOnlyView] + [isStepRuleInReadOnlyView, openAccordionId] ); if (isSaved) { @@ -201,7 +234,7 @@ export const CreateRuleComponent = React.memo(() => { size="xs" onClick={manageIsEditable.bind(null, RuleStep.defineRule)} > - {`Edit`} + {i18n.EDIT_RULE} ) } @@ -210,13 +243,14 @@ export const CreateRuleComponent = React.memo(() => { setHeightAccordion(height)} /> - + { size="xs" onClick={manageIsEditable.bind(null, RuleStep.aboutRule)} > - {`Edit`} + {i18n.EDIT_RULE} ) } @@ -240,12 +274,13 @@ export const CreateRuleComponent = React.memo(() => { - + - + { size="xs" onClick={manageIsEditable.bind(null, RuleStep.scheduleRule)} > - {`Edit`} + {i18n.EDIT_RULE} ) } @@ -269,10 +304,11 @@ export const CreateRuleComponent = React.memo(() => { - + diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts index 884f3f3741228a..329bcc286fb70c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts @@ -9,3 +9,7 @@ import { i18n } from '@kbn/i18n'; export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.createRule.pageTitle', { defaultMessage: 'Create new rule', }); + +export const EDIT_RULE = i18n.translate('xpack.siem.detectionEngine.createRule.editRuleButton', { + defaultMessage: 'Edit', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 1bc2bc24517e3c..4d887c7cb5b6ec 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -113,7 +113,10 @@ export const RuleDetailsComponent = memo(({ signalsIn (({ signalsIn diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 8e32f82dff0b1c..10b7f0e832f192 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -49,6 +49,7 @@ interface ScheduleStepRuleForm extends StepRuleForm { export const EditRuleComponent = memo(() => { const { ruleId } = useParams(); const [loading, rule] = useRule(ruleId); + const [initForm, setInitForm] = useState(false); const [myAboutRuleForm, setMyAboutRuleForm] = useState({ data: null, @@ -249,7 +250,7 @@ export const EditRuleComponent = memo(() => { }, []); if (isSaved || (rule != null && rule.immutable)) { - return ; + return ; } return ( @@ -257,7 +258,7 @@ export const EditRuleComponent = memo(() => { { responsive={false} > - + {i18n.CANCEL} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index 46301ae808919d..47b5c1051bcfca 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -33,7 +33,6 @@ export const getStepsData = ({ filters: rule.filters as esFilters.Filter[], saved_id: rule.saved_id ?? null, }, - useIndicesConfig: 'true', } : null; const aboutRuleData: AboutStepRule | null = @@ -45,6 +44,10 @@ export const getStepsData = ({ threats: rule.threats as IMitreEnterpriseAttack[], falsePositives: rule.false_positives, riskScore: rule.risk_score, + timeline: { + id: rule.timeline_id ?? null, + title: rule.timeline_title ?? null, + }, } : null; const scheduleRuleData: ScheduleStepRule | null = diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx index 8b4cc2a213589b..ef67f0a7d22c66 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx @@ -8,6 +8,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../../components/link_to/redirect_to_detection_engine'; import { FormattedRelativePreferenceDate } from '../../../components/formatted_date'; import { getEmptyTagValue } from '../../../components/empty_value'; import { HeaderPage } from '../../../components/header_page'; @@ -32,7 +33,10 @@ export const RulesComponent = React.memo(() => { /> { - + {i18n.ADD_NEW_RULE} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index 9b535034810bd5..ec4206623bad90 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -8,6 +8,7 @@ import { esFilters } from '../../../../../../../../src/plugins/data/common'; import { Rule } from '../../../containers/detection_engine/rules'; import { FieldValueQueryBar } from './components/query_bar'; import { FormData, FormHook } from './components/shared_imports'; +import { FieldValueTimeline } from './components/pick_timeline'; export interface EuiBasicTableSortTypes { field: string; @@ -76,11 +77,11 @@ export interface AboutStepRule extends StepRuleData { references: string[]; falsePositives: string[]; tags: string[]; + timeline: FieldValueTimeline; threats: IMitreEnterpriseAttack[]; } export interface DefineStepRule extends StepRuleData { - useIndicesConfig: string; index: string[]; queryBar: FieldValueQueryBar; } @@ -108,6 +109,8 @@ export interface AboutStepRuleJson { references: string[]; false_positives: string[]; tags: string[]; + timeline_id: string | null; + timeline_title: string | null; threats: IMitreEnterpriseAttack[]; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index edf196b96f5d02..f6ac0435cd7c1b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -52,6 +52,7 @@ export const fullRuleAlertParamsRest = (): RuleAlertParamsRest => ({ created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', timeline_id: 'timeline-id', + timeline_title: 'timeline-title', }); export const typicalPayload = (): Partial => ({ @@ -271,6 +272,7 @@ export const getResult = (): RuleAlertType => ({ outputIndex: '.siem-signals', savedId: 'some-id', timelineId: 'some-timeline-id', + timelineTitle: 'some-timeline-title', meta: { someMeta: 'someField' }, filters: [ { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json index afe9bac9d87fec..79fb136afd52ae 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/signals_mapping.json @@ -36,6 +36,9 @@ "timeline_id": { "type": "keyword" }, + "timeline_title": { + "type": "keyword" + }, "max_signals": { "type": "keyword" }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 256b341fca656a..3d9719a7b248b2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -74,6 +74,7 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou updated_at: updatedAt, references, timeline_id: timelineId, + timeline_title: timelineTitle, version, } = payloadRule; const ruleIdOrUuid = ruleId ?? uuid.v4(); @@ -112,6 +113,7 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou outputIndex: finalIndex, savedId, timelineId, + timelineTitle, meta, filters, ruleId: ruleIdOrUuid, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 476d5b8a49ba2d..cf8fb2a28288f0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -44,6 +44,7 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, + timeline_title: timelineTitle, meta, filters, rule_id: ruleId, @@ -101,6 +102,7 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = outputIndex: finalIndex, savedId, timelineId, + timelineTitle, meta, filters, ruleId: ruleId != null ? ruleId : uuid.v4(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index b30b6c791522b0..180a75bdaaeead 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -50,6 +50,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, + timeline_title: timelineTitle, meta, filters, rule_id: ruleId, @@ -82,6 +83,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou outputIndex, savedId, timelineId, + timelineTitle, meta, filters, id, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index ec3d9514fa5db8..6db8a8902915aa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -38,6 +38,7 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, + timeline_title: timelineTitle, meta, filters, rule_id: ruleId, @@ -77,6 +78,7 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { outputIndex, savedId, timelineId, + timelineTitle, meta, filters, id, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index b1f61d11458fe3..44d47ad435682c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -79,6 +79,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -141,6 +142,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -205,6 +207,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -269,6 +272,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -331,6 +335,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -396,6 +401,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -461,6 +467,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -526,6 +533,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', to: 'now', type: 'query', version: 1, @@ -642,6 +650,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', version: 1, }; expect(output).toEqual({ @@ -714,6 +723,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', version: 1, }; expect(output).toEqual(expected); @@ -875,6 +885,7 @@ describe('utils', () => { }, saved_id: 'some-id', timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', version: 1, }; expect(output).toEqual(expected); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index b9bf3f8a942fcb..714035a4234132 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -85,6 +85,7 @@ export const transformAlertToRule = (alert: RuleAlertType): Partial { test('You can omit the query string when filters are present', () => { expect( - addPrepackagedRulesSchema.validate< - Partial & { meta: string }> - >({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1099,7 +1097,7 @@ describe('add prepackaged rules schema', () => { ).toBeFalsy(); }); - test('validates with timeline_id', () => { + test('validates with timeline_id and timeline_title', () => { expect( addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', @@ -1117,7 +1115,131 @@ describe('add prepackaged rules schema', () => { language: 'kuery', version: 1, timeline_id: 'timeline-id', + timeline_title: 'timeline-title', }).error ).toBeFalsy(); }); + + test('You cannot omit timeline_title when timeline_id is present', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + version: 1, + timeline_id: 'timeline-id', + }).error + ).toBeTruthy(); + }); + + test('You cannot have a null value for timeline_title when timeline_id is present', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + version: 1, + timeline_id: 'timeline-id', + timeline_title: null, + }).error + ).toBeTruthy(); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + version: 1, + timeline_id: 'timeline-id', + timeline_title: '', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + version: 1, + timeline_id: '', + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title without timeline_id', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + immutable: true, + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + language: 'kuery', + filters: [], + max_signals: 1, + version: 1, + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts index c993b05cb5f299..49907b4a975e6d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts @@ -21,6 +21,7 @@ import { language, saved_id, timeline_id, + timeline_title, meta, risk_score, max_signals, @@ -63,6 +64,7 @@ export const addPrepackagedRulesSchema = Joi.object({ otherwise: Joi.forbidden(), }), timeline_id, + timeline_title, meta, risk_score: risk_score.required(), max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index 8dc00b66e97a3e..87916bea606493 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -1024,7 +1024,7 @@ describe('create rules schema', () => { test('You can omit the query string when filters are present', () => { expect( - createRulesSchema.validate & { meta: string }>>({ + createRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1045,7 +1045,7 @@ describe('create rules schema', () => { ).toBeFalsy(); }); - test('timeline_id validates', () => { + test('validates with timeline_id and timeline_title', () => { expect( createRulesSchema.validate>({ rule_id: 'rule-1', @@ -1062,8 +1062,122 @@ describe('create rules schema', () => { references: ['index-1'], query: 'some query', language: 'kuery', - timeline_id: 'some_id', + timeline_id: 'timeline-id', + timeline_title: 'timeline-title', }).error ).toBeFalsy(); }); + + test('You cannot omit timeline_title when timeline_id is present', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', + }).error + ).toBeTruthy(); + }); + + test('You cannot have a null value for timeline_title when timeline_id is present', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', + timeline_title: null, + }).error + ).toBeTruthy(); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: 'some_id', + timeline_title: '', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_id: '', + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title without timeline_id', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + output_index: '.siem-signals', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts index 614451312d04da..df5c1694d6c782 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts @@ -22,6 +22,7 @@ import { output_index, saved_id, timeline_id, + timeline_title, meta, risk_score, max_signals, @@ -57,6 +58,7 @@ export const createRulesSchema = Joi.object({ otherwise: Joi.forbidden(), }), timeline_id, + timeline_title, meta, risk_score: risk_score.required(), max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts index 68d3166c74d6da..c8331bb7820dd1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts @@ -24,6 +24,11 @@ export const language = Joi.string().valid('kuery', 'lucene'); export const output_index = Joi.string(); export const saved_id = Joi.string(); export const timeline_id = Joi.string(); +export const timeline_title = Joi.string().when('timeline_id', { + is: Joi.exist(), + then: Joi.required(), + otherwise: Joi.forbidden(), +}); export const meta = Joi.object(); export const max_signals = Joi.number().greater(0); export const name = Joi.string(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts index 1f00e0a13866a4..f713840ab43f93 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -867,7 +867,7 @@ describe('update rules schema', () => { ).toBeTruthy(); }); - test('timeline_id validates', () => { + test('validates with timeline_id and timeline_title', () => { expect( updateRulesSchema.validate>({ id: 'rule-1', @@ -881,7 +881,101 @@ describe('update rules schema', () => { type: 'saved_query', saved_id: 'some id', timeline_id: 'some-id', + timeline_title: 'some-title', }).error ).toBeFalsy(); }); + + test('You cannot omit timeline_title when timeline_id is present', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'some-id', + }).error + ).toBeTruthy(); + }); + + test('You cannot have a null value for timeline_title when timeline_id is present', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'timeline-id', + timeline_title: null, + }).error + ).toBeTruthy(); + }); + + test('You cannot have empty string for timeline_title when timeline_id is present', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: 'some-id', + timeline_title: '', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title with an empty timeline_id', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_id: '', + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); + + test('You cannot have timeline_title without timeline_id', () => { + expect( + updateRulesSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + timeline_title: 'some-title', + }).error + ).toBeTruthy(); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts index afd8a5fce48331..9c3188738faea7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts @@ -22,6 +22,7 @@ import { output_index, saved_id, timeline_id, + timeline_title, meta, risk_score, max_signals, @@ -53,6 +54,7 @@ export const updateRulesSchema = Joi.object({ output_index, saved_id, timeline_id, + timeline_title, meta, risk_score, max_signals, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index 07cf0b0c716cc0..d2f76907d7aa3e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -19,6 +19,7 @@ export const createRules = async ({ language, savedId, timelineId, + timelineTitle, meta, filters, ruleId, @@ -56,6 +57,7 @@ export const createRules = async ({ outputIndex, savedId, timelineId, + timelineTitle, meta, filters, maxSignals, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index 9acfbf8c43221c..9c3be64f71a0dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -26,6 +26,7 @@ export const installPrepackagedRules = async ( language, saved_id: savedId, timeline_id: timelineId, + timeline_title: timelineTitle, meta, filters, rule_id: ruleId, @@ -55,6 +56,7 @@ export const installPrepackagedRules = async ( outputIndex, savedId, timelineId, + timelineTitle, meta, filters, ruleId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index c9dac82b6eb8f7..0fe4b15437af85 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -74,6 +74,7 @@ export const updateRules = async ({ outputIndex, savedId, timelineId, + timelineTitle, meta, filters, from, @@ -118,6 +119,7 @@ export const updateRules = async ({ outputIndex, savedId, timelineId, + timelineTitle, meta, filters, index, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_timelineid.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_timelineid.json index 2f995029447ffe..eb87a14e0c6885 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_timelineid.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_timelineid.json @@ -7,5 +7,6 @@ "from": "now-6m", "to": "now", "query": "user.name: root or user.name: admin", - "timeline_id": "timeline-id" + "timeline_id": "timeline-id", + "timeline_title": "timeline_title" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json index 60095a0a6a8334..46a2feeefd49e3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_everything.json @@ -78,5 +78,6 @@ "Some plain text string here explaining why this is a valid thing to look out for" ], "timeline_id": "timeline_id", + "timeline_title": "timeline_title", "version": 1 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json index 2628b69eb064d7..16d5d6cc2b36a2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/saved_queries/saved_query_with_everything.json @@ -78,5 +78,6 @@ "Some plain text string here explaining why this is a valid thing to look out for" ], "saved_id": "test-saved-id", - "timeline_id": "test-timeline-id" + "timeline_id": "test-timeline-id", + "timeline_title": "test-timeline-title" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json index 4da285e5b09bf9..7fc8de9fe8f9ec 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_query_everything.json @@ -78,5 +78,6 @@ "Some plain text string here explaining why this is a valid thing to look out for" ], "timeline_id": "other-timeline-id", + "timeline_title": "other-timeline-title", "version": 42 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json index 8cfa3303f54a62..27dee7dd81463c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_timelineid.json @@ -1,4 +1,5 @@ { "rule_id": "query-rule-id", - "timeline_id": "other-timeline-id" + "timeline_id": "other-timeline-id", + "timeline_title": "other-timeline-title" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 5e50b65b517172..ede82a597b238e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -31,6 +31,7 @@ export const sampleRuleAlertParams = ( filters: undefined, savedId: undefined, timelineId: undefined, + timelineTitle: undefined, meta: undefined, threats: undefined, version: 1, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts index 0a3526d32e511a..1093ff3a8a462d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts @@ -34,6 +34,7 @@ export const buildRule = ({ false_positives: ruleParams.falsePositives, saved_id: ruleParams.savedId, timeline_id: ruleParams.timelineId, + timeline_title: ruleParams.timelineTitle, meta: ruleParams.meta, max_signals: ruleParams.maxSignals, risk_score: ruleParams.riskScore, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 87d31abbc5371a..ab2c1733b04ca2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -42,6 +42,7 @@ export const signalRulesAlertType = ({ outputIndex: schema.nullable(schema.string()), savedId: schema.nullable(schema.string()), timelineId: schema.nullable(schema.string()), + timelineTitle: schema.nullable(schema.string()), meta: schema.nullable(schema.object({}, { allowUnknowns: true })), query: schema.nullable(schema.string()), filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index f4a8263da6ba42..ff0f2a8782cd21 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -44,6 +44,7 @@ export interface RuleAlertParams { tags: string[]; to: string; timelineId: string | undefined | null; + timelineTitle: string | undefined | null; threats: ThreatParams[] | undefined | null; type: 'query' | 'saved_query'; version: number; @@ -60,6 +61,7 @@ export type RuleAlertParamsRest = Omit< | 'savedId' | 'riskScore' | 'timelineId' + | 'timelineTitle' | 'outputIndex' | 'updatedAt' | 'createdAt' @@ -68,6 +70,7 @@ export type RuleAlertParamsRest = Omit< false_positives: RuleAlertParams['falsePositives']; saved_id: RuleAlertParams['savedId']; timeline_id: RuleAlertParams['timelineId']; + timeline_title: RuleAlertParams['timelineTitle']; max_signals: RuleAlertParams['maxSignals']; risk_score: RuleAlertParams['riskScore']; output_index: RuleAlertParams['outputIndex'];