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';