Skip to content

Commit

Permalink
[SECURITY SOLEIL] Fix selection of event type when no siem index sign…
Browse files Browse the repository at this point in the history
…al created (#68291)

* fix selection of event type when no siem index signal created

* including the term signal for the old timeline

* fix import path

* Add a specific msg in the inspect modal if we do not have the alert index created

* fix import if eventType is siganl to match it to alert

* forget to update test

* fix signal view

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
XavierM and elasticmachine committed Jun 5, 2020
1 parent eaca7ee commit e3d88a4
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 8 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}}';
Expand Down Expand Up @@ -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(<i>{'No alert index found'}</i>);
});

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.');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down Expand Up @@ -88,6 +89,15 @@ const manageStringify = (object: Record<string, unknown> | Response): string =>
}
};

export const formatIndexPatternRequested = (indices: string[] = []) => {
if (indices.length === 1 && indices[0] === NO_ALERT_INDEX) {
return <i>{i18n.NO_ALERT_INDEX_FOUND}</i>;
}
return indices.length > 0
? indices.filter((i) => i !== NO_ALERT_INDEX).join(', ')
: i18n.SOMETHING_WENT_WRONG;
};

export const ModalInspectQuery = ({
closeModal,
isShowing = false,
Expand All @@ -113,7 +123,7 @@ export const ModalInspectQuery = ({
),
description: (
<span data-test-subj="index-pattern-description">
{inspectRequest != null ? inspectRequest.index.join(', ') : i18n.SOMETHING_WENT_WRONG}
{formatIndexPatternRequested(inspectRequest?.index ?? [])}
</span>
),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
);
Original file line number Diff line number Diff line change
@@ -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<ReturnSignalIndex>;
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(
<TestProviders>
<MockedProvider mocks={mocks} addTypename={false}>
<StatefulTimeline {...props} />
</MockedProvider>
</TestProviders>
);
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(
<TestProviders>
<MockedProvider mocks={mocks} addTypename={false}>
<StatefulTimeline {...props} />
</MockedProvider>
</TestProviders>
);
await act(async () => {
await wait();
wrapper.update();
const timeline = wrapper.find(Timeline);
expect(timeline.props().indexToAdd).toEqual(['mock-siem-signals-index']);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,7 +31,7 @@ export interface OwnProps {
usersViewing: string[];
}

type Props = OwnProps & PropsFromRedux;
export type Props = OwnProps & PropsFromRedux;

const StatefulTimelineComponent = React.memo<Props>(
({
Expand Down Expand Up @@ -67,11 +68,11 @@ const StatefulTimelineComponent = React.memo<Props>(
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const PickEventTypeComponents: React.FC<PickEventTypeProps> = ({
<EuiSuperSelect
data-test-subj="pick-event-type"
fullWidth={false}
valueOfSelected={eventType}
valueOfSelected={eventType === 'signal' ? 'alert' : eventType}
onChange={onChangeEventType}
options={eventTypeOptions}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down

0 comments on commit e3d88a4

Please sign in to comment.