diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 482794804685d..d04d1f2c91b97 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -31,6 +31,7 @@ export const DEFAULT_INTERVAL_PAUSE = true; export const DEFAULT_INTERVAL_TYPE = 'manual'; export const DEFAULT_INTERVAL_VALUE = 300000; // ms export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; +export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51'; /** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */ export const DEFAULT_INDEX_PATTERN = [ diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx index 153a1703059c2..3451ddacb6538 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx @@ -9,7 +9,8 @@ import { mount } from 'enzyme'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { ModalInspectQuery } from './modal'; +import { NO_ALERT_INDEX } from '../../../../common/constants'; +import { ModalInspectQuery, formatIndexPatternRequested } from './modal'; const request = '{"index": ["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"allowNoIndices": true, "ignoreUnavailable": true, "body": { "aggregations": {"hosts": {"cardinality": {"field": "host.name" } }, "hosts_histogram": {"auto_date_histogram": {"field": "@timestamp","buckets": "6"},"aggs": { "count": {"cardinality": {"field": "host.name" }}}}}, "query": {"bool": {"filter": [{"range": { "@timestamp": {"gte": 1562290224506,"lte": 1562376624506 }}}]}}, "size": 0, "track_total_hits": false}}'; @@ -244,4 +245,31 @@ describe('Modal Inspect', () => { expect(closeModal).toHaveBeenCalled(); }); }); + + describe('formatIndexPatternRequested', () => { + test('Return specific messages to NO_ALERT_INDEX if we only have one index and we match the index name `NO_ALERT_INDEX`', () => { + const expected = formatIndexPatternRequested([NO_ALERT_INDEX]); + expect(expected).toEqual({'No alert index found'}); + }); + + test('Ignore NO_ALERT_INDEX if you have more than one indices', () => { + const expected = formatIndexPatternRequested([NO_ALERT_INDEX, 'indice-1']); + expect(expected).toEqual('indice-1'); + }); + + test('Happy path', () => { + const expected = formatIndexPatternRequested(['indice-1, indice-2']); + expect(expected).toEqual('indice-1, indice-2'); + }); + + test('Empty array with no indices', () => { + const expected = formatIndexPatternRequested([]); + expect(expected).toEqual('Sorry about that, something went wrong.'); + }); + + test('Undefined indices', () => { + const expected = formatIndexPatternRequested(undefined); + expect(expected).toEqual('Sorry about that, something went wrong.'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx index 1563c005af5b6..e9f7edf86d4ba 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx @@ -22,6 +22,7 @@ import numeral from '@elastic/numeral'; import React, { ReactNode } from 'react'; import styled from 'styled-components'; +import { NO_ALERT_INDEX } from '../../../../common/constants'; import * as i18n from './translations'; const DescriptionListStyled = styled(EuiDescriptionList)` @@ -88,6 +89,15 @@ const manageStringify = (object: Record | Response): string => } }; +export const formatIndexPatternRequested = (indices: string[] = []) => { + if (indices.length === 1 && indices[0] === NO_ALERT_INDEX) { + return {i18n.NO_ALERT_INDEX_FOUND}; + } + return indices.length > 0 + ? indices.filter((i) => i !== NO_ALERT_INDEX).join(', ') + : i18n.SOMETHING_WENT_WRONG; +}; + export const ModalInspectQuery = ({ closeModal, isShowing = false, @@ -113,7 +123,7 @@ export const ModalInspectQuery = ({ ), description: ( - {inspectRequest != null ? inspectRequest.index.join(', ') : i18n.SOMETHING_WENT_WRONG} + {formatIndexPatternRequested(inspectRequest?.index ?? [])} ), }, diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/translations.ts b/x-pack/plugins/security_solution/public/common/components/inspect/translations.ts index c51423087911f..4a8da8050dd9a 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/inspect/translations.ts @@ -60,3 +60,10 @@ export const REQUEST_TIMESTAMP_DESC = i18n.translate( defaultMessage: 'Time when the start of the request has been logged', } ); + +export const NO_ALERT_INDEX_FOUND = i18n.translate( + 'xpack.securitySolution.inspect.modal.noAlertIndexFound', + { + defaultMessage: 'No alert index found', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx new file mode 100644 index 0000000000000..581fa125d21e2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -0,0 +1,133 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { act } from 'react-dom/test-utils'; +import useResizeObserver from 'use-resize-observer/polyfilled'; + +import { + useSignalIndex, + ReturnSignalIndex, +} from '../../../alerts/containers/detection_engine/alerts/use_signal_index'; +import { mocksSource } from '../../../common/containers/source/mock'; +import { wait } from '../../../common/lib/helpers'; +import { defaultHeaders, mockTimelineData, TestProviders } from '../../../common/mock'; +import { Direction } from '../../../graphql/types'; +import { timelineQuery } from '../../containers/index.gql_query'; +import { timelineActions } from '../../store/timeline'; + +import { Sort } from './body/sort'; +import { mockDataProviders } from './data_providers/mock/mock_data_providers'; +import { StatefulTimeline, Props as StatefulTimelineProps } from './index'; +import { Timeline } from './timeline'; + +jest.mock('../../../common/lib/kibana'); + +const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; +jest.mock('use-resize-observer/polyfilled'); +mockUseResizeObserver.mockImplementation(() => ({})); + +const mockUseSignalIndex: jest.Mock = useSignalIndex as jest.Mock; +jest.mock('../../../alerts/containers/detection_engine/alerts/use_signal_index'); + +describe('StatefulTimeline', () => { + let props = {} as StatefulTimelineProps; + const sort: Sort = { + columnId: '@timestamp', + sortDirection: Direction.desc, + }; + const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); + const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); + + const mocks = [ + { request: { query: timelineQuery }, result: { data: { events: mockTimelineData } } }, + ...mocksSource, + ]; + + beforeEach(() => { + props = { + addProvider: timelineActions.addProvider, + columns: defaultHeaders, + createTimeline: timelineActions.createTimeline, + dataProviders: mockDataProviders, + eventType: 'raw', + end: endDate, + filters: [], + id: 'foo', + isLive: false, + itemsPerPage: 5, + itemsPerPageOptions: [5, 10, 20], + kqlMode: 'search', + kqlQueryExpression: '', + onClose: jest.fn(), + onDataProviderEdited: timelineActions.dataProviderEdited, + removeColumn: timelineActions.removeColumn, + removeProvider: timelineActions.removeProvider, + show: true, + showCallOutUnauthorizedMsg: false, + sort, + start: startDate, + updateColumns: timelineActions.updateColumns, + updateDataProviderEnabled: timelineActions.updateDataProviderEnabled, + updateDataProviderExcluded: timelineActions.updateDataProviderExcluded, + updateDataProviderKqlQuery: timelineActions.updateDataProviderKqlQuery, + updateHighlightedDropAndProviderId: timelineActions.updateHighlightedDropAndProviderId, + updateItemsPerPage: timelineActions.updateItemsPerPage, + updateItemsPerPageOptions: timelineActions.updateItemsPerPageOptions, + updateSort: timelineActions.updateSort, + upsertColumn: timelineActions.upsertColumn, + usersViewing: ['elastic'], + }; + }); + + describe('indexToAdd', () => { + test('Make sure that indexToAdd return an unknown index if signalIndex does not exist', async () => { + mockUseSignalIndex.mockImplementation(() => ({ + loading: false, + signalIndexExists: false, + signalIndexName: undefined, + })); + const wrapper = mount( + + + + + + ); + await act(async () => { + await wait(); + wrapper.update(); + const timeline = wrapper.find(Timeline); + expect(timeline.props().indexToAdd).toEqual([ + 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51', + ]); + }); + }); + + test('Make sure that indexToAdd return siem signal index if signalIndex exist', async () => { + mockUseSignalIndex.mockImplementation(() => ({ + loading: false, + signalIndexExists: true, + signalIndexName: 'mock-siem-signals-index', + })); + const wrapper = mount( + + + + + + ); + await act(async () => { + await wait(); + wrapper.update(); + const timeline = wrapper.find(Timeline); + expect(timeline.props().indexToAdd).toEqual(['mock-siem-signals-index']); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index c52be64f94bf1..42fd6422d3a38 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -8,6 +8,7 @@ import React, { useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; +import { NO_ALERT_INDEX } from '../../../../common/constants'; import { WithSource } from '../../../common/containers/source'; import { useSignalIndex } from '../../../alerts/containers/detection_engine/alerts/use_signal_index'; import { inputsModel, inputsSelectors, State } from '../../../common/store'; @@ -30,7 +31,7 @@ export interface OwnProps { usersViewing: string[]; } -type Props = OwnProps & PropsFromRedux; +export type Props = OwnProps & PropsFromRedux; const StatefulTimelineComponent = React.memo( ({ @@ -67,11 +68,11 @@ const StatefulTimelineComponent = React.memo( eventType && signalIndexExists && signalIndexName != null && - ['signal', 'all'].includes(eventType) + ['signal', 'alert', 'all'].includes(eventType) ) { return [signalIndexName]; } - return []; + return [NO_ALERT_INDEX]; // Following index does not exist so we won't show any events; }, [eventType, signalIndexExists, signalIndexName]); const onDataProviderRemoved: OnDataProviderRemoved = useCallback( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx index 5a3805af0ca43..b0682290ee849 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx @@ -79,7 +79,7 @@ const PickEventTypeComponents: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 5efcb84539123..7363a60974275 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -92,7 +92,7 @@ class TimelineQueryComponent extends QueryTemplate< indexPattern == null || (indexPattern != null && indexPattern.title === '') ? [ ...(['all', 'raw'].includes(eventType) ? defaultKibanaIndex : []), - ...(['all', 'signal'].includes(eventType) ? indexToAdd : []), + ...(['all', 'alert', 'signal'].includes(eventType) ? indexToAdd : []), ] : indexPattern?.title.split(',') ?? []; const variables: GetTimelineQuery.Variables = { diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index f7e848e8a9e1b..caad70226365a 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -18,7 +18,7 @@ import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/t export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages export type KqlMode = 'filter' | 'search'; -export type EventType = 'all' | 'raw' | 'alert'; +export type EventType = 'all' | 'raw' | 'alert' | 'signal'; export type ColumnHeaderType = 'not-filtered' | 'text-filter';